Files
zn-ai/ChatPageMigrationPlan.md
2026-04-14 07:36:40 +08:00

249 lines
10 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.

# Chat 对话功能迁移重构计划
## 参考来源
- **源文件**: `ClawX/src/pages/Chat/index.tsx` 及其子组件
- **目标文件**: `zn-ai/src/pages/home/index.vue``zn-ai/src/pages/home/ChatBox.vue`
## 源实现思路分析
`ClawX/src/pages/Chat/index.tsx` 的核心架构:
1. **状态层集中化**所有聊天状态messages、sending、loading、error、streamingMessage 等)托管在 `useChatStore`Zustand页面组件只负责订阅与渲染。
2. **组件原子化**
- `ChatMessage`单条消息渲染markdown、thinking、tool_use、附件图片
- `ChatInput`:输入框 + 发送/停止按钮。
- `ChatToolbar`会话选择器、thinking 显隐切换、刷新按钮。
- `ExecutionGraphCard`:用户消息后的任务执行可视化卡片。
3. **流式消息处理**:通过 `streamingMessage``streamingTools` 实现未落库前的增量渲染;`pendingFinal` 标识等待最终响应的状态。
4. **生命周期管理**
- 切走页面时调用 `cleanupEmptySession()` 清理空会话。
- 加载历史消息时保留乐观用户消息,避免闪烁。
- 轮询 + 安全超时机制防止消息卡死。
5. **错误与加载态**
- 底部 `error` 条显示全局错误并可一键清除。
- `minLoading` 在 history 加载时显示透明遮罩 + LoadingSpinner。
- 发送中显示 `TypingIndicator` / `ActivityIndicator`
6. **WelcomeScreen**:当 `messages.length === 0 && !sending` 时展示欢迎页 + 快捷操作按钮。
---
## 迁移目标
`ChatBox.vue` 中沉淀的 880+ 行“上帝组件”逻辑解耦,引入 **Pinia Store + 原子组件** 的 ClawX 式架构,同时保留 zn-ai 现有的视觉风格(蓝色主题、头像布局、输入框样式)。
---
## 实现步骤
### 1. 提取 Pinia Chat Store状态层迁移
新建 `zn-ai/src/store/chat.ts`,将 `ChatBox.vue` 中的以下逻辑迁入 Store
| ChatBox.vue 现有逻辑 | Store 对应设计 |
|---|---|
| `chatMsgList` | `state.messages` |
| `isSendingMessage` | `state.sending` |
| `isSessionActive` | `state.pendingFinal` / `state.loading` |
| WebSocket 管理(`initWebSocket``sendWebSocketMessage` | Store Actions`initConnection``sendMessage``stopRun` |
| `pendingMap` / `pendingTimeouts` 超时回退 | Store 内部 Map + 定时器(不暴露给 UI |
| `handleWebSocketMessage` | Store Action`handleStreamEvent` |
| `loadConversationMessages` | Store Action`loadHistory(sessionId)` |
| `createConversationRequest` | Store Action`createSession()` |
| `resetConversation` / `cleanup` | Store Action`resetSession()` |
**Store 核心 State 设计**
```ts
interface ChatState {
messages: ChatMessage[]; // 当前会话消息列表
sending: boolean; // 是否正在发送/等待回复
loading: boolean; // 是否正在加载历史
error: string | null; // 全局错误提示
streamingMessage: Partial<ChatMessage> | null; // 流式增量消息
currentSessionId: string; // 当前会话 ID
}
```
> **说明**zn-ai 当前使用 WebSocket 直联后端ClawX 使用 Gateway RPC本次迁移**只搬架构、不搬协议**Store 内部仍继续使用现有的 `WebSocketManager`。
### 2. 组件拆分UI 层迁移)
`ChatBox.vue` 按职责拆成以下子组件(放置于 `zn-ai/src/pages/home/components/chat/`
#### 2.1 `ChatMessage.vue`
职责:渲染单条消息。
- Props`msg: ChatMessage``isStreaming?: boolean`
- 复用现有 `ChatRoleMe.vue``ChatRoleAI.vue``ChatAvatar.vue``ChatNameTime.vue``ChatAttach.vue``ChatAIMark.vue``ChatOperation.vue` 的能力。
- **新增**:支持 `streamingMessage` 的渲染(内容可能未完成)。
#### 2.2 `ChatInput.vue`(由现有 `ChatInputArea.vue` 升级)
职责:输入框 + 发送/停止按钮。
- Props`modelValue: string``sending: boolean``disabled?: boolean`
- Events`send``stop``update:modelValue``attach`
- **新增**:当 `sending = true` 时按钮显示停止图标并触发 `stop`
#### 2.3 `ChatEmpty.vue`WelcomeScreen 等价物)
职责:空会话欢迎页。
- 迁移现有 `ChatBox.vue` 中的引导页 DOM大标题“你好我今天能帮你什么
- 可保留现有的 `TaskCenter` 插槽或快捷操作按钮区域。
#### 2.4 `ChatErrorBar.vue`
职责:底部错误条。
- Props`error: string | null`
- Events`dismiss`
- 样式参考 ClawX 的红色背景条:`bg-red-50` + 左侧 `AlertCircle` 图标 + 右侧“Dismiss”按钮。
#### 2.5 `ChatLoadingOverlay.vue`(可选)
职责history 加载时透明遮罩 + LoadingSpinner。
- 若 zn-ai 现有 `ChatLoading.vue` 已满足,可直接复用。
### 3. ChatBox.vue 瘦身改造
改造后 `ChatBox.vue` 只承担:**Store 订阅 + 组件拼装 + 少量 prop 透传**。
```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 in chatStore.messages"
:key="msg.messageId"
class="flex items-start gap-3"
:class="msg.messageRole === MessageRole.ME ? 'justify-end' : 'justify-start'"
>
<ChatAvatar v-if="msg.messageRole === MessageRole.AI" :src="aiAvatar" />
<ChatMessage :msg="msg" />
<ChatAvatar v-if="msg.messageRole === MessageRole.ME" :src="userAvatar" />
</div>
<!-- 流式消息占位(未落库前的 AI 回复) -->
<div v-if="chatStore.streamingMessage" class="flex items-start gap-3 justify-start">
<ChatAvatar :src="aiAvatar" />
<ChatMessage :msg="chatStore.streamingMessage" is-streaming />
</div>
<!-- 发送中 Indicator -->
<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"
@send="onSend"
@stop="chatStore.stopRun()"
@attach="onAttach"
/>
</div>
</div>
</template>
```
```ts
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useChatStore } from '@store/chat';
import { MessageRole } from './model/ChatModel';
// ... 引入各子组件
const chatStore = useChatStore();
const inputMessage = ref('');
const isEmpty = computed(() =>
chatStore.messages.length === 0 && !chatStore.sending && !chatStore.streamingMessage
);
const showTypingIndicator = computed(() =>
chatStore.sending && !chatStore.streamingMessage
);
const onSend = () => {
const text = inputMessage.value.trim();
if (!text) return;
chatStore.sendMessage(text);
inputMessage.value = '';
};
</script>
```
### 4. 流式消息与 Indicator 支持
在 Store 的 WebSocket `onMessage` 回调中:
1. **首次收到内容**:创建 `streamingMessage`AI 角色、初始内容)。
2. **后续收到增量**`streamingMessage.messageContent += data.content`
3. **收到 finish**:将 `streamingMessage` 推入 `messages[]`,然后清空 `streamingMessage`,结束 `sending`
4. **异常/超时**:清空 `streamingMessage` 并设置 `error`
**新增 `ChatTypingIndicator.vue`**(参考 ClawX `TypingIndicator`
- 左侧 AI 头像,右侧三个跳动的圆点,用于 `sending && !streamingMessage` 时占位。
### 5. 生命周期与页面级整合(`home/index.vue`
`home/index.vue` 中补充 Store 生命周期绑定:
```ts
import { useChatStore } from '@store/chat';
const chatStore = useChatStore();
onMounted(() => {
chatStore.initConnection();
});
onBeforeUnmount(() => {
chatStore.resetSession();
});
```
> 当用户从首页切到其他页面时,调用 `resetSession()` 关闭 WebSocket、清理定时器避免后台继续接收消息。
### 6. 历史消息加载与空会话清理
- **选择历史会话**`ChatHistory.vue``@select-chat` 事件触发 `chatStore.loadHistory(conversationId)`
- **新建对话**`@new-chat` 触发 `chatStore.createSession()`,然后清空 `messages`
- **切页清理**Store 中实现 `cleanupEmptySession()`,如果当前会话没有任何消息且不是历史来源,则自动重置 `conversationId`,避免产生幽灵会话。
### 7. 样式与兼容性保持
- **颜色主题**:继续使用 zn-ai 现有的 `#2B7FFF` 蓝色高亮、`#E5E8EE` 边框色。
- **头像布局**左右头像顺序不变AI 左、用户右)。
- **输入框样式**`ChatInput` 保留现有的圆角、阴影、发送按钮样式。
- **引导页**:保留现有的 `TaskCenter` 和欢迎文案。
---
## 涉及文件
| 新建/修改 | 文件路径 | 说明 |
|---|---|---|
| 新建 | `zn-ai/src/store/chat.ts` | Pinia Chat Store承载状态与 WebSocket 逻辑 |
| 新建 | `zn-ai/src/pages/home/components/chat/ChatMessage.vue` | 单条消息渲染(聚合现有原子组件) |
| 新建 | `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/ChatInputArea.vue` | 增加 `sending` 态与 `stop` 事件 |
| 重构 | `zn-ai/src/pages/home/ChatBox.vue` | 大幅瘦身,仅负责拼装子组件 |
| 调整 | `zn-ai/src/pages/home/index.vue` | 引入 Store 生命周期、事件透传 |
---
## 验收标准
- [ ] `ChatBox.vue` 代码量从 800+ 行降至 150 行以内,不再包含 WebSocket 细节。
- [ ] 新建 `chat.ts` Pinia Store 能正常初始化 WebSocket、发送消息、接收回复。
- [ ] 用户发送消息后,输入框清空,列表底部出现 `ChatTypingIndicator`;收到首包内容后切换为 `ChatMessage` 流式渲染。
- [ ] 收到完成标识后,流式消息固化到消息列表,状态恢复正常。
- [ ] 发生超时或错误时,底部出现 `ChatErrorBar`,可点击清除。
- [ ] 切换历史会话时,通过 Store 加载历史消息并正确渲染。
- [ ] 新建对话时,列表回到空状态(`ChatEmpty`)。
- [ ] 离开首页时 WebSocket 被正确关闭,无后台消息泄漏。