bowenliang123/dsh-context
bowenliang123/dsh-contextDSH 上下文洞察面板:Context 仪表盘 + /context命令 + Context 浏览器,查看 Context的分类组成、内容详情、演进趋势、压缩/注入事件、统计等一站式 Context 全生命周期管理。
DSH Web GUI 的 DeepSeek API 余额浮窗:角落胶囊显示余额,点击展开总额、赠金与充值明细,双击立即刷新且每 5 秒轮询一次;宿主侧代理 /user/balance,密钥不下发到浏览器。
DSH Web GUI 的 DeepSeek API 余额小浮窗 —— 仿搜狗输入法状态条的胶囊式悬浮窗。
index.html,不依赖会话、不占用对话上下文。峰/谷 徽章
表示当前时段是峰时还是谷时;并带一个很小的 ▼ 提示可展开。localStorage;深色/浅色跟随系统主题。data-open 属性上,仅供测试/诊断读取(不改行为)。| 文件 | 作用 |
|---|---|
package.json | 插件清单:声明 dsh.bundle.patch,这是被 dsh plugin 认成 bundle 层的关键 |
cordis.patch.yml | 这个包自带的 profile 层:insert 一行 id: balance-float / name: dsh-plugin-balance-float |
lib/index.js | 宿主侧:注册 GET /x-balance-float 代理路由 + 注入浮窗脚本 |
lib/client.js | 浏览器侧:浮窗 DOM/CSS/拖拽/轮询逻辑(被注入为内联脚本) |
tools/verify-package.mjs | 插件包独立验证:清单、patch、路由、注入、点击式交互(不需要 DSH 在跑) |
tools/harness.mjs | 离线验证:桩上下文 + 真实上游调用 + 生成 widget.html |
tools/test-credentials.mjs | 密钥解析回归测试(两种 YAML 写法、凭证服务、环境变量优先级) |
tools/screenshot-server.mjs | 截图台:同源提供页面与余额路由,便于无头截图 |
tools/verify-interactions.mjs | 用 CDP 验证"点击才展开"、双击刷新、5 秒轮询 |
tools/shot-open.mjs | 点击一次后截图(确认展开态渲染) |
tools/gui-shot.mjs | 通过 CDP 对真实 GUI 截图 |
宿主在每次渲染 index 时重新读取 lib/client.js,所以改浮窗样式/参数只需刷新页面,
不用重装插件,也不用重启 dsh web。
DeepSeek 的 /user/balance 需要 Authorization: Bearer <key>,且不下发 CORS 头。
把密钥放进浏览器既暴露凭证又会被 CORS 拦住,因此由宿主读取本机凭证并代理,
浏览器只请求同源的 /x-balance-float。
密钥解析顺序(每次请求现取,不在插件里长期缓存):
DEEPSEEK_API_KEY 环境变量;ctx.credentials.resolve('DEEPSEEK_API_KEY')(环境 + .env + 凭证存储);
访问该服务必须在 try 里——服务不可用时读属性本身就会抛,抛出去会让整条路由返回 400;$DSH_HOME/.credentials.yaml。⚠️ 第 3 步必须同时认两种 YAML 写法:refs: { DEEPSEEK_API_KEY: sk-x }(DSH 重启后会把
文件改写成这种流式写法)和块写法(refs: 换行后缩进的 DEEPSEEK_API_KEY: sk-x)。
最初只认块写法,重启后浮窗就显示「余额异常 · 未找到 DEEPSEEK_API_KEY」。
tools/test-credentials.mjs 是这条的回归测试,改动密钥解析后必须跑。
密钥不会出现在任何响应里。
dsh plugin --profile web add D:\DeepSeek_Workspace\dsh-plugin-balance-float
dsh plugin 是 pnpm 的转发器,会做三件事:
pnpm add,本包以 link: 形式进入 dependencies;package.json 里声明了 dsh.bundle.patch,reconcilePlugins 会自动
把 dsh-plugin-balance-float 加进 profile 的 dsh.profile.bundles 列表;insert 的 balance-float 就是宿主路由 + 注入。卸载:
dsh plugin --profile web remove dsh-plugin-balance-float
file:// 绝对路径早期版本是在 profile 的 cordis.patch.yml 里手写一行 file:///D:/.../lib/index.js?v=3。
那样能用,但不是插件包该有的形态:换机器/换目录/DSH 升级都会失效,也没有依赖记录。
现在与 open-sea-skin、dsh-builtin-browser 等第三方插件一致——patch 里写裸包名,
由 profile 的 node_modules 解析。
| 改的是 | 怎么生效 |
|---|---|
lib/client.js(浮窗外观/参数) | 宿主每次渲染 index 都重读该文件 —— 刷新页面即可,前提是宿主读的那个文件就是你改的那个。从 GitHub/npm 安装时,宿主读的是 profile 里的安装副本,不是你的工作目录,所以改动要先 push 再重装;用 link: 安装才会直接读工作目录,那才是"改完刷新即见" |
lib/index.js(宿主逻辑) | Node 按 URL 缓存 ESM,且运行中的进程不会重读 bundles 列表 —— 重启 dsh web(或重装插件后重启) |
package.json / cordis.patch.yml | 同上:bundles 列表只在 boot 时读一次,需要重启 |
⚠️ 实测结论:改 profile 的 package.json(bundles 列表)不会热生效。 运行中的 dsh
只在 boot 时组合一次 bundles 列表;patchReload: live 监视的是 cordis.patch.yml 这类
patch 文件,不包括 package.json。所以「装好插件但 GUI 没反应」时,重启进程即可。
node tools/verify-package.mjs # 插件包独立验证(不需要 DSH 在跑)
node tools/verify-tariff.mjs http://127.0.0.1:3931/ # 峰/谷 徽章的每个边界(需要 rig)
node tools/test-credentials.mjs # 两种 YAML 写法 + 凭证服务分支(回归测试)
node tools/harness.mjs # 路由、缓存、HEAD/POST 语义、真实余额
node tools/screenshot-server.mjs 3931 # 然后浏览器打开 http://127.0.0.1:3931/?balance-float=open
node tools/verify-interactions.mjs http://127.0.0.1:3931/ # 需要 rig 在跑
verify-tariff.mjs 用假 Date 冻结页面时钟、强制重绘,再通过 CDP 的 pierced DOM 从
closed shadow root 里读出徽章。它的期望值是照着定价规则手写的(不是拿组件自己的
逻辑算的),所以窗口写错会失败,而不会"自己跟自己一致"。
DeepSeek 对谷时段按峰时价格的 50% 计费。据官方定价页:
Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday, excluding Chinese public holidays. All other hours are off-peak, including weekends and Chinese public holidays in full.
即峰时段 = 周一至周五的 UTC 01:00–04:00 与 06:00–10:00(北京时间 09:00–12:00、 14:00–18:00);其余全部为谷,含周末与节假日全天。徽章在浏览器里按 UTC 计算,所以 不受本机时区影响,并且每 5 秒刷新时重新判断。
⚠️ 未计入中国法定节假日:那需要内置一份每年都会过期的日历。所以工作日的节假日会显示
峰,而实际按 谷 计费。徽章的悬停提示里写明了这一点。
verify-package.mjs 的实测输出(它模拟 loader 的方式 import 包入口并挂到桩上下文):
--- package manifest ---
PASS dsh.bundle.patch declared ./cordis.patch.yml
PASS patch file readable ...\cordis.patch.yml
PASS patch inserts our module bare package name in patch
PASS entry point exists in exports ./lib/index.js
PASS client script is shipped ["lib/index.js","lib/client.js","cordis.patch.yml","README.md"]
--- host half ---
PASS module exports apply entry=./lib/index.js name=balance-float inject=["webServer"]
PASS registered the balance route /x-balance-float
PASS injects the widget script script row: body
PASS injects the host style row .dsh-balance-float-host{...}
PASS widget uses click-only toggle no hover listeners in injected script
PASS route answers JSON {"ok":true,"available":true,...}
PASS route handles HEAD status=200
PASS route rejects other methods status=405 allow=GET, HEAD
VERIFY_PACKAGE: ALL_PASS
?balance-float=open 让浮窗以展开态启动(截图用);__RIG_MODE__=http-error
会把浮窗切到失败分支。渲染结果见 widget-expanded.png、widget-collapsed.png、
widget-error.png、widget-error-open.png、real-gui.png。
verify-interactions.mjs 的实测输出(真实 Edge + CDP,检测"点击才展开"这条规则):
baseline state (collapsed): closed
--- 1) hover only, no click ---
state after hover: closed -> PASS (hover does not open)
--- 2) single click ---
state after click: open -> PASS (panel opened)
--- 3) click again ---
state after 2nd click: closed -> PASS (panel closed)
--- 3b) click elsewhere closes it ---
state after outside click: closed -> PASS
--- 4) double click refreshes once ---
extra refresh requests: 1
--- 5) auto refresh rhythm over 12s ---
requests in 12s: 2 gaps(ms): 5010
检测方式说明:面板在 closed shadow root 里,页面脚本看不到它,所以浮窗把展开状态
镜像到宿主元素的 data-open 属性上供测试读取(只加属性,不改行为)。踩过的坑:最初想用
CDP 的 DOM.getDocument {pierce:true} 数影子树节点数,但那个数字在展开前后都是 40,
判据不灵敏、会误报"点击没生效"。
GET/HEAD,其它方法返回 405 且带 Allow 头。lib/client.js 的 REFRESH_MS,
缓存时长在 lib/index.js 的 OK_TTL_MS / ERROR_TTL_MS。bad-key,连接失败归为 unreachable,非 JSON 归为 bad-response,
凭证缺失归为 no-api-key;四种情况浮窗都显示可读的中文原因并保留双击重试。dblclick 完成,拖动过的指针序列不会触发(preventDefault 在拖动时发生)。dsh plugin --profile web remove dsh-plugin-balance-float
pnpm 会移除依赖,reconcilePlugins 会顺带把它从 profile 的 bundles 列表里摘掉。
删完重启 dsh web(bundles 列表只在 boot 时读)。
| 现象 | 根因 | 处理 |
|---|---|---|
dsh plugin add 报 'pnpm' is not recognized | %APPDATA%\npm 里只有 pnpm.ps1,没有 pnpm.cmd;dsh 用 spawnSync(shell:true) 起的是 cmd,cmd 只认 .cmd/.exe | 补了一个 pnpm.cmd shim(指向 node_modules\pnpm\pnpm.exe) |
| 装好后 GUI 仍是 404 | 运行中的进程只在 boot 时读一次 bundles 列表,package.json 不在 live 监视范围内 | 重启 dsh web;重启后路由 200、余额正常 |
patch 里写相对路径 ./… 不靠谱 | loader 的解析基准不确定(profile 目录?patch 所在目录?) | 照抄第三方插件约定:写裸包名 |
改宿主代码后旧的 ?v= 讲究 | 那是 file:// 直挂时代的产物(Node 按 URL 缓存 ESM) | 插件包方式下不再需要;重启进程即可 |