cotutor 0.3.0 → 0.4.1

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 (209) hide show
  1. package/LICENSE +9 -7
  2. package/NOTICE +3 -3
  3. package/README.md +65 -18
  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 +11 -0
  17. package/dist/app/cli/cert.js +36 -9
  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 +11 -5
  22. package/dist/app/cli/doctor.js +156 -181
  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 +379 -27
  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 +152 -0
  42. package/dist/app/cli/service.js +443 -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 +42 -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 +236 -0
  58. package/dist/app/cli/tray.js +670 -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 +47 -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/machine/update.d.ts +25 -0
  77. package/dist/app/machine/update.js +88 -0
  78. package/dist/app/mock/mock-live.js +1 -1
  79. package/dist/app/mock/mock.d.ts +4 -2
  80. package/dist/app/mock/mock.js +20 -12
  81. package/dist/app/replay.js +4 -4
  82. package/dist/app/skills/home-doc.js +1 -1
  83. package/dist/app/workspace.d.ts +9 -4
  84. package/dist/app/workspace.js +33 -10
  85. package/dist/app/ws/route.d.ts +8 -2
  86. package/dist/app/ws/route.js +7 -3
  87. package/dist/board/parse.d.ts +0 -2
  88. package/dist/board/parse.js +0 -6
  89. package/dist/board/presentation.d.ts +9 -15
  90. package/dist/board/presentation.js +8 -10
  91. package/dist/brain/api-tools.js +2 -1
  92. package/dist/brain/api.d.ts +12 -17
  93. package/dist/brain/api.js +16 -33
  94. package/dist/brain/index.d.ts +23 -71
  95. package/dist/brain/index.js +6 -69
  96. package/dist/brain/log.d.ts +5 -0
  97. package/dist/brain/log.js +22 -0
  98. package/dist/brain/runtime.d.ts +30 -0
  99. package/dist/brain/runtime.js +23 -0
  100. package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
  101. package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
  102. package/dist/brain/stream.d.ts +5 -5
  103. package/dist/brain/stream.js +7 -7
  104. package/dist/brain/transcript.d.ts +3 -3
  105. package/dist/brain/transcript.js +6 -6
  106. package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
  107. package/dist/brain/types.js +14 -0
  108. package/dist/chalk/glyphs/skeleton.d.ts +3 -3
  109. package/dist/chalk/glyphs/skeleton.js +1 -1
  110. package/dist/lib/index.d.ts +1 -1
  111. package/dist/lib/index.js +1 -1
  112. package/dist/memory/diary.js +3 -3
  113. package/dist/realtime/bailian.d.ts +1 -1
  114. package/dist/realtime/bailian.js +3 -2
  115. package/dist/realtime/preview.js +1 -1
  116. package/dist/realtime/voices.d.ts +2 -0
  117. package/dist/realtime/voices.js +57 -57
  118. package/dist/runtime/activities.d.ts +13 -1
  119. package/dist/runtime/activities.js +28 -2
  120. package/dist/runtime/activity-state.js +2 -2
  121. package/dist/runtime/agent-file.d.ts +3 -3
  122. package/dist/runtime/agent-file.js +3 -3
  123. package/dist/runtime/context/tutor-rules.d.ts +2 -2
  124. package/dist/runtime/context/tutor-rules.js +3 -3
  125. package/dist/runtime/home/home.d.ts +12 -1
  126. package/dist/runtime/home/home.js +8 -1
  127. package/dist/runtime/home/store.d.ts +1 -1
  128. package/dist/runtime/home/store.js +36 -21
  129. package/dist/runtime/journal/mix.d.ts +15 -6
  130. package/dist/runtime/journal/mix.js +60 -19
  131. package/dist/runtime/journal/recorder.d.ts +58 -13
  132. package/dist/runtime/journal/recorder.js +106 -16
  133. package/dist/runtime/journal/store.d.ts +4 -4
  134. package/dist/runtime/journal/store.js +6 -6
  135. package/dist/runtime/kid-input.js +2 -3
  136. package/dist/runtime/live/call.d.ts +12 -0
  137. package/dist/runtime/live/call.js +51 -18
  138. package/dist/runtime/live/turns.js +1 -1
  139. package/dist/runtime/runner.d.ts +9 -55
  140. package/dist/runtime/runner.js +52 -173
  141. package/dist/scenes/paint/painter.js +4 -3
  142. package/dist/schema/config.d.ts +43 -22
  143. package/dist/schema/config.js +45 -47
  144. package/dist/schema/conversation.d.ts +6 -0
  145. package/dist/schema/home.d.ts +11 -1
  146. package/dist/schema/home.js +7 -2
  147. package/dist/schema/workspace.d.ts +27 -0
  148. package/dist/schema/workspace.js +28 -0
  149. package/dist/shared/default-port.d.ts +2 -0
  150. package/dist/shared/default-port.js +2 -0
  151. package/dist/shared/qianwen.d.ts +14 -10
  152. package/dist/shared/qianwen.js +26 -22
  153. package/dist/shared/site-url.d.ts +2 -0
  154. package/dist/shared/site-url.js +5 -0
  155. package/dist/shared/upgrade.d.ts +9 -0
  156. package/dist/shared/upgrade.js +23 -0
  157. package/dist/ui/dev/dev-page.js +25 -252
  158. package/dist/ui/dev/dev-theme.d.ts +1 -1
  159. package/dist/ui/dev/dev-theme.js +1 -1
  160. package/dist/ui/dev/voice-gallery-page.js +1 -1
  161. package/dist/ui/kid/board-mark.d.ts +2 -2
  162. package/dist/ui/kid/board-mark.js +2 -2
  163. package/dist/ui/kid/home-tiles.d.ts +14 -0
  164. package/dist/ui/kid/home-tiles.js +44 -0
  165. package/dist/ui/kid/kid-page.js +68 -103
  166. package/dist/ui/kid/voice-ui.d.ts +1 -1
  167. package/dist/ui/kid/voice-ui.js +1 -1
  168. package/dist/ui/kid/word-emoji.js +5 -3
  169. package/dist/ui/parent/parent-home-page.js +337 -7
  170. package/dist/ui/parent/parent-page.js +6 -2
  171. package/dist/ui/parent/parent-settings-page.d.ts +1 -0
  172. package/dist/ui/parent/parent-settings-page.js +133 -0
  173. package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
  174. package/dist/ui/{dev → parent}/qr-page.js +19 -14
  175. package/dist/ui/shared/brand.d.ts +3 -1
  176. package/dist/ui/shared/brand.js +3 -1
  177. package/grades/grade-2/README.md +1 -1
  178. package/grades/grade-2/VALIDATION.md +1 -1
  179. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
  180. 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
  181. 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
  182. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
  183. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
  184. 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
  185. 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
  186. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
  187. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
  188. 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
  189. 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
  190. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
  191. package/package.json +6 -2
  192. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
  193. package/skills/cotutor-board/references/photo.md +2 -2
  194. package/skills/cotutor-home/SKILL.md +1 -0
  195. package/skills/cotutor-home/references/tutor.md +2 -1
  196. package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
  197. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
  198. package/skills/cotutor-tutor/SKILL.md +5 -13
  199. package/dist/brain/cli/adapters.d.ts +0 -60
  200. package/dist/brain/cli/adapters.js +0 -91
  201. package/dist/brain/cli/qwen.d.ts +0 -11
  202. package/dist/brain/cli/qwen.js +0 -141
  203. package/dist/brain/cli/run-plan.d.ts +0 -59
  204. package/dist/brain/cli/run-plan.js +0 -71
  205. package/dist/brain/cli/run.d.ts +0 -35
  206. package/dist/brain/cli/run.js +0 -256
  207. package/dist/brain/cli/types.js +0 -3
  208. package/dist/brain/cli/warm.d.ts +0 -53
  209. package/dist/brain/cli/warm.js +0 -103
@@ -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);--json \u91CC\u4E0D\u8FC7\u7684\u9879\u5E26 cmd(\u80FD\u76F4\u63A5\u8DD1)\u6216 human(\u8981\u4EBA\u6765)\n cotutor upgrade [--workspace <dir>] [--force <\u8001\u5E08>]... \u8001\u5E08\u6587\u4EF6\u3001activity \u4E0E skill \u6362\u65B0\u7248:\u6CA1\u6539\u8FC7\u7684\u76F4\u63A5\u6362,\u6539\u8FC7\u7684\u53EA\u62A5 diff(--force <\u8001\u5E08> \u6216 --force <\u8001\u5E08>/<\u540D> \u624D\u8986\u76D6,\u539F\u6587\u7559 .bak)\n cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json \u8865\u7F3A:\u65B0\u51FA\u5382\u8001\u5E08 / \u8FD0\u884C\u65F6 / \u547D\u4EE4\u6A21\u677F\u65D7\u6807(\u53EA\u52A0\u7F3A\u7684,\u4F60\u6539\u8FC7\u7684\u503C\u4E0D\u52A8)\n cotutor add <\u8001\u5E08\u540D> --display <\u663E\u793A\u540D> [--subject <\u5B66\u79D1>] [--avatar <emoji|\u56FE\u7247\u76F8\u5BF9\u8DEF\u5F84>] [--hidden] \u52A0\u4E00\u4F4D\u81EA\u5BB6\u7684\u8001\u5E08:\u51FA\u6A21\u677F\u6587\u4EF6\u3001\u8FDB cotutor.json\u3001\u5EFA\u76EE\u5F55(\u540D\u5B57\u4EE5 -tutor \u7ED3\u5C3E\u7684\u662F\u8001\u5E08,figshot \u7ED9\u5B83\u505A\u5934\u50CF)\n cotutor add-theme <\u4E3B\u9898\u540D> [--from <\u4E3B\u9898>] [--workspace <dir>] \u52A0\u4E00\u4E2A\u81EA\u5BB6\u7684\u4E3B\u9898:\u62F7\u4E00\u4EFD(\u7F3A\u7701\u51FA\u5382\u7684 default)\u5230 themes/<\u4E3B\u9898\u540D>/,\u6539 cotutor.json \u7684 kid.theme \u6362\u8FC7\u53BB\n cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <\u76EE\u5F55>] \u5728\u7EC8\u7AEF\u91CC\u8D77\u670D\u52A1(\u4E00 workspace \u4E00\u8FDB\u7A0B;~/.config/cotutor/certs/ \u6709\u8BC1\u4E66\u5C31\u8D70 HTTPS;--trace \u6BCF\u4E00\u8F6E\u7684\u4E8B\u4EF6\u6309\u9053\u6253\u5370;\u53EA\u6253\u5370\u5BB6\u957F\u7AEF\u9996\u9875\u7684\u5730\u5740,\u5B69\u5B50\u7684\u5E73\u677F\u5728\u90A3\u91CC\u626B\u7801;--open \u987A\u624B\u6253\u5F00,\u65E7\u540D --open-qr \u7167\u6837\u8BA4;--log-dir \u8F93\u51FA\u6309\u5929\u843D\u6587\u4EF6,\u540E\u53F0\u670D\u52A1\u7528;\u5173\u4E86\u7EC8\u7AEF\u5C31\u505C,\u4E00\u76F4\u5F00\u7740\u7528 cotutor service)\n cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] \u6253\u5F00\u5BB6\u957F\u7AEF(\u8BFB cotutor.json,\u4E0D\u7528\u8BB0\u7AEF\u53E3);--sandbox \u5F00\u8BD5\u7528\u7684\u90A3\u4E2A(\u7AEF\u53E3\u6C38\u8FDC\u662F\u771F\u7684 +1);--print \u53EA\u6253\u5370\u5730\u5740;\u6CA1\u5728\u8DD1\u5C31\u8BF4\u600E\u4E48\u8D77,\u4EE5 1 \u9000\n cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] [--no-open] \u628A serve \u88C5\u6210\u8FD9\u53F0\u7535\u8111\u7684\u540E\u53F0\u670D\u52A1(macOS launchd\u3001Linux systemd \u7528\u6237\u670D\u52A1\u3001Windows \u7528\u6237\u7684\u767B\u5F55\u9879,\u90FD\u4E0D\u8981\u7BA1\u7406\u5458):\u767B\u5F55\u540E\u81EA\u5DF1\u8D77\u6765\u3001\u5D29\u4E86 10 \u79D2\u540E\u518D\u8D77\u3001\u65E5\u5FD7\u6309\u5929\u843D\u6587\u4EF6\u7559 14 \u5929;\u4E00\u4E2A workspace \u4E00\u4E2A;\u7B2C\u4E00\u6B21 install \u6253\u5F00\u4E00\u6B21\u5BB6\u957F\u7AEF(--no-open \u4E0D\u5F00),\u4EE5\u540E\u7684\u542F\u52A8\u90FD\u4E0D\u5F00;\u6362\u4E86 node \u7248\u672C\u6216\u5305\u632A\u4E86\u5730\u65B9\u518D install \u4E00\u6B21\n cotutor tray on | off | status [--json] \u83DC\u5355\u680F / \u4EFB\u52A1\u680F\u56FE\u6807(macOS\u3001Windows;service install \u65F6\u4E00\u8D77\u88C5,--no-tray \u4E0D\u5E26):\u6B63\u5E38\u65F6\u53EA\u6709 logo,\u6709\u4E8B\u51FA\u6A59\u70B9,\u6CA1\u5728\u8DD1\u65F6\u53D8\u6DE1\u52A0\u7EA2\u70B9\u5E76\u901A\u77E5\u4E00\u6B21;\u70B9\u5B83\u770B\u72B6\u6001\u3001\u6253\u5F00\u5BB6\u957F\u7AEF\u3001\u91CD\u542F\u3001\u770B\u65E5\u5FD7\u3001\u9000\u51FA;off \u5173\u6389\u5E76\u8BB0\u4F4F\n cotutor service status [--json] | logs [--lines <n>] [--follow] \u540E\u53F0\u670D\u52A1\u5728\u4E0D\u5728\u8DD1\u3001\u7AEF\u53E3\u7B54\u4E0D\u7B54\u5E94\u3001\u8DD1\u7684\u54EA\u4E2A\u7248\u672C\u3001vault \u8BFB\u4E0D\u8BFB\u5F97\u5230\u3001\u65E5\u5FD7\u5728\u54EA;--json \u8FD8\u7B54\u5E73\u677F\u5730\u5740\u3001\u8BC1\u4E66\u3001\u767E\u70BC\u914D\u6CA1\u914D\u3001\u56FE\u6807;logs \u770B\u6700\u8FD1\u7684\u65E5\u5FD7\n cotutor pin [off | status] [--pin <\u6570\u5B57>] [--json] \u5BB6\u957F\u7AEF /parent \u4E0E\u5DE5\u4F5C\u53F0 /dev \u7684\u9501:\u673A\u5668\u7EA7 PIN(\u6240\u6709 workspace \u5171\u7528),iPad \u4E0A\u8F93\u4E00\u6B21\u3001\u8FD9\u53F0\u7535\u8111\u514D\u5BC6;\u6CA1\u8BBE\u5C31\u5168\u5F00(\u5BB6\u957F\u5728\u65C1\u8FB9\u53EF\u4EE5\u4E0D\u8BBE);off \u64A4\u6389\n cotutor cert [--host <\u540D\u6216IP>]... \u7528 mkcert \u5EFA\u8FD9\u53F0\u673A\u5668\u7684\u81EA\u7B7E\u8BC1\u4E66\u5230 ~/.config/cotutor/certs/(\u5E73\u677F\u3001\u624B\u673A\u4E0A\u5F55\u97F3\u8981 HTTPS;\u6240\u6709 workspace \u5171\u7528)\n cotutor send <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--runtime <\u540D>] [--new] [--photo a.jpg,b.png] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--lane main,tts] [--quiet] \u7EC8\u7AEF\u91CC\u53D1\u4E00\u6761,\u73B0\u573A\u6309\u9053\u6253\u5370\u6BCF\u9053\u5DE5\u5E8F\u7684\u4E8B\u4EF6,\u8BF4\u5B8C\u6253\u5370\u7ED3\u679C(\u5B69\u5B50\u7684\u8BDD\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF:\u4E0A\u4E0B\u6587\u5305\u5E26 live:\u3001\u4E0D\u8865\u63D0\u95EE\u5361,\u53EA\u662F\u6CA1\u6709\u5634;--from system \u4E0D\u7B97;--new \u5F00\u65B0\u8BDD\u9898;--photo \u5E26\u4F5C\u4E1A\u7167\u7247,\u62F7\u8FDB captures/,\u6709\u7167\u7247\u65F6\u6D88\u606F\u53EF\u4EE5\u7A7A;--activity \u50CF\u5B69\u5B50\u70B9\u4E86\u9996\u9875\u7684 activity \u6309\u94AE,\u65B0\u5F00\u8BDD\u9898,\u53C2\u6570\u540C\u6309\u94AE\u4E0B\u9762\u90A3\u51E0\u884C;--quiet \u53EA\u8981\u7ED3\u679C)\n cotutor pack <\u8001\u5E08> [<\u6D88\u606F>] [--from kid|system] [--at <ISO\u65F6\u95F4>] [--activity <\u540D> [--param '\u540D=\u503C;\u540D=\u503C']] [--json] \u5E72\u8DD1\u4E0A\u4E0B\u6587\u5305(--activity \u770B activity \u8BDD\u9898\u7B2C\u4E00\u6761\u8001\u5E08\u62FF\u5230\u4EC0\u4E48):\u4E0D\u8D77\u6A21\u578B,\u6253\u5370\u73B0\u5728\u4F1A\u53D1\u7ED9\u8001\u5E08\u7684\u90A3\u4EFD + \u6BCF\u6BB5\u6765\u81EA\u54EA\u4E2A\u6587\u4EF6\u3001\u90A3\u91CC\u4E00\u5171\u51E0\u6761\u3001\u6309\u653F\u7B56\u5E26\u4E86\u51E0\u6761(\u6539\u4E86\u6863\u6848 / \u65E5\u8BB0 / \u8BA1\u5212\u7ACB\u523B\u770B\u6548\u679C)\n cotutor replay <\u8001\u5E08> <job> [<\u65E5\u671F>] [--runtime <\u540D>] [--json] \u56DE\u653E\u4E00\u8F6E:\u540C\u4E00\u95EE\u6309\u73B0\u5728\u7684 vault \u4E0E\u63D0\u793A\u8BCD\u518D\u8DD1\u4E00\u904D(\u843D evals/,\u4E0D\u8FDB\u5B69\u5B50\u7684\u5BF9\u8BDD\u3001\u4E0D\u914D\u97F3),\u8DD1\u5B8C\u5E76\u6392\u6253\u5370\u4E0A\u4E0B\u6587\u5305 / \u8BB2\u7A3F / \u5361 / \u8BFB\u4E86\u4EC0\u4E48\u7684 diff\n cotutor compare <\u8001\u5E08> [<evalJob>] [<\u65E5\u671F>] [--json] \u518D\u770B\u4E00\u6B21\u56DE\u653E\u7684\u5BF9\u7167(\u4E0D\u7ED9 evalJob \u5C31\u5217\u8FD9\u5929\u56DE\u653E\u8FC7\u54EA\u4E9B)\n cotutor show <\u8001\u5E08> <job> [<\u65E5\u671F>] [--json] [--evals] [--workspace <dir>] \u770B\u4E00\u8F6E:\u95EE\u4E86\u4EC0\u4E48\u3001\u4E0A\u4E0B\u6587\u5305\u3001\u5F53\u65F6\u7684\u8001\u5E08\u6587\u4EF6\u4E0E\u6280\u80FD hash\u3001\u8BB2\u7A3F\u4E0E\u5361\u3001\u8BFB\u4E86\u4EC0\u4E48\u3001token \u4E0E\u7528\u65F6\u3001\u7ED9\u5BB6\u957F\u7684\u5C3E\u5DF4;--json \u662F\u5BB6\u957F\u7AEF\u300C\u770B\u539F\u6587\u300D\u540C\u4E00\u4EFD\u6570\u636E,\u7ED9\u7F16\u7A0B\u52A9\u624B\u5206\u6790\u7528\n cotutor trace <\u8001\u5E08> <job> [<\u65E5\u671F>] [--lane \u2026] [--workspace <dir>] \u56DE\u653E\u4E00\u8F6E\u7684\u4E8B\u4EF6(<\u65E5\u671F>.<job>.events.jsonl;\u6392\u67E5\u6628\u5929\u90A3\u8F6E\u7528)\n cotutor rate <\u8001\u5E08> <\u8BDD\u9898> <1-5> [--date <\u65E5\u671F>] \u7ED9\u4E00\u4E2A\u8BDD\u9898\u6253\u661F(\u4E0E\u5BB6\u957F\u7AEF\u540C\u4E00\u6761\u8DEF;\u2265 vault.keepScore \u7684\u8BDD\u9898\u8BB0\u8D26\u65F6\u6458\u8981\u624D\u8FDB\u65E5\u8BB0)\n cotutor bookkeep <\u8001\u5E08> [--date <\u65E5\u671F>] [--thread <\u8BDD\u9898>]... \u8BB0\u8D26:\u8FD9\u5929\u6BCF\u4E2A\u8FD8\u6CA1\u8BB0\u8FC7\u7684\u8BDD\u9898\u5404\u8D77\u4E00\u8F6E\u8BB0\u8D26\u4EFB\u52A1,\u8001\u5E08\u56DE\u300C## \u8BB0\u8D26\u300D,\u5E94\u7528\u5199\u8FDB vault \u7684\u65E5\u8BB0(\u8BDD\u9898\u540D\u3001\u5B69\u5B50\u95EE\u3001\u6458\u8981\u3001\u89C2\u5BDF)\n cotutor home check [--published] [--json] \u68C0\u67E5\u9996\u9875\u8349\u7A3F home/draft.md(\u6216\u5DF2\u53D1\u5E03\u7684\u90A3\u4EFD):\u5B69\u5B50\u4F1A\u770B\u5230\u54EA\u51E0\u5F20\u5361\u3001\u6BCF\u4F4D\u8001\u5E08\u7684\u6309\u94AE\u3001\u8981\u6539\u7684\u4E0E\u63D0\u9192(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor home publish [--force] [--from <\u6587\u4EF6>] [--json] \u53D1\u5E03\u9996\u9875:\u68C0\u67E5 \u2192 \u7559\u5386\u53F2 home/history/<id>.md \u2192 \u5199 home/published.json(\u5B69\u5B50\u7AEF\u53EA\u8BFB\u5B83);\u6709\u8981\u6539\u7684\u4E0D\u53D1,--force \u4E22\u6389\u90A3\u51E0\u5F20\u7167\u53D1;--from \u53D1\u53E6\u4E00\u4EFD(\u5982\u5386\u53F2)\n cotutor home show [--json] \u73B0\u5728\u53D1\u5E03\u7684\u662F\u54EA\u4EFD\u3001\u51E0\u5929\u524D\u3001\u6BCF\u4E2A\u6309\u94AE\u88AB\u70B9\u4E86\u51E0\u6B21(\u54EA\u5929\u3001\u54EA\u4E2A\u8BDD\u9898)\n cotutor activity check [<\u8001\u5E08> [<\u540D>]] [--json] \u68C0\u67E5 activity \u6587\u4EF6(agents/<\u8001\u5E08>/activities/<\u540D>.md):\u5BB6\u957F\u586B\u7684\u3001\u5E94\u7528\u7B97\u7684\u3001\u5E94\u7528\u62E6\u7684\u3001\u88C1\u6BB5\u3001\u63D2\u69FD\u3001\u6B63\u6587\u7528\u7684\u5361\u3001\u7B2C\u4E00\u53E5\u4E0E\u6536(\u5E26\u884C\u53F7);\u4E0D\u82B1\u94B1\n cotutor activity eval <\u8001\u5E08> <\u540D> [--runs <n>] [--only <\u4F8B\u5B50>] [--kid-model <\u5343\u95EE\u6A21\u578B>] [--json] \u7167 <\u540D>.\u4F8B\u5B50.md \u8DD1:\u771F\u8001\u5E08 + \u6A21\u62DF\u5B69\u5B50(\u5343\u95EE,\u7F3A\u7701 qwen3.8-flash,\u6709 Token Plan \u8D70\u5B83)\u591A\u8F6E(\u7167\u5B9E\u65F6\u5199,\u540C\u5B69\u5B50\u7AEF),\u5224\u53EA\u770B\u677F\u4E66;\u4E00\u4E2A\u4F8B\u5B50\u8DD1 n \u6B21(\u7F3A\u7701 3)\u6B21\u6B21\u90FD\u8FC7\u624D\u7B97\u8FC7;\u843D evals/,\u753B\u5BB6\u662F\u5047\u7684;\u82B1\u94B1\n cotutor chalk <check|build|bake|snap|dub> <scene.ts|\u8BFE\u5305 id> [...] \u505A\u5C0F\u8BFE\u5802\u7684\u8BFE\u5305:check \u6821\u9A8C\u573A\u666F\u3001build \u51FA\u8BFE\u5305\u5230 bundles/\u3001bake \u70E4\u753B\u9762\u3001snap \u6BCF\u6B65\u622A\u56FE\u3001dub \u914D\u97F3(cotutor chalk \u770B\u5404\u81EA\u7684\u9009\u9879;bake / snap \u8981\u672C\u673A\u7684 Chrome)\n cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] \u4E0D\u7ECF\u771F\u5B9E\u8001\u5E08\u4E0E\u767E\u70BC,\u7528\u56FA\u5B9A\u7684\u677F\u4E66 JSON \u8D77\u5B69\u5B50\u7AEF,\u6D4B\u524D\u7AEF\u4EA4\u4E92\u4E0E\u6E32\u67D3(\u4E0D\u9700\u8981 workspace;\u8001\u5E08\u90FD\u8D70\u5B9E\u65F6\u8001\u5E08\u9875,\u5047\u5634\u7167\u811A\u672C\u5FF5\u9759\u97F3\u7684\u58F0\u97F3;nokey = \u6CA1\u914D\u767E\u70BC,\u5B69\u5B50\u7AEF\u53EA\u51FA\u300C\u8001\u5E08\u4EEC\u8FD8\u6CA1\u5230\u300D)\n cotutor --version | --help\nworkspace\u89E3\u6790:--workspace > COTUTOR_WORKSPACE > cwd \u6216\u7956\u5148\u6709 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ \u4E0B\u552F\u4E00\u7684\u5B69\u5B50\u76EE\u5F55\n\u8BD5\u7528:\u8981 workspace \u7684\u547D\u4EE4\u52A0 --sandbox,\u6362\u5230\u771F workspace \u65C1\u8FB9\u7684 .sandbox/<\u76EE\u5F55\u540D>/,\u8BD5\u7684\u5BF9\u8BDD\u4E0E vault \u7684\u5199\u5165\u4E0D\u8FDB\u5B69\u5B50\u7684\u8BB0\u5F55(\u7B2C\u4E00\u6B21\u81EA\u52A8\u5206:\u8BBE\u7F6E\u7167\u62F7,\u5BF9\u8BDD\u4E0E\u7167\u7247\u4E0D\u5E26,vault \u62F7\u4E00\u4EFD;\u6807\u9898\u524D\u52A0\u300C\u8BD5\u7528\u300D\u3001\u7AEF\u53E3 +1\u3001\u81EA\u52A8\u8BB0\u8D26\u5173\u6389);--sandbox --fresh \u91CD\u65B0\u5206\n";
@@ -1,16 +1,20 @@
1
1
  /** 命令用法(cotutor --help;技能 cotutor-analyze 的 references/命令与文件.md 从这里取分析用的那几行,所以放单独文件,别的模块 import 不会跑到 CLI) */
2
2
  export const USAGE = `用法:
3
- cotutor init <slug> [--dir <path>] [--name <孩子名>] [--grade <1-6>] [--port <n>] 建 ~/cotutor/<slug>/ 骨架(幂等补缺;默认二年级教学包)
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 层的坑摆出来
5
+ cotutor doctor [--workspace <dir>] [--json] [--live] 逐项体检;--live 真连一次百炼实时模型、真合成一句配音(花一分钱)把 API 层的坑摆出来(真跑老师用 cotutor send);--json 里不过的项带 cmd(能直接跑)或 human(要人来)
6
6
  cotutor upgrade [--workspace <dir>] [--force <老师>]... 老师文件、activity 与 skill 换新版:没改过的直接换,改过的只报 diff(--force <老师> 或 --force <老师>/<名> 才覆盖,原文留 .bak)
7
7
  cotutor upgrade --config [--dry-run] [--workspace <dir>] cotutor.json 补缺:新出厂老师 / 运行时 / 命令模板旗标(只加缺的,你改过的值不动)
8
8
  cotutor add <老师名> --display <显示名> [--subject <学科>] [--avatar <emoji|图片相对路径>] [--hidden] 加一位自家的老师:出模板文件、进 cotutor.json、建目录(名字以 -tutor 结尾的是老师,figshot 给它做头像)
9
9
  cotutor add-theme <主题名> [--from <主题>] [--workspace <dir>] 加一个自家的主题:拷一份(缺省出厂的 default)到 themes/<主题名>/,改 cotutor.json 的 kid.theme 换过去
10
- cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open-qr] 起服务(一 workspace 一进程;~/.config/cotutor/certs/ 有证书就走 HTTPS;--trace 每一轮的事件按道打印;/dev/qr 是扫码页,--open-qr 顺手打开)
10
+ cotutor serve [--workspace <dir>] [--port <n>] [--http] [--trace] [--open] [--log-dir <目录>] 在终端里起服务(一 workspace 一进程;~/.config/cotutor/certs/ 有证书就走 HTTPS;--trace 每一轮的事件按道打印;只打印家长端首页的地址,孩子的平板在那里扫码;--open 顺手打开,旧名 --open-qr 照样认;--log-dir 输出按天落文件,后台服务用;关了终端就停,一直开着用 cotutor service)
11
+ cotutor open [--workspace <dir>] [--sandbox] [--print] [--json] 打开家长端(读 cotutor.json,不用记端口);--sandbox 开试用的那个(端口永远是真的 +1);--print 只打印地址;没在跑就说怎么起,以 1 退
12
+ cotutor service install | uninstall | start | stop | restart [--workspace <dir>] [--no-tray] [--no-open] 把 serve 装成这台电脑的后台服务(macOS launchd、Linux systemd 用户服务、Windows 用户的登录项,都不要管理员):登录后自己起来、崩了 10 秒后再起、日志按天落文件留 14 天;一个 workspace 一个;第一次 install 打开一次家长端(--no-open 不开),以后的启动都不开;换了 node 版本或包挪了地方再 install 一次
13
+ cotutor tray on | off | status [--json] 菜单栏 / 任务栏图标(macOS、Windows;service install 时一起装,--no-tray 不带):正常时只有 logo,有事出橙点,没在跑时变淡加红点并通知一次;点它看状态、打开家长端、重启、看日志、退出;off 关掉并记住
14
+ cotutor service status [--json] | logs [--lines <n>] [--follow] 后台服务在不在跑、端口答不答应、跑的哪个版本、vault 读不读得到、日志在哪;--json 还答平板地址、证书、百炼配没配、图标;logs 看最近的日志
11
15
  cotutor pin [off | status] [--pin <数字>] [--json] 家长端 /parent 与工作台 /dev 的锁:机器级 PIN(所有 workspace 共用),iPad 上输一次、这台电脑免密;没设就全开(家长在旁边可以不设);off 撤掉
12
- cotutor cert [--host <名或IP>]... 用 mkcert 建这台机器的自签证书到 ~/.config/cotutor/certs/(iPad / iPhone 上录音要 HTTPS;所有 workspace 共用)
13
- cotutor send <老师> [<消息>] [--from kid|system] [--runtime <名>] [--new] [--photo a.jpg,b.png] [--activity <名> [--param '名=值;名=值']] [--lane main,tts] [--quiet] 终端里发一条,现场按道打印每道工序的事件,说完打印结果(与页面同一条路;--new 开新话题;--photo 带作业照片,拷进 captures/,有照片时消息可以空;--activity 像孩子点了首页的 activity 按钮,新开话题,参数同按钮下面那几行;--quiet 只要结果)
16
+ cotutor cert [--host <名或IP>]... 用 mkcert 建这台机器的自签证书到 ~/.config/cotutor/certs/(平板、手机上录音要 HTTPS;所有 workspace 共用)
17
+ cotutor send <老师> [<消息>] [--from kid|system] [--runtime <名>] [--new] [--photo a.jpg,b.png] [--activity <名> [--param '名=值;名=值']] [--lane main,tts] [--quiet] 终端里发一条,现场按道打印每道工序的事件,说完打印结果(孩子的话照实时写,同孩子端:上下文包带 live:、不补提问卡,只是没有嘴;--from system 不算;--new 开新话题;--photo 带作业照片,拷进 captures/,有照片时消息可以空;--activity 像孩子点了首页的 activity 按钮,新开话题,参数同按钮下面那几行;--quiet 只要结果)
14
18
  cotutor pack <老师> [<消息>] [--from kid|system] [--at <ISO时间>] [--activity <名> [--param '名=值;名=值']] [--json] 干跑上下文包(--activity 看 activity 话题第一条老师拿到什么):不起模型,打印现在会发给老师的那份 + 每段来自哪个文件、那里一共几条、按政策带了几条(改了档案 / 日记 / 计划立刻看效果)
15
19
  cotutor replay <老师> <job> [<日期>] [--runtime <名>] [--json] 回放一轮:同一问按现在的 vault 与提示词再跑一遍(落 evals/,不进孩子的对话、不配音),跑完并排打印上下文包 / 讲稿 / 卡 / 读了什么的 diff
16
20
  cotutor compare <老师> [<evalJob>] [<日期>] [--json] 再看一次回放的对照(不给 evalJob 就列这天回放过哪些)
@@ -22,9 +26,10 @@ export const USAGE = `用法:
22
26
  cotutor home publish [--force] [--from <文件>] [--json] 发布首页:检查 → 留历史 home/history/<id>.md → 写 home/published.json(孩子端只读它);有要改的不发,--force 丢掉那几张照发;--from 发另一份(如历史)
23
27
  cotutor home show [--json] 现在发布的是哪份、几天前、每个按钮被点了几次(哪天、哪个话题)
24
28
  cotutor activity check [<老师> [<名>]] [--json] 检查 activity 文件(agents/<老师>/activities/<名>.md):家长填的、应用算的、应用拦的、裁段、插槽、正文用的卡、第一句与收(带行号);不花钱
25
- cotutor activity eval <老师> <名> [--runs <n>] [--only <例子>] [--kid-model <千问模型>] [--json] 照 <名>.例子.md 跑:真老师 + 模拟孩子(千问,缺省 qwen3.8-flash,有 Token Plan 走它)多轮,判只看板书;一个例子跑 n 次(缺省 3)次次都过才算过;落 evals/,画家是假的;花钱
29
+ cotutor activity eval <老师> <名> [--runs <n>] [--only <例子>] [--kid-model <千问模型>] [--json] 照 <名>.例子.md 跑:真老师 + 模拟孩子(千问,缺省 qwen3.8-flash,有 Token Plan 走它)多轮(照实时写,同孩子端),判只看板书;一个例子跑 n 次(缺省 3)次次都过才算过;落 evals/,画家是假的;花钱
26
30
  cotutor chalk <check|build|bake|snap|dub> <scene.ts|课包 id> [...] 做小课堂的课包:check 校验场景、build 出课包到 bundles/、bake 烤画面、snap 每步截图、dub 配音(cotutor chalk 看各自的选项;bake / snap 要本机的 Chrome)
27
31
  cotutor mock [--port <n>] [--scenario normal|nokey] [--delay <ms>] [--http] 不经真实老师与百炼,用固定的板书 JSON 起孩子端,测前端交互与渲染(不需要 workspace;老师都走实时老师页,假嘴照脚本念静音的声音;nokey = 没配百炼,孩子端只出「老师们还没到」)
28
32
  cotutor --version | --help
29
33
  workspace解析:--workspace > COTUTOR_WORKSPACE > cwd 或祖先有 cotutor.json > ~/.config/cotutor/config.json > ~/cotutor/ 下唯一的孩子目录
34
+ 试用:要 workspace 的命令加 --sandbox,换到真 workspace 旁边的 .sandbox/<目录名>/,试的对话与 vault 的写入不进孩子的记录(第一次自动分:设置照拷,对话与照片不带,vault 拷一份;标题前加「试用」、端口 +1、自动记账关掉);--sandbox --fresh 重新分
30
35
  `;
@@ -25,6 +25,7 @@
25
25
  import { join } from 'node:path';
26
26
  import { DICTATION_SAY_MAX, DICTATION_WORD_MAX, DICTATION_WORDS_MAX, DictationPropsSchema, dictationEmoji, dictationWordLang } from "../../board/cards/index.js";
27
27
  import { localDate } from "../../shared/time.js";
28
+ import { cardVoice } from "../../schema/index.js";
28
29
  import { readPublished } from "../../runtime/home/store.js";
29
30
  import { tianzigeData } from "../../ui/kid/tianzige.js";
30
31
  import { synthesize } from "../../media/tts.js";
@@ -42,7 +43,7 @@ import { EVENT_KINDS, ID_RE, createPhoto, createSession, listSessions, photoFile
42
43
  /** 真服务的依赖:首页从 home/published.json 读,音色从 cotutor.json,配音缓存在 .cotutor/tts/(和卡的配音同一份) */
43
44
  export function dictationDeps(ctx) {
44
45
  const ws = () => ctx.ws;
45
- const voiced = (t) => Boolean(ws().config.tutors[t]?.voice);
46
+ const voiced = (t) => Boolean(cardVoice(ws().config.tutors, t));
46
47
  return {
47
48
  root: ctx.ws.root,
48
49
  now: ctx.now,
@@ -72,7 +73,7 @@ export function dictationDeps(ctx) {
72
73
  return Boolean(tutor && voiced(tutor));
73
74
  },
74
75
  async say(text, tutor) {
75
- const voice = tutor ? ws().config.tutors[tutor]?.voice : undefined;
76
+ const voice = tutor ? cardVoice(ws().config.tutors, tutor) : undefined;
76
77
  if (!voice)
77
78
  return null;
78
79
  const r = await synthesize({ text, voice }, { cacheDir: ws().dirs.tts, env: process.env });
@@ -7,8 +7,8 @@ import { type ConversationIndex, type ConversationMessage, type TutorExtra } fro
7
7
  import type { BoardCard, Device } from '../../board/presentation.ts';
8
8
  import type { Workspace } from '../../schema/index.ts';
9
9
  import { type Access, type Who } from './access.ts';
10
- import { type ListenInfo } from '../../ui/dev/qr-page.ts';
11
- import { Runner, type SendInput } from '../../runtime/runner.ts';
10
+ import { type ListenInfo } from '../../ui/parent/qr-page.ts';
11
+ import { Runner } from '../../runtime/runner.ts';
12
12
  import { type Bailian } from '../machine/bailian.ts';
13
13
  import { type VaultWrite } from '../../runtime/journal/store.ts';
14
14
  import type { Draw } from '../../media/draw.ts';
@@ -35,6 +35,8 @@ export interface AppContext {
35
35
  configError: string | null;
36
36
  /** 在哪个地址上听着(serve 在 listen 之后填;扫码页 /qr 每次现问——局域网 IP 会在服务跑着的时候变)。没起端口(测试走 route)→ null */
37
37
  listen: (() => ListenInfo) | null;
38
+ /** 现在开着几通电话(serve 接 ws 那边的数;没起端口 → null) */
39
+ openCalls: (() => number) | null;
38
40
  /** 重读 cotutor.json(改了才读) */
39
41
  reload(): Promise<void>;
40
42
  /** 家长端与工作台的锁(access.ts):机器级 PIN,没设就全开;测试可注入 */
@@ -45,7 +47,6 @@ export interface AppContext {
45
47
  export declare function createContext(ws: Workspace, opts?: {
46
48
  now?: () => Date;
47
49
  env?: NodeJS.ProcessEnv;
48
- warm?: boolean;
49
50
  draw?: Draw;
50
51
  access?: Access;
51
52
  bailian?: Bailian;
@@ -89,7 +90,7 @@ export interface KidHome {
89
90
  figshot: {
90
91
  port: number;
91
92
  } | null;
92
- /** 孩子的惯用手(cotutor.json 的 kid.hand,缺省 right):left = 平板横屏实时老师页的浮层挪到右边 */
93
+ /** 按住说话那一栏在哪边(cotutor.json 的 kid.hand,家长端设置页改;缺省 right = 在左边):left = 平板横屏实时老师页的那一栏挪到右边 */
93
94
  hand: 'right' | 'left';
94
95
  /** 百炼配好了:没配孩子端只出「老师们还没到」,家长端配 */
95
96
  ready: boolean;
@@ -115,12 +116,6 @@ export interface KidHistory {
115
116
  }
116
117
  export declare function kidHistory(ctx: AppContext, tutor: string, days: number): Promise<KidHistory>;
117
118
  export declare function kidDay(ctx: AppContext, tutor: string, date: string): Promise<KidDay>;
118
- /** 孩子端发一条的验与拼(旧入口,测试用):Runner.accept(./kid-input.ts)的结果换成路由的形状 */
119
- export declare function kidSendInput(ctx: AppContext, tutor: string, body: unknown, date: string, deviceId?: string | null): Promise<{
120
- input: SendInput;
121
- } | {
122
- error: RouteResult;
123
- }>;
124
119
  /** 家长板书页的一天:形状同 KidDay;答案不剥,系统发的也在 */
125
120
  export interface ParentDay {
126
121
  tutor: string;
@@ -3,14 +3,13 @@
3
3
  * R1 的查询接口照旧;R2 加:对话(列日期、看一天的家长视图、发消息)、cotutor.json 补丁(老师团页)、工作台 /dev(2026-09-22 之前叫家长页 /parent;
4
4
  * 现在 /parent 是家长端 = 家长板书页,给不懂技术的家长日常用)。
5
5
  * R3 加:孩子端 `/`(kidPage)与 /api/kid/*(首页:课程表 + 老师卡 + 家长发布的首页卡;对话:服务端过滤后的孩子视图;发消息:from 固定 kid)、配音文件 /api/audio。
6
- * 配置热重载:每个请求先看 cotutor.json 的 mtime,改了就重读;改坏了留旧配置并把错误挂在 /api/health 上。
6
+ * 配置热重载:每个请求先读一遍 cotutor.json,原文变了就重读(不比修改时间,Windows 上连写两次常常一样);改坏了留旧配置并把错误挂在 /api/health 上。
7
7
  */
8
- import { createHash } from 'node:crypto';
9
8
  import { existsSync } from 'node:fs';
10
- import { appendFile, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
9
+ import { appendFile, mkdir, readFile, readdir, stat } from 'node:fs/promises';
11
10
  import { join } from 'node:path';
12
11
  import { foldRuns } from "../../brain/transcript.js";
13
- import { RuntimeError } from "../../brain/cli/run-plan.js";
12
+ import { RuntimeError } from "../../brain/index.js";
14
13
  import { PHOTO_MAX_SIDE } from "../../ui/kid/photo-edit.js";
15
14
  import { currentThread, lastJobOf, sameRuntime } from "../../runtime/journal/conversation.js";
16
15
  import { localDate, weekOf } from "../../shared/time.js";
@@ -21,9 +20,7 @@ import {} from "../../schema/index.js";
21
20
  import { NEUTRAL_COLOR, SUBJECT_COLORS, TUTOR_PALETTE, tutorColors } from "../../ui/shared/tutor-color.js";
22
21
  import { boardMarkOf } from "../../ui/kid/board-mark.js";
23
22
  import { buildReel, playRecordOk, reelCardTook, reelHolds, reelMissRuns } from "../../ui/kid/reel.js";
24
- import { isRealtimeVoice, REALTIME_VOICES } from "../../realtime/voices.js";
25
- import { previewVoice } from "../../realtime/preview.js";
26
- import { DATE_RE, FocusSchema, MESSAGE_FROM, MOUTH_VOICE, listTutors, mouthVoice, subjectLang } from "../../schema/index.js";
23
+ import { DATE_RE, FocusSchema, MESSAGE_FROM, listTutors, subjectLang, viaSendsLabel } from "../../schema/index.js";
27
24
  import { ConfigError, patchConfig, reloadIfChanged, workspaceReport } from "../workspace.js";
28
25
  import { UsageError } from "../../shared/errors.js";
29
26
  import { redactHome } from "../../shared/home.js";
@@ -32,10 +29,13 @@ import { configGapsOf, upgradeConfig } from "../cli/migrate.js";
32
29
  import { ICON_SIZES, appIconPng, webManifest } from "../../ui/shared/icon.js";
33
30
  import { faviconResponse } from "../../ui/shared/favicon.js";
34
31
  import { cookieOf, createAccess, gatePage, isKidPath } from "./access.js";
32
+ import { factoryAvatarFile, shownAvatar } from "./avatars.js";
35
33
  import { deviceCookie, newDeviceId, parseKidLog, readDevices, recordKidLog, renameDevice, validDevice } from "../../runtime/journal/kid-log.js";
36
34
  import { DEVICE_COOKIE } from "../../schema/kid-device.js";
37
35
  import { BUILD, codeChanged, headNow, shortCommit } from "../build.js";
38
- import { qrPage } from "../../ui/dev/qr-page.js";
36
+ import { knownLatest } from "../machine/update.js";
37
+ import { isNewer } from "../../shared/upgrade.js";
38
+ import { qrPage } from "../../ui/parent/qr-page.js";
39
39
  import { kidPage } from "../../ui/kid/kid-page.js";
40
40
  import { parentPage } from "../../ui/parent/parent-page.js";
41
41
  import { kidHomeView } from "../../runtime/home/store.js";
@@ -50,14 +50,16 @@ import { BusyError, Runner } from "../../runtime/runner.js";
50
50
  import { sendFile } from "./send-file.js";
51
51
  import { talkRoute } from "../talk/route.js";
52
52
  import { parentHomeRoute } from "./parent-home.js";
53
+ import { parentLookRoute } from "./parent-look.js";
53
54
  import { bailianRoute, createBailian } from "../machine/bailian.js";
54
55
  import { PARENT_HOME_PAGE } from "../../ui/parent/parent-home-page.js";
56
+ import { PARENT_SETTINGS_PAGE } from "../../ui/parent/parent-settings-page.js";
55
57
  import { dictationRoute } from "../dictation/route.js";
56
58
  import { IndexError, capturePathOk, deleteThread, listDates, rateThread, readErrLog, readIndex, readTranscript, scanCards, undoVaultWrite, writeCapture, writeCardImage, writeCardState } from "../../runtime/journal/store.js";
57
59
  import { memoryLineText } from "../../memory/vault-notes.js";
58
60
  import { childItem } from "../../memory/proposals.js";
59
61
  import { IMAGE_EXT, parseCardState, stripSecrets } from "../../board/cards/index.js";
60
- import { resolve, sep } from 'node:path';
62
+ import { isAbsolute, resolve, sep } from 'node:path';
61
63
  import { bundleAsset, stageAsset, stageIndex, stageVersion } from "./stage.js";
62
64
  import { themeFiles } from "./theme.js";
63
65
  import { enrichPaints } from "../../scenes/paint/painter.js";
@@ -67,24 +69,24 @@ import { lettersData } from "../../ui/kid/letters.js";
67
69
  import { fixtureOf, rawView } from "./raw-view.js";
68
70
  import { DeviceSchema } from "../../schema/index.js";
69
71
  import { CanvasStateSchema } from "../../board/cards/canvas.js";
70
- import { PAPER_KEY_RE, adoptPaper, dropPaper, readPaper, writePaper } from "../../board/paper.js";
71
- import { synthesize, TTS_VOICES } from "../../media/tts.js";
72
+ import { PAPER_KEY_RE, dropPaper, readPaper, writePaper } from "../../board/paper.js";
72
73
  import { addTutorFile, readTutorFile, removeTutorFile, writeTutorFile } from "../cli/tutors.js";
73
74
  export function createContext(ws, opts = {}) {
74
- let mtime = -1;
75
+ let configText = null;
75
76
  const ctx = {
76
77
  ws,
77
78
  runner: new Runner(() => ctx.ws, opts),
78
79
  now: opts.now ?? (() => new Date()),
79
80
  configError: null,
80
81
  listen: null,
82
+ openCalls: null,
81
83
  access: opts.access ?? createAccess({ now: () => ctx.now() }),
82
84
  bailian: opts.bailian ?? createBailian({ env: opts.env }),
83
85
  async reload() {
84
86
  try {
85
- const r = await reloadIfChanged(ctx.ws, mtime);
87
+ const r = await reloadIfChanged(ctx.ws, configText);
86
88
  ctx.ws = r.ws;
87
- mtime = r.mtime;
89
+ configText = r.text;
88
90
  ctx.configError = null;
89
91
  }
90
92
  catch (err) {
@@ -102,7 +104,7 @@ export async function dayView(ctx, tutor, date) {
102
104
  const runs = {};
103
105
  const errors = {};
104
106
  for (const m of index.messages) {
105
- const t = await readTranscript(ctx.ws, tutor, date, m.job, m.runtime);
107
+ const t = await readTranscript(ctx.ws, tutor, date, m.job);
106
108
  runs[m.job] = t ? foldRuns(t.items) : [];
107
109
  if (m.result === 'error') {
108
110
  const tail = (await readErrLog(ctx.ws, tutor, date, m.job)).trim().split('\n').slice(-5).join('\n');
@@ -118,7 +120,7 @@ export async function kidTutors(ctx) {
118
120
  const out = [];
119
121
  const colors = tutorColors(ctx.ws.config.tutors);
120
122
  for (const t of listTutors(ctx.ws.config, { kidOnly: true })) {
121
- out.push({ name: t.name, display: t.display, avatar: t.avatar ?? null, subject: t.subject ?? null, color: colors[t.name], mark: boardMarkOf(t.subject), lang: subjectLang(t.subject), hasVoice: Boolean(t.voice), extras: t.extras.filter((x) => (x !== 'talk' || ctx.ws.config.talk.enabled)), listen: ctx.ws.config.live.listen });
123
+ out.push({ name: t.name, display: t.display, avatar: shownAvatar(t), subject: t.subject ?? null, color: colors[t.name], mark: boardMarkOf(t.subject), lang: subjectLang(t.subject), hasVoice: Boolean(t.voice), extras: t.extras.filter((x) => (x !== 'talk' || ctx.ws.config.talk.enabled)), listen: ctx.ws.config.live.listen });
122
124
  }
123
125
  return out;
124
126
  }
@@ -162,11 +164,6 @@ export async function kidDay(ctx, tutor, date) {
162
164
  }
163
165
  return { tutor, date, messages, pending: active && active.date === date ? active.job : null, thread: currentThread(index) };
164
166
  }
165
- /** 孩子端发一条的验与拼(旧入口,测试用):Runner.accept(./kid-input.ts)的结果换成路由的形状 */
166
- export async function kidSendInput(ctx, tutor, body, date, deviceId) {
167
- const r = await ctx.runner.accept(tutor, body, date, deviceId);
168
- return 'reject' in r ? { error: { status: r.reject.status, json: { error: r.reject.error, ...(r.reject.message ? { message: r.reject.message } : {}) } } } : r;
169
- }
170
167
  export async function parentDay(ctx, tutor, date) {
171
168
  const index = await readIndex(ctx.ws, tutor, date);
172
169
  const active = ctx.runner.running(tutor);
@@ -249,7 +246,7 @@ export async function parentReel(ctx, tutor, date, thread) {
249
246
  const kid = await kidDay(ctx, tutor, date);
250
247
  const device = [...mine].reverse().find((m) => m.device)?.device ?? null;
251
248
  const t = ctx.ws.config.tutors[tutor];
252
- const face = { name: tutor, display: t?.display ?? tutor, avatar: t?.avatar ?? null, subject: t?.subject ?? null, color: tutorColors(ctx.ws.config.tutors)[tutor] ?? NEUTRAL_COLOR, mark: boardMarkOf(t?.subject) };
249
+ const face = { name: tutor, display: t?.display ?? tutor, avatar: t ? shownAvatar(t) : null, subject: t?.subject ?? null, color: tutorColors(ctx.ws.config.tutors)[tutor] ?? NEUTRAL_COLOR, mark: boardMarkOf(t?.subject) };
253
250
  const live = date === localDate(ctx.now()) || mine.some((m) => m.result === 'running');
254
251
  return { tutor, date, thread, reel, messages: day.messages.filter((m) => m.thread === thread), kid: kid.messages.filter((m) => m.thread === thread), device, face, live, realtime: mine.some((m) => m.live) };
255
252
  }
@@ -316,7 +313,7 @@ export async function overview(ctx, date) {
316
313
  if (m.lecture && !th.lecture)
317
314
  th.lecture = { bundle: m.lecture.bundle, title: m.lecture.title, ...(m.lecture.video ? { video: true } : {}), watchedMs: m.lecture.watchedMs, finished: m.lecture.finished, marks: (m.lecture.marks ?? []).length };
318
315
  // 孩子自己说的第一句:首页开场 / 接着按钮发出去的那句(正文就是按钮上的字)不算
319
- if (th.said === null && m.from === 'kid' && m.text.trim() && !(typeof m.via?.button === 'number' && m.via.label === m.text))
316
+ if (th.said === null && m.from === 'kid' && m.text.trim() && !(m.via && viaSendsLabel(m.via) && m.via.label === m.text))
320
317
  th.said = Array.from(m.text.trim()).slice(0, 30).join('');
321
318
  const t0 = Date.parse(m.timing?.startedAt ?? '');
322
319
  const dt = m.timing?.doneMs;
@@ -357,7 +354,7 @@ export async function overview(ctx, date) {
357
354
  th.misses = n;
358
355
  }
359
356
  const booking = index.messages.some((m) => (m.bookkeep || m.tidy) && m.result === 'running');
360
- tutors.push({ name: t.name, display: t.display, avatar: t.avatar ?? null, subject: t.subject ?? null, color: colors[t.name] ?? NEUTRAL_COLOR, turns: index.messages.length, threads: [...by.values()], booking, extras: t.extras.filter((x) => (x !== 'talk' || ctx.ws.config.talk.enabled)) });
357
+ tutors.push({ name: t.name, display: t.display, avatar: shownAvatar(t), subject: t.subject ?? null, color: colors[t.name] ?? NEUTRAL_COLOR, turns: index.messages.length, threads: [...by.values()], booking, extras: t.extras.filter((x) => (x !== 'talk' || ctx.ws.config.talk.enabled)) });
361
358
  }
362
359
  const today = localDate(ctx.now());
363
360
  const names = tutors.map((t) => t.name);
@@ -416,9 +413,6 @@ const MOUTH_FILE_RE = /^\d{4}-\d{2}-\d{2}\.\d{4}-\d+\.mouth\.wav$/;
416
413
  const IMAGE_TYPES = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif', svg: 'image/svg+xml' };
417
414
  /** 作业照片一张最多这么大(解码后;页面先缩到长边 PHOTO_MAX_SIDE 的 jpeg,通常几百 KB) */
418
415
  export const PHOTO_MAX_BYTES = 3_000_000;
419
- /** 音色页的试听句;与 tutors.*.voice 里合法的 id 形状(qwen-audio-3.0-tts-plus-xxx) */
420
- const PREVIEW_TEXT = '你好呀,我是你的老师。今天我们一起来学一个新东西,准备好了吗?';
421
- const VOICE_ID_RE = /^[A-Za-z0-9._:-]{1,120}$/;
422
416
  /**
423
417
  * 上传作业照片(R5):body {image: "data:image/jpeg;base64,…"}(也认 png)→ 落 captures/<日期>/<HHMM>-<n>.jpg → {path}。
424
418
  * 孩子端与家长端同一条路;照片只是落盘,发消息时把 path 放进 photos[] 才到老师那里。
@@ -457,11 +451,6 @@ async function paperRoute(ctx, tutor, date, method, url, body) {
457
451
  await dropPaper(ws, tutor, date, 'new');
458
452
  return { status: 200, json: { ok: true, key } };
459
453
  }
460
- /** 页面拉今天时的预热参数:?thread= 是页面选着的话题;同一话题 30 秒看一次 */
461
- function warmOpts(url) {
462
- const thread = url.searchParams.get('thread');
463
- return { throttleMs: 30_000, ...(thread ? { thread } : {}) };
464
- }
465
454
  /** who:请求是谁发的(createHandler 按来源地址与 cookie 算);直接调 route 的测试与 CLI 缺省 local = 本机免密 */
466
455
  export async function route(method, path, ctx, body, who = 'local', client = {}) {
467
456
  await ctx.reload();
@@ -469,10 +458,12 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
469
458
  const url = new URL(path, 'http://x');
470
459
  const p = url.pathname;
471
460
  try {
472
- // 服务起来的时刻、版本、跑的是哪个提交;仓里的提交变了 = 代码更新了,重起才用上(build.ts)
461
+ // 服务起来的时刻、版本、跑的是哪个提交;仓里的提交变了 = 代码更新了,重起才用上(build.ts)。
462
+ // pid、mode(后台服务 service / 终端里 foreground)、vault 读不读得到(macOS 上后台服务读「文稿」、iCloud 云盘里的 vault 会被系统拦,没有弹窗):cotutor service status 看
473
463
  if (p === '/api/health') {
474
464
  const head = await headNow();
475
- return { status: 200, json: { ok: true, configError: ctx.configError, started: BUILD.started, version: BUILD.version, commit: shortCommit(BUILD.commit), dirty: BUILD.dirty, head: shortCommit(head), changed: codeChanged(BUILD.commit, head) } };
465
+ const vault = await readdir(ws.paths.vault).then(() => 'ok', (err) => (err.code === 'EPERM' || err.code === 'EACCES' ? 'denied' : 'missing'));
466
+ return { status: 200, json: { ok: true, configError: ctx.configError, started: BUILD.started, version: BUILD.version, commit: shortCommit(BUILD.commit), dirty: BUILD.dirty, head: shortCommit(head), changed: codeChanged(BUILD.commit, head), pid: process.pid, mode: process.env.COTUTOR_SERVICE ? 'service' : 'foreground', vault, calls: ctx.openCalls?.() ?? 0, bailian: ctx.bailian.status().ready, latest: isNewer(knownLatest(), BUILD.version) ? knownLatest() : null } };
476
467
  }
477
468
  // 家长端与工作台的锁(access.ts):对 PIN 换 cookie;设了 PIN、没凭证的设备,孩子端以外的页面回输 PIN 的页、接口回 401。没设 PIN 什么都不拦
478
469
  if (p === '/api/auth') {
@@ -592,7 +583,7 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
592
583
  policyDefaults: ws.config.policyDefaults,
593
584
  paths: ws.config.paths,
594
585
  resolvedPaths: Object.fromEntries(Object.entries(ws.paths).map(([k, v]) => [k, redactHome(v)])),
595
- /** 实时老师:总开关、嘴的模型、哪几位老师走实时用哪个音色(工作台音色页「实时的嘴」改) */
586
+ /** 实时老师:总开关、嘴的模型、哪几位老师走实时用哪个音色(家长端「样子与声音」改) */
596
587
  live: ws.config.live,
597
588
  theme: ws.config.kid.theme,
598
589
  https: ws.config.server.https ?? null,
@@ -611,9 +602,8 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
611
602
  if (method === 'PATCH') {
612
603
  if (!isObj(body))
613
604
  return { status: 400, json: { error: 'bad_request', message: '要一个 JSON 对象' } };
614
- // 工作台能改孩子那段的只有 kid.hand(左撇子镜像);kid 的别的字段照旧直接编辑文件
615
- const handOnly = isObj(body.kid) && Object.keys(body.kid).length > 0 && Object.keys(body.kid).every((k) => k === 'hand');
616
- await patchConfig(ws, body, handOnly ? { also: ['kid'] } : {});
605
+ // 孩子那段(kid)工作台不改:按住说话在哪边在家长端设置页(/api/parent/settings),别的直接编辑文件
606
+ await patchConfig(ws, body);
617
607
  await ctx.reload();
618
608
  return { status: 200, json: { ok: true, tutors: listTutors(ctx.ws.config), runtime: ctx.ws.config.runtimes.default } };
619
609
  }
@@ -725,8 +715,6 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
725
715
  if (tail === 'paper')
726
716
  return paperRoute(ctx, tutor, date, method, url, body);
727
717
  if (tail === 'today' && method === 'GET') {
728
- // 孩子点进这位老师 / 选了话题(页面轮询也走这里,同一话题 30 秒看一次):把下一轮的老师进程提前起好
729
- void ctx.runner.prewarm(tutor, warmOpts(url)).catch(() => { });
730
718
  return { status: 200, json: await kidDay(ctx, tutor, date) };
731
719
  }
732
720
  // 以前的某一天(只读回放;未来的日期与坏日期 400)
@@ -739,15 +727,7 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
739
727
  const days = Math.min(365, Math.max(1, Number(url.searchParams.get('days') ?? 30) || 30));
740
728
  return { status: 200, json: await kidHistory(ctx, tutor, days) };
741
729
  }
742
- if (tail === 'messages' && method === 'POST') {
743
- const r = await kidSendInput(ctx, tutor, body, date, client.device);
744
- if ('error' in r)
745
- return r.error;
746
- const started = await ctx.runner.send(tutor, r.input);
747
- if (started.thread === started.job)
748
- void adoptPaper(ws, tutor, started.date, started.thread);
749
- return { status: 202, json: { tutor, date: started.date, job: started.job, thread: started.thread } };
750
- }
730
+ // 孩子端发一条只走实时那条连接(/api/live/<老师>/ws,src/runtime/live/turns.ts);2026-11-04 删了 HTTP 的发
751
731
  return { status: 405, json: { error: 'method_not_allowed' } };
752
732
  }
753
733
  // 字幕重放:实时那一轮嘴照念的声音在哪、多长、讲稿每句从哪开始(毫秒);只给实时的轮,没落过声音 404
@@ -787,13 +767,17 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
787
767
  return putCardState(ctx, ws, tutor, job, Number(nStr), body);
788
768
  }
789
769
  // 老师头像(R5b,2026-09-16):cotutor.json 里 avatar 是图片相对路径时(figshot 写的 avatars/<老师>.png)从这里取;
770
+ // 没写(或还是旧的出厂 emoji)的三门主科给包里的出厂头像(avatars.ts);
790
771
  // emoji 头像、越界、不是图、不存在都 404(孩子端退回显示 emoji / 首字)。no-cache:figshot 换了脸孩子端要马上见到
791
772
  const av = /^\/api\/kid\/avatar\/([a-z0-9][a-z0-9-]*)$/.exec(p);
792
773
  if (av && method === 'GET') {
774
+ const factory = ws.config.tutors[av[1]] ? factoryAvatarFile(ws.config.tutors[av[1]]) : null;
775
+ if (factory)
776
+ return { status: 200, file: factory, contentType: 'image/webp', cacheControl: 'no-cache' };
793
777
  const rel = ws.config.tutors[av[1]]?.avatar ?? '';
794
778
  const file = resolve(ws.root, rel);
795
779
  const ext = IMAGE_EXT.exec(rel)?.[1]?.toLowerCase() ?? '';
796
- if (!rel || rel.startsWith('/') || !file.startsWith(ws.root + sep) || !IMAGE_TYPES[ext])
780
+ if (!rel || isAbsolute(rel) || !file.startsWith(ws.root + sep) || !IMAGE_TYPES[ext])
797
781
  return { status: 404, json: { error: 'not_found' } };
798
782
  if (!(await stat(file).catch(() => null))?.isFile())
799
783
  return { status: 404, json: { error: 'not_found' } };
@@ -826,7 +810,7 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
826
810
  const rel = url.searchParams.get('p') ?? '';
827
811
  const file = resolve(ws.root, rel);
828
812
  const ext = IMAGE_EXT.exec(rel)?.[1]?.toLowerCase() ?? '';
829
- if (!rel || rel.startsWith('/') || !file.startsWith(ws.root + sep) || !IMAGE_TYPES[ext])
813
+ if (!rel || isAbsolute(rel) || !file.startsWith(ws.root + sep) || !IMAGE_TYPES[ext])
830
814
  return { status: 404, json: { error: 'not_found' } };
831
815
  if (!(await stat(file).catch(() => null))?.isFile())
832
816
  return { status: 404, json: { error: 'not_found' } };
@@ -857,49 +841,6 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
857
841
  return { status: 404, json: { error: 'not_found', message: `${d} 没有 ${job} 这一轮` } };
858
842
  return { status: 200, json: fixture ? fixtureOf(view) : view };
859
843
  }
860
- // 音色页(2026-09-17):卡的配音认的全部音色(src/media/tts-voices.ts),家长挑之前先听
861
- if (p === '/api/tts/voices' && method === 'GET') {
862
- const inUse = {};
863
- for (const [name, t] of Object.entries(ws.config.tutors))
864
- if (t.voice)
865
- (inUse[t.voice] ??= []).push(name);
866
- return { status: 200, json: { ok: true, count: TTS_VOICES.length, voices: TTS_VOICES, inUse, sample: PREVIEW_TEXT } };
867
- }
868
- // 嘴的音色(工作台音色页「实时的嘴」):这个 realtime 模型认的 56 个(src/realtime/voices.ts)、谁在用(没挑的用缺省)
869
- if (p === '/api/tts/realtime' && method === 'GET') {
870
- const inUse = {};
871
- for (const t of listTutors(ws.config, { kidOnly: true }))
872
- (inUse[mouthVoice(ws.config.live, t.name)] ??= []).push(t.name);
873
- return { status: 200, json: { voices: REALTIME_VOICES, inUse, fallback: MOUTH_VOICE, model: ws.config.live.model, sample: PREVIEW_TEXT } };
874
- }
875
- // 试听嘴的一个音色:真连一次 realtime 照念这一句(几分钱),存在 .cotutor/live-preview/,同一句同一音色以后直接给文件
876
- if (p === '/api/tts/realtime/preview' && method === 'GET') {
877
- const voice = url.searchParams.get('voice')?.trim() ?? '';
878
- if (!isRealtimeVoice(voice))
879
- return { status: 400, json: { error: 'bad_voice', message: `这个模型不认「${voice}」这个音色` } };
880
- const text = url.searchParams.get('text')?.trim().slice(0, 200) || PREVIEW_TEXT;
881
- const dir = join(ws.root, '.cotutor', 'live-preview');
882
- const file = join(dir, `${createHash('sha1').update(`${ws.config.live.model}\n${voice}\n${text}`).digest('hex').slice(0, 20)}.wav`);
883
- if (!(await stat(file).catch(() => null))?.isFile()) {
884
- const r = await previewVoice({ voice, text, model: ws.config.live.model, env: process.env, endpoint: process.env.COTUTOR_LIVE_ENDPOINT });
885
- if ('error' in r)
886
- return { status: 502, json: { error: 'realtime_failed', voice, message: r.error } };
887
- await mkdir(dir, { recursive: true });
888
- await writeFile(file, r.wav);
889
- }
890
- return { status: 200, file, contentType: 'audio/wav' };
891
- }
892
- // 试听一个音色:同一句同一音色合成一次就在配音缓存里(.cotutor/tts/),之后直接给文件;失败回 JSON 说原因(页面拿 message 显示)
893
- if (p === '/api/tts/preview' && method === 'GET') {
894
- const voice = url.searchParams.get('voice')?.trim() ?? '';
895
- if (!VOICE_ID_RE.test(voice))
896
- return { status: 400, json: { error: 'bad_voice', message: '音色 id 只能是字母数字与 . _ : -' } };
897
- const text = url.searchParams.get('text')?.trim().slice(0, 200) || PREVIEW_TEXT;
898
- const r = await synthesize({ text, voice }, { cacheDir: ws.dirs.tts, env: process.env });
899
- if (!r.file)
900
- return { status: 502, json: { error: 'tts_failed', voice, message: r.error } };
901
- return { status: 200, file: r.file, contentType: 'audio/mpeg' };
902
- }
903
844
  // 话题打星(《obsidian仓库设计.md》§4):PUT {rating: 1–5 | null};记账:POST {threads?: [..]} → 每个话题一轮记账任务,老师回「## 记账」段,应用写日记
904
845
  const rate = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/threads\/([^/]+)\/rating$/.exec(p);
905
846
  if (rate && method === 'PUT') {
@@ -1045,6 +986,8 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
1045
986
  thread: typeof body.thread === 'string' ? body.thread : undefined,
1046
987
  photos,
1047
988
  device: device?.data,
989
+ // 孩子的话照实时写(同孩子端、同 replay:上下文包带 live:、不补提问卡、讲稿不截);嘴不在,接住与打断不带。系统发的不算(2026-11-04)
990
+ ...(from === 'kid' ? { live: {} } : {}),
1048
991
  });
1049
992
  return { status: 202, json: { tutor, date: started.date, job: started.job, thread: started.thread, runtime: started.plan.runtime, resume: started.plan.resume } };
1050
993
  }
@@ -1086,6 +1029,9 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
1086
1029
  const phr = await parentHomeRoute(method, p, ctx, body);
1087
1030
  if (phr)
1088
1031
  return phr;
1032
+ const plr = await parentLookRoute(method, p, url, ctx, body);
1033
+ if (plr)
1034
+ return plr;
1089
1035
  const blr = await bailianRoute(method, p, ctx, body, who);
1090
1036
  if (blr)
1091
1037
  return blr;
@@ -1099,12 +1045,9 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
1099
1045
  return tx;
1100
1046
  if (method !== 'GET')
1101
1047
  return { status: 405, json: { error: 'method_not_allowed' } };
1102
- // 工作台:对话原始视图、看原文、老师团、音色、设置、首页排版——给有技术背景的家长与开发者;家长端在 /parent
1048
+ // 工作台:对话原始视图、看原文、老师团、activity、设置——给有技术背景的家长与开发者;家长端在 /parent
1103
1049
  if (p === '/dev')
1104
1050
  return { status: 200, html: DEV_PAGE };
1105
- // 扫码页:在电脑上打开,iPad 用相机扫(不在孩子端的入口里)
1106
- if (p === '/dev/qr')
1107
- return ctx.listen ? { status: 200, html: qrPage(ws.config.title, ctx.listen(), url.searchParams.get('via') === 'ip' ? 'ip' : 'name', url.searchParams.get('to') === 'parent' ? 'parent' : 'kid') } : { status: 404, json: { error: 'not_listening' } };
1108
1051
  // 按住说话的试验页(真机上比策略用;不在孩子端与家长端的入口里)
1109
1052
  // 按钮样张:老师页那颗「头像 + 按住说话」的每一种样子,调动画时看这一页
1110
1053
  if (p === '/dev/voice')
@@ -1131,6 +1074,12 @@ export async function route(method, path, ctx, body, who = 'local', client = {})
1131
1074
  // 家长端「首页」(设孩子的首页与几项常改的设置)
1132
1075
  if (p === '/parent/home')
1133
1076
  return { status: 200, html: PARENT_HOME_PAGE };
1077
+ // 家长端「设置」(设一次就不常动的:孩子的平板上按住说话那一栏放哪边)
1078
+ if (p === '/parent/settings')
1079
+ return { status: 200, html: PARENT_SETTINGS_PAGE };
1080
+ // 二维码的打印页:家长端「扫码打开」的「打印 ›」开它(不在孩子端的入口里)
1081
+ if (p === '/parent/qr')
1082
+ return ctx.listen ? { status: 200, html: qrPage(ws.config.title, ctx.listen(), url.searchParams.get('via') === 'ip' ? 'ip' : 'name', url.searchParams.get('to') === 'parent' ? 'parent' : 'kid') } : { status: 404, json: { error: 'not_listening' } };
1134
1083
  // 录像里孩子的屏幕:家长端录像时开在 iframe 里;直接打开也能看(调试),?tutor=&date=&thread=[&at=时:分:秒]
1135
1084
  if (p === '/reel')
1136
1085
  return { status: 200, html: kidPage(esc(ws.config.title), { reel: true }, await stageVersion()) };
@@ -0,0 +1,17 @@
1
+ /** 本包自带的头像目录(仓库检出与 npm 安装都在包根 avatars/) */
2
+ export declare const PACKAGE_AVATARS_DIR: string;
3
+ /** 下发给页面的出厂头像名的前缀(不是 workspace 里的路径) */
4
+ export declare const FACTORY_PREFIX = "factory:";
5
+ type AvatarOf = {
6
+ avatar?: string;
7
+ subject?: string;
8
+ };
9
+ /** 这门学科的出厂头像在包里的文件(不管这位老师现在用不用);不是三门主科 → null */
10
+ export declare function subjectAvatarFile(subject: string | undefined): string | null;
11
+ /** cotutor.json 里家长定的头像:没写或还是旧版出厂的 emoji → null(用出厂的) */
12
+ export declare const ownAvatar: (t: AvatarOf) => string | null;
13
+ /** 出厂头像在包里的文件;不用出厂的 → null */
14
+ export declare function factoryAvatarFile(t: AvatarOf): string | null;
15
+ /** 下发给页面的 avatar:出厂的给「factory:<文件名>」,家长写的原样,都没有 null(页面显示学科首字) */
16
+ export declare function shownAvatar(t: AvatarOf): string | null;
17
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 老师的出厂头像:三门主科各一张木钉娃娃(包根 avatars/<学科>.webp,出处与重渲见 scripts/avatars/peg.py)。
3
+ * cotutor.json 里这位老师没写 avatar,或还是旧版 init 写进去的那个 emoji(🧮 📚 🔤),就用它;家长写了别的(emoji 或图片路径)照家长的。
4
+ * 不拷进 workspace:图跟着包走,换了包就换了图。页面按 avatar 的后缀判定是不是图,所以下发的是一个带后缀的名字(FACTORY_PREFIX 打头),
5
+ * 图本身和 figshot 的一样从 /api/kid/avatar/<老师> 取。
6
+ */
7
+ import { join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { canonicalSubject } from "../../schema/index.js";
10
+ /** 本包自带的头像目录(仓库检出与 npm 安装都在包根 avatars/) */
11
+ export const PACKAGE_AVATARS_DIR = fileURLToPath(new URL('../../../avatars/', import.meta.url));
12
+ const SUBJECT_AVATARS = { 语文: 'chinese.webp', 数学: 'math.webp', 英语: 'english.webp' };
13
+ /** 2026-11-02 以前 init 写进 cotutor.json 的出厂 emoji:当作没写 */
14
+ const OLD_FACTORY_EMOJI = new Set(['📚', '🧮', '🔤']);
15
+ /** 下发给页面的出厂头像名的前缀(不是 workspace 里的路径) */
16
+ export const FACTORY_PREFIX = 'factory:';
17
+ /** 用出厂头像的话是哪张(文件名);家长自己定了、或不是三门主科 → null */
18
+ const factoryName = (t) => (t.avatar && !OLD_FACTORY_EMOJI.has(t.avatar) ? null : SUBJECT_AVATARS[canonicalSubject(t.subject) ?? ''] ?? null);
19
+ /** 这门学科的出厂头像在包里的文件(不管这位老师现在用不用);不是三门主科 → null */
20
+ export function subjectAvatarFile(subject) {
21
+ const f = SUBJECT_AVATARS[canonicalSubject(subject) ?? ''];
22
+ return f ? join(PACKAGE_AVATARS_DIR, f) : null;
23
+ }
24
+ /** cotutor.json 里家长定的头像:没写或还是旧版出厂的 emoji → null(用出厂的) */
25
+ export const ownAvatar = (t) => (t.avatar && !OLD_FACTORY_EMOJI.has(t.avatar) ? t.avatar : null);
26
+ /** 出厂头像在包里的文件;不用出厂的 → null */
27
+ export function factoryAvatarFile(t) {
28
+ const f = factoryName(t);
29
+ return f ? join(PACKAGE_AVATARS_DIR, f) : null;
30
+ }
31
+ /** 下发给页面的 avatar:出厂的给「factory:<文件名>」,家长写的原样,都没有 null(页面显示学科首字) */
32
+ export function shownAvatar(t) {
33
+ const f = factoryName(t);
34
+ return f ? FACTORY_PREFIX + f : (t.avatar ?? null);
35
+ }
@@ -123,7 +123,7 @@ export async function checkDevTutorFile(ws, tutor, text) {
123
123
  preview: {
124
124
  text: body,
125
125
  chars: charsOf(body),
126
- board: guide ? { chars: charsOf(guide.body), cards: kinds ? [...kinds] : [] } : null,
126
+ board: guide ? { chars: charsOf(guide), cards: kinds ? [...kinds] : [] } : null,
127
127
  rules,
128
128
  },
129
129
  diff: shipped === null ? null : diffHunks(shipped, text),