Files
IC-Coder-Plugin/src/services/dialogService.ts
Roe-xin 8751944053 feat: 添加个人规则功能
- 新增个人规则管理模块 (personalRulesManager.ts)
   - 支持创建、编辑、删除多条规则
   - 规则存储在用户目录 ~/.iccoder/rules/
   - 对话时自动将规则传递给后端
   - 添加后端对接文档和 webpack 优化指南
2026-03-07 15:13:54 +08:00

1175 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 对话服务
* 整合 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();