Repository navigation
Releases: qgx1992/dsh-ui-tools
Release list
v0.4.33 — compat-check 在无 kernels 目录的桌面版上自动从 app.asar 定位内核(此前整条检查链直接退出 2)
v0.4.33 — 修 compat-check 在「本机没有 kernels 目录」的桌面版上直接退出
本次只动 tools/ 下的检查脚本,lib/client.js 零改动 —— 插件运行时行为与前一个版本完全相同。升级只为让「本仓库的测试能跑起来」这件事对得上号。
① 现象:一个把失败伪装成成功的管道
npm run check 的真实退出码是 2,唯一失败点是 tools/compat-check.mjs:
找不到本机 cordis(DSH 内核副本)。可设 DSH_CORDIS_ENTRY 指向 cordis/lib/index.js 再跑。
检查链是 && 串联的,它一失败,排在后面的 tools/live-smoke.mjs 从来没被执行过。
更阴的是:把输出管道接成 npm run check | tail -30 时,看到的退出码是 0(管道返回的是末段进程的状态),人很容易照着「exit 0」判定全绿读过去。
② 根因:定位逻辑只认两个已经不存在的目录
locateCordis() / locateSemver() 原本只扫两处:
%APPDATA%\DSH-Exoskeleton\kernels~/.dsh/kernels
而桌面版从 0.2.1-rc.2 起把内核整体打进 ...\Programs\DeepSeek Harness\resources\app.asar,内部布局是 dsh/node_modules/@deepseek-ai/cordis。本机这两个 kernels 目录根本不存在,于是定位必然失败 → process.exit(2)。
对照实验证明断言本身是健康的:手工把 DSH_CORDIS_ENTRY / DSH_SEMVER_ENTRY 指向一份可用的 cordis 与 semver 后,立刻变成 127/127 通过。坏的只是「定位方式」,不是断言。
③ 修法:定位改为三级,最后一级直接读 app.asar
顺序是 显式环境变量 → kernels 目录扫描 → app.asar 解包回退。新增一套共用实现:
| 函数 | 职责 |
|---|---|
desktopAsarPaths() |
定位桌面版 app.asar 与其 .unpacked 目录 |
openAsar() |
读头 16 字节,readUInt32LE(12) 取目录表大小,条目偏移从 16 + headerSize 起算 |
asarNode() / asarRead() |
按路径查目录表节点、读实体(unpacked: true 的节点从 app.asar.unpacked 读) |
asarExtract() |
把一个节点递归落盘(处理 files / link / 普通文件三态) |
extractFromAsar() |
按 package.json 的 dependencies 递归解出所需闭包,返回按 main 解析的入口 |
几个刻意的取舍:
- 只解所需闭包,不解整个包:cordis 实测只带出
@deepseek-ai/cosmokit+@standard-schema/spec,合计 464 KB,而不是把 182 MB 的app.asar摊开。 - 落到标准布局:
%TEMP%\dsh-ui-tools-asar\<体积>-<修改时间>\node_modules\<pkg>\,去掉dsh/node_modules前缀后铺成普通node_modules树,包内部的裸导入按 Node 规则逐级向上就能解析到兄弟依赖,不需要改写任何 import。 - 缓存名带内核指纹:目录名含 asar 的体积与修改时间,内核升级自动换新目录;新目录建立时清掉旧 stamp,不会越积越多。
- 入口按
main解析,不写死lib/index.js。 semver也走这条回退:它若缺席,场景 0b 的声明层断言(engines.dsh覆盖全部内核版本)会直接 FAIL。
④ 顺手收口:两份 asar 解析合成一份
loadKernelAssemblers() 的来源② 原本另写了一份手写 asar 解析,与 extractFromAsar() 是两套独立实现 —— 历史上正因两处漂移,来源② 曾漏掉 dsh/node_modules 前缀而静默失效。现在统一复用同一套读取函数。
exit(2) 的错误文案也改成列出全部已尝试来源,避免下次再花时间猜“它到底找过哪里”。
⑤ 负向对照:这条断言不是摆设
修的过程中第一版 extractFromAsar 把包路径写成不带 dsh/node_modules 前缀的形式,asarNode 查不到 → 仍然 exit 2,白改一次;加上前缀并改为「包相对路径 + 内部前缀 + main 解析」后才通过。
另做两条缓存边界实测:手工造一个陈旧 cache 目录 → 跑完被自动清掉;把整个 cache 删空 → 自动重建并仍然 127/127。
⑥ 验证
npm run check 退出码 0、0 FAIL:
| 检查项 | 结果 |
|---|---|
check-hooks-order |
7/7 |
check-model-seat-hug |
38/38 |
check-context-menu |
228/228 |
check-profile-drift |
3/3(1 个已安装 profile) |
compat-check |
127/127,来源已打印为 app.asar → ...\@deepseek-ai\cordis\lib\index.js |
live-smoke |
SKIP(拿不到 URL/token 时的设计行为,退出码 0,非失败) |
打 tag 前按项目规矩做了「tag 内容 = 该版本实际代码」校验:git archive 导出本提交到干净临时目录(里面没有 node_modules)后跑同一套检查 —— 全部通过,package.json 版本号 0.4.33,行为矩阵为「新内核 6/6 槽位功能;旧内核 5/6 + 无横幅(功能五缺席属预期降级)」,与本文件声称的一致。这也顺带证明 asar 回退不依赖仓库里的 node_modules。
安装 / 升级
dsh plugin --profile desktop add github:qgx1992/dsh-ui-tools#v0.4.33
如果你之前用的是 commit hash 钉法(github:qgx1992/dsh-ui-tools#<40 位 hash> 或 #main),建议改成上面的 tag 钉法,之后每版只改版本号。
但要说清楚:本版插件行为与前版完全一致(lib/client.js 零改动),不升级也不影响任何已装功能;这次升级的意义只在于让仓库自带的检查链能跑通。
v0.4.15 — 修掉功能六两个真实缺陷:所有菜单项点了没反应 + 刷新页面只报 t is not defined
v0.4.15 — 修掉功能六两个真实缺陷
用户实测「右键工作区 → 复制工作区路径」和「刷新页面」两项都没反应。它们其实是两个互相独立的 bug。
① 所有菜单项都点不动(不只是这两项)
视口监听写成了 window.addEventListener("blur", onViewportChange, true)。blur 不冒泡,但有捕获阶段,所以挂在 window 上的捕获监听会收到元素级失焦:
按下鼠标 → 焦点从菜单第一项(菜单打开时 first.focus() 留在它身上)移到被点的那一项 → 前一项触发 blur → closeCtxMenu() 把菜单节点从 body 摘掉 → 紧接着的 pointerup / mouseup 落到菜单背后的列表行上 → click 永远不会派发。
真机 HUD 轨迹(铁证,全程没有 C:click):
C:pointerdown>span IN_MENU ON_ITEM
C:mousedown>span IN_MENU ON_ITEM
C:pointerup>div.hIlkoa_projectRow[rk=workspace:…]{r=treeitem}
C:mouseup>div.hIlkoa_projectRow[…]
MENU_CLOSED
修法:只监听窗口自身失焦,不加 capture。
const onWindowBlur = (event) => { if (event.target !== window) return; closeCtxMenu(); };
window.addEventListener("blur", onWindowBlur);resize 仍保留捕获监听,拆卸段对称 removeEventListener。
② 刷新页面只弹一条 t is not defined、页面纹丝不动
ctxReload() 位于模块作用域,函数体第一行 ctxToast(t("toast.reloading")) 却引用了 installContextMenu(scope, t, uiWorkspace) 的形参 t —— 那个作用域里根本没有 t,ReferenceError 又抛在 try 之外,于是 window.location.reload() 与 150ms 的 location.replace 兜底一次都没跑到,只剩菜单项 catch 弹出的那条报错。
修法:ctxReload(reloadingText) 改为收参数,三个调用点(选中文本 / 工作区行 / 会话行)统一传 ctxReload(t("toast.reloading"))。
断言与负向对照
tools/check-context-menu.mjs 94 → 109 条:新增 §8b(元素级 blur 不许关菜单、窗口自身失焦仍要关)、§8c(点「刷新页面」不抛异常且 window.__reloaded === true),各带负向对照;§14 新增静态断言「ctxReload 剥注释后不得出现 t(」「三处调用点必须传文案」「window 的 blur 监听不得带 capture」;并修掉测试台自身一个从未被发现的缺陷:4 处负向对照沿用上一节 DOM,querySelector(".dsh-ctx-menu") 命中残留的旧菜单节点(点到旧闭包),负向对照因此恒以「没弹菜单」假红 —— 改为每节重建 DOM 后才真正生效。
负向对照实测(本轮真跑,改完即还原):
| 改坏什么 | 实测 |
|---|---|
blur 监听改回带 capture 的旧写法 |
109/109 → 106/109 |
ctxReload 改回引用 t |
109/109 → 106/109 |
三处调用点改回无参 ctxReload() |
109/109 → 107/109 |
真机验收(真实鼠标输入,非消息注入)
- 复制工作区路径 → 剪贴板由哨兵值变成该工作区的真实路径(
D:\888yhxy)。 - 刷新页面 → 抓到「HARNESS / Loading plugins…」启动画面,约 1.4s 后窗口标题恢复为会话名,无错误对话框,宿主 boot 标识不变。
npm run check 退出码 0:钩子钉 7/7 + 座位钉 28/28 + 右键钉 109/109 + 漂移钉 4/4 + compat-check 101/101 + live-smoke SKIP。git archive v0.4.15 导出后跑同一套同样全绿,且导出包的 lib/client.js SHA256 与真机实装文件逐字节一致。
安装 / 升级
dsh plugin --profile desktop add github:qgx1992/dsh-ui-tools#v0.4.15
⚠ 真机踩坑(顺带记下)
把 profile 里的实装 client.js 就地改掉之后,靠 DSH 对插件 bundle 的热更新(约 10s 自动重跑新代码、不必刷新页面)验证是安全的;但此时若再触发整页 reload,shell 的 boot 审计会报 dsh-ui-tools: import failed (see console for the import error) 并弹出「应用无法启动」原生对话框(logs/crash-*-web-boot.log 只有摘要行、没有底层异常栈),而冷启动同一份文件完全正常 —— 怀疑是宿主进程启动时算出的插件 rev 与磁盘现文件不一致。结论:改完实装文件后,要么只靠热更新验证,要么连宿主一起重启。
v0.4.14 — 修复功能六 uiWorkspace inject 事故(六个功能全部消失)+ 右键菜单动作改调真实服务方法
v0.4.14 — 修复功能六的「六个功能全部消失」级 inject 事故 + 右键菜单动作改调真实服务方法
这一版全部是修 v0.4.13 首发后暴露的真实缺陷,来自同一位用户的连续报障。两处缺陷都不是「少个小功能」,而是插件整体不可用或功能整体失效。
① 最严重:uiWorkspace 写进入口 inject → 六个功能全部消失
v0.4.13 首发版把功能六需要的官方 uiWorkspace 服务(由 dsh-client-ui-workspace 提供)直接加进了入口 inject。
cordis 的 inject 语义是「必须等到这些服务就绪才开始」,任何一个等不到,整条 fiber 就停在 PENDING。而这个服务在精简 bundle / 旧内核里可能不存在 —— 于是后果不是「右键菜单少一个功能」,而是:
dsh-ui-tools: pending (waiting for services: uiWorkspace)
FAIL boot 审计 0 失败 → 无「1 entry did not activate」/无横幅
即六个功能一起消失 + 顶部加载横幅。用户看到的表象是「右键菜单里的项都没效果」,极易误诊成菜单自身的事件 bug —— 实际菜单根本没装上。这与 v0.4.2 在 uiConversation 上踩的是同一个坑。
修法:uiWorkspace 移出入口 inject,改为可选获取:
ctx.inject(["sessions", "workspaces"], (scope) => {
const uiWorkspace = ctx.get("uiWorkspace"); // 缺席时是 undefined,不抛错
// 每个调用点都探测:typeof uiWorkspace?.startSession === "function"
});cordis 的 ctx.get(name) 在服务缺席时返回 undefined 而不抛错(内部 const impl = key && this.store[key]; if (!impl) return;),所以这是一条干净的降级路径而非崩溃。
② 右键菜单动作几乎全失效:调了内核里不存在的方法
用户报障原文:
当前右键工作区菜单:新建会话,复制工作区路径,刷新页面失效。右键会话菜单:只有重命名有效果,其他选项均无效果
根因是几处动作调用了内核里根本不存在的方法:
| 动作 | 首发时的错误调用 | 正确调用 |
|---|---|---|
| 新建会话 | workspaces.startSession(id) |
uiWorkspace.startSession(id) |
| 创建会话分支后切过去 | sessions.open(childId) |
uiWorkspace.openSession(childId) |
| 刷新页面 | 直接 location.reload()(无兜底) |
ctxReload() |
workspaces 是纯 WorkspaceController,没有 startSession;sessions 上确实有个叫 open 的方法,但它不收参数,语义是「打开本会话的事件流」,跟「切到某个会话」毫无关系 —— 写 sessions.open(childId) 语法合法、运行不抛错、但什么都不会发生。这是一种最阴的错法:不报错、不生效,用户只能看到「点了没反应」。加上动作抛错被 ctxToast 吞掉,故障彻底静默。
复制工作区路径 一并修掉(改为稳定取 workspace.path 并加 execCommand 降级)。
断言为什么没拦住?——「断言的价值上限 = 假数据对真实契约的还原度」
这是本版最值得记的教训。tools/check-context-menu.mjs 首发时是 82/82 全绿的,但它放过了上述全部 bug。原因:测试台的假服务里,workspaces 上挂了 startSession、sessions 上挂了可用的 open —— 测试台照着一个不存在的契约写,于是把 bug 固化成了「正确」。
因此新增 §14 服务契约(右键钉 82 → 94):
- 静态真实方法表:列出
sessions/workspaces/uiWorkspace各自真实存在的方法名,逐条核对源码没有调用表外方法。假服务可以随便补方法、骗得过运行时断言,骗不过方法表。 - 点名两条历史写错的(
sessions.open、workspaces.startSession)——这条断言最尖锐,因为它专门盯着真出过事的两个名字。 - 两组运行时负向对照。
验证
npm run check 退出码 0:
| 检查 | 结果 |
|---|---|
check-hooks-order.mjs |
7/7 |
check-model-seat-hug.mjs |
28/28 |
check-context-menu.mjs |
94/94 |
check-profile-drift.mjs |
4/4 |
compat-check.mjs |
101/101 |
live-smoke.mjs |
SKIP(未提供真机 URL) |
三组负向对照均实测变红(改坏 → 跑断言 → 还原 → 比对 SHA256):
| 改坏什么 | 实测 |
|---|---|
把 "uiWorkspace" 塞回入口 inject |
compat-check 101/101 → 50/85,报 pending (waiting for services: uiWorkspace) |
uiWorkspace.startSession 改回 workspaces.startSession |
右键钉 94/94 → 91/94,报「调用了内核里不存在的方法 — workspaces.startSession」 |
把 window.location.reload() 改成 void 0 |
右键钉 94/94 → 93/94 |
tag 导出物独立复验:git archive v0.4.14 解出的 lib/client.js SHA256 = F0AE9915…,与工作区一致;在导出目录里跑完整 npm run check 得到同一结论。
升级
# profile 依赖写的是 #v0.4.14 即可;改完务必 pnpm install 重解析
pnpm install⚠ 两个已知坑(都写进 AGENT.md 了):
- 改了 tag 要用
pnpm update强制重解析 —— 光改 specifier 可能沿用 lockfile 里的旧解析。 - 绝不就地
Copy-Item覆盖 profile 里的安装副本 —— 那是 pnpm store 的硬链接,就地写会穿透污染内容寻址 store(pnpm store status会报错)。正解是先Remove-Item断链再Copy-Item(本版部署即用此法,部署后fsutil hardlink list只剩一个路径)。
刷新页面即生效(纯浏览器插件、无构建步骤)。
改动文件
lib/client.js(inject 与可选获取、三处动作改调真实方法、ctxReload)、tools/check-context-menu.mjs(§14)、tools/compat-check.mjs(行为矩阵文案)、README.md、package.json(版本 0.4.13 → 0.4.14)。
v0.4.13 — 新增会话/工作区右键菜单;功能一白字与弹出列表实底
v0.4.13
本次新增功能六「会话 / 工作区列表右键菜单」(插件由五功能变为六功能),并按用户要求调整功能一的两处样式,同时补上三份守护断言。
新增:功能六 右键菜单
- 会话行右键:重命名 / 归档会话 / 创建会话分支 / 复制会话 ID / 复制工作目录 / 刷新页面
- 工作区行右键:新建会话 / 复制工作区路径 / 归档工作区全部会话 / 刷新页面
- 选中文本后右键:复制所选文本 / 发送所选文本到输入框
关键设计取舍
- 目标识别不依赖 aria-label 与界面语言,用官方渲染时写死的
data-row-key前缀(session:/workspace:)。官方 row-actions 按钮在空白会话与已归档会话上根本不渲染,只靠「有会话操作按钮」会把这类行判成非会话行。 - 解析走三级降级(DOM 行 id →
aria-selected→ 标题唯一命中),任一级无法唯一确定就返回null并保留浏览器原生菜单,绝不猜;标题命中多个时同样拒绝 —— 宁可不管,也不操作错会话。 - 实现完全走纯 DOM:只挂 document 级
contextmenu/pointerdown/keydown三个监听,菜单节点appendChild到 body,不注册任何 slot、不搬动也不包裹 slot 渲染出来的节点、无MutationObserver、无轮询定时器。因此旧内核上也照常可用(无 alpha 依赖,不进alphaFeatures子 fiber;用到的sessions/workspaces本就在入口 inject 里,不新增声明)。 - 无障碍:
role=menu/menuitem与 roving tabindex,支持Esc关闭、方向键与Home/End移动、点击外部关闭、已preventDefault的事件不再接管(与其他右键类插件共存不打架);危险项(归档)走独立样式类。
功能一样式调整(按用户要求)
- 两个主按钮常态文字色改为纯白
#fff(.mss-caret用color: inherit,箭头随字一起变白)。不引主题 token 而写死白:本机 DSH 主题偏好为system且 WindowsAppsUseLightTheme=0(深色),composer 底为深色故白字对比最高;而--dsw-alias-brand-text在本机解析为#0f1115(近黑)、--dsw-alias-label-primary-foreground也只在浅色主题下才是白,两者都会随主题解析成非白。 - 弹出列表由毛玻璃改为完全不透明实底:
background直接取--dsw-specific-menu,删掉两条backdrop-filter;描边 / 圆角 / 三级投影保留以维持浮层分层。面板文字仍是--dsw-alias-label-primary(深色字配浅色实底、不跟着按钮变白)。
新增守护断言(已并入 npm run check)
| 工具 | 条数 | 覆盖 |
|---|---|---|
tools/check-context-menu.mjs |
82 | 判据识别 / 三级降级与唯一性保护 / 菜单组装与空菜单拒绝 / 三类目标分发 / 键盘与外部点击 / 卸载后监听全解绑 / 动作真调到官方服务而非空壳 / 抛错不冒泡 / 不引用参考插件包名 / CSS 并入共享标签 / 开关门控语义 |
tools/check-hooks-order.mjs |
7 | 词法扫描所有函数组件,条件 return 跨越 Hook 调用即 FAIL,防「Hook 槽位残留 undefined」崩溃 |
tools/check-profile-drift.mjs |
4 | 比对 ~/.dsh/profiles/*/ 的声明、pnpm-lock specifier、node_modules 实装版本三者一致 |
check-model-seat-hug 由 19 条增至 28 条(新增白字 4 条与面板实底 5 条);compat-check 由 98 条增至 101 条(新增样式标签唯一 / 六份 CSS 哨兵齐全 / 废弃 id 不再出现)。
设置页
新增开关「会话 / 工作区列表右键菜单」(偏好字段 ctxMenuEnabled,默认开)。关掉时一根监听都不挂,浏览器原生右键菜单原样保留;门控显式订阅偏好仓库,因此改开关即时生效、无需刷新页面,卸载时订阅与监听一并清理。
验证
| 断言套件 | 结果 |
|---|---|
tools/check-hooks-order.mjs |
7/7 通过 |
tools/check-model-seat-hug.mjs |
28/28 通过 |
tools/check-context-menu.mjs |
82/82 通过 |
tools/compat-check.mjs |
101/101 通过(cordis 0.2.1-alpha.2) |
| 行为矩阵 | 新内核 5/5 功能;旧内核 4/5 功能 + 无横幅(功能五缺席属预期降级) |
以上结果均从本 tag 导出物独立跑出(git archive v0.4.13),证明该 tag 的产物本身可用。
升级
profile 的依赖写法指向本 tag 即可:
github:qgx1992/dsh-ui-tools#v0.4.13
改完 specifier 后必须让 pnpm 重新解析(pnpm install),不要手工往 node_modules 里拷文件 —— 那会穿透 pnpm 的硬链接写入内容寻址 store,导致后续 install 报 ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE。
v0.4.11 — 两个主按钮背景指定为透明 + v0.4.10 收尾全仓库口径扫
v0.4.11 — 两个主按钮背景指定为透明 + v0.4.10 收尾全仓库口径扫
背景
上一版 v0.4.10 把输入框的模型双按钮往右推、紧贴「听写」按钮之后,这两个按钮(供应商 / 模型)仍带着一层 color-mix(… 8%, transparent) 的品牌色底,在输入框右侧显得像两块「有底的方块」,跟官方 composer 里其它无底控件不连贯。
同时 v0.4.10 是跨两个版本合并发布的(它同时含 v0.4.9 与 v0.4.10 的全部改动),收尾时留下了几处不报错、也不被任何检查覆盖的口径漂移:README 的验证计数与内核路径过期、「明确不覆盖的范围」里还在讲一个 v0.4.9 已整体移除的功能、tools/live-smoke.mjs 里残留 16 处已删功能的断言。
本次一并处理。
实现
1) 两个主按钮背景 = 透明(用户要求)
lib/client.js 的 MSS_CSS 里,[data-mss-btn] 的常态背景:
[data-mss-btn] {
…
- background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4176e6) 8%, transparent);
+ background: transparent;
…
}- 常态完全透明:不再有那层 8% 品牌色底,视觉上就是输入框里两个「纯文字 + 箭头」的透明框,与官方 composer 其它无底控件连成一片。
- 保留交互反馈:
:hover:not(:disabled)与[aria-expanded="true"]仍是 16% 半透明淡色。按钮若连悬停/展开都没有任何视觉反馈,就失去了「可点」的提示。若你希望连悬停也全透明,把那两条的background一并改成transparent即可。 - 该规则块其余声明(尺寸 / 圆角 / 字色 / 省略号 / 过渡)一字未动。
2) 新增 5 条守护断言(tools/check-model-seat-hug.mjs,14 → 19 条)
这条需求的失败模式是「改完刷新页面看不出差别」,而且只读源码文本抓不到(写回 color-mix 照样是合法 CSS)。所以断言沿用该文件既有的做法:求值后的 CSS 按 cascade 取实际生效声明(它会真求值 MSS_CSS 模板串,而不是 grep 源码)。钉住四面:
- 常态背景必须是
transparent; - 没有另设不透明的
background-color把它盖掉; :hover与[aria-expanded="true"]各自仍保留半透明反馈(防「改一处、静默坏另一处」);- 该规则不得牵连面板内的下拉项(
[data-mss-row]不命中)。
负向对照有效:把 background: transparent 改回 color-mix(… 8%, transparent) → 18/19、exit 1(新增的那条变红),改回来即恢复 19/19。
3) 全仓库口径扫尾(v0.4.10 的遗留)
| # | 残留 | 处理 |
|---|---|---|
| ① | README 与 AGENT.md 的验证口径写 93/93 + 0.2.1-alpha.1 |
更新为实测的 98/98 + 0.2.1-alpha.2,并注明「断言计数随内核枚举动态变化,门槛是零 FAIL,别当固定指标读」 |
| ② | README「明确不覆盖的范围」仍有一条在讲「修改的文件」tab 的取数逻辑(该功能 v0.4.9 已整体移除) | 删掉,改为一句指向变更记录的口径说明 |
| ③ | tools/live-smoke.mjs 16 处已删功能痕迹 |
见下 |
| ④ | issue #1(v0.4.7「撤回异常」归因)技术已闭环但仍 OPEN | 核对后关闭 |
③ 的细节(该文件不在 npm run check 里,所以从来不报红,但真跑必炸):MARKERS.modifiedFiles([data-mfs-root])、pageerror 过滤里的 mfs、整段「2b 功能三取数」断言、设置页「四个偏好行都在」(真实设置页自 v0.4.9 起只有 3 行,这条必然 FAIL)、以及 demo 段误读的 row-mfs / mfs-toggle / mfs-hint(真实 demo 里这三个 id 早已是 row-alpha / alpha-toggle / alpha-hint,原样跑会直接抛异常)。同时更新了过期的文件头注释(v0.4.2 验证用、内核 0.1.2-alpha.3),补上 DSH_SMOKE_URL 环境变量入口,并把它并入 npm run check(无 URL+token 时优雅 SKIP、exit 0,不阻塞)。
4) 功能编号统一(附带发现的真矛盾)
v0.4.9 移除「功能三 修改的文件」后,编号整体前移一位(旧四→三、旧五→四、旧六→五),但源码注释里仍大量写着「功能六」指速度计,甚至同一文件里出现「功能五」既指设置页又指速度计的自相矛盾。本次把源码注释与断言 label 统一校正为现行编号,并把速度计那段的分隔哨兵注释由
/* ── 功能六取数段(compat-check 直接截取本段做断言,勿动这两条分隔注释)── */
改名为「功能五取数段」——tools/compat-check.mjs 靠这个字符串定位(3 处 indexOf),已同步修改。README 里 v0.4.6 / v0.4.7 历史条目中的「功能六」有意保留(那是在描述当时的行为,判据是「这句在描述当前行为还是当时行为」)。
另修掉 lib/client.js 文件头「六个 UI 工具合并」与下方只列 5 项的自相矛盾。
验证
| 项 | 结果 |
|---|---|
node --check lib/client.js |
通过 |
node tools/check-model-seat-hug.mjs |
19/19 通过(新增 5 条背景断言) |
node tools/compat-check.mjs |
98/98 通过(cordis 0.2.1-alpha.2),零 FAIL |
node tools/live-smoke.mjs(无参) |
SKIP、exit 0(未提供 URL+token,属预期) |
npm run check(四段串联) |
exit 0,零 FAIL |
| 负向对照 | 把透明背景改回 color-mix → 18/19、exit 1 |
| 行为矩阵 | 新内核 5/5 功能;旧内核 4/5 功能 + 无横幅(功能五缺席属预期降级) |
| tag 内容校验 | git archive v0.4.11 导出后跑全套:19/19 + 98/98、行为矩阵 5/5 功能、package.json version = 0.4.11 |
tag 校验这一步是 v0.4.10 发布时踩过的坑(当时 v0.4.9 的 tag 打在了删代码之前的 commit 上,发出去的是名实不符的坏 artifact,最终撤回)。本次已按要求先导出 tag 内容、跑完套件、并核对功能计数与版本号,均一致。
导出的 lib/client.js SHA256:65AA8E6492C1F8692E88BCFB3CD6AC3252BC4AAA2888B0E7A5D07FA774325536
升级
dsh plugin --profile web add github:qgx1992/dsh-ui-tools#v0.4.11或手动改 ~/.dsh/profiles/web/package.json 的依赖:
"dsh-ui-tools": "github:qgx1992/dsh-ui-tools#v0.4.11"注意:如果你之前把依赖钉在某个 commit hash 上,请改用上面的发布标签。commit hash 钉法会在任何一次 pnpm install / 桌面版升级时把插件回退到那个旧 commit,表现为「上次明明修好了,这次又复现」——pnpm-lock.yaml 里还有一份同样的解析结果,两处都要换。
装完重启一次 DSH;之后本包再改 lib/client.js 只需刷新页面(纯浏览器 bundle,无构建步骤)。
本版改动文件
lib/client.js、tools/check-model-seat-hug.mjs、tools/compat-check.mjs、tools/live-smoke.mjs、demo/two-button*.html(3 个静态预览页同步透明背景)、README.md、package.json
v0.4.10 — 功能一模型双按钮自适应紧贴听写按钮,修复两处真实样式 bug
模型双按钮自适应紧贴听写按钮 + 两处真实样式 bug 修复
① 紧贴的实现(纯 CSS,lib/client.js 的 MSS_CSS)
官方 composer 右侧动作区结构:
div.trailing (gap:12px; margin-left:auto)
├── div.standardControls ← 插件座位所在槽的父容器
│ ├── [slot conversation.input.right] ← 座位
│ └── [slot conversation.input.model] ← 官方模型按钮(被插件隐藏)
├── div.activity ← 听写按钮的槽
│ └── span.triggerAnchor > button[aria-label="开始录音"]
└── button.primary ← 发送
座位在 .standardControls 内,听写按钮在兄弟容器 .activity 内 —— 纯 CSS 没有「叔侄选择器」,:has() 也只能向下或向兄弟,无法直接互相定位。可行做法是把座位的父容器连同座位一起推到最右,并把两容器之间那段来自 trailing 的 gap 收掉:
div:has(> [data-mss-seat]) { margin-left: auto; }
div:has(> div > [data-mss-seat]):has(+ div [aria-label="开始录音"]),
div:has(> div > [data-mss-seat]):has(+ div [aria-label="Start recording"]) { column-gap: 0; }
div:has(> div > [data-mss-seat]) + div:has([aria-label="开始录音"]),
div:has(> div > [data-mss-seat]) + div:has([aria-label="Start recording"]) { margin-left: 0; }不用哈希类名:内核用 CSS Module,uV2eYG_trailing 这类类名随构建变化;aria-label 是文案契约、稳定。听写按钮的 aria-label 是二态的(开始录音 / Start recording),中英都写进选择器。
降级行为:浏览器不支持 :has() 时整段被丢弃,座位回到默认 gap:12px——只是「不够贴近」,不会错位或消失,故不需要 @supports 兜底。
② 修掉一个潜伏的真实 bug(官方模型按钮不再被隐藏)
原选择器 :has(+ div > [aria-label=…]) 用的是直接子元素匹配,而真实 DOM 里按钮嵌在 span 内 → 该规则永远不命中。已改为后代匹配 :has(+ div [aria-label=…])。
③ 修掉一个更隐蔽的 bug(CSS 模板串提前闭合)
MSS_CSS 是模板串,我在其前置注释里写了一个反引号,把字符串提前闭合,使 ${OFFICIAL_TRIG} 原样留在产物 CSS 里 → 选择器出现不存在的 $ 元素,官方模型按钮不再被隐藏、与插件座位同时出现。
症状极具迷惑性:.js 语法检查通过、页面不报错、规则只是静默失效。
修法:注释去掉反引号;含引号的选择器常量改为两半拼接(OFFICIAL_TRIG_A / OFFICIAL_TRIG_B),避免打破「模板串字面量里的 ${ 不算插值点」这一成对例外。
这条已升格为 AGENT.md 铁律 R7。
新增样式选择器回归钉 tools/check-model-seat-hug.mjs
14 条断言,零依赖(不引入 jsdom),手写 DOM 骨架忠实复制内核 InputBar 层次(含听写按钮被 span 包裹):
- 座位父容器拿到
margin-left:auto、两容器横缝收到 0、activity左边距清 0 - 中英两套听写文案均命中
- 不误伤:无座位时、听写按钮缺席时
- CSS 里不出现内核 CSS Module 哈希类名
- 官方模型按钮仍被隐藏
关键设计是「求值而非读源码文本」:读取 MSS_CSS 文本会让「官方按钮仍被隐藏」恒红;只有把模板串真正求值再匹配选择器,才能同时覆盖「模板串提前闭合」这类事故。
验证
| 指标 | 结果 |
|---|---|
check-model-seat-hug |
14/14 全绿 |
compat-check |
93/93 全绿 |
| 负向对照 | 人为破坏模板串插值 → 隐藏规则确实失效、断言变红 |
升级
github:qgx1992/dsh-ui-tools#v0.4.10
⚠️ 若你的 profilepackage.json里写的是旧的版本标签或 commit hash(如#v0.4.8),重装依赖/升级桌面版会把插件回退到旧版本——表现为「上次明明修好了,这次又复现」。请把 specifier 改成上面的发布标签并重装。
关于本 tag 与 main 的文档差异(v0.4.10 收尾说明)
本 tag 指向提交 4f039c5。此后 main 上还有两个纯文档/注释提交(d7e59c4、51e62fe),清理 v0.4.9 移除「修改的文件」选项卡后残留的引用:
README.md与demo/里指向已移除功能的过期描述(含删除demo/modified-files.html这个旧功能的静态预览页)- 源码注释里「抛错会连『修改的文件』一起挂掉」这类指向已删功能的因果句
tools/compat-check.mjs断言标签里未被重编号带上的旧功能编号
这些改动不含任何行为变化(lib/client.js 只改注释,断言数量与结果不变:check-model-seat-hug 14/14、compat-check 93/93),因此不重打 tag、不发新版本号——避免同一个版本号对应两份不同内容(v0.4.9 的 tag 事故正是这个成因)。
按本 tag 安装即功能完整,无需任何操作。
v0.4.8 — 加固功能六撤回防御(区分「读不到 context.current」与「确认未物化」),0.2.0-rc.1 纳入验证矩阵
针对 issue #1 的三项加固
issue #1 标题:v0.4.7 未修复功能六的撤回异常:DSH 0.2.0-rc.1 上 4 个 loader entry 被级联击落
核实结论(先说结论)
issue 的现象描述真实(确有撤回异常、确有同 bundle 级联击落),但归因不成立:
| 证据 | 事实 |
|---|---|
| 时间线 | 其 crash log 的 shell pid 14200 启动于 17:57,而我安装 v0.4.7 完成于 18:26 —— 两份日志记录的都是 v0.4.6 期间的运行 |
| 重启后 | 19:08 之后(已装 v0.4.7)的两次启动未再产生 crash log |
| 真内核验证 | 从 app.asar 提取 0.2.0-rc.1 的真实 dsh-client-ui-conversation,用其真装配器驱动插件 Definition:context.current 在该版本存在且语义未变,v0.4.7 通过 |
故 issue 假设的「context.current 在 0.2.0 上失效」被证伪。但 issue 指出的次要问题确实成立,本版逐项修掉。
① 修正矛盾注释
tspLiveDefinition 的 doc 仍写「turn/end 置 ended,buildViewNode 随即返回 null」,与 v0.4.7 新不变量(返回同 key + hidden)直接打架,会误导后续维护者 —— issue 这点说得对。
② 拆分防御分支(issue #1 的核心洞见,实为真问题)
原写法把「内核未提供 context.current」与「确认从未物化」合并成同一个 previous = null:
const previous = context.current !== void 0 && typeof context.current?.get === "function"
? (context.current.get("chat") ?? null)
: null; // ← 两种截然不同的情况塌缩成同一个值一旦内核换了字段形态(改名 / 去掉 / 换成不可读容器),防御会静默失效、异常照旧复现且不留痕迹。
现拆为两个判据:canReadCurrent(能否读到)与 previous(已知时上一帧节点)。读不到时不敢断言「未物化」,一律保守返回同 key + hidden,并只告警一次:
const canReadCurrent = currentStore != null && typeof currentStore.get === "function";
const previous = canReadCurrent ? (currentStore.get("chat") ?? null) : null;
if (!hasData && previous === null && canReadCurrent) return null; // 只有能确认才允许 null取舍:撤回异常的代价(整条 chat 快照链停更、后续提问全不显示、同 bundle 插件被级联击落)远大于多挂一帧隐藏节点。
③ 把 0.2.0-rc.1 纳入验证矩阵
- 0b 版本表补全:原只列 21 个,漏掉整条 0.1.7 / 0.2.0 线,使「两种 semver 语义判决一致」断言假绿(旧区间在这 6 个版本上确有分流)。现为 29 个已发布版本 + 3 个哨兵。
- B3 逐内核遍历:由「只测第一个内核」改为遍历全部可发现内核,并新增
app.asar来源 —— 桌面版大版本升级后真正生效的 client 包在 asar 内,kernels/下可能没有对应版本,不读 asar 就等于让最需要验证的那版内核缺席。 engines.dsh收紧:… || >=0.1.6-0→… || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0 || >=0.3.0-0(两处值一致)。旧写法对0.1.7-*/0.2.0-*预发布版在普通语义下全部漏判。
验证
| 指标 | 结果 |
|---|---|
| compat-check | 90 → 110 断言全绿 |
| B3 覆盖内核 | 5 个:0.1.7-rc.2 / 0.1.7-alpha.1 / 0.1.6-alpha.2 / 0.1.5-rc.2 / app.asar(0.2.0-rc.1),每个各 5 条断言 |
| 负向对照 | 防御分支退回合并写法 → FAIL(109/110),证明新断言真的抓得住 |
升级
github:qgx1992/dsh-ui-tools#v0.4.8
⚠️ 若你的 profilepackage.json里写的是旧的 commit hash(如#ba6787b…),任何一次重装依赖或升级桌面版都会把插件回退到那个旧版本 —— 表现为「上次修好了,这次又复现」。请把 specifier 改成上面的发布标签并重装。
v0.4.7 — 修复功能六导致回合结束后界面不再刷新、后续提问不显示
修复:功能六导致回合结束后界面不再刷新、后续提问不显示
症状:回合结束后界面停止刷新,之后每次发送的提问都不显示。
根因
功能六的「生成中估算条」是一个自建 chat 节点 Definition(token-speed-live)。
内核 ConversationNodeAssembler.buildTargetUpserts() 的判定是:
previous = context.current.get(target) // 上一帧已物化的节点
node = buildViewNode(...)
node === null && previous !== null -> 抛错
conversation Definition "token-speed-live" withdrew materialized target "chat";
return the same key with hidden visibility instead
该异常从 flush() 冒出后整条 chat 视图快照链停更。
0.1.6-alpha.2 与 0.1.7-alpha.1 均复现;0.1.6 起该校验即已存在,因此这不是新内核新增的行为,而是功能六自 v0.4.6 引入以来一直存在,在「本回合确实产生过可见输出」时触发。
关键难点
真机的流式增量是 transient 记录;回合收尾内核走 settleAssistant():先 retire 全部 transient,再 replayContext() 重放 —— 状态被倒推回 firstTokenAt=null / estTokens=0(并不是 ended=true)。
所以「state.ended / 无输出 → 返回 null」这种只看 state 的写法照样触发撤回异常。
修复
判据改用内核给的 context.current(上一帧已物化的节点):只要它非空就返回同 key 节点、仅切 visibility(有数据且未结束 → visible,否则 hidden);仅「从未物化且当前无数据」才返回 null(安全路径)。
为什么此前没测出来
- 真机流式的
transient+settleAssistant路径未被任何测试覆盖; - 原有断言只调
live.buildViewNode(...)纯函数、从不传context.current,等于绕过了内核唯一的撤回判定依据; - compat-check 用的是假内核,根本没实现该校验。
回归加固
新增 场景 B3「功能六 × 真内核 ConversationNodeAssembler」:从本机内核副本载入真内核装配器 + 真 bundle 截取的 Definition,按真机事件序列(durable 起止 + transient 增量 + settleAssistant)逐帧驱动,断言:
- 全程不抛 withdrew;
- 节点
visible↔hidden正确切换; - 多回合连续会话(后续提问)依旧刷新。
另补 4 条纯函数断言,显式传 context.current。
| 指标 | 结果 |
|---|---|
| compat-check | 78 → 89 断言全绿 |
| 负向对照 | 换回只看 state 的写法 → 8 条 FAIL(含真内核 settleAssistant 抛 withdrew、后续提问全程抛错、visible 帧数 3→1) |
| 真内核端到端 | 3 回合连续会话 0 异常,visible 帧 3 次 |
安装
# 本仓库以 github: 分发
dsh-ui-tools: github:qgx1992/dsh-ui-tools#v0.4.7
⚠️ 经github:装进 profile 后,node_modules/dsh-ui-tools/lib/client.js是 pnpm store 的硬链接副本,不是本仓库文件。手动改仓库代码后运行中的 GUI 仍加载旧 bundle,需按 AGENT.md §4 同步或重装 profile 依赖。
v0.4.6 — 新增功能六「输出速度计 tok/s」
v0.4.6 — 新增功能六「输出速度计 tok/s」
给回合输出速度补一个常驻读数,不用再点开「本轮用时和速度」弹窗。
两处显示
| 时机 | 位置 | 显示 |
|---|---|---|
| 回合结束后 | 官方「用时 xx秒」pill 左侧紧邻处 | ⚡42 tok/s(精确值) |
| 生成中 | 消息流末尾 | ⚡≈ 25 tok/s 生成中估算 |
数值口径
精确值与内核「本轮用时和速度」弹窗里的 TPS 完全同源:先按 messageId 定位回合,再折叠该回合全部已固化 assistant 节点,只累计同时带 timing.firstTokenTime 与 usage.outputTokens 的 step,按
ΣoutputTokens ÷ Σ(completedTime − firstTokenTime)
求值(对应内核 deriveTurnMetrics / assistantStepReading)。数据不足时不显示,绝不猜一个假值。
生成中只能给估算:内核在流式期间不上报 token 数 —— assistant/live-chunk 里的 usage chunk 被 publication: "none" 抑制,不推给客户端。故按已输出字符折算(CJK 1 字符 ≈ 1 token,其余 4 字符 ≈ 1 token),并始终带「≈」与「生成中估算」标注;回合结束即让位给精确 pill。
实现方式(依旧不改 DSH 任何一行源码)
- 精确 pill 注册进官方 list 槽
conversation.chat.assistant-actions,order: -10。该槽渲染在MessageIconActions的extraActions位 —— 与官方「用时」pill 同一条动作条,list 槽按order升序,负数 order 即紧贴其左侧。该槽另被官方dsh-client-ui-message-feedback使用(order: 10),list 槽为加性,无冲突。 - 生成中估算条 走官方 Definition 通道
ctx.uiConversation.events.register(...)(ui-goal、ui-workflow-run同款),自建token-speed-livechat 节点,并以该 kind 注册conversation.chat.node渲染器。- 为什么不能用
conversation.chat.turnTail:该槽的数据源必须匹配到turn/end才产出节点(tailData无turn/end即返回null),生成期间根本不存在;assistant-actions所在动作条同理。这一点在实现中实测确认。 - 节点排序锚点取大于任何真实事件 seq 的常量,使其稳定落在本回合末尾(锚在首个增量处会因
anchorSeq比较跑到流式正文上方)。 - 估算分母只累计相邻输出增量的间隔并对间隔设上限(
TSP_LIVE_GAP_CAP_MS),工具执行等无输出停顿不计入 —— 否则读数会随时间衰减到接近 0,看起来像掉速。
- 为什么不能用
- Definition 通道形状探测:不符时
console.warn并只让生成中那段缺席。实现过程中自测抓到一处真实缺陷:原先直接调用会让整个alphaFeatures子 fiber FAIL,把「修改的文件」一起拖下水;现已修复。
开关
设置页「DSH UI 工具」新增一行「在「用时」旁显示输出速度计(tok/s)」,默认开;偏好字段 tokenSpeedEnabled(同一个 localStorage key)。旧内核上该开关与功能三一同灰显。
已知边界(如实说明)
历史回合可能没有读数:firstTokenTime 来自流式增量(瞬态事件,不进持久化日志),因此重新加载页面后看到的历史回合通常缺该字段 —— 此时插件与内核「用时」弹窗都不显示 TPS(同一口径、同一结论)。在本回合刚跑完、未重载页面的情况下读数正常。
验收
npm run check(真实 cordis,0.1.6-alpha.2):78/78 全绿(v0.4.5 基线 49 条无回归;新增精确口径与边界、Definition 状态机全流程、两个组件的 DOM 与开关门控)。负向对照有效:把排序锚点改坏、或去掉 Definition 通道的形状守卫,各触发 FAIL。tools/live-smoke.mjs真机:22/22 通过,新增「pill 与官方『用时』在同一动作条内且位于其左侧」的位置契约断言,以及「与内核 TPS 口径一致」断言(内核有则必须有、内核无则必须无)。- 位置契约经真机实测确认(临时补丁强制渲染后量取 DOM),非推断。
- 内核兼容矩阵不变:新内核 6/6 功能;
0.1.1-rc.x上 4/6(功能三、六缺席,静默降级、无横幅)。
升级
# profile 内更新本插件(当前安装为固定 commit,需重装指向新版本)本包以 github:qgx1992/dsh-ui-tools 分发,不经 npm(npm 上 dsh-ui-tools 名字属于他人)。
完整改动:v0.4.5...v0.4.6
v0.4.5 — 新增机器可读内核约束 engines.dsh
v0.4.5 — 新增机器可读内核约束 engines.dsh,修正版本口径
此前本包只在 README 与 package.json 的 description 里用自然语言描述内核要求,没有任何字段可供工具读取。本次补上生态实际消费的机器可读字段,并修正一处在真实 semver 下不成立的版本说法。
声明内容
"engines": { "dsh": "^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0 || >=0.1.6-0" },
"dsh": { "engines": { "dsh": "^0.1.1-0 || ^0.1.2-0 || ^0.1.3-0 || ^0.1.5-0 || >=0.1.6-0" } }两处同值:dshmarket 的 manifestFacts() 读顶层 engines.dsh,缺失时回落 dsh.engines.dsh。
为什么是逐条发布线,而不是 >=0.1.2-alpha.1
semver 规定预发布版只与同一 x.y.z 元组比较,所以:
| 区间写法 | 0.1.1-rc.2 |
0.1.2-alpha.3 |
0.1.5-rc.2 |
|---|---|---|---|
>=0.1.2-alpha.1(旧 README 口径) |
✗ 漏 | ✓ | ✗ 漏 |
>=0.1.1-rc.1 |
✓ | ✗ 漏 | ✗ 漏 |
本次逐条 ^0.1.N-0 + >=0.1.6-0 |
✓ | ✓ | ✓ |
实测 >=0.1.2-alpha.1 并不匹配 0.1.5-rc.2(本仓库当前实测内核)——这正是要修的隐患。-0 后缀把每条线的下界取到该线最早的预发布,末项兜住后续新线且不硬顶未来内核(误判未来内核会让市场拒绝合法升级)。
验证
- compat-check 新增场景 0b(6 条断言,合计 49/49 全绿):用内核自带真实 semver 对 npm 上全部 21 个已发布内核版本 + 2 个未来哨兵逐项断言,并要求普通语义与市场侧
includePrerelease语义判决一致(不一致即失败)。 - 负向对照有效:把区间换回
>=0.1.2-alpha.1触发 4 条 FAIL。 - 端到端验证:用市场真实的
manifestFacts()+deriveHostCompatibility()从本package.json提取,0.1.1-rc.2/0.1.2-alpha.3/0.1.5-rc.1/0.1.5-rc.2/0.1.6-alpha.1全部判为compatible。 - 未改动任何运行期逻辑(
lib/client.js与 v0.4.4 逐字一致),内核兼容行为不变。
生效范围的如实说明
市场是按 npm 包名向 registry 取这份事实的。本包目前以 github:qgx1992/dsh-ui-tools 分发,该更新路径对 git 源不查 registry(usesNpmUpdateTarget = !restore && !isGit);且 dsh-ui-tools 这个 npm 名字属于他人。因此这条声明当下的作用是统一生态惯例、并为将来可能的 npm 发布备好,不构成本机 github 安装路径的运行时保险——旧内核自动降级仍由 alphaFeatures 子 fiber 实现。
安装:
dsh plugin --profile web add github:qgx1992/dsh-ui-tools