Compare commits
18 Commits
ef2159f1bd
...
acf3f9ff37
| Author | SHA1 | Date | |
|---|---|---|---|
| acf3f9ff37 | |||
| c27b08cccf | |||
| 9fc3c9f056 | |||
| 60d8eaf0eb | |||
| df6f983e83 | |||
| acf60f2a17 | |||
| f933d84cd1 | |||
| b794d1ceb0 | |||
| 259310a29d | |||
| 715eac5949 | |||
| c2936395d9 | |||
| 8762eacb3e | |||
| 3d535fd3e1 | |||
| ecdbe0bdc0 | |||
| c49aaf753c | |||
| 0f8674e1c7 | |||
| 28b75e8475 | |||
| 16e91bd2c0 |
BIN
src/assets/model/Auto.png
Normal file
BIN
src/assets/model/Auto.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/model/Max.png
Normal file
BIN
src/assets/model/Max.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/model/Sy.png
Normal file
BIN
src/assets/model/Sy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/model/lite.png
Normal file
BIN
src/assets/model/lite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -1,9 +1,15 @@
|
||||
/**
|
||||
* 配置管理
|
||||
* 后端地址已预配置,用户无需手动设置
|
||||
* 支持 dev(本地开发)和 test(测试服务器)两种环境
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
/** 环境类型 */
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "test";
|
||||
|
||||
/** 配置项接口 */
|
||||
export interface IccoderConfig {
|
||||
/** 后端服务地址 */
|
||||
@ -14,19 +20,40 @@ export interface IccoderConfig {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/** 默认配置(预配置,不暴露给用户) */
|
||||
const DEFAULT_CONFIG: IccoderConfig = {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
/** 环境配置 */
|
||||
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
/** 本地开发环境 */
|
||||
dev: {
|
||||
backendUrl: "http://localhost:2233",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
},
|
||||
/** 测试服务器环境 */
|
||||
test: {
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
},
|
||||
/** 生产环境 */
|
||||
prod: {
|
||||
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境
|
||||
*/
|
||||
export function getCurrentEnv(): Environment {
|
||||
return CURRENT_ENV;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置项
|
||||
* 直接返回预配置的值,用户无需手动配置
|
||||
*/
|
||||
export function getConfig(): IccoderConfig {
|
||||
return { ...DEFAULT_CONFIG };
|
||||
return { ...ENV_CONFIG[CURRENT_ENV] };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,7 +61,6 @@ export function getConfig(): IccoderConfig {
|
||||
*/
|
||||
export function getApiUrl(path: string): string {
|
||||
const { backendUrl } = getConfig();
|
||||
// 确保 URL 格式正确
|
||||
const baseUrl = backendUrl.endsWith("/")
|
||||
? backendUrl.slice(0, -1)
|
||||
: backendUrl;
|
||||
|
||||
@ -12,7 +12,9 @@ import {
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
import { compactDialog } from "../services/apiClient";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
import { MessageType } from "../types/chatHistory";
|
||||
@ -58,7 +60,10 @@ export async function showICHelperPanel(
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@ -80,8 +85,28 @@ export async function showICHelperPanel(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
@ -193,7 +218,40 @@ export async function showICHelperPanel(
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
// 新增:压缩会话
|
||||
case "compressConversation":
|
||||
{
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
compactDialog(taskId)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "✅ 会话压缩完成",
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: "❌ 没有活跃的会话",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
@ -528,6 +586,9 @@ async function selectConversation(
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 lastTaskId,用于压缩等操作
|
||||
setLastTaskId(taskId);
|
||||
|
||||
// 更新面板的任务映射,确保后续对话保存到正确的任务中
|
||||
const panelId = (panel as any).__uniqueId;
|
||||
historyManager.setPanelTask(panelId, taskId, workspacePath);
|
||||
|
||||
@ -126,6 +126,55 @@ export async function healthCheck(): Promise<{ status: string }> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话请求
|
||||
*/
|
||||
export interface StopDialogRequest {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话响应
|
||||
*/
|
||||
export interface StopDialogResponse {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止对话
|
||||
* POST /api/dialog/stop
|
||||
*/
|
||||
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
|
||||
console.log(`[API] 停止对话: taskId=${taskId}`);
|
||||
return request<StopDialogResponse>('/api/dialog/stop', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/** 压缩对话响应 */
|
||||
export interface CompactDialogResponse {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动压缩对话历史
|
||||
* POST /api/dialog/compact
|
||||
*/
|
||||
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
|
||||
console.log(`[API] 压缩对话: taskId=${taskId}`);
|
||||
return request<CompactDialogResponse>('/api/dialog/compact', {
|
||||
method: 'POST',
|
||||
body: { taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功的工具结果
|
||||
*/
|
||||
|
||||
@ -3,12 +3,15 @@
|
||||
* 整合 SSE 通信、工具执行、用户交互
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
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, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||
import { submitToolConfirm, submitAnswer } from './apiClient';
|
||||
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
|
||||
import { ChatHistoryManager } from '../utils/chatHistoryManager';
|
||||
|
||||
/**
|
||||
* 消息段落类型
|
||||
@ -70,6 +73,8 @@ export interface DialogCallbacks {
|
||||
onError?: (message: string) => void;
|
||||
/** 通知消息 */
|
||||
onNotification?: (message: string) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,6 +157,120 @@ export class DialogSession {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载知识图谱数据
|
||||
* 从 .iccoder/knowledge.json 读取
|
||||
*/
|
||||
private async loadKnowledgeData(): Promise<string | null> {
|
||||
console.log('[DialogSession] loadKnowledgeData 开始执行');
|
||||
|
||||
// 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空)
|
||||
const workspaceFolders = await this.waitForWorkspaceFolders();
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
console.log('[DialogSession] 没有工作区文件夹');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 多根工作区场景:优先读取实际存在 knowledge.json 的根目录
|
||||
for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) {
|
||||
const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json');
|
||||
console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString());
|
||||
|
||||
try {
|
||||
const content = await this.readTextFileWithRetry(knowledgeUri, 5);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败
|
||||
try {
|
||||
const parsed = JSON.parse(content) as any;
|
||||
|
||||
// 兼容:后端 KnowledgeGraph.isEmpty() 可能被序列化为 "empty",老后端反序列化会失败
|
||||
if (parsed && typeof parsed === 'object' && 'empty' in parsed) {
|
||||
delete parsed.empty;
|
||||
}
|
||||
|
||||
const sanitized = JSON.stringify(parsed);
|
||||
console.log('[DialogSession] 知识图谱已清洗, sanitizedLen:', sanitized.length);
|
||||
return sanitized;
|
||||
} catch (e) {
|
||||
console.warn('[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:', e);
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[DialogSession] 加载知识图谱失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async waitForWorkspaceFolders(): Promise<readonly vscode.WorkspaceFolder[] | undefined> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
return folders;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return vscode.workspace.workspaceFolders;
|
||||
}
|
||||
|
||||
private getWorkspaceFolderCandidates(
|
||||
workspaceFolders: readonly vscode.WorkspaceFolder[]
|
||||
): vscode.WorkspaceFolder[] {
|
||||
const result: vscode.WorkspaceFolder[] = [];
|
||||
|
||||
// 1) 当前激活文件所在的 workspace folder(如果有)
|
||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||
if (activeFolder) {
|
||||
result.push(activeFolder);
|
||||
}
|
||||
|
||||
// 2) 其它 workspace folders(去重)
|
||||
for (const folder of workspaceFolders) {
|
||||
if (!result.some(f => f.uri.toString() === folder.uri.toString())) {
|
||||
result.push(folder);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise<string | null> {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(uri);
|
||||
const text = Buffer.from(bytes).toString('utf-8');
|
||||
if (!text || !text.trim()) {
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
} catch (error) {
|
||||
// 文件不存在:不是错误,直接返回 null
|
||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const retryable =
|
||||
(error instanceof vscode.FileSystemError && error.code === 'Unavailable') ||
|
||||
(typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code));
|
||||
|
||||
if (!retryable || attempt >= maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delayMs = 50 * attempt;
|
||||
console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具操作描述(用于确认对话框)
|
||||
*/
|
||||
@ -210,13 +329,29 @@ export class DialogSession {
|
||||
this.currentTextSegment = null;
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
// 获取压缩数据和新消息(用于后端重启后恢复)
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const compactedData = await historyManager.loadCompactedData(this.taskId);
|
||||
const newMessages = historyManager.getNewMessagesSinceCompaction();
|
||||
|
||||
// 加载知识图谱数据
|
||||
const knowledgeData = await this.loadKnowledgeData();
|
||||
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
|
||||
|
||||
const request: DialogRequest = {
|
||||
taskId: this.taskId,
|
||||
message,
|
||||
userId: config.userId,
|
||||
mode: mode || 'agent'
|
||||
mode: mode || 'agent',
|
||||
compactedData: compactedData || undefined,
|
||||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||||
knowledgeData: knowledgeData || undefined
|
||||
};
|
||||
|
||||
// 追踪用户消息
|
||||
historyManager.trackUserMessage(message);
|
||||
|
||||
const sseCallbacks: SSECallbacks = {
|
||||
onTextDelta: (data) => {
|
||||
this.accumulatedText += data.text;
|
||||
@ -420,6 +555,12 @@ export class DialogSession {
|
||||
onComplete: (data) => {
|
||||
this.isActive = false;
|
||||
this.finalizeTextSegment();
|
||||
|
||||
// 追踪 AI 消息(用于后端重启后恢复)
|
||||
if (this.accumulatedText) {
|
||||
historyManager.trackAiMessage(this.accumulatedText);
|
||||
}
|
||||
|
||||
// 发送所有段落
|
||||
callbacks.onComplete?.(this.segments);
|
||||
},
|
||||
@ -500,6 +641,17 @@ export class DialogSession {
|
||||
}
|
||||
},
|
||||
|
||||
onMemoryCompacted: async (data) => {
|
||||
console.log('[DialogSession] onMemoryCompacted:', data.taskId);
|
||||
// 保存压缩数据到本地
|
||||
await historyManager.saveCompactedData(data.compactedData);
|
||||
},
|
||||
|
||||
onContextUsage: (data) => {
|
||||
console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens);
|
||||
callbacks.onContextUsage?.(data);
|
||||
},
|
||||
|
||||
onOpen: () => {
|
||||
console.log('[DialogSession] SSE 连接已建立');
|
||||
},
|
||||
@ -530,6 +682,25 @@ export class DialogSession {
|
||||
}
|
||||
this.isActive = false;
|
||||
userInteractionManager.cancelAll();
|
||||
|
||||
// 通知后端停止处理
|
||||
stopDialog(this.taskId).catch(err => {
|
||||
console.warn('[DialogSession] 停止对话请求失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的消息段落(用于中止时保存)
|
||||
*/
|
||||
getSegments(): MessageSegment[] {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取累积的文本内容
|
||||
*/
|
||||
getAccumulatedText(): string {
|
||||
return this.accumulatedText;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -27,8 +27,10 @@ import type {
|
||||
AgentStartEvent,
|
||||
AgentProgressEvent,
|
||||
AgentCompleteEvent,
|
||||
AgentErrorEvent
|
||||
AgentErrorEvent,
|
||||
ContextUsageEvent
|
||||
} from '../types/api';
|
||||
import type { MemoryCompactedEvent } from '../types/memory';
|
||||
|
||||
/**
|
||||
* SSE 事件回调接口
|
||||
@ -68,6 +70,10 @@ export interface SSECallbacks {
|
||||
onAgentComplete?: (data: AgentCompleteEvent) => void;
|
||||
/** 子智能体错误 */
|
||||
onAgentError?: (data: AgentErrorEvent) => void;
|
||||
/** 记忆压缩完成 */
|
||||
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
|
||||
/** 上下文使用量更新 */
|
||||
onContextUsage?: (data: ContextUsageEvent) => void;
|
||||
/** 连接打开 */
|
||||
onOpen?: () => void;
|
||||
/** 连接关闭 */
|
||||
@ -319,6 +325,12 @@ function dispatchEvent(
|
||||
case 'agent_error':
|
||||
callbacks.onAgentError?.(data as AgentErrorEvent);
|
||||
break;
|
||||
case 'memory_compacted':
|
||||
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
|
||||
break;
|
||||
case 'context_usage':
|
||||
callbacks.onContextUsage?.(data as ContextUsageEvent);
|
||||
break;
|
||||
default:
|
||||
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
|
||||
}
|
||||
|
||||
@ -313,22 +313,19 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
|
||||
* 保存知识图谱到 .iccoder/knowledge.json
|
||||
*/
|
||||
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
const iccoderDir = path.join(workspacePath, '.iccoder');
|
||||
const knowledgePath = path.join(iccoderDir, 'knowledge.json');
|
||||
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
|
||||
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
|
||||
|
||||
// 确保 .iccoder 目录存在
|
||||
if (!fs.existsSync(iccoderDir)) {
|
||||
fs.mkdirSync(iccoderDir, { recursive: true });
|
||||
}
|
||||
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
|
||||
await vscode.workspace.fs.createDirectory(iccoderDirUri);
|
||||
|
||||
// 写入知识图谱
|
||||
fs.writeFileSync(knowledgePath, args.data, 'utf-8');
|
||||
// 写入知识图谱(UTF-8)
|
||||
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
|
||||
|
||||
return `知识图谱已保存: .iccoder/knowledge.json`;
|
||||
}
|
||||
@ -338,21 +335,36 @@ async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
|
||||
* 从 .iccoder/knowledge.json 加载知识图谱
|
||||
*/
|
||||
async function executeKnowledgeLoad(): Promise<string> {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
const workspaceFolder = getWorkspaceFolder();
|
||||
if (!workspaceFolder) {
|
||||
throw new Error('请先打开一个工作区');
|
||||
}
|
||||
|
||||
const workspacePath = workspaceFolders[0].uri.fsPath;
|
||||
const knowledgePath = path.join(workspacePath, '.iccoder', 'knowledge.json');
|
||||
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
|
||||
|
||||
// 如果文件不存在,返回空图谱
|
||||
if (!fs.existsSync(knowledgePath)) {
|
||||
return JSON.stringify({ directed: true, nodes: [], links: [] });
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
|
||||
const content = Buffer.from(bytes).toString('utf-8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
// 文件不存在:返回空图谱
|
||||
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
|
||||
// 与后端 KnowledgeGraph 结构保持一致(nodes/edges + nodeClass 多态字段)
|
||||
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(knowledgePath, 'utf-8');
|
||||
return content;
|
||||
const activeUri = vscode.window.activeTextEditor?.document?.uri;
|
||||
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
|
||||
return activeFolder ?? folders[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -32,6 +32,13 @@ export class UserInteractionManager {
|
||||
this.webviewPanel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebView 面板
|
||||
*/
|
||||
getWebviewPanel(): vscode.WebviewPanel | null {
|
||||
return this.webviewPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ask_user 事件
|
||||
* @param event ask_user 事件数据
|
||||
@ -60,13 +67,13 @@ export class UserInteractionManager {
|
||||
reject
|
||||
});
|
||||
|
||||
// 设置超时(5分钟)
|
||||
// 设置超时(2小时)
|
||||
setTimeout(() => {
|
||||
if (this.pendingQuestions.has(askId)) {
|
||||
this.pendingQuestions.delete(askId);
|
||||
reject(new Error('用户回答超时'));
|
||||
}
|
||||
}, 300000);
|
||||
}, 7200000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
* 对应后端 IC Coder Backend 的接口格式
|
||||
*/
|
||||
|
||||
import { CompactedMemory, CompactedMessage } from './memory';
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
/**
|
||||
@ -27,6 +29,12 @@ export interface DialogRequest {
|
||||
userId: string;
|
||||
/** 运行模式 */
|
||||
mode: RunMode;
|
||||
/** 压缩后的记忆数据(用于后端重启后恢复) */
|
||||
compactedData?: CompactedMemory;
|
||||
/** 压缩后产生的新消息 */
|
||||
newMessages?: CompactedMessage[];
|
||||
/** 知识图谱数据(JSON 字符串,用于恢复知识图谱) */
|
||||
knowledgeData?: string;
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
@ -45,6 +53,8 @@ export type SSEEventType =
|
||||
| 'agent_progress' // 子智能体进度
|
||||
| 'agent_complete' // 子智能体完成
|
||||
| 'agent_error' // 子智能体错误
|
||||
| 'memory_compacted' // 记忆压缩完成
|
||||
| 'context_usage' // 上下文使用量
|
||||
| 'complete' // 对话完成
|
||||
| 'error' // 错误
|
||||
| 'warning' // 警告
|
||||
@ -172,6 +182,13 @@ export interface AgentErrorEvent {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** context_usage 事件数据 */
|
||||
export interface ContextUsageEvent {
|
||||
currentTokens: number;
|
||||
maxTokens: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
// ============== 工具调用协议 (MCP 格式) ==============
|
||||
|
||||
/**
|
||||
|
||||
@ -5,7 +5,8 @@ export enum MessageType {
|
||||
SYSTEM = "SYSTEM",
|
||||
USER = "USER",
|
||||
AI = "AI",
|
||||
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT"
|
||||
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT",
|
||||
COMPACTION_SUMMARY = "COMPACTION_SUMMARY" // 压缩摘要
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,10 +70,22 @@ export interface ToolExecutionResultMessage extends BaseMessage {
|
||||
text: string; // JSON字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩摘要消息
|
||||
*/
|
||||
export interface CompactionSummaryMessage extends BaseMessage {
|
||||
type: MessageType.COMPACTION_SUMMARY;
|
||||
summary: string;
|
||||
version: number;
|
||||
compactedAt: string;
|
||||
originalMessageCount: number;
|
||||
compactedMessageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 联合消息类型
|
||||
*/
|
||||
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage;
|
||||
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage | CompactionSummaryMessage;
|
||||
|
||||
/**
|
||||
* 对话轮次元数据
|
||||
|
||||
42
src/types/memory.ts
Normal file
42
src/types/memory.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 压缩记忆相关类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 压缩后的记忆数据
|
||||
*/
|
||||
export interface CompactedMemory {
|
||||
taskId: string;
|
||||
version: number;
|
||||
compactedAt: string;
|
||||
summary: string;
|
||||
recentMessages: CompactedMessage[];
|
||||
originalMessageCount: number;
|
||||
compactedMessageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩消息格式
|
||||
*/
|
||||
export interface CompactedMessage {
|
||||
type: 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT';
|
||||
content: string;
|
||||
toolCall?: ToolCallInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用信息
|
||||
*/
|
||||
export interface ToolCallInfo {
|
||||
toolName: string;
|
||||
toolInput: string;
|
||||
toolOutput?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记忆压缩 SSE 事件
|
||||
*/
|
||||
export interface MemoryCompactedEvent {
|
||||
taskId: string;
|
||||
compactedData: CompactedMemory;
|
||||
}
|
||||
@ -9,8 +9,10 @@ import {
|
||||
UserMessage,
|
||||
AiMessage,
|
||||
SystemMessage,
|
||||
ToolExecutionResultMessage
|
||||
ToolExecutionResultMessage,
|
||||
CompactionSummaryMessage
|
||||
} from '../types/chatHistory';
|
||||
import { CompactedMemory, CompactedMessage } from '../types/memory';
|
||||
|
||||
/**
|
||||
* 会话历史管理器
|
||||
@ -23,6 +25,8 @@ export class ChatHistoryManager {
|
||||
private currentProjectPath: string | null = null;
|
||||
// 存储每个面板的任务信息(taskId 和 projectPath)
|
||||
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
|
||||
// 追踪压缩后产生的新消息
|
||||
private newMessagesSinceCompaction: CompactedMessage[] = [];
|
||||
|
||||
private constructor() {
|
||||
// 设置存储路径: ~/.iccoder
|
||||
@ -690,4 +694,203 @@ export class ChatHistoryManager {
|
||||
hasMore: end < total
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 压缩数据相关方法 ==========
|
||||
|
||||
/**
|
||||
* 保存压缩数据(存入 conversation.json 作为压缩摘要消息)
|
||||
*/
|
||||
public async saveCompactedData(compacted: CompactedMemory): Promise<void> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === compacted.taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取现有对话历史
|
||||
const taskDir = this.getTaskDir(projectPath, compacted.taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
let messages: ChatMessage[] = [];
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
messages = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
} catch {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
summary: compacted.summary,
|
||||
version: compacted.version,
|
||||
compactedAt: compacted.compactedAt,
|
||||
originalMessageCount: compacted.originalMessageCount,
|
||||
compactedMessageCount: compacted.compactedMessageCount
|
||||
};
|
||||
|
||||
// 添加到对话历史
|
||||
messages.push(summaryMessage);
|
||||
|
||||
// 保存
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8');
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
|
||||
// 重置新消息追踪
|
||||
this.newMessagesSinceCompaction = [];
|
||||
|
||||
console.log(`[ChatHistoryManager] 压缩摘要已保存到 conversation.json: taskId=${compacted.taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载压缩数据(从 conversation.json 构建)
|
||||
*/
|
||||
public async loadCompactedData(taskId: string): Promise<CompactedMemory | null> {
|
||||
// 尝试从多个来源获取 projectPath
|
||||
let projectPath = this.currentProjectPath;
|
||||
|
||||
if (!projectPath) {
|
||||
for (const [, taskInfo] of this.panelTaskMap) {
|
||||
if (taskInfo.taskId === taskId) {
|
||||
projectPath = taskInfo.projectPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
console.log('[ChatHistoryManager] loadCompactedData: projectPath 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取 conversation.json
|
||||
const taskDir = this.getTaskDir(projectPath, taskId);
|
||||
const conversationPath = path.join(taskDir, 'conversation.json');
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(conversationPath);
|
||||
const content = await vscode.workspace.fs.readFile(uri);
|
||||
const messages: ChatMessage[] = JSON.parse(Buffer.from(content).toString('utf-8'));
|
||||
|
||||
if (messages.length === 0) {
|
||||
console.log('[ChatHistoryManager] conversation.json 为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从 conversation.json 构建 CompactedMemory
|
||||
return this.buildCompactedMemoryFromConversation(taskId, messages);
|
||||
} catch {
|
||||
console.log('[ChatHistoryManager] conversation.json 不存在:', conversationPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 conversation.json 构建 CompactedMemory
|
||||
*/
|
||||
private buildCompactedMemoryFromConversation(taskId: string, messages: ChatMessage[]): CompactedMemory {
|
||||
// 查找最后一个压缩摘要消息
|
||||
let lastSummary: CompactionSummaryMessage | null = null;
|
||||
let summaryIndex = -1;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
lastSummary = messages[i] as CompactionSummaryMessage;
|
||||
summaryIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取摘要后的消息(或全部消息)
|
||||
const recentMessages = summaryIndex >= 0
|
||||
? messages.slice(summaryIndex + 1)
|
||||
: messages;
|
||||
|
||||
// 转换为 CompactedMessage 格式
|
||||
const compactedMessages: CompactedMessage[] = recentMessages.map(msg => ({
|
||||
type: this.mapMessageType(msg.type),
|
||||
content: this.extractMessageContent(msg)
|
||||
}));
|
||||
|
||||
return {
|
||||
taskId,
|
||||
version: lastSummary?.version || Date.now(),
|
||||
compactedAt: lastSummary?.compactedAt || new Date().toISOString(),
|
||||
summary: lastSummary?.summary || '',
|
||||
recentMessages: compactedMessages,
|
||||
originalMessageCount: messages.length,
|
||||
compactedMessageCount: compactedMessages.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射消息类型
|
||||
*/
|
||||
private mapMessageType(type: MessageType): 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT' {
|
||||
switch (type) {
|
||||
case MessageType.USER: return 'USER';
|
||||
case MessageType.AI: return 'AI';
|
||||
case MessageType.SYSTEM: return 'SYSTEM';
|
||||
case MessageType.TOOL_EXECUTION_RESULT: return 'TOOL_RESULT';
|
||||
default: return 'USER';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息内容
|
||||
*/
|
||||
private extractMessageContent(msg: ChatMessage): string {
|
||||
switch (msg.type) {
|
||||
case MessageType.USER:
|
||||
return (msg as UserMessage).contents?.[0]?.text || '';
|
||||
case MessageType.AI:
|
||||
return (msg as AiMessage).text || '';
|
||||
case MessageType.SYSTEM:
|
||||
return (msg as SystemMessage).text || '';
|
||||
case MessageType.TOOL_EXECUTION_RESULT:
|
||||
return (msg as ToolExecutionResultMessage).text || '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取压缩后产生的新消息
|
||||
*/
|
||||
public getNewMessagesSinceCompaction(): CompactedMessage[] {
|
||||
return this.newMessagesSinceCompaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(用户消息)
|
||||
*/
|
||||
public trackUserMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'USER',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(AI消息)
|
||||
*/
|
||||
public trackAiMessage(text: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'AI',
|
||||
content: text
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
|
||||
import type { RunMode } from '../types/api';
|
||||
import type { RunMode } from "../types/api";
|
||||
|
||||
/** 是否使用后端服务(可通过配置控制) */
|
||||
let useBackendService = true;
|
||||
@ -27,12 +27,15 @@ let useBackendService = true;
|
||||
/** 当前对话会话 */
|
||||
let currentSession: DialogSession | null = null;
|
||||
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
planTitle: string;
|
||||
extensionPath: string;
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
@ -45,7 +48,7 @@ export function setPendingPlanExecution(
|
||||
taskId: string
|
||||
): void {
|
||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||
console.log('[MessageHandler] 设置待执行计划:', planTitle, 'taskId:', taskId);
|
||||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,29 +93,28 @@ export async function handleUserMessage(
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用,回退到本地模式:", error);
|
||||
// 后端不可用时,使用本地模拟回复
|
||||
console.error("后端服务不可用:", error);
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "后端服务不可用",
|
||||
type: "error",
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 本地模拟回复(后端不可用时的 fallback)
|
||||
console.log("使用本地模拟回复");
|
||||
const reply = getMockReply(text);
|
||||
|
||||
// 记录AI回复到历史(允许失败)
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
await historyManager.addAiMessage(reply);
|
||||
} catch (error) {
|
||||
console.warn("记录AI回复历史失败:", error);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: reply,
|
||||
});
|
||||
}, 500);
|
||||
// 如果没有 extensionPath,显示错误
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "无法处理消息:缺少必要参数",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,13 +125,15 @@ async function handleUserMessageWithBackend(
|
||||
text: string,
|
||||
extensionPath: string,
|
||||
mode?: RunMode,
|
||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
): Promise<void> {
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
if (reuseTaskId) {
|
||||
console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId);
|
||||
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,117 +147,150 @@ async function handleUserMessageWithBackend(
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
currentSession!.sendMessage(text, {
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
currentSession!.sendMessage(
|
||||
text,
|
||||
{
|
||||
onText: (fullText, isStreaming) => {
|
||||
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
|
||||
},
|
||||
|
||||
onSegmentUpdate: (segments) => {
|
||||
// 实时发送段落更新,按后端返回顺序展示
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
});
|
||||
},
|
||||
|
||||
onToolStart: (toolName) => {
|
||||
// 更新状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: `正在执行 ${toolName}...`,
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onToolComplete: (toolName, result) => {
|
||||
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onToolError: (toolName, error) => {
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "等待用户回答...",
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onComplete: async (segments) => {
|
||||
// 隐藏状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
|
||||
console.log(
|
||||
"[MessageHandler] segments 内容:",
|
||||
JSON.stringify(segments)
|
||||
);
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
isComplete: true,
|
||||
});
|
||||
console.log("[MessageHandler] postMessage 返回值:", result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const {
|
||||
panel: execPanel,
|
||||
planTitle,
|
||||
extensionPath: execPath,
|
||||
taskId: reuseTaskId,
|
||||
} = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log(
|
||||
"[MessageHandler] 自动执行计划:",
|
||||
planTitle,
|
||||
"复用 taskId:",
|
||||
reuseTaskId
|
||||
);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
"agent",
|
||||
reuseTaskId // 复用 Plan 模式的 taskId
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
onError: (message) => {
|
||||
panel.webview.postMessage({
|
||||
command: "hideLoading",
|
||||
});
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
text: `❌ 错误: ${message}`,
|
||||
});
|
||||
// 恢复输入状态
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: [],
|
||||
isComplete: true,
|
||||
});
|
||||
reject(new Error(message));
|
||||
},
|
||||
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
|
||||
onContextUsage: (data) => {
|
||||
// 发送上下文使用量到 WebView
|
||||
panel.webview.postMessage({
|
||||
command: "contextUsage",
|
||||
currentTokens: data.currentTokens,
|
||||
maxTokens: data.maxTokens,
|
||||
percentage: data.percentage,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
onSegmentUpdate: (segments) => {
|
||||
// 实时发送段落更新,按后端返回顺序展示
|
||||
panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
});
|
||||
},
|
||||
|
||||
onToolStart: (toolName) => {
|
||||
// 更新状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: `正在执行 ${toolName}...`,
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onToolComplete: (toolName, result) => {
|
||||
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onToolError: (toolName, error) => {
|
||||
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
|
||||
},
|
||||
|
||||
onQuestion: (askId, question, options) => {
|
||||
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
|
||||
panel.webview.postMessage({
|
||||
command: "updateStatus",
|
||||
text: "等待用户回答...",
|
||||
type: "working",
|
||||
});
|
||||
},
|
||||
|
||||
onComplete: async (segments) => {
|
||||
// 隐藏状态栏
|
||||
panel.webview.postMessage({
|
||||
command: "hideStatus",
|
||||
});
|
||||
|
||||
// 最后一次发送完整的段落
|
||||
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
|
||||
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
|
||||
|
||||
const result = await panel.webview.postMessage({
|
||||
command: "updateSegments",
|
||||
segments: segments,
|
||||
isComplete: true,
|
||||
});
|
||||
console.log('[MessageHandler] postMessage 返回值:', result);
|
||||
|
||||
// 保存完整的 segments 到历史记录
|
||||
try {
|
||||
// 将完整的 segments 保存到一条 AI 消息中
|
||||
// 这样加载时可以完整还原对话样式
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
.map(s => s.content)
|
||||
.join('\n');
|
||||
|
||||
await historyManager.addAiMessage(textContent, undefined, segments);
|
||||
} catch (error) {
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const { panel: execPanel, planTitle, extensionPath: execPath, taskId: reuseTaskId } = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log('[MessageHandler] 自动执行计划:', planTitle, '复用 taskId:', reuseTaskId);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
'agent',
|
||||
reuseTaskId // 复用 Plan 模式的 taskId
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[MessageHandler] 自动执行计划失败:', err);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
}, mode);
|
||||
mode
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -273,7 +310,35 @@ export async function handleUserAnswer(
|
||||
/**
|
||||
* 中止当前对话
|
||||
*/
|
||||
export function abortCurrentDialog(): void {
|
||||
export async function abortCurrentDialog(): Promise<void> {
|
||||
if (currentSession) {
|
||||
// 保存当前已有的对话内容
|
||||
const segments = currentSession.getSegments();
|
||||
if (segments && segments.length > 0) {
|
||||
try {
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
const textContent = segments
|
||||
.filter((s) => s.type === "text" && s.content)
|
||||
.map((s) => s.content)
|
||||
.join("\n");
|
||||
|
||||
// 添加中止标记
|
||||
const abortedContent = textContent + "\n\n[对话已被用户中止]";
|
||||
await historyManager.addAiMessage(abortedContent, undefined, segments);
|
||||
console.log("[MessageHandler] 已保存中止前的对话内容");
|
||||
} catch (error) {
|
||||
console.warn("[MessageHandler] 保存中止对话失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知 WebView 重置分段消息容器
|
||||
const panel = userInteractionManager.getWebviewPanel();
|
||||
if (panel) {
|
||||
panel.webview.postMessage({ command: "resetSegmentedMessage" });
|
||||
console.log("[MessageHandler] 已发送重置分段消息命令");
|
||||
}
|
||||
|
||||
dialogManager.abortCurrentSession();
|
||||
currentSession = null;
|
||||
}
|
||||
@ -282,7 +347,15 @@ export function abortCurrentDialog(): void {
|
||||
* 获取当前会话的 taskId
|
||||
*/
|
||||
export function getCurrentTaskId(): string | null {
|
||||
return currentSession?.getTaskId() || null;
|
||||
return currentSession?.getTaskId() || lastTaskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最后的 taskId(加载历史会话时调用)
|
||||
*/
|
||||
export function setLastTaskId(taskId: string): void {
|
||||
lastTaskId = taskId;
|
||||
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,52 +371,52 @@ export async function handlePlanAction(
|
||||
planTitle: string,
|
||||
extensionPath: string
|
||||
): Promise<void> {
|
||||
console.log('[handlePlanAction] action:', action, 'planTitle:', planTitle);
|
||||
console.log("[handlePlanAction] action:", action, "planTitle:", planTitle);
|
||||
|
||||
switch (action) {
|
||||
case 'confirm':
|
||||
case "confirm":
|
||||
// 确认执行:切换到 Agent 模式并发送执行消息
|
||||
panel.webview.postMessage({
|
||||
command: 'switchMode',
|
||||
mode: 'agent'
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 发送执行消息
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
'agent'
|
||||
"agent"
|
||||
);
|
||||
break;
|
||||
|
||||
case 'modify':
|
||||
case "modify":
|
||||
// 修改计划:提示用户输入修改建议
|
||||
const modification = await vscode.window.showInputBox({
|
||||
prompt: '请输入您对计划的修改建议',
|
||||
placeHolder: '例如:第2步需要先检查文件是否存在...',
|
||||
ignoreFocusOut: true
|
||||
prompt: "请输入您对计划的修改建议",
|
||||
placeHolder: "例如:第2步需要先检查文件是否存在...",
|
||||
ignoreFocusOut: true,
|
||||
});
|
||||
if (modification) {
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
'plan'
|
||||
"plan"
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
case "cancel":
|
||||
// 取消计划:通知用户
|
||||
panel.webview.postMessage({
|
||||
command: 'addMessage',
|
||||
text: '计划已取消。',
|
||||
sender: 'bot'
|
||||
command: "addMessage",
|
||||
text: "计划已取消。",
|
||||
sender: "bot",
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('[handlePlanAction] 未知操作:', action);
|
||||
console.warn("[handlePlanAction] 未知操作:", action);
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,7 +455,9 @@ function parseFileOperation(text: string): {
|
||||
}
|
||||
|
||||
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配)
|
||||
const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/);
|
||||
const renameMatch = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
|
||||
);
|
||||
if (renameMatch) {
|
||||
const oldPath = renameMatch[1].trim();
|
||||
const newPath = renameMatch[2].trim();
|
||||
@ -397,7 +472,9 @@ function parseFileOperation(text: string): {
|
||||
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
|
||||
const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
||||
const replaceMatch1 = lowerText.match(
|
||||
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
);
|
||||
if (replaceMatch1) {
|
||||
const filePath = replaceMatch1[1].trim();
|
||||
const searchText = replaceMatch1[2].trim();
|
||||
@ -411,7 +488,9 @@ function parseFileOperation(text: string): {
|
||||
}
|
||||
|
||||
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
|
||||
const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/);
|
||||
const replaceMatch2 = lowerText.match(
|
||||
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
|
||||
);
|
||||
if (replaceMatch2) {
|
||||
const filePath = replaceMatch2[1].trim();
|
||||
const searchText = replaceMatch2[2].trim();
|
||||
@ -739,41 +818,6 @@ export async function handleReplaceInFile(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模拟回复
|
||||
*/
|
||||
function getMockReply(question: string): string {
|
||||
const replies = [
|
||||
`已收到您的问题:"${question}"
|
||||
|
||||
这是一个演示版本,实际需要连接AI服务。
|
||||
|
||||
示例回复:这是一个计数器模板:
|
||||
\`\`\`verilog
|
||||
module counter (
|
||||
input clk,
|
||||
input rst_n,
|
||||
output reg [3:0] count
|
||||
);
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) count <= 0;
|
||||
else count <= count + 1;
|
||||
end
|
||||
endmodule
|
||||
\`\`\``,
|
||||
|
||||
`感谢提问!关于"${question}",在真实版本中我会:
|
||||
1. 分析您的代码上下文
|
||||
2. 提供优化建议
|
||||
3. 生成完整代码
|
||||
4. 解释设计原理
|
||||
|
||||
当前是演示版,请点击侧边栏按钮快速生成代码。`,
|
||||
];
|
||||
|
||||
return replies[Math.floor(Math.random() * replies.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将代码插入到编辑器
|
||||
*/
|
||||
@ -866,7 +910,8 @@ async function handleVCDGeneration(
|
||||
|
||||
if (!projectCheck.hasTestbench) {
|
||||
errorMsg += "• ❌ 缺少 testbench 文件\n";
|
||||
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
errorMsg +=
|
||||
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
|
||||
} else {
|
||||
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
|
||||
}
|
||||
@ -910,9 +955,7 @@ async function handleVCDGeneration(
|
||||
fileName: fileName,
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`VCD 文件生成成功: ${fileName}`
|
||||
);
|
||||
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
|
||||
} else {
|
||||
panel.webview.postMessage({
|
||||
command: "receiveMessage",
|
||||
|
||||
@ -24,7 +24,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
|
||||
localResourceRoots: [
|
||||
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
@ -39,8 +42,29 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
const iconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
|
||||
);
|
||||
|
||||
// 获取模型图标URI
|
||||
const autoIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
|
||||
);
|
||||
const liteIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
|
||||
);
|
||||
const syIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
|
||||
);
|
||||
const maxIconUri = panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
|
||||
);
|
||||
|
||||
// 设置HTML内容
|
||||
panel.webview.html = getWebviewContent(iconUri.toString());
|
||||
panel.webview.html = getWebviewContent(
|
||||
iconUri.toString(),
|
||||
autoIconUri.toString(),
|
||||
liteIconUri.toString(),
|
||||
syIconUri.toString(),
|
||||
maxIconUri.toString()
|
||||
);
|
||||
|
||||
// 处理消息
|
||||
panel.webview.onDidReceiveMessage(
|
||||
@ -90,7 +114,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
break;
|
||||
// 新增:中止对话
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
void abortCurrentDialog();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@ -29,7 +29,12 @@ import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
|
||||
/**
|
||||
* 获取输入区域的 HTML 内容
|
||||
*/
|
||||
export function getInputAreaContent(): string {
|
||||
export function getInputAreaContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
): string {
|
||||
return `
|
||||
<div class="input-area">
|
||||
<div class="input-group">
|
||||
@ -40,13 +45,13 @@ export function getInputAreaContent(): string {
|
||||
</div>
|
||||
<textarea
|
||||
id="messageInput"
|
||||
placeholder="输入您的问题..."
|
||||
placeholder="输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
||||
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
|
||||
></textarea>
|
||||
<div class="input-bottom-row">
|
||||
<div class="mode-selector">
|
||||
${getModeSelectorContent()}
|
||||
${getModelSelectorContent()}
|
||||
${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
|
||||
</div>
|
||||
<div class="input-actions">
|
||||
${getContextCompressContent()}
|
||||
@ -197,6 +202,11 @@ export function getInputAreaStyles(): string {
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
textarea:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
/* 简洁的滚动条样式 */
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@ -300,11 +310,17 @@ export function getInputAreaScript(): string {
|
||||
sendIconContainer.style.display = 'none';
|
||||
stopIconContainer.style.display = 'block';
|
||||
isConversationActive = true;
|
||||
// 禁用输入框
|
||||
messageInput.disabled = true;
|
||||
messageInput.placeholder = '正在处理中,请稍候...';
|
||||
} else {
|
||||
sendButton.classList.remove('sending');
|
||||
sendIconContainer.style.display = 'block';
|
||||
stopIconContainer.style.display = 'none';
|
||||
isConversationActive = false;
|
||||
// 启用输入框
|
||||
messageInput.disabled = false;
|
||||
messageInput.placeholder = '输入您的问题,按 Enter 发送,Shift + Enter 换行...';
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,6 +340,11 @@ export function getInputAreaScript(): string {
|
||||
const text = messageInput.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
// 如果正在对话中,阻止发送新消息
|
||||
if (isConversationActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查工作区状态
|
||||
if (!hasWorkspace) {
|
||||
// 如果没有工作区,阻止发送并清空输入框
|
||||
|
||||
@ -1097,9 +1097,11 @@ export function getMessageAreaScript(): string {
|
||||
console.log('[WebView] 对话完成,添加操作按钮');
|
||||
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.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
@ -1107,7 +1109,22 @@ export function getMessageAreaScript(): string {
|
||||
.join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
|
||||
// 点赞按钮
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
|
||||
// 点踩按钮
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
|
||||
// 重置当前分段消息容器
|
||||
@ -1241,9 +1258,11 @@ export function getMessageAreaScript(): string {
|
||||
// 添加操作按钮
|
||||
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.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments
|
||||
.filter(s => s.type === 'text' && s.content)
|
||||
@ -1251,7 +1270,22 @@ export function getMessageAreaScript(): string {
|
||||
.join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
|
||||
// 点赞按钮
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
|
||||
// 点踩按钮
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
container.appendChild(actionsDiv);
|
||||
|
||||
messagesEl.appendChild(container);
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
/**
|
||||
* 获取模型选择器的 HTML 内容
|
||||
*/
|
||||
export function getModelSelectorContent(): string {
|
||||
export function getModelSelectorContent(
|
||||
autoIcon: string = '',
|
||||
liteIcon: string = '',
|
||||
syIcon: string = '',
|
||||
maxIcon: string = ''
|
||||
): string {
|
||||
return `
|
||||
<!-- 模型选择 -->
|
||||
<div class="tooltip">
|
||||
@ -17,10 +22,22 @@ export function getModelSelectorContent(): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="select-dropdown" id="modelDropdown">
|
||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">Lite</div>
|
||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">Auto</div>
|
||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">Syntaxic</div>
|
||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">Max</div>
|
||||
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">
|
||||
${autoIcon ? `<img src="${autoIcon}" class="model-icon" alt="Auto">` : ''}
|
||||
<span class="option-label">Auto</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">
|
||||
${liteIcon ? `<img src="${liteIcon}" class="model-icon" alt="Lite">` : ''}
|
||||
<span class="option-label">Lite</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">
|
||||
${syIcon ? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">` : ''}
|
||||
<span class="option-label">Syntaxic</span>
|
||||
</div>
|
||||
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">
|
||||
${maxIcon ? `<img src="${maxIcon}" class="model-icon" alt="Max">` : ''}
|
||||
<span class="option-label">Max</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择器的 tooltip 容器 -->
|
||||
<div id="modelTooltip" class="model-tooltip"></div>
|
||||
@ -88,10 +105,12 @@ export function getModelSelectorStyles(): string {
|
||||
#modelDropdown .select-option {
|
||||
position: relative;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
#modelDropdown .select-option:hover {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
@ -100,6 +119,17 @@ export function getModelSelectorStyles(): string {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.model-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.option-label {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 模型选择器的 tooltip 样式 */
|
||||
.model-tooltip {
|
||||
position: fixed;
|
||||
|
||||
@ -17,14 +17,17 @@ import {
|
||||
getMessageAreaStyles,
|
||||
getMessageAreaScript,
|
||||
} from "./messageArea";
|
||||
import {
|
||||
getAgentCardStyles,
|
||||
getAgentCardScript,
|
||||
} from "./agentCard";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
*/
|
||||
export function getWebviewContent(iconUri?: string): string {
|
||||
export function getWebviewContent(
|
||||
iconUri?: string,
|
||||
autoIconUri?: string,
|
||||
liteIconUri?: string,
|
||||
syIconUri?: string,
|
||||
maxIconUri?: string
|
||||
): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@ -394,14 +397,14 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
<span id="statusText">思考中...</span>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<!-- <div class="quick-actions">
|
||||
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
|
||||
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
|
||||
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
|
||||
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
${getInputAreaContent()}
|
||||
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@ -565,6 +568,19 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'resetSegmentedMessage':
|
||||
// 重置分段消息容器(停止对话时调用)
|
||||
console.log('[WebView] 重置分段消息容器');
|
||||
currentSegmentedMessage = null;
|
||||
break;
|
||||
|
||||
case 'contextUsage':
|
||||
// 更新上下文使用量显示
|
||||
if (typeof updateContextDisplay === 'function') {
|
||||
updateContextDisplay(message.currentTokens, message.maxTokens);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'hideLoading':
|
||||
// 隐藏加载指示器
|
||||
hideLoadingIndicator();
|
||||
|
||||
Reference in New Issue
Block a user