10 KiB
10 KiB
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 的核心架构:
- 状态层集中化:所有聊天状态(messages、sending、loading、error、streamingMessage 等)托管在
useChatStore(Zustand)中,页面组件只负责订阅与渲染。 - 组件原子化:
ChatMessage:单条消息渲染(markdown、thinking、tool_use、附件图片)。ChatInput:输入框 + 发送/停止按钮。ChatToolbar:会话选择器、thinking 显隐切换、刷新按钮。ExecutionGraphCard:用户消息后的任务执行可视化卡片。
- 流式消息处理:通过
streamingMessage和streamingTools实现未落库前的增量渲染;pendingFinal标识等待最终响应的状态。 - 生命周期管理:
- 切走页面时调用
cleanupEmptySession()清理空会话。 - 加载历史消息时保留乐观用户消息,避免闪烁。
- 轮询 + 安全超时机制防止消息卡死。
- 切走页面时调用
- 错误与加载态:
- 底部
error条显示全局错误并可一键清除。 minLoading在 history 加载时显示透明遮罩 + LoadingSpinner。- 发送中显示
TypingIndicator/ActivityIndicator。
- 底部
- 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 设计:
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 透传。
<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>
<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 回调中:
- 首次收到内容:创建
streamingMessage(AI 角色、初始内容)。 - 后续收到增量:
streamingMessage.messageContent += data.content。 - 收到 finish:将
streamingMessage推入messages[],然后清空streamingMessage,结束sending。 - 异常/超时:清空
streamingMessage并设置error。
新增 ChatTypingIndicator.vue(参考 ClawX TypingIndicator):
- 左侧 AI 头像,右侧三个跳动的圆点,用于
sending && !streamingMessage时占位。
5. 生命周期与页面级整合(home/index.vue)
在 home/index.vue 中补充 Store 生命周期绑定:
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.tsPinia Store 能正常初始化 WebSocket、发送消息、接收回复。 - 用户发送消息后,输入框清空,列表底部出现
ChatTypingIndicator;收到首包内容后切换为ChatMessage流式渲染。 - 收到完成标识后,流式消息固化到消息列表,状态恢复正常。
- 发生超时或错误时,底部出现
ChatErrorBar,可点击清除。 - 切换历史会话时,通过 Store 加载历史消息并正确渲染。
- 新建对话时,列表回到空状态(
ChatEmpty)。 - 离开首页时 WebSocket 被正确关闭,无后台消息泄漏。