增加任务循环

This commit is contained in:
锦恢 2025-04-10 02:36:08 +08:00
parent e095af2d8c
commit 0670cf8318
8 changed files with 222 additions and 23 deletions

View File

@ -29,6 +29,7 @@
- [ ] 支持通过大模型进行在线验证 - [ ] 支持通过大模型进行在线验证
- [ ] 支持 completion/complete 协议字段 - [ ] 支持 completion/complete 协议字段
- [ ] 支持 对用户对应服务器的调试工作内容进行保存 - [ ] 支持 对用户对应服务器的调试工作内容进行保存
- [ ] 高危操作权限确认
## Dev ## Dev

View File

@ -18,6 +18,7 @@
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-babel": "~5.0.0",
@ -31,7 +32,7 @@
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3", "eslint-plugin-vue": "^8.0.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"typescript": "^5.9.0-dev.20250407", "typescript": "^4.4.3",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0" "unplugin-vue-components": "^0.26.0"
} }
@ -2357,6 +2358,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/lodash": {
"version": "4.17.16", "version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
@ -2372,6 +2379,22 @@
"@types/lodash": "*" "@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": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",
@ -12341,17 +12364,16 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.0-dev.20250407", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.0-dev.20250407.tgz", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-JW8/Our6MR+QYS3M134UaLWtEYdVXWzwlbg6rj3fmF9TppADEdaSNiJK90M2wmfSuu5j8Nefk93oSrZF03JkGw==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=4.2.0"
} }
}, },
"node_modules/uc.micro": { "node_modules/uc.micro": {

View File

@ -18,6 +18,7 @@
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-babel": "~5.0.0",
@ -31,7 +32,7 @@
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3", "eslint-plugin-vue": "^8.0.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"typescript": "^5.9.0-dev.20250407", "typescript": "^4.4.3",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0" "unplugin-vue-components": "^0.26.0"
} }

View File

@ -2,8 +2,10 @@ import { ToolItem } from "@/hook/type";
import { ref } from "vue"; import { ref } from "vue";
export interface ChatMessage { export interface ChatMessage {
role: 'user' | 'assistant' | 'system'; role: 'user' | 'assistant' | 'system' | 'tool';
content: string; content: string;
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
} }
// 新增状态和工具数据 // 新增状态和工具数据
@ -27,6 +29,12 @@ export interface ChatStorage {
settings: ChatSetting settings: ChatSetting
} }
export interface ToolCall {
id?: string;
name: string;
arguments: string;
}
export const allTools = ref<ToolItem[]>([]); export const allTools = ref<ToolItem[]>([]);
export function getToolSchema(enableTools: EnableToolItem[]) { export function getToolSchema(enableTools: EnableToolItem[]) {
@ -36,12 +44,11 @@ export function getToolSchema(enableTools: EnableToolItem[]) {
const tool = allTools.value[i]; const tool = allTools.value[i];
toolsSchema.push({ toolsSchema.push({
type: 'function',
function: {
name: tool.name, name: tool.name,
description: tool.description || "", description: tool.description || "",
parameters: { parameters: tool.inputSchema
type: "function",
properties: tool.inputSchema.properties,
required: tool.inputSchema.required
} }
}); });
} }

View File

@ -29,7 +29,10 @@
<div class="message-content" v-else> <div class="message-content" v-else>
<div class="message-role">Tool</div> <div class="message-role">Tool</div>
<div class="message-text"> <div class="message-text">
<span>Tool</span> <div v-if="streamingToolCalls.length > 0">
<span>正在调用工具: {{ streamingToolCalls[0].name }}</span>
</div>
<span v-else>{{ message.content }}</span>
</div> </div>
</div> </div>
@ -74,7 +77,7 @@ import { useI18n } from 'vue-i18n';
import { useMessageBridge } from "@/api/message-bridge"; import { useMessageBridge } from "@/api/message-bridge";
import { ElMessage, ScrollbarInstance } from 'element-plus'; import { ElMessage, ScrollbarInstance } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ChatMessage, ChatStorage, getToolSchema } from './chat'; import { ChatMessage, ChatStorage, getToolSchema, ToolCall } from './chat';
import Setting from './setting.vue'; import Setting from './setting.vue';
import { llmManager, llms } from '@/views/setting/llm'; import { llmManager, llms } from '@/views/setting/llm';
@ -115,7 +118,10 @@ if (!tabStorage.messages) {
} }
const isLoading = ref(false); const isLoading = ref(false);
const streamingContent = ref(''); const streamingContent = ref('');
const streamingToolCalls = ref<ToolCall[]>([]);
const chatContainerRef = ref<any>(null); const chatContainerRef = ref<any>(null);
const messageListRef = ref<any>(null); const messageListRef = ref<any>(null);
@ -227,11 +233,45 @@ const handleSend = () => {
return; return;
} }
const { chunk } = data.msg; const { chunk } = data.msg;
const content = chunk.choices[0]?.delta?.content || ''; const content = chunk.choices[0]?.delta?.content || '';
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
if (content) { if (content) {
streamingContent.value += 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 }); }, { once: false });
@ -249,6 +289,19 @@ const handleSend = () => {
}); });
streamingContent.value = ''; 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; isLoading.value = false;
chunkHandler(); chunkHandler();
}, { once: true }); }, { once: true });

View File

@ -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<string>,
private readonly streamingToolCalls: Ref<ToolCall[]>,
private readonly messages: ChatMessage[],
private readonly onError: (msg: string) => void
) {}
private handleToolCalls(toolCalls: ToolCall[]) {
// 这里预留给调用方实现工具执行逻辑
return Promise.resolve("工具执行结果");
}
private continueConversation(chatData: any) {
return new Promise<void>((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 : '未知错误');
}
}
}

View File

@ -22,6 +22,9 @@ export async function chatCompletionHandler(client: MCPClient | undefined, data:
apiKey apiKey
}); });
console.log(tools);
const stream = await client.chat.completions.create({ const stream = await client.chat.completions.create({
model, model,
messages, messages,

View File

@ -10,11 +10,7 @@
"messages": [ "messages": [
{ {
"role": "user", "role": "user",
"content": "test" "content": "test add"
},
{
"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"
} }
], ],
"settings": { "settings": {