dsh-llm-workbuddy 0.1.13 → 0.1.15
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 +44 -0
- package/README.md +23 -6
- package/lib/client.js +107 -47
- package/lib/index.js +96 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@
|
|
|
4
4
|
每次发版请同步 `package.json` 的 `version` 并打一个 `git tag`(如 `v0.1.13`),
|
|
5
5
|
在 GitHub 创建 Release 时本文件即为更新说明来源。
|
|
6
6
|
|
|
7
|
+
## [0.1.15] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **刷新模型看不到新上架模型**:内置目录此前同时充当白名单,代理
|
|
11
|
+
`/v1/models` 里凡不在目录中的模型一律被丢弃,所以平台新上架的模型
|
|
12
|
+
(如 `deepseek-v4.1-flash`)刷新多少次都不会出现。现改为**黑名单**
|
|
13
|
+
`RETIRED_MODEL_IDS`:只隐藏实测不可用(上游 `service info not found`)的
|
|
14
|
+
旧模型 id,其余照单显示。顺带放出 7 个别名仍在服务、却被旧白名单误藏的模型
|
|
15
|
+
(`minimax-m2.7`、`glm-5.0-turbo`、`hy3-preview`、`deepseek-v3-1-lkeap`、
|
|
16
|
+
`deepseek-v3-0324-lkeap`、`deepseek-r1-0528-lkeap`、`hunyuan-2.0-instruct`)。
|
|
17
|
+
- 内置目录新增 `deepseek-v4.1-flash`,并在代理不可达时作为兜底条目。
|
|
18
|
+
- 🔍 诊断的「模型出字」误报修复:探针用 `hy3` + `max_tokens: 4` 发一次请求,
|
|
19
|
+
而推理模型会把这 4 个 token 全花在思考上、`content` 返回空串,于是
|
|
20
|
+
"代理正常"被误判成「代理返回了空响应」。现承认"有计费 completion token +
|
|
21
|
+
有 choice"即为正常。
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **模型名后显示积分消耗倍率**:读取代理 `/v1/models` 的 `credits` 字段
|
|
25
|
+
(`"x0.06 credits"`)并拼成 `Deepseek-V4-Flash ×0.06`;实时值优先,内置目录
|
|
26
|
+
`credits` 兜底。平台未声明倍率的模型只显示模型名——不会用 0 或猜测值冒充。
|
|
27
|
+
- 内置目录补齐各模型 `credits`(取自 WorkBuddy 应用 `product.json`)。
|
|
28
|
+
|
|
29
|
+
### Changed(代理侧,`.workbuddy-src` / workbuddy2api)
|
|
30
|
+
- `/v1/models` 透出 `credits` 字段(`model_to_codex_format`),倍率不再由各客户端
|
|
31
|
+
各自维护。
|
|
32
|
+
- 官方应用 `product.json` 与本地 `models_config.json` 合并时倍率单独回填:官方
|
|
33
|
+
条目为空则沿用本地倍率,避免官方合并把倍率抹平。
|
|
34
|
+
- `models_config.json` 补充 `deepseek-v4.1-flash`(1M 上下文 / 50k 输出)。
|
|
35
|
+
|
|
36
|
+
## [0.1.14] - 2026-08-28
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- 状态胶囊精简:外面只保留一个 **⚙️ 设置** 按钮,点击展开菜单显示原来的
|
|
40
|
+
四个操作(🔍 诊断 / 📊 用量 / 🎁 签到 / 🔄 刷新模型),每个都带明确 title。
|
|
41
|
+
- 浮层(诊断 / 用量)统一加 **右上角 ✕ 关闭按钮**,并支持**点击浮层外区域**与
|
|
42
|
+
**按 Esc** 关闭,不再只能"再点一次菜单项"才收起。
|
|
43
|
+
- 菜单交互修复(与本次功能一并发布):
|
|
44
|
+
- 点击设置按钮可正常展开/收起(解决 `render()` 重建 DOM 触发外部点击误关的问题,
|
|
45
|
+
给设置按钮加 `stopPropagation()`)。
|
|
46
|
+
- 点击菜单项后立即收起菜单(此前只改状态不重渲染,要等下次轮询才消失)。
|
|
47
|
+
- 点击页面空白处亦可收起。
|
|
48
|
+
|
|
7
49
|
## [0.1.13] - 2026-08-28
|
|
8
50
|
|
|
9
51
|
### Added
|
|
@@ -37,6 +79,8 @@
|
|
|
37
79
|
---
|
|
38
80
|
|
|
39
81
|
<!-- 历史版本锚点(便于生成 Release 时对比区间) -->
|
|
82
|
+
[0.1.15]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.14...v0.1.15
|
|
83
|
+
[0.1.14]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.13...v0.1.14
|
|
40
84
|
[0.1.13]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.12...v0.1.13
|
|
41
85
|
[0.1.12]: https://github.com/zdk119746/dsh-llm-workbuddy/compare/v0.1.11...v0.1.12
|
|
42
86
|
[0.1.11]: https://github.com/zdk119746/dsh-llm-workbuddy/releases/tag/v0.1.11
|
package/README.md
CHANGED
|
@@ -213,13 +213,17 @@ Web 登录后终端脚本也读得到同一份会话。
|
|
|
213
213
|
- 启动时在 `document.body` 末尾挂一个 `position:fixed` 的胶囊(右下角)。
|
|
214
214
|
- 每 **5 秒** `GET /api/workbuddy/status`;点「登录」后加快到每 **2 秒**轮询、
|
|
215
215
|
最多 30 次,直到 `authenticated:true`。
|
|
216
|
-
-
|
|
216
|
+
- 胶囊外面只显示一个 **⚙️ 设置** 按钮(带 title「WorkBuddy 设置」)。点击展开菜单,
|
|
217
|
+
内含四个带明确 title 的操作:**🔍 诊断 / 📊 用量 / 🎁 签到 / 🔄 刷新模型**;
|
|
218
|
+
点击页面空白或选中某项后菜单自动收起。
|
|
219
|
+
- **🔍 诊断**:点它 `POST /api/workbuddy/diagnose`,弹出一个面板
|
|
217
220
|
显示**真实健康状态**(登录、会话文件、代理进程、登录令牌、模型能否出字),
|
|
218
221
|
发现问题时附带**可复制的重启命令**(一键复制到终端执行)。
|
|
219
|
-
-
|
|
222
|
+
- **📊 用量**:点它 `GET /api/workbuddy/usage`,弹出一个面板
|
|
220
223
|
显示**今日/累计 token 用量**、**积分消耗**(上游每次返回的 `credit` 累加)与
|
|
221
224
|
**按模型明细**(数据来自本地台账 `$DSH_HOME/llm-workbuddy/usage.jsonl`)。
|
|
222
225
|
注:代理不暴露余额/剩余积分接口,只能统计**已消耗**积分,无法显示账户剩余。
|
|
226
|
+
- 诊断 / 用量浮层**右上角都有 ✕ 关闭按钮**,也可点击浮层外区域或按 Esc 关闭。
|
|
223
227
|
- 状态映射:
|
|
224
228
|
- `authenticated && proxyUp` → 🟢 绿,显示 `WorkBuddy · <昵称>`
|
|
225
229
|
- 否则 → 🔴 红,显示「登录」按钮;`proxyUp` 为 false 时额外提示 `代理未运行`
|
|
@@ -280,16 +284,29 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
|
|
|
280
284
|
# discovery: true # 实时拉取代理的 /v1/models(30s 缓存)
|
|
281
285
|
# models: [...] # 静态目录(代理不可达时的兜底)
|
|
282
286
|
# 每个条目可带 reasoningEffort: 'low'|'medium'|'high'(默认推理等级)
|
|
287
|
+
# 以及 credits: 'x0.06 credits'(显示为模型名后的 ×0.06)
|
|
283
288
|
# loginScript: '' # 登录脚本绝对/相对路径;默认用包内 login_workbuddy.py
|
|
284
289
|
# sessionFile: ~/.codebuddy-session.json # 会话文件路径;默认同上
|
|
285
290
|
```
|
|
286
291
|
|
|
287
292
|
模型列表默认取插件内置目录;代理可达时改为实时拉取 `/v1/models`(支持
|
|
288
293
|
`{"models": [...]}` / `{"data": [...]}` 两种返回),未列出的模型 id 仍可原样
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
传递。**内置目录不再是白名单**:平台新上架的模型刷新后即出现在选择器里,
|
|
295
|
+
无需等插件发版;只有已下架/不可用的模型 id(`RETIRED_MODEL_IDS`,上游返回
|
|
296
|
+
`service info not found`)会被隐藏。每个模型的推理等级(`reasoningEffort`)
|
|
297
|
+
默认来自内置目录,代理可达时优先沿用目录值(代理 `/v1/models` 当前只上报
|
|
298
|
+
`High`,不区分模型)。
|
|
299
|
+
|
|
300
|
+
**积分倍率**:模型名后面会拼上平台声明的积分倍率,例如
|
|
301
|
+
`Deepseek-V4-Flash ×0.06`。倍率来自代理 `/v1/models` 的 `credits` 字段
|
|
302
|
+
(`"x0.06 credits"`,平台写在 WorkBuddy 应用 `product.json` /
|
|
303
|
+
`models_config.json` 里),实时值优先、内置目录兜底;平台没声明的模型
|
|
304
|
+
(如个别新模型尚未写入 `product.json`)只显示模型名,不会猜一个数字。
|
|
305
|
+
|
|
306
|
+
**刷新模型**:⚙️ 设置 → 🔄 刷新模型 会清掉插件的 30s 发现缓存并重新拉取
|
|
307
|
+
代理列表。注意代理自身的模型清单来自本机 WorkBuddy 应用的 `product.json`
|
|
308
|
+
与 `models_config.json`——两者都没有的模型,刷新也不会出现,需先更新应用或
|
|
309
|
+
把模型补进代理配置。
|
|
293
310
|
|
|
294
311
|
---
|
|
295
312
|
|
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 += '<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
var
|
|
116
|
-
var
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
203
|
-
|
|
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
|
-
|
|
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.
|
|
223
|
-
'<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,15 +268,7 @@ 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 =
|
|
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; })
|
|
@@ -260,13 +292,12 @@ function runWidget() {
|
|
|
260
292
|
var note = hasCredit
|
|
261
293
|
? "数据来源:本地用量台账(含上游每次返回的积分消耗;代理不暴露余额接口,无法显示剩余积分)"
|
|
262
294
|
: "数据来源:本地 token 用量台账(" + d.total.requests + " 次累计);当前模型为免费/折扣档,上游回报积分为 0";
|
|
263
|
-
el.
|
|
264
|
-
'<div
|
|
265
|
-
'<div style="margin-top:8px;color:#64748b;font-size:11px">' + esc(note) + "</div>";
|
|
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>";
|
|
266
297
|
})
|
|
267
298
|
.catch(function (err) {
|
|
268
|
-
el.
|
|
269
|
-
'<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>";
|
|
270
301
|
});
|
|
271
302
|
}
|
|
272
303
|
|
|
@@ -282,6 +313,35 @@ function runWidget() {
|
|
|
282
313
|
return (Math.round(v * 100) / 100).toString();
|
|
283
314
|
}
|
|
284
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
|
+
|
|
285
345
|
/** 手动领取今日签到(幂等:官方对已签到返回业务拒绝,不会出错)。 */
|
|
286
346
|
function onCheckin() {
|
|
287
347
|
var btn = host.querySelector("#wb-checkin");
|
package/lib/index.js
CHANGED
|
@@ -77,26 +77,53 @@ const DISCOVERY_TIMEOUT_MS = 2_000;
|
|
|
77
77
|
* CodeBuddy/WorkBuddy platform (from workbuddy2api's models_config.json).
|
|
78
78
|
* It is advisory and replaced by the live `/v1/models` answer whenever the
|
|
79
79
|
* proxy is reachable; entries not announced by the proxy are still listed.
|
|
80
|
+
*
|
|
81
|
+
* `credits` is the platform's declared multiplier (`"x0.06 credits"`), shown
|
|
82
|
+
* after the model name. The live answer wins when it carries one — this copy
|
|
83
|
+
* only covers the proxy-down / discovery-off fallback.
|
|
80
84
|
*/
|
|
81
85
|
const DEFAULT_MODELS = [
|
|
82
|
-
{ id: "deepseek-v4-
|
|
83
|
-
{ id: "deepseek-v4-
|
|
84
|
-
{ id: "deepseek-
|
|
85
|
-
{ id: "
|
|
86
|
-
{ id: "glm-5.
|
|
87
|
-
{ id: "glm-
|
|
88
|
-
{ id: "
|
|
89
|
-
{ id: "kimi-
|
|
90
|
-
{ id: "kimi-k2.
|
|
91
|
-
{ id: "kimi-k2.
|
|
92
|
-
{ id: "
|
|
93
|
-
{ id: "
|
|
94
|
-
{ id: "
|
|
86
|
+
{ id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1_000_000, maxTokens: 50_000, inputModalities: ["text", "image"], reasoningEffort: "high" },
|
|
87
|
+
{ id: "deepseek-v4-pro", name: "Deepseek-V4-Pro", contextWindow: 1_000_000, maxTokens: 50_000, inputModalities: ["text", "image"], reasoningEffort: "high", credits: "x0.16 credits" },
|
|
88
|
+
{ id: "deepseek-v4-flash", name: "Deepseek-V4-Flash", contextWindow: 1_000_000, maxTokens: 50_000, inputModalities: ["text", "image"], reasoningEffort: "high", credits: "x0.06 credits" },
|
|
89
|
+
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2", contextWindow: 96_000, maxTokens: 32_000, reasoningEffort: "medium", credits: "x0.29 credits" },
|
|
90
|
+
{ id: "glm-5.2", name: "GLM-5.2", contextWindow: 1_000_000, maxTokens: 48_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x0.79 credits" },
|
|
91
|
+
{ id: "glm-5.1", name: "GLM-5.1", contextWindow: 200_000, maxTokens: 48_000, reasoningEffort: "medium", credits: "x0.79 credits" },
|
|
92
|
+
{ id: "glm-5v-turbo", name: "GLM-5v-Turbo", contextWindow: 200_000, maxTokens: 64_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x0.95 credits" },
|
|
93
|
+
{ id: "kimi-k3-1", name: "Kimi-K3", contextWindow: 1_000_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x1.62 credits" },
|
|
94
|
+
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code", contextWindow: 256_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x0.57 credits" },
|
|
95
|
+
{ id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x0.52 credits" },
|
|
96
|
+
{ id: "kimi-k2.5", name: "Kimi-K2.5", contextWindow: 164_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x0.45 credits" },
|
|
97
|
+
{ id: "minimax-m3", name: "MiniMax-M3", contextWindow: 512_000, maxTokens: 128_000, inputModalities: ["text", "image"], reasoningEffort: "medium", credits: "x0.25 credits" },
|
|
98
|
+
{ id: "hy3", name: "Hy3", contextWindow: 192_000, maxTokens: 64_000, inputModalities: ["text", "image"], reasoningEffort: "high", credits: "x0.00 credits" },
|
|
99
|
+
{ id: "hunyuan-2.0-thinking", name: "Hunyuan-2.0-Thinking", contextWindow: 128_000, maxTokens: 24_000, reasoningEffort: "medium", credits: "x0.04 credits" },
|
|
95
100
|
{ id: "hunyuan-chat", name: "Hunyuan-Turbos", contextWindow: 200_000, maxTokens: 8_192 },
|
|
96
101
|
{ id: "auto", name: "Auto", contextWindow: 168_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "high" },
|
|
97
|
-
{ id: "default", name: "Default", contextWindow: 200_000, maxTokens: 24_000 },
|
|
102
|
+
{ id: "default", name: "Default", contextWindow: 200_000, maxTokens: 24_000, credits: "x2.00 credits" },
|
|
98
103
|
];
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Models the proxy still announces but that the upstream platform rejects with
|
|
107
|
+
* `service info not found` (each id re-probed 2026-09-10 against the live
|
|
108
|
+
* endpoint). The catalog is no longer a whitelist — a model the platform
|
|
109
|
+
* launches shows up after a refresh without a plugin update — so dead ids are
|
|
110
|
+
* hidden here by name instead.
|
|
111
|
+
*/
|
|
112
|
+
const RETIRED_MODEL_IDS = new Set([
|
|
113
|
+
"minimax-m2.5",
|
|
114
|
+
"glm-5.0",
|
|
115
|
+
"glm-4.7",
|
|
116
|
+
"glm-4.6",
|
|
117
|
+
"glm-4.6v",
|
|
118
|
+
"kimi-k2-thinking",
|
|
119
|
+
"deepseek-v3-1",
|
|
120
|
+
"deepseek-v3-1-volc",
|
|
121
|
+
"kimi-k2-instruct-taiji",
|
|
122
|
+
"completion-gf",
|
|
123
|
+
"default-1.1",
|
|
124
|
+
"default-1.2",
|
|
125
|
+
]);
|
|
126
|
+
|
|
100
127
|
/** Selectable reasoning efforts exposed to the harness UI, in display order. */
|
|
101
128
|
const REASONING_EFFORTS = [
|
|
102
129
|
{ id: "low", name: "Low" },
|
|
@@ -156,6 +183,26 @@ function liveInputModalities(entry) {
|
|
|
156
183
|
return valid.length > 0 ? [...new Set(valid)] : ["text"];
|
|
157
184
|
}
|
|
158
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Parse the platform's credit declaration into the `×0.06` suffix appended to
|
|
188
|
+
* a model name. The platform writes these as `"x0.06 credits"` (product.json /
|
|
189
|
+
* models_config.json), and the proxy relays the string verbatim; a bare number
|
|
190
|
+
* or an already-compact `×1.62` also parses. Anything without a recognizable
|
|
191
|
+
* number yields `undefined`, so callers omit the suffix rather than guess —
|
|
192
|
+
* an unknown multiplier must not be rendered as 0.
|
|
193
|
+
*/
|
|
194
|
+
function creditsMultiplier(value) {
|
|
195
|
+
if (typeof value !== "string") return undefined;
|
|
196
|
+
const match = /^[x×*]?\s*(\d+(?:\.\d+)?)\s*(?:credits?)?$/i.exec(value.trim());
|
|
197
|
+
return match === null ? undefined : `×${match[1]}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Append the credit multiplier to a model's display name, when declared. */
|
|
201
|
+
function withCredits(name, credits) {
|
|
202
|
+
const multiplier = creditsMultiplier(credits);
|
|
203
|
+
return multiplier === undefined ? name : `${name} ${multiplier}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
159
206
|
|
|
160
207
|
// #region serialize
|
|
161
208
|
|
|
@@ -580,7 +627,7 @@ function modelInfo(provider, model) {
|
|
|
580
627
|
return {
|
|
581
628
|
provider,
|
|
582
629
|
id: model.id,
|
|
583
|
-
name: model.name ?? model.id,
|
|
630
|
+
name: withCredits(model.name ?? model.id, model.credits),
|
|
584
631
|
...(model.description === undefined ? {} : { description: model.description }),
|
|
585
632
|
inputModalities: model.inputModalities ?? ["text"],
|
|
586
633
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
@@ -673,6 +720,10 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
673
720
|
description: typeof entry.description === "string" && entry.description.length > 0
|
|
674
721
|
? entry.description
|
|
675
722
|
: undefined,
|
|
723
|
+
// Platform-declared credit multiplier, relayed by the proxy.
|
|
724
|
+
credits: typeof entry.credits === "string" && entry.credits.trim().length > 0
|
|
725
|
+
? entry.credits.trim()
|
|
726
|
+
: undefined,
|
|
676
727
|
modalities: liveInputModalities(entry),
|
|
677
728
|
contextWindow: Number.isInteger(entry.context_window) && entry.context_window > 0
|
|
678
729
|
? entry.context_window
|
|
@@ -689,19 +740,23 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
689
740
|
if (seen.has(entry.id)) continue;
|
|
690
741
|
seen.add(entry.id);
|
|
691
742
|
const catalog = byId.get(entry.id);
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
if (catalog === undefined) continue;
|
|
743
|
+
// Retired ids are hidden by name (RETIRED_MODEL_IDS) rather than by
|
|
744
|
+
// absence from the catalog: the proxy still announces models upstream
|
|
745
|
+
// rejects, but a model the platform launches must appear after a
|
|
746
|
+
// refresh without waiting for a new plugin release. The catalog is now
|
|
747
|
+
// only a metadata overlay plus a fallback for unannounced entries.
|
|
748
|
+
if (RETIRED_MODEL_IDS.has(entry.id)) continue;
|
|
699
749
|
const effort = liveReasoningEffort(entry, catalog?.reasoningEffort);
|
|
700
750
|
const reasoning = modelReasoningInfo(effort);
|
|
701
751
|
merged.push({
|
|
702
752
|
provider,
|
|
703
753
|
id: entry.id,
|
|
704
|
-
|
|
754
|
+
// The proxy's multiplier tracks the platform; the catalog's is a
|
|
755
|
+
// snapshot, so the live value wins and the catalog only fills gaps.
|
|
756
|
+
name: withCredits(
|
|
757
|
+
entry.name ?? catalog?.name ?? entry.id,
|
|
758
|
+
entry.credits ?? catalog?.credits,
|
|
759
|
+
),
|
|
705
760
|
...(catalog?.description !== undefined || entry.description !== undefined
|
|
706
761
|
? { description: catalog?.description ?? entry.description }
|
|
707
762
|
: {}),
|
|
@@ -843,6 +898,9 @@ const catalogModel = z.object({
|
|
|
843
898
|
maxTokens: z.number().step(1).min(1),
|
|
844
899
|
inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"]),
|
|
845
900
|
reasoningEffort: z.string(),
|
|
901
|
+
// Platform-declared credit multiplier, e.g. "x0.06 credits"; displayed after
|
|
902
|
+
// the model name as "×0.06".
|
|
903
|
+
credits: z.string(),
|
|
846
904
|
});
|
|
847
905
|
|
|
848
906
|
export const Config = z.object({
|
|
@@ -890,6 +948,10 @@ function resolveModels(models) {
|
|
|
890
948
|
if (reasoningEffort !== undefined && !REASONING_EFFORT_IDS.has(reasoningEffort)) {
|
|
891
949
|
throw new Error(`dsh-llm-workbuddy: catalog model "${model.id}" reasoningEffort must be one of low|medium|high`);
|
|
892
950
|
}
|
|
951
|
+
const credits = typeof model.credits === "string" ? model.credits.trim() : undefined;
|
|
952
|
+
if (credits !== undefined && credits.length > 0 && creditsMultiplier(credits) === undefined) {
|
|
953
|
+
throw new Error(`dsh-llm-workbuddy: catalog model "${model.id}" credits must read like "x0.06 credits"`);
|
|
954
|
+
}
|
|
893
955
|
return {
|
|
894
956
|
id: model.id,
|
|
895
957
|
...(model.name === undefined ? {} : { name: model.name }),
|
|
@@ -898,6 +960,7 @@ function resolveModels(models) {
|
|
|
898
960
|
...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
|
|
899
961
|
inputModalities: [...inputModalities],
|
|
900
962
|
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
963
|
+
...(credits === undefined || credits.length === 0 ? {} : { credits }),
|
|
901
964
|
};
|
|
902
965
|
});
|
|
903
966
|
}
|
|
@@ -1234,10 +1297,19 @@ async function probeChatWorking(baseURL) {
|
|
|
1234
1297
|
return { chatWorking: false, chatError: `HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}` };
|
|
1235
1298
|
}
|
|
1236
1299
|
const body = await response.json().catch(() => null);
|
|
1237
|
-
const
|
|
1300
|
+
const choice = body?.choices?.[0];
|
|
1301
|
+
const content = choice?.message?.content;
|
|
1238
1302
|
if (typeof content === "string" && content.length > 0) {
|
|
1239
1303
|
return { chatWorking: true };
|
|
1240
1304
|
}
|
|
1305
|
+
// A reasoning model spends the whole 4-token cap on thinking and answers
|
|
1306
|
+
// with `content: ""` + `finish_reason: "length"` — that is a healthy proxy,
|
|
1307
|
+
// not an empty response (probed against hy3, which is all-thinking at this
|
|
1308
|
+
// cap). Any billed completion token proves the upstream round trip worked.
|
|
1309
|
+
const completionTokens = body?.usage?.completion_tokens;
|
|
1310
|
+
if (choice !== undefined && typeof completionTokens === "number" && completionTokens > 0) {
|
|
1311
|
+
return { chatWorking: true };
|
|
1312
|
+
}
|
|
1241
1313
|
return { chatWorking: false, chatError: "代理返回了空响应" };
|
|
1242
1314
|
} catch (error) {
|
|
1243
1315
|
return { chatWorking: false, chatError: String(error?.message ?? error) };
|
package/package.json
CHANGED