release: v2.2.6 [2026-06-13 01:02:05]

### 新功能
- **翰林院向量化质量升级**:
  - **边界感知切块**:替换四个来源(聊天记录/小说/世界书/手动)的纯字符硬切——优先在段落边界断开,其次句末标点(含中文引号闭合),极端长串才硬切;句子/对话不再被拦腰截断,embedding 质量同步受益。仅影响新录入,已有向量无需重建
  - **注入时序重排**:检索结果注入提示词前按时序重排(聊天记录按楼层、小说按卷/章/节——中文数字章节号可解析),rerank 只决定"选哪些块",不再决定呈现顺序;修复"不打不相识的剧情之后紧跟关系亲密"这类因按相关度排序导致的认知时间错乱
  - **断层提示**:聊天记录相邻块楼层跳跃时自动插入"与上文相隔约 N 楼,并非连续发生"提示行,消除中间剧情缺失造成的割裂感
  - **时间标识**:新录入的聊天记录块在来源标识中带上消息发送时间(ST 向量存储不持久化元数据,时间必须写入块文本才能在检索后取回;旧格式块兼容解析)
- **记忆块工作流(memory-blocks)**:剧情优化新增"自定义记忆块"体系——占位符驱动的并发工作流框架
  - 在剧情优化面板「匹配替换 (sulv)」下方可增删自定义块:每个块定义一个占位符,执行剧情优化时主/拦截提示词中的占位符会被块的产出替换
  - **静态块**:直接输出固定内容;**AI 调用块**:用所选 API 功能槽独立请求一次,把回复(或其中指定 `<标签>` 的内容)作为替换值
  - 原有 sulv1-4 速率占位符迁入同一框架,行为与旧版逐字节一致
  - 块定义为纯 JSON、随设置持久化,为后续导入导出与战斗系统接入预留扩展点
  - 框架层新增**顺序拼接式 Chain**(`composeChain`):与占位符替换并列的第二种组合范式——同链的块并发执行后按 `order` 排序、以 `separator` 拼接并可选 `header/footer` 包裹,产出一个完整注入块;为记忆注入合成块与战斗系统"底部战报块"预留的承载结构,本版本暂无 UI 入口
- **API 连接配置**:
  - 角色世界书(cwb)与一键生卡(autoCharCard)纳入旧配置自动迁移:老用户首次加载会把旧 URL / Key / 模型自动迁移为连接配置并分配槽位(一键生卡仅在规划者与执行者配置一致或规划者为空时迁移,避免悄悄改变行为)
  - **profile 已分配时参数控件 informational 化**:主面板 / 并发剧情优化 / 角色世界书 / 术语表的温度、maxTokens 控件在槽位分配 profile 后自动禁用并显示"由连接配置控制"提示,消除"改了没效果"的用户陷阱
  - **profile 状态卡新增"本设备无 Key"警示**:API Key 仅保存在最初填写它的设备/浏览器上(安全设计,不随云端设置同步),换设备后状态卡会直接亮出警示徽标,不必等到调用报错才发现
### 修复
- **独立聊天记忆从摆设变真功能(原作遗留坑)**:此前向量数据"随卡不随聊天"——开启"独立聊天记忆"后录入仍存进角色库、查询却去查一个从未被写入过的聊天集合、计数恒为 0,整体静默失效。现已重构为聊天级分桶:
  - 独立模式下,聊天记录类向量按当前聊天隔离存储与检索,同一张卡开多个聊天(不同剧情线)的记忆互不污染
  - 小说 / 世界书 / 手动录入属于"知识",仍随角色卡跨聊天共享;全局库不受影响
  - 知识管理列表为聊天专属库显示"聊天级"徽标;聊天级库禁止移动到全局
  - 统一模式(默认关闭独立记忆)的存量数据与行为完全不变
  - 已知限制:聊天专属记忆跟随聊天文件,重命名聊天文件会使其失联(与 ST 官方向量扩展同等限制)
- **超级排序截断顺序修正**:开启"超级排序"时,时序重排发生在 top_n 截断之前,导致保留的是"时序最早"而非"最相关"的块,检索结果长期偏向最旧的聊天记录。现改为先按相关度截取 top_n、再做时序排序
- **翰林院向量化失败("向量化块数量不识别"反馈)**:
  - 一次性清洗 profile-sync 历史污染:`retrieval/rerank.apiKey` 中的掩码占位符在持久层根治(此前仅读取侧防御);`apiEndpoint` / `rerank.apiMode` 的非法值(如被旧版写入的空字符串)归一化为 `custom`
  - 修复 `apiEndpoint` 为空/非法时请求被硬定向到 `api.openai.com`、无视用户自定义 URL 的问题(CSP 拦截 / 401 的元凶)
  - 修复**本地代理(LM Studio/Ollama)模式**自始就缺少 URL 分支、同样被错误定向到 openai.com 的问题
  - API 模式下拉补全 `OpenAI 官方` / `Azure` 选项;默认 API 模式改为 `custom`(与默认 URL 配套),新用户不再因选项缺失导致首次保存写入空值
  - profile-sync 给下拉框赋不存在选项值的污染源头修复(影响所有模块面板,不止翰林院)
- **Rerank "API Key 未提供"报错升级**:当原因是"连接配置在本设备没有可用 Key"时,报错会直接说明 Key 的设备本地性并指引到 API 连接配置重新填写(向量化 Google 直连、获取模型列表同步处理)
- **旧配置迁移**:一键生卡迁移时排除掩码占位符,避免把历史污染的假 Key 迁入新连接配置
- **超级记忆稳定性专项**(针对"工作不大稳定"反馈,4 处根因一次修复):
  - **切聊天竞态污染**:CHAT_CHANGED 时超级记忆立即全量同步,而表格系统延迟 100ms 才加载新聊天的表格,导致【旧聊天】的表格内容被写进【新角色】的记忆世界书;两边表名不同时旧表条目无 GC 兜底会**永久残留**("记忆串台"元凶)。现 CHAT_CHANGED 只确保世界书存在,新状态同步交由 `loadTables()` 完成后的自动推送,单次且时序正确
  - **死代码双轨存储拆除**:`saveStateToMetadata` / `tryRestoreStateFromMetadata` 把表格状态写到 `msg.metadata`——该字段非 ST 持久化位(同 v2.2.5 二次填表修过的坑),写入即蒸发、恢复永远为空,且每次同步还白调一次 `saveChat()`。整条链路删除,表格状态唯一信源为表格系统的 `msg.extra.amily2_tables_data`
  - **`awaitSync()` 穿透**:同步队列正忙时 `pushUpdate` 会用一个立即 resolve 的空 Promise 覆盖 `_syncPromise`,Pipeline Stage 4 等待形同虚设、后续阶段在同步未完成时被放行。现忙时不覆盖,正在运行的 drain 循环自然吃掉新入队项
  - **开关打开不生效**:启动时若总开关为关,初始化早退且不注册监听器;此后在 UI 勾选开关只写设置,超级记忆直到刷新页面前都是死的。现勾选即触发初始化(幂等)
  - 附带:`forceSyncAll` 的表格角色推断改为复用 `events-schema.inferTableRole`,消除两处重复逻辑漂移风险;每次切聊天的双倍全量同步(restore 路径一次 + 显式一次)随死代码移除归一
### 重构
- 表格核心 `manager.js` 瘦身(约 1050 → 600 行):19 个 UI 突变操作拆分至 `actions/ui-mutations.js`,SuperMemory 事件分发拆分至 `events-dispatch.js`;全部经 re-export 保持兼容,外部调用路径零改动
- 角色世界书最后 2 处散乱的厂商 URL 判断迁移至 `detectVendor` 统一入口,业务路径上不再有硬编码的 URL substring 判断
This commit is contained in:
Jenkins CI
2026-06-13 01:02:05 +08:00
parent 1a4a10d42d
commit 0d7e3b799e
35 changed files with 1981 additions and 1034 deletions

View File

@@ -5,16 +5,17 @@
* - 状态infra/store.js (currentTablesState / highlights / updatedTables)
* - 持久化infra/persistence.js (saveStateToMessage / commitToLastMessage)
* - 推演actions/applyOperations.js (executor.js 改造为 legacy formatter)
* - UI 突变actions/ui-mutations.js (addRow / addColumn / ... / clearAllTablesPhase 0.4)
* - SuperMemory 分发events-dispatch.js (dispatchTableUpdate / dispatchAllTablesUpdate)
* - 渲染rendering.js (3 个 toCsv)
* - 模板templates.js
* - 预设preset.js
*
* 本文件保留:
* - 默认表格模板 + getDefaultTables
* - SuperMemory 事件分发dispatchTableUpdate / dispatchAllTablesUpdate / triggerSync
* - triggerSyncSuperMemory 全量同步入口
* - loadTables 的多档回退逻辑
* - 16 个 UI 突变addRow / addColumn / ... / clearAllTables
* - updateTableFromText 编排
* - updateTableFromText / updateTableFromOps 编排
* - rollbackState / rollbackAndRefill
*
* 所有原先 export 的接口一律保留兼容(移走的统一 re-export调用方零改动。
@@ -33,7 +34,31 @@ import { applyOperations } from './actions/applyOperations.js';
import { fillWithSecondaryApi } from './secondary-filler.js';
import { renderTables } from '../../ui/table-bindings.js';
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
import { createTableUpdateEvent, inferTableRole } from './events-schema.js';
import { dispatchTableUpdate, dispatchAllTablesUpdate } from './events-dispatch.js';
// ── UI 突变Phase 0.4 迁至 actions/ui-mutations.js此处 re-export 保持兼容) ──
import { commitPendingDeletions } from './actions/ui-mutations.js';
export {
deleteColumn,
moveRow,
insertRow,
addRow,
addColumn,
updateHeader,
deleteRow,
restoreRow,
commitPendingDeletions,
insertColumn,
moveColumn,
deleteTable,
addTable,
renameTable,
moveTable,
updateTableRules,
updateRow,
clearAllTables,
updateColumnWidth,
} from './actions/ui-mutations.js';
// ── 新模块IAD 拆分后的依赖) ────────────────────────────────────────────
import {
@@ -77,45 +102,7 @@ import {
importGlobalPreset as _presetImportGlobalPreset,
} from './preset.js';
// ── 私有:SuperMemory 事件分发 ────────────────────────────────────────────
/**
* 把单个表格的最新状态推送给 SuperMemory优先 Bus 直调,降级 CustomEvent
* @param {number} tableIndex
*/
function dispatchTableUpdate(tableIndex) {
const settings = extension_settings[extensionName] || {};
if (settings.super_memory_enabled === false) return;
const state = getState();
if (!state || !state[tableIndex]) return;
const table = state[tableIndex];
const role = inferTableRole(table.name);
const smBus = window.Amily2Bus?.query('SuperMemory');
if (smBus?.pushUpdate) {
smBus.pushUpdate({
tableName: table.name,
data: table.rows,
headers: table.headers,
rowStatuses: table.rowStatuses ?? [],
role,
});
} else {
document.dispatchEvent(createTableUpdateEvent(table));
}
log(`[SuperMemory] Dispatched update for ${table.name} (role: ${role})`, 'info');
}
/**
* 触发所有表格的全量同步Pipeline 变更后调用)。
*/
function dispatchAllTablesUpdate() {
const state = getState();
if (!state) return;
log('[SuperMemory] Dispatching update events for ALL tables...', 'info');
state.forEach((_, index) => dispatchTableUpdate(index));
}
// ── SuperMemory 同步入口 ──────────────────────────────────────────────────
/**
* 主动触发所有表格同步到 SuperMemory外部调用入口
@@ -352,438 +339,6 @@ export function saveTables(sourceAction = '未知操作') {
return true;
}
// ── 16 个 UI 突变 ─────────────────────────────────────────────────────────
export function deleteColumn(tableIndex, colIndex) {
const tables = getState();
if (!tables || !tables[tableIndex] || colIndex < 0 || colIndex >= tables[tableIndex].headers.length) {
log(`删除列失败:在表格 ${tableIndex} 中找不到索引为 ${colIndex} 的列。`, 'error');
return;
}
tables[tableIndex].headers.splice(colIndex, 1);
tables[tableIndex].rows.forEach(row => {
if (row.length > colIndex) row.splice(colIndex, 1);
});
if (tables[tableIndex].columnWidths && tables[tableIndex].columnWidths.length > colIndex) {
tables[tableIndex].columnWidths.splice(colIndex, 1);
}
log(`成功删除了表格 ${tableIndex} 的第 ${colIndex + 1} 列。`, 'success');
saveTables(tables);
dispatchTableUpdate(tableIndex);
}
export function moveRow(tableIndex, rowIndex, direction) {
const tables = getState();
const table = tables?.[tableIndex];
if (!table || rowIndex < 0 || rowIndex >= table.rows.length) return;
const newIndex = direction === 'up' ? rowIndex - 1 : rowIndex + 1;
if (newIndex < 0 || newIndex >= table.rows.length) return;
const [movedRow] = table.rows.splice(rowIndex, 1);
table.rows.splice(newIndex, 0, movedRow);
if (table.rowStatuses && table.rowStatuses.length === table.rows.length + 1) {
const [movedStatus] = table.rowStatuses.splice(rowIndex, 1);
table.rowStatuses.splice(newIndex, 0, movedStatus);
}
log(`成功将表格 ${tableIndex} 的第 ${rowIndex + 1} 行移动到第 ${newIndex + 1} 行。`, 'success');
saveTables(tables);
dispatchTableUpdate(tableIndex);
}
export function insertRow(tableIndex, data, position = 'below') {
const tables = getState();
const table = tables?.[tableIndex];
if (!table) {
log(`插入行失败:找不到索引为 ${tableIndex} 的表格。`, 'error');
return;
}
let insertIndex;
if (typeof data === 'number') {
insertIndex = position === 'above' ? data : data + 1;
} else {
insertIndex = table.rows.length;
}
if (insertIndex < 0) insertIndex = 0;
if (insertIndex > table.rows.length) insertIndex = table.rows.length;
const newRow = new Array(table.headers.length).fill('');
if (typeof data === 'object' && data !== null) {
for (const colIndex in data) {
const cIndex = parseInt(colIndex, 10);
if (!isNaN(cIndex) && cIndex < newRow.length) {
newRow[cIndex] = data[colIndex];
addHighlight(tableIndex, insertIndex, cIndex);
}
}
}
table.rows.splice(insertIndex, 0, newRow);
if (!table.rowStatuses) table.rowStatuses = Array(table.rows.length).fill('normal');
table.rowStatuses.splice(insertIndex, 0, 'normal');
markTableUpdated(tableIndex);
dispatchTableUpdate(tableIndex);
log(`成功在表格 ${table.name} (索引 ${tableIndex}) 的第 ${insertIndex + 1} 行位置插入了新行。`, 'success');
commitToLastMessage(tables);
}
export function addRow(tableIndex) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex];
const colCount = table.headers.length;
const newRow = Array(colCount).fill('');
table.rows.push(newRow);
if (!table.rowStatuses) table.rowStatuses = Array(table.rows.length).fill('normal');
table.rowStatuses.push('normal');
markTableUpdated(tableIndex);
dispatchTableUpdate(tableIndex);
log(`表格 [${table.name}] 新增了一行。`, 'info');
commitToLastMessage(tables);
}
export function addColumn(tableIndex) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex];
const newHeader = `新列 ${table.headers.length + 1}`;
table.headers.push(newHeader);
table.rows.forEach(row => row.push(''));
if (!table.columnWidths) table.columnWidths = [];
table.columnWidths.push(null);
log(`表格 [${table.name}] 新增了一列。`, 'info');
commitToLastMessage(tables);
}
export function updateHeader(tableIndex, colIndex, value) {
const tables = getState();
if (!tables || !tables[tableIndex] || tables[tableIndex].headers[colIndex] === undefined) return;
const tableName = tables[tableIndex].name;
const originalHeader = tables[tableIndex].headers[colIndex];
tables[tableIndex].headers[colIndex] = value;
log(`表格 [${tableName}] 的表头“${originalHeader}”已更新为“${value}”。`, 'info');
commitToLastMessage(tables);
}
export async function deleteRow(tableIndex, rowIndex) {
const tables = getState();
const table = tables?.[tableIndex];
if (!table || !table.rows[rowIndex]) return;
if (!table.rowStatuses) {
table.rowStatuses = Array(table.rows.length).fill('normal');
}
table.rowStatuses[rowIndex] = 'pending-deletion';
markTableUpdated(tableIndex);
log(`表格 [${table.name}] 的第 ${rowIndex + 1} 行已标记为待删除。`, 'info');
const context = getContext();
if (context.chat?.length > 0) {
const lastMessage = context.chat[context.chat.length - 1];
if (_persistSaveStateToMessage(tables, lastMessage)) {
await saveChat();
renderTables();
dispatchTableUpdate(tableIndex);
return;
}
}
await saveChatDebounced();
renderTables();
dispatchTableUpdate(tableIndex);
}
export async function restoreRow(tableIndex, rowIndex) {
const tables = getState();
const table = tables?.[tableIndex];
if (!table || !table.rows[rowIndex] || !table.rowStatuses) return;
table.rowStatuses[rowIndex] = 'normal';
markTableUpdated(tableIndex);
log(`表格 [${table.name}] 的第 ${rowIndex + 1} 行已恢复。`, 'info');
const context = getContext();
if (context.chat?.length > 0) {
const lastMessage = context.chat[context.chat.length - 1];
if (_persistSaveStateToMessage(tables, lastMessage)) {
await saveChat();
renderTables();
dispatchTableUpdate(tableIndex);
return;
}
}
await saveChatDebounced();
renderTables();
dispatchTableUpdate(tableIndex);
}
export function commitPendingDeletions() {
const tables = getState();
if (!tables) return false;
let deletionCount = 0;
tables.forEach((table, tableIndex) => {
if (!table.rowStatuses || table.rowStatuses.length === 0) return;
let tableHadDeletions = false;
for (let i = table.rows.length - 1; i >= 0; i--) {
if (table.rowStatuses[i] === 'pending-deletion') {
table.rows.splice(i, 1);
table.rowStatuses.splice(i, 1);
deletionCount++;
tableHadDeletions = true;
}
}
if (tableHadDeletions) markTableUpdated(tableIndex);
});
if (deletionCount > 0) {
log(`已提交并永久删除了 ${deletionCount} 行。`, 'info');
const updated = _storeGetUpdatedTables();
if (updated.size > 0) {
updated.forEach(tableIndex => dispatchTableUpdate(tableIndex));
}
return true;
}
return false;
}
export function insertColumn(tableIndex, colIndex, position) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex];
const insertAt = position === 'left' ? colIndex : colIndex + 1;
table.headers.splice(insertAt, 0, '新列');
table.rows.forEach(row => row.splice(insertAt, 0, ''));
if (!table.columnWidths) table.columnWidths = [];
table.columnWidths.splice(insertAt, 0, null);
log(`表格 [${table.name}] 在第 ${colIndex + 1} 列的${position === 'left' ? '左侧' : '右侧'}插入了新列。`, 'info');
commitToLastMessage(tables);
}
export function moveColumn(tableIndex, colIndex, direction) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex];
const headers = table.headers;
const rows = table.rows;
const targetIndex = direction === 'left' ? colIndex - 1 : colIndex + 1;
if (targetIndex < 0 || targetIndex >= headers.length) {
log(`无法移动列:索引 ${colIndex} 已在边界。`, 'warn');
return;
}
const [headerToMove] = headers.splice(colIndex, 1);
headers.splice(targetIndex, 0, headerToMove);
rows.forEach(row => {
const [cellToMove] = row.splice(colIndex, 1);
row.splice(targetIndex, 0, cellToMove);
});
if (table.columnWidths && table.columnWidths.length > colIndex) {
const [widthToMove] = table.columnWidths.splice(colIndex, 1);
table.columnWidths.splice(targetIndex, 0, widthToMove);
}
log(`表格 [${table.name}] 的列“${headerToMove}”已向${direction === 'left' ? '左' : '右'}移动。`, 'info');
commitToLastMessage(tables);
}
export function deleteTable(tableIndex) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const tableName = tables[tableIndex].name;
tables.splice(tableIndex, 1);
log(`表格 [${tableName}] 已被成功废黜。`, 'success');
const success = commitToLastMessage(tables);
if (success) {
log('废黜表格后的状态已强制写入最新消息并立即保存。', 'success');
} else {
log('无法找到可锚定的消息或保存失败,删除操作可能不会被持久化!', 'error');
}
}
export function addTable(tableName) {
if (!tableName || !tableName.trim()) {
log('无法创建表格:名称不能为空。', 'error');
toastr.error('表格名称不能为空。', '创建失败');
return;
}
let tables = getState();
if (!tables) {
loadTables();
tables = getState();
}
if (tables.some(table => table.name === tableName.trim())) {
log(`无法创建表格:名为 "${tableName}" 的表格已存在。`, 'error');
toastr.error(`名为 "${tableName}" 的表格已存在。`, '创建失败');
return;
}
const newTable = {
name: tableName.trim(),
headers: ['新列 1'],
rows: [],
rowStatuses: [],
columnWidths: [],
note: '这是一个新创建的表格。',
rule_add: '允许',
rule_delete: '允许',
rule_update: '允许',
charLimitRules: {},
rowLimitRule: 0,
};
tables.push(newTable);
log(`已成功创建新表格:[${tableName.trim()}]。`, 'success');
const success = commitToLastMessage(tables);
if (success) {
log('新表格状态已强制写入最新消息并立即保存。', 'success');
} else {
log('无法找到可锚定的消息或保存失败,新表格可能不会被持久化!', 'error');
}
}
export function renameTable(tableIndex, newName) {
const tables = getState();
if (!tables || !tables[tableIndex]) {
log('重命名失败:表格不存在。', 'error');
toastr.error('表格不存在。', '重命名失败');
return;
}
const trimmedName = newName.trim();
if (!trimmedName) {
log('重命名失败:名称不能为空。', 'error');
toastr.error('表格名称不能为空。', '重命名失败');
return;
}
if (tables.some((table, index) => index !== tableIndex && table.name === trimmedName)) {
log(`重命名失败:名为 "${trimmedName}" 的表格已存在。`, 'error');
toastr.error(`名为 "${trimmedName}" 的表格已存在。`, '重命名失败');
return;
}
const oldName = tables[tableIndex].name;
tables[tableIndex].name = trimmedName;
log(`表格 "${oldName}" 已重命名为 "${trimmedName}"。`, 'success');
commitToLastMessage(tables);
}
export function moveTable(tableIndex, direction) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const newIndex = direction === 'up' ? tableIndex - 1 : tableIndex + 1;
if (newIndex < 0 || newIndex >= tables.length) {
log(`无法移动表格:索引 ${tableIndex} 已在边界。`, 'warn');
return;
}
const temp = tables[tableIndex];
tables[tableIndex] = tables[newIndex];
tables[newIndex] = temp;
log(`表格 [${temp.name}] 的顺序已调整。`, 'success');
const success = commitToLastMessage(tables);
if (success) {
log('表格顺序调整后的状态已强制写入最新消息并立即保存。', 'success');
} else {
log('无法找到可锚定的消息或保存失败,顺序调整可能不会被持久化!', 'error');
}
}
export function updateTableRules(tableIndex, newRules) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex];
table.note = newRules.note;
table.rule_add = newRules.rule_add;
table.rule_delete = newRules.rule_delete;
table.rule_update = newRules.rule_update;
table.charLimitRules = newRules.charLimitRules;
table.rowLimitRule = newRules.rowLimitRule;
table.simplifyRowThreshold = newRules.simplifyRowThreshold;
delete table.charLimitRule;
log(`表格 [${table.name}] 的规则已更新。`, 'info');
commitToLastMessage(tables);
}
export function updateRow(tableIndex, rowIndex, data) {
const tables = getState();
if (!tables || !tables[tableIndex]) {
log(`AI指令错误尝试在不存在的表格索引 ${tableIndex} 中操作。`, 'error');
return;
}
const table = tables[tableIndex];
if (rowIndex >= table.rows.length) {
log(`AI指令意图更新不存在的行 (rowIndex: ${rowIndex}),已智能转换为在表格 [${table.name}] 末尾新增一行。`, 'warn');
insertRow(tableIndex, data);
return;
}
const row = table.rows[rowIndex];
for (const colIndex in data) {
const cIndex = parseInt(colIndex, 10);
if (cIndex < row.length) {
row[cIndex] = data[cIndex];
addHighlight(tableIndex, rowIndex, cIndex);
}
}
markTableUpdated(tableIndex);
dispatchTableUpdate(tableIndex);
log(`AI 指令更新了表格 [${table.name}] 的第 ${rowIndex + 1} 行。`, 'info');
commitToLastMessage(tables);
}
export function clearAllTables() {
const tables = getState();
if (!tables) {
log('无法清空:当前表格状态为空。', 'error');
return;
}
tables.forEach((table, tableIndex) => {
if (table.rows.length > 0) markTableUpdated(tableIndex);
table.rows = [];
table.rowStatuses = [];
});
log('所有表格的行数据已在内存中清空。', 'warn');
dispatchAllTablesUpdate();
const success = commitToLastMessage(tables);
if (success) {
log('清空行数据后的状态已强制写入最新消息并立即保存。', 'success');
toastr.success('所有表格的剧情内容已清空。', '操作完成');
} else {
log('无法找到可锚定的消息或保存失败,清空操作可能不会被持久化!', 'error');
}
}
// ── 渲染 wrapper注入当前 state ────────────────────────────────────────
export function convertTablesToCsvString() {
@@ -1031,19 +586,6 @@ export async function rollbackAndRefill() {
// ── 杂项 ──────────────────────────────────────────────────────────────────
export function updateColumnWidth(tableIndex, colIndex, width) {
const tables = getState();
if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex];
if (!table.columnWidths) table.columnWidths = [];
while (table.columnWidths.length < table.headers.length) {
table.columnWidths.push(null);
}
table.columnWidths[colIndex] = width;
commitToLastMessage(tables);
}
export function isCurrentTablesEmpty() {
const tables = getState();
if (!tables || tables.length === 0) return true;