Merge branch 'feat/back-to-front' of https://git.pengyejiatu.com/pengyejiatu/IC-Coder-Plugin into feat/back-to-front

This commit is contained in:
Roe-xin
2026-01-12 18:10:34 +08:00
6 changed files with 184 additions and 106 deletions

View File

@ -10,7 +10,6 @@ import {
handleUserAnswer, handleUserAnswer,
abortCurrentDialog, abortCurrentDialog,
handlePlanAction, handlePlanAction,
setPendingPlanExecution,
getCurrentTaskId, getCurrentTaskId,
setLastTaskId, setLastTaskId,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
@ -282,7 +281,7 @@ export async function showICHelperPanel(
break; break;
// 新增:处理用户回答 // 新增:处理用户回答
case "submitAnswer": case "submitAnswer":
handleUserAnswer( void handleUserAnswer(
message.askId, message.askId,
message.selected, message.selected,
message.customInput message.customInput
@ -328,27 +327,20 @@ export async function showICHelperPanel(
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送) // 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
case "planAction": case "planAction":
if (message.action === "confirm") { if (message.action === "confirm") {
// 确认执行:切换到 Agent 模式 // 确认执行:切换到 Agent 模式UI 切换)
panel.webview.postMessage({ panel.webview.postMessage({
command: "switchMode", command: "switchMode",
mode: "agent", mode: "agent",
}); });
// 获取当前会话的 taskId用于复用知识图谱数据 // 注意:不再设置待执行计划;后端 LLM 会在同一对话中自动执行计划
const taskId = getCurrentTaskId(); } else if (message.action === "modify" || message.action === "cancel") {
if (taskId) { void handlePlanAction(
// 设置待执行的计划,对话结束后自动执行(复用 taskId panel,
setPendingPlanExecution( message.action,
panel, message.planTitle || "",
message.planTitle || "计划", context.extensionPath,
context.extensionPath, message.model
taskId, );
message.model // 传递服务等级
);
} else {
console.warn(
"[ICHelperPanel] 无法获取当前 taskId知识图谱数据可能丢失"
);
}
} }
break; break;
// 添加文件上下文 - 显示工作区文件列表 // 添加文件上下文 - 显示工作区文件列表

View File

@ -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'; 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> { export async function fetchBalance(): Promise<number | null> {
const userInfo = getCachedUserInfo();
if (!userInfo?.userId) {
console.warn('[CreditsService] 无法查询余额:未登录');
return null;
}
try { try {
const response = await getCreditBalance(userInfo.userId); // 获取 JWT token
if (response.success && response.balance !== undefined) { const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
updateCachedBalance(response.balance); if (!session?.accessToken) {
return response.balance; 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 { } else {
console.warn('[CreditsService] 查询余额失败:', response.error); console.warn('[CreditsService] 查询余额失败:', response.error || response.message);
return null; return null;
} }
} catch (error) { } 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();
});
}
/** /**
* 获取当前余额(优先使用缓存,过期则主动查询) * 获取当前余额(优先使用缓存,过期则主动查询)
*/ */

View File

@ -96,6 +96,7 @@ export class DialogSession {
private hasCompleted = false; // 标记是否已收到 complete 事件 private hasCompleted = false; // 标记是否已收到 complete 事件
private segments: MessageSegment[] = []; private segments: MessageSegment[] = [];
private currentTextSegment: MessageSegment | null = null; private currentTextSegment: MessageSegment | null = null;
private completeCallback: ((segments: MessageSegment[]) => void) | null = null; // 保存完成回调,用于 abort 时触发
constructor(extensionPath: string, existingTaskId?: string) { constructor(extensionPath: string, existingTaskId?: string) {
// 支持复用现有 taskId用于 Plan 模式确认后继续执行) // 支持复用现有 taskId用于 Plan 模式确认后继续执行)
@ -337,6 +338,7 @@ export class DialogSession {
this.accumulatedText = ''; this.accumulatedText = '';
this.segments = []; this.segments = [];
this.currentTextSegment = null; this.currentTextSegment = null;
this.completeCallback = callbacks.onComplete || null; // 保存完成回调,用于 abort 时触发
const config = getConfig(); const config = getConfig();
@ -458,6 +460,8 @@ export class DialogSession {
callbacks.onToolComplete?.(data.tool_name, data.result); callbacks.onToolComplete?.(data.tool_name, data.result);
// 实时发送段落更新 // 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
// 追踪工具执行结果(用于后端重启后恢复)
historyManager.trackToolResult(data.tool_name, data.result);
}, },
onToolError: (data) => { onToolError: (data) => {
@ -465,6 +469,8 @@ export class DialogSession {
callbacks.onToolError?.(data.tool_name, data.error); callbacks.onToolError?.(data.tool_name, data.error);
// 实时发送段落更新 // 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
// 追踪工具执行错误(用于后端重启后恢复)
historyManager.trackToolResult(data.tool_name, `[错误] ${data.error}`);
}, },
onToolConfirm: async (data: ToolConfirmEvent) => { onToolConfirm: async (data: ToolConfirmEvent) => {
@ -842,13 +848,25 @@ export class DialogSession {
* 中止当前对话 * 中止当前对话
*/ */
abort(): void { abort(): void {
// 先标记完成,防止 onClose 重复触发
const wasActive = this.isActive;
this.hasCompleted = true;
this.isActive = false;
if (this.sseController) { if (this.sseController) {
this.sseController.abort(); this.sseController.abort();
this.sseController = null; this.sseController = null;
} }
this.isActive = false;
userInteractionManager.cancelAll(); 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 => { stopDialog(this.taskId).catch(err => {
console.warn('[DialogSession] 停止对话请求失败:', err); console.warn('[DialogSession] 停止对话请求失败:', err);

View File

@ -715,6 +715,10 @@ export class ChatHistoryManager {
if (!projectPath) { if (!projectPath) {
console.error('[ChatHistoryManager] 无法保存压缩数据projectPath 为空'); console.error('[ChatHistoryManager] 无法保存压缩数据projectPath 为空');
// 通知用户压缩数据保存失败
vscode.window.showWarningMessage(
'对话历史压缩数据保存失败:无法确定项目路径。后端重启后可能无法恢复完整对话历史。'
);
return; 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 = { const summaryMessage: CompactionSummaryMessage = {
type: MessageType.COMPACTION_SUMMARY, type: MessageType.COMPACTION_SUMMARY,
@ -893,4 +910,14 @@ export class ChatHistoryManager {
content: text content: text
}); });
} }
/**
* 追踪新消息(工具执行结果)
*/
public trackToolResult(toolName: string, result: string): void {
this.newMessagesSinceCompaction.push({
type: 'TOOL_RESULT',
content: `[${toolName}] ${result}`
});
}
} }

View File

@ -31,29 +31,6 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */ /** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null; 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 // 优先使用 reuseTaskId其次使用 historyManager 的 taskId
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId(); const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
// 创建或复用会话 // 创建会话dialogManager 会自动处理旧会话的中止)
if (!currentSession || !currentSession.active) { currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined); // 保存 taskId 用于后续操作(如压缩)
// 保存 taskId 用于后续操作(如压缩) lastTaskId = currentSession.getTaskId();
lastTaskId = currentSession.getTaskId(); console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
}
// 显示状态栏 // 显示状态栏
panel.webview.postMessage({ panel.webview.postMessage({
@ -246,43 +221,6 @@ async function handleUserMessageWithBackend(
console.warn("保存AI响应历史失败:", error); 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(); resolve();
}, },

View File

@ -303,6 +303,7 @@ export function getConversationHistoryBarScript(): string {
let totalHistory = 0; let totalHistory = 0;
let hasMoreHistory = false; let hasMoreHistory = false;
let isLoadingHistory = false; let isLoadingHistory = false;
let currentLoadRequestId = 0; // 请求 ID用于防止并发加载
const HISTORY_PAGE_SIZE = 10; const HISTORY_PAGE_SIZE = 10;
const MAX_HISTORY_ITEMS = 100; const MAX_HISTORY_ITEMS = 100;
@ -346,11 +347,15 @@ export function getConversationHistoryBarScript(): string {
return; return;
} }
// 生成新的请求 ID用于防止并发加载
const requestId = ++currentLoadRequestId;
isLoadingHistory = true; isLoadingHistory = true;
vscode.postMessage({ vscode.postMessage({
command: 'loadConversationHistory', command: 'loadConversationHistory',
offset: currentOffset, offset: currentOffset,
limit: HISTORY_PAGE_SIZE limit: HISTORY_PAGE_SIZE,
requestId: requestId
}); });
} }
@ -362,11 +367,19 @@ export function getConversationHistoryBarScript(): string {
return; 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; totalHistory = data.total;
hasMoreHistory = data.hasMore; hasMoreHistory = data.hasMore;
currentOffset += data.items.length; currentOffset = conversationHistory.length;
const historyList = document.getElementById('historyList'); const historyList = document.getElementById('historyList');
if (!historyList) { if (!historyList) {
@ -454,9 +467,10 @@ export function getConversationHistoryBarScript(): string {
}); });
} }
// 监听下拉菜单滚动事件 // 监听下拉菜单滚动事件(防止重复注册)
const historyDropdownMenu = document.getElementById('historyDropdownMenu'); const historyDropdownMenu = document.getElementById('historyDropdownMenu');
if (historyDropdownMenu) { if (historyDropdownMenu && !historyDropdownMenu._scrollListenerAdded) {
historyDropdownMenu._scrollListenerAdded = true;
historyDropdownMenu.addEventListener('scroll', () => { historyDropdownMenu.addEventListener('scroll', () => {
const menu = historyDropdownMenu; const menu = historyDropdownMenu;
const scrollTop = menu.scrollTop; const scrollTop = menu.scrollTop;