cotutor 0.4.1 → 0.4.3

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.
Files changed (46) hide show
  1. package/README.md +24 -6
  2. package/dist/app/cli/backup.d.ts +68 -0
  3. package/dist/app/cli/backup.js +243 -0
  4. package/dist/app/cli/doctor-fix.d.ts +18 -0
  5. package/dist/app/cli/doctor-fix.js +70 -0
  6. package/dist/app/cli/doctor.js +19 -2
  7. package/dist/app/cli/init.d.ts +2 -0
  8. package/dist/app/cli/init.js +6 -3
  9. package/dist/app/cli/main.js +117 -2
  10. package/dist/app/cli/report.d.ts +11 -0
  11. package/dist/app/cli/report.js +110 -0
  12. package/dist/app/cli/serve.d.ts +0 -1
  13. package/dist/app/cli/serve.js +14 -0
  14. package/dist/app/cli/service.d.ts +26 -0
  15. package/dist/app/cli/service.js +36 -5
  16. package/dist/app/cli/skeleton.d.ts +1 -1
  17. package/dist/app/cli/skeleton.js +2 -2
  18. package/dist/app/cli/tray.js +6 -4
  19. package/dist/app/cli/update-cmd.d.ts +38 -0
  20. package/dist/app/cli/update-cmd.js +174 -0
  21. package/dist/app/cli/usage.d.ts +1 -1
  22. package/dist/app/cli/usage.js +4 -1
  23. package/dist/app/http/app.d.ts +2 -0
  24. package/dist/app/http/app.js +18 -3
  25. package/dist/app/http/parent-home.js +4 -0
  26. package/dist/app/http/parent-setup.d.ts +19 -0
  27. package/dist/app/http/parent-setup.js +96 -0
  28. package/dist/app/machine/bailian.d.ts +26 -0
  29. package/dist/app/machine/bailian.js +91 -4
  30. package/dist/app/machine/update.d.ts +14 -1
  31. package/dist/app/machine/update.js +35 -14
  32. package/dist/brain/api.js +6 -3
  33. package/dist/memory/vault-notes.d.ts +4 -0
  34. package/dist/memory/vault-notes.js +11 -0
  35. package/dist/realtime/preview.d.ts +16 -0
  36. package/dist/realtime/preview.js +43 -1
  37. package/dist/runtime/live/call.d.ts +2 -0
  38. package/dist/runtime/live/call.js +6 -1
  39. package/dist/shared/upgrade.d.ts +5 -3
  40. package/dist/shared/upgrade.js +8 -4
  41. package/dist/ui/kid/kid-page.js +8 -2
  42. package/dist/ui/parent/parent-home-page.js +67 -3
  43. package/dist/ui/parent/parent-settings-page.js +65 -1
  44. package/package.json +7 -1
  45. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +1 -1
  46. 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
+ }
@@ -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);--json \u91CC\u4E0D\u8FC7\u7684\u9879\u5E26 cmd(\u80FD\u76F4\u63A5\u8DD1)\u6216 human(\u8981\u4EBA\u6765)\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";
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] [--fix] \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);--fix \u628A\u5E26 cmd \u7684\u4E00\u6761\u6761\u4FEE\u6389,\u8981\u4EBA\u6765\u7684\u7559\u7740\n cotutor report [--workspace <dir>] \u8BCA\u65AD\u4FE1\u606F:\u7248\u672C\u3001\u7CFB\u7EDF\u3001\u670D\u52A1\u3001\u4F53\u68C0\u6CA1\u8FC7\u7684\u3001\u6700\u8FD1\u62A5\u9519\u7684\u539F\u56E0;\u4E0D\u542B\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u7167\u7247\u3001\u5F55\u97F3\u3001\u6863\u6848\u548C key,\u540D\u5B57\u548C IP \u5DF2\u62B9\u6389,\u5148\u770B\u518D\u53D1\n cotutor backup [list | restore <\u540D\u5B57> [--yes]] [--workspace <dir>] \u5907\u4EFD\u5230 workspace \u65C1\u8FB9\u7684 .backups/:\u4E0D\u542B\u5F55\u97F3;\u670D\u52A1\u5F00\u7740\u65F6\u6BCF\u5929\u4E00\u4EFD(\u7559 14 \u4EFD),cotutor upgrade \u524D\u4E00\u4EFD(\u7559 5 \u4EFD);\u6CA1\u53D8\u7684\u6587\u4EF6\u786C\u94FE\u63A5,\u4E0D\u5360\u7B2C\u4E8C\u4EFD\u5730\u65B9;restore \u5148\u628A\u73B0\u5728\u7684\u4E5F\u5907\u4E00\u4EFD\u518D\u62F7\u56DE\u53BB,\u4E0D\u52A0 --yes \u53EA\u8BF4\u4F1A\u505A\u4EC0\u4E48\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";
@@ -2,7 +2,10 @@
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);--json 里不过的项带 cmd(能直接跑)或 human(要人来)
5
+ cotutor doctor [--workspace <dir>] [--json] [--live] [--fix] 逐项体检;--live 真连一次百炼实时模型、真合成一句配音(花一分钱)把 API 层的坑摆出来(真跑老师用 cotutor send);--json 里不过的项带 cmd(能直接跑)或 human(要人来);--fix 把带 cmd 的一条条修掉,要人来的留着
6
+ cotutor report [--workspace <dir>] 诊断信息:版本、系统、服务、体检没过的、最近报错的原因;不含孩子的对话、照片、录音、档案和 key,名字和 IP 已抹掉,先看再发
7
+ cotutor backup [list | restore <名字> [--yes]] [--workspace <dir>] 备份到 workspace 旁边的 .backups/:不含录音;服务开着时每天一份(留 14 份),cotutor upgrade 前一份(留 5 份);没变的文件硬链接,不占第二份地方;restore 先把现在的也备一份再拷回去,不加 --yes 只说会做什么
8
+ cotutor update [--anyway] 升级一条命令管到底:问 npm 有没有新版本,换程序,每个孩子更新出厂文件、重起服务和图标;孩子在通话时不动(--anyway 才动),npm 没成服务不动
6
9
  cotutor upgrade [--workspace <dir>] [--force <老师>]... 老师文件、activity 与 skill 换新版:没改过的直接换,改过的只报 diff(--force <老师> 或 --force <老师>/<名> 才覆盖,原文留 .bak)
7
10
  cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json 补缺:新出厂老师 / 运行时 / 命令模板旗标(只加缺的,你改过的值不动)
8
11
  cotutor add <老师名> --display <显示名> [--subject <学科>] [--avatar <emoji|图片相对路径>] [--hidden] 加一位自家的老师:出模板文件、进 cotutor.json、建目录(名字以 -tutor 结尾的是老师,figshot 给它做头像)
@@ -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;
@@ -33,7 +33,7 @@ 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 } from "../machine/update.js";
36
+ import { knownLatest, knownNotes } from "../machine/update.js";
37
37
  import { isNewer } from "../../shared/upgrade.js";
38
38
  import { qrPage } from "../../ui/parent/qr-page.js";
39
39
  import { kidPage } from "../../ui/kid/kid-page.js";
@@ -82,6 +82,12 @@ export function createContext(ws, opts = {}) {
82
82
  openCalls: null,
83
83
  access: opts.access ?? createAccess({ now: () => ctx.now() }),
84
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
+ },
85
91
  async reload() {
86
92
  try {
87
93
  const r = await reloadIfChanged(ctx.ws, configText);
@@ -96,6 +102,15 @@ export function createContext(ws, opts = {}) {
96
102
  }
97
103
  },
98
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
+ });
99
114
  return ctx;
100
115
  }
101
116
  const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c] ?? c);
@@ -129,7 +144,7 @@ export async function kidHome(ctx, now) {
129
144
  const date = localDate(now);
130
145
  const view = await kidHomeView(ws, now);
131
146
  const figshot = ws.config.figshot ? { port: ws.config.figshot.port } : null;
132
- 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() };
133
148
  }
134
149
  export async function kidHistory(ctx, tutor, days) {
135
150
  const today = localDate(ctx.now());
@@ -463,7 +478,7 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
463
478
  if (p === '/api/health') {
464
479
  const head = await headNow();
465
480
  const vault = await readdir(ws.paths.vault).then(() => 'ok', (err) => (err.code === 'EPERM' || err.code === 'EACCES' ? 'denied' : 'missing'));
466
- 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, latest: isNewer(knownLatest(), BUILD.version) ? knownLatest() : null } };
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() : [] } };
467
482
  }
468
483
  // 家长端与工作台的锁(access.ts):对 PIN 换 cookie;设了 PIN、没凭证的设备,孩子端以外的页面回输 PIN 的页、接口回 401。没设 PIN 什么都不拦
469
484
  if (p === '/api/auth') {
@@ -25,6 +25,7 @@ import { wordEmoji } from "../../ui/kid/word-emoji.js";
25
25
  import { shownAvatar } from "./avatars.js";
26
26
  import { qrNotes, qrSvg } from "../../ui/shared/qr-svg.js";
27
27
  import { readDevices } from "../../runtime/journal/kid-log.js";
28
+ import { parentSetupRoute } from "./parent-setup.js";
28
29
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
29
30
  const bad = (message) => ({ status: 400, json: { error: 'bad_request', message } });
30
31
  /** 家长端列的老师:有脸的(-tutor 结尾)、开着的;藏着的也列(家长要能把他放回首页) */
@@ -160,6 +161,9 @@ async function kidQr(ctx) {
160
161
  };
161
162
  }
162
163
  export async function parentHomeRoute(method, p, ctx, body) {
164
+ const care = await parentSetupRoute(method, p, ctx, body);
165
+ if (care)
166
+ return care;
163
167
  if (p === '/api/parent/home' && method === 'GET')
164
168
  return { status: 200, json: await parentHome(ctx) };
165
169
  if (p === '/api/parent/qr' && method === 'GET')
@@ -0,0 +1,19 @@
1
+ import type { AppContext, RouteResult } from './app.ts';
2
+ export interface SetupStep {
3
+ id: string;
4
+ done: boolean;
5
+ /** true = 不做孩子用不了;false = 建议 */
6
+ required: boolean;
7
+ title: string;
8
+ detail: string;
9
+ /** 要在这台电脑的终端里跑的 */
10
+ commands?: string[];
11
+ /** 这一步直接在页面上选年级 */
12
+ grades?: boolean;
13
+ link?: {
14
+ href: string;
15
+ text: string;
16
+ };
17
+ }
18
+ export declare function setupSteps(ctx: AppContext): Promise<SetupStep[]>;
19
+ export declare function parentSetupRoute(method: string, p: string, ctx: AppContext, body: unknown): Promise<RouteResult | null>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 家长端「还差几步」、备份、诊断信息。第一次打开家长端,家长要知道接下来做什么,不用开终端跑 doctor。
3
+ *
4
+ * GET /api/parent/setup {steps: [{id, done, required, title, detail, commands?, grades?}]}
5
+ * POST /api/parent/setup/grade {grade: 1-6}:档案没写学期时,按年级算出 school_start 写进档案
6
+ * GET /api/parent/report {text}:诊断信息(cli/report.ts,已抹掉名字和 key,给人先看再发)
7
+ * GET /api/parent/backups {dir, list: [...], commands}(给家长抄的命令,Windows 上带 .cmd)
8
+ * POST /api/parent/backups 现在备份一份
9
+ *
10
+ * 步骤用 doctor 的结果,不另查一遍;只挑家长要动手的那几项,说家长听得懂的话。别的(老师文件、技能)doctor --fix 能修,不打扰家长。
11
+ */
12
+ import { readFile, writeFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { schoolStartFor, setSchoolStart } from "../../memory/vault-notes.js";
15
+ import { scanVault } from "../../memory/vault.js";
16
+ import { readDevices } from "../../runtime/journal/kid-log.js";
17
+ import { cliLine } from "../../shared/upgrade.js";
18
+ import { redactHome } from "../workspace.js";
19
+ import { BUILD } from "../build.js";
20
+ import { backupsOf, listBackups, makeBackup } from "../cli/backup.js";
21
+ import { doctorWorkspace } from "../cli/doctor.js";
22
+ import { buildReport } from "../cli/report.js";
23
+ export async function setupSteps(ctx) {
24
+ const doc = await doctorWorkspace(ctx.ws.root, { probeEnv: true, firewall: false, now: ctx.now() });
25
+ const by = (name) => doc.checks.find((c) => c.name === name);
26
+ const devices = Object.keys(await readDevices(ctx.ws)).length;
27
+ const https = Boolean(ctx.listen?.().https);
28
+ const certOnDisk = by('https')?.ok === true;
29
+ const profile = by('vault.profile');
30
+ const steps = [
31
+ { id: 'bailian', done: ctx.bailian.status().ready, required: true, title: '填百炼的 key', detail: '老师开口和写讲稿都靠它。在这一页下面「百炼」那块填,保存前会先试连。' },
32
+ {
33
+ id: 'https', done: https, required: true, title: '给平板签证书', detail: '平板上按住说话要 HTTPS。证书要在这台电脑上签一次,根证书还要装到平板上。',
34
+ link: { href: 'https://github.com/TarsLab/cotutor/blob/main/docs/iPad与iPhone.md', text: '一步步的说明' },
35
+ commands: (certOnDisk ? ['cotutor service restart'] : ['cotutor cert', 'cotutor service restart']).map((c) => cliLine(c)),
36
+ },
37
+ { id: 'tablet', done: devices > 0, required: true, title: '让孩子的平板连上', detail: '用平板的相机扫这一页上「扫码打开」的那张码。' },
38
+ ];
39
+ if (profile)
40
+ steps.push({ id: 'semester', done: profile.ok, required: true, title: '告诉老师孩子几年级', detail: profile.ok ? profile.detail : '老师要知道现在是几年级哪个学期,才讲得对。', grades: !profile.ok });
41
+ const access = by('server.access');
42
+ if (access)
43
+ steps.push({ id: 'pin', done: access.ok, required: false, title: '给家长端设个密码', detail: '现在连上家里网络的设备都能打开家长端。家长在旁边可以不设。', commands: [cliLine('cotutor pin')] });
44
+ const sleep = by('mac.sleep') ?? by('windows.sleep');
45
+ if (sleep)
46
+ steps.push({ id: 'sleep', done: sleep.ok, required: false, title: '别让这台电脑睡着', detail: sleep.ok ? sleep.detail : sleep.fix ?? sleep.detail });
47
+ return steps;
48
+ }
49
+ export async function parentSetupRoute(method, p, ctx, body) {
50
+ if (p === '/api/parent/setup') {
51
+ if (method !== 'GET')
52
+ return { status: 405, json: { error: 'method_not_allowed' } };
53
+ return { status: 200, json: { steps: await setupSteps(ctx) } };
54
+ }
55
+ if (p === '/api/parent/setup/grade') {
56
+ if (method !== 'POST')
57
+ return { status: 405, json: { error: 'method_not_allowed' } };
58
+ const grade = typeof body === 'object' && body !== null ? body.grade : undefined;
59
+ if (typeof grade !== 'number' || !Number.isInteger(grade) || grade < 1 || grade > 6)
60
+ return { status: 400, json: { error: 'bad_request', message: '年级要 1 到 6' } };
61
+ const found = (await scanVault(ctx.ws)).notes.find((n) => n.props.cotutor === 'profile');
62
+ if (!found)
63
+ return { status: 409, json: { error: 'no_profile', message: '还没有档案:在终端里跑 cotutor init 补一篇' } };
64
+ if (found.props.school_start?.trim() || found.props.semester?.trim())
65
+ return { status: 409, json: { error: 'already_set', message: '档案里已经写了学期,要改就直接改档案' } };
66
+ const file = join(ctx.ws.paths.vault, found.path);
67
+ await writeFile(file, setSchoolStart(await readFile(file, 'utf8'), schoolStartFor(grade, ctx.now())));
68
+ return { status: 200, json: { ok: true } };
69
+ }
70
+ if (p === '/api/parent/report') {
71
+ if (method !== 'GET')
72
+ return { status: 405, json: { error: 'method_not_allowed' } };
73
+ const t = ctx.bailian.trouble();
74
+ const text = await buildReport(ctx.ws, {
75
+ now: ctx.now(),
76
+ health: { version: BUILD.version, started: BUILD.started, mode: process.env.COTUTOR_SERVICE ? 'service' : 'foreground', scheme: ctx.listen?.().https ? 'https' : 'http', bailianTrouble: t, configError: ctx.configError },
77
+ });
78
+ return { status: 200, json: { text } };
79
+ }
80
+ if (p === '/api/parent/backups') {
81
+ const target = { root: ctx.ws.root, vault: ctx.ws.paths.vault };
82
+ if (method === 'POST') {
83
+ try {
84
+ const b = await makeBackup(target, { why: 'manual', version: BUILD.version, now: ctx.now() });
85
+ return { status: 200, json: { ok: true, backup: b } };
86
+ }
87
+ catch (err) {
88
+ return { status: 500, json: { error: 'backup_failed', message: err instanceof Error ? err.message : String(err) } };
89
+ }
90
+ }
91
+ if (method !== 'GET')
92
+ return { status: 405, json: { error: 'method_not_allowed' } };
93
+ return { status: 200, json: { dir: redactHome(backupsOf(ctx.ws.root)), list: await listBackups(ctx.ws.root), commands: { list: cliLine('cotutor backup list'), restore: cliLine('cotutor backup restore <名字> --yes') } } };
94
+ }
95
+ return null;
96
+ }
@@ -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;