cotutor 0.2.0 → 0.3.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 (871) hide show
  1. package/LICENSE +247 -21
  2. package/NOTICE +42 -0
  3. package/README.md +92 -20
  4. package/bin/cotutor.js +4 -4
  5. package/cards/canvas/card.css +8 -0
  6. package/cards/canvas/card.md +68 -0
  7. package/cards/choice/card.css +18 -0
  8. package/cards/choice/card.md +55 -0
  9. package/cards/code/card.css +5 -0
  10. package/cards/code/card.md +46 -0
  11. package/cards/dictation/card.css +10 -0
  12. package/cards/dictation/card.md +97 -0
  13. package/cards/image/card.css +10 -0
  14. package/cards/image/card.md +52 -0
  15. package/cards/lecture/card.css +9 -0
  16. package/cards/lecture/card.md +69 -0
  17. package/cards/paint/card.css +20 -0
  18. package/cards/paint/card.md +77 -0
  19. package/cards/photo/card.css +15 -0
  20. package/cards/photo/card.md +61 -0
  21. package/cards/read/card.css +6 -0
  22. package/cards/read/card.md +51 -0
  23. package/cards/text/card.css +3 -0
  24. package/cards/text/card.md +69 -0
  25. package/cards/tianzige/card.css +17 -0
  26. package/cards/tianzige/card.md +78 -0
  27. package/cards/tutor/card.css +66 -0
  28. package/cards/tutor/card.md +148 -0
  29. package/cards/word/card.css +24 -0
  30. package/cards/word/card.md +91 -0
  31. package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +159 -0
  32. package/dist/app/activity-eval-rules.d.ts +62 -0
  33. package/dist/app/activity-eval-rules.js +276 -0
  34. package/dist/app/activity-eval.d.ts +65 -0
  35. package/dist/app/activity-eval.js +158 -0
  36. package/dist/app/build.d.ts +11 -0
  37. package/dist/app/build.js +45 -0
  38. package/dist/app/cli/activities.d.ts +63 -0
  39. package/dist/app/cli/activities.js +158 -0
  40. package/dist/{cli → app/cli}/cert.js +3 -3
  41. package/dist/app/cli/chalk/bake.d.ts +10 -0
  42. package/dist/app/cli/chalk/bake.js +58 -0
  43. package/dist/app/cli/chalk/browser.d.ts +16 -0
  44. package/dist/app/cli/chalk/browser.js +142 -0
  45. package/dist/app/cli/chalk/build.d.ts +14 -0
  46. package/dist/app/cli/chalk/build.js +100 -0
  47. package/dist/app/cli/chalk/dub.d.ts +51 -0
  48. package/dist/app/cli/chalk/dub.js +106 -0
  49. package/dist/app/cli/chalk/load-scene.d.ts +34 -0
  50. package/dist/app/cli/chalk/load-scene.js +117 -0
  51. package/dist/app/cli/chalk/main.d.ts +7 -0
  52. package/dist/app/cli/chalk/main.js +198 -0
  53. package/dist/app/cli/chalk/serve.d.ts +4 -0
  54. package/dist/app/cli/chalk/serve.js +28 -0
  55. package/dist/app/cli/chalk/snap.d.ts +65 -0
  56. package/dist/app/cli/chalk/snap.js +103 -0
  57. package/dist/app/cli/chalk/typecheck.d.ts +7 -0
  58. package/dist/app/cli/chalk/typecheck.js +51 -0
  59. package/dist/{cli → app/cli}/doctor.d.ts +4 -3
  60. package/dist/app/cli/doctor.js +617 -0
  61. package/dist/app/cli/grades.d.ts +14 -0
  62. package/dist/app/cli/grades.js +44 -0
  63. package/dist/{cli → app/cli}/init.d.ts +3 -1
  64. package/dist/app/cli/init.js +155 -0
  65. package/dist/app/cli/main.js +715 -0
  66. package/dist/app/cli/migrate.d.ts +58 -0
  67. package/dist/app/cli/migrate.js +253 -0
  68. package/dist/{cli → app/cli}/serve.d.ts +12 -5
  69. package/dist/app/cli/serve.js +98 -0
  70. package/dist/app/cli/shipped.d.ts +30 -0
  71. package/dist/app/cli/shipped.js +47 -0
  72. package/dist/app/cli/skeleton.d.ts +38 -0
  73. package/dist/app/cli/skeleton.js +161 -0
  74. package/dist/app/cli/skills.d.ts +55 -0
  75. package/dist/app/cli/skills.js +189 -0
  76. package/dist/app/cli/themes.d.ts +52 -0
  77. package/dist/app/cli/themes.js +148 -0
  78. package/dist/{cli → app/cli}/tutors.d.ts +15 -26
  79. package/dist/{cli → app/cli}/tutors.js +86 -110
  80. package/dist/app/cli/usage.d.ts +2 -0
  81. package/dist/app/cli/usage.js +30 -0
  82. package/dist/app/dictation/recognize.d.ts +18 -0
  83. package/dist/app/dictation/recognize.js +6 -0
  84. package/dist/app/dictation/route.d.ts +52 -0
  85. package/dist/app/dictation/route.js +510 -0
  86. package/dist/app/diff.d.ts +11 -0
  87. package/dist/app/diff.js +71 -0
  88. package/dist/app/http/access.d.ts +62 -0
  89. package/dist/app/http/access.js +180 -0
  90. package/dist/app/http/app.d.ts +246 -0
  91. package/dist/app/http/app.js +1212 -0
  92. package/dist/app/http/dev-activities.d.ts +115 -0
  93. package/dist/app/http/dev-activities.js +193 -0
  94. package/dist/app/http/dev-overview.d.ts +108 -0
  95. package/dist/app/http/dev-overview.js +209 -0
  96. package/dist/app/http/dev-tutor-files.d.ts +80 -0
  97. package/dist/app/http/dev-tutor-files.js +151 -0
  98. package/dist/app/http/parent-home.d.ts +37 -0
  99. package/dist/app/http/parent-home.js +203 -0
  100. package/dist/app/http/range.d.ts +9 -0
  101. package/dist/app/http/range.js +19 -0
  102. package/dist/app/http/raw-view.d.ts +117 -0
  103. package/dist/app/http/raw-view.js +149 -0
  104. package/dist/app/http/send-file.d.ts +7 -0
  105. package/dist/app/http/send-file.js +29 -0
  106. package/dist/{server → app/http}/stage.d.ts +9 -0
  107. package/dist/{server → app/http}/stage.js +34 -6
  108. package/dist/app/http/theme.d.ts +13 -0
  109. package/dist/app/http/theme.js +41 -0
  110. package/dist/app/machine/bailian.d.ts +28 -0
  111. package/dist/app/machine/bailian.js +88 -0
  112. package/dist/app/mock/mock-live.d.ts +16 -0
  113. package/dist/app/mock/mock-live.js +140 -0
  114. package/dist/{server → app/mock}/mock.d.ts +36 -5
  115. package/dist/app/mock/mock.js +1117 -0
  116. package/dist/app/replay.d.ts +94 -0
  117. package/dist/app/replay.js +170 -0
  118. package/dist/app/skills/analyze-doc.d.ts +3 -0
  119. package/dist/app/skills/analyze-doc.js +47 -0
  120. package/dist/app/skills/home-doc.d.ts +5 -0
  121. package/dist/app/skills/home-doc.js +112 -0
  122. package/dist/app/skills/tune-doc.d.ts +3 -0
  123. package/dist/app/skills/tune-doc.js +80 -0
  124. package/dist/app/talk/list.d.ts +11 -0
  125. package/dist/app/talk/list.js +9 -0
  126. package/dist/app/talk/route.d.ts +16 -0
  127. package/dist/app/talk/route.js +142 -0
  128. package/dist/app/workspace.d.ts +58 -0
  129. package/dist/{cli → app}/workspace.js +77 -52
  130. package/dist/app/ws/route.d.ts +25 -0
  131. package/dist/app/ws/route.js +29 -0
  132. package/dist/app/ws/ws.d.ts +31 -0
  133. package/dist/app/ws/ws.js +169 -0
  134. package/dist/board/canvas-chat.d.ts +97 -0
  135. package/dist/board/canvas-chat.js +175 -0
  136. package/dist/board/card-guards.d.ts +15 -0
  137. package/dist/board/card-guards.js +38 -0
  138. package/dist/board/cards/canvas.d.ts +41 -0
  139. package/dist/board/cards/canvas.js +83 -0
  140. package/dist/{cards → board/cards}/choice.js +0 -12
  141. package/dist/{cards → board/cards}/code.js +0 -3
  142. package/dist/board/cards/dictation.d.ts +42 -0
  143. package/dist/board/cards/dictation.js +105 -0
  144. package/dist/board/cards/docs.d.ts +47 -0
  145. package/dist/board/cards/docs.js +152 -0
  146. package/dist/{cards → board/cards}/image.js +1 -10
  147. package/dist/board/cards/index.d.ts +62 -0
  148. package/dist/board/cards/index.js +144 -0
  149. package/dist/{cards → board/cards}/kind.d.ts +10 -4
  150. package/dist/board/cards/kind.js +9 -0
  151. package/dist/board/cards/lecture.d.ts +26 -0
  152. package/dist/board/cards/lecture.js +62 -0
  153. package/dist/board/cards/paint.d.ts +38 -0
  154. package/dist/board/cards/paint.js +85 -0
  155. package/dist/board/cards/photo.d.ts +15 -0
  156. package/dist/board/cards/photo.js +29 -0
  157. package/dist/{cards → board/cards}/read.js +1 -10
  158. package/dist/board/cards/text.d.ts +20 -0
  159. package/dist/board/cards/text.js +59 -0
  160. package/dist/board/cards/tianzige.d.ts +16 -0
  161. package/dist/board/cards/tianzige.js +64 -0
  162. package/dist/board/cards/tutor.d.ts +72 -0
  163. package/dist/board/cards/tutor.js +134 -0
  164. package/dist/board/cards/word.d.ts +20 -0
  165. package/dist/board/cards/word.js +76 -0
  166. package/dist/board/lecture.d.ts +100 -0
  167. package/dist/board/lecture.js +204 -0
  168. package/dist/board/paper.d.ts +20 -0
  169. package/dist/board/paper.js +76 -0
  170. package/dist/board/parse.d.ts +120 -0
  171. package/dist/board/parse.js +330 -0
  172. package/dist/board/presentation.d.ts +295 -0
  173. package/dist/board/presentation.js +615 -0
  174. package/dist/board/sections.d.ts +16 -0
  175. package/dist/board/sections.js +130 -0
  176. package/dist/brain/api-tools.d.ts +8 -0
  177. package/dist/brain/api-tools.js +190 -0
  178. package/dist/brain/api.d.ts +62 -0
  179. package/dist/brain/api.js +411 -0
  180. package/dist/brain/cli/adapters.d.ts +60 -0
  181. package/dist/brain/cli/adapters.js +91 -0
  182. package/dist/brain/cli/qwen.d.ts +11 -0
  183. package/dist/brain/cli/qwen.js +141 -0
  184. package/dist/brain/cli/run-plan.d.ts +59 -0
  185. package/dist/brain/cli/run-plan.js +71 -0
  186. package/dist/brain/cli/run.d.ts +35 -0
  187. package/dist/brain/cli/run.js +256 -0
  188. package/dist/brain/cli/stream-json.d.ts +18 -0
  189. package/dist/brain/cli/stream-json.js +86 -0
  190. package/dist/brain/cli/types.d.ts +144 -0
  191. package/dist/brain/cli/types.js +3 -0
  192. package/dist/brain/cli/warm.d.ts +53 -0
  193. package/dist/brain/cli/warm.js +103 -0
  194. package/dist/brain/index.d.ts +145 -0
  195. package/dist/brain/index.js +74 -0
  196. package/dist/brain/stream.d.ts +20 -0
  197. package/dist/brain/stream.js +70 -0
  198. package/dist/brain/transcript.d.ts +45 -0
  199. package/dist/brain/transcript.js +132 -0
  200. package/dist/chalk/actions.d.ts +37 -0
  201. package/dist/chalk/actions.js +69 -0
  202. package/dist/chalk/bundle.d.ts +26 -0
  203. package/dist/chalk/bundle.js +69 -0
  204. package/dist/chalk/check.d.ts +28 -0
  205. package/dist/chalk/check.js +291 -0
  206. package/dist/chalk/circle.d.ts +64 -0
  207. package/dist/chalk/circle.js +193 -0
  208. package/dist/chalk/clock.d.ts +84 -0
  209. package/dist/chalk/clock.js +84 -0
  210. package/dist/chalk/geometry.d.ts +44 -0
  211. package/dist/chalk/geometry.js +139 -0
  212. package/dist/chalk/glyphs/capitals.d.ts +12 -0
  213. package/dist/chalk/glyphs/capitals.js +264 -0
  214. package/dist/chalk/glyphs/index.d.ts +21 -0
  215. package/dist/chalk/glyphs/index.js +18 -0
  216. package/dist/chalk/glyphs/letters.d.ts +13 -0
  217. package/dist/chalk/glyphs/letters.js +280 -0
  218. package/dist/chalk/glyphs/metrics.d.ts +33 -0
  219. package/dist/chalk/glyphs/metrics.js +28 -0
  220. package/dist/chalk/glyphs/registry.d.ts +7 -0
  221. package/dist/chalk/glyphs/registry.js +8 -0
  222. package/dist/chalk/glyphs/sample.d.ts +32 -0
  223. package/dist/chalk/glyphs/sample.js +102 -0
  224. package/dist/chalk/glyphs/skeleton.d.ts +64 -0
  225. package/dist/chalk/glyphs/skeleton.js +126 -0
  226. package/dist/chalk/glyphs/types.d.ts +58 -0
  227. package/dist/chalk/glyphs/types.js +17 -0
  228. package/dist/chalk/index.d.ts +23 -0
  229. package/dist/chalk/index.js +15 -0
  230. package/dist/chalk/manifest.d.ts +41 -0
  231. package/dist/chalk/manifest.js +6 -0
  232. package/dist/chalk/paint.d.ts +180 -0
  233. package/dist/chalk/paint.js +210 -0
  234. package/dist/chalk/pointer.d.ts +8 -0
  235. package/dist/chalk/pointer.js +21 -0
  236. package/dist/chalk/prepare.d.ts +11 -0
  237. package/dist/chalk/prepare.js +15 -0
  238. package/dist/chalk/scene-schema.d.ts +199 -0
  239. package/dist/chalk/scene-schema.js +91 -0
  240. package/dist/chalk/seed.d.ts +3 -0
  241. package/dist/chalk/seed.js +10 -0
  242. package/dist/chalk/text-metrics.d.ts +8 -0
  243. package/dist/chalk/text-metrics.js +156 -0
  244. package/dist/chalk/timeline.d.ts +47 -0
  245. package/dist/chalk/timeline.js +84 -0
  246. package/dist/chalk/types.d.ts +150 -0
  247. package/dist/chalk/types.js +8 -0
  248. package/dist/lib/index.d.ts +14 -12
  249. package/dist/lib/index.js +14 -12
  250. package/dist/media/draw.d.ts +23 -0
  251. package/dist/media/draw.js +44 -0
  252. package/dist/media/mp3.d.ts +6 -0
  253. package/dist/media/mp3.js +58 -0
  254. package/dist/media/mp4.d.ts +1 -0
  255. package/dist/media/mp4.js +58 -0
  256. package/dist/media/tts-voices.d.ts +18 -0
  257. package/dist/media/tts-voices.js +606 -0
  258. package/dist/media/tts.d.ts +56 -0
  259. package/dist/media/tts.js +174 -0
  260. package/dist/media/vision.d.ts +25 -0
  261. package/dist/media/vision.js +71 -0
  262. package/dist/memory/commit.d.ts +67 -0
  263. package/dist/memory/commit.js +130 -0
  264. package/dist/memory/context.d.ts +86 -0
  265. package/dist/memory/context.js +97 -0
  266. package/dist/memory/diary.d.ts +84 -0
  267. package/dist/memory/diary.js +183 -0
  268. package/dist/memory/grade.d.ts +2 -0
  269. package/dist/memory/grade.js +7 -0
  270. package/dist/{lib → memory}/plan.d.ts +1 -1
  271. package/dist/{lib → memory}/plan.js +1 -1
  272. package/dist/memory/proposals.d.ts +11 -0
  273. package/dist/memory/proposals.js +43 -0
  274. package/dist/memory/retrieval.d.ts +44 -0
  275. package/dist/memory/retrieval.js +157 -0
  276. package/dist/{lib → memory}/timetable.d.ts +0 -3
  277. package/dist/{lib → memory}/timetable.js +0 -11
  278. package/dist/memory/vault-notes.d.ts +165 -0
  279. package/dist/memory/vault-notes.js +359 -0
  280. package/dist/memory/vault.d.ts +70 -0
  281. package/dist/memory/vault.js +169 -0
  282. package/dist/realtime/bailian.d.ts +120 -0
  283. package/dist/realtime/bailian.js +288 -0
  284. package/dist/realtime/cues.d.ts +51 -0
  285. package/dist/realtime/cues.js +94 -0
  286. package/dist/realtime/listen.d.ts +32 -0
  287. package/dist/realtime/listen.js +53 -0
  288. package/dist/realtime/preview.d.ts +23 -0
  289. package/dist/realtime/preview.js +38 -0
  290. package/dist/realtime/prompt.d.ts +8 -0
  291. package/dist/realtime/prompt.js +13 -0
  292. package/dist/realtime/voices.d.ts +19 -0
  293. package/dist/realtime/voices.js +71 -0
  294. package/dist/runtime/activities.d.ts +17 -0
  295. package/dist/runtime/activities.js +46 -0
  296. package/dist/runtime/activity-state.d.ts +22 -0
  297. package/dist/runtime/activity-state.js +83 -0
  298. package/dist/runtime/activity.d.ts +63 -0
  299. package/dist/runtime/activity.js +428 -0
  300. package/dist/runtime/agent-file.d.ts +24 -0
  301. package/dist/runtime/agent-file.js +59 -0
  302. package/dist/runtime/auto-bookkeep.d.ts +30 -0
  303. package/dist/runtime/auto-bookkeep.js +84 -0
  304. package/dist/{lib → runtime/context}/context-pack.d.ts +3 -3
  305. package/dist/runtime/context/context-pack.js +182 -0
  306. package/dist/runtime/context/thread-digest.d.ts +4 -0
  307. package/dist/runtime/context/thread-digest.js +35 -0
  308. package/dist/runtime/context/tutor-rules.d.ts +25 -0
  309. package/dist/runtime/context/tutor-rules.js +47 -0
  310. package/dist/runtime/delivery/delivery.d.ts +94 -0
  311. package/dist/runtime/delivery/delivery.js +145 -0
  312. package/dist/runtime/delivery/playback.d.ts +25 -0
  313. package/dist/runtime/delivery/playback.js +29 -0
  314. package/dist/runtime/delivery/player.d.ts +145 -0
  315. package/dist/runtime/delivery/player.js +221 -0
  316. package/dist/runtime/delivery/queue.d.ts +89 -0
  317. package/dist/runtime/delivery/queue.js +59 -0
  318. package/dist/runtime/home/edit.d.ts +39 -0
  319. package/dist/runtime/home/edit.js +132 -0
  320. package/dist/runtime/home/home.d.ts +180 -0
  321. package/dist/runtime/home/home.js +311 -0
  322. package/dist/runtime/home/store.d.ts +142 -0
  323. package/dist/runtime/home/store.js +440 -0
  324. package/dist/runtime/journal/conversation.d.ts +46 -0
  325. package/dist/runtime/journal/conversation.js +89 -0
  326. package/dist/runtime/journal/events.d.ts +27 -0
  327. package/dist/runtime/journal/events.js +136 -0
  328. package/dist/runtime/journal/kid-log.d.ts +105 -0
  329. package/dist/runtime/journal/kid-log.js +133 -0
  330. package/dist/runtime/journal/ledger.d.ts +17 -0
  331. package/dist/{lib → runtime/journal}/ledger.js +7 -40
  332. package/dist/runtime/journal/mix.d.ts +33 -0
  333. package/dist/runtime/journal/mix.js +88 -0
  334. package/dist/runtime/journal/mouth-index.d.ts +26 -0
  335. package/dist/runtime/journal/mouth-index.js +34 -0
  336. package/dist/runtime/journal/mouth.d.ts +54 -0
  337. package/dist/runtime/journal/mouth.js +102 -0
  338. package/dist/runtime/journal/recorder.d.ts +46 -0
  339. package/dist/runtime/journal/recorder.js +58 -0
  340. package/dist/runtime/journal/store.d.ts +123 -0
  341. package/dist/runtime/journal/store.js +316 -0
  342. package/dist/runtime/kid-input.d.ts +16 -0
  343. package/dist/runtime/kid-input.js +195 -0
  344. package/dist/runtime/kid-view.d.ts +124 -0
  345. package/dist/runtime/kid-view.js +157 -0
  346. package/dist/runtime/lecture.d.ts +86 -0
  347. package/dist/runtime/lecture.js +237 -0
  348. package/dist/runtime/live/call.d.ts +219 -0
  349. package/dist/runtime/live/call.js +1211 -0
  350. package/dist/runtime/live/turns.d.ts +48 -0
  351. package/dist/runtime/live/turns.js +102 -0
  352. package/dist/runtime/runner.d.ts +289 -0
  353. package/dist/runtime/runner.js +898 -0
  354. package/dist/runtime/talk-run.d.ts +21 -0
  355. package/dist/runtime/talk-run.js +34 -0
  356. package/dist/scenes/dictation/match.d.ts +57 -0
  357. package/dist/scenes/dictation/match.js +255 -0
  358. package/dist/scenes/dictation/recognize.d.ts +86 -0
  359. package/dist/scenes/dictation/recognize.js +163 -0
  360. package/dist/scenes/dictation/store.d.ts +112 -0
  361. package/dist/scenes/dictation/store.js +99 -0
  362. package/dist/scenes/look.d.ts +26 -0
  363. package/dist/scenes/look.js +1 -0
  364. package/dist/scenes/paint/painter.d.ts +84 -0
  365. package/dist/scenes/paint/painter.js +170 -0
  366. package/dist/scenes/paint/prompt.d.ts +23 -0
  367. package/dist/scenes/paint/prompt.js +36 -0
  368. package/dist/scenes/talk/en-words.d.ts +8 -0
  369. package/dist/scenes/talk/en-words.js +44 -0
  370. package/dist/scenes/talk/list.d.ts +44 -0
  371. package/dist/scenes/talk/list.js +88 -0
  372. package/dist/scenes/talk/store.d.ts +27 -0
  373. package/dist/scenes/talk/store.js +61 -0
  374. package/dist/scenes/talk/warmup.d.ts +16 -0
  375. package/dist/scenes/talk/warmup.js +39 -0
  376. package/dist/schema/activity.d.ts +149 -0
  377. package/dist/schema/activity.js +113 -0
  378. package/dist/schema/board.d.ts +143 -2
  379. package/dist/schema/board.js +15 -3
  380. package/dist/schema/config.d.ts +309 -94
  381. package/dist/schema/config.js +259 -72
  382. package/dist/schema/context-pack.d.ts +76 -14
  383. package/dist/schema/context-pack.js +91 -10
  384. package/dist/{lib/conversation.d.ts → schema/conversation-files.d.ts} +11 -33
  385. package/dist/schema/conversation-files.js +26 -0
  386. package/dist/schema/conversation.d.ts +385 -49
  387. package/dist/schema/conversation.js +135 -19
  388. package/dist/schema/events.d.ts +138 -0
  389. package/dist/schema/events.js +1 -0
  390. package/dist/schema/home.d.ts +46 -0
  391. package/dist/schema/home.js +30 -0
  392. package/dist/schema/index.d.ts +14 -3
  393. package/dist/schema/index.js +14 -3
  394. package/dist/schema/kid-device.d.ts +20 -0
  395. package/dist/schema/kid-device.js +32 -0
  396. package/dist/schema/ledger.d.ts +8 -63
  397. package/dist/schema/ledger.js +7 -31
  398. package/dist/schema/memory.d.ts +73 -0
  399. package/dist/schema/memory.js +47 -0
  400. package/dist/schema/plan.d.ts +2 -2
  401. package/dist/schema/plan.js +2 -2
  402. package/dist/schema/sections.d.ts +32 -25
  403. package/dist/schema/sections.js +21 -22
  404. package/dist/schema/theme.d.ts +25 -0
  405. package/dist/schema/theme.js +26 -0
  406. package/dist/schema/timetable.d.ts +2 -2
  407. package/dist/schema/timetable.js +2 -2
  408. package/dist/schema/workspace.d.ts +41 -0
  409. package/dist/schema/workspace.js +9 -0
  410. package/dist/shared/errors.d.ts +8 -0
  411. package/dist/shared/errors.js +13 -0
  412. package/dist/shared/home.d.ts +2 -0
  413. package/dist/shared/home.js +16 -0
  414. package/dist/shared/human-notes.d.ts +5 -0
  415. package/dist/shared/human-notes.js +7 -0
  416. package/dist/shared/qianwen.d.ts +50 -0
  417. package/dist/shared/qianwen.js +109 -0
  418. package/dist/shared/time.d.ts +9 -0
  419. package/dist/shared/time.js +22 -0
  420. package/dist/shared/tokens.d.ts +18 -0
  421. package/dist/shared/tokens.js +11 -0
  422. package/dist/shared/wav.d.ts +10 -0
  423. package/dist/shared/wav.js +50 -0
  424. package/dist/stage/chunk-226TNLQA.js +129 -0
  425. package/dist/stage/chunk-24LGQEKE.js +1 -0
  426. package/dist/stage/chunk-25XV6UY3.js +10 -0
  427. package/dist/stage/chunk-2AZI3NHJ.js +1 -0
  428. package/dist/stage/chunk-2BQZTTBE.js +1 -0
  429. package/dist/stage/chunk-2M7CGY3Y.js +11 -0
  430. package/dist/stage/chunk-2VR4WFQ7.js +10 -0
  431. package/dist/stage/chunk-2ZYXBUWG.js +9 -0
  432. package/dist/stage/chunk-32LAWMGY.js +1 -0
  433. package/dist/stage/chunk-3LEMTSOO.js +11 -0
  434. package/dist/stage/chunk-3QCKGZYE.js +32 -0
  435. package/dist/stage/chunk-44P66WFA.js +1 -0
  436. package/dist/stage/chunk-4AM6FRR6.js +7 -0
  437. package/dist/stage/chunk-4QDE4SUK.js +1 -0
  438. package/dist/stage/chunk-4QESHV3U.js +1 -0
  439. package/dist/stage/chunk-4SG5QPHB.js +2 -0
  440. package/dist/stage/chunk-4T4UVCOC.js +1 -0
  441. package/dist/stage/chunk-4WUEPPG3.js +34 -0
  442. package/dist/stage/chunk-4YCN6UZ5.js +99 -0
  443. package/dist/stage/chunk-5V7ZY2AP.js +1 -0
  444. package/dist/stage/chunk-5VS5Y2VJ.js +6 -0
  445. package/dist/stage/chunk-5W4NO2ZD.js +62 -0
  446. package/dist/stage/chunk-65CRNICM.js +1 -0
  447. package/dist/stage/chunk-66MULO7F.js +5 -0
  448. package/dist/stage/chunk-6COFQTBS.js +162 -0
  449. package/dist/stage/chunk-6VZG3M3I.js +8 -0
  450. package/dist/stage/chunk-7HVDH43T.js +1 -0
  451. package/dist/stage/chunk-7JLJN4EZ.js +8 -0
  452. package/dist/stage/chunk-7QDSP4LN.js +1 -0
  453. package/dist/stage/chunk-7R6OQAO3.js +1 -0
  454. package/dist/stage/chunk-7VTTHZKW.js +7 -0
  455. package/dist/stage/chunk-ATHR5KEN.js +10 -0
  456. package/dist/stage/chunk-B6R5LTP2.js +36 -0
  457. package/dist/stage/chunk-BD4WE264.js +1 -0
  458. package/dist/stage/chunk-BONANU47.js +206 -0
  459. package/dist/stage/chunk-BTVW7OFK.js +89 -0
  460. package/dist/stage/chunk-BUTJIDHG.js +8 -0
  461. package/dist/stage/chunk-C2IPYMML.js +84 -0
  462. package/dist/stage/chunk-C54FTPPV.js +9 -0
  463. package/dist/stage/chunk-C6LOUF2Z.js +90 -0
  464. package/dist/stage/chunk-CFF3PILO.js +339 -0
  465. package/dist/stage/chunk-CI2FQ2JY.js +9 -0
  466. package/dist/stage/chunk-CW5O4RV7.js +12 -0
  467. package/dist/stage/chunk-CYABNQYD.js +1 -0
  468. package/dist/stage/chunk-D7FFA4NS.js +15 -0
  469. package/dist/stage/chunk-DUNNXU6Z.js +8 -0
  470. package/dist/stage/chunk-E3E4Q6JF.js +6 -0
  471. package/dist/stage/chunk-EB3U3LH7.js +9 -0
  472. package/dist/stage/chunk-EGSZ736B.js +8 -0
  473. package/dist/stage/chunk-EOBXW5VQ.js +2 -0
  474. package/dist/stage/chunk-ERZC3B7Y.js +11 -0
  475. package/dist/stage/chunk-ESDOOBIH.js +11 -0
  476. package/dist/stage/chunk-EVU4QAL4.js +178 -0
  477. package/dist/stage/chunk-EVVZI2NY.js +39 -0
  478. package/dist/stage/chunk-FBKAZUKD.js +39 -0
  479. package/dist/stage/chunk-FDVKYQJN.js +1 -0
  480. package/dist/stage/chunk-FGPVYHVR.js +5 -0
  481. package/dist/stage/chunk-FHUD4AZC.js +1 -0
  482. package/dist/stage/chunk-FKUJ2DMY.js +96 -0
  483. package/dist/stage/chunk-FNZGVIJV.js +1 -0
  484. package/dist/stage/chunk-GBYSEGYM.js +78 -0
  485. package/dist/stage/chunk-GCO4TDHX.js +1 -0
  486. package/dist/stage/chunk-GKTML2LG.js +10 -0
  487. package/dist/stage/chunk-GMDIHHXQ.js +11 -0
  488. package/dist/stage/chunk-GOUNACI3.js +10 -0
  489. package/dist/stage/chunk-GXAYATPF.js +3 -0
  490. package/dist/stage/chunk-H2K2DDXV.js +8 -0
  491. package/dist/stage/chunk-H3UJI6ZY.js +13 -0
  492. package/dist/stage/chunk-H6PUGCME.js +7 -0
  493. package/dist/stage/chunk-HNBUIMIG.js +1 -0
  494. package/dist/stage/chunk-HRZY2BJQ.js +10 -0
  495. package/dist/stage/chunk-HSPJGALW.js +30 -0
  496. package/dist/stage/chunk-IBYORHIP.js +25 -0
  497. package/dist/stage/chunk-IHF4EWTY.js +1 -0
  498. package/dist/stage/chunk-ISBRPNKI.js +1 -0
  499. package/dist/stage/chunk-IZ3OI3C2.js +2 -0
  500. package/dist/stage/chunk-JEFZAC2R.js +1 -0
  501. package/dist/stage/chunk-JFIGRDPO.js +1 -0
  502. package/dist/stage/chunk-JFVYBOOC.js +9 -0
  503. package/dist/stage/chunk-JGD4TUTQ.js +1 -0
  504. package/dist/stage/chunk-JO4BEHDH.js +4 -0
  505. package/dist/stage/chunk-JOWBS42C.js +1 -0
  506. package/dist/stage/chunk-JP3TM2TG.js +1 -0
  507. package/dist/stage/chunk-JVK2C52O.js +1 -0
  508. package/dist/stage/chunk-K4I76XBX.js +14 -0
  509. package/dist/stage/chunk-KA6CZZ4M.js +1 -0
  510. package/dist/stage/chunk-KESSJ36M.js +1 -0
  511. package/dist/stage/chunk-LKATEVRU.js +1 -0
  512. package/dist/stage/chunk-LS4EJ2EI.js +1 -0
  513. package/dist/stage/chunk-LUKJQC2X.js +70 -0
  514. package/dist/stage/chunk-LWULR3SM.js +1 -0
  515. package/dist/stage/chunk-LWVRFMKX.js +2 -0
  516. package/dist/stage/chunk-LXC4ZBPZ.js +1 -0
  517. package/dist/stage/chunk-M2UP255H.js +1 -0
  518. package/dist/stage/chunk-MAPPXUZQ.js +231 -0
  519. package/dist/stage/chunk-MBMPW7Z5.js +1 -0
  520. package/dist/stage/chunk-MD3HMMTK.js +1 -0
  521. package/dist/stage/chunk-MLPCUIKR.js +17 -0
  522. package/dist/stage/chunk-MNQIRPDQ.js +8 -0
  523. package/dist/stage/chunk-MRG7MJVS.js +1 -0
  524. package/dist/stage/chunk-MU6EXW72.js +10 -0
  525. package/dist/stage/chunk-MVU2X6LU.js +4 -0
  526. package/dist/stage/chunk-N6UA5CZV.js +36 -0
  527. package/dist/stage/chunk-NLZIT2TJ.js +7 -0
  528. package/dist/stage/chunk-NOJR3J7M.js +1 -0
  529. package/dist/stage/chunk-NT7ARDOL.js +1 -0
  530. package/dist/stage/chunk-NWOGMPZH.js +1 -0
  531. package/dist/stage/chunk-NXE4KMG3.js +40 -0
  532. package/dist/stage/chunk-O4YCZXD2.js +257 -0
  533. package/dist/stage/chunk-OAJZ7HLK.js +1 -0
  534. package/dist/stage/chunk-OCFCFASO.js +1 -0
  535. package/dist/stage/chunk-ODGPG5RY.js +7 -0
  536. package/dist/stage/chunk-ODUEA52Q.js +3 -0
  537. package/dist/stage/chunk-OGDJ3FCI.js +1 -0
  538. package/dist/stage/chunk-OLDJ55TT.js +1 -0
  539. package/dist/stage/chunk-OOCL4ATI.js +2 -0
  540. package/dist/stage/chunk-OQEFDUDV.js +1 -0
  541. package/dist/stage/chunk-OQWVEWG7.js +4 -0
  542. package/dist/stage/chunk-OUIX6BGK.js +1 -0
  543. package/dist/stage/chunk-OV7OZYUO.js +7 -0
  544. package/dist/stage/chunk-PFGDY33O.js +1 -0
  545. package/dist/stage/chunk-PFJDUJB3.js +139 -0
  546. package/dist/stage/chunk-PGEW76ER.js +1 -0
  547. package/dist/stage/chunk-PP76TAQX.js +32 -0
  548. package/dist/stage/chunk-PTBOP4OT.js +2 -0
  549. package/dist/stage/chunk-PX2RVHCK.js +1 -0
  550. package/dist/stage/chunk-Q7K3PNAD.js +1 -0
  551. package/dist/stage/chunk-QDDPLZDB.js +1 -0
  552. package/dist/stage/chunk-QLGLZ77F.js +1 -0
  553. package/dist/stage/chunk-QOPFDFHI.js +1 -0
  554. package/dist/stage/chunk-QXT4KAEJ.js +1 -0
  555. package/dist/stage/chunk-R4YVZCIT.js +1 -0
  556. package/dist/stage/chunk-R6P6EHWA.js +62 -0
  557. package/dist/stage/chunk-RCH3OXBI.js +9 -0
  558. package/dist/stage/chunk-RHPE44I2.js +1 -0
  559. package/dist/stage/chunk-RJ2R4FVC.js +1 -0
  560. package/dist/stage/chunk-RJH2YVTD.js +1 -0
  561. package/dist/stage/chunk-RLWWW5OB.js +6 -0
  562. package/dist/stage/chunk-S3WO35Z7.js +120 -0
  563. package/dist/stage/chunk-S7ZGX6MW.js +1 -0
  564. package/dist/stage/chunk-SIAWX27B.js +1 -0
  565. package/dist/stage/chunk-SLG4X622.js +1 -0
  566. package/dist/stage/chunk-SLJRBC77.js +168 -0
  567. package/dist/stage/chunk-SRORWQJZ.js +1 -0
  568. package/dist/stage/chunk-STZTUIIF.js +2 -0
  569. package/dist/stage/chunk-SVQ6MWNV.js +9 -0
  570. package/dist/stage/chunk-TL3HLL2S.js +132 -0
  571. package/dist/stage/chunk-U3DOE6SF.js +24 -0
  572. package/dist/stage/chunk-UABQIZKW.js +6 -0
  573. package/dist/stage/chunk-UDK3R764.js +1 -0
  574. package/dist/stage/chunk-UFF4E54B.js +41 -0
  575. package/dist/stage/chunk-UFP5Q4AY.js +1 -0
  576. package/dist/stage/chunk-UMH2TSW2.js +72 -0
  577. package/dist/stage/chunk-UTASVNF5.js +10 -0
  578. package/dist/stage/chunk-UW7ML5ML.js +88 -0
  579. package/dist/stage/chunk-V3J7F6OP.js +1 -0
  580. package/dist/stage/chunk-VEHP5B5Q.js +5 -0
  581. package/dist/stage/chunk-VRXWB733.js +9 -0
  582. package/dist/stage/chunk-VVDHBBS5.js +5 -0
  583. package/dist/stage/chunk-VYFOH6FU.js +106 -0
  584. package/dist/stage/chunk-W3VZTLQS.js +3 -0
  585. package/dist/stage/chunk-W7WNRWTZ.js +8 -0
  586. package/dist/stage/chunk-WHUCV53C.js +1 -0
  587. package/dist/stage/chunk-WULMYHIF.js +1 -0
  588. package/dist/stage/chunk-X54EX7AW.js +9 -0
  589. package/dist/stage/chunk-X565ISZM.js +1 -0
  590. package/dist/stage/chunk-XGZMDECB.js +1 -0
  591. package/dist/stage/chunk-XHONC4RF.js +172 -0
  592. package/dist/stage/chunk-XIK4XQ6E.js +24 -0
  593. package/dist/stage/chunk-XJQQWFTE.js +1 -0
  594. package/dist/stage/chunk-XM4WOU3I.js +13 -0
  595. package/dist/stage/chunk-XM7Y5HP5.js +38 -0
  596. package/dist/stage/chunk-XNYYRGD5.js +292 -0
  597. package/dist/stage/chunk-XOYVR3SV.js +1 -0
  598. package/dist/stage/chunk-XQIW5HCE.js +1 -0
  599. package/dist/stage/chunk-YEWL7FUW.js +8 -0
  600. package/dist/stage/chunk-YI55PY3V.js +1 -0
  601. package/dist/stage/chunk-YM63HI6W.js +1 -0
  602. package/dist/stage/chunk-YNLGLU4P.js +9 -0
  603. package/dist/stage/chunk-YTNMNOWP.js +1 -0
  604. package/dist/stage/chunk-YV62XKTC.js +1 -0
  605. package/dist/stage/chunk-YVBA3QW6.js +1 -0
  606. package/dist/stage/chunk-Z2SY2HOC.js +1 -0
  607. package/dist/stage/chunk-ZBTCQEBK.js +10 -0
  608. package/dist/stage/chunk-ZCUFN74D.js +1 -0
  609. package/dist/stage/chunk-ZHOULXEJ.js +11 -0
  610. package/dist/stage/chunk-ZTRZIET2.js +1 -0
  611. package/dist/stage/index.html +1 -0
  612. package/dist/stage/stage.css +1 -1
  613. package/dist/stage/stage.js +1 -4383
  614. package/dist/ui/dev/dev-page.d.ts +1 -0
  615. package/dist/ui/dev/dev-page.js +2248 -0
  616. package/dist/ui/dev/dev-theme.d.ts +6 -0
  617. package/dist/ui/dev/dev-theme.js +67 -0
  618. package/dist/ui/dev/qr-page.d.ts +8 -0
  619. package/dist/ui/dev/qr-page.js +61 -0
  620. package/dist/ui/dev/voice-gallery-page.d.ts +1 -0
  621. package/dist/ui/dev/voice-gallery-page.js +121 -0
  622. package/dist/ui/dev/voice-scenes.d.ts +21 -0
  623. package/dist/ui/dev/voice-scenes.js +30 -0
  624. package/dist/ui/dictation/en-box.d.ts +2 -0
  625. package/dist/ui/dictation/en-box.js +86 -0
  626. package/dist/ui/dictation/en-page.d.ts +1 -0
  627. package/dist/ui/dictation/en-page.js +484 -0
  628. package/dist/ui/dictation/page.d.ts +1 -0
  629. package/dist/ui/dictation/page.js +1084 -0
  630. package/dist/ui/dictation/parent-page.d.ts +1 -0
  631. package/dist/ui/dictation/parent-page.js +234 -0
  632. package/dist/ui/dictation/reel-mode.d.ts +1 -0
  633. package/dist/ui/dictation/reel-mode.js +87 -0
  634. package/dist/ui/dictation/reel-page.d.ts +1 -0
  635. package/dist/ui/dictation/reel-page.js +62 -0
  636. package/dist/ui/dictation/reel.d.ts +163 -0
  637. package/dist/ui/dictation/reel.js +436 -0
  638. package/dist/ui/dictation/ui.d.ts +44 -0
  639. package/dist/ui/dictation/ui.js +80 -0
  640. package/dist/ui/dictation/unlock.d.ts +7 -0
  641. package/dist/ui/dictation/unlock.js +16 -0
  642. package/dist/ui/kid/audio-client.d.ts +11 -0
  643. package/dist/ui/kid/audio-client.js +74 -0
  644. package/dist/ui/kid/board-mark-art.d.ts +37 -0
  645. package/dist/ui/kid/board-mark-art.js +5 -0
  646. package/dist/ui/kid/board-mark.d.ts +24 -0
  647. package/dist/ui/kid/board-mark.js +58 -0
  648. package/dist/ui/kid/kid-log.d.ts +15 -0
  649. package/dist/ui/kid/kid-log.js +87 -0
  650. package/dist/ui/kid/kid-nav.d.ts +11 -0
  651. package/dist/ui/kid/kid-nav.js +22 -0
  652. package/dist/ui/kid/kid-page.d.ts +9 -0
  653. package/dist/ui/kid/kid-page.js +3719 -0
  654. package/dist/ui/kid/letters.d.ts +26 -0
  655. package/dist/ui/kid/letters.js +51 -0
  656. package/dist/ui/kid/photo-edit.d.ts +83 -0
  657. package/dist/ui/kid/photo-edit.js +106 -0
  658. package/dist/ui/kid/photo-ui.d.ts +23 -0
  659. package/dist/ui/kid/photo-ui.js +60 -0
  660. package/dist/ui/kid/reel.d.ts +419 -0
  661. package/dist/ui/kid/reel.js +648 -0
  662. package/dist/ui/kid/tianzige.d.ts +8 -0
  663. package/dist/ui/kid/tianzige.js +45 -0
  664. package/dist/ui/kid/tutor-face.d.ts +35 -0
  665. package/dist/ui/kid/tutor-face.js +84 -0
  666. package/dist/ui/kid/voice-ui.d.ts +70 -0
  667. package/dist/ui/kid/voice-ui.js +316 -0
  668. package/dist/ui/kid/voice-view.d.ts +71 -0
  669. package/dist/ui/kid/voice-view.js +75 -0
  670. package/dist/ui/kid/word-draw.d.ts +2 -0
  671. package/dist/ui/kid/word-draw.js +107 -0
  672. package/dist/ui/kid/word-emoji.d.ts +10 -0
  673. package/dist/ui/kid/word-emoji.js +66 -0
  674. package/dist/ui/kid/word-layout.d.ts +25 -0
  675. package/dist/ui/kid/word-layout.js +29 -0
  676. package/dist/ui/parent/parent-home-page.d.ts +1 -0
  677. package/dist/ui/parent/parent-home-page.js +414 -0
  678. package/dist/ui/parent/parent-page.d.ts +2 -0
  679. package/dist/ui/parent/parent-page.js +507 -0
  680. package/dist/ui/shared/brand.d.ts +36 -0
  681. package/dist/ui/shared/brand.js +51 -0
  682. package/dist/ui/shared/favicon.d.ts +12 -0
  683. package/dist/ui/shared/favicon.js +96 -0
  684. package/dist/ui/shared/icon.d.ts +9 -0
  685. package/dist/ui/shared/icon.js +35 -0
  686. package/dist/ui/shared/inline-lib.d.ts +1 -0
  687. package/dist/ui/shared/inline-lib.js +28 -0
  688. package/dist/ui/shared/logo.d.ts +31 -0
  689. package/dist/ui/shared/logo.js +43 -0
  690. package/dist/ui/shared/page-dom.d.ts +12 -0
  691. package/dist/ui/shared/page-dom.js +36 -0
  692. package/dist/ui/shared/png.d.ts +1 -0
  693. package/dist/ui/shared/png.js +45 -0
  694. package/dist/ui/shared/qr-svg.d.ts +3 -0
  695. package/dist/ui/shared/qr-svg.js +32 -0
  696. package/dist/ui/shared/qr.d.ts +14 -0
  697. package/dist/ui/shared/qr.js +281 -0
  698. package/dist/ui/shared/reel-dock.d.ts +22 -0
  699. package/dist/ui/shared/reel-dock.js +365 -0
  700. package/dist/ui/shared/tutor-color.d.ts +15 -0
  701. package/dist/ui/shared/tutor-color.js +47 -0
  702. package/dist/ui/talk/page.d.ts +6 -0
  703. package/dist/ui/talk/page.js +294 -0
  704. package/grades/grade-2/README.md +11 -0
  705. package/grades/grade-2/VALIDATION.md +41 -0
  706. package/grades/grade-2/agents/chinese-tutor/activities//344/275/240/350/257/264/346/210/221/347/224/273.md +157 -0
  707. package/grades/grade-2/agents/chinese-tutor/activities//344/275/240/350/257/264/346/210/221/347/224/273./344/276/213/345/255/220.md +78 -0
  708. package/grades/grade-2/agents/chinese-tutor/activities//344/276/213/345/255/220/345/233/276//345/200/237/344/274/236.jpg +0 -0
  709. package/grades/grade-2/agents/chinese-tutor/activities//344/276/213/345/255/220/345/233/276//345/260/217/347/214/264/346/221/230/346/241/203.jpg +0 -0
  710. package/grades/grade-2/agents/chinese-tutor/activities//345/206/231/350/257/235.md +64 -0
  711. package/grades/grade-2/agents/chinese-tutor/activities//345/206/231/350/257/235./344/276/213/345/255/220.md +31 -0
  712. package/grades/grade-2/agents/chinese-tutor/activities//345/220/254/345/206/231.md +48 -0
  713. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +79 -0
  714. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240./344/276/213/345/255/220.md +35 -0
  715. package/grades/grade-2/agents/chinese-tutor/activities//345/260/217/350/257/276/345/240/202.md +53 -0
  716. package/grades/grade-2/agents/chinese-tutor/activities//346/211/271/346/224/271.md +81 -0
  717. package/grades/grade-2/agents/chinese-tutor/activities//346/211/271/346/224/271./344/276/213/345/255/220.md +35 -0
  718. package/grades/grade-2/agents/chinese-tutor/activities//347/234/213/345/233/276/350/257/264/350/257/235.md +94 -0
  719. package/grades/grade-2/agents/chinese-tutor/activities//347/234/213/345/233/276/350/257/264/350/257/235./344/276/213/345/255/220.md +63 -0
  720. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +82 -0
  721. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240./344/276/213/345/255/220.md +35 -0
  722. package/grades/grade-2/agents/chinese-tutor.md +59 -0
  723. package/grades/grade-2/agents/english-tutor/activities//345/217/243/350/257/255/350/257/276.md +149 -0
  724. package/grades/grade-2/agents/english-tutor/activities//345/217/243/350/257/255/350/257/276./344/276/213/345/255/220.md +13 -0
  725. package/grades/grade-2/agents/english-tutor/activities//345/220/254/345/206/231.md +47 -0
  726. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +78 -0
  727. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240./344/276/213/345/255/220.md +35 -0
  728. package/grades/grade-2/agents/english-tutor/activities//345/260/217/350/257/276/345/240/202.md +53 -0
  729. package/grades/grade-2/agents/english-tutor/activities//346/211/271/346/224/271.md +80 -0
  730. package/grades/grade-2/agents/english-tutor/activities//346/211/271/346/224/271./344/276/213/345/255/220.md +35 -0
  731. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +86 -0
  732. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240./344/276/213/345/255/220.md +35 -0
  733. package/grades/grade-2/agents/english-tutor.md +57 -0
  734. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +78 -0
  735. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240./344/276/213/345/255/220.md +35 -0
  736. package/grades/grade-2/agents/math-tutor/activities//345/260/217/350/257/276/345/240/202.md +56 -0
  737. package/grades/grade-2/agents/math-tutor/activities//345/260/217/350/257/276/345/240/202./344/276/213/345/255/220.md +52 -0
  738. package/grades/grade-2/agents/math-tutor/activities//346/211/271/346/224/271.md +80 -0
  739. package/grades/grade-2/agents/math-tutor/activities//346/211/271/346/224/271./344/276/213/345/255/220.md +35 -0
  740. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +81 -0
  741. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240./344/276/213/345/255/220.md +35 -0
  742. package/grades/grade-2/agents/math-tutor.md +51 -0
  743. package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/index.json +44 -0
  744. package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-1.mp3 +0 -0
  745. package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-2.mp3 +0 -0
  746. package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-3.mp3 +0 -0
  747. package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-4.mp3 +0 -0
  748. package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-5.mp3 +0 -0
  749. package/grades/grade-2/examples/2026-10-27-pingjunfen/bake.json +1 -0
  750. package/grades/grade-2/examples/2026-10-27-pingjunfen/manifest.json +63 -0
  751. package/grades/grade-2/examples/2026-10-27-pingjunfen/scene.json +247 -0
  752. package/grades/grade-2/examples/2026-10-27-pingjunfen.md +40 -0
  753. package/grades/grade-2/examples/2026-10-27-pingjunfen.ts +26 -0
  754. package/grades/grade-2/examples//345/271/263/345/235/207/345/210/206./344/276/213/345/255/220.md +24 -0
  755. package/grades/grade-2/examples//346/225/260/345/255/246/345/271/263/345/235/207/345/210/206.md +42 -0
  756. package/grades/grade-2/validation/2026-10-27.json +4410 -0
  757. package/package.json +26 -15
  758. package/skills/chalk-cli/SKILL.md +62 -0
  759. package/skills/chalk-scene/SKILL.md +91 -0
  760. package/skills/chalk-teaching/SKILL.md +82 -0
  761. package/skills/chalk-teaching/models//344/272/244/351/224/231/346/225/260/345/210/227/345/210/206/350/241/214.md +8 -0
  762. package/skills/chalk-teaching/models//345/207/221/345/215/201.md +7 -0
  763. package/skills/chalk-teaching/models//345/221/250/346/234/237/350/247/204/345/276/213.md +6 -0
  764. package/skills/chalk-teaching/models//345/256/236/347/211/251/345/233/276-/345/242/236/345/207/217.md +6 -0
  765. package/skills/chalk-teaching/models//345/256/236/347/211/251/345/233/276-/346/257/224/345/244/232/345/260/221-/346/261/202/345/244/247/346/225/260/346/261/202/345/260/217/346/225/260.md +7 -0
  766. package/skills/chalk-teaching/models//345/256/236/347/211/251/345/233/276-/346/257/224/345/244/232/345/260/221-/346/261/202/345/267/256.md +6 -0
  767. package/skills/chalk-teaching/models//345/260/217/346/243/222/350/277/233/344/275/215/345/212/240.md +7 -0
  768. package/skills/chalk-teaching/models//345/260/217/346/243/222/351/200/200/344/275/215/345/207/217.md +6 -0
  769. package/skills/chalk-teaching/models//346/216/222/351/230/237/347/202/271/351/230/265.md +7 -0
  770. package/skills/chalk-teaching/models//346/227/266/351/227/264/346/225/260/350/275/264.md +6 -0
  771. package/skills/chalk-teaching/models//346/245/274/345/261/202/351/227/264/351/232/224/345/233/276.md +7 -0
  772. package/skills/chalk-teaching/models//347/240/264/345/215/201.md +7 -0
  773. package/skills/chalk-teaching/models//347/253/226/345/274/217/351/200/200/344/275/215.md +6 -0
  774. package/skills/chalk-teaching/models//347/272/277/346/256/265/345/233/276.md +7 -0
  775. package/skills/chalk-teaching/models-index.md +24 -0
  776. package/skills/chalk-teaching/references//350/256/262/350/247/243/346/250/241/346/235/277/344/270/211/346/241/243.md +32 -0
  777. package/skills/chalk-teaching/references//350/257/264/346/230/216/346/226/207/344/273/266.md +102 -0
  778. package/skills/chalk-verify/SKILL.md +57 -0
  779. package/skills/chalk-verify/references//350/257/204/345/210/244/346/240/207/345/207/206.md +69 -0
  780. package/skills/cotutor-analyze/SKILL.md +106 -0
  781. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +31 -0
  782. package/skills/cotutor-board/SKILL.md +373 -0
  783. package/skills/cotutor-board/references/README.md +38 -0
  784. package/skills/cotutor-board/references/canvas.md +68 -0
  785. package/skills/cotutor-board/references/choice.md +55 -0
  786. package/skills/cotutor-board/references/code.md +46 -0
  787. package/skills/cotutor-board/references/image.md +52 -0
  788. package/skills/cotutor-board/references/lecture.md +69 -0
  789. package/skills/cotutor-board/references/paint.md +77 -0
  790. package/skills/cotutor-board/references/photo.md +61 -0
  791. package/skills/cotutor-board/references/read.md +51 -0
  792. package/skills/cotutor-board/references/text.md +69 -0
  793. package/skills/cotutor-board/references/tianzige.md +78 -0
  794. package/skills/cotutor-board/references/word.md +91 -0
  795. package/skills/cotutor-home/SKILL.md +169 -0
  796. package/skills/cotutor-home/references/dictation.md +97 -0
  797. package/skills/cotutor-home/references/tutor.md +148 -0
  798. package/skills/cotutor-home/references//345/221/275/344/273/244.md +21 -0
  799. package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +142 -0
  800. package/skills/cotutor-tune/SKILL.md +147 -0
  801. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +50 -0
  802. package/skills/cotutor-tutor/SKILL.md +261 -0
  803. package/skills/cotutor-tutor/references//344/270/212/344/270/213/346/226/207/345/214/205.md +78 -0
  804. package/themes/default/kid.css +86 -0
  805. package/themes/default/theme.json +26 -0
  806. package/agents/chinese-tutor.md +0 -67
  807. package/agents/homework-tutor.md +0 -17
  808. package/agents/math-tutor.md +0 -68
  809. package/agents/planner.md +0 -18
  810. package/agents/reading-tutor.md +0 -65
  811. package/agents/scene-maker.md +0 -41
  812. package/dist/cards/canvas.d.ts +0 -22
  813. package/dist/cards/canvas.js +0 -65
  814. package/dist/cards/fill.d.ts +0 -14
  815. package/dist/cards/fill.js +0 -64
  816. package/dist/cards/index.d.ts +0 -48
  817. package/dist/cards/index.js +0 -115
  818. package/dist/cards/kind.js +0 -7
  819. package/dist/cards/scene.d.ts +0 -24
  820. package/dist/cards/scene.js +0 -67
  821. package/dist/cards/text.d.ts +0 -18
  822. package/dist/cards/text.js +0 -41
  823. package/dist/cli/doctor.js +0 -355
  824. package/dist/cli/init.js +0 -116
  825. package/dist/cli/main.js +0 -243
  826. package/dist/cli/serve.js +0 -56
  827. package/dist/cli/skeleton.d.ts +0 -39
  828. package/dist/cli/skeleton.js +0 -184
  829. package/dist/cli/skills.d.ts +0 -40
  830. package/dist/cli/skills.js +0 -170
  831. package/dist/cli/workspace.d.ts +0 -69
  832. package/dist/lib/agent-file.d.ts +0 -10
  833. package/dist/lib/agent-file.js +0 -16
  834. package/dist/lib/board.d.ts +0 -15
  835. package/dist/lib/board.js +0 -107
  836. package/dist/lib/context-pack.js +0 -55
  837. package/dist/lib/conversation.js +0 -88
  838. package/dist/lib/kid-board.d.ts +0 -148
  839. package/dist/lib/kid-board.js +0 -294
  840. package/dist/lib/kid-view.d.ts +0 -63
  841. package/dist/lib/kid-view.js +0 -93
  842. package/dist/lib/ledger.d.ts +0 -29
  843. package/dist/lib/run-plan.d.ts +0 -30
  844. package/dist/lib/run-plan.js +0 -32
  845. package/dist/lib/sections.d.ts +0 -12
  846. package/dist/lib/sections.js +0 -151
  847. package/dist/lib/stream.d.ts +0 -15
  848. package/dist/lib/stream.js +0 -69
  849. package/dist/lib/transcript.d.ts +0 -36
  850. package/dist/lib/transcript.js +0 -95
  851. package/dist/server/app.d.ts +0 -74
  852. package/dist/server/app.js +0 -406
  853. package/dist/server/kid-page.d.ts +0 -1
  854. package/dist/server/kid-page.js +0 -768
  855. package/dist/server/mock.js +0 -403
  856. package/dist/server/parent-page.d.ts +0 -8
  857. package/dist/server/parent-page.js +0 -393
  858. package/dist/server/runner.d.ts +0 -64
  859. package/dist/server/runner.js +0 -277
  860. package/dist/server/scene-props.d.ts +0 -16
  861. package/dist/server/scene-props.js +0 -39
  862. package/dist/server/store.d.ts +0 -43
  863. package/dist/server/store.js +0 -210
  864. package/dist/server/tts.d.ts +0 -51
  865. package/dist/server/tts.js +0 -99
  866. /package/dist/{cli → app/cli}/cert.d.ts +0 -0
  867. /package/dist/{cli → app/cli}/main.d.ts +0 -0
  868. /package/dist/{cards → board/cards}/choice.d.ts +0 -0
  869. /package/dist/{cards → board/cards}/code.d.ts +0 -0
  870. /package/dist/{cards → board/cards}/image.d.ts +0 -0
  871. /package/dist/{cards → board/cards}/read.d.ts +0 -0
@@ -0,0 +1,1117 @@
1
+ /**
2
+ * 模拟接口(`cotutor mock`):不经真实老师与百炼,用固定的板书 JSON 把孩子端页面喂起来,
3
+ * 专门测前端的交互逻辑与渲染效果(卡一张张铺、停下等、追问追加)。老师都走实时老师页,假嘴念静音的声音、字幕照走。
4
+ * 不需要 workspace。老师的每一轮从各自的脚本里按顺序取(脚本就是老师会写的正文,过真解析器);脚本用完给一句收尾话。
5
+ * 卡不配音:点读段、单词卡挂上段名(同真服务的 card.assets),/api/audio 一律回一段静音,页面照真服务那样放音频、等放完(不用浏览器合成声)。
6
+ * 老师「想」的期间按流式模拟:卡在 delay 里一张张出现(pending 条目带 partial 板书),想完才有讲稿与声音。
7
+ * 卡的状态 PUT 假存在内存里(过真的 state 契约),today 里并回卡上;发消息接 {text, action, focus},「交给老师」后照常追加下一节。
8
+ * 场景:normal(缺省)/ nokey(没配百炼,孩子端只出「老师们还没到」)。
9
+ * 首页是一份写死的「已发布」(mockHomeMd,过真解析器与真排法):语文老师有开场与接着昨天的按钮,数学老师有开场,英语老师没写(补一张);
10
+ * 发消息带 via 时照真服务的规则换成按钮上的字、定话题。
11
+ * 老师都走实时老师页,那条 WebSocket 是 mock-live.ts 的假嘴(照脚本「念」静音的声音,不花钱)。
12
+ */
13
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { readdir } from 'node:fs/promises';
15
+ import { createServer as createHttp } from 'node:http';
16
+ import { createServer as createHttps } from 'node:https';
17
+ import { hostname, tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { parseBoard } from "../../board/parse.js";
20
+ import { cardAssets, parseCardState, stripSecrets, DictationPropsSchema } from "../../board/cards/index.js";
21
+ import { fileURLToPath } from 'node:url';
22
+ import { bundleAsset, stageAsset, stageIndex, stageVersion } from "../http/stage.js";
23
+ import { sendFile } from "../http/send-file.js";
24
+ import { subjectLang, tutorExtras } from "../../schema/index.js";
25
+ import { NEUTRAL_COLOR, tutorColors } from "../../ui/shared/tutor-color.js";
26
+ import { boardMarkOf } from "../../ui/kid/board-mark.js";
27
+ import { backTutorOf } from "../../ui/kid/tutor-face.js";
28
+ import { enrichLectures, ensureBaked, lectureFrameSvg, parseRing, parseSvgMs, readLecture, storedMarks } from "../../runtime/lecture.js";
29
+ import { tianzigeData } from "../../ui/kid/tianzige.js";
30
+ import { handleDictation } from "../dictation/route.js";
31
+ import { lettersData } from "../../ui/kid/letters.js";
32
+ import { mockLiveUpgrade } from "./mock-live.js";
33
+ import { DRAW_ONLY_TEXT, PAPER_KEY_RE, paperDrawing } from "../../board/paper.js";
34
+ import { CanvasStateSchema } from "../../board/cards/canvas.js";
35
+ /** 看完小课堂的第一问,mock 老师这样答:对着画的地方说,放课里拆开那一捆的那一段;视频的放轮着分那一段 */
36
+ const MOCK_VIDEO_LECTURE_REPLY = [
37
+ '你画的是轮着分的那一下。',
38
+ '',
39
+ '```lecture',
40
+ '2026-10-06-pingjunfen 0:05-0:11',
41
+ '再看一遍怎么轮着分。',
42
+ '```',
43
+ '',
44
+ '我们回到轮着分的地方,再看一遍。[[play]]',
45
+ '',
46
+ '每人分到几块?',
47
+ ].join('\n');
48
+ const MOCK_LECTURE_REPLY = [
49
+ '你画的是右边散的 3 根。8 根要从哪儿拿?',
50
+ '',
51
+ '```lecture',
52
+ '2026-09-18-po13-jian-8 0:19-0:30',
53
+ '再看一遍拆开那一捆。',
54
+ '```',
55
+ '',
56
+ '我们回到拆开那一捆的地方,再看一遍。[[play]]',
57
+ '',
58
+ '拿走 8 根,这一捆还剩几根?',
59
+ ].join('\n');
60
+ /** mock 的课包目录:仓库里的样本(tests/fixtures/bundles/),小课堂从这里放 */
61
+ export const MOCK_BUNDLES_DIR = fileURLToPath(new URL('../../../tests/fixtures/bundles/', import.meta.url));
62
+ /** mock 的视频小课堂目录:仓库里的样本(tests/fixtures/lectures/) */
63
+ export const MOCK_LECTURES_DIR = fileURLToPath(new URL('../../../tests/fixtures/lectures/', import.meta.url));
64
+ const MOCK_LECTURE_DIRS = { dirs: { bundles: MOCK_BUNDLES_DIR, lectures: MOCK_LECTURES_DIR } };
65
+ import { lineDurationMs, plainLine, readyBeats } from "../../board/presentation.js";
66
+ import { wavOfPcm } from "../../shared/wav.js";
67
+ import { buildReel, playRecordOk, reelHolds, reelMissRuns } from "../../ui/kid/reel.js";
68
+ import { weekOf } from "../../shared/time.js";
69
+ import {} from "../../schema/index.js";
70
+ import { lanAddresses, listenInfo } from "../cli/serve.js";
71
+ import { qrPage } from "../../ui/dev/qr-page.js";
72
+ import { USER_CERT_DIR } from "../workspace.js";
73
+ import { kidThreads } from "../../runtime/kid-view.js";
74
+ import { ICON_SIZES, appIconPng, webManifest } from "../../ui/shared/icon.js";
75
+ import { faviconResponse } from "../../ui/shared/favicon.js";
76
+ import { kidPage } from "../../ui/kid/kid-page.js";
77
+ import { parentPage } from "../../ui/parent/parent-page.js";
78
+ import { arrangeHome, homeOwners, kidButtons, parseHome } from "../../runtime/home/home.js";
79
+ import { packageTheme } from "../cli/themes.js";
80
+ import { cardsCss } from "../../board/cards/docs.js";
81
+ import { VOICE_GALLERY_PAGE } from "../../ui/dev/voice-gallery-page.js";
82
+ /** 脚本 → 板书节:走真解析器(mock 也是解析器的一次演练) */
83
+ export function sectionFromScript(md) {
84
+ return parseBoard(md).section;
85
+ }
86
+ /**
87
+ * 写死的样子:几节预装板书的底色 / emoji 与带笔的标注(真老师是写在围栏行与讲稿的 [词] 里的),直接套到解析出来的节上,
88
+ * 页面走的是同一条渲染路(look 的槽、带 pen 的标注;行由页面按卡的宽度排)。键 = 老师名:脚本序号。
89
+ */
90
+ export const MOCK_LOOKS = {
91
+ // 勾股定理:验证是方法卡(moss),封面的「三条边」画圈(讲到第一句时)、16 下划线(老师自己标的 直角边 / 斜边 / 25 保留,不重复)
92
+ 'math-tutor:0': {
93
+ marks: [{ line: 0, card: 0, phrase: '三条边', pen: 'circle' }, { line: 3, card: 3, phrase: '16', pen: 'underline' }],
94
+ look: { '3': { tint: 'moss' }, '4': { emoji: '💡' } },
95
+ },
96
+ 'math-tutor:1': { marks: [], look: { '0': { tint: 'moss' } } },
97
+ 'chinese-tutor:0': { marks: [{ line: 1, card: 1, phrase: '多做一步', pen: 'marker' }], look: { '1': { emoji: '💡' } } },
98
+ };
99
+ function withMockLooks(section, tutor, i) {
100
+ const out = MOCK_LOOKS[`${tutor}:${i}`];
101
+ if (!out)
102
+ return section;
103
+ const cards = section.cards.map((c, n) => (out.look[String(n)] ? { ...c, look: { ...(c.look ?? {}), ...out.look[String(n)] } } : c));
104
+ const lines = section.lines.map((l, n) => ({ ...l, marks: [...l.marks, ...out.marks.filter((m) => m.line === n).map(({ card, phrase, pen }) => ({ card, phrase, pen }))] }));
105
+ return { ...section, cards, lines };
106
+ }
107
+ /** 孩子发出后几毫秒内画图卡算「画家在画」(含老师想的那几秒;真的 qwen-image-2.0-pro 约 15 秒) */
108
+ const MOCK_PAINT_MS = 20000;
109
+ /** 画图卡的占位图:方的 2×2 四格,每格一个圆圈数字 */
110
+ const MOCK_PAINT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"><rect width="1024" height="1024" fill="#fbfaf6"/>${[[24, 24, '#dcecf7'], [524, 24, '#fdeccb'], [24, 524, '#e3f1d8'], [524, 524, '#f6dde4']].map(([x, y, c], i) => `<rect x="${x}" y="${y}" width="476" height="476" rx="24" fill="${c}" stroke="#8a8781" stroke-width="3"/><rect x="${x}" y="${Number(y) + 300}" width="476" height="176" fill="#a9d18e"/><circle cx="${Number(x) + 238}" cy="${Number(y) + 250}" r="70" fill="#9a6235"/><circle cx="${Number(x) + 62}" cy="${Number(y) + 62}" r="34" fill="#fff" stroke="#1f8a4c" stroke-width="6"/><text x="${Number(x) + 62}" y="${Number(y) + 76}" font-size="40" text-anchor="middle" fill="#1f8a4c" font-family="sans-serif" font-weight="700">${i + 1}</text>`).join('')}</svg>`;
111
+ export const MOCK_TUTORS = [
112
+ {
113
+ name: 'chinese-tutor',
114
+ display: '语文老师',
115
+ subject: '语文',
116
+ avatar: '语',
117
+ preloaded: 1,
118
+ firstQuestion: '画蛇添足是什么意思?',
119
+ script: [
120
+ `你有没有过这种事:本来做得好好的,又多加了一点,结果反而糟了?
121
+
122
+ ~~~text
123
+ # 画蛇添足
124
+ 一个成语,一杯酒的故事
125
+ ~~~
126
+
127
+ 这个成语说的是:[多做一步,反而坏事]。
128
+
129
+ ~~~text
130
+ 画蛇添足 = 多做一步,反而坏事
131
+ ~~~
132
+
133
+ 它出自《战国策》,原文是这样的。
134
+
135
+ ~~~read
136
+ 楚有祠者,赐其舍人卮酒。
137
+ 舍人相谓曰:数人饮之不足,一人饮之有余。
138
+ 请画地为蛇,先成者饮酒。
139
+ ~~~
140
+
141
+ 几个人分一壶酒,酒不够大家喝,一个人喝正好,就比赛画蛇,[先成者饮酒],谁先画完谁喝。
142
+
143
+ ~~~choice
144
+ 如果第一个画完蛇的人,不去给蛇添上脚,酒是谁的?
145
+ - [x] 他自己的
146
+ - [ ] 第二个画完的
147
+ - [ ] 大家平分
148
+ ~~~
149
+
150
+ 我想问你:如果第一个画完蛇的人,不给蛇添脚,酒本来是谁的?`,
151
+ `你是这么想的:酒是他自己的。
152
+
153
+ ~~~text
154
+ 先画完的人本来就赢了
155
+ ~~~
156
+
157
+ 他多画了脚,蛇就[不是蛇]了,第二个画完的说,你画的不是蛇。
158
+
159
+ ~~~text
160
+ 为蛇足者,终亡其酒。
161
+ ~~~
162
+
163
+ 古人把这件事记成一句话:[为蛇足者,终亡其酒]。
164
+
165
+ ~~~choice
166
+ 画蛇添足,就是做到了还要怎样,反而把事情弄糟?
167
+ - [x] 多做一步
168
+ - [ ] 少做一步
169
+ - [ ] 马上停下
170
+ ~~~
171
+
172
+ 你来选一选:画蛇添足,就是做到了还要怎样?`,
173
+ `~~~text
174
+ 做到了,就停下来
175
+ ~~~
176
+
177
+ 说得好,这就是画蛇添足要提醒我们的:[做到了,就停下来]。
178
+
179
+ ~~~canvas
180
+ 画一条蛇,不要给它添脚。
181
+ ~~~
182
+
183
+ 你来画一条蛇,画好了给我看看。`,
184
+ `~~~tianzige
185
+ 鼓励
186
+ gǔ lì
187
+ ~~~
188
+
189
+ 你说的是加油打气的那个鼓励吧,看,鼓先写,励后写。
190
+
191
+ ~~~text
192
+ 鼓励:给人加油、打气
193
+ ~~~
194
+
195
+ 鼓是敲鼓的鼓,励是[努力]的力多一个厂字头,一个人在厂下面使劲。
196
+
197
+ 你想再看一遍哪个字?点一下它就再写一遍。`,
198
+ // 你说我画:出题版的画图卡;mock 不真画,刚出来几秒是「画家在画」,之后给一张占位的四格
199
+ `你说得清清楚楚,四个词都放好了。
200
+
201
+ ~~~read
202
+ 春天的早上,小熊和小猴在地里种西瓜。
203
+ 小熊把种子埋进土里,小猴坐在树下吃桃子。
204
+ 中午太阳很大,小熊给小苗浇水,小猴躺在树下睡大觉。
205
+ 过了一个夏天,小熊的地里结满了大西瓜,小猴的地里只长了草。
206
+ 小熊推着满满一车西瓜,送给小猴一个,小猴低着头,脸红红的。
207
+ ~~~
208
+
209
+ 这是你出的题,画家开始画了。
210
+
211
+ ~~~paint
212
+ 题: 图上画了谁?他们分别在干什么?结果又是怎样的?用上词语宝库里的词,写几句话。
213
+ 词: 播种、睡大觉、满满、羞愧
214
+ 角色: 小熊:棕色,戴黄色草帽,穿绿色背带裤
215
+ 角色: 小猴:黄褐色,穿红背心
216
+ 1. 春天的早上,地里。小熊弯着腰把西瓜种子埋进土里;小猴坐在树下吃桃子。
217
+ 2. 中午,大太阳。小熊提着水壶给小苗浇水;小猴躺在树荫下睡大觉。
218
+ 3. 夏天,小熊的地里结满大西瓜,小熊抱着一个笑着;小猴的地里只长了草。
219
+ 4. 小熊推着满满一车西瓜,递给小猴一个;小猴低着头,脸红红的。
220
+ ~~~
221
+
222
+ 你猜,画家会把小猴画成什么样子?`,
223
+ ],
224
+ },
225
+ {
226
+ name: 'math-tutor',
227
+ display: '数学老师',
228
+ subject: '数学',
229
+ avatar: '数',
230
+ preloaded: 1,
231
+ firstQuestion: '勾股定理是什么?',
232
+ script: [
233
+ `~~~text
234
+ # 勾股定理
235
+ 直角三角形三条边的关系
236
+ ~~~
237
+
238
+ 我们先认一认直角三角形的三条边。
239
+
240
+ ~~~text
241
+ # 认边
242
+ 两条短边叫直角边,最长的一条叫斜边
243
+ ~~~
244
+
245
+ 两条短边叫[直角边],最长的一条叫[斜边]。
246
+
247
+ ~~~text formula
248
+ 直角边² + 直角边² = 斜边²
249
+ ~~~
250
+
251
+ 先记住这个公式:[直角边² + 直角边² = 斜边²]。
252
+
253
+ ~~~text
254
+ # 验证
255
+ 3 的平方是 9,4 的平方是 16,9 加 16 等于 25
256
+ ~~~
257
+
258
+ 拿 3 和 4 试试:9 加 16 等于 [25],正好是 5 的平方,所以斜边是 5。
259
+
260
+ ~~~text
261
+ 知道两条直角边,平方相加再开方,就是斜边
262
+ ~~~
263
+
264
+ ~~~choice
265
+ 一个直角三角形,两条直角边是 6 和 8,斜边是多少?
266
+ - [x] 10
267
+ - [ ] 12
268
+ - [ ] 14
269
+ ~~~
270
+
271
+ 你来选一选:两条直角边是 [6 和 8],斜边是多少?`,
272
+ `对,6 的平方加 8 的平方是 100,正好是 10 的平方。
273
+
274
+ ~~~text
275
+ # 反过来想
276
+ 如果知道斜边和一条直角边,也能求另一条直角边
277
+ ~~~
278
+
279
+ 反过来也行:知道[斜边]和一条直角边,也能求另一条。
280
+
281
+ ~~~text formula
282
+ 斜边² − 直角边² = 另一条直角边²
283
+ ~~~
284
+
285
+ ~~~choice
286
+ 一个直角三角形,斜边是 13,一条直角边是 5,另一条直角边是多少?
287
+ - [ ] 8
288
+ - [x] 12
289
+ - [ ] 18
290
+ ~~~
291
+
292
+ 你来选一选:斜边是 13,一条直角边是 5,另一条直角边是多少?`,
293
+ `对,169 减 25 等于 144,12 的平方正好是 144。
294
+
295
+ ~~~text
296
+ 3、4、5 和 5、12、13 都是常见的勾股数组
297
+ ~~~
298
+
299
+ 我们换一道找规律的题,课里讲过这一段。
300
+
301
+ ~~~lecture
302
+ 2026-09-04-guilv5 0:00
303
+ 75、70、65,后面三个空填什么?
304
+ ~~~
305
+
306
+ 我们回到课里看一遍。[[play]]
307
+
308
+ 看完了,你自己说说,每次少几?`,
309
+ ],
310
+ },
311
+ {
312
+ name: 'english-tutor',
313
+ display: '英语老师',
314
+ subject: '英语',
315
+ avatar: '英',
316
+ preloaded: 0,
317
+ firstQuestion: '',
318
+ script: [
319
+ `Today we learn three fruits. 今天学三种水果。
320
+
321
+ ~~~text
322
+ # Fruits
323
+ 水果
324
+ ~~~
325
+
326
+ ~~~read
327
+ apple 苹果
328
+ banana 香蕉
329
+ orange 橘子
330
+ ~~~
331
+
332
+ Listen and repeat: [apple], [banana], [orange]. 点一下听一下,跟着我读。
333
+
334
+ ~~~image
335
+ captures/2026-09-10/fruits.png
336
+ 三种水果,你家有哪种?
337
+ ~~~
338
+
339
+ ~~~word
340
+ 🍎 apple
341
+ ap-ple
342
+ ~~~
343
+
344
+ This is how we write it: a, p, p, l, e. 看我写一遍。
345
+
346
+ Which one do you want to try first, [apple], [banana], or [orange]? 你先读哪一个?`,
347
+ // 拍给我看卡(cards/photo/card.md):末句不是问句,下沿照样是「等你拍…」带拍照、相册
348
+ `Good job! 读得真好。
349
+
350
+ ~~~photo
351
+ 家里的一种水果
352
+ ~~~
353
+
354
+ Find a fruit at home. 找一个家里的水果,拍下来给我看。`,
355
+ ],
356
+ },
357
+ ];
358
+ /** mock 的首页原文(昨晚 21:30 发布的那份;接着按钮指向 past 里昨天的话题,job = 0930-<老师名长度>) */
359
+ export function mockHomeMd(today, yesterday) {
360
+ return `---
361
+ for: ${today}
362
+ ---
363
+
364
+ \`\`\`tutor chinese-tutor
365
+ 我要预习小蝌蚪找妈妈
366
+ 讲法: 第 22 课。先带他把 1–3 自然段读顺,重点字 塘、脑、袋
367
+ 接着 ${yesterday} 0930-13 接着讲画蛇添足
368
+ 讲法: 昨天讲到第二节,先问他还记不记得那杯酒
369
+ \`\`\`
370
+
371
+ \`\`\`tutor math-tutor
372
+ 再练两道退位减法
373
+ 讲法: 出 52−7、80−3,先让他说怎么想
374
+ 小课堂 2026-09-18-po13-jian-8 13 减 8 怎么拆
375
+ 小课堂 2026-10-06-pingjunfen 平均分怎么分
376
+ \`\`\`
377
+
378
+ \`\`\`text
379
+ # 今晚
380
+ 先读课文,再练两道题
381
+ \`\`\`
382
+
383
+ \`\`\`tianzige
384
+ 塘脑袋
385
+ táng nǎo dai
386
+ \`\`\`
387
+
388
+ \`\`\`dictation chinese-tutor
389
+ 春天
390
+ 鼓励 鼓励,老师鼓励我的鼓励
391
+ 叶 树叶的叶
392
+ \`\`\`
393
+
394
+ \`\`\`dictation english-tutor
395
+ red
396
+ yellow yellow, a yellow bus
397
+ 🚌 bus
398
+ \`\`\`
399
+
400
+ ## 为什么这么排
401
+
402
+ - mock 的样例
403
+ `;
404
+ }
405
+ /** 拍照认词表的假识别:不看照片,照《小蝌蚪找妈妈》那页回(词语一行、写字一行,宽大没认准,识字不默认选),再回三行全文;一行隔 100 毫秒,像真的一行一行出来 */
406
+ const MOCK_SAY = { 脑袋: '小蝌蚪的大脑袋', 灰色: '灰色的小蝌蚪', 眼睛: '大大的眼睛', 肚皮: '白白的肚皮', 孩子: '青蛙妈妈的孩子', 宽大: '宽大的嘴巴', 两: '两个人的两', 哪: '在哪里的哪', 塘: '池塘的塘' };
407
+ const MOCK_PHOTO = [
408
+ ...['脑袋', '灰色', '眼睛', '肚皮', '孩子', '宽大'].map((text, i) => ({ text, kind: 'word', pick: true, box: [80 + i * 140, 640, 190 + i * 140, 700], sure: text !== '宽大', say: `${text},${MOCK_SAY[text]}` })),
409
+ ...['两', '哪'].map((text, i) => ({ text, kind: 'char', pick: true, box: [80 + i * 92, 480, 150 + i * 92, 550], sure: true, say: `${text},${MOCK_SAY[text]}` })),
410
+ { text: '塘', kind: 'read', pick: false, box: [80, 320, 150, 390], sure: true, say: `塘,${MOCK_SAY['塘']}` },
411
+ ];
412
+ const MOCK_FULL = ['池塘 里 有 一群 小蝌蚪 , 大大的 脑袋 , 黑灰色 的 身子 。', '小蝌蚪 游 哇 游 , 过了 几天 , 长出 两条 后腿 。', '他们 看见 鲤鱼 妈妈 在 教 小鲤鱼 捕食 。'];
413
+ /** 英文单词的假识别(英语老师卡上的拍照听写):一页单词表 */
414
+ const MOCK_PHOTO_EN = ['red', 'yellow', 'green', 'bus'].map((text, i) => ({ text, kind: 'word', pick: true, box: [80 + i * 200, 600, 240 + i * 200, 660], sure: true, say: `${text}, a ${text} ${text === 'bus' ? 'stop' : 'car'}`, ...(text === 'bus' ? { emoji: '🚌' } : {}) }));
415
+ const MOCK_FULL_EN = ['What colour is it', "It's yellow", 'I can see a red bus'];
416
+ const mockRecognize = async (_image, onProgress, lang) => {
417
+ const t0 = Date.now();
418
+ const en = lang === 'en';
419
+ const p = { items: [], lesson: en ? 'Unit 3 Colours' : '小蝌蚪找妈妈', full: [] };
420
+ onProgress({ ...p });
421
+ if (en) {
422
+ for (const it of MOCK_PHOTO_EN) {
423
+ await new Promise((r) => setTimeout(r, 100));
424
+ p.items = [...p.items, it];
425
+ onProgress({ ...p });
426
+ }
427
+ p.full = MOCK_FULL_EN;
428
+ onProgress({ ...p });
429
+ return { ok: true, model: 'mock', ...p, ms: Date.now() - t0 };
430
+ }
431
+ for (const it of MOCK_PHOTO) {
432
+ await new Promise((r) => setTimeout(r, 100));
433
+ p.items = [...p.items, it];
434
+ onProgress({ ...p });
435
+ }
436
+ for (const f of MOCK_FULL) {
437
+ await new Promise((r) => setTimeout(r, 100));
438
+ p.full = [...p.full, f];
439
+ onProgress({ ...p });
440
+ }
441
+ return { ok: true, model: 'mock', ...p, ms: Date.now() - t0 };
442
+ };
443
+ const pad = (n) => String(n).padStart(2, '0');
444
+ const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
445
+ const isObj = (x) => typeof x === 'object' && x !== null && !Array.isArray(x);
446
+ export function createMock(opts = {}) {
447
+ const scenario = opts.scenario ?? 'normal';
448
+ const delay = opts.delayMs ?? 1800;
449
+ const now = opts.now ?? (() => new Date());
450
+ const yesterday = () => localDate(new Date(now().getTime() - 86400000));
451
+ const title = opts.title ?? '小明的老师们';
452
+ const messages = new Map();
453
+ /** 左栏的纸:老师|话题 → 那张纸;不落盘 */
454
+ const papers = new Map();
455
+ /** 孩子端传上来的图(照片、纸上交的画、清空前那几版):路径 → 字节,看大图、画卡缩略图给真图;别的路径照旧占位 */
456
+ const uploads = new Map();
457
+ /** 以前的:昨天每位讲过课的老师有一个话题(拿脚本最后一节充数),只读回放用 */
458
+ const past = new Map();
459
+ const cursor = new Map();
460
+ const inflight = new Map();
461
+ let seq = 0;
462
+ /** 孩子端发来的实录(不打盘):老师 → [{话题, 记录}] */
463
+ const mockPlays = {};
464
+ const nextJob = () => { const d = now(); return `${pad(d.getHours())}${pad(d.getMinutes())}-${++seq}`; };
465
+ for (const t of MOCK_TUTORS) {
466
+ const list = [];
467
+ for (let i = 0; i < t.preloaded && i < t.script.length; i++) {
468
+ const section = withMockLooks(sectionFromScript(t.script[i]), t.name, i);
469
+ const job = nextJob();
470
+ list.push({ job, thread: list[0]?.thread ?? job, at: now().toISOString(), question: i === 0 ? t.firstQuestion : '继续', reply: section.lines[section.lines.length - 1]?.text ?? null, pending: false, artifacts: [], section });
471
+ }
472
+ messages.set(t.name, list);
473
+ cursor.set(t.name, Math.min(t.preloaded, t.script.length));
474
+ if (t.preloaded && t.script.length) {
475
+ const section = sectionFromScript(t.script[t.script.length - 1]);
476
+ const job = `0930-${t.name.length}`;
477
+ past.set(t.name, [{ job, thread: job, at: `${yesterday()}T09:30`, question: '昨天问的:' + t.firstQuestion, reply: section.lines[section.lines.length - 1]?.text ?? null, pending: false, artifacts: [], section }]);
478
+ }
479
+ }
480
+ /** 话题打星(家长端清单上):<老师>/<话题> → 1–5;记过账的话题(同样的键) */
481
+ const ratings = new Map();
482
+ const booked = new Set();
483
+ /** 孩子这回发的消息是真钟几点发的(mock 的 now 可能是假钟;画图卡「在画」按它算,种子里的直接是画好的) */
484
+ const sentAt = new Map();
485
+ /** 画图卡(mock 不真画):发出后 MOCK_PAINT_MS 内是「画家在画」,之后给一张占位的四格(/api/kid/image 认 mock-paint/);家长端多一行 note */
486
+ const mockPaints = (section, m, parent = false) => {
487
+ if (!section.cards.some((c) => c.kind === 'paint'))
488
+ return section;
489
+ const t = sentAt.get(m.job);
490
+ const drawing = t !== undefined && Date.now() - t < MOCK_PAINT_MS;
491
+ return { ...section, cards: section.cards.map((c, n) => (c.kind !== 'paint' ? c : { ...c, props: { ...c.props, drawn: drawing ? 'drawing' : 'done', ...(drawing ? {} : { pic: `mock-paint/${m.job}-${n}.png` }), ...(parent ? { note: drawing ? '在画(mock)' : '画好了:qwen-image-2.0-pro,15 秒(mock 的占位图)' } : {}) } })) };
492
+ };
493
+ /** 下发孩子端的形状:答案剥掉、状态并到卡上、小课堂卡补那一段的起止(样本课包在仓库里) */
494
+ const kidMessage = async (m) => {
495
+ const { states, action: _a, submitted: _s, lectureWatch: _w, lectureLog: _l, drawing, ...rest } = m;
496
+ if (rest.lecture?.marks)
497
+ rest.lecture = { ...rest.lecture, marks: rest.lecture.marks.map(({ text: _t, ...k }) => k) };
498
+ const mine = [...messages.values()].find((l) => l.includes(m)) ?? [];
499
+ const done = new Set(mine.flatMap((x) => (x.submitted ? [x.submitted] : [])));
500
+ // 卡的配音同真服务挂在 assets 上(<job>.cards/<n>/<段号>.mp3),都是静音(见 /api/audio)
501
+ const voiced = (c, i) => { const a = cardAssets(c); return a.length ? { assets: a.map((x) => `mock.${m.job}.cards/${i}/${x.file}`) } : {}; };
502
+ const withState = m.section ? { ...m.section, cards: m.section.cards.map((c, i) => ({ ...c, ...voiced(c, i), ...(done.has(`${m.job}/${i}`) ? { submitted: true } : {}), ...(states && i in states ? { state: states[i] } : {}) })) } : null;
503
+ const section = withState ? mockPaints(await enrichLectures(MOCK_LECTURE_DIRS, stripSecrets(withState)), m) : null;
504
+ return { ...rest, ...(drawing && rest.question !== null ? { drawing: { image: drawing.image, strokes: drawing.strokes, ...(drawing.ms !== undefined ? { ms: drawing.ms } : {}) } } : {}), section };
505
+ };
506
+ // 学科色同真服务:按老师的先后定(src/ui/shared/tutor-color.ts)
507
+ const colors = tutorColors(Object.fromEntries(MOCK_TUTORS.map((t) => [t.name, { subject: t.subject }])));
508
+ const tutorsJson = () => MOCK_TUTORS.map((t) => ({ name: t.name, display: t.display, avatar: t.avatar, subject: t.subject, color: colors[t.name], mark: boardMarkOf(t.subject), lang: subjectLang(t.subject), hasVoice: false, extras: tutorExtras({ subject: t.subject }).filter((x) => x !== 'talk'), listen: 'hold' }));
509
+ /** 小课堂:样本课包与样本视频在仓库里(tests/fixtures/bundles/、lectures/),起来时和真服务一样读一遍(readLecture)、记住 */
510
+ const lectures = new Map();
511
+ const lecturesLoaded = (async () => {
512
+ for (const dir of [MOCK_BUNDLES_DIR, MOCK_LECTURES_DIR]) {
513
+ for (const id of await readdir(dir).catch(() => [])) {
514
+ const l = await readLecture(MOCK_LECTURE_DIRS, id);
515
+ if (l && !lectures.has(id))
516
+ lectures.set(id, l);
517
+ }
518
+ }
519
+ })();
520
+ const mockLecture = (id) => { const l = lectures.get(id); return l ? { ...l, ms: l.clock.total } : null; };
521
+ const home = () => {
522
+ const d = now();
523
+ const parsed = parseHome(mockHomeMd(localDate(d), yesterday())).cards;
524
+ const names = MOCK_TUTORS.map((t) => t.name);
525
+ const ownerOf = homeOwners(parsed, names);
526
+ const owners = new Map(parsed.map((c, i) => [c, ownerOf[i]]));
527
+ const cards = arrangeHome(parsed, names).map((c) => {
528
+ if (c.kind !== 'tutor')
529
+ return { ...stripSecrets({ cards: [c], lines: [] }).cards[0], owner: owners.get(c) ?? null }; // 同真服务:答案(听写卡的字)不下发;非老师卡带归属
530
+ const name = String(c.props.tutor);
531
+ const list = messages.get(name) ?? [];
532
+ const asked = list.find((m) => m.question !== null && m.thread === list[list.length - 1]?.thread);
533
+ // mock 的话题都是新话题进去的:今天孩子问过的那个,挂在新话题按钮上(空白板上的胶囊)
534
+ const resumes = asked ? new Map([['free', { thread: asked.thread, title: (asked.question ?? '').slice(0, 10), at: list[list.length - 1]?.at ?? '' }]]) : undefined;
535
+ const alive = (date, thread) => (date === yesterday() && (past.get(name) ?? []).some((m) => m.thread === thread)) || (date === localDate(d) && list.some((m) => m.thread === thread));
536
+ return { kind: 'tutor', props: { tutor: name, buttons: kidButtons((c.props.buttons ?? []), { resumes, alive, lecture: (id) => Boolean(mockLecture(id)), today: localDate(d) }).map((b) => { const l = b.kind === 'lecture' ? mockLecture(b.bundle) : null; return l ? { ...b, title: l.title, ms: l.ms, ...(l.video ? { video: true } : {}) } : b; }) } };
537
+ });
538
+ // figshot:和配了 figshot 的 workspace 一样给端口;这台电脑上 figshot 没开着,页面照样藏着这张卡
539
+ return { title, date: localDate(d), tutors: tutorsJson(), home: mockHomeId(), cards, figshot: { port: 8477 }, hand: 'right', ready: scenario !== 'nokey' };
540
+ };
541
+ const mockHomeId = () => `${yesterday()}-2130`;
542
+ let dictationRoot = null;
543
+ // 听写的暂存放临时目录,进程退出时删掉(不删每起一次 mock 留一个,攒到上千个)
544
+ const dictationDir = () => {
545
+ if (dictationRoot)
546
+ return dictationRoot;
547
+ const dir = (dictationRoot = mkdtempSync(join(tmpdir(), 'cotutor-mock-dictation-')));
548
+ process.once('exit', () => rmSync(dir, { recursive: true, force: true }));
549
+ return dir;
550
+ };
551
+ const dictationDeps = () => ({
552
+ root: dictationDir(),
553
+ now,
554
+ card: async (home, n) => {
555
+ if (home !== mockHomeId())
556
+ return null;
557
+ const c = parseHome(mockHomeMd(localDate(now()), yesterday())).cards.filter((x) => x.kind === 'dictation')[n];
558
+ const r = c ? DictationPropsSchema.safeParse(c.props) : null;
559
+ return r?.success ? r.data : null;
560
+ },
561
+ // 同真服务:卡上写的(或拍照听写从哪位老师卡上进来的)老师在就用他,不然语文老师
562
+ reader: (t) => (t && MOCK_TUTORS.some((x) => x.name === t) ? t : 'chinese-tutor'),
563
+ display: (t) => MOCK_TUTORS.find((x) => x.name === t)?.display ?? '老师',
564
+ face: (t) => backTutorOf(Object.fromEntries(MOCK_TUTORS.map((x) => [x.name, { display: x.display, subject: x.subject }])), t),
565
+ // 念的是一段 0.8 秒的静音(不配音、不念浏览器合成声):页面照真服务那样放音频、等放完
566
+ say: async () => {
567
+ const f = join(dictationDir(), 'say-silence.wav');
568
+ if (!existsSync(f))
569
+ writeFileSync(f, wavOfPcm(Buffer.alloc(24000 * 2 * 0.8), 24000));
570
+ return f;
571
+ },
572
+ voiced: () => true,
573
+ medians: async (ch) => (await tianzigeData(ch))?.medians ?? null,
574
+ recognize: mockRecognize,
575
+ });
576
+ /** via → 按钮(真服务在 runtime/home/store.ts resolveVia;mock 从同一份原文取) */
577
+ const mockButton = (name, via) => {
578
+ if (!isObj(via))
579
+ return null;
580
+ if (via.button === 'new' || via.button === 'recent')
581
+ return via.button;
582
+ if (via.home !== mockHomeId() || typeof via.button !== 'number')
583
+ return null;
584
+ const card = parseHome(mockHomeMd(localDate(now()), yesterday())).cards.find((c) => c.kind === 'tutor' && c.props.tutor === name);
585
+ return (card?.props.buttons ?? [])[via.button] ?? null;
586
+ };
587
+ const answer = (m, full) => {
588
+ if (full) {
589
+ m.section = full;
590
+ m.reply = full.lines[full.lines.length - 1]?.text ?? null;
591
+ }
592
+ else {
593
+ m.section = sectionFromScript('这个我们明天接着说,好不好?');
594
+ m.reply = '这个我们明天接着说,好不好?';
595
+ }
596
+ m.pending = false;
597
+ };
598
+ /** 流式模拟:想的期间每隔一段露一张卡(讲稿句跟到那张卡为止),整段想完才定稿 */
599
+ const think = (t, m, cur = cursor) => {
600
+ const i = cur.get(t.name) ?? 0;
601
+ // 看完小课堂的第一问:老师放课里的那一段(小课堂卡 + [[play]]);不占脚本的位置
602
+ const lectureFirst = Boolean(m.lecture && !m.lecture.again);
603
+ const step = lectureFirst ? (m.lecture?.video ? MOCK_VIDEO_LECTURE_REPLY : MOCK_LECTURE_REPLY) : t.script[i];
604
+ if (!lectureFirst)
605
+ cur.set(t.name, i + 1);
606
+ const full = step ? withMockLooks(sectionFromScript(step), t.name, i) : null;
607
+ const n = full ? full.cards.length : 0;
608
+ const tick = delay / (n + 1);
609
+ return new Promise((resolve) => {
610
+ let k = 0;
611
+ const reveal = () => {
612
+ k++;
613
+ if (k <= n && full) {
614
+ // 拍的就绪同真服务一个规则(readyBeats;mock 没配音 = 不等配音):露到第 k 张卡时前 k-1 拍关了 → 页面就绪一拍播一拍;卡前的句(锚 null)也在,和真 runner 一样
615
+ // 铺到第 k 张时前 k-1 张的样子与行都已定(卡带 look,layout 只到已定的那几张)
616
+ const part = { cards: full.cards.slice(0, k).map((c, i) => (i < k - 1 ? c : (({ look: _l, ...rest }) => rest)(c))), lines: full.lines.filter((l) => l.anchor === null || l.anchor < k - 1) };
617
+ const rows = full.layout ? full.layout.rows.map((r) => r.filter((i) => i < k - 1)).filter((r) => r.length) : [];
618
+ m.section = { ...part, ...(full.layout && rows.length ? { layout: { for: full.layout.for, rows } } : {}), partial: true, ready: readyBeats(part, { done: false }) };
619
+ setTimeout(reveal, tick);
620
+ }
621
+ else {
622
+ answer(m, full);
623
+ resolve();
624
+ }
625
+ };
626
+ setTimeout(reveal, tick);
627
+ });
628
+ };
629
+ const route = async (method, path, body) => {
630
+ await lecturesLoaded;
631
+ const url = new URL(path, 'http://x');
632
+ const p = url.pathname;
633
+ if (p === '/')
634
+ return { status: 200, html: kidPage(title, {}, await stageVersion()) };
635
+ if (p === '/dev/qr')
636
+ return mock.listen ? { status: 200, html: qrPage(title, mock.listen(), url.searchParams.get('via') === 'ip' ? 'ip' : 'name', url.searchParams.get('to') === 'parent' ? 'parent' : 'kid') } : { status: 404, json: { error: 'not_listening' } };
637
+ if (p === '/dev/voice')
638
+ return { status: 200, html: VOICE_GALLERY_PAGE };
639
+ if (p === '/manifest.webmanifest')
640
+ return { status: 200, json: webManifest(title), contentType: 'application/manifest+json; charset=utf-8' };
641
+ // 家长端「记录」:自己的一页(parent-page.ts);清单接口照真服务(答案不剥,第一节带一条给家长的尾巴与记忆)。mock 没有工作台 /dev
642
+ if (p === '/parent')
643
+ return { status: 200, html: parentPage(title) };
644
+ if (p === '/reel')
645
+ return { status: 200, html: kidPage(title, { reel: true }, await stageVersion()) };
646
+ if (p === '/preview')
647
+ return { status: 200, html: kidPage(title, { preview: true }, await stageVersion()) };
648
+ if (p === '/parent/manifest.webmanifest')
649
+ return { status: 200, json: webManifest(`${title} · 家长`, { startUrl: '/parent', scope: '/parent' }), contentType: 'application/manifest+json; charset=utf-8' };
650
+ // 主题:mock 没有 workspace,直接给包里的出厂 default
651
+ if (p === '/kid/theme.css')
652
+ return { status: 200, html: `${cardsCss()}\n\n${(await packageTheme()).css}`, contentType: 'text/css; charset=utf-8' };
653
+ if (p === '/kid/theme.json')
654
+ return { status: 200, json: (await packageTheme()).manifest };
655
+ const icon = /^\/icon-(\d{2,4})\.png$/.exec(p);
656
+ if (icon) {
657
+ const n = Number(icon[1]);
658
+ return ICON_SIZES.includes(n) ? { status: 200, body: appIconPng(n), contentType: 'image/png' } : { status: 404, json: { error: 'not_found' } };
659
+ }
660
+ const fav = faviconResponse(p);
661
+ if (fav)
662
+ return { status: 200, ...fav, cacheControl: 'no-cache' };
663
+ if (p === '/api/health')
664
+ return { status: 200, json: { ok: true, mock: true, scenario } };
665
+ if (p === '/api/kid/home' && method === 'GET')
666
+ return { status: 200, json: home() };
667
+ // 孩子端的流水(kid-log.ts):mock 照收,不落盘
668
+ if (p === '/api/kid/log' && method === 'POST')
669
+ return { status: 200, json: { ok: true } };
670
+ // 听写(src/app/dictation/,和真服务同一套接口):卡从 mock 的首页取,存在临时目录,念的是静音
671
+ const tx = await handleDictation(method, url, dictationDeps(), body);
672
+ if (tx)
673
+ return tx;
674
+ const ov = /^\/api\/overview\/(today|\d{4}-\d{2}-\d{2})$/.exec(p);
675
+ if (ov && method === 'GET') {
676
+ const today = localDate(now());
677
+ const date = ov[1] === 'today' ? today : ov[1];
678
+ if (date > today)
679
+ return { status: 400, json: { error: 'bad_request' } };
680
+ const listOf = (name, d = date) => (d === today ? messages.get(name) : d === yesterday() ? past.get(name) : undefined) ?? [];
681
+ const tutors = MOCK_TUTORS.map((t) => {
682
+ const list = listOf(t.name);
683
+ const by = new Map();
684
+ for (const m of list) {
685
+ let th = by.get(m.thread);
686
+ if (!th) {
687
+ th = { thread: m.thread, at: m.at, title: Array.from((m.question ?? '').trim()).slice(0, 20).join(''), from: 'kid', via: null, start: { kind: 'new', label: '新话题' }, said: (m.question ?? '').trim() || null, endAt: null, notes: [], remembered: [], writes: [], sections: 0, cards: 0, stoppedAt: null, rating: ratings.get(`${t.name}/${m.thread}`) ?? null, booked: booked.has(`${t.name}/${m.thread}`) };
688
+ by.set(m.thread, th);
689
+ }
690
+ // 给家长的、记住了:同 mock 的板书接口,那天第一条带
691
+ if (m === list[0] && !m.pending) {
692
+ th.notes.push('第一遍就答上了,后面那句是我故意留的:看他会不会自己往下想。');
693
+ th.remembered.push(`${date} 讲故事时爱抢着说结局,可以先让他猜`);
694
+ }
695
+ if (m.pending)
696
+ th.stoppedAt = 'writing';
697
+ else if (m.section && (m.section.cards.length || m.section.lines.length)) {
698
+ th.sections++;
699
+ th.cards += m.section.cards.length;
700
+ th.stoppedAt = m.section.lines[m.section.lines.length - 1]?.ask ? 'ask' : null;
701
+ }
702
+ }
703
+ // 按住说话连着没发出去:攒着的实录里的 hold;mock 的消息没有真时刻,发成的一句只认打字发出去的
704
+ if (date === today)
705
+ for (const th of by.values()) {
706
+ const plays = (mockPlays[t.name] ?? []).filter((x) => x.thread === th.thread).map((x) => x.rec);
707
+ const n = Math.max(0, ...reelMissRuns(reelHolds(plays), plays.filter((r) => r.k === 'type' && r.e === 'send').map((r) => r.at)).map((x) => x.n));
708
+ if (n)
709
+ th.misses = n;
710
+ }
711
+ return { name: t.name, display: t.display, avatar: t.avatar, subject: t.subject, color: colors[t.name], turns: list.length, threads: [...by.values()], booking: false, extras: tutorExtras({ subject: t.subject }).filter((x) => x !== 'talk') };
712
+ });
713
+ const week = weekOf(date, today).map((d) => ({ date: d, tutors: MOCK_TUTORS.filter((t) => listOf(t.name, d).length).map((t) => t.name) }));
714
+ const out = { title, date, today, tutors, week, ready: scenario !== 'nokey' };
715
+ return { status: 200, json: out };
716
+ }
717
+ const pb = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(today|\d{4}-\d{2}-\d{2})\/board$/.exec(p);
718
+ if (pb && method === 'GET') {
719
+ const [, name, tail] = pb;
720
+ const t = MOCK_TUTORS.find((x) => x.name === name);
721
+ if (!t)
722
+ return { status: 404, json: { error: 'no_such_tutor' } };
723
+ const today = localDate(now());
724
+ const date = tail === 'today' ? today : tail;
725
+ if (date > today)
726
+ return { status: 400, json: { error: 'bad_request' } };
727
+ const list = (date === today ? messages.get(name) : date === yesterday() ? past.get(name) : undefined) ?? [];
728
+ const out = await Promise.all(list.map(async (m, i) => {
729
+ const { states, ...rest } = m;
730
+ const withState = (c, k) => (states && k in states ? { ...c, state: states[k] } : c);
731
+ const section = m.section ? mockPaints(await enrichLectures(MOCK_LECTURE_DIRS, { ...m.section, cards: m.section.cards.map(withState) }), m, true) : null;
732
+ return { ...rest, from: 'kid', section, ...(i === 0 && !m.pending ? { parentText: '## 家长\n第一遍就答上了,后面那句是我故意留的:看他会不会自己往下想。', remembered: [`${date} 讲故事时爱抢着说结局,可以先让他猜`] } : {}) };
733
+ }));
734
+ const pending = list.find((m) => m.pending);
735
+ return { status: 200, json: { tutor: name, date, messages: out, pending: pending ? pending.job : null, thread: list.length ? list[list.length - 1].thread : null } };
736
+ }
737
+ // 看录像:mock 的消息没有真时刻(种子全是同一刻),照一个固定节奏排——
738
+ // 等老师 3 秒(第二轮 14 秒,看「等老师」的压缩)、念完孩子想 8 秒(第一轮 40 秒,看「孩子想了」);卡的状态算在下一轮开口前 5 秒
739
+ /** 看小课堂的那条:录像要它的看的过程(mock 的家长端条目带着 lecture 与 lectureLog) */
740
+ const lectureOfMock = (m) => {
741
+ const l = m.lecture;
742
+ if (!l)
743
+ return {};
744
+ return { lecture: { bundle: l.bundle, title: l.title, watchedMs: 0, finished: true, pauses: 0, ...(l.again ? { again: true } : {}), ...(l.video ? { video: true } : {}), ...(l.marks ? { marks: l.marks } : {}), ...(Array.isArray(m.lectureLog) ? { log: m.lectureLog } : {}) } };
745
+ };
746
+ const rl = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(today|\d{4}-\d{2}-\d{2})\/threads\/([^/]+)\/reel$/.exec(p);
747
+ if (rl && method === 'GET') {
748
+ const [, name, tail, raw] = rl;
749
+ const board = await route('GET', `/api/conversations/${name}/${tail}/board`);
750
+ if (board.status !== 200)
751
+ return board;
752
+ const day = board.json;
753
+ const thread = decodeURIComponent(raw);
754
+ const mine = day.messages.filter((m) => m.thread === thread);
755
+ let t = now().getTime() - 10 * 60000;
756
+ const starts = [];
757
+ const conv = mine.map((m, k) => {
758
+ const wait = k === 1 ? 14000 : 3000;
759
+ starts.push(t);
760
+ const startedAt = new Date(t).toISOString();
761
+ t += wait + (m.section?.lines ?? []).reduce((n, l) => n + lineDurationMs(l.text) + 200, 0) + (k === 0 ? 40000 : 8000);
762
+ return { job: m.job, thread, at: startedAt, from: m.from, text: m.question ?? '', result: m.pending ? 'running' : 'ok', artifacts: [], section: m.section, timing: { startedAt, doneMs: wait }, ...(m.action ? { action: m.action } : {}), ...lectureOfMock(m) };
763
+ });
764
+ const cards = {};
765
+ mine.forEach((m, k) => (m.section?.cards ?? []).forEach((c, n) => { if (c.state !== undefined)
766
+ (cards[m.job] ??= {})[n] = { at: new Date((starts[k + 1] ?? t) - 5000).toISOString(), turn: m.job, state: c.state }; }));
767
+ const plays = (mockPlays[name] ?? []).filter((x) => x.thread === thread).map((x) => x.rec);
768
+ const reel = buildReel({ messages: conv, events: {}, cards, now: now().getTime(), plays });
769
+ // mock 的孩子接口只认 today 是今天(写日期的走「以前的」那份)
770
+ const kidDay = await route('GET', `/api/kid/conversations/${name}/${tail === localDate(now()) ? 'today' : tail}`);
771
+ const kid = kidDay.status === 200 ? kidDay.json.messages.filter((m) => m.thread === thread) : [];
772
+ const tt = MOCK_TUTORS.find((x) => x.name === name);
773
+ const face = { name, display: tt?.display ?? name, avatar: tt?.avatar ?? null, subject: tt?.subject ?? null, color: colors[name] ?? NEUTRAL_COLOR, mark: boardMarkOf(tt?.subject) };
774
+ return reel ? { status: 200, json: { tutor: name, date: day.date, thread, reel, messages: mine, kid, device: null, face } } : { status: 404, json: { error: 'no_reel' } };
775
+ }
776
+ // 记账(家长端清单上的「记账」):这天还没记过的话题都算记了(真服务是每个话题起一轮老师;mock 立刻记上)
777
+ const bk = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/bookkeep$/.exec(p);
778
+ if (bk && method === 'POST') {
779
+ const [, name] = bk;
780
+ const list = messages.get(name);
781
+ if (!list)
782
+ return { status: 404, json: { error: 'no_such_tutor' } };
783
+ const queued = [...new Set(list.filter((m) => m.section && !m.pending).map((m) => m.thread))].filter((th) => !booked.has(`${name}/${th}`));
784
+ for (const th of queued)
785
+ booked.add(`${name}/${th}`);
786
+ return { status: 202, json: { tutor: name, queued, skipped: [] } };
787
+ }
788
+ // 删掉一个话题:列表里去掉;还在想的 409(mock 没有 vault,撤销记 0)
789
+ const del = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/threads\/([^/]+)$/.exec(p);
790
+ if (del && method === 'DELETE') {
791
+ const [, name, , thread] = del;
792
+ const store = messages;
793
+ const list = store.get(name);
794
+ if (!list || !list.some((m) => m.thread === thread))
795
+ return { status: 404, json: { error: 'no_such_thread' } };
796
+ if (list.some((m) => m.thread === thread && m.pending))
797
+ return { status: 409, json: { error: 'busy' } };
798
+ store.set(name, list.filter((m) => m.thread !== thread));
799
+ ratings.delete(`${name}/${thread}`);
800
+ return { status: 200, json: { tutor: name, thread, undo: { memory: 0, diary: 0, misses: [] } } };
801
+ }
802
+ const rate = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/threads\/([^/]+)\/rating$/.exec(p);
803
+ if (rate && method === 'PUT') {
804
+ const [, name, , thread] = rate;
805
+ if (!MOCK_TUTORS.some((x) => x.name === name))
806
+ return { status: 404, json: { error: 'no_such_tutor' } };
807
+ const rating = isObj(body) ? body.rating : undefined;
808
+ if (!(rating === null || (typeof rating === 'number' && Number.isInteger(rating) && rating >= 1 && rating <= 5)))
809
+ return { status: 400, json: { error: 'bad_request' } };
810
+ if (rating === null)
811
+ ratings.delete(`${name}/${thread}`);
812
+ else
813
+ ratings.set(`${name}/${thread}`, rating);
814
+ return { status: 200, json: { tutor: name, thread, rating } };
815
+ }
816
+ const pk = p;
817
+ const kid = /^\/api\/kid\/conversations\/([a-z0-9][a-z0-9-]*)\/(today|messages|history|photos|play|paper|\d{4}-\d{2}-\d{2})$/.exec(pk);
818
+ if (kid) {
819
+ const [, name, tail] = kid;
820
+ const t = MOCK_TUTORS.find((x) => x.name === name);
821
+ if (!t)
822
+ return { status: 404, json: { error: 'no_such_tutor' } };
823
+ const list = messages.get(name) ?? [];
824
+ const date = localDate(now());
825
+ // 录像的实录:收下就丢(mock 的录像按固定节奏排,不用它);不收的话页面每 10 秒撞一个 404
826
+ // 实录:不打盘,攒在内存里给录像用(按 sentAt 校钟、形状不对的丢掉、孩子端发的卡改动不收,同真服务)
827
+ if (tail === 'play' && method === 'POST') {
828
+ if (!isObj(body) || typeof body.thread !== 'string' || !Array.isArray(body.records))
829
+ return { status: 200, json: { kept: 0 } };
830
+ const skew = typeof body.sentAt === 'number' ? now().getTime() - body.sentAt : 0;
831
+ const kept = body.records.filter((x) => playRecordOk(x) && x.k !== 'card').map((x) => ({ ...x, at: Math.round(x.at + skew) }));
832
+ (mockPlays[name] ??= []).push(...kept.map((rec) => ({ thread: body.thread, rec })));
833
+ return { status: 200, json: { kept: kept.length } };
834
+ }
835
+ // 作业照片:不落盘,回一个像样的假路径(缩略图由 /api/kid/image 的占位 svg 顶)
836
+ if (tail === 'photos' && method === 'POST') {
837
+ if (!isObj(body) || typeof body.image !== 'string' || !body.image.startsWith('data:image/'))
838
+ return { status: 400, json: { error: 'bad_request' } };
839
+ const d = now();
840
+ const up = /^data:(image\/(?:jpeg|png));base64,(.+)$/.exec(body.image);
841
+ const path = `captures/${date}/${String(d.getHours()).padStart(2, '0')}${String(d.getMinutes()).padStart(2, '0')}-${list.length + 1 + uploads.size}.${up && up[1] === 'image/png' ? 'png' : 'jpg'}`;
842
+ if (up)
843
+ uploads.set(path, { type: up[1], data: Buffer.from(up[2], 'base64') });
844
+ return { status: 201, json: { path } };
845
+ }
846
+ // 左栏的纸:同真服务(app.ts 的 paperRoute),存在内存里
847
+ if (tail === 'paper') {
848
+ const key = method === 'GET' ? (url.searchParams.get('key') ?? '') : isObj(body) && typeof body.key === 'string' ? body.key : '';
849
+ if (!PAPER_KEY_RE.test(key))
850
+ return { status: 400, json: { error: 'bad_request' } };
851
+ if (method === 'GET')
852
+ return { status: 200, json: { key, state: papers.get(`${name}|${key}`) ?? { ink: [] } } };
853
+ if (method !== 'PUT')
854
+ return { status: 405, json: { error: 'method_not_allowed' } };
855
+ const r = CanvasStateSchema.safeParse(isObj(body) ? body.state : null);
856
+ if (!r.success)
857
+ return { status: 400, json: { error: 'bad_state' } };
858
+ papers.set(`${name}|${key}`, { ink: r.data.ink, ...(r.data.cleared?.length ? { cleared: r.data.cleared } : {}) });
859
+ if (isObj(body) && body.from === 'new' && key !== 'new')
860
+ papers.delete(`${name}|new`);
861
+ return { status: 200, json: { ok: true, key } };
862
+ }
863
+ if (tail === 'today' && method === 'GET') {
864
+ const shown = (messages.get(name) ?? []);
865
+ const pending = shown.find((m) => m.pending);
866
+ const out = { tutor: name, date, messages: await Promise.all(shown.map(kidMessage)), pending: pending ? pending.job : null, thread: shown.length ? shown[shown.length - 1].thread : null };
867
+ return { status: 200, json: out };
868
+ }
869
+ if (tail === 'history' && method === 'GET') {
870
+ const days = [];
871
+ const todayThreads = kidThreads(await Promise.all((messages.get(name) ?? []).map(kidMessage))).reverse();
872
+ if (todayThreads.length)
873
+ days.push({ date, threads: todayThreads });
874
+ const old = past.get(name) ?? [];
875
+ if (old.length)
876
+ days.push({ date: yesterday(), threads: kidThreads(old) });
877
+ const out = { tutor: name, today: date, days };
878
+ return { status: 200, json: out };
879
+ }
880
+ if (/^\d{4}-/.test(tail) && method === 'GET') {
881
+ if (tail > date)
882
+ return { status: 400, json: { error: 'bad_request' } };
883
+ const old = tail === yesterday() ? (past.get(name) ?? []) : [];
884
+ const out = { tutor: name, date: tail, messages: await Promise.all(old.map(kidMessage)), pending: null, thread: old.length ? old[old.length - 1].thread : null };
885
+ return { status: 200, json: out };
886
+ }
887
+ if (tail === 'messages' && method === 'POST') {
888
+ if (!isObj(body) || typeof body.text !== 'string')
889
+ return { status: 400, json: { error: 'bad_request' } };
890
+ const action = body.action === 'submit' ? 'submit' : undefined;
891
+ const photos = Array.isArray(body.photos) ? body.photos.filter((x) => typeof x === 'string') : [];
892
+ const button = body.via === undefined ? undefined : mockButton(name, body.via);
893
+ if (button === null)
894
+ return { status: 400, json: { error: 'bad_via' } };
895
+ let said = body.text;
896
+ let newThread = body.newThread === true;
897
+ let wantThread = body.thread;
898
+ // 小课堂:看完才能开口,字是孩子说的,新话题;画的那几处现算成一段话(同 runner)。问过以后再看一遍又画了:同一话题的下一条带上(again)
899
+ // 一处几笔(strokes);2026-10-19 以前一笔一处(path),照读
900
+ const marksIn = isObj(body.lecture) && Array.isArray(body.lecture.marks) ? body.lecture.marks.map(({ path, strokes, ...m }) => ({ ...m, strokes: strokes ?? (path ? [path] : []) })) : [];
901
+ const logIn = isObj(body.lecture) && Array.isArray(body.lecture.log) ? body.lecture.log : [];
902
+ const lec = isObj(body.lecture) && typeof body.lecture.bundle === 'string' ? { bundle: body.lecture.bundle, finished: body.lecture.finished === true, again: false, watchedMs: Number(body.lecture.watchedMs) || 0, pauses: Number(body.lecture.pauses) || 0 } : null;
903
+ if (lec && (typeof button !== 'object' || button === null || button.kind !== 'lecture')) {
904
+ const from = list.find((x) => x.thread === body.thread && x.lecture && !x.lecture.again);
905
+ if (!from || from.lecture?.bundle !== lec.bundle || (!marksIn.length && !logIn.length))
906
+ return { status: 400, json: { error: 'bad_request' } };
907
+ lec.again = true;
908
+ }
909
+ if (typeof button === 'object' && button !== null && button.kind === 'lecture') {
910
+ if (!lec || lec.bundle !== button.bundle || !lec.finished)
911
+ return { status: 400, json: { error: 'bad_request' } };
912
+ newThread = true;
913
+ wantThread = undefined;
914
+ }
915
+ else if (button && button !== 'new' && button !== 'recent') {
916
+ said = button.label;
917
+ if (button.kind === 'continue' && button.date === date) {
918
+ wantThread = button.thread;
919
+ newThread = false;
920
+ }
921
+ else {
922
+ newThread = true;
923
+ wantThread = undefined;
924
+ }
925
+ }
926
+ const drawIn = isObj(body.drawing) && typeof body.drawing.key === 'string' && PAPER_KEY_RE.test(body.drawing.key) && typeof body.drawing.image === 'string' ? { key: body.drawing.key, image: body.drawing.image } : null;
927
+ if (body.drawing !== undefined && !drawIn)
928
+ return { status: 400, json: { error: 'bad_request' } };
929
+ const drawing = drawIn ? paperDrawing(papers.get(`${name}|${drawIn.key}`) ?? { ink: [] }, drawIn.image) : null;
930
+ if (!said.trim() && !action && !photos.length && !drawing)
931
+ return { status: 400, json: { error: 'bad_request' } };
932
+ if (list.some((m) => m.pending))
933
+ return { status: 409, json: { error: 'busy' } };
934
+ const focusCard = isObj(body.focus) && typeof body.focus.card === 'string' ? body.focus.card : null;
935
+ if (action === 'submit' && focusCard && list.some((x) => x.submitted === focusCard))
936
+ return { status: 409, json: { error: 'submitted' } };
937
+ const text = said.trim() || (action === 'submit' ? '(交了答案,没说话)' : photos.length === 1 ? '(拍了一张)' : photos.length ? `(拍了 ${photos.length} 张)` : DRAW_ONLY_TEXT);
938
+ const job = nextJob();
939
+ // 话题:newThread → 自己的 job;指定的要在今天的列表里;缺省接当前(末条)的
940
+ let thread = job;
941
+ if (!newThread && list.length) {
942
+ if (wantThread !== undefined) {
943
+ if (typeof wantThread !== 'string' || !list.some((x) => x.thread === wantThread))
944
+ return { status: 400, json: { error: 'bad_request' } };
945
+ thread = wantThread;
946
+ }
947
+ else
948
+ thread = list[list.length - 1].thread;
949
+ }
950
+ const lt = lec ? mockLecture(lec.bundle) : null;
951
+ const marks = lt ? storedMarks(lt, marksIn, photos.length) : [];
952
+ sentAt.set(job, Date.now());
953
+ // 新话题的第一条:话题还没有时画的那张纸归它(同真服务的 adoptPaper)
954
+ if (thread === job && papers.has(`${name}|new`) && !papers.has(`${name}|${thread}`)) {
955
+ papers.set(`${name}|${thread}`, papers.get(`${name}|new`));
956
+ papers.delete(`${name}|new`);
957
+ }
958
+ const m = { job, thread, at: now().toISOString(), question: text, reply: null, pending: true, artifacts: [], section: null, ...(body.live === true ? { live: true } : {}), ...(action ? { action } : {}), ...(action === 'submit' && focusCard ? { submitted: focusCard } : {}), ...(photos.length ? { photos } : {}), ...(drawing ? { drawing } : {}), ...(lec ? { lecture: { bundle: lec.bundle, title: lt?.title ?? lec.bundle, ...(lec.again ? { again: true } : {}), ...(lt?.video ? { video: true } : {}), ...(marks.length ? { marks } : {}) }, lectureWatch: { watchedMs: lec.watchedMs, finished: lec.finished, pauses: lec.pauses }, ...(logIn.length ? { lectureLog: logIn } : {}) } : {}) };
959
+ list.push(m);
960
+ const done = think(t, m, cursor).then(() => { inflight.delete(m.job); });
961
+ inflight.set(m.job, done);
962
+ return { status: 202, json: { tutor: name, date, job: m.job, thread } };
963
+ }
964
+ return { status: 405, json: { error: 'method_not_allowed' } };
965
+ }
966
+ // 卡的状态:孩子的走 /api/kid/…
967
+ const card = /^\/api\/kid\/conversations\/([a-z0-9][a-z0-9-]*)\/cards\/([^/]+)\/(\d+)$/.exec(pk);
968
+ if (card) {
969
+ const [, name, job, n] = card;
970
+ const m = (messages.get(name) ?? []).find((x) => x.job === job);
971
+ const target = m?.section?.cards[Number(n)];
972
+ if (!target)
973
+ return { status: 404, json: { error: 'no_such_card' } };
974
+ if (method !== 'PUT')
975
+ return { status: 405, json: { error: 'method_not_allowed' } };
976
+ if ((messages.get(name) ?? []).some((x) => x.submitted === `${job}/${n}`))
977
+ return { status: 409, json: { error: 'submitted' } };
978
+ const r = parseCardState(target, body);
979
+ if (!r.ok)
980
+ return { status: 400, json: { error: 'bad_state' } };
981
+ (m.states ??= {})[Number(n)] = r.state;
982
+ return { status: 200, json: { ok: true, card: `${job}/${n}`, state: r.state } };
983
+ }
984
+ const hz = /^\/api\/kid\/tianzige\/([^/]+)$/.exec(p);
985
+ if (hz) {
986
+ // 田字格卡:数据包就在 node_modules 里,mock 也给真笔顺
987
+ const d = await tianzigeData(decodeURIComponent(hz[1]));
988
+ return d ? { status: 200, json: d } : { status: 404, json: { error: 'not_found' } };
989
+ }
990
+ const lt = /^\/api\/kid\/letters\/([^/]+)$/.exec(p);
991
+ if (lt) {
992
+ // 单词卡:字形包就在 node_modules 里,mock 也给真笔顺
993
+ const d = lettersData(decodeURIComponent(lt[1]));
994
+ return d ? { status: 200, json: d } : { status: 404, json: { error: 'not_found' } };
995
+ }
996
+ if (p === '/api/kid/image') {
997
+ // 图片卡:任何路径都给一张占位 svg(写着路径),前端能看到版式;画图卡(mock-paint/)给一张方的四格
998
+ const rel = url.searchParams.get('p') ?? '';
999
+ if (rel.startsWith('mock-paint/'))
1000
+ return { status: 200, contentType: 'image/svg+xml', html: MOCK_PAINT_SVG };
1001
+ const up = uploads.get(rel);
1002
+ if (up)
1003
+ return { status: 200, contentType: up.type, body: up.data };
1004
+ const esc = rel.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c] ?? c);
1005
+ return { status: 200, contentType: 'image/svg+xml', html: `<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7d9a8"/><stop offset="1" stop-color="#8fbf9f"/></linearGradient></defs><rect width="800" height="500" fill="url(#g)"/><circle cx="260" cy="250" r="90" fill="#e8743b"/><circle cx="420" cy="230" r="80" fill="#f4c542"/><circle cx="560" cy="270" r="85" fill="#e0508a"/><text x="400" y="460" font-size="22" text-anchor="middle" fill="#2b2b2b" font-family="sans-serif">${esc}</text></svg>` };
1006
+ }
1007
+ if (p.startsWith('/stage/') && method === 'GET') {
1008
+ if (p === '/stage/') {
1009
+ const html = await stageIndex();
1010
+ return html === null ? { status: 404, json: { error: 'not_found' } } : { status: 200, html };
1011
+ }
1012
+ const f = await stageAsset(p);
1013
+ return f ? { status: 200, file: f.file, contentType: f.contentType } : { status: 404, json: { error: 'not_found' } };
1014
+ }
1015
+ // 视频小课堂:同真服务(app.ts)
1016
+ const lv = /^\/api\/kid\/lectures\/([a-z0-9][a-z0-9-]*)\/(lecture\.json|video\.mp4)$/.exec(p);
1017
+ if (lv && method === 'GET') {
1018
+ const l = lectures.get(lv[1]);
1019
+ if (!l?.video)
1020
+ return { status: 404, json: { error: 'not_found' } };
1021
+ if (lv[2] === 'video.mp4')
1022
+ return { status: 200, file: join(MOCK_LECTURES_DIR, lv[1], 'video.mp4'), contentType: 'video/mp4' };
1023
+ return { status: 200, json: { title: l.title, total: l.clock.total, segments: l.clock.segments.map((x) => ({ start: x.start, len: x.len, line: x.line })) } };
1024
+ }
1025
+ const frame = /^\/api\/bundles\/([a-z0-9][a-z0-9-]*)\/frame\.svg$/.exec(p);
1026
+ if (frame && method === 'GET') {
1027
+ const svg = await lectureFrameSvg({ dirs: { bundles: MOCK_BUNDLES_DIR } }, frame[1], parseSvgMs(url.searchParams.get('svg')), parseRing(url.searchParams.get('ring')), { bake: ensureBaked });
1028
+ return svg === null ? { status: 404, json: { error: 'not_found' } } : { status: 200, body: Buffer.from(svg), contentType: 'image/svg+xml; charset=utf-8' };
1029
+ }
1030
+ if (p.startsWith('/api/bundles/') && method === 'GET') {
1031
+ const f = await bundleAsset(MOCK_BUNDLES_DIR, p);
1032
+ return f ? { status: 200, file: f.file, contentType: f.contentType } : { status: 404, json: { error: 'not_found' } };
1033
+ }
1034
+ // 卡的配音(点读段、单词):mock 不配音,一律回 0.8 秒的静音
1035
+ if (p.startsWith('/api/audio/') && method === 'GET')
1036
+ return { status: 200, contentType: 'audio/wav', body: wavOfPcm(Buffer.alloc(24000 * 2 * 0.8), 24000) };
1037
+ // 字幕重放:真服务端给 src/runtime/live/call.ts 落的那份嘴的声音;mock 没有嘴,每句按字数估多长、放一段静音
1038
+ const mouth = /^\/api\/kid\/conversations\/([a-z0-9][a-z0-9-]*)\/mouth\/(\d{4}-\d{2}-\d{2})\/(\d{4}-\d+)$/.exec(p);
1039
+ if (mouth && method === 'GET') {
1040
+ const day = (await route('GET', `/api/kid/conversations/${mouth[1]}/today`)).json;
1041
+ const m = day?.messages.find((x) => x.job === mouth[3]);
1042
+ const said = m && m.live && m.section ? m.section.lines : [];
1043
+ if (!said.length)
1044
+ return { status: 404, json: { error: 'not_found' } };
1045
+ let at = 0;
1046
+ const lines = said.map((l, line) => { const r = { line, ms: at }; at += lineDurationMs(plainLine(l.text)); return r; });
1047
+ return { status: 200, json: { audio: `/api/mock/silence.wav?ms=${at}`, ms: at, lines } };
1048
+ }
1049
+ if (p === '/api/mock/silence.wav' && method === 'GET') {
1050
+ const ms = Math.min(120_000, Math.max(0, Number(url.searchParams.get('ms')) || 0));
1051
+ return { status: 200, contentType: 'audio/wav', body: wavOfPcm(Buffer.alloc(Math.round(ms * 24) * 2), 24000) };
1052
+ }
1053
+ if (p.startsWith('/api/'))
1054
+ return { status: 404, json: { error: 'not_found', path: p } };
1055
+ return { status: 404, json: { error: 'not_found', path: p } };
1056
+ };
1057
+ const settle = async () => { await Promise.all([...inflight.values()]); };
1058
+ const handler = (req, res) => {
1059
+ void (async () => {
1060
+ const chunks = [];
1061
+ for await (const c of req)
1062
+ chunks.push(c);
1063
+ let body;
1064
+ try {
1065
+ body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : undefined;
1066
+ }
1067
+ catch {
1068
+ body = undefined;
1069
+ }
1070
+ const r = await route(req.method ?? 'GET', req.url ?? '/', body);
1071
+ if (r.file !== undefined)
1072
+ await sendFile(req, res, { status: r.status, file: r.file, contentType: r.contentType, cacheControl: 'private, max-age=3600' });
1073
+ else if (r.body !== undefined) {
1074
+ res.writeHead(r.status, { 'content-type': r.contentType ?? 'application/octet-stream', 'cache-control': r.cacheControl ?? 'private, max-age=3600' });
1075
+ res.end(Buffer.from(r.body));
1076
+ }
1077
+ else if (r.html !== undefined) {
1078
+ res.writeHead(r.status, { 'content-type': r.contentType ?? 'text/html; charset=utf-8', 'cache-control': 'no-store' });
1079
+ res.end(r.html);
1080
+ }
1081
+ else {
1082
+ res.writeHead(r.status, { 'content-type': r.contentType ?? 'application/json; charset=utf-8', 'cache-control': 'no-store' });
1083
+ res.end(JSON.stringify(r.json ?? null));
1084
+ }
1085
+ })().catch((err) => { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'internal', message: String(err) })); });
1086
+ };
1087
+ const settleJob = async (job) => { await inflight.get(job); };
1088
+ const mock = { route, settle, settleJob, handler, listen: null };
1089
+ return mock;
1090
+ }
1091
+ export async function serveMock(opts = {}) {
1092
+ const mock = createMock(opts);
1093
+ let tls = null;
1094
+ if (!opts.http) {
1095
+ try {
1096
+ tls = { cert: readFileSync(join(USER_CERT_DIR, 'cert.pem')), key: readFileSync(join(USER_CERT_DIR, 'key.pem')) };
1097
+ }
1098
+ catch {
1099
+ tls = null;
1100
+ }
1101
+ }
1102
+ const server = tls ? createHttps(tls, mock.handler) : createHttp(mock.handler);
1103
+ {
1104
+ const up = mockLiveUpgrade(mock);
1105
+ server.on('upgrade', (req, socket, head) => { if (!up(req, socket, head))
1106
+ socket.destroy(); });
1107
+ }
1108
+ await new Promise((resolve, reject) => {
1109
+ server.once('error', reject);
1110
+ server.listen(opts.port ?? 8790, opts.host ?? '0.0.0.0', () => resolve());
1111
+ });
1112
+ const port = server.address().port;
1113
+ const scheme = tls ? 'https' : 'http';
1114
+ const urls = [hostname(), ...lanAddresses()].map((h) => `${scheme}://${h}:${port}/`);
1115
+ mock.listen = () => listenInfo(Boolean(tls), port);
1116
+ return { server, mock, port, https: Boolean(tls), urls, qrPage: `${scheme}://localhost:${port}/dev/qr` };
1117
+ }