From dfc817e3e32bbbdc7c520016222b652510c959aa Mon Sep 17 00:00:00 2001 From: Marcus <1922576605@qq.com> Date: Sun, 29 Jun 2025 01:25:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=80=E4=BA=9B=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E4=BB=A5=E5=8F=8Atoken=E6=B6=88=E8=80=97=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/avatar.vue | 4 +- web/src/components/markdown.vue | 45 +++++++++++++++++---- web/src/interfaces/chat_service.ts | 27 ++++++++----- web/src/services/chat_service.ts | 63 +++++++++++++++++------------- web/src/stores/chat_store.ts | 14 ++++++- web/src/stores/index.ts | 4 +- web/src/utils/media.ts | 36 ++++++++++++----- web/src/views/CommunityView.vue | 17 +++++--- 8 files changed, 146 insertions(+), 64 deletions(-) diff --git a/web/src/components/avatar.vue b/web/src/components/avatar.vue index 8ce912d..1773180 100644 --- a/web/src/components/avatar.vue +++ b/web/src/components/avatar.vue @@ -6,8 +6,8 @@ const { avatar } = defineProps<{ diff --git a/web/src/components/markdown.vue b/web/src/components/markdown.vue index 015d256..18b6708 100644 --- a/web/src/components/markdown.vue +++ b/web/src/components/markdown.vue @@ -1,10 +1,13 @@ diff --git a/web/src/interfaces/chat_service.ts b/web/src/interfaces/chat_service.ts index d04f064..7d62063 100644 --- a/web/src/interfaces/chat_service.ts +++ b/web/src/interfaces/chat_service.ts @@ -1,24 +1,31 @@ export interface IChatWithLLMRequest { - messages: Message[] + messages: Message[]; /** * 要使用的模型的 ID */ - model: string + model: string; } export interface Message { - content?: string - role?: string - [property: string]: any + content?: string; + role?: string; + usage?: UsageInfo; + [property: string]: any; } export interface ModelInfo { - model_id: string - model_name: string - model_type: string + model_id: string; + model_name: string; + model_type: string; } export interface ModelListInfo { - vendor: string - models: ModelInfo[] + vendor: string; + models: ModelInfo[]; +} + +export interface UsageInfo { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; } diff --git a/web/src/services/chat_service.ts b/web/src/services/chat_service.ts index d075ea7..336e197 100644 --- a/web/src/services/chat_service.ts +++ b/web/src/services/chat_service.ts @@ -1,20 +1,21 @@ -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 = () => { }, ) { - let response - let buffer = "" - let accumulatedContent = "" + let response; + let buffer = ""; + let accumulatedContent = ""; try { response = await fetch("/v1/chat/completions", { method: "POST", @@ -23,69 +24,77 @@ export class ChatService { "Content-Type": "application/json", }, 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 + 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() + const trimmedLine = line.trim(); if (!trimmedLine) - continue + 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 + accumulatedContent += data.choices[0].delta.content; // 触发回调 - onProgress(accumulatedContent) + onProgress(accumulatedContent); + } + + // 处理使用信息 + 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) + console.error("JSON解析失败:", err); } } } } } catch (err) { - console.error("Error:", 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/stores/chat_store.ts b/web/src/stores/chat_store.ts index 1016a8d..c8c2827 100644 --- a/web/src/stores/chat_store.ts +++ b/web/src/stores/chat_store.ts @@ -1,4 +1,4 @@ -import type { IChatWithLLMRequest, ModelInfo, ModelListInfo } from "@/interfaces"; +import type { IChatWithLLMRequest, ModelInfo, ModelListInfo, UsageInfo } from "@/interfaces"; import { ChatService } from "@/services"; export const useChatStore = defineStore("chat", () => { @@ -16,6 +16,7 @@ export const useChatStore = defineStore("chat", () => { const chatWithLLM = async ( request: IChatWithLLMRequest, onProgress: (content: string) => void, // 接收进度回调 + getUsageInfo: (object: UsageInfo) => void = () => { }, ) => { if (completing.value) throw new Error("正在响应中"); @@ -24,6 +25,8 @@ export const useChatStore = defineStore("chat", () => { try { await ChatService.ChatWithLLM(token, request, (content) => { onProgress(content); + }, (object: UsageInfo) => { + getUsageInfo(object); }); } catch (error) { @@ -71,6 +74,15 @@ export const useChatStore = defineStore("chat", () => { }); } historyMessages.value[historyMessages.value.length - 1].content = content; + }, (usageInfo: UsageInfo) => { + // 处理使用usage信息回调 + // 如果最后一条消息是助手的回复,则更新使用信息 + if ( + historyMessages.value.length > 0 + && historyMessages.value[historyMessages.value.length - 1].role === "assistant" + ) { + historyMessages.value[historyMessages.value.length - 1].usage = usageInfo; + } }); } } diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts index fd59d5d..0637980 100644 --- a/web/src/stores/index.ts +++ b/web/src/stores/index.ts @@ -1,2 +1,2 @@ -export * from "./asr_store" -export * from "./chat_store" +export * from "./asr_store"; +export * from "./chat_store"; diff --git a/web/src/utils/media.ts b/web/src/utils/media.ts index 1793bad..9c47b46 100644 --- a/web/src/utils/media.ts +++ b/web/src/utils/media.ts @@ -7,37 +7,55 @@ export const matchMedia = ( if (type === "sm") { if (window.matchMedia("(max-width: 767.98px)").matches) { /* 窗口小于或等于 */ - matchFunc?.() + matchFunc?.(); } else { - mismatchFunc?.() + mismatchFunc?.(); } } else if (type === "md") { if (window.matchMedia("(max-width: 992px)").matches) { /* 窗口小于或等于 */ - matchFunc?.() + matchFunc?.(); } else { - mismatchFunc?.() + mismatchFunc?.(); } } else if (type === "lg") { if (window.matchMedia("(max-width: 1200px)").matches) { /* 窗口小于或等于 */ - matchFunc?.() + matchFunc?.(); } else { - mismatchFunc?.() + mismatchFunc?.(); } } else { if (window.matchMedia(`(max-width: ${type}px)`).matches) { /* 窗口小于或等于 */ - matchFunc?.() + matchFunc?.(); } else { - mismatchFunc?.() + mismatchFunc?.(); } } -} +}; + +export const useWindowWidth = () => { + const width = ref(window.innerWidth); + + const updateWidth = () => { + width.value = window.innerWidth; + }; + + onMounted(() => { + window.addEventListener("resize", updateWidth); + }); + + onUnmounted(() => { + window.removeEventListener("resize", updateWidth); + }); + + return width; +}; diff --git a/web/src/views/CommunityView.vue b/web/src/views/CommunityView.vue index 92c4d17..77dbea4 100644 --- a/web/src/views/CommunityView.vue +++ b/web/src/views/CommunityView.vue @@ -49,7 +49,6 @@ watch(() => modelList.value, (newVal) => { if (newVal.length > 0 && newVal[0].models.length > 0) { modelInfo.value = newVal[0].models[0]; } - console.log("Options updated:", options.value); } }, { immediate: true, deep: true }); @@ -89,17 +88,17 @@ onMounted(() => {
- +
- 助手: + 助手: 你好,我是你的智能助手,请问有什么可以帮助你的吗?
- + @@ -107,6 +106,9 @@ onMounted(() => {
{{ msg.role === 'user' ? '你:' : '助手:' }} +
+ Tokens: {{ msg.usage?.total_tokens }} +
@@ -121,7 +123,12 @@ onMounted(() => {
- +