cortico-world-workbuddy 0.1.7 → 0.3.4
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 +214 -0
- package/README.md +95 -101
- package/SECURITY.md +29 -16
- package/describe.json +9 -9
- package/dist/webui.js +61 -117
- package/package.json +9 -8
- package/scripts/build-web.mjs +97 -0
- package/scripts/clean-legacy-injection.mjs +77 -0
- package/scripts/clean-workbuddy.mjs +101 -0
- package/scripts/contract-regression.mjs +221 -0
- package/scripts/contract.mjs +370 -0
- package/scripts/gen-describe.mjs +97 -0
- package/scripts/lib/app-root.mjs +41 -0
- package/scripts/reinstall.ps1 +90 -53
- package/scripts/selfcheck.mjs +58 -13
- package/scripts/verify-ui.mjs +31 -66
- package/src/ENV_PROMPT.md +4 -10
- package/src/config.js +7 -24
- package/src/credentials.js +20 -4
- package/src/definition.js +3 -3
- package/src/promo.js +150 -40
- package/src/safety.js +8 -3
- package/src/upstream.js +103 -105
- package/src/usage.js +82 -43
- package/src/wbproc.js +6 -8
- package/src/webui/client.js +65 -130
- package/src/world.js +219 -310
- package/ENV_PROMPT.md +0 -11
- package/assets/workbuddy-home.js +0 -266
- package/scripts/host-inject.mjs +0 -80
- package/src/host.js +0 -269
package/SECURITY.md
CHANGED
|
@@ -11,34 +11,47 @@ judge the risk before installing.
|
|
|
11
11
|
| `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth\workbuddy-desktop.info` | To learn your WorkBuddy login | `auth.accessToken`, `auth.refreshToken`, `auth.expiresAt`, `auth.domain`, `account` |
|
|
12
12
|
| `%APPDATA%\WorkBuddy*\*.json` (if present) | Older WorkBuddy builds store the login here | same fields when stored in plain text |
|
|
13
13
|
| `%LOCALAPPDATA%\CodeBuddyExtension\Data\Public\auth` (fallback) | Same as above, directory scan | same fields |
|
|
14
|
-
| `
|
|
15
|
-
| `<Coopanion data>\home\companion\config.json` | Read
|
|
14
|
+
| `WorkBuddy.exe` / `WorkBuddy AI.exe` (known install locations) | Only to ask the app for its own at-rest key | the key payload printed by the app's own `loggerGet()` binding; never stored |
|
|
15
|
+
| `<Coopanion data>\home\companion\config.json` | Read `worlds.workbuddy` | model id, `envPrompt` / `safeOnly` flags |
|
|
16
16
|
|
|
17
17
|
The token is decrypted in memory only when the file is encrypted, is used only as the
|
|
18
18
|
`Authorization: Bearer` header for WorkBuddy's own endpoints, and is never written anywhere except
|
|
19
19
|
the Coopanion deployment `.env` if you paste one in the account panel yourself (the framework's
|
|
20
|
-
`storeSecret`).
|
|
20
|
+
`storeSecret`). Refresh tokens are never sent with chat requests.
|
|
21
21
|
|
|
22
22
|
## Where data goes
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
By default, only these two hosts, both WorkBuddy's:
|
|
25
25
|
|
|
26
|
-
- `https://copilot.tencent.com` — model catalogue and chat completions
|
|
27
|
-
- `https://www.codebuddy.cn` — credit balance
|
|
28
|
-
- `https://www.workbuddy.ai` — catalogue, chat, credits (region `global`)
|
|
26
|
+
- `https://copilot.tencent.com` — model catalogue and chat completions
|
|
27
|
+
- `https://www.codebuddy.cn` — credit balance
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
If you set `worlds.workbuddy.baseUrl` (a proxy or mirror), **every** request — catalogue,
|
|
30
|
+
chat and credit balance — goes to that host instead, and the token is sent there too. That is
|
|
31
|
+
your own configuration choice, not a default. No other hosts are contacted. No logs contain
|
|
32
|
+
tokens: every log line goes through `maskToken()`.
|
|
31
33
|
|
|
32
34
|
## What it writes
|
|
33
35
|
|
|
34
|
-
- `<Coopanion data>/
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
- `<Coopanion data>/home/companion/data/workbuddy-catalog.json` — cached model list, no credentials.
|
|
37
|
+
- `<Coopanion data>/home/companion/data/workbuddy-usage.jsonl` and `workbuddy-balance.json` — local
|
|
38
|
+
usage ledger and credit snapshot, no tokens.
|
|
39
|
+
- `worlds.workbuddy.*` in the companion config (model, `envPrompt`, `safeOnly`).
|
|
40
|
+
- The deployment `.env` — only when you paste a token or clear it in the Account panel
|
|
41
|
+
(the sentinels `wb-cleared` / `wb-auto` are written there; a real token only if you pasted one).
|
|
42
|
+
|
|
43
|
+
**Coopanion program files are never touched by the running extension.** Since 0.3.0 it does not
|
|
44
|
+
inject anything into `resources/app/build/console` and keeps no backup of host files. Coopanion's
|
|
45
|
+
own uninstall route removes only the package; it does not run extension cleanup code. After
|
|
46
|
+
uninstalling, run `node scripts/clean-workbuddy.mjs --check`, then `--clean`, from the source tree.
|
|
47
|
+
That explicit command removes only the `worlds.workbuddy` config block, the three data files above,
|
|
48
|
+
and the `accessToken` line in `.env`; it preserves every other setting and secret and never prints
|
|
49
|
+
the token. Ordinary `stop()` and restart never delete usage data.
|
|
50
|
+
|
|
51
|
+
If you upgraded from 0.2.x, an old one-line `<script>` reference, `workbuddy-home.js` and
|
|
52
|
+
`index.html.workbuddy-backup` may still be left over from that older version. Remove them once with
|
|
53
|
+
`node scripts/clean-legacy-injection.mjs --clean` (`--check` lists what it would touch). Neither
|
|
54
|
+
cleanup script runs automatically, and the extension never re-adds the injection.
|
|
42
55
|
|
|
43
56
|
## Reporting a vulnerability
|
|
44
57
|
|
package/describe.json
CHANGED
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "wb_select_model",
|
|
26
|
-
"description": "Select which WorkBuddy model wb_chat should use by default. The id must come from wb_list_models.
|
|
26
|
+
"description": "Select which WorkBuddy model wb_chat should use by default. The id must come from wb_list_models. Persists across restarts.",
|
|
27
27
|
"summary": {
|
|
28
|
-
"zh": "选择 wb_chat
|
|
29
|
-
"en": "Selects the default WorkBuddy model for wb_chat."
|
|
28
|
+
"zh": "选择 wb_chat 默认使用的 WorkBuddy 模型,选择会持久保存。",
|
|
29
|
+
"en": "Selects the default WorkBuddy model for wb_chat; the choice persists across restarts."
|
|
30
30
|
},
|
|
31
31
|
"tags": [
|
|
32
32
|
"write"
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"name": "wb_chat",
|
|
50
|
-
"description": "Ask the selected WorkBuddy model a single-turn question and return its answer.
|
|
50
|
+
"description": "Ask the selected WorkBuddy model a single-turn question and return its answer. This spends the user's own WorkBuddy credits at the model's multiplier; keep prompts concise. Use the model parameter only to override the selected model for one call. Requires the WorkBuddy desktop app to be running.",
|
|
51
51
|
"summary": {
|
|
52
|
-
"zh": "
|
|
53
|
-
"en": "Asks the selected WorkBuddy model a single-turn question
|
|
52
|
+
"zh": "向当前选中的 WorkBuddy 模型发一句单轮提问并取回答,会消耗用户自己的 WorkBuddy 积分。",
|
|
53
|
+
"en": "Asks the selected WorkBuddy model a single-turn question; spends the user's own WorkBuddy credits."
|
|
54
54
|
},
|
|
55
55
|
"tags": [
|
|
56
56
|
"write"
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"name": "wb_credits",
|
|
82
82
|
"description": "Show the remaining WorkBuddy credit balance of the signed-in account (read-only). Requires the WorkBuddy desktop app to be running.",
|
|
83
83
|
"summary": {
|
|
84
|
-
"zh": "
|
|
85
|
-
"en": "
|
|
84
|
+
"zh": "读取当前 WorkBuddy 账号的剩余积分与各积分包的已用/剩余(只读)。",
|
|
85
|
+
"en": "Shows remaining WorkBuddy credits and per-package usage (read-only)."
|
|
86
86
|
},
|
|
87
87
|
"tags": [
|
|
88
88
|
"read"
|
|
@@ -103,5 +103,5 @@
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
],
|
|
106
|
-
"envPrompt": "## WorkBuddy bridge\n\nThis machine has a WorkBuddy desktop
|
|
106
|
+
"envPrompt": "## WorkBuddy bridge\n\nThis machine has a signed-in WorkBuddy desktop app bridged in (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}}.\nSpent today through the bridge: {{workbuddy.todayCredit}} credits / {{workbuddy.todayTokens}} tokens (locally recorded).\nThe multiplier above is the model's own price plus any time-limited price change this bridge can verify at the moment (including free windows). An offer that only shows a badge may not change the price; `wb_list_models` 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- Safe-list gate: {{workbuddy.safeOnly}}. When it is `on`, only the models WorkBuddy publishes as public chat models are callable — `disabled` entries, internal routing aliases (`auto`, `fast-model`, `balanced-model`, `deep-model`), media/code models and delisted ones are refused. Do not try to work around a refusal; tell the user why and offer a safe model instead. `wb_list_models` with `all=true` shows the refused ones with reasons.\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. Call it when the user asks for WorkBuddy or when a WorkBuddy model would clearly help; do not route every turn through it.\n- `wb_credits` is read-only and costs nothing (also requires the desktop process)."
|
|
107
107
|
}
|
package/dist/webui.js
CHANGED
|
@@ -21,7 +21,6 @@ var TEXT = {
|
|
|
21
21
|
source: "\u6765\u6E90",
|
|
22
22
|
token: "Token\uFF08\u63A9\u7801\uFF09",
|
|
23
23
|
account: "\u8D26\u53F7",
|
|
24
|
-
region: "\u533A\u57DF",
|
|
25
24
|
upstream: "\u4E0A\u6E38",
|
|
26
25
|
rediscover: "\u91CD\u65B0\u68C0\u6D4B\u672C\u673A\u767B\u5F55",
|
|
27
26
|
pasteToken: "\u624B\u52A8\u7C98\u8D34 access token",
|
|
@@ -34,34 +33,25 @@ var TEXT = {
|
|
|
34
33
|
lastError: "\u6700\u8FD1\u9519\u8BEF",
|
|
35
34
|
models: "\u4E2A\u6A21\u578B",
|
|
36
35
|
currentModel: "\u5F53\u524D\u6A21\u578B",
|
|
37
|
-
priorityTitle: "\u4E2D\u8F6C\u7AD9\u4F18\u5148",
|
|
38
|
-
priorityLabel: "\u662F\u5426\u542F\u7528Workbuddy",
|
|
39
|
-
priorityDesc: "\u4E0E\u300C\u5F00\u59CB\u300D\u9875\u7684\u300C\u4E2D\u8F6C\u7AD9\u4F18\u5148\u300D\u6309\u94AE\u662F\u540C\u4E00\u4EFD\u72B6\u6001\uFF1A\u6253\u5F00\u540E\u684C\u5BA0\u6BCF\u8F6E\u5BF9\u8BDD\u4F18\u5148\u8C03\u7528 WorkBuddy \u7684\u6A21\u578B\uFF0C\u5173\u95ED\u5219\u5B8C\u5168\u8DF3\u8FC7 WorkBuddy\u3002\u9700\u8981 WorkBuddy \u684C\u9762\u7AEF\u4E3B\u8FDB\u7A0B\u5728\u8FD0\u884C\u3002",
|
|
40
36
|
procRunning: "WorkBuddy \u4E3B\u8FDB\u7A0B\uFF1A\u8FD0\u884C\u4E2D",
|
|
41
37
|
procStopped: "WorkBuddy \u4E3B\u8FDB\u7A0B\uFF1A\u672A\u68C0\u6D4B\u5230",
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
enabledOff: "\u5DF2\u5173\u95ED\uFF1A\u684C\u5BA0\u8DF3\u8FC7 WorkBuddy",
|
|
45
|
-
hostOk: "\u5F00\u59CB\u9875\u6309\u94AE\uFF1A\u5DF2\u6CE8\u5165",
|
|
46
|
-
hostBroken: "\u5F00\u59CB\u9875\u6309\u94AE\uFF1A\u672A\u6CE8\u5165\uFF08\u5BBF\u4E3B\u5347\u7EA7\u53EF\u80FD\u8986\u76D6\u4E86\u63A7\u5236\u53F0\u6587\u4EF6\uFF09",
|
|
47
|
-
hostUnknown: "\u5F00\u59CB\u9875\u6309\u94AE\uFF1A\u72B6\u6001\u672A\u77E5",
|
|
48
|
-
repair: "\u91CD\u65B0\u6CE8\u5165\u5F00\u59CB\u9875\u6309\u94AE",
|
|
49
|
-
repairing: "\u6B63\u5728\u91CD\u65B0\u6CE8\u5165\u2026",
|
|
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",
|
|
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",
|
|
38
|
+
usageTitle: "\u7528\u91CF\u4E0E\u6210\u672C",
|
|
39
|
+
usageDesc: "\u7ECF\u7531 WorkBuddy \u6D88\u8017\u7684 token \u4E0E\u79EF\u5206\uFF08\u672C\u5730\u8BB0\u8D26\uFF09\uFF1A\u79EF\u5206\u53D6\u4E0A\u6E38\u6BCF\u6B21\u8C03\u7528\u56DE\u4F20\u7684\u771F\u5B9E\u6263\u8D39\uFF08\u53EF\u80FD\u4E3A 0\uFF09\uFF0Ctoken \u53D6\u540C\u4E00\u6BB5 SSE \u7684 usage\uFF1B\u500D\u7387\u6309\u76EE\u5F55 + \u9650\u65F6\u4F18\u60E0\u5B9E\u65F6\u8BA1\u7B97\u3002",
|
|
54
40
|
usageRefresh: "\u5237\u65B0\u4F59\u989D",
|
|
55
41
|
usageClear: "\u6E05\u7A7A\u672C\u5730\u8D26\u672C",
|
|
56
42
|
usageCleared: "\u672C\u5730\u8D26\u672C\u5DF2\u6E05\u7A7A",
|
|
57
43
|
metricCalls: "\u8C03\u7528",
|
|
44
|
+
metricFailed: "\u5931\u8D25",
|
|
58
45
|
metricCredit: "\u79EF\u5206",
|
|
59
46
|
metricInput: "\u8F93\u5165 tok",
|
|
60
47
|
metricOutput: "\u8F93\u51FA tok",
|
|
48
|
+
metricCached: "\u7F13\u5B58\u547D\u4E2D",
|
|
49
|
+
metricTotal: "\u5171 tok",
|
|
61
50
|
spanToday: "\u4ECA\u5929",
|
|
62
51
|
spanWeek: "\u8FD1 7 \u5929",
|
|
63
52
|
spanMonth: "\u8FD1 30 \u5929",
|
|
64
|
-
|
|
53
|
+
spanAll: "\u5168\u90E8",
|
|
54
|
+
byModel: "\u6309\u6A21\u578B\uFF08\u8FD1 30 \u5929\uFF09",
|
|
65
55
|
balanceTitle: "\u79EF\u5206\u5305\u4F59\u989D",
|
|
66
56
|
colPkgName: "\u79EF\u5206\u5305",
|
|
67
57
|
colPkgRemain: "\u5269\u4F59",
|
|
@@ -107,7 +97,6 @@ var TEXT = {
|
|
|
107
97
|
source: "Source",
|
|
108
98
|
token: "Token (masked)",
|
|
109
99
|
account: "Account",
|
|
110
|
-
region: "Region",
|
|
111
100
|
upstream: "Upstream",
|
|
112
101
|
rediscover: "Re-detect local sign-in",
|
|
113
102
|
pasteToken: "Paste an access token manually",
|
|
@@ -120,34 +109,25 @@ var TEXT = {
|
|
|
120
109
|
lastError: "Last error",
|
|
121
110
|
models: "models",
|
|
122
111
|
currentModel: "Current model",
|
|
123
|
-
priorityTitle: "Relay first",
|
|
124
|
-
priorityLabel: "Enable Workbuddy",
|
|
125
|
-
priorityDesc: "Same state as the \u201CRelay first\u201D button on the Home page: when on, the pet asks a WorkBuddy model first every turn; when off, WorkBuddy is skipped entirely. Requires the WorkBuddy desktop main process.",
|
|
126
112
|
procRunning: "WorkBuddy process: running",
|
|
127
113
|
procStopped: "WorkBuddy process: not detected",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
enabledOff: "Off: the pet skips WorkBuddy",
|
|
131
|
-
hostOk: "Home button: injected",
|
|
132
|
-
hostBroken: "Home button: not injected (a host update may have replaced the console files)",
|
|
133
|
-
hostUnknown: "Home button: unknown",
|
|
134
|
-
repair: "Re-inject the home button",
|
|
135
|
-
repairing: "Re-injecting\u2026",
|
|
136
|
-
repaired: "Fixed. Reopen the console to see the button",
|
|
137
|
-
repairFail: "Repair failed",
|
|
138
|
-
usageTitle: "Usage",
|
|
139
|
-
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.",
|
|
114
|
+
usageTitle: "Usage & cost",
|
|
115
|
+
usageDesc: "Tokens and credits spent through WorkBuddy (locally recorded): credits are the per-call figure the upstream returns (may be 0), tokens come from the same SSE usage block; multipliers combine the catalog with time-limited offers in real time.",
|
|
140
116
|
usageRefresh: "Refresh balance",
|
|
141
117
|
usageClear: "Clear local ledger",
|
|
142
118
|
usageCleared: "Local ledger cleared",
|
|
143
119
|
metricCalls: "Calls",
|
|
120
|
+
metricFailed: "Failed",
|
|
144
121
|
metricCredit: "Credits",
|
|
145
122
|
metricInput: "Input tok",
|
|
146
123
|
metricOutput: "Output tok",
|
|
124
|
+
metricCached: "Cached",
|
|
125
|
+
metricTotal: "Total tok",
|
|
147
126
|
spanToday: "Today",
|
|
148
127
|
spanWeek: "Last 7 days",
|
|
149
128
|
spanMonth: "Last 30 days",
|
|
150
|
-
|
|
129
|
+
spanAll: "All",
|
|
130
|
+
byModel: "By model (last 30 days)",
|
|
151
131
|
balanceTitle: "Credit packages",
|
|
152
132
|
colPkgName: "Package",
|
|
153
133
|
colPkgRemain: "Remaining",
|
|
@@ -210,9 +190,10 @@ function mountModels(ctx) {
|
|
|
210
190
|
const head = ui.rowbar();
|
|
211
191
|
const refreshBtn = ui.button(s.refresh, { variant: "primary" });
|
|
212
192
|
const stateChip = ui.chip("", "plain");
|
|
193
|
+
const procChip = ui.chip("", "plain");
|
|
213
194
|
const currentChip = ui.chip("", "accent");
|
|
214
195
|
const allBox = ui.checkbox(s.showAllModels, { checked: false });
|
|
215
|
-
head.append(refreshBtn, stateChip, currentChip, allBox.el);
|
|
196
|
+
head.append(refreshBtn, stateChip, procChip, currentChip, allBox.el);
|
|
216
197
|
sheet.body.append(head);
|
|
217
198
|
const errLine = ui.msgline();
|
|
218
199
|
sheet.body.append(errLine);
|
|
@@ -227,6 +208,7 @@ function mountModels(ctx) {
|
|
|
227
208
|
const safeCount = models.filter((m) => m.safety?.allow).length;
|
|
228
209
|
stateChip.textContent = `${s.lastRefresh}: ${fmtTs(state?.catalogAt, s)} \xB7 ${safeCount}/${models.length} ${s.models} \xB7 ${state?.safeOnly === false ? s.safeOnlyOff : s.safeOnlyOn}`;
|
|
229
210
|
currentChip.textContent = `${s.currentModel}: ${state?.model || "\u2014"}`;
|
|
211
|
+
procChip.textContent = state?.running ? s.procRunning : s.procStopped;
|
|
230
212
|
}
|
|
231
213
|
function pickable(m) {
|
|
232
214
|
if (state?.safeOnly === false) return true;
|
|
@@ -366,7 +348,6 @@ function mountAccount(ctx) {
|
|
|
366
348
|
{ k: s.source, v: state?.tokenSource ?? "\u2014" },
|
|
367
349
|
{ k: s.token, v: state?.tokenMask ?? "\u2014" },
|
|
368
350
|
{ k: s.account, v: state?.account?.name ?? "\u2014" },
|
|
369
|
-
{ k: s.region, v: state?.region ?? "\u2014" },
|
|
370
351
|
{ k: s.upstream, v: state?.baseUrl ?? "\u2014" }
|
|
371
352
|
];
|
|
372
353
|
if (state?.lastError) rows.push({ k: s.lastError, v: state.lastError });
|
|
@@ -429,83 +410,14 @@ function mountAccount(ctx) {
|
|
|
429
410
|
errLine.textContent = String(err?.message ?? err);
|
|
430
411
|
});
|
|
431
412
|
}
|
|
432
|
-
function mountPriority(ctx) {
|
|
433
|
-
const s = strings(ctx);
|
|
434
|
-
const { ui } = ctx;
|
|
435
|
-
const sheet = ui.sheet({ title: s.priorityTitle, desc: s.priorityDesc });
|
|
436
|
-
ctx.root.append(sheet.el);
|
|
437
|
-
const statusLine = ui.msgline();
|
|
438
|
-
const hostLine = ui.msgline();
|
|
439
|
-
const hostBar = ui.actions();
|
|
440
|
-
const repairBtn = ui.button(s.repair, {
|
|
441
|
-
size: "sm",
|
|
442
|
-
variant: "plain",
|
|
443
|
-
onClick: async () => {
|
|
444
|
-
repairBtn.disabled = true;
|
|
445
|
-
hostLine.textContent = s.repairing;
|
|
446
|
-
try {
|
|
447
|
-
const r = await ctx.invoke("repair");
|
|
448
|
-
hostLine.textContent = r?.ok ? s.repaired : `${s.repairFail}\uFF1A${r?.error ?? r?.hostUi?.reason ?? ""}`;
|
|
449
|
-
ui.toast(r?.ok ? s.repaired : s.repairFail, r?.ok ? "ok" : "bad");
|
|
450
|
-
await load();
|
|
451
|
-
} catch (err) {
|
|
452
|
-
hostLine.textContent = `${s.repairFail}\uFF1A${String(err?.message ?? err)}`;
|
|
453
|
-
ui.toast(s.repairFail, "bad");
|
|
454
|
-
} finally {
|
|
455
|
-
repairBtn.disabled = false;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
hostBar.append(repairBtn);
|
|
460
|
-
function apply(r) {
|
|
461
|
-
if (!r) return;
|
|
462
|
-
box.setChecked(r.prefer === true);
|
|
463
|
-
statusLine.textContent = r.running ? s.procRunning : s.procStopped;
|
|
464
|
-
const host = r.hostUi;
|
|
465
|
-
if (!host) {
|
|
466
|
-
hostLine.textContent = s.hostUnknown;
|
|
467
|
-
hostBar.style.display = "none";
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
hostLine.textContent = host.ok ? s.hostOk : `${s.hostBroken}\uFF08${host.label ?? host.reason}\uFF09`;
|
|
471
|
-
hostBar.style.display = host.ok ? "none" : "";
|
|
472
|
-
}
|
|
473
|
-
const box = ui.checkbox(s.priorityLabel, {
|
|
474
|
-
checked: false,
|
|
475
|
-
title: s.priorityDesc,
|
|
476
|
-
onChange: async (checked) => {
|
|
477
|
-
try {
|
|
478
|
-
const r = await ctx.invoke("set", [checked]);
|
|
479
|
-
if (r?.ok) {
|
|
480
|
-
apply(r);
|
|
481
|
-
ui.toast(checked ? s.enabledOn : s.enabledOff);
|
|
482
|
-
} else {
|
|
483
|
-
apply(r);
|
|
484
|
-
box.setChecked(false);
|
|
485
|
-
ui.toast(r?.error ?? s.noProcess, "bad");
|
|
486
|
-
}
|
|
487
|
-
} catch (err) {
|
|
488
|
-
box.setChecked(false);
|
|
489
|
-
ui.toast(String(err?.message ?? err), "bad");
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
sheet.body.append(box.el, statusLine, hostLine, hostBar);
|
|
494
|
-
async function load() {
|
|
495
|
-
apply(await ctx.invoke("state"));
|
|
496
|
-
}
|
|
497
|
-
ctx.stream({
|
|
498
|
-
message: () => load().catch(() => {
|
|
499
|
-
})
|
|
500
|
-
});
|
|
501
|
-
load().catch((err) => {
|
|
502
|
-
statusLine.textContent = String(err?.message ?? err);
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
413
|
function fmtNum(n) {
|
|
506
414
|
if (typeof n !== "number" || !Number.isFinite(n)) return "\u2014";
|
|
507
415
|
return String(Math.round(n * 100) / 100);
|
|
508
416
|
}
|
|
417
|
+
function fmtCredit(bucket) {
|
|
418
|
+
const known = fmtNum(bucket?.credit);
|
|
419
|
+
return (bucket?.creditUnknown ?? 0) > 0 ? `${known} + ?` : known;
|
|
420
|
+
}
|
|
509
421
|
function mountUsage(ctx) {
|
|
510
422
|
const s = strings(ctx);
|
|
511
423
|
const { ui } = ctx;
|
|
@@ -521,13 +433,22 @@ function mountUsage(ctx) {
|
|
|
521
433
|
const errLine = ui.msgline();
|
|
522
434
|
sheet.body.append(errLine);
|
|
523
435
|
const overview = ui.table({
|
|
524
|
-
head: [
|
|
436
|
+
head: [
|
|
437
|
+
"",
|
|
438
|
+
s.metricCalls,
|
|
439
|
+
s.metricFailed,
|
|
440
|
+
s.metricCredit,
|
|
441
|
+
s.metricInput,
|
|
442
|
+
s.metricOutput,
|
|
443
|
+
s.metricCached,
|
|
444
|
+
s.metricTotal
|
|
445
|
+
],
|
|
525
446
|
maxHeight: "220px"
|
|
526
447
|
});
|
|
527
448
|
sheet.body.append(overview.el);
|
|
528
449
|
const modelSection = ui.section(s.byModel);
|
|
529
450
|
const modelTable = ui.table({
|
|
530
|
-
head: [s.colModel, s.metricCalls, s.metricCredit, s.metricInput, s.metricOutput],
|
|
451
|
+
head: [s.colModel, s.metricCalls, s.metricFailed, s.metricCredit, s.metricInput, s.metricOutput, s.metricTotal],
|
|
531
452
|
maxHeight: "260px"
|
|
532
453
|
});
|
|
533
454
|
modelSection.append(modelTable.el);
|
|
@@ -553,21 +474,43 @@ function mountUsage(ctx) {
|
|
|
553
474
|
let state = null;
|
|
554
475
|
function spanRow(label, bucket) {
|
|
555
476
|
const b = bucket ?? {};
|
|
556
|
-
return [
|
|
477
|
+
return [
|
|
478
|
+
label,
|
|
479
|
+
String(b.calls ?? 0),
|
|
480
|
+
String(b.failed ?? 0),
|
|
481
|
+
fmtCredit(b),
|
|
482
|
+
fmtNum(b.prompt),
|
|
483
|
+
fmtNum(b.completion),
|
|
484
|
+
fmtNum(b.cached),
|
|
485
|
+
fmtNum(b.total)
|
|
486
|
+
];
|
|
557
487
|
}
|
|
558
488
|
function render() {
|
|
559
489
|
const totals = state?.summary?.totals ?? {};
|
|
560
|
-
todayChip.textContent = `${s.todayCredit}: ${
|
|
490
|
+
todayChip.textContent = `${s.todayCredit}: ${fmtCredit(totals.today)} \xB7 ${s.todayTokens}: ${fmtNum(
|
|
561
491
|
(totals.today?.prompt ?? 0) + (totals.today?.completion ?? 0)
|
|
562
492
|
)}`;
|
|
563
493
|
balanceChip.textContent = `${s.balanceTitle}: ${fmtNum(state?.balance?.total)}`;
|
|
564
|
-
const spans = [
|
|
494
|
+
const spans = [
|
|
495
|
+
spanRow(s.spanToday, totals.today),
|
|
496
|
+
spanRow(s.spanWeek, totals.week),
|
|
497
|
+
spanRow(s.spanMonth, totals.month),
|
|
498
|
+
spanRow(s.spanAll, totals.all)
|
|
499
|
+
];
|
|
565
500
|
overview.clear();
|
|
566
501
|
for (const row of spans) overview.addRow(row);
|
|
567
502
|
const byModel = state?.summary?.byModel ?? [];
|
|
568
503
|
modelTable.clear(byModel.length > 0 ? void 0 : s.noData);
|
|
569
504
|
for (const m of byModel) {
|
|
570
|
-
modelTable.addRow([
|
|
505
|
+
modelTable.addRow([
|
|
506
|
+
m.model,
|
|
507
|
+
String(m.calls ?? 0),
|
|
508
|
+
String(m.failed ?? 0),
|
|
509
|
+
fmtCredit(m),
|
|
510
|
+
fmtNum(m.prompt),
|
|
511
|
+
fmtNum(m.completion),
|
|
512
|
+
fmtNum(m.total)
|
|
513
|
+
]);
|
|
571
514
|
}
|
|
572
515
|
const packages = state?.balance?.packages ?? [];
|
|
573
516
|
pkgTable.clear(packages.length > 0 ? void 0 : s.noData);
|
|
@@ -612,7 +555,9 @@ function mountUsage(ctx) {
|
|
|
612
555
|
recentTable.addRow([
|
|
613
556
|
when,
|
|
614
557
|
r.model ?? "\u2014",
|
|
615
|
-
|
|
558
|
+
// 账本里的字段名就是 total/prompt/completion(不是 totalTokens),
|
|
559
|
+
// 以前这里恒读出 0,最近调用表的 token 列一直是 0。
|
|
560
|
+
fmtNum(r.total ?? (r.prompt ?? 0) + (r.completion ?? 0)),
|
|
616
561
|
r.ok === false ? "\u2014" : fmtNum(r.credit),
|
|
617
562
|
typeof r.durationMs === "number" ? `${r.durationMs} ms` : "\u2014"
|
|
618
563
|
]);
|
|
@@ -662,7 +607,6 @@ var client_default = {
|
|
|
662
607
|
panels: {
|
|
663
608
|
models: { mount: mountModels },
|
|
664
609
|
account: { mount: mountAccount },
|
|
665
|
-
priority: { mount: mountPriority },
|
|
666
610
|
usage: { mount: mountUsage }
|
|
667
611
|
}
|
|
668
612
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-workbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "把 Windows 端已登录的 WorkBuddy 客户端模型桥接进 Coopanion:读取本机登录态,列出模型目录与默认积分倍率,供 Coo 调用。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"locales": {
|
|
34
34
|
"zh": {
|
|
35
35
|
"displayName": "WorkBuddy 桥接",
|
|
36
|
-
"description": "把本机已登录的 WorkBuddy
|
|
36
|
+
"description": "把本机已登录的 WorkBuddy 客户端模型接入 Coopanion,面板中可选模型并显示其默认积分倍率。"
|
|
37
37
|
},
|
|
38
38
|
"en": {
|
|
39
39
|
"displayName": "WorkBuddy Bridge",
|
|
@@ -49,16 +49,17 @@
|
|
|
49
49
|
"CHANGELOG.md",
|
|
50
50
|
"describe.json",
|
|
51
51
|
"scripts",
|
|
52
|
+
"!scripts/visual",
|
|
52
53
|
"LICENSE",
|
|
53
|
-
"SECURITY.md"
|
|
54
|
-
"ENV_PROMPT.md"
|
|
54
|
+
"SECURITY.md"
|
|
55
55
|
],
|
|
56
56
|
"scripts": {
|
|
57
|
-
"build:web": "
|
|
57
|
+
"build:web": "node scripts/build-web.mjs",
|
|
58
58
|
"selfcheck": "node scripts/selfcheck.mjs",
|
|
59
|
-
"host-inject": "node scripts/host-inject.mjs",
|
|
60
59
|
"verify": "node scripts/verify-ui.mjs --runs 3",
|
|
61
|
-
"prepack": "
|
|
60
|
+
"prepack": "node scripts/build-web.mjs --strict",
|
|
61
|
+
"gen:describe": "node scripts/gen-describe.mjs",
|
|
62
|
+
"contract": "node scripts/contract.mjs"
|
|
62
63
|
},
|
|
63
64
|
"author": {
|
|
64
65
|
"name": "大肥鱼 (aechoa215)",
|
|
@@ -77,6 +78,6 @@
|
|
|
77
78
|
"win32"
|
|
78
79
|
],
|
|
79
80
|
"devDependencies": {
|
|
80
|
-
"esbuild": "
|
|
81
|
+
"esbuild": "0.28.2"
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 重建 `dist/webui.js`(面板产物)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么不用 `esbuild` 直接当 script:`prepack` 会在别人机器上跑,
|
|
6
|
+
* 那里可能没有 `npm install` 过(没有 node_modules/esbuild),直接调 `esbuild` 会
|
|
7
|
+
* `'esbuild' is not recognized`,整条 `npm pack` 就失败了。
|
|
8
|
+
*
|
|
9
|
+
* 解析顺序:
|
|
10
|
+
* 1. 本包的 devDependency(`node_modules/esbuild`,版本由 lock 固定,CI 用这个);
|
|
11
|
+
* 2. 宿主 Coopanion 自带的那份(`--config.strict-dep-builds` 之外的本机兜底);
|
|
12
|
+
* 3. 都没有:`dist/webui.js` 已存在就警告并退出 0(产物已随包提交),否则报错。
|
|
13
|
+
*
|
|
14
|
+
* `--strict`:找不到 esbuild 时**不允许**沿用旧产物(返回 1)。
|
|
15
|
+
* `prepack` 走 strict —— 否则没装依赖的机器会静默把过期 dist 发布出去(审查报告第 6 条)。
|
|
16
|
+
*
|
|
17
|
+
* 用法:node scripts/build-web.mjs [--verbose] [--strict]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync as readTextSync, statSync } from 'node:fs';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const pkgDir = fileURLToPath(new URL('..', import.meta.url));
|
|
27
|
+
const verbose = process.argv.includes('--verbose');
|
|
28
|
+
const strict = process.argv.includes('--strict');
|
|
29
|
+
const entry = join(pkgDir, 'src', 'webui', 'client.js');
|
|
30
|
+
const outfile = join(pkgDir, 'dist', 'webui.js');
|
|
31
|
+
const args = [entry, '--bundle', '--format=esm', `--outfile=${outfile}`, '--log-level=warning'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 宿主 Coopanion 的 app 目录(只用来兜底取它自带的 esbuild)。
|
|
35
|
+
* 按「存在 build/console/index.html」判定,探测失败返回空串。
|
|
36
|
+
*/
|
|
37
|
+
function hostAppRoot() {
|
|
38
|
+
const local = process.env.LOCALAPPDATA || '';
|
|
39
|
+
const candidates = [
|
|
40
|
+
local && join(local, 'Programs', 'Coopanion', 'resources', 'app'),
|
|
41
|
+
process.env.ProgramFiles && join(process.env.ProgramFiles, 'Coopanion', 'resources', 'app'),
|
|
42
|
+
process.env['ProgramFiles(x86)'] && join(process.env['ProgramFiles(x86)'], 'Coopanion', 'resources', 'app'),
|
|
43
|
+
].filter(Boolean);
|
|
44
|
+
return candidates.find((dir) => existsSync(join(dir, 'build', 'console', 'index.html'))) ?? '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 找到可用的 esbuild 启动脚本(bin/esbuild 是 JS 包装,用 node 跑)。 */
|
|
48
|
+
function resolveEsbuild() {
|
|
49
|
+
const require = createRequire(import.meta.url);
|
|
50
|
+
const candidates = [];
|
|
51
|
+
try {
|
|
52
|
+
candidates.push(require.resolve('esbuild/bin/esbuild'));
|
|
53
|
+
} catch {
|
|
54
|
+
/* 本地没装 devDependencies,走宿主兜底 */
|
|
55
|
+
}
|
|
56
|
+
const appRoot = process.env.COOPANION_APP_ROOT || hostAppRoot();
|
|
57
|
+
if (appRoot) candidates.push(join(appRoot, 'node_modules', 'esbuild', 'bin', 'esbuild'));
|
|
58
|
+
|
|
59
|
+
for (const bin of candidates) {
|
|
60
|
+
if (!existsSync(bin)) continue;
|
|
61
|
+
// 版本号在同一目录的 package.json 里
|
|
62
|
+
let version = 'unknown';
|
|
63
|
+
try {
|
|
64
|
+
version = JSON.parse(readTextSync(join(dirname(bin), '..', 'package.json'), 'utf8')).version;
|
|
65
|
+
} catch {
|
|
66
|
+
/* 版本读不到不影响构建 */
|
|
67
|
+
}
|
|
68
|
+
return { bin, version };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const found = resolveEsbuild();
|
|
74
|
+
if (!found) {
|
|
75
|
+
console.error('✗ 找不到 esbuild(本地 devDependencies 与宿主都没装)。');
|
|
76
|
+
console.error(' 先跑一次 `npm ci`(有 lockfile,锁的是 devDependency 的 esbuild)再重试。');
|
|
77
|
+
if (!existsSync(outfile)) process.exit(1);
|
|
78
|
+
if (strict) {
|
|
79
|
+
console.error('✗ --strict:拒绝沿用已有的 dist/webui.js(可能是过期产物)。');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
console.warn('! 非 strict 模式:沿用已有的 dist/webui.js(本地开发可以,发布不行)。');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (verbose) console.log(`· esbuild ${found.version}:${found.bin}`);
|
|
87
|
+
mkdirSync(join(pkgDir, 'dist'), { recursive: true });
|
|
88
|
+
const result = spawnSync(process.execPath, [found.bin, ...args], { cwd: pkgDir, encoding: 'utf8' });
|
|
89
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
90
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
91
|
+
if (result.status !== 0) {
|
|
92
|
+
console.error(`✗ esbuild 失败(退出码 ${result.status})`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const size = statSync(outfile).size;
|
|
97
|
+
console.log(`✓ dist/webui.js 已重建(esbuild ${found.version},${size} 字节)`);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 一次性迁移工具:清理 0.2.x 主页注入留在宿主 `build/console` 里的三样东西。
|
|
4
|
+
*
|
|
5
|
+
* 0.3.0 起扩展本体**不再改写宿主任何文件**,所以这个脚本不由扩展自动执行:
|
|
6
|
+
* 装过 0.2.x 的用户主动跑一次即可。它只删本扩展自己留下的东西:
|
|
7
|
+
* - `index.html` 里引用 `workbuddy-home.js` 的那一行 `<script>`
|
|
8
|
+
* - `workbuddy-home.js`
|
|
9
|
+
* - `index.html.workbuddy-backup`
|
|
10
|
+
* 绝不用备份覆盖 `index.html`(避免把宿主升级过的文件降级),也不碰其余内容。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* node scripts/clean-legacy-injection.mjs --check # 只列出会动的项,不改文件
|
|
14
|
+
* node scripts/clean-legacy-injection.mjs --clean # 执行清理(幂等)
|
|
15
|
+
* COOPANION_APP_ROOT=<...>/resources/app # 显式指定宿主位置
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { findAppRoot } from './lib/app-root.mjs';
|
|
21
|
+
|
|
22
|
+
const pkgDir = fileURLToPath(new URL('..', import.meta.url));
|
|
23
|
+
const checkOnly = process.argv.includes('--check');
|
|
24
|
+
const doClean = process.argv.includes('--clean');
|
|
25
|
+
if (checkOnly === doClean) {
|
|
26
|
+
console.error('用法:node scripts/clean-legacy-injection.mjs --check | --clean(二选一)');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const appRoot = process.env.COOPANION_APP_ROOT || findAppRoot({ packageDir: pkgDir });
|
|
31
|
+
if (!appRoot) {
|
|
32
|
+
console.error('✗ 找不到 Coopanion 的 resources/app(可设 COOPANION_APP_ROOT 指定)');
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
const consoleDir = join(appRoot, 'build', 'console');
|
|
36
|
+
const htmlPath = join(consoleDir, 'index.html');
|
|
37
|
+
if (!existsSync(htmlPath)) {
|
|
38
|
+
console.error(`✗ 找不到 ${htmlPath}`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 只认「引用了 workbuddy-home.js 的 script 行」,其他一行都不动。 */
|
|
43
|
+
const INJECT_LINE = /<script[^>]*workbuddy-home\.js/i;
|
|
44
|
+
|
|
45
|
+
const text = readFileSync(htmlPath, 'utf8');
|
|
46
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
47
|
+
const lines = text.split(/\r?\n/);
|
|
48
|
+
const kept = lines.filter((line) => !INJECT_LINE.test(line));
|
|
49
|
+
const removedLines = lines.length - kept.length;
|
|
50
|
+
|
|
51
|
+
const files = [
|
|
52
|
+
join(consoleDir, 'workbuddy-home.js'),
|
|
53
|
+
join(consoleDir, 'index.html.workbuddy-backup'),
|
|
54
|
+
].filter((p) => existsSync(p));
|
|
55
|
+
|
|
56
|
+
console.log(`宿主:${consoleDir}`);
|
|
57
|
+
console.log(`index.html 注入引用行:${removedLines} 行`);
|
|
58
|
+
for (const p of files) console.log(`残留文件:${p}`);
|
|
59
|
+
|
|
60
|
+
if (removedLines === 0 && files.length === 0) {
|
|
61
|
+
console.log('✓ 已经是干净的,无需清理(扩展本体不会重新注入)');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
if (checkOnly) {
|
|
65
|
+
console.log('(--check:以上为待清理项,未做任何改动;执行请加 --clean)');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (removedLines > 0) {
|
|
70
|
+
writeFileSync(htmlPath, kept.join(newline), 'utf8');
|
|
71
|
+
console.log(`✓ 已剥掉 ${removedLines} 行注入引用(保留原有 ${newline === '\r\n' ? 'CRLF' : 'LF'} 换行风格)`);
|
|
72
|
+
}
|
|
73
|
+
for (const p of files) {
|
|
74
|
+
rmSync(p, { force: true });
|
|
75
|
+
console.log(`✓ 已删除 ${p}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('✓ 清理完成;扩展本体不会重新注入,卸载也不需要还原。');
|