cotutor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +9 -7
  2. package/NOTICE +3 -3
  3. package/README.md +36 -13
  4. package/avatars/chinese.webp +0 -0
  5. package/avatars/english.webp +0 -0
  6. package/avatars/math.webp +0 -0
  7. package/bin/cotutor.js +13 -3
  8. package/cards/photo/card.css +10 -3
  9. package/cards/photo/card.md +2 -2
  10. package/cards/tutor/card.css +13 -0
  11. package/cards/tutor/card.md +2 -1
  12. package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
  13. package/dist/app/activity-eval.js +3 -2
  14. package/dist/app/cli/activities.d.ts +8 -1
  15. package/dist/app/cli/activities.js +42 -4
  16. package/dist/app/cli/cert.d.ts +5 -0
  17. package/dist/app/cli/cert.js +20 -7
  18. package/dist/app/cli/chalk/browser.d.ts +2 -2
  19. package/dist/app/cli/chalk/browser.js +22 -7
  20. package/dist/app/cli/chalk/main.js +3 -2
  21. package/dist/app/cli/doctor.d.ts +6 -5
  22. package/dist/app/cli/doctor.js +107 -171
  23. package/dist/app/cli/firewall.d.ts +24 -0
  24. package/dist/app/cli/firewall.js +79 -0
  25. package/dist/app/cli/init.js +8 -5
  26. package/dist/app/cli/main.js +347 -24
  27. package/dist/app/cli/migrate.d.ts +2 -11
  28. package/dist/app/cli/migrate.js +1 -59
  29. package/dist/app/cli/port-holder.d.ts +16 -0
  30. package/dist/app/cli/port-holder.js +34 -0
  31. package/dist/app/cli/port.d.ts +37 -0
  32. package/dist/app/cli/port.js +116 -0
  33. package/dist/app/cli/sandbox.d.ts +25 -0
  34. package/dist/app/cli/sandbox.js +166 -0
  35. package/dist/app/cli/serve-log.d.ts +13 -0
  36. package/dist/app/cli/serve-log.js +90 -0
  37. package/dist/app/cli/serve.d.ts +18 -4
  38. package/dist/app/cli/serve.js +78 -8
  39. package/dist/app/cli/service-windows.d.ts +50 -0
  40. package/dist/app/cli/service-windows.js +117 -0
  41. package/dist/app/cli/service.d.ts +138 -0
  42. package/dist/app/cli/service.js +429 -0
  43. package/dist/app/cli/skeleton.d.ts +1 -1
  44. package/dist/app/cli/skeleton.js +13 -13
  45. package/dist/app/cli/skills.d.ts +5 -1
  46. package/dist/app/cli/skills.js +14 -4
  47. package/dist/app/cli/tray-icons.d.ts +11 -0
  48. package/dist/app/cli/tray-icons.js +87 -0
  49. package/dist/app/cli/tray-ids.d.ts +6 -0
  50. package/dist/app/cli/tray-ids.js +6 -0
  51. package/dist/app/cli/tray-mac.d.ts +8 -0
  52. package/dist/app/cli/tray-mac.js +140 -0
  53. package/dist/app/cli/tray-run.d.ts +1 -0
  54. package/dist/app/cli/tray-run.js +33 -0
  55. package/dist/app/cli/tray-windows.d.ts +5 -0
  56. package/dist/app/cli/tray-windows.js +166 -0
  57. package/dist/app/cli/tray.d.ts +205 -0
  58. package/dist/app/cli/tray.js +618 -0
  59. package/dist/app/cli/usage.d.ts +1 -1
  60. package/dist/app/cli/usage.js +11 -6
  61. package/dist/app/dictation/route.js +3 -2
  62. package/dist/app/http/app.d.ts +5 -10
  63. package/dist/app/http/app.js +45 -98
  64. package/dist/app/http/avatars.d.ts +17 -0
  65. package/dist/app/http/avatars.js +35 -0
  66. package/dist/app/http/dev-tutor-files.js +1 -1
  67. package/dist/app/http/parent-home.d.ts +14 -0
  68. package/dist/app/http/parent-home.js +69 -6
  69. package/dist/app/http/parent-look.d.ts +2 -0
  70. package/dist/app/http/parent-look.js +151 -0
  71. package/dist/app/http/raw-view.js +3 -3
  72. package/dist/app/http/voice-preview.d.ts +4 -0
  73. package/dist/app/http/voice-preview.js +34 -0
  74. package/dist/app/machine/bailian.d.ts +0 -2
  75. package/dist/app/machine/bailian.js +5 -6
  76. package/dist/app/mock/mock-live.js +1 -1
  77. package/dist/app/mock/mock.d.ts +4 -2
  78. package/dist/app/mock/mock.js +20 -12
  79. package/dist/app/replay.js +4 -4
  80. package/dist/app/skills/home-doc.js +1 -1
  81. package/dist/app/workspace.d.ts +9 -4
  82. package/dist/app/workspace.js +33 -10
  83. package/dist/app/ws/route.d.ts +8 -2
  84. package/dist/app/ws/route.js +7 -3
  85. package/dist/board/parse.d.ts +0 -2
  86. package/dist/board/parse.js +0 -6
  87. package/dist/board/presentation.d.ts +9 -15
  88. package/dist/board/presentation.js +8 -10
  89. package/dist/brain/api-tools.js +2 -1
  90. package/dist/brain/api.d.ts +12 -17
  91. package/dist/brain/api.js +16 -33
  92. package/dist/brain/index.d.ts +23 -71
  93. package/dist/brain/index.js +6 -69
  94. package/dist/brain/log.d.ts +5 -0
  95. package/dist/brain/log.js +22 -0
  96. package/dist/brain/runtime.d.ts +30 -0
  97. package/dist/brain/runtime.js +23 -0
  98. package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
  99. package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
  100. package/dist/brain/stream.d.ts +5 -5
  101. package/dist/brain/stream.js +7 -7
  102. package/dist/brain/transcript.d.ts +3 -3
  103. package/dist/brain/transcript.js +6 -6
  104. package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
  105. package/dist/brain/types.js +14 -0
  106. package/dist/chalk/glyphs/skeleton.d.ts +3 -3
  107. package/dist/chalk/glyphs/skeleton.js +1 -1
  108. package/dist/lib/index.d.ts +1 -1
  109. package/dist/lib/index.js +1 -1
  110. package/dist/memory/diary.js +3 -3
  111. package/dist/realtime/bailian.d.ts +1 -1
  112. package/dist/realtime/bailian.js +3 -2
  113. package/dist/realtime/preview.js +1 -1
  114. package/dist/realtime/voices.d.ts +2 -0
  115. package/dist/realtime/voices.js +57 -57
  116. package/dist/runtime/activities.d.ts +13 -1
  117. package/dist/runtime/activities.js +28 -2
  118. package/dist/runtime/activity-state.js +2 -2
  119. package/dist/runtime/agent-file.d.ts +3 -3
  120. package/dist/runtime/agent-file.js +3 -3
  121. package/dist/runtime/context/tutor-rules.d.ts +2 -2
  122. package/dist/runtime/context/tutor-rules.js +3 -3
  123. package/dist/runtime/home/home.d.ts +12 -1
  124. package/dist/runtime/home/home.js +8 -1
  125. package/dist/runtime/home/store.d.ts +1 -1
  126. package/dist/runtime/home/store.js +36 -21
  127. package/dist/runtime/journal/mix.d.ts +15 -6
  128. package/dist/runtime/journal/mix.js +60 -19
  129. package/dist/runtime/journal/recorder.d.ts +58 -13
  130. package/dist/runtime/journal/recorder.js +106 -16
  131. package/dist/runtime/journal/store.d.ts +4 -4
  132. package/dist/runtime/journal/store.js +6 -6
  133. package/dist/runtime/kid-input.js +2 -3
  134. package/dist/runtime/live/call.d.ts +12 -0
  135. package/dist/runtime/live/call.js +51 -18
  136. package/dist/runtime/live/turns.js +1 -1
  137. package/dist/runtime/runner.d.ts +9 -55
  138. package/dist/runtime/runner.js +52 -173
  139. package/dist/scenes/paint/painter.js +4 -3
  140. package/dist/schema/config.d.ts +43 -22
  141. package/dist/schema/config.js +45 -47
  142. package/dist/schema/conversation.d.ts +6 -0
  143. package/dist/schema/home.d.ts +11 -1
  144. package/dist/schema/home.js +7 -2
  145. package/dist/schema/workspace.d.ts +27 -0
  146. package/dist/schema/workspace.js +28 -0
  147. package/dist/shared/default-port.d.ts +2 -0
  148. package/dist/shared/default-port.js +2 -0
  149. package/dist/shared/qianwen.d.ts +14 -10
  150. package/dist/shared/qianwen.js +26 -22
  151. package/dist/shared/site-url.d.ts +2 -0
  152. package/dist/shared/site-url.js +5 -0
  153. package/dist/ui/dev/dev-page.js +25 -252
  154. package/dist/ui/dev/dev-theme.d.ts +1 -1
  155. package/dist/ui/dev/dev-theme.js +1 -1
  156. package/dist/ui/dev/voice-gallery-page.js +1 -1
  157. package/dist/ui/kid/board-mark.d.ts +2 -2
  158. package/dist/ui/kid/board-mark.js +2 -2
  159. package/dist/ui/kid/home-tiles.d.ts +14 -0
  160. package/dist/ui/kid/home-tiles.js +44 -0
  161. package/dist/ui/kid/kid-page.js +68 -103
  162. package/dist/ui/kid/voice-ui.d.ts +1 -1
  163. package/dist/ui/kid/voice-ui.js +1 -1
  164. package/dist/ui/kid/word-emoji.js +5 -3
  165. package/dist/ui/parent/parent-home-page.js +315 -7
  166. package/dist/ui/parent/parent-page.js +6 -2
  167. package/dist/ui/parent/parent-settings-page.d.ts +1 -0
  168. package/dist/ui/parent/parent-settings-page.js +133 -0
  169. package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
  170. package/dist/ui/{dev → parent}/qr-page.js +19 -14
  171. package/dist/ui/shared/brand.d.ts +3 -1
  172. package/dist/ui/shared/brand.js +3 -1
  173. package/grades/grade-2/README.md +1 -1
  174. package/grades/grade-2/VALIDATION.md +1 -1
  175. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
  176. package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
  177. package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
  178. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
  179. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
  180. package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
  181. package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
  182. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
  183. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
  184. package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
  185. package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
  186. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
  187. package/package.json +2 -1
  188. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
  189. package/skills/cotutor-board/references/photo.md +2 -2
  190. package/skills/cotutor-home/SKILL.md +1 -0
  191. package/skills/cotutor-home/references/tutor.md +2 -1
  192. package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
  193. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
  194. package/skills/cotutor-tutor/SKILL.md +5 -13
  195. package/dist/brain/cli/adapters.d.ts +0 -60
  196. package/dist/brain/cli/adapters.js +0 -91
  197. package/dist/brain/cli/qwen.d.ts +0 -11
  198. package/dist/brain/cli/qwen.js +0 -141
  199. package/dist/brain/cli/run-plan.d.ts +0 -59
  200. package/dist/brain/cli/run-plan.js +0 -71
  201. package/dist/brain/cli/run.d.ts +0 -35
  202. package/dist/brain/cli/run.js +0 -256
  203. package/dist/brain/cli/types.js +0 -3
  204. package/dist/brain/cli/warm.d.ts +0 -53
  205. package/dist/brain/cli/warm.js +0 -103
@@ -17,7 +17,7 @@ export async function previewVoice(o) {
17
17
  });
18
18
  const why = await s.connect();
19
19
  if (why !== null)
20
- return { error: why === 'no_auth' ? '没有百炼的 key 或业务空间 id(DASHSCOPE_API_KEY、DASHSCOPE_WORKSPACE_ID)' : `连不上:${why}` };
20
+ return { error: why === 'no_auth' ? '没有百炼的 key 或业务空间 id(家长端「首页」里填,存在 ~/.config/cotutor/bailian.json)' : `连不上:${why}` };
21
21
  s.createResponse(readCue([o.text]));
22
22
  const until = Date.now() + (o.timeoutMs ?? 20_000);
23
23
  while (!done && !err && Date.now() < until)
@@ -11,6 +11,8 @@ export interface RealtimeVoice {
11
11
  voice: string;
12
12
  /** 官方的中文名 */
13
13
  name: string;
14
+ /** 男 / 女:官方表没有这一栏,照名字与描述标;看不出的三个(Theo Calm、Arda、longanlingxin)2026-11-03 真念一句量了音高 */
15
+ gender: '男' | '女';
14
16
  trait: string;
15
17
  lang: string;
16
18
  }
@@ -6,64 +6,64 @@
6
6
  * 英语老师的讲稿是中文里夹英文(「apple 苹果,banana 香蕉」):2026-10-20 真连(照念 → 转写回字)Serena、Tina、Maia、Liora Mira、Mione、Theo Calm 都念对,
7
7
  * Jennifer、Mia 的中文带英文腔(Jennifer:「腿」成了 tway,「它是什么颜色」没了),trait 里记着。
8
8
  */
9
- const v = (voice, name, trait, lang = '普通话') => ({ voice, name, trait, lang });
9
+ const v = (voice, name, gender, trait, lang = '普通话') => ({ voice, name, gender, trait, lang });
10
10
  export const REALTIME_VOICES = [
11
- v('Tina', '甜甜', '甜美温暖,解决问题果断(缺省)'),
12
- v('Cindy', '林欣宜', '台湾腔嗲嗲的小姐姐', '台湾口音'),
13
- v('Liora Mira', '清欢', '温柔的烟火人间感'),
14
- v('Raymond', '林川野', '声音清亮,爱吃外卖的宅男'),
15
- v('Zane', '泽恩', '磁性迷人'),
16
- v('Katerina', '卡捷琳娜', '御姐音,韵律感强'),
17
- v('Ryan', '甜茶', '节奏感强,戏感足'),
18
- v('Mia', '舒然', '细腻,生活美学风格的温柔博主;中英夹着说时中文带英文腔'),
19
- v('Cici', '绵绵', '邻家妹妹,活泼软糯'),
20
- v('Theo Calm', '予安', '沉静,治愈感'),
21
- v('Serena', '苏瑶', '温柔小姐姐'),
22
- v('Maia', '四月', '知性与温柔'),
23
- v('Evan', '江晨', '男大学生,年下奶狗'),
24
- v('Qiao', '小乔妹', '表面甜妹,个性十足', '台湾口音'),
25
- v('Momo', '茉兔', '撒娇搞怪'),
26
- v('Wil', '伟伦', '深圳长大的港台腔小哥哥'),
27
- v('Angel', '安琪', '带台式口音,甜美'),
28
- v('Li Cassian', '李公公', '话中有留白,善察言观色'),
29
- v('Joyner', '阿逗', '搞笑、夸张、接地气'),
30
- v('Gold', '金爷', '西海岸黑人说唱歌手风格'),
31
- v('Jennifer', '詹妮弗', '电影质感的美语女声;中英夹着说时中文带英文腔(苹果念成 pingguo)'),
32
- v('Aiden', '艾登', '精通厨艺的美语大男孩'),
33
- v('Mione', '敏儿', '成熟知性的英国邻家妹妹'),
34
- v('Sunny', '晴儿', '甜美的川妹子', '四川话'),
35
- v('Dylan', '晓东', '北京胡同长大的少年', '北京话'),
36
- v('Eric', '程川', '成都市井男子,跳脱', '四川话'),
37
- v('Peter', '李彼得', '天津相声,专业捧哏', '天津话'),
38
- v('Joseph Chen', '阿樸伯', '南洋老华侨', '闽南话'),
39
- v('Marcus', '秦川', '老陕风格,面宽话短', '陕西话'),
40
- v('Li', '老李', '南京话伯伯,爱骂骂咧咧', '南京话'),
41
- v('Rocky', '阿强', '幽默风趣,在线陪聊', '粤语'),
42
- v('Kiki', '阿清', '甜美的港妹闺蜜', '粤语'),
43
- v('Sohee', '素熙', '温柔开朗的韩国欧尼'),
44
- v('Eliška', '艾莉卡', '中欧风格'),
45
- v('Alek', '阿列克', '战斗民族的冷与暖'),
46
- v('Arda', '阿尔达', '干净利落,温润'),
47
- v('Dolce', '多尔切', '慵懒的意大利大叔'),
48
- v('Lenn', '莱恩', '德国青年,理性中带叛逆'),
49
- v('Ono Anna', '小野杏', '鬼灵精怪的青梅竹马'),
50
- v('Sonrisa', '索尼莎', '热情开朗的拉美大姐'),
51
- v('Bodega', '博德加', '热情的西班牙大叔'),
52
- v('Andre', '安德雷', '磁性、沉稳的男生'),
53
- v('Radio Gol', '拉迪奥·戈尔', '足球解说风格'),
54
- v('Rizky', '阿力', '印尼青年,声线个性'),
55
- v('Roya', '萝雅', '热爱运动的女孩'),
56
- v('Hana', '阿幸', '越南成熟姐姐'),
57
- v('Jakub', '雅克', '波兰文艺青年,声线磁性'),
58
- v('Griet', '海娜', '荷兰成熟文艺女性'),
59
- v('Marina', '玛丽娜', '多元文化城市长大的女孩'),
60
- v('Siiri', '西芮', '内敛温柔,语速舒缓'),
61
- v('Ingrid', '林恩', '挪威乡村姑娘'),
62
- v('Sigga', '海娜', '冰岛小镇知性女青年'),
63
- v('Bea', '雅娜', '爱喝咖啡的甜美姐姐'),
64
- v('Chloe', '思怡', '马来西亚白领女生'),
65
- v('Emilien', '埃米尔安', '浪漫的法国大哥哥'),
66
- v('longanlingxin', '龙安灵心', '知心温暖音'),
11
+ v('Tina', '甜甜', '女', '甜美温暖,解决问题果断(缺省)'),
12
+ v('Cindy', '林欣宜', '女', '台湾腔嗲嗲的小姐姐', '台湾口音'),
13
+ v('Liora Mira', '清欢', '女', '温柔的烟火人间感'),
14
+ v('Raymond', '林川野', '男', '声音清亮,爱吃外卖的宅男'),
15
+ v('Zane', '泽恩', '男', '磁性迷人'),
16
+ v('Katerina', '卡捷琳娜', '女', '御姐音,韵律感强'),
17
+ v('Ryan', '甜茶', '男', '节奏感强,戏感足'),
18
+ v('Mia', '舒然', '女', '细腻,生活美学风格的温柔博主;中英夹着说时中文带英文腔'),
19
+ v('Cici', '绵绵', '女', '邻家妹妹,活泼软糯'),
20
+ v('Theo Calm', '予安', '男', '沉静,治愈感'),
21
+ v('Serena', '苏瑶', '女', '温柔小姐姐'),
22
+ v('Maia', '四月', '女', '知性与温柔'),
23
+ v('Evan', '江晨', '男', '男大学生,年下奶狗'),
24
+ v('Qiao', '小乔妹', '女', '表面甜妹,个性十足', '台湾口音'),
25
+ v('Momo', '茉兔', '女', '撒娇搞怪'),
26
+ v('Wil', '伟伦', '男', '深圳长大的港台腔小哥哥'),
27
+ v('Angel', '安琪', '女', '带台式口音,甜美'),
28
+ v('Li Cassian', '李公公', '男', '话中有留白,善察言观色'),
29
+ v('Joyner', '阿逗', '男', '搞笑、夸张、接地气'),
30
+ v('Gold', '金爷', '男', '西海岸黑人说唱歌手风格'),
31
+ v('Jennifer', '詹妮弗', '女', '电影质感的美语女声;中英夹着说时中文带英文腔(苹果念成 pingguo)'),
32
+ v('Aiden', '艾登', '男', '精通厨艺的美语大男孩'),
33
+ v('Mione', '敏儿', '女', '成熟知性的英国邻家妹妹'),
34
+ v('Sunny', '晴儿', '女', '甜美的川妹子', '四川话'),
35
+ v('Dylan', '晓东', '男', '北京胡同长大的少年', '北京话'),
36
+ v('Eric', '程川', '男', '成都市井男子,跳脱', '四川话'),
37
+ v('Peter', '李彼得', '男', '天津相声,专业捧哏', '天津话'),
38
+ v('Joseph Chen', '阿樸伯', '男', '南洋老华侨', '闽南话'),
39
+ v('Marcus', '秦川', '男', '老陕风格,面宽话短', '陕西话'),
40
+ v('Li', '老李', '男', '南京话伯伯,爱骂骂咧咧', '南京话'),
41
+ v('Rocky', '阿强', '男', '幽默风趣,在线陪聊', '粤语'),
42
+ v('Kiki', '阿清', '女', '甜美的港妹闺蜜', '粤语'),
43
+ v('Sohee', '素熙', '女', '温柔开朗的韩国欧尼'),
44
+ v('Eliška', '艾莉卡', '女', '中欧风格'),
45
+ v('Alek', '阿列克', '男', '战斗民族的冷与暖'),
46
+ v('Arda', '阿尔达', '男', '干净利落,温润'),
47
+ v('Dolce', '多尔切', '男', '慵懒的意大利大叔'),
48
+ v('Lenn', '莱恩', '男', '德国青年,理性中带叛逆'),
49
+ v('Ono Anna', '小野杏', '女', '鬼灵精怪的青梅竹马'),
50
+ v('Sonrisa', '索尼莎', '女', '热情开朗的拉美大姐'),
51
+ v('Bodega', '博德加', '男', '热情的西班牙大叔'),
52
+ v('Andre', '安德雷', '男', '磁性、沉稳的男生'),
53
+ v('Radio Gol', '拉迪奥·戈尔', '男', '足球解说风格'),
54
+ v('Rizky', '阿力', '男', '印尼青年,声线个性'),
55
+ v('Roya', '萝雅', '女', '热爱运动的女孩'),
56
+ v('Hana', '阿幸', '女', '越南成熟姐姐'),
57
+ v('Jakub', '雅克', '男', '波兰文艺青年,声线磁性'),
58
+ v('Griet', '海娜', '女', '荷兰成熟文艺女性'),
59
+ v('Marina', '玛丽娜', '女', '多元文化城市长大的女孩'),
60
+ v('Siiri', '西芮', '女', '内敛温柔,语速舒缓'),
61
+ v('Ingrid', '林恩', '女', '挪威乡村姑娘'),
62
+ v('Sigga', '海娜', '女', '冰岛小镇知性女青年'),
63
+ v('Bea', '雅娜', '女', '爱喝咖啡的甜美姐姐'),
64
+ v('Chloe', '思怡', '女', '马来西亚白领女生'),
65
+ v('Emilien', '埃米尔安', '男', '浪漫的法国大哥哥'),
66
+ v('longanlingxin', '龙安灵心', '女', '知心温暖音'),
67
67
  ];
68
68
  /** 这个模型认不认这个音色(工作台、doctor 拦写错的) */
69
69
  export function isRealtimeVoice(voice) {
@@ -12,6 +12,18 @@ export declare const EXAMPLES_SUFFIX = ".\u4F8B\u5B50";
12
12
  export declare function listActivities(agentsDir: string, tutor?: string, opts?: {
13
13
  examples?: boolean;
14
14
  }): Promise<ActivityRef[]>;
15
- /** workspace 里那份的位置 */
15
+ /** workspace 里那份的位置(就是这个名字,不认改过的名;装、升级、写都用它) */
16
16
  export declare function activityFile(root: string, tutor: string, name: string): string;
17
+ /**
18
+ * 改过名的出厂 activity:旧名 → 新名。2026-11-03 批改 → 检查作业(孩子端方砖上的字)。
19
+ * upgrade 把 workspace 里的旧文件挪成新名(src/app/cli/activities.ts);旧草稿里的 `activity 批改`、旧话题记的名字照样认(activityName)
20
+ */
21
+ export declare const ACTIVITY_RENAMES: Readonly<Record<string, string>>;
22
+ /**
23
+ * 这个名字在这位老师家里实际是哪个文件:有同名的就是它;旧名的文件挪走了认新名;新名的还没有(没 upgrade 过)认旧名。都没有 → 原样
24
+ * (读的时候用:跑话题、首页检查、方砖;lint 的 fileName 也要用这个,不然 name 对不上)
25
+ */
26
+ export declare function activityName(root: string, tutor: string, name: string): string;
27
+ /** 读的时候找的那份:认改过的名(activityName) */
28
+ export declare function findActivityFile(root: string, tutor: string, name: string): string;
17
29
  export declare function activityRunFor(root: string, tutor: string, name: string, raw?: string): Promise<ActivityRun>;
@@ -2,6 +2,7 @@
2
2
  * 读 activity 文件(从 app/cli/activities.ts 拆出来,2026-10-29,行为不变):在哪、每位老师有哪几个、按「名=值」的参数拼成一次跑。
3
3
  * 运行层每轮都要读,所以不放在命令行那一层;装、升级、还原、检查仍在 app/cli/activities.ts。
4
4
  */
5
+ import { existsSync } from 'node:fs';
5
6
  import { readFile, readdir } from 'node:fs/promises';
6
7
  import { join } from 'node:path';
7
8
  import { parseActivity, resolveParams } from "./activity.js";
@@ -22,12 +23,37 @@ export async function listActivities(agentsDir, tutor, opts = {}) {
22
23
  }
23
24
  return out;
24
25
  }
25
- /** workspace 里那份的位置 */
26
+ /** workspace 里那份的位置(就是这个名字,不认改过的名;装、升级、写都用它) */
26
27
  export function activityFile(root, tutor, name) {
27
28
  return join(root, 'agents', tutor, ACTIVITIES_DIR, `${name}.md`);
28
29
  }
30
+ /**
31
+ * 改过名的出厂 activity:旧名 → 新名。2026-11-03 批改 → 检查作业(孩子端方砖上的字)。
32
+ * upgrade 把 workspace 里的旧文件挪成新名(src/app/cli/activities.ts);旧草稿里的 `activity 批改`、旧话题记的名字照样认(activityName)
33
+ */
34
+ export const ACTIVITY_RENAMES = { 批改: '检查作业' };
35
+ /**
36
+ * 这个名字在这位老师家里实际是哪个文件:有同名的就是它;旧名的文件挪走了认新名;新名的还没有(没 upgrade 过)认旧名。都没有 → 原样
37
+ * (读的时候用:跑话题、首页检查、方砖;lint 的 fileName 也要用这个,不然 name 对不上)
38
+ */
39
+ export function activityName(root, tutor, name) {
40
+ const has = (n) => existsSync(activityFile(root, tutor, n));
41
+ if (has(name))
42
+ return name;
43
+ const to = ACTIVITY_RENAMES[name];
44
+ if (to && has(to))
45
+ return to;
46
+ const from = Object.keys(ACTIVITY_RENAMES).find((k) => ACTIVITY_RENAMES[k] === name);
47
+ if (from && has(from))
48
+ return from;
49
+ return name;
50
+ }
51
+ /** 读的时候找的那份:认改过的名(activityName) */
52
+ export function findActivityFile(root, tutor, name) {
53
+ return activityFile(root, tutor, activityName(root, tutor, name));
54
+ }
29
55
  export async function activityRunFor(root, tutor, name, raw = '') {
30
- const file = activityFile(root, tutor, name);
56
+ const file = findActivityFile(root, tutor, name);
31
57
  const text = await readFile(file, 'utf8').catch(() => null);
32
58
  if (text === null)
33
59
  throw new UsageError(`${file} 不在;cotutor activity check ${tutor} 看这位老师有哪些`);
@@ -3,7 +3,7 @@
3
3
  * 数这个话题画了几张、今天还剩几张、上一张为什么没画。纯判断在 src/runtime/activity.ts(gatePasses / failingGate),这里只管读盘。
4
4
  */
5
5
  import { readFile } from 'node:fs/promises';
6
- import { activityFile } from "./activities.js";
6
+ import { findActivityFile } from "./activities.js";
7
7
  import { failingGate, parseActivity } from "./activity.js";
8
8
  import { threads } from "../schema/index.js";
9
9
  import {} from "../schema/index.js";
@@ -15,7 +15,7 @@ export function threadActivity(messages, thread) {
15
15
  }
16
16
  /** 读 workspace 里那份 activity 文件;不在 → null */
17
17
  export async function readActivity(ws, tutor, name) {
18
- const text = await readFile(activityFile(ws.root, tutor, name), 'utf8').catch(() => null);
18
+ const text = await readFile(findActivityFile(ws.root, tutor, name), 'utf8').catch(() => null);
19
19
  return text === null ? null : parseActivity(text).activity;
20
20
  }
21
21
  /** 这个话题里的画图卡,文件顺序:哪一轮第几张 */
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * 老师文件(agents/<name>.md):YAML frontmatter + 正文即系统提示。
3
- * 这里只认平铺的 `key: value` frontmatter(两个 CLI 的公共子集:name / description / maxTurns / permissionMode / memory);
4
- * 正文给没有 --agent 的 CLI 经 {agentBody} 塞进 --append-system-prompt。
3
+ * 这里只认平铺的 `key: value` frontmatter(name / description / maxTurns / permissionMode / memory,当初两个 CLI 的公共子集,应用只读 name);
4
+ * 正文是系统提示的前半(后面接板书写法)。
5
5
  */
6
- /** 两个 CLI 的公共子集:老师文件 frontmatter 只写这几个键(人设 / 政策 / 开关在 cotutor.json);技能 cotutor-tune 的字段表从这里取 */
6
+ /** 老师文件 frontmatter 只写这几个键(人设 / 政策 / 开关在 cotutor.json);技能 cotutor-tune 的字段表从这里取 */
7
7
  export declare const AGENT_FRONTMATTER_KEYS: readonly ["name", "description", "maxTurns", "permissionMode", "memory"];
8
8
  export interface AgentFile {
9
9
  frontmatter: Record<string, string>;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * 老师文件(agents/<name>.md):YAML frontmatter + 正文即系统提示。
3
- * 这里只认平铺的 `key: value` frontmatter(两个 CLI 的公共子集:name / description / maxTurns / permissionMode / memory);
4
- * 正文给没有 --agent 的 CLI 经 {agentBody} 塞进 --append-system-prompt。
3
+ * 这里只认平铺的 `key: value` frontmatter(name / description / maxTurns / permissionMode / memory,当初两个 CLI 的公共子集,应用只读 name);
4
+ * 正文是系统提示的前半(后面接板书写法)。
5
5
  */
6
- /** 两个 CLI 的公共子集:老师文件 frontmatter 只写这几个键(人设 / 政策 / 开关在 cotutor.json);技能 cotutor-tune 的字段表从这里取 */
6
+ /** 老师文件 frontmatter 只写这几个键(人设 / 政策 / 开关在 cotutor.json);技能 cotutor-tune 的字段表从这里取 */
7
7
  export const AGENT_FRONTMATTER_KEYS = ['name', 'description', 'maxTurns', 'permissionMode', 'memory'];
8
8
  export function parseAgentFile(text) {
9
9
  const open = /^---[^\S\n]*\n/.exec(text);
@@ -15,8 +15,8 @@ export declare function boardGuideBody(skillMd: string): string;
15
15
  /** 上下文包 boardGuide: 行在预载时写的话(事实陈述,不让老师自查) */
16
16
  export declare const BOARD_GUIDE_IN_SYSTEM = "\u5DF2\u5728\u4F60\u7684\u7CFB\u7EDF\u63D0\u793A\u91CC(\u300C# \u677F\u4E66\u600E\u4E48\u5199\u300D),\u4E0D\u7528\u8BFB";
17
17
  /**
18
- * 回归检查(与 CLI 无关):板书写法已经递到手里,老师这轮还用工具去读它 → 预载没起作用(换模型 / 换 CLI / 升版本后最先坏的地方)。
19
- * 认三种:技能工具(claude 的 Skill、qwen 的 skill,按 src/brain/cli/ 的工具类别认)点名 cotutor-board;任何工具的参数里带 cotutor-board/SKILL.md(Read、cat、别家 CLI 的读文件工具),或按老师裁的那份 .cotutor/board/。
18
+ * 回归检查:板书写法已经在系统提示里,老师这轮还用工具去读它 → 系统提示里那份没起作用(换模型后最先坏的地方)。
19
+ * 认三种:技能工具(旧轮次 claude 的 Skill、qwen 的 skill,按工具类别认)点名 cotutor-board;任何工具的参数里带 cotutor-board/SKILL.md(read_file 等),或旧轮次按老师裁的那份 .cotutor/board/。
20
20
  * references/<种类>.md 是按需读的,不算。
21
21
  */
22
22
  export declare function boardGuideReads(tools: readonly {
@@ -7,7 +7,7 @@ import { parseAgentFile } from "../agent-file.js";
7
7
  import { stripHumanNotes } from "../../shared/human-notes.js";
8
8
  import { applyCardGuards } from "../../board/card-guards.js";
9
9
  import { BOARD_CARD_KINDS } from "../../schema/config.js";
10
- import { toolKind } from "../../brain/cli/adapters.js";
10
+ import { toolKind } from "../../brain/index.js";
11
11
  import { SKILLS_DIR } from "../../schema/workspace.js";
12
12
  export const TUTOR_SKILL = 'cotutor-tutor';
13
13
  /** 相对 workspace 根 */
@@ -38,8 +38,8 @@ export function boardGuideBody(skillMd) {
38
38
  /** 上下文包 boardGuide: 行在预载时写的话(事实陈述,不让老师自查) */
39
39
  export const BOARD_GUIDE_IN_SYSTEM = '已在你的系统提示里(「# 板书怎么写」),不用读';
40
40
  /**
41
- * 回归检查(与 CLI 无关):板书写法已经递到手里,老师这轮还用工具去读它 → 预载没起作用(换模型 / 换 CLI / 升版本后最先坏的地方)。
42
- * 认三种:技能工具(claude 的 Skill、qwen 的 skill,按 src/brain/cli/ 的工具类别认)点名 cotutor-board;任何工具的参数里带 cotutor-board/SKILL.md(Read、cat、别家 CLI 的读文件工具),或按老师裁的那份 .cotutor/board/。
41
+ * 回归检查:板书写法已经在系统提示里,老师这轮还用工具去读它 → 系统提示里那份没起作用(换模型后最先坏的地方)。
42
+ * 认三种:技能工具(旧轮次 claude 的 Skill、qwen 的 skill,按工具类别认)点名 cotutor-board;任何工具的参数里带 cotutor-board/SKILL.md(read_file 等),或旧轮次按老师裁的那份 .cotutor/board/。
43
43
  * references/<种类>.md 是按需读的,不算。
44
44
  */
45
45
  export function boardGuideReads(tools) {
@@ -3,7 +3,7 @@
3
3
  * 解析走板书同一个解析器(place = home);永不抛错,问题都带行号(1 起)与修法。要读盘的引用(话题在不在)由调用方查好传进来。
4
4
  */
5
5
  import type { TutorButton } from '../../board/cards/index.ts';
6
- import { type Activity } from '../../schema/index.ts';
6
+ import { type Activity, type HomeTileId } from '../../schema/index.ts';
7
7
  import type { BoardCard } from '../../board/presentation.ts';
8
8
  export type HomeIssueLevel = 'fix' | 'note';
9
9
  export interface HomeIssue {
@@ -130,6 +130,17 @@ export type KidHomeButton = {
130
130
  video?: true;
131
131
  resume?: Resume;
132
132
  };
133
+ /** 孩子端老师卡上常驻的一块方砖(HOME_TILES):label = activity 的名字;resume = 今天从这个 activity 进去的话题(和 activity 按钮同一个入口) */
134
+ export interface KidHomeTile {
135
+ id: HomeTileId;
136
+ label: string;
137
+ resume?: Resume;
138
+ }
139
+ /** 这位老师牌上的方砖:开着的(on,牌上的先后)里,家里那个 activity 用得了的才出 */
140
+ export declare function kidTiles(on: readonly HomeTileId[], opts: {
141
+ usable: (activity: string) => boolean;
142
+ resumes?: ReadonlyMap<string, Resume>;
143
+ }): KidHomeTile[];
133
144
  export declare const NEW_THREAD_LABEL = "\u65B0\u8BDD\u9898";
134
145
  /** 一个话题是从哪个入口进去的:小课堂(按课包)、activity(按名字,参数不比)、自由话题(新话题按钮或没有按钮);别的按钮(开场、接着)进去的 → null */
135
146
  export declare function resumeKey(first: {
@@ -1,4 +1,4 @@
1
- import { LECTURE_ACTIVITY } from "../../schema/index.js";
1
+ import { HOME_TILES, LECTURE_ACTIVITY } from "../../schema/index.js";
2
2
  import { resolveParams } from "../activity.js";
3
3
  import { parseBoard } from "../../board/parse.js";
4
4
  const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -227,6 +227,13 @@ export function homeOwners(cards, tutors) {
227
227
  return who ?? cur;
228
228
  });
229
229
  }
230
+ /** 这位老师牌上的方砖:开着的(on,牌上的先后)里,家里那个 activity 用得了的才出 */
231
+ export function kidTiles(on, opts) {
232
+ return HOME_TILES.filter((t) => on.includes(t.id) && opts.usable(t.activity)).map((t) => {
233
+ const r = opts.resumes?.get(`activity:${t.activity}`);
234
+ return { id: t.id, label: t.activity, ...(r ? { resume: r } : {}) };
235
+ });
236
+ }
230
237
  export const NEW_THREAD_LABEL = '新话题';
231
238
  const RESUME_MAX = 10;
232
239
  /** 一个话题是从哪个入口进去的:小课堂(按课包)、activity(按名字,参数不比)、自由话题(新话题按钮或没有按钮);别的按钮(开场、接着)进去的 → null */
@@ -116,7 +116,7 @@ export declare function messageVia(via: HomeVia, r: ResolvedVia): MessageVia;
116
116
  export declare function continueContext(ws: Workspace, tutor: string, date: string, thread: string): Promise<NonNullable<ContextPack['continue']> | null>;
117
117
  export interface HomeClick {
118
118
  tutor: string;
119
- button: number | 'new' | 'recent';
119
+ button: HomeVia['button'];
120
120
  label: string;
121
121
  uses: {
122
122
  date: string;
@@ -10,10 +10,10 @@ import { UsageError } from "../../shared/errors.js";
10
10
  import { localDate } from "../../shared/time.js";
11
11
  import { conversationFiles, threads } from "../../schema/index.js";
12
12
  import { kidQuestions } from "../../memory/diary.js";
13
- import { activityFile } from "../activities.js";
13
+ import { activityName, findActivityFile } from "../activities.js";
14
14
  import { lintActivity, parseActivity, resolveParams } from "../activity.js";
15
- import { arrangeHome, dropFixes, homeActivities, homeOwners, faceTutor, homeId, homeIssues, homeLectures, homeRefs, homeTutors, kidButtons, parseHome, threadKey, todayResumes } from "./home.js";
16
- import { HOME_ID_RE, LECTURE_ACTIVITY, PublishedHomeSchema } from "../../schema/index.js";
15
+ import { arrangeHome, dropFixes, homeActivities, homeOwners, faceTutor, homeId, homeIssues, homeLectures, homeRefs, homeTutors, kidButtons, kidTiles, parseHome, threadKey, todayResumes } from "./home.js";
16
+ import { HOME_ID_RE, HOME_TILES, LECTURE_ACTIVITY, PublishedHomeSchema, tutorTiles } from "../../schema/index.js";
17
17
  import { listDates, readCardStates, readIndex } from "../journal/store.js";
18
18
  import { bakeSoon, inspectLecture, readLecture } from "../lecture.js";
19
19
  export function homeFiles(ws) {
@@ -78,10 +78,10 @@ async function checkContext(ws, doc, now) {
78
78
  }
79
79
  const activities = new Map();
80
80
  for (const r of homeActivities(doc)) {
81
- const text = await readFile(activityFile(ws.root, r.tutor, r.name), 'utf8').catch(() => null);
81
+ const text = await readFile(findActivityFile(ws.root, r.tutor, r.name), 'utf8').catch(() => null);
82
82
  if (text === null)
83
83
  continue;
84
- const { activity, issues } = lintActivity(text, { fileName: r.name, tutorCards: ws.config.tutors[r.tutor]?.cards });
84
+ const { activity, issues } = lintActivity(text, { fileName: activityName(ws.root, r.tutor, r.name), tutorCards: ws.config.tutors[r.tutor]?.cards });
85
85
  activities.set(`${r.tutor}/${r.name}`, { activity, fixes: issues.filter((i) => i.level === 'fix').length });
86
86
  }
87
87
  return { tutors: tutorInfos(ws), threads: await liveThreads(ws, homeRefs(doc)), lectures, lectureProblems, unbaked, today: localDate(now), activities };
@@ -93,10 +93,10 @@ export async function checkHome(ws, md, now) {
93
93
  }
94
94
  /** 孩子端能不能用这个 activity:文件在、检查没有「要改」(提醒不算);用不了 → null */
95
95
  export async function usableActivity(ws, tutor, name) {
96
- const text = await readFile(activityFile(ws.root, tutor, name), 'utf8').catch(() => null);
96
+ const text = await readFile(findActivityFile(ws.root, tutor, name), 'utf8').catch(() => null);
97
97
  if (text === null)
98
98
  return null;
99
- const { activity, issues } = lintActivity(text, { fileName: name, tutorCards: ws.config.tutors[tutor]?.cards });
99
+ const { activity, issues } = lintActivity(text, { fileName: activityName(ws.root, tutor, name), tutorCards: ws.config.tutors[tutor]?.cards });
100
100
  return issues.some((i) => i.level === 'fix') ? null : activity;
101
101
  }
102
102
  /** 已发布那份里现在还坏着的引用(doctor 与家长端用) */
@@ -112,7 +112,7 @@ export async function publishHome(ws, opts) {
112
112
  const f = homeFiles(ws);
113
113
  const src = opts.from ? (isAbsolute(opts.from) ? opts.from : resolve(ws.root, opts.from)) : f.draft;
114
114
  const md = await readFile(src, 'utf8').catch(() => null);
115
- const shown = relative(ws.root, src) || src;
115
+ const shown = relative(ws.root, src).split(sep).join('/') || src;
116
116
  if (md === null)
117
117
  throw new UsageError(opts.from ? `读不到 ${opts.from}` : '还没有草稿 home/draft.md(用 cotutor-home 技能写一份)');
118
118
  return publishHomeMd(ws, md, shown, opts);
@@ -233,8 +233,9 @@ export async function kidHomeView(ws, now, opts = {}) {
233
233
  continue;
234
234
  }
235
235
  const name = String(c.props.tutor);
236
+ const resumes = await resumesOf(ws, name, today);
236
237
  const buttons = kidButtons((c.props.buttons ?? []), {
237
- resumes: await resumesOf(ws, name, today),
238
+ resumes,
238
239
  alive: (date, thread) => alive.has(threadKey(name, date, thread)),
239
240
  lecture: (bundle) => Boolean(lectures.get(bundle)),
240
241
  activity: (a, params) => { const act = usable.get(`${name}/${a}`); return Boolean(act) && resolveParams(act, params).issues.length === 0; },
@@ -244,7 +245,12 @@ export async function kidHomeView(ws, now, opts = {}) {
244
245
  // 橙点:这份首页给他排了按钮或卡,而发布以来孩子还没找过他
245
246
  const arranged = buttons.some((b) => typeof b.id === 'number') || cards.some((x) => x.kind !== 'tutor' && owners.get(x) === name);
246
247
  const news = Boolean(publishedAt && arranged && !(await talkedSince(ws, name, publishedAt, now)));
247
- out.push({ kind: 'tutor', props: { tutor: name, buttons, ...(news ? { news: true } : {}) } });
248
+ // 常驻的方砖(预习、检查作业、复习):不经发布,家里有那个 activity、没关掉就在
249
+ const tiles = [];
250
+ for (const t of HOME_TILES)
251
+ if (tutorTiles(ws.config.tutors[name] ?? {}).includes(t.id) && (await usableActivity(ws, name, t.activity)))
252
+ tiles.push(t.id);
253
+ out.push({ kind: 'tutor', props: { tutor: name, buttons, tiles: kidTiles(tiles, { usable: () => true, resumes }), ...(news ? { news: true } : {}) } });
248
254
  }
249
255
  return { home: id, cards: out };
250
256
  }
@@ -259,7 +265,23 @@ export async function resolveVia(ws, tutor, via) {
259
265
  return { kind: 'new', label: '新话题' };
260
266
  if (via.button === 'recent')
261
267
  return { kind: 'recent', label: '接着刚才的' };
262
- if (via.home === null)
268
+ // activity 的参数在点的那一刻按 activity 文件解析、补缺省,跟着话题走(家长后来改了只管以后的话题);文件不在、参数不对 → null
269
+ const runOf = async (name, params) => {
270
+ const text = await readFile(findActivityFile(ws.root, tutor, name), 'utf8').catch(() => null);
271
+ if (text === null)
272
+ return null;
273
+ const r = resolveParams(parseActivity(text).activity, params);
274
+ return r.issues.length ? null : { name, params: r.values };
275
+ };
276
+ // 方砖:不看是哪份首页(缺省首页也有);关掉了、家里那个 activity 用不了 → null。参数全用缺省(三份都可空,老师先请孩子拍材料)
277
+ const tile = HOME_TILES.find((t) => t.id === via.button);
278
+ if (tile) {
279
+ if (!tutorTiles(ws.config.tutors[tutor] ?? {}).includes(tile.id) || !(await usableActivity(ws, tutor, tile.activity)))
280
+ return null;
281
+ const run = await runOf(tile.activity, {});
282
+ return run ? { kind: 'activity', label: tile.activity, activity: tile.activity, params: run.params } : null;
283
+ }
284
+ if (via.home === null || typeof via.button !== 'number')
263
285
  return null;
264
286
  const { home } = await readPublished(ws);
265
287
  if (!home || home.id !== via.home)
@@ -271,14 +293,6 @@ export async function resolveVia(ws, tutor, via) {
271
293
  const brief = b.brief ? { brief: b.brief } : {};
272
294
  if (b.kind === 'start')
273
295
  return { kind: 'start', label: b.label, ...brief };
274
- // activity 的参数在点的那一刻按 activity 文件解析、补缺省,跟着话题走(家长后来改了只管以后的话题);文件不在、参数不对 → null
275
- const runOf = async (name, params) => {
276
- const text = await readFile(activityFile(ws.root, tutor, name), 'utf8').catch(() => null);
277
- if (text === null)
278
- return null;
279
- const r = resolveParams(parseActivity(text).activity, params);
280
- return r.issues.length ? null : { name, params: r.values };
281
- };
282
296
  // 小课堂看完照哪个 activity 聊:写了 activity: 是它,没写是这位老师的「小课堂」;文件不在 → null
283
297
  if (b.kind === 'lecture') {
284
298
  const run = await runOf(b.activity ?? LECTURE_ACTIVITY, {});
@@ -393,7 +407,8 @@ export function formatCheck(ws, check, title, kid) {
393
407
  const ref = b.kind === 'continue' ? ` → ${b.date} ${b.thread}` : b.kind === 'lecture' ? ` → 小课堂 ${b.bundle}` : act?.kind === 'activity' ? ` → activity ${act.activity}${Object.keys(act.params).length ? `(${Object.entries(act.params).map(([k, v]) => `${k}: ${v}`).join(';')})` : ''}` : '';
394
408
  return `${BUTTON_ICON[b.kind]} ${b.label}${ref}${brief}`;
395
409
  });
396
- out.push(` ${display(name)} ${name}${written.has(name) ? '' : '(没写,应用补)'}:${buttons.join(' | ')}`);
410
+ const tiles = (c.props.tiles ?? []).map((x) => `▦ ${x.label}`);
411
+ out.push(` ${display(name)} ${name}${written.has(name) ? '' : '(没写,应用补)'}:${[...buttons.slice(0, 1), ...tiles, ...buttons.slice(1)].join(' | ')}${tiles.length ? '(▦ 是常驻的方砖,不用写)' : ''}`);
397
412
  }
398
413
  const rest = kid.cards.filter((x) => x.kind !== 'tutor');
399
414
  if (rest.length) {
@@ -424,7 +439,7 @@ export function formatStats(ws, s) {
424
439
  const h = s.home;
425
440
  const tutorsN = h.cards.filter((c) => c.kind === 'tutor').length;
426
441
  const out = [`已发布 ${h.id}(${s.days === 0 ? '今天' : `${s.days} 天前`}${h.for ? `,for ${h.for}` : ''})· ${tutorsN} 张老师卡 · ${h.cards.length - tutorsN} 张别的卡 · 原文 ${h.source}`];
427
- const icon = (b) => (b === 'new' ? '✨' : b === 'recent' ? '↻' : '·');
442
+ const icon = (b) => (b === 'new' ? '✨' : b === 'recent' ? '↻' : typeof b === 'string' ? '▦' : '·');
428
443
  for (const c of s.clicks) {
429
444
  const who = ws.config.tutors[c.tutor]?.display ?? c.tutor;
430
445
  out.push(` ${who} ${icon(c.button)} ${c.label}:${c.uses.length ? `点了 ${c.uses.length} 次(${c.uses.map((u) => `${u.date} ${u.thread}`).join('、')})` : '没点过'}`);
@@ -20,14 +20,23 @@ export declare function playingOf(segs: {
20
20
  start: number;
21
21
  end: number;
22
22
  }[]): [number, number][];
23
- /** 两边混成一条(PCM16 单声道 24k,长到 ms 为止;挂断那一刻还没放完的老师的声音不要) */
23
+ /** 混出来的一条 + 老师出声的时段 + 孩子有声的时段 */
24
+ export interface Mixed {
25
+ pcm: Buffer;
26
+ playing: [number, number][];
27
+ kidVoice: [number, number][];
28
+ }
29
+ /** 一口气混完(测试、短的) */
24
30
  export declare function mixCall(o: {
25
31
  kid: Chunk[];
26
32
  tutor: Chunk[];
27
33
  flushes: number[];
28
34
  ms: number;
29
- }): {
30
- pcm: Buffer;
31
- playing: [number, number][];
32
- kidVoice: [number, number][];
33
- };
35
+ }): Mixed;
36
+ /** 分片混:结果同 mixCall,中间让出事件循环(通话里落录像走这个) */
37
+ export declare function mixCallSliced(o: {
38
+ kid: Chunk[];
39
+ tutor: Chunk[];
40
+ flushes: number[];
41
+ ms: number;
42
+ }): Promise<Mixed>;
@@ -53,36 +53,77 @@ export function playingOf(segs) {
53
53
  }
54
54
  return out.map(([a, b]) => [Math.round(a), Math.round(b)]);
55
55
  }
56
- /** 两边混成一条(PCM16 单声道 24k,长到 ms 为止;挂断那一刻还没放完的老师的声音不要) */
57
- export function mixCall(o) {
56
+ /** 分片混:每混这么多个采样(10 秒)让一下事件循环——通话中切段时就在混,不能卡住正在说的那一路 */
57
+ const SLICE_SAMPLES = MIX_RATE * 10;
58
+ /**
59
+ * 两边混成一条(PCM16 单声道 24k,长到 ms 为止;挂断那一刻还没放完的老师的声音不要)。
60
+ * 直接往 Int16 里加、加一次削一次顶(两边叠着才会削,和先加完再削只差一个最低位);不另开整段的 Float32(一个钟头就是 345 MB)。
61
+ * 生成器:每混 SLICE_SAMPLES 个采样 yield 一次,mixCall 一口气跑完,mixCallSliced 每次 yield 让一下事件循环
62
+ */
63
+ function* mixing(o) {
58
64
  const n = Math.max(0, Math.round((o.ms * MIX_RATE) / 1000));
59
- const acc = new Float32Array(n);
60
- const put = (at, i16, len) => {
61
- const base = Math.round((at * MIX_RATE) / 1000);
62
- for (let k = 0; k < len; k++) {
63
- const j = base + k;
64
- if (j >= 0 && j < n)
65
- acc[j] += i16(k);
66
- }
65
+ const pcm = Buffer.alloc(n * 2);
66
+ let work = 0;
67
+ const add = (j, v) => {
68
+ const s = pcm.readInt16LE(j * 2) + Math.round(v);
69
+ pcm.writeInt16LE(s > 32767 ? 32767 : s < -32768 ? -32768 : s, j * 2);
67
70
  };
71
+ const voiced = [];
68
72
  for (const c of o.kid) {
69
73
  const m = samplesOf(c.pcm);
70
74
  if (!m)
71
75
  continue;
72
76
  const len = Math.floor((m * MIX_RATE) / KID_RATE);
73
77
  const at = c.t - (m * 1000) / KID_RATE;
74
- put(at, (k) => {
78
+ const base = Math.round((at * MIX_RATE) / 1000);
79
+ for (let k = 0; k < len; k++) {
80
+ const j = base + k;
81
+ if (j < 0 || j >= n)
82
+ continue;
75
83
  const x = (k * KID_RATE) / MIX_RATE, i = Math.floor(x), f = x - i;
76
84
  const a = c.pcm.readInt16LE(Math.min(i, m - 1) * 2), b = c.pcm.readInt16LE(Math.min(i + 1, m - 1) * 2);
77
- return a + (b - a) * f;
78
- }, len);
85
+ add(j, a + (b - a) * f);
86
+ }
87
+ if (rms(c.pcm) >= KID_VOICE)
88
+ voiced.push({ start: at, end: c.t });
89
+ work += len;
90
+ if (work >= SLICE_SAMPLES) {
91
+ work = 0;
92
+ yield;
93
+ }
79
94
  }
80
95
  const segs = scheduleTutor(o.tutor, o.flushes).filter((s) => s.start < o.ms);
81
- for (const s of segs)
82
- put(s.start, (k) => s.pcm.readInt16LE(k * 2), samplesOf(s.pcm));
83
- const pcm = Buffer.alloc(n * 2);
84
- for (let j = 0; j < n; j++)
85
- pcm.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(acc[j]))), j * 2);
86
- const voiced = o.kid.filter((c) => rms(c.pcm) >= KID_VOICE).map((c) => ({ start: c.t - (samplesOf(c.pcm) * 1000) / KID_RATE, end: c.t }));
96
+ for (const s of segs) {
97
+ const base = Math.round((s.start * MIX_RATE) / 1000);
98
+ const len = samplesOf(s.pcm);
99
+ for (let k = 0; k < len; k++) {
100
+ const j = base + k;
101
+ if (j >= 0 && j < n)
102
+ add(j, s.pcm.readInt16LE(k * 2));
103
+ }
104
+ work += len;
105
+ if (work >= SLICE_SAMPLES) {
106
+ work = 0;
107
+ yield;
108
+ }
109
+ }
87
110
  return { pcm, playing: playingOf(segs.map((s) => ({ start: s.start, end: Math.min(s.end, o.ms) }))), kidVoice: playingOf(voiced) };
88
111
  }
112
+ /** 一口气混完(测试、短的) */
113
+ export function mixCall(o) {
114
+ const g = mixing(o);
115
+ let r = g.next();
116
+ while (!r.done)
117
+ r = g.next();
118
+ return r.value;
119
+ }
120
+ /** 分片混:结果同 mixCall,中间让出事件循环(通话里落录像走这个) */
121
+ export async function mixCallSliced(o) {
122
+ const g = mixing(o);
123
+ let r = g.next();
124
+ while (!r.done) {
125
+ await new Promise((resolve) => setImmediate(resolve));
126
+ r = g.next();
127
+ }
128
+ return r.value;
129
+ }