Files
CamTalk/docs/13-结束视频保留对话设计方案.md
cfy666 ae9a27c300 feat: 结束视频后保留对话,支持继续文字聊天
- useVisionSession 新增 stopVideo 回调(停媒体流,保持连接和消息)
- App.tsx 控制区从二态改为三态(initial/video/textOnly)
- 文字对话态显示「重新开始视频」和「结束会话」按钮
- 新增 CSS 样式(video-ended-hint、btn--outline)
- 新增 i18n key(stopVideo/endSession/resumeVideo/video.ended)
- 新增设计方案文档
2026-06-20 13:48:26 +08:00

10 KiB
Raw Blame History

结束视频后保留对话并支持继续文字聊天

创建日期2026-06-20 状态:草案

1. 背景与目标

1.1 现状问题

当前点击"结束对话"按钮会执行完整的 teardown 流程:

  1. 停止 VAD、麦克风、摄像头
  2. 断开 WebSocket 连接
  3. 清空所有聊天消息setMessages([])
  4. 清空对话历史historyRef.current = []
  5. 重置统计数据
  6. UI 切回初始界面(显示"开始视频通话"按钮)

问题:用户想结束视频通话后,保留聊天记录并继续通过文字输入对话,但当前实现会丢失所有对话内容。

1.2 目标

目标 说明
结束视频后保留对话 点击"结束视频"后,聊天记录保持不变
支持继续文字对话 视频结束后,用户可通过文字输入继续与 AI 对话
可恢复视频 视频结束后,用户可随时重新开启视频
完全结束可选 提供"结束会话"选项,彻底断开并清空

2. 状态设计

2.1 三态模型

引入三个会话状态,替代当前的二态(初始/通话)模型:

┌──────────┐    startSession()    ┌──────────┐
│  initial  │ ──────────────────→ │  video   │
│  初始态    │                     │  视频通话  │
└──────────┘                      └──────────┘
      ↑                                │
      │                          stopVideo()
      │                                │
      │                                ▼
      │                         ┌──────────┐
      │ stopSession()           │ textOnly │
      └──────────────────────── │ 文字对话  │
                                └──────────┘
                                      │
                                startSession()
                                      │
                                      ▼
                                ┌──────────┐
                                │  video   │
                                │  视频通话  │
                                └──────────┘

2.2 各状态属性

状态 WebSocket 摄像头 麦克风 VAD 消息 文字输入
initial 断开 关闭 关闭 停止 可用(自动连接)
video 连接 开启 开启 运行 可用
textOnly 连接 关闭 关闭 停止 保留 可用

2.3 派生状态

当前代码中 isConnected 是从 connectionStatus === "connected" 派生的布尔值。为支持三态,新增派生变量:

// 是否在会话中video 或 textOnly
const hasSession = isConnected || (connectionStatus === "disconnected" && messages.length > 0);

注意textOnly 状态下 WebSocket 保持连接(isConnected === true),所以 hasSession 实际上主要靠 isConnected 判断。只有在 textOnly 状态下 WebSocket 异常断开时,messages.length > 0 才作为兜底。

3. 详细设计

3.1 useVisionSession.ts 改动

3.1.1 新增 stopVideo 回调

只停止媒体流,保持 WebSocket 连接和消息:

/** 结束视频,保留聊天和连接 */
const stopVideo = useCallback(async () => {
  // 1. 停止观察模式
  stopObserving();
  setMode("dialogue");

  // 2. 停止媒体流
  await stopVAD();
  stopMic();
  stopCamera();

  // 3. 停止 TTS 播放
  ttsPlayerRef.current?.stop();
  setIsAudioPlaying(false);

  // 4. 重置处理状态(但保留消息和历史)
  setCurrentReply("");
  setIsProcessing(false);
  setIsCameraOn(false);
  setIsMicOn(false);

  // 注意:以下不执行
  // - disconnect()         → 保持 WebSocket 连接
  // - setMessages([])      → 保留聊天记录
  // - historyRef.current=[] → 保留对话历史
  // - setStats(...)         → 保留统计数据
}, [stopObserving, stopVAD, stopMic, stopCamera]);

3.1.2 stopSession 保持不变

stopSession 仍然执行完全 teardown断开 + 清空),作为"结束会话"使用。

3.1.3 return 新增导出

return {
  // ...existing...
  stopVideo,   // 新增
  // ...existing...
};

3.2 App.tsx 改动

3.2.1 解构新增

const {
  // ...existing...
  stopVideo,  // 新增
  // ...existing...
} = useVisionSession(...)

3.2.2 视频下方控制区改为三态

当前代码(二态):

{!isConnected ? (
  /* 初始态 */
) : (
  /* 通话态 */
)}

改为三态:

{!isConnected ? (
  /* 初始态:开始按钮 + 设备选择 + 模式切换(不变) */
) : isCameraOn ? (
  /* 视频通话态:摄像头/麦克风/识别/打断 + "结束视频" 按钮 + 模式切换 */
) : (
  /* 文字对话态:
     - "📹 视频已结束" 提示
     - "📹 重新开始视频" 按钮
     - "结束会话" 按钮
  */
)}

3.2.3 按钮变化

视频通话态(原"结束对话"改为"结束视频"

<button className="btn btn--danger" onClick={stopVideo}>
  {tr("controls.stopVideo")}
</button>

文字对话态(新增):

<div className="video-controls__text-only">
  <div className="video-ended-hint">
    <span>📹 {tr("video.ended")}</span>
    <span className="video-ended-hint__sub">{tr("video.ended.hint")}</span>
  </div>
  <div className="video-controls__toolbar">
    <button className="btn btn--primary" onClick={startSession}>
      {tr("controls.resumeVideo")}
    </button>
    <button className="btn btn--danger btn--outline" onClick={stopSession}>
      {tr("controls.endSession")}
    </button>
  </div>
</div>

3.2.4 视频预览区

当前已有逻辑:{!isConnected && !stream && <placeholder>}。关闭摄像头后 stream 为 null自动显示占位符。无需额外改动

但在 textOnly 状态下 isConnected 为 true所以需要额外判断

{(!isConnected || !isCameraOn) && !stream && (
  <div className="video-placeholder">
    <span className="video-placeholder__icon">📷</span>
    <span className="video-placeholder__text">{tr("video.cameraOff")}</span>
    <span className="video-placeholder__hint">{tr("video.cameraOff.hint")}</span>
  </div>
)}

3.2.5 状态栏

isConnected && stats.queryCount > 0 改为在 textOnly 状态下也显示:

{isConnected && stats.queryCount > 0 && (
  <span className="chat-panel-header__stats">
    {stats.queryCount} {tr("statusbar.recognitions")}
    {stats.totalTokens > 0 && ` · ${stats.totalTokens.toLocaleString()} ${tr("statusbar.tokens")}`}
    {` · ${formatTime(elapsed)}`}
  </span>
)}

isConnected 在 textOnly 状态下为 trueWebSocket 未断开),所以无需改动

3.3 i18n 新增

Key zh-CN en-US ja-JP
controls.stopVideo 结束视频 End Video ビデオ終了
controls.endSession 结束会话 End Session セッション終了
controls.resumeVideo 📹 重新开始视频 📹 Resume Video 📹 ビデオ再開
video.ended 视频已结束 Video Ended ビデオ終了
video.ended.hint 您可以继续在下方输入文字对话 You can continue chatting below 下にテキストを入力して会話を続けることができます

3.4 CSS 样式

新增 .video-controls__text-only.video-ended-hint 样式,复用现有 .btn.video-controls__toolbar 样式。

4. 边界情况处理

4.1 textOnly 状态下 WebSocket 异常断开

sendTextMessage 已有自动重连逻辑:检测到未连接时,先加入 pendingMessagesRef,再调用 connect()。重连成功后自动 flush 待发队列。无需改动

4.2 textOnly 状态下无摄像头画面

sendTextMessagecaptureFrame() 在无摄像头时返回 nulldataUrlToBase64(null) 返回空字符串。服务端 PipelineInput.ImageData 为空时History 节点跳过图像构建多模态消息。无需改动

4.3 textOnly 状态下刷新页面

消息通过 localStorage 持久化(camtalk:session:<uuid>),刷新后从 localStorage 恢复。但 WebSocket 断开,isConnected 为 falseUI 显示初始态。用户可点击"开始视频通话"或直接输入文字。无需改动

4.4 textOnly 状态下切换会话

selectSession 会先 persist 当前会话消息,然后加载目标会话消息。切换后 isConnected 取决于目标会话的 WebSocket 状态。无需改动

4.5 textOnly 状态下 TTS 播放

stopVideo 已调用 ttsPlayerRef.current?.stop() 停止播放。后续文字对话中如果 AI 回复触发 TTSTTS 仍可正常播放WebSocket 连接保持)。无需改动

5. 不改动的部分

模块 原因
useVisionSession.stopSession 保持完全 teardown 行为不变
sendTextMessage 已支持自动连接 + 无摄像头发送
ChatPanel 组件 文字输入框始终显示,无需改动
WebSocket Handler 服务端无需感知客户端的 video/textOnly 状态
Session Manager 会话管理不受影响
useSessionList 会话列表管理不受影响

6. 验证清单

场景 预期结果
视频通话中点击"结束视频" 摄像头/麦克风关闭,消息保留,可继续打字
文字对话态输入文字发送 AI 正常回复无图片TTS 正常播放
文字对话态点击"重新开始视频" 摄像头/麦克风重新开启,恢复正常视频通话
文字对话态点击"结束会话" 清空消息,断开连接,回到初始态
文字对话态刷新页面 消息从 localStorage 恢复,可继续打字
文字对话态切换到其他会话 当前会话消息保存,加载目标会话消息
文字对话态 WebSocket 异常断开 自动重连,重连后可继续发消息