docs: add feature documentation

This commit is contained in:
youzini
2026-05-31 17:07:58 +00:00
parent 2a099cb5ef
commit 32315c33e8
6 changed files with 298 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
# ENA Planner
ENA Planner 是一个**可选的、发送前剧情规划**子系统。它独立于核心记忆系统,默认关闭。
实现:`ena-planner/`(独立模块)、`ui/panel-ena-sections.js``runtime/planner-recall-controller.js`
## 做什么
在用户消息真正发给模型**之前**,先用一个单独的"规划师"LLM 生成幕后指引,附加到用户输入里。规划师不直接扮演角色、不写正文,只规划下一轮 AI 回复的走向。
产出带标签的规划文本:
- `<plot>` — 剧情走向指引(导演式的下一步规划)
- `<note>` — 写作笔记(风格/连贯性指示)
过滤后的规划输出会附加到真实用户输入后面再发送:`原始输入 + \n\n + 规划输出`
## 管线
```
拦截发送(点击发送/回车)
→ 构建规划消息buildPlannerMessages
→ 收集上下文:角色卡 + BME 记忆召回 + 近期 AI 对话 + 历史 <plot> + 世界书 + 用户输入
→ 渲染模板/宏EJS、ST 宏)
→ 组装提示词块(优先用 planner 任务预设,回退遗留块)
→ 调用规划师 LLMcallPlanner可流式
→ 过滤响应(去 think保留配置的标签
→ 注入 textarea 并发送
```
启动入口 `initEnaPlanner(bmeRuntime)`:迁移旧存储、加载配置/日志、安装发送拦截器、暴露 `window.stBmeEnaPlanner`
发送拦截条件planner 启用、未在规划中、textarea 非空、输入非 trivial、未 bypass、`skipIfPlotPresent`)输入未含 `<plot>`
## 与 ST-BME 的集成
ENA Planner 集成的是**召回**,不是提取:
- 它调用 BME 召回获取记忆块作为规划上下文(`runPlannerRecallForEna`)。
- 规划输出注入用户文本后,主生成会把规划标签当作用户消息的一部分看到。
- 它**不**直接运行提取,也**不**把规划结果写进记忆图谱。后续提取走正常聊天/提取路径。
### 召回交接handoff
规划输出附加到 textarea 后,正常生成本会对"增强后的输入"(原文 + `<plot>`/`<note>`)做召回——但那会用改变后的文本检索,结果偏差。
为避免这点ENA 注册一个**一次性召回交接**含原始用户输入、增强消息、召回结果、注入文本source 标记 `planner-handoff`。正常生成召回会读取这个交接,把召回输入覆盖回原始用户文本,并复用已算好的召回结果(`cachedRecallPayload`)。
这套机制的实现见 `runtime/planner-recall-controller.js``runtime/reroll-recall-input.js``runtime/generation-recall-transactions.js`
## 规划召回 vs 正常召回
| 维度 | 规划召回 | 正常召回 |
| --- | --- | --- |
| 时机 | 规划师 LLM 调用前 | 主生成前 |
| 查询源 | 原始用户输入 | 用户输入(或交接复用) |
| 上下文 | `recallLlmContextMessages`(夹 0..20 | 标准召回上下文 |
| 门禁 | 跳过 trivial、需图谱可读、历史恢复、向量就绪 | 同左 |
## 配置(独立存储)
ENA 配置存在 `STBME_EnaPlanner.json`,不在主设置里。关键默认:
| 设置 | 默认 |
| --- | --- |
| `enabled` | false |
| `skipIfPlotPresent` | true |
| `plotCount` | 2 |
| `responseKeepTags` | plot, note, plot-log, state |
| `includeGlobalWorldbooks` | false |
| API stream | true |
| `logsPersist` / `logsMax` | true / 20 |
面板的"ENA 规划器"区(`data-config-section="planner"`)提供启用、跳过已有 plot、测试输入、API 预设、世界书选项、保留/排除标签、plot 数量、日志等控件。
> 关于命名:代码里 "ENA" 作为模块/产品名使用Ena Planner / ENA 规划器),未找到明确的缩写展开;实际提示词角色是"剧情规划师 / Story Planner"。

View File

@@ -0,0 +1,28 @@
# 隐藏旧楼层与渲染限制
为了控制超长聊天的提示词成本ST-BME 可以隐藏旧楼层、限制聊天区渲染。本文档说明这两个机制,以及它们与历史安全的关键交互。
实现:`ui/hide-engine.js``ui/message-render-limit.js`
## 隐藏旧楼层
把久远的楼层对模型隐藏(不进提示词),靠记忆图谱替代它们提供长期上下文。这是 ST-BME 的核心价值之一:用结构化记忆替代原始历史。
隐藏由 `ui/hide-engine.js` 处理。被隐藏的楼层仍在聊天里,只是不发给模型。
## 渲染限制
`ui/message-render-limit.js` 限制聊天区实际渲染的消息数(`getMessageRenderLimitSettings` / `applyMessageRenderLimit`。这是性能优化——DOM 里只保留最近 N 条。
## 关键交互:渲染切片 ≠ 历史删除
这是最重要的一点。渲染限制会让 `context.chat` 看起来"变短",但这只是渲染切片,不是历史真的被删。
> ST-BME 必须区分"渲染切片"和"历史删除"。把渲染切片误当成历史变短,会触发破坏性历史恢复、误清空运行时图谱。
`getRenderLimitedHistoryRecoveryGuard`(在 `ui/message-render-limit.js`)提供这个保护:当检测到当前聊天可能只是渲染切片时,暂停破坏性恢复。详见 [`history-safety.md`](history-safety.md)。
## 边界
- 隐藏旧楼层、渲染限制、总结折叠能降低成本,但不能消除所有开销——超长聊天仍有成本。
- 旧楼层隐藏后若模型仍看到太多内容,检查隐藏设置和渲染限制是否生效。

View File

@@ -0,0 +1,47 @@
# 历史安全恢复、渲染保护、Restore Lock
ST-BME 的记忆图谱依赖"楼层 → 已提取"的映射。但宿主聊天历史会被各种操作扰动编辑、删除、swipe、reroll、只渲染最近 N 条、切换聊天。本文档说明保护机制,确保这些扰动不会误清空或错误覆盖记忆。
实现散布在 `maintenance/chat-history.js``maintenance/reroll-recovery-controller.js``index.js` 的历史检测路径,以及 [`../architecture/control-plane.md`](../architecture/control-plane.md) 描述的身份/持久化控制平面。
## 历史变动恢复
当检测到聊天历史与图谱记录不一致(楼层被编辑/删除/重排ST-BME 尝试恢复:
```
检测历史变动
→ 优先 replay按日志重放增量变化
→ replay 失败则全量重建(从聊天历史重新提取)
```
全量重建优先正确性,但较慢(消耗 LLM 调用)。`recoverHistoryIfNeeded` 是这条路径的核心编排(被抽到 `maintenance/reroll-recovery-controller.js`,是过去最难、最 bug 多的函数之一)。
## 渲染切片保护
SillyTavern 可能只在 DOM 里渲染最近 N 条消息(性能优化)。如果 ST-BME 把这个"渲染切片"误当成"完整聊天历史变短了",就会错误地清空运行时图谱。
> 当 ST-BME 检测到当前 `context.chat` 很可能只是最近 N 条渲染切片时,暂停破坏性历史恢复,避免误清空。`inspectHistoryMutation()` 会跳过这类渲染切片误判。
详见 [`hide-and-render.md`](hide-and-render.md)。
## Restore Lock
恢复过程是异步的。如果恢复进行到一半,用户切了聊天或触发了图谱变更,就可能写坏数据。
> Restore Lock 在历史恢复期间阻断图谱变更操作。变更门禁(`ensureGraphMutationReady` / `getGraphMutationBlockReason`)会返回"已暂停:正在恢复"类的原因,而不是让变更穿透。
恢复过程中还会用 `assertRecoveryChatStillActive` 校验聊天没被切走——切走则抛 abort安全中止而不是把恢复结果写到错误的聊天上。
## 与控制平面的关系
历史安全本质上是控制平面身份/持久化不变量的应用:
- 身份四通道分离确保恢复时不会把别的聊天身份当成当前聊天。
- 持久化 reducer 确保恢复期间的 pending/accepted 状态正确流转。
- recovery-only tiershadow/metadata不能推进确认状态所以恢复用的临时数据不会被误当成"已安全落地"。
详见 [`../architecture/control-plane.md`](../architecture/control-plane.md)。
## 手动提取时的提示
手动触发提取时若恰逢历史恢复未完成,会提示"历史恢复暂停"——这是 Restore Lock 在起作用,等恢复完成即可。过去这里出现过"陈旧 pending 卡住"的 bug已由持久化 reducer 的自动清除不变量修复。

View File

@@ -0,0 +1,57 @@
# 记忆模型
ST-BME 的记忆是一张图谱:**节点**是记忆单元,**关系**(边)连接它们。本文档说明节点类型、关系类型,以及主客观分层和故事时间线这两套横切机制。
节点/关系 schema 定义在 `graph/` 下,可被任务预设扩展。
## 节点类型
常见类型(具体以 schema 定义为准):
| 类型 | 含义 |
| --- | --- |
| `event` | 事件:某个时间点发生的事 |
| `pov_memory` | 视角记忆:某个角色视角下的认知/记忆 |
| `thread` | 线索:跨多个事件的剧情线 |
| `synopsis` | 概要:全局或区段的总结 |
| `reflection` | 反思:从事件中提炼的洞察 |
| `rule` | 规则:设定/约束类记忆 |
| 实体类 | 角色、地点、物品等命名实体 |
每个节点带 `importance`(重要度 0-10`accessCount`(被召回次数)、`createdTime``scope`(归属范围)、可选 `storyTime`/`storyTimeSpan` 等。
节点不被物理删除,而是**归档**`archived = true`)——这保护了历史恢复和审计。
## 关系类型
边带 `relation`(关系名)、`strength`(强度 0-1`edgeType`。关键的几种:
| 关系 | 说明 |
| --- | --- |
| `related` | 一般关联(提取时在同批节点间默认建立,强度 0.25 |
| `temporal_update` | 时序更新(节点状态随时间变化,强度默认 0.95 |
| `updates` | 状态更新事件指向被更新节点(强度 0.9 |
| `contradicts` | 矛盾/抑制edgeType 255——在图扩散里作为**抑制边**,反向传播负能量 |
抑制边是 ST-BME 处理"矛盾记忆"的机制:见 [`../algorithms/diffusion-and-dynamics.md`](../algorithms/diffusion-and-dynamics.md)。
## 主客观分层
记忆按"谁的视角"分层,这是召回时认知边界过滤的基础:
- **角色 POV**:某角色主观认知的记忆。注入时按 owner 分组(`[Memory - Character POV: <owner>]`)。
- **用户 POV**:用户视角记忆。注入时带"非角色事实"警告(避免模型把用户知道的当成角色知道的)。
- **客观**:不绑定视角的事实,又分"当前区域"和"全局"。
召回时通过认知门(`computeKnowledgeGateForNode`)决定某节点对当前视角是否可见,不可见则跳过。详见 [`../algorithms/retrieval.md`](../algorithms/retrieval.md) 的"认知边界过滤"。
## 故事时间线
记忆带"故事内时间"(不是真实时间),用于按剧情时间线组织:
- `event` / `pov_memory`:时间点 `storyTime`
- `thread` / `synopsis` / `reflection`:时间跨度 `storyTimeSpan`
提取时可由 LLM 给出 `batchStoryTime` 或操作级 `storyTime`,解析后更新时间线段。时序合成边(`temporalLinkStrength=0.2`)让时间相邻的记忆在图扩散里有弱连接。
故事时间线状态见 `graph/story-timeline.js`,认知/区域状态见 `graph/knowledge-state.js`

View File

@@ -0,0 +1,64 @@
# Native / WASM 加速
ST-BME 对几个计算密集操作提供 Rust/WASM 加速,灰度发布,默认 fail-open失败自动回退 JS。本文档说明加速了什么、怎么门控、怎么回退。
实现:`native/stbme-core/`Rust 源)、`vendor/wasm/stbme_core.js`JS 包装)、`ui/graph-native-bridge.js``sync/bme-db.js` 的 native gate。
## 加速了什么
| 操作 | native 钩子 | 阈值(达到才用 native |
| --- | --- | --- |
| **图谱布局** | `solve_layout` | 节点 ≥ 280 或 边 ≥ 1600 |
| **持久化增量构建** | `build_persist_delta*` | 记录 ≥ 20000 且 结构增量 ≥ 600 且 序列化字符 ≥ 4000000 |
| **快照 hydrate/加载** | `build_hydrate_records` | 记录 ≥ 30000 |
> 没有加速embedding 和召回图扩散。embedding 走 `vector/` 的 API/直连路径;扩散是纯 JS`retrieval/diffusion.js`)。注意 `host/st-native-render.js` 不是 WASM 加速器,是 ST 原生模板/宏渲染助手。
## Fail-open 回退
核心标志:
- `nativeEngineFailOpen: true`(默认)— native 失败时回退 JS不报错
- `graphNativeForceDisable: false`(默认)— 全局禁用 native
回退逻辑:
- **图谱布局**worker 不可用时fail-open 返回 skipped 让渲染器跑 JS 布局strict 模式fail-open false才抛 `native-failed-hard`
- **持久化增量**native 预加载/构建失败且 fail-open 时,记警告、用 JS 增量。
- **hydrate**同理native 不可用且 fail-open 时用 JS hydrate。
`graphNativeForceDisable` 为真时布局、hydrate、持久化增量全部跳过 native 尝试。
## 灰度发布
设置文件明确把这块标为"灰度"。通过 `nativeRolloutVersion` 做迁移:
- 当前 `NATIVE_ROLLOUT_VERSION = 2`
- 持久版本 < 1迁移开启 native 布局/增量/hydrate 默认
- 持久版本 < 2 hydrate 阈值是旧值 12000更新到 30000
- 然后设 `nativeRolloutVersion = 2`
这套迁移让老用户平滑升级到新默认同时保留用户手动 opt-out手动关掉的不被迁移覆盖)。
## WASM 核心模块
JS 包装 `vendor/wasm/stbme_core.js`
- 先尝试 wasm-pack 产物`./pkg/stbme_core_pkg.js` + `.wasm`
- 失败则用全局 fallback loader `globalThis.__stBmeLoadRustWasmLayout`
- 通过 `getNativeModuleStatus()` 暴露状态
暴露的函数`solveLayout()``installNativePersistDeltaHook()` `__stBmeNativeBuildPersistDelta`)、`installNativeHydrateHook()` `__stBmeNativeHydrateSnapshotRecords`)、`getNativeModuleStatus()` / `resetNativeModuleStatus()`
Rust 导出`solve_layout``build_persist_delta` / `_compact` / `_compact_hash``build_hydrate_records`
桥接模式 `persistNativeDeltaBridgeMode`默认 `json`控制增量传输格式`hash` 模式走 compact hash 路径
## 测试
- `tests/native-layout-parity.mjs` native 布局结果与 JS 布局的平均绝对差在阈值内保证 native JS 算出来一致)。
- `tests/native-rollout-matrix.mjs` 迁移选项归一化各操作的阈值门控边界值如 279/28029999/30000)。
## 面板控件
面板显示 native 状态全局禁用 / 按阈值自动尝试fail-open / strict并绑定 `graphNativeForceDisable``nativeEngineFailOpen``graphUseNativeLayout``persistUseNativeDelta``loadUseNativeHydrate` 等开关和阈值输入遇到异常可在面板强制关闭 native

View File

@@ -0,0 +1,26 @@
# 持久召回卡片
召回卡片是挂在聊天消息上的 UI 元素,显示"这条消息生成时召回了哪些记忆"。它既是用户可见的透明度功能,也是 reroll 复用的存储载体。
实现:`ui/recall-message-ui.js``ui/recall-message-ui-controller.js``retrieval/recall-persistence.js`
## 做什么
每次生成前召回发生时,召回结果(选中的节点、注入文本、输入指纹)被持久化并绑定到对应的用户楼层。卡片把这个记录渲染在消息旁,用户可以看到、展开。
## 为什么持久化
两个目的:
1. **透明度**:用户能看到记忆系统在每轮"想起了什么"。
2. **reroll 复用**reroll 助手楼层时,如果上方用户楼层没变,已持久化的召回记录可以直接复用,跳过新检索。详见 [`../architecture/control-plane.md`](../architecture/control-plane.md) 的 reroll 不变量和 [`../algorithms/retrieval.md`](../algorithms/retrieval.md) 的持久复用。
## 控制器封装
`ui/recall-message-ui-controller.js` 是一个工厂,封装了卡片挂载/刷新所需的全部状态定时器、MutationObserver、session、诊断 Map。这是 VM 测试税重构的产物——这些有状态的 UI 逻辑从 `index.js` 抽出,状态不再泄漏到模块级全局。
## 边界
> 如果第三方主题移除了标准消息 DOM 或楼层索引属性,卡片可能跳过挂载。这是已知限制——卡片依赖宿主的消息 DOM 结构和楼层索引。
MutationObserver 用于在消息 DOM 变动时重新挂载卡片,节流处理避免频繁触发。