Files
deerflow-code/web-code/components/agent/doc.md
T
2026-05-27 19:43:15 +08:00

737 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent 组件开发问答(Q&A)
> 目录范围:`src/components/agent`
>
> 涵盖文件:
> - `AgentExpertCenterPage.tsx`
> - `AgentExpertEditPage.tsx`
> - `AgentChatPage.tsx`
> - `SkillCreatorPage.tsx`
> - `SkillsWorkshop.tsx`
> - `SkillCreatorThreadSidebar.tsx`
> - `AgentConversationList.tsx`
> - `SkillCreatorConversationItem.tsx`
> - `ChatInputComposer.tsx`
> - `AgentChatPage.css`
---
## Q1:这个目录整体是干什么的?
**A:这是“专家 Agent + 技能工坊 + 技能创建对话”的前端 UI 组件集合。**
- `AgentExpertCenterPage`:专家列表入口(我的专家/发布专家)。
- `AgentExpertEditPage`:专家配置编辑页(左侧对话预览 + 右侧配置)。
- `AgentChatPage`:某个专家的真实聊天页(含线程、文件上传、流式回答)。
- `SkillsWorkshop`:技能资产中心(搜索/筛选/接入/发布/删除/编辑)。
- `SkillCreatorPage`:技能创建/改写专用对话页(支持文件树预览和保存弹窗)。
- `SkillCreatorThreadSidebar`:复用的会话侧边栏(获取线程、新建、删除、切换)。
- `AgentConversationList` + `SkillCreatorConversationItem`:统一消息列表与消息卡片渲染。
- `ChatInputComposer`:统一输入框(发送/停止/上传)。
- `AgentChatPage.css`:`AgentChatPage` 专用动画和滚动条样式。
---
## Q2:核心共享数据模型有哪些?
**A:对话层主要围绕 `ConversationMessage`。**
- 定义位置:`AgentConversationList.tsx`
- 字段:
- `id`: 消息唯一标识。
- `role`: `'user' | 'assistant'`
- `content`: 文本内容(markdown 渲染)。
- `steps`: 思考步骤列表(数据来自 `data-thinking-step`)。
- `thinkings`: 推理内容列表(来自 `reasoning-*` 事件)。
- `toolCalls`: 工具调用(输入输出)列表。
**A:文件树使用 `FileNode`。**
- 在 `AgentChatPage`、`SkillCreatorPage` 内部定义并使用。
- 字段:`name/path/type/size/extension/children`。
**A:线程侧边栏使用 `SkillCreatorThread`。**
- 定义位置:`SkillCreatorThreadSidebar.tsx`
- 字段:`id/title/archived/expert_agent_id/created_at/updated_at`。
---
## Q3:整个目录的“数据流主干”怎么走?
**A:可以看成 3 条主链路。**
### 1) 专家链路(Center -> Edit -> Chat)
1. `AgentExpertCenterPage` 拉专家列表(我的/发布)。
2. 点击“编辑”跳 `AgentExpertEditPage`,加载专家详情 + 工具 + 技能绑定状态。
3. 点击“进行对话”跳 `AgentChatPage`,加载该专家详情和对应会话消息。
### 2) 技能链路(Workshop -> SkillCreator)
1. `SkillsWorkshop` 拉技能列表(official/mine),本地映射成 UI `Skill`。
2. 新建技能:创建会话后跳 `SkillCreatorPage?id=threadId`。
3. AI 辅助编辑已有技能:先创建线程,再 copy skill 到线程,再跳转 `SkillCreatorPage`。
### 3) 对话链路(输入 -> SSE -> UI -> 持久化)
1. `ChatInputComposer` 触发 `onSend`。
2. 页面组件(`AgentChatPage` 或 `SkillCreatorPage`)执行 `handleSendMessage`。
3. 先写入用户消息 + assistant 占位消息。
4. 调流式接口读取 SSE:
- `data-thinking-step` -> 更新 `steps`
- `reasoning-start/delta/end` -> 更新 `thinkings`
- `tool-input-available/tool-output-available` -> 更新 `toolCalls`
- `text-delta` -> 追加 `content`
5. 完成后将 assistant 结果再次 POST 到消息接口持久化。
---
## Q4:`AgentExpertCenterPage` 组件是干什么的?
**A:专家资产“总览入口”,负责展示列表和基础操作入口。**
### 组件职责
- 展示“我的专家”和“发布专家”两种视图。
- 打开专家详情弹窗(支持发布状态切换)。
- 提供“编辑”和“进行对话”跳转入口。
### 数据来源与接口
- 列表查询:
- `POST /api/v1/expert-agents/list`(按 `is_draft`)
- 我的专家场景还会额外 `GET /api/v1/expert-agents` 做合并去重。
- 状态切换:
- `PATCH /api/v1/expert-agents/{id}`(更新 `is_draft`)
### 数据流
- `activeTab` 改变 -> `fetchExpertList` 触发 -> 更新 `agentList`。
- `agentList` -> `cards(useMemo)` -> 渲染卡片。
- 卡片按钮:
- 编辑 -> `navigate('/qa-center/agent-expert-edit?id=...')`
- 对话 -> `navigate('/qa-center/agent-chat?id=...')`
### 维护注意
- “创建专家”按钮目前是 UI 壳子,未接创建接口。
- “删除”按钮当前仅 `console.log`,未接真实删除逻辑。
---
## Q5:`AgentExpertEditPage` 组件是干什么的?
**A:专家配置编辑工作台,同时内置一个“测试对话区”。**
### 组件职责
- 左侧:和该专家进行流式对话(非真实聊天线程,使用 no_real 线程)。
- 右侧:编辑专家基础配置(名称、模型、描述、系统提示词)。
- 管理工具绑定、技能绑定(弹窗操作)。
### 关键状态
- 对话相关:`messages/inputValue/threadId/isStreaming`
- 配置相关:`formValues`
- 工具相关:`toolOptions/selectedTools/pendingTools/boundToolIdMap`
- 技能相关:`skillOptions/selectedSkills/boundSkillMap/skillKeyword`
### 主要接口
- 专家详情:`GET /api/v1/expert-agents/{agentId}`
- 保存专家:`PUT /api/v1/expert-agents/{agentId}`
- 绑定工具:
- 查询:`GET /api/v1/expert-agents/{agentId}/tools`
- 添加:`POST /api/v1/expert-agents/{agentId}/tools`
- 删除:`DELETE /api/v1/expert-agents/{agentId}/tools/{toolId}`
- 绑定技能:
- 查询:`GET /api/v1/expert-agents/{agentId}/skills`
- 添加:`POST /api/v1/expert-agents/{agentId}/skills`
- 删除:`DELETE /api/v1/expert-agents/{agentId}/skills/{skillId}`
- 测试对话:
- 线程:`GET /api/v1/expert-agent-chat/threads/no_real/{agentId}`
- 用户消息:`POST /api/v1/expert-agent-chat/messages`
- SSE:`POST /api/v1/expert-agent-chat/run/stream/help_expert`
### 数据流(对话)
- 用户发送 -> append user msg -> 保证 threadId -> 保存用户消息 -> append assistant 占位 -> SSE 增量更新。
### 维护注意
- 右侧 `preview` 区底部 `ChatInputComposer` 是只读展示,不参与真实请求。
- 工具与技能弹窗是“前端状态 + 后端同步”双层状态,调试时优先检查 refresh 查询是否成功。
---
## Q6:`AgentChatPage` 组件是干什么的?
**A:专家“真实聊天页”,并带线程管理、文件上传和文件预览。**
### 组件职责
- 根据 URL `id` 读取专家详情。
- 管理当前线程(localStorage 持久化)。
- 获取历史消息并转换为统一消息结构。
- 流式发送消息并渲染思考过程。
- 管理会话文件(上传、树形结构、预览、侧栏缩放)。
### 关键状态
- 会话:`currentThreadId/messages/inputValue/isStreaming/isLoading`
- UI:`isThreadSidebarOpen/autoScrollEnabled/showScrollButton`
- 文件:`fileList/fileListThreadId/isFileSidebarOpen/expandedPaths/selectedFile/filePreview*`
### 主要接口
- 专家详情:`GET /api/v1/expert-agents/{agentId}`
- 会话消息:
- 历史:`GET /api/v1/expert-agent-chat/threads/{id}/messages`
- 保存用户:`POST /api/v1/expert-agent-chat/messages`
- SSE:`POST /api/v1/expert-agent-chat/run/stream`
- 保存助手:`POST /api/v1/expert-agent-chat/messages`
- 文件:
- 列表:`GET /api/v1/expert-agent-chat/threads/{id}/files/all`
- 上传:`POST /api/v1/expert-agent-chat/threads/{id}/files/upload`
- 预览:`GET /api/v1/expert-agent-chat/threads/{id}/files/preview?path=...`
### 关键行为细节
- **发送即出现 assistant 占位**:点击发送后立即插入 assistant 空消息,保证用户立刻看到“连接中”态。
- **连接中显示规则**:由 `AgentConversationList` 判断“最后一条 assistant 且还没有任何生成内容”才显示连接状态。
- **失败回滚**:发送或流式失败时移除该 assistant 占位,避免残留空卡片。
- **停止流式**:`AbortController` 中断请求,并清理 loading 态字段。
### 维护注意
- `threadStorageKey` 与 `agentId` 强绑定,切换专家会切换独立 thread 记忆。
- `normalizeHistoryContent` 是历史消息兼容核心,后端 content 结构变更时先改这里。
---
## Q7:`SkillCreatorPage` 组件是干什么的?
**A:技能创建/重构专用对话页,结构上是 `AgentChatPage` 的“技能场景定制版”。**
### 组件职责
- 从 query/localStorage 同步当前线程 ID。
- 加载技能会话历史与会话文件。
- 走技能专属流式接口输出思考步骤/推理/tool call/文本。
- 提供“保存技能”弹窗(当前主要是 UI + 本地状态)。
### 与 `AgentChatPage` 的主要差异
- 线程 ID 同步到 URL(`useSearchParams`)。
- 有独立 `isConnecting` 状态,用于连接中阶段展示。
- 流式接口是:`POST /api/v1/expert-agent-chat/skills-agent`
- 页面内注入了 dot/shimmer 动画样式(供消息连接中态使用)。
### 主要接口
- 会话消息:`GET /threads/{id}/messages`
- 流式:`POST /skills-agent`
- 保存 assistant:`POST /messages`
- 文件:`GET all` / `POST upload` / `GET preview`
### 数据流
- 发消息 -> append 用户 + assistant 占位 -> `isConnecting=true` -> 收到首个流块后 `isConnecting=false` -> 增量刷新 assistant -> 持久化。
### 维护注意
- 保存技能按钮目前仅 `console.log` 输出技能名和描述,未接正式后端保存接口。
---
## Q8:`SkillsWorkshop` 组件是干什么的?
**A:技能资产管理页(发现、筛选、接入、发布、删除、进入编辑)。**
### 组件职责
- 拉取 `official` 与 `mine` 技能,映射成统一 UI 结构。
- 支持搜索、类型筛选、分类筛选。
- 支持官方技能接入到“我的技能”。
- 支持发布、删除、编辑(编辑弹窗支持 AI 编辑和上传覆盖)。
- 支持“新建技能体”:Agent 对话创建 / 上传文件创建。
### 主要接口
- 列表:`GET /api/v1/skills?type=official|mine`
- 发布:`POST /api/v1/expert-agents/skills/publish`
- 删除我的技能:`DELETE /api/v1/skills/mine/{skillId}`
- 上传覆盖:`PUT /api/v1/skills/{skillId}/upload`
- 接入官方技能:`POST /api/v1/skills/mine/add`
- 上传创建:`POST /api/v1/skills/upload`
- Agent 创建线程:`POST /api/v1/expert-agent-chat/threads`
- AI 编辑复制技能:`POST /threads/{threadId}/copy-skill/{skillId}`
### 数据流
- filter/search 改变 -> `useEffect` 重新拉取技能 -> 本地去重并缓存到 `skills`。
- 卡片编辑 -> `editingSkill` 打开弹窗 -> 选择 AI 编辑时创建线程后跳 `SkillCreatorPage`。
### 维护注意
- `inferCategory` 是关键词规则映射,新增技能类型时可优先扩展该规则。
- “进入”按钮目前使用 `skill.id` 跳 `skill-creator`,若后端语义改成 threadId 需调整。
---
## Q9:`SkillCreatorThreadSidebar` 组件是干什么的?
**A:会话线程管理的复用侧边栏。**
### 组件职责
- 打开时加载会话列表。
- 新建会话并自动选中。
- 删除会话并在当前会话被删时自动回退到其它会话。
- 支持线程过滤(如仅展示当前专家线程)。
### 输入输出(Props 视角)
- 输入:`authedFetch/currentThreadId/isOpen/isBusy/threadFilter/createThreadPayload...`
- 输出:
- `onThreadSelect(threadId)`
- `onClose()`
### 数据流
- `isOpen` 从 false -> true:触发 `refreshThreads`。
- 新建成功:更新本地列表并触发 `onThreadSelect`。
- 删除成功:本地移除;若删的是当前线程,自动选择 fallback。
### 维护注意
- `createThreadPayload` 是高复用扩展点,场景页可注入 `expert_agent_id/title` 等字段。
---
## Q10:`AgentConversationList` 组件是干什么的?
**A:统一遍历消息并决定“连接中状态”是否显示。**
### 核心逻辑
- 逐条渲染 `SkillCreatorConversationItem`。
- 只在以下条件满足时给最后一条 assistant 传 `isConnecting=true`:
- 页面处于连接/流式阶段;
- 当前消息是最后一条 assistant;
- 且还没有任何实际生成内容(`content/steps/thinkings/toolCalls` 都为空)。
### 价值
- 将“连接中占位显示时机”从页面组件抽离,统一控制,减少重复判断代码。
---
## Q11:`SkillCreatorConversationItem` 组件是干什么的?
**A:单条消息卡片渲染器(用户消息 + 助手多面板消息)。**
### 渲染结构
- 用户消息:右侧气泡 + user avatar。
- 助手消息:左侧 avatar + 多段内容:
- `StepsSummary`
- `ThinkingPanel[]`
- `ToolCallPanel[]`
- 连接中动画文案(`正在连接技能服务...`)
- Markdown 正文
### 数据来源
- 完全由父级传入 `message` 和 `isConnecting` 控制,不直接请求后端。
### 维护注意
- 此组件是所有对话页的统一呈现层;若改动 message schema,要同步更新这里的各面板。
---
## Q12:`ChatInputComposer` 组件是干什么的?
**A:通用输入组件,封装输入、发送/停止、上传文件。**
### 输入输出(Props)
- 输入:`value/disabled/readOnly/isSending/isDarkMode/showUpload/placeholder...`
- 回调:
- `onChange(value)`
- `onSend()`
- `onStop()`
- `onUploadFile(file)`
### 关键交互
- Enter(非 Shift)触发发送(前提:`submitEnabled` 且非 `isSending`)。
- 发送按钮在发送中切换为停止按钮(`onClick={isSending ? onStop : onSend}`)。
- 文件上传通过隐藏 `input[type=file]` + 按钮触发。
### 维护注意
- 当前实现里文本框只受 `disabled` 控制,不再因为 `isSending` 禁止输入。
- 上传按钮仍受 `disabled || !onUploadFile` 控制。
---
## Q13:`AgentChatPage.css` 主要做了什么?
**A:提供 `AgentChatPage` 的辅助样式。**
- `.agent-resizing`:拖拽文件侧栏时全局 cursor/user-select 控制。
- `.agent-file-tree-scroll` / `.agent-file-preview-scroll`:滚动条样式统一。
- `.agent-file-markdown-preview`:markdown 首尾边距和图片样式收敛。
- `.animate-breathing` 等动画类:预留呼吸动效。
---
## Q14:典型业务流程:发送一条消息时,数据如何传输?
**A:以 `AgentChatPage` 为例。**
1. 用户在 `ChatInputComposer` 输入后点击发送。
2. `handleSendMessage` 校验 `text/threadId/isStreaming`。
3. 立即写入:
- user message
- assistant 占位 message(空内容)
4. 调 `POST /messages` 落用户消息。
5. 调 `POST /run/stream` 开始 SSE。
6. 每个 SSE event 增量更新 assistant message:
- step -> `steps`
- reasoning -> `thinkings`
- tool -> `toolCalls`
- text -> `content`
7. 流结束后将聚合结果组装成 `persistedContent`,再 `POST /messages` 落库。
8. 刷新文件列表(对可能新增输出文件做同步)。
---
## Q15:典型业务流程:从技能工坊进入技能创建页,数据如何传输?
**A:两种入口。**
### 入口 A:新建技能(Agent 构建)
1. `SkillsWorkshop.handleCreateSkillByAgent` 创建线程。
2. 将 `threadId` 写入 localStorage。
3. 跳转 `navigate('/qa-center/skill-creator?id=threadId')`。
4. `SkillCreatorPage` 用 query/localStorage 同步 `currentThreadId`,然后拉消息和文件。
### 入口 B:AI 辅助编辑已有技能
1. 创建线程。
2. `POST /threads/{threadId}/copy-skill/{skillId}` 把技能内容复制到线程工作区。
3. 跳转 SkillCreator 页面,后续在线程中对话编辑。
---
## Q16:如果要新增一个“对话页”,建议复用哪些组件?
**A:建议最小复用组合如下。**
- 消息列表:`AgentConversationList`
- 单消息卡片:`SkillCreatorConversationItem`(自动包含 steps/thinking/tool 等面板)
- 输入框:`ChatInputComposer`
- 线程侧边栏:`SkillCreatorThreadSidebar`
**页面层只负责:**
- 状态管理(`messages/input/isStreaming/currentThreadId`)
- 接口请求(历史 + SSE + 持久化)
- 事件映射(把后端事件转为 `ConversationMessage` 结构)
---
## Q17:维护时最容易踩坑的点有哪些?
**A:建议优先关注这 8 个点。**
1. **SSE 事件兼容性**:后端 event type 或字段名变化会直接导致 UI 不更新。
2. **历史内容兼容**:`normalizeHistoryContent` 没跟上 content schema 会出现历史消息空白。
3. **线程切换中断**:切换线程时要记得 abort 流并清状态,避免串流到新线程。
4. **占位消息回收**:发送失败/流失败需移除 assistant 占位,避免残留空卡片。
5. **threadId 语义一致性**:某些页面参数传 skillId/agentId,需避免误当 threadId 使用。
6. **工具/技能绑定状态双源**:本地状态和后端状态不同步时,优先以 refresh 查询结果为准。
7. **上传入口 accept 限制**:上传文件类型修改时要同步后端校验和 UI 提示。
8. **深色主题样式**:该目录深色/浅色分支很多,改样式需双主题回归。
---
## Q18:后续文档扩展建议
**A:当前文档适合开发入门,下一步可补充“接口契约版”。**
- 补完整 SSE event JSON 示例(每种事件一例)。
- 增加错误码到 UI 提示映射表。
- 补一份“页面状态机图”(Idle / Connecting / Streaming / Stopped / Error)。
- 给每个页面补“可测点清单”(单测/联调/回归)。
---
## Q19:能否给一份可联调的“接口契约附录”?
**A:可以,以下按模块整理。**
### 1) 专家模块(Expert)
- 查询专家列表
- `POST /api/v1/expert-agents/list`
- 请求体:
- `is_draft: boolean`
- 响应(数组项):
- `id/name/avatar_url/description/model_name/is_draft/user_id/created_at`
- 查询专家详情
- `GET /api/v1/expert-agents/{agentId}`
- 响应关键字段:
- `id/name/description/system_prompt/model_name`
- 更新专家配置
- `PUT /api/v1/expert-agents/{agentId}`
- 请求体关键字段:
- `name`
- `description`
- `system_prompt`
- `model_name`
- `avatar_url`(当前传 `null`)
- 切换草稿/发布状态
- `PATCH /api/v1/expert-agents/{agentId}`
- 请求体:
- `is_draft: boolean`
### 2) 线程与消息模块(Chat Thread + Message)
- 查询线程列表
- `GET /api/v1/expert-agent-chat/threads`
- 响应:
- `threads: SkillCreatorThread[]`
- `archived_threads: SkillCreatorThread[]`
- 新建线程
- `POST /api/v1/expert-agent-chat/threads`
- 请求体常见字段:
- `title`
- `archived`
- `is_real_chat`
- `expert_agent_id`(专家聊天场景)
- 删除线程
- `DELETE /api/v1/expert-agent-chat/threads/{threadId}`
- 查询线程消息历史
- `GET /api/v1/expert-agent-chat/threads/{threadId}/messages`
- 响应消息项关键字段:
- `id/thread_id/role/content/created_at`
- 保存消息(用户或助手)
- `POST /api/v1/expert-agent-chat/messages`
- 请求体(用户消息):
- `thread_id`
- `role: "user"`
- `content: { type: "text", text: string }`
- 请求体(助手消息):
- `thread_id`
- `role: "assistant"`
- `content: persistedContent[]`
### 3) 流式回复模块(SSE)
- 专家聊天流式
- `POST /api/v1/expert-agent-chat/run/stream`
- 请求体关键字段:
- `chat_id`
- `expert_agent_id`
- `user_query`
- `messages`
- 专家编辑页测试流式
- `POST /api/v1/expert-agent-chat/run/stream/help_expert`
- 技能创建流式
- `POST /api/v1/expert-agent-chat/skills-agent`
- 请求体关键字段:
- `chat_id`
- `user_query`
### 4) 文件模块(线程文件)
- 查询线程文件树
- `GET /api/v1/expert-agent-chat/threads/{threadId}/files/all`
- 上传文件
- `POST /api/v1/expert-agent-chat/threads/{threadId}/files/upload`
- form-data:
- `file`
- 文件预览
- `GET /api/v1/expert-agent-chat/threads/{threadId}/files/preview?path=...`
- 兼容响应:
- 文本
- JSON 字符串
- JSON 对象中的 `content`
### 5) 技能模块(Skills)
- 查询技能列表
- `GET /api/v1/skills?type=official|mine|all&keyword=&page=1&page_size=100`
- 发布技能
- `POST /api/v1/expert-agents/skills/publish`
- 请求体:
- `skill_id`
- 添加官方技能到我的技能
- `POST /api/v1/skills/mine/add`
- 请求体:
- `official_skill_id`
- 删除我的技能
- `DELETE /api/v1/skills/mine/{skillId}`
- 上传技能文件创建
- `POST /api/v1/skills/upload`
- 上传覆盖已有技能
- `PUT /api/v1/skills/{skillId}/upload`
### 6) 专家绑定工具/技能
- 工具
- 查询:`GET /api/v1/expert-agents/{agentId}/tools`
- 绑定:`POST /api/v1/expert-agents/{agentId}/tools`(`tool_name`)
- 解绑:`DELETE /api/v1/expert-agents/{agentId}/tools/{toolId}`
- 技能
- 查询:`GET /api/v1/expert-agents/{agentId}/skills`
- 绑定:`POST /api/v1/expert-agents/{agentId}/skills`(`skill_id`)
- 解绑:`DELETE /api/v1/expert-agents/{agentId}/skills/{skillId}`
---
## Q20:SSE 事件规范可以补全吗?
**A:可以,以下是前端当前实现依赖的事件集合。**
### 1) 事件封包格式
- 每行以 `data: ` 开头
- 正文为 JSON 字符串
- 结束标记:`data: [DONE]`
### 2) 前端识别的事件类型
- `data-thinking-step`
- 用途:更新执行步骤面板
- 关键字段:
- `data.id`
- `data.title`
- `data.status`(`completed` 或其它)
- `data.items[]`
- `reasoning-start`
- 用途:开始一段思考
- 行为:创建新的 thinking 段,记录开始时间
- `reasoning-delta`
- 用途:思考内容增量
- 关键字段:
- `delta`
- `reasoning-end`
- 用途:结束当前思考段
- 行为:计算该段耗时并写入 `duration`
- `tool-input-available`
- 用途:记录工具调用入参
- 关键字段:
- `toolCallId`
- `toolName`
- `input`
- `tool-output-available`
- 用途:记录工具调用输出
- 关键字段:
- `toolCallId`
- `output`
- `text-delta`
- 用途:助手正文 token 增量
- 关键字段:
- `delta`
### 3) 推荐后端保证
- 同一次回复里的 `toolCallId` 全局唯一。
- `reasoning-start` 与 `reasoning-end` 成对(允许无 `delta`)。
- `data-thinking-step` 同一个 `id` 可重复更新状态。
- `text-delta` 在结束前允许为空片段,但尽量减少空片段。
### 4) 前端容错策略(已实现)
- JSON parse 失败:跳过该行,不中断整流。
- 未识别 event.type:忽略。
- `output` 非字符串:尝试 `result` 字段,否则 `JSON.stringify`。
- `AbortError`:按“用户主动停止”处理,不弹致命错误。
---
## Q21:可以给一份页面级测试清单吗?
**A:可以,以下清单按页面拆分,适合联调和回归。**
### A. `AgentChatPage` 测试清单
- 打开页面后,能加载专家信息(名称、描述)。
- 未选线程时,空态正确显示;选中线程后历史消息正确渲染。
- 点击发送后立即出现 assistant 占位卡片,显示连接中状态。
- 首个有效流式事件到达后,连接中占位消失,转为正常内容增量渲染。
- 发送失败/流失败时,assistant 占位会被回收,不留空白卡片。
- 点击停止后,流式中断,按钮状态恢复,后续不再追加内容。
- 上传文件后文件树刷新;文件预览支持 text/json 两类返回。
- 切换线程时会中断当前流,且不会把旧线程流内容写到新线程。
### B. `SkillCreatorPage` 测试清单
- URL `id` 与 localStorage 线程 ID 同步正确。
- 新线程空态和快捷提示按钮显示正常。
- 发送后 `isConnecting=true`,收到首包后 `isConnecting=false`。
- steps/thinking/tool/text 四类流式内容都能正确出现。
- 线程切换、停止流、文件上传/预览路径行为与 `AgentChatPage` 一致。
- 保存技能弹窗开关正常(当前为 UI 行为)。
### C. `AgentExpertEditPage` 测试清单
- 进入页面后基础信息(name/model/description/system_prompt)能回填。
- 保存专家配置后接口成功,按钮 loading 状态恢复。
- 工具弹窗:勾选新增 -> 确认添加 -> 列表刷新一致。
- 技能弹窗:绑定/解绑/一键绑定后状态与后端一致。
- 左侧测试对话能正常流式返回,滚动与发送状态正确。
### D. `SkillsWorkshop` 测试清单
- `all`/`mine` 切换结果正确,搜索词可生效。
- 分类筛选(writing/analysis/decision/validation/workflow)正确过滤。
- 官方技能“+”接入后卡片状态转“已接入”。
- 我的技能菜单中的发布/删除操作正确执行。
- 编辑弹窗中 AI 编辑路径可成功创建线程并跳转 SkillCreator。
- 上传创建、上传覆盖两条路径成功后状态正确恢复(含 input 清空)。
### E. `AgentExpertCenterPage` 测试清单
- mine/published 标签切换后列表正确变化。
- mine 模式会按当前用户过滤,且与全量接口合并去重后正确展示。
- 详情弹窗中的发布开关可切换并刷新列表。
- 编辑、对话跳转路径参数正确。
### F. 主题与可用性回归
- 深色/浅色主题下对话气泡、按钮、边框、hover 都可读。
- 长会话场景滚动性能可接受,自动滚动按钮行为符合预期。
- 键盘发送(Enter)与换行(Shift+Enter)行为正确。