Compare commits
34 Commits
21c9eaa44d
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| edc3babe6b | |||
| d5d239ae3a | |||
| 73237507e9 | |||
| c8245ba0d6 | |||
| 1d401c6841 | |||
| 92b0a971f2 | |||
| 15808b8569 | |||
| 91e05a26cd | |||
| eb64af9d50 | |||
| ebe0fc5a12 | |||
| 8f7ffd6cc9 | |||
| 2dd242c54b | |||
| 29f132e48c | |||
| 32925bad8e | |||
| 8596f5074b | |||
| 5e0212d2a0 | |||
| 041ed0b446 | |||
| d9cf838ace | |||
| 07ad8bb36b | |||
| 6fe1209801 | |||
| d92496854d | |||
| e9abf0b689 | |||
| 1132cf0262 | |||
| b6e1e209a2 | |||
| b26edb8877 | |||
| 3526322811 | |||
| b688df56ba | |||
| 1002380b28 | |||
| 1e1c731c3f | |||
| 8701c80f90 | |||
| f63bdee88c | |||
| 1d89bc89fe | |||
| ab9b099e9b | |||
| 1e004f1a83 |
40
AGENT.md
40
AGENT.md
@@ -11,8 +11,8 @@
|
||||
- 文件上传与附件管理
|
||||
- 前后端统一的管理控制台
|
||||
|
||||
当前阶段已经完成平台骨架、公共接口规范、知识库/知识文档管理、文档上传、文档解析、解析快照与手动切片入口。
|
||||
后续重点从"文档可切片"推进到"向量可检索"、"模型可路由"和"Agent 可运行"。
|
||||
当前阶段已经完成平台骨架、公共接口规范、知识库/知识文档管理、文档上传、文档解析、解析快照、手动切片入口、模型平台基础配置与 Agent 定义管理/调试入口。
|
||||
后续重点从"文档可切片"推进到"向量可检索"、"模型可路由"和"Agent 运行时可编排"。
|
||||
|
||||
## 2. 总体设计思路
|
||||
|
||||
@@ -74,18 +74,24 @@
|
||||
|
||||
### 3.3 Agent 运行模块
|
||||
|
||||
后续平台重点能力,建议逐步补齐:
|
||||
当前已落地最小可用能力:
|
||||
|
||||
- Agent 定义
|
||||
- Prompt 模板
|
||||
- 工具注册与调用
|
||||
- 会话上下文与记忆
|
||||
- 执行日志与任务状态
|
||||
- `agent_definition`:Agent 定义管理(CRUD、编码唯一校验、知识库绑定校验)
|
||||
- Agent 管理接口:`/api/agents/list`、`/api/agents/query`、`/api/agents/detail`、`/api/agents/save`、`/api/agents/delete`
|
||||
- Agent 调试接口:`POST /api/agents/{agentId}/chat`,支持普通对话与 RAG 对话两种模式
|
||||
- Agent 调试链路:用户问题向量化 -> `rag_chunk_embedding` 相似度召回 -> 组装上下文 -> Chat 模型回答 -> 返回引用切片
|
||||
- 统一模型调用日志:通过 `ChatModelGateway` 与 `model_call_log` 记录请求 ID、模型、耗时与 token 信息
|
||||
|
||||
后续平台重点能力:
|
||||
|
||||
- Prompt 模板管理
|
||||
- 会话上下文持久化与记忆
|
||||
- 工具注册与调用协议
|
||||
- 执行任务状态与日志
|
||||
- 多步骤编排
|
||||
|
||||
建议未来增加的核心对象:
|
||||
建议后续补齐的核心对象:
|
||||
|
||||
- `agent_definition`
|
||||
- `agent_session`
|
||||
- `agent_message`
|
||||
- `agent_task`
|
||||
@@ -104,6 +110,8 @@
|
||||
- 知识库管理页(完整 CRUD + 概览卡片 + 双栏详情 + 批量上传入口)
|
||||
- 知识文档页(条件查询 + 批量上传 + 解析重试 + 批量切片 + 编辑/启停用/删除)
|
||||
- 切片任务页(解析成功/失败文档概览与切片入口)
|
||||
- Agent 管理页(Agent 定义管理与知识库绑定)
|
||||
- Agent 调试页(普通对话 / RAG 对话切换、引用切片回显)
|
||||
|
||||
前端技术要点:
|
||||
|
||||
@@ -125,7 +133,7 @@
|
||||
- 附件管理页面前端联调
|
||||
- RAG 检索配置、向量索引任务和最近任务页面联调
|
||||
- 模型服务商、模型配置、路由规则和调用日志管理
|
||||
- Agent 调试页
|
||||
- Agent 会话历史与运行日志页
|
||||
- 执行日志查看
|
||||
|
||||
## 4. 当前接口设计原则
|
||||
@@ -219,7 +227,7 @@
|
||||
5. ~~接入切片生成与切片持久化~~(已完成定长/分隔符切片与手动切片入口)
|
||||
6. 建设模型服务商配置与模型路由层
|
||||
7. 接入 Embedding / Chat 模型并完成向量写入
|
||||
8. 建立 Agent 运行时骨架
|
||||
8. 完善 Agent 运行时骨架(会话、工具、任务)
|
||||
9. ~~补前端控制台基础骨架~~(已完成,部分高级页面待联调)
|
||||
|
||||
剩余重点:
|
||||
@@ -227,17 +235,17 @@
|
||||
- 完成模型服务商配置、模型配置、路由规则和调用日志基础能力
|
||||
- 接入 Embedding,生成并保存 `rag_chunk_embedding`
|
||||
- 补齐索引任务、重试、重建索引和最近任务接口
|
||||
- 接入 OpenAI-compatible / Spring AI 适配层并实现最小模型调用链路
|
||||
- 扩展 Agent 会话、工具调用与任务编排能力
|
||||
|
||||
## 7. 下一步建议
|
||||
|
||||
结合当前代码状态,接下来建议重点做:
|
||||
|
||||
- 实现模型服务商和模型配置表:支持 Ollama、硅基流动、百炼等 OpenAI-compatible 来源
|
||||
- 实现 Embedding 网关:对已落库切片调用 Embedding 模型并写入 `rag_chunk_embedding`
|
||||
- 完成 RAG 全量向量化链路,确保知识库可稳定召回
|
||||
- 为 Agent 调试链路补齐会话持久化与多轮上下文管理
|
||||
- 建立 Agent 工具注册与调用协议,沉淀最小工具集
|
||||
- 把 `indexStatus` 从手工字段推进为真实状态流转
|
||||
- 补齐重建索引、失败重试、最近任务接口和前端展示
|
||||
- 接入模型路由,实现本地小模型与云端大模型的成本优先调用链路
|
||||
|
||||
## 8. 文档用途说明
|
||||
|
||||
|
||||
49
README.md
49
README.md
@@ -3,8 +3,8 @@
|
||||
Common Agent 是一个规划中的通用 Agent 平台,技术路线基于 Java、Spring Boot 和 Spring AI。
|
||||
项目目标是建设一套完整的前后端系统,支持 Agent 编排、工具调用、会话管理、RAG 知识库和平台管理能力。
|
||||
|
||||
当前项目已经完成基础工程、公共模块、RAG 元数据管理、文档上传、文档解析入口、解析快照、手动切片入口、前端知识库与知识文档管理页面。
|
||||
Agent 运行时、RAG 向量化、检索问答、模型服务商配置与更多平台管理能力会在后续阶段逐步实现。
|
||||
当前项目已经完成基础工程、公共模块、RAG 元数据管理、文档上传、文档解析入口、解析快照、手动切片入口、模型服务商配置基础能力、Agent 定义管理与调试页面。
|
||||
会话持久化、工具调用编排、RAG 全量向量化与检索问答能力会在后续阶段逐步完善。
|
||||
|
||||
## 项目愿景
|
||||
|
||||
@@ -13,7 +13,7 @@ Common Agent 希望成为一个可复用的企业级 AI 应用基础平台:
|
||||
- Agent 运行时:支持对话、工具调用、记忆、任务执行和流程编排。
|
||||
- RAG 知识库:支持文档导入、解析、切片、向量化、检索和基于上下文的回答生成。
|
||||
- 模型抽象:通过 Spring AI 统一接入聊天模型、Embedding 模型和重排序模型。
|
||||
- 管理控制台:提供会话、Agent、知识库、文档、提示词和系统配置的 Web 管理界面。
|
||||
- 管理控制台:提供 Agent、知识库、文档、模型配置和系统配置的 Web 管理界面。
|
||||
- 多环境部署:支持本地开发、测试环境和生产环境的配置隔离。
|
||||
|
||||
## 当前技术栈
|
||||
@@ -36,7 +36,7 @@ common_agent
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # Axios 封装与各模块 API
|
||||
│ │ ├── layouts/ # AdminLayout 管理后台布局
|
||||
│ │ ├── pages/ # 业务页面(工作台、枚举、附件、知识库、文档)
|
||||
│ │ ├── pages/ # 业务页面(系统、RAG、Agent)
|
||||
│ │ ├── router/ # Vue Router 配置
|
||||
│ │ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── styles/ # 全局样式
|
||||
@@ -59,14 +59,16 @@ common_agent
|
||||
│ │ │ ├── handler/ # GlobalExceptionHandler
|
||||
│ │ │ ├── mapper/ # SysAttachmentMapper, SysEnumMapper
|
||||
│ │ │ └── service/ # 接口与实现
|
||||
│ │ └── rag/ # RAG 知识库模块
|
||||
│ │ ├── constant/ # RagSystemConstants
|
||||
│ │ ├── controller/ # RagStoreController, RagDocumentController
|
||||
│ │ ├── dto/ # 请求/响应 DTO
|
||||
│ │ ├── entity/ # RagStore, RagDocument, RagChunk, RagChunkEmbedding
|
||||
│ │ ├── enums/ # RagParseStatusEnum, RagIndexStatusEnum, RagChunkStrategyEnum
|
||||
│ │ ├── mapper/ # RagDocumentMapper, RagStoreMapper
|
||||
│ │ └── service/ # 接口与实现
|
||||
│ │ ├── rag/ # RAG 知识库模块
|
||||
│ │ │ ├── constant/ # RagSystemConstants
|
||||
│ │ │ ├── controller/ # RagStoreController, RagDocumentController
|
||||
│ │ │ ├── dto/ # 请求/响应 DTO
|
||||
│ │ │ ├── entity/ # RagStore, RagDocument, RagChunk, RagChunkEmbedding
|
||||
│ │ │ ├── enums/ # RagParseStatusEnum, RagIndexStatusEnum, RagChunkStrategyEnum
|
||||
│ │ │ ├── mapper/ # RagDocumentMapper, RagStoreMapper
|
||||
│ │ │ └── service/ # 接口与实现
|
||||
│ │ ├── modelprovider/ # 模型服务商、模型配置、路由、网关与调用日志
|
||||
│ │ └── agent/ # Agent 定义管理与调试链路
|
||||
│ ├── main/resources/
|
||||
│ │ ├── application.yaml # 环境选择
|
||||
│ │ ├── application-dev.yaml # 开发环境配置
|
||||
@@ -74,8 +76,12 @@ common_agent
|
||||
│ └── test/java/ # 单元测试(结构稳定性测试 + 前端 API 测试)
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md # 架构说明
|
||||
│ └── ROADMAP.md # 开发路线图
|
||||
│ ├── ROADMAP.md # 开发路线图
|
||||
│ ├── MODEL_PROVIDER_REQUIREMENTS.md # 模型服务商配置与路由需求
|
||||
│ ├── MODEL_PROVIDER_DESIGN.md # 模型服务商配置与路由设计
|
||||
│ └── MODEL_PROVIDER_SCHEMA.sql # 模型平台与Agent核心表结构
|
||||
├── AGENT.md # 平台设计草案
|
||||
├── agent-page-apis.md # Agent页面后端接口清单
|
||||
├── pom.xml
|
||||
└── README.md
|
||||
```
|
||||
@@ -152,6 +158,8 @@ npm run build
|
||||
| 知识库 | 完整 CRUD + 双栏详情 |
|
||||
| 知识文档 | 条件查询 + 批量上传 + 解析重试 + 批量切片 + 编辑/启停用/删除 |
|
||||
| 切片任务 | 解析成功/失败文档概览 + 切片入口 |
|
||||
| Agent管理 | Agent 定义 CRUD + 知识库绑定 |
|
||||
| Agent调试 | 普通对话 / RAG 对话切换 + 引用切片回显 |
|
||||
|
||||
当前 UI 规范:
|
||||
|
||||
@@ -171,16 +179,24 @@ npm run build
|
||||
|
||||
## RAG 当前能力边界
|
||||
|
||||
当前 RAG 已经从元数据管理推进到"上传 + 解析 + 手动切片"阶段:
|
||||
当前 RAG 已经从元数据管理推进到"上传 + 解析 + 手动切片 + Agent 调试召回"阶段:
|
||||
|
||||
- 知识库:支持列表、条件查询、详情、总览、单库文档概览、新增、编辑、删除。
|
||||
- 知识文档:支持列表、条件查询、详情、新增/编辑、删除、批量上传。
|
||||
- 文档解析:基于 Apache Tika 支持 TXT/Markdown/LOG、PDF、Word、Excel 文本抽取,解析时更新 `parseStatus` 并保存解析快照。
|
||||
- 文档切片:支持按解析快照进行手动异步切片,已落地定长切片和分隔符切片,写入 `rag_chunk`。
|
||||
- 向量表:`rag_chunk_embedding` 实体、Mapper、Service 已有结构,向量写入、检索召回和重排序仍待接入。
|
||||
- 模型配置:已补充模型服务商配置与路由需求/设计文档,后续用于统一接入 Ollama、硅基流动、百炼等来源。
|
||||
- 向量表:`rag_chunk_embedding` 实体、Mapper、Service 已有结构,向量写入与召回 SQL 已用于 Agent 调试链路,RAG 检索问答接口仍待补齐。
|
||||
- 模型配置:模型服务商、模型配置、路由规则、调用日志基础能力已落地,Embedding/Chat 网关可用于 RAG 与 Agent 调试调用。
|
||||
- 前端:知识库页、知识文档页、RAG 工作台和切片任务页已经接入当前接口,检索配置、最近任务、重建索引仍是后续能力。
|
||||
|
||||
## Agent 当前能力边界
|
||||
|
||||
- Agent 定义:支持 `agent_definition` 的列表、查询、详情、新增/更新、删除。
|
||||
- Agent 对话:支持 `POST /api/agents/{agentId}/chat`,`ragEnabled=true` 时走 RAG 召回,`false` 时走普通对话。
|
||||
- RAG 对话流程:用户问题向量化 -> 按知识库召回 TopK 切片 -> 组装系统提示词与上下文 -> Chat 模型回答。
|
||||
- 调试回显:返回答案、请求 ID 和引用切片,便于前端页面展示与排障。
|
||||
- 当前限制:尚未持久化 `agent_session/agent_message`,工具调用和任务编排仍在规划中。
|
||||
|
||||
## 规划模块
|
||||
|
||||
- `agent-core`:Agent 执行模型、工具注册、记忆和编排能力。
|
||||
@@ -197,6 +213,7 @@ npm run build
|
||||
- [模型服务商配置与路由需求](docs/MODEL_PROVIDER_REQUIREMENTS.md)
|
||||
- [模型服务商配置与路由设计](docs/MODEL_PROVIDER_DESIGN.md)
|
||||
- [平台设计草案](AGENT.md)
|
||||
- [Agent 页面接口清单](agent-page-apis.md)
|
||||
|
||||
## 参考资料
|
||||
|
||||
|
||||
159
agent-page-apis.md
Normal file
159
agent-page-apis.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Agent 页面后端接口清单
|
||||
|
||||
本文对应前端页面:[AgentManagePage.vue](frontend/src/pages/agent/AgentManagePage.vue) 和 [AgentDebugPage.vue](frontend/src/pages/agent/AgentDebugPage.vue)。
|
||||
|
||||
## 1. 页面目标
|
||||
|
||||
Agent 页面分为两块:
|
||||
|
||||
- Agent 管理:维护 `agent_definition` 基础配置(编码、名称、知识库绑定、状态、系统提示词)。
|
||||
- Agent 调试:选择 Agent 发起对话,支持普通对话与 RAG 对话切换,并回显引用切片。
|
||||
|
||||
## 2. Agent 管理接口
|
||||
|
||||
### 2.1 查询全部 Agent
|
||||
|
||||
- `POST /api/agents/list`
|
||||
|
||||
返回类型:
|
||||
|
||||
- `RequestResult<List<AgentDefinitionResponse>>`
|
||||
|
||||
### 2.2 条件查询 Agent
|
||||
|
||||
- `POST /api/agents/query`
|
||||
|
||||
请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"agentCode": "AGENT_RAG_HELPER",
|
||||
"agentName": "知识助手",
|
||||
"status": "ENABLED",
|
||||
"storeId": 1001
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 查询 Agent 详情
|
||||
|
||||
- `GET /api/agents/detail?id={id}`
|
||||
|
||||
### 2.4 新增或更新 Agent
|
||||
|
||||
- `POST /api/agents/save`
|
||||
|
||||
请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"agentCode": "AGENT_RAG_HELPER",
|
||||
"agentName": "知识问答助手",
|
||||
"systemPrompt": "你是企业知识助手,请优先基于知识库回答。",
|
||||
"storeId": 1001,
|
||||
"status": "ENABLED",
|
||||
"remark": "客服场景"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `id` 为空时新增,非空时更新。
|
||||
- `agentCode` 全局唯一。
|
||||
- `storeId` 必须指向已存在的 `rag_store`。
|
||||
- `status` 默认 `ENABLED`,可选 `ENABLED` / `DISABLED`。
|
||||
|
||||
### 2.5 删除 Agent
|
||||
|
||||
- `POST /api/agents/delete?id={id}`
|
||||
|
||||
## 3. Agent 调试接口
|
||||
|
||||
### 3.1 发起对话
|
||||
|
||||
- `POST /api/agents/{agentId}/chat`
|
||||
|
||||
请求体示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{ "role": "user", "content": "请说明请假流程" }
|
||||
],
|
||||
"ragEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"resultcode": "0",
|
||||
"message": null,
|
||||
"data": {
|
||||
"agentId": 1,
|
||||
"agentCode": "AGENT_RAG_HELPER",
|
||||
"agentName": "知识问答助手",
|
||||
"storeId": 1001,
|
||||
"storeName": "企业知识库",
|
||||
"answer": "根据知识库,先提交 OA 审批单。",
|
||||
"modelRequestId": "f4215d13d0b3493e963297f15428e2f2",
|
||||
"references": [
|
||||
{
|
||||
"chunkId": 9001,
|
||||
"documentId": 8001,
|
||||
"chunkContent": "请假流程:员工先在OA提交审批单...",
|
||||
"score": 0.9123
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 对话模式说明
|
||||
|
||||
### 4.1 `ragEnabled=true`(默认)
|
||||
|
||||
执行路径:
|
||||
|
||||
1. 从消息列表中提取最后一条 `role=user` 的问题。
|
||||
2. 读取该 Agent 绑定知识库的生效 Embedding 配置。
|
||||
3. 生成查询向量并在 `rag_chunk_embedding` 按知识库 TopK 召回切片。
|
||||
4. 将系统提示词、召回片段和会话消息组装后调用 Chat 模型。
|
||||
5. 返回回答 + 引用切片 + `modelRequestId`。
|
||||
|
||||
### 4.2 `ragEnabled=false`
|
||||
|
||||
执行路径:
|
||||
|
||||
- 跳过向量化与召回,直接使用会话消息调用 Chat 模型,返回普通对话结果。
|
||||
|
||||
## 5. 调试联调前置条件
|
||||
|
||||
### 5.1 普通对话前置条件
|
||||
|
||||
- Agent 状态为 `ENABLED`。
|
||||
- Agent 已绑定存在的知识库。
|
||||
- 已配置可用的 Chat 路由(`taskType=CHAT_SIMPLE` 或 `RAG_ANSWER`)。
|
||||
|
||||
### 5.2 RAG 对话前置条件
|
||||
|
||||
- 满足普通对话前置条件。
|
||||
- 知识库存在生效 `rag_store_model_config` 且已绑定 Embedding 模型。
|
||||
- 目标知识库至少有可用向量数据(`rag_chunk_embedding`)。
|
||||
|
||||
## 6. 常见失败提示
|
||||
|
||||
- `Agent已停用,暂不支持对话`:需启用 Agent。
|
||||
- `当前知识库未配置Embedding模型,无法执行检索对话`:需先配置知识库 Embedding 模型。
|
||||
- `未召回到可用知识切片,请先完成知识库切片与向量化`:需补齐切片向量化流程。
|
||||
|
||||
## 7. 相关代码入口
|
||||
|
||||
- `src/main/java/com/bruce/agent/controller/AgentDefinitionController.java`
|
||||
- `src/main/java/com/bruce/agent/service/impl/AgentDefinitionServiceImpl.java`
|
||||
- `src/main/java/com/bruce/agent/entity/AgentDefinition.java`
|
||||
- `src/main/java/com/bruce/modelprovider/gateway/ChatModelGatewayImpl.java`
|
||||
- `frontend/src/api/agent.ts`
|
||||
- `frontend/src/pages/agent/AgentManagePage.vue`
|
||||
- `frontend/src/pages/agent/AgentDebugPage.vue`
|
||||
60
common-agent-agent/pom.xml
Normal file
60
common-agent-agent/pom.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>common-agent-agent</artifactId>
|
||||
<name>common-agent-agent</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-rag</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-modelprovider</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.bruce.agent.controller;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/agents")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentDefinitionController {
|
||||
|
||||
private final IAgentDefinitionService agentDefinitionService;
|
||||
|
||||
@PostMapping("/list")
|
||||
public RequestResult<List<AgentDefinitionResponse>> list() {
|
||||
return RequestResult.success(agentDefinitionService.listResponses());
|
||||
}
|
||||
|
||||
@PostMapping("/query")
|
||||
public RequestResult<List<AgentDefinitionResponse>> query(@RequestBody(required = false) AgentDefinitionQueryRequest request) {
|
||||
return RequestResult.success(agentDefinitionService.query(request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
public RequestResult<AgentDefinitionResponse> detail(@RequestParam("id") Long id) {
|
||||
return RequestResult.success(agentDefinitionService.getResponseById(id));
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
public RequestResult<Boolean> save(@RequestBody AgentDefinitionSaveRequest request) {
|
||||
return RequestResult.success(agentDefinitionService.saveOrUpdate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||
return RequestResult.success(agentDefinitionService.removeById(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{agentId}/chat")
|
||||
public RequestResult<AgentChatResponse> chat(@PathVariable("agentId") Long agentId,
|
||||
@RequestBody AgentChatRequest request) {
|
||||
return RequestResult.success(agentDefinitionService.chat(agentId, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容前端实现文档中的运行入口路径。
|
||||
*/
|
||||
@PostMapping("/{agentId}/runs")
|
||||
public RequestResult<AgentChatResponse> run(@PathVariable("agentId") Long agentId,
|
||||
@RequestBody AgentChatRequest request) {
|
||||
log.info("Agent运行入口开始,agentId={}, messageCount={}",
|
||||
agentId, request.getMessages() == null ? 0 : request.getMessages().size());
|
||||
return RequestResult.success(agentDefinitionService.chat(agentId, request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.bruce.agent.controller;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.service.IAgentWorkspaceService;
|
||||
import com.bruce.agent.vo.AgentMessageVO;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import com.bruce.agent.vo.AgentWorkspaceVO;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 会话控制器。
|
||||
* <p>
|
||||
* 负责会话创建、详情、消息查询与工作台聚合查询,保持前端只消费 DTO / VO 契约。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/agent-sessions")
|
||||
@RequiredArgsConstructor
|
||||
public class AgentSessionController {
|
||||
|
||||
private final IAgentSessionService agentSessionService;
|
||||
private final IAgentMessageService agentMessageService;
|
||||
private final IAgentWorkspaceService agentWorkspaceService;
|
||||
|
||||
@PostMapping("/create")
|
||||
public RequestResult<Boolean> create(@RequestBody AgentSessionCreateDTO request) {
|
||||
log.info("Agent会话创建请求开始,agentId={}, sessionCode={}", request.getAgentId(), request.getSessionCode());
|
||||
return RequestResult.success(agentSessionService.createSession(request));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
public RequestResult<AgentSessionDetailVO> detail(@RequestParam("id") Long id) {
|
||||
log.info("Agent会话详情查询开始,sessionId={}", id);
|
||||
return RequestResult.success(agentSessionService.getDetailById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容前端实现文档中的资源化会话详情路径。
|
||||
*/
|
||||
@GetMapping("/{sessionId}")
|
||||
public RequestResult<AgentSessionDetailVO> detailByPath(@PathVariable("sessionId") Long sessionId) {
|
||||
log.info("Agent会话详情按路径查询开始,sessionId={}", sessionId);
|
||||
return RequestResult.success(agentSessionService.getDetailById(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容前端实现文档中的 Agent 会话列表路径。
|
||||
*/
|
||||
@GetMapping("/agents/{agentId}/sessions")
|
||||
public RequestResult<List<AgentSessionDetailVO>> sessionsByAgent(@PathVariable("agentId") Long agentId) {
|
||||
log.info("Agent会话列表查询开始,agentId={}", agentId);
|
||||
return RequestResult.success(agentSessionService.listByAgentId(agentId));
|
||||
}
|
||||
|
||||
@GetMapping("/{sessionId}/messages")
|
||||
public RequestResult<List<AgentMessageVO>> messages(@PathVariable("sessionId") Long sessionId) {
|
||||
log.info("Agent消息列表查询开始,sessionId={}", sessionId);
|
||||
return RequestResult.success(agentMessageService.listBySessionId(sessionId));
|
||||
}
|
||||
|
||||
@PostMapping("/{sessionId}/messages")
|
||||
public RequestResult<Boolean> appendMessage(@PathVariable("sessionId") Long sessionId,
|
||||
@RequestBody AgentSessionMessageCreateDTO request) {
|
||||
request.setSessionId(sessionId);
|
||||
log.info("Agent消息写入请求开始,sessionId={}, role={}, requestId={}",
|
||||
sessionId, request.getRole(), request.getRequestId());
|
||||
return RequestResult.success(agentMessageService.appendMessage(request));
|
||||
}
|
||||
|
||||
@GetMapping("/workspace")
|
||||
public RequestResult<AgentWorkspaceVO> workspace(@RequestParam("agentId") Long agentId,
|
||||
@RequestParam(value = "sessionId", required = false) Long sessionId) {
|
||||
log.info("Agent工作台查询开始,agentId={}, sessionId={}", agentId, sessionId);
|
||||
return RequestResult.success(agentWorkspaceService.getWorkspace(agentId, sessionId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AgentChatRequest {
|
||||
private List<AgentMessage> messages;
|
||||
private Boolean ragEnabled;
|
||||
|
||||
@Data
|
||||
public static class AgentMessage {
|
||||
private String role;
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AgentDefinitionQueryRequest {
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String status;
|
||||
private Long storeId;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AgentDefinitionSaveRequest {
|
||||
private Long id;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String systemPrompt;
|
||||
private Long storeId;
|
||||
private String status;
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AgentSessionCreateDTO {
|
||||
private Long agentId;
|
||||
private String sessionCode;
|
||||
private Long workflowRunId;
|
||||
private String title;
|
||||
private String metadataJson;
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.bruce.agent.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AgentSessionMessageCreateDTO {
|
||||
private Long sessionId;
|
||||
private String role;
|
||||
private String content;
|
||||
private String citationJson;
|
||||
private Integer tokenCount;
|
||||
private String requestId;
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.bruce.agent.dto.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AgentChatResponse {
|
||||
private Long agentId;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private Long sessionId;
|
||||
private String sessionCode;
|
||||
private String answer;
|
||||
private String modelRequestId;
|
||||
private List<ReferenceChunk> references;
|
||||
|
||||
@Data
|
||||
public static class ReferenceChunk {
|
||||
private Long chunkId;
|
||||
private Long documentId;
|
||||
private String chunkContent;
|
||||
private Double score;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bruce.agent.dto.response;
|
||||
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
@Data
|
||||
public class AgentDefinitionResponse {
|
||||
private Long id;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String systemPrompt;
|
||||
private Long storeId;
|
||||
private String status;
|
||||
private String remark;
|
||||
|
||||
public static AgentDefinitionResponse fromEntity(AgentDefinition entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
AgentDefinitionResponse response = new AgentDefinitionResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.bruce.agent.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.bruce.common.domain.model.BaseEntity;
|
||||
import com.bruce.common.typehandler.PgJsonbStringTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("agent_capability_binding")
|
||||
public class AgentCapabilityBinding extends BaseEntity {
|
||||
|
||||
@TableField("owner_type")
|
||||
private String ownerType;
|
||||
|
||||
@TableField("owner_id")
|
||||
private Long ownerId;
|
||||
|
||||
@TableField("capability_type")
|
||||
private String capabilityType;
|
||||
|
||||
@TableField("capability_id")
|
||||
private Long capabilityId;
|
||||
|
||||
private Boolean enabled;
|
||||
|
||||
@TableField(value = "config_json", typeHandler = PgJsonbStringTypeHandler.class)
|
||||
private String configJson;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bruce.agent.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.bruce.common.domain.model.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("agent_definition")
|
||||
public class AgentDefinition extends BaseEntity {
|
||||
|
||||
@TableField("agent_code")
|
||||
private String agentCode;
|
||||
|
||||
@TableField("agent_name")
|
||||
private String agentName;
|
||||
|
||||
@TableField("system_prompt")
|
||||
private String systemPrompt;
|
||||
|
||||
@TableField("store_id")
|
||||
private Long storeId;
|
||||
|
||||
private String status;
|
||||
|
||||
@TableField("remark")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.bruce.agent.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.bruce.common.domain.model.BaseEntity;
|
||||
import com.bruce.common.typehandler.PgJsonbStringTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("agent_message")
|
||||
public class AgentMessage extends BaseEntity {
|
||||
|
||||
@TableField("session_id")
|
||||
private Long sessionId;
|
||||
|
||||
private String role;
|
||||
|
||||
private String content;
|
||||
|
||||
@TableField(value = "citation_json", typeHandler = PgJsonbStringTypeHandler.class)
|
||||
private String citationJson;
|
||||
|
||||
@TableField("token_count")
|
||||
private Integer tokenCount;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.bruce.agent.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.bruce.common.domain.model.BaseEntity;
|
||||
import com.bruce.common.typehandler.PgJsonbStringTypeHandler;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("agent_session")
|
||||
public class AgentSession extends BaseEntity {
|
||||
|
||||
@TableField("session_code")
|
||||
private String sessionCode;
|
||||
|
||||
@TableField("agent_id")
|
||||
private Long agentId;
|
||||
|
||||
@TableField("workflow_run_id")
|
||||
private Long workflowRunId;
|
||||
|
||||
private String title;
|
||||
|
||||
private String status;
|
||||
|
||||
@TableField(value = "metadata_json", typeHandler = PgJsonbStringTypeHandler.class)
|
||||
private String metadataJson;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.bruce.agent.factory;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 定义工厂,统一处理请求、实体和返回对象转换。
|
||||
*/
|
||||
@Component
|
||||
public class AgentDefinitionFactory {
|
||||
|
||||
public AgentDefinition toEntity(AgentDefinitionSaveRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
AgentDefinition entity = new AgentDefinition();
|
||||
entity.setId(request.getId());
|
||||
entity.setAgentCode(trimToNull(request.getAgentCode()));
|
||||
entity.setAgentName(trimToNull(request.getAgentName()));
|
||||
entity.setSystemPrompt(trimToNull(request.getSystemPrompt()));
|
||||
entity.setStoreId(request.getStoreId());
|
||||
entity.setStatus(trimToNull(request.getStatus()));
|
||||
entity.setRemark(trimToNull(request.getRemark()));
|
||||
return entity;
|
||||
}
|
||||
|
||||
public AgentDefinitionResponse toResponse(AgentDefinition entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
AgentDefinitionResponse response = new AgentDefinitionResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<AgentDefinitionResponse> toResponses(List<AgentDefinition> entities) {
|
||||
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.bruce.agent.factory;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.vo.AgentMessageVO;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 消息工厂,统一处理消息入参与返回转换。
|
||||
*/
|
||||
@Component
|
||||
public class AgentMessageFactory {
|
||||
|
||||
public AgentMessage toEntity(AgentSessionMessageCreateDTO request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
AgentMessage entity = new AgentMessage();
|
||||
entity.setSessionId(request.getSessionId());
|
||||
entity.setRole(trimToNull(request.getRole()));
|
||||
entity.setContent(trimToNull(request.getContent()));
|
||||
entity.setCitationJson(trimToNull(request.getCitationJson()));
|
||||
entity.setTokenCount(request.getTokenCount());
|
||||
entity.setRemark(trimToNull(request.getRemark()));
|
||||
return entity;
|
||||
}
|
||||
|
||||
public AgentMessageVO toVO(AgentMessage entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
AgentMessageVO vo = new AgentMessageVO();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
public List<AgentMessageVO> toVOList(List<AgentMessage> entities) {
|
||||
return entities == null ? List.of() : entities.stream().map(this::toVO).toList();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.bruce.agent.factory;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 会话工厂,统一处理会话转换逻辑。
|
||||
*/
|
||||
@Component
|
||||
public class AgentSessionFactory {
|
||||
|
||||
public AgentSession toEntity(AgentSessionCreateDTO request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSession entity = new AgentSession();
|
||||
entity.setAgentId(request.getAgentId());
|
||||
entity.setSessionCode(trimToNull(request.getSessionCode()));
|
||||
entity.setWorkflowRunId(request.getWorkflowRunId());
|
||||
entity.setTitle(trimToNull(request.getTitle()));
|
||||
entity.setMetadataJson(trimToNull(request.getMetadataJson()));
|
||||
entity.setRemark(trimToNull(request.getRemark()));
|
||||
return entity;
|
||||
}
|
||||
|
||||
public AgentSessionDetailVO toDetailVO(AgentSession entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSessionDetailVO detailVO = new AgentSessionDetailVO();
|
||||
BeanUtils.copyProperties(entity, detailVO);
|
||||
return detailVO;
|
||||
}
|
||||
|
||||
public List<AgentSessionDetailVO> toDetailVOList(List<AgentSession> entities) {
|
||||
return entities == null ? List.of() : entities.stream().map(this::toDetailVO).toList();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bruce.agent.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bruce.agent.entity.AgentCapabilityBinding;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AgentCapabilityBindingMapper extends BaseMapper<AgentCapabilityBinding> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bruce.agent.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AgentDefinitionMapper extends BaseMapper<AgentDefinition> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bruce.agent.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AgentMessageMapper extends BaseMapper<AgentMessage> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bruce.agent.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AgentSessionMapper extends BaseMapper<AgentSession> {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.bruce.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAgentDefinitionService extends IService<AgentDefinition> {
|
||||
List<AgentDefinitionResponse> listResponses();
|
||||
|
||||
List<AgentDefinitionResponse> query(AgentDefinitionQueryRequest request);
|
||||
|
||||
AgentDefinitionResponse getResponseById(Long id);
|
||||
|
||||
boolean saveOrUpdate(AgentDefinitionSaveRequest request);
|
||||
|
||||
AgentChatResponse chat(Long agentId, AgentChatRequest request);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bruce.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.vo.AgentMessageVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAgentMessageService extends IService<AgentMessage> {
|
||||
|
||||
boolean appendMessage(AgentSessionMessageCreateDTO request);
|
||||
|
||||
AgentMessage appendMessageEntity(AgentSessionMessageCreateDTO request);
|
||||
|
||||
List<AgentMessageVO> listBySessionId(Long sessionId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.bruce.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAgentSessionService extends IService<AgentSession> {
|
||||
|
||||
boolean createSession(AgentSessionCreateDTO request);
|
||||
|
||||
AgentSession createSessionEntity(AgentSessionCreateDTO request);
|
||||
|
||||
List<AgentSessionDetailVO> listByAgentId(Long agentId);
|
||||
|
||||
AgentSessionDetailVO getDetailById(Long sessionId);
|
||||
|
||||
boolean closeSession(Long sessionId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.bruce.agent.service;
|
||||
|
||||
import com.bruce.agent.vo.AgentWorkspaceVO;
|
||||
|
||||
public interface IAgentWorkspaceService {
|
||||
|
||||
AgentWorkspaceVO getWorkspace(Long agentId, Long sessionId);
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package com.bruce.agent.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.factory.AgentDefinitionFactory;
|
||||
import com.bruce.agent.mapper.AgentDefinitionMapper;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import com.bruce.modelprovider.client.OpenAiChatMessage;
|
||||
import com.bruce.modelprovider.entity.ModelCallLog;
|
||||
import com.bruce.modelprovider.entity.RagStoreModelConfig;
|
||||
import com.bruce.modelprovider.gateway.ChatModelGateway;
|
||||
import com.bruce.modelprovider.gateway.ChatRequest;
|
||||
import com.bruce.modelprovider.gateway.ChatResult;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingModelGateway;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingRequest;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingResult;
|
||||
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
|
||||
import com.bruce.rag.dto.response.RagChunkRecallResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.mapper.RagChunkEmbeddingMapper;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMapper, AgentDefinition>
|
||||
implements IAgentDefinitionService {
|
||||
|
||||
private static final int DEFAULT_TOP_K = 5;
|
||||
|
||||
private final IRagStoreService ragStoreService;
|
||||
private final IRagStoreModelConfigService ragStoreModelConfigService;
|
||||
private final RagChunkEmbeddingMapper ragChunkEmbeddingMapper;
|
||||
private final EmbeddingModelGateway embeddingModelGateway;
|
||||
private final ChatModelGateway chatModelGateway;
|
||||
private final IAgentSessionService agentSessionService;
|
||||
private final IAgentMessageService agentMessageService;
|
||||
private final AgentDefinitionFactory agentDefinitionFactory;
|
||||
|
||||
@Override
|
||||
public List<AgentDefinitionResponse> listResponses() {
|
||||
return agentDefinitionFactory.toResponses(lambdaQuery()
|
||||
.orderByAsc(AgentDefinition::getAgentCode)
|
||||
.list());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDefinitionResponse> query(AgentDefinitionQueryRequest request) {
|
||||
AgentDefinitionQueryRequest queryRequest = request == null ? new AgentDefinitionQueryRequest() : request;
|
||||
return agentDefinitionFactory.toResponses(lambdaQuery()
|
||||
.eq(StringUtils.hasText(queryRequest.getAgentCode()), AgentDefinition::getAgentCode, trimToNull(queryRequest.getAgentCode()))
|
||||
.like(StringUtils.hasText(queryRequest.getAgentName()), AgentDefinition::getAgentName, trimToNull(queryRequest.getAgentName()))
|
||||
.eq(StringUtils.hasText(queryRequest.getStatus()), AgentDefinition::getStatus, trimToNull(queryRequest.getStatus()))
|
||||
.eq(queryRequest.getStoreId() != null, AgentDefinition::getStoreId, queryRequest.getStoreId())
|
||||
.orderByAsc(AgentDefinition::getAgentCode)
|
||||
.list());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentDefinitionResponse getResponseById(Long id) {
|
||||
return agentDefinitionFactory.toResponse(getById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdate(AgentDefinitionSaveRequest request) {
|
||||
validateSaveRequest(request);
|
||||
if (ragStoreService.getById(request.getStoreId()) == null) {
|
||||
throw new IllegalArgumentException("绑定知识库不存在,ID: " + request.getStoreId());
|
||||
}
|
||||
String agentCode = request.getAgentCode().trim();
|
||||
AgentDefinition duplicate = lambdaQuery()
|
||||
.eq(AgentDefinition::getAgentCode, agentCode)
|
||||
.ne(request.getId() != null, AgentDefinition::getId, request.getId())
|
||||
.one();
|
||||
if (duplicate != null) {
|
||||
throw new IllegalArgumentException("Agent编码已存在: " + agentCode);
|
||||
}
|
||||
|
||||
AgentDefinition requestEntity = agentDefinitionFactory.toEntity(request);
|
||||
AgentDefinition entity = request.getId() == null ? new AgentDefinition() : getById(request.getId());
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("Agent不存在,ID: " + request.getId());
|
||||
}
|
||||
entity.setAgentCode(requestEntity.getAgentCode());
|
||||
entity.setAgentName(requestEntity.getAgentName());
|
||||
entity.setSystemPrompt(requestEntity.getSystemPrompt());
|
||||
entity.setStoreId(requestEntity.getStoreId());
|
||||
entity.setStatus(StringUtils.hasText(requestEntity.getStatus())
|
||||
? requestEntity.getStatus()
|
||||
: EnableStatusEnum.ENABLED.name());
|
||||
entity.setRemark(requestEntity.getRemark());
|
||||
|
||||
boolean result = request.getId() == null ? save(entity) : updateById(entity);
|
||||
log.info("Agent定义保存完成,agentId={}, agentCode={}, storeId={}, result={}",
|
||||
entity.getId(), entity.getAgentCode(), entity.getStoreId(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentChatResponse chat(Long agentId, AgentChatRequest request) {
|
||||
log.info("Agent兼容聊天开始,agentId={}", agentId);
|
||||
if (agentId == null) {
|
||||
throw new IllegalArgumentException("Agent ID不能为空");
|
||||
}
|
||||
if (request == null || request.getMessages() == null || request.getMessages().isEmpty()) {
|
||||
throw new IllegalArgumentException("对话消息不能为空");
|
||||
}
|
||||
AgentDefinition agent = getById(agentId);
|
||||
if (agent == null) {
|
||||
throw new IllegalArgumentException("Agent不存在,ID: " + agentId);
|
||||
}
|
||||
if (!EnableStatusEnum.ENABLED.name().equals(agent.getStatus())) {
|
||||
throw new IllegalArgumentException("Agent已停用,暂不支持对话");
|
||||
}
|
||||
if (agent.getStoreId() == null) {
|
||||
throw new IllegalArgumentException("Agent未绑定知识库,请先保存知识库配置");
|
||||
}
|
||||
RagStore store = ragStoreService.getById(agent.getStoreId());
|
||||
if (store == null) {
|
||||
throw new IllegalArgumentException("绑定知识库不存在,ID: " + agent.getStoreId());
|
||||
}
|
||||
|
||||
String queryText = resolveLatestUserMessage(request.getMessages());
|
||||
boolean ragEnabled = request.getRagEnabled() == null || request.getRagEnabled();
|
||||
List<RagChunkRecallResponse> recalls = List.of();
|
||||
if (ragEnabled) {
|
||||
RagStoreModelConfig storeModelConfig = ragStoreModelConfigService.getActiveEntity(agent.getStoreId());
|
||||
if (storeModelConfig == null || storeModelConfig.getEmbeddingModelId() == null) {
|
||||
throw new IllegalArgumentException("当前知识库未配置Embedding模型,无法执行检索对话");
|
||||
}
|
||||
EmbeddingRequest embeddingRequest = new EmbeddingRequest();
|
||||
embeddingRequest.setTexts(List.of(queryText));
|
||||
embeddingRequest.setTaskType("RAG_QUERY_EMBEDDING");
|
||||
embeddingRequest.setMatchScope("RAG_STORE");
|
||||
embeddingRequest.setScopeId(agent.getStoreId());
|
||||
embeddingRequest.setBizType("AGENT_CHAT");
|
||||
embeddingRequest.setBizId(String.valueOf(agentId));
|
||||
embeddingRequest.setExpectedDimension(storeModelConfig.getEmbeddingDimension());
|
||||
EmbeddingResult queryEmbedding = embeddingModelGateway.embed(embeddingRequest);
|
||||
if (queryEmbedding.getVectors() == null || queryEmbedding.getVectors().isEmpty()) {
|
||||
throw new IllegalArgumentException("查询向量生成失败,请检查Embedding模型配置");
|
||||
}
|
||||
|
||||
String queryVector = toVectorLiteral(queryEmbedding.getVectors().getFirst());
|
||||
recalls = ragChunkEmbeddingMapper.queryTopKByStore(agent.getStoreId(), queryVector, DEFAULT_TOP_K);
|
||||
if (recalls.isEmpty()) {
|
||||
throw new IllegalArgumentException("未召回到可用知识切片,请先完成知识库切片与向量化");
|
||||
}
|
||||
}
|
||||
|
||||
AgentSession session = createCompatibilitySession(agentId, request);
|
||||
appendRequestMessages(session.getId(), request.getMessages());
|
||||
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
chatRequest.setTaskType(ragEnabled ? "RAG_ANSWER" : "CHAT_SIMPLE");
|
||||
chatRequest.setMatchScope("AGENT");
|
||||
chatRequest.setScopeId(agentId);
|
||||
chatRequest.setBizType("AGENT_CHAT");
|
||||
chatRequest.setBizId(String.valueOf(agentId));
|
||||
chatRequest.setMessages(buildChatMessages(agent, recalls, request.getMessages(), ragEnabled));
|
||||
|
||||
ChatResult chatResult = chatModelGateway.chat(chatRequest);
|
||||
appendAssistantMessage(session.getId(), recalls, chatResult);
|
||||
|
||||
AgentChatResponse response = new AgentChatResponse();
|
||||
response.setAgentId(agent.getId());
|
||||
response.setAgentCode(agent.getAgentCode());
|
||||
response.setAgentName(agent.getAgentName());
|
||||
response.setStoreId(agent.getStoreId());
|
||||
response.setStoreName(store.getStoreName());
|
||||
response.setSessionId(session.getId());
|
||||
response.setSessionCode(session.getSessionCode());
|
||||
response.setAnswer(chatResult.getContent());
|
||||
response.setModelRequestId(resolveRequestId(chatResult));
|
||||
response.setReferences(toReferenceChunks(recalls));
|
||||
log.info("Agent兼容聊天结束,agentId={}, sessionId={}, requestId={}, referenceCount={}",
|
||||
agentId, session.getId(), resolveRequestId(chatResult), recalls.size());
|
||||
return response;
|
||||
}
|
||||
|
||||
private void validateSaveRequest(AgentDefinitionSaveRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("保存请求不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getAgentCode())) {
|
||||
throw new IllegalArgumentException("Agent编码不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getAgentName())) {
|
||||
throw new IllegalArgumentException("Agent名称不能为空");
|
||||
}
|
||||
if (request.getStoreId() == null) {
|
||||
throw new IllegalArgumentException("绑定知识库不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveLatestUserMessage(List<AgentChatRequest.AgentMessage> messages) {
|
||||
for (int index = messages.size() - 1; index >= 0; index--) {
|
||||
AgentChatRequest.AgentMessage message = messages.get(index);
|
||||
if (message != null
|
||||
&& "user".equalsIgnoreCase(message.getRole())
|
||||
&& StringUtils.hasText(message.getContent())) {
|
||||
return message.getContent();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("缺少用户提问内容");
|
||||
}
|
||||
|
||||
private List<OpenAiChatMessage> buildChatMessages(AgentDefinition agent,
|
||||
List<RagChunkRecallResponse> recalls,
|
||||
List<AgentChatRequest.AgentMessage> rawMessages,
|
||||
boolean ragEnabled) {
|
||||
List<OpenAiChatMessage> messages = new ArrayList<>();
|
||||
OpenAiChatMessage instructionMessage = new OpenAiChatMessage();
|
||||
instructionMessage.setRole("system");
|
||||
instructionMessage.setContent(buildSystemInstruction(agent));
|
||||
messages.add(instructionMessage);
|
||||
|
||||
if (ragEnabled) {
|
||||
OpenAiChatMessage contextMessage = new OpenAiChatMessage();
|
||||
contextMessage.setRole("system");
|
||||
contextMessage.setContent(buildContextText(recalls));
|
||||
messages.add(contextMessage);
|
||||
}
|
||||
|
||||
for (AgentChatRequest.AgentMessage rawMessage : rawMessages) {
|
||||
if (rawMessage == null || !StringUtils.hasText(rawMessage.getContent())) {
|
||||
continue;
|
||||
}
|
||||
OpenAiChatMessage message = new OpenAiChatMessage();
|
||||
message.setRole(normalizeRole(rawMessage.getRole()));
|
||||
message.setContent(rawMessage.getContent());
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private String buildSystemInstruction(AgentDefinition agent) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (StringUtils.hasText(agent.getSystemPrompt())) {
|
||||
builder.append(agent.getSystemPrompt().trim()).append("\n\n");
|
||||
}
|
||||
builder.append("请优先基于已给出的知识库引用片段回答。");
|
||||
builder.append("如果引用无法支持结论,请明确告知“知识库中暂无直接依据”。");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String buildContextText(List<RagChunkRecallResponse> recalls) {
|
||||
StringBuilder builder = new StringBuilder("以下是知识库召回片段:\n");
|
||||
for (int i = 0; i < recalls.size(); i++) {
|
||||
RagChunkRecallResponse recall = recalls.get(i);
|
||||
builder.append(i + 1)
|
||||
.append(". [chunkId=")
|
||||
.append(recall.getChunkId())
|
||||
.append(", score=")
|
||||
.append(String.format("%.4f", recall.getScore() == null ? 0D : recall.getScore()))
|
||||
.append("] ")
|
||||
.append(recall.getChunkContent())
|
||||
.append("\n");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private List<AgentChatResponse.ReferenceChunk> toReferenceChunks(List<RagChunkRecallResponse> recalls) {
|
||||
return recalls.stream().map(recall -> {
|
||||
AgentChatResponse.ReferenceChunk chunk = new AgentChatResponse.ReferenceChunk();
|
||||
chunk.setChunkId(recall.getChunkId());
|
||||
chunk.setDocumentId(recall.getDocumentId());
|
||||
chunk.setChunkContent(recall.getChunkContent());
|
||||
chunk.setScore(recall.getScore());
|
||||
return chunk;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧调试接口时,统一创建独立会话并沉淀会话编码,便于工作台后续追踪完整对话。
|
||||
*/
|
||||
private AgentSession createCompatibilitySession(Long agentId, AgentChatRequest request) {
|
||||
if (agentSessionService == null) {
|
||||
AgentSession session = new AgentSession();
|
||||
session.setId(0L);
|
||||
session.setAgentId(agentId);
|
||||
session.setSessionCode("chat_mock");
|
||||
session.setTitle(resolveLatestUserMessage(request.getMessages()));
|
||||
session.setStatus("ACTIVE");
|
||||
session.setMetadataJson("{\"source\":\"compat_chat\"}");
|
||||
session.setRemark("兼容聊天接口自动创建");
|
||||
return session;
|
||||
}
|
||||
AgentSessionCreateDTO createDTO = new AgentSessionCreateDTO();
|
||||
createDTO.setAgentId(agentId);
|
||||
createDTO.setSessionCode("chat_" + UUID.randomUUID().toString().replace("-", ""));
|
||||
createDTO.setTitle(resolveLatestUserMessage(request.getMessages()));
|
||||
createDTO.setMetadataJson("{\"source\":\"compat_chat\"}");
|
||||
createDTO.setRemark("兼容聊天接口自动创建");
|
||||
AgentSession session = agentSessionService.createSessionEntity(createDTO);
|
||||
agentSessionService.save(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
private void appendRequestMessages(Long sessionId, List<AgentChatRequest.AgentMessage> messages) {
|
||||
if (agentMessageService == null) {
|
||||
return;
|
||||
}
|
||||
for (AgentChatRequest.AgentMessage rawMessage : messages) {
|
||||
if (rawMessage == null || !StringUtils.hasText(rawMessage.getContent())) {
|
||||
continue;
|
||||
}
|
||||
AgentSessionMessageCreateDTO createDTO = new AgentSessionMessageCreateDTO();
|
||||
createDTO.setSessionId(sessionId);
|
||||
createDTO.setRole(normalizeRole(rawMessage.getRole()));
|
||||
createDTO.setContent(rawMessage.getContent().trim());
|
||||
createDTO.setCitationJson("[]");
|
||||
createDTO.setRemark("兼容聊天接口消息");
|
||||
AgentMessage entity = agentMessageService.appendMessageEntity(createDTO);
|
||||
agentMessageService.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private AgentMessage appendAssistantMessage(Long sessionId,
|
||||
List<RagChunkRecallResponse> recalls,
|
||||
ChatResult chatResult) {
|
||||
if (agentMessageService == null) {
|
||||
AgentMessage message = new AgentMessage();
|
||||
message.setSessionId(sessionId);
|
||||
message.setRole("assistant");
|
||||
message.setContent(chatResult.getContent());
|
||||
message.setCitationJson(toCitationJson(recalls));
|
||||
message.setTokenCount(chatResult.getCallLog() == null ? null : chatResult.getCallLog().getTotalTokens());
|
||||
message.setRemark("兼容聊天接口模型回复");
|
||||
return message;
|
||||
}
|
||||
AgentSessionMessageCreateDTO createDTO = new AgentSessionMessageCreateDTO();
|
||||
createDTO.setSessionId(sessionId);
|
||||
createDTO.setRole("assistant");
|
||||
createDTO.setContent(chatResult.getContent());
|
||||
createDTO.setCitationJson(toCitationJson(recalls));
|
||||
ModelCallLog callLog = chatResult.getCallLog();
|
||||
createDTO.setRequestId(callLog == null ? null : callLog.getRequestId());
|
||||
createDTO.setTokenCount(callLog == null ? null : callLog.getTotalTokens());
|
||||
createDTO.setRemark("兼容聊天接口模型回复");
|
||||
AgentMessage entity = agentMessageService.appendMessageEntity(createDTO);
|
||||
agentMessageService.save(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private String resolveRequestId(ChatResult chatResult) {
|
||||
ModelCallLog callLog = chatResult == null ? null : chatResult.getCallLog();
|
||||
return callLog == null ? null : callLog.getRequestId();
|
||||
}
|
||||
|
||||
private String toCitationJson(List<RagChunkRecallResponse> recalls) {
|
||||
if (recalls == null || recalls.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int i = 0; i < recalls.size(); i++) {
|
||||
RagChunkRecallResponse recall = recalls.get(i);
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append("{\"chunkId\":")
|
||||
.append(recall.getChunkId())
|
||||
.append(",\"documentId\":")
|
||||
.append(recall.getDocumentId())
|
||||
.append(",\"score\":")
|
||||
.append(recall.getScore() == null ? 0D : recall.getScore())
|
||||
.append('}');
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String normalizeRole(String role) {
|
||||
if (!StringUtils.hasText(role)) {
|
||||
return "user";
|
||||
}
|
||||
String normalized = role.trim().toLowerCase();
|
||||
if ("system".equals(normalized) || "assistant".equals(normalized) || "user".equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
|
||||
private String toVectorLiteral(List<Double> vector) {
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int index = 0; index < vector.size(); index++) {
|
||||
if (index > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(vector.get(index));
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.bruce.agent.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.factory.AgentMessageFactory;
|
||||
import com.bruce.agent.mapper.AgentMessageMapper;
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.vo.AgentMessageVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentMessageServiceImpl extends ServiceImpl<AgentMessageMapper, AgentMessage> implements IAgentMessageService {
|
||||
|
||||
private final IAgentSessionService agentSessionService;
|
||||
private final AgentMessageFactory agentMessageFactory;
|
||||
|
||||
@Override
|
||||
public boolean appendMessage(AgentSessionMessageCreateDTO request) {
|
||||
AgentMessage entity = appendMessageEntity(request);
|
||||
boolean saved = save(entity);
|
||||
log.info("Agent消息写入完成,sessionId={}, role={}, messageId={}, requestId={}, result={}",
|
||||
entity.getSessionId(), entity.getRole(), entity.getId(), request.getRequestId(), saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentMessage appendMessageEntity(AgentSessionMessageCreateDTO request) {
|
||||
validateRequest(request);
|
||||
AgentSession session = loadSession(request.getSessionId());
|
||||
if ("CLOSED".equals(session.getStatus())) {
|
||||
throw new IllegalArgumentException("会话已关闭,不能继续写入消息");
|
||||
}
|
||||
AgentMessage entity = loadFactory().toEntity(request);
|
||||
if (!StringUtils.hasText(entity.getCitationJson())) {
|
||||
entity.setCitationJson("[]");
|
||||
} else if (!entity.getCitationJson().trim().startsWith("[")) {
|
||||
throw new IllegalArgumentException("citationJson必须是数组结构");
|
||||
}
|
||||
log.info("Agent消息写入开始,sessionId={}, role={}, requestId={}",
|
||||
entity.getSessionId(), entity.getRole(), request.getRequestId());
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentMessageVO> listBySessionId(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
throw new IllegalArgumentException("会话ID不能为空");
|
||||
}
|
||||
List<AgentMessage> messages = lambdaQuery()
|
||||
.eq(AgentMessage::getSessionId, sessionId)
|
||||
.orderByAsc(AgentMessage::getCreateTime)
|
||||
.orderByAsc(AgentMessage::getId)
|
||||
.list();
|
||||
return loadFactory().toVOList(messages);
|
||||
}
|
||||
|
||||
public AgentSession loadSession(Long sessionId) {
|
||||
AgentSession session = agentSessionService.getById(sessionId);
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("会话不存在,ID: " + sessionId);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private void validateRequest(AgentSessionMessageCreateDTO request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("消息请求不能为空");
|
||||
}
|
||||
if (request.getSessionId() == null) {
|
||||
throw new IllegalArgumentException("会话ID不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getRole())) {
|
||||
throw new IllegalArgumentException("消息角色不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getContent())) {
|
||||
throw new IllegalArgumentException("消息内容不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private AgentMessageFactory loadFactory() {
|
||||
return agentMessageFactory == null ? new AgentMessageFactory() : agentMessageFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.bruce.agent.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.factory.AgentSessionFactory;
|
||||
import com.bruce.agent.mapper.AgentSessionMapper;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentSessionServiceImpl extends ServiceImpl<AgentSessionMapper, AgentSession> implements IAgentSessionService {
|
||||
|
||||
private static final String SESSION_STATUS_ACTIVE = "ACTIVE";
|
||||
private static final String SESSION_STATUS_CLOSED = "CLOSED";
|
||||
|
||||
private final ObjectProvider<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||
private final AgentSessionFactory agentSessionFactory;
|
||||
|
||||
@Override
|
||||
public boolean createSession(AgentSessionCreateDTO request) {
|
||||
AgentSession entity = createSessionEntity(request);
|
||||
boolean saved = save(entity);
|
||||
log.info("Agent会话创建完成,agentId={}, sessionCode={}, sessionId={}, result={}",
|
||||
entity.getAgentId(), entity.getSessionCode(), entity.getId(), saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentSession createSessionEntity(AgentSessionCreateDTO request) {
|
||||
validateCreateRequest(request);
|
||||
IAgentDefinitionService agentDefinitionService = agentDefinitionServiceProvider.getIfAvailable();
|
||||
if (agentDefinitionService == null) {
|
||||
throw new IllegalStateException("Agent定义服务未就绪,暂无法创建会话");
|
||||
}
|
||||
AgentDefinition agent = agentDefinitionService.getById(request.getAgentId());
|
||||
if (agent == null) {
|
||||
throw new IllegalArgumentException("Agent不存在,ID: " + request.getAgentId());
|
||||
}
|
||||
if (!EnableStatusEnum.ENABLED.name().equals(agent.getStatus())) {
|
||||
throw new IllegalArgumentException("Agent已停用,无法创建会话");
|
||||
}
|
||||
if (baseMapper != null) {
|
||||
AgentSession duplicate = lambdaQuery()
|
||||
.eq(AgentSession::getSessionCode, request.getSessionCode().trim())
|
||||
.one();
|
||||
if (duplicate != null) {
|
||||
throw new IllegalArgumentException("会话编码已存在: " + request.getSessionCode().trim());
|
||||
}
|
||||
}
|
||||
AgentSession entity = loadFactory().toEntity(request);
|
||||
entity.setStatus(SESSION_STATUS_ACTIVE);
|
||||
if (!StringUtils.hasText(entity.getMetadataJson())) {
|
||||
entity.setMetadataJson("{}");
|
||||
}
|
||||
log.info("Agent会话创建开始,agentId={}, sessionCode={}, workflowRunId={}",
|
||||
entity.getAgentId(), entity.getSessionCode(), entity.getWorkflowRunId());
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentSessionDetailVO> listByAgentId(Long agentId) {
|
||||
if (agentId == null) {
|
||||
throw new IllegalArgumentException("Agent ID不能为空");
|
||||
}
|
||||
List<AgentSession> sessions = lambdaQuery()
|
||||
.eq(AgentSession::getAgentId, agentId)
|
||||
.orderByDesc(AgentSession::getUpdateTime)
|
||||
.orderByDesc(AgentSession::getId)
|
||||
.list();
|
||||
return loadFactory().toDetailVOList(sessions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentSessionDetailVO getDetailById(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
throw new IllegalArgumentException("会话ID不能为空");
|
||||
}
|
||||
return loadFactory().toDetailVO(getById(sessionId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean closeSession(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
throw new IllegalArgumentException("会话ID不能为空");
|
||||
}
|
||||
AgentSession session = getById(sessionId);
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("会话不存在,ID: " + sessionId);
|
||||
}
|
||||
session.setStatus(SESSION_STATUS_CLOSED);
|
||||
boolean updated = updateById(session);
|
||||
log.info("Agent会话关闭完成,sessionId={}, sessionCode={}, result={}",
|
||||
session.getId(), session.getSessionCode(), updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private void validateCreateRequest(AgentSessionCreateDTO request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("创建会话请求不能为空");
|
||||
}
|
||||
if (request.getAgentId() == null) {
|
||||
throw new IllegalArgumentException("Agent ID不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getSessionCode())) {
|
||||
throw new IllegalArgumentException("会话编码不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private AgentSessionFactory loadFactory() {
|
||||
return agentSessionFactory == null ? new AgentSessionFactory() : agentSessionFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.bruce.agent.service.impl;
|
||||
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.service.IAgentWorkspaceService;
|
||||
import com.bruce.agent.vo.AgentMessageVO;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import com.bruce.agent.vo.AgentWorkspaceVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentWorkspaceServiceImpl implements IAgentWorkspaceService {
|
||||
|
||||
private final IAgentDefinitionService agentDefinitionService;
|
||||
private final IAgentSessionService agentSessionService;
|
||||
private final IAgentMessageService agentMessageService;
|
||||
|
||||
@Override
|
||||
public AgentWorkspaceVO getWorkspace(Long agentId, Long sessionId) {
|
||||
log.info("Agent工作台查询开始,agentId={}, sessionId={}", agentId, sessionId);
|
||||
if (agentId == null) {
|
||||
throw new IllegalArgumentException("Agent ID不能为空");
|
||||
}
|
||||
AgentDefinition agent = agentDefinitionService.getById(agentId);
|
||||
if (agent == null) {
|
||||
throw new IllegalArgumentException("Agent不存在,ID: " + agentId);
|
||||
}
|
||||
|
||||
List<AgentSessionDetailVO> sessions = agentSessionService.listByAgentId(agentId);
|
||||
AgentSessionDetailVO currentSession = resolveSession(sessionId, sessions);
|
||||
List<AgentMessageVO> messages = currentSession == null ? List.of() : agentMessageService.listBySessionId(currentSession.getId());
|
||||
|
||||
AgentWorkspaceVO workspace = new AgentWorkspaceVO();
|
||||
workspace.setAgentId(agent.getId());
|
||||
workspace.setAgentCode(agent.getAgentCode());
|
||||
workspace.setAgentName(agent.getAgentName());
|
||||
workspace.setStoreId(agent.getStoreId());
|
||||
workspace.setStatus(agent.getStatus());
|
||||
workspace.setSessions(sessions);
|
||||
workspace.setMessages(messages);
|
||||
|
||||
if (currentSession != null) {
|
||||
workspace.setSessionId(currentSession.getId());
|
||||
workspace.setSessionCode(currentSession.getSessionCode());
|
||||
workspace.setSessionTitle(currentSession.getTitle());
|
||||
workspace.setSessionStatus(currentSession.getStatus());
|
||||
workspace.setWorkflowRunId(currentSession.getWorkflowRunId());
|
||||
}
|
||||
|
||||
int totalTokens = messages.stream()
|
||||
.map(AgentMessageVO::getTokenCount)
|
||||
.filter(Objects::nonNull)
|
||||
.mapToInt(Integer::intValue)
|
||||
.sum();
|
||||
int citationCount = (int) messages.stream()
|
||||
.filter(message -> StringUtils.hasText(message.getCitationJson()) && message.getCitationJson().contains("chunkId"))
|
||||
.count();
|
||||
String latestRequestId = messages.stream()
|
||||
.map(AgentMessageVO::getRequestId)
|
||||
.filter(StringUtils::hasText)
|
||||
.reduce((first, second) -> second)
|
||||
.orElse(null);
|
||||
|
||||
workspace.setTotalTokens(totalTokens);
|
||||
workspace.setCitationCount(citationCount);
|
||||
workspace.setLatestRequestId(latestRequestId);
|
||||
log.info("Agent工作台查询结束,agentId={}, sessionId={}, messageCount={}, totalTokens={}",
|
||||
agentId, workspace.getSessionId(), messages.size(), totalTokens);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private AgentSessionDetailVO resolveSession(Long sessionId, List<AgentSessionDetailVO> sessions) {
|
||||
if (sessions == null || sessions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (sessionId == null) {
|
||||
return sessions.get(0);
|
||||
}
|
||||
return sessions.stream().filter(item -> sessionId.equals(item.getId())).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.bruce.agent.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class AgentMessageVO {
|
||||
private Long id;
|
||||
private Long sessionId;
|
||||
private String role;
|
||||
private String content;
|
||||
private String citationJson;
|
||||
private Integer tokenCount;
|
||||
private String requestId;
|
||||
private String remark;
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bruce.agent.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class AgentSessionDetailVO {
|
||||
private Long id;
|
||||
private Long agentId;
|
||||
private String sessionCode;
|
||||
private Long workflowRunId;
|
||||
private String title;
|
||||
private String status;
|
||||
private String metadataJson;
|
||||
private String remark;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.bruce.agent.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AgentWorkspaceVO {
|
||||
private Long agentId;
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private Long storeId;
|
||||
private String status;
|
||||
private Long sessionId;
|
||||
private String sessionCode;
|
||||
private String sessionTitle;
|
||||
private String sessionStatus;
|
||||
private Long workflowRunId;
|
||||
private Integer totalTokens;
|
||||
private Integer citationCount;
|
||||
private String latestRequestId;
|
||||
private List<AgentSessionDetailVO> sessions;
|
||||
private List<AgentMessageVO> messages;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.bruce.agent;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.agent.controller.AgentDefinitionController;
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.mapper.AgentDefinitionMapper;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.impl.AgentDefinitionServiceImpl;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AgentComponentStructureTests {
|
||||
|
||||
@Test
|
||||
void agentComponentsShouldReuseMybatisPlusBaseTypes() {
|
||||
assertTrue(BaseMapper.class.isAssignableFrom(AgentDefinitionMapper.class));
|
||||
assertTrue(IService.class.isAssignableFrom(IAgentDefinitionService.class));
|
||||
assertTrue(ServiceImpl.class.isAssignableFrom(AgentDefinitionServiceImpl.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentControllerShouldExposeRequestResultMethods() throws NoSuchMethodException {
|
||||
Method listMethod = AgentDefinitionController.class.getMethod("list");
|
||||
Method queryMethod = AgentDefinitionController.class.getMethod("query", AgentDefinitionQueryRequest.class);
|
||||
Method detailMethod = AgentDefinitionController.class.getMethod("detail", Long.class);
|
||||
Method saveMethod = AgentDefinitionController.class.getMethod("save", AgentDefinitionSaveRequest.class);
|
||||
Method deleteMethod = AgentDefinitionController.class.getMethod("delete", Long.class);
|
||||
Method chatMethod = AgentDefinitionController.class.getMethod("chat", Long.class, AgentChatRequest.class);
|
||||
|
||||
Method listServiceMethod = IAgentDefinitionService.class.getMethod("listResponses");
|
||||
Method queryServiceMethod = IAgentDefinitionService.class.getMethod("query", AgentDefinitionQueryRequest.class);
|
||||
Method detailServiceMethod = IAgentDefinitionService.class.getMethod("getResponseById", Long.class);
|
||||
Method saveServiceMethod = IAgentDefinitionService.class.getMethod("saveOrUpdate", AgentDefinitionSaveRequest.class);
|
||||
Method chatServiceMethod = IAgentDefinitionService.class.getMethod("chat", Long.class, AgentChatRequest.class);
|
||||
|
||||
assertEquals(RequestResult.class, listMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, queryMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, detailMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, saveMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, deleteMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, chatMethod.getReturnType());
|
||||
|
||||
assertEquals(List.class, listServiceMethod.getReturnType());
|
||||
assertEquals(List.class, queryServiceMethod.getReturnType());
|
||||
assertEquals(AgentDefinitionResponse.class, detailServiceMethod.getReturnType());
|
||||
assertEquals(boolean.class, saveServiceMethod.getReturnType());
|
||||
assertEquals(AgentChatResponse.class, chatServiceMethod.getReturnType());
|
||||
assertEquals(AgentDefinitionResponse.class, AgentDefinitionResponse.class.getMethod("fromEntity", AgentDefinition.class).getReturnType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.bruce.agent;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentChatRequest;
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.service.impl.AgentDefinitionServiceImpl;
|
||||
import com.bruce.modelprovider.entity.ModelCallLog;
|
||||
import com.bruce.modelprovider.entity.RagStoreModelConfig;
|
||||
import com.bruce.modelprovider.gateway.ChatRequest;
|
||||
import com.bruce.modelprovider.gateway.ChatResult;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingRequest;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingResult;
|
||||
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
|
||||
import com.bruce.rag.dto.response.RagChunkRecallResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.mapper.RagChunkEmbeddingMapper;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import com.bruce.modelprovider.gateway.ChatModelGateway;
|
||||
import com.bruce.modelprovider.gateway.EmbeddingModelGateway;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AgentDefinitionServiceImplTests {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private AgentDefinitionServiceImpl agentDefinitionService;
|
||||
|
||||
@Mock
|
||||
private IRagStoreService ragStoreService;
|
||||
|
||||
@Mock
|
||||
private IRagStoreModelConfigService ragStoreModelConfigService;
|
||||
|
||||
@Mock
|
||||
private RagChunkEmbeddingMapper ragChunkEmbeddingMapper;
|
||||
|
||||
@Mock
|
||||
private EmbeddingModelGateway embeddingModelGateway;
|
||||
|
||||
@Mock
|
||||
private ChatModelGateway chatModelGateway;
|
||||
|
||||
@Test
|
||||
void saveOrUpdateShouldValidateBoundStoreExists() {
|
||||
AgentDefinitionSaveRequest request = new AgentDefinitionSaveRequest();
|
||||
request.setAgentCode("A_1");
|
||||
request.setAgentName("Agent 1");
|
||||
request.setStoreId(1001L);
|
||||
|
||||
when(ragStoreService.getById(1001L)).thenReturn(null);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> agentDefinitionService.saveOrUpdate(request));
|
||||
assertTrue(exception.getMessage().contains("绑定知识库不存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldRejectDisabledAgent() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setStoreId(2001L);
|
||||
agent.setStatus("DISABLED");
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("你好");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> agentDefinitionService.chat(1001L, request));
|
||||
assertTrue(exception.getMessage().contains("停用"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldRejectAgentWithoutStore() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setStatus("ENABLED");
|
||||
agent.setStoreId(null);
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("你好");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> agentDefinitionService.chat(1001L, request));
|
||||
assertTrue(exception.getMessage().contains("未绑定知识库"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldUseStoreScopedRecallAndReturnAnswer() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setAgentCode("AGENT_1");
|
||||
agent.setAgentName("知识助手");
|
||||
agent.setSystemPrompt("你是企业知识助手");
|
||||
agent.setStoreId(2001L);
|
||||
agent.setStatus("ENABLED");
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
RagStore store = new RagStore();
|
||||
store.setId(2001L);
|
||||
store.setStoreName("企业知识库");
|
||||
when(ragStoreService.getById(2001L)).thenReturn(store);
|
||||
|
||||
RagStoreModelConfig modelConfig = new RagStoreModelConfig();
|
||||
modelConfig.setStoreId(2001L);
|
||||
modelConfig.setEmbeddingModelId(3001L);
|
||||
modelConfig.setEmbeddingDimension(1024);
|
||||
when(ragStoreModelConfigService.getActiveEntity(2001L)).thenReturn(modelConfig);
|
||||
|
||||
EmbeddingResult embeddingResult = new EmbeddingResult();
|
||||
embeddingResult.setVectors(List.of(List.of(0.12, 0.34, 0.56)));
|
||||
when(embeddingModelGateway.embed(any(EmbeddingRequest.class))).thenReturn(embeddingResult);
|
||||
|
||||
RagChunkRecallResponse recall = new RagChunkRecallResponse();
|
||||
recall.setChunkId(4001L);
|
||||
recall.setDocumentId(5001L);
|
||||
recall.setChunkContent("公司请假流程:先提交审批单。");
|
||||
recall.setScore(0.91);
|
||||
when(ragChunkEmbeddingMapper.queryTopKByStore(anyLong(), anyString(), anyInt()))
|
||||
.thenReturn(List.of(recall));
|
||||
|
||||
ModelCallLog callLog = new ModelCallLog();
|
||||
callLog.setRequestId("req_001");
|
||||
ChatResult chatResult = new ChatResult();
|
||||
chatResult.setContent("根据知识库,先在OA提交请假审批。");
|
||||
chatResult.setCallLog(callLog);
|
||||
when(chatModelGateway.chat(any(ChatRequest.class))).thenReturn(chatResult);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("请假流程是什么?");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
AgentChatResponse response = agentDefinitionService.chat(1001L, request);
|
||||
|
||||
assertEquals(1001L, response.getAgentId());
|
||||
assertEquals(2001L, response.getStoreId());
|
||||
assertEquals("企业知识库", response.getStoreName());
|
||||
assertEquals("根据知识库,先在OA提交请假审批。", response.getAnswer());
|
||||
assertEquals("req_001", response.getModelRequestId());
|
||||
assertEquals(1, response.getReferences().size());
|
||||
assertEquals(4001L, response.getReferences().getFirst().getChunkId());
|
||||
|
||||
ArgumentCaptor<EmbeddingRequest> embeddingRequestCaptor = ArgumentCaptor.forClass(EmbeddingRequest.class);
|
||||
verify(embeddingModelGateway).embed(embeddingRequestCaptor.capture());
|
||||
EmbeddingRequest embeddingRequest = embeddingRequestCaptor.getValue();
|
||||
assertEquals("RAG_QUERY_EMBEDDING", embeddingRequest.getTaskType());
|
||||
assertEquals("RAG_STORE", embeddingRequest.getMatchScope());
|
||||
assertEquals(2001L, embeddingRequest.getScopeId());
|
||||
|
||||
verify(ragChunkEmbeddingMapper).queryTopKByStore(anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatShouldSupportSimpleModeWithoutRagRecall() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1001L);
|
||||
agent.setAgentCode("AGENT_1");
|
||||
agent.setAgentName("知识助手");
|
||||
agent.setStoreId(2001L);
|
||||
agent.setStatus("ENABLED");
|
||||
doReturn(agent).when(agentDefinitionService).getById(1001L);
|
||||
|
||||
RagStore store = new RagStore();
|
||||
store.setId(2001L);
|
||||
store.setStoreName("企业知识库");
|
||||
when(ragStoreService.getById(2001L)).thenReturn(store);
|
||||
|
||||
ModelCallLog callLog = new ModelCallLog();
|
||||
callLog.setRequestId("req_simple_001");
|
||||
ChatResult chatResult = new ChatResult();
|
||||
chatResult.setContent("这是普通对话回答。");
|
||||
chatResult.setCallLog(callLog);
|
||||
when(chatModelGateway.chat(any(ChatRequest.class))).thenReturn(chatResult);
|
||||
|
||||
AgentChatRequest request = new AgentChatRequest();
|
||||
request.setRagEnabled(false);
|
||||
AgentChatRequest.AgentMessage message = new AgentChatRequest.AgentMessage();
|
||||
message.setRole("user");
|
||||
message.setContent("直接聊聊今天安排");
|
||||
request.setMessages(List.of(message));
|
||||
|
||||
AgentChatResponse response = agentDefinitionService.chat(1001L, request);
|
||||
|
||||
assertEquals("这是普通对话回答。", response.getAnswer());
|
||||
assertTrue(response.getReferences().isEmpty());
|
||||
verify(embeddingModelGateway, never()).embed(any(EmbeddingRequest.class));
|
||||
verify(ragChunkEmbeddingMapper, never()).queryTopKByStore(anyLong(), anyString(), anyInt());
|
||||
|
||||
ArgumentCaptor<ChatRequest> chatRequestCaptor = ArgumentCaptor.forClass(ChatRequest.class);
|
||||
verify(chatModelGateway).chat(chatRequestCaptor.capture());
|
||||
assertEquals("CHAT_SIMPLE", chatRequestCaptor.getValue().getTaskType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.bruce.agent.controller;
|
||||
|
||||
import com.bruce.agent.dto.response.AgentChatResponse;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.service.IAgentWorkspaceService;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import com.bruce.common.handler.GlobalExceptionHandler;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 验证 Agent 文档草案兼容路径。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AgentCompatControllerTests {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Mock
|
||||
private IAgentDefinitionService agentDefinitionService;
|
||||
|
||||
@Mock
|
||||
private IAgentSessionService agentSessionService;
|
||||
|
||||
@Mock
|
||||
private IAgentMessageService agentMessageService;
|
||||
|
||||
@Mock
|
||||
private IAgentWorkspaceService agentWorkspaceService;
|
||||
|
||||
@InjectMocks
|
||||
private AgentDefinitionController agentDefinitionController;
|
||||
|
||||
@InjectMocks
|
||||
private AgentSessionController agentSessionController;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(agentDefinitionController, agentSessionController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentRunCompatShouldReturnChatResponse() throws Exception {
|
||||
AgentChatResponse response = new AgentChatResponse();
|
||||
response.setAgentId(1001L);
|
||||
response.setAgentCode("presale_agent");
|
||||
response.setAnswer("这是兼容运行入口返回的答案");
|
||||
response.setModelRequestId("req-1001");
|
||||
|
||||
when(agentDefinitionService.chat(org.mockito.ArgumentMatchers.eq(1001L), any())).thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/api/agents/1001/runs")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "请总结合同重点"
|
||||
}
|
||||
],
|
||||
"ragEnabled": true
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.resultcode").value("0"))
|
||||
.andExpect(jsonPath("$.data.agentCode").value("presale_agent"))
|
||||
.andExpect(jsonPath("$.data.modelRequestId").value("req-1001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionsCompatShouldReturnStructuredSessionList() throws Exception {
|
||||
AgentSessionDetailVO session = new AgentSessionDetailVO();
|
||||
session.setId(2001L);
|
||||
session.setAgentId(1001L);
|
||||
session.setSessionCode("session_001");
|
||||
session.setStatus("ACTIVE");
|
||||
|
||||
when(agentSessionService.listByAgentId(1001L)).thenReturn(List.of(session));
|
||||
|
||||
mockMvc.perform(get("/api/agent-sessions/agents/1001/sessions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.resultcode").value("0"))
|
||||
.andExpect(jsonPath("$.data[0].sessionCode").value("session_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionDetailCompatShouldReturnStructuredDetail() throws Exception {
|
||||
AgentSessionDetailVO session = new AgentSessionDetailVO();
|
||||
session.setId(2001L);
|
||||
session.setSessionCode("session_001");
|
||||
session.setStatus("ACTIVE");
|
||||
|
||||
when(agentSessionService.getDetailById(2001L)).thenReturn(session);
|
||||
|
||||
mockMvc.perform(get("/api/agent-sessions/2001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.resultcode").value("0"))
|
||||
.andExpect(jsonPath("$.data.sessionCode").value("session_001"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.bruce.agent.controller;
|
||||
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.service.IAgentWorkspaceService;
|
||||
import com.bruce.agent.vo.AgentWorkspaceVO;
|
||||
import com.bruce.common.handler.GlobalExceptionHandler;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 验证 Agent 工作台聚合接口的查询参数绑定和返回结构。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AgentSessionControllerTests {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Mock
|
||||
private IAgentSessionService agentSessionService;
|
||||
|
||||
@Mock
|
||||
private IAgentMessageService agentMessageService;
|
||||
|
||||
@Mock
|
||||
private IAgentWorkspaceService agentWorkspaceService;
|
||||
|
||||
@InjectMocks
|
||||
private AgentSessionController agentSessionController;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(agentSessionController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void workspaceShouldReturnStructuredAggregateView() throws Exception {
|
||||
AgentWorkspaceVO workspace = new AgentWorkspaceVO();
|
||||
workspace.setAgentId(1001L);
|
||||
workspace.setAgentCode("presale_agent");
|
||||
workspace.setAgentName("售前问答 Agent");
|
||||
workspace.setSessionId(2001L);
|
||||
workspace.setSessionCode("session_001");
|
||||
workspace.setLatestRequestId("req-1001");
|
||||
workspace.setCitationCount(2);
|
||||
|
||||
when(agentWorkspaceService.getWorkspace(1001L, null)).thenReturn(workspace);
|
||||
|
||||
mockMvc.perform(get("/api/agent-sessions/workspace").param("agentId", "1001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.resultcode").value("0"))
|
||||
.andExpect(jsonPath("$.data.agentId").value(1001))
|
||||
.andExpect(jsonPath("$.data.agentName").value("售前问答 Agent"))
|
||||
.andExpect(jsonPath("$.data.latestRequestId").value("req-1001"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.bruce.agent.factory;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AgentFactoryTests {
|
||||
|
||||
private final AgentDefinitionFactory agentDefinitionFactory = new AgentDefinitionFactory();
|
||||
private final AgentSessionFactory agentSessionFactory = new AgentSessionFactory();
|
||||
private final AgentMessageFactory agentMessageFactory = new AgentMessageFactory();
|
||||
|
||||
@Test
|
||||
void agentDefinitionFactoryShouldTrimRequestAndBuildResponse() {
|
||||
AgentDefinitionSaveRequest request = new AgentDefinitionSaveRequest();
|
||||
request.setId(1L);
|
||||
request.setAgentCode(" AGENT_RAG_HELPER ");
|
||||
request.setAgentName(" 知识问答助手 ");
|
||||
request.setSystemPrompt(" 你是企业知识助手 ");
|
||||
request.setStoreId(1001L);
|
||||
request.setStatus(" ENABLED ");
|
||||
request.setRemark(" 默认Agent ");
|
||||
|
||||
AgentDefinition entity = agentDefinitionFactory.toEntity(request);
|
||||
assertEquals("AGENT_RAG_HELPER", entity.getAgentCode());
|
||||
assertEquals("知识问答助手", entity.getAgentName());
|
||||
assertEquals("你是企业知识助手", entity.getSystemPrompt());
|
||||
assertEquals("ENABLED", entity.getStatus());
|
||||
assertEquals("默认Agent", entity.getRemark());
|
||||
|
||||
AgentDefinitionResponse response = agentDefinitionFactory.toResponse(entity);
|
||||
assertEquals(1001L, response.getStoreId());
|
||||
assertEquals("AGENT_RAG_HELPER", response.getAgentCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentSessionFactoryShouldBuildSessionEntityAndDetailView() {
|
||||
AgentSessionCreateDTO request = new AgentSessionCreateDTO();
|
||||
request.setAgentId(1L);
|
||||
request.setSessionCode(" session_001 ");
|
||||
request.setWorkflowRunId(2001L);
|
||||
request.setTitle(" 产品问答会话 ");
|
||||
request.setMetadataJson(" {\"source\":\"debug\"} ");
|
||||
request.setRemark(" 首轮调试 ");
|
||||
|
||||
AgentSession entity = agentSessionFactory.toEntity(request);
|
||||
assertNotNull(entity);
|
||||
assertEquals("session_001", entity.getSessionCode());
|
||||
assertEquals("产品问答会话", entity.getTitle());
|
||||
assertEquals("{\"source\":\"debug\"}", entity.getMetadataJson());
|
||||
assertEquals("首轮调试", entity.getRemark());
|
||||
|
||||
AgentSessionDetailVO detailVO = agentSessionFactory.toDetailVO(entity);
|
||||
assertEquals(1L, detailVO.getAgentId());
|
||||
assertEquals("session_001", detailVO.getSessionCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentMessageFactoryShouldBuildMessageEntityAndPreserveCitationJson() {
|
||||
AgentSessionMessageCreateDTO request = new AgentSessionMessageCreateDTO();
|
||||
request.setSessionId(10L);
|
||||
request.setRole(" assistant ");
|
||||
request.setContent(" 这里是回答内容 ");
|
||||
request.setCitationJson(" [{\"chunkId\":1}] ");
|
||||
request.setTokenCount(256);
|
||||
request.setRemark(" 回答成功 ");
|
||||
|
||||
AgentMessage entity = agentMessageFactory.toEntity(request);
|
||||
assertEquals(10L, entity.getSessionId());
|
||||
assertEquals("assistant", entity.getRole());
|
||||
assertEquals("这里是回答内容", entity.getContent());
|
||||
assertEquals("[{\"chunkId\":1}]", entity.getCitationJson());
|
||||
assertEquals(256, entity.getTokenCount());
|
||||
assertTrue(entity.getRemark().contains("回答成功"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.bruce.agent.session;
|
||||
|
||||
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
|
||||
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.impl.AgentMessageServiceImpl;
|
||||
import com.bruce.agent.service.impl.AgentSessionServiceImpl;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AgentSessionServiceTests {
|
||||
|
||||
@Mock
|
||||
private IAgentDefinitionService agentDefinitionService;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private AgentSessionServiceImpl agentSessionService;
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private AgentMessageServiceImpl agentMessageService;
|
||||
|
||||
@Test
|
||||
void createSessionShouldRejectDisabledAgent() {
|
||||
when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService);
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1L);
|
||||
agent.setStatus("DISABLED");
|
||||
when(agentDefinitionService.getById(1L)).thenReturn(agent);
|
||||
|
||||
AgentSessionCreateDTO request = new AgentSessionCreateDTO();
|
||||
request.setAgentId(1L);
|
||||
request.setSessionCode("session_001");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> agentSessionService.createSession(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSessionShouldPersistActiveSession() {
|
||||
when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService);
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1L);
|
||||
agent.setStatus(EnableStatusEnum.ENABLED.name());
|
||||
when(agentDefinitionService.getById(1L)).thenReturn(agent);
|
||||
doAnswer(invocation -> true).when(agentSessionService).save(any(AgentSession.class));
|
||||
|
||||
AgentSessionCreateDTO request = new AgentSessionCreateDTO();
|
||||
request.setAgentId(1L);
|
||||
request.setSessionCode("session_001");
|
||||
request.setTitle("产品问答");
|
||||
request.setMetadataJson("{\"source\":\"debug\"}");
|
||||
|
||||
boolean result = agentSessionService.createSession(request);
|
||||
assertTrue(result);
|
||||
|
||||
ArgumentCaptor<AgentSession> captor = ArgumentCaptor.forClass(AgentSession.class);
|
||||
verify(agentSessionService).save(captor.capture());
|
||||
assertEquals("session_001", captor.getValue().getSessionCode());
|
||||
assertEquals("ACTIVE", captor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendMessageShouldRejectClosedSession() {
|
||||
AgentSession session = new AgentSession();
|
||||
session.setId(10L);
|
||||
session.setStatus("CLOSED");
|
||||
doReturn(session).when(agentMessageService).loadSession(10L);
|
||||
|
||||
AgentSessionMessageCreateDTO request = new AgentSessionMessageCreateDTO();
|
||||
request.setSessionId(10L);
|
||||
request.setRole("user");
|
||||
request.setContent("你好");
|
||||
request.setCitationJson("[]");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> agentMessageService.appendMessage(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendMessageShouldPersistCitationJson() {
|
||||
AgentSession session = new AgentSession();
|
||||
session.setId(10L);
|
||||
session.setStatus("ACTIVE");
|
||||
doReturn(session).when(agentMessageService).loadSession(10L);
|
||||
doAnswer(invocation -> true).when(agentMessageService).save(any(AgentMessage.class));
|
||||
|
||||
AgentSessionMessageCreateDTO request = new AgentSessionMessageCreateDTO();
|
||||
request.setSessionId(10L);
|
||||
request.setRole("assistant");
|
||||
request.setContent("这里是回答");
|
||||
request.setCitationJson("[{\"chunkId\":1}]");
|
||||
request.setTokenCount(128);
|
||||
|
||||
boolean result = agentMessageService.appendMessage(request);
|
||||
assertTrue(result);
|
||||
|
||||
ArgumentCaptor<AgentMessage> captor = ArgumentCaptor.forClass(AgentMessage.class);
|
||||
verify(agentMessageService).save(captor.capture());
|
||||
assertEquals("[{\"chunkId\":1}]", captor.getValue().getCitationJson());
|
||||
assertEquals(128, captor.getValue().getTokenCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.bruce.agent.workspace;
|
||||
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.agent.service.IAgentMessageService;
|
||||
import com.bruce.agent.service.IAgentSessionService;
|
||||
import com.bruce.agent.service.impl.AgentWorkspaceServiceImpl;
|
||||
import com.bruce.agent.vo.AgentMessageVO;
|
||||
import com.bruce.agent.vo.AgentSessionDetailVO;
|
||||
import com.bruce.agent.vo.AgentWorkspaceVO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AgentWorkspaceServiceTests {
|
||||
|
||||
@Mock
|
||||
private IAgentDefinitionService agentDefinitionService;
|
||||
|
||||
@Mock
|
||||
private IAgentSessionService agentSessionService;
|
||||
|
||||
@Mock
|
||||
private IAgentMessageService agentMessageService;
|
||||
|
||||
@InjectMocks
|
||||
private AgentWorkspaceServiceImpl agentWorkspaceService;
|
||||
|
||||
@Test
|
||||
void getWorkspaceShouldAggregateAgentSessionAndMessages() {
|
||||
AgentDefinition agent = new AgentDefinition();
|
||||
agent.setId(1L);
|
||||
agent.setAgentCode("AGENT_RAG_HELPER");
|
||||
agent.setAgentName("知识问答助手");
|
||||
agent.setStoreId(1001L);
|
||||
agent.setStatus("ENABLED");
|
||||
|
||||
AgentSessionDetailVO session = new AgentSessionDetailVO();
|
||||
session.setId(10L);
|
||||
session.setAgentId(1L);
|
||||
session.setSessionCode("session_001");
|
||||
session.setStatus("ACTIVE");
|
||||
session.setTitle("产品问答");
|
||||
session.setWorkflowRunId(2001L);
|
||||
|
||||
AgentMessageVO userMessage = new AgentMessageVO();
|
||||
userMessage.setRole("user");
|
||||
userMessage.setContent("产品支持哪些模型?");
|
||||
AgentMessageVO assistantMessage = new AgentMessageVO();
|
||||
assistantMessage.setRole("assistant");
|
||||
assistantMessage.setContent("当前支持 OpenAI Compatible 协议模型。");
|
||||
assistantMessage.setCitationJson("[{\"chunkId\":1}]");
|
||||
assistantMessage.setTokenCount(256);
|
||||
assistantMessage.setRequestId("req-001");
|
||||
|
||||
when(agentDefinitionService.getById(1L)).thenReturn(agent);
|
||||
when(agentSessionService.listByAgentId(1L)).thenReturn(List.of(session));
|
||||
when(agentMessageService.listBySessionId(10L)).thenReturn(List.of(userMessage, assistantMessage));
|
||||
|
||||
AgentWorkspaceVO workspace = agentWorkspaceService.getWorkspace(1L, 10L);
|
||||
|
||||
assertNotNull(workspace);
|
||||
assertEquals("AGENT_RAG_HELPER", workspace.getAgentCode());
|
||||
assertEquals("知识问答助手", workspace.getAgentName());
|
||||
assertEquals(10L, workspace.getSessionId());
|
||||
assertEquals("session_001", workspace.getSessionCode());
|
||||
assertEquals(2, workspace.getMessages().size());
|
||||
assertEquals(1, workspace.getCitationCount());
|
||||
assertEquals(256, workspace.getTotalTokens());
|
||||
assertEquals("req-001", workspace.getLatestRequestId());
|
||||
}
|
||||
}
|
||||
85
common-agent-boot/pom.xml
Normal file
85
common-agent-boot/pom.xml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>common-agent-boot</artifactId>
|
||||
<name>common-agent-boot</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-rag</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-modelprovider</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-agent</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-workflow</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-mcp</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-skill</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-observability</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,11 +1,22 @@
|
||||
package com.bruce;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@MapperScan(basePackages = {
|
||||
"com.bruce.common.mapper",
|
||||
"com.bruce.rag.mapper",
|
||||
"com.bruce.modelprovider.mapper",
|
||||
"com.bruce.agent.mapper",
|
||||
"com.bruce.workflow.mapper",
|
||||
"com.bruce.mcp.mapper",
|
||||
"com.bruce.skill.mapper",
|
||||
"com.bruce.observability.mapper"
|
||||
})
|
||||
public class CommonAgentApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bruce.dashboard.controller;
|
||||
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import com.bruce.dashboard.service.IStudioDashboardService;
|
||||
import com.bruce.dashboard.vo.StudioDashboardVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Studio 首页聚合接口。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/studio/dashboard")
|
||||
public class StudioDashboardController {
|
||||
|
||||
private final IStudioDashboardService studioDashboardService;
|
||||
|
||||
@GetMapping
|
||||
public RequestResult<StudioDashboardVO> detail() {
|
||||
log.info("Studio 首页总览查询开始");
|
||||
StudioDashboardVO dashboard = studioDashboardService.getDashboard();
|
||||
log.info("Studio 首页总览查询结束,projectName={}, recentRunCount={}",
|
||||
dashboard.getProjectName(), dashboard.getRecentRuns().size());
|
||||
return RequestResult.success(dashboard);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.bruce.dashboard.service;
|
||||
|
||||
import com.bruce.dashboard.vo.StudioDashboardVO;
|
||||
|
||||
/**
|
||||
* Studio 总览工作台聚合服务。
|
||||
*/
|
||||
public interface IStudioDashboardService {
|
||||
|
||||
/**
|
||||
* 汇总当前项目的发布旅程、运行摘要和风险提示。
|
||||
*/
|
||||
StudioDashboardVO getDashboard();
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.bruce.dashboard.service.impl;
|
||||
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.dashboard.service.IStudioDashboardService;
|
||||
import com.bruce.dashboard.vo.StudioDashboardChecklistItemVO;
|
||||
import com.bruce.dashboard.vo.StudioDashboardLifecycleStepVO;
|
||||
import com.bruce.dashboard.vo.StudioDashboardMetricsVO;
|
||||
import com.bruce.dashboard.vo.StudioDashboardRecentRunVO;
|
||||
import com.bruce.dashboard.vo.StudioDashboardVO;
|
||||
import com.bruce.mcp.service.IMcpServerService;
|
||||
import com.bruce.modelprovider.service.IModelWorkspaceService;
|
||||
import com.bruce.modelprovider.vo.ModelWorkspaceVO;
|
||||
import com.bruce.observability.service.IObservabilityRunService;
|
||||
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
|
||||
import com.bruce.rag.service.IKnowledgeWorkspaceService;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import com.bruce.rag.vo.KnowledgeWorkspaceVO;
|
||||
import com.bruce.skill.service.ISkillDefinitionService;
|
||||
import com.bruce.workflow.service.IProjectService;
|
||||
import com.bruce.workflow.service.IWorkflowDefinitionService;
|
||||
import com.bruce.workflow.vo.ProjectVO;
|
||||
import com.bruce.workflow.vo.WorkflowDefinitionVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Studio 首页聚合实现。
|
||||
* <p>
|
||||
* 该服务只汇总现有模块已经稳定的主数据和运行摘要,不引入新的存储表。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StudioDashboardServiceImpl implements IStudioDashboardService {
|
||||
|
||||
private final IProjectService projectService;
|
||||
private final IWorkflowDefinitionService workflowDefinitionService;
|
||||
private final IObservabilityRunService observabilityRunService;
|
||||
private final IModelWorkspaceService modelWorkspaceService;
|
||||
private final IRagStoreService ragStoreService;
|
||||
private final IKnowledgeWorkspaceService knowledgeWorkspaceService;
|
||||
private final ObjectProvider<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||
private final IMcpServerService mcpServerService;
|
||||
private final ISkillDefinitionService skillDefinitionService;
|
||||
|
||||
@Override
|
||||
public StudioDashboardVO getDashboard() {
|
||||
log.info("Studio 首页聚合开始");
|
||||
List<ProjectVO> projects = projectService.listProjects();
|
||||
ProjectVO currentProject = projects.isEmpty() ? null : projects.get(0);
|
||||
List<WorkflowDefinitionVO> workflows = currentProject == null ? List.of() : workflowDefinitionService.listByProjectId(currentProject.getId());
|
||||
List<ObservabilityRunSummaryVO> recentRuns = observabilityRunService.listRecentRuns();
|
||||
ModelWorkspaceVO modelWorkspace = modelWorkspaceService.getWorkspace();
|
||||
IAgentDefinitionService agentDefinitionService = agentDefinitionServiceProvider.getIfAvailable();
|
||||
List<AgentDefinitionResponse> agents = agentDefinitionService == null ? List.of() : agentDefinitionService.listResponses();
|
||||
int mcpServerCount = mcpServerService.listServers().size();
|
||||
int skillCount = skillDefinitionService.listDefinitions().size();
|
||||
|
||||
KnowledgeWorkspaceVO knowledgeWorkspace = null;
|
||||
if (!ragStoreService.listResponses().isEmpty()) {
|
||||
Long firstStoreId = ragStoreService.listResponses().get(0).getId();
|
||||
knowledgeWorkspace = knowledgeWorkspaceService.getWorkspace(firstStoreId);
|
||||
}
|
||||
|
||||
StudioDashboardVO dashboard = new StudioDashboardVO();
|
||||
dashboard.setProjectName(currentProject == null ? "Common Agent Studio" : currentProject.getProjectName());
|
||||
dashboard.setEnvironment(currentProject == null ? "Dev" : currentProject.getEnvironment());
|
||||
dashboard.setPublishStatus(currentProject == null ? "DRAFT" : currentProject.getPublishStatus());
|
||||
dashboard.setLifecycleSteps(buildLifecycleSteps(knowledgeWorkspace, workflows, agents, recentRuns));
|
||||
dashboard.setReadinessChecklist(buildChecklist(knowledgeWorkspace, workflows, agents, modelWorkspace, mcpServerCount, skillCount));
|
||||
dashboard.setMetrics(buildMetrics(recentRuns));
|
||||
dashboard.setRecentRuns(buildRecentRuns(recentRuns, workflows, agents));
|
||||
dashboard.setWarningTitle(buildWarningTitle(modelWorkspace, workflows));
|
||||
dashboard.setWarningMessage(buildWarningMessage(modelWorkspace, workflows, knowledgeWorkspace));
|
||||
log.info("Studio 首页聚合结束,projectName={}, workflowCount={}, runCount={}",
|
||||
dashboard.getProjectName(), workflows.size(), recentRuns.size());
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
private List<StudioDashboardLifecycleStepVO> buildLifecycleSteps(KnowledgeWorkspaceVO knowledgeWorkspace,
|
||||
List<WorkflowDefinitionVO> workflows,
|
||||
List<AgentDefinitionResponse> agents,
|
||||
List<ObservabilityRunSummaryVO> recentRuns) {
|
||||
List<StudioDashboardLifecycleStepVO> steps = new ArrayList<>();
|
||||
steps.add(step("知识接入", "上传、解析、切片、向量化", knowledgeWorkspace != null && knowledgeWorkspace.getDocumentCount() > 0 ? "done" : "idle"));
|
||||
steps.add(step("能力编排", "Workflow 连接模型、工具与 Skill", workflows.isEmpty() ? "idle" : "running"));
|
||||
steps.add(step("对话调试", "验证引用、成本、延迟与回答质量", agents.isEmpty() ? "idle" : "running"));
|
||||
steps.add(step("发布观测", "版本快照、运行追踪、异常排查", recentRuns.isEmpty() ? "idle" : "done"));
|
||||
return steps;
|
||||
}
|
||||
|
||||
private List<StudioDashboardChecklistItemVO> buildChecklist(KnowledgeWorkspaceVO knowledgeWorkspace,
|
||||
List<WorkflowDefinitionVO> workflows,
|
||||
List<AgentDefinitionResponse> agents,
|
||||
ModelWorkspaceVO modelWorkspace,
|
||||
int mcpServerCount,
|
||||
int skillCount) {
|
||||
List<StudioDashboardChecklistItemVO> items = new ArrayList<>();
|
||||
items.add(checkItem("知识库已绑定 Embedding 模型", knowledgeWorkspace != null && knowledgeWorkspace.getEmbeddingModelId() != null));
|
||||
items.add(checkItem("Workflow 已存在可编辑草稿", !workflows.isEmpty()));
|
||||
items.add(checkItem("Agent 已绑定默认知识库与能力", !agents.isEmpty()));
|
||||
items.add(checkItem("MCP / Skill 基础能力已接入", mcpServerCount > 0 && skillCount > 0));
|
||||
items.add(checkItem("模型路由已配置至少一个启用规则", modelWorkspace.getEnabledRouteRuleCount() != null && modelWorkspace.getEnabledRouteRuleCount() > 0));
|
||||
return items;
|
||||
}
|
||||
|
||||
private StudioDashboardMetricsVO buildMetrics(List<ObservabilityRunSummaryVO> recentRuns) {
|
||||
StudioDashboardMetricsVO metrics = new StudioDashboardMetricsVO();
|
||||
metrics.setTodayRunCount(recentRuns.size());
|
||||
long successCount = recentRuns.stream().filter(run -> "SUCCESS".equals(run.getStatus())).count();
|
||||
double successRate = recentRuns.isEmpty() ? 100D : successCount * 100.0 / recentRuns.size();
|
||||
metrics.setSuccessRate(roundDouble(successRate));
|
||||
metrics.setP50Latency(formatP50Latency(recentRuns));
|
||||
metrics.setEstimatedCost(formatCost(recentRuns));
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private List<StudioDashboardRecentRunVO> buildRecentRuns(List<ObservabilityRunSummaryVO> recentRuns,
|
||||
List<WorkflowDefinitionVO> workflows,
|
||||
List<AgentDefinitionResponse> agents) {
|
||||
return recentRuns.stream().limit(5).map(run -> {
|
||||
StudioDashboardRecentRunVO item = new StudioDashboardRecentRunVO();
|
||||
item.setId(run.getRequestId());
|
||||
item.setName(resolveRunName(run.getWorkflowId(), workflows, agents));
|
||||
item.setType(run.getWorkflowId() == null ? "Agent" : "Workflow");
|
||||
item.setStatus(formatRunStatus(run.getStatus()));
|
||||
item.setLatency(formatDuration(run.getDurationMs()));
|
||||
item.setCost("¥" + roundBigDecimal(run.getEstimatedCost() == null ? BigDecimal.ZERO : run.getEstimatedCost()));
|
||||
return item;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private String buildWarningTitle(ModelWorkspaceVO modelWorkspace, List<WorkflowDefinitionVO> workflows) {
|
||||
if (modelWorkspace.getEnabledRouteRuleCount() == null || modelWorkspace.getEnabledRouteRuleCount() == 0) {
|
||||
return "发布前仍需补齐模型路由";
|
||||
}
|
||||
if (workflows.isEmpty()) {
|
||||
return "发布前仍需创建至少一个 Workflow";
|
||||
}
|
||||
return "生产发布前仍需确认路由兜底";
|
||||
}
|
||||
|
||||
private String buildWarningMessage(ModelWorkspaceVO modelWorkspace,
|
||||
List<WorkflowDefinitionVO> workflows,
|
||||
KnowledgeWorkspaceVO knowledgeWorkspace) {
|
||||
if (modelWorkspace.getRecentFailedCallCount() != null && modelWorkspace.getRecentFailedCallCount() > 0) {
|
||||
return "最近存在失败模型调用,建议先补齐 fallback 模型并复核错误上下文。";
|
||||
}
|
||||
if (knowledgeWorkspace != null && knowledgeWorkspace.getPendingTaskCount() != null && knowledgeWorkspace.getPendingTaskCount() > 0) {
|
||||
return "当前知识库仍有待索引文档,建议完成索引后再进行发布联调。";
|
||||
}
|
||||
if (workflows.isEmpty()) {
|
||||
return "当前项目尚无可试跑 Workflow,建议先完成最小链路编排。";
|
||||
}
|
||||
return "AGENT_PLAN 任务建议补齐 fallback 模型和最大延迟阈值后再发布。";
|
||||
}
|
||||
|
||||
private StudioDashboardLifecycleStepVO step(String name, String description, String status) {
|
||||
StudioDashboardLifecycleStepVO step = new StudioDashboardLifecycleStepVO();
|
||||
step.setName(name);
|
||||
step.setDescription(description);
|
||||
step.setStatus(status);
|
||||
return step;
|
||||
}
|
||||
|
||||
private StudioDashboardChecklistItemVO checkItem(String label, boolean done) {
|
||||
StudioDashboardChecklistItemVO item = new StudioDashboardChecklistItemVO();
|
||||
item.setLabel(label);
|
||||
item.setDone(done);
|
||||
return item;
|
||||
}
|
||||
|
||||
private String resolveRunName(Long workflowId, List<WorkflowDefinitionVO> workflows, List<AgentDefinitionResponse> agents) {
|
||||
if (workflowId != null) {
|
||||
return workflows.stream()
|
||||
.filter(workflow -> workflowId.equals(workflow.getId()))
|
||||
.map(WorkflowDefinitionVO::getWorkflowName)
|
||||
.findFirst()
|
||||
.orElse("Workflow 运行");
|
||||
}
|
||||
return agents.isEmpty() ? "Agent 调试会话" : agents.get(0).getAgentName();
|
||||
}
|
||||
|
||||
private String formatRunStatus(String status) {
|
||||
if ("SUCCESS".equals(status)) {
|
||||
return "成功";
|
||||
}
|
||||
if ("FAILED".equals(status)) {
|
||||
return "失败";
|
||||
}
|
||||
if ("RUNNING".equals(status)) {
|
||||
return "运行中";
|
||||
}
|
||||
return status == null ? "-" : status;
|
||||
}
|
||||
|
||||
private String formatDuration(Integer durationMs) {
|
||||
if (durationMs == null) {
|
||||
return "-";
|
||||
}
|
||||
if (durationMs >= 1000) {
|
||||
return roundBigDecimal(BigDecimal.valueOf(durationMs).divide(BigDecimal.valueOf(1000), 2, RoundingMode.HALF_UP)) + "s";
|
||||
}
|
||||
return durationMs + "ms";
|
||||
}
|
||||
|
||||
private String formatP50Latency(List<ObservabilityRunSummaryVO> recentRuns) {
|
||||
if (recentRuns.isEmpty()) {
|
||||
return "-";
|
||||
}
|
||||
List<Integer> durations = recentRuns.stream()
|
||||
.map(ObservabilityRunSummaryVO::getDurationMs)
|
||||
.filter(value -> value != null && value > 0)
|
||||
.sorted()
|
||||
.toList();
|
||||
if (durations.isEmpty()) {
|
||||
return "-";
|
||||
}
|
||||
Integer p50 = durations.get(durations.size() / 2);
|
||||
return formatDuration(p50);
|
||||
}
|
||||
|
||||
private String formatCost(List<ObservabilityRunSummaryVO> recentRuns) {
|
||||
BigDecimal total = recentRuns.stream()
|
||||
.map(ObservabilityRunSummaryVO::getEstimatedCost)
|
||||
.filter(cost -> cost != null && cost.compareTo(BigDecimal.ZERO) > 0)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return "¥" + roundBigDecimal(total);
|
||||
}
|
||||
|
||||
private Double roundDouble(double value) {
|
||||
return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
private String roundBigDecimal(BigDecimal value) {
|
||||
return value.setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.bruce.dashboard.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Studio 发布就绪项。
|
||||
*/
|
||||
@Data
|
||||
public class StudioDashboardChecklistItemVO {
|
||||
private String label;
|
||||
private Boolean done;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bruce.dashboard.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Studio 总览生命周期步骤。
|
||||
*/
|
||||
@Data
|
||||
public class StudioDashboardLifecycleStepVO {
|
||||
private String name;
|
||||
private String description;
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.bruce.dashboard.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Studio 运行指标摘要。
|
||||
*/
|
||||
@Data
|
||||
public class StudioDashboardMetricsVO {
|
||||
private Integer todayRunCount;
|
||||
private Double successRate;
|
||||
private String p50Latency;
|
||||
private String estimatedCost;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bruce.dashboard.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Studio 最近运行摘要。
|
||||
*/
|
||||
@Data
|
||||
public class StudioDashboardRecentRunVO {
|
||||
private String id;
|
||||
private String name;
|
||||
private String type;
|
||||
private String status;
|
||||
private String latency;
|
||||
private String cost;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bruce.dashboard.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Studio 首页聚合视图。
|
||||
*/
|
||||
@Data
|
||||
public class StudioDashboardVO {
|
||||
private String projectName;
|
||||
private String environment;
|
||||
private String publishStatus;
|
||||
private List<StudioDashboardLifecycleStepVO> lifecycleSteps = new ArrayList<>();
|
||||
private List<StudioDashboardChecklistItemVO> readinessChecklist = new ArrayList<>();
|
||||
private StudioDashboardMetricsVO metrics;
|
||||
private List<StudioDashboardRecentRunVO> recentRuns = new ArrayList<>();
|
||||
private String warningTitle;
|
||||
private String warningMessage;
|
||||
}
|
||||
5
common-agent-boot/src/main/resources/ai-config.ini
Normal file
5
common-agent-boot/src/main/resources/ai-config.ini
Normal file
@@ -0,0 +1,5 @@
|
||||
# AI 独立配置文件(建议仅本地/环境覆盖使用,不提交真实密钥)
|
||||
# 格式:ai.secret-refs[<secret_ref>]=<api_key>
|
||||
|
||||
ai.secret-refs[SILICONFLOW_API_KEY]=your-key
|
||||
|
||||
15
common-agent-boot/src/main/resources/application-dev.yaml
Normal file
15
common-agent-boot/src/main/resources/application-dev.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://110.42.106.130:5431/common_agent?currentSchema=common_agent
|
||||
username: common_agent
|
||||
password: common_agent
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
common:
|
||||
attachment:
|
||||
base-path: /data/common-agent/attachments
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.bruce.dashboard;
|
||||
|
||||
import com.bruce.common.handler.GlobalExceptionHandler;
|
||||
import com.bruce.dashboard.controller.StudioDashboardController;
|
||||
import com.bruce.dashboard.service.IStudioDashboardService;
|
||||
import com.bruce.dashboard.vo.StudioDashboardMetricsVO;
|
||||
import com.bruce.dashboard.vo.StudioDashboardVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 验证 Studio 首页聚合接口响应结构,确保前端首页可稳定消费。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StudioDashboardControllerTests {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Mock
|
||||
private IStudioDashboardService studioDashboardService;
|
||||
|
||||
@InjectMocks
|
||||
private StudioDashboardController studioDashboardController;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(studioDashboardController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailShouldReturnStructuredDashboardView() throws Exception {
|
||||
StudioDashboardMetricsVO metrics = new StudioDashboardMetricsVO();
|
||||
metrics.setTodayRunCount(12);
|
||||
metrics.setSuccessRate(98.5D);
|
||||
metrics.setP50Latency("1.28s");
|
||||
metrics.setEstimatedCost("¥4.82");
|
||||
|
||||
StudioDashboardVO dashboard = new StudioDashboardVO();
|
||||
dashboard.setProjectName("Common Agent Studio");
|
||||
dashboard.setEnvironment("Dev");
|
||||
dashboard.setPublishStatus("DRAFT");
|
||||
dashboard.setMetrics(metrics);
|
||||
dashboard.setWarningTitle("发布前仍需补齐模型路由");
|
||||
|
||||
when(studioDashboardService.getDashboard()).thenReturn(dashboard);
|
||||
|
||||
mockMvc.perform(get("/api/studio/dashboard"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.resultcode").value("0"))
|
||||
.andExpect(jsonPath("$.data.projectName").value("Common Agent Studio"))
|
||||
.andExpect(jsonPath("$.data.environment").value("Dev"))
|
||||
.andExpect(jsonPath("$.data.metrics.todayRunCount").value(12));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.bruce.dashboard;
|
||||
|
||||
import com.bruce.agent.dto.response.AgentDefinitionResponse;
|
||||
import com.bruce.agent.service.IAgentDefinitionService;
|
||||
import com.bruce.dashboard.service.impl.StudioDashboardServiceImpl;
|
||||
import com.bruce.dashboard.vo.StudioDashboardVO;
|
||||
import com.bruce.mcp.service.IMcpServerService;
|
||||
import com.bruce.modelprovider.service.IModelWorkspaceService;
|
||||
import com.bruce.modelprovider.vo.ModelWorkspaceVO;
|
||||
import com.bruce.observability.service.IObservabilityRunService;
|
||||
import com.bruce.observability.vo.ObservabilityRunSummaryVO;
|
||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||
import com.bruce.rag.service.IKnowledgeWorkspaceService;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import com.bruce.rag.vo.KnowledgeWorkspaceVO;
|
||||
import com.bruce.skill.service.ISkillDefinitionService;
|
||||
import com.bruce.workflow.service.IProjectService;
|
||||
import com.bruce.workflow.service.IWorkflowDefinitionService;
|
||||
import com.bruce.workflow.vo.ProjectVO;
|
||||
import com.bruce.workflow.vo.WorkflowDefinitionVO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StudioDashboardServiceTests {
|
||||
|
||||
@Mock
|
||||
private IProjectService projectService;
|
||||
|
||||
@Mock
|
||||
private IWorkflowDefinitionService workflowDefinitionService;
|
||||
|
||||
@Mock
|
||||
private IObservabilityRunService observabilityRunService;
|
||||
|
||||
@Mock
|
||||
private IModelWorkspaceService modelWorkspaceService;
|
||||
|
||||
@Mock
|
||||
private IRagStoreService ragStoreService;
|
||||
|
||||
@Mock
|
||||
private IKnowledgeWorkspaceService knowledgeWorkspaceService;
|
||||
|
||||
@Mock
|
||||
private IAgentDefinitionService agentDefinitionService;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<IAgentDefinitionService> agentDefinitionServiceProvider;
|
||||
|
||||
@Mock
|
||||
private IMcpServerService mcpServerService;
|
||||
|
||||
@Mock
|
||||
private ISkillDefinitionService skillDefinitionService;
|
||||
|
||||
@InjectMocks
|
||||
private StudioDashboardServiceImpl studioDashboardService;
|
||||
|
||||
@Test
|
||||
void getDashboardShouldAggregateLifecycleReadinessAndRecentRuns() {
|
||||
ProjectVO project = new ProjectVO();
|
||||
project.setId(101L);
|
||||
project.setProjectName("Common Agent Studio");
|
||||
project.setEnvironment("Dev");
|
||||
project.setPublishStatus("DRAFT");
|
||||
|
||||
WorkflowDefinitionVO workflow = new WorkflowDefinitionVO();
|
||||
workflow.setId(201L);
|
||||
workflow.setWorkflowName("合同知识召回");
|
||||
|
||||
ObservabilityRunSummaryVO run = new ObservabilityRunSummaryVO();
|
||||
run.setRequestId("req-1001");
|
||||
run.setWorkflowId(201L);
|
||||
run.setStatus("SUCCESS");
|
||||
run.setDurationMs(1420);
|
||||
run.setEstimatedCost(BigDecimal.valueOf(0.018));
|
||||
|
||||
ModelWorkspaceVO modelWorkspace = new ModelWorkspaceVO();
|
||||
modelWorkspace.setEnabledRouteRuleCount(2);
|
||||
modelWorkspace.setRecentFailedCallCount(0);
|
||||
|
||||
RagStoreResponse store = new RagStoreResponse();
|
||||
store.setId(1001L);
|
||||
|
||||
KnowledgeWorkspaceVO knowledgeWorkspace = new KnowledgeWorkspaceVO();
|
||||
knowledgeWorkspace.setEmbeddingModelId(88L);
|
||||
knowledgeWorkspace.setDocumentCount(9);
|
||||
knowledgeWorkspace.setPendingTaskCount(1);
|
||||
|
||||
AgentDefinitionResponse agent = new AgentDefinitionResponse();
|
||||
agent.setId(301L);
|
||||
agent.setAgentName("售前问答 Agent");
|
||||
|
||||
when(projectService.listProjects()).thenReturn(List.of(project));
|
||||
when(workflowDefinitionService.listByProjectId(101L)).thenReturn(List.of(workflow));
|
||||
when(observabilityRunService.listRecentRuns()).thenReturn(List.of(run));
|
||||
when(modelWorkspaceService.getWorkspace()).thenReturn(modelWorkspace);
|
||||
when(ragStoreService.listResponses()).thenReturn(List.of(store));
|
||||
when(knowledgeWorkspaceService.getWorkspace(1001L)).thenReturn(knowledgeWorkspace);
|
||||
when(agentDefinitionServiceProvider.getIfAvailable()).thenReturn(agentDefinitionService);
|
||||
when(agentDefinitionService.listResponses()).thenReturn(List.of(agent));
|
||||
when(mcpServerService.listServers()).thenReturn(List.of());
|
||||
when(skillDefinitionService.listDefinitions()).thenReturn(List.of());
|
||||
|
||||
StudioDashboardVO dashboard = studioDashboardService.getDashboard();
|
||||
|
||||
assertNotNull(dashboard);
|
||||
assertEquals("Common Agent Studio", dashboard.getProjectName());
|
||||
assertEquals("Dev", dashboard.getEnvironment());
|
||||
assertEquals(4, dashboard.getLifecycleSteps().size());
|
||||
assertEquals(5, dashboard.getReadinessChecklist().size());
|
||||
assertEquals(1, dashboard.getMetrics().getTodayRunCount());
|
||||
assertEquals(100D, dashboard.getMetrics().getSuccessRate());
|
||||
assertEquals("1.42s", dashboard.getRecentRuns().get(0).getLatency());
|
||||
assertEquals("合同知识召回", dashboard.getRecentRuns().get(0).getName());
|
||||
assertEquals("当前知识库仍有待索引文档,建议完成索引后再进行发布联调。", dashboard.getWarningMessage());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bruce.common.config;
|
||||
package com.bruce.integration.config;
|
||||
|
||||
import com.bruce.common.config.EntityAuditMetaObjectHandler;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import org.apache.ibatis.reflection.SystemMetaObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bruce.common.enumconfig;
|
||||
package com.bruce.integration.enumconfig;
|
||||
|
||||
import com.bruce.common.enums.CommonStatusEnum;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bruce.common.enumconfig;
|
||||
package com.bruce.integration.enumconfig;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.bruce.common.domain.entity.SysEnum;
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.bruce.integration.enumconfig;
|
||||
|
||||
import com.bruce.common.domain.entity.SysEnum;
|
||||
import com.bruce.common.enums.PersistableSysEnumDefinition;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* sys_enum 初始化测试的辅助工具。
|
||||
* <p>
|
||||
* 该类只服务于测试入口,用于把代码里的枚举定义组装成可落库的数据结构,
|
||||
* 并在真正写库前完成组级唯一性校验。
|
||||
*/
|
||||
final class SysEnumDefinitionSyncSupport {
|
||||
|
||||
private SysEnumDefinitionSyncSupport() {
|
||||
}
|
||||
|
||||
static EnumGroup groupOf(List<? extends PersistableSysEnumDefinition> definitions) {
|
||||
if (definitions == null || definitions.isEmpty()) {
|
||||
throw new IllegalArgumentException("枚举定义不能为空");
|
||||
}
|
||||
PersistableSysEnumDefinition first = definitions.getFirst();
|
||||
validateGroupMembers(first, definitions);
|
||||
validateUniqueValuesAndSorts(first, definitions);
|
||||
return new EnumGroup(first.getCatalog(), first.getType(), List.copyOf(definitions));
|
||||
}
|
||||
|
||||
static void validateUniqueGroupKeys(List<EnumGroup> groups) {
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (EnumGroup group : groups) {
|
||||
String key = group.catalog() + "/" + group.type();
|
||||
if (!keys.add(key)) {
|
||||
throw new IllegalArgumentException("存在重复的枚举分组: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<SysEnum> toEntities(EnumGroup group) {
|
||||
return group.definitions().stream()
|
||||
.map(item -> {
|
||||
SysEnum sysEnum = new SysEnum();
|
||||
sysEnum.setCatalog(group.catalog());
|
||||
sysEnum.setType(group.type());
|
||||
sysEnum.setName(item.getName());
|
||||
sysEnum.setValue(item.getValue());
|
||||
sysEnum.setStrvalue(item.getStrvalue());
|
||||
sysEnum.setSort(item.getSort());
|
||||
sysEnum.setRemark(item.getRemark());
|
||||
return sysEnum;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static void validateGroupMembers(
|
||||
PersistableSysEnumDefinition first,
|
||||
List<? extends PersistableSysEnumDefinition> definitions
|
||||
) {
|
||||
for (PersistableSysEnumDefinition item : definitions) {
|
||||
if (!first.getCatalog().equals(item.getCatalog()) || !first.getType().equals(item.getType())) {
|
||||
throw new IllegalArgumentException("同一枚举组中的 catalog/type 必须一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateUniqueValuesAndSorts(
|
||||
PersistableSysEnumDefinition first,
|
||||
List<? extends PersistableSysEnumDefinition> definitions
|
||||
) {
|
||||
Set<Integer> values = new HashSet<>();
|
||||
Set<Integer> sorts = new HashSet<>();
|
||||
for (PersistableSysEnumDefinition item : definitions) {
|
||||
if (!values.add(item.getValue())) {
|
||||
throw new IllegalArgumentException("枚举值重复: " + first.getCatalog() + "/" + first.getType() + "/" + item.getValue());
|
||||
}
|
||||
if (!sorts.add(item.getSort())) {
|
||||
throw new IllegalArgumentException("枚举排序重复: " + first.getCatalog() + "/" + first.getType() + "/" + item.getSort());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record EnumGroup(
|
||||
String catalog,
|
||||
String type,
|
||||
List<? extends PersistableSysEnumDefinition> definitions
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bruce.common.enumconfig;
|
||||
package com.bruce.integration.enumconfig;
|
||||
|
||||
import com.bruce.common.domain.entity.SysEnum;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
@@ -0,0 +1,313 @@
|
||||
package com.bruce.integration.schema;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.bruce.agent.entity.AgentCapabilityBinding;
|
||||
import com.bruce.agent.entity.AgentDefinition;
|
||||
import com.bruce.agent.entity.AgentMessage;
|
||||
import com.bruce.agent.entity.AgentSession;
|
||||
import com.bruce.common.domain.entity.SysAttachment;
|
||||
import com.bruce.common.domain.entity.SysEnum;
|
||||
import com.bruce.common.domain.model.BaseEntity;
|
||||
import com.bruce.mcp.entity.McpCapability;
|
||||
import com.bruce.mcp.entity.McpServer;
|
||||
import com.bruce.modelprovider.entity.ModelCallLog;
|
||||
import com.bruce.modelprovider.entity.ModelConfig;
|
||||
import com.bruce.modelprovider.entity.ModelProvider;
|
||||
import com.bruce.modelprovider.entity.ModelRouteRule;
|
||||
import com.bruce.modelprovider.entity.RagStoreModelConfig;
|
||||
import com.bruce.rag.entity.RagChunk;
|
||||
import com.bruce.rag.entity.RagChunkEmbedding;
|
||||
import com.bruce.rag.entity.RagDocument;
|
||||
import com.bruce.rag.entity.RagDocumentParseResult;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.skill.entity.SkillDefinition;
|
||||
import com.bruce.skill.entity.SkillVersion;
|
||||
import com.bruce.workflow.entity.StudioProject;
|
||||
import com.bruce.workflow.entity.WorkflowDefinition;
|
||||
import com.bruce.workflow.entity.WorkflowRun;
|
||||
import com.bruce.workflow.entity.WorkflowRunStep;
|
||||
import com.bruce.workflow.entity.WorkflowVersion;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 校验 SQL 建表脚本与实体字段映射保持一致。
|
||||
* <p>
|
||||
* 这组测试直接读取 script/sql 下的建表脚本,把数据库字段与 Java Entity 的映射关系做逐表比对,
|
||||
* 用来补强此前偏结构性的 mapper/repository 验证。
|
||||
*/
|
||||
class SqlEntityMappingContractTests {
|
||||
|
||||
private static final Path SQL_DIR = Path.of("..", "script", "sql");
|
||||
|
||||
/**
|
||||
* BaseEntity 约定的公共审计字段,需要在所有业务表脚本中保留。
|
||||
*/
|
||||
private static final Set<String> BASE_COLUMNS = Set.of(
|
||||
"id", "create_by", "create_time", "update_by", "update_time", "version"
|
||||
);
|
||||
|
||||
@Test
|
||||
void entityMappedColumnsShouldExistInSqlScripts() throws IOException {
|
||||
Map<String, Set<String>> tableColumns = loadTableColumns();
|
||||
for (Class<?> entityClass : entityClasses()) {
|
||||
TableName tableName = entityClass.getAnnotation(TableName.class);
|
||||
assertNotNull(tableName, "实体缺少 @TableName: " + entityClass.getName());
|
||||
|
||||
String table = tableName.value();
|
||||
Set<String> sqlColumns = tableColumns.get(table);
|
||||
assertNotNull(sqlColumns, "SQL 脚本未找到表定义: " + table);
|
||||
|
||||
Set<String> entityColumns = collectEntityColumns(entityClass);
|
||||
for (String column : entityColumns) {
|
||||
assertTrue(sqlColumns.contains(column),
|
||||
() -> "表 " + table + " 缺少实体映射字段 " + column + ",实体: " + entityClass.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sqlTablesShouldHaveExpectedEntityCoverage() throws IOException {
|
||||
Map<String, Set<String>> tableColumns = loadTableColumns();
|
||||
Set<String> entityTables = new LinkedHashSet<>();
|
||||
for (Class<?> entityClass : entityClasses()) {
|
||||
entityTables.add(entityClass.getAnnotation(TableName.class).value());
|
||||
}
|
||||
|
||||
Set<String> expectedTables = Set.of(
|
||||
"sys_enum",
|
||||
"sys_attachment",
|
||||
"rag_store",
|
||||
"rag_document",
|
||||
"rag_document_parse_result",
|
||||
"rag_chunk",
|
||||
"rag_chunk_embedding",
|
||||
"agent_definition",
|
||||
"agent_session",
|
||||
"agent_message",
|
||||
"agent_capability_binding",
|
||||
"model_provider",
|
||||
"model_config",
|
||||
"model_route_rule",
|
||||
"rag_store_model_config",
|
||||
"model_call_log",
|
||||
"studio_project",
|
||||
"workflow_definition",
|
||||
"workflow_version",
|
||||
"workflow_run",
|
||||
"workflow_run_step",
|
||||
"mcp_server",
|
||||
"mcp_capability",
|
||||
"skill_definition",
|
||||
"skill_version"
|
||||
);
|
||||
|
||||
assertEquals(expectedTables, entityTables, "实体覆盖的表清单应与当前模块表一致");
|
||||
assertTrue(tableColumns.keySet().containsAll(expectedTables), "SQL 脚本应覆盖全部模块表");
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityTablesShouldRetainBaseAuditColumns() throws IOException {
|
||||
Map<String, Set<String>> tableColumns = loadTableColumns();
|
||||
for (Class<?> entityClass : entityClasses()) {
|
||||
String table = entityClass.getAnnotation(TableName.class).value();
|
||||
Set<String> sqlColumns = tableColumns.get(table);
|
||||
assertNotNull(sqlColumns, "SQL 脚本未找到表定义: " + table);
|
||||
assertTrue(sqlColumns.containsAll(BASE_COLUMNS),
|
||||
() -> "表 " + table + " 缺少 BaseEntity 审计字段,实际字段: " + sqlColumns);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sqlColumnParserShouldIgnoreConstraintsAndIndexes() throws IOException {
|
||||
Map<String, Set<String>> tableColumns = loadTableColumns();
|
||||
Collection<Set<String>> allColumns = tableColumns.values();
|
||||
assertFalse(allColumns.stream().flatMap(Set::stream).anyMatch(column -> column.startsWith("constraint")),
|
||||
"列解析不应把约束名当成字段");
|
||||
assertFalse(allColumns.stream().flatMap(Set::stream).anyMatch(column -> column.startsWith("foreign")),
|
||||
"列解析不应把外键定义当成字段");
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> loadTableColumns() throws IOException {
|
||||
Map<String, Set<String>> tableColumns = new LinkedHashMap<>();
|
||||
Pattern createTablePattern = Pattern.compile("CREATE TABLE(?: IF NOT EXISTS)?\\s+([a-zA-Z0-9_]+)\\s*\\(",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
for (Path sqlFile : sqlFiles()) {
|
||||
List<String> lines = Files.readAllLines(sqlFile, StandardCharsets.UTF_8);
|
||||
String currentTable = null;
|
||||
Set<String> currentColumns = null;
|
||||
int nesting = 0;
|
||||
for (String rawLine : lines) {
|
||||
String line = rawLine.trim();
|
||||
if (line.isEmpty() || line.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentTable == null) {
|
||||
Matcher matcher = createTablePattern.matcher(line);
|
||||
if (matcher.find()) {
|
||||
currentTable = matcher.group(1).toLowerCase(Locale.ROOT);
|
||||
currentColumns = new LinkedHashSet<>();
|
||||
tableColumns.put(currentTable, currentColumns);
|
||||
nesting = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
nesting += count(line, '(');
|
||||
nesting -= count(line, ')');
|
||||
|
||||
if (!line.startsWith("CONSTRAINT")
|
||||
&& !line.startsWith("PRIMARY KEY")
|
||||
&& !line.startsWith("FOREIGN KEY")
|
||||
&& !line.startsWith("UNIQUE")
|
||||
&& !line.startsWith("CHECK")) {
|
||||
String column = extractColumnName(line);
|
||||
if (column != null) {
|
||||
currentColumns.add(column);
|
||||
}
|
||||
}
|
||||
|
||||
if (nesting <= 0) {
|
||||
currentTable = null;
|
||||
currentColumns = null;
|
||||
nesting = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tableColumns;
|
||||
}
|
||||
|
||||
private List<Path> sqlFiles() throws IOException {
|
||||
List<Path> sqlFiles = new ArrayList<>();
|
||||
try (var paths = Files.list(SQL_DIR)) {
|
||||
paths.filter(path -> path.getFileName().toString().endsWith(".sql"))
|
||||
.sorted()
|
||||
.forEach(sqlFiles::add);
|
||||
}
|
||||
return sqlFiles;
|
||||
}
|
||||
|
||||
private String extractColumnName(String line) {
|
||||
String sanitized = line.replace(",", "").trim();
|
||||
if (sanitized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int firstSpace = sanitized.indexOf(' ');
|
||||
if (firstSpace <= 0) {
|
||||
return null;
|
||||
}
|
||||
String column = sanitized.substring(0, firstSpace).trim();
|
||||
if (!column.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
|
||||
return null;
|
||||
}
|
||||
return column.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private int count(String text, char target) {
|
||||
int result = 0;
|
||||
for (int index = 0; index < text.length(); index++) {
|
||||
if (text.charAt(index) == target) {
|
||||
result++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<String> collectEntityColumns(Class<?> entityClass) {
|
||||
Set<String> columns = new LinkedHashSet<>();
|
||||
for (Field field : allFields(entityClass)) {
|
||||
if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
TableField tableField = field.getAnnotation(TableField.class);
|
||||
String column = tableField == null || tableField.value().isBlank()
|
||||
? camelToSnake(field.getName())
|
||||
: tableField.value();
|
||||
columns.add(column.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private List<Field> allFields(Class<?> entityClass) {
|
||||
List<Field> fields = new ArrayList<>();
|
||||
Class<?> current = entityClass;
|
||||
while (current != null && current != Object.class) {
|
||||
fields.addAll(Arrays.asList(current.getDeclaredFields()));
|
||||
if (current == BaseEntity.class) {
|
||||
break;
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private String camelToSnake(String value) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
char current = value.charAt(index);
|
||||
if (Character.isUpperCase(current)) {
|
||||
if (index > 0) {
|
||||
builder.append('_');
|
||||
}
|
||||
builder.append(Character.toLowerCase(current));
|
||||
} else {
|
||||
builder.append(current);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private List<Class<?>> entityClasses() {
|
||||
return List.of(
|
||||
SysEnum.class,
|
||||
SysAttachment.class,
|
||||
RagStore.class,
|
||||
RagDocument.class,
|
||||
RagDocumentParseResult.class,
|
||||
RagChunk.class,
|
||||
RagChunkEmbedding.class,
|
||||
AgentDefinition.class,
|
||||
AgentSession.class,
|
||||
AgentMessage.class,
|
||||
AgentCapabilityBinding.class,
|
||||
ModelProvider.class,
|
||||
ModelConfig.class,
|
||||
ModelRouteRule.class,
|
||||
RagStoreModelConfig.class,
|
||||
ModelCallLog.class,
|
||||
StudioProject.class,
|
||||
WorkflowDefinition.class,
|
||||
WorkflowVersion.class,
|
||||
WorkflowRun.class,
|
||||
WorkflowRunStep.class,
|
||||
McpServer.class,
|
||||
McpCapability.class,
|
||||
SkillDefinition.class,
|
||||
SkillVersion.class
|
||||
);
|
||||
}
|
||||
}
|
||||
61
common-agent-common/pom.xml
Normal file
61
common-agent-common/pom.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.bruce</groupId>
|
||||
<artifactId>common-agent-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>common-agent-common</artifactId>
|
||||
<name>common-agent-common</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-parsers-standard-package</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -3,26 +3,39 @@ package com.bruce.common.controller;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
|
||||
import com.bruce.common.dto.response.SysAttachmentResponse;
|
||||
import com.bruce.common.factory.SysAttachmentFactory;
|
||||
import com.bruce.common.service.ISysAttachmentService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "系统附件管理")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/attachments")
|
||||
@RequiredArgsConstructor
|
||||
public class SysAttachmentController {
|
||||
|
||||
@Autowired
|
||||
private ISysAttachmentService sysAttachmentService;
|
||||
private final ISysAttachmentService sysAttachmentService;
|
||||
|
||||
private final SysAttachmentFactory sysAttachmentFactory;
|
||||
|
||||
@Operation(summary = "上传附件")
|
||||
@PostMapping("/upload")
|
||||
public RequestResult<SysAttachmentResponse> upload(@ModelAttribute SysAttachmentUploadRequest request) {
|
||||
return RequestResult.success(SysAttachmentResponse.fromEntity(sysAttachmentService.upload(request)));
|
||||
log.info("上传附件开始,sourceType={}, sourceId={}",
|
||||
request == null ? null : request.getSourceType(),
|
||||
request == null ? null : request.getSourceId());
|
||||
SysAttachmentResponse response = sysAttachmentFactory.toResponse(sysAttachmentService.upload(request));
|
||||
log.info("上传附件结束,attachmentId={}, sourceType={}, sourceId={}",
|
||||
response == null ? null : response.getId(),
|
||||
request == null ? null : request.getSourceType(),
|
||||
request == null ? null : request.getSourceId());
|
||||
return RequestResult.success(response);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.bruce.common.dto.response;
|
||||
|
||||
import com.bruce.common.domain.entity.SysAttachment;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
@Data
|
||||
@Schema(description = "系统附件响应")
|
||||
@@ -41,13 +39,4 @@ public class SysAttachmentResponse {
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
public static SysAttachmentResponse fromEntity(SysAttachment entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysAttachmentResponse response = new SysAttachmentResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.bruce.common.dto.response;
|
||||
|
||||
import com.bruce.common.domain.entity.SysEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
@Data
|
||||
@Schema(description = "系统枚举响应")
|
||||
@@ -32,13 +30,4 @@ public class SysEnumResponse {
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
public static SysEnumResponse fromEntity(SysEnum entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysEnumResponse response = new SysEnumResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bruce.common.factory;
|
||||
|
||||
import com.bruce.common.domain.entity.SysAttachment;
|
||||
import com.bruce.common.dto.response.SysAttachmentResponse;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 系统附件工厂,集中处理附件元数据出参转换。
|
||||
*/
|
||||
@Component
|
||||
public class SysAttachmentFactory {
|
||||
|
||||
public SysAttachmentResponse toResponse(SysAttachment entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysAttachmentResponse response = new SysAttachmentResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.bruce.common.factory;
|
||||
|
||||
import com.bruce.common.domain.entity.SysEnum;
|
||||
import com.bruce.common.dto.request.SysEnumBatchSaveRequest;
|
||||
import com.bruce.common.dto.request.SysEnumSaveRequest;
|
||||
import com.bruce.common.dto.response.SysEnumResponse;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统枚举工厂,统一负责请求对象、实体对象和响应对象之间的转换。
|
||||
*/
|
||||
@Component
|
||||
public class SysEnumFactory {
|
||||
|
||||
/**
|
||||
* 将保存请求转换为实体,避免在服务层散落字段拷贝逻辑。
|
||||
*/
|
||||
public SysEnum toEntity(SysEnumSaveRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
SysEnum entity = new SysEnum();
|
||||
BeanUtils.copyProperties(request, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将批量请求中的单个枚举项转换为实体,catalog/type 由外层分组统一提供。
|
||||
*/
|
||||
public SysEnum toEntity(String catalog, String type, SysEnumBatchSaveRequest.Item item) {
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
SysEnum entity = new SysEnum();
|
||||
entity.setCatalog(catalog);
|
||||
entity.setType(type);
|
||||
entity.setName(item.getName());
|
||||
entity.setValue(item.getValue());
|
||||
entity.setStrvalue(item.getStrvalue());
|
||||
entity.setSort(item.getSort());
|
||||
entity.setRemark(item.getRemark());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体转换为返回对象,保持接口层不直接暴露实体。
|
||||
*/
|
||||
public SysEnumResponse toResponse(SysEnum entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
SysEnumResponse response = new SysEnumResponse();
|
||||
BeanUtils.copyProperties(entity, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<SysEnumResponse> toResponses(List<SysEnum> entities) {
|
||||
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
@@ -17,6 +19,15 @@ public class GlobalExceptionHandler {
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, exception.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
MissingServletRequestParameterException.class,
|
||||
MethodArgumentTypeMismatchException.class
|
||||
})
|
||||
public ResponseEntity<RequestResult<Void>> handleBadRequest(Exception exception) {
|
||||
log.warn("GlobalExceptionHandler.handleBadRequest, message={}", exception.getMessage(), exception);
|
||||
return buildResponse(HttpStatus.BAD_REQUEST, "请求参数不合法");
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<RequestResult<Void>> handleException(Exception exception) {
|
||||
log.error("GlobalExceptionHandler.handleException", exception);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user