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
@@ -1,91 +0,0 @@
1
- /**
2
- * 适配器注册表:运行时写了 `cli` 就按它找,没写按 run[0] 的文件名。
3
- * 都对不上(测试的假 CLI 是 `node …_fake-cli.ts`、自家套的壳脚本没写 cli)就按通用的 stream-json 读,
4
- * 只复用流协议与 Qwen 的工具参数,不设置 Qwen 的运行环境。
5
- */
6
- import { basename } from 'node:path';
7
- import { POLICY_DEFAULTS, fillRuntime } from "../../schema/index.js";
8
- import { parseStreamJson, streamJsonUserMessage } from "./stream-json.js";
9
- import { qwen } from "./qwen.js";
10
- export { textsOf, toolsOf } from "./types.js";
11
- /** 登记在这里的才认;接一个新 CLI 加一行 */
12
- export const ADAPTERS = [qwen];
13
- /** 自定义壳脚本与测试进程的 stream-json 协议,不准备 Qwen 的家目录与认证。 */
14
- export const STREAM_JSON = {
15
- name: 'stream-json',
16
- parse: parseStreamJson,
17
- stdinMessage: streamJsonUserMessage,
18
- toolList: qwen.toolList,
19
- toolArgs: qwen.toolArgs,
20
- toolKind: qwen.toolKind,
21
- env: (env) => env,
22
- runtimes: () => ({}),
23
- };
24
- /** 历史对话里的工具名仍要能展示读过的文件,不用于启动旧 CLI。 */
25
- const LEGACY_TOOL_KINDS = { Read: 'read', Bash: 'shell', Grep: 'search', Glob: 'search', Edit: 'edit', Write: 'edit', NotebookEdit: 'edit', MultiEdit: 'edit', Skill: 'skill' };
26
- /** 运行时的 CLI 名:cli 字段,或 run[0] 的文件名 */
27
- export function runtimeCli(runtime) {
28
- return runtime.cli ?? basename(runtime.run[0] ?? '');
29
- }
30
- export function adapterNamed(name) {
31
- return ADAPTERS.find((a) => a.name === name) ?? null;
32
- }
33
- /** 旧配置仍可读取,但退役的运行器不能通过通用协议兜底启动。 */
34
- export function runtimeIssue(runtime) {
35
- const retired = [runtime.cli, basename(runtime.run[0] ?? ''), basename(runtime.resume?.[0] ?? '')].some((name) => name && /^claude(?:\.exe|\.cmd)?$/i.test(name));
36
- return retired ? 'Claude CLI 已移除;先 cotutor upgrade --config 补齐出厂运行时,把 runtimes.default 和 tutors.*.runtime 改为 api 或 api-flash(应用直连百炼;用 Qwen Code 就改为 qwen 或 qwen-flash),再删除旧运行时条目' : null;
37
- }
38
- export function adapterFor(runtime) {
39
- const issue = runtimeIssue(runtime);
40
- if (issue)
41
- throw new Error(issue);
42
- return adapterNamed(runtimeCli(runtime)) ?? STREAM_JSON;
43
- }
44
- /** 工具名属于哪一类:问遍登记的适配器(各家工具名不重),都不认就是 other。老数据只存了名字,也能认 */
45
- export function toolKind(name) {
46
- for (const a of ADAPTERS) {
47
- const k = a.toolKind(name);
48
- if (k !== 'other')
49
- return k;
50
- }
51
- return LEGACY_TOOL_KINDS[name] ?? 'other';
52
- }
53
- /** 模板管得了这轮带不带工具吗:用了 `{tools}` 或 `{toolArgs}`。管不了的,应用也就不替它省掉上下文包里只有工具才用得上的路径 */
54
- export function controlsTools(runtime) {
55
- return [...runtime.run, ...runtime.resume].some((a) => a.includes('{tools}') || a === '{toolArgs}');
56
- }
57
- /** 出厂运行时:各适配器的模板合在一起(default 由 skeleton 定) */
58
- export function factoryRuntimes() {
59
- return Object.assign({}, ...ADAPTERS.map((a) => a.runtimes()));
60
- }
61
- /**
62
- * 填模板:独占一个参数的 `{toolArgs}` 换成适配器给的几个参数(可以是零个),`{tools}` 换成适配器的工具名单,其余占位照 schema 的 fillRuntime。
63
- */
64
- export function fillArgs(adapter, argv, vars) {
65
- const set = vars.tools ?? 'on';
66
- const spread = argv.flatMap((a) => (a === '{toolArgs}' ? adapter.toolArgs(set, { readDirs: vars.readDirs ?? [] }) : [a]));
67
- return fillRuntime(spread, { ...vars, effort: vars.effort ?? POLICY_DEFAULTS.effort, tools: adapter.toolList(set) });
68
- }
69
- /** 起老师进程的环境:基础环境 + 这轮计划里适配器定的变量,再交给适配器补齐认证环境 */
70
- export function processEnv(plan, base) {
71
- const adapter = adapterNamed(plan.cli) ?? STREAM_JSON;
72
- return adapter.env({ ...base, ...plan.env });
73
- }
74
- /** 按计划里记的适配器名读输出(老数据没有就按 stream-json) */
75
- export function parserOf(cli) {
76
- return (cli ? adapterNamed(cli) : null)?.parse ?? STREAM_JSON.parse;
77
- }
78
- /** 按运行时名读输出(落盘的老轮次只记了运行时名;运行时后来删了或改了就按 stream-json)。API 运行时的日志不归这里读,见 src/brain/index.ts 的 logParser */
79
- export function parserForRuntime(runtimes, name) {
80
- const r = name && name !== 'default' ? runtimes[name] : undefined;
81
- return r && typeof r === 'object' && Array.isArray(r.run) ? (adapterNamed(runtimeCli(r)) ?? STREAM_JSON).parse : STREAM_JSON.parse;
82
- }
83
- /** 起进程前让这个 CLI 的适配器备好 workspace 里的机器文件(qwen 的隔离家目录);没有这一步的 CLI 什么都不做 */
84
- export async function prepareCli(cli, root) {
85
- await adapterNamed(cli)?.prepare?.(root);
86
- }
87
- /** 进程起来报的工具表里,不该有的那几样(适配器不核对就是空) */
88
- export function unexpectedTools(cli, set, tools) {
89
- const want = adapterNamed(cli)?.expectedTools?.(set);
90
- return want ? tools.filter((t) => !want.includes(t)) : [];
91
- }
@@ -1,11 +0,0 @@
1
- import type { CliAdapter } from './types.ts';
2
- /** workspace 里归 qwen 的目录(机器文件,prepare 每次对一遍) */
3
- export declare const QWEN_DIR = ".cotutor/qwen";
4
- /** 出厂模型(2026-10-09 五个模型 7 道真题对比:不想时 2–8 秒,讲得准也细;3.7-plus 快但浅,3.8-flash 不想时把盘算写进讲稿——
5
- * 2026-10-13 英语老师三问没再见到,出厂 qwen-flash 运行时给英语老师) */
6
- export declare const QWEN_DEFAULT_MODEL = "qwen3.8-max";
7
- /** 隔离的家里那份 settings.json(没有 key,只写环境变量名) */
8
- export declare function qwenHomeSettings(): Record<string, unknown>;
9
- /** 这几份机器文件:绝对路径 → 内容 */
10
- export declare function qwenFiles(root: string): Record<string, string>;
11
- export declare const qwen: CliAdapter;
@@ -1,141 +0,0 @@
1
- /**
2
- * Qwen Code(`qwen`,0.25 起)的适配器。输出读法在 stream-json.ts,运行约定:
3
- * - **隔离的家**:不用 `--bare`(它不读任何配置、不认 `--core-tools`、自带 read_file / edit / run_shell_command 一套)。
4
- * `QWEN_HOME` 指 workspace 的 `.cotutor/qwen/home/`(不改 HOME),里面的 settings.json 由这里生成:模型供应商(key 只写环境变量名)、
5
- * 关掉它自己的记忆整理(不然一轮跑完在后台用 write_file 写文件)、使用统计、遥测、防休眠。会话落在 `.cotutor/qwen/runtime/`。
6
- * - **工具**:`{toolArgs}` 展开成 `--core-tools` + `--exclude-tools`;不带 = 工具表为空,只读 = read_file / glob / grep_search。
7
- * 模板带 `--approval-mode default`:shell、写、读 workspace 外都被拒(`--yolo` 会放行 workspace 外的读)。
8
- * - **思考量**:没有旗标;按 effort 指一份系统配置(`QWEN_CODE_SYSTEM_SETTINGS_PATH`,`model.reasoningEffort`)。
9
- * - **key 与端点**:settings.json 只写 `${COTUTOR_QWEN_BASE_URL}` 与环境变量名 `COTUTOR_QWEN_API_KEY`(qwen 读配置时展开);起进程时按 `src/shared/qianwen.ts`
10
- * 定走 Token Plan 还是百炼按量,把这两个填进子进程的环境,不落盘。出厂的几个模型 Token Plan 都有,有它的 key 就走它。
11
- * - 没有钱数,只报 token;超时(退出码 55)不吐 result;每轮一条 goal_state 事件(读法里不出事件)。
12
- */
13
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
14
- import { dirname, join } from 'node:path';
15
- import { compatibleBase, qianwenKeys, qianwenLine } from "../../shared/qianwen.js";
16
- import { parseStreamJson, streamJsonUserMessage } from "./stream-json.js";
17
- const KINDS = {
18
- read_file: 'read',
19
- read_many_files: 'read',
20
- zoom_image: 'read',
21
- list_directory: 'read',
22
- run_shell_command: 'shell',
23
- glob: 'search',
24
- grep_search: 'search',
25
- edit: 'edit',
26
- write_file: 'edit',
27
- notebook_edit: 'edit',
28
- skill: 'skill',
29
- };
30
- /** 只读那轮给的核心工具(读文件含图片、按名找、按内容找) */
31
- const READ_TOOLS = ['read_file', 'glob', 'grep_search'];
32
- /** `--core-tools` 管不到的非核心工具,要一个个排除(0.25 的 system/init 里出现过的;ask_user_question 与计划模式那两个是不加 --bare 时才有的,2026-10-09 真跑里核对出来) */
33
- const NON_CORE = ['agent', 'skill', 'list_agents', 'task_stop', 'send_message', 'record_artifact', 'get_goal', 'update_goal', 'tool_call', 'tool_search', 'report_findings', 'enter_worktree', 'exit_worktree', 'ask_user_question', 'enter_plan_mode', 'exit_plan_mode'];
34
- /** workspace 里归 qwen 的目录(机器文件,prepare 每次对一遍) */
35
- export const QWEN_DIR = '.cotutor/qwen';
36
- const homeOf = (root) => join(root, QWEN_DIR, 'home');
37
- const effortFile = (root, level) => join(root, QWEN_DIR, `effort-${level}.json`);
38
- /** 政策 effort → 模型的思考档:low 不想(孩子在等);qwen3.7 系列只有开关,none 以外都是开 */
39
- const EFFORT = { low: 'none', medium: 'low', high: 'xhigh' };
40
- /**
41
- * 带工具的轮次(作业照片、记账)至少想一点:2026-10-09 真跑,同一道照片题 none 那次 read_file 都没调、请孩子说是哪页;
42
- * low 那次读了图、认出题目(11 秒)。孩子随口的话不带工具,不想照旧(3–6 秒)
43
- */
44
- const levelOf = (effort, tools) => (tools !== 'off' && EFFORT[effort] === 'none' ? 'low' : EFFORT[effort]);
45
- /** settings.json 里写的占位;qwen 读配置时拿子进程环境展开 */
46
- const BASE_ENV = 'COTUTOR_QWEN_BASE_URL';
47
- const KEY_ENV = 'COTUTOR_QWEN_API_KEY';
48
- const toggle = { thinking: true, toggleOnly: true, disableField: 'enable_thinking' };
49
- const tiers = { thinking: true, efforts: ['low', 'medium', 'xhigh'], defaultEffort: 'xhigh', disableField: 'reasoning_effort' };
50
- /** 出厂模型(2026-10-09 五个模型 7 道真题对比:不想时 2–8 秒,讲得准也细;3.7-plus 快但浅,3.8-flash 不想时把盘算写进讲稿——
51
- * 2026-10-13 英语老师三问没再见到,出厂 qwen-flash 运行时给英语老师) */
52
- export const QWEN_DEFAULT_MODEL = 'qwen3.8-max';
53
- /** 千问上能用的几个(Token Plan 与百炼按量都有);运行时模板的 -m 选其中一个 */
54
- const MODELS = [
55
- { id: 'qwen3.7-plus', capabilities: { reasoning: toggle }, generationConfig: { contextWindowSize: 1_000_000, modalities: { image: true } } },
56
- { id: 'qwen3.7-max', capabilities: { reasoning: toggle }, generationConfig: { contextWindowSize: 1_000_000 } },
57
- { id: 'qwen3.8-flash', capabilities: { reasoning: tiers }, generationConfig: { contextWindowSize: 1_000_000, modalities: { image: true, video: true } } },
58
- { id: 'qwen3.8-max', capabilities: { reasoning: tiers }, generationConfig: { contextWindowSize: 1_000_000, modalities: { image: true, video: true } } },
59
- ];
60
- /** 隔离的家里那份 settings.json(没有 key,只写环境变量名) */
61
- export function qwenHomeSettings() {
62
- return {
63
- $version: 4,
64
- security: { auth: { selectedType: 'openai' } },
65
- model: { name: QWEN_DEFAULT_MODEL },
66
- modelProviders: { openai: MODELS.map((m) => ({ ...m, baseUrl: `\${${BASE_ENV}}`, envKey: KEY_ENV })) },
67
- memory: { enableManagedAutoMemory: false, enableManagedAutoDream: false },
68
- privacy: { usageStatisticsEnabled: false },
69
- telemetry: { enabled: false },
70
- general: { disableAutoUpdate: true, disableUpdateNag: true, preventSystemSleep: false },
71
- };
72
- }
73
- /** 这几份机器文件:绝对路径 → 内容 */
74
- export function qwenFiles(root) {
75
- const files = {
76
- // 整个目录不进 git:会话记录(runtime/)是对话原文,同 conversations/ 只留本机;其余都是这里每次重新生成的
77
- [join(root, QWEN_DIR, '.gitignore')]: '# qwen 的隔离家目录与会话记录(cotutor 生成,对话原文只留本机)\n*\n',
78
- [join(homeOf(root), 'settings.json')]: `${JSON.stringify(qwenHomeSettings(), null, 2)}\n`,
79
- };
80
- for (const level of new Set(Object.values(EFFORT)))
81
- files[effortFile(root, level)] = `${JSON.stringify({ $version: 4, model: { reasoningEffort: level } }, null, 2)}\n`;
82
- return files;
83
- }
84
- /** 别处的设置会盖掉家里选的模型与供应商,起进程时去掉 */
85
- const FOREIGN = ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', 'QWEN_MODEL'];
86
- export const qwen = {
87
- name: 'qwen',
88
- parse: parseStreamJson,
89
- stdinMessage: streamJsonUserMessage,
90
- // qwen 的带工具只有只读一档(read / on 一样):shell 与写本来就不给
91
- toolList: (set) => (set !== 'off' ? READ_TOOLS.join(',') : ''),
92
- // --core-tools 是白名单,不带时也要给一个(给空等于不限),再把它排除掉;非核心工具不归 --core-tools 管,总要排除。
93
- // 只读那轮把 workspace 根与 vault 加进可读范围:会话 cwd 是老师目录,作业照片(captures/)与教材在它外面,读外面要征得同意——
94
- // 消息走 stdin 时它就停在那儿等,一直等到 --max-wall-time(2026-10-09 真跑,开了思考的那次读照片挂住)
95
- toolArgs: (set, { readDirs }) => set !== 'off'
96
- ? ['--core-tools', READ_TOOLS.join(','), '--exclude-tools', NON_CORE.join(','), ...readDirs.flatMap((d) => ['--include-directories', d])]
97
- : ['--core-tools', 'read_file', '--exclude-tools', ['read_file', ...NON_CORE].join(',')],
98
- toolKind: (name) => KINDS[name] ?? 'other',
99
- expectedTools: (set) => (set !== 'off' ? READ_TOOLS : []),
100
- planEnv: ({ root, effort, tools }) => root ? { QWEN_HOME: homeOf(root), QWEN_RUNTIME_DIR: join(root, QWEN_DIR, 'runtime'), QWEN_CODE_SYSTEM_SETTINGS_PATH: effortFile(root, levelOf(effort, tools)) } : {},
101
- async prepare(root) {
102
- for (const [file, text] of Object.entries(qwenFiles(root))) {
103
- if ((await readFile(file, 'utf8').catch(() => null)) === text)
104
- continue;
105
- await mkdir(dirname(file), { recursive: true });
106
- await writeFile(file, text);
107
- }
108
- },
109
- env: (env) => {
110
- const out = { ...env, QWEN_USAGE_STATISTICS_ENABLED: 'false', QWEN_CODE_MODELS_DEV: 'off', QWEN_CODE_SUPPRESS_YOLO_WARNING: '1' };
111
- for (const k of FOREIGN)
112
- delete out[k];
113
- const line = qianwenLine(QWEN_DEFAULT_MODEL, qianwenKeys(out));
114
- if (line) {
115
- out[BASE_ENV] = compatibleBase(line.host);
116
- out[KEY_ENV] = line.key;
117
- }
118
- return out;
119
- },
120
- check: ({ version, env }) => {
121
- const v = /(\d+)\.(\d+)\.(\d+)/.exec(version);
122
- const old = !v || Number(v[1]) === 0 && Number(v[2]) < 25;
123
- const line = qianwenLine(QWEN_DEFAULT_MODEL, qianwenKeys(env));
124
- return [
125
- { name: 'version', ok: !old, detail: old ? `qwen ${version || '?'}:要 0.25 起(关工具的旗标、隔离的家、思考量配置都按 0.25 定的)` : `qwen ${version}`, fix: old ? 'qwen update(或 brew upgrade qwen-code)' : undefined },
126
- line
127
- ? { name: 'key', ok: true, detail: line.plan ? '走千问 Token Plan(QWENCLOUD_TOKEN_PLAN_API_KEY)' : '走百炼按量(DASHSCOPE_API_KEY,或本机 qwen / voxtell 的配置)' }
128
- : { name: 'key', ok: false, detail: '千问的 key 一把都拿不到:老师进程起来就会 401', fix: '在起 serve 的 shell 里 export QWENCLOUD_TOKEN_PLAN_API_KEY(Token Plan,sk-sp-)或 DASHSCOPE_API_KEY(百炼按量)' },
129
- ];
130
- },
131
- runtimes: () => {
132
- // 消息走 stdin(预热能用;数组旗标 --core-tools 之类会吞掉跟在后面的消息,不进 argv 正好避开);-m 选模型(家里那份 settings.json 列着的);
133
- // {systemBody} = 老师正文 + 板书写法,整份换掉 qwen 自己的系统提示;{toolArgs} 由适配器展开;--approval-mode default:shell、写、读 workspace 外都拒
134
- // qwen-flash 只差模型(2026-10-13 英语老师出厂走它:Token Plan 上便宜、开口 2.5–4 秒)
135
- const of = (model) => {
136
- const flags = ['-m', model, '--system-prompt', '{systemBody}', '--input-format', 'stream-json', '--output-format', 'stream-json', '--include-partial-messages', '--approval-mode', 'default', '{toolArgs}', '--max-wall-time', '10m'];
137
- return { stdin: 'stream-json', run: ['qwen', ...flags], resume: ['qwen', '--resume', '{session}', ...flags] };
138
- };
139
- return { qwen: of(QWEN_DEFAULT_MODEL), 'qwen-flash': of('qwen3.8-flash') };
140
- },
141
- };
@@ -1,59 +0,0 @@
1
- import { streamJsonUserMessage } from './stream-json.ts';
2
- import type { ToolSet } from './types.ts';
3
- import { type ConversationIndex, type CotutorConfig, type Policy, type Runtime } from '../../schema/index.ts';
4
- export interface RunPlan {
5
- /** 运行时名(runtimes 里的键) */
6
- runtime: string;
7
- /** 适配器名(src/brain/cli/;认不出的是 stream-json) */
8
- cli: string;
9
- /** 这轮带不带工具(模板管不了工具的运行时也照记,只是没用上) */
10
- tools: ToolSet;
11
- /** 完整命令行,argv[0] 是可执行文件 */
12
- argv: string[];
13
- /** 这次是 resume 还是新开 */
14
- resume: boolean;
15
- /** resume 的会话 id */
16
- session: string | null;
17
- /** 运行时 stdin: "stream-json":要写进 stdin 的那一行(用户消息,带换行);消息不在 argv 里 */
18
- stdin?: string;
19
- /** 适配器按这轮的计划定的环境变量(qwen 的思考量配置之类);和 argv 一起进预热的比对 */
20
- env: Record<string, string>;
21
- }
22
- export declare class RuntimeError extends Error {
23
- }
24
- export declare function getRuntime(config: CotutorConfig, name?: string): {
25
- name: string;
26
- runtime: Runtime;
27
- };
28
- /** 模板里有没有用到某个占位符(决定要不要去读老师正文)。API 运行时没有模板:系统提示由应用给,算用了 {systemBody} */
29
- export declare function runtimeUses(runtime: Runtime, placeholder: string): boolean;
30
- /**
31
- * 这个运行时把板书写法预载进系统提示了吗:模板用了 {boardFile} / {systemBody},或者(2026-09-19 头一版)写死了 cotutor-board/SKILL.md 的路径。
32
- * 没有 → 应用在话题第一条注入 <cotutor-board>。
33
- */
34
- export declare function boardPreloaded(runtime: Runtime): boolean;
35
- export declare function planRun(config: CotutorConfig, index: Pick<ConversationIndex, 'session'>, vars: {
36
- agent: string;
37
- prompt: string;
38
- agentBody?: string;
39
- systemBody?: string;
40
- boardFile?: string;
41
- runtime?: string;
42
- effort?: Policy['effort'];
43
- tools?: ToolSet;
44
- root?: string;
45
- readDirs?: readonly string[];
46
- }): RunPlan;
47
- /** stream-json 输入的一条用户消息(Qwen 的 `--input-format stream-json` 一行一条;别的 CLI 看它的适配器) */
48
- export declare const stdinMessage: typeof streamJsonUserMessage;
49
- /**
50
- * 断流后接着跑的那条消息(policy.stall):resume 同一个会话发给老师。被杀的那段回复只流到了 stdout,没进会话,
51
- * 所以断在半截的正文(open)原样递回去,让它从断处接着写;应用把 open 拼在这次的收尾正文前面。
52
- * open 为空 = 这一段一个字没出来,已经做完的工具调用与整段都在会话里,接着把这一轮做完。
53
- */
54
- export declare function stallPrompt(open: string): string;
55
- /**
56
- * 老师把工具调用写成了正文、停在那里等结果(这轮没给它那个工具,qwen 照自己的格式写出来;板书解析器把那块剥掉了):
57
- * resume 同一个会话,告诉它不会有结果,接着直接回孩子。那块前面写的话已经进了会话,应用拼在这次的前面
58
- */
59
- export declare function toolLeakPrompt(): string;
@@ -1,71 +0,0 @@
1
- /**
2
- * 一次运行的命令规划(纯函数):按运行时选 run 还是 resume、填占位符、定 cwd 相对路径。
3
- * 规则:当天索引里已有会话**且**是同一运行时跑出来的 → resume;否则新开(跨天索引本来就是新的,零点后第一条自然不带 --resume;
4
- * 换了运行时也新开——不同运行时的会话不混用)。{agentBody} 只给模板里真用到它的运行时读老师正文。
5
- */
6
- import { adapterFor, fillArgs } from "./adapters.js";
7
- import { streamJsonUserMessage } from "./stream-json.js";
8
- import { POLICY_DEFAULTS, isApiRuntime } from "../../schema/index.js";
9
- export class RuntimeError extends Error {
10
- }
11
- export function getRuntime(config, name) {
12
- const n = name ?? config.runtimes.default;
13
- const p = config.runtimes[n];
14
- if (n === 'default' || !p || typeof p === 'string') {
15
- const known = Object.keys(config.runtimes).filter((k) => k !== 'default');
16
- throw new RuntimeError(`运行时 "${n}" 不存在;cotutor.json 的 runtimes 里有:${known.join('、')}`);
17
- }
18
- return { name: n, runtime: p };
19
- }
20
- /** 模板里有没有用到某个占位符(决定要不要去读老师正文)。API 运行时没有模板:系统提示由应用给,算用了 {systemBody} */
21
- export function runtimeUses(runtime, placeholder) {
22
- if (isApiRuntime(runtime))
23
- return placeholder === '{systemBody}';
24
- return [...runtime.run, ...runtime.resume].some((a) => a.includes(placeholder));
25
- }
26
- /**
27
- * 这个运行时把板书写法预载进系统提示了吗:模板用了 {boardFile} / {systemBody},或者(2026-09-19 头一版)写死了 cotutor-board/SKILL.md 的路径。
28
- * 没有 → 应用在话题第一条注入 <cotutor-board>。
29
- */
30
- export function boardPreloaded(runtime) {
31
- return runtimeUses(runtime, '{boardFile}') || runtimeUses(runtime, '{systemBody}') || runtimeUses(runtime, 'cotutor-board/SKILL.md');
32
- }
33
- export function planRun(config, index, vars) {
34
- const { name, runtime } = getRuntime(config, vars.runtime);
35
- if (isApiRuntime(runtime))
36
- throw new RuntimeError(`运行时 "${name}" 是 API 运行时,不起命令行(src/brain/api.ts)`);
37
- const adapter = adapterFor(runtime);
38
- const session = index.session && index.session.runtime === name ? index.session.id : null;
39
- const template = session ? runtime.resume : runtime.run;
40
- const effort = vars.effort ?? POLICY_DEFAULTS.effort;
41
- return {
42
- runtime: name,
43
- cli: adapter.name,
44
- tools: vars.tools ?? 'on',
45
- argv: fillArgs(adapter, template, { agent: vars.agent, prompt: vars.prompt, session: session ?? undefined, agentBody: vars.agentBody, systemBody: vars.systemBody, boardFile: vars.boardFile, effort: vars.effort, tools: vars.tools, readDirs: vars.readDirs }),
46
- resume: session !== null,
47
- session,
48
- ...(runtime.stdin ? { stdin: adapter.stdinMessage(vars.prompt) } : {}),
49
- env: adapter.planEnv?.({ root: vars.root, effort, tools: vars.tools ?? 'on' }) ?? {},
50
- };
51
- }
52
- /** stream-json 输入的一条用户消息(Qwen 的 `--input-format stream-json` 一行一条;别的 CLI 看它的适配器) */
53
- export const stdinMessage = streamJsonUserMessage;
54
- /**
55
- * 断流后接着跑的那条消息(policy.stall):resume 同一个会话发给老师。被杀的那段回复只流到了 stdout,没进会话,
56
- * 所以断在半截的正文(open)原样递回去,让它从断处接着写;应用把 open 拼在这次的收尾正文前面。
57
- * open 为空 = 这一段一个字没出来,已经做完的工具调用与整段都在会话里,接着把这一轮做完。
58
- */
59
- export function stallPrompt(open) {
60
- const rule = '不道歉,不提断了,不重复前面的。';
61
- return open
62
- ? `(应用注)你刚才那段回复传到一半网络断了。孩子那边已经看到、听到的就是下面 <<< 与 >>> 之间这些:\n<<<\n${open}\n>>>\n从断的地方直接接着写,字要接得上;${rule}`
63
- : `(应用注)你刚才那段回复网络断了,一个字没传出来。已经做完的工具调用和写完的段都还在,接着把这一轮做完;${rule}`;
64
- }
65
- /**
66
- * 老师把工具调用写成了正文、停在那里等结果(这轮没给它那个工具,qwen 照自己的格式写出来;板书解析器把那块剥掉了):
67
- * resume 同一个会话,告诉它不会有结果,接着直接回孩子。那块前面写的话已经进了会话,应用拼在这次的前面
68
- */
69
- export function toolLeakPrompt() {
70
- return '(应用注)你刚才写的工具调用没有执行,孩子也看不到。这一轮不用工具,接着直接回孩子;不提工具,不道歉,不重复前面的。';
71
- }
@@ -1,35 +0,0 @@
1
- import { type RunPlan } from './run-plan.ts';
2
- import type { CliRuntime, CotutorConfig, Policy } from '../../schema/index.ts';
3
- import type { BrainExecutor, BrainHooks, BrainRequest, BrainRun, WarmKey } from '../index.ts';
4
- import { type Slot } from './warm.ts';
5
- export declare class CliBrain implements BrainExecutor {
6
- private readonly spares;
7
- private readonly workspace;
8
- /** 起子进程用的环境(测试注入);缺省 process.env */
9
- private readonly env;
10
- constructor(opts: {
11
- workspace: () => {
12
- config: CotutorConfig;
13
- root: string;
14
- };
15
- env?: NodeJS.ProcessEnv;
16
- warmIdleMs?: number;
17
- });
18
- plan(req: BrainRequest): RunPlan;
19
- prepare(plan: RunPlan): Promise<void>;
20
- canWarm(runtime: CliRuntime): boolean;
21
- warm(tutor: string, slot: Slot, plan: RunPlan, key: WarmKey): void;
22
- warmed(tutor: string, slot?: Slot): boolean;
23
- dropWarm(tutor: string, slot?: Slot): void;
24
- close(): void;
25
- private processEnv;
26
- start(req: BrainRequest, plan: RunPlan, ctx: {
27
- tutor: string;
28
- key: WarmKey;
29
- files: {
30
- log: string;
31
- err: string;
32
- };
33
- stall: Policy['stall'];
34
- }, hooks: BrainHooks): BrainRun;
35
- }