Browse Source

docs: 首页对外 Agent 设计方案

Co-authored-by: CodeFree <codefree@chinatelcom.cn>
feat/ai-sdk-v6-upgrade
npmrun 16 hours ago
parent
commit
377e11946e
  1. 578
      docs/superpowers/specs/2026-08-06-homepage-agent-design.md

578
docs/superpowers/specs/2026-08-06-homepage-agent-design.md

@ -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…
Cancel
Save