增加任务循环
This commit is contained in:
parent
e095af2d8c
commit
0670cf8318
@ -29,6 +29,7 @@
|
|||||||
- [ ] 支持通过大模型进行在线验证
|
- [ ] 支持通过大模型进行在线验证
|
||||||
- [ ] 支持 completion/complete 协议字段
|
- [ ] 支持 completion/complete 协议字段
|
||||||
- [ ] 支持 对用户对应服务器的调试工作内容进行保存
|
- [ ] 支持 对用户对应服务器的调试工作内容进行保存
|
||||||
|
- [ ] 高危操作权限确认
|
||||||
|
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
34
renderer/package-lock.json
generated
34
renderer/package-lock.json
generated
@ -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": {
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
name: tool.name,
|
type: 'function',
|
||||||
description: tool.description || "",
|
function: {
|
||||||
parameters: {
|
name: tool.name,
|
||||||
type: "function",
|
description: tool.description || "",
|
||||||
properties: tool.inputSchema.properties,
|
parameters: tool.inputSchema
|
||||||
required: tool.inputSchema.required
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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 });
|
||||||
|
116
renderer/src/components/main-panel/chat/task-loop.ts
Normal file
116
renderer/src/components/main-panel/chat/task-loop.ts
Normal 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 : '未知错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user