feat: 实现会话配置与多轮上下文
All checks were successful
Backend CI / ci (pull_request) Successful in 1m0s
Frontend CI / ci (pull_request) Successful in 1m0s

- 新增 ConfigPanel 组件:TTS 开关、detail level、语言选择
- 新增 storage.ts:配置持久化到 localStorage
- useVisionSession:连接后自动发送 config,维护最近 10 轮对话历史
- App:齿轮按钮展开配置面板,HD 角标,TTS 播放联动 ttsEnabled

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-13 14:19:47 +08:00
parent 8033807f7c
commit 7e3762b589
5 changed files with 267 additions and 18 deletions

View File

@@ -11,17 +11,24 @@ import { encodeAudioToBase64, dataUrlToBase64 } from "../lib/audio";
import { getErrorMessage } from "../lib/errors";
import { TTSPlayer } from "../lib/ttsPlayer";
import { showToast } from "../lib/toast";
import { loadConfig, saveConfig } from "../lib/storage";
import { useCamera } from "../components/CameraManager";
import { useMicrophone } from "../components/MicManager";
import { useVAD } from "../components/EdgeProcessor";
import { useWebSocketManager } from "../components/WebSocketManager";
import type { ChatMessage, ServerMessage, LLMDoneMessage } from "../types";
import type { ChatMessage, SessionConfig, ServerMessage, LLMDoneMessage } from "../types";
const MAX_HISTORY_ROUNDS = 10;
export function useVisionSession() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [currentReply, setCurrentReply] = useState<string>("");
const [isProcessing, setIsProcessing] = useState(false);
const [isAudioPlaying, setIsAudioPlaying] = useState(false);
const [config, setConfig] = useState<SessionConfig>(loadConfig);
// 对话历史role + content用于多轮上下文
const historyRef = useRef<Array<{ role: string; content: string }>>([]);
// TTS 播放器
const ttsPlayerRef = useRef<TTSPlayer | null>(null);
@@ -44,6 +51,40 @@ export function useVisionSession() {
isProcessingRef.current = isProcessing;
}, [isProcessing]);
// WebSocket 连接成功后发送 config
useEffect(() => {
if (status === "connected") {
send({
type: "config",
payload: {
tts_enabled: config.ttsEnabled,
detail_level: config.detailLevel,
language: config.language,
},
});
}
}, [status]); // eslint-disable-line react-hooks/exhaustive-deps -- 仅在连接状态变化时发送
/** 更新会话配置 */
const updateConfig = useCallback((partial: Partial<SessionConfig>) => {
setConfig((prev) => {
const next = { ...prev, ...partial };
saveConfig(next);
// 如果已连接,立即发送更新
if (status === "connected") {
send({
type: "config",
payload: {
tts_enabled: next.ttsEnabled,
detail_level: next.detailLevel,
language: next.language,
},
});
}
return next;
});
}, [status, send]);
// VAD语音结束时自动发送 query
const {
isSpeaking,
@@ -111,6 +152,13 @@ export function useVisionSession() {
case "llm_done": {
const done = msg as LLMDoneMessage;
// 记录到对话历史
historyRef.current.push({ role: "assistant", content: done.full_text });
// 裁剪历史到最近 N 轮
if (historyRef.current.length > MAX_HISTORY_ROUNDS * 2) {
historyRef.current = historyRef.current.slice(-MAX_HISTORY_ROUNDS * 2);
}
setMessages((prev) => [
...prev,
{
@@ -145,14 +193,6 @@ export function useVisionSession() {
return unsub;
}, [getTTSPlayer]);
// 连接断开时显示提示
useEffect(() => {
if (status === "disconnected") {
// 只在非主动断开时提示(通过检查是否有活跃会话判断)
// 这里简单处理,由 App 层根据状态显示
}
}, [status]);
/** 启动会话 */
const startSession = useCallback(async () => {
// 1. 获取摄像头和麦克风
@@ -182,6 +222,7 @@ export function useVisionSession() {
setMessages([]);
setCurrentReply("");
setIsProcessing(false);
historyRef.current = [];
}, [stopVAD, stopMic, stopCamera, disconnect]);
/** 打断当前回复 */
@@ -192,9 +233,11 @@ export function useVisionSession() {
setIsAudioPlaying(false);
// 将未完成的流式内容保存为最终消息
if (currentReply) {
const interrupted = currentReply + "(已打断)";
historyRef.current.push({ role: "assistant", content: interrupted });
setMessages((prev) => [
...prev,
{ role: "assistant", content: currentReply + "(已打断)", timestamp: Date.now() },
{ role: "assistant", content: interrupted, timestamp: Date.now() },
]);
}
setCurrentReply("");
@@ -212,6 +255,8 @@ export function useVisionSession() {
connectionStatus: status,
videoRef,
stream,
config,
updateConfig,
startSession,
stopSession,
interrupt,