cotutor 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +9 -7
  2. package/NOTICE +3 -3
  3. package/README.md +36 -13
  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 +5 -0
  17. package/dist/app/cli/cert.js +20 -7
  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 +6 -5
  22. package/dist/app/cli/doctor.js +107 -171
  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 +347 -24
  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 +138 -0
  42. package/dist/app/cli/service.js +429 -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 +33 -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 +205 -0
  58. package/dist/app/cli/tray.js +618 -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 +45 -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/mock/mock-live.js +1 -1
  77. package/dist/app/mock/mock.d.ts +4 -2
  78. package/dist/app/mock/mock.js +20 -12
  79. package/dist/app/replay.js +4 -4
  80. package/dist/app/skills/home-doc.js +1 -1
  81. package/dist/app/workspace.d.ts +9 -4
  82. package/dist/app/workspace.js +33 -10
  83. package/dist/app/ws/route.d.ts +8 -2
  84. package/dist/app/ws/route.js +7 -3
  85. package/dist/board/parse.d.ts +0 -2
  86. package/dist/board/parse.js +0 -6
  87. package/dist/board/presentation.d.ts +9 -15
  88. package/dist/board/presentation.js +8 -10
  89. package/dist/brain/api-tools.js +2 -1
  90. package/dist/brain/api.d.ts +12 -17
  91. package/dist/brain/api.js +16 -33
  92. package/dist/brain/index.d.ts +23 -71
  93. package/dist/brain/index.js +6 -69
  94. package/dist/brain/log.d.ts +5 -0
  95. package/dist/brain/log.js +22 -0
  96. package/dist/brain/runtime.d.ts +30 -0
  97. package/dist/brain/runtime.js +23 -0
  98. package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
  99. package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
  100. package/dist/brain/stream.d.ts +5 -5
  101. package/dist/brain/stream.js +7 -7
  102. package/dist/brain/transcript.d.ts +3 -3
  103. package/dist/brain/transcript.js +6 -6
  104. package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
  105. package/dist/brain/types.js +14 -0
  106. package/dist/chalk/glyphs/skeleton.d.ts +3 -3
  107. package/dist/chalk/glyphs/skeleton.js +1 -1
  108. package/dist/lib/index.d.ts +1 -1
  109. package/dist/lib/index.js +1 -1
  110. package/dist/memory/diary.js +3 -3
  111. package/dist/realtime/bailian.d.ts +1 -1
  112. package/dist/realtime/bailian.js +3 -2
  113. package/dist/realtime/preview.js +1 -1
  114. package/dist/realtime/voices.d.ts +2 -0
  115. package/dist/realtime/voices.js +57 -57
  116. package/dist/runtime/activities.d.ts +13 -1
  117. package/dist/runtime/activities.js +28 -2
  118. package/dist/runtime/activity-state.js +2 -2
  119. package/dist/runtime/agent-file.d.ts +3 -3
  120. package/dist/runtime/agent-file.js +3 -3
  121. package/dist/runtime/context/tutor-rules.d.ts +2 -2
  122. package/dist/runtime/context/tutor-rules.js +3 -3
  123. package/dist/runtime/home/home.d.ts +12 -1
  124. package/dist/runtime/home/home.js +8 -1
  125. package/dist/runtime/home/store.d.ts +1 -1
  126. package/dist/runtime/home/store.js +36 -21
  127. package/dist/runtime/journal/mix.d.ts +15 -6
  128. package/dist/runtime/journal/mix.js +60 -19
  129. package/dist/runtime/journal/recorder.d.ts +58 -13
  130. package/dist/runtime/journal/recorder.js +106 -16
  131. package/dist/runtime/journal/store.d.ts +4 -4
  132. package/dist/runtime/journal/store.js +6 -6
  133. package/dist/runtime/kid-input.js +2 -3
  134. package/dist/runtime/live/call.d.ts +12 -0
  135. package/dist/runtime/live/call.js +51 -18
  136. package/dist/runtime/live/turns.js +1 -1
  137. package/dist/runtime/runner.d.ts +9 -55
  138. package/dist/runtime/runner.js +52 -173
  139. package/dist/scenes/paint/painter.js +4 -3
  140. package/dist/schema/config.d.ts +43 -22
  141. package/dist/schema/config.js +45 -47
  142. package/dist/schema/conversation.d.ts +6 -0
  143. package/dist/schema/home.d.ts +11 -1
  144. package/dist/schema/home.js +7 -2
  145. package/dist/schema/workspace.d.ts +27 -0
  146. package/dist/schema/workspace.js +28 -0
  147. package/dist/shared/default-port.d.ts +2 -0
  148. package/dist/shared/default-port.js +2 -0
  149. package/dist/shared/qianwen.d.ts +14 -10
  150. package/dist/shared/qianwen.js +26 -22
  151. package/dist/shared/site-url.d.ts +2 -0
  152. package/dist/shared/site-url.js +5 -0
  153. package/dist/ui/dev/dev-page.js +25 -252
  154. package/dist/ui/dev/dev-theme.d.ts +1 -1
  155. package/dist/ui/dev/dev-theme.js +1 -1
  156. package/dist/ui/dev/voice-gallery-page.js +1 -1
  157. package/dist/ui/kid/board-mark.d.ts +2 -2
  158. package/dist/ui/kid/board-mark.js +2 -2
  159. package/dist/ui/kid/home-tiles.d.ts +14 -0
  160. package/dist/ui/kid/home-tiles.js +44 -0
  161. package/dist/ui/kid/kid-page.js +68 -103
  162. package/dist/ui/kid/voice-ui.d.ts +1 -1
  163. package/dist/ui/kid/voice-ui.js +1 -1
  164. package/dist/ui/kid/word-emoji.js +5 -3
  165. package/dist/ui/parent/parent-home-page.js +315 -7
  166. package/dist/ui/parent/parent-page.js +6 -2
  167. package/dist/ui/parent/parent-settings-page.d.ts +1 -0
  168. package/dist/ui/parent/parent-settings-page.js +133 -0
  169. package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
  170. package/dist/ui/{dev → parent}/qr-page.js +19 -14
  171. package/dist/ui/shared/brand.d.ts +3 -1
  172. package/dist/ui/shared/brand.js +3 -1
  173. package/grades/grade-2/README.md +1 -1
  174. package/grades/grade-2/VALIDATION.md +1 -1
  175. package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
  176. 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
  177. 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
  178. package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
  179. package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
  180. 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
  181. 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
  182. package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
  183. package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
  184. 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
  185. 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
  186. package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
  187. package/package.json +2 -1
  188. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
  189. package/skills/cotutor-board/references/photo.md +2 -2
  190. package/skills/cotutor-home/SKILL.md +1 -0
  191. package/skills/cotutor-home/references/tutor.md +2 -1
  192. package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
  193. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
  194. package/skills/cotutor-tutor/SKILL.md +5 -13
  195. package/dist/brain/cli/adapters.d.ts +0 -60
  196. package/dist/brain/cli/adapters.js +0 -91
  197. package/dist/brain/cli/qwen.d.ts +0 -11
  198. package/dist/brain/cli/qwen.js +0 -141
  199. package/dist/brain/cli/run-plan.d.ts +0 -59
  200. package/dist/brain/cli/run-plan.js +0 -71
  201. package/dist/brain/cli/run.d.ts +0 -35
  202. package/dist/brain/cli/run.js +0 -256
  203. package/dist/brain/cli/types.js +0 -3
  204. package/dist/brain/cli/warm.d.ts +0 -53
  205. package/dist/brain/cli/warm.js +0 -103
@@ -11,9 +11,8 @@
11
11
  * 进程 cwd 是老师目录 agents/<name>/。
12
12
  */
13
13
  import { type IncomingMark } from './lecture.ts';
14
- import { type BrainExecutor, type Slot } from '../brain/index.ts';
14
+ import { type BrainExecutor, type RunPlan } from '../brain/index.ts';
15
15
  import { type BoardSection, type Device } from '../board/presentation.ts';
16
- import { type RunPlan } from '../brain/cli/run-plan.ts';
17
16
  import { type RunEvent, type RunEventEnvelope } from '../schema/index.ts';
18
17
  import { type ContextPack, type ConversationIndex, type ActivityRun, type ConversationMessage, type Focus, type MessageFrom, type MessageVia } from '../schema/index.ts';
19
18
  import type { Workspace } from '../schema/index.ts';
@@ -59,7 +58,7 @@ export interface SendInput {
59
58
  drawing?: NonNullable<ConversationMessage['drawing']>;
60
59
  /** 这条是回放(app/replay.ts):原轮的 job,记进消息;调用方已经把 Runner 指到 evals/ */
61
60
  replayOf?: string;
62
- /** cotutor activity eval 跑的一轮:不写记忆、不预热(画照画,画家由 eval 换成假的) */
61
+ /** cotutor activity eval 跑的一轮:不写记忆(画照画,画家由 eval 换成假的) */
63
62
  eval?: true;
64
63
  /** 孩子看完小课堂后的第一条:上下文包带 lecture:(课的每句、看的情况、画过的几处),消息记 lecture;again = 问过以后再看一遍又画了 */
65
64
  lecture?: {
@@ -130,58 +129,35 @@ export declare function attachActivity(ws: Workspace, tutor: string, pack: Conte
130
129
  thread: string;
131
130
  now: number;
132
131
  }, inSystem?: boolean): Promise<string[]>;
133
- /** 按老师裁的板书写法落在哪(相对 workspace 根;机器文件,内容变了文件名跟着变) */
134
- export declare const TUTOR_BOARD_DIR = ".cotutor/board";
135
132
  /** activity 在系统提示里时,上下文包 activityGuide 那一行 */
136
133
  export declare const ACTIVITY_IN_SYSTEM = "\u5DF2\u5728\u4F60\u7684\u7CFB\u7EDF\u63D0\u793A\u91CC(\u8FD9\u4E2A\u8BDD\u9898\u7684 activity,\u7167\u5B83\u5E26)";
137
134
  /**
138
135
  * activity 话题的系统提示(九点五):不读老师文件,人设与做法都是 activity 自己的。
139
136
  * body 「老师怎么带」按参数与这个话题的卡裁好、剥了注释
140
137
  * kinds 这个话题的卡:activity 头上的 cards 加 text(不并老师的)
141
- * guide 只讲这几种卡的板书写法,写到 .cotutor/board/
138
+ * guide 只讲这几种卡的板书写法
142
139
  * memory 这个 activity 自己的记忆:agent: <老师>/<activity>,文件名 <老师显示名>·<activity>
143
140
  * activity 文件不在、没有「老师怎么带」→ null(退回老师文件)。
144
141
  */
145
142
  export interface ActivityParts {
146
143
  body: string;
147
144
  kinds: string[];
148
- guide: {
149
- body: string;
150
- path: string;
151
- };
145
+ guide: string;
152
146
  memory: {
153
147
  agent: string;
154
148
  display: string;
155
149
  };
156
150
  }
157
151
  export declare function activityParts(ws: Workspace, tutor: string, run: ActivityRun): Promise<ActivityParts | null>;
158
- /**
159
- * 这位老师的板书写法:正文 + 相对 workspace 根的路径(给 {boardFile} 与上下文包)。
160
- * 没写 cards → 出厂的 cotutor-board SKILL.md;写了 → 按清单从包里现拼(boardGuideFor),写到 .cotutor/board/<老师>-<hash>.md,
161
- * 文件名带内容的 hash:清单一改路径就变,预热进程的 argv 对不上,不会拿着旧写法去答。读不到 SKILL.md → null。
162
- */
163
- export declare function boardGuideOf(ws: Workspace, tutor: string): Promise<{
164
- body: string;
165
- path: string;
166
- } | null>;
167
- /**
168
- * 板书写法怎么递给老师(2026-09-19):有脸的老师、板书没关才带。运行时预载了 → 上下文包只写一行事实;
169
- * 没预载 → 原文作为一条笔记放进话题第一条(<cotutor-board>),排在守则后面,续话题没改过写「未变」。返回板书写法(给 {systemBody} 与 {boardFile})。
170
- */
171
- export declare function attachBoardGuide(ws: Workspace, tutor: string, pack: ContextPack, o: {
172
- preloaded: boolean;
173
- boardOff: boolean;
174
- }): Promise<{
175
- body: string;
176
- path: string;
177
- } | null>;
152
+ /** 这位老师的板书写法:没写 cards → 出厂的 cotutor-board SKILL.md;写了 → 按清单从包里现拼(boardGuideFor)。读不到 SKILL.md → null */
153
+ export declare function boardGuideOf(ws: Workspace, tutor: string): Promise<string | null>;
178
154
  /** 这轮带的笔记版本:role → `路径@hash`(记进消息;同一话题下一轮对得上就不再带原文) */
179
155
  export declare function noteVersions(pack: ContextPack): Record<string, string>;
180
156
  /** 续会话时:上一轮这个话题已经带过、版本没变的笔记去掉原文,YAML 里注明「未变」 */
181
157
  export declare function dropSeenNotes(pack: ContextPack, seen: Record<string, string>): ContextPack;
182
158
  /** 这个话题里最近一次记下的笔记版本(新话题、没记过 → {}) */
183
159
  export declare function seenNotes(messages: readonly ConversationMessage[], thread: string): Record<string, string>;
184
- /** 老师该读得到的目录:workspace 根(作业照片、技能)与 vault(教材、日记);会话 cwd 是老师目录,这些在它外面(qwen 读外面要先划进来) */
160
+ /** 只读工具读得到的目录:workspace 根(作业照片、技能)与 vault(教材、日记) */
185
161
  export declare function readDirsOf(ws: Workspace): string[];
186
162
  /** 干跑:现在给这位老师发这句话,上下文包会是什么样、每段从哪来、截了多少;不起模型、不写盘 */
187
163
  export declare function packDryRun(ws: Workspace, tutor: string, input: {
@@ -205,16 +181,9 @@ export declare class Runner {
205
181
  private readonly listeners;
206
182
  /** noteEvent 每个事件文件一条追加链 */
207
183
  private readonly notes;
208
- /** 脑执行:起老师、读输出、断流接着跑、预热下一轮(src/brain/) */
184
+ /** 脑执行:请模型、读输出(src/brain/) */
209
185
  readonly brain: BrainExecutor;
210
- /** 预热池的旧入口(测试看有没有、杀掉);迁完删 */
211
- get spares(): {
212
- has(tutor: string, slot?: Slot): boolean;
213
- drop(tutor: string, slot?: Slot): void;
214
- };
215
- /** 页面拉今天时的预热:每位老师(与带的话题)上次看的时刻 */
216
- private readonly prewarmedAt;
217
- /** 起子进程用的环境(测试注入) */
186
+ /** key、假端点从哪读的环境(测试注入) */
218
187
  get env(): NodeJS.ProcessEnv | undefined;
219
188
  /** 当前的 workspace(cotutor.json 热重载后是新的;实时老师经它拿) */
220
189
  get ws(): Workspace;
@@ -223,8 +192,6 @@ export declare class Runner {
223
192
  constructor(getWs: () => Workspace, opts?: {
224
193
  now?: () => Date;
225
194
  env?: NodeJS.ProcessEnv;
226
- warmIdleMs?: number;
227
- warm?: boolean;
228
195
  draw?: Draw;
229
196
  });
230
197
  /** 孩子端发一条的验与拼(HTTP 与实时老师共用,./kid-input.ts):拼成 send 的入参,或回 reject;不起老师 */
@@ -239,19 +206,6 @@ export declare class Runner {
239
206
  onEvent(fn: (e: RunEventEnvelope) => void): () => void;
240
207
  /** 别处给某一轮补事件(实时老师的嘴,src/runtime/live/call.ts;t 已经换成离这一轮老师进程起来的毫秒):追加到 events.jsonl、给订阅者;这一轮跑完了也行 */
241
208
  noteEvent(tutor: string, date: string, job: string, event: RunEvent): Promise<void>;
242
- /** 杀掉所有预热的进程(测试收尾;服务退出时它们的 stdin 断了会自己退) */
243
- close(): void;
244
- /**
245
- * 预热:给这位老师起好下一轮的进程,两个位置都补齐——
246
- * fresh:新开会话(新话题、接着上次都用它);
247
- * resume:接 thread(页面选着的话题;不给或今天没有 = 当天末条所在的话题,和 send 缺省一样)的会话,它没有会话就不留。
248
- * 只给有脸的、开着的、运行时 stdin: "stream-json"、这会儿没在跑的老师起;throttleMs 内同一话题看过就不再看(页面轮询今天)。
249
- * 起了返回 true;起不了不报错,下一轮冷起。warm: false(cotutor send:跑完就退,留着备用进程会把它挂一小时)不起。
250
- */
251
- prewarm(tutor: string, opts?: {
252
- throttleMs?: number;
253
- thread?: string;
254
- }): Promise<boolean>;
255
209
  /** 正在回的那条已经出来的板书(流式);没在跑或还没出卡 → null */
256
210
  partial(tutor: string): BoardSection | null;
257
211
  send(tutor: string, input: SendInput): Promise<SendStarted>;
@@ -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 };