diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d016fb..183abd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log + +## [main] 0.0.8 +- 大模型 API 测试时更加完整的报错 +- 修复 0.0.7 引入的bug:修改对话无法发出 +- 修复 bug:富文本编辑器粘贴文本会带样式 +- 修复 bug:富文本编辑器发送前缀为空的字符会全部为空 +- 修复 bug:流式传输进行 function calling 时,多工具的索引串流导致的 JSON Schema 反序列化失败 +- 修复 bug:大模型返回大量重复错误信息 + ## [main] 0.0.7 - 优化页面布局,使得调试窗口可以显示更多内容 - 扩大默认的上下文长度 10 -> 20 diff --git a/package.json b/package.json index a869533..0b1f844 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openmcp", "displayName": "OpenMCP", "description": "An all in one MCP Client/TestTool", - "version": "0.0.7", + "version": "0.0.8", "publisher": "kirigaya", "author": { "name": "kirigaya", diff --git a/renderer/public/iconfont.css b/renderer/public/iconfont.css index 5c04cb3..feacc0c 100644 --- a/renderer/public/iconfont.css +++ b/renderer/public/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4870215 */ - src: url('iconfont.woff2?t=1746529081655') format('woff2'), - url('iconfont.woff?t=1746529081655') format('woff'), - url('iconfont.ttf?t=1746529081655') format('truetype'); + src: url('iconfont.woff2?t=1746703816245') format('woff2'), + url('iconfont.woff?t=1746703816245') format('woff'), + url('iconfont.ttf?t=1746703816245') format('truetype'); } .iconfont { @@ -13,6 +13,10 @@ -moz-osx-font-smoothing: grayscale; } +.icon-waiting:before { + content: "\e6d0"; +} + .icon-timeout:before { content: "\edf5"; } diff --git a/renderer/public/iconfont.woff2 b/renderer/public/iconfont.woff2 index 1ba9948..45332ae 100644 Binary files a/renderer/public/iconfont.woff2 and b/renderer/public/iconfont.woff2 differ diff --git a/renderer/src/components/main-panel/chat/chat-box/index.vue b/renderer/src/components/main-panel/chat/chat-box/index.vue index 64c7313..ce4c1af 100644 --- a/renderer/src/components/main-panel/chat/chat-box/index.vue +++ b/renderer/src/components/main-panel/chat/chat-box/index.vue @@ -61,6 +61,9 @@ const streamingContent = inject('streamingContent') as Ref; const streamingToolCalls = inject('streamingToolCalls') as Ref; const scrollToBottom = inject('scrollToBottom') as () => Promise; const updateScrollHeight = inject('updateScrollHeight') as () => void; +const chatContext = inject('chatContext') as any; + +chatContext.handleSend = handleSend; function handleSend(newMessage?: string) { // 将富文本信息转换成纯文本信息 @@ -77,11 +80,7 @@ function handleSend(newMessage?: string) { loop.registerOnError((error) => { - ElMessage({ - message: error.msg, - type: 'error', - duration: 3000 - }); + ElMessage.error(error.msg); if (error.state === MessageState.ReceiveChunkError) { tabStorage.messages.push({ @@ -125,8 +124,6 @@ function handleAbort() { } } -provide('handleSend', handleSend); - onMounted(() => { updateScrollHeight(); diff --git a/renderer/src/components/main-panel/chat/chat-box/rich-textarea.vue b/renderer/src/components/main-panel/chat/chat-box/rich-textarea.vue index b0913cb..c689b58 100644 --- a/renderer/src/components/main-panel/chat/chat-box/rich-textarea.vue +++ b/renderer/src/components/main-panel/chat/chat-box/rich-textarea.vue @@ -10,6 +10,7 @@ class="rich-editor" :placeholder="placeholder" @input="handleInput" + @paste="handlePaste" @keydown.backspace="handleBackspace" @keydown.enter="handleKeydown" @compositionstart="handleCompositionStart" @@ -171,6 +172,32 @@ function handleKeydown(event: KeyboardEvent) { } } +function handlePaste(event: ClipboardEvent) { + event.preventDefault(); // 阻止默认粘贴行为 + const clipboardData = event.clipboardData; + if (clipboardData) { + const pastedText = clipboardData.getData('text/plain'); + const editorElement = editor.value; + if (editorElement instanceof HTMLDivElement) { + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + const textNode = document.createTextNode(pastedText); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } + } + } + + if (editor.value) { + editor.value.dispatchEvent(new Event('input')); + } +} + function handleCompositionStart() { isComposing.value = true; } diff --git a/renderer/src/components/main-panel/chat/core/task-loop.ts b/renderer/src/components/main-panel/chat/core/task-loop.ts index 87bb65b..0d4138c 100644 --- a/renderer/src/components/main-panel/chat/core/task-loop.ts +++ b/renderer/src/components/main-panel/chat/core/task-loop.ts @@ -12,6 +12,7 @@ export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string }; interface TaskLoopOptions { maxEpochs: number; + maxJsonParseRetry: number; } interface IErrorMssage { @@ -19,6 +20,10 @@ interface IErrorMssage { msg: string } +interface IDoConversationResult { + stop: boolean; +} + /** * @description 对任务循环进行的抽象封装 */ @@ -34,15 +39,19 @@ export class TaskLoop { private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {}, private onDone: () => void = () => {}, private onEpoch: () => void = () => {}, - private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20 }, + private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20, maxJsonParseRetry: 3 }, ) { } private async handleToolCalls(toolCalls: ToolCall[]) { // TODO: 调用多个工具并返回调用结果? + const toolCall = toolCalls[0]; + console.log('debug toolcall'); + console.log(toolCalls); + let toolName: string; let toolArgs: Record; @@ -131,15 +140,15 @@ export class TaskLoop { if (currentCall === undefined) { // 新的工具调用开始 - this.streamingToolCalls.value = [{ + this.streamingToolCalls.value[toolCall.index] = { id: toolCall.id, - index: 0, + index: toolCall.index, type: 'function', function: { name: toolCall.function?.name || '', arguments: toolCall.function?.arguments || '' } - }]; + }; } else { // 累积现有工具调用的信息 if (currentCall) { @@ -150,7 +159,7 @@ export class TaskLoop { currentCall.function.name = toolCall.function.name; } if (toolCall.function?.arguments) { - currentCall.function.arguments += toolCall.function.arguments; + currentCall.function.arguments += toolCall.function.arguments; } } } @@ -167,16 +176,9 @@ export class TaskLoop { private doConversation(chatData: ChatCompletionCreateParamsBase) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => { - if (data.code !== 200) { - this.onError({ - state: MessageState.ReceiveChunkError, - msg: data.msg || '请求模型服务时发生错误' - }); - resolve(); - return; - } + // data.code 一定为 200,否则不会走这个 route const { chunk } = data.msg as { chunk: ChatCompletionChunk }; // 处理增量的 content 和 tool_calls @@ -187,11 +189,34 @@ export class TaskLoop { this.onChunk(chunk); }, { once: false }); - this.bridge.addCommandListener('llm/chat/completions/done', data => { + const doneHandler = this.bridge.addCommandListener('llm/chat/completions/done', data => { this.onDone(); chunkHandler(); + errorHandler(); + + resolve({ + stop: false + }); + }, { once: true }); + + console.log('register error handler'); + + const errorHandler = this.bridge.addCommandListener('llm/chat/completions/error', data => { + + console.log('enter error report'); + + this.onError({ + state: MessageState.ReceiveChunkError, + msg: data.msg || '请求模型服务时发生错误' + }); + + chunkHandler(); + doneHandler(); + + resolve({ + stop: true + }); - resolve(); }, { once: true }); this.bridge.postMessage({ @@ -273,6 +298,10 @@ export class TaskLoop { this.onEpoch = handler; } + public setMaxEpochs(maxEpochs: number) { + this.taskOptions.maxEpochs = maxEpochs; + } + /** * @description 开启循环,异步更新 DOM */ @@ -288,6 +317,8 @@ export class TaskLoop { } }); + let jsonParseErrorRetryCount = 0; + for (let i = 0; i < this.taskOptions.maxEpochs; ++ i) { this.onEpoch(); @@ -308,7 +339,10 @@ export class TaskLoop { this.currentChatId = chatData.id!; // 发送请求 - await this.doConversation(chatData); + const doConverationResult = await this.doConversation(chatData); + + console.log(doConverationResult); + // 如果存在需要调度的工具 if (this.streamingToolCalls.value.length > 0) { @@ -333,11 +367,25 @@ export class TaskLoop { if (toolCallResult.state === MessageState.ParseJsonError) { // 如果是因为解析 JSON 错误,则重新开始 tabStorage.messages.pop(); - redLog('解析 JSON 错误 ' + this.streamingToolCalls.value[0]?.function?.arguments); - continue; - } + jsonParseErrorRetryCount ++; - if (toolCallResult.state === MessageState.Success) { + redLog('解析 JSON 错误 ' + this.streamingToolCalls.value[0]?.function?.arguments); + + // 如果因为 JSON 错误而失败太多,就只能中断了 + if (jsonParseErrorRetryCount >= this.taskOptions.maxJsonParseRetry) { + tabStorage.messages.push({ + role: 'assistant', + content: `解析 JSON 错误,无法继续调用工具 (累计错误次数 ${this.taskOptions.maxJsonParseRetry})`, + extraInfo: { + created: Date.now(), + state: toolCallResult.state, + serverName: llms[llmManager.currentModelIndex].id || 'unknown', + usage: undefined + } + }); + break; + } + } else if (toolCallResult.state === MessageState.Success) { const toolCall = this.streamingToolCalls.value[0]; tabStorage.messages.push({ @@ -351,10 +399,7 @@ export class TaskLoop { usage: this.completionUsage } }); - } - - - if (toolCallResult.state === MessageState.ToolCall) { + } else if (toolCallResult.state === MessageState.ToolCall) { const toolCall = this.streamingToolCalls.value[0]; tabStorage.messages.push({ @@ -385,7 +430,11 @@ export class TaskLoop { } else { // 一些提示 + break; + } + // 回答聚合完成后根据 stop 来决定是否提前中断 + if (doConverationResult.stop) { break; } } diff --git a/renderer/src/components/main-panel/chat/core/usage.ts b/renderer/src/components/main-panel/chat/core/usage.ts index a6b51d2..5b0237f 100644 --- a/renderer/src/components/main-panel/chat/core/usage.ts +++ b/renderer/src/components/main-panel/chat/core/usage.ts @@ -31,6 +31,18 @@ export function makeUsageStatistic(extraInfo: IExtraInfo): UsageStatistic | unde total: usage.prompt_tokens + usage.completion_tokens, cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10, } + + + default: + if (usage.prompt_tokens && usage.completion_tokens) { + 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, + } + } + return undefined; } return undefined; diff --git a/renderer/src/components/main-panel/chat/index.vue b/renderer/src/components/main-panel/chat/index.vue index 72f7dff..88bdd82 100644 --- a/renderer/src/components/main-panel/chat/index.vue +++ b/renderer/src/components/main-panel/chat/index.vue @@ -165,6 +165,11 @@ provide('streamingToolCalls', streamingToolCalls); provide('isLoading', isLoading); provide('autoScroll', autoScroll); +const chatContext = { + handleSend: undefined +}; +provide('chatContext', chatContext); + // 修改 scrollToBottom 方法 async function scrollToBottom() { if (!scrollbarRef.value || !messageListRef.value) return; diff --git a/renderer/src/components/main-panel/chat/message/assistant.vue b/renderer/src/components/main-panel/chat/message/assistant.vue index 792bd7e..5ffe68c 100644 --- a/renderer/src/components/main-panel/chat/message/assistant.vue +++ b/renderer/src/components/main-panel/chat/message/assistant.vue @@ -1,13 +1,13 @@