From 03828cfbb68c2513ca509e7473906bba21cbd260 Mon Sep 17 00:00:00 2001 From: ZLY Date: Sat, 28 Feb 2026 17:18:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=A2=9E=E5=8A=A0=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E6=80=9D=E8=80=83=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- media/webview/script.js | 53 ++++++++++++++++++++++++++++++++++ media/webview/style.css | 54 ++++++++++++++++++++++++++++++++++ src/MessageHandler.ts | 64 ++++++++++++++++++++++++++++++++--------- src/utils/modelApi.ts | 5 ++-- 4 files changed, 160 insertions(+), 16 deletions(-) diff --git a/media/webview/script.js b/media/webview/script.js index 2a28b44..50f808f 100644 --- a/media/webview/script.js +++ b/media/webview/script.js @@ -5,6 +5,7 @@ let streamingMessageContent = ''; // 用于存储当前流式消息的内容 let currentReader = null; // 用于存储当前流的reader,以便可以取消 let isRequestInProgress = false; // 标记是否有请求正在进行 let currentSessionHistory = []; // 存储当前会话历史 +let currentExplanationElement = null; // 存储当前的思考内容元素 const vscode = acquireVsCodeApi(); document.addEventListener('DOMContentLoaded', () => { @@ -180,6 +181,55 @@ document.addEventListener('DOMContentLoaded', () => { chatBox.appendChild(streamingMessageElement); streamingMessageContent = ''; + currentExplanationElement = null; // 重置思考内容元素 + + // 滚动到底部 + chatBox.scrollTop = chatBox.scrollHeight; + } + + // 显示思考内容 + function showExplanation(explanationText) { + if (!streamingMessageElement) return; + + // 创建可折叠的思考内容框 + const explanationContainer = document.createElement('details'); + explanationContainer.className = 'explanation-container'; + explanationContainer.open = true; // 默认展开 + + const summary = document.createElement('summary'); + summary.className = 'explanation-summary'; + summary.textContent = '💭 思考过程'; + + const explanationContent = document.createElement('div'); + explanationContent.className = 'explanation-content'; + + // 使用 marked 渲染 Markdown 内容 + if (marked && marked.parse) { + explanationContent.innerHTML = marked.parse(explanationText); + } else { + explanationContent.textContent = explanationText; + } + + explanationContainer.appendChild(summary); + explanationContainer.appendChild(explanationContent); + + // 将思考内容插入到流式消息内容之前 + const contentDiv = streamingMessageElement.querySelector('.streaming-content'); + if (contentDiv) { + streamingMessageElement.insertBefore(explanationContainer, contentDiv); + } else { + streamingMessageElement.insertBefore(explanationContainer, streamingMessageElement.firstChild); + } + + // 高亮思考内容中的代码块 + const codeBlocks = explanationContent.querySelectorAll('pre code'); + codeBlocks.forEach(block => { + if (hljs) { + hljs.highlightElement(block); + } + }); + + currentExplanationElement = explanationContainer; // 滚动到底部 chatBox.scrollTop = chatBox.scrollHeight; @@ -454,6 +504,9 @@ document.addEventListener('DOMContentLoaded', () => { } else if (message.command === 'startStream') { // 开始流式传输 startStreaming(); + } else if (message.command === 'explanation') { + // 显示思考内容 + showExplanation(message.data); } else if (message.command === 'streamData') { // 处理流式数据 handleStreamData(message.data); diff --git a/media/webview/style.css b/media/webview/style.css index cf2cb30..02b065e 100644 --- a/media/webview/style.css +++ b/media/webview/style.css @@ -547,3 +547,57 @@ code { color: var(--vscode-foreground); font-weight: 500; } + +/* 思考内容框样式 */ +.explanation-container { + margin-bottom: 12px; + border: 1px solid var(--vscode-editorWidget-border); + border-radius: 4px; + background-color: rgba(0, 122, 204, 0.05); + overflow: hidden; +} + +.explanation-summary { + padding: 8px 12px; + cursor: pointer; + font-weight: 600; + font-size: 13px; + color: var(--vscode-foreground); + background-color: rgba(0, 122, 204, 0.1); + border-bottom: 1px solid var(--vscode-editorWidget-border); + user-select: none; + list-style: none; + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +.explanation-summary:hover { + background-color: rgba(0, 122, 204, 0.15); +} + +.explanation-summary::-webkit-details-marker { + display: none; +} + +.explanation-summary::before { + content: '▶'; + display: inline-block; + margin-right: 6px; + transition: transform 0.2s; + font-size: 10px; +} + +.explanation-container[open] .explanation-summary::before { + transform: rotate(90deg); +} + +.explanation-content { + padding: 10px 12px; + font-size: 13px; + line-height: 1.6; + color: var(--vscode-foreground); + white-space: pre-wrap; + word-wrap: break-word; + background-color: var(--vscode-editor-background); +} diff --git a/src/MessageHandler.ts b/src/MessageHandler.ts index 95514a0..7f63fac 100644 --- a/src/MessageHandler.ts +++ b/src/MessageHandler.ts @@ -64,7 +64,7 @@ export class MessageHandler { this.handleRestoreState(message); break; case 'log' : - console.log( ...message.data); + console.log(...message.data); break; } } @@ -99,14 +99,46 @@ export class MessageHandler { if (done) break; const chunk = decoder.decode(value, {stream: true}); - console.log("chunk:",chunk) + console.log("chunk:", chunk) + + // 检查是否包含 event: explanation + if (chunk.includes('event: explanation')) { + let explanationChunk = chunk; + // 直接提取 data: 后面的所有内容 + const explanationIndex = chunk.indexOf('data: '); + explanationChunk = chunk.substring(explanationIndex, explanationChunk.length - 1); + + // 处理截取后的内容 + if (explanationChunk.startsWith('data:')) { + // 移除data:前缀 + explanationChunk = explanationChunk.replace(/^data:/, ''); + // 逐个匹配换行符,当出现两个或更多连续换行符时删除多余的 + explanationChunk = explanationChunk.replace(/^(\n{2,})/, (match) => { + // 删除所有连续的换行符 + return ''; + }); + } + + if (explanationChunk) { + let explanationText = explanationChunk; + console.log("explanationText:", explanationText); + + // 发送 explanation 到前端 + this.provider._postMessage({ + command: 'explanation', + data: explanationText + }); + } + continue; // 跳过这个 chunk 的其他处理 + } + // 检查是否包含event: end事件 let processedChunk = chunk; if (chunk.includes('event: end')) { // 只处理event: end之前的内容 const endIndex = chunk.indexOf('event: end'); processedChunk = chunk.substring(0, endIndex); - + // 处理截取后的内容 if (processedChunk.startsWith('data:')) { // 移除data:前缀 @@ -117,21 +149,21 @@ export class MessageHandler { return ''; }); } - + if (processedChunk) { aiMessage += processedChunk; - + // 发送流式数据到前端 this.provider._postMessage({ command: 'streamData', data: processedChunk }); } - + // 停止继续处理 break; } - + if (chunk.startsWith('data:')) { // 移除data:前缀 processedChunk = chunk.replace(/^data:/, ''); @@ -180,14 +212,14 @@ export class MessageHandler { // 解析AI响应中的多个代码块并生成独立的codeDiff信息 const codeBlocks = aiMessage.match(/```[\s\S]*?```/g) || []; // console.log("codeBlocks:",codeBlocks) - + // 存储所有生成的codeDiff const allCodeDiffs = []; - + for (const block of codeBlocks) { // 生成codeDiff const codeDiff = generateCodeDiff(fileContent, block); - + if (codeDiff) { // @ts-ignore allCodeDiffs.push(codeDiff); @@ -210,10 +242,10 @@ export class MessageHandler { for (let i = 0; i < allCodeDiffs.length; i++) { const codeDiff = allCodeDiffs[i]; const filePath = message.fileContentPath; - + // 创建唯一的工作区变更键 const changeKey = `${filePath}`; - + workspaceChanges[changeKey] = { original: fileContent, @@ -231,13 +263,17 @@ export class MessageHandler { } } } catch (error: any) { - console.log("error:",error) + console.log("error:", error) // 检查是否是由于取消请求导致的中断 if (error.name === 'AbortError' || error.message.includes('cancel') || error.message.includes('中断')) { this.provider._postMessage({command: 'requestCancelled'}); } else if (error.message.includes('terminated') || error.message.includes('other side closed') || error.name === 'TypeError') { // 处理网络连接中断错误,但仍显示已接收的内容 - this.provider._postMessage({command: 'addMessage', role: 'ai', content: '网络连接中断,但以下为已接收的内容:'}); + this.provider._postMessage({ + command: 'addMessage', + role: 'ai', + content: '网络连接中断,但以下为已接收的内容:' + }); // 如果有已接收的内容,也显示出来 // 通知前端流传输完成 this.provider._postMessage({ diff --git a/src/utils/modelApi.ts b/src/utils/modelApi.ts index 0469abe..2050833 100644 --- a/src/utils/modelApi.ts +++ b/src/utils/modelApi.ts @@ -11,7 +11,7 @@ function getApiConfig(context: vscode.ExtensionContext, enableDeepThinking: bool // 根据是否启用深度思考选择不同的端点 const endpoint = enableDeepThinking - ? '/comp/api/v1/chat/completions-stream-cot' + ? '/comp/api/v1/chat/completions-stream-cot-v2' : '/comp/api/v1/chat/completions-stream'; // 如果用户配置了完整URL,优先使用 @@ -107,9 +107,10 @@ export async function callQwenAPI( } }; - // 如果启用深度思考,添加 enable_cot 参数 + // 如果启用深度思考,添加 enable_cot 和 enable_explanation 参数 if (enableDeepThinking) { requestBody.enable_cot = true; + requestBody.enable_explanation = true; } const params = {