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:
user
2026-02-04 19:25:39 +08:00
parent 1fd223930d
commit e78cd230d9
12 changed files with 303 additions and 48 deletions

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

9
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "memory-manager-concurrent", "name": "memory-manager-concurrent",
"version": "0.4.2", "version": "0.4.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "memory-manager-concurrent", "name": "memory-manager-concurrent",
"version": "0.4.2", "version": "0.4.7",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.10",
@@ -351,6 +351,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -377,6 +378,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -449,6 +451,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -1370,6 +1373,7 @@
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8", "@types/estree": "^1.0.8",
@@ -1419,6 +1423,7 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@discoveryjs/json-ext": "^0.5.0", "@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1", "@webpack-cli/configtest": "^2.1.1",

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
/** /**
* OpenAI API 提供商 * OpenAI API 提供商
* @module api/providers/openai * @module api/providers/openai
@@ -5,7 +6,7 @@
/** /**
* 模拟流式进度管理器 * 模拟流式进度管理器
* 使用时间驱动的平滑进度增长,提供稳定的视觉体验 * 使用检查点驱动的进度增长,模拟真实的流式传输体验
*/ */
class SimulatedProgressManager { class SimulatedProgressManager {
constructor(taskId, progressTracker, config = {}) { constructor(taskId, progressTracker, config = {}) {
@@ -15,17 +16,34 @@ class SimulatedProgressManager {
this.currentProgress = 0; this.currentProgress = 0;
this.intervalId = null; this.intervalId = null;
this.isCompleted = false; 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.updateInterval = config.updateInterval || 50; // 更新间隔(毫秒)
this.duration = config.duration || 30000; // 预估总时长(毫秒) this.completionDuration = config.completionDuration || 300; // 完成动画时长
this.updateInterval = config.updateInterval || 100; // 更新间隔(毫秒)
// 使用缓动函数使进度更自然(开始快,后面慢) // 流数据相关
this.easingFn = (t) => { this.totalCharsReceived = 0;
// ease-out-cubic: 1 - (1 - t)^3 this.lastCharsReceived = 0;
return 1 - Math.pow(1 - t, 3);
};
} }
/** /**
@@ -40,29 +58,103 @@ class SimulatedProgressManager {
return; return;
} }
const elapsed = Date.now() - this.startTime; this.tick();
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.updateInterval); }, 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 已接收字符数 * @param {number} charsReceived 已接收字符数
*/ */
onStreamData(charsReceived) { onStreamData(charsReceived) {
// 当收到流数据时,适度加速进度 this.totalCharsReceived = charsReceived;
// 每收到 100 字符,进度至少推进一点 const newChars = charsReceived - this.lastCharsReceived;
const minProgress = Math.min(this.maxProgress, 10 + charsReceived / 50); this.lastCharsReceived = charsReceived;
if (minProgress > this.currentProgress) {
this.currentProgress = minProgress; // 根据收到的字符数加速进度
this.updateProgress(this.currentProgress); // 每收到数据,至少推进到当前检查点的 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() { complete() {
this.isCompleted = true; this.isCompleted = true;
this.stop(); 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
} }
/** /**

View File

@@ -20,6 +20,7 @@ export const defaultConfig = Object.freeze({
sendIndexOnly: false, sendIndexOnly: false,
showSummaryCheck: false, showSummaryCheck: false,
enableRecentPlot: true, // 启用剧情末尾(截取并注入到汇总检查) enableRecentPlot: true, // 启用剧情末尾(截取并注入到汇总检查)
recentPlotLength: 200, // 剧情末尾截取字数10-300
// 索引合并模式配置 // 索引合并模式配置
indexMergeEnabled: false, // 是否启用索引合并 indexMergeEnabled: false, // 是否启用索引合并
indexMergeConfig: { indexMergeConfig: {

View File

@@ -90,11 +90,12 @@ const Logger = {
/** /**
* 调试日志(受 showLogs 控制) * 调试日志(受 showLogs 控制)
* 注意:使用 console.log 而非 console.debug确保在所有浏览器设置下可见
* @param {...any} args 日志参数 * @param {...any} args 日志参数
*/ */
debug: (...args) => { debug: (...args) => {
if (Logger.shouldShowLogs()) { if (Logger.shouldShowLogs()) {
console.debug(Logger.prefix, ...args); console.log(Logger.prefix, "[DEBUG]", ...args);
} }
}, },
@@ -195,7 +196,7 @@ const Logger = {
debug: (...args) => { debug: (...args) => {
if (Logger.shouldShowLogs()) { if (Logger.shouldShowLogs()) {
console.debug(modulePrefix, ...args); console.log(modulePrefix, "[DEBUG]", ...args);
} }
}, },

View File

@@ -503,10 +503,11 @@ export async function processMemoryForMessage(userMessage) {
const context = getRecentContext(chat, contextRounds); const context = getRecentContext(chat, contextRounds);
// 获取标签过滤配置(用于最近剧情截取) // 获取标签过滤配置(用于最近剧情截取)
// [标签过滤调用点2] 用于处理最后一条助手消息的末尾200字 // [标签过滤调用点2] 用于处理最后一条助手消息的末尾
const tagFilterConfig = globalConfig.contextTagFilter; const tagFilterConfig = globalConfig.contextTagFilter;
// 从最后一条助手消息中截取末尾200 // 从最后一条助手消息中截取末尾(使用配置的字数,默认200
const recentPlotLength = globalSettings.recentPlotLength ?? 200;
let latestContext = ""; let latestContext = "";
if ( if (
globalSettings.enableRecentPlot !== false && globalSettings.enableRecentPlot !== false &&
@@ -530,7 +531,7 @@ export async function processMemoryForMessage(userMessage) {
// 使用 filterContentByRole 处理标签过滤AI消息 = false // 使用 filterContentByRole 处理标签过滤AI消息 = false
content = filterContentByRole(content, tagFilterConfig, false); content = filterContentByRole(content, tagFilterConfig, false);
latestContext = content.slice(-200).trim(); latestContext = content.slice(-recentPlotLength).trim();
} }
} }

View File

@@ -1725,12 +1725,11 @@ async function generatePlotOptimize(userInput = "") {
Logger.log("[剧情优化] 调用 progressTracker.addTask"); Logger.log("[剧情优化] 调用 progressTracker.addTask");
try { try {
// 确保任务被正确添加并且进度UI被显示 // 确保任务被正确添加并且进度UI被显示
// addTask 已经设置了 status: "running",不需要再调用 startTask
progressTracker.addTask("plot_optimize", "剧情优化", "plot"); progressTracker.addTask("plot_optimize", "剧情优化", "plot");
Logger.log("[剧情优化] addTask 调用成功"); Logger.log("[剧情优化] addTask 调用成功");
// 立即更新进度为1%,确保进度条显示 // 立即更新进度为5%,确保进度条有初始显示
progressTracker.updateStreamProgress("plot_optimize", 1); progressTracker.updateStreamProgress("plot_optimize", 5);
// 立即开始任务,确保状态正确
progressTracker.startTask("plot_optimize");
} catch (e) { } catch (e) {
Logger.error("[剧情优化] addTask 调用失败:", e); Logger.error("[剧情优化] addTask 调用失败:", e);
} }

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
/** /**
* 进度追踪器模块 * 进度追踪器模块
* @module ui/components/progress-tracker * @module ui/components/progress-tracker
@@ -6,11 +7,12 @@
import Logger from "@core/logger"; import Logger from "@core/logger";
// 消息进度面板引用(将在初始化时注入) // 消息进度面板引用(将在初始化时注入)
/** @type {any} */
let messageProgressPanel = null; let messageProgressPanel = null;
/** /**
* 设置消息进度面板引用 * 设置消息进度面板引用
* @param {object} panel 消息进度面板实例 * @param {any} panel 消息进度面板实例
*/ */
export function setMessageProgressPanel(panel) { export function setMessageProgressPanel(panel) {
messageProgressPanel = panel; messageProgressPanel = panel;
@@ -182,6 +184,15 @@ export class ProgressTracker {
type, type,
); );
Logger.log("[ProgressTracker] addTask 被调用:", taskId, name, 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)) { if (this.tasks.has(taskId)) {
const task = this.tasks.get(taskId); const task = this.tasks.get(taskId);
task.status = "running"; task.status = "running";
@@ -213,15 +224,6 @@ export class ProgressTracker {
!!messageProgressPanel, !!messageProgressPanel,
); );
if (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(); const activeTasks = new Map();
for (const [id, task] of this.tasks) { for (const [id, task] of this.tasks) {
if (task.status !== "success" && task.status !== "error") { if (task.status !== "success" && task.status !== "error") {

View File

@@ -85,6 +85,18 @@ export class MemorySearchPanel {
this.toggleMinimize(); 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 document
.getElementById("mm-search-confirm") .getElementById("mm-search-confirm")
@@ -901,6 +913,68 @@ export class MemorySearchPanel {
return historicalLines.join("\n"); 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} 条记忆,请点击「确认注入」完成操作`);
}
}
}
/** /**
* 确认选择 * 确认选择
*/ */

View File

@@ -641,11 +641,26 @@ function bindSettingsEvents() {
?.addEventListener("change", (e) => { ?.addEventListener("change", (e) => {
const checked = e.target.checked; const checked = e.target.checked;
updateGlobalSettings({ enableRecentPlot: 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') { if (typeof toastr !== 'undefined') {
toastr.success(`剧情末尾已${checked ? "启用" : "禁用"}`, "记忆管理并发系统"); 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 document
.getElementById("mm-context-rounds") .getElementById("mm-context-rounds")
@@ -1506,6 +1521,21 @@ export function loadGlobalSettingsUI() {
recentPlotCheckbox.checked = settings.enableRecentPlot !== false; 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 contextRoundsInput = document.getElementById("mm-context-rounds");
const contextRoundsValue = document.getElementById("mm-context-rounds-value"); const contextRoundsValue = document.getElementById("mm-context-rounds-value");

View File

@@ -37,6 +37,9 @@
<div class="mm-search-panel-footer"> <div class="mm-search-panel-footer">
<div class="mm-search-actions"> <div class="mm-search-actions">
<button id="mm-search-inject-all" class="mm-btn mm-btn-primary" title="全选所有未拒绝的条目">
<i class="fa-solid fa-bolt"></i> 一键全选
</button>
<button id="mm-search-confirm" class="mm-btn mm-btn-secondary" disabled> <button id="mm-search-confirm" class="mm-btn mm-btn-secondary" disabled>
<i class="fa-solid fa-check"></i> 确认注入 <i class="fa-solid fa-check"></i> 确认注入
</button> </button>

View File

@@ -34,6 +34,23 @@
> >
</div> </div>
<!-- 剧情末尾字数设置(当启用剧情末尾时显示) -->
<div class="mm-setting-item" id="mm-recent-plot-length-container" style="display: none;">
<label>剧情末尾字数</label>
<div class="mm-slider-row">
<input
type="range"
id="mm-recent-plot-length"
value="200"
min="10"
max="300"
step="10"
/>
<span id="mm-recent-plot-length-value">200</span>
</div>
<small class="mm-hint">截取最后一条AI消息末尾的字数10-300</small>
</div>
<!-- 功能开关折叠卡片 --> <!-- 功能开关折叠卡片 -->
<div class="mm-collapse-card" id="mm-feature-switch-card"> <div class="mm-collapse-card" id="mm-feature-switch-card">
<div class="mm-collapse-header" id="mm-feature-switch-toggle"> <div class="mm-collapse-header" id="mm-feature-switch-toggle">