fex:尝试修复流式显示工具调用不穿插显示的问题
This commit is contained in:
@ -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) => {
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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 {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('[WebView] 脚本开始执行');
|
||||
const vscode = acquireVsCodeApi();
|
||||
console.log('[WebView] vscode API 已获取');
|
||||
const messagesEl = document.getElementById('messages');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
const filePathInput = document.getElementById('filePathInput');
|
||||
const fileContentEl = document.getElementById('fileContent');
|
||||
console.log('[WebView] DOM 元素已获取, messagesEl:', !!messagesEl);
|
||||
const errorMessageEl = document.getElementById('errorMessage');
|
||||
const fileEditorSection = document.getElementById('fileEditorSection');
|
||||
const fileEditorTextarea = document.getElementById('fileEditorTextarea');
|
||||
@ -1147,6 +1218,10 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
addMessage(message.text, 'bot');
|
||||
}
|
||||
break;
|
||||
case 'receiveSegments':
|
||||
// 渲染分段消息
|
||||
renderSegments(message.segments);
|
||||
break;
|
||||
case 'updateStreamingMessage':
|
||||
// 流式更新消息
|
||||
updateOrCreateStreamingMessage(message.text);
|
||||
@ -1209,7 +1284,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
messageContent.textContent = text;
|
||||
div.appendChild(messageContent);
|
||||
|
||||
messagesContainer.appendChild(div);
|
||||
messagesEl.appendChild(div);
|
||||
currentStreamingMessage = div;
|
||||
} 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;
|
||||
}
|
||||
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
// 显示加载指示器
|
||||
@ -1261,8 +1336,8 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
</div>
|
||||
<span class="loading-text">\${text}</span>
|
||||
\`;
|
||||
messagesContainer.appendChild(loadingIndicator);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
messagesEl.appendChild(loadingIndicator);
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\\n/g, '<br>');
|
||||
}
|
||||
|
||||
// 添加工具状态消息
|
||||
function addToolStatus(toolName, status, detail) {
|
||||
const statusIcons = {
|
||||
@ -1313,8 +1476,8 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||
\`;
|
||||
messagesContainer.appendChild(div);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
// 显示用户问题
|
||||
@ -1357,8 +1520,8 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
};
|
||||
|
||||
messagesContainer.appendChild(div);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
// 提交用户回答
|
||||
|
||||
Reference in New Issue
Block a user