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
@@ -5,6 +5,8 @@
5
5
  * 人设(display / avatar / voice)、政策、开关全在这里。
6
6
  */
7
7
  import { z } from 'zod';
8
+ import { DEFAULT_PORT } from "../shared/default-port.js";
9
+ export { DEFAULT_PORT };
8
10
  export const PolicySchema = z.object({
9
11
  /** 老师说给孩子听的每一句的字数上限;超出在句末截断(板书后按句算) */
10
12
  replyMaxChars: z.number().int().positive().describe('老师说给孩子听的每一句的字数上限(板书讲稿一行一句,按句截);超出在句末截断'),
@@ -24,7 +26,7 @@ export const PolicySchema = z.object({
24
26
  * 孩子的话,老师带不带工具(2026-10-04):填进运行时模板的 {tools}。ray 9 月的真实对话,用了工具的轮首拍就绪中位数 31.7 秒(24 轮),没用的 6.8 秒(37 轮);
25
27
  * 孩子最喜欢的是回得快的那位老师。off 时要用的东西由应用放进上下文包
26
28
  */
27
- tools: z.enum(['off', 'on']).describe('孩子说的话,老师带不带工具:off = 不带,只凭上下文包答,开口快(缺省);on = 带只读工具(查教材、读 vault,慢)。带照片的那条、记账,总是带只读工具(read_file / glob / grep_search)。模板里没有 {tools} / {toolArgs} 的运行时不受影响'),
29
+ tools: z.enum(['off', 'on']).describe('孩子说的话,老师带不带工具:off = 不带,只凭上下文包答,开口快(缺省);on = 带只读工具(查教材、读 vault,慢)。带照片的那条、记账,总是带只读工具(read_file / glob / grep_search)'),
28
30
  /** 板书开关:auto = 老师判断要不要出卡(缺省);off = 只说话不出卡 */
29
31
  board: z.enum(['auto', 'off']).describe('板书:auto = 讲题讲概念时老师出卡(缺省);off = 只说话不出卡'),
30
32
  /**
@@ -60,12 +62,23 @@ export const BOARD_CARD_KINDS = ['text', 'read', 'choice', 'image', 'tianzige',
60
62
  export const AGENT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
61
63
  /** 老师卡上的常驻入口:不经首页发布、一直在那位老师的卡上 */
62
64
  export const TUTOR_EXTRAS = ['dictation', 'talk'];
65
+ /**
66
+ * 老师卡上常驻的三块方砖(设计稿 https://claude.ai/artifact/NpPFoyvuF17dbRSEUuRJfW):先后就是牌上的先后。
67
+ * id 记在消息的 via.button;activity 是老师家里同名的文件(agents/<老师>/activities/<名>.md),孩子看到的字也是它。
68
+ * 家里有那个文件才出那一块(三门主科出厂都有);cotutor.json 的 tiles 只记关掉的,升级来的老 workspace 自动就有
69
+ */
70
+ export const HOME_TILES = [
71
+ { id: 'preview', activity: '预习' },
72
+ { id: 'check', activity: '检查作业' },
73
+ { id: 'review', activity: '复习' },
74
+ ];
75
+ export const HOME_TILE_IDS = HOME_TILES.map((t) => t.id);
63
76
  export const TutorSchema = z.object({
64
77
  /** 显示名(孩子端头像下的字、计划文件的 H2 标题) */
65
78
  display: z.string().min(1).describe('显示名:孩子端头像下的字,计划文件的 H2 标题'),
66
79
  subject: z.string().optional().describe('学科:与课程表的学科列、观察的 subject、vault 入口文件的 subject 属性对齐即归到这位老师'),
67
80
  /** 头像:一个 emoji 或图片相对路径 */
68
- avatar: z.string().optional().describe('头像:一个 emoji,或 workspace 根以内的图片相对路径(如 avatars/math-tutor.png;figshot pick --workspace 会写这个)'),
81
+ avatar: z.string().optional().describe('头像:一个 emoji,或 workspace 根以内的图片相对路径(如 avatars/math-tutor.png;figshot pick --workspace 会写这个);不写 = 语文、数学、英语用出厂头像,其余显示学科首字'),
69
82
  /** 学科色:头像的环、牌的边、新话题气泡(白字压在上面)、听写页的按钮;不写 = 自动(src/ui/shared/tutor-color.ts) */
70
83
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/, '要 #rrggbb').optional().describe('学科色 #rrggbb:头像的环、牌的边、新话题气泡(白字压在上面,要够深,doctor 会查)、听写页的按钮;不写 = 语文粉、数学蓝、英语绿,其余按先后自动分、不撞色'),
71
84
  /** 卡的配音音色 id(src/media/tts-voices.ts);没有 = 卡不配音 */
@@ -79,6 +92,8 @@ export const TutorSchema = z.object({
79
92
  /** 这位老师用哪个运行时(runtimes 里的键);不配用 runtimes.default。要更大预算与时限的老师配一个自己的 */
80
93
  runtime: z.string().optional().describe('这位老师用的运行时(runtimes 里的键;不配用 default)——预算、时限不同的老师配自己的'),
81
94
  extras: z.array(z.enum(TUTOR_EXTRAS)).optional().describe('老师卡上的常驻入口:dictation 听写(含拍照听写)、talk 口语课(要 talk.enabled);不写按学科给缺省:语文有听写,其余没有;[] = 都不要'),
95
+ /** 三块方砖(HOME_TILES)哪块关掉:只记 false;没写 = 开着(家里有那个 activity 文件才出) */
96
+ tiles: z.object(Object.fromEntries(HOME_TILE_IDS.map((id) => [id, z.boolean().optional()]))).strict().optional().describe('老师卡上常驻的方砖:preview 预习、check 检查作业、review 复习;写 false 关掉,没写 = 开着(老师家里有那个 activity 文件才出)'),
82
97
  });
83
98
  /** 三门主科的别名:学科写「英文」「English」也认作英语,颜色、听写用英文、语文缺省带听写都照它 */
84
99
  const SUBJECT_ALIASES = {
@@ -106,26 +121,23 @@ export function tutorExtras(t, talkOn = false) {
106
121
  const s = canonicalSubject(t.subject);
107
122
  return s === '语文' ? ['dictation'] : s === '英语' && talkOn ? ['talk'] : [];
108
123
  }
124
+ /** 这位老师开着的方砖(牌上的先后);家里有没有那个 activity 文件不在这里管 */
125
+ export function tutorTiles(t) {
126
+ return HOME_TILE_IDS.filter((id) => t.tiles?.[id] !== false);
127
+ }
109
128
  /**
110
- * 运行时:一个 CLI 一组 {run, resume} 命令模板。占位符:
111
- * {agent} 老师名 / {agentBody} 老师文件正文(给没有 --agent 的 CLI 塞系统提示)/ {prompt} 消息 / {session} 会话 id。
112
- * 板书写法的两种预载({boardFile} 板书技能 SKILL.md 的绝对路径,给能从文件追加系统提示的 CLI;{systemBody} 老师正文 + 板书写法,给只收一段系统提示文字的 CLI):
113
- * 模板里用了其中一个,应用就当板书写法已在系统提示里;都没用,应用在话题第一条消息里注入 <cotutor-board>(任何 CLI 都成立的退路)。
114
- * {effort} 这位老师的政策 effort(low / medium / high),qwen 由适配器写入思考量配置。
115
- * 带不带工具由适配器展开(src/brain/cli/):{tools} 填一串工具名,独占一个参数的 {toolArgs} 展开成几个参数。
116
- * 政策旋钮(预算、轮数、模型)写进模板,换 agent 只换运行时。
117
- * stdin: "stream-json":消息不进命令行,写进 stdin(`--input-format stream-json`),
118
- * 模板里就不写 {prompt};argv 与消息无关,应用把下一轮的进程提前起好等着。
129
+ * 旧的命令行运行时:一组 {run, resume} 命令模板(起 Qwen Code;更早是 Claude CLI)。2026-11-02 起不再支持,
130
+ * 留着这个形状只为旧 cotutor.json 读得进来:doctor 报出来,用到它的老师发不出去(src/brain/runtime.ts)。
119
131
  */
120
132
  export const CliRuntimeSchema = z.object({
121
- cli: z.string().min(1).optional().describe('这是哪个 CLI(src/brain/cli/ 里的适配器名:qwen);不写按 run 的第一个词认。套了壳脚本的运行时要写'),
133
+ cli: z.string().min(1).optional(),
122
134
  run: z.array(z.string()).min(1),
123
135
  resume: z.array(z.string()).min(1),
124
- stdin: z.literal('stream-json').optional().describe('消息写进 stdin(模板带 --input-format stream-json、不写 {prompt});这样的运行时,应用提前把下一轮的老师进程起好等着(预热)'),
125
- });
136
+ stdin: z.literal('stream-json').optional(),
137
+ }).describe('旧的命令行运行时(Qwen Code 等),2026-11-02 起不再支持:改成 {api, model}');
126
138
  /**
127
- * 内置 API 运行时(2026-10-29):应用自己直连模型(src/brain/api.ts),不起 CLI、不用装 Qwen Code。
128
- * 现在只有百炼(OpenAI 兼容的 Chat Completions,端点是业务空间 id 的域名);凭证是机器级的百炼配置(家长端「首页」里填的按量 key 与业务空间 id)。
139
+ * API 运行时(2026-10-29;2026-11-02 起只有它):应用自己直连模型(src/brain/api.ts)。
140
+ * 现在只有百炼(OpenAI 兼容的 Chat Completions);凭证是机器级的 ~/.config/cotutor/(家长端「首页」填的按量 key 与业务空间 id,开发机还有 Token Plan)。
129
141
  * 系统提示 = 老师正文 + 板书写法;同一话题的模型历史由应用存;effort 映射成思考开关与思考量;工具只有只读的三样。
130
142
  */
131
143
  export const ApiRuntimeSchema = z.object({
@@ -193,6 +205,14 @@ export const LiveSchema = z.object({
193
205
  });
194
206
  /** 这位老师的嘴用哪个音色 */
195
207
  export const mouthVoice = (live, tutor) => live.voices[tutor] ?? SHIPPED_MOUTHS[tutor] ?? MOUTH_VOICE;
208
+ /** 卡的配音(点读、单词卡、听写念词)的缺省:出厂三位各一个(2026-11-03 照 Ray 那里用着的),不写进 cotutor.json;别的老师没配就不配音 */
209
+ export const SHIPPED_CARD_VOICES = {
210
+ 'chinese-tutor': 'qwen-audio-3.0-tts-plus-longfengyutong',
211
+ 'math-tutor': 'qwen-audio-3.0-tts-plus-longyingsongliu',
212
+ 'english-tutor': 'qwen-audio-3.0-tts-plus-loongnorahu',
213
+ };
214
+ /** 这位老师卡的配音用哪个音色:家长挑的,没挑用出厂的;都没有 undefined(不配音) */
215
+ export const cardVoice = (tutors, tutor) => tutors[tutor]?.voice ?? SHIPPED_CARD_VOICES[tutor];
196
216
  /**
197
217
  * 听写:拍照认词表用哪个模型。挑词的要求在听写 activity 文件「认字收到什么」;
198
218
  * 下面两份是出厂那节的原文,workspace 里没有听写 activity 文件时照它。回什么格式(一行一段 JSON)由应用填(src/scenes/dictation/recognize.ts 的 FORMAT)。
@@ -256,21 +276,21 @@ export const CotutorConfigSchema = z
256
276
  grade: z.string().optional(),
257
277
  /** 孩子端用哪个主题(themes/<name>/);缺省 default(出厂主题,init 拷进 workspace) */
258
278
  theme: z.string().regex(AGENT_NAME_RE).default('default').describe('孩子端的主题:themes/<名字>/(theme.json + kid.css);cotutor add-theme <名字> 拷一份出厂的来改'),
259
- /** 孩子的惯用手:left = 左撇子,平板横屏实时老师页的左栏浮层挪到右边(写字的手空着,另一只手按住说话);缺省右手,不写进文件 */
260
- hand: z.enum(['right', 'left']).optional().describe('孩子的惯用手:left = 左撇子,平板横屏实时老师页的浮层(按住说话)挪到右边;缺省右手(浮层在左边)。工作台设置页能切'),
279
+ /** 按住说话那一栏放哪边:left = 平板横屏实时老师页的那一栏(与画板那列工具)挪到右边,写字的在左边;缺省 right(那一栏在左边),不写进文件 */
280
+ hand: z.enum(['right', 'left']).optional().describe('按住说话那一栏放哪边:left = 平板横屏实时老师页的那一栏挪到右边;缺省 right(在左边)。家长端设置页能切'),
261
281
  }),
262
282
  server: z
263
283
  .object({
264
- port: z.number().int().min(1).max(65535).default(5180),
284
+ port: z.number().int().min(1).max(65535).default(DEFAULT_PORT).describe('端口。cotutor init 选一次写在这里(443 优先);改了平板要重新扫码、重加主屏幕图标'),
265
285
  /** 只给这个 workspace 用的证书(相对 workspace 根),是例外;不配则用机器级 ~/.config/cotutor/certs/(cotutor cert 会建,所有 workspace 共用;iPad 上录音要 HTTPS) */
266
286
  https: z.object({ cert: z.string().min(1), key: z.string().min(1) }).optional(),
267
287
  })
268
- .default({ port: 5180 }),
288
+ .default({ port: DEFAULT_PORT }),
269
289
  paths: z.record(z.string(), z.string()).default({}).describe('角色 → 目录:vault 指 Obsidian vault 根;diary / plans / profile / timetable / textbooks / reference 相对 vault(缺省 日记 / 计划 / 孩子.md / 课程表.md / 教材 / 参考),不配 vault 就相对 workspace 根;profile 只是 init 新建档案的位置,老师按 cotutor: profile 属性找;captures(作业照片)相对 workspace 根'),
270
290
  vault: VaultPolicySchema.default({ keepScore: 4, autoBookkeep: false }).describe('vault 的写入政策:keepScore 话题打几星起才把摘要沉淀进日记(缺省 4);autoBookkeep 自动记账(缺省关)'),
271
291
  policyDefaults: PolicyPatchSchema.default({}).describe('所有老师的政策缺省;没写的用出厂缺省(60 字 / 板书 auto / 观察 10 条、计划 10 行、原文 4000 字)'),
272
292
  tutors: z.record(z.string().regex(AGENT_NAME_RE), TutorSchema).default({}).describe('老师表:键 = agents/<键>.md 的 frontmatter name;人设、开关、政策都在这里,老师文件里只有正文'),
273
- runtimes: RuntimesSchema.describe('运行时:default 指一个键;每个运行时是 {run, resume} 命令模板(起 CLI)或 {api, model}(应用直连模型),占位 {agent} {agentBody} {systemBody} {boardFile} {prompt} {session};模型、预算、时限写在这里'),
293
+ runtimes: RuntimesSchema.describe('运行时:default 指一个键;每个运行时是 {api, model}(应用直连百炼);老师用哪个模型写在这里'),
274
294
  figshot: z
275
295
  .object({ port: z.number().int().min(1).max(65535).default(8477) })
276
296
  .optional()
@@ -278,7 +298,7 @@ export const CotutorConfigSchema = z
278
298
  dictation: DictationSchema.default(DICTATION_DEFAULT).describe('听写(/dictation):拍照认词表用的模型(改了不用重起服务);挑词的要求在听写 activity 文件「认字收到什么」'),
279
299
  paint: PaintSchema.default(PAINT_DEFAULT).describe('你说我画:画图卡(paint)用的文生图模型、尺寸(改了不用重起服务);画风在 activity 文件「画家收到什么」'),
280
300
  talk: TalkSchema.default(TALK_DEFAULT).describe('口语课(/talk):孩子拍一张今天学的,改好口语单交给英语老师的实时页(先热身、再打电话);enabled 出厂 false'),
281
- live: LiveSchema.default(LIVE_DEFAULT).describe('实时老师:有脸的老师都由 realtime 模型开口、老师 CLI 在后面写板书(讲稿不配音、嘴照念);voices 挑嘴的音色;要百炼按量 key 与业务空间 id'),
301
+ live: LiveSchema.default(LIVE_DEFAULT).describe('实时老师:有脸的老师都由 realtime 模型开口、脑在后面写板书(讲稿不配音、嘴照念);voices 挑嘴的音色;要百炼按量 key 与业务空间 id'),
282
302
  })
283
303
  .superRefine((c, ctx) => {
284
304
  for (const name of Object.keys(c.live.voices)) {
@@ -289,20 +309,9 @@ export const CotutorConfigSchema = z
289
309
  ctx.addIssue({
290
310
  code: 'custom',
291
311
  path: ['runtimes', 'default'],
292
- message: `运行时 "${c.runtimes.default}" 不存在;runtimes 里要有同名的 {run, resume}(或 API 运行时 {api, model})`,
312
+ message: `运行时 "${c.runtimes.default}" 不存在;runtimes 里要有同名的 {api, model}`,
293
313
  });
294
314
  }
295
- for (const [name, r] of Object.entries(c.runtimes)) {
296
- if (typeof r === 'string' || isApiRuntime(r) || !r.stdin)
297
- continue;
298
- for (const key of ['run', 'resume']) {
299
- const argv = r[key];
300
- const at = argv.indexOf('--input-format');
301
- if (argv.some((a) => a.includes('{prompt}')) || at < 0 || argv[at + 1] !== 'stream-json') {
302
- ctx.addIssue({ code: 'custom', path: ['runtimes', name, key], message: `运行时 "${name}" 设了 stdin: "stream-json",${key} 模板要带 --input-format stream-json、不写 {prompt}` });
303
- }
304
- }
305
- }
306
315
  });
307
316
  /** 老师的有效政策 = POLICY_DEFAULTS ← policyDefaults ← tutors[name].policy */
308
317
  export function resolvePolicy(config, tutor) {
@@ -340,22 +349,11 @@ export function listTutors(config, opts = {}) {
340
349
  subject: t.subject,
341
350
  avatar: t.avatar,
342
351
  color: t.color,
343
- voice: t.voice,
352
+ voice: cardVoice(config.tutors, name),
344
353
  enabled: t.enabled,
345
354
  hidden: t.hidden,
346
355
  policy: resolvePolicy(config, name),
347
356
  extras: tutorExtras(t, config.talk.enabled),
357
+ tiles: tutorTiles(t),
348
358
  }));
349
359
  }
350
- /** 运行时模板填占位符;{tools} 填适配器给的工具名单(src/brain/cli/ 的 fillArgs 先算好,没给就填空);{agentBody} / {systemBody} / {boardFile} 只在给了值时替换,否则原样留着(doctor 会报);{effort} 没给用出厂缺省(课文件排版这类不认老师政策的调用) */
351
- export function fillRuntime(argv, vars) {
352
- return argv.map((a) => a
353
- .replaceAll('{agent}', vars.agent)
354
- .replaceAll('{prompt}', vars.prompt)
355
- .replaceAll('{session}', vars.session ?? '')
356
- .replaceAll('{agentBody}', vars.agentBody ?? '{agentBody}')
357
- .replaceAll('{systemBody}', vars.systemBody ?? '{systemBody}')
358
- .replaceAll('{boardFile}', vars.boardFile ?? '{boardFile}')
359
- .replaceAll('{effort}', vars.effort ?? POLICY_DEFAULTS.effort)
360
- .replaceAll('{tools}', vars.tools ?? ''));
361
- }
@@ -240,7 +240,10 @@ export declare const ConversationMessageSchema: z.ZodObject<{
240
240
  via: z.ZodOptional<z.ZodObject<{
241
241
  home: z.ZodNullable<z.ZodString>;
242
242
  button: z.ZodUnion<readonly [z.ZodNumber, z.ZodEnum<{
243
+ check: "check";
243
244
  recent: "recent";
245
+ preview: "preview";
246
+ review: "review";
244
247
  new: "new";
245
248
  }>]>;
246
249
  label: z.ZodString;
@@ -483,7 +486,10 @@ export declare const ConversationIndexSchema: z.ZodObject<{
483
486
  via: z.ZodOptional<z.ZodObject<{
484
487
  home: z.ZodNullable<z.ZodString>;
485
488
  button: z.ZodUnion<readonly [z.ZodNumber, z.ZodEnum<{
489
+ check: "check";
486
490
  recent: "recent";
491
+ preview: "preview";
492
+ review: "review";
487
493
  new: "new";
488
494
  }>]>;
489
495
  label: z.ZodString;
@@ -25,20 +25,30 @@ export declare const PublishedHomeSchema: z.ZodObject<{
25
25
  warnings: z.ZodDefault<z.ZodArray<z.ZodString>>;
26
26
  }, z.core.$strip>;
27
27
  export type PublishedHome = z.infer<typeof PublishedHomeSchema>;
28
- /** 孩子从首页哪个按钮进来的:home = 发布的 id(没发布过的缺省首页是 null),button = 老师卡上文件里的第几个按钮,或应用加的 new / recent */
28
+ /** 孩子从首页哪个按钮进来的:home = 发布的 id(没发布过的缺省首页是 null),button = 老师卡上文件里的第几个按钮,或应用加的 new / recent,或常驻的方砖(HOME_TILES 的 id) */
29
29
  export declare const HomeViaSchema: z.ZodObject<{
30
30
  home: z.ZodNullable<z.ZodString>;
31
31
  button: z.ZodUnion<readonly [z.ZodNumber, z.ZodEnum<{
32
+ check: "check";
32
33
  recent: "recent";
34
+ preview: "preview";
35
+ review: "review";
33
36
  new: "new";
34
37
  }>]>;
35
38
  }, z.core.$strip>;
39
+ /** 这一下发出去的字是按钮上的字、不是孩子说的(家长排的按钮、方砖);新话题、接着刚才的不算 */
40
+ export declare function viaSendsLabel(via: {
41
+ button: number | string;
42
+ }): boolean;
36
43
  export type HomeVia = z.infer<typeof HomeViaSchema>;
37
44
  /** 消息里记的 via:再带上按钮上的字(发布的首页以后会换,记下来 home show 才说得清) */
38
45
  export declare const MessageViaSchema: z.ZodObject<{
39
46
  home: z.ZodNullable<z.ZodString>;
40
47
  button: z.ZodUnion<readonly [z.ZodNumber, z.ZodEnum<{
48
+ check: "check";
41
49
  recent: "recent";
50
+ preview: "preview";
51
+ review: "review";
42
52
  new: "new";
43
53
  }>]>;
44
54
  label: z.ZodString;
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { z } from 'zod';
6
6
  import { BoardCardSchema } from "./board.js";
7
+ import { HOME_TILE_IDS } from "./config.js";
7
8
  const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
8
9
  /** 发布的 id:发布时刻 YYYY-MM-DD-HHMM,同一分钟再发加 -2、-3 */
9
10
  export const HOME_ID_RE = /^\d{4}-\d{2}-\d{2}-\d{4}(?:-\d+)?$/;
@@ -21,10 +22,14 @@ export const PublishedHomeSchema = z.object({
21
22
  /** 发布时还在的提醒(家长端「首页」页显示) */
22
23
  warnings: z.array(z.string()).default([]),
23
24
  });
24
- /** 孩子从首页哪个按钮进来的:home = 发布的 id(没发布过的缺省首页是 null),button = 老师卡上文件里的第几个按钮,或应用加的 new / recent */
25
+ /** 孩子从首页哪个按钮进来的:home = 发布的 id(没发布过的缺省首页是 null),button = 老师卡上文件里的第几个按钮,或应用加的 new / recent,或常驻的方砖(HOME_TILES 的 id) */
25
26
  export const HomeViaSchema = z.object({
26
27
  home: z.string().regex(HOME_ID_RE).nullable(),
27
- button: z.union([z.number().int().nonnegative(), z.enum(['new', 'recent'])]),
28
+ button: z.union([z.number().int().nonnegative(), z.enum(['new', 'recent', ...HOME_TILE_IDS])]),
28
29
  });
30
+ /** 这一下发出去的字是按钮上的字、不是孩子说的(家长排的按钮、方砖);新话题、接着刚才的不算 */
31
+ export function viaSendsLabel(via) {
32
+ return typeof via.button === 'number' || HOME_TILE_IDS.includes(via.button);
33
+ }
29
34
  /** 消息里记的 via:再带上按钮上的字(发布的首页以后会换,记下来 home show 才说得清) */
30
35
  export const MessageViaSchema = HomeViaSchema.extend({ label: z.string() });
@@ -2,6 +2,7 @@
2
2
  * 一个孩子的 workspace 解析好的样子(app/workspace.ts 的 loadWorkspace 给;从那边挪来,2026-10-29):根、从哪认出来的、cotutor.json、各角色的绝对路径、几个目录。
3
3
  * 类型放在 schema:运行层、记忆、场景都按它取自己要的那几样,不用去引命令行那一层。
4
4
  */
5
+ import { z } from 'zod';
5
6
  import type { CotutorConfig, PathRole } from './config.ts';
6
7
  /**
7
8
  * 技能目录,相对 workspace 根(2026-10-30 从 .claude/skills 挪来):Codex、Qwen Code 在 workspace 里对话时直接读它;
@@ -12,10 +13,36 @@ export declare const SKILLS_DIR = ".agents/skills";
12
13
  export declare function tutorFilePath(agentsDir: string, name: string): string;
13
14
  /** workspace 是怎么找到的:--workspace / COTUTOR_WORKSPACE / cwd 祖先 / ~/.config/cotutor/config.json / ~/cotutor/ 唯一目录 */
14
15
  export type RootSource = 'flag' | 'env' | 'cwd' | 'user-config' | 'home-single';
16
+ /**
17
+ * sandbox(2026-11-04):手动试功能用的 workspace,从真的那个分出来;对话、照片、vault 的写入都留在它里面,不进孩子的记录。
18
+ * 命令加 --sandbox 就换到它(src/app/cli/sandbox.ts),下游照常当一个 workspace 用。
19
+ * 位置:真 workspace 旁边的 .sandbox/<目录名>/(~/cotutor/ray → ~/cotutor/.sandbox/ray);点开头,「~/cotutor 下唯一目录」的兜底看不见它。
20
+ * 分的时候根下照拷,只跳过孩子的状态(SANDBOX_SKIP;.cotutor/ 只拷 SANDBOX_KEEP_MACHINE);vault 在 workspace 外面就整份拷到 sandbox 的 vault/。
21
+ * cotutor.json 改几处:标题前加「试用 · 」(孩子端、家长端、扫码页都看得出)、端口 +1(和真的并排开)、paths 指进 sandbox、自动记账关掉(试出来的话题不值得花钱记)。
22
+ * 标记 .cotutor/sandbox.json 最后写:有它才算分完;--fresh 只删带这个标记的目录。
23
+ */
24
+ export declare const SANDBOX_DIR = ".sandbox";
25
+ export declare const SANDBOX_MARK = ".cotutor/sandbox.json";
26
+ /** 标题前缀:「试用 · Ray的老师们」 */
27
+ export declare const SANDBOX_TITLE = "\u8BD5\u7528";
28
+ /** 外面的 vault 拷进 sandbox 后的目录名(相对 sandbox 根) */
29
+ export declare const SANDBOX_VAULT = "vault";
30
+ /** workspace 根下是孩子的状态、不拷进 sandbox 的;其余都算设置,照拷(拷多了无妨,拷少了试的就不像真的) */
31
+ export declare const SANDBOX_SKIP: readonly string[];
32
+ /** .cotutor/ 里照拷的;其余是缓存、设备表、孩子端流水 */
33
+ export declare const SANDBOX_KEEP_MACHINE: readonly string[];
34
+ export declare const SandboxMarkSchema: z.ZodObject<{
35
+ from: z.ZodString;
36
+ vault: z.ZodString;
37
+ forkedAt: z.ZodString;
38
+ }, z.core.$strip>;
39
+ export type SandboxMark = z.infer<typeof SandboxMarkSchema>;
15
40
  export interface Workspace {
16
41
  root: string;
17
42
  source: RootSource;
18
43
  config: CotutorConfig;
44
+ /** 这是个 sandbox(根下有 .cotutor/sandbox.json)就有,记着从哪分出来的 */
45
+ sandbox?: SandboxMark;
19
46
  /** 角色 → 绝对路径;vault 侧角色相对 vault 解析,没配 vault 就相对 workspace 根;captures 相对 workspace 根 */
20
47
  paths: Record<PathRole, string> & Record<string, string>;
21
48
  dirs: {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * 一个孩子的 workspace 解析好的样子(app/workspace.ts 的 loadWorkspace 给;从那边挪来,2026-10-29):根、从哪认出来的、cotutor.json、各角色的绝对路径、几个目录。
3
+ * 类型放在 schema:运行层、记忆、场景都按它取自己要的那几样,不用去引命令行那一层。
4
+ */
5
+ import { z } from 'zod';
1
6
  /**
2
7
  * 技能目录,相对 workspace 根(2026-10-30 从 .claude/skills 挪来):Codex、Qwen Code 在 workspace 里对话时直接读它;
3
8
  * Claude Code 只认 .claude/skills,那里是一条指过来的相对链(init / upgrade 建)
@@ -7,3 +12,26 @@ export const SKILLS_DIR = '.agents/skills';
7
12
  export function tutorFilePath(agentsDir, name) {
8
13
  return `${agentsDir}/${name}.md`;
9
14
  }
15
+ /**
16
+ * sandbox(2026-11-04):手动试功能用的 workspace,从真的那个分出来;对话、照片、vault 的写入都留在它里面,不进孩子的记录。
17
+ * 命令加 --sandbox 就换到它(src/app/cli/sandbox.ts),下游照常当一个 workspace 用。
18
+ * 位置:真 workspace 旁边的 .sandbox/<目录名>/(~/cotutor/ray → ~/cotutor/.sandbox/ray);点开头,「~/cotutor 下唯一目录」的兜底看不见它。
19
+ * 分的时候根下照拷,只跳过孩子的状态(SANDBOX_SKIP;.cotutor/ 只拷 SANDBOX_KEEP_MACHINE);vault 在 workspace 外面就整份拷到 sandbox 的 vault/。
20
+ * cotutor.json 改几处:标题前加「试用 · 」(孩子端、家长端、扫码页都看得出)、端口 +1(和真的并排开)、paths 指进 sandbox、自动记账关掉(试出来的话题不值得花钱记)。
21
+ * 标记 .cotutor/sandbox.json 最后写:有它才算分完;--fresh 只删带这个标记的目录。
22
+ */
23
+ export const SANDBOX_DIR = '.sandbox';
24
+ export const SANDBOX_MARK = '.cotutor/sandbox.json';
25
+ /** 标题前缀:「试用 · Ray的老师们」 */
26
+ export const SANDBOX_TITLE = '试用';
27
+ /** 外面的 vault 拷进 sandbox 后的目录名(相对 sandbox 根) */
28
+ export const SANDBOX_VAULT = 'vault';
29
+ /** workspace 根下是孩子的状态、不拷进 sandbox 的;其余都算设置,照拷(拷多了无妨,拷少了试的就不像真的) */
30
+ export const SANDBOX_SKIP = ['conversations', 'ledger', 'captures', 'talk', 'dictation', 'evals', 'snaps', '.cotutor', '.git', 'node_modules', '.DS_Store'];
31
+ /** .cotutor/ 里照拷的;其余是缓存、设备表、孩子端流水 */
32
+ export const SANDBOX_KEEP_MACHINE = ['shipped.json', 'cotutor.schema.json'];
33
+ export const SandboxMarkSchema = z.object({
34
+ from: z.string().min(1).describe('从哪个 workspace 分出来的(绝对路径)'),
35
+ vault: z.string().min(1).describe('分的时候拷的是哪个 vault(绝对路径)'),
36
+ forkedAt: z.string().min(1).describe('什么时候分的(ISO)'),
37
+ });
@@ -0,0 +1,2 @@
1
+ /** cotutor.json 没写端口时用的,只给旧配置:改了它们的地址就变了,平板要重新扫码。新 workspace 的端口 init 选好写进文件(src/app/cli/port.ts) */
2
+ export declare const DEFAULT_PORT = 5180;
@@ -0,0 +1,2 @@
1
+ /** cotutor.json 没写端口时用的,只给旧配置:改了它们的地址就变了,平板要重新扫码。新 workspace 的端口 init 选好写进文件(src/app/cli/port.ts) */
2
+ export const DEFAULT_PORT = 5180;
@@ -1,6 +1,5 @@
1
1
  export declare const TOKEN_PLAN_HOST = "token-plan.maas.qianwenaiapi.com";
2
2
  export declare const DASHSCOPE_HOST = "dashscope.aliyuncs.com";
3
- export declare const TOKEN_PLAN_KEY_ENV = "QWENCLOUD_TOKEN_PLAN_API_KEY";
4
3
  /**
5
4
  * Token Plan 个人版调得通的模型(2026-10-13:文档表、token-plan 的 /models、实测三处对过)。
6
5
  * 没有 omni 系列、qwen-image-2.0、qwen-plus
@@ -13,24 +12,20 @@ export interface QianwenKeys {
13
12
  /** 百炼按量的 key */
14
13
  payg: string | null;
15
14
  }
16
- /** 本机 qwen 配好的百炼 key(~/.qwen/settings.json 的 env);读不到就是 null */
17
- export declare function localDashscopeKey(home?: string): string | null;
18
- /** voxtell 的配置(~/.config/voxtell/config.json):按量 key 与业务空间 id */
19
- export declare function voxtellConfig(home?: string): {
20
- apiKey: string | null;
21
- workspaceId: string | null;
22
- };
23
15
  /** 家长端配的百炼(机器级,几个孩子的 workspace 共用;写在 src/app/machine/bailian.ts):按量 key 与业务空间 id */
24
16
  export declare const bailianFile: (home?: string) => string;
25
17
  export declare function cotutorBailian(home?: string): {
26
18
  apiKey: string | null;
27
19
  workspaceId: string | null;
28
20
  };
29
- /** 百炼的业务空间 id(按量的 realtime、配音走它的域名):先看家长端配的,再看环境,再看 voxtell 的配置 */
21
+ /** 开发机的 Token Plan:{"apiKey": "sk-sp-…"},手写;家长端存 bailian.json 是整份重写,所以不和它放一起 */
22
+ export declare const tokenPlanFile: (home?: string) => string;
23
+ export declare const tokenPlanKey: (home?: string) => string | null;
24
+ /** 百炼的业务空间 id(按量的 realtime、配音走它的域名) */
30
25
  export declare function bailianWorkspaceId(env?: NodeJS.ProcessEnv): string | null;
31
26
  /** 业务空间的域名(北京地域);公网域名连 realtime 1 秒就断、配音不认 */
32
27
  export declare const workspaceHost: (workspaceId: string) => string;
33
- /** 本机的两把 key:先看家长端配的,再看环境,再看 qwen、voxtell 的配置;哪处放错了套(按量的位置放了 `sk-sp-`)就挪到对的那格 */
28
+ /** 本机的两把 key(env 只取 HOME,测试拿它指别的家);放错了套(bailian.json 里放了 `sk-sp-`)就挪到对的那格 */
34
29
  export declare function qianwenKeys(env?: NodeJS.ProcessEnv): QianwenKeys;
35
30
  export interface QianwenLine {
36
31
  /** 域名(不带协议):https 与 wss 都是它 */
@@ -44,6 +39,15 @@ export declare function qianwenLine(model: string, keys: QianwenKeys): QianwenLi
44
39
  /** 几种接口的地址 */
45
40
  export declare const chatUrl: (host: string) => string;
46
41
  export declare const compatibleBase: (host: string) => string;
42
+ /**
43
+ * 兼容接口(老师的脑,src/brain/api.ts)这个模型发到哪:Token Plan 有它、本机有 token-plan.json 就走 Token Plan(2026-11-02 起,同配音、画图);
44
+ * 否则百炼按量,知道业务空间 id 走它的域名,不知道走公网域名
45
+ */
46
+ export declare function compatibleLine(model: string, env?: NodeJS.ProcessEnv): {
47
+ baseURL: string;
48
+ key: string;
49
+ plan: boolean;
50
+ } | null;
47
51
  export declare const imageUrl: (host: string) => string;
48
52
  export declare const ttsUrl: (host: string) => string;
49
53
  /** 兼容接口的流(data: {...} 一行一块):拼出 delta.content;流里报错 → null */
@@ -2,16 +2,15 @@
2
2
  * 千问的两套账:千问 AI 平台的 Token Plan(`sk-sp-`,只认 token-plan 域名,按订阅 Credits 抵)
3
3
  * 与百炼按量(`sk-` / `sk-ws-`,dashscope 域名)。两把 key 互不通用。
4
4
  * 一条线调哪个模型:Token Plan 有这个模型、本机又有它的 key 就走 Token Plan,否则走按量(2026-10-13 定)。key 只进请求头与子进程环境。
5
- * - Token Plan 的 key:环境变量 QWENCLOUD_TOKEN_PLAN_API_KEY(Qwen Code 官方页用的名字);DASHSCOPE_API_KEY 里放的是 `sk-sp-` 也认作它。
6
- * - 按量的 key:先看家长在家长端配的(~/.config/cotutor/bailian.json),再看 DASHSCOPE_API_KEY(不是 `sk-sp-` 时),
7
- * 再看本机 qwen(~/.qwen/settings.json 的 env)、voxtell(~/.config/voxtell/config.json)的配置。
5
+ * key 只放机器级的 ~/.config/cotutor/(2026-11-02 起,所有命令读同一处;环境变量、本机 qwen、voxtell 的配置都不看):
6
+ * - bailian.json:百炼按量 key + 业务空间 id,家长端「首页」写(src/app/machine/bailian.ts),每台都要有;
7
+ * - token-plan.json:Token Plan 的 key,只有开发机有,手写;文件在就是开关。
8
8
  */
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { homedir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  export const TOKEN_PLAN_HOST = 'token-plan.maas.qianwenaiapi.com';
13
13
  export const DASHSCOPE_HOST = 'dashscope.aliyuncs.com';
14
- export const TOKEN_PLAN_KEY_ENV = 'QWENCLOUD_TOKEN_PLAN_API_KEY';
15
14
  /**
16
15
  * Token Plan 个人版调得通的模型(2026-10-13:文档表、token-plan 的 /models、实测三处对过)。
17
16
  * 没有 omni 系列、qwen-image-2.0、qwen-plus
@@ -40,37 +39,29 @@ function readJson(file) {
40
39
  }
41
40
  }
42
41
  const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
43
- /** 本机 qwen 配好的百炼 key(~/.qwen/settings.json 的 env);读不到就是 null */
44
- export function localDashscopeKey(home = homedir()) {
45
- const env = readJson(join(home, '.qwen', 'settings.json'))?.env;
46
- return env && typeof env === 'object' ? text(env.DASHSCOPE_API_KEY) : null;
47
- }
48
- /** voxtell 的配置(~/.config/voxtell/config.json):按量 key 与业务空间 id */
49
- export function voxtellConfig(home = homedir()) {
50
- const v = readJson(join(home, '.config', 'voxtell', 'config.json'));
51
- return { apiKey: text(v?.apiKey), workspaceId: text(v?.workspaceId) };
52
- }
53
42
  /** 家长端配的百炼(机器级,几个孩子的 workspace 共用;写在 src/app/machine/bailian.ts):按量 key 与业务空间 id */
54
43
  export const bailianFile = (home = homedir()) => join(home, '.config', 'cotutor', 'bailian.json');
55
44
  export function cotutorBailian(home = homedir()) {
56
45
  const v = readJson(bailianFile(home));
57
46
  return { apiKey: text(v?.apiKey), workspaceId: text(v?.workspaceId) };
58
47
  }
59
- /** 百炼的业务空间 id(按量的 realtime、配音走它的域名):先看家长端配的,再看环境,再看 voxtell 的配置 */
48
+ /** 开发机的 Token Plan:{"apiKey": "sk-sp-…"},手写;家长端存 bailian.json 是整份重写,所以不和它放一起 */
49
+ export const tokenPlanFile = (home = homedir()) => join(home, '.config', 'cotutor', 'token-plan.json');
50
+ export const tokenPlanKey = (home = homedir()) => text(readJson(tokenPlanFile(home))?.apiKey);
51
+ /** 百炼的业务空间 id(按量的 realtime、配音走它的域名) */
60
52
  export function bailianWorkspaceId(env = process.env) {
61
- const home = env.HOME || homedir();
62
- return cotutorBailian(home).workspaceId || text(env.DASHSCOPE_WORKSPACE_ID) || voxtellConfig(home).workspaceId;
53
+ return cotutorBailian(env.HOME || homedir()).workspaceId;
63
54
  }
64
55
  /** 业务空间的域名(北京地域);公网域名连 realtime 1 秒就断、配音不认 */
65
56
  export const workspaceHost = (workspaceId) => `${workspaceId}.cn-beijing.maas.aliyuncs.com`;
66
- /** 本机的两把 key:先看家长端配的,再看环境,再看 qwen、voxtell 的配置;哪处放错了套(按量的位置放了 `sk-sp-`)就挪到对的那格 */
57
+ /** 本机的两把 key(env 只取 HOME,测试拿它指别的家);放错了套(bailian.json 里放了 `sk-sp-`)就挪到对的那格 */
67
58
  export function qianwenKeys(env = process.env) {
68
59
  const home = env.HOME || homedir();
69
- const named = text(env[TOKEN_PLAN_KEY_ENV]);
70
- const candidates = [cotutorBailian(home).apiKey, text(env.DASHSCOPE_API_KEY), localDashscopeKey(home), voxtellConfig(home).apiKey];
60
+ const plan = tokenPlanKey(home);
61
+ const payg = cotutorBailian(home).apiKey;
71
62
  return {
72
- plan: named ?? candidates.find((k) => k && isPlanKey(k)) ?? null,
73
- payg: candidates.find((k) => k && !isPlanKey(k)) ?? null,
63
+ plan: plan && isPlanKey(plan) ? plan : payg && isPlanKey(payg) ? payg : null,
64
+ payg: payg && !isPlanKey(payg) ? payg : null,
74
65
  };
75
66
  }
76
67
  /** 这个模型走哪条线;两把 key 都用不上 → null */
@@ -84,6 +75,19 @@ export function qianwenLine(model, keys) {
84
75
  /** 几种接口的地址 */
85
76
  export const chatUrl = (host) => `https://${host}/compatible-mode/v1/chat/completions`;
86
77
  export const compatibleBase = (host) => `https://${host}/compatible-mode/v1`;
78
+ /**
79
+ * 兼容接口(老师的脑,src/brain/api.ts)这个模型发到哪:Token Plan 有它、本机有 token-plan.json 就走 Token Plan(2026-11-02 起,同配音、画图);
80
+ * 否则百炼按量,知道业务空间 id 走它的域名,不知道走公网域名
81
+ */
82
+ export function compatibleLine(model, env = process.env) {
83
+ const line = qianwenLine(model, qianwenKeys(env));
84
+ if (!line)
85
+ return null;
86
+ if (line.plan)
87
+ return { baseURL: compatibleBase(line.host), key: line.key, plan: true };
88
+ const ws = bailianWorkspaceId(env);
89
+ return { baseURL: compatibleBase(ws ? workspaceHost(ws) : line.host), key: line.key, plan: false };
90
+ }
87
91
  export const imageUrl = (host) => `https://${host}/api/v1/services/aigc/multimodal-generation/generation`;
88
92
  export const ttsUrl = (host) => `https://${host}/api/v1/services/audio/tts/SpeechSynthesizer`;
89
93
  /** 兼容接口的流(data: {...} 一行一块):拼出 delta.content;流里报错 → null */
@@ -0,0 +1,2 @@
1
+ /** 地址:默认端口不写(https 的 443、http 的 80)。不用 new URL:它会把主机名压成小写 */
2
+ export declare function siteUrl(scheme: 'http' | 'https', host: string, port: number, path?: string): string;
@@ -0,0 +1,5 @@
1
+ /** 地址:默认端口不写(https 的 443、http 的 80)。不用 new URL:它会把主机名压成小写 */
2
+ export function siteUrl(scheme, host, port, path = '/') {
3
+ const isDefault = (scheme === 'https' && port === 443) || (scheme === 'http' && port === 80);
4
+ return `${scheme}://${host}${isDefault ? '' : `:${port}`}${path}`;
5
+ }
@@ -0,0 +1,9 @@
1
+ /** 版本比较与升级步骤:服务、托盘(轻入口,不带 zod)、家长端、doctor 共用 */
2
+ /** 升级程序的三步。分开写不用 &&:Windows 默认的 PowerShell 5.1 不认 &&;顺序是先换文件再重起,新代码不会读到旧文件;restart 顺手把图标也换上新代码 */
3
+ export declare const UPGRADE_STEPS: readonly ["npm i -g cotutor", "cotutor upgrade", "cotutor service restart"];
4
+ /** 给家长看的那份。Windows 上带 .cmd:PowerShell 缺省禁止跑脚本,不带后缀找到的是被拦的 .ps1,第一条就报错;.cmd 在 PowerShell 和 cmd.exe 里都能跑(2026-11-07 真机) */
5
+ export declare function upgradeSteps(platform?: NodeJS.Platform): string[];
6
+ /** 正式版的版本号。预发布不算;从网上拿来的还要显示给家长,其余一律不认 */
7
+ export declare function isPlainVersion(v: unknown): v is string;
8
+ /** a 比 b 新;任一边不是正式版的版本号 → false,宁可不提醒 */
9
+ export declare function isNewer(a: unknown, b: unknown): boolean;
@@ -0,0 +1,23 @@
1
+ /** 版本比较与升级步骤:服务、托盘(轻入口,不带 zod)、家长端、doctor 共用 */
2
+ /** 升级程序的三步。分开写不用 &&:Windows 默认的 PowerShell 5.1 不认 &&;顺序是先换文件再重起,新代码不会读到旧文件;restart 顺手把图标也换上新代码 */
3
+ export const UPGRADE_STEPS = ['npm i -g cotutor', 'cotutor upgrade', 'cotutor service restart'];
4
+ /** 给家长看的那份。Windows 上带 .cmd:PowerShell 缺省禁止跑脚本,不带后缀找到的是被拦的 .ps1,第一条就报错;.cmd 在 PowerShell 和 cmd.exe 里都能跑(2026-11-07 真机) */
5
+ export function upgradeSteps(platform = process.platform) {
6
+ return UPGRADE_STEPS.map((s) => (platform === 'win32' ? s.replace(' ', '.cmd ') : s));
7
+ }
8
+ const PLAIN = /^(\d+)\.(\d+)\.(\d+)$/;
9
+ /** 正式版的版本号。预发布不算;从网上拿来的还要显示给家长,其余一律不认 */
10
+ export function isPlainVersion(v) {
11
+ return typeof v === 'string' && v.length <= 32 && PLAIN.test(v);
12
+ }
13
+ /** a 比 b 新;任一边不是正式版的版本号 → false,宁可不提醒 */
14
+ export function isNewer(a, b) {
15
+ if (!isPlainVersion(a) || !isPlainVersion(b))
16
+ return false;
17
+ const x = a.split('.').map(Number);
18
+ const y = b.split('.').map(Number);
19
+ for (let i = 0; i < 3; i++)
20
+ if (x[i] !== y[i])
21
+ return x[i] > y[i];
22
+ return false;
23
+ }