From 0670cf83183c9dc673151c97ff7fdfb210b97c38 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Thu, 10 Apr 2025 02:36:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BB=BB=E5=8A=A1=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + renderer/package-lock.json | 34 ++++- renderer/package.json | 3 +- .../src/components/main-panel/chat/chat.ts | 21 ++-- .../src/components/main-panel/chat/index.vue | 61 ++++++++- .../components/main-panel/chat/task-loop.ts | 116 ++++++++++++++++++ service/src/controller/llm.ts | 3 + service/tabs.json | 6 +- 8 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 renderer/src/components/main-panel/chat/task-loop.ts diff --git a/README.md b/README.md index 9269f95..0bfc2b9 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ - [ ] 支持通过大模型进行在线验证 - [ ] 支持 completion/complete 协议字段 - [ ] 支持 对用户对应服务器的调试工作内容进行保存 +- [ ] 高危操作权限确认 ## Dev diff --git a/renderer/package-lock.json b/renderer/package-lock.json index ddf1852..e8dc661 100644 --- a/renderer/package-lock.json +++ b/renderer/package-lock.json @@ -18,6 +18,7 @@ "vue-router": "^4.0.3" }, "devDependencies": { + "@types/markdown-it": "^14.1.2", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "@vue/cli-plugin-babel": "~5.0.0", @@ -31,7 +32,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "prettier": "^2.4.1", - "typescript": "^5.9.0-dev.20250407", + "typescript": "^4.4.3", "unplugin-auto-import": "^0.17.5", "unplugin-vue-components": "^0.26.0" } @@ -2357,6 +2358,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.17.16", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", @@ -2372,6 +2379,22 @@ "@types/lodash": "*" } }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", @@ -12341,17 +12364,16 @@ } }, "node_modules/typescript": { - "version": "5.9.0-dev.20250407", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250407.tgz", - "integrity": "sha512-JW8/Our6MR+QYS3M134UaLWtEYdVXWzwlbg6rj3fmF9TppADEdaSNiJK90M2wmfSuu5j8Nefk93oSrZF03JkGw==", + "version": "4.9.5", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/uc.micro": { diff --git a/renderer/package.json b/renderer/package.json index e61679b..9790ee4 100644 --- a/renderer/package.json +++ b/renderer/package.json @@ -18,6 +18,7 @@ "vue-router": "^4.0.3" }, "devDependencies": { + "@types/markdown-it": "^14.1.2", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "@vue/cli-plugin-babel": "~5.0.0", @@ -31,7 +32,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "prettier": "^2.4.1", - "typescript": "^5.9.0-dev.20250407", + "typescript": "^4.4.3", "unplugin-auto-import": "^0.17.5", "unplugin-vue-components": "^0.26.0" } diff --git a/renderer/src/components/main-panel/chat/chat.ts b/renderer/src/components/main-panel/chat/chat.ts index cee1f21..9c757fc 100644 --- a/renderer/src/components/main-panel/chat/chat.ts +++ b/renderer/src/components/main-panel/chat/chat.ts @@ -2,8 +2,10 @@ import { ToolItem } from "@/hook/type"; import { ref } from "vue"; export interface ChatMessage { - role: 'user' | 'assistant' | 'system'; + role: 'user' | 'assistant' | 'system' | 'tool'; content: string; + tool_call_id?: string + name?: string // 工具名称,当 role 为 tool } // 新增状态和工具数据 @@ -27,6 +29,12 @@ export interface ChatStorage { settings: ChatSetting } +export interface ToolCall { + id?: string; + name: string; + arguments: string; +} + export const allTools = ref([]); export function getToolSchema(enableTools: EnableToolItem[]) { @@ -36,12 +44,11 @@ export function getToolSchema(enableTools: EnableToolItem[]) { const tool = allTools.value[i]; toolsSchema.push({ - name: tool.name, - description: tool.description || "", - parameters: { - type: "function", - properties: tool.inputSchema.properties, - required: tool.inputSchema.required + type: 'function', + function: { + name: tool.name, + description: tool.description || "", + parameters: tool.inputSchema } }); } diff --git a/renderer/src/components/main-panel/chat/index.vue b/renderer/src/components/main-panel/chat/index.vue index 665cb75..552fd4c 100644 --- a/renderer/src/components/main-panel/chat/index.vue +++ b/renderer/src/components/main-panel/chat/index.vue @@ -29,7 +29,10 @@
Tool
- Tool +
+ 正在调用工具: {{ streamingToolCalls[0].name }} +
+ {{ message.content }}
@@ -74,7 +77,7 @@ import { useI18n } from 'vue-i18n'; import { useMessageBridge } from "@/api/message-bridge"; import { ElMessage, ScrollbarInstance } from 'element-plus'; import { tabs } from '../panel'; -import { ChatMessage, ChatStorage, getToolSchema } from './chat'; +import { ChatMessage, ChatStorage, getToolSchema, ToolCall } from './chat'; import Setting from './setting.vue'; import { llmManager, llms } from '@/views/setting/llm'; @@ -115,7 +118,10 @@ if (!tabStorage.messages) { } const isLoading = ref(false); + const streamingContent = ref(''); +const streamingToolCalls = ref([]); + const chatContainerRef = ref(null); const messageListRef = ref(null); @@ -227,11 +233,45 @@ const handleSend = () => { return; } const { chunk } = data.msg; + const content = chunk.choices[0]?.delta?.content || ''; - + const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0]; + if (content) { streamingContent.value += content; - scrollToBottom(); // 内容更新时滚动到底部 + scrollToBottom(); + } + + if (toolCall) { + if (toolCall.index === 0) { + // 新的工具调用开始 + streamingToolCalls.value = [{ + id: toolCall.id, + name: toolCall.function?.name || '', + arguments: toolCall.function?.arguments || '' + }]; + } else { + // 累积现有工具调用的信息 + const currentCall = streamingToolCalls.value[toolCall.index]; + if (currentCall) { + if (toolCall.id) { + currentCall.id = toolCall.id; + } + if (toolCall.function?.name) { + currentCall.name = toolCall.function.name; + } + if (toolCall.function?.arguments) { + currentCall.arguments += toolCall.function.arguments; + } + } + } + } + + const finishReason = chunk.choices[0]?.finish_reason; + if (finishReason === 'tool_calls') { + // 工具调用完成,这里可以处理工具调用 + console.log('Tool calls completed:', streamingToolCalls.value); + streamingToolCalls.value = []; } }, { once: false }); @@ -249,6 +289,19 @@ const handleSend = () => { }); streamingContent.value = ''; } + // 如果有工具调用结果,也加入消息列表 + if (streamingToolCalls.value.length > 0) { + streamingToolCalls.value.forEach(tool => { + if (tool.id) { + tabStorage.messages.push({ + role: 'tool', + tool_call_id: tool.id, + content: tool.arguments + }); + } + }); + streamingToolCalls.value = []; + } isLoading.value = false; chunkHandler(); }, { once: true }); diff --git a/renderer/src/components/main-panel/chat/task-loop.ts b/renderer/src/components/main-panel/chat/task-loop.ts new file mode 100644 index 0000000..7a5a6b8 --- /dev/null +++ b/renderer/src/components/main-panel/chat/task-loop.ts @@ -0,0 +1,116 @@ +import { Ref } from "vue"; +import { ToolCall, ChatMessage } from "./chat"; +import { useMessageBridge } from "@/api/message-bridge"; + +export class TaskLoop { + private bridge = useMessageBridge(); + + constructor( + private readonly streamingContent: Ref, + private readonly streamingToolCalls: Ref, + private readonly messages: ChatMessage[], + private readonly onError: (msg: string) => void + ) {} + + private handleToolCalls(toolCalls: ToolCall[]) { + // 这里预留给调用方实现工具执行逻辑 + return Promise.resolve("工具执行结果"); + } + + private continueConversation(chatData: any) { + return new Promise((resolve, reject) => { + const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => { + if (data.code !== 200) { + this.onError(data.msg || '请求模型服务时发生错误'); + reject(new Error(data.msg)); + return; + } + const { chunk } = data.msg; + + const content = chunk.choices[0]?.delta?.content || ''; + const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0]; + + if (content) { + this.streamingContent.value += content; + } + + if (toolCall) { + if (toolCall.index === 0) { + this.streamingToolCalls.value = [{ + id: toolCall.id, + name: toolCall.function?.name || '', + arguments: toolCall.function?.arguments || '' + }]; + } else { + const currentCall = this.streamingToolCalls.value[toolCall.index]; + if (currentCall) { + if (toolCall.id) currentCall.id = toolCall.id; + if (toolCall.function?.name) currentCall.name = toolCall.function.name; + if (toolCall.function?.arguments) currentCall.arguments += toolCall.function.arguments; + } + } + } + }, { once: false }); + + this.bridge.addCommandListener('llm/chat/completions/done', async data => { + if (data.code !== 200) { + this.onError(data.msg || '模型服务处理完成但返回错误'); + reject(new Error(data.msg)); + return; + } + + if (this.streamingContent.value) { + this.messages.push({ + role: 'assistant', + content: this.streamingContent.value + }); + this.streamingContent.value = ''; + } + + if (this.streamingToolCalls.value.length > 0) { + try { + const toolResult = await this.handleToolCalls(this.streamingToolCalls.value); + + // 将工具执行结果添加到消息列表 + this.streamingToolCalls.value.forEach(tool => { + if (tool.id) { + this.messages.push({ + role: 'tool', + tool_call_id: tool.id, + content: toolResult + }); + } + }); + + // 继续与模型对话 + await this.continueConversation(chatData); + } catch (error) { + reject(error); + return; + } finally { + this.streamingToolCalls.value = []; + } + } + + chunkHandler(); + resolve(); + }, { once: true }); + + this.bridge.postMessage({ + command: 'llm/chat/completions', + data: chatData + }); + }); + } + + public async start(chatData: any) { + this.streamingContent.value = ''; + this.streamingToolCalls.value = []; + + try { + await this.continueConversation(chatData); + } catch (error) { + this.onError(error instanceof Error ? error.message : '未知错误'); + } + } +} \ No newline at end of file diff --git a/service/src/controller/llm.ts b/service/src/controller/llm.ts index cfdae3b..a9cd8d0 100644 --- a/service/src/controller/llm.ts +++ b/service/src/controller/llm.ts @@ -22,6 +22,9 @@ export async function chatCompletionHandler(client: MCPClient | undefined, data: apiKey }); + console.log(tools); + + const stream = await client.chat.completions.create({ model, messages, diff --git a/service/tabs.json b/service/tabs.json index ba232a0..9d0a345 100644 --- a/service/tabs.json +++ b/service/tabs.json @@ -10,11 +10,7 @@ "messages": [ { "role": "user", - "content": "test" - }, - { - "role": "assistant", - "content": "错误: OpenAI API error: 422 Failed to deserialize the JSON body into the target type: tools[0]: missing field `type` at line 31 column 5" + "content": "test add" } ], "settings": {