mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: harden recall card display takeover
This commit is contained in:
@@ -1,441 +0,0 @@
|
||||
# Hide / is_system 解耦与提取窗口收敛方案
|
||||
|
||||
## 背景与用户真实诉求
|
||||
|
||||
用户要解决的不是单点 bug,而是两个长期耦合问题:
|
||||
|
||||
1. **自动隐藏旧楼层应只负责 `/hide` / `/unhide`**
|
||||
- 不希望 BME 再本地改 `message.is_system`
|
||||
- “重新应用当前隐藏”和“取消隐藏”也应收敛成 `/hide` / `/unhide`
|
||||
|
||||
2. **BME 提取应按用户在“配置 -> 详细参数”里设置的上下文窗口读取**
|
||||
- 目标参数是 `extractContextTurns`
|
||||
- 主 AI 通过隐藏减少 token
|
||||
- BME 仍能读到足够上下文,但不会无限读太多
|
||||
|
||||
用户不希望继续出现以下情况:
|
||||
|
||||
1. 隐藏状态影响 BME 是否能读到上下文
|
||||
2. 隐藏逻辑与历史恢复/提取逻辑继续共享 `is_system`
|
||||
3. 改掉一处后,另一处又因为 `is_system` 语义不清而出新 bug
|
||||
|
||||
---
|
||||
|
||||
## 这次梳理后的核心结论
|
||||
|
||||
### 结论 1:宿主 ST 的 `/hide` 本身就会改底层消息对象的 `is_system`
|
||||
|
||||
这个结论已经通过运行时实测确认:
|
||||
|
||||
1. 隐藏前:普通 assistant 消息对象没有 `is_system`
|
||||
2. 手动执行 `/hide 6-6`
|
||||
3. 隐藏后:同一条消息出现 `is_system: true`
|
||||
|
||||
这意味着:
|
||||
|
||||
1. **不能把“去掉 BME 自己的 `is_system` 双写”当成最终解**
|
||||
2. 即使删掉 `hide-engine.js` 里的 `markManagedSystemRange` / `restoreManagedSystemFlags`
|
||||
3. 宿主 `/hide` 仍然会把普通历史消息变成 `is_system=true`
|
||||
|
||||
因此,若 BME 提取链路继续按 `is_system` 过滤消息,用户的目标仍然无法实现。
|
||||
|
||||
---
|
||||
|
||||
### 结论 2:当前提取链路虽然已经部分松绑,但还没有真正完成“纯 `/hide`”
|
||||
|
||||
当前代码状态:
|
||||
|
||||
1. [chat-history.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\chat-history.js)
|
||||
- 已新增 `isBmeManagedHiddenMessage`
|
||||
- 已新增 `isSystemMessageForExtraction`
|
||||
- `getAssistantTurns`
|
||||
- `buildExtractionMessages`
|
||||
- `getChatIndexForPlayableSeq`
|
||||
这些核心函数已经不再把 `extra.__st_bme_hide_managed === true` 的消息视为不可提取
|
||||
|
||||
2. [index.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\index.js)
|
||||
- `getSmartTriggerDecision` 相关路径已开始复用上述提取判定
|
||||
|
||||
但问题在于:
|
||||
|
||||
1. 这些改动目前只照顾到了 **BME 自己打了 `__st_bme_hide_managed` 标记** 的消息
|
||||
2. **宿主手动 `/hide`** 会直接把消息写成 `is_system=true`,但不会带 BME 标记
|
||||
3. 所以“真正的纯 `/hide` 设计”还没有完成
|
||||
|
||||
换句话说:
|
||||
|
||||
> 现在已经从“完全依赖 `is_system`”前进到了“BME 自己隐藏的消息可以继续提取”,但还没有前进到“凡是被 `/hide` 隐藏的普通楼层都能继续被 BME 按窗口读取”。
|
||||
|
||||
---
|
||||
|
||||
### 结论 3:宿主 `/hide` 大概率没有稳定附加标记,阶段 2 不应继续押注“找宿主字段”
|
||||
|
||||
基于当前实测:
|
||||
|
||||
1. 宿主手动 `/hide` 后,消息会新增 `is_system: true`
|
||||
2. 当前没有证据表明 `extra` 或其他 message 字段会稳定补充“这是 host hidden ordinary message”的标记
|
||||
|
||||
因此,阶段 2 的主策略不应是:
|
||||
|
||||
1. 继续猜测 `extra.hidden`
|
||||
2. 继续猜测宿主会补别的 message-level 标记
|
||||
|
||||
更稳的策略应改为:
|
||||
|
||||
1. **让 hide-engine 暴露“BME 当前管理的隐藏范围”查询能力**
|
||||
2. extraction 侧按 index 查询“这个楼层是否在 BME 管理隐藏范围内”
|
||||
3. 把“BME 自动隐藏的普通楼层”和“真正 system 消息”区分开
|
||||
|
||||
这条策略的边界也要说清楚:
|
||||
|
||||
1. 它优先解决的是**用户最初诉求里的“BME 自动隐藏旧楼层”**
|
||||
2. 它不自动等价于“宿主任意手动 `/hide` 的所有楼层都被 BME 当可提取消息”
|
||||
|
||||
也就是说,第一轮落地目标应是:
|
||||
|
||||
> 保证 BME 自己自动 `/hide` 的旧楼层不会再干扰 extraction,而不是一次性接管所有外部手动 `/hide` 场景。
|
||||
|
||||
---
|
||||
|
||||
### 结论 4:仍有若干非提取链路在按 `is_system` 过滤,但不应与本次目标混为一谈
|
||||
|
||||
本次梳理中仍能看到这些位置:
|
||||
|
||||
1. [index.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\index.js)
|
||||
- `getLatestUserChatMessage`
|
||||
- `getLastNonSystemChatMessage`
|
||||
|
||||
2. [recall-controller.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\recall-controller.js)
|
||||
- `buildRecallRecentMessagesController` 仍跳过 `is_system`
|
||||
|
||||
3. [recall-persistence.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\recall-persistence.js)
|
||||
- `resolveGenerationTargetUserMessageIndex` 在 normal generation 下会跳过 `is_system`
|
||||
|
||||
这些逻辑未必是 bug。它们更偏:
|
||||
|
||||
1. recall / send-intent / prompt 注入输入整形
|
||||
2. 面向主 AI 可见聊天尾部,而不是 extraction 读取窗口
|
||||
|
||||
所以不建议在“纯 `/hide` + extraction 去耦”阶段把 recall 逻辑一起大改。否则改动面会过大,容易把“主 AI 的可见上下文策略”和“BME 的提取上下文策略”混在一起。
|
||||
|
||||
---
|
||||
|
||||
## 现状问题图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["旧楼层被隐藏"] --> B["宿主 /hide 将普通消息写成 is_system=true"]
|
||||
B --> C["如果 BME 仍按 is_system 过滤"]
|
||||
C --> D["提取窗口读不到被隐藏楼层"]
|
||||
D --> E["用户设置的 extractContextTurns 失去意义"]
|
||||
|
||||
A --> F["BME hide-engine 还会本地双写 is_system"]
|
||||
F --> G["进一步加重隐藏系统与提取系统耦合"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 已确认的代码位置
|
||||
|
||||
### A. 当前仍在本地双写 `is_system` 的隐藏引擎
|
||||
|
||||
[hide-engine.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\hide-engine.js)
|
||||
|
||||
关键位置:
|
||||
|
||||
1. `markManagedSystemRange`
|
||||
- 直接写 `message.is_system = true`
|
||||
- 写入 `extra.__st_bme_hide_managed = true`
|
||||
- 同步 DOM `is_system` attribute
|
||||
|
||||
2. `restoreManagedSystemFlags`
|
||||
- 直接写回 `message.is_system = false`
|
||||
- 删除 `extra.__st_bme_hide_managed`
|
||||
- 同步 DOM `is_system` attribute
|
||||
|
||||
### B. 当前提取窗口的核心入口
|
||||
|
||||
[chat-history.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\chat-history.js)
|
||||
|
||||
关键函数:
|
||||
|
||||
1. `isAssistantChatMessage`
|
||||
2. `getAssistantTurns`
|
||||
3. `buildExtractionMessages`
|
||||
4. `getChatIndexForPlayableSeq`
|
||||
5. `getChatIndexForAssistantSeq`
|
||||
|
||||
### C. 当前仍会影响提取/恢复批次推进的上层入口
|
||||
|
||||
[extraction-controller.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\extraction-controller.js)
|
||||
|
||||
关键函数:
|
||||
|
||||
1. `runExtractionController`
|
||||
2. `onManualExtractController`
|
||||
3. `onRerollController`
|
||||
4. `executeExtractionBatchController`
|
||||
|
||||
### D. 当前“读取窗口配置”的用户入口
|
||||
|
||||
[panel.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\panel.js)
|
||||
|
||||
关键字段:
|
||||
|
||||
1. `bme-setting-extract-context-turns`
|
||||
2. `settings.extractContextTurns`
|
||||
|
||||
这说明用户最初说的“BME 读取用户自己设置的 N 楼层”并不是新概念,代码里已经有配置入口;问题在于提取链路还没有完全摆脱 `is_system` 对窗口的干扰。
|
||||
|
||||
---
|
||||
|
||||
## 设计判断
|
||||
|
||||
### 判断 1:不要再把 `is_system` 当成 extraction 的最终真相
|
||||
|
||||
在当前宿主语义下:
|
||||
|
||||
1. `is_system=true`
|
||||
2. 既可能表示“真正的系统消息”
|
||||
3. 也可能表示“被 `/hide` 隐藏的普通历史楼层”
|
||||
|
||||
因此:
|
||||
|
||||
1. 对主 AI prompt 组装来说,`is_system` 也许仍然有意义
|
||||
2. 但对 BME extraction 来说,`is_system` 已经不是可靠的“是否可读”判据
|
||||
|
||||
### 判断 2:要把“主 AI 可见消息集合”和“BME 提取消息集合”彻底拆开
|
||||
|
||||
建议明确分成两套语义:
|
||||
|
||||
1. **主 AI 可见集合**
|
||||
- 可以继续受 `/hide` 影响
|
||||
- 这是节约 token 的目的
|
||||
|
||||
2. **BME 提取集合**
|
||||
- 应由“真实楼层窗口 + `extractContextTurns`”决定
|
||||
- 不应因为楼层被 `/hide` 而自动丢失
|
||||
|
||||
### 判断 3:在 extraction 真正去耦之前,不要删除 hide-engine 的本地双写
|
||||
|
||||
原因不是双写本身正确,而是现在直接删会导致两个风险:
|
||||
|
||||
1. 提取链路仍可能把宿主 `/hide` 后的消息当成不可提取
|
||||
2. 现有测试和状态恢复逻辑仍依赖 `__st_bme_hide_managed` 追踪“哪些是 BME 自己接管过的消息”
|
||||
|
||||
所以:
|
||||
|
||||
> hide-engine 的本地双写最终应删除,但删除动作必须放到 extraction 语义彻底收敛之后。
|
||||
|
||||
### 判断 4:`managedSystemIndices` 在阶段 4 不能直接消失,而要重定义语义
|
||||
|
||||
当前 `hideState.managedSystemIndices` 同时承担两层职责:
|
||||
|
||||
1. 追踪“哪些消息曾被 BME 本地写成 `is_system=true`”
|
||||
2. 作为 `__st_bme_hide_managed` 的间接来源,帮助 extraction 判断“哪些是 BME 自己接管过的隐藏范围”
|
||||
|
||||
当阶段 4 删除本地双写后:
|
||||
|
||||
1. 第一层职责不再需要
|
||||
2. 第二层职责仍然需要,只是语义应变成:
|
||||
- “BME 当前管理的隐藏范围/索引集合”
|
||||
- 而不是“BME 本地改过 `is_system` 的消息集合”
|
||||
|
||||
所以阶段 3 -> 4 的过渡不能只是删函数,还必须同步:
|
||||
|
||||
1. 重命名或重定义 `managedSystemIndices`
|
||||
2. 让 extraction helper 改为查询“managed hide range”而不是 `__st_bme_hide_managed`
|
||||
|
||||
---
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
### 阶段 1:先把 extraction 的“可读消息判定”抽象成独立策略
|
||||
|
||||
目标:
|
||||
|
||||
1. 不要让 `chat-history.js` 继续直接用“`is_system` + BME marker”做最终判定
|
||||
2. 改成一层明确的语义函数,例如:
|
||||
- `isManagedHiddenMessageAtIndex`
|
||||
- `isTrueSystemMessageForExtraction`
|
||||
- `isExtractionVisibleMessage`
|
||||
|
||||
建议动作:
|
||||
|
||||
1. 在 [chat-history.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\chat-history.js) 收口所有提取可见性判断
|
||||
2. 让:
|
||||
- `getAssistantTurns`
|
||||
- `buildExtractionMessages`
|
||||
- `getChatIndexForPlayableSeq`
|
||||
- `getChatIndexForAssistantSeq`
|
||||
全部只依赖这组新 helper
|
||||
|
||||
目的:
|
||||
|
||||
1. 以后改宿主 `/hide` 兼容策略时,只改一层 helper
|
||||
2. 不再把 `is_system` 判断分散在多个函数里
|
||||
|
||||
### 阶段 2:改成“由 hide-engine 暴露管理范围”,不要继续押注宿主附加标记
|
||||
|
||||
当前已知:
|
||||
|
||||
1. 宿主 `/hide` 会把普通消息改成 `is_system=true`
|
||||
2. 当前没有可靠证据表明宿主会补充稳定的 message-level 隐藏标记
|
||||
|
||||
因此阶段 2 建议改成:
|
||||
|
||||
1. 在 [hide-engine.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\hide-engine.js) 暴露查询接口,例如:
|
||||
- `isInManagedHideRange(index)`
|
||||
- 或 `isManagedHiddenIndex(index)`
|
||||
2. extraction 侧不再猜测“这条 `is_system` 是否是 host hide 后的普通消息”
|
||||
3. 而是直接问 hide-engine:
|
||||
- “这个 index 是否处在 BME 当前管理的隐藏范围内?”
|
||||
|
||||
这样做的好处:
|
||||
|
||||
1. 不依赖宿主是否打标记
|
||||
2. 不依赖消息内容特征猜测
|
||||
3. 与用户真实需求更一致,因为用户要解决的是 **BME 自动隐藏旧楼层** 场景
|
||||
|
||||
这也意味着阶段 2 的设计边界应明确写入:
|
||||
|
||||
1. 第一轮保证“BME 自动隐藏”与 extraction 解耦
|
||||
2. 宿主手动 `/hide` 是否也纳入 extraction,可放在后续兼容层处理
|
||||
|
||||
### 阶段 3:让 extraction 真正按窗口读取,而不是按 hidden/system 可见性读取
|
||||
|
||||
目标:
|
||||
|
||||
1. 真正实现“BME 读取用户配置的 N 楼层”
|
||||
2. `extractContextTurns` 成为决定提取上下文的主参数
|
||||
|
||||
建议动作:
|
||||
|
||||
1. 在 [chat-history.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\chat-history.js) 明确:
|
||||
- assistant turn 序列如何计算
|
||||
- `startIdx/endIdx` 对应的上下文窗口如何取
|
||||
- 哪些消息只是“不进入主 AI prompt”,但仍进入 extraction
|
||||
|
||||
2. 确保 [extraction-controller.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\extraction-controller.js) 的:
|
||||
- 自动提取
|
||||
- 手动提取
|
||||
- reroll / replay
|
||||
全部共享同一套 assistant turn 与 context window 判定
|
||||
|
||||
3. 验证 `extractContextTurns` 的语义在 UI 和代码里保持一致
|
||||
- 用户设置多少,就读取多少个上下文轮次
|
||||
|
||||
阶段 3 还要额外补一条验证说明:
|
||||
|
||||
1. 当前 [chat-history.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\chat-history.js) 的
|
||||
`contextStart = Math.max(0, startIdx - contextTurns * 2)`
|
||||
本质上是按 chat index 偏移,不是按“真实可提取 turn 数”回溯
|
||||
2. 当中间夹杂真正 system 消息时,用户设置的 `extractContextTurns` 可能仍会少读
|
||||
|
||||
这条不一定是 blocker,但阶段 3 验收必须补测试:
|
||||
|
||||
1. 中间夹有真正 system 消息时,窗口是否仍符合用户对“最近 N 个 turn”的预期
|
||||
2. 若不符合,再决定是否把窗口算法从“index 偏移”升级成“按 assistant/user turn 回溯”
|
||||
|
||||
### 阶段 4:只有在阶段 3 通过后,才移除 hide-engine 的本地 `is_system` 双写
|
||||
|
||||
目标:
|
||||
|
||||
1. 把隐藏引擎收敛成纯 `/hide` / `/unhide`
|
||||
|
||||
建议动作:
|
||||
|
||||
1. 在 [hide-engine.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\hide-engine.js) 删除或废弃:
|
||||
- `markManagedSystemRange`
|
||||
- `restoreManagedSystemFlags`
|
||||
- `syncSystemAttribute`
|
||||
- `__st_bme_hide_managed` 相关逻辑
|
||||
|
||||
2. 保留:
|
||||
- 范围计算
|
||||
- slash command 调度
|
||||
- 增量隐藏检查
|
||||
- unhide 管理
|
||||
- managed hide range 查询接口
|
||||
|
||||
3. 重写相关测试,使其不再断言:
|
||||
- “applyHideSettings 后 chat[i].is_system 被 BME 写成 true”
|
||||
|
||||
而改为断言:
|
||||
|
||||
1. 发出了正确的 `/hide` / `/unhide` 命令
|
||||
2. extraction 在隐藏开启时仍能读到配置窗口内的上下文
|
||||
3. `managedSystemIndices`(或其重命名版本)已从“本地双写追踪器”转成“managed hide range 状态”
|
||||
|
||||
---
|
||||
|
||||
## 需要修改/复核的文件清单
|
||||
|
||||
### 必改
|
||||
|
||||
1. [chat-history.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\chat-history.js)
|
||||
- 提取可见性判定的唯一真源
|
||||
|
||||
2. [extraction-controller.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\extraction-controller.js)
|
||||
- 自动提取 / 手动提取 / reroll / replay 是否完整复用新判定
|
||||
|
||||
3. [hide-engine.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\hide-engine.js)
|
||||
- 最终收敛为纯 `/hide` / `/unhide`
|
||||
|
||||
4. [tests\chat-history.mjs](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\tests\chat-history.mjs)
|
||||
- 扩展为“宿主 `/hide` 产生的普通 system 化消息仍可被 extraction 读取”的测试
|
||||
|
||||
5. [tests\hide-engine.mjs](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\tests\hide-engine.mjs)
|
||||
- 重写对 `is_system` 的旧预期
|
||||
|
||||
### 视范围决定是否同步调整
|
||||
|
||||
1. [index.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\index.js)
|
||||
- 任何仍影响 extraction 预判的 `is_system` 过滤
|
||||
|
||||
2. [panel.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\panel.js)
|
||||
- 仅确认配置语义,无需大改
|
||||
|
||||
3. [recall-controller.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\recall-controller.js)
|
||||
4. [recall-persistence.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\recall-persistence.js)
|
||||
- 建议暂不并入第一轮,除非后续验证发现 recall 也必须读取被隐藏楼层
|
||||
|
||||
另外明确说明两处当前不建议改动:
|
||||
|
||||
1. [index.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\index.js) 的 `getLatestUserChatMessage`
|
||||
2. [index.js](C:\Users\brian\OneDrive\Desktop\ST-Bionic-Memory-Ecology-past\index.js) 的 `getLastNonSystemChatMessage`
|
||||
|
||||
原因:
|
||||
|
||||
1. 这两处属于 recall / send-intent 输入整形
|
||||
2. 面向主 AI 可见尾部,而不是 extraction 读取窗口
|
||||
3. 当前保持按裸 `is_system` 跳过隐藏楼层是合理的,不应并入本次 extraction 解耦
|
||||
|
||||
---
|
||||
|
||||
## 建议测试矩阵
|
||||
|
||||
### A. 纯 extraction 语义
|
||||
|
||||
1. 宿主 `/hide` 前后,同一条普通 assistant 消息都应仍可被提取窗口覆盖
|
||||
2. `extractContextTurns=2` 时,只读取目标 assistant 前固定窗口,不无限扩张
|
||||
3. 自动提取、手动提取、replay、reroll 的窗口语义一致
|
||||
4. 中间夹有真正 system 消息时,窗口语义是否仍满足“最近 N 个 turn”的产品预期
|
||||
|
||||
### B. 隐藏与主 AI 可见性
|
||||
|
||||
1. 开启旧楼层隐藏后,主 AI 仍只看到保留窗口
|
||||
2. BME 仍能从被隐藏楼层中拿到所需上下文
|
||||
3. BME 自动隐藏场景依赖的是 managed hide range,而不是宿主附加消息标记
|
||||
|
||||
### C. 回归风险
|
||||
|
||||
1. 不再因隐藏状态变化触发历史误恢复
|
||||
2. 自动提取在新聊天中继续正常推进
|
||||
3. 历史恢复后 extraction status 不再残留“AI 生成中”
|
||||
|
||||
---
|
||||
|
||||
## 对另一个 AI 的最短结论
|
||||
|
||||
> 用户的目标是“隐藏只负责 `/hide`,提取只负责按 `extractContextTurns` 读真实楼层窗口”。本次梳理已确认宿主 ST 的 `/hide` 本身就会把普通消息写成 `is_system=true`,因此不能靠删除 BME 本地 `is_system` 双写来完成解耦。当前最稳的阶段 2 主策略,不是继续寻找宿主附加标记,而是让 `hide-engine.js` 暴露 managed hide range 查询接口,由 extraction 按 index 反查“这个楼层是否是 BME 自动隐藏范围的一部分”,从而把 BME 自动隐藏的普通楼层与真正 system 消息区分开。只有在 extraction 彻底摆脱 `is_system` 依赖后,才能安全把 `hide-engine.js` 收敛成纯 `/hide` / `/unhide`。
|
||||
@@ -1,406 +0,0 @@
|
||||
# ST-BME Recall Card 用户输入显示开关方案
|
||||
|
||||
## 背景与用户痛点
|
||||
当前 ST-BME 在聊天楼层里会额外渲染一张 Recall Card,用来展示:
|
||||
|
||||
- 本轮用户输入
|
||||
- 相关记忆召回
|
||||
- 召回节点数量
|
||||
- token 估算
|
||||
- 展开的召回图与注入内容
|
||||
|
||||
这张卡目前是“额外附着在用户消息下面”的显示层,而不是替换原始用户消息本身。因此会出现一个明显问题:
|
||||
|
||||
- 聊天界面里先看到原始用户输入
|
||||
- Recall Card 里又重复显示一遍“本轮用户输入”
|
||||
|
||||
结果就是视觉重复。尤其对那些本来就自己做了用户输入栏美化的使用者来说,这张卡顶部的“本轮用户输入”区域会和现有前端样式冲突,形成一个突兀的“黄框重复展示”。
|
||||
|
||||
用户的核心诉求有两个:
|
||||
|
||||
1. “美化用户输入”必须变成可选项,不能强制显示。
|
||||
2. 如果用户选择“要显示美化后的用户输入”,那就必须同步隐藏原始用户输入,不能出现两份一模一样的文本并排或上下重复。
|
||||
|
||||
重要边界:
|
||||
|
||||
- 当前插件功能实际上是可用的,问题主要在显示策略。
|
||||
- 不要改召回逻辑、注入逻辑、持久化逻辑、图谱逻辑、检索逻辑。
|
||||
- 这是一个 UI 显示层改造,不是功能链路重写。
|
||||
|
||||
## 已定位结论
|
||||
这个问题已经确认是前端展示层造成的,不是后端或 prompt 注入重复。
|
||||
|
||||
### 1. 黄框来源
|
||||
Recall Card 本体由 [recall-message-ui.js](../../recall-message-ui.js) 创建:
|
||||
|
||||
- `createRecallCardElement(...)` 负责生成整张卡
|
||||
- 其中“本轮用户输入”部分是直接写死渲染的
|
||||
|
||||
关键位置:
|
||||
|
||||
- [recall-message-ui.js:183](../../recall-message-ui.js#L183)
|
||||
- [recall-message-ui.js:203](../../recall-message-ui.js#L203)
|
||||
- [recall-message-ui.js:207](../../recall-message-ui.js#L207)
|
||||
|
||||
### 2. 黄框样式来源
|
||||
Recall Card 的外观样式在 [style.css](../../style.css):
|
||||
|
||||
- 卡片容器: [style.css:2777](../../style.css#L2777)
|
||||
- 用户输入 label: [style.css:2787](../../style.css#L2787)
|
||||
- 用户输入文本: [style.css:2797](../../style.css#L2797)
|
||||
|
||||
### 3. 卡片挂载方式
|
||||
Recall Card 不是一条新消息,也不是替换原消息。
|
||||
它是附加在原始 user 楼层 DOM 下面:
|
||||
|
||||
- 锚点解析: [index.js:1822](../../index.js#L1822)
|
||||
- 卡片挂载: [index.js:1999](../../index.js#L1999)
|
||||
|
||||
而且传入卡片的 `userMessageText` 就是原始 `message.mes`:
|
||||
|
||||
- [index.js:2002](../../index.js#L2002)
|
||||
|
||||
### 4. 后端/数据链路没有重复注入
|
||||
Recall Card 展示的数据来自用户消息上的持久化 recall 记录,不是额外造了一条消息:
|
||||
|
||||
- 读取持久化记录: [recall-persistence.js:24](../../recall-persistence.js#L24)
|
||||
- 只要有 `injectionText` 才渲染卡片: [index.js:1952](../../index.js#L1952)
|
||||
|
||||
因此,用户在酒馆后端看到的“只有用户输入 + 调回的记忆”这一观察是对的。现在的重复只发生在前端视觉层。
|
||||
|
||||
## 目标
|
||||
在不改变 ST-BME 现有功能链路的前提下,为 Recall Card 增加一个“美化用户输入”的显示策略开关。
|
||||
|
||||
最终需要满足:
|
||||
|
||||
- 用户可以关闭 Recall Card 顶部那块“本轮用户输入”展示
|
||||
- 用户也可以保留这块美化展示
|
||||
- 当保留美化展示时,要自动隐藏原始 user 消息文本,避免视觉重复
|
||||
- 当关闭美化展示时,要确保原始 user 消息文本正常显示
|
||||
- 不影响记忆召回、持久化、注入、展开图谱、编辑、删除、重跑召回等现有能力
|
||||
|
||||
## UI 放置要求
|
||||
用户指定要把“美化用户输入”的选项放在“功能开关”页,位置参考截图中的空位。
|
||||
|
||||
建议放置方式:
|
||||
|
||||
- 放在“隐藏旧楼层”这张卡附近
|
||||
- 作为同级的新配置卡,或作为该区域右侧空位中的独立卡片
|
||||
- 文案应一眼说明“这只是显示策略,不影响召回本身”
|
||||
|
||||
建议标题:
|
||||
|
||||
- `美化用户输入`
|
||||
|
||||
建议副说明:
|
||||
|
||||
- `控制 Recall Card 是否接管本轮用户输入的展示方式,不影响实际召回与注入。`
|
||||
|
||||
## 推荐方案
|
||||
不要只做一个简单布尔值。更稳妥的是做成一个三态“显示模式”,这样另一位实现 AI 会更容易避免歧义。
|
||||
|
||||
建议新增设置字段:
|
||||
|
||||
- `recallCardUserInputDisplayMode`
|
||||
|
||||
建议取值:
|
||||
|
||||
1. `off`
|
||||
不在 Recall Card 内显示“本轮用户输入”区域。
|
||||
原始 user 消息保持原样显示。
|
||||
|
||||
2. `beautify_only`
|
||||
在 Recall Card 内显示“本轮用户输入”区域。
|
||||
同时隐藏原始 user 消息文本。
|
||||
这是最符合当前用户诉求的模式。
|
||||
|
||||
3. `mirror`
|
||||
在 Recall Card 内显示“本轮用户输入”区域。
|
||||
原始 user 消息也继续显示。
|
||||
这个模式保留当前行为,作为兼容选项。
|
||||
|
||||
默认值建议:
|
||||
|
||||
- 为兼容旧版本与已有用户习惯,默认值建议设为 `mirror`
|
||||
|
||||
原因:
|
||||
|
||||
- 不会改变现有安装用户的默认视觉结果
|
||||
- 只是新增可选项,不会破坏已有使用体验
|
||||
- 用户可以手动切换成自己想要的模式
|
||||
|
||||
如果维护者更希望新装即减少视觉重复,也可以考虑默认 `off`。但那属于产品决策,不是技术必须。
|
||||
|
||||
## 最小改动原则
|
||||
这次改动必须严格限制在“显示层”和“设置层”。
|
||||
|
||||
允许改动:
|
||||
|
||||
- `index.js`
|
||||
- `panel.html`
|
||||
- `panel.js`
|
||||
- `recall-message-ui.js`
|
||||
- `style.css`
|
||||
|
||||
不要改动:
|
||||
|
||||
- `recall-controller.js`
|
||||
- `retriever.js`
|
||||
- `injector.js`
|
||||
- `recall-persistence.js`
|
||||
- 任何召回算法、注入算法、存储结构、图谱结构
|
||||
|
||||
## 实施方案
|
||||
|
||||
### 一、设置层
|
||||
在 [index.js](../../index.js) 的默认设置中新增字段:
|
||||
|
||||
- 位置: [index.js:343](../../index.js#L343)
|
||||
- 新增:`recallCardUserInputDisplayMode: "mirror"`
|
||||
|
||||
要求:
|
||||
|
||||
- 通过现有 `getSettings()` 和 `updateModuleSettings()` 走统一设置链路
|
||||
- 不新增独立存储机制
|
||||
- 不改服务端设置保存结构的总体行为,只是增加一个普通字段
|
||||
|
||||
### 二、配置面板层
|
||||
在“功能开关”页面增加“美化用户输入”设置入口。
|
||||
|
||||
建议实现方式:
|
||||
|
||||
- 在 [panel.html](../../panel.html) 的 `toggles` 区块中新增一张配置卡
|
||||
- 位置靠近“隐藏旧楼层”卡片,使用截图中右侧空位
|
||||
- 在 [panel.js](../../panel.js) 中补充读写绑定
|
||||
|
||||
建议交互形式:
|
||||
|
||||
- 使用 `select`
|
||||
- 三个选项分别对应:
|
||||
- `关闭美化,仅显示原始输入`
|
||||
- `由 Recall Card 接管显示,并隐藏原始输入`
|
||||
- `Recall Card 与原始输入同时显示(兼容模式)`
|
||||
|
||||
为什么不建议只放 checkbox:
|
||||
|
||||
- 因为 checkbox 很难同时表达“关闭”“替代”“保留重复”三种模式
|
||||
- 三态更清楚,也更利于向后兼容
|
||||
|
||||
如果 UI 组件层面确实只适合 checkbox,也可以退化为:
|
||||
|
||||
- `启用用户输入美化`
|
||||
- `启用后隐藏原始用户输入`
|
||||
|
||||
但三态仍然是首选。
|
||||
|
||||
### 三、Recall Card 渲染层
|
||||
在 [recall-message-ui.js](../../recall-message-ui.js) 里,只改“本轮用户输入”这块的渲染条件,不动其他内容。
|
||||
|
||||
具体要求:
|
||||
|
||||
- `createRecallCardElement(...)` 增加一个新的显示模式参数
|
||||
- `updateRecallCardData(...)` 也能同步接收该模式
|
||||
- 当模式为 `off` 时:
|
||||
- 不创建 `userLabel`
|
||||
- 不创建 `userText`
|
||||
- 或者创建后直接隐藏,但更推荐不创建
|
||||
- 当模式为 `beautify_only` 或 `mirror` 时:
|
||||
- 保持现有用户输入区渲染
|
||||
|
||||
不要改动:
|
||||
|
||||
- 召回条
|
||||
- 节点数 badge
|
||||
- token hint
|
||||
- 展开/折叠
|
||||
- 图谱渲染
|
||||
- 注入文本展示
|
||||
- 编辑/删除/重跑召回按钮逻辑
|
||||
|
||||
### 四、原始用户输入隐藏层
|
||||
这部分是本次方案的关键,也是最容易误伤其他逻辑的地方。
|
||||
|
||||
目标:
|
||||
|
||||
- 只隐藏原始 user 消息正文文本
|
||||
- 不能把整条 `.mes` 或 `.mes_block` 隐藏掉
|
||||
- 否则 Recall Card 自己也会跟着消失
|
||||
|
||||
建议做法:
|
||||
|
||||
1. 在 `index.js` 的 Recall Card 刷新流程中,拿到目标 `messageElement` 后:
|
||||
- 定位其原始文本容器,优先找 `.mes_text`
|
||||
2. 根据 `recallCardUserInputDisplayMode` 决定是否给该文本容器加一个 ST-BME 专用 class 或 data attribute
|
||||
3. 在 `style.css` 里为这个专用 class 提供隐藏样式
|
||||
|
||||
建议新增 class:
|
||||
|
||||
- `bme-hide-original-user-text`
|
||||
|
||||
建议样式原则:
|
||||
|
||||
- 仅隐藏文本区域本身
|
||||
- 不要影响按钮区、头像区、楼层容器尺寸计算
|
||||
|
||||
这里推荐优先用“受控 class 切换”,不要直接写行内 `display:none`,原因是:
|
||||
|
||||
- 刷新时更容易恢复
|
||||
- DOM 重绘后更容易重新应用
|
||||
- 更利于调试
|
||||
|
||||
强制实现约束:
|
||||
|
||||
- 只能在当前目标楼层的 `messageElement` 作用域内查找 `.mes_text`
|
||||
- 推荐写法是 `messageElement.querySelector('.mes_text')`
|
||||
- 不允许使用 `document.querySelectorAll('.mes_text')`、全局批量扫描后再按索引猜测匹配对象,或任何会波及其他楼层的全局操作
|
||||
|
||||
原因:
|
||||
|
||||
- 这个需求只应该影响“当前挂载 Recall Card 的那一条 user 楼层”
|
||||
- 如果实现成全局 `.mes_text` 操作,最容易出现误隐藏其他消息、切换模式后残留、以及聊天重绘时状态串楼层的问题
|
||||
|
||||
### 五、刷新与恢复逻辑
|
||||
Recall Card UI 不是一次性静态渲染,而是会随消息刷新、设置变更、聊天切换重新挂载或更新。
|
||||
|
||||
因此必须保证:
|
||||
|
||||
- 切到 `beautify_only` 时,已存在的卡片能立即隐藏原始输入
|
||||
- 切到 `off` 或 `mirror` 时,已隐藏的原始输入能立即恢复
|
||||
- 删除 Recall Card 时,原始输入也要恢复
|
||||
- 聊天切换或楼层 DOM 重建后,显示状态能重新正确应用
|
||||
|
||||
建议实现策略:
|
||||
|
||||
- 在 `refreshPersistedRecallMessageUi()` 流程中统一应用
|
||||
- 在 `cleanupRecallArtifacts(...)` / `cleanupRecallCardElement(...)` 附近补一层“恢复原始文本显示”的兜底
|
||||
- 在设置更新时,若 patch 包含 `recallCardUserInputDisplayMode`,主动触发一次 Recall Card UI refresh
|
||||
|
||||
## 兼容性要求
|
||||
|
||||
### 必须保持不变
|
||||
- Recall 是否执行
|
||||
- Recall 结果如何写入持久化记录
|
||||
- 注入文本如何进入 prompt
|
||||
- token 估算
|
||||
- 展开的节点图
|
||||
- 编辑 recall 注入文本
|
||||
- 删除 recall 记录
|
||||
- 重跑 recall
|
||||
- 非 user 楼层不挂载 Recall Card 的规则
|
||||
|
||||
### 必须新增保证
|
||||
- 无论用户怎样切换这个显示模式,都不能影响后端实际发送内容
|
||||
- 无论用户怎样切换这个显示模式,都不能让 recall 记录丢失
|
||||
- 无论用户怎样切换这个显示模式,都不能改变注入结果
|
||||
|
||||
## 建议验收场景
|
||||
另一位实现 AI 可以按下面场景验收。
|
||||
|
||||
### 场景 1:兼容模式
|
||||
设置为 `mirror`
|
||||
|
||||
期望:
|
||||
|
||||
- 行为与当前版本一致
|
||||
- 原始用户输入可见
|
||||
- Recall Card 顶部“本轮用户输入”也可见
|
||||
|
||||
### 场景 2:关闭美化
|
||||
设置为 `off`
|
||||
|
||||
期望:
|
||||
|
||||
- 原始用户输入可见
|
||||
- Recall Card 仍保留“相关记忆召回”条、节点数、token、展开内容
|
||||
- 只是顶部“本轮用户输入”区域不再显示
|
||||
|
||||
### 场景 3:美化接管
|
||||
设置为 `beautify_only`
|
||||
|
||||
期望:
|
||||
|
||||
- 原始用户输入文本被隐藏
|
||||
- Recall Card 顶部“本轮用户输入”仍显示
|
||||
- 聊天界面不再看到两份重复文本
|
||||
|
||||
### 场景 4:设置动态切换
|
||||
在已有聊天记录上来回切换三种模式
|
||||
|
||||
期望:
|
||||
|
||||
- 不需要重开聊天
|
||||
- UI 立即生效
|
||||
- 不出现隐藏状态残留
|
||||
|
||||
### 场景 5:删除 recall 记录
|
||||
在 `beautify_only` 模式下删除某条 Recall Card
|
||||
|
||||
期望:
|
||||
|
||||
- Recall Card 消失
|
||||
- 原始用户输入文本恢复显示
|
||||
|
||||
### 场景 6:刷新 / 切聊天 / 重新挂载
|
||||
|
||||
期望:
|
||||
|
||||
- 模式设置持久生效
|
||||
- DOM 重建后显示仍然正确
|
||||
|
||||
### 场景 7:多条消息并存时的作用域验证
|
||||
|
||||
期望:
|
||||
|
||||
- 在一个有多条 user 消息、且其中只有部分楼层存在 Recall Card 的聊天里
|
||||
- 切换 `beautify_only` 时,只隐藏挂载了 Recall Card 的目标楼层原始文本
|
||||
- 没有 Recall Card 的其他 user 楼层不得被隐藏
|
||||
- 切回 `off` 或 `mirror` 时,只恢复对应目标楼层,不出现跨楼层串改
|
||||
|
||||
## 风险点与防误改提醒
|
||||
|
||||
### 风险 1:误把整条消息隐藏
|
||||
如果实现时隐藏的是 `.mes`、`.mes_block` 或更外层容器,会把 Recall Card 自己也一起隐藏。
|
||||
|
||||
正确做法:
|
||||
|
||||
- 只处理原始用户文本区域
|
||||
- 而且这个文本区域必须通过当前 `messageElement` 局部查询获得,不能用全局 `.mes_text` 选择器批量处理
|
||||
|
||||
### 风险 2:把显示问题误改成数据问题
|
||||
这个需求不是要删 `message.mes`,也不是要清理持久化 recall 记录。
|
||||
|
||||
正确做法:
|
||||
|
||||
- 只改 DOM 渲染与 class 切换
|
||||
|
||||
### 风险 3:设置切换后残留隐藏状态
|
||||
如果只在创建卡片时加隐藏样式,而不在 refresh / cleanup 时恢复,会导致切换模式后文本状态错乱。
|
||||
|
||||
正确做法:
|
||||
|
||||
- 在刷新和清理路径都处理恢复逻辑
|
||||
|
||||
### 风险 4:误动 Recall Card 其他区域
|
||||
用户只对“本轮用户输入这块美化显示”有意见,不是要取消整个 Recall Card。
|
||||
|
||||
正确做法:
|
||||
|
||||
- 只拆分顶部 user-input 区块的显示策略
|
||||
- 保留下面的 recall bar 与展开内容
|
||||
|
||||
## 推荐实施顺序
|
||||
|
||||
1. 在 `index.js` 增加默认设置字段
|
||||
2. 在 `panel.html` / `panel.js` 增加配置项,并放到“功能开关”页截图所示空位
|
||||
3. 在 `recall-message-ui.js` 给顶部 user-input 区块加显示模式控制
|
||||
4. 在 `index.js` 增加“隐藏/恢复原始 user 文本”的 DOM 协调逻辑
|
||||
5. 在 `style.css` 增加专用隐藏 class
|
||||
6. 跑一轮上述验收场景
|
||||
|
||||
## 给实现 AI 的一句话总结
|
||||
这次改动的本质是:
|
||||
|
||||
- 保留 Recall Card 功能
|
||||
- 只把 Recall Card 顶部“本轮用户输入”的显示变成可选
|
||||
- 并在“由 Recall Card 接管显示”时隐藏原始 user 文本
|
||||
- 不要动任何 recall / injection / persistence 的核心逻辑
|
||||
@@ -2799,6 +2799,8 @@
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--bme-on-surface, #e4e1e6);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
@@ -1662,6 +1662,76 @@ async function testRecallCardUserTextRefreshesWithoutCardRecreate() {
|
||||
}
|
||||
}
|
||||
|
||||
async function testRecallCardDisplayModeToggleRestoresOriginalUserText() {
|
||||
const chat = [
|
||||
{
|
||||
is_user: true,
|
||||
mes: "line-1\nline-2",
|
||||
extra: {
|
||||
bme_recall: buildPersistedRecallRecord({
|
||||
injectionText: "recall-0",
|
||||
selectedNodeIds: ["n1"],
|
||||
nowIso: "2026-01-01T00:00:00.000Z",
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
const harness = await createRecallUiHarness({ chat });
|
||||
const messageElement = createMessageElement(harness.document, 0, {
|
||||
stableId: true,
|
||||
withMesBlock: true,
|
||||
isUser: true,
|
||||
});
|
||||
const userTextElement = messageElement.querySelector(".mes_text");
|
||||
userTextElement.textContent = chat[0].mes;
|
||||
harness.chatRoot.appendChild(messageElement);
|
||||
|
||||
try {
|
||||
harness.context.getSettings = () => ({
|
||||
panelTheme: "crimson",
|
||||
recallCardUserInputDisplayMode: "beautify_only",
|
||||
});
|
||||
harness.api.refreshPersistedRecallMessageUi();
|
||||
|
||||
let card = harness.chatRoot.querySelector(".bme-recall-card");
|
||||
assert.equal(card?.dataset.userInputDisplayMode, "beautify_only");
|
||||
assert.equal(
|
||||
userTextElement.classList.contains("bme-hide-original-user-text"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
card?.querySelector(".bme-recall-user-text")?.textContent,
|
||||
"line-1\nline-2",
|
||||
);
|
||||
|
||||
harness.context.getSettings = () => ({
|
||||
panelTheme: "crimson",
|
||||
recallCardUserInputDisplayMode: "mirror",
|
||||
});
|
||||
harness.api.refreshPersistedRecallMessageUi();
|
||||
|
||||
card = harness.chatRoot.querySelector(".bme-recall-card");
|
||||
assert.equal(card?.dataset.userInputDisplayMode, "mirror");
|
||||
assert.equal(
|
||||
userTextElement.classList.contains("bme-hide-original-user-text"),
|
||||
false,
|
||||
);
|
||||
|
||||
delete chat[0].extra.bme_recall;
|
||||
harness.api.refreshPersistedRecallMessageUi();
|
||||
assert.equal(
|
||||
userTextElement.classList.contains("bme-hide-original-user-text"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
harness.chatRoot.querySelectorAll(".bme-recall-card").length,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
harness.restoreGlobals();
|
||||
}
|
||||
}
|
||||
|
||||
function makeEvent(seq, title) {
|
||||
return createNode({
|
||||
type: "event",
|
||||
@@ -4277,6 +4347,7 @@ await testRecallCardDoesNotMountOnNonUserFloor();
|
||||
await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates();
|
||||
await testRecallCardExpandedContentRerendersAfterRecordUpdate();
|
||||
await testRecallCardUserTextRefreshesWithoutCardRecreate();
|
||||
await testRecallCardDisplayModeToggleRestoresOriginalUserText();
|
||||
await testRecallSubGraphAndDataLayerEntryPoints();
|
||||
await testRerollUsesBatchBoundaryRollbackAndPersistsState();
|
||||
await testHistoryRecoveryAbortClearsVectorRepairState();
|
||||
|
||||
Reference in New Issue
Block a user