Compare commits
6 Commits
4918399325
...
7c1f1fae07
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c1f1fae07 | |||
| c61e29a41f | |||
| 703912bb5f | |||
| 8ad6a48e8f | |||
| ba75541dd6 | |||
| f87adab7be |
21
package.json
21
package.json
@ -91,6 +91,26 @@
|
||||
"type": "webview"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "IC Coder",
|
||||
"properties": {
|
||||
"icCoder.backendUrl": {
|
||||
"type": "string",
|
||||
"default": "http://localhost:2233",
|
||||
"description": "后端服务地址"
|
||||
},
|
||||
"icCoder.timeout": {
|
||||
"type": "number",
|
||||
"default": 60000,
|
||||
"description": "请求超时时间(毫秒)"
|
||||
},
|
||||
"icCoder.userId": {
|
||||
"type": "string",
|
||||
"default": "default-user",
|
||||
"description": "用户ID(临时配置)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@ -125,6 +145,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@wavedrom/doppler": "^1.14.0",
|
||||
"eventsource-parser": "^3.0.6",
|
||||
"iconv-lite": "^0.7.1",
|
||||
"onml": "^2.1.0",
|
||||
"style-mod": "^4.1.3",
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
||||
'@wavedrom/doppler':
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0
|
||||
eventsource-parser:
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
iconv-lite:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@ -662,6 +665,10 @@ packages:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
eventsource-parser@3.0.6:
|
||||
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@ -2119,6 +2126,8 @@ snapshots:
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
46
src/config/settings.ts
Normal file
46
src/config/settings.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 配置管理
|
||||
* 从 VSCode 设置读取配置项
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/** 配置项接口 */
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
backendUrl: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 用户ID(临时使用,后续对接认证) */
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG: IccoderConfig = {
|
||||
backendUrl: 'http://localhost:8080',
|
||||
timeout: 60000,
|
||||
userId: 'default-user'
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
*/
|
||||
export function getConfig(): IccoderConfig {
|
||||
const config = vscode.workspace.getConfiguration('icCoder');
|
||||
|
||||
return {
|
||||
backendUrl: config.get<string>('backendUrl', DEFAULT_CONFIG.backendUrl),
|
||||
timeout: config.get<number>('timeout', DEFAULT_CONFIG.timeout),
|
||||
userId: config.get<string>('userId', DEFAULT_CONFIG.userId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后端 API 地址
|
||||
*/
|
||||
export function getApiUrl(path: string): string {
|
||||
const { backendUrl } = getConfig();
|
||||
// 确保 URL 格式正确
|
||||
const baseUrl = backendUrl.endsWith('/') ? backendUrl.slice(0, -1) : backendUrl;
|
||||
const apiPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${baseUrl}${apiPath}`;
|
||||
}
|
||||
@ -6,7 +6,9 @@ import {
|
||||
handleReadFile,
|
||||
handleUpdateFile,
|
||||
handleRenameFile,
|
||||
handleReplaceInFile
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog
|
||||
} from "../utils/messageHandler";
|
||||
|
||||
/**
|
||||
@ -61,6 +63,14 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(message.askId, message.selected, message.customInput);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
|
||||
154
src/services/apiClient.ts
Normal file
154
src/services/apiClient.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* API 客户端
|
||||
* 封装与后端的 HTTP 通信
|
||||
*/
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
*/
|
||||
interface RequestOptions {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
const url = new URL(getApiUrl(path));
|
||||
const { timeout } = getConfig();
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
timeout: options.timeout || timeout
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as T);
|
||||
} else {
|
||||
reject(new Error(json.error || json.message || `HTTP ${res.statusCode}`));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error(`解析响应失败: ${data}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工具执行结果
|
||||
* POST /api/tool/result
|
||||
*/
|
||||
export async function submitToolResult(result: ToolCallResult): Promise<ToolResultResponse> {
|
||||
console.log(`[API] 提交工具结果: callId=${result.id}`);
|
||||
return request<ToolResultResponse>('/api/tool/result', {
|
||||
method: 'POST',
|
||||
body: result
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交用户回答
|
||||
* POST /api/task/answer
|
||||
*/
|
||||
export async function submitAnswer(answer: AnswerRequest): Promise<AnswerResponse> {
|
||||
console.log(`[API] 提交用户回答: askId=${answer.askId}`);
|
||||
return request<AnswerResponse>('/api/task/answer', {
|
||||
method: 'POST',
|
||||
body: answer
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* GET /api/dialog/health
|
||||
*/
|
||||
export async function healthCheck(): Promise<{ status: string }> {
|
||||
return request<{ status: string }>('/api/dialog/health', {
|
||||
method: 'GET',
|
||||
timeout: 5000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功的工具结果
|
||||
*/
|
||||
export function createSuccessResult(id: number, text: string): ToolCallResult {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
content: [{ type: 'text', text }],
|
||||
isError: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建业务错误的工具结果(如编译失败)
|
||||
*/
|
||||
export function createBusinessErrorResult(id: number, errorMessage: string): ToolCallResult {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
content: [{ type: 'text', text: errorMessage }],
|
||||
isError: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统错误的工具结果
|
||||
*/
|
||||
export function createSystemErrorResult(id: number, code: number, message: string): ToolCallResult {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code, message }
|
||||
};
|
||||
}
|
||||
224
src/services/dialogService.ts
Normal file
224
src/services/dialogService.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 对话服务
|
||||
* 整合 SSE 通信、工具执行、用户交互
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler';
|
||||
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
||||
import { userInteractionManager } from './userInteraction';
|
||||
import { getConfig } from '../config/settings';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api';
|
||||
|
||||
/**
|
||||
* 对话回调接口
|
||||
*/
|
||||
export interface DialogCallbacks {
|
||||
/** 收到文本(可能多次调用,流式) */
|
||||
onText?: (text: string, isStreaming: boolean) => void;
|
||||
/** 工具开始执行 */
|
||||
onToolStart?: (toolName: string) => void;
|
||||
/** 工具执行完成 */
|
||||
onToolComplete?: (toolName: string, result: string) => void;
|
||||
/** 工具执行错误 */
|
||||
onToolError?: (toolName: string, error: string) => void;
|
||||
/** 显示问题(ask_user) */
|
||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||||
/** 对话完成 */
|
||||
onComplete?: () => void;
|
||||
/** 错误 */
|
||||
onError?: (message: string) => void;
|
||||
/** 通知消息 */
|
||||
onNotification?: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话会话
|
||||
*/
|
||||
export class DialogSession {
|
||||
private taskId: string;
|
||||
private sseController: SSEController | null = null;
|
||||
private toolContext: ToolExecutorContext;
|
||||
private accumulatedText = '';
|
||||
private isActive = false;
|
||||
|
||||
constructor(extensionPath: string) {
|
||||
this.taskId = generateTaskId();
|
||||
this.toolContext = createToolExecutorContext(extensionPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务ID
|
||||
*/
|
||||
getTaskId(): string {
|
||||
return this.taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否活跃
|
||||
*/
|
||||
get active(): boolean {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息并开始流式对话
|
||||
*/
|
||||
async sendMessage(
|
||||
message: string,
|
||||
callbacks: DialogCallbacks
|
||||
): Promise<void> {
|
||||
if (this.isActive) {
|
||||
callbacks.onError?.('当前有对话正在进行中');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActive = true;
|
||||
this.accumulatedText = '';
|
||||
|
||||
const config = getConfig();
|
||||
const request: DialogRequest = {
|
||||
taskId: this.taskId,
|
||||
message,
|
||||
userId: config.userId,
|
||||
toolMode: 'AGENT'
|
||||
};
|
||||
|
||||
const sseCallbacks: SSECallbacks = {
|
||||
onTextDelta: (data) => {
|
||||
this.accumulatedText += data.text;
|
||||
console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length);
|
||||
callbacks.onText?.(this.accumulatedText, true);
|
||||
},
|
||||
|
||||
onToolCall: async (data: ToolCallRequest) => {
|
||||
callbacks.onToolStart?.(data.params.name);
|
||||
try {
|
||||
await executeToolCall(data, this.toolContext);
|
||||
callbacks.onToolComplete?.(data.params.name, '执行完成');
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '未知错误';
|
||||
callbacks.onToolError?.(data.params.name, errorMsg);
|
||||
}
|
||||
},
|
||||
|
||||
onToolStart: (data) => {
|
||||
callbacks.onToolStart?.(data.tool_name);
|
||||
},
|
||||
|
||||
onToolComplete: (data) => {
|
||||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||||
},
|
||||
|
||||
onToolError: (data) => {
|
||||
callbacks.onToolError?.(data.tool_name, data.error);
|
||||
},
|
||||
|
||||
onAskUser: async (data: AskUserEvent) => {
|
||||
callbacks.onQuestion?.(data.askId, data.question, data.options);
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(data, this.taskId);
|
||||
} catch (error) {
|
||||
console.error('[DialogSession] 处理用户问题失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onComplete: (data) => {
|
||||
this.isActive = false;
|
||||
// 发送最终文本(非流式)
|
||||
if (this.accumulatedText) {
|
||||
callbacks.onText?.(this.accumulatedText, false);
|
||||
}
|
||||
callbacks.onComplete?.();
|
||||
},
|
||||
|
||||
onError: (data) => {
|
||||
this.isActive = false;
|
||||
callbacks.onError?.(data.message);
|
||||
},
|
||||
|
||||
onWarning: (data) => {
|
||||
callbacks.onNotification?.(`⚠️ ${data.message}`);
|
||||
},
|
||||
|
||||
onNotification: (data) => {
|
||||
callbacks.onNotification?.(data.message);
|
||||
},
|
||||
|
||||
onOpen: () => {
|
||||
console.log('[DialogSession] SSE 连接已建立');
|
||||
},
|
||||
|
||||
onClose: () => {
|
||||
console.log('[DialogSession] SSE 连接已关闭');
|
||||
this.isActive = false;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.sseController = await startStreamDialog(request, sseCallbacks);
|
||||
} catch (error) {
|
||||
this.isActive = false;
|
||||
const errorMsg = error instanceof Error ? error.message : '连接失败';
|
||||
callbacks.onError?.(errorMsg);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止当前对话
|
||||
*/
|
||||
abort(): void {
|
||||
if (this.sseController) {
|
||||
this.sseController.abort();
|
||||
this.sseController = null;
|
||||
}
|
||||
this.isActive = false;
|
||||
userInteractionManager.cancelAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交用户回答
|
||||
*/
|
||||
async submitAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
): Promise<void> {
|
||||
await userInteractionManager.receiveAnswer(askId, selected, customInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局对话会话管理
|
||||
*/
|
||||
class DialogManager {
|
||||
private currentSession: DialogSession | null = null;
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
createSession(extensionPath: string): DialogSession {
|
||||
// 如果有活跃会话,先中止
|
||||
if (this.currentSession?.active) {
|
||||
this.currentSession.abort();
|
||||
}
|
||||
this.currentSession = new DialogSession(extensionPath);
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话
|
||||
*/
|
||||
getCurrentSession(): DialogSession | null {
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止当前会话
|
||||
*/
|
||||
abortCurrentSession(): void {
|
||||
this.currentSession?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
export const dialogManager = new DialogManager();
|
||||
296
src/services/sseHandler.ts
Normal file
296
src/services/sseHandler.ts
Normal file
@ -0,0 +1,296 @@
|
||||
/**
|
||||
* SSE 事件处理器
|
||||
* 处理与后端的流式通信
|
||||
* 使用 eventsource-parser + Node.js 原生 http 模块
|
||||
*/
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { URL } from 'url';
|
||||
import { createParser, type EventSourceParser } from 'eventsource-parser';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type {
|
||||
DialogRequest,
|
||||
SSEEventType,
|
||||
TextDeltaEvent,
|
||||
ToolCallRequest,
|
||||
AskUserEvent,
|
||||
CompleteEvent,
|
||||
ErrorEvent,
|
||||
ToolStartEvent,
|
||||
ToolCompleteEvent,
|
||||
ToolErrorEvent,
|
||||
WarningEvent,
|
||||
NotificationEvent,
|
||||
DepthUpdateEvent
|
||||
} from '../types/api';
|
||||
|
||||
/**
|
||||
* SSE 事件回调接口
|
||||
*/
|
||||
export interface SSECallbacks {
|
||||
/** 收到文本增量 */
|
||||
onTextDelta?: (data: TextDeltaEvent) => void;
|
||||
/** 收到工具调用请求 */
|
||||
onToolCall?: (data: ToolCallRequest) => void;
|
||||
/** 工具开始执行 */
|
||||
onToolStart?: (data: ToolStartEvent) => void;
|
||||
/** 工具执行完成 */
|
||||
onToolComplete?: (data: ToolCompleteEvent) => void;
|
||||
/** 工具执行错误 */
|
||||
onToolError?: (data: ToolErrorEvent) => void;
|
||||
/** 收到用户提问 */
|
||||
onAskUser?: (data: AskUserEvent) => void;
|
||||
/** 对话完成 */
|
||||
onComplete?: (data: CompleteEvent) => void;
|
||||
/** 错误 */
|
||||
onError?: (data: ErrorEvent) => void;
|
||||
/** 警告 */
|
||||
onWarning?: (data: WarningEvent) => void;
|
||||
/** 通知 */
|
||||
onNotification?: (data: NotificationEvent) => void;
|
||||
/** 深度更新 */
|
||||
onDepthUpdate?: (data: DepthUpdateEvent) => void;
|
||||
/** 连接打开 */
|
||||
onOpen?: () => void;
|
||||
/** 连接关闭 */
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE 会话控制器
|
||||
*/
|
||||
export class SSEController {
|
||||
private request: http.ClientRequest | null = null;
|
||||
private isConnected = false;
|
||||
private isAborted = false;
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
get connected(): boolean {
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求对象
|
||||
*/
|
||||
setRequest(req: http.ClientRequest): void {
|
||||
this.request = req;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接状态
|
||||
*/
|
||||
setConnected(connected: boolean): void {
|
||||
this.isConnected = connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已中止
|
||||
*/
|
||||
get aborted(): boolean {
|
||||
return this.isAborted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止当前连接
|
||||
*/
|
||||
abort(): void {
|
||||
if (this.request && !this.isAborted) {
|
||||
this.isAborted = true;
|
||||
this.request.destroy();
|
||||
this.request = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起流式对话
|
||||
* @param request 对话请求
|
||||
* @param callbacks 事件回调
|
||||
* @returns SSE 控制器(用于中止连接)
|
||||
*/
|
||||
export async function startStreamDialog(
|
||||
request: DialogRequest,
|
||||
callbacks: SSECallbacks
|
||||
): Promise<SSEController> {
|
||||
const controller = new SSEController();
|
||||
|
||||
const urlString = getApiUrl('/api/dialog/stream');
|
||||
const url = new URL(urlString);
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
};
|
||||
|
||||
const req = httpModule.request(options, (res) => {
|
||||
// 检查响应状态
|
||||
if (res.statusCode !== 200) {
|
||||
let errorBody = '';
|
||||
res.on('data', chunk => errorBody += chunk);
|
||||
res.on('end', () => {
|
||||
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
console.log('[SSE] 连接已建立');
|
||||
controller.setConnected(true);
|
||||
callbacks.onOpen?.();
|
||||
resolve(controller);
|
||||
|
||||
// 创建 SSE 解析器
|
||||
const parser = createParser({
|
||||
onEvent: (event) => {
|
||||
const eventType = event.event as SSEEventType;
|
||||
const eventData = event.data;
|
||||
|
||||
if (!eventData) {
|
||||
console.log(`[SSE] 收到空事件: ${eventType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(eventData);
|
||||
console.log(`[SSE] 收到事件: ${eventType}`, data);
|
||||
|
||||
// 分发事件到对应回调
|
||||
dispatchEvent(eventType, data, callbacks);
|
||||
} catch (e) {
|
||||
console.error(`[SSE] 解析事件数据失败: ${eventData}`, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 设置编码
|
||||
res.setEncoding('utf8');
|
||||
|
||||
// 处理数据流
|
||||
res.on('data', (chunk: string) => {
|
||||
if (!controller.aborted) {
|
||||
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
|
||||
parser.feed(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理连接关闭
|
||||
res.on('end', () => {
|
||||
console.log('[SSE] 连接已关闭');
|
||||
controller.setConnected(false);
|
||||
callbacks.onClose?.();
|
||||
});
|
||||
|
||||
// 处理错误
|
||||
res.on('error', (err) => {
|
||||
if (!controller.aborted) {
|
||||
console.error('[SSE] 响应错误:', err);
|
||||
controller.setConnected(false);
|
||||
callbacks.onError?.({ message: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 保存请求引用用于中止
|
||||
controller.setRequest(req);
|
||||
|
||||
// 处理请求错误
|
||||
req.on('error', (err) => {
|
||||
if (!controller.aborted) {
|
||||
console.error('[SSE] 请求错误:', err);
|
||||
controller.setConnected(false);
|
||||
callbacks.onError?.({ message: err.message });
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理超时
|
||||
const { timeout } = getConfig();
|
||||
req.setTimeout(timeout, () => {
|
||||
if (!controller.aborted) {
|
||||
console.error('[SSE] 请求超时');
|
||||
controller.abort();
|
||||
const error = new Error('请求超时');
|
||||
callbacks.onError?.({ message: error.message });
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送请求体
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分发 SSE 事件到对应回调
|
||||
*/
|
||||
function dispatchEvent(
|
||||
eventType: SSEEventType,
|
||||
data: unknown,
|
||||
callbacks: SSECallbacks
|
||||
): void {
|
||||
switch (eventType) {
|
||||
case 'text_delta':
|
||||
callbacks.onTextDelta?.(data as TextDeltaEvent);
|
||||
break;
|
||||
case 'tool_call':
|
||||
callbacks.onToolCall?.(data as ToolCallRequest);
|
||||
break;
|
||||
case 'tool_start':
|
||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||
break;
|
||||
case 'tool_complete':
|
||||
callbacks.onToolComplete?.(data as ToolCompleteEvent);
|
||||
break;
|
||||
case 'tool_error':
|
||||
callbacks.onToolError?.(data as ToolErrorEvent);
|
||||
break;
|
||||
case 'ask_user':
|
||||
callbacks.onAskUser?.(data as AskUserEvent);
|
||||
break;
|
||||
case 'complete':
|
||||
callbacks.onComplete?.(data as CompleteEvent);
|
||||
break;
|
||||
case 'error':
|
||||
callbacks.onError?.(data as ErrorEvent);
|
||||
break;
|
||||
case 'warning':
|
||||
callbacks.onWarning?.(data as WarningEvent);
|
||||
break;
|
||||
case 'notification':
|
||||
callbacks.onNotification?.(data as NotificationEvent);
|
||||
break;
|
||||
case 'depth_update':
|
||||
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
|
||||
break;
|
||||
default:
|
||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成任务ID
|
||||
*/
|
||||
export function generateTaskId(): string {
|
||||
return `task-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
||||
}
|
||||
272
src/services/toolExecutor.ts
Normal file
272
src/services/toolExecutor.ts
Normal file
@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 工具执行器
|
||||
* 接收后端的 tool_call 事件,执行本地工具,返回结果
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { readFileContent, readDirectory } from '../utils/readFiles';
|
||||
import { createOrOverwriteFile } from '../utils/createFiles';
|
||||
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
|
||||
import {
|
||||
submitToolResult,
|
||||
createSuccessResult,
|
||||
createBusinessErrorResult,
|
||||
createSystemErrorResult
|
||||
} from './apiClient';
|
||||
import type {
|
||||
ToolCallRequest,
|
||||
ToolName,
|
||||
FileReadArgs,
|
||||
FileWriteArgs,
|
||||
FileListArgs,
|
||||
SyntaxCheckArgs,
|
||||
SimulationArgs,
|
||||
WaveformSummaryArgs
|
||||
} from '../types/api';
|
||||
|
||||
/**
|
||||
* 工具执行器上下文
|
||||
*/
|
||||
export interface ToolExecutorContext {
|
||||
/** 扩展路径(用于 iverilog) */
|
||||
extensionPath: string;
|
||||
/** 工作区路径 */
|
||||
workspacePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行工具调用
|
||||
* @param request 工具调用请求
|
||||
* @param context 执行上下文
|
||||
*/
|
||||
export async function executeToolCall(
|
||||
request: ToolCallRequest,
|
||||
context: ToolExecutorContext
|
||||
): Promise<void> {
|
||||
const toolName = request.params.name as ToolName;
|
||||
const args = request.params.arguments;
|
||||
const callId = request.id;
|
||||
|
||||
console.log(`[ToolExecutor] 执行工具: ${toolName}, callId=${callId}`, args);
|
||||
|
||||
try {
|
||||
let resultText: string;
|
||||
|
||||
switch (toolName) {
|
||||
case 'file_read':
|
||||
resultText = await executeFileRead(args as unknown as FileReadArgs);
|
||||
break;
|
||||
case 'file_write':
|
||||
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
|
||||
break;
|
||||
case 'file_list':
|
||||
resultText = await executeFileList(args as unknown as FileListArgs);
|
||||
break;
|
||||
case 'syntax_check':
|
||||
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
|
||||
break;
|
||||
case 'simulation':
|
||||
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
|
||||
break;
|
||||
case 'waveform_summary':
|
||||
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`未知工具: ${toolName}`);
|
||||
}
|
||||
|
||||
// 提交成功结果
|
||||
const result = createSuccessResult(callId, resultText);
|
||||
await submitToolResult(result);
|
||||
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
|
||||
|
||||
// 提交错误结果
|
||||
const result = createBusinessErrorResult(callId, errorMessage);
|
||||
await submitToolResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_read 工具
|
||||
*/
|
||||
async function executeFileRead(args: FileReadArgs): Promise<string> {
|
||||
const content = await readFileContent(args.path);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_write 工具
|
||||
*/
|
||||
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
|
||||
await createOrOverwriteFile(args.path, args.content);
|
||||
return `文件已写入: ${args.path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 file_list 工具
|
||||
*/
|
||||
async function executeFileList(args: FileListArgs): Promise<string> {
|
||||
const dirPath = args.path || '.';
|
||||
const extensions = args.extension ? [args.extension] : undefined;
|
||||
|
||||
const files = await readDirectory(dirPath, extensions);
|
||||
const fileList = files.map(f => f.path).join('\n');
|
||||
|
||||
return fileList || '(目录为空)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 syntax_check 工具
|
||||
* 将代码写入临时文件,调用 iverilog 检查语法
|
||||
*/
|
||||
async function executeSyntaxCheck(
|
||||
args: SyntaxCheckArgs,
|
||||
context: ToolExecutorContext
|
||||
): Promise<string> {
|
||||
// 检查 iverilog 是否可用
|
||||
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
|
||||
if (!iverilogCheck.available) {
|
||||
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
|
||||
}
|
||||
|
||||
// 创建临时文件
|
||||
const tempDir = os.tmpdir();
|
||||
const tempFile = path.join(tempDir, `iccoder_syntax_${Date.now()}.v`);
|
||||
|
||||
try {
|
||||
// 写入代码到临时文件
|
||||
fs.writeFileSync(tempFile, args.code, 'utf-8');
|
||||
|
||||
// 调用 iverilog 进行语法检查
|
||||
const { spawn } = require('child_process');
|
||||
const iverilogPath = getIverilogPath(context.extensionPath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
|
||||
}
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
// 清理临时文件
|
||||
try {
|
||||
fs.unlinkSync(tempFile);
|
||||
} catch (e) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
resolve('语法检查通过,无错误。');
|
||||
} else {
|
||||
resolve(`语法检查发现错误:\n${stderr || stdout}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
try {
|
||||
fs.unlinkSync(tempFile);
|
||||
} catch (e) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// 确保清理临时文件
|
||||
try {
|
||||
fs.unlinkSync(tempFile);
|
||||
} catch (e) {
|
||||
// 忽略
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 simulation 工具
|
||||
*/
|
||||
async function executeSimulation(
|
||||
args: SimulationArgs,
|
||||
context: ToolExecutorContext
|
||||
): Promise<string> {
|
||||
// 获取工作区路径
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const projectPath = workspaceFolders[0].uri.fsPath;
|
||||
|
||||
// 调用现有的 generateVCD 函数
|
||||
const result = await generateVCD(projectPath, context.extensionPath);
|
||||
|
||||
if (result.success) {
|
||||
let message = result.message;
|
||||
if (result.stdout) {
|
||||
message += `\n\n仿真输出:\n${result.stdout}`;
|
||||
}
|
||||
return message;
|
||||
} else {
|
||||
let errorMessage = result.message;
|
||||
if (result.stderr) {
|
||||
errorMessage += `\n\n错误输出:\n${result.stderr}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 waveform_summary 工具
|
||||
* TODO: 实现 VCD 波形分析
|
||||
*/
|
||||
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
|
||||
// TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件
|
||||
// 目前返回一个占位响应
|
||||
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 iverilog 路径
|
||||
*/
|
||||
function getIverilogPath(extensionPath: string): string {
|
||||
const platform = process.platform;
|
||||
if (platform === 'win32') {
|
||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
|
||||
} else {
|
||||
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工具执行器上下文
|
||||
*/
|
||||
export function createToolExecutorContext(extensionPath: string): ToolExecutorContext {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
|
||||
|
||||
return {
|
||||
extensionPath,
|
||||
workspacePath
|
||||
};
|
||||
}
|
||||
154
src/services/userInteraction.ts
Normal file
154
src/services/userInteraction.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 用户交互处理器
|
||||
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { submitAnswer } from './apiClient';
|
||||
import type { AskUserEvent, AnswerRequest } from '../types/api';
|
||||
|
||||
/**
|
||||
* 待处理的用户问题
|
||||
*/
|
||||
interface PendingQuestion {
|
||||
askId: string;
|
||||
taskId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
resolve: (answer: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户交互管理器
|
||||
*/
|
||||
export class UserInteractionManager {
|
||||
private pendingQuestions = new Map<string, PendingQuestion>();
|
||||
private webviewPanel: vscode.WebviewPanel | null = null;
|
||||
|
||||
/**
|
||||
* 设置 WebView 面板(用于发送消息)
|
||||
*/
|
||||
setWebviewPanel(panel: vscode.WebviewPanel): void {
|
||||
this.webviewPanel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ask_user 事件
|
||||
* @param event ask_user 事件数据
|
||||
* @param taskId 当前任务ID
|
||||
*/
|
||||
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
||||
const { askId, question, options } = event;
|
||||
|
||||
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
|
||||
|
||||
// 通过 WebView 显示问题
|
||||
if (this.webviewPanel) {
|
||||
this.webviewPanel.webview.postMessage({
|
||||
command: 'showQuestion',
|
||||
askId,
|
||||
question,
|
||||
options
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 Promise 等待用户回答
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pendingQuestions.set(askId, {
|
||||
askId,
|
||||
taskId,
|
||||
question,
|
||||
options,
|
||||
resolve: (answer: string) => {
|
||||
this.submitUserAnswer(askId, taskId, answer)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
},
|
||||
reject
|
||||
});
|
||||
|
||||
// 设置超时(5分钟)
|
||||
setTimeout(() => {
|
||||
if (this.pendingQuestions.has(askId)) {
|
||||
this.pendingQuestions.delete(askId);
|
||||
reject(new Error('用户回答超时'));
|
||||
}
|
||||
}, 300000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户提交的回答(从 WebView 调用)
|
||||
* @param askId 问题ID
|
||||
* @param selected 选中的选项
|
||||
* @param customInput 自定义输入
|
||||
*/
|
||||
async receiveAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
): Promise<void> {
|
||||
const pending = this.pendingQuestions.get(askId);
|
||||
if (!pending) {
|
||||
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建答案
|
||||
const answer = customInput || selected?.join(', ') || '';
|
||||
|
||||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||||
|
||||
// 移除待处理问题
|
||||
this.pendingQuestions.delete(askId);
|
||||
|
||||
// 触发 resolve
|
||||
pending.resolve(answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交用户回答到后端
|
||||
*/
|
||||
private async submitUserAnswer(
|
||||
askId: string,
|
||||
taskId: string,
|
||||
answer: string
|
||||
): Promise<void> {
|
||||
const request: AnswerRequest = {
|
||||
askId,
|
||||
taskId,
|
||||
customInput: answer
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await submitAnswer(request);
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || '提交回答失败');
|
||||
}
|
||||
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
|
||||
} catch (error) {
|
||||
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有待处理的问题
|
||||
*/
|
||||
cancelAll(): void {
|
||||
for (const [askId, pending] of this.pendingQuestions) {
|
||||
pending.reject(new Error('用户交互已取消'));
|
||||
}
|
||||
this.pendingQuestions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的问题
|
||||
*/
|
||||
hasPendingQuestions(): boolean {
|
||||
return this.pendingQuestions.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
export const userInteractionManager = new UserInteractionManager();
|
||||
243
src/types/api.ts
Normal file
243
src/types/api.ts
Normal file
@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 后端 API 类型定义
|
||||
* 对应后端 IC Coder Backend 的接口格式
|
||||
*/
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
/**
|
||||
* 对话请求
|
||||
* POST /api/dialog/stream
|
||||
*/
|
||||
export interface DialogRequest {
|
||||
/** 任务ID(用于记忆隔离) */
|
||||
taskId: string;
|
||||
/** 用户消息 */
|
||||
message: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 工具模式 */
|
||||
toolMode: 'ASK' | 'AGENT';
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
|
||||
/** SSE 事件类型枚举 */
|
||||
export type SSEEventType =
|
||||
| 'text_delta' // 文本增量
|
||||
| 'tool_call' // 客户端工具调用请求
|
||||
| 'tool_start' // 工具开始执行
|
||||
| 'tool_complete' // 工具执行完成
|
||||
| 'tool_error' // 工具执行错误
|
||||
| 'ask_user' // 向用户提问
|
||||
| 'complete' // 对话完成
|
||||
| 'error' // 错误
|
||||
| 'warning' // 警告
|
||||
| 'notification' // 通知
|
||||
| 'depth_update'; // 深度更新
|
||||
|
||||
/** text_delta 事件数据 */
|
||||
export interface TextDeltaEvent {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** tool_start 事件数据 */
|
||||
export interface ToolStartEvent {
|
||||
tool_name: string;
|
||||
tool_input: unknown;
|
||||
}
|
||||
|
||||
/** tool_complete 事件数据 */
|
||||
export interface ToolCompleteEvent {
|
||||
tool_name: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
/** tool_error 事件数据 */
|
||||
export interface ToolErrorEvent {
|
||||
tool_name: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
/** complete 事件数据 */
|
||||
export interface CompleteEvent {
|
||||
status: string;
|
||||
finish_reason: string;
|
||||
}
|
||||
|
||||
/** error 事件数据 */
|
||||
export interface ErrorEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** warning 事件数据 */
|
||||
export interface WarningEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** notification 事件数据 */
|
||||
export interface NotificationEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** depth_update 事件数据 */
|
||||
export interface DepthUpdateEvent {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
// ============== 工具调用协议 (MCP 格式) ==============
|
||||
|
||||
/**
|
||||
* 工具调用请求(MCP格式)
|
||||
* 后端通过 SSE tool_call 事件推送
|
||||
*/
|
||||
export interface ToolCallRequest {
|
||||
/** JSON-RPC版本,固定为"2.0" */
|
||||
jsonrpc: '2.0';
|
||||
/** 请求ID,用于匹配响应 */
|
||||
id: number;
|
||||
/** 方法名,固定为"tools/call" */
|
||||
method: 'tools/call';
|
||||
/** 调用参数 */
|
||||
params: {
|
||||
/** 工具名称 */
|
||||
name: string;
|
||||
/** 工具参数 */
|
||||
arguments: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具执行结果(MCP格式)
|
||||
* POST /api/tool/result
|
||||
*/
|
||||
export interface ToolCallResult {
|
||||
/** JSON-RPC版本 */
|
||||
jsonrpc: '2.0';
|
||||
/** 请求ID,与ToolCallRequest.id对应 */
|
||||
id: number;
|
||||
/** 执行结果(与error互斥) */
|
||||
result?: ToolResultContent;
|
||||
/** 错误信息(与result互斥) */
|
||||
error?: ToolResultError;
|
||||
}
|
||||
|
||||
/** 工具执行结果内容 */
|
||||
export interface ToolResultContent {
|
||||
/** 内容列表 */
|
||||
content: ContentItem[];
|
||||
/** 是否为错误结果(业务错误,如编译失败) */
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
/** 内容项 */
|
||||
export interface ContentItem {
|
||||
/** 内容类型:text, image, resource */
|
||||
type: string;
|
||||
/** 文本内容 */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** 工具系统错误 */
|
||||
export interface ToolResultError {
|
||||
/** 错误码 */
|
||||
code: number;
|
||||
/** 错误消息 */
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============== 用户回答 ==============
|
||||
|
||||
/**
|
||||
* 用户回答请求
|
||||
* POST /api/task/answer
|
||||
*/
|
||||
export interface AnswerRequest {
|
||||
/** 问题ID */
|
||||
askId: string;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 选中的选项列表 */
|
||||
selected?: string[];
|
||||
/** 自定义输入内容 */
|
||||
customInput?: string;
|
||||
}
|
||||
|
||||
/** 用户回答响应 */
|
||||
export interface AnswerResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============== 工具结果响应 ==============
|
||||
|
||||
/** 工具结果响应 */
|
||||
export interface ToolResultResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
export type ToolName =
|
||||
| 'file_read'
|
||||
| 'file_write'
|
||||
| 'file_list'
|
||||
| 'syntax_check'
|
||||
| 'simulation'
|
||||
| 'waveform_summary';
|
||||
|
||||
/** file_read 工具参数 */
|
||||
export interface FileReadArgs {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/** file_write 工具参数 */
|
||||
export interface FileWriteArgs {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** file_list 工具参数 */
|
||||
export interface FileListArgs {
|
||||
path?: string;
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
/** syntax_check 工具参数 */
|
||||
export interface SyntaxCheckArgs {
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** simulation 工具参数 */
|
||||
export interface SimulationArgs {
|
||||
rtlPath: string;
|
||||
tbPath: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
/** waveform_summary 工具参数 */
|
||||
export interface WaveformSummaryArgs {
|
||||
vcdPath: string;
|
||||
signals: string;
|
||||
checkpoints?: string;
|
||||
}
|
||||
|
||||
/** 工具参数联合类型 */
|
||||
export type ToolArgs =
|
||||
| FileReadArgs
|
||||
| FileWriteArgs
|
||||
| FileListArgs
|
||||
| SyntaxCheckArgs
|
||||
| SimulationArgs
|
||||
| WaveformSummaryArgs;
|
||||
@ -14,6 +14,15 @@ import {
|
||||
checkIverilogAvailable,
|
||||
} from "./iverilogRunner";
|
||||
import { ChatHistoryManager } from "./chatHistoryManager";
|
||||
import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
|
||||
/** 是否使用后端服务(可通过配置控制) */
|
||||
let useBackendService = true;
|
||||
|
||||
/** 当前对话会话 */
|
||||
let currentSession: DialogSession | null = null;
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
@ -25,33 +34,53 @@ export async function handleUserMessage(
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
// 记录用户消息到历史
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
await historyManager.addUserMessage(text);
|
||||
// 记录用户消息到历史(允许失败,不阻塞主流程)
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
await historyManager.addUserMessage(text);
|
||||
} catch (error) {
|
||||
console.warn("记录消息历史失败(可能没有打开工作区):", error);
|
||||
}
|
||||
|
||||
// 检查是否是 VCD 生成命令
|
||||
// 设置 WebView 面板用于用户交互
|
||||
userInteractionManager.setWebviewPanel(panel);
|
||||
|
||||
// 检查是否是 VCD 生成命令(本地处理)
|
||||
if (isVCDGenerationCommand(text)) {
|
||||
await handleVCDGeneration(panel, extensionPath || "");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是文件操作命令
|
||||
// 检查是否是文件操作命令(本地处理)
|
||||
const fileOperation = parseFileOperation(text);
|
||||
|
||||
console.log("解析结果:", fileOperation);
|
||||
|
||||
if (fileOperation) {
|
||||
console.log("执行文件操作:", fileOperation.type, fileOperation.filePath);
|
||||
await handleFileOperation(panel, fileOperation);
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通消息处理
|
||||
console.log("作为普通消息处理");
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用,回退到本地模式:", error);
|
||||
// 后端不可用时,使用本地模拟回复
|
||||
}
|
||||
}
|
||||
|
||||
// 本地模拟回复(后端不可用时的 fallback)
|
||||
console.log("使用本地模拟回复");
|
||||
const reply = getMockReply(text);
|
||||
|
||||
// 记录助手回复到历史
|
||||
await historyManager.addAiMessage(reply);
|
||||
// 记录AI回复到历史(允许失败)
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
await historyManager.addAiMessage(reply);
|
||||
} catch (error) {
|
||||
console.warn("记录AI回复历史失败:", error);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({
|
||||
@ -61,6 +90,122 @@ export async function handleUserMessage(
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用后端服务处理用户消息
|
||||
*/
|
||||
async function handleUserMessageWithBackend(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath: string
|
||||
): Promise<void> {
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath);
|
||||
}
|
||||
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
|
||||
// 显示加载状态
|
||||
panel.webview.postMessage({
|
||||
command: "showLoading",
|
||||
text: "正在思考...",
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
currentSession!.sendMessage(text, {
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 暂时只在完成时发送消息(非流式)
|
||||
if (!isStreaming) {
|
||||
console.log('[MessageHandler] 发送最终消息, 文本长度:', fullText.length);
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: fullText,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onToolStart: (toolName) => {
|
||||
panel.webview.postMessage({
|
||||
command: "toolStart",
|
||||
toolName,
|
||||
});
|
||||
},
|
||||
|
||||
onToolComplete: (toolName, result) => {
|
||||
panel.webview.postMessage({
|
||||
command: "toolComplete",
|
||||
toolName,
|
||||
result,
|
||||
});
|
||||
},
|
||||
|
||||
onToolError: (toolName, error) => {
|
||||
panel.webview.postMessage({
|
||||
command: "toolError",
|
||||
toolName,
|
||||
error,
|
||||
});
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
panel.webview.postMessage({
|
||||
command: "showQuestion",
|
||||
askId,
|
||||
question,
|
||||
options,
|
||||
});
|
||||
},
|
||||
|
||||
onComplete: async () => {
|
||||
// 隐藏加载状态
|
||||
panel.webview.postMessage({
|
||||
command: "hideLoading",
|
||||
});
|
||||
|
||||
// 记录到历史(如果有累积文本)
|
||||
// 注意:实际文本已通过 onText 发送
|
||||
resolve();
|
||||
},
|
||||
|
||||
onError: (message) => {
|
||||
panel.webview.postMessage({
|
||||
command: "hideLoading",
|
||||
});
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
});
|
||||
reject(new Error(message));
|
||||
},
|
||||
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户回答(从 WebView 调用)
|
||||
*/
|
||||
export async function handleUserAnswer(
|
||||
askId: string,
|
||||
selected?: string[],
|
||||
customInput?: string
|
||||
): Promise<void> {
|
||||
if (currentSession) {
|
||||
await currentSession.submitAnswer(askId, selected, customInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止当前对话
|
||||
*/
|
||||
export function abortCurrentDialog(): void {
|
||||
dialogManager.abortCurrentSession();
|
||||
currentSession = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件操作命令
|
||||
*/
|
||||
|
||||
@ -5,12 +5,17 @@ import {
|
||||
insertCodeToEditor,
|
||||
handleReadFile,
|
||||
handleCreateFile,
|
||||
handleUpdateFile,
|
||||
handleRenameFile,
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
} from "../utils/messageHandler";
|
||||
|
||||
/**
|
||||
* 创建并显示IC 侧边栏视图
|
||||
*/
|
||||
export function showICHelperPanel(content: vscode.ExtensionContext) {
|
||||
export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
// 创建WebView面板
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"icCoder", // 面板ID
|
||||
@ -19,20 +24,20 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(content.extensionUri, "media")],
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
}
|
||||
);
|
||||
|
||||
// 设置标签页图标
|
||||
panel.iconPath = vscode.Uri.joinPath(
|
||||
content.extensionUri,
|
||||
context.extensionUri,
|
||||
"media",
|
||||
"图案(方底).png"
|
||||
);
|
||||
|
||||
// 获取页面内图标URI
|
||||
const iconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(content.extensionUri, "media", "图案(方底).png")
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "图案(方底).png")
|
||||
);
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
@ -42,11 +47,20 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
|
||||
(message) => {
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text);
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
break;
|
||||
case "updateFile":
|
||||
handleUpdateFile(panel, message.filePath, message.content);
|
||||
break;
|
||||
case "renameFile":
|
||||
handleRenameFile(panel, message.oldPath, message.newPath);
|
||||
break;
|
||||
case "replaceInFile":
|
||||
handleReplaceInFile(panel, message.filePath, message.searchText, message.replaceText);
|
||||
break;
|
||||
case "insertCode":
|
||||
insertCodeToEditor(message.code);
|
||||
break;
|
||||
@ -61,10 +75,18 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
|
||||
case "showInfo":
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(message.askId, message.selected, message.customInput);
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
content.subscriptions
|
||||
context.subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -538,6 +538,148 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 流式消息样式 */
|
||||
.streaming .message-content {
|
||||
border-right: 2px solid var(--vscode-focusBorder);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 50% { border-color: var(--vscode-focusBorder); }
|
||||
51%, 100% { border-color: transparent; }
|
||||
}
|
||||
|
||||
/* 加载指示器样式 */
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-focusBorder);
|
||||
animation: loadingDot 1.4s infinite ease-in-out;
|
||||
}
|
||||
.loading-dots span:nth-child(1) { animation-delay: 0s; }
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes loadingDot {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 工具状态样式 */
|
||||
.tool-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
.tool-status.tool-start {
|
||||
border-left: 3px solid var(--vscode-charts-blue);
|
||||
}
|
||||
.tool-status.tool-complete {
|
||||
border-left: 3px solid var(--vscode-charts-green);
|
||||
}
|
||||
.tool-status.tool-error {
|
||||
border-left: 3px solid var(--vscode-charts-red);
|
||||
}
|
||||
.tool-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.tool-status-text {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.tool-detail {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 用户问题样式 */
|
||||
.question-message {
|
||||
padding: 16px;
|
||||
}
|
||||
.question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.question-option {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.question-option:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
.question-option.selected {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
.question-message.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.question-message.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -947,12 +1089,44 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
});
|
||||
|
||||
// 流式消息相关状态
|
||||
let currentStreamingMessage = null;
|
||||
let loadingIndicator = null;
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
console.log('[WebView] 收到消息:', message.command, message);
|
||||
|
||||
switch (message.command) {
|
||||
case 'receiveMessage':
|
||||
addMessage(message.text, 'bot');
|
||||
// 完成流式消息或普通消息
|
||||
if (currentStreamingMessage) {
|
||||
finalizeStreamingMessage(message.text);
|
||||
} else {
|
||||
addMessage(message.text, 'bot');
|
||||
}
|
||||
break;
|
||||
case 'updateStreamingMessage':
|
||||
// 流式更新消息
|
||||
updateOrCreateStreamingMessage(message.text);
|
||||
break;
|
||||
case 'showLoading':
|
||||
showLoadingIndicator(message.text || '正在思考...');
|
||||
break;
|
||||
case 'hideLoading':
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
case 'toolStart':
|
||||
addToolStatus(message.toolName, 'start');
|
||||
break;
|
||||
case 'toolComplete':
|
||||
addToolStatus(message.toolName, 'complete', message.result);
|
||||
break;
|
||||
case 'toolError':
|
||||
addToolStatus(message.toolName, 'error', message.error);
|
||||
break;
|
||||
case 'showQuestion':
|
||||
showUserQuestion(message.askId, message.question, message.options);
|
||||
break;
|
||||
case 'fileContent':
|
||||
displayFileContent(message.content, message.filePath);
|
||||
@ -972,6 +1146,163 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
});
|
||||
|
||||
// 更新或创建流式消息
|
||||
function updateOrCreateStreamingMessage(text) {
|
||||
hideLoadingIndicator();
|
||||
|
||||
if (!currentStreamingMessage) {
|
||||
// 创建新的流式消息元素
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message streaming';
|
||||
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContent.textContent = text;
|
||||
div.appendChild(messageContent);
|
||||
|
||||
messagesContainer.appendChild(div);
|
||||
currentStreamingMessage = div;
|
||||
} else {
|
||||
// 更新现有消息内容
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 完成流式消息
|
||||
function finalizeStreamingMessage(finalText) {
|
||||
if (currentStreamingMessage) {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = finalText;
|
||||
}
|
||||
currentStreamingMessage.classList.remove('streaming');
|
||||
|
||||
// 添加操作按钮
|
||||
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 = () => copyMessage(finalText, copyBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
|
||||
currentStreamingMessage.appendChild(actionsDiv);
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 显示加载指示器
|
||||
function showLoadingIndicator(text) {
|
||||
hideLoadingIndicator();
|
||||
|
||||
loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'message bot-message loading-message';
|
||||
loadingIndicator.innerHTML = \`
|
||||
<div class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="loading-text">\${text}</span>
|
||||
\`;
|
||||
messagesContainer.appendChild(loadingIndicator);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 隐藏加载指示器
|
||||
function hideLoadingIndicator() {
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
loadingIndicator = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加工具状态消息
|
||||
function addToolStatus(toolName, status, detail) {
|
||||
const statusIcons = {
|
||||
start: '🔧',
|
||||
complete: '✅',
|
||||
error: '❌'
|
||||
};
|
||||
const statusTexts = {
|
||||
start: '正在执行',
|
||||
complete: '执行完成',
|
||||
error: '执行失败'
|
||||
};
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message tool-status tool-\${status}\`;
|
||||
div.innerHTML = \`
|
||||
<span class="tool-icon">\${statusIcons[status]}</span>
|
||||
<span class="tool-name">\${toolName}</span>
|
||||
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||
\`;
|
||||
messagesContainer.appendChild(div);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 显示用户问题
|
||||
function showUserQuestion(askId, question, options) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message question-message';
|
||||
div.innerHTML = \`
|
||||
<div class="question-text">\${question}</div>
|
||||
<div class="question-options">
|
||||
\${options.map((opt, i) => \`
|
||||
<button class="question-option" data-ask-id="\${askId}" data-option="\${opt}">
|
||||
\${opt}
|
||||
</button>
|
||||
\`).join('')}
|
||||
<div class="custom-input-container">
|
||||
<input type="text" class="custom-input" placeholder="或输入自定义回答..." />
|
||||
<button class="custom-submit" data-ask-id="\${askId}">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 绑定选项点击事件
|
||||
div.querySelectorAll('.question-option').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const selected = btn.dataset.option;
|
||||
submitAnswer(askId, [selected]);
|
||||
div.classList.add('answered');
|
||||
btn.classList.add('selected');
|
||||
};
|
||||
});
|
||||
|
||||
// 绑定自定义输入提交
|
||||
const customInput = div.querySelector('.custom-input');
|
||||
const customSubmit = div.querySelector('.custom-submit');
|
||||
customSubmit.onclick = () => {
|
||||
const value = customInput.value.trim();
|
||||
if (value) {
|
||||
submitAnswer(askId, null, value);
|
||||
div.classList.add('answered');
|
||||
}
|
||||
};
|
||||
|
||||
messagesContainer.appendChild(div);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 提交用户回答
|
||||
function submitAnswer(askId, selected, customInput) {
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: selected,
|
||||
customInput: customInput
|
||||
});
|
||||
}
|
||||
|
||||
// 支持回车键读取文件
|
||||
filePathInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
||||
Reference in New Issue
Block a user