docs/plan/2026-05-20-technical-design.md
Rama main · réplica de lectura del repositorio original.
# Agent 工作台 - 技术方案文档
> 版本: v0.3 | 更新时间: 2026-05-20
> 关联文档: [requirement.md](./requirement.md)
> **文档状态(2026-07)**:本文档为早期技术方案,部分模块已变更。产品说明、部署与使用手册以 [skills/mao-cli/SKILL.md](../skills/mao-cli/SKILL.md) 为准;架构概览亦见 [project.md](../skills/mao-cli/reference/project.md)。源码与下列差异:
> - **Hub 模块**已删除(`V017__remove_hub.sql`)
> - 前端流式通道为 **WebSocket**(`StreamingWsHandler`),LLM 内部仍通过 OkHttp SSE 拉流;**0.0.82 起 WS 改为首帧鉴权**(`auth` 帧携带 token,URL 不再传 token)
> - 对象存储支持 **本地文件系统** 与 **阿里云 OSS**
> - **集成配置(LDAP/飞书登录/上传/Tavily·TinyFish 搜索/OSS)0.0.82 起迁入 `system_setting` 表**,管理后台可视化编辑、热生效;新增 `settings:read`/`settings:write` 权限与 `SETTINGS_SECRET` 加密主密钥
> - Flyway 迁移脚本现为 **46 个**(非文档初稿中的 24 个)
> - 开源协议:**MIT**,仅提供自部署
---
## 1. 技术选型明细
### 1.1 后端技术栈
| 组件 | 选型 | 版本建议 | 选型理由 |
|------|------|---------|---------|
| 语言 | Java | 17+ (LTS) | 虚拟线程支持,企业级生态成熟 |
| 框架 | Spring Boot | 3.x | 自动配置、生态丰富、社区活跃 |
| Web 框架 | **Spring MVC** | - | 团队熟悉,SSE 通过 `SseEmitter` 实现 |
| ORM | MyBatis-Plus | 3.5.x | 灵活的 SQL 控制,适合复杂查询 |
| 认证 | Spring Security | - | LDAP / OAuth2 开箱即用 |
| HTTP 客户端 | OkHttp / Apache HttpClient | - | LLM API 调用,支持 SSE 流式读取 |
| 对象存储 | MinIO (S3 兼容) | - | 私有化部署,S3 协议标准 |
| LLM 客户端 | 自研 OpenAI 协议适配层 | - | 统一协议,流式 SSE 解析 |
| MCP 客户端 | 自研 MCP SDK (Java) | - | MCP 协议规范实现 |
| API 文档 | SpringDoc (OpenAPI 3) | 2.x | 自动生成 API 文档 |
| 工具库 | Hutool / Guava | - | 常用工具方法 |
### 1.2 前端技术栈
| 组件 | 选型 | 版本建议 | 选型理由 |
|------|------|---------|---------|
| 框架 | Vue 3 | 3.4+ | Composition API、更好的 TS 支持 |
| 构建工具 | Vite | 5.x | 快速构建、HMR |
| 状态管理 | Pinia | 2.x | Vue 3 官方推荐,轻量 |
| UI 组件库 | **Element Plus** | 2.x | 企业级 UI 组件丰富,中文文档完善 |
| HTTP 客户端 | Axios | 1.x | 拦截器机制适合 Token 注入 |
| 路由 | Vue Router | 4.x | Vue 3 配套 |
| 国际化 | Vue I18n | 9.x | 多语言支持(可选) |
| Markdown 渲染 | markdown-it | - | Agent 对话内容渲染 |
| 代码高亮 | highlight.js / Shiki | - | 代码块语法高亮 |
### 1.3 桌面客户端技术栈
| 组件 | 选型 | 版本建议 | 选型理由 |
|------|------|---------|---------|
| 框架 | Electron | 30+ | 跨平台成熟方案 |
| 前端 | Vue 3 + Vite | - | 独立工程,与管理后台独立维护 |
| 窗口管理 | electron-window-state | - | 窗口位置/大小记忆 |
| 自动更新 | electron-updater | - | 应用内自动更新 |
| 安全 | contextIsolation + sandbox | - | Electron 安全最佳实践 |
| IPC 通信 | electron IPC + 预加载脚本 | - | 主进程与渲染进程安全通信 |
### 1.4 基础设施
| 组件 | 选型 | 备注 |
|------|------|------|
| 数据库 | MySQL 8.x | 主数据存储 |
| 对象存储 | MinIO | 文件存储(私有化) |
| 反向代理 | Nginx | 负载均衡 + HTTPS 终结 |
| 日志 | SLF4J + Logback | 应用日志,可选接 ELK |
---
## 2. 工程结构
### 2.1 Monorepo 目录规划
```
mao/
├── backend/ # 后端 Java 服务
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/cn/etarch/mao/
│ │ ├── WorkbenchApplication.java
│ │ ├── config/ # 配置类
│ │ │ ├── SecurityConfig.java
│ │ │ ├── WebMvcConfig.java
│ │ │ └── LlmConfig.java
│ │ ├── common/ # 公共模块
│ │ │ ├── result/ # 统一响应
│ │ │ ├── exception/ # 全局异常
│ │ │ ├── constant/ # 常量
│ │ │ └── util/
│ │ ├── auth/ # 认证模块
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── ldap/ # LDAP 集成
│ │ │ └── feishu/ # 飞书 OAuth
│ │ ├── user/ # 用户模块
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── mapper/
│ │ │ └── entity/
│ │ ├── agent/ # Agent 管理模块
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── mapper/
│ │ │ └── entity/
│ │ ├── harness/ # Agent Harness 核心
│ │ │ ├── core/ # 核心引擎
│ │ │ │ ├── AgentLoop.java
│ │ │ │ ├── PromptEngine.java
│ │ │ │ ├── ContextManager.java
│ │ │ │ └── ExecutionPipeline.java
│ │ │ ├── llm/ # LLM 适配层
│ │ │ │ ├── LlmAdapter.java
│ │ │ │ ├── LlmClient.java
│ │ │ │ ├── OpenAiProtocol.java
│ │ │ │ └── model/
│ │ │ ├── skill/ # Skills 体系
│ │ │ │ ├── SkillRegistry.java
│ │ │ │ ├── SkillDispatcher.java
│ │ │ │ ├── SkillExecutor.java
│ │ │ │ └── builtin/ # 内置 Skills
│ │ │ ├── mcp/ # MCP 协议
│ │ │ │ ├── McpClient.java
│ │ │ │ ├── McpServerManager.java
│ │ │ │ └── McpToolAdapter.java
│ │ │ └── safety/ # 安全沙箱
│ │ │ ├── SafetyGuard.java
│ │ │ └── PermissionChecker.java
│ │ ├── session/ # 会话模块
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── mapper/
│ │ │ └── entity/
│ │ ├── hub/ # Hub 模块
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ └── entity/
│ │ ├── audit/ # 审计模块
│ │ │ ├── interceptor/ # 审计拦截器
│ │ │ ├── service/
│ │ │ └── entity/
│ │ └── file/ # 文件模块
│ │ ├── controller/
│ │ └── service/
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ └── db/migration/ # 数据库迁移脚本
│
├── admin/ # 管理后台 (Vue 3)
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
│ ├── main.ts
│ ├── App.vue
│ ├── router/
│ ├── stores/ # Pinia stores
│ ├── views/
│ │ ├── auth/ # 登录
│ │ ├── user/ # 用户管理
│ │ ├── model/ # 模型管理
│ │ ├── agent/ # Agent 管理
│ │ ├── skill/ # Skills 管理
│ │ ├── hub/ # Hub 管理
│ │ ├── audit/ # 审计日志
│ │ └── system/ # 系统配置
│ ├── components/
│ ├── composables/
│ ├── api/ # API 接口层
│ └── utils/
│
├── desktop/ # 桌面客户端 (Electron + Vue 3)
│ ├── package.json
│ ├── electron/
│ │ ├── main.ts # Electron 主进程
│ │ ├── preload.ts # 预加载脚本
│ │ └── ipc/ # IPC 处理
│ ├── src/ # Vue 3 渲染进程
│ │ ├── main.ts
│ │ ├── App.vue
│ │ ├── router/
│ │ ├── stores/
│ │ ├── views/
│ │ │ ├── auth/ # 登录
│ │ │ ├── workbench/ # 工作台主页
│ │ │ ├── chat/ # Agent 对话
│ │ │ ├── hub/ # Agent Hub
│ │ │ ├── agent-create/ # 创建 Agent
│ │ │ └── settings/ # 设置
│ │ ├── components/
│ │ │ ├── chat/ # 对话组件
│ │ │ ├── agent/ # Agent 相关组件
│ │ │ └── common/
│ │ ├── composables/
│ │ ├── api/
│ │ └── utils/
│ └── resources/ # 应用图标等资源
│
└── docs/ # 文档
├── requirement.md
└── 2026-05-20-technical-design.md
```
---
## 3. 数据库设计
### 3.1 核心表结构
```sql
-- ========== 用户与权限 ==========
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(64) NOT NULL UNIQUE,
`display_name` VARCHAR(128) NOT NULL,
`email` VARCHAR(128),
`avatar_url` VARCHAR(512),
`auth_type` VARCHAR(20) NOT NULL COMMENT 'LDAP / FEISHU',
`password_hash` VARCHAR(128) COMMENT 'LDAP 用户可为空',
`feishu_user_id` VARCHAR(64) COMMENT '飞书用户 ID',
`department_id` BIGINT,
`status` TINYINT DEFAULT 1 COMMENT '1-启用 0-禁用',
`last_login_at` DATETIME,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `role` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL UNIQUE COMMENT '角色名称',
`code` VARCHAR(64) NOT NULL UNIQUE COMMENT '角色编码',
`description` VARCHAR(256),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `user_role` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `permission` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`code` VARCHAR(128) NOT NULL UNIQUE COMMENT '权限编码,如 agent:read, agent:write',
`description` VARCHAR(256),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `role_permission` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`role_id` BIGINT NOT NULL,
`permission_id` BIGINT NOT NULL,
UNIQUE KEY `uk_role_perm` (`role_id`, `permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `department` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`parent_id` BIGINT DEFAULT 0,
`sort_order` INT DEFAULT 0,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ========== AI 模型配置 ==========
CREATE TABLE `llm_model` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL COMMENT '模型显示名称',
`provider` VARCHAR(64) COMMENT '供应商标识',
`base_url` VARCHAR(512) NOT NULL COMMENT 'API 地址',
`api_key` VARCHAR(512) NOT NULL COMMENT '加密存储的 API Key',
`model_id` VARCHAR(128) NOT NULL COMMENT '模型标识,如 gpt-4o',
`max_tokens` INT DEFAULT 4096,
`temperature_max` DECIMAL(3,2) DEFAULT 2.00,
`status` TINYINT DEFAULT 1 COMMENT '1-启用 0-禁用',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ========== Agent ==========
CREATE TABLE `agent` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`description` TEXT,
`icon_url` VARCHAR(512),
`system_prompt` TEXT NOT NULL COMMENT '系统提示词(人格设定)',
`model_id` BIGINT NOT NULL COMMENT '关联的 LLM 模型',
`creator_id` BIGINT NOT NULL COMMENT '创建者用户 ID',
`type` VARCHAR(20) NOT NULL COMMENT 'SYSTEM-系统级 PERSONAL-个人',
`visibility` VARCHAR(20) DEFAULT 'PRIVATE' COMMENT 'PRIVATE / HUB / SPECIFIC',
`status` VARCHAR(20) DEFAULT 'DRAFT' COMMENT 'DRAFT/PUBLISHED/ARCHIVED',
`token_limit` INT DEFAULT 0 COMMENT '单次对话 Token 限额,0-不限',
`max_rounds` INT DEFAULT 0 COMMENT '单次对话最大轮数,0-不限',
`config_json` JSON COMMENT '扩展配置',
`published_at` DATETIME COMMENT '发布到 Hub 的时间',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `agent_tag` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`agent_id` BIGINT NOT NULL,
`tag` VARCHAR(64) NOT NULL,
INDEX `idx_agent_tag` (`agent_id`, `tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `agent_skill` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`agent_id` BIGINT NOT NULL,
`skill_id` BIGINT NOT NULL COMMENT '关联 skill 表',
`config` JSON COMMENT '该 Agent 对此 Skill 的定制配置',
UNIQUE KEY `uk_agent_skill` (`agent_id`, `skill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `agent_mcp_config` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`agent_id` BIGINT NOT NULL,
`server_url` VARCHAR(512) NOT NULL COMMENT 'MCP Server 地址',
`transport` VARCHAR(20) DEFAULT 'SSE' COMMENT 'SSE / STDIO',
`config` JSON COMMENT '额外配置',
`status` TINYINT DEFAULT 1,
INDEX `idx_agent` (`agent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `agent_permission` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`agent_id` BIGINT NOT NULL,
`permission_type` VARCHAR(20) NOT NULL COMMENT 'ROLE / DEPARTMENT / USER',
`permission_value` BIGINT NOT NULL COMMENT '对应 role_id / department_id / user_id',
UNIQUE KEY `uk_agent_perm` (`agent_id`, `permission_type`, `permission_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ========== Skills ==========
CREATE TABLE `skill` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(128) NOT NULL,
`description` TEXT,
`type` VARCHAR(20) NOT NULL COMMENT 'BUILTIN-内置',
`input_schema` JSON COMMENT '输入参数 JSON Schema',
`output_schema` JSON COMMENT '输出参数 JSON Schema',
`impl_class` VARCHAR(256) COMMENT '内置 Skill 的 Java 实现类全限定名',
`creator_id` BIGINT,
`status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT 'ACTIVE/DISABLED',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ========== 会话与消息 ==========
CREATE TABLE `session` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`agent_id` BIGINT NOT NULL,
`title` VARCHAR(256) COMMENT '会话标题,可自动生成',
`status` VARCHAR(20) DEFAULT 'ACTIVE' COMMENT 'ACTIVE/ARCHIVED',
`is_pinned` TINYINT DEFAULT 0,
`is_favorite` TINYINT DEFAULT 0,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_user` (`user_id`),
INDEX `idx_agent` (`agent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `message` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`session_id` BIGINT NOT NULL,
`role` VARCHAR(20) NOT NULL COMMENT 'USER / ASSISTANT / SYSTEM / TOOL',
`content` MEDIUMTEXT COMMENT '消息内容',
`tool_call_id` VARCHAR(128) COMMENT '工具调用 ID(tool role 消息时使用)',
`tool_calls` JSON COMMENT '工具调用列表(assistant 消息中的 function_call)',
`token_count` INT DEFAULT 0 COMMENT '本条消息的 Token 消耗',
`model_id` BIGINT COMMENT '使用的模型',
`metadata` JSON COMMENT '扩展元数据',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_session` (`session_id`),
INDEX `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ========== Hub ==========
CREATE TABLE `hub_installation` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`agent_id` BIGINT NOT NULL COMMENT 'Hub 中的 Agent',
`installed_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_user_agent` (`user_id`, `agent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ========== 审计日志 ==========
CREATE TABLE `audit_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT,
`username` VARCHAR(64),
`action` VARCHAR(64) NOT NULL COMMENT '操作类型',
`resource_type` VARCHAR(64) COMMENT '资源类型',
`resource_id` VARCHAR(64) COMMENT '资源 ID',
`detail` JSON COMMENT '操作详情',
`ip` VARCHAR(45),
`user_agent` VARCHAR(512),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_user` (`user_id`),
INDEX `idx_action` (`action`),
INDEX `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `api_call_log` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT,
`session_id` BIGINT,
`agent_id` BIGINT,
`endpoint` VARCHAR(256) NOT NULL,
`method` VARCHAR(10),
`request_body` MEDIUMTEXT,
`response_code` INT,
`latency_ms` INT,
`llm_model` VARCHAR(128),
`llm_tokens_in` INT DEFAULT 0,
`llm_tokens_out` INT DEFAULT 0,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_session` (`session_id`),
INDEX `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
---
## 4. API 设计
### 4.1 API 总览
| 模块 | 方法 | 路径 | 说明 |
|------|------|------|------|
| **认证** | POST | `/api/v1/auth/login` | 统一登录入口 |
| | POST | `/api/v1/auth/feishu/qrcode` | 获取飞书扫码二维码 |
| | POST | `/api/v1/auth/feishu/callback` | 飞书扫码回调 |
| | POST | `/api/v1/auth/refresh` | 刷新 Token |
| | POST | `/api/v1/auth/logout` | 退出登录 |
| **用户** | GET | `/api/v1/users/me` | 获取当前用户信息 |
| | GET | `/api/v1/users` | 用户列表(管理员) |
| | PUT | `/api/v1/users/{id}/status` | 启用/禁用用户 |
| **模型** | GET | `/api/v1/models` | 模型列表 |
| | POST | `/api/v1/models` | 创建模型配置 |
| | PUT | `/api/v1/models/{id}` | 更新模型配置 |
| | POST | `/api/v1/models/{id}/test` | 测试模型连通性 |
| **Agent** | GET | `/api/v1/agents` | 用户可用的 Agent 列表 |
| | GET | `/api/v1/agents/{id}` | Agent 详情 |
| | POST | `/api/v1/agents` | 创建 Agent |
| | PUT | `/api/v1/agents/{id}` | 更新 Agent |
| | DELETE | `/api/v1/agents/{id}` | 删除 Agent |
| | POST | `/api/v1/agents/{id}/publish` | 发布到 Hub |
| **会话** | POST | `/api/v1/sessions` | 创建会话 |
| | GET | `/api/v1/sessions` | 会话列表 |
| | GET | `/api/v1/sessions/{id}` | 会话详情 |
| | DELETE | `/api/v1/sessions/{id}` | 删除会话 |
| | PUT | `/api/v1/sessions/{id}/pin` | 置顶/取消置顶 |
| **对话** | POST | `/api/v1/sessions/{id}/messages` | 发送消息(触发 Agent) |
| | GET | `/api/v1/sessions/{id}/messages` | 历史消息列表 |
| | GET | `/api/v1/sessions/{id}/stream` | SSE 流式输出 |
| **Hub** | GET | `/api/v1/hub/agents` | Hub Agent 列表 |
| | POST | `/api/v1/hub/agents/{id}/install` | 安装 Agent |
| | DELETE | `/api/v1/hub/agents/{id}/install` | 卸载 Agent |
| **Skills** | GET | `/api/v1/skills` | Skills 列表 |
| | POST | `/api/v1/skills` | 创建/上传 Skill |
| | PUT | `/api/v1/skills/{id}` | 更新 Skill |
| **文件** | POST | `/api/v1/files/upload` | 上传文件 |
| | GET | `/api/v1/files/{id}/download` | 下载文件 |
| **审计** | GET | `/api/v1/audit/logs` | 审计日志查询 |
| | GET | `/api/v1/audit/api-logs` | API 调用日志查询 |
### 4.2 统一响应格式
```json
{
"code": 0,
"message": "success",
"data": { ... },
"timestamp": 1716105600000
}
```
错误码规范:
| 范围 | 含义 |
|------|------|
| 0 | 成功 |
| 1001-1999 | 认证/授权错误 |
| 2001-2999 | 参数校验错误 |
| 3001-3999 | 业务逻辑错误 |
| 5001-5999 | 服务端内部错误 |
### 4.3 SSE 流式输出协议
对话采用 SSE(Server-Sent Events)实现流式输出:
```
GET /api/v1/sessions/{id}/stream
Authorization: Bearer <token>
Accept: text/event-stream
# 事件类型
event: message_start
data: {"message_id": "msg_123", "model": "gpt-4o"}
event: content_delta
data: {"delta": "你好"}
event: content_delta
data: {"delta": ",有什么"}
event: tool_call_start
data: {"tool_call_id": "tc_456", "tool_name": "web_search", "arguments": "{"query":"..."}"}
event: tool_call_result
data: {"tool_call_id": "tc_456", "result": "..."}
event: message_end
data: {"message_id": "msg_123", "usage": {"prompt_tokens": 120, "completion_tokens": 85}}
event: error
data: {"code": 5001, "message": "LLM 调用超时"}
```
---
## 5. 技术难点与解决方案
### 5.1 Agent Harness 核心引擎设计
**难点:** Harness 是整个系统的核心,需要统一处理不同 Agent 的人格、工具、上下文,同时保持可扩展性。
**方案:** 采用 Pipeline + 插件化架构
```
┌─────────────────────────────────────────────────────────────┐
│ Execution Pipeline │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │ Prompt │──▶│ LLM │──▶│ Response │──▶│ Output ││
│ │ Compose │ │ Call │ │ Parse │ │ Render ││
│ └──────────┘ └──────────┘ └────┬─────┘ └──────────┘│
│ │ │
│ ┌──────▼──────┐ │
│ │ Tool Call? │ │
│ └──────┬──────┘ │
│ Yes │ No → Output │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Tool Dispatch Layer │ │
│ │ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Skill │ │ MCP │ │ │
│ │ │ Executor │ │ Client │ │ │
│ │ └────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Result Feedback │
│ (回到 LLM Call) │
└─────────────────────────────────────────────────────────────┘
```
**关键设计决策:**
1. **Agent Loop 作为核心循环**:借鉴 Codex/ChatGPT 的 Agent Loop 模式,LLM 输出 → 解析 → 执行工具 → 结果反馈 → 再次 LLM 调用,直到 LLM 返回纯文本结果或达到最大轮次。
2. **统一的 Tool 接口**:所有工具(内置 Skill、MCP 工具)实现统一的 `Tool` 接口:
```java
public interface Tool {
String getName();
String getDescription();
JsonSchema getInputSchema();
ToolResult execute(JsonObject arguments, ExecutionContext context);
}
```
3. **异步执行 + 流式反馈**:工具执行异步化,执行过程中通过 SSE 实时推送进度给客户端。
### 5.2 LLM 统一协议适配
**难点:** 需要对接不同厂商的 LLM 服务,虽然都声称兼容 OpenAI 协议,但实际上各家有细微差异。
**方案:** 三层适配架构
```
┌───────────────────────────────────────┐
│ LlmAdapter (接口层) │
│ 定义统一的调用接口:chat() / stream() │
└───────────────┬───────────────────────┘
│
┌───────────────▼───────────────────────┐
│ OpenAiProtocol (协议层) │
│ OpenAI 兼容格式的请求/响应构建与解析 │
│ - ChatCompletion 请求构建 │
│ - SSE 流式响应解析 │
│ - function_call / tool_calls 解析 │
└───────────────┬───────────────────────┘
│
┌───────────────▼───────────────────────┐
│ ProviderAdapter (供应商适配层) │
│ 处理各厂商的协议差异: │
│ - 请求头差异(认证方式) │
│ - 响应格式微调(字段映射) │
│ - 特有参数(如 Claude 的 system) │
└───────────────────────────────────────┘
```
核心类设计:
```java
// 统一适配接口
public interface LlmAdapter {
ChatResponse chat(ChatRequest request, LlmModelConfig config);
void stream(ChatRequest request, LlmModelConfig config, StreamCallback callback);
}
// Stream 回调接口
public interface StreamCallback {
void onChunk(StreamChunk chunk);
void onComplete(ChatUsage usage);
void onError(Throwable t);
}
// OpenAI 协议实现(默认,基于 OkHttp)
public class OpenAiLlmAdapter implements LlmAdapter {
private final OkHttpClient httpClient;
@Override
public void stream(ChatRequest request, LlmModelConfig config, StreamCallback callback) {
RequestBody body = RequestBody.create(
buildRequestBody(request), MediaType.parse("application/json"));
Request httpRequest = new Request.Builder()
.url(config.getBaseUrl() + "/chat/completions")
.header("Authorization", "Bearer " + config.getApiKey())
.post(body)
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
BufferedSource source = response.body().source();
while (!source.exhausted()) {
String line = source.readUtf8Line();
if (line != null && line.startsWith("data: ")) {
StreamChunk chunk = parseStreamChunk(line.substring(6));
callback.onChunk(chunk);
}
}
callback.onComplete(extractUsage(response));
} catch (Exception e) {
callback.onError(e);
}
}
}
```
### 5.3 MCP 协议对接
**难点:** MCP (Model Context Protocol) 是 Anthropic 提出的开放协议,Java 生态缺乏成熟 SDK,需要自研。MCP 支持 SSE 和 STDIO 两种传输方式。
**方案:** 自研轻量 MCP Java Client
```
┌─────────────────────────────────────────┐
│ MCP Client │
│ │
│ ┌─────────────────────────────────┐ │
│ │ McpServerManager │ │
│ │ - 服务器连接池管理 │ │
│ │ - 健康检查 / 自动重连 │ │
│ └────────────┬────────────────────┘ │
│ │ │
│ ┌────────────▼────────────────────┐ │
│ │ Transport Layer │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ SSE │ │ STDIO │ │ │
│ │ │ Transport │ │ Transport│ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └────────────┬────────────────────┘ │
│ │ │
│ ┌────────────▼────────────────────┐ │
│ │ Protocol Layer │ │
│ │ - initialize / initialized │ │
│ │ - tools/list │ │
│ │ - tools/call │ │
│ │ - resources/list │ │
│ │ - resources/read │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
**核心流程:**
1. **连接阶段**:Agent 初始化时,根据 `agent_mcp_config` 表中的配置,建立与 MCP Server 的连接
2. **工具发现**:调用 `tools/list` 获取该 MCP Server 提供的所有工具,自动转换为 Harness 内部的 `Tool` 接口
3. **工具调用**:当 LLM 返回 tool_call 且目标工具属于 MCP 时,通过 MCP 协议转发调用
4. **生命周期管理**:连接池管理、心跳检测、断线重连
### 5.4 用户上传 Skills(暂缓)
**当前策略:** MVP 阶段仅支持管理后台注册的**内置 Skills** + **MCP Server 工具**两种来源。用户上传自定义脚本的能力预留设计,Phase 2 再实现。
**预留设计:**
- `skill` 表保留 `type` 字段,未来扩展 `USER_UPLOAD` 类型
- `Tool` 接口设计已考虑动态加载,未来可增加 `ScriptTool` 实现类
- Phase 2 实现时需引入 Docker 沙箱 + 代码审核流程
### 5.5 Agent 对话的流式输出
**难点:** 整个链路(客户端 → 后端 → LLM → 后端 → 客户端)都需要保持流式,任何一环阻塞都会影响体验。
**方案:** 全链路 SSE 流式管道(Spring MVC + SseEmitter)
```
客户端 (Electron) 服务端 (Spring MVC) LLM API
│ │ │
│ POST /messages │ │
│ ──────────────────────▶ │ │
│ │ POST /chat/completions │
│ │ (stream: true) │
│ │ ────────────────────────▶ │
│ │ │
│ GET /stream (SSE) │ ◀── SSE chunks ────────── │
│ ──────────────────────▶ │ │
│ │ │
│ ◀── content_delta ───── │ ◀── data: {...} ───────── │
│ ◀── content_delta ───── │ ◀── data: {...} ───────── │
│ ◀── tool_call_start ─── │ ◀── data: {...} ───────── │
│ ◀── tool_call_result ── │ (内部执行工具) │
│ ◀── content_delta ───── │ ◀── 第二轮 LLM 调用 ────── │
│ ◀── message_end ─────── │ │
│ │ │
```
**技术实现要点:**
1. **后端使用 Spring MVC + `SseEmitter`**:Servlet 异步机制实现 SSE,无需切换到 WebFlux
2. **LLM 调用使用 OkHttp**:通过 `ResponseBody.source()` 逐行读取 SSE 流,回调推送到 `SseEmitter`
3. **客户端使用 EventSource API**:Electron 渲染进程中标准的 SSE 消费方式
4. **Agent Loop 运行在独立线程池**:避免阻塞 Servlet 线程,通过 `SseEmitter` 跨线程推送事件
```java
// 后端核心流式处理
@GetMapping(value = "/api/v1/sessions/{sessionId}/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@PathVariable Long sessionId,
@RequestParam String eventId) {
SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); // 5 分钟超时
// 提交到独立线程池执行 Agent Loop
agentExecutor.submit(() -> {
try {
harnessService.execute(sessionId, eventId, new AgentEventListener() {
@Override
public void onContentDelta(String delta) {
emitter.send(SseEmitter.event()
.name("content_delta")
.data(Map.of("delta", delta)));
}
@Override
public void onToolCallStart(ToolCall call) {
emitter.send(SseEmitter.event()
.name("tool_call_start")
.data(call));
}
@Override
public void onMessageEnd(Usage usage) {
emitter.send(SseEmitter.event()
.name("message_end")
.data(usage));
emitter.complete();
}
@Override
public void onError(Throwable t) {
emitter.send(SseEmitter.event()
.name("error")
.data(Map.of("message", t.getMessage())));
emitter.completeWithError(t);
}
});
} catch (Exception e) {
emitter.completeWithError(e);
}
});
emitter.onTimeout(() -> emitter.complete());
emitter.onError(e -> log.warn("SSE connection error", e));
return emitter;
}
// Harness 内部 - 使用 OkHttp 读取 LLM SSE 流
public void execute(Long sessionId, String eventId, AgentEventListener listener) {
// 1. 加载 Agent 配置 + 构建 Prompt
// 2. 进入 Agent Loop
AgentContext context = buildContext(sessionId, eventId);
while (context.hasNextRound()) {
// OkHttp 同步请求,逐行读取 SSE
try (Response response = llmClient.stream(context.buildRequest())) {
BufferedSource source = response.body().source();
while (!source.exhausted()) {
String line = source.readUtf8Line();
StreamChunk chunk = parseSSE(line);
switch (chunk.getType()) {
case DELTA -> listener.onContentDelta(chunk.getDelta());
case TOOL_CALL -> handleToolCall(chunk, context, listener);
}
}
}
// 如果有工具调用,执行后继续下一轮
if (context.hasPendingToolCalls()) {
executeToolCalls(context, listener);
} else {
break; // 纯文本回复,结束循环
}
}
listener.onMessageEnd(context.getUsage());
}
```
### 5.6 上下文窗口管理
**难点:** LLM 有 Token 上下文限制,长对话会超出窗口。需要智能管理上下文,保证对话连贯性。
**方案:** 三级上下文策略
```
┌─────────────────────────────────────────────────────┐
│ Context Manager │
│ │
│ Level 1: 系统提示词(固定,始终保留) │
│ ┌─────────────────────────────────────────────┐ │
│ │ System Prompt + Agent 人格 + 工具描述 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Level 2: 近期对话(滑动窗口,优先保留) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 最近 N 轮完整对话(user + assistant + tool) │ │
│ │ N 由 max_context_rounds 配置决定 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Level 3: 历史摘要(压缩旧对话) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 对超出窗口的旧对话生成摘要,作为 context 注入 │ │
│ │ 摘要可由 LLM 自动生成或规则截断 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Token 预算分配: │ │
│ │ 系统提示词: ~15% │ │
│ │ 工具描述: ~10% │ │
│ │ 历史摘要: ~10% │ │
│ │ 近期对话: ~50% │ │
│ │ 预留输出: ~15% │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
**摘要生成策略:**
- 当对话轮数超过 `max_context_rounds` 时,将最早的对话批次送入 LLM 生成摘要
- 摘要格式:`[对话摘要] 用户询问了XX,助手完成了XX,关键结论:XX`
- 摘要随对话推进持续更新(增量摘要)
### 5.7 并发控制与限流
**难点:** 多用户并发使用 Agent,LLM API 有速率限制,需要合理控制并发。
**方案:** 多级限流 + 线程池控制
```
请求进入
│
▼
┌──────────────┐
│ 用户级限流 │ 应用内滑动窗口
│ (每用户 QPS) │ 例:10 次/分钟
└──────┬───────┘
▼
┌──────────────┐
│ Agent 级限流 │ 应用内计数器
│ (并发会话数) │ 例:同一 Agent 最多 50 并发
└──────┬───────┘
▼
┌──────────────┐
│ 模型级限流 │ 应用内令牌桶
│ (API 调用速率)│ 例:60 次/分钟
└──────┬───────┘
▼
┌──────────────┐
│ 线程池调度 │ ThreadPoolExecutor
│ (并发控制) │ 核心线程 / 最大线程 / 队列
└──────────────┘
```
**实现细节:**
- 用户级限流:应用内滑动窗口计数
- 模型级限流:读取 `llm_model` 配置中的速率限制参数
- 并发控制:Agent 执行提交到专用线程池,通过有界队列 + CallerRunsPolicy 背压
- 客户端体验:线程池满时返回 `event: queue_position` 告知用户排队位置
- 线程池配置建议:核心线程 20,最大线程 100,队列容量 200
### 5.8 Electron 安全与性能
**难点:** Electron 应用需要兼顾安全性和性能,避免常见安全漏洞。
**方案:**
```
┌─────────────────────────────────────────────────────┐
│ Electron 安全架构 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 主进程 │◀─IPC──▶│ 渲染进程 │ │
│ │ (Node.js) │ │ (Vue 3) │ │
│ │ │ │ │ │
│ │ - 窗口管理 │ │ - UI 渲染 │ │
│ │ - 系统交互 │ │ - 用户交互 │ │
│ │ - 自动更新 │ │ - API 调用 │ │
│ │ - 文件操作 │ │ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 安全配置: │
│ - contextIsolation: true │
│ - nodeIntegration: false │
│ - sandbox: true │
│ - preload.ts 白名单暴露 IPC 接口 │
└─────────────────────────────────────────────────────┘
```
**性能优化:**
- Vue 3 组件懒加载 + 路由懒加载
- 对话列表虚拟滚动(大量消息时)
- Markdown 渲染结果缓存
- 主进程与渲染进程职责分离,避免阻塞 UI
---
## 6. 安全架构
### 6.1 认证流程
```
LDAP 登录:
用户 ──▶ 输入账号密码 ──▶ 后端 ──▶ LDAP Server 验证
│
成功 ◀───┘
│
生成 JWT Token
│
返回给客户端
飞书扫码登录:
用户 ──▶ 点击飞书登录 ──▶ 后端生成二维码 URL
│
用户 ◀── 展示二维码 │
│
用户手机扫码 ──▶ 飞书 ──▶ 回调后端 /auth/feishu/callback
│
验证授权码 ──▶ 获取用户信息
│
匹配/创建用户 ──▶ 生成 JWT Token
│
返回给客户端
```
### 6.2 权限模型
```
User ──▶ UserRole ──▶ Role ──▶ RolePermission ──▶ Permission
│
┌─────────┼─────────┐
▼ ▼ ▼
agent:read agent:write ...
│
▼
AgentPermission (Agent 级权限)
│
┌───────────┼───────────┐
▼ ▼ ▼
ROLE DEPARTMENT USER
(角色可见) (部门可见) (指定用户)
```
**权限校验流程:**
1. API 请求进入 → JWT 解析 → 获取用户 ID
2. 查询用户角色 → 获取权限列表
3. 检查是否有对应 API 的权限编码
4. 如果是 Agent 操作,额外检查 AgentPermission(该用户是否有权访问此 Agent)
### 6.3 数据安全
| 数据类型 | 保护措施 |
|---------|---------|
| 用户密码 | bcrypt 哈希存储 |
| API Key | AES-256 加密存储,仅服务端使用 |
| 对话内容 | 传输加密 (HTTPS) + 存储访问控制 |
| 审计日志 | 仅管理员可查,不可修改/删除 |
---
## 7. 部署架构
### 7.1 私有化部署拓扑
```
┌─────────────────────────────────────────────────────────────┐
│ 企业内网 / 私有云 │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Nginx (反向代理) │ │
│ │ HTTPS 终结 + 负载均衡 │ │
│ └────────────┬──────────────┬──────────────┬────────────┘ │
│ │ │ │ │
│ ┌────────────▼───┐ ┌──────▼──────┐ ┌───▼────────────┐ │
│ │ Backend │ │ Admin │ │ MinIO │ │
│ │ (Spring Boot)│ │ (Vue 3) │ │ (文件存储) │ │
│ │ x N 实例 │ │ 静态资源 │ │ │ │
│ └────────┬───────┘ └─────────────┘ └────────────────┘ │
│ │ │
│ ┌────────▼──────────────────────────────────────────────┐ │
│ │ 内部网络 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ MySQL │ │ LDAP │ │ │
│ │ │ 8.x │ │ Server │ │ │
│ │ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 外部依赖: │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ LLM API (OpenAI / Claude / 自部署模型) │ │
│ │ 飞书开放平台 (扫码登录) │ │
│ │ MCP Server (外部工具服务) │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 8. 关键依赖与风险评估
| 风险项 | 影响 | 等级 | 应对措施 |
|--------|------|------|---------|
| LLM API 稳定性 | 对话功能不可用 | 高 | 多模型备份、降级策略、超时重试 |
| MCP 协议演进 | 需要持续适配 | 中 | 抽象协议层,隔离变化影响 |
| Electron 包体大小 | 用户下载体验 | 低 | asar 打包 + 按需分发 |
| 长对话 Token 超限 | 对话截断 | 中 | 智能上下文管理 + 摘要压缩 |
| 并发压力 | 响应变慢 | 中 | 多级限流 + 线程池背压 + 水平扩展 |
---
## 9. 已确认决策
| 问题 | 决策 |
|------|------|
| Spring WebFlux vs Spring MVC | **Spring MVC** + `SseEmitter` 实现 SSE |
| 消息队列选型 | **暂不引入 MQ**,使用线程池控制并发 |
| 用户上传 Skills | **MVP 跳过**,仅支持内置 Skills + MCP |
| 前端代码共享 | **独立维护**,管理后台与桌面端各自独立 |
> 暂无待讨论问题,可继续补充新需求或进入工程初始化阶段。