Repository navigation
v1.7.0 — 同一构建通吃 DSH 0.1.5 / 0.1.7,设置入口搬进左栏
一句话:这一版终于能在 DSH 0.1.7 上活下来了,顺带把设置入口搬进了左栏、把页面重新排了一遍。
以前一个构建只能伺候 0.1.5 那条线,0.1.7 上是真的会出事 —— 不是"卡片不显示"这种小事,是整个 DSH 直接退出。现在两条线一个构建通吃。
一、0.1.7 上一启用插件,DSH 就原地去世
先说症状,因为它实在太有迷惑性了:用户装好插件、点启用、进程直接没了。
桌面端只留下一句 DSH Host exited (1)。日志?日志里最后一行还是个无关紧要的 warning,然后就戛然而止,一个 error 都没有。
这种"日志被齐刷刷截断"的样子,其实是宿主的一个设计在作祟:DSH 装了 installFailLoud,进程里任何没被捕获的异常、任何没接住的 Promise rejection,都会让它当场 exit(1)。而那句真正的死因只写进 stderr —— 打包版 Windows 应用压根没有控制台,于是它就悄无声息地消失了。查了半天日志找不到错,别怀疑自己,它真的没写进去。
那么是谁抛的?三个坑叠在一起,而且全在插件这边,内核无辜:
坑 1:宿主那边的 API 换了个名字,老代码还在敲旧门。 0.1.7 把 settings.installSection(...) 整个删了,换成了 configure({auto}, owner)。我们的代码虽然做了"先看看有没有"的判断,但问题是 —— 判断失败时它只是 warn 一声就算了,设置页从此永远不出现;而万一服务真在场,它就直接裸调,在 0.1.7 上"啪"地从 apply() 里抛出去。宿主一看:未捕获异常,走你。
坑 2:客户端 inject 里写了两个"有你没我"的服务名。 0.1.5 提供 settingsScope,0.1.7 换成了 configForms。而 cordis 的依赖闸门是硬的:inject 里只要有一个服务不存在,apply 就永远不会被执行,fiber 就永远挂在那里 PENDING。桌面端报 renderer boot failed (plugins: dsh-workbuddy-xdpool),然后拒绝打开。它不报错,它就是不开。
坑 3:卡片注册的那个槽位,0.1.7 里根本不存在。 settings.plugin.item 是 0.1.5 的专属座位。0.1.7 的设置左栏只认 settings.section(内置的通用 / 模型 / 插件页都坐那儿)。我们一直往一个不存在的座位上放东西,于是卡片安安静静地不显示 —— 不报错、不警告,就是不出现。
怎么修的: 一律改成"先探路、再走",不猜。
- 宿主侧:有
installSection就用它(0.1.5),没有就用configure({auto:true})(0.1.7),两个都没有才 warn。写入时的命名空间也得跟着线路换 —— 0.1.5 用自己起的名字,0.1.7 得用宿主给这个插件分配的条目 id(ctx.fiber.entry.options.id)。这里猜错的代价是每次保存都报No configurable plugin entry。 - 客户端:
inject只留两条线都有的slots/locale,设置服务改用ctx.get()软探测(服务不在就返回 undefined,不抛)。 - 顺手声明
engines.dsh: ">=0.1.5-rc.1 <0.2.0",让插件市场把两条线都认成兼容;另外把dsh.client.inject里早就停更的dsh-client-runtime噪声清掉了。
二、0.1.7 上保存没反应,或者说"没保存成功"
0.1.7 有个硬门禁:schema 里一个 volatile 字段都没标的条目,直接拒收。write() 抛 has no volatile fields,describe() 干脆跳过它 —— 页面就是一片空白。我们的 Config 当时一个都没标,于是保存必炸、页面必空。
还有个更阴的:0.1.7 把 volatile 字段以活引用 {get(): T} 的形式交回来(这样改设置不用重挂载就能生效)。我们直接拿它去比较 —— 拿一个对象比一个字符串,结果就是刚存进去的值,读回来是"没设置"。
对,这就是那个"我明明填了保留积分,重开又变回 0"的元凶。不是没存进去,是读错了。
怎么修的: Config 全部 12 个字段都过一遍 asVolatile() —— schemastery 3.18.3+ 会调 .volatile();0.1.5 那条线是 3.18.2,压根没这个方法,那就退化成什么都不做(不能手写 meta.volatile,那会绕过 schemastery 自己的摆放校验,塞给 0.1.5 一个它看不懂的东西)。读取则统一走 unwrapVolatileDeep() 把活引用剥开。另外监听 loader/volatile-update,保存后运行中的实例立刻重读,不用重启。
三、设置入口搬进左栏 + 页面重新排了一遍
- 入口:改注册到
settings.section,左栏现在多出一行,名字叫 XD Pool。 - 图标:这个槽位的注册契约只投影
id/order/label,没有 icon 字段 —— 宿主对不认识的 id 一律画个齿轮。想换图标只能自己动手:照着dshmarket的做法,按 label 在 DOM 里认出自己那一行、打个标记,再用 CSS 把宿主的 svg 藏起来,用mask-image+currentColor把自己的图标盖上去(颜色跟着主题走)。外加一个MutationObserver盯着 —— 切语言、切主题时行元素会被重建,标记会掉。 - 页面:以前要点一下才展开,现在整页直接铺开,并且重新排成分区卡片:页头(图标 / 标题 / 描述,右边放「重新检测账号」「清除冷却」)、状态卡片(地区切换 + 状态灯 + 汇总 + 账号使用方式)、自动化卡片、账号卡片、模型卡片。顺带修了个陈年小毛病:原来有个
if (!open) return,意思是没展开就一次都不去拉状态 —— 所以你刚点开永远是空的,得傻等一轮轮询。现在挂载即拉。
四、顺手修的几件小事(都来自 0.1.7 真机排查)
- 取密钥的超时 10 秒 → 30 秒。 那个子进程是拿 WorkBuddy 的 Electron 二进制当普通 Node 跑,冷启动第一次 spawn 实测就要 4.5 秒。碰上同时在跑 pnpm install、或者杀毒软件在全盘扫描,10 秒预算直接被击穿 → 所有加密凭据被读成"未登录",还要再背 60 秒负缓存。成功结果本来就是进程级缓存,加长预算最多只付一次。
- 补上盘符根目录。
D:\workbuddy\、D:\workbuddyai\是真实安装位置,可我们只扫Program Files,一级扫描永远够不着它们。 - 顶层 IIFE 补
.catch()。 那个 IIFE 里的await一旦失败就变成 unhandled rejection,在installFailLoud下同样是直接杀宿主。 - 保存收益账本失败不再拖垮宿主。 这条来自社区反馈:以前 scheduler 把「写 settings」的调用放在
try里但没 await —— 而try/catch只抓得住同步抛错,Promise 的 rejection 会直接漏成 unhandled rejection,在installFailLoud下就是exit(1)。那位朋友遇到的正是这个:打开设置页 → 读状态 → 滚动收益账本 → 写盘失败 → 整个 DSH 起不来。他把原因归到「写入方法名从set()改成了update()」,那确实是当时那一次抛错的直接原因(我们 1.6.0 已经改过了),但结构上的隐患当时还在:只要保存以任何理由失败(服务缺失、文件被锁、校验不通过),宿主照样会崩。现在给持久化回调的返回值挂上 rejection 处理,同时保留同步抛错的原有兜底 —— 保存失败只会记一条 warning,账本继续在内存里服务卡片,绝不影响已经领到的奖励。
验证
typecheck/typecheck:client全绿;vitest 222 用例全绿(原 198 + 新增 24)。- 新增
tests/host-compat.test.ts:两条线的注册分支互斥且都不抛、两个 API 都没有时只 warn、全部字段都标了 volatile、活引用的浅 / 深解引用、inject不含互斥服务名、注册槽位是settings.section、0.1.7 用条目 id / 0.1.5 用 namespace、整页无折叠且轮询不再依赖展开状态、左栏名称与图标注入、异步持久化失败不产生 unhandled rejection(该用例经反向验证:还原成旧写法时它会失败)。 - 拿真实的 0.1.7 schemastery 3.18.4 解析真实的 Config(12 个字段全部变成活引用、schema 校验无异常);再用假宿主加载真实产物
lib/index.js/lib/client.js,确认apply()在 0.1.7 / 0.1.5 / 两个服务都没有 这三种情形下都不抛异常。 lib/与src/同步重建(不然从 GitHub 源码装的人拿到的还是旧产物)。
最后
这一版由用户在本机 DSH Desktop 2.0.14(内核 0.1.7-rc.1)实测确认:入口出来了、卡片显示了、名字和图标都对了。
感谢反馈问题的朋友 —— 那个"日志戛然而止"的现象描述得非常关键,否则光看日志根本无从下手。