diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 92800d1..c8f2007 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -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; // 添加文件上下文 - 显示工作区文件列表 diff --git a/src/services/creditsService.ts b/src/services/creditsService.ts index d55b7f2..5b55a61 100644 --- a/src/services/creditsService.ts +++ b/src/services/creditsService.ts @@ -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 { - 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 { } } +/** + * 调用 StrangeLoop 余额接口 + */ +async function callStrangeLoopBalance(token: string): Promise { + 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(); + }); +} + /** * 获取当前余额(优先使用缓存,过期则主动查询) */ diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 515fa4b..fab077f 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -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); diff --git a/src/utils/chatHistoryManager.ts b/src/utils/chatHistoryManager.ts index 9f702d4..f36ce07 100644 --- a/src/utils/chatHistoryManager.ts +++ b/src/utils/chatHistoryManager.ts @@ -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}` + }); + } } diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 2d0c5da..b0ee6b1 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -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(); }, diff --git a/src/views/conversationHistoryBar.ts b/src/views/conversationHistoryBar.ts index 1e8cde0..80e5785 100644 --- a/src/views/conversationHistoryBar.ts +++ b/src/views/conversationHistoryBar.ts @@ -303,6 +303,7 @@ export function getConversationHistoryBarScript(): string { let totalHistory = 0; let hasMoreHistory = false; let isLoadingHistory = false; + let currentLoadRequestId = 0; // 请求 ID,用于防止并发加载 const HISTORY_PAGE_SIZE = 10; const MAX_HISTORY_ITEMS = 100; @@ -346,11 +347,15 @@ export function getConversationHistoryBarScript(): string { return; } + // 生成新的请求 ID,用于防止并发加载 + const requestId = ++currentLoadRequestId; + isLoadingHistory = true; vscode.postMessage({ command: 'loadConversationHistory', offset: currentOffset, - limit: HISTORY_PAGE_SIZE + limit: HISTORY_PAGE_SIZE, + requestId: requestId }); } @@ -362,11 +367,19 @@ export function getConversationHistoryBarScript(): string { return; } - // 追加新数据 - conversationHistory = conversationHistory.concat(data.items); + // 追加新数据(去重) + const existingIds = new Set(conversationHistory.map(item => item.id)); + const newItems = []; + for (const item of data.items) { + if (!existingIds.has(item.id)) { + existingIds.add(item.id); + newItems.push(item); + } + } + conversationHistory = conversationHistory.concat(newItems); totalHistory = data.total; hasMoreHistory = data.hasMore; - currentOffset += data.items.length; + currentOffset = conversationHistory.length; const historyList = document.getElementById('historyList'); if (!historyList) { @@ -454,9 +467,10 @@ export function getConversationHistoryBarScript(): string { }); } - // 监听下拉菜单滚动事件 + // 监听下拉菜单滚动事件(防止重复注册) const historyDropdownMenu = document.getElementById('historyDropdownMenu'); - if (historyDropdownMenu) { + if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) { + historyDropdownMenu._scrollListenerAdded = true; historyDropdownMenu.addEventListener('scroll', () => { const menu = historyDropdownMenu; const scrollTop = menu.scrollTop;