fex:尝试修复流式显示工具调用不穿插显示的问题
This commit is contained in:
@ -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) => {
|
||||||
|
|||||||
@ -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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交用户回答
|
// 提交用户回答
|
||||||
|
|||||||
Reference in New Issue
Block a user