基于
integrated-medical-agent-dify-card-solution.md第五章(数据库设计)和第十八章(工程模块设计)的实现成果。
本次实现分三个阶段完成,共涉及 43 个文件(新建 39 + 修改 4)。
| 阶段 | 内容 | 模块 | 文件数 |
|---|---|---|---|
| Phase 1 | AgentEngine 引擎框架 + DO + Mapper + Service 接口 | mcp-api / system-api | 33 |
| Phase 2 | OpenPlatform 对话 + 会话管理 API | openplatform | 10(新建) + 2(修改) |
| Phase 3 | Admin 端引擎配置 CRUD 接口 | system-api / emoon-system | 4(新建) + 2(修改) |
位于 emoon-infra/emoon-modules-api/emoon-mcp-api/src/main/java/com/emoon/mcp/engine/
| 文件 | 说明 |
|---|---|
AgentEngine.java |
引擎核心接口,定义 chat()(同步)、chatStream()(流式)、getEngineType()(路由标识) |
AgentRequest.java |
统一请求对象,包含 agentId、query、conversationId、engineConfig、inputs、files |
AgentResponse.java |
统一响应对象,包含 reply、cardKey、cardData、conversationId、messageId、usage、finished |
AgentEngineFactory.java |
引擎工厂,Spring 启动时自动收集所有 AgentEngine Bean,按 getEngineType() 路由 |
路由机制:
AgentEngineFactory.getEngine("dify") -> DifyAgentEngine(待实现)
AgentEngineFactory.getEngine("mock") -> MockAgentEngine(已有骨架)
AgentEngineFactory.getEngine("direct") -> DirectLLMAgentEngine(待实现)
位于 emoon-infra/emoon-modules-api/emoon-system-api/src/main/java/com/emoon/system/domain/
| DO 类 | 对应表 | 关键字段 | 说明 |
|---|---|---|---|
AiAgentEngineConfig |
ai_agent_engine_config | id, configName, engineType, configJson, status | 引擎连接配置(baseUrl/apiKey 等存在 configJson 中) |
AiAgentApp |
ai_agent_app | id, agentId, agentName, agentType, engineConfigId | 智能体元数据 |
AiUsageLog |
ai_usage_log | agentId, engineType, promptTokens, totalTokens | 调用日志 |
AiCardCategory |
ai_card_category | categoryKey, name, parentId, sortOrder | 卡片分类(树形) |
AiCardDefinition |
ai_card_definition | cardKey, version, schemaJson, uiConfigJson | 卡片 UI 模板 |
AiCardPlugin |
ai_card_plugin | pluginId, name, version, manifestJson, auditStatus | 第三方卡片插件 |
位于 emoon-infra/emoon-modules-api/emoon-mcp-api/src/main/java/com/emoon/mcp/domain/
| DO 类 | 对应表 | 关键字段 | 说明 |
|---|---|---|---|
AiConversation |
ai_conversation | conversationId, agentId(Long), engineType, status | 会话记录 |
AiCardInstance |
ai_card_instance | instanceId, cardKey, cardVersion, stateJson | 卡片实例 |
所有 DO 的共同特征:
TenantEntity(含 tenantId)-> BaseEntity(含 createBy/updateBy/createTime/updateTime)delFlag、status 字段类型为 String("0"/"1"),不是 intjava.util.DateagentId 在 AiConversation 中是 Long 类型(FK 到 ai_agent_app.id),不是 String 业务 ID| Mapper | 泛型 | 模块 |
|---|---|---|
AiAgentEngineConfigMapper |
BaseMapperPlus<AiAgentEngineConfig, AiAgentEngineConfigVo> |
system-api |
AiAgentAppMapper |
BaseMapperPlus<AiAgentApp, AiAgentApp> |
system-api |
AiUsageLogMapper |
BaseMapperPlus<AiUsageLog, AiUsageLog> |
system-api |
AiCardCategoryMapper |
BaseMapperPlus<AiCardCategory, AiCardCategory> |
system-api |
AiCardDefinitionMapper |
BaseMapperPlus<AiCardDefinition, AiCardDefinition> |
system-api |
AiCardActionLogMapper |
BaseMapperPlus<AiCardActionLog, AiCardActionLog> |
system-api |
AiCardPluginMapper |
BaseMapperPlus<AiCardPlugin, AiCardPlugin> |
system-api |
AiCardGrayConfigMapper |
BaseMapperPlus<AiCardGrayConfig, AiCardGrayConfig> |
system-api |
AiConversationMapper |
BaseMapperPlus<AiConversation, AiConversation> |
mcp-api |
AiCardInstanceMapper |
BaseMapperPlus<AiCardInstance, AiCardInstance> |
mcp-api |
位于 emoon-infra/emoon-modules-api/emoon-system-api/src/main/java/com/emoon/system/service/
每个 Service 接口提供标准的 queryById、queryList、insert、update、deleteWithValidByIds 方法。
位于 emoon-openplatform/src/main/java/com/emoon/openplatform/
| 路径 | 说明 |
|---|---|
domain/dto/request/AgentChatRequest.java |
对话请求 DTO |
domain/dto/request/ConversationCreateRequest.java |
创建会话请求 DTO |
domain/dto/resp/AgentChatResponse.java |
对话响应 DTO(同步 + SSE 共用) |
domain/dto/resp/ConversationVo.java |
会话视图对象 |
util/SignUtil.java |
签名验证工具(MD5,5 分钟有效期) |
service/IConversationService.java |
会话管理 Service 接口 |
service/IAgentChatService.java |
对话 Service 接口 |
service/impl/ConversationServiceImpl.java |
会话管理实现 |
service/impl/AgentChatServiceImpl.java |
对话核心编排实现 |
controller/v1/AgentController.java |
REST 控制器(4 个接口) |
修改的文件:
| 路径 | 变更 |
|------|------|
| pom.xml | 新增 emoon-mcp-api、emoon-system-api 依赖 |
| enums/SystemEnum.java | 新增 4 个错误码(AGENT_NOT_FOUND/AGENT_DISABLED/ENGINE_CONFIG_NOT_FOUND/CONVERSATION_NOT_FOUND) |
所有 OpenPlatform API 使用请求头签名认证:
| Header | 说明 |
|---|---|
X-Emoon-Timestamp |
毫秒时间戳 |
X-Emoon-Request-Id |
请求唯一标识(UUID) |
X-Emoon-Public-Key |
项目公钥(查 sys_project 表) |
X-Emoon-Sign |
签名值 |
签名算法:
sign = MD5(requestId + "&" + timestamp + "&" + payload + "&" + privateKey).toLowerCase()
payload:POST 请求为 JSON body 字符串,GET 请求为空字符串支持同步和 SSE 流式两种模式,由 stream 字段控制。
请求体:
{
"agentId": "agent_123456",
"query": "帮我挂个内科的号",
"conversationId": null,
"stream": false,
"userId": 10001,
"inputs": {},
"files": []
}
同步响应(stream=false):
{
"code": 200,
"msg": "操作成功",
"data": {
"reply": "好的,我来帮您挂内科的号。请问您想挂哪个医生?",
"cardKey": null,
"cardData": null,
"conversationId": "conv_a1b2c3d4-xxxx",
"messageId": "msg_x1y2z3",
"usage": {
"promptTokens": 120,
"completionTokens": 45,
"totalTokens": 165
},
"finished": true
}
}
SSE 流式响应(stream=true):
返回 text/event-stream,每个事件的 data 字段是一个 AgentChatResponse JSON:
data: {"reply":"好的","conversationId":"conv_xxx","messageId":"msg_xxx","finished":false}
data: {"reply":",我来帮您","conversationId":"conv_xxx","messageId":"msg_xxx","finished":false}
data: {"reply":"挂内科的号。","conversationId":"conv_xxx","messageId":"msg_xxx","usage":{"promptTokens":120,"completionTokens":45,"totalTokens":165},"finished":true}
请求体:
{
"agentId": "agent_123456",
"userId": 10001,
"conversationName": "挂号咨询"
}
响应:
{
"code": 200,
"msg": "操作成功",
"data": {
"conversationId": "conv_a1b2c3d4-xxxx",
"agentId": 1,
"conversationName": "挂号咨询",
"status": "active",
"messageCount": 0,
"totalTokens": 0,
"lastMessageTime": null,
"createTime": "2026-03-26T12:00:00.000+00:00"
}
}
请求参数(Query String): | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | agentId | String | 否 | 按智能体筛选 | | userId | Long | 否 | 按用户筛选 |
响应:
{
"code": 200,
"msg": "操作成功",
"data": [
{
"conversationId": "conv_a1b2c3d4-xxxx",
"agentId": 1,
"conversationName": "挂号咨询",
"status": "active",
"messageCount": 5,
"totalTokens": 820,
"lastMessageTime": "2026-03-26T12:05:00.000+00:00",
"createTime": "2026-03-26T12:00:00.000+00:00"
}
]
}
路径参数: | 参数 | 说明 | |------|------| | conversationId | 会话 UUID |
响应:与列表中单个元素结构相同。
新建文件:
| 路径 | 说明 |
|---|---|
system-api/.../domain/bo/AiAgentEngineConfigBo.java |
业务对象,@AutoMapper + 入参校验 |
system-api/.../domain/vo/AiAgentEngineConfigVo.java |
视图对象,@AutoMapper 自动转换 |
system-api/.../service/impl/AiAgentEngineConfigServiceImpl.java |
Service 实现(分页/增/改/删/唯一校验) |
emoon-system/.../controller/system/AiAgentEngineConfigController.java |
REST 控制器 |
修改文件:
| 路径 | 变更 |
|---|---|
system-api/.../service/IAiAgentEngineConfigService.java |
重构为 Bo/Vo 模式,增加分页查询 |
system-api/.../mapper/AiAgentEngineConfigMapper.java |
第二泛型参数改为 AiAgentEngineConfigVo |
基础路径:/system/ai/engineConfig
所有接口需要 Sa-Token 权限认证(管理后台登录态)。
请求参数(Query String):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| configName | String | 否 | 模糊匹配 |
| engineType | String | 否 | 精确匹配:dify / direct / spring_ai / mock |
| status | String | 否 | 精确匹配:0=启用 1=停用 |
| projectId | Integer | 否 | 精确匹配 |
| pageNum | Integer | 是 | 页码 |
| pageSize | Integer | 是 | 每页大小 |
权限:ai:engineConfig:list
cURL 示例:
curl -X GET 'http://localhost:8080/system/ai/engineConfig/list?pageNum=1&pageSize=10&engineType=dify' \
-H 'Authorization: Bearer <token>'
响应:
{
"code": 200,
"msg": "操作成功",
"rows": [
{
"id": 1,
"projectId": 1,
"configName": "导诊助手-Dify配置",
"engineType": "dify",
"configJson": "{\"baseUrl\":\"http://8.136.61.90/v1\",\"secretKey\":\"app-abc123xyz\"}",
"status": "0",
"createTime": "2026-03-26T12:00:00.000+00:00",
"updateTime": "2026-03-26T12:00:00.000+00:00"
}
],
"total": 1
}
用于下拉选择框。参数同 list 但不需要 pageNum/pageSize。
权限:ai:engineConfig:list
cURL 示例:
curl -X GET 'http://localhost:8080/system/ai/engineConfig/listAll?engineType=dify' \
-H 'Authorization: Bearer <token>'
响应:
{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 1,
"projectId": 1,
"configName": "导诊助手-Dify配置",
"engineType": "dify",
"configJson": "{\"baseUrl\":\"http://8.136.61.90/v1\",\"secretKey\":\"app-abc123xyz\"}",
"status": "0",
"createTime": "2026-03-26T12:00:00.000+00:00",
"updateTime": "2026-03-26T12:00:00.000+00:00"
}
]
}
权限:ai:engineConfig:query
cURL 示例:
curl -X GET 'http://localhost:8080/system/ai/engineConfig/1' \
-H 'Authorization: Bearer <token>'
响应:
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1,
"projectId": 1,
"configName": "导诊助手-Dify配置",
"engineType": "dify",
"configJson": "{\"baseUrl\":\"http://8.136.61.90/v1\",\"secretKey\":\"app-abc123xyz\"}",
"status": "0",
"createTime": "2026-03-26T12:00:00.000+00:00",
"updateTime": "2026-03-26T12:00:00.000+00:00"
}
}
权限:ai:engineConfig:add
cURL 示例(Dify 类型):
curl -X POST 'http://localhost:8080/system/ai/engineConfig' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"projectId": 1,
"configName": "导诊助手-Dify配置",
"engineType": "dify",
"configJson": "{\"baseUrl\":\"http://8.136.61.90/v1\",\"secretKey\":\"app-abc123xyz\"}",
"status": "0"
}'
cURL 示例(Direct 直连大模型类型):
curl -X POST 'http://localhost:8080/system/ai/engineConfig' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"projectId": 1,
"configName": "GPT-4o直连配置",
"engineType": "direct",
"configJson": "{\"baseUrl\":\"https://api.openai.com/v1\",\"apiKey\":\"sk-xxx\",\"model\":\"gpt-4o\"}",
"status": "0"
}'
cURL 示例(SpringAI 类型):
curl -X POST 'http://localhost:8080/system/ai/engineConfig' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"projectId": 1,
"configName": "SpringAI配置",
"engineType": "spring_ai",
"configJson": "{\"baseUrl\":\"https://api.openai.com/v1\",\"apiKey\":\"sk-xxx\",\"model\":\"gpt-4o\",\"temperature\":0.7,\"maxTokens\":2000}",
"status": "0"
}'
cURL 示例(Mock 测试类型):
curl -X POST 'http://localhost:8080/system/ai/engineConfig' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"projectId": 1,
"configName": "本地Mock配置",
"engineType": "mock",
"configJson": "{\"mockResponse\":\"我是模拟回复,用于开发测试\"}",
"status": "0"
}'
响应:
{
"code": 200,
"msg": "操作成功"
}
权限:ai:engineConfig:edit
cURL 示例:
curl -X PUT 'http://localhost:8080/system/ai/engineConfig' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"id": 1,
"configName": "导诊助手-Dify配置(更新)",
"engineType": "dify",
"configJson": "{\"baseUrl\":\"http://8.136.61.90/v1\",\"secretKey\":\"app-new-key-456\"}"
}'
响应:
{
"code": 200,
"msg": "操作成功"
}
权限:ai:engineConfig:remove
cURL 示例(单个删除):
curl -X DELETE 'http://localhost:8080/system/ai/engineConfig/1' \
-H 'Authorization: Bearer <token>'
cURL 示例(批量删除):
curl -X DELETE 'http://localhost:8080/system/ai/engineConfig/1,2,3' \
-H 'Authorization: Bearer <token>'
响应:
{
"code": 200,
"msg": "操作成功"
}
ai_agent_engine_config.config_json 字段按引擎类型存储不同的 JSON 结构:
engine_type = "dify"){
"baseUrl": "http://8.136.61.90/v1",
"secretKey": "app-abc123xyz"
}
| 字段 | 说明 |
|---|---|
| baseUrl | Dify 实例调用地址(同实例内所有 agent 相同) |
| secretKey | 该 agent 的专属 API 密钥(每个 agent 不同,是路由的唯一标识) |
engine_type = "direct"){
"baseUrl": "https://api.openai.com/v1",
"apiKey": "sk-xxx",
"model": "gpt-4o"
}
| 字段 | 说明 |
|---|---|
| baseUrl | 大模型 API 地址 |
| apiKey | API 密钥 |
| model | 指定模型 |
engine_type = "spring_ai"){
"baseUrl": "https://api.openai.com/v1",
"apiKey": "sk-xxx",
"model": "gpt-4o",
"temperature": 0.7,
"maxTokens": 2000
}
engine_type = "mock"){
"mockResponse": "我是模拟回复,用于开发测试"
}
以下示例使用 cURL 演示完整的调用流程。
签名计算公式:MD5(requestId + "&" + timestamp + "&" + payload + "&" + privateKey)
# 变量准备
TIMESTAMP=$(date +%s000)
REQUEST_ID=$(uuidgen)
PUBLIC_KEY="your-public-key"
PRIVATE_KEY="your-private-key"
# 计算签名(以创建会话为例)
PAYLOAD='{"agentId":"agent_123456","userId":10001,"conversationName":"测试会话"}'
SIGN=$(echo -n "${REQUEST_ID}&${TIMESTAMP}&${PAYLOAD}&${PRIVATE_KEY}" | md5)
curl -X POST 'http://localhost:8080/api/v1/conversation/create' \
-H 'Content-Type: application/json' \
-H "X-Emoon-Timestamp: ${TIMESTAMP}" \
-H "X-Emoon-Request-Id: ${REQUEST_ID}" \
-H "X-Emoon-Public-Key: ${PUBLIC_KEY}" \
-H "X-Emoon-Sign: ${SIGN}" \
-d '{
"agentId": "agent_123456",
"userId": 10001,
"conversationName": "测试会话"
}'
PAYLOAD='{"agentId":"agent_123456","query":"帮我挂个内科的号","conversationId":"conv_xxx","stream":false,"userId":10001}'
SIGN=$(echo -n "${REQUEST_ID}&${TIMESTAMP}&${PAYLOAD}&${PRIVATE_KEY}" | md5)
curl -X POST 'http://localhost:8080/api/v1/agent/chat' \
-H 'Content-Type: application/json' \
-H "X-Emoon-Timestamp: ${TIMESTAMP}" \
-H "X-Emoon-Request-Id: ${REQUEST_ID}" \
-H "X-Emoon-Public-Key: ${PUBLIC_KEY}" \
-H "X-Emoon-Sign: ${SIGN}" \
-d '{
"agentId": "agent_123456",
"query": "帮我挂个内科的号",
"conversationId": "conv_xxx",
"stream": false,
"userId": 10001
}'
PAYLOAD='{"agentId":"agent_123456","query":"帮我挂个内科的号","stream":true,"userId":10001}'
SIGN=$(echo -n "${REQUEST_ID}&${TIMESTAMP}&${PAYLOAD}&${PRIVATE_KEY}" | md5)
curl -N -X POST 'http://localhost:8080/api/v1/agent/chat' \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-H "X-Emoon-Timestamp: ${TIMESTAMP}" \
-H "X-Emoon-Request-Id: ${REQUEST_ID}" \
-H "X-Emoon-Public-Key: ${PUBLIC_KEY}" \
-H "X-Emoon-Sign: ${SIGN}" \
-d '{
"agentId": "agent_123456",
"query": "帮我挂个内科的号",
"stream": true,
"userId": 10001
}'
-N参数禁用 cURL 缓冲,用于实时接收 SSE 事件流。
SIGN=$(echo -n "${REQUEST_ID}&${TIMESTAMP}&&${PRIVATE_KEY}" | md5)
curl -X GET 'http://localhost:8080/api/v1/conversation/list?agentId=agent_123456&userId=10001' \
-H "X-Emoon-Timestamp: ${TIMESTAMP}" \
-H "X-Emoon-Request-Id: ${REQUEST_ID}" \
-H "X-Emoon-Public-Key: ${PUBLIC_KEY}" \
-H "X-Emoon-Sign: ${SIGN}"
注意:GET 请求的 payload 为空字符串,签名中对应
&&部分。
SIGN=$(echo -n "${REQUEST_ID}&${TIMESTAMP}&&${PRIVATE_KEY}" | md5)
curl -X GET 'http://localhost:8080/api/v1/conversation/conv_a1b2c3d4-xxxx' \
-H "X-Emoon-Timestamp: ${TIMESTAMP}" \
-H "X-Emoon-Request-Id: ${REQUEST_ID}" \
-H "X-Emoon-Public-Key: ${PUBLIC_KEY}" \
-H "X-Emoon-Sign: ${SIGN}"
| 错误码 | 枚举值 | 说明 |
|---|---|---|
| 3 | INVALID_PUBLIC_KEY | 公钥不正确 |
| 6 | ILLEGAL_REQUEST | 签名验证失败 |
| 8 | PROJECT_NOT_EXISTS | 项目不存在 |
| 11 | PROJECT_NO_AGENT_PERMISSION | 项目无该智能体权限 |
| 12 | AGENT_NOT_FOUND | 智能体不存在 |
| 13 | AGENT_DISABLED | 智能体已停用 |
| 14 | ENGINE_CONFIG_NOT_FOUND | 引擎配置不存在 |
| 15 | CONVERSATION_NOT_FOUND | 会话不存在 |
| 决策 | 说明 |
|---|---|
| SysProjectDo 无 tenantId | 使用默认值 "000000" |
| agentId 在 AiConversation 中是 Long | FK 到 ai_agent_app.id(主键),API 层用 String 业务 ID 后在 Service 中转换 |
| engineType 不在 AiAgentApp 上 | 通过 engineConfigId 关联 AiAgentEngineConfig 获取,避免冗余 |
| configJson 用 String 存储 | 不做 JSON 类型映射,保持灵活性,不同引擎结构不同 |
| delFlag/status 是 String 类型 | 与框架保持一致,比较用 "0".equals(...) |
| SSE 超时设为 0 | new SseEmitter(0L) 表示无超时,由引擎自行控制结束 |
| 签名算法使用 MD5 | 沿用现有项目签名方案(SignUtil) |
| ServiceImpl 放在 system-api | 遵循项目现有架构,不单独分离 |
| 功能 | 说明 |
|---|---|
| DifyAgentEngine | 对接 Dify REST API 的引擎实现 |
| DirectLLMAgentEngine | 直连大模型引擎实现(基于 SpringAI ChatClient) |
| 消息历史存储 | 存储对话消息记录并提供查询接口 |
| Admin 端智能体 CRUD | AiAgentApp 的增删改查管理接口 |
| Admin 端卡片管理 | 卡片定义、分类、插件的管理接口 |
| 前端菜单/权限配置 | 在 sys_menu 中添加 AI 管理相关菜单和权限 |