cotutor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +9 -7
  2. package/NOTICE +3 -3
  3. package/README.md +36 -13
  4. package/avatars/chinese.webp +0 -0
  5. package/avatars/english.webp +0 -0
  6. package/avatars/math.webp +0 -0
  7. package/bin/cotutor.js +13 -3
  8. package/cards/photo/card.css +10 -3
  9. package/cards/photo/card.md +2 -2
  10. package/cards/tutor/card.css +13 -0
  11. package/cards/tutor/card.md +2 -1
  12. package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
  13. package/dist/app/activity-eval.js +3 -2
  14. package/dist/app/cli/activities.d.ts +8 -1
  15. package/dist/app/cli/activities.js +42 -4
  16. package/dist/app/cli/cert.d.ts +5 -0
  17. package/dist/app/cli/cert.js +20 -7
  18. package/dist/app/cli/chalk/browser.d.ts +2 -2
  19. package/dist/app/cli/chalk/browser.js +22 -7
  20. package/dist/app/cli/chalk/main.js +3 -2
  21. package/dist/app/cli/doctor.d.ts +6 -5
  22. package/dist/app/cli/doctor.js +107 -171
  23. package/dist/app/cli/firewall.d.ts +24 -0
  24. package/dist/app/cli/firewall.js +79 -0
  25. package/dist/app/cli/init.js +8 -5
  26. package/dist/app/cli/main.js +347 -24
  27. package/dist/app/cli/migrate.d.ts +2 -11
  28. package/dist/app/cli/migrate.js +1 -59
  29. package/dist/app/cli/port-holder.d.ts +16 -0
  30. package/dist/app/cli/port-holder.js +34 -0
  31. package/dist/app/cli/port.d.ts +37 -0
  32. package/dist/app/cli/port.js +116 -0
  33. package/dist/app/cli/sandbox.d.ts +25 -0
  34. package/dist/app/cli/sandbox.js +166 -0
  35. package/dist/app/cli/serve-log.d.ts +13 -0
  36. package/dist/app/cli/serve-log.js +90 -0
  37. package/dist/app/cli/serve.d.ts +18 -4
  38. package/dist/app/cli/serve.js +78 -8
  39. package/dist/app/cli/service-windows.d.ts +50 -0
  40. package/dist/app/cli/service-windows.js +117 -0
  41. package/dist/app/cli/service.d.ts +138 -0
  42. package/dist/app/cli/service.js +429 -0
  43. package/dist/app/cli/skeleton.d.ts +1 -1
  44. package/dist/app/cli/skeleton.js +13 -13
  45. package/dist/app/cli/skills.d.ts +5 -1
  46. package/dist/app/cli/skills.js +14 -4
  47. package/dist/app/cli/tray-icons.d.ts +11 -0
  48. package/dist/app/cli/tray-icons.js +87 -0
  49. package/dist/app/cli/tray-ids.d.ts +6 -0
  50. package/dist/app/cli/tray-ids.js +6 -0
  51. package/dist/app/cli/tray-mac.d.ts +8 -0
  52. package/dist/app/cli/tray-mac.js +140 -0
  53. package/dist/app/cli/tray-run.d.ts +1 -0
  54. package/dist/app/cli/tray-run.js +33 -0
  55. package/dist/app/cli/tray-windows.d.ts +5 -0
  56. package/dist/app/cli/tray-windows.js +166 -0
  57. package/dist/app/cli/tray.d.ts +205 -0
  58. package/dist/app/cli/tray.js +618 -0
  59. package/dist/app/cli/usage.d.ts +1 -1
  60. package/dist/app/cli/usage.js +11 -6
  61. package/dist/app/dictation/route.js +3 -2
  62. package/dist/app/http/app.d.ts +5 -10
  63. package/dist/app/http/app.js +45 -98
  64. package/dist/app/http/avatars.d.ts +17 -0
  65. package/dist/app/http/avatars.js +35 -0
  66. package/dist/app/http/dev-tutor-files.js +1 -1
  67. package/dist/app/http/parent-home.d.ts +14 -0
  68. package/dist/app/http/parent-home.js +69 -6
  69. package/dist/app/http/parent-look.d.ts +2 -0
  70. package/dist/app/http/parent-look.js +151 -0
  71. package/dist/app/http/raw-view.js +3 -3
  72. package/dist/app/http/voice-preview.d.ts +4 -0
  73. package/dist/app/http/voice-preview.js +34 -0
  74. package/dist/app/machine/bailian.d.ts +0 -2
  75. package/dist/app/machine/bailian.js +5 -6
  76. package/dist/app/mock/mock-live.js +1 -1
  77. package/dist/app/mock/mock.d.ts +4 -2
  78. package/dist/app/mock/mock.js +20 -12
  79. package/dist/app/replay.js +4 -4
  80. package/dist/app/skills/home-doc.js +1 -1
  81. package/dist/app/workspace.d.ts +9 -4
  82. package/dist/app/workspace.js +33 -10
  83. package/dist/app/ws/route.d.ts +8 -2
  84. package/dist/app/ws/route.js +7 -3
  85. package/dist/board/parse.d.ts +0 -2
  86. package/dist/board/parse.js +0 -6
  87. package/dist/board/presentation.d.ts +9 -15
  88. package/dist/board/presentation.js +8 -10
  89. package/dist/brain/api-tools.js +2 -1
  90. package/dist/brain/api.d.ts +12 -17
  91. package/dist/brain/api.js +16 -33
  92. package/dist/brain/index.d.ts +23 -71
  93. package/dist/brain/index.js +6 -69
  94. package/dist/brain/log.d.ts +5 -0
  95. package/dist/brain/log.js +22 -0
  96. package/dist/brain/runtime.d.ts +30 -0
  97. package/dist/brain/runtime.js +23 -0
  98. package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
  99. package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
  100. package/dist/brain/stream.d.ts +5 -5
  101. package/dist/brain/stream.js +7 -7
  102. package/dist/brain/transcript.d.ts +3 -3
  103. package/dist/brain/transcript.js +6 -6
  104. package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
  105. package/dist/brain/types.js +14 -0
  106. package/dist/chalk/glyphs/skeleton.d.ts +3 -3
  107. package/dist/chalk/glyphs/skeleton.js +1 -1
  108. package/dist/lib/index.d.ts +1 -1
  109. package/dist/lib/index.js +1 -1
  110. package/dist/memory/diary.js +3 -3
  111. package/dist/realtime/bailian.d.ts +1 -1
  112. package/dist/realtime/bailian.js +3 -2
  113. package/dist/realtime/preview.js +1 -1
  114. package/dist/realtime/voices.d.ts +2 -0
  115. package/dist/realtime/voices.js +57 -57
  116. package/dist/runtime/activities.d.ts +13 -1
  117. package/dist/runtime/activities.js +28 -2
  118. package/dist/runtime/activity-state.js +2 -2
  119. package/dist/runtime/agent-file.d.ts +3 -3
  120. package/dist/runtime/agent-file.js +3 -3
  121. package/dist/runtime/context/tutor-rules.d.ts +2 -2
  122. package/dist/runtime/context/tutor-rules.js +3 -3
  123. package/dist/runtime/home/home.d.ts +12 -1
  124. package/dist/runtime/home/home.js +8 -1
  125. package/dist/runtime/home/store.d.ts +1 -1
  126. package/dist/runtime/home/store.js +36 -21
  127. package/dist/runtime/journal/mix.d.ts +15 -6
  128. package/dist/runtime/journal/mix.js +60 -19
  129. package/dist/runtime/journal/recorder.d.ts +58 -13
  130. package/dist/runtime/journal/recorder.js +106 -16
  131. package/dist/runtime/journal/store.d.ts +4 -4
  132. package/dist/runtime/journal/store.js +6 -6
  133. package/dist/runtime/kid-input.js +2 -3
  134. package/dist/runtime/live/call.d.ts +12 -0
  135. package/dist/runtime/live/call.js +51 -18
  136. package/dist/runtime/live/turns.js +1 -1
  137. package/dist/runtime/runner.d.ts +9 -55
  138. package/dist/runtime/runner.js +52 -173
  139. package/dist/scenes/paint/painter.js +4 -3
  140. package/dist/schema/config.d.ts +43 -22
  141. package/dist/schema/config.js +45 -47
  142. package/dist/schema/conversation.d.ts +6 -0
  143. package/dist/schema/home.d.ts +11 -1
  144. package/dist/schema/home.js +7 -2
  145. package/dist/schema/workspace.d.ts +27 -0
  146. package/dist/schema/workspace.js +28 -0
  147. package/dist/shared/default-port.d.ts +2 -0
  148. package/dist/shared/default-port.js +2 -0
  149. package/dist/shared/qianwen.d.ts +14 -10
  150. package/dist/shared/qianwen.js +26 -22
  151. package/dist/shared/site-url.d.ts +2 -0
  152. package/dist/shared/site-url.js +5 -0
  153. package/dist/ui/dev/dev-page.js +25 -252
  154. package/dist/ui/dev/dev-theme.d.ts +1 -1
  155. package/dist/ui/dev/dev-theme.js +1 -1
  156. package/dist/ui/dev/voice-gallery-page.js +1 -1
  157. package/dist/ui/kid/board-mark.d.ts +2 -2
  158. package/dist/ui/kid/board-mark.js +2 -2
  159. package/dist/ui/kid/home-tiles.d.ts +14 -0
  160. package/dist/ui/kid/home-tiles.js +44 -0
  161. package/dist/ui/kid/kid-page.js +68 -103
  162. package/dist/ui/kid/voice-ui.d.ts +1 -1
  163. package/dist/ui/kid/voice-ui.js +1 -1
  164. package/dist/ui/kid/word-emoji.js +5 -3
  165. package/dist/ui/parent/parent-home-page.js +315 -7
  166. package/dist/ui/parent/parent-page.js +6 -2
  167. package/dist/ui/parent/parent-settings-page.d.ts +1 -0
  168. package/dist/ui/parent/parent-settings-page.js +133 -0
  169. package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
  170. package/dist/ui/{dev → parent}/qr-page.js +19 -14
  171. package/dist/ui/shared/brand.d.ts +3 -1
  172. package/dist/ui/shared/brand.js +3 -1
  173. package/grades/grade-2/README.md +1 -1
  174. package/grades/grade-2/VALIDATION.md +1 -1
  175. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
  176. package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
  177. package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
  178. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
  179. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
  180. package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
  181. package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
  182. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
  183. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
  184. package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
  185. package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
  186. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
  187. package/package.json +2 -1
  188. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
  189. package/skills/cotutor-board/references/photo.md +2 -2
  190. package/skills/cotutor-home/SKILL.md +1 -0
  191. package/skills/cotutor-home/references/tutor.md +2 -1
  192. package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
  193. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
  194. package/skills/cotutor-tutor/SKILL.md +5 -13
  195. package/dist/brain/cli/adapters.d.ts +0 -60
  196. package/dist/brain/cli/adapters.js +0 -91
  197. package/dist/brain/cli/qwen.d.ts +0 -11
  198. package/dist/brain/cli/qwen.js +0 -141
  199. package/dist/brain/cli/run-plan.d.ts +0 -59
  200. package/dist/brain/cli/run-plan.js +0 -71
  201. package/dist/brain/cli/run.d.ts +0 -35
  202. package/dist/brain/cli/run.js +0 -256
  203. package/dist/brain/cli/types.js +0 -3
  204. package/dist/brain/cli/warm.d.ts +0 -53
  205. package/dist/brain/cli/warm.js +0 -103
@@ -0,0 +1,618 @@
1
+ /**
2
+ * 托盘图标(macOS 菜单栏、Windows 任务栏;Linux 没做,GNOME 缺省不显示托盘):家长不开终端也看得到服务好不好、点得开家长端。
3
+ * 三层:
4
+ * node 进程(cotutor tray run,tray-run.ts 是它的轻入口):隔几秒问每个后台服务的 /api/health,判断状态,决定显示什么、要不要通知;这里全是能测的 TS。
5
+ * 显示脚本(tray-mac.ts 的 JXA、tray-windows.ts 的 PowerShell,都是系统自带、不用签名):只管画和回报点击,不判断。
6
+ * 登录项:macOS 一个 LaunchAgent,Windows 一条 Run 项 + wscript 隐藏启动器;service install 时一起装(--no-tray 不装,cotutor tray off 关掉并记住)。
7
+ * 为什么不放在 serve 里:服务挂了图标也没了,就说不出「没在跑」;多个孩子时一台电脑也只该有一个图标。
8
+ * 为什么不用 npm 的托盘库、Electron:见 docs/wip;systray2 只有 x86 的 Mac 二进制且没签名,Electron 要多下一百多 MB。
9
+ * 状态:正常只有 logo(和菜单栏别的图标一样单色);有事才出状态点,橙 = 要处理,红 = 没在跑(logo 同时变淡)。正常 → 有事、→ 没在跑时通知一次,恢复不通知。
10
+ * 「退出 cotutor」= 停掉所有后台服务并退出图标,下次登录自己回来;正开着电话时先问一声。
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ import { appendFileSync, existsSync, renameSync, statSync } from 'node:fs';
14
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { createInterface } from 'node:readline';
17
+ import { DEFAULT_PORT } from "../../shared/default-port.js";
18
+ import { UsageError } from "../../shared/errors.js";
19
+ import { siteUrl } from "../../shared/site-url.js";
20
+ import { portHolder } from "./port-holder.js";
21
+ import { RUN_KEY, STARTUP_APPROVED_KEY, encodeLauncher, parseProcs, psArgs, runValue, startupDisabled, winCommandLine } from "./service-windows.js";
22
+ import { binPath, controlService, listServices, parseLaunchctlPrint, probeHealth, wscriptPath, xml } from "./service.js";
23
+ import { macTrayIcons, windowsTrayIcons } from "./tray-icons.js";
24
+ import { HELPER_NO_SESSION, TRAY_AUMID, TRAY_LABEL, TRAY_RUN_VALUE } from "./tray-ids.js";
25
+ import { MAC_TRAY_SCRIPT } from "./tray-mac.js";
26
+ import { WINDOWS_TRAY_SCRIPT, findTrayScript, trayLauncher } from "./tray-windows.js";
27
+ export { HELPER_NO_SESSION, TRAY_AUMID, TRAY_LABEL, TRAY_RUN_VALUE };
28
+ /** 多久问一次 */
29
+ export const TRAY_POLL_MS = 5_000;
30
+ /** 连着几次问不到才算没在跑:重起一下、网络抖一下不报 */
31
+ export const TRAY_STRIKES = 2;
32
+ /** 图标刚起来时服务可能还没起来:这么久里问不到算「正在启动」(Windows 登录后约一分钟才跑 Run 项) */
33
+ export const TRAY_START_GRACE_MS = 90_000;
34
+ /** 点了启动、重启以后等多久 */
35
+ export const TRAY_RESTART_GRACE_MS = 30_000;
36
+ /** 两次问之间隔太久 = 刚睡醒:网络还没回来,先别报 */
37
+ export const TRAY_WAKE_GAP_MS = 30_000;
38
+ export const TRAY_WAKE_GRACE_MS = 30_000;
39
+ const HOLDER_EVERY_MS = 60_000;
40
+ const HELPER_RESTART_MS = 10_000;
41
+ const LOG_MAX = 1024 * 1024;
42
+ const MAC_FULL_DISK = 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles';
43
+ /** 跑了多久,给家长看 */
44
+ export function uptimeText(startedMs, now) {
45
+ const min = Math.max(0, (now - startedMs) / 60_000);
46
+ if (min < 60)
47
+ return '不到 1 小时';
48
+ if (min < 48 * 60)
49
+ return `${Math.floor(min / 60)} 小时`;
50
+ return `${Math.floor(min / 1440)} 天`;
51
+ }
52
+ /** 服务在跑,但有事要家长处理:先后就是轻重,菜单上只说第一件 */
53
+ export function issuesOf(p, o) {
54
+ const h = p.health;
55
+ if (!h)
56
+ return [];
57
+ const url = siteUrl(h.scheme ?? 'https', 'localhost', p.port, '/parent/home');
58
+ const out = [];
59
+ if (h.configError)
60
+ out.push({ text: 'cotutor.json 写坏了:改的没生效', fix: { kind: 'open', target: p.root } });
61
+ if (h.vault === 'denied')
62
+ out.push(o.platform === 'darwin' ? { text: '读不到 vault:要给 node 开磁盘权限', fix: { kind: 'open', target: MAC_FULL_DISK } } : { text: '读不到 vault:看看这个目录的权限', fix: { kind: 'open', target: p.root } });
63
+ else if (h.vault === 'missing')
64
+ out.push({ text: 'vault 不在了:看看 cotutor.json 的 paths.vault', fix: { kind: 'open', target: p.root } });
65
+ if (h.bailian === false)
66
+ out.push({ text: '还没填百炼:老师们到不了', fix: { kind: 'open', target: url } });
67
+ if (h.version && h.version !== o.version)
68
+ out.push({ text: `在跑的是 ${h.version},装的是 ${o.version}:重启一下`, fix: { kind: 'restart' } });
69
+ return out;
70
+ }
71
+ const rank = (l) => (l === 'bad' ? 2 : l === 'warn' ? 1 : 0);
72
+ /** 记着每个服务上一轮的样子,算这一轮的样子和要不要通知;不碰任何系统,测试直接喂 */
73
+ export class TrayModel {
74
+ mem = new Map();
75
+ last = 0;
76
+ o;
77
+ constructor(o) { this.o = o; }
78
+ /** 刚点了启动、重启:这一阵问不到是正常的 */
79
+ expect(name, now) {
80
+ const m = this.mem.get(name);
81
+ if (!m)
82
+ return;
83
+ m.row = null;
84
+ m.misses = 0;
85
+ m.graceUntil = now + TRAY_RESTART_GRACE_MS;
86
+ }
87
+ update(probes, now) {
88
+ const woke = this.last > 0 && now - this.last > TRAY_WAKE_GAP_MS;
89
+ this.last = now;
90
+ const rows = [];
91
+ const notes = [];
92
+ const multi = probes.length > 1;
93
+ for (const p of probes) {
94
+ let m = this.mem.get(p.name);
95
+ if (!m) {
96
+ m = { row: null, misses: 0, graceUntil: now + TRAY_START_GRACE_MS };
97
+ this.mem.set(p.name, m);
98
+ }
99
+ if (woke)
100
+ m.graceUntil = Math.max(m.graceUntil, now + TRAY_WAKE_GRACE_MS);
101
+ let row;
102
+ if (p.health) {
103
+ m.misses = 0;
104
+ m.graceUntil = 0;
105
+ row = this.healthy(p, now);
106
+ }
107
+ else {
108
+ m.misses++;
109
+ if (now < m.graceUntil)
110
+ row = m.row ?? this.placeholder(p, 'starting', '');
111
+ else if (m.misses < TRAY_STRIKES && m.row && m.row.level !== 'starting')
112
+ row = m.row;
113
+ else
114
+ row = this.placeholder(p, 'bad', p.holder ? `端口 ${p.port} 被占着:${p.holder} 在用` : '孩子的平板现在打不开');
115
+ }
116
+ const prev = m.level;
117
+ // 第一次看到就是「要处理」不通知(每次登录都弹太烦,菜单里看得到);变差才通知
118
+ if (rank(row.level) > rank(prev) && !(row.level === 'warn' && (prev === undefined || prev === 'starting')))
119
+ notes.push(noteFor(row, multi, this.o.platform));
120
+ m.level = row.level;
121
+ m.row = row;
122
+ rows.push(row);
123
+ }
124
+ for (const name of [...this.mem.keys()])
125
+ if (!probes.some((p) => p.name === name))
126
+ this.mem.delete(name);
127
+ return { rows, notes };
128
+ }
129
+ healthy(p, now) {
130
+ const h = p.health;
131
+ const issues = issuesOf(p, this.o);
132
+ const started = Date.parse(h.started);
133
+ const running = h.mode === 'foreground' ? '在终端里跑,关了终端就停' : `已运行 ${uptimeText(Number.isNaN(started) ? now : started, now)}`;
134
+ return {
135
+ name: p.name,
136
+ kid: p.kid,
137
+ logDir: p.logDir,
138
+ level: issues.length ? 'warn' : 'ok',
139
+ url: siteUrl(h.scheme ?? 'https', 'localhost', p.port, '/parent/home'),
140
+ detail: issues.length ? issues[0].text : `${running} · 版本 ${h.version ?? '?'}`,
141
+ issues: issues.length,
142
+ fix: issues[0]?.fix ?? null,
143
+ calls: h.calls ?? 0,
144
+ };
145
+ }
146
+ placeholder(p, level, detail) {
147
+ return { name: p.name, kid: p.kid, logDir: p.logDir, level, url: null, detail, issues: 0, fix: null, calls: 0 };
148
+ }
149
+ }
150
+ const titleOf = (r, multi) => {
151
+ if (!multi)
152
+ return r.level === 'ok' ? 'cotutor 正常' : r.level === 'warn' ? `有 ${r.issues} 件事要处理` : r.level === 'bad' ? '后台服务没在跑' : 'cotutor 正在启动';
153
+ return `${r.kid} · ${r.level === 'ok' ? '正常' : r.level === 'warn' ? `有 ${r.issues} 件事要处理` : r.level === 'bad' ? '没在跑' : '正在启动'}`;
154
+ };
155
+ function noteFor(r, multi, platform) {
156
+ // 点通知回不到这里(Mac 的来源是系统的脚本程序;Windows 的 PowerShell 5.1 接不了 WinRT 的事件),只能请家长去点图标
157
+ const hint = platform === 'darwin' ? '点菜单栏的 cotutor 图标看看' : '点任务栏的 cotutor 图标看看';
158
+ const who = multi ? r.kid : 'cotutor ';
159
+ if (r.level === 'bad')
160
+ return { type: 'notify', title: `${who}${multi ? '的' : ''}后台服务没在跑`, body: `孩子的平板现在打不开,${hint}` };
161
+ return { type: 'notify', title: `${who}有 ${r.issues} 件事要处理`, body: `${r.detail},${hint}` };
162
+ }
163
+ /** 这一轮显示什么 */
164
+ export function trayShow(rows) {
165
+ const multi = rows.length > 1;
166
+ const worst = rows.reduce((a, r) => (!a || rank(r.level) > rank(a.level) ? r : a), null);
167
+ const level = (worst?.level ?? 'ok');
168
+ const menu = [];
169
+ for (const r of rows) {
170
+ menu.push({ kind: 'info', title: titleOf(r, multi), ...(r.level === 'starting' ? {} : { level: r.level }) });
171
+ if (r.detail && (!multi || r.level !== 'ok'))
172
+ menu.push({ kind: 'sub', title: r.detail });
173
+ if (!multi)
174
+ menu.push({ kind: 'sep' });
175
+ const act = (what, title, primary = false) => { menu.push({ kind: 'action', id: `${what}:${r.name}`, title, ...(primary ? { primary } : {}) }); };
176
+ if (r.level === 'ok' || r.level === 'warn') {
177
+ if (r.level === 'warn')
178
+ act('fix', '看看怎么修', true);
179
+ act('open', '打开家长端');
180
+ if (!multi)
181
+ menu.push({ kind: 'sep' });
182
+ act('restart', '重启服务');
183
+ act('logs', '看日志');
184
+ }
185
+ else if (r.level === 'bad') {
186
+ act('start', '启动服务', true);
187
+ act('logs', '看日志');
188
+ }
189
+ else
190
+ act('logs', '看日志');
191
+ menu.push({ kind: 'sep' });
192
+ }
193
+ menu.push({ kind: 'action', id: 'quit', title: '退出 cotutor' });
194
+ return { type: 'show', icon: level === 'starting' ? 'ok' : level, tip: worst ? titleOf(worst, multi) : 'cotutor', menu };
195
+ }
196
+ export const QUIT_CONFIRM = {
197
+ id: 'quit',
198
+ title: '孩子正在和老师通话',
199
+ body: '现在退出会挂断这通电话,孩子的平板也打不开了,下次登录这台电脑后自动恢复。确定退出吗?',
200
+ yes: '退出',
201
+ no: '先不退出',
202
+ };
203
+ /** 主循环:问状态、发给显示脚本、接点击;都退了才返回退出码。0 = 不用再起(点了退出、没有服务了、没有桌面) */
204
+ export function runTray(d) {
205
+ return new Promise((resolve) => {
206
+ const model = new TrayModel({ platform: d.platform, version: d.version });
207
+ const pollMs = d.pollMs ?? TRAY_POLL_MS;
208
+ const holders = new Map();
209
+ let helper = null;
210
+ let shown = '';
211
+ let rows = [];
212
+ let services = [];
213
+ let stopping = false;
214
+ let ticking = false;
215
+ let again = false;
216
+ let timer = null;
217
+ let finished = false;
218
+ const finish = (code) => {
219
+ if (finished)
220
+ return;
221
+ finished = true;
222
+ stopping = true;
223
+ if (timer)
224
+ clearTimeout(timer);
225
+ resolve(code);
226
+ };
227
+ const quitAll = async () => {
228
+ stopping = true;
229
+ if (timer)
230
+ clearTimeout(timer);
231
+ for (const s of services) {
232
+ try {
233
+ await d.control(s.root, 'stop');
234
+ }
235
+ catch (err) {
236
+ d.note(`停 ${s.name} 没成:${String(err)}`);
237
+ }
238
+ }
239
+ d.note('退出:服务都停了');
240
+ if (helper) {
241
+ helper.send({ type: 'quit' });
242
+ helper.close();
243
+ }
244
+ // 显示脚本不退也不等它
245
+ setTimeout(() => finish(0), 3000).unref();
246
+ };
247
+ const control = (name, what) => {
248
+ const s = services.find((x) => x.name === name);
249
+ if (!s)
250
+ return;
251
+ model.expect(name, d.now());
252
+ void d.control(s.root, what).catch((err) => {
253
+ d.note(`${what} ${name} 没成:${String(err)}`);
254
+ helper?.send({ type: 'notify', title: 'cotutor 没起来', body: err instanceof UsageError ? err.message : '看看日志' });
255
+ }).finally(() => { void tick(); });
256
+ void tick();
257
+ };
258
+ const onClick = (id) => {
259
+ d.note(`点了 ${id}`);
260
+ if (id === 'quit') {
261
+ if (rows.some((r) => r.calls > 0))
262
+ helper?.send({ type: 'confirm', ...QUIT_CONFIRM });
263
+ else
264
+ void quitAll();
265
+ return;
266
+ }
267
+ const at = id.indexOf(':');
268
+ const what = id.slice(0, at);
269
+ const r = rows.find((x) => x.name === id.slice(at + 1));
270
+ if (!r)
271
+ return;
272
+ if (what === 'open' && r.url)
273
+ d.open(r.url);
274
+ else if (what === 'logs' && r.logDir)
275
+ d.open(r.logDir);
276
+ else if (what === 'start' || what === 'restart')
277
+ control(r.name, what);
278
+ else if (what === 'fix' && r.fix) {
279
+ if (r.fix.kind === 'restart')
280
+ control(r.name, 'restart');
281
+ else
282
+ d.open(r.fix.target);
283
+ }
284
+ };
285
+ const startHelper = () => {
286
+ helper = d.spawnHelper({
287
+ message(m) {
288
+ if (m.type === 'ready') {
289
+ shown = '';
290
+ void tick();
291
+ }
292
+ else if (m.type === 'click' && m.id)
293
+ onClick(m.id);
294
+ else if (m.type === 'confirm' && m.id === 'quit' && m.yes)
295
+ void quitAll();
296
+ else if (m.type === 'error')
297
+ d.note(`显示脚本:${m.message ?? ''}`);
298
+ },
299
+ exit(code) {
300
+ helper = null;
301
+ if (stopping) {
302
+ finish(0);
303
+ return;
304
+ }
305
+ if (code === HELPER_NO_SESSION) {
306
+ d.note('这个会话没有桌面,不显示图标');
307
+ finish(0);
308
+ return;
309
+ }
310
+ d.note(`显示脚本退了(${code ?? '?'}),隔一会儿再起`);
311
+ setTimeout(() => { if (!stopping)
312
+ startHelper(); }, d.helperRestartMs ?? HELPER_RESTART_MS).unref();
313
+ },
314
+ });
315
+ };
316
+ const tick = async () => {
317
+ if (stopping)
318
+ return;
319
+ if (ticking) {
320
+ again = true;
321
+ return;
322
+ }
323
+ ticking = true;
324
+ if (timer)
325
+ clearTimeout(timer);
326
+ try {
327
+ services = await d.list();
328
+ if (!services.length) {
329
+ // 服务都卸了:图标也没事可做
330
+ stopping = true;
331
+ d.note('没有后台服务了,退出');
332
+ if (helper) {
333
+ helper.send({ type: 'quit' });
334
+ helper.close();
335
+ }
336
+ setTimeout(() => finish(0), 3000).unref();
337
+ return;
338
+ }
339
+ const now = d.now();
340
+ const probes = [];
341
+ for (const s of services) {
342
+ const w = await d.inspect(s);
343
+ const health = await d.probe(w.port);
344
+ let holder = null;
345
+ if (!health) {
346
+ const c = holders.get(s.name);
347
+ if (c && now - c.at < HOLDER_EVERY_MS)
348
+ holder = c.who;
349
+ else {
350
+ holder = await d.holder(w.port);
351
+ holders.set(s.name, { at: now, who: holder });
352
+ }
353
+ }
354
+ else
355
+ holders.delete(s.name);
356
+ probes.push({ name: s.name, kid: w.kid, root: s.root, logDir: s.logDir, port: w.port, health, holder });
357
+ }
358
+ const r = model.update(probes, d.now());
359
+ rows = r.rows;
360
+ const show = trayShow(rows);
361
+ const text = JSON.stringify(show);
362
+ if (helper && text !== shown) {
363
+ helper.send(show);
364
+ shown = text;
365
+ d.note(`显示:${show.icon} ${show.tip}`);
366
+ }
367
+ for (const n of r.notes) {
368
+ helper?.send(n);
369
+ d.note(`通知:${n.title}`);
370
+ }
371
+ }
372
+ catch (err) {
373
+ d.note(`这一轮出错:${String(err)}`);
374
+ }
375
+ finally {
376
+ ticking = false;
377
+ if (!stopping) {
378
+ if (again) {
379
+ again = false;
380
+ void tick();
381
+ }
382
+ else
383
+ timer = setTimeout(() => { void tick(); }, pollMs);
384
+ }
385
+ }
386
+ };
387
+ startHelper();
388
+ });
389
+ }
390
+ // ---- 真的那一套:文件放哪、怎么起显示脚本、登录项 ----
391
+ export const traySupported = (platform) => platform === 'darwin' || platform === 'win32';
392
+ /** 显示脚本、图标、日志放哪 */
393
+ export function trayDir(host) {
394
+ if (host.platform === 'win32')
395
+ return join(host.env.LOCALAPPDATA || join(host.home, 'AppData', 'Local'), 'cotutor', 'tray');
396
+ return join(host.home, 'Library', 'Application Support', 'cotutor', 'tray');
397
+ }
398
+ const offMark = (dir) => join(dir, 'off');
399
+ const plistFile = (host) => join(host.home, 'Library', 'LaunchAgents', `${TRAY_LABEL}.plist`);
400
+ const launcherFile = (dir) => join(dir, 'start.js');
401
+ /** 图标改了画法就改这个数,已经写过的会重写 */
402
+ const ASSETS_VERSION = 1;
403
+ /** 写显示脚本(每次起来都写,升级后马上生效)和图标(版本变了才画,画一次要几百毫秒) */
404
+ export async function writeTrayFiles(platform, dir) {
405
+ const icons = join(dir, 'icons');
406
+ await mkdir(icons, { recursive: true });
407
+ const stamp = join(icons, `v${ASSETS_VERSION}`);
408
+ if (!existsSync(stamp)) {
409
+ const all = platform === 'win32' ? windowsTrayIcons() : macTrayIcons();
410
+ for (const [name, png] of Object.entries(all))
411
+ await writeFile(join(icons, name), png);
412
+ await writeFile(stamp, '');
413
+ }
414
+ if (platform === 'win32') {
415
+ const file = join(dir, 'tray.ps1');
416
+ await writeFile(file, Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(WINDOWS_TRAY_SCRIPT, 'utf8')]));
417
+ return file;
418
+ }
419
+ const file = join(dir, 'tray.js');
420
+ await writeFile(file, MAC_TRAY_SCRIPT);
421
+ return file;
422
+ }
423
+ /** 起显示脚本 */
424
+ export function spawnTrayHelper(platform, file, dir, note, on) {
425
+ const env = { ...process.env, COTUTOR_TRAY_DIR: dir };
426
+ const child = platform === 'win32'
427
+ ? spawn('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, env })
428
+ : spawn('/usr/bin/osascript', ['-l', 'JavaScript', file], { stdio: ['pipe', 'pipe', 'pipe'], env });
429
+ child.once('error', (err) => { note(`显示脚本起不来:${err.message}`); });
430
+ child.once('exit', (code) => on.exit(code));
431
+ child.stdin?.on('error', () => undefined);
432
+ if (child.stdout)
433
+ createInterface({ input: child.stdout }).on('line', (line) => {
434
+ try {
435
+ on.message(JSON.parse(line));
436
+ }
437
+ catch {
438
+ note(`显示脚本:${line}`);
439
+ }
440
+ });
441
+ if (child.stderr)
442
+ createInterface({ input: child.stderr }).on('line', (line) => { if (line.trim())
443
+ note(`显示脚本:${line}`); });
444
+ return {
445
+ send(m) { child.stdin?.write(`${JSON.stringify(m)}\n`); },
446
+ close() { child.stdin?.end(); },
447
+ };
448
+ }
449
+ /** 打开地址或目录(Windows 不弹黑窗口) */
450
+ export function openTarget(platform, target) {
451
+ const [cmd, args] = platform === 'darwin' ? ['/usr/bin/open', [target]] : platform === 'win32' ? ['cmd', ['/c', 'start', '', target]] : ['xdg-open', [target]];
452
+ const child = spawn(cmd, args, { stdio: 'ignore', windowsHide: true });
453
+ child.on('error', () => undefined);
454
+ child.unref();
455
+ }
456
+ /** 日志:一个文件,过 1 MB 换一次,只留上一份 */
457
+ export function trayNote(dir) {
458
+ const file = join(dir, 'tray.log');
459
+ return (text) => {
460
+ try {
461
+ try {
462
+ if (statSync(file).size > LOG_MAX)
463
+ renameSync(file, `${file}.1`);
464
+ }
465
+ catch { /* 还没有 */ }
466
+ appendFileSync(file, `${new Date().toISOString()} ${text}\n`);
467
+ }
468
+ catch { /* 写不了日志不能把图标带倒 */ }
469
+ };
470
+ }
471
+ /** 孩子叫什么、哪个端口:只读这两样,不加载 zod(托盘进程常驻,要轻);读不了就用服务名和缺省端口 */
472
+ export async function inspectService(s) {
473
+ try {
474
+ const c = JSON.parse(await readFile(join(s.root, 'cotutor.json'), 'utf8'));
475
+ const kid = typeof c.kid?.name === 'string' && c.kid.name ? c.kid.name : typeof c.kid?.slug === 'string' && c.kid.slug ? c.kid.slug : s.name;
476
+ return { kid, port: typeof c.server?.port === 'number' ? c.server.port : DEFAULT_PORT };
477
+ }
478
+ catch {
479
+ return { kid: s.name, port: DEFAULT_PORT };
480
+ }
481
+ }
482
+ /** 真的依赖,给 runTray */
483
+ export function realTrayDeps(host, dir, file, version) {
484
+ const note = trayNote(dir);
485
+ return {
486
+ platform: host.platform,
487
+ version,
488
+ note,
489
+ now: () => Date.now(),
490
+ list: () => listServices(host),
491
+ inspect: inspectService,
492
+ probe: (port) => probeHealth(port),
493
+ holder: async (port) => (await portHolder(host, port))?.name ?? null,
494
+ control: (root, what) => controlService(host, root, what),
495
+ open: (target) => openTarget(host.platform, target),
496
+ spawnHelper: (on) => spawnTrayHelper(host.platform, file, dir, note, on),
497
+ };
498
+ }
499
+ /** 登录项跑的命令 */
500
+ export const trayArgs = (host, o = {}) => [o.node ?? process.execPath, o.bin ?? binPath(), 'tray', 'run', '--dir', trayDir(host)];
501
+ /** macOS 的 LaunchAgent:登录就起,崩了拉起,以 0 退(点了退出)不拉 */
502
+ export function trayPlist(args, dir, path) {
503
+ const str = (v) => `<string>${xml(v)}</string>`;
504
+ return `<?xml version="1.0" encoding="UTF-8"?>
505
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
506
+ <!-- cotutor tray on 写的;改了会被下次盖掉。关掉:cotutor tray off -->
507
+ <plist version="1.0">
508
+ <dict>
509
+ <key>Label</key>${str(TRAY_LABEL)}
510
+ <key>ProgramArguments</key>
511
+ <array>
512
+ ${args.map((a) => ` ${str(a)}`).join('\n')}
513
+ </array>
514
+ <key>EnvironmentVariables</key>
515
+ <dict>
516
+ <key>PATH</key>${str(path)}
517
+ </dict>
518
+ <key>RunAtLoad</key><true/>
519
+ <key>KeepAlive</key>
520
+ <dict>
521
+ <key>SuccessfulExit</key><false/>
522
+ </dict>
523
+ <key>ThrottleInterval</key><integer>10</integer>
524
+ <key>ProcessType</key><string>Interactive</string>
525
+ <key>LimitLoadToSessionType</key><string>Aqua</string>
526
+ <key>StandardOutPath</key>${str(join(dir, 'launchd.log'))}
527
+ <key>StandardErrorPath</key>${str(join(dir, 'launchd.log'))}
528
+ </dict>
529
+ </plist>
530
+ `;
531
+ }
532
+ async function stopWindowsTray(host, dir) {
533
+ const procs = parseProcs((await host.run('powershell.exe', psArgs(findTrayScript(dir)))).stdout);
534
+ for (const p of procs)
535
+ await host.run('taskkill.exe', ['/PID', String(p.pid), '/T', '/F']);
536
+ }
537
+ /** 装上并起来;已经装过就照现在的 node、bin 重写一遍再起(升级后新代码才生效)。started = false:登录项写好了,这个会话里起不来,下次登录出现 */
538
+ export async function installTray(host, o = {}) {
539
+ if (!traySupported(host.platform))
540
+ throw new UsageError(`${host.platform} 上没做托盘图标:cotutor service status 看状态`);
541
+ const dir = trayDir(host);
542
+ await mkdir(dir, { recursive: true });
543
+ await rm(offMark(dir), { force: true });
544
+ const args = trayArgs(host, o);
545
+ if (host.platform === 'win32') {
546
+ await stopWindowsTray(host, dir);
547
+ const file = launcherFile(dir);
548
+ await writeFile(file, encodeLauncher(trayLauncher(winCommandLine(args))));
549
+ const reg = await host.run('reg.exe', ['add', RUN_KEY, '/v', TRAY_RUN_VALUE, '/t', 'REG_SZ', '/d', runValue(wscriptPath(host), file), '/f']);
550
+ if (reg.code !== 0)
551
+ throw new UsageError(`写登录项没成:${(reg.stderr || reg.stdout).trim()}`);
552
+ await host.run('reg.exe', ['delete', STARTUP_APPROVED_KEY, '/v', TRAY_RUN_VALUE, '/f']);
553
+ const r = await host.run(wscriptPath(host), ['//B', '//Nologo', file]);
554
+ return { file, started: r.code === 0 };
555
+ }
556
+ const file = plistFile(host);
557
+ await mkdir(join(host.home, 'Library', 'LaunchAgents'), { recursive: true });
558
+ await host.run('launchctl', ['bootout', `gui/${host.uid}/${TRAY_LABEL}`]);
559
+ await writeFile(file, trayPlist(args, dir, host.env.PATH ?? '/usr/bin:/bin'));
560
+ const r = await host.run('launchctl', ['bootstrap', `gui/${host.uid}`, file]);
561
+ return { file, started: r.code === 0 };
562
+ }
563
+ /** 卸掉:停下、删登录项。remember = 家长说不要(以后 service install 也不再装) */
564
+ export async function removeTray(host, o = {}) {
565
+ if (!traySupported(host.platform))
566
+ return false;
567
+ const dir = trayDir(host);
568
+ const had = (await trayStatus(host)).installed;
569
+ if (host.platform === 'win32') {
570
+ await stopWindowsTray(host, dir);
571
+ await host.run('reg.exe', ['delete', RUN_KEY, '/v', TRAY_RUN_VALUE, '/f']);
572
+ await host.run('reg.exe', ['delete', STARTUP_APPROVED_KEY, '/v', TRAY_RUN_VALUE, '/f']);
573
+ await host.run('reg.exe', ['delete', `HKCU\\Software\\Classes\\AppUserModelId\\${TRAY_AUMID}`, '/f']);
574
+ await host.run('reg.exe', ['delete', `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings\\${TRAY_AUMID}`, '/f']);
575
+ await rm(launcherFile(dir), { force: true });
576
+ }
577
+ else {
578
+ await host.run('launchctl', ['bootout', `gui/${host.uid}/${TRAY_LABEL}`]);
579
+ await rm(plistFile(host), { force: true });
580
+ }
581
+ if (o.remember) {
582
+ await mkdir(dir, { recursive: true });
583
+ await writeFile(offMark(dir), '');
584
+ }
585
+ return had;
586
+ }
587
+ export async function trayStatus(host) {
588
+ if (!traySupported(host.platform))
589
+ return { supported: false, installed: false, running: false, pid: null, off: false, autostart: null };
590
+ const dir = trayDir(host);
591
+ const off = existsSync(offMark(dir));
592
+ if (host.platform === 'win32') {
593
+ const installed = existsSync(launcherFile(dir));
594
+ const procs = parseProcs((await host.run('powershell.exe', psArgs(findTrayScript(dir)))).stdout);
595
+ const autostart = installed && (await host.run('reg.exe', ['query', RUN_KEY, '/v', TRAY_RUN_VALUE])).code === 0 && !startupDisabled((await host.run('reg.exe', ['query', STARTUP_APPROVED_KEY, '/v', TRAY_RUN_VALUE])).stdout);
596
+ return { supported: true, installed, running: procs.length > 0, pid: procs[0]?.pid ?? null, off, autostart };
597
+ }
598
+ const installed = existsSync(plistFile(host));
599
+ const r = await host.run('launchctl', ['print', `gui/${host.uid}/${TRAY_LABEL}`]);
600
+ const st = r.code === 0 ? parseLaunchctlPrint(r.stdout) : { running: false, pid: null };
601
+ return { supported: true, installed, running: st.running, pid: st.pid, off, autostart: null };
602
+ }
603
+ /** service install 时跟着装:没做的系统、家长关掉过都跳过;返回给人看的结果 */
604
+ export async function ensureTray(host, o = {}) {
605
+ if (!traySupported(host.platform))
606
+ return 'unsupported';
607
+ if (existsSync(offMark(trayDir(host))))
608
+ return 'off';
609
+ return (await installTray(host, o)).started ? 'started' : 'later';
610
+ }
611
+ /** 服务卸完了:图标也撤(不算家长说不要,以后再装服务还会带上) */
612
+ export async function dropTrayIfNoServices(host) {
613
+ if (!traySupported(host.platform))
614
+ return false;
615
+ if ((await listServices(host)).length)
616
+ return false;
617
+ return removeTray(host);
618
+ }
@@ -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)\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\u8D77\u4E00\u6B21\u8001\u5E08\u4E0E\u914D\u97F3(\u82B1\u4E00\u5206\u94B1)\u628A API \u5C42\u7684\u5751\u6446\u51FA\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-qr] \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;/dev/qr \u662F\u626B\u7801\u9875,--open-qr \u987A\u624B\u6253\u5F00)\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/(iPad / iPhone \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(\u4E0E\u9875\u9762\u540C\u4E00\u6761\u8DEF;--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,\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";
2
+ export declare const USAGE = "\u7528\u6CD5:\n cotutor init <slug> [--dir <path>] [--name <\u5B69\u5B50\u540D>] [--grade <1-6>] [--port <n>] \u5EFA ~/cotutor/<slug>/ \u9AA8\u67B6(\u5E42\u7B49\u8865\u7F3A;\u9ED8\u8BA4\u4E8C\u5E74\u7EA7\u6559\u5B66\u5305;\u7AEF\u53E3\u5728\u8FD9\u91CC\u9009\u4E00\u6B21\u5199\u8FDB cotutor.json:443 \u4F18\u5148,\u4E0D\u884C\u5C31 8443\u30018445\u2026,\u4EE5\u540E\u4E0D\u53D8)\n cotutor grades [--json] \u5217\u51FA\u672C\u5305\u53EF\u5B89\u88C5\u7684\u5E74\u7EA7\u6559\u5B66\u5305\n cotutor doctor [--workspace <dir>] [--json] [--live] \u9010\u9879\u4F53\u68C0;--live \u771F\u8FDE\u4E00\u6B21\u767E\u70BC\u5B9E\u65F6\u6A21\u578B\u3001\u771F\u5408\u6210\u4E00\u53E5\u914D\u97F3(\u82B1\u4E00\u5206\u94B1)\u628A API \u5C42\u7684\u5751\u6446\u51FA\u6765(\u771F\u8DD1\u8001\u5E08\u7528 cotutor send)\n cotutor upgrade [--workspace <dir>] [--force <\u8001\u5E08>]... \u8001\u5E08\u6587\u4EF6\u3001activity \u4E0E skill \u6362\u65B0\u7248:\u6CA1\u6539\u8FC7\u7684\u76F4\u63A5\u6362,\u6539\u8FC7\u7684\u53EA\u62A5 diff(--force <\u8001\u5E08> \u6216 --force <\u8001\u5E08>/<\u540D> \u624D\u8986\u76D6,\u539F\u6587\u7559 .bak)\n cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json \u8865\u7F3A:\u65B0\u51FA\u5382\u8001\u5E08 / \u8FD0\u884C\u65F6 / \u547D\u4EE4\u6A21\u677F\u65D7\u6807(\u53EA\u52A0\u7F3A\u7684,\u4F60\u6539\u8FC7\u7684\u503C\u4E0D\u52A8)\n cotutor add <\u8001\u5E08\u540D> --display <\u663E\u793A\u540D> [--subject <\u5B66\u79D1>] [--avatar <emoji|\u56FE\u7247\u76F8\u5BF9\u8DEF\u5F84>] [--hidden] \u52A0\u4E00\u4F4D\u81EA\u5BB6\u7684\u8001\u5E08:\u51FA\u6A21\u677F\u6587\u4EF6\u3001\u8FDB cotutor.json\u3001\u5EFA\u76EE\u5F55(\u540D\u5B57\u4EE5 -tutor \u7ED3\u5C3E\u7684\u662F\u8001\u5E08,figshot \u7ED9\u5B83\u505A\u5934\u50CF)\n cotutor add-theme <\u4E3B\u9898\u540D> [--from <\u4E3B\u9898>] [--workspace <dir>] \u52A0\u4E00\u4E2A\u81EA\u5BB6\u7684\u4E3B\u9898:\u62F7\u4E00\u4EFD(\u7F3A\u7701\u51FA\u5382\u7684 default)\u5230 themes/<\u4E3B\u9898\u540D>/,\u6539 cotutor.json \u7684 kid.theme \u6362\u8FC7\u53BB\n cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <\u76EE\u5F55>] \u5728\u7EC8\u7AEF\u91CC\u8D77\u670D\u52A1(\u4E00 workspace \u4E00\u8FDB\u7A0B;~/.config/cotutor/certs/ \u6709\u8BC1\u4E66\u5C31\u8D70 HTTPS;--trace \u6BCF\u4E00\u8F6E\u7684\u4E8B\u4EF6\u6309\u9053\u6253\u5370;\u53EA\u6253\u5370\u5BB6\u957F\u7AEF\u9996\u9875\u7684\u5730\u5740,\u5B69\u5B50\u7684\u5E73\u677F\u5728\u90A3\u91CC\u626B\u7801;--open \u987A\u624B\u6253\u5F00,\u65E7\u540D --open-qr \u7167\u6837\u8BA4;--log-dir \u8F93\u51FA\u6309\u5929\u843D\u6587\u4EF6,\u540E\u53F0\u670D\u52A1\u7528;\u5173\u4E86\u7EC8\u7AEF\u5C31\u505C,\u4E00\u76F4\u5F00\u7740\u7528 cotutor service)\n cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] \u6253\u5F00\u5BB6\u957F\u7AEF(\u8BFB cotutor.json,\u4E0D\u7528\u8BB0\u7AEF\u53E3);--sandbox \u5F00\u8BD5\u7528\u7684\u90A3\u4E2A(\u7AEF\u53E3\u6C38\u8FDC\u662F\u771F\u7684 +1);--print \u53EA\u6253\u5370\u5730\u5740;\u6CA1\u5728\u8DD1\u5C31\u8BF4\u600E\u4E48\u8D77,\u4EE5 1 \u9000\n cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] \u628A serve \u88C5\u6210\u8FD9\u53F0\u7535\u8111\u7684\u540E\u53F0\u670D\u52A1(macOS launchd\u3001Linux systemd \u7528\u6237\u670D\u52A1\u3001Windows \u7528\u6237\u7684\u767B\u5F55\u9879,\u90FD\u4E0D\u8981\u7BA1\u7406\u5458):\u767B\u5F55\u540E\u81EA\u5DF1\u8D77\u6765\u3001\u5D29\u4E86 10 \u79D2\u540E\u518D\u8D77\u3001\u65E5\u5FD7\u6309\u5929\u843D\u6587\u4EF6\u7559 14 \u5929;\u4E00\u4E2A workspace \u4E00\u4E2A;\u6362\u4E86 node \u7248\u672C\u6216\u5305\u632A\u4E86\u5730\u65B9\u518D install \u4E00\u6B21\n cotutor tray on | off | status [--json] \u83DC\u5355\u680F / \u4EFB\u52A1\u680F\u56FE\u6807(macOS\u3001Windows;service install \u65F6\u4E00\u8D77\u88C5,--no-tray \u4E0D\u5E26):\u6B63\u5E38\u65F6\u53EA\u6709 logo,\u6709\u4E8B\u51FA\u6A59\u70B9,\u6CA1\u5728\u8DD1\u65F6\u53D8\u6DE1\u52A0\u7EA2\u70B9\u5E76\u901A\u77E5\u4E00\u6B21;\u70B9\u5B83\u770B\u72B6\u6001\u3001\u6253\u5F00\u5BB6\u957F\u7AEF\u3001\u91CD\u542F\u3001\u770B\u65E5\u5FD7\u3001\u9000\u51FA;off \u5173\u6389\u5E76\u8BB0\u4F4F\n cotutor service status [--json] | logs [--lines <n>] [--follow] \u540E\u53F0\u670D\u52A1\u5728\u4E0D\u5728\u8DD1\u3001\u7AEF\u53E3\u7B54\u4E0D\u7B54\u5E94\u3001\u8DD1\u7684\u54EA\u4E2A\u7248\u672C\u3001vault \u8BFB\u4E0D\u8BFB\u5F97\u5230\u3001\u65E5\u5FD7\u5728\u54EA;logs \u770B\u6700\u8FD1\u7684\u65E5\u5FD7\n cotutor pin [off | status] [--pin <\u6570\u5B57>] [--json] \u5BB6\u957F\u7AEF /parent \u4E0E\u5DE5\u4F5C\u53F0 /dev \u7684\u9501:\u673A\u5668\u7EA7 PIN(\u6240\u6709 workspace \u5171\u7528),iPad \u4E0A\u8F93\u4E00\u6B21\u3001\u8FD9\u53F0\u7535\u8111\u514D\u5BC6;\u6CA1\u8BBE\u5C31\u5168\u5F00(\u5BB6\u957F\u5728\u65C1\u8FB9\u53EF\u4EE5\u4E0D\u8BBE);off \u64A4\u6389\n cotutor cert [--host <\u540D\u6216IP>]... \u7528 mkcert \u5EFA\u8FD9\u53F0\u673A\u5668\u7684\u81EA\u7B7E\u8BC1\u4E66\u5230 ~/.config/cotutor/certs/(\u5E73\u677F\u3001\u624B\u673A\u4E0A\u5F55\u97F3\u8981 HTTPS;\u6240\u6709 workspace \u5171\u7528)\n cotutor send <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--runtime <\u540D>] [--new] [--photo a.jpg,b.png] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--lane main,tts] [--quiet] \u7EC8\u7AEF\u91CC\u53D1\u4E00\u6761,\u73B0\u573A\u6309\u9053\u6253\u5370\u6BCF\u9053\u5DE5\u5E8F\u7684\u4E8B\u4EF6,\u8BF4\u5B8C\u6253\u5370\u7ED3\u679C(\u5B69\u5B50\u7684\u8BDD\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF:\u4E0A\u4E0B\u6587\u5305\u5E26 live:\u3001\u4E0D\u8865\u63D0\u95EE\u5361,\u53EA\u662F\u6CA1\u6709\u5634;--from system \u4E0D\u7B97;--new \u5F00\u65B0\u8BDD\u9898;--photo \u5E26\u4F5C\u4E1A\u7167\u7247,\u62F7\u8FDB captures/,\u6709\u7167\u7247\u65F6\u6D88\u606F\u53EF\u4EE5\u7A7A;--activity \u50CF\u5B69\u5B50\u70B9\u4E86\u9996\u9875\u7684 activity \u6309\u94AE,\u65B0\u5F00\u8BDD\u9898,\u53C2\u6570\u540C\u6309\u94AE\u4E0B\u9762\u90A3\u51E0\u884C;--quiet \u53EA\u8981\u7ED3\u679C)\n cotutor pack <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--at <ISO\u65F6\u95F4>] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--json] \u5E72\u8DD1\u4E0A\u4E0B\u6587\u5305(--activity \u770B activity \u8BDD\u9898\u7B2C\u4E00\u6761\u8001\u5E08\u62FF\u5230\u4EC0\u4E48):\u4E0D\u8D77\u6A21\u578B,\u6253\u5370\u73B0\u5728\u4F1A\u53D1\u7ED9\u8001\u5E08\u7684\u90A3\u4EFD + \u6BCF\u6BB5\u6765\u81EA\u54EA\u4E2A\u6587\u4EF6\u3001\u90A3\u91CC\u4E00\u5171\u51E0\u6761\u3001\u6309\u653F\u7B56\u5E26\u4E86\u51E0\u6761(\u6539\u4E86\u6863\u6848 / \u65E5\u8BB0 / \u8BA1\u5212\u7ACB\u523B\u770B\u6548\u679C)\n cotutor replay <\u8001\u5E08> <job> [<\u65E5\u671F>] [--runtime <\u540D>] [--json] \u56DE\u653E\u4E00\u8F6E:\u540C\u4E00\u95EE\u6309\u73B0\u5728\u7684 vault \u4E0E\u63D0\u793A\u8BCD\u518D\u8DD1\u4E00\u904D(\u843D evals/,\u4E0D\u8FDB\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u4E0D\u914D\u97F3),\u8DD1\u5B8C\u5E76\u6392\u6253\u5370\u4E0A\u4E0B\u6587\u5305 / \u8BB2\u7A3F / \u5361 / \u8BFB\u4E86\u4EC0\u4E48\u7684 diff\n cotutor compare <\u8001\u5E08> [<evalJob>] [<\u65E5\u671F>] [--json] \u518D\u770B\u4E00\u6B21\u56DE\u653E\u7684\u5BF9\u7167(\u4E0D\u7ED9 evalJob \u5C31\u5217\u8FD9\u5929\u56DE\u653E\u8FC7\u54EA\u4E9B)\n cotutor show <\u8001\u5E08> <job> [<\u65E5\u671F>] [--json] [--evals] [--workspace <dir>] \u770B\u4E00\u8F6E:\u95EE\u4E86\u4EC0\u4E48\u3001\u4E0A\u4E0B\u6587\u5305\u3001\u5F53\u65F6\u7684\u8001\u5E08\u6587\u4EF6\u4E0E\u6280\u80FD hash\u3001\u8BB2\u7A3F\u4E0E\u5361\u3001\u8BFB\u4E86\u4EC0\u4E48\u3001token \u4E0E\u7528\u65F6\u3001\u7ED9\u5BB6\u957F\u7684\u5C3E\u5DF4;--json \u662F\u5BB6\u957F\u7AEF\u300C\u770B\u539F\u6587\u300D\u540C\u4E00\u4EFD\u6570\u636E,\u7ED9\u7F16\u7A0B\u52A9\u624B\u5206\u6790\u7528\n cotutor trace <\u8001\u5E08> <job> [<\u65E5\u671F>] [--lane \u2026] [--workspace <dir>] \u56DE\u653E\u4E00\u8F6E\u7684\u4E8B\u4EF6(<\u65E5\u671F>.<job>.events.jsonl;\u6392\u67E5\u6628\u5929\u90A3\u8F6E\u7528)\n cotutor rate <\u8001\u5E08> <\u8BDD\u9898> <1-5> [--date <\u65E5\u671F>] \u7ED9\u4E00\u4E2A\u8BDD\u9898\u6253\u661F(\u4E0E\u5BB6\u957F\u7AEF\u540C\u4E00\u6761\u8DEF;\u2265 vault.keepScore \u7684\u8BDD\u9898\u8BB0\u8D26\u65F6\u6458\u8981\u624D\u8FDB\u65E5\u8BB0)\n cotutor bookkeep <\u8001\u5E08> [--date <\u65E5\u671F>] [--thread <\u8BDD\u9898>]... \u8BB0\u8D26:\u8FD9\u5929\u6BCF\u4E2A\u8FD8\u6CA1\u8BB0\u8FC7\u7684\u8BDD\u9898\u5404\u8D77\u4E00\u8F6E\u8BB0\u8D26\u4EFB\u52A1,\u8001\u5E08\u56DE\u300C## \u8BB0\u8D26\u300D,\u5E94\u7528\u5199\u8FDB vault \u7684\u65E5\u8BB0(\u8BDD\u9898\u540D\u3001\u5B69\u5B50\u95EE\u3001\u6458\u8981\u3001\u89C2\u5BDF)\n cotutor home check [--published] [--json] \u68C0\u67E5\u9996\u9875\u8349\u7A3F home/draft.md(\u6216\u5DF2\u53D1\u5E03\u7684\u90A3\u4EFD):\u5B69\u5B50\u4F1A\u770B\u5230\u54EA\u51E0\u5F20\u5361\u3001\u6BCF\u4F4D\u8001\u5E08\u7684\u6309\u94AE\u3001\u8981\u6539\u7684\u4E0E\u63D0\u9192(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor home publish [--force] [--from <\u6587\u4EF6>] [--json] \u53D1\u5E03\u9996\u9875:\u68C0\u67E5 \u2192 \u7559\u5386\u53F2 home/history/<id>.md \u2192 \u5199 home/published.json(\u5B69\u5B50\u7AEF\u53EA\u8BFB\u5B83);\u6709\u8981\u6539\u7684\u4E0D\u53D1,--force \u4E22\u6389\u90A3\u51E0\u5F20\u7167\u53D1;--from \u53D1\u53E6\u4E00\u4EFD(\u5982\u5386\u53F2)\n cotutor home show [--json] \u73B0\u5728\u53D1\u5E03\u7684\u662F\u54EA\u4EFD\u3001\u51E0\u5929\u524D\u3001\u6BCF\u4E2A\u6309\u94AE\u88AB\u70B9\u4E86\u51E0\u6B21(\u54EA\u5929\u3001\u54EA\u4E2A\u8BDD\u9898)\n cotutor activity check [<\u8001\u5E08> [<\u540D>]] [--json] \u68C0\u67E5 activity \u6587\u4EF6(agents/<\u8001\u5E08>/activities/<\u540D>.md):\u5BB6\u957F\u586B\u7684\u3001\u5E94\u7528\u7B97\u7684\u3001\u5E94\u7528\u62E6\u7684\u3001\u88C1\u6BB5\u3001\u63D2\u69FD\u3001\u6B63\u6587\u7528\u7684\u5361\u3001\u7B2C\u4E00\u53E5\u4E0E\u6536(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor activity eval <\u8001\u5E08> <\u540D> [--runs <n>] [--only <\u4F8B\u5B50>] [--kid-model <\u5343\u95EE\u6A21\u578B>] [--json] \u7167 <\u540D>.\u4F8B\u5B50.md \u8DD1:\u771F\u8001\u5E08 + \u6A21\u62DF\u5B69\u5B50(\u5343\u95EE,\u7F3A\u7701 qwen3.8-flash,\u6709 Token Plan \u8D70\u5B83)\u591A\u8F6E(\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF),\u5224\u53EA\u770B\u677F\u4E66;\u4E00\u4E2A\u4F8B\u5B50\u8DD1 n \u6B21(\u7F3A\u7701 3)\u6B21\u6B21\u90FD\u8FC7\u624D\u7B97\u8FC7;\u843D evals/,\u753B\u5BB6\u662F\u5047\u7684;\u82B1\u94B1\n cotutor chalk <check|build|bake|snap|dub> <scene.ts|\u8BFE\u5305 id> [...] \u505A\u5C0F\u8BFE\u5802\u7684\u8BFE\u5305:check \u6821\u9A8C\u573A\u666F\u3001build \u51FA\u8BFE\u5305\u5230 bundles/\u3001bake \u70E4\u753B\u9762\u3001snap \u6BCF\u6B65\u622A\u56FE\u3001dub \u914D\u97F3(cotutor chalk \u770B\u5404\u81EA\u7684\u9009\u9879;bake / snap \u8981\u672C\u673A\u7684 Chrome)\n cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] \u4E0D\u7ECF\u771F\u5B9E\u8001\u5E08\u4E0E\u767E\u70BC,\u7528\u56FA\u5B9A\u7684\u677F\u4E66 JSON \u8D77\u5B69\u5B50\u7AEF,\u6D4B\u524D\u7AEF\u4EA4\u4E92\u4E0E\u6E32\u67D3(\u4E0D\u9700\u8981 workspace;\u8001\u5E08\u90FD\u8D70\u5B9E\u65F6\u8001\u5E08\u9875,\u5047\u5634\u7167\u811A\u672C\u5FF5\u9759\u97F3\u7684\u58F0\u97F3;nokey = \u6CA1\u914D\u767E\u70BC,\u5B69\u5B50\u7AEF\u53EA\u51FA\u300C\u8001\u5E08\u4EEC\u8FD8\u6CA1\u5230\u300D)\n cotutor --version | --help\nworkspace\u89E3\u6790:--workspace > COTUTOR_WORKSPACE > cwd \u6216\u7956\u5148\u6709 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ \u4E0B\u552F\u4E00\u7684\u5B69\u5B50\u76EE\u5F55\n\u8BD5\u7528:\u8981 workspace \u7684\u547D\u4EE4\u52A0 --sandbox,\u6362\u5230\u771F workspace \u65C1\u8FB9\u7684 .sandbox/<\u76EE\u5F55\u540D>/,\u8BD5\u7684\u5BF9\u8BDD\u4E0E vault \u7684\u5199\u5165\u4E0D\u8FDB\u5B69\u5B50\u7684\u8BB0\u5F55(\u7B2C\u4E00\u6B21\u81EA\u52A8\u5206:\u8BBE\u7F6E\u7167\u62F7,\u5BF9\u8BDD\u4E0E\u7167\u7247\u4E0D\u5E26,vault \u62F7\u4E00\u4EFD;\u6807\u9898\u524D\u52A0\u300C\u8BD5\u7528\u300D\u3001\u7AEF\u53E3 +1\u3001\u81EA\u52A8\u8BB0\u8D26\u5173\u6389);--sandbox --fresh \u91CD\u65B0\u5206\n";