cortico-world-workbuddy 0.1.5 → 0.1.6

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.1.6
4
+
5
+ 新增「用量」面板与真实的时段优惠计算(只在本机验证,未发布)。
6
+
7
+ - **优惠来自 `/v3/config`**:改用应用身份 UA(`WorkBuddy/5.8.3 … CLI/2.161.0`)拉取目录,
8
+ 同时拿到 `modelPromotions` / `modelTiers`。用 CLI 身份只能读到精简后的目录,
9
+ 促销与档位字段整个缺失——这是之前倍率「永远显示原价」的根因。
10
+ - **新增 `src/promo.js`**:解析 `schedule`(`timezone` / `validFrom` / `validUntil` / 跨零点的 `daily`
11
+ 时段、`dailyUnusable`)、`discount`(`discountedCredits` 就是生效倍率本身,不是相对系数)、
12
+ `badge`(含 `display:"activeOnly"`)与 `priority`;按优先级选出此刻生效的那条。
13
+ `priceViewOf(model, now)` 给出 `{base, effective, promoId, badge, windowText, hoverText, isFree}`。
14
+ - **新增「用量」面板**(`world:workbuddy` 第 4 个页签):区间概览(今天 / 近 7 天 / 近 30 天)、
15
+ 按模型聚合、积分包明细、限时优惠清单、最近调用明细;带「刷新余额」与「清空本地账本」。
16
+ - **新增 `src/usage.js`**:每次 `wb_chat` 往 `<dataDir>/workbuddy-usage.jsonl` 记一行
17
+ (tokens 来自 SSE 的 `usage`,积分来自 `usage.credit` 即上游真实扣费,倍率记当时生效值),
18
+ 读的时候裁掉 90 天以前的行;余额快照另存 `workbuddy-balance.json`。
19
+ - `fetchCredits` 现在返回 `{total, used, packages[]}`(逐包剩余 / 已用 / 周期),
20
+ `wb_credits` 一并打印「本周期已用」。
21
+ - `wb_chat` 回执补充「用量:输入 N tok / 输出 N tok / 缓存命中 N tok / 共 N tok / 本次消耗积分 N(N ms)」。
22
+ - `wb_list_models` 标注此刻生效倍率(含原价对比、时段)与优惠徽标;`wb_select_model` 提示此刻倍率。
23
+ - 环境提示词新增 `{{workbuddy.todayCredit}}` / `{{workbuddy.todayTokens}}`,并在倍率旁说明已含生效中的限时优惠。
24
+ - 详情页徽标增加「今日积分」。
25
+ - 仓库补 `package-lock.json`(锁定 esbuild 0.24.2),CI 改为 `npm ci`:`dist` 的构建产物必须逐字节稳定,否则「dist 未同步」的检查会误报。
26
+
3
27
  ## 0.1.5
4
28
 
5
29
  发布前收尾:停用 / 卸载彻底不留痕,并把元数据指向真实仓库。
package/README.md CHANGED
@@ -139,18 +139,38 @@ Coopanion (Coo)
139
139
 
140
140
  | 面板 | 内容 |
141
141
  |---|---|
142
- | 模型目录 | 刷新目录;逐模型一行:名称/id、**默认积分倍率**(x0 标「免费」)、上下文、推理档、「选用」按钮 |
142
+ | 模型目录 | 刷新目录;逐模型一行:名称/id、**此刻倍率**(含限时折扣时显示 `x0.5 ← x0.79 23:00–07:50`,x0 标「免费」)、上下文、推理档、「选用」按钮 |
143
143
  | 账号 | 登录态来源(本机发现 / 手动)、掩码 token、积分余额查询、重新检测、手动 token 存取 |
144
144
  | 是否启用 WorkBuddy | 「中转站优先」开关 + 主进程状态,与开始页按钮同步 |
145
+ | 用量 | 区间概览(今天 / 近 7 天 / 近 30 天)、按模型聚合、积分包明细、限时优惠清单、最近调用;带「刷新余额」「清空本地账本」 |
146
+
147
+ ### 倍率与限时优惠怎么算
148
+
149
+ - 目录来自 `GET /v3/config`(应用身份 UA):一次同时拿到 `models`、`modelPromotions`、`modelTiers`。
150
+ 用 CLI 身份只能读到精简目录,促销字段会整段缺失——这是「倍率永远显示原价」的根因。
151
+ - 一条促销的生效条件是 `schedule`:`timezone`(默认按条目给的时区算)、`validFrom`/`validUntil`、
152
+ `daily` 时段(支持 `23:00`→`07:50` 这类跨零点窗口)。命中的条目按 `priority` 取最高;平手时促销优先于档位。
153
+ - `discount.discountedCredits`(如 `0.50x`)**就是生效倍率本身**,不是相对原价的系数。
154
+ `displayMode: "replace"` 直接替换显示,`"strikethrough"` 表示「划线原价 + 新价」并排。
155
+ - `badge.display: "activeOnly"` 的徽标只在生效时段出现(如「限时免费」试用)。
156
+
157
+ ### 用量统计口径
158
+
159
+ - 每次 `wb_chat` 记一行到 `%APPDATA%\..\Coopanion\data\home\companion\data\workbuddy-usage.jsonl`
160
+ (实机路径;实际就是 `ctx.dataDir`):`ts/model/region/ok/promptTokens/completionTokens/totalTokens/cachedTokens/reasoningTokens/credit/multiplier/promoId/source/durationMs`。
161
+ - `credit` 取 SSE 最后一段的 `usage.credit`,即**上游真实扣费**;倍率 0.11 的模型单次请求不足 1 分时上游就是 0,这里照抄不折算。
162
+ - 积分包余额另存 `workbuddy-balance.json`(`POST /v2/billing/meter/get-user-resource` 的 `*Precise` 字段求和),
163
+ 面板上的「刷新余额」会重取一次。
164
+ - 账本只保留 90 天,读的时候顺带裁掉;「清空本地账本」会把它清成空文件,服务端数据不受影响。
145
165
 
146
166
  ## 工具(给 Coo / 模型调用)
147
167
 
148
168
  | 工具 | 作用 |
149
169
  |---|---|
150
- | `wb_list_models` | 列出目录中全部模型及各自默认积分倍率(只读) |
151
- | `wb_select_model` | 按 id 选择 `wb_chat` 的默认模型,持久化 |
152
- | `wb_chat` | 用选中模型回答一个单轮问题(**真实消耗积分**,需主进程在跑) |
153
- | `wb_credits` | 只读查询积分余额(需主进程在跑) |
170
+ | `wb_list_models` | 列出目录中全部模型及**此刻倍率**(含限时优惠与时段),并单列优惠清单(只读) |
171
+ | `wb_select_model` | 按 id 选择 `wb_chat` 的默认模型,持久化;提示此刻倍率 |
172
+ | `wb_chat` | 用选中模型回答一个单轮问题(**真实消耗积分**,需主进程在跑);回执附输入/输出/缓存 token 与本次积分 |
173
+ | `wb_credits` | 只读查询积分余额与本周期已用(需主进程在跑) |
154
174
 
155
175
  ## 配置(`worlds.workbuddy.*`)
156
176
 
package/describe.json CHANGED
@@ -98,5 +98,5 @@
98
98
  }
99
99
  }
100
100
  ],
101
- "envPrompt": "## WorkBuddy bridge\n\nThis machine has a WorkBuddy desktop sign-in bridged in (region: {{workbuddy.region}}; signed in: {{workbuddy.authed}}).\nThe model catalog holds {{workbuddy.catalogSize}} models. The currently selected model is `{{workbuddy.model}}` with default credit multiplier {{workbuddy.multiplier}}.\nWorkBuddy desktop main process: {{workbuddy.process}}. Relay-first switch: {{workbuddy.priority}}.\n\n- Use `wb_list_models` to see every model with its multiplier before choosing; prefer low-multiplier models for casual tasks.\n- Use `wb_select_model` when the user asks to switch the WorkBuddy model.\n- `wb_chat` only works while the WorkBuddy desktop main process is running; otherwise it returns「未检测到 WorkBuddy 主进程」. Every call spends the user's own credits at that model's multiplier.\n- `wb_credits` is read-only and costs nothing (also requires the desktop process).\n\n### Relay first: {{workbuddy.priority}}\n\n- Switch `on` **and** process `running`: every user turn that expects a conversational answer (question, chat, writing, reasoning, rewrite, translation, summary) must be answered by a WorkBuddy model first — call `wb_chat` (omit `model` to use `{{workbuddy.model}}`), then speak the returned answer back in your own voice and name the model that answered. Only fall back to your own model if the call fails.\n- Switch `off` (default): do not call `wb_chat`; answer with your own model. If the user explicitly asks for WorkBuddy, `wb_chat` is still allowed — the desktop process must be running.\n- Tool errors and empty answers are not a reason to flip the switch; just say what happened.\n"
101
+ "envPrompt": "## WorkBuddy bridge\n\nThis machine has a WorkBuddy desktop sign-in bridged in (region: {{workbuddy.region}}; signed in: {{workbuddy.authed}}).\nThe model catalog holds {{workbuddy.catalogSize}} models. The currently selected model is `{{workbuddy.model}}` with default credit multiplier {{workbuddy.multiplier}}.\nWorkBuddy desktop main process: {{workbuddy.process}}. Relay-first switch: {{workbuddy.priority}}.\nSpent today through the bridge: {{workbuddy.todayCredit}} credits / {{workbuddy.todayTokens}} tokens (locally recorded).\nThe multiplier above already includes any time-limited discount or free window that is active right now; `wb_list_models` also marks which offers are active and their hours.\n\n- Use `wb_list_models` to see every model with its multiplier before choosing; prefer low-multiplier models for casual tasks.\n- Use `wb_select_model` when the user asks to switch the WorkBuddy model.\n- `wb_chat` only works while the WorkBuddy desktop main process is running; otherwise it returns「未检测到 WorkBuddy 主进程」. Every call spends the user's own credits at that model's multiplier.\n- `wb_credits` is read-only and costs nothing (also requires the desktop process).\n\n### Relay first: {{workbuddy.priority}}\n\n- Switch `on` **and** process `running`: every user turn that expects a conversational answer (question, chat, writing, reasoning, rewrite, translation, summary) must be answered by a WorkBuddy model first — call `wb_chat` (omit `model` to use `{{workbuddy.model}}`), then speak the returned answer back in your own voice and name the model that answered. Only fall back to your own model if the call fails.\n- Switch `off` (default): do not call `wb_chat`; answer with your own model. If the user explicitly asks for WorkBuddy, `wb_chat` is still allowed — the desktop process must be running.\n- Tool errors and empty answers are not a reason to flip the switch; just say what happened.\n"
102
102
  }
package/dist/webui.js CHANGED
@@ -48,7 +48,36 @@ var TEXT = {
48
48
  repair: "\u91CD\u65B0\u6CE8\u5165\u5F00\u59CB\u9875\u6309\u94AE",
49
49
  repairing: "\u6B63\u5728\u91CD\u65B0\u6CE8\u5165\u2026",
50
50
  repaired: "\u5DF2\u4FEE\u590D\uFF0C\u91CD\u65B0\u6253\u5F00\u63A7\u5236\u53F0\u5373\u53EF\u770B\u5230\u6309\u94AE",
51
- repairFail: "\u4FEE\u590D\u5931\u8D25"
51
+ repairFail: "\u4FEE\u590D\u5931\u8D25",
52
+ usageTitle: "\u7528\u91CF",
53
+ usageDesc: "\u672C\u673A\u8BB0\u8D26\u7684\u8C03\u7528\u7528\u91CF\u4E0E WorkBuddy \u79EF\u5206\u5305\u4F59\u989D\u3002\u79EF\u5206\u53D6\u4E0A\u6E38\u6BCF\u6B21\u8C03\u7528\u56DE\u4F20\u7684\u771F\u5B9E\u6263\u8D39\uFF08\u53EF\u80FD\u4E3A 0\uFF09\uFF0C\u500D\u7387\u6309\u76EE\u5F55 + \u9650\u65F6\u4F18\u60E0\u5B9E\u65F6\u8BA1\u7B97\u3002",
54
+ usageRefresh: "\u5237\u65B0\u4F59\u989D",
55
+ usageClear: "\u6E05\u7A7A\u672C\u5730\u8D26\u672C",
56
+ usageCleared: "\u672C\u5730\u8D26\u672C\u5DF2\u6E05\u7A7A",
57
+ metricCalls: "\u8C03\u7528",
58
+ metricCredit: "\u79EF\u5206",
59
+ metricInput: "\u8F93\u5165 tok",
60
+ metricOutput: "\u8F93\u51FA tok",
61
+ spanToday: "\u4ECA\u5929",
62
+ spanWeek: "\u8FD1 7 \u5929",
63
+ spanMonth: "\u8FD1 30 \u5929",
64
+ byModel: "\u6309\u6A21\u578B",
65
+ balanceTitle: "\u79EF\u5206\u5305\u4F59\u989D",
66
+ colPkgName: "\u79EF\u5206\u5305",
67
+ colPkgRemain: "\u5269\u4F59",
68
+ colPkgUsed: "\u5DF2\u7528",
69
+ colPkgCycle: "\u5468\u671F",
70
+ promosTitle: "\u9650\u65F6\u4F18\u60E0 / \u514D\u8D39",
71
+ promoActive: "\u751F\u6548\u4E2D",
72
+ promoSoon: "\u672A\u751F\u6548",
73
+ recentTitle: "\u6700\u8FD1\u8C03\u7528",
74
+ colTime: "\u65F6\u95F4",
75
+ colTokens: "token",
76
+ colDuration: "\u8017\u65F6",
77
+ noData: "\u6682\u65E0\u6570\u636E",
78
+ totalCredit: "\u5408\u8BA1\u79EF\u5206",
79
+ todayCredit: "\u4ECA\u65E5\u79EF\u5206",
80
+ todayTokens: "\u4ECA\u65E5 token"
52
81
  },
53
82
  en: {
54
83
  refresh: "Refresh from WorkBuddy",
@@ -98,16 +127,55 @@ var TEXT = {
98
127
  repair: "Re-inject the home button",
99
128
  repairing: "Re-injecting\u2026",
100
129
  repaired: "Fixed. Reopen the console to see the button",
101
- repairFail: "Repair failed"
130
+ repairFail: "Repair failed",
131
+ usageTitle: "Usage",
132
+ usageDesc: "Locally recorded call usage plus the WorkBuddy credit balance. Credits come from the per-call figure the upstream returns (may be 0); multipliers combine the catalog with time-limited offers in real time.",
133
+ usageRefresh: "Refresh balance",
134
+ usageClear: "Clear local ledger",
135
+ usageCleared: "Local ledger cleared",
136
+ metricCalls: "Calls",
137
+ metricCredit: "Credits",
138
+ metricInput: "Input tok",
139
+ metricOutput: "Output tok",
140
+ spanToday: "Today",
141
+ spanWeek: "Last 7 days",
142
+ spanMonth: "Last 30 days",
143
+ byModel: "By model",
144
+ balanceTitle: "Credit packages",
145
+ colPkgName: "Package",
146
+ colPkgRemain: "Remaining",
147
+ colPkgUsed: "Used",
148
+ colPkgCycle: "Cycle",
149
+ promosTitle: "Limited-time offers / free",
150
+ promoActive: "active",
151
+ promoSoon: "scheduled",
152
+ recentTitle: "Recent calls",
153
+ colTime: "Time",
154
+ colTokens: "tokens",
155
+ colDuration: "Latency",
156
+ noData: "No data",
157
+ totalCredit: "Total credits",
158
+ todayCredit: "Credits today",
159
+ todayTokens: "Tokens today"
102
160
  }
103
161
  };
104
162
  function strings(ctx) {
105
163
  return TEXT[ctx.language === "en" ? "en" : "zh"];
106
164
  }
107
165
  function fmtMultiplier(m, s) {
108
- if (typeof m.creditMultiplier !== "number") return { text: "\u2014", free: false };
109
- if (m.creditMultiplier === 0) return { text: `x0 \xB7 ${s.multiplierFree}`, free: true };
110
- return { text: `x${m.creditMultiplier}`, free: false };
166
+ const price = m?.price;
167
+ const base = typeof price?.base === "number" ? price.base : typeof m?.creditMultiplier === "number" ? m.creditMultiplier : null;
168
+ const eff = typeof price?.effective === "number" ? price.effective : base;
169
+ if (base === null) return { text: "\u2014", free: false, promo: "" };
170
+ if (eff === 0) return { text: `x0 \xB7 ${s.multiplierFree}${price?.windowText ? ` ${price.windowText}` : ""}`, free: true, promo: price?.badge ?? "" };
171
+ if (typeof eff === "number" && eff < base) {
172
+ return {
173
+ text: `x${eff} \u2190 x${base}${price?.windowText ? ` ${price.windowText}` : ""}`,
174
+ free: false,
175
+ promo: price?.badge ?? ""
176
+ };
177
+ }
178
+ return { text: `x${base}`, free: false, promo: price?.badge ?? "" };
111
179
  }
112
180
  function fmtTs(ms, s) {
113
181
  if (!ms) return s.notRefreshed;
@@ -157,6 +225,7 @@ function mountModels(ctx) {
157
225
  nameCell.append(sub);
158
226
  if (m.description) nameCell.title = m.description;
159
227
  const mulPill = ui.pill(mul.text, mul.free ? "on" : "plain");
228
+ if (mul.promo) mulPill.title = mul.promo;
160
229
  const btn = ui.button(isCurrent ? s.selected : s.select, {
161
230
  size: "sm",
162
231
  variant: isCurrent ? "plain" : "primary",
@@ -387,11 +456,168 @@ function mountPriority(ctx) {
387
456
  statusLine.textContent = String(err?.message ?? err);
388
457
  });
389
458
  }
459
+ function fmtNum(n) {
460
+ if (typeof n !== "number" || !Number.isFinite(n)) return "\u2014";
461
+ return String(Math.round(n * 100) / 100);
462
+ }
463
+ function mountUsage(ctx) {
464
+ const s = strings(ctx);
465
+ const { ui } = ctx;
466
+ const sheet = ui.sheet({ title: `${s.usageTitle} \xB7 WorkBuddy`, desc: s.usageDesc });
467
+ ctx.root.append(sheet.el);
468
+ const bar = ui.rowbar();
469
+ const refreshBtn = ui.button(s.usageRefresh, { variant: "primary", size: "sm" });
470
+ const clearBtn = ui.button(s.usageClear, { variant: "plain", size: "sm" });
471
+ const todayChip = ui.chip("", "accent");
472
+ const balanceChip = ui.chip("", "plain");
473
+ bar.append(refreshBtn, clearBtn, todayChip, balanceChip);
474
+ sheet.body.append(bar);
475
+ const errLine = ui.msgline();
476
+ sheet.body.append(errLine);
477
+ const overview = ui.table({
478
+ head: ["", s.metricCalls, s.metricCredit, s.metricInput, s.metricOutput],
479
+ maxHeight: "220px"
480
+ });
481
+ sheet.body.append(overview.el);
482
+ const modelSection = ui.section(s.byModel);
483
+ const modelTable = ui.table({
484
+ head: [s.colModel, s.metricCalls, s.metricCredit, s.metricInput, s.metricOutput],
485
+ maxHeight: "260px"
486
+ });
487
+ modelSection.append(modelTable.el);
488
+ sheet.body.append(modelSection);
489
+ const pkgSection = ui.section(s.balanceTitle);
490
+ const pkgTable = ui.table({
491
+ head: [s.colPkgName, s.colPkgRemain, s.colPkgUsed, s.colPkgCycle],
492
+ maxHeight: "220px"
493
+ });
494
+ pkgSection.append(pkgTable.el);
495
+ sheet.body.append(pkgSection);
496
+ const promoSection = ui.section(s.promosTitle);
497
+ const promoWrap = ui.h("div");
498
+ promoSection.append(promoWrap);
499
+ sheet.body.append(promoSection);
500
+ const recentSection = ui.section(s.recentTitle);
501
+ const recentTable = ui.table({
502
+ head: [s.colTime, s.colModel, s.colTokens, s.metricCredit, s.colDuration],
503
+ maxHeight: "300px"
504
+ });
505
+ recentSection.append(recentTable.el);
506
+ sheet.body.append(recentSection);
507
+ let state = null;
508
+ function spanRow(label, bucket) {
509
+ const b = bucket ?? {};
510
+ return [label, String(b.calls ?? 0), fmtNum(b.credit), fmtNum(b.prompt), fmtNum(b.completion)];
511
+ }
512
+ function render() {
513
+ const totals = state?.summary?.totals ?? {};
514
+ todayChip.textContent = `${s.todayCredit}: ${fmtNum(totals.today?.credit)} \xB7 ${s.todayTokens}: ${fmtNum(
515
+ (totals.today?.prompt ?? 0) + (totals.today?.completion ?? 0)
516
+ )}`;
517
+ balanceChip.textContent = `${s.balanceTitle}: ${fmtNum(state?.balance?.total)}`;
518
+ const spans = [spanRow(s.spanToday, totals.today), spanRow(s.spanWeek, totals.week), spanRow(s.spanMonth, totals.month)];
519
+ overview.clear();
520
+ for (const row of spans) overview.addRow(row);
521
+ const byModel = state?.summary?.byModel ?? [];
522
+ modelTable.clear(byModel.length > 0 ? void 0 : s.noData);
523
+ for (const m of byModel) {
524
+ modelTable.addRow([m.model, String(m.calls ?? 0), fmtNum(m.credit), fmtNum(m.prompt), fmtNum(m.completion)]);
525
+ }
526
+ const packages = state?.balance?.packages ?? [];
527
+ pkgTable.clear(packages.length > 0 ? void 0 : s.noData);
528
+ for (const p of packages) {
529
+ const cycle = [p.cycleStart, p.cycleEnd].filter(Boolean).join(" \u2192 ") || "\u2014";
530
+ pkgTable.addRow([p.name ?? "\u2014", fmtNum(p.remain), fmtNum(p.used), cycle]);
531
+ }
532
+ promoWrap.replaceChildren();
533
+ const promos = state?.promos ?? [];
534
+ if (promos.length === 0) {
535
+ promoWrap.append(ui.h("div", "dim", s.noData));
536
+ }
537
+ for (const p of promos) {
538
+ const row = ui.h("div");
539
+ row.style.display = "flex";
540
+ row.style.gap = "8px";
541
+ row.style.alignItems = "center";
542
+ row.style.padding = "4px 0";
543
+ row.append(ui.pill(p.badge || p.id, p.active ? "on" : "plain"));
544
+ const text = [
545
+ p.discountText || p.hoverText,
546
+ p.windowText || p.absoluteText,
547
+ (p.models ?? []).join(" / "),
548
+ p.active ? s.promoActive : s.promoSoon
549
+ ].filter(Boolean).join(" \xB7 ");
550
+ const label = ui.h("div", "dim", text);
551
+ label.style.fontSize = "12px";
552
+ if (p.hoverText) label.title = p.hoverText;
553
+ row.append(label);
554
+ promoWrap.append(row);
555
+ }
556
+ const recent = state?.summary?.recent ?? [];
557
+ recentTable.clear(recent.length > 0 ? void 0 : s.noData);
558
+ for (const r of recent) {
559
+ const when = (() => {
560
+ try {
561
+ return new Date(r.ts).toLocaleString();
562
+ } catch {
563
+ return String(r.ts);
564
+ }
565
+ })();
566
+ recentTable.addRow([
567
+ when,
568
+ r.model ?? "\u2014",
569
+ fmtNum(r.totalTokens ?? (r.promptTokens ?? 0) + (r.completionTokens ?? 0)),
570
+ r.ok === false ? "\u2014" : fmtNum(r.credit),
571
+ typeof r.durationMs === "number" ? `${r.durationMs} ms` : "\u2014"
572
+ ]);
573
+ }
574
+ }
575
+ async function load() {
576
+ state = await ctx.invoke("state");
577
+ errLine.textContent = state?.ok === false ? state.error ?? "" : "";
578
+ render();
579
+ }
580
+ refreshBtn.addEventListener("click", async () => {
581
+ const off = ui.disable(refreshBtn);
582
+ refreshBtn.textContent = s.refreshing;
583
+ try {
584
+ state = await ctx.invoke("refresh");
585
+ render();
586
+ ui.toast(`${s.balanceTitle}: ${fmtNum(state?.balance?.total)}`);
587
+ } catch (err) {
588
+ errLine.textContent = String(err?.message ?? err);
589
+ ui.toast(String(err?.message ?? err), "bad");
590
+ } finally {
591
+ refreshBtn.textContent = s.usageRefresh;
592
+ off.dispose();
593
+ }
594
+ });
595
+ clearBtn.addEventListener("click", async () => {
596
+ const off = ui.disable(clearBtn);
597
+ try {
598
+ state = await ctx.invoke("clear");
599
+ render();
600
+ ui.toast(s.usageCleared);
601
+ } catch (err) {
602
+ ui.toast(String(err?.message ?? err), "bad");
603
+ } finally {
604
+ off.dispose();
605
+ }
606
+ });
607
+ ctx.stream({
608
+ message: () => load().catch(() => {
609
+ })
610
+ });
611
+ load().catch((err) => {
612
+ errLine.textContent = String(err?.message ?? err);
613
+ });
614
+ }
390
615
  var client_default = {
391
616
  panels: {
392
617
  models: { mount: mountModels },
393
618
  account: { mount: mountAccount },
394
- priority: { mount: mountPriority }
619
+ priority: { mount: mountPriority },
620
+ usage: { mount: mountUsage }
395
621
  }
396
622
  };
397
623
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-workbuddy",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "把 Windows 端已登录的 WorkBuddy 客户端模型桥接进 Coopanion:读取本机登录态,列出模型目录与默认积分倍率,供 Coo 调用。",
6
6
  "license": "MIT",
@@ -341,6 +341,21 @@ for (let run = 1; run <= RUNS; run += 1) {
341
341
  return dom === false && api.prefer === false ? true : false;
342
342
  });
343
343
  row.checks.panelToggleOff = true;
344
+ // --- 用量面板:页签 → 渲染 → 概览表有行 ---
345
+ await evaluate(clickByText('用量'));
346
+ const usage = await waitFor('用量面板渲染', async () => {
347
+ const v = await evaluate(`(() => {
348
+ const btn = [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '刷新余额' && b.offsetParent !== null);
349
+ const chip = [...document.querySelectorAll('*')].find((n) => n.children.length === 0 && (n.textContent || '').startsWith('今日积分'));
350
+ const rows = document.querySelectorAll('table tr').length;
351
+ return { hasRefresh: !!btn, chip: (chip?.textContent || '').trim(), rows };
352
+ })()`);
353
+ return v.hasRefresh && v.rows >= 4 ? v : false;
354
+ });
355
+ row.checks.usageRendered = usage.hasRefresh === true;
356
+ row.checks.usageRows = usage.rows;
357
+ row.checks.usageChip = usage.chip;
358
+ if (SHOTS && run === 1) { await sleep(300); await shot('world-usage'); }
344
359
  if (KEEP) await shot(`run${run}-world`);
345
360
  void pane;
346
361
  } catch (err) {
package/src/ENV_PROMPT.md CHANGED
@@ -3,6 +3,8 @@
3
3
  This machine has a WorkBuddy desktop sign-in bridged in (region: {{workbuddy.region}}; signed in: {{workbuddy.authed}}).
4
4
  The model catalog holds {{workbuddy.catalogSize}} models. The currently selected model is `{{workbuddy.model}}` with default credit multiplier {{workbuddy.multiplier}}.
5
5
  WorkBuddy desktop main process: {{workbuddy.process}}. Relay-first switch: {{workbuddy.priority}}.
6
+ Spent today through the bridge: {{workbuddy.todayCredit}} credits / {{workbuddy.todayTokens}} tokens (locally recorded).
7
+ The multiplier above already includes any time-limited discount or free window that is active right now; `wb_list_models` also marks which offers are active and their hours.
6
8
 
7
9
  - Use `wb_list_models` to see every model with its multiplier before choosing; prefer low-multiplier models for casual tasks.
8
10
  - Use `wb_select_model` when the user asks to switch the WorkBuddy model.