【实践笔记】DSH 0.1.7 删掉了 settings 的双向绑定:插件要改三处,还有一个“改完不生效”的坑 #7873
Replies: 2 comments
"改完不生效"这条最值得单独提——它比"要改三处"更容易修1. 请把两类问题分开你的实践笔记里其实混了两件事,而它们的处置完全不同:
建议把第 2 条单独拆出来,并给出最小复现:改了哪一处、期望看到什么、实际看到什么、以及重启后是否生效。 2. 我能提供的一条已核实事实(与 settings 的写入有关)settings 的写入走原子替换 + 跨进程写锁( ⇒ 也就是说:如果"改完不生效"表现为"写没落盘",那在 0.1.7-rc.2 上可能已经被修(旧行为是:持有锁的进程已退出时,锁仍在、后来者只能超时)。请先在最新版上复现一次——这一条很可能直接决定你的现象还在不在。 3. 请补三样
4. 你的"实践笔记"这个形式很好它比单点 bug 报告更有价值(能帮同迁移的人少踩坑)。建议结构固定成:变更点 → 旧写法 → 新写法 → 不生效的表现 → 规避。这样它既能被维护者用来补迁移文档,也能被插件作者直接照抄。 5. 版本提醒
一条边界我确认的是 settings 写入是原子替换 + 跨进程锁、且 rc.2 修过"锁持有者已退出"这一类——这是我对"改完不生效"最可能的成因给出的方向。你那三处具体是什么、以及不生效的确切表现以你补充的为准。 |
|
顺着 PerryLink 那条补实测,他问的三样都跑过了。 先说结论:不是"写没落盘"。三处我都核过—— 三处指的是:profile 的 再补一条这两天才踩到的,跟"谁权威"直接相关:宿主传给 |
Uh oh!
There was an error while loading. Please reload this page.
0.1.7 这次把 settings 改得挺彻底:插件以前那套"注册命名空间 + 双向绑定"整个没了,也没留别名。
我把自己的一个插件迁过去,迁移很快就完了,但是迁移过去以后在"设置面板能显示、但改完不生效"
这个BUG上栽了。写出来省得别人再绕。
一句话说清:0.1.7 的 settings 变成了单向投影——宿主只把 Loader 里插件条目的 Config
投影成表单,写入落到 profile 的 cordis.patch.yml 上,但它不会再通知插件。旧的 scope.watch()
是实时回调,删掉之后,插件如果只在加载那一刻读一次配置,你在面板里改什么它都不知道。
改法就三处:host 侧把 settingsSchema 改成 export const Config 并给每个可写字段盖 .volatile();
读取从 scope.get()/scope.watch() 改成 settings.describe();客户端把
ctx.settingsScope.bind({namespace}) 换成 ctx.configForms.get(条目 id)。组件体一行都不用动。
真要提醒的是第三条之后那件事:watch 没了,你得自己保证"读的是新鲜值"。我一开始没意识到这点,
面板里加了扫描目录、点插件自己的按钮,它回我"还没设置要扫描的文件夹"——配置明明写进文件了。
--- 后面是AI助手提供的具体技术细节,不关心的可以跳过 ---
具体是什么变了
以前(0.1.5/0.1.6)插件这么写:
0.1.7 这套全没了。settings 服务现在只做一件事:把 Loader 里本插件条目的 Config
投影成一份可编辑的表单文档。命名空间就是 profile 里那个条目的 id。
两个硬要求,都不报错,所以特别容易踩:
整个条目被 describe() 静默过滤:面板直接消失,日志里一个字都没有。
.volatile() 需要 schemastery >= 3.18.4,低版本上会在模块求值阶段抛错。写个降级包装就行:
客户端侧:
快照结构没变(getSnapshot / subscribe / set / unset / mutate 都在),所以渲染组件的代码不用改。
然后是那个把我骗了两天的坑
watch 没了之后,"改设置"这件事的语义变了:写进去 ≠ 插件知道。
迁移完我以为行为跟以前一样,结果是这样:
去看 profile 的 cordis.patch.yml,值明明在里面(写入链路是好的)。查了一圈才发现:
插件只在 apply 那一刻读了一次配置,之后一直用内存里那份旧快照,而宿主这次写入并没有
重新 apply 插件。所以表现成"面板显示正常、值也落盘了、插件就是不认"。
这跟"改完要重启一次才生效"很容易混。在 0.1.5 上 scope.watch() 是实时的,所以这是 0.1.7
引入的行为变化,不是老毛病。
改法很土,但很有效:把"取配置"改成每次用之前都重读一遍。
需要覆盖的入口别漏:每个工具的 execute、每个斜杠命令的 handler、还有挂在生命周期钩子上的
自动任务(比如我那个"打开 DSH 时检查要不要补扫")。describe() 是内存操作,多调几次不心疼。
顺手记一个定位法,遇到"面板写了但插件不认"按顺序做三步就够了:
最后两句
客户端那一半(settingsScope 被移除导致插件 boot 卡住、报错却指向 client-half failed)已经有人
报过了:#7445。我这篇补的是 host 侧和运行期那一半——它们症状更隐蔽,因为面板看起来完全正常。
我的插件是直接要求 0.1.7+ 的,没做向下兼容。如果谁手里的插件还要同时跑 0.1.6,欢迎说说你那段
兼容探测是怎么写的(我猜得靠"settings 服务上有没有 describe"来判断,但没实测过,不敢乱给)。
如果你也踩了同一处,或者你那边是完全另一副症状,回帖说一声。
以下是AI助手提供的技术细节,不关心的可以跳过
附:实测环境与验证方式
全新 profile、没有任何配置,然后在设置面板里加目录并把库位置填成一个文件夹,
不重启直接点插件页面上的扫描按钮 -> 报"扫描完成 · 看了 3 个文件 · 新增 3 个"。
0.1.2 在同一步会报"还没设置要扫描的文件夹"。
附:新旧 API 对照
附:两个容易连坐的相邻坑
mkt-<包名>,插件里写死的包名就对不上,表现是"路由都 200、面板却报配置面板不可用"。
重启后由 bundle 层加载,id 才恢复成 cordis.patch.yml 里写的那个。前置条件是包已经在
dsh.profile.bundles 里。
文件和目录。用户很可能把文件夹填进"文件路径"那一格,而 SQLite 拿目录当库文件会直接
unable to open database file,整条链路挂掉。判定"像目录"(已存在的目录 / 以分隔符结尾 /
没有扩展名)就拼成 <目录>/index.db。
附:几篇相关的既有帖子
附:非官方实践笔记,与 DeepSeek 官方无关。
[Practice note] DSH 0.1.7 turned settings into a one-way projection: three things plugins must change, plus a "the panel renders but edits do nothing" bug
0.1.7 rebuilt settings thoroughly: the plugin-side "register a namespace + two-way binding" model is
gone, with no alias left behind. Porting my plugin was quick — what took the time was a bug that
showed up afterwards: the settings panel renders fine, but edits never take effect. Writing it down
so nobody has to rediscover it.
The short version: in 0.1.7 settings are a one-way projection. The host projects the Config of your
Loader entry into an editable form and writes your edits into the profile's cordis.patch.yml — but
it never notifies the plugin. The old scope.watch() was a live callback; with it gone, a plugin that
reads its config once at apply time simply never sees what you changed in the panel.
Three changes fix the API half: host side, turn settingsSchema into export const Config with every
writable field marked .volatile(); read through settings.describe() instead of scope.get()/watch();
and on the client, replace ctx.settingsScope.bind({namespace}) with ctx.configForms.get(entryId).
The component body itself needs no changes.
The part worth flagging is what comes after: with watch gone, keeping a fresh config is now your job.
I found out the hard way — I added a scan folder in the panel, clicked the plugin's own button, and
it answered "no folders configured yet", while the value was already sitting in the config file.
--- Technical details below are provided by an AI assistant; skip if you are not interested ---
What actually changed
Before 0.1.7:
In 0.1.7 none of that exists. The settings service only projects the Config of your Loader entry
into a document, and the namespace is that entry's id.
Two hard requirements, both silent when you get them wrong:
unwrapped and the entry is filtered out silently.
the whole entry is filtered out of describe(): the panel just disappears, with nothing logged.
.volatile() needs schemastery >= 3.18.4 and throws at module evaluation time on older versions, so
guard it: const vol = (s) => (typeof s.volatile === "function" ? s.volatile() : s);
Client side: the snapshot shape is unchanged (getSnapshot / subscribe / set / unset / mutate), so
only the binding and the inject entry change.
The trap that cost me two days
The value was in cordis.patch.yml the whole time. The plugin had read its config once, at apply
time, and the host's write did not re-apply the plugin — so it kept answering from a stale snapshot.
It looks exactly like "settings need a restart, it's a legacy quirk": on 0.1.5 scope.watch() made
edits live, so this is a 0.1.7 behaviour change, not an old wart.
The fix is unglamorous but reliable: re-read before every use.
Cover every entry point: each tool's execute, each slash command's handler, and anything you hung
on a lifecycle hook. describe() is an in-memory call; calling it often is cheap.
A three-step triage for "the panel writes, the plugin disagrees":
Two closing notes
The client half — settingsScope removed, plugins hanging on boot, reported as a misleading
"client-half failed" — is already covered by #7445. This post adds the host half and the runtime
half, which are sneakier because the settings panel looks perfectly healthy.
My plugin now requires 0.1.7+, so I did not build a compatibility shim. If your plugin still has to
run on 0.1.6 as well, I would like to hear how you probe for it — my guess is checking whether the
settings service exposes describe(), but I have not tested that, so I will not pretend otherwise.
Non-official practice note, not affiliated with DeepSeek. If you hit the same thing — or a
completely different symptom — say so below.
Technical details (appendix)
Environment: DSH 0.1.7-rc.2 (npm), Windows 11, web profile, Node 22. Plugin: dsh-lost-and-found
(a local file index), ported in 0.1.2, runtime read fixed in 0.1.3.
How it was verified: a separate isolated instance (own DSH_HOME, own port, node_modules junction —
the live instance was never touched), a fresh profile with no configuration at all. Added a folder
and set the database location to a folder through the settings panel, then clicked the plugin's own
scan button without restarting: "scan finished, looked at 3 files, added 3". On 0.1.2 the same step
answers "no folders configured yet".
API map:
Two adjacent traps:
plugin market, the entry id becomes mkt-; a hard-coded package name no longer matches and
you get "configuration panel unavailable" while every route still answers 200. After a restart the
bundle layer loads it with the id from cordis.patch.yml. Prerequisite: the package is listed in
dsh.profile.bundles.
database) should accept both a file and a folder. Users will type a folder, and SQLite answers
"unable to open database file" when handed a directory, taking the whole pipeline down. Treat
"looks like a directory" (existing directory / trailing separator / no extension) as a folder and
append index.db.
Related threads: #7445 (client-side settingsScope removal), #7654 (0.1.5 -> 0.1.7 recovery checklist),
#1447 / #1454 / #164 (older watcher bugs), #2239 / #1606 (settings namespace exposure discussion).
All reactions