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
@@ -0,0 +1,443 @@
1
+ /**
2
+ * cotutor service:把一个 workspace 的 serve 装成这台电脑的后台服务——登录后自己起来、崩了自己拉起、日志按天落文件(serve-log.ts)。
3
+ * macOS:用户级 LaunchAgent ~/Library/LaunchAgents/com.tarslab.cotutor.<名>.plist(launchctl bootstrap gui/<uid>);日志 ~/Library/Logs/cotutor/<名>/
4
+ * Linux:systemd 用户服务 ~/.config/systemd/user/cotutor-<名>.service(systemctl --user);日志 ~/.local/state/cotutor/logs/<名>/;不登录也要跑要 loginctl enable-linger
5
+ * Windows:用户的 Run 注册表项 + wscript 跑的隐藏启动器 %LOCALAPPDATA%\cotutor\service\cotutor-<名>.js + 看护进程 cotutor service run(runKeeper);为什么这样见 service-windows.ts;日志 %LOCALAPPDATA%\cotutor\logs\<名>\
6
+ * 一个 workspace 一个服务;找服务按文件里写的 --workspace 找,不按名字推(两个同名目录的 workspace 不会认错)。
7
+ * 装的那一刻把 node、bin、PATH 写死:launchd 的 PATH 里没有 Homebrew 和 nvm;换了 node 版本、包挪了地方,再 install 一次(幂等)。
8
+ * 退出码 0 不拉起(端口被别人占着时 serve 以 0 退,不一直重试);崩了(非 0、被杀)隔 10 秒拉起。
9
+ */
10
+ import { execFile, spawn } from 'node:child_process';
11
+ import { createHash } from 'node:crypto';
12
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, realpathSync, renameSync, statSync } from 'node:fs';
13
+ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
14
+ import { request as httpRequest } from 'node:http';
15
+ import { request as httpsRequest } from 'node:https';
16
+ import { homedir } from 'node:os';
17
+ import { basename, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { UsageError } from "../../shared/errors.js";
20
+ import { logFileFor } from "./serve-log.js";
21
+ import { RUN_KEY, STARTUP_APPROVED_KEY, decodeServiceFile, encodeLauncher, findProcsScript, launcherArgs, parseProcs, psArgs, runValue, startupDisabled, windowsLauncher } from "./service-windows.js";
22
+ /** 命令的输出:先按 UTF-8;中文 Windows 上 reg、taskkill 这些回的是 GBK */
23
+ function decodeOutput(buf) {
24
+ try {
25
+ return new TextDecoder('utf-8', { fatal: true }).decode(buf);
26
+ }
27
+ catch {
28
+ return new TextDecoder('gbk').decode(buf);
29
+ }
30
+ }
31
+ export const realHost = () => ({
32
+ platform: process.platform,
33
+ home: homedir(),
34
+ uid: process.getuid?.() ?? 0,
35
+ env: process.env,
36
+ run: (cmd, args) => new Promise((resolve) => {
37
+ execFile(cmd, args, { timeout: 30_000, encoding: 'buffer', windowsHide: true }, (err, stdout, stderr) => {
38
+ const code = err ? (typeof err.code === 'number' ? err.code : 1) : 0;
39
+ const errText = decodeOutput(stderr ?? Buffer.alloc(0));
40
+ resolve({ code, stdout: decodeOutput(stdout ?? Buffer.alloc(0)), stderr: errText || (err ? err.message : '') });
41
+ });
42
+ }),
43
+ });
44
+ const LABEL_PREFIX = 'com.tarslab.cotutor.';
45
+ const UNIT_PREFIX = 'cotutor-';
46
+ /** 这个系统做没做 */
47
+ export function servicePlatform(platform) {
48
+ if (platform === 'darwin' || platform === 'linux' || platform === 'win32')
49
+ return platform;
50
+ throw new UsageError(`${platform} 上没做后台服务,用 cotutor serve 在前台跑`);
51
+ }
52
+ const winLocal = (host) => host.env.LOCALAPPDATA || join(host.home, 'AppData', 'Local');
53
+ /** 服务名:workspace 目录名的小写字母数字连字符;撞了别的 workspace 加路径的短 hash */
54
+ export function serviceName(root, taken = false) {
55
+ const base = basename(root).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
56
+ return taken ? `${base}-${createHash('sha256').update(root).digest('hex').slice(0, 6)}` : base;
57
+ }
58
+ /** 服务文件放哪 */
59
+ export function serviceDir(host) {
60
+ const p = servicePlatform(host.platform);
61
+ if (p === 'darwin')
62
+ return join(host.home, 'Library', 'LaunchAgents');
63
+ if (p === 'win32')
64
+ return join(winLocal(host), 'cotutor', 'service');
65
+ return join(host.env.XDG_CONFIG_HOME || join(host.home, '.config'), 'systemd', 'user');
66
+ }
67
+ /** 日志放哪(一个服务一个目录) */
68
+ export function serviceLogDir(host, name) {
69
+ const p = servicePlatform(host.platform);
70
+ if (p === 'darwin')
71
+ return join(host.home, 'Library', 'Logs', 'cotutor', name);
72
+ if (p === 'win32')
73
+ return join(winLocal(host), 'cotutor', 'logs', name);
74
+ return join(host.env.XDG_STATE_HOME || join(host.home, '.local', 'state'), 'cotutor', 'logs', name);
75
+ }
76
+ const fileOf = (host, name) => {
77
+ const p = servicePlatform(host.platform);
78
+ return join(serviceDir(host), p === 'darwin' ? `${LABEL_PREFIX}${name}.plist` : p === 'win32' ? `${UNIT_PREFIX}${name}.js` : `${UNIT_PREFIX}${name}.service`);
79
+ };
80
+ const labelOf = (platform, name) => (platform === 'darwin' ? `${LABEL_PREFIX}${name}` : `${UNIT_PREFIX}${name}`);
81
+ /** serve 起来之前的输出(node 自己报的、模块没加载上)去哪:给人看的一句 */
82
+ export function earlyLogHint(platform, s) {
83
+ if (platform === 'linux')
84
+ return `journalctl --user -u ${s.label}.service`;
85
+ return join(s.logDir ?? '', platform === 'win32' ? KEEPER_LOG : 'launchd.log');
86
+ }
87
+ /** 看护进程的日志(Windows):serve 退了几次、起来之前的输出 */
88
+ export const KEEPER_LOG = 'keeper.log';
89
+ const KEEPER_LOG_MAX = 5 * 1024 * 1024;
90
+ const KEEPER_RESTART_MS = 10_000;
91
+ /**
92
+ * service install 之后开不开家长端:只有这个孩子第一次装时开,下一步就是填百炼、扫码。
93
+ * 别的启动都不开:登录、崩溃重起、升级、重启都不是人要看页面;放进 serve 的话,一直崩就隔 10 秒开一个标签页。
94
+ * ssh 进来的不开:没人看,macOS 的 open 还会开到那台电脑的屏幕上。Linux 要有图形会话。--open 是人明说的,重装也开;--no-open 最大
95
+ */
96
+ export function opensAfterInstall(o) {
97
+ if (o.noOpen)
98
+ return false;
99
+ if (o.open)
100
+ return true;
101
+ if (!o.firstTime || o.env.SSH_CONNECTION || o.env.SSH_TTY)
102
+ return false;
103
+ return o.platform !== 'linux' || Boolean(o.env.DISPLAY || o.env.WAYLAND_DISPLAY);
104
+ }
105
+ /**
106
+ * 看护进程(Windows;cotutor service run):起 serve,非 0 隔 10 秒再起,0 不起(端口被占着);同 launchd 的 KeepAlive、systemd 的 Restart=on-failure。
107
+ * 给 serve 带上 COTUTOR_SERVICE(mode 算后台服务、端口被占以 0 退);返回退出码
108
+ */
109
+ export async function runKeeper(o) {
110
+ mkdirSync(o.logDir, { recursive: true });
111
+ const logFile = join(o.logDir, KEEPER_LOG);
112
+ const note = (text) => { try {
113
+ appendFileSync(logFile, `${new Date().toISOString()} ${text}\n`);
114
+ }
115
+ catch { /* 写不了日志不能把看护带倒 */ } };
116
+ for (;;) {
117
+ try {
118
+ if (statSync(logFile).size > KEEPER_LOG_MAX)
119
+ renameSync(logFile, `${logFile}.1`);
120
+ }
121
+ catch { /* 还没有 */ }
122
+ const fd = openSync(logFile, 'a');
123
+ const child = spawn(o.node ?? process.execPath, [o.bin ?? binPath(), 'serve', '--workspace', o.root, '--log-dir', o.logDir], { stdio: ['ignore', fd, fd], windowsHide: true, env: { ...process.env, COTUTOR_SERVICE: o.label } });
124
+ const code = await new Promise((resolve) => {
125
+ child.once('error', (err) => { note(`serve 起不来:${err.message}`); resolve(null); });
126
+ child.once('exit', (c) => resolve(c));
127
+ });
128
+ closeSync(fd);
129
+ if (code === 0) {
130
+ note('serve 以 0 退(端口被占着?),不再起;cotutor service start 再起');
131
+ return 0;
132
+ }
133
+ note(`serve 退了(${code ?? '没起来'}),${Math.round((o.restartMs ?? KEEPER_RESTART_MS) / 1000)} 秒后再起`);
134
+ await new Promise((r) => setTimeout(r, o.restartMs ?? KEEPER_RESTART_MS));
135
+ }
136
+ }
137
+ export const wscriptPath = (host) => join(host.env.SystemRoot || 'C:\\Windows', 'System32', 'wscript.exe');
138
+ /** 这个 workspace 的看护进程与 serve 现在的进程 */
139
+ async function windowsProcs(host, root) {
140
+ return parseProcs((await host.run('powershell.exe', psArgs(findProcsScript(root)))).stdout);
141
+ }
142
+ /** 停:看护进程连着子进程一起杀;看护没了留下的 serve 也杀 */
143
+ async function stopWindows(host, root) {
144
+ const procs = await windowsProcs(host, root);
145
+ for (const p of [...procs].sort((a, b) => Number(b.keeper) - Number(a.keeper)))
146
+ await host.run('taskkill.exe', ['/PID', String(p.pid), '/T', '/F']);
147
+ }
148
+ /** 起:wscript 跑启动器,藏着窗口起看护进程,马上回来 */
149
+ async function startWindows(host, file) {
150
+ const r = await host.run(wscriptPath(host), ['//B', '//Nologo', file]);
151
+ if (r.code !== 0)
152
+ throw new UsageError(`wscript 没成(系统可能关了 Windows Script Host):${(r.stderr || r.stdout).trim()}`);
153
+ }
154
+ export const xml = (s) => s.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
155
+ const unxml = (s) => s.replaceAll('&lt;', '<').replaceAll('&gt;', '>').replaceAll('&quot;', '"').replaceAll('&apos;', "'").replaceAll('&amp;', '&');
156
+ /** macOS 的 LaunchAgent */
157
+ export function launchdPlist(s) {
158
+ const str = (v) => `<string>${xml(v)}</string>`;
159
+ return `<?xml version="1.0" encoding="UTF-8"?>
160
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
161
+ <!-- cotutor service install 写的;改了会被下次 install 盖掉。卸掉:cotutor service uninstall --workspace ${xml(s.root)} -->
162
+ <plist version="1.0">
163
+ <dict>
164
+ <key>Label</key>${str(s.label)}
165
+ <key>ProgramArguments</key>
166
+ <array>
167
+ ${s.args.map((a) => ` ${str(a)}`).join('\n')}
168
+ </array>
169
+ <key>WorkingDirectory</key>${str(s.root)}
170
+ <key>EnvironmentVariables</key>
171
+ <dict>
172
+ <key>PATH</key>${str(s.path)}
173
+ <key>COTUTOR_SERVICE</key>${str(s.label)}
174
+ </dict>
175
+ <key>RunAtLoad</key><true/>
176
+ <key>KeepAlive</key>
177
+ <dict>
178
+ <key>SuccessfulExit</key><false/>
179
+ </dict>
180
+ <key>ThrottleInterval</key><integer>10</integer>
181
+ <key>ProcessType</key><string>Interactive</string>
182
+ <key>StandardOutPath</key>${str(join(s.logDir, 'launchd.log'))}
183
+ <key>StandardErrorPath</key>${str(join(s.logDir, 'launchd.log'))}
184
+ </dict>
185
+ </plist>
186
+ `;
187
+ }
188
+ /** systemd 的一个参数:双引号括起来,反斜杠与引号转义,% 写两个 */
189
+ const sd = (v) => `"${v.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')}"`;
190
+ /** Linux 的 systemd 用户服务 */
191
+ export function systemdUnit(s) {
192
+ return `# cotutor service install 写的;改了会被下次 install 盖掉。卸掉:cotutor service uninstall --workspace ${s.root}
193
+ [Unit]
194
+ Description=cotutor (${s.name})
195
+ After=network-online.target
196
+
197
+ [Service]
198
+ ExecStart=${s.args.map(sd).join(' ')}
199
+ WorkingDirectory=${sd(s.root)}
200
+ Environment=${sd(`PATH=${s.path}`)}
201
+ Environment=${sd(`COTUTOR_SERVICE=${s.label}`)}
202
+ Restart=on-failure
203
+ RestartSec=10
204
+
205
+ [Install]
206
+ WantedBy=default.target
207
+ `;
208
+ }
209
+ /** 从服务文件里读回跑的参数(找它是哪个 workspace 的) */
210
+ export function serviceArgs(text) {
211
+ const js = launcherArgs(text);
212
+ if (js)
213
+ return js;
214
+ const plist = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text);
215
+ if (plist)
216
+ return [...plist[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unxml(m[1]));
217
+ const line = /^ExecStart=(.*)$/m.exec(text)?.[1];
218
+ if (!line)
219
+ return [];
220
+ const out = [];
221
+ for (const m of line.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g))
222
+ out.push(m[1] !== undefined ? m[1].replace(/\\(.)/g, '$1').replaceAll('%%', '%') : m[2]);
223
+ return out;
224
+ }
225
+ const workspaceOf = (args) => { const i = args.indexOf('--workspace'); return i >= 0 && args[i + 1] ? args[i + 1] : null; };
226
+ const real = (p) => { try {
227
+ return realpathSync(p);
228
+ }
229
+ catch {
230
+ return p;
231
+ } };
232
+ /** 这台电脑上 cotutor 装的服务(各是哪个 workspace 的) */
233
+ export async function listServices(host) {
234
+ const dir = serviceDir(host);
235
+ const platform = servicePlatform(host.platform);
236
+ const pattern = platform === 'darwin' ? /^com\.tarslab\.cotutor\.(.+)\.plist$/ : platform === 'win32' ? /^cotutor-(.+)\.js$/ : /^cotutor-(.+)\.service$/;
237
+ const out = [];
238
+ for (const f of (await readdir(dir).catch(() => [])).sort()) {
239
+ const m = pattern.exec(f);
240
+ if (!m)
241
+ continue;
242
+ const args = serviceArgs(decodeServiceFile(await readFile(join(dir, f)).catch(() => Buffer.alloc(0))));
243
+ const root = workspaceOf(args);
244
+ if (!root)
245
+ continue;
246
+ const li = args.indexOf('--log-dir');
247
+ out.push({ name: m[1], label: labelOf(platform, m[1]), file: join(dir, f), args, root, logDir: li >= 0 ? args[li + 1] ?? null : null });
248
+ }
249
+ return out;
250
+ }
251
+ /** 这个 workspace 的服务;没装 → null(Windows 上路径不分大小写) */
252
+ export async function findService(host, root) {
253
+ const norm = (p) => (host.platform === 'win32' ? real(p).toLowerCase() : real(p));
254
+ const want = norm(root);
255
+ return (await listServices(host)).find((s) => norm(s.root) === want) ?? null;
256
+ }
257
+ /** cotutor 的 bin(npm 装的是 dist/app/cli → 包根;仓里直跑是 src/app/cli → 仓根,一样往上三层) */
258
+ export const binPath = () => fileURLToPath(new URL('../../../bin/cotutor.js', import.meta.url));
259
+ /** 装:写服务文件、交给 launchd / systemd 起来;已经装过就照现在的 node、bin、PATH 重写一遍再起 */
260
+ export async function installService(host, root, opts = {}) {
261
+ const platform = servicePlatform(host.platform);
262
+ const had = await findService(host, root);
263
+ let name = had?.name ?? serviceName(root);
264
+ if (!had && existsSync(fileOf(host, name)))
265
+ name = serviceName(root, true);
266
+ const label = labelOf(platform, name);
267
+ const logDir = serviceLogDir(host, name);
268
+ // Windows 上跑的是看护进程,它再起 serve(那边没有 launchd / systemd 替你拉起)
269
+ const run = platform === 'win32' ? ['service', 'run'] : ['serve'];
270
+ const spec = { name, label, root, logDir, path: host.env.PATH ?? '/usr/bin:/bin', args: [opts.node ?? process.execPath, opts.bin ?? binPath(), ...run, '--workspace', root, '--log-dir', logDir] };
271
+ const file = fileOf(host, name);
272
+ await mkdir(serviceDir(host), { recursive: true });
273
+ await mkdir(logDir, { recursive: true });
274
+ if (platform === 'win32') {
275
+ if (had)
276
+ await stopWindows(host, root);
277
+ await writeFile(file, encodeLauncher(windowsLauncher(spec.args, root)));
278
+ const reg = await host.run('reg.exe', ['add', RUN_KEY, '/v', label, '/t', 'REG_SZ', '/d', runValue(wscriptPath(host), file), '/f']);
279
+ if (reg.code !== 0)
280
+ throw new UsageError(`写登录项没成:${(reg.stderr || reg.stdout).trim()}`);
281
+ // 再装一次 = 要它:以前在任务管理器里关掉的记录也去掉(没有这一笔时报错,不管)
282
+ await host.run('reg.exe', ['delete', STARTUP_APPROVED_KEY, '/v', label, '/f']);
283
+ await startWindows(host, file);
284
+ }
285
+ else if (platform === 'darwin') {
286
+ // 先卸下旧的(没装过会报错,不管),再装上;bootstrap 会按 RunAtLoad 马上起
287
+ await host.run('launchctl', ['bootout', `gui/${host.uid}/${label}`]);
288
+ await writeFile(file, launchdPlist(spec));
289
+ const r = await host.run('launchctl', ['bootstrap', `gui/${host.uid}`, file]);
290
+ if (r.code !== 0)
291
+ throw new UsageError(`launchctl bootstrap 没成:${(r.stderr || r.stdout).trim()}`);
292
+ }
293
+ else {
294
+ await writeFile(file, systemdUnit(spec));
295
+ await host.run('systemctl', ['--user', 'daemon-reload']);
296
+ const r = await host.run('systemctl', ['--user', 'enable', '--now', `${label}.service`]);
297
+ if (r.code !== 0)
298
+ throw new UsageError(`systemctl --user enable 没成:${(r.stderr || r.stdout).trim()}`);
299
+ if (had)
300
+ await host.run('systemctl', ['--user', 'restart', `${label}.service`]);
301
+ }
302
+ return { spec, file, replaced: Boolean(had) };
303
+ }
304
+ /** 卸:停下、删服务文件;日志留着 */
305
+ export async function uninstallService(host, root) {
306
+ const s = await findService(host, root);
307
+ if (!s)
308
+ return null;
309
+ const platform = servicePlatform(host.platform);
310
+ if (platform === 'win32') {
311
+ await stopWindows(host, s.root);
312
+ await host.run('reg.exe', ['delete', RUN_KEY, '/v', s.label, '/f']);
313
+ await host.run('reg.exe', ['delete', STARTUP_APPROVED_KEY, '/v', s.label, '/f']);
314
+ }
315
+ else if (platform === 'darwin')
316
+ await host.run('launchctl', ['bootout', `gui/${host.uid}/${s.label}`]);
317
+ else
318
+ await host.run('systemctl', ['--user', 'disable', '--now', `${s.label}.service`]);
319
+ await rm(s.file, { force: true });
320
+ if (host.platform === 'linux')
321
+ await host.run('systemctl', ['--user', 'daemon-reload']);
322
+ return s;
323
+ }
324
+ const need = async (host, root) => {
325
+ const s = await findService(host, root);
326
+ if (!s)
327
+ throw new UsageError('这个 workspace 没装后台服务:cotutor service install');
328
+ return s;
329
+ };
330
+ /** 起 / 停 / 重起。macOS 的停是从 launchd 卸下、Windows 的停是杀进程(服务文件都还在,下次登录又会起;要一直不起用 uninstall) */
331
+ export async function controlService(host, root, what) {
332
+ const s = await need(host, root);
333
+ const platform = servicePlatform(host.platform);
334
+ if (platform === 'win32') {
335
+ if (what !== 'start')
336
+ await stopWindows(host, s.root);
337
+ if (what === 'stop')
338
+ return s;
339
+ // 已经在跑就不再起一个(再起一个也会因为端口被占以 0 退,只是多转一圈)
340
+ if (!(await windowsProcs(host, s.root)).some((p) => p.keeper))
341
+ await startWindows(host, s.file);
342
+ return s;
343
+ }
344
+ if (platform === 'darwin') {
345
+ const target = `gui/${host.uid}/${s.label}`;
346
+ const loaded = (await host.run('launchctl', ['print', target])).code === 0;
347
+ if (what === 'stop') {
348
+ if (loaded)
349
+ await host.run('launchctl', ['bootout', target]);
350
+ return s;
351
+ }
352
+ const r = loaded ? await host.run('launchctl', what === 'restart' ? ['kickstart', '-k', target] : ['kickstart', target]) : await host.run('launchctl', ['bootstrap', `gui/${host.uid}`, s.file]);
353
+ if (r.code !== 0)
354
+ throw new UsageError(`launchctl 没成:${(r.stderr || r.stdout).trim()}`);
355
+ return s;
356
+ }
357
+ const r = await host.run('systemctl', ['--user', what, `${s.label}.service`]);
358
+ if (r.code !== 0)
359
+ throw new UsageError(`systemctl --user ${what} 没成:${(r.stderr || r.stdout).trim()}`);
360
+ return s;
361
+ }
362
+ /** launchctl print 那一大段里要的几行(一层缩进的 state / pid / last exit code) */
363
+ export function parseLaunchctlPrint(text) {
364
+ const field = (k) => new RegExp(`^\\t${k} = (.*)$`, 'm').exec(text)?.[1]?.trim() ?? null;
365
+ const pid = Number(field('pid'));
366
+ return { running: field('state') === 'running', pid: Number.isInteger(pid) && pid > 0 ? pid : null, lastExit: field('last exit code') };
367
+ }
368
+ /** systemctl show -p ActiveState,MainPID,ExecMainStatus 的输出 */
369
+ export function parseSystemctlShow(text) {
370
+ const kv = Object.fromEntries(text.split('\n').map((l) => l.split('=')).filter((p) => p.length >= 2).map(([k, ...v]) => [k.trim(), v.join('=').trim()]));
371
+ const pid = Number(kv.MainPID);
372
+ return { running: kv.ActiveState === 'active', pid: pid > 0 ? pid : null, lastExit: kv.ExecMainStatus ?? null };
373
+ }
374
+ /** 本机问一下 serve 起来没有(自签证书不验);先 https 再 http */
375
+ export async function probeHealth(port, timeoutMs = 1500) {
376
+ const get = (secure) => new Promise((resolve) => {
377
+ const req = (secure ? httpsRequest : httpRequest)({ host: '127.0.0.1', port, path: '/api/health', method: 'GET', timeout: timeoutMs, ...(secure ? { rejectUnauthorized: false } : {}) }, (res) => {
378
+ let body = '';
379
+ res.setEncoding('utf8');
380
+ res.on('data', (c) => { body += c; });
381
+ res.on('end', () => { try {
382
+ const j = JSON.parse(body);
383
+ resolve(j.ok ? { ...j, scheme: secure ? 'https' : 'http' } : null);
384
+ }
385
+ catch {
386
+ resolve(null);
387
+ } });
388
+ });
389
+ req.on('timeout', () => req.destroy());
390
+ req.on('error', () => resolve(null));
391
+ req.end();
392
+ });
393
+ return (await get(true)) ?? (await get(false));
394
+ }
395
+ /** 装没装、在不在跑、问得到不(端口来自 cotutor.json 或 --port) */
396
+ export async function serviceStatus(host, root, port) {
397
+ const s = await findService(host, root);
398
+ let st = { running: false, pid: null, lastExit: null };
399
+ let autostart = null;
400
+ if (s && host.platform === 'win32') {
401
+ const procs = await windowsProcs(host, s.root);
402
+ st = { running: procs.some((p) => p.keeper), pid: (procs.find((p) => !p.keeper) ?? procs[0])?.pid ?? null, lastExit: null };
403
+ autostart = (await host.run('reg.exe', ['query', RUN_KEY, '/v', s.label])).code === 0 && !startupDisabled((await host.run('reg.exe', ['query', STARTUP_APPROVED_KEY, '/v', s.label])).stdout);
404
+ }
405
+ else if (s) {
406
+ const r = host.platform === 'darwin'
407
+ ? await host.run('launchctl', ['print', `gui/${host.uid}/${s.label}`])
408
+ : await host.run('systemctl', ['--user', 'show', '-p', 'ActiveState,MainPID,ExecMainStatus', `${s.label}.service`]);
409
+ if (r.code === 0)
410
+ st = host.platform === 'darwin' ? parseLaunchctlPrint(r.stdout) : parseSystemctlShow(r.stdout);
411
+ }
412
+ const health = await probeHealth(port);
413
+ return { installed: s, ...st, health, logFile: s?.logDir ? logFileFor(s.logDir, new Date()) : null, autostart };
414
+ }
415
+ /** 起来以后等它答应(装完、重起完);等不到 → null */
416
+ export async function waitHealthy(port, ms = 15_000, since) {
417
+ const until = Date.now() + ms;
418
+ while (Date.now() < until) {
419
+ const h = await probeHealth(port, 1000);
420
+ if (h && (!since || h.started > since))
421
+ return h;
422
+ await new Promise((r) => setTimeout(r, 300));
423
+ }
424
+ return null;
425
+ }
426
+ /** 日志的最后 n 行(今天的没有就找最近一天的) */
427
+ export async function tailLog(logDir, n = 50) {
428
+ const files = (await readdir(logDir).catch(() => [])).filter((f) => /^\d{4}-\d{2}-\d{2}\.log$/.test(f)).sort();
429
+ const last = files.at(-1);
430
+ if (!last)
431
+ return null;
432
+ const file = join(logDir, last);
433
+ const lines = (await readFile(file, 'utf8').catch(() => '')).split('\n');
434
+ if (lines.at(-1) === '')
435
+ lines.pop();
436
+ return { file, lines: lines.slice(-n) };
437
+ }
438
+ /** pmset -g custom 里「AC Power:」那一段的 sleep(接着电源几分钟没人碰就睡,0 = 不睡);没有那一段 → null */
439
+ export function acSleepMinutes(pmset) {
440
+ const ac = /^AC Power:\s*$([\s\S]*?)(?=^\S|(?![\s\S]))/m.exec(pmset)?.[1];
441
+ const v = ac ? /^\s+sleep\s+(\d+)/m.exec(ac)?.[1] : undefined;
442
+ return v === undefined ? null : Number(v);
443
+ }
@@ -10,7 +10,7 @@ export interface ShippedAgent {
10
10
  description: string;
11
11
  }
12
12
  export declare function shippedAgents(dir?: string): Promise<ShippedAgent[]>;
13
- export declare const GITIGNORE = "# \u8F6C\u5F55\u65E5\u5FD7\u4F53\u79EF\u5927\u3001\u53EF\u4ECE\u5BF9\u8BDD\u7D22\u5F15\u91CD\u5EFA\u5173\u952E\u4FE1\u606F;\u8981\u7559\u5168\u6863\u5C31\u5220\u6389\u4E0B\u9762\u8FD9\u884C\nconversations/**/*.log\n# \u914D\u97F3\u53EF\u91CD\u65B0\u5408\u6210\nconversations/**/*.mp3\n# \u56DE\u653E(cotutor replay)\u662F\u8C03\u63D0\u793A\u8BCD\u4E0E vault \u65F6\u7684\u5BF9\u7167,\u4E0D\u662F\u5B69\u5B50\u7684\u5BF9\u8BDD\nevals/\n# \u505A\u597D\u7684\u8BFE(\u8BFE\u5305\u3001\u89C6\u9891\u8BFE)\u4E0E\u8BFE\u5305\u622A\u56FE:\u4F53\u79EF\u5927,\u6E90\u5728\u5BB6\u957F\u505A\u8BFE\u7684\u5730\u65B9\nbundles/\nlectures/\nsnaps/\n# \u53E3\u8BED\u8BFE(\u5B69\u5B50\u7684\u539F\u58F0\u3001\u7167\u7247)\ntalk/\n# \u4F5C\u4E1A\u7167\u7247(\u5B69\u5B50\u5728\u8001\u5E08\u9875\u4E0A\u62CD\u7684;paths.captures):\u4F53\u79EF\u5927\u3001\u53EA\u662F\u8FD9\u4E00\u8F6E\u7684\u7D20\u6750,\u8BA4\u51FA\u7684\u6587\u5B57\u5728\u65E5\u8BB0\u91CC\ncaptures/\n# \u5BB6\u957F\u5728 workspace \u91CC\u7528 Claude Code \u65F6\u5B83\u7684\u4E2A\u4EBA\u8BBE\u7F6E\n.claude/settings.local.json\n# .cotutor/ \u662F\u673A\u5668\u6587\u4EF6(\u677F\u4E66\u5199\u6CD5\u7F13\u5B58\u3001\u914D\u97F3\u7F13\u5B58\u3001\u4E8C\u7EF4\u7801\u3001\u65E5\u5FD7\u3001CLI \u58F3\u3001qwen \u7684\u5BB6),\u53EA\u7559\u51FA\u5382 hash \u4E0E schema\n.cotutor/*\n!.cotutor/shipped.json\n!.cotutor/cotutor.schema.json\nnode_modules/\n.DS_Store\n";
13
+ export declare const GITIGNORE = "# \u8F6C\u5F55\u65E5\u5FD7\u4F53\u79EF\u5927\u3001\u53EF\u4ECE\u5BF9\u8BDD\u7D22\u5F15\u91CD\u5EFA\u5173\u952E\u4FE1\u606F;\u8981\u7559\u5168\u6863\u5C31\u5220\u6389\u4E0B\u9762\u8FD9\u884C\nconversations/**/*.log\n# \u914D\u97F3\u53EF\u91CD\u65B0\u5408\u6210\nconversations/**/*.mp3\n# \u56DE\u653E(cotutor replay)\u662F\u8C03\u63D0\u793A\u8BCD\u4E0E vault \u65F6\u7684\u5BF9\u7167,\u4E0D\u662F\u5B69\u5B50\u7684\u5BF9\u8BDD\nevals/\n# \u505A\u597D\u7684\u8BFE(\u8BFE\u5305\u3001\u89C6\u9891\u8BFE)\u4E0E\u8BFE\u5305\u622A\u56FE:\u4F53\u79EF\u5927,\u6E90\u5728\u5BB6\u957F\u505A\u8BFE\u7684\u5730\u65B9\nbundles/\nlectures/\nsnaps/\n# \u53E3\u8BED\u8BFE(\u5B69\u5B50\u7684\u539F\u58F0\u3001\u7167\u7247)\ntalk/\n# \u4F5C\u4E1A\u7167\u7247(\u5B69\u5B50\u5728\u8001\u5E08\u9875\u4E0A\u62CD\u7684;paths.captures):\u4F53\u79EF\u5927\u3001\u53EA\u662F\u8FD9\u4E00\u8F6E\u7684\u7D20\u6750,\u8BA4\u51FA\u7684\u6587\u5B57\u5728\u65E5\u8BB0\u91CC\ncaptures/\n# \u5BB6\u957F\u5728 workspace \u91CC\u7528 Claude Code \u65F6\u5B83\u7684\u4E2A\u4EBA\u8BBE\u7F6E\n.claude/settings.local.json\n# .cotutor/ \u662F\u673A\u5668\u6587\u4EF6(\u677F\u4E66\u5199\u6CD5\u7F13\u5B58\u3001\u914D\u97F3\u7F13\u5B58\u3001\u4E8C\u7EF4\u7801\u3001\u65E5\u5FD7),\u53EA\u7559\u51FA\u5382 hash \u4E0E schema\n.cotutor/*\n!.cotutor/shipped.json\n!.cotutor/cotutor.schema.json\nnode_modules/\n.DS_Store\n";
14
14
  /** 档案模板(paths.profile,缺了才写):cotutor: profile 的那篇,原文整篇进每个话题的第一条 */
15
15
  export declare function profileTemplate(name: string): string;
16
16
  /** 参考目录的 README(缺了才写) */
@@ -11,8 +11,8 @@ import { readFile, readdir } from 'node:fs/promises';
11
11
  import { join } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { parseAgentFile } from "../../runtime/agent-file.js";
14
- import { CONFIG_SCHEMA_FILE, DICTATION_DEFAULT, cotutorJsonSchema } from "../../schema/index.js";
15
- import { FACTORY_API_RUNTIMES, factoryRuntimes } from "../../brain/index.js";
14
+ import { CONFIG_SCHEMA_FILE, DEFAULT_PORT, DICTATION_DEFAULT, cotutorJsonSchema } from "../../schema/index.js";
15
+ import { FACTORY_API_RUNTIMES } from "../../brain/index.js";
16
16
  import { DEFAULT_GRADE, PACKAGE_GRADES_DIR } from "./grades.js";
17
17
  import { mkdir, writeFile } from 'node:fs/promises';
18
18
  export const DIRS = ['agents', 'ledger', 'conversations', 'bundles'];
@@ -47,7 +47,7 @@ talk/
47
47
  captures/
48
48
  # 家长在 workspace 里用 Claude Code 时它的个人设置
49
49
  .claude/settings.local.json
50
- # .cotutor/ 是机器文件(板书写法缓存、配音缓存、二维码、日志、CLI 壳、qwen 的家),只留出厂 hash 与 schema
50
+ # .cotutor/ 是机器文件(板书写法缓存、配音缓存、二维码、日志),只留出厂 hash 与 schema
51
51
  .cotutor/*
52
52
  !.cotutor/shipped.json
53
53
  !.cotutor/cotutor.schema.json
@@ -113,16 +113,17 @@ permissionMode: bypassPermissions
113
113
  (孩子常见的说法或答偏了,怎么接。)
114
114
  `;
115
115
  }
116
- /** 出厂老师的条目;cards = 板书写法里讲哪几种卡(《卡片协议.md》「谁拿到哪些卡」,2026-10-07),text 总在 */
116
+ /** 出厂老师的条目;cards = 板书写法里讲哪几种卡(《卡片协议.md》「谁拿到哪些卡」,2026-10-07),text 总在;
117
+ * 不写 avatar:三门主科用包里的出厂头像(src/app/http/avatars.ts) */
117
118
  const TUTOR_DEFAULTS = {
118
- // 数学多想一会儿:算错的代价大(别的老师用缺省 low);走 qwen3.8-max(同语文老师)。运行时 api = 应用直连百炼(2026-10-31 起出厂缺省;Qwen Code 那条是 qwen)
119
- 'math-tutor': { display: '数学老师', subject: '数学', avatar: '🧮', runtime: 'api', cards: ['text', 'choice', 'image', 'lecture', 'canvas', 'code'], policy: { effort: 'medium' } },
119
+ // 数学多想一会儿:算错的代价大(别的老师用缺省 low);走 qwen3.8-max(同语文老师)。运行时 api = 应用直连百炼(2026-10-31 起出厂缺省)
120
+ 'math-tutor': { display: '数学老师', subject: '数学', runtime: 'api', cards: ['text', 'choice', 'image', 'lecture', 'canvas', 'code'], policy: { effort: 'medium' } },
120
121
  // 画图卡(paint)不在这里:只在「你说我画」activity 的话题里有(agents/chinese-tutor/activities/你说我画.md 头上的 cards)
121
122
  // 语文老师走 qwen3.8-max(你说我画、看图说话两套 activity eval 在它上面验证;2026-10-31 Ray 上 API 与 Qwen Code 对照过)
122
- 'chinese-tutor': { display: '语文老师', subject: '语文', avatar: '📚', runtime: 'api', cards: ['text', 'read', 'choice', 'image', 'tianzige', 'lecture', 'canvas'] },
123
+ 'chinese-tutor': { display: '语文老师', subject: '语文', runtime: 'api', cards: ['text', 'read', 'choice', 'image', 'tianzige', 'lecture', 'canvas'] },
123
124
  // 英语也走 api(qwen3.8-max):2026-10-31 api-flash 真跑英语四个 activity,13 次里 5 次第一轮只出一张拍照卡、没有讲稿(实时页嘴没话可念);max 在 Ray 上都过。
124
125
  // 想省钱改 api-flash,先跑 activity eval。medium 是 2026-10-27 给 flash 加的(low 偶尔只出卡或念规则,详见年级教学包验证记录)
125
- 'english-tutor': { display: '英语老师', subject: '英语', avatar: '🔤', runtime: 'api', cards: ['text', 'read', 'choice', 'image', 'word', 'lecture'], policy: { effort: 'medium' } },
126
+ 'english-tutor': { display: '英语老师', subject: '英语', runtime: 'api', cards: ['text', 'read', 'choice', 'image', 'word', 'lecture'], policy: { effort: 'medium' } },
126
127
  };
127
128
  /** cotutor.json 模板:只在文件不存在时写入;政策文件永不自动重建或覆盖(家长的决定不由机器代做)。 */
128
129
  export function configTemplate(input) {
@@ -130,7 +131,7 @@ export function configTemplate(input) {
130
131
  for (const a of input.tutors) {
131
132
  const p = TUTOR_DEFAULTS[a.name];
132
133
  tutors[a.name] = p
133
- ? { display: p.display, ...(p.subject ? { subject: p.subject } : {}), avatar: p.avatar, enabled: p.enabled ?? true, ...(p.hidden ? { hidden: true } : {}), ...(p.runtime ? { runtime: p.runtime } : {}), ...(p.cards ? { cards: p.cards } : {}), ...(p.policy ? { policy: p.policy } : {}) }
134
+ ? { display: p.display, ...(p.subject ? { subject: p.subject } : {}), enabled: p.enabled ?? true, ...(p.hidden ? { hidden: true } : {}), ...(p.runtime ? { runtime: p.runtime } : {}), ...(p.cards ? { cards: p.cards } : {}), ...(p.policy ? { policy: p.policy } : {}) }
134
135
  : { display: a.name, enabled: true };
135
136
  }
136
137
  const cfg = {
@@ -138,13 +139,12 @@ export function configTemplate(input) {
138
139
  version: 1,
139
140
  title: input.name ? `${input.name}的老师们` : 'cotutor',
140
141
  kid: { slug: input.slug, ...(input.name ? { name: input.name } : {}) },
141
- server: { port: input.port ?? 5180 },
142
+ server: { port: input.port ?? DEFAULT_PORT },
142
143
  paths: {},
143
144
  policyDefaults: {},
144
145
  tutors,
145
- // 缺省 api:应用直连百炼,家长端「首页」填一次按量 key + 业务空间 id 就能用(2026-10-31)。
146
- // Qwen Code 那两条留作备用(开发者、有 Token Plan 的):由适配器给(src/brain/cli/<cli>.ts 的 runtimes(),旗标为什么这么写的注释也在那里)
147
- runtimes: { default: 'api', ...FACTORY_API_RUNTIMES, ...factoryRuntimes() },
146
+ // 缺省 api:应用直连百炼,家长端「首页」填一次按量 key + 业务空间 id 就能用(2026-10-31);2026-11-02 起只有 API 运行时
147
+ runtimes: { default: 'api', ...FACTORY_API_RUNTIMES },
148
148
  // 听写的拍照认词:模型与挑词要求(prompt 一行一个字符串)写进文件,家长看得见、改得动
149
149
  dictation: DICTATION_DEFAULT,
150
150
  // 字段说明不写在这里:一写进去就冻住(政策文件永不覆盖),$schema 指的 schema 文件每次 init / upgrade 从 zod 的 .describe() 刷新
@@ -51,5 +51,9 @@ export interface SkillUpgradeStep {
51
51
  export declare function upgradeSkills(root: string): Promise<SkillUpgradeStep[]>;
52
52
  /** Claude Code 只认 .claude/skills(Codex、Qwen Code 认 .agents/skills):在那里放一条相对链,两边看到的是同一份 */
53
53
  export declare const CLAUDE_SKILLS_LINK = ".claude/skills";
54
- /** init / upgrade 用:没有就建链;已经是链不动;是实体目录(老 workspace 没搬完)不碰,报一声 */
54
+ /**
55
+ * init / upgrade 用:没有就建链;已经是链不动;是实体目录(老 workspace 没搬完)不碰,报一声。
56
+ * Windows 上建目录联接(junction):符号链接要「创建符号链接」权限,家长平常开的终端没有(UAC 拿掉了,开发者模式也多半没开)。
57
+ * 联接读回来是绝对路径,所以按指到哪儿比、不按字面比。还是建不了就跳过:这条链只给在 workspace 里用 Claude Code 的人,cotutor 自己不读它。
58
+ */
55
59
  export declare function linkClaudeSkills(root: string): Promise<SkillStep>;
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { createHash } from 'node:crypto';
10
10
  import { cp, lstat, mkdir, readFile, readdir, readlink, rm, symlink } from 'node:fs/promises';
11
- import { dirname, join, relative } from 'node:path';
11
+ import { dirname, join, relative, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { BOARD_SKILL } from "../../board/cards/docs.js";
14
14
  import { ANALYZE_SKILL } from "../skills/analyze-doc.js";
@@ -171,7 +171,11 @@ export async function upgradeSkills(root) {
171
171
  }
172
172
  /** Claude Code 只认 .claude/skills(Codex、Qwen Code 认 .agents/skills):在那里放一条相对链,两边看到的是同一份 */
173
173
  export const CLAUDE_SKILLS_LINK = '.claude/skills';
174
- /** init / upgrade 用:没有就建链;已经是链不动;是实体目录(老 workspace 没搬完)不碰,报一声 */
174
+ /**
175
+ * init / upgrade 用:没有就建链;已经是链不动;是实体目录(老 workspace 没搬完)不碰,报一声。
176
+ * Windows 上建目录联接(junction):符号链接要「创建符号链接」权限,家长平常开的终端没有(UAC 拿掉了,开发者模式也多半没开)。
177
+ * 联接读回来是绝对路径,所以按指到哪儿比、不按字面比。还是建不了就跳过:这条链只给在 workspace 里用 Claude Code 的人,cotutor 自己不读它。
178
+ */
175
179
  export async function linkClaudeSkills(root) {
176
180
  const link = join(root, CLAUDE_SKILLS_LINK);
177
181
  const target = join('..', SKILLS_DIR);
@@ -179,11 +183,17 @@ export async function linkClaudeSkills(root) {
179
183
  const st = await lstat(link).catch(() => null);
180
184
  if (st?.isSymbolicLink()) {
181
185
  const to = await readlink(link);
182
- return to === target ? { item, action: 'exists' } : { item, action: 'kept', note: `已经是指向 ${to} 的链,没动` };
186
+ const key = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
187
+ return key(resolve(dirname(link), to.replace(/^\\\\\?\\/, ''))) === key(resolve(root, SKILLS_DIR)) ? { item, action: 'exists' } : { item, action: 'kept', note: `已经是指向 ${to} 的链,没动` };
183
188
  }
184
189
  if (st)
185
190
  return { item, action: 'kept', note: `${CLAUDE_SKILLS_LINK} 是实体目录,没建链:把里面的技能挪进 ${SKILLS_DIR}/、删掉它,再跑 cotutor upgrade` };
186
191
  await mkdir(dirname(link), { recursive: true });
187
- await symlink(target, link);
192
+ try {
193
+ await symlink(target, link, process.platform === 'win32' ? 'junction' : undefined);
194
+ }
195
+ catch (err) {
196
+ return { item, action: 'kept', note: `建不了链(${err.code ?? err}),跳过:只影响在 workspace 里用 Claude Code 看技能,cotutor 照常用` };
197
+ }
188
198
  return { item, action: 'created', note: 'Claude Code 只认 .claude/skills,链到同一份' };
189
199
  }
@@ -0,0 +1,11 @@
1
+ export declare const TRAY_DOT: {
2
+ r: number;
3
+ cx: number;
4
+ };
5
+ /** 没在跑时 logo 的不透明度(#F3F3F3 上墨色对比度 3.3,#202020 上白色 5.8) */
6
+ export declare const TRAY_DIM = 0.55;
7
+ export declare const TRAY_WIN_SIZES: readonly [16, 20, 24, 32, 40, 48];
8
+ /** 文件名 → 内容:macOS 那几张 */
9
+ export declare function macTrayIcons(): Record<string, Buffer>;
10
+ /** 文件名 → 内容:Windows 那几张(<white|ink>-<ok|warn|bad>-<尺寸>.png、dot-<状态>.png) */
11
+ export declare function windowsTrayIcons(): Record<string, Buffer>;