cotutor 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +249 -21
- package/NOTICE +42 -0
- package/README.md +115 -20
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +15 -5
- package/cards/canvas/card.css +8 -0
- package/cards/canvas/card.md +68 -0
- package/cards/choice/card.css +18 -0
- package/cards/choice/card.md +55 -0
- package/cards/code/card.css +5 -0
- package/cards/code/card.md +46 -0
- package/cards/dictation/card.css +10 -0
- package/cards/dictation/card.md +97 -0
- package/cards/image/card.css +10 -0
- package/cards/image/card.md +52 -0
- package/cards/lecture/card.css +9 -0
- package/cards/lecture/card.md +69 -0
- package/cards/paint/card.css +20 -0
- package/cards/paint/card.md +77 -0
- package/cards/photo/card.css +22 -0
- package/cards/photo/card.md +61 -0
- package/cards/read/card.css +6 -0
- package/cards/read/card.md +51 -0
- package/cards/text/card.css +3 -0
- package/cards/text/card.md +69 -0
- package/cards/tianzige/card.css +17 -0
- package/cards/tianzige/card.md +78 -0
- package/cards/tutor/card.css +79 -0
- package/cards/tutor/card.md +149 -0
- package/cards/word/card.css +24 -0
- package/cards/word/card.md +91 -0
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +159 -0
- package/dist/app/activity-eval-rules.d.ts +62 -0
- package/dist/app/activity-eval-rules.js +276 -0
- package/dist/app/activity-eval.d.ts +65 -0
- package/dist/app/activity-eval.js +159 -0
- package/dist/app/build.d.ts +11 -0
- package/dist/app/build.js +45 -0
- package/dist/app/cli/activities.d.ts +70 -0
- package/dist/app/cli/activities.js +196 -0
- package/dist/app/cli/cert.d.ts +12 -0
- package/dist/app/cli/cert.js +49 -0
- package/dist/app/cli/chalk/bake.d.ts +10 -0
- package/dist/app/cli/chalk/bake.js +58 -0
- package/dist/app/cli/chalk/browser.d.ts +16 -0
- package/dist/app/cli/chalk/browser.js +157 -0
- package/dist/app/cli/chalk/build.d.ts +14 -0
- package/dist/app/cli/chalk/build.js +100 -0
- package/dist/app/cli/chalk/dub.d.ts +51 -0
- package/dist/app/cli/chalk/dub.js +106 -0
- package/dist/app/cli/chalk/load-scene.d.ts +34 -0
- package/dist/app/cli/chalk/load-scene.js +117 -0
- package/dist/app/cli/chalk/main.d.ts +7 -0
- package/dist/app/cli/chalk/main.js +199 -0
- package/dist/app/cli/chalk/serve.d.ts +4 -0
- package/dist/app/cli/chalk/serve.js +28 -0
- package/dist/app/cli/chalk/snap.d.ts +65 -0
- package/dist/app/cli/chalk/snap.js +103 -0
- package/dist/app/cli/chalk/typecheck.d.ts +7 -0
- package/dist/app/cli/chalk/typecheck.js +51 -0
- package/dist/app/cli/doctor.d.ts +24 -0
- package/dist/app/cli/doctor.js +553 -0
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/grades.d.ts +14 -0
- package/dist/app/cli/grades.js +44 -0
- package/dist/{cli → app/cli}/init.d.ts +3 -1
- package/dist/app/cli/init.js +158 -0
- package/dist/app/cli/main.js +1038 -0
- package/dist/app/cli/migrate.d.ts +49 -0
- package/dist/app/cli/migrate.js +195 -0
- package/dist/app/cli/port-holder.d.ts +16 -0
- package/dist/app/cli/port-holder.js +34 -0
- package/dist/app/cli/port.d.ts +37 -0
- package/dist/app/cli/port.js +116 -0
- package/dist/app/cli/sandbox.d.ts +25 -0
- package/dist/app/cli/sandbox.js +166 -0
- package/dist/app/cli/serve-log.d.ts +13 -0
- package/dist/app/cli/serve-log.js +90 -0
- package/dist/app/cli/serve.d.ts +51 -0
- package/dist/app/cli/serve.js +168 -0
- package/dist/app/cli/service-windows.d.ts +50 -0
- package/dist/app/cli/service-windows.js +117 -0
- package/dist/app/cli/service.d.ts +138 -0
- package/dist/app/cli/service.js +429 -0
- package/dist/app/cli/shipped.d.ts +30 -0
- package/dist/app/cli/shipped.js +47 -0
- package/dist/app/cli/skeleton.d.ts +38 -0
- package/dist/app/cli/skeleton.js +161 -0
- package/dist/app/cli/skills.d.ts +59 -0
- package/dist/app/cli/skills.js +199 -0
- package/dist/app/cli/themes.d.ts +52 -0
- package/dist/app/cli/themes.js +148 -0
- package/dist/app/cli/tray-icons.d.ts +11 -0
- package/dist/app/cli/tray-icons.js +87 -0
- package/dist/app/cli/tray-ids.d.ts +6 -0
- package/dist/app/cli/tray-ids.js +6 -0
- package/dist/app/cli/tray-mac.d.ts +8 -0
- package/dist/app/cli/tray-mac.js +140 -0
- package/dist/app/cli/tray-run.d.ts +1 -0
- package/dist/app/cli/tray-run.js +33 -0
- package/dist/app/cli/tray-windows.d.ts +5 -0
- package/dist/app/cli/tray-windows.js +166 -0
- package/dist/app/cli/tray.d.ts +205 -0
- package/dist/app/cli/tray.js +618 -0
- package/dist/{cli → app/cli}/tutors.d.ts +15 -26
- package/dist/{cli → app/cli}/tutors.js +86 -110
- package/dist/app/cli/usage.d.ts +2 -0
- package/dist/app/cli/usage.js +35 -0
- package/dist/app/dictation/recognize.d.ts +18 -0
- package/dist/app/dictation/recognize.js +6 -0
- package/dist/app/dictation/route.d.ts +52 -0
- package/dist/app/dictation/route.js +511 -0
- package/dist/app/diff.d.ts +11 -0
- package/dist/app/diff.js +71 -0
- package/dist/app/http/access.d.ts +62 -0
- package/dist/app/http/access.js +180 -0
- package/dist/app/http/app.d.ts +241 -0
- package/dist/app/http/app.js +1159 -0
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-activities.d.ts +115 -0
- package/dist/app/http/dev-activities.js +193 -0
- package/dist/app/http/dev-overview.d.ts +108 -0
- package/dist/app/http/dev-overview.js +209 -0
- package/dist/app/http/dev-tutor-files.d.ts +80 -0
- package/dist/app/http/dev-tutor-files.js +151 -0
- package/dist/app/http/parent-home.d.ts +51 -0
- package/dist/app/http/parent-home.js +266 -0
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/range.d.ts +9 -0
- package/dist/app/http/range.js +19 -0
- package/dist/app/http/raw-view.d.ts +117 -0
- package/dist/app/http/raw-view.js +149 -0
- package/dist/app/http/send-file.d.ts +7 -0
- package/dist/app/http/send-file.js +29 -0
- package/dist/{server → app/http}/stage.d.ts +9 -0
- package/dist/{server → app/http}/stage.js +34 -6
- package/dist/app/http/theme.d.ts +13 -0
- package/dist/app/http/theme.js +41 -0
- package/dist/app/http/voice-preview.d.ts +4 -0
- package/dist/app/http/voice-preview.js +34 -0
- package/dist/app/machine/bailian.d.ts +26 -0
- package/dist/app/machine/bailian.js +87 -0
- package/dist/app/mock/mock-live.d.ts +16 -0
- package/dist/app/mock/mock-live.js +140 -0
- package/dist/{server → app/mock}/mock.d.ts +38 -5
- package/dist/app/mock/mock.js +1125 -0
- package/dist/app/replay.d.ts +94 -0
- package/dist/app/replay.js +170 -0
- package/dist/app/skills/analyze-doc.d.ts +3 -0
- package/dist/app/skills/analyze-doc.js +47 -0
- package/dist/app/skills/home-doc.d.ts +5 -0
- package/dist/app/skills/home-doc.js +112 -0
- package/dist/app/skills/tune-doc.d.ts +3 -0
- package/dist/app/skills/tune-doc.js +80 -0
- package/dist/app/talk/list.d.ts +11 -0
- package/dist/app/talk/list.js +9 -0
- package/dist/app/talk/route.d.ts +16 -0
- package/dist/app/talk/route.js +142 -0
- package/dist/app/workspace.d.ts +63 -0
- package/dist/{cli → app}/workspace.js +100 -52
- package/dist/app/ws/route.d.ts +31 -0
- package/dist/app/ws/route.js +33 -0
- package/dist/app/ws/ws.d.ts +31 -0
- package/dist/app/ws/ws.js +169 -0
- package/dist/board/canvas-chat.d.ts +97 -0
- package/dist/board/canvas-chat.js +175 -0
- package/dist/board/card-guards.d.ts +15 -0
- package/dist/board/card-guards.js +38 -0
- package/dist/board/cards/canvas.d.ts +41 -0
- package/dist/board/cards/canvas.js +83 -0
- package/dist/{cards → board/cards}/choice.js +0 -12
- package/dist/{cards → board/cards}/code.js +0 -3
- package/dist/board/cards/dictation.d.ts +42 -0
- package/dist/board/cards/dictation.js +105 -0
- package/dist/board/cards/docs.d.ts +47 -0
- package/dist/board/cards/docs.js +152 -0
- package/dist/{cards → board/cards}/image.js +1 -10
- package/dist/board/cards/index.d.ts +62 -0
- package/dist/board/cards/index.js +144 -0
- package/dist/{cards → board/cards}/kind.d.ts +10 -4
- package/dist/board/cards/kind.js +9 -0
- package/dist/board/cards/lecture.d.ts +26 -0
- package/dist/board/cards/lecture.js +62 -0
- package/dist/board/cards/paint.d.ts +38 -0
- package/dist/board/cards/paint.js +85 -0
- package/dist/board/cards/photo.d.ts +15 -0
- package/dist/board/cards/photo.js +29 -0
- package/dist/{cards → board/cards}/read.js +1 -10
- package/dist/board/cards/text.d.ts +20 -0
- package/dist/board/cards/text.js +59 -0
- package/dist/board/cards/tianzige.d.ts +16 -0
- package/dist/board/cards/tianzige.js +64 -0
- package/dist/board/cards/tutor.d.ts +72 -0
- package/dist/board/cards/tutor.js +134 -0
- package/dist/board/cards/word.d.ts +20 -0
- package/dist/board/cards/word.js +76 -0
- package/dist/board/lecture.d.ts +100 -0
- package/dist/board/lecture.js +204 -0
- package/dist/board/paper.d.ts +20 -0
- package/dist/board/paper.js +76 -0
- package/dist/board/parse.d.ts +118 -0
- package/dist/board/parse.js +324 -0
- package/dist/board/presentation.d.ts +289 -0
- package/dist/board/presentation.js +613 -0
- package/dist/board/sections.d.ts +16 -0
- package/dist/board/sections.js +130 -0
- package/dist/brain/api-tools.d.ts +8 -0
- package/dist/brain/api-tools.js +191 -0
- package/dist/brain/api.d.ts +57 -0
- package/dist/brain/api.js +394 -0
- package/dist/brain/index.d.ts +97 -0
- package/dist/brain/index.js +11 -0
- package/dist/brain/log.d.ts +5 -0
- package/dist/brain/log.js +22 -0
- package/dist/brain/runtime.d.ts +30 -0
- package/dist/brain/runtime.js +23 -0
- package/dist/brain/stream-json.d.ts +16 -0
- package/dist/brain/stream-json.js +82 -0
- package/dist/brain/stream.d.ts +20 -0
- package/dist/brain/stream.js +70 -0
- package/dist/brain/transcript.d.ts +45 -0
- package/dist/brain/transcript.js +132 -0
- package/dist/brain/types.d.ts +99 -0
- package/dist/brain/types.js +14 -0
- package/dist/chalk/actions.d.ts +37 -0
- package/dist/chalk/actions.js +69 -0
- package/dist/chalk/bundle.d.ts +26 -0
- package/dist/chalk/bundle.js +69 -0
- package/dist/chalk/check.d.ts +28 -0
- package/dist/chalk/check.js +291 -0
- package/dist/chalk/circle.d.ts +64 -0
- package/dist/chalk/circle.js +193 -0
- package/dist/chalk/clock.d.ts +84 -0
- package/dist/chalk/clock.js +84 -0
- package/dist/chalk/geometry.d.ts +44 -0
- package/dist/chalk/geometry.js +139 -0
- package/dist/chalk/glyphs/capitals.d.ts +12 -0
- package/dist/chalk/glyphs/capitals.js +264 -0
- package/dist/chalk/glyphs/index.d.ts +21 -0
- package/dist/chalk/glyphs/index.js +18 -0
- package/dist/chalk/glyphs/letters.d.ts +13 -0
- package/dist/chalk/glyphs/letters.js +280 -0
- package/dist/chalk/glyphs/metrics.d.ts +33 -0
- package/dist/chalk/glyphs/metrics.js +28 -0
- package/dist/chalk/glyphs/registry.d.ts +7 -0
- package/dist/chalk/glyphs/registry.js +8 -0
- package/dist/chalk/glyphs/sample.d.ts +32 -0
- package/dist/chalk/glyphs/sample.js +102 -0
- package/dist/chalk/glyphs/skeleton.d.ts +64 -0
- package/dist/chalk/glyphs/skeleton.js +126 -0
- package/dist/chalk/glyphs/types.d.ts +58 -0
- package/dist/chalk/glyphs/types.js +17 -0
- package/dist/chalk/index.d.ts +23 -0
- package/dist/chalk/index.js +15 -0
- package/dist/chalk/manifest.d.ts +41 -0
- package/dist/chalk/manifest.js +6 -0
- package/dist/chalk/paint.d.ts +180 -0
- package/dist/chalk/paint.js +210 -0
- package/dist/chalk/pointer.d.ts +8 -0
- package/dist/chalk/pointer.js +21 -0
- package/dist/chalk/prepare.d.ts +11 -0
- package/dist/chalk/prepare.js +15 -0
- package/dist/chalk/scene-schema.d.ts +199 -0
- package/dist/chalk/scene-schema.js +91 -0
- package/dist/chalk/seed.d.ts +3 -0
- package/dist/chalk/seed.js +10 -0
- package/dist/chalk/text-metrics.d.ts +8 -0
- package/dist/chalk/text-metrics.js +156 -0
- package/dist/chalk/timeline.d.ts +47 -0
- package/dist/chalk/timeline.js +84 -0
- package/dist/chalk/types.d.ts +150 -0
- package/dist/chalk/types.js +8 -0
- package/dist/lib/index.d.ts +14 -12
- package/dist/lib/index.js +14 -12
- package/dist/media/draw.d.ts +23 -0
- package/dist/media/draw.js +44 -0
- package/dist/media/mp3.d.ts +6 -0
- package/dist/media/mp3.js +58 -0
- package/dist/media/mp4.d.ts +1 -0
- package/dist/media/mp4.js +58 -0
- package/dist/media/tts-voices.d.ts +18 -0
- package/dist/media/tts-voices.js +606 -0
- package/dist/media/tts.d.ts +56 -0
- package/dist/media/tts.js +174 -0
- package/dist/media/vision.d.ts +25 -0
- package/dist/media/vision.js +71 -0
- package/dist/memory/commit.d.ts +67 -0
- package/dist/memory/commit.js +130 -0
- package/dist/memory/context.d.ts +86 -0
- package/dist/memory/context.js +97 -0
- package/dist/memory/diary.d.ts +84 -0
- package/dist/memory/diary.js +183 -0
- package/dist/memory/grade.d.ts +2 -0
- package/dist/memory/grade.js +7 -0
- package/dist/{lib → memory}/plan.d.ts +1 -1
- package/dist/{lib → memory}/plan.js +1 -1
- package/dist/memory/proposals.d.ts +11 -0
- package/dist/memory/proposals.js +43 -0
- package/dist/memory/retrieval.d.ts +44 -0
- package/dist/memory/retrieval.js +157 -0
- package/dist/{lib → memory}/timetable.d.ts +0 -3
- package/dist/{lib → memory}/timetable.js +0 -11
- package/dist/memory/vault-notes.d.ts +165 -0
- package/dist/memory/vault-notes.js +359 -0
- package/dist/memory/vault.d.ts +70 -0
- package/dist/memory/vault.js +169 -0
- package/dist/realtime/bailian.d.ts +120 -0
- package/dist/realtime/bailian.js +289 -0
- package/dist/realtime/cues.d.ts +51 -0
- package/dist/realtime/cues.js +94 -0
- package/dist/realtime/listen.d.ts +32 -0
- package/dist/realtime/listen.js +53 -0
- package/dist/realtime/preview.d.ts +23 -0
- package/dist/realtime/preview.js +38 -0
- package/dist/realtime/prompt.d.ts +8 -0
- package/dist/realtime/prompt.js +13 -0
- package/dist/realtime/voices.d.ts +21 -0
- package/dist/realtime/voices.js +71 -0
- package/dist/runtime/activities.d.ts +29 -0
- package/dist/runtime/activities.js +72 -0
- package/dist/runtime/activity-state.d.ts +22 -0
- package/dist/runtime/activity-state.js +83 -0
- package/dist/runtime/activity.d.ts +63 -0
- package/dist/runtime/activity.js +428 -0
- package/dist/runtime/agent-file.d.ts +24 -0
- package/dist/runtime/agent-file.js +59 -0
- package/dist/runtime/auto-bookkeep.d.ts +30 -0
- package/dist/runtime/auto-bookkeep.js +84 -0
- package/dist/{lib → runtime/context}/context-pack.d.ts +3 -3
- package/dist/runtime/context/context-pack.js +182 -0
- package/dist/runtime/context/thread-digest.d.ts +4 -0
- package/dist/runtime/context/thread-digest.js +35 -0
- package/dist/runtime/context/tutor-rules.d.ts +25 -0
- package/dist/runtime/context/tutor-rules.js +47 -0
- package/dist/runtime/delivery/delivery.d.ts +94 -0
- package/dist/runtime/delivery/delivery.js +145 -0
- package/dist/runtime/delivery/playback.d.ts +25 -0
- package/dist/runtime/delivery/playback.js +29 -0
- package/dist/runtime/delivery/player.d.ts +145 -0
- package/dist/runtime/delivery/player.js +221 -0
- package/dist/runtime/delivery/queue.d.ts +89 -0
- package/dist/runtime/delivery/queue.js +59 -0
- package/dist/runtime/home/edit.d.ts +39 -0
- package/dist/runtime/home/edit.js +132 -0
- package/dist/runtime/home/home.d.ts +191 -0
- package/dist/runtime/home/home.js +318 -0
- package/dist/runtime/home/store.d.ts +142 -0
- package/dist/runtime/home/store.js +455 -0
- package/dist/runtime/journal/conversation.d.ts +46 -0
- package/dist/runtime/journal/conversation.js +89 -0
- package/dist/runtime/journal/events.d.ts +27 -0
- package/dist/runtime/journal/events.js +136 -0
- package/dist/runtime/journal/kid-log.d.ts +105 -0
- package/dist/runtime/journal/kid-log.js +133 -0
- package/dist/runtime/journal/ledger.d.ts +17 -0
- package/dist/{lib → runtime/journal}/ledger.js +7 -40
- package/dist/runtime/journal/mix.d.ts +42 -0
- package/dist/runtime/journal/mix.js +129 -0
- package/dist/runtime/journal/mouth-index.d.ts +26 -0
- package/dist/runtime/journal/mouth-index.js +34 -0
- package/dist/runtime/journal/mouth.d.ts +54 -0
- package/dist/runtime/journal/mouth.js +102 -0
- package/dist/runtime/journal/recorder.d.ts +91 -0
- package/dist/runtime/journal/recorder.js +148 -0
- package/dist/runtime/journal/store.d.ts +123 -0
- package/dist/runtime/journal/store.js +316 -0
- package/dist/runtime/kid-input.d.ts +16 -0
- package/dist/runtime/kid-input.js +194 -0
- package/dist/runtime/kid-view.d.ts +124 -0
- package/dist/runtime/kid-view.js +157 -0
- package/dist/runtime/lecture.d.ts +86 -0
- package/dist/runtime/lecture.js +237 -0
- package/dist/runtime/live/call.d.ts +231 -0
- package/dist/runtime/live/call.js +1244 -0
- package/dist/runtime/live/turns.d.ts +48 -0
- package/dist/runtime/live/turns.js +102 -0
- package/dist/runtime/runner.d.ts +243 -0
- package/dist/runtime/runner.js +777 -0
- package/dist/runtime/talk-run.d.ts +21 -0
- package/dist/runtime/talk-run.js +34 -0
- package/dist/scenes/dictation/match.d.ts +57 -0
- package/dist/scenes/dictation/match.js +255 -0
- package/dist/scenes/dictation/recognize.d.ts +86 -0
- package/dist/scenes/dictation/recognize.js +163 -0
- package/dist/scenes/dictation/store.d.ts +112 -0
- package/dist/scenes/dictation/store.js +99 -0
- package/dist/scenes/look.d.ts +26 -0
- package/dist/scenes/look.js +1 -0
- package/dist/scenes/paint/painter.d.ts +84 -0
- package/dist/scenes/paint/painter.js +171 -0
- package/dist/scenes/paint/prompt.d.ts +23 -0
- package/dist/scenes/paint/prompt.js +36 -0
- package/dist/scenes/talk/en-words.d.ts +8 -0
- package/dist/scenes/talk/en-words.js +44 -0
- package/dist/scenes/talk/list.d.ts +44 -0
- package/dist/scenes/talk/list.js +88 -0
- package/dist/scenes/talk/store.d.ts +27 -0
- package/dist/scenes/talk/store.js +61 -0
- package/dist/scenes/talk/warmup.d.ts +16 -0
- package/dist/scenes/talk/warmup.js +39 -0
- package/dist/schema/activity.d.ts +149 -0
- package/dist/schema/activity.js +113 -0
- package/dist/schema/board.d.ts +143 -2
- package/dist/schema/board.js +15 -3
- package/dist/schema/config.d.ts +339 -103
- package/dist/schema/config.js +269 -84
- package/dist/schema/context-pack.d.ts +76 -14
- package/dist/schema/context-pack.js +91 -10
- package/dist/{lib/conversation.d.ts → schema/conversation-files.d.ts} +11 -33
- package/dist/schema/conversation-files.js +26 -0
- package/dist/schema/conversation.d.ts +391 -49
- package/dist/schema/conversation.js +135 -19
- package/dist/schema/events.d.ts +138 -0
- package/dist/schema/events.js +1 -0
- package/dist/schema/home.d.ts +56 -0
- package/dist/schema/home.js +35 -0
- package/dist/schema/index.d.ts +14 -3
- package/dist/schema/index.js +14 -3
- package/dist/schema/kid-device.d.ts +20 -0
- package/dist/schema/kid-device.js +32 -0
- package/dist/schema/ledger.d.ts +8 -63
- package/dist/schema/ledger.js +7 -31
- package/dist/schema/memory.d.ts +73 -0
- package/dist/schema/memory.js +47 -0
- package/dist/schema/plan.d.ts +2 -2
- package/dist/schema/plan.js +2 -2
- package/dist/schema/sections.d.ts +32 -25
- package/dist/schema/sections.js +21 -22
- package/dist/schema/theme.d.ts +25 -0
- package/dist/schema/theme.js +26 -0
- package/dist/schema/timetable.d.ts +2 -2
- package/dist/schema/timetable.js +2 -2
- package/dist/schema/workspace.d.ts +68 -0
- package/dist/schema/workspace.js +37 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/errors.d.ts +8 -0
- package/dist/shared/errors.js +13 -0
- package/dist/shared/home.d.ts +2 -0
- package/dist/shared/home.js +16 -0
- package/dist/shared/human-notes.d.ts +5 -0
- package/dist/shared/human-notes.js +7 -0
- package/dist/shared/qianwen.d.ts +54 -0
- package/dist/shared/qianwen.js +113 -0
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/shared/time.d.ts +9 -0
- package/dist/shared/time.js +22 -0
- package/dist/shared/tokens.d.ts +18 -0
- package/dist/shared/tokens.js +11 -0
- package/dist/shared/wav.d.ts +10 -0
- package/dist/shared/wav.js +50 -0
- package/dist/stage/chunk-226TNLQA.js +129 -0
- package/dist/stage/chunk-24LGQEKE.js +1 -0
- package/dist/stage/chunk-25XV6UY3.js +10 -0
- package/dist/stage/chunk-2AZI3NHJ.js +1 -0
- package/dist/stage/chunk-2BQZTTBE.js +1 -0
- package/dist/stage/chunk-2M7CGY3Y.js +11 -0
- package/dist/stage/chunk-2VR4WFQ7.js +10 -0
- package/dist/stage/chunk-2ZYXBUWG.js +9 -0
- package/dist/stage/chunk-32LAWMGY.js +1 -0
- package/dist/stage/chunk-3LEMTSOO.js +11 -0
- package/dist/stage/chunk-3QCKGZYE.js +32 -0
- package/dist/stage/chunk-44P66WFA.js +1 -0
- package/dist/stage/chunk-4AM6FRR6.js +7 -0
- package/dist/stage/chunk-4QDE4SUK.js +1 -0
- package/dist/stage/chunk-4QESHV3U.js +1 -0
- package/dist/stage/chunk-4SG5QPHB.js +2 -0
- package/dist/stage/chunk-4T4UVCOC.js +1 -0
- package/dist/stage/chunk-4WUEPPG3.js +34 -0
- package/dist/stage/chunk-4YCN6UZ5.js +99 -0
- package/dist/stage/chunk-5V7ZY2AP.js +1 -0
- package/dist/stage/chunk-5VS5Y2VJ.js +6 -0
- package/dist/stage/chunk-5W4NO2ZD.js +62 -0
- package/dist/stage/chunk-65CRNICM.js +1 -0
- package/dist/stage/chunk-66MULO7F.js +5 -0
- package/dist/stage/chunk-6COFQTBS.js +162 -0
- package/dist/stage/chunk-6VZG3M3I.js +8 -0
- package/dist/stage/chunk-7HVDH43T.js +1 -0
- package/dist/stage/chunk-7JLJN4EZ.js +8 -0
- package/dist/stage/chunk-7QDSP4LN.js +1 -0
- package/dist/stage/chunk-7R6OQAO3.js +1 -0
- package/dist/stage/chunk-7VTTHZKW.js +7 -0
- package/dist/stage/chunk-ATHR5KEN.js +10 -0
- package/dist/stage/chunk-B6R5LTP2.js +36 -0
- package/dist/stage/chunk-BD4WE264.js +1 -0
- package/dist/stage/chunk-BONANU47.js +206 -0
- package/dist/stage/chunk-BTVW7OFK.js +89 -0
- package/dist/stage/chunk-BUTJIDHG.js +8 -0
- package/dist/stage/chunk-C2IPYMML.js +84 -0
- package/dist/stage/chunk-C54FTPPV.js +9 -0
- package/dist/stage/chunk-C6LOUF2Z.js +90 -0
- package/dist/stage/chunk-CFF3PILO.js +339 -0
- package/dist/stage/chunk-CI2FQ2JY.js +9 -0
- package/dist/stage/chunk-CW5O4RV7.js +12 -0
- package/dist/stage/chunk-CYABNQYD.js +1 -0
- package/dist/stage/chunk-D7FFA4NS.js +15 -0
- package/dist/stage/chunk-DUNNXU6Z.js +8 -0
- package/dist/stage/chunk-E3E4Q6JF.js +6 -0
- package/dist/stage/chunk-EB3U3LH7.js +9 -0
- package/dist/stage/chunk-EGSZ736B.js +8 -0
- package/dist/stage/chunk-EOBXW5VQ.js +2 -0
- package/dist/stage/chunk-ERZC3B7Y.js +11 -0
- package/dist/stage/chunk-ESDOOBIH.js +11 -0
- package/dist/stage/chunk-EVU4QAL4.js +178 -0
- package/dist/stage/chunk-EVVZI2NY.js +39 -0
- package/dist/stage/chunk-FBKAZUKD.js +39 -0
- package/dist/stage/chunk-FDVKYQJN.js +1 -0
- package/dist/stage/chunk-FGPVYHVR.js +5 -0
- package/dist/stage/chunk-FHUD4AZC.js +1 -0
- package/dist/stage/chunk-FKUJ2DMY.js +96 -0
- package/dist/stage/chunk-FNZGVIJV.js +1 -0
- package/dist/stage/chunk-GBYSEGYM.js +78 -0
- package/dist/stage/chunk-GCO4TDHX.js +1 -0
- package/dist/stage/chunk-GKTML2LG.js +10 -0
- package/dist/stage/chunk-GMDIHHXQ.js +11 -0
- package/dist/stage/chunk-GOUNACI3.js +10 -0
- package/dist/stage/chunk-GXAYATPF.js +3 -0
- package/dist/stage/chunk-H2K2DDXV.js +8 -0
- package/dist/stage/chunk-H3UJI6ZY.js +13 -0
- package/dist/stage/chunk-H6PUGCME.js +7 -0
- package/dist/stage/chunk-HNBUIMIG.js +1 -0
- package/dist/stage/chunk-HRZY2BJQ.js +10 -0
- package/dist/stage/chunk-HSPJGALW.js +30 -0
- package/dist/stage/chunk-IBYORHIP.js +25 -0
- package/dist/stage/chunk-IHF4EWTY.js +1 -0
- package/dist/stage/chunk-ISBRPNKI.js +1 -0
- package/dist/stage/chunk-IZ3OI3C2.js +2 -0
- package/dist/stage/chunk-JEFZAC2R.js +1 -0
- package/dist/stage/chunk-JFIGRDPO.js +1 -0
- package/dist/stage/chunk-JFVYBOOC.js +9 -0
- package/dist/stage/chunk-JGD4TUTQ.js +1 -0
- package/dist/stage/chunk-JO4BEHDH.js +4 -0
- package/dist/stage/chunk-JOWBS42C.js +1 -0
- package/dist/stage/chunk-JP3TM2TG.js +1 -0
- package/dist/stage/chunk-JVK2C52O.js +1 -0
- package/dist/stage/chunk-K4I76XBX.js +14 -0
- package/dist/stage/chunk-KA6CZZ4M.js +1 -0
- package/dist/stage/chunk-KESSJ36M.js +1 -0
- package/dist/stage/chunk-LKATEVRU.js +1 -0
- package/dist/stage/chunk-LS4EJ2EI.js +1 -0
- package/dist/stage/chunk-LUKJQC2X.js +70 -0
- package/dist/stage/chunk-LWULR3SM.js +1 -0
- package/dist/stage/chunk-LWVRFMKX.js +2 -0
- package/dist/stage/chunk-LXC4ZBPZ.js +1 -0
- package/dist/stage/chunk-M2UP255H.js +1 -0
- package/dist/stage/chunk-MAPPXUZQ.js +231 -0
- package/dist/stage/chunk-MBMPW7Z5.js +1 -0
- package/dist/stage/chunk-MD3HMMTK.js +1 -0
- package/dist/stage/chunk-MLPCUIKR.js +17 -0
- package/dist/stage/chunk-MNQIRPDQ.js +8 -0
- package/dist/stage/chunk-MRG7MJVS.js +1 -0
- package/dist/stage/chunk-MU6EXW72.js +10 -0
- package/dist/stage/chunk-MVU2X6LU.js +4 -0
- package/dist/stage/chunk-N6UA5CZV.js +36 -0
- package/dist/stage/chunk-NLZIT2TJ.js +7 -0
- package/dist/stage/chunk-NOJR3J7M.js +1 -0
- package/dist/stage/chunk-NT7ARDOL.js +1 -0
- package/dist/stage/chunk-NWOGMPZH.js +1 -0
- package/dist/stage/chunk-NXE4KMG3.js +40 -0
- package/dist/stage/chunk-O4YCZXD2.js +257 -0
- package/dist/stage/chunk-OAJZ7HLK.js +1 -0
- package/dist/stage/chunk-OCFCFASO.js +1 -0
- package/dist/stage/chunk-ODGPG5RY.js +7 -0
- package/dist/stage/chunk-ODUEA52Q.js +3 -0
- package/dist/stage/chunk-OGDJ3FCI.js +1 -0
- package/dist/stage/chunk-OLDJ55TT.js +1 -0
- package/dist/stage/chunk-OOCL4ATI.js +2 -0
- package/dist/stage/chunk-OQEFDUDV.js +1 -0
- package/dist/stage/chunk-OQWVEWG7.js +4 -0
- package/dist/stage/chunk-OUIX6BGK.js +1 -0
- package/dist/stage/chunk-OV7OZYUO.js +7 -0
- package/dist/stage/chunk-PFGDY33O.js +1 -0
- package/dist/stage/chunk-PFJDUJB3.js +139 -0
- package/dist/stage/chunk-PGEW76ER.js +1 -0
- package/dist/stage/chunk-PP76TAQX.js +32 -0
- package/dist/stage/chunk-PTBOP4OT.js +2 -0
- package/dist/stage/chunk-PX2RVHCK.js +1 -0
- package/dist/stage/chunk-Q7K3PNAD.js +1 -0
- package/dist/stage/chunk-QDDPLZDB.js +1 -0
- package/dist/stage/chunk-QLGLZ77F.js +1 -0
- package/dist/stage/chunk-QOPFDFHI.js +1 -0
- package/dist/stage/chunk-QXT4KAEJ.js +1 -0
- package/dist/stage/chunk-R4YVZCIT.js +1 -0
- package/dist/stage/chunk-R6P6EHWA.js +62 -0
- package/dist/stage/chunk-RCH3OXBI.js +9 -0
- package/dist/stage/chunk-RHPE44I2.js +1 -0
- package/dist/stage/chunk-RJ2R4FVC.js +1 -0
- package/dist/stage/chunk-RJH2YVTD.js +1 -0
- package/dist/stage/chunk-RLWWW5OB.js +6 -0
- package/dist/stage/chunk-S3WO35Z7.js +120 -0
- package/dist/stage/chunk-S7ZGX6MW.js +1 -0
- package/dist/stage/chunk-SIAWX27B.js +1 -0
- package/dist/stage/chunk-SLG4X622.js +1 -0
- package/dist/stage/chunk-SLJRBC77.js +168 -0
- package/dist/stage/chunk-SRORWQJZ.js +1 -0
- package/dist/stage/chunk-STZTUIIF.js +2 -0
- package/dist/stage/chunk-SVQ6MWNV.js +9 -0
- package/dist/stage/chunk-TL3HLL2S.js +132 -0
- package/dist/stage/chunk-U3DOE6SF.js +24 -0
- package/dist/stage/chunk-UABQIZKW.js +6 -0
- package/dist/stage/chunk-UDK3R764.js +1 -0
- package/dist/stage/chunk-UFF4E54B.js +41 -0
- package/dist/stage/chunk-UFP5Q4AY.js +1 -0
- package/dist/stage/chunk-UMH2TSW2.js +72 -0
- package/dist/stage/chunk-UTASVNF5.js +10 -0
- package/dist/stage/chunk-UW7ML5ML.js +88 -0
- package/dist/stage/chunk-V3J7F6OP.js +1 -0
- package/dist/stage/chunk-VEHP5B5Q.js +5 -0
- package/dist/stage/chunk-VRXWB733.js +9 -0
- package/dist/stage/chunk-VVDHBBS5.js +5 -0
- package/dist/stage/chunk-VYFOH6FU.js +106 -0
- package/dist/stage/chunk-W3VZTLQS.js +3 -0
- package/dist/stage/chunk-W7WNRWTZ.js +8 -0
- package/dist/stage/chunk-WHUCV53C.js +1 -0
- package/dist/stage/chunk-WULMYHIF.js +1 -0
- package/dist/stage/chunk-X54EX7AW.js +9 -0
- package/dist/stage/chunk-X565ISZM.js +1 -0
- package/dist/stage/chunk-XGZMDECB.js +1 -0
- package/dist/stage/chunk-XHONC4RF.js +172 -0
- package/dist/stage/chunk-XIK4XQ6E.js +24 -0
- package/dist/stage/chunk-XJQQWFTE.js +1 -0
- package/dist/stage/chunk-XM4WOU3I.js +13 -0
- package/dist/stage/chunk-XM7Y5HP5.js +38 -0
- package/dist/stage/chunk-XNYYRGD5.js +292 -0
- package/dist/stage/chunk-XOYVR3SV.js +1 -0
- package/dist/stage/chunk-XQIW5HCE.js +1 -0
- package/dist/stage/chunk-YEWL7FUW.js +8 -0
- package/dist/stage/chunk-YI55PY3V.js +1 -0
- package/dist/stage/chunk-YM63HI6W.js +1 -0
- package/dist/stage/chunk-YNLGLU4P.js +9 -0
- package/dist/stage/chunk-YTNMNOWP.js +1 -0
- package/dist/stage/chunk-YV62XKTC.js +1 -0
- package/dist/stage/chunk-YVBA3QW6.js +1 -0
- package/dist/stage/chunk-Z2SY2HOC.js +1 -0
- package/dist/stage/chunk-ZBTCQEBK.js +10 -0
- package/dist/stage/chunk-ZCUFN74D.js +1 -0
- package/dist/stage/chunk-ZHOULXEJ.js +11 -0
- package/dist/stage/chunk-ZTRZIET2.js +1 -0
- package/dist/stage/index.html +1 -0
- package/dist/stage/stage.css +1 -1
- package/dist/stage/stage.js +1 -4383
- package/dist/ui/dev/dev-page.d.ts +1 -0
- package/dist/ui/dev/dev-page.js +2021 -0
- package/dist/ui/dev/dev-theme.d.ts +6 -0
- package/dist/ui/dev/dev-theme.js +67 -0
- package/dist/ui/dev/voice-gallery-page.d.ts +1 -0
- package/dist/ui/dev/voice-gallery-page.js +121 -0
- package/dist/ui/dev/voice-scenes.d.ts +21 -0
- package/dist/ui/dev/voice-scenes.js +30 -0
- package/dist/ui/dictation/en-box.d.ts +2 -0
- package/dist/ui/dictation/en-box.js +86 -0
- package/dist/ui/dictation/en-page.d.ts +1 -0
- package/dist/ui/dictation/en-page.js +484 -0
- package/dist/ui/dictation/page.d.ts +1 -0
- package/dist/ui/dictation/page.js +1084 -0
- package/dist/ui/dictation/parent-page.d.ts +1 -0
- package/dist/ui/dictation/parent-page.js +234 -0
- package/dist/ui/dictation/reel-mode.d.ts +1 -0
- package/dist/ui/dictation/reel-mode.js +87 -0
- package/dist/ui/dictation/reel-page.d.ts +1 -0
- package/dist/ui/dictation/reel-page.js +62 -0
- package/dist/ui/dictation/reel.d.ts +163 -0
- package/dist/ui/dictation/reel.js +436 -0
- package/dist/ui/dictation/ui.d.ts +44 -0
- package/dist/ui/dictation/ui.js +80 -0
- package/dist/ui/dictation/unlock.d.ts +7 -0
- package/dist/ui/dictation/unlock.js +16 -0
- package/dist/ui/kid/audio-client.d.ts +11 -0
- package/dist/ui/kid/audio-client.js +74 -0
- package/dist/ui/kid/board-mark-art.d.ts +37 -0
- package/dist/ui/kid/board-mark-art.js +5 -0
- package/dist/ui/kid/board-mark.d.ts +24 -0
- package/dist/ui/kid/board-mark.js +58 -0
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-log.d.ts +15 -0
- package/dist/ui/kid/kid-log.js +87 -0
- package/dist/ui/kid/kid-nav.d.ts +11 -0
- package/dist/ui/kid/kid-nav.js +22 -0
- package/dist/ui/kid/kid-page.d.ts +9 -0
- package/dist/ui/kid/kid-page.js +3684 -0
- package/dist/ui/kid/letters.d.ts +26 -0
- package/dist/ui/kid/letters.js +51 -0
- package/dist/ui/kid/photo-edit.d.ts +83 -0
- package/dist/ui/kid/photo-edit.js +106 -0
- package/dist/ui/kid/photo-ui.d.ts +23 -0
- package/dist/ui/kid/photo-ui.js +60 -0
- package/dist/ui/kid/reel.d.ts +419 -0
- package/dist/ui/kid/reel.js +648 -0
- package/dist/ui/kid/tianzige.d.ts +8 -0
- package/dist/ui/kid/tianzige.js +45 -0
- package/dist/ui/kid/tutor-face.d.ts +35 -0
- package/dist/ui/kid/tutor-face.js +84 -0
- package/dist/ui/kid/voice-ui.d.ts +70 -0
- package/dist/ui/kid/voice-ui.js +316 -0
- package/dist/ui/kid/voice-view.d.ts +71 -0
- package/dist/ui/kid/voice-view.js +75 -0
- package/dist/ui/kid/word-draw.d.ts +2 -0
- package/dist/ui/kid/word-draw.js +107 -0
- package/dist/ui/kid/word-emoji.d.ts +10 -0
- package/dist/ui/kid/word-emoji.js +68 -0
- package/dist/ui/kid/word-layout.d.ts +25 -0
- package/dist/ui/kid/word-layout.js +29 -0
- package/dist/ui/parent/parent-home-page.d.ts +1 -0
- package/dist/ui/parent/parent-home-page.js +722 -0
- package/dist/ui/parent/parent-page.d.ts +2 -0
- package/dist/ui/parent/parent-page.js +511 -0
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/parent/qr-page.d.ts +8 -0
- package/dist/ui/parent/qr-page.js +66 -0
- package/dist/ui/shared/brand.d.ts +38 -0
- package/dist/ui/shared/brand.js +53 -0
- package/dist/ui/shared/favicon.d.ts +12 -0
- package/dist/ui/shared/favicon.js +96 -0
- package/dist/ui/shared/icon.d.ts +9 -0
- package/dist/ui/shared/icon.js +35 -0
- package/dist/ui/shared/inline-lib.d.ts +1 -0
- package/dist/ui/shared/inline-lib.js +28 -0
- package/dist/ui/shared/logo.d.ts +31 -0
- package/dist/ui/shared/logo.js +43 -0
- package/dist/ui/shared/page-dom.d.ts +12 -0
- package/dist/ui/shared/page-dom.js +36 -0
- package/dist/ui/shared/png.d.ts +1 -0
- package/dist/ui/shared/png.js +45 -0
- package/dist/ui/shared/qr-svg.d.ts +3 -0
- package/dist/ui/shared/qr-svg.js +32 -0
- package/dist/ui/shared/qr.d.ts +14 -0
- package/dist/ui/shared/qr.js +281 -0
- package/dist/ui/shared/reel-dock.d.ts +22 -0
- package/dist/ui/shared/reel-dock.js +365 -0
- package/dist/ui/shared/tutor-color.d.ts +15 -0
- package/dist/ui/shared/tutor-color.js +47 -0
- package/dist/ui/talk/page.d.ts +6 -0
- package/dist/ui/talk/page.js +294 -0
- package/grades/grade-2/README.md +11 -0
- package/grades/grade-2/VALIDATION.md +41 -0
- package/grades/grade-2/agents/chinese-tutor/activities//344/275/240/350/257/264/346/210/221/347/224/273.md +157 -0
- 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
- 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
- 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
- package/grades/grade-2/agents/chinese-tutor/activities//345/206/231/350/257/235.md +64 -0
- package/grades/grade-2/agents/chinese-tutor/activities//345/206/231/350/257/235./344/276/213/345/255/220.md +31 -0
- package/grades/grade-2/agents/chinese-tutor/activities//345/220/254/345/206/231.md +48 -0
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +79 -0
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/chinese-tutor/activities//345/260/217/350/257/276/345/240/202.md +53 -0
- package/grades/grade-2/agents/chinese-tutor/activities//346/243/200/346/237/245/344/275/234/344/270/232.md +81 -0
- package/grades/grade-2/agents/chinese-tutor/activities//346/243/200/346/237/245/344/275/234/344/270/232./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/chinese-tutor/activities//347/234/213/345/233/276/350/257/264/350/257/235.md +94 -0
- 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
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +82 -0
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/chinese-tutor.md +59 -0
- package/grades/grade-2/agents/english-tutor/activities//345/217/243/350/257/255/350/257/276.md +149 -0
- 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
- package/grades/grade-2/agents/english-tutor/activities//345/220/254/345/206/231.md +47 -0
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +78 -0
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/english-tutor/activities//345/260/217/350/257/276/345/240/202.md +53 -0
- package/grades/grade-2/agents/english-tutor/activities//346/243/200/346/237/245/344/275/234/344/270/232.md +80 -0
- package/grades/grade-2/agents/english-tutor/activities//346/243/200/346/237/245/344/275/234/344/270/232./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +86 -0
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/english-tutor.md +57 -0
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +78 -0
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/math-tutor/activities//345/260/217/350/257/276/345/240/202.md +56 -0
- 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
- package/grades/grade-2/agents/math-tutor/activities//346/243/200/346/237/245/344/275/234/344/270/232.md +80 -0
- package/grades/grade-2/agents/math-tutor/activities//346/243/200/346/237/245/344/275/234/344/270/232./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +81 -0
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240./344/276/213/345/255/220.md +35 -0
- package/grades/grade-2/agents/math-tutor.md +51 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/index.json +44 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-1.mp3 +0 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-2.mp3 +0 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-3.mp3 +0 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-4.mp3 +0 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/audio/step-5.mp3 +0 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/bake.json +1 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/manifest.json +63 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen/scene.json +247 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen.md +40 -0
- package/grades/grade-2/examples/2026-10-27-pingjunfen.ts +26 -0
- package/grades/grade-2/examples//345/271/263/345/235/207/345/210/206./344/276/213/345/255/220.md +24 -0
- package/grades/grade-2/examples//346/225/260/345/255/246/345/271/263/345/235/207/345/210/206.md +42 -0
- package/grades/grade-2/validation/2026-10-27.json +4410 -0
- package/package.json +27 -15
- package/skills/chalk-cli/SKILL.md +62 -0
- package/skills/chalk-scene/SKILL.md +91 -0
- package/skills/chalk-teaching/SKILL.md +82 -0
- 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
- package/skills/chalk-teaching/models//345/207/221/345/215/201.md +7 -0
- package/skills/chalk-teaching/models//345/221/250/346/234/237/350/247/204/345/276/213.md +6 -0
- package/skills/chalk-teaching/models//345/256/236/347/211/251/345/233/276-/345/242/236/345/207/217.md +6 -0
- 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
- 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
- package/skills/chalk-teaching/models//345/260/217/346/243/222/350/277/233/344/275/215/345/212/240.md +7 -0
- package/skills/chalk-teaching/models//345/260/217/346/243/222/351/200/200/344/275/215/345/207/217.md +6 -0
- package/skills/chalk-teaching/models//346/216/222/351/230/237/347/202/271/351/230/265.md +7 -0
- package/skills/chalk-teaching/models//346/227/266/351/227/264/346/225/260/350/275/264.md +6 -0
- package/skills/chalk-teaching/models//346/245/274/345/261/202/351/227/264/351/232/224/345/233/276.md +7 -0
- package/skills/chalk-teaching/models//347/240/264/345/215/201.md +7 -0
- package/skills/chalk-teaching/models//347/253/226/345/274/217/351/200/200/344/275/215.md +6 -0
- package/skills/chalk-teaching/models//347/272/277/346/256/265/345/233/276.md +7 -0
- package/skills/chalk-teaching/models-index.md +24 -0
- 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
- package/skills/chalk-teaching/references//350/257/264/346/230/216/346/226/207/344/273/266.md +102 -0
- package/skills/chalk-verify/SKILL.md +57 -0
- package/skills/chalk-verify/references//350/257/204/345/210/244/346/240/207/345/207/206.md +69 -0
- package/skills/cotutor-analyze/SKILL.md +106 -0
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +31 -0
- package/skills/cotutor-board/SKILL.md +373 -0
- package/skills/cotutor-board/references/README.md +38 -0
- package/skills/cotutor-board/references/canvas.md +68 -0
- package/skills/cotutor-board/references/choice.md +55 -0
- package/skills/cotutor-board/references/code.md +46 -0
- package/skills/cotutor-board/references/image.md +52 -0
- package/skills/cotutor-board/references/lecture.md +69 -0
- package/skills/cotutor-board/references/paint.md +77 -0
- package/skills/cotutor-board/references/photo.md +61 -0
- package/skills/cotutor-board/references/read.md +51 -0
- package/skills/cotutor-board/references/text.md +69 -0
- package/skills/cotutor-board/references/tianzige.md +78 -0
- package/skills/cotutor-board/references/word.md +91 -0
- package/skills/cotutor-home/SKILL.md +170 -0
- package/skills/cotutor-home/references/dictation.md +97 -0
- package/skills/cotutor-home/references/tutor.md +149 -0
- package/skills/cotutor-home/references//345/221/275/344/273/244.md +21 -0
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +143 -0
- package/skills/cotutor-tune/SKILL.md +147 -0
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +53 -0
- package/skills/cotutor-tutor/SKILL.md +253 -0
- package/skills/cotutor-tutor/references//344/270/212/344/270/213/346/226/207/345/214/205.md +78 -0
- package/themes/default/kid.css +86 -0
- package/themes/default/theme.json +26 -0
- package/agents/chinese-tutor.md +0 -67
- package/agents/homework-tutor.md +0 -17
- package/agents/math-tutor.md +0 -68
- package/agents/planner.md +0 -18
- package/agents/reading-tutor.md +0 -65
- package/agents/scene-maker.md +0 -41
- package/dist/cards/canvas.d.ts +0 -22
- package/dist/cards/canvas.js +0 -65
- package/dist/cards/fill.d.ts +0 -14
- package/dist/cards/fill.js +0 -64
- package/dist/cards/index.d.ts +0 -48
- package/dist/cards/index.js +0 -115
- package/dist/cards/kind.js +0 -7
- package/dist/cards/scene.d.ts +0 -24
- package/dist/cards/scene.js +0 -67
- package/dist/cards/text.d.ts +0 -18
- package/dist/cards/text.js +0 -41
- package/dist/cli/cert.d.ts +0 -7
- package/dist/cli/cert.js +0 -36
- package/dist/cli/doctor.d.ts +0 -22
- package/dist/cli/doctor.js +0 -355
- package/dist/cli/init.js +0 -116
- package/dist/cli/main.js +0 -243
- package/dist/cli/serve.d.ts +0 -30
- package/dist/cli/serve.js +0 -56
- package/dist/cli/skeleton.d.ts +0 -39
- package/dist/cli/skeleton.js +0 -184
- package/dist/cli/skills.d.ts +0 -40
- package/dist/cli/skills.js +0 -170
- package/dist/cli/workspace.d.ts +0 -69
- package/dist/lib/agent-file.d.ts +0 -10
- package/dist/lib/agent-file.js +0 -16
- package/dist/lib/board.d.ts +0 -15
- package/dist/lib/board.js +0 -107
- package/dist/lib/context-pack.js +0 -55
- package/dist/lib/conversation.js +0 -88
- package/dist/lib/kid-board.d.ts +0 -148
- package/dist/lib/kid-board.js +0 -294
- package/dist/lib/kid-view.d.ts +0 -63
- package/dist/lib/kid-view.js +0 -93
- package/dist/lib/ledger.d.ts +0 -29
- package/dist/lib/run-plan.d.ts +0 -30
- package/dist/lib/run-plan.js +0 -32
- package/dist/lib/sections.d.ts +0 -12
- package/dist/lib/sections.js +0 -151
- package/dist/lib/stream.d.ts +0 -15
- package/dist/lib/stream.js +0 -69
- package/dist/lib/transcript.d.ts +0 -36
- package/dist/lib/transcript.js +0 -95
- package/dist/server/app.d.ts +0 -74
- package/dist/server/app.js +0 -406
- package/dist/server/kid-page.d.ts +0 -1
- package/dist/server/kid-page.js +0 -768
- package/dist/server/mock.js +0 -403
- package/dist/server/parent-page.d.ts +0 -8
- package/dist/server/parent-page.js +0 -393
- package/dist/server/runner.d.ts +0 -64
- package/dist/server/runner.js +0 -277
- package/dist/server/scene-props.d.ts +0 -16
- package/dist/server/scene-props.js +0 -39
- package/dist/server/store.d.ts +0 -43
- package/dist/server/store.js +0 -210
- package/dist/server/tts.d.ts +0 -51
- package/dist/server/tts.js +0 -99
- /package/dist/{cli → app/cli}/main.d.ts +0 -0
- /package/dist/{cards → board/cards}/choice.d.ts +0 -0
- /package/dist/{cards → board/cards}/code.d.ts +0 -0
- /package/dist/{cards → board/cards}/image.d.ts +0 -0
- /package/dist/{cards → board/cards}/read.d.ts +0 -0
|
@@ -0,0 +1,1125 @@
|
|
|
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 { 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 { HOME_TILES, HOME_TILE_IDS, canonicalSubject, 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, localName, siteUrl } from "../cli/serve.js";
|
|
71
|
+
import { qrPage } from "../../ui/parent/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, kidTiles, 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
|
+
// 方砖:三门主科出厂都有预习、检查作业、复习(真服务看老师家里有没有那个 activity 文件)
|
|
537
|
+
const main = ['语文', '数学', '英语'].includes(canonicalSubject(MOCK_TUTORS.find((t) => t.name === name)?.subject) ?? '');
|
|
538
|
+
return { kind: 'tutor', props: { tutor: name, tiles: main ? kidTiles(HOME_TILE_IDS, { usable: () => true }) : [], 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; }) } };
|
|
539
|
+
});
|
|
540
|
+
// figshot:和配了 figshot 的 workspace 一样给端口;这台电脑上 figshot 没开着,页面照样藏着这张卡
|
|
541
|
+
return { title, date: localDate(d), tutors: tutorsJson(), home: mockHomeId(), cards, figshot: { port: 8477 }, hand: 'right', ready: scenario !== 'nokey' };
|
|
542
|
+
};
|
|
543
|
+
const mockHomeId = () => `${yesterday()}-2130`;
|
|
544
|
+
let dictationRoot = null;
|
|
545
|
+
// 听写的暂存放临时目录,进程退出时删掉(不删每起一次 mock 留一个,攒到上千个)
|
|
546
|
+
const dictationDir = () => {
|
|
547
|
+
if (dictationRoot)
|
|
548
|
+
return dictationRoot;
|
|
549
|
+
const dir = (dictationRoot = mkdtempSync(join(tmpdir(), 'cotutor-mock-dictation-')));
|
|
550
|
+
process.once('exit', () => rmSync(dir, { recursive: true, force: true }));
|
|
551
|
+
return dir;
|
|
552
|
+
};
|
|
553
|
+
const dictationDeps = () => ({
|
|
554
|
+
root: dictationDir(),
|
|
555
|
+
now,
|
|
556
|
+
card: async (home, n) => {
|
|
557
|
+
if (home !== mockHomeId())
|
|
558
|
+
return null;
|
|
559
|
+
const c = parseHome(mockHomeMd(localDate(now()), yesterday())).cards.filter((x) => x.kind === 'dictation')[n];
|
|
560
|
+
const r = c ? DictationPropsSchema.safeParse(c.props) : null;
|
|
561
|
+
return r?.success ? r.data : null;
|
|
562
|
+
},
|
|
563
|
+
// 同真服务:卡上写的(或拍照听写从哪位老师卡上进来的)老师在就用他,不然语文老师
|
|
564
|
+
reader: (t) => (t && MOCK_TUTORS.some((x) => x.name === t) ? t : 'chinese-tutor'),
|
|
565
|
+
display: (t) => MOCK_TUTORS.find((x) => x.name === t)?.display ?? '老师',
|
|
566
|
+
face: (t) => backTutorOf(Object.fromEntries(MOCK_TUTORS.map((x) => [x.name, { display: x.display, subject: x.subject }])), t),
|
|
567
|
+
// 念的是一段 0.8 秒的静音(不配音、不念浏览器合成声):页面照真服务那样放音频、等放完
|
|
568
|
+
say: async () => {
|
|
569
|
+
const f = join(dictationDir(), 'say-silence.wav');
|
|
570
|
+
if (!existsSync(f))
|
|
571
|
+
writeFileSync(f, wavOfPcm(Buffer.alloc(24000 * 2 * 0.8), 24000));
|
|
572
|
+
return f;
|
|
573
|
+
},
|
|
574
|
+
voiced: () => true,
|
|
575
|
+
medians: async (ch) => (await tianzigeData(ch))?.medians ?? null,
|
|
576
|
+
recognize: mockRecognize,
|
|
577
|
+
});
|
|
578
|
+
/** via → 按钮(真服务在 runtime/home/store.ts resolveVia;mock 从同一份原文取) */
|
|
579
|
+
const mockButton = (name, via) => {
|
|
580
|
+
if (!isObj(via))
|
|
581
|
+
return null;
|
|
582
|
+
if (via.button === 'new' || via.button === 'recent')
|
|
583
|
+
return via.button;
|
|
584
|
+
const tile = HOME_TILES.find((t) => t.id === via.button);
|
|
585
|
+
if (tile)
|
|
586
|
+
return { kind: 'activity', label: tile.activity, activity: tile.activity, params: {} };
|
|
587
|
+
if (via.home !== mockHomeId() || typeof via.button !== 'number')
|
|
588
|
+
return null;
|
|
589
|
+
const card = parseHome(mockHomeMd(localDate(now()), yesterday())).cards.find((c) => c.kind === 'tutor' && c.props.tutor === name);
|
|
590
|
+
return (card?.props.buttons ?? [])[via.button] ?? null;
|
|
591
|
+
};
|
|
592
|
+
const answer = (m, full) => {
|
|
593
|
+
if (full) {
|
|
594
|
+
m.section = full;
|
|
595
|
+
m.reply = full.lines[full.lines.length - 1]?.text ?? null;
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
m.section = sectionFromScript('这个我们明天接着说,好不好?');
|
|
599
|
+
m.reply = '这个我们明天接着说,好不好?';
|
|
600
|
+
}
|
|
601
|
+
m.pending = false;
|
|
602
|
+
};
|
|
603
|
+
/** 流式模拟:想的期间每隔一段露一张卡(讲稿句跟到那张卡为止),整段想完才定稿 */
|
|
604
|
+
const think = (t, m, cur = cursor) => {
|
|
605
|
+
const i = cur.get(t.name) ?? 0;
|
|
606
|
+
// 看完小课堂的第一问:老师放课里的那一段(小课堂卡 + [[play]]);不占脚本的位置
|
|
607
|
+
const lectureFirst = Boolean(m.lecture && !m.lecture.again);
|
|
608
|
+
const step = lectureFirst ? (m.lecture?.video ? MOCK_VIDEO_LECTURE_REPLY : MOCK_LECTURE_REPLY) : t.script[i];
|
|
609
|
+
if (!lectureFirst)
|
|
610
|
+
cur.set(t.name, i + 1);
|
|
611
|
+
const full = step ? withMockLooks(sectionFromScript(step), t.name, i) : null;
|
|
612
|
+
const n = full ? full.cards.length : 0;
|
|
613
|
+
const tick = delay / (n + 1);
|
|
614
|
+
return new Promise((resolve) => {
|
|
615
|
+
let k = 0;
|
|
616
|
+
const reveal = () => {
|
|
617
|
+
k++;
|
|
618
|
+
if (k <= n && full) {
|
|
619
|
+
// 拍的就绪同真服务一个规则(readyBeats;mock 没配音 = 不等配音):露到第 k 张卡时前 k-1 拍关了 → 页面就绪一拍播一拍;卡前的句(锚 null)也在,和真 runner 一样
|
|
620
|
+
// 铺到第 k 张时前 k-1 张的样子与行都已定(卡带 look,layout 只到已定的那几张)
|
|
621
|
+
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) };
|
|
622
|
+
const rows = full.layout ? full.layout.rows.map((r) => r.filter((i) => i < k - 1)).filter((r) => r.length) : [];
|
|
623
|
+
m.section = { ...part, ...(full.layout && rows.length ? { layout: { for: full.layout.for, rows } } : {}), partial: true, ready: readyBeats(part, { done: false }) };
|
|
624
|
+
setTimeout(reveal, tick);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
answer(m, full);
|
|
628
|
+
resolve();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
setTimeout(reveal, tick);
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
/** internal = 实时那条连接交来的孩子发的一条(kidSend);HTTP 上不收(同真服务,2026-11-04 删了 HTTP 的发) */
|
|
635
|
+
const routeIn = async (method, path, body, internal = false) => {
|
|
636
|
+
await lecturesLoaded;
|
|
637
|
+
const url = new URL(path, 'http://x');
|
|
638
|
+
const p = url.pathname;
|
|
639
|
+
if (p === '/')
|
|
640
|
+
return { status: 200, html: kidPage(title, {}, await stageVersion()) };
|
|
641
|
+
if (p === '/parent/qr')
|
|
642
|
+
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' } };
|
|
643
|
+
if (p === '/dev/voice')
|
|
644
|
+
return { status: 200, html: VOICE_GALLERY_PAGE };
|
|
645
|
+
if (p === '/manifest.webmanifest')
|
|
646
|
+
return { status: 200, json: webManifest(title), contentType: 'application/manifest+json; charset=utf-8' };
|
|
647
|
+
// 家长端「记录」:自己的一页(parent-page.ts);清单接口照真服务(答案不剥,第一节带一条给家长的尾巴与记忆)。mock 没有工作台 /dev
|
|
648
|
+
if (p === '/parent')
|
|
649
|
+
return { status: 200, html: parentPage(title) };
|
|
650
|
+
if (p === '/reel')
|
|
651
|
+
return { status: 200, html: kidPage(title, { reel: true }, await stageVersion()) };
|
|
652
|
+
if (p === '/preview')
|
|
653
|
+
return { status: 200, html: kidPage(title, { preview: true }, await stageVersion()) };
|
|
654
|
+
if (p === '/parent/manifest.webmanifest')
|
|
655
|
+
return { status: 200, json: webManifest(`${title} · 家长`, { startUrl: '/parent', scope: '/parent' }), contentType: 'application/manifest+json; charset=utf-8' };
|
|
656
|
+
// 主题:mock 没有 workspace,直接给包里的出厂 default
|
|
657
|
+
if (p === '/kid/theme.css')
|
|
658
|
+
return { status: 200, html: `${cardsCss()}\n\n${(await packageTheme()).css}`, contentType: 'text/css; charset=utf-8' };
|
|
659
|
+
if (p === '/kid/theme.json')
|
|
660
|
+
return { status: 200, json: (await packageTheme()).manifest };
|
|
661
|
+
const icon = /^\/icon-(\d{2,4})\.png$/.exec(p);
|
|
662
|
+
if (icon) {
|
|
663
|
+
const n = Number(icon[1]);
|
|
664
|
+
return ICON_SIZES.includes(n) ? { status: 200, body: appIconPng(n), contentType: 'image/png' } : { status: 404, json: { error: 'not_found' } };
|
|
665
|
+
}
|
|
666
|
+
const fav = faviconResponse(p);
|
|
667
|
+
if (fav)
|
|
668
|
+
return { status: 200, ...fav, cacheControl: 'no-cache' };
|
|
669
|
+
if (p === '/api/health')
|
|
670
|
+
return { status: 200, json: { ok: true, mock: true, scenario } };
|
|
671
|
+
if (p === '/api/kid/home' && method === 'GET')
|
|
672
|
+
return { status: 200, json: home() };
|
|
673
|
+
// 孩子端的流水(kid-log.ts):mock 照收,不落盘
|
|
674
|
+
if (p === '/api/kid/log' && method === 'POST')
|
|
675
|
+
return { status: 200, json: { ok: true } };
|
|
676
|
+
// 听写(src/app/dictation/,和真服务同一套接口):卡从 mock 的首页取,存在临时目录,念的是静音
|
|
677
|
+
const tx = await handleDictation(method, url, dictationDeps(), body);
|
|
678
|
+
if (tx)
|
|
679
|
+
return tx;
|
|
680
|
+
const ov = /^\/api\/overview\/(today|\d{4}-\d{2}-\d{2})$/.exec(p);
|
|
681
|
+
if (ov && method === 'GET') {
|
|
682
|
+
const today = localDate(now());
|
|
683
|
+
const date = ov[1] === 'today' ? today : ov[1];
|
|
684
|
+
if (date > today)
|
|
685
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
686
|
+
const listOf = (name, d = date) => (d === today ? messages.get(name) : d === yesterday() ? past.get(name) : undefined) ?? [];
|
|
687
|
+
const tutors = MOCK_TUTORS.map((t) => {
|
|
688
|
+
const list = listOf(t.name);
|
|
689
|
+
const by = new Map();
|
|
690
|
+
for (const m of list) {
|
|
691
|
+
let th = by.get(m.thread);
|
|
692
|
+
if (!th) {
|
|
693
|
+
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}`) };
|
|
694
|
+
by.set(m.thread, th);
|
|
695
|
+
}
|
|
696
|
+
// 给家长的、记住了:同 mock 的板书接口,那天第一条带
|
|
697
|
+
if (m === list[0] && !m.pending) {
|
|
698
|
+
th.notes.push('第一遍就答上了,后面那句是我故意留的:看他会不会自己往下想。');
|
|
699
|
+
th.remembered.push(`${date} 讲故事时爱抢着说结局,可以先让他猜`);
|
|
700
|
+
}
|
|
701
|
+
if (m.pending)
|
|
702
|
+
th.stoppedAt = 'writing';
|
|
703
|
+
else if (m.section && (m.section.cards.length || m.section.lines.length)) {
|
|
704
|
+
th.sections++;
|
|
705
|
+
th.cards += m.section.cards.length;
|
|
706
|
+
th.stoppedAt = m.section.lines[m.section.lines.length - 1]?.ask ? 'ask' : null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// 按住说话连着没发出去:攒着的实录里的 hold;mock 的消息没有真时刻,发成的一句只认打字发出去的
|
|
710
|
+
if (date === today)
|
|
711
|
+
for (const th of by.values()) {
|
|
712
|
+
const plays = (mockPlays[t.name] ?? []).filter((x) => x.thread === th.thread).map((x) => x.rec);
|
|
713
|
+
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));
|
|
714
|
+
if (n)
|
|
715
|
+
th.misses = n;
|
|
716
|
+
}
|
|
717
|
+
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') };
|
|
718
|
+
});
|
|
719
|
+
const week = weekOf(date, today).map((d) => ({ date: d, tutors: MOCK_TUTORS.filter((t) => listOf(t.name, d).length).map((t) => t.name) }));
|
|
720
|
+
const out = { title, date, today, tutors, week, ready: scenario !== 'nokey' };
|
|
721
|
+
return { status: 200, json: out };
|
|
722
|
+
}
|
|
723
|
+
const pb = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(today|\d{4}-\d{2}-\d{2})\/board$/.exec(p);
|
|
724
|
+
if (pb && method === 'GET') {
|
|
725
|
+
const [, name, tail] = pb;
|
|
726
|
+
const t = MOCK_TUTORS.find((x) => x.name === name);
|
|
727
|
+
if (!t)
|
|
728
|
+
return { status: 404, json: { error: 'no_such_tutor' } };
|
|
729
|
+
const today = localDate(now());
|
|
730
|
+
const date = tail === 'today' ? today : tail;
|
|
731
|
+
if (date > today)
|
|
732
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
733
|
+
const list = (date === today ? messages.get(name) : date === yesterday() ? past.get(name) : undefined) ?? [];
|
|
734
|
+
const out = await Promise.all(list.map(async (m, i) => {
|
|
735
|
+
const { states, ...rest } = m;
|
|
736
|
+
const withState = (c, k) => (states && k in states ? { ...c, state: states[k] } : c);
|
|
737
|
+
const section = m.section ? mockPaints(await enrichLectures(MOCK_LECTURE_DIRS, { ...m.section, cards: m.section.cards.map(withState) }), m, true) : null;
|
|
738
|
+
return { ...rest, from: 'kid', section, ...(i === 0 && !m.pending ? { parentText: '## 家长\n第一遍就答上了,后面那句是我故意留的:看他会不会自己往下想。', remembered: [`${date} 讲故事时爱抢着说结局,可以先让他猜`] } : {}) };
|
|
739
|
+
}));
|
|
740
|
+
const pending = list.find((m) => m.pending);
|
|
741
|
+
return { status: 200, json: { tutor: name, date, messages: out, pending: pending ? pending.job : null, thread: list.length ? list[list.length - 1].thread : null } };
|
|
742
|
+
}
|
|
743
|
+
// 看录像:mock 的消息没有真时刻(种子全是同一刻),照一个固定节奏排——
|
|
744
|
+
// 等老师 3 秒(第二轮 14 秒,看「等老师」的压缩)、念完孩子想 8 秒(第一轮 40 秒,看「孩子想了」);卡的状态算在下一轮开口前 5 秒
|
|
745
|
+
/** 看小课堂的那条:录像要它的看的过程(mock 的家长端条目带着 lecture 与 lectureLog) */
|
|
746
|
+
const lectureOfMock = (m) => {
|
|
747
|
+
const l = m.lecture;
|
|
748
|
+
if (!l)
|
|
749
|
+
return {};
|
|
750
|
+
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 } : {}) } };
|
|
751
|
+
};
|
|
752
|
+
const rl = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(today|\d{4}-\d{2}-\d{2})\/threads\/([^/]+)\/reel$/.exec(p);
|
|
753
|
+
if (rl && method === 'GET') {
|
|
754
|
+
const [, name, tail, raw] = rl;
|
|
755
|
+
const board = await route('GET', `/api/conversations/${name}/${tail}/board`);
|
|
756
|
+
if (board.status !== 200)
|
|
757
|
+
return board;
|
|
758
|
+
const day = board.json;
|
|
759
|
+
const thread = decodeURIComponent(raw);
|
|
760
|
+
const mine = day.messages.filter((m) => m.thread === thread);
|
|
761
|
+
let t = now().getTime() - 10 * 60000;
|
|
762
|
+
const starts = [];
|
|
763
|
+
const conv = mine.map((m, k) => {
|
|
764
|
+
const wait = k === 1 ? 14000 : 3000;
|
|
765
|
+
starts.push(t);
|
|
766
|
+
const startedAt = new Date(t).toISOString();
|
|
767
|
+
t += wait + (m.section?.lines ?? []).reduce((n, l) => n + lineDurationMs(l.text) + 200, 0) + (k === 0 ? 40000 : 8000);
|
|
768
|
+
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) };
|
|
769
|
+
});
|
|
770
|
+
const cards = {};
|
|
771
|
+
mine.forEach((m, k) => (m.section?.cards ?? []).forEach((c, n) => { if (c.state !== undefined)
|
|
772
|
+
(cards[m.job] ??= {})[n] = { at: new Date((starts[k + 1] ?? t) - 5000).toISOString(), turn: m.job, state: c.state }; }));
|
|
773
|
+
const plays = (mockPlays[name] ?? []).filter((x) => x.thread === thread).map((x) => x.rec);
|
|
774
|
+
const reel = buildReel({ messages: conv, events: {}, cards, now: now().getTime(), plays });
|
|
775
|
+
// mock 的孩子接口只认 today 是今天(写日期的走「以前的」那份)
|
|
776
|
+
const kidDay = await route('GET', `/api/kid/conversations/${name}/${tail === localDate(now()) ? 'today' : tail}`);
|
|
777
|
+
const kid = kidDay.status === 200 ? kidDay.json.messages.filter((m) => m.thread === thread) : [];
|
|
778
|
+
const tt = MOCK_TUTORS.find((x) => x.name === name);
|
|
779
|
+
const face = { name, display: tt?.display ?? name, avatar: tt?.avatar ?? null, subject: tt?.subject ?? null, color: colors[name] ?? NEUTRAL_COLOR, mark: boardMarkOf(tt?.subject) };
|
|
780
|
+
return reel ? { status: 200, json: { tutor: name, date: day.date, thread, reel, messages: mine, kid, device: null, face } } : { status: 404, json: { error: 'no_reel' } };
|
|
781
|
+
}
|
|
782
|
+
// 记账(家长端清单上的「记账」):这天还没记过的话题都算记了(真服务是每个话题起一轮老师;mock 立刻记上)
|
|
783
|
+
const bk = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/bookkeep$/.exec(p);
|
|
784
|
+
if (bk && method === 'POST') {
|
|
785
|
+
const [, name] = bk;
|
|
786
|
+
const list = messages.get(name);
|
|
787
|
+
if (!list)
|
|
788
|
+
return { status: 404, json: { error: 'no_such_tutor' } };
|
|
789
|
+
const queued = [...new Set(list.filter((m) => m.section && !m.pending).map((m) => m.thread))].filter((th) => !booked.has(`${name}/${th}`));
|
|
790
|
+
for (const th of queued)
|
|
791
|
+
booked.add(`${name}/${th}`);
|
|
792
|
+
return { status: 202, json: { tutor: name, queued, skipped: [] } };
|
|
793
|
+
}
|
|
794
|
+
// 删掉一个话题:列表里去掉;还在想的 409(mock 没有 vault,撤销记 0)
|
|
795
|
+
const del = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/threads\/([^/]+)$/.exec(p);
|
|
796
|
+
if (del && method === 'DELETE') {
|
|
797
|
+
const [, name, , thread] = del;
|
|
798
|
+
const store = messages;
|
|
799
|
+
const list = store.get(name);
|
|
800
|
+
if (!list || !list.some((m) => m.thread === thread))
|
|
801
|
+
return { status: 404, json: { error: 'no_such_thread' } };
|
|
802
|
+
if (list.some((m) => m.thread === thread && m.pending))
|
|
803
|
+
return { status: 409, json: { error: 'busy' } };
|
|
804
|
+
store.set(name, list.filter((m) => m.thread !== thread));
|
|
805
|
+
ratings.delete(`${name}/${thread}`);
|
|
806
|
+
return { status: 200, json: { tutor: name, thread, undo: { memory: 0, diary: 0, misses: [] } } };
|
|
807
|
+
}
|
|
808
|
+
const rate = /^\/api\/conversations\/([a-z0-9][a-z0-9-]*)\/(\d{4}-\d{2}-\d{2})\/threads\/([^/]+)\/rating$/.exec(p);
|
|
809
|
+
if (rate && method === 'PUT') {
|
|
810
|
+
const [, name, , thread] = rate;
|
|
811
|
+
if (!MOCK_TUTORS.some((x) => x.name === name))
|
|
812
|
+
return { status: 404, json: { error: 'no_such_tutor' } };
|
|
813
|
+
const rating = isObj(body) ? body.rating : undefined;
|
|
814
|
+
if (!(rating === null || (typeof rating === 'number' && Number.isInteger(rating) && rating >= 1 && rating <= 5)))
|
|
815
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
816
|
+
if (rating === null)
|
|
817
|
+
ratings.delete(`${name}/${thread}`);
|
|
818
|
+
else
|
|
819
|
+
ratings.set(`${name}/${thread}`, rating);
|
|
820
|
+
return { status: 200, json: { tutor: name, thread, rating } };
|
|
821
|
+
}
|
|
822
|
+
const pk = p;
|
|
823
|
+
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);
|
|
824
|
+
if (kid) {
|
|
825
|
+
const [, name, tail] = kid;
|
|
826
|
+
const t = MOCK_TUTORS.find((x) => x.name === name);
|
|
827
|
+
if (!t)
|
|
828
|
+
return { status: 404, json: { error: 'no_such_tutor' } };
|
|
829
|
+
const list = messages.get(name) ?? [];
|
|
830
|
+
const date = localDate(now());
|
|
831
|
+
// 录像的实录:收下就丢(mock 的录像按固定节奏排,不用它);不收的话页面每 10 秒撞一个 404
|
|
832
|
+
// 实录:不打盘,攒在内存里给录像用(按 sentAt 校钟、形状不对的丢掉、孩子端发的卡改动不收,同真服务)
|
|
833
|
+
if (tail === 'play' && method === 'POST') {
|
|
834
|
+
if (!isObj(body) || typeof body.thread !== 'string' || !Array.isArray(body.records))
|
|
835
|
+
return { status: 200, json: { kept: 0 } };
|
|
836
|
+
const skew = typeof body.sentAt === 'number' ? now().getTime() - body.sentAt : 0;
|
|
837
|
+
const kept = body.records.filter((x) => playRecordOk(x) && x.k !== 'card').map((x) => ({ ...x, at: Math.round(x.at + skew) }));
|
|
838
|
+
(mockPlays[name] ??= []).push(...kept.map((rec) => ({ thread: body.thread, rec })));
|
|
839
|
+
return { status: 200, json: { kept: kept.length } };
|
|
840
|
+
}
|
|
841
|
+
// 作业照片:不落盘,回一个像样的假路径(缩略图由 /api/kid/image 的占位 svg 顶)
|
|
842
|
+
if (tail === 'photos' && method === 'POST') {
|
|
843
|
+
if (!isObj(body) || typeof body.image !== 'string' || !body.image.startsWith('data:image/'))
|
|
844
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
845
|
+
const d = now();
|
|
846
|
+
const up = /^data:(image\/(?:jpeg|png));base64,(.+)$/.exec(body.image);
|
|
847
|
+
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'}`;
|
|
848
|
+
if (up)
|
|
849
|
+
uploads.set(path, { type: up[1], data: Buffer.from(up[2], 'base64') });
|
|
850
|
+
return { status: 201, json: { path } };
|
|
851
|
+
}
|
|
852
|
+
// 左栏的纸:同真服务(app.ts 的 paperRoute),存在内存里
|
|
853
|
+
if (tail === 'paper') {
|
|
854
|
+
const key = method === 'GET' ? (url.searchParams.get('key') ?? '') : isObj(body) && typeof body.key === 'string' ? body.key : '';
|
|
855
|
+
if (!PAPER_KEY_RE.test(key))
|
|
856
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
857
|
+
if (method === 'GET')
|
|
858
|
+
return { status: 200, json: { key, state: papers.get(`${name}|${key}`) ?? { ink: [] } } };
|
|
859
|
+
if (method !== 'PUT')
|
|
860
|
+
return { status: 405, json: { error: 'method_not_allowed' } };
|
|
861
|
+
const r = CanvasStateSchema.safeParse(isObj(body) ? body.state : null);
|
|
862
|
+
if (!r.success)
|
|
863
|
+
return { status: 400, json: { error: 'bad_state' } };
|
|
864
|
+
papers.set(`${name}|${key}`, { ink: r.data.ink, ...(r.data.cleared?.length ? { cleared: r.data.cleared } : {}) });
|
|
865
|
+
if (isObj(body) && body.from === 'new' && key !== 'new')
|
|
866
|
+
papers.delete(`${name}|new`);
|
|
867
|
+
return { status: 200, json: { ok: true, key } };
|
|
868
|
+
}
|
|
869
|
+
if (tail === 'today' && method === 'GET') {
|
|
870
|
+
const shown = (messages.get(name) ?? []);
|
|
871
|
+
const pending = shown.find((m) => m.pending);
|
|
872
|
+
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 };
|
|
873
|
+
return { status: 200, json: out };
|
|
874
|
+
}
|
|
875
|
+
if (tail === 'history' && method === 'GET') {
|
|
876
|
+
const days = [];
|
|
877
|
+
const todayThreads = kidThreads(await Promise.all((messages.get(name) ?? []).map(kidMessage))).reverse();
|
|
878
|
+
if (todayThreads.length)
|
|
879
|
+
days.push({ date, threads: todayThreads });
|
|
880
|
+
const old = past.get(name) ?? [];
|
|
881
|
+
if (old.length)
|
|
882
|
+
days.push({ date: yesterday(), threads: kidThreads(old) });
|
|
883
|
+
const out = { tutor: name, today: date, days };
|
|
884
|
+
return { status: 200, json: out };
|
|
885
|
+
}
|
|
886
|
+
if (/^\d{4}-/.test(tail) && method === 'GET') {
|
|
887
|
+
if (tail > date)
|
|
888
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
889
|
+
const old = tail === yesterday() ? (past.get(name) ?? []) : [];
|
|
890
|
+
const out = { tutor: name, date: tail, messages: await Promise.all(old.map(kidMessage)), pending: null, thread: old.length ? old[old.length - 1].thread : null };
|
|
891
|
+
return { status: 200, json: out };
|
|
892
|
+
}
|
|
893
|
+
if (tail === 'messages' && method === 'POST' && internal) {
|
|
894
|
+
if (!isObj(body) || typeof body.text !== 'string')
|
|
895
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
896
|
+
const action = body.action === 'submit' ? 'submit' : undefined;
|
|
897
|
+
const photos = Array.isArray(body.photos) ? body.photos.filter((x) => typeof x === 'string') : [];
|
|
898
|
+
const button = body.via === undefined ? undefined : mockButton(name, body.via);
|
|
899
|
+
if (button === null)
|
|
900
|
+
return { status: 400, json: { error: 'bad_via' } };
|
|
901
|
+
let said = body.text;
|
|
902
|
+
let newThread = body.newThread === true;
|
|
903
|
+
let wantThread = body.thread;
|
|
904
|
+
// 小课堂:看完才能开口,字是孩子说的,新话题;画的那几处现算成一段话(同 runner)。问过以后再看一遍又画了:同一话题的下一条带上(again)
|
|
905
|
+
// 一处几笔(strokes);2026-10-19 以前一笔一处(path),照读
|
|
906
|
+
const marksIn = isObj(body.lecture) && Array.isArray(body.lecture.marks) ? body.lecture.marks.map(({ path, strokes, ...m }) => ({ ...m, strokes: strokes ?? (path ? [path] : []) })) : [];
|
|
907
|
+
const logIn = isObj(body.lecture) && Array.isArray(body.lecture.log) ? body.lecture.log : [];
|
|
908
|
+
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;
|
|
909
|
+
if (lec && (typeof button !== 'object' || button === null || button.kind !== 'lecture')) {
|
|
910
|
+
const from = list.find((x) => x.thread === body.thread && x.lecture && !x.lecture.again);
|
|
911
|
+
if (!from || from.lecture?.bundle !== lec.bundle || (!marksIn.length && !logIn.length))
|
|
912
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
913
|
+
lec.again = true;
|
|
914
|
+
}
|
|
915
|
+
if (typeof button === 'object' && button !== null && button.kind === 'lecture') {
|
|
916
|
+
if (!lec || lec.bundle !== button.bundle || !lec.finished)
|
|
917
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
918
|
+
newThread = true;
|
|
919
|
+
wantThread = undefined;
|
|
920
|
+
}
|
|
921
|
+
else if (button && button !== 'new' && button !== 'recent') {
|
|
922
|
+
said = button.label;
|
|
923
|
+
if (button.kind === 'continue' && button.date === date) {
|
|
924
|
+
wantThread = button.thread;
|
|
925
|
+
newThread = false;
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
newThread = true;
|
|
929
|
+
wantThread = undefined;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
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;
|
|
933
|
+
if (body.drawing !== undefined && !drawIn)
|
|
934
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
935
|
+
const drawing = drawIn ? paperDrawing(papers.get(`${name}|${drawIn.key}`) ?? { ink: [] }, drawIn.image) : null;
|
|
936
|
+
if (!said.trim() && !action && !photos.length && !drawing)
|
|
937
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
938
|
+
if (list.some((m) => m.pending))
|
|
939
|
+
return { status: 409, json: { error: 'busy' } };
|
|
940
|
+
const focusCard = isObj(body.focus) && typeof body.focus.card === 'string' ? body.focus.card : null;
|
|
941
|
+
if (action === 'submit' && focusCard && list.some((x) => x.submitted === focusCard))
|
|
942
|
+
return { status: 409, json: { error: 'submitted' } };
|
|
943
|
+
const text = said.trim() || (action === 'submit' ? '(交了答案,没说话)' : photos.length === 1 ? '(拍了一张)' : photos.length ? `(拍了 ${photos.length} 张)` : DRAW_ONLY_TEXT);
|
|
944
|
+
const job = nextJob();
|
|
945
|
+
// 话题:newThread → 自己的 job;指定的要在今天的列表里;缺省接当前(末条)的
|
|
946
|
+
let thread = job;
|
|
947
|
+
if (!newThread && list.length) {
|
|
948
|
+
if (wantThread !== undefined) {
|
|
949
|
+
if (typeof wantThread !== 'string' || !list.some((x) => x.thread === wantThread))
|
|
950
|
+
return { status: 400, json: { error: 'bad_request' } };
|
|
951
|
+
thread = wantThread;
|
|
952
|
+
}
|
|
953
|
+
else
|
|
954
|
+
thread = list[list.length - 1].thread;
|
|
955
|
+
}
|
|
956
|
+
const lt = lec ? mockLecture(lec.bundle) : null;
|
|
957
|
+
const marks = lt ? storedMarks(lt, marksIn, photos.length) : [];
|
|
958
|
+
sentAt.set(job, Date.now());
|
|
959
|
+
// 新话题的第一条:话题还没有时画的那张纸归它(同真服务的 adoptPaper)
|
|
960
|
+
if (thread === job && papers.has(`${name}|new`) && !papers.has(`${name}|${thread}`)) {
|
|
961
|
+
papers.set(`${name}|${thread}`, papers.get(`${name}|new`));
|
|
962
|
+
papers.delete(`${name}|new`);
|
|
963
|
+
}
|
|
964
|
+
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 } : {}) } : {}) };
|
|
965
|
+
list.push(m);
|
|
966
|
+
const done = think(t, m, cursor).then(() => { inflight.delete(m.job); });
|
|
967
|
+
inflight.set(m.job, done);
|
|
968
|
+
return { status: 202, json: { tutor: name, date, job: m.job, thread } };
|
|
969
|
+
}
|
|
970
|
+
return { status: 405, json: { error: 'method_not_allowed' } };
|
|
971
|
+
}
|
|
972
|
+
// 卡的状态:孩子的走 /api/kid/…
|
|
973
|
+
const card = /^\/api\/kid\/conversations\/([a-z0-9][a-z0-9-]*)\/cards\/([^/]+)\/(\d+)$/.exec(pk);
|
|
974
|
+
if (card) {
|
|
975
|
+
const [, name, job, n] = card;
|
|
976
|
+
const m = (messages.get(name) ?? []).find((x) => x.job === job);
|
|
977
|
+
const target = m?.section?.cards[Number(n)];
|
|
978
|
+
if (!target)
|
|
979
|
+
return { status: 404, json: { error: 'no_such_card' } };
|
|
980
|
+
if (method !== 'PUT')
|
|
981
|
+
return { status: 405, json: { error: 'method_not_allowed' } };
|
|
982
|
+
if ((messages.get(name) ?? []).some((x) => x.submitted === `${job}/${n}`))
|
|
983
|
+
return { status: 409, json: { error: 'submitted' } };
|
|
984
|
+
const r = parseCardState(target, body);
|
|
985
|
+
if (!r.ok)
|
|
986
|
+
return { status: 400, json: { error: 'bad_state' } };
|
|
987
|
+
(m.states ??= {})[Number(n)] = r.state;
|
|
988
|
+
return { status: 200, json: { ok: true, card: `${job}/${n}`, state: r.state } };
|
|
989
|
+
}
|
|
990
|
+
const hz = /^\/api\/kid\/tianzige\/([^/]+)$/.exec(p);
|
|
991
|
+
if (hz) {
|
|
992
|
+
// 田字格卡:数据包就在 node_modules 里,mock 也给真笔顺
|
|
993
|
+
const d = await tianzigeData(decodeURIComponent(hz[1]));
|
|
994
|
+
return d ? { status: 200, json: d } : { status: 404, json: { error: 'not_found' } };
|
|
995
|
+
}
|
|
996
|
+
const lt = /^\/api\/kid\/letters\/([^/]+)$/.exec(p);
|
|
997
|
+
if (lt) {
|
|
998
|
+
// 单词卡:字形包就在 node_modules 里,mock 也给真笔顺
|
|
999
|
+
const d = lettersData(decodeURIComponent(lt[1]));
|
|
1000
|
+
return d ? { status: 200, json: d } : { status: 404, json: { error: 'not_found' } };
|
|
1001
|
+
}
|
|
1002
|
+
if (p === '/api/kid/image') {
|
|
1003
|
+
// 图片卡:任何路径都给一张占位 svg(写着路径),前端能看到版式;画图卡(mock-paint/)给一张方的四格
|
|
1004
|
+
const rel = url.searchParams.get('p') ?? '';
|
|
1005
|
+
if (rel.startsWith('mock-paint/'))
|
|
1006
|
+
return { status: 200, contentType: 'image/svg+xml', html: MOCK_PAINT_SVG };
|
|
1007
|
+
const up = uploads.get(rel);
|
|
1008
|
+
if (up)
|
|
1009
|
+
return { status: 200, contentType: up.type, body: up.data };
|
|
1010
|
+
const esc = rel.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);
|
|
1011
|
+
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>` };
|
|
1012
|
+
}
|
|
1013
|
+
if (p.startsWith('/stage/') && method === 'GET') {
|
|
1014
|
+
if (p === '/stage/') {
|
|
1015
|
+
const html = await stageIndex();
|
|
1016
|
+
return html === null ? { status: 404, json: { error: 'not_found' } } : { status: 200, html };
|
|
1017
|
+
}
|
|
1018
|
+
const f = await stageAsset(p);
|
|
1019
|
+
return f ? { status: 200, file: f.file, contentType: f.contentType } : { status: 404, json: { error: 'not_found' } };
|
|
1020
|
+
}
|
|
1021
|
+
// 视频小课堂:同真服务(app.ts)
|
|
1022
|
+
const lv = /^\/api\/kid\/lectures\/([a-z0-9][a-z0-9-]*)\/(lecture\.json|video\.mp4)$/.exec(p);
|
|
1023
|
+
if (lv && method === 'GET') {
|
|
1024
|
+
const l = lectures.get(lv[1]);
|
|
1025
|
+
if (!l?.video)
|
|
1026
|
+
return { status: 404, json: { error: 'not_found' } };
|
|
1027
|
+
if (lv[2] === 'video.mp4')
|
|
1028
|
+
return { status: 200, file: join(MOCK_LECTURES_DIR, lv[1], 'video.mp4'), contentType: 'video/mp4' };
|
|
1029
|
+
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 })) } };
|
|
1030
|
+
}
|
|
1031
|
+
const frame = /^\/api\/bundles\/([a-z0-9][a-z0-9-]*)\/frame\.svg$/.exec(p);
|
|
1032
|
+
if (frame && method === 'GET') {
|
|
1033
|
+
const svg = await lectureFrameSvg({ dirs: { bundles: MOCK_BUNDLES_DIR } }, frame[1], parseSvgMs(url.searchParams.get('svg')), parseRing(url.searchParams.get('ring')), { bake: ensureBaked });
|
|
1034
|
+
return svg === null ? { status: 404, json: { error: 'not_found' } } : { status: 200, body: Buffer.from(svg), contentType: 'image/svg+xml; charset=utf-8' };
|
|
1035
|
+
}
|
|
1036
|
+
if (p.startsWith('/api/bundles/') && method === 'GET') {
|
|
1037
|
+
const f = await bundleAsset(MOCK_BUNDLES_DIR, p);
|
|
1038
|
+
return f ? { status: 200, file: f.file, contentType: f.contentType } : { status: 404, json: { error: 'not_found' } };
|
|
1039
|
+
}
|
|
1040
|
+
// 卡的配音(点读段、单词):mock 不配音,一律回 0.8 秒的静音
|
|
1041
|
+
if (p.startsWith('/api/audio/') && method === 'GET')
|
|
1042
|
+
return { status: 200, contentType: 'audio/wav', body: wavOfPcm(Buffer.alloc(24000 * 2 * 0.8), 24000) };
|
|
1043
|
+
// 字幕重放:真服务端给 src/runtime/live/call.ts 落的那份嘴的声音;mock 没有嘴,每句按字数估多长、放一段静音
|
|
1044
|
+
const mouth = /^\/api\/kid\/conversations\/([a-z0-9][a-z0-9-]*)\/mouth\/(\d{4}-\d{2}-\d{2})\/(\d{4}-\d+)$/.exec(p);
|
|
1045
|
+
if (mouth && method === 'GET') {
|
|
1046
|
+
const day = (await route('GET', `/api/kid/conversations/${mouth[1]}/today`)).json;
|
|
1047
|
+
const m = day?.messages.find((x) => x.job === mouth[3]);
|
|
1048
|
+
const said = m && m.live && m.section ? m.section.lines : [];
|
|
1049
|
+
if (!said.length)
|
|
1050
|
+
return { status: 404, json: { error: 'not_found' } };
|
|
1051
|
+
let at = 0;
|
|
1052
|
+
const lines = said.map((l, line) => { const r = { line, ms: at }; at += lineDurationMs(plainLine(l.text)); return r; });
|
|
1053
|
+
return { status: 200, json: { audio: `/api/mock/silence.wav?ms=${at}`, ms: at, lines } };
|
|
1054
|
+
}
|
|
1055
|
+
if (p === '/api/mock/silence.wav' && method === 'GET') {
|
|
1056
|
+
const ms = Math.min(120_000, Math.max(0, Number(url.searchParams.get('ms')) || 0));
|
|
1057
|
+
return { status: 200, contentType: 'audio/wav', body: wavOfPcm(Buffer.alloc(Math.round(ms * 24) * 2), 24000) };
|
|
1058
|
+
}
|
|
1059
|
+
if (p.startsWith('/api/'))
|
|
1060
|
+
return { status: 404, json: { error: 'not_found', path: p } };
|
|
1061
|
+
return { status: 404, json: { error: 'not_found', path: p } };
|
|
1062
|
+
};
|
|
1063
|
+
const settle = async () => { await Promise.all([...inflight.values()]); };
|
|
1064
|
+
const handler = (req, res) => {
|
|
1065
|
+
void (async () => {
|
|
1066
|
+
const chunks = [];
|
|
1067
|
+
for await (const c of req)
|
|
1068
|
+
chunks.push(c);
|
|
1069
|
+
let body;
|
|
1070
|
+
try {
|
|
1071
|
+
body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : undefined;
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
body = undefined;
|
|
1075
|
+
}
|
|
1076
|
+
const r = await route(req.method ?? 'GET', req.url ?? '/', body);
|
|
1077
|
+
if (r.file !== undefined)
|
|
1078
|
+
await sendFile(req, res, { status: r.status, file: r.file, contentType: r.contentType, cacheControl: 'private, max-age=3600' });
|
|
1079
|
+
else if (r.body !== undefined) {
|
|
1080
|
+
res.writeHead(r.status, { 'content-type': r.contentType ?? 'application/octet-stream', 'cache-control': r.cacheControl ?? 'private, max-age=3600' });
|
|
1081
|
+
res.end(Buffer.from(r.body));
|
|
1082
|
+
}
|
|
1083
|
+
else if (r.html !== undefined) {
|
|
1084
|
+
res.writeHead(r.status, { 'content-type': r.contentType ?? 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
1085
|
+
res.end(r.html);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
res.writeHead(r.status, { 'content-type': r.contentType ?? 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
1089
|
+
res.end(JSON.stringify(r.json ?? null));
|
|
1090
|
+
}
|
|
1091
|
+
})().catch((err) => { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'internal', message: String(err) })); });
|
|
1092
|
+
};
|
|
1093
|
+
const settleJob = async (job) => { await inflight.get(job); };
|
|
1094
|
+
const route = (method, path, body) => routeIn(method, path, body);
|
|
1095
|
+
const kidSend = (tutor, body) => routeIn('POST', `/api/kid/conversations/${tutor}/messages`, isObj(body) ? { ...body, live: true } : body, true);
|
|
1096
|
+
const mock = { route, kidSend, settle, settleJob, handler, listen: null };
|
|
1097
|
+
return mock;
|
|
1098
|
+
}
|
|
1099
|
+
export async function serveMock(opts = {}) {
|
|
1100
|
+
const mock = createMock(opts);
|
|
1101
|
+
let tls = null;
|
|
1102
|
+
if (!opts.http) {
|
|
1103
|
+
try {
|
|
1104
|
+
tls = { cert: readFileSync(join(USER_CERT_DIR, 'cert.pem')), key: readFileSync(join(USER_CERT_DIR, 'key.pem')) };
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
tls = null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const server = tls ? createHttps(tls, mock.handler) : createHttp(mock.handler);
|
|
1111
|
+
{
|
|
1112
|
+
const up = mockLiveUpgrade(mock);
|
|
1113
|
+
server.on('upgrade', (req, socket, head) => { if (!up(req, socket, head))
|
|
1114
|
+
socket.destroy(); });
|
|
1115
|
+
}
|
|
1116
|
+
await new Promise((resolve, reject) => {
|
|
1117
|
+
server.once('error', reject);
|
|
1118
|
+
server.listen(opts.port ?? 8790, opts.host ?? '0.0.0.0', () => resolve());
|
|
1119
|
+
});
|
|
1120
|
+
const port = server.address().port;
|
|
1121
|
+
const scheme = tls ? 'https' : 'http';
|
|
1122
|
+
const urls = [localName(), ...lanAddresses()].map((h) => siteUrl(scheme, h, port));
|
|
1123
|
+
mock.listen = () => listenInfo(Boolean(tls), port);
|
|
1124
|
+
return { server, mock, port, https: Boolean(tls), urls, qrPage: siteUrl(scheme, 'localhost', port, '/parent/qr') };
|
|
1125
|
+
}
|