|
我在开发一个小的简单的插件,因为不支持工作空间多文件夹,所以想根据Session来添加多个只读的参考库,使用dsh + flash开发。 目前自己的插件
|
| 能力 | 方案 | 代价 |
|---|---|---|
| 存储 | sidecar JSON:<dshHome>/plugin-data/ref-lib/<sessionId>.json(dshHomePath('plugin-data', 'ref-lib')),node 端原子读写(writeFileSync+renameSync);冷读时:sidecar → 旧日志 ref-lib/set 迁移 → 父会话继承 → 空 |
无(插件 node half 有完整 fs 权限;天然 per-session、随 dsh home 持久化) |
| UI 读(打开面板/刷新) | ctx.remote.commands.execute('/ref-lib list') → node 端读 sidecar → 返回文本 → client 解析 |
每次打开面板渲染一张命令结果卡片 |
| UI 写(添加/移除) | ctx.remote.commands.execute('/ref-lib add <path>' / remove <id>) → node 端更新 sidecar |
渲染一张命令结果卡片 |
| 目录选择 | ctx.workspaces.pickDirectory()(host 原生 OS 选择器,与"选择工作区"同源) |
无 |
| 上下文注入 | ctx.systemPrompt.context({ name: 'reference-libs', order: 150, text }),按 context.agent?.session 从 sidecar 读列表渲染 |
无 |
| UI 挂载 | conversation.session.header.utilities(list 型 slot,per-session scope) |
无 |
| 命令入口 | /ref-lib add/list/remove(对话可用,也是 UI 的中转载体) |
无 |
工程形态:标准 bundle(dsh.bundle + dsh.client 双面插件),node half 用
Service 类(default export,loader 按 static inject 注入),client half 构建为
window.__ModuleLoader__.load({ id, factory }) 闭包工厂(平台模块 external),
UI 走 settings.section/conversation.session.header.* 等 list 型 slot。
4. 给官方的扩展点建议(按解除阻塞的优先级)
- 插件事件注册口 /
Session.append的ignorable写入面(对应随附 issue):
让插件能安全持久化 per-session 事件。设计笔记自己写明"gains that surface with its
first user / deferred until such a consumer exists"——我们就是那个 consumer。 - settings 配置客户端白名单插件化(如
ctx.settings.expose(ns)或注册时声明
configClient: true):插件 UI 可直接读写自己的配置,摆脱命令卡片污染。 - 插件自定义 RPC(或 client→node 事件通道):插件注册自己的方法进
rpc-map,
让 UI 与 node 端有官方数据通道;这将同时解决读与写的污染与绕行。 - (可选)投影支持非事件数据源:让插件把 sidecar 状态接入投影,UI 一次读取。
5. 一句话总结
rc.6 对第三方插件开发者而言,"浏览器端可见 + 可持久化 + 不污染会话"三者无法兼得:
settings 白名单拒绝 UI 直读直写,session 自定义事件会让日志整条拒读,命令分发是唯一
执行通道但必然渲染卡片。最终方案被迫采用 sidecar 文件存储 + 命令中转——能用,
但每一步都在绕行官方缺失的扩展点。
问题概述
第三方插件可以通过 Session.append 写入自定义会话事件(用 declare module 扩展 SessionEventMap 后,例如 session.append('ref-lib/set', { libs: [...] })),写入时没有任何报错——append 不做事件类型的词汇表校验。但持久化读取路径会拒绝加载任何包含"仓库内置白名单(KNOWN_SESSION_EVENT_TYPES)之外事件类型"的日志,除非该事件在信封上带 ignorable: true 标记——而 Session.append 根本不提供设置 ignorable 的途径(其 opts 只接受 surface 元数据 surfaceOp/sourceEventSeqs,且仅对消息类事件可用)。
对插件作者的实际后果:写入成功,会话在下次加载/重启时变得永久不可读(harness 明确"拒绝重建会话")。harness 自己也无法读回它当初接受的日志。
复现步骤
- 插件扩展会话事件词汇表:
declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { 'my-plugin/state': { /* json */ } } }
- 在命令处理器(或任何持有
Session的地方):session.append('my-plugin/state', { any: 'payload' }) // 成功,已持久化
- 重启 harness 并尝试加载/恢复该会话。
→ 加载被拒绝:日志含my-plugin/state,它不在KNOWN_SESSION_EVENT_TYPES内,且没有ignorable: true。
期望行为
既然插件能写会话事件,就应该能(a)把它标记为 ignorable(信息性事件,让 first-party 读取器安全跳过),或(b)注册自己的事件类型让读取器能解释日志。目前两个途径都不存在。
实际行为
- 写入路径:成功(
append不做词汇表校验,只做旧版形状校验)。 - 读取路径:loud refusal(显式拒绝) 整条日志——会话无法恢复,插件侧没有任何 API 可以规避。
证据(来自 master 源码)
-
packages/core/session/src/known-event-types.ts(生成文件)头部注释:"The persistence read path refuses to interpret a log containing a type outside this set unless the event carries the envelope's
ignorablemarker … such a log was likely written by a newer harness, and silently skipping a required event would reconstruct a wrong session. Downstream (out-of-repo) plugin events are outside this list by construction; a registration surface for them is deferred until such a consumer exists."
(持久化读取路径拒绝解释任何包含白名单外事件类型的日志,除非事件带ignorable标记……这类日志很可能是更新的 harness 写的,静默跳过必需事件会重建出错误的会话。仓库外插件事件按构造天然不在该列表内;给它们提供注册口被延后,直到出现这样的消费者。) -
packages/core/session/src/types.ts—SessionEvent.ignorable字段:"A writer sets
trueonly on purely informational records whose loss cannot affect reconstruction; defaulting to required means a forgotten marker over-refuses (an inconvenience) rather than silently resuming a gutted session."
(写入者只在"丢失不影响重建的信息性记录"上设true;默认是"必需",忘记标记只会过度拒绝(不便),而不会静默恢复一个被掏空的会话。) -
packages/core/session/src/index.ts—Session.append签名:append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : [] ): SessionEvent<T>
SurfaceIntent只携带surfaceOp/sourceEventSeqs;没有ignorable参数(非消息类事件甚至没有 opts)。 -
设计笔记
.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md:"Until a registration surface exists, an out-of-repo plugin's events refuse resume under first-party readers — the pre-release stance accepts that, and the refusal is loud rather than silent."
(在注册口存在之前,仓库外插件的事件在 first-party 读取器下拒绝恢复——预发布阶段接受这一点,拒绝是显式而非静默的。)
"writers do not yet setignorable(no producer needs it), soSession.appendgains that surface with its first user."
(写入者目前都不设ignorable(没有生产者需要它),所以Session.append会在第一个用户出现时获得该写入面。)
"Per-plugin runtime registration of known event types … a registration surface for them is deferred until such a consumer exists."
(按插件运行时注册已知事件类型……它们的注册口被延后,直到出现这样的消费者。)
影响
第三方插件作者(即被延后的注释所等待的"消费者")无法在不破坏会话日志的前提下持久化自己的 per-session 状态。笔记和代码都明确把这件事延后到"出现消费者"——本 issue 就是那个消费者回来报告。
补充约束:这也与 settings 配置客户端白名单(packages/host/apiproxy/src/api-proxy.ts 的 exposedNamespaces(),同为 deferred)相互作用——插件作者目前没有任何官方认可的浏览器可见状态通道。
建议修复(任一即可解除插件作者的阻塞)
Session.append增加可选ignorable参数(或信封接受它):插件可以写入信息性事件,让 first-party 读取器安全跳过。改动最小,也符合笔记自身的计划("会在第一个用户出现时获得该写入面")。写入者必须显式选择(默认保持 required)。- 按插件运行时注册已知事件类型(即被延后的"注册口"):读取器解释日志时,查询"仓库白名单 ∪ 已注册插件事件类型"。
- 两者结合:注册用于 required 插件事件,
ignorable显式选择用于信息性事件。
如维护者认可方向,我愿意协助实现或测试。
Replies: 3 comments
|
全部主张在 master(47f943859b)上逐行验证通过,你这个「事故」是同一个已知缺口的最新一起现场报告——我先给验证,再补家族脉络和修复方向。 1. 源码验证(约束 4 完全属实)
2. 家族脉络:这是第 4 份现场报告
3. 设计意图澄清(重要) 4. 修复设计(与 #1584 结论一致)
5. 顺带一句:你列的约束 1(settings 白名单)是维护者已明确标记 deferred 的 #1606 线程;约束 2(无插件自定义 RPC)与它同属"下游插件表面"工作流——和 session-append 缺口一起,是插件生态的三个最大堵点。你的文档把六个约束按踩坑顺序整理得很好,建议保留在帖子里作为维护者的第一手参考。 你最后的 scoped 变通(不 append 自定义事件,改用命令分发/系统提示注入)是正确的绕行方向;等 |
|
感谢 https://github.com/Quophic/dsh-persona-memory |
|
插件事件没带 会话医生可以 如果加载错误其实是 seq gap / torn 而不是 unknown type,可以用: dsh plugin --profile web add "github:xiaoshenming/dsh-session-surgeon#main" |
感谢 https://github.com/Quophic/dsh-persona-memory
目前找到了替代方式,使用自定义webServer的方式实现