# Chat 对话功能迁移重构计划(v2) ## 目标 彻底抛弃 zn-ai 现有的 `WebSocketManager` 直联方式,**全面对齐 ClawX 的对话架构与协议栈**(Gateway RPC + 结构化消息流)。UI 视觉与交互布局**严格沿用 zn-ai 现有设计**(蓝色主题 `#2B7FFF`、头像左右布局、圆角输入框、欢迎页文案)。 --- ## 参考来源 - **ClawX 源文件**: - `ClawX/src/stores/chat.ts` — Zustand Chat Store(状态、会话、流式、附件缓存、错误恢复) - `ClawX/src/stores/chat/types.ts` — `RawMessage`、`ContentBlock`、`ToolStatus`、`ChatSession` - `ClawX/src/pages/Chat/index.tsx` — 页面组装、生命周期、欢迎页、Indicator - `ClawX/src/pages/Chat/ChatMessage.tsx` — 消息渲染(markdown、thinking、tool、image、attachment) - `ClawX/src/pages/Chat/ChatInput.tsx` — 输入框(附件、Agent @提及、发送/停止) - `ClawX/src/pages/Chat/ExecutionGraphCard.tsx` — 任务执行可视化 - **zn-ai 目标文件**: - `zn-ai/src/pages/home/index.vue` - `zn-ai/src/pages/home/ChatBox.vue` - `zn-ai/src/pages/home/components/ChatInputArea.vue` - `zn-ai/src/pages/home/components/ChatRoleAI.vue` - `zn-ai/src/pages/home/components/ChatRoleMe.vue` - `zn-ai/src/pages/home/model/ChatModel.ts` --- ## 一、协议与模型层重构 ### 1.1 消息模型:从 `ChatMessage` class 迁移到 `RawMessage` 协议 zn-ai 当前 `ChatMessage` 只有 `messageContent: string` + `messageContentList: string[]`,无法承载 ClawX 的结构化内容。需重构为: ```ts // zn-ai/src/pages/home/model/ChatModel.ts export interface AttachedFileMeta { fileName: string; mimeType: string; fileSize: number; preview: string | null; filePath?: string; source?: 'user-upload' | 'tool-result' | 'message-ref'; } export interface ContentBlock { type: 'text' | 'image' | 'thinking' | 'tool_use' | 'tool_result' | 'toolCall' | 'toolResult'; text?: string; thinking?: string; source?: { type: string; media_type?: string; data?: string; url?: string }; data?: string; mimeType?: string; id?: string; name?: string; input?: unknown; arguments?: unknown; content?: unknown; } export interface RawMessage { role: 'user' | 'assistant' | 'system' | 'toolresult'; content: string | ContentBlock[]; timestamp?: number; id?: string; toolCallId?: string; toolName?: string; details?: unknown; isError?: boolean; _attachedFiles?: AttachedFileMeta[]; } export interface ToolStatus { id?: string; toolCallId?: string; name: string; status: 'running' | 'completed' | 'error'; durationMs?: number; summary?: string; updatedAt: number; } export interface ChatSession { key: string; label?: string; displayName?: string; thinkingLevel?: string; model?: string; updatedAt?: number; } ``` > **说明**: > - `messageContentList` 和 `isLoading` 字段将被移除。流式渲染不再依赖数组分段,而是通过 `streamingMessage` 增量更新。 > - 历史消息加载后,UI 直接使用 `md.render()` 对 `extractText(msg)` 的结果做一次性 markdown 渲染。 > - `question`(问题标签)作为 zn-ai 特有字段,可在 `ChatMessage` 组件的 footer slot 中继续保留。 --- ## 二、Pinia Chat Store(核心状态层) 新建 `zn-ai/src/store/chat.ts`,**完整复刻 ClawX Chat Store 的语义与行为**,仅将 Zustand API 替换为 Pinia `defineStore`。 ### 2.1 State 设计 ```ts interface ChatState { // 消息 messages: RawMessage[]; loading: boolean; // 历史消息加载中 error: string | null; // 底部全局错误条 // 流式与运行时 sending: boolean; // 是否正在发送/等待回复 activeRunId: string | null; // 当前运行 ID(用于事件去重) streamingText: string; // 纯文本流式缓存 streamingMessage: unknown | null; // 当前流式消息对象 streamingTools: ToolStatus[]; // 工具调用实时状态列表 pendingFinal: boolean; // 是否处于 tool-result 后到最终回复前的等待期 lastUserMessageAt: number | null; // 用于保留乐观用户消息 pendingToolImages: AttachedFileMeta[]; // 等待挂到最终 assistant 消息的图片 // 会话 sessions: ChatSession[]; currentSessionKey: string; currentAgentId: string; sessionLabels: Record; sessionLastActivity: Record; // Thinking showThinking: boolean; thinkingLevel: string | null; } ``` ### 2.2 Actions(与 ClawX 1:1 映射) | Action | 职责 | |--------|------| | `loadSessions()` | 加载会话列表,去重、排序、补全 label | | `switchSession(key)` | 切会话,清理旧会话轮询,清空当前流式状态 | | `newSession()` | 新建会话,清理空会话 | | `deleteSession(key)` | 删除会话(本地 + 远端 `/api/sessions/delete`) | | `cleanupEmptySession()` | **页面切走时调用**:若当前会话无消息且无活动,从 sidebar 移除 | | `loadHistory(quiet?)` | 加载历史消息,保留乐观用户消息、异步加载缺失图片预览 | | `sendMessage(text, attachments?, targetAgentId?)` | 发送消息,乐观插入用户消息,启动 history poll 与安全超时 | | `abortRun()` | 中断当前运行,调用 `chat.abort` | | `handleChatEvent(event)` | **核心**:处理 Gateway 推送的 `started/delta/final/error/aborted` 事件 | | `toggleThinking()` | 切换 thinking 显隐 | | `refresh()` | 刷新 sessions + history | | `clearError()` | 清除错误条 | ### 2.3 必须移植的内部机制(ClawX 精髓) - **History Poll Timer**:发送后若 `streamingMessage` 为空且长时间无事件,定时轮询 `chat.history` 以展示中间 tool-call 进度。 - **Safety Timeout(90s)**:`_lastChatEventAt` 跟踪最后收到事件的时间,超 90 秒无响应自动报错并终止 `sending`。 - **Error Recovery Timer(15s)**:收到 `error` 事件后,给 Gateway 内部重试留 15 秒宽限期,期间保持 `sending = true`。 - **Chat Event Deduplication**:通过 `runId|sessionKey|seq|eventState` 去重,防止重复渲染。 - **Image Cache & Preview Loader**: - `localStorage` 缓存图片附件(`clawx:image-cache`)。 - `loadMissingPreviews()` 异步从 `/api/files/thumbnails` 加载图片缩略图。 - **Tool Result Image Enrichment**:将 `tool_result` 中的图片/文件收集到 `pendingToolImages`,最终挂载到下一个 `assistant` 消息的 `_attachedFiles`。 > **后端依赖**: > Store 依赖后端提供与 ClawX 等价的 RPC / HTTP 接口: > - `chat.history` / `chat.send` / `chat.abort` > - `sessions.list` > - `/api/sessions/delete` > - `/api/files/thumbnails` > - `/api/files/stage-paths` / `/api/files/stage-buffer` > - `/api/sessions/transcript`(子 Agent 对话加载) > - Gateway WebSocket 事件流(`started/delta/final/error/aborted`) --- ## 三、组件拆分(UI 层) 所有新组件放置在 `zn-ai/src/pages/home/components/chat/`。 ### 3.1 `ChatMessage.vue`(替代旧 `ChatRoleAI.vue` + `ChatRoleMe.vue`) **职责**:统一渲染单条消息(用户 / AI / 流式)。 **Props**: ```ts interface Props { message: RawMessage; showThinking?: boolean; suppressToolCards?: boolean; isStreaming?: boolean; streamingTools?: ToolStatus[]; } ``` **内部逻辑**(参考 ClawX `ChatMessage.tsx`): - `extractText(message)` 提取主文本,`md.render()` 渲染 markdown。 - `extractThinking(message)` 在 `showThinking=true` 时显示可折叠 Thinking 块。 - `extractToolUse(message)` 渲染 Tool 调用卡片(可展开查看参数)。 - `extractImages(message)` 渲染图片(用户消息上方缩略图,AI 消息下方大图)。 - `_attachedFiles` 渲染附件:图片预览 / 文件卡片(可点击打开文件夹)。 - **Hover Bar**:AI 消息底部显示时间戳 + 复制按钮;用户消息底部显示时间戳。 - **Streaming Cursor**:`isStreaming=true` 且为 AI 消息时,末尾显示闪烁光标。 **样式约定(zn-ai 风格)**: - 用户气泡:`bg-[#2B7FFF] text-white rounded-2xl px-4 py-3`(或保持 zn-ai 现有 `bg-[#f7f9fc] text-gray-700`,由设计者最终拍板,计划默认采用 `#2B7FFF` 高亮以统一品牌色)。 - AI 气泡:`bg-white border border-[#E5E8EE] text-gray-800 rounded-2xl px-4 py-3`。 - 头像布局:AI 左、用户右,与 zn-ai 现有完全一致。 ### 3.2 `ChatInput.vue`(由 `ChatInputArea.vue` 升级) **Props / Events**: ```ts interface Props { modelValue: string; sending: boolean; disabled?: boolean; isEmpty?: boolean; } // Events: send(text, attachments?, targetAgentId?), stop, update:modelValue ``` **新增能力(完整对齐 ClawX `ChatInput.tsx`)**: - **发送/停止切换**:`sending=true` 时按钮变为停止图标,触发 `stop`。 - **附件系统**: - 点击 `Paperclip` 图标选择文件,调用 `/api/files/stage-paths`。 - 粘贴文件(`Ctrl+V`)调用 `/api/files/stage-buffer`。 - 拖拽文件到输入框上传。 - 附件预览条(图片缩略图 + 文件卡片 + staging 旋转遮罩 + 错误状态 + 删除按钮)。 - **Agent @提及**:`@` 按钮弹出 Agent 选择器(依赖 `useAgentsStore`),选中后在输入框上方显示 chip。 - **自动增高**:textarea 随内容自动增高,最大高度 200px。 - **Enter 发送 / Shift+Enter 换行**。 ### 3.3 `ChatEmpty.vue`(WelcomeScreen 等价物) **职责**:当 `messages.length === 0 && !sending` 时展示。 **内容**: - 保留 zn-ai 现有大标题:“你好,我今天能帮你什么?” - 保留快捷操作按钮(如“智能问数”、“写代码”、“查数据”等),样式沿用 zn-ai 的圆角小按钮。 - 保留 `TaskCenter` 插槽区域(若 zn-ai 引导页需要)。 ### 3.4 `ChatErrorBar.vue` **职责**:底部固定错误提示条。 **样式**:`bg-red-50 border-t border-red-200`,左侧 `AlertCircle` 图标,右侧“Dismiss”按钮,文字 `text-red-600`。 ### 3.5 `ChatTypingIndicator.vue` **职责**:`sending && !pendingFinal && !hasAnyStreamContent` 时显示。 **样式**:左侧 AI 头像,右侧圆角气泡内三个跳动圆点,颜色使用 zn-ai 的 `#BEDBFF` / `#2B7FFF` 渐变或纯灰色。 ### 3.6 `ChatActivityIndicator.vue`(ClawX 遗漏项) **职责**:`sending && pendingFinal && !shouldRenderStreaming` 时显示。 **样式**:左侧 AI 头像,右侧气泡内显示 “Processing tool results…” + `Loader2` 旋转图标。 ### 3.7 `AttachmentPreview.vue` **职责**:输入框上方附件预览条中的单卡片。 **Props**:`attachment: FileAttachment`、`onRemove: () => void`。 **状态渲染**: - `staging`:半透明遮罩 + 旋转 loading。 - `error`:红色遮罩 + Error 文字。 - `ready`:图片显示 64×64 缩略图,文件显示图标 + 文件名 + 大小。 ### 3.8 `ExecutionGraphCard.vue` **职责**:在用户消息后展示当前 Agent 运行的任务步骤可视化。 **Props**: ```ts interface Props { agentLabel: string; sessionLabel: string; steps: TaskStep[]; active: boolean; onJumpToTrigger?: () => void; onJumpToReply?: () => void; } ``` > 该组件直接复刻 ClawX 的 `ExecutionGraphCard.tsx` 逻辑,但样式改用 zn-ai 的圆角卡片、蓝色高亮、灰色边框风格。 --- ## 四、`ChatBox.vue` 瘦身改造 改造后 `ChatBox.vue` **仅负责 Store 订阅 + 组件拼装 + 事件透传**,代码量目标 150 行以内。 ```html ``` ```ts ``` --- ## 五、页面级生命周期与事件整合 ### 5.1 `home/index.vue` ```ts import { onMounted, onBeforeUnmount } from 'vue'; import { useChatStore } from '@store/chat'; const chatStore = useChatStore(); onMounted(() => { chatStore.loadSessions(); }); onBeforeUnmount(() => { chatStore.cleanupEmptySession(); }); // 选择历史会话 const handleSelectChat = (conversationId: string) => { guide.value = false; chatStore.switchSession(conversationId); }; // 新建对话 const handleNewChat = () => { guide.value = true; chatStore.newSession(); }; ``` ### 5.2 Gateway 事件监听 ClawX 通过 Gateway WebSocket 接收 `chat.event`。zn-ai 需要在前端初始化 Gateway 连接(`useGatewayStore` 或等价实现),并将事件路由到 `chatStore.handleChatEvent(event)`。 ```ts // 在 Gateway 连接初始化后 gateway.onChatEvent = (event: Record) => { chatStore.handleChatEvent(event); }; ``` --- ## 六、`ChatHistory.vue` 联动改造 当前 `ChatHistory.vue` 自己维护 `groups` 和 `selectedConversationId`。改造后: - **数据来源**:从 `chatStore.sessions` + `chatStore.sessionLabels` + `chatStore.sessionLastActivity` 读取。 - **选择事件**:`@select-chat` 触发 `chatStore.switchSession(sessionKey)`。 - **新建事件**:`@new-chat` 触发 `chatStore.newSession()`。 - **删除/重命名**:调用 Store Action 或直接走 API,完成后触发 `chatStore.loadSessions()`。 --- ## 七、样式与兼容性保持 | 元素 | zn-ai 现有风格 | 迁移后保持 | |------|---------------|-----------| | 颜色主题 | `#2B7FFF` 蓝色高亮 | 延续 | | 边框色 | `#E5E8EE` | 延续 | | 头像布局 | AI 左、用户右 | 延续 | | 用户气泡 | `bg-[#f7f9fc]` 或自定义 | 延续 zn-ai 浅灰底,若品牌要求可改为 `#2B7FFF` | | AI 气泡 | 白色底 + 浅灰边框 | 延续 | | 输入框 | 圆角、阴影、白色背景 | 延续,增加附件预览条和 Agent chip | | 欢迎页 | 大标题 + TaskCenter | 延续 | | Loading | `ChatLoading.vue` 旋转点 | `ChatTypingIndicator` 替换为跳动圆点风格,或统一为 zn-ai 旋转点 | --- ## 八、涉及文件清单 | 新建/修改 | 文件路径 | 说明 | |-----------|----------|------| | **重构** | `zn-ai/src/pages/home/model/ChatModel.ts` | 从 class 改为 `RawMessage` / `ContentBlock` / `ToolStatus` / `ChatSession` 接口 | | **新建** | `zn-ai/src/store/chat.ts` | Pinia Chat Store,完整复刻 ClawX 状态与机制 | | **新建** | `zn-ai/src/pages/home/components/chat/ChatMessage.vue` | 统一消息渲染(markdown、thinking、tool、image、attachment、hover bar) | | **新建** | `zn-ai/src/pages/home/components/chat/ChatInput.vue` | 输入框 + 附件 + Agent @提及 + 发送/停止 | | **新建** | `zn-ai/src/pages/home/components/chat/ChatEmpty.vue` | 空会话欢迎页 | | **新建** | `zn-ai/src/pages/home/components/chat/ChatErrorBar.vue` | 底部错误条 | | **新建** | `zn-ai/src/pages/home/components/chat/ChatTypingIndicator.vue` | 发送中跳动圆点 | | **新建** | `zn-ai/src/pages/home/components/chat/ChatActivityIndicator.vue` | tool processing 状态指示器 | | **新建** | `zn-ai/src/pages/home/components/chat/AttachmentPreview.vue` | 附件预览卡片 | | **新建** | `zn-ai/src/pages/home/components/chat/ExecutionGraphCard.vue` | 任务执行步骤可视化 | | **改造** | `zn-ai/src/pages/home/components/ChatRoleAI.vue` | **废弃或大幅简化**:由 `ChatMessage.vue` 接管 | | **改造** | `zn-ai/src/pages/home/components/ChatRoleMe.vue` | **废弃或大幅简化**:由 `ChatMessage.vue` 接管 | | **改造** | `zn-ai/src/pages/home/components/ChatInputArea.vue` | **废弃**:由 `ChatInput.vue` 替换 | | **重构** | `zn-ai/src/pages/home/ChatBox.vue` | 瘦身至 150 行以内,仅负责拼装 | | **调整** | `zn-ai/src/pages/home/index.vue` | Store 生命周期、事件透传 | | **调整** | `zn-ai/src/pages/home/ChatHistory.vue` | 数据源改为 `chatStore.sessions`,联动 `switchSession` / `newSession` | --- ## 九、验收标准 - [ ] `ChatBox.vue` 代码量降至 150 行以内,不包含任何协议细节或定时器。 - [ ] 新建 `chat.ts` Pinia Store 能正常初始化、加载会话、加载历史、发送消息、接收流式事件。 - [ ] 消息模型完全兼容 `RawMessage`(`content` 支持 `string | ContentBlock[]`)。 - [ ] 用户发送消息后,输入框清空,列表底部出现 `ChatTypingIndicator`;收到首包内容后切换为 `ChatMessage` 流式渲染,末尾带闪烁光标。 - [ ] Tool-use 期间正确显示 `ChatActivityIndicator` 和 `streamingTools` 状态条。 - [ ] 收到 `final` 事件后,流式消息固化到 `messages`,状态恢复正常;`tool_result` 中的图片自动附加到最终 AI 消息。 - [ ] 发生超时或错误时,底部出现 `ChatErrorBar`,可点击清除;90 秒 safety timeout 能自动终止卡死状态。 - [ ] 切换历史会话时,通过 `switchSession` 加载历史消息并正确渲染;新建对话时回到 `ChatEmpty`。 - [ ] 离开首页时调用 `cleanupEmptySession`,无 ghost session 残留。 - [ ] `ExecutionGraphCard` 在用户消息后正确渲染任务步骤(含子 Agent 分支)。 - [ ] UI 颜色、头像布局、输入框样式与 zn-ai 现有设计保持一致。