- 新增个人规则管理模块 (personalRulesManager.ts) - 支持创建、编辑、删除多条规则 - 规则存储在用户目录 ~/.iccoder/rules/ - 对话时自动将规则传递给后端 - 添加后端对接文档和 webpack 优化指南
1175 lines
36 KiB
TypeScript
1175 lines
36 KiB
TypeScript
/**
|
||
* 对话服务
|
||
* 整合 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,
|
||
ServiceTier,
|
||
ToolConfirmEvent,
|
||
PlanConfirmEvent,
|
||
} from "../types/api";
|
||
import { submitToolConfirm, submitAnswer, stopDialog } from "./apiClient";
|
||
import { getActiveRules } from "../utils/personalRulesManager";
|
||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||
import { getUserIdFromToken, isTokenExpired } from "../utils/jwtUtils";
|
||
import { updateCachedBalance } from "./creditsService";
|
||
|
||
/**
|
||
* 消息段落类型
|
||
*/
|
||
export interface MessageSegment {
|
||
type: "text" | "tool" | "question" | "agent" | "plan" | "progress";
|
||
content?: string;
|
||
toolName?: string;
|
||
toolStatus?: "running" | "success" | "error";
|
||
toolResult?: string;
|
||
toolDescription?: string;
|
||
askId?: string;
|
||
questions?: import("../types/api").QuestionItem[];
|
||
// 智能体相关字段
|
||
agentId?: string;
|
||
agentName?: string;
|
||
agentStatus?: "running" | "completed" | "error";
|
||
agentSteps?: AgentStep[];
|
||
// 计划相关字段
|
||
planTitle?: string;
|
||
planPhases?: import("../types/api").PlanPhase[];
|
||
planSteps?: string[];
|
||
planSummary?: string;
|
||
// 进度条相关字段(独立于 plan,用于执行模式)
|
||
progressPhases?: import("../types/api").PlanPhase[];
|
||
}
|
||
|
||
/**
|
||
* 智能体执行步骤
|
||
*/
|
||
export interface AgentStep {
|
||
step: number;
|
||
toolName: string;
|
||
toolInput?: unknown;
|
||
toolResult?: string;
|
||
status: "running" | "completed" | "error";
|
||
}
|
||
|
||
/**
|
||
* 对话回调接口
|
||
*/
|
||
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 模式) */
|
||
onToolConfirm?: (
|
||
confirmId: number,
|
||
toolName: string,
|
||
toolInput: Record<string, unknown>
|
||
) => void;
|
||
/** 计划确认请求(Plan 模式) */
|
||
onPlanConfirm?: (
|
||
confirmId: number,
|
||
title: string,
|
||
phases: import("../types/api").PlanPhase[] | undefined,
|
||
steps: string[] | undefined,
|
||
summary: string
|
||
) => void;
|
||
/** 显示问题(ask_user) */
|
||
onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
|
||
/** 实时更新段落(流式过程中) */
|
||
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
||
/** 对话完成,返回所有段落 */
|
||
onComplete?: (segments: MessageSegment[]) => void;
|
||
/** 错误 */
|
||
onError?: (message: string) => void;
|
||
/** 通知消息 */
|
||
onNotification?: (message: string) => void;
|
||
/** 上下文使用量更新 */
|
||
onContextUsage?: (data: {
|
||
currentTokens: number;
|
||
maxTokens: number;
|
||
percentage: number;
|
||
}) => void;
|
||
/** 阶段进度更新 */
|
||
onPhaseProgress?: (phaseId: string, status: string) => void;
|
||
}
|
||
|
||
/**
|
||
* 对话会话
|
||
*/
|
||
export class DialogSession {
|
||
private taskId: string;
|
||
private sseController: SSEController | null = null;
|
||
private toolContext: ToolExecutorContext;
|
||
private accumulatedText = "";
|
||
private isActive = false;
|
||
private hasCompleted = false; // 标记是否已收到 complete 事件
|
||
private segments: MessageSegment[] = [];
|
||
private currentTextSegment: MessageSegment | null = null;
|
||
private completeCallback: ((segments: MessageSegment[]) => void) | null =
|
||
null; // 保存完成回调,用于 abort 时触发
|
||
|
||
constructor(extensionPath: string, existingTaskId?: string) {
|
||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||
this.taskId = existingTaskId || generateTaskId();
|
||
this.toolContext = createToolExecutorContext(extensionPath);
|
||
}
|
||
|
||
/**
|
||
* 添加文本到当前文本段落
|
||
*/
|
||
private appendText(text: string): void {
|
||
if (!this.currentTextSegment) {
|
||
this.currentTextSegment = { type: "text", content: "" };
|
||
this.segments.push(this.currentTextSegment);
|
||
}
|
||
this.currentTextSegment.content =
|
||
(this.currentTextSegment.content || "") + text;
|
||
}
|
||
|
||
/**
|
||
* 结束当前文本段落
|
||
*/
|
||
private finalizeTextSegment(): void {
|
||
this.currentTextSegment = null;
|
||
}
|
||
|
||
/**
|
||
* 添加工具段落
|
||
*/
|
||
private addToolSegment(
|
||
toolName: string,
|
||
status: "running" | "success" | "error",
|
||
result?: string
|
||
): MessageSegment {
|
||
this.finalizeTextSegment();
|
||
const segment: MessageSegment = {
|
||
type: "tool",
|
||
toolName,
|
||
toolStatus: status,
|
||
toolResult: result,
|
||
};
|
||
this.segments.push(segment);
|
||
return segment;
|
||
}
|
||
|
||
/**
|
||
* 更新工具段落状态
|
||
*/
|
||
private updateToolSegment(
|
||
toolName: string,
|
||
status: "success" | "error",
|
||
result?: string,
|
||
description?: string
|
||
): void {
|
||
// 找到最后一个匹配的工具段落
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (
|
||
seg.type === "tool" &&
|
||
seg.toolName === toolName &&
|
||
seg.toolStatus === "running"
|
||
) {
|
||
seg.toolStatus = status;
|
||
seg.toolResult = result;
|
||
if (description !== undefined) {
|
||
seg.toolDescription = description;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取任务ID
|
||
*/
|
||
getTaskId(): string {
|
||
return this.taskId;
|
||
}
|
||
|
||
/**
|
||
* 是否活跃
|
||
*/
|
||
get active(): boolean {
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 获取工具操作描述(用于确认对话框)
|
||
*/
|
||
private getToolDescription(
|
||
toolName: string,
|
||
toolInput: Record<string, unknown>
|
||
): string {
|
||
const lines: string[] = [];
|
||
|
||
switch (toolName) {
|
||
case "file_write":
|
||
lines.push(`文件路径: ${toolInput.path || "未知"}`);
|
||
if (toolInput.content) {
|
||
const content = String(toolInput.content);
|
||
lines.push(`内容长度: ${content.length} 字符`);
|
||
lines.push(
|
||
`内容预览: ${content.substring(0, 100)}${
|
||
content.length > 100 ? "..." : ""
|
||
}`
|
||
);
|
||
}
|
||
break;
|
||
case "file_delete":
|
||
lines.push(`删除文件: ${toolInput.path || "未知"}`);
|
||
break;
|
||
case "syntax_check":
|
||
lines.push("执行语法检查");
|
||
if (toolInput.code) {
|
||
const code = String(toolInput.code);
|
||
lines.push(`代码长度: ${code.length} 字符`);
|
||
}
|
||
break;
|
||
case "simulation":
|
||
lines.push(`RTL文件: ${toolInput.rtlPath || "未知"}`);
|
||
lines.push(`TB文件: ${toolInput.tbPath || "未知"}`);
|
||
if (toolInput.duration) {
|
||
lines.push(`仿真时长: ${toolInput.duration}`);
|
||
}
|
||
break;
|
||
default:
|
||
lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`);
|
||
}
|
||
|
||
return lines.join("\n");
|
||
}
|
||
|
||
/**
|
||
* 发送消息并开始流式对话
|
||
*/
|
||
async sendMessage(
|
||
message: string,
|
||
callbacks: DialogCallbacks,
|
||
mode?: RunMode,
|
||
serviceTier?: ServiceTier // 新增:服务等级参数
|
||
): Promise<void> {
|
||
if (this.isActive) {
|
||
callbacks.onError?.("当前有对话正在进行中");
|
||
return;
|
||
}
|
||
|
||
this.isActive = true;
|
||
this.hasCompleted = false; // 重置完成标志
|
||
this.accumulatedText = "";
|
||
this.segments = [];
|
||
this.currentTextSegment = null;
|
||
this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发
|
||
|
||
const config = getConfig();
|
||
|
||
// 从登录 session 获取真实 userId 和 token
|
||
let userId = config.userId; // 默认值
|
||
let token: string | undefined;
|
||
try {
|
||
console.log("[DialogSession] 尝试获取登录 session...");
|
||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||
silent: true,
|
||
});
|
||
console.log(
|
||
"[DialogSession] session 结果:",
|
||
session ? "已获取" : "null/undefined"
|
||
);
|
||
if (session?.accessToken) {
|
||
console.log(
|
||
"[DialogSession] accessToken 长度:",
|
||
session.accessToken.length
|
||
);
|
||
|
||
// 检测 token 是否过期
|
||
const expired = isTokenExpired(session.accessToken);
|
||
if (expired === true) {
|
||
console.error("[DialogSession] token 已过期,需要重新登录");
|
||
vscode.window
|
||
.showErrorMessage("登录已过期,请重新登录", "重新登录")
|
||
.then((selection) => {
|
||
if (selection === "重新登录") {
|
||
vscode.commands.executeCommand("ic-coder.login", {
|
||
forceReauth: true,
|
||
});
|
||
}
|
||
});
|
||
throw new Error("登录已过期,请重新登录");
|
||
}
|
||
|
||
token = session.accessToken; // 保存 token 用于扣费
|
||
const parsedUserId = getUserIdFromToken(session.accessToken);
|
||
console.log("[DialogSession] 解析的 userId:", parsedUserId);
|
||
if (parsedUserId) {
|
||
userId = parsedUserId;
|
||
console.log("[DialogSession] 使用真实 userId:", userId);
|
||
}
|
||
} else {
|
||
console.log(
|
||
"[DialogSession] 未获取到 accessToken,使用默认 userId:",
|
||
userId
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.warn("[DialogSession] 获取登录 session 失败:", error);
|
||
}
|
||
|
||
// 获取压缩数据和新消息(用于后端重启后恢复)
|
||
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"
|
||
);
|
||
|
||
console.log(
|
||
"[DialogSession] serviceTier 参数:",
|
||
serviceTier,
|
||
"-> 使用:",
|
||
serviceTier || config.serviceTier
|
||
);
|
||
|
||
const request: DialogRequest = {
|
||
taskId: this.taskId,
|
||
message,
|
||
userId,
|
||
mode: mode || "agent",
|
||
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
|
||
token, // JWT token 用于扣费
|
||
compactedData: compactedData || undefined,
|
||
newMessages: newMessages.length > 0 ? newMessages : undefined,
|
||
knowledgeData: knowledgeData || undefined,
|
||
personalRules: getActiveRules() || undefined,
|
||
};
|
||
|
||
// 追踪用户消息
|
||
historyManager.trackUserMessage(message);
|
||
|
||
const sseCallbacks: SSECallbacks = {
|
||
onTextDelta: (data) => {
|
||
this.accumulatedText += data.text;
|
||
this.appendText(data.text);
|
||
console.log(
|
||
"[DialogSession] onTextDelta, 累积文本长度:",
|
||
this.accumulatedText.length
|
||
);
|
||
callbacks.onText?.(this.accumulatedText, true);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onToolCall: async (data: ToolCallRequest) => {
|
||
const toolName = data.params.name;
|
||
console.log("[DialogSession] onToolCall:", toolName);
|
||
|
||
// 检查是否有活跃的智能体(如果有,工具执行会显示在智能体卡片内,不需要单独显示)
|
||
const hasActiveAgent = this.segments.some(
|
||
(s) => s.type === "agent" && s.agentStatus === "running"
|
||
);
|
||
|
||
if (hasActiveAgent) {
|
||
console.log(
|
||
"[DialogSession] onToolCall: 智能体执行中,跳过工具段落:",
|
||
toolName
|
||
);
|
||
} else {
|
||
// 检查是否已经有相同的工具段落(可能由 onToolStart 添加)
|
||
const lastToolSegment = this.segments
|
||
.filter((s) => s.type === "tool")
|
||
.pop();
|
||
if (
|
||
lastToolSegment &&
|
||
lastToolSegment.toolName === toolName &&
|
||
lastToolSegment.toolStatus === "running"
|
||
) {
|
||
console.log(
|
||
"[DialogSession] onToolCall: 跳过重复的工具段落:",
|
||
toolName
|
||
);
|
||
} else {
|
||
this.addToolSegment(toolName, "running");
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
}
|
||
|
||
// 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复
|
||
try {
|
||
await executeToolCall(data, this.toolContext);
|
||
if (!hasActiveAgent) {
|
||
this.updateToolSegment(toolName, "success", "执行完成");
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
// 也不调用 callbacks.onToolComplete,避免重复
|
||
} catch (error) {
|
||
const errorMsg = error instanceof Error ? error.message : "未知错误";
|
||
if (!hasActiveAgent) {
|
||
this.updateToolSegment(toolName, "error", errorMsg);
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
callbacks.onToolError?.(toolName, errorMsg);
|
||
}
|
||
},
|
||
|
||
onToolStart: (data) => {
|
||
console.log("[DialogSession] onToolStart:", data.tool_name);
|
||
// 检查是否已经有相同的工具段落(可能由 onToolCall 添加)
|
||
const lastToolSegment = this.segments
|
||
.filter((s) => s.type === "tool")
|
||
.pop();
|
||
if (
|
||
lastToolSegment &&
|
||
lastToolSegment.toolName === data.tool_name &&
|
||
lastToolSegment.toolStatus === "running"
|
||
) {
|
||
console.log("[DialogSession] 跳过重复的工具段落:", data.tool_name);
|
||
} else {
|
||
this.addToolSegment(data.tool_name, "running");
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
console.log("[DialogSession] segments 数量:", this.segments.length);
|
||
callbacks.onToolStart?.(data.tool_name);
|
||
},
|
||
|
||
onToolComplete: (data) => {
|
||
this.updateToolSegment(data.tool_name, "success", data.result, data.description);
|
||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
// 追踪工具执行结果(用于后端重启后恢复)
|
||
historyManager.trackToolResult(data.tool_name, data.result);
|
||
},
|
||
|
||
onToolError: (data) => {
|
||
this.updateToolSegment(data.tool_name, "error", data.error);
|
||
callbacks.onToolError?.(data.tool_name, data.error);
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
// 追踪工具执行错误(用于后端重启后恢复)
|
||
historyManager.trackToolResult(data.tool_name, `[错误] ${data.error}`);
|
||
},
|
||
|
||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||
console.log(
|
||
"[DialogSession] onToolConfirm:",
|
||
data.toolName,
|
||
data.confirmId
|
||
);
|
||
|
||
// 结束当前文本段落
|
||
this.finalizeTextSegment();
|
||
|
||
// 生成工具描述
|
||
const toolDescription = this.getToolDescription(
|
||
data.toolName,
|
||
data.toolInput
|
||
);
|
||
|
||
// 构建问题文本
|
||
const toolNameMap: Record<string, string> = {
|
||
file_write: "写入文件",
|
||
file_delete: "删除文件",
|
||
syntax_check: "语法检查",
|
||
simulation: "运行仿真",
|
||
};
|
||
const toolDisplayName = toolNameMap[data.toolName] || data.toolName;
|
||
const question = `确认执行操作:${toolDisplayName}\n\n${toolDescription}`;
|
||
|
||
// 生成唯一的 askId
|
||
const askId = `tool_confirm_${data.confirmId}`;
|
||
|
||
// 添加问题段落到聊天界面
|
||
this.segments.push({
|
||
type: "question",
|
||
askId: askId,
|
||
questions: [{
|
||
question: question,
|
||
options: ["确认执行", "取消"],
|
||
multiSelect: false
|
||
}],
|
||
});
|
||
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
|
||
// 调用回调通知 UI
|
||
callbacks.onToolConfirm?.(
|
||
data.confirmId,
|
||
data.toolName,
|
||
data.toolInput
|
||
);
|
||
|
||
// 使用 userInteractionManager 等待用户回答
|
||
try {
|
||
await userInteractionManager.handleAskUser(
|
||
{
|
||
askId: askId,
|
||
questions: [{
|
||
question: question,
|
||
options: ["确认执行", "取消"],
|
||
multiSelect: false
|
||
}]
|
||
} as AskUserEvent,
|
||
this.taskId
|
||
);
|
||
|
||
// 注意:用户回答后,需要在 receiveAnswer 中处理 tool_confirm 类型的 askId
|
||
// 这里不直接调用 submitToolConfirm,而是在 userInteractionManager 中统一处理
|
||
} catch (error) {
|
||
console.error("[DialogSession] 处理工具确认失败:", error);
|
||
// 如果出错,默认取消执行
|
||
try {
|
||
await submitToolConfirm({
|
||
confirmId: data.confirmId,
|
||
taskId: this.taskId,
|
||
approved: false,
|
||
});
|
||
} catch (submitError) {
|
||
console.error("[DialogSession] 发送取消响应失败:", submitError);
|
||
}
|
||
}
|
||
},
|
||
|
||
onPlanConfirm: async (data: PlanConfirmEvent) => {
|
||
console.log("[DialogSession] onPlanConfirm:", data.title);
|
||
|
||
// 结束当前文本段落
|
||
this.finalizeTextSegment();
|
||
|
||
const askId = `ask_${data.confirmId}`;
|
||
|
||
// 添加计划段落到聊天界面(包含 askId 用于响应)
|
||
// 支持新格式(phases)和旧格式(steps)
|
||
this.segments.push({
|
||
type: "plan",
|
||
askId: askId,
|
||
planTitle: data.title,
|
||
planPhases: data.phases,
|
||
planSteps: data.steps,
|
||
planSummary: data.summary,
|
||
});
|
||
|
||
// 实时发送段落更新
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
|
||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||
const planEvent = {
|
||
askId: askId,
|
||
questions: [{
|
||
question: `请确认执行计划:${data.title}`,
|
||
options: ["确认执行", "修改计划", "取消"],
|
||
multiSelect: false
|
||
}]
|
||
};
|
||
try {
|
||
await userInteractionManager.handleAskUser(
|
||
planEvent as AskUserEvent,
|
||
this.taskId
|
||
);
|
||
} catch (error) {
|
||
console.error("[DialogSession] 处理计划确认失败:", error);
|
||
}
|
||
|
||
// 调用回调通知 UI
|
||
callbacks.onPlanConfirm?.(
|
||
data.confirmId,
|
||
data.title,
|
||
data.phases,
|
||
data.steps,
|
||
data.summary
|
||
);
|
||
},
|
||
|
||
onPhaseProgress: (data: import("../types/api").PhaseProgressEvent) => {
|
||
console.log(
|
||
"[DialogSession] onPhaseProgress:",
|
||
data.phaseId,
|
||
data.status
|
||
);
|
||
|
||
// 1. 尝试更新 plan segment(兼容旧逻辑)
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (seg.type === "plan" && seg.planPhases) {
|
||
seg.planPhases = seg.planPhases.map((phase) => {
|
||
if (phase.id === data.phaseId) {
|
||
return { ...phase, status: data.status };
|
||
}
|
||
return phase;
|
||
});
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 2. 通知外部更新独立进度条
|
||
callbacks.onPhaseProgress?.(data.phaseId, data.status);
|
||
},
|
||
|
||
onPlanStepAdd: (data: import("../types/api").PlanStepAddEvent) => {
|
||
console.log("[DialogSession] onPlanStepAdd:", data.phaseId, data.step);
|
||
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (seg.type === "plan" && seg.planPhases) {
|
||
seg.planPhases = seg.planPhases.map((phase) => {
|
||
if (phase.id === data.phaseId) {
|
||
const newSteps = [...(phase.steps || [])];
|
||
if (data.index >= 0 && data.index < newSteps.length) {
|
||
newSteps.splice(data.index, 0, data.step);
|
||
} else {
|
||
newSteps.push(data.step);
|
||
}
|
||
return { ...phase, steps: newSteps };
|
||
}
|
||
return phase;
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onPlanStepRemove: (data: import("../types/api").PlanStepRemoveEvent) => {
|
||
console.log(
|
||
"[DialogSession] onPlanStepRemove:",
|
||
data.phaseId,
|
||
data.stepIndex
|
||
);
|
||
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (seg.type === "plan" && seg.planPhases) {
|
||
seg.planPhases = seg.planPhases.map((phase) => {
|
||
if (phase.id === data.phaseId && phase.steps) {
|
||
const newSteps = [...phase.steps];
|
||
newSteps.splice(data.stepIndex, 1);
|
||
return { ...phase, steps: newSteps };
|
||
}
|
||
return phase;
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onPlanStepUpdate: (data: import("../types/api").PlanStepUpdateEvent) => {
|
||
console.log(
|
||
"[DialogSession] onPlanStepUpdate:",
|
||
data.phaseId,
|
||
data.stepIndex
|
||
);
|
||
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (seg.type === "plan" && seg.planPhases) {
|
||
seg.planPhases = seg.planPhases.map((phase) => {
|
||
if (phase.id === data.phaseId && phase.steps) {
|
||
const newSteps = [...phase.steps];
|
||
if (data.stepIndex >= 0 && data.stepIndex < newSteps.length) {
|
||
newSteps[data.stepIndex] = data.step;
|
||
}
|
||
return { ...phase, steps: newSteps };
|
||
}
|
||
return phase;
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onPlanSummaryUpdate: (
|
||
data: import("../types/api").PlanSummaryUpdateEvent
|
||
) => {
|
||
console.log("[DialogSession] onPlanSummaryUpdate");
|
||
|
||
for (let i = this.segments.length - 1; i >= 0; i--) {
|
||
const seg = this.segments[i];
|
||
if (seg.type === "plan") {
|
||
seg.planSummary = data.summary;
|
||
break;
|
||
}
|
||
}
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onAskUser: async (data: AskUserEvent) => {
|
||
this.finalizeTextSegment();
|
||
this.segments.push({
|
||
type: "question",
|
||
askId: data.askId,
|
||
questions: data.questions,
|
||
});
|
||
// 实时发送段落更新(包含问题)
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
// 同时调用 onQuestion 用于更新状态栏等
|
||
callbacks.onQuestion?.(data.askId, data.questions);
|
||
try {
|
||
await userInteractionManager.handleAskUser(data, this.taskId);
|
||
} catch (error) {
|
||
console.error("[DialogSession] 处理用户问题失败:", error);
|
||
}
|
||
},
|
||
|
||
onComplete: (data) => {
|
||
this.isActive = false;
|
||
this.hasCompleted = true; // 标记已收到 complete 事件
|
||
this.finalizeTextSegment();
|
||
|
||
// 追踪 AI 消息(用于后端重启后恢复)
|
||
if (this.accumulatedText) {
|
||
historyManager.trackAiMessage(this.accumulatedText);
|
||
}
|
||
|
||
// 发送所有段落
|
||
callbacks.onComplete?.(this.segments);
|
||
},
|
||
|
||
onError: (data) => {
|
||
this.isActive = false;
|
||
|
||
// 检测登录状态过期(只弹一次窗,不再传递错误)
|
||
if (
|
||
data.message.includes("LOGIN_EXPIRED") ||
|
||
data.message.includes("登录状态已过期")
|
||
) {
|
||
vscode.window
|
||
.showErrorMessage("登录状态已过期,请重新登录", "重新登录")
|
||
.then((selection) => {
|
||
if (selection === "重新登录") {
|
||
vscode.commands.executeCommand("ic-coder.login", {
|
||
forceReauth: true,
|
||
});
|
||
}
|
||
});
|
||
// 登录过期错误已处理,不再传递给外部
|
||
return;
|
||
}
|
||
|
||
callbacks.onError?.(data.message);
|
||
},
|
||
|
||
onWarning: (data) => {
|
||
callbacks.onNotification?.(`⚠️ ${data.message}`);
|
||
},
|
||
|
||
onNotification: (data) => {
|
||
callbacks.onNotification?.(data.message);
|
||
},
|
||
|
||
// 智能体事件处理
|
||
onAgentStart: (data) => {
|
||
console.log("[DialogSession] onAgentStart:", data.agentId);
|
||
this.finalizeTextSegment();
|
||
this.segments.push({
|
||
type: "agent",
|
||
agentId: data.agentId,
|
||
agentName: data.agentName,
|
||
content: data.instruction,
|
||
agentStatus: "running",
|
||
agentSteps: [],
|
||
});
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
},
|
||
|
||
onAgentProgress: (data) => {
|
||
console.log(
|
||
"[DialogSession] onAgentProgress:",
|
||
data.agentId,
|
||
data.step,
|
||
data.status
|
||
);
|
||
const agentSegment = this.segments.find(
|
||
(s) => s.type === "agent" && s.agentId === data.agentId
|
||
);
|
||
if (agentSegment && agentSegment.agentSteps) {
|
||
if (data.status === "running") {
|
||
agentSegment.agentSteps.push({
|
||
step: data.step,
|
||
toolName: data.toolName,
|
||
toolInput: data.toolInput,
|
||
status: "running",
|
||
});
|
||
} else {
|
||
const step = agentSegment.agentSteps.find(
|
||
(s) => s.step === data.step
|
||
);
|
||
if (step) {
|
||
step.status = data.status;
|
||
step.toolResult = data.toolResult;
|
||
}
|
||
}
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
onAgentComplete: (data) => {
|
||
console.log("[DialogSession] onAgentComplete:", data.agentId);
|
||
const agentSegment = this.segments.find(
|
||
(s) => s.type === "agent" && s.agentId === data.agentId
|
||
);
|
||
if (agentSegment) {
|
||
agentSegment.agentStatus = "completed";
|
||
agentSegment.content = data.summary;
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
onAgentError: (data) => {
|
||
console.log("[DialogSession] onAgentError:", data.agentId, data.error);
|
||
const agentSegment = this.segments.find(
|
||
(s) => s.type === "agent" && s.agentId === data.agentId
|
||
);
|
||
if (agentSegment) {
|
||
agentSegment.agentStatus = "error";
|
||
agentSegment.content = data.error;
|
||
callbacks.onSegmentUpdate?.(this.segments);
|
||
}
|
||
},
|
||
|
||
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);
|
||
},
|
||
|
||
onCreditUpdate: (data) => {
|
||
console.log(
|
||
"[DialogSession] onCreditUpdate: 扣除",
|
||
data.deductedCredits,
|
||
"剩余",
|
||
data.remainingCredits
|
||
);
|
||
// 更新余额缓存
|
||
updateCachedBalance(data.remainingCredits);
|
||
// 资源点余额低于阈值时弹窗提醒
|
||
const LOW_CREDIT_THRESHOLD = 5;
|
||
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
|
||
vscode.window
|
||
.showWarningMessage(
|
||
`资源点余额不足!当前剩余 ${data.remainingCredits.toFixed(
|
||
2
|
||
)} 点,请及时充值。`,
|
||
"去充值"
|
||
)
|
||
.then((selection) => {
|
||
if (selection === "去充值") {
|
||
// 打开充值页面
|
||
vscode.env.openExternal(
|
||
vscode.Uri.parse("https://iccoder.com/recharge")
|
||
);
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
onOpen: () => {
|
||
console.log("[DialogSession] SSE 连接已建立");
|
||
},
|
||
|
||
onClose: () => {
|
||
console.log("[DialogSession] SSE 连接已关闭");
|
||
// 如果没有收到 complete 事件,需要补充完成逻辑
|
||
if (!this.hasCompleted && this.isActive) {
|
||
console.log("[DialogSession] 未收到 complete 事件,补充完成处理");
|
||
this.finalizeTextSegment();
|
||
if (this.accumulatedText) {
|
||
historyManager.trackAiMessage(this.accumulatedText);
|
||
}
|
||
callbacks.onComplete?.(this.segments);
|
||
}
|
||
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 {
|
||
// 先标记完成,防止 onClose 重复触发
|
||
const wasActive = this.isActive;
|
||
this.hasCompleted = true;
|
||
this.isActive = false;
|
||
|
||
if (this.sseController) {
|
||
this.sseController.abort();
|
||
this.sseController = null;
|
||
}
|
||
userInteractionManager.cancelAll();
|
||
|
||
// 如果之前是活跃状态,触发完成回调以结束 Promise
|
||
if (wasActive && this.completeCallback) {
|
||
this.finalizeTextSegment();
|
||
console.log("[DialogSession] abort 触发完成回调");
|
||
this.completeCallback(this.segments);
|
||
this.completeCallback = null;
|
||
}
|
||
|
||
// 通知后端停止处理
|
||
stopDialog(this.taskId).catch((err) => {
|
||
console.warn("[DialogSession] 停止对话请求失败:", err);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取当前的消息段落(用于中止时保存)
|
||
*/
|
||
getSegments(): MessageSegment[] {
|
||
return this.segments;
|
||
}
|
||
|
||
/**
|
||
* 获取累积的文本内容
|
||
*/
|
||
getAccumulatedText(): string {
|
||
return this.accumulatedText;
|
||
}
|
||
|
||
/**
|
||
* 提交用户回答
|
||
*/
|
||
async submitAnswer(
|
||
askId: string,
|
||
selected?: string[],
|
||
customInput?: string,
|
||
answers?: { [questionIndex: string]: string[] }
|
||
): Promise<void> {
|
||
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||
// 如果 pendingQuestions 中有问题,走正常流程
|
||
// 如果没有,receiveAnswer 会使用 fallbackTaskId 直接发送到后端
|
||
await userInteractionManager.receiveAnswer(
|
||
askId,
|
||
selected,
|
||
customInput,
|
||
answers,
|
||
this.taskId
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 全局对话会话管理
|
||
*/
|
||
class DialogManager {
|
||
private currentSession: DialogSession | null = null;
|
||
|
||
/**
|
||
* 创建新会话
|
||
* @param extensionPath 扩展路径
|
||
* @param existingTaskId 可选,复用现有的 taskId(用于 Plan 模式确认后继续执行)
|
||
*/
|
||
createSession(extensionPath: string, existingTaskId?: string): DialogSession {
|
||
// 如果有活跃会话,先中止
|
||
if (this.currentSession?.active) {
|
||
this.currentSession.abort();
|
||
}
|
||
this.currentSession = new DialogSession(extensionPath, existingTaskId);
|
||
return this.currentSession;
|
||
}
|
||
|
||
/**
|
||
* 获取当前会话
|
||
*/
|
||
getCurrentSession(): DialogSession | null {
|
||
return this.currentSession;
|
||
}
|
||
|
||
/**
|
||
* 中止当前会话
|
||
*/
|
||
abortCurrentSession(): void {
|
||
this.currentSession?.abort();
|
||
this.currentSession = null; // 清空会话,确保下次创建新会话
|
||
}
|
||
}
|
||
|
||
export const dialogManager = new DialogManager();
|