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.
Files changed (209) hide show
  1. package/LICENSE +9 -7
  2. package/NOTICE +3 -3
  3. package/README.md +65 -18
  4. package/avatars/chinese.webp +0 -0
  5. package/avatars/english.webp +0 -0
  6. package/avatars/math.webp +0 -0
  7. package/bin/cotutor.js +13 -3
  8. package/cards/photo/card.css +10 -3
  9. package/cards/photo/card.md +2 -2
  10. package/cards/tutor/card.css +13 -0
  11. package/cards/tutor/card.md +2 -1
  12. package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
  13. package/dist/app/activity-eval.js +3 -2
  14. package/dist/app/cli/activities.d.ts +8 -1
  15. package/dist/app/cli/activities.js +42 -4
  16. package/dist/app/cli/cert.d.ts +11 -0
  17. package/dist/app/cli/cert.js +36 -9
  18. package/dist/app/cli/chalk/browser.d.ts +2 -2
  19. package/dist/app/cli/chalk/browser.js +22 -7
  20. package/dist/app/cli/chalk/main.js +3 -2
  21. package/dist/app/cli/doctor.d.ts +11 -5
  22. package/dist/app/cli/doctor.js +156 -181
  23. package/dist/app/cli/firewall.d.ts +24 -0
  24. package/dist/app/cli/firewall.js +79 -0
  25. package/dist/app/cli/init.js +8 -5
  26. package/dist/app/cli/main.js +379 -27
  27. package/dist/app/cli/migrate.d.ts +2 -11
  28. package/dist/app/cli/migrate.js +1 -59
  29. package/dist/app/cli/port-holder.d.ts +16 -0
  30. package/dist/app/cli/port-holder.js +34 -0
  31. package/dist/app/cli/port.d.ts +37 -0
  32. package/dist/app/cli/port.js +116 -0
  33. package/dist/app/cli/sandbox.d.ts +25 -0
  34. package/dist/app/cli/sandbox.js +166 -0
  35. package/dist/app/cli/serve-log.d.ts +13 -0
  36. package/dist/app/cli/serve-log.js +90 -0
  37. package/dist/app/cli/serve.d.ts +18 -4
  38. package/dist/app/cli/serve.js +78 -8
  39. package/dist/app/cli/service-windows.d.ts +50 -0
  40. package/dist/app/cli/service-windows.js +117 -0
  41. package/dist/app/cli/service.d.ts +152 -0
  42. package/dist/app/cli/service.js +443 -0
  43. package/dist/app/cli/skeleton.d.ts +1 -1
  44. package/dist/app/cli/skeleton.js +13 -13
  45. package/dist/app/cli/skills.d.ts +5 -1
  46. package/dist/app/cli/skills.js +14 -4
  47. package/dist/app/cli/tray-icons.d.ts +11 -0
  48. package/dist/app/cli/tray-icons.js +87 -0
  49. package/dist/app/cli/tray-ids.d.ts +6 -0
  50. package/dist/app/cli/tray-ids.js +6 -0
  51. package/dist/app/cli/tray-mac.d.ts +8 -0
  52. package/dist/app/cli/tray-mac.js +140 -0
  53. package/dist/app/cli/tray-run.d.ts +1 -0
  54. package/dist/app/cli/tray-run.js +42 -0
  55. package/dist/app/cli/tray-windows.d.ts +5 -0
  56. package/dist/app/cli/tray-windows.js +166 -0
  57. package/dist/app/cli/tray.d.ts +236 -0
  58. package/dist/app/cli/tray.js +670 -0
  59. package/dist/app/cli/usage.d.ts +1 -1
  60. package/dist/app/cli/usage.js +11 -6
  61. package/dist/app/dictation/route.js +3 -2
  62. package/dist/app/http/app.d.ts +5 -10
  63. package/dist/app/http/app.js +47 -98
  64. package/dist/app/http/avatars.d.ts +17 -0
  65. package/dist/app/http/avatars.js +35 -0
  66. package/dist/app/http/dev-tutor-files.js +1 -1
  67. package/dist/app/http/parent-home.d.ts +14 -0
  68. package/dist/app/http/parent-home.js +69 -6
  69. package/dist/app/http/parent-look.d.ts +2 -0
  70. package/dist/app/http/parent-look.js +151 -0
  71. package/dist/app/http/raw-view.js +3 -3
  72. package/dist/app/http/voice-preview.d.ts +4 -0
  73. package/dist/app/http/voice-preview.js +34 -0
  74. package/dist/app/machine/bailian.d.ts +0 -2
  75. package/dist/app/machine/bailian.js +5 -6
  76. package/dist/app/machine/update.d.ts +25 -0
  77. package/dist/app/machine/update.js +88 -0
  78. package/dist/app/mock/mock-live.js +1 -1
  79. package/dist/app/mock/mock.d.ts +4 -2
  80. package/dist/app/mock/mock.js +20 -12
  81. package/dist/app/replay.js +4 -4
  82. package/dist/app/skills/home-doc.js +1 -1
  83. package/dist/app/workspace.d.ts +9 -4
  84. package/dist/app/workspace.js +33 -10
  85. package/dist/app/ws/route.d.ts +8 -2
  86. package/dist/app/ws/route.js +7 -3
  87. package/dist/board/parse.d.ts +0 -2
  88. package/dist/board/parse.js +0 -6
  89. package/dist/board/presentation.d.ts +9 -15
  90. package/dist/board/presentation.js +8 -10
  91. package/dist/brain/api-tools.js +2 -1
  92. package/dist/brain/api.d.ts +12 -17
  93. package/dist/brain/api.js +16 -33
  94. package/dist/brain/index.d.ts +23 -71
  95. package/dist/brain/index.js +6 -69
  96. package/dist/brain/log.d.ts +5 -0
  97. package/dist/brain/log.js +22 -0
  98. package/dist/brain/runtime.d.ts +30 -0
  99. package/dist/brain/runtime.js +23 -0
  100. package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
  101. package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
  102. package/dist/brain/stream.d.ts +5 -5
  103. package/dist/brain/stream.js +7 -7
  104. package/dist/brain/transcript.d.ts +3 -3
  105. package/dist/brain/transcript.js +6 -6
  106. package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
  107. package/dist/brain/types.js +14 -0
  108. package/dist/chalk/glyphs/skeleton.d.ts +3 -3
  109. package/dist/chalk/glyphs/skeleton.js +1 -1
  110. package/dist/lib/index.d.ts +1 -1
  111. package/dist/lib/index.js +1 -1
  112. package/dist/memory/diary.js +3 -3
  113. package/dist/realtime/bailian.d.ts +1 -1
  114. package/dist/realtime/bailian.js +3 -2
  115. package/dist/realtime/preview.js +1 -1
  116. package/dist/realtime/voices.d.ts +2 -0
  117. package/dist/realtime/voices.js +57 -57
  118. package/dist/runtime/activities.d.ts +13 -1
  119. package/dist/runtime/activities.js +28 -2
  120. package/dist/runtime/activity-state.js +2 -2
  121. package/dist/runtime/agent-file.d.ts +3 -3
  122. package/dist/runtime/agent-file.js +3 -3
  123. package/dist/runtime/context/tutor-rules.d.ts +2 -2
  124. package/dist/runtime/context/tutor-rules.js +3 -3
  125. package/dist/runtime/home/home.d.ts +12 -1
  126. package/dist/runtime/home/home.js +8 -1
  127. package/dist/runtime/home/store.d.ts +1 -1
  128. package/dist/runtime/home/store.js +36 -21
  129. package/dist/runtime/journal/mix.d.ts +15 -6
  130. package/dist/runtime/journal/mix.js +60 -19
  131. package/dist/runtime/journal/recorder.d.ts +58 -13
  132. package/dist/runtime/journal/recorder.js +106 -16
  133. package/dist/runtime/journal/store.d.ts +4 -4
  134. package/dist/runtime/journal/store.js +6 -6
  135. package/dist/runtime/kid-input.js +2 -3
  136. package/dist/runtime/live/call.d.ts +12 -0
  137. package/dist/runtime/live/call.js +51 -18
  138. package/dist/runtime/live/turns.js +1 -1
  139. package/dist/runtime/runner.d.ts +9 -55
  140. package/dist/runtime/runner.js +52 -173
  141. package/dist/scenes/paint/painter.js +4 -3
  142. package/dist/schema/config.d.ts +43 -22
  143. package/dist/schema/config.js +45 -47
  144. package/dist/schema/conversation.d.ts +6 -0
  145. package/dist/schema/home.d.ts +11 -1
  146. package/dist/schema/home.js +7 -2
  147. package/dist/schema/workspace.d.ts +27 -0
  148. package/dist/schema/workspace.js +28 -0
  149. package/dist/shared/default-port.d.ts +2 -0
  150. package/dist/shared/default-port.js +2 -0
  151. package/dist/shared/qianwen.d.ts +14 -10
  152. package/dist/shared/qianwen.js +26 -22
  153. package/dist/shared/site-url.d.ts +2 -0
  154. package/dist/shared/site-url.js +5 -0
  155. package/dist/shared/upgrade.d.ts +9 -0
  156. package/dist/shared/upgrade.js +23 -0
  157. package/dist/ui/dev/dev-page.js +25 -252
  158. package/dist/ui/dev/dev-theme.d.ts +1 -1
  159. package/dist/ui/dev/dev-theme.js +1 -1
  160. package/dist/ui/dev/voice-gallery-page.js +1 -1
  161. package/dist/ui/kid/board-mark.d.ts +2 -2
  162. package/dist/ui/kid/board-mark.js +2 -2
  163. package/dist/ui/kid/home-tiles.d.ts +14 -0
  164. package/dist/ui/kid/home-tiles.js +44 -0
  165. package/dist/ui/kid/kid-page.js +68 -103
  166. package/dist/ui/kid/voice-ui.d.ts +1 -1
  167. package/dist/ui/kid/voice-ui.js +1 -1
  168. package/dist/ui/kid/word-emoji.js +5 -3
  169. package/dist/ui/parent/parent-home-page.js +337 -7
  170. package/dist/ui/parent/parent-page.js +6 -2
  171. package/dist/ui/parent/parent-settings-page.d.ts +1 -0
  172. package/dist/ui/parent/parent-settings-page.js +133 -0
  173. package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
  174. package/dist/ui/{dev → parent}/qr-page.js +19 -14
  175. package/dist/ui/shared/brand.d.ts +3 -1
  176. package/dist/ui/shared/brand.js +3 -1
  177. package/grades/grade-2/README.md +1 -1
  178. package/grades/grade-2/VALIDATION.md +1 -1
  179. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
  180. 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
  181. 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
  182. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
  183. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
  184. 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
  185. 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
  186. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
  187. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
  188. 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
  189. 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
  190. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
  191. package/package.json +6 -2
  192. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
  193. package/skills/cotutor-board/references/photo.md +2 -2
  194. package/skills/cotutor-home/SKILL.md +1 -0
  195. package/skills/cotutor-home/references/tutor.md +2 -1
  196. package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
  197. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
  198. package/skills/cotutor-tutor/SKILL.md +5 -13
  199. package/dist/brain/cli/adapters.d.ts +0 -60
  200. package/dist/brain/cli/adapters.js +0 -91
  201. package/dist/brain/cli/qwen.d.ts +0 -11
  202. package/dist/brain/cli/qwen.js +0 -141
  203. package/dist/brain/cli/run-plan.d.ts +0 -59
  204. package/dist/brain/cli/run-plan.js +0 -71
  205. package/dist/brain/cli/run.d.ts +0 -35
  206. package/dist/brain/cli/run.js +0 -256
  207. package/dist/brain/cli/types.js +0 -3
  208. package/dist/brain/cli/warm.d.ts +0 -53
  209. package/dist/brain/cli/warm.js +0 -103
@@ -11,26 +11,24 @@
11
11
  * 进程 cwd 是老师目录 agents/<name>/。
12
12
  */
13
13
  import { lecturePack, readLecture, storedMarks } from "./lecture.js";
14
- import { existsSync } from 'node:fs';
15
- import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
16
- import { dirname, isAbsolute, join, relative, sep } from 'node:path';
14
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
15
+ import { isAbsolute, join, relative, sep } from 'node:path';
17
16
  import { buildContextPack } from "./context/context-pack.js";
18
- import { addMessage, applyRun, lastJobOf, changedCards, jobId, sameRuntime, sessionFor } from "./journal/conversation.js";
17
+ import { addMessage, applyRun, changedCards, jobId, sameRuntime, sessionFor } from "./journal/conversation.js";
19
18
  import { localDate, localMinute } from "../shared/time.js";
20
- import { cardId, conversationFiles, threads } from "../schema/index.js";
19
+ import { cardVoice, cardId, conversationFiles, threads } from "../schema/index.js";
21
20
  import { bookkeepingPrompt, kidQuestions, textbookHeadings } from "../memory/diary.js";
22
21
  import { cardAssets, cardLabel, describeCard, tutorCardKinds } from "../board/cards/index.js";
23
22
  import { boardGuideFor, cardGuidesFor } from "../board/cards/docs.js";
24
23
  import { activityCards, parseActivity, renderTutorSection } from "./activity.js";
25
- import { activityFile } from "./activities.js";
24
+ import { findActivityFile } from "./activities.js";
26
25
  import { activityState, paintGate, paintTool, readActivity, threadActivity } from "./activity-state.js";
27
- import { controlsTools, createBrain } from "../brain/index.js";
28
- import { endsWithToolCall, lineFlags, parseBoard } from "../board/parse.js";
26
+ import { createBrain, getRuntime } from "../brain/index.js";
27
+ import { lineFlags, parseBoard } from "../board/parse.js";
29
28
  import { readyBeats, beatsOf } from "../board/presentation.js";
30
29
  import { deriveKidView, truncateReply } from "./kid-view.js";
31
30
  import { parseSections } from "../board/sections.js";
32
31
  import { threadDigest } from "./context/thread-digest.js";
33
- import { getRuntime, runtimeUses, boardPreloaded } from "../brain/cli/run-plan.js";
34
32
  import { beatTimings } from "./journal/events.js";
35
33
  import {} from "../schema/index.js";
36
34
  import { BOARD_CARD_KINDS, MEMORY_MAX_PER_TURN, MEMORY_TIDY_CAP, VAULT_PACK_ROLES, isApiRuntime, resolvePolicy } from "../schema/index.js";
@@ -45,8 +43,7 @@ import { TUTOR_RULES_PATH, takesTutorRules, tutorRulesBody, BOARD_GUIDE_IN_SYSTE
45
43
  import { DubQueue } from "../media/tts.js";
46
44
  import { continueContext } from "./home/store.js";
47
45
  import { kidInput } from "./kid-input.js";
48
- import { dashscopeAuth } from "../realtime/bailian.js";
49
- import { qianwenKeys } from "../shared/qianwen.js";
46
+ import { compatibleLine } from "../shared/qianwen.js";
50
47
  import { DRAW_ONLY_TEXT, drawingPackLine } from "../board/paper.js";
51
48
  import { makeDraw } from "../media/draw.js";
52
49
  import { paintLines, startPaints } from "../scenes/paint/painter.js";
@@ -123,7 +120,7 @@ export async function gatherContext(ws, tutor, input, report) {
123
120
  */
124
121
  export async function attachActivity(ws, tutor, pack, run, where, inSystem = false) {
125
122
  pack.activity = { name: run.name, params: run.params, state: {} };
126
- const path = relative(ws.root, activityFile(ws.root, tutor, run.name)).split(sep).join('/');
123
+ const path = relative(ws.root, findActivityFile(ws.root, tutor, run.name)).split(sep).join('/');
127
124
  const text = await readFile(join(ws.root, path), 'utf8').catch(() => null);
128
125
  if (text === null) {
129
126
  pack.activityGuide = `缺:${path} 不在,照你老师文件里的做`;
@@ -150,8 +147,6 @@ export async function attachActivity(ws, tutor, pack, run, where, inSystem = fal
150
147
  pack.activityGuide = path;
151
148
  return [];
152
149
  }
153
- /** 按老师裁的板书写法落在哪(相对 workspace 根;机器文件,内容变了文件名跟着变) */
154
- export const TUTOR_BOARD_DIR = '.cotutor/board';
155
150
  /** activity 在系统提示里时,上下文包 activityGuide 那一行 */
156
151
  export const ACTIVITY_IN_SYSTEM = '已在你的系统提示里(这个话题的 activity,照它带)';
157
152
  export async function activityParts(ws, tutor, run) {
@@ -160,85 +155,42 @@ export async function activityParts(ws, tutor, run) {
160
155
  return null;
161
156
  const kinds = [...new Set(['text', ...a.cards.filter((k) => BOARD_CARD_KINDS.includes(k))])];
162
157
  const body = renderTutorSection(a, { params: run.params, cards: kinds }) ?? '';
163
- const guideBody = boardGuideFor(kinds);
164
- const guide = { body: guideBody, path: `${TUTOR_BOARD_DIR}/${tutor}-${textHash(guideBody).slice(0, 8)}.md` };
165
158
  const display = ws.config.tutors[tutor]?.display ?? tutor;
166
- await writeIfChanged(join(ws.root, guide.path), guideBody);
167
- return { body, kinds, guide, memory: { agent: `${tutor}/${run.name}`, display: `${display}·${run.name}` } };
159
+ return { body, kinds, guide: boardGuideFor(kinds), memory: { agent: `${tutor}/${run.name}`, display: `${display}·${run.name}` } };
168
160
  }
169
- async function writeIfChanged(file, text) {
170
- if ((await readFile(file, 'utf8').catch(() => null)) === text)
171
- return;
172
- await mkdir(dirname(file), { recursive: true });
173
- await writeFile(file, text);
174
- }
175
- /**
176
- * 这位老师的板书写法:正文 + 相对 workspace 根的路径(给 {boardFile} 与上下文包)。
177
- * 没写 cards → 出厂的 cotutor-board SKILL.md;写了 → 按清单从包里现拼(boardGuideFor),写到 .cotutor/board/<老师>-<hash>.md,
178
- * 文件名带内容的 hash:清单一改路径就变,预热进程的 argv 对不上,不会拿着旧写法去答。读不到 SKILL.md → null。
179
- */
161
+ /** 这位老师的板书写法:没写 cards → 出厂的 cotutor-board SKILL.md;写了 → 按清单从包里现拼(boardGuideFor)。读不到 SKILL.md → null */
180
162
  export async function boardGuideOf(ws, tutor) {
181
163
  const kinds = tutorCardKinds(ws.config.tutors[tutor]?.cards);
182
- if (!kinds) {
183
- try {
184
- return { body: boardGuideBody(await readFile(join(ws.root, BOARD_GUIDE_PATH), 'utf8')), path: BOARD_GUIDE_PATH };
185
- }
186
- catch {
187
- return null;
188
- }
189
- }
190
- const body = boardGuideFor(kinds);
191
- const path = `${TUTOR_BOARD_DIR}/${tutor}-${textHash(body).slice(0, 8)}.md`;
192
- const file = join(ws.root, path);
193
- if (!existsSync(file)) {
194
- await mkdir(join(ws.root, TUTOR_BOARD_DIR), { recursive: true });
195
- await writeFile(file, body);
164
+ if (kinds)
165
+ return boardGuideFor(kinds);
166
+ try {
167
+ return boardGuideBody(await readFile(join(ws.root, BOARD_GUIDE_PATH), 'utf8'));
196
168
  }
197
- return { body, path };
198
- }
199
- /**
200
- * 板书写法怎么递给老师(2026-09-19):有脸的老师、板书没关才带。运行时预载了 → 上下文包只写一行事实;
201
- * 没预载 → 原文作为一条笔记放进话题第一条(<cotutor-board>),排在守则后面,续话题没改过写「未变」。返回板书写法(给 {systemBody} 与 {boardFile})。
202
- */
203
- export async function attachBoardGuide(ws, tutor, pack, o) {
204
- if (!takesTutorRules(tutor) || o.boardOff)
205
- return null;
206
- const guide = await boardGuideOf(ws, tutor);
207
- if (!guide) {
208
- pack.boardGuide = `缺:${BOARD_GUIDE_PATH} 不在(cotutor upgrade 补),照你会的写`;
169
+ catch {
209
170
  return null;
210
171
  }
211
- if (o.preloaded) {
212
- pack.boardGuide = BOARD_GUIDE_IN_SYSTEM;
213
- return guide;
214
- }
215
- pack.boardGuide = guide.path;
216
- const notes = pack.notes ?? [];
217
- const at = notes.findIndex((n) => n.role === 'rules');
218
- notes.splice(at + 1, 0, { role: 'boardGuide', path: guide.path, text: guide.body });
219
- pack.notes = notes;
220
- return guide;
221
172
  }
222
- /** 回复停在写成正文的工具调用上(后面只剩空行;「## 」尾巴不算):交给脑,CLI 据此 resume 一次 */
223
- const stuckOnTool = (text) => endsWithToolCall(parseSections(text).body);
224
173
  /**
225
- * 老师正文({agentBody})与系统提示({systemBody} = 正文 + 板书写法)、板书写法的文件({boardFile}):模板用到才读。send 与预热同一份,预热起的进程 argv 才对得上。
226
- * 顺手把板书写法挂到上下文包上(attachBoardGuide)。没递板书写法(工具人、板书关了)的 {boardFile} 照旧是出厂的 SKILL.md。
174
+ * 系统提示(2026-09-19 起板书写法跟着进系统提示):有脸的老师、板书没关才带板书写法,上下文包只写一行「已在系统提示里」。
175
+ * activity 话题:系统提示换成 activity 的人设与做法 + 它的卡的写法;工具人(没有守则)的 activity 退回老师文件 + 第一条带原文(attachActivity)。
227
176
  */
228
- async function systemParts(ws, tutor, runtime, policy, pack, act = null) {
229
- const wantsSystemBody = runtimeUses(runtime, '{systemBody}');
230
- // activity 话题:系统提示换成 activity 的(Qwen 走 {systemBody});模板没有它就退回老师文件 + 第一条带原文
231
- if (act && takesTutorRules(tutor) && wantsSystemBody) {
232
- const boardOff = policy.board === 'off';
177
+ async function systemParts(ws, tutor, policy, pack, act = null) {
178
+ const boardOff = policy.board === 'off';
179
+ if (act && takesTutorRules(tutor)) {
233
180
  if (!boardOff)
234
181
  pack.boardGuide = BOARD_GUIDE_IN_SYSTEM;
235
- const systemBody = boardOff ? act.body : `${act.body}\n\n${act.guide.body}`;
236
- return { agentBody: act.body, systemBody, boardFile: join(ws.root, boardOff ? BOARD_GUIDE_PATH : act.guide.path), agent: tutor, activityInSystem: true };
182
+ return { systemBody: boardOff ? act.body : `${act.body}\n\n${act.guide}`, activityInSystem: true };
237
183
  }
238
- const agentBody = runtimeUses(runtime, '{agentBody}') || wantsSystemBody ? await readAgentBody(ws, tutor) : undefined;
239
- const guide = await attachBoardGuide(ws, tutor, pack, { preloaded: boardPreloaded(runtime), boardOff: policy.board === 'off' });
240
- const systemBody = wantsSystemBody && agentBody !== undefined ? (guide ? `${agentBody}\n\n${guide.body}` : agentBody) : undefined;
241
- return { agentBody, systemBody, boardFile: join(ws.root, guide?.path ?? BOARD_GUIDE_PATH), agent: tutor, activityInSystem: false };
184
+ const agentBody = await readAgentBody(ws, tutor);
185
+ if (!takesTutorRules(tutor) || boardOff)
186
+ return { systemBody: agentBody, activityInSystem: false };
187
+ const guide = await boardGuideOf(ws, tutor);
188
+ if (!guide) {
189
+ pack.boardGuide = `缺:${BOARD_GUIDE_PATH} 不在(cotutor upgrade 补),照你会的写`;
190
+ return { systemBody: agentBody, activityInSystem: false };
191
+ }
192
+ pack.boardGuide = BOARD_GUIDE_IN_SYSTEM;
193
+ return { systemBody: `${agentBody}\n\n${guide}`, activityInSystem: false };
242
194
  }
243
195
  /** 这轮带的笔记版本:role → `路径@hash`(记进消息;同一话题下一轮对得上就不再带原文) */
244
196
  export function noteVersions(pack) {
@@ -262,7 +214,7 @@ export function seenNotes(messages, thread) {
262
214
  return messages[i].notes;
263
215
  return {};
264
216
  }
265
- /** 老师该读得到的目录:workspace 根(作业照片、技能)与 vault(教材、日记);会话 cwd 是老师目录,这些在它外面(qwen 读外面要先划进来) */
217
+ /** 只读工具读得到的目录:workspace 根(作业照片、技能)与 vault(教材、日记) */
266
218
  export function readDirsOf(ws) {
267
219
  return [...new Set([ws.root, ws.paths.vault])];
268
220
  }
@@ -277,13 +229,12 @@ export async function packDryRun(ws, tutor, input) {
277
229
  const policy = resolvePolicy(ws.config, tutor);
278
230
  if (policy.board === 'off')
279
231
  pack.board = 'off';
280
- const { runtime } = getRuntime(ws.config, t.runtime);
281
232
  // 和 send 同一条规矩:孩子的话不带工具时,只有工具才用得上的路径不进包
282
- if (policy.tools !== 'on' && input.from === 'kid' && controlsTools(runtime)) {
233
+ if (policy.tools !== 'on' && input.from === 'kid') {
283
234
  delete pack.refs;
284
235
  delete pack.vault;
285
236
  }
286
- const parts = await systemParts(ws, tutor, runtime, policy, pack, act);
237
+ const parts = await systemParts(ws, tutor, policy, pack, act);
287
238
  if (input.activity)
288
239
  await attachActivity(ws, tutor, pack, input.activity, { date: localDate(input.at), messages: [], thread: '', now: input.at.getTime() }, parts.activityInSystem);
289
240
  return { prompt: buildContextPack(pack, input.text, policy.contextPack), pack, report };
@@ -297,7 +248,7 @@ export function vaultPack(paths) {
297
248
  if (!p)
298
249
  continue;
299
250
  const rel = relative(root, p);
300
- out[r] = !rel || rel.startsWith('..') || isAbsolute(rel) ? p : rel;
251
+ out[r] = !rel || rel.startsWith('..') || isAbsolute(rel) ? p : rel.split(sep).join('/');
301
252
  }
302
253
  return out;
303
254
  }
@@ -310,15 +261,9 @@ export class Runner {
310
261
  listeners = new Set();
311
262
  /** noteEvent 每个事件文件一条追加链 */
312
263
  notes = new Map();
313
- /** 脑执行:起老师、读输出、断流接着跑、预热下一轮(src/brain/) */
264
+ /** 脑执行:请模型、读输出(src/brain/) */
314
265
  brain;
315
- /** 预热池的旧入口(测试看有没有、杀掉);迁完删 */
316
- get spares() {
317
- return { has: (tutor, slot) => this.brain.warmed(tutor, slot), drop: (tutor, slot) => this.brain.dropWarm(tutor, slot) };
318
- }
319
- /** 页面拉今天时的预热:每位老师(与带的话题)上次看的时刻 */
320
- prewarmedAt = new Map();
321
- /** 起子进程用的环境(测试注入) */
266
+ /** key、假端点从哪读的环境(测试注入) */
322
267
  get env() {
323
268
  return this.opts.env;
324
269
  }
@@ -332,10 +277,10 @@ export class Runner {
332
277
  this.getWs = getWs;
333
278
  this.opts = opts;
334
279
  this.draw = opts.draw ?? makeDraw({ env: opts.env });
335
- // API 运行时的凭证与实时那边同一条查找链(家长端配的在最前);只有按量 key、没有业务空间 id 也能用(走公网域名,同 qwen)。
280
+ // API 运行时的 key 与配音、画图同一套选法(~/.config/cotutor/):Token Plan 有这个模型就走它,否则百炼按量;只有按量 key、没有业务空间 id 也能用(走公网域名)。
336
281
  // COTUTOR_BRAIN_ENDPOINT:测试与探针指到假百炼
337
282
  const env = opts.env ?? process.env;
338
- this.brain = createBrain({ workspace: () => { const ws = this.getWs(); return { config: ws.config, root: ws.root }; }, env: opts.env, warmIdleMs: opts.warmIdleMs, auth: () => dashscopeAuth(env) ?? (qianwenKeys(env).payg ? { key: qianwenKeys(env).payg, workspaceId: null } : null), endpoint: env.COTUTOR_BRAIN_ENDPOINT || undefined });
283
+ this.brain = createBrain({ workspace: () => { const ws = this.getWs(); return { config: ws.config, root: ws.root }; }, line: (model) => compatibleLine(model, env), endpoint: env.COTUTOR_BRAIN_ENDPOINT || undefined });
339
284
  }
340
285
  /** 孩子端发一条的验与拼(HTTP 与实时老师共用,./kid-input.ts):拼成 send 的入参,或回 reject;不起老师 */
341
286
  accept(tutor, body, date, deviceId) {
@@ -373,61 +318,6 @@ export class Runner {
373
318
  }
374
319
  }
375
320
  }
376
- /** 杀掉所有预热的进程(测试收尾;服务退出时它们的 stdin 断了会自己退) */
377
- close() {
378
- this.brain.close();
379
- }
380
- /**
381
- * 预热:给这位老师起好下一轮的进程,两个位置都补齐——
382
- * fresh:新开会话(新话题、接着上次都用它);
383
- * resume:接 thread(页面选着的话题;不给或今天没有 = 当天末条所在的话题,和 send 缺省一样)的会话,它没有会话就不留。
384
- * 只给有脸的、开着的、运行时 stdin: "stream-json"、这会儿没在跑的老师起;throttleMs 内同一话题看过就不再看(页面轮询今天)。
385
- * 起了返回 true;起不了不报错,下一轮冷起。warm: false(cotutor send:跑完就退,留着备用进程会把它挂一小时)不起。
386
- */
387
- async prewarm(tutor, opts = {}) {
388
- if (this.opts.warm === false)
389
- return false;
390
- const t0 = Date.now();
391
- const seen = `${tutor}\n${opts.thread ?? ''}`;
392
- if (opts.throttleMs && t0 - (this.prewarmedAt.get(seen) ?? -Infinity) < opts.throttleMs)
393
- return false;
394
- this.prewarmedAt.set(seen, t0);
395
- const ws = this.getWs();
396
- const t = ws.config.tutors[tutor];
397
- if (!t?.enabled || !takesTutorRules(tutor) || this.active.has(tutor))
398
- return false;
399
- const { runtime } = getRuntime(ws.config, t.runtime);
400
- if (!this.brain.canWarm(runtime))
401
- return false;
402
- const date = localDate((this.opts.now ?? (() => new Date()))());
403
- const index = await readIndex(ws, tutor, date);
404
- const known = threads(index.messages);
405
- const thread = opts.thread && known.includes(opts.thread) ? opts.thread : known.at(-1);
406
- const session = thread ? sessionFor(index, thread) : null;
407
- const policy = resolvePolicy(ws.config, tutor);
408
- const parts = await systemParts(ws, tutor, runtime, policy, { at: '', plan: [], recent: [] });
409
- const base = { prompt: '', runtime: t.runtime, effort: policy.effort, tools: policy.tools === 'on' ? 'on' : 'off', readDirs: readDirsOf(ws) };
410
- // 预热的是孩子的下一句:工具照孩子那轮填(带照片的对不上,那轮冷起)。新话题照老师文件;接着的话题是 activity 的,照它的系统提示
411
- const fresh = this.brain.plan({ ...base, ...parts, session: null });
412
- const run = thread ? threadActivity(index.messages, thread) : null;
413
- const act = run ? await activityParts(ws, tutor, run) : null;
414
- const resumeParts = act ? await systemParts(ws, tutor, runtime, policy, { at: '', plan: [], recent: [] }, act) : parts;
415
- const resume = session ? this.brain.plan({ ...base, ...resumeParts, session }) : null;
416
- const cwd = join(ws.dirs.agents, tutor);
417
- await mkdir(cwd, { recursive: true });
418
- // 读索引这会儿孩子可能已经开口了:这轮在跑就不起(它收尾时会再起)
419
- if (this.active.has(tutor))
420
- return false;
421
- await this.brain.prepare(fresh);
422
- if (this.active.has(tutor))
423
- return false;
424
- this.brain.warm(tutor, 'fresh', fresh, { cwd, date, lastJob: null });
425
- if (resume?.resume && thread)
426
- this.brain.warm(tutor, 'resume', resume, { cwd, date, lastJob: lastJobOf(index, thread) });
427
- else
428
- this.brain.dropWarm(tutor, 'resume');
429
- return true;
430
- }
431
321
  /** 正在回的那条已经出来的板书(流式);没在跑或还没出卡 → null */
432
322
  partial(tutor) {
433
323
  return this.active.get(tutor)?.partial ?? null;
@@ -499,7 +389,7 @@ export class Runner {
499
389
  const query = input.from === 'kid' ? [text, input.home?.brief].filter(Boolean).join('\n') : '';
500
390
  const gathered = await gatherContext(ws, tutor, { from: input.from, at: now, focus: input.focus, ...(query ? { query } : {}), ...(act ? { agent: act.memory.agent, kinds: act.kinds } : {}) });
501
391
  const policy = resolvePolicy(ws.config, tutor);
502
- const { agentBody, systemBody, boardFile, agent, activityInSystem } = await systemParts(ws, tutor, runtime, policy, gathered, act);
392
+ const { systemBody, activityInSystem } = await systemParts(ws, tutor, policy, gathered, act);
503
393
  const activityRun = input.bookkeep ? null : threadRun;
504
394
  const activityWarn = activityRun ? await attachActivity(ws, tutor, gathered, activityRun, { date, messages: index.messages, thread, now: now.getTime() }, activityInSystem) : [];
505
395
  // 家长笔记原文:新会话整篇带;续会话时这个话题带过、没改的只写「未变」
@@ -529,10 +419,9 @@ export class Runner {
529
419
  }));
530
420
  // 这轮带不带工具(policy.tools,2026-10-04):孩子说的话缺省不带,只凭上下文包答;带照片的、交了画的(要 Read 看图)、
531
421
  // 系统任务带只读的(以前是整份白名单,照片里一句话就能让老师拿 Bash 读写 workspace 外);policy.tools: on 的老师总是整份。
532
- // 不带的那轮,只有工具才用得上的路径(refs / vault)不进包;模板管不了工具的运行时(没有 {tools} / {toolArgs}),照旧
422
+ // 不带的那轮,只有工具才用得上的路径(refs / vault)不进包
533
423
  const tools = policy.tools === 'on' ? 'on' : input.from !== 'kid' || seen.length > 0 || drawings > 0 ? 'read' : 'off';
534
- const bare = tools === 'off' && controlsTools(runtime);
535
- if (bare) {
424
+ if (tools === 'off') {
536
425
  delete pack.refs;
537
426
  delete pack.vault;
538
427
  }
@@ -568,29 +457,22 @@ export class Runner {
568
457
  pack.live = { ...(l.caught ? { caught: l.caught } : {}), ...(l.cut !== undefined ? { cut: l.cut } : {}), ...(l.warmup?.length ? { warmup: l.warmup } : {}), ...(l.phone !== undefined ? { phone: l.phone } : {}), ...(l.app ? { app: l.app } : {}), ...(l.draw?.length ? { draw: l.draw } : {}) };
569
458
  }
570
459
  const prompt = buildContextPack(pack, text, policy.contextPack);
571
- const req = { agent, prompt, agentBody, systemBody, boardFile, runtime: input.runtime ?? t.runtime, effort: policy.effort, tools, readDirs: readDirsOf(ws), session, ...(compact ? { compact: { digest: threadDigest(index.messages.filter((_, i) => known[i] === thread)) } } : {}), ...(pack.photoFiles?.length ? { images: pack.photoFiles } : {}), stuckOnTool };
460
+ const req = { agent: tutor, prompt, systemBody, runtime: input.runtime ?? t.runtime, effort: policy.effort, tools, readDirs: readDirsOf(ws), session, ...(compact ? { compact: { digest: threadDigest(index.messages.filter((_, i) => known[i] === thread)) } } : {}), ...(pack.photoFiles?.length ? { images: pack.photoFiles } : {}) };
572
461
  const plan = this.brain.plan(req);
573
462
  // 原声:落在这轮旁边(删话题一起删);写不下就当没有,消息照发
574
463
  const started = addMessage(index, { job, thread, at: pack.at, from: input.from, text, focus: input.focus, ...(input.action ? { action: input.action } : {}), ...(cards.length ? { cards: cards.map(({ card, text }) => ({ card, text })) } : {}), ...(photos.length ? { photos } : {}), ...(input.drawing ? { drawing: input.drawing } : {}), ...(input.device ? { device: input.device } : {}), ...(input.deviceId ? { deviceId: input.deviceId } : {}), ...(input.bookkeep ? { bookkeep: input.bookkeep } : {}), ...(input.tidy ? { tidy: true } : {}), ...(input.replayOf ? { replayOf: input.replayOf } : {}), ...(input.eval ? { eval: true } : {}), ...(input.live ? { live: true } : {}), ...(input.via ? { via: input.via } : {}), ...(continued && input.continues ? { continues: { date: input.continues.date, thread: input.continues.thread } } : {}), ...(input.lecture ? { lecture: { bundle: input.lecture.bundle, title: lecture?.title ?? input.lecture.bundle, watchedMs: input.lecture.watchedMs, finished: input.lecture.finished, pauses: input.lecture.pauses, ...(input.lecture.again ? { again: true } : {}), ...(lecture?.video ? { video: true } : {}), ...(marks.length ? { marks } : {}), ...(input.lecture.log?.length ? { log: input.lecture.log } : {}) } } : {}), ...(Object.keys(notes).length ? { notes } : {}), ...(fresh && activityRun ? { activity: activityRun } : {}), ...(fresh && input.talk ? { talk: input.talk } : {}), ...(compact ? { compacted: true } : {}), ...(lectureWarn.length || activityWarn.length ? { warnings: [...lectureWarn, ...activityWarn] } : {}), result: 'running', artifacts: [], runtime: plan.runtime });
575
464
  await writeIndex(ws, started);
576
465
  // 运行记录(《vault重构方案》§7):除了老师文件与技能,还记这轮带进上下文的每篇笔记、替老师查的每个文件,事后重建得出老师看到的是哪一版
577
466
  const notesSent = (pack.notes ?? []).map((n) => ({ role: n.role, path: n.path, hash: textHash(n.text), chars: n.text.length, ...(n.cut ? { cut: true } : {}) }));
578
- await writeRunFile(ws, tutor, date, job, { at: pack.at, prompt, plan, agentBody: agentBody !== undefined, sources: { ...(await snapshotSources(ws, tutor)), notes: notesSent, retrieved: gathered.retrieved } });
579
- // 这个 CLI 在 workspace 里要的机器文件(qwen 的隔离家目录):备不好这轮照起,起不来的原因会在 err.log
580
- await this.brain.prepare(plan);
467
+ await writeRunFile(ws, tutor, date, job, { at: pack.at, prompt, plan, agentBody: true, sources: { ...(await snapshotSources(ws, tutor)), notes: notesSent, retrieved: gathered.retrieved } });
581
468
  active.job = job;
582
469
  if (input.live)
583
470
  active.live = true;
584
471
  // 实时的一轮讲稿是嘴念的,不按 replyMaxChars 截(截了孩子听到半句:10-20「题目已经…」后面「你补上 ēng 就行」没了)
585
472
  const runPolicy = input.live ? { ...policy, replyMaxChars: Number.MAX_SAFE_INTEGER } : policy;
586
- // 预热的进程按这个话题末条 job 对(新开记 null);spawn 在同一拍里接过来
587
- const lastJob = plan.resume ? lastJobOf(index, thread) : null;
588
- active.done = this.spawn(ws, tutor, date, job, thread, req, plan, runPolicy, active, lastJob).finally(() => {
473
+ active.done = this.spawn(ws, tutor, date, job, thread, req, plan, runPolicy, active).finally(() => {
589
474
  if (this.active.get(tutor) === active)
590
475
  this.active.delete(tutor);
591
- // 孩子 / 家长这轮完了,马上起好下一轮的进程;系统轮(记账、整理记忆)之后孩子多半不在,不起
592
- if (input.from !== 'system' && !input.replayOf && !input.eval)
593
- void this.prewarm(tutor).catch(() => { });
594
476
  });
595
477
  return { tutor, date, job, thread, plan, done: active.done };
596
478
  }
@@ -694,7 +576,7 @@ export class Runner {
694
576
  const shared = Boolean(ws.config.tutors[tutor]) && tutor.endsWith('-tutor');
695
577
  return planMemory(ws, { agent: owner?.agent ?? tutor, display: owner?.display ?? ws.config.tutors[tutor]?.display ?? tutor, items, max: asked?.tidy ? null : MEMORY_MAX_PER_TURN, date, said, shared, ...(seen ? { seen } : {}), ...(childSeen ? { childSeen } : {}) });
696
578
  }
697
- async spawn(ws, tutor, date, job, thread, req, plan, policy, active, lastJob) {
579
+ async spawn(ws, tutor, date, job, thread, req, plan, policy, active) {
698
580
  const replyMaxChars = policy.replyMaxChars;
699
581
  const files = conversationFiles(ws.dirs.conversations, tutor, date);
700
582
  // 埋点:从这一轮起算,首卡 = partial 板书第一次有卡(孩子端第一次看到东西),done = 进程退出,dubbed = 配音收尾
@@ -718,8 +600,8 @@ export class Runner {
718
600
  }
719
601
  };
720
602
  // 讲稿不配音(嘴照念):拍关就就绪;卡自己的声音(点读段、单词)照配
721
- const cardVoice = ws.config.tutors[tutor]?.voice;
722
- const queue = cardVoice ? new DubQueue(cardVoice, files.err(job), { cacheDir: ws.dirs.tts, env: this.opts.env }) : null;
603
+ const voice = cardVoice(ws.config.tutors, tutor);
604
+ const queue = voice ? new DubQueue(voice, files.err(job), { cacheDir: ws.dirs.tts, env: this.opts.env }) : null;
723
605
  if (queue)
724
606
  queue.report = (e) => emit(e.kind === 'queued' ? { lane: 'tts', kind: 'queued', label: e.label } : e.kind === 'done' ? { lane: 'tts', kind: 'done', label: e.label, ms: e.ms, file: e.file ?? '' } : { lane: 'tts', kind: 'failed', label: e.label, ms: e.ms, error: e.error ?? '?' });
725
607
  let cardsSeen = 0;
@@ -765,24 +647,21 @@ export class Runner {
765
647
  emit({ lane: 'main', kind: 'line', line: linesSeen, text: lines[linesSeen].text, ...lineFlags(lines[linesSeen], section.cards) });
766
648
  settleReady();
767
649
  };
768
- // 起老师(src/brain/):预热的进程在这一拍里接过来(send 查完 busy 到这里之间没有 await);正文、工具、断流随流回报
769
- const brainRun = this.brain.start(req, plan, { tutor, key: { cwd: join(ws.dirs.agents, tutor), date, lastJob }, files: { log: files.log(job), err: files.err(job), history: files.brain(thread) }, stall: policy.stall }, {
770
- onStart: (s) => emit({ lane: 'main', kind: 'start', cli: s.cli, runtime: s.runtime, resume: s.resume, ...(s.warmBornAt !== undefined ? { warmMs: t0 - s.warmBornAt } : {}) }),
650
+ // 请老师(src/brain/):正文、工具随流回报
651
+ const brainRun = this.brain.start(req, plan, { tutor, files: { log: files.log(job), err: files.err(job), history: files.brain(thread) }, stall: policy.stall }, {
652
+ onStart: (s) => emit({ lane: 'main', kind: 'start', cli: s.cli, runtime: s.runtime, resume: s.resume }),
771
653
  onText: () => {
772
654
  dirty = true;
773
655
  if (!timer)
774
656
  timer = setTimeout(reparse, 150);
775
657
  },
776
658
  onTool: (tl) => emit({ lane: 'main', kind: 'tool', name: tl.name, sub: tl.sub }),
777
- onStall: (s) => emit({ lane: 'main', kind: 'stall', idleMs: s.idleMs, retry: s.retry, kept: s.kept }),
778
659
  onExit: (at) => {
779
660
  timing.doneMs = at - t0;
780
661
  if (timer)
781
662
  clearTimeout(timer);
782
663
  },
783
664
  });
784
- if (brainRun.warm)
785
- timing.warm = true;
786
665
  const { transcript, tools, warnings } = await brainRun.done;
787
666
  emit({ lane: 'main', kind: 'exit', ok: transcript.final.ok, ...(transcript.final.reason ? { reason: transcript.final.reason } : {}), ...(transcript.final.inTokens !== undefined ? { tokens: { in: transcript.final.inTokens, out: transcript.final.outTokens ?? 0 } } : {}), ...(transcript.final.numTurns !== undefined ? { turns: transcript.final.numTurns } : {}) });
788
667
  const kidView = deriveKidView(transcript, { replyMaxChars }, { live: active.live });
@@ -5,7 +5,7 @@
5
5
  * 一张一张排着画。孩子端没有错误:没画成就停在分幅那几句,原因只给家长看。
6
6
  */
7
7
  import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
8
- import { join, relative } from 'node:path';
8
+ import { join, relative, sep } from 'node:path';
9
9
  import { cardId, conversationFiles } from "../../schema/index.js";
10
10
  import { PaintPropsSchema } from "../../board/cards/index.js";
11
11
  import { PAINT_DEFAULT, PAINT_NEGATIVE } from "../../schema/index.js";
@@ -124,7 +124,7 @@ export function paintNote(r) {
124
124
  const why = e.startsWith('daily:') ? `今天画了 ${e.slice(6).split('/')[0]} 张,到了当时一天的上限(现在不限了)`
125
125
  : e.startsWith('gate:') ? `应用拦下了,没交给画家(activity「应用拦的」:${e.slice(5)})`
126
126
  : e === 'replay' ? '回放不画'
127
- : e === 'no_key' ? '拿不到百炼 key(DASHSCOPE_API_KEY,或本机 ~/.qwen/settings.json)'
127
+ : e === 'no_key' ? '拿不到百炼 key(家长端「首页」里填)'
128
128
  : e === 'stale' ? '画到一半服务重起了'
129
129
  : e.includes('DataInspectionFailed') ? '百炼的内容审核没过'
130
130
  : e === 'timeout' ? '等了太久(超时)'
@@ -148,7 +148,8 @@ export async function enrichPaints(ws, tutor, date, job, at, section, opts = {})
148
148
  if (r?.state === 'drawing' && since(r.at) > PAINT_STALE_MS)
149
149
  r = { state: 'none', at: r.at, error: 'stale' };
150
150
  const drawn = r ? (r.state === 'done' ? 'done' : r.state) : since(at) > PAINT_STALE_MS ? 'none' : 'drawing';
151
- const pic = drawn === 'done' ? relative(ws.root, files.cardAsset(job, n, PAINT_PNG)) : undefined;
151
+ // 孩子端按它拼取图的地址:Windows 上 relative 给的是反斜杠,换成 /
152
+ const pic = drawn === 'done' ? relative(ws.root, files.cardAsset(job, n, PAINT_PNG)).split(sep).join('/') : undefined;
152
153
  return { ...c, props: { ...c.props, drawn, ...(pic ? { pic } : {}), ...(opts.parent ? { note: paintNote(r) } : {}) } };
153
154
  }));
154
155
  return { ...section, cards };
@@ -5,6 +5,8 @@
5
5
  * 人设(display / avatar / voice)、政策、开关全在这里。
6
6
  */
7
7
  import { z } from 'zod';
8
+ import { DEFAULT_PORT } from '../shared/default-port.ts';
9
+ export { DEFAULT_PORT };
8
10
  export declare const PolicySchema: z.ZodObject<{
9
11
  replyMaxChars: z.ZodNumber;
10
12
  contextPack: z.ZodObject<{
@@ -68,6 +70,23 @@ export declare const AGENT_NAME_RE: RegExp;
68
70
  /** 老师卡上的常驻入口:不经首页发布、一直在那位老师的卡上 */
69
71
  export declare const TUTOR_EXTRAS: readonly ["dictation", "talk"];
70
72
  export type TutorExtra = (typeof TUTOR_EXTRAS)[number];
73
+ /**
74
+ * 老师卡上常驻的三块方砖(设计稿 https://claude.ai/artifact/NpPFoyvuF17dbRSEUuRJfW):先后就是牌上的先后。
75
+ * id 记在消息的 via.button;activity 是老师家里同名的文件(agents/<老师>/activities/<名>.md),孩子看到的字也是它。
76
+ * 家里有那个文件才出那一块(三门主科出厂都有);cotutor.json 的 tiles 只记关掉的,升级来的老 workspace 自动就有
77
+ */
78
+ export declare const HOME_TILES: readonly [{
79
+ readonly id: "preview";
80
+ readonly activity: "预习";
81
+ }, {
82
+ readonly id: "check";
83
+ readonly activity: "检查作业";
84
+ }, {
85
+ readonly id: "review";
86
+ readonly activity: "复习";
87
+ }];
88
+ export type HomeTileId = (typeof HOME_TILES)[number]['id'];
89
+ export declare const HOME_TILE_IDS: [HomeTileId, ...HomeTileId[]];
71
90
  export declare const TutorSchema: z.ZodObject<{
72
91
  display: z.ZodString;
73
92
  subject: z.ZodOptional<z.ZodString>;
@@ -119,6 +138,11 @@ export declare const TutorSchema: z.ZodObject<{
119
138
  dictation: "dictation";
120
139
  talk: "talk";
121
140
  }>>>;
141
+ tiles: z.ZodOptional<z.ZodObject<{
142
+ check: z.ZodOptional<z.ZodBoolean>;
143
+ preview: z.ZodOptional<z.ZodBoolean>;
144
+ review: z.ZodOptional<z.ZodBoolean>;
145
+ }, z.core.$strict>>;
122
146
  }, z.core.$strip>;
123
147
  export type Tutor = z.infer<typeof TutorSchema>;
124
148
  /** 学科的标准名:三门主科的别名归一(语文 / 数学 / 英语),别的原样(去两头空白);没写 null */
@@ -129,16 +153,11 @@ export declare function subjectLang(subject: string | null | undefined): 'zh' |
129
153
  * 这位老师卡上的常驻入口:配了照配的;没配按学科——语文 = 听写;英语 = 口语课,要口语课开着(talkOn = talk.enabled)
130
154
  */
131
155
  export declare function tutorExtras(t: Pick<Tutor, 'extras' | 'subject'>, talkOn?: boolean): TutorExtra[];
156
+ /** 这位老师开着的方砖(牌上的先后);家里有没有那个 activity 文件不在这里管 */
157
+ export declare function tutorTiles(t: Pick<Tutor, 'tiles'>): HomeTileId[];
132
158
  /**
133
- * 运行时:一个 CLI 一组 {run, resume} 命令模板。占位符:
134
- * {agent} 老师名 / {agentBody} 老师文件正文(给没有 --agent 的 CLI 塞系统提示)/ {prompt} 消息 / {session} 会话 id。
135
- * 板书写法的两种预载({boardFile} 板书技能 SKILL.md 的绝对路径,给能从文件追加系统提示的 CLI;{systemBody} 老师正文 + 板书写法,给只收一段系统提示文字的 CLI):
136
- * 模板里用了其中一个,应用就当板书写法已在系统提示里;都没用,应用在话题第一条消息里注入 <cotutor-board>(任何 CLI 都成立的退路)。
137
- * {effort} 这位老师的政策 effort(low / medium / high),qwen 由适配器写入思考量配置。
138
- * 带不带工具由适配器展开(src/brain/cli/):{tools} 填一串工具名,独占一个参数的 {toolArgs} 展开成几个参数。
139
- * 政策旋钮(预算、轮数、模型)写进模板,换 agent 只换运行时。
140
- * stdin: "stream-json":消息不进命令行,写进 stdin(`--input-format stream-json`),
141
- * 模板里就不写 {prompt};argv 与消息无关,应用把下一轮的进程提前起好等着。
159
+ * 旧的命令行运行时:一组 {run, resume} 命令模板(起 Qwen Code;更早是 Claude CLI)。2026-11-02 起不再支持,
160
+ * 留着这个形状只为旧 cotutor.json 读得进来:doctor 报出来,用到它的老师发不出去(src/brain/runtime.ts)。
142
161
  */
143
162
  export declare const CliRuntimeSchema: z.ZodObject<{
144
163
  cli: z.ZodOptional<z.ZodString>;
@@ -148,8 +167,8 @@ export declare const CliRuntimeSchema: z.ZodObject<{
148
167
  }, z.core.$strip>;
149
168
  export type CliRuntime = z.infer<typeof CliRuntimeSchema>;
150
169
  /**
151
- * 内置 API 运行时(2026-10-29):应用自己直连模型(src/brain/api.ts),不起 CLI、不用装 Qwen Code。
152
- * 现在只有百炼(OpenAI 兼容的 Chat Completions,端点是业务空间 id 的域名);凭证是机器级的百炼配置(家长端「首页」里填的按量 key 与业务空间 id)。
170
+ * API 运行时(2026-10-29;2026-11-02 起只有它):应用自己直连模型(src/brain/api.ts)。
171
+ * 现在只有百炼(OpenAI 兼容的 Chat Completions);凭证是机器级的 ~/.config/cotutor/(家长端「首页」填的按量 key 与业务空间 id,开发机还有 Token Plan)。
153
172
  * 系统提示 = 老师正文 + 板书写法;同一话题的模型历史由应用存;effort 映射成思考开关与思考量;工具只有只读的三样。
154
173
  */
155
174
  export declare const ApiRuntimeSchema: z.ZodObject<{
@@ -244,6 +263,12 @@ export declare const LiveSchema: z.ZodObject<{
244
263
  export type LiveConfig = z.infer<typeof LiveSchema>;
245
264
  /** 这位老师的嘴用哪个音色 */
246
265
  export declare const mouthVoice: (live: Pick<LiveConfig, "voices">, tutor: string) => string;
266
+ /** 卡的配音(点读、单词卡、听写念词)的缺省:出厂三位各一个(2026-11-03 照 Ray 那里用着的),不写进 cotutor.json;别的老师没配就不配音 */
267
+ export declare const SHIPPED_CARD_VOICES: Readonly<Record<string, string>>;
268
+ /** 这位老师卡的配音用哪个音色:家长挑的,没挑用出厂的;都没有 undefined(不配音) */
269
+ export declare const cardVoice: (tutors: Readonly<Record<string, {
270
+ voice?: string;
271
+ }>>, tutor: string) => string | undefined;
247
272
  /**
248
273
  * 听写:拍照认词表用哪个模型。挑词的要求在听写 activity 文件「认字收到什么」;
249
274
  * 下面两份是出厂那节的原文,workspace 里没有听写 activity 文件时照它。回什么格式(一行一段 JSON)由应用填(src/scenes/dictation/recognize.ts 的 FORMAT)。
@@ -378,6 +403,11 @@ export declare const CotutorConfigSchema: z.ZodObject<{
378
403
  dictation: "dictation";
379
404
  talk: "talk";
380
405
  }>>>;
406
+ tiles: z.ZodOptional<z.ZodObject<{
407
+ check: z.ZodOptional<z.ZodBoolean>;
408
+ preview: z.ZodOptional<z.ZodBoolean>;
409
+ review: z.ZodOptional<z.ZodBoolean>;
410
+ }, z.core.$strict>>;
381
411
  }, z.core.$strip>>>;
382
412
  runtimes: z.ZodObject<{
383
413
  default: z.ZodString;
@@ -437,19 +467,10 @@ export interface TutorView {
437
467
  policy: Policy;
438
468
  /** 老师卡上的常驻入口(已按学科补缺省) */
439
469
  extras: TutorExtra[];
470
+ /** 开着的方砖(没管家里有没有那个文件) */
471
+ tiles: HomeTileId[];
440
472
  }
441
473
  /** 老师列表(带有效政策);kidOnly = 只要孩子端能看到的(enabled 且不 hidden) */
442
474
  export declare function listTutors(config: CotutorConfig, opts?: {
443
475
  kidOnly?: boolean;
444
476
  }): TutorView[];
445
- /** 运行时模板填占位符;{tools} 填适配器给的工具名单(src/brain/cli/ 的 fillArgs 先算好,没给就填空);{agentBody} / {systemBody} / {boardFile} 只在给了值时替换,否则原样留着(doctor 会报);{effort} 没给用出厂缺省(课文件排版这类不认老师政策的调用) */
446
- export declare function fillRuntime(argv: readonly string[], vars: {
447
- agent: string;
448
- prompt: string;
449
- session?: string;
450
- agentBody?: string;
451
- systemBody?: string;
452
- boardFile?: string;
453
- effort?: Policy['effort'];
454
- tools?: string;
455
- }): string[];