-
+
+
+
+
+
-
+
聊天
@@ -32,7 +56,10 @@ const { onlineCount } = storeToRefs(chatStore);
-
diff --git a/web/src/main.ts b/web/src/main.ts
index 81aafaa..58d9ea9 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -1,14 +1,14 @@
-import { createPinia } from 'pinia'
-import { createApp } from 'vue'
-import App from './App.vue'
+import { createPinia } from "pinia";
+import { createApp } from "vue";
+import App from "./App.vue";
-import router from './router'
-import './style.css'
+import router from "./router";
+import "./style.css";
-const pinia = createPinia()
-const app = createApp(App)
+const pinia = createPinia();
+const app = createApp(App);
-app.use(pinia)
-app.use(router)
+app.use(pinia);
+app.use(router);
-app.mount('#app')
+app.mount("#app");
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
index 5591234..e4daaed 100644
--- a/web/src/router/index.ts
+++ b/web/src/router/index.ts
@@ -1,28 +1,28 @@
-import { createRouter, createWebHistory } from 'vue-router'
+import { createRouter, createWebHistory } from "vue-router";
-import BasicLayout from '@/layouts/BasicLayout.vue'
-import { resetDescription, setTitle } from '@/utils'
-import community from '@/views/CommunityView.vue'
+import BasicLayout from "@/layouts/BasicLayout.vue";
+import { resetDescription, setTitle } from "@/utils";
+import community from "@/views/CommunityView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
- path: '/',
+ path: "/",
component: BasicLayout,
children: [
{
- path: '',
- name: 'community',
+ path: "",
+ name: "community",
component: community,
meta: {
- title: '社区',
- },
- },
- ],
- },
- ],
-})
+ title: "对话"
+ }
+ }
+ ]
+ }
+ ]
+});
// // 权限检查函数,检查并决定是否允许访问
// const checkPermission: NavigationGuard = (to, from, next) => {
@@ -64,17 +64,17 @@ const router = createRouter({
// // 添加导航守卫
router.beforeEach((to, from, next) => {
- setTitle(to.meta.title as string)
- resetDescription()
+ setTitle(to.meta.title as string);
+ resetDescription();
// context.loadingBar?.start();
// 在每个路由导航前执行权限检查
// checkPermission(to, from, next);
- next()
-})
+ next();
+});
// router.afterEach(() => {
// context.loadingBar?.finish();
// });
-export default router
+export default router;
diff --git a/web/src/services/audio_websocket.ts b/web/src/services/audio_websocket.ts
new file mode 100644
index 0000000..ec0fa6a
--- /dev/null
+++ b/web/src/services/audio_websocket.ts
@@ -0,0 +1,30 @@
+import { useWebSocketStore } from "@/services";
+
+export const useAudioWebSocket = () => {
+ const webSocketStore = useWebSocketStore();
+
+ const sendMessage = (data: string | Uint8Array) => {
+ if (webSocketStore.connected) {
+ if (typeof data === "string") {
+ webSocketStore.send(data);
+ } else {
+ webSocketStore.websocket?.send(data);
+ }
+ }
+ };
+
+ const ensureConnection = async (): Promise
=> {
+ if (!webSocketStore.connected) {
+ webSocketStore.connect();
+ await new Promise((resolve) => {
+ const check = () => {
+ if (webSocketStore.connected) resolve();
+ else setTimeout(check, 100);
+ };
+ check();
+ });
+ }
+ };
+
+ return { sendMessage, ensureConnection };
+};
diff --git a/web/src/services/base_service.ts b/web/src/services/base_service.ts
index 963ed66..4067320 100644
--- a/web/src/services/base_service.ts
+++ b/web/src/services/base_service.ts
@@ -5,27 +5,33 @@ import { context } from "@/utils";
const BaseClientService = axios.create();
// 添加请求拦截器
-BaseClientService.interceptors.request.use((config) => {
- // 在发送请求之前做些什么
- return config;
-}, (e) => {
- // 对请求错误做些什么
- return Promise.reject(e);
-});
-
-// 添加响应拦截器
-BaseClientService.interceptors.response.use((response) => {
- // 2xx 范围内的状态码都会触发该函数。
- // 对响应数据做点什么
- return response;
-}, (e) => {
- // “50”开头统一处理
- if (e.response?.status >= 500 && e.response?.status < 600) {
- context.message?.error("服务器开小差了,请稍后再试");
+BaseClientService.interceptors.request.use(
+ (config) => {
+ // 在发送请求之前做些什么
+ return config;
+ },
+ (e) => {
+ // 对请求错误做些什么
return Promise.reject(e);
}
- return Promise.reject(e);
-});
+);
+
+// 添加响应拦截器
+BaseClientService.interceptors.response.use(
+ (response) => {
+ // 2xx 范围内的状态码都会触发该函数。
+ // 对响应数据做点什么
+ return response;
+ },
+ (e) => {
+ // “50”开头统一处理
+ if (e.response?.status >= 500 && e.response?.status < 600) {
+ context.message?.error("服务器开小差了,请稍后再试");
+ return Promise.reject(e);
+ }
+ return Promise.reject(e);
+ }
+);
/** 基础URL */
export const BaseUrl = "/v1";
diff --git a/web/src/services/chat_service.ts b/web/src/services/chat_service.ts
index d075ea7..4957670 100644
--- a/web/src/services/chat_service.ts
+++ b/web/src/services/chat_service.ts
@@ -1,91 +1,140 @@
-import type { AxiosRequestConfig } from "axios"
+import type { AxiosRequestConfig } from "axios";
-import type { IChatWithLLMRequest } from "@/interfaces"
-import BaseClientService, { BaseUrl } from "./base_service.ts"
+import type { IChatWithLLMRequest, UsageInfo } from "@/interfaces";
+import BaseClientService, { BaseUrl } from "./base_service.ts";
export class ChatService {
- public static basePath = BaseUrl
+ public static basePath = BaseUrl;
/** Chat with LLM */
public static async ChatWithLLM(
accessToken: string,
request: IChatWithLLMRequest,
onProgress: (content: string) => void,
+ getUsageInfo: (object: UsageInfo) => void = () => {},
+ getThinking: (thinkingContent: string) => void
) {
- let response
- let buffer = ""
- let accumulatedContent = ""
+ let response;
+ let buffer = "";
+ let accumulatedContent = "";
+ let thinking = false;
+ let thinkingContent = "";
try {
response = await fetch("/v1/chat/completions", {
method: "POST",
headers: {
- "Authorization": `Bearer ${accessToken}`,
- "Content-Type": "application/json",
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json"
},
- body: JSON.stringify(request),
- })
+ body: JSON.stringify(request)
+ });
if (!response.ok) {
// eslint-disable-next-line unicorn/error-message
- throw new Error()
+ throw new Error();
}
- const reader = response.body?.getReader()
- const decoder = new TextDecoder()
+ const reader = response.body?.getReader();
+ const decoder = new TextDecoder();
while (true) {
- const { done, value } = await reader!.read()
+ const { done, value } = await reader!.read();
- if (done)
- break
+ if (done) break;
// 将二进制数据转为字符串并存入缓冲区
- buffer += decoder.decode(value)
+ buffer += decoder.decode(value);
// 查找换行符分割数据
- const lines = buffer.split("\n")
+ const lines = buffer.split("\n");
// 保留未处理完的部分
- buffer = lines.pop() || ""
+ buffer = lines.pop() || "";
// 处理每一行
for (const line of lines) {
- const trimmedLine = line.trim()
- if (!trimmedLine)
- continue
+ const trimmedLine = line.trim();
+ if (!trimmedLine) continue;
if (trimmedLine.startsWith("data: ")) {
- const jsonStr = trimmedLine.slice(6)
+ const jsonStr = trimmedLine.slice(6);
// 处理结束标记
if (jsonStr === "[DONE]") {
- onProgress(accumulatedContent) // 最终更新
- return
+ onProgress(accumulatedContent); // 最终更新
+ return;
}
try {
- const data = JSON.parse(jsonStr)
+ const data = JSON.parse(jsonStr);
+
+ // 处理内容
if (data.choices?.[0]?.delta?.content) {
- // 累积内容
- accumulatedContent += data.choices[0].delta.content
- // 触发回调
- onProgress(accumulatedContent)
+ const content = data.choices[0].delta.content;
+
+ if (content.includes("")) {
+ thinking = true;
+ // 只处理前的内容
+ const [beforeThink, afterThink] = content.split("");
+ if (beforeThink) {
+ accumulatedContent += beforeThink;
+ onProgress(accumulatedContent);
+ }
+ thinkingContent = "";
+ if (afterThink) {
+ thinkingContent += afterThink;
+ getThinking(thinkingContent);
+ }
+ continue;
+ }
+
+ if (content.includes("")) {
+ thinking = false;
+ // 只处理后的内容
+ const [beforeEndThink, afterEndThink] =
+ content.split("");
+ thinkingContent += beforeEndThink;
+ getThinking(thinkingContent);
+ if (afterEndThink) {
+ accumulatedContent += afterEndThink;
+ onProgress(accumulatedContent);
+ }
+ thinkingContent = "";
+ continue;
+ }
+
+ if (thinking) {
+ thinkingContent += content;
+ getThinking(thinkingContent);
+ } else {
+ accumulatedContent += content;
+ onProgress(accumulatedContent);
+ }
}
- }
- catch (err) {
- console.error("JSON解析失败:", err)
+
+ // 处理使用信息
+ if (data.usage) {
+ const { prompt_tokens, completion_tokens, total_tokens } =
+ data.usage;
+ getUsageInfo({
+ prompt_tokens,
+ completion_tokens,
+ total_tokens
+ });
+ }
+ } catch (err) {
+ console.error("JSON解析失败:", err);
}
}
}
}
- }
- catch (err) {
- console.error("Error:", err)
+ } catch (err) {
+ console.error("Error:", err);
}
}
// 获取模型列表
public static GetModelList(config?: AxiosRequestConfig) {
- return BaseClientService.get(`${this.basePath}/model/list`, config)
+ return BaseClientService.get(`${this.basePath}/model/list`, config);
}
}
diff --git a/web/src/services/index.ts b/web/src/services/index.ts
index 44a55fc..fef3ef3 100644
--- a/web/src/services/index.ts
+++ b/web/src/services/index.ts
@@ -1,3 +1,4 @@
-export * from "./base_service"
-export * from "./chat_service"
-export * from "./websocket"
+export * from "./audio_websocket";
+export * from "./base_service";
+export * from "./chat_service";
+export * from "./websocket";
diff --git a/web/src/services/websocket.ts b/web/src/services/websocket.ts
index 397ea0c..489ec21 100644
--- a/web/src/services/websocket.ts
+++ b/web/src/services/websocket.ts
@@ -1,28 +1,143 @@
-import { useChatStore } from "@/stores";
+import { useChatStore, useTtsStore } from "@/stores";
// WebSocket
export const useWebSocketStore = defineStore("websocket", () => {
const websocket = ref();
const connected = ref(false);
const chatStore = useChatStore();
+ const ttsStore = useTtsStore();
const { onlineCount } = storeToRefs(chatStore);
const onmessage = (e: MessageEvent) => {
- const data = JSON.parse(e.data);
- switch (data.type) {
- case "count":
- onlineCount.value = data.online_count;
- break;
- case "asr_result":
- chatStore.addMessageToHistory(data.result);
+ // 检查消息类型
+ if (e.data instanceof ArrayBuffer) {
+ // 处理二进制音频数据(兜底处理,新版本应该不会用到)
+ console.log("收到二进制音频数据,大小:", e.data.byteLength);
+ console.warn("收到旧格式的二进制数据,无法确定messageId");
+ // 可以选择忽略或者作为兜底处理
+ } else if (e.data instanceof Blob) {
+ // 如果是Blob,转换为ArrayBuffer(兜底处理)
+ e.data.arrayBuffer().then((buffer: ArrayBuffer) => {
+ console.log("收到Blob音频数据,大小:", buffer.byteLength);
+ console.warn("收到旧格式的Blob数据,无法确定messageId");
+ });
+ } else if (typeof e.data === "string") {
+ // 处理文本JSON消息
+ try {
+ const data = JSON.parse(e.data);
+ switch (data.type) {
+ case "count":
+ onlineCount.value = data.online_count;
+ break;
+ case "asr_result":
+ chatStore.addMessageToHistory(data.result);
+ break;
+
+ // 新的TTS消息格式处理
+ case "tts_audio_data":
+ // 新的音频数据格式,包含messageId和hex格式的音频数据
+ if (data.messageId && data.audioData) {
+ console.log(
+ `收到TTS音频数据 [${data.messageId}],hex长度:`,
+ data.audioData.length
+ );
+ try {
+ // 将hex字符串转换为ArrayBuffer
+ const bytes = data.audioData
+ .match(/.{1,2}/g)
+ ?.map((byte: string) => Number.parseInt(byte, 16));
+ if (bytes) {
+ const buffer = new Uint8Array(bytes).buffer;
+ console.log(
+ `转换后的音频数据大小 [${data.messageId}]:`,
+ buffer.byteLength
+ );
+ ttsStore.handleAudioData(buffer, data.messageId);
+ } else {
+ console.error(`音频数据格式错误 [${data.messageId}]`);
+ }
+ } catch (error) {
+ console.error(`音频数据转换失败 [${data.messageId}]:`, error);
+ ttsStore.handleError(
+ `音频数据转换失败: ${error}`,
+ data.messageId
+ );
+ }
+ } else {
+ console.error("tts_audio_data消息格式错误:", data);
+ }
+ break;
+
+ case "tts_audio_complete":
+ // TTS音频传输完成
+ if (data.messageId) {
+ console.log(`TTS音频传输完成 [${data.messageId}]`);
+ ttsStore.finishConversion(data.messageId);
+ } else {
+ console.log("TTS音频传输完成(无messageId)");
+ // 兜底处理,可能是旧格式
+ ttsStore.finishConversion(data.messageId);
+ }
+ break;
+
+ case "tts_complete":
+ // TTS会话结束
+ if (data.messageId) {
+ console.log(`TTS会话结束 [${data.messageId}]`);
+ // 可以添加额外的清理逻辑
+ } else {
+ console.log("TTS会话结束");
+ }
+ break;
+
+ case "tts_error":
+ // TTS错误
+ if (data.messageId) {
+ console.error(`TTS错误 [${data.messageId}]:`, data.message);
+ ttsStore.handleError(data.message, data.messageId);
+ } else {
+ console.error("TTS错误:", data.message);
+ // 兜底处理,可能是旧格式
+ ttsStore.handleError(data.message, data.messageId || "unknown");
+ }
+ break;
+
+ // 保留旧的消息类型作为兜底处理
+ case "tts_audio_complete_legacy":
+ case "tts_complete_legacy":
+ case "tts_error_legacy":
+ console.log("收到旧格式TTS消息:", data.type);
+ // 可以选择处理或忽略
+ break;
+
+ default:
+ console.log("未知消息类型:", data.type, data);
+ }
+ } catch (error) {
+ console.error("JSON解析错误:", error, "原始数据:", e.data);
+ }
+ } else {
+ console.warn("收到未知格式的消息:", typeof e.data, e.data);
}
};
const send = (data: string) => {
- if (websocket.value && websocket.value.readyState === WebSocket.OPEN)
+ if (websocket.value && websocket.value.readyState === WebSocket.OPEN) {
websocket.value?.send(data);
+ } else {
+ console.warn("WebSocket未连接,无法发送消息:", data);
+ }
};
+
+ const sendBinary = (data: ArrayBuffer | Uint8Array) => {
+ if (websocket.value && websocket.value.readyState === WebSocket.OPEN) {
+ websocket.value?.send(data);
+ } else {
+ console.warn("WebSocket未连接,无法发送二进制数据");
+ }
+ };
+
const close = () => {
websocket.value?.close();
};
@@ -33,12 +148,15 @@ export const useWebSocketStore = defineStore("websocket", () => {
websocket.value.onopen = () => {
connected.value = true;
+ console.log("WebSocket连接成功");
let pingIntervalId: NodeJS.Timeout | undefined;
- if (pingIntervalId)
- clearInterval(pingIntervalId);
- pingIntervalId = setInterval(() => send("ping"), 30 * 1000);
+ if (pingIntervalId) clearInterval(pingIntervalId);
+ pingIntervalId = setInterval(() => {
+ // 修改ping格式为JSON格式,与后端保持一致
+ send(JSON.stringify({ type: "ping" }));
+ }, 30 * 1000);
if (websocket.value) {
websocket.value.onmessage = onmessage;
@@ -46,21 +164,29 @@ export const useWebSocketStore = defineStore("websocket", () => {
websocket.value.onerror = (e: Event) => {
console.error(`WebSocket错误:${(e as ErrorEvent).message}`);
};
- websocket.value.onclose = () => {
+
+ websocket.value.onclose = (e: CloseEvent) => {
connected.value = false;
+ console.log(`WebSocket连接关闭: ${e.code} ${e.reason}`);
setTimeout(() => {
+ console.log("尝试重新连接WebSocket...");
connect(); // 尝试重新连接
}, 1000); // 1秒后重试连接
};
}
};
+
+ websocket.value.onerror = (e: Event) => {
+ console.error("WebSocket连接错误:", e);
+ };
};
return {
websocket,
connected,
send,
+ sendBinary,
close,
- connect,
+ connect
};
});
diff --git a/web/src/stores/asr_store.ts b/web/src/stores/asr_store.ts
index 10b02e3..f5b6b76 100644
--- a/web/src/stores/asr_store.ts
+++ b/web/src/stores/asr_store.ts
@@ -23,8 +23,7 @@ export const useAsrStore = defineStore("asr", () => {
if (webSocketStore.connected) {
if (typeof data === "string") {
webSocketStore.send(data);
- }
- else {
+ } else {
webSocketStore.websocket?.send(data);
}
}
@@ -53,8 +52,7 @@ export const useAsrStore = defineStore("asr", () => {
* 开始录音
*/
const startRecording = async () => {
- if (isRecording.value)
- return;
+ if (isRecording.value) return;
messages.value = [];
// 确保 WebSocket 已连接
if (!webSocketStore.connected) {
@@ -62,8 +60,7 @@ export const useAsrStore = defineStore("asr", () => {
// 等待连接建立
await new Promise((resolve) => {
const check = () => {
- if (webSocketStore.connected)
- resolve();
+ if (webSocketStore.connected) resolve();
else setTimeout(check, 100);
};
check();
@@ -73,11 +70,14 @@ export const useAsrStore = defineStore("asr", () => {
// 获取麦克风音频流
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 创建音频上下文,采样率16kHz
- audioContext = new (window.AudioContext || (window as any).webkitAudioContext)({
- sampleRate: 16000,
+ audioContext = new (window.AudioContext ||
+ (window as any).webkitAudioContext)({
+ sampleRate: 16000
});
// 用Blob方式创建AudioWorklet模块的URL
- const blob = new Blob([audioProcessorCode], { type: "application/javascript" });
+ const blob = new Blob([audioProcessorCode], {
+ type: "application/javascript"
+ });
const processorUrl = URL.createObjectURL(blob);
// 加载AudioWorklet模块
await audioContext.audioWorklet.addModule(processorUrl);
@@ -89,7 +89,7 @@ export const useAsrStore = defineStore("asr", () => {
workletNode = new AudioWorkletNode(audioContext, "audio-processor", {
numberOfInputs: 1,
numberOfOutputs: 1,
- channelCount: 1,
+ channelCount: 1
});
// 监听来自AudioWorklet的音频数据
workletNode.port.onmessage = (event) => {
@@ -104,8 +104,7 @@ export const useAsrStore = defineStore("asr", () => {
mediaStreamSource.connect(workletNode);
workletNode.connect(audioContext.destination);
isRecording.value = true;
- }
- catch (err) {
+ } catch (err) {
// 麦克风权限失败或AudioWorklet加载失败
console.error("需要麦克风权限才能录音", err);
}
@@ -115,8 +114,7 @@ export const useAsrStore = defineStore("asr", () => {
* 停止录音
*/
const stopRecording = () => {
- if (!isRecording.value)
- return;
+ if (!isRecording.value) return;
// 通知后端录音结束
sendMessage(JSON.stringify({ type: "asr_end" }));
@@ -124,7 +122,7 @@ export const useAsrStore = defineStore("asr", () => {
// 停止所有音轨
if (mediaStreamSource?.mediaStream) {
const tracks = mediaStreamSource.mediaStream.getTracks();
- tracks.forEach(track => track.stop());
+ tracks.forEach((track) => track.stop());
}
// 断开音频节点
@@ -149,6 +147,6 @@ export const useAsrStore = defineStore("asr", () => {
messages,
startRecording,
stopRecording,
- sendMessage,
+ sendMessage
};
});
diff --git a/web/src/stores/chat_store.ts b/web/src/stores/chat_store.ts
index 1016a8d..8392837 100644
--- a/web/src/stores/chat_store.ts
+++ b/web/src/stores/chat_store.ts
@@ -1,35 +1,51 @@
-import type { IChatWithLLMRequest, ModelInfo, ModelListInfo } from "@/interfaces";
+import type {
+ IChatWithLLMRequest,
+ ModelInfo,
+ ModelListInfo,
+ UsageInfo
+} from "@/interfaces";
import { ChatService } from "@/services";
export const useChatStore = defineStore("chat", () => {
- const token = ("sk-fkGVZBrAqvIxLjlF3b5f19EfBb63486c90Fa5a1fBd7076Ee");
+ const token = "sk-fkGVZBrAqvIxLjlF3b5f19EfBb63486c90Fa5a1fBd7076Ee";
// 默认模型
const modelInfo = ref(null);
// 历史消息
const historyMessages = ref([]);
// 是否正在响应
const completing = ref(false);
+ // 是否正在思考
+ const thinking = ref(false);
// 在线人数
const onlineCount = ref(0);
// 与 LLM 聊天
const chatWithLLM = async (
request: IChatWithLLMRequest,
- onProgress: (content: string) => void, // 接收进度回调
+ onProgress: (content: string) => void, // 接收内容进度回调
+ getUsageInfo: (object: UsageInfo) => void = () => {}, // 接收使用信息回调
+ getThinking: (thinkingContent: string) => void = () => {} // 接收思维链内容回调
) => {
- if (completing.value)
- throw new Error("正在响应中");
+ if (completing.value) throw new Error("正在响应中");
completing.value = true; // 开始请求
try {
- await ChatService.ChatWithLLM(token, request, (content) => {
- onProgress(content);
- });
- }
- catch (error) {
+ await ChatService.ChatWithLLM(
+ token,
+ request,
+ (content) => {
+ onProgress(content);
+ },
+ (object: UsageInfo) => {
+ getUsageInfo(object);
+ },
+ (thinkingContent: string) => {
+ getThinking(thinkingContent);
+ }
+ );
+ } catch (error) {
console.error("请求失败:", error);
- }
- finally {
+ } finally {
completing.value = false;
}
};
@@ -37,12 +53,11 @@ export const useChatStore = defineStore("chat", () => {
// 添加消息到历史记录
const addMessageToHistory = (message: string) => {
const content = message.trim();
- if (!content)
- return;
+ if (!content) return;
historyMessages.value.push({
role: "user",
- content,
+ content
});
};
@@ -51,30 +66,67 @@ export const useChatStore = defineStore("chat", () => {
historyMessages.value = [];
};
- watch(historyMessages, (newVal) => {
- // 当历史消息变化时,发送请求
- if (newVal.length > 0) {
- const lastMessage = newVal[newVal.length - 1];
- if (lastMessage.role === "user" && modelInfo.value) {
- chatWithLLM({
- messages: newVal,
- model: modelInfo.value?.model_id,
- }, (content) => {
- // 处理进度回调
- if (
- historyMessages.value.length === 0
- || historyMessages.value[historyMessages.value.length - 1].role !== "assistant"
- ) {
- historyMessages.value.push({
- role: "assistant",
- content: "",
- });
- }
- historyMessages.value[historyMessages.value.length - 1].content = content;
- });
- }
+ // 确保最后一条消息是助手消息,如果最后一条消息不是,就加一条空的占位,不然后面的思维链会丢失
+ const ensureAssistantMessage = () => {
+ if (
+ historyMessages.value.length === 0 ||
+ historyMessages.value[historyMessages.value.length - 1].role !==
+ "assistant"
+ ) {
+ historyMessages.value.push({
+ role: "assistant",
+ content: ""
+ });
}
- }, { deep: true });
+ };
+
+ watch(
+ historyMessages,
+ (newVal) => {
+ // 当历史消息变化时,发送请求
+ if (newVal.length > 0) {
+ const lastMessage = newVal[newVal.length - 1];
+ if (lastMessage.role === "user" && modelInfo.value) {
+ chatWithLLM(
+ {
+ messages: newVal,
+ model: modelInfo.value?.model_id
+ },
+ // 处理进度回调,文本
+ (content) => {
+ ensureAssistantMessage();
+ thinking.value = false;
+ historyMessages.value[historyMessages.value.length - 1].content =
+ content;
+ },
+ // 处理使用usage信息回调
+ (usageInfo: UsageInfo) => {
+ // 如果最后一条消息是助手的回复,则更新使用信息
+ if (
+ historyMessages.value.length > 0 &&
+ historyMessages.value[historyMessages.value.length - 1].role ===
+ "assistant"
+ ) {
+ historyMessages.value[historyMessages.value.length - 1].usage =
+ usageInfo;
+ }
+ },
+ // 处理思维链内容回调
+ (thinkingContent: string) => {
+ ensureAssistantMessage();
+ thinking.value = true;
+ historyMessages.value[historyMessages.value.length - 1].thinking =
+ thinkingContent;
+ }
+ ).then(() => {
+ historyMessages.value[historyMessages.value.length - 1].id =
+ new Date().getTime().toString();
+ });
+ }
+ }
+ },
+ { deep: true }
+ );
// 模型列表
const modelList = ref([]);
@@ -84,11 +136,22 @@ export const useChatStore = defineStore("chat", () => {
try {
const response = await ChatService.GetModelList();
modelList.value = response.data.data;
- }
- catch (error) {
+ } catch (error) {
console.error("获取模型列表失败:", error);
}
};
- return { token, completing, chatWithLLM, historyMessages, addMessageToHistory, clearHistoryMessages, getModelList, modelList, modelInfo, onlineCount };
+ return {
+ token,
+ completing,
+ chatWithLLM,
+ historyMessages,
+ addMessageToHistory,
+ clearHistoryMessages,
+ getModelList,
+ modelList,
+ modelInfo,
+ onlineCount,
+ thinking
+ };
});
diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts
index fd59d5d..9647e3d 100644
--- a/web/src/stores/index.ts
+++ b/web/src/stores/index.ts
@@ -1,2 +1,4 @@
-export * from "./asr_store"
-export * from "./chat_store"
+export * from "./asr_store";
+export * from "./chat_store";
+export * from "./layout_store";
+export * from "./tts_store";
\ No newline at end of file
diff --git a/web/src/stores/layout_store.ts b/web/src/stores/layout_store.ts
new file mode 100644
index 0000000..98db981
--- /dev/null
+++ b/web/src/stores/layout_store.ts
@@ -0,0 +1,44 @@
+import { matchMedia } from "@/utils";
+
+export const useLayoutStore = defineStore("layout", () => {
+ // 侧边栏状态
+ const hiddenLeftSidebar = ref(false);
+ // 简洁按钮
+ const simpleMode = ref(false);
+
+ const handleResize = () => {
+ matchMedia(
+ "sm",
+ () => {
+ hiddenLeftSidebar.value = true;
+ },
+ () => {
+ hiddenLeftSidebar.value = false;
+ }
+ );
+ matchMedia(
+ "536",
+ () => {
+ simpleMode.value = true;
+ },
+ () => {
+ simpleMode.value = false;
+ }
+ );
+ };
+
+ const toggleLeftSidebar = () => {
+ hiddenLeftSidebar.value = !hiddenLeftSidebar.value;
+ };
+
+ window.addEventListener("resize", handleResize);
+
+ onMounted(() => {
+ handleResize();
+ });
+
+ onUnmounted(() => {
+ window.removeEventListener("resize", handleResize);
+ });
+ return { hiddenLeftSidebar, toggleLeftSidebar, simpleMode };
+});
diff --git a/web/src/stores/tts_store.ts b/web/src/stores/tts_store.ts
new file mode 100644
index 0000000..a8a22fb
--- /dev/null
+++ b/web/src/stores/tts_store.ts
@@ -0,0 +1,302 @@
+import { useAudioWebSocket } from "@/services";
+import { createAudioUrl, mergeAudioChunks } from "@/utils";
+
+interface AudioState {
+ isPlaying: boolean;
+ isLoading: boolean;
+ audioElement: HTMLAudioElement | null;
+ audioUrl: string | null;
+ audioChunks: ArrayBuffer[];
+ hasError: boolean;
+ errorMessage: string;
+}
+
+export const useTtsStore = defineStore("tts", () => {
+ // 多音频状态管理 - 以消息ID为key
+ const audioStates = ref