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:
@ -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)
|
|
||||||
setPendingPlanExecution(
|
|
||||||
panel,
|
panel,
|
||||||
message.planTitle || "计划",
|
message.action,
|
||||||
|
message.planTitle || "",
|
||||||
context.extensionPath,
|
context.extensionPath,
|
||||||
taskId,
|
message.model
|
||||||
message.model // 传递服务等级
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
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';
|
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();
|
try {
|
||||||
if (!userInfo?.userId) {
|
// 获取 JWT token
|
||||||
|
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
|
||||||
|
if (!session?.accessToken) {
|
||||||
console.warn('[CreditsService] 无法查询余额:未登录');
|
console.warn('[CreditsService] 无法查询余额:未登录');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const token = session.accessToken;
|
||||||
const response = await getCreditBalance(userInfo.userId);
|
console.log('[CreditsService] 开始查询余额,token 长度:', token.length);
|
||||||
if (response.success && response.balance !== undefined) {
|
|
||||||
updateCachedBalance(response.balance);
|
// 直接调用 StrangeLoop 的 /api/credit/balance 接口
|
||||||
return response.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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前余额(优先使用缓存,过期则主动查询)
|
* 获取当前余额(优先使用缓存,过期则主动查询)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}`
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user