Compare commits
5 Commits
bdc55c727a
...
fd11eadc19
| Author | SHA1 | Date | |
|---|---|---|---|
| fd11eadc19 | |||
| 1231ef0892 | |||
| a1e88d473b | |||
| a02027e7c9 | |||
| 772b067202 |
@ -10,7 +10,6 @@ import {
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
setLastTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
@ -282,7 +281,7 @@ export async function showICHelperPanel(
|
||||
break;
|
||||
// 新增:处理用户回答
|
||||
case "submitAnswer":
|
||||
handleUserAnswer(
|
||||
void handleUserAnswer(
|
||||
message.askId,
|
||||
message.selected,
|
||||
message.customInput
|
||||
@ -328,27 +327,20 @@ export async function showICHelperPanel(
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式
|
||||
// 确认执行:切换到 Agent 模式(UI 切换)
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||
setPendingPlanExecution(
|
||||
panel,
|
||||
message.planTitle || "计划",
|
||||
context.extensionPath,
|
||||
taskId,
|
||||
message.model // 传递服务等级
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||||
);
|
||||
}
|
||||
// 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
|
||||
} else if (message.action === "modify" || message.action === "cancel") {
|
||||
void handlePlanAction(
|
||||
panel,
|
||||
message.action,
|
||||
message.planTitle || "",
|
||||
context.extensionPath,
|
||||
message.model
|
||||
);
|
||||
}
|
||||
break;
|
||||
// 添加文件上下文 - 显示工作区文件列表
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
* 负责缓存余额、主动查询、发送前检测
|
||||
*/
|
||||
|
||||
import { getCreditBalance } from './apiClient';
|
||||
import * as vscode from 'vscode';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getStrangeLoopApiUrl } from '../config/settings';
|
||||
import { getCachedUserInfo } from './userService';
|
||||
|
||||
/** 低余额阈值 */
|
||||
@ -43,22 +47,41 @@ function isCacheValid(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动查询余额
|
||||
* StrangeLoop 余额响应类型
|
||||
*/
|
||||
interface StrangeLoopBalanceResponse {
|
||||
userId?: number;
|
||||
availableCredits?: number;
|
||||
totalCredits?: number;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动查询余额(直接调用 StrangeLoop 接口)
|
||||
*/
|
||||
export async function fetchBalance(): Promise<number | null> {
|
||||
const userInfo = getCachedUserInfo();
|
||||
if (!userInfo?.userId) {
|
||||
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getCreditBalance(userInfo.userId);
|
||||
if (response.success && response.balance !== undefined) {
|
||||
updateCachedBalance(response.balance);
|
||||
return response.balance;
|
||||
// 获取 JWT token
|
||||
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||
if (!session?.accessToken) {
|
||||
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = session.accessToken;
|
||||
console.log('[CreditsService] 开始查询余额,token 长度:', token.length);
|
||||
|
||||
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
|
||||
const response = await callStrangeLoopBalance(token);
|
||||
|
||||
if (response.availableCredits !== undefined) {
|
||||
const balance = response.availableCredits;
|
||||
updateCachedBalance(balance);
|
||||
console.log('[CreditsService] 余额查询成功:', balance);
|
||||
return balance;
|
||||
} else {
|
||||
console.warn('[CreditsService] 查询余额失败:', response.error);
|
||||
console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
@ -67,6 +90,72 @@ export async function fetchBalance(): Promise<number | null> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 StrangeLoop 余额接口
|
||||
*/
|
||||
async function callStrangeLoopBalance(token: string): Promise<StrangeLoopBalanceResponse> {
|
||||
const urlStr = getStrangeLoopApiUrl('/api/credit/balance');
|
||||
const url = new URL(urlStr);
|
||||
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const httpModule = isHttps ? https : http;
|
||||
|
||||
// 余额查询使用固定短超时,避免阻塞发送前检查
|
||||
const BALANCE_TIMEOUT_MS = 5000;
|
||||
|
||||
const requestOptions: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: BALANCE_TIMEOUT_MS
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpModule.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('[CreditsService] 响应状态码:', res.statusCode);
|
||||
console.log('[CreditsService] 响应内容:', data);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(json as StrangeLoopBalanceResponse);
|
||||
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
// 登录过期或无权限
|
||||
resolve({ error: '登录已过期,请重新登录' });
|
||||
} else {
|
||||
resolve({ error: json.error || json.message || json.msg || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ error: `解析响应失败: ${data}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('请求超时'));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前余额(优先使用缓存,过期则主动查询)
|
||||
*/
|
||||
|
||||
@ -96,6 +96,7 @@ export class DialogSession {
|
||||
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 模式确认后继续执行)
|
||||
@ -337,6 +338,7 @@ export class DialogSession {
|
||||
this.accumulatedText = '';
|
||||
this.segments = [];
|
||||
this.currentTextSegment = null;
|
||||
this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
@ -458,6 +460,8 @@ export class DialogSession {
|
||||
callbacks.onToolComplete?.(data.tool_name, data.result);
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
// 追踪工具执行结果(用于后端重启后恢复)
|
||||
historyManager.trackToolResult(data.tool_name, data.result);
|
||||
},
|
||||
|
||||
onToolError: (data) => {
|
||||
@ -465,6 +469,8 @@ export class DialogSession {
|
||||
callbacks.onToolError?.(data.tool_name, data.error);
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
// 追踪工具执行错误(用于后端重启后恢复)
|
||||
historyManager.trackToolResult(data.tool_name, `[错误] ${data.error}`);
|
||||
},
|
||||
|
||||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||
@ -842,13 +848,25 @@ export class DialogSession {
|
||||
* 中止当前对话
|
||||
*/
|
||||
abort(): void {
|
||||
// 先标记完成,防止 onClose 重复触发
|
||||
const wasActive = this.isActive;
|
||||
this.hasCompleted = true;
|
||||
this.isActive = false;
|
||||
|
||||
if (this.sseController) {
|
||||
this.sseController.abort();
|
||||
this.sseController = null;
|
||||
}
|
||||
this.isActive = false;
|
||||
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);
|
||||
|
||||
@ -715,6 +715,10 @@ export class ChatHistoryManager {
|
||||
|
||||
if (!projectPath) {
|
||||
console.error('[ChatHistoryManager] 无法保存压缩数据:projectPath 为空');
|
||||
// 通知用户压缩数据保存失败
|
||||
vscode.window.showWarningMessage(
|
||||
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -731,6 +735,19 @@ export class ChatHistoryManager {
|
||||
// 文件不存在,使用空数组
|
||||
}
|
||||
|
||||
// 版本检查:防止旧版本覆盖新版本(从尾部扫描,与加载逻辑一致)
|
||||
let existingSummary: CompactionSummaryMessage | null = null;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
|
||||
existingSummary = messages[i] as CompactionSummaryMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingSummary && existingSummary.version >= compacted.version) {
|
||||
console.log(`[ChatHistoryManager] 跳过旧版本压缩数据: 现有版本=${existingSummary.version}, 新版本=${compacted.version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建压缩摘要消息
|
||||
const summaryMessage: CompactionSummaryMessage = {
|
||||
type: MessageType.COMPACTION_SUMMARY,
|
||||
@ -893,4 +910,14 @@ export class ChatHistoryManager {
|
||||
content: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪新消息(工具执行结果)
|
||||
*/
|
||||
public trackToolResult(toolName: string, result: string): void {
|
||||
this.newMessagesSinceCompaction.push({
|
||||
type: 'TOOL_RESULT',
|
||||
content: `[${toolName}] ${result}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,29 +31,6 @@ let currentSession: DialogSession | null = null;
|
||||
/** 最后一个活跃的 taskId(用于压缩等操作) */
|
||||
let lastTaskId: string | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
planTitle: string;
|
||||
extensionPath: string;
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
serviceTier?: ServiceTier; // 保存服务等级
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||||
*/
|
||||
export function setPendingPlanExecution(
|
||||
panel: vscode.WebviewPanel,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
taskId: string,
|
||||
serviceTier?: ServiceTier
|
||||
): void {
|
||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId, serviceTier };
|
||||
console.log("[MessageHandler] 设置待执行计划:", planTitle, "taskId:", taskId, "serviceTier:", serviceTier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
*/
|
||||
@ -159,13 +136,11 @@ async function handleUserMessageWithBackend(
|
||||
// 优先使用 reuseTaskId,其次使用 historyManager 的 taskId
|
||||
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
|
||||
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
||||
}
|
||||
// 创建会话(dialogManager 会自动处理旧会话的中止)
|
||||
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
|
||||
// 保存 taskId 用于后续操作(如压缩)
|
||||
lastTaskId = currentSession.getTaskId();
|
||||
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
|
||||
|
||||
// 显示状态栏
|
||||
panel.webview.postMessage({
|
||||
@ -246,43 +221,6 @@ async function handleUserMessageWithBackend(
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const {
|
||||
panel: execPanel,
|
||||
planTitle,
|
||||
extensionPath: execPath,
|
||||
taskId: reuseTaskId,
|
||||
serviceTier: savedServiceTier,
|
||||
} = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log(
|
||||
"[MessageHandler] 自动执行计划:",
|
||||
planTitle,
|
||||
"复用 taskId:",
|
||||
reuseTaskId,
|
||||
"serviceTier:",
|
||||
savedServiceTier
|
||||
);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
"agent",
|
||||
reuseTaskId, // 复用 Plan 模式的 taskId
|
||||
savedServiceTier // 传递保存的服务等级
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[MessageHandler] 自动执行计划失败:", err);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user