fex:尝试修复流式显示工具调用不穿插显示的问题

This commit is contained in:
XiaoFeng
2025-12-17 10:03:40 +08:00
parent c21ad95963
commit 6c5d470bad
3 changed files with 305 additions and 41 deletions

View File

@ -9,6 +9,20 @@ import { userInteractionManager } from './userInteraction';
import { getConfig } from '../config/settings'; import { getConfig } from '../config/settings';
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api'; 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; onToolError?: (toolName: string, error: string) => void;
/** 显示问题ask_user */ /** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void; onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 对话完成 */ /** 对话完成,返回所有段落 */
onComplete?: () => void; onComplete?: (segments: MessageSegment[]) => void;
/** 错误 */ /** 错误 */
onError?: (message: string) => void; onError?: (message: string) => void;
/** 通知消息 */ /** 通知消息 */
@ -40,12 +54,62 @@ export class DialogSession {
private toolContext: ToolExecutorContext; private toolContext: ToolExecutorContext;
private accumulatedText = ''; private accumulatedText = '';
private isActive = false; private isActive = false;
private segments: MessageSegment[] = [];
private currentTextSegment: MessageSegment | null = null;
constructor(extensionPath: string) { constructor(extensionPath: string) {
this.taskId = generateTaskId(); this.taskId = generateTaskId();
this.toolContext = createToolExecutorContext(extensionPath); 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 * 获取任务ID
*/ */
@ -74,6 +138,8 @@ export class DialogSession {
this.isActive = true; this.isActive = true;
this.accumulatedText = ''; this.accumulatedText = '';
this.segments = [];
this.currentTextSegment = null;
const config = getConfig(); const config = getConfig();
const request: DialogRequest = { const request: DialogRequest = {
@ -86,34 +152,64 @@ export class DialogSession {
const sseCallbacks: SSECallbacks = { const sseCallbacks: SSECallbacks = {
onTextDelta: (data) => { onTextDelta: (data) => {
this.accumulatedText += data.text; this.accumulatedText += data.text;
this.appendText(data.text);
console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length); console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length);
callbacks.onText?.(this.accumulatedText, true); callbacks.onText?.(this.accumulatedText, true);
}, },
onToolCall: async (data: ToolCallRequest) => { 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 { try {
await executeToolCall(data, this.toolContext); await executeToolCall(data, this.toolContext);
callbacks.onToolComplete?.(data.params.name, '执行完成'); this.updateToolSegment(toolName, 'success', '执行完成');
// 也不调用 callbacks.onToolComplete避免重复
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误'; const errorMsg = error instanceof Error ? error.message : '未知错误';
callbacks.onToolError?.(data.params.name, errorMsg); this.updateToolSegment(toolName, 'error', errorMsg);
callbacks.onToolError?.(toolName, errorMsg);
} }
}, },
onToolStart: (data) => { 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); callbacks.onToolStart?.(data.tool_name);
}, },
onToolComplete: (data) => { onToolComplete: (data) => {
this.updateToolSegment(data.tool_name, 'success', data.result);
callbacks.onToolComplete?.(data.tool_name, data.result); callbacks.onToolComplete?.(data.tool_name, data.result);
}, },
onToolError: (data) => { onToolError: (data) => {
this.updateToolSegment(data.tool_name, 'error', data.error);
callbacks.onToolError?.(data.tool_name, data.error); callbacks.onToolError?.(data.tool_name, data.error);
}, },
onAskUser: async (data: AskUserEvent) => { 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); callbacks.onQuestion?.(data.askId, data.question, data.options);
try { try {
await userInteractionManager.handleAskUser(data, this.taskId); await userInteractionManager.handleAskUser(data, this.taskId);
@ -124,11 +220,9 @@ export class DialogSession {
onComplete: (data) => { onComplete: (data) => {
this.isActive = false; this.isActive = false;
// 发送最终文本(非流式) this.finalizeTextSegment();
if (this.accumulatedText) { // 发送所有段落
callbacks.onText?.(this.accumulatedText, false); callbacks.onComplete?.(this.segments);
}
callbacks.onComplete?.();
}, },
onError: (data) => { onError: (data) => {

View File

@ -116,33 +116,31 @@ async function handleUserMessageWithBackend(
currentSession!.sendMessage(text, { currentSession!.sendMessage(text, {
onText: (fullText, isStreaming) => { onText: (fullText, isStreaming) => {
if (isStreaming) { if (isStreaming) {
// 流式更新状态为"生成中" // 流式更新消息
panel.webview.postMessage({ panel.webview.postMessage({
command: "updateStatus", command: "updateStreamingMessage",
text: "生成中...",
type: "working",
});
} else {
// 完成时发送消息并隐藏状态
console.log('[MessageHandler] 发送最终消息, 文本长度:', fullText.length);
panel.webview.postMessage({
command: "hideStatus",
});
panel.webview.postMessage({
command: "receiveMessage",
text: fullText, text: fullText,
}); });
} }
// 注意:完成时通过 onComplete 发送分段消息
}, },
onToolStart: (toolName) => { onToolStart: (toolName) => {
// 实时显示工具状态
panel.webview.postMessage({ panel.webview.postMessage({
command: "toolStart", command: "toolStart",
toolName, toolName,
}); });
// 同时更新状态栏
panel.webview.postMessage({
command: "updateStatus",
text: `正在执行 ${toolName}...`,
type: "working",
});
}, },
onToolComplete: (toolName, result) => { onToolComplete: (toolName, result) => {
// 实时更新工具状态
panel.webview.postMessage({ panel.webview.postMessage({
command: "toolComplete", command: "toolComplete",
toolName, toolName,
@ -151,6 +149,7 @@ async function handleUserMessageWithBackend(
}, },
onToolError: (toolName, error) => { onToolError: (toolName, error) => {
// 实时显示工具错误
panel.webview.postMessage({ panel.webview.postMessage({
command: "toolError", command: "toolError",
toolName, toolName,
@ -159,22 +158,30 @@ async function handleUserMessageWithBackend(
}, },
onQuestion: (askId, question, options) => { onQuestion: (askId, question, options) => {
// 问题会在分段消息中显示,这里只更新状态栏
panel.webview.postMessage({ panel.webview.postMessage({
command: "showQuestion", command: "updateStatus",
askId, text: "等待用户回答...",
question, type: "working",
options,
}); });
}, },
onComplete: async () => { onComplete: async (segments) => {
// 隐藏加载状态 // 隐藏状态
panel.webview.postMessage({ 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(); resolve();
}, },

View File

@ -681,6 +681,74 @@ export function getWebviewContent(iconUri?: string): string {
display: none; 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 { .status-bar {
display: flex; display: flex;
@ -842,12 +910,15 @@ export function getWebviewContent(iconUri?: string): string {
</div> </div>
<script> <script>
console.log('[WebView] 脚本开始执行');
const vscode = acquireVsCodeApi(); const vscode = acquireVsCodeApi();
console.log('[WebView] vscode API 已获取');
const messagesEl = document.getElementById('messages'); const messagesEl = document.getElementById('messages');
const messageInput = document.getElementById('messageInput'); const messageInput = document.getElementById('messageInput');
const modeSelect = document.getElementById('modeSelect'); const modeSelect = document.getElementById('modeSelect');
const filePathInput = document.getElementById('filePathInput'); const filePathInput = document.getElementById('filePathInput');
const fileContentEl = document.getElementById('fileContent'); const fileContentEl = document.getElementById('fileContent');
console.log('[WebView] DOM 元素已获取, messagesEl:', !!messagesEl);
const errorMessageEl = document.getElementById('errorMessage'); const errorMessageEl = document.getElementById('errorMessage');
const fileEditorSection = document.getElementById('fileEditorSection'); const fileEditorSection = document.getElementById('fileEditorSection');
const fileEditorTextarea = document.getElementById('fileEditorTextarea'); const fileEditorTextarea = document.getElementById('fileEditorTextarea');
@ -1147,6 +1218,10 @@ export function getWebviewContent(iconUri?: string): string {
addMessage(message.text, 'bot'); addMessage(message.text, 'bot');
} }
break; break;
case 'receiveSegments':
// 渲染分段消息
renderSegments(message.segments);
break;
case 'updateStreamingMessage': case 'updateStreamingMessage':
// 流式更新消息 // 流式更新消息
updateOrCreateStreamingMessage(message.text); updateOrCreateStreamingMessage(message.text);
@ -1209,7 +1284,7 @@ export function getWebviewContent(iconUri?: string): string {
messageContent.textContent = text; messageContent.textContent = text;
div.appendChild(messageContent); div.appendChild(messageContent);
messagesContainer.appendChild(div); messagesEl.appendChild(div);
currentStreamingMessage = div; currentStreamingMessage = div;
} else { } else {
// 更新现有消息内容 // 更新现有消息内容
@ -1220,7 +1295,7 @@ export function getWebviewContent(iconUri?: string): string {
} }
// 滚动到底部 // 滚动到底部
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// 完成流式消息 // 完成流式消息
@ -1246,7 +1321,7 @@ export function getWebviewContent(iconUri?: string): string {
currentStreamingMessage = null; currentStreamingMessage = null;
} }
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// 显示加载指示器 // 显示加载指示器
@ -1261,8 +1336,8 @@ export function getWebviewContent(iconUri?: string): string {
</div> </div>
<span class="loading-text">\${text}</span> <span class="loading-text">\${text}</span>
\`; \`;
messagesContainer.appendChild(loadingIndicator); messagesEl.appendChild(loadingIndicator);
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// 隐藏加载指示器 // 隐藏加载指示器
@ -1292,6 +1367,94 @@ export function getWebviewContent(iconUri?: string): string {
} }
} }
// 渲染分段消息
function renderSegments(segments) {
console.log('[WebView] renderSegments 被调用, segments:', segments);
if (!segments || segments.length === 0) {
console.log('[WebView] segments 为空,跳过渲染');
return;
}
// 移除流式消息(如果有)
if (currentStreamingMessage) {
console.log('[WebView] 移除流式消息');
currentStreamingMessage.remove();
currentStreamingMessage = null;
}
// 移除所有工具状态消息(因为会在分段中显示)
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
toolStatuses.forEach(el => {
console.log('[WebView] 移除工具状态消息:', el.className);
el.remove();
});
// 创建消息容器
const container = document.createElement('div');
container.className = 'message bot-message segmented-message';
segments.forEach((segment, index) => {
const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type;
if (segment.type === 'text' && segment.content) {
segmentDiv.innerHTML = formatText(segment.content);
} else if (segment.type === 'tool') {
const statusIcon = segment.toolStatus === 'success' ? '✅' :
segment.toolStatus === 'error' ? '❌' : '🔧';
const statusClass = 'tool-' + (segment.toolStatus || 'running');
segmentDiv.className += ' ' + statusClass;
segmentDiv.innerHTML = \`
<div class="tool-segment-header">
<span class="tool-segment-icon">\${statusIcon}</span>
<span class="tool-segment-name">\${segment.toolName || '工具'}</span>
</div>
\${segment.toolResult ? \`<div class="tool-segment-result">\${segment.toolResult}</div>\` : ''}
\`;
} else if (segment.type === 'question') {
segmentDiv.innerHTML = \`
<div class="question-segment">
<div class="question-text">\${segment.question || ''}</div>
<div class="question-options">
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
</div>
</div>
\`;
}
container.appendChild(segmentDiv);
});
// 添加操作按钮
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn';
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
copyBtn.onclick = () => {
const textContent = segments
.filter(s => s.type === 'text' && s.content)
.map(s => s.content)
.join('\\n');
copyMessage(textContent, copyBtn);
};
actionsDiv.appendChild(copyBtn);
container.appendChild(actionsDiv);
messagesEl.appendChild(container);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// 格式化文本(简单的换行处理)
function formatText(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\\n/g, '<br>');
}
// 添加工具状态消息 // 添加工具状态消息
function addToolStatus(toolName, status, detail) { function addToolStatus(toolName, status, detail) {
const statusIcons = { const statusIcons = {
@ -1313,8 +1476,8 @@ export function getWebviewContent(iconUri?: string): string {
<span class="tool-status-text">\${statusTexts[status]}</span> <span class="tool-status-text">\${statusTexts[status]}</span>
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''} \${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
\`; \`;
messagesContainer.appendChild(div); messagesEl.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// 显示用户问题 // 显示用户问题
@ -1357,8 +1520,8 @@ export function getWebviewContent(iconUri?: string): string {
} }
}; };
messagesContainer.appendChild(div); messagesEl.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesEl.scrollTop = messagesEl.scrollHeight;
} }
// 提交用户回答 // 提交用户回答