Files
ST-Amily2-Chat-Optimisation/core/table-system/manager.js
Jenkins CI 0d7e3b799e 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 判断
2026-06-13 01:02:05 +08:00

599 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file manager.js —— Phase 0 重构后承担"剩余的编排层"。
*
* 大部分功能已迁出:
* - 状态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
* - triggerSyncSuperMemory 全量同步入口)
* - loadTables 的多档回退逻辑
* - updateTableFromText / updateTableFromOps 编排
* - rollbackState / rollbackAndRefill
*
* 所有原先 export 的接口一律保留兼容(移走的统一 re-export调用方零改动。
*
* @typedef {import('./dto/Table.js').TableState} TableState
*/
import { getContext, extension_settings } from '/scripts/extensions.js';
import { saveChat } from '/script.js';
import { saveChatDebounced } from '../../utils/utils.js';
import { extensionName } from '../../utils/settings.js';
import { log } from './logger.js';
import { executeCommands } from './executor.js';
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 { 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 {
getState,
setState,
addHighlight as _storeAddHighlight,
getHighlights as _storeGetHighlights,
clearHighlights as _storeClearHighlights,
markTableUpdated,
getUpdatedTables as _storeGetUpdatedTables,
clearUpdatedTables as _storeClearUpdatedTables,
} from './infra/store.js';
import {
saveStateToMessage as _persistSaveStateToMessage,
commitToLastMessage,
TABLE_DATA_KEY,
} from './infra/persistence.js';
import {
tablesToCsv,
tablesToCsvWithSelection,
tablesToCsvContentOnly,
} from './rendering.js';
import {
getBatchFillerRuleTemplate as _tplGetBatchFillerRuleTemplate,
saveBatchFillerRuleTemplate as _tplSaveBatchFillerRuleTemplate,
getBatchFillerFlowTemplate as _tplGetBatchFillerFlowTemplate,
saveBatchFillerFlowTemplate as _tplSaveBatchFillerFlowTemplate,
getAiFlowTemplateForInjection as _tplGetAiFlowTemplateForInjection,
saveAiTemplate as _tplSaveAiTemplate,
getAiTemplate as _tplGetAiTemplate,
} from './templates.js';
import {
exportPreset as _presetExportPreset,
exportPresetFull as _presetExportPresetFull,
importPreset as _presetImportPreset,
clearGlobalPreset as _presetClearGlobalPreset,
importGlobalPreset as _presetImportGlobalPreset,
} from './preset.js';
// ── SuperMemory 同步入口 ──────────────────────────────────────────────────
/**
* 主动触发所有表格同步到 SuperMemory外部调用入口
*/
export function triggerSync() {
dispatchAllTablesUpdate();
}
// ── 状态访问store 包装层) ──────────────────────────────────────────────
export function addHighlight(tableIndex, rowIndex, colIndex) {
_storeAddHighlight(tableIndex, rowIndex, colIndex);
}
export function getHighlights() {
return _storeGetHighlights();
}
export function clearHighlights() {
_storeClearHighlights();
}
export function getUpdatedTables() {
return _storeGetUpdatedTables();
}
export function clearUpdatedTables() {
_storeClearUpdatedTables();
}
export function setMemoryState(newState) {
setState(newState);
}
export function getMemoryState() {
return getState();
}
export function loadMemoryState(state) {
if (!state) return;
setState(state);
renderTables();
updateOrInsertTableInChat();
log('[SuperMemory] 已从元数据恢复内存状态并刷新 UI。', 'info');
}
export function saveMemoryState() {
const context = getContext();
if (context.chat && context.chat.length > 0) {
const lastMessage = context.chat[context.chat.length - 1];
if (_persistSaveStateToMessage(getState(), lastMessage)) {
// 不在此处强制 saveChat避免高频调用方决定时机
return true;
}
}
return false;
}
export function saveStateToMessage(stateToSave, targetMessage) {
return _persistSaveStateToMessage(stateToSave, targetMessage);
}
// ── 默认模板 ──────────────────────────────────────────────────────────────
const defaultTemplate = {
"tables": [
{
"name": "时空栏",
"headers": ["日期", "时段", "时间", "地点", "此地角色"],
"note": "【核心作用】此表格用于精确追踪故事发生的即时时空背景,确保时间与空间的连续性。它应该始终只包含一行,代表当前的“镜头”位置。\n【字段详解】\n- 日期: 格式为'YYYY-MM-DD'。若日期未知,请根据上下文合理推断或设定一个初始日期,如'大夏3年-9月-10日'。\n- 时段: 严格遵循规定凌晨0-5时早晨5-8时上午8-11时中午11-13时下午13-16时傍晚16-19时晚上19-24时。\n- 时间: 格式为'HH:MM'。若时间未知,可根据时段估算,如'08:30'。\n- 地点: 描述当前场景发生的具体位置,应尽可能精确,例如'XX街的咖啡馆'而非'城里'。\n- 此地角色: 列出当前场景中所有在场且参与互动的主要角色,用'/'分隔。",
"rule_add": "【触发条件】当故事开始,且此表格为空时,必须立即根据初始场景创建第一行。",
"rule_delete": "【触发条件】任何时候,如果此表格的行数超过一行,必须删除旧的行,只保留最新、最准确的一行。",
"rule_update": "【触发条件】当以下任一情况发生时,必须更新此行:\n1. 时间发生显著跳跃(例如,'几小时后'、'第二天')。\n2. 角色从一个地点移动到另一个地点。\n3. 场景中关键角色的出入导致在场人员发生变化。",
"charLimitRules": {},
"rowLimitRule": 1,
"rows": []
},
{
"name": "角色栏",
"headers": ["角色名", "外貌", "身形", "衣着", "性格", "身份", "职业", "与<user>关系", "爱好", "住所", "其他重要信息"],
"note": "【核心作用】此表格是角色关系和状态的核心数据库,用于记录所有在故事中出现的重要角色的详细信息。\n【字段详解】\n- 角色名: 角色的唯一标识。\n- 外貌: 描述五官、发型、发色、肤色等面部特征。\n- 身形: 描述身高、体型、肌肉状况、特殊身体标记(如伤疤)等。\n- 衣着: 描述角色当前或标志性的穿着,包括服装、配饰等。\n- 性格: 概括角色的核心性格特质使用1-3个关键词如'勇敢/鲁莽/忠诚'。\n- 身份: 角色的社会背景或出身,如'贵族后裔'、'流浪者'。\n- 职业: 角色赖以谋生的工作或职责,如'佣兵'、'学者'。\n- 与<user>关系: 描述该角色与主角<user>之间的社会或情感关系,如'盟友'、'导师'、'敌人'。\n- 爱好: 角色的兴趣和消遣活动。\n- 住所: 角色的常住地。\n- 其他重要信息: 记录任何不属于以上类别但对角色至关重要的信息,如特殊能力、过去的经历等。",
"rule_add": "【触发条件】当一个有名有姓的角色首次出现,并与<user>或当前剧情发生有意义的互动时,必须为其创建新的一行。",
"rule_delete": "【触发条件】当一个角色被确认永久性死亡(非假死或失踪),且其存在不再对后续剧情有直接影响时,可以删除该行。",
"rule_update": "【触发条件】当角色的任何信息发生持久性或关键性变化时,必须更新对应单元格。例如:\n1. 外貌/身形/衣着发生永久性改变(如断肢、换上新装备)。\n2. 性格因重大事件而扭转。\n3. 身份或职业发生变更(如继承王位、被解雇)。\n4. 与<user>的关系发生根本性转变(如从敌人变为盟友)。",
"charLimitRules": { "10": 30 },
"rowLimitRule": 0,
"rows": []
},
{
"name": "关系栏",
"headers": ["主动方", "被动方", "关系", "详情"],
"columnWidths": [],
"note": "【核心作用】专门用于记录除主角<user>以外的角色之间的复杂人际关系网NPC to NPC。\n【字段详解】\n- 主动方: 关系的发起者或主体(例如'艾克')。\n- 被动方: 关系的接收者或对象(例如'莉娜')。\n- 关系: 用简短的词汇描述两者之间的关系本质,如'暗恋'、'世仇'、'师徒'。\n- 详情: 对这段关系的具体描述或背景补充。",
"rule_add": "【触发条件】当两个NPC之间展现出明确的、非临时性的人际关系时应添加新行。",
"rule_delete": "【触发条件】当两个NPC之间的关系彻底断绝且不再影响剧情或者其中一方彻底消失/死亡时,可以删除。",
"rule_update": "【触发条件】当两个NPC之间的关系性质发生转变如从'盟友'变为'背叛者')时,必须更新。",
"charLimitRules": {},
"rowLimitRule": 0,
"rows": [],
"rowStatuses": []
},
{
"name": "任务栏",
"headers": ["任务名", "类型", "详情", "状态", "执行者", "地点", "开始时间/结束时间", "结果"],
"note": "【核心作用】追踪故事中的主要情节线、目标和挑战。只记录对剧情发展有重大影响的“任务”,忽略日常琐事。\n【字段详解】\n- 任务名: 任务的简洁概括,如'寻找失落的神器'。\n- 类型: 任务的分类,如'主线'、'支线'、'个人'、'约定'。\n- 详情: 对任务目标和背景的简要描述。\n- 状态: 任务的当前进展,如'未开始'、'进行中'、'已完成'、'已失败'、'已取消'。\n- 执行者: 负责完成此任务的角色名。\n- 地点: 任务关键环节发生的地点。\n- 开始时间/结束时间: 记录任务的起止时间,格式'YYYY-MM-DD',若未结束则结束时间留空。\n- 结果: 任务完成或失败后的最终结果。",
"rule_add": "【触发条件】当以下情况发生时,应添加新行:\n1. 角色接下一个明确的、有目标的委托或命令。\n2. 角色们达成一个具体的、需要在未来执行的约定。\n3. 角色为自己设定一个长期的、关键性的目标。",
"rule_delete": "【触发条件】当任务列表超过10行时优先删除最早的、已经“已完成”且与当前剧情关联度最低的任务。如果存在内容完全重复的任务应删除。",
"rule_update": "【触发条件】当任务的“状态”发生任何变化时,必须更新。例如,从'进行中'变为'已完成'。当任务的“详情”或“结果”有新的关键信息补充时,也应更新。",
"charLimitRules": {},
"rowLimitRule": 10,
"rows": []
},
{
"name": "物品栏",
"headers": ["物品名", "类型", "详情", "状态", "拥有者", "重要原因"],
"note": "【核心作用】记录那些在故事中具有特殊功能、背景或情感价值的关键物品。普通物品不应记录。\n【字段详解】\n- 物品名: 物品的名称。\n- 类型: 物品的分类,如'武器'、'道具'、'信物'、'关键物品'。\n- 详情: 描述物品的外观、材质和已知功能。\n- 状态: 物品的当前状况,如'完好'、'破损'、'能量耗尽'。\n- 拥有者: 当前持有该物品的角色名。\n- 重要原因: 解释该物品为何重要,例如'是解开谜题的钥匙'或'是母亲的遗物'。",
"rule_add": "【触发条件】当一个物品被明确赋予了特殊意义(如被赠予、在关键事件中扮演重要角色)或展示出独特功能时,应为其创建条目。",
"rule_delete": "【触发条件】当一个物品被彻底摧毁、消耗完毕或永久失去其特殊意义时,可以删除。",
"rule_update": "【触发条件】当物品的“状态”(如被损坏)、“拥有者”(如被转交或被盗)或“详情”(如发现了新功能)发生变化时,必须更新。",
"charLimitRules": {},
"rowLimitRule": 0,
"rows": []
},
{
"name": "技能栏",
"headers": ["技能名", "技能效果"],
"note": "【核心作用】专门用于记录主角<user>掌握的各种技能、魔法、被动能力或特殊专长。\n【字段详解】\n- 技能名: 技能的正式名称。\n- 技能效果: 清晰、简洁地描述该技能使用时产生的具体效果、消耗和限制条件。",
"rule_add": "【触发条件】当<user>在故事中首次成功施展或习得一个全新的、表格中未记录的技能时,必须添加。",
"rule_delete": "【触发条件】如果发现表格中存在两个描述完全相同的重复技能,应删除其中一个。如果记录了非<user>的技能,应立即删除。",
"rule_update": "【触发条件】当一个已知技能的效果发生进化、变异或被添加了新的限制/效果时(例如,技能升级),必须更新其“技能效果”描述。",
"charLimitRules": {},
"rowLimitRule": 0,
"rows": []
},
{
"name": "设定栏",
"headers": ["类型", "具体描述"],
"note": "【核心作用】此表格记录了来自<user>的、超越故事本身的“元指令”或世界观设定拥有最高解释权。内容应被严格遵守禁止AI自行修改。\n【字段详解】\n- 类型: 指令的分类,如'世界观设定'、'剧情走向要求'、'角色行为禁令'。\n- 具体描述: 完整、准确地记录<user>提出的具体要求。",
"rule_add": "【触发条件】当<user>通过括号、旁白或其他明确的“第四面墙”方式,提出关于故事背景、规则或未来走向的指令时,必须记录于此。",
"rule_delete": "【触发条件】只能在<user>明确表示要移除或废弃某条设定时,才能删除对应行。",
"rule_update": "【触发条件】只能在<user>明确表示要修改某条设定时,才能更新对应行的描述。",
"charLimitRules": {},
"rowLimitRule": 0,
"rows": []
}
]
};
function getDefaultTables() {
log('从预设模板生成默认表格...', 'info');
const tables = JSON.parse(JSON.stringify(defaultTemplate.tables));
tables.forEach(table => {
table.charLimitRule = { columnIndex: -1, limit: 0 };
table.rowLimitRule = 0;
table.columnWidths = [];
});
return tables;
}
// ── 加载 ──────────────────────────────────────────────────────────────────
export function loadTables(stopIndex = -1) {
const context = getContext();
// 1. 优先从聊天记录中找已存的状态
if (context && context.chat && context.chat.length > 0) {
const startIndex = (stopIndex === -1 ? context.chat.length - 1 : stopIndex - 1);
for (let i = startIndex; i >= 0; i--) {
const message = context.chat[i];
if (message.extra && message.extra[TABLE_DATA_KEY]) {
log(`在第 ${i} 条消息中找到基准表格数据。`, 'info');
let loadedState = JSON.parse(JSON.stringify(message.extra[TABLE_DATA_KEY]));
loadedState.forEach(table => {
if (table.note === undefined) table.note = '无';
if (table.rule_add === undefined) table.rule_add = '允许';
if (table.rule_delete === undefined) table.rule_delete = '允许';
if (table.rule_update === undefined) table.rule_update = '允许';
// 多列规则兼容
if (table.charLimitRule && !table.charLimitRules) {
table.charLimitRules = {};
if (table.charLimitRule.columnIndex !== -1 && table.charLimitRule.limit > 0) {
table.charLimitRules[table.charLimitRule.columnIndex] = table.charLimitRule.limit;
}
}
delete table.charLimitRule;
if (table.rowLimitRule === undefined) table.rowLimitRule = 0;
if (table.columnWidths === undefined) table.columnWidths = [];
if (!table.rowStatuses) {
table.rowStatuses = Array(table.rows.length).fill('normal');
}
});
setState(loadedState);
dispatchAllTablesUpdate();
return getState();
}
}
}
// 2. 全局预设
if (extension_settings[extensionName]?.global_table_preset) {
log('未在聊天记录中找到表格,正在加载全局预设...', 'info');
try {
const globalPreset = extension_settings[extensionName].global_table_preset;
setState(JSON.parse(JSON.stringify(globalPreset.tables)));
if (globalPreset.batchFillerRuleTemplate !== undefined) {
_tplSaveBatchFillerRuleTemplate(globalPreset.batchFillerRuleTemplate);
}
if (globalPreset.batchFillerFlowTemplate !== undefined) {
_tplSaveBatchFillerFlowTemplate(globalPreset.batchFillerFlowTemplate);
}
dispatchAllTablesUpdate();
return getState();
} catch (error) {
log(`加载全局预设失败: ${error.message}`, 'error');
}
}
// 3. 默认模板
log('未找到任何表格数据或全局预设,使用默认模板。', 'info');
setState(getDefaultTables());
dispatchAllTablesUpdate();
return getState();
}
export function saveTables(sourceAction = '未知操作') {
log(`UI操作 "${sourceAction}" 已更新内存状态。`, 'info');
return true;
}
// ── 渲染 wrapper注入当前 state ────────────────────────────────────────
export function convertTablesToCsvString() {
let state = getState();
if (!state) {
loadTables();
state = getState();
}
return tablesToCsv(state);
}
export function convertSelectedTablesToCsvString(selectedIndices) {
let state = getState();
if (!state) {
loadTables();
state = getState();
}
return tablesToCsvWithSelection(state, selectedIndices);
}
export function convertTablesToCsvStringForContentOnly() {
return tablesToCsvContentOnly(getState());
}
// ── 模板re-export ─────────────────────────────────────────────────────
export const getBatchFillerRuleTemplate = _tplGetBatchFillerRuleTemplate;
export const saveBatchFillerRuleTemplate = _tplSaveBatchFillerRuleTemplate;
export const getBatchFillerFlowTemplate = _tplGetBatchFillerFlowTemplate;
export const saveBatchFillerFlowTemplate = _tplSaveBatchFillerFlowTemplate;
export const getAiFlowTemplateForInjection = _tplGetAiFlowTemplateForInjection;
export const saveAiTemplate = _tplSaveAiTemplate;
export const getAiTemplate = _tplGetAiTemplate;
// ── 文本指令应用updateTableFromText ───────────────────────────────────
export async function updateTableFromText(textContent, options = {}) {
const settings = extension_settings[extensionName] || {};
if (settings.table_system_enabled === false) {
log('表格系统总开关已关闭,跳过 <Amily2Edit> 标签处理。', 'info');
return;
}
if (!textContent) {
log('AI返回内容为空无法更新表格。', 'warn');
return;
}
const { finalState, hasChanges, changes } = executeCommands(textContent, getState());
if (!hasChanges) {
log('AI指令未产生任何实质性变更。', 'info');
return;
}
setState(finalState);
if (options.immediateDelete) {
commitPendingDeletions();
}
changes.forEach(change => {
markTableUpdated(change.tableIndex);
if (change.type === 'update' || change.type === 'insert') {
if (change.rowIndex !== undefined && change.colIndex !== undefined) {
addHighlight(change.tableIndex, change.rowIndex, change.colIndex);
}
}
});
log(`成功执行了 ${changes.length} 处变更。`, 'success');
const affectedTables = [...new Set(changes.map(c => c.tableIndex))];
affectedTables.forEach(tableIndex => dispatchTableUpdate(tableIndex));
const context = getContext();
if (context.chat && context.chat.length > 0) {
const lastMessage = context.chat[context.chat.length - 1];
if (_persistSaveStateToMessage(getState(), lastMessage)) {
await saveChat();
toastr.success('已根据AI的指示成功更新表格', '填表完成');
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
return;
}
}
saveChatDebounced();
toastr.success('已根据AI的指示成功更新表格', '填表完成');
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
}
/**
* 直接从 Operation[] 应用变更Function Call 路径),跳过文本解析。
* 后续流程与 updateTableFromText 完全一致。
*
* @param {import('./dto/Operation.js').Operation[]} ops
* @param {Object} options - 同 updateTableFromText 的 options
*/
export async function updateTableFromOps(ops, options = {}) {
const settings = extension_settings[extensionName] || {};
if (settings.table_system_enabled === false) return;
if (!Array.isArray(ops) || ops.length === 0) {
log('Function Call 返回操作列表为空,无需更新表格。', 'info');
return;
}
const { state, changes } = applyOperations(getState(), ops);
if (changes.length === 0) {
log('Function Call 操作未产生任何实质性变更。', 'info');
return;
}
setState(state);
if (options.immediateDelete) {
commitPendingDeletions();
}
changes.forEach(change => {
markTableUpdated(change.tableIndex);
if (change.type === 'update' || change.type === 'insert') {
if (change.rowIndex !== undefined && change.colIndex !== undefined) {
addHighlight(change.tableIndex, change.rowIndex, change.colIndex);
}
}
});
log(`Function Call 成功执行了 ${changes.length} 处变更。`, 'success');
const affectedTables = [...new Set(changes.map(c => c.tableIndex))];
affectedTables.forEach(tableIndex => dispatchTableUpdate(tableIndex));
const context = getContext();
if (context.chat && context.chat.length > 0) {
const lastMessage = context.chat[context.chat.length - 1];
if (_persistSaveStateToMessage(getState(), lastMessage)) {
await saveChat();
toastr.success('已根据AI的指示成功更新表格', '填表完成');
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
return;
}
}
saveChatDebounced();
toastr.success('已根据AI的指示成功更新表格', '填表完成');
document.dispatchEvent(new CustomEvent('amily2-force-ui-reload'));
}
// ── 预设re-export 或 wrapper ─────────────────────────────────────────
export const exportPreset = _presetExportPreset;
export const exportPresetFull = _presetExportPresetFull;
export const clearGlobalPreset = _presetClearGlobalPreset;
export const importGlobalPreset = _presetImportGlobalPreset;
/**
* importPreset wrapper在 setState 之后注入 SuperMemory 全量同步。
* 兼容旧签名 importPreset(callback) 和新 importPreset({ onAfterApply, onImported })。
*/
export function importPreset(onImportedOrHooks) {
/** @type {{ onAfterApply?: () => void, onImported?: () => void }} */
const hooks = typeof onImportedOrHooks === 'function'
? { onImported: onImportedOrHooks }
: (onImportedOrHooks || {});
return _presetImportPreset({
onAfterApply: () => {
dispatchAllTablesUpdate();
if (hooks.onAfterApply) hooks.onAfterApply();
},
onImported: hooks.onImported,
});
}
// ── 回滚 ──────────────────────────────────────────────────────────────────
export async function rollbackState() {
const context = getContext();
if (!context || !context.chat || context.chat.length < 2) {
log('无法回退:聊天记录不足。', 'warn');
toastr.warning('聊天记录不足,无法执行回退操作。');
return false;
}
const chat = context.chat;
const lastMessageIndex = chat.length - 1;
const lastMessage = chat[lastMessageIndex];
log(`正在尝试从第 ${lastMessageIndex - 1} 条消息加载表格状态...`, 'info');
const previousState = loadTables(lastMessageIndex);
if (!previousState) {
log('未能在上一楼找到可用的表格状态,无法回退。', 'error');
toastr.error('未能在上一楼找到可用的表格状态。');
return false;
}
setState(previousState);
if (_persistSaveStateToMessage(previousState, lastMessage)) {
await saveChat();
log('已成功将回退后的状态保存至最新消息。', 'success');
} else {
log('回退状态保存失败,操作中止。', 'error');
toastr.error('未能保存回退状态,操作中止。');
return false;
}
renderTables();
updateOrInsertTableInChat();
log('UI已更新以显示回退后的状态。', 'info');
return true;
}
export async function rollbackAndRefill() {
const settings = extension_settings[extensionName] || {};
if (settings.table_system_enabled === false) {
log('表格系统总开关已关闭,跳过回退填表。', 'info');
toastr.info('表格系统总开关已关闭,无法执行回退填表。');
return;
}
toastr.info('正在执行回退并重新填表...');
const rollbackSuccess = await rollbackState();
if (!rollbackSuccess) {
toastr.error('状态回退失败,已中止操作。');
return;
}
toastr.success('状态回退成功,准备重新填表...');
const context = getContext();
const lastMessage = context.chat[context.chat.length - 1];
try {
await fillWithSecondaryApi(lastMessage, true, { targetMessage: lastMessage });
log('回退并重新填表操作完成。', 'success');
} catch (error) {
log(`回退重填过程中发生错误: ${error.message}`, 'error');
toastr.error(`重新填表失败: ${error.message}`);
}
}
// ── 杂项 ──────────────────────────────────────────────────────────────────
export function isCurrentTablesEmpty() {
const tables = getState();
if (!tables || tables.length === 0) return true;
return tables.every(table => !table.rows || table.rows.length === 0);
}
// ── 模块初始化 ─────────────────────────────────────────────────────────────
// 模块加载时执行一次初始 loadTables
loadTables();