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.
- package/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +36 -13
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +13 -3
- package/cards/photo/card.css +10 -3
- package/cards/photo/card.md +2 -2
- package/cards/tutor/card.css +13 -0
- package/cards/tutor/card.md +2 -1
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
- package/dist/app/activity-eval.js +3 -2
- package/dist/app/cli/activities.d.ts +8 -1
- package/dist/app/cli/activities.js +42 -4
- package/dist/app/cli/cert.d.ts +5 -0
- package/dist/app/cli/cert.js +20 -7
- package/dist/app/cli/chalk/browser.d.ts +2 -2
- package/dist/app/cli/chalk/browser.js +22 -7
- package/dist/app/cli/chalk/main.js +3 -2
- package/dist/app/cli/doctor.d.ts +6 -5
- package/dist/app/cli/doctor.js +107 -171
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/init.js +8 -5
- package/dist/app/cli/main.js +347 -24
- package/dist/app/cli/migrate.d.ts +2 -11
- package/dist/app/cli/migrate.js +1 -59
- package/dist/app/cli/port-holder.d.ts +16 -0
- package/dist/app/cli/port-holder.js +34 -0
- package/dist/app/cli/port.d.ts +37 -0
- package/dist/app/cli/port.js +116 -0
- package/dist/app/cli/sandbox.d.ts +25 -0
- package/dist/app/cli/sandbox.js +166 -0
- package/dist/app/cli/serve-log.d.ts +13 -0
- package/dist/app/cli/serve-log.js +90 -0
- package/dist/app/cli/serve.d.ts +18 -4
- package/dist/app/cli/serve.js +78 -8
- package/dist/app/cli/service-windows.d.ts +50 -0
- package/dist/app/cli/service-windows.js +117 -0
- package/dist/app/cli/service.d.ts +138 -0
- package/dist/app/cli/service.js +429 -0
- package/dist/app/cli/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +13 -13
- package/dist/app/cli/skills.d.ts +5 -1
- package/dist/app/cli/skills.js +14 -4
- package/dist/app/cli/tray-icons.d.ts +11 -0
- package/dist/app/cli/tray-icons.js +87 -0
- package/dist/app/cli/tray-ids.d.ts +6 -0
- package/dist/app/cli/tray-ids.js +6 -0
- package/dist/app/cli/tray-mac.d.ts +8 -0
- package/dist/app/cli/tray-mac.js +140 -0
- package/dist/app/cli/tray-run.d.ts +1 -0
- package/dist/app/cli/tray-run.js +33 -0
- package/dist/app/cli/tray-windows.d.ts +5 -0
- package/dist/app/cli/tray-windows.js +166 -0
- package/dist/app/cli/tray.d.ts +205 -0
- package/dist/app/cli/tray.js +618 -0
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +11 -6
- package/dist/app/dictation/route.js +3 -2
- package/dist/app/http/app.d.ts +5 -10
- package/dist/app/http/app.js +45 -98
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-tutor-files.js +1 -1
- package/dist/app/http/parent-home.d.ts +14 -0
- package/dist/app/http/parent-home.js +69 -6
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/raw-view.js +3 -3
- package/dist/app/http/voice-preview.d.ts +4 -0
- package/dist/app/http/voice-preview.js +34 -0
- package/dist/app/machine/bailian.d.ts +0 -2
- package/dist/app/machine/bailian.js +5 -6
- package/dist/app/mock/mock-live.js +1 -1
- package/dist/app/mock/mock.d.ts +4 -2
- package/dist/app/mock/mock.js +20 -12
- package/dist/app/replay.js +4 -4
- package/dist/app/skills/home-doc.js +1 -1
- package/dist/app/workspace.d.ts +9 -4
- package/dist/app/workspace.js +33 -10
- package/dist/app/ws/route.d.ts +8 -2
- package/dist/app/ws/route.js +7 -3
- package/dist/board/parse.d.ts +0 -2
- package/dist/board/parse.js +0 -6
- package/dist/board/presentation.d.ts +9 -15
- package/dist/board/presentation.js +8 -10
- package/dist/brain/api-tools.js +2 -1
- package/dist/brain/api.d.ts +12 -17
- package/dist/brain/api.js +16 -33
- package/dist/brain/index.d.ts +23 -71
- package/dist/brain/index.js +6 -69
- package/dist/brain/log.d.ts +5 -0
- package/dist/brain/log.js +22 -0
- package/dist/brain/runtime.d.ts +30 -0
- package/dist/brain/runtime.js +23 -0
- package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
- package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
- package/dist/brain/stream.d.ts +5 -5
- package/dist/brain/stream.js +7 -7
- package/dist/brain/transcript.d.ts +3 -3
- package/dist/brain/transcript.js +6 -6
- package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
- package/dist/brain/types.js +14 -0
- package/dist/chalk/glyphs/skeleton.d.ts +3 -3
- package/dist/chalk/glyphs/skeleton.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/memory/diary.js +3 -3
- package/dist/realtime/bailian.d.ts +1 -1
- package/dist/realtime/bailian.js +3 -2
- package/dist/realtime/preview.js +1 -1
- package/dist/realtime/voices.d.ts +2 -0
- package/dist/realtime/voices.js +57 -57
- package/dist/runtime/activities.d.ts +13 -1
- package/dist/runtime/activities.js +28 -2
- package/dist/runtime/activity-state.js +2 -2
- package/dist/runtime/agent-file.d.ts +3 -3
- package/dist/runtime/agent-file.js +3 -3
- package/dist/runtime/context/tutor-rules.d.ts +2 -2
- package/dist/runtime/context/tutor-rules.js +3 -3
- package/dist/runtime/home/home.d.ts +12 -1
- package/dist/runtime/home/home.js +8 -1
- package/dist/runtime/home/store.d.ts +1 -1
- package/dist/runtime/home/store.js +36 -21
- package/dist/runtime/journal/mix.d.ts +15 -6
- package/dist/runtime/journal/mix.js +60 -19
- package/dist/runtime/journal/recorder.d.ts +58 -13
- package/dist/runtime/journal/recorder.js +106 -16
- package/dist/runtime/journal/store.d.ts +4 -4
- package/dist/runtime/journal/store.js +6 -6
- package/dist/runtime/kid-input.js +2 -3
- package/dist/runtime/live/call.d.ts +12 -0
- package/dist/runtime/live/call.js +51 -18
- package/dist/runtime/live/turns.js +1 -1
- package/dist/runtime/runner.d.ts +9 -55
- package/dist/runtime/runner.js +52 -173
- package/dist/scenes/paint/painter.js +4 -3
- package/dist/schema/config.d.ts +43 -22
- package/dist/schema/config.js +45 -47
- package/dist/schema/conversation.d.ts +6 -0
- package/dist/schema/home.d.ts +11 -1
- package/dist/schema/home.js +7 -2
- package/dist/schema/workspace.d.ts +27 -0
- package/dist/schema/workspace.js +28 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/qianwen.d.ts +14 -10
- package/dist/shared/qianwen.js +26 -22
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/ui/dev/dev-page.js +25 -252
- package/dist/ui/dev/dev-theme.d.ts +1 -1
- package/dist/ui/dev/dev-theme.js +1 -1
- package/dist/ui/dev/voice-gallery-page.js +1 -1
- package/dist/ui/kid/board-mark.d.ts +2 -2
- package/dist/ui/kid/board-mark.js +2 -2
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-page.js +68 -103
- package/dist/ui/kid/voice-ui.d.ts +1 -1
- package/dist/ui/kid/voice-ui.js +1 -1
- package/dist/ui/kid/word-emoji.js +5 -3
- package/dist/ui/parent/parent-home-page.js +315 -7
- package/dist/ui/parent/parent-page.js +6 -2
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
- package/dist/ui/{dev → parent}/qr-page.js +19 -14
- package/dist/ui/shared/brand.d.ts +3 -1
- package/dist/ui/shared/brand.js +3 -1
- package/grades/grade-2/README.md +1 -1
- package/grades/grade-2/VALIDATION.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
- 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
- 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
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
- 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
- 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
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
- 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
- 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
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/package.json +2 -1
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
- package/skills/cotutor-board/references/photo.md +2 -2
- package/skills/cotutor-home/SKILL.md +1 -0
- package/skills/cotutor-home/references/tutor.md +2 -1
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
- package/skills/cotutor-tutor/SKILL.md +5 -13
- package/dist/brain/cli/adapters.d.ts +0 -60
- package/dist/brain/cli/adapters.js +0 -91
- package/dist/brain/cli/qwen.d.ts +0 -11
- package/dist/brain/cli/qwen.js +0 -141
- package/dist/brain/cli/run-plan.d.ts +0 -59
- package/dist/brain/cli/run-plan.js +0 -71
- package/dist/brain/cli/run.d.ts +0 -35
- package/dist/brain/cli/run.js +0 -256
- package/dist/brain/cli/types.js +0 -3
- package/dist/brain/cli/warm.d.ts +0 -53
- package/dist/brain/cli/warm.js +0 -103
package/dist/app/cli/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cotutor CLI 入口。命令:init / doctor / serve;全局 --workspace <dir>、--json。
|
|
2
|
+
* cotutor CLI 入口。命令:init / doctor / serve;全局 --workspace <dir>、--sandbox [--fresh]、--json。
|
|
3
3
|
* exit:0 通过 / 1 体检不过 / 2 用法错误 / 4 内部错误。
|
|
4
4
|
*/
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -7,6 +7,7 @@ import { readFileSync } from 'node:fs';
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { constants as osConstants } from 'node:os';
|
|
10
|
+
import { basename, join } from 'node:path';
|
|
10
11
|
import { doctorWorkspace } from "./doctor.js";
|
|
11
12
|
import { initWorkspace } from "./init.js";
|
|
12
13
|
import { gradePacks } from "./grades.js";
|
|
@@ -19,7 +20,12 @@ import { linkClaudeSkills, upgradeSkills } from "./skills.js";
|
|
|
19
20
|
import { addTheme, upgradeThemes } from "./themes.js";
|
|
20
21
|
import { patchConfig } from "../workspace.js";
|
|
21
22
|
import { resolveRoot } from "../workspace.js";
|
|
22
|
-
import {
|
|
23
|
+
import { bindFailure, portHolder, whoHolds } from "./port.js";
|
|
24
|
+
import { httpsFiles, serveWorkspace, siteUrl } from "./serve.js";
|
|
25
|
+
import { teeLogs } from "./serve-log.js";
|
|
26
|
+
import { controlService, earlyLogHint, findService, installService, listServices, probeHealth, realHost, runKeeper, servicePlatform, serviceStatus, tailLog, uninstallService, waitHealthy } from "./service.js";
|
|
27
|
+
import { openSandbox } from "./sandbox.js";
|
|
28
|
+
import { dropTrayIfNoServices, ensureTray, installTray, removeTray, trayDir, trayStatus, traySupported } from "./tray.js";
|
|
23
29
|
import { serveMock } from "../mock/mock.js";
|
|
24
30
|
import { UsageError, loadWorkspace, redactDeep, redactHome, workspaceReport } from "../workspace.js";
|
|
25
31
|
import { USAGE } from "./usage.js";
|
|
@@ -67,21 +73,27 @@ function parseArgs(argv, valued) {
|
|
|
67
73
|
}
|
|
68
74
|
return { cmd, positionals, flags };
|
|
69
75
|
}
|
|
70
|
-
/**
|
|
71
|
-
async function
|
|
72
|
-
process.stdout.write(` 扫码页 ${page}\n 在这台电脑上打开,iPad / iPhone 用相机扫;--open-qr 顺手打开\n`);
|
|
73
|
-
if (!open)
|
|
74
|
-
return;
|
|
76
|
+
/** 用默认浏览器打开(各系统各自的打开命令,失败不吭声) */
|
|
77
|
+
async function openInBrowser(page) {
|
|
75
78
|
const { spawn } = await import('node:child_process');
|
|
76
79
|
const [cmd, args] = process.platform === 'darwin' ? ['open', [page]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', page]] : ['xdg-open', [page]];
|
|
77
80
|
spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => { }).unref();
|
|
78
81
|
}
|
|
82
|
+
/** 打开哪一页的那一行(地址独占行尾:后面紧跟全角括号的话,终端不把它认成链接);--open(旧名 --open-qr)顺手打开 */
|
|
83
|
+
async function printOpen(label, page, hint, open) {
|
|
84
|
+
process.stdout.write(` ${label} ${page}\n${' '.repeat(label.length * 2 + 3)}${hint}\n`);
|
|
85
|
+
if (open)
|
|
86
|
+
await openInBrowser(page);
|
|
87
|
+
}
|
|
88
|
+
const wantsOpen = (flags) => flags.open === true || flags['open-qr'] === true;
|
|
89
|
+
/** 不用 workspace 的命令:--sandbox 对它们没有意义 */
|
|
90
|
+
const NO_WORKSPACE = ['grades', 'init', 'mock', 'pin', 'cert', 'tray'];
|
|
79
91
|
export async function main(argv) {
|
|
80
92
|
let json = false;
|
|
81
93
|
try {
|
|
82
|
-
const { cmd, positionals, flags } = parseArgs(argv, ['workspace', 'dir', 'name', 'grade', 'port', 'from', 'runtime', 'force', 'display', 'subject', 'avatar', 'description', 'scenario', 'delay', 'lane', 'job', 'date', 'thread', 'at', 'photo', 'activity', 'param', 'runs', 'only', 'kid-model', 'pin', 'out', 'bundles', 'step', 'voice', 'tutor', 'instruction']);
|
|
94
|
+
const { cmd, positionals, flags } = parseArgs(argv, ['workspace', 'dir', 'name', 'grade', 'port', 'from', 'runtime', 'force', 'display', 'subject', 'avatar', 'description', 'scenario', 'delay', 'lane', 'job', 'date', 'thread', 'at', 'photo', 'activity', 'param', 'runs', 'only', 'kid-model', 'pin', 'out', 'bundles', 'step', 'voice', 'tutor', 'instruction', 'log-dir', 'lines']);
|
|
83
95
|
json = flags.json === true;
|
|
84
|
-
|
|
96
|
+
let workspace = typeof flags.workspace === 'string' ? flags.workspace : undefined;
|
|
85
97
|
// --version / --help 是旗标不是命令,parseArgs 把它们收进 flags,cmd 拿不到,所以在 switch 前处理
|
|
86
98
|
if (flags.version === true || cmd === '-v') {
|
|
87
99
|
process.stdout.write(`${version()}\n`);
|
|
@@ -91,6 +103,22 @@ export async function main(argv) {
|
|
|
91
103
|
process.stdout.write(USAGE);
|
|
92
104
|
return;
|
|
93
105
|
}
|
|
106
|
+
// --sandbox:要 workspace 的命令都换到它的试用副本(src/app/cli/sandbox.ts),下面照常把它当 --workspace
|
|
107
|
+
if (flags.fresh === true && flags.sandbox !== true)
|
|
108
|
+
throw new UsageError('--fresh 只跟 --sandbox 一起用:cotutor serve --sandbox --fresh 重新分出试用 workspace');
|
|
109
|
+
if (flags.sandbox === true) {
|
|
110
|
+
if (NO_WORKSPACE.includes(cmd))
|
|
111
|
+
throw new UsageError(`${cmd} 不用 workspace,--sandbox 没有意义`);
|
|
112
|
+
if (cmd === 'service')
|
|
113
|
+
throw new UsageError('试用 workspace 不装后台服务:试的时候 cotutor serve --sandbox 在前台跑');
|
|
114
|
+
const sb = await openSandbox(workspace, { fresh: flags.fresh === true });
|
|
115
|
+
workspace = sb.root;
|
|
116
|
+
if (!json) {
|
|
117
|
+
process.stderr.write(sb.created ? `分出试用 workspace → ${redactHome(sb.root)}(从 ${redactHome(sb.from)};设置照拷,对话与照片不带,vault 拷了一份)\n` : `试用 workspace ${redactHome(sb.root)}\n`);
|
|
118
|
+
for (const n of sb.notes)
|
|
119
|
+
process.stderr.write(` ${n}\n`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
94
122
|
switch (cmd) {
|
|
95
123
|
case 'grades': {
|
|
96
124
|
const packs = (await gradePacks()).map(({ grade }) => ({ grade }));
|
|
@@ -137,21 +165,310 @@ export async function main(argv) {
|
|
|
137
165
|
}
|
|
138
166
|
case 'serve': {
|
|
139
167
|
const port = typeof flags.port === 'string' ? Number(flags.port) : undefined;
|
|
140
|
-
|
|
168
|
+
// 后台服务(cotutor service 装的)带 --log-dir:输出按天落文件
|
|
169
|
+
const logDir = typeof flags['log-dir'] === 'string' ? flags['log-dir'] : undefined;
|
|
170
|
+
if (logDir)
|
|
171
|
+
teeLogs(logDir);
|
|
172
|
+
// 没接住的错误:记一笔再以非 0 退(后台服务隔 10 秒再起);和 Node 缺省一样会退,只是日志里有时刻和这句
|
|
173
|
+
const fatal = (what) => (err) => {
|
|
174
|
+
process.stderr.write(`cotutor serve: ${what},退出${process.env.COTUTOR_SERVICE ? '(后台服务十秒后自己再起)' : ''}\n${err instanceof Error ? err.stack : String(err)}\n`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
};
|
|
177
|
+
process.on('uncaughtException', fatal('没接住的错误'));
|
|
178
|
+
process.on('unhandledRejection', fatal('没接住的 Promise 错误'));
|
|
179
|
+
let r;
|
|
180
|
+
try {
|
|
181
|
+
r = await serveWorkspace({ workspace, port, http: flags.http === true });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const code = err.code;
|
|
185
|
+
if (code !== 'EADDRINUSE' && code !== 'EACCES')
|
|
186
|
+
throw err;
|
|
187
|
+
const busy = err.port ?? port ?? 0;
|
|
188
|
+
// 后台服务以 0 退:launchd / systemd 只在崩了时拉起,不一直重试抢端口;系统不让绑的,重试也没用
|
|
189
|
+
const service = Boolean(process.env.COTUTOR_SERVICE);
|
|
190
|
+
const who = code === 'EADDRINUSE' ? await whoHolds(realHost(), busy) : null;
|
|
191
|
+
process.stderr.write(`cotutor serve: ${bindFailure(code, busy, who, service)}\n`);
|
|
192
|
+
process.exitCode = service ? 0 : 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
141
195
|
if (flags.trace === true) {
|
|
142
196
|
const keep = laneFilter(typeof flags.lane === 'string' ? flags.lane : undefined);
|
|
143
197
|
r.ctx.runner.onEvent((e) => { if (keep(e.event))
|
|
144
198
|
process.stdout.write(`${e.tutor} ${e.job} ${formatEvent(e.event)}\n`); });
|
|
145
199
|
}
|
|
146
200
|
process.stdout.write(`cotutor serve ${JSON.stringify(workspaceReport(r.ws))}\n`);
|
|
201
|
+
if (r.ws.sandbox)
|
|
202
|
+
process.stdout.write(` 试用:从 ${redactHome(r.ws.sandbox.from)} 分出来,对话、照片、vault 的写入都留在这里,不进孩子的记录;--sandbox --fresh 重新分\n`);
|
|
147
203
|
for (const w of r.warnings)
|
|
148
204
|
process.stdout.write(` ! ${w}\n`);
|
|
149
205
|
if (!r.https)
|
|
150
|
-
process.stdout.write(' ! HTTP
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
206
|
+
process.stdout.write(' ! HTTP:平板、手机上按住说话要 HTTPS;cotutor cert 建证书后重启即走 HTTPS\n');
|
|
207
|
+
// 只给家长端一个入口:孩子的平板扫码、排首页、记录都在里面,工作台从它的右上角进
|
|
208
|
+
await printOpen('家长端', r.parentPage, '在这台电脑上打开:孩子的平板扫码、排首页、看记录都在里面;--open 顺手打开', wantsOpen(flags));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
case 'open': {
|
|
212
|
+
// 不用记端口:读 cotutor.json 打开家长端;--sandbox 开试用的那个(上面已经换了 workspace),--print 只打印不开
|
|
213
|
+
const ws = loadWorkspace(workspace);
|
|
214
|
+
const port = ws.config.server.port;
|
|
215
|
+
const page = siteUrl(httpsFiles(ws) ? 'https' : 'http', 'localhost', port, '/parent/home');
|
|
216
|
+
const running = Boolean(await probeHealth(port));
|
|
217
|
+
if (json)
|
|
218
|
+
process.stdout.write(`${JSON.stringify({ url: page, running }, null, 2)}\n`);
|
|
219
|
+
else if (running)
|
|
220
|
+
process.stdout.write(`${page}\n`);
|
|
221
|
+
else
|
|
222
|
+
process.stderr.write(`${page} 没在跑(端口 ${port}):${ws.sandbox ? 'cotutor serve --sandbox 起试用的' : 'cotutor service start,或在终端里 cotutor serve'}\n`);
|
|
223
|
+
if (!running) {
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!json && flags.print !== true)
|
|
228
|
+
await openInBrowser(page);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
case 'tray': {
|
|
232
|
+
// 菜单栏 / 任务栏图标(tray.ts);tray run 由 bin/cotutor.js 直接走轻入口,不到这里
|
|
233
|
+
const sub = positionals[0];
|
|
234
|
+
if (sub !== 'on' && sub !== 'off' && sub !== 'status')
|
|
235
|
+
throw new UsageError(`tray 后面跟 on / off / status 之一。\n${USAGE}`);
|
|
236
|
+
const host = realHost();
|
|
237
|
+
if (!traySupported(host.platform))
|
|
238
|
+
throw new UsageError(`${host.platform} 上没做托盘图标(GNOME 缺省不显示托盘):cotutor service status 看服务状态`);
|
|
239
|
+
const word = host.platform === 'darwin' ? '菜单栏图标' : '任务栏图标';
|
|
240
|
+
if (sub === 'on') {
|
|
241
|
+
if (!(await listServices(host)).length)
|
|
242
|
+
throw new UsageError('还没装后台服务,图标没东西可看:cotutor service install 会一起装上图标');
|
|
243
|
+
const r = await installTray(host);
|
|
244
|
+
if (json)
|
|
245
|
+
process.stdout.write(`${JSON.stringify(redactDeep(r), null, 2)}\n`);
|
|
246
|
+
else
|
|
247
|
+
process.stdout.write(r.started ? `${word}起来了:登录后自己出现,点它看状态、打开家长端${host.platform === 'win32' ? '(新图标先收在任务栏右下角的 ^ 里)' : ''}\n` : `${word}的登录项写好了,这个会话里没起来,下次登录出现\n`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (sub === 'off') {
|
|
251
|
+
const had = await removeTray(host, { remember: true });
|
|
252
|
+
if (json)
|
|
253
|
+
process.stdout.write(`${JSON.stringify({ removed: had })}\n`);
|
|
254
|
+
else
|
|
255
|
+
process.stdout.write(`${had ? `关了${word}` : `${word}本来就没开`};后台服务不受影响,以后 service install 也不再带上。要回来:cotutor tray on\n`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const st = await trayStatus(host);
|
|
259
|
+
if (json) {
|
|
260
|
+
process.stdout.write(`${JSON.stringify(redactDeep({ ...st, dir: trayDir(host) }), null, 2)}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
process.stdout.write(!st.installed ? `${word}:没开${st.off ? '(家长关掉的)' : ''};cotutor tray on\n` : `${word}:${st.running ? `在跑,pid ${st.pid ?? '?'}` : '没在跑(点过退出就是这样,下次登录回来)'}\n`);
|
|
264
|
+
if (st.autostart === false)
|
|
265
|
+
process.stdout.write(' ! 下次登录不会自己起来:登录项被关掉或删掉了(任务管理器的「启动应用」、杀毒软件)。要它:cotutor tray on\n');
|
|
266
|
+
if (st.installed)
|
|
267
|
+
process.stdout.write(` 日志 ${redactHome(join(trayDir(host), 'tray.log'))}\n`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
case 'service': {
|
|
271
|
+
const sub = positionals[0];
|
|
272
|
+
// 看护进程(Windows 的启动器起的,不是给人敲的):起 serve,崩了再起
|
|
273
|
+
if (sub === 'run') {
|
|
274
|
+
const logDir = flags['log-dir'];
|
|
275
|
+
if (typeof workspace !== 'string' || typeof logDir !== 'string')
|
|
276
|
+
throw new UsageError('service run 要 --workspace 与 --log-dir');
|
|
277
|
+
process.exitCode = await runKeeper({ root: workspace, logDir, label: `cotutor-${basename(logDir)}` });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const SUBS = ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs'];
|
|
281
|
+
if (!sub || !SUBS.includes(sub))
|
|
282
|
+
throw new UsageError(`service 后面跟 ${SUBS.join(' / ')} 之一。\n${USAGE}`);
|
|
283
|
+
const host = realHost();
|
|
284
|
+
const platform = servicePlatform(host.platform);
|
|
285
|
+
const ws = loadWorkspace(workspace);
|
|
286
|
+
if (ws.sandbox)
|
|
287
|
+
throw new UsageError('试用 workspace 不装后台服务:试的时候 cotutor serve --sandbox 在前台跑');
|
|
288
|
+
const root = ws.root;
|
|
289
|
+
const port = ws.config.server.port;
|
|
290
|
+
const scheme = httpsFiles(ws) ? 'https' : 'http';
|
|
291
|
+
const parentPage = siteUrl(scheme, 'localhost', port, '/parent/home');
|
|
292
|
+
const when = (iso) => { const d = new Date(iso); const ago = Math.round((Date.now() - d.getTime()) / 60_000); return `${localDate(d)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}(${ago < 60 ? `${ago} 分钟` : ago < 2880 ? `${Math.round(ago / 60)} 小时` : `${Math.round(ago / 1440)} 天`}前)`; };
|
|
293
|
+
// 后台读不到 vault(macOS 拦着后台进程读「文稿」、iCloud 云盘)、版本对不上:装完、起完、看状态都提
|
|
294
|
+
const notes = (h) => {
|
|
295
|
+
const out = [];
|
|
296
|
+
if (h?.vault === 'denied')
|
|
297
|
+
out.push(platform === 'darwin'
|
|
298
|
+
? `! 后台服务读不到 vault(${redactHome(ws.paths.vault)}):macOS 不让后台进程读「文稿」「桌面」和 iCloud 云盘,也不弹窗问。系统设置 › 隐私与安全性 › 完全磁盘访问权限,点 + 加上 ${process.execPath}(或把 vault 挪出这几处),然后 cotutor service restart`
|
|
299
|
+
: `! 后台服务读不到 vault(${redactHome(ws.paths.vault)}):看看这个目录的权限,然后 cotutor service restart`);
|
|
300
|
+
if (h?.version && h.version !== version())
|
|
301
|
+
out.push(`! 在跑的是 ${h.version},装的是 ${version()}:cotutor service restart 换上新的`);
|
|
302
|
+
return out;
|
|
303
|
+
};
|
|
304
|
+
const tellLog = async (dir) => {
|
|
305
|
+
const t = dir ? await tailLog(dir, 15) : null;
|
|
306
|
+
if (t) {
|
|
307
|
+
process.stdout.write(` ${redactHome(t.file)} 最后几行:\n`);
|
|
308
|
+
for (const l of t.lines)
|
|
309
|
+
process.stdout.write(` ${l}\n`);
|
|
310
|
+
}
|
|
311
|
+
else if (dir)
|
|
312
|
+
process.stdout.write(` 日志还没有;起来之前的输出看 ${redactHome(earlyLogHint(platform, { label: `cotutor-${basename(dir)}`, logDir: dir }))}\n`);
|
|
313
|
+
};
|
|
314
|
+
if (sub === 'install') {
|
|
315
|
+
const before = await probeHealth(port);
|
|
316
|
+
const had = await findService(host, root);
|
|
317
|
+
if (before && before.mode !== 'service')
|
|
318
|
+
throw new UsageError(`端口 ${port} 上有一个 cotutor serve 在终端里跑着${before.pid ? `(pid ${before.pid})` : ''}:先关掉它(那个终端里 Ctrl-C),再 cotutor service install`);
|
|
319
|
+
if (before && !had)
|
|
320
|
+
throw new UsageError(`端口 ${port} 上已经是另一个 workspace 的后台服务:两个孩子各用各的端口(cotutor.json 的 server.port)`);
|
|
321
|
+
const r = await installService(host, root);
|
|
322
|
+
const h = await waitHealthy(port, 20_000, before?.started);
|
|
323
|
+
// 菜单栏图标一起装:它出问题不能让服务也算没装好
|
|
324
|
+
let tray = 'skipped';
|
|
325
|
+
if (flags['no-tray'] !== true) {
|
|
326
|
+
try {
|
|
327
|
+
tray = await ensureTray(host);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
tray = 'failed';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (json) {
|
|
334
|
+
process.stdout.write(`${JSON.stringify(redactDeep({ ...r, healthy: Boolean(h), health: h, tray }), null, 2)}\n`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
process.stdout.write(`${r.replaced ? '重装了' : '装好了'}后台服务 ${r.spec.label}(${redactHome(r.file)})\n`);
|
|
338
|
+
process.stdout.write(` 登录这台电脑后自己起来,崩了隔 10 秒再起;关了终端照样跑。日志 ${redactHome(join(r.spec.logDir, '<日期>.log'))},留 14 天\n`);
|
|
339
|
+
if (platform !== 'linux')
|
|
340
|
+
process.stdout.write(` 电脑睡着时平板连不上:有通话时 cotutor 拦着它闲置睡眠,别的时候看系统设置(cotutor doctor 会提);合盖照样睡\n`);
|
|
341
|
+
if (platform === 'win32') {
|
|
342
|
+
process.stdout.write(' 没有黑窗口。任务管理器的「启动应用」里看不到 cotutor 这个名字,显示的是 Windows Based Script Host;登录后约一分钟起来,在不在跑看 cotutor service status\n');
|
|
343
|
+
// 平板连不连得进来:Windows 第一次监听时弹一次「允许访问」,点了取消(或没看见)就再也连不上;不在终端里跑了,这里不说就没地方说了
|
|
344
|
+
const { judgeFirewall, probeFirewall } = await import("./firewall.js");
|
|
345
|
+
const state = await probeFirewall(port);
|
|
346
|
+
const v = state && judgeFirewall(state, port, process.execPath);
|
|
347
|
+
if (v && !v.ok)
|
|
348
|
+
process.stdout.write(` ! ${v.detail}\n 修复:${v.fix ?? ''}\n 弄好以后 cotutor doctor 再查一次\n`);
|
|
349
|
+
}
|
|
350
|
+
if (platform === 'linux') {
|
|
351
|
+
const lg = await host.run('loginctl', ['show-user', process.env.USER ?? '', '-p', 'Linger']);
|
|
352
|
+
if (/Linger=no/.test(lg.stdout))
|
|
353
|
+
process.stdout.write(` ! 现在只在你登录着时跑;不登录也要跑:sudo loginctl enable-linger ${process.env.USER ?? '<用户名>'}\n`);
|
|
354
|
+
}
|
|
355
|
+
if (tray === 'started' || tray === 'later')
|
|
356
|
+
process.stdout.write(` ${platform === 'darwin' ? '菜单栏' : '任务栏'}多了 cotutor 图标:不用开终端就能看状态、打开家长端${tray === 'later' ? '(这个会话里没起来,下次登录出现)' : platform === 'win32' ? '(新图标先收在右下角的 ^ 里)' : ''};不要它:cotutor tray off\n`);
|
|
357
|
+
else if (tray === 'failed')
|
|
358
|
+
process.stdout.write(' ! 图标没装上,服务不受影响;cotutor tray on 再试\n');
|
|
359
|
+
if (h) {
|
|
360
|
+
await printOpen('家长端', parentPage, '孩子的平板扫码、排首页、看记录都在里面', wantsOpen(flags));
|
|
361
|
+
for (const n of notes(h))
|
|
362
|
+
process.stdout.write(`${n}\n`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
process.stdout.write(`! 装上了,但 20 秒内没答应(端口 ${port})。cotutor service logs 看看:\n`);
|
|
366
|
+
await tellLog(r.spec.logDir);
|
|
367
|
+
process.exitCode = 1;
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (sub === 'uninstall') {
|
|
372
|
+
const s = await uninstallService(host, root);
|
|
373
|
+
// 这是最后一个服务:图标也没事可做了
|
|
374
|
+
const trayGone = s ? await dropTrayIfNoServices(host).catch(() => false) : false;
|
|
375
|
+
if (json) {
|
|
376
|
+
process.stdout.write(`${JSON.stringify(redactDeep({ removed: s, trayRemoved: trayGone }), null, 2)}\n`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
process.stdout.write(s ? `卸了后台服务 ${s.label};日志留在 ${redactHome(s.logDir ?? '')},不要了自己删\n要在终端里跑:cotutor serve\n` : '这个 workspace 没装后台服务\n');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (sub === 'start' || sub === 'stop' || sub === 'restart') {
|
|
383
|
+
const before = sub === 'stop' ? null : await probeHealth(port);
|
|
384
|
+
if (sub === 'start' && before?.mode === 'service') {
|
|
385
|
+
process.stdout.write(`后台服务已经在跑了(pid ${before.pid ?? '?'})\n`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (sub !== 'stop' && before && before.mode !== 'service')
|
|
389
|
+
throw new UsageError(`端口 ${port} 上有一个 cotutor serve 在终端里跑着:先关掉它(Ctrl-C)`);
|
|
390
|
+
const s = await controlService(host, root, sub);
|
|
391
|
+
if (sub === 'stop') {
|
|
392
|
+
process.stdout.write(`停了 ${s.label}(下次${platform === 'linux' ? '开机' : '登录'}又会自己起来;一直不要它:cotutor service uninstall)\n`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const h = await waitHealthy(port, 20_000, before?.started);
|
|
396
|
+
if (h) {
|
|
397
|
+
process.stdout.write(`${sub === 'start' ? '起来了' : '重起了'}:pid ${h.pid ?? '?'},版本 ${h.version ?? '?'}\n`);
|
|
398
|
+
for (const n of notes(h))
|
|
399
|
+
process.stdout.write(`${n}\n`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
process.stdout.write(`! 20 秒内没答应(端口 ${port}):\n`);
|
|
403
|
+
await tellLog(s.logDir);
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (sub === 'status') {
|
|
409
|
+
const st = await serviceStatus(host, root, port);
|
|
410
|
+
// 没答应时看看是不是别的程序占着:后台服务遇到这种情况只写一句日志就不起了,这里要说得出来
|
|
411
|
+
const holder = st.health ? null : await portHolder(host, port);
|
|
412
|
+
if (json) {
|
|
413
|
+
process.stdout.write(`${JSON.stringify(redactDeep({ ...st, portHolder: holder, version: version(), parentPage }), null, 2)}\n`);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!st.installed)
|
|
417
|
+
process.stdout.write('后台服务:没装(cotutor service install)\n');
|
|
418
|
+
else
|
|
419
|
+
process.stdout.write(`后台服务:装了 ${st.installed.label}(${redactHome(st.installed.file)})\n ${st.running ? `在跑:pid ${st.pid ?? '?'}` : `没在跑${st.lastExit ? `(上次退出:${st.lastExit})` : ''};cotutor service start`}\n`);
|
|
420
|
+
const h = st.health;
|
|
421
|
+
if (h)
|
|
422
|
+
process.stdout.write(` 端口 ${port} 答应:${h.mode === 'service' ? '后台服务' : `终端里的 cotutor serve(pid ${h.pid ?? '?'})`},版本 ${h.version ?? '?'},起来于 ${when(h.started)}\n 家长端 ${parentPage}\n`);
|
|
423
|
+
else
|
|
424
|
+
process.stdout.write(holder ? ` 端口 ${port} 没答应,被占着:${holder.name}(pid ${holder.pid})在用。腾出来以后 cotutor service start;或改 cotutor.json 的 server.port,平板要重新扫码、重加主屏幕图标\n` : ` 端口 ${port} 没答应\n`);
|
|
425
|
+
for (const n of notes(h))
|
|
426
|
+
process.stdout.write(`${n}\n`);
|
|
427
|
+
if (st.autostart === false)
|
|
428
|
+
process.stdout.write(' ! 下次登录不会自己起来:登录项被关掉或删掉了(任务管理器的「启动应用」、杀毒软件)。要它:cotutor service install 再装一次\n');
|
|
429
|
+
if (st.installed?.logDir)
|
|
430
|
+
process.stdout.write(` 日志 ${redactHome(st.installed.logDir)}(cotutor service logs)\n`);
|
|
431
|
+
if (st.installed && !h)
|
|
432
|
+
await tellLog(st.installed.logDir);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// logs
|
|
436
|
+
const s = await findService(host, root);
|
|
437
|
+
if (!s?.logDir)
|
|
438
|
+
throw new UsageError('这个 workspace 没装后台服务,没有日志;终端里跑的 cotutor serve 输出就在那个终端里');
|
|
439
|
+
const n = typeof flags.lines === 'string' ? Number(flags.lines) : 50;
|
|
440
|
+
if (!(Number.isInteger(n) && n > 0))
|
|
441
|
+
throw new UsageError('--lines 要是正整数');
|
|
442
|
+
const t = await tailLog(s.logDir, n);
|
|
443
|
+
if (t)
|
|
444
|
+
for (const l of t.lines)
|
|
445
|
+
process.stdout.write(`${l}\n`);
|
|
446
|
+
else
|
|
447
|
+
process.stdout.write(`还没有日志;起来之前的输出看 ${redactHome(earlyLogHint(platform, s))}\n`);
|
|
448
|
+
if (flags.follow === true) {
|
|
449
|
+
const { open: openFile, stat: statFile } = await import('node:fs/promises');
|
|
450
|
+
const { logFileFor } = await import("./serve-log.js");
|
|
451
|
+
let file = t?.file ?? logFileFor(s.logDir, new Date());
|
|
452
|
+
let pos = (await statFile(file).catch(() => null))?.size ?? 0;
|
|
453
|
+
setInterval(() => {
|
|
454
|
+
void (async () => {
|
|
455
|
+
const today = logFileFor(s.logDir, new Date());
|
|
456
|
+
if (today !== file && (await statFile(today).catch(() => null))) {
|
|
457
|
+
file = today;
|
|
458
|
+
pos = 0;
|
|
459
|
+
}
|
|
460
|
+
const st = await statFile(file).catch(() => null);
|
|
461
|
+
if (!st || st.size <= pos)
|
|
462
|
+
return;
|
|
463
|
+
const fh = await openFile(file, 'r');
|
|
464
|
+
const buf = Buffer.alloc(st.size - pos);
|
|
465
|
+
await fh.read(buf, 0, buf.length, pos);
|
|
466
|
+
await fh.close();
|
|
467
|
+
pos = st.size;
|
|
468
|
+
process.stdout.write(buf);
|
|
469
|
+
})();
|
|
470
|
+
}, 500);
|
|
471
|
+
}
|
|
155
472
|
return;
|
|
156
473
|
}
|
|
157
474
|
case 'mock': {
|
|
@@ -170,11 +487,11 @@ export async function main(argv) {
|
|
|
170
487
|
process.once(s, () => process.exit(128 + osConstants.signals[s]));
|
|
171
488
|
process.stdout.write(`cotutor mock 场景 ${scenario}(不经真实老师与百炼:老师都走实时老师页,假嘴念静音的声音)\n`);
|
|
172
489
|
if (!r.https)
|
|
173
|
-
process.stdout.write(' ! HTTP
|
|
490
|
+
process.stdout.write(' ! HTTP:平板、手机上按住说话要 HTTPS;cotutor cert 建证书后重启即走 HTTPS\n');
|
|
174
491
|
for (const u of r.urls)
|
|
175
492
|
process.stdout.write(` ${u}\n`);
|
|
176
493
|
process.stdout.write(' 孩子端 /,家长端 /parent;直接开某位老师:/?tutor=chinese-tutor\n');
|
|
177
|
-
await
|
|
494
|
+
await printOpen('扫码页', r.qrPage, '在这台电脑上打开,平板、手机用相机扫;--open 顺手打开', wantsOpen(flags));
|
|
178
495
|
return;
|
|
179
496
|
}
|
|
180
497
|
case 'add': {
|
|
@@ -227,7 +544,7 @@ export async function main(argv) {
|
|
|
227
544
|
process.stdout.write(`✓ ${st.item.padEnd(24)} ${st.note ?? '建好了'}\n`);
|
|
228
545
|
process.stdout.write(dryRun
|
|
229
546
|
? `\n${r.gaps.length} 项要改,还没动文件:cotutor upgrade --config 真改(只加缺的、删不再用的出厂段,你改过的值不动)\n`
|
|
230
|
-
: `\n改了 ${r.gaps.length} 项到 ${redactHome(r.file)};服务在跑的话不用重启(
|
|
547
|
+
: `\n改了 ${r.gaps.length} 项到 ${redactHome(r.file)};服务在跑的话不用重启(改了就热重载)。\n`);
|
|
231
548
|
}
|
|
232
549
|
return;
|
|
233
550
|
}
|
|
@@ -248,7 +565,7 @@ export async function main(argv) {
|
|
|
248
565
|
process.stdout.write(`${s.action === 'kept' ? '!' : '✓'} ${s.item.padEnd(24)} ${s.note ?? ''}\n`);
|
|
249
566
|
if (link.action !== 'exists')
|
|
250
567
|
process.stdout.write(`${link.action === 'kept' ? '!' : '✓'} ${link.item.padEnd(24)} ${link.note ?? ''}\n`);
|
|
251
|
-
const word = { upgraded: '已换新', latest: '已是最新', 'kept-custom': '自定义,保留', forced: '已覆盖(原文 .bak)', installed: '补上了', retired: '不再出厂,删了' };
|
|
568
|
+
const word = { upgraded: '已换新', latest: '已是最新', 'kept-custom': '自定义,保留', forced: '已覆盖(原文 .bak)', installed: '补上了', retired: '不再出厂,删了', renamed: '改了名' };
|
|
252
569
|
for (const s of skillSteps)
|
|
253
570
|
process.stdout.write(`${s.action === 'kept-custom' ? '!' : '✓'} skill ${s.name.padEnd(18)} ${word[s.action]}${s.basedOn && s.action !== 'latest' ? `(基于 ${s.basedOn})` : ''}${s.machine ? '(机器件)' : ''}\n`);
|
|
254
571
|
for (const s of themeSteps)
|
|
@@ -264,7 +581,7 @@ export async function main(argv) {
|
|
|
264
581
|
}
|
|
265
582
|
}
|
|
266
583
|
for (const s of activitySteps) {
|
|
267
|
-
process.stdout.write(`${s.action === 'kept-custom' ? '!' : '✓'} activity ${s.key.padEnd(18)} ${word[s.action]}${s.basedOn && s.action !== 'latest' ? `(基于 ${s.basedOn})` : ''}\n`);
|
|
584
|
+
process.stdout.write(`${s.action === 'kept-custom' ? '!' : '✓'} activity ${s.key.padEnd(18)} ${word[s.action]}${s.from ? `(原来叫「${s.from}」)` : ''}${s.basedOn && s.action !== 'latest' ? `(基于 ${s.basedOn})` : ''}\n`);
|
|
268
585
|
if (s.diff?.length) {
|
|
269
586
|
process.stdout.write(` 你的 vs 新版(- 你的 / + 新版),想用新版:cotutor upgrade --force ${s.key}\n`);
|
|
270
587
|
for (const l of s.diff.slice(0, 40))
|
|
@@ -375,7 +692,13 @@ export async function main(argv) {
|
|
|
375
692
|
process.stdout.write(`${JSON.stringify(redactDeep(r), null, 2)}\n`);
|
|
376
693
|
else {
|
|
377
694
|
process.stdout.write(`证书:${redactHome(r.cert)}\n私钥:${redactHome(r.key)}\n主机:${r.hosts.join(' ')}\n`);
|
|
378
|
-
|
|
695
|
+
// 根证书怎么到设备上:Mac 隔空投送;Windows 等没有,用微信、邮件或网盘传
|
|
696
|
+
const rootCa = redactHome(join(r.caRoot, 'rootCA.pem'));
|
|
697
|
+
const send = process.platform === 'darwin' ? '隔空投送过去' : '用微信、邮件或网盘传到设备上';
|
|
698
|
+
process.stdout.write(`平板、手机要先信任这台机器的根证书 ${rootCa}(每台设备各做一次):\n`);
|
|
699
|
+
process.stdout.write(` iPad / iPhone:${send} → 设置里安装描述文件 → 通用 › 关于本机 › 证书信任设置里**把开关打开**(装了不等于信任)\n`);
|
|
700
|
+
process.stdout.write(` 安卓:${process.platform === 'darwin' ? '传到设备上' : send} → 设置里搜「CA 证书」,按提示安装(各家菜单不一样,会提示「网络可能受到监控」)\n`);
|
|
701
|
+
process.stdout.write(`然后 cotutor service restart(在终端里跑的就重开 cotutor serve),cotutor open 打开家长端,用相机扫「扫码打开」那张。地址认电脑名,换 Wi-Fi、IP 变了都不用重签;改了电脑名才要重跑这条。详见 docs/iPad与iPhone.md\n`);
|
|
379
702
|
}
|
|
380
703
|
return;
|
|
381
704
|
}
|
|
@@ -662,8 +985,7 @@ export async function main(argv) {
|
|
|
662
985
|
throw new UsageError(`--photo ${f} 太大(${(data.length / 1e6).toFixed(1)} MB,最多 ${PHOTO_MAX_BYTES / 1e6} MB);先缩,如 sips -s format jpeg -Z 2000 ${f} --out x.jpg`);
|
|
663
986
|
photos.push(await writeCapture(ws, new Date(), data, ext === 'png' ? 'png' : 'jpg'));
|
|
664
987
|
}
|
|
665
|
-
|
|
666
|
-
const ctx = createContext(ws, { warm: false });
|
|
988
|
+
const ctx = createContext(ws);
|
|
667
989
|
// 现场打印每道工序的事件(--quiet / --json 不打);事件同时落在 events.jsonl,事后 cotutor trace 能回放
|
|
668
990
|
if (!json && flags.quiet !== true) {
|
|
669
991
|
const keep = laneFilter(typeof flags.lane === 'string' ? flags.lane : undefined);
|
|
@@ -671,7 +993,8 @@ export async function main(argv) {
|
|
|
671
993
|
process.stdout.write(`${formatEvent(e.event)}\n`); });
|
|
672
994
|
}
|
|
673
995
|
const activity = activityName ? await activityRunFor(ws.root, tutor, activityName, typeof flags.param === 'string' ? flags.param : '') : undefined;
|
|
674
|
-
|
|
996
|
+
// 孩子的话照实时写(同孩子端:上下文包带 live:、不补提问卡、讲稿不截;嘴不在,接住与打断不带);--from system 不算
|
|
997
|
+
const started = await ctx.runner.send(tutor, { from: from, text, runtime: typeof flags.runtime === 'string' ? flags.runtime : undefined, newThread: flags.new === true || Boolean(activity), ...(photos.length ? { photos } : {}), ...(activity ? { activity } : {}), ...(from === 'kid' ? { live: {} } : {}) });
|
|
675
998
|
if (!json)
|
|
676
999
|
process.stdout.write(`→ ${tutor} ${started.date} ${started.job} 话题 ${started.thread}(${started.plan.runtime}${started.plan.resume ? ',resume ' + started.plan.session : ',新会话'})…\n`);
|
|
677
1000
|
const index = await started.done;
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { type InstallStep } from './tutors.ts';
|
|
2
2
|
/** section:出厂模板里有的整段(dictation 这种后加的),你这份没有;retired:出厂写进去、应用已经不读的整段,补缺时删掉 */
|
|
3
|
-
export type GapKind = 'tutor' | 'runtime' | '
|
|
3
|
+
export type GapKind = 'tutor' | 'runtime' | 'cards' | 'section' | 'retired';
|
|
4
4
|
export interface ConfigGap {
|
|
5
5
|
kind: GapKind;
|
|
6
|
-
/** 点位:tutors.math-tutor / runtimes.
|
|
6
|
+
/** 点位:tutors.math-tutor / runtimes.api-flash / dictation */
|
|
7
7
|
path: string;
|
|
8
8
|
/** 人话:缺的是什么、补上有什么用 */
|
|
9
9
|
detail: string;
|
|
10
10
|
/** 补上之后这个点位的值(--dry-run 打印、页面提示用);retired 是 undefined(删掉) */
|
|
11
11
|
value: unknown;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
* 把出厂模板里有、这份没有的旗标插进来,**位置照出厂模板**:
|
|
15
|
-
* 插到「出厂模板里排在它前面、这份也有」的那个旗标之后;一个都找不到就放末尾。
|
|
16
|
-
* 已经有的旗标一律不碰——值被家长改过(预算 2 → 5)是她的决定。
|
|
17
|
-
*/
|
|
18
|
-
export declare function insertMissingFlags(mine: readonly string[], factory: readonly string[]): {
|
|
19
|
-
argv: string[];
|
|
20
|
-
added: string[];
|
|
21
|
-
};
|
|
22
13
|
/**
|
|
23
14
|
* 这份 cotutor.json 比出厂模板少了什么。只看「有没有」,不看值——值是家长的。
|
|
24
15
|
* 注意:出厂老师是家长手动从 tutors 里删掉的(页面删不了出厂老师),这里也会当成缺,
|
package/dist/app/cli/migrate.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* cotutor.json 是**政策文件**:init 只在缺失时写模板,已有的那份机器永不改(家长的决定不由机器代做)。
|
|
5
5
|
* 代价是包更新带来的新出厂件不会自己出现在老 workspace 里,而且「没有」的表现是**静默**的——
|
|
6
6
|
* 所以有这一层:把「出厂模板有、你这份没有」算出来(doctor 的 `config.migrate` 点名、家长端设置页顶部提示),
|
|
7
|
-
* `cotutor upgrade --config`
|
|
8
|
-
* (改小的预算、加过的 `-m qwen3.8-flash`、换掉的 default 运行时全都原样留着)。
|
|
7
|
+
* `cotutor upgrade --config` 只**补缺**——加缺的键;家长写过的值一个都不动(改小的预算、换掉的 default 运行时全都原样留着)。
|
|
9
8
|
* 唯一删的是 RETIRED 里点了名的出厂段(机器写进去、应用已经不读的,如 2026-10-31 的 tts);家长自己加的键不在表里,不碰。
|
|
10
9
|
*
|
|
11
10
|
* 差异是**现算的**:拿家长自己的 slug / name / port 生成一份出厂模板,和她这份逐项比,
|
|
@@ -19,42 +18,6 @@ import { configTemplate, shippedAgents } from "./skeleton.js";
|
|
|
19
18
|
import { installTutors } from "./tutors.js";
|
|
20
19
|
import { CONFIG_FILE, ConfigError, readJson } from "../workspace.js";
|
|
21
20
|
const isObj = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
-
/** 命令模板里一个旗标带几个词:`--output-format stream-json` 是两个,`--include-partial-messages` 是一个(值不以 - 打头) */
|
|
23
|
-
function flagSpan(argv, i) {
|
|
24
|
-
const span = [argv[i]];
|
|
25
|
-
for (let j = i + 1; j < argv.length && !argv[j].startsWith('-'); j++)
|
|
26
|
-
span.push(argv[j]);
|
|
27
|
-
return span;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* 把出厂模板里有、这份没有的旗标插进来,**位置照出厂模板**:
|
|
31
|
-
* 插到「出厂模板里排在它前面、这份也有」的那个旗标之后;一个都找不到就放末尾。
|
|
32
|
-
* 已经有的旗标一律不碰——值被家长改过(预算 2 → 5)是她的决定。
|
|
33
|
-
*/
|
|
34
|
-
export function insertMissingFlags(mine, factory) {
|
|
35
|
-
const argv = [...mine];
|
|
36
|
-
const added = [];
|
|
37
|
-
for (let i = 0; i < factory.length; i++) {
|
|
38
|
-
const tok = factory[i];
|
|
39
|
-
if (!tok.startsWith('-') || argv.includes(tok))
|
|
40
|
-
continue;
|
|
41
|
-
const span = flagSpan(factory, i);
|
|
42
|
-
let at = argv.length;
|
|
43
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
44
|
-
const prev = factory[j];
|
|
45
|
-
if (!prev.startsWith('-'))
|
|
46
|
-
continue;
|
|
47
|
-
const k = argv.indexOf(prev);
|
|
48
|
-
if (k === -1)
|
|
49
|
-
continue;
|
|
50
|
-
at = k + flagSpan(argv, k).length;
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
argv.splice(at, 0, ...span);
|
|
54
|
-
added.push(...span);
|
|
55
|
-
}
|
|
56
|
-
return { argv, added };
|
|
57
|
-
}
|
|
58
21
|
/** 出厂模板:拿这份配置自己的 slug / name / port 生成,免得差异里混进「孩子叫什么」这种无关项 */
|
|
59
22
|
async function factoryConfig(raw, agentsDir) {
|
|
60
23
|
const kid = isObj(raw.kid) ? raw.kid : {};
|
|
@@ -98,27 +61,6 @@ export async function configGaps(raw, agentsDir) {
|
|
|
98
61
|
continue;
|
|
99
62
|
gaps.push({ kind: 'runtime', path: `runtimes.${name}`, detail: `出厂运行时 ${name} 不在 runtimes 里`, value: rt });
|
|
100
63
|
}
|
|
101
|
-
for (const [name, rt] of Object.entries(factoryRuntimes)) {
|
|
102
|
-
if (name === 'default' || !isObj(rt))
|
|
103
|
-
continue;
|
|
104
|
-
const mine = mineRuntimes[name];
|
|
105
|
-
if (!isObj(mine))
|
|
106
|
-
continue; // 整个运行时都缺,上面那轮已经报了
|
|
107
|
-
// 出厂模板换了说话方式(消息改走 stdin)而你的改过:逐个补旗标会拼出过不了契约的模板,不动(家长的决定)
|
|
108
|
-
if (rt.stdin && !mine.stdin)
|
|
109
|
-
continue;
|
|
110
|
-
for (const key of ['run', 'resume']) {
|
|
111
|
-
const f = rt[key];
|
|
112
|
-
const u = mine[key];
|
|
113
|
-
if (!Array.isArray(f) || !Array.isArray(u))
|
|
114
|
-
continue;
|
|
115
|
-
const { argv, added } = insertMissingFlags(u, f);
|
|
116
|
-
if (!added.length)
|
|
117
|
-
continue;
|
|
118
|
-
const what = `缺出厂旗标 ${added.join(' ')}`;
|
|
119
|
-
gaps.push({ kind: 'flag', path: `runtimes.${name}.${key}`, detail: `${name} 的 ${key} 命令模板${what}`, value: argv });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
64
|
// 后加的整段(2026-10-10 听写的拍照认词要求):写进文件家长才看得见、改得动;已经有这一段就不碰
|
|
123
65
|
for (const key of SECTIONS) {
|
|
124
66
|
if (key in raw || !(key in factory))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ServiceHost } from './service.ts';
|
|
2
|
+
export interface PortHolder {
|
|
3
|
+
pid: number;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/** lsof -Fpc 的输出:一行 p<pid>,一行 c<命令名> */
|
|
7
|
+
export declare function parseLsof(out: string): PortHolder | null;
|
|
8
|
+
/**
|
|
9
|
+
* 谁在监听(Windows):只认对端端口是 0,不看 State。2026-11-06 Windows 11 build 26300 上监听的 socket 状态是乱的:
|
|
10
|
+
* netstat 显示 CLOSED、FIN_WAIT_1,-State Listen 查不到,读 State 还可能抛异常
|
|
11
|
+
*/
|
|
12
|
+
export declare function holderScript(port: number): string;
|
|
13
|
+
/** 上面那段的输出;坏的、空的当不知道 */
|
|
14
|
+
export declare function parseHolder(text: string): PortHolder | null;
|
|
15
|
+
/** 谁在监听这个端口;查不到(别人的进程、没装 lsof)→ null */
|
|
16
|
+
export declare function portHolder(host: Pick<ServiceHost, 'platform' | 'run'>, port: number): Promise<PortHolder | null>;
|