跳到正文
dsh-market 浏览插件 GitHub EN

Yochor-Debug/dsh-float

DSH Web GUI 的 DeepSeek API 余额浮窗:角落胶囊显示余额,点击展开总额、赠金与充值明细,双击立即刷新且每 5 秒轮询一次;宿主侧代理 /user/balance,密钥不下发到浏览器。

Star 数 ★ 0 分类 用量与计费 收录于 2026-09-22

安装

在 DeepSeek Harness 里通过 dsh-market 安装

dsh plugin --profile web add dshmarket

或使用命令行

dsh plugin --profile web add github:Yochor-Debug/dsh-float

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。

截图

README

DSH Web GUI 的 DeepSeek API 余额小浮窗 —— 仿搜狗输入法状态条的胶囊式悬浮窗。

  • 打开 DSH(页面加载)时自动出现,关闭页面时随之消失:浮窗由宿主插件注入到每个 index.html,不依赖会话、不占用对话上下文。
  • 折叠态只显示余额(绿/黄/红圆点表示充足/偏低/不足),余额后面还有一个 峰/谷 徽章 表示当前时段是峰时还是谷时;并带一个很小的 ▼ 提示可展开。
  • 点击展开明细:总额、赠金、充值、上次刷新时间。悬停不会弹任何东西——鼠标划过 胶囊不产生反应;再点一次收起,点页面其它地方也会收起。 (早期版本是"悬停即展开",实测会让人觉得浮窗在跟着鼠标闪,已改为点击触发。)
  • 双击浮窗立即刷新(折叠态、展开态都支持),刷新时 ↻ 图标转一圈作为反馈;面板右上角的 ↻ 按钮同样可以手动刷新。
  • 每 5 秒自动刷新一次;页面切到后台时不刷新,切回前台立刻补一次。
  • 可拖动,位置自动吸附在视口内并记在 localStorage;深色/浅色跟随系统主题。
  • 浮窗位于 closed shadow root 中,脱离 React 渲染树,不与界面样式互相污染。 展开状态会镜像到宿主元素的 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。

密钥解析顺序(每次请求现取,不在插件里长期缓存):

  1. DEEPSEEK_API_KEY 环境变量;
  2. DSH 凭证服务 ctx.credentials.resolve('DEEPSEEK_API_KEY')(环境 + .env + 凭证存储); 访问该服务必须在 try 里——服务不可用时读属性本身就会抛,抛出去会让整条路由返回 400;
  3. 兜底:直接解析 $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 的转发器,会做三件事:

  1. 在 profile 目录里执行 pnpm add,本包以 link: 形式进入 dependencies;
  2. 因为本包 package.json 里声明了 dsh.bundle.patch,reconcilePlugins 会自动 把 dsh-plugin-balance-float 加进 profile 的 dsh.profile.bundles 列表;
  3. 于是它成为 profile 的一个层,层里 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 头。
  • 宿主对上游结果做缓存:成功 20 秒、失败 10 秒,所以浮窗 5 秒轮询不会等频打到 DeepSeek(最多每 20 秒一次真实请求)。轮询频率在 lib/client.js 的 REFRESH_MS, 缓存时长在 lib/index.js 的 OK_TTL_MS / ERROR_TTL_MS。
  • 上游 401/403 归为 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) 插件包方式下不再需要;重启进程即可

内容来自项目 README(GitHub)↗

评论

评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。