mirror of
https://github.com/Cola-Echo/memory-manager-concurrent.git
synced 2026-06-06 04:15:52 +00:00
feat: 优化进度条体验和修复一键全选功能
- 进度条改用检查点驱动模拟真实流式传输进度 (5→15→25→35→45→60→75→85→92→100) - 每个检查点间使用 ease-out 缓动平滑过渡 - 完成时从当前进度平滑动画到 100% - 修复一键全选按钮事件绑定问题 - 添加调试日志帮助诊断问题 - 修复 addSystemMessage 使用不存在容器的问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* OpenAI API 提供商
|
||||
* @module api/providers/openai
|
||||
@@ -5,7 +6,7 @@
|
||||
|
||||
/**
|
||||
* 模拟流式进度管理器
|
||||
* 使用时间驱动的平滑进度增长,提供稳定的视觉体验
|
||||
* 使用检查点驱动的进度增长,模拟真实的流式传输体验
|
||||
*/
|
||||
class SimulatedProgressManager {
|
||||
constructor(taskId, progressTracker, config = {}) {
|
||||
@@ -15,17 +16,34 @@ class SimulatedProgressManager {
|
||||
this.currentProgress = 0;
|
||||
this.intervalId = null;
|
||||
this.isCompleted = false;
|
||||
this.completionIntervalId = null;
|
||||
|
||||
// 检查点配置:模拟真实的处理阶段
|
||||
// [进度值, 到达该点的预估时间(ms), 停留时间(ms)]
|
||||
this.checkpoints = config.checkpoints || [
|
||||
{ progress: 5, time: 500, pause: 100 }, // 初始化
|
||||
{ progress: 15, time: 2000, pause: 200 }, // 开始处理
|
||||
{ progress: 25, time: 4000, pause: 150 }, // 解析请求
|
||||
{ progress: 35, time: 7000, pause: 300 }, // 生成中...
|
||||
{ progress: 45, time: 10000, pause: 200 }, // 持续生成
|
||||
{ progress: 60, time: 15000, pause: 400 }, // 主要内容
|
||||
{ progress: 75, time: 20000, pause: 300 }, // 接近完成
|
||||
{ progress: 85, time: 25000, pause: 200 }, // 收尾阶段
|
||||
{ progress: 92, time: 30000, pause: 0 }, // 等待完成
|
||||
];
|
||||
|
||||
this.currentCheckpointIndex = 0;
|
||||
this.lastCheckpointTime = Date.now();
|
||||
this.isPaused = false;
|
||||
this.pauseEndTime = 0;
|
||||
|
||||
// 配置参数
|
||||
this.maxProgress = config.maxProgress || 92; // 模拟进度最大值
|
||||
this.duration = config.duration || 30000; // 预估总时长(毫秒)
|
||||
this.updateInterval = config.updateInterval || 100; // 更新间隔(毫秒)
|
||||
this.updateInterval = config.updateInterval || 50; // 更新间隔(毫秒)
|
||||
this.completionDuration = config.completionDuration || 300; // 完成动画时长
|
||||
|
||||
// 使用缓动函数使进度更自然(开始快,后面慢)
|
||||
this.easingFn = (t) => {
|
||||
// ease-out-cubic: 1 - (1 - t)^3
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
};
|
||||
// 流数据相关
|
||||
this.totalCharsReceived = 0;
|
||||
this.lastCharsReceived = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,29 +58,103 @@ class SimulatedProgressManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
const t = Math.min(elapsed / this.duration, 1);
|
||||
const easedProgress = this.easingFn(t) * this.maxProgress;
|
||||
|
||||
// 确保进度只增不减
|
||||
if (easedProgress > this.currentProgress) {
|
||||
this.currentProgress = easedProgress;
|
||||
this.updateProgress(this.currentProgress);
|
||||
}
|
||||
this.tick();
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧更新
|
||||
*/
|
||||
tick() {
|
||||
const now = Date.now();
|
||||
|
||||
// 如果在停顿中,等待停顿结束
|
||||
if (this.isPaused) {
|
||||
if (now >= this.pauseEndTime) {
|
||||
this.isPaused = false;
|
||||
this.lastCheckpointTime = now;
|
||||
this.currentCheckpointIndex++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前和下一个检查点
|
||||
if (this.currentCheckpointIndex >= this.checkpoints.length) {
|
||||
return; // 已到达最后一个检查点
|
||||
}
|
||||
|
||||
const currentCp = this.currentCheckpointIndex > 0
|
||||
? this.checkpoints[this.currentCheckpointIndex - 1]
|
||||
: { progress: 0, time: 0, pause: 0 };
|
||||
const nextCp = this.checkpoints[this.currentCheckpointIndex];
|
||||
|
||||
// 计算当前检查点段的进度
|
||||
const segmentStartTime = this.currentCheckpointIndex === 0
|
||||
? this.startTime
|
||||
: this.lastCheckpointTime;
|
||||
const segmentDuration = nextCp.time - (currentCp.time || 0);
|
||||
const elapsed = now - segmentStartTime;
|
||||
const t = Math.min(elapsed / segmentDuration, 1);
|
||||
|
||||
// 使用 ease-out 缓动在检查点之间过渡
|
||||
const eased = 1 - Math.pow(1 - t, 2);
|
||||
const progressInSegment = currentCp.progress + (nextCp.progress - currentCp.progress) * eased;
|
||||
|
||||
// 更新进度(只增不减)
|
||||
if (progressInSegment > this.currentProgress) {
|
||||
this.currentProgress = progressInSegment;
|
||||
this.updateProgress(this.currentProgress);
|
||||
}
|
||||
|
||||
// 到达检查点时触发停顿
|
||||
if (t >= 1) {
|
||||
this.currentProgress = nextCp.progress;
|
||||
this.updateProgress(this.currentProgress);
|
||||
|
||||
if (nextCp.pause > 0) {
|
||||
this.isPaused = true;
|
||||
this.pauseEndTime = now + nextCp.pause;
|
||||
} else {
|
||||
this.lastCheckpointTime = now;
|
||||
this.currentCheckpointIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收到流数据时调用,加速进度
|
||||
* @param {number} charsReceived 已接收字符数
|
||||
*/
|
||||
onStreamData(charsReceived) {
|
||||
// 当收到流数据时,适度加速进度
|
||||
// 每收到 100 字符,进度至少推进一点
|
||||
const minProgress = Math.min(this.maxProgress, 10 + charsReceived / 50);
|
||||
if (minProgress > this.currentProgress) {
|
||||
this.currentProgress = minProgress;
|
||||
this.updateProgress(this.currentProgress);
|
||||
this.totalCharsReceived = charsReceived;
|
||||
const newChars = charsReceived - this.lastCharsReceived;
|
||||
this.lastCharsReceived = charsReceived;
|
||||
|
||||
// 根据收到的字符数加速进度
|
||||
// 每收到数据,至少推进到当前检查点的 50%
|
||||
if (newChars > 0 && this.currentCheckpointIndex < this.checkpoints.length) {
|
||||
const currentCp = this.currentCheckpointIndex > 0
|
||||
? this.checkpoints[this.currentCheckpointIndex - 1]
|
||||
: { progress: 0 };
|
||||
const nextCp = this.checkpoints[this.currentCheckpointIndex];
|
||||
|
||||
// 根据字符数推进进度
|
||||
const charBasedProgress = Math.min(
|
||||
nextCp.progress,
|
||||
currentCp.progress + (charsReceived / 30) // 每30字符推进1%
|
||||
);
|
||||
|
||||
if (charBasedProgress > this.currentProgress) {
|
||||
this.currentProgress = charBasedProgress;
|
||||
this.updateProgress(this.currentProgress);
|
||||
|
||||
// 如果超过当前检查点,跳到下一个
|
||||
if (this.currentProgress >= nextCp.progress) {
|
||||
this.currentCheckpointIndex++;
|
||||
this.lastCheckpointTime = Date.now();
|
||||
this.isPaused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,12 +169,42 @@ class SimulatedProgressManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成进度
|
||||
* 完成进度(带平滑动画从当前进度过渡到100%)
|
||||
*/
|
||||
complete() {
|
||||
this.isCompleted = true;
|
||||
this.stop();
|
||||
this.updateProgress(100);
|
||||
|
||||
// 平滑过渡到 100%
|
||||
const startProgress = this.currentProgress;
|
||||
const progressDiff = 100 - startProgress;
|
||||
const startTime = Date.now();
|
||||
const duration = this.completionDuration;
|
||||
|
||||
// 如果差距很小,直接完成
|
||||
if (progressDiff <= 1) {
|
||||
this.updateProgress(100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 setInterval 进行平滑动画
|
||||
this.completionIntervalId = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用 ease-out 缓动
|
||||
const eased = 1 - Math.pow(1 - t, 2);
|
||||
const newProgress = startProgress + progressDiff * eased;
|
||||
|
||||
this.currentProgress = newProgress;
|
||||
this.updateProgress(newProgress);
|
||||
|
||||
if (t >= 1) {
|
||||
clearInterval(this.completionIntervalId);
|
||||
this.completionIntervalId = null;
|
||||
this.updateProgress(100);
|
||||
}
|
||||
}, 16); // ~60fps
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ export const defaultConfig = Object.freeze({
|
||||
sendIndexOnly: false,
|
||||
showSummaryCheck: false,
|
||||
enableRecentPlot: true, // 启用剧情末尾(截取并注入到汇总检查)
|
||||
recentPlotLength: 200, // 剧情末尾截取字数(10-300)
|
||||
// 索引合并模式配置
|
||||
indexMergeEnabled: false, // 是否启用索引合并
|
||||
indexMergeConfig: {
|
||||
|
||||
@@ -90,11 +90,12 @@ const Logger = {
|
||||
|
||||
/**
|
||||
* 调试日志(受 showLogs 控制)
|
||||
* 注意:使用 console.log 而非 console.debug,确保在所有浏览器设置下可见
|
||||
* @param {...any} args 日志参数
|
||||
*/
|
||||
debug: (...args) => {
|
||||
if (Logger.shouldShowLogs()) {
|
||||
console.debug(Logger.prefix, ...args);
|
||||
console.log(Logger.prefix, "[DEBUG]", ...args);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -195,7 +196,7 @@ const Logger = {
|
||||
|
||||
debug: (...args) => {
|
||||
if (Logger.shouldShowLogs()) {
|
||||
console.debug(modulePrefix, ...args);
|
||||
console.log(modulePrefix, "[DEBUG]", ...args);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -503,10 +503,11 @@ export async function processMemoryForMessage(userMessage) {
|
||||
const context = getRecentContext(chat, contextRounds);
|
||||
|
||||
// 获取标签过滤配置(用于最近剧情截取)
|
||||
// [标签过滤调用点2] 用于处理最后一条助手消息的末尾200字
|
||||
// [标签过滤调用点2] 用于处理最后一条助手消息的末尾
|
||||
const tagFilterConfig = globalConfig.contextTagFilter;
|
||||
|
||||
// 从最后一条助手消息中截取末尾200字
|
||||
// 从最后一条助手消息中截取末尾(使用配置的字数,默认200)
|
||||
const recentPlotLength = globalSettings.recentPlotLength ?? 200;
|
||||
let latestContext = "";
|
||||
if (
|
||||
globalSettings.enableRecentPlot !== false &&
|
||||
@@ -530,7 +531,7 @@ export async function processMemoryForMessage(userMessage) {
|
||||
// 使用 filterContentByRole 处理标签过滤(AI消息 = false)
|
||||
content = filterContentByRole(content, tagFilterConfig, false);
|
||||
|
||||
latestContext = content.slice(-200).trim();
|
||||
latestContext = content.slice(-recentPlotLength).trim();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1725,12 +1725,11 @@ async function generatePlotOptimize(userInput = "") {
|
||||
Logger.log("[剧情优化] 调用 progressTracker.addTask");
|
||||
try {
|
||||
// 确保任务被正确添加,并且进度UI被显示
|
||||
// addTask 已经设置了 status: "running",不需要再调用 startTask
|
||||
progressTracker.addTask("plot_optimize", "剧情优化", "plot");
|
||||
Logger.log("[剧情优化] addTask 调用成功");
|
||||
// 立即更新进度为1%,确保进度条显示
|
||||
progressTracker.updateStreamProgress("plot_optimize", 1);
|
||||
// 立即开始任务,确保状态正确
|
||||
progressTracker.startTask("plot_optimize");
|
||||
// 立即更新进度为5%,确保进度条有初始显示
|
||||
progressTracker.updateStreamProgress("plot_optimize", 5);
|
||||
} catch (e) {
|
||||
Logger.error("[剧情优化] addTask 调用失败:", e);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* 进度追踪器模块
|
||||
* @module ui/components/progress-tracker
|
||||
@@ -6,11 +7,12 @@
|
||||
import Logger from "@core/logger";
|
||||
|
||||
// 消息进度面板引用(将在初始化时注入)
|
||||
/** @type {any} */
|
||||
let messageProgressPanel = null;
|
||||
|
||||
/**
|
||||
* 设置消息进度面板引用
|
||||
* @param {object} panel 消息进度面板实例
|
||||
* @param {any} panel 消息进度面板实例
|
||||
*/
|
||||
export function setMessageProgressPanel(panel) {
|
||||
messageProgressPanel = panel;
|
||||
@@ -182,6 +184,15 @@ export class ProgressTracker {
|
||||
type,
|
||||
);
|
||||
Logger.log("[ProgressTracker] addTask 被调用:", taskId, name, type);
|
||||
|
||||
// 先确保 messageProgressPanel 容器已创建(在添加任务数据之前)
|
||||
if (messageProgressPanel && !messageProgressPanel.container) {
|
||||
Logger.log("[ProgressTracker] 预先初始化 messageProgressPanel 容器");
|
||||
messageProgressPanel.createDOM();
|
||||
messageProgressPanel.bindEvents();
|
||||
messageProgressPanel.loadPosition();
|
||||
}
|
||||
|
||||
if (this.tasks.has(taskId)) {
|
||||
const task = this.tasks.get(taskId);
|
||||
task.status = "running";
|
||||
@@ -213,15 +224,6 @@ export class ProgressTracker {
|
||||
!!messageProgressPanel,
|
||||
);
|
||||
if (messageProgressPanel) {
|
||||
// 确保 messageProgressPanel 已初始化(首次调用时需要创建 DOM)
|
||||
Logger.log(
|
||||
"[ProgressTracker] messageProgressPanel.container 状态:",
|
||||
!!messageProgressPanel.container,
|
||||
);
|
||||
if (!messageProgressPanel.container) {
|
||||
Logger.log("[ProgressTracker] 初始化 messageProgressPanel");
|
||||
messageProgressPanel.init();
|
||||
}
|
||||
const activeTasks = new Map();
|
||||
for (const [id, task] of this.tasks) {
|
||||
if (task.status !== "success" && task.status !== "error") {
|
||||
|
||||
@@ -85,6 +85,18 @@ export class MemorySearchPanel {
|
||||
this.toggleMinimize();
|
||||
});
|
||||
|
||||
// 一键全选按钮
|
||||
const injectAllBtn = document.getElementById("mm-search-inject-all");
|
||||
if (injectAllBtn) {
|
||||
injectAllBtn.addEventListener("click", () => {
|
||||
Logger.debug("[一键全选] 按钮被点击");
|
||||
this.selectAllUnrejected();
|
||||
});
|
||||
Logger.debug("[记忆搜索助手] 一键全选按钮事件已绑定");
|
||||
} else {
|
||||
Logger.warn("[记忆搜索助手] 一键全选按钮未找到,事件未绑定");
|
||||
}
|
||||
|
||||
// 确认注入按钮
|
||||
document
|
||||
.getElementById("mm-search-confirm")
|
||||
@@ -901,6 +913,68 @@ export class MemorySearchPanel {
|
||||
return historicalLines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键全选所有未拒绝、未移除的记忆
|
||||
* 将所有未被拒绝的搜索结果标记为已采纳,用户再点确认注入完成操作
|
||||
*/
|
||||
selectAllUnrejected() {
|
||||
// 获取所有搜索结果项
|
||||
const container = document.getElementById("mm-search-books-container");
|
||||
if (!container) {
|
||||
Logger.warn("[一键全选] 容器 mm-search-books-container 未找到");
|
||||
return;
|
||||
}
|
||||
|
||||
const allResultItems = container.querySelectorAll(".mm-search-result-item");
|
||||
Logger.debug(`[一键全选] 找到 ${allResultItems.length} 个搜索结果项`);
|
||||
|
||||
if (allResultItems.length === 0) {
|
||||
// 使用第一个世界书面板显示消息
|
||||
if (this.summaryBooks.length > 0) {
|
||||
this.addBookSystemMessage(this.summaryBooks[0].name, "没有可选择的搜索结果");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedCount = 0;
|
||||
const beforeCount = this.selectedMemories.length;
|
||||
|
||||
for (const resultItem of allResultItems) {
|
||||
// 跳过已拒绝的
|
||||
if (resultItem.classList.contains("mm-rejected")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过已经采纳的(已在 selectedMemories 中)
|
||||
if (resultItem.classList.contains("mm-adopted")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有 _memoryData
|
||||
if (!resultItem._memoryData) {
|
||||
Logger.warn("[一键全选] 搜索结果项缺少 _memoryData:", resultItem.dataset.resultId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 通过已有的 adoptMemory 方法采纳
|
||||
this.adoptMemory(resultItem);
|
||||
selectedCount++;
|
||||
}
|
||||
|
||||
const actualAdopted = this.selectedMemories.length - beforeCount;
|
||||
Logger.debug(`[一键全选] 尝试选择 ${selectedCount} 条,实际采纳 ${actualAdopted} 条`);
|
||||
|
||||
// 使用第一个世界书面板显示消息
|
||||
const firstBookName = this.summaryBooks.length > 0 ? this.summaryBooks[0].name : null;
|
||||
if (firstBookName) {
|
||||
if (actualAdopted === 0) {
|
||||
this.addBookSystemMessage(firstBookName, "没有新的条目可选择(可能都已采纳或拒绝)");
|
||||
} else {
|
||||
this.addBookSystemMessage(firstBookName, `已全选 ${actualAdopted} 条记忆,请点击「确认注入」完成操作`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
*/
|
||||
|
||||
@@ -641,11 +641,26 @@ function bindSettingsEvents() {
|
||||
?.addEventListener("change", (e) => {
|
||||
const checked = e.target.checked;
|
||||
updateGlobalSettings({ enableRecentPlot: checked });
|
||||
// 显示/隐藏字数滑条
|
||||
const lengthContainer = document.getElementById("mm-recent-plot-length-container");
|
||||
if (lengthContainer) {
|
||||
lengthContainer.style.display = checked ? "block" : "none";
|
||||
}
|
||||
if (typeof toastr !== 'undefined') {
|
||||
toastr.success(`剧情末尾已${checked ? "启用" : "禁用"}`, "记忆管理并发系统");
|
||||
}
|
||||
});
|
||||
|
||||
// 剧情末尾字数滑块
|
||||
document
|
||||
.getElementById("mm-recent-plot-length")
|
||||
?.addEventListener("input", (e) => {
|
||||
const value = parseInt(e.target.value) ?? 200;
|
||||
const valueEl = document.getElementById("mm-recent-plot-length-value");
|
||||
if (valueEl) valueEl.textContent = value;
|
||||
updateGlobalSettings({ recentPlotLength: value });
|
||||
});
|
||||
|
||||
// 上下文轮数滑块
|
||||
document
|
||||
.getElementById("mm-context-rounds")
|
||||
@@ -1506,6 +1521,21 @@ export function loadGlobalSettingsUI() {
|
||||
recentPlotCheckbox.checked = settings.enableRecentPlot !== false;
|
||||
}
|
||||
|
||||
// 剧情末尾字数滑条
|
||||
const recentPlotLengthContainer = document.getElementById("mm-recent-plot-length-container");
|
||||
const recentPlotLengthInput = document.getElementById("mm-recent-plot-length");
|
||||
const recentPlotLengthValue = document.getElementById("mm-recent-plot-length-value");
|
||||
if (recentPlotLengthContainer) {
|
||||
// 根据启用状态显示/隐藏
|
||||
recentPlotLengthContainer.style.display = settings.enableRecentPlot !== false ? "block" : "none";
|
||||
}
|
||||
if (recentPlotLengthInput) {
|
||||
recentPlotLengthInput.value = settings.recentPlotLength ?? 200;
|
||||
}
|
||||
if (recentPlotLengthValue) {
|
||||
recentPlotLengthValue.textContent = settings.recentPlotLength ?? 200;
|
||||
}
|
||||
|
||||
// 上下文轮次
|
||||
const contextRoundsInput = document.getElementById("mm-context-rounds");
|
||||
const contextRoundsValue = document.getElementById("mm-context-rounds-value");
|
||||
|
||||
Reference in New Issue
Block a user