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.
- package/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +36 -13
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +13 -3
- package/cards/photo/card.css +10 -3
- package/cards/photo/card.md +2 -2
- package/cards/tutor/card.css +13 -0
- package/cards/tutor/card.md +2 -1
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
- package/dist/app/activity-eval.js +3 -2
- package/dist/app/cli/activities.d.ts +8 -1
- package/dist/app/cli/activities.js +42 -4
- package/dist/app/cli/cert.d.ts +5 -0
- package/dist/app/cli/cert.js +20 -7
- package/dist/app/cli/chalk/browser.d.ts +2 -2
- package/dist/app/cli/chalk/browser.js +22 -7
- package/dist/app/cli/chalk/main.js +3 -2
- package/dist/app/cli/doctor.d.ts +6 -5
- package/dist/app/cli/doctor.js +107 -171
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/init.js +8 -5
- package/dist/app/cli/main.js +347 -24
- package/dist/app/cli/migrate.d.ts +2 -11
- package/dist/app/cli/migrate.js +1 -59
- 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 +18 -4
- package/dist/app/cli/serve.js +78 -8
- 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/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +13 -13
- package/dist/app/cli/skills.d.ts +5 -1
- package/dist/app/cli/skills.js +14 -4
- 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/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +11 -6
- package/dist/app/dictation/route.js +3 -2
- package/dist/app/http/app.d.ts +5 -10
- package/dist/app/http/app.js +45 -98
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-tutor-files.js +1 -1
- package/dist/app/http/parent-home.d.ts +14 -0
- package/dist/app/http/parent-home.js +69 -6
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/raw-view.js +3 -3
- 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 +0 -2
- package/dist/app/machine/bailian.js +5 -6
- package/dist/app/mock/mock-live.js +1 -1
- package/dist/app/mock/mock.d.ts +4 -2
- package/dist/app/mock/mock.js +20 -12
- package/dist/app/replay.js +4 -4
- package/dist/app/skills/home-doc.js +1 -1
- package/dist/app/workspace.d.ts +9 -4
- package/dist/app/workspace.js +33 -10
- package/dist/app/ws/route.d.ts +8 -2
- package/dist/app/ws/route.js +7 -3
- package/dist/board/parse.d.ts +0 -2
- package/dist/board/parse.js +0 -6
- package/dist/board/presentation.d.ts +9 -15
- package/dist/board/presentation.js +8 -10
- package/dist/brain/api-tools.js +2 -1
- package/dist/brain/api.d.ts +12 -17
- package/dist/brain/api.js +16 -33
- package/dist/brain/index.d.ts +23 -71
- package/dist/brain/index.js +6 -69
- 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/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
- package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
- package/dist/brain/stream.d.ts +5 -5
- package/dist/brain/stream.js +7 -7
- package/dist/brain/transcript.d.ts +3 -3
- package/dist/brain/transcript.js +6 -6
- package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
- package/dist/brain/types.js +14 -0
- package/dist/chalk/glyphs/skeleton.d.ts +3 -3
- package/dist/chalk/glyphs/skeleton.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/memory/diary.js +3 -3
- package/dist/realtime/bailian.d.ts +1 -1
- package/dist/realtime/bailian.js +3 -2
- package/dist/realtime/preview.js +1 -1
- package/dist/realtime/voices.d.ts +2 -0
- package/dist/realtime/voices.js +57 -57
- package/dist/runtime/activities.d.ts +13 -1
- package/dist/runtime/activities.js +28 -2
- package/dist/runtime/activity-state.js +2 -2
- package/dist/runtime/agent-file.d.ts +3 -3
- package/dist/runtime/agent-file.js +3 -3
- package/dist/runtime/context/tutor-rules.d.ts +2 -2
- package/dist/runtime/context/tutor-rules.js +3 -3
- package/dist/runtime/home/home.d.ts +12 -1
- package/dist/runtime/home/home.js +8 -1
- package/dist/runtime/home/store.d.ts +1 -1
- package/dist/runtime/home/store.js +36 -21
- package/dist/runtime/journal/mix.d.ts +15 -6
- package/dist/runtime/journal/mix.js +60 -19
- package/dist/runtime/journal/recorder.d.ts +58 -13
- package/dist/runtime/journal/recorder.js +106 -16
- package/dist/runtime/journal/store.d.ts +4 -4
- package/dist/runtime/journal/store.js +6 -6
- package/dist/runtime/kid-input.js +2 -3
- package/dist/runtime/live/call.d.ts +12 -0
- package/dist/runtime/live/call.js +51 -18
- package/dist/runtime/live/turns.js +1 -1
- package/dist/runtime/runner.d.ts +9 -55
- package/dist/runtime/runner.js +52 -173
- package/dist/scenes/paint/painter.js +4 -3
- package/dist/schema/config.d.ts +43 -22
- package/dist/schema/config.js +45 -47
- package/dist/schema/conversation.d.ts +6 -0
- package/dist/schema/home.d.ts +11 -1
- package/dist/schema/home.js +7 -2
- package/dist/schema/workspace.d.ts +27 -0
- package/dist/schema/workspace.js +28 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/qianwen.d.ts +14 -10
- package/dist/shared/qianwen.js +26 -22
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/ui/dev/dev-page.js +25 -252
- package/dist/ui/dev/dev-theme.d.ts +1 -1
- package/dist/ui/dev/dev-theme.js +1 -1
- package/dist/ui/dev/voice-gallery-page.js +1 -1
- package/dist/ui/kid/board-mark.d.ts +2 -2
- package/dist/ui/kid/board-mark.js +2 -2
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-page.js +68 -103
- package/dist/ui/kid/voice-ui.d.ts +1 -1
- package/dist/ui/kid/voice-ui.js +1 -1
- package/dist/ui/kid/word-emoji.js +5 -3
- package/dist/ui/parent/parent-home-page.js +315 -7
- package/dist/ui/parent/parent-page.js +6 -2
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
- package/dist/ui/{dev → parent}/qr-page.js +19 -14
- package/dist/ui/shared/brand.d.ts +3 -1
- package/dist/ui/shared/brand.js +3 -1
- package/grades/grade-2/README.md +1 -1
- package/grades/grade-2/VALIDATION.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
- 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
- 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
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
- 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
- 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
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
- 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
- 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
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/package.json +2 -1
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
- package/skills/cotutor-board/references/photo.md +2 -2
- package/skills/cotutor-home/SKILL.md +1 -0
- package/skills/cotutor-home/references/tutor.md +2 -1
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
- package/skills/cotutor-tutor/SKILL.md +5 -13
- package/dist/brain/cli/adapters.d.ts +0 -60
- package/dist/brain/cli/adapters.js +0 -91
- package/dist/brain/cli/qwen.d.ts +0 -11
- package/dist/brain/cli/qwen.js +0 -141
- package/dist/brain/cli/run-plan.d.ts +0 -59
- package/dist/brain/cli/run-plan.js +0 -71
- package/dist/brain/cli/run.d.ts +0 -35
- package/dist/brain/cli/run.js +0 -256
- package/dist/brain/cli/types.js +0 -3
- package/dist/brain/cli/warm.d.ts +0 -53
- package/dist/brain/cli/warm.js +0 -103
package/dist/realtime/preview.js
CHANGED
|
@@ -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(
|
|
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)
|
package/dist/realtime/voices.js
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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(
|
|
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(
|
|
4
|
-
*
|
|
3
|
+
* 这里只认平铺的 `key: value` frontmatter(name / description / maxTurns / permissionMode / memory,当初两个 CLI 的公共子集,应用只读 name);
|
|
4
|
+
* 正文是系统提示的前半(后面接板书写法)。
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
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(
|
|
4
|
-
*
|
|
3
|
+
* 这里只认平铺的 `key: value` frontmatter(name / description / maxTurns / permissionMode / memory,当初两个 CLI 的公共子集,应用只读 name);
|
|
4
|
+
* 正文是系统提示的前半(后面接板书写法)。
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
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
|
-
*
|
|
19
|
-
* 认三种:技能工具(claude 的 Skill、qwen 的 skill
|
|
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/
|
|
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
|
-
*
|
|
42
|
-
* 认三种:技能工具(claude 的 Skill、qwen 的 skill
|
|
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:
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
/**
|
|
57
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
}
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|