You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

23 KiB

首页对外 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

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

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

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

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 读 tempTokenWHERE 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 的 userIdtempToken 必须匹配当前身份。

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),服务重启归零:

// 单 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 人设
  • 会话模板
  • 全屏对话模式
  • 多语言支持
  • 语音功能
  • 快捷指令(/ 弹出工具列表)
  • 文件上传
  • 管理员反馈数据查看页面