cotutor 0.1.0 → 0.2.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/README.md +35 -24
- package/agents/chinese-tutor.md +67 -0
- package/agents/{homework-aide.md → homework-tutor.md} +4 -4
- package/agents/math-tutor.md +68 -0
- package/agents/reading-tutor.md +65 -0
- package/agents/scene-maker.md +41 -0
- package/dist/cards/canvas.d.ts +22 -0
- package/dist/cards/canvas.js +65 -0
- package/dist/cards/choice.d.ts +20 -0
- package/dist/cards/choice.js +76 -0
- package/dist/cards/code.d.ts +9 -0
- package/dist/cards/code.js +13 -0
- package/dist/cards/fill.d.ts +14 -0
- package/dist/cards/fill.js +64 -0
- package/dist/cards/image.d.ts +13 -0
- package/dist/cards/image.js +35 -0
- package/dist/cards/index.d.ts +48 -0
- package/dist/cards/index.js +115 -0
- package/dist/cards/kind.d.ts +27 -0
- package/dist/cards/kind.js +7 -0
- package/dist/cards/read.d.ts +8 -0
- package/dist/cards/read.js +27 -0
- package/dist/cards/scene.d.ts +24 -0
- package/dist/cards/scene.js +67 -0
- package/dist/cards/text.d.ts +18 -0
- package/dist/cards/text.js +41 -0
- package/dist/cli/cert.d.ts +7 -0
- package/dist/cli/cert.js +36 -0
- package/dist/cli/doctor.d.ts +5 -2
- package/dist/cli/doctor.js +183 -18
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +41 -39
- package/dist/cli/main.js +128 -8
- package/dist/cli/serve.d.ts +15 -4
- package/dist/cli/serve.js +40 -6
- package/dist/cli/skeleton.d.ts +23 -5
- package/dist/cli/skeleton.js +131 -29
- package/dist/cli/skills.d.ts +40 -0
- package/dist/cli/skills.js +170 -0
- package/dist/cli/tutors.d.ts +84 -0
- package/dist/cli/tutors.js +274 -0
- package/dist/cli/workspace.d.ts +9 -1
- package/dist/cli/workspace.js +25 -4
- package/dist/lib/agent-file.d.ts +1 -1
- package/dist/lib/board.d.ts +15 -0
- package/dist/lib/board.js +107 -0
- package/dist/lib/context-pack.js +10 -1
- package/dist/lib/conversation.d.ts +43 -6
- package/dist/lib/conversation.js +53 -9
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/kid-board.d.ts +148 -0
- package/dist/lib/kid-board.js +294 -0
- package/dist/lib/kid-view.d.ts +44 -7
- package/dist/lib/kid-view.js +63 -8
- package/dist/lib/ledger.d.ts +8 -0
- package/dist/lib/ledger.js +8 -0
- package/dist/lib/plan.d.ts +2 -0
- package/dist/lib/plan.js +9 -0
- package/dist/lib/run-plan.d.ts +30 -0
- package/dist/lib/run-plan.js +32 -0
- package/dist/lib/stream.d.ts +15 -0
- package/dist/lib/stream.js +69 -0
- package/dist/lib/timetable.d.ts +22 -0
- package/dist/lib/timetable.js +106 -0
- package/dist/schema/board.d.ts +59 -0
- package/dist/schema/board.js +30 -0
- package/dist/schema/config.d.ts +76 -18
- package/dist/schema/config.js +67 -37
- package/dist/schema/context-pack.d.ts +7 -0
- package/dist/schema/context-pack.js +6 -0
- package/dist/schema/conversation.d.ts +114 -4
- package/dist/schema/conversation.js +29 -5
- package/dist/schema/index.d.ts +6 -2
- package/dist/schema/index.js +6 -2
- package/dist/schema/json-schema.d.ts +2 -0
- package/dist/schema/json-schema.js +23 -0
- package/dist/schema/ledger.d.ts +1 -1
- package/dist/schema/timetable.d.ts +26 -0
- package/dist/schema/timetable.js +18 -0
- package/dist/server/app.d.ts +67 -6
- package/dist/server/app.js +399 -38
- package/dist/server/kid-page.d.ts +1 -0
- package/dist/server/kid-page.js +768 -0
- package/dist/server/mock.d.ts +58 -0
- package/dist/server/mock.js +403 -0
- package/dist/server/parent-page.d.ts +8 -0
- package/dist/server/parent-page.js +393 -0
- package/dist/server/runner.d.ts +64 -0
- package/dist/server/runner.js +277 -0
- package/dist/server/scene-props.d.ts +16 -0
- package/dist/server/scene-props.js +39 -0
- package/dist/server/stage.d.ts +15 -0
- package/dist/server/stage.js +69 -0
- package/dist/server/store.d.ts +43 -0
- package/dist/server/store.js +210 -0
- package/dist/server/tts.d.ts +51 -0
- package/dist/server/tts.js +99 -0
- package/dist/stage/Assistant-Bold-ZDZZ6JHA.woff2 +0 -0
- package/dist/stage/Assistant-Medium-DZ25RZU3.woff2 +0 -0
- package/dist/stage/Assistant-Regular-PLF2XOGW.woff2 +0 -0
- package/dist/stage/Assistant-SemiBold-CZ5MX6FK.woff2 +0 -0
- package/dist/stage/index.html +10 -0
- package/dist/stage/stage.css +1 -0
- package/dist/stage/stage.js +4383 -0
- package/package.json +13 -4
- package/agents/chinese-teacher.md +0 -19
- package/agents/math-teacher.md +0 -19
- package/agents/reading-teacher.md +0 -19
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片卡:第一行是图(workspace 里的相对路径——产物、孩子拍的照片;或 http(s) 地址),后面几行是图注。
|
|
3
|
+
* 路径由 /api/kid/image?p= 供图,只认 workspace 根以内的图片文件;舞台里双指缩放。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { type CardKind } from './kind.ts';
|
|
7
|
+
export declare const ImagePropsSchema: z.ZodObject<{
|
|
8
|
+
src: z.ZodString;
|
|
9
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type ImageProps = z.infer<typeof ImagePropsSchema>;
|
|
12
|
+
export declare const IMAGE_EXT: RegExp;
|
|
13
|
+
export declare const image: CardKind<ImageProps>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片卡:第一行是图(workspace 里的相对路径——产物、孩子拍的照片;或 http(s) 地址),后面几行是图注。
|
|
3
|
+
* 路径由 /api/kid/image?p= 供图,只认 workspace 根以内的图片文件;舞台里双指缩放。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { bodyLines } from "./kind.js";
|
|
7
|
+
export const ImagePropsSchema = z.object({
|
|
8
|
+
/** 相对 workspace 根的路径,或 http(s):// 地址 */
|
|
9
|
+
src: z.string().min(1),
|
|
10
|
+
caption: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
export const IMAGE_EXT = /\.(png|jpe?g|webp|gif|svg)$/i;
|
|
13
|
+
export const image = {
|
|
14
|
+
name: 'image',
|
|
15
|
+
props: ImagePropsSchema,
|
|
16
|
+
parse(body) {
|
|
17
|
+
const [src, ...rest] = bodyLines(body);
|
|
18
|
+
if (!src)
|
|
19
|
+
throw new Error('第一行要是图片路径或地址');
|
|
20
|
+
if (!/^https?:\/\//.test(src) && !IMAGE_EXT.test(src))
|
|
21
|
+
throw new Error('要是 png / jpg / webp / gif / svg 图片');
|
|
22
|
+
const caption = rest.join('\n');
|
|
23
|
+
return { src, ...(caption ? { caption } : {}) };
|
|
24
|
+
},
|
|
25
|
+
doc: `### image — 图片
|
|
26
|
+
|
|
27
|
+
第一行是图:workspace 里的相对路径(产物、孩子拍的照片,如 \`vault/照片/2026-09-10-作业.jpg\`)或 http(s) 地址;后面几行是图注。孩子点开能放大。
|
|
28
|
+
|
|
29
|
+
\`\`\`\`
|
|
30
|
+
\`\`\`image
|
|
31
|
+
vault/照片/2026-09-10-作业.jpg
|
|
32
|
+
你昨天写的这道题,看第二行。
|
|
33
|
+
\`\`\`
|
|
34
|
+
\`\`\`\``,
|
|
35
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BoardCard, BoardSection } from '../lib/kid-board.ts';
|
|
2
|
+
import type { CardKind } from './kind.ts';
|
|
3
|
+
export type { CardKind } from './kind.ts';
|
|
4
|
+
export { text, TEXT_STYLES, type TextProps, type TextStyle } from './text.ts';
|
|
5
|
+
export { read, type ReadProps } from './read.ts';
|
|
6
|
+
export { choice, type ChoiceProps } from './choice.ts';
|
|
7
|
+
export { fill, type FillProps } from './fill.ts';
|
|
8
|
+
export { code, type CodeProps } from './code.ts';
|
|
9
|
+
export { image, IMAGE_EXT, type ImageProps } from './image.ts';
|
|
10
|
+
export { scene, BUNDLE_ID_RE, type SceneProps, type SceneState } from './scene.ts';
|
|
11
|
+
export { canvas, type CanvasProps, type CanvasState } from './canvas.ts';
|
|
12
|
+
export declare const CARD_KINDS: readonly CardKind<any>[];
|
|
13
|
+
export declare function cardKind(name: string): CardKind | undefined;
|
|
14
|
+
export interface ParsedCard {
|
|
15
|
+
card: BoardCard;
|
|
16
|
+
/** 没解析成时的一句(家长视图转录里显示);孩子端什么也不报 */
|
|
17
|
+
warning?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 围栏 → 卡。标签第一个词是 kind,其余是修饰;不认识的标签当代码卡原样显示;
|
|
21
|
+
* 认识但正文解析不出 → 文字卡显示原文 + warning。永不抛错。
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseCard(tag: string, body: string): ParsedCard;
|
|
24
|
+
/** 孩子端的一节:每张卡过本种卡的 strip(答案不下发);不认识的 kind 原样;孩子自己的状态与已生成的资产照给 */
|
|
25
|
+
export declare function stripSecrets(section: BoardSection): BoardSection;
|
|
26
|
+
/** 一张卡的标题(舞台顶栏、给老师的描述里用):文字卡的 title / text,选择题的问题,其余第一段有字的 */
|
|
27
|
+
export declare function cardLabel(card: BoardCard): string;
|
|
28
|
+
/** 这张卡要后台预生成的配音(文件名相对它的资产目录);没有的种类 → [] */
|
|
29
|
+
export declare function cardAssets(card: BoardCard): {
|
|
30
|
+
file: string;
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
export type CardStateResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
state: unknown;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
why: string;
|
|
39
|
+
};
|
|
40
|
+
/** 孩子端 PUT 上来的状态过本种卡的契约;这种卡没有状态、或不认识的 kind → 不收 */
|
|
41
|
+
export declare function parseCardState(card: BoardCard, raw: unknown): CardStateResult;
|
|
42
|
+
/**
|
|
43
|
+
* 给老师的一句:「choice「问题」 选了「B …」(答案:「…」)」。props 用索引里的(带答案),状态是孩子的;
|
|
44
|
+
* 没有 describe 的种类把状态原样 JSON。id 由调用方拼在前面(<job>/<n>)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function describeCard(card: BoardCard, state: unknown): string;
|
|
47
|
+
/** 给老师看的语法表(出厂到 workspace 的 .cotutor/板书语法.md;从各种卡的 doc 拼出来,不手写) */
|
|
48
|
+
export declare function boardSyntaxDoc(): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 卡的注册表:加一种卡 = 加一个文件 + 这里一行;解析器、语法表、孩子端剥秘密都从这里走。
|
|
3
|
+
* 页面那一半在 kid-page.ts(渲染)与 kid-board.ts(标注落点),按 kind 名对上。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { canvas } from "./canvas.js";
|
|
7
|
+
import { choice } from "./choice.js";
|
|
8
|
+
import { code } from "./code.js";
|
|
9
|
+
import { fill } from "./fill.js";
|
|
10
|
+
import { image } from "./image.js";
|
|
11
|
+
import { read } from "./read.js";
|
|
12
|
+
import { scene } from "./scene.js";
|
|
13
|
+
import { text } from "./text.js";
|
|
14
|
+
export { text, TEXT_STYLES } from "./text.js";
|
|
15
|
+
export { read } from "./read.js";
|
|
16
|
+
export { choice } from "./choice.js";
|
|
17
|
+
export { fill } from "./fill.js";
|
|
18
|
+
export { code } from "./code.js";
|
|
19
|
+
export { image, IMAGE_EXT } from "./image.js";
|
|
20
|
+
export { scene, BUNDLE_ID_RE } from "./scene.js";
|
|
21
|
+
export { canvas } from "./canvas.js";
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
export const CARD_KINDS = [text, read, choice, fill, image, scene, canvas, code];
|
|
24
|
+
export function cardKind(name) {
|
|
25
|
+
return CARD_KINDS.find((k) => k.name === name);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 围栏 → 卡。标签第一个词是 kind,其余是修饰;不认识的标签当代码卡原样显示;
|
|
29
|
+
* 认识但正文解析不出 → 文字卡显示原文 + warning。永不抛错。
|
|
30
|
+
*/
|
|
31
|
+
export function parseCard(tag, body) {
|
|
32
|
+
const [name, ...mods] = tag.trim().split(/\s+/).filter(Boolean);
|
|
33
|
+
if (!name)
|
|
34
|
+
return { card: { kind: 'code', props: code.parse(body, []) } };
|
|
35
|
+
const kind = cardKind(name.toLowerCase());
|
|
36
|
+
if (!kind)
|
|
37
|
+
return { card: { kind: 'code', props: code.parse(body, [name]) } };
|
|
38
|
+
try {
|
|
39
|
+
const props = kind.props.parse(kind.parse(body, mods));
|
|
40
|
+
return { card: { kind: kind.name, props } };
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const why = err instanceof z.ZodError ? err.issues.map((i) => `${i.path.join('.')}:${i.message}`).join(';') : err instanceof Error ? err.message : String(err);
|
|
44
|
+
return { card: { kind: 'text', props: { text: body.trim() || name } }, warning: `卡片没解析成:${name} — ${why}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 孩子端的一节:每张卡过本种卡的 strip(答案不下发);不认识的 kind 原样;孩子自己的状态与已生成的资产照给 */
|
|
48
|
+
export function stripSecrets(section) {
|
|
49
|
+
return {
|
|
50
|
+
...section,
|
|
51
|
+
cards: section.cards.map((c) => {
|
|
52
|
+
const k = cardKind(c.kind);
|
|
53
|
+
return k?.strip ? { ...c, props: k.strip(c.props) } : c;
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** 一张卡的标题(舞台顶栏、给老师的描述里用):文字卡的 title / text,选择题的问题,其余第一段有字的 */
|
|
58
|
+
export function cardLabel(card) {
|
|
59
|
+
const p = card.props;
|
|
60
|
+
const s = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
61
|
+
const first = s(p.title) || s(p.question) || s(p.text) || s(p.caption) || s(p.prompt) || (Array.isArray(p.segments) ? s(p.segments[0]) : '');
|
|
62
|
+
const cps = Array.from(first.replace(/\s+/g, ' '));
|
|
63
|
+
return cps.length > 40 ? `${cps.slice(0, 40).join('')}…` : cps.join('');
|
|
64
|
+
}
|
|
65
|
+
/** 这张卡要后台预生成的配音(文件名相对它的资产目录);没有的种类 → [] */
|
|
66
|
+
export function cardAssets(card) {
|
|
67
|
+
const k = cardKind(card.kind);
|
|
68
|
+
if (!k?.assets)
|
|
69
|
+
return [];
|
|
70
|
+
const r = k.props.safeParse(card.props);
|
|
71
|
+
return r.success ? k.assets(r.data) : [];
|
|
72
|
+
}
|
|
73
|
+
/** 孩子端 PUT 上来的状态过本种卡的契约;这种卡没有状态、或不认识的 kind → 不收 */
|
|
74
|
+
export function parseCardState(card, raw) {
|
|
75
|
+
const k = cardKind(card.kind);
|
|
76
|
+
if (!k?.state)
|
|
77
|
+
return { ok: false, why: `${card.kind} 卡没有状态` };
|
|
78
|
+
const r = k.state.safeParse(raw);
|
|
79
|
+
if (!r.success)
|
|
80
|
+
return { ok: false, why: r.error.issues.map((i) => `${i.path.join('.')}:${i.message}`).join(';') };
|
|
81
|
+
return { ok: true, state: r.data };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 给老师的一句:「choice「问题」 选了「B …」(答案:「…」)」。props 用索引里的(带答案),状态是孩子的;
|
|
85
|
+
* 没有 describe 的种类把状态原样 JSON。id 由调用方拼在前面(<job>/<n>)。
|
|
86
|
+
*/
|
|
87
|
+
export function describeCard(card, state) {
|
|
88
|
+
const k = cardKind(card.kind);
|
|
89
|
+
const head = `${card.kind}「${cardLabel(card)}」`;
|
|
90
|
+
if (!k?.describe || !k.state)
|
|
91
|
+
return `${head} ${JSON.stringify(state)}`;
|
|
92
|
+
const r = k.state.safeParse(state);
|
|
93
|
+
return `${head} ${r.success ? k.describe(card.props, r.data) : JSON.stringify(state)}`;
|
|
94
|
+
}
|
|
95
|
+
/** 给老师看的语法表(出厂到 workspace 的 .cotutor/板书语法.md;从各种卡的 doc 拼出来,不手写) */
|
|
96
|
+
export function boardSyntaxDoc() {
|
|
97
|
+
const kinds = CARD_KINDS.map((k) => k.doc.trim()).join('\n\n');
|
|
98
|
+
return `# 板书怎么写
|
|
99
|
+
|
|
100
|
+
回复正文就是孩子看到的板书,只有两种东西:
|
|
101
|
+
|
|
102
|
+
- **普通段落 = 你说的话。** 一行一句,每句会被念出来、显示在字幕行,所以不要写标题、列表、粗体、括号注释。句子里用方括号标出要在板上敲的词,如「这叫[底]」,那个词要在某张卡上出现。末句写成问句就停下等孩子。
|
|
103
|
+
- **围栏 = 板上的卡。** 围栏的语言标签是卡的种类,后面的词是修饰;正文按各种卡的写法。卡写在讲它的那句话前面,卡与话交错。不认识的标签当代码卡原样显示;正文写得不对的卡退成一段文字,孩子端不会报错。
|
|
104
|
+
|
|
105
|
+
一节 4–8 张卡、6–12 句话;一次只讲一个想法,讲完就问。随口问答就一两句话,没有卡。**板书写完就停**:最后一个字是问孩子的那句,后面不要再补总结、不要再用工具——孩子看到的是你这轮最后一段话,再补一句板书就丢了。
|
|
106
|
+
|
|
107
|
+
孩子在卡上做的事(选了、填了)会在下一条消息的上下文包里以 \`cards:\` 段告诉你,一张卡一行(卡的编号、种类、标题、做了什么、答案);孩子只交答案没说话时消息正文是「(交了答案,没说话)」。孩子看到的卡上没有对错,对错由你口头说。上下文包里 \`focus.card\` 是孩子发消息时正开着的那张卡。
|
|
108
|
+
|
|
109
|
+
对家长说的话、要拍板的事、要转交的事,用「## 家长」「## 待裁量」(question: 一句话;options: 列表)「## 转交」三个段放在正文末尾,孩子看不到;板书到第一个「## 」为止。
|
|
110
|
+
|
|
111
|
+
## 卡的种类
|
|
112
|
+
|
|
113
|
+
${kinds}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一种卡的服务端定义(《板书卡片设计.md》§1):props 契约、围栏正文怎么解析、哪些字段不下发孩子端、给老师看的写法;
|
|
3
|
+
* 有交互的卡再加 state 契约(孩子在卡上做的事,PUT 进来先过它)与 describe(把 props + state 说成给老师看的一句,进上下文包)。
|
|
4
|
+
* 页面那一半(紧凑态怎么画、舞台里怎么交互、标注落在哪些字上)在 kid-page.ts / kid-board.ts;两边靠 kind 名对上。
|
|
5
|
+
*/
|
|
6
|
+
import type { z } from 'zod';
|
|
7
|
+
export interface CardKind<P = Record<string, unknown>, S = unknown> {
|
|
8
|
+
name: string;
|
|
9
|
+
props: z.ZodType<P>;
|
|
10
|
+
/** 围栏正文 + 标签后的修饰词 → props;抛错 = 没解析成(退成文字卡显示原文,家长视图报一行) */
|
|
11
|
+
parse(body: string, mods: readonly string[]): P;
|
|
12
|
+
/** 剥掉不下发孩子端的字段(答案);没有 = 全给 */
|
|
13
|
+
strip?(props: P): P;
|
|
14
|
+
/** 孩子在卡上做的事的形状;没有 = 这种卡没有状态,PUT 一律 400 */
|
|
15
|
+
state?: z.ZodType<S>;
|
|
16
|
+
/** props + state → 给老师看的一句(不含卡的标题,那句由注册表拼);没有 = 状态原样 JSON */
|
|
17
|
+
describe?(props: P, state: S): string;
|
|
18
|
+
/** 这张卡要在后台预生成的配音资产(点读的段):文件名相对这张卡的资产目录 <日期>.<job>.cards/<n>/,text 是要念的字 */
|
|
19
|
+
assets?(props: P): {
|
|
20
|
+
file: string;
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
/** 语法表里的一段:写法 + 例子(给老师看),由 boardSyntaxDoc 拼成 .cotutor/板书语法.md */
|
|
24
|
+
doc: string;
|
|
25
|
+
}
|
|
26
|
+
/** 围栏正文 → 非空行(各自 trim) */
|
|
27
|
+
export declare function bodyLines(body: string): string[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 点读卡:一行一段,孩子点哪段听哪段;配音在一轮跑完后后台按段合成到 .cards/<n>/<段>.mp3,还没好的段退浏览器合成声。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type CardKind } from './kind.ts';
|
|
4
|
+
export declare const ReadPropsSchema: z.ZodObject<{
|
|
5
|
+
segments: z.ZodArray<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type ReadProps = z.infer<typeof ReadPropsSchema>;
|
|
8
|
+
export declare const read: CardKind<ReadProps>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** 点读卡:一行一段,孩子点哪段听哪段;配音在一轮跑完后后台按段合成到 .cards/<n>/<段>.mp3,还没好的段退浏览器合成声。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { bodyLines } from "./kind.js";
|
|
4
|
+
export const ReadPropsSchema = z.object({ segments: z.array(z.string().min(1)).min(1) });
|
|
5
|
+
export const read = {
|
|
6
|
+
name: 'read',
|
|
7
|
+
props: ReadPropsSchema,
|
|
8
|
+
parse(body) {
|
|
9
|
+
const segments = bodyLines(body);
|
|
10
|
+
if (!segments.length)
|
|
11
|
+
throw new Error('一行都没有(一行一段)');
|
|
12
|
+
return { segments };
|
|
13
|
+
},
|
|
14
|
+
assets(p) {
|
|
15
|
+
return p.segments.map((text, k) => ({ file: `${k + 1}.mp3`, text }));
|
|
16
|
+
},
|
|
17
|
+
doc: `### read — 点读
|
|
18
|
+
|
|
19
|
+
一行一段(一句诗、一句课文、一个单词;英文后面可以跟中文),孩子点哪段听哪段。原文、古诗、生词、单词表用它。
|
|
20
|
+
|
|
21
|
+
\`\`\`\`
|
|
22
|
+
\`\`\`read
|
|
23
|
+
床前明月光,疑是地上霜。
|
|
24
|
+
举头望明月,低头思故乡。
|
|
25
|
+
\`\`\`
|
|
26
|
+
\`\`\`\``,
|
|
27
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 场景卡(《drawtell接入与场景卡.md》§2):正文是课包 id,课包在 workspace 的 bundles/<id>/(drawtell build 的产物,scene-maker 作业出)。
|
|
3
|
+
* 解析器只认 id;题面、步数、有没有做好(ready)、缩略图是服务端下发时从课包现读的快照(src/server/scene-props.ts),
|
|
4
|
+
* 所以课包落地后卡自己变成可播,不用老师再说一遍。状态 = 孩子看到第几步、看完没;紧凑态「N 步 ▷」,舞台是 drawtell 播放器。
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { type CardKind } from './kind.ts';
|
|
8
|
+
export declare const BUNDLE_ID_RE: RegExp;
|
|
9
|
+
export declare const ScenePropsSchema: z.ZodObject<{
|
|
10
|
+
bundle: z.ZodString;
|
|
11
|
+
text: z.ZodOptional<z.ZodString>;
|
|
12
|
+
title: z.ZodOptional<z.ZodString>;
|
|
13
|
+
problem: z.ZodOptional<z.ZodString>;
|
|
14
|
+
steps: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
+
ready: z.ZodOptional<z.ZodBoolean>;
|
|
16
|
+
thumb: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type SceneProps = z.infer<typeof ScenePropsSchema>;
|
|
19
|
+
export declare const SceneStateSchema: z.ZodObject<{
|
|
20
|
+
step: z.ZodNumber;
|
|
21
|
+
done: z.ZodBoolean;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type SceneState = z.infer<typeof SceneStateSchema>;
|
|
24
|
+
export declare const scene: CardKind<SceneProps, SceneState>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 场景卡(《drawtell接入与场景卡.md》§2):正文是课包 id,课包在 workspace 的 bundles/<id>/(drawtell build 的产物,scene-maker 作业出)。
|
|
3
|
+
* 解析器只认 id;题面、步数、有没有做好(ready)、缩略图是服务端下发时从课包现读的快照(src/server/scene-props.ts),
|
|
4
|
+
* 所以课包落地后卡自己变成可播,不用老师再说一遍。状态 = 孩子看到第几步、看完没;紧凑态「N 步 ▷」,舞台是 drawtell 播放器。
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { bodyLines } from "./kind.js";
|
|
8
|
+
export const BUNDLE_ID_RE = /^[a-z0-9][a-z0-9-]{0,80}$/;
|
|
9
|
+
export const ScenePropsSchema = z.object({
|
|
10
|
+
/** 课包 id(bundles/<id>/) */
|
|
11
|
+
bundle: z.string().regex(BUNDLE_ID_RE),
|
|
12
|
+
/** 老师在 id 后面写的一句(课包还没到时孩子看到它) */
|
|
13
|
+
text: z.string().optional(),
|
|
14
|
+
/** 以下是下发时从课包读的快照;没到就没有 */
|
|
15
|
+
title: z.string().optional(),
|
|
16
|
+
problem: z.string().optional(),
|
|
17
|
+
/** 每步讲稿 */
|
|
18
|
+
steps: z.array(z.string()).optional(),
|
|
19
|
+
/** 课包在不在(scene.json + manifest.json 都在) */
|
|
20
|
+
ready: z.boolean().optional(),
|
|
21
|
+
/** 终帧缩略图(相对 workspace 根,/api/kid/image?p= 供图);没截过就没有 */
|
|
22
|
+
thumb: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
export const SceneStateSchema = z.object({
|
|
25
|
+
/** 看到第几步(1 起;0 = 还没开始) */
|
|
26
|
+
step: z.number().int().nonnegative(),
|
|
27
|
+
done: z.boolean(),
|
|
28
|
+
});
|
|
29
|
+
export const scene = {
|
|
30
|
+
name: 'scene',
|
|
31
|
+
props: ScenePropsSchema,
|
|
32
|
+
parse(body) {
|
|
33
|
+
const [first, ...rest] = bodyLines(body);
|
|
34
|
+
if (!first)
|
|
35
|
+
throw new Error('第一行要是课包 id');
|
|
36
|
+
if (first.startsWith('{'))
|
|
37
|
+
throw new Error('行内 JSON 场景还没接(下一步);先写课包 id');
|
|
38
|
+
if (!BUNDLE_ID_RE.test(first))
|
|
39
|
+
throw new Error(`课包 id 只能是小写字母、数字、连字符:${first}`);
|
|
40
|
+
const text = rest.join(' ').trim();
|
|
41
|
+
return { bundle: first, ...(text ? { text } : {}) };
|
|
42
|
+
},
|
|
43
|
+
state: SceneStateSchema,
|
|
44
|
+
describe(p, s) {
|
|
45
|
+
const total = p.steps?.length ?? 0;
|
|
46
|
+
if (!p.ready)
|
|
47
|
+
return '课包还没做好,孩子看不了';
|
|
48
|
+
if (s.done)
|
|
49
|
+
return `看完了(共 ${total} 步)`;
|
|
50
|
+
if (s.step <= 0)
|
|
51
|
+
return '还没开始看';
|
|
52
|
+
return `看到第 ${s.step} 步(共 ${total} 步)停在气口`;
|
|
53
|
+
},
|
|
54
|
+
doc: `### scene — 讲解动画(课包)
|
|
55
|
+
|
|
56
|
+
正文第一行是课包 id(bundles/<id>/,由 scene-maker 作业做出来;id 形如 \`2026-09-10-guilv\`),第二行可以写一句给孩子的话。
|
|
57
|
+
课包没做好之前孩子看到「图还在路上」,做好了卡自己变成可播,舞台里是逐笔画出来的讲解,自带讲稿与声音。
|
|
58
|
+
讲作业题、要画图才讲得清的题用它:回复里放这张卡 + 一句「我去把图画出来」,同时「## 转交」给 scene-maker(to: scene-maker;why: 题面与讲法要点;refs: 课包 id)。
|
|
59
|
+
讲稿里 \`[[play]]\` 锚到它,念到那句就把动画铺满播,播完接着念下一句。一天最多起两个作业(policy scenes.dailyMax)。
|
|
60
|
+
|
|
61
|
+
\`\`\`\`
|
|
62
|
+
\`\`\`scene
|
|
63
|
+
2026-09-10-guilv
|
|
64
|
+
我去把这道题画出来,画好了我们一起看。
|
|
65
|
+
\`\`\`
|
|
66
|
+
\`\`\`\``,
|
|
67
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** 文字卡:一段字;样式是修饰词(cover / note / quote / formula / step),不占种类。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { CardKind } from './kind.ts';
|
|
4
|
+
export declare const TEXT_STYLES: readonly ["cover", "note", "quote", "formula", "step"];
|
|
5
|
+
export type TextStyle = (typeof TEXT_STYLES)[number];
|
|
6
|
+
export declare const TextPropsSchema: z.ZodObject<{
|
|
7
|
+
style: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
step: "step";
|
|
9
|
+
cover: "cover";
|
|
10
|
+
note: "note";
|
|
11
|
+
formula: "formula";
|
|
12
|
+
quote: "quote";
|
|
13
|
+
}>>;
|
|
14
|
+
title: z.ZodOptional<z.ZodString>;
|
|
15
|
+
text: z.ZodString;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type TextProps = z.infer<typeof TextPropsSchema>;
|
|
18
|
+
export declare const text: CardKind<TextProps>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** 文字卡:一段字;样式是修饰词(cover / note / quote / formula / step),不占种类。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const TEXT_STYLES = ['cover', 'note', 'quote', 'formula', 'step'];
|
|
4
|
+
export const TextPropsSchema = z.object({
|
|
5
|
+
style: z.enum(TEXT_STYLES).optional(),
|
|
6
|
+
/** cover 的标题 / step 的名字 */
|
|
7
|
+
title: z.string().optional(),
|
|
8
|
+
/** 正文;cover 时是副标题(可空) */
|
|
9
|
+
text: z.string(),
|
|
10
|
+
});
|
|
11
|
+
export const text = {
|
|
12
|
+
name: 'text',
|
|
13
|
+
props: TextPropsSchema,
|
|
14
|
+
parse(body, mods) {
|
|
15
|
+
const style = mods.map((m) => m.toLowerCase()).find((m) => TEXT_STYLES.includes(m));
|
|
16
|
+
const trimmed = body.replace(/^\s*\n/, '').trimEnd();
|
|
17
|
+
if (!trimmed.trim())
|
|
18
|
+
throw new Error('正文是空的');
|
|
19
|
+
if (style === 'cover' || style === 'step') {
|
|
20
|
+
const nl = trimmed.indexOf('\n');
|
|
21
|
+
const title = (nl < 0 ? trimmed : trimmed.slice(0, nl)).trim();
|
|
22
|
+
const rest = nl < 0 ? '' : trimmed.slice(nl + 1).trim();
|
|
23
|
+
return { style, title, text: rest };
|
|
24
|
+
}
|
|
25
|
+
return { ...(style ? { style } : {}), text: trimmed.trim() };
|
|
26
|
+
},
|
|
27
|
+
doc: `### text — 一段字
|
|
28
|
+
|
|
29
|
+
\`\`\`text\` 后可加一个修饰词定样子:\`cover\`(封面:第一行标题,第二行副标题)、\`step\`(一步:第一行是这步的名字,下面是内容)、\`formula\`(算式,单独一行)、\`note\`(一句要记住的话)、\`quote\`(引一句原文);不加修饰就是普通一段。
|
|
30
|
+
|
|
31
|
+
\`\`\`\`
|
|
32
|
+
\`\`\`text cover
|
|
33
|
+
三角形的面积
|
|
34
|
+
两个一样的三角形拼成一个平行四边形
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
\`\`\`text formula
|
|
38
|
+
三角形面积 = 底 × 高 ÷ 2
|
|
39
|
+
\`\`\`
|
|
40
|
+
\`\`\`\``,
|
|
41
|
+
};
|
package/dist/cli/cert.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cotutor cert:用 mkcert 给这台机器签一张证书到 ~/.config/cotutor/certs/(cert.pem + key.pem),serve 看到就走 HTTPS。
|
|
3
|
+
* 为什么要:iPad Safari 的录音 / 语音识别只在安全上下文里给;局域网 IP 不是 localhost,得自签。
|
|
4
|
+
* 为什么放机器级而不是 workspace:签的是本机名与局域网 IP,与孩子无关;多个 workspace 共用一张,IP 变了只重签一次(2026-09-09 拍板)。
|
|
5
|
+
* 主机名 = 本机名、本机名.local、所有局域网 IPv4、localhost、127.0.0.1,再加 --host 给的。已有证书就覆盖(证书可再生)。不需要 workspace。
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { mkdir } from 'node:fs/promises';
|
|
9
|
+
import { hostname } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
import { lanAddresses } from "./serve.js";
|
|
13
|
+
import { USER_CERT_DIR, UsageError } from "./workspace.js";
|
|
14
|
+
const execFileP = promisify(execFile);
|
|
15
|
+
export async function makeCert(extraHosts = []) {
|
|
16
|
+
let caRoot;
|
|
17
|
+
try {
|
|
18
|
+
caRoot = (await execFileP('mkcert', ['-CAROOT'], { timeout: 8000 })).stdout.trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new UsageError('PATH 里没有 mkcert。装:brew install mkcert && mkcert -install(在 Mac 上信任一次根证书);iPad 端见 cert 命令输出的提示。');
|
|
22
|
+
}
|
|
23
|
+
const host = hostname();
|
|
24
|
+
const hosts = [...new Set([host, host.endsWith('.local') ? host : `${host}.local`, ...lanAddresses(), 'localhost', '127.0.0.1', ...extraHosts])];
|
|
25
|
+
const dir = USER_CERT_DIR;
|
|
26
|
+
await mkdir(dir, { recursive: true });
|
|
27
|
+
const cert = join(dir, 'cert.pem');
|
|
28
|
+
const key = join(dir, 'key.pem');
|
|
29
|
+
try {
|
|
30
|
+
await execFileP('mkcert', ['-cert-file', cert, '-key-file', key, ...hosts], { timeout: 60_000 });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
throw new UsageError(`mkcert 签证书失败:${err instanceof Error ? err.message : String(err)}\n先跑 mkcert -install,再重试。`);
|
|
34
|
+
}
|
|
35
|
+
return { cert, key, hosts, caRoot };
|
|
36
|
+
}
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -12,8 +12,11 @@ export interface DoctorReport {
|
|
|
12
12
|
source: RootSource | null;
|
|
13
13
|
checks: DoctorCheck[];
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
export declare function
|
|
15
|
+
/** 运行时的 CLI 名(run[0] 的 basename):决定链在 .claude 还是 .qwen 下 */
|
|
16
|
+
export declare function runtimeCli(run: readonly string[]): string;
|
|
17
|
+
/** 已知的环境坑,从 CLI 的输出里认出来给修复命令(错误信息即修复指南) */
|
|
18
|
+
export declare function explainLlmFailure(text: string, runtime: readonly string[]): string;
|
|
17
19
|
export declare function doctorWorkspace(override?: string, opts?: ResolveOptions & {
|
|
18
20
|
probeEnv?: boolean;
|
|
21
|
+
live?: boolean;
|
|
19
22
|
}): Promise<DoctorReport>;
|