dsh-llm-workbuddy 0.1.12 → 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ 本文件记录每个版本的改动。版本号遵循语义化版本(MAJOR.MINOR.PATCH),
4
+ 每次发版请同步 `package.json` 的 `version` 并打一个 `git tag`(如 `v0.1.13`),
5
+ 在 GitHub 创建 Release 时本文件即为更新说明来源。
6
+
7
+ ## [0.1.14] - 2026-08-28
8
+
9
+ ### Changed
10
+ - 状态胶囊精简:外面只保留一个 **⚙️ 设置** 按钮,点击展开菜单显示原来的
11
+ 四个操作(🔍 诊断 / 📊 用量 / 🎁 签到 / 🔄 刷新模型),每个都带明确 title。
12
+ - 浮层(诊断 / 用量)统一加 **右上角 ✕ 关闭按钮**,并支持**点击浮层外区域**与
13
+ **按 Esc** 关闭,不再只能"再点一次菜单项"才收起。
14
+ - 菜单交互修复(与本次功能一并发布):
15
+ - 点击设置按钮可正常展开/收起(解决 `render()` 重建 DOM 触发外部点击误关的问题,
16
+ 给设置按钮加 `stopPropagation()`)。
17
+ - 点击菜单项后立即收起菜单(此前只改状态不重渲染,要等下次轮询才消失)。
18
+ - 点击页面空白处亦可收起。
19
+
20
+ ## [0.1.13] - 2026-08-28
21
+
22
+ ### Added
23
+ - 用量面板新增**积分消耗**统计。WorkBuddy 上游在每次对话的 usage 中返回
24
+ `credit` 字段(如 `deepseek-v4-pro` → `0.02`),插件此前丢弃了该字段;
25
+ 现将其写入本地台账,并在 📊 用量面板展示今日/累计/按模型的积分消耗。
26
+
27
+ ### Notes
28
+ - 代理未暴露账户余额接口,因此只能统计**已消耗**积分,无法显示剩余余额。
29
+ - 免费/折扣档模型上游回报 `credit: 0`,面板会提示"当前模型积分为 0"。
30
+
31
+ ## [0.1.12] - 2026-08-28
32
+
33
+ ### Fixed
34
+ - 恢复模型目录即白名单的过滤逻辑,模型选择器不再显示已下架/不可用的
35
+ 老模型(`deepseek-v3-1`、`glm-4.6`、`kimi-k2`、`minimax-m2.5`、
36
+ `kimi-k2-thinking`、`hunyuan-image-v3.0` 等)。
37
+
38
+ ### Added(随此前未提交批次一并发布)
39
+ - 本地 token 用量台账(`$DSH_HOME/llm-workbuddy/usage.jsonl`)与
40
+ `GET /api/workbuddy/usage` 接口,📊 用量面板展示今日/累计/按模型明细。
41
+ - 一键诊断端点 `POST /api/workbuddy/diagnose`,真实发请求探测模型能否出字。
42
+ - 请求图片压缩,确保上游接受多模态输入。
43
+
44
+ ## [0.1.11] 及更早
45
+
46
+ - `0.1.11`:适配代理动态模型列表(product.json 透传)。
47
+ - `0.1.10`:过滤已下架模型(glm-5.0/4.7/4.6、minimax-m2.5 等)。
48
+ - `0.1.9` 及之前:见 git 提交历史(`git log`)。
49
+
50
+ ---
51
+
52
+ <!-- 历史版本锚点(便于生成 Release 时对比区间) -->
53
+ [0.1.14]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.13...v0.1.14
54
+ [0.1.13]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.12...v0.1.13
55
+ [0.1.12]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.11...v0.1.12
56
+ [0.1.11]: https://github.com/zdk119746/dsh-llm-workbuddy/releases/tag/v0.1.11
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dsh-llm-workbuddy.svg)](https://www.npmjs.com/package/dsh-llm-workbuddy)
4
4
  [![license](https://img.shields.io/npm/l/dsh-llm-workbuddy.svg)](https://github.com/zdk119746/dsh-llm-workbuddy/blob/main/LICENSE)
5
+ [![Changelog](https://img.shields.io/badge/changelog-CHANGELOG-blue)](./CHANGELOG.md)
5
6
 
6
7
  在 DeepSeek Harness 中使用你的 **WorkBuddy / CodeBuddy** 账号模型的 LLM 适配器插件。
7
8
 
@@ -199,7 +200,7 @@ Web 登录后终端脚本也读得到同一份会话。
199
200
  | `GET /api/workbuddy/status` | 读取会话文件(默认 `~/.codebuddy-session.json`,或配置的 `sessionFile`)的 `auth.expiresAt` 判断会话是否有效,并 `fetch` 代理 `/health` 判断 `proxyUp`;返回 JSON:`{ sessionFile, authenticated, expiresAt, account, proxyUp, tokenValid, loginScriptAvailable }`;非 GET 返回 405 |
200
201
  | `POST /api/workbuddy/login` | 若已有有效会话则直接返回 `alreadyLoggedIn`;否则用系统 `python3` `spawn` 包内 `login_workbuddy.py --session-file <sessionFile>`,从子进程 stdout 解析出 `authUrl` 立即返回 `{ authUrl, pending:true }`(设备流在后台继续,前端轮询 status 感知完成);非 POST 返回 405 |
201
202
  | `POST /api/workbuddy/diagnose` | **一键诊断**:真实探测健康状态,返回 `{ ok, session, health, chat, loginScriptAvailable, restartCommand }`。与 `/status` 不同,它除了探 `/health`,还会**真实发一次最小模型请求**(`chat.chatWorking`),能戳穿"胶囊显示成功但模型全 500"的假象;`ok:false` 时附带 `restartCommand`(自动区分本地 monorepo 布局与标准安装);非 POST 返回 405 |
202
- | `GET /api/workbuddy/usage` | **用量统计**:读取本地 token 用量台账(`$DSH_HOME/llm-workbuddy/usage.jsonl`),返回 `{ today, byModel, total }`(今日/按模型/累计的 input/output tokens 与请求次数);非 GET 返回 405 |
203
+ | `GET /api/workbuddy/usage` | **用量统计**:读取本地 token 用量台账(`$DSH_HOME/llm-workbuddy/usage.jsonl`),返回 `{ today, byModel, total }`(今日/按模型/累计的 input/output tokens、请求次数,以及**积分消耗 `credit`**);非 GET 返回 405 |
203
204
 
204
205
  > 会话文件与登录脚本路径的解析顺序:
205
206
  > 1. 配置里显式指定的 `sessionFile` / `loginScript`;
@@ -212,12 +213,17 @@ Web 登录后终端脚本也读得到同一份会话。
212
213
  - 启动时在 `document.body` 末尾挂一个 `position:fixed` 的胶囊(右下角)。
213
214
  - 每 **5 秒** `GET /api/workbuddy/status`;点「登录」后加快到每 **2 秒**轮询、
214
215
  最多 30 次,直到 `authenticated:true`。
215
- - 胶囊里有个 **🔍 诊断** 按钮:点它 `POST /api/workbuddy/diagnose`,弹出一个面板
216
+ - 胶囊外面只显示一个 **⚙️ 设置** 按钮(带 title「WorkBuddy 设置」)。点击展开菜单,
217
+ 内含四个带明确 title 的操作:**🔍 诊断 / 📊 用量 / 🎁 签到 / 🔄 刷新模型**;
218
+ 点击页面空白或选中某项后菜单自动收起。
219
+ - **🔍 诊断**:点它 `POST /api/workbuddy/diagnose`,弹出一个面板
216
220
  显示**真实健康状态**(登录、会话文件、代理进程、登录令牌、模型能否出字),
217
221
  发现问题时附带**可复制的重启命令**(一键复制到终端执行)。
218
- - 胶囊里有个 **📊 用量** 按钮:点它 `GET /api/workbuddy/usage`,弹出一个面板
219
- 显示**今日/累计 token 用量**与**按模型明细**(数据来自本地台账
220
- `$DSH_HOME/llm-workbuddy/usage.jsonl`)。
222
+ - **📊 用量**:点它 `GET /api/workbuddy/usage`,弹出一个面板
223
+ 显示**今日/累计 token 用量**、**积分消耗**(上游每次返回的 `credit` 累加)与
224
+ **按模型明细**(数据来自本地台账 `$DSH_HOME/llm-workbuddy/usage.jsonl`)。
225
+ 注:代理不暴露余额/剩余积分接口,只能统计**已消耗**积分,无法显示账户剩余。
226
+ - 诊断 / 用量浮层**右上角都有 ✕ 关闭按钮**,也可点击浮层外区域或按 Esc 关闭。
221
227
  - 状态映射:
222
228
  - `authenticated && proxyUp` → 🟢 绿,显示 `WorkBuddy · <昵称>`
223
229
  - 否则 → 🔴 红,显示「登录」按钮;`proxyUp` 为 false 时额外提示 `代理未运行`
@@ -335,3 +341,18 @@ glm-5.0、minimax-m2.5 等,上游返回 `service info not found`)会被过
335
341
  > 第三方 workbuddy2api 代理**不在包内**,需单独安装(见「安装代理」章节)。
336
342
  > 仓库根目录的 `start-workbuddy.sh` / `login-workbuddy.sh` / `.workbuddy/` 是本地
337
343
  > 开发用的配套文件,不随 npm 包发布。
344
+
345
+ ## 更新日志 / Changelog
346
+
347
+ 每次发版的改动记录见 [CHANGELOG.md](./CHANGELOG.md)。
348
+
349
+ ### 发版流程(确保 GitHub 有更新说明)
350
+
351
+ 1. 改完代码后,在 `package.json` 里 `version` 自增(语义化版本)。
352
+ 2. 在 `CHANGELOG.md` 顶部补一段本次版本(`## [x.y.z] - 日期`)的 Added / Fixed / Changed。
353
+ 3. 提交并打 tag:`git commit -am "release: x.y.z"` 然后 `git tag vx.y.z`。
354
+ 4. `git push origin main --tags`,到 GitHub 用该 tag 创建 **Release**——
355
+ Release 的描述直接引用 CHANGELOG 对应段落,仓库页面就有了「改了啥」的说明。
356
+
357
+ > 注意:本机 `dsh web` 通过符号链接直接加载本地目录,因此**本地改动能立刻重启生效**,
358
+ > GitHub 上的版本只影响通过 `dsh plugin add dsh-llm-workbuddy` 安装的其他用户。
package/lib/client.js CHANGED
@@ -73,13 +73,34 @@ function runWidget() {
73
73
  ".wb-btn{margin-left:4px;border:0;border-radius:6px;background:#2563eb;color:#fff;" +
74
74
  "padding:3px 8px;cursor:pointer;font:inherit;}" +
75
75
  ".wb-btn:disabled{opacity:.6;cursor:default;}" +
76
- ".wb-warn{margin-left:6px;color:#f59e0b;font-size:11px;}";
76
+ ".wb-warn{margin-left:6px;color:#f59e0b;font-size:11px;}" +
77
+ ".wb-settle{position:relative;}" +
78
+ ".wb-setbtn{margin-left:4px;border:0;border-radius:6px;background:#374151;color:#e5e7eb;" +
79
+ "padding:3px 8px;cursor:pointer;font:inherit;}" +
80
+ ".wb-menu{position:absolute;right:0;bottom:38px;display:flex;flex-direction:column;gap:4px;" +
81
+ "background:#0f172a;border:1px solid #334155;border-radius:8px;padding:6px;min-width:148px;" +
82
+ "box-shadow:0 6px 20px rgba(0,0,0,.5);}" +
83
+ ".wb-menu .wb-item{display:flex;align-items:center;gap:6px;width:100%;text-align:left;" +
84
+ "border:0;border-radius:6px;background:#1e293b;color:#e2e8f0;padding:6px 8px;cursor:pointer;" +
85
+ "font:inherit;white-space:nowrap;}" +
86
+ ".wb-menu .wb-item:hover{background:#334155;}" +
87
+ ".wb-menu .wb-item .wb-ico{font-size:13px;}" +
88
+ ".wb-menu .wb-item:disabled{opacity:.6;cursor:default;}" +
89
+ ".wb-panel{position:fixed;right:12px;bottom:44px;z-index:2147483647;width:340px;max-height:340px;overflow:auto;" +
90
+ "background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:10px;" +
91
+ "font:12px/1.5 system-ui,sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.5);}" +
92
+ ".wb-panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;}" +
93
+ ".wb-panel-head .wb-ttl{font-weight:700;}" +
94
+ ".wb-panel .wb-close{border:0;background:transparent;color:#94a3b8;cursor:pointer;" +
95
+ "font-size:14px;line-height:1;padding:2px 6px;border-radius:4px;}" +
96
+ ".wb-panel .wb-close:hover{background:#1e293b;color:#e2e8f0;}";
77
97
  document.head.appendChild(style);
78
98
  document.body.appendChild(host);
79
99
 
80
100
  var status = null;
81
101
  var busy = false;
82
102
  var timer = null;
103
+ var menuOpen = false;
83
104
 
84
105
  function fmtExpiry(expiresAt) {
85
106
  if (!expiresAt) return "";
@@ -105,35 +126,60 @@ function runWidget() {
105
126
  html += '<button class="wb-btn" id="wb-login"' + disabled + ">" +
106
127
  (busy ? "登录中…" : "登录") + "</button>";
107
128
  }
108
- // 诊断按钮始终显示,便于随时自查真实健康状态。
109
- html += '<button class="wb-btn wb-diagnose" id="wb-diagnose" title="一键诊断">🔍</button>';
110
- // 用量按钮:查看今日/累计 token 用量与模型明细。
111
- html += '<button class="wb-btn wb-usage" id="wb-usage" title="用量统计">📊</button>';
112
- // 签到按钮:✅ 今日已处理 / 🎁 今日未处理(点击立即领取;10 点后也会自动领取)。
113
- var ck = status.checkin;
114
- if (ck) {
115
- var ckMsg = ck.lastResult && ck.lastResult.message ? "\n" + ck.lastResult.message : "";
116
- var ckTitle = ck.handledToday ? "今日已处理签到" + ckMsg : "今日还没签到(每天 10 点后自动领取)\n点击立即领取";
117
- var ckIcon = ck.handledToday && ck.lastResult && ck.lastResult.ok ? "✅" : "🎁";
118
- html += '<button class="wb-btn wb-checkin" id="wb-checkin" title="' + esc(ckTitle) + '">' + ckIcon + "</button>";
129
+ // 只外显一个设置按钮:点击展开诊断/用量/签到/刷新模型四个操作。
130
+ html += '<span class="wb-settle">' +
131
+ '<button class="wb-setbtn" id="wb-settings" title="WorkBuddy 设置(诊断 / 用量 / 签到 / 刷新模型)">⚙️</button>';
132
+ if (menuOpen) {
133
+ // 签到按钮:✅ 今日已处理 / 🎁 今日未处理(点击立即领取;10 点后也会自动领取)。
134
+ var ck = status.checkin;
135
+ var ckTitle = "今日还没签到(每天 10 点后自动领取)\n点击立即领取";
136
+ var ckIcon = "🎁";
137
+ var ckDisabled = "";
138
+ if (ck) {
139
+ var ckMsg = ck.lastResult && ck.lastResult.message ? "\n" + ck.lastResult.message : "";
140
+ ckTitle = ck.handledToday ? "今日已处理签到" + ckMsg : ckTitle;
141
+ ckIcon = ck.handledToday && ck.lastResult && ck.lastResult.ok ? "✅" : "🎁";
142
+ ckDisabled = ck.handledToday ? " disabled" : "";
143
+ }
144
+ html += '<span class="wb-menu">' +
145
+ '<button class="wb-item" id="wb-diagnose" title="一键诊断:真实探测登录、代理、令牌与模型出字状态">🔍 诊断</button>' +
146
+ '<button class="wb-item" id="wb-usage" title="用量统计:查看今日/累计 token 与积分消耗、按模型明细">📊 用量</button>' +
147
+ '<button class="wb-item" id="wb-checkin" title="' + esc(ckTitle) + '"' + ckDisabled + '>' + ckIcon + " 签到</button>" +
148
+ '<button class="wb-item" id="wb-models" title="刷新模型列表:从代理重读模型并让模型选择器立即重载">🔄 刷新模型</button>' +
149
+ "</span>";
119
150
  }
120
- // 刷新模型按钮:从代理重读模型列表(跟随 WorkBuddy 应用更新)并让模型选择器立即重载。
121
- html += '<button class="wb-btn wb-models" id="wb-models" title="刷新模型列表">🔄</button>';
151
+ html += "</span>";
122
152
  if (!status.proxyUp) html += '<span class="wb-warn">代理未运行</span>';
123
153
  html += "</div>";
124
154
  host.innerHTML = html;
125
155
  var btn = host.querySelector("#wb-login");
126
156
  if (btn) btn.addEventListener("click", onLogin);
157
+ var setBtn = host.querySelector("#wb-settings");
158
+ if (setBtn) setBtn.addEventListener("click", function (e) {
159
+ // 阻止冒泡:render() 会重建 host 的 DOM,被点按钮随即脱离文档,
160
+ // 否则 document 的外部点击监听器会误判为"点在外面"而立刻关掉菜单。
161
+ e.stopPropagation();
162
+ menuOpen = !menuOpen;
163
+ render();
164
+ });
127
165
  var dg = host.querySelector("#wb-diagnose");
128
- if (dg) dg.addEventListener("click", onDiagnose);
166
+ if (dg) dg.addEventListener("click", function () { menuOpen = false; render(); onDiagnose(); });
129
167
  var us = host.querySelector("#wb-usage");
130
- if (us) us.addEventListener("click", onUsage);
168
+ if (us) us.addEventListener("click", function () { menuOpen = false; render(); onUsage(); });
131
169
  var ckBtn = host.querySelector("#wb-checkin");
132
- if (ckBtn) ckBtn.addEventListener("click", onCheckin);
170
+ if (ckBtn) ckBtn.addEventListener("click", function () { menuOpen = false; render(); onCheckin(); });
133
171
  var mdBtn = host.querySelector("#wb-models");
134
- if (mdBtn) mdBtn.addEventListener("click", onRefreshModels);
172
+ if (mdBtn) mdBtn.addEventListener("click", function () { menuOpen = false; render(); onRefreshModels(); });
135
173
  }
136
174
 
175
+ // 点击页面其它位置关闭设置菜单。
176
+ document.addEventListener("click", function (e) {
177
+ if (!menuOpen) return;
178
+ if (host.contains(e.target)) return;
179
+ menuOpen = false;
180
+ render();
181
+ });
182
+
137
183
  function refresh() {
138
184
  return fetch("/api/workbuddy/status")
139
185
  .then(function (r) { return r.ok ? r.json() : null; })
@@ -177,15 +223,7 @@ function runWidget() {
177
223
  function onDiagnose() {
178
224
  var panel = document.getElementById("wb-diagnose-panel");
179
225
  if (panel) { panel.remove(); return; }
180
- var el = document.createElement("div");
181
- el.id = "wb-diagnose-panel";
182
- el.style.cssText =
183
- "position:fixed;right:12px;bottom:44px;z-index:2147483647;width:340px;max-height:340px;overflow:auto;" +
184
- "background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;" +
185
- "padding:10px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.5);";
186
- el.innerHTML = '<div class="wb-dg-title" style="font-weight:700;margin-bottom:6px">WorkBuddy 诊断中…</div>' +
187
- '<div class="wb-dg-body" style="white-space:pre-wrap;word-break:break-word;color:#94a3b8">正在探测…</div>';
188
- document.body.appendChild(el);
226
+ var el = makePanel("wb-diagnose-panel", "WorkBuddy 诊断");
189
227
 
190
228
  fetch("/api/workbuddy/diagnose", { method: "POST" })
191
229
  .then(function (r) { return r.json(); })
@@ -199,18 +237,20 @@ function runWidget() {
199
237
  lines.push("登录令牌: " + (d.health && d.health.tokenValid ? "有效" : "无效"));
200
238
  lines.push("模型出字: " + (d.chat && d.chat.chatWorking ? "正常" : "失败"));
201
239
  if (d.chat && d.chat.chatError) lines.push("出字错误: " + d.chat.chatError);
202
- var html = '<div class="wb-dg-title" style="font-weight:700;margin-bottom:6px">' + title + '</div>' +
203
- '<div class="wb-dg-body" style="white-space:pre-wrap;word-break:break-word">' + lines.map(esc).join("\n") + "</div>";
240
+ var body = el.querySelector(".wb-panel-body");
241
+ var html = '<div style="font-weight:700;margin-bottom:4px">' + esc(title) + '</div>' +
242
+ '<div>' + lines.map(esc).join("\n") + "</div>";
204
243
  if (!d.ok && d.restartCommand) {
205
244
  html += '<div style="margin-top:8px;font-weight:700">重启命令(复制到终端执行):</div>' +
206
245
  '<pre style="background:#1e293b;border-radius:6px;padding:8px;font-size:11px;overflow:auto;white-space:pre-wrap">' +
207
246
  esc(d.restartCommand) + "</pre>" +
208
247
  '<button class="wb-btn" id="wb-copy-cmd">复制重启命令</button>';
209
248
  }
210
- el.innerHTML = html;
249
+ body.innerHTML = html;
211
250
  var copy = document.getElementById("wb-copy-cmd");
212
251
  if (copy) {
213
- copy.addEventListener("click", function () {
252
+ copy.addEventListener("click", function (e) {
253
+ e.stopPropagation();
214
254
  navigator.clipboard.writeText(d.restartCommand).then(function () {
215
255
  copy.textContent = "已复制 ✓";
216
256
  setTimeout(function () { copy.textContent = "复制重启命令"; }, 1500);
@@ -219,8 +259,8 @@ function runWidget() {
219
259
  }
220
260
  })
221
261
  .catch(function (err) {
222
- el.innerHTML = '<div class="wb-dg-title" style="font-weight:700;color:#f87171">诊断失败</div>' +
223
- '<div class="wb-dg-body">' + esc(String(err && err.message || err)) + "</div>";
262
+ el.querySelector(".wb-panel-body").innerHTML =
263
+ '<div style="font-weight:700;color:#f87171">诊断失败</div><div>' + esc(String(err && err.message || err)) + "</div>";
224
264
  });
225
265
  }
226
266
 
@@ -228,37 +268,36 @@ function runWidget() {
228
268
  function onUsage() {
229
269
  var panel = document.getElementById("wb-usage-panel");
230
270
  if (panel) { panel.remove(); return; }
231
- var el = document.createElement("div");
232
- el.id = "wb-usage-panel";
233
- el.style.cssText =
234
- "position:fixed;right:12px;bottom:44px;z-index:2147483647;width:340px;max-height:340px;overflow:auto;" +
235
- "background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;" +
236
- "padding:10px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.5);";
237
- el.innerHTML = '<div class="wb-usage-title" style="font-weight:700;margin-bottom:6px">WorkBuddy 用量统计</div>' +
238
- '<div class="wb-usage-body" style="color:#94a3b8">加载中…</div>';
239
- document.body.appendChild(el);
271
+ var el = makePanel("wb-usage-panel", "WorkBuddy 用量统计");
240
272
 
241
273
  fetch("/api/workbuddy/usage")
242
274
  .then(function (r) { return r.ok ? r.json() : null; })
243
275
  .then(function (d) {
244
276
  if (!d) throw new Error("空响应");
277
+ var hasCredit = (d.total.credit || 0) > 0;
245
278
  var lines = [];
246
279
  lines.push("今日:输入 " + d.today.inputTokens + " / 输出 " + d.today.outputTokens + " tokens(" + d.today.requests + " 次请求)");
247
280
  lines.push("累计:输入 " + d.total.inputTokens + " / 输出 " + d.total.outputTokens + " tokens(" + d.total.requests + " 次请求)");
281
+ if (hasCredit) {
282
+ lines.push("今日积分消耗:" + fmtCredit(d.today.credit) + " | 累计:" + fmtCredit(d.total.credit));
283
+ }
248
284
  lines.push("");
249
285
  lines.push("按模型明细:");
250
286
  if (d.byModel.length === 0) lines.push(" 暂无数据");
251
287
  for (var i = 0; i < d.byModel.length; i += 1) {
252
288
  var m = d.byModel[i];
253
- lines.push(" " + m.model + ":输入 " + m.inputTokens + " / 输出 " + m.outputTokens + "" + m.requests + " 次)");
289
+ var extra = (m.credit || 0) > 0 ? ",积分 " + fmtCredit(m.credit) : "";
290
+ lines.push(" " + m.model + ":输入 " + m.inputTokens + " / 输出 " + m.outputTokens + "(" + m.requests + " 次" + extra + ")");
254
291
  }
255
- el.innerHTML = '<div class="wb-usage-title" style="font-weight:700;margin-bottom:6px">WorkBuddy 用量统计</div>' +
256
- '<div class="wb-usage-body" style="white-space:pre-wrap;word-break:break-word">' + esc(lines.join("\n")) + "</div>" +
257
- '<div style="margin-top:8px;color:#64748b;font-size:11px">数据来源:本地 token 用量台账(' + esc(d.total.requests) + " 次累计)</div>";
292
+ var note = hasCredit
293
+ ? "数据来源:本地用量台账(含上游每次返回的积分消耗;代理不暴露余额接口,无法显示剩余积分)"
294
+ : "数据来源:本地 token 用量台账(" + d.total.requests + " 次累计);当前模型为免费/折扣档,上游回报积分为 0";
295
+ el.querySelector(".wb-panel-body").innerHTML =
296
+ esc(lines.join("\n")) + '<div style="margin-top:8px;color:#64748b;font-size:11px">' + esc(note) + "</div>";
258
297
  })
259
298
  .catch(function (err) {
260
- el.innerHTML = '<div class="wb-usage-title" style="font-weight:700;color:#f87171">用量加载失败</div>' +
261
- '<div class="wb-usage-body">' + esc(String(err && err.message || err)) + "</div>";
299
+ el.querySelector(".wb-panel-body").innerHTML =
300
+ '<div style="font-weight:700;color:#f87171">用量加载失败</div><div>' + esc(String(err && err.message || err)) + "</div>";
262
301
  });
263
302
  }
264
303
 
@@ -268,6 +307,41 @@ function runWidget() {
268
307
  });
269
308
  }
270
309
 
310
+ /** 格式化积分消耗:保留 2 位小数,去掉无意义的尾零。 */
311
+ function fmtCredit(n) {
312
+ var v = Number(n) || 0;
313
+ return (Math.round(v * 100) / 100).toString();
314
+ }
315
+
316
+ /**
317
+ * 创建一个可关闭的浮层(诊断/用量面板共用)。右上角带 ❌,点击浮层外区域或按
318
+ * Esc 均可关闭。返回的 el 由调用方填充内容;再次点击对应菜单项也可关闭
319
+ * (调用方已对 getElementById(id) 做了存在即 remove 的处理)。
320
+ * @param {string} id 浮层 DOM id(如 "wb-usage-panel")
321
+ * @param {string} title 标题文字
322
+ * @returns {HTMLElement}
323
+ */
324
+ function makePanel(id, title) {
325
+ var el = document.createElement("div");
326
+ el.id = id;
327
+ el.className = "wb-panel";
328
+ el.innerHTML =
329
+ '<div class="wb-panel-head"><span class="wb-ttl">' + esc(title) + "</span>" +
330
+ '<button class="wb-close" title="关闭">✕</button></div>' +
331
+ '<div class="wb-panel-body" style="white-space:pre-wrap;word-break:break-word;color:#94a3b8">加载中…</div>';
332
+ document.body.appendChild(el);
333
+ var close = function () { el.remove(); document.removeEventListener("click", onDocClick, true); document.removeEventListener("keydown", onKey); };
334
+ el.querySelector(".wb-close").addEventListener("click", close);
335
+ var onDocClick = function (e) { if (!el.contains(e.target)) close(); };
336
+ var onKey = function (e) { if (e.key === "Escape") close(); };
337
+ // 用捕获阶段,避免被状态胶囊的 document 点击监听(菜单外部关闭)提前吞掉。
338
+ setTimeout(function () {
339
+ document.addEventListener("click", onDocClick, true);
340
+ document.addEventListener("keydown", onKey);
341
+ }, 0);
342
+ return el;
343
+ }
344
+
271
345
  /** 手动领取今日签到(幂等:官方对已签到返回业务拒绝,不会出错)。 */
272
346
  function onCheckin() {
273
347
  var btn = host.querySelector("#wb-checkin");
package/lib/index.js CHANGED
@@ -354,11 +354,17 @@ function mapFinishReason(reason) {
354
354
  function mapUsage(usage) {
355
355
  const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens;
356
356
  const reasoning = usage.completion_tokens_details?.reasoning_tokens;
357
+ // The upstream reports per-request `credit` consumption in the usage block
358
+ // (e.g. deepseek-v4-pro → 0.02). Free/discounted models report 0, so we
359
+ // only keep the field when it is a positive number — that is the real
360
+ // 积分 spend we want to surface (the proxy exposes no balance endpoint).
361
+ const credit = typeof usage.credit === "number" && usage.credit > 0 ? usage.credit : undefined;
357
362
  return {
358
363
  inputTokens: usage.prompt_tokens - (cacheRead ?? 0),
359
364
  outputTokens: usage.completion_tokens,
360
365
  ...(cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {}),
361
366
  ...(reasoning !== undefined ? { reasoningTokens: reasoning } : {}),
367
+ ...(credit !== undefined ? { credit } : {}),
362
368
  };
363
369
  }
364
370
 
@@ -387,6 +393,7 @@ function recordUsage(model, usage) {
387
393
  output: usage.outputTokens ?? 0,
388
394
  cacheRead: usage.cacheReadTokens ?? 0,
389
395
  reasoning: usage.reasoningTokens ?? 0,
396
+ credit: usage.credit ?? 0,
390
397
  });
391
398
  appendFileSync(file, `${line}\n`);
392
399
  // Trim the ledger when it exceeds the cap: keep only the newest lines.
@@ -410,12 +417,14 @@ function readUsageLedger() {
410
417
  const byModel = new Map();
411
418
  let todayInput = 0;
412
419
  let todayOutput = 0;
420
+ let todayCredit = 0;
413
421
  let todayRequests = 0;
414
422
  let totalInput = 0;
415
423
  let totalOutput = 0;
424
+ let totalCredit = 0;
416
425
  let totalRequests = 0;
417
426
  if (!existsSync(file)) {
418
- return { today: { inputTokens: 0, outputTokens: 0, requests: 0 }, byModel: [], total: { inputTokens: 0, outputTokens: 0, requests: 0 } };
427
+ return { today: { inputTokens: 0, outputTokens: 0, credit: 0, requests: 0 }, byModel: [], total: { inputTokens: 0, outputTokens: 0, credit: 0, requests: 0 } };
419
428
  }
420
429
  const lines = readFileSync(file, "utf8").split("\n").filter(Boolean);
421
430
  for (const line of lines) {
@@ -423,28 +432,32 @@ function readUsageLedger() {
423
432
  try { entry = JSON.parse(line); } catch { continue; }
424
433
  const input = entry.input ?? 0;
425
434
  const output = entry.output ?? 0;
435
+ const credit = entry.credit ?? 0;
426
436
  totalInput += input;
427
437
  totalOutput += output;
438
+ totalCredit += credit;
428
439
  totalRequests += 1;
429
440
  if ((entry.ts ?? 0) >= todayMs) {
430
441
  todayInput += input;
431
442
  todayOutput += output;
443
+ todayCredit += credit;
432
444
  todayRequests += 1;
433
445
  }
434
446
  const model = String(entry.model ?? "unknown");
435
- const agg = byModel.get(model) ?? { inputTokens: 0, outputTokens: 0, requests: 0 };
447
+ const agg = byModel.get(model) ?? { inputTokens: 0, outputTokens: 0, credit: 0, requests: 0 };
436
448
  agg.inputTokens += input;
437
449
  agg.outputTokens += output;
450
+ agg.credit += credit;
438
451
  agg.requests += 1;
439
452
  byModel.set(model, agg);
440
453
  }
441
454
  const byModelList = [...byModel.entries()]
442
455
  .map(([model, agg]) => ({ model, ...agg }))
443
- .sort((a, b) => b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens));
456
+ .sort((a, b) => b.credit - a.credit || b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens));
444
457
  return {
445
- today: { inputTokens: todayInput, outputTokens: todayOutput, requests: todayRequests },
458
+ today: { inputTokens: todayInput, outputTokens: todayOutput, credit: todayCredit, requests: todayRequests },
446
459
  byModel: byModelList,
447
- total: { inputTokens: totalInput, outputTokens: totalOutput, requests: totalRequests },
460
+ total: { inputTokens: totalInput, outputTokens: totalOutput, credit: totalCredit, requests: totalRequests },
448
461
  };
449
462
  }
450
463
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-workbuddy",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "WorkBuddy (via the local workbuddy2api proxy) LLM provider adapter for DeepSeek Harness, with a Web login-status widget",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -44,7 +44,8 @@
44
44
  "lib",
45
45
  "login_workbuddy.py",
46
46
  "cordis.patch.yml",
47
- "README.md"
47
+ "README.md",
48
+ "CHANGELOG.md"
48
49
  ],
49
50
  "peerDependencies": {
50
51
  "@deepseek-ai/cordis": "^4.0.1",