feat: 新增开发规划
This commit is contained in:
248
ChatPageMigrationPlan.md
Normal file
248
ChatPageMigrationPlan.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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 被正确关闭,无后台消息泄漏。
|
||||
Reference in New Issue
Block a user