dsh-whale-widget 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +41 -6
  2. package/lib/index.js +135 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,7 @@ DeepSeek Harness(DSH)Web 界面右下角的常驻挂件:小鲸鱼气泡图
11
11
  | 你要挂在哪 | 用哪条分支 | 安装方式 |
12
12
  |---|---|---|
13
13
  | **DSH Web 界面右下角**(就是这个 README 描述的插件) | `main`(默认分支) | `dsh plugin --profile web add dsh-whale-widget`(推荐,装 npm 已发布版);也可以从本仓库装 `dsh plugin --profile web add github:MeteorNOX/DeepSeek-Balance-Whale-Widget`,但那样装的是 **main 当前状态、不跟随已发布版本** |
14
+ | **官方桌面客户端(Electron)右下角** | `main`(同一个包) | ⚠️ **不能用** `--profile web`(桌面端读的是 `desktop` profile,CLI 也拒绝 `--profile desktop`)—— 在桌面端会话里**让 DSH 自己装**,见下方「官方桌面端(Electron 客户端)请先看这一节」 |
14
15
  | **Codex 桌面应用**(跟随 Codex 窗口、无独立网页) | [`For-Codex`](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/tree/For-Codex) | 该分支的 `api-balance-whale`:解压到 `%USERPROFILE%\plugins\api-balance-whale`,再按分支内的 [安装说明](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/blob/For-Codex/docs/INSTALL-AND-ROLLBACK-0.2.0.md) 注册计划任务(也可以直接交给 Codex 自己装喵~) |
15
16
 
16
17
  ⚠️ **两条分支互不兼容**:`For-Codex` 的插件不能用 `dsh plugin … add` 装进 DSH 网页;本主分支的插件也不能在 Codex 桌面里运行。上表第一行是本仓库默认分支(`main`)的能力,第二行是另一个分支的能力。
@@ -24,7 +25,7 @@ DeepSeek Harness(DSH)Web 界面右下角的常驻挂件:小鲸鱼气泡图
24
25
  ### 记账与显示
25
26
 
26
27
  - 🐋 **常驻自启**:随 DSH Web 界面每次打开自动出现(标准 DSH bundle 插件)
27
- - 💰 **余额**:60 秒自动刷新 + 点击鲸鱼手动刷新;余额变化时数字**滚动动画**;瞬时网络抖动自动沿用最近余额不报错
28
+ - 💰 **余额**:60 秒自动刷新 + 点击鲸鱼手动刷新;余额变化时数字**滚动动画**;瞬时网络抖动自动沿用最近余额不报错。**两种取数方式,API key 优先**:① DSH 凭据里的 `DEEPSEEK_API_KEY`;② **DSH 账号登录态** —— 通过「设置 → 账号与余额」登录了 DeepSeek 账号、又没配 API key 时自动使用(**不需要去 platform 建 key**)
28
29
  - 📊 **今日已用(小鲸鱼记账)**:不需要任何令牌。余额**下降**按观测累计为消费,余额**上升**(充值 / 赠金)单独记录、不会冲掉已有消费;检测到余额增加会提示「待核对余额调整」,可在「小鲸鱼记账 → **DeepSeek(内置)→ 设置 → 余额校正**」按实际累计到账金额与非调用扣减校正 —— **「已观测消费」与「余额校正」都是 DeepSeek 账户口径**(由该 API key 的余额观测得出,不含其它厂商;「本机模型费用」才是所有模型的本地估算)(逐轮明细保留 90 天或最多 2 万条、逐日归档保留 365 天,超期数据归档到 `.dshw-usage-archive.json`);金额按 8 位小数记账、显示保留两位
29
30
  - 💬 **每轮对话消耗**:监听 DSH 本机会话事件,按模型**真实 usage**(input / cache / output / reasoning tokens)换算金额,每轮结束弹出消耗泡泡;可开关、自动关闭秒数可设(0 = 不自动关闭),**泡泡内容可自定义**(模块化,金额用 `{cost}` 引用),入口:菜单 → 每轮消耗提示 → 「自定义提示」
30
31
  - ⛰️ **峰谷定价**:工作日高峰 9:00–12:00、14:00–18:00(北京时间),其余空闲;2026-08-23 起**周末全天谷价**。峰谷模块支持状态文字、倒计时、"梁文峰谷 / !?强强?! / 简洁"等多种样式
@@ -143,6 +144,33 @@ dsh-whale-widget/
143
144
 
144
145
  ## 安装
145
146
 
147
+ ### 官方桌面端(Electron 客户端)请先看这一节 ⚠️
148
+
149
+ **桌面端读的不是 `web` profile。** 官方桌面客户端用的是 **`desktop` profile**(`%USERPROFILE%\.dsh\profiles\desktop`),而 `dsh plugin` 命令行**按设计拒绝**碰它:
150
+
151
+ ```
152
+ error: profile "desktop" is managed exclusively by the Electron application
153
+ ```
154
+
155
+ 所以下面「方式 A~D」里的 `--profile web` 命令**对桌面端不适用** —— 按它装完,插件落在 `web` profile 里,而桌面窗口读的是 `desktop` profile,一个字也读不到。表现就是「右下角什么都没有(连 ☰ 也没有)、控制台也不报错」。
156
+
157
+ **桌面端的正确装法:让桌面端里的 DSH 自己装。**
158
+
159
+ 在桌面客户端的会话里直接说一句就够了(例如「把 dsh-whale-widget 装上」)—— 它会调用内置的 `plugin_manager` 工具,而那个工具的**作用域就是当前 profile**(桌面端 = `desktop`),由客户端自带的插件管理器在 profile 目录内用**自带的 pnpm** 完成安装,并把 `dsh-whale-widget` 写进该 profile 的 `dsh.profile.bundles`。需要指定版本时也可以写 `dsh-whale-widget@<版本号>`(例如 `dsh-whale-widget@0.3.13`,**填当下的最新版**)这样的安装规格。
160
+
161
+ > 等价的手工路径是在该 profile 目录里用客户端自带的 pnpm 装一次、再手工写进 `dsh.profile.bundles`,但**不推荐** —— 那是客户端自己管理的目录。
162
+
163
+ **生效方式(实测)**:新装一个包通常**热生效**(工具返回 `"application":"applied"`);**换版本(升级)需要重启客户端**(返回 `"restart-required"`)。
164
+
165
+ **装完怎么自检**(两条分别对应宿主半区与客户端半区):
166
+
167
+ - **宿主半区**:`%USERPROFILE%\.dsh\.dshw-turn.json` 存在,且 `seq` 随对话递增;
168
+ - **客户端半区**:桌面端 `%APPDATA%\@deepseek-ai\dsh-desktop\Local Storage\leveldb` 里出现 `dshw-pos` / `dshw-last-seq` —— 这两个键**只有挂件前端**会写。
169
+
170
+ 桌面端界面里看不到插件版本号时,可以看 `%USERPROFILE%\.dsh\profiles\desktop\package.json` 的 `dsh.profile.bundles` 和同目录的 `pnpm-lock.yaml`。
171
+
172
+ > 下面方式 A~D 都是 **Web(`dsh web`)** 的安装方式。
173
+
146
174
  ### 方式 A:已有本插件的完整资源包(本地目录 / 压缩包)(推荐)
147
175
 
148
176
  适用于别人直接发给你一个 zip,或你手上已有一份解压好的插件目录(目录里应有 `package.json`、`cordis.patch.yml`、`lib/`、`assets/`、`README.md`)。
@@ -264,14 +292,17 @@ MeteorNOX/DeepSeek-Balance-Whale-Widget,或者我本地已经有这个插件
264
292
  - curl "http://127.0.0.1:3080/dsh-whale/audio-fragment.wav?id=end_a" 也应返回 200 audio/wav
265
293
  (用来确认资源包里的内置音效已随包就位;若是 404 说明 assets/ 不完整)
266
294
 
267
- 另外请检查 DSH 凭据里是否配置了 DEEPSEEK_API_KEY(没有就提示用户配置)。
295
+ 另外请检查 DSH 凭据里是否配置了 DEEPSEEK_API_KEY(没有就提示用户配置;如果用户是用 DSH 账号登录的,较新 DSH 上不配 key 也能取余额,**别把"没 key"当成故障**)。
268
296
  ```
269
297
 
270
298
  ## 凭据(安装后必读)
271
299
 
272
- 只需一个凭据:
300
+ **余额有两种取数方式(API key 优先,两者互不干扰):**
273
301
 
274
- - **`DEEPSEEK_API_KEY`(必需)**:DeepSeek API 密钥,用于拉取余额(`GET https://api.deepseek.com/user/balance`)。在 DSH 凭据服务里配置(凭据管理界面 / `.dsh/.credentials.yaml`)。
302
+ - **`DEEPSEEK_API_KEY`(推荐)**:DeepSeek API 密钥,用于拉取余额(`GET https://api.deepseek.com/user/balance`)。在 DSH 凭据服务里配置(凭据管理界面 / `.dsh/.credentials.yaml`)。
303
+ - **DSH 账号登录态(可选,**不需要** API key)**:如果你是通过 DSH 的「设置 → 账号与余额」**登录 DeepSeek 账号**的,插件会在**没有 API key 时自动**改用 DSH 自己的账号服务取余额(充值钱包 + 赠金钱包,两者相加作为记账基准)。token、平台请求头与失效清理都由 DSH 负责,**插件不接触你的账号令牌**。
304
+ - 需要**较新的 DSH**(如桌面端 `0.1.7-rc.2`)才有这个服务;老版本、未登录、或账号没有余额钱包时,会回到下面的「未配置」提示。
305
+ - 账号态与 API key 态的**账本是分开的**(按账号标识区分):从 API key 换成账号登录后,"今日已用"会从新的观测重新起算。
275
306
 
276
307
  > **不需要** `DEEPSEEK_PLATFORM_TOKEN`。早期版本的"实时·令牌"模式已下线,今日已用统一由**小鲸鱼记账**(余额差 + 会话事件)计算,零令牌开箱即用。
277
308
 
@@ -352,13 +383,16 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
352
383
 
353
384
  ## 常见问题
354
385
 
355
- - **挂件不出现**:确认安装命令成功;`dsh --profile web --dump-config` 里能看到 `dsh-whale-widget`;重启 `dsh web` 后 F5。
386
+ - **挂件不出现**:
387
+ - **Web(`dsh web`)**:确认安装命令成功;`dsh --profile web --dump-config` 里能看到 `dsh-whale-widget`;重启 `dsh web` 后 F5。
388
+ - **官方桌面端**:先确认装对了 profile —— 桌面端读的是 **`desktop` profile**,**不能**用 `--profile web`(而 `--profile desktop` 会被 CLI 拒绝,这是设计如此)。正确入口见上面「**官方桌面端(Electron 客户端)请先看这一节**」,并用那里的**两条自检**判断是「没装到位」还是「装了但没渲染」。
356
389
  - **本插件所有接口(`/dsh-whale/*`)的访问校验**:默认只接受**回环地址**(`127.0.0.1` / `localhost` / `[::1]`)的请求,并拒绝跨站标记(`Sec-Fetch-Site: cross-site`)与 Origin 和 Host 不同源的请求 —— 这是防「恶意网页读写本机接口」的信任栅栏(issue #92 / #136)。如果你把 `dsh web` 放在**反向代理或局域网地址**后面,请用环境变量声明允许的 Host(逗号分隔,可带端口),否则会被 403:
357
390
  ```bash
358
391
  DSHW_TRUSTED_HOSTS=dsh.example.com,10.0.0.5:3080 dsh web
359
392
  ```
360
393
  - **图片/音效不显示、没声音**:确认插件包内 `assets/` 完整(`DSniang1.png`、`*.mp3`、`minecraft-exp-orb.wav` 等);缺失时相关功能静默降级。
361
- - **余额报「未配置 DEEPSEEK_API_KEY」**:去 DSH 凭据里配置。
394
+ - **余额报「未配置 DEEPSEEK_API_KEY」**:去 DSH 凭据里配置 API key;**或者**用「设置 → 账号与余额」登录 DeepSeek 账号(较新 DSH 支持)—— 没配 key 时插件会自动走账号态,不需要去 platform 建 key。
395
+ - **余额拿到了、但提示「账本不可用 / 已停止写入以保护原记录」**:账本文件(`.dshw-usage.json`)被外部改坏或结构异常,插件**拒绝覆盖**它以免丢数据。先把它备份改名让插件重建,再按需用「余额校正」补账。
362
396
  - **今日已用显示 `--`**:先等一次成功的余额观测;统计从该观测时刻开始,起点之前的消费不在此区间内。
363
397
  - **今日已用与官网有差异**:先核对同一账户、同一币种与同一统计区间;若有充值或其它余额调整,用「小鲸鱼记账 → DeepSeek(内置)→ 设置 → 余额校正」填写实际到账金额。余额接口只返回余额快照、不提供充值流水,充值与消费发生在同一次刷新间隔时需要实际到账金额才能校正。
364
398
  - **充值后消费数字没变**:这是预期行为——充值不会增加消费;数字上方会出现「待核对余额调整」,提示你补充本统计区间的累计到账金额。
@@ -389,6 +423,7 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
389
423
 
390
424
  ## 致谢
391
425
 
426
+ - **DSH 账号登录态读余额**(端点与鉴权头、凭据记录的位置、DSH 自身 `deepseekAccount` 服务的推荐调用方式,以及三个集成坑:账户标识字符集、赠金要计入基准、服务可能不存在)由 GitHub 用户 [@yybai25](https://github.com/yybai25) 在 [#157](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/issues/157) 中给出完整规格与**已在自己机器上验证过的参考实现**;**0.3.14 据此实现**(只调 DSH 服务、不接触账号令牌)。感谢他的贡献。
392
427
  - 充值记账修复方案(余额上升与下降分开记账、显式余额校正公式、按账户/币种隔离观测窗口、账本原子写入与迁移备份)由 GitHub 用户 [@Yang-huai406](https://github.com/Yang-huai406) 独立设计并实现为可运行的修复分支;**0.3.1 在该方案基础上移植合并**,并保留本项目既有的音效修复。感谢他的支持。
393
428
  - OpenCode Go 订阅额度(多窗口额度 `quota.json.windows`、按窗口展示与紧凑重置倒计时、「订阅额度」模块的窗口选择)由 GitHub 用户 [@ELFsay](https://github.com/ELFsay) 提交([#99](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/pull/99)),已合入 `main`。感谢他的贡献。
394
429
  - **历史合并 PR 的贡献者**(按合入顺序,均已进入本仓库代码 / 发布流程):
package/lib/index.js CHANGED
@@ -582,10 +582,50 @@ function loadWidgetJs() {
582
582
  return widgetJsCache ? widgetJsCache.text : ''
583
583
  }
584
584
 
585
+ // —— 桌面端(Electron)结构化注入行:**内联 script 行**,不是 script-src 行(issue #154)——
586
+ // 桌面壳的 index.html 从安装包静态 dist 直出,`tapIndex`(函数变换)永远过不去;唯一通道是
587
+ // `webserver/index-inject` 推的结构化行,经 IPC 交给页面侧解释器逐行应用。
588
+ // 但解释器对两种 script 行的处理**不对称**(desktop 前端 `web-boot`):
589
+ // case "script": createElement + textContent + append —— 没有 await,**不可能"加载失败"**
590
+ // case "script-src": await loadScript(src) —— 失败即 reject,而那个 reject 会
591
+ // reject 掉 __DSH_BOOT_READY__ ⇒ **应用起不来**
592
+ // 再加一层:外壳把这张表在**宿主启动时收集一次**后缓存,**没有任何刷新路径** ⇒ 插件在运行期被关掉时,
593
+ // 表里仍留着这一行,而 `/dsh-whale/*` 路由已注销 ⇒ 加载 404 ⇒ 就是 issue #154 那个
594
+ // "desktop web: failed to load /dsh-whale/widget.js" 致命错误。
595
+ // 所以改成内联行:由**我们自己**建 `<script src=…>` 并**吞掉** onerror —— 路由在就正常加载,
596
+ // 路由不在就静默失败,宿主永远不会因为我们起不来。
597
+ const DESKTOP_WIDGET_ROW_TEXT =
598
+ '(function(){try{var d=document.body||document.head||document.documentElement;if(!d)return;' +
599
+ 'var s=document.createElement("script");s.src="/dsh-whale/widget.js";' +
600
+ 's.onerror=function(){};d.appendChild(s)}catch(e){}})()'
601
+
585
602
  export default {
586
603
  name: 'whale-balance-widget',
587
- inject: ['webServer', 'credentials', 'connection'],
588
- apply(ctx) {
604
+ // v758:**有意去掉对象级 `inject: ['webServer','credentials','connection']`**(issue #154 的隐患面)。
605
+ // 对象级 inject 会把整个 apply() 推迟到三个服务就绪之后;而桌面端的注入表是**宿主启动时一次性
606
+ // 收集**的(`dsh-desktop-host`:`collectIndexInjections()` → IPC → 渲染层),订阅一旦晚于那次收集,
607
+ // 这一行就永远进不了表 = 桌面端挂件完全不出现(#152/#153 那位报告人无法稳定复现的正是这个竞态)。
608
+ // 现在 apply() 立刻执行、**第一件事就是注册注入行**(它不依赖任何服务),其余逻辑原样放进
609
+ // `root.inject([...], cb)` 局部等待 —— 语义与原来的对象级 inject 等价,但行的注册不再被推迟。
610
+ apply(root) {
611
+ const rowDisposers = []
612
+ root.effect(() => () => { for (const d of rowDisposers) { try { d() } catch (err) {} } })
613
+ // ① 结构化注入行:桌面端唯一能生效的通道(web 形态另有 tapIndex,两条并存、各自去重)
614
+ rowDisposers.push(root.on('webserver/index-inject', (table) => {
615
+ try {
616
+ if (!Array.isArray(table)) return
617
+ for (const row of table) {
618
+ if (!row) continue
619
+ if (row.kind === 'script-src' && row.src === '/dsh-whale/widget.js') return // 旧版本推过 → 不重复
620
+ if (row.kind === 'script' && typeof row.text === 'string'
621
+ && row.text.indexOf('/dsh-whale/widget.js') >= 0) return
622
+ }
623
+ table.push({ kind: 'script', placement: 'body', text: DESKTOP_WIDGET_ROW_TEXT })
624
+ } catch (err) {}
625
+ }))
626
+
627
+ // ② 其余逻辑:等齐三个服务后再跑(等价于原来的对象级 inject,但不再挡住 ①)
628
+ root.inject(['webServer', 'credentials', 'connection'], (ctx) => {
589
629
  // —— 浏览器信任栅栏(issue #92 建立,issue #136 补 fail-closed)——
590
630
  // dsh 的 connection 服务提供 requestRejection(req):Host/Origin 被伪造(DNS 重绑定)或未认证的
591
631
  // 请求必须被拒。**但这个方法在较老的 dsh 上不存在**(#136 实测:0.1.5-rc.2/rc.3 有、0.1.0-rc.3 没有),
@@ -838,6 +878,60 @@ export default {
838
878
  )
839
879
  }
840
880
 
881
+ // v759(issue #157):**DSH 账号登录态**的余额路径。
882
+ // 只通过 DSH 账号登录的用户没有 API key,余额在 DeepSeek 平台的账号接口上。
883
+ // **不要自己拼 HTTP**:token 注入、5 个 `x-client-*` 头、401/`40003` 失效清理全部由 DSH 的
884
+ // `deepseekAccount` 服务负责(DSH 自己的「账号与余额」设置卡片就是 `ctx.deepseekAccount.getBalance(client)`)。
885
+ // ⚠️ 必须用 `ctx.get('deepseekAccount')` 读**可选服务**,**绝不能写进 inject** —— 老宿主根本没有这个服务,
886
+ // 一旦进 inject,整个插件会永远等不到服务而**完全不 apply**(比没有这个功能严重得多)。
887
+ // ⚠️ 与 API key 路径**互不干扰**(API key 永远优先;DSH 自己的注释也是 "Neither route falls back to the other")。
888
+ async function fetchAccountBalance() {
889
+ let account = null
890
+ try { account = typeof ctx.get === 'function' ? ctx.get('deepseekAccount') : null } catch (err) { account = null }
891
+ if (!account || typeof account.getBalance !== 'function') return null
892
+ // 账户标识:优先用账号服务给的稳定 id(不同账号 → 不同账本 scope);取不到就退回固定标识。
893
+ // 一律 sha256[:24] 十六进制 —— accounting.mjs 校验 scope 必须匹配 /^[a-zA-Z0-9_-]{1,80}$/
894
+ // (带冒号这种可读写法会直接抛「账户标识无效」,见 issue #157 报告人踩的坑 1)。
895
+ let accountId = null
896
+ try {
897
+ const state = typeof account.getState === 'function' ? account.getState() : null
898
+ accountId = (state && (state.userId || (state.profile && state.profile.userId))) || null
899
+ } catch (err) { accountId = null }
900
+ let result
901
+ try {
902
+ result = await account.getBalance({
903
+ version: String(process.env.DSH_CLIENT_VERSION || process.env.DSH_VERSION || '') || 'unknown',
904
+ locale: String(process.env.DSH_LOCALE || '') || 'zh_CN',
905
+ timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60, // 东为正的整秒
906
+ })
907
+ } catch (err) { return null }
908
+ // 未登录 / 没有 grant / 平台失败 → 一律返回 null,让调用方退回原来的文案(**不缓存错误**)
909
+ if (!result || result.status !== 'ready' || !Array.isArray(result.value) || result.value.length === 0) return null
910
+ const accNum = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
911
+ const wallets = result.value.filter((w) => w && accNum(w.balance) !== null)
912
+ if (wallets.length === 0) return null
913
+ const currency = wallets.some((w) => String(w.currency || '').toUpperCase() === 'CNY')
914
+ ? 'CNY'
915
+ : String((wallets[0] && wallets[0].currency) || 'CNY').toUpperCase()
916
+ const sumOf = (list) => (Array.isArray(list) ? list : [])
917
+ .filter((w) => w && String(w.currency || 'CNY').toUpperCase() === currency && accNum(w.balance) !== null)
918
+ .reduce((s, w) => s + Number(w.balance), 0)
919
+ const recharge = sumOf(result.value)
920
+ const bonus = sumOf(result.bonusWallets)
921
+ return {
922
+ ok: true,
923
+ // 充值 + 赠金:与 API key 路径的 `total_balance` 同口径(那个字段本身也是两者相加),
924
+ // 否则"按余额差"推算的今日已用会偏小(issue #157 报告人的坑 2)。
925
+ totalBalance: Number((recharge + bonus).toFixed(6)),
926
+ rechargeBalance: Number(recharge.toFixed(6)),
927
+ bonusBalance: Number(bonus.toFixed(6)),
928
+ currency,
929
+ balanceSource: 'account',
930
+ accountTag: createHash('sha256').update('dsh-account:' + (accountId === null ? 'default' : String(accountId))).digest('hex').slice(0, 24),
931
+ updatedAt: new Date().toISOString(),
932
+ }
933
+ }
934
+
841
935
  async function fetchBalance() {
842
936
  // v739(用户反馈「每次新实例的第一次余额请求都失败」):
843
937
  // ① 冷启动时凭据服务可能尚未就绪(resolve 抛错),这与「确实没配置」不同 → 短延迟重试一次;
@@ -854,7 +948,13 @@ export default {
854
948
  }
855
949
  }
856
950
  if (!cred) {
857
- return { ok: false, code: 'NO_KEY', error: '未配置 DEEPSEEK_API_KEY' }
951
+ // v759(issue #157):没有 API key 时退回 DSH 账号登录态;两条路互不干扰,API key 始终第一优先。
952
+ const accountPayload = await fetchAccountBalance()
953
+ if (accountPayload) return accountPayload
954
+ return {
955
+ ok: false, code: 'NO_KEY',
956
+ error: '未配置 DEEPSEEK_API_KEY,且账号余额不可用(未登录 DeepSeek 账号,或该账号没有余额钱包)',
957
+ }
858
958
  }
859
959
  let lastErr = null
860
960
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -890,6 +990,7 @@ export default {
890
990
  totalBalance: Number(info.total_balance),
891
991
  accountTag: createHash('sha256').update(String(cred.value)).digest('hex').slice(0, 24),
892
992
  currency: String(info.currency || 'CNY'),
993
+ balanceSource: 'apikey',
893
994
  updatedAt: new Date().toISOString(),
894
995
  }
895
996
  }
@@ -1052,7 +1153,7 @@ export default {
1052
1153
  const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
1053
1154
  for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
1054
1155
  return {
1055
- ok: true, version: '0.3.12', today: todayData, days7,
1156
+ ok: true, version: '0.3.14', today: todayData, days7,
1056
1157
  total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
1057
1158
  all: {
1058
1159
  days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
@@ -2252,7 +2353,7 @@ export default {
2252
2353
  const summary = daySummary(led, todayKey())
2253
2354
  const nowSec = Math.floor(Date.now() / 1000)
2254
2355
  return {
2255
- ...visible, version: '0.3.12', isPeak: isPeakTime(nowSec),
2356
+ ...visible, version: '0.3.14', isPeak: isPeakTime(nowSec),
2256
2357
  // 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
2257
2358
  peakNextChangeAt: nextPeakChangeAt(nowSec),
2258
2359
  peakHolidays: HOLIDAY_VALLEY_LIST,
@@ -2271,9 +2372,26 @@ export default {
2271
2372
  balanceInFlight = fetchBalance()
2272
2373
  .then((payload) => {
2273
2374
  if (payload.ok) {
2274
- recordLedgerUsage(payload.totalBalance, payload.currency, payload.accountTag, Date.parse(payload.updatedAt))
2275
- balanceCache = { at: Date.now(), payload }
2276
- return publicBalance(payload)
2375
+ // v759(issue #157 报告人踩到):**"记不了账"不该吞掉已经拿到的余额**。
2376
+ // 以前 recordLedgerUsage 抛错会被最外层 .catch 包成「余额服务异常: …」,
2377
+ // 界面看起来像"接口没通",其实余额已经拿到了(accountTag/币种不合法就会这样)。
2378
+ // 但要与"**账本整个读不出来**"区分开:那种情况必须保持 fail-closed(见下面的 rethrow)——
2379
+ // 一边是"余额拿到了、只是没记上账",一边是"账本已经坏了、连今日已用都算不出来",
2380
+ // 后者若也回 ok:true,用户会以为一切正常,而账本其实不可用。
2381
+ let ledgerError = ''
2382
+ try {
2383
+ recordLedgerUsage(payload.totalBalance, payload.currency, payload.accountTag, Date.parse(payload.updatedAt))
2384
+ } catch (err) {
2385
+ ledgerError = String((err && err.message) || err).slice(0, 160)
2386
+ }
2387
+ let visible
2388
+ try {
2389
+ visible = publicBalance(payload) // 读账本算今日已用/近 7 天
2390
+ } catch (err) {
2391
+ throw err // 账本读不出来(结构损坏等)→ 交给最外层 .catch,报 ok:false 并保留原记录
2392
+ }
2393
+ balanceCache = { at: Date.now(), payload } // 先缓存:之后即使记账失败,余额也继续显示
2394
+ return ledgerError ? { ...visible, ledgerError } : visible
2277
2395
  }
2278
2396
  if (payload.transient && balanceCache) {
2279
2397
  return { ...publicBalance(balanceCache.payload), stale: true, error: payload.error }
@@ -3531,27 +3649,20 @@ export default {
3531
3649
  return html + tag
3532
3650
  }))
3533
3651
 
3534
- // —— 官方 DSH 桌面端(Electron)适配(issue #152 / #153)——
3535
- // 桌面壳的 index.html 是**从安装包静态 dist 直接读盘**的(`dsh-app://app/`),
3536
- // 永远不经过宿主的 renderIndex(),所以上面的 tapIndex 在桌面端不生效 —— 于是"宿主半区活着、
3537
- // 客户端半区从未被请求"。桌面端走的是**结构化行注入**:宿主启动时 collectIndexInjections()
3538
- // 经 IPC 把行表交给渲染层,渲染层按 kind 逐行应用(见 @deepseek-ai/dsh-host-webserver 的
3539
- // injections.d.ts:`{ kind:'script-src'; placement:'head'|'body'; src:string }`,外部经典脚本)。
3540
- // 这里补同一行;**两条并存**:Web 形态仍走 tapIndex,两边都只会注入一次(各自去重)。
3541
- disposers.push(ctx.on('webserver/index-inject', (table) => {
3542
- try {
3543
- if (!Array.isArray(table)) return
3544
- for (const row of table) {
3545
- if (row && row.kind === 'script-src' && row.src === '/dsh-whale/widget.js') return
3546
- }
3547
- table.push({ kind: 'script-src', placement: 'body', src: '/dsh-whale/widget.js' })
3548
- } catch (err) {}
3549
- }))
3652
+ // —— 官方 DSH 桌面端(Electron)适配(issue #152 / #153 / #154)——
3653
+ // 桌面壳的 index.html 是**从安装包静态 dist 直接读盘**的(`dsh-app://app/`),永远不经过宿主的
3654
+ // renderIndex(),所以上面的 tapIndex 在桌面端不生效 —— 于是"宿主半区活着、客户端半区从未被请求"。
3655
+ // 桌面端唯一的注入通道是 `webserver/index-inject` 的结构化行(宿主启动时 collectIndexInjections()
3656
+ // 经 IPC 交给渲染层,渲染层按 kind 逐行应用)。
3657
+ // **该行的注册已上移到 apply() 开头**(见 `DESKTOP_WIDGET_ROW_TEXT` 上方那段注释):两个原因 ——
3658
+ // ① 桌面端的注入表是**一次性收集**的,行必须尽早进表;② 行本身改成了**内联 script 行**,
3659
+ // 因为页面侧解释器对 `script-src` 是"加载失败即 reject 整个 boot"(issue #154 的致命启动错误)。
3550
3660
 
3551
3661
  ctx.effect(() => () => {
3552
3662
  for (const d of disposers) {
3553
3663
  try { d() } catch (err) {}
3554
3664
  }
3555
3665
  })
3666
+ }) // ← 结束 root.inject(['webServer','credentials','connection'], cb)
3556
3667
  },
3557
3668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-widget",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义",
5
5
  "keywords": [
6
6
  "dsh",