cortico-world-workbuddy 0.3.0 → 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 +127 -2
- package/README.md +71 -54
- package/SECURITY.md +29 -16
- package/describe.json +2 -2
- package/dist/webui.js +58 -72
- package/package.json +3 -3
- 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 +45 -17
- package/scripts/reinstall.ps1 +15 -4
- package/scripts/selfcheck.mjs +1 -1
- package/scripts/verify-ui.mjs +19 -33
- package/src/ENV_PROMPT.md +4 -10
- package/src/config.js +5 -16
- package/src/credentials.js +11 -3
- package/src/definition.js +3 -3
- package/src/promo.js +64 -12
- package/src/upstream.js +31 -63
- package/src/usage.js +27 -21
- package/src/wbproc.js +0 -7
- package/src/webui/client.js +62 -84
- package/src/world.js +96 -139
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,27 +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: "\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
|
-
usageTitle: "\u7528\u91CF",
|
|
46
|
-
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",
|
|
47
40
|
usageRefresh: "\u5237\u65B0\u4F59\u989D",
|
|
48
41
|
usageClear: "\u6E05\u7A7A\u672C\u5730\u8D26\u672C",
|
|
49
42
|
usageCleared: "\u672C\u5730\u8D26\u672C\u5DF2\u6E05\u7A7A",
|
|
50
43
|
metricCalls: "\u8C03\u7528",
|
|
44
|
+
metricFailed: "\u5931\u8D25",
|
|
51
45
|
metricCredit: "\u79EF\u5206",
|
|
52
46
|
metricInput: "\u8F93\u5165 tok",
|
|
53
47
|
metricOutput: "\u8F93\u51FA tok",
|
|
48
|
+
metricCached: "\u7F13\u5B58\u547D\u4E2D",
|
|
49
|
+
metricTotal: "\u5171 tok",
|
|
54
50
|
spanToday: "\u4ECA\u5929",
|
|
55
51
|
spanWeek: "\u8FD1 7 \u5929",
|
|
56
52
|
spanMonth: "\u8FD1 30 \u5929",
|
|
57
|
-
|
|
53
|
+
spanAll: "\u5168\u90E8",
|
|
54
|
+
byModel: "\u6309\u6A21\u578B\uFF08\u8FD1 30 \u5929\uFF09",
|
|
58
55
|
balanceTitle: "\u79EF\u5206\u5305\u4F59\u989D",
|
|
59
56
|
colPkgName: "\u79EF\u5206\u5305",
|
|
60
57
|
colPkgRemain: "\u5269\u4F59",
|
|
@@ -100,7 +97,6 @@ var TEXT = {
|
|
|
100
97
|
source: "Source",
|
|
101
98
|
token: "Token (masked)",
|
|
102
99
|
account: "Account",
|
|
103
|
-
region: "Region",
|
|
104
100
|
upstream: "Upstream",
|
|
105
101
|
rediscover: "Re-detect local sign-in",
|
|
106
102
|
pasteToken: "Paste an access token manually",
|
|
@@ -113,27 +109,25 @@ var TEXT = {
|
|
|
113
109
|
lastError: "Last error",
|
|
114
110
|
models: "models",
|
|
115
111
|
currentModel: "Current model",
|
|
116
|
-
priorityTitle: "Relay first",
|
|
117
|
-
priorityLabel: "Enable Workbuddy",
|
|
118
|
-
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.",
|
|
119
112
|
procRunning: "WorkBuddy process: running",
|
|
120
113
|
procStopped: "WorkBuddy process: not detected",
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
enabledOff: "Off: the pet skips WorkBuddy",
|
|
124
|
-
usageTitle: "Usage",
|
|
125
|
-
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.",
|
|
126
116
|
usageRefresh: "Refresh balance",
|
|
127
117
|
usageClear: "Clear local ledger",
|
|
128
118
|
usageCleared: "Local ledger cleared",
|
|
129
119
|
metricCalls: "Calls",
|
|
120
|
+
metricFailed: "Failed",
|
|
130
121
|
metricCredit: "Credits",
|
|
131
122
|
metricInput: "Input tok",
|
|
132
123
|
metricOutput: "Output tok",
|
|
124
|
+
metricCached: "Cached",
|
|
125
|
+
metricTotal: "Total tok",
|
|
133
126
|
spanToday: "Today",
|
|
134
127
|
spanWeek: "Last 7 days",
|
|
135
128
|
spanMonth: "Last 30 days",
|
|
136
|
-
|
|
129
|
+
spanAll: "All",
|
|
130
|
+
byModel: "By model (last 30 days)",
|
|
137
131
|
balanceTitle: "Credit packages",
|
|
138
132
|
colPkgName: "Package",
|
|
139
133
|
colPkgRemain: "Remaining",
|
|
@@ -196,9 +190,10 @@ function mountModels(ctx) {
|
|
|
196
190
|
const head = ui.rowbar();
|
|
197
191
|
const refreshBtn = ui.button(s.refresh, { variant: "primary" });
|
|
198
192
|
const stateChip = ui.chip("", "plain");
|
|
193
|
+
const procChip = ui.chip("", "plain");
|
|
199
194
|
const currentChip = ui.chip("", "accent");
|
|
200
195
|
const allBox = ui.checkbox(s.showAllModels, { checked: false });
|
|
201
|
-
head.append(refreshBtn, stateChip, currentChip, allBox.el);
|
|
196
|
+
head.append(refreshBtn, stateChip, procChip, currentChip, allBox.el);
|
|
202
197
|
sheet.body.append(head);
|
|
203
198
|
const errLine = ui.msgline();
|
|
204
199
|
sheet.body.append(errLine);
|
|
@@ -213,6 +208,7 @@ function mountModels(ctx) {
|
|
|
213
208
|
const safeCount = models.filter((m) => m.safety?.allow).length;
|
|
214
209
|
stateChip.textContent = `${s.lastRefresh}: ${fmtTs(state?.catalogAt, s)} \xB7 ${safeCount}/${models.length} ${s.models} \xB7 ${state?.safeOnly === false ? s.safeOnlyOff : s.safeOnlyOn}`;
|
|
215
210
|
currentChip.textContent = `${s.currentModel}: ${state?.model || "\u2014"}`;
|
|
211
|
+
procChip.textContent = state?.running ? s.procRunning : s.procStopped;
|
|
216
212
|
}
|
|
217
213
|
function pickable(m) {
|
|
218
214
|
if (state?.safeOnly === false) return true;
|
|
@@ -352,7 +348,6 @@ function mountAccount(ctx) {
|
|
|
352
348
|
{ k: s.source, v: state?.tokenSource ?? "\u2014" },
|
|
353
349
|
{ k: s.token, v: state?.tokenMask ?? "\u2014" },
|
|
354
350
|
{ k: s.account, v: state?.account?.name ?? "\u2014" },
|
|
355
|
-
{ k: s.region, v: state?.region ?? "\u2014" },
|
|
356
351
|
{ k: s.upstream, v: state?.baseUrl ?? "\u2014" }
|
|
357
352
|
];
|
|
358
353
|
if (state?.lastError) rows.push({ k: s.lastError, v: state.lastError });
|
|
@@ -415,53 +410,14 @@ function mountAccount(ctx) {
|
|
|
415
410
|
errLine.textContent = String(err?.message ?? err);
|
|
416
411
|
});
|
|
417
412
|
}
|
|
418
|
-
function mountPriority(ctx) {
|
|
419
|
-
const s = strings(ctx);
|
|
420
|
-
const { ui } = ctx;
|
|
421
|
-
const sheet = ui.sheet({ title: s.priorityTitle, desc: s.priorityDesc });
|
|
422
|
-
ctx.root.append(sheet.el);
|
|
423
|
-
const statusLine = ui.msgline();
|
|
424
|
-
function apply(r) {
|
|
425
|
-
if (!r) return;
|
|
426
|
-
box.setChecked(r.prefer === true);
|
|
427
|
-
statusLine.textContent = r.running ? s.procRunning : s.procStopped;
|
|
428
|
-
}
|
|
429
|
-
const box = ui.checkbox(s.priorityLabel, {
|
|
430
|
-
checked: false,
|
|
431
|
-
title: s.priorityDesc,
|
|
432
|
-
onChange: async (checked) => {
|
|
433
|
-
try {
|
|
434
|
-
const r = await ctx.invoke("set", [checked]);
|
|
435
|
-
if (r?.ok) {
|
|
436
|
-
apply(r);
|
|
437
|
-
ui.toast(checked ? s.enabledOn : s.enabledOff);
|
|
438
|
-
} else {
|
|
439
|
-
apply(r);
|
|
440
|
-
box.setChecked(false);
|
|
441
|
-
ui.toast(r?.error ?? s.noProcess, "bad");
|
|
442
|
-
}
|
|
443
|
-
} catch (err) {
|
|
444
|
-
box.setChecked(false);
|
|
445
|
-
ui.toast(String(err?.message ?? err), "bad");
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
sheet.body.append(box.el, statusLine);
|
|
450
|
-
async function load() {
|
|
451
|
-
apply(await ctx.invoke("state"));
|
|
452
|
-
}
|
|
453
|
-
ctx.stream({
|
|
454
|
-
message: () => load().catch(() => {
|
|
455
|
-
})
|
|
456
|
-
});
|
|
457
|
-
load().catch((err) => {
|
|
458
|
-
statusLine.textContent = String(err?.message ?? err);
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
413
|
function fmtNum(n) {
|
|
462
414
|
if (typeof n !== "number" || !Number.isFinite(n)) return "\u2014";
|
|
463
415
|
return String(Math.round(n * 100) / 100);
|
|
464
416
|
}
|
|
417
|
+
function fmtCredit(bucket) {
|
|
418
|
+
const known = fmtNum(bucket?.credit);
|
|
419
|
+
return (bucket?.creditUnknown ?? 0) > 0 ? `${known} + ?` : known;
|
|
420
|
+
}
|
|
465
421
|
function mountUsage(ctx) {
|
|
466
422
|
const s = strings(ctx);
|
|
467
423
|
const { ui } = ctx;
|
|
@@ -477,13 +433,22 @@ function mountUsage(ctx) {
|
|
|
477
433
|
const errLine = ui.msgline();
|
|
478
434
|
sheet.body.append(errLine);
|
|
479
435
|
const overview = ui.table({
|
|
480
|
-
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
|
+
],
|
|
481
446
|
maxHeight: "220px"
|
|
482
447
|
});
|
|
483
448
|
sheet.body.append(overview.el);
|
|
484
449
|
const modelSection = ui.section(s.byModel);
|
|
485
450
|
const modelTable = ui.table({
|
|
486
|
-
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],
|
|
487
452
|
maxHeight: "260px"
|
|
488
453
|
});
|
|
489
454
|
modelSection.append(modelTable.el);
|
|
@@ -509,21 +474,43 @@ function mountUsage(ctx) {
|
|
|
509
474
|
let state = null;
|
|
510
475
|
function spanRow(label, bucket) {
|
|
511
476
|
const b = bucket ?? {};
|
|
512
|
-
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
|
+
];
|
|
513
487
|
}
|
|
514
488
|
function render() {
|
|
515
489
|
const totals = state?.summary?.totals ?? {};
|
|
516
|
-
todayChip.textContent = `${s.todayCredit}: ${
|
|
490
|
+
todayChip.textContent = `${s.todayCredit}: ${fmtCredit(totals.today)} \xB7 ${s.todayTokens}: ${fmtNum(
|
|
517
491
|
(totals.today?.prompt ?? 0) + (totals.today?.completion ?? 0)
|
|
518
492
|
)}`;
|
|
519
493
|
balanceChip.textContent = `${s.balanceTitle}: ${fmtNum(state?.balance?.total)}`;
|
|
520
|
-
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
|
+
];
|
|
521
500
|
overview.clear();
|
|
522
501
|
for (const row of spans) overview.addRow(row);
|
|
523
502
|
const byModel = state?.summary?.byModel ?? [];
|
|
524
503
|
modelTable.clear(byModel.length > 0 ? void 0 : s.noData);
|
|
525
504
|
for (const m of byModel) {
|
|
526
|
-
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
|
+
]);
|
|
527
514
|
}
|
|
528
515
|
const packages = state?.balance?.packages ?? [];
|
|
529
516
|
pkgTable.clear(packages.length > 0 ? void 0 : s.noData);
|
|
@@ -620,7 +607,6 @@ var client_default = {
|
|
|
620
607
|
panels: {
|
|
621
608
|
models: { mount: mountModels },
|
|
622
609
|
account: { mount: mountAccount },
|
|
623
|
-
priority: { mount: mountPriority },
|
|
624
610
|
usage: { mount: mountUsage }
|
|
625
611
|
}
|
|
626
612
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-workbuddy",
|
|
3
|
-
"version": "0.3.
|
|
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",
|
|
@@ -78,6 +78,6 @@
|
|
|
78
78
|
"win32"
|
|
79
79
|
],
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"esbuild": "
|
|
81
|
+
"esbuild": "0.28.2"
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -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('✓ 清理完成;扩展本体不会重新注入,卸载也不需要还原。');
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 显式卸载清理:只清本扩展自己的配置块、三份数据文件和手动 token/哨兵。
|
|
4
|
+
* 宿主界面卸载只删扩展包,不会调用 World.stop();普通 stop()/重启又不能删用户数据,
|
|
5
|
+
* 所以清理由用户在卸载后主动执行。--check 只读,--clean 才修改,且绝不打印 token。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node scripts/clean-workbuddy.mjs --check
|
|
9
|
+
* node scripts/clean-workbuddy.mjs --clean
|
|
10
|
+
* 隔离验证可显式传 --bot-dir <部署目录> --data-dir <数据目录>。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { findAppRoot } from './lib/app-root.mjs';
|
|
16
|
+
|
|
17
|
+
export const WORKBUDDY_DATA_FILES = ['workbuddy-usage.jsonl', 'workbuddy-catalog.json', 'workbuddy-balance.json'];
|
|
18
|
+
|
|
19
|
+
function readConfig(path) {
|
|
20
|
+
if (!existsSync(path)) return { value: null, text: '' };
|
|
21
|
+
const text = readFileSync(path, 'utf8');
|
|
22
|
+
return { value: JSON.parse(text), text };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 返回残留位置与布尔状态,不返回任何凭据内容。 */
|
|
26
|
+
export function inspectWorkbuddyResidue({ botDir, dataDir }) {
|
|
27
|
+
const configPath = join(botDir, 'config.json');
|
|
28
|
+
const envPath = join(botDir, '.env');
|
|
29
|
+
const config = readConfig(configPath);
|
|
30
|
+
const env = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
|
|
31
|
+
return {
|
|
32
|
+
botDir,
|
|
33
|
+
dataDir,
|
|
34
|
+
configPath,
|
|
35
|
+
envPath,
|
|
36
|
+
configPresent: config.value?.worlds && Object.hasOwn(config.value.worlds, 'workbuddy'),
|
|
37
|
+
envPresent: /^[\t ]*accessToken[\t ]*=/m.test(env),
|
|
38
|
+
dataFiles: WORKBUDDY_DATA_FILES.map((name) => join(dataDir, name)).filter((path) => existsSync(path)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 幂等清理;返回清理前后状态供 CLI 与契约测试核对。 */
|
|
43
|
+
export function cleanWorkbuddyResidue({ botDir, dataDir, clean = false }) {
|
|
44
|
+
const before = inspectWorkbuddyResidue({ botDir, dataDir });
|
|
45
|
+
if (clean) {
|
|
46
|
+
if (before.configPresent) {
|
|
47
|
+
const config = readConfig(before.configPath);
|
|
48
|
+
delete config.value.worlds.workbuddy;
|
|
49
|
+
const indent = /^\{\r?\n([\t ]+)\S/.exec(config.text)?.[1] ?? ' ';
|
|
50
|
+
const newline = config.text.includes('\r\n') ? '\r\n' : '\n';
|
|
51
|
+
const trailing = config.text.endsWith(newline) ? newline : '';
|
|
52
|
+
writeFileSync(before.configPath, `${JSON.stringify(config.value, null, indent)}${trailing}`, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
if (before.envPresent) {
|
|
55
|
+
const env = readFileSync(before.envPath, 'utf8');
|
|
56
|
+
const next = env.replace(/^[\t ]*accessToken[\t ]*=.*(?:\r?\n|$)/gm, '');
|
|
57
|
+
writeFileSync(before.envPath, next, 'utf8');
|
|
58
|
+
}
|
|
59
|
+
for (const path of before.dataFiles) rmSync(path, { force: true });
|
|
60
|
+
}
|
|
61
|
+
return { before, after: inspectWorkbuddyResidue({ botDir, dataDir }) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function argValue(name) {
|
|
65
|
+
const index = process.argv.indexOf(name);
|
|
66
|
+
return index >= 0 ? process.argv[index + 1] : '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runCli() {
|
|
70
|
+
const check = process.argv.includes('--check');
|
|
71
|
+
const clean = process.argv.includes('--clean');
|
|
72
|
+
if (check === clean) {
|
|
73
|
+
console.error('用法:node scripts/clean-workbuddy.mjs --check | --clean(二选一)');
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
const pkgDir = fileURLToPath(new URL('..', import.meta.url));
|
|
77
|
+
const appRoot = process.env.COOPANION_APP_ROOT || findAppRoot({ packageDir: pkgDir });
|
|
78
|
+
const defaultBotDir = appRoot ? join(appRoot, '..', '..', 'data', 'home', 'companion') : '';
|
|
79
|
+
const botDir = resolve(argValue('--bot-dir') || defaultBotDir);
|
|
80
|
+
const dataDir = resolve(argValue('--data-dir') || join(defaultBotDir, 'data'));
|
|
81
|
+
if (!defaultBotDir && (!argValue('--bot-dir') || !argValue('--data-dir'))) {
|
|
82
|
+
console.error('✗ 找不到 Coopanion 部署目录;请显式传 --bot-dir 与 --data-dir');
|
|
83
|
+
process.exit(2);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const result = cleanWorkbuddyResidue({ botDir, dataDir, clean });
|
|
87
|
+
const state = clean ? result.after : result.before;
|
|
88
|
+
console.log(`配置 worlds.workbuddy:${state.configPresent ? '存在' : '无'}`);
|
|
89
|
+
console.log(`.env accessToken:${state.envPresent ? '存在(值未读取到输出)' : '无'}`);
|
|
90
|
+
for (const path of state.dataFiles) console.log(`数据文件:${path}`);
|
|
91
|
+
if (!clean) console.log('(--check:未做任何改动;卸载扩展后执行 --clean 清理)');
|
|
92
|
+
const left = state.configPresent || state.envPresent || state.dataFiles.length > 0;
|
|
93
|
+
console.log(clean && !left ? '✓ 本扩展的配置、数据与凭据残留已清理' : left ? '发现上述本扩展残留' : '✓ 没有本扩展残留');
|
|
94
|
+
process.exit(clean && left ? 1 : 0);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`✗ 清理失败:${err?.message ?? err}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) runCli();
|