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