Files
ST-Amily2-Chat-Optimisation/TableTODO.md

16 KiB
Raw Permalink Blame History

TableTODO — 表格模块重构清单

创建于 2026-04-28。劳动节假后启动。 主线:解耦 → 三模式填表legacy / json / toolcall。 跨方向依赖Bus tool-call 升级)见 51TODO.md Phase A。


一、动机

现行表格填表让 LLM 输出 <Amily2Edit>insertRow(0, {0:"x",1:"y"})</Amily2Edit> 这种"四不像"自定义文本格式,由 executor.js#parseFunctionCall 自实现的 brace-depth + quote-state 状态机解析。高温下:

  • 引号转义错乱、嵌套对象内逗号未转义 → 参数切错位
  • data 对象键写成无引号字段名 → 多层 JSON.parse fallback 仍可能失败
  • 一处 LLM 偷懒不输出 <Amily2Edit> → 整批回滚重试

目标:把"格式契约"从 prompt 字符串约定改成 schema 约定,让 LLM 直接吐结构化数据,砍掉自实现解析器。同时保留 legacy 文本模式确保老用户行为不变。

模式 输出形态 解析复杂度 兼容性
legacy(默认) <Amily2Edit>insertRow(...)</Amily2Edit> 文本块 高(现行解析器) 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 mutablecurrentTablesStatehighlightedCellsupdatedTablesmanager.js:16-20
  • 20+ export 函数直接 mutate没有封装边界

2.3 持久化模式被复制 16 次

每个 UI 突变 export 末尾都有同款样板:

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 大量重复

2.5 业务层硬依赖 UI 层

manager.js:9-10

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
  • gettermanager.js:1244-1259
  • 占位符替换 flowTemplate.replace('{{{Amily2TableData}}}', ...)secondary-filler / batch-filler / reorganizer / injector 各自一份

2.7 格式锁死(重构核心痛点)

<Amily2Edit> 文本格式硬编码在 4 处:

2.8 循环依赖

2.9 TableSystemService 是半成品门面

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   : <Amily2Edit> 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 抽成纯函数
  • schemaOp = { op: 'insertRow'|'updateRow'|'deleteRow', tableIndex, rowIndex?, data? }
  • APIapplyOperations(state, ops): { state, changes }
  • executor.js 改名 → formatters/legacy.js,只保留文本解析 → 输出 Op[] → 调 applyOperations

0.4 拆 mutations.js

  • 文件:core/table-system/domain/mutations.js
  • 把 manager.js 里 16 个突变 exportaddRow / addColumn 等)搬过来
  • 全部改为:调 store.setState + persist.commitToLastMessage + 发事件
  • 删除对 ui/* 的所有 import改为 store.subscribe 让 UI 自己订阅刷新

0.5 拆 rendering.js

0.6 拆 templates.js + preset.js

  • domain/templates.jsgetBatchFillerRuleTemplate / saveBatchFillerRuleTemplate / Flow 同款
  • domain/preset.jsexportPreset / 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 BJSON 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 CToolCall formatter

依赖 Phase 0 + 51TODO Phase ABus tool-call 升级)+ Phase BB 已经把 formatter 切换走通了)。

C.1 formatters/toolcall.js

  • 注册 Bus 工具:table.insertRow / table.updateRow / table.deleteRow
  • 工具 parameters 用标准 JSONSchema 描述
  • handler 内部调 applyOperations(其实是收集 Op[] 累加)
  • 让 fillerShared 在该模式下走 model.callWithToolsloop 跑完后取累计的 Op[]

C.2 终止条件

  • LLM 在某轮没有吐 tool_calls 即停(对应"我已填完"的语义信号)
  • maxSteps 兜底

C.3 Prompt 调整

  • toolcall 模式下不需要 <Amily2Edit> 教学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.22.0.3 删除

八、不在范围内(明确不做)

  • 不重写 ui/table-bindings.jsUI 层独立演进)
  • 不改持久化 schemamessage.extra.amily2_tables_data 保持)
  • 不改 SuperMemory 集成(继续走 Bus query + CustomEvent fallback
  • 不引入 TypeScriptDTS 注释为主)
  • Phase 0 阶段不动 prompt 模板内容(只挪文件位置)

九、入手顺序

  1. Phase 0.3operations.js—— 影响面小,立刻能验证 executor 抽离不破坏 legacy
  2. Phase 0.1 + 0.2store + 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