# TableTODO — 表格模块重构清单 > 创建于 2026-04-28。劳动节假后启动。 > 主线:解耦 → 三模式填表(legacy / json / toolcall)。 > 跨方向依赖(Bus tool-call 升级)见 [51TODO.md](51TODO.md) Phase A。 --- ## 一、动机 现行表格填表让 LLM 输出 `insertRow(0, {0:"x",1:"y"})` 这种"四不像"自定义文本格式,由 [executor.js#parseFunctionCall](core/table-system/executor.js#L98) 自实现的 brace-depth + quote-state 状态机解析。高温下: - 引号转义错乱、嵌套对象内逗号未转义 → 参数切错位 - `data` 对象键写成无引号字段名 → 多层 JSON.parse fallback 仍可能失败 - 一处 LLM 偷懒不输出 `` → 整批回滚重试 **目标**:把"格式契约"从 prompt 字符串约定改成 schema 约定,让 LLM 直接吐结构化数据,砍掉自实现解析器。同时保留 legacy 文本模式确保老用户行为不变。 | 模式 | 输出形态 | 解析复杂度 | 兼容性 | |------|---------|-----------|--------| | `legacy`(默认) | `insertRow(...)` 文本块 | 高(现行解析器) | 100% 老行为 | | `json` | `{ "operations": [{op, tableIndex, ...}] }` 单 JSON 块 | 中(JSON.parse + schema 校验) | 新模式 | | `toolcall` | OpenAI tool_calls 多步迭代 | 低(结构化原生) | 依赖 Bus 升级(51TODO Phase A) | --- ## 二、当前耦合分析(2026-04-28 摸底) ### 2.1 manager.js 是上帝模块 - 1745 行,51 个 export - 七层职责混杂:状态容器 / 持久化 / UI 突变操作 / LLM 指令执行 / Markdown 提示词渲染 / 模板 getter setter / 预设导入导出 / 回滚 / 跨模块事件分发 ### 2.2 状态所有权 - module-level mutable:`currentTablesState`、`highlightedCells`、`updatedTables`([manager.js:16-20](core/table-system/manager.js#L16-L20)) - 20+ export 函数直接 mutate,没有封装边界 ### 2.3 持久化模式被复制 16 次 每个 UI 突变 export 末尾都有同款样板: ```js const context = getContext(); if (context.chat && context.chat.length > 0) { const lastMessage = context.chat[context.chat.length - 1]; if (saveStateToMessage(currentTablesState, lastMessage)) { saveChat(); return; } } saveChatDebounced(); ``` 受影响:addRow / addColumn / updateHeader / deleteRow / restoreRow / insertColumn / moveColumn / deleteTable / addTable / renameTable / moveTable / updateTableRules / updateRow / clearAllTables / updateColumnWidth / insertRow ### 2.4 三个 filler 大量重复 - [secondary-filler.js#getWorldBookContext](core/table-system/secondary-filler.js#L16) ≈ [batch-filler.js#getWorldBookContext](core/table-system/batch-filler.js#L25)(含微妙差异:character book 来源处理不同) - mixed-order 拼装循环 + `callNccsAI vs callAI` 分支三处 copy - 三者都调 `updateTableFromText(rawContent)` 收尾 ### 2.5 业务层硬依赖 UI 层 [manager.js:9-10](core/table-system/manager.js#L9-L10): ```js import { renderTables } from '../../ui/table-bindings.js'; import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js'; ``` 在 loadMemoryState / deleteRow / restoreRow / rollbackState / updateTableFromText 里直调。逻辑和渲染焊死。 ### 2.6 提示词构建散在 4 个文件 - 模板常量:[settings.js](core/table-system/settings.js) - getter:[manager.js:1244-1259](core/table-system/manager.js#L1244) - 占位符替换 `flowTemplate.replace('{{{Amily2TableData}}}', ...)`:secondary-filler / batch-filler / reorganizer / injector 各自一份 ### 2.7 格式锁死(重构核心痛点) `` 文本格式硬编码在 4 处: - [executor.js#L98-202](core/table-system/executor.js#L98) 解析器 - [settings.js#L11-16](core/table-system/settings.js#L11) 模板示例 - [manager.js#updateTableFromText](core/table-system/manager.js#L1266) 入口 - [secondary-filler.js#L292](core/table-system/secondary-filler.js#L292) 失败检测 `if (!rawContent.includes(''))` ### 2.8 循环依赖 - [manager.js:5](core/table-system/manager.js#L5) → `secondary-filler.js` - [secondary-filler.js:7](core/table-system/secondary-filler.js#L7) → `manager.js` - 引发点:`manager.rollbackAndRefill` 需要调 `fillWithSecondaryApi` ### 2.9 TableSystemService 是半成品门面 [TableSystemService.js](core/table-system/TableSystemService.js) 把 manager / executor / secondary-filler / ui 全 import 后再 expose,没解耦任何东西,只是 Bus 注册帖。 --- ## 三、目标分层 ``` ┌────────────────────────────────────────────────────────────┐ │ UI Layer (existing, untouched) │ │ ui/table-bindings.js · ui/message-table-renderer.js │ └────────────────────▲────────────────────────────────────────┘ │ 仅订阅事件,不被业务层 import ┌────────────────────┴────────────────────────────────────────┐ │ Service Layer (TableSystemService 真正承担门面) │ │ ├─ 编排:fill/reorganize/rollback │ │ ├─ Bus 注册 │ │ └─ 通过事件通知 UI(而非 import) │ └────────────────────▲────────────────────────────────────────┘ │ ┌────────────────────┴────────────────────────────────────────┐ │ Pipeline Layer (新增,三模式落地点) │ │ ├─ formatters/legacy.js : prompt + parse │ │ ├─ formatters/json.js : JSON prompt + parse │ │ ├─ formatters/toolcall.js : Bus tool_calls (依赖 Bus 升级) │ │ ├─ formatters/index.js : 按 settings 分发 │ │ └─ filler/ │ │ ├─ shared.js : worldbook + history + 拼装 │ │ ├─ secondary.js : 触发条件 + 用 shared │ │ └─ batch.js : 批次循环 + 用 shared │ └────────────────────▲────────────────────────────────────────┘ │ 输出统一 Operation[] ┌────────────────────┴────────────────────────────────────────┐ │ Operation Layer (从 executor.js 抽出) │ │ operations.js │ │ ├─ applyOperations(state, ops) → { state, changes } │ │ └─ schema: Op = { op, tableIndex, ...args } │ └────────────────────▲────────────────────────────────────────┘ │ ┌────────────────────┴────────────────────────────────────────┐ │ Domain Layer (从 manager.js 拆出) │ │ ├─ store.js : currentTablesState 单一所有权 + 订阅 │ │ ├─ persist.js : saveStateToMessage / load / 持久化封装 │ │ ├─ mutations.js : addRow/addColumn/.../updateRow 突变 API │ │ ├─ rendering.js : convertTablesToCsvString * 3 (纯函数) │ │ ├─ templates.js : prompt 模板 getter setter │ │ └─ preset.js : 导入导出 / 全局预设 │ └──────────────────────────────────────────────────────────────┘ ``` **关键原则** - Domain Layer 是纯逻辑,**禁止 import UI** - Service Layer 与 UI 通过事件解耦(已有 events-schema.js 基础设施) - Pipeline Layer 的 formatter 是可插拔的,新增格式 = 加文件,不动旧文件 - `currentTablesState` 由 store.js 独占,对外只有 `getState() / setState() / subscribe()` --- ## 四、Phase 0:解耦准备(必须先做) 下列任务**不引入新功能**,只重排现有代码。每条独立可 ship。 ### 0.1 抽出 store.js(单一所有权) - 文件:`core/table-system/domain/store.js` - 把 `currentTablesState` / `highlightedCells` / `updatedTables` 搬过来 - 提供:`getState() / setState() / addHighlight / clearHighlights / getUpdatedTables / subscribe(listener)` - manager.js 改为代理调用 ### 0.2 抽出 persist.js(消除 16 处持久化样板) - 文件:`core/table-system/domain/persist.js` - 提供 `commitToLastMessage(state)`:封装 `getContext + saveStateToMessage + saveChat + fallback` - 替换 manager.js 16 处样板 ### 0.3 抽出 operations.js(解锁三模式的关键) - 文件:`core/table-system/operations.js` - 把 [executor.js insertRow/updateRow/deleteRow](core/table-system/executor.js#L3-L89) 抽成纯函数 - schema:`Op = { op: 'insertRow'|'updateRow'|'deleteRow', tableIndex, rowIndex?, data? }` - API:`applyOperations(state, ops): { state, changes }` - executor.js 改名 → `formatters/legacy.js`,只保留文本解析 → 输出 Op[] → 调 applyOperations ### 0.4 拆 mutations.js - 文件:`core/table-system/domain/mutations.js` - 把 manager.js 里 16 个突变 export(addRow / addColumn 等)搬过来 - 全部改为:调 store.setState + persist.commitToLastMessage + 发事件 - **删除**对 ui/* 的所有 import;改为 `store.subscribe` 让 UI 自己订阅刷新 ### 0.5 拆 rendering.js - 文件:`core/table-system/domain/rendering.js` - 把 [convertTablesToCsvString](core/table-system/manager.js#L1005) / [convertSelectedTablesToCsvString](core/table-system/manager.js#L1096) / [convertTablesToCsvStringForContentOnly](core/table-system/manager.js#L1201) 搬过来 - 都做成纯函数 `(state, options?) => string`,不依赖 store ### 0.6 拆 templates.js + preset.js - `domain/templates.js`:getBatchFillerRuleTemplate / saveBatchFillerRuleTemplate / Flow 同款 - `domain/preset.js`:exportPreset / importPreset / clearGlobalPreset / importGlobalPreset ### 0.7 抽出 fillerShared.js(消除三 filler 重复) - 文件:`core/table-system/filler/shared.js` - 提供: - `getWorldBookContext(settings)` — 合并 secondary 和 batch 两份的差异,参数化处理 - `buildHistoryContext(opts)` — 统一对话历史拼装 - `buildMessages(scope, { worldbook, history, coreContent, flowPrompt, ruleTemplate })` — mixed-order 循环 + presetPrompts 拼装 - `callModel(messages, settings)` — 统一 nccsEnabled 分支 - secondary-filler.js / batch-filler.js / reorganizer.js 改用 shared ### 0.8 解循环依赖 - manager.js 的 `rollbackAndRefill` 不直接 import `fillWithSecondaryApi` - 改为:在 service 层 (TableSystemService) 编排"先 rollback 再 fill" - manager(或新的 mutations.js)只暴露 rollbackState ### 0.9 TableSystemService 真正变成门面 - 不再 `import * as TableManager` + 一一 expose - 改为:内部组合 store / persist / mutations / formatters / filler,对外只暴露稳定接口 - 现有 `processMessageUpdate` 保留 **Phase 0 完成验收**: - [ ] manager.js 缩到 < 200 行(仅作为 deprecation 兼容层重导出 + 标 @deprecated) - [ ] 任何 domain/* 文件都不 import ui/* - [ ] 三个 filler 共用 fillerShared.js,各自只有 ~100 行 - [ ] 现行 legacy 模式行为完全不变(手动验证) --- ## 五、Phase B:JSON formatter > 依赖 Phase 0。不依赖 Bus 升级(Phase A)。 ### B.1 formatters/json.js - prompt 模板:教 LLM 输出 `{ "operations": [{ "op": "insertRow", "tableIndex": 0, "data": { "0": "...", "1": "..." } }] }` - 解析:`JSON.parse` + schema 校验 → Op[] - 输出 Op[] 给 applyOperations ### B.2 设置项与 UI - 新设置:`settings.table_filling_format: 'legacy' | 'json' | 'toolcall'`,默认 `legacy` - 表格设置面板加 dropdown - 默认值保证老用户零感知 ### B.3 集成到 fillerShared - shared.callModel 调完后传 raw response 给当前 formatter - formatter 返回 Op[] - shared 负责 applyOperations + persist + 发事件 **Phase B 验收**: - [ ] 切换到 json 模式后,手动跑分步填表 + 批量填表 + 重新整理 三种场景都能成功 - [ ] 回切 legacy 行为不变 --- ## 六、Phase C:ToolCall formatter > 依赖 Phase 0 + 51TODO Phase A(Bus tool-call 升级)+ Phase B(B 已经把 formatter 切换走通了)。 ### C.1 formatters/toolcall.js - 注册 Bus 工具:`table.insertRow / table.updateRow / table.deleteRow` - 工具 parameters 用标准 JSONSchema 描述 - handler 内部调 `applyOperations`(其实是收集 Op[] 累加) - 让 fillerShared 在该模式下走 `model.callWithTools`,loop 跑完后取累计的 Op[] ### C.2 终止条件 - LLM 在某轮没有吐 tool_calls 即停(对应"我已填完"的语义信号) - maxSteps 兜底 ### C.3 Prompt 调整 - toolcall 模式下不需要 `` 教学,prompt 简化 - 但要保留 `{{{Amily2TableData}}}` 注入当前状态作为参考 **Phase C 验收**: - [ ] toolcall 模式跑通分步填表 - [ ] 串表问题肉眼对比 legacy 显著减少 - [ ] handler 内 tableIndex 不存在时回喂 LLM 能自纠 --- ## 七、表格部分决策点 > 重构前需要确认: 1. **填表格式开关粒度**:全局一个?还是分步 / 批量 / 重整 三个独立? - 倾向:全局一个 `table_filling_format`,简化 UI 2. **JSON 模式形态**: - A:单 JSON 块 `{"operations":[...]}` 直球到底 - B:允许 LLM 在 ops 前后写自由文本(像 toolcall 那样夹带推理) - 倾向:A,简单可靠 3. **toolcall 终止条件**: - A:模型某轮无 tool_calls 即停 + maxSteps 兜底 - B:必须显式调 `commit_table_changes` 工具才算完 - 倾向:A 4. **manager.js 兜底兼容期**: - 拆解后保留 manager.js 作 re-export 兼容层多久? - 倾向:保留至 2.0.2,2.0.3 删除 --- ## 八、不在范围内(明确不做) - 不重写 ui/table-bindings.js(UI 层独立演进) - 不改持久化 schema(`message.extra.amily2_tables_data` 保持) - 不改 SuperMemory 集成(继续走 Bus query + CustomEvent fallback) - 不引入 TypeScript(DTS 注释为主) - Phase 0 阶段不动 prompt 模板内容(只挪文件位置) --- ## 九、入手顺序 1. Phase 0.3(operations.js)—— 影响面小,立刻能验证 executor 抽离不破坏 legacy 2. Phase 0.1 + 0.2(store + persist)—— 给后续 mutations 拆解铺路 3. Phase 0.4-0.6 —— manager.js 收缩主战 4. Phase 0.7-0.9 —— filler 重复消除 + 循环依赖 5. Phase 0 整体回归 6. Phase B(独立可走,不等 Bus 升级) 7. Phase C(等 51TODO Phase A 完成后再做) --- ## 十、工时(粗) | Phase | 预估 | 风险 | |-------|------|------| | 0.1-0.3 (store/persist/operations) | 1 天 | 低 | | 0.4-0.6 (mutations/rendering/templates) | 1 天 | 中(manager.js 删减易漏) | | 0.7-0.9 (filler / 循环依赖 / Service) | 1 天 | 中(filler 三方差异需仔细对齐) | | Phase B | 0.5 天 | 低 | | Phase C | 0.5 天 | 低(前置都搞完了,纯组装) | | 回归测试 | 1 天 | — | 合计 ~5 天人时(不含 Bus 升级,那部分见 51TODO)。