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
@@ -14,14 +14,14 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'no
14
14
  import { readdir } from 'node:fs/promises';
15
15
  import { createServer as createHttp } from 'node:http';
16
16
  import { createServer as createHttps } from 'node:https';
17
- import { hostname, tmpdir } from 'node:os';
17
+ import { tmpdir } from 'node:os';
18
18
  import { join } from 'node:path';
19
19
  import { parseBoard } from "../../board/parse.js";
20
20
  import { cardAssets, parseCardState, stripSecrets, DictationPropsSchema } from "../../board/cards/index.js";
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { bundleAsset, stageAsset, stageIndex, stageVersion } from "../http/stage.js";
23
23
  import { sendFile } from "../http/send-file.js";
24
- import { subjectLang, tutorExtras } from "../../schema/index.js";
24
+ import { HOME_TILES, HOME_TILE_IDS, canonicalSubject, subjectLang, tutorExtras } from "../../schema/index.js";
25
25
  import { NEUTRAL_COLOR, tutorColors } from "../../ui/shared/tutor-color.js";
26
26
  import { boardMarkOf } from "../../ui/kid/board-mark.js";
27
27
  import { backTutorOf } from "../../ui/kid/tutor-face.js";
@@ -67,15 +67,15 @@ import { wavOfPcm } from "../../shared/wav.js";
67
67
  import { buildReel, playRecordOk, reelHolds, reelMissRuns } from "../../ui/kid/reel.js";
68
68
  import { weekOf } from "../../shared/time.js";
69
69
  import {} from "../../schema/index.js";
70
- import { lanAddresses, listenInfo } from "../cli/serve.js";
71
- import { qrPage } from "../../ui/dev/qr-page.js";
70
+ import { lanAddresses, listenInfo, localName, siteUrl } from "../cli/serve.js";
71
+ import { qrPage } from "../../ui/parent/qr-page.js";
72
72
  import { USER_CERT_DIR } from "../workspace.js";
73
73
  import { kidThreads } from "../../runtime/kid-view.js";
74
74
  import { ICON_SIZES, appIconPng, webManifest } from "../../ui/shared/icon.js";
75
75
  import { faviconResponse } from "../../ui/shared/favicon.js";
76
76
  import { kidPage } from "../../ui/kid/kid-page.js";
77
77
  import { parentPage } from "../../ui/parent/parent-page.js";
78
- import { arrangeHome, homeOwners, kidButtons, parseHome } from "../../runtime/home/home.js";
78
+ import { arrangeHome, homeOwners, kidButtons, kidTiles, parseHome } from "../../runtime/home/home.js";
79
79
  import { packageTheme } from "../cli/themes.js";
80
80
  import { cardsCss } from "../../board/cards/docs.js";
81
81
  import { VOICE_GALLERY_PAGE } from "../../ui/dev/voice-gallery-page.js";
@@ -533,7 +533,9 @@ export function createMock(opts = {}) {
533
533
  // mock 的话题都是新话题进去的:今天孩子问过的那个,挂在新话题按钮上(空白板上的胶囊)
534
534
  const resumes = asked ? new Map([['free', { thread: asked.thread, title: (asked.question ?? '').slice(0, 10), at: list[list.length - 1]?.at ?? '' }]]) : undefined;
535
535
  const alive = (date, thread) => (date === yesterday() && (past.get(name) ?? []).some((m) => m.thread === thread)) || (date === localDate(d) && list.some((m) => m.thread === thread));
536
- return { kind: 'tutor', props: { tutor: name, buttons: kidButtons((c.props.buttons ?? []), { resumes, alive, lecture: (id) => Boolean(mockLecture(id)), today: localDate(d) }).map((b) => { const l = b.kind === 'lecture' ? mockLecture(b.bundle) : null; return l ? { ...b, title: l.title, ms: l.ms, ...(l.video ? { video: true } : {}) } : b; }) } };
536
+ // 方砖:三门主科出厂都有预习、检查作业、复习(真服务看老师家里有没有那个 activity 文件)
537
+ const main = ['语文', '数学', '英语'].includes(canonicalSubject(MOCK_TUTORS.find((t) => t.name === name)?.subject) ?? '');
538
+ return { kind: 'tutor', props: { tutor: name, tiles: main ? kidTiles(HOME_TILE_IDS, { usable: () => true }) : [], buttons: kidButtons((c.props.buttons ?? []), { resumes, alive, lecture: (id) => Boolean(mockLecture(id)), today: localDate(d) }).map((b) => { const l = b.kind === 'lecture' ? mockLecture(b.bundle) : null; return l ? { ...b, title: l.title, ms: l.ms, ...(l.video ? { video: true } : {}) } : b; }) } };
537
539
  });
538
540
  // figshot:和配了 figshot 的 workspace 一样给端口;这台电脑上 figshot 没开着,页面照样藏着这张卡
539
541
  return { title, date: localDate(d), tutors: tutorsJson(), home: mockHomeId(), cards, figshot: { port: 8477 }, hand: 'right', ready: scenario !== 'nokey' };
@@ -579,6 +581,9 @@ export function createMock(opts = {}) {
579
581
  return null;
580
582
  if (via.button === 'new' || via.button === 'recent')
581
583
  return via.button;
584
+ const tile = HOME_TILES.find((t) => t.id === via.button);
585
+ if (tile)
586
+ return { kind: 'activity', label: tile.activity, activity: tile.activity, params: {} };
582
587
  if (via.home !== mockHomeId() || typeof via.button !== 'number')
583
588
  return null;
584
589
  const card = parseHome(mockHomeMd(localDate(now()), yesterday())).cards.find((c) => c.kind === 'tutor' && c.props.tutor === name);
@@ -626,13 +631,14 @@ export function createMock(opts = {}) {
626
631
  setTimeout(reveal, tick);
627
632
  });
628
633
  };
629
- const route = async (method, path, body) => {
634
+ /** internal = 实时那条连接交来的孩子发的一条(kidSend);HTTP 上不收(同真服务,2026-11-04 删了 HTTP 的发) */
635
+ const routeIn = async (method, path, body, internal = false) => {
630
636
  await lecturesLoaded;
631
637
  const url = new URL(path, 'http://x');
632
638
  const p = url.pathname;
633
639
  if (p === '/')
634
640
  return { status: 200, html: kidPage(title, {}, await stageVersion()) };
635
- if (p === '/dev/qr')
641
+ if (p === '/parent/qr')
636
642
  return mock.listen ? { status: 200, html: qrPage(title, mock.listen(), url.searchParams.get('via') === 'ip' ? 'ip' : 'name', url.searchParams.get('to') === 'parent' ? 'parent' : 'kid') } : { status: 404, json: { error: 'not_listening' } };
637
643
  if (p === '/dev/voice')
638
644
  return { status: 200, html: VOICE_GALLERY_PAGE };
@@ -884,7 +890,7 @@ export function createMock(opts = {}) {
884
890
  const out = { tutor: name, date: tail, messages: await Promise.all(old.map(kidMessage)), pending: null, thread: old.length ? old[old.length - 1].thread : null };
885
891
  return { status: 200, json: out };
886
892
  }
887
- if (tail === 'messages' && method === 'POST') {
893
+ if (tail === 'messages' && method === 'POST' && internal) {
888
894
  if (!isObj(body) || typeof body.text !== 'string')
889
895
  return { status: 400, json: { error: 'bad_request' } };
890
896
  const action = body.action === 'submit' ? 'submit' : undefined;
@@ -1085,7 +1091,9 @@ export function createMock(opts = {}) {
1085
1091
  })().catch((err) => { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'internal', message: String(err) })); });
1086
1092
  };
1087
1093
  const settleJob = async (job) => { await inflight.get(job); };
1088
- const mock = { route, settle, settleJob, handler, listen: null };
1094
+ const route = (method, path, body) => routeIn(method, path, body);
1095
+ const kidSend = (tutor, body) => routeIn('POST', `/api/kid/conversations/${tutor}/messages`, isObj(body) ? { ...body, live: true } : body, true);
1096
+ const mock = { route, kidSend, settle, settleJob, handler, listen: null };
1089
1097
  return mock;
1090
1098
  }
1091
1099
  export async function serveMock(opts = {}) {
@@ -1111,7 +1119,7 @@ export async function serveMock(opts = {}) {
1111
1119
  });
1112
1120
  const port = server.address().port;
1113
1121
  const scheme = tls ? 'https' : 'http';
1114
- const urls = [hostname(), ...lanAddresses()].map((h) => `${scheme}://${h}:${port}/`);
1122
+ const urls = [localName(), ...lanAddresses()].map((h) => siteUrl(scheme, h, port));
1115
1123
  mock.listen = () => listenInfo(Boolean(tls), port);
1116
- return { server, mock, port, https: Boolean(tls), urls, qrPage: `${scheme}://localhost:${port}/dev/qr` };
1124
+ return { server, mock, port, https: Boolean(tls), urls, qrPage: siteUrl(scheme, 'localhost', port, '/parent/qr') };
1117
1125
  }
@@ -10,9 +10,9 @@
10
10
  import { readFile } from 'node:fs/promises';
11
11
  import { join } from 'node:path';
12
12
  import { cardLabel } from "../board/cards/index.js";
13
- import { logParser } from "../brain/index.js";
13
+ import { parseLogLine } from "../brain/index.js";
14
14
  import { UsageError } from "../shared/errors.js";
15
- import { conversationFiles, threads } from "../schema/index.js";
15
+ import { conversationFiles, threads, viaSendsLabel } from "../schema/index.js";
16
16
  import { compactDiff, diffLines } from "./diff.js";
17
17
  import { tokenText } from "../shared/tokens.js";
18
18
  import { toolCalls } from "../brain/transcript.js";
@@ -61,7 +61,7 @@ export async function startReplay(ws, tutor, date, job, opts = {}) {
61
61
  replayOf: job,
62
62
  runtime: opts.runtime,
63
63
  ...(m.via ? { via: m.via } : {}),
64
- ...(m.via && typeof m.via.button === 'number' ? { home: { button: m.via.label, ...(button?.brief ? { brief: button.brief } : {}) } } : {}),
64
+ ...(m.via && viaSendsLabel(m.via) ? { home: { button: m.via.label, ...(button?.brief ? { brief: button.brief } : {}) } } : {}),
65
65
  ...(m.continues && continued ? { continues: { ...m.continues, pack: continued } } : {}),
66
66
  ...(activity ? { activity } : {}),
67
67
  // 原轮是实时的:回放也按实时写(上下文包带 live:、不补提问卡);嘴不在,接住与打断的 caught / cut 不带
@@ -119,7 +119,7 @@ async function toolsOf(ws, tutor, date, m) {
119
119
  if (m.tools)
120
120
  return m.tools;
121
121
  const text = await readFile(conversationFiles(ws.dirs.conversations, tutor, date).log(m.job), 'utf8').catch(() => '');
122
- return toolCalls(text, logParser(ws.config.runtimes, m.runtime));
122
+ return toolCalls(text, parseLogLine);
123
123
  }
124
124
  export async function compareReplay(ws, tutor, date, evalJob) {
125
125
  const evalWs = evalWorkspace(ws);
@@ -35,7 +35,7 @@ export function homeSyntaxDoc(dir = PACKAGE_CARDS_DIR) {
35
35
  - **第一个 \`## \` 起是给家长的**(「## 为什么这么排」),孩子看不到。
36
36
  - 围栏外的普通行首页不显示(检查会提醒),要给孩子看的写进卡里。
37
37
 
38
- 应用保证的:每位孩子端露出的老师都有一张老师卡(没写就补一张只有「新话题」的);「新话题」永远是第一个按钮;应用不加「接着刚才的」——今天从同一个入口聊过的,孩子进老师页后在空白板上接(新话题是一个胶囊,activity、小课堂先问接着那个还是重新开始);引用坏了(话题找不到)的按钮不出现。
38
+ 应用保证的:每位孩子端露出的老师都有一张老师卡(没写就补一张只有「新话题」的);「新话题」永远是第一个按钮;老师家里有预习、检查作业、复习这几个 activity 的,新话题下面常驻这几块方砖(不用写,家长端能关,不占按钮的名额);应用不加「接着刚才的」——今天从同一个入口聊过的,孩子进老师页后在空白板上接(新话题是一个胶囊,activity、小课堂先问接着那个还是重新开始);引用坏了(话题找不到)的按钮不出现。
39
39
 
40
40
  老师卡的按钮最多 ${TUTOR_BUTTONS_MAX} 个,每个最多 ${BUTTON_LABEL_MAX} 个字。
41
41
 
@@ -1,6 +1,6 @@
1
1
  import { ConfigError, UsageError } from '../shared/errors.ts';
2
2
  import { redactDeep, redactHome } from '../shared/home.ts';
3
- import { type RootSource, type Workspace } from '../schema/workspace.ts';
3
+ import { type RootSource, type SandboxMark, type Workspace } from '../schema/workspace.ts';
4
4
  import { type CotutorConfig } from '../schema/index.ts';
5
5
  export { ConfigError, UsageError, redactDeep, redactHome };
6
6
  export type { RootSource, Workspace };
@@ -29,6 +29,8 @@ export declare function resolveRoot(override?: string, opts?: ResolveOptions): R
29
29
  /** 校验 cotutor.json 的形状;失败 → ConfigError,信息是逐条修复指南 */
30
30
  export declare function parseConfig(raw: unknown, file: string): CotutorConfig;
31
31
  export declare function resolvePaths(root: string, paths: Record<string, string>): Workspace['paths'];
32
+ /** 根下的 sandbox 标记(.cotutor/sandbox.json):没有 → null;坏了响亮报错(不当成真 workspace 静默往下跑) */
33
+ export declare function readSandboxMark(root: string): SandboxMark | null;
32
34
  export declare function assembleWorkspace(root: string, source: RootSource, config: CotutorConfig): Workspace;
33
35
  /** 定根 + 读配置 + 校验 + 展开角色。cotutor.json 缺失也是错(它是必需的)。 */
34
36
  export declare function loadWorkspace(override?: string, opts?: ResolveOptions): Workspace;
@@ -51,8 +53,11 @@ export declare function deepMerge(base: unknown, patch: unknown): unknown;
51
53
  export declare function patchConfig(ws: Workspace, patch: Record<string, unknown>, opts?: {
52
54
  also?: readonly string[];
53
55
  }): Promise<CotutorConfig>;
54
- /** 文件改了(页面写的、或家长在编辑器改的)就重读;坏了抛 ConfigError,调用方决定是留旧的还是响 */
55
- export declare function reloadIfChanged(ws: Workspace, lastMtime: number): Promise<{
56
+ /**
57
+ * 文件内容变了(页面写的、或家长在编辑器改的)就重读;坏了抛 ConfigError,调用方决定是留旧的还是响。
58
+ * 比原文不比修改时间:Windows 上写 .tmp 再 rename 连写两次,修改时间常常一模一样(10-31 实测 200 次里 105 次);文件只有几 K,每次读一遍不贵。
59
+ */
60
+ export declare function reloadIfChanged(ws: Workspace, lastText: string | null): Promise<{
56
61
  ws: Workspace;
57
- mtime: number;
62
+ text: string;
58
63
  }>;
@@ -7,10 +7,10 @@
7
7
  * 配置"在但解析不了"响亮报错,不静默走缺省(沿 drawtell 的教训)。
8
8
  */
9
9
  import { readFileSync, readdirSync, statSync } from 'node:fs';
10
- import { writeFile, rename, stat } from 'node:fs/promises';
10
+ import { readFile, writeFile, rename } from 'node:fs/promises';
11
11
  import { ConfigError, UsageError } from "../shared/errors.js";
12
12
  import { redactDeep, redactHome } from "../shared/home.js";
13
- import { SKILLS_DIR } from "../schema/workspace.js";
13
+ import { SANDBOX_MARK, SKILLS_DIR, SandboxMarkSchema } from "../schema/workspace.js";
14
14
  import { homedir } from 'node:os';
15
15
  import { dirname, isAbsolute, join, resolve } from 'node:path';
16
16
  import { CotutorConfigSchema, PATH_ROLES, ROLE_DEFAULTS, WORKSPACE_ROLES, explainIssues, } from "../schema/index.js";
@@ -136,11 +136,24 @@ export function resolvePaths(root, paths) {
136
136
  out[role] = expandPath(value, vault);
137
137
  return out;
138
138
  }
139
+ /** 根下的 sandbox 标记(.cotutor/sandbox.json):没有 → null;坏了响亮报错(不当成真 workspace 静默往下跑) */
140
+ export function readSandboxMark(root) {
141
+ const file = join(root, SANDBOX_MARK);
142
+ const raw = readJson(file);
143
+ if (raw === null)
144
+ return null;
145
+ const r = SandboxMarkSchema.safeParse(raw);
146
+ if (!r.success)
147
+ throw new ConfigError(file, explainIssues(r.error.issues).map((l) => ` - ${l}`).join('\n'));
148
+ return r.data;
149
+ }
139
150
  export function assembleWorkspace(root, source, config) {
151
+ const sandbox = readSandboxMark(root);
140
152
  return {
141
153
  root,
142
154
  source,
143
155
  config,
156
+ ...(sandbox ? { sandbox } : {}),
144
157
  paths: resolvePaths(root, config.paths),
145
158
  dirs: {
146
159
  agents: join(root, 'agents'),
@@ -178,6 +191,7 @@ export function workspaceReport(ws) {
178
191
  vault: ws.paths.vault,
179
192
  tutors: Object.keys(ws.config.tutors),
180
193
  runtime: ws.config.runtimes.default,
194
+ ...(ws.sandbox ? { sandbox: { from: ws.sandbox.from, forkedAt: ws.sandbox.forkedAt } } : {}),
181
195
  });
182
196
  }
183
197
  // ---- cotutor.json 的补丁写回与热重载(从 runtime/journal/store.ts 搬来,2026-10-29) ----
@@ -233,13 +247,22 @@ export async function patchConfig(ws, patch, opts = {}) {
233
247
  await rename(tmp, ws.files.config);
234
248
  return r.data;
235
249
  }
236
- /** 文件改了(页面写的、或家长在编辑器改的)就重读;坏了抛 ConfigError,调用方决定是留旧的还是响 */
237
- export async function reloadIfChanged(ws, lastMtime) {
238
- const st = await stat(ws.files.config);
239
- if (st.mtimeMs === lastMtime)
240
- return { ws, mtime: lastMtime };
241
- const raw = readJson(ws.files.config);
242
- if (raw === null)
250
+ /**
251
+ * 文件内容变了(页面写的、或家长在编辑器改的)就重读;坏了抛 ConfigError,调用方决定是留旧的还是响。
252
+ * 比原文不比修改时间:Windows 上写 .tmp 再 rename 连写两次,修改时间常常一模一样(10-31 实测 200 次里 105 次);文件只有几 K,每次读一遍不贵。
253
+ */
254
+ export async function reloadIfChanged(ws, lastText) {
255
+ const text = await readFile(ws.files.config, 'utf8').catch(() => null);
256
+ if (text === null)
243
257
  throw new ConfigError(ws.files.config, '不存在了');
244
- return { ws: assembleWorkspace(ws.root, ws.source, parseConfig(raw, ws.files.config)), mtime: st.mtimeMs };
258
+ if (text === lastText)
259
+ return { ws, text };
260
+ let raw;
261
+ try {
262
+ raw = JSON.parse(text);
263
+ }
264
+ catch (err) {
265
+ throw new ConfigError(ws.files.config, `不是合法 JSON:${err instanceof Error ? err.message : String(err)}`);
266
+ }
267
+ return { ws: assembleWorkspace(ws.root, ws.source, parseConfig(raw, ws.files.config)), text };
245
268
  }
@@ -18,8 +18,14 @@ export interface LiveRouteDeps {
18
18
  echoWaitMs?: number;
19
19
  ringMs?: number;
20
20
  stallMs?: number;
21
+ idleCutMs?: number;
22
+ maxTakeMs?: number;
21
23
  onFinished?(tutor: string, why: string): void;
22
24
  }
23
- export declare function makeLiveUpgrade(deps?: LiveRouteDeps): (req: IncomingMessage, socket: Duplex, head: Buffer, ctx: LiveApp) => Promise<boolean>;
25
+ /** 接升级的函数;openCalls = 现在开着几通(serve 看它定这台电脑能不能睡) */
26
+ export type LiveUpgrade = ((req: IncomingMessage, socket: Duplex, head: Buffer, ctx: LiveApp) => Promise<boolean>) & {
27
+ openCalls(): number;
28
+ };
29
+ export declare function makeLiveUpgrade(deps?: LiveRouteDeps): LiveUpgrade;
24
30
  /** 探针:COTUTOR_LIVE_ENDPOINT 指到假百炼 */
25
- export declare const liveUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer, ctx: LiveApp) => Promise<boolean>;
31
+ export declare const liveUpgrade: LiveUpgrade;
@@ -3,7 +3,8 @@ import { acceptWebSocket, isUpgrade } from "./ws.js";
3
3
  import { cookieOf } from "../http/access.js";
4
4
  import { DEVICE_COOKIE } from "../../schema/kid-device.js";
5
5
  export function makeLiveUpgrade(deps = {}) {
6
- return async function liveUpgrade(req, socket, head, ctx) {
6
+ const open = new Set();
7
+ const liveUpgrade = async function liveUpgrade(req, socket, head, ctx) {
7
8
  const m = /^\/api\/live\/([a-z0-9][a-z0-9-]*)\/ws(?:\?.*)?$/.exec(req.url ?? '');
8
9
  if (!m || !isUpgrade(req))
9
10
  return false;
@@ -20,10 +21,13 @@ export function makeLiveUpgrade(deps = {}) {
20
21
  onClose: () => call?.onSocketClose(),
21
22
  });
22
23
  const listen = new URL(req.url ?? '', 'http://x').searchParams.get('listen') === 'open' ? 'open' : 'hold';
23
- call = new LiveCall(sock, { ctx, tutor, deviceId: cookieOf(req.headers.cookie, DEVICE_COOKIE), listen, endpoint: deps.endpoint, env: deps.env, heartbeatMs: deps.heartbeatMs, keepAliveMs: deps.keepAliveMs, swapTokens: deps.swapTokens, idleSwapMs: deps.idleSwapMs, echoWaitMs: deps.echoWaitMs, ringMs: deps.ringMs, stallMs: deps.stallMs, onFinished: (why) => deps.onFinished?.(tutor, why) });
24
+ call = new LiveCall(sock, { ctx, tutor, deviceId: cookieOf(req.headers.cookie, DEVICE_COOKIE), listen, endpoint: deps.endpoint, env: deps.env, heartbeatMs: deps.heartbeatMs, keepAliveMs: deps.keepAliveMs, swapTokens: deps.swapTokens, idleSwapMs: deps.idleSwapMs, echoWaitMs: deps.echoWaitMs, ringMs: deps.ringMs, stallMs: deps.stallMs, idleCutMs: deps.idleCutMs, maxTakeMs: deps.maxTakeMs, onFinished: (why) => { if (call)
25
+ open.delete(call); deps.onFinished?.(tutor, why); } });
26
+ open.add(call);
24
27
  void call.start();
25
28
  return true;
26
29
  };
30
+ return Object.assign(liveUpgrade, { openCalls: () => open.size });
27
31
  }
28
32
  /** 探针:COTUTOR_LIVE_ENDPOINT 指到假百炼 */
29
- export const liveUpgrade = makeLiveUpgrade(process.env.COTUTOR_LIVE_ENDPOINT ? { endpoint: process.env.COTUTOR_LIVE_ENDPOINT, env: { ...process.env, DASHSCOPE_API_KEY: process.env.DASHSCOPE_API_KEY ?? 'sk-probe' } } : {});
33
+ export const liveUpgrade = makeLiveUpgrade(process.env.COTUTOR_LIVE_ENDPOINT ? { endpoint: process.env.COTUTOR_LIVE_ENDPOINT } : {});
@@ -78,8 +78,6 @@ export declare function playsLecture(line: {
78
78
  cues?: readonly BoardCue[];
79
79
  }, cards: readonly BoardCard[]): boolean;
80
80
  export declare function parseBoard(text: string, opts?: ParseBoardOptions): ParsedBoard;
81
- /** 回复停在写成正文的工具调用上(后面只剩空行):老师在等工具的结果,这一轮其实没写完(runner 据此 resume 一次) */
82
- export declare function endsWithToolCall(text: string): boolean;
83
81
  /** 原文一行的角色:解析器把它当什么了 */
84
82
  export type SourceRole =
85
83
  /** 讲稿一句(念出来) */
@@ -247,12 +247,6 @@ export function parseBoard(text, opts = {}) {
247
247
  },
248
248
  };
249
249
  }
250
- /** 回复停在写成正文的工具调用上(后面只剩空行):老师在等工具的结果,这一轮其实没写完(runner 据此 resume 一次) */
251
- export function endsWithToolCall(text) {
252
- const { spans } = parseBoard(text);
253
- const last = spans.toolCalls[spans.toolCalls.length - 1];
254
- return Boolean(last) && text.split('\n').slice(last[1] + 1).every((l) => !l.trim());
255
- }
256
250
  const SECTION_HEAD = /^##\s+(.+?)\s*$/;
257
251
  /**
258
252
  * 家长端「看原文」用:把老师原文的每一行标上解析器怎么读的。
@@ -152,7 +152,7 @@ export interface BoardMessage {
152
152
  reply: string | null;
153
153
  pending: boolean;
154
154
  section?: BoardSection | null;
155
- /** 孩子这条带的作业照片(相对 workspace 根;R5):节头上回显缩略图 */
155
+ /** 孩子这条带的作业照片(相对 workspace 根):答拍给我看卡的进那张卡;别的由老师用 image 卡放上板 */
156
156
  photos?: string[];
157
157
  /** 看完小课堂后的第一条:这一节前面是小课堂卡 */
158
158
  lecture?: KidLecture;
@@ -163,14 +163,14 @@ export interface BoardMessage {
163
163
  /** 按住说话的(索引里带 voice):板上去口头禅、一行截断 */
164
164
  /** 首页按钮发的:按钮上的字,不是孩子说的 */
165
165
  fromHome?: boolean;
166
- /** 实时的一轮(讲稿嘴照念,不配音) */
166
+ /** 实时的一轮(讲稿嘴照念):板上不看它,一律照实时的画(2026-11-04);只给字幕重放、录像用 */
167
167
  live?: boolean;
168
168
  }
169
169
  /** 板上孩子的一句话:text 是显示的(语音去了口头禅),full 是原话 */
170
170
  export interface KidSaid {
171
171
  text: string;
172
172
  full: string;
173
- /** 答拍给我看卡时拍来的照片:进那张卡的答案栏,节头不再回显 */
173
+ /** 答拍给我看卡时拍来的照片:放大放在那张卡里(2026-11-04 之前缩在卡下沿) */
174
174
  photos?: string[];
175
175
  }
176
176
  export interface BoardEntry extends BoardSection {
@@ -178,27 +178,23 @@ export interface BoardEntry extends BoardSection {
178
178
  at?: string;
179
179
  /** 老师还在说:卡只增不改;前 ready 拍就绪了就能播(2026-09-13 之前是整轮跑完才播);整轮跑完换成正式的一节 */
180
180
  partial?: boolean;
181
- /** 孩子问这节时拍的照片(节头上回显) */
182
- photos?: string[];
183
181
  /** 这一节答的是看完小课堂后的第一问:节前画小课堂卡 */
184
182
  lecture?: KidLecture;
185
183
  /** 这一节答的是孩子从纸上交的画:节前一张孩子的画卡 */
186
184
  drawing?: KidDrawing;
187
- /** 孩子引出这一节的那句话;saidOn = 它答的是哪一节的哪张问题卡(写进那张卡的答案栏),没有 = 在这一节开头单独一行。实时的节头不是它(看 head),它只给左栏用 */
185
+ /** 孩子引出这一节的那句话;saidOn = 它答的是哪一节的哪张问题卡。节头不是它(看 head):它给左栏用,答拍给我看卡的还进那张卡 */
188
186
  said?: KidSaid;
189
187
  saidOn?: {
190
188
  job: string;
191
189
  card: number;
192
190
  };
193
- /** 实时的一轮:卡闭合了就铺(不按拍);不出卡的板上留讲稿的字 */
194
- live?: true;
195
- /** 实时:这一轮开头是脑写的节头(只有一行 # 标题的文字卡),从这里起新的一件事,head 是它的标题(liveHead) */
191
+ /** 这一轮开头是脑写的节头(只有一行 # 标题的文字卡),从这里起新的一件事,head 是它的标题(liveHead) */
196
192
  head?: string;
197
- /** 实时:这一轮开头没写节头,接着上一节画 */
193
+ /** 这一轮开头没写节头,接着上一节画 */
198
194
  cont?: true;
199
195
  }
200
196
  /**
201
- * 实时的一节怎么接:新一节的开头由脑定,不再机械是孩子那句。
197
+ * 一节怎么接(板上只有实时一种画法,2026-11-04;以前的话题也这样画):新一节的开头由脑定,不再机械是孩子那句。
202
198
  * 这一轮第一张卡是节头 → head;没写(只有讲稿、开头是别的卡、还没写出卡)→ cont,接着上一节。一轮中间的节头卡,是从那张卡起新一节
203
199
  */
204
200
  export declare function liveHead(s: Pick<BoardSection, 'cards'>): {
@@ -225,7 +221,7 @@ export declare function kidSaid(messages: readonly BoardMessage[]): Map<string,
225
221
  }>;
226
222
  /**
227
223
  * 孩子端条目 → 板书节(没有 section 的条目不出节);
228
- * 孩子的话挂在它引出的那一节上(said / saidOn,kidSaid);还在跑的只有带 partial 板书(流式,已有卡或已有配好的句;实时的有句就算)才出节且标 partial,其余不出。
224
+ * 孩子的话挂在它引出的那一节上(said / saidOn,kidSaid);还在跑的只有带 partial 板书(流式,已有卡或已有句)才出节且标 partial,其余不出。
229
225
  */
230
226
  export declare function sectionsFromMessages(messages: readonly BoardMessage[]): BoardEntry[];
231
227
  /** 一节在目录里的名字:小节标题 > 有名字的文字卡 > 第一张有字的卡 > 第一句讲稿;截到 14 个字 */
@@ -269,13 +265,11 @@ export type PlayerEvent = {
269
265
  type: 'fresh';
270
266
  sections: number[];
271
267
  };
272
- /** stop 只停声音(卡的点读、单词);paint = 把某节的标注画齐;showNow = 选中态照现在的位置;scrollLast = 滚到最后一节 */
268
+ /** stop 只停声音(卡的点读、单词);paint = 把某节的标注画齐;scrollLast = 滚到最后一节。板上不描「念到哪张」(卡和讲稿不同步,2026-11-04 起以前的话题也这样) */
273
269
  export type PlayerEffect = {
274
270
  kind: 'stop';
275
271
  } | {
276
272
  kind: 'render';
277
- } | {
278
- kind: 'showNow';
279
273
  } | {
280
274
  kind: 'paint';
281
275
  section: number;
@@ -433,7 +433,7 @@ export function isQuestion(text) {
433
433
  return t.endsWith('?') || t.endsWith('?');
434
434
  }
435
435
  /**
436
- * 实时的一节怎么接:新一节的开头由脑定,不再机械是孩子那句。
436
+ * 一节怎么接(板上只有实时一种画法,2026-11-04;以前的话题也这样画):新一节的开头由脑定,不再机械是孩子那句。
437
437
  * 这一轮第一张卡是节头 → head;没写(只有讲稿、开头是别的卡、还没写出卡)→ cont,接着上一节。一轮中间的节头卡,是从那张卡起新一节
438
438
  */
439
439
  export function liveHead(s) {
@@ -488,16 +488,16 @@ export function kidSaid(messages) {
488
488
  }
489
489
  /**
490
490
  * 孩子端条目 → 板书节(没有 section 的条目不出节);
491
- * 孩子的话挂在它引出的那一节上(said / saidOn,kidSaid);还在跑的只有带 partial 板书(流式,已有卡或已有配好的句;实时的有句就算)才出节且标 partial,其余不出。
491
+ * 孩子的话挂在它引出的那一节上(said / saidOn,kidSaid);还在跑的只有带 partial 板书(流式,已有卡或已有句)才出节且标 partial,其余不出。
492
492
  */
493
493
  export function sectionsFromMessages(messages) {
494
494
  const out = [];
495
495
  const says = kidSaid(messages);
496
496
  for (const m of messages) {
497
497
  const k = says.get(m.job);
498
- const at = { ...(m.at ? { at: m.at } : {}), ...(m.photos?.length && !k?.said.photos ? { photos: m.photos } : {}), ...(m.lecture ? { lecture: m.lecture } : {}), ...(m.drawing ? { drawing: m.drawing } : {}), ...(k ? { said: k.said, ...(k.on ? { saidOn: k.on } : {}) } : {}), ...(m.live ? { live: true, ...liveHead(m.section ?? { cards: [] }) } : {}) };
498
+ const at = { ...(m.at ? { at: m.at } : {}), ...(m.lecture ? { lecture: m.lecture } : {}), ...(m.drawing ? { drawing: m.drawing } : {}), ...(k ? { said: k.said, ...(k.on ? { saidOn: k.on } : {}) } : {}), ...liveHead(m.section ?? { cards: [] }) };
499
499
  if (m.pending) {
500
- if (m.section && m.section.partial && (m.section.cards.length || (m.live && m.section.lines.length)))
500
+ if (m.section && m.section.partial && (m.section.cards.length || m.section.lines.length))
501
501
  out.push({ job: m.job, ...at, cards: m.section.cards, lines: m.section.lines, partial: true, ready: m.section.ready ?? 0, ...(m.section.layout ? { layout: m.section.layout } : {}) });
502
502
  continue;
503
503
  }
@@ -558,7 +558,7 @@ export function step(model, ev, ctx) {
558
558
  case 'liveStart':
559
559
  fx.push({ kind: 'paint', section: ev.section });
560
560
  m = { state: { section: ev.section, line: -1, status: 'thinking' } };
561
- fx.push({ kind: 'render' }, { kind: 'showNow' });
561
+ fx.push({ kind: 'render' });
562
562
  break;
563
563
  case 'liveBeat':
564
564
  if (m.state.section !== ev.section || m.state.status !== 'thinking')
@@ -569,12 +569,10 @@ export function step(model, ev, ctx) {
569
569
  fx.push({ kind: 'paint', section: ev.section });
570
570
  if (m.state.section !== ev.section)
571
571
  break;
572
- if (m.state.status !== 'thinking') {
573
- fx.push({ kind: 'showNow' });
572
+ if (m.state.status !== 'thinking')
574
573
  break;
575
- }
576
574
  m = { state: playerAtEnd(secs) };
577
- fx.push({ kind: 'render' }, { kind: 'showNow' });
575
+ fx.push({ kind: 'render' });
578
576
  break;
579
577
  case 'liveDropped':
580
578
  fx.push({ kind: 'stop' });
@@ -587,7 +585,7 @@ export function step(model, ev, ctx) {
587
585
  for (const i of ev.sections)
588
586
  fx.push({ kind: 'paint', section: i });
589
587
  m = { state: playerAtEnd(secs) };
590
- fx.push({ kind: 'render' }, { kind: 'showNow' }, { kind: 'scrollLast' });
588
+ fx.push({ kind: 'render' }, { kind: 'scrollLast' });
591
589
  break;
592
590
  }
593
591
  return { model: m, effects: fx };
@@ -137,8 +137,9 @@ export function readOnlyTools(roots, opts = {}) {
137
137
  const relOf = async (p) => {
138
138
  for (const root of await realRoots()) {
139
139
  const rel = relative(root, p);
140
+ // 模型给的通配按 / 写(如 随便/*期中*.md),Windows 上 relative 给的是反斜杠
140
141
  if (!outside(rel))
141
- return rel;
142
+ return rel.split(sep).join('/');
142
143
  }
143
144
  return p;
144
145
  };
@@ -1,57 +1,52 @@
1
1
  import { type ModelMessage } from 'ai';
2
- import type { CliEvent } from './cli/types.ts';
3
- import { type RunPlan } from './cli/run-plan.ts';
2
+ import { type RunPlan } from './runtime.ts';
4
3
  import { type ApiRuntime, type CotutorConfig, type Policy } from '../schema/index.ts';
5
- import type { BrainHooks, BrainRequest, BrainRun, WarmKey } from './index.ts';
4
+ import type { BrainHooks, BrainRequest, BrainRun } from './index.ts';
6
5
  /** 计划里记的 cli 名:API 的轮次按它分流 */
7
6
  export declare const API_CLI = "api";
8
7
  /**
9
8
  * 出厂的 API 运行时(2026-10-31 起新装缺省 api,Qwen Code 那两条留作备用):模型同那两条——api 对 qwen(qwen3.8-max),api-flash 对 qwen-flash(qwen3.8-flash)
10
9
  */
11
10
  export declare const FACTORY_API_RUNTIMES: Record<string, ApiRuntime>;
12
- /** .log 的一行 → 统一事件(API 轮次的日志就是一行一条统一事件);不是这种行回 [] */
13
- export declare function parseEventLine(line: string): CliEvent[];
14
11
  /** 政策 effort → 思考开关与思考量(同 qwen:low 不想,孩子在等;带工具的轮次至少想一点,不然照片题连资料都不查) */
15
12
  export declare function thinkingOf(effort: Policy['effort'] | undefined, tools: BrainRequest['tools']): {
16
13
  enableThinking: boolean;
17
14
  thinkingBudget?: number;
18
15
  };
19
16
  /**
20
- * 有思考档的模型照 qwen 运行时发 reasoning_effort(src/brain/cli/qwen.ts 的 EFFORT):想一点 = low、不限 = xhigh;不想或没有档 → null,照 thinkingOf 发。
17
+ * 有思考档的模型发 reasoning_effort(照当时 Qwen Code 的档):想一点 = low、不限 = xhigh;不想或没有档 → null,照 thinkingOf 发。
21
18
  * low 是它自己看着想:闲聊想一两百 token,认照片题想得多;thinking_budget 是死卡,1024 时有照片的话题每轮都想满(2026-10-31)
22
19
  */
23
20
  export declare function effortOf(model: string, effort: Policy['effort'] | undefined, tools: BrainRequest['tools']): 'low' | 'xhigh' | null;
24
21
  /** AI SDK 的 alibaba 只认 thinking_budget:在 fetch 这一层把请求体里的换成 reasoning_effort(SDK 给的中间件口子) */
25
22
  export declare function effortFetch(effort: 'low' | 'xhigh', base?: typeof fetch): typeof fetch;
26
- /**
27
- * 百炼 Chat Completions 的端点:知道业务空间 id 就走它的域名(原型验过,同 realtime);只有按量 key 就走公网域名(qwen 那边按量的线也走它,2026-10-29 真跑通)
28
- */
29
- export declare function bailianBase(workspaceId: string | null): string;
30
23
  /**
31
24
  * 已经结束的一轮,拼回历史时的老师那条:正文照原样,思考不带,工具往返换成一句「(用了 …,回了 N 字;结果这里不再带,要看再查)」。
32
25
  * 只剩一条纯文字的回复,历史里永远没有配不上对的工具消息
33
26
  */
34
27
  export declare function slimTurn(messages: readonly ModelMessage[]): string;
28
+ /** 这个模型发到哪、带哪把 key(src/shared/qianwen.ts 的 compatibleLine);null = 没配 */
29
+ export type BrainLine = (model: string) => {
30
+ baseURL: string;
31
+ key: string;
32
+ plan: boolean;
33
+ } | null;
35
34
  export declare class ApiBrain {
36
35
  private readonly workspace;
37
- private readonly auth;
38
- /** 测试与探针:…/compatible-mode/v1(假百炼);不给按业务空间 id 拼 */
36
+ private readonly line;
37
+ /** 测试与探针:…/compatible-mode/v1(假百炼);不给按模型选线 */
39
38
  private readonly endpoint;
40
39
  constructor(opts: {
41
40
  workspace: () => {
42
41
  config: CotutorConfig;
43
42
  root: string;
44
43
  };
45
- auth: () => {
46
- key: string;
47
- workspaceId: string | null;
48
- } | null;
44
+ line: BrainLine;
49
45
  endpoint?: string;
50
46
  });
51
47
  plan(req: BrainRequest): RunPlan;
52
48
  start(req: BrainRequest, plan: RunPlan, ctx: {
53
49
  tutor: string;
54
- key: WarmKey;
55
50
  files: {
56
51
  log: string;
57
52
  err: string;