1 changed files with 578 additions and 0 deletions
@ -0,0 +1,578 @@ |
|||
# 首页对外 Agent 设计 |
|||
|
|||
## 概述 |
|||
|
|||
将首页从空白欢迎页改造为完整的对外 agent 对话界面,支持 session 管理、工具调用、推理展示等功能。采用混合模式:未登录用户可体验(受限功能),登录后解锁完整功能并持久化历史。 |
|||
|
|||
## 用户分层 |
|||
|
|||
| 维度 | 未登录用户 | 已登录用户 | |
|||
|------|-----------|-----------| |
|||
| Session 存储 | 服务端临时 session(HttpOnly cookie token 关联),2h TTL | 持久化 session,软删除永久保留 | |
|||
| 模型 | 管理员配置的默认模型(不可切换) | 用户偏好模型(session 内可切换) | |
|||
| 推理 | 固定开启(不可切换) | session 级别开关,默认开启 | |
|||
| 工具 | 白名单工具自动启用(不可切换) | session 级别开关,默认开启 | |
|||
| 消息上限 | 单 session 20 条 assistant 回复 + IP 日 50 条 assistant 回复 | 无限制 | |
|||
| 反馈 | 不可用 | 可用 | |
|||
| System prompt 查看 | 不可见 | 工具栏按钮查看 | |
|||
| 工具栏 | 仅显示模型名 | 模型选择 + 推理开关 + 工具开关 + system prompt 按钮 | |
|||
|
|||
## 整体架构 |
|||
|
|||
### 布局 |
|||
|
|||
全屏占满,侧边栏(260px 固定)+ 主对话区。对话区消息限宽 800px 居中。移动端(< 768px)侧边栏抽屉化。 |
|||
|
|||
### 核心数据流 |
|||
|
|||
``` |
|||
用户输入 → POST /api/agent/chat (sessionId, content) |
|||
→ 服务端校验身份(cookie token 或登录态) |
|||
→ 限流检查(未登录:session 计数 + IP 计数) |
|||
→ 保存 user message |
|||
→ 从 DB 加载 session 历史 messages |
|||
→ 注入 system prompt + 工具(按用户角色过滤) |
|||
→ streamText (ai-sdk) → 流式响应 |
|||
→ onFinish: 持久化 assistant message(含 parts JSON、modelId、token 用量) |
|||
→ 更新 session lastActiveAt |
|||
→ 首轮对话后异步生成标题 |
|||
``` |
|||
|
|||
### 新增 API 路由 |
|||
|
|||
``` |
|||
/api/agent/sessions GET — 分页列表(15/页) |
|||
POST — 新建 session |
|||
/api/agent/sessions/:id GET — session 详情 + 首页消息(30条) |
|||
PUT — 重命名 |
|||
DELETE — 软删除 |
|||
/api/agent/sessions/:id/config PUT — 更新 session 配置(modelId/enableThinking/enableTools) |
|||
/api/agent/sessions/:id/messages GET — 消息分页加载 |
|||
/api/agent/chat POST — 发送消息(流式响应) |
|||
/api/agent/chat/tool-approve POST — 工具审批(仅已登录用户) |
|||
/api/agent/feedback POST — 提交/修改反馈 |
|||
/api/agent/migrate POST — 登录后迁移临时 session |
|||
|
|||
/api/admin/agent-config GET — 读取所有配置 |
|||
/api/admin/agent-config/system-prompt PUT — 更新 system prompt |
|||
/api/admin/agent-config/default-model PUT — 更新默认模型 + 标题模型 |
|||
/api/admin/agent-config/public-tools PUT — 更新公开工具白名单 |
|||
``` |
|||
|
|||
## 数据库 Schema |
|||
|
|||
### `agent_sessions` 表 |
|||
|
|||
```typescript |
|||
export const agentSessions = sqliteTable("agent_sessions", { |
|||
id: text("id").primaryKey(), // UUID |
|||
userId: integer("user_id"), // null = 临时 session |
|||
tempToken: text("temp_token", { length: 64 }), // 临时 session 的 cookie token(正式 session 为 null) |
|||
title: text("title", { length: 100 }).notNull().default("新对话"), |
|||
modelId: integer("model_id"), // session 使用的模型(null = 用默认/偏好模型) |
|||
enableThinking: integer("enable_thinking").default(1).notNull(), // 0/1 |
|||
enableTools: integer("enable_tools").default(1).notNull(), // 0/1 |
|||
lastActiveAt: integer("last_active_at", { mode: "timestamp_ms" }).defaultNow().notNull(), |
|||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }), // 临时 session 的过期时间 |
|||
deletedAt: integer("deleted_at", { mode: "timestamp_ms" }), // 软删除时间 |
|||
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), |
|||
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).defaultNow().$onUpdate(() => new Date()).notNull(), |
|||
}) |
|||
``` |
|||
|
|||
### `agent_messages` 表 |
|||
|
|||
```typescript |
|||
export const agentMessages = sqliteTable("agent_messages", { |
|||
id: text("id").primaryKey(), // UUID |
|||
sessionId: text("session_id").notNull(), |
|||
role: text("role", { length: 20 }).notNull(), // "user" | "assistant" |
|||
content: text("content").notNull().default(""), // 纯文本内容 |
|||
parts: text("parts"), // JSON string,存 MessagePart[] |
|||
modelId: integer("model_id"), // assistant 消息使用的模型 |
|||
inputTokens: integer("input_tokens"), // assistant 消息的输入 token |
|||
outputTokens: integer("output_tokens"), // assistant 消息的输出 token |
|||
sortOrder: integer("sort_order").notNull(), // 同 session 内顺序 |
|||
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), |
|||
}) |
|||
``` |
|||
|
|||
### `agent_message_feedback` 表 |
|||
|
|||
```typescript |
|||
export const agentMessageFeedback = sqliteTable("agent_message_feedback", { |
|||
id: integer("id").primaryKey({ autoIncrement: true }), |
|||
messageId: text("message_id").notNull(), |
|||
userId: integer("user_id").notNull(), |
|||
feedback: text("feedback", { length: 10 }).notNull(), // "like" | "dislike" |
|||
comment: text("comment"), // 可选文字说明 |
|||
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), |
|||
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).defaultNow().$onUpdate(() => new Date()).notNull(), |
|||
}) |
|||
``` |
|||
|
|||
### `agent_config` 表 |
|||
|
|||
```typescript |
|||
export const agentConfig = sqliteTable("agent_config", { |
|||
key: text("key").primaryKey(), |
|||
value: text("value").notNull(), // JSON string |
|||
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).defaultNow().$onUpdate(() => new Date()).notNull(), |
|||
}) |
|||
``` |
|||
|
|||
**预设配置项(无默认值,管理员首次使用前需自行配置):** |
|||
- `system_prompt` — string,管理员配置的 system prompt |
|||
- `default_model_id` — number,未登录用户使用的默认模型 |
|||
- `title_model_id` — number,生成 session 标题的低成本模型 |
|||
- `public_tool_slugs` — string[],公开工具白名单 |
|||
|
|||
**注意:** 首次部署时 `agent_config` 表为空。管理员需在后台页面配置以上项后,agent 功能才能正常工作。未配置时: |
|||
- 未登录用户新建 session → 报错"默认模型未配置" |
|||
- 标题生成 → 跳过,保持"新对话"标题 |
|||
- 未登录用户工具 → 空列表(无白名单) |
|||
|
|||
### 索引 |
|||
|
|||
- `agent_sessions`: `userId` + `deletedAt`(列表查询)、`tempToken`(临时 session 查找) |
|||
- `agent_messages`: `sessionId` + `sortOrder`(消息分页) |
|||
- `agent_message_feedback`: `messageId` + `userId`(唯一约束) |
|||
|
|||
## 后端 API 设计 |
|||
|
|||
### Session 管理 |
|||
|
|||
#### `GET /api/agent/sessions` |
|||
分页列表,按 `lastActiveAt` 倒序,每页 15 条。 |
|||
|
|||
``` |
|||
请求: ?page=1 |
|||
响应: { |
|||
code: 0, |
|||
data: { |
|||
list: Session[], // id, title, lastActiveAt |
|||
total: number, |
|||
page: number, |
|||
pageSize: 15, |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**身份识别逻辑:** |
|||
- 已登录:`WHERE userId = ? AND deletedAt IS NULL` |
|||
- 未登录:从 cookie 读 `tempToken`,`WHERE tempToken = ? AND deletedAt IS NULL AND expiresAt > now` |
|||
- 未登录且无 token:返回空列表 |
|||
|
|||
#### `POST /api/agent/sessions` |
|||
新建 session。 |
|||
|
|||
``` |
|||
请求: { modelId?: number } |
|||
响应: { code: 0, data: Session } |
|||
``` |
|||
|
|||
**逻辑:** |
|||
- 已登录:`userId` = 当前用户,`modelId` = 传入值 || 用户偏好模型 || 管理员默认模型 |
|||
- 未登录:生成 `tempToken`(64 位随机 hex),写入 HttpOnly cookie(`path=/; httpOnly; secure; sameSite=strict`),`modelId` = 管理员默认模型,`expiresAt` = now + 2h |
|||
|
|||
#### `GET /api/agent/sessions/:id` |
|||
Session 详情 + 首页消息(最近 30 条)。 |
|||
|
|||
``` |
|||
响应: { |
|||
code: 0, |
|||
data: { |
|||
session: Session, |
|||
messages: AgentMessage[], // 最近 30 条,按 sortOrder 正序 |
|||
hasMore: boolean, |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**权限校验:** session 的 `userId` 或 `tempToken` 必须匹配当前身份。 |
|||
|
|||
#### `PUT /api/agent/sessions/:id` |
|||
重命名。 |
|||
|
|||
``` |
|||
请求: { title: string } |
|||
响应: { code: 0 } |
|||
``` |
|||
|
|||
#### `DELETE /api/agent/sessions/:id` |
|||
软删除。 |
|||
|
|||
``` |
|||
逻辑: deletedAt = now |
|||
``` |
|||
|
|||
#### `GET /api/agent/sessions/:id/messages` |
|||
消息分页加载(向上加载更早消息)。 |
|||
|
|||
``` |
|||
请求: ?before=<sortOrder>&limit=30 |
|||
响应: { code: 0, data: { messages: AgentMessage[], hasMore: boolean } } |
|||
``` |
|||
|
|||
### 对话 |
|||
|
|||
#### `POST /api/agent/chat` |
|||
发送消息,流式响应。 |
|||
|
|||
``` |
|||
请求: { |
|||
sessionId: string, |
|||
content: string, // regenerate 时也必填,默认取最后一条 user message 内容(前端传入) |
|||
editMessageId?: string, // 编辑消息时传 |
|||
regenerate?: boolean, // 重新生成时传 |
|||
} |
|||
响应: SSE 流(ai-sdk toUIMessageStreamResponse) |
|||
响应头(未登录用户): |
|||
X-RateLimit-Session-Remaining: number — 当前 session 剩余 assistant 回复次数 |
|||
X-RateLimit-Ip-Remaining: number — 当前 IP 今日剩余 assistant 回复次数 |
|||
``` |
|||
|
|||
**核心流程:** |
|||
1. 身份校验 + session 权限校验 |
|||
2. 限流检查(未登录:session 计数 ≥ 20 或 IP 计数 ≥ 50 → 403) |
|||
3. 处理编辑/重新生成(截断 messages) |
|||
4. 保存 user message(`sortOrder` = 当前最大 + 1) |
|||
5. 读取 session 历史 messages |
|||
6. 读取 `agent_config` 的 system prompt |
|||
7. 工具注入: |
|||
- 已登录 + session.enableTools:全部 enabled 工具(需用户审批后执行) |
|||
- 已登录 + !session.enableTools:无工具 |
|||
- 未登录:`public_tool_slugs` 白名单工具(强制启用,自动执行,无需审批) |
|||
8. `streamText` 调用(复用现有 `resolveModel` 逻辑) |
|||
9. `onFinish` 回调:保存 assistant message(含 parts、modelId、token 用量),更新 session `lastActiveAt`,限流计数 +1 |
|||
10. 第一条 assistant 回复完成后,异步调用 `titleModelId` 生成标题 |
|||
|
|||
**编辑消息(截断 + 重新生成):** |
|||
- 前端传 `editMessageId` + `content` |
|||
- 服务端删除该 message 之后的所有 messages(硬删除,含 feedback) |
|||
- 用编辑后的内容更新该 message |
|||
- 继续正常对话流程 |
|||
|
|||
**重新生成:** |
|||
- 前端传 `regenerate: true` + `content`(取最后一条 user message 内容) |
|||
- 服务端删除最后一条 assistant message(含 feedback) |
|||
- 用传入的 content 重新生成 |
|||
|
|||
### Session 配置更新 |
|||
|
|||
#### `PUT /api/agent/sessions/:id/config` |
|||
更新 session 级别配置(仅已登录用户)。 |
|||
|
|||
``` |
|||
请求: { modelId?: number, enableThinking?: boolean, enableTools?: boolean } |
|||
响应: { code: 0 } |
|||
逻辑: 仅更新传入的字段,未登录用户 403 |
|||
``` |
|||
|
|||
### 工具审批 |
|||
|
|||
已登录用户的工具调用需要审批: |
|||
- LLM 发起 tool-call → 流式返回 tool-call part(状态"待审批") |
|||
- 前端展示审批按钮(同意/拒绝) |
|||
- 用户选择后调用审批 API → 服务端执行或拒绝 |
|||
- 未登录用户的白名单工具自动执行,无需审批 |
|||
|
|||
#### `POST /api/agent/chat/tool-approve` |
|||
工具审批(仅已登录用户)。 |
|||
|
|||
``` |
|||
请求: { sessionId: string, messageId: string, toolCallId: string, approved: boolean } |
|||
响应: { code: 0 } |
|||
逻辑: |
|||
- approved=true: 执行工具,返回 tool-result |
|||
- approved=false: 返回拒绝结果给 LLM,LLM 自行处理 |
|||
``` |
|||
|
|||
### 反馈 |
|||
|
|||
#### `POST /api/agent/feedback` |
|||
提交/修改反馈(仅登录用户)。 |
|||
|
|||
``` |
|||
请求: { messageId: string, feedback: "like" | "dislike", comment?: string } |
|||
响应: { code: 0 } |
|||
逻辑: UPSERT(messageId + userId 唯一约束) |
|||
``` |
|||
|
|||
### 迁移 |
|||
|
|||
#### `POST /api/agent/migrate` |
|||
登录后迁移临时 session(前端弹窗确认后调用)。 |
|||
|
|||
``` |
|||
请求: { tempToken: string } |
|||
响应: { code: 0, data: { migratedCount: number } } |
|||
逻辑: |
|||
1. 查询 tempToken 关联的所有未删除、未过期临时 session |
|||
2. 更新 userId = 当前用户,tempToken = null,expiresAt = null |
|||
3. 迁移的 session 置底(lastActiveAt 保持原值,排序靠后) |
|||
4. 清除 tempToken cookie |
|||
``` |
|||
|
|||
### 管理员配置 API |
|||
|
|||
``` |
|||
GET /api/admin/agent-config — 读取所有配置 |
|||
PUT /api/admin/agent-config/system-prompt — 更新 system prompt |
|||
PUT /api/admin/agent-config/default-model — 更新默认模型 + 标题模型 |
|||
PUT /api/admin/agent-config/public-tools — 更新公开工具白名单 |
|||
``` |
|||
|
|||
### 限流实现 |
|||
|
|||
内存缓存(Map),服务重启归零: |
|||
|
|||
```typescript |
|||
// 单 session 计数 |
|||
Map<sessionId, { count: number, date: string }> |
|||
|
|||
// IP 日计数 |
|||
Map<ip, { count: number, date: string }> |
|||
``` |
|||
|
|||
每日 0 点重置(日期字符串比对)。计数时机:assistant 回复完成后(`onFinish`)。 |
|||
|
|||
**前端获取剩余次数:** |
|||
- 未登录用户的 `/api/agent/chat` 响应头返回: |
|||
- `X-RateLimit-Session-Remaining` — 当前 session 剩余次数(20 - 已用) |
|||
- `X-RateLimit-Ip-Remaining` — 当前 IP 今日剩余次数(50 - 已用) |
|||
- 前端从响应头读取,更新 `useAgentLimit` 状态 |
|||
- 达到上限时禁用输入框 + 显示提示 |
|||
|
|||
### 定时任务 |
|||
|
|||
复用 `server/scheduler`,每 10 分钟扫描: |
|||
- 删除 `expiresAt < now AND deletedAt IS NULL` 的临时 session 及其消息(硬删除) |
|||
|
|||
## 前端架构 |
|||
|
|||
### 页面与组件结构 |
|||
|
|||
``` |
|||
app/pages/index.vue — 首页 agent 主页面(layout: 'home') |
|||
app/components/agent/ |
|||
├── AgentSidebar.vue — 侧边栏(session 列表 + 新对话按钮 + 用户信息) |
|||
├── AgentSidebarItem.vue — session 列表项(含操作菜单) |
|||
├── AgentChatArea.vue — 主对话区(工具栏 + 消息列表 + 输入框) |
|||
├── AgentToolbar.vue — 顶部工具栏(汉堡 + 模型选择 + 开关 + system prompt 查看) |
|||
├── AgentMessageList.vue — 消息列表(含日期分隔线 + 滚动管理) |
|||
├── AgentMessageBubble.vue — 单条消息气泡(左右分列) |
|||
├── AgentMessageParts.vue — 消息 parts 渲染(text/reasoning/tool-call) |
|||
├── AgentToolCall.vue — 工具调用折叠展示(含审批按钮) |
|||
├── AgentReasoning.vue — 推理过程展示 |
|||
├── AgentInputBox.vue — 底部输入框(多行自适应 + 附件预留 + 发送) |
|||
├── AgentModelSelector.vue — 模型下拉选择器 |
|||
├── AgentSystemPromptModal.vue — system prompt 查看弹窗 |
|||
├── AgentFeedbackButtons.vue — 👍/👎 反馈按钮 |
|||
└── AgentEmptyState.vue — 空 session 状态 |
|||
|
|||
app/pages/admin/agent-config/ |
|||
├── system-prompt.vue — system prompt 编辑 |
|||
├── default-model.vue — 默认模型 + 标题模型选择 |
|||
└── public-tools.vue — 公开工具白名单多选 |
|||
``` |
|||
|
|||
### Composables |
|||
|
|||
``` |
|||
app/composables/useAgentSession.ts — session CRUD + 列表分页 + 切换 |
|||
app/composables/useAgentChat.ts — 对话核心逻辑(扩展自 useLlmChat) |
|||
app/composables/useAgentFeedback.ts — 反馈提交/修改 |
|||
app/composables/useAgentLimit.ts — 未登录限流状态(前端展示用) |
|||
``` |
|||
|
|||
### `useAgentChat` 核心设计 |
|||
|
|||
基于现有 `useLlmChat` 改造,关键差异: |
|||
1. 不再前端传完整 messages,只传当前消息内容 |
|||
2. sessionId 驱动,切换 session 时重新加载消息 |
|||
3. 支持编辑消息(editMessageId)和重新生成(regenerate) |
|||
4. 停止生成时保留 text parts,丢弃进行中的 tool-call parts |
|||
5. 流式结束后从响应获取 token 用量,更新 assistant message |
|||
|
|||
### 状态管理流程 |
|||
|
|||
``` |
|||
首页加载 |
|||
→ useAgentSession.init() |
|||
→ 检测登录态 |
|||
→ 已登录:加载 session 列表(第 1 页),无 session 则自动新建 |
|||
→ 未登录:检查 cookie token,有则加载临时 session 列表,无则自动新建(触发 token 生成) |
|||
→ useAgentChat.loadMessages(currentSessionId) |
|||
→ 加载最近 30 条消息 |
|||
|
|||
切换 session |
|||
→ useAgentChat.clear() |
|||
→ useAgentChat.loadMessages(newSessionId) |
|||
→ 滚动到底部 |
|||
|
|||
发送消息 |
|||
→ useAgentChat.send(content) |
|||
→ 流式渲染 assistant 回复 |
|||
→ 完成后前端乐观更新 session 列表顺序 |
|||
|
|||
编辑消息 |
|||
→ 点击"编辑" → 内容填入输入框 |
|||
→ 发送时带 editMessageId → 截断 + 重新生成 |
|||
|
|||
重新生成 |
|||
→ 点击"重新生成" → 带 regenerate: true → 删除最后 assistant + 重新生成 |
|||
|
|||
停止生成 |
|||
→ AbortController.abort() |
|||
→ 保留 text parts,清理进行中 tool-call parts |
|||
→ "重新生成"按钮出现在最后一条 assistant 消息下方 |
|||
``` |
|||
|
|||
### 滚动管理 |
|||
|
|||
- **新消息生成**:用户在底部 → 自动滚动;用户向上看历史 → 不自动滚动,显示"回到最新"按钮 |
|||
- **加载历史**:滚动到顶部触发加载,保持当前视口位置 |
|||
- **切换 session**:加载完成后滚动到底部 |
|||
|
|||
### 移动端适配 |
|||
|
|||
- `< 768px`:侧边栏变为抽屉,汉堡按钮在工具栏左侧,滑出时有遮罩 |
|||
- 消息限宽 800px 在移动端自动变为 `100% - padding` |
|||
|
|||
### 样式约定 |
|||
|
|||
- 消息气泡:用户右侧(主色调背景)、assistant 左侧(卡片背景) |
|||
- 工具栏高度 56px,底部固定 |
|||
- 输入框最小高度 64px,自适应增长,最大 200px |
|||
- 代码块:语法高亮 + 右上角复制按钮 |
|||
- 工具调用:折叠式,工具名 + 状态图标(✓/✗/调用中.../待审批) |
|||
- 推理过程:默认展开,显示在回复文本上方 |
|||
- 日期分隔线:跨天时插入居中分隔线 |
|||
- Markdown 渲染:基础 + 表格 + 代码高亮 |
|||
- 每条 assistant 回复下方显示模型名 + token 用量 |
|||
|
|||
### 交互细节 |
|||
|
|||
- 输入框:多行自适应 + Enter 发送 + Shift+Enter 换行 |
|||
- 附件按钮:UI 预留灰色禁用位置,后续迭代启用 |
|||
- Session 列表项:点击切换 + "..."操作菜单(重命名、删除) |
|||
- 新建 session:侧边栏顶部按钮,新 session 置顶 |
|||
- Session 切换:立即清空 + loading + 加载完成渲染 |
|||
- 重新生成按钮:仅最后一条 assistant 消息下方显示 |
|||
- 编辑按钮:仅最后一条用户消息下方显示,点击后内容填入底部输入框 |
|||
- 反馈:仅登录用户,每条 assistant 消息下方 👍/👎,可修改 |
|||
- System prompt 查看:仅登录用户,工具栏按钮 → 弹窗展示 |
|||
|
|||
## 边界情况与错误处理 |
|||
|
|||
### 身份与权限 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| 未登录用户访问他人 session | 403 | |
|||
| 未登录用户 cookie token 过期(2h) | session 列表返回空,当前对话提示"session 已过期",自动新建临时 session | |
|||
| 未登录用户清除了 cookie | 旧临时 session 成为孤儿数据,定时任务 2h 后清理;用户获得新临时 session | |
|||
| 已登录用户访问他人 session | 403 | |
|||
| session 已软删除 | 404 | |
|||
|
|||
### 限流边界 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| 未登录达到 session 上限(20 条 assistant) | 输入框禁用 + "已达对话上限,登录后继续" + 登录按钮 | |
|||
| 未登录达到 IP 日上限(50 条 assistant) | 输入框禁用 + "今日对话次数已达上限,登录后继续" + 登录按钮 | |
|||
| 限流计数时机 | assistant 回复完成后计数(`onFinish`) | |
|||
| 限流计数与停止生成 | 停止生成不产生完整 assistant 回复,不计入限流 | |
|||
|
|||
### 对话异常 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| LLM 请求失败 | 报错提示,不保存 assistant message,用户可重新生成 | |
|||
| LLM 返回空内容 | 提示"模型未返回内容",删除空 assistant message | |
|||
| 工具调用失败 | tool-call part 标记"执行失败"(红色 ✗),LLM 自行处理错误 | |
|||
| 工具审批被拒(已登录用户) | tool-call part 标记"已拒绝"(红色 ✗),LLM 收到拒绝结果 | |
|||
| 流式中断(网络断开) | 保留已生成 text parts,清理进行中 tool-call parts,提示"网络中断" | |
|||
| 停止生成 | 保留 text parts,清理进行中 tool-call parts,显示"重新生成"按钮 | |
|||
|
|||
### Session 迁移 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| 迁移时临时 session 已过期 | 跳过过期 session,只迁移未过期的 | |
|||
| 迁移时用户取消 | 不迁移,临时 session 继续按原 TTL 过期 | |
|||
| 迁移后 cookie token | 清除 `tempToken` cookie | |
|||
| 迁移的 session 位置 | 置底(`lastActiveAt` 保持原值,排序靠后) | |
|||
|
|||
### 消息操作 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| 编辑消息后截断 | 硬删除被截断的 messages 及其 feedback | |
|||
| 重新生成 | 硬删除最后一条 assistant message 及其 feedback | |
|||
| 编辑最后一条 user message | 正常流程(截断 0 条 + 更新内容 + 重新生成) | |
|||
| 编辑中间某条 user message | 截断该消息之后所有 messages + 更新内容 + 重新生成 | |
|||
|
|||
### 数据一致性 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| assistant message 保存失败 | 日志记录,消息仍返回给用户(不阻塞流式响应) | |
|||
| 标题生成失败 | session 保持"新对话"默认标题,日志记录 | |
|||
| 反馈的 message 被截断删除 | feedback 一并硬删除 | |
|||
|
|||
### 前端状态同步 |
|||
|
|||
| 场景 | 处理 | |
|||
|------|------| |
|||
| 多标签页同时操作 | 不做特殊处理(个人 agent 场景) | |
|||
| session 列表顺序 | 发送/接收完成后前端乐观更新 `lastActiveAt` 并重新排序列表 | |
|||
| 新建 session | 前端立即插入列表顶部,API 返回后替换为真实数据 | |
|||
|
|||
## 测试策略 |
|||
|
|||
### 后端测试 |
|||
|
|||
| 模块 | 测试点 | |
|||
|------|--------| |
|||
| Session CRUD | 创建/查询/重命名/软删除/权限校验 | |
|||
| 临时 session | token 生成、cookie 设置、过期判定、孤儿清理 | |
|||
| 限流 | session 计数、IP 计数、每日重置、达到上限拦截 | |
|||
| 对话流程 | 消息保存、历史加载、流式响应、token 用量记录 | |
|||
| 编辑/重新生成 | 截断逻辑、feedback 级联删除 | |
|||
| 工具注入 | 已登录全量(含审批)/已登录关闭/未登录白名单(自动执行) | |
|||
| 标题生成 | 首轮 assistant 回复后触发、失败兜底 | |
|||
| 迁移 | 全量迁移、过期跳过、cookie 清除、置底排序 | |
|||
| 反馈 | 提交/修改/撤销、未登录拦截 | |
|||
| 工具审批 | 同意执行/拒绝返回、未登录自动执行 | |
|||
|
|||
### 前端测试 |
|||
|
|||
| 模块 | 测试点 | |
|||
|------|--------| |
|||
| 消息渲染 | text/reasoning/tool-call/tool-result 各 part 类型 | |
|||
| 工具调用状态 | 调用中/成功/失败/拒绝/待审批 折叠展示 | |
|||
| 滚动管理 | 自动滚动/保持视口/回到最新/顶部加载 | |
|||
| 停止生成 | text 保留、tool-call 清理、重新生成按钮 | |
|||
| 编辑消息 | 输入框填充、截断、重新生成 | |
|||
| 限流 UI | 输入框禁用、提示文案、登录按钮、剩余次数显示 | |
|||
| 工具审批 | 审批按钮展示、同意/拒绝流程 | |
|||
| 移动端 | 抽屉化、遮罩、汉堡按钮 | |
|||
|
|||
## 不在本次范围内(后续迭代预留) |
|||
|
|||
- Session 分享功能(生成公开链接) |
|||
- 会话导出功能 |
|||
- 会话内搜索 |
|||
- 会话标签/分类 |
|||
- 收藏/置顶消息 |
|||
- 会话统计 |
|||
- 自定义 agent 人设 |
|||
- 会话模板 |
|||
- 全屏对话模式 |
|||
- 多语言支持 |
|||
- 语音功能 |
|||
- 快捷指令(`/` 弹出工具列表) |
|||
- 文件上传 |
|||
- 管理员反馈数据查看页面 |
|||
Loading…
Reference in new issue