cotutor 0.4.1 → 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 +12 -5
- package/dist/app/cli/doctor.js +11 -2
- package/dist/app/cli/main.js +14 -2
- package/dist/app/cli/serve.d.ts +0 -1
- package/dist/app/cli/serve.js +5 -0
- package/dist/app/cli/service.d.ts +26 -0
- package/dist/app/cli/service.js +36 -5
- package/dist/app/cli/tray.js +6 -4
- 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 +1 -0
- package/dist/app/http/app.d.ts +2 -0
- package/dist/app/http/app.js +18 -3
- package/dist/app/machine/bailian.d.ts +26 -0
- package/dist/app/machine/bailian.js +91 -4
- package/dist/app/machine/update.d.ts +14 -1
- package/dist/app/machine/update.js +35 -14
- 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 +3 -3
- package/dist/shared/upgrade.js +3 -3
- package/dist/ui/kid/kid-page.js +8 -2
- package/dist/ui/parent/parent-home-page.js +26 -3
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -100,6 +100,8 @@ import { loadWorkspace } from 'cotutor/workspace'; // 解
|
|
|
100
100
|
|
|
101
101
|
换了运行时,之前的话题接不上,也不导入历史:孩子接着说会开新话题;换之前聊的话题不记账,家长端卡上标「换过运行时 · 不记账」,记录照样能看。模型、预算与老师运行时都在 workspace 的 `cotutor.json` 里,编辑器按首行 `$schema` 提供字段说明。
|
|
102
102
|
|
|
103
|
+
百炼欠费、key 失效或这台电脑连不上百炼时,图标出橙点并通知,家长端首页说清楚是哪一种;要家长动手的那几种,孩子端显示「老师们还没到,叫爸爸妈妈来看一下」。处理好以后几分钟内自己恢复,也可以在家长端点「试连」。
|
|
104
|
+
|
|
103
105
|
`cotutor doctor` 检查缺失配置并给修复建议;`--live` 会实际连接服务。页面上的老师起不来时,先看 doctor 和 `conversations/<老师>/<日期>.<job>.err.log`。家长端与工作台需要访问锁时,运行 `cotutor pin` 设置本机 PIN。
|
|
104
106
|
|
|
105
107
|
## 数据与升级
|
|
@@ -108,7 +110,15 @@ import { loadWorkspace } from 'cotutor/workspace'; // 解
|
|
|
108
110
|
|
|
109
111
|
**有新版本时**,后台服务每天问一次 npm(只发一个普通请求,不带孩子或使用的任何信息),菜单栏(任务栏)图标的菜单和家长端「首页」顶部会说,通知一次,孩子端不说。不会自动升级,因为要重启服务,孩子可能正在上课。不想让它问:在 `~/.config/cotutor/update.json` 写 `{"off": true}`。
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
升级挑孩子不用的时候,运行一条命令就够了(Windows 上写成 `cotutor.cmd update`,因为 PowerShell 缺省禁止运行脚本,提示里给的就是这样):
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
cotutor update
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
它问 npm 有没有新版本,换上新程序,把每个孩子未修改的出厂老师、activity、主题与技能更新掉,再重起服务和图标。孩子正在通话时它不动;下载没成时服务也不动,还跑旧版。每个版本改了什么见 [更新说明](CHANGELOG.md)。
|
|
120
|
+
|
|
121
|
+
0.4.1 及更早的版本还没有这条命令,第一次要依次运行下面三条,Windows 上同样写成 `npm.cmd`、`cotutor.cmd`。几个孩子的话,后两条每个孩子各跑一次,加 `--workspace ~/cotutor/<孩子>`:
|
|
112
122
|
|
|
113
123
|
```sh
|
|
114
124
|
npm i -g cotutor # 换上新版程序
|
|
@@ -116,12 +126,9 @@ cotutor upgrade # 更新未修改的出厂老师、activit
|
|
|
116
126
|
cotutor service restart # 服务和图标换上新代码(在终端里跑的就重开 serve)
|
|
117
127
|
```
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
几个孩子的话,后两条每个孩子各跑一次,加 `--workspace ~/cotutor/<孩子>`。然后看一眼体检,它提示要迁移配置时再跑后面两条:
|
|
129
|
+
升完它提示要迁移配置时再跑:
|
|
122
130
|
|
|
123
131
|
```sh
|
|
124
|
-
cotutor doctor
|
|
125
132
|
cotutor upgrade --config --dry-run # 预览 cotutor.json 的迁移
|
|
126
133
|
cotutor upgrade --config # 执行配置迁移
|
|
127
134
|
```
|
package/dist/app/cli/doctor.js
CHANGED
|
@@ -263,8 +263,13 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
263
263
|
const { portHolder, portState } = await import("./port.js");
|
|
264
264
|
const port = opts.port ?? ws.config.server.port;
|
|
265
265
|
const fix = '腾出这个端口;或改 cotutor.json 的 server.port,平板要重新扫码、重加主屏幕图标';
|
|
266
|
-
|
|
266
|
+
const running = await probeHealth(port);
|
|
267
|
+
if (running) {
|
|
267
268
|
push({ name: 'server.port', ok: true, required: false, detail: `端口 ${port} 上 cotutor 在答应` });
|
|
269
|
+
// 配好了不等于用得了:欠费、key 失效是跑起来才知道的,问在跑的那个服务
|
|
270
|
+
if (running.bailianTrouble)
|
|
271
|
+
push({ name: 'bailian.runtime', ok: false, required: false, detail: `${running.bailianTrouble.message}(${running.bailianTrouble.why ?? ''})`, fix: running.bailianTrouble.fix });
|
|
272
|
+
}
|
|
268
273
|
else {
|
|
269
274
|
const state = await portState(port);
|
|
270
275
|
if (state === 'free')
|
|
@@ -540,7 +545,11 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
540
545
|
if (!s)
|
|
541
546
|
push({ name: 'service', ok: false, required: false, detail: '没装后台服务:在终端里跑的 cotutor serve,关了终端就停', fix: 'cotutor service install(登录后自己起来、崩了自己再起)', cmd: `cotutor service install --no-open${flag}` });
|
|
542
547
|
else {
|
|
548
|
+
const { stableNode } = await import("./service.js");
|
|
543
549
|
const gone = s.args.slice(0, 2).filter((f) => !existsSync(f));
|
|
550
|
+
// 还没坏,但 brew upgrade 以后就坏了,而且坏的时候图标也起不来,没人报警
|
|
551
|
+
if (!gone.length && stableNode(s.args[0]) !== s.args[0])
|
|
552
|
+
push({ name: 'service.node', ok: false, required: false, detail: `后台服务写死了带版本号的 node(${redactHome(s.args[0])}),brew upgrade 以后服务和图标都起不来`, fix: 'cotutor service install(换成不带版本号的路径)', cmd: `cotutor service install --no-open${flag}` });
|
|
544
553
|
push({ name: 'service', ok: !gone.length, required: false, detail: gone.length ? `后台服务 ${s.label} 指的 ${gone.map((f) => redactHome(f)).join('、')} 不在了(换了 node 版本、包挪了地方)` : `后台服务 ${s.label}(cotutor service status 看在不在跑)`, fix: gone.length ? 'cotutor service install(照现在的 node 与包重写一遍)' : undefined, cmd: gone.length ? `cotutor service install${flag}` : undefined });
|
|
545
554
|
}
|
|
546
555
|
}
|
|
@@ -565,7 +574,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
565
574
|
const { readUpdateRecord } = await import("../machine/update.js");
|
|
566
575
|
const u = await readUpdateRecord();
|
|
567
576
|
if (!u.off && isNewer(u.latest, BUILD.version))
|
|
568
|
-
push({ name: 'update', ok: false, required: false, detail: `有新版本 ${u.latest}(现在是 ${BUILD.version})`, fix:
|
|
577
|
+
push({ name: 'update', ok: false, required: false, detail: `有新版本 ${u.latest}(现在是 ${BUILD.version})`, fix: `挑孩子不用的时候,运行:${upgradeSteps().join(' ; ')}` });
|
|
569
578
|
}
|
|
570
579
|
// ---- 用户配置 ----
|
|
571
580
|
try {
|
package/dist/app/cli/main.js
CHANGED
|
@@ -86,7 +86,7 @@ function printOpen(label, page, hint, open) {
|
|
|
86
86
|
}
|
|
87
87
|
const wantsOpen = (flags) => flags.open === true || flags['open-qr'] === true;
|
|
88
88
|
/** 不用 workspace 的命令:--sandbox 对它们没有意义 */
|
|
89
|
-
const NO_WORKSPACE = ['grades', 'init', 'mock', 'pin', 'cert', 'tray'];
|
|
89
|
+
const NO_WORKSPACE = ['grades', 'init', 'mock', 'pin', 'cert', 'tray', 'update'];
|
|
90
90
|
export async function main(argv) {
|
|
91
91
|
let json = false;
|
|
92
92
|
try {
|
|
@@ -301,10 +301,12 @@ export async function main(argv) {
|
|
|
301
301
|
: `! 后台服务读不到 vault(${redactHome(ws.paths.vault)}):看看这个目录的权限,然后 cotutor service restart`);
|
|
302
302
|
if (h?.version && h.version !== version())
|
|
303
303
|
out.push(`! 在跑的是 ${h.version},装的是 ${version()}:cotutor service restart 换上新的`);
|
|
304
|
+
if (h?.bailianTrouble)
|
|
305
|
+
out.push(`! ${h.bailianTrouble.message}:${h.bailianTrouble.fix}`);
|
|
304
306
|
if (h?.scheme === 'http' && httpsFiles(ws))
|
|
305
307
|
out.push('! 证书有了,服务还在走 HTTP(平板上按住说话不可用):cotutor service restart');
|
|
306
308
|
if (isNewer(h?.latest, version()))
|
|
307
|
-
out.push(`! 有新版本 ${h?.latest}(现在是 ${version()})
|
|
309
|
+
out.push(`! 有新版本 ${h?.latest}(现在是 ${version()}),升级运行:${upgradeSteps().join(' ; ')}`);
|
|
308
310
|
return out;
|
|
309
311
|
};
|
|
310
312
|
const tellLog = async (dir) => {
|
|
@@ -552,6 +554,16 @@ export async function main(argv) {
|
|
|
552
554
|
process.stdout.write(`加了主题 ${name}(拷自 ${r.from}):\n ${redactHome(r.dir)}/theme.json ← 槽的清单(名字 + 给什么用)\n ${redactHome(r.dir)}/kid.css ← 样式,改了刷新就有\n把 cotutor.json 的 kid.theme 改成 "${name}" 就换过去了(服务不用重启)。\n`);
|
|
553
555
|
return;
|
|
554
556
|
}
|
|
557
|
+
case 'update': {
|
|
558
|
+
const { realUpdateDeps, runUpdate } = await import("./update-cmd.js");
|
|
559
|
+
process.exitCode = await runUpdate(realUpdateDeps(() => { try {
|
|
560
|
+
return resolveRoot(workspace).root;
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return null;
|
|
564
|
+
} }), { anyway: flags.anyway === true });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
555
567
|
case 'upgrade': {
|
|
556
568
|
if (flags.grade !== undefined)
|
|
557
569
|
throw new UsageError('upgrade 只更新已安装的教学包,不能用 --grade 切换年级');
|
package/dist/app/cli/serve.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ export interface ServeResult {
|
|
|
25
25
|
parentPage: string;
|
|
26
26
|
warnings: string[];
|
|
27
27
|
}
|
|
28
|
-
/** 证书路径:cotutor.json 的 server.https 优先(相对 workspace 根),否则机器级 ~/.config/cotutor/certs/;都没有 → null */
|
|
29
28
|
export declare function httpsFiles(ws: Workspace): {
|
|
30
29
|
cert: string;
|
|
31
30
|
key: string;
|
package/dist/app/cli/serve.js
CHANGED
|
@@ -22,6 +22,7 @@ import { AUTO_BOOKKEEP_EVERY_MS, autoBookkeep } from "../../runtime/auto-bookkee
|
|
|
22
22
|
import { USER_CERT_DIR, expandPath, loadWorkspace } from "../workspace.js";
|
|
23
23
|
export { siteUrl };
|
|
24
24
|
/** 证书路径:cotutor.json 的 server.https 优先(相对 workspace 根),否则机器级 ~/.config/cotutor/certs/;都没有 → null */
|
|
25
|
+
const BAILIAN_RECHECK_MS = 120_000;
|
|
25
26
|
export function httpsFiles(ws) {
|
|
26
27
|
const h = ws.config.server.https;
|
|
27
28
|
if (h)
|
|
@@ -126,6 +127,10 @@ export async function serveWorkspace(opts = {}) {
|
|
|
126
127
|
}, AUTO_BOOKKEEP_EVERY_MS);
|
|
127
128
|
timer.unref();
|
|
128
129
|
server.once('close', () => clearInterval(timer));
|
|
130
|
+
// 嘴连不上时隔一阵再试:充上钱、网回来了,没人点也会恢复,孩子端和图标跟着好
|
|
131
|
+
const recheck = setInterval(() => { void ctx.bailian.recheck().catch(() => undefined); }, BAILIAN_RECHECK_MS);
|
|
132
|
+
recheck.unref();
|
|
133
|
+
server.once('close', () => clearInterval(recheck));
|
|
129
134
|
if (opts.awake !== false) {
|
|
130
135
|
const stop = holdAwake(() => liveUpgrade.openCalls() > 0 || Object.keys(ctx.ws.config.tutors).some((t) => ctx.runner.running(t) !== null));
|
|
131
136
|
server.once('close', stop);
|
|
@@ -28,6 +28,22 @@ export declare function earlyLogHint(platform: ServicePlatform, s: {
|
|
|
28
28
|
}): string;
|
|
29
29
|
/** 看护进程的日志(Windows):serve 退了几次、起来之前的输出 */
|
|
30
30
|
export declare const KEEPER_LOG = "keeper.log";
|
|
31
|
+
/**
|
|
32
|
+
* launchctl bootout 发完信号就回,进程还没退;这时 bootstrap 报 5: Input/output error,服务就停在那儿了(2026-11-07 实测重装 12 次失败 10 次)。
|
|
33
|
+
* 等它真的卸下来再往下走。launchd 最多等进程 20 秒(ExitTimeOut 缺省),再不走它自己会杀。
|
|
34
|
+
*/
|
|
35
|
+
export declare function launchdUnload(host: Pick<ServiceHost, 'run'>, target: string, waitMs?: number): Promise<void>;
|
|
36
|
+
/** 装上;刚卸下的那一下还可能报错,隔一秒再试几次,真的不行才算不行 */
|
|
37
|
+
export declare function launchdLoad(host: Pick<ServiceHost, 'run' | 'uid'>, file: string, tries?: number): Promise<{
|
|
38
|
+
code: number;
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* 写进服务文件、登录项的 node。Homebrew 的 process.execPath 是带版本号的路径(实测 /opt/homebrew/Cellar/node/26.12.1_1/bin/node),
|
|
44
|
+
* brew upgrade 清掉旧版后,服务和图标下次登录都起不来,连报警的都没了;opt/<formula> 这个链接 brew 一直维护。别的管理器(nvm、asdf)不会自己删旧版,不动。
|
|
45
|
+
*/
|
|
46
|
+
export declare function stableNode(execPath: string, exists?: (p: string) => boolean): string;
|
|
31
47
|
/**
|
|
32
48
|
* service install 之后开不开家长端:只有这个孩子第一次装时开,下一步就是填百炼、扫码。
|
|
33
49
|
* 别的启动都不开:登录、崩溃重起、升级、重启都不是人要看页面;放进 serve 的话,一直崩就隔 10 秒开一个标签页。
|
|
@@ -120,8 +136,18 @@ export interface Health {
|
|
|
120
136
|
calls?: number;
|
|
121
137
|
/** 百炼配了没有(托盘:没配算有事要处理) */
|
|
122
138
|
bailian?: boolean;
|
|
139
|
+
/** 跑起来以后百炼出的问题(欠费、key 失效、连不上);没有 → null(machine/bailian.ts) */
|
|
140
|
+
bailianTrouble?: {
|
|
141
|
+
kind: string;
|
|
142
|
+
message: string;
|
|
143
|
+
fix: string;
|
|
144
|
+
why?: string;
|
|
145
|
+
blocked?: boolean;
|
|
146
|
+
} | null;
|
|
123
147
|
/** npm 上比在跑的这份新的版本;没有 → null(machine/update.ts) */
|
|
124
148
|
latest?: string | null;
|
|
149
|
+
/** 那个版本写的更新说明 */
|
|
150
|
+
latestNotes?: string[];
|
|
125
151
|
configError?: string | null;
|
|
126
152
|
/** 问到的是 https 还是 http(这边加的,不是服务回的) */
|
|
127
153
|
scheme?: 'https' | 'http';
|
package/dist/app/cli/service.js
CHANGED
|
@@ -88,6 +88,37 @@ export function earlyLogHint(platform, s) {
|
|
|
88
88
|
export const KEEPER_LOG = 'keeper.log';
|
|
89
89
|
const KEEPER_LOG_MAX = 5 * 1024 * 1024;
|
|
90
90
|
const KEEPER_RESTART_MS = 10_000;
|
|
91
|
+
const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
|
|
92
|
+
/**
|
|
93
|
+
* launchctl bootout 发完信号就回,进程还没退;这时 bootstrap 报 5: Input/output error,服务就停在那儿了(2026-11-07 实测重装 12 次失败 10 次)。
|
|
94
|
+
* 等它真的卸下来再往下走。launchd 最多等进程 20 秒(ExitTimeOut 缺省),再不走它自己会杀。
|
|
95
|
+
*/
|
|
96
|
+
export async function launchdUnload(host, target, waitMs = 25_000) {
|
|
97
|
+
await host.run('launchctl', ['bootout', target]);
|
|
98
|
+
const until = Date.now() + waitMs;
|
|
99
|
+
while ((await host.run('launchctl', ['print', target])).code === 0 && Date.now() < until)
|
|
100
|
+
await sleep(200);
|
|
101
|
+
}
|
|
102
|
+
/** 装上;刚卸下的那一下还可能报错,隔一秒再试几次,真的不行才算不行 */
|
|
103
|
+
export async function launchdLoad(host, file, tries = 5) {
|
|
104
|
+
let r = await host.run('launchctl', ['bootstrap', `gui/${host.uid}`, file]);
|
|
105
|
+
for (let i = 1; i < tries && r.code !== 0; i++) {
|
|
106
|
+
await sleep(1000);
|
|
107
|
+
r = await host.run('launchctl', ['bootstrap', `gui/${host.uid}`, file]);
|
|
108
|
+
}
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 写进服务文件、登录项的 node。Homebrew 的 process.execPath 是带版本号的路径(实测 /opt/homebrew/Cellar/node/26.12.1_1/bin/node),
|
|
113
|
+
* brew upgrade 清掉旧版后,服务和图标下次登录都起不来,连报警的都没了;opt/<formula> 这个链接 brew 一直维护。别的管理器(nvm、asdf)不会自己删旧版,不动。
|
|
114
|
+
*/
|
|
115
|
+
export function stableNode(execPath, exists = existsSync) {
|
|
116
|
+
const m = /^(.*)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/.exec(execPath);
|
|
117
|
+
if (!m)
|
|
118
|
+
return execPath;
|
|
119
|
+
const opt = `${m[1]}/opt/${m[2]}/bin/node`;
|
|
120
|
+
return exists(opt) ? opt : execPath;
|
|
121
|
+
}
|
|
91
122
|
/**
|
|
92
123
|
* service install 之后开不开家长端:只有这个孩子第一次装时开,下一步就是填百炼、扫码。
|
|
93
124
|
* 别的启动都不开:登录、崩溃重起、升级、重启都不是人要看页面;放进 serve 的话,一直崩就隔 10 秒开一个标签页。
|
|
@@ -267,7 +298,7 @@ export async function installService(host, root, opts = {}) {
|
|
|
267
298
|
const logDir = serviceLogDir(host, name);
|
|
268
299
|
// Windows 上跑的是看护进程,它再起 serve(那边没有 launchd / systemd 替你拉起)
|
|
269
300
|
const run = platform === 'win32' ? ['service', 'run'] : ['serve'];
|
|
270
|
-
const spec = { name, label, root, logDir, path: host.env.PATH ?? '/usr/bin:/bin', args: [opts.node ?? process.execPath, opts.bin ?? binPath(), ...run, '--workspace', root, '--log-dir', logDir] };
|
|
301
|
+
const spec = { name, label, root, logDir, path: host.env.PATH ?? '/usr/bin:/bin', args: [opts.node ?? stableNode(process.execPath), opts.bin ?? binPath(), ...run, '--workspace', root, '--log-dir', logDir] };
|
|
271
302
|
const file = fileOf(host, name);
|
|
272
303
|
await mkdir(serviceDir(host), { recursive: true });
|
|
273
304
|
await mkdir(logDir, { recursive: true });
|
|
@@ -284,9 +315,9 @@ export async function installService(host, root, opts = {}) {
|
|
|
284
315
|
}
|
|
285
316
|
else if (platform === 'darwin') {
|
|
286
317
|
// 先卸下旧的(没装过会报错,不管),再装上;bootstrap 会按 RunAtLoad 马上起
|
|
287
|
-
await host
|
|
318
|
+
await launchdUnload(host, `gui/${host.uid}/${label}`);
|
|
288
319
|
await writeFile(file, launchdPlist(spec));
|
|
289
|
-
const r = await host
|
|
320
|
+
const r = await launchdLoad(host, file);
|
|
290
321
|
if (r.code !== 0)
|
|
291
322
|
throw new UsageError(`launchctl bootstrap 没成:${(r.stderr || r.stdout).trim()}`);
|
|
292
323
|
}
|
|
@@ -346,10 +377,10 @@ export async function controlService(host, root, what) {
|
|
|
346
377
|
const loaded = (await host.run('launchctl', ['print', target])).code === 0;
|
|
347
378
|
if (what === 'stop') {
|
|
348
379
|
if (loaded)
|
|
349
|
-
await host
|
|
380
|
+
await launchdUnload(host, target);
|
|
350
381
|
return s;
|
|
351
382
|
}
|
|
352
|
-
const r = loaded ? await host.run('launchctl', what === 'restart' ? ['kickstart', '-k', target] : ['kickstart', target]) : await host
|
|
383
|
+
const r = loaded ? await host.run('launchctl', what === 'restart' ? ['kickstart', '-k', target] : ['kickstart', target]) : await launchdLoad(host, s.file);
|
|
353
384
|
if (r.code !== 0)
|
|
354
385
|
throw new UsageError(`launchctl 没成:${(r.stderr || r.stdout).trim()}`);
|
|
355
386
|
return s;
|
package/dist/app/cli/tray.js
CHANGED
|
@@ -20,7 +20,7 @@ import { siteUrl } from "../../shared/site-url.js";
|
|
|
20
20
|
import { isNewer } from "../../shared/upgrade.js";
|
|
21
21
|
import { portHolder } from "./port-holder.js";
|
|
22
22
|
import { RUN_KEY, STARTUP_APPROVED_KEY, encodeLauncher, parseProcs, psArgs, runValue, startupDisabled, winCommandLine } from "./service-windows.js";
|
|
23
|
-
import { binPath, controlService, listServices, parseLaunchctlPrint, probeHealth, wscriptPath, xml } from "./service.js";
|
|
23
|
+
import { binPath, controlService, listServices, parseLaunchctlPrint, probeHealth, wscriptPath, xml, launchdLoad, launchdUnload, stableNode } from "./service.js";
|
|
24
24
|
import { macTrayIcons, windowsTrayIcons } from "./tray-icons.js";
|
|
25
25
|
import { HELPER_NO_SESSION, TRAY_AUMID, TRAY_LABEL, TRAY_RUN_VALUE } from "./tray-ids.js";
|
|
26
26
|
import { MAC_TRAY_SCRIPT } from "./tray-mac.js";
|
|
@@ -65,6 +65,8 @@ export function issuesOf(p, o) {
|
|
|
65
65
|
out.push({ text: 'vault 不在了:看看 cotutor.json 的 paths.vault', fix: { kind: 'open', target: p.root } });
|
|
66
66
|
if (h.bailian === false)
|
|
67
67
|
out.push({ text: '还没填百炼:老师们到不了', fix: { kind: 'open', target: url } });
|
|
68
|
+
else if (h.bailianTrouble)
|
|
69
|
+
out.push({ text: h.bailianTrouble.message, fix: { kind: 'open', target: url } });
|
|
68
70
|
if (h.version && h.version !== o.version)
|
|
69
71
|
out.push({ text: `在跑的是 ${h.version},装的是 ${o.version}:重启一下`, fix: { kind: 'restart' } });
|
|
70
72
|
return out;
|
|
@@ -540,7 +542,7 @@ export function realTrayDeps(host, dir, file, version) {
|
|
|
540
542
|
};
|
|
541
543
|
}
|
|
542
544
|
/** 登录项跑的命令 */
|
|
543
|
-
export const trayArgs = (host, o = {}) => [o.node ?? process.execPath, o.bin ?? binPath(), 'tray', 'run', '--dir', trayDir(host)];
|
|
545
|
+
export const trayArgs = (host, o = {}) => [o.node ?? stableNode(process.execPath), o.bin ?? binPath(), 'tray', 'run', '--dir', trayDir(host)];
|
|
544
546
|
/** macOS 的 LaunchAgent:登录就起,崩了拉起,以 0 退(点了退出)不拉 */
|
|
545
547
|
export function trayPlist(args, dir, path) {
|
|
546
548
|
const str = (v) => `<string>${xml(v)}</string>`;
|
|
@@ -598,9 +600,9 @@ export async function installTray(host, o = {}) {
|
|
|
598
600
|
}
|
|
599
601
|
const file = plistFile(host);
|
|
600
602
|
await mkdir(join(host.home, 'Library', 'LaunchAgents'), { recursive: true });
|
|
601
|
-
await host
|
|
603
|
+
await launchdUnload(host, `gui/${host.uid}/${TRAY_LABEL}`);
|
|
602
604
|
await writeFile(file, trayPlist(args, dir, host.env.PATH ?? '/usr/bin:/bin'));
|
|
603
|
-
const r = await host
|
|
605
|
+
const r = await launchdLoad(host, file);
|
|
604
606
|
return { file, started: r.code === 0 };
|
|
605
607
|
}
|
|
606
608
|
/** 卸掉:停下、删登录项。remember = 家长说不要(以后 service install 也不再装) */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Release } from '../machine/update.ts';
|
|
2
|
+
import { type Health } from './service.ts';
|
|
3
|
+
export interface UpdateService {
|
|
4
|
+
name: string;
|
|
5
|
+
kid: string;
|
|
6
|
+
root: string;
|
|
7
|
+
port: number;
|
|
8
|
+
node: string;
|
|
9
|
+
health: Health | null;
|
|
10
|
+
}
|
|
11
|
+
export interface UpdateDeps {
|
|
12
|
+
current: string;
|
|
13
|
+
/** 从仓库跑的:版本跟着 git 走,npm 不管 */
|
|
14
|
+
fromRepo: boolean;
|
|
15
|
+
nodeVersion: string;
|
|
16
|
+
release(): Promise<Release | null>;
|
|
17
|
+
services(): Promise<UpdateService[]>;
|
|
18
|
+
health(port: number): Promise<Health | null>;
|
|
19
|
+
npm(spec: string, registry: string | null): Promise<{
|
|
20
|
+
code: number;
|
|
21
|
+
out: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** 现在装着的版本:读文件,npm 刚换过 */
|
|
24
|
+
installed(): string | null;
|
|
25
|
+
/** 跑新的 bin */
|
|
26
|
+
cli(args: string[]): Promise<{
|
|
27
|
+
code: number;
|
|
28
|
+
out: string;
|
|
29
|
+
}>;
|
|
30
|
+
/** 没有后台服务时,在终端里跑 serve 的那个孩子 */
|
|
31
|
+
workspace(): string | null;
|
|
32
|
+
stableNode(path: string): string;
|
|
33
|
+
print(line: string): void;
|
|
34
|
+
}
|
|
35
|
+
export declare function runUpdate(d: UpdateDeps, o?: {
|
|
36
|
+
anyway?: boolean;
|
|
37
|
+
}): Promise<number>;
|
|
38
|
+
export declare function realUpdateDeps(workspace: () => string | null): UpdateDeps;
|
|
@@ -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);--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] \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
|
@@ -3,6 +3,7 @@ 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
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 给它做头像)
|
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;
|