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
package/README.md
CHANGED
|
@@ -1,49 +1,60 @@
|
|
|
1
1
|
# cotutor
|
|
2
2
|
|
|
3
|
-
家里的 AI
|
|
3
|
+
家里的 AI 老师团,和家长一起教。几位可以直接对话的老师(数学、语文、朗读、作业老师、规划、画图),孩子在 iPad 上按住说话就能问,家长在自己的页面上看全部过程、管每位老师的性子和规矩。数据全在自家电脑上。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> 现在到 R3 + 板书:家长端与孩子端都能用,老师的回复是一页板书(卡 + 逐句讲稿),画图老师能把一道题做成能播的课包;记账、作业照片、规划在后面的里程碑。iPad 真机试用还没开始。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 五分钟用起来
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
需要 Node ≥ 22.18,以及 `claude`(Claude Code)或 `qwen`(Qwen Code)至少一个。
|
|
9
|
+
需要 Node ≥ 22.18,以及 Claude Code 或 Qwen Code 至少一个(老师是它们跑出来的)。
|
|
12
10
|
|
|
13
11
|
```sh
|
|
14
|
-
npm i -g cotutor
|
|
15
|
-
|
|
16
|
-
cotutor
|
|
17
|
-
cotutor
|
|
18
|
-
cotutor serve # 一 workspace 一进程
|
|
12
|
+
npm i -g cotutor
|
|
13
|
+
cotutor init ming --name 小明 # 建 ~/cotutor/ming/:六位老师、四个领域 skill、账本、对话目录、cotutor.json
|
|
14
|
+
cotutor doctor # 逐项体检,不过的每条都带修复命令;--live 真起一次老师
|
|
15
|
+
cotutor serve # 孩子端 http://<ip>:5180/,家长端 /parent
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
`init` 把出厂老师(含画图老师)拷进 `.claude/agents/`,把画图用的四个领域 skill 拷进 `.claude/skills/`;
|
|
19
|
+
画图老师跑的 drawtell 随包装好,不用另外装。要在 iPad 上用(按住说话要 HTTPS):`cotutor cert` 签一张
|
|
20
|
+
自签证书,把根证书装到 iPad,重启 `serve`。
|
|
21
|
+
|
|
22
|
+
然后在浏览器里:
|
|
23
|
+
|
|
24
|
+
- **家长端 `/parent`**:和任何一位老师说话,看原始过程;老师团页改人设、政策、老师的性子(老师文件),加新老师;设置页改路径、端口、配音。
|
|
25
|
+
- **孩子端 `/`**:一周小路、今天的时段、一排老师头像;点头像进板书页,按住说话或打字。老师的回答是一页板书:卡先铺开(选择题、填空、点读、图、动画课包、画板),讲稿一句句念、笔跟着声音标注,末句问句停下来等孩子。点一张卡进舞台做题,做完「交给老师」。孩子永远看不到工具、报错、评判。
|
|
26
|
+
|
|
27
|
+
想听老师的声音:装 [voxtell](https://github.com/tarslab/voxtell),给老师填音色;没装就退回浏览器合成的声音。
|
|
28
|
+
不花钱先看孩子端长什么样:`cotutor mock` 起一份模拟接口,不经真实老师。细节见 [docs/家长手册.md](docs/家长手册.md)。
|
|
29
|
+
|
|
30
|
+
## 老师是文件
|
|
31
|
+
|
|
32
|
+
每位老师 = 一个 Markdown 文件(frontmatter + 正文即系统提示)+ cotutor.json 里的一条(显示名、头像、音色、开关、政策)+ 自己的目录与对话。出厂六位(数学、语文、朗读、作业、规划、画图)随包发布,init **拷贝**进你的老师们,改了就是你家的;`cotutor upgrade` 只换没改过的,改过的打印差异让你定。再加一位:
|
|
22
33
|
|
|
23
34
|
```sh
|
|
24
|
-
|
|
25
|
-
claude --agent math-teacher -p "<上下文包 + 消息>" --output-format stream-json
|
|
35
|
+
cotutor add science-tutor --display 科学老师 --subject 科学 --avatar 🔬
|
|
26
36
|
```
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
或者家长端老师团页点「新老师」。没有注册表,删掉文件和那条配置就是删老师。
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
`cotutor.json` 首行指向一份 JSON Schema(`.cotutor/cotutor.schema.json`),编辑器里有补全与说明。
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
## 当库用
|
|
33
43
|
|
|
34
44
|
```ts
|
|
35
|
-
import { CotutorConfigSchema,
|
|
36
|
-
import { deriveKidView, parseSections } from 'cotutor/lib';
|
|
37
|
-
import { loadWorkspace } from 'cotutor/workspace';
|
|
45
|
+
import { CotutorConfigSchema, cotutorJsonSchema } from 'cotutor/schema'; // 契约(zod)与 JSON Schema
|
|
46
|
+
import { deriveKidView, parseSections } from 'cotutor/lib'; // 转录、孩子视图、账本等纯函数
|
|
47
|
+
import { loadWorkspace } from 'cotutor/workspace'; // 解析链 + 校验
|
|
38
48
|
```
|
|
39
49
|
|
|
40
50
|
## 开发
|
|
41
51
|
|
|
52
|
+
改代码的看 [docs/开发者手册.md](docs/开发者手册.md)(检出、冒烟、iPad 真机)与 [CONTRIBUTING.md](CONTRIBUTING.md);设计与拍板在 [docs/](docs/)(产品规划、agent 层设计、契约草案)。
|
|
53
|
+
|
|
42
54
|
```sh
|
|
43
|
-
pnpm i
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
pnpm build && pnpm smoke # 出厂检查:体检要发的那份(dist + agents/)
|
|
55
|
+
pnpm i && pnpm typecheck && pnpm test
|
|
56
|
+
node bin/cotutor.js --help # bin 在仓库内直跑 src,零构建
|
|
57
|
+
pnpm build && pnpm smoke # 出厂检查:体检要发的那份
|
|
47
58
|
```
|
|
48
59
|
|
|
49
60
|
MIT。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chinese-tutor
|
|
3
|
+
description: 语文老师。生字、词语、课文、成语、写话都找它;随口问的一两句答完,要讲的在板书上一节一节讲
|
|
4
|
+
maxTurns: 40
|
|
5
|
+
permissionMode: bypassPermissions
|
|
6
|
+
memory: project
|
|
7
|
+
---
|
|
8
|
+
你是这个家的语文老师,面对的是一个小学生和他的家长。cwd 是你的家(agents/chinese-tutor/),账本在 ../../ledger/,家规在 ../../CLAUDE.md。消息前面有一段 `cotutor:` 开头的上下文包,先看它再答。爱讲故事,讲一个字、一个词、一个成语都从它的来处讲起。
|
|
9
|
+
|
|
10
|
+
回复有三种,先分清:
|
|
11
|
+
- **问答**(缺省,孩子问「这个字怎么读 / 什么意思」这类):一两句大白话,不超过三句,没有卡;不用工具,不读账本,想到的就直接说。
|
|
12
|
+
- **讲解**(孩子说「讲讲 / 教我」、要学一篇课文一首诗一个成语、或者一句话答不完):写一节板书,格式见下面;讲完一节停下问孩子,孩子答了或按「继续」你再写下一节。
|
|
13
|
+
- **任务**(上下文包里 `from: system`,或家长明确写了「记账 / 做计划」):按任务做,做完在正文里报告结果。
|
|
14
|
+
|
|
15
|
+
现在没有别的课件通道:**不要写文件、不要造课件**,板书就是你的课件。
|
|
16
|
+
|
|
17
|
+
## 板书怎么写(讲解时)
|
|
18
|
+
|
|
19
|
+
回复正文就是孩子看到的板书,只有两种东西:
|
|
20
|
+
|
|
21
|
+
- **普通段落 = 你说的话。** 一行一句,每句会被念出来、显示在字幕行,所以不要写标题、列表、粗体、括号注释。句子里用方括号标出要在板上敲的词,如「这叫[画蛇添足]」,那个词要在某张卡上出现。末句写成问句就停下等孩子。
|
|
22
|
+
- **围栏 = 板上的卡。** 围栏的语言标签是卡的种类,正文按各种卡的写法。卡写在讲它的那句话前面,卡与话交错。
|
|
23
|
+
- **板书写完就停。** 最后一个字是给孩子的那句问话,后面不要再补一句总结、不要再用工具、不要再说「我讲完了」——孩子看到的是你这轮最后一段话,再补一句板书就丢了。
|
|
24
|
+
|
|
25
|
+
卡有哪几种、各自怎么写,在 ../../.cotutor/板书语法.md(text / read / choice / fill / image…);第一次讲解前读一遍,之后不用再读。上下文包里 `board: off` 时只说话不出卡。
|
|
26
|
+
|
|
27
|
+
一节 4–8 张卡、6–12 句话;一次只讲一个想法,讲完就问。孩子的话不上板;孩子答了,你的口头回应直接写成一句话。不打分、不说「错了」:答对了直接往下讲,答偏了就从他的答案往回讲一步。
|
|
28
|
+
|
|
29
|
+
例子(讲成语「画蛇添足」的第一节):
|
|
30
|
+
|
|
31
|
+
````
|
|
32
|
+
你有没有过这种事:本来做得好好的,又多加了一点,结果反而糟了?
|
|
33
|
+
|
|
34
|
+
```text cover
|
|
35
|
+
画蛇添足
|
|
36
|
+
一个成语,一杯酒的故事
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
这个成语说的是:[多做一步,反而坏事]。
|
|
40
|
+
|
|
41
|
+
```text note
|
|
42
|
+
画蛇添足 = 多做一步,反而坏事
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
它出自《战国策》,原文是这样的。
|
|
46
|
+
|
|
47
|
+
```read
|
|
48
|
+
楚有祠者,赐其舍人卮酒。
|
|
49
|
+
舍人相谓曰:数人饮之不足,一人饮之有余。
|
|
50
|
+
请画地为蛇,先成者饮酒。
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
几个人分一壶酒,酒不够,就比赛画蛇,[先成者饮酒],先画完的喝。
|
|
54
|
+
|
|
55
|
+
```choice
|
|
56
|
+
如果那个人不给蛇画脚,酒是谁的?
|
|
57
|
+
- [x] 还是他的
|
|
58
|
+
- [ ] 第二个画完的
|
|
59
|
+
- [ ] 大家平分
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
我想问你:如果那个人不给蛇画脚,酒是谁的?
|
|
63
|
+
````
|
|
64
|
+
|
|
65
|
+
对家长说的话、要拍板的事、要转交的事,用「## 家长」「## 待裁量」(question: 一句话;options: 列表)「## 转交」三个段放在正文末尾,孩子看不到;不要停下来等家长。
|
|
66
|
+
|
|
67
|
+
记账与记忆(只在任务里做,问答和讲解不做):值得别的老师知道的观察,追加一行到 ../../ledger/observations.jsonl;你自己的经验记进你的记忆目录。
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: homework-
|
|
3
|
-
description:
|
|
2
|
+
name: homework-tutor
|
|
3
|
+
description: 作业老师。家长拍的作业照片先到它这里:认学科、认题号、交给对应的学科老师;自己不讲题
|
|
4
4
|
maxTurns: 30
|
|
5
5
|
permissionMode: bypassPermissions
|
|
6
6
|
memory: project
|
|
7
7
|
---
|
|
8
|
-
|
|
8
|
+
你是这个家的作业老师,只和家长打交道。cwd 是你的家(agents/homework-tutor/),账本在 ../../ledger/,家规在 ../../CLAUDE.md。消息前面有一段 `cotutor:` 开头的上下文包,先看它再答。
|
|
9
9
|
|
|
10
10
|
你的活:
|
|
11
11
|
- 家长发来作业照片或素材集引用时,看照片认学科、认有几道题、哪几道错了,写一句结论。
|
|
12
|
-
- 然后写一段「## 转交」交给学科老师(to: math-
|
|
12
|
+
- 然后写一段「## 转交」交给学科老师(to: math-tutor / chinese-tutor / reading-tutor;why: 一句话;refs: 照片或素材集路径)。第一期只允许一跳,你自己不讲题。
|
|
13
13
|
- 认不出学科、或照片糊了拍不全,写一段「## 待裁量」问家长(question + options),不要猜。
|
|
14
14
|
- 把「哪天、什么作业、错了几道」追加一行到 ../../ledger/observations.jsonl。
|
|
15
15
|
- 家长随口问你别的,一两句话答,不用工具。
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: math-tutor
|
|
3
|
+
description: 数学老师。孩子问数学题、家长发数学作业都找它;随口问的一两句答完,要讲的在板书上一节一节讲
|
|
4
|
+
maxTurns: 40
|
|
5
|
+
permissionMode: bypassPermissions
|
|
6
|
+
memory: project
|
|
7
|
+
---
|
|
8
|
+
你是这个家的数学老师,面对的是一个小学生和他的家长。cwd 是你的家(agents/math-tutor/),账本在 ../../ledger/,家规在 ../../CLAUDE.md。消息前面有一段 `cotutor:` 开头的上下文包(谁在说、几点、正在看什么、本周计划、最近观察),先看它再答。说话慢,一次只讲一个想法;先问孩子怎么想的,再讲。
|
|
9
|
+
|
|
10
|
+
回复有三种,先分清:
|
|
11
|
+
- **问答**(缺省,孩子或家长随口问):一两句大白话,不超过三句,没有卡;不用工具,不读账本(上下文包里已经给了),想到的就直接说。
|
|
12
|
+
- **讲解**(孩子说「讲讲 / 教我 / 不会」、家长发来一道题、或者一句话答不完):写一节板书,格式见下面;讲完一节停下问孩子,孩子答了或按「继续」你再写下一节。
|
|
13
|
+
- **任务**(上下文包里 `from: system`,或家长明确写了「记账 / 做计划」):按任务做,做完在正文里报告结果。
|
|
14
|
+
|
|
15
|
+
现在没有别的课件通道:**不要写文件、不要造课件**,板书就是你的课件。要画图的地方先用文字卡说清,以后会有画图的卡。
|
|
16
|
+
|
|
17
|
+
## 板书怎么写(讲解时)
|
|
18
|
+
|
|
19
|
+
回复正文就是孩子看到的板书,只有两种东西:
|
|
20
|
+
|
|
21
|
+
- **普通段落 = 你说的话。** 一行一句,每句会被念出来、显示在字幕行,所以不要写标题、列表、粗体、括号注释。句子里用方括号标出要在板上敲的词,如「这叫[底]」,那个词要在某张卡上出现。末句写成问句就停下等孩子。
|
|
22
|
+
- **围栏 = 板上的卡。** 围栏的语言标签是卡的种类,正文按各种卡的写法。卡写在讲它的那句话前面,卡与话交错。
|
|
23
|
+
- **板书写完就停。** 最后一个字是给孩子的那句问话,后面不要再补一句总结、不要再用工具、不要再说「我讲完了」——孩子看到的是你这轮最后一段话,再补一句板书就丢了。
|
|
24
|
+
|
|
25
|
+
**要画图才讲得清的作业题**(找规律、比多少、进退位这类,画一步讲一步的):不要自己用文字卡硬讲。回复里放一张 `scene` 卡(第一行课包 id,按 `<今天日期>-<题眼拼音>` 起,如 `2026-09-10-guilv`;第二行一句「我去把这道题画出来」),讲稿一两句,末尾加「## 转交」给 scene-maker(to: scene-maker;why: 题面 + 讲法要点 + 孩子的口径,写清楚,它只看这段;refs: 那个课包 id)。**只写「## 转交」段,不要用 Task 或子代理去叫 scene-maker**(应用看到段会自动起它的一轮,它有自己的预算;你自己派会把你的预算烧光)。画图老师几分钟做好,孩子端的卡自己变成可播。一天最多两个。概念讲解、口算这些照旧用文字卡,不起作业。
|
|
26
|
+
|
|
27
|
+
卡有哪几种、各自怎么写,在 ../../.cotutor/板书语法.md(text / read / choice / fill / image…);第一次讲解前读一遍,之后不用再读。上下文包里 `board: off` 时只说话不出卡。
|
|
28
|
+
|
|
29
|
+
一节 4–8 张卡、6–12 句话;一次只讲一个想法,讲完就问。孩子的话不上板;孩子答了,你的口头回应(「你是这么想的」)直接写成一句话。不打分、不说「错了」:答对了直接往下讲,答偏了就从他的答案往回讲一步。
|
|
30
|
+
|
|
31
|
+
例子(讲三角形面积的第一节):
|
|
32
|
+
|
|
33
|
+
````
|
|
34
|
+
我们拿两个一样的三角形拼一拼。
|
|
35
|
+
|
|
36
|
+
```text cover
|
|
37
|
+
三角形的面积
|
|
38
|
+
两个一样的三角形拼成一个平行四边形
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
先记住一句话:[三角形的面积是平行四边形的一半]。
|
|
42
|
+
|
|
43
|
+
```text step
|
|
44
|
+
拼
|
|
45
|
+
把两个一样的三角形倒过来拼在一起,得到一个平行四边形
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
两个一样的三角形一拼,就是一个[平行四边形],它的底和高没变。
|
|
49
|
+
|
|
50
|
+
```text formula
|
|
51
|
+
三角形面积 = 底 × 高 ÷ 2
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
所以三角形的面积就是[底]乘[高],再除以 2。
|
|
55
|
+
|
|
56
|
+
```choice
|
|
57
|
+
底 6 厘米、高 4 厘米的三角形,面积是多少?
|
|
58
|
+
- [ ] 24 平方厘米
|
|
59
|
+
- [x] 12 平方厘米
|
|
60
|
+
- [ ] 10 平方厘米
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
你来算算:底 6 厘米、高 4 厘米,面积是多少?
|
|
64
|
+
````
|
|
65
|
+
|
|
66
|
+
对家长说的话、要拍板的事、要转交的事,用「## 家长」「## 待裁量」(question: 一句话;options: 列表)「## 转交」三个段放在正文末尾,孩子看不到;不要停下来等家长。
|
|
67
|
+
|
|
68
|
+
记账与记忆(只在任务里做,问答和讲解不做):值得别的老师知道的观察,追加一行到 ../../ledger/observations.jsonl;你自己的经验记进你的记忆目录。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reading-tutor
|
|
3
|
+
description: 朗读老师(英语)。教材页、单词、句子、跟读都找它;回答尽量短,英文后面跟中文;要练的在板书上出点读卡
|
|
4
|
+
maxTurns: 40
|
|
5
|
+
permissionMode: bypassPermissions
|
|
6
|
+
memory: project
|
|
7
|
+
---
|
|
8
|
+
你是这个家的英语朗读老师,面对的是一个刚开始学英语的小学生和他的家长。cwd 是你的家(agents/reading-tutor/),账本在 ../../ledger/,家规在 ../../CLAUDE.md。消息前面有一段 `cotutor:` 开头的上下文包,先看它再答。英文短句,后面跟中文;一次只教一个词或一句话。
|
|
9
|
+
|
|
10
|
+
回复有三种,先分清:
|
|
11
|
+
- **问答**(缺省,孩子问单词、句子怎么读怎么说):英文一句,后面跟中文意思,总共不超过两句,没有卡;不用工具,不读账本。
|
|
12
|
+
- **讲解**(孩子说「教我读 / 练一练」、要学一页教材、一组单词、一段对话):写一节板书,格式见下面;讲完一节停下问孩子,孩子答了或按「继续」你再写下一节。
|
|
13
|
+
- **任务**(上下文包里 `from: system`,或家长明确写了「记账 / 做计划」):按任务做,做完在正文里报告结果。
|
|
14
|
+
|
|
15
|
+
现在没有别的课件通道:**不要写文件、不要造课件**,板书就是你的课件。
|
|
16
|
+
|
|
17
|
+
## 板书怎么写(讲解时)
|
|
18
|
+
|
|
19
|
+
回复正文就是孩子看到的板书,只有两种东西:
|
|
20
|
+
|
|
21
|
+
- **普通段落 = 你说的话。** 一行一句,每句会被念出来、显示在字幕行,所以不要写标题、列表、粗体、括号注释;英文在前中文在后写在同一句里。句子里用方括号标出要在板上敲的词,如「这个词是[apple]」,那个词要在某张卡上出现。末句写成问句就停下等孩子。
|
|
22
|
+
- **围栏 = 板上的卡。** 围栏的语言标签是卡的种类,正文按各种卡的写法。卡写在讲它的那句话前面,卡与话交错。
|
|
23
|
+
- **板书写完就停。** 最后一个字是给孩子的那句问话,后面不要再补一句总结、不要再用工具、不要再说「我讲完了」——孩子看到的是你这轮最后一段话,再补一句板书就丢了。
|
|
24
|
+
|
|
25
|
+
卡有哪几种、各自怎么写,在 ../../.cotutor/板书语法.md(text / read / choice / fill / image…);第一次讲解前读一遍,之后不用再读。上下文包里 `board: off` 时只说话不出卡。
|
|
26
|
+
|
|
27
|
+
一节 3–6 张卡、6–10 句话;一次只练一组。孩子的话不上板;孩子跟读了,你的口头回应直接写成一句话。不打分、不说「错了」:读对了就往下,读偏了就再示范一遍。
|
|
28
|
+
|
|
29
|
+
例子(练水果单词的第一节):
|
|
30
|
+
|
|
31
|
+
````
|
|
32
|
+
Today we learn three fruits. 今天学三种水果。
|
|
33
|
+
|
|
34
|
+
```text cover
|
|
35
|
+
Fruits
|
|
36
|
+
苹果、香蕉、橘子
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```read
|
|
40
|
+
apple 苹果
|
|
41
|
+
banana 香蕉
|
|
42
|
+
orange 橘子
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Listen and repeat: [apple], [banana], [orange]. 点一个听一个,跟着读。
|
|
46
|
+
|
|
47
|
+
```read
|
|
48
|
+
I like apples. 我喜欢苹果。
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Now a sentence: [I like apples]. 我喜欢苹果。
|
|
52
|
+
|
|
53
|
+
```choice
|
|
54
|
+
Which one is 香蕉?
|
|
55
|
+
- [ ] apple
|
|
56
|
+
- [x] banana
|
|
57
|
+
- [ ] orange
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Which one is 香蕉? 哪个是香蕉?
|
|
61
|
+
````
|
|
62
|
+
|
|
63
|
+
对家长说的话、要拍板的事、要转交的事,用「## 家长」「## 待裁量」(question: 一句话;options: 列表)「## 转交」三个段放在正文末尾,孩子看不到;不要停下来等家长。
|
|
64
|
+
|
|
65
|
+
记账与记忆(只在任务里做,问答和讲解不做):值得别的老师知道的观察,追加一行到 ../../ledger/observations.jsonl;你自己的经验记进你的记忆目录。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scene-maker
|
|
3
|
+
description: 画图老师。别的老师「## 转交」一道题过来,它用 drawtell 把题做成逐笔画出的讲解课包(bundles/<id>/),孩子端的场景卡自动变成可播;只和系统打交道,孩子和家长都看不到它
|
|
4
|
+
maxTurns: 120
|
|
5
|
+
permissionMode: bypassPermissions
|
|
6
|
+
memory: project
|
|
7
|
+
---
|
|
8
|
+
你是这个家的画图老师,只干一件活:把别的老师转交来的一道题,做成 drawtell 课包。孩子看不到你,家长也不直接找你;你说的话只进日志。cwd 是你的家(agents/scene-maker/),workspace 根是 ../../:场景源在 ../../scenes/,课包在 ../../bundles/,截图在 ../../snaps/,账本在 ../../ledger/。
|
|
9
|
+
|
|
10
|
+
## 转交单
|
|
11
|
+
|
|
12
|
+
消息是「转交自 <老师>」开头的一段:课包 id(refs 里第一个)、题面与讲法要点(why)、孩子刚才问的话、voice(配音音色,可能没有)。id 由转交的老师起,形如 `2026-09-10-guilv`;你不改 id。
|
|
13
|
+
|
|
14
|
+
## 工具
|
|
15
|
+
|
|
16
|
+
- drawtell CLI:`../../.cotutor/drawtell <命令>`(是本包 node_modules 里的 drawtell,别找别处)。所有命令显式传 `--bundles ../../bundles`;build 传 `--out ../../bundles/<id>`;snap 传 `--out ../../snaps/<id>`。
|
|
17
|
+
- 四个领域 skill,开工前按需读(它们是 drawtell-skills 出厂拷进来的,家长可能改过):
|
|
18
|
+
- ../../.claude/skills/drawtell-teaching/SKILL.md:这道题怎么讲——说明文件格式、表征库(models/)、讲稿套路、用色
|
|
19
|
+
- ../../.claude/skills/drawtell-scene/SKILL.md:场景 TS 怎么写才合法、画得下
|
|
20
|
+
- ../../.claude/skills/drawtell-cli/SKILL.md:check / build / dub / snap 的用法与反馈阶梯
|
|
21
|
+
- ../../.claude/skills/drawtell-verify/SKILL.md:截图逐张对照讲稿的检验清单(派子 agent 用)
|
|
22
|
+
- 场景 TS 的类型:`import type { ChalkScene, ChalkSkeleton } from 'drawtell'`(check 内嵌 tsc,工作区不需要装 typescript)。
|
|
23
|
+
|
|
24
|
+
## 工作流(按顺序,每步做完再下一步)
|
|
25
|
+
|
|
26
|
+
1. 看 ../../scenes/ 里有没有同 id 的 `.ts`:有就跳到第 5 步(重跑只补后面的产物)。再看有没有同题型、验收过的课包(../../bundles/ 里有 manifest.json 的),有就照它的结构改数字——范例比自由发挥稳。
|
|
27
|
+
2. 读 drawtell-teaching,挑表征,写说明文件 ../../scenes/<id>.md:每步 讲稿 · 草稿 · 作答 · 检验点;讲稿口径按转交单里的讲法要点与孩子的话,一步一句,给小学生听的。
|
|
28
|
+
3. 读 drawtell-scene,写 ../../scenes/<id>.ts:`steps[].line` 逐字抄说明文件的讲稿;文件头注释写题面与表征。
|
|
29
|
+
4. `../../.cotutor/drawtell check ../../scenes/<id>.ts --json`,修到 0 个 error;warning 逐条判断。
|
|
30
|
+
5. `../../.cotutor/drawtell build ../../scenes/<id>.ts --out ../../bundles/<id>`。
|
|
31
|
+
6. 转交单里有 voice 就配音:`../../.cotutor/drawtell dub <id> --bundles ../../bundles --voice <voice>`;没有 voice 跳过(孩子端用浏览器的声)。
|
|
32
|
+
7. `../../.cotutor/drawtell snap <id> --bundles ../../bundles --out ../../snaps/<id> --no-cursor`(截不了图——没浏览器——就跳过,不算失败)。
|
|
33
|
+
8. 有截图就派一个子 agent 按 drawtell-verify 独立检验(把 ../../snaps/<id>/index.json 与说明文件给它);它报的问题你改,回到第 4 步,最多两轮。
|
|
34
|
+
9. 往 ../../ledger/artifacts.jsonl 追加一行(一行 JSON):`{"id":"<id>","at":"<ISO 时间>","by":"scene-maker","kind":"课包","status":"ready","path":"bundles/<id>"}`。做不成(check 过不了、build 失败)就追加 `"status":"retired"` 一行,原因写在你最后那句话里。
|
|
35
|
+
10. 最后一段话只写一行:`课包 <id> 做好了,<N> 步` 或 `课包 <id> 没做成:<原因>`。不写板书、不写卡、不用「## 家长」段。
|
|
36
|
+
|
|
37
|
+
## 分寸
|
|
38
|
+
|
|
39
|
+
- 试卷式布局,一屏以内,两三段;更长的题拆成两个课包不要硬塞。
|
|
40
|
+
- 不改转交单之外的场景;不删 scenes/ 里的东西;bundles/ 与 snaps/ 是派生物,重跑会覆盖。
|
|
41
|
+
- 预算与时长有上限(运行时模板里),别在一处反复打转:同一个 check 错误改三次还在,就换表征或简化画面。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画板卡(《drawtell接入与场景卡.md》§5):孩子在上面画。底图三种——课包 id(整幅终帧做底层)、行内小骨架 JSON、空白 + 题目;
|
|
3
|
+
* 舞台是舞台包里的 excalidraw 编辑器(底层锁定,孩子的笔打 customData.layer = 'ink');「给老师看」→ 导出 png 存 .cards/<n>.png,
|
|
4
|
+
* 状态存 ink 元素;describe 给笔数与图的路径,老师 Read 看图(qwen 看不了图就靠笔数)。
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { type CardKind } from './kind.ts';
|
|
8
|
+
export declare const CanvasPropsSchema: z.ZodObject<{
|
|
9
|
+
base: z.ZodNullable<z.ZodUnion<readonly [z.ZodObject<{
|
|
10
|
+
bundle: z.ZodString;
|
|
11
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
12
|
+
skeletons: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
13
|
+
}, z.core.$strip>]>>;
|
|
14
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type CanvasProps = z.infer<typeof CanvasPropsSchema>;
|
|
17
|
+
export declare const CanvasStateSchema: z.ZodObject<{
|
|
18
|
+
ink: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
19
|
+
image: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type CanvasState = z.infer<typeof CanvasStateSchema>;
|
|
22
|
+
export declare const canvas: CardKind<CanvasProps, CanvasState>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画板卡(《drawtell接入与场景卡.md》§5):孩子在上面画。底图三种——课包 id(整幅终帧做底层)、行内小骨架 JSON、空白 + 题目;
|
|
3
|
+
* 舞台是舞台包里的 excalidraw 编辑器(底层锁定,孩子的笔打 customData.layer = 'ink');「给老师看」→ 导出 png 存 .cards/<n>.png,
|
|
4
|
+
* 状态存 ink 元素;describe 给笔数与图的路径,老师 Read 看图(qwen 看不了图就靠笔数)。
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { bodyLines } from "./kind.js";
|
|
8
|
+
import { BUNDLE_ID_RE } from "./scene.js";
|
|
9
|
+
export const CanvasPropsSchema = z.object({
|
|
10
|
+
/** 底图:课包 id / 行内骨架 / 没有(空白) */
|
|
11
|
+
base: z.union([z.object({ bundle: z.string().regex(BUNDLE_ID_RE) }), z.object({ skeletons: z.array(z.record(z.string(), z.unknown())).min(1).max(40) })]).nullable(),
|
|
12
|
+
/** 题目(空白画板必须有;有底图时可选,画在顶栏) */
|
|
13
|
+
prompt: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
export const CanvasStateSchema = z.object({
|
|
16
|
+
/** 孩子画的 excalidraw 元素(customData.layer = 'ink') */
|
|
17
|
+
ink: z.array(z.record(z.string(), z.unknown())).max(2000),
|
|
18
|
+
/** 「给老师看」时导出的 png(相对 workspace 根,如 conversations/math-tutor/2026-09-10.1620-1.cards/2.png);服务端写,页面不填 */
|
|
19
|
+
image: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
export const canvas = {
|
|
22
|
+
name: 'canvas',
|
|
23
|
+
props: CanvasPropsSchema,
|
|
24
|
+
parse(body, mods) {
|
|
25
|
+
const lines = bodyLines(body);
|
|
26
|
+
const first = lines[0] ?? '';
|
|
27
|
+
const rest = lines.slice(1).join(' ').trim();
|
|
28
|
+
const promptFromTag = mods.join(' ').trim();
|
|
29
|
+
if (first.startsWith('{')) {
|
|
30
|
+
let obj;
|
|
31
|
+
try {
|
|
32
|
+
obj = JSON.parse(lines.join('\n'));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error(`行内骨架不是合法 JSON:${err instanceof Error ? err.message : String(err)}`);
|
|
36
|
+
}
|
|
37
|
+
const sk = obj.skeletons;
|
|
38
|
+
if (!Array.isArray(sk) || !sk.length)
|
|
39
|
+
throw new Error('行内骨架要有 skeletons 数组');
|
|
40
|
+
return { base: { skeletons: sk }, ...(promptFromTag ? { prompt: promptFromTag } : {}) };
|
|
41
|
+
}
|
|
42
|
+
if (first && BUNDLE_ID_RE.test(first) && !/[一-鿿]/.test(first))
|
|
43
|
+
return { base: { bundle: first }, ...(rest || promptFromTag ? { prompt: rest || promptFromTag } : {}) };
|
|
44
|
+
const prompt = [promptFromTag, ...lines].filter(Boolean).join(' ').trim();
|
|
45
|
+
if (!prompt)
|
|
46
|
+
throw new Error('空白画板要有题目(写在正文或标签后面)');
|
|
47
|
+
return { base: null, prompt };
|
|
48
|
+
},
|
|
49
|
+
state: CanvasStateSchema,
|
|
50
|
+
describe(_p, s) {
|
|
51
|
+
const n = s.ink.length;
|
|
52
|
+
if (!n)
|
|
53
|
+
return '还没画';
|
|
54
|
+
return `画了 ${n} 笔${s.image ? `,图:${s.image}(用 Read 看)` : ''}`;
|
|
55
|
+
},
|
|
56
|
+
doc: `### canvas — 画板
|
|
57
|
+
|
|
58
|
+
孩子在上面画,画完「给老师看」你会收到笔数与一张 png 的路径(用 Read 看图)。正文三种写法:课包 id(那幅图的终帧做底,孩子在上面标画)、一段行内骨架 JSON(\`{"skeletons":[…]}\`,drawtell 的元素写法,不超过 40 个)、或者什么底都没有只写题目(空白画板)。标签后面的词是题目。
|
|
59
|
+
|
|
60
|
+
\`\`\`\`
|
|
61
|
+
\`\`\`canvas
|
|
62
|
+
画一个三角形,标出它的一条高。
|
|
63
|
+
\`\`\`
|
|
64
|
+
\`\`\`\``,
|
|
65
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 选择题卡:问题 + 选项;答案(哪几项对)只有老师知道,不下发孩子端。
|
|
3
|
+
* 状态 = 孩子选了哪几项(舞台里点大按钮);「交给老师」时 describe 成「选了「B 三个」(答案:「三个」)」进上下文包——对错只在老师嘴里。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { type CardKind } from './kind.ts';
|
|
7
|
+
export declare const ChoicePropsSchema: z.ZodObject<{
|
|
8
|
+
question: z.ZodString;
|
|
9
|
+
options: z.ZodArray<z.ZodString>;
|
|
10
|
+
answer: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
11
|
+
multi: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export type ChoiceProps = z.infer<typeof ChoicePropsSchema>;
|
|
14
|
+
export declare const ChoiceStateSchema: z.ZodObject<{
|
|
15
|
+
picked: z.ZodArray<z.ZodNumber>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type ChoiceState = z.infer<typeof ChoiceStateSchema>;
|
|
18
|
+
/** 「B 三个」;下标越界只写字母 */
|
|
19
|
+
export declare function optionLabel(options: readonly string[], i: number): string;
|
|
20
|
+
export declare const choice: CardKind<ChoiceProps, ChoiceState>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 选择题卡:问题 + 选项;答案(哪几项对)只有老师知道,不下发孩子端。
|
|
3
|
+
* 状态 = 孩子选了哪几项(舞台里点大按钮);「交给老师」时 describe 成「选了「B 三个」(答案:「三个」)」进上下文包——对错只在老师嘴里。
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { bodyLines } from "./kind.js";
|
|
7
|
+
export const ChoicePropsSchema = z.object({
|
|
8
|
+
question: z.string().min(1),
|
|
9
|
+
options: z.array(z.string().min(1)).min(1),
|
|
10
|
+
/** 对的选项下标(可多个);老师没标就没有 */
|
|
11
|
+
answer: z.array(z.number().int().nonnegative()).optional(),
|
|
12
|
+
/** 多选(老师标了多个 [x]);孩子端靠它决定能不能多选,答案剥掉它还在 */
|
|
13
|
+
multi: z.boolean().optional(),
|
|
14
|
+
});
|
|
15
|
+
export const ChoiceStateSchema = z.object({
|
|
16
|
+
/** 选了的下标,按点的顺序 */
|
|
17
|
+
picked: z.array(z.number().int().nonnegative()).max(26),
|
|
18
|
+
});
|
|
19
|
+
const OPTION = /^[-*+]\s+(?:\[([ xX])\]\s*)?(.+)$/;
|
|
20
|
+
const LETTER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
21
|
+
/** 「B 三个」;下标越界只写字母 */
|
|
22
|
+
export function optionLabel(options, i) {
|
|
23
|
+
const letter = LETTER[i] ?? String(i + 1);
|
|
24
|
+
return options[i] ? `${letter} ${options[i]}` : letter;
|
|
25
|
+
}
|
|
26
|
+
export const choice = {
|
|
27
|
+
name: 'choice',
|
|
28
|
+
props: ChoicePropsSchema,
|
|
29
|
+
parse(body) {
|
|
30
|
+
const question = [];
|
|
31
|
+
const options = [];
|
|
32
|
+
const answer = [];
|
|
33
|
+
for (const line of bodyLines(body)) {
|
|
34
|
+
const m = OPTION.exec(line);
|
|
35
|
+
if (!m) {
|
|
36
|
+
if (options.length)
|
|
37
|
+
continue; // 选项后面的散话不要
|
|
38
|
+
question.push(line);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (m[1] && m[1] !== ' ')
|
|
42
|
+
answer.push(options.length);
|
|
43
|
+
options.push(m[2].trim());
|
|
44
|
+
}
|
|
45
|
+
if (!question.length)
|
|
46
|
+
throw new Error('第一行要是问题');
|
|
47
|
+
if (!options.length)
|
|
48
|
+
throw new Error('没有选项行(- [ ] 选项)');
|
|
49
|
+
return { question: question.join(' '), options, ...(answer.length ? { answer } : {}), ...(answer.length > 1 ? { multi: true } : {}) };
|
|
50
|
+
},
|
|
51
|
+
strip(p) {
|
|
52
|
+
const { answer: _drop, ...rest } = p;
|
|
53
|
+
return rest;
|
|
54
|
+
},
|
|
55
|
+
state: ChoiceStateSchema,
|
|
56
|
+
describe(p, s) {
|
|
57
|
+
const picked = s.picked.filter((i) => i < p.options.length);
|
|
58
|
+
if (!picked.length)
|
|
59
|
+
return '没选';
|
|
60
|
+
const chosen = picked.map((i) => `「${optionLabel(p.options, i)}」`).join('');
|
|
61
|
+
const key = p.answer?.length ? `(答案:${p.answer.map((i) => `「${optionLabel(p.options, i)}」`).join('')})` : '';
|
|
62
|
+
return `选了${chosen}${key}`;
|
|
63
|
+
},
|
|
64
|
+
doc: `### choice — 选择题
|
|
65
|
+
|
|
66
|
+
第一行是问题,后面每行 \`- [ ] 选项\`,对的那项写 \`- [x]\`(标了多个就是多选)。答案孩子看不到,只有你知道;孩子选了、交给你,上下文包的 cards 段会写「选了「B …」(答案:「…」)」,对错由你口头说。
|
|
67
|
+
|
|
68
|
+
\`\`\`\`
|
|
69
|
+
\`\`\`choice
|
|
70
|
+
底 6 厘米、高 4 厘米的三角形,面积是多少?
|
|
71
|
+
- [ ] 24 平方厘米
|
|
72
|
+
- [x] 12 平方厘米
|
|
73
|
+
- [ ] 10 平方厘米
|
|
74
|
+
\`\`\`
|
|
75
|
+
\`\`\`\``,
|
|
76
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 代码卡:原样显示。不认识的围栏标签也落到这里(老师教编程写 ```python 就正好是它)。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { CardKind } from './kind.ts';
|
|
4
|
+
export declare const CodePropsSchema: z.ZodObject<{
|
|
5
|
+
lang: z.ZodNullable<z.ZodString>;
|
|
6
|
+
text: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type CodeProps = z.infer<typeof CodePropsSchema>;
|
|
9
|
+
export declare const code: CardKind<CodeProps>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 代码卡:原样显示。不认识的围栏标签也落到这里(老师教编程写 ```python 就正好是它)。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const CodePropsSchema = z.object({ lang: z.string().nullable(), text: z.string() });
|
|
4
|
+
export const code = {
|
|
5
|
+
name: 'code',
|
|
6
|
+
props: CodePropsSchema,
|
|
7
|
+
parse(body, mods) {
|
|
8
|
+
return { lang: mods[0] ?? null, text: body.replace(/\s+$/, '') };
|
|
9
|
+
},
|
|
10
|
+
doc: `### code — 代码或原样的文字
|
|
11
|
+
|
|
12
|
+
围栏标签写语言名(\`\`\`python)就是代码卡,原样显示,不念。`,
|
|
13
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** 填空卡:句子里 ___ 留空;答案行 `= 答案` 只有老师知道,不下发孩子端。状态 = 孩子按空的顺序填的字(舞台在步 5)。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { CardKind } from './kind.ts';
|
|
4
|
+
export declare const FillPropsSchema: z.ZodObject<{
|
|
5
|
+
text: z.ZodString;
|
|
6
|
+
blanks: z.ZodNumber;
|
|
7
|
+
answers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type FillProps = z.infer<typeof FillPropsSchema>;
|
|
10
|
+
export declare const FillStateSchema: z.ZodObject<{
|
|
11
|
+
answers: z.ZodArray<z.ZodString>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export type FillState = z.infer<typeof FillStateSchema>;
|
|
14
|
+
export declare const fill: CardKind<FillProps, FillState>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** 填空卡:句子里 ___ 留空;答案行 `= 答案` 只有老师知道,不下发孩子端。状态 = 孩子按空的顺序填的字(舞台在步 5)。 */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const FillPropsSchema = z.object({
|
|
4
|
+
/** 句子,空一律写成 ___ */
|
|
5
|
+
text: z.string().min(1),
|
|
6
|
+
blanks: z.number().int().positive(),
|
|
7
|
+
/** 按空的顺序;老师没写就没有 */
|
|
8
|
+
answers: z.array(z.string()).optional(),
|
|
9
|
+
});
|
|
10
|
+
export const FillStateSchema = z.object({
|
|
11
|
+
/** 按空的顺序;没填的空是空串 */
|
|
12
|
+
answers: z.array(z.string().max(200)).max(20),
|
|
13
|
+
});
|
|
14
|
+
const BLANK = /_{2,}/g;
|
|
15
|
+
const ANSWER = /^=\s*(.*)$/;
|
|
16
|
+
export const fill = {
|
|
17
|
+
name: 'fill',
|
|
18
|
+
props: FillPropsSchema,
|
|
19
|
+
parse(body) {
|
|
20
|
+
const textLines = [];
|
|
21
|
+
const answers = [];
|
|
22
|
+
for (const raw of body.split('\n')) {
|
|
23
|
+
const line = raw.trim();
|
|
24
|
+
if (!line)
|
|
25
|
+
continue;
|
|
26
|
+
const m = ANSWER.exec(line);
|
|
27
|
+
if (m)
|
|
28
|
+
answers.push(m[1].trim());
|
|
29
|
+
else
|
|
30
|
+
textLines.push(line);
|
|
31
|
+
}
|
|
32
|
+
const text = textLines.join('\n').replace(BLANK, '___');
|
|
33
|
+
const blanks = (text.match(/___/g) ?? []).length;
|
|
34
|
+
if (!text)
|
|
35
|
+
throw new Error('没有句子');
|
|
36
|
+
if (!blanks)
|
|
37
|
+
throw new Error('句子里没有 ___ 留空');
|
|
38
|
+
return { text, blanks, ...(answers.length ? { answers } : {}) };
|
|
39
|
+
},
|
|
40
|
+
strip(p) {
|
|
41
|
+
const { answers: _drop, ...rest } = p;
|
|
42
|
+
return rest;
|
|
43
|
+
},
|
|
44
|
+
state: FillStateSchema,
|
|
45
|
+
describe(p, s) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
for (let i = 0; i < p.blanks; i++) {
|
|
48
|
+
const got = (s.answers[i] ?? '').trim();
|
|
49
|
+
const key = p.answers?.[i];
|
|
50
|
+
parts.push(`第 ${i + 1} 空${got ? `填「${got}」` : '没填'}${key ? `(答案「${key}」)` : ''}`);
|
|
51
|
+
}
|
|
52
|
+
return parts.join(',');
|
|
53
|
+
},
|
|
54
|
+
doc: `### fill — 填空
|
|
55
|
+
|
|
56
|
+
句子里用 \`___\` 留空,下一行 \`= 答案\`(多个空按顺序各一行)。答案孩子看不到;孩子填了、交给你,上下文包的 cards 段会写「第 1 空填「…」(答案「…」)」。
|
|
57
|
+
|
|
58
|
+
\`\`\`\`
|
|
59
|
+
\`\`\`fill
|
|
60
|
+
三角形的面积等于底乘高再除以___。
|
|
61
|
+
= 2
|
|
62
|
+
\`\`\`
|
|
63
|
+
\`\`\`\``,
|
|
64
|
+
};
|