From 3de7ef68ba58f14fb20feb3de409b05ed8a1c88d Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Thu, 17 Apr 2025 16:44:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=88=90=E6=9C=AC=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + renderer/src/App.vue | 2 +- .../src/components/main-panel/chat/chat.ts | 13 ++- .../src/components/main-panel/chat/index.vue | 29 +++++-- .../main-panel/chat/message-meta.vue | 76 +++++++++++++++++ .../components/main-panel/chat/task-loop.ts | 52 ++++++++++-- .../src/components/main-panel/chat/usage.ts | 37 +++++++++ service/src/controller/index.ts | 2 + service/tabs.untitle.json | 4 - service/tabs.锦恢的 MCP Server.json | 83 ++++++++++++++++++- 10 files changed, 274 insertions(+), 25 deletions(-) create mode 100644 renderer/src/components/main-panel/chat/message-meta.vue create mode 100644 renderer/src/components/main-panel/chat/usage.ts delete mode 100644 service/tabs.untitle.json diff --git a/README.md b/README.md index b4de460..f1f6f3a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ ## TODO - [x] 完成最基本的各类基础设施 +- [ ] chat 模式下支持进行成本分析 - [ ] 支持同时调试多个 MCP Server - [ ] 支持通过大模型进行在线验证 - [ ] 支持 completion/complete 协议字段 diff --git a/renderer/src/App.vue b/renderer/src/App.vue index 3f8c13f..a99d729 100644 --- a/renderer/src/App.vue +++ b/renderer/src/App.vue @@ -29,7 +29,7 @@ bridge.addCommandListener('hello', data => { function initDebug() { - connectionArgs.commandString = 'mcp run ../servers/main.py'; + connectionArgs.commandString = 'uv run mcp run ../servers/main.py'; connectionMethods.current = 'STDIO'; setTimeout(async () => { diff --git a/renderer/src/components/main-panel/chat/chat.ts b/renderer/src/components/main-panel/chat/chat.ts index b4a7732..9a0e6bb 100644 --- a/renderer/src/components/main-panel/chat/chat.ts +++ b/renderer/src/components/main-panel/chat/chat.ts @@ -1,12 +1,23 @@ import { ToolItem } from "@/hook/type"; import { ref } from "vue"; +import type { OpenAI } from 'openai'; +type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; + +export interface IExtraInfo { + created: number, + serverName: string, + usage?: ChatCompletionChunk['usage']; + [key: string]: any +} + export interface ChatMessage { role: 'user' | 'assistant' | 'system' | 'tool'; content: string; tool_call_id?: string name?: string // 工具名称,当 role 为 tool - tool_calls?: ToolCall[] + tool_calls?: ToolCall[], + extraInfo: IExtraInfo } // 新增状态和工具数据 diff --git a/renderer/src/components/main-panel/chat/index.vue b/renderer/src/components/main-panel/chat/index.vue index 9db42a9..438c299 100644 --- a/renderer/src/components/main-panel/chat/index.vue +++ b/renderer/src/components/main-panel/chat/index.vue @@ -22,6 +22,7 @@
+ @@ -81,6 +82,7 @@ + @@ -129,12 +131,16 @@ import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, ne import { useI18n } from 'vue-i18n'; import { ElMessage, ScrollbarInstance } from 'element-plus'; import { tabs } from '../panel'; -import { ChatMessage, ChatStorage, getToolSchema, ToolCall } from './chat'; +import { ChatMessage, ChatStorage, getToolSchema, IExtraInfo, ToolCall } from './chat'; + import Setting from './setting.vue'; +import MessageMeta from './message-meta.vue'; + // 引入 markdown.ts 中的函数 import { markdownToHtml, copyToClipboard } from './markdown'; -import { TaskLoop } from './task-loop'; +import { ChatCompletionChunk, TaskLoop } from './task-loop'; +import { llmManager, llms } from '@/views/setting/llm'; defineComponent({ name: 'chat' }); @@ -174,6 +180,7 @@ interface IRenderMessage { toolResult?: string; tool_calls?: ToolCall[]; showJson?: Ref; + extraInfo: IExtraInfo; } const renderMessages = computed(() => { @@ -182,7 +189,8 @@ const renderMessages = computed(() => { if (message.role === 'user') { messages.push({ role: 'user', - content: message.content + content: message.content, + extraInfo: message.extraInfo }); } else if (message.role === 'assistant') { if (message.tool_calls) { @@ -190,12 +198,14 @@ const renderMessages = computed(() => { role: 'assistant/tool_calls', content: message.content, tool_calls: message.tool_calls, - showJson: ref(false) + showJson: ref(false), + extraInfo: message.extraInfo }); } else { messages.push({ role: 'assistant/content', - content: message.content + content: message.content, + extraInfo: message.extraInfo }); } @@ -302,7 +312,11 @@ const handleSend = () => { tabStorage.messages.push({ role: 'assistant', - content: `错误: ${msg}` + content: `错误: ${msg}`, + extraInfo: { + created: Date.now(), + serverName: llms[llmManager.currentModelIndex].id || 'unknown' + } }); isLoading.value = false; @@ -361,7 +375,6 @@ const jsonResultToHtml = (jsonString: string) => { return html; }; -// 新增格式化工具参数的方法 const formatToolArguments = (args: string) => { try { const parsed = JSON.parse(args); @@ -510,9 +523,7 @@ const formatToolArguments = (args: string) => { \ No newline at end of file diff --git a/renderer/src/components/main-panel/chat/task-loop.ts b/renderer/src/components/main-panel/chat/task-loop.ts index 6eb1d1f..2e2c6ab 100644 --- a/renderer/src/components/main-panel/chat/task-loop.ts +++ b/renderer/src/components/main-panel/chat/task-loop.ts @@ -6,8 +6,8 @@ import type { OpenAI } from 'openai'; import { callTool } from "../tool/tools"; import { llmManager, llms } from "@/views/setting/llm"; -type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; -type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string }; +export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; +export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string }; interface TaskLoopOptions { maxEpochs: number; } @@ -18,6 +18,7 @@ interface TaskLoopOptions { export class TaskLoop { private bridge = useMessageBridge(); private currentChatId = ''; + private completionUsage: ChatCompletionChunk['usage'] | undefined; constructor( private readonly streamingContent: Ref, @@ -27,7 +28,9 @@ export class TaskLoop { private onDone: () => void = () => {}, private onEpoch: () => void = () => {}, private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20 }, - ) {} + ) { + + } private async handleToolCalls(toolCalls: ToolCall[]) { // TODO: 调用多个工具并返回调用结果? @@ -89,6 +92,13 @@ export class TaskLoop { } } + private handleChunkUsage(chunk: ChatCompletionChunk) { + const usage = chunk.usage; + if (usage) { + this.completionUsage = usage; + } + } + private doConversation(chatData: ChatCompletionCreateParamsBase) { return new Promise((resolve, reject) => { @@ -99,15 +109,16 @@ export class TaskLoop { return; } const { chunk } = data.msg as { chunk: ChatCompletionChunk }; - + // 处理增量的 content 和 tool_calls this.handleChunkDeltaContent(chunk); this.handleChunkDeltaToolCalls(chunk); + this.handleChunkUsage(chunk); this.onChunk(chunk); }, { once: false }); - this.bridge.addCommandListener('llm/chat/completions/done', data => { + this.bridge.addCommandListener('llm/chat/completions/done', data => { this.onDone(); chunkHandler(); @@ -140,6 +151,7 @@ export class TaskLoop { // 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息 const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength); userMessages.push(...loadMessages); + // 增加一个id用于锁定状态 const id = crypto.randomUUID(); @@ -188,7 +200,14 @@ export class TaskLoop { */ public async start(tabStorage: ChatStorage, userMessage: string) { // 添加目前的消息 - tabStorage.messages.push({ role: 'user', content: userMessage }); + tabStorage.messages.push({ + role: 'user', + content: userMessage, + extraInfo: { + created: Date.now(), + serverName: llms[llmManager.currentModelIndex].id || 'unknown' + } + }); for (let i = 0; i < this.taskOptions.maxEpochs; ++ i) { @@ -197,6 +216,7 @@ export class TaskLoop { // 初始累计清空 this.streamingContent.value = ''; this.streamingToolCalls.value = []; + this.completionUsage = undefined; // 构造 chatData const chatData = this.makeChatData(tabStorage); @@ -212,7 +232,11 @@ export class TaskLoop { tabStorage.messages.push({ role: 'assistant', content: this.streamingContent.value || '', - tool_calls: this.streamingToolCalls.value + tool_calls: this.streamingToolCalls.value, + extraInfo: { + created: Date.now(), + serverName: llms[llmManager.currentModelIndex].id || 'unknown' + } }); const toolCallResult = await this.handleToolCalls(this.streamingToolCalls.value); @@ -222,14 +246,24 @@ export class TaskLoop { tabStorage.messages.push({ role: 'tool', tool_call_id: toolCall.id || toolCall.function.name, - content: toolCallResult + content: toolCallResult, + extraInfo: { + created: Date.now(), + serverName: llms[llmManager.currentModelIndex].id || 'unknown', + usage: this.completionUsage + } }); } } else if (this.streamingContent.value) { tabStorage.messages.push({ role: 'assistant', - content: this.streamingContent.value + content: this.streamingContent.value, + extraInfo: { + created: Date.now(), + serverName: llms[llmManager.currentModelIndex].id || 'unknown', + usage: this.completionUsage + } }); break; diff --git a/renderer/src/components/main-panel/chat/usage.ts b/renderer/src/components/main-panel/chat/usage.ts new file mode 100644 index 0000000..5a560f0 --- /dev/null +++ b/renderer/src/components/main-panel/chat/usage.ts @@ -0,0 +1,37 @@ +import { IExtraInfo } from "./chat"; + +export interface UsageStatistic { + input: number; + output: number; + total: number; + cacheHitRatio: number; +} + +export function makeUsageStatistic(extraInfo: IExtraInfo): UsageStatistic | undefined { + if (extraInfo.serverName === 'unknown' || extraInfo.usage === undefined || extraInfo.usage === null) { + return undefined; + } + + const usage = extraInfo.usage; + + switch (extraInfo.serverName) { + case 'deepseek': + return { + input: usage.prompt_tokens, + output: usage.completion_tokens, + total: usage.prompt_tokens + usage.completion_tokens, + cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10, + } + + case 'openai': + return { + // TODO: 完成其他的数值统计 + input: usage?.prompt_tokens, + output: usage?.completion_tokens, + total: usage.prompt_tokens + usage.completion_tokens, + cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10, + } + } + + return undefined; +} \ No newline at end of file diff --git a/service/src/controller/index.ts b/service/src/controller/index.ts index 548a933..2dd6819 100644 --- a/service/src/controller/index.ts +++ b/service/src/controller/index.ts @@ -27,6 +27,8 @@ async function connectHandler(option: MCPOptions, webview: PostMessageble) { // 比如 error: Failed to spawn: `server.py` // Caused by: No such file or directory (os error 2) + console.log('error', error); + const connectResult = { code: 500, msg: (error as any).toString() diff --git a/service/tabs.untitle.json b/service/tabs.untitle.json deleted file mode 100644 index 497bc1e..0000000 --- a/service/tabs.untitle.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tabs": [], - "currentIndex": -1 -} \ No newline at end of file diff --git a/service/tabs.锦恢的 MCP Server.json b/service/tabs.锦恢的 MCP Server.json index 044cec9..c5fc1f7 100644 --- a/service/tabs.锦恢的 MCP Server.json +++ b/service/tabs.锦恢的 MCP Server.json @@ -1,5 +1,5 @@ { - "currentIndex": 1, + "currentIndex": 2, "tabs": [ { "name": "资源", @@ -18,6 +18,87 @@ "storage": { "currentPromptName": "translate" } + }, + { + "name": "交互测试", + "icon": "icon-robot", + "type": "blank", + "componentIndex": 3, + "storage": { + "messages": [ + { + "role": "user", + "content": "你好,请问什么是", + "extraInfo": { + "created": 1744876735890, + "serverName": "deepseek" + } + }, + { + "role": "assistant", + "content": "你好!请问你是想问什么呢?可以具体一点吗?比如:\n\n- 什么是人工智能?\n- 什么是区块链?\n- 什么是量子计算?\n- 或者其他任何你想了解的概念或问题?\n\n告诉我你的具体需求,我会尽力解答!", + "extraInfo": { + "created": 1744876742266, + "serverName": "deepseek", + "usage": { + "prompt_tokens": 453, + "completion_tokens": 49, + "total_tokens": 502, + "prompt_tokens_details": { + "cached_tokens": 448 + }, + "prompt_cache_hit_tokens": 448, + "prompt_cache_miss_tokens": 5 + } + } + }, + { + "role": "user", + "content": "你的名字是", + "extraInfo": { + "created": 1744878380791, + "serverName": "openai" + } + }, + { + "role": "assistant", + "content": "错误: OpenAI API error: 404 404 page not found", + "extraInfo": { + "created": 1744878381940, + "serverName": "openai" + } + } + ], + "settings": { + "modelIndex": 0, + "enableTools": [ + { + "name": "add", + "description": "对两个数字进行实数域的加法", + "enabled": true + }, + { + "name": "multiply", + "description": "对两个数字进行实数域的乘法运算", + "enabled": true + }, + { + "name": "is_even", + "description": "判断一个整数是否为偶数", + "enabled": true + }, + { + "name": "capitalize", + "description": "将字符串首字母大写", + "enabled": true + } + ], + "enableWebSearch": false, + "temperature": 0.7, + "contextLength": 10, + "systemPrompt": "" + } + } } ] } \ No newline at end of file