Compare commits
32 Commits
8abea44aa7
...
codex/原型设计
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fe1209801 | |||
| d92496854d | |||
| e9abf0b689 | |||
| 1132cf0262 | |||
| b6e1e209a2 | |||
| b26edb8877 | |||
| 3526322811 | |||
| b688df56ba | |||
| 1002380b28 | |||
| 1e1c731c3f | |||
| 8701c80f90 | |||
| f63bdee88c | |||
| 1d89bc89fe | |||
| ab9b099e9b | |||
| 1e004f1a83 | |||
| 21c9eaa44d | |||
| 5d7ca5b31f | |||
| cc745cad47 | |||
| e312c5da8a | |||
| 58a6786f17 | |||
| 705034d41b | |||
| 780abf11f1 | |||
| d8079d6277 | |||
| e51903efbe | |||
| cfa5d1f4e1 | |||
| e37e8dfca6 | |||
| bd8bfeb607 | |||
| dfc9847e4d | |||
| 130fa25275 | |||
| af068e04bc | |||
| 5a872ef197 | |||
| 7575fbfeb0 |
116
AGENT.md
116
AGENT.md
@@ -11,7 +11,8 @@
|
||||
- 文件上传与附件管理
|
||||
- 前后端统一的管理控制台
|
||||
|
||||
当前阶段以"先搭平台骨架,再逐步补智能能力"为主,优先保证工程结构、接口规范、知识库链路和可扩展性。
|
||||
当前阶段已经完成平台骨架、公共接口规范、知识库/知识文档管理、文档上传、文档解析、解析快照、手动切片入口、模型平台基础配置与 Agent 定义管理/调试入口。
|
||||
后续重点从"文档可切片"推进到"向量可检索"、"模型可路由"和"Agent 运行时可编排"。
|
||||
|
||||
## 2. 总体设计思路
|
||||
|
||||
@@ -37,6 +38,7 @@
|
||||
|
||||
- `sys_enum`:系统枚举配置(已完成 CRUD、批量新增、管理端查询)
|
||||
- `sys_attachment`:附件与文件上传(已完成本地上传、元数据持久化)
|
||||
- 文档解析抽象:`DocumentParser`、`DocumentParserFactory` 与 Tika 解析实现(已完成 TXT/Markdown/LOG、PDF、Word、Excel 文本抽取)
|
||||
- 统一 DTO / `RequestResult`(已完成)
|
||||
- 通用状态枚举、启用禁用枚举(已完成)
|
||||
- 全局异常处理 `GlobalExceptionHandler`(已完成)
|
||||
@@ -45,19 +47,24 @@
|
||||
|
||||
### 3.2 RAG 知识库模块
|
||||
|
||||
当前已有完整的元数据管理层:
|
||||
当前已有元数据管理、文档上传和解析入口:
|
||||
|
||||
- `rag_store`:知识库主表(已完成 CRUD、编码唯一性校验)
|
||||
- `rag_document`:知识库文档表(已完成实体、Mapper、Service、条件查询)
|
||||
- `rag_document`:知识库文档表(已完成 CRUD、条件查询、批量上传、启停用)
|
||||
- `rag_chunk`:知识切片表结构、实体、Mapper、Service、定长/分隔符切片器与手动切片入口(已完成基础闭环)
|
||||
- `rag_chunk_embedding`:切片向量表结构、实体、Mapper、Service(已完成结构,待模型网关接入后生成向量)
|
||||
- RAG 解析状态枚举 `RagParseStatusEnum`(已完成)
|
||||
- RAG 索引状态枚举 `RagIndexStatusEnum`(已完成)
|
||||
- RAG 切片策略枚举 `RagChunkStrategyEnum`(已完成)
|
||||
- 文档解析接口 `/api/rag/documents/parse`(已完成状态流转、文本抽取和解析快照保存)
|
||||
- 文档切片接口 `/api/rag/documents/chunk`(已完成按解析快照生成并替换 `rag_chunk`)
|
||||
|
||||
后续计划继续扩展:
|
||||
|
||||
- 文档切片
|
||||
- 向量化
|
||||
- 检索召回
|
||||
- 索引任务
|
||||
- 接入模型服务商配置与模型路由,统一管理 Ollama、硅基流动、百炼等模型来源
|
||||
- 调用 Embedding 模型并写入 `rag_chunk_embedding`
|
||||
- 检索召回与重排序
|
||||
- 索引任务、失败重试和任务日志
|
||||
|
||||
当前设计原则:
|
||||
|
||||
@@ -67,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`
|
||||
@@ -91,11 +104,14 @@
|
||||
已具备的页面与布局:
|
||||
|
||||
- 左侧管理菜单与品牌区(232px 侧边栏)
|
||||
- 工作台(占位)
|
||||
- RAG 工作台(文档解析与切片概览)
|
||||
- 系统枚举管理页(完整 CRUD + 批量新增)
|
||||
- 附件管理入口(占位)
|
||||
- 知识库管理页(完整 CRUD + 双栏详情)
|
||||
- 知识文档入口(占位)
|
||||
- 知识库管理页(完整 CRUD + 概览卡片 + 双栏详情 + 批量上传入口)
|
||||
- 知识文档页(条件查询 + 批量上传 + 解析重试 + 批量切片 + 编辑/启停用/删除)
|
||||
- 切片任务页(解析成功/失败文档概览与切片入口)
|
||||
- Agent 管理页(Agent 定义管理与知识库绑定)
|
||||
- Agent 调试页(普通对话 / RAG 对话切换、引用切片回显)
|
||||
|
||||
前端技术要点:
|
||||
|
||||
@@ -115,8 +131,9 @@
|
||||
后续控制台至少继续覆盖:
|
||||
|
||||
- 附件管理页面前端联调
|
||||
- 知识文档管理页面前端联调
|
||||
- Agent 调试页
|
||||
- RAG 检索配置、向量索引任务和最近任务页面联调
|
||||
- 模型服务商、模型配置、路由规则和调用日志管理
|
||||
- Agent 会话历史与运行日志页
|
||||
- 执行日志查看
|
||||
|
||||
## 4. 当前接口设计原则
|
||||
@@ -141,6 +158,35 @@
|
||||
6. OpenAPI 注解覆盖
|
||||
所有 Controller、DTO 使用 `@Tag`、`@Operation`、`@Schema` 注解。
|
||||
|
||||
## 4.1 代码注释约定
|
||||
|
||||
为方便后续多人协作和 Agent 接力阅读,新增以下约定:
|
||||
|
||||
1. 新增或修改核心业务代码时,需要补充中文注释
|
||||
注释优先覆盖类职责、关键方法、关键分支和重要参数含义,避免只写重复代码字面的无效注释。
|
||||
|
||||
2. 每次提交代码时,同步检查对应改动是否已经补齐中文注释
|
||||
尤其是新引入的工厂、策略、服务编排、状态流转和复杂转换逻辑,默认需要有中文说明。
|
||||
|
||||
3. 注释以“帮助后来者快速理解设计意图”为目标
|
||||
不追求注释数量,重点说明为什么这样做、边界是什么、哪些地方后续还会扩展。
|
||||
|
||||
## 4.2 结构化枚举约定
|
||||
|
||||
为保证前后端协议、代码定义和数据库配置一致,新增以下长期规则:
|
||||
|
||||
1. 长期固定的结构化文本字段,统一采用枚举值传输
|
||||
不再以字符串名称作为接口协议值,前后端统一传整型枚举值。
|
||||
|
||||
2. 这类枚举必须先定义为 Java 枚举类
|
||||
Java 枚举类作为单一事实来源,再派生前端常量和 `sys_enum` 配置。
|
||||
|
||||
3. 每次新增或修改结构化枚举时,必须同步纳入 `sys_enum` 初始化测试
|
||||
通过统一测试入口按 `catalog + type` 先删后全量重建,避免数据库枚举配置漂移。
|
||||
|
||||
4. `catalog + type` 在枚举组层面必须唯一
|
||||
一旦重复,会破坏枚举组重建语义,因此视为非法设计。
|
||||
|
||||
## 5. 数据与存储设计
|
||||
|
||||
### 5.1 关系型数据库
|
||||
@@ -177,25 +223,29 @@
|
||||
1. ~~统一接口层规范~~ DTO、返回体、基础校验、通用异常处理(已完成)
|
||||
2. ~~收紧基础模块~~ `sys_enum`、`sys_attachment`(已完成)
|
||||
3. ~~补全 RAG 基础元数据管理~~ `rag_store`、`rag_document`(已完成)
|
||||
4. 接入 Spring AI
|
||||
5. 建立 Agent 运行时骨架
|
||||
6. ~~补前端控制台基础骨架~~(已完成,部分页面待联调)
|
||||
4. ~~补全 RAG 文档上传与解析入口~~ 批量上传、Tika 文本抽取、解析状态流转(已完成)
|
||||
5. ~~接入切片生成与切片持久化~~(已完成定长/分隔符切片与手动切片入口)
|
||||
6. 建设模型服务商配置与模型路由层
|
||||
7. 接入 Embedding / Chat 模型并完成向量写入
|
||||
8. 完善 Agent 运行时骨架(会话、工具、任务)
|
||||
9. ~~补前端控制台基础骨架~~(已完成,部分高级页面待联调)
|
||||
|
||||
剩余重点:
|
||||
|
||||
- 完善 RAG 文档上传、解析、索引的业务闭环
|
||||
- 补齐前端附件管理、知识文档页面的表单与接口联调
|
||||
- 接入 Spring AI 并实现模型调用链路
|
||||
- 完成模型服务商配置、模型配置、路由规则和调用日志基础能力
|
||||
- 接入 Embedding,生成并保存 `rag_chunk_embedding`
|
||||
- 补齐索引任务、重试、重建索引和最近任务接口
|
||||
- 扩展 Agent 会话、工具调用与任务编排能力
|
||||
|
||||
## 7. 下一步建议
|
||||
|
||||
结合当前代码状态,接下来建议重点做:
|
||||
|
||||
- 实现知识库文档上传并自动创建 `rag_document` 记录
|
||||
- 建立文档解析任务入口与状态流转
|
||||
- 为后续切片与向量化预留任务入口
|
||||
- 补齐前端附件管理、知识文档页面的联调
|
||||
- 接入 Spring AI,实现最小模型调用链路
|
||||
- 完成 RAG 全量向量化链路,确保知识库可稳定召回
|
||||
- 为 Agent 调试链路补齐会话持久化与多轮上下文管理
|
||||
- 建立 Agent 工具注册与调用协议,沉淀最小工具集
|
||||
- 把 `indexStatus` 从手工字段推进为真实状态流转
|
||||
- 补齐重建索引、失败重试、最近任务接口和前端展示
|
||||
|
||||
## 8. 文档用途说明
|
||||
|
||||
@@ -209,5 +259,7 @@
|
||||
|
||||
- `agent-runtime.md`
|
||||
- `rag-design.md`
|
||||
- `MODEL_PROVIDER_REQUIREMENTS.md`
|
||||
- `MODEL_PROVIDER_DESIGN.md`
|
||||
- `api-style.md`
|
||||
- `frontend-console.md`
|
||||
- `frontend-console.md`
|
||||
|
||||
71
README.md
71
README.md
@@ -3,8 +3,8 @@
|
||||
Common Agent 是一个规划中的通用 Agent 平台,技术路线基于 Java、Spring Boot 和 Spring AI。
|
||||
项目目标是建设一套完整的前后端系统,支持 Agent 编排、工具调用、会话管理、RAG 知识库和平台管理能力。
|
||||
|
||||
当前项目处于基础工程阶段。后端骨架(含 DTO、统一返回体、全局异常处理、审计自动填充)、PostgreSQL 配置、MyBatis-Plus、Lombok、多环境配置文件和前端控制台基础页面已经完成;
|
||||
Agent 运行时、RAG 文档解析与向量化和更多管理功能会在后续阶段逐步实现。
|
||||
当前项目已经完成基础工程、公共模块、RAG 元数据管理、文档上传、文档解析入口、解析快照、手动切片入口、模型服务商配置基础能力、Agent 定义管理与调试页面。
|
||||
会话持久化、工具调用编排、RAG 全量向量化与检索问答能力会在后续阶段逐步完善。
|
||||
|
||||
## 项目愿景
|
||||
|
||||
@@ -13,7 +13,7 @@ Common Agent 希望成为一个可复用的企业级 AI 应用基础平台:
|
||||
- Agent 运行时:支持对话、工具调用、记忆、任务执行和流程编排。
|
||||
- RAG 知识库:支持文档导入、解析、切片、向量化、检索和基于上下文的回答生成。
|
||||
- 模型抽象:通过 Spring AI 统一接入聊天模型、Embedding 模型和重排序模型。
|
||||
- 管理控制台:提供会话、Agent、知识库、文档、提示词和系统配置的 Web 管理界面。
|
||||
- 管理控制台:提供 Agent、知识库、文档、模型配置和系统配置的 Web 管理界面。
|
||||
- 多环境部署:支持本地开发、测试环境和生产环境的配置隔离。
|
||||
|
||||
## 当前技术栈
|
||||
@@ -22,7 +22,7 @@ Common Agent 希望成为一个可复用的企业级 AI 应用基础平台:
|
||||
|------|------|
|
||||
| 后端 | Java 21, Spring Boot 4.0.6, MyBatis-Plus 3.5.16 |
|
||||
| 数据库 | PostgreSQL |
|
||||
| 工具库 | Lombok, Springdoc OpenAPI 2.8.13, Jackson |
|
||||
| 工具库 | Lombok, Springdoc OpenAPI 2.8.13, Jackson, Apache Tika 3.2.3 |
|
||||
| 构建 | Maven Wrapper |
|
||||
| 前端 | Vue 3, TypeScript 5.9, Vite, Element Plus, Pinia, Vue Router 4 |
|
||||
| 前端测试 | Vitest, @vue/test-utils |
|
||||
@@ -36,7 +36,7 @@ common_agent
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # Axios 封装与各模块 API
|
||||
│ │ ├── layouts/ # AdminLayout 管理后台布局
|
||||
│ │ ├── pages/ # 业务页面(工作台、枚举、附件、知识库、文档)
|
||||
│ │ ├── pages/ # 业务页面(系统、RAG、Agent)
|
||||
│ │ ├── router/ # Vue Router 配置
|
||||
│ │ ├── stores/ # Pinia 状态管理
|
||||
│ │ ├── styles/ # 全局样式
|
||||
@@ -48,6 +48,7 @@ common_agent
|
||||
│ │ ├── common/ # 公共模块
|
||||
│ │ │ ├── config/ # AttachmentProperties, EntityAuditMetaObjectHandler
|
||||
│ │ │ ├── controller/ # SysAttachmentController, SysEnumController
|
||||
│ │ │ ├── document/parse/ # 文档解析抽象与 Tika 实现
|
||||
│ │ │ ├── domain/
|
||||
│ │ │ │ ├── entity/ # SysAttachment, SysEnum
|
||||
│ │ │ │ └── model/ # BaseEntity, RequestResult
|
||||
@@ -58,14 +59,16 @@ common_agent
|
||||
│ │ │ ├── handler/ # GlobalExceptionHandler
|
||||
│ │ │ ├── mapper/ # SysAttachmentMapper, SysEnumMapper
|
||||
│ │ │ └── service/ # 接口与实现
|
||||
│ │ └── rag/ # RAG 知识库模块
|
||||
│ │ ├── constant/ # RagSystemConstants
|
||||
│ │ ├── controller/ # RagStoreController, RagDocumentController
|
||||
│ │ ├── dto/ # 请求/响应 DTO
|
||||
│ │ ├── entity/ # RagStore, RagDocument
|
||||
│ │ ├── enums/ # RagParseStatusEnum, RagIndexStatusEnum
|
||||
│ │ ├── 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 # 开发环境配置
|
||||
@@ -73,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
|
||||
```
|
||||
@@ -145,11 +152,14 @@ npm run build
|
||||
|
||||
| 页面 | 状态 |
|
||||
|------|------|
|
||||
| 工作台 | 占位 |
|
||||
| RAG 工作台 | 文档解析与切片概览 |
|
||||
| 系统枚举 | 完整 CRUD + 批量新增 |
|
||||
| 附件管理 | 占位 |
|
||||
| 知识库 | 完整 CRUD + 双栏详情 |
|
||||
| 知识文档 | 占位 |
|
||||
| 知识文档 | 条件查询 + 批量上传 + 解析重试 + 批量切片 + 编辑/启停用/删除 |
|
||||
| 切片任务 | 解析成功/失败文档概览 + 切片入口 |
|
||||
| Agent管理 | Agent 定义 CRUD + 知识库绑定 |
|
||||
| Agent调试 | 普通对话 / RAG 对话切换 + 引用切片回显 |
|
||||
|
||||
当前 UI 规范:
|
||||
|
||||
@@ -167,11 +177,31 @@ npm run build
|
||||
- 大整数 ID 通过 `@JsonSerialize(ToStringSerializer.class)` 防止前端精度丢失
|
||||
- 全局异常处理返回 HTTP 400/500 状态码
|
||||
|
||||
## RAG 当前能力边界
|
||||
|
||||
当前 RAG 已经从元数据管理推进到"上传 + 解析 + 手动切片 + Agent 调试召回"阶段:
|
||||
|
||||
- 知识库:支持列表、条件查询、详情、总览、单库文档概览、新增、编辑、删除。
|
||||
- 知识文档:支持列表、条件查询、详情、新增/编辑、删除、批量上传。
|
||||
- 文档解析:基于 Apache Tika 支持 TXT/Markdown/LOG、PDF、Word、Excel 文本抽取,解析时更新 `parseStatus` 并保存解析快照。
|
||||
- 文档切片:支持按解析快照进行手动异步切片,已落地定长切片和分隔符切片,写入 `rag_chunk`。
|
||||
- 向量表:`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 执行模型、工具注册、记忆和编排能力。
|
||||
- `rag-core`:文档导入、切片、Embedding、检索和引用元数据。
|
||||
- `model-provider`:基于 Spring AI 的聊天模型、Embedding、重排序和模型配置层。
|
||||
- `rag-core`:文档导入、解析、切片、Embedding、检索和引用元数据。
|
||||
- `model-provider`:模型服务商配置、模型路由、调用日志,以及基于 OpenAI-compatible / Spring AI 的聊天模型、Embedding、重排序适配层。
|
||||
- `platform-api`:面向前端和外部系统的 REST API。
|
||||
- `platform-admin`:平台管理前端。
|
||||
- `common-infra`:持久化、审计日志、安全、租户隔离和可观测性。
|
||||
@@ -180,10 +210,13 @@ npm run build
|
||||
|
||||
- [架构说明](docs/ARCHITECTURE.md)
|
||||
- [开发路线图](docs/ROADMAP.md)
|
||||
- [模型服务商配置与路由需求](docs/MODEL_PROVIDER_REQUIREMENTS.md)
|
||||
- [模型服务商配置与路由设计](docs/MODEL_PROVIDER_DESIGN.md)
|
||||
- [平台设计草案](AGENT.md)
|
||||
- [Agent 页面接口清单](agent-page-apis.md)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Spring AI Reference](https://docs.spring.io/spring-ai/reference/)
|
||||
- [Spring AI RAG Reference](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html)
|
||||
- [MyBatis-Plus](https://baomidou.com/)
|
||||
- [MyBatis-Plus](https://baomidou.com/)
|
||||
|
||||
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`
|
||||
@@ -40,6 +40,7 @@
|
||||
- 全局异常处理 `GlobalExceptionHandler`。
|
||||
- MyBatis-Plus 审计自动填充 `EntityAuditMetaObjectHandler`。
|
||||
- 附件本地存储配置 `AttachmentProperties`。
|
||||
- 文档解析抽象与 Apache Tika 解析实现。
|
||||
- 系统枚举管理能力(CRUD + 批量新增 + 管理端查询)。
|
||||
- 附件上传能力(本地磁盘 + 元数据持久化)。
|
||||
|
||||
@@ -58,6 +59,11 @@
|
||||
| SysAttachmentController | `common/controller/SysAttachmentController.java` |
|
||||
| SysEnumServiceImpl | `common/service/impl/SysEnumServiceImpl.java` |
|
||||
| SysAttachmentServiceImpl | `common/service/impl/SysAttachmentServiceImpl.java` |
|
||||
| DocumentParserFactory | `common/document/parse/DocumentParserFactory.java` |
|
||||
| TxtDocumentParser | `common/document/parse/impl/TxtDocumentParser.java` |
|
||||
| PdfDocumentParser | `common/document/parse/impl/PdfDocumentParser.java` |
|
||||
| WordDocumentParser | `common/document/parse/impl/WordDocumentParser.java` |
|
||||
| ExcelDocumentParser | `common/document/parse/impl/ExcelDocumentParser.java` |
|
||||
| CommonStatusEnum | `common/enums/CommonStatusEnum.java` |
|
||||
| EnableStatusEnum | `common/enums/EnableStatusEnum.java` |
|
||||
|
||||
@@ -80,7 +86,8 @@
|
||||
|
||||
- 维护 RAG 知识库主数据(CRUD + 编码唯一性校验)。
|
||||
- 维护知识库文档与附件的关联关系。
|
||||
- 定义解析状态、索引状态和 RAG 相关来源常量。
|
||||
- 支持知识文档批量上传、解析入口、解析快照、手动切片入口和状态流转。
|
||||
- 定义切片、向量、解析状态、索引状态和 RAG 相关来源常量。
|
||||
|
||||
关键类:
|
||||
|
||||
@@ -88,28 +95,90 @@
|
||||
|----|------|
|
||||
| RagStore | `rag/entity/RagStore.java` |
|
||||
| RagDocument | `rag/entity/RagDocument.java` |
|
||||
| RagChunk | `rag/entity/RagChunk.java` |
|
||||
| RagChunkEmbedding | `rag/entity/RagChunkEmbedding.java` |
|
||||
| RagStoreController | `rag/controller/RagStoreController.java` |
|
||||
| RagDocumentController | `rag/controller/RagDocumentController.java` |
|
||||
| RagStoreServiceImpl | `rag/service/impl/RagStoreServiceImpl.java` |
|
||||
| RagDocumentServiceImpl | `rag/service/impl/RagDocumentServiceImpl.java` |
|
||||
| RagDocumentParseServiceImpl | `rag/service/impl/RagDocumentParseServiceImpl.java` |
|
||||
| RagDocumentChunkServiceImpl | `rag/service/impl/RagDocumentChunkServiceImpl.java` |
|
||||
| ChunkerFactory | `rag/parse/ChunkerFactory.java` |
|
||||
| FixedLengthChunker | `rag/parse/impl/FixedLengthChunker.java` |
|
||||
| DelimiterChunker | `rag/parse/impl/DelimiterChunker.java` |
|
||||
| RagParseStatusEnum | `rag/enums/RagParseStatusEnum.java` |
|
||||
| RagIndexStatusEnum | `rag/enums/RagIndexStatusEnum.java` |
|
||||
| RagChunkStrategyEnum | `rag/enums/RagChunkStrategyEnum.java` |
|
||||
| RagSystemConstants | `rag/constant/RagSystemConstants.java` |
|
||||
|
||||
接口列表:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/rag/store/list` | 查询全部知识库 |
|
||||
| POST | `/api/rag/store/query` | 知识库条件查询 |
|
||||
| GET | `/api/rag/store/detail` | 获取知识库详情 |
|
||||
| GET | `/api/rag/store/overview` | 获取知识库总览 |
|
||||
| GET | `/api/rag/store/documentOverview` | 获取单个知识库文档概览 |
|
||||
| POST | `/api/rag/store/save` | 新增/更新知识库 |
|
||||
| POST | `/api/rag/store/delete` | 删除知识库 |
|
||||
| POST | `/api/rag/documents/list` | 查询全部知识文档 |
|
||||
| POST | `/api/rag/documents/query` | 知识文档条件查询 |
|
||||
| GET | `/api/rag/documents/detail` | 获取知识文档详情 |
|
||||
| POST | `/api/rag/documents/save` | 新增/更新知识文档 |
|
||||
| POST | `/api/rag/documents/delete` | 删除知识文档 |
|
||||
| POST | `/api/rag/documents/batchUpload` | 批量上传文档并创建 `rag_document` |
|
||||
| POST | `/api/rag/documents/parse` | 批量解析知识文档 |
|
||||
| POST | `/api/rag/documents/chunk` | 按策略异步生成文档切片 |
|
||||
|
||||
当前边界:
|
||||
|
||||
- 元数据管理层已完成(知识库 CRUD、文档查询)。
|
||||
- 尚未实现"文档上传后自动建档"、"解析入库"、"切片向量化"、"检索问答"等业务流程。
|
||||
- 知识库 CRUD、文档 CRUD、批量上传、Tika 文本解析、解析快照和状态流转已完成。
|
||||
- `rag_chunk` 已支持基于解析快照的手动异步切片,当前已落地定长切片和分隔符切片。
|
||||
- `rag_chunk_embedding` 已支持按知识库向量相似度召回 TopK,用于 Agent 调试链路引用回显。
|
||||
- RAG 对外检索问答接口、索引任务化和重排序能力仍在后续建设中。
|
||||
|
||||
### 3.3 Agent 模块
|
||||
|
||||
包路径:`com.bruce.agent`
|
||||
|
||||
职责:
|
||||
|
||||
- 维护 Agent 定义主数据(CRUD + 编码唯一性 + 绑定知识库校验)。
|
||||
- 提供 Agent 调试对话接口,支持普通对话与 RAG 对话模式切换。
|
||||
- 在 RAG 对话模式下,完成“问题向量化 -> 切片召回 -> 上下文组装 -> Chat 模型回答”的最小链路。
|
||||
- 返回引用切片和请求 ID,便于前端调试与调用追踪。
|
||||
|
||||
关键类:
|
||||
|
||||
| 类 | 路径 |
|
||||
|----|------|
|
||||
| AgentDefinition | `agent/entity/AgentDefinition.java` |
|
||||
| AgentDefinitionController | `agent/controller/AgentDefinitionController.java` |
|
||||
| AgentDefinitionServiceImpl | `agent/service/impl/AgentDefinitionServiceImpl.java` |
|
||||
| AgentDefinitionResponse | `agent/dto/response/AgentDefinitionResponse.java` |
|
||||
| AgentChatResponse | `agent/dto/response/AgentChatResponse.java` |
|
||||
| ChatModelGateway | `modelprovider/gateway/ChatModelGateway.java` |
|
||||
| ChatModelGatewayImpl | `modelprovider/gateway/ChatModelGatewayImpl.java` |
|
||||
| ChatRequest | `modelprovider/gateway/ChatRequest.java` |
|
||||
| ChatResult | `modelprovider/gateway/ChatResult.java` |
|
||||
|
||||
接口列表:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/agents/list` | 查询全部 Agent |
|
||||
| POST | `/api/agents/query` | Agent 条件查询 |
|
||||
| GET | `/api/agents/detail` | 获取 Agent 详情 |
|
||||
| POST | `/api/agents/save` | 新增/更新 Agent |
|
||||
| POST | `/api/agents/delete` | 删除 Agent |
|
||||
| POST | `/api/agents/{agentId}/chat` | Agent 调试对话 |
|
||||
|
||||
当前边界:
|
||||
|
||||
- `agent_definition` 与前端 Agent 管理页已完成联调。
|
||||
- 对话入口已支持 `ragEnabled` 开关,`true` 走 RAG 召回,`false` 走普通对话。
|
||||
- 尚未落地会话持久化(`agent_session` / `agent_message`)和工具调用编排。
|
||||
|
||||
## 4. 数据模型关系
|
||||
|
||||
@@ -121,8 +190,11 @@
|
||||
| `sys_attachment` | 附件元数据 | 独立,被 rag_document 引用 |
|
||||
| `rag_store` | 知识库主表 | 独立 |
|
||||
| `rag_document` | 知识库文档表 | 关联 `rag_store.id` 和 `sys_attachment.id` |
|
||||
| `rag_chunk` | 知识切片表 | 关联 `rag_store.id` 和 `rag_document.id` |
|
||||
| `rag_chunk_embedding` | 切片向量表 | 关联 `rag_store.id`、`rag_document.id` 和 `rag_chunk.id` |
|
||||
| `agent_definition` | Agent 定义表 | 关联 `rag_store.id` |
|
||||
|
||||
`rag_document` 是 RAG 模块与附件模块的连接点。
|
||||
`rag_document` 是 RAG 模块与附件模块的连接点,`rag_chunk` 和 `rag_chunk_embedding` 是检索链路核心落点,`agent_definition` 负责把 Agent 与知识库绑定到同一调用链路。
|
||||
|
||||
## 5. 配置与运行
|
||||
|
||||
@@ -139,17 +211,31 @@
|
||||
- **后端测试**:围绕结构约束的单元测试(Mapper/Service/Controller 继承体系、实体字段注解、方法签名验证)。
|
||||
- **前端测试**:Vitest + @vue/test-utils,覆盖路由定义、布局组件、页面渲染、API 调用和 Long 类型解析。
|
||||
|
||||
## 6.1 注释规范
|
||||
|
||||
- 新增或修改核心业务代码时,需补充中文注释,优先说明类职责、方法目的、关键判断和扩展边界。
|
||||
- 每次提交代码时,需要同步检查本次改动是否已经补齐对应中文注释,避免后续阅读只能靠反推代码语义。
|
||||
- 注释应聚焦设计意图和边界,不建议堆砌“变量赋值”“循环遍历”这类低价值说明。
|
||||
|
||||
## 6.2 结构化枚举规范
|
||||
|
||||
- 长期固定的结构化文本字段,统一采用整型枚举值作为前后端传输协议,不再直接传递字符串名称。
|
||||
- 后端 Java 枚举类是这类结构化枚举的单一事实来源,前端常量和 `sys_enum` 数据都基于它同步。
|
||||
- 新增或修改结构化枚举时,需要通过统一的枚举初始化测试按 `catalog + type` 先删后全量重建写入 `sys_enum`。
|
||||
- 不同枚举组之间的 `catalog + type` 必须唯一,否则会破坏枚举组重建的确定性。
|
||||
|
||||
## 7. 当前不足
|
||||
|
||||
- RAG 尚未进入"可用链路",只有元数据管理层。
|
||||
- Agent 运行时相关模型与服务尚未开始建设。
|
||||
- 前端部分页面(工作台、附件管理、知识文档)为占位状态。
|
||||
- RAG 尚未形成独立检索问答接口,当前召回能力主要用于 Agent 调试链路。
|
||||
- Agent 运行时尚未持久化会话,工具调用与任务编排仍未落地。
|
||||
- 前端部分页面(附件管理、检索配置、最近任务)为占位或后续能力提示。
|
||||
- 缺少鉴权、租户、操作日志。
|
||||
|
||||
## 8. 建议演进方向
|
||||
|
||||
1. 补 RAG 最小闭环:上传附件 → 建立文档 → 状态流转 → 解析占位。
|
||||
2. 接入 Spring AI,实现最小模型调用链路。
|
||||
3. 建设 Agent 域模型:Agent、Session、Message、Tool、Task。
|
||||
4. 补齐前端占位页面的表单与联调。
|
||||
5. 衔接模型供应商、工作流编排和前端管理台。
|
||||
1. 补 RAG 最小检索闭环:解析文本 → 生成切片 → 生成向量 → 检索召回。
|
||||
2. 把当前 Agent 调试链路升级为会话化运行:沉淀 Session、Message 和上下文裁剪策略。
|
||||
3. 建设 Agent 工具注册与调用协议,补齐任务状态与执行日志。
|
||||
4. 补齐索引任务、重试、重建索引和前端任务视图。
|
||||
5. 衔接模型供应商、Spring AI 适配层、工作流编排和前端管理台。
|
||||
|
||||
|
||||
899
docs/MODEL_PROVIDER_DESIGN.md
Normal file
899
docs/MODEL_PROVIDER_DESIGN.md
Normal file
@@ -0,0 +1,899 @@
|
||||
# 模型服务商配置与路由设计文档
|
||||
|
||||
## 1. 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 所属项目 | Common Agent |
|
||||
| 文档类型 | 设计文档 |
|
||||
| 编写日期 | 2026-05-25 |
|
||||
| 对应需求 | `docs/MODEL_PROVIDER_REQUIREMENTS.md` |
|
||||
| 目标阶段 | RAG 向量导入与模型网关基础能力 |
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
本设计用于在 Common Agent 中新增模型服务商配置与模型路由能力,使系统可以统一接入 Ollama、硅基流动、百炼、OpenAI 等模型服务,并为 RAG 和后续 Agent 运行时提供统一模型调用入口。
|
||||
|
||||
核心设计目标:
|
||||
|
||||
1. 业务模块只依赖平台内部模型网关,不直接依赖具体服务商。
|
||||
2. 首期优先支持 OpenAI-compatible 协议,减少多服务商适配成本。
|
||||
3. 支持本地模型和云端模型混用,实现成本控制。
|
||||
4. 支持 RAG Embedding 模型配置化,并保证同一知识库向量模型一致。
|
||||
5. 支持调用日志,为排障、统计和后续成本分析打基础。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Admin["管理控制台"] --> ProviderApi["模型配置 API"]
|
||||
Rag["RAG 模块"] --> ModelGateway["ModelGateway"]
|
||||
Agent["Agent 模块"] --> ModelGateway
|
||||
ModelGateway --> Router["ModelRouteService"]
|
||||
Router --> Config["模型配置与路由表"]
|
||||
ModelGateway --> ClientFactory["ModelClientFactory"]
|
||||
ClientFactory --> Ollama["Ollama OpenAI-compatible"]
|
||||
ClientFactory --> SiliconFlow["SiliconFlow OpenAI-compatible"]
|
||||
ClientFactory --> DashScope["DashScope OpenAI-compatible"]
|
||||
ClientFactory --> Other["其他 OpenAI-compatible 服务"]
|
||||
ModelGateway --> Log["ModelCallLogService"]
|
||||
Rag --> RagChunk["rag_chunk"]
|
||||
Rag --> RagEmbedding["rag_chunk_embedding"]
|
||||
```
|
||||
|
||||
设计上将模型平台能力拆成四层:
|
||||
|
||||
| 层级 | 职责 |
|
||||
|------|------|
|
||||
| 配置层 | 管理服务商、模型、路由规则和知识库模型绑定 |
|
||||
| 路由层 | 根据任务类型、范围和策略选择具体模型 |
|
||||
| 调用层 | 通过统一客户端调用 Chat、Embedding、Rerank 等能力 |
|
||||
| 观测层 | 记录调用日志、耗时、token、费用估算和错误信息 |
|
||||
|
||||
## 4. 包结构设计
|
||||
|
||||
建议新增后端包:
|
||||
|
||||
```text
|
||||
src/main/java/com/bruce/modelprovider
|
||||
├── controller
|
||||
├── dto
|
||||
│ ├── request
|
||||
│ └── response
|
||||
├── entity
|
||||
├── enums
|
||||
├── mapper
|
||||
├── service
|
||||
│ └── impl
|
||||
├── gateway
|
||||
├── client
|
||||
└── config
|
||||
```
|
||||
|
||||
各包职责:
|
||||
|
||||
| 包 | 职责 |
|
||||
|----|------|
|
||||
| `controller` | 对外暴露服务商、模型、路由规则、日志查询接口 |
|
||||
| `dto` | 请求和响应对象,不直接暴露实体 |
|
||||
| `entity` | 数据库实体,继承 `BaseEntity` |
|
||||
| `enums` | 服务商类型、模型类型、任务类型、路由策略、调用状态 |
|
||||
| `mapper` | MyBatis-Plus `BaseMapper` |
|
||||
| `service` | 配置管理、路由选择、调用日志 |
|
||||
| `gateway` | 面向业务模块的模型调用入口 |
|
||||
| `client` | 具体协议客户端,例如 OpenAI-compatible 客户端 |
|
||||
| `config` | 模型平台配置,例如默认超时、批量大小、密钥加密开关 |
|
||||
|
||||
## 5. 核心数据模型
|
||||
|
||||
### 5.1 `model_provider` 模型服务商表
|
||||
|
||||
用于保存服务商基础配置。
|
||||
|
||||
```sql
|
||||
CREATE TABLE model_provider (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
provider_code VARCHAR(64) NOT NULL,
|
||||
provider_name VARCHAR(100) NOT NULL,
|
||||
provider_type VARCHAR(50) NOT NULL,
|
||||
protocol_type VARCHAR(50) NOT NULL DEFAULT 'OPENAI_COMPATIBLE',
|
||||
base_url VARCHAR(500) NOT NULL,
|
||||
auth_type VARCHAR(50) NOT NULL DEFAULT 'BEARER_TOKEN',
|
||||
secret_ref VARCHAR(200),
|
||||
api_key_cipher TEXT,
|
||||
timeout_ms INTEGER NOT NULL DEFAULT 60000,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
health_status VARCHAR(50) NOT NULL DEFAULT 'UNKNOWN',
|
||||
last_health_check_time TIMESTAMP,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_provider_code UNIQUE (provider_code)
|
||||
);
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `provider_code` | 服务商编码,例如 `ollama-main`、`siliconflow` |
|
||||
| `provider_type` | 服务商类型,例如 `OLLAMA`、`SILICONFLOW`、`DASHSCOPE`、`OPENAI`、`CUSTOM` |
|
||||
| `protocol_type` | 协议类型,首期使用 `OPENAI_COMPATIBLE` |
|
||||
| `base_url` | API 基础地址,例如 `https://api.siliconflow.cn/v1` |
|
||||
| `secret_ref` | 密钥引用,例如环境变量名或配置中心键 |
|
||||
| `api_key_cipher` | 可选的加密密钥内容,前端不返回 |
|
||||
| `health_status` | 健康检查状态 |
|
||||
|
||||
### 5.2 `model_config` 模型配置表
|
||||
|
||||
用于保存服务商下的具体模型。
|
||||
|
||||
```sql
|
||||
CREATE TABLE model_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
provider_id BIGINT NOT NULL,
|
||||
model_code VARCHAR(100) NOT NULL,
|
||||
model_name VARCHAR(200) NOT NULL,
|
||||
upstream_model VARCHAR(200) NOT NULL,
|
||||
model_type VARCHAR(50) NOT NULL,
|
||||
context_window INTEGER,
|
||||
max_output_tokens INTEGER,
|
||||
embedding_dimension INTEGER,
|
||||
input_price_per_1k NUMERIC(12, 8),
|
||||
output_price_per_1k NUMERIC(12, 8),
|
||||
local_model BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
default_model BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
capabilities_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
options_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_config_provider_code UNIQUE (provider_id, model_code),
|
||||
CONSTRAINT fk_model_config_provider_id FOREIGN KEY (provider_id) REFERENCES model_provider (id)
|
||||
);
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `model_code` | 平台内部模型编码 |
|
||||
| `model_name` | 展示名称 |
|
||||
| `upstream_model` | 上游真实模型名,例如 `Qwen/Qwen3-Embedding-0.6B`、`qwen2.5:7b` |
|
||||
| `model_type` | `CHAT`、`EMBEDDING`、`RERANK`、`MULTIMODAL` |
|
||||
| `embedding_dimension` | Embedding 输出维度,RAG 首期使用 1024 |
|
||||
| `capabilities_json` | 能力标签,例如是否支持工具调用、视觉、JSON 输出 |
|
||||
| `options_json` | 模型调用默认参数,例如 `temperature`、`topP`、`dimensions` |
|
||||
|
||||
### 5.3 `model_route_rule` 模型路由规则表
|
||||
|
||||
用于根据任务类型和范围选择模型。
|
||||
|
||||
```sql
|
||||
CREATE TABLE model_route_rule (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
route_code VARCHAR(100) NOT NULL,
|
||||
route_name VARCHAR(100) NOT NULL,
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
match_scope VARCHAR(50) NOT NULL DEFAULT 'GLOBAL',
|
||||
scope_id BIGINT,
|
||||
primary_model_id BIGINT NOT NULL,
|
||||
fallback_model_ids_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
route_strategy VARCHAR(50) NOT NULL DEFAULT 'MANUAL',
|
||||
max_latency_ms INTEGER,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_route_rule_code UNIQUE (route_code),
|
||||
CONSTRAINT fk_model_route_primary_model_id FOREIGN KEY (primary_model_id) REFERENCES model_config (id)
|
||||
);
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `task_type` | 任务类型,例如 `RAG_EMBEDDING`、`CHAT_SIMPLE`、`AGENT_PLANNING` |
|
||||
| `match_scope` | 匹配范围,例如 `GLOBAL`、`RAG_STORE`、`AGENT` |
|
||||
| `scope_id` | 范围 ID,例如知识库 ID 或 Agent ID |
|
||||
| `primary_model_id` | 主模型 |
|
||||
| `fallback_model_ids_json` | 备用模型 ID 列表 |
|
||||
| `route_strategy` | `LOCAL_FIRST`、`COST_FIRST`、`QUALITY_FIRST`、`MANUAL` |
|
||||
|
||||
### 5.4 `rag_store_model_config` 知识库模型绑定表
|
||||
|
||||
用于固定知识库的 Embedding 模型和维度,避免同一知识库混用向量空间。
|
||||
|
||||
```sql
|
||||
CREATE TABLE rag_store_model_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
store_id BIGINT NOT NULL,
|
||||
embedding_model_id BIGINT NOT NULL,
|
||||
embedding_dimension INTEGER NOT NULL DEFAULT 1024,
|
||||
chunk_strategy INTEGER,
|
||||
chunk_size INTEGER,
|
||||
chunk_overlap INTEGER,
|
||||
delimiter VARCHAR(50),
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
index_version INTEGER NOT NULL DEFAULT 1,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_rag_store_model_config_store_active UNIQUE (store_id, active),
|
||||
CONSTRAINT fk_rag_store_model_config_store_id FOREIGN KEY (store_id) REFERENCES rag_store (id),
|
||||
CONSTRAINT fk_rag_store_model_config_embedding_model_id FOREIGN KEY (embedding_model_id) REFERENCES model_config (id)
|
||||
);
|
||||
```
|
||||
|
||||
设计约束:
|
||||
|
||||
- 首期每个知识库只有一个生效配置。
|
||||
- 更换 Embedding 模型或维度时,`index_version` 增加,并触发重建索引。
|
||||
- 检索时只使用当前生效配置对应的向量。
|
||||
|
||||
### 5.5 `model_call_log` 模型调用日志表
|
||||
|
||||
用于记录模型调用行为。
|
||||
|
||||
```sql
|
||||
CREATE TABLE model_call_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
provider_id BIGINT,
|
||||
model_id BIGINT,
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
biz_type VARCHAR(50),
|
||||
biz_id VARCHAR(100),
|
||||
call_type VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
total_tokens INTEGER,
|
||||
estimated_cost NUMERIC(14, 8),
|
||||
duration_ms INTEGER,
|
||||
request_hash VARCHAR(64),
|
||||
error_code VARCHAR(100),
|
||||
error_message VARCHAR(1000),
|
||||
create_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
CONSTRAINT uk_model_call_log_request_id UNIQUE (request_id)
|
||||
);
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `request_id` | 单次模型调用请求 ID |
|
||||
| `task_type` | 任务类型 |
|
||||
| `biz_type` | 业务类型,例如 `RAG_DOCUMENT_INDEX` |
|
||||
| `biz_id` | 业务 ID,例如文档 ID |
|
||||
| `call_type` | `CHAT`、`EMBEDDING`、`RERANK` |
|
||||
| `status` | `SUCCESS`、`FAILED`、`TIMEOUT`、`FALLBACK_SUCCESS` |
|
||||
| `request_hash` | 请求内容哈希,用于排障和幂等分析,不保存完整敏感内容 |
|
||||
|
||||
## 6. 枚举设计
|
||||
|
||||
新增枚举应实现现有 `PersistableSysEnumDefinition`,并同步到 `sys_enum`。
|
||||
|
||||
| 枚举 | 值 |
|
||||
|------|----|
|
||||
| `ModelProviderTypeEnum` | `OLLAMA`、`SILICONFLOW`、`DASHSCOPE`、`OPENAI`、`CUSTOM` |
|
||||
| `ModelProtocolTypeEnum` | `OPENAI_COMPATIBLE` |
|
||||
| `ModelTypeEnum` | `CHAT`、`EMBEDDING`、`RERANK`、`MULTIMODAL` |
|
||||
| `ModelTaskTypeEnum` | `RAG_EMBEDDING`、`RAG_QUERY_EMBEDDING`、`RAG_ANSWER`、`CHAT_SIMPLE`、`CHAT_COMPLEX`、`AGENT_PLANNING`、`RERANK` |
|
||||
| `ModelRouteStrategyEnum` | `LOCAL_FIRST`、`COST_FIRST`、`QUALITY_FIRST`、`MANUAL` |
|
||||
| `ModelCallStatusEnum` | `SUCCESS`、`FAILED`、`TIMEOUT`、`FALLBACK_SUCCESS` |
|
||||
| `ModelHealthStatusEnum` | `UNKNOWN`、`HEALTHY`、`UNHEALTHY` |
|
||||
|
||||
## 7. 服务接口设计
|
||||
|
||||
### 7.1 配置管理服务
|
||||
|
||||
```java
|
||||
public interface IModelProviderService extends IService<ModelProvider> {
|
||||
List<ModelProviderResponse> query(ModelProviderQueryRequest request);
|
||||
ModelProviderResponse getResponseById(Long id);
|
||||
boolean saveOrUpdate(ModelProviderSaveRequest request);
|
||||
boolean checkHealth(Long id);
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public interface IModelConfigService extends IService<ModelConfig> {
|
||||
List<ModelConfigResponse> query(ModelConfigQueryRequest request);
|
||||
ModelConfigResponse getResponseById(Long id);
|
||||
boolean saveOrUpdate(ModelConfigSaveRequest request);
|
||||
ModelConfig getEnabledModel(Long modelId);
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 路由服务
|
||||
|
||||
```java
|
||||
public interface IModelRouteService {
|
||||
ModelRouteDecision route(ModelRouteContext context);
|
||||
}
|
||||
```
|
||||
|
||||
`ModelRouteContext` 应包含:
|
||||
|
||||
- `taskType`
|
||||
- `matchScope`
|
||||
- `scopeId`
|
||||
- `requiredModelType`
|
||||
- `preferredLocal`
|
||||
- `requiredEmbeddingDimension`
|
||||
- `bizType`
|
||||
- `bizId`
|
||||
|
||||
`ModelRouteDecision` 应包含:
|
||||
|
||||
- 主模型。
|
||||
- 备用模型列表。
|
||||
- 路由策略。
|
||||
- 决策原因。
|
||||
|
||||
### 7.3 模型网关
|
||||
|
||||
```java
|
||||
public interface EmbeddingModelGateway {
|
||||
EmbeddingResult embed(EmbeddingRequest request);
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public interface ChatModelGateway {
|
||||
ChatResult chat(ChatRequest request);
|
||||
}
|
||||
```
|
||||
|
||||
`EmbeddingRequest` 应包含:
|
||||
|
||||
- 文本列表。
|
||||
- 任务类型。
|
||||
- 匹配范围。
|
||||
- 范围 ID。
|
||||
- 业务类型。
|
||||
- 业务 ID。
|
||||
- 期望维度。
|
||||
|
||||
`EmbeddingResult` 应包含:
|
||||
|
||||
- 模型 ID。
|
||||
- 模型名称。
|
||||
- 维度。
|
||||
- 向量列表。
|
||||
- 调用日志 ID。
|
||||
|
||||
## 8. 客户端设计
|
||||
|
||||
首期优先实现 `OpenAiCompatibleModelClient`,统一调用以下接口:
|
||||
|
||||
- `POST /v1/embeddings`
|
||||
- `POST /v1/chat/completions`
|
||||
|
||||
OpenAI-compatible 客户端输入来自数据库配置:
|
||||
|
||||
| 配置来源 | 字段 |
|
||||
|----------|------|
|
||||
| `model_provider.base_url` | 服务基础地址 |
|
||||
| `model_provider.secret_ref` / `api_key_cipher` | 鉴权信息 |
|
||||
| `model_config.upstream_model` | 上游模型名 |
|
||||
| `model_config.options_json` | 调用参数 |
|
||||
| `model_config.embedding_dimension` | Embedding 维度 |
|
||||
|
||||
### 8.1 Spring AI 使用方式
|
||||
|
||||
项目可以在两个阶段使用 Spring AI:
|
||||
|
||||
第一阶段:使用项目自定义 `ModelGateway` 和 OpenAI-compatible HTTP 客户端,优先解决多服务商动态配置问题。
|
||||
|
||||
第二阶段:在稳定后引入 Spring AI 的 `EmbeddingModel`、`ChatModel` 抽象或适配器,将动态客户端包装为平台内部统一接口。
|
||||
|
||||
这样设计的原因是 Spring Boot 自动配置更适合单默认服务商,而本项目需要从数据库动态选择多个 provider。业务层保持 `ModelGateway` 抽象,后续替换底层实现不会影响 RAG 和 Agent。
|
||||
|
||||
### 8.2 Ollama 适配
|
||||
|
||||
Ollama 使用 OpenAI-compatible 地址:
|
||||
|
||||
```text
|
||||
http://<ollama-host>:11434/v1
|
||||
```
|
||||
|
||||
配置示例:
|
||||
|
||||
| 字段 | 示例 |
|
||||
|------|------|
|
||||
| `provider_code` | `ollama-main` |
|
||||
| `provider_type` | `OLLAMA` |
|
||||
| `protocol_type` | `OPENAI_COMPATIBLE` |
|
||||
| `base_url` | `http://10.0.0.10:11434/v1` |
|
||||
| `auth_type` | `NONE` 或 `BEARER_TOKEN` |
|
||||
| `model_config.upstream_model` | `qwen2.5:7b` |
|
||||
|
||||
部署建议:
|
||||
|
||||
- 开发环境可以通过内网访问。
|
||||
- 生产环境不要直接开放 11434 到公网。
|
||||
- 推荐使用 VPN、Tailscale、Cloudflare Tunnel、Nginx 鉴权反向代理或安全网关。
|
||||
- 如果必须公网访问,需要 HTTPS、鉴权、IP 白名单和访问日志。
|
||||
|
||||
### 8.3 硅基流动适配
|
||||
|
||||
配置示例:
|
||||
|
||||
| 字段 | 示例 |
|
||||
|------|------|
|
||||
| `provider_code` | `siliconflow` |
|
||||
| `provider_type` | `SILICONFLOW` |
|
||||
| `protocol_type` | `OPENAI_COMPATIBLE` |
|
||||
| `base_url` | `https://api.siliconflow.cn/v1` |
|
||||
| `secret_ref` | `SILICONFLOW_API_KEY` |
|
||||
| `model_config.upstream_model` | `Qwen/Qwen3-Embedding-0.6B` |
|
||||
| `embedding_dimension` | `1024` |
|
||||
|
||||
RAG 首期推荐使用 1024 维 Embedding,匹配当前 `rag_chunk_embedding.embedding VECTOR(1024)`。
|
||||
|
||||
### 8.4 百炼适配
|
||||
|
||||
百炼 OpenAI-compatible 配置示例:
|
||||
|
||||
| 字段 | 示例 |
|
||||
|------|------|
|
||||
| `provider_code` | `dashscope` |
|
||||
| `provider_type` | `DASHSCOPE` |
|
||||
| `protocol_type` | `OPENAI_COMPATIBLE` |
|
||||
| `base_url` | `https://dashscope.aliyuncs.com/compatible-mode/v1` |
|
||||
| `secret_ref` | `DASHSCOPE_API_KEY` |
|
||||
| `model_config.upstream_model` | `text-embedding-v4` |
|
||||
| `embedding_dimension` | `1024` |
|
||||
|
||||
## 9. 路由策略设计
|
||||
|
||||
### 9.1 路由优先级
|
||||
|
||||
模型路由按以下顺序匹配:
|
||||
|
||||
1. 业务范围精确规则,例如某个知识库或某个 Agent。
|
||||
2. 任务类型规则,例如 `RAG_EMBEDDING`。
|
||||
3. 全局默认规则。
|
||||
4. 模型类型默认模型。
|
||||
|
||||
如果没有匹配到模型,应返回清晰错误,不隐式选择不确定模型。
|
||||
|
||||
### 9.2 策略说明
|
||||
|
||||
| 策略 | 行为 |
|
||||
|------|------|
|
||||
| `MANUAL` | 使用规则中指定的主模型 |
|
||||
| `LOCAL_FIRST` | 优先选择本地模型,失败后使用备用云端模型 |
|
||||
| `COST_FIRST` | 在可用模型中优先选择成本低的模型 |
|
||||
| `QUALITY_FIRST` | 优先选择质量更高或优先级更高的模型 |
|
||||
|
||||
首期可以只实现 `MANUAL` 和 `LOCAL_FIRST`,其余策略先完成数据结构和枚举。
|
||||
|
||||
### 9.3 失败兜底
|
||||
|
||||
主模型调用失败时:
|
||||
|
||||
1. 记录主模型失败日志。
|
||||
2. 判断是否存在备用模型。
|
||||
3. 按备用模型顺序重试。
|
||||
4. 若备用模型成功,返回结果并记录 `FALLBACK_SUCCESS`。
|
||||
5. 若全部失败,返回最后一次错误,并将业务状态更新为失败。
|
||||
|
||||
RAG 向量导入首期应谨慎使用 fallback。Embedding 模型 fallback 只有在维度和语义模型族一致时才允许自动切换,否则应失败并提示重建或重新配置。
|
||||
|
||||
## 10. RAG 向量导入设计
|
||||
|
||||
### 10.1 当前 RAG 状态
|
||||
|
||||
当前项目已有:
|
||||
|
||||
- `rag_document`
|
||||
- `rag_document_parse_result`
|
||||
- `rag_chunk`
|
||||
- `rag_chunk_embedding`
|
||||
- `RagDocumentParseServiceImpl`
|
||||
- `RagDocumentChunkServiceImpl`
|
||||
- `FixedLengthChunker`
|
||||
- `DelimiterChunker`
|
||||
|
||||
下一步需要把切片服务和 Embedding 网关串起来。
|
||||
|
||||
### 10.2 目标流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as 用户/后台
|
||||
participant Doc as RagDocumentService
|
||||
participant Parse as RagDocumentParseService
|
||||
participant Chunk as RagDocumentChunkService
|
||||
participant Gateway as EmbeddingModelGateway
|
||||
participant Provider as 上游模型服务
|
||||
participant DB as PostgreSQL
|
||||
|
||||
User->>Doc: 上传文档
|
||||
Doc->>Parse: 自动解析
|
||||
Parse->>DB: 保存解析快照
|
||||
Parse->>DB: 更新 parseStatus=PARSED
|
||||
Doc->>Chunk: 提交切片索引任务
|
||||
Chunk->>DB: 更新 indexStatus=INDEXING
|
||||
Chunk->>DB: 读取解析快照
|
||||
Chunk->>DB: 删除旧切片和旧向量
|
||||
Chunk->>DB: 写入 rag_chunk
|
||||
Chunk->>Gateway: 批量生成 Embedding
|
||||
Gateway->>Provider: POST /v1/embeddings
|
||||
Provider-->>Gateway: 返回向量
|
||||
Gateway->>DB: 记录 model_call_log
|
||||
Chunk->>DB: 写入 rag_chunk_embedding
|
||||
Chunk->>DB: 更新 indexStatus=INDEXED
|
||||
```
|
||||
|
||||
### 10.3 状态流转
|
||||
|
||||
`rag_document.index_status` 流转:
|
||||
|
||||
```text
|
||||
PENDING -> INDEXING -> INDEXED
|
||||
PENDING -> INDEXING -> FAILED
|
||||
INDEXED -> INDEXING -> INDEXED
|
||||
INDEXED -> INDEXING -> FAILED
|
||||
```
|
||||
|
||||
失败时应写入 `rag_document.error_message`。
|
||||
|
||||
### 10.4 切片与向量写入规则
|
||||
|
||||
1. 生成新切片前,先删除当前文档旧向量,再删除旧切片。
|
||||
2. 新切片保存成功后,再批量调用 Embedding。
|
||||
3. Embedding 成功后写入 `rag_chunk_embedding`。
|
||||
4. 每条 embedding 记录保存 `embedding_model`、`embedding_dimension`、`content_hash`。
|
||||
5. `embedding` 字段以 pgvector 可接受的字符串格式保存,例如 `[0.1,0.2,0.3]`。
|
||||
6. 向量数量必须等于切片数量,否则本次索引失败。
|
||||
|
||||
### 10.5 知识库模型绑定
|
||||
|
||||
RAG 索引选择模型的优先级:
|
||||
|
||||
1. `rag_store_model_config` 中该知识库绑定的 Embedding 模型。
|
||||
2. `model_route_rule` 中 `match_scope=RAG_STORE` 的规则。
|
||||
3. `model_route_rule` 中 `task_type=RAG_EMBEDDING` 的全局规则。
|
||||
4. 全局默认 Embedding 模型。
|
||||
|
||||
如果以上都不存在,索引任务失败并提示需要配置 Embedding 模型。
|
||||
|
||||
## 11. API 设计
|
||||
|
||||
### 11.1 服务商接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/model/providers/query` | 查询服务商 |
|
||||
| GET | `/api/model/providers/detail` | 服务商详情 |
|
||||
| POST | `/api/model/providers/save` | 新增或修改服务商 |
|
||||
| POST | `/api/model/providers/delete` | 删除服务商 |
|
||||
| POST | `/api/model/providers/checkHealth` | 健康检查 |
|
||||
|
||||
服务商详情接口不返回 `apiKeyCipher` 明文。
|
||||
|
||||
### 11.2 模型接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/model/configs/query` | 查询模型 |
|
||||
| GET | `/api/model/configs/detail` | 模型详情 |
|
||||
| POST | `/api/model/configs/save` | 新增或修改模型 |
|
||||
| POST | `/api/model/configs/delete` | 删除模型 |
|
||||
|
||||
### 11.3 路由规则接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/model/routes/query` | 查询路由规则 |
|
||||
| GET | `/api/model/routes/detail` | 路由规则详情 |
|
||||
| POST | `/api/model/routes/save` | 新增或修改路由规则 |
|
||||
| POST | `/api/model/routes/delete` | 删除路由规则 |
|
||||
|
||||
### 11.4 调用日志接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/model/call-logs/query` | 查询调用日志 |
|
||||
| GET | `/api/model/call-logs/detail` | 调用日志详情 |
|
||||
|
||||
### 11.5 RAG 模型配置接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/rag/store/modelConfig` | 查询知识库模型配置 |
|
||||
| POST | `/api/rag/store/modelConfig/save` | 保存知识库模型配置 |
|
||||
| POST | `/api/rag/store/rebuildIndex` | 重建知识库索引 |
|
||||
|
||||
## 12. DTO 设计要点
|
||||
|
||||
所有 API 保持现有项目规范:
|
||||
|
||||
- Controller 返回 `RequestResult<T>`。
|
||||
- 请求对象使用业务语义明确的 `Request` 类,例如 `ModelProviderSaveRequest`。
|
||||
- 响应对象使用业务语义明确的 `Response` 类,例如 `ModelProviderResponse`。
|
||||
- 响应 DTO 提供 `fromEntity()` 静态转换。
|
||||
- Long ID 输出给前端时使用 `ToStringSerializer`。
|
||||
|
||||
`ModelProviderResponse` 不包含完整密钥,只包含:
|
||||
|
||||
- `secretRef`
|
||||
- `hasApiKey`
|
||||
- `authType`
|
||||
|
||||
## 13. 密钥处理设计
|
||||
|
||||
### 13.1 推荐方式
|
||||
|
||||
首期推荐使用 `secret_ref`:
|
||||
|
||||
```text
|
||||
model_provider.secret_ref = SILICONFLOW_API_KEY
|
||||
```
|
||||
|
||||
运行时从环境变量读取:
|
||||
|
||||
```text
|
||||
SILICONFLOW_API_KEY=sk-***
|
||||
```
|
||||
|
||||
优点:
|
||||
|
||||
- 数据库不保存密钥。
|
||||
- 本地、测试、生产可以使用不同环境变量。
|
||||
- 实现简单,风险较低。
|
||||
|
||||
### 13.2 可选加密方式
|
||||
|
||||
如需在数据库中保存密钥,应保存 `api_key_cipher`,并使用环境变量中的主密钥加解密:
|
||||
|
||||
```text
|
||||
COMMON_AGENT_SECRET_KEY=<secret-from-environment>
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- 主密钥不入库。
|
||||
- 前端不返回密钥明文。
|
||||
- 修改密钥时只允许写入,不允许读取原文。
|
||||
|
||||
## 14. 调用日志与费用估算
|
||||
|
||||
调用日志记录在 `model_call_log`。
|
||||
|
||||
费用估算规则:
|
||||
|
||||
```text
|
||||
estimatedCost = promptTokens / 1000 * inputPricePer1k
|
||||
+ completionTokens / 1000 * outputPricePer1k
|
||||
```
|
||||
|
||||
Embedding 模型通常只使用输入 token 费用。若上游不返回 token 信息,首期可以记录空值,后续通过本地 tokenizer 或文本长度估算。
|
||||
|
||||
日志脱敏规则:
|
||||
|
||||
- 不记录完整 API Key。
|
||||
- 不默认记录完整 prompt。
|
||||
- 可记录请求哈希。
|
||||
- 错误信息截断到 1000 字以内。
|
||||
|
||||
## 15. 健康检查设计
|
||||
|
||||
健康检查目标是判断服务商是否可用。
|
||||
|
||||
首期检查方式:
|
||||
|
||||
- OpenAI-compatible 服务调用 `/v1/models`。
|
||||
- Ollama 同样可通过 OpenAI-compatible `/v1/models` 检查。
|
||||
- 如果服务商不支持 `/v1/models`,可配置跳过健康检查或使用轻量模型调用。
|
||||
|
||||
健康状态:
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `UNKNOWN` | 尚未检查 |
|
||||
| `HEALTHY` | 最近一次检查成功 |
|
||||
| `UNHEALTHY` | 最近一次检查失败 |
|
||||
|
||||
## 16. 异常处理设计
|
||||
|
||||
常见异常:
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| 服务商停用 | 路由阶段直接失败 |
|
||||
| 模型停用 | 路由阶段跳过或失败 |
|
||||
| 缺少 API Key | 调用前失败,提示密钥未配置 |
|
||||
| 上游超时 | 记录 `TIMEOUT`,尝试备用模型 |
|
||||
| 上游返回错误 | 记录 `FAILED`,保留错误码和摘要 |
|
||||
| Embedding 维度不匹配 | RAG 索引失败,不写入向量 |
|
||||
| 向量数量不匹配 | RAG 索引失败,清理本次中间数据 |
|
||||
|
||||
## 17. 前端页面设计
|
||||
|
||||
前端延续当前后台风格,保持信息密度和可扫描性。
|
||||
|
||||
建议新增页面:
|
||||
|
||||
| 页面 | 功能 |
|
||||
|------|------|
|
||||
| 模型服务商 | 服务商列表、启停用、健康检查、编辑 |
|
||||
| 模型配置 | 模型列表、类型筛选、价格和能力配置 |
|
||||
| 路由规则 | 按任务类型配置主模型和备用模型 |
|
||||
| 调用日志 | 查询调用状态、耗时、服务商、模型、错误 |
|
||||
| 知识库模型配置 | 在知识库详情中配置 Embedding 模型和重建索引 |
|
||||
|
||||
首期后端优先,前端可以在 RAG 最小闭环完成后接入。
|
||||
|
||||
## 18. 测试设计
|
||||
|
||||
### 18.1 单元测试
|
||||
|
||||
需要覆盖:
|
||||
|
||||
- 实体字段结构。
|
||||
- Mapper 和 Service 继承结构。
|
||||
- DTO 转换。
|
||||
- 服务商编码唯一校验。
|
||||
- 模型编码唯一校验。
|
||||
- 路由优先级。
|
||||
- 密钥引用解析。
|
||||
- Embedding 维度校验。
|
||||
|
||||
### 18.2 RAG 集成测试
|
||||
|
||||
需要覆盖:
|
||||
|
||||
- 文档解析快照转切片。
|
||||
- 切片批量调用 Embedding 网关。
|
||||
- 向量数量与切片数量一致。
|
||||
- 向量写入 `rag_chunk_embedding`。
|
||||
- 索引状态成功流转。
|
||||
- Embedding 失败时索引状态为 `FAILED`。
|
||||
|
||||
### 18.3 客户端测试
|
||||
|
||||
OpenAI-compatible 客户端使用 Mock Web Server 或类似方式测试:
|
||||
|
||||
- `/v1/embeddings` 成功响应。
|
||||
- `/v1/chat/completions` 成功响应。
|
||||
- 401 鉴权失败。
|
||||
- 429 限流。
|
||||
- 5xx 上游错误。
|
||||
- 超时。
|
||||
|
||||
## 19. 实施计划
|
||||
|
||||
### 19.1 第一阶段:配置数据结构
|
||||
|
||||
1. 新增 SQL 脚本。
|
||||
2. 新增实体、Mapper、Service、Controller。
|
||||
3. 新增枚举并同步 `sys_enum` 测试。
|
||||
4. 新增结构稳定性测试。
|
||||
|
||||
### 19.2 第二阶段:Embedding 网关
|
||||
|
||||
1. 新增 `EmbeddingModelGateway`。
|
||||
2. 新增 OpenAI-compatible Embedding 客户端。
|
||||
3. 新增密钥解析器。
|
||||
4. 新增调用日志服务。
|
||||
5. 完成 Mock 单元测试。
|
||||
|
||||
### 19.3 第三阶段:RAG 向量导入
|
||||
|
||||
1. 扩展 `RagDocumentChunkServiceImpl`。
|
||||
2. 切片后调用 Embedding 网关。
|
||||
3. 写入 `rag_chunk_embedding`。
|
||||
4. 更新 `indexStatus`。
|
||||
5. 完成失败回滚和日志记录。
|
||||
|
||||
### 19.4 第四阶段:路由与前端
|
||||
|
||||
1. 新增路由规则服务。
|
||||
2. RAG 按知识库配置或路由规则选择模型。
|
||||
3. 接入前端模型配置页面。
|
||||
4. 接入调用日志查询页面。
|
||||
|
||||
## 20. 配置示例
|
||||
|
||||
### 20.1 Ollama 服务商
|
||||
|
||||
```json
|
||||
{
|
||||
"providerCode": "ollama-main",
|
||||
"providerName": "远程 Ollama",
|
||||
"providerType": "OLLAMA",
|
||||
"protocolType": "OPENAI_COMPATIBLE",
|
||||
"baseUrl": "http://10.0.0.10:11434/v1",
|
||||
"authType": "NONE",
|
||||
"timeoutMs": 120000,
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### 20.2 硅基流动 Embedding 模型
|
||||
|
||||
```json
|
||||
{
|
||||
"providerId": "1001",
|
||||
"modelCode": "siliconflow-qwen3-embedding-0_6b",
|
||||
"modelName": "硅基流动 Qwen3 Embedding 0.6B",
|
||||
"upstreamModel": "Qwen/Qwen3-Embedding-0.6B",
|
||||
"modelType": "EMBEDDING",
|
||||
"embeddingDimension": 1024,
|
||||
"localModel": false,
|
||||
"enabled": true,
|
||||
"optionsJson": {
|
||||
"dimensions": 1024,
|
||||
"encoding_format": "float"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 20.3 RAG Embedding 路由规则
|
||||
|
||||
```json
|
||||
{
|
||||
"routeCode": "global-rag-embedding",
|
||||
"routeName": "全局 RAG 向量模型",
|
||||
"taskType": "RAG_EMBEDDING",
|
||||
"matchScope": "GLOBAL",
|
||||
"primaryModelId": "2001",
|
||||
"fallbackModelIds": [],
|
||||
"routeStrategy": "MANUAL",
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
## 21. 风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
|------|------|
|
||||
| 动态多服务商与 Spring AI 自动配置存在差异 | 先用项目内部 `ModelGateway` 隔离,后续逐步适配 Spring AI |
|
||||
| Ollama 远程服务暴露风险 | 文档和配置中明确生产环境必须通过安全通道访问 |
|
||||
| Embedding 模型混用导致检索漂移 | 知识库绑定模型和维度,变更时重建索引 |
|
||||
| 云端 API 费用不可控 | 路由策略、调用日志和费用估算逐步完善 |
|
||||
| 上游接口返回格式差异 | 首期只承诺 OpenAI-compatible,特殊服务商后续单独适配 |
|
||||
| 大批量向量化超时 | 支持批量大小配置、异步任务和失败重试 |
|
||||
|
||||
## 22. 后续扩展
|
||||
|
||||
后续可以扩展:
|
||||
|
||||
- Rerank 模型网关。
|
||||
- Agent 级模型配置。
|
||||
- 用户级额度和限流。
|
||||
- 模型质量评测。
|
||||
- 模型调用缓存。
|
||||
- Prompt 模板与模型绑定。
|
||||
- 多索引版本并存。
|
||||
- 调用日志聚合报表。
|
||||
|
||||
## 23. 参考资料
|
||||
|
||||
- Ollama OpenAI-compatible API: https://docs.ollama.com/api/openai-compatibility
|
||||
- Ollama Embeddings: https://docs.ollama.com/capabilities/embeddings
|
||||
- Spring AI OpenAI Embeddings: https://docs.spring.io/spring-ai/reference/api/embeddings/openai-embeddings.html
|
||||
- SiliconFlow Embedding 模型列表: https://www.siliconflow.com/models/embedding
|
||||
|
||||
449
docs/MODEL_PROVIDER_REQUIREMENTS.md
Normal file
449
docs/MODEL_PROVIDER_REQUIREMENTS.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# 模型服务商配置与路由需求文档
|
||||
|
||||
## 1. 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 所属项目 | Common Agent |
|
||||
| 文档类型 | 需求文档 |
|
||||
| 编写日期 | 2026-05-25 |
|
||||
| 目标阶段 | RAG 最小检索闭环与模型平台化前置能力 |
|
||||
| 相关模块 | `model-provider`、`rag-core`、`platform-admin` |
|
||||
|
||||
## 2. 背景
|
||||
|
||||
Common Agent 的长期目标是建设一套可复用的企业级 AI 应用基础平台,后续需要支持 RAG、Agent 编排、工具调用、会话管理和模型统一接入。
|
||||
|
||||
当前项目已经完成基础工程、附件上传、文档解析、RAG 知识库和知识文档管理。下一步需要将解析结果切片、生成向量并写入 `rag_chunk_embedding`,再继续建设检索召回和 Agent 运行时。
|
||||
|
||||
在模型接入层面,项目需要同时支持:
|
||||
|
||||
- 自建模型服务,例如云服务器上的 Ollama。
|
||||
- 云端模型平台,例如硅基流动、阿里云百炼、OpenAI 兼容服务。
|
||||
- 不同任务使用不同模型,例如简单任务走本地小模型,复杂推理走云端大模型。
|
||||
- RAG 向量化使用稳定且可控的 embedding 模型,并保证同一知识库内向量空间一致。
|
||||
|
||||
因此,本阶段需要建设一个轻量但可扩展的模型服务商配置与路由能力,作为 RAG 向量导入和后续 Agent 调用的统一入口。
|
||||
|
||||
## 3. 建设目标
|
||||
|
||||
### 3.1 业务目标
|
||||
|
||||
1. 支持在后台配置多个模型服务商,包括本地 Ollama、硅基流动、百炼和其他 OpenAI-compatible 服务。
|
||||
2. 支持在后台维护每个服务商下的具体模型,例如聊天模型、Embedding 模型、重排序模型。
|
||||
3. 支持按任务类型选择模型,达到“小任务本地处理,大任务云端处理”的成本控制目标。
|
||||
4. 支持 RAG 向量导入从配置中选择 Embedding 模型,不在代码中写死服务商和模型名。
|
||||
5. 支持记录模型调用日志,为后续成本统计、质量评估、失败排查和限流策略提供数据基础。
|
||||
|
||||
### 3.2 技术目标
|
||||
|
||||
1. 抽象统一的 `ModelGateway`,业务模块不直接依赖具体服务商 SDK。
|
||||
2. 优先兼容 OpenAI API 形态,降低接入 Ollama、硅基流动、百炼等平台的差异成本。
|
||||
3. 将 API Key 与业务配置解耦,避免密钥明文散落在业务代码或普通配置中。
|
||||
4. 为后续 Spring AI 接入、Agent 运行时和模型调用观测保留扩展点。
|
||||
5. 保持与当前项目风格一致:Spring Boot、MyBatis-Plus、DTO、统一返回体、结构化枚举。
|
||||
|
||||
## 4. 范围说明
|
||||
|
||||
### 4.1 本阶段范围
|
||||
|
||||
本阶段覆盖以下能力:
|
||||
|
||||
- 模型服务商配置管理。
|
||||
- 模型配置管理。
|
||||
- 模型路由规则管理。
|
||||
- 模型调用日志记录。
|
||||
- Embedding 调用网关。
|
||||
- Chat 调用网关的基础接口设计。
|
||||
- RAG 切片向量导入接入模型网关。
|
||||
- Ollama 远程服务接入设计。
|
||||
- 硅基流动等 OpenAI-compatible 云服务接入设计。
|
||||
|
||||
### 4.2 本阶段不覆盖
|
||||
|
||||
本阶段不实现以下完整能力:
|
||||
|
||||
- 完整 Agent 运行时。
|
||||
- 多租户权限体系。
|
||||
- 复杂模型质量评测平台。
|
||||
- 自动提示词优化。
|
||||
- 按用户或部门计费结算。
|
||||
- 高级模型编排工作流。
|
||||
- 完整密钥管理系统。
|
||||
|
||||
这些能力可以在模型服务商配置与路由能力稳定后继续扩展。
|
||||
|
||||
## 5. 术语定义
|
||||
|
||||
| 术语 | 说明 |
|
||||
|------|------|
|
||||
| 模型服务商 | 提供模型调用能力的上游服务,例如 Ollama、硅基流动、百炼、OpenAI |
|
||||
| 模型配置 | 某个服务商下的具体模型配置,例如 `Qwen/Qwen3-Embedding-0.6B` |
|
||||
| 模型路由 | 根据任务类型、成本、质量、可用性选择具体模型的规则 |
|
||||
| OpenAI-compatible | 兼容 OpenAI API 风格的服务接口,通常包含 `/v1/chat/completions`、`/v1/embeddings` |
|
||||
| 本地模型 | 由用户自行部署和维护的模型服务,例如云服务器上的 Ollama |
|
||||
| 云端模型 | 第三方平台托管的模型服务,例如硅基流动或百炼 |
|
||||
| Embedding | 将文本转换为向量,用于 RAG 检索召回 |
|
||||
| Rerank | 对召回结果二次排序,提高检索结果相关性 |
|
||||
|
||||
## 6. 当前问题
|
||||
|
||||
### 6.1 模型配置不可管理
|
||||
|
||||
当前项目尚未接入统一模型配置层。如果在 RAG 向量化阶段直接写死模型服务商、模型名和 API 地址,后续切换服务商会产生重复改造。
|
||||
|
||||
### 6.2 成本控制缺少入口
|
||||
|
||||
不同任务对模型能力要求不同。简单分类、标题生成、短文本摘要可以使用本地小模型,复杂推理、长上下文问答和 Agent 规划适合使用云端大模型。当前缺少统一路由层,无法系统性节省调用成本。
|
||||
|
||||
### 6.3 RAG 向量模型需要稳定约束
|
||||
|
||||
同一知识库中的向量必须来自同一个语义空间。随意混用不同 Embedding 模型或不同维度,会导致检索结果不稳定。当前 `rag_chunk_embedding` 已有模型名和维度字段,但缺少知识库级别的模型绑定和重建索引约束。
|
||||
|
||||
### 6.4 调用可观测性不足
|
||||
|
||||
模型调用需要记录耗时、状态、错误、token、费用估算和业务来源。当前没有统一日志表,后续无法分析失败原因、成本趋势和模型效果。
|
||||
|
||||
### 6.5 密钥管理需要提前约束
|
||||
|
||||
模型服务商通常需要 API Key。若直接明文存储在数据库或配置文件中,会带来泄露风险。项目早期可以简化实现,但必须明确密钥引用和加密策略。
|
||||
|
||||
## 7. 用户角色
|
||||
|
||||
| 角色 | 诉求 |
|
||||
|------|------|
|
||||
| 平台管理员 | 配置模型服务商、模型、路由规则和密钥引用 |
|
||||
| 知识库维护者 | 为知识库选择 Embedding 模型,触发索引重建 |
|
||||
| Agent 配置者 | 为不同 Agent 选择默认模型或路由策略 |
|
||||
| 系统开发者 | 通过统一网关调用模型,不关心具体服务商差异 |
|
||||
| 运维人员 | 查看模型服务可用性、调用失败、耗时和成本趋势 |
|
||||
|
||||
## 8. 典型使用场景
|
||||
|
||||
### 8.1 RAG 使用硅基流动生成向量
|
||||
|
||||
管理员配置硅基流动服务商,添加 `Qwen/Qwen3-Embedding-0.6B` 模型,设置维度为 1024。知识库选择该模型作为 Embedding 模型。文档上传后,系统解析、切片、调用模型生成向量,并写入 `rag_chunk_embedding`。
|
||||
|
||||
### 8.2 简单任务使用远程 Ollama
|
||||
|
||||
用户在云服务器部署 Ollama,并开放安全访问地址。管理员配置 Ollama 服务商和本地小模型,例如 `qwen2.5:7b`。系统在标题生成、短摘要、分类等任务中优先调用 Ollama,减少云端模型费用。
|
||||
|
||||
### 8.3 复杂任务使用云端大模型
|
||||
|
||||
当任务类型为复杂 Agent 规划、长上下文问答或工具调用编排时,路由规则选择云端大模型。若主模型调用失败,可以按规则切换到备用模型。
|
||||
|
||||
### 8.4 Embedding 模型变更触发索引重建
|
||||
|
||||
知识库已经使用某个 Embedding 模型完成索引。管理员切换该知识库的 Embedding 模型时,系统标记该知识库需要重建索引,并阻止新旧模型向量混合检索。
|
||||
|
||||
### 8.5 查看模型调用日志
|
||||
|
||||
运维人员进入模型调用日志页面,查看某段时间内的调用次数、失败率、平均耗时和费用估算,定位某个服务商或模型的异常。
|
||||
|
||||
## 9. 功能需求
|
||||
|
||||
### 9.1 模型服务商管理
|
||||
|
||||
系统应支持新增、编辑、启用、停用和查询模型服务商。
|
||||
|
||||
服务商至少包含:
|
||||
|
||||
- 服务商编码。
|
||||
- 服务商名称。
|
||||
- 服务商类型。
|
||||
- API 基础地址。
|
||||
- 鉴权方式。
|
||||
- 密钥引用。
|
||||
- 超时时间。
|
||||
- 默认优先级。
|
||||
- 是否启用。
|
||||
- 备注。
|
||||
|
||||
服务商编码应全局唯一。
|
||||
|
||||
### 9.2 模型配置管理
|
||||
|
||||
系统应支持在服务商下维护具体模型。
|
||||
|
||||
模型至少包含:
|
||||
|
||||
- 所属服务商。
|
||||
- 模型编码。
|
||||
- 上游模型名称。
|
||||
- 模型类型。
|
||||
- 上下文窗口。
|
||||
- 输出 token 上限。
|
||||
- Embedding 维度。
|
||||
- 是否本地模型。
|
||||
- 是否默认模型。
|
||||
- 能力标签。
|
||||
- 价格配置。
|
||||
- 是否启用。
|
||||
|
||||
同一服务商下模型编码应唯一。
|
||||
|
||||
### 9.3 模型路由规则管理
|
||||
|
||||
系统应支持按任务类型配置模型路由。
|
||||
|
||||
路由规则至少包含:
|
||||
|
||||
- 任务类型。
|
||||
- 匹配范围。
|
||||
- 主模型。
|
||||
- 备用模型列表。
|
||||
- 路由策略。
|
||||
- 最大允许耗时。
|
||||
- 是否启用。
|
||||
|
||||
任务类型初始支持:
|
||||
|
||||
- RAG 文档向量化。
|
||||
- RAG 查询向量化。
|
||||
- RAG 问答生成。
|
||||
- 简单文本处理。
|
||||
- 复杂文本处理。
|
||||
- Agent 规划。
|
||||
- Rerank。
|
||||
|
||||
### 9.4 模型网关
|
||||
|
||||
系统应提供统一模型网关,供业务模块调用。
|
||||
|
||||
模型网关应支持:
|
||||
|
||||
- 根据任务类型选择模型。
|
||||
- 调用 Embedding 模型生成向量。
|
||||
- 调用 Chat 模型生成回复。
|
||||
- 记录调用日志。
|
||||
- 在主模型失败时按规则尝试备用模型。
|
||||
- 返回统一错误信息。
|
||||
|
||||
业务模块不应直接调用具体服务商接口。
|
||||
|
||||
### 9.5 RAG 向量导入
|
||||
|
||||
系统应将 RAG 文档索引流程接入模型网关。
|
||||
|
||||
向量导入流程应支持:
|
||||
|
||||
1. 读取文档解析快照。
|
||||
2. 按切片策略生成 `rag_chunk`。
|
||||
3. 根据知识库或全局路由选择 Embedding 模型。
|
||||
4. 调用模型生成向量。
|
||||
5. 写入 `rag_chunk_embedding`。
|
||||
6. 更新文档索引状态。
|
||||
7. 记录模型调用日志。
|
||||
|
||||
同一知识库应固定 Embedding 模型和维度。模型变更时应触发重建索引流程。
|
||||
|
||||
### 9.6 Ollama 接入
|
||||
|
||||
系统应支持配置远程 Ollama 服务。
|
||||
|
||||
Ollama 接入要求:
|
||||
|
||||
- 支持 OpenAI-compatible 地址,例如 `http://host:11434/v1`。
|
||||
- 支持 Chat 模型。
|
||||
- 支持 Embedding 模型。
|
||||
- 支持模型健康检查。
|
||||
- 支持设置本地模型标签。
|
||||
|
||||
Ollama 远程访问不应直接裸露在公网。生产环境应通过 VPN、内网、反向代理鉴权、Tailscale、Cloudflare Tunnel 或安全网关访问。
|
||||
|
||||
### 9.7 云端 OpenAI-compatible 服务接入
|
||||
|
||||
系统应支持硅基流动、百炼、OpenAI 或其他兼容接口。
|
||||
|
||||
接入要求:
|
||||
|
||||
- 服务商可配置 `baseUrl`。
|
||||
- 服务商可配置 `apiKey` 引用。
|
||||
- 模型可配置上游模型名。
|
||||
- Embedding 支持设置维度。
|
||||
- Chat 支持设置温度、最大输出等参数。
|
||||
|
||||
### 9.8 调用日志
|
||||
|
||||
系统应记录模型调用日志。
|
||||
|
||||
日志至少包含:
|
||||
|
||||
- 请求 ID。
|
||||
- 服务商。
|
||||
- 模型。
|
||||
- 任务类型。
|
||||
- 业务来源。
|
||||
- 调用状态。
|
||||
- 耗时。
|
||||
- token 数量。
|
||||
- 费用估算。
|
||||
- 错误码。
|
||||
- 错误消息。
|
||||
- 请求哈希。
|
||||
- 创建时间。
|
||||
|
||||
日志中不得记录完整 API Key。
|
||||
|
||||
### 9.9 后台管理界面
|
||||
|
||||
后台管理界面应逐步提供:
|
||||
|
||||
- 服务商列表。
|
||||
- 模型列表。
|
||||
- 路由规则配置。
|
||||
- 调用日志查询。
|
||||
- 模型健康检查结果。
|
||||
- 知识库 Embedding 配置入口。
|
||||
|
||||
首期可先完成后端接口和 SQL 结构,前端在 RAG 最小闭环后接入。
|
||||
|
||||
## 10. 非功能需求
|
||||
|
||||
### 10.1 可扩展性
|
||||
|
||||
模型服务商接入应通过统一接口扩展。新增 OpenAI-compatible 服务商时,原则上只需要新增配置,不需要修改业务调用代码。
|
||||
|
||||
### 10.2 可用性
|
||||
|
||||
模型调用应支持超时控制和失败兜底。云端模型失败时,可以根据路由规则切换备用模型。本地 Ollama 不可用时,不应阻塞整个系统启动。
|
||||
|
||||
### 10.3 性能
|
||||
|
||||
Embedding 批量生成应支持批处理,减少网络请求次数。批量大小应可配置,避免超过上游接口限制。
|
||||
|
||||
### 10.4 可观测性
|
||||
|
||||
每次模型调用应有请求 ID,并记录耗时、状态和错误原因。后续可接入监控告警。
|
||||
|
||||
### 10.5 可维护性
|
||||
|
||||
新增核心代码应补充中文注释,说明类职责、关键分支和扩展边界。新增枚举应同步纳入系统枚举初始化测试。
|
||||
|
||||
## 11. 数据安全需求
|
||||
|
||||
1. API Key 不得写死在代码中。
|
||||
2. API Key 不应明文返回给前端。
|
||||
3. 数据库可保存 `secretRef` 或加密后的 `apiKeyCipher`。
|
||||
4. 若保存加密密钥,主加密密钥应来自环境变量或外部配置,不应存储在数据库。
|
||||
5. 调用日志不得保存完整请求正文中的敏感信息。
|
||||
6. 管理接口后续应接入权限控制。
|
||||
7. Ollama 远程服务应限制访问来源,避免未授权公网调用。
|
||||
|
||||
## 12. 成本控制需求
|
||||
|
||||
系统应支持以下成本控制策略:
|
||||
|
||||
- 本地优先:简单任务优先走 Ollama。
|
||||
- 成本优先:优先选择单价较低的云模型。
|
||||
- 质量优先:复杂任务优先选择能力更强的模型。
|
||||
- 手动指定:某个知识库或 Agent 固定使用指定模型。
|
||||
- 失败兜底:主模型失败后切换备用模型。
|
||||
|
||||
调用日志应记录费用估算所需字段,为后续成本报表提供基础。
|
||||
|
||||
## 13. RAG 特殊约束
|
||||
|
||||
### 13.1 向量维度约束
|
||||
|
||||
当前 `rag_chunk_embedding.embedding` 使用 `VECTOR(1024)`。因此首期 Embedding 模型应统一输出 1024 维向量。
|
||||
|
||||
适配方向:
|
||||
|
||||
- 硅基流动:可使用 `Qwen/Qwen3-Embedding-0.6B`,配置 1024 维。
|
||||
- Ollama:应选择输出维度与表结构匹配的 embedding 模型。
|
||||
- 百炼:可使用支持 1024 维的文本向量模型。
|
||||
|
||||
### 13.2 模型一致性约束
|
||||
|
||||
同一知识库同一索引版本内,应使用同一个 Embedding 模型和维度。
|
||||
|
||||
当知识库 Embedding 模型或维度变化时:
|
||||
|
||||
- 已有索引应标记为需要重建。
|
||||
- 新索引写入前应清理旧切片向量,或写入新的索引版本。
|
||||
- 检索时应只使用当前生效模型生成的向量。
|
||||
|
||||
### 13.3 失败状态约束
|
||||
|
||||
向量生成失败时,文档 `indexStatus` 应更新为 `FAILED`,并记录失败原因。部分切片成功、部分失败时,首期按整个文档索引失败处理,避免半成品参与检索。
|
||||
|
||||
## 14. 验收标准
|
||||
|
||||
### 14.1 服务商配置验收
|
||||
|
||||
- 可以新增一个 Ollama 服务商。
|
||||
- 可以新增一个硅基流动服务商。
|
||||
- 可以为服务商配置至少一个 Chat 模型和一个 Embedding 模型。
|
||||
- 服务商编码和模型编码唯一性校验生效。
|
||||
|
||||
### 14.2 路由验收
|
||||
|
||||
- 可以为 RAG 文档向量化配置默认 Embedding 模型。
|
||||
- 可以为简单文本处理配置 Ollama 模型。
|
||||
- 可以为复杂文本处理配置云端模型。
|
||||
- 主模型不可用时,可以根据配置切换备用模型。
|
||||
|
||||
### 14.3 RAG 向量导入验收
|
||||
|
||||
- 文档解析完成后,可以生成切片。
|
||||
- 切片可以调用配置的 Embedding 模型生成 1024 维向量。
|
||||
- 向量可以写入 `rag_chunk_embedding`。
|
||||
- 文档索引状态可以从 `PENDING` 流转到 `INDEXING`、`INDEXED` 或 `FAILED`。
|
||||
- 调用日志可以记录 Embedding 调用信息。
|
||||
|
||||
### 14.4 安全验收
|
||||
|
||||
- 前端查询服务商详情时不返回明文 API Key。
|
||||
- 调用日志中不包含完整 API Key。
|
||||
- 未配置密钥时,云端服务调用失败信息清晰。
|
||||
- Ollama 远程地址可配置,但文档中明确生产环境访问安全要求。
|
||||
|
||||
### 14.5 测试验收
|
||||
|
||||
- 后端新增实体、Mapper、Service、Controller 结构测试。
|
||||
- 模型路由单元测试。
|
||||
- Embedding 网关单元测试。
|
||||
- RAG 向量导入服务单元测试。
|
||||
- 配置缺失、模型停用、服务商停用、调用失败等异常测试。
|
||||
|
||||
## 15. 分阶段里程碑
|
||||
|
||||
### 15.1 阶段一:模型配置基础
|
||||
|
||||
- 新增模型服务商表。
|
||||
- 新增模型配置表。
|
||||
- 新增基础 CRUD 接口。
|
||||
- 支持 OpenAI-compatible 服务商配置。
|
||||
- 支持密钥引用字段。
|
||||
|
||||
### 15.2 阶段二:Embedding 网关与 RAG 接入
|
||||
|
||||
- 新增 Embedding 调用网关。
|
||||
- 接入硅基流动或 Ollama Embedding。
|
||||
- RAG 切片后调用 Embedding 并写入 `rag_chunk_embedding`。
|
||||
- 更新文档索引状态。
|
||||
- 记录调用日志。
|
||||
|
||||
### 15.3 阶段三:模型路由与失败兜底
|
||||
|
||||
- 新增模型路由规则。
|
||||
- 支持按任务类型选择模型。
|
||||
- 支持主备模型调用。
|
||||
- 支持本地优先、成本优先、质量优先策略。
|
||||
|
||||
### 15.4 阶段四:后台管理与成本观测
|
||||
|
||||
- 前端接入服务商管理。
|
||||
- 前端接入模型管理。
|
||||
- 前端接入路由规则管理。
|
||||
- 前端接入调用日志查询。
|
||||
- 增加费用估算和调用趋势展示。
|
||||
|
||||
## 16. 参考资料
|
||||
|
||||
- Ollama OpenAI-compatible API: https://docs.ollama.com/api/openai-compatibility
|
||||
- Ollama Embeddings: https://docs.ollama.com/capabilities/embeddings
|
||||
- Spring AI OpenAI Embeddings: https://docs.spring.io/spring-ai/reference/api/embeddings/openai-embeddings.html
|
||||
- SiliconFlow Embedding 模型列表: https://www.siliconflow.com/models/embedding
|
||||
|
||||
263
docs/MODEL_PROVIDER_SCHEMA.sql
Normal file
263
docs/MODEL_PROVIDER_SCHEMA.sql
Normal file
@@ -0,0 +1,263 @@
|
||||
-- 模型平台与RAG模型绑定核心表(首期手工维护,后续可迁移到 Flyway/Liquibase)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_provider (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
provider_code VARCHAR(64) NOT NULL,
|
||||
provider_name VARCHAR(100) NOT NULL,
|
||||
provider_type VARCHAR(50) NOT NULL,
|
||||
protocol_type VARCHAR(50) NOT NULL DEFAULT 'OPENAI_COMPATIBLE',
|
||||
base_url VARCHAR(500) NOT NULL,
|
||||
auth_type VARCHAR(50) NOT NULL DEFAULT 'BEARER_TOKEN',
|
||||
secret_ref VARCHAR(200),
|
||||
api_key_cipher TEXT,
|
||||
timeout_ms INTEGER NOT NULL DEFAULT 60000,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
health_status VARCHAR(50) NOT NULL DEFAULT 'UNKNOWN',
|
||||
last_health_check_time TIMESTAMP,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_provider_code UNIQUE (provider_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
provider_id BIGINT NOT NULL,
|
||||
model_code VARCHAR(100) NOT NULL,
|
||||
model_name VARCHAR(200) NOT NULL,
|
||||
upstream_model VARCHAR(200) NOT NULL,
|
||||
model_type VARCHAR(50) NOT NULL,
|
||||
context_window INTEGER,
|
||||
max_output_tokens INTEGER,
|
||||
embedding_dimension INTEGER,
|
||||
input_price_per_1k NUMERIC(12, 8),
|
||||
output_price_per_1k NUMERIC(12, 8),
|
||||
local_model BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
default_model BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
capabilities_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
options_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_config_provider_code UNIQUE (provider_id, model_code),
|
||||
CONSTRAINT fk_model_config_provider_id FOREIGN KEY (provider_id) REFERENCES model_provider (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_route_rule (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
route_code VARCHAR(100) NOT NULL,
|
||||
route_name VARCHAR(100) NOT NULL,
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
match_scope VARCHAR(50) NOT NULL DEFAULT 'GLOBAL',
|
||||
scope_id BIGINT,
|
||||
primary_model_id BIGINT NOT NULL,
|
||||
fallback_model_ids_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
route_strategy VARCHAR(50) NOT NULL DEFAULT 'MANUAL',
|
||||
max_latency_ms INTEGER,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_route_rule_code UNIQUE (route_code),
|
||||
CONSTRAINT fk_model_route_primary_model_id FOREIGN KEY (primary_model_id) REFERENCES model_config (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rag_store_model_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
store_id BIGINT NOT NULL,
|
||||
embedding_model_id BIGINT NOT NULL,
|
||||
embedding_dimension INTEGER NOT NULL DEFAULT 1024,
|
||||
chunk_strategy INTEGER,
|
||||
chunk_size INTEGER,
|
||||
chunk_overlap INTEGER,
|
||||
delimiter VARCHAR(50),
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
index_version INTEGER NOT NULL DEFAULT 1,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_rag_store_model_config_store_active UNIQUE (store_id, active),
|
||||
CONSTRAINT fk_rag_store_model_config_store_id FOREIGN KEY (store_id) REFERENCES rag_store (id),
|
||||
CONSTRAINT fk_rag_store_model_config_embedding_model_id FOREIGN KEY (embedding_model_id) REFERENCES model_config (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
agent_code VARCHAR(100) NOT NULL,
|
||||
agent_name VARCHAR(200) NOT NULL,
|
||||
system_prompt TEXT,
|
||||
store_id BIGINT NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ENABLED',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_definition_code UNIQUE (agent_code),
|
||||
CONSTRAINT fk_agent_definition_store_id FOREIGN KEY (store_id) REFERENCES rag_store (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_call_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
provider_id BIGINT,
|
||||
model_id BIGINT,
|
||||
task_type VARCHAR(50) NOT NULL,
|
||||
biz_type VARCHAR(50),
|
||||
biz_id VARCHAR(100),
|
||||
call_type VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
total_tokens INTEGER,
|
||||
estimated_cost NUMERIC(14, 8),
|
||||
duration_ms INTEGER,
|
||||
request_hash VARCHAR(64),
|
||||
error_code VARCHAR(100),
|
||||
error_message VARCHAR(1000),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_model_call_log_request_id UNIQUE (request_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE model_provider IS '模型服务商配置表';
|
||||
COMMENT ON COLUMN model_provider.id IS 'ID';
|
||||
COMMENT ON COLUMN model_provider.provider_code IS '服务商编码';
|
||||
COMMENT ON COLUMN model_provider.provider_name IS '服务商名称';
|
||||
COMMENT ON COLUMN model_provider.provider_type IS '服务商类型';
|
||||
COMMENT ON COLUMN model_provider.protocol_type IS '协议类型';
|
||||
COMMENT ON COLUMN model_provider.base_url IS '服务基础地址';
|
||||
COMMENT ON COLUMN model_provider.auth_type IS '鉴权类型';
|
||||
COMMENT ON COLUMN model_provider.secret_ref IS '密钥环境变量引用';
|
||||
COMMENT ON COLUMN model_provider.api_key_cipher IS '密钥密文';
|
||||
COMMENT ON COLUMN model_provider.timeout_ms IS '超时时间(毫秒)';
|
||||
COMMENT ON COLUMN model_provider.priority IS '优先级';
|
||||
COMMENT ON COLUMN model_provider.enabled IS '是否启用';
|
||||
COMMENT ON COLUMN model_provider.health_status IS '健康状态';
|
||||
COMMENT ON COLUMN model_provider.last_health_check_time IS '最近健康检查时间';
|
||||
COMMENT ON COLUMN model_provider.version IS '版本';
|
||||
COMMENT ON COLUMN model_provider.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN model_provider.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN model_provider.remark IS '备注';
|
||||
COMMENT ON COLUMN model_provider.create_by IS '创建者';
|
||||
COMMENT ON COLUMN model_provider.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE model_config IS '模型配置表';
|
||||
COMMENT ON COLUMN model_config.id IS 'ID';
|
||||
COMMENT ON COLUMN model_config.provider_id IS '服务商ID';
|
||||
COMMENT ON COLUMN model_config.model_code IS '模型编码';
|
||||
COMMENT ON COLUMN model_config.model_name IS '模型名称';
|
||||
COMMENT ON COLUMN model_config.upstream_model IS '上游模型名称';
|
||||
COMMENT ON COLUMN model_config.model_type IS '模型类型';
|
||||
COMMENT ON COLUMN model_config.context_window IS '上下文窗口大小';
|
||||
COMMENT ON COLUMN model_config.max_output_tokens IS '最大输出Token数';
|
||||
COMMENT ON COLUMN model_config.embedding_dimension IS '向量维度';
|
||||
COMMENT ON COLUMN model_config.input_price_per_1k IS '输入千Token单价';
|
||||
COMMENT ON COLUMN model_config.output_price_per_1k IS '输出千Token单价';
|
||||
COMMENT ON COLUMN model_config.local_model IS '是否本地模型';
|
||||
COMMENT ON COLUMN model_config.default_model IS '是否默认模型';
|
||||
COMMENT ON COLUMN model_config.capabilities_json IS '能力配置JSON';
|
||||
COMMENT ON COLUMN model_config.options_json IS '扩展选项JSON';
|
||||
COMMENT ON COLUMN model_config.enabled IS '是否启用';
|
||||
COMMENT ON COLUMN model_config.version IS '版本';
|
||||
COMMENT ON COLUMN model_config.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN model_config.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN model_config.remark IS '备注';
|
||||
COMMENT ON COLUMN model_config.create_by IS '创建者';
|
||||
COMMENT ON COLUMN model_config.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE model_route_rule IS '模型路由规则表';
|
||||
COMMENT ON COLUMN model_route_rule.id IS 'ID';
|
||||
COMMENT ON COLUMN model_route_rule.route_code IS '路由规则编码';
|
||||
COMMENT ON COLUMN model_route_rule.route_name IS '路由规则名称';
|
||||
COMMENT ON COLUMN model_route_rule.task_type IS '任务类型';
|
||||
COMMENT ON COLUMN model_route_rule.match_scope IS '匹配范围';
|
||||
COMMENT ON COLUMN model_route_rule.scope_id IS '匹配范围业务ID';
|
||||
COMMENT ON COLUMN model_route_rule.primary_model_id IS '主模型ID';
|
||||
COMMENT ON COLUMN model_route_rule.fallback_model_ids_json IS '降级模型ID列表JSON';
|
||||
COMMENT ON COLUMN model_route_rule.route_strategy IS '路由策略';
|
||||
COMMENT ON COLUMN model_route_rule.max_latency_ms IS '最大延迟限制(毫秒)';
|
||||
COMMENT ON COLUMN model_route_rule.enabled IS '是否启用';
|
||||
COMMENT ON COLUMN model_route_rule.version IS '版本';
|
||||
COMMENT ON COLUMN model_route_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN model_route_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN model_route_rule.remark IS '备注';
|
||||
COMMENT ON COLUMN model_route_rule.create_by IS '创建者';
|
||||
COMMENT ON COLUMN model_route_rule.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE rag_store_model_config IS '知识库模型配置表';
|
||||
COMMENT ON COLUMN rag_store_model_config.id IS 'ID';
|
||||
COMMENT ON COLUMN rag_store_model_config.store_id IS '知识库ID';
|
||||
COMMENT ON COLUMN rag_store_model_config.embedding_model_id IS 'Embedding模型ID';
|
||||
COMMENT ON COLUMN rag_store_model_config.embedding_dimension IS '向量维度';
|
||||
COMMENT ON COLUMN rag_store_model_config.chunk_strategy IS '切片策略';
|
||||
COMMENT ON COLUMN rag_store_model_config.chunk_size IS '切片大小';
|
||||
COMMENT ON COLUMN rag_store_model_config.chunk_overlap IS '切片重叠大小';
|
||||
COMMENT ON COLUMN rag_store_model_config.delimiter IS '切片分隔符';
|
||||
COMMENT ON COLUMN rag_store_model_config.active IS '是否生效';
|
||||
COMMENT ON COLUMN rag_store_model_config.index_version IS '索引版本号';
|
||||
COMMENT ON COLUMN rag_store_model_config.version IS '版本';
|
||||
COMMENT ON COLUMN rag_store_model_config.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN rag_store_model_config.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN rag_store_model_config.remark IS '备注';
|
||||
COMMENT ON COLUMN rag_store_model_config.create_by IS '创建者';
|
||||
COMMENT ON COLUMN rag_store_model_config.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE agent_definition IS 'Agent定义表';
|
||||
COMMENT ON COLUMN agent_definition.id IS 'ID';
|
||||
COMMENT ON COLUMN agent_definition.agent_code IS 'Agent编码';
|
||||
COMMENT ON COLUMN agent_definition.agent_name IS 'Agent名称';
|
||||
COMMENT ON COLUMN agent_definition.system_prompt IS '系统提示词';
|
||||
COMMENT ON COLUMN agent_definition.store_id IS '绑定知识库ID';
|
||||
COMMENT ON COLUMN agent_definition.status IS '状态';
|
||||
COMMENT ON COLUMN agent_definition.version IS '版本';
|
||||
COMMENT ON COLUMN agent_definition.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN agent_definition.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN agent_definition.remark IS '备注';
|
||||
COMMENT ON COLUMN agent_definition.create_by IS '创建者';
|
||||
COMMENT ON COLUMN agent_definition.update_by IS '更新者';
|
||||
|
||||
COMMENT ON TABLE model_call_log IS '模型调用日志表';
|
||||
COMMENT ON COLUMN model_call_log.id IS 'ID';
|
||||
COMMENT ON COLUMN model_call_log.request_id IS '请求唯一ID';
|
||||
COMMENT ON COLUMN model_call_log.provider_id IS '服务商ID';
|
||||
COMMENT ON COLUMN model_call_log.model_id IS '模型ID';
|
||||
COMMENT ON COLUMN model_call_log.task_type IS '任务类型';
|
||||
COMMENT ON COLUMN model_call_log.biz_type IS '业务类型';
|
||||
COMMENT ON COLUMN model_call_log.biz_id IS '业务ID';
|
||||
COMMENT ON COLUMN model_call_log.call_type IS '调用类型';
|
||||
COMMENT ON COLUMN model_call_log.status IS '调用状态';
|
||||
COMMENT ON COLUMN model_call_log.prompt_tokens IS '输入Token数';
|
||||
COMMENT ON COLUMN model_call_log.completion_tokens IS '输出Token数';
|
||||
COMMENT ON COLUMN model_call_log.total_tokens IS '总Token数';
|
||||
COMMENT ON COLUMN model_call_log.estimated_cost IS '预估成本';
|
||||
COMMENT ON COLUMN model_call_log.duration_ms IS '耗时(毫秒)';
|
||||
COMMENT ON COLUMN model_call_log.request_hash IS '请求哈希';
|
||||
COMMENT ON COLUMN model_call_log.error_code IS '错误码';
|
||||
COMMENT ON COLUMN model_call_log.error_message IS '错误信息摘要';
|
||||
COMMENT ON COLUMN model_call_log.version IS '版本';
|
||||
COMMENT ON COLUMN model_call_log.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN model_call_log.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN model_call_log.remark IS '备注';
|
||||
COMMENT ON COLUMN model_call_log.create_by IS '创建者';
|
||||
COMMENT ON COLUMN model_call_log.update_by IS '更新者';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Common Agent 开发路线图
|
||||
|
||||
本文档基于 2026-05-21 当前分支代码整理,用来区分"已经完成""建议优先做""中期建设项"。
|
||||
本文档基于 2026-05-27 当前分支代码整理,用来区分"已经完成""建议优先做""中期建设项"。
|
||||
|
||||
## 已完成
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- Spring Boot 4.0.6 后端工程初始化。
|
||||
- PostgreSQL 数据源与多环境配置文件(dev / template)。
|
||||
- MyBatis-Plus 3.5.16、Lombok、Springdoc OpenAPI 2.8.13 已接入。
|
||||
- Apache Tika 3.2.3 已接入,用于文档文本抽取。
|
||||
- Maven Wrapper。
|
||||
|
||||
### 公共能力
|
||||
@@ -18,6 +19,8 @@
|
||||
- `RequestResult<T>` 统一 API 返回体。
|
||||
- `GlobalExceptionHandler` 全局异常处理。
|
||||
- `AttachmentProperties` 附件本地存储配置。
|
||||
- 文档解析抽象:`DocumentParser`、`DocumentParserFactory`、`DocumentParseContext`、`DocumentParseResult`。
|
||||
- Tika 解析实现:TXT/Markdown/LOG、PDF、Word、Excel。
|
||||
- `sys_enum` 完整能力:实体、Mapper、Service、Controller、DTO 层。
|
||||
- 支持单条增删改查、批量新增、管理端条件查询(含关键词搜索)。
|
||||
- 批量新增内含重复值校验。
|
||||
@@ -27,10 +30,17 @@
|
||||
### RAG 基础能力
|
||||
|
||||
- `rag_store`、`rag_document` 表结构与实体定义。
|
||||
- `rag_chunk`、`rag_chunk_embedding` 表结构与实体定义。
|
||||
- RAG 知识库完整 CRUD(含编码唯一性校验)。
|
||||
- 知识文档条件查询服务。
|
||||
- 知识文档完整 CRUD、条件查询、批量上传。
|
||||
- 文档解析入口:支持批量提交文档 ID,状态流转为 PARSING / PARSED / FAILED。
|
||||
- 解析快照:解析成功后保存 `rag_document_parse_result`,供后续切片复用。
|
||||
- 切片基础能力:已完成 `Chunker`、`ChunkerFactory`、定长切片和分隔符切片。
|
||||
- 文档切片入口:支持按切片策略异步生成并替换 `rag_chunk`。
|
||||
- 知识库总览接口与单库文档概览接口。
|
||||
- RAG 解析状态枚举 `RagParseStatusEnum`(UPLOADED / PARSING / PARSED / FAILED)。
|
||||
- RAG 索引状态枚举 `RagIndexStatusEnum`(PENDING / INDEXING / INDEXED / FAILED)。
|
||||
- RAG 切片策略枚举 `RagChunkStrategyEnum`(FIXED_LENGTH / PARAGRAPH / HEADING / TABLE_ROW / DELIMITER / SEMANTIC)。
|
||||
- RAG 来源常量 `RagSystemConstants`。
|
||||
|
||||
### 前端控制台
|
||||
@@ -38,41 +48,54 @@
|
||||
- Vue 3 + TypeScript + Vite + Element Plus + Pinia + Vue Router 工程。
|
||||
- `AdminLayout.vue` 管理后台布局(侧边栏菜单 + 主内容区)。
|
||||
- 系统枚举管理页:完整 CRUD + 批量新增对话框 + 关键词搜索 + 响应式布局。
|
||||
- 知识库管理页:完整 CRUD + 概览卡片 + 双栏详情 + 编辑对话框。
|
||||
- 知识库管理页:完整 CRUD + 概览卡片 + 双栏详情 + 编辑对话框 + 批量上传入口。
|
||||
- 知识文档管理页:条件查询 + 批量上传 + 解析重试 + 批量切片入口 + 编辑/启停用/删除。
|
||||
- RAG 工作台与切片任务页:展示文档解析/切片概览并提供切片入口。
|
||||
- RAG 文档批量上传组件:支持锁定知识库或选择知识库上传。
|
||||
- Agent 管理页:支持 Agent 定义新增、编辑、删除、状态管理和知识库绑定。
|
||||
- Agent 调试页:支持普通对话 / RAG 对话切换、请求 ID 与引用切片回显。
|
||||
- API 层:Axios 封装 + Long 类型安全解析 + 统一错误拦截。
|
||||
- 单元测试:Vitest + @vue/test-utils,覆盖路由、布局、页面和 API。
|
||||
|
||||
### 模型平台与 Agent 最小链路
|
||||
|
||||
- 模型服务商、模型配置、路由规则、知识库模型绑定、调用日志核心表结构已落地(`docs/MODEL_PROVIDER_SCHEMA.sql`)。
|
||||
- `EmbeddingModelGateway` 和 `ChatModelGateway` 已落地,统一走 OpenAI-compatible 协议调用上游模型。
|
||||
- `AgentDefinitionController` 与 `AgentDefinitionServiceImpl` 已提供 Agent CRUD 与调试对话接口。
|
||||
- Agent 调试链路已接入:问题向量化 -> `rag_chunk_embedding` 召回 -> 组装上下文 -> Chat 回答 -> 返回引用切片。
|
||||
|
||||
### 质量保障
|
||||
|
||||
- 后端结构稳定性单元测试。
|
||||
- 前端组件与 API 单元测试。
|
||||
- Agent 结构与服务单元测试(`AgentComponentStructureTests`、`AgentDefinitionServiceImplTests`)。
|
||||
|
||||
## 短期优先级
|
||||
|
||||
建议优先完成下面几项,把 RAG 元数据管理层升级为可用的业务闭环:
|
||||
建议优先完成下面几项,把 RAG 上传解析切片链路升级为可检索链路:
|
||||
|
||||
1. 知识库文档上传接口:上传文件后自动创建 `rag_document` 记录。
|
||||
2. 文档解析任务入口与状态流转。
|
||||
3. 向量化任务入口与状态流转。
|
||||
4. 知识库文档新增、详情、启停用、重试等管理接口。
|
||||
5. 前端附件管理页面联调。
|
||||
6. 前端知识文档页面联调。
|
||||
1. 打通文档切片后的全量向量写入,确保 `rag_chunk_embedding` 可持续更新。
|
||||
2. 新增独立 RAG 检索问答接口,避免仅依赖 Agent 调试入口消费召回能力。
|
||||
3. 索引任务入口:把文档或知识库的 `indexStatus` 推进为真实状态流转。
|
||||
4. 补齐重建索引、失败重试、最近任务接口。
|
||||
5. 落地 Agent 会话持久化(`agent_session`、`agent_message`)与多轮上下文管理。
|
||||
6. 补齐 Agent 工具注册、工具调用协议和任务执行日志。
|
||||
|
||||
## RAG 最小闭环
|
||||
|
||||
在基础规范层补齐后,当前 RAG 元数据层已完成,下一步建设业务闭环:
|
||||
当前上传与解析入口已完成,下一步建设最小检索闭环:
|
||||
|
||||
1. 附件上传后自动创建 `rag_document` 记录。
|
||||
2. 建立文档解析任务入口(占位解析器)。
|
||||
3. 解析状态、索引状态按流程流转。
|
||||
4. 接入占位向量化接口。
|
||||
5. 提供知识库文档管理完整接口(新增、详情、启停用、重试、删除)。
|
||||
1. ~~批量上传文件,自动创建 `sys_attachment` 与 `rag_document`。~~
|
||||
2. ~~调用解析入口,使用 Tika 抽取文本并更新 `parseStatus`。~~
|
||||
3. ~~根据切片策略生成 `rag_chunk`。~~
|
||||
4. 调用 Embedding 模型生成向量并写入 `rag_chunk_embedding`(已被 Agent 调试链路消费)。
|
||||
5. 提供独立检索接口,按 query 向量召回切片并返回引用元数据。
|
||||
|
||||
## Agent 核心能力
|
||||
|
||||
RAG 数据链路稳定后,再进入 Agent 主线:
|
||||
Agent 主线能力按以下顺序继续推进:
|
||||
|
||||
1. Agent 定义管理。
|
||||
1. ~~Agent 定义管理。~~
|
||||
2. 会话与消息模型。
|
||||
3. 工具注册与工具调用协议。
|
||||
4. Prompt 模板管理。
|
||||
@@ -98,6 +121,11 @@ RAG 数据链路稳定后,再进入 Agent 主线:
|
||||
- 上传接口返回模型:`SysAttachmentResponse`。
|
||||
- 枚举查询接口规范:POST `/api/sys-enum/queryForManagement`。
|
||||
- RAG 文档状态字段:`parseStatus` + `indexStatus` + `enabled`。
|
||||
- RAG 文档批量上传接口:POST `/api/rag/documents/batchUpload`。
|
||||
- RAG 文档解析接口:POST `/api/rag/documents/parse`,当前同步解析、保存解析快照并返回解析元数据。
|
||||
- RAG 文档切片接口:POST `/api/rag/documents/chunk`,当前异步生成并替换 `rag_chunk`。
|
||||
- Agent 管理接口:`/api/agents/list`、`/api/agents/query`、`/api/agents/detail`、`/api/agents/save`、`/api/agents/delete`。
|
||||
- Agent 调试接口:POST `/api/agents/{agentId}/chat`,支持 `ragEnabled` 开关。
|
||||
- 大整数 ID 通过 `@JsonSerialize(ToStringSerializer.class)` 输出为字符串。
|
||||
|
||||
## 里程碑
|
||||
@@ -109,14 +137,16 @@ RAG 数据链路稳定后,再进入 Agent 主线:
|
||||
|
||||
### 里程碑 2:RAG 可演示
|
||||
|
||||
- 实现知识库文档上传、建档、状态流转。
|
||||
- 预留解析和索引任务接口。
|
||||
- 前端知识库页面完整联调。
|
||||
- 已完成知识库文档上传、建档、解析状态流转和手动切片持久化。
|
||||
- 下一步完成模型配置、向量化和检索接口。
|
||||
- 前端知识库、知识文档、RAG 工作台和切片任务页面已接入当前接口,索引任务和检索配置待接入。
|
||||
|
||||
### 里程碑 3:Agent 最小运行时
|
||||
|
||||
- 支持一个可配置 Agent、一个会话、一次模型调用、一次工具调用。
|
||||
- 已完成:支持一个可配置 Agent、一次模型调用与 RAG 召回调试链路。
|
||||
- 待完成:会话持久化、工具调用与任务编排。
|
||||
|
||||
### 里程碑 4:平台管理化
|
||||
|
||||
- 补齐前端占位页面联调与后台配置能力,形成完整平台雏形。
|
||||
- 补齐前端占位页面联调与后台配置能力,形成完整平台雏形。
|
||||
|
||||
|
||||
432
docs/STUDIO_PROTOTYPE_SCHEMA.sql
Normal file
432
docs/STUDIO_PROTOTYPE_SCHEMA.sql
Normal file
@@ -0,0 +1,432 @@
|
||||
-- Common Agent Studio prototype schema draft.
|
||||
-- These tables extend the current RAG, model-provider and agent_definition data model
|
||||
-- without replacing existing core entities.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS studio_project (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_code VARCHAR(100) NOT NULL,
|
||||
project_name VARCHAR(200) NOT NULL,
|
||||
environment VARCHAR(50) NOT NULL DEFAULT 'DEV',
|
||||
publish_status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
current_version VARCHAR(50),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_studio_project_code UNIQUE (project_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_id BIGINT,
|
||||
workflow_code VARCHAR(100) NOT NULL,
|
||||
workflow_name VARCHAR(200) NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
bound_agent_id BIGINT,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_workflow_definition_code UNIQUE (workflow_code),
|
||||
CONSTRAINT fk_workflow_definition_project_id FOREIGN KEY (project_id) REFERENCES studio_project (id),
|
||||
CONSTRAINT fk_workflow_definition_agent_id FOREIGN KEY (bound_agent_id) REFERENCES agent_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_version (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
workflow_id BIGINT NOT NULL,
|
||||
version_no INTEGER NOT NULL,
|
||||
snapshot_name VARCHAR(100) NOT NULL,
|
||||
graph_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
publish_status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
published_time TIMESTAMP,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_workflow_version_no UNIQUE (workflow_id, version_no),
|
||||
CONSTRAINT fk_workflow_version_workflow_id FOREIGN KEY (workflow_id) REFERENCES workflow_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_run (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
workflow_id BIGINT,
|
||||
workflow_version_id BIGINT,
|
||||
agent_id BIGINT,
|
||||
run_source VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
input_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
output_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
duration_ms INTEGER,
|
||||
estimated_cost NUMERIC(14, 8),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_workflow_run_request_id UNIQUE (request_id),
|
||||
CONSTRAINT fk_workflow_run_workflow_id FOREIGN KEY (workflow_id) REFERENCES workflow_definition (id),
|
||||
CONSTRAINT fk_workflow_run_version_id FOREIGN KEY (workflow_version_id) REFERENCES workflow_version (id),
|
||||
CONSTRAINT fk_workflow_run_agent_id FOREIGN KEY (agent_id) REFERENCES agent_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_run_step (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
run_id BIGINT NOT NULL,
|
||||
node_id VARCHAR(100) NOT NULL,
|
||||
node_type VARCHAR(50) NOT NULL,
|
||||
node_name VARCHAR(200),
|
||||
status VARCHAR(50) NOT NULL,
|
||||
input_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
output_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
duration_ms INTEGER,
|
||||
error_message VARCHAR(1000),
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT fk_workflow_run_step_run_id FOREIGN KEY (run_id) REFERENCES workflow_run (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mcp_server (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
server_code VARCHAR(100) NOT NULL,
|
||||
server_name VARCHAR(200) NOT NULL,
|
||||
import_type VARCHAR(50) NOT NULL,
|
||||
endpoint_url VARCHAR(500),
|
||||
package_name VARCHAR(200),
|
||||
manifest_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
auth_type VARCHAR(50),
|
||||
secret_ref VARCHAR(200),
|
||||
health_status VARCHAR(50) NOT NULL DEFAULT 'UNKNOWN',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_mcp_server_code UNIQUE (server_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mcp_capability (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
server_id BIGINT NOT NULL,
|
||||
capability_code VARCHAR(150) NOT NULL,
|
||||
capability_name VARCHAR(200) NOT NULL,
|
||||
capability_type VARCHAR(50) NOT NULL,
|
||||
schema_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_mcp_capability_code UNIQUE (server_id, capability_code),
|
||||
CONSTRAINT fk_mcp_capability_server_id FOREIGN KEY (server_id) REFERENCES mcp_server (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_definition (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_code VARCHAR(100) NOT NULL,
|
||||
skill_name VARCHAR(200) NOT NULL,
|
||||
skill_type VARCHAR(50) NOT NULL,
|
||||
description VARCHAR(1000),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_skill_definition_code UNIQUE (skill_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_version (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_id BIGINT NOT NULL,
|
||||
version_no INTEGER NOT NULL,
|
||||
prompt_text TEXT,
|
||||
code_text TEXT,
|
||||
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
variable_schema_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
test_result_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
publish_status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
published_time TIMESTAMP,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_skill_version_no UNIQUE (skill_id, version_no),
|
||||
CONSTRAINT fk_skill_version_skill_id FOREIGN KEY (skill_id) REFERENCES skill_definition (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_session (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_code VARCHAR(100) NOT NULL,
|
||||
agent_id BIGINT NOT NULL,
|
||||
workflow_run_id BIGINT,
|
||||
title VARCHAR(200),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_session_code UNIQUE (session_code),
|
||||
CONSTRAINT fk_agent_session_agent_id FOREIGN KEY (agent_id) REFERENCES agent_definition (id),
|
||||
CONSTRAINT fk_agent_session_run_id FOREIGN KEY (workflow_run_id) REFERENCES workflow_run (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
session_id BIGINT NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
citation_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
token_count INTEGER,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT fk_agent_message_session_id FOREIGN KEY (session_id) REFERENCES agent_session (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_capability_binding (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
owner_type VARCHAR(50) NOT NULL,
|
||||
owner_id BIGINT NOT NULL,
|
||||
capability_type VARCHAR(50) NOT NULL,
|
||||
capability_id BIGINT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
create_time TIMESTAMP,
|
||||
update_time TIMESTAMP,
|
||||
remark VARCHAR(500) DEFAULT '',
|
||||
create_by VARCHAR(64),
|
||||
update_by VARCHAR(64),
|
||||
CONSTRAINT uk_agent_capability_binding UNIQUE (owner_type, owner_id, capability_type, capability_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_studio_project_environment ON studio_project (environment);
|
||||
CREATE INDEX IF NOT EXISTS idx_studio_project_publish_status ON studio_project (publish_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_definition_project_id ON workflow_definition (project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_definition_status ON workflow_definition (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_version_workflow_id ON workflow_version (workflow_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_run_workflow_id ON workflow_run (workflow_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_run_status ON workflow_run (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_run_step_run_id ON workflow_run_step (run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_session_agent_id ON agent_session (agent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_session_status ON agent_session (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_message_session_id ON agent_message (session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_message_role ON agent_message (role);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_capability_owner ON agent_capability_binding (owner_type, owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_capability_type ON agent_capability_binding (capability_type, capability_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_server_import_type ON mcp_server (import_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_server_health_status ON mcp_server (health_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_capability_server_id ON mcp_capability (server_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_capability_type ON mcp_capability (capability_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_definition_status ON skill_definition (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_definition_type ON skill_definition (skill_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_version_skill_id ON skill_version (skill_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_version_publish_status ON skill_version (publish_status);
|
||||
|
||||
COMMENT ON TABLE studio_project IS 'Studio项目空间表';
|
||||
COMMENT ON COLUMN studio_project.id IS 'ID';
|
||||
COMMENT ON COLUMN studio_project.project_code IS '项目编码';
|
||||
COMMENT ON COLUMN studio_project.project_name IS '项目名称';
|
||||
COMMENT ON COLUMN studio_project.environment IS '环境';
|
||||
COMMENT ON COLUMN studio_project.publish_status IS '发布状态';
|
||||
COMMENT ON COLUMN studio_project.current_version IS '当前发布版本';
|
||||
COMMENT ON COLUMN studio_project.version IS '版本';
|
||||
COMMENT ON COLUMN studio_project.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN studio_project.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN studio_project.remark IS '备注';
|
||||
COMMENT ON COLUMN studio_project.create_by IS '创建者';
|
||||
COMMENT ON COLUMN studio_project.update_by IS '更新者';
|
||||
COMMENT ON TABLE workflow_definition IS 'Workflow定义表';
|
||||
COMMENT ON COLUMN workflow_definition.id IS 'ID';
|
||||
COMMENT ON COLUMN workflow_definition.project_id IS '所属项目ID';
|
||||
COMMENT ON COLUMN workflow_definition.workflow_code IS 'Workflow编码';
|
||||
COMMENT ON COLUMN workflow_definition.workflow_name IS 'Workflow名称';
|
||||
COMMENT ON COLUMN workflow_definition.description IS 'Workflow描述';
|
||||
COMMENT ON COLUMN workflow_definition.bound_agent_id IS '绑定Agent ID';
|
||||
COMMENT ON COLUMN workflow_definition.status IS '状态';
|
||||
COMMENT ON COLUMN workflow_definition.version IS '版本';
|
||||
COMMENT ON COLUMN workflow_definition.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN workflow_definition.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN workflow_definition.remark IS '备注';
|
||||
COMMENT ON COLUMN workflow_definition.create_by IS '创建者';
|
||||
COMMENT ON COLUMN workflow_definition.update_by IS '更新者';
|
||||
COMMENT ON TABLE workflow_version IS 'Workflow版本快照表';
|
||||
COMMENT ON COLUMN workflow_version.id IS 'ID';
|
||||
COMMENT ON COLUMN workflow_version.workflow_id IS 'Workflow定义ID';
|
||||
COMMENT ON COLUMN workflow_version.version_no IS '版本号';
|
||||
COMMENT ON COLUMN workflow_version.snapshot_name IS '快照名称';
|
||||
COMMENT ON COLUMN workflow_version.graph_json IS '编排图JSON';
|
||||
COMMENT ON COLUMN workflow_version.publish_status IS '发布状态';
|
||||
COMMENT ON COLUMN workflow_version.published_time IS '发布时间';
|
||||
COMMENT ON COLUMN workflow_version.version IS '版本';
|
||||
COMMENT ON COLUMN workflow_version.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN workflow_version.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN workflow_version.remark IS '备注';
|
||||
COMMENT ON COLUMN workflow_version.create_by IS '创建者';
|
||||
COMMENT ON COLUMN workflow_version.update_by IS '更新者';
|
||||
COMMENT ON TABLE workflow_run IS 'Workflow运行记录表';
|
||||
COMMENT ON COLUMN workflow_run.id IS 'ID';
|
||||
COMMENT ON COLUMN workflow_run.request_id IS '请求唯一ID';
|
||||
COMMENT ON COLUMN workflow_run.workflow_id IS 'Workflow定义ID';
|
||||
COMMENT ON COLUMN workflow_run.workflow_version_id IS 'Workflow版本ID';
|
||||
COMMENT ON COLUMN workflow_run.agent_id IS '执行Agent ID';
|
||||
COMMENT ON COLUMN workflow_run.run_source IS '运行来源';
|
||||
COMMENT ON COLUMN workflow_run.status IS '运行状态';
|
||||
COMMENT ON COLUMN workflow_run.input_json IS '输入参数JSON';
|
||||
COMMENT ON COLUMN workflow_run.output_json IS '输出结果JSON';
|
||||
COMMENT ON COLUMN workflow_run.duration_ms IS '耗时(毫秒)';
|
||||
COMMENT ON COLUMN workflow_run.estimated_cost IS '预估成本';
|
||||
COMMENT ON COLUMN workflow_run.version IS '版本';
|
||||
COMMENT ON COLUMN workflow_run.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN workflow_run.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN workflow_run.remark IS '备注';
|
||||
COMMENT ON COLUMN workflow_run.create_by IS '创建者';
|
||||
COMMENT ON COLUMN workflow_run.update_by IS '更新者';
|
||||
COMMENT ON TABLE workflow_run_step IS 'Workflow运行步骤表';
|
||||
COMMENT ON COLUMN workflow_run_step.id IS 'ID';
|
||||
COMMENT ON COLUMN workflow_run_step.run_id IS '运行记录ID';
|
||||
COMMENT ON COLUMN workflow_run_step.node_id IS '节点ID';
|
||||
COMMENT ON COLUMN workflow_run_step.node_type IS '节点类型';
|
||||
COMMENT ON COLUMN workflow_run_step.node_name IS '节点名称';
|
||||
COMMENT ON COLUMN workflow_run_step.status IS '步骤状态';
|
||||
COMMENT ON COLUMN workflow_run_step.input_json IS '步骤输入JSON';
|
||||
COMMENT ON COLUMN workflow_run_step.output_json IS '步骤输出JSON';
|
||||
COMMENT ON COLUMN workflow_run_step.duration_ms IS '耗时(毫秒)';
|
||||
COMMENT ON COLUMN workflow_run_step.error_message IS '错误信息';
|
||||
COMMENT ON COLUMN workflow_run_step.version IS '版本';
|
||||
COMMENT ON COLUMN workflow_run_step.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN workflow_run_step.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN workflow_run_step.remark IS '备注';
|
||||
COMMENT ON COLUMN workflow_run_step.create_by IS '创建者';
|
||||
COMMENT ON COLUMN workflow_run_step.update_by IS '更新者';
|
||||
COMMENT ON TABLE mcp_server IS 'MCP服务表';
|
||||
COMMENT ON COLUMN mcp_server.id IS 'ID';
|
||||
COMMENT ON COLUMN mcp_server.server_code IS '服务编码';
|
||||
COMMENT ON COLUMN mcp_server.server_name IS '服务名称';
|
||||
COMMENT ON COLUMN mcp_server.import_type IS '导入方式';
|
||||
COMMENT ON COLUMN mcp_server.endpoint_url IS '服务端点地址';
|
||||
COMMENT ON COLUMN mcp_server.package_name IS '安装包名称';
|
||||
COMMENT ON COLUMN mcp_server.manifest_json IS 'Manifest声明JSON';
|
||||
COMMENT ON COLUMN mcp_server.auth_type IS '鉴权类型';
|
||||
COMMENT ON COLUMN mcp_server.secret_ref IS '密钥引用';
|
||||
COMMENT ON COLUMN mcp_server.health_status IS '健康状态';
|
||||
COMMENT ON COLUMN mcp_server.enabled IS '是否启用';
|
||||
COMMENT ON COLUMN mcp_server.version IS '版本';
|
||||
COMMENT ON COLUMN mcp_server.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN mcp_server.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN mcp_server.remark IS '备注';
|
||||
COMMENT ON COLUMN mcp_server.create_by IS '创建者';
|
||||
COMMENT ON COLUMN mcp_server.update_by IS '更新者';
|
||||
COMMENT ON TABLE mcp_capability IS 'MCP能力表';
|
||||
COMMENT ON COLUMN mcp_capability.id IS 'ID';
|
||||
COMMENT ON COLUMN mcp_capability.server_id IS '所属服务ID';
|
||||
COMMENT ON COLUMN mcp_capability.capability_code IS '能力编码';
|
||||
COMMENT ON COLUMN mcp_capability.capability_name IS '能力名称';
|
||||
COMMENT ON COLUMN mcp_capability.capability_type IS '能力类型';
|
||||
COMMENT ON COLUMN mcp_capability.schema_json IS '能力Schema JSON';
|
||||
COMMENT ON COLUMN mcp_capability.enabled IS '是否启用';
|
||||
COMMENT ON COLUMN mcp_capability.version IS '版本';
|
||||
COMMENT ON COLUMN mcp_capability.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN mcp_capability.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN mcp_capability.remark IS '备注';
|
||||
COMMENT ON COLUMN mcp_capability.create_by IS '创建者';
|
||||
COMMENT ON COLUMN mcp_capability.update_by IS '更新者';
|
||||
COMMENT ON TABLE skill_definition IS 'Skill定义表';
|
||||
COMMENT ON COLUMN skill_definition.id IS 'ID';
|
||||
COMMENT ON COLUMN skill_definition.skill_code IS 'Skill编码';
|
||||
COMMENT ON COLUMN skill_definition.skill_name IS 'Skill名称';
|
||||
COMMENT ON COLUMN skill_definition.skill_type IS 'Skill类型';
|
||||
COMMENT ON COLUMN skill_definition.description IS 'Skill描述';
|
||||
COMMENT ON COLUMN skill_definition.status IS '状态';
|
||||
COMMENT ON COLUMN skill_definition.version IS '版本';
|
||||
COMMENT ON COLUMN skill_definition.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN skill_definition.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN skill_definition.remark IS '备注';
|
||||
COMMENT ON COLUMN skill_definition.create_by IS '创建者';
|
||||
COMMENT ON COLUMN skill_definition.update_by IS '更新者';
|
||||
COMMENT ON TABLE skill_version IS 'Skill版本表';
|
||||
COMMENT ON COLUMN skill_version.id IS 'ID';
|
||||
COMMENT ON COLUMN skill_version.skill_id IS 'Skill定义ID';
|
||||
COMMENT ON COLUMN skill_version.version_no IS '版本号';
|
||||
COMMENT ON COLUMN skill_version.prompt_text IS '提示词内容';
|
||||
COMMENT ON COLUMN skill_version.code_text IS '代码内容';
|
||||
COMMENT ON COLUMN skill_version.config_json IS '运行配置JSON';
|
||||
COMMENT ON COLUMN skill_version.variable_schema_json IS '变量Schema JSON';
|
||||
COMMENT ON COLUMN skill_version.test_result_json IS '测试结果JSON';
|
||||
COMMENT ON COLUMN skill_version.publish_status IS '发布状态';
|
||||
COMMENT ON COLUMN skill_version.published_time IS '发布时间';
|
||||
COMMENT ON COLUMN skill_version.version IS '版本';
|
||||
COMMENT ON COLUMN skill_version.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN skill_version.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN skill_version.remark IS '备注';
|
||||
COMMENT ON COLUMN skill_version.create_by IS '创建者';
|
||||
COMMENT ON COLUMN skill_version.update_by IS '更新者';
|
||||
COMMENT ON TABLE agent_session IS 'Agent会话表';
|
||||
COMMENT ON COLUMN agent_session.id IS 'ID';
|
||||
COMMENT ON COLUMN agent_session.session_code IS '会话编码';
|
||||
COMMENT ON COLUMN agent_session.agent_id IS 'Agent ID';
|
||||
COMMENT ON COLUMN agent_session.workflow_run_id IS '来源Workflow运行ID';
|
||||
COMMENT ON COLUMN agent_session.title IS '会话标题';
|
||||
COMMENT ON COLUMN agent_session.status IS '会话状态';
|
||||
COMMENT ON COLUMN agent_session.metadata_json IS '会话元数据JSON';
|
||||
COMMENT ON COLUMN agent_session.version IS '版本';
|
||||
COMMENT ON COLUMN agent_session.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN agent_session.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN agent_session.remark IS '备注';
|
||||
COMMENT ON COLUMN agent_session.create_by IS '创建者';
|
||||
COMMENT ON COLUMN agent_session.update_by IS '更新者';
|
||||
COMMENT ON TABLE agent_message IS 'Agent消息表';
|
||||
COMMENT ON COLUMN agent_message.id IS 'ID';
|
||||
COMMENT ON COLUMN agent_message.session_id IS '会话ID';
|
||||
COMMENT ON COLUMN agent_message.role IS '消息角色';
|
||||
COMMENT ON COLUMN agent_message.content IS '消息内容';
|
||||
COMMENT ON COLUMN agent_message.citation_json IS '引用信息JSON';
|
||||
COMMENT ON COLUMN agent_message.token_count IS 'Token数量';
|
||||
COMMENT ON COLUMN agent_message.version IS '版本';
|
||||
COMMENT ON COLUMN agent_message.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN agent_message.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN agent_message.remark IS '备注';
|
||||
COMMENT ON COLUMN agent_message.create_by IS '创建者';
|
||||
COMMENT ON COLUMN agent_message.update_by IS '更新者';
|
||||
COMMENT ON TABLE agent_capability_binding IS 'Agent能力绑定表';
|
||||
COMMENT ON COLUMN agent_capability_binding.id IS 'ID';
|
||||
COMMENT ON COLUMN agent_capability_binding.owner_type IS '绑定主体类型';
|
||||
COMMENT ON COLUMN agent_capability_binding.owner_id IS '绑定主体ID';
|
||||
COMMENT ON COLUMN agent_capability_binding.capability_type IS '能力类型';
|
||||
COMMENT ON COLUMN agent_capability_binding.capability_id IS '能力ID';
|
||||
COMMENT ON COLUMN agent_capability_binding.enabled IS '是否启用';
|
||||
COMMENT ON COLUMN agent_capability_binding.config_json IS '绑定配置JSON';
|
||||
COMMENT ON COLUMN agent_capability_binding.version IS '版本';
|
||||
COMMENT ON COLUMN agent_capability_binding.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN agent_capability_binding.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN agent_capability_binding.remark IS '备注';
|
||||
COMMENT ON COLUMN agent_capability_binding.create_by IS '创建者';
|
||||
COMMENT ON COLUMN agent_capability_binding.update_by IS '更新者';
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# Vue3 Frontend Framework Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在当前仓库根目录下新增 `frontend/` Vue3 前端工程,形成可独立运行的前后端分离管理台骨架。
|
||||
|
||||
**Architecture:** 前端采用 `Vite + Vue 3 + TypeScript + Vue Router + Pinia + Axios`,以 `frontend/` 作为独立工程目录。页面层按“管理台主布局 + 模块页面”组织,接口请求按模块拆到 `api/`,统一解析后端 `RequestResult<T>` 返回体,并通过开发代理转发到 Spring Boot 后端。
|
||||
|
||||
**Tech Stack:** Vue 3、TypeScript、Vite、Vue Router、Pinia、Axios、CSS Variables
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 初始化前端工程目录和基础依赖
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/package.json`
|
||||
- Create: `frontend/tsconfig.json`
|
||||
- Create: `frontend/tsconfig.node.json`
|
||||
- Create: `frontend/vite.config.ts`
|
||||
- Create: `frontend/index.html`
|
||||
- Create: `frontend/.gitignore`
|
||||
|
||||
- [ ] 建立前端工程元数据、脚本和 Vite 配置
|
||||
- [ ] 配置开发服务器代理到后端 `/api`
|
||||
- [ ] 固定 TypeScript 和构建入口
|
||||
|
||||
### Task 2: 建立应用入口与全局框架
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/main.ts`
|
||||
- Create: `frontend/src/App.vue`
|
||||
- Create: `frontend/src/styles/reset.css`
|
||||
- Create: `frontend/src/styles/theme.css`
|
||||
- Create: `frontend/src/styles/global.css`
|
||||
|
||||
- [ ] 注册 Router 与 Pinia
|
||||
- [ ] 建立全局样式、主题变量和基础页面容器
|
||||
- [ ] 保证桌面与移动端都能正常加载
|
||||
|
||||
### Task 3: 建立路由、布局和导航骨架
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/router/index.ts`
|
||||
- Create: `frontend/src/layouts/AdminLayout.vue`
|
||||
- Create: `frontend/src/components/app/AppSidebar.vue`
|
||||
- Create: `frontend/src/components/app/AppHeader.vue`
|
||||
- Create: `frontend/src/components/app/AppShellCard.vue`
|
||||
|
||||
- [ ] 建立管理台布局
|
||||
- [ ] 配置仪表盘、系统枚举、附件管理、RAG知识库、RAG文档路由
|
||||
- [ ] 建立侧边导航和顶部栏
|
||||
|
||||
### Task 4: 建立接口层与通用类型
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/types/http.ts`
|
||||
- Create: `frontend/src/types/sys-enum.ts`
|
||||
- Create: `frontend/src/types/attachment.ts`
|
||||
- Create: `frontend/src/types/rag.ts`
|
||||
- Create: `frontend/src/api/http.ts`
|
||||
- Create: `frontend/src/api/sys-enum.ts`
|
||||
- Create: `frontend/src/api/attachment.ts`
|
||||
- Create: `frontend/src/api/rag.ts`
|
||||
|
||||
- [ ] 建立 `RequestResult<T>` 前端类型
|
||||
- [ ] 建立 Axios 实例和统一响应解包
|
||||
- [ ] 按模块拆分 API 请求方法
|
||||
|
||||
### Task 5: 建立页面骨架与基础交互
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/views/dashboard/DashboardView.vue`
|
||||
- Create: `frontend/src/views/sys-enum/SysEnumView.vue`
|
||||
- Create: `frontend/src/views/attachment/AttachmentView.vue`
|
||||
- Create: `frontend/src/views/rag-store/RagStoreView.vue`
|
||||
- Create: `frontend/src/views/rag-document/RagDocumentView.vue`
|
||||
- Create: `frontend/src/components/common/PageSection.vue`
|
||||
- Create: `frontend/src/components/common/EmptyState.vue`
|
||||
|
||||
- [ ] 建立四个业务页面的查询区、状态卡片和表格占位
|
||||
- [ ] 系统枚举、RAG 页面接入真实查询请求
|
||||
- [ ] 附件页面预留上传表单和结果展示
|
||||
|
||||
### Task 6: 补充工程说明与验证
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Create: `frontend/README.md`
|
||||
|
||||
- [ ] 追加前端启动说明
|
||||
- [ ] 记录 Node 版本、运行命令和代理约定
|
||||
- [ ] 执行 `npm install`、`npm run build` 做基础验证
|
||||
28
docs/前端实现文档/0.前端模块总览.md
Normal file
28
docs/前端实现文档/0.前端模块总览.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 前端模块总览
|
||||
|
||||
## 1. 当前原型入口
|
||||
|
||||
Studio 原型页面位于 `frontend/src/pages/studio/`,路由集中在 `frontend/src/router/index.ts`,模拟数据位于 `frontend/src/data/studioMock.ts`。
|
||||
|
||||
## 2. 页面与模块映射
|
||||
|
||||
| 页面 | 模块 |
|
||||
|------|------|
|
||||
| `StudioDashboardPage.vue` | 工作台与发布就绪 |
|
||||
| `KnowledgeWorkspacePage.vue` | 知识资产 |
|
||||
| `IngestionPipelinePage.vue` | 文件解析管道 |
|
||||
| `ModelWorkspacePage.vue` | 模型与路由 |
|
||||
| `WorkflowBuilderPage.vue` | Workflow 编排 |
|
||||
| `AgentWorkspacePage.vue` | Agent 对话调试 |
|
||||
| `McpImportPage.vue` | MCP 能力接入 |
|
||||
| `SkillWorkspacePage.vue` | Skill 编辑 |
|
||||
| `ObservabilityPage.vue` | 运行观测 |
|
||||
|
||||
## 3. 前端实现原则
|
||||
|
||||
- 页面使用聚合 ViewModel,避免页面直接拼多个低层接口。
|
||||
- 已落地旧接口保持兼容,新 Studio 接口以聚合资源为主。
|
||||
- Long ID 继续按字符串处理,避免 JS 精度问题。
|
||||
- 枚举值按整型协议处理,页面展示通过枚举字典或常量映射。
|
||||
- 管理后台保持信息密度、稳定布局和清晰状态提示。
|
||||
|
||||
29
docs/前端实现文档/1.系统基础模块前端实现.md
Normal file
29
docs/前端实现文档/1.系统基础模块前端实现.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 系统基础模块前端实现
|
||||
|
||||
## 1. 页面范围
|
||||
|
||||
系统基础前端能力主要体现为枚举字典、上传控件和统一请求处理。当前已有系统枚举管理页,后续 Studio 页面继续复用这些能力。
|
||||
|
||||
## 2. API 使用
|
||||
|
||||
| 能力 | 接口 |
|
||||
|------|------|
|
||||
| 枚举管理查询 | `POST /api/sys-enum/queryForManagement` |
|
||||
| 枚举详情 | `GET /api/sys-enum/detail` |
|
||||
| 枚举保存 | `POST /api/sys-enum/save` |
|
||||
| 枚举删除 | `POST /api/sys-enum/delete` |
|
||||
| 附件上传 | `POST /api/attachments/upload` |
|
||||
|
||||
## 3. 实现约定
|
||||
|
||||
- API 层继续使用 `frontend/src/api/request.ts` 解包 `RequestResult<T>`。
|
||||
- Long ID 继续通过 `json-bigint` 安全解析。
|
||||
- 枚举展示通过字典或本地常量映射,不直接依赖数据库展示顺序。
|
||||
- 上传失败时展示后端错误摘要,不吞掉异常。
|
||||
|
||||
## 4. Studio 复用点
|
||||
|
||||
- 文件解析管道复用附件上传能力。
|
||||
- 知识资产、模型路由、Workflow、Agent 等页面复用枚举字典。
|
||||
- 运行观测页面复用统一状态颜色和错误提示格式。
|
||||
|
||||
46
docs/前端实现文档/2.知识资产与文件解析模块前端实现.md
Normal file
46
docs/前端实现文档/2.知识资产与文件解析模块前端实现.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 知识资产与文件解析模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
| 页面 | 职责 |
|
||||
|------|------|
|
||||
| `KnowledgeWorkspacePage.vue` | 展示知识库列表、配置、文档状态和发布影响 |
|
||||
| `IngestionPipelinePage.vue` | 展示上传、解析、切片、向量化和任务日志 |
|
||||
| 旧 `RagStoresPage.vue` | 保留知识库 CRUD 联调参考 |
|
||||
| 旧 `RagDocumentsPage.vue` | 保留知识文档 CRUD 联调参考 |
|
||||
|
||||
## 2. 聚合 ViewModel
|
||||
|
||||
知识工作台建议使用 `KnowledgeWorkspaceView`:
|
||||
|
||||
- 当前知识库基础信息。
|
||||
- 文档健康度。
|
||||
- Embedding 模型和维度。
|
||||
- 检索配置。
|
||||
- 文档列表摘要。
|
||||
- 待处理任务。
|
||||
- 发布影响说明。
|
||||
|
||||
文件解析管道建议使用 `IngestionRunView`:
|
||||
|
||||
- 上传文件列表。
|
||||
- 管道阶段状态。
|
||||
- 解析文本预览。
|
||||
- 切片预览。
|
||||
- 任务日志。
|
||||
|
||||
## 3. 接口草案
|
||||
|
||||
- `GET /api/knowledge/workspaces/{storeId}`
|
||||
- `POST /api/knowledge/ingestion-runs`
|
||||
- `GET /api/knowledge/ingestion-runs/{runId}`
|
||||
- `POST /api/rag/documents/parse`
|
||||
- `POST /api/rag/documents/chunk`
|
||||
|
||||
## 4. 交互状态
|
||||
|
||||
- 解析失败显示可重试状态。
|
||||
- 索引中显示进行中状态,不允许误标为可检索。
|
||||
- 切片参数变更后提示需要重建索引。
|
||||
- Embedding 模型变更后提示会影响当前知识库向量空间。
|
||||
|
||||
31
docs/前端实现文档/3.模型与路由模块前端实现.md
Normal file
31
docs/前端实现文档/3.模型与路由模块前端实现.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 模型与路由模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
`ModelWorkspacePage.vue` 当前展示任务路由规则,保留 `model_provider`、`model_config`、`model_route_rule` 语义。
|
||||
|
||||
## 2. ViewModel
|
||||
|
||||
`ModelRoutingView` 建议包含:
|
||||
|
||||
- 服务商健康概览。
|
||||
- 模型配置列表。
|
||||
- 任务路由规则。
|
||||
- Fallback 关系。
|
||||
- 最近模型调用失败摘要。
|
||||
|
||||
## 3. 接口草案
|
||||
|
||||
- `POST /api/model-providers/query`
|
||||
- `POST /api/model-configs/query`
|
||||
- `POST /api/model-route-rules/query`
|
||||
- `POST /api/model-route-rules/save`
|
||||
- `GET /api/model-call-logs/query`
|
||||
|
||||
## 4. 交互规则
|
||||
|
||||
- 禁用服务商时提示关联模型和路由受影响。
|
||||
- Embedding 模型维度变更时提示知识库需要重建索引。
|
||||
- 草稿路由不得用于生产发布就绪校验。
|
||||
- Fallback 模型为空时在发布检查中提示风险。
|
||||
|
||||
32
docs/前端实现文档/4.Workflow编排模块前端实现.md
Normal file
32
docs/前端实现文档/4.Workflow编排模块前端实现.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Workflow 编排模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
`WorkflowBuilderPage.vue` 提供节点库、画布、运行 Trace 抽屉和节点 Inspector。
|
||||
|
||||
## 2. ViewModel
|
||||
|
||||
`WorkflowBuilderView` 建议包含:
|
||||
|
||||
- Workflow 基本信息。
|
||||
- 当前草稿版本。
|
||||
- 节点列表。
|
||||
- 边列表。
|
||||
- 当前选中节点配置。
|
||||
- 最近测试运行 Trace。
|
||||
|
||||
## 3. 接口草案
|
||||
|
||||
- `GET /api/workflows/{workflowId}`
|
||||
- `POST /api/workflows/save-draft`
|
||||
- `POST /api/workflows/{workflowId}/publish`
|
||||
- `POST /api/workflows/{workflowId}/runs`
|
||||
- `GET /api/workflows/runs/{runId}`
|
||||
|
||||
## 4. 交互规则
|
||||
|
||||
- 保存草稿只更新草稿版本。
|
||||
- 运行测试生成 `workflow_run` 和 `workflow_run_step`。
|
||||
- 发布前调用发布就绪检查。
|
||||
- 节点 Inspector 只编辑当前节点配置,不直接改其他节点。
|
||||
|
||||
34
docs/前端实现文档/5.Agent会话模块前端实现.md
Normal file
34
docs/前端实现文档/5.Agent会话模块前端实现.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Agent 会话模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
`AgentWorkspacePage.vue` 当前展示 Agent 对话调试、引用切片、运行追踪和成本延迟指标。
|
||||
|
||||
## 2. ViewModel
|
||||
|
||||
`AgentWorkspaceView` 建议包含:
|
||||
|
||||
- 当前 Agent 基本信息。
|
||||
- 当前会话消息列表。
|
||||
- 输入框状态。
|
||||
- 引用切片列表。
|
||||
- 模型请求 ID。
|
||||
- 运行追踪步骤。
|
||||
- 成本、延迟和 Token 统计。
|
||||
|
||||
## 3. 接口草案
|
||||
|
||||
- `POST /api/agents/{agentId}/runs`
|
||||
- `GET /api/agents/{agentId}/sessions`
|
||||
- `GET /api/agent-sessions/{sessionId}`
|
||||
- `POST /api/agent-sessions/{sessionId}/messages`
|
||||
|
||||
现有 `POST /api/agents/{agentId}/chat` 保留为兼容调试入口。
|
||||
|
||||
## 4. 交互规则
|
||||
|
||||
- RAG 开关关闭时不展示引用切片。
|
||||
- 未召回知识切片时给出明确提示。
|
||||
- 模型调用失败时展示 `request_id` 和错误摘要。
|
||||
- 会话保存失败不得清空用户输入。
|
||||
|
||||
20
docs/前端实现文档/6.MCP能力接入模块前端实现.md
Normal file
20
docs/前端实现文档/6.MCP能力接入模块前端实现.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# MCP 能力接入模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
`McpImportPage.vue` 展示导入方式、Manifest 预览和能力预览。
|
||||
|
||||
## 2. 接口草案
|
||||
|
||||
- `POST /api/mcp/import`
|
||||
- `GET /api/mcp/servers/{serverCode}/capabilities`
|
||||
- `POST /api/mcp/servers/query`
|
||||
- `POST /api/mcp/capabilities/save`
|
||||
|
||||
## 3. 交互规则
|
||||
|
||||
- 导入前校验地址或 Manifest 格式。
|
||||
- 能力预览成功后才允许保存。
|
||||
- 未授权能力显示待授权状态。
|
||||
- 已停用 Server 的能力不可被 Workflow 新增选择。
|
||||
|
||||
20
docs/前端实现文档/7.Skill编辑模块前端实现.md
Normal file
20
docs/前端实现文档/7.Skill编辑模块前端实现.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Skill 编辑模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
`SkillWorkspacePage.vue` 提供 Prompt、Code、Config 编辑区,测试面板和版本列表。
|
||||
|
||||
## 2. 接口草案
|
||||
|
||||
- `GET /api/skills/{skillCode}`
|
||||
- `PUT /api/skills/{skillCode}/draft`
|
||||
- `POST /api/skills/{skillCode}/test`
|
||||
- `POST /api/skills/{skillCode}/publish`
|
||||
|
||||
## 3. 交互规则
|
||||
|
||||
- 发布版本只读。
|
||||
- 草稿变更后提示未发布。
|
||||
- 测试结果保存到版本草稿。
|
||||
- Workflow 只能选择发布版本或明确选择草稿测试。
|
||||
|
||||
29
docs/前端实现文档/8.运行观测模块前端实现.md
Normal file
29
docs/前端实现文档/8.运行观测模块前端实现.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 运行观测模块前端实现
|
||||
|
||||
## 1. 页面
|
||||
|
||||
`ObservabilityPage.vue` 展示运行记录和步骤日志。
|
||||
|
||||
## 2. ViewModel
|
||||
|
||||
`ObservabilityView` 建议包含:
|
||||
|
||||
- 运行列表。
|
||||
- 当前运行详情。
|
||||
- 步骤日志。
|
||||
- 模型调用摘要。
|
||||
- 成本和延迟统计。
|
||||
|
||||
## 3. 接口草案
|
||||
|
||||
- `GET /api/observability/runs`
|
||||
- `GET /api/observability/runs/{requestId}`
|
||||
- `GET /api/observability/model-calls`
|
||||
- `GET /api/observability/runs/{requestId}/export`
|
||||
|
||||
## 4. 交互规则
|
||||
|
||||
- 异常运行高亮展示。
|
||||
- 步骤日志按执行时间排序。
|
||||
- 导出日志不包含密钥或完整敏感请求体。
|
||||
|
||||
38
docs/后端实现文档/0.后端模块总览.md
Normal file
38
docs/后端实现文档/0.后端模块总览.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 后端模块总览
|
||||
|
||||
## 1. 当前代码边界
|
||||
|
||||
当前后端已有 `common`、`rag`、`modelprovider`、`agent` 包。后续重新实现时,可以保留这些包作为技术分层落点,但业务文档按产品域拆分。
|
||||
|
||||
## 2. 后端实现原则
|
||||
|
||||
- Controller 不直接暴露实体。
|
||||
- 请求使用 `XxxRequest`,响应使用 `XxxResponse`。
|
||||
- 统一返回 `RequestResult<T>`。
|
||||
- 实体继承 `BaseEntity`,保持审计字段和乐观锁字段。
|
||||
- Mapper 继续使用 MyBatis-Plus `BaseMapper<T>`。
|
||||
- Service 负责业务校验、状态流转和跨表协调。
|
||||
|
||||
## 3. 模块落点建议
|
||||
|
||||
| 产品域 | 后端包建议 |
|
||||
|--------|------------|
|
||||
| 系统基础 | `com.bruce.common` |
|
||||
| 知识资产与文件解析 | `com.bruce.rag` |
|
||||
| 模型与路由 | `com.bruce.modelprovider` |
|
||||
| Workflow 编排 | `com.bruce.workflow` |
|
||||
| Agent 会话 | `com.bruce.agent` |
|
||||
| MCP 能力接入 | `com.bruce.mcp` |
|
||||
| Skill 编辑 | `com.bruce.skill` |
|
||||
| 运行观测 | `com.bruce.observability` 或复用运行来源模块 |
|
||||
|
||||
## 4. 枚举实现约束
|
||||
|
||||
新增结构化枚举时,继续实现 `PersistableSysEnumDefinition`,并同步:
|
||||
|
||||
- Java 枚举定义。
|
||||
- `sys_enum` 初始化测试。
|
||||
- `script/sql/18.studio_enum.sql` 或对应模块枚举脚本。
|
||||
- 前端枚举常量或字典接口。
|
||||
|
||||
|
||||
40
docs/后端实现文档/1.系统基础模块后端实现.md
Normal file
40
docs/后端实现文档/1.系统基础模块后端实现.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 系统基础模块后端实现
|
||||
|
||||
## 1. 包结构
|
||||
|
||||
当前实现位于 `com.bruce.common`:
|
||||
|
||||
- `controller`:枚举和附件 API。
|
||||
- `domain/entity`:`SysEnum`、`SysAttachment`。
|
||||
- `domain/model`:`BaseEntity`、`RequestResult`。
|
||||
- `document/parse`:文档解析抽象和 Tika 实现。
|
||||
- `enums`:通用枚举与可持久化枚举契约。
|
||||
- `service`:枚举与附件服务。
|
||||
|
||||
## 2. Controller 约定
|
||||
|
||||
Controller 只接收请求 DTO 或基础参数,不直接暴露实体。响应统一使用 `RequestResult<T>`。
|
||||
|
||||
## 3. Service 约定
|
||||
|
||||
`SysEnumService` 负责:
|
||||
|
||||
- 管理端查询。
|
||||
- 单条保存和删除。
|
||||
- 批量保存。
|
||||
- 按 `catalog + type` 全量替换初始化。
|
||||
|
||||
`SysAttachmentService` 负责:
|
||||
|
||||
- 校验上传文件。
|
||||
- 生成存储路径。
|
||||
- 保存本地文件。
|
||||
- 写入附件元数据。
|
||||
|
||||
## 4. 后续实现注意
|
||||
|
||||
- 不调整 `PersistableSysEnumDefinition` 现有方法。
|
||||
- 不调整 `sys_enum` 表字段。
|
||||
- 新增枚举时同步初始化测试和 SQL 初始化脚本。
|
||||
- 文档解析器新增类型时,只扩展解析器实现和工厂注册,不影响 RAG 业务服务。
|
||||
|
||||
40
docs/后端实现文档/2.知识资产与文件解析模块后端实现.md
Normal file
40
docs/后端实现文档/2.知识资产与文件解析模块后端实现.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 知识资产与文件解析模块后端实现
|
||||
|
||||
## 1. 当前实现落点
|
||||
|
||||
当前实现位于 `com.bruce.rag`,包含知识库、文档、解析、切片和向量实体。
|
||||
|
||||
## 2. Controller
|
||||
|
||||
已落地接口继续保留:
|
||||
|
||||
- `/api/rag/store/list`
|
||||
- `/api/rag/store/query`
|
||||
- `/api/rag/store/detail`
|
||||
- `/api/rag/store/overview`
|
||||
- `/api/rag/store/documentOverview`
|
||||
- `/api/rag/documents/query`
|
||||
- `/api/rag/documents/batchUpload`
|
||||
- `/api/rag/documents/parse`
|
||||
- `/api/rag/documents/chunk`
|
||||
|
||||
Studio 聚合接口后续新增在知识资产模块,不替代旧接口。
|
||||
|
||||
## 3. Service
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `IRagStoreService` | 知识库 CRUD、概览统计 |
|
||||
| `IRagDocumentService` | 文档 CRUD、批量上传 |
|
||||
| `IRagDocumentParseService` | 解析状态流转和解析快照 |
|
||||
| `IRagDocumentChunkService` | 根据解析快照生成切片 |
|
||||
| `IRagChunkEmbeddingService` | 向量写入和召回 |
|
||||
|
||||
## 4. 校验规则
|
||||
|
||||
- 知识库编码唯一。
|
||||
- 文档必须绑定存在的知识库和附件。
|
||||
- 只有解析成功的文档允许切片。
|
||||
- 向量化必须读取知识库生效模型配置。
|
||||
- 重新切片或模型变更后,索引状态必须回到待索引或索引中。
|
||||
|
||||
42
docs/后端实现文档/3.模型与路由模块后端实现.md
Normal file
42
docs/后端实现文档/3.模型与路由模块后端实现.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 模型与路由模块后端实现
|
||||
|
||||
## 1. 当前实现落点
|
||||
|
||||
当前实现位于 `com.bruce.modelprovider`,包含服务商、模型、路由、网关、客户端和调用日志。
|
||||
|
||||
## 2. Controller
|
||||
|
||||
- `ModelProviderController`
|
||||
- `ModelConfigController`
|
||||
- `ModelRouteRuleController`
|
||||
- `RagStoreModelConfigController`
|
||||
- `ModelCallLogController`
|
||||
|
||||
## 3. Service
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `IModelProviderService` | 服务商 CRUD 和健康检查 |
|
||||
| `IModelConfigService` | 模型配置 CRUD |
|
||||
| `IModelRouteRuleService` | 路由规则 CRUD |
|
||||
| `IModelRouteService` | 根据任务上下文选择模型 |
|
||||
| `IRagStoreModelConfigService` | 知识库模型绑定 |
|
||||
| `IModelCallLogService` | 调用日志写入与查询 |
|
||||
|
||||
## 4. 网关
|
||||
|
||||
业务模块只能依赖:
|
||||
|
||||
- `ChatModelGateway`
|
||||
- `EmbeddingModelGateway`
|
||||
|
||||
不得直接依赖具体上游客户端。
|
||||
|
||||
## 5. 校验规则
|
||||
|
||||
- 服务商编码唯一。
|
||||
- 同一服务商下模型编码唯一。
|
||||
- 路由主模型必须启用。
|
||||
- 路由任务类型必须来自枚举。
|
||||
- 知识库 Embedding 维度必须与模型配置一致。
|
||||
|
||||
39
docs/后端实现文档/4.Workflow编排模块后端实现.md
Normal file
39
docs/后端实现文档/4.Workflow编排模块后端实现.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Workflow 编排模块后端实现
|
||||
|
||||
## 1. 包建议
|
||||
|
||||
后续实现建议新增 `com.bruce.workflow`:
|
||||
|
||||
- `controller`
|
||||
- `dto/request`
|
||||
- `dto/response`
|
||||
- `entity`
|
||||
- `enums`
|
||||
- `mapper`
|
||||
- `service`
|
||||
- `runner`
|
||||
|
||||
## 2. Controller 草案
|
||||
|
||||
- `WorkflowDefinitionController`
|
||||
- `WorkflowVersionController`
|
||||
- `WorkflowRunController`
|
||||
|
||||
## 3. Service 草案
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `IWorkflowDefinitionService` | 定义 CRUD、绑定 Agent |
|
||||
| `IWorkflowVersionService` | 草稿保存、发布快照 |
|
||||
| `IWorkflowRunService` | 运行测试、运行记录查询 |
|
||||
| `IWorkflowRunner` | 按图执行节点并写入步骤日志 |
|
||||
|
||||
## 4. 校验规则
|
||||
|
||||
- Workflow 编码唯一。
|
||||
- 发布版本号不可重复。
|
||||
- 运行时必须使用确定的版本快照。
|
||||
- 节点类型必须来自枚举。
|
||||
- LLM 节点必须配置任务类型。
|
||||
- Knowledge Retrieval 节点必须配置知识库或继承项目默认知识库。
|
||||
|
||||
30
docs/后端实现文档/5.Agent会话模块后端实现.md
Normal file
30
docs/后端实现文档/5.Agent会话模块后端实现.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Agent 会话模块后端实现
|
||||
|
||||
## 1. 当前实现落点
|
||||
|
||||
当前 Agent 定义和调试入口位于 `com.bruce.agent`。后续会话能力建议继续放在该包下。
|
||||
|
||||
## 2. Controller 草案
|
||||
|
||||
- `AgentDefinitionController`:保留定义管理和兼容调试入口。
|
||||
- `AgentSessionController`:新增会话查询、详情和消息发送。
|
||||
- `AgentRunController`:新增运行入口,关联 Workflow 或模型调用。
|
||||
|
||||
## 3. Service 草案
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `IAgentDefinitionService` | Agent 定义管理 |
|
||||
| `IAgentSessionService` | 会话创建、关闭、查询 |
|
||||
| `IAgentMessageService` | 消息写入和引用保存 |
|
||||
| `IAgentRunService` | 对话编排、RAG 召回、模型调用 |
|
||||
| `IAgentCapabilityBindingService` | 能力绑定维护 |
|
||||
|
||||
## 4. 校验规则
|
||||
|
||||
- Agent 必须启用才允许发起对话。
|
||||
- RAG 对话必须绑定可用知识库和 Embedding 配置。
|
||||
- 消息内容不能为空。
|
||||
- `citation_json` 必须是数组结构。
|
||||
- 会话关闭后不允许继续写入消息。
|
||||
|
||||
21
docs/后端实现文档/6.MCP能力接入模块后端实现.md
Normal file
21
docs/后端实现文档/6.MCP能力接入模块后端实现.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# MCP 能力接入模块后端实现
|
||||
|
||||
## 1. 包建议
|
||||
|
||||
后续实现建议新增 `com.bruce.mcp`,包含 Controller、DTO、Entity、Mapper、Service 和 MCP Client 适配。
|
||||
|
||||
## 2. Service 草案
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `IMcpServerService` | Server 导入、启停用、健康检查 |
|
||||
| `IMcpCapabilityService` | 能力发现、保存、启停用 |
|
||||
| `IMcpImportService` | 处理 URL、package、Manifest 导入 |
|
||||
|
||||
## 3. 校验规则
|
||||
|
||||
- Server 编码唯一。
|
||||
- 能力编码在 Server 下唯一。
|
||||
- 导入方式必须来自枚举。
|
||||
- 能力 schema 必须为合法 JSON。
|
||||
|
||||
21
docs/后端实现文档/7.Skill编辑模块后端实现.md
Normal file
21
docs/后端实现文档/7.Skill编辑模块后端实现.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Skill 编辑模块后端实现
|
||||
|
||||
## 1. 包建议
|
||||
|
||||
后续实现建议新增 `com.bruce.skill`。
|
||||
|
||||
## 2. Service 草案
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `ISkillDefinitionService` | Skill 定义管理 |
|
||||
| `ISkillVersionService` | 草稿保存、测试、发布、归档 |
|
||||
| `ISkillRunner` | 运行 Skill 并生成测试结果 |
|
||||
|
||||
## 3. 校验规则
|
||||
|
||||
- Skill 编码唯一。
|
||||
- 版本号在 Skill 内唯一。
|
||||
- Prompt、Code、Config 至少一种非空。
|
||||
- 变量 schema 必须为合法 JSON。
|
||||
|
||||
25
docs/后端实现文档/8.运行观测模块后端实现.md
Normal file
25
docs/后端实现文档/8.运行观测模块后端实现.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 运行观测模块后端实现
|
||||
|
||||
## 1. 包建议
|
||||
|
||||
后续实现可新增 `com.bruce.observability`,也可以先由 Workflow、Agent 和模型模块分别提供查询接口。
|
||||
|
||||
## 2. Controller 草案
|
||||
|
||||
- `ObservabilityRunController`
|
||||
- `ObservabilityModelCallController`
|
||||
|
||||
## 3. Service 草案
|
||||
|
||||
| Service | 职责 |
|
||||
|---------|------|
|
||||
| `IObservabilityRunService` | 聚合运行记录 |
|
||||
| `IObservabilityTraceService` | 聚合步骤、会话和模型调用 |
|
||||
| `IObservabilityExportService` | 导出脱敏日志 |
|
||||
|
||||
## 4. 校验规则
|
||||
|
||||
- 只允许查询脱敏后的运行摘要。
|
||||
- 导出接口必须过滤密钥和完整请求内容。
|
||||
- requestId 不存在时返回空结果而不是内部异常。
|
||||
|
||||
57
docs/数据库设计/0.数据库与枚举总览.md
Normal file
57
docs/数据库设计/0.数据库与枚举总览.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 数据库与枚举总览
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文集中说明本轮数据库和枚举脚本的更新范围,避免需求文档、设计文档、前端文档、后端文档与 SQL 脚本出现冲突。
|
||||
|
||||
## 2. SQL 脚本分层
|
||||
|
||||
| 脚本位置 | 作用 |
|
||||
|----------|------|
|
||||
| `script/` | 可执行的模块化建表或初始化脚本 |
|
||||
| `docs/MODEL_PROVIDER_SCHEMA.sql` | 模型平台 schema 汇总 |
|
||||
| `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | Studio 原型 schema 汇总 |
|
||||
|
||||
模块化脚本是后续落库执行的优先参考,`docs/*SCHEMA.sql` 是完整设计快照。
|
||||
|
||||
## 3. 必须保持不变的内容
|
||||
|
||||
`script/sql/1.enum.sql` 中 `sys_enum` 表结构不变:
|
||||
|
||||
- `catalog`
|
||||
- `type`
|
||||
- `name`
|
||||
- `value`
|
||||
- `strvalue`
|
||||
- `sort`
|
||||
- `version`
|
||||
- `create_time`
|
||||
- `update_time`
|
||||
- `remark`
|
||||
- `create_by`
|
||||
- `update_by`
|
||||
|
||||
Java 枚举契约 `PersistableSysEnumDefinition` 的格式不变。后续如果实现新枚举类,仍按现有 `getCatalog()`、`getType()`、`getValue()`、`getLabel()`、`getRemark()` 风格实现。
|
||||
|
||||
## 4. 本轮新增脚本
|
||||
|
||||
| 脚本 | 内容 |
|
||||
|------|------|
|
||||
| `script/sql/11.studio_project.sql` | Studio 项目空间表 |
|
||||
| `script/sql/12.workflow.sql` | Workflow 定义、版本、运行、步骤日志 |
|
||||
| `script/sql/14.mcp.sql` | MCP Server 与能力表 |
|
||||
| `script/sql/15.skill.sql` | Skill 定义与版本表 |
|
||||
| `script/sql/13.agent_session.sql` | Agent 会话与消息表 |
|
||||
| `script/sql/16.agent_capability_binding.sql` | Agent/Workflow 与 MCP/Skill/知识能力绑定表 |
|
||||
| `script/sql/18.studio_enum.sql` | Studio 相关枚举初始化 |
|
||||
|
||||
## 5. 校验规则
|
||||
|
||||
- 所有新增表必须包含 `id`、`version`、`create_time`、`update_time`、`remark`、`create_by`、`update_by`。
|
||||
- 需要唯一业务编码的表必须增加唯一约束。
|
||||
- 需要跨模块引用的字段必须在文档中说明外键关系。
|
||||
- JSON 扩展字段统一使用 `JSONB`。
|
||||
- 枚举初始化脚本必须使用 `ON CONFLICT (catalog, type, name) DO UPDATE`,与现有脚本风格一致。
|
||||
|
||||
|
||||
|
||||
40
docs/数据库设计/2.知识资产数据库设计.md
Normal file
40
docs/数据库设计/2.知识资产数据库设计.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 知识资产数据库设计
|
||||
|
||||
## 1. 表关系
|
||||
|
||||
| 表 | 说明 | 关键关系 |
|
||||
|----|------|----------|
|
||||
| `rag_store` | 知识库主表 | 独立业务编码 |
|
||||
| `rag_document` | 知识文档 | 引用 `rag_store`、`sys_attachment` |
|
||||
| `rag_document_parse_result` | 解析快照 | 引用 `rag_store`、`rag_document` |
|
||||
| `rag_chunk` | 知识切片 | 引用 `rag_store`、`rag_document` |
|
||||
| `rag_chunk_embedding` | 切片向量 | 引用 `rag_store`、`rag_document`、`rag_chunk` |
|
||||
| `rag_store_model_config` | 知识库模型配置 | 引用 `rag_store`、`model_config` |
|
||||
|
||||
## 2. 脚本清单
|
||||
|
||||
- `script/sql/3.rag_store.sql`
|
||||
- `script/sql/4.rag_document.sql`
|
||||
- `script/sql/5.rag_document_parse_result.sql`
|
||||
- `script/sql/6.rag_chunk.sql`
|
||||
- `script/sql/7.rag_chunk_embedding.sql`
|
||||
- `docs/MODEL_PROVIDER_SCHEMA.sql` 中的 `rag_store_model_config`
|
||||
|
||||
## 3. 枚举组
|
||||
|
||||
| catalog | type | Java 枚举 |
|
||||
|---------|------|-----------|
|
||||
| `rag` | `parse_status` | `RagParseStatusEnum` |
|
||||
| `rag` | `index_status` | `RagIndexStatusEnum` |
|
||||
| `rag` | `chunk_strategy` | `RagChunkStrategyEnum` |
|
||||
|
||||
## 4. 一致性要求
|
||||
|
||||
- `rag_document.attachment_id` 保持唯一,避免一个附件重复建档。
|
||||
- `rag_document_parse_result.document_id` 保持唯一,表示一个文档一个当前解析快照。
|
||||
- `rag_chunk` 使用 `(document_id, chunk_index)` 保证同文档切片序号唯一。
|
||||
- `rag_chunk_embedding` 使用 `(chunk_id, embedding_model)` 防止同模型重复写入。
|
||||
- RAG 现有脚本与本文档一致,本轮不调整 `sys_enum` 格式。
|
||||
|
||||
|
||||
|
||||
40
docs/数据库设计/3.模型与路由数据库设计.md
Normal file
40
docs/数据库设计/3.模型与路由数据库设计.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 模型与路由数据库设计
|
||||
|
||||
## 1. 表关系
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `model_provider` | 模型服务商配置 |
|
||||
| `model_config` | 服务商下具体模型 |
|
||||
| `model_route_rule` | 任务路由规则 |
|
||||
| `rag_store_model_config` | 知识库 Embedding 与切片配置 |
|
||||
| `model_call_log` | 模型调用日志 |
|
||||
|
||||
## 2. 关键约束
|
||||
|
||||
- `model_provider.provider_code` 全局唯一。
|
||||
- `model_config` 使用 `(provider_id, model_code)` 唯一。
|
||||
- `model_route_rule.route_code` 全局唯一。
|
||||
- `rag_store_model_config` 使用 `(store_id, active)` 约束一个知识库只有一个生效配置。
|
||||
- `model_call_log.request_id` 全局唯一。
|
||||
|
||||
## 3. 枚举组
|
||||
|
||||
| catalog | type |
|
||||
|---------|------|
|
||||
| `model_provider` | `provider_type` |
|
||||
| `model_provider` | `protocol_type` |
|
||||
| `model_provider` | `model_type` |
|
||||
| `model_provider` | `task_type` |
|
||||
| `model_provider` | `route_strategy` |
|
||||
| `model_provider` | `call_status` |
|
||||
| `model_provider` | `health_status` |
|
||||
|
||||
## 4. 脚本
|
||||
|
||||
- `docs/MODEL_PROVIDER_SCHEMA.sql` 是完整 schema 快照。
|
||||
- `script/sql/9.model_provider.sql` 是后续执行入口。
|
||||
- 若模型枚举变更,需要同步 Java 枚举、初始化测试和 SQL 枚举脚本。
|
||||
|
||||
|
||||
|
||||
35
docs/数据库设计/4.Workflow数据库设计.md
Normal file
35
docs/数据库设计/4.Workflow数据库设计.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Workflow 数据库设计
|
||||
|
||||
## 1. 表结构
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `studio_project` | Studio 项目空间 |
|
||||
| `workflow_definition` | Workflow 主定义 |
|
||||
| `workflow_version` | Workflow 版本快照 |
|
||||
| `workflow_run` | Workflow 运行记录 |
|
||||
| `workflow_run_step` | Workflow 节点步骤日志 |
|
||||
|
||||
## 2. 关键约束
|
||||
|
||||
- `studio_project.project_code` 唯一。
|
||||
- `workflow_definition.workflow_code` 唯一。
|
||||
- `workflow_version` 使用 `(workflow_id, version_no)` 唯一。
|
||||
- `workflow_run.request_id` 唯一。
|
||||
- `workflow_definition.bound_agent_id` 可引用 `agent_definition`。
|
||||
|
||||
## 3. 脚本
|
||||
|
||||
- `script/sql/11.studio_project.sql`
|
||||
- `script/sql/12.workflow.sql`
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`
|
||||
|
||||
## 4. JSON 字段
|
||||
|
||||
- `workflow_version.graph_json` 保存流程图快照。
|
||||
- `workflow_run.input_json` 与 `output_json` 保存运行输入输出摘要。
|
||||
- `workflow_run_step.input_json` 与 `output_json` 保存节点级输入输出摘要。
|
||||
|
||||
JSON 字段只保存运行必要摘要,不保存敏感密钥。
|
||||
|
||||
|
||||
35
docs/数据库设计/5.Agent会话数据库设计.md
Normal file
35
docs/数据库设计/5.Agent会话数据库设计.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Agent 会话数据库设计
|
||||
|
||||
## 1. 表结构
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `agent_definition` | Agent 定义 |
|
||||
| `agent_session` | Agent 会话 |
|
||||
| `agent_message` | Agent 消息 |
|
||||
| `agent_capability_binding` | 能力绑定 |
|
||||
|
||||
## 2. 关键约束
|
||||
|
||||
- `agent_definition.agent_code` 唯一。
|
||||
- `agent_session.session_code` 唯一。
|
||||
- `agent_session.agent_id` 引用 `agent_definition`。
|
||||
- `agent_session.workflow_run_id` 可引用 `workflow_run`。
|
||||
- `agent_message.session_id` 引用 `agent_session`。
|
||||
- `agent_capability_binding` 使用 `(owner_type, owner_id, capability_type, capability_id)` 唯一。
|
||||
|
||||
## 3. JSON 字段
|
||||
|
||||
- `agent_session.metadata_json` 保存会话扩展信息。
|
||||
- `agent_message.citation_json` 保存引用切片摘要。
|
||||
- `agent_capability_binding.config_json` 保存能力绑定配置。
|
||||
|
||||
## 4. 脚本
|
||||
|
||||
- `script/sql/8.agent_definition.sql`
|
||||
- `script/sql/13.agent_session.sql`
|
||||
- `script/sql/16.agent_capability_binding.sql`
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`
|
||||
|
||||
|
||||
|
||||
22
docs/数据库设计/6.MCP数据库设计.md
Normal file
22
docs/数据库设计/6.MCP数据库设计.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# MCP 数据库设计
|
||||
|
||||
## 1. 表结构
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `mcp_server` | MCP 服务配置 |
|
||||
| `mcp_capability` | MCP 能力清单 |
|
||||
|
||||
## 2. 约束
|
||||
|
||||
- `mcp_server.server_code` 唯一。
|
||||
- `mcp_capability` 使用 `(server_id, capability_code)` 唯一。
|
||||
- `mcp_capability.server_id` 引用 `mcp_server`。
|
||||
|
||||
## 3. 脚本
|
||||
|
||||
- `script/sql/14.mcp.sql`
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`
|
||||
- `script/sql/18.studio_enum.sql`
|
||||
|
||||
|
||||
22
docs/数据库设计/7.Skill数据库设计.md
Normal file
22
docs/数据库设计/7.Skill数据库设计.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Skill 数据库设计
|
||||
|
||||
## 1. 表结构
|
||||
|
||||
| 表 | 说明 |
|
||||
|----|------|
|
||||
| `skill_definition` | Skill 定义 |
|
||||
| `skill_version` | Skill 版本 |
|
||||
|
||||
## 2. 约束
|
||||
|
||||
- `skill_definition.skill_code` 唯一。
|
||||
- `skill_version` 使用 `(skill_id, version_no)` 唯一。
|
||||
- `skill_version.skill_id` 引用 `skill_definition`。
|
||||
|
||||
## 3. 脚本
|
||||
|
||||
- `script/sql/15.skill.sql`
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`
|
||||
- `script/sql/18.studio_enum.sql`
|
||||
|
||||
|
||||
32
docs/数据库设计/8.运行观测数据库设计.md
Normal file
32
docs/数据库设计/8.运行观测数据库设计.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 运行观测数据库设计
|
||||
|
||||
## 1. 表范围
|
||||
|
||||
运行观测复用已有运行表,不单独新增观测主表:
|
||||
|
||||
- `workflow_run`
|
||||
- `workflow_run_step`
|
||||
- `model_call_log`
|
||||
- `agent_session`
|
||||
- `agent_message`
|
||||
|
||||
## 2. 索引建议
|
||||
|
||||
- `workflow_run.request_id`
|
||||
- `workflow_run.status`
|
||||
- `workflow_run_step.run_id`
|
||||
- `model_call_log.request_id`
|
||||
- `model_call_log.task_type`
|
||||
- `agent_session.agent_id`
|
||||
|
||||
## 3. 脚本
|
||||
|
||||
索引分散在:
|
||||
|
||||
- `script/sql/12.workflow.sql`
|
||||
- `script/sql/9.model_provider.sql`
|
||||
- `script/sql/13.agent_session.sql`
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`
|
||||
|
||||
|
||||
|
||||
82
docs/数据库设计/9.模块一致性校验.md
Normal file
82
docs/数据库设计/9.模块一致性校验.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 模块一致性校验
|
||||
|
||||
## 1. 校验范围
|
||||
|
||||
本次校验覆盖:
|
||||
|
||||
- `需求分析/`
|
||||
- `设计文档/`
|
||||
- `数据库设计/`
|
||||
- `前端实现文档/`
|
||||
- `后端实现文档/`
|
||||
- `script/*.sql`
|
||||
- `docs/MODEL_PROVIDER_SCHEMA.sql`
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`
|
||||
|
||||
## 2. 文档占位检查
|
||||
|
||||
已检查常见未决占位关键词,当前新增文档与 SQL 脚本中未保留未决占位。
|
||||
|
||||
## 3. 数据库脚本一致性
|
||||
|
||||
| 模块 | 汇总 schema | 模块化脚本 | 结论 |
|
||||
|------|-------------|------------|------|
|
||||
| 模型与路由 | `docs/MODEL_PROVIDER_SCHEMA.sql` | `script/sql/9.model_provider.sql` | 已补齐 `rag_store_model_config.store_id` 外键 |
|
||||
| Studio 项目 | `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | `script/sql/11.studio_project.sql` | 表、唯一约束、索引一致 |
|
||||
| Workflow | `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | `script/sql/12.workflow.sql` | 定义、版本、运行、步骤表一致 |
|
||||
| Agent 会话 | `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | `script/sql/13.agent_session.sql` | 会话和消息表一致 |
|
||||
| 能力绑定 | `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | `script/sql/16.agent_capability_binding.sql` | 唯一约束和索引一致 |
|
||||
| MCP | `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | `script/sql/14.mcp.sql` | Server 与 Capability 表一致 |
|
||||
| Skill | `docs/STUDIO_PROTOTYPE_SCHEMA.sql` | `script/sql/15.skill.sql` | 定义与版本表一致 |
|
||||
|
||||
## 4. 枚举一致性
|
||||
|
||||
`script/sql/18.studio_enum.sql` 保持 `sys_enum` 既有格式:
|
||||
|
||||
- `catalog`
|
||||
- `type`
|
||||
- `name`
|
||||
- `value`
|
||||
- `strvalue`
|
||||
- `sort`
|
||||
- `version`
|
||||
- `remark`
|
||||
|
||||
校验结果:
|
||||
|
||||
- 枚举行数:48。
|
||||
- 枚举组数:14。
|
||||
- 同一 `catalog + type` 下 `value` 无重复。
|
||||
- 同一 `catalog + type` 下 `sort` 无重复。
|
||||
|
||||
## 5. 跨模块依赖检查
|
||||
|
||||
| 依赖 | 结论 |
|
||||
|------|------|
|
||||
| 知识资产依赖系统附件 | `rag_document.attachment_id` 引用 `sys_attachment` 的设计保持不变 |
|
||||
| 知识资产依赖模型配置 | `rag_store_model_config` 连接 `rag_store` 与 `model_config` |
|
||||
| Workflow 依赖 Agent | `workflow_definition.bound_agent_id` 引用 `agent_definition` |
|
||||
| Workflow 运行依赖 Agent | `workflow_run.agent_id` 引用 `agent_definition` |
|
||||
| Agent 会话依赖 Workflow 运行 | `agent_session.workflow_run_id` 引用 `workflow_run` |
|
||||
| MCP/Skill 依赖能力绑定 | `agent_capability_binding` 通过类型和 ID 绑定能力 |
|
||||
| 运行观测依赖运行日志 | 复用 `workflow_run`、`workflow_run_step`、`model_call_log`、`agent_session` |
|
||||
|
||||
## 6. 保留约束
|
||||
|
||||
- 不修改 `sys_enum` 表结构。
|
||||
- 不修改 `PersistableSysEnumDefinition` 枚举契约。
|
||||
- 不把结构化枚举协议从整型值改为字符串值。
|
||||
- 本轮不修改 Java、Vue 或测试代码。
|
||||
|
||||
## 7. 后续实现提示
|
||||
|
||||
后续按文档重写代码时,新增 Java 枚举必须补齐:
|
||||
|
||||
- Java 枚举类。
|
||||
- `SysEnumDataInitTests` 初始化入口。
|
||||
- `EnumDefinitionTests` 稳定性断言。
|
||||
- `script/sql/18.studio_enum.sql` 初始化数据。
|
||||
- 前端字典或常量映射。
|
||||
|
||||
|
||||
|
||||
51
docs/设计文档/0.模块设计总览.md
Normal file
51
docs/设计文档/0.模块设计总览.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Common Agent Studio 模块设计总览
|
||||
|
||||
## 1. 总体设计
|
||||
|
||||
Common Agent Studio 以一次 AI 应用发布旅程为主线:知识接入、模型路由、Workflow 编排、Agent 调试、MCP/Skill 扩展和运行观测围绕同一个项目空间协作。
|
||||
|
||||
旧代码中的 `common`、`rag`、`modelprovider`、`agent` 仍作为实现包存在,但新文档按产品域组织,便于后续重新实现时形成更清晰的模块边界。
|
||||
|
||||
## 2. 产品域关系
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Common["系统基础"] --> Knowledge["知识资产与文件解析"]
|
||||
Common --> Model["模型与路由"]
|
||||
Knowledge --> Workflow["Workflow 编排"]
|
||||
Model --> Workflow
|
||||
Workflow --> Agent["Agent 会话"]
|
||||
MCP["MCP 能力接入"] --> Workflow
|
||||
Skill["Skill 编辑"] --> Workflow
|
||||
Agent --> Observability["运行观测"]
|
||||
Workflow --> Observability
|
||||
Model --> Observability
|
||||
```
|
||||
|
||||
## 3. 数据主线
|
||||
|
||||
| 主线 | 表 |
|
||||
|------|------|
|
||||
| 系统基础 | `sys_enum`、`sys_attachment` |
|
||||
| 知识资产 | `rag_store`、`rag_document`、`rag_document_parse_result`、`rag_chunk`、`rag_chunk_embedding` |
|
||||
| 模型路由 | `model_provider`、`model_config`、`model_route_rule`、`rag_store_model_config`、`model_call_log` |
|
||||
| Studio 项目 | `studio_project` |
|
||||
| Workflow | `workflow_definition`、`workflow_version`、`workflow_run`、`workflow_run_step` |
|
||||
| Agent | `agent_definition`、`agent_session`、`agent_message`、`agent_capability_binding` |
|
||||
| MCP | `mcp_server`、`mcp_capability` |
|
||||
| Skill | `skill_definition`、`skill_version` |
|
||||
|
||||
## 4. 接口命名原则
|
||||
|
||||
- 已落地接口保持兼容,例如 `/api/rag/documents/query`、`/api/agents/{agentId}/chat`。
|
||||
- Studio 新接口使用聚合资源命名,例如 `/api/knowledge/workspaces/{storeId}`、`/api/workflows/{workflowId}/runs`。
|
||||
- 请求和响应继续使用 DTO,不直接暴露实体。
|
||||
- 统一返回体保持 `RequestResult<T>`。
|
||||
|
||||
## 5. 状态设计原则
|
||||
|
||||
- 长期稳定状态使用结构化枚举。
|
||||
- 枚举设计必须保留 `sys_enum` 当前格式。
|
||||
- 数据库中状态字段允许继续使用 `VARCHAR` 保存枚举代码,前后端协议层对结构化枚举继续传整型值。
|
||||
- 页面展示文案来自枚举定义或前端常量映射,不在业务逻辑中散落硬编码。
|
||||
|
||||
33
docs/设计文档/1.系统基础模块设计.md
Normal file
33
docs/设计文档/1.系统基础模块设计.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 系统基础模块设计
|
||||
|
||||
## 1. 模块边界
|
||||
|
||||
系统基础模块不承载具体 AI 业务逻辑,只提供跨模块复用能力。知识资产、模型路由、Workflow、Agent、MCP、Skill 和观测模块都可以依赖它。
|
||||
|
||||
## 2. 核心对象
|
||||
|
||||
| 对象 | 说明 |
|
||||
|------|------|
|
||||
| `SysEnum` | 系统枚举配置,面向前端字典和初始化脚本 |
|
||||
| `SysAttachment` | 附件元数据,保存文件名称、路径、来源和大小 |
|
||||
| `BaseEntity` | 主键、审计字段、乐观锁字段 |
|
||||
| `RequestResult<T>` | 统一 API 响应信封 |
|
||||
| `DocumentParser` | 文档文本抽取接口 |
|
||||
| `DocumentParserFactory` | 根据文件类型选择解析器 |
|
||||
|
||||
## 3. 枚举设计原则
|
||||
|
||||
结构化枚举继续以 Java 枚举为单一事实来源。Java 枚举实现 `PersistableSysEnumDefinition`,暴露 `catalog`、`type`、`name`、`value`、`strvalue`、`sort` 和 `remark`。
|
||||
|
||||
`sys_enum` 表结构不变,新增 Studio 枚举只能新增枚举组或枚举行,不能调整原字段含义。
|
||||
|
||||
## 4. 状态与错误
|
||||
|
||||
- 业务启停用统一使用 `EnableStatusEnum` 或模块自有状态。
|
||||
- 长流程处理状态使用模块自有枚举,但必须同步到 `sys_enum`。
|
||||
- 全局异常处理将校验错误转换为统一响应。
|
||||
|
||||
## 5. 依赖关系
|
||||
|
||||
系统基础模块不能依赖其他业务模块。业务模块可依赖系统基础模块的枚举、附件、解析、返回体和异常处理。
|
||||
|
||||
45
docs/设计文档/2.知识资产与文件解析模块设计.md
Normal file
45
docs/设计文档/2.知识资产与文件解析模块设计.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 知识资产与文件解析模块设计
|
||||
|
||||
## 1. 领域模型
|
||||
|
||||
| 对象 | 职责 |
|
||||
|------|------|
|
||||
| 知识库 | 聚合文档、模型配置、检索配置和索引版本 |
|
||||
| 知识文档 | 关联附件,维护解析和索引状态 |
|
||||
| 解析快照 | 保存文本抽取结果,作为切片输入 |
|
||||
| 知识切片 | 保存切片内容、序号、元数据和启用状态 |
|
||||
| 切片向量 | 保存 Embedding 向量、模型名和维度 |
|
||||
| 知识库模型配置 | 固定知识库 Embedding 模型和切片配置 |
|
||||
|
||||
## 2. 状态流转
|
||||
|
||||
文档解析状态:
|
||||
|
||||
`UPLOADED -> PARSING -> PARSED`,失败时进入 `FAILED`。
|
||||
|
||||
文档索引状态:
|
||||
|
||||
`PENDING -> INDEXING -> INDEXED`,失败时进入 `FAILED`。
|
||||
|
||||
切片策略继续使用整型枚举值,例如 `1` 表示固定长度,`5` 表示按分隔符。
|
||||
|
||||
## 3. 数据流
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Upload["上传文件"] --> Attachment["sys_attachment"]
|
||||
Attachment --> Document["rag_document"]
|
||||
Document --> Parse["rag_document_parse_result"]
|
||||
Parse --> Chunk["rag_chunk"]
|
||||
Chunk --> Embedding["rag_chunk_embedding"]
|
||||
Embedding --> Retrieval["检索召回"]
|
||||
```
|
||||
|
||||
## 4. 设计约束
|
||||
|
||||
- `rag_document` 只引用附件和知识库,不存储大段解析文本。
|
||||
- 解析快照按文档唯一,重新解析时更新快照。
|
||||
- `rag_chunk_embedding` 必须记录模型和维度,防止向量空间混用。
|
||||
- 知识库模型配置由模型与路由模块维护,但知识资产模块负责消费。
|
||||
- 检索配置要面向 Workflow 和 Agent 复用,不绑定某一个页面。
|
||||
|
||||
48
docs/设计文档/3.模型与路由模块设计.md
Normal file
48
docs/设计文档/3.模型与路由模块设计.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 模型与路由模块设计
|
||||
|
||||
## 1. 分层设计
|
||||
|
||||
| 层 | 职责 |
|
||||
|----|------|
|
||||
| 配置层 | 管理服务商、模型、路由规则和知识库模型绑定 |
|
||||
| 路由层 | 根据任务类型、范围和策略选择模型 |
|
||||
| 调用层 | 通过 OpenAI-compatible 客户端调用 Chat/Embedding |
|
||||
| 观测层 | 写入模型调用日志,供运行观测查询 |
|
||||
|
||||
## 2. 路由输入
|
||||
|
||||
模型路由至少需要:
|
||||
|
||||
- `taskType`
|
||||
- `matchScope`
|
||||
- `scopeId`
|
||||
- `bizType`
|
||||
- `bizId`
|
||||
- 调用参数。
|
||||
|
||||
`matchScope` 初始支持 `GLOBAL`、`RAG_STORE`、`AGENT`。
|
||||
|
||||
## 3. 路由输出
|
||||
|
||||
路由结果包含:
|
||||
|
||||
- 服务商。
|
||||
- 模型配置。
|
||||
- 上游模型名。
|
||||
- 超时时间。
|
||||
- Fallback 列表。
|
||||
- 调用选项。
|
||||
|
||||
## 4. 失败策略
|
||||
|
||||
- 主模型失败时按 Fallback 顺序重试。
|
||||
- 全部失败时返回统一错误。
|
||||
- 每次调用均写入 `model_call_log`。
|
||||
- Fallback 成功时状态记为 `FALLBACK_SUCCESS`。
|
||||
|
||||
## 5. 与其他模块关系
|
||||
|
||||
- 知识资产模块使用 Embedding 路由和知识库模型绑定。
|
||||
- Workflow 和 Agent 使用 Chat 路由。
|
||||
- 运行观测读取 `model_call_log`。
|
||||
|
||||
47
docs/设计文档/4.Workflow编排模块设计.md
Normal file
47
docs/设计文档/4.Workflow编排模块设计.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Workflow 编排模块设计
|
||||
|
||||
## 1. 核心模型
|
||||
|
||||
| 对象 | 说明 |
|
||||
|------|------|
|
||||
| Studio 项目 | 聚合多个 Workflow、Agent 和发布配置 |
|
||||
| Workflow 定义 | 可编辑流程主数据 |
|
||||
| Workflow 版本 | 发布或草稿快照,保存 `graph_json` |
|
||||
| Workflow 运行 | 一次测试或正式运行 |
|
||||
| Workflow 步骤 | 单个节点的输入、输出、耗时和错误 |
|
||||
|
||||
## 2. 图模型
|
||||
|
||||
`workflow_version.graph_json` 保存节点与边:
|
||||
|
||||
- 节点:`id`、`type`、`label`、`config`、`position`
|
||||
- 边:`from`、`to`、`condition`
|
||||
|
||||
图模型由后端做最小结构校验,复杂交互由前端画布负责。
|
||||
|
||||
## 3. 运行链路
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Start["Start"] --> Retrieve["Knowledge Retrieval"]
|
||||
Retrieve --> LLM["LLM"]
|
||||
LLM --> Tool["MCP Tool"]
|
||||
Tool --> Skill["Skill"]
|
||||
Skill --> Answer["Answer"]
|
||||
```
|
||||
|
||||
## 4. 状态设计
|
||||
|
||||
- Workflow 定义状态:草稿、启用、停用、归档。
|
||||
- 发布状态:草稿、已发布、已归档。
|
||||
- 运行状态:排队中、运行中、成功、失败、取消。
|
||||
- 节点状态:等待、运行中、成功、失败、跳过。
|
||||
|
||||
## 5. 依赖关系
|
||||
|
||||
- Knowledge Retrieval 节点依赖知识资产模块。
|
||||
- LLM 节点依赖模型与路由模块。
|
||||
- MCP Tool 节点依赖 MCP 模块。
|
||||
- Skill 节点依赖 Skill 模块。
|
||||
- 运行记录被运行观测模块消费。
|
||||
|
||||
51
docs/设计文档/5.Agent会话模块设计.md
Normal file
51
docs/设计文档/5.Agent会话模块设计.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Agent 会话模块设计
|
||||
|
||||
## 1. 核心模型
|
||||
|
||||
| 对象 | 说明 |
|
||||
|------|------|
|
||||
| Agent 定义 | Agent 配置主数据 |
|
||||
| Agent 会话 | 一段连续对话上下文 |
|
||||
| Agent 消息 | 会话中的单条消息 |
|
||||
| 能力绑定 | Agent 或 Workflow 绑定知识库、MCP、Skill 等能力 |
|
||||
|
||||
## 2. 对话模式
|
||||
|
||||
- 普通对话:直接根据会话消息调用 Chat 模型。
|
||||
- RAG 对话:先向量化问题,再召回知识切片,再组装上下文调用 Chat 模型。
|
||||
- Workflow 对话:通过绑定的 Workflow 运行,消息与运行记录互相关联。
|
||||
|
||||
## 3. 引用设计
|
||||
|
||||
`agent_message.citation_json` 保存回答引用:
|
||||
|
||||
- 文档 ID。
|
||||
- 切片 ID。
|
||||
- 分数。
|
||||
- 引用文本摘要。
|
||||
- 知识库 ID。
|
||||
|
||||
引用只保存必要摘要,完整切片仍以 `rag_chunk` 为准。
|
||||
|
||||
## 4. 状态设计
|
||||
|
||||
会话状态建议包括:
|
||||
|
||||
- ACTIVE:活跃。
|
||||
- CLOSED:已关闭。
|
||||
- FAILED:异常终止。
|
||||
|
||||
消息角色建议包括:
|
||||
|
||||
- USER。
|
||||
- ASSISTANT。
|
||||
- SYSTEM。
|
||||
- TOOL。
|
||||
|
||||
## 5. 模块依赖
|
||||
|
||||
- 依赖知识资产模块完成 RAG 召回。
|
||||
- 依赖模型与路由模块完成 Chat 调用。
|
||||
- 可依赖 Workflow 模块执行复杂流程。
|
||||
- 运行观测模块读取会话关联的运行记录。
|
||||
|
||||
27
docs/设计文档/6.MCP能力接入模块设计.md
Normal file
27
docs/设计文档/6.MCP能力接入模块设计.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# MCP 能力接入模块设计
|
||||
|
||||
## 1. 核心模型
|
||||
|
||||
| 对象 | 说明 |
|
||||
|------|------|
|
||||
| MCP Server | 外部能力服务 |
|
||||
| MCP Capability | Server 暴露的工具或资源 |
|
||||
| Manifest | Server 能力声明 |
|
||||
|
||||
## 2. 导入流程
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input["输入 URL/package/Manifest"] --> Validate["校验连接与声明"]
|
||||
Validate --> Server["写入 mcp_server"]
|
||||
Server --> Capability["写入 mcp_capability"]
|
||||
Capability --> Binding["绑定给 Workflow/Agent"]
|
||||
```
|
||||
|
||||
## 3. 设计约束
|
||||
|
||||
- `secret_ref` 只保存密钥引用,不保存明文密钥。
|
||||
- `manifest_json` 保存原始能力声明摘要。
|
||||
- `schema_json` 保存单个能力输入输出 schema。
|
||||
- Server 停用时,其能力不应被新运行选择。
|
||||
|
||||
24
docs/设计文档/7.Skill编辑模块设计.md
Normal file
24
docs/设计文档/7.Skill编辑模块设计.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Skill 编辑模块设计
|
||||
|
||||
## 1. 核心模型
|
||||
|
||||
| 对象 | 说明 |
|
||||
|------|------|
|
||||
| Skill 定义 | Skill 主数据 |
|
||||
| Skill 版本 | Prompt、Code、Config 与测试结果快照 |
|
||||
|
||||
## 2. 版本规则
|
||||
|
||||
- 草稿版本可编辑。
|
||||
- 发布版本不可直接修改。
|
||||
- 归档版本仅可查看。
|
||||
- Workflow 运行必须引用确定版本。
|
||||
|
||||
## 3. 数据设计
|
||||
|
||||
- `prompt_text` 保存提示词。
|
||||
- `code_text` 保存脚本或函数片段。
|
||||
- `config_json` 保存运行配置。
|
||||
- `variable_schema_json` 保存输入输出变量定义。
|
||||
- `test_result_json` 保存最近测试结果摘要。
|
||||
|
||||
36
docs/设计文档/8.运行观测模块设计.md
Normal file
36
docs/设计文档/8.运行观测模块设计.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 运行观测模块设计
|
||||
|
||||
## 1. 观测主键
|
||||
|
||||
运行观测以 `request_id` 为主线串联:
|
||||
|
||||
- Workflow 运行。
|
||||
- Workflow 步骤。
|
||||
- Agent 会话。
|
||||
- 模型调用日志。
|
||||
|
||||
## 2. 展示维度
|
||||
|
||||
- 名称。
|
||||
- 类型。
|
||||
- 状态。
|
||||
- 延迟。
|
||||
- 成本。
|
||||
- 步骤输出。
|
||||
- 错误摘要。
|
||||
|
||||
## 3. 数据来源
|
||||
|
||||
| 来源 | 表 |
|
||||
|------|----|
|
||||
| 流程运行 | `workflow_run` |
|
||||
| 步骤日志 | `workflow_run_step` |
|
||||
| 模型调用 | `model_call_log` |
|
||||
| Agent 会话 | `agent_session`、`agent_message` |
|
||||
|
||||
## 4. 设计约束
|
||||
|
||||
- 观测模块只读业务运行数据。
|
||||
- 不保存完整 Prompt 或敏感密钥。
|
||||
- 错误信息只保存摘要,详细日志由运行环境负责。
|
||||
|
||||
67
docs/需求分析/0.模块总览.md
Normal file
67
docs/需求分析/0.模块总览.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Common Agent Studio 模块总览
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
本文用于统一本轮文档重构的模块口径。后续需求分析、设计文档、前端实现文档、后端实现文档和数据库设计均以本文的产品域模块为准,不再直接按旧后端包名划分。
|
||||
|
||||
本轮只处理文档和 SQL 脚本,不修改 Java、Vue、测试代码。
|
||||
|
||||
## 2. 模块划分
|
||||
|
||||
| 序号 | 模块 | 范围 |
|
||||
|------|------|------|
|
||||
| 1 | 系统基础 | 系统枚举、附件、审计字段、统一返回体、文档解析抽象 |
|
||||
| 2 | 知识资产与文件解析 | 知识库、知识文档、解析快照、切片、向量、索引任务 |
|
||||
| 3 | 模型与路由 | 模型服务商、模型配置、任务路由、知识库模型绑定、调用日志 |
|
||||
| 4 | Workflow 编排 | 项目空间、Workflow 定义、版本快照、运行记录、步骤日志 |
|
||||
| 5 | Agent 会话 | Agent 定义、调试运行、会话、消息、引用切片 |
|
||||
| 6 | MCP 能力接入 | MCP Server 导入、能力发现、能力启停用 |
|
||||
| 7 | Skill 编辑 | Skill 定义、版本、Prompt/Code/Config、测试结果 |
|
||||
| 8 | 运行观测 | Workflow Trace、模型调用日志、成本、延迟、异常排查 |
|
||||
|
||||
## 3. 目录规范
|
||||
|
||||
| 目录 | 用途 |
|
||||
|------|------|
|
||||
| `需求分析/` | 描述业务目标、角色、场景、范围和验收标准 |
|
||||
| `设计文档/` | 描述领域模型、状态流转、模块依赖和接口形态 |
|
||||
| `数据库设计/` | 描述表结构、枚举、脚本同步规则和跨模块关系 |
|
||||
| `前端实现文档/` | 描述页面、ViewModel、API 调用和交互状态 |
|
||||
| `后端实现文档/` | 描述 Controller、DTO、Service、Entity、Mapper 和校验规则 |
|
||||
|
||||
## 4. 脚本范围
|
||||
|
||||
本轮 SQL 脚本属于正式交付范围:
|
||||
|
||||
- `script/*.sql`:面向落库执行的模块化脚本。
|
||||
- `docs/MODEL_PROVIDER_SCHEMA.sql`:模型平台 schema 汇总。
|
||||
- `docs/STUDIO_PROTOTYPE_SCHEMA.sql`:Studio 原型 schema 汇总。
|
||||
|
||||
新增或调整数据库设计时,必须同步更新对应脚本。新增或调整枚举设计时,必须同步更新 `sys_enum` 初始化脚本。
|
||||
|
||||
## 5. 枚举约束
|
||||
|
||||
`sys_enum` 表结构保持不变,Java 枚举契约保持现有格式不变:
|
||||
|
||||
- `catalog`
|
||||
- `type`
|
||||
- `name`
|
||||
- `value`
|
||||
- `strvalue`
|
||||
- `sort`
|
||||
- `remark`
|
||||
|
||||
前后端结构化枚举继续使用整型 `value` 作为协议值,不改成字符串协议。
|
||||
|
||||
## 6. 交叉引用规则
|
||||
|
||||
每个模块文档必须说明:
|
||||
|
||||
- 关联数据库表。
|
||||
- 关联枚举组。
|
||||
- 关联 SQL 脚本。
|
||||
- 关联前端页面或原型 View。
|
||||
- 关联后端接口草案。
|
||||
|
||||
最终以 `数据库设计/9.模块一致性校验.md` 统一检查跨模块冲突。
|
||||
|
||||
41
docs/需求分析/1.系统基础模块需求.md
Normal file
41
docs/需求分析/1.系统基础模块需求.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 系统基础模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
系统基础模块为 Common Agent Studio 提供所有产品域共用的底座能力,包括系统枚举、附件上传、审计字段、统一响应、文档解析抽象和全局异常处理。
|
||||
|
||||
## 2. 用户角色
|
||||
|
||||
| 角色 | 诉求 |
|
||||
|------|------|
|
||||
| 平台管理员 | 维护系统枚举、检查附件上传和基础配置 |
|
||||
| 开发者 | 复用统一 DTO、返回体、审计字段和解析能力 |
|
||||
| 前端开发者 | 使用一致的枚举字典和错误响应 |
|
||||
| 运维人员 | 通过统一字段排查创建人、更新时间和异常信息 |
|
||||
|
||||
## 3. 功能需求
|
||||
|
||||
1. 系统枚举必须支持按 `catalog + type` 查询,用于前端字典、后台管理和初始化校验。
|
||||
2. `sys_enum` 结构必须保持现状,不因 Studio 新增模块调整字段格式。
|
||||
3. 附件模块必须支持本地上传、元数据入库和业务来源关联。
|
||||
4. 文档解析抽象必须支持 TXT/Markdown/LOG、PDF、Word、Excel 的文本抽取。
|
||||
5. 所有业务接口继续返回 `RequestResult<T>`。
|
||||
6. 所有业务实体继续继承公共审计字段和乐观锁字段。
|
||||
|
||||
## 4. 非功能需求
|
||||
|
||||
- 枚举值稳定,不能随展示文案调整而改变。
|
||||
- 附件路径不直接暴露为外部可访问地址。
|
||||
- 异常响应保持统一结构,便于前端统一提示。
|
||||
- 文档解析失败必须返回可定位的错误摘要。
|
||||
|
||||
## 5. 关联资料
|
||||
|
||||
- 表:`sys_enum`、`sys_attachment`
|
||||
- 枚举:`common/enable_status`、`common/common_status`
|
||||
- 脚本:`script/sql/1.enum.sql`、`script/sql/2.attachment.sql`
|
||||
- 后端入口:`SysEnumController`、`SysAttachmentController`、`DocumentParserFactory`
|
||||
- 前端入口:系统枚举 API、文件上传组件、枚举字典调用
|
||||
|
||||
|
||||
|
||||
39
docs/需求分析/2.知识资产与文件解析模块需求.md
Normal file
39
docs/需求分析/2.知识资产与文件解析模块需求.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 知识资产与文件解析模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
知识资产与文件解析模块负责把外部文件变成可检索知识资产,覆盖知识库维护、文件上传、文本解析、切片、向量化和索引状态管理。
|
||||
|
||||
## 2. 核心场景
|
||||
|
||||
1. 知识维护者创建知识库并批量上传文档。
|
||||
2. 系统保存附件元数据并创建 `rag_document`。
|
||||
3. 文档解析管道抽取文本并保存解析快照。
|
||||
4. 用户选择切片策略生成 `rag_chunk`。
|
||||
5. 系统调用 Embedding 模型写入 `rag_chunk_embedding`。
|
||||
6. 知识库达到可检索状态后供 Workflow 和 Agent 调用。
|
||||
|
||||
## 3. 功能需求
|
||||
|
||||
- 知识库支持新增、编辑、删除、查询和概览统计。
|
||||
- 文档支持上传、解析、解析失败重试、切片和索引状态查看。
|
||||
- 解析结果必须落到 `rag_document_parse_result`,切片不能直接依赖原始附件。
|
||||
- 同一文档重新切片时,必须替换旧切片并推动索引重建。
|
||||
- 知识库必须绑定稳定的 Embedding 模型和向量维度。
|
||||
- 前端需要展示文档健康度、解析失败数、待向量化任务数和发布影响。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- 能从知识库视角看到文档数量、解析状态、索引状态和切片数量。
|
||||
- 能从文件解析管道看到上传、解析、切片、向量化、可检索的阶段。
|
||||
- 枚举值与现有 `RagParseStatusEnum`、`RagIndexStatusEnum`、`RagChunkStrategyEnum` 一致。
|
||||
|
||||
## 5. 关联资料
|
||||
|
||||
- 表:`rag_store`、`rag_document`、`rag_document_parse_result`、`rag_chunk`、`rag_chunk_embedding`、`rag_store_model_config`
|
||||
- 枚举:`rag/parse_status`、`rag/index_status`、`rag/chunk_strategy`
|
||||
- 脚本:`script/sql/3.rag_store.sql`、`script/sql/4.rag_document.sql`、`script/sql/5.rag_document_parse_result.sql`、`script/sql/6.rag_chunk.sql`、`script/sql/7.rag_chunk_embedding.sql`
|
||||
- 前端原型:`KnowledgeWorkspacePage.vue`、`IngestionPipelinePage.vue`
|
||||
|
||||
|
||||
|
||||
42
docs/需求分析/3.模型与路由模块需求.md
Normal file
42
docs/需求分析/3.模型与路由模块需求.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 模型与路由模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
模型与路由模块负责统一管理模型服务商、模型配置、任务路由、知识库模型绑定和模型调用日志,为 RAG、Workflow、Agent 和 Skill 提供统一模型入口。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
- 支持配置 Ollama、硅基流动、百炼、OpenAI 和自定义 OpenAI-compatible 服务。
|
||||
- 支持维护 Chat、Embedding、Rerank、多模态模型。
|
||||
- 支持按任务类型配置主模型、Fallback 模型和路由策略。
|
||||
- 支持知识库固定 Embedding 模型、向量维度、切片策略和索引版本。
|
||||
- 支持记录模型调用状态、耗时、Token、成本和错误摘要。
|
||||
|
||||
## 3. 任务类型
|
||||
|
||||
初始任务类型包括:
|
||||
|
||||
- RAG 文档向量化。
|
||||
- RAG 查询向量化。
|
||||
- RAG 问答生成。
|
||||
- 简单文本处理。
|
||||
- 复杂文本处理。
|
||||
- Agent 规划。
|
||||
- 重排序。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- 业务模块不直接调用上游模型服务。
|
||||
- 同一知识库不能混用不同 Embedding 向量空间。
|
||||
- 所有调用必须有 `request_id`,方便观测模块追踪。
|
||||
- 路由规则禁用时不能被选中。
|
||||
|
||||
## 5. 关联资料
|
||||
|
||||
- 表:`model_provider`、`model_config`、`model_route_rule`、`rag_store_model_config`、`model_call_log`
|
||||
- 枚举:`model_provider/provider_type`、`protocol_type`、`model_type`、`task_type`、`route_strategy`、`call_status`、`health_status`
|
||||
- 脚本:`docs/MODEL_PROVIDER_SCHEMA.sql`、`script/sql/9.model_provider.sql`
|
||||
- 前端原型:`ModelWorkspacePage.vue`
|
||||
|
||||
|
||||
|
||||
29
docs/需求分析/4.Workflow编排模块需求.md
Normal file
29
docs/需求分析/4.Workflow编排模块需求.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Workflow 编排模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
Workflow 编排模块负责把知识检索、模型调用、MCP 工具、Skill 和回答输出组织成可保存、可测试、可发布、可追踪的图形化流程。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
- 支持项目空间管理,区分环境和发布状态。
|
||||
- 支持 Workflow 草稿编辑、保存和发布版本。
|
||||
- 支持节点库:Start、LLM、Knowledge Retrieval、MCP Tool、Skill、Condition、Answer。
|
||||
- 支持 JSON Graph 保存画布结构。
|
||||
- 支持运行测试并生成运行记录和步骤日志。
|
||||
- 支持绑定默认 Agent,供对话调试和发布使用。
|
||||
|
||||
## 3. 发布要求
|
||||
|
||||
- 草稿可以保存但不能直接作为生产版本。
|
||||
- 发布时必须生成不可变版本快照。
|
||||
- 发布前必须检查模型路由、知识库索引、MCP 授权和 Skill 发布状态。
|
||||
|
||||
## 4. 关联资料
|
||||
|
||||
- 表:`studio_project`、`workflow_definition`、`workflow_version`、`workflow_run`、`workflow_run_step`
|
||||
- 枚举:`studio/environment`、`studio/publish_status`、`workflow/status`、`workflow/run_status`、`workflow/node_type`
|
||||
- 脚本:`script/sql/11.studio_project.sql`、`script/sql/12.workflow.sql`
|
||||
- 前端原型:`StudioDashboardPage.vue`、`WorkflowBuilderPage.vue`
|
||||
|
||||
|
||||
32
docs/需求分析/5.Agent会话模块需求.md
Normal file
32
docs/需求分析/5.Agent会话模块需求.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Agent 会话模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
Agent 会话模块负责 Agent 定义、对话调试、会话持久化、消息记录、引用切片和运行追踪,使一次调试或发布后的对话可以被复盘。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
- 支持 Agent 定义管理,包含编码、名称、系统提示词、默认知识库和状态。
|
||||
- 支持普通对话和 RAG 对话。
|
||||
- 支持会话持久化,记录 `agent_session`。
|
||||
- 支持消息持久化,记录用户、Agent、系统等角色消息。
|
||||
- 支持保存引用切片 JSON,便于回答溯源。
|
||||
- 支持关联 Workflow 运行记录,形成端到端 Trace。
|
||||
|
||||
## 3. 会话场景
|
||||
|
||||
1. 用户选择 Agent 输入调试问题。
|
||||
2. 系统创建或复用会话。
|
||||
3. RAG 模式下执行检索召回。
|
||||
4. 调用 Chat 模型生成回答。
|
||||
5. 写入消息、引用、模型请求 ID 和运行追踪。
|
||||
|
||||
## 4. 关联资料
|
||||
|
||||
- 表:`agent_definition`、`agent_session`、`agent_message`、`agent_capability_binding`
|
||||
- 脚本:`script/sql/8.agent_definition.sql`、`script/sql/13.agent_session.sql`、`script/sql/16.agent_capability_binding.sql`
|
||||
- 前端原型:`AgentWorkspacePage.vue`
|
||||
- 后端入口:`AgentDefinitionController`、`AgentDefinitionServiceImpl`
|
||||
|
||||
|
||||
|
||||
22
docs/需求分析/6.MCP能力接入模块需求.md
Normal file
22
docs/需求分析/6.MCP能力接入模块需求.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# MCP 能力接入模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
MCP 能力接入模块负责导入外部 MCP Server,读取能力清单,并把工具或资源能力绑定给 Workflow 和 Agent 使用。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
- 支持 URL、npm package、JSON Manifest 三种导入方式。
|
||||
- 支持保存 Server 连接信息、鉴权方式、密钥引用和 Manifest。
|
||||
- 支持发现工具、资源等能力。
|
||||
- 支持能力启停用。
|
||||
- 支持健康状态展示。
|
||||
|
||||
## 3. 关联资料
|
||||
|
||||
- 表:`mcp_server`、`mcp_capability`
|
||||
- 枚举:`mcp/import_type`、`mcp/capability_type`、`mcp/health_status`
|
||||
- 脚本:`script/sql/14.mcp.sql`、`script/sql/18.studio_enum.sql`
|
||||
- 前端原型:`McpImportPage.vue`
|
||||
|
||||
|
||||
22
docs/需求分析/7.Skill编辑模块需求.md
Normal file
22
docs/需求分析/7.Skill编辑模块需求.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Skill 编辑模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
Skill 编辑模块负责维护可复用的提示词、代码片段、配置和测试结果,使 Workflow 或 Agent 可以复用稳定的能力单元。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
- 支持 Skill 定义管理。
|
||||
- 支持版本草稿、发布和归档。
|
||||
- 支持 Prompt、Code、Config 三类编辑区域。
|
||||
- 支持变量 schema 和测试结果保存。
|
||||
- 支持发布版本供 Workflow 或 Agent 绑定。
|
||||
|
||||
## 3. 关联资料
|
||||
|
||||
- 表:`skill_definition`、`skill_version`
|
||||
- 枚举:`skill/skill_type`、`skill/status`、`studio/publish_status`
|
||||
- 脚本:`script/sql/15.skill.sql`、`script/sql/18.studio_enum.sql`
|
||||
- 前端原型:`SkillWorkspacePage.vue`
|
||||
|
||||
|
||||
22
docs/需求分析/8.运行观测模块需求.md
Normal file
22
docs/需求分析/8.运行观测模块需求.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 运行观测模块需求
|
||||
|
||||
## 1. 模块目标
|
||||
|
||||
运行观测模块负责展示 Workflow、Agent、MCP、Skill 和模型调用的运行记录,支撑排障、成本分析和发布质量评估。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
- 展示最近运行记录。
|
||||
- 展示步骤日志和节点输出摘要。
|
||||
- 展示模型调用耗时、Token 和成本。
|
||||
- 支持按 requestId 追踪一次完整运行。
|
||||
- 支持导出日志。
|
||||
|
||||
## 3. 关联资料
|
||||
|
||||
- 表:`workflow_run`、`workflow_run_step`、`model_call_log`、`agent_session`、`agent_message`
|
||||
- 脚本:`script/sql/12.workflow.sql`、`script/sql/9.model_provider.sql`、`script/sql/13.agent_session.sql`
|
||||
- 前端原型:`ObservabilityPage.vue`
|
||||
|
||||
|
||||
|
||||
34
frontend/README.md
Normal file
34
frontend/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Common Agent Frontend
|
||||
|
||||
## 启动方式
|
||||
|
||||
进入前端目录:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm config set registry https://registry.npmjs.org/
|
||||
pnpm install
|
||||
```
|
||||
|
||||
启动开发服务:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产包:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
本地预览生产包:
|
||||
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
171
frontend/pnpm-lock.yaml
generated
171
frontend/pnpm-lock.yaml
generated
@@ -56,7 +56,7 @@ importers:
|
||||
version: 4.1.6(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.3(@types/node@24.12.4))
|
||||
vue-tsc:
|
||||
specifier: ^3.1.8
|
||||
version: 3.3.1(typescript@5.9.3)
|
||||
version: 3.3.0(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -553,17 +553,17 @@ packages:
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/devtools-api@7.7.7':
|
||||
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
|
||||
'@vue/devtools-api@7.7.9':
|
||||
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
||||
|
||||
'@vue/devtools-kit@7.7.7':
|
||||
resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==}
|
||||
'@vue/devtools-kit@7.7.9':
|
||||
resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==}
|
||||
|
||||
'@vue/devtools-shared@7.7.7':
|
||||
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
|
||||
'@vue/devtools-shared@7.7.9':
|
||||
resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
|
||||
|
||||
'@vue/language-core@3.3.1':
|
||||
resolution: {integrity: sha512-NP8g6V7x81NVOXbLupUvYY6i6LqUkjkVowe2epRedmpgaFCOdjgWHE/rQBvEJ4r7koAYODIjGeBWEdt6n7jYXQ==}
|
||||
'@vue/language-core@3.3.0':
|
||||
resolution: {integrity: sha512-EyUxq1b8Yoxk6hQ6X33BIRnfFLb9Rbm9w/8G8y6uMxlQu7CW7yy9JS/z54xSpIvBvVWX6Lt5v1aBGwmrqD4aJw==}
|
||||
|
||||
'@vue/reactivity@3.5.34':
|
||||
resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==}
|
||||
@@ -620,16 +620,12 @@ packages:
|
||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
agent-base@6.0.0:
|
||||
resolution: {integrity: sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==}
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
agent-base@7.1.0:
|
||||
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
agent-base@7.1.2:
|
||||
resolution: {integrity: sha512-JVzqkCNRT+VfqzzgPWDPnwvDheSAUdiMUn3NoLXpDJF5lRqeJqyC9iGsAxIOAW+mzIdq+uP1TvcX6bMtrH0agg==}
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
alien-signals@3.2.1:
|
||||
@@ -664,8 +660,8 @@ packages:
|
||||
axios@1.16.1:
|
||||
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
|
||||
|
||||
balanced-match@1.0.0:
|
||||
resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==}
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
@@ -673,8 +669,8 @@ packages:
|
||||
bignumber.js@9.0.0:
|
||||
resolution: {integrity: sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==}
|
||||
|
||||
birpc@2.3.0:
|
||||
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
|
||||
birpc@2.9.0:
|
||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
||||
|
||||
brace-expansion@2.1.0:
|
||||
resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
|
||||
@@ -698,8 +694,8 @@ packages:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
commander@10.0.0:
|
||||
resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==}
|
||||
commander@10.0.1:
|
||||
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
config-chain@1.1.13:
|
||||
@@ -833,8 +829,8 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
foreground-child@3.1.0:
|
||||
resolution: {integrity: sha512-lXeSPRCndWPaipZbtI4CkvTZpF6OPsy19dkvf7+5AHeJD+w+iAKPc9Q78xWBmX4SdR+8xrtY9jTXs/YDv8q+Ug==}
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.5:
|
||||
@@ -897,9 +893,8 @@ packages:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ini@1.3.4:
|
||||
resolution: {integrity: sha512-VUA7WAWNCWfm6/8f9kAb8Y6iGBWnmCfgFS5dTrv2C38LLm1KUmpY388mCVCJCsMKQomvOQ1oW8/edXdChd9ZXQ==}
|
||||
deprecated: Please update to ini >=1.3.6 to avoid a prototype pollution issue
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
@@ -915,9 +910,8 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jackspeak@3.1.2:
|
||||
resolution: {integrity: sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==}
|
||||
engines: {node: '>=14'}
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
js-beautify@1.15.4:
|
||||
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
|
||||
@@ -953,12 +947,11 @@ packages:
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
lru-cache@10.2.0:
|
||||
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.5.0:
|
||||
resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==}
|
||||
lru-cache@11.4.0:
|
||||
resolution: {integrity: sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
magic-string@0.30.21:
|
||||
@@ -1119,8 +1112,8 @@ packages:
|
||||
std-env@4.1.0:
|
||||
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||
|
||||
string-width@4.2.0:
|
||||
resolution: {integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==}
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
@@ -1264,16 +1257,16 @@ packages:
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
vue-component-type-helpers@3.3.1:
|
||||
resolution: {integrity: sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==}
|
||||
vue-component-type-helpers@3.3.0:
|
||||
resolution: {integrity: sha512-vwR8DDsBysI9NWXa0okPFpCcW+BUC3sPTuLBNo1faMzw4QWMFd+3/lFYFu29ZN0q+8UReXWJHEYesC9dcXYCLg==}
|
||||
|
||||
vue-router@4.6.4:
|
||||
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
vue-tsc@3.3.1:
|
||||
resolution: {integrity: sha512-webBP3jhlxzhELZ2g+11KJ6pg5OVY1xWhWrj7N/yQMi1CrtxJnW+tUACyRVeDK0cQNLP2Va5HNYK8pe+7c+msw==}
|
||||
vue-tsc@3.3.0:
|
||||
resolution: {integrity: sha512-kY8RcoTOENASi0P1GLPvJgA2+hoGF+t8We1UGgmnAb1r/GjTUMSE3zz+WGfjPORZNnBHdAt67sVPhBLXWunkeg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
@@ -1306,8 +1299,8 @@ packages:
|
||||
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
which@2.0.1:
|
||||
resolution: {integrity: sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==}
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1353,7 +1346,7 @@ snapshots:
|
||||
'@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
lru-cache: 11.5.0
|
||||
lru-cache: 11.4.0
|
||||
|
||||
'@asamuzakjp/dom-selector@6.8.1':
|
||||
dependencies:
|
||||
@@ -1361,7 +1354,7 @@ snapshots:
|
||||
bidi-js: 1.0.3
|
||||
css-tree: 3.2.1
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.5.0
|
||||
lru-cache: 11.4.0
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||
|
||||
@@ -1502,7 +1495,7 @@ snapshots:
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.0
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.2.0
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
@@ -1710,25 +1703,25 @@ snapshots:
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/devtools-api@7.7.7':
|
||||
'@vue/devtools-api@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.7
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
|
||||
'@vue/devtools-kit@7.7.7':
|
||||
'@vue/devtools-kit@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 7.7.7
|
||||
birpc: 2.3.0
|
||||
'@vue/devtools-shared': 7.7.9
|
||||
birpc: 2.9.0
|
||||
hookable: 5.5.3
|
||||
mitt: 3.0.1
|
||||
perfect-debounce: 1.0.0
|
||||
speakingurl: 14.0.1
|
||||
superjson: 2.2.6
|
||||
|
||||
'@vue/devtools-shared@7.7.7':
|
||||
'@vue/devtools-shared@7.7.9':
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
|
||||
'@vue/language-core@3.3.1':
|
||||
'@vue/language-core@3.3.0':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
@@ -1767,7 +1760,7 @@ snapshots:
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
js-beautify: 1.15.4
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
vue-component-type-helpers: 3.3.1
|
||||
vue-component-type-helpers: 3.3.0
|
||||
optionalDependencies:
|
||||
'@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.9.3))
|
||||
|
||||
@@ -1791,23 +1784,13 @@ snapshots:
|
||||
|
||||
abbrev@2.0.0: {}
|
||||
|
||||
agent-base@6.0.0:
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
alien-signals@3.2.1: {}
|
||||
|
||||
@@ -1837,7 +1820,7 @@ snapshots:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
balanced-match@1.0.0: {}
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
dependencies:
|
||||
@@ -1845,11 +1828,11 @@ snapshots:
|
||||
|
||||
bignumber.js@9.0.0: {}
|
||||
|
||||
birpc@2.3.0: {}
|
||||
birpc@2.9.0: {}
|
||||
|
||||
brace-expansion@2.1.0:
|
||||
dependencies:
|
||||
balanced-match: 1.0.0
|
||||
balanced-match: 1.0.2
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
@@ -1868,11 +1851,11 @@ snapshots:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
commander@10.0.0: {}
|
||||
commander@10.0.1: {}
|
||||
|
||||
config-chain@1.1.13:
|
||||
dependencies:
|
||||
ini: 1.3.4
|
||||
ini: 1.3.8
|
||||
proto-list: 1.2.4
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
@@ -1885,7 +1868,7 @@ snapshots:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.1
|
||||
which: 2.0.2
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
@@ -1897,7 +1880,7 @@ snapshots:
|
||||
'@asamuzakjp/css-color': 4.1.2
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1)
|
||||
css-tree: 3.2.1
|
||||
lru-cache: 11.5.0
|
||||
lru-cache: 11.4.0
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
@@ -1927,7 +1910,7 @@ snapshots:
|
||||
editorconfig@1.0.7:
|
||||
dependencies:
|
||||
'@one-ini/wasm': 0.1.1
|
||||
commander: 10.0.0
|
||||
commander: 10.0.1
|
||||
minimatch: 9.0.9
|
||||
semver: 7.8.0
|
||||
|
||||
@@ -1948,7 +1931,7 @@ snapshots:
|
||||
memoize-one: 6.0.0
|
||||
normalize-wheel-es: 1.2.0
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
vue-component-type-helpers: 3.3.1
|
||||
vue-component-type-helpers: 3.3.0
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
@@ -2018,7 +2001,7 @@ snapshots:
|
||||
|
||||
follow-redirects@1.16.0: {}
|
||||
|
||||
foreground-child@3.1.0:
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
@@ -2056,8 +2039,8 @@ snapshots:
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.1.0
|
||||
jackspeak: 3.1.2
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.9
|
||||
minipass: 7.1.3
|
||||
package-json-from-dist: 1.0.1
|
||||
@@ -2085,26 +2068,26 @@ snapshots:
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.0
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.0
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.2
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ini@1.3.4: {}
|
||||
ini@1.3.8: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
@@ -2114,7 +2097,7 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@3.1.2:
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
@@ -2172,9 +2155,9 @@ snapshots:
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
lru-cache@10.2.0: {}
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.5.0: {}
|
||||
lru-cache@11.4.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
@@ -2226,7 +2209,7 @@ snapshots:
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.2.0
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.3
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@@ -2239,7 +2222,7 @@ snapshots:
|
||||
|
||||
pinia@3.0.4(typescript@5.9.3)(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.7
|
||||
'@vue/devtools-api': 7.7.9
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
@@ -2315,7 +2298,7 @@ snapshots:
|
||||
|
||||
std-env@4.1.0: {}
|
||||
|
||||
string-width@4.2.0:
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
@@ -2412,17 +2395,17 @@ snapshots:
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vue-component-type-helpers@3.3.1: {}
|
||||
vue-component-type-helpers@3.3.0: {}
|
||||
|
||||
vue-router@4.6.4(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
vue-tsc@3.3.1(typescript@5.9.3):
|
||||
vue-tsc@3.3.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.28
|
||||
'@vue/language-core': 3.3.1
|
||||
'@vue/language-core': 3.3.0
|
||||
typescript: 5.9.3
|
||||
|
||||
vue@3.5.34(typescript@5.9.3):
|
||||
@@ -2450,7 +2433,7 @@ snapshots:
|
||||
tr46: 6.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
|
||||
which@2.0.1:
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
@@ -2462,7 +2445,7 @@ snapshots:
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
esbuild: false
|
||||
|
||||
43
frontend/src/api/__tests__/agent.spec.ts
Normal file
43
frontend/src/api/__tests__/agent.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
chatWithAgent,
|
||||
deleteAgent,
|
||||
getAgentById,
|
||||
listAgents,
|
||||
queryAgents,
|
||||
saveAgent,
|
||||
} from '../agent';
|
||||
import { get, post } from '../request';
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('agent api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('maps agent endpoints correctly', () => {
|
||||
listAgents();
|
||||
queryAgents({ agentCode: 'demo' });
|
||||
getAgentById('1001');
|
||||
saveAgent({ agentCode: 'agent_1', agentName: 'Agent 1', storeId: '2001', status: 'ENABLED' });
|
||||
deleteAgent('1001');
|
||||
chatWithAgent('1001', { messages: [{ role: 'user', content: '你好' }] });
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/agents/list');
|
||||
expect(post).toHaveBeenCalledWith('/agents/query', { agentCode: 'demo' });
|
||||
expect(get).toHaveBeenCalledWith('/agents/detail', { params: { id: '1001' } });
|
||||
expect(post).toHaveBeenCalledWith('/agents/save', {
|
||||
agentCode: 'agent_1',
|
||||
agentName: 'Agent 1',
|
||||
storeId: '2001',
|
||||
status: 'ENABLED',
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith('/agents/delete', undefined, { params: { id: '1001' } });
|
||||
expect(post).toHaveBeenCalledWith('/agents/1001/chat', { messages: [{ role: 'user', content: '你好' }] });
|
||||
});
|
||||
});
|
||||
62
frontend/src/api/__tests__/modelProvider.spec.ts
Normal file
62
frontend/src/api/__tests__/modelProvider.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
checkModelProviderHealth,
|
||||
deleteModelConfig,
|
||||
deleteModelProvider,
|
||||
deleteModelRouteRule,
|
||||
getRagStoreModelConfig,
|
||||
queryModelCallLogs,
|
||||
queryModelConfigs,
|
||||
queryModelProviders,
|
||||
queryModelRouteRules,
|
||||
rebuildRagStoreIndex,
|
||||
saveModelConfig,
|
||||
saveModelProvider,
|
||||
saveModelRouteRule,
|
||||
saveRagStoreModelConfig,
|
||||
} from '../modelProvider';
|
||||
import { get, post } from '../request';
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('model provider api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls model platform endpoints with expected params', () => {
|
||||
queryModelProviders();
|
||||
saveModelProvider({ providerCode: 'OPENAI_MAIN' } as never);
|
||||
deleteModelProvider('1');
|
||||
checkModelProviderHealth('1');
|
||||
queryModelConfigs();
|
||||
saveModelConfig({ modelCode: 'EMB_1' } as never);
|
||||
deleteModelConfig('2');
|
||||
queryModelRouteRules();
|
||||
saveModelRouteRule({ routeCode: 'RAG_GLOBAL' } as never);
|
||||
deleteModelRouteRule('3');
|
||||
queryModelCallLogs({ taskType: 'RAG_EMBEDDING' });
|
||||
getRagStoreModelConfig('10');
|
||||
saveRagStoreModelConfig({ storeId: '10', embeddingModelId: '2' } as never);
|
||||
rebuildRagStoreIndex('10');
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/model/providers/query');
|
||||
expect(post).toHaveBeenCalledWith('/model/providers/save', { providerCode: 'OPENAI_MAIN' });
|
||||
expect(post).toHaveBeenCalledWith('/model/providers/delete', undefined, { params: { id: '1' } });
|
||||
expect(post).toHaveBeenCalledWith('/model/providers/checkHealth', undefined, { params: { id: '1' } });
|
||||
expect(post).toHaveBeenCalledWith('/model/configs/query');
|
||||
expect(post).toHaveBeenCalledWith('/model/configs/save', { modelCode: 'EMB_1' });
|
||||
expect(post).toHaveBeenCalledWith('/model/configs/delete', undefined, { params: { id: '2' } });
|
||||
expect(post).toHaveBeenCalledWith('/model/routes/query');
|
||||
expect(post).toHaveBeenCalledWith('/model/routes/save', { routeCode: 'RAG_GLOBAL' });
|
||||
expect(post).toHaveBeenCalledWith('/model/routes/delete', undefined, { params: { id: '3' } });
|
||||
expect(post).toHaveBeenCalledWith('/model/call-logs/query', { taskType: 'RAG_EMBEDDING' });
|
||||
expect(get).toHaveBeenCalledWith('/rag/store/modelConfig', { params: { storeId: '10' } });
|
||||
expect(post).toHaveBeenCalledWith('/rag/store/modelConfig/save', { storeId: '10', embeddingModelId: '2' });
|
||||
expect(post).toHaveBeenCalledWith('/rag/store/rebuildIndex', undefined, { params: { storeId: '10' } });
|
||||
});
|
||||
});
|
||||
70
frontend/src/api/agent.ts
Normal file
70
frontend/src/api/agent.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { get, post } from './request';
|
||||
|
||||
export interface AgentDefinition {
|
||||
id?: string;
|
||||
agentCode: string;
|
||||
agentName: string;
|
||||
systemPrompt?: string;
|
||||
storeId: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface AgentDefinitionQueryRequest {
|
||||
agentCode?: string;
|
||||
agentName?: string;
|
||||
status?: string;
|
||||
storeId?: string;
|
||||
}
|
||||
|
||||
export interface AgentMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AgentChatRequest {
|
||||
messages: AgentMessage[];
|
||||
ragEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentReferenceChunk {
|
||||
chunkId: string;
|
||||
documentId: string;
|
||||
chunkContent: string;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface AgentChatResponse {
|
||||
agentId: string;
|
||||
agentCode: string;
|
||||
agentName: string;
|
||||
storeId: string;
|
||||
storeName?: string;
|
||||
answer: string;
|
||||
modelRequestId: string;
|
||||
references: AgentReferenceChunk[];
|
||||
}
|
||||
|
||||
export function listAgents() {
|
||||
return post<AgentDefinition[]>('/agents/list');
|
||||
}
|
||||
|
||||
export function queryAgents(query?: AgentDefinitionQueryRequest) {
|
||||
return post<AgentDefinition[], AgentDefinitionQueryRequest | undefined>('/agents/query', query);
|
||||
}
|
||||
|
||||
export function getAgentById(id: string) {
|
||||
return get<AgentDefinition>('/agents/detail', { params: { id } });
|
||||
}
|
||||
|
||||
export function saveAgent(data: Partial<AgentDefinition> & { id?: string }) {
|
||||
return post<boolean>('/agents/save', data);
|
||||
}
|
||||
|
||||
export function deleteAgent(id: string) {
|
||||
return post<boolean>('/agents/delete', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
export function chatWithAgent(agentId: string, data: AgentChatRequest) {
|
||||
return post<AgentChatResponse, AgentChatRequest>(`/agents/${agentId}/chat`, data);
|
||||
}
|
||||
52
frontend/src/api/modelEnums.ts
Normal file
52
frontend/src/api/modelEnums.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { listForManagement, type SysEnum } from './sysEnums';
|
||||
|
||||
export interface EnumOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const enumCache = new Map<string, EnumOption[]>();
|
||||
|
||||
function buildCacheKey(catalog: string, type: string) {
|
||||
return `${catalog}::${type}`;
|
||||
}
|
||||
|
||||
function mapOption(item: SysEnum): EnumOption {
|
||||
const value = item.strvalue && item.strvalue.trim()
|
||||
? item.strvalue
|
||||
: item.name;
|
||||
return {
|
||||
label: item.name,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadEnumOptions(catalog: string, type: string, forceRefresh = false) {
|
||||
const cacheKey = buildCacheKey(catalog, type);
|
||||
if (!forceRefresh && enumCache.has(cacheKey)) {
|
||||
return enumCache.get(cacheKey) ?? [];
|
||||
}
|
||||
const response = await listForManagement({ catalog, type, keyword: '' });
|
||||
const options = (response.data ?? []).map(mapOption);
|
||||
enumCache.set(cacheKey, options);
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function loadModelProviderEnumOptions(forceRefresh = false) {
|
||||
const catalog = 'model_provider';
|
||||
const types = [
|
||||
'provider_type',
|
||||
'protocol_type',
|
||||
'auth_type',
|
||||
'model_type',
|
||||
'task_type',
|
||||
'route_strategy',
|
||||
'health_status',
|
||||
'call_status',
|
||||
'match_scope',
|
||||
];
|
||||
const entries = await Promise.all(
|
||||
types.map(async (type) => [type, await loadEnumOptions(catalog, type, forceRefresh)] as const),
|
||||
);
|
||||
return Object.fromEntries(entries) as Record<string, EnumOption[]>;
|
||||
}
|
||||
141
frontend/src/api/modelProvider.ts
Normal file
141
frontend/src/api/modelProvider.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { get, post } from './request';
|
||||
|
||||
export interface ModelProvider {
|
||||
id?: string;
|
||||
providerCode: string;
|
||||
providerName: string;
|
||||
providerType: string;
|
||||
protocolType: string;
|
||||
baseUrl: string;
|
||||
authType: string;
|
||||
secretRef?: string;
|
||||
hasApiKey?: boolean;
|
||||
timeoutMs: number;
|
||||
priority: number;
|
||||
enabled: boolean;
|
||||
healthStatus?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
id?: string;
|
||||
providerId: string;
|
||||
modelCode: string;
|
||||
modelName: string;
|
||||
upstreamModel: string;
|
||||
modelType: string;
|
||||
embeddingDimension?: number;
|
||||
localModel: boolean;
|
||||
defaultModel: boolean;
|
||||
optionsJson?: string;
|
||||
enabled: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ModelRouteRule {
|
||||
id?: string;
|
||||
routeCode: string;
|
||||
routeName: string;
|
||||
taskType: string;
|
||||
matchScope: string;
|
||||
scopeId?: string;
|
||||
primaryModelId: string;
|
||||
fallbackModelIdsJson?: string;
|
||||
routeStrategy: string;
|
||||
maxLatencyMs?: number;
|
||||
enabled: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ModelCallLog {
|
||||
id?: string;
|
||||
requestId: string;
|
||||
providerId?: string;
|
||||
modelId?: string;
|
||||
taskType: string;
|
||||
bizType?: string;
|
||||
bizId?: string;
|
||||
callType: string;
|
||||
status: string;
|
||||
durationMs?: number;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface ModelCallLogQueryRequest {
|
||||
taskType?: string;
|
||||
providerId?: string;
|
||||
modelId?: string;
|
||||
status?: string;
|
||||
bizType?: string;
|
||||
}
|
||||
|
||||
export interface RagStoreModelConfig {
|
||||
id?: string;
|
||||
storeId: string;
|
||||
embeddingModelId: string;
|
||||
embeddingDimension: number;
|
||||
chunkStrategy?: number;
|
||||
chunkSize?: number;
|
||||
chunkOverlap?: number;
|
||||
delimiter?: string;
|
||||
active?: boolean;
|
||||
indexVersion?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export function queryModelProviders() {
|
||||
return post<ModelProvider[]>('/model/providers/query');
|
||||
}
|
||||
|
||||
export function saveModelProvider(data: Partial<ModelProvider> & { id?: string }) {
|
||||
return post<boolean>('/model/providers/save', data);
|
||||
}
|
||||
|
||||
export function deleteModelProvider(id: string) {
|
||||
return post<boolean>('/model/providers/delete', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
export function checkModelProviderHealth(id: string) {
|
||||
return post<boolean>('/model/providers/checkHealth', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
export function queryModelConfigs() {
|
||||
return post<ModelConfig[]>('/model/configs/query');
|
||||
}
|
||||
|
||||
export function saveModelConfig(data: Partial<ModelConfig> & { id?: string }) {
|
||||
return post<boolean>('/model/configs/save', data);
|
||||
}
|
||||
|
||||
export function deleteModelConfig(id: string) {
|
||||
return post<boolean>('/model/configs/delete', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
export function queryModelRouteRules() {
|
||||
return post<ModelRouteRule[]>('/model/routes/query');
|
||||
}
|
||||
|
||||
export function saveModelRouteRule(data: Partial<ModelRouteRule> & { id?: string }) {
|
||||
return post<boolean>('/model/routes/save', data);
|
||||
}
|
||||
|
||||
export function deleteModelRouteRule(id: string) {
|
||||
return post<boolean>('/model/routes/delete', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
export function queryModelCallLogs(query?: ModelCallLogQueryRequest) {
|
||||
return post<ModelCallLog[], ModelCallLogQueryRequest | undefined>('/model/call-logs/query', query);
|
||||
}
|
||||
|
||||
export function getRagStoreModelConfig(storeId: string) {
|
||||
return get<RagStoreModelConfig>('/rag/store/modelConfig', { params: { storeId } });
|
||||
}
|
||||
|
||||
export function saveRagStoreModelConfig(data: Partial<RagStoreModelConfig> & { storeId: string }) {
|
||||
return post<boolean>('/rag/store/modelConfig/save', data);
|
||||
}
|
||||
|
||||
export function rebuildRagStoreIndex(storeId: string) {
|
||||
return post<boolean>('/rag/store/rebuildIndex', undefined, { params: { storeId } });
|
||||
}
|
||||
@@ -47,16 +47,28 @@ export interface RagDocumentBatchUploadRequest {
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export type RagChunkStrategy =
|
||||
| 'FIXED_LENGTH'
|
||||
| 'PARAGRAPH'
|
||||
| 'HEADING'
|
||||
| 'TABLE_ROW'
|
||||
| 'DELIMITER'
|
||||
| 'SEMANTIC';
|
||||
/**
|
||||
* RAG 切片策略枚举值。
|
||||
* <p>
|
||||
* 前后端统一传递枚举值,不再传递字符串名称。
|
||||
*/
|
||||
export const RAG_CHUNK_STRATEGY = {
|
||||
FIXED_LENGTH: 1,
|
||||
PARAGRAPH: 2,
|
||||
HEADING: 3,
|
||||
TABLE_ROW: 4,
|
||||
DELIMITER: 5,
|
||||
SEMANTIC: 6,
|
||||
} as const;
|
||||
|
||||
export type RagChunkStrategy = (typeof RAG_CHUNK_STRATEGY)[keyof typeof RAG_CHUNK_STRATEGY];
|
||||
|
||||
export interface RagDocumentParseRequest {
|
||||
documentIds: string[];
|
||||
}
|
||||
|
||||
export interface RagDocumentChunkRequest {
|
||||
documentIds: string[];
|
||||
chunkStrategy: RagChunkStrategy;
|
||||
chunkSize?: number;
|
||||
chunkOverlap?: number;
|
||||
@@ -115,3 +127,11 @@ export function batchUploadRagDocuments(data: RagDocumentBatchUploadRequest) {
|
||||
export function parseRagDocuments(data: RagDocumentParseRequest) {
|
||||
return post<RagDocumentParseResponse[], RagDocumentParseRequest>('/rag/documents/parse', data);
|
||||
}
|
||||
|
||||
export function retryParseRagDocuments(data: RagDocumentParseRequest) {
|
||||
return post<RagDocumentParseResponse[], RagDocumentParseRequest>('/rag/documents/retryParse', data);
|
||||
}
|
||||
|
||||
export function chunkRagDocuments(data: RagDocumentChunkRequest) {
|
||||
return post<boolean, RagDocumentChunkRequest>('/rag/documents/chunk', data);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computed, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
batchUploadRagDocuments,
|
||||
type RagDocument,
|
||||
SOURCE_TYPE_RAG,
|
||||
} from '@/api/ragDocuments';
|
||||
import type { RagStore } from '@/api/ragStores';
|
||||
@@ -17,7 +18,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void;
|
||||
(event: 'uploaded'): void;
|
||||
(event: 'uploaded', documentIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const submitting = ref(false);
|
||||
@@ -83,16 +84,19 @@ async function submitUpload() {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await batchUploadRagDocuments({
|
||||
const response = await batchUploadRagDocuments({
|
||||
storeId: uploadStoreId.value,
|
||||
sourceType: SOURCE_TYPE_RAG,
|
||||
files: uploadFiles.value,
|
||||
documentSummary: uploadSummary.value || undefined,
|
||||
remark: uploadRemark.value || undefined,
|
||||
});
|
||||
const ids = (response.data ?? [])
|
||||
.map((doc: RagDocument) => doc.id ?? '')
|
||||
.filter(Boolean);
|
||||
visible.value = false;
|
||||
ElMessage.success('文档已上传');
|
||||
emit('uploaded');
|
||||
emit('uploaded', ids);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
59
frontend/src/components/rag/chunk/RagChunkTaskBoard.vue
Normal file
59
frontend/src/components/rag/chunk/RagChunkTaskBoard.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
total: number;
|
||||
parsed: number;
|
||||
failed: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="task-board">
|
||||
<div class="task-card">
|
||||
<strong>{{ total }}</strong>
|
||||
<span>文档总数</span>
|
||||
</div>
|
||||
<div class="task-card">
|
||||
<strong>{{ parsed }}</strong>
|
||||
<span>可切片文档</span>
|
||||
</div>
|
||||
<div class="task-card">
|
||||
<strong>{{ failed }}</strong>
|
||||
<span>解析失败文档</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.task-card strong {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: #2f3a4f;
|
||||
}
|
||||
|
||||
.task-card span {
|
||||
color: #738099;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
frontend/src/components/rag/document/RagFlowOverview.vue
Normal file
38
frontend/src/components/rag/document/RagFlowOverview.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { RagDocument } from '@/api/ragDocuments';
|
||||
|
||||
const props = defineProps<{
|
||||
documents: RagDocument[];
|
||||
}>();
|
||||
|
||||
const total = computed(() => props.documents.length);
|
||||
const parsing = computed(() => props.documents.filter((row) => row.parseStatus === 'PARSING').length);
|
||||
const parsed = computed(() => props.documents.filter((row) => row.parseStatus === 'PARSED').length);
|
||||
const failed = computed(() => props.documents.filter((row) => row.parseStatus === 'FAILED').length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flow-overview">
|
||||
<el-tag type="info">总文档 {{ total }}</el-tag>
|
||||
<el-tag type="warning">解析中 {{ parsing }}</el-tag>
|
||||
<el-tag type="success">可切片 {{ parsed }}</el-tag>
|
||||
<el-tag type="danger">解析失败 {{ failed }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flow-overview {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 28px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flow-overview {
|
||||
padding: 14px 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
frontend/src/data/studioMock.ts
Normal file
143
frontend/src/data/studioMock.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
export type PipelineStatus = 'done' | 'running' | 'blocked' | 'idle';
|
||||
|
||||
export interface LifecycleStep {
|
||||
name: string;
|
||||
description: string;
|
||||
status: PipelineStatus;
|
||||
}
|
||||
|
||||
export interface RecentRun {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
latency: string;
|
||||
cost: string;
|
||||
}
|
||||
|
||||
export interface KnowledgeDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
parseStatus: string;
|
||||
indexStatus: string;
|
||||
chunks: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface WorkflowNode {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
description: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface WorkflowEdge {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface TraceStep {
|
||||
node: string;
|
||||
status: string;
|
||||
duration: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
export const lifecycleSteps: LifecycleStep[] = [
|
||||
{ name: '知识接入', description: '上传、解析、切片、向量化', status: 'done' },
|
||||
{ name: '能力编排', description: 'Workflow 连接模型、工具与 Skill', status: 'running' },
|
||||
{ name: '对话调试', description: '验证引用、成本、延迟与回答质量', status: 'running' },
|
||||
{ name: '发布观测', description: '版本快照、运行追踪、异常排查', status: 'idle' },
|
||||
];
|
||||
|
||||
export const readinessChecklist = [
|
||||
{ label: '知识库已绑定 Embedding 模型', done: true },
|
||||
{ label: 'Workflow 草稿存在未发布节点变更', done: false },
|
||||
{ label: 'Agent 已绑定默认知识库与 Skill', done: true },
|
||||
{ label: '生产环境路由规则仍需压测', done: false },
|
||||
];
|
||||
|
||||
export const recentRuns: RecentRun[] = [
|
||||
{ id: 'run-1842', name: '售前问答 Agent', type: 'Agent', status: '成功', latency: '1.42s', cost: '¥0.018' },
|
||||
{ id: 'run-1841', name: '合同知识召回', type: 'Workflow', status: '成功', latency: '860ms', cost: '¥0.006' },
|
||||
{ id: 'run-1840', name: 'MCP: jira.search', type: 'MCP', status: '重试', latency: '2.8s', cost: '¥0.000' },
|
||||
];
|
||||
|
||||
export const knowledgeStores = [
|
||||
{ id: '1001', name: '产品制度库', docs: 128, health: 96, status: '可检索' },
|
||||
{ id: '1002', name: '交付项目资料', docs: 64, health: 82, status: '索引中' },
|
||||
{ id: '1003', name: '客服 FAQ', docs: 214, health: 91, status: '可检索' },
|
||||
];
|
||||
|
||||
export const knowledgeDocuments: KnowledgeDocument[] = [
|
||||
{ id: 'doc-01', name: '售前方案模板.pdf', parseStatus: 'PARSED', indexStatus: 'INDEXED', chunks: 42, updatedAt: '10分钟前' },
|
||||
{ id: 'doc-02', name: '项目实施手册.docx', parseStatus: 'PARSED', indexStatus: 'INDEXING', chunks: 88, updatedAt: '23分钟前' },
|
||||
{ id: 'doc-03', name: '服务条款更新.md', parseStatus: 'FAILED', indexStatus: 'PENDING', chunks: 0, updatedAt: '今天 09:12' },
|
||||
{ id: 'doc-04', name: '客服高频问题.xlsx', parseStatus: 'PARSED', indexStatus: 'INDEXED', chunks: 119, updatedAt: '昨天' },
|
||||
];
|
||||
|
||||
export const ingestionSteps: LifecycleStep[] = [
|
||||
{ name: '上传', description: '4 个文件已入库 sys_attachment', status: 'done' },
|
||||
{ name: '解析', description: 'Tika 抽取文本并保存快照', status: 'done' },
|
||||
{ name: '切片', description: '固定长度 800 / overlap 120', status: 'running' },
|
||||
{ name: '向量化', description: 'Qwen3 Embedding 1024 维', status: 'idle' },
|
||||
{ name: '可检索', description: '等待索引任务完成', status: 'idle' },
|
||||
];
|
||||
|
||||
export const workflowNodes: WorkflowNode[] = [
|
||||
{ id: 'start', type: 'START', label: 'Start', description: '接收用户问题', x: 4, y: 42 },
|
||||
{ id: 'retrieve', type: 'KNOWLEDGE_RETRIEVAL', label: 'Knowledge Retrieval', description: 'TopK=6 / score>0.72', x: 25, y: 18 },
|
||||
{ id: 'llm', type: 'LLM', label: 'LLM', description: 'RAG_ANSWER 路由', x: 47, y: 42 },
|
||||
{ id: 'mcp', type: 'MCP_TOOL', label: 'MCP Tool', description: 'jira.search / docs.lookup', x: 47, y: 70 },
|
||||
{ id: 'skill', type: 'SKILL', label: 'Skill', description: '答案审校与引用整理', x: 69, y: 42 },
|
||||
{ id: 'answer', type: 'ANSWER', label: 'Answer', description: '返回回答与引用', x: 88, y: 42 },
|
||||
];
|
||||
|
||||
export const workflowEdges: WorkflowEdge[] = [
|
||||
{ from: 'start', to: 'retrieve' },
|
||||
{ from: 'retrieve', to: 'llm' },
|
||||
{ from: 'llm', to: 'skill' },
|
||||
{ from: 'mcp', to: 'skill' },
|
||||
{ from: 'skill', to: 'answer' },
|
||||
];
|
||||
|
||||
export const traceSteps: TraceStep[] = [
|
||||
{ node: 'Start', status: '完成', duration: '4ms', output: '用户问题已标准化' },
|
||||
{ node: 'Knowledge Retrieval', status: '完成', duration: '218ms', output: '召回 6 个切片' },
|
||||
{ node: 'LLM', status: '完成', duration: '1.12s', output: '生成 612 tokens' },
|
||||
{ node: 'Skill', status: '完成', duration: '88ms', output: '引用格式已校验' },
|
||||
];
|
||||
|
||||
export const chatMessages = [
|
||||
{ role: 'user', content: '如果客户要求私有化部署,售前方案里必须说明哪些内容?' },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '建议说明部署拓扑、模型服务商、知识库索引策略、权限边界、日志留存周期和故障恢复方式。当前回答引用了 3 个知识切片。',
|
||||
},
|
||||
];
|
||||
|
||||
export const citations = [
|
||||
{ title: '售前方案模板.pdf', score: '0.91', text: '私有化部署章节应覆盖基础设施、网络、安全与运维边界。' },
|
||||
{ title: '项目实施手册.docx', score: '0.87', text: '交付计划需包含数据导入、索引重建与验收标准。' },
|
||||
{ title: '服务条款更新.md', score: '0.82', text: '客户数据默认不出域,模型调用日志需保留审计字段。' },
|
||||
];
|
||||
|
||||
export const mcpCapabilities = [
|
||||
{ name: 'jira.search', type: 'tool', status: '已启用', description: '按项目、状态、负责人检索任务' },
|
||||
{ name: 'docs.lookup', type: 'resource', status: '已启用', description: '读取外部文档中心条目' },
|
||||
{ name: 'deploy.trigger', type: 'tool', status: '待授权', description: '触发测试环境部署流水线' },
|
||||
];
|
||||
|
||||
export const skillVersions = [
|
||||
{ version: 'v4', status: 'Draft', updatedAt: '刚刚', note: '增加引用一致性检查' },
|
||||
{ version: 'v3', status: 'Published', updatedAt: '昨天', note: '生产环境当前版本' },
|
||||
{ version: 'v2', status: 'Archived', updatedAt: '5天前', note: '旧版回答润色策略' },
|
||||
];
|
||||
|
||||
export const modelRoutes = [
|
||||
{ task: 'RAG_ANSWER', primary: 'qwen-plus', fallback: 'deepseek-v3', latency: '1800ms', status: '启用' },
|
||||
{ task: 'RAG_EMBEDDING', primary: 'Qwen3-Embedding', fallback: '无', latency: '900ms', status: '启用' },
|
||||
{ task: 'AGENT_PLAN', primary: 'gpt-4.1', fallback: 'qwen-max', latency: '3200ms', status: '草稿' },
|
||||
];
|
||||
@@ -1,44 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Box,
|
||||
ChatDotRound,
|
||||
Collection,
|
||||
Connection,
|
||||
Cpu,
|
||||
DataBoard,
|
||||
Document,
|
||||
Files,
|
||||
Grid,
|
||||
MagicStick,
|
||||
Monitor,
|
||||
Operation,
|
||||
UploadFilled,
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/dashboard', label: '工作台', icon: DataBoard },
|
||||
{ path: '/system/enums', label: '系统枚举', icon: Grid },
|
||||
{ path: '/system/attachments', label: '附件管理', icon: Files },
|
||||
{ path: '/rag/stores', label: '知识库', icon: Collection },
|
||||
{ path: '/rag/documents', label: '知识文档', icon: Document },
|
||||
const studioMenuItems = [
|
||||
{ path: '/studio', label: '工作台', icon: DataBoard },
|
||||
{ path: '/knowledge', label: '知识资产', icon: Collection },
|
||||
{ path: '/knowledge/ingestion', label: '文件解析', icon: UploadFilled },
|
||||
{ path: '/workflows', label: 'Workflow', icon: Connection },
|
||||
{ path: '/agents', label: 'Agent 对话', icon: ChatDotRound },
|
||||
{ path: '/mcp', label: 'MCP', icon: Operation },
|
||||
{ path: '/skills', label: 'Skills', icon: MagicStick },
|
||||
{ path: '/models', label: '模型', icon: Cpu },
|
||||
{ path: '/observability', label: '观测', icon: Monitor },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="admin-layout">
|
||||
<el-aside class="admin-sidebar" width="232px">
|
||||
<el-container class="admin-layout studio-shell">
|
||||
<el-aside class="admin-sidebar studio-sidebar" width="248px">
|
||||
<div class="brand">
|
||||
<el-icon :size="24">
|
||||
<Box />
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span>Common Agent</span>
|
||||
<div>
|
||||
<span>Common Agent Studio</span>
|
||||
<small>AI Agent Development Platform</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-menu class="side-menu" :default-active="$route.path" router>
|
||||
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
|
||||
<el-menu class="side-menu studio-menu" :default-active="$route.path" router>
|
||||
<el-menu-item v-for="item in studioMenuItems" :key="item.path" :index="item.path">
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
<div class="sidebar-status">
|
||||
<span>Dev 环境</span>
|
||||
<strong>Draft / Published</strong>
|
||||
</div>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-main class="admin-main">
|
||||
<el-container class="studio-content-shell">
|
||||
<header class="studio-topbar">
|
||||
<div class="project-switcher">
|
||||
<span>项目</span>
|
||||
<strong>通用 AI Agent 开发平台</strong>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<span class="run-status">运行状态正常</span>
|
||||
<el-button>版本快照</el-button>
|
||||
<el-button type="primary">发布</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<el-main class="admin-main studio-main">
|
||||
<RouterView />
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
@@ -6,16 +6,16 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import AdminLayout from '../AdminLayout.vue';
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ meta: { title: '系统枚举' } }),
|
||||
useRoute: () => ({ meta: { title: '工作台' } }),
|
||||
}));
|
||||
|
||||
describe('AdminLayout', () => {
|
||||
it('does not render a duplicate page header above the main page content', () => {
|
||||
it('renders the Studio shell navigation without legacy admin groups', () => {
|
||||
const wrapper = mount(AdminLayout, {
|
||||
global: {
|
||||
plugins: [createPinia(), ElementPlus],
|
||||
mocks: {
|
||||
$route: { path: '/system/enums' },
|
||||
$route: { path: '/studio' },
|
||||
},
|
||||
stubs: {
|
||||
RouterView: { template: '<main data-test="router-view" />' },
|
||||
@@ -25,5 +25,12 @@ describe('AdminLayout', () => {
|
||||
|
||||
expect(wrapper.find('.admin-header').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-test="router-view"]').exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('Common Agent Studio');
|
||||
expect(wrapper.text()).toContain('知识资产');
|
||||
expect(wrapper.text()).toContain('Workflow');
|
||||
expect(wrapper.text()).toContain('观测');
|
||||
expect(wrapper.text()).not.toContain('系统管理');
|
||||
expect(wrapper.text()).not.toContain('RAG');
|
||||
expect(wrapper.text()).not.toContain('Agent管理');
|
||||
});
|
||||
});
|
||||
|
||||
270
frontend/src/pages/agent/AgentDebugPage.vue
Normal file
270
frontend/src/pages/agent/AgentDebugPage.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { chatWithAgent, listAgents, type AgentDefinition, type AgentMessage, type AgentReferenceChunk } from '@/api/agent';
|
||||
|
||||
interface ChatBubble {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
references?: AgentReferenceChunk[];
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const sending = ref(false);
|
||||
const agents = ref<AgentDefinition[]>([]);
|
||||
const selectedAgentId = ref('');
|
||||
const inputText = ref('');
|
||||
const messages = ref<ChatBubble[]>([]);
|
||||
const ragEnabled = ref(true);
|
||||
|
||||
const selectedAgent = computed(() => agents.value.find((agent) => agent.id === selectedAgentId.value));
|
||||
|
||||
async function loadAgents() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await listAgents();
|
||||
agents.value = (response.data ?? []).filter((item) => item.status === 'ENABLED');
|
||||
if (!selectedAgentId.value && agents.value.length > 0) {
|
||||
const firstAgent = agents.value[0];
|
||||
selectedAgentId.value = firstAgent && firstAgent.id ? firstAgent.id : '';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestMessages(nextUserText: string): AgentMessage[] {
|
||||
const historyMessages: AgentMessage[] = messages.value.map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
}));
|
||||
historyMessages.push({ role: 'user', content: nextUserText });
|
||||
return historyMessages;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const trimmed = inputText.value.trim();
|
||||
if (!selectedAgentId.value) {
|
||||
ElMessage.warning('请先选择Agent');
|
||||
return;
|
||||
}
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
const requestMessages = buildRequestMessages(trimmed);
|
||||
const userBubble: ChatBubble = {
|
||||
id: `${Date.now()}_u`,
|
||||
role: 'user',
|
||||
content: trimmed,
|
||||
};
|
||||
messages.value.push(userBubble);
|
||||
inputText.value = '';
|
||||
|
||||
sending.value = true;
|
||||
try {
|
||||
const response = await chatWithAgent(selectedAgentId.value, {
|
||||
messages: requestMessages,
|
||||
ragEnabled: ragEnabled.value,
|
||||
});
|
||||
const result = response.data;
|
||||
messages.value.push({
|
||||
id: `${Date.now()}_a`,
|
||||
role: 'assistant',
|
||||
content: result?.answer ?? '',
|
||||
references: result?.references ?? [],
|
||||
requestId: result?.modelRequestId,
|
||||
});
|
||||
} finally {
|
||||
sending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
messages.value = [];
|
||||
}
|
||||
|
||||
onMounted(loadAgents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel agent-debug">
|
||||
<div class="page-panel__header">
|
||||
<h2>Agent 调试</h2>
|
||||
<span>Chat Debugger</span>
|
||||
</div>
|
||||
|
||||
<div class="debug-toolbar">
|
||||
<el-select v-model="selectedAgentId" class="debug-toolbar__agent" :loading="loading" placeholder="请选择Agent">
|
||||
<el-option
|
||||
v-for="item in agents"
|
||||
:key="item.id"
|
||||
:label="`${item.agentName}(${item.agentCode})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-switch v-model="ragEnabled" active-text="RAG对话" inactive-text="普通对话" />
|
||||
<el-button @click="loadAgents">刷新Agent</el-button>
|
||||
<el-button @click="clearChat">清空会话</el-button>
|
||||
</div>
|
||||
|
||||
<div class="debug-chat">
|
||||
<div v-for="bubble in messages" :key="bubble.id" class="chat-row" :class="`chat-row--${bubble.role}`">
|
||||
<div class="chat-bubble">
|
||||
<div class="chat-bubble__role">{{ bubble.role === 'user' ? '用户' : '助手' }}</div>
|
||||
<div class="chat-bubble__content">{{ bubble.content }}</div>
|
||||
<template v-if="bubble.role === 'assistant'">
|
||||
<div v-if="bubble.references?.length" class="chat-bubble__refs">
|
||||
<div class="chat-bubble__refs-title">引用切片</div>
|
||||
<ul>
|
||||
<li v-for="reference in bubble.references" :key="reference.chunkId">
|
||||
<span class="ref-meta">#{{ reference.chunkId }} · 相似度 {{ (reference.score ?? 0).toFixed(4) }}</span>
|
||||
<span>{{ reference.chunkContent }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="bubble.requestId" class="chat-bubble__request-id">requestId: {{ bubble.requestId }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="messages.length === 0" class="chat-empty">
|
||||
选择Agent后输入问题,发起对话调试。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-input">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="none"
|
||||
:disabled="sending || !selectedAgent"
|
||||
placeholder="输入问题,回车发送(Shift+Enter换行)"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
/>
|
||||
<el-button type="primary" :loading="sending" :disabled="!selectedAgent" @click="sendMessage">发送</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.debug-toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 16px 22px 12px;
|
||||
}
|
||||
|
||||
.debug-toolbar__agent {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.debug-chat {
|
||||
flex: 1;
|
||||
min-height: 420px;
|
||||
max-height: 58vh;
|
||||
padding: 12px 22px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--app-border-soft);
|
||||
border-bottom: 1px solid var(--app-border-soft);
|
||||
background: #fafcff;
|
||||
}
|
||||
|
||||
.chat-row {
|
||||
display: flex;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.chat-row--user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat-row--assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
width: min(80%, 860px);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--app-border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.chat-row--user .chat-bubble {
|
||||
background: #eef5ff;
|
||||
border-color: #d3e5ff;
|
||||
}
|
||||
|
||||
.chat-bubble__role {
|
||||
margin-bottom: 6px;
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-bubble__content {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.chat-bubble__refs {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.chat-bubble__refs-title {
|
||||
margin-bottom: 8px;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-bubble__refs ul {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.chat-bubble__refs li {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ref-meta {
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-bubble__request-id {
|
||||
margin-top: 8px;
|
||||
color: var(--app-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-empty {
|
||||
color: var(--app-text-muted);
|
||||
text-align: center;
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
.debug-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 22px 18px;
|
||||
}
|
||||
|
||||
.debug-input .el-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
</style>
|
||||
195
frontend/src/pages/agent/AgentManagePage.vue
Normal file
195
frontend/src/pages/agent/AgentManagePage.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { deleteAgent, queryAgents, saveAgent, type AgentDefinition } from '@/api/agent';
|
||||
import { listRagStores, type RagStore } from '@/api/ragStores';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const agents = ref<AgentDefinition[]>([]);
|
||||
const stores = ref<RagStore[]>([]);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 'ENABLED' },
|
||||
{ label: '禁用', value: 'DISABLED' },
|
||||
];
|
||||
|
||||
const editForm = reactive<AgentDefinition>({
|
||||
agentCode: '',
|
||||
agentName: '',
|
||||
systemPrompt: '',
|
||||
storeId: '',
|
||||
status: 'ENABLED',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (editForm.id ? '编辑Agent' : '新增Agent'));
|
||||
|
||||
function resetForm(row?: AgentDefinition) {
|
||||
editForm.id = row?.id;
|
||||
editForm.agentCode = row?.agentCode ?? '';
|
||||
editForm.agentName = row?.agentName ?? '';
|
||||
editForm.systemPrompt = row?.systemPrompt ?? '';
|
||||
editForm.storeId = row?.storeId ?? stores.value[0]?.id ?? '';
|
||||
editForm.status = row?.status ?? 'ENABLED';
|
||||
editForm.remark = row?.remark ?? '';
|
||||
}
|
||||
|
||||
async function loadStores() {
|
||||
const response = await listRagStores();
|
||||
stores.value = response.data ?? [];
|
||||
}
|
||||
|
||||
async function loadAgents() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryAgents();
|
||||
agents.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: AgentDefinition) {
|
||||
resetForm(row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitAgent() {
|
||||
if (!editForm.agentCode || !editForm.agentName || !editForm.storeId) {
|
||||
ElMessage.warning('请填写Agent编码、名称和绑定知识库');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveAgent({ ...editForm });
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
await loadAgents();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAgent(row: AgentDefinition) {
|
||||
if (!row.id) {
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除Agent「${row.agentName || row.agentCode}」?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
await deleteAgent(row.id);
|
||||
ElMessage.success('已删除');
|
||||
await loadAgents();
|
||||
}
|
||||
|
||||
function storeLabel(storeId?: string) {
|
||||
const store = stores.value.find((item) => item.id === storeId);
|
||||
return store?.storeName ?? store?.storeCode ?? storeId ?? '-';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadStores();
|
||||
resetForm();
|
||||
await loadAgents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>Agent 管理</h2>
|
||||
<span>Agent Config</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="toolbar__actions">
|
||||
<el-button :icon="RefreshRight" @click="loadAgents">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增Agent</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="agents" row-key="id">
|
||||
<el-table-column prop="agentCode" label="Agent编码" min-width="140" />
|
||||
<el-table-column prop="agentName" label="Agent名称" min-width="140" />
|
||||
<el-table-column label="知识库" min-width="140">
|
||||
<template #default="{ row }">{{ storeLabel(row.storeId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ENABLED' ? 'success' : 'info'">
|
||||
{{ row.status === 'ENABLED' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="systemPrompt" label="系统提示词" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="removeAgent(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="720px">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Agent编码" required>
|
||||
<el-input v-model="editForm.agentCode" placeholder="如 AGENT_RAG_HELPER" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Agent名称" required>
|
||||
<el-input v-model="editForm.agentName" placeholder="如 知识问答助手" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定知识库" required>
|
||||
<el-select v-model="editForm.storeId">
|
||||
<el-option
|
||||
v-for="store in stores"
|
||||
:key="store.id"
|
||||
:label="`${store.storeName}(${store.storeCode})`"
|
||||
:value="store.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统提示词">
|
||||
<el-input v-model="editForm.systemPrompt" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="editForm.status">
|
||||
<el-radio-button v-for="item in statusOptions" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitAgent">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 22px;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<main class="not-found">
|
||||
<h1>404</h1>
|
||||
<RouterLink to="/dashboard">返回工作台</RouterLink>
|
||||
<RouterLink to="/studio">返回工作台</RouterLink>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>工作台</h2>
|
||||
<span>Console</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,638 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Operation, Search, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import {
|
||||
deleteRagDocument,
|
||||
getRagDocumentById,
|
||||
parseRagDocuments,
|
||||
queryRagDocuments,
|
||||
saveRagDocument,
|
||||
type RagChunkStrategy,
|
||||
type RagDocument,
|
||||
} from '@/api/ragDocuments';
|
||||
import { queryRagStores, type RagStore } from '@/api/ragStores';
|
||||
import RagDocumentBatchUploadDialog from '@/components/rag/RagDocumentBatchUploadDialog.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const parseSubmitting = ref(false);
|
||||
const route = useRoute();
|
||||
|
||||
const storeOptions = ref<RagStore[]>([]);
|
||||
const docRows = ref<RagDocument[]>([]);
|
||||
|
||||
const queryForm = reactive({
|
||||
storeId: '',
|
||||
parseStatus: '',
|
||||
indexStatus: '',
|
||||
enabled: '' as string,
|
||||
keyword: '',
|
||||
});
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const uploadDialogVisible = ref(false);
|
||||
const parseDialogVisible = ref(false);
|
||||
const selectedDocuments = ref<RagDocument[]>([]);
|
||||
|
||||
const editForm = reactive({
|
||||
id: '',
|
||||
storeId: '',
|
||||
attachmentId: '',
|
||||
documentTitle: '',
|
||||
documentSummary: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const parseForm = reactive({
|
||||
documentIds: [] as string[],
|
||||
chunkStrategy: 'FIXED_LENGTH' as RagChunkStrategy,
|
||||
chunkSize: 800,
|
||||
chunkOverlap: 120,
|
||||
delimiter: '。',
|
||||
});
|
||||
|
||||
const chunkStrategyOptions: Array<{ label: string; value: RagChunkStrategy; description: string }> = [
|
||||
{ label: '固定长度切片', value: 'FIXED_LENGTH', description: '按指定长度和重叠长度切分通用文本' },
|
||||
{ label: '按段落切片', value: 'PARAGRAPH', description: '按空行、自然段落边界切分' },
|
||||
{ label: '按标题层级切片', value: 'HEADING', description: '按标题和章节层级组织内容' },
|
||||
{ label: '按表格行切片', value: 'TABLE_ROW', description: '适合 Excel 表格和结构化明细数据' },
|
||||
{ label: '按分隔符切片', value: 'DELIMITER', description: '按句号、换行符或自定义分隔符切分' },
|
||||
{ label: '语义切片', value: 'SEMANTIC', description: '后续结合语义边界或模型能力切分' },
|
||||
];
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const kw = queryForm.keyword.trim().toLowerCase();
|
||||
return docRows.value.filter(
|
||||
(row) => {
|
||||
const matchStore = !queryForm.storeId || row.storeId === queryForm.storeId;
|
||||
const matchParseStatus = !queryForm.parseStatus || row.parseStatus === queryForm.parseStatus;
|
||||
const matchIndexStatus = !queryForm.indexStatus || row.indexStatus === queryForm.indexStatus;
|
||||
const matchEnabled = !queryForm.enabled || String(row.enabled ?? false) === queryForm.enabled;
|
||||
const matchKeyword =
|
||||
!kw ||
|
||||
(row.documentTitle && row.documentTitle.toLowerCase().includes(kw)) ||
|
||||
(row.documentSummary && row.documentSummary.toLowerCase().includes(kw)) ||
|
||||
(row.remark && row.remark.toLowerCase().includes(kw));
|
||||
return matchStore && matchParseStatus && matchIndexStatus && matchEnabled && matchKeyword;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
async function loadStores() {
|
||||
try {
|
||||
const response = await queryRagStores();
|
||||
storeOptions.value = response.data ?? [];
|
||||
} catch {
|
||||
storeOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryRagDocuments({
|
||||
...(queryForm.storeId ? { storeId: queryForm.storeId } : {}),
|
||||
...(queryForm.parseStatus ? { parseStatus: queryForm.parseStatus } : {}),
|
||||
...(queryForm.indexStatus ? { indexStatus: queryForm.indexStatus } : {}),
|
||||
...(queryForm.enabled ? { enabled: queryForm.enabled === 'true' } : {}),
|
||||
});
|
||||
docRows.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadDocs();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryForm.storeId = '';
|
||||
queryForm.parseStatus = '';
|
||||
queryForm.indexStatus = '';
|
||||
queryForm.enabled = '';
|
||||
queryForm.keyword = '';
|
||||
loadDocs();
|
||||
}
|
||||
|
||||
function openUploadDialog() {
|
||||
if (storeOptions.value.length === 0) {
|
||||
ElMessage.warning('请先创建知识库');
|
||||
return;
|
||||
}
|
||||
uploadDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: RagDocument[]) {
|
||||
selectedDocuments.value = rows;
|
||||
}
|
||||
|
||||
function openParseDialog(rows: RagDocument[]) {
|
||||
const ids = rows.map((row) => String(row.id ?? '')).filter(Boolean);
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning('请选择需要解析的文档');
|
||||
return;
|
||||
}
|
||||
parseForm.documentIds = ids;
|
||||
parseForm.chunkStrategy = 'FIXED_LENGTH';
|
||||
parseForm.chunkSize = 800;
|
||||
parseForm.chunkOverlap = 120;
|
||||
parseForm.delimiter = '。';
|
||||
parseDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openBatchParseDialog() {
|
||||
openParseDialog(selectedDocuments.value);
|
||||
}
|
||||
|
||||
async function openEditDialog(row: RagDocument) {
|
||||
const detail = row.id ? (await getRagDocumentById(String(row.id))).data : row;
|
||||
|
||||
editForm.id = String(detail.id ?? '');
|
||||
editForm.storeId = detail.storeId;
|
||||
editForm.attachmentId = detail.attachmentId ?? '';
|
||||
editForm.documentTitle = detail.documentTitle ?? '';
|
||||
editForm.documentSummary = detail.documentSummary ?? '';
|
||||
editForm.enabled = detail.enabled ?? true;
|
||||
editForm.remark = detail.remark ?? '';
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editForm.id || !editForm.storeId || !editForm.documentTitle) {
|
||||
ElMessage.warning('请填写文档标题');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveRagDocument({
|
||||
id: editForm.id,
|
||||
storeId: editForm.storeId,
|
||||
attachmentId: editForm.attachmentId || undefined,
|
||||
documentTitle: editForm.documentTitle,
|
||||
documentSummary: editForm.documentSummary || undefined,
|
||||
enabled: editForm.enabled,
|
||||
remark: editForm.remark || undefined,
|
||||
});
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success('文档信息已更新');
|
||||
await loadDocs();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDoc(row: RagDocument) {
|
||||
if (!row.id) return;
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除文档「${row.documentTitle || '未命名'}」?`,
|
||||
'删除确认',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||
);
|
||||
|
||||
await deleteRagDocument(String(row.id));
|
||||
ElMessage.success('文档已删除');
|
||||
await loadDocs();
|
||||
}
|
||||
|
||||
function toggleEnabled(row: RagDocument) {
|
||||
if (!row.id) return;
|
||||
const newEnabled = !row.enabled;
|
||||
saveRagDocument({
|
||||
id: String(row.id),
|
||||
storeId: row.storeId,
|
||||
documentTitle: row.documentTitle ?? '',
|
||||
enabled: newEnabled,
|
||||
}).then(() => {
|
||||
row.enabled = newEnabled;
|
||||
ElMessage.success(`已${newEnabled ? '启用' : '停用'}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function submitParse() {
|
||||
if (parseForm.documentIds.length === 0) {
|
||||
ElMessage.warning('请选择需要解析的文档');
|
||||
return;
|
||||
}
|
||||
parseSubmitting.value = true;
|
||||
try {
|
||||
await parseRagDocuments({
|
||||
documentIds: parseForm.documentIds,
|
||||
chunkStrategy: parseForm.chunkStrategy,
|
||||
chunkSize: parseForm.chunkSize,
|
||||
chunkOverlap: parseForm.chunkOverlap,
|
||||
delimiter: parseForm.delimiter,
|
||||
});
|
||||
parseDialogVisible.value = false;
|
||||
ElMessage.success('解析任务已提交');
|
||||
await loadDocs();
|
||||
} finally {
|
||||
parseSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getStoreName(storeId: string) {
|
||||
const store = storeOptions.value.find((s) => String(s.id) === storeId);
|
||||
return store ? store.storeName : '-';
|
||||
}
|
||||
|
||||
function getStatusLabel(status?: string | null) {
|
||||
const map: Record<string, string> = {
|
||||
UPLOADED: '已上传',
|
||||
PARSING: '解析中',
|
||||
PARSED: '已解析',
|
||||
FAILED: '解析失败',
|
||||
PENDING: '待索引',
|
||||
INDEXING: '索引中',
|
||||
INDEXED: '已索引',
|
||||
};
|
||||
return status ? (map[status] ?? status) : '-';
|
||||
}
|
||||
|
||||
function getParseStatusType(status?: string | null) {
|
||||
const success = ['PARSED'];
|
||||
const warning = ['UPLOADED', 'PARSING'];
|
||||
const danger = ['FAILED'];
|
||||
if (!status) return 'info';
|
||||
if (success.includes(status)) return 'success';
|
||||
if (warning.includes(status)) return 'warning';
|
||||
if (danger.includes(status)) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function getIndexStatusType(status?: string | null) {
|
||||
const success = ['INDEXED'];
|
||||
const warning = ['PENDING', 'INDEXING'];
|
||||
const danger = ['FAILED'];
|
||||
if (!status) return 'info';
|
||||
if (success.includes(status)) return 'success';
|
||||
if (warning.includes(status)) return 'warning';
|
||||
if (danger.includes(status)) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const routeStoreId = route.query.storeId;
|
||||
if (typeof routeStoreId === 'string') {
|
||||
queryForm.storeId = routeStoreId;
|
||||
}
|
||||
loadStores();
|
||||
loadDocs();
|
||||
});
|
||||
import RagDocumentsPage from '@/pages/rag/documents/RagDocumentsPage.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel rag-doc-page">
|
||||
<div class="page-panel__header">
|
||||
<h2>知识文档</h2>
|
||||
<span>Documents</span>
|
||||
</div>
|
||||
|
||||
<div class="document-query-bar" data-test="document-query-bar">
|
||||
<el-form class="document-query-form" data-test="document-query-form" inline>
|
||||
<el-form-item label="知识库">
|
||||
<el-select
|
||||
v-model="queryForm.storeId"
|
||||
data-test="doc-store-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option
|
||||
v-for="store in storeOptions"
|
||||
:key="String(store.id)"
|
||||
:label="store.storeName"
|
||||
:value="String(store.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="解析状态">
|
||||
<el-select
|
||||
v-model="queryForm.parseStatus"
|
||||
data-test="doc-parse-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option label="已上传" value="UPLOADED" />
|
||||
<el-option label="解析中" value="PARSING" />
|
||||
<el-option label="已解析" value="PARSED" />
|
||||
<el-option label="解析失败" value="FAILED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="索引状态">
|
||||
<el-select
|
||||
v-model="queryForm.indexStatus"
|
||||
data-test="doc-index-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option label="待索引" value="PENDING" />
|
||||
<el-option label="索引中" value="INDEXING" />
|
||||
<el-option label="已索引" value="INDEXED" />
|
||||
<el-option label="索引失败" value="FAILED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-select
|
||||
v-model="queryForm.enabled"
|
||||
data-test="doc-enabled-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option label="启用" value="true" />
|
||||
<el-option label="停用" value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="queryForm.keyword"
|
||||
data-test="doc-keyword-input"
|
||||
placeholder="搜索标题/摘要/备注"
|
||||
clearable
|
||||
class="query-control query-control--keyword"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="document-query-form__actions">
|
||||
<el-button data-test="doc-search" type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button
|
||||
data-test="open-batch-parse"
|
||||
:icon="Operation"
|
||||
:disabled="selectedDocuments.length === 0"
|
||||
@click="openBatchParseDialog"
|
||||
>
|
||||
批量解析
|
||||
</el-button>
|
||||
<el-button
|
||||
data-test="open-doc-upload"
|
||||
type="primary"
|
||||
:icon="UploadFilled"
|
||||
@click="openUploadDialog"
|
||||
>
|
||||
批量上传
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredRows"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column type="index" label="编号" width="70" align="center" />
|
||||
<el-table-column prop="documentTitle" label="文档标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="所属知识库" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getStoreName(row.storeId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解析状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getParseStatusType(row.parseStatus)" size="small">
|
||||
{{ getStatusLabel(row.parseStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="索引状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getIndexStatusType(row.indexStatus)" size="small">
|
||||
{{ getStatusLabel(row.indexStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.enabled ?? false"
|
||||
size="small"
|
||||
@change="toggleEnabled(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="documentSummary" label="摘要" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="210" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button :data-test="`doc-parse-${row.id}`" link type="primary" @click="openParseDialog([row])">解析</el-button>
|
||||
<el-button :data-test="`doc-edit-${row.id}`" link type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="removeDoc(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty v-if="!loading && filteredRows.length === 0" description="暂无知识文档" />
|
||||
|
||||
<RagDocumentBatchUploadDialog
|
||||
v-model="uploadDialogVisible"
|
||||
:stores="storeOptions"
|
||||
:locked-store-id="queryForm.storeId || null"
|
||||
@uploaded="loadDocs"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="parseDialogVisible"
|
||||
data-test="document-parse-dialog"
|
||||
title="解析配置"
|
||||
width="620px"
|
||||
>
|
||||
<el-form :model="parseForm" label-width="112px">
|
||||
<el-form-item label="文档数量">
|
||||
<el-tag>{{ parseForm.documentIds.length }} 个文档</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="切片方式">
|
||||
<el-radio-group v-model="parseForm.chunkStrategy" class="chunk-strategy-group">
|
||||
<el-radio
|
||||
v-for="strategy in chunkStrategyOptions"
|
||||
:key="strategy.value"
|
||||
:value="strategy.value"
|
||||
class="chunk-strategy-option"
|
||||
>
|
||||
<span class="chunk-strategy-option__label">{{ strategy.label }}</span>
|
||||
<small>{{ strategy.description }}</small>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parseForm.chunkStrategy === 'FIXED_LENGTH'" label="切片长度">
|
||||
<el-input-number v-model="parseForm.chunkSize" :min="100" :max="4000" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parseForm.chunkStrategy === 'FIXED_LENGTH'" label="重叠长度">
|
||||
<el-input-number v-model="parseForm.chunkOverlap" :min="0" :max="1000" :step="20" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="parseForm.chunkStrategy === 'DELIMITER'" label="分隔符">
|
||||
<el-input v-model="parseForm.delimiter" maxlength="20" placeholder="如 。、换行符或自定义符号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="parseDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
data-test="document-parse-submit"
|
||||
type="primary"
|
||||
:loading="parseSubmitting"
|
||||
@click="submitParse"
|
||||
>
|
||||
开始解析
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑文档" width="560px">
|
||||
<el-form :model="editForm" label-width="96px">
|
||||
<el-form-item label="文档标题" required>
|
||||
<el-input v-model="editForm.documentTitle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文档摘要">
|
||||
<el-input v-model="editForm.documentSummary" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="editForm.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
<RagDocumentsPage />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-doc-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.document-query-bar {
|
||||
padding: 18px 28px 17px;
|
||||
border-bottom: 1px solid #e8edf5;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.document-query-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-form-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-form-item__label) {
|
||||
height: 38px;
|
||||
padding-right: 8px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-input__wrapper),
|
||||
.document-query-form :deep(.el-select__wrapper) {
|
||||
min-height: 38px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px #d8dee9 inset;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-input__wrapper:hover),
|
||||
.document-query-form :deep(.el-select__wrapper:hover) {
|
||||
box-shadow: 0 0 0 1px #b9c6d8 inset;
|
||||
}
|
||||
|
||||
.query-control--select {
|
||||
width: 168px;
|
||||
}
|
||||
|
||||
.query-control--keyword {
|
||||
width: 225px;
|
||||
}
|
||||
|
||||
.document-query-form__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.document-query-form__actions :deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chunk-strategy-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chunk-strategy-option {
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chunk-strategy-option :deep(.el-radio__label) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chunk-strategy-option__label {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chunk-strategy-option small {
|
||||
color: #7a8599;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.document-query-bar {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.document-query-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.query-control--select,
|
||||
.query-control--keyword {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.document-query-form__actions {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.document-query-form__actions :deep(.el-form-item__content) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chunk-strategy-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,14 @@ import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } fr
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||
import {
|
||||
getRagStoreModelConfig,
|
||||
queryModelConfigs,
|
||||
rebuildRagStoreIndex,
|
||||
saveRagStoreModelConfig,
|
||||
type ModelConfig,
|
||||
} from '@/api/modelProvider';
|
||||
|
||||
import {
|
||||
deleteRagStore,
|
||||
@@ -28,6 +36,20 @@ const activeStoreId = ref<string | null>(null);
|
||||
const activeStore = ref<RagStore | null>(null);
|
||||
const pageOverview = ref<RagStoreOverview | null>(null);
|
||||
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
|
||||
const embeddingModels = ref<ModelConfig[]>([]);
|
||||
const ragConfigLoading = ref(false);
|
||||
const ragConfigSaving = ref(false);
|
||||
const ragConfig = reactive({
|
||||
id: '',
|
||||
embeddingModelId: '',
|
||||
embeddingDimension: 1024,
|
||||
chunkStrategy: null as number | null,
|
||||
chunkSize: null as number | null,
|
||||
chunkOverlap: null as number | null,
|
||||
delimiter: '',
|
||||
remark: '',
|
||||
});
|
||||
const chunkStrategyOptions = ref<EnumOption[]>([]);
|
||||
|
||||
const queryForm = reactive({
|
||||
storeName: '',
|
||||
@@ -91,6 +113,8 @@ async function loadStores(preferredStoreId?: string | null) {
|
||||
activeStoreId.value = null;
|
||||
activeStore.value = null;
|
||||
activeStoreDocumentOverview.value = null;
|
||||
ragConfig.id = '';
|
||||
ragConfig.embeddingModelId = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,6 +147,7 @@ async function selectStore(storeId: string) {
|
||||
]);
|
||||
activeStore.value = storeResponse.data ?? null;
|
||||
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
|
||||
await loadRagModelConfig(storeId);
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
@@ -223,10 +248,6 @@ async function removeStore() {
|
||||
await loadStores();
|
||||
}
|
||||
|
||||
function showFutureMessage(actionName: string) {
|
||||
ElMessage.info(`${actionName} 会在下一批接口里补齐`);
|
||||
}
|
||||
|
||||
function openBatchUploadDialog() {
|
||||
if (!activeStore.value?.id) {
|
||||
ElMessage.warning('请选择知识库');
|
||||
@@ -252,13 +273,85 @@ async function refreshAfterUpload() {
|
||||
]);
|
||||
}
|
||||
|
||||
async function loadRagModelConfig(storeId: string) {
|
||||
ragConfigLoading.value = true;
|
||||
try {
|
||||
const response = await getRagStoreModelConfig(storeId);
|
||||
const data = response.data;
|
||||
ragConfig.id = data?.id ?? '';
|
||||
ragConfig.embeddingModelId = data?.embeddingModelId ?? '';
|
||||
ragConfig.embeddingDimension = data?.embeddingDimension ?? 1024;
|
||||
ragConfig.chunkStrategy = data?.chunkStrategy ?? null;
|
||||
ragConfig.chunkSize = data?.chunkSize ?? null;
|
||||
ragConfig.chunkOverlap = data?.chunkOverlap ?? null;
|
||||
ragConfig.delimiter = data?.delimiter ?? '';
|
||||
ragConfig.remark = data?.remark ?? '';
|
||||
} finally {
|
||||
ragConfigLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStoreModelConfig() {
|
||||
if (!activeStoreId.value) {
|
||||
return;
|
||||
}
|
||||
if (!ragConfig.embeddingModelId) {
|
||||
ElMessage.warning('请选择 Embedding 模型');
|
||||
return;
|
||||
}
|
||||
ragConfigSaving.value = true;
|
||||
try {
|
||||
await saveRagStoreModelConfig({
|
||||
id: ragConfig.id || undefined,
|
||||
storeId: activeStoreId.value,
|
||||
embeddingModelId: ragConfig.embeddingModelId,
|
||||
embeddingDimension: ragConfig.embeddingDimension,
|
||||
chunkStrategy: ragConfig.chunkStrategy ?? undefined,
|
||||
chunkSize: ragConfig.chunkSize ?? undefined,
|
||||
chunkOverlap: ragConfig.chunkOverlap ?? undefined,
|
||||
delimiter: ragConfig.delimiter || undefined,
|
||||
remark: ragConfig.remark || undefined,
|
||||
});
|
||||
ElMessage.success('知识库模型配置已保存');
|
||||
await loadRagModelConfig(activeStoreId.value);
|
||||
} finally {
|
||||
ragConfigSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerRebuildIndex() {
|
||||
if (!activeStoreId.value) {
|
||||
return;
|
||||
}
|
||||
await rebuildRagStoreIndex(activeStoreId.value);
|
||||
ElMessage.success('已触发重建索引');
|
||||
}
|
||||
|
||||
function syncEmbeddingDimensionFromModel() {
|
||||
const model = embeddingModels.value.find((item) => item.id === ragConfig.embeddingModelId);
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (model.embeddingDimension) {
|
||||
ragConfig.embeddingDimension = model.embeddingDimension;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusTagType(status?: string | null) {
|
||||
return status === '启用' ? 'success' : 'info';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOverview();
|
||||
loadStores();
|
||||
Promise.all([
|
||||
loadOverview(),
|
||||
loadStores(),
|
||||
queryModelConfigs().then((response) => {
|
||||
embeddingModels.value = (response.data ?? []).filter((item) => item.modelType === 'EMBEDDING');
|
||||
}),
|
||||
loadModelProviderEnumOptions().then((enums) => {
|
||||
chunkStrategyOptions.value = enums.chunk_strategy ?? [];
|
||||
}),
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -351,7 +444,7 @@ onMounted(() => {
|
||||
>
|
||||
批量导入文件
|
||||
</el-button>
|
||||
<el-button :icon="FolderAdd" @click="showFutureMessage('重建索引')">重建索引</el-button>
|
||||
<el-button :icon="FolderAdd" @click="triggerRebuildIndex">重建索引</el-button>
|
||||
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -415,12 +508,56 @@ onMounted(() => {
|
||||
</el-descriptions>
|
||||
</article>
|
||||
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
<article class="detail-card">
|
||||
<div class="detail-card__header">
|
||||
<h4>检索配置</h4>
|
||||
<span>下一批接口补充</span>
|
||||
<el-button type="primary" link :loading="ragConfigSaving" @click="saveStoreModelConfig">
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-loading="ragConfigLoading" class="rag-config-form">
|
||||
<el-form :model="ragConfig" label-width="112px">
|
||||
<el-form-item label="Embedding 模型" required>
|
||||
<el-select
|
||||
v-model="ragConfig.embeddingModelId"
|
||||
placeholder="请选择模型"
|
||||
@change="syncEmbeddingDimensionFromModel"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in embeddingModels"
|
||||
:key="item.id"
|
||||
:label="`${item.modelName}(${item.modelCode})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="向量维度">
|
||||
<el-input-number v-model="ragConfig.embeddingDimension" :min="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="切片策略">
|
||||
<el-select v-model="ragConfig.chunkStrategy" clearable placeholder="可选">
|
||||
<el-option
|
||||
v-for="item in chunkStrategyOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="Number(item.value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="切片大小">
|
||||
<el-input-number v-model="ragConfig.chunkSize" :min="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="切片重叠">
|
||||
<el-input-number v-model="ragConfig.chunkOverlap" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分隔符">
|
||||
<el-input v-model="ragConfig.delimiter" placeholder="可选,如 \\n\\n" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="ragConfig.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-empty description="检索模式、Embedding 模型、Chunk 参数待后端补充" />
|
||||
</article>
|
||||
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
@@ -721,6 +858,11 @@ onMounted(() => {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.rag-config-form :deep(.el-select),
|
||||
.rag-config-form :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RagDocumentsPage from '../RagDocumentsPage.vue';
|
||||
import { getRagDocumentById, parseRagDocuments, queryRagDocuments } from '@/api/ragDocuments';
|
||||
import { chunkRagDocuments, getRagDocumentById, queryRagDocuments, retryParseRagDocuments } from '@/api/ragDocuments';
|
||||
import { queryRagStores } from '@/api/ragStores';
|
||||
|
||||
const routeQuery = vi.hoisted(() => ({ storeId: undefined as string | undefined }));
|
||||
@@ -29,6 +29,14 @@ vi.mock('@/api/ragStores', () => ({
|
||||
|
||||
vi.mock('@/api/ragDocuments', () => ({
|
||||
SOURCE_TYPE_RAG: 'RAG',
|
||||
RAG_CHUNK_STRATEGY: {
|
||||
FIXED_LENGTH: 1,
|
||||
PARAGRAPH: 2,
|
||||
HEADING: 3,
|
||||
TABLE_ROW: 4,
|
||||
DELIMITER: 5,
|
||||
SEMANTIC: 6,
|
||||
},
|
||||
queryRagDocuments: vi.fn((query?: { storeId?: string }) => {
|
||||
const rows = [
|
||||
{
|
||||
@@ -77,12 +85,149 @@ vi.mock('@/api/ragDocuments', () => ({
|
||||
saveRagDocument: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
deleteRagDocument: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
||||
parseRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
||||
chunkRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
retryParseRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
||||
}));
|
||||
|
||||
describe('RagDocumentsPage', () => {
|
||||
it('loads documents from query api', async () => {
|
||||
beforeEach(() => {
|
||||
routeQuery.storeId = undefined;
|
||||
vi.useRealTimers();
|
||||
|
||||
vi.mocked(queryRagDocuments).mockReset();
|
||||
vi.mocked(queryRagDocuments).mockImplementation((query?: { storeId?: string }) => {
|
||||
const rows = [
|
||||
{
|
||||
id: '11',
|
||||
storeId: '1',
|
||||
attachmentId: '101',
|
||||
documentTitle: '产品制度总则',
|
||||
documentSummary: '制度摘要',
|
||||
parseStatus: 'UPLOADED',
|
||||
indexStatus: 'PENDING',
|
||||
enabled: true,
|
||||
remark: '制度文档',
|
||||
createTime: '2026-05-21 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
storeId: '2',
|
||||
attachmentId: '202',
|
||||
documentTitle: 'FAQ 手册',
|
||||
documentSummary: 'FAQ 摘要',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'INDEXED',
|
||||
enabled: false,
|
||||
remark: '常见问题',
|
||||
createTime: '2026-05-21 11:00:00',
|
||||
},
|
||||
];
|
||||
const data = query?.storeId ? rows.filter((row) => row.storeId === query.storeId) : rows;
|
||||
return Promise.resolve({ resultcode: '0', message: null, data });
|
||||
});
|
||||
|
||||
vi.mocked(retryParseRagDocuments).mockReset();
|
||||
vi.mocked(retryParseRagDocuments).mockResolvedValue({ resultcode: '0', message: null, data: [] });
|
||||
|
||||
vi.mocked(chunkRagDocuments).mockReset();
|
||||
vi.mocked(chunkRagDocuments).mockResolvedValue({ resultcode: '0', message: null, data: true });
|
||||
|
||||
vi.mocked(getRagDocumentById).mockReset();
|
||||
vi.mocked(getRagDocumentById).mockImplementation((id: string) =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
id,
|
||||
storeId: '2',
|
||||
attachmentId: '202',
|
||||
documentTitle: 'FAQ 手册',
|
||||
documentSummary: 'FAQ 摘要',
|
||||
enabled: false,
|
||||
remark: '常见问题',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps polling document status after retry parse is submitted', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(queryRagDocuments)
|
||||
.mockResolvedValueOnce({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{
|
||||
id: '33',
|
||||
storeId: '1',
|
||||
attachmentId: '303',
|
||||
documentTitle: '失败文档',
|
||||
documentSummary: '失败摘要',
|
||||
parseStatus: 'FAILED',
|
||||
indexStatus: 'PENDING',
|
||||
enabled: true,
|
||||
remark: '失败',
|
||||
createTime: '2026-05-21 11:00:00',
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{
|
||||
id: '33',
|
||||
storeId: '1',
|
||||
attachmentId: '303',
|
||||
documentTitle: '失败文档',
|
||||
documentSummary: '失败摘要',
|
||||
parseStatus: 'PARSING',
|
||||
indexStatus: 'PENDING',
|
||||
enabled: true,
|
||||
remark: '失败',
|
||||
createTime: '2026-05-21 11:00:00',
|
||||
},
|
||||
],
|
||||
})
|
||||
.mockResolvedValue({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{
|
||||
id: '33',
|
||||
storeId: '1',
|
||||
attachmentId: '303',
|
||||
documentTitle: '失败文档',
|
||||
documentSummary: '失败摘要',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'PENDING',
|
||||
enabled: true,
|
||||
remark: '失败',
|
||||
createTime: '2026-05-21 11:00:00',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="doc-retry-parse-33"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
await flushPromises();
|
||||
|
||||
expect(retryParseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] });
|
||||
expect(vi.mocked(queryRagDocuments)).toHaveBeenCalledTimes(3);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('loads documents from query api', async () => {
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
@@ -98,7 +243,6 @@ describe('RagDocumentsPage', () => {
|
||||
});
|
||||
|
||||
it('renders document filters as a form-style query bar', async () => {
|
||||
routeQuery.storeId = undefined;
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
@@ -130,7 +274,6 @@ describe('RagDocumentsPage', () => {
|
||||
});
|
||||
|
||||
it('loads backend detail when editing a row', async () => {
|
||||
routeQuery.storeId = undefined;
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
@@ -144,8 +287,7 @@ describe('RagDocumentsPage', () => {
|
||||
expect(getRagDocumentById).toHaveBeenCalledWith('22');
|
||||
});
|
||||
|
||||
it('opens parse dialog with chunk strategy options from row action', async () => {
|
||||
routeQuery.storeId = undefined;
|
||||
it('opens chunk dialog with chunk strategy options from row action', async () => {
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
@@ -153,17 +295,16 @@ describe('RagDocumentsPage', () => {
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="doc-parse-11"]').trigger('click');
|
||||
await wrapper.get('[data-test="doc-chunk-22"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-test="document-parse-dialog"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-test="document-chunk-dialog"]').exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('固定长度切片');
|
||||
expect(wrapper.text()).toContain('按分隔符切片');
|
||||
expect(wrapper.text()).toContain('语义切片');
|
||||
});
|
||||
|
||||
it('submits parse request with selected chunk strategy', async () => {
|
||||
routeQuery.storeId = undefined;
|
||||
it('submits chunk request with selected chunk strategy', async () => {
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
@@ -171,20 +312,52 @@ describe('RagDocumentsPage', () => {
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="doc-parse-11"]').trigger('click');
|
||||
await wrapper.get('[data-test="doc-chunk-22"]').trigger('click');
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="document-parse-submit"]').trigger('click');
|
||||
await wrapper.get('[data-test="document-chunk-submit"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(parseRagDocuments).toHaveBeenCalledWith({
|
||||
documentIds: ['11'],
|
||||
chunkStrategy: 'FIXED_LENGTH',
|
||||
expect(chunkRagDocuments).toHaveBeenCalledWith({
|
||||
documentIds: ['22'],
|
||||
chunkStrategy: 1,
|
||||
chunkSize: 800,
|
||||
chunkOverlap: 120,
|
||||
delimiter: '。',
|
||||
});
|
||||
});
|
||||
|
||||
it('retries parse for failed document', async () => {
|
||||
vi.mocked(queryRagDocuments).mockResolvedValueOnce({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{
|
||||
id: '33',
|
||||
storeId: '1',
|
||||
attachmentId: '303',
|
||||
documentTitle: '失败文档',
|
||||
documentSummary: '失败摘要',
|
||||
parseStatus: 'FAILED',
|
||||
indexStatus: 'PENDING',
|
||||
enabled: true,
|
||||
remark: '失败',
|
||||
createTime: '2026-05-21 11:00:00',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="doc-retry-parse-33"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(retryParseRagDocuments).toHaveBeenCalledWith({ documentIds: ['33'] });
|
||||
});
|
||||
|
||||
it('renders reusable upload dialog with drag upload area', async () => {
|
||||
routeQuery.storeId = '1';
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
queryRagStores,
|
||||
saveRagStore,
|
||||
} from '@/api/ragStores';
|
||||
import {
|
||||
getRagStoreModelConfig,
|
||||
queryModelConfigs,
|
||||
rebuildRagStoreIndex,
|
||||
saveRagStoreModelConfig,
|
||||
} from '@/api/modelProvider';
|
||||
|
||||
const routerPush = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -24,6 +30,52 @@ vi.mock('@/api/ragDocuments', () => ({
|
||||
batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/modelEnums', () => ({
|
||||
loadModelProviderEnumOptions: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
chunk_strategy: [
|
||||
{ label: '固定长度', value: '1' },
|
||||
],
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/modelProvider', () => ({
|
||||
queryModelConfigs: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{
|
||||
id: '88',
|
||||
providerId: '1',
|
||||
modelCode: 'TEXT_EMBED_3_LARGE',
|
||||
modelName: 'text-embedding-3-large',
|
||||
modelType: 'EMBEDDING',
|
||||
embeddingDimension: 1024,
|
||||
localModel: false,
|
||||
defaultModel: true,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
getRagStoreModelConfig: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
id: '9',
|
||||
storeId: '1',
|
||||
embeddingModelId: '88',
|
||||
embeddingDimension: 1024,
|
||||
},
|
||||
}),
|
||||
),
|
||||
saveRagStoreModelConfig: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
rebuildRagStoreIndex: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/ragStores', () => ({
|
||||
getRagStoreOverview: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
@@ -142,6 +194,8 @@ describe('RagStoresPage', () => {
|
||||
expect(queryRagStores).toHaveBeenCalled();
|
||||
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
||||
expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1');
|
||||
expect(queryModelConfigs).toHaveBeenCalled();
|
||||
expect(getRagStoreModelConfig).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('filters stores by name and updates detail when a store is selected', async () => {
|
||||
@@ -225,4 +279,23 @@ describe('RagStoresPage', () => {
|
||||
expect(wrapper.text()).toContain('拖拽文件到此处');
|
||||
expect(wrapper.text()).toContain('产品制度库');
|
||||
});
|
||||
|
||||
it('saves store model config and triggers rebuild index', async () => {
|
||||
const wrapper = mount(RagStoresPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
const saveButtons = wrapper.findAll('button').filter((button) => button.text().includes('保存配置'));
|
||||
await saveButtons[0]?.trigger('click');
|
||||
await flushPromises();
|
||||
expect(saveRagStoreModelConfig).toHaveBeenCalled();
|
||||
|
||||
const rebuildButtons = wrapper.findAll('button').filter((button) => button.text().includes('重建索引'));
|
||||
await rebuildButtons[0]?.trigger('click');
|
||||
await flushPromises();
|
||||
expect(rebuildRagStoreIndex).toHaveBeenCalledWith('1');
|
||||
});
|
||||
});
|
||||
|
||||
749
frontend/src/pages/rag/documents/RagDocumentsPage.vue
Normal file
749
frontend/src/pages/rag/documents/RagDocumentsPage.vue
Normal file
@@ -0,0 +1,749 @@
|
||||
<script setup lang="ts">
|
||||
import { Operation, Search, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import {
|
||||
chunkRagDocuments,
|
||||
deleteRagDocument,
|
||||
getRagDocumentById,
|
||||
queryRagDocuments,
|
||||
retryParseRagDocuments,
|
||||
saveRagDocument,
|
||||
RAG_CHUNK_STRATEGY,
|
||||
type RagChunkStrategy,
|
||||
type RagDocument,
|
||||
} from '@/api/ragDocuments';
|
||||
import { queryRagStores, type RagStore } from '@/api/ragStores';
|
||||
import RagChunkTaskBoard from '@/components/rag/chunk/RagChunkTaskBoard.vue';
|
||||
import RagDocumentBatchUploadDialog from '@/components/rag/RagDocumentBatchUploadDialog.vue';
|
||||
import RagFlowOverview from '@/components/rag/document/RagFlowOverview.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const parseSubmitting = ref(false);
|
||||
const retryParsing = ref(false);
|
||||
const route = useRoute();
|
||||
|
||||
const storeOptions = ref<RagStore[]>([]);
|
||||
const docRows = ref<RagDocument[]>([]);
|
||||
|
||||
const queryForm = reactive({
|
||||
storeId: '',
|
||||
parseStatus: '',
|
||||
indexStatus: '',
|
||||
enabled: '' as string,
|
||||
keyword: '',
|
||||
});
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const uploadDialogVisible = ref(false);
|
||||
const chunkDialogVisible = ref(false);
|
||||
const selectedDocuments = ref<RagDocument[]>([]);
|
||||
|
||||
const editForm = reactive({
|
||||
id: '',
|
||||
storeId: '',
|
||||
attachmentId: '',
|
||||
documentTitle: '',
|
||||
documentSummary: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const chunkForm = reactive({
|
||||
documentIds: [] as string[],
|
||||
chunkStrategy: RAG_CHUNK_STRATEGY.FIXED_LENGTH as RagChunkStrategy,
|
||||
chunkSize: 800,
|
||||
chunkOverlap: 120,
|
||||
delimiter: '。',
|
||||
});
|
||||
|
||||
const chunkStrategyOptions: Array<{ label: string; value: RagChunkStrategy; description: string }> = [
|
||||
{ label: '固定长度切片', value: RAG_CHUNK_STRATEGY.FIXED_LENGTH, description: '按指定长度和重叠长度切分通用文本' },
|
||||
{ label: '按段落切片', value: RAG_CHUNK_STRATEGY.PARAGRAPH, description: '按空行、自然段落边界切分' },
|
||||
{ label: '按标题层级切片', value: RAG_CHUNK_STRATEGY.HEADING, description: '按标题和章节层级组织内容' },
|
||||
{ label: '按表格行切片', value: RAG_CHUNK_STRATEGY.TABLE_ROW, description: '适合 Excel 表格和结构化明细数据' },
|
||||
{ label: '按分隔符切片', value: RAG_CHUNK_STRATEGY.DELIMITER, description: '按句号、换行符或自定义分隔符切分' },
|
||||
{ label: '语义切片', value: RAG_CHUNK_STRATEGY.SEMANTIC, description: '后续结合语义边界或模型能力切分' },
|
||||
];
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const kw = queryForm.keyword.trim().toLowerCase();
|
||||
return docRows.value.filter(
|
||||
(row) => {
|
||||
const matchStore = !queryForm.storeId || row.storeId === queryForm.storeId;
|
||||
const matchParseStatus = !queryForm.parseStatus || row.parseStatus === queryForm.parseStatus;
|
||||
const matchIndexStatus = !queryForm.indexStatus || row.indexStatus === queryForm.indexStatus;
|
||||
const matchEnabled = !queryForm.enabled || String(row.enabled ?? false) === queryForm.enabled;
|
||||
const matchKeyword =
|
||||
!kw ||
|
||||
(row.documentTitle && row.documentTitle.toLowerCase().includes(kw)) ||
|
||||
(row.documentSummary && row.documentSummary.toLowerCase().includes(kw)) ||
|
||||
(row.remark && row.remark.toLowerCase().includes(kw));
|
||||
return matchStore && matchParseStatus && matchIndexStatus && matchEnabled && matchKeyword;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const parsedCount = computed(() => docRows.value.filter((row) => row.parseStatus === 'PARSED').length);
|
||||
const failedCount = computed(() => docRows.value.filter((row) => row.parseStatus === 'FAILED').length);
|
||||
const selectedParsedCount = computed(() => selectedDocuments.value.filter((row) => row.parseStatus === 'PARSED').length);
|
||||
const selectedFailedCount = computed(() => selectedDocuments.value.filter((row) => row.parseStatus === 'FAILED').length);
|
||||
|
||||
async function loadStores() {
|
||||
try {
|
||||
const response = await queryRagStores();
|
||||
storeOptions.value = response.data ?? [];
|
||||
} catch {
|
||||
storeOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryRagDocuments({
|
||||
...(queryForm.storeId ? { storeId: queryForm.storeId } : {}),
|
||||
...(queryForm.parseStatus ? { parseStatus: queryForm.parseStatus } : {}),
|
||||
...(queryForm.indexStatus ? { indexStatus: queryForm.indexStatus } : {}),
|
||||
...(queryForm.enabled ? { enabled: queryForm.enabled === 'true' } : {}),
|
||||
});
|
||||
docRows.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshParseProgress(documentIds: string[]) {
|
||||
if (documentIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
let rounds = 0;
|
||||
while (rounds < 6) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
await loadDocs();
|
||||
const targetDocs = docRows.value.filter((row) => documentIds.includes(String(row.id ?? '')));
|
||||
const allDone = targetDocs.every((row) => row.parseStatus === 'PARSED' || row.parseStatus === 'FAILED');
|
||||
if (allDone) {
|
||||
return;
|
||||
}
|
||||
rounds += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function markRowsParsing(documentIds: string[]) {
|
||||
if (documentIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const idSet = new Set(documentIds);
|
||||
docRows.value = docRows.value.map((row) => {
|
||||
if (!idSet.has(String(row.id ?? ''))) {
|
||||
return row;
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
parseStatus: 'PARSING',
|
||||
errorMessage: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUploaded(documentIds: string[]) {
|
||||
await loadDocs();
|
||||
await refreshParseProgress(documentIds);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadDocs();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryForm.storeId = '';
|
||||
queryForm.parseStatus = '';
|
||||
queryForm.indexStatus = '';
|
||||
queryForm.enabled = '';
|
||||
queryForm.keyword = '';
|
||||
loadDocs();
|
||||
}
|
||||
|
||||
function openUploadDialog() {
|
||||
if (storeOptions.value.length === 0) {
|
||||
ElMessage.warning('请先创建知识库');
|
||||
return;
|
||||
}
|
||||
uploadDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: RagDocument[]) {
|
||||
selectedDocuments.value = rows;
|
||||
}
|
||||
|
||||
function openChunkDialog(rows: RagDocument[]) {
|
||||
const parsedRows = rows.filter((row) => row.parseStatus === 'PARSED');
|
||||
const ids = parsedRows.map((row) => String(row.id ?? '')).filter(Boolean);
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning('请选择解析完成(PARSED)的文档进行切片');
|
||||
return;
|
||||
}
|
||||
if (parsedRows.length < rows.length) {
|
||||
ElMessage.warning(`已自动跳过 ${rows.length - parsedRows.length} 个未解析完成文档`);
|
||||
}
|
||||
chunkForm.documentIds = ids;
|
||||
chunkForm.chunkStrategy = RAG_CHUNK_STRATEGY.FIXED_LENGTH;
|
||||
chunkForm.chunkSize = 800;
|
||||
chunkForm.chunkOverlap = 120;
|
||||
chunkForm.delimiter = '。';
|
||||
chunkDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openBatchChunkDialog() {
|
||||
openChunkDialog(selectedDocuments.value);
|
||||
}
|
||||
|
||||
async function openEditDialog(row: RagDocument) {
|
||||
const detail = row.id ? (await getRagDocumentById(String(row.id))).data : row;
|
||||
|
||||
editForm.id = String(detail.id ?? '');
|
||||
editForm.storeId = detail.storeId;
|
||||
editForm.attachmentId = detail.attachmentId ?? '';
|
||||
editForm.documentTitle = detail.documentTitle ?? '';
|
||||
editForm.documentSummary = detail.documentSummary ?? '';
|
||||
editForm.enabled = detail.enabled ?? true;
|
||||
editForm.remark = detail.remark ?? '';
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editForm.id || !editForm.storeId || !editForm.documentTitle) {
|
||||
ElMessage.warning('请填写文档标题');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveRagDocument({
|
||||
id: editForm.id,
|
||||
storeId: editForm.storeId,
|
||||
attachmentId: editForm.attachmentId || undefined,
|
||||
documentTitle: editForm.documentTitle,
|
||||
documentSummary: editForm.documentSummary || undefined,
|
||||
enabled: editForm.enabled,
|
||||
remark: editForm.remark || undefined,
|
||||
});
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success('文档信息已更新');
|
||||
await loadDocs();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDoc(row: RagDocument) {
|
||||
if (!row.id) return;
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除文档「${row.documentTitle || '未命名'}」?`,
|
||||
'删除确认',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||
);
|
||||
|
||||
await deleteRagDocument(String(row.id));
|
||||
ElMessage.success('文档已删除');
|
||||
await loadDocs();
|
||||
}
|
||||
|
||||
function toggleEnabled(row: RagDocument) {
|
||||
if (!row.id) return;
|
||||
const newEnabled = !row.enabled;
|
||||
saveRagDocument({
|
||||
id: String(row.id),
|
||||
storeId: row.storeId,
|
||||
documentTitle: row.documentTitle ?? '',
|
||||
enabled: newEnabled,
|
||||
}).then(() => {
|
||||
row.enabled = newEnabled;
|
||||
ElMessage.success(`已${newEnabled ? '启用' : '停用'}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function submitChunk() {
|
||||
if (chunkForm.documentIds.length === 0) {
|
||||
ElMessage.warning('请选择需要切片的文档');
|
||||
return;
|
||||
}
|
||||
parseSubmitting.value = true;
|
||||
try {
|
||||
await chunkRagDocuments({
|
||||
documentIds: chunkForm.documentIds,
|
||||
chunkStrategy: chunkForm.chunkStrategy,
|
||||
chunkSize: chunkForm.chunkSize,
|
||||
chunkOverlap: chunkForm.chunkOverlap,
|
||||
delimiter: chunkForm.delimiter,
|
||||
});
|
||||
chunkDialogVisible.value = false;
|
||||
ElMessage.success('切片任务已提交');
|
||||
await loadDocs();
|
||||
} finally {
|
||||
parseSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function retryParseRows(rows: RagDocument[]) {
|
||||
const ids = rows
|
||||
.filter((row) => row.parseStatus === 'FAILED')
|
||||
.map((row) => String(row.id ?? ''))
|
||||
.filter(Boolean);
|
||||
if (ids.length === 0) {
|
||||
ElMessage.warning('请选择解析失败(FAILED)的文档重试解析');
|
||||
return;
|
||||
}
|
||||
retryParsing.value = true;
|
||||
try {
|
||||
await retryParseRagDocuments({ documentIds: ids });
|
||||
markRowsParsing(ids);
|
||||
ElMessage.success('已提交解析重试任务');
|
||||
await loadDocs();
|
||||
await refreshParseProgress(ids);
|
||||
} finally {
|
||||
retryParsing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getStoreName(storeId: string) {
|
||||
const store = storeOptions.value.find((s) => String(s.id) === storeId);
|
||||
return store ? store.storeName : '-';
|
||||
}
|
||||
|
||||
function getStatusLabel(status?: string | null) {
|
||||
const map: Record<string, string> = {
|
||||
UPLOADED: '已上传',
|
||||
PARSING: '解析中',
|
||||
PARSED: '已解析',
|
||||
FAILED: '解析失败',
|
||||
PENDING: '待索引',
|
||||
INDEXING: '索引中',
|
||||
INDEXED: '已索引',
|
||||
};
|
||||
return status ? (map[status] ?? status) : '-';
|
||||
}
|
||||
|
||||
function getParseStatusType(status?: string | null) {
|
||||
const success = ['PARSED'];
|
||||
const warning = ['UPLOADED', 'PARSING'];
|
||||
const danger = ['FAILED'];
|
||||
if (!status) return 'info';
|
||||
if (success.includes(status)) return 'success';
|
||||
if (warning.includes(status)) return 'warning';
|
||||
if (danger.includes(status)) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function getIndexStatusType(status?: string | null) {
|
||||
const success = ['INDEXED'];
|
||||
const warning = ['PENDING', 'INDEXING'];
|
||||
const danger = ['FAILED'];
|
||||
if (!status) return 'info';
|
||||
if (success.includes(status)) return 'success';
|
||||
if (warning.includes(status)) return 'warning';
|
||||
if (danger.includes(status)) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const routeStoreId = route.query.storeId;
|
||||
if (typeof routeStoreId === 'string') {
|
||||
queryForm.storeId = routeStoreId;
|
||||
}
|
||||
loadStores();
|
||||
loadDocs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel rag-doc-page">
|
||||
<div class="page-panel__header">
|
||||
<h2>知识文档</h2>
|
||||
<span>上传自动解析,切片手动触发</span>
|
||||
</div>
|
||||
|
||||
<RagFlowOverview :documents="docRows" />
|
||||
<div class="task-board-wrap">
|
||||
<RagChunkTaskBoard :total="docRows.length" :parsed="parsedCount" :failed="failedCount" />
|
||||
</div>
|
||||
|
||||
<div class="document-query-bar" data-test="document-query-bar">
|
||||
<el-form class="document-query-form" data-test="document-query-form" inline>
|
||||
<el-form-item label="知识库">
|
||||
<el-select
|
||||
v-model="queryForm.storeId"
|
||||
data-test="doc-store-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option
|
||||
v-for="store in storeOptions"
|
||||
:key="String(store.id)"
|
||||
:label="store.storeName"
|
||||
:value="String(store.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="解析状态">
|
||||
<el-select
|
||||
v-model="queryForm.parseStatus"
|
||||
data-test="doc-parse-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option label="已上传" value="UPLOADED" />
|
||||
<el-option label="解析中" value="PARSING" />
|
||||
<el-option label="已解析" value="PARSED" />
|
||||
<el-option label="解析失败" value="FAILED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="索引状态">
|
||||
<el-select
|
||||
v-model="queryForm.indexStatus"
|
||||
data-test="doc-index-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option label="待索引" value="PENDING" />
|
||||
<el-option label="索引中" value="INDEXING" />
|
||||
<el-option label="已索引" value="INDEXED" />
|
||||
<el-option label="索引失败" value="FAILED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-select
|
||||
v-model="queryForm.enabled"
|
||||
data-test="doc-enabled-filter"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="query-control query-control--select"
|
||||
>
|
||||
<el-option label="启用" value="true" />
|
||||
<el-option label="停用" value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="queryForm.keyword"
|
||||
data-test="doc-keyword-input"
|
||||
placeholder="搜索标题/摘要/备注"
|
||||
clearable
|
||||
class="query-control query-control--keyword"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="document-query-form__actions">
|
||||
<el-button data-test="doc-search" type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button
|
||||
data-test="open-batch-chunk"
|
||||
:icon="Operation"
|
||||
:disabled="selectedParsedCount === 0"
|
||||
@click="openBatchChunkDialog"
|
||||
>
|
||||
批量切片({{ selectedParsedCount }})
|
||||
</el-button>
|
||||
<el-button
|
||||
data-test="retry-batch-parse"
|
||||
:loading="retryParsing"
|
||||
:disabled="selectedFailedCount === 0"
|
||||
@click="retryParseRows(selectedDocuments)"
|
||||
>
|
||||
重试解析({{ selectedFailedCount }})
|
||||
</el-button>
|
||||
<el-button
|
||||
data-test="open-doc-upload"
|
||||
type="primary"
|
||||
:icon="UploadFilled"
|
||||
@click="openUploadDialog"
|
||||
>
|
||||
批量上传
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredRows"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column type="index" label="编号" width="70" align="center" />
|
||||
<el-table-column prop="documentTitle" label="文档标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="所属知识库" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getStoreName(row.storeId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解析状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getParseStatusType(row.parseStatus)" size="small">
|
||||
{{ getStatusLabel(row.parseStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="索引状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getIndexStatusType(row.indexStatus)" size="small">
|
||||
{{ getStatusLabel(row.indexStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.enabled ?? false"
|
||||
size="small"
|
||||
@change="toggleEnabled(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="documentSummary" label="摘要" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
:data-test="`doc-chunk-${row.id}`"
|
||||
link
|
||||
type="primary"
|
||||
:disabled="row.parseStatus !== 'PARSED'"
|
||||
@click="openChunkDialog([row])"
|
||||
>
|
||||
切片
|
||||
</el-button>
|
||||
<el-button
|
||||
:data-test="`doc-retry-parse-${row.id}`"
|
||||
link
|
||||
type="warning"
|
||||
:disabled="row.parseStatus !== 'FAILED'"
|
||||
@click="retryParseRows([row])"
|
||||
>
|
||||
重试解析
|
||||
</el-button>
|
||||
<el-button :data-test="`doc-edit-${row.id}`" link type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="removeDoc(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty v-if="!loading && filteredRows.length === 0" description="暂无知识文档" />
|
||||
|
||||
<RagDocumentBatchUploadDialog
|
||||
v-model="uploadDialogVisible"
|
||||
:stores="storeOptions"
|
||||
:locked-store-id="queryForm.storeId || null"
|
||||
@uploaded="handleUploaded"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="chunkDialogVisible"
|
||||
data-test="document-chunk-dialog"
|
||||
title="切片配置"
|
||||
width="620px"
|
||||
>
|
||||
<el-form :model="chunkForm" label-width="112px">
|
||||
<el-form-item label="文档数量">
|
||||
<el-tag>{{ chunkForm.documentIds.length }} 个文档</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="切片方式">
|
||||
<el-radio-group v-model="chunkForm.chunkStrategy" class="chunk-strategy-group">
|
||||
<el-radio
|
||||
v-for="strategy in chunkStrategyOptions"
|
||||
:key="strategy.value"
|
||||
:value="strategy.value"
|
||||
class="chunk-strategy-option"
|
||||
>
|
||||
<span class="chunk-strategy-option__label">{{ strategy.label }}</span>
|
||||
<small>{{ strategy.description }}</small>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="chunkForm.chunkStrategy === RAG_CHUNK_STRATEGY.FIXED_LENGTH" label="切片长度">
|
||||
<el-input-number v-model="chunkForm.chunkSize" :min="100" :max="4000" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="chunkForm.chunkStrategy === RAG_CHUNK_STRATEGY.FIXED_LENGTH" label="重叠长度">
|
||||
<el-input-number v-model="chunkForm.chunkOverlap" :min="0" :max="1000" :step="20" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="chunkForm.chunkStrategy === RAG_CHUNK_STRATEGY.DELIMITER" label="分隔符">
|
||||
<el-input v-model="chunkForm.delimiter" maxlength="20" placeholder="如 。、换行符或自定义符号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="chunkDialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
data-test="document-chunk-submit"
|
||||
type="primary"
|
||||
:loading="parseSubmitting"
|
||||
@click="submitChunk"
|
||||
>
|
||||
开始切片
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="editDialogVisible" title="编辑文档" width="560px">
|
||||
<el-form :model="editForm" label-width="96px">
|
||||
<el-form-item label="文档标题" required>
|
||||
<el-input v-model="editForm.documentTitle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文档摘要">
|
||||
<el-input v-model="editForm.documentSummary" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="editForm.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-doc-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-board-wrap {
|
||||
padding: 10px 28px 0;
|
||||
}
|
||||
|
||||
.document-query-bar {
|
||||
padding: 18px 28px 17px;
|
||||
border-bottom: 1px solid #e8edf5;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.document-query-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-form-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-form-item__label) {
|
||||
height: 38px;
|
||||
padding-right: 8px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-input__wrapper),
|
||||
.document-query-form :deep(.el-select__wrapper) {
|
||||
min-height: 38px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px #d8dee9 inset;
|
||||
}
|
||||
|
||||
.document-query-form :deep(.el-input__wrapper:hover),
|
||||
.document-query-form :deep(.el-select__wrapper:hover) {
|
||||
box-shadow: 0 0 0 1px #b9c6d8 inset;
|
||||
}
|
||||
|
||||
.query-control--select {
|
||||
width: 168px;
|
||||
}
|
||||
|
||||
.query-control--keyword {
|
||||
width: 225px;
|
||||
}
|
||||
|
||||
.document-query-form__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.document-query-form__actions :deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chunk-strategy-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chunk-strategy-option {
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chunk-strategy-option :deep(.el-radio__label) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chunk-strategy-option__label {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chunk-strategy-option small {
|
||||
color: #7a8599;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-board-wrap {
|
||||
padding: 10px 16px 0;
|
||||
}
|
||||
|
||||
.document-query-bar {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.document-query-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.query-control--select,
|
||||
.query-control--keyword {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.document-query-form__actions {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.document-query-form__actions :deep(.el-form-item__content) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chunk-strategy-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
frontend/src/pages/rag/tasks/RagChunkTasksPage.vue
Normal file
96
frontend/src/pages/rag/tasks/RagChunkTasksPage.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { queryRagDocuments } from '@/api/ragDocuments';
|
||||
import RagChunkTaskBoard from '@/components/rag/chunk/RagChunkTaskBoard.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const docs = ref<any[]>([]);
|
||||
const router = useRouter();
|
||||
|
||||
const failedDocs = computed(() => docs.value.filter((row) => row.parseStatus === 'FAILED'));
|
||||
const parsedDocs = computed(() => docs.value.filter((row) => row.parseStatus === 'PARSED'));
|
||||
|
||||
async function loadDocs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryRagDocuments();
|
||||
docs.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDocs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel chunk-task-page">
|
||||
<div class="page-panel__header">
|
||||
<h2>切片任务</h2>
|
||||
<span>聚焦可切片文档与异常解析文档</span>
|
||||
</div>
|
||||
|
||||
<div class="task-board-wrap">
|
||||
<RagChunkTaskBoard :total="docs.length" :parsed="parsedDocs.length" :failed="failedDocs.length" />
|
||||
</div>
|
||||
|
||||
<div class="task-actions">
|
||||
<el-button type="primary" @click="router.push('/rag/documents')">去执行切片</el-button>
|
||||
<el-button :loading="loading" @click="loadDocs">刷新任务视图</el-button>
|
||||
</div>
|
||||
|
||||
<div class="task-list">
|
||||
<h3>解析失败文档</h3>
|
||||
<el-empty v-if="!loading && failedDocs.length === 0" description="当前没有失败文档" />
|
||||
<el-table v-else :data="failedDocs" border>
|
||||
<el-table-column prop="documentTitle" label="文档标题" min-width="220" />
|
||||
<el-table-column prop="parseStatus" label="解析状态" width="120" />
|
||||
<el-table-column prop="errorMessage" label="失败原因" min-width="280" />
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chunk-task-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-board-wrap {
|
||||
padding: 10px 28px 0;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 16px 28px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
padding: 0 28px 24px;
|
||||
}
|
||||
|
||||
.task-list h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 15px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-board-wrap {
|
||||
padding: 10px 16px 0;
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
padding: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
76
frontend/src/pages/rag/workbench/RagWorkbenchPage.vue
Normal file
76
frontend/src/pages/rag/workbench/RagWorkbenchPage.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { queryRagDocuments } from '@/api/ragDocuments';
|
||||
import RagChunkTaskBoard from '@/components/rag/chunk/RagChunkTaskBoard.vue';
|
||||
import RagFlowOverview from '@/components/rag/document/RagFlowOverview.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const docs = ref<any[]>([]);
|
||||
const router = useRouter();
|
||||
|
||||
const parsedCount = () => docs.value.filter((row) => row.parseStatus === 'PARSED').length;
|
||||
const failedCount = () => docs.value.filter((row) => row.parseStatus === 'FAILED').length;
|
||||
|
||||
async function loadDocs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryRagDocuments();
|
||||
docs.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDocs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel rag-workbench">
|
||||
<div class="page-panel__header">
|
||||
<h2>RAG 工作台</h2>
|
||||
<span>上传自动解析,切片手动触发</span>
|
||||
</div>
|
||||
|
||||
<RagFlowOverview :documents="docs" />
|
||||
|
||||
<div class="workbench-board">
|
||||
<RagChunkTaskBoard :total="docs.length" :parsed="parsedCount()" :failed="failedCount()" />
|
||||
</div>
|
||||
|
||||
<div class="workbench-actions">
|
||||
<el-button type="primary" @click="router.push('/rag/documents')">进入文档管理</el-button>
|
||||
<el-button @click="router.push('/rag/tasks/chunk')">查看切片任务</el-button>
|
||||
<el-button :loading="loading" @click="loadDocs">刷新概览</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-workbench {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.workbench-board {
|
||||
padding: 10px 28px 0;
|
||||
}
|
||||
|
||||
.workbench-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 18px 28px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workbench-board {
|
||||
padding: 10px 16px 0;
|
||||
}
|
||||
|
||||
.workbench-actions {
|
||||
padding: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
frontend/src/pages/studio/AgentWorkspacePage.vue
Normal file
69
frontend/src/pages/studio/AgentWorkspacePage.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ChatDotRound, Coin, Timer } from '@element-plus/icons-vue';
|
||||
|
||||
import { chatMessages, citations, traceSteps } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page agent-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">AgentWorkspaceView</p>
|
||||
<h1>Agent 对话调试</h1>
|
||||
</div>
|
||||
<el-button type="primary">发布 Agent</el-button>
|
||||
</header>
|
||||
|
||||
<div class="agent-layout">
|
||||
<section class="studio-panel chat-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>售前问答 Agent</h2>
|
||||
<span>POST /api/agents/1001/runs</span>
|
||||
</div>
|
||||
<el-tag>Draft</el-tag>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<article v-for="message in chatMessages" :key="message.content" :class="message.role">
|
||||
<strong>{{ message.role === 'user' ? '用户' : 'Agent' }}</strong>
|
||||
<p>{{ message.content }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="chat-composer">
|
||||
<span>输入调试问题,运行会写入 agent_session / agent_message 草案</span>
|
||||
<el-button type="primary"><el-icon><ChatDotRound /></el-icon> 发送</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel citation-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>引用切片</h2>
|
||||
<span>3 个来源</span>
|
||||
</div>
|
||||
<article v-for="citation in citations" :key="citation.title" class="citation-card">
|
||||
<strong>{{ citation.title }}</strong>
|
||||
<el-tag type="success">score {{ citation.score }}</el-tag>
|
||||
<p>{{ citation.text }}</p>
|
||||
</article>
|
||||
</aside>
|
||||
|
||||
<aside class="studio-panel run-inspector">
|
||||
<div class="panel-heading compact">
|
||||
<h2>运行追踪</h2>
|
||||
<span>modelRequestId: f4215d</span>
|
||||
</div>
|
||||
<div class="metric-mini">
|
||||
<span><el-icon><Timer /></el-icon> 1.42s</span>
|
||||
<span><el-icon><Coin /></el-icon> ¥0.018</span>
|
||||
<span>1,248 tokens</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li v-for="step in traceSteps" :key="step.node">
|
||||
<time>{{ step.duration }}</time>
|
||||
<span>{{ step.node }} · {{ step.output }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
73
frontend/src/pages/studio/IngestionPipelinePage.vue
Normal file
73
frontend/src/pages/studio/IngestionPipelinePage.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
|
||||
import { ingestionSteps } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page ingestion-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">IngestionPipelineView</p>
|
||||
<h1>文件解析管道</h1>
|
||||
</div>
|
||||
<el-button type="primary">启动索引任务</el-button>
|
||||
</header>
|
||||
|
||||
<div class="ingestion-layout">
|
||||
<section class="studio-panel upload-panel">
|
||||
<div class="upload-dropzone">
|
||||
<el-icon><UploadFilled /></el-icon>
|
||||
<strong>拖拽文件到这里</strong>
|
||||
<span>支持 PDF / Word / Excel / Markdown / TXT,上传后自动创建 ingestion run。</span>
|
||||
<el-button type="primary">选择文件</el-button>
|
||||
</div>
|
||||
<div class="pipeline-timeline">
|
||||
<article v-for="step in ingestionSteps" :key="step.name" :class="`is-${step.status}`">
|
||||
<div class="timeline-dot" />
|
||||
<div class="timeline-content">
|
||||
<strong>{{ step.name }}</strong>
|
||||
<span>{{ step.description }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel preview-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>解析与切片预览</h2>
|
||||
<span>GET /api/knowledge/ingestion-runs/run-20260531</span>
|
||||
</div>
|
||||
<div class="preview-split">
|
||||
<article>
|
||||
<h3>解析文本</h3>
|
||||
<p>私有化部署章节应覆盖基础设施、网络、安全与运维边界。平台需说明模型服务商、知识库索引策略与日志留存周期...</p>
|
||||
</article>
|
||||
<article>
|
||||
<h3>切片 #24</h3>
|
||||
<p>chunk_size=800, overlap=120, strategy=FIXED_LENGTH。该切片将进入 rag_chunk 并在向量化后写入 rag_chunk_embedding。</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="pipeline-controls">
|
||||
<label>切片策略 <strong>固定长度</strong></label>
|
||||
<label>Chunk Size <strong>800</strong></label>
|
||||
<label>Overlap <strong>120</strong></label>
|
||||
<label>Embedding <strong>Qwen3 1024d</strong></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel task-log-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>任务日志</h2>
|
||||
<span>run-20260531</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li><time>23:08:12</time><span>上传 4 个文件并创建 rag_document</span></li>
|
||||
<li><time>23:08:24</time><span>Tika 解析完成 3 个文件</span></li>
|
||||
<li class="warn"><time>23:08:31</time><span>服务条款更新.md 编码检测失败,等待重试</span></li>
|
||||
<li><time>23:08:40</time><span>切片任务进行中 68 / 119</span></li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
94
frontend/src/pages/studio/KnowledgeWorkspacePage.vue
Normal file
94
frontend/src/pages/studio/KnowledgeWorkspacePage.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { DataAnalysis, Document, Setting } from '@element-plus/icons-vue';
|
||||
|
||||
import { knowledgeDocuments, knowledgeStores } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page workspace-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">KnowledgeWorkspaceView</p>
|
||||
<h1>知识资产</h1>
|
||||
</div>
|
||||
<el-button type="primary">新建知识库</el-button>
|
||||
</header>
|
||||
|
||||
<div class="three-column-layout">
|
||||
<aside class="studio-panel collection-rail">
|
||||
<div class="panel-heading compact">
|
||||
<h2>知识集合</h2>
|
||||
<span>{{ knowledgeStores.length }} 个库</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="store in knowledgeStores"
|
||||
:key="store.id"
|
||||
class="collection-item"
|
||||
:class="{ active: store.id === '1001' }"
|
||||
>
|
||||
<strong>{{ store.name }}</strong>
|
||||
<span>{{ store.docs }} 文档 · 健康度 {{ store.health }}%</span>
|
||||
<em>{{ store.status }}</em>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main class="studio-panel knowledge-main">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>产品制度库</h2>
|
||||
<span>绑定旧数据语义:rag_store / rag_document / rag_chunk_embedding</span>
|
||||
</div>
|
||||
<el-tag type="success">可检索</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="config-grid">
|
||||
<article>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<strong>Embedding 模型</strong>
|
||||
<span>Qwen3-Embedding · 1024 维</span>
|
||||
</article>
|
||||
<article>
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<strong>检索配置</strong>
|
||||
<span>TopK 6 · Score ≥ 0.72 · Rerank 关闭</span>
|
||||
</article>
|
||||
<article>
|
||||
<el-icon><Document /></el-icon>
|
||||
<strong>索引版本</strong>
|
||||
<span>index_version 14 · Draft 快照</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="document-table">
|
||||
<div class="table-row table-head">
|
||||
<span>文档</span><span>解析</span><span>索引</span><span>切片</span><span>更新</span>
|
||||
</div>
|
||||
<div v-for="doc in knowledgeDocuments" :key="doc.id" class="table-row">
|
||||
<strong>{{ doc.name }}</strong>
|
||||
<span>{{ doc.parseStatus }}</span>
|
||||
<span>{{ doc.indexStatus }}</span>
|
||||
<span>{{ doc.chunks }}</span>
|
||||
<span>{{ doc.updatedAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="studio-panel inspector-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>状态 Inspector</h2>
|
||||
<span>聚合接口</span>
|
||||
</div>
|
||||
<dl class="inspector-list">
|
||||
<dt>Workspace API</dt>
|
||||
<dd>GET /api/knowledge/workspaces/1001</dd>
|
||||
<dt>文档健康度</dt>
|
||||
<dd>96% · 1 个解析失败</dd>
|
||||
<dt>待处理任务</dt>
|
||||
<dd>2 个文档等待向量化</dd>
|
||||
<dt>发布影响</dt>
|
||||
<dd>更新后需要 Workflow 重新验证引用质量</dd>
|
||||
</dl>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
49
frontend/src/pages/studio/McpImportPage.vue
Normal file
49
frontend/src/pages/studio/McpImportPage.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, Upload } from '@element-plus/icons-vue';
|
||||
|
||||
import { mcpCapabilities } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page mcp-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">McpImportView</p>
|
||||
<h1>MCP 导入</h1>
|
||||
</div>
|
||||
<el-button type="primary"><el-icon><Upload /></el-icon> 导入 Server</el-button>
|
||||
</header>
|
||||
|
||||
<div class="mcp-layout">
|
||||
<section class="studio-panel import-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>外部能力接入</h2>
|
||||
<span>POST /api/mcp/import</span>
|
||||
</div>
|
||||
<div class="import-options">
|
||||
<button class="active"><el-icon><Link /></el-icon><strong>URL</strong><span>https://mcp.example.com/sse</span></button>
|
||||
<button><strong>npm package</strong><span>@acme/mcp-jira</span></button>
|
||||
<button><strong>JSON Manifest</strong><span>粘贴 server 能力声明</span></button>
|
||||
</div>
|
||||
<div class="manifest-box">
|
||||
<span>{ "server": "jira", "transport": "sse", "auth": "oauth2" }</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel capability-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>能力预览</h2>
|
||||
<span>GET /api/mcp/servers/jira/capabilities</span>
|
||||
</div>
|
||||
<div class="capability-grid">
|
||||
<article v-for="item in mcpCapabilities" :key="item.name">
|
||||
<el-tag>{{ item.type }}</el-tag>
|
||||
<strong>{{ item.name }}</strong>
|
||||
<p>{{ item.description }}</p>
|
||||
<span>{{ item.status }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
38
frontend/src/pages/studio/ModelWorkspacePage.vue
Normal file
38
frontend/src/pages/studio/ModelWorkspacePage.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { modelRoutes } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page model-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">ModelRoutingView</p>
|
||||
<h1>模型与路由</h1>
|
||||
</div>
|
||||
<el-button type="primary">新增路由</el-button>
|
||||
</header>
|
||||
|
||||
<div class="studio-panel model-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>任务路由规则</h2>
|
||||
<span>保留 model_provider / model_config / model_route_rule 语义</span>
|
||||
</div>
|
||||
<div class="document-table">
|
||||
<div class="table-row table-head">
|
||||
<span>任务</span><span>主模型</span><span>Fallback</span><span>最大延迟</span><span>状态</span>
|
||||
</div>
|
||||
<div v-for="route in modelRoutes" :key="route.task" class="table-row">
|
||||
<strong>{{ route.task }}</strong>
|
||||
<span>{{ route.primary }}</span>
|
||||
<span>{{ route.fallback }}</span>
|
||||
<span>{{ route.latency }}</span>
|
||||
<span class="status-cell">
|
||||
<span class="status-pill" :class="route.status === '启用' ? 'is-success' : 'is-warning'">
|
||||
{{ route.status }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
53
frontend/src/pages/studio/ObservabilityPage.vue
Normal file
53
frontend/src/pages/studio/ObservabilityPage.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { recentRuns, traceSteps } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page observability-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">ObservabilityView</p>
|
||||
<h1>运行观测</h1>
|
||||
</div>
|
||||
<el-button>导出日志</el-button>
|
||||
</header>
|
||||
|
||||
<div class="observability-layout">
|
||||
<section class="studio-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>运行记录</h2>
|
||||
<span>workflow_run / workflow_run_step / model_call_log</span>
|
||||
</div>
|
||||
<div class="run-table">
|
||||
<div class="run-row run-head">
|
||||
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
||||
</div>
|
||||
<div v-for="run in recentRuns" :key="run.id" class="run-row">
|
||||
<strong>{{ run.name }}</strong>
|
||||
<span>{{ run.type }}</span>
|
||||
<span class="status-cell">
|
||||
<span class="status-pill" :class="run.status === '成功' ? 'is-success' : 'is-warning'">
|
||||
{{ run.status }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ run.latency }}</span>
|
||||
<span>{{ run.cost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>步骤日志</h2>
|
||||
<span>run-1842</span>
|
||||
</div>
|
||||
<ol class="log-list">
|
||||
<li v-for="step in traceSteps" :key="step.node">
|
||||
<time>{{ step.duration }}</time>
|
||||
<span>{{ step.node }} · {{ step.status }} · {{ step.output }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
56
frontend/src/pages/studio/SkillWorkspacePage.vue
Normal file
56
frontend/src/pages/studio/SkillWorkspacePage.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { skillVersions } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page skill-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">SkillWorkspaceView</p>
|
||||
<h1>Skill 编辑与使用</h1>
|
||||
</div>
|
||||
<el-button type="primary">测试 Skill</el-button>
|
||||
</header>
|
||||
|
||||
<div class="skill-layout">
|
||||
<section class="studio-panel skill-editor">
|
||||
<div class="panel-heading">
|
||||
<h2>引用审校 Skill</h2>
|
||||
<span>PUT /api/skills/skill-citation/draft</span>
|
||||
</div>
|
||||
<div class="editor-tabs">
|
||||
<button class="active">Prompt</button>
|
||||
<button>Code</button>
|
||||
<button>Config</button>
|
||||
</div>
|
||||
<pre class="prompt-editor">你是回答审校器。请检查答案是否完整引用知识库切片,并输出:
|
||||
1. answer_quality
|
||||
2. missing_citations
|
||||
3. rewrite_suggestion</pre>
|
||||
<div class="variable-grid">
|
||||
<label>变量 <strong>answer</strong></label>
|
||||
<label>变量 <strong>citations[]</strong></label>
|
||||
<label>输出 <strong>quality_score</strong></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="studio-panel test-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>测试面板</h2>
|
||||
<span>POST /api/skills/skill-citation/test</span>
|
||||
</div>
|
||||
<div class="test-result">
|
||||
<strong>quality_score: 0.86</strong>
|
||||
<p>建议补充“日志留存周期”的引用来源,并将私有化部署边界写得更明确。</p>
|
||||
</div>
|
||||
<div class="version-list">
|
||||
<article v-for="version in skillVersions" :key="version.version">
|
||||
<strong>{{ version.version }}</strong>
|
||||
<span>{{ version.status }}</span>
|
||||
<em>{{ version.note }} · {{ version.updatedAt }}</em>
|
||||
</article>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
99
frontend/src/pages/studio/StudioDashboardPage.vue
Normal file
99
frontend/src/pages/studio/StudioDashboardPage.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowRight, Check, Warning } from '@element-plus/icons-vue';
|
||||
|
||||
import { lifecycleSteps, readinessChecklist, recentRuns } from '@/data/studioMock';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page dashboard-page">
|
||||
<header class="studio-hero">
|
||||
<div>
|
||||
<p class="studio-kicker">项目 / Common Agent Studio</p>
|
||||
<h1>从知识接入到 Agent 发布的一体化工作台</h1>
|
||||
<p>
|
||||
使用新的聚合 ViewModel 驱动原型:知识资产、Workflow、MCP、Skill、Agent 调试与观测都围绕一次发布旅程组织。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-button type="primary">新建 Workflow</el-button>
|
||||
<el-button>导入 MCP</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="lifecycle-strip">
|
||||
<article v-for="(step, index) in lifecycleSteps" :key="step.name" class="lifecycle-step" :class="`is-${step.status}`">
|
||||
<div class="step-index">{{ index + 1 }}</div>
|
||||
<div>
|
||||
<strong>{{ step.name }}</strong>
|
||||
<span>{{ step.description }}</span>
|
||||
</div>
|
||||
<el-icon v-if="index < lifecycleSteps.length - 1"><ArrowRight /></el-icon>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<section class="studio-panel readiness-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>发布就绪检查</h2>
|
||||
<span>ViewModel: StudioDashboardView</span>
|
||||
</div>
|
||||
<el-tag type="warning">Draft</el-tag>
|
||||
</div>
|
||||
<ul class="check-list">
|
||||
<li v-for="item in readinessChecklist" :key="item.label" :class="{ done: item.done }">
|
||||
<el-icon>
|
||||
<Check v-if="item.done" />
|
||||
<span v-else class="pending-dot" />
|
||||
</el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel metrics-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>运行概览</h2>
|
||||
<span>环境: Dev</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<div><strong>27</strong><span>今日运行</span></div>
|
||||
<div><strong>96.4%</strong><span>成功率</span></div>
|
||||
<div><strong>1.28s</strong><span>P50 延迟</span></div>
|
||||
<div><strong>¥4.82</strong><span>预估成本</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel recent-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>最近运行</h2>
|
||||
<span>Run Trace</span>
|
||||
</div>
|
||||
<div class="run-table">
|
||||
<div class="run-row run-head">
|
||||
<span>名称</span><span>类型</span><span>状态</span><span>延迟</span><span>成本</span>
|
||||
</div>
|
||||
<div v-for="run in recentRuns" :key="run.id" class="run-row">
|
||||
<strong>{{ run.name }}</strong>
|
||||
<span>{{ run.type }}</span>
|
||||
<span class="status-cell">
|
||||
<span class="status-pill" :class="run.status === '成功' ? 'is-success' : 'is-warning'">
|
||||
{{ run.status }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ run.latency }}</span>
|
||||
<span>{{ run.cost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="studio-panel warning-panel">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<div>
|
||||
<h2>生产发布前仍需确认路由兜底</h2>
|
||||
<p>AGENT_PLAN 任务当前只有草稿路由,建议补齐 fallback 模型和最大延迟阈值。</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
112
frontend/src/pages/studio/WorkflowBuilderPage.vue
Normal file
112
frontend/src/pages/studio/WorkflowBuilderPage.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { Connection, Cpu, VideoPlay } from '@element-plus/icons-vue';
|
||||
|
||||
import { traceSteps, workflowEdges, workflowNodes } from '@/data/studioMock';
|
||||
|
||||
const nodeById = Object.fromEntries(workflowNodes.map((node) => [node.id, node]));
|
||||
const canvasEdges = workflowEdges.flatMap((edge) => {
|
||||
const from = nodeById[edge.from];
|
||||
const to = nodeById[edge.to];
|
||||
|
||||
if (!from || !to) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${edge.from}-${edge.to}`,
|
||||
x1: from.x + 5,
|
||||
y1: from.y + 4,
|
||||
x2: to.x,
|
||||
y2: to.y + 4,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="studio-page workflow-page">
|
||||
<header class="page-title-row">
|
||||
<div>
|
||||
<p class="studio-kicker">WorkflowBuilderView · Draft / Published</p>
|
||||
<h1>Workflow 图形化编排</h1>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button>保存草稿</el-button>
|
||||
<el-button type="primary"><el-icon><VideoPlay /></el-icon> 运行测试</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workflow-layout">
|
||||
<aside class="studio-panel node-library">
|
||||
<div class="panel-heading compact">
|
||||
<h2>节点库</h2>
|
||||
<span>JSON Graph</span>
|
||||
</div>
|
||||
<button>Start</button>
|
||||
<button>LLM</button>
|
||||
<button>Knowledge Retrieval</button>
|
||||
<button>MCP Tool</button>
|
||||
<button>Skill</button>
|
||||
<button>Condition</button>
|
||||
<button>Answer</button>
|
||||
</aside>
|
||||
|
||||
<main class="studio-panel workflow-canvas">
|
||||
<div class="canvas-toolbar">
|
||||
<span><el-icon><Connection /></el-icon> workflow-support-rag</span>
|
||||
<span>版本快照 v7</span>
|
||||
<span>环境: Dev</span>
|
||||
</div>
|
||||
<div class="canvas-surface">
|
||||
<svg class="edge-layer" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<line
|
||||
v-for="edge in canvasEdges"
|
||||
:key="edge.id"
|
||||
:x1="edge.x1"
|
||||
:y1="edge.y1"
|
||||
:x2="edge.x2"
|
||||
:y2="edge.y2"
|
||||
/>
|
||||
</svg>
|
||||
<article
|
||||
v-for="node in workflowNodes"
|
||||
:key="node.id"
|
||||
class="workflow-node"
|
||||
:class="{ selected: node.id === 'llm' }"
|
||||
:style="{ left: `${node.x}%`, top: `${node.y}%` }"
|
||||
>
|
||||
<span>{{ node.type }}</span>
|
||||
<strong>{{ node.label }}</strong>
|
||||
<em>{{ node.description }}</em>
|
||||
</article>
|
||||
</div>
|
||||
<div class="run-trace-drawer">
|
||||
<strong>Run Trace</strong>
|
||||
<div v-for="step in traceSteps" :key="step.node">
|
||||
<span>{{ step.node }}</span>
|
||||
<em>{{ step.status }} · {{ step.duration }} · {{ step.output }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="studio-panel inspector-panel">
|
||||
<div class="panel-heading compact">
|
||||
<h2>节点 Inspector</h2>
|
||||
<span>LLM</span>
|
||||
</div>
|
||||
<dl class="inspector-list">
|
||||
<dt>任务类型</dt>
|
||||
<dd>RAG_ANSWER</dd>
|
||||
<dt>输入 Schema</dt>
|
||||
<dd>question, retrieved_chunks, conversation</dd>
|
||||
<dt>输出 Schema</dt>
|
||||
<dd>answer, citations, safety_flags</dd>
|
||||
<dt>路由策略</dt>
|
||||
<dd>primary qwen-plus / fallback deepseek-v3</dd>
|
||||
</dl>
|
||||
<button class="blue-command"><el-icon><Cpu /></el-icon> 打开模型路由</button>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
160
frontend/src/pages/system/ModelCallLogsPage.vue
Normal file
160
frontend/src/pages/system/ModelCallLogsPage.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { RefreshRight, Search } from '@element-plus/icons-vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||
import {
|
||||
queryModelCallLogs,
|
||||
queryModelConfigs,
|
||||
queryModelProviders,
|
||||
type ModelCallLog,
|
||||
type ModelConfig,
|
||||
type ModelProvider,
|
||||
} from '@/api/modelProvider';
|
||||
|
||||
const loading = ref(false);
|
||||
const logs = ref<ModelCallLog[]>([]);
|
||||
const providers = ref<ModelProvider[]>([]);
|
||||
const models = ref<ModelConfig[]>([]);
|
||||
const taskTypeOptions = ref<EnumOption[]>([]);
|
||||
const statusOptions = ref<EnumOption[]>([]);
|
||||
const enumError = ref('');
|
||||
|
||||
const queryForm = reactive({
|
||||
taskType: '',
|
||||
providerId: '',
|
||||
modelId: '',
|
||||
status: '',
|
||||
bizType: '',
|
||||
});
|
||||
|
||||
async function loadBaseData(forceRefresh = false) {
|
||||
enumError.value = '';
|
||||
try {
|
||||
const [enumResult, providerResult, modelResult] = await Promise.all([
|
||||
loadModelProviderEnumOptions(forceRefresh),
|
||||
queryModelProviders(),
|
||||
queryModelConfigs(),
|
||||
]);
|
||||
taskTypeOptions.value = enumResult.task_type ?? [];
|
||||
statusOptions.value = enumResult.call_status ?? [];
|
||||
providers.value = providerResult.data ?? [];
|
||||
models.value = modelResult.data ?? [];
|
||||
} catch (error) {
|
||||
enumError.value = error instanceof Error ? error.message : '基础数据加载失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryModelCallLogs({
|
||||
taskType: queryForm.taskType || undefined,
|
||||
providerId: queryForm.providerId || undefined,
|
||||
modelId: queryForm.modelId || undefined,
|
||||
status: queryForm.status || undefined,
|
||||
bizType: queryForm.bizType || undefined,
|
||||
});
|
||||
logs.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
queryForm.taskType = '';
|
||||
queryForm.providerId = '';
|
||||
queryForm.modelId = '';
|
||||
queryForm.status = '';
|
||||
queryForm.bizType = '';
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
function providerName(providerId?: string) {
|
||||
return providers.value.find((item) => item.id === providerId)?.providerName ?? providerId ?? '-';
|
||||
}
|
||||
|
||||
function modelName(modelId?: string) {
|
||||
const model = models.value.find((item) => item.id === modelId);
|
||||
return model?.modelName ?? model?.modelCode ?? modelId ?? '-';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBaseData();
|
||||
await loadLogs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>调用日志</h2>
|
||||
<span>Call Logs</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-alert v-if="enumError" type="error" :closable="false" :title="`基础数据加载失败:${enumError}`" show-icon />
|
||||
<el-form :model="queryForm" inline>
|
||||
<el-form-item label="任务类型">
|
||||
<el-select v-model="queryForm.taskType" clearable placeholder="全部">
|
||||
<el-option v-for="item in taskTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务商">
|
||||
<el-select v-model="queryForm.providerId" clearable placeholder="全部">
|
||||
<el-option
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
:label="provider.providerName"
|
||||
:value="provider.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型">
|
||||
<el-select v-model="queryForm.modelId" clearable placeholder="全部">
|
||||
<el-option v-for="model in models" :key="model.id" :label="model.modelName" :value="model.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryForm.status" clearable placeholder="全部">
|
||||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-input v-model="queryForm.bizType" clearable placeholder="如 RAG" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="loadLogs">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
<el-button :icon="RefreshRight" @click="loadBaseData(true)">重试基础数据</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="logs" row-key="id">
|
||||
<el-table-column prop="requestId" label="请求ID" min-width="150" />
|
||||
<el-table-column label="服务商" min-width="120">
|
||||
<template #default="{ row }">{{ providerName(row.providerId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型" min-width="120">
|
||||
<template #default="{ row }">{{ modelName(row.modelId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="taskType" label="任务类型" min-width="120" />
|
||||
<el-table-column prop="bizType" label="业务类型" min-width="100" />
|
||||
<el-table-column prop="bizId" label="业务ID" min-width="120" />
|
||||
<el-table-column prop="callType" label="调用类型" min-width="100" />
|
||||
<el-table-column prop="status" label="状态" min-width="90" />
|
||||
<el-table-column prop="durationMs" label="耗时(ms)" width="100" />
|
||||
<el-table-column prop="errorCode" label="错误码" min-width="100" />
|
||||
<el-table-column prop="errorMessage" label="错误摘要" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
padding: 16px 22px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
243
frontend/src/pages/system/ModelConfigsPage.vue
Normal file
243
frontend/src/pages/system/ModelConfigsPage.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||
import {
|
||||
deleteModelConfig,
|
||||
queryModelConfigs,
|
||||
queryModelProviders,
|
||||
saveModelConfig,
|
||||
type ModelConfig,
|
||||
type ModelProvider,
|
||||
} from '@/api/modelProvider';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const models = ref<ModelConfig[]>([]);
|
||||
const providers = ref<ModelProvider[]>([]);
|
||||
const modelTypeOptions = ref<EnumOption[]>([]);
|
||||
const enumError = ref('');
|
||||
|
||||
const editForm = reactive<ModelConfig>({
|
||||
providerId: '',
|
||||
modelCode: '',
|
||||
modelName: '',
|
||||
upstreamModel: '',
|
||||
modelType: '',
|
||||
embeddingDimension: 1024,
|
||||
localModel: false,
|
||||
defaultModel: false,
|
||||
optionsJson: '{}',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (editForm.id ? '编辑模型' : '新增模型'));
|
||||
|
||||
function resetForm(row?: ModelConfig) {
|
||||
editForm.id = row?.id;
|
||||
editForm.providerId = row?.providerId ?? providers.value[0]?.id ?? '';
|
||||
editForm.modelCode = row?.modelCode ?? '';
|
||||
editForm.modelName = row?.modelName ?? '';
|
||||
editForm.upstreamModel = row?.upstreamModel ?? '';
|
||||
editForm.modelType = row?.modelType ?? modelTypeOptions.value[0]?.value ?? '';
|
||||
editForm.embeddingDimension = row?.embeddingDimension ?? 1024;
|
||||
editForm.localModel = row?.localModel ?? false;
|
||||
editForm.defaultModel = row?.defaultModel ?? false;
|
||||
editForm.optionsJson = row?.optionsJson ?? '{}';
|
||||
editForm.enabled = row?.enabled ?? true;
|
||||
editForm.remark = row?.remark ?? '';
|
||||
}
|
||||
|
||||
async function loadBaseData(forceRefresh = false) {
|
||||
enumError.value = '';
|
||||
try {
|
||||
const [enumResult, providerResult] = await Promise.all([
|
||||
loadModelProviderEnumOptions(forceRefresh),
|
||||
queryModelProviders(),
|
||||
]);
|
||||
modelTypeOptions.value = enumResult.model_type ?? [];
|
||||
providers.value = providerResult.data ?? [];
|
||||
} catch (error) {
|
||||
enumError.value = error instanceof Error ? error.message : '基础数据加载失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryModelConfigs();
|
||||
models.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: ModelConfig) {
|
||||
resetForm(row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitModel() {
|
||||
if (!editForm.providerId || !editForm.modelCode || !editForm.modelName || !editForm.modelType) {
|
||||
ElMessage.warning('请填写服务商、模型编码、模型名称和模型类型');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveModelConfig({ ...editForm });
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
await loadModels();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeModel(row: ModelConfig) {
|
||||
if (!row.id) {
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除模型「${row.modelName}」?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
await deleteModelConfig(row.id);
|
||||
ElMessage.success('已删除');
|
||||
await loadModels();
|
||||
}
|
||||
|
||||
function providerName(providerId?: string) {
|
||||
const provider = providers.value.find((item) => item.id === providerId);
|
||||
return provider?.providerName ?? providerId ?? '-';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBaseData();
|
||||
resetForm();
|
||||
await loadModels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>模型配置</h2>
|
||||
<span>Models</span>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<el-alert v-if="enumError" type="error" :closable="false" :title="`基础数据加载失败:${enumError}`" show-icon />
|
||||
<div class="toolbar__actions">
|
||||
<el-button @click="loadBaseData(true)">重试基础数据</el-button>
|
||||
<el-button :icon="RefreshRight" @click="loadModels">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增模型</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="models" row-key="id">
|
||||
<el-table-column prop="modelCode" label="模型编码" min-width="140" />
|
||||
<el-table-column prop="modelName" label="模型名称" min-width="140" />
|
||||
<el-table-column label="服务商" min-width="130">
|
||||
<template #default="{ row }">{{ providerName(row.providerId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="upstreamModel" label="上游模型" min-width="140" />
|
||||
<el-table-column prop="modelType" label="模型类型" min-width="110" />
|
||||
<el-table-column prop="embeddingDimension" label="向量维度" width="100" />
|
||||
<el-table-column label="本地模型" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.localModel ? 'success' : 'info'">{{ row.localModel ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认模型" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.defaultModel ? 'success' : 'info'">{{ row.defaultModel ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="removeModel(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="680px">
|
||||
<el-form :model="editForm" label-width="108px">
|
||||
<el-form-item label="服务商" required>
|
||||
<el-select v-model="editForm.providerId" placeholder="请选择服务商">
|
||||
<el-option
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
:label="provider.providerName"
|
||||
:value="provider.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型编码" required>
|
||||
<el-input v-model="editForm.modelCode" placeholder="如 TEXT_EMBED_3L" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称" required>
|
||||
<el-input v-model="editForm.modelName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上游模型">
|
||||
<el-input v-model="editForm.upstreamModel" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型类型" required>
|
||||
<el-select v-model="editForm.modelType" placeholder="请选择">
|
||||
<el-option v-for="item in modelTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="向量维度">
|
||||
<el-input-number v-model="editForm.embeddingDimension" :min="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选项JSON">
|
||||
<el-input v-model="editForm.optionsJson" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="本地模型">
|
||||
<el-switch v-model="editForm.localModel" />
|
||||
</el-form-item>
|
||||
<el-form-item label="默认模型">
|
||||
<el-switch v-model="editForm.defaultModel" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="editForm.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitModel">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 22px;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
278
frontend/src/pages/system/ModelProvidersPage.vue
Normal file
278
frontend/src/pages/system/ModelProvidersPage.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<script setup lang="ts">
|
||||
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||
import {
|
||||
checkModelProviderHealth,
|
||||
deleteModelProvider,
|
||||
queryModelProviders,
|
||||
saveModelProvider,
|
||||
type ModelProvider,
|
||||
} from '@/api/modelProvider';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const checkingId = ref<string | null>(null);
|
||||
const dialogVisible = ref(false);
|
||||
const providers = ref<ModelProvider[]>([]);
|
||||
const enumLoading = ref(false);
|
||||
const enumError = ref('');
|
||||
const providerTypeOptions = ref<EnumOption[]>([]);
|
||||
const protocolTypeOptions = ref<EnumOption[]>([]);
|
||||
const authTypeOptions = ref<EnumOption[]>([]);
|
||||
const healthStatusOptions = ref<EnumOption[]>([]);
|
||||
|
||||
const editForm = reactive<ModelProvider>({
|
||||
providerCode: '',
|
||||
providerName: '',
|
||||
providerType: '',
|
||||
protocolType: '',
|
||||
baseUrl: '',
|
||||
authType: '',
|
||||
secretRef: '',
|
||||
timeoutMs: 60000,
|
||||
priority: 100,
|
||||
enabled: true,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (editForm.id ? '编辑服务商' : '新增服务商'));
|
||||
|
||||
function resetForm(row?: ModelProvider) {
|
||||
editForm.id = row?.id;
|
||||
editForm.providerCode = row?.providerCode ?? '';
|
||||
editForm.providerName = row?.providerName ?? '';
|
||||
editForm.providerType = row?.providerType ?? providerTypeOptions.value[0]?.value ?? '';
|
||||
editForm.protocolType = row?.protocolType ?? protocolTypeOptions.value[0]?.value ?? '';
|
||||
editForm.baseUrl = row?.baseUrl ?? '';
|
||||
editForm.authType = row?.authType ?? authTypeOptions.value[0]?.value ?? '';
|
||||
editForm.secretRef = row?.secretRef ?? '';
|
||||
editForm.timeoutMs = row?.timeoutMs ?? 60000;
|
||||
editForm.priority = row?.priority ?? 100;
|
||||
editForm.enabled = row?.enabled ?? true;
|
||||
editForm.remark = row?.remark ?? '';
|
||||
}
|
||||
|
||||
async function loadEnums(forceRefresh = false) {
|
||||
enumLoading.value = true;
|
||||
enumError.value = '';
|
||||
try {
|
||||
const result = await loadModelProviderEnumOptions(forceRefresh);
|
||||
providerTypeOptions.value = result.provider_type ?? [];
|
||||
protocolTypeOptions.value = result.protocol_type ?? [];
|
||||
authTypeOptions.value = result.auth_type ?? [];
|
||||
healthStatusOptions.value = result.health_status ?? [];
|
||||
} catch (error) {
|
||||
enumError.value = error instanceof Error ? error.message : '枚举加载失败';
|
||||
} finally {
|
||||
enumLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryModelProviders();
|
||||
providers.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: ModelProvider) {
|
||||
resetForm(row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitProvider() {
|
||||
if (!editForm.providerCode || !editForm.providerName || !editForm.baseUrl) {
|
||||
ElMessage.warning('请填写服务商编码、服务商名称和基础地址');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveModelProvider({ ...editForm });
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
await loadProviders();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeProvider(row: ModelProvider) {
|
||||
if (!row.id) {
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除服务商「${row.providerName}」?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
await deleteModelProvider(row.id);
|
||||
ElMessage.success('已删除');
|
||||
await loadProviders();
|
||||
}
|
||||
|
||||
async function checkHealth(row: ModelProvider) {
|
||||
if (!row.id) {
|
||||
return;
|
||||
}
|
||||
checkingId.value = row.id;
|
||||
try {
|
||||
await checkModelProviderHealth(row.id);
|
||||
ElMessage.success('健康检查已完成');
|
||||
await loadProviders();
|
||||
} finally {
|
||||
checkingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function healthLabel(value?: string) {
|
||||
const option = healthStatusOptions.value.find((item) => item.value === value);
|
||||
return option?.label ?? value ?? '-';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEnums();
|
||||
resetForm();
|
||||
await loadProviders();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>模型服务商</h2>
|
||||
<span>Providers</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="toolbar__left">
|
||||
<el-alert
|
||||
v-if="enumError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
:title="`枚举加载失败:${enumError}`"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<div class="toolbar__actions">
|
||||
<el-button :loading="enumLoading" @click="loadEnums(true)">重试枚举</el-button>
|
||||
<el-button :icon="RefreshRight" @click="loadProviders">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增服务商</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="providers" row-key="id">
|
||||
<el-table-column prop="providerCode" label="服务商编码" min-width="140" />
|
||||
<el-table-column prop="providerName" label="服务商名称" min-width="140" />
|
||||
<el-table-column prop="providerType" label="服务商类型" min-width="120" />
|
||||
<el-table-column prop="protocolType" label="协议类型" min-width="130" />
|
||||
<el-table-column prop="authType" label="鉴权类型" min-width="120" />
|
||||
<el-table-column prop="secretRef" label="密钥引用" min-width="140" />
|
||||
<el-table-column label="已配置密钥" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.hasApiKey ? 'success' : 'info'">{{ row.hasApiKey ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="健康状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ healthLabel(row.healthStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:loading="checkingId === row.id"
|
||||
@click="checkHealth(row)"
|
||||
>
|
||||
健康检查
|
||||
</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="removeProvider(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px">
|
||||
<el-form :model="editForm" label-width="108px">
|
||||
<el-form-item label="服务商编码" required>
|
||||
<el-input v-model="editForm.providerCode" placeholder="如 OPENAI_MAIN" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务商名称" required>
|
||||
<el-input v-model="editForm.providerName" placeholder="如 OpenAI 主账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务商类型">
|
||||
<el-select v-model="editForm.providerType" placeholder="请选择">
|
||||
<el-option v-for="item in providerTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议类型">
|
||||
<el-select v-model="editForm.protocolType" placeholder="请选择">
|
||||
<el-option v-for="item in protocolTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="基础地址" required>
|
||||
<el-input v-model="editForm.baseUrl" placeholder="https://api.example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="鉴权类型">
|
||||
<el-select v-model="editForm.authType" placeholder="请选择">
|
||||
<el-option v-for="item in authTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="密钥引用">
|
||||
<el-input v-model="editForm.secretRef" placeholder="如 OPENAI_API_KEY" />
|
||||
</el-form-item>
|
||||
<el-form-item label="超时(毫秒)">
|
||||
<el-input-number v-model="editForm.timeoutMs" :min="1000" :step="1000" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级">
|
||||
<el-input-number v-model="editForm.priority" :min="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="editForm.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitProvider">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 22px;
|
||||
}
|
||||
|
||||
.toolbar__left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
245
frontend/src/pages/system/ModelRouteRulesPage.vue
Normal file
245
frontend/src/pages/system/ModelRouteRulesPage.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import { Delete, Edit, Plus, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||||
import {
|
||||
deleteModelRouteRule,
|
||||
queryModelConfigs,
|
||||
queryModelRouteRules,
|
||||
saveModelRouteRule,
|
||||
type ModelConfig,
|
||||
type ModelRouteRule,
|
||||
} from '@/api/modelProvider';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const routes = ref<ModelRouteRule[]>([]);
|
||||
const models = ref<ModelConfig[]>([]);
|
||||
const taskTypeOptions = ref<EnumOption[]>([]);
|
||||
const routeStrategyOptions = ref<EnumOption[]>([]);
|
||||
const matchScopeOptions = ref<EnumOption[]>([]);
|
||||
const enumError = ref('');
|
||||
|
||||
const editForm = reactive<ModelRouteRule>({
|
||||
routeCode: '',
|
||||
routeName: '',
|
||||
taskType: '',
|
||||
matchScope: '',
|
||||
scopeId: '',
|
||||
primaryModelId: '',
|
||||
fallbackModelIdsJson: '[]',
|
||||
routeStrategy: '',
|
||||
maxLatencyMs: 0,
|
||||
enabled: true,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (editForm.id ? '编辑路由规则' : '新增路由规则'));
|
||||
|
||||
function resetForm(row?: ModelRouteRule) {
|
||||
editForm.id = row?.id;
|
||||
editForm.routeCode = row?.routeCode ?? '';
|
||||
editForm.routeName = row?.routeName ?? '';
|
||||
editForm.taskType = row?.taskType ?? taskTypeOptions.value[0]?.value ?? '';
|
||||
editForm.matchScope = row?.matchScope ?? matchScopeOptions.value[0]?.value ?? '';
|
||||
editForm.scopeId = row?.scopeId ?? '';
|
||||
editForm.primaryModelId = row?.primaryModelId ?? models.value[0]?.id ?? '';
|
||||
editForm.fallbackModelIdsJson = row?.fallbackModelIdsJson ?? '[]';
|
||||
editForm.routeStrategy = row?.routeStrategy ?? routeStrategyOptions.value[0]?.value ?? '';
|
||||
editForm.maxLatencyMs = row?.maxLatencyMs ?? 0;
|
||||
editForm.enabled = row?.enabled ?? true;
|
||||
editForm.remark = row?.remark ?? '';
|
||||
}
|
||||
|
||||
async function loadBaseData(forceRefresh = false) {
|
||||
enumError.value = '';
|
||||
try {
|
||||
const [enumResult, modelResult] = await Promise.all([
|
||||
loadModelProviderEnumOptions(forceRefresh),
|
||||
queryModelConfigs(),
|
||||
]);
|
||||
taskTypeOptions.value = enumResult.task_type ?? [];
|
||||
routeStrategyOptions.value = enumResult.route_strategy ?? [];
|
||||
matchScopeOptions.value = enumResult.match_scope ?? [];
|
||||
models.value = modelResult.data ?? [];
|
||||
} catch (error) {
|
||||
enumError.value = error instanceof Error ? error.message : '基础数据加载失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRouteRules() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryModelRouteRules();
|
||||
routes.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row: ModelRouteRule) {
|
||||
resetForm(row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitRouteRule() {
|
||||
if (!editForm.routeCode || !editForm.taskType || !editForm.primaryModelId) {
|
||||
ElMessage.warning('请填写路由编码、任务类型和主模型');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSON.parse(editForm.fallbackModelIdsJson ?? '[]');
|
||||
} catch {
|
||||
ElMessage.warning('降级模型JSON格式不正确');
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveModelRouteRule({ ...editForm });
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
await loadRouteRules();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRouteRule(row: ModelRouteRule) {
|
||||
if (!row.id) {
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除路由规则「${row.routeName || row.routeCode}」?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
await deleteModelRouteRule(row.id);
|
||||
ElMessage.success('已删除');
|
||||
await loadRouteRules();
|
||||
}
|
||||
|
||||
function modelLabel(modelId?: string) {
|
||||
const model = models.value.find((item) => item.id === modelId);
|
||||
return model?.modelName ?? model?.modelCode ?? modelId ?? '-';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBaseData();
|
||||
resetForm();
|
||||
await loadRouteRules();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>路由规则</h2>
|
||||
<span>Routes</span>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<el-alert v-if="enumError" type="error" :closable="false" :title="`基础数据加载失败:${enumError}`" show-icon />
|
||||
<div class="toolbar__actions">
|
||||
<el-button @click="loadBaseData(true)">重试基础数据</el-button>
|
||||
<el-button :icon="RefreshRight" @click="loadRouteRules">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增规则</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="routes" row-key="id">
|
||||
<el-table-column prop="routeCode" label="规则编码" min-width="140" />
|
||||
<el-table-column prop="routeName" label="规则名称" min-width="140" />
|
||||
<el-table-column prop="taskType" label="任务类型" min-width="120" />
|
||||
<el-table-column prop="matchScope" label="匹配范围" min-width="100" />
|
||||
<el-table-column label="主模型" min-width="140">
|
||||
<template #default="{ row }">{{ modelLabel(row.primaryModelId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fallbackModelIdsJson" label="降级模型JSON" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="routeStrategy" label="策略" min-width="100" />
|
||||
<el-table-column prop="maxLatencyMs" label="最大延迟(ms)" width="120" />
|
||||
<el-table-column label="启用" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="removeRouteRule(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="680px">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="规则编码" required>
|
||||
<el-input v-model="editForm.routeCode" placeholder="如 RAG_EMBEDDING_GLOBAL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规则名称">
|
||||
<el-input v-model="editForm.routeName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" required>
|
||||
<el-select v-model="editForm.taskType">
|
||||
<el-option v-for="item in taskTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="匹配范围">
|
||||
<el-select v-model="editForm.matchScope">
|
||||
<el-option v-for="item in matchScopeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="范围业务ID">
|
||||
<el-input v-model="editForm.scopeId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="主模型" required>
|
||||
<el-select v-model="editForm.primaryModelId">
|
||||
<el-option v-for="item in models" :key="item.id" :label="`${item.modelName}(${item.modelCode})`" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="降级模型JSON">
|
||||
<el-input v-model="editForm.fallbackModelIdsJson" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路由策略">
|
||||
<el-select v-model="editForm.routeStrategy">
|
||||
<el-option v-for="item in routeStrategyOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大延迟(ms)">
|
||||
<el-input-number v-model="editForm.maxLatencyMs" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="editForm.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitRouteRule">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 22px;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>附件管理</h2>
|
||||
<span>Files</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -3,15 +3,19 @@ import { describe, expect, it } from 'vitest';
|
||||
import { routes } from '../index';
|
||||
|
||||
describe('router', () => {
|
||||
it('defines the admin shell routes', () => {
|
||||
it('defines the Studio product routes', () => {
|
||||
const paths = routes.map((route) => route.path);
|
||||
|
||||
expect(paths).toContain('/');
|
||||
expect(paths).toContain('/dashboard');
|
||||
expect(paths).toContain('/system/enums');
|
||||
expect(paths).toContain('/system/attachments');
|
||||
expect(paths).toContain('/rag/stores');
|
||||
expect(paths).toContain('/rag/documents');
|
||||
expect(paths).toContain('/studio');
|
||||
expect(paths).toContain('/knowledge');
|
||||
expect(paths).toContain('/knowledge/ingestion');
|
||||
expect(paths).toContain('/workflows');
|
||||
expect(paths).toContain('/agents');
|
||||
expect(paths).toContain('/mcp');
|
||||
expect(paths).toContain('/skills');
|
||||
expect(paths).toContain('/models');
|
||||
expect(paths).toContain('/observability');
|
||||
expect(paths).toContain('/:pathMatch(.*)*');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,48 +1,76 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import DashboardPage from '@/pages/dashboard/DashboardPage.vue';
|
||||
import NotFoundPage from '@/pages/common/NotFoundPage.vue';
|
||||
import RagDocumentsPage from '@/pages/rag/RagDocumentsPage.vue';
|
||||
import RagStoresPage from '@/pages/rag/RagStoresPage.vue';
|
||||
import SystemAttachmentsPage from '@/pages/system/SystemAttachmentsPage.vue';
|
||||
import SystemEnumsPage from '@/pages/system/SystemEnumsPage.vue';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import NotFoundPage from '@/pages/common/NotFoundPage.vue';
|
||||
import AgentWorkspacePage from '@/pages/studio/AgentWorkspacePage.vue';
|
||||
import IngestionPipelinePage from '@/pages/studio/IngestionPipelinePage.vue';
|
||||
import KnowledgeWorkspacePage from '@/pages/studio/KnowledgeWorkspacePage.vue';
|
||||
import McpImportPage from '@/pages/studio/McpImportPage.vue';
|
||||
import ModelWorkspacePage from '@/pages/studio/ModelWorkspacePage.vue';
|
||||
import ObservabilityPage from '@/pages/studio/ObservabilityPage.vue';
|
||||
import SkillWorkspacePage from '@/pages/studio/SkillWorkspacePage.vue';
|
||||
import StudioDashboardPage from '@/pages/studio/StudioDashboardPage.vue';
|
||||
import WorkflowBuilderPage from '@/pages/studio/WorkflowBuilderPage.vue';
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard',
|
||||
redirect: '/studio',
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: DashboardPage,
|
||||
path: '/studio',
|
||||
name: 'studio-dashboard',
|
||||
component: StudioDashboardPage,
|
||||
meta: { title: '工作台' },
|
||||
},
|
||||
{
|
||||
path: '/system/enums',
|
||||
name: 'system-enums',
|
||||
component: SystemEnumsPage,
|
||||
meta: { title: '系统枚举' },
|
||||
path: '/knowledge',
|
||||
name: 'knowledge-workspace',
|
||||
component: KnowledgeWorkspacePage,
|
||||
meta: { title: '知识资产' },
|
||||
},
|
||||
{
|
||||
path: '/system/attachments',
|
||||
name: 'system-attachments',
|
||||
component: SystemAttachmentsPage,
|
||||
meta: { title: '附件管理' },
|
||||
path: '/knowledge/ingestion',
|
||||
name: 'knowledge-ingestion',
|
||||
component: IngestionPipelinePage,
|
||||
meta: { title: '文件解析' },
|
||||
},
|
||||
{
|
||||
path: '/rag/stores',
|
||||
name: 'rag-stores',
|
||||
component: RagStoresPage,
|
||||
meta: { title: '知识库' },
|
||||
path: '/workflows',
|
||||
name: 'workflow-builder',
|
||||
component: WorkflowBuilderPage,
|
||||
meta: { title: 'Workflow' },
|
||||
},
|
||||
{
|
||||
path: '/rag/documents',
|
||||
name: 'rag-documents',
|
||||
component: RagDocumentsPage,
|
||||
meta: { title: '知识文档' },
|
||||
path: '/agents',
|
||||
name: 'agent-workspace',
|
||||
component: AgentWorkspacePage,
|
||||
meta: { title: 'Agent 对话' },
|
||||
},
|
||||
{
|
||||
path: '/mcp',
|
||||
name: 'mcp-import',
|
||||
component: McpImportPage,
|
||||
meta: { title: 'MCP 导入' },
|
||||
},
|
||||
{
|
||||
path: '/skills',
|
||||
name: 'skill-workspace',
|
||||
component: SkillWorkspacePage,
|
||||
meta: { title: 'Skill 编辑' },
|
||||
},
|
||||
{
|
||||
path: '/models',
|
||||
name: 'model-workspace',
|
||||
component: ModelWorkspacePage,
|
||||
meta: { title: '模型路由' },
|
||||
},
|
||||
{
|
||||
path: '/observability',
|
||||
name: 'observability',
|
||||
component: ObservabilityPage,
|
||||
meta: { title: '观测' },
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
@@ -53,49 +81,14 @@ export const routes: RouteRecordRaw[] = [
|
||||
];
|
||||
|
||||
const routerRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: AdminLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'dashboard',
|
||||
component: DashboardPage,
|
||||
meta: { title: '工作台' },
|
||||
},
|
||||
{
|
||||
path: 'system/enums',
|
||||
name: 'system-enums',
|
||||
component: SystemEnumsPage,
|
||||
meta: { title: '系统枚举' },
|
||||
},
|
||||
{
|
||||
path: 'system/attachments',
|
||||
name: 'system-attachments',
|
||||
component: SystemAttachmentsPage,
|
||||
meta: { title: '附件管理' },
|
||||
},
|
||||
{
|
||||
path: 'rag/stores',
|
||||
name: 'rag-stores',
|
||||
component: RagStoresPage,
|
||||
meta: { title: '知识库' },
|
||||
},
|
||||
{
|
||||
path: 'rag/documents',
|
||||
name: 'rag-documents',
|
||||
component: RagDocumentsPage,
|
||||
meta: { title: '知识文档' },
|
||||
},
|
||||
],
|
||||
children: routes.filter((route) => route.path !== '/:pathMatch(.*)*'),
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
name: 'not-found-shell',
|
||||
component: NotFoundPage,
|
||||
meta: { title: '页面不存在' },
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# 知识库页面后端接口清单
|
||||
|
||||
本文对应前端页面:[RagStoresPage.vue](/D:/Code/common_agent/frontend/src/pages/RagStoresPage.vue)
|
||||
本文对应前端页面:[RagStoresPage.vue](frontend/src/pages/rag/RagStoresPage.vue) 和 [RagDocumentsPage.vue](frontend/src/pages/rag/RagDocumentsPage.vue)。
|
||||
|
||||
## 1. 页面目标
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
- 右侧当前知识库详情
|
||||
- 当前知识库级别操作:编辑、批量导入文件、重建索引
|
||||
|
||||
因此接口建议拆成 `全局概览`、`知识库列表/详情`、`单库动作` 三组。
|
||||
因此接口拆成 `全局概览`、`知识库列表/详情`、`文档管理`、`后续索引动作` 四组。
|
||||
|
||||
## 2. 本批已实现并已用于前端联调的接口
|
||||
|
||||
### 2.1 查询全部知识库
|
||||
|
||||
- `GET /api/rag/stores`
|
||||
- `POST /api/rag/store/list`
|
||||
|
||||
当前返回类型:
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
|
||||
对应代码:
|
||||
|
||||
- [RagStoreController.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/controller/RagStoreController.java)
|
||||
- [RagStoreResponse.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java)
|
||||
- `src/main/java/com/bruce/rag/controller/RagStoreController.java`
|
||||
- `src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java`
|
||||
|
||||
### 2.2 按条件查询知识库
|
||||
|
||||
- `POST /api/rag/stores/query`
|
||||
- `POST /api/rag/store/query`
|
||||
|
||||
请求体:
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
|
||||
对应代码:
|
||||
|
||||
- [RagStoreQueryRequest.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/request/RagStoreQueryRequest.java)
|
||||
- `src/main/java/com/bruce/rag/dto/request/RagStoreQueryRequest.java`
|
||||
|
||||
### 2.3 查询知识库详情
|
||||
|
||||
- `GET /api/rag/stores/{id}`
|
||||
- `GET /api/rag/store/detail?id={id}`
|
||||
|
||||
返回类型:
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
### 2.4 新增或修改知识库
|
||||
|
||||
- `POST /api/rag/stores`
|
||||
- `POST /api/rag/store/save`
|
||||
|
||||
请求体:
|
||||
|
||||
@@ -97,17 +97,50 @@
|
||||
|
||||
### 2.5 删除知识库
|
||||
|
||||
- `DELETE /api/rag/stores/{id}`
|
||||
- `POST /api/rag/store/delete?id={id}`
|
||||
|
||||
返回类型:
|
||||
|
||||
- `RequestResult<Boolean>`
|
||||
|
||||
## 3. 当前项目里已有但本批前端未联调的接口
|
||||
### 2.6 知识库总览统计
|
||||
|
||||
- `GET /api/rag/store/overview`
|
||||
|
||||
返回类型:
|
||||
|
||||
- `RequestResult<RagStoreOverviewResponse>`
|
||||
|
||||
字段:
|
||||
|
||||
- `totalStores`
|
||||
- `totalDocuments`
|
||||
- `totalChunks`(当前为 `null`,待切片入库后统计)
|
||||
- `retrievableStores`
|
||||
|
||||
### 2.7 知识库文档概览
|
||||
|
||||
- `GET /api/rag/store/documentOverview?storeId={storeId}`
|
||||
|
||||
返回类型:
|
||||
|
||||
- `RequestResult<RagStoreDocumentOverviewResponse>`
|
||||
|
||||
字段:
|
||||
|
||||
- `storeId`
|
||||
- `storeName`
|
||||
- `documentCount`
|
||||
- `enabledDocumentCount`
|
||||
- `parsedDocumentCount`
|
||||
- `indexedDocumentCount`
|
||||
- `lastUploadTime`
|
||||
|
||||
## 3. 当前文档管理接口
|
||||
|
||||
### 3.1 查询全部知识文档
|
||||
|
||||
- `GET /api/rag/documents`
|
||||
- `POST /api/rag/documents/list`
|
||||
|
||||
### 3.2 按条件查询知识文档
|
||||
|
||||
@@ -117,50 +150,92 @@
|
||||
|
||||
对应代码:
|
||||
|
||||
- [RagDocumentController.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/controller/RagDocumentController.java)
|
||||
- [RagDocumentQueryRequest.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/request/RagDocumentQueryRequest.java)
|
||||
- `src/main/java/com/bruce/rag/controller/RagDocumentController.java`
|
||||
- `src/main/java/com/bruce/rag/dto/request/RagDocumentQueryRequest.java`
|
||||
|
||||
## 4. 下一批建议补充的接口
|
||||
### 3.3 查询知识文档详情
|
||||
|
||||
当前已有接口能支撑最基础的列表查询,但还不足以支撑统计卡片、右侧详情聚合和单库动作。建议补下面几个接口。
|
||||
- `GET /api/rag/documents/detail?id={id}`
|
||||
|
||||
### 4.1 知识库总览统计
|
||||
### 3.4 新增或修改知识文档
|
||||
|
||||
- `GET /api/rag/stores/overview`
|
||||
- `POST /api/rag/documents/save`
|
||||
|
||||
用途:
|
||||
### 3.5 删除知识文档
|
||||
|
||||
- 顶部 4 张卡片数据
|
||||
- `POST /api/rag/documents/delete?id={id}`
|
||||
|
||||
返回建议:
|
||||
### 3.6 批量上传文档到知识库
|
||||
|
||||
- `POST /api/rag/documents/batchUpload`
|
||||
|
||||
请求类型:
|
||||
|
||||
- `multipart/form-data`
|
||||
|
||||
表单字段:
|
||||
|
||||
- `storeId`:知识库 ID
|
||||
- `sourceType`:默认为 `RAG`
|
||||
- `files`:文件数组
|
||||
- `documentSummary`:可选,批量设置文档摘要
|
||||
- `remark`:可选
|
||||
|
||||
当前行为:
|
||||
|
||||
- 逐个调用附件上传服务,写入 `sys_attachment`
|
||||
- 自动创建 `rag_document`
|
||||
- 新文档默认 `parseStatus=UPLOADED`、`indexStatus=PENDING`、`enabled=true`
|
||||
|
||||
### 3.7 批量解析知识文档
|
||||
|
||||
- `POST /api/rag/documents/parse`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"resultcode": "0",
|
||||
"message": null,
|
||||
"data": {
|
||||
"storeCount": 12,
|
||||
"documentCount": 1286,
|
||||
"chunkCount": 24390,
|
||||
"retrievableStoreCount": 9
|
||||
}
|
||||
"documentIds": [1, 2]
|
||||
}
|
||||
```
|
||||
|
||||
建议响应 DTO:
|
||||
当前行为:
|
||||
|
||||
- `RagStoreOverviewResponse`
|
||||
- 根据附件后缀或 content type 选择 Tika 解析器
|
||||
- 解析结果写入 `rag_document_parse_result`
|
||||
- 解析成功后更新 `parseStatus=PARSED`
|
||||
- 解析失败后更新 `parseStatus=FAILED` 和 `errorMessage`
|
||||
|
||||
字段建议:
|
||||
### 3.8 批量切片知识文档
|
||||
|
||||
- `storeCount`
|
||||
- `documentCount`
|
||||
- `chunkCount`
|
||||
- `retrievableStoreCount`
|
||||
- `POST /api/rag/documents/chunk`
|
||||
|
||||
### 4.2 知识库列表查询增强版
|
||||
请求体:
|
||||
|
||||
- `POST /api/rag/stores/manage/query`
|
||||
```json
|
||||
{
|
||||
"documentIds": [1, 2],
|
||||
"chunkStrategy": 1,
|
||||
"chunkSize": 800,
|
||||
"chunkOverlap": 120,
|
||||
"delimiter": "。"
|
||||
}
|
||||
```
|
||||
|
||||
当前行为:
|
||||
|
||||
- `chunkStrategy` 使用 `RagChunkStrategyEnum` 的整型枚举值,例如 `1` 表示固定长度切片,`5` 表示按分隔符切片。
|
||||
- 只处理已经存在解析快照的文档。
|
||||
- 按策略生成 `rag_chunk`,写入前会替换该文档已有切片。
|
||||
- 当前尚未调用 Embedding 模型写入 `rag_chunk_embedding`。
|
||||
|
||||
## 4. 下一批建议补充的接口
|
||||
|
||||
当前已有接口能支撑知识库、文档、上传、解析和手动切片入口。下一批建议聚焦向量索引、模型配置和任务化。
|
||||
|
||||
### 4.1 知识库列表查询增强版
|
||||
|
||||
- `POST /api/rag/store/manage/query`
|
||||
|
||||
用途:
|
||||
|
||||
@@ -216,9 +291,9 @@
|
||||
- `retrievable`
|
||||
- `updateTime`
|
||||
|
||||
### 4.3 查询单个知识库详情增强版
|
||||
### 4.2 查询单个知识库详情增强版
|
||||
|
||||
- `GET /api/rag/stores/{id}/detail`
|
||||
- `GET /api/rag/store/detailPlus?id={id}`
|
||||
|
||||
用途:
|
||||
|
||||
@@ -258,86 +333,9 @@
|
||||
|
||||
- `RagStoreDetailResponse`
|
||||
|
||||
### 4.4 新建知识库独立接口
|
||||
### 4.3 发起当前知识库重建索引
|
||||
|
||||
- `POST /api/rag/stores`
|
||||
|
||||
请求体建议:
|
||||
|
||||
```json
|
||||
{
|
||||
"storeCode": "PROD_DOC",
|
||||
"storeName": "产品制度库",
|
||||
"description": "产品制度、业务规范、流程材料"
|
||||
}
|
||||
```
|
||||
|
||||
返回建议:
|
||||
|
||||
- 返回新建后的 `id` 或完整 `RagStoreDetailResponse`
|
||||
|
||||
建议请求 DTO:
|
||||
|
||||
- `RagStoreSaveRequest`
|
||||
|
||||
### 4.5 编辑知识库独立接口
|
||||
|
||||
- `PUT /api/rag/stores/{id}`
|
||||
|
||||
请求体建议:
|
||||
|
||||
```json
|
||||
{
|
||||
"storeCode": "PROD_DOC",
|
||||
"storeName": "产品制度库",
|
||||
"description": "产品制度、业务规范、流程材料",
|
||||
"status": "ENABLED"
|
||||
}
|
||||
```
|
||||
|
||||
用途:
|
||||
|
||||
- 右侧“编辑”按钮
|
||||
|
||||
### 4.6 当前知识库批量导入文件
|
||||
|
||||
- `POST /api/rag/stores/{id}/documents/import`
|
||||
|
||||
用途:
|
||||
|
||||
- 右侧“批量导入文件”按钮
|
||||
|
||||
建议请求类型:
|
||||
|
||||
- `multipart/form-data`
|
||||
|
||||
表单字段建议:
|
||||
|
||||
- `files`: 文件数组
|
||||
- `remark`: 批次备注,可选
|
||||
|
||||
返回建议:
|
||||
|
||||
```json
|
||||
{
|
||||
"resultcode": "0",
|
||||
"message": null,
|
||||
"data": {
|
||||
"taskId": 1001,
|
||||
"storeId": 1,
|
||||
"fileCount": 12,
|
||||
"status": "PROCESSING"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
建议响应 DTO:
|
||||
|
||||
- `RagImportTaskResponse`
|
||||
|
||||
### 4.7 发起当前知识库重建索引
|
||||
|
||||
- `POST /api/rag/stores/{id}/reindex`
|
||||
- `POST /api/rag/store/reindex?storeId={id}`
|
||||
|
||||
用途:
|
||||
|
||||
@@ -365,9 +363,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 4.8 查询当前知识库最近任务
|
||||
### 4.4 查询当前知识库最近任务
|
||||
|
||||
- `GET /api/rag/stores/{id}/tasks?limit=10`
|
||||
- `GET /api/rag/store/tasks?storeId={id}&limit=10`
|
||||
|
||||
用途:
|
||||
|
||||
@@ -404,31 +402,48 @@
|
||||
|
||||
## 5. 这页前后端最小联调顺序
|
||||
|
||||
如果想尽快把这页从演示版切到真实联调版,建议按下面顺序接:
|
||||
当前知识库页和知识文档页已经接入基础接口。下一步联调顺序建议:
|
||||
|
||||
1. 先复用已有:
|
||||
- `POST /api/rag/stores/query`
|
||||
1. 完成向量入库:
|
||||
- `POST /api/rag/documents/chunk` 生成 `rag_chunk`
|
||||
- 通过模型网关调用 Embedding 并写入 `rag_chunk_embedding`
|
||||
|
||||
2. 然后新增:
|
||||
- `GET /api/rag/stores/overview`
|
||||
- `GET /api/rag/stores/{id}/detail`
|
||||
2. 完成索引入口:
|
||||
- `POST /api/rag/documents/index`
|
||||
- `POST /api/rag/store/reindex`
|
||||
|
||||
3. 再补动作接口:
|
||||
- `POST /api/rag/stores`
|
||||
- `PUT /api/rag/stores/{id}`
|
||||
- `POST /api/rag/stores/{id}/documents/import`
|
||||
- `POST /api/rag/stores/{id}/reindex`
|
||||
- `GET /api/rag/stores/{id}/tasks`
|
||||
3. 再补任务查询:
|
||||
- `GET /api/rag/store/tasks`
|
||||
- `GET /api/rag/documents/tasks`
|
||||
|
||||
## 6. 当前前端实现说明
|
||||
|
||||
当前前端页已经按上述页面结构实现,但由于后端尚未提供完整聚合接口,页面中的统计、详情和任务区先以演示数据承载。
|
||||
当前前端已经接入:
|
||||
|
||||
后端接口齐备后,前端建议按下面方式替换:
|
||||
- 统计卡片:`/api/rag/store/overview`
|
||||
- 左侧列表:`/api/rag/store/query`
|
||||
- 右侧详情:`/api/rag/store/detail`
|
||||
- 文档概览:`/api/rag/store/documentOverview`
|
||||
- 批量上传:`/api/rag/documents/batchUpload`
|
||||
- 知识文档列表:`/api/rag/documents/query`
|
||||
- 批量解析:`/api/rag/documents/parse`
|
||||
- 批量切片:`/api/rag/documents/chunk`
|
||||
|
||||
- 统计卡片:改调 `/api/rag/stores/overview`
|
||||
- 左侧列表:改调 `/api/rag/stores/manage/query`
|
||||
- 右侧详情:改调 `/api/rag/stores/{id}/detail`
|
||||
- 批量导入:改调 `/api/rag/stores/{id}/documents/import`
|
||||
- 重建索引:改调 `/api/rag/stores/{id}/reindex`
|
||||
- 最近任务:改调 `/api/rag/stores/{id}/tasks`
|
||||
仍待后端补齐后再联调:
|
||||
|
||||
- 重建索引
|
||||
- 最近任务
|
||||
- 模型服务商与 Embedding 模型配置
|
||||
- 检索配置
|
||||
- 检索测试/召回预览
|
||||
|
||||
## 7. 与 Agent 调试链路的关联
|
||||
|
||||
当前 RAG 切片与向量数据已经被 Agent 调试页直接消费:
|
||||
|
||||
- Agent 调试接口 `POST /api/agents/{agentId}/chat` 在 `ragEnabled=true` 时会读取 `rag_chunk_embedding` 进行 TopK 召回。
|
||||
- 若未完成切片向量化,Agent 调试会返回“未召回到可用知识切片”。
|
||||
|
||||
关联文档:
|
||||
|
||||
- [Agent 页面后端接口清单](agent-page-apis.md)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user