cotutor 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +65 -18
- 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 +11 -0
- package/dist/app/cli/cert.js +36 -9
- 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 +11 -5
- package/dist/app/cli/doctor.js +156 -181
- 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 +379 -27
- 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 +152 -0
- package/dist/app/cli/service.js +443 -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 +42 -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 +236 -0
- package/dist/app/cli/tray.js +670 -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 +47 -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/machine/update.d.ts +25 -0
- package/dist/app/machine/update.js +88 -0
- 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/shared/upgrade.d.ts +9 -0
- package/dist/shared/upgrade.js +23 -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 +337 -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 +6 -2
- 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
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一次通话的录像记录(录数据,不录屏幕):口语课与实时老师共用。
|
|
3
|
+
* 发给孩子端(o)/ 孩子端发来(i)的每条消息带离开始的毫秒;两边的声音每块带到达的毫秒;孩子端清播放队列的时刻;屏幕多大。
|
|
4
|
+
* 按段收(2026-11-06):页面开着不关,一通电话能挂几个钟头,声音全攒在内存里挂断时再混,一个钟头就上 G。
|
|
5
|
+
* cut() 收下这一段、接着录新的一段;什么时候切由调用方定(call.ts:两轮之间安静着、没动静够久了或这一段够长了)。
|
|
6
|
+
* ms() 一直是离通话开始的毫秒(调用方的时钟,切段不动它);收下的那一段里的时刻都换成离这一段开始的毫秒。
|
|
7
|
+
* 一段尾巴上没动静的那截不要(TAKE_TAIL_MS 之后);没动静的一段收下来 stirred = false,调用方丢掉;
|
|
8
|
+
* 没动静的一段第一次有动静时,TAKE_LEAD_MS 之前的那截丢掉(孩子走开一个钟头再回来,录像不从一个钟头前开始)。
|
|
9
|
+
* 收下的(RawTake)交给 renderTake:两边按到达时刻混成一条(src/runtime/journal/mix.ts,分片混、不卡事件循环),给出录像的数据(reel.json 的形状)与要落盘的声音;落在哪由调用方定。
|
|
10
|
+
*/
|
|
11
|
+
import { type Chunk } from './mix.ts';
|
|
1
12
|
export type ReelMsg = [number, 'o' | 'i', Record<string, unknown>];
|
|
2
|
-
/** reel.json
|
|
13
|
+
/** reel.json:消息、老师出声的时段、孩子有声的时段、屏幕多大;声音另在同名的 .wav(24k 单声道) */
|
|
3
14
|
export interface CallReel {
|
|
4
15
|
v: 1;
|
|
5
16
|
startedAt: string;
|
|
@@ -9,38 +20,72 @@ export interface CallReel {
|
|
|
9
20
|
playing: [number, number][];
|
|
10
21
|
kidVoice: [number, number][];
|
|
11
22
|
}
|
|
23
|
+
/** 收下的一段,还没混:时刻都是离这一段开始的毫秒(开始之前到的块是负的,混的时候落在外面的不要) */
|
|
24
|
+
export interface RawTake {
|
|
25
|
+
startedAt: Date;
|
|
26
|
+
ms: number;
|
|
27
|
+
view: [number, number] | null;
|
|
28
|
+
msgs: ReelMsg[];
|
|
29
|
+
kid: Chunk[];
|
|
30
|
+
tutor: Chunk[];
|
|
31
|
+
flushes: number[];
|
|
32
|
+
/** 这一段有没有动静(孩子按下、发来、嘴说了……);没有的不落 */
|
|
33
|
+
stirred: boolean;
|
|
34
|
+
}
|
|
12
35
|
export interface CallTake {
|
|
13
36
|
ms: number;
|
|
14
37
|
reel: CallReel;
|
|
15
38
|
/** 两边按时刻混好的一条(MIX_RATE) */
|
|
16
39
|
callPcm: Buffer;
|
|
17
|
-
/** 两边各自首尾相接(孩子 16k、老师 24k),没有时刻 */
|
|
18
|
-
kidPcm: Buffer;
|
|
19
|
-
tutorPcm: Buffer;
|
|
20
40
|
}
|
|
41
|
+
/** 一段尾巴上:最后一次动静之后留这么久 */
|
|
42
|
+
export declare const TAKE_TAIL_MS = 2000;
|
|
43
|
+
/** 一段开头:第一次动静之前留这么久 */
|
|
44
|
+
export declare const TAKE_LEAD_MS = 2000;
|
|
21
45
|
export declare class CallRecorder {
|
|
22
46
|
readonly startedAt: Date;
|
|
23
47
|
private readonly now;
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
48
|
+
private msgs;
|
|
49
|
+
private kidChunks;
|
|
50
|
+
private tutorChunks;
|
|
51
|
+
private flushes;
|
|
28
52
|
private size;
|
|
53
|
+
/** 这一段从哪一刻起(离通话开始的毫秒) */
|
|
54
|
+
private base;
|
|
55
|
+
/** 这一段最后一次动静(老师的声音算到它在孩子端放完);没动静时 = base */
|
|
56
|
+
private lastStir;
|
|
57
|
+
private stirredNow;
|
|
58
|
+
/** 老师的声音在孩子端排到哪(估的,flush 不截;只用来算最后一次动静) */
|
|
59
|
+
private tutorEnd;
|
|
29
60
|
constructor(now?: () => Date);
|
|
30
|
-
/**
|
|
61
|
+
/** 离通话开始多少毫秒(切段不动它) */
|
|
31
62
|
ms(): number;
|
|
63
|
+
/** 这一段多久没动静了 */
|
|
64
|
+
quietMs(): number;
|
|
65
|
+
/** 这一段录了多久 */
|
|
66
|
+
takeMs(): number;
|
|
67
|
+
/** 这一段有过动静没有 */
|
|
68
|
+
get stirred(): boolean;
|
|
32
69
|
/** 发给孩子端的一条;{t:'flush'} 同时记下清队列的时刻 */
|
|
33
70
|
out(m: Record<string, unknown>): void;
|
|
34
71
|
/** 孩子端发来的一条 */
|
|
35
72
|
in(m: Record<string, unknown>, t?: number): void;
|
|
36
73
|
/** 孩子端清了播放队列,但没有一条 flush 消息(比如换段时页面自己清) */
|
|
37
74
|
flush(): void;
|
|
38
|
-
/** 孩子的一块声音(16k);t 缺省现在(开口前垫上的那几块带自己到的时刻) */
|
|
75
|
+
/** 孩子的一块声音(16k);t 缺省现在(开口前垫上的那几块带自己到的时刻)。不算动静:一直听时没人说话也一直在推 */
|
|
39
76
|
kid(pcm: Buffer, t?: number): void;
|
|
40
|
-
/** 老师的一块声音(24k) */
|
|
77
|
+
/** 老师的一块声音(24k):算动静,算到它在孩子端放完 */
|
|
41
78
|
tutor(pcm: Buffer): void;
|
|
42
79
|
/** 孩子端报的屏幕尺寸;不像样的不记 */
|
|
43
80
|
view(w: unknown, h: unknown): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
81
|
+
/** 收这一段,从现在起录新的一段(调用方在两轮之间安静时切) */
|
|
82
|
+
cut(): RawTake;
|
|
83
|
+
/** 挂了:收最后一段(ms 缺省现在) */
|
|
84
|
+
finish(ms?: number): RawTake;
|
|
85
|
+
/** 有动静:这一段头一次有动静就把 TAKE_LEAD_MS 之前的丢掉 */
|
|
86
|
+
private stir;
|
|
87
|
+
/** 这一段到 end 为止(有动静的截到最后一次动静 + TAKE_TAIL_MS),时刻换成离这一段开始的毫秒 */
|
|
88
|
+
private take;
|
|
46
89
|
}
|
|
90
|
+
/** 混声音、拼录像的数据(分片混,不卡事件循环) */
|
|
91
|
+
export declare function renderTake(raw: RawTake): Promise<CallTake>;
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 一次通话的录像记录(录数据,不录屏幕):口语课与实时老师共用。
|
|
3
3
|
* 发给孩子端(o)/ 孩子端发来(i)的每条消息带离开始的毫秒;两边的声音每块带到达的毫秒;孩子端清播放队列的时刻;屏幕多大。
|
|
4
|
-
*
|
|
4
|
+
* 按段收(2026-11-06):页面开着不关,一通电话能挂几个钟头,声音全攒在内存里挂断时再混,一个钟头就上 G。
|
|
5
|
+
* cut() 收下这一段、接着录新的一段;什么时候切由调用方定(call.ts:两轮之间安静着、没动静够久了或这一段够长了)。
|
|
6
|
+
* ms() 一直是离通话开始的毫秒(调用方的时钟,切段不动它);收下的那一段里的时刻都换成离这一段开始的毫秒。
|
|
7
|
+
* 一段尾巴上没动静的那截不要(TAKE_TAIL_MS 之后);没动静的一段收下来 stirred = false,调用方丢掉;
|
|
8
|
+
* 没动静的一段第一次有动静时,TAKE_LEAD_MS 之前的那截丢掉(孩子走开一个钟头再回来,录像不从一个钟头前开始)。
|
|
9
|
+
* 收下的(RawTake)交给 renderTake:两边按到达时刻混成一条(src/runtime/journal/mix.ts,分片混、不卡事件循环),给出录像的数据(reel.json 的形状)与要落盘的声音;落在哪由调用方定。
|
|
5
10
|
*/
|
|
6
|
-
import {
|
|
11
|
+
import { mixCallSliced } from "./mix.js";
|
|
12
|
+
/** 一段尾巴上:最后一次动静之后留这么久 */
|
|
13
|
+
export const TAKE_TAIL_MS = 2000;
|
|
14
|
+
/** 一段开头:第一次动静之前留这么久 */
|
|
15
|
+
export const TAKE_LEAD_MS = 2000;
|
|
16
|
+
/** 不算动静的:孩子端报屏幕尺寸、说再见;嘴连上、换连接、断了、挂断(只记不发的那几条也在) */
|
|
17
|
+
const STILL_IN = new Set(['view', 'bye']);
|
|
18
|
+
const STILL_OUT = new Set(['ready', 'swap', 'lost', 'down', 'end']);
|
|
19
|
+
/** 孩子端排老师的声音起步(同 mix.ts 的 LEAD_MS):估这块放完的时刻用 */
|
|
20
|
+
const TUTOR_LEAD_MS = 60;
|
|
21
|
+
const TUTOR_RATE = 24000;
|
|
7
22
|
export class CallRecorder {
|
|
8
23
|
startedAt;
|
|
9
24
|
now;
|
|
@@ -12,21 +27,40 @@ export class CallRecorder {
|
|
|
12
27
|
tutorChunks = [];
|
|
13
28
|
flushes = [];
|
|
14
29
|
size = null;
|
|
30
|
+
/** 这一段从哪一刻起(离通话开始的毫秒) */
|
|
31
|
+
base = 0;
|
|
32
|
+
/** 这一段最后一次动静(老师的声音算到它在孩子端放完);没动静时 = base */
|
|
33
|
+
lastStir = 0;
|
|
34
|
+
stirredNow = false;
|
|
35
|
+
/** 老师的声音在孩子端排到哪(估的,flush 不截;只用来算最后一次动静) */
|
|
36
|
+
tutorEnd = 0;
|
|
15
37
|
constructor(now = () => new Date()) {
|
|
16
38
|
this.now = now;
|
|
17
39
|
this.startedAt = now();
|
|
18
40
|
}
|
|
19
|
-
/**
|
|
41
|
+
/** 离通话开始多少毫秒(切段不动它) */
|
|
20
42
|
ms() { return this.now().getTime() - this.startedAt.getTime(); }
|
|
43
|
+
/** 这一段多久没动静了 */
|
|
44
|
+
quietMs() { return this.ms() - this.lastStir; }
|
|
45
|
+
/** 这一段录了多久 */
|
|
46
|
+
takeMs() { return this.ms() - this.base; }
|
|
47
|
+
/** 这一段有过动静没有 */
|
|
48
|
+
get stirred() { return this.stirredNow; }
|
|
21
49
|
/** 发给孩子端的一条;{t:'flush'} 同时记下清队列的时刻 */
|
|
22
50
|
out(m) {
|
|
23
51
|
const t = this.ms();
|
|
24
52
|
this.msgs.push([t, 'o', m]);
|
|
25
|
-
if (m.t === 'flush')
|
|
53
|
+
if (m.t === 'flush') {
|
|
26
54
|
this.flushes.push(t);
|
|
55
|
+
this.tutorEnd = 0;
|
|
56
|
+
}
|
|
57
|
+
if (!STILL_OUT.has(String(m.t)))
|
|
58
|
+
this.stir(t);
|
|
27
59
|
}
|
|
28
60
|
/** 孩子端发来的一条 */
|
|
29
61
|
in(m, t = this.ms()) {
|
|
62
|
+
if (!STILL_IN.has(String(m.t)))
|
|
63
|
+
this.stir(t);
|
|
30
64
|
// 一直听时孩子那一句的开口 / 说完(补记的 hold / release)要记回它当时的时刻,按时刻插进去,录像按顺序读
|
|
31
65
|
let i = this.msgs.length;
|
|
32
66
|
while (i > 0 && this.msgs[i - 1][0] > t)
|
|
@@ -34,25 +68,81 @@ export class CallRecorder {
|
|
|
34
68
|
this.msgs.splice(i, 0, [t, 'i', m]);
|
|
35
69
|
}
|
|
36
70
|
/** 孩子端清了播放队列,但没有一条 flush 消息(比如换段时页面自己清) */
|
|
37
|
-
flush() {
|
|
38
|
-
|
|
71
|
+
flush() {
|
|
72
|
+
const t = this.ms();
|
|
73
|
+
this.flushes.push(t);
|
|
74
|
+
this.tutorEnd = 0;
|
|
75
|
+
this.stir(t);
|
|
76
|
+
}
|
|
77
|
+
/** 孩子的一块声音(16k);t 缺省现在(开口前垫上的那几块带自己到的时刻)。不算动静:一直听时没人说话也一直在推 */
|
|
39
78
|
kid(pcm, t = this.ms()) { this.kidChunks.push({ t, pcm }); }
|
|
40
|
-
/** 老师的一块声音(24k) */
|
|
41
|
-
tutor(pcm) {
|
|
79
|
+
/** 老师的一块声音(24k):算动静,算到它在孩子端放完 */
|
|
80
|
+
tutor(pcm) {
|
|
81
|
+
const t = this.ms();
|
|
82
|
+
this.tutorChunks.push({ t, pcm });
|
|
83
|
+
this.tutorEnd = Math.max(t + TUTOR_LEAD_MS, this.tutorEnd) + ((pcm.length >> 1) * 1000) / TUTOR_RATE;
|
|
84
|
+
this.stir(t);
|
|
85
|
+
this.lastStir = Math.max(this.lastStir, this.tutorEnd);
|
|
86
|
+
}
|
|
42
87
|
/** 孩子端报的屏幕尺寸;不像样的不记 */
|
|
43
88
|
view(w, h) {
|
|
44
89
|
if (typeof w === 'number' && typeof h === 'number' && w > 0 && h > 0)
|
|
45
90
|
this.size = [Math.round(w), Math.round(h)];
|
|
46
91
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
const
|
|
92
|
+
/** 收这一段,从现在起录新的一段(调用方在两轮之间安静时切) */
|
|
93
|
+
cut() {
|
|
94
|
+
const now = this.ms();
|
|
95
|
+
const raw = this.take(now);
|
|
96
|
+
this.msgs = [];
|
|
97
|
+
this.kidChunks = [];
|
|
98
|
+
this.tutorChunks = [];
|
|
99
|
+
this.flushes = [];
|
|
100
|
+
this.base = now;
|
|
101
|
+
this.lastStir = now;
|
|
102
|
+
this.stirredNow = false;
|
|
103
|
+
this.tutorEnd = 0;
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
/** 挂了:收最后一段(ms 缺省现在) */
|
|
107
|
+
finish(ms = this.ms()) { return this.take(ms); }
|
|
108
|
+
/** 有动静:这一段头一次有动静就把 TAKE_LEAD_MS 之前的丢掉 */
|
|
109
|
+
stir(t) {
|
|
110
|
+
if (!this.stirredNow) {
|
|
111
|
+
this.stirredNow = true;
|
|
112
|
+
const from = t - TAKE_LEAD_MS;
|
|
113
|
+
if (from > this.base) {
|
|
114
|
+
this.base = from;
|
|
115
|
+
this.msgs = this.msgs.filter(([at]) => at >= from);
|
|
116
|
+
this.kidChunks = this.kidChunks.filter((c) => c.t > from);
|
|
117
|
+
this.tutorChunks = this.tutorChunks.filter((c) => c.t > from);
|
|
118
|
+
this.flushes = this.flushes.filter((at) => at >= from);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (t > this.lastStir)
|
|
122
|
+
this.lastStir = t;
|
|
123
|
+
}
|
|
124
|
+
/** 这一段到 end 为止(有动静的截到最后一次动静 + TAKE_TAIL_MS),时刻换成离这一段开始的毫秒 */
|
|
125
|
+
take(end) {
|
|
126
|
+
const b = this.base;
|
|
127
|
+
const stop = this.stirredNow ? Math.min(end, this.lastStir + TAKE_TAIL_MS) : end;
|
|
50
128
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
129
|
+
startedAt: new Date(this.startedAt.getTime() + b),
|
|
130
|
+
ms: Math.max(0, Math.round(stop - b)),
|
|
131
|
+
view: this.size,
|
|
132
|
+
msgs: this.msgs.map(([t, d, m]) => [Math.max(0, t - b), d, m]),
|
|
133
|
+
kid: this.kidChunks.map((c) => ({ t: c.t - b, pcm: c.pcm })),
|
|
134
|
+
tutor: this.tutorChunks.map((c) => ({ t: c.t - b, pcm: c.pcm })),
|
|
135
|
+
flushes: this.flushes.map((t) => t - b),
|
|
136
|
+
stirred: this.stirredNow,
|
|
56
137
|
};
|
|
57
138
|
}
|
|
58
139
|
}
|
|
140
|
+
/** 混声音、拼录像的数据(分片混,不卡事件循环) */
|
|
141
|
+
export async function renderTake(raw) {
|
|
142
|
+
const mix = await mixCallSliced({ kid: raw.kid, tutor: raw.tutor, flushes: raw.flushes, ms: raw.ms });
|
|
143
|
+
return {
|
|
144
|
+
ms: raw.ms,
|
|
145
|
+
reel: { v: 1, startedAt: raw.startedAt.toISOString(), ms: raw.ms, view: raw.view, msgs: raw.msgs, playing: mix.playing, kidVoice: mix.kidVoice },
|
|
146
|
+
callPcm: mix.pcm,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -11,8 +11,8 @@ export declare function readIndex(ws: Workspace, tutor: string, date: string): P
|
|
|
11
11
|
export declare function writeIndex(ws: Workspace, index: ConversationIndex): Promise<void>;
|
|
12
12
|
/** 有过对话的日期,新的在前 */
|
|
13
13
|
export declare function listDates(ws: Workspace, tutor: string): Promise<string[]>;
|
|
14
|
-
/**
|
|
15
|
-
export declare function readTranscript(ws: Workspace, tutor: string, date: string, job: string
|
|
14
|
+
/** 一轮的转录:统一事件,旧轮次的 stream-json 也读(src/brain/log.ts) */
|
|
15
|
+
export declare function readTranscript(ws: Workspace, tutor: string, date: string, job: string): Promise<Transcript | null>;
|
|
16
16
|
/**
|
|
17
17
|
* 一天里卡的目录 <date>.<job>.cards/ 全扫一遍:<n>.json 是孩子做的事(坏文件跳过,孩子端不报),<n>/<k>.mp3 是后台生成好的资产。
|
|
18
18
|
*/
|
|
@@ -72,7 +72,7 @@ export interface RunFile {
|
|
|
72
72
|
session: string | null;
|
|
73
73
|
/** 老师文件与技能的快照;2026-09-15 之前的轮次没有 */
|
|
74
74
|
sources?: RunSources;
|
|
75
|
-
/**
|
|
75
|
+
/** 老师正文进了系统提示(API 运行时总是;旧轮次看那时的命令模板) */
|
|
76
76
|
agentBody: boolean;
|
|
77
77
|
}
|
|
78
78
|
export declare function writeRunFile(ws: Workspace, tutor: string, date: string, job: string, r: {
|
|
@@ -91,7 +91,7 @@ export declare function readRunFile(ws: Workspace, tutor: string, date: string,
|
|
|
91
91
|
export declare function readErrLog(ws: Workspace, tutor: string, date: string, job: string): Promise<string>;
|
|
92
92
|
/** 这轮起跑时老师文件与技能长什么样(记进 run.json;读不到的就 null / 空,不拦这一轮) */
|
|
93
93
|
export declare function snapshotSources(ws: Workspace, name: string): Promise<RunSources>;
|
|
94
|
-
/** 老师文件正文(
|
|
94
|
+
/** 老师文件正文(系统提示的前半);从 agents/<name>.md 读 */
|
|
95
95
|
export declare function readAgentBody(ws: Workspace, name: string): Promise<string>;
|
|
96
96
|
/** 给一个话题打星(1–5;null 清掉):话题要在这天的索引里;写回索引 */
|
|
97
97
|
export declare function rateThread(ws: Workspace, tutor: string, date: string, thread: string, rating: number | null): Promise<ConversationIndex>;
|
|
@@ -10,7 +10,7 @@ import { emptyIndex } from "./conversation.js";
|
|
|
10
10
|
import { localDate } from "../../shared/time.js";
|
|
11
11
|
import { cardAssetName, conversationFiles, threads, tutorFilePath } from "../../schema/index.js";
|
|
12
12
|
import { parseTranscript } from "../../brain/transcript.js";
|
|
13
|
-
import {
|
|
13
|
+
import { parseLogLine } from "../../brain/index.js";
|
|
14
14
|
import { ConversationIndexSchema, DATE_RE, explainIssues, } from "../../schema/index.js";
|
|
15
15
|
import { undoThreadVault } from "../../memory/vault.js";
|
|
16
16
|
import { recoverPending } from "../../memory/commit.js";
|
|
@@ -64,10 +64,10 @@ export async function listDates(ws, tutor) {
|
|
|
64
64
|
return [];
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
export async function readTranscript(ws, tutor, date, job
|
|
67
|
+
/** 一轮的转录:统一事件,旧轮次的 stream-json 也读(src/brain/log.ts) */
|
|
68
|
+
export async function readTranscript(ws, tutor, date, job) {
|
|
69
69
|
try {
|
|
70
|
-
return parseTranscript(await readFile(conversationFiles(ws.dirs.conversations, tutor, date).log(job), 'utf8'),
|
|
70
|
+
return parseTranscript(await readFile(conversationFiles(ws.dirs.conversations, tutor, date).log(job), 'utf8'), parseLogLine);
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
73
73
|
return null;
|
|
@@ -209,7 +209,7 @@ export async function snapshotSources(ws, name) {
|
|
|
209
209
|
const file = tutorFilePath(ws.dirs.agents, name);
|
|
210
210
|
try {
|
|
211
211
|
const body = await readFile(file, 'utf8');
|
|
212
|
-
agent = { file: relative(ws.root, file), hash: sha(body), body };
|
|
212
|
+
agent = { file: relative(ws.root, file).split(sep).join('/'), hash: sha(body), body };
|
|
213
213
|
}
|
|
214
214
|
catch {
|
|
215
215
|
/* 读不到:记 null */
|
|
@@ -224,7 +224,7 @@ export async function snapshotSources(ws, name) {
|
|
|
224
224
|
}
|
|
225
225
|
return { agent, skills };
|
|
226
226
|
}
|
|
227
|
-
/** 老师文件正文(
|
|
227
|
+
/** 老师文件正文(系统提示的前半);从 agents/<name>.md 读 */
|
|
228
228
|
export async function readAgentBody(ws, name) {
|
|
229
229
|
const file = tutorFilePath(ws.dirs.agents, name);
|
|
230
230
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 怎么告诉孩子端由各自的协议定(HTTP 回 JSON,WS 回 err)。不起老师。
|
|
2
|
+
* 孩子端发一条的验与拼:孩子端只走实时那条连接(src/runtime/live/turns.ts;HTTP 的发 2026-11-04 删了),经 Runner.accept 进来。
|
|
3
|
+
* 验正文、话题、首页按钮、小课堂、activity、照片,拼成 Runner.send 的入参;不对回 reject(状态码 + 错误名),连接上回 {t:'err'}。不起老师。
|
|
5
4
|
*/
|
|
6
5
|
import { FocusSchema, HomeViaSchema, DeviceSchema } from "../schema/index.js";
|
|
7
6
|
import { BUNDLE_ID_RE } from "../board/cards/kind.js";
|
|
@@ -30,6 +30,9 @@ export interface LiveDeps {
|
|
|
30
30
|
ringMs?: number;
|
|
31
31
|
/** 测试:要嘴说了多久一个字没出算卡住 */
|
|
32
32
|
stallMs?: number;
|
|
33
|
+
/** 测试:录像这一段多久没动静、录了多久就切(心跳时看) */
|
|
34
|
+
idleCutMs?: number;
|
|
35
|
+
maxTakeMs?: number;
|
|
33
36
|
onFinished?(why: string): void;
|
|
34
37
|
}
|
|
35
38
|
export declare class LiveCall {
|
|
@@ -110,6 +113,8 @@ export declare class LiveCall {
|
|
|
110
113
|
private chatQuiet;
|
|
111
114
|
private chatQuietUntil;
|
|
112
115
|
private offEvents;
|
|
116
|
+
/** 录像一段一段排着落盘(按顺序、一次混一段);挂了等它们都落完 */
|
|
117
|
+
private saving;
|
|
113
118
|
private ended;
|
|
114
119
|
private readonly sock;
|
|
115
120
|
private readonly deps;
|
|
@@ -155,6 +160,13 @@ export declare class LiveCall {
|
|
|
155
160
|
private mouthEnd;
|
|
156
161
|
/** 孩子按下(或不是说的那一轮开始):嘴在出就掐、还在放就清孩子端;排着的丢掉;记下孩子听到第几句 */
|
|
157
162
|
private interrupt;
|
|
163
|
+
/**
|
|
164
|
+
* 录像切段:两轮之间安静着(孩子没在说、嘴没在说也没在放、脑没在写、没在等转写),这一段没动静够久了或录得够长了,就收下落盘。
|
|
165
|
+
* 一轮不会被切开:录像按轮在一段里找按下、松手、turn 和 read
|
|
166
|
+
*/
|
|
167
|
+
private maybeCut;
|
|
168
|
+
/** 落一段录像(排在前面那几段后面):没动静的、只有连上那两条的不落 */
|
|
169
|
+
private saveTake;
|
|
158
170
|
/** 孩子的一条文本消息 */
|
|
159
171
|
onText(raw: string): void;
|
|
160
172
|
/** 一条孩子端的消息;at = 到的墙钟(现连时补的那几条照原来到的时刻记事件) */
|