From 6c5d470bad2ac1df1651a718711c2c32dffa7094 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:03:40 +0800 Subject: [PATCH] =?UTF-8?q?fex:=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E6=98=BE=E7=A4=BA=E5=B7=A5=E5=85=B7=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E4=B8=8D=E7=A9=BF=E6=8F=92=E6=98=BE=E7=A4=BA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/dialogService.ts | 114 +++++++++++++++++++-- src/utils/messageHandler.ts | 51 +++++----- src/views/webviewContent.ts | 181 ++++++++++++++++++++++++++++++++-- 3 files changed, 305 insertions(+), 41 deletions(-) diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 7bfa9fa..a106a01 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -9,6 +9,20 @@ import { userInteractionManager } from './userInteraction'; import { getConfig } from '../config/settings'; import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api'; +/** + * 消息段落类型 + */ +export interface MessageSegment { + type: 'text' | 'tool' | 'question'; + content?: string; + toolName?: string; + toolStatus?: 'running' | 'success' | 'error'; + toolResult?: string; + askId?: string; + question?: string; + options?: string[]; +} + /** * 对话回调接口 */ @@ -23,8 +37,8 @@ export interface DialogCallbacks { onToolError?: (toolName: string, error: string) => void; /** 显示问题(ask_user) */ onQuestion?: (askId: string, question: string, options: string[]) => void; - /** 对话完成 */ - onComplete?: () => void; + /** 对话完成,返回所有段落 */ + onComplete?: (segments: MessageSegment[]) => void; /** 错误 */ onError?: (message: string) => void; /** 通知消息 */ @@ -40,12 +54,62 @@ export class DialogSession { private toolContext: ToolExecutorContext; private accumulatedText = ''; private isActive = false; + private segments: MessageSegment[] = []; + private currentTextSegment: MessageSegment | null = null; constructor(extensionPath: string) { this.taskId = generateTaskId(); this.toolContext = createToolExecutorContext(extensionPath); } + /** + * 添加文本到当前文本段落 + */ + private appendText(text: string): void { + if (!this.currentTextSegment) { + this.currentTextSegment = { type: 'text', content: '' }; + this.segments.push(this.currentTextSegment); + } + this.currentTextSegment.content = (this.currentTextSegment.content || '') + text; + } + + /** + * 结束当前文本段落 + */ + private finalizeTextSegment(): void { + this.currentTextSegment = null; + } + + /** + * 添加工具段落 + */ + private addToolSegment(toolName: string, status: 'running' | 'success' | 'error', result?: string): MessageSegment { + this.finalizeTextSegment(); + const segment: MessageSegment = { + type: 'tool', + toolName, + toolStatus: status, + toolResult: result + }; + this.segments.push(segment); + return segment; + } + + /** + * 更新工具段落状态 + */ + private updateToolSegment(toolName: string, status: 'success' | 'error', result?: string): void { + // 找到最后一个匹配的工具段落 + for (let i = this.segments.length - 1; i >= 0; i--) { + const seg = this.segments[i]; + if (seg.type === 'tool' && seg.toolName === toolName && seg.toolStatus === 'running') { + seg.toolStatus = status; + seg.toolResult = result; + break; + } + } + } + /** * 获取任务ID */ @@ -74,6 +138,8 @@ export class DialogSession { this.isActive = true; this.accumulatedText = ''; + this.segments = []; + this.currentTextSegment = null; const config = getConfig(); const request: DialogRequest = { @@ -86,34 +152,64 @@ export class DialogSession { const sseCallbacks: SSECallbacks = { onTextDelta: (data) => { this.accumulatedText += data.text; + this.appendText(data.text); console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length); callbacks.onText?.(this.accumulatedText, true); }, onToolCall: async (data: ToolCallRequest) => { - callbacks.onToolStart?.(data.params.name); + const toolName = data.params.name; + console.log('[DialogSession] onToolCall:', toolName); + // 检查是否已经有相同的工具段落(可能由 onToolStart 添加) + const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); + if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') { + console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName); + } else { + this.addToolSegment(toolName, 'running'); + } + // 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复 try { await executeToolCall(data, this.toolContext); - callbacks.onToolComplete?.(data.params.name, '执行完成'); + this.updateToolSegment(toolName, 'success', '执行完成'); + // 也不调用 callbacks.onToolComplete,避免重复 } catch (error) { const errorMsg = error instanceof Error ? error.message : '未知错误'; - callbacks.onToolError?.(data.params.name, errorMsg); + this.updateToolSegment(toolName, 'error', errorMsg); + callbacks.onToolError?.(toolName, errorMsg); } }, onToolStart: (data) => { + console.log('[DialogSession] onToolStart:', data.tool_name); + // 检查是否已经有相同的工具段落(可能由 onToolCall 添加) + const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); + if (lastToolSegment && lastToolSegment.toolName === data.tool_name && lastToolSegment.toolStatus === 'running') { + console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name); + } else { + this.addToolSegment(data.tool_name, 'running'); + } + console.log('[DialogSession] segments 数量:', this.segments.length); callbacks.onToolStart?.(data.tool_name); }, onToolComplete: (data) => { + this.updateToolSegment(data.tool_name, 'success', data.result); callbacks.onToolComplete?.(data.tool_name, data.result); }, onToolError: (data) => { + this.updateToolSegment(data.tool_name, 'error', data.error); callbacks.onToolError?.(data.tool_name, data.error); }, onAskUser: async (data: AskUserEvent) => { + this.finalizeTextSegment(); + this.segments.push({ + type: 'question', + askId: data.askId, + question: data.question, + options: data.options + }); callbacks.onQuestion?.(data.askId, data.question, data.options); try { await userInteractionManager.handleAskUser(data, this.taskId); @@ -124,11 +220,9 @@ export class DialogSession { onComplete: (data) => { this.isActive = false; - // 发送最终文本(非流式) - if (this.accumulatedText) { - callbacks.onText?.(this.accumulatedText, false); - } - callbacks.onComplete?.(); + this.finalizeTextSegment(); + // 发送所有段落 + callbacks.onComplete?.(this.segments); }, onError: (data) => { diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 150aec5..99b040f 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -116,33 +116,31 @@ async function handleUserMessageWithBackend( currentSession!.sendMessage(text, { onText: (fullText, isStreaming) => { if (isStreaming) { - // 流式时更新状态为"生成中" + // 流式更新消息 panel.webview.postMessage({ - command: "updateStatus", - text: "生成中...", - type: "working", - }); - } else { - // 完成时发送消息并隐藏状态 - console.log('[MessageHandler] 发送最终消息, 文本长度:', fullText.length); - panel.webview.postMessage({ - command: "hideStatus", - }); - panel.webview.postMessage({ - command: "receiveMessage", + command: "updateStreamingMessage", text: fullText, }); } + // 注意:完成时通过 onComplete 发送分段消息 }, onToolStart: (toolName) => { + // 实时显示工具状态 panel.webview.postMessage({ command: "toolStart", toolName, }); + // 同时更新状态栏 + panel.webview.postMessage({ + command: "updateStatus", + text: `正在执行 ${toolName}...`, + type: "working", + }); }, onToolComplete: (toolName, result) => { + // 实时更新工具状态 panel.webview.postMessage({ command: "toolComplete", toolName, @@ -151,6 +149,7 @@ async function handleUserMessageWithBackend( }, onToolError: (toolName, error) => { + // 实时显示工具错误 panel.webview.postMessage({ command: "toolError", toolName, @@ -159,22 +158,30 @@ async function handleUserMessageWithBackend( }, onQuestion: (askId, question, options) => { + // 问题会在分段消息中显示,这里只更新状态栏 panel.webview.postMessage({ - command: "showQuestion", - askId, - question, - options, + command: "updateStatus", + text: "等待用户回答...", + type: "working", }); }, - onComplete: async () => { - // 隐藏加载状态 + onComplete: async (segments) => { + // 隐藏状态栏 panel.webview.postMessage({ - command: "hideLoading", + command: "hideStatus", }); - // 记录到历史(如果有累积文本) - // 注意:实际文本已通过 onText 发送 + // 发送分段消息 + console.log('[MessageHandler] 发送分段消息, 段落数:', segments.length); + console.log('[MessageHandler] segments 内容:', JSON.stringify(segments)); + + const result = await panel.webview.postMessage({ + command: "receiveSegments", + segments: segments, + }); + console.log('[MessageHandler] postMessage 返回值:', result); + resolve(); }, diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 0f64eef..b68ffd3 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -681,6 +681,74 @@ export function getWebviewContent(iconUri?: string): string { display: none; } + /* 分段消息样式 */ + .segmented-message { + padding: 0; + } + .message-segment { + padding: 10px 14px; + } + .segment-text { + line-height: 1.6; + } + .segment-tool { + background: var(--vscode-textBlockQuote-background); + border-radius: 6px; + margin: 8px 0; + padding: 10px 14px; + } + .tool-segment-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + } + .tool-segment-icon { + font-size: 14px; + } + .tool-segment-name { + font-weight: 500; + color: var(--vscode-foreground); + } + .tool-segment-result { + margin-top: 6px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + padding-left: 22px; + } + .segment-tool.tool-success { + border-left: 3px solid var(--vscode-charts-green); + } + .segment-tool.tool-error { + border-left: 3px solid var(--vscode-charts-red); + } + .segment-tool.tool-running { + border-left: 3px solid var(--vscode-charts-blue); + } + .segment-question { + background: var(--vscode-textBlockQuote-background); + border-radius: 6px; + margin: 8px 0; + padding: 12px 14px; + border-left: 3px solid var(--vscode-charts-orange); + } + .question-segment .question-text { + margin-bottom: 8px; + font-weight: 500; + } + .question-segment .question-options { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .question-opt { + padding: 4px 10px; + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border-radius: 4px; + font-size: 12px; + } + /* 状态栏样式 */ .status-bar { display: flex; @@ -842,12 +910,15 @@ export function getWebviewContent(iconUri?: string): string {