cotutor 0.4.2 → 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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * 诊断信息(cotutor report、家长端「设置」里的「生成诊断信息」):出了问题时一次说清,发给开发者或帮忙的 AI 助手,不用来回问。
3
+ * 不含孩子的对话、照片、录音、档案和 key。只收:版本和系统、服务状态、体检没过的、最近出错的那几轮的原因(不要孩子说了什么)、日志里报错的行、孩子端页面的报错。
4
+ * 孩子的名字、key、业务空间、电脑名、IP、用户名一律抹掉(scrub),最后整篇再抹一遍。给人先看再发,不自动上传。
5
+ */
6
+ import { arch, homedir, hostname, platform, release, userInfo } from 'node:os';
7
+ import { localDate } from "../../shared/time.js";
8
+ import { dashscopeAuth } from "../../realtime/bailian.js";
9
+ import { listDates, readIndex } from "../../runtime/journal/store.js";
10
+ import { readKidLog } from "../../runtime/journal/kid-log.js";
11
+ import { BUILD } from "../build.js";
12
+ import { listTutors } from "../../schema/config.js";
13
+ import { doctorWorkspace } from "./doctor.js";
14
+ import { listBackups } from "./backup.js";
15
+ import { findService, realHost, tailLog } from "./service.js";
16
+ import { trayStatus } from "./tray.js";
17
+ import { computerName } from "./serve.js";
18
+ const DAYS = 7;
19
+ const MAX_LINE = 180;
20
+ const MAX_ERRORS = 15;
21
+ const LOG_ERROR = /error|fail|失败|没成|崩|exception|ENO[A-Z]+|EADDR|EPERM|EACCES/i;
22
+ const cut = (s, n = MAX_LINE) => (s.length > n ? `${s.slice(0, n)}…` : s);
23
+ /** 抹掉能认出这家人的东西。太短的(一个字的小名)不抹,不然满篇都被替换 */
24
+ export function scrub(text, secrets) {
25
+ let out = text.replace(/sk-[A-Za-z0-9._-]{4,}/g, 'sk-***').replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, (ip) => (ip === '127.0.0.1' ? ip : '<IP>'));
26
+ const home = homedir();
27
+ if (home)
28
+ out = out.split(home).join('$HOME');
29
+ const list = [...new Set(secrets.filter((s) => typeof s === 'string' && s.trim().length >= 2))].sort((a, b) => b.length - a.length);
30
+ for (const s of list)
31
+ out = out.split(s).join('<隐去>');
32
+ return out;
33
+ }
34
+ /** 这家人的名字们 */
35
+ function secretsOf(ws, env) {
36
+ const auth = dashscopeAuth(env);
37
+ let user = '';
38
+ try {
39
+ user = userInfo().username;
40
+ }
41
+ catch { /* 没有也行 */ }
42
+ const name = computerName();
43
+ return [ws.config.kid.name, ws.config.kid.slug, ws.config.title, auth?.workspaceId, auth?.key, user, name, `${name}.local`, hostname()].filter((s) => Boolean(s));
44
+ }
45
+ const DATE_N = (now, n) => Array.from({ length: n }, (_, i) => localDate(new Date(now.getTime() - i * 24 * 3600_000)));
46
+ /** 最近几天没成的那几轮:只要原因,不要内容 */
47
+ async function failedTurns(ws, now) {
48
+ const out = [];
49
+ const since = DATE_N(now, DAYS).at(-1);
50
+ for (const t of listTutors(ws.config)) {
51
+ for (const date of (await listDates(ws, t.name).catch(() => [])).filter((d) => d >= since)) {
52
+ const index = await readIndex(ws, t.name, date).catch(() => null);
53
+ for (const m of index?.messages ?? [])
54
+ if (m.result === 'error')
55
+ out.push(`${date} ${t.name} ${cut(String(m.error ?? '没说原因'), 120)}`);
56
+ }
57
+ }
58
+ return out.sort().reverse().slice(0, MAX_ERRORS);
59
+ }
60
+ /** 孩子端页面报的错:错误信息和位置,不要上下文(ctx 里可能有孩子正在做的) */
61
+ async function kidErrors(ws, now) {
62
+ const rows = await readKidLog(ws, DATE_N(now, DAYS)).catch(() => []);
63
+ const seen = new Map();
64
+ for (const r of rows)
65
+ if (r.k === 'error') {
66
+ const key = `${r.page} ${cut(r.msg, 120)} @ ${cut(r.where, 60)}`;
67
+ seen.set(key, (seen.get(key) ?? 0) + 1);
68
+ }
69
+ return [...seen].slice(-MAX_ERRORS).map(([k, n]) => (n > 1 ? `${k} ×${n}` : k));
70
+ }
71
+ /** 服务日志里报错的行 */
72
+ async function logErrors(logDir) {
73
+ if (!logDir)
74
+ return [];
75
+ const t = await tailLog(logDir, 400).catch(() => null);
76
+ return (t?.lines ?? []).filter((l) => LOG_ERROR.test(l)).slice(-MAX_ERRORS).map((l) => cut(l));
77
+ }
78
+ export async function buildReport(ws, o) {
79
+ const now = o.now ?? new Date();
80
+ const env = o.env ?? process.env;
81
+ const host = realHost();
82
+ const h = o.health;
83
+ const lines = [];
84
+ const section = (title, items) => {
85
+ lines.push('', `## ${title}`);
86
+ lines.push(...(items.length ? items.map((s) => `- ${s}`) : ['- 没有']));
87
+ };
88
+ lines.push('# cotutor 诊断信息', '不含孩子的对话、照片、录音、档案和 key;孩子的名字、电脑名、IP 已隐去。', '');
89
+ lines.push(`- 版本:${BUILD.version ?? '?'}${BUILD.commit ? `(从仓库跑的,${BUILD.commit.slice(0, 7)})` : ''}`);
90
+ lines.push(`- 系统:${platform()} ${release()} ${arch()};Node ${process.version}`);
91
+ lines.push(`- 时间:${now.toISOString()}`);
92
+ const s = await findService(host, ws.root).catch(() => null);
93
+ const tray = await trayStatus(host).catch(() => null);
94
+ const running = h ? `在跑(${h.mode === 'service' ? '后台服务' : '终端里'},版本 ${h.version ?? '?'},起来于 ${h.started},走 ${h.scheme ?? '?'})` : '没答应';
95
+ section('服务', [
96
+ running,
97
+ `后台服务:${s ? '装了' : '没装'}`,
98
+ tray?.supported ? `图标:${tray.installed ? (tray.running ? '在跑' : '装了,没在跑') : '没装'}` : '图标:这个系统上没有',
99
+ ...(h?.bailianTrouble ? [`百炼故障:${h.bailianTrouble.kind}(${h.bailianTrouble.why ?? ''})`] : []),
100
+ ...(h?.configError ? [`配置:${cut(h.configError)}`] : []),
101
+ ]);
102
+ const doc = await doctorWorkspace(ws.root, { probeEnv: true, firewall: false, now, env }).catch(() => null);
103
+ section('体检没过的', doc ? doc.checks.filter((c) => !c.ok).map((c) => `${c.required ? '必需' : '建议'} ${c.name}:${cut(c.detail, 140)}`) : ['体检没跑成']);
104
+ const backups = await listBackups(ws.root).catch(() => []);
105
+ section('备份', backups[0] ? [`最近一份 ${backups[0].at}(${backups[0].why}),共 ${backups.length} 份`] : ['还没有']);
106
+ section(`最近 ${DAYS} 天没成的话题(只有原因)`, await failedTurns(ws, now));
107
+ section('服务日志里报错的行', await logErrors(s?.logDir ?? null));
108
+ section(`孩子端页面最近 ${DAYS} 天的报错`, await kidErrors(ws, now));
109
+ return `${scrub(lines.join('\n'), secretsOf(ws, env))}\n`;
110
+ }
@@ -18,6 +18,8 @@ import { answersLocally } from "./port.js";
18
18
  import { awakeScript, psArgs } from "./service-windows.js";
19
19
  import { createContext, createHandler } from "../http/app.js";
20
20
  import { liveUpgrade } from "../ws/route.js";
21
+ import { startDailyBackups } from "./backup.js";
22
+ import { BUILD } from "../build.js";
21
23
  import { AUTO_BOOKKEEP_EVERY_MS, autoBookkeep } from "../../runtime/auto-bookkeep.js";
22
24
  import { USER_CERT_DIR, expandPath, loadWorkspace } from "../workspace.js";
23
25
  export { siteUrl };
@@ -131,6 +133,13 @@ export async function serveWorkspace(opts = {}) {
131
133
  const recheck = setInterval(() => { void ctx.bailian.recheck().catch(() => undefined); }, BAILIAN_RECHECK_MS);
132
134
  recheck.unref();
133
135
  server.once('close', () => clearInterval(recheck));
136
+ // 每天备份一份(backup.ts):试用的不备;孩子在通话时往后推
137
+ const stopBackups = startDailyBackups({
138
+ target: () => (ctx.ws.sandbox ? null : { root: ctx.ws.root, vault: ctx.ws.paths.vault }),
139
+ busy: () => liveUpgrade.openCalls() > 0,
140
+ version: BUILD.version,
141
+ });
142
+ server.once('close', stopBackups);
134
143
  if (opts.awake !== false) {
135
144
  const stop = holdAwake(() => liveUpgrade.openCalls() > 0 || Object.keys(ctx.ws.config.tutors).some((t) => ctx.runner.running(t) !== null));
136
145
  server.once('close', stop);
@@ -12,7 +12,7 @@ export interface ShippedAgent {
12
12
  export declare function shippedAgents(dir?: string): Promise<ShippedAgent[]>;
13
13
  export declare const GITIGNORE = "# \u8F6C\u5F55\u65E5\u5FD7\u4F53\u79EF\u5927\u3001\u53EF\u4ECE\u5BF9\u8BDD\u7D22\u5F15\u91CD\u5EFA\u5173\u952E\u4FE1\u606F;\u8981\u7559\u5168\u6863\u5C31\u5220\u6389\u4E0B\u9762\u8FD9\u884C\nconversations/**/*.log\n# \u914D\u97F3\u53EF\u91CD\u65B0\u5408\u6210\nconversations/**/*.mp3\n# \u56DE\u653E(cotutor replay)\u662F\u8C03\u63D0\u793A\u8BCD\u4E0E vault \u65F6\u7684\u5BF9\u7167,\u4E0D\u662F\u5B69\u5B50\u7684\u5BF9\u8BDD\nevals/\n# \u505A\u597D\u7684\u8BFE(\u8BFE\u5305\u3001\u89C6\u9891\u8BFE)\u4E0E\u8BFE\u5305\u622A\u56FE:\u4F53\u79EF\u5927,\u6E90\u5728\u5BB6\u957F\u505A\u8BFE\u7684\u5730\u65B9\nbundles/\nlectures/\nsnaps/\n# \u53E3\u8BED\u8BFE(\u5B69\u5B50\u7684\u539F\u58F0\u3001\u7167\u7247)\ntalk/\n# \u4F5C\u4E1A\u7167\u7247(\u5B69\u5B50\u5728\u8001\u5E08\u9875\u4E0A\u62CD\u7684;paths.captures):\u4F53\u79EF\u5927\u3001\u53EA\u662F\u8FD9\u4E00\u8F6E\u7684\u7D20\u6750,\u8BA4\u51FA\u7684\u6587\u5B57\u5728\u65E5\u8BB0\u91CC\ncaptures/\n# \u5BB6\u957F\u5728 workspace \u91CC\u7528 Claude Code \u65F6\u5B83\u7684\u4E2A\u4EBA\u8BBE\u7F6E\n.claude/settings.local.json\n# .cotutor/ \u662F\u673A\u5668\u6587\u4EF6(\u677F\u4E66\u5199\u6CD5\u7F13\u5B58\u3001\u914D\u97F3\u7F13\u5B58\u3001\u4E8C\u7EF4\u7801\u3001\u65E5\u5FD7),\u53EA\u7559\u51FA\u5382 hash \u4E0E schema\n.cotutor/*\n!.cotutor/shipped.json\n!.cotutor/cotutor.schema.json\nnode_modules/\n.DS_Store\n";
14
14
  /** 档案模板(paths.profile,缺了才写):cotutor: profile 的那篇,原文整篇进每个话题的第一条 */
15
- export declare function profileTemplate(name: string): string;
15
+ export declare function profileTemplate(name: string, schoolStart?: string): string;
16
16
  /** 参考目录的 README(缺了才写) */
17
17
  export declare const REFERENCE_README = "# \u53C2\u8003\n\n\u8FD9\u91CC\u653E\u653F\u7B56\u4E0E\u4F60\u81EA\u5DF1\u7684\u7B14\u8BB0:\u8BB2\u67D0\u7C7B\u9898\u7684\u504F\u597D\u3001\u8584\u5F31\u70B9\u3001\u5FCC\u8BB3\u3001\u6309\u9898\u578B\u6574\u7406\u7684\u4E1C\u897F\u2014\u2014\u968F\u4FBF\u5EFA,\u673A\u5668\u53EA\u8BFB\u4E0D\u5199\u3002\n\u8BA9\u8001\u5E08\u770B\u5230\u7684\u529E\u6CD5:\u5728\u6863\u6848\u6216\u8FD9\u4F4D\u8001\u5E08\u7684\u5165\u53E3\u6587\u4EF6(`cotutor: subject` \u7684\u90A3\u7BC7)\u91CC\u94FE\u8FC7\u53BB(`[[\u627E\u89C4\u5F8B\u586B\u6570]]`);\u8001\u5E08\u62FF\u5230\u8DEF\u5F84,\u8981\u7528\u65F6\u81EA\u5DF1\u8BFB\u3002\n";
18
18
  export interface TutorTemplateInput {
@@ -55,12 +55,12 @@ node_modules/
55
55
  .DS_Store
56
56
  `;
57
57
  /** 档案模板(paths.profile,缺了才写):cotutor: profile 的那篇,原文整篇进每个话题的第一条 */
58
- export function profileTemplate(name) {
58
+ export function profileTemplate(name, schoolStart) {
59
59
  return `---
60
60
  cotutor: profile
61
61
  nickname: ${name}
62
62
  birthday:
63
- school_start:
63
+ school_start:${schoolStart ? ` ${schoolStart}` : ''}
64
64
  city:
65
65
  ---
66
66
 
@@ -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 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
+ 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,9 @@
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 只说会做什么
6
8
  cotutor update [--anyway] 升级一条命令管到底:问 npm 有没有新版本,换程序,每个孩子更新出厂文件、重起服务和图标;孩子在通话时不动(--anyway 才动),npm 没成服务不动
7
9
  cotutor upgrade [--workspace <dir>] [--force <老师>]... 老师文件、activity 与 skill 换新版:没改过的直接换,改过的只报 diff(--force <老师> 或 --force <老师>/<名> 才覆盖,原文留 .bak)
8
10
  cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json 补缺:新出厂老师 / 运行时 / 命令模板旗标(只加缺的,你改过的值不动)
@@ -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
+ }
@@ -20,6 +20,10 @@ export interface VaultNote {
20
20
  }
21
21
  /** 只认平铺的 `key: value`;值去掉成对引号。没有 frontmatter → {} */
22
22
  export declare function frontmatter(md: string): Record<string, string>;
23
+ /** 现在读 grade 年级的孩子,读一年级是哪年 9 月(学年从 9 月算,同下面的 semesterAt;7、8 月算刚读完的那个年级) */
24
+ export declare function schoolStartFor(grade: number, now: Date): string;
25
+ /** 把 school_start 写进档案的 frontmatter:有空着的那行就填,没有就加在第一行 --- 后面;别的一个字不动 */
26
+ export declare function setSchoolStart(md: string, value: string): string;
23
27
  /**
24
28
  * 某天是几年级的哪个学期 / 假期:9–1 月上学期、2 月寒假、3–6 月下学期、7–8 月暑假(刚读完的那个年级的暑假)。
25
29
  * 学年从 9 月算;`school_start` 是读一年级那年的 9 月(写别的月份也按它所在学年算)。年级不在 1–12 → null。
@@ -30,6 +30,17 @@ export function frontmatter(md) {
30
30
  return {};
31
31
  }
32
32
  const CN_DIGITS = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
33
+ /** 现在读 grade 年级的孩子,读一年级是哪年 9 月(学年从 9 月算,同下面的 semesterAt;7、8 月算刚读完的那个年级) */
34
+ export function schoolStartFor(grade, now) {
35
+ const year = now.getMonth() + 1 >= 9 ? now.getFullYear() : now.getFullYear() - 1;
36
+ return `${year - (grade - 1)}-09`;
37
+ }
38
+ /** 把 school_start 写进档案的 frontmatter:有空着的那行就填,没有就加在第一行 --- 后面;别的一个字不动 */
39
+ export function setSchoolStart(md, value) {
40
+ if (/^school_start:.*$/m.test(md))
41
+ return md.replace(/^school_start:.*$/m, `school_start: ${value}`);
42
+ return md.replace(/^---\r?\n/, (m) => `${m}school_start: ${value}\n`);
43
+ }
33
44
  /**
34
45
  * 某天是几年级的哪个学期 / 假期:9–1 月上学期、2 月寒假、3–6 月下学期、7–8 月暑假(刚读完的那个年级的暑假)。
35
46
  * 学年从 9 月算;`school_start` 是读一年级那年的 9 月(写别的月份也按它所在学年算)。年级不在 1–12 → null。
@@ -3,6 +3,8 @@
3
3
  export declare const UPGRADE_STEPS: readonly ["cotutor update"];
4
4
  /** 给家长看的那份。Windows 上带 .cmd:PowerShell 缺省禁止跑脚本,不带后缀找到的是被拦的 .ps1,一跑就报错;.cmd 在 PowerShell 和 cmd.exe 里都能跑(2026-11-07 真机) */
5
5
  export declare function upgradeSteps(platform?: NodeJS.Platform): string[];
6
+ /** 给家长抄的一条命令:Windows 上第一个词带 .cmd,原因同上 */
7
+ export declare function cliLine(cmd: string, platform?: NodeJS.Platform): string;
6
8
  /** 正式版的版本号。预发布不算;从网上拿来的还要显示给家长,其余一律不认 */
7
9
  export declare function isPlainVersion(v: unknown): v is string;
8
10
  /** a 比 b 新;任一边不是正式版的版本号 → false,宁可不提醒 */
@@ -3,7 +3,11 @@
3
3
  export const UPGRADE_STEPS = ['cotutor update'];
4
4
  /** 给家长看的那份。Windows 上带 .cmd:PowerShell 缺省禁止跑脚本,不带后缀找到的是被拦的 .ps1,一跑就报错;.cmd 在 PowerShell 和 cmd.exe 里都能跑(2026-11-07 真机) */
5
5
  export function upgradeSteps(platform = process.platform) {
6
- return UPGRADE_STEPS.map((s) => (platform === 'win32' ? s.replace(' ', '.cmd ') : s));
6
+ return UPGRADE_STEPS.map((s) => cliLine(s, platform));
7
+ }
8
+ /** 给家长抄的一条命令:Windows 上第一个词带 .cmd,原因同上 */
9
+ export function cliLine(cmd, platform = process.platform) {
10
+ return platform === 'win32' ? cmd.replace(/^(cotutor|npm)(?= |$)/, '$1.cmd') : cmd;
7
11
  }
8
12
  const PLAIN = /^(\d+)\.(\d+)\.(\d+)$/;
9
13
  /** 正式版的版本号。预发布不算;从网上拿来的还要显示给家长,其余一律不认 */
@@ -219,6 +219,19 @@ ${BRAND_CSS}
219
219
  }
220
220
  .upd { position:relative; display:flex; flex-direction:column; gap:6px; margin:0 0 12px; padding:12px 14px; border-radius:14px; background:var(--card); box-shadow:0 0 0 1px var(--line); font-size:15px; }
221
221
  .upd.bad { box-shadow:0 0 0 1px var(--accent); }
222
+ .setup { position:relative; display:flex; flex-direction:column; gap:2px; margin:0 0 12px; padding:12px 14px; border-radius:14px; background:var(--card); box-shadow:0 0 0 1px var(--line); font-size:15px; }
223
+ .setup > b { padding-right:64px; }
224
+ .setup .sp { display:flex; gap:10px; padding:8px 0; border-top:1px solid var(--line); }
225
+ .setup .sp:first-of-type { border-top:0; }
226
+ .setup .mk { flex:none; width:20px; text-align:center; color:var(--dim); }
227
+ .setup .sp.on .mk { color:var(--accent); }
228
+ .setup .sp.on b { font-weight:500; color:var(--dim); }
229
+ .setup .tx { display:flex; flex-direction:column; gap:4px; min-width:0; flex:1; }
230
+ .setup small { color:var(--dim); font-size:14px; line-height:1.5; }
231
+ .setup pre { margin:2px 0 0; padding:10px 12px; border-radius:10px; background:var(--paper); font:13px/1.6 ui-monospace,Menlo,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; user-select:all; -webkit-user-select:all; }
232
+ .setup .gr { display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; }
233
+ .setup .gr button { min-height:44px; padding:0 12px; border:0; border-radius:10px; background:var(--paper); box-shadow:0 0 0 1px var(--line); color:inherit; font-size:15px; cursor:pointer; }
234
+ .setup .hide { position:absolute; top:6px; right:6px; min-height:44px; padding:0 10px; border:0; background:transparent; color:var(--accent); font-size:15px; cursor:pointer; }
222
235
  .upd ul { margin:0; padding-left:18px; font-size:14px; }
223
236
  .upd .sub { color:var(--dim); font-size:14px; padding-right:64px; }
224
237
  .upd pre { margin:2px 0 0; padding:10px 12px; border-radius:10px; background:var(--paper); font:13px/1.6 ui-monospace,Menlo,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; user-select:all; -webkit-user-select:all; }
@@ -227,6 +240,7 @@ ${BRAND_CSS}
227
240
  <div class="wrap" id="main">
228
241
  <div class="top"><span class="ttl"><span id="ct-mark" class="ct-draw ct-hover">${titleLogo(true)}</span><b id="title">家长</b></span><nav class="seg"><a class="on" href="/parent/home">首页</a><a href="/parent">记录</a><a href="/parent/settings">设置</a></nav><small id="pub"></small><a class="devl" href="/dev" target="_blank" title="工作台:细调老师文件、activity(新窗口)">工作台<svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 2h6v6"/><path d="M10 2 3 9"/></svg></a></div>
229
242
  <div id="troublebar"></div>
243
+ <div id="setupbar"></div>
230
244
  <div id="updatebar"></div>
231
245
  <div id="qrtop"></div>
232
246
  <div class="main">
@@ -374,6 +388,32 @@ ${pageDomJs(3200)}
374
388
 
375
389
  // 有新版本(服务每天问一次 npm,machine/update.ts):只在家长端说,孩子端不说;点「知道了」这个版本不再提,读不了 localStorage 就每次都说
376
390
  const UPGRADE_STEPS = ${JSON.stringify(upgradeSteps())};
391
+ // 还差几步(parent-setup.ts):第一次打开家长端,告诉家长接下来做什么。必需的都好了就只剩建议,「知道了」以后不再提
392
+ const showSetup = async () => {
393
+ const box = $('#setupbar');
394
+ const d = await api('GET', '/api/parent/setup').catch(() => null);
395
+ if (!d) { box.replaceChildren(); return; }
396
+ let hidden = false;
397
+ try { hidden = localStorage.getItem('cotutor.setupHidden') === '1'; } catch {}
398
+ const must = d.steps.filter((s) => s.required);
399
+ const left = must.filter((s) => !s.done);
400
+ const extra = d.steps.filter((s) => !s.required && !s.done);
401
+ if (!left.length && (hidden || !extra.length)) { box.replaceChildren(); return; }
402
+ const pickGrade = async (g) => {
403
+ try { await api('POST', '/api/parent/setup/grade', { grade: g }); toast('记下了:' + g + ' 年级'); } catch (e) { toast(e.message); }
404
+ showSetup();
405
+ };
406
+ const row = (s) => h('div', { class: 'sp' + (s.done ? ' on' : '') },
407
+ h('span', { class: 'mk' }, s.done ? '✓' : '○'),
408
+ h('span', { class: 'tx' }, h('b', {}, s.title),
409
+ s.done ? null : h('small', {}, s.detail, s.link ? ' ' : null, s.link ? h('a', { class: 'lnk', href: s.link.href, target: '_blank', rel: 'noopener' }, s.link.text) : null),
410
+ !s.done && s.commands ? h('pre', {}, s.commands.map((c) => h('div', {}, c))) : null,
411
+ !s.done && s.grades ? h('span', { class: 'gr' }, [1, 2, 3, 4, 5, 6].map((g) => h('button', { type: 'button', on: { click: () => pickGrade(g) } }, g + ' 年级'))) : null));
412
+ box.replaceChildren(h('div', { class: 'setup' },
413
+ h('b', {}, left.length ? '还差 ' + left.length + ' 步,孩子就能用了' : '都好了,还有几件可以做的'),
414
+ ...(left.length ? must.concat(extra) : extra).map(row),
415
+ left.length ? null : h('button', { type: 'button', class: 'hide', on: { click: () => { try { localStorage.setItem('cotutor.setupHidden', '1'); } catch {} box.replaceChildren(); } } }, '知道了')));
416
+ };
377
417
  // 跑起来以后百炼出的问题(欠费、key 失效、连不上,machine/bailian.ts):配好了不等于用得了。试连通了就没了
378
418
  const showTrouble = (hh) => {
379
419
  const box = $('#troublebar');
@@ -406,6 +446,7 @@ ${pageDomJs(3200)}
406
446
  };
407
447
  const load = async () => {
408
448
  showUpdate();
449
+ showSetup();
409
450
  try { D = await api('GET', '/api/parent/home'); } catch (e) { toast('读不到:' + e.message); return; }
410
451
  BL.st = await api('GET', '/api/parent/bailian').catch(() => null);
411
452
  QR.d = await api('GET', '/api/parent/qr').catch(() => null);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 家长端「设置」(/parent/settings):设一次就不常动的。现在只有一项——孩子的平板上,按住说话那一栏放哪边(cotutor.json 的 kid.hand)。
2
+ * 家长端「设置」(/parent/settings):设一次就不常动的。备份和诊断信息也放这(parent-setup.ts)。孩子的平板上,按住说话那一栏放哪边(cotutor.json 的 kid.hand)。
3
3
  * 两张示意图(不是截图,页面现画的 SVG):横着的平板,窄的一栏是老师头像 + 按住说话,宽的一边是写字的白纸;点一张就存。
4
4
  * 孩子端首页的接口带着它(/api/kid/home 的 hand),孩子下次进老师页就换过来;画板那一列工具跟着一起挪。
5
5
  * 零依赖内联脚本,只走 /api/parent/settings(GET 读,PATCH {kid: {hand}} 改)。
@@ -32,6 +32,10 @@ ${BRAND_CSS}
32
32
  .seg > * { flex:1; height:36px; border-radius:9px; font-size:15px; color:var(--dim); display:flex; align-items:center; justify-content:center; text-decoration:none; }
33
33
  .seg > .on { background:#fff; color:var(--ink); font-weight:600; box-shadow:0 1px 2px rgba(0,0,0,.08); }
34
34
  .gh { font-size:13px; color:var(--mute); padding:8px 16px 6px; }
35
+ .acts { display:flex; flex-wrap:wrap; gap:8px; padding:0 3px 12px; }
36
+ .act { display:inline-flex; align-items:center; min-height:44px; padding:0 14px; border:0; border-radius:10px; background:var(--paper, transparent); box-shadow:0 0 0 1px var(--line); color:inherit; font-size:15px; text-decoration:none; cursor:pointer; }
37
+ .act[disabled] { opacity:.5; }
38
+ #rep-text { margin:0 3px 12px; padding:10px 12px; max-height:320px; overflow:auto; border-radius:10px; box-shadow:0 0 0 1px var(--line); font:12px/1.55 ui-monospace,Menlo,Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; }
35
39
  .grp { background:var(--card); border-radius:14px; padding:14px; display:flex; flex-direction:column; gap:12px; }
36
40
  .q { font-size:16px; font-weight:500; padding:0 2px; }
37
41
  .hands { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@@ -63,6 +67,22 @@ ${BRAND_CSS}
63
67
  <div class="note">平板横着用的时候,老师页上那一栏(老师的头像、按住说话)放在这边,另一边留给写字;画板上那一列工具也跟着放这边。孩子下次进老师页就换过来。</div>
64
68
  </div>
65
69
  </div>
70
+ <div>
71
+ <div class="gh">备份</div>
72
+ <div class="grp">
73
+ <div class="note" id="bk-note">读取中……</div>
74
+ <div class="acts"><button type="button" class="act" id="bk-go">现在备份一份</button></div>
75
+ </div>
76
+ </div>
77
+ <div>
78
+ <div class="gh">遇到问题</div>
79
+ <div class="grp">
80
+ <div class="note">生成一份诊断信息,发给开发者或帮你的 AI 助手,不用来回问。里面没有孩子的对话、照片、录音、档案和 key,名字和 IP 已经抹掉。不会自动发出去,你可以先看一遍。</div>
81
+ <div class="acts"><button type="button" class="act" id="rep-go">生成诊断信息</button></div>
82
+ <pre id="rep-text" hidden></pre>
83
+ <div class="acts" id="rep-acts" hidden><button type="button" class="act" id="rep-copy">复制</button><a class="act" href="https://github.com/TarsLab/cotutor/issues/new?template=bug.yml" target="_blank" rel="noopener">到 GitHub 反馈</a></div>
84
+ </div>
85
+ </div>
66
86
  ${BRAND_FOOTER}
67
87
  </div>
68
88
  <div id="toast"></div>
@@ -125,8 +145,52 @@ ${pageDomJs(3200)}
125
145
  if (S) { document.title = S.title + ' · 家长 · 设置'; $('#title').textContent = S.title; }
126
146
  draw();
127
147
  };
148
+ // 备份:只读和「现在备一份」;恢复要停服务,在终端里做,这里说怎么做
149
+ const bkLoad = async () => {
150
+ const d = await api('GET', '/api/parent/backups').catch(() => null);
151
+ const note = $('#bk-note');
152
+ if (!d) { note.textContent = '读不到备份的情况'; return; }
153
+ const last = d.list[0];
154
+ const mb = (n) => (n / 1024 / 1024).toFixed(1) + ' MB';
155
+ note.textContent = (last ? '最近一份 ' + new Date(last.at).toLocaleString() + ',' + last.files + ' 个文件,' + mb(last.bytes) + ';一共留着 ' + d.list.length + ' 份。' : '还没有备份。') + '放在 ' + d.dir + '。服务开着时每天自动备一份,升级前也备一份;录音占地方,不在备份里。要恢复:在这台电脑的终端里跑 ' + d.commands.list + ' 看有哪些,再 ' + d.commands.restore + '。';
156
+ };
157
+ $('#bk-go').addEventListener('click', async () => {
158
+ const b = $('#bk-go');
159
+ b.disabled = true;
160
+ try { await api('POST', '/api/parent/backups', {}); toast('备份好了'); } catch (e) { toast('没备份成:' + e.message); }
161
+ b.disabled = false;
162
+ bkLoad();
163
+ });
164
+ // 诊断信息:先给人看,复制是人点的;不自动发
165
+ $('#rep-go').addEventListener('click', async () => {
166
+ const b = $('#rep-go');
167
+ b.disabled = true;
168
+ try {
169
+ const r = await api('GET', '/api/parent/report');
170
+ $('#rep-text').textContent = r.text;
171
+ $('#rep-text').hidden = false;
172
+ $('#rep-acts').hidden = false;
173
+ } catch (e) { toast('没生成成:' + e.message); }
174
+ b.disabled = false;
175
+ });
176
+ $('#rep-copy').addEventListener('click', async () => {
177
+ const text = $('#rep-text').textContent;
178
+ try {
179
+ await navigator.clipboard.writeText(text);
180
+ toast('复制好了');
181
+ } catch {
182
+ // 不是 https 的地址没有剪贴板接口:选中,让人自己复制
183
+ const range = document.createRange();
184
+ range.selectNodeContents($('#rep-text'));
185
+ const sel = getSelection();
186
+ sel.removeAllRanges();
187
+ sel.addRange(range);
188
+ toast('已选中,按复制');
189
+ }
190
+ });
128
191
  draw();
129
192
  load();
193
+ bkLoad();
130
194
  })();
131
195
  </script>
132
196
  </body></html>
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "cotutor",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "releaseNotes": [
5
- "百炼欠费、key 失效或连不上时,图标和家长端会说清楚,孩子那边会请爸爸妈妈来看,不再一直转",
6
- "升级只要一条命令 cotutor update,几个孩子一次升好,孩子在通话时不动",
7
- "用 Homebrew 装 Node 的电脑:Node 升级后服务和图标不会再起不来",
8
- "有新版本时能直接看到这次改了什么"
5
+ "自动备份:服务开着时每天一份,升级前一份,可以恢复;录音占地方,不在备份里",
6
+ "家长端首页多了「还差几步」,第一次打开就知道接下来做什么;选一下年级老师就知道孩子上几年级",
7
+ "遇到问题时,家长端「设置」能生成诊断信息,不含孩子的对话和 key,先看再发",
8
+ "`cotutor init --grade` 自动算好学期,`cotutor doctor --fix` 一次修掉能自动修的"
9
9
  ],
10
10
  "description": "AI tutors that co-teach with parents at home: real-time voice, an interactive whiteboard, and learning memory in your own Obsidian vault. Self-hosted; built for China's elementary curriculum.",
11
11
  "type": "module",