deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:fangwen9527/dsh-composer-ux
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
English summary — A DeepSeek Harness Web plugin that upgrades the composer (chat input) experience: configurable send / newline keys, a native-style 7-item right-click context menu, a resizable & scrollable settings panel with persisted size, a global on/off switch, a quick-command panel (built-in prompts, click-to-insert, per-item "always append on send"), a prompt optimizer that runs a separate model call before you send, automatic
x-opencode-sessionrequest-header injection so OpenCode (Go) routes work inside DSH, and — on Windows — a default-terminal switch that replaces the model's PowerShell tool with Git Bash (git-anchored discovery, WSL excluded, per-session tool-surface trim, official-identical sandbox/approval/timeout semantics), a composer stats-line upgrade that renders the built-in cache-hit percentage with three decimals (keeping upstream's "never round a partial hit up to 100%" rule), a cost capsule whose peak/off-peak prices you can edit per model in settings (empty field = official list price; peak/off-peak is decided by each usage event's own timestamp, not by when you look at the panel), plus a one-click "restart DSH" button in the settings-card header (market-proven self-restart: detached node helper, port-release wait, hidden-console relaunch, same-origin fence, boot-id reload). Install withdsh plugin --profile <name> add github:fangwen9527/dsh-composer-ux— the builtlib/ships in this repository, so there is no build step and no build authorization. License: MIT.
DeepSeek Harness Web 输入体验增强插件:

上图即 0.5.0 的设置页(六栏都开着)。注意标题行只有卡级开关与从属控件 —— 右键菜单那 7 个条目开关已按实测反馈搬进卡内的「自定义」档(见第 0 条)。
enabled && 该栏开关)。前六栏默认关;第 7 栏「统计行」默认开(它是唯一例外,理由见下条)。settingsScope.bind → configForms.get、宿主 settings.register 被删除、事件名换代),代码按能力探测分支,不维护两份产物。~/.dsh/settings.yaml:0.1.7 的官方升级会把它改名成 settings.yaml.imported 并逐节导入 profile patch(一次性、不可逆),而本插件这一段在旧版上导不进去(那时还没有 Config 字段表),旧值只会留在那个 .imported 文件里。false,而是按「设置文档里有没有『你在用』的痕迹」迁移 —— 碰过的栏保持开着、没碰过的才是关;全新安装(空白文档)才是六栏全关。「快捷指令」那一栏多一条文件判据(0.3.0 起条目存在 quick-prompts.json,设置文档里看不出来)。node test/check-sections.mjs 把它单独列一行说明。node test/check-sections.mjs 拿你真实的 settings.yaml 跑一遍迁移,打印七栏会变成什么(只读,不写任何文件)。@linxin666/dsh-web-all 的「Web 插件」页:顶部常显「中文名 + 内嵌英文包名 dsh-composer-ux 的一行描述 + 总开关」;其下八个栏目为可折叠卡片 —— 标题行左侧是「标题 + 一句动态概览 + 展开箭头」,右侧是这一栏的开关与从属控件(设置面板的缩放开关、默认终端的三档都在这里),长说明与其余控件在展开后的内容区;默认全部折叠、不记忆展开状态,可同时展开多个。标题行只放卡级开关与从属控件:右键菜单那 7 个条目开关按用户后来的反馈搬回了卡内(它们只对「自定义」档有意义,见下一条)。node test/settings-render.mjs:把设置页渲染成 HTML,断言三档正文互斥、那 7 行只在自定义档、标题行没有第二个入口)。.navList 就有 overflow-y: auto),所以 0.6.0 把「导航滚动」那个开关整项删掉了 —— 留着就是重复实现。x-opencode-session(详见下节)。# 安装
dsh plugin --profile web add dsh-composer-ux
# 更新到最新版
dsh plugin --profile web update dsh-composer-ux
npm 包:dsh-composer-ux —— 预构建产物,安装期不在本地执行任何代码,也不需要 pnpm 的构建授权。(--profile web 换成你自己的 profile 名即可。)
# 从 GitHub 安装(等价;lib/ 构建产物已提交,没有 prepare 脚本,因此不需要 pnpm 的构建授权)
dsh plugin --profile web add github:fangwen9527/dsh-composer-ux
# 锁定 commit 安装(更安全:后续推送无法悄悄改变实际运行的内容)
dsh plugin --profile web add github:fangwen9527/dsh-composer-ux#<commit-sha>
# 本地目录 / 源码开发(等价于 link;改完 src/ 先跑 node build.mjs)
dsh plugin --profile web add <你克隆或解压出来的目录>
本插件按官方「打包与安装插件」规范打包为可安装组合包(bundle):package.json 声明 dsh.bundle.patch → ./cordis.patch.yml,该层以包名插入插件行 dsh-composer-ux,装进 profile 后由 pnpm/Node 从 node_modules 解析到 lib/index.js。
装完按 DSH 提示重启一次(Host 半的插件代码只在进程启动时 import),客户端半刷新页面即生效。
发现渠道:仓库已打官方发现用的 dsh-plugin topic(另带 deepseek-harness / cordis / dsh / opencode 等关键词);社区目录(mydsh.dev、dshbase.com、dsplugin.app)按该 topic 自动同步收录。仓库里的 cordis.patch.yml 就是随包发布的组合层文件,dsh.bundle.patch 指向它即可。
从 DSH 源码检出直接 --patch 挂载的本地开发方式见下文「加载」一节。
[data-composer-input]);其他输入框不受影响。isComposing / keyCode 229)一律放行,绝不误发、误换行。/ @ 触发器菜单打开时 Enter 仍优先选择菜单项。设置 → 输入体验 → 右键菜单 里是三选一的菜单来源(只影响右键,不影响键位与其它功能):
| 浏览器 | 首次/之后 | 改或撤销授权 | 免掉弹窗的办法 |
|---|---|---|---|
| Chrome / Edge | 弹出后点「允许」即记住这个站点,之后不再问 | 地址栏最左的网站图标 → 网站设置(Edge 叫「此站点的权限」)→ 剪贴板;或 chrome://settings/content/clipboard(Edge 是 edge://settings/content/clipboard) |
不需要:允许一次即可 |
| Firefox | 每次点「粘贴」都会弹一个只有「粘贴(P)」一项的小窗(约 1 秒后才可点),点它才完成 | 无(没有站点授权面板) | 关不掉:这是 Firefox 的安全机制,网页不允许静默读剪贴板。不想多这一步就按 Ctrl+V,或把「菜单来源」切成「浏览器 / 官方」档 |
0.4.0 实测更正:早先文档里教过「改
about:config里某个剪贴板首选项就不弹窗」——那是错的,那个弹窗与任何首选项都无关(用户在 Firefox 上照做后弹窗照旧)。依据:MDN Clipboard API 的安全说明(读到不允许的内容时,浏览器弹的临时菜单里只有一个 Paste 项、约 1 秒后才可点)、caniuse(只有带clipboardRead权限的扩展不显示粘贴提示)。能免掉它的设置都属于「允许任何网站静默读剪贴板」那一类,本插件不教、也不建议改。
chrome:///edge://这些地址不能做成网页里的链接(浏览器禁止页面跳转到内部协议),只能手输或复制粘贴到地址栏。被拒绝授权或无响应时,菜单里会提示「请用 Ctrl+V 粘贴」。
自定义档的细节:
preventDefault 掉浏览器菜单,并和「浏览器」档一样挡住同一层里其它插件的捕获监听(否则两边会各弹一个菜单)。输入框工具行里、「展开」按钮的左侧有一个同款胶囊按钮「快捷指令」(槽位 conversation.input.right,order 89 < 官方「展开」的 90)。点开展开面板:
+ 新增一个分类);分类的改名 / 排序 / 删除,以及条目的增删改、上下移、恢复内置 9 条,都在 设置 → 输入体验 → 快捷指令清单 里。blank(这个会话还没有任何消息):发完第一条它自己就为 false,所以不需要插件自己记状态;刷新页面、切走再切回来都不会重复附加。withInsertMode 一次把两个标志写对。autoSend 与 autoSendFirst 都写成 true,按「每次」处理(每次插入本来就包含第一次),并在下次写盘时修正回互斥状态。rewrite/requirement/quality 必须附一段在你原话里逐字存在的引文;宿主逐条做字面比对,对不上就只丢那一条并记账。rewrite 按引文位置回填,没被覆盖的原话原样保留,其余条目按节追加在末尾。于是"替你发明一条你没说过的需求"在结构上做不到。N 条补全 · 丢弃 M 条 · 重试过一次 · 自定义提示词;模型没按条目契约输出时走整段照收的兜底并标注 未校验依据(保证改造不会让原本能用的优化变成失败),输出像条目信封但半截坏掉时直接失败、绝不把坏 JSON 写进输入框。快捷指令(分类 + 条目 + 每条的插入模式)存在 $DSH_HOME/quick-prompts.json(默认 ~/.dsh/quick-prompts.json):
fsync → rename」原子替换,并用 <file>.lock 串行化写入;写到一半断电不会留下半截 JSON,并发保存也不会互相截断。quick-prompts.json.bad-<时间戳> 再如实报错,绝不静默返回空列表。这一条是刻意的:若回退成「读不懂就当空列表」,用户下一次保存就会把空列表写回去,真数据被覆盖。categories / name / title / text / autoSend / order),同一个文件两边都读得懂。⚠️ 但不要同时装两个插件——同一个文件两个写者会互相覆盖(对方还是非原子写)。autoSendFirst: true(只在为真时写出来)。对方的插件会忽略它,并在它重新保存时丢掉这个键——也就是说「仅首次」在那边会退化成「关」。出网请求由宿主的模型适配器发出,浏览器侧碰不到模型路由;宿主半与客户端半之间也没有别的受支持通道。所以「优化提示词」是一次往返:浏览器 POST /composer-ux/optimize → 宿主用 ctx.get('llm').stream(...) 独立跑一次模型调用 → 把装配好的正文回给浏览器填进输入框。这条路径与 WestFox-AwA/dsh-prompt-optimizer 同构。
提示词的来历(如实写):0.5.x 那三档是逐字提取自它 0.5 线的 lib/index.js(BSD-3-Clause,作者「啃轮胎的西狐」)。0.6.0 起机制与提示词都改按它 0.6 线(po06/lib/interpreter.js 的 SYSTEM_PROMPT + validateProvenance、po06/lib/compiler.js 的固定节序与预算丢弃)重写,不再是逐字提取 —— 那些文件里没有现成可抄的"三档改写提示词",能借的是机制本身:条目化产出、逐字引文、只丢单条、降级出声。落在 src/optimizer-prompt.ts(提示词)与 src/optimizer-assemble.ts(校验 + 装配)两个文件,署名与来源说明保留在各个文件头。
优化用的模型跟随你当前的默认模型(agentDefaultModel.currentSelection()),不额外配置;每次优化会花一次模型调用,但不占对话轮次、不进会话历史。
Enter 那一路沿用既有的合成 Enter 回放;官方发送按钮那一路在捕获阶段认下点击、先把附加内容写回编辑器、再用同一个按钮重放一次点击。这样官方对「发送 / 排队 / 打断」的判定原样生效,本插件不做第二套提交语义。发送键与停止键共用同一个位置,靠图形区分——停止渲染 <rect>(方块),发送渲染 <path>(箭头),与界面文案、语言无关。
OpenCode 的接口要求客户端在每次请求里带上一个稳定的会话 ID 请求头(官方 Go 文档「可以在哪里使用?」第 3 条:为每段对话在 x-opencode-session 中发送会话 ID,以便其优化路由与提示词缓存)。DSH 的 Models 设置页明确不提供请求头编辑器(源码注释与 README.zh.md 都写明 profile headers 属于部署配置),所以本插件把这一项代办了。
llm-pi-ai 的 provider profile —— providers.<路由>.headers.<头名>。这是 DSH 里唯一受支持的出网请求头入口:它作为 pi-ai 的 optionsHeaders 最后合并(能覆盖默认头),且该适配器每次请求都重读配置,所以改完下一次请求即生效,不用重启、不用手工改 settings.yaml。settings.mutate 的路径寻址——只动我们那一个键,绝不重述或删除你写在同一个 profile 里的其它字段(models / apiKeyEnv / 其它 headers 都不碰)。models 是必填项,凭空造一个只有 headers 的路由会让整份配置校验失败,所以只写已存在的路由。判据:opencode 开头(DSH 内置的 opencode-go 走这条,它的 baseURL 由 pi-ai 目录内置、配置里读不到),或该路由的 baseURL 主机是 opencode.ai(含子域)。后一条是为自建别名路由准备的:例如把 OpenCode 端点配成 go: { baseURL: https://opencode.ai/zen/go/v1 },名字里没有 opencode 也能被认出来。https://opencode.ai.evil.example/v1 这类仿冒主机不会被匹配。llm/stream 钩子被设计成只能读不能改,GenerateOptions 里没有 headers 字段),所以这里退一步用固定值。已知代价:所有对话挤同一个上游(没有负载分散);单段对话内的缓存命中不受影响。另外它不保证缓存一定命中(还取决于上游模型与网关策略)。new Headers())校验头名与头值;非法值被拒绝,原因写进栏目的「状态」行,而不是把整条路由弄坏。MissingSessionID:"Request is missing x-opencode-session and cannot be routed efficiently." —— 没有它,DSH 里的 opencode-go 完全不可用。input 174 → 46、cacheRead 192 → 320(总前缀 366 不变),第二次有更多内容直接命中缓存。test/opencode-header-wire-probe.mjs 起一个本地端点,DSH 发过去的推理请求上确实带着 x-opencode-session: <值>,以及它自己的 user-agent: deepseek-harness/0.1.5-rc.2 (+https://github.com/deepseek-ai/deepseek-harness)(正好满足 OpenCode 文档对客户端标识的第 2 条要求)。Windows 上 DSH 给模型的终端工具是 PowerShell(工具名 pwsh),而模型的训练语料里 bash 占绝对多数。这一栏把终端换成 Git Bash:模型看到的工具就叫 bash,pwsh 从它的工具列表里消失 —— 一个会话只面对一个终端工具。
自动(探测到 Git Bash 就用,找不到就保持 PowerShell —— 默认,开箱即用)/ Git Bash(强制换,没探测到会回落并在状态行说明)/ PowerShell(保持 DSH 默认,本插件完全不介入终端)。git.exe 反推同一个安装根 —— 本机就是这样命中 D:\Git 的);Program Files\Git、Program Files (x86)\Git、%ProgramW6432%\Git、%LOCALAPPDATA%\Programs\Git(安装器以普通用户身份运行时默认落这里,以管理员运行才落 Program Files);%SCOOP%\apps\git\current)与 Chocolatey 便携包(…\chocolatey\lib\git.portable\tools);%LOCALAPPDATA%\GitHubDesktop\app-*\resources\app\git)、旧 GitHub for Windows 的 PortableGit(%LOCALAPPDATA%\GitHub\PortableGit_*)、Visual Studio 内嵌(…\Microsoft Visual Studio\<年份>\<版本>\Common7\IDE\…\Team Explorer\Git)—— 这三处带版本号,靠"列一眼子目录"枚举(新的排前面);C:\msys64\usr\bin)、Cygwin(C:\cygwin64\bin);Git / PortableGit / msys64 / cygwin64(覆盖装在 D:\Git、D:\PortableGit、自己解压到任意盘的情况);%LOCALAPPDATA%\Programs\Niubash\niu.exe);bash.exe 很可能就是 WSL 的启动器,见下条)。bin\bash.exe 又有 usr\bin\bash.exe(或 mingw64\bin\bash.exe),只列前者。本机实测两者的差别:前者会给出 MSYSTEM=MINGW64、把 PATH 前置成 /mingw64/bin:/usr/bin,于是 head / grep / uname 都在、中文文件名当参数也正常;后者 MSYSTEM 为空、PATH 只有继承来的 Windows PATH(里面只有 D:\Git\cmd),coreutils 全部 command not found —— 交给模型就是命令大面积失败。没有 bin\bash.exe 兄弟的来源(例如 MSYS2 只提供 usr\bin\bash.exe)照常列出;你自己手填的路径不受这条限制(那是你的选择)。Program Files(本机就在 D:\Git)。只按固定目录找会一边找不到、一边退到 PATH,而 Windows 上 PATH 里的 bash.exe 很可能就是 WSL 的启动器。C:\Windows\System32\bash.exe 与 …\Microsoft\WindowsApps\bash.exe 一律不用 —— 它把 D:\x 解释成 /mnt/d/x,与模型手里的 Windows 路径、工作目录、%TEMP% 全都不兼容。只有在它确实存在时,卡片的状态行才会提一句「已排除 N 个 WSL 的 bash.exe」,不会在没装 WSL 的机器上凭空报警。agent/created(新会话)与设置变更时遍历所有在跑会话重新下发,不需要开新会话;切回 PowerShell 会把之前下发的限制撤销。[stderr] 分段、(no output) 兜底、标记顺序(沙箱拒绝 → 升级提示 → 超时 → [killed by signal: X] 或 [exit code: N] 在最末)、终端卡片(exit 状态拆成 pill,可点开看命令 / cwd / 输出)、后台任务(run_in_background + job_output / job_kill)、超时(默认 120s、上限 600s)、输出截断并把完整输出落盘、沙箱约束与 sandbox_permissions 升级审批。非零退出不是错误,只是末尾一个标记。subprocess、某个会话本来就看不到 pwsh(此时官方 restrict 会拒绝)、非 Windows —— 一律只降级并在状态行写明原因,不抛错、不 veto 别的插件、不影响会话本身。bash 重试即可(卡片上也写了这句)。terminal_* 六件套)模型侧除了上面那个一次性的 bash 工具,官方还有一套会话跨调用存活的持久终端工具:terminal_open / terminal_read / terminal_send / terminal_signal / terminal_close / terminal_list(REPL、dev server、需要交互输入的程序靠它们)。这一档本插件常驻挂载,不需要任何设置项。
@deepseek-ai/dsh-tool-terminal 官方标为可选(packages/terminal/tool-terminal/README.zh.md:「需要选择启用」),没有任何 bundle 默认挂它;而且 0.1.7-rc.2 的桌面版根本没随包发布它 —— app.asar 的 284 个 @deepseek-ai 包里有 dsh-terminal、dsh-terminal-bash、dsh-api-terminal-controller、dsh-client-ui-sidebar-terminal,唯独没有 dsh-tool-terminal(全盘无路径痕迹)。所以本插件把它写成自己的依赖(package.json 的 dependencies,区间 >=0.1.7-rc.1 <0.1.8),再由 cordis.patch.yml 里一行挂上。cordis.patch.yml 里插三行 —— ① @deepseek-ai/dsh-terminal(提供 ctx.terminals 服务)② @deepseek-ai/dsh-terminal-bash(注册 type=shell 的 PTY 后端)③ @deepseek-ai/dsh-tool-terminal(那 6 个模型工具)。缺一行都不行,而且必须在同一层。const inject = ["terminals", "tools", "systemPrompt"],而 ① 在官方组合里只出现在 sdk-minimal 与 web-app 的 minimal 预设里(packages/bundle/web-app/presets/minimal.patch.yml 的 persistent-shell 组),standard 预设和 profile 层都没有 —— 于是 ③ 在 desktop profile 这层静默 pending:不报错、不白屏、一个工具都不注册,只有模型工具表里空着。(这跟 0.6.2 白屏是同一形状,只是那次发生在客户端半、被启动审计抓住。)「包能解析到 + 守卫放行」≠「它激活了」——这条教训写在 cordis.patch.yml 顶部。cordis:group + isolate: { terminals: true } 给每个 agent 一份服务;我们放 profile 层是一份共享服务,会话归属由 API 自己保证(spawn(owner, …) 按 owner 隔离、工具里 requireAgent(exec.agent)),功能等价。而且 profile 层注册的模型工具能被 agent 看见 —— 同层的 @changfenhuang/dsh-genui 就是这么把 render_ui 送进工具表的。@deepseek-ai/dsh-tool-terminal 在随包发布的那份 app.asar 里不存在(dsh-terminal、dsh-terminal-bash、dsh-api-terminal-controller、dsh-client-ui-sidebar-terminal 都在,唯独没有它;dsh-web-app 的依赖里也没声明 ⇒ 从没被安装过),所以写成插件自己的依赖(package.json 的 dependencies,区间 >=0.1.7-rc.1 <0.1.8 || >=0.2.0-rc.1 <0.3.0 —— 两个窗口各带 pre 比较器,否则 0.2.0-rc.1 这种 pre 版本用普通区间匹配不上)。① ② 则是安装目录里就有的官方包,按包名引用即可解析。plugin-compatibility.ts)拿「包的 @deepseek-ai/dsh-* peer 区间 vs 运行时版本」比,不满足就直接给这一行打 disabled(patch 里看不出来,plugin_manager list_plugins 里是 enabled:false / fiberPhase:null)。而桌面壳自称的版本不是运行时版本 —— 读 app.asar 里 @deepseek-ai/dsh-app-boot 的 version 才是(本机实测:壳写 0.1.7-rc.2,官方包全是 0.2.0-rc.1)。本机因此一度整行被静默判掉,换装 dsh-tool-terminal@0.2.0-rc.1 后立刻生效。C:\Windows\System32\bash.exe 与 …\Microsoft\WindowsApps\bash.exe 是 WSL 启动器,会把 D:\x 解释成 /mnt/d/x,与模型手里的 Windows 路径/工作目录全不兼容(上面「默认终端」栏也是硬排除它的)。探测里用 path.basename/dirname 判掉这两种;真机实测:排除前 PTY 起成 WSL bash(pwd = /mnt/d/1zcode/dsh插件),排除后是 Git Bash(uname -a = MINGW64_NT-… Msys、pwd = /d/1zcode/dsh插件、grep/head 都在 /usr/bin)。!!js 自检:解析不到就 disabled: true,而加载器根本不会初始化 disabled 行,于是「缺包」退化成「这个功能不存在」。① ② 不能加同一个守卫 —— 守卫用 profile 目录的 createRequire,解析不到共享层里的包,加了会把服务永远禁掉。守卫的基准取 DSH_HOME(有就用)否则 os.homedir() + '/.dsh',再扫 profiles/node_modules 与 profiles/*/node_modules;ctx.get('profileContext') / DSH_PROFILE_DIR 只当额外候选且各自 try/catch —— 不依赖 DSH 内部上下文更稳(更正一句:先前我把该行不出现归因于"基准取不到",后来证明真因是上面那条版本不匹配;基准改成这样属于防御性加固,不是那个 bug 的修复)。另两个坑:表达式必须用 try/catch 包住(disabled 抛错算「条目失败」,一样白屏);③ 不能带 group: true(会迫使加载器初始化 disabled 行)。shellPath: '/bin/bash'(dsh-terminal-bash/src/config.ts:54,Windows 上解析不到),所以 ② 用同步 !!js 探测(PATH 上那份 git 反推 …/Git/bin/bash.exe,加 Program Files / LOCALAPPDATA / msys64 等落点);探不到就让 shellPath 留空并回落官方 pwsh 方言(resolvePwshPath)——与插件「找不到 bash 就保持 PowerShell」的策略一致。timeoutMs 对齐官方 minimal 预设的 300000。dsh-tool-terminal 的 peer 精确锁 0.1.7-rc.2(7 个官方包)。DSH 换大版本后,要么把依赖区间放宽到新窗口,要么让它被预检禁掉 —— 后者只是这 6 个工具静默消失,不会白屏。test/terminal-mount.mjs 有一条断言盯着这个窗口。PowerShell(插件完全不介入终端)时,这 6 个工具照常在。tool.call.toolview 渲染位(运行时 Slot 里可见),工具一亮就有官方样式的卡片。输入框下方那行用量统计里的「缓存命中 12%」,官方给的是整数;这一栏把它改成三位小数(缓存命中 12.346%)。
设置页位置:设置 → 输入体验 → 「统计行」(0.7.0 新增的第 7 栏,唯一默认开的栏)。只有一个开关(标题行上那一个)—— 这一栏只有一件事可开可关,所以不另设子开关(与「OpenCode 请求头」那一栏同一处理)。
为什么会有这一栏:社区插件 dsh-cache-precision 提供了这条路子,用户看到后要求把同样的能力并进本插件。能力移植、实现没有照抄 —— 下面每条都与它不同,且各有测试盯着。
数字口径与官方完全一致:缓存读 ÷(未缓存输入 + 缓存读 + 缓存写)(三个桶互不重叠,与官方 billedInputTokens() 同一份)。所以屏幕上那两个百分比不会互相打架。
不撒谎(最重要的一条):官方的整数档有一条很少人注意的规则 —— 当整数四舍五入会把「没满」显示成 100% 时,它会自动多给几位(例如 99.95%),源码注释原话是 "without rounding a partial hit to 100%"。本插件把默认档抬到三位后保留了这条规则:三位小数一旦会凑成 100.000% 就继续加位。阶梯如下:
| 真实命中率 | 官方(整数档) | 本插件(三位档) | 直接 toFixed(3)(不采纳) |
|---|---|---|---|
| 12.3456% | 12% |
12.346% |
12.346% |
| 99.995% | 100% |
99.995% |
99.995% |
| 99.999% | 100% |
99.999% |
99.999% |
| 99.9995% | 99.9995% |
99.9995% |
100.000%(把没满说成满) |
| 真的满命中 | 100% |
100.000% |
100.000% |
test/stats-line.mjs 把 DSH 的
packages/client/ui-chat/src/client/chat/token-format.ts(rc.1)那 4 个函数原样抄进来,
对 400 个分母全量 + 大分母边界 + 3000 组伪随机比对 digits = 0 档,逐例一致。只改输入框下面这一行(用户明确选择的范围):点开统计行的弹窗、每轮用量弹窗里的百分比保持官方原样
—— 那两个弹窗里数字是独立的 <dd>12.3%</dd>,与"整段必须就是 缓存命中 xx%"这条判据天然不冲突
(有 5 条"不许碰"的断言盯着)。顺带一句:弹窗自己用的是官方 1 位小数档,所以会有"下面 12.346%、弹窗 12.3%"的口径差,这是选择而不是漏改。
同步无障碍名字:官方那颗胶囊的 aria-label 是 `${总数} · 缓存命中 12%`。只改可见文字的话,
读屏用户听到的还是整数,所以两处一起改(社区插件没管这一处)。
定位与作用面:靠官方稳定属性 [data-composer-stats](StatsPills 的根元素),不靠 CSS Modules 类名(那串带哈希,升级就会变)。
display:none 的锚点落在同一个 composer dock 里,由它往上找"同时装着我和统计行"的最近祖先(上限 6 层),MutationObserver 只盯这一小块。页面上有多个 composer 时也只动自己那一个。document.body 上跑 TreeWalker + 观察器,等于给整个页面挂监听(每个流式 token 都会喂它)。本插件宁可功能不生效,也不留一个全页监听。只改文本,不碰那一行的样式。参考实现还给那一行写行内 max-width 想放宽 260px,没有移植 —— 它的前提在 DSH 0.1.7 上不成立:--dsh-chat-content-width 只作用在消息列与输入卡片上(.card / .composerHero),而真正包着统计行的 .dock / .composerStack 没有宽度上限;.composerHero 又只在空白会话生效,统计行却只在活动会话里渲染,两者永不同时出现。所以那一行本来就能比聊天列宽得多,而参考实现那个上限反而比可用宽度小 —— 平时不生效、内容极长时还会比官方更早截断。完整证据链与撤掉的理由见 src/client/stats-line.ts 文件头与 CHANGELOG 的 [0.7.0] 节。
关掉会怎样:把官方那一版原样写回去(三位小数退回官方整数档),不留半截状态。
护栏:node test/stats-line.mjs(52 项)盯小数语义、两个字符串变换与"不许碰"的范围;node test/stats-dom.mjs(17 项)用按官方源码复刻的假 DOM 盯定位与改写范围(这两件坏事都是静默的),并断言"不写任何行内样式";两条都已进 npm test。
test/mutation-guards.mjs 的 AF–AM 八条变异盯"退化成 toFixed(3) / 判据放宽 / 默认改成关 / 扫全页 / 不同步 aria-label / 去掉层数上限 / 撤掉的加宽长回来 / 加宽常量重新引入"这八种退化。
deepseek-flash、deepseek-v4-pro 两行常显,官方现役就这两个;deepseek-v4-flash 等旧名是别名,不单列),非 DeepSeek 模型只有一档「平坦价」(它们没有峰谷概念,摆两档会让人以为谷价时段还能再省一半)。下面一行「再加一个模型」可以填任意模型名(中转/自建路由的名字),同名模型在不同渠道价不同时写成 provider:model(例如 opencode:gpt-5.6-luna)。1,02、2元、-1 会留在框里标红(parsePriceText 三态),不写进设置 —— 把它当"清空"的话,价格会悄悄回到官方价,而用户以为改成功了。¥0.42,与官方那行统计同排、排在其后,order 100);点开后是贴在它上方的浮层 —— 各 route 的 token 与花费、合计、高峰档 / 空闲档各自的小计、三分项(未缓存输入 / 缓存命中 / 输出)各自的 token 数与花费、缓存命中率、两档生效单价、模型名与刊例价核对日期,并注明"按官方刊例价估算、未含中转加价"。StatsPills.module.css 里 .root 的两条(calc(var(--dsh-content-font-size-secondary, 13px) - 1px) 与 calc(20px + var(--dsh-content-font-delta-secondary, 0px))),亮度取 .pill 的"静止 label-tertiary、hover/展开 label-secondary"。坑:那两条在 .root 上,不在 .pill 上 —— .pill 里的 font: inherit 只是为了抵消 button 的 UA 字体;本条目是同一槽位里的另一条记录,不在那颗 .root 里,只写 font: inherit 会继承输入框那一层(默认 14px),真机上看就是"插件输入框下方的字比官方的大"(2026-09-28 用户反馈)。所以这里显式写 .root 的两条,并不用 font: 简写(简写会把字号重置回继承)。document.body,位置由胶囊的视口矩形算出,点别处或按 Esc 关闭。未缓存输入 × miss价 + 缓存命中 × hit价 + 输出 × out价(每 1M tokens)。cacheWriteTokens 不单独计价(官方价表只有"缓存命中/未命中"两行输入价,且实测本机投影里恒为 0,所以它只进"计费输入"的显示、不进费用)。峰谷按每笔用量真正发生的时间判定,高峰价是空闲价的 2 倍;用户覆盖价由设置页「金额」栏提供。legacy 峰谷制之前 / peak-2026-08 / flash-2026-09-10),折叠时给每条用量打上那一刻的档位 id,route key 因此多一维:同一模型在调价前后是两条分列,各按当时的价结算。Pro 那次没调价(腾讯云公告只列了 Flash 两个型号,官方页今天仍单列 Pro 价),所以它两档同值。time 判档、按 (provider, model, 档位, 价格档) 拆桶,同一步的替换增量沿用该步第一次判定的档(否则一次跨过 09:00 的请求会被拆成两档、凭空多出一个"高峰用量");事件既没有 request/header 时间、自己也没有 time 时,判定函数收到 NaN,宿主半退回"现在"(=旧行为),绝不静默判成空闲档。空闲档 · 2 小时 15 分钟后转高峰,因节假日/周末而谷价时会写明原因),胶囊的 title 也带上;设置里可调提前几分钟(1–60,默认 5)、进峰前/离峰前分别提不提醒,以及要不要额外发浏览器系统通知(需要授权;去重放在模块级按切换点记 —— 放组件里会因为设置一变、组件重挂而连发几条同样的通知)。相位规则与宿主半同源(节假日表由 /composer-ux/usage 回给客户端),不会出现"胶囊说还有 3 分钟进峰、面板说不是"。GET https://api.deepseek.com/user/balance。API Key 只在宿主半读:从 llm-deepseek 那一行的 apiKeyEnv(默认 DEEPSEEK_API_KEY)经 credentials.resolve() 取,与官方适配器同一条路,浏览器拿不到。安全底线:端点白名单只放行 api.deepseek.com(拒子域名、伪装域、http://、非 443 端口),baseURL 被指向第三方时一个请求都不发、也不把 baseURL 原文回给浏览器(只回主机名)。余额总额用分项相加(granted + topped_up,不用平台自己的 total_balance);拿不准就不显示(不显示 0、也不显示旧值)。test/official-pricing.mjs,45 条)。api.json(5.2 MB、215 个 provider、7831 个模型),压成约 450 KB 落 $DSH_HOME/storages/composer-ux/prices.json(设置文档里只留"什么时候同步的、多少条",不然 settings.yaml 要被撑大)。models.dev 里的 DeepSeek 行整块丢掉 —— 它只有平坦的谷价、没有峰谷与历史档语义,留着迟早被谁误用成"DeepSeek 单价",结果是所有峰价被静默算成谷价。autoSyncDue 判定:开关不是真 true、时间读不到、距上次成功不到一天 —— 都不发请求。失败只写日志,界面仍显示上次成功的时间。settings/document-updated 只在 describe() 里比对 raw 变化后发出,而我们自己 mutate 写设置时并不调 describe() —— 设置页开着时会被界面刷新顺带触发,设置页没开着(例如后台的自动同步跑完)就不会发;而折叠缓存按 seq 去重、改规则不会重折旧事件,金额会静静地停在旧价格档上、界面上看不出。所以宿主半加了模块级 moneyInvalidators 注册表:用量路由登记"重读规则 + 作废价目缓存 + 作废折叠缓存",两条同步路径写完设置直接调 invalidateMoney(),事件只作补充。src/provider-prices.ts(生成的,别手改:node scripts/gen-provider-prices.mjs <models.dev-api.json>)收了 11 个 provider / 346 个模型,快照日期写在文件头。没点过同步时就是非 DeepSeek 模型的兜底价;点过一次「同步第三方价目」后最新数据会盖住同名条目,界面分别标明"内置快照价"还是已同步。查价优先级:用户覆盖价 > 已同步价目 > 内置快照 > 未定价。(provider, model) 查价 —— 精确 provider → PROVIDER_ALIASES 别名(deepseek-official→deepseek、kimi-coding→moonshotai…)→ 按模型 id 全局唯一匹配(glm-5 在两家都有的价时拒绝猜)。认不出价就说"未定价"(单价全 0,明细页写明"去同步第三方价目或给这行填个价"),不再把 DeepSeek 的 flash 价静默套到第三方模型头上 —— 编一个看着合理的假数字,比承认不知道更糟。tokenUsage 投影只有 token 桶,全库没有一处把"钱"送到客户端;llm-pi-ai 里那个 cost 只活在 provider 内部,而且用户自定义的路由(profile 里手写的 provider)拿到的是 NO_COST(全 0)。所以费用只能由本插件按刊例价算。tokenUsage 投影(与官方统计行同一份),模型读 modelSelection 投影(来自 request/header 事件)。会话中途换过模型时按最后一次请求的模型计价(近似,浮层里写明)。GET /composer-ux/usage?sessionId=…(src/host.ts 注册):首次(或发现 seq 落后)用官方 sessionQuery.readSession(sessionId) 完整读一次,之后靠 session/event 订阅增量喂折叠缓存(createUsageCache,纯逻辑在 src/usage-fold.ts)—— 所以胶囊取价是 O(1),不必反复重读整份日志。播种窗口(读日志期间追加的事件)会先缓冲、读完按 seq 补上:不这么做的话水位会跳过快照里那些中间事件,金额永久少算(feed() 的 seq 幂等,补也补不进去)。事件形状按本版官方 SessionEventMap 的真实声明(assistant/attempt 的 stream 里取最后一条 usage 块,或 assistant/message 的 data.usage;本版没有 assistant/chunk 事件),口径对齐官方 token-meter 的 usage-projection(同一步替换、llm/retry-started 重开替换槽)。test/usage-fold.mjs(64 项)逐条钉住差分、归属、重试、脏数据、逐笔分档与增量缓存;分列出不来时面板会显示"读了 N 条事件、M 条 usage、来源哪条路",一眼就能定位。src/client/session-cost.ts):最短 600ms 间隔、期间有变化排一次尾随请求(保证流结束后的数字是准的)、同一时刻只有一个请求在飞、旧响应按序号丢弃。宿主半那份还没到手、或会话日志与投影对不上时,退回本地的"按当前档位估算"并在面板里说明是哪种口径 —— 胶囊从不空着。agreesWithProjection 是精确比较)时才把分列当权威,此时分列算出的命中率与官方胶囊逐位相同;否则退回投影口径。两个坑:客户端投影的未缓存输入叫 uncachedInputTokens、日志里叫 inputTokens —— 第一版在 billedInputTokens() 里认对了名字,却在调用处传了 inputTokens,于是分母丢掉整块未缓存输入、命中率恒 100%(旁边官方胶囊 98.206%)。现在调用处也走官方键名,test/client-registration.mjs 同时钉住"调用处键名"与"与输入框下面那一行同一套函数"。(provider, model)(0.10.0 起):DeepSeek 路由(provider 名含 deepseek 或模型名以 deepseek 开头)走官方价目表与历史档;其它 provider 走同步来的第三方价目,认不出就写"未定价"。所以用 OpenCode(Zen)之类的接口跑 DeepSeek 模型,用量仍按 DeepSeek 官方价进这个金额;跑非 DeepSeek 模型则按 models.dev 的价估算。代价如浮层所写:Zen 可能有自己的加价/订阅,这个数字不等于你付给 Zen 的钱。dsh-plugin-usage-meter 1.9.1(MIT,Copyright (c) 2026 fancr-code);0.10.0 起人民币列改用官方人民币页原值,历史档 / 节假日表 / 周末生效点与 dsh-cost-meter 1.7.44 交叉核对过。node test/pricing.mjs 把那份实现的几个函数原样抄进来当基准逐样本对拍:刊例价表逐项、峰谷判定(含节假日与周末边界)、定价解析(模型 × 币种 × 时段 × 覆盖价)、费用、金额、Token 格式,外加历史档与"节假日对金额的影响"两节(同一笔用量在国庆当天与普通工作日相差恰好 2 倍)。connection.requestRejection(request)(Host/Origin 围栏 + 浏览器登录令牌)。webServer 是可以绑 0.0.0.0 的,漏掉它等于把会话用量、设置写入与账号余额摊给同网段任何一台机器 —— 2026-09-29 评审时发现并补上(此前只有重启路由挂了这道关卡)。0.1 显示成 ¥0.1、把 0 显示成 ¥0.);② 覆盖价的币种语义——出处是"有覆盖价时先整体重建人民币档、美元再从人民币折算",写成"在美元列上套覆盖价再折算"会差一个汇率(≈15 倍)而屏幕上只是个数字,所以专门有一条断言守着。装了新插件、改了宿主半代码(比如本插件的「默认终端」、键位 schema)之后,DSH 需要重启才会加载新代码。设置页「输入体验」卡片抬头右端(GitHub 链接左边)有一枚「重启 DSH」。
机制照搬插件市场 dsh-market(它的 src/restart.ts 里挂着一串 issue 号,每条都是"重启按钮按下去没用"的具体死法):
node -e <源码>,detached + unref),宿主自己 500ms 后退出(延迟是为了让这个 HTTP 响应先发出去)。connect 探一次,最多 30 秒,通了再等 300ms(Windows 的 TIME_WAIT 尾巴)。固定 sleep 会让新宿主 EADDRINUSE 当场死掉。detached = DETACHED_PROCESS = 没有控制台,新宿主之后起的每个控制台子进程都会弹一个黑窗口;powershell -NoProfile -WindowStyle Hidden 给它一个隐藏控制台,助手那层再带 windowsHide。boot 号判断成功:每 1.5 秒问一次状态,号变了(说明新进程接管了端口)就 location.reload();60 秒还没变才报超时,并把日志路径告诉你。其它几点:
<系统临时目录>/composer-ux-restart-<时间戳>.out.log|err.log(失败时界面会把路径显示出来)。connection.requestRejection(Host/Origin 围栏 + 浏览器令牌),再过本插件自己的"回环 peer + 无转发头 + Origin 与 Host 同源"——这是"杀进程"的接口,跨站页面一定带自己的 Origin,挡在这里。--inspect / inspector.url()),或者它在 systemd 下当服务跑(重启权归 supervisor,自己重启会把 cgroup 里的接管进程一起收掉)。这时按钮禁用并说明原因。process.emit('SIGTERM') 而不是 process.kill(pid,'SIGTERM'):DSH 在 apps/cli/src/profile-boot.ts 注册了 SIGTERM handler(先 fiber.dispose() 再退出,自带 5 秒上限)。而 Windows 上 process.kill 等价于 TerminateProcess —— 本机实测 handler 一次都跑不到。兜底:10 秒后还活着就 exit(0)。仓库根目录(克隆或解压出来即是):
dsh-composer-ux/
├── package.json # dsh.client 清单(platform: web)+ exports["./client"] + dsh.bundle.patch 清单
├── cordis.patch.yml # 组合包层(随包发布):按包名 dsh-composer-ux 插入插件行
├── cordis.dev.patch.yml # 本地开发覆盖层(file:/// 绝对路径,已 gitignore,不随包发布)
├── CHANGELOG.md # 版本更新日志
├── build.mjs # esbuild 构建:lib/index.js(Host)+ lib/client.js(浏览器),并做发行后处理
├── scripts/
│ ├── gen-provider-prices.mjs # 从 models.dev 的 api.json 生成 src/provider-prices.ts(内置第三方价目快照)
│ └── live-smoke.mjs # 手动联网复核:官方页 / models.dev / 余额端点(只读;不进 npm test)
├── src/
│ ├── host.ts # Host 半:settings namespace + 请求头镜像 + 「默认终端」+ 金额三条路由(用量/价目同步/余额)+ 「重启 DSH」
│ ├── settings-contract.ts # 字段/默认值/菜单元数据 + 栏开关的迁移判据(零依赖共享)
│ ├── restart.ts # 「重启 DSH」:助手进程/等端口/隐藏控制台/信任关卡/优雅退出(零 import)
│ ├── pricing.ts # 金额:历史价档 / 节假日与峰谷判定 / provider 感知定价 / 覆盖价 / 费用与格式(零 import,两半共用)
│ ├── usage-fold.ts # 金额:会话事件按 (provider, model, 峰谷档, 价格档) 归因折叠 + 增量缓存(零 import,宿主半用)
│ ├── official-pricing.ts # 金额:官方价格页 HTML → 人民币/美元两列价(真页面夹具逐项钉住,零 import)
│ ├── price-sync.ts # 金额:抓官方两页合成价格档 + models.dev 压成第三方价目 + 落盘 + autoSyncDue(宿主半,允许 node API)
│ ├── provider-prices.ts # 金额:内置第三方价目**快照**(生成物,11 provider / 346 模型;由 scripts/gen-provider-prices.mjs 生成)
│ ├── balance.ts # 金额:官方余额响应消毒 + 查询端点白名单(零 import,两半共用)
│ ├── client.tsx # Browser 半:设置页 + 菜单浮层 + 拦截器
│ ├── terminal/ # 「默认终端」(Windows:pwsh → Git Bash),全部零官方运行时依赖
│ │ ├── discover.ts # 探测:git 锚定反推 bash、硬排除 WSL、多候选排序(纯函数)
│ │ ├── render.ts # 结果渲染与 exit 状态解析(与官方逐字对齐)
│ │ ├── sandbox.ts # 沙箱策略面 + 升级审批(fail-closed,与官方同语义)
│ │ ├── tool.ts # bash 工具定义:schema / 执行 / 后台 / 中止 / 终端卡片
│ │ ├── contracts.ts # 字段、三档、候选净化、生效判定、状态行文案(纯函数)
│ │ └── host.ts # 宿主半接线:按会话下发 restrict+register+section、立刻覆盖在跑会话
│ └── client/
│ ├── chords.ts # 键位编码/录制校验
│ ├── interceptors.ts # keydown/contextmenu 捕获拦截 + 回放
│ ├── SettingsSection.tsx # 设置页「输入体验」(顶部常显 + 折叠栏目 + 默认终端 / 统计行卡片)
│ ├── settings-style.ts # 折叠卡片样式表(dsh-ux-* 类名注入)
│ ├── ContextMenuHost.tsx # shell.overlay 右键菜单
│ ├── stats-line.ts # 统计行:小数语义(不撒谎)+ 两个字符串变换(纯逻辑)
│ ├── stats-dom.ts # 统计行:定位与改写(DOM 助手,不 import React)
│ ├── StatsLineEntry.tsx # 统计行:composer dock 上的隐形条目 + 观察器(React 胶水)
│ ├── CostChipEntry.tsx # 金额:composer dock 上的金额胶囊 + portal 浮层明细(React 胶水)
│ ├── session-cost.ts # 金额:向宿主半取"逐笔准时"的费用(节流 + 尾随 + 单飞)
│ ├── money-admin.ts # 金额:价目同步与余额查询的宿主调用封装(两条独立路径)
│ ├── peak-alert.ts # 金额:峰谷相位、倒计时文案与(可选)系统通知
│ ├── CostCard.tsx # 金额:设置页那一栏(单价 / 节假日 / 峰谷提醒 / 余额 / 同步价目)
│ └── styles.ts # --dsw-* 令牌内联样式
├── docs/settings-panel-0.5.0.png # README 顶部那张设置页截图(用 tag 固定的 raw 链接引用,不进 npm 包)
├── test/
│ ├── host-header-mirror.mjs # 请求头镜像的行为测试(假 settings 驱动构建产物)
│ ├── host-settings-generations.mjs # 宿主半两代设置服务(0.1.6 的 get/register 与 0.1.7 的 describe)
│ ├── quick-commands.mjs # 快捷指令 / 优化接口 / 路由注册 / 重启机制
│ ├── quick-store.mjs # 快捷指令文件存储与写回路径
│ ├── terminal-policy.mjs # 默认终端:探测 / 渲染 / 升级审批 / bash 工具 / 宿主半接线
│ ├── terminal-mount.mjs # 官方持久终端三件套:挂载行结构 + 用加载器同一个求值器真跑 !!js 探测与守卫
│ ├── stats-line.mjs # 统计行:小数语义(与官方源码逐例对)/ 文本与 aria-label 变换 / 开关
│ ├── stats-dom.mjs # 统计行:假 DOM 钉住定位、改写范围与"不碰样式"
│ ├── pricing.mjs # 金额:与出处实现(MIT)原文逐样本对拍 + 有意差异 + 历史档与节假日
│ ├── usage-fold.mjs # 金额:按 route 归因的折叠规则(差分 / 归属 / 脏数据 / 价格档 / clear())
│ ├── price-sync.mjs # 金额:价目同步的消毒与四条失败路径(抓不到绝不覆盖本地价)
│ ├── official-pricing.mjs # 金额:官方价格页解析(真页面夹具,45 条)
│ ├── balance.mjs # 金额:余额响应消毒(80 条)+ 端点白名单(伪装域逐例)
│ ├── client-registration.mjs # 客户端注册协议、右键行为、抬头按钮、七栏开关、金额栏护栏与设置写入校验
│ ├── settings-service-adopt.mjs # 客户端两代设置服务认领(settingsScope / configForms / 都没有)
│ ├── mutation-guards.mjs # 变异测试(手动跑):把每条护栏拆掉,测试必须变红
│ ├── settings-render.mjs # 真渲染测试:借 profile 的 react 把设置页渲染成 HTML(已进 npm test)
│ ├── check-sections.mjs # 升级前自查(只读):拿真实 settings.yaml 跑一遍栏开关迁移
│ └── opencode-header-wire-probe.mjs # 线级探针:本地端点,用来看 DSH 出网请求带了什么头
└── lib/ # 构建产物(运行所需)
node build.mjs # 产出 lib/index.js + lib/client.js
npm test # 16 个套件;当前 1581 passed, 0 failed(2026-09-29 实测)
node test/mutation-guards.mjs # 手动跑:变异测试,证明那套护栏真的在咬人(76 条,须单独跑)
node test/settings-render.mjs # 已进 npm test:把设置页真渲染成 HTML,断言版式与互斥显示(69 条)
node scripts/live-smoke.mjs # 手动跑:**联网复核**(官方页 vs 写死的价目表逐格对比 / models.dev vs 内置快照逐条对比 / 余额端点白名单与状态码);只读,不写任何文件
node test/check-sections.mjs # 只读:升级前看七栏会变成什么
node test/host-header-mirror.mjs # 只跑宿主半的请求头测试(写入/撤销/改名/幂等/不误删)
node test/host-settings-generations.mjs # 只跑宿主半的「两代设置服务」兼容(0.1.6 / 0.1.7 形状)
node test/terminal-policy.mjs # 只跑「默认终端」(探测 / 渲染 / 审批 / 工具 / 接线)
# 线级探针(可选):起一个本地端点,再把某条路由临时指过来,即可看到真实出网请求头
node test/opencode-header-wire-probe.mjs 8799 600000
# 若 DSH 源码检出不在默认路径:
$env:DSH_REPO_PATH='D:/DeepSeek Harness'; node build.mjs
cd <你的 DeepSeek Harness 检出目录>
pnpm dsh web --patch <本仓库的绝对路径>/cordis.dev.patch.yml
把尖括号替换成你的真实路径;插件行的 name 必须是 file:/// 形式的绝对 URL(Windows 下 D:/... 裸路径无法被 ESM loader 导入)。该文件只在本机存在(已 gitignore),内容如下:
- insert:
- id: composer-ux
name: 'file:///<本仓库的绝对路径>/lib/index.js'
src/ 后执行 node build.mjs;lib/client.js):客户端包在激活时读进内存,之后要靠 bundle 重新扫描(rev 变化)才会下发新内容;刷新浏览器即生效。lib/index.js):必须重启 DSH 进程。Cordis 加载器只在插件行的 name 变化时才重新 import(vendor/loader/lib/index.js),而 ESM 按 URL 缓存模块,改了文件内容不会失效——实测加临时探针并改动插件行 name 触发重载,探针都没有出现在启动日志里。所以:功能开关是随点随生效的,但宿主半代码的更新要重启一次。cordis.patch.yml)是 live 的(profile 的 patchReload: live),不用重启。除快捷指令之外的设置存在 profile 的 cordis.patch.yml 里本插件那一行($DSH_HOME/profiles/<profile>/cordis.patch.yml 的 - id: composer-ux → config)——0.1.7 起上游已废弃 $DSH_HOME/settings.yaml(它被改名成 settings.yaml.imported 后一次性导入);0.1.6 及以前才是 settings.yaml 的 composer-ux 分区。删掉那一项即恢复这些设置的默认值。
快捷指令(分类 + 条目 + 插入模式)不在设置文档里,它存 $DSH_HOME/quick-prompts.json(见上「数据存在哪」)。设置文档里那份 quickPrompts 只是 0.2.x 的迁移种子:只在 quick-prompts.json 不存在时被读一次(用来把旧列表搬成「默认」分类),平时不参与任何行为,插件也不会再写它。
quickPrompts: 这一项)——不影响插件运行;之后若 JSON 丢失,只会回落到内置 9 条。删掉后它不会自己长回来。quick-prompts.json,不要指望这份种子:它是旧快照,不会随你的后续修改更新。第一层:写入锁成了孤儿锁。 DSH 用 <profile>/package.json.lock(wx 独占创建 + 内容为持有者 PID)串行化跨进程写入,对争用方的规定是「绝不删除已存在的锁——锁的年龄证明不了持有者已经停下;孤儿锁属于操作者动作」。而硬杀(restart-webui.bat 的 taskkill /T /F)只要正好落在一次设置写入中间,这把锁就永久留在磁盘上。后果不是"某次写入失败",而是该 profile 此后每一次设置写入都在 2 秒后超时:读得到、写不进,界面上只表现为"点了没反应",一句报错都没有(2026-09-23 真机连撞两次,两次都是重启留下的)。
src/settings-lock.ts,逐条钉在测试第 12 节。Get-Content "$env:USERPROFILE\.dsh\profiles\web\package.json.lock" # 先确认持有者 PID 是否还活着
Remove-Item "$env:USERPROFILE\.dsh\profiles\web\package.json.lock" -Force
第二层:写入落盘了,但界面还是旧值。 这说明运行中的进程没有把这次配置变更接进运行时(文件里的值是对的)。设置页自 0.6.1 起会在写入被拒、或"写了但运行时没变"时把原因显示在卡片顶部;遇到第二种情况重启一次 DSH 即可(重启会按文件里的值启动)。
插件市场的装前体检会扫描宿主代码里的混淆/动态执行特征(eval / new Function / 超长 base64 块)。本插件的 lib/ 里这些特征为零:
new Function 还原」的分支。本插件所有 schema 都传函数回调,从不使用字符串回调,因此 build.mjs 在打包后会把该分支替换为等价空实现;若将来依赖升级导致模式失配,构建会直接报错退出,不会悄悄带着 new Function 发行。lib/ 为纯 JavaScript,安装时不需要执行任何构建脚本(因此不需要 pnpm 的构建授权,也不会在安装期于用户机器上执行代码)。settings.section、shell.overlay 槽位、ctx.settingsScope、ctx.settings(Host)、[data-composer-input] DOM 标记。CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。