diff --git a/docs/agent-architecture.md b/docs/agent-architecture.md new file mode 100644 index 0000000..e81917d --- /dev/null +++ b/docs/agent-architecture.md @@ -0,0 +1,517 @@ +# Agent 独立化架构文档 + +> 分支:`feat/agent-independence` | Commit:`adeb09a` | 日期:2026-08-09 + +## 目录 + +- [1. 设计目标](#1-设计目标) +- [2. 数据库 Schema](#2-数据库-schema) +- [3. 服务端架构](#3-服务端架构) +- [4. API 端点](#4-api-端点) +- [5. 前端架构](#5-前端架构) +- [6. 多 Session 并行流式输出](#6-多-session-并行流式输出) +- [7. 标题生成机制](#7-标题生成机制) +- [8. 模型解析优先级](#8-模型解析优先级) +- [9. 速率限制](#9-速率限制) +- [10. 协作接口预留](#10-协作接口预留) +- [11. Seed 数据](#11-seed-数据) +- [12. 已知问题与后续工作](#12-已知问题与后续工作) + +--- + +## 1. 设计目标 + +将原本单一的"agent"(全局配置 + 硬编码 system prompt)改造为**独立化、可配置的多 agent 架构**,为多 agent 协作做准备。 + +### 核心变更 + +| 改造前 | 改造后 | +|--------|--------| +| 全局 system prompt(`/api/agent/system-prompt`) | per-agent `systemPrompt` 字段(DB) | +| 工具集全局共享 | per-agent 工具关联(多对多表) | +| 模型由用户选择 | 模型优先级:用户 preferred > agent.defaultModelId > 全局 fallback | +| 路由 `/api/agent/...` | 路由 `/api/agents/:agentSlug/...` | +| 前端单页 `/` | 前端动态路由 `/chat/:agentSlug` | +| 单 session 流式 | 多 session 并行流式(instance 隔离) | + +### 兼容性 + +- **Breaking change**:旧 API 路径 `/api/agent/...` 已删除 +- DB 迁移策略:清库重建(dev 阶段) +- 旧迁移文件全部删除,仅保留单个 `0000_natural_ken_ellis.sql` + +--- + +## 2. 数据库 Schema + +文件:`packages/drizzle-pkg/lib/schema/agent.ts` + +### 2.1 `agents` 表(新增) + +Agent 定义表,每个 agent 是一个独立可配置的 AI 助手。 + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `id` | integer PK | auto | 主键 | +| `slug` | text(50) unique | - | URL 标识(如 `default`、`coder`) | +| `name` | text(100) | - | 显示名称 | +| `description` | text | null | 描述 | +| `systemPrompt` | text | `""` | 系统提示词 | +| `defaultModelId` | integer | null | 默认模型 ID | +| `titleModelId` | integer | null | 标题生成模型 ID(fallback 到 defaultModelId) | +| `titleStrategy` | text(20) | `"llm"` | 标题策略:`llm` / `first-line` / `none` | +| `enableThinking` | integer | `0` | 是否启用思考模式 | +| `enableTools` | integer | `1` | 是否启用工具 | +| `maxStepCount` | integer | `6` | 最大工具调用步数 | +| `isDefault` | integer | `0` | 是否默认 agent(全局唯一) | +| `isCallable` | integer | `0` | 是否可被其他 agent 调用(协作接口) | +| `sortOrder` | integer | `0` | 排序 | +| `enabled` | integer | `1` | 是否启用 | +| `createdAt` / `updatedAt` | timestamp_ms | now | 时间戳 | + +索引:`agents_enabled_idx`、`agents_sort_order_idx` + +### 2.2 `agent_tool_associations` 表(新增) + +Agent 与工具的多对多关联表。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | integer PK | 主键 | +| `agentId` | integer FK → agents.id (cascade) | Agent ID | +| `toolId` | text FK → agentTools.id (cascade) | 工具 ID | +| `needsApproval` | integer | 是否需要审批(覆盖工具级配置) | +| `sortOrder` | integer | 排序 | +| `createdAt` | timestamp_ms | 创建时间 | + +唯一索引:`agent_tool_assoc_uniq(agentId, toolId)` + +### 2.3 `agent_sessions` 表(改造) + +新增 `agentId` 外键字段。 + +| 新增字段 | 类型 | 说明 | +|----------|------|------| +| `agentId` | integer FK → agents.id (**set null**) | 所属 agent(agent 删除时 session 保留) | + +> `enableThinking` / `enableTools` 改为**可空**(null 表示继承 agent 配置) + +新增索引:`idx_agent_sessions_agent(agentId)` + +### 2.4 其他表(未改动) + +- `agent_messages`:消息表 +- `agent_message_feedback`:消息反馈表 + +--- + +## 3. 服务端架构 + +### 3.1 目录结构 + +``` +server/service/agent/ +├── agent.ts # Agent CRUD + 工具关联查询 +├── chat-engine.ts # 核心聊天引擎(executeChat) +├── session.ts # Session CRUD + 消息持久化 +├── title.ts # 标题生成(llm / first-line / none) +├── rate-limit.ts # 速率限制(per-agentSlug 隔离) +├── abort-manager.ts # 流式中断管理 +├── collaboration.ts # 协作接口 stub(预留) +├── stream-buffer.ts # 流缓冲(SSE 重连支持) +├── stored-part.ts # 消息 parts 存储类型 +└── types.ts # 类型定义 + +server/service/llm/ +└── model-resolver.ts # 模型解析(用户/系统/任意) +``` + +### 3.2 Agent Service (`agent.ts`) + +核心函数: + +| 函数 | 说明 | +|------|------| +| `getAgentBySlug(slug)` | 按 slug 查询启用的 agent | +| `getAgentBySlugAny(slug)` | 按 slug 查询(含禁用) | +| `getDefaultAgent()` | 获取默认 agent(isDefault=1,fallback 到 sortOrder 最小) | +| `listAgents(onlyEnabled)` | 列出 agent | +| `getAgentWithTools(slug)` | Agent + 关联工具 | +| `getAgentTools(agentId)` | 获取 agent 的工具列表 | +| `createAgent(input)` | 创建 agent(含工具关联 + isDefault 唯一性维护) | +| `updateAgent(id, input)` | 更新 agent(含工具关联重建) | +| `deleteAgent(id)` | 删除 agent(cascade 删关联,session 的 agentId set null) | + +### 3.3 Chat Engine (`chat-engine.ts`) + +从原 HTTP handler 中抽离的核心逻辑,函数签名: + +```typescript +export interface ChatEngineParams { + agent: AgentRow; + session: AgentSessionRow; + user: { id: number; role: string | null } | null; + tempToken: string | null; + body: { content, editMessageId?, regenerate?, ... }; + ip: string; + preferredModelId?: number | null; + requestId?: string; + registerAbort: (sessionId, controller) => void; + unregisterAbort: (sessionId) => void; + setRateLimitHeaders?: (headers) => void; + onClientClose?: (cb) => void; +} + +export async function executeChat(params: ChatEngineParams): Promise +``` + +关键流程: +1. 速率限制检查(未登录用户) +2. 模型解析(优先级见 [§8](#8-模型解析优先级)) +3. 构建 ModelMessage(含历史消息 + 工具调用 parts) +4. `streamText()` 流式生成 +5. `onFinish` 回调:保存消息 + 触发标题生成(fire-and-forget) +6. `onAbort` 回调:保存已生成内容 + +### 3.4 Model Resolver (`model-resolver.ts`) + +| 函数 | 说明 | +|------|------| +| `resolveModelForUser(modelId, userId)` | 为用户解析模型(查用户私有 provider) | +| `resolveModelAny(modelId)` | 任意解析(系统级,用于未登录用户) | +| `toLanguageModel(resolved)` | 转换为 AI SDK 的 `LanguageModel` | +| `resolveLanguageModel(provider, modelId)` | 底层:根据 parseMode 创建 OpenAI / OpenAI-Compatible | + +### 3.5 Rate Limit (`rate-limit.ts`) + +- **Key 隔离**:`${agentSlug}:${sessionId}` / `${agentSlug}:${ip}` +- **限制**:每 session 20 次、每 IP 每日 50 次 +- 仅对未登录用户生效 + +### 3.6 Abort Manager (`abort-manager.ts`) + +全局 `Map`,支持按 session 中断流式生成。 + +--- + +## 4. API 端点 + +### 4.1 Agent 管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/agents` | 列出所有 agent | +| POST | `/api/agents` | 创建 agent | +| GET | `/api/agents/:agentSlug` | 获取 agent 详情 | +| PUT | `/api/agents/:agentSlug` | 更新 agent | +| DELETE | `/api/agents/:agentSlug` | 删除 agent | +| GET | `/api/agents/:agentSlug/info` | 获取 agent 信息(含工具列表,前端初始化用) | + +### 4.2 Session 管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/agents/:agentSlug/sessions` | 列出 session | +| POST | `/api/agents/:agentSlug/sessions` | 创建 session | +| GET | `/api/agents/:agentSlug/sessions/:id` | 获取 session 详情 | +| PUT | `/api/agents/:agentSlug/sessions/:id` | 更新 session(标题等) | +| DELETE | `/api/agents/:agentSlug/sessions/:id` | 删除 session | +| PUT | `/api/agents/:agentSlug/sessions/:id/config` | 更新 session 配置(modelId、enableThinking、enableTools) | +| GET | `/api/agents/:agentSlug/sessions/:id/messages` | 获取 session 消息 | + +### 4.3 聊天 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/agents/:agentSlug/chat` | 发送消息(启动流式) | +| GET | `/api/agents/:agentSlug/chat/stream` | SSE 流(断线重连) | +| POST | `/api/agents/:agentSlug/chat/stop` | 停止生成 | +| POST | `/api/agents/:agentSlug/chat/tool-approve` | 工具审批 | +| POST | `/api/agents/:agentSlug/chat/cleanup-approvals` | 清理审批 | + +### 4.4 其他 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/agents/:agentSlug/models` | 可用模型列表 | +| POST | `/api/agents/:agentSlug/feedback` | 消息反馈 | +| POST | `/api/agents/:agentSlug/migrate` | 旧数据迁移 | + +### 4.5 已删除的端点 + +- `GET /api/agent/system-prompt` — 已废弃(system prompt 在 agent 配置中) +- 所有 `/api/agent/...` 路径 + +### 4.6 鉴权白名单 + +文件:`packages/common/config/index.ts` + +`API_ALLOWLIST` 和 `FRONTEND_PAGE_ALLOWLIST` 已更新,支持 `:agentSlug` 参数匹配。 + +`FRONTEND_PAGE_ALLOWLIST` 新增 `/chat/:agentSlug`,允许未登录用户访问聊天页面。 + +--- + +## 5. 前端架构 + +### 5.1 路由 + +| 路径 | 说明 | +|------|------| +| `/` | 重定向到 `/chat/default` | +| `/chat/:agentSlug` | Agent 聊天页面 | + +文件: +- `app/pages/index.vue` — 重定向页 +- `app/pages/chat/[agentSlug]/index.vue` — 主聊天页面 + +### 5.2 Composables + +#### `useAgentSessions(agentSlug: string)` + +Session 列表管理,**组件级实例**(非全局)。 + +关键函数: +- `fetchSessions()` — 加载 session 列表 +- `createSession()` — 创建新 session +- `newSession()` — 智能创建:若最新 session 为空(`lastActiveAt === createdAt`)则复用,否则创建新的 +- `selectSession(id)` — 切换 session +- `deleteSession(id)` — 删除 session +- `touchSessionOrder(id)` — 将 session 移到列表顶部(更新 `lastActiveAt`) +- `refreshSessionTitle(id)` — 从服务端同步标题 + +#### `useAgentChatStore(options)` + +多 session 聊天实例管理,**核心架构**。 + +``` +useAgentChatStore +├── instances: Map # 每个 session 独立实例 +├── currentSessionId: ref +├── currentInstance: computed → 当前 session 的 instance +└── messages/isLoading/... : computed → 代理到 currentInstance +``` + +关键设计: +- **每个 session 有独立的 `AgentChatInstance`**(独立的 `messages` ref、`isLoading` ref、`abortController`) +- 切换 session 只是改 `currentSessionId`,旧 instance 的流**继续在后台运行** +- `getInstance(sid)` — 获取/创建 instance +- `removeInstance(sid)` — 停止 + 清除 + 移除 instance +- `stopAll()` — 停止所有 instance 的流(**新建对话时不调用**) +- `getLoadingSessionIds()` — 获取所有正在流式输出的 session + +#### `useAgentChat(options)` + +单 session 聊天逻辑,**闭包隔离**。 + +关键函数: +- `send(content, opts?)` — 发送消息 + 流式处理 +- `processStream(res, assistantIdx)` — SSE 流解析(操作闭包内的 `messages` ref) +- `stopGeneration()` — 停止生成(abort) +- `stopAndSave()` — 停止 + 保存已生成内容 +- `loadMessages(sid)` — 加载历史消息 +- `respondToApproval(toolCallId, approved)` — 工具审批响应 + +### 5.3 页面组件 (`app/pages/chat/[agentSlug]/index.vue`) + +关键逻辑: + +```typescript +// 流完成回调 → 3秒后刷新标题 +onStreamComplete: (sid) => { + setTimeout(() => sessions.refreshSessionTitle(sid), 3000); +} + +// 发送消息 +async function handleSend(content: string) { + // 1. 确保有 session + // 2. touchSessionOrder(sid) — 先更新 lastActiveAt(避免 newSession 复用) + // 3. await chat.send(content) + // 4. 若是新 session,refreshTitleWithRetry(sid) +} + +// 新建对话 +async function handleNewSession() { + // 若当前 session 为空且非 loading → 不创建(复用) + // 否则 newSession() + // 不调用 stopAll() — 其他 session 的流继续运行 +} +``` + +### 5.4 Auth Routes + +文件:`app/utils/auth-routes.ts` + +`matchesExactOrPrefix` 增强:支持 `:param` 正则匹配(如 `/chat/:agentSlug`)。 + +--- + +## 6. 多 Session 并行流式输出 + +### 设计 + +不同 session 可以**同时**流式输出,新建对话**不会**停止其他 session 的流。 + +### 实现原理 + +1. **Instance 隔离**:每个 session 在 `useAgentChatStore` 中有独立的 `AgentChatInstance`,包含独立的 `messages` ref 和 `abortController` +2. **闭包捕获**:`processStream` 操作的是 instance 闭包内的 `messages`,不是 store 的 computed +3. **切换不中断**:`handleNewSession` 不调用 `stopAll()`,旧 session 的 `processStream` promise 继续在后台执行 +4. **数据不串**:切换 session 后,`currentInstance` 指向新 instance,旧 instance 的流写入自己的 `messages` ref + +### 验证场景 + +- 流式输出中点"新对话" → 新 session 创建 → 页面切换到空状态 → 旧 session 流继续 +- 旧 session 流完成后 → `onStreamComplete(sid)` 触发 → `refreshSessionTitle(sid)` 更新左侧标题 + +--- + +## 7. 标题生成机制 + +### 触发链路 + +``` +用户发送首条消息 + → chat-engine.ts onFinish 回调 + → generateSessionTitle() (fire-and-forget, .catch()) + → 根据 agent.titleStrategy: + - "none": 跳过 + - "first-line": 截取首行(≤50字) + - "llm": 调用 titleModelId 生成 + → updateSession(sessionId, { title }) +``` + +### 前端同步 + +``` +onStreamComplete(sid) + → setTimeout(3s) → refreshSessionTitle(sid) → GET session → 更新 sessions.value 中的 title +``` + +新 session 额外重试(`refreshTitleWithRetry`):3s / 6s / 10s / 15s / 22s 多次刷新。 + +### 切换 session 的影响 + +- **服务端**:`onFinish` 在服务端触发,与前端无关,标题会正确写入 DB +- **前端**:`onStreamComplete` 在 instance 闭包中,切走 session 后仍会执行;`sessions` ref 是同一组件实例的 ref,`refreshSessionTitle` 仍能找到并更新 session + +**结论:切换 session 不影响标题生成。** + +--- + +## 8. 模型解析优先级 + +``` +已登录用户: + session.modelId ?? preferredModelId ?? agent.defaultModelId + → resolveModelForUser(modelId, userId) + +未登录用户: + session.modelId ?? agent.defaultModelId + → resolveModelAny(modelId) +``` + +- `preferredModelId`:用户在前端选择的模型(请求 body 传入) +- `session.modelId`:session 级别持久化的模型选择 +- `agent.defaultModelId`:agent 配置的默认模型 + +--- + +## 9. 速率限制 + +| 维度 | Key 格式 | 限制 | +|------|----------|------| +| Session | `${agentSlug}:${sessionId}` | 20 次 | +| IP | `${agentSlug}:${ip}` | 50 次/日 | + +- 仅对**未登录用户**生效 +- 按 agentSlug 隔离(不同 agent 独立计数) + +--- + +## 10. 协作接口预留 + +文件:`server/service/agent/collaboration.ts` + +```typescript +export interface AgentInvocation { + agentSlug: string; + input: string; + context?: { sessionId?: string; userId?: number | null }; +} + +export interface AgentInvocationResult { + agentSlug: string; + output: string; + ok: boolean; + error?: string; +} + +export async function invokeAgent(invocation: AgentInvocation): Promise +``` + +当前为 **stub**,仅校验 agent 存在 + `isCallable` 字段,返回 "not yet implemented"。 + +`agents.isCallable` 字段控制 agent 是否可被其他 agent 调用。 + +--- + +## 11. Seed 数据 + +文件:`packages/drizzle-pkg/seed.ts` + +### 管理员账号 + +- 用户名:`admin` +- 密码:`123456qaz` + +### 工具(11 个) + +| ID | Slug | 类型 | +|----|------|------| +| tool_fetch_html | fetch_html | fetch | +| tool_fetch_md | fetch_md | fetch | +| tool_fetch_json | fetch_json | fetch | +| tool_calculator | calculator | calculator | +| tool_datetime | datetime | datetime | +| tool_uuid | uuid | uuid | +| tool_base64 | base64 | base64 | +| tool_json_formatter | json_formatter | json-formatter | +| tool_regex_tester | regex_tester | regex-tester | +| tool_user_info | user_info | user-info | +| tool_search | search | search | + +### Agent(2 个) + +| Slug | Name | 工具数 | 特性 | +|------|------|--------|------| +| `default` | 默认助手 | 11(全部) | enableThinking=0, maxStepCount=6, isDefault=1, titleStrategy=llm | +| `coder` | 编程助手 | 3(fetch_md, search, regex_tester) | enableThinking=1, maxStepCount=12, isCallable=1, titleStrategy=first-line | + +--- + +## 12. 已知问题与后续工作 + +### 已完成 + +- [x] DB schema 改造 + seed + 清库重建 +- [x] Service 层全部改造 +- [x] API 端点创建 + 旧目录删除 +- [x] 前端路由迁移 + composables 改造 +- [x] default + coder agent 端到端验证 +- [x] "输出时新建对话" bug 修复 +- [x] auth-routes `:param` 匹配修复 +- [x] fetch 工具 config 解析修复(zod safeParse + fallback) + +### 待验证 + +- [ ] coder agent 端到端测试(工具调用、思考模式) +- [ ] 跨 agent session 访问拒绝(已验证基本逻辑) +- [ ] 多 session 并行流式输出压力测试 + +### 后续工作 + +- [ ] Agent 管理后台 UI(CRUD 界面) +- [ ] 协作接口实现(`invokeAgent` 编排逻辑) +- [ ] Agent 级别模型配置 UI +- [ ] 更多 agent 类型(如 researcher、translator)