520 lines
20 KiB
Markdown
520 lines
20 KiB
Markdown
# 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<string, string>;
|
||
sessionLastActivity: Record<string, number>;
|
||
|
||
// 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
|
||
<template>
|
||
<div class="flex flex-col h-full py-6 px-6 overflow-hidden">
|
||
<!-- 空状态 -->
|
||
<ChatEmpty v-if="isEmpty" />
|
||
|
||
<!-- 消息列表 -->
|
||
<div v-else ref="listRef" class="flex-1 overflow-y-auto py-6 space-y-6">
|
||
<!-- 历史消息 -->
|
||
<div
|
||
v-for="(msg, idx) in chatStore.messages"
|
||
:key="msg.id || `msg-${idx}`"
|
||
class="space-y-3"
|
||
>
|
||
<!-- 消息行 -->
|
||
<div class="flex items-start gap-3"
|
||
:class="msg.role === 'user' ? 'justify-end' : 'justify-start'">
|
||
<ChatAvatar v-if="msg.role !== 'user'" :src="aiAvatar" />
|
||
<ChatMessage
|
||
:message="msg"
|
||
:show-thinking="chatStore.showThinking"
|
||
:suppress-tool-cards="shouldSuppressToolCards(idx)"
|
||
/>
|
||
<ChatAvatar v-if="msg.role === 'user'" :src="userAvatar" />
|
||
</div>
|
||
|
||
<!-- 执行图卡片(若该用户消息触发了 agent run) -->
|
||
<ExecutionGraphCard
|
||
v-if="runCardAtIndex(idx)"
|
||
v-bind="runCardAtIndex(idx)!"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 流式消息 -->
|
||
<div v-if="shouldRenderStreaming" class="flex items-start gap-3 justify-start">
|
||
<ChatAvatar :src="aiAvatar" />
|
||
<ChatMessage
|
||
:message="streamMsgAsRaw"
|
||
:show-thinking="chatStore.showThinking"
|
||
:is-streaming="true"
|
||
:streaming-tools="chatStore.streamingTools"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Indicator -->
|
||
<ChatActivityIndicator v-if="showActivityIndicator" />
|
||
<ChatTypingIndicator v-if="showTypingIndicator" />
|
||
</div>
|
||
|
||
<!-- 错误条 -->
|
||
<ChatErrorBar :error="chatStore.error" @dismiss="chatStore.clearError()" />
|
||
|
||
<!-- 输入区 -->
|
||
<div class="flex flex-col gap-3 mt-4">
|
||
<ChatInput
|
||
v-model="inputMessage"
|
||
:sending="chatStore.sending"
|
||
:disabled="!isGatewayRunning"
|
||
:is-empty="isEmpty"
|
||
@send="onSend"
|
||
@stop="chatStore.abortRun()"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
```ts
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||
import { useChatStore } from '@store/chat';
|
||
import { useGatewayStore } from '@store/gateway'; // 若 zn-ai 已有等价 store
|
||
// ... 引入各子组件
|
||
|
||
const chatStore = useChatStore();
|
||
const inputMessage = ref('');
|
||
|
||
const isEmpty = computed(() =>
|
||
chatStore.messages.length === 0 && !chatStore.sending
|
||
);
|
||
|
||
// Indicator 显示逻辑(与 ClawX 完全一致)
|
||
const hasAnyStreamContent = computed(() => {
|
||
const sm = chatStore.streamingMessage;
|
||
// ... 根据 streamText/streamThinking/streamTools/streamImages 判断
|
||
return false; // 省略具体实现
|
||
});
|
||
|
||
const shouldRenderStreaming = computed(() =>
|
||
chatStore.sending && hasAnyStreamContent.value
|
||
);
|
||
|
||
const showActivityIndicator = computed(() =>
|
||
chatStore.sending && chatStore.pendingFinal && !shouldRenderStreaming.value
|
||
);
|
||
|
||
const showTypingIndicator = computed(() =>
|
||
chatStore.sending && !chatStore.pendingFinal && !hasAnyStreamContent.value
|
||
);
|
||
|
||
const onSend = (text: string, attachments?: any[], targetAgentId?: string | null) => {
|
||
if (!text.trim() && (!attachments || attachments.length === 0)) return;
|
||
chatStore.sendMessage(text, attachments, targetAgentId);
|
||
inputMessage.value = '';
|
||
};
|
||
|
||
// 执行图卡片计算(与 ClawX userRunCards 逻辑对齐)
|
||
const runCardAtIndex = (idx: number) => {
|
||
// TODO: 在 ChatBox 内复刻 ClawX 的 deriveTaskSteps + subagent transcripts 逻辑
|
||
return null;
|
||
};
|
||
|
||
const shouldSuppressToolCards = (idx: number) => {
|
||
// TODO: 若该消息处于 ExecutionGraphCard 范围内,则 suppress tool cards
|
||
return false;
|
||
};
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 五、页面级生命周期与事件整合
|
||
|
||
### 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<string, unknown>) => {
|
||
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 现有设计保持一致。
|