cotutor 0.4.0 → 0.4.2
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/README.md +39 -8
- package/dist/app/cli/cert.d.ts +6 -0
- package/dist/app/cli/cert.js +16 -2
- package/dist/app/cli/doctor.d.ts +5 -0
- package/dist/app/cli/doctor.js +65 -17
- package/dist/app/cli/init.js +1 -1
- package/dist/app/cli/main.js +58 -17
- package/dist/app/cli/serve.d.ts +0 -1
- package/dist/app/cli/serve.js +5 -0
- package/dist/app/cli/service.d.ts +40 -0
- package/dist/app/cli/service.js +50 -5
- package/dist/app/cli/tray-run.js +11 -2
- package/dist/app/cli/tray.d.ts +35 -4
- package/dist/app/cli/tray.js +67 -13
- package/dist/app/cli/update-cmd.d.ts +38 -0
- package/dist/app/cli/update-cmd.js +174 -0
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +4 -3
- package/dist/app/http/app.d.ts +2 -0
- package/dist/app/http/app.js +19 -2
- package/dist/app/machine/bailian.d.ts +26 -0
- package/dist/app/machine/bailian.js +91 -4
- package/dist/app/machine/update.d.ts +38 -0
- package/dist/app/machine/update.js +109 -0
- package/dist/brain/api.js +6 -3
- package/dist/realtime/preview.d.ts +16 -0
- package/dist/realtime/preview.js +43 -1
- package/dist/runtime/live/call.d.ts +2 -0
- package/dist/runtime/live/call.js +6 -1
- package/dist/shared/upgrade.d.ts +9 -0
- package/dist/shared/upgrade.js +23 -0
- package/dist/ui/kid/kid-page.js +8 -2
- package/dist/ui/parent/parent-home-page.js +47 -2
- package/package.json +11 -2
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +1 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cotutor update:升级一条命令管到底。问 npm 有没有新版本 → 换程序 → 每个孩子更新出厂文件、重起服务和图标。
|
|
3
|
+
* 不自动跑:要重起服务,孩子可能在上课,所以孩子在通话时不动(--anyway 才动)。npm 没成就停,服务一点没碰,还在跑旧版。
|
|
4
|
+
* 换完程序以后只用子进程跑新的 bin:这个进程还是旧代码,再 import 会读到新文件,新旧混着。所以这里用到的模块都写在文件头,别改成动态 import。
|
|
5
|
+
* 再跑一次也行:程序已经是新的、只差重起的(手动 npm i -g 过、上次中断了),接着把后面做完。
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { isNewer } from "../../shared/upgrade.js";
|
|
11
|
+
import { BUILD } from "../build.js";
|
|
12
|
+
import { fetchRelease } from "../machine/update.js";
|
|
13
|
+
import { binPath, listServices, probeHealth, realHost, stableNode } from "./service.js";
|
|
14
|
+
import { inspectService } from "./tray.js";
|
|
15
|
+
const MIRROR = 'https://registry.npmmirror.com';
|
|
16
|
+
const nodeTooOld = (have, min) => {
|
|
17
|
+
const [major = 0, minor = 0] = have.replace(/^v/, '').split('.').map(Number);
|
|
18
|
+
return major < min[0] || (major === min[0] && minor < min[1]);
|
|
19
|
+
};
|
|
20
|
+
/** 只说要家长知道的:升好了的、已是最新的不用一行行念 */
|
|
21
|
+
const worthSaying = (out) => out.split('\n').map((l) => l.trimEnd()).filter((l) => l.trim() && !/已是最新|已换新/.test(l));
|
|
22
|
+
async function updateOne(d, s, target) {
|
|
23
|
+
let ok = true;
|
|
24
|
+
const up = await d.cli(['upgrade', '--workspace', s.root]);
|
|
25
|
+
for (const l of worthSaying(up.out))
|
|
26
|
+
d.print(` ${l}`);
|
|
27
|
+
if (up.code !== 0) {
|
|
28
|
+
d.print(` ! ${s.kid}:出厂文件没更新完,先接着往下走`);
|
|
29
|
+
ok = false;
|
|
30
|
+
}
|
|
31
|
+
// 以前装的写死了带版本号的 node(Homebrew):趁这次换掉,不然下次 brew upgrade 就起不来。--no-tray:图标下一步 restart 管,没开图标的不能装回来
|
|
32
|
+
if (d.stableNode(s.node) !== s.node) {
|
|
33
|
+
const re = await d.cli(['service', 'install', '--no-open', '--no-tray', '--workspace', s.root]);
|
|
34
|
+
if (re.code !== 0) {
|
|
35
|
+
for (const l of worthSaying(re.out))
|
|
36
|
+
d.print(` ${l}`);
|
|
37
|
+
ok = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const restart = await d.cli(['service', 'restart', '--workspace', s.root]);
|
|
41
|
+
for (const l of restart.out.split('\n'))
|
|
42
|
+
if (l.trim().startsWith('!'))
|
|
43
|
+
d.print(` ${l.trim()}`);
|
|
44
|
+
const h = await d.health(s.port);
|
|
45
|
+
if (restart.code === 0 && h?.version === target)
|
|
46
|
+
d.print(`✓ ${s.kid}:在跑 ${target}`);
|
|
47
|
+
else {
|
|
48
|
+
d.print(`! ${s.kid}:服务没换上 ${target}(在跑 ${h?.version ?? '没答应'})。cotutor service logs --workspace ${s.root} 看看`);
|
|
49
|
+
ok = false;
|
|
50
|
+
}
|
|
51
|
+
const doc = await d.cli(['doctor', '--workspace', s.root, '--json']);
|
|
52
|
+
try {
|
|
53
|
+
const checks = JSON.parse(doc.out).checks;
|
|
54
|
+
const migrate = checks.find((c) => c.name === 'config.migrate' && !c.ok);
|
|
55
|
+
if (migrate)
|
|
56
|
+
d.print(` ! ${s.kid} 的配置要迁移:${migrate.cmd ?? 'cotutor upgrade --config'}`);
|
|
57
|
+
}
|
|
58
|
+
catch { /* 体检读不了不影响升级 */ }
|
|
59
|
+
return ok;
|
|
60
|
+
}
|
|
61
|
+
export async function runUpdate(d, o = {}) {
|
|
62
|
+
if (d.fromRepo) {
|
|
63
|
+
d.print('这份是从仓库跑的:git pull,再 cotutor upgrade、cotutor service restart。cotutor update 只管 npm 装的。');
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const services = await d.services();
|
|
67
|
+
const rel = await d.release();
|
|
68
|
+
const newer = rel !== null && isNewer(rel.version, d.current);
|
|
69
|
+
// 程序已经是新的、服务还在跑旧版:手动 npm i -g 过,或者上次中断了
|
|
70
|
+
const behind = services.filter((s) => s.health?.version && s.health.version !== d.current);
|
|
71
|
+
if (!newer && !behind.length) {
|
|
72
|
+
if (!rel) {
|
|
73
|
+
d.print('问不到 npm 上的版本(断网或连不上),现在是 ' + d.current + '。过一会儿再试。');
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
d.print(`已经是最新的:${d.current}`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const busy = services.filter((s) => (s.health?.calls ?? 0) > 0);
|
|
80
|
+
if (busy.length && !o.anyway) {
|
|
81
|
+
d.print(`${busy.map((s) => s.kid).join('、')}正在和老师通话,升级要重起服务,会挂断。等下课再升;一定要现在升:cotutor update --anyway`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
let target = d.current;
|
|
85
|
+
if (newer) {
|
|
86
|
+
if (rel.minNode && nodeTooOld(d.nodeVersion, rel.minNode)) {
|
|
87
|
+
d.print(`新版本 ${rel.version} 要 Node ${rel.minNode.join('.')} 以上,这台电脑是 ${d.nodeVersion}。先到 https://nodejs.org 装新的 LTS 版,再跑一次 cotutor update。服务没动。`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
d.print(`现在是 ${d.current},新版本是 ${rel.version}`);
|
|
91
|
+
for (const n of rel.notes)
|
|
92
|
+
d.print(` · ${n}`);
|
|
93
|
+
d.print('正在下载新版本,要一两分钟……');
|
|
94
|
+
const got = await d.npm(rel.version, rel.mirror ? MIRROR : null);
|
|
95
|
+
if (got.code !== 0) {
|
|
96
|
+
const tail = worthSaying(got.out).slice(-6).map((l) => ` ${l}`).join('\n');
|
|
97
|
+
d.print(/EACCES|EPERM/.test(got.out)
|
|
98
|
+
? `这个账户写不了 npm 的全局目录,服务没动,还在跑 ${d.current}。当初是用 sudo 装的话,在终端里跑 sudo npm i -g cotutor@${rel.version},再跑一次 cotutor update。\n${tail}`
|
|
99
|
+
: `下载没成,服务没动,还在跑 ${d.current}。连不上 npm 的话过一会儿再试。\n${tail}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const now = d.installed();
|
|
103
|
+
if (now !== rel.version) {
|
|
104
|
+
d.print(`npm 说装好了,但这里还是 ${now ?? '读不到版本'}:npm 可能装到了别的目录(npm prefix -g 看装到了哪)。服务没动。`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
target = rel.version;
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
d.print(`程序已经是 ${d.current},把${behind.map((s) => s.kid).join('、')}的服务换上去`);
|
|
111
|
+
let ok = true;
|
|
112
|
+
for (const s of services)
|
|
113
|
+
ok = (await updateOne(d, s, target)) && ok;
|
|
114
|
+
if (!services.length) {
|
|
115
|
+
const root = d.workspace();
|
|
116
|
+
if (root) {
|
|
117
|
+
const up = await d.cli(['upgrade', '--workspace', root]);
|
|
118
|
+
for (const l of worthSaying(up.out))
|
|
119
|
+
d.print(` ${l}`);
|
|
120
|
+
if (up.code !== 0)
|
|
121
|
+
ok = false;
|
|
122
|
+
d.print('没装后台服务:在终端里跑的 cotutor serve 要关掉重开才用上新版。');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!ok)
|
|
126
|
+
return 1;
|
|
127
|
+
d.print(newer ? `升好了:${d.current} → ${target}` : `都换好了:${target}`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const capture = (cmd, args, shell = false) => new Promise((resolve) => {
|
|
131
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell });
|
|
132
|
+
let out = '';
|
|
133
|
+
const take = (c) => { out += c.toString('utf8'); };
|
|
134
|
+
child.stdout?.on('data', take);
|
|
135
|
+
child.stderr?.on('data', take);
|
|
136
|
+
child.on('error', (err) => resolve({ code: 1, out: `${out}${err.message}\n` }));
|
|
137
|
+
child.on('close', (code) => resolve({ code: code ?? 1, out }));
|
|
138
|
+
});
|
|
139
|
+
/** 用跟这个 node 配套的 npm:PATH 里排第一的可能是另一套 Node 的,会装到别处。Windows 上 .cmd 要经 shell 才跑得起来(Node 18.20 起),路径带空格要引起来 */
|
|
140
|
+
function runNpm(spec, registry) {
|
|
141
|
+
const win = process.platform === 'win32';
|
|
142
|
+
const near = join(dirname(process.execPath), win ? 'npm.cmd' : 'npm');
|
|
143
|
+
const npm = existsSync(near) ? near : win ? 'npm.cmd' : 'npm';
|
|
144
|
+
const args = ['i', '-g', `cotutor@${spec}`, '--no-audit', '--no-fund', ...(registry ? [`--registry=${registry}`] : [])];
|
|
145
|
+
return capture(win ? `"${npm}"` : npm, args, win);
|
|
146
|
+
}
|
|
147
|
+
export function realUpdateDeps(workspace) {
|
|
148
|
+
const host = realHost();
|
|
149
|
+
const bin = binPath();
|
|
150
|
+
return {
|
|
151
|
+
current: BUILD.version ?? '0.0.0',
|
|
152
|
+
fromRepo: Boolean(BUILD.commit),
|
|
153
|
+
nodeVersion: process.version,
|
|
154
|
+
release: () => fetchRelease(),
|
|
155
|
+
services: async () => Promise.all((await listServices(host)).map(async (s) => {
|
|
156
|
+
const w = await inspectService(s);
|
|
157
|
+
return { name: s.name, kid: w.kid, root: s.root, port: w.port, node: s.args[0], health: await probeHealth(w.port) };
|
|
158
|
+
})),
|
|
159
|
+
health: (port) => probeHealth(port),
|
|
160
|
+
npm: runNpm,
|
|
161
|
+
installed: () => {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(readFileSync(join(dirname(bin), '..', 'package.json'), 'utf8')).version;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
cli: (args) => capture(process.execPath, [bin, ...args]),
|
|
170
|
+
workspace,
|
|
171
|
+
stableNode,
|
|
172
|
+
print: (line) => process.stdout.write(`${line}\n`),
|
|
173
|
+
};
|
|
174
|
+
}
|
package/dist/app/cli/usage.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** 命令用法(cotutor --help;技能 cotutor-analyze 的 references/命令与文件.md 从这里取分析用的那几行,所以放单独文件,别的模块 import 不会跑到 CLI) */
|
|
2
|
-
export declare const USAGE = "\u7528\u6CD5:\n cotutor init <slug> [--dir <path>] [--name <\u5B69\u5B50\u540D>] [--grade <1-6>] [--port <n>] \u5EFA ~/cotutor/<slug>/ \u9AA8\u67B6(\u5E42\u7B49\u8865\u7F3A;\u9ED8\u8BA4\u4E8C\u5E74\u7EA7\u6559\u5B66\u5305;\u7AEF\u53E3\u5728\u8FD9\u91CC\u9009\u4E00\u6B21\u5199\u8FDB cotutor.json:443 \u4F18\u5148,\u4E0D\u884C\u5C31 8443\u30018445\u2026,\u4EE5\u540E\u4E0D\u53D8)\n cotutor grades [--json] \u5217\u51FA\u672C\u5305\u53EF\u5B89\u88C5\u7684\u5E74\u7EA7\u6559\u5B66\u5305\n cotutor doctor [--workspace <dir>] [--json] [--live] \u9010\u9879\u4F53\u68C0;--live \u771F\u8FDE\u4E00\u6B21\u767E\u70BC\u5B9E\u65F6\u6A21\u578B\u3001\u771F\u5408\u6210\u4E00\u53E5\u914D\u97F3(\u82B1\u4E00\u5206\u94B1)\u628A API \u5C42\u7684\u5751\u6446\u51FA\u6765(\u771F\u8DD1\u8001\u5E08\u7528 cotutor send)\n cotutor upgrade [--workspace <dir>] [--force <\u8001\u5E08>]... \u8001\u5E08\u6587\u4EF6\u3001activity \u4E0E skill \u6362\u65B0\u7248:\u6CA1\u6539\u8FC7\u7684\u76F4\u63A5\u6362,\u6539\u8FC7\u7684\u53EA\u62A5 diff(--force <\u8001\u5E08> \u6216 --force <\u8001\u5E08>/<\u540D> \u624D\u8986\u76D6,\u539F\u6587\u7559 .bak)\n cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json \u8865\u7F3A:\u65B0\u51FA\u5382\u8001\u5E08 / \u8FD0\u884C\u65F6 / \u547D\u4EE4\u6A21\u677F\u65D7\u6807(\u53EA\u52A0\u7F3A\u7684,\u4F60\u6539\u8FC7\u7684\u503C\u4E0D\u52A8)\n cotutor add <\u8001\u5E08\u540D> --display <\u663E\u793A\u540D> [--subject <\u5B66\u79D1>] [--avatar <emoji|\u56FE\u7247\u76F8\u5BF9\u8DEF\u5F84>] [--hidden] \u52A0\u4E00\u4F4D\u81EA\u5BB6\u7684\u8001\u5E08:\u51FA\u6A21\u677F\u6587\u4EF6\u3001\u8FDB cotutor.json\u3001\u5EFA\u76EE\u5F55(\u540D\u5B57\u4EE5 -tutor \u7ED3\u5C3E\u7684\u662F\u8001\u5E08,figshot \u7ED9\u5B83\u505A\u5934\u50CF)\n cotutor add-theme <\u4E3B\u9898\u540D> [--from <\u4E3B\u9898>] [--workspace <dir>] \u52A0\u4E00\u4E2A\u81EA\u5BB6\u7684\u4E3B\u9898:\u62F7\u4E00\u4EFD(\u7F3A\u7701\u51FA\u5382\u7684 default)\u5230 themes/<\u4E3B\u9898\u540D>/,\u6539 cotutor.json \u7684 kid.theme \u6362\u8FC7\u53BB\n cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <\u76EE\u5F55>] \u5728\u7EC8\u7AEF\u91CC\u8D77\u670D\u52A1(\u4E00 workspace \u4E00\u8FDB\u7A0B;~/.config/cotutor/certs/ \u6709\u8BC1\u4E66\u5C31\u8D70 HTTPS;--trace \u6BCF\u4E00\u8F6E\u7684\u4E8B\u4EF6\u6309\u9053\u6253\u5370;\u53EA\u6253\u5370\u5BB6\u957F\u7AEF\u9996\u9875\u7684\u5730\u5740,\u5B69\u5B50\u7684\u5E73\u677F\u5728\u90A3\u91CC\u626B\u7801;--open \u987A\u624B\u6253\u5F00,\u65E7\u540D --open-qr \u7167\u6837\u8BA4;--log-dir \u8F93\u51FA\u6309\u5929\u843D\u6587\u4EF6,\u540E\u53F0\u670D\u52A1\u7528;\u5173\u4E86\u7EC8\u7AEF\u5C31\u505C,\u4E00\u76F4\u5F00\u7740\u7528 cotutor service)\n cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] \u6253\u5F00\u5BB6\u957F\u7AEF(\u8BFB cotutor.json,\u4E0D\u7528\u8BB0\u7AEF\u53E3);--sandbox \u5F00\u8BD5\u7528\u7684\u90A3\u4E2A(\u7AEF\u53E3\u6C38\u8FDC\u662F\u771F\u7684 +1);--print \u53EA\u6253\u5370\u5730\u5740;\u6CA1\u5728\u8DD1\u5C31\u8BF4\u600E\u4E48\u8D77,\u4EE5 1 \u9000\n cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] \u628A serve \u88C5\u6210\u8FD9\u53F0\u7535\u8111\u7684\u540E\u53F0\u670D\u52A1(macOS launchd\u3001Linux systemd \u7528\u6237\u670D\u52A1\u3001Windows \u7528\u6237\u7684\u767B\u5F55\u9879,\u90FD\u4E0D\u8981\u7BA1\u7406\u5458):\u767B\u5F55\u540E\u81EA\u5DF1\u8D77\u6765\u3001\u5D29\u4E86 10 \u79D2\u540E\u518D\u8D77\u3001\u65E5\u5FD7\u6309\u5929\u843D\u6587\u4EF6\u7559 14 \u5929;\u4E00\u4E2A workspace \u4E00\u4E2A;\u6362\u4E86 node \u7248\u672C\u6216\u5305\u632A\u4E86\u5730\u65B9\u518D install \u4E00\u6B21\n cotutor tray on | off | status [--json] \u83DC\u5355\u680F / \u4EFB\u52A1\u680F\u56FE\u6807(macOS\u3001Windows;service install \u65F6\u4E00\u8D77\u88C5,--no-tray \u4E0D\u5E26):\u6B63\u5E38\u65F6\u53EA\u6709 logo,\u6709\u4E8B\u51FA\u6A59\u70B9,\u6CA1\u5728\u8DD1\u65F6\u53D8\u6DE1\u52A0\u7EA2\u70B9\u5E76\u901A\u77E5\u4E00\u6B21;\u70B9\u5B83\u770B\u72B6\u6001\u3001\u6253\u5F00\u5BB6\u957F\u7AEF\u3001\u91CD\u542F\u3001\u770B\u65E5\u5FD7\u3001\u9000\u51FA;off \u5173\u6389\u5E76\u8BB0\u4F4F\n cotutor service status [--json] | logs [--lines <n>] [--follow] \u540E\u53F0\u670D\u52A1\u5728\u4E0D\u5728\u8DD1\u3001\u7AEF\u53E3\u7B54\u4E0D\u7B54\u5E94\u3001\u8DD1\u7684\u54EA\u4E2A\u7248\u672C\u3001vault \u8BFB\u4E0D\u8BFB\u5F97\u5230\u3001\u65E5\u5FD7\u5728\u54EA;logs \u770B\u6700\u8FD1\u7684\u65E5\u5FD7\n cotutor pin [off | status] [--pin <\u6570\u5B57>] [--json] \u5BB6\u957F\u7AEF /parent \u4E0E\u5DE5\u4F5C\u53F0 /dev \u7684\u9501:\u673A\u5668\u7EA7 PIN(\u6240\u6709 workspace \u5171\u7528),iPad \u4E0A\u8F93\u4E00\u6B21\u3001\u8FD9\u53F0\u7535\u8111\u514D\u5BC6;\u6CA1\u8BBE\u5C31\u5168\u5F00(\u5BB6\u957F\u5728\u65C1\u8FB9\u53EF\u4EE5\u4E0D\u8BBE);off \u64A4\u6389\n cotutor cert [--host <\u540D\u6216IP>]... \u7528 mkcert \u5EFA\u8FD9\u53F0\u673A\u5668\u7684\u81EA\u7B7E\u8BC1\u4E66\u5230 ~/.config/cotutor/certs/(\u5E73\u677F\u3001\u624B\u673A\u4E0A\u5F55\u97F3\u8981 HTTPS;\u6240\u6709 workspace \u5171\u7528)\n cotutor send <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--runtime <\u540D>] [--new] [--photo a.jpg,b.png] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--lane main,tts] [--quiet] \u7EC8\u7AEF\u91CC\u53D1\u4E00\u6761,\u73B0\u573A\u6309\u9053\u6253\u5370\u6BCF\u9053\u5DE5\u5E8F\u7684\u4E8B\u4EF6,\u8BF4\u5B8C\u6253\u5370\u7ED3\u679C(\u5B69\u5B50\u7684\u8BDD\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF:\u4E0A\u4E0B\u6587\u5305\u5E26 live:\u3001\u4E0D\u8865\u63D0\u95EE\u5361,\u53EA\u662F\u6CA1\u6709\u5634;--from system \u4E0D\u7B97;--new \u5F00\u65B0\u8BDD\u9898;--photo \u5E26\u4F5C\u4E1A\u7167\u7247,\u62F7\u8FDB captures/,\u6709\u7167\u7247\u65F6\u6D88\u606F\u53EF\u4EE5\u7A7A;--activity \u50CF\u5B69\u5B50\u70B9\u4E86\u9996\u9875\u7684 activity \u6309\u94AE,\u65B0\u5F00\u8BDD\u9898,\u53C2\u6570\u540C\u6309\u94AE\u4E0B\u9762\u90A3\u51E0\u884C;--quiet \u53EA\u8981\u7ED3\u679C)\n cotutor pack <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--at <ISO\u65F6\u95F4>] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--json] \u5E72\u8DD1\u4E0A\u4E0B\u6587\u5305(--activity \u770B activity \u8BDD\u9898\u7B2C\u4E00\u6761\u8001\u5E08\u62FF\u5230\u4EC0\u4E48):\u4E0D\u8D77\u6A21\u578B,\u6253\u5370\u73B0\u5728\u4F1A\u53D1\u7ED9\u8001\u5E08\u7684\u90A3\u4EFD + \u6BCF\u6BB5\u6765\u81EA\u54EA\u4E2A\u6587\u4EF6\u3001\u90A3\u91CC\u4E00\u5171\u51E0\u6761\u3001\u6309\u653F\u7B56\u5E26\u4E86\u51E0\u6761(\u6539\u4E86\u6863\u6848 / \u65E5\u8BB0 / \u8BA1\u5212\u7ACB\u523B\u770B\u6548\u679C)\n cotutor replay <\u8001\u5E08> <job> [<\u65E5\u671F>] [--runtime <\u540D>] [--json] \u56DE\u653E\u4E00\u8F6E:\u540C\u4E00\u95EE\u6309\u73B0\u5728\u7684 vault \u4E0E\u63D0\u793A\u8BCD\u518D\u8DD1\u4E00\u904D(\u843D evals/,\u4E0D\u8FDB\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u4E0D\u914D\u97F3),\u8DD1\u5B8C\u5E76\u6392\u6253\u5370\u4E0A\u4E0B\u6587\u5305 / \u8BB2\u7A3F / \u5361 / \u8BFB\u4E86\u4EC0\u4E48\u7684 diff\n cotutor compare <\u8001\u5E08> [<evalJob>] [<\u65E5\u671F>] [--json] \u518D\u770B\u4E00\u6B21\u56DE\u653E\u7684\u5BF9\u7167(\u4E0D\u7ED9 evalJob \u5C31\u5217\u8FD9\u5929\u56DE\u653E\u8FC7\u54EA\u4E9B)\n cotutor show <\u8001\u5E08> <job> [<\u65E5\u671F>] [--json] [--evals] [--workspace <dir>] \u770B\u4E00\u8F6E:\u95EE\u4E86\u4EC0\u4E48\u3001\u4E0A\u4E0B\u6587\u5305\u3001\u5F53\u65F6\u7684\u8001\u5E08\u6587\u4EF6\u4E0E\u6280\u80FD hash\u3001\u8BB2\u7A3F\u4E0E\u5361\u3001\u8BFB\u4E86\u4EC0\u4E48\u3001token \u4E0E\u7528\u65F6\u3001\u7ED9\u5BB6\u957F\u7684\u5C3E\u5DF4;--json \u662F\u5BB6\u957F\u7AEF\u300C\u770B\u539F\u6587\u300D\u540C\u4E00\u4EFD\u6570\u636E,\u7ED9\u7F16\u7A0B\u52A9\u624B\u5206\u6790\u7528\n cotutor trace <\u8001\u5E08> <job> [<\u65E5\u671F>] [--lane \u2026] [--workspace <dir>] \u56DE\u653E\u4E00\u8F6E\u7684\u4E8B\u4EF6(<\u65E5\u671F>.<job>.events.jsonl;\u6392\u67E5\u6628\u5929\u90A3\u8F6E\u7528)\n cotutor rate <\u8001\u5E08> <\u8BDD\u9898> <1-5> [--date <\u65E5\u671F>] \u7ED9\u4E00\u4E2A\u8BDD\u9898\u6253\u661F(\u4E0E\u5BB6\u957F\u7AEF\u540C\u4E00\u6761\u8DEF;\u2265 vault.keepScore \u7684\u8BDD\u9898\u8BB0\u8D26\u65F6\u6458\u8981\u624D\u8FDB\u65E5\u8BB0)\n cotutor bookkeep <\u8001\u5E08> [--date <\u65E5\u671F>] [--thread <\u8BDD\u9898>]... \u8BB0\u8D26:\u8FD9\u5929\u6BCF\u4E2A\u8FD8\u6CA1\u8BB0\u8FC7\u7684\u8BDD\u9898\u5404\u8D77\u4E00\u8F6E\u8BB0\u8D26\u4EFB\u52A1,\u8001\u5E08\u56DE\u300C## \u8BB0\u8D26\u300D,\u5E94\u7528\u5199\u8FDB vault \u7684\u65E5\u8BB0(\u8BDD\u9898\u540D\u3001\u5B69\u5B50\u95EE\u3001\u6458\u8981\u3001\u89C2\u5BDF)\n cotutor home check [--published] [--json] \u68C0\u67E5\u9996\u9875\u8349\u7A3F home/draft.md(\u6216\u5DF2\u53D1\u5E03\u7684\u90A3\u4EFD):\u5B69\u5B50\u4F1A\u770B\u5230\u54EA\u51E0\u5F20\u5361\u3001\u6BCF\u4F4D\u8001\u5E08\u7684\u6309\u94AE\u3001\u8981\u6539\u7684\u4E0E\u63D0\u9192(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor home publish [--force] [--from <\u6587\u4EF6>] [--json] \u53D1\u5E03\u9996\u9875:\u68C0\u67E5 \u2192 \u7559\u5386\u53F2 home/history/<id>.md \u2192 \u5199 home/published.json(\u5B69\u5B50\u7AEF\u53EA\u8BFB\u5B83);\u6709\u8981\u6539\u7684\u4E0D\u53D1,--force \u4E22\u6389\u90A3\u51E0\u5F20\u7167\u53D1;--from \u53D1\u53E6\u4E00\u4EFD(\u5982\u5386\u53F2)\n cotutor home show [--json] \u73B0\u5728\u53D1\u5E03\u7684\u662F\u54EA\u4EFD\u3001\u51E0\u5929\u524D\u3001\u6BCF\u4E2A\u6309\u94AE\u88AB\u70B9\u4E86\u51E0\u6B21(\u54EA\u5929\u3001\u54EA\u4E2A\u8BDD\u9898)\n cotutor activity check [<\u8001\u5E08> [<\u540D>]] [--json] \u68C0\u67E5 activity \u6587\u4EF6(agents/<\u8001\u5E08>/activities/<\u540D>.md):\u5BB6\u957F\u586B\u7684\u3001\u5E94\u7528\u7B97\u7684\u3001\u5E94\u7528\u62E6\u7684\u3001\u88C1\u6BB5\u3001\u63D2\u69FD\u3001\u6B63\u6587\u7528\u7684\u5361\u3001\u7B2C\u4E00\u53E5\u4E0E\u6536(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor activity eval <\u8001\u5E08> <\u540D> [--runs <n>] [--only <\u4F8B\u5B50>] [--kid-model <\u5343\u95EE\u6A21\u578B>] [--json] \u7167 <\u540D>.\u4F8B\u5B50.md \u8DD1:\u771F\u8001\u5E08 + \u6A21\u62DF\u5B69\u5B50(\u5343\u95EE,\u7F3A\u7701 qwen3.8-flash,\u6709 Token Plan \u8D70\u5B83)\u591A\u8F6E(\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF),\u5224\u53EA\u770B\u677F\u4E66;\u4E00\u4E2A\u4F8B\u5B50\u8DD1 n \u6B21(\u7F3A\u7701 3)\u6B21\u6B21\u90FD\u8FC7\u624D\u7B97\u8FC7;\u843D evals/,\u753B\u5BB6\u662F\u5047\u7684;\u82B1\u94B1\n cotutor chalk <check|build|bake|snap|dub> <scene.ts|\u8BFE\u5305 id> [...] \u505A\u5C0F\u8BFE\u5802\u7684\u8BFE\u5305:check \u6821\u9A8C\u573A\u666F\u3001build \u51FA\u8BFE\u5305\u5230 bundles/\u3001bake \u70E4\u753B\u9762\u3001snap \u6BCF\u6B65\u622A\u56FE\u3001dub \u914D\u97F3(cotutor chalk \u770B\u5404\u81EA\u7684\u9009\u9879;bake / snap \u8981\u672C\u673A\u7684 Chrome)\n cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] \u4E0D\u7ECF\u771F\u5B9E\u8001\u5E08\u4E0E\u767E\u70BC,\u7528\u56FA\u5B9A\u7684\u677F\u4E66 JSON \u8D77\u5B69\u5B50\u7AEF,\u6D4B\u524D\u7AEF\u4EA4\u4E92\u4E0E\u6E32\u67D3(\u4E0D\u9700\u8981 workspace;\u8001\u5E08\u90FD\u8D70\u5B9E\u65F6\u8001\u5E08\u9875,\u5047\u5634\u7167\u811A\u672C\u5FF5\u9759\u97F3\u7684\u58F0\u97F3;nokey = \u6CA1\u914D\u767E\u70BC,\u5B69\u5B50\u7AEF\u53EA\u51FA\u300C\u8001\u5E08\u4EEC\u8FD8\u6CA1\u5230\u300D)\n cotutor --version | --help\nworkspace\u89E3\u6790:--workspace > COTUTOR_WORKSPACE > cwd \u6216\u7956\u5148\u6709 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ \u4E0B\u552F\u4E00\u7684\u5B69\u5B50\u76EE\u5F55\n\u8BD5\u7528:\u8981 workspace \u7684\u547D\u4EE4\u52A0 --sandbox,\u6362\u5230\u771F workspace \u65C1\u8FB9\u7684 .sandbox/<\u76EE\u5F55\u540D>/,\u8BD5\u7684\u5BF9\u8BDD\u4E0E vault \u7684\u5199\u5165\u4E0D\u8FDB\u5B69\u5B50\u7684\u8BB0\u5F55(\u7B2C\u4E00\u6B21\u81EA\u52A8\u5206:\u8BBE\u7F6E\u7167\u62F7,\u5BF9\u8BDD\u4E0E\u7167\u7247\u4E0D\u5E26,vault \u62F7\u4E00\u4EFD;\u6807\u9898\u524D\u52A0\u300C\u8BD5\u7528\u300D\u3001\u7AEF\u53E3 +1\u3001\u81EA\u52A8\u8BB0\u8D26\u5173\u6389);--sandbox --fresh \u91CD\u65B0\u5206\n";
|
|
2
|
+
export declare const USAGE = "\u7528\u6CD5:\n cotutor init <slug> [--dir <path>] [--name <\u5B69\u5B50\u540D>] [--grade <1-6>] [--port <n>] \u5EFA ~/cotutor/<slug>/ \u9AA8\u67B6(\u5E42\u7B49\u8865\u7F3A;\u9ED8\u8BA4\u4E8C\u5E74\u7EA7\u6559\u5B66\u5305;\u7AEF\u53E3\u5728\u8FD9\u91CC\u9009\u4E00\u6B21\u5199\u8FDB cotutor.json:443 \u4F18\u5148,\u4E0D\u884C\u5C31 8443\u30018445\u2026,\u4EE5\u540E\u4E0D\u53D8)\n cotutor grades [--json] \u5217\u51FA\u672C\u5305\u53EF\u5B89\u88C5\u7684\u5E74\u7EA7\u6559\u5B66\u5305\n cotutor doctor [--workspace <dir>] [--json] [--live] \u9010\u9879\u4F53\u68C0;--live \u771F\u8FDE\u4E00\u6B21\u767E\u70BC\u5B9E\u65F6\u6A21\u578B\u3001\u771F\u5408\u6210\u4E00\u53E5\u914D\u97F3(\u82B1\u4E00\u5206\u94B1)\u628A API \u5C42\u7684\u5751\u6446\u51FA\u6765(\u771F\u8DD1\u8001\u5E08\u7528 cotutor send);--json \u91CC\u4E0D\u8FC7\u7684\u9879\u5E26 cmd(\u80FD\u76F4\u63A5\u8DD1)\u6216 human(\u8981\u4EBA\u6765)\n cotutor update [--anyway] \u5347\u7EA7\u4E00\u6761\u547D\u4EE4\u7BA1\u5230\u5E95:\u95EE npm \u6709\u6CA1\u6709\u65B0\u7248\u672C,\u6362\u7A0B\u5E8F,\u6BCF\u4E2A\u5B69\u5B50\u66F4\u65B0\u51FA\u5382\u6587\u4EF6\u3001\u91CD\u8D77\u670D\u52A1\u548C\u56FE\u6807;\u5B69\u5B50\u5728\u901A\u8BDD\u65F6\u4E0D\u52A8(--anyway \u624D\u52A8),npm \u6CA1\u6210\u670D\u52A1\u4E0D\u52A8\n cotutor upgrade [--workspace <dir>] [--force <\u8001\u5E08>]... \u8001\u5E08\u6587\u4EF6\u3001activity \u4E0E skill \u6362\u65B0\u7248:\u6CA1\u6539\u8FC7\u7684\u76F4\u63A5\u6362,\u6539\u8FC7\u7684\u53EA\u62A5 diff(--force <\u8001\u5E08> \u6216 --force <\u8001\u5E08>/<\u540D> \u624D\u8986\u76D6,\u539F\u6587\u7559 .bak)\n cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json \u8865\u7F3A:\u65B0\u51FA\u5382\u8001\u5E08 / \u8FD0\u884C\u65F6 / \u547D\u4EE4\u6A21\u677F\u65D7\u6807(\u53EA\u52A0\u7F3A\u7684,\u4F60\u6539\u8FC7\u7684\u503C\u4E0D\u52A8)\n cotutor add <\u8001\u5E08\u540D> --display <\u663E\u793A\u540D> [--subject <\u5B66\u79D1>] [--avatar <emoji|\u56FE\u7247\u76F8\u5BF9\u8DEF\u5F84>] [--hidden] \u52A0\u4E00\u4F4D\u81EA\u5BB6\u7684\u8001\u5E08:\u51FA\u6A21\u677F\u6587\u4EF6\u3001\u8FDB cotutor.json\u3001\u5EFA\u76EE\u5F55(\u540D\u5B57\u4EE5 -tutor \u7ED3\u5C3E\u7684\u662F\u8001\u5E08,figshot \u7ED9\u5B83\u505A\u5934\u50CF)\n cotutor add-theme <\u4E3B\u9898\u540D> [--from <\u4E3B\u9898>] [--workspace <dir>] \u52A0\u4E00\u4E2A\u81EA\u5BB6\u7684\u4E3B\u9898:\u62F7\u4E00\u4EFD(\u7F3A\u7701\u51FA\u5382\u7684 default)\u5230 themes/<\u4E3B\u9898\u540D>/,\u6539 cotutor.json \u7684 kid.theme \u6362\u8FC7\u53BB\n cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <\u76EE\u5F55>] \u5728\u7EC8\u7AEF\u91CC\u8D77\u670D\u52A1(\u4E00 workspace \u4E00\u8FDB\u7A0B;~/.config/cotutor/certs/ \u6709\u8BC1\u4E66\u5C31\u8D70 HTTPS;--trace \u6BCF\u4E00\u8F6E\u7684\u4E8B\u4EF6\u6309\u9053\u6253\u5370;\u53EA\u6253\u5370\u5BB6\u957F\u7AEF\u9996\u9875\u7684\u5730\u5740,\u5B69\u5B50\u7684\u5E73\u677F\u5728\u90A3\u91CC\u626B\u7801;--open \u987A\u624B\u6253\u5F00,\u65E7\u540D --open-qr \u7167\u6837\u8BA4;--log-dir \u8F93\u51FA\u6309\u5929\u843D\u6587\u4EF6,\u540E\u53F0\u670D\u52A1\u7528;\u5173\u4E86\u7EC8\u7AEF\u5C31\u505C,\u4E00\u76F4\u5F00\u7740\u7528 cotutor service)\n cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] \u6253\u5F00\u5BB6\u957F\u7AEF(\u8BFB cotutor.json,\u4E0D\u7528\u8BB0\u7AEF\u53E3);--sandbox \u5F00\u8BD5\u7528\u7684\u90A3\u4E2A(\u7AEF\u53E3\u6C38\u8FDC\u662F\u771F\u7684 +1);--print \u53EA\u6253\u5370\u5730\u5740;\u6CA1\u5728\u8DD1\u5C31\u8BF4\u600E\u4E48\u8D77,\u4EE5 1 \u9000\n cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] [--no-open] \u628A serve \u88C5\u6210\u8FD9\u53F0\u7535\u8111\u7684\u540E\u53F0\u670D\u52A1(macOS launchd\u3001Linux systemd \u7528\u6237\u670D\u52A1\u3001Windows \u7528\u6237\u7684\u767B\u5F55\u9879,\u90FD\u4E0D\u8981\u7BA1\u7406\u5458):\u767B\u5F55\u540E\u81EA\u5DF1\u8D77\u6765\u3001\u5D29\u4E86 10 \u79D2\u540E\u518D\u8D77\u3001\u65E5\u5FD7\u6309\u5929\u843D\u6587\u4EF6\u7559 14 \u5929;\u4E00\u4E2A workspace \u4E00\u4E2A;\u7B2C\u4E00\u6B21 install \u6253\u5F00\u4E00\u6B21\u5BB6\u957F\u7AEF(--no-open \u4E0D\u5F00),\u4EE5\u540E\u7684\u542F\u52A8\u90FD\u4E0D\u5F00;\u6362\u4E86 node \u7248\u672C\u6216\u5305\u632A\u4E86\u5730\u65B9\u518D install \u4E00\u6B21\n cotutor tray on | off | status [--json] \u83DC\u5355\u680F / \u4EFB\u52A1\u680F\u56FE\u6807(macOS\u3001Windows;service install \u65F6\u4E00\u8D77\u88C5,--no-tray \u4E0D\u5E26):\u6B63\u5E38\u65F6\u53EA\u6709 logo,\u6709\u4E8B\u51FA\u6A59\u70B9,\u6CA1\u5728\u8DD1\u65F6\u53D8\u6DE1\u52A0\u7EA2\u70B9\u5E76\u901A\u77E5\u4E00\u6B21;\u70B9\u5B83\u770B\u72B6\u6001\u3001\u6253\u5F00\u5BB6\u957F\u7AEF\u3001\u91CD\u542F\u3001\u770B\u65E5\u5FD7\u3001\u9000\u51FA;off \u5173\u6389\u5E76\u8BB0\u4F4F\n cotutor service status [--json] | logs [--lines <n>] [--follow] \u540E\u53F0\u670D\u52A1\u5728\u4E0D\u5728\u8DD1\u3001\u7AEF\u53E3\u7B54\u4E0D\u7B54\u5E94\u3001\u8DD1\u7684\u54EA\u4E2A\u7248\u672C\u3001vault \u8BFB\u4E0D\u8BFB\u5F97\u5230\u3001\u65E5\u5FD7\u5728\u54EA;--json \u8FD8\u7B54\u5E73\u677F\u5730\u5740\u3001\u8BC1\u4E66\u3001\u767E\u70BC\u914D\u6CA1\u914D\u3001\u56FE\u6807;logs \u770B\u6700\u8FD1\u7684\u65E5\u5FD7\n cotutor pin [off | status] [--pin <\u6570\u5B57>] [--json] \u5BB6\u957F\u7AEF /parent \u4E0E\u5DE5\u4F5C\u53F0 /dev \u7684\u9501:\u673A\u5668\u7EA7 PIN(\u6240\u6709 workspace \u5171\u7528),iPad \u4E0A\u8F93\u4E00\u6B21\u3001\u8FD9\u53F0\u7535\u8111\u514D\u5BC6;\u6CA1\u8BBE\u5C31\u5168\u5F00(\u5BB6\u957F\u5728\u65C1\u8FB9\u53EF\u4EE5\u4E0D\u8BBE);off \u64A4\u6389\n cotutor cert [--host <\u540D\u6216IP>]... \u7528 mkcert \u5EFA\u8FD9\u53F0\u673A\u5668\u7684\u81EA\u7B7E\u8BC1\u4E66\u5230 ~/.config/cotutor/certs/(\u5E73\u677F\u3001\u624B\u673A\u4E0A\u5F55\u97F3\u8981 HTTPS;\u6240\u6709 workspace \u5171\u7528)\n cotutor send <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--runtime <\u540D>] [--new] [--photo a.jpg,b.png] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--lane main,tts] [--quiet] \u7EC8\u7AEF\u91CC\u53D1\u4E00\u6761,\u73B0\u573A\u6309\u9053\u6253\u5370\u6BCF\u9053\u5DE5\u5E8F\u7684\u4E8B\u4EF6,\u8BF4\u5B8C\u6253\u5370\u7ED3\u679C(\u5B69\u5B50\u7684\u8BDD\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF:\u4E0A\u4E0B\u6587\u5305\u5E26 live:\u3001\u4E0D\u8865\u63D0\u95EE\u5361,\u53EA\u662F\u6CA1\u6709\u5634;--from system \u4E0D\u7B97;--new \u5F00\u65B0\u8BDD\u9898;--photo \u5E26\u4F5C\u4E1A\u7167\u7247,\u62F7\u8FDB captures/,\u6709\u7167\u7247\u65F6\u6D88\u606F\u53EF\u4EE5\u7A7A;--activity \u50CF\u5B69\u5B50\u70B9\u4E86\u9996\u9875\u7684 activity \u6309\u94AE,\u65B0\u5F00\u8BDD\u9898,\u53C2\u6570\u540C\u6309\u94AE\u4E0B\u9762\u90A3\u51E0\u884C;--quiet \u53EA\u8981\u7ED3\u679C)\n cotutor pack <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--at <ISO\u65F6\u95F4>] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--json] \u5E72\u8DD1\u4E0A\u4E0B\u6587\u5305(--activity \u770B activity \u8BDD\u9898\u7B2C\u4E00\u6761\u8001\u5E08\u62FF\u5230\u4EC0\u4E48):\u4E0D\u8D77\u6A21\u578B,\u6253\u5370\u73B0\u5728\u4F1A\u53D1\u7ED9\u8001\u5E08\u7684\u90A3\u4EFD + \u6BCF\u6BB5\u6765\u81EA\u54EA\u4E2A\u6587\u4EF6\u3001\u90A3\u91CC\u4E00\u5171\u51E0\u6761\u3001\u6309\u653F\u7B56\u5E26\u4E86\u51E0\u6761(\u6539\u4E86\u6863\u6848 / \u65E5\u8BB0 / \u8BA1\u5212\u7ACB\u523B\u770B\u6548\u679C)\n cotutor replay <\u8001\u5E08> <job> [<\u65E5\u671F>] [--runtime <\u540D>] [--json] \u56DE\u653E\u4E00\u8F6E:\u540C\u4E00\u95EE\u6309\u73B0\u5728\u7684 vault \u4E0E\u63D0\u793A\u8BCD\u518D\u8DD1\u4E00\u904D(\u843D evals/,\u4E0D\u8FDB\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u4E0D\u914D\u97F3),\u8DD1\u5B8C\u5E76\u6392\u6253\u5370\u4E0A\u4E0B\u6587\u5305 / \u8BB2\u7A3F / \u5361 / \u8BFB\u4E86\u4EC0\u4E48\u7684 diff\n cotutor compare <\u8001\u5E08> [<evalJob>] [<\u65E5\u671F>] [--json] \u518D\u770B\u4E00\u6B21\u56DE\u653E\u7684\u5BF9\u7167(\u4E0D\u7ED9 evalJob \u5C31\u5217\u8FD9\u5929\u56DE\u653E\u8FC7\u54EA\u4E9B)\n cotutor show <\u8001\u5E08> <job> [<\u65E5\u671F>] [--json] [--evals] [--workspace <dir>] \u770B\u4E00\u8F6E:\u95EE\u4E86\u4EC0\u4E48\u3001\u4E0A\u4E0B\u6587\u5305\u3001\u5F53\u65F6\u7684\u8001\u5E08\u6587\u4EF6\u4E0E\u6280\u80FD hash\u3001\u8BB2\u7A3F\u4E0E\u5361\u3001\u8BFB\u4E86\u4EC0\u4E48\u3001token \u4E0E\u7528\u65F6\u3001\u7ED9\u5BB6\u957F\u7684\u5C3E\u5DF4;--json \u662F\u5BB6\u957F\u7AEF\u300C\u770B\u539F\u6587\u300D\u540C\u4E00\u4EFD\u6570\u636E,\u7ED9\u7F16\u7A0B\u52A9\u624B\u5206\u6790\u7528\n cotutor trace <\u8001\u5E08> <job> [<\u65E5\u671F>] [--lane \u2026] [--workspace <dir>] \u56DE\u653E\u4E00\u8F6E\u7684\u4E8B\u4EF6(<\u65E5\u671F>.<job>.events.jsonl;\u6392\u67E5\u6628\u5929\u90A3\u8F6E\u7528)\n cotutor rate <\u8001\u5E08> <\u8BDD\u9898> <1-5> [--date <\u65E5\u671F>] \u7ED9\u4E00\u4E2A\u8BDD\u9898\u6253\u661F(\u4E0E\u5BB6\u957F\u7AEF\u540C\u4E00\u6761\u8DEF;\u2265 vault.keepScore \u7684\u8BDD\u9898\u8BB0\u8D26\u65F6\u6458\u8981\u624D\u8FDB\u65E5\u8BB0)\n cotutor bookkeep <\u8001\u5E08> [--date <\u65E5\u671F>] [--thread <\u8BDD\u9898>]... \u8BB0\u8D26:\u8FD9\u5929\u6BCF\u4E2A\u8FD8\u6CA1\u8BB0\u8FC7\u7684\u8BDD\u9898\u5404\u8D77\u4E00\u8F6E\u8BB0\u8D26\u4EFB\u52A1,\u8001\u5E08\u56DE\u300C## \u8BB0\u8D26\u300D,\u5E94\u7528\u5199\u8FDB vault \u7684\u65E5\u8BB0(\u8BDD\u9898\u540D\u3001\u5B69\u5B50\u95EE\u3001\u6458\u8981\u3001\u89C2\u5BDF)\n cotutor home check [--published] [--json] \u68C0\u67E5\u9996\u9875\u8349\u7A3F home/draft.md(\u6216\u5DF2\u53D1\u5E03\u7684\u90A3\u4EFD):\u5B69\u5B50\u4F1A\u770B\u5230\u54EA\u51E0\u5F20\u5361\u3001\u6BCF\u4F4D\u8001\u5E08\u7684\u6309\u94AE\u3001\u8981\u6539\u7684\u4E0E\u63D0\u9192(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor home publish [--force] [--from <\u6587\u4EF6>] [--json] \u53D1\u5E03\u9996\u9875:\u68C0\u67E5 \u2192 \u7559\u5386\u53F2 home/history/<id>.md \u2192 \u5199 home/published.json(\u5B69\u5B50\u7AEF\u53EA\u8BFB\u5B83);\u6709\u8981\u6539\u7684\u4E0D\u53D1,--force \u4E22\u6389\u90A3\u51E0\u5F20\u7167\u53D1;--from \u53D1\u53E6\u4E00\u4EFD(\u5982\u5386\u53F2)\n cotutor home show [--json] \u73B0\u5728\u53D1\u5E03\u7684\u662F\u54EA\u4EFD\u3001\u51E0\u5929\u524D\u3001\u6BCF\u4E2A\u6309\u94AE\u88AB\u70B9\u4E86\u51E0\u6B21(\u54EA\u5929\u3001\u54EA\u4E2A\u8BDD\u9898)\n cotutor activity check [<\u8001\u5E08> [<\u540D>]] [--json] \u68C0\u67E5 activity \u6587\u4EF6(agents/<\u8001\u5E08>/activities/<\u540D>.md):\u5BB6\u957F\u586B\u7684\u3001\u5E94\u7528\u7B97\u7684\u3001\u5E94\u7528\u62E6\u7684\u3001\u88C1\u6BB5\u3001\u63D2\u69FD\u3001\u6B63\u6587\u7528\u7684\u5361\u3001\u7B2C\u4E00\u53E5\u4E0E\u6536(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor activity eval <\u8001\u5E08> <\u540D> [--runs <n>] [--only <\u4F8B\u5B50>] [--kid-model <\u5343\u95EE\u6A21\u578B>] [--json] \u7167 <\u540D>.\u4F8B\u5B50.md \u8DD1:\u771F\u8001\u5E08 + \u6A21\u62DF\u5B69\u5B50(\u5343\u95EE,\u7F3A\u7701 qwen3.8-flash,\u6709 Token Plan \u8D70\u5B83)\u591A\u8F6E(\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF),\u5224\u53EA\u770B\u677F\u4E66;\u4E00\u4E2A\u4F8B\u5B50\u8DD1 n \u6B21(\u7F3A\u7701 3)\u6B21\u6B21\u90FD\u8FC7\u624D\u7B97\u8FC7;\u843D evals/,\u753B\u5BB6\u662F\u5047\u7684;\u82B1\u94B1\n cotutor chalk <check|build|bake|snap|dub> <scene.ts|\u8BFE\u5305 id> [...] \u505A\u5C0F\u8BFE\u5802\u7684\u8BFE\u5305:check \u6821\u9A8C\u573A\u666F\u3001build \u51FA\u8BFE\u5305\u5230 bundles/\u3001bake \u70E4\u753B\u9762\u3001snap \u6BCF\u6B65\u622A\u56FE\u3001dub \u914D\u97F3(cotutor chalk \u770B\u5404\u81EA\u7684\u9009\u9879;bake / snap \u8981\u672C\u673A\u7684 Chrome)\n cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] \u4E0D\u7ECF\u771F\u5B9E\u8001\u5E08\u4E0E\u767E\u70BC,\u7528\u56FA\u5B9A\u7684\u677F\u4E66 JSON \u8D77\u5B69\u5B50\u7AEF,\u6D4B\u524D\u7AEF\u4EA4\u4E92\u4E0E\u6E32\u67D3(\u4E0D\u9700\u8981 workspace;\u8001\u5E08\u90FD\u8D70\u5B9E\u65F6\u8001\u5E08\u9875,\u5047\u5634\u7167\u811A\u672C\u5FF5\u9759\u97F3\u7684\u58F0\u97F3;nokey = \u6CA1\u914D\u767E\u70BC,\u5B69\u5B50\u7AEF\u53EA\u51FA\u300C\u8001\u5E08\u4EEC\u8FD8\u6CA1\u5230\u300D)\n cotutor --version | --help\nworkspace\u89E3\u6790:--workspace > COTUTOR_WORKSPACE > cwd \u6216\u7956\u5148\u6709 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ \u4E0B\u552F\u4E00\u7684\u5B69\u5B50\u76EE\u5F55\n\u8BD5\u7528:\u8981 workspace \u7684\u547D\u4EE4\u52A0 --sandbox,\u6362\u5230\u771F workspace \u65C1\u8FB9\u7684 .sandbox/<\u76EE\u5F55\u540D>/,\u8BD5\u7684\u5BF9\u8BDD\u4E0E vault \u7684\u5199\u5165\u4E0D\u8FDB\u5B69\u5B50\u7684\u8BB0\u5F55(\u7B2C\u4E00\u6B21\u81EA\u52A8\u5206:\u8BBE\u7F6E\u7167\u62F7,\u5BF9\u8BDD\u4E0E\u7167\u7247\u4E0D\u5E26,vault \u62F7\u4E00\u4EFD;\u6807\u9898\u524D\u52A0\u300C\u8BD5\u7528\u300D\u3001\u7AEF\u53E3 +1\u3001\u81EA\u52A8\u8BB0\u8D26\u5173\u6389);--sandbox --fresh \u91CD\u65B0\u5206\n";
|
package/dist/app/cli/usage.js
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
export const USAGE = `用法:
|
|
3
3
|
cotutor init <slug> [--dir <path>] [--name <孩子名>] [--grade <1-6>] [--port <n>] 建 ~/cotutor/<slug>/ 骨架(幂等补缺;默认二年级教学包;端口在这里选一次写进 cotutor.json:443 优先,不行就 8443、8445…,以后不变)
|
|
4
4
|
cotutor grades [--json] 列出本包可安装的年级教学包
|
|
5
|
-
cotutor doctor [--workspace <dir>] [--json] [--live] 逐项体检;--live 真连一次百炼实时模型、真合成一句配音(花一分钱)把 API 层的坑摆出来(真跑老师用 cotutor send)
|
|
5
|
+
cotutor doctor [--workspace <dir>] [--json] [--live] 逐项体检;--live 真连一次百炼实时模型、真合成一句配音(花一分钱)把 API 层的坑摆出来(真跑老师用 cotutor send);--json 里不过的项带 cmd(能直接跑)或 human(要人来)
|
|
6
|
+
cotutor update [--anyway] 升级一条命令管到底:问 npm 有没有新版本,换程序,每个孩子更新出厂文件、重起服务和图标;孩子在通话时不动(--anyway 才动),npm 没成服务不动
|
|
6
7
|
cotutor upgrade [--workspace <dir>] [--force <老师>]... 老师文件、activity 与 skill 换新版:没改过的直接换,改过的只报 diff(--force <老师> 或 --force <老师>/<名> 才覆盖,原文留 .bak)
|
|
7
8
|
cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json 补缺:新出厂老师 / 运行时 / 命令模板旗标(只加缺的,你改过的值不动)
|
|
8
9
|
cotutor add <老师名> --display <显示名> [--subject <学科>] [--avatar <emoji|图片相对路径>] [--hidden] 加一位自家的老师:出模板文件、进 cotutor.json、建目录(名字以 -tutor 结尾的是老师,figshot 给它做头像)
|
|
9
10
|
cotutor add-theme <主题名> [--from <主题>] [--workspace <dir>] 加一个自家的主题:拷一份(缺省出厂的 default)到 themes/<主题名>/,改 cotutor.json 的 kid.theme 换过去
|
|
10
11
|
cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <目录>] 在终端里起服务(一 workspace 一进程;~/.config/cotutor/certs/ 有证书就走 HTTPS;--trace 每一轮的事件按道打印;只打印家长端首页的地址,孩子的平板在那里扫码;--open 顺手打开,旧名 --open-qr 照样认;--log-dir 输出按天落文件,后台服务用;关了终端就停,一直开着用 cotutor service)
|
|
11
12
|
cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] 打开家长端(读 cotutor.json,不用记端口);--sandbox 开试用的那个(端口永远是真的 +1);--print 只打印地址;没在跑就说怎么起,以 1 退
|
|
12
|
-
cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] 把 serve 装成这台电脑的后台服务(macOS launchd、Linux systemd 用户服务、Windows 用户的登录项,都不要管理员):登录后自己起来、崩了 10 秒后再起、日志按天落文件留 14 天;一个 workspace
|
|
13
|
+
cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] [--no-open] 把 serve 装成这台电脑的后台服务(macOS launchd、Linux systemd 用户服务、Windows 用户的登录项,都不要管理员):登录后自己起来、崩了 10 秒后再起、日志按天落文件留 14 天;一个 workspace 一个;第一次 install 打开一次家长端(--no-open 不开),以后的启动都不开;换了 node 版本或包挪了地方再 install 一次
|
|
13
14
|
cotutor tray on | off | status [--json] 菜单栏 / 任务栏图标(macOS、Windows;service install 时一起装,--no-tray 不带):正常时只有 logo,有事出橙点,没在跑时变淡加红点并通知一次;点它看状态、打开家长端、重启、看日志、退出;off 关掉并记住
|
|
14
|
-
cotutor service status [--json] | logs [--lines <n>] [--follow] 后台服务在不在跑、端口答不答应、跑的哪个版本、vault
|
|
15
|
+
cotutor service status [--json] | logs [--lines <n>] [--follow] 后台服务在不在跑、端口答不答应、跑的哪个版本、vault 读不读得到、日志在哪;--json 还答平板地址、证书、百炼配没配、图标;logs 看最近的日志
|
|
15
16
|
cotutor pin [off | status] [--pin <数字>] [--json] 家长端 /parent 与工作台 /dev 的锁:机器级 PIN(所有 workspace 共用),iPad 上输一次、这台电脑免密;没设就全开(家长在旁边可以不设);off 撤掉
|
|
16
17
|
cotutor cert [--host <名或IP>]... 用 mkcert 建这台机器的自签证书到 ~/.config/cotutor/certs/(平板、手机上录音要 HTTPS;所有 workspace 共用)
|
|
17
18
|
cotutor send <老师> [<消息>] [--from kid|system] [--runtime <名>] [--new] [--photo a.jpg,b.png] [--activity <名> [--param '名=值;名=值']] [--lane main,tts] [--quiet] 终端里发一条,现场按道打印每道工序的事件,说完打印结果(孩子的话照实时写,同孩子端:上下文包带 live:、不补提问卡,只是没有嘴;--from system 不算;--new 开新话题;--photo 带作业照片,拷进 captures/,有照片时消息可以空;--activity 像孩子点了首页的 activity 按钮,新开话题,参数同按钮下面那几行;--quiet 只要结果)
|
package/dist/app/http/app.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface AppContext {
|
|
|
43
43
|
access: Access;
|
|
44
44
|
/** 百炼(bailian.ts):孩子端要它才打得开,家长端配;测试可注入 */
|
|
45
45
|
bailian: Bailian;
|
|
46
|
+
/** 嘴的连接好了 / 坏了(call.ts 报上来) */
|
|
47
|
+
mouthHealth(why: string | null, midCall?: boolean): Promise<void>;
|
|
46
48
|
}
|
|
47
49
|
export declare function createContext(ws: Workspace, opts?: {
|
|
48
50
|
now?: () => Date;
|
package/dist/app/http/app.js
CHANGED
|
@@ -33,6 +33,8 @@ import { factoryAvatarFile, shownAvatar } from "./avatars.js";
|
|
|
33
33
|
import { deviceCookie, newDeviceId, parseKidLog, readDevices, recordKidLog, renameDevice, validDevice } from "../../runtime/journal/kid-log.js";
|
|
34
34
|
import { DEVICE_COOKIE } from "../../schema/kid-device.js";
|
|
35
35
|
import { BUILD, codeChanged, headNow, shortCommit } from "../build.js";
|
|
36
|
+
import { knownLatest, knownNotes } from "../machine/update.js";
|
|
37
|
+
import { isNewer } from "../../shared/upgrade.js";
|
|
36
38
|
import { qrPage } from "../../ui/parent/qr-page.js";
|
|
37
39
|
import { kidPage } from "../../ui/kid/kid-page.js";
|
|
38
40
|
import { parentPage } from "../../ui/parent/parent-page.js";
|
|
@@ -80,6 +82,12 @@ export function createContext(ws, opts = {}) {
|
|
|
80
82
|
openCalls: null,
|
|
81
83
|
access: opts.access ?? createAccess({ now: () => ctx.now() }),
|
|
82
84
|
bailian: opts.bailian ?? createBailian({ env: opts.env }),
|
|
85
|
+
async mouthHealth(why, midCall) {
|
|
86
|
+
if (why === null)
|
|
87
|
+
ctx.bailian.fine('mouth');
|
|
88
|
+
else
|
|
89
|
+
await ctx.bailian.note(midCall ? 'call' : 'mouth', why);
|
|
90
|
+
},
|
|
83
91
|
async reload() {
|
|
84
92
|
try {
|
|
85
93
|
const r = await reloadIfChanged(ctx.ws, configText);
|
|
@@ -94,6 +102,15 @@ export function createContext(ws, opts = {}) {
|
|
|
94
102
|
}
|
|
95
103
|
},
|
|
96
104
|
};
|
|
105
|
+
// 脑的结果:请求失败(欠费、key 失效)记下给家长看;成功了就算好了。不是请求的错(比如卡住)不算百炼的事
|
|
106
|
+
ctx.runner.onEvent((e) => {
|
|
107
|
+
if (e.event.lane !== 'main' || e.event.kind !== 'exit')
|
|
108
|
+
return;
|
|
109
|
+
if (e.event.ok)
|
|
110
|
+
ctx.bailian.fine('brain');
|
|
111
|
+
else if (e.event.reason?.startsWith('api:'))
|
|
112
|
+
void ctx.bailian.note('brain', e.event.reason);
|
|
113
|
+
});
|
|
97
114
|
return ctx;
|
|
98
115
|
}
|
|
99
116
|
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);
|
|
@@ -127,7 +144,7 @@ export async function kidHome(ctx, now) {
|
|
|
127
144
|
const date = localDate(now);
|
|
128
145
|
const view = await kidHomeView(ws, now);
|
|
129
146
|
const figshot = ws.config.figshot ? { port: ws.config.figshot.port } : null;
|
|
130
|
-
return { title: ws.config.title, date, tutors: await kidTutors(ctx), home: view.home, cards: view.cards, figshot, hand: ws.config.kid.hand ?? 'right', ready: ctx.bailian.status().ready };
|
|
147
|
+
return { title: ws.config.title, date, tutors: await kidTutors(ctx), home: view.home, cards: view.cards, figshot, hand: ws.config.kid.hand ?? 'right', ready: ctx.bailian.status().ready && !ctx.bailian.blocked() };
|
|
131
148
|
}
|
|
132
149
|
export async function kidHistory(ctx, tutor, days) {
|
|
133
150
|
const today = localDate(ctx.now());
|
|
@@ -461,7 +478,7 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
|
|
|
461
478
|
if (p === '/api/health') {
|
|
462
479
|
const head = await headNow();
|
|
463
480
|
const vault = await readdir(ws.paths.vault).then(() => 'ok', (err) => (err.code === 'EPERM' || err.code === 'EACCES' ? 'denied' : 'missing'));
|
|
464
|
-
return { status: 200, json: { ok: true, configError: ctx.configError, started: BUILD.started, version: BUILD.version, commit: shortCommit(BUILD.commit), dirty: BUILD.dirty, head: shortCommit(head), changed: codeChanged(BUILD.commit, head), pid: process.pid, mode: process.env.COTUTOR_SERVICE ? 'service' : 'foreground', vault, calls: ctx.openCalls?.() ?? 0, bailian: ctx.bailian.status().ready } };
|
|
481
|
+
return { status: 200, json: { ok: true, configError: ctx.configError, started: BUILD.started, version: BUILD.version, commit: shortCommit(BUILD.commit), dirty: BUILD.dirty, head: shortCommit(head), changed: codeChanged(BUILD.commit, head), pid: process.pid, mode: process.env.COTUTOR_SERVICE ? 'service' : 'foreground', vault, calls: ctx.openCalls?.() ?? 0, bailian: ctx.bailian.status().ready, bailianTrouble: ctx.bailian.trouble(), latest: isNewer(knownLatest(), BUILD.version) ? knownLatest() : null, latestNotes: isNewer(knownLatest(), BUILD.version) ? knownNotes() : [] } };
|
|
465
482
|
}
|
|
466
483
|
// 家长端与工作台的锁(access.ts):对 PIN 换 cookie;设了 PIN、没凭证的设备,孩子端以外的页面回输 PIN 的页、接口回 401。没设 PIN 什么都不拦
|
|
467
484
|
if (p === '/api/auth') {
|
|
@@ -11,15 +11,41 @@ export interface BailianStatus {
|
|
|
11
11
|
keyTail: string | null;
|
|
12
12
|
workspaceId: string | null;
|
|
13
13
|
}
|
|
14
|
+
export type TroubleKind = 'key' | 'money' | 'space' | 'network' | 'other';
|
|
15
|
+
/** 谁报的:嘴连不上、通话中百炼报错、脑的请求失败 */
|
|
16
|
+
export type TroubleSource = 'mouth' | 'call' | 'brain';
|
|
17
|
+
export interface BailianTrouble {
|
|
18
|
+
kind: TroubleKind;
|
|
19
|
+
message: string;
|
|
20
|
+
fix: string;
|
|
21
|
+
/** 百炼的原话(错误码),排查用 */
|
|
22
|
+
why: string;
|
|
23
|
+
/** 孩子端要显示「叫爸爸妈妈」 */
|
|
24
|
+
blocked: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** 按错误码分类;官方表里没有的算 other */
|
|
27
|
+
export declare function troubleKind(why: string): TroubleKind;
|
|
14
28
|
export interface Bailian {
|
|
15
29
|
status(): BailianStatus;
|
|
16
30
|
/** 试连;不给 auth 就用现在那份。null = 通 */
|
|
17
31
|
probe(auth?: BailianAuth): Promise<string | null>;
|
|
18
32
|
save(auth: BailianAuth): Promise<void>;
|
|
33
|
+
/** 跑起来以后的故障;没有 → null */
|
|
34
|
+
trouble(): BailianTrouble | null;
|
|
35
|
+
/** 孩子端该灰着:嘴连不上,而且要家长动手 */
|
|
36
|
+
blocked(): boolean;
|
|
37
|
+
/** 出错了:记一笔;「连不上」这种说不清的先问清原因 */
|
|
38
|
+
note(source: TroubleSource, why: string): Promise<void>;
|
|
39
|
+
/** 好了;不给 source 就全清(换了 key、试连通了) */
|
|
40
|
+
fine(source?: TroubleSource): void;
|
|
41
|
+
/** 嘴连不上时隔一阵再试一次,好了就清(充上钱、网回来了,没人点也会恢复) */
|
|
42
|
+
recheck(): Promise<void>;
|
|
19
43
|
}
|
|
20
44
|
export declare function createBailian(opts?: {
|
|
21
45
|
env?: NodeJS.ProcessEnv;
|
|
22
46
|
probe?: (auth?: BailianAuth) => Promise<string | null>;
|
|
47
|
+
ask?: () => Promise<string | null>;
|
|
48
|
+
now?: () => number;
|
|
23
49
|
}): Bailian;
|
|
24
50
|
/** 试连没通的原因,写给家长看 */
|
|
25
51
|
export declare function probeMessage(why: string): string;
|
|
@@ -10,31 +10,111 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 改 key 要么在这台电脑上(本机免密),要么设了家长密码:没设 PIN 时局域网谁都进得了家长端,不让别的设备往这台电脑写 key。
|
|
12
12
|
* 测试与探针指了假的实时端点(COTUTOR_LIVE_ENDPOINT)就当配好了,试连也连那个。
|
|
13
|
+
*
|
|
14
|
+
* 跑起来以后也会坏(欠费、key 失效、这台电脑断网):ready 只说「填过」,看不出来。嘴和脑出错时记一笔(note),/api/health 带给图标和家长端;
|
|
15
|
+
* 要家长动手的那几类,孩子端显示「叫爸爸妈妈」(blocked)。错误码照官方错误码表(help.aliyun.com/zh/model-studio/error-code);欠费没法真测,是照表写的,假 key 是真跑过的。
|
|
13
16
|
*/
|
|
14
17
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
15
18
|
import { homedir } from 'node:os';
|
|
16
19
|
import { dirname } from 'node:path';
|
|
17
20
|
import { bailianFile } from "../../shared/qianwen.js";
|
|
18
21
|
import { dashscopeAuth } from "../../realtime/bailian.js";
|
|
19
|
-
import { probeRealtime } from "../../realtime/preview.js";
|
|
22
|
+
import { askRealtime, probeRealtime, unclearWhy } from "../../realtime/preview.js";
|
|
23
|
+
const TROUBLE_TEXT = {
|
|
24
|
+
key: { message: '百炼的 key 不对或失效了', fix: '到家长端「首页」换一把百炼按量的 key。' },
|
|
25
|
+
money: { message: '百炼欠费或没开通按量付费', fix: '到百炼控制台充值或开通,好了几分钟内自己恢复。' },
|
|
26
|
+
space: { message: '这把 key 用不了这个业务空间', fix: '到家长端「首页」核对业务空间 id。' },
|
|
27
|
+
network: { message: '这台电脑连不上百炼', fix: '看看这台电脑的网络,好了几分钟内自己恢复。' },
|
|
28
|
+
other: { message: '百炼这几次都没连上', fix: '过一会儿再看;一直这样,到家长端「首页」点试连。' },
|
|
29
|
+
};
|
|
30
|
+
/** 按错误码分类;官方表里没有的算 other */
|
|
31
|
+
export function troubleKind(why) {
|
|
32
|
+
if (/InvalidApiKey|invalid_api_key|\b401\b/i.test(why))
|
|
33
|
+
return 'key';
|
|
34
|
+
if (/Arrearage|BillOverdue|AllocationQuota|CommodityNotPurchased|Unpurchased|insufficient_quota/i.test(why))
|
|
35
|
+
return 'money';
|
|
36
|
+
if (/AccessDenied|\b403\b/i.test(why))
|
|
37
|
+
return 'space';
|
|
38
|
+
if (/^network$|^timeout$|请求或流断了/.test(why))
|
|
39
|
+
return 'network';
|
|
40
|
+
return 'other';
|
|
41
|
+
}
|
|
42
|
+
/** 这几类等不好,要家长动手,一次就算;网络和别的可能只是抖一下,连着两次才算 */
|
|
43
|
+
const needsParent = (kind) => kind === 'key' || kind === 'money' || kind === 'space';
|
|
44
|
+
const STRIKES = 2;
|
|
45
|
+
/** 脑和通话中的错没法自己再试(再试要花钱),多久没再犯就算过去了;还在坏的话孩子下一次用又会记上 */
|
|
46
|
+
const EVENT_TTL_MS = 3600_000;
|
|
20
47
|
export function createBailian(opts = {}) {
|
|
21
48
|
const env = opts.env ?? process.env;
|
|
22
49
|
const home = env.HOME || homedir();
|
|
23
50
|
const fake = env.COTUTOR_LIVE_ENDPOINT || undefined;
|
|
24
|
-
|
|
51
|
+
const now = opts.now ?? Date.now;
|
|
52
|
+
const ask = opts.ask ?? (() => askRealtime({ env, endpoint: fake }));
|
|
53
|
+
const notes = new Map();
|
|
54
|
+
const probe = opts.probe ?? ((auth) => probeRealtime({ auth, env, endpoint: fake }));
|
|
55
|
+
const counted = (source) => {
|
|
56
|
+
const n = notes.get(source);
|
|
57
|
+
if (!n)
|
|
58
|
+
return null;
|
|
59
|
+
if (source !== 'mouth' && now() - n.at > EVENT_TTL_MS) {
|
|
60
|
+
notes.delete(source);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return needsParent(n.kind) || n.count >= STRIKES ? n : null;
|
|
64
|
+
};
|
|
65
|
+
const bailian = {
|
|
25
66
|
status() {
|
|
26
67
|
const auth = dashscopeAuth(env);
|
|
27
68
|
if (!auth)
|
|
28
69
|
return { ready: Boolean(fake), keyTail: null, workspaceId: null };
|
|
29
70
|
return { ready: true, keyTail: auth.key.slice(-4), workspaceId: auth.workspaceId };
|
|
30
71
|
},
|
|
31
|
-
probe
|
|
72
|
+
probe,
|
|
32
73
|
async save(auth) {
|
|
33
74
|
const file = bailianFile(home);
|
|
34
75
|
await mkdir(dirname(file), { recursive: true });
|
|
35
76
|
await writeFile(file, `${JSON.stringify({ apiKey: auth.key, workspaceId: auth.workspaceId, at: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
77
|
+
notes.clear();
|
|
78
|
+
},
|
|
79
|
+
trouble() {
|
|
80
|
+
const n = counted('mouth') ?? counted('call') ?? counted('brain');
|
|
81
|
+
return n ? { kind: n.kind, ...TROUBLE_TEXT[n.kind], why: n.why.slice(0, 200), blocked: bailian.blocked() } : null;
|
|
82
|
+
},
|
|
83
|
+
blocked() {
|
|
84
|
+
const n = counted('mouth');
|
|
85
|
+
return Boolean(n && needsParent(n.kind));
|
|
86
|
+
},
|
|
87
|
+
async note(source, why) {
|
|
88
|
+
// 通话自己结束了、还没配:都不是故障(没配有 ready 管)
|
|
89
|
+
if (why === 'ended' || why === 'no_auth')
|
|
90
|
+
return;
|
|
91
|
+
const real = unclearWhy(why) ? (await ask().catch(() => null)) ?? why : why;
|
|
92
|
+
if (real === 'no_auth')
|
|
93
|
+
return;
|
|
94
|
+
const kind = troubleKind(real);
|
|
95
|
+
// 通话中百炼报的多半是这一句的事(声音太短之类),只认要家长动手的
|
|
96
|
+
if (source === 'call' && !needsParent(kind))
|
|
97
|
+
return;
|
|
98
|
+
const was = notes.get(source);
|
|
99
|
+
notes.set(source, { why: real, kind, count: (was?.count ?? 0) + 1, at: now() });
|
|
100
|
+
},
|
|
101
|
+
fine(source) {
|
|
102
|
+
if (source)
|
|
103
|
+
notes.delete(source);
|
|
104
|
+
else
|
|
105
|
+
notes.clear();
|
|
106
|
+
},
|
|
107
|
+
async recheck() {
|
|
108
|
+
if (!notes.has('mouth'))
|
|
109
|
+
return;
|
|
110
|
+
const why = await probe();
|
|
111
|
+
if (why === null || why === 'no_auth')
|
|
112
|
+
notes.delete('mouth');
|
|
113
|
+
else
|
|
114
|
+
notes.set('mouth', { why, kind: troubleKind(why), count: STRIKES, at: now() });
|
|
36
115
|
},
|
|
37
116
|
};
|
|
117
|
+
return bailian;
|
|
38
118
|
}
|
|
39
119
|
/** 试连没通的原因,写给家长看 */
|
|
40
120
|
export function probeMessage(why) {
|
|
@@ -44,6 +124,9 @@ export function probeMessage(why) {
|
|
|
44
124
|
return '连不上:key 或业务空间 id 不对,或者这台电脑上不了网';
|
|
45
125
|
if (why === 'timeout')
|
|
46
126
|
return '连上了,等不到回音(超时),过一会儿再试';
|
|
127
|
+
const kind = troubleKind(why);
|
|
128
|
+
if (kind !== 'other')
|
|
129
|
+
return `${TROUBLE_TEXT[kind].message}。${TROUBLE_TEXT[kind].fix}`;
|
|
47
130
|
return `百炼说:${why}`;
|
|
48
131
|
}
|
|
49
132
|
const KEY_RE = /^sk-[A-Za-z0-9._-]{16,}$/;
|
|
@@ -54,12 +137,16 @@ export async function bailianRoute(method, p, ctx, body, who) {
|
|
|
54
137
|
if (p !== '/api/parent/bailian' && p !== '/api/parent/bailian/test')
|
|
55
138
|
return null;
|
|
56
139
|
const canEdit = who === 'local' || (await ctx.access.pinSet());
|
|
57
|
-
const view = () => ({ ...ctx.bailian.status(), canEdit });
|
|
140
|
+
const view = () => ({ ...ctx.bailian.status(), canEdit, trouble: ctx.bailian.trouble() });
|
|
58
141
|
if (p === '/api/parent/bailian/test') {
|
|
59
142
|
if (method !== 'POST')
|
|
60
143
|
return { status: 405, json: { error: 'method_not_allowed' } };
|
|
61
144
|
const t0 = Date.now();
|
|
62
145
|
const why = await ctx.bailian.probe();
|
|
146
|
+
if (why === null)
|
|
147
|
+
ctx.bailian.fine();
|
|
148
|
+
else
|
|
149
|
+
await ctx.bailian.note('mouth', why);
|
|
63
150
|
return { status: 200, json: { ok: why === null, ms: Date.now() - t0, ...(why === null ? {} : { message: probeMessage(why) }) } };
|
|
64
151
|
}
|
|
65
152
|
if (method === 'GET')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const UPDATE_URLS: string[];
|
|
2
|
+
export declare const updateFile: () => string;
|
|
3
|
+
export interface UpdateRecord {
|
|
4
|
+
checkedAt?: string;
|
|
5
|
+
latest?: string;
|
|
6
|
+
notes?: string[];
|
|
7
|
+
off?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** 网上来的,要显示给家长:只留几行短的纯文本 */
|
|
10
|
+
export declare function cleanNotes(raw: unknown): string[];
|
|
11
|
+
/** 读不到、写坏了 → 空的,当作没问过 */
|
|
12
|
+
export declare function readUpdateRecord(file?: string): Promise<UpdateRecord>;
|
|
13
|
+
export interface Release {
|
|
14
|
+
version: string;
|
|
15
|
+
notes: string[];
|
|
16
|
+
/** 新版本要的 Node 主次版本,如 [22, 18];没写 → null */
|
|
17
|
+
minNode: [number, number] | null;
|
|
18
|
+
/** 答的是不是 npm 本家;不是就说明 npm 不通,下载也要走镜像 */
|
|
19
|
+
mirror: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** 依次问,谁先答谁算;都问不到 → null */
|
|
22
|
+
export declare function fetchRelease(fetchFn?: typeof fetch, urls?: string[]): Promise<Release | null>;
|
|
23
|
+
/** 这个进程现在知道的最新版本;还不知道、关掉了 → null */
|
|
24
|
+
export declare const knownLatest: () => string | null;
|
|
25
|
+
/** 那个版本的更新说明 */
|
|
26
|
+
export declare const knownNotes: () => string[];
|
|
27
|
+
export interface UpdateOptions {
|
|
28
|
+
now?: number;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
file?: string;
|
|
31
|
+
}
|
|
32
|
+
/** 文件里的还新鲜就用它,否则问一次并存下;问不到就继续用上次的 */
|
|
33
|
+
export declare function refreshLatest(o?: UpdateOptions): Promise<string | null>;
|
|
34
|
+
/** serve 起来时调:先用存着的那份,一分钟后问(刚开机网可能还没好),之后每小时看一眼,过了一天才真的问。定时器不拦进程退出 */
|
|
35
|
+
export declare function startUpdateChecks(o?: UpdateOptions & {
|
|
36
|
+
firstMs?: number;
|
|
37
|
+
everyMs?: number;
|
|
38
|
+
}): () => void;
|