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.
- package/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +65 -18
- 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 +11 -0
- package/dist/app/cli/cert.js +36 -9
- 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 +11 -5
- package/dist/app/cli/doctor.js +156 -181
- 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 +379 -27
- 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 +152 -0
- package/dist/app/cli/service.js +443 -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 +42 -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 +236 -0
- package/dist/app/cli/tray.js +670 -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 +47 -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/machine/update.d.ts +25 -0
- package/dist/app/machine/update.js +88 -0
- 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/shared/upgrade.d.ts +9 -0
- package/dist/shared/upgrade.js +23 -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 +337 -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 +6 -2
- 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/doctor.js
CHANGED
|
@@ -6,19 +6,18 @@
|
|
|
6
6
|
import { isRealtimeVoice } from "../../realtime/voices.js";
|
|
7
7
|
import { dashscopeAuth } from "../../realtime/bailian.js";
|
|
8
8
|
import { TTS_MODEL, ttsTarget } from "../../media/tts.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { compatibleLine, isPlanKey, tokenPlanKey } from "../../shared/qianwen.js";
|
|
10
|
+
import { runtimeIssue } from "../../brain/index.js";
|
|
11
|
+
import { homedir } from 'node:os';
|
|
11
12
|
import { access, lstat, readFile, readdir, stat } from 'node:fs/promises';
|
|
12
|
-
import { constants as fsConstants } from 'node:fs';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { boardPreloaded, planRun, runtimeUses } from "../../brain/cli/run-plan.js";
|
|
16
|
-
import { BOARD_GUIDE_PATH, boardGuideBody, takesTutorRules } from "../../runtime/context/tutor-rules.js";
|
|
13
|
+
import { constants as fsConstants, existsSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
15
|
+
import { takesTutorRules } from "../../runtime/context/tutor-rules.js";
|
|
17
16
|
import { parseAgentFile } from "../../runtime/agent-file.js";
|
|
18
17
|
import { localDate } from "../../shared/time.js";
|
|
19
18
|
import { MIN_CONTRAST, contrastWithWhite } from "../../ui/shared/tutor-color.js";
|
|
20
19
|
import { memoryPath, missingEntry, pickNotes } from "../../memory/vault-notes.js";
|
|
21
|
-
import { SKILLS_DIR, isApiRuntime, listTutors, mouthVoice, resolvePolicy, tutorFilePath } from "../../schema/index.js";
|
|
20
|
+
import { SKILLS_DIR, cardVoice, isApiRuntime, listTutors, mouthVoice, resolvePolicy, tutorFilePath } from "../../schema/index.js";
|
|
22
21
|
import { kindsFor, tutorCardKinds } from "../../board/cards/index.js";
|
|
23
22
|
import { scanVault } from "../../memory/vault.js";
|
|
24
23
|
import { parseArtifactEvents } from "../../runtime/journal/ledger.js";
|
|
@@ -26,15 +25,23 @@ import { parseTimetable } from "../../memory/timetable.js";
|
|
|
26
25
|
import { configGapsOf, legacyLayout } from "./migrate.js";
|
|
27
26
|
import { httpsFiles } from "./serve.js";
|
|
28
27
|
import { readPin } from "../http/access.js";
|
|
28
|
+
import { BUILD } from "../build.js";
|
|
29
|
+
import { isNewer, upgradeSteps } from "../../shared/upgrade.js";
|
|
29
30
|
import { gradeOfSemester } from "../../memory/grade.js";
|
|
30
31
|
import { DIRS } from "./skeleton.js";
|
|
31
32
|
import { tutorStatuses, readManifest } from "./tutors.js";
|
|
32
|
-
import { tokenText } from "../../shared/tokens.js";
|
|
33
33
|
import { workspaceGradePack } from "./grades.js";
|
|
34
34
|
import { CONFIG_FILE, ConfigError, USER_CONFIG, UsageError, assembleWorkspace, expandPath, parseConfig, readJson, redactHome, resolveRoot, } from "../workspace.js";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
35
|
+
/** 每条不过的要么能跑(cmd)要么要人来(human),没有第三种:新加的检查项忘了写 cmd,也只是交给人,不会让 agent 瞎猜 */
|
|
36
|
+
function finish(report) {
|
|
37
|
+
for (const c of report.checks) {
|
|
38
|
+
if (c.ok)
|
|
39
|
+
delete c.cmd;
|
|
40
|
+
else if (!c.cmd)
|
|
41
|
+
c.human = true;
|
|
42
|
+
}
|
|
43
|
+
return report;
|
|
44
|
+
}
|
|
38
45
|
async function statOrNull(p) {
|
|
39
46
|
return stat(p).catch(() => null);
|
|
40
47
|
}
|
|
@@ -46,113 +53,19 @@ async function evictedCount(dir) {
|
|
|
46
53
|
return 0;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
|
-
/** 运行时的 CLI 名(run[0] 的 basename) */
|
|
50
|
-
export function runtimeCli(run) {
|
|
51
|
-
return basename(run[0] ?? '');
|
|
52
|
-
}
|
|
53
|
-
/** 已知的环境坑,从 CLI 的输出里认出来给修复命令(错误信息即修复指南) */
|
|
54
|
-
export function explainLlmFailure(text, runtime) {
|
|
55
|
-
const bin = runtimeCli(runtime);
|
|
56
|
-
if (/does not support this model|version [\d.]+ or newer is required/i.test(text)) {
|
|
57
|
-
return `这版 ${bin} 不认模型:检查 cotutor.json 的运行时 run / resume 中 -m 后的模型名,或运行 ${bin} update 升级`;
|
|
58
|
-
}
|
|
59
|
-
if (/403|request not allowed/i.test(text))
|
|
60
|
-
return `API 拒了(403):检查 ${bin} 使用的千问 key、端点与模型权限`;
|
|
61
|
-
if (/not logged in|login|authentication|401|unauthorized/i.test(text))
|
|
62
|
-
return `${bin} 的 key 无效:检查 QWENCLOUD_TOKEN_PLAN_API_KEY 或 DASHSCOPE_API_KEY,再跑 cotutor doctor`;
|
|
63
|
-
if (/ENOENT|command not found/i.test(text))
|
|
64
|
-
return `PATH 里没有 ${bin}`;
|
|
65
|
-
return `看上面的原文;不认识的错先在终端手跑一次同样的命令`;
|
|
66
|
-
}
|
|
67
56
|
/**
|
|
68
|
-
* --live
|
|
69
|
-
* 花一分钱级别的费用,所以缺省不跑。配音同理:老师配了 voice 就合成一句。
|
|
57
|
+
* --live:老师用的是 API 运行时,不在这里真起一轮(真跑一轮用 cotutor send);配音照真合成一句(老师配了 voice 的),花一分钱级别,所以缺省不跑。
|
|
70
58
|
*/
|
|
71
59
|
async function probeLive(ws, push, env) {
|
|
72
|
-
const { spawn } = await import('node:child_process');
|
|
73
60
|
const { listTutors } = await import("../../schema/index.js");
|
|
74
61
|
const { synthesize } = await import("../../media/tts.js");
|
|
75
|
-
const { parseTranscript } = await import("../../brain/transcript.js");
|
|
76
|
-
const { parseAgentFile } = await import("../../runtime/agent-file.js");
|
|
77
62
|
const { tmpdir } = await import('node:os');
|
|
78
63
|
const { mkdtemp, rm } = await import('node:fs/promises');
|
|
79
64
|
const tutors = listTutors(ws.config).filter((t) => t.enabled);
|
|
80
|
-
const first = tutors[0];
|
|
81
|
-
if (!first) {
|
|
82
|
-
push({ name: 'live.agent', ok: false, required: false, detail: '没有开着的老师,没法探', fix: 'cotutor.json 里至少开一位' });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
65
|
const runtimeName = ws.config.runtimes.default;
|
|
86
66
|
const runtime = ws.config.runtimes[runtimeName];
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
if (isApiRuntime(runtime)) {
|
|
90
|
-
push({ name: `live.${runtimeName}`, ok: true, required: false, detail: `API 运行时(百炼 ${runtime.model}),不起 CLI;真跑一轮用 cotutor send` });
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (runtimeIssue(runtime))
|
|
94
|
-
return; // 静态体检已报退役,不启动旧 CLI
|
|
95
|
-
let agentBody;
|
|
96
|
-
try {
|
|
97
|
-
agentBody = parseAgentFile(await readFile(tutorFilePath(ws.dirs.agents, first.name), 'utf8')).body;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
/* 上面 tutor.* 已报 */
|
|
101
|
-
}
|
|
102
|
-
let boardBody;
|
|
103
|
-
try {
|
|
104
|
-
boardBody = boardGuideBody(await readFile(join(ws.root, BOARD_GUIDE_PATH), 'utf8'));
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
/* skills.* 已报 */
|
|
108
|
-
}
|
|
109
|
-
const systemBody = agentBody !== undefined ? (boardBody && takesTutorRules(first.name) ? `${agentBody}\n\n${boardBody}` : agentBody) : undefined;
|
|
110
|
-
// 和真跑同一条规划:命令行、stdin 那行、环境变量都由这个运行时的 CLI 适配器定
|
|
111
|
-
const plan = planRun(ws.config, { session: null }, { agent: first.name, prompt: LIVE_PROMPT, agentBody, systemBody, boardFile: join(ws.root, BOARD_GUIDE_PATH), runtime: runtimeName, root: ws.root, readDirs: [...new Set([ws.root, ws.paths.vault])] });
|
|
112
|
-
const { argv } = plan;
|
|
113
|
-
const parse = parserOf(plan.cli);
|
|
114
|
-
await prepareCli(plan.cli, ws.root);
|
|
115
|
-
const cwd = join(ws.dirs.agents, first.name);
|
|
116
|
-
const run = await new Promise((resolveRun) => {
|
|
117
|
-
let out = '';
|
|
118
|
-
let err = '';
|
|
119
|
-
const child = spawn(argv[0], argv.slice(1), { cwd, env: processEnv(plan, { ...env, COTUTOR_WORKSPACE: ws.root }), stdio: [plan.stdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
|
|
120
|
-
const timer = setTimeout(() => child.kill(), 120_000);
|
|
121
|
-
// 消息走 stdin 的运行时:写一条,看到收尾就关 stdin
|
|
122
|
-
if (plan.stdin) {
|
|
123
|
-
child.stdin?.on('error', () => { });
|
|
124
|
-
child.stdin?.write(plan.stdin);
|
|
125
|
-
}
|
|
126
|
-
let rest = '';
|
|
127
|
-
child.stdout?.on('data', (d) => {
|
|
128
|
-
out += d.toString();
|
|
129
|
-
rest += d.toString();
|
|
130
|
-
const lines = rest.split('\n');
|
|
131
|
-
rest = lines.pop() ?? '';
|
|
132
|
-
if (plan.stdin && lines.some((l) => parse(l).some((e) => e.kind === 'result')))
|
|
133
|
-
child.stdin?.end();
|
|
134
|
-
});
|
|
135
|
-
child.stderr?.on('data', (d) => (err += d.toString()));
|
|
136
|
-
child.once('error', (e) => {
|
|
137
|
-
clearTimeout(timer);
|
|
138
|
-
resolveRun({ out, err, code: null, spawnErr: e.message });
|
|
139
|
-
});
|
|
140
|
-
child.once('close', (code) => {
|
|
141
|
-
clearTimeout(timer);
|
|
142
|
-
resolveRun({ out, err, code });
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
const t = parseTranscript(run.out, parse);
|
|
146
|
-
const said = t.items.filter((i) => i.kind === 'text' && !i.sub).map((i) => i.text).join(' ');
|
|
147
|
-
const ok = Boolean(t.final?.ok) && !run.spawnErr;
|
|
148
|
-
const raw = run.spawnErr ?? (t.final ? `${t.final.reason ?? ''} ${said}`.trim() : `没有 result 事件(exit ${run.code}):${run.err.trim().split('\n').slice(-3).join(' / ')}`);
|
|
149
|
-
push({
|
|
150
|
-
name: `live.${runtimeName}`,
|
|
151
|
-
ok,
|
|
152
|
-
required: false,
|
|
153
|
-
detail: ok ? `${first.display} 回了「${(t.final?.text ?? '').slice(0, 20)}」${t.final?.inTokens !== undefined ? ` · ${tokenText({ in: t.final.inTokens, out: t.final.outTokens ?? 0 })}` : ''}` : `起不来或没答上:${raw.slice(0, 300)}`,
|
|
154
|
-
fix: ok ? undefined : explainLlmFailure(raw, runtime.run),
|
|
155
|
-
});
|
|
67
|
+
if (runtime && typeof runtime !== 'string' && isApiRuntime(runtime))
|
|
68
|
+
push({ name: `live.${runtimeName}`, ok: true, required: false, detail: `API 运行时(百炼 ${runtime.model}),不起老师;真跑一轮用 cotutor send` });
|
|
156
69
|
const voiced = tutors.find((x) => x.voice);
|
|
157
70
|
if (!voiced) {
|
|
158
71
|
push({ name: 'live.tts', ok: true, required: false, detail: '没有老师配 voice,不探配音(点读、单词卡没有声音)' });
|
|
@@ -163,7 +76,7 @@ async function probeLive(ws, push, env) {
|
|
|
163
76
|
const r = await synthesize({ text: '你好', voice: voiced.voice }, { cacheDir: dir, env });
|
|
164
77
|
const size = r.file ? ((await stat(r.file).catch(() => null))?.size ?? 0) : 0;
|
|
165
78
|
await rm(dir, { recursive: true, force: true });
|
|
166
|
-
push({ name: 'live.tts', ok: Boolean(r.file), required: false, detail: r.file ? `${voiced.display} 的音色 ${voiced.voice} 能合成(${size} 字节)` : `${voiced.display} 的音色 ${voiced.voice} 没合成:${r.error}`, fix: r.file ? undefined : '家长端「首页」里填百炼的 key 与业务空间 id
|
|
79
|
+
push({ name: 'live.tts', ok: Boolean(r.file), required: false, detail: r.file ? `${voiced.display} 的音色 ${voiced.voice} 能合成(${size} 字节)` : `${voiced.display} 的音色 ${voiced.voice} 没合成:${r.error}`, fix: r.file ? undefined : '家长端「首页」里填百炼的 key 与业务空间 id;音色到家长端首页的「样子与声音」重挑' });
|
|
167
80
|
}
|
|
168
81
|
export async function doctorWorkspace(override, opts = {}) {
|
|
169
82
|
const probeEnv = opts.probeEnv ?? true;
|
|
@@ -188,9 +101,14 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
188
101
|
detail: err.message,
|
|
189
102
|
fix: err instanceof ConfigError ? `修复 ${redactHome(err.file)} 的 JSON,或删掉它` : 'cotutor init <slug>,或用 --workspace / COTUTOR_WORKSPACE 指定',
|
|
190
103
|
});
|
|
191
|
-
return { ok: false, root: null, source: null, checks };
|
|
104
|
+
return finish({ ok: false, root: null, source: null, checks });
|
|
192
105
|
}
|
|
193
106
|
push({ name: 'workspace', ok: true, required: true, detail: `${redactHome(root)}(来源:${source})` });
|
|
107
|
+
// cmd 要能照原样跑:体检时用 --workspace 指的,修的时候也得指
|
|
108
|
+
// 路径写 $HOME/…,和别的字段一样:用户名不传给 agent 的厂商;redactDeep 只换开头,管不到命令中间的路径;sh 与 PowerShell 都展开
|
|
109
|
+
const quoted = `"${redactHome(root)}"`;
|
|
110
|
+
const flag = source === 'flag' ? ` --workspace ${quoted}` : '';
|
|
111
|
+
const upgrade = `cotutor upgrade${flag}`;
|
|
194
112
|
// ---- cotutor.json ----
|
|
195
113
|
let ws = null;
|
|
196
114
|
const configFile = join(root, CONFIG_FILE);
|
|
@@ -216,12 +134,20 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
216
134
|
push({ name: CONFIG_FILE, ok: false, required: true, detail: err.message, fix: '按上面逐条修;政策文件机器不重建,改坏了靠 git 回退' });
|
|
217
135
|
}
|
|
218
136
|
let teachingPack = null;
|
|
137
|
+
// 补缺的 init:slug 从配置来;年级照已装的写(init 不切换年级);建在缺省位置的不用写 --dir
|
|
138
|
+
const initCmd = () => {
|
|
139
|
+
if (!ws)
|
|
140
|
+
return undefined;
|
|
141
|
+
const slug = ws.config.kid.slug;
|
|
142
|
+
const grade = teachingPack ? ` --grade ${teachingPack.grade}` : '';
|
|
143
|
+
return `cotutor init ${slug}${grade}${root === join(homedir(), 'cotutor', slug) ? '' : ` --dir ${quoted}`}`;
|
|
144
|
+
};
|
|
219
145
|
try {
|
|
220
146
|
const manifest = await readManifest(root);
|
|
221
147
|
teachingPack = await workspaceGradePack(root);
|
|
222
148
|
push({ name: 'teachingPack', ok: true, required: true, detail: `${teachingPack.grade} 年级教学包 · ${redactHome(teachingPack.dir)}` });
|
|
223
149
|
if (!manifest.teachingPack)
|
|
224
|
-
push({ name: 'teachingPack.origin', ok: false, required: false, detail: '旧版出厂内容按二年级来源读取,尚未补记;孩子档案不变', fix: 'cotutor upgrade 补记来源,保留自定义文件与原 hash' });
|
|
150
|
+
push({ name: 'teachingPack.origin', ok: false, required: false, detail: '旧版出厂内容按二年级来源读取,尚未补记;孩子档案不变', fix: 'cotutor upgrade 补记来源,保留自定义文件与原 hash', cmd: upgrade });
|
|
225
151
|
}
|
|
226
152
|
catch (err) {
|
|
227
153
|
push({ name: 'teachingPack', ok: false, required: true, detail: err.message, fix: '检查 .cotutor/shipped.json 的 teachingPack 与本包 grades/;恢复原来源记录或安装包含该年级的版本,不要改成其他年级绕过' });
|
|
@@ -235,6 +161,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
235
161
|
required: false,
|
|
236
162
|
detail: gaps.length ? `${gaps.length} 项要改:${gaps.map((g) => g.detail).join(';')}` : 'cotutor.json 有出厂模板里的全部老师、运行时与旗标,也没有不再用的段',
|
|
237
163
|
fix: gaps.length ? 'cotutor upgrade --config --dry-run 先看,再 cotutor upgrade --config 改(只加缺的、删不再用的出厂段,你改过的值不动)' : undefined,
|
|
164
|
+
cmd: gaps.length ? `cotutor upgrade --config${flag}` : undefined,
|
|
238
165
|
});
|
|
239
166
|
}
|
|
240
167
|
// ---- 骨架 ----
|
|
@@ -248,17 +175,18 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
248
175
|
required: true,
|
|
249
176
|
detail: missing.length ? `缺 ${missing.join(' ')}` : [...DIRS].map((d) => `${d}/`).join(' '),
|
|
250
177
|
fix: missing.length ? `cotutor init <slug> --dir ${redactHome(root)}(幂等补缺)` : undefined,
|
|
178
|
+
cmd: missing.length ? initCmd() : undefined,
|
|
251
179
|
});
|
|
252
180
|
// 老布局(2026-10-30 之前):老师文件在 .claude/agents/、技能在 .claude/skills/,现在都读不到了
|
|
253
181
|
const legacy = await legacyLayout(root);
|
|
254
182
|
if (legacy.length)
|
|
255
|
-
push({ name: 'layout', ok: false, required: true, detail: `${legacy.join(' ')} 是老位置:老师文件现在在 agents/<老师>.md,技能在 ${SKILLS_DIR}/`, fix: 'cotutor upgrade(原样挪过去,不改内容;新位置已有同名的不挪、会点名)' });
|
|
183
|
+
push({ name: 'layout', ok: false, required: true, detail: `${legacy.join(' ')} 是老位置:老师文件现在在 agents/<老师>.md,技能在 ${SKILLS_DIR}/`, fix: 'cotutor upgrade(原样挪过去,不改内容;新位置已有同名的不挪、会点名)', cmd: upgrade });
|
|
256
184
|
// 家规(根的 QWEN.md)2026-09-15 起不出厂:有就是家长自建的共同规矩,提一句;没有不缺
|
|
257
185
|
const rulesThere = (await Promise.all(['QWEN.md'].map(async (f) => ((await statOrNull(join(root, f))) ? f : null)))).filter(Boolean);
|
|
258
186
|
push({ name: 'rules', ok: true, required: false, detail: rulesThere.length ? `家长自建的家规 ${rulesThere.join(' ')} 在,所有老师每轮都读` : '没有根目录的 QWEN.md(不需要:老师要知道的都在老师文件与技能里)' });
|
|
259
187
|
if (ws) {
|
|
260
188
|
// ---- 老师:定义文件(拷贝)+ 名字一致 + 出厂 / 自定义状态 + 老师目录 ----
|
|
261
|
-
// 老师文件只有 agents/<name>.md 这一份:应用读正文递进系统提示
|
|
189
|
+
// 老师文件只有 agents/<name>.md 这一份:应用读正文递进系统提示
|
|
262
190
|
const statuses = new Map((teachingPack ? await tutorStatuses(root) : []).map((s) => [s.name, s]));
|
|
263
191
|
for (const name of Object.keys(ws.config.tutors)) {
|
|
264
192
|
const shipped = statuses.has(name);
|
|
@@ -283,6 +211,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
283
211
|
: shipped
|
|
284
212
|
? `cotutor init 补拷(拷自本包 agents/${name}.md),或把老师键改成文件里的 name`
|
|
285
213
|
: `这是自家加的老师:写 agents/${name}.md(frontmatter name: ${name}),cotutor add ${name} --display <显示名> 可出模板;或把 cotutor.json 里这条删掉`,
|
|
214
|
+
cmd: !ok && shipped ? initCmd() : undefined,
|
|
286
215
|
});
|
|
287
216
|
const st = statuses.get(name);
|
|
288
217
|
if (!st)
|
|
@@ -295,15 +224,16 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
295
224
|
required: false,
|
|
296
225
|
detail: label[st.state],
|
|
297
226
|
fix: st.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined,
|
|
227
|
+
cmd: st.state === 'upgradable' ? upgrade : undefined,
|
|
298
228
|
});
|
|
299
229
|
}
|
|
300
230
|
const home = join(ws.dirs.agents, name);
|
|
301
231
|
const there = (await statOrNull(home))?.isDirectory() ?? false;
|
|
302
|
-
push({ name: `tutor.${name}.home`, ok: there, required: true, detail: there ? `agents/${name}/ 在(会话 cwd)` : `agents/${name}/ 不在`, fix: there ? undefined : 'cotutor init 补建' });
|
|
232
|
+
push({ name: `tutor.${name}.home`, ok: there, required: true, detail: there ? `agents/${name}/ 在(会话 cwd)` : `agents/${name}/ 不在`, fix: there ? undefined : 'cotutor init 补建', cmd: there ? undefined : initCmd() });
|
|
303
233
|
// 音色:一老师一音色走 API 是正路;没配的孩子端只剩浏览器合成声(只该在测试里);hidden 的老师不对孩子说话,不用音色
|
|
304
234
|
const tc = ws.config.tutors[name];
|
|
305
|
-
if (tc.enabled && !tc.hidden && !
|
|
306
|
-
push({ name: `tutor.${name}.voice`, ok: false, required: false, detail: `${tc.display} 没配音色(cotutor.json tutors.${name}.voice),卡上的声音(点读卡、单词卡)配不了——只适合测试`, fix: '
|
|
235
|
+
if (tc.enabled && !tc.hidden && !cardVoice(ws.config.tutors, name))
|
|
236
|
+
push({ name: `tutor.${name}.voice`, ok: false, required: false, detail: `${tc.display} 没配音色(cotutor.json tutors.${name}.voice),卡上的声音(点读卡、单词卡)配不了——只适合测试`, fix: '家长端「首页」点这位老师的「样子与声音」挑一个' });
|
|
307
237
|
// 卡的清单(《卡片协议.md》「谁拿到哪些卡」):有脸的老师才递板书写法;只是报一声讲哪几种
|
|
308
238
|
if (takesTutorRules(name)) {
|
|
309
239
|
const kinds = tutorCardKinds(tc.cards);
|
|
@@ -317,6 +247,36 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
317
247
|
push({ name: `tutor.${name}.avatar`, ok: there, required: false, detail: there ? `头像 ${tc.avatar}` : inside ? `头像 ${tc.avatar} 不在,孩子端退回显示首字` : `头像 ${tc.avatar} 在 workspace 根之外,页面不给`, fix: there ? undefined : `figshot pick --workspace ${redactHome(root)} 存一张,或把 cotutor.json 的 tutors.${name}.avatar 改回 emoji` });
|
|
318
248
|
}
|
|
319
249
|
}
|
|
250
|
+
if (process.platform === 'win32' && (opts.firewall ?? probeEnv)) {
|
|
251
|
+
// 平板连不连得进来:Windows 只在第一次监听时问一次「允许访问」,点了取消就再也连不上(firewall.ts)
|
|
252
|
+
const { judgeFirewall, probeFirewall } = await import("./firewall.js");
|
|
253
|
+
const port = opts.port ?? ws.config.server.port;
|
|
254
|
+
const state = await probeFirewall(port);
|
|
255
|
+
if (state) {
|
|
256
|
+
const v = judgeFirewall(state, port, process.execPath);
|
|
257
|
+
push({ name: 'firewall', ok: v.ok, required: false, detail: v.detail, fix: v.fix });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (probeEnv) {
|
|
261
|
+
// 端口起不起得来:被别的程序占了,后台服务只在日志里写一句就不起了(serve 自己要绑,它起来时不查这项)
|
|
262
|
+
const { probeHealth, realHost } = await import("./service.js");
|
|
263
|
+
const { portHolder, portState } = await import("./port.js");
|
|
264
|
+
const port = opts.port ?? ws.config.server.port;
|
|
265
|
+
const fix = '腾出这个端口;或改 cotutor.json 的 server.port,平板要重新扫码、重加主屏幕图标';
|
|
266
|
+
if (await probeHealth(port))
|
|
267
|
+
push({ name: 'server.port', ok: true, required: false, detail: `端口 ${port} 上 cotutor 在答应` });
|
|
268
|
+
else {
|
|
269
|
+
const state = await portState(port);
|
|
270
|
+
if (state === 'free')
|
|
271
|
+
push({ name: 'server.port', ok: true, required: false, detail: `端口 ${port} 空着,起得来` });
|
|
272
|
+
else if (state === 'denied')
|
|
273
|
+
push({ name: 'server.port', ok: false, required: false, detail: `系统不让这个用户监听端口 ${port},服务起不来`, fix });
|
|
274
|
+
else {
|
|
275
|
+
const p = await portHolder(realHost(), port);
|
|
276
|
+
push({ name: 'server.port', ok: false, required: false, detail: `端口 ${port} 被占着${p ? `:${p.name}(pid ${p.pid})在用` : ''},服务起不来`, fix });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
320
280
|
{
|
|
321
281
|
// 舞台包:小课堂 / 画板卡要它;没有只是重卡打不开,轻卡与对话照常。做课包(cotutor chalk bake / snap)与服务端烤画面也开它,在本机的 Chrome 里
|
|
322
282
|
const { stageBuilt } = await import("../http/stage.js");
|
|
@@ -324,15 +284,15 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
324
284
|
push({ name: 'stage.bundle', ok: built, required: false, detail: built ? 'dist/stage/ 在(舞台包:小课堂与画板卡的舞台)' : 'dist/stage/ 不在,小课堂与画板卡的舞台打不开(轻卡照常)', fix: built ? undefined : '仓库根 pnpm run build:stage(npm 装的包自带)' });
|
|
325
285
|
const { chromePath } = await import("./chalk/browser.js");
|
|
326
286
|
const chrome = chromePath(env);
|
|
327
|
-
push({ name: 'chalk.chrome', ok: chrome !== null, required: false, detail: chrome ?
|
|
287
|
+
push({ name: 'chalk.chrome', ok: chrome !== null, required: false, detail: chrome ? `本机的浏览器在(${redactHome(chrome)}):做课包截图、烤画面用它` : '找不到 Chrome:cotutor chalk bake / snap 做不了,没烤过的课包孩子端在浏览器里现画(慢、iPad 上可能不顺)', fix: chrome ? undefined : '装 Google Chrome,或环境变量 CHROME 指到 Chrome / Chromium / Edge 的可执行文件' });
|
|
328
288
|
const { CLAUDE_SKILLS_LINK, skillStatuses } = await import("./skills.js");
|
|
329
289
|
for (const sk of teachingPack ? await skillStatuses(root) : []) {
|
|
330
290
|
const label = { latest: sk.machine ? '机器件,最新' : '出厂件,最新', upgradable: sk.machine ? '机器件,和包里不一样(改过或包已更新)' : `出厂件,基于 ${sk.basedOn},包已更新`, custom: `自定义(基于 ${sk.basedOn})`, untracked: '自定义(没有出厂记录)', missing: sk.machine ? '缺(机器件,老师或家长的技能靠它)' : '缺' };
|
|
331
|
-
push({ name: `skill.${sk.name}`, ok: sk.state !== 'missing' && sk.state !== 'upgradable', required: Boolean(sk.machine), detail: `${SKILLS_DIR}/${sk.name}/:${label[sk.state]}`, fix: sk.state === 'missing' ? 'cotutor init 补拷' : sk.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined });
|
|
291
|
+
push({ name: `skill.${sk.name}`, ok: sk.state !== 'missing' && sk.state !== 'upgradable', required: Boolean(sk.machine), detail: `${SKILLS_DIR}/${sk.name}/:${label[sk.state]}`, fix: sk.state === 'missing' ? 'cotutor init 补拷' : sk.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined, cmd: sk.state === 'missing' ? initCmd() : sk.state === 'upgradable' ? upgrade : undefined });
|
|
332
292
|
}
|
|
333
293
|
// Claude Code 只认 .claude/skills:那里该是指向 .agents/skills 的链;老师用不到,家长在 workspace 里用 Claude Code 才要
|
|
334
294
|
const linked = (await lstat(join(root, CLAUDE_SKILLS_LINK)).catch(() => null))?.isSymbolicLink() ?? false;
|
|
335
|
-
push({ name: 'skills.claude', ok: linked, required: false, detail: linked ? `${CLAUDE_SKILLS_LINK} 链到 ${SKILLS_DIR}(Claude Code 读的是同一份)` : `${CLAUDE_SKILLS_LINK} 不是链,在 workspace 里用 Claude Code 看不到 ${SKILLS_DIR}/ 的技能`, fix: linked ? undefined : 'cotutor upgrade 建链(是实体目录的话先把里面的挪进 .agents/skills/ 再删掉)' });
|
|
295
|
+
push({ name: 'skills.claude', ok: linked, required: false, detail: linked ? `${CLAUDE_SKILLS_LINK} 链到 ${SKILLS_DIR}(Claude Code 读的是同一份)` : `${CLAUDE_SKILLS_LINK} 不是链,在 workspace 里用 Claude Code 看不到 ${SKILLS_DIR}/ 的技能`, fix: linked ? undefined : 'cotutor upgrade 建链(是实体目录的话先把里面的挪进 .agents/skills/ 再删掉)', cmd: linked ? undefined : upgrade });
|
|
336
296
|
// ---- activity:老师的家里 agents/<老师>/activities/<名>.md;出厂的看新旧,每份都过一遍 activity check ----
|
|
337
297
|
{
|
|
338
298
|
const { activityStatuses, checkActivities } = await import("./activities.js");
|
|
@@ -340,7 +300,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
340
300
|
const label = { latest: '出厂件,最新', upgradable: '出厂件,包已更新', custom: '自定义', untracked: '自定义(没有出厂记录)', missing: '缺', broken: '读不到' };
|
|
341
301
|
for (const s of states.values())
|
|
342
302
|
if (s.state === 'missing')
|
|
343
|
-
push({ name: `activity.${s.key}`, ok: false, required: false, detail: `agents/${s.key.replace('/', '/activities/')}.md 不在`, fix: 'cotutor init 补拷' });
|
|
303
|
+
push({ name: `activity.${s.key}`, ok: false, required: false, detail: `agents/${s.key.replace('/', '/activities/')}.md 不在`, fix: 'cotutor init 补拷', cmd: initCmd() });
|
|
344
304
|
// 学科色:手改 cotutor.json 写的颜色,新话题气泡的白字要压得住
|
|
345
305
|
for (const [name, t] of Object.entries(ws.config.tutors)) {
|
|
346
306
|
if (!t.color)
|
|
@@ -364,6 +324,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
364
324
|
required: false,
|
|
365
325
|
detail: fixes.length ? `${origin};${fixes.length} 条要改,第一条:${fixes[0].line ? `第 ${fixes[0].line} 行 ` : ''}${fixes[0].text}` : origin,
|
|
366
326
|
fix: fixes.length ? `cotutor activity check ${r.tutor} ${r.name} 看全` : st?.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined,
|
|
327
|
+
cmd: !fixes.length && st?.state === 'upgradable' ? upgrade : undefined,
|
|
367
328
|
});
|
|
368
329
|
}
|
|
369
330
|
}
|
|
@@ -377,7 +338,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
377
338
|
push({ name: 'theme.manifest', ok: true, required: false, detail: `themes/${name}/:${Object.keys(t.manifest.tints).length} 个底色槽、${Object.keys(t.manifest.looks).length} 个字形槽、${Object.keys(t.manifest.pens).length} 支笔,default = ${t.manifest.default}` });
|
|
378
339
|
}
|
|
379
340
|
catch (err) {
|
|
380
|
-
push({ name: 'theme.manifest', ok: false, required: false, detail: `themes/${name}/ 用不了(服务退回出厂 default):${err instanceof Error ? err.message : String(err)}`, fix: name === 'default' ? 'cotutor init 补拷(已有的不动)或 cotutor upgrade' : `修 themes/${name}/theme.json 与 kid.css,或把 cotutor.json 的 kid.theme 改回 default
|
|
341
|
+
push({ name: 'theme.manifest', ok: false, required: false, detail: `themes/${name}/ 用不了(服务退回出厂 default):${err instanceof Error ? err.message : String(err)}`, fix: name === 'default' ? 'cotutor init 补拷(已有的不动)或 cotutor upgrade' : `修 themes/${name}/theme.json 与 kid.css,或把 cotutor.json 的 kid.theme 改回 default`, cmd: name === 'default' ? initCmd() : undefined });
|
|
381
342
|
}
|
|
382
343
|
for (const th of teachingPack ? await themeStatuses(root) : []) {
|
|
383
344
|
if (!th.shipped) {
|
|
@@ -385,7 +346,7 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
385
346
|
continue;
|
|
386
347
|
}
|
|
387
348
|
const label = { latest: '出厂件,最新', upgradable: `出厂件,基于 ${th.basedOn},包已更新`, custom: `自定义(基于 ${th.basedOn})`, untracked: '自定义(没有出厂记录)', missing: '缺' };
|
|
388
|
-
push({ name: `theme.${th.name}.origin`, ok: th.state !== 'missing' && th.state !== 'upgradable', required: false, detail: `themes/${th.name}/:${label[th.state]}`, fix: th.state === 'missing' ? 'cotutor init 补拷' : th.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined });
|
|
349
|
+
push({ name: `theme.${th.name}.origin`, ok: th.state !== 'missing' && th.state !== 'upgradable', required: false, detail: `themes/${th.name}/:${label[th.state]}`, fix: th.state === 'missing' ? 'cotutor init 补拷' : th.state === 'upgradable' ? 'cotutor upgrade 换新版' : undefined, cmd: th.state === 'missing' ? initCmd() : th.state === 'upgradable' ? upgrade : undefined });
|
|
389
350
|
}
|
|
390
351
|
}
|
|
391
352
|
}
|
|
@@ -508,36 +469,24 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
508
469
|
}
|
|
509
470
|
// ---- 家长端与工作台的锁(src/app/http/access.ts):机器级 PIN;没设不算错,提一句 ----
|
|
510
471
|
const pin = await readPin();
|
|
511
|
-
push({ name: 'server.access', ok: pin !== null, required: false, detail: pin ? '
|
|
472
|
+
push({ name: 'server.access', ok: pin !== null, required: false, detail: pin ? '已设 PIN' : '家长端、工作台没上锁', fix: pin ? undefined : 'cotutor pin' });
|
|
512
473
|
// ---- HTTPS:iPad 上录音要;没有只提醒 ----
|
|
513
474
|
const tls = httpsFiles(ws);
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
for (const [name, t] of Object.entries(ws.config.tutors))
|
|
519
|
-
if (takesTutorRules(name) && t.enabled !== false)
|
|
520
|
-
used.add(t.runtime ?? ws.config.runtimes.default);
|
|
521
|
-
for (const name of used) {
|
|
522
|
-
const rt = ws.config.runtimes[name];
|
|
523
|
-
if (!rt || typeof rt === 'string')
|
|
524
|
-
continue;
|
|
525
|
-
if (isApiRuntime(rt)) {
|
|
526
|
-
push({ name: `runtime.${name}.board`, ok: true, required: false, detail: 'API 运行时:板书写法连老师正文一起进系统提示' });
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
const inRun = boardPreloaded({ run: rt.run, resume: rt.run });
|
|
530
|
-
const inResume = boardPreloaded({ run: rt.resume, resume: rt.resume });
|
|
531
|
-
const how = runtimeUses(rt, '{boardFile}') ? '{boardFile} 追加进系统提示' : runtimeUses(rt, '{systemBody}') ? '{systemBody} 连老师正文一起进系统提示' : '模板里写死的 SKILL.md 路径';
|
|
532
|
-
if (inRun !== inResume)
|
|
533
|
-
push({ name: `runtime.${name}.board`, ok: false, required: false, detail: `板书写法的预载只在 ${inRun ? 'run' : 'resume'} 模板里,两边不一致(压缩后系统提示按 resume 的旗标重建)`, fix: 'cotutor upgrade --config 补旗标,或手改 cotutor.json 让两条模板一致' });
|
|
534
|
-
else
|
|
535
|
-
push({ name: `runtime.${name}.board`, ok: true, required: false, detail: inRun ? `板书写法预载进系统提示(${how}),各话题共享缓存` : '板书写法由应用放进每个话题第一条(<cotutor-board>);这个 CLI 能从文件或参数收系统提示的话,模板里用 {boardFile} / {systemBody} 更省' });
|
|
536
|
-
}
|
|
537
|
-
}
|
|
475
|
+
const { mkcertHint, mkcertReady } = await import("./cert.js");
|
|
476
|
+
// mkcert -install 要开机密码或点弹窗,是人的事;做过了,签证书就不用人了
|
|
477
|
+
const canSign = !tls && probeEnv && await mkcertReady();
|
|
478
|
+
push({ name: 'https', ok: tls !== null, required: false, detail: tls ? `${redactHome(tls.cert)}` : '没有证书,serve 走 HTTP(平板、手机上按住说话不可用)', fix: tls ? undefined : `cotutor cert(需要 mkcert:${mkcertHint()}),签完 cotutor service restart`, cmd: canSign ? 'cotutor cert' : undefined });
|
|
538
479
|
// ---- 百炼:老师靠实时模型开口,没配孩子端打不开;--live 真试连一次(握手就挂,不花钱) ----
|
|
480
|
+
// key 只认 ~/.config/cotutor/(2026-11-02 起);shell 里还 export 着旧的,说一句免得以为配了
|
|
539
481
|
const bl = dashscopeAuth(env);
|
|
540
|
-
|
|
482
|
+
const oldEnv = env.DASHSCOPE_API_KEY ? '(环境变量 DASHSCOPE_API_KEY 已经不读了)' : '';
|
|
483
|
+
push({ name: 'bailian', ok: Boolean(bl), required: true, detail: bl ? `百炼按量 key …${bl.key.slice(-4)}、业务空间 ${bl.workspaceId}` : `没有百炼按量的 key 或业务空间 id:孩子端打不开(只出「老师们还没到」)${oldEnv}`, fix: bl ? undefined : '家长端「首页」里配(在这台电脑上开,或者先 cotutor pin 设家长密码),存在 ~/.config/cotutor/bailian.json;Token Plan 的 sk-sp- 不行' });
|
|
484
|
+
// Token Plan 只有开发机有,没有就不提
|
|
485
|
+
const plan = tokenPlanKey(env.HOME || homedir());
|
|
486
|
+
if (plan)
|
|
487
|
+
push({ name: 'token-plan', ok: isPlanKey(plan), required: false, detail: isPlanKey(plan) ? `Token Plan key …${plan.slice(-4)}:Token Plan 有的模型先走它` : 'token-plan.json 里的 key 不是 sk-sp- 开头,不认', fix: isPlanKey(plan) ? undefined : '换成千问 Token Plan 的 key,或删掉 ~/.config/cotutor/token-plan.json' });
|
|
488
|
+
else if (env.QWENCLOUD_TOKEN_PLAN_API_KEY)
|
|
489
|
+
push({ name: 'token-plan', ok: false, required: false, detail: '环境变量 QWENCLOUD_TOKEN_PLAN_API_KEY 已经不读了,全走百炼按量', fix: '写进 ~/.config/cotutor/token-plan.json:{"apiKey": "sk-sp-…"}' });
|
|
541
490
|
if (bl && opts.live) {
|
|
542
491
|
const { probeRealtime } = await import("../../realtime/preview.js");
|
|
543
492
|
const { probeMessage } = await import("../machine/bailian.js");
|
|
@@ -552,51 +501,72 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
552
501
|
{
|
|
553
502
|
const bad = Object.entries(lv.voices).filter(([, v]) => !isRealtimeVoice(v));
|
|
554
503
|
const who = listTutors(ws.config, { kidOnly: true }).map((t) => `${t.name} 用 ${mouthVoice(lv, t.name)}`).join('、') || '没有有脸的老师';
|
|
555
|
-
push({ name: 'live.voices', ok: !bad.length, required: false, detail: bad.length ? `${bad.map(([t, v]) => `${t} 的「${v}」`).join('、')}:这个模型不认,嘴会一直不出声` : `嘴的音色:${who}`, fix: bad.length ? '
|
|
504
|
+
push({ name: 'live.voices', ok: !bad.length, required: false, detail: bad.length ? `${bad.map(([t, v]) => `${t} 的「${v}」`).join('、')}:这个模型不认,嘴会一直不出声` : `嘴的音色:${who}`, fix: bad.length ? '家长端首页的「样子与声音」里换一个说话的声音,或改 cotutor.json 的 live.voices' : undefined });
|
|
556
505
|
}
|
|
557
|
-
//
|
|
506
|
+
// 旧配置里的命令行运行时(Qwen Code 等)读得进来,用不了:用到它的老师发不出去
|
|
558
507
|
const usedRuntimes = new Set([ws.config.runtimes.default, ...Object.values(ws.config.tutors).filter((t) => t.enabled).map((t) => t.runtime ?? ws.config.runtimes.default)]);
|
|
559
508
|
for (const [name, rt] of Object.entries(ws.config.runtimes)) {
|
|
560
509
|
if (typeof rt === 'string' || isApiRuntime(rt))
|
|
561
510
|
continue;
|
|
562
511
|
const issue = runtimeIssue(rt);
|
|
563
512
|
if (issue)
|
|
564
|
-
push({ name: `runtime.${name}`, ok: false, required: usedRuntimes.has(name), detail: issue, fix: '按 README.md 的「旧 workspace
|
|
513
|
+
push({ name: `runtime.${name}`, ok: false, required: usedRuntimes.has(name), detail: issue, fix: '按 README.md 的「旧 workspace 换运行时」说明改' });
|
|
565
514
|
}
|
|
566
|
-
// ----
|
|
515
|
+
// ---- 运行时:API 的走哪条线 ----
|
|
567
516
|
if (probeEnv) {
|
|
568
517
|
for (const [runtime, p] of Object.entries(ws.config.runtimes)) {
|
|
569
|
-
if (runtime === 'default' || typeof p === 'string')
|
|
570
|
-
continue;
|
|
571
|
-
if (isApiRuntime(p)) {
|
|
572
|
-
push({ name: `runtime.${runtime}`, ok: true, required: usedRuntimes.has(runtime), detail: `API 运行时(百炼 ${p.model}),不用装 CLI;凭证看 bailian 那条` });
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
if (runtimeIssue(p))
|
|
576
|
-
continue;
|
|
577
|
-
const bin = runtimeCli(p.run);
|
|
578
|
-
const required = usedRuntimes.has(runtime);
|
|
579
|
-
// 没有老师用的 CLI 运行时(新装缺省 api,Qwen Code 那两条是备用):不探,免得没装 qwen 的家长看到一串「起不来」
|
|
580
|
-
if (!required) {
|
|
581
|
-
push({ name: `runtime.${runtime}`, ok: true, required: false, detail: `备用(${bin}),没有老师用它;要用就把老师的 runtime 改成 ${runtime},再跑一次 doctor` });
|
|
518
|
+
if (runtime === 'default' || typeof p === 'string' || !isApiRuntime(p))
|
|
582
519
|
continue;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const { stdout } = await execFileP(bin, ['--version'], { timeout: 8000 });
|
|
586
|
-
const version = stdout.trim().split('\n')[0];
|
|
587
|
-
push({ name: `runtime.${runtime}`, ok: true, required, detail: `${bin} ${version}` });
|
|
588
|
-
// 这个 CLI 的适配器要多看的几样(qwen:版本、百炼 key)
|
|
589
|
-
for (const c of adapterFor(p).check?.({ version, env }) ?? [])
|
|
590
|
-
push({ name: `runtime.${runtime}.${c.name}`, ok: c.ok, required, detail: c.detail, fix: c.fix });
|
|
591
|
-
}
|
|
592
|
-
catch {
|
|
593
|
-
push({ name: `runtime.${runtime}`, ok: false, required, detail: `PATH 里没有 ${bin}`, fix: required ? `装 ${bin},或把 runtimes.default 改成装了的那个运行时` : undefined });
|
|
594
|
-
}
|
|
520
|
+
const via = compatibleLine(p.model, env)?.plan ? '走 Token Plan' : '走百炼按量';
|
|
521
|
+
push({ name: `runtime.${runtime}`, ok: true, required: usedRuntimes.has(runtime), detail: `API 运行时(${p.model},${via});凭证看 bailian 那条` });
|
|
595
522
|
}
|
|
596
523
|
}
|
|
597
524
|
}
|
|
598
525
|
if (ws && opts.live)
|
|
599
526
|
await probeLive(ws, push, env);
|
|
527
|
+
// ---- 后台服务(cotutor service):装了没有、写死的 node 与 bin 还在不在;接着电源会不会闲置睡眠(睡着时平板连不上) ----
|
|
528
|
+
if (probeEnv && root && (process.platform === 'darwin' || process.platform === 'linux' || process.platform === 'win32')) {
|
|
529
|
+
const { findService, realHost } = await import("./service.js");
|
|
530
|
+
const s = await findService({ ...realHost(), env }, root).catch(() => null);
|
|
531
|
+
if (s) {
|
|
532
|
+
// 菜单栏 / 任务栏图标:家长不开终端看状态靠它;家长自己关掉的不提
|
|
533
|
+
const { trayStatus, traySupported } = await import("./tray.js");
|
|
534
|
+
if (traySupported(process.platform)) {
|
|
535
|
+
const t = await trayStatus({ ...realHost(), env }).catch(() => null);
|
|
536
|
+
if (t && !t.off)
|
|
537
|
+
push({ name: 'tray', ok: t.installed, required: false, detail: t.installed ? `托盘图标装了${t.running ? ',在跑' : ',现在没在跑(点过退出,下次登录回来)'}` : '没装托盘图标:看状态只能开终端', fix: t.installed ? undefined : 'cotutor tray on', cmd: t.installed ? undefined : 'cotutor tray on' });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!s)
|
|
541
|
+
push({ name: 'service', ok: false, required: false, detail: '没装后台服务:在终端里跑的 cotutor serve,关了终端就停', fix: 'cotutor service install(登录后自己起来、崩了自己再起)', cmd: `cotutor service install --no-open${flag}` });
|
|
542
|
+
else {
|
|
543
|
+
const gone = s.args.slice(0, 2).filter((f) => !existsSync(f));
|
|
544
|
+
push({ name: 'service', ok: !gone.length, required: false, detail: gone.length ? `后台服务 ${s.label} 指的 ${gone.map((f) => redactHome(f)).join('、')} 不在了(换了 node 版本、包挪了地方)` : `后台服务 ${s.label}(cotutor service status 看在不在跑)`, fix: gone.length ? 'cotutor service install(照现在的 node 与包重写一遍)' : undefined, cmd: gone.length ? `cotutor service install${flag}` : undefined });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (probeEnv && process.platform === 'darwin') {
|
|
548
|
+
const { acSleepMinutes } = await import("./service.js");
|
|
549
|
+
const { execFile } = await import('node:child_process');
|
|
550
|
+
const text = await new Promise((resolve) => execFile('/usr/bin/pmset', ['-g', 'custom'], { timeout: 5000, encoding: 'utf8' }, (_e, out) => resolve(String(out ?? ''))));
|
|
551
|
+
const min = acSleepMinutes(text);
|
|
552
|
+
if (min !== null)
|
|
553
|
+
push({ name: 'mac.sleep', ok: min === 0, required: false, detail: min === 0 ? '接着电源时不闲置睡眠' : `接着电源 ${min} 分钟没人碰就睡,睡着时平板连不上(有通话时 cotutor 拦着它睡,别的时候拦不了)`, fix: min === 0 ? undefined : '系统设置 › 电池 › 选项,打开「显示器关闭时,防止接通电源的 Mac 自动进入睡眠」(台式 Mac 在 系统设置 › 节能);合盖照样睡' });
|
|
554
|
+
}
|
|
555
|
+
if (probeEnv && process.platform === 'win32') {
|
|
556
|
+
const { winAcSleepSeconds } = await import("./service-windows.js");
|
|
557
|
+
const { execFile } = await import('node:child_process');
|
|
558
|
+
const text = await new Promise((resolve) => execFile('powercfg.exe', ['/query', 'SCHEME_CURRENT', 'SUB_SLEEP', 'STANDBYIDLE'], { timeout: 5000, encoding: 'latin1', windowsHide: true }, (_e, out) => resolve(String(out ?? ''))));
|
|
559
|
+
const sec = winAcSleepSeconds(text);
|
|
560
|
+
if (sec !== null)
|
|
561
|
+
push({ name: 'windows.sleep', ok: sec === 0, required: false, detail: sec === 0 ? '接着电源时不闲置睡眠' : `接着电源 ${Math.round(sec / 60)} 分钟没人碰就睡,睡着时平板连不上(有通话时 cotutor 拦着它睡,别的时候拦不了)`, fix: sec === 0 ? undefined : '设置 › 系统 › 电源和电池 › 屏幕、睡眠和休眠超时,「插入电源时,使设备在以下时间后进入睡眠状态」选「从不」;合盖看「电源按钮和盖子」那一项' });
|
|
562
|
+
}
|
|
563
|
+
// ---- 有没有新版本:只看服务问来存下的那份,体检不上网;什么时候升级家长定(要重起服务,孩子可能在上课),所以没有 cmd ----
|
|
564
|
+
{
|
|
565
|
+
const { readUpdateRecord } = await import("../machine/update.js");
|
|
566
|
+
const u = await readUpdateRecord();
|
|
567
|
+
if (!u.off && isNewer(u.latest, BUILD.version))
|
|
568
|
+
push({ name: 'update', ok: false, required: false, detail: `有新版本 ${u.latest}(现在是 ${BUILD.version})`, fix: `挑孩子不用的时候,依次运行:${upgradeSteps().join(' ; ')}` });
|
|
569
|
+
}
|
|
600
570
|
// ---- 用户配置 ----
|
|
601
571
|
try {
|
|
602
572
|
const text = await readFile(USER_CONFIG, 'utf8');
|
|
@@ -611,7 +581,12 @@ export async function doctorWorkspace(override, opts = {}) {
|
|
|
611
581
|
catch {
|
|
612
582
|
push({ name: 'user-config', ok: true, required: false, detail: `未配置(可选):{"workspace": "…"} 写进 ${redactHome(USER_CONFIG)};cotutor init 会代劳` });
|
|
613
583
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
584
|
+
// sandbox(--sandbox 分出来的试用 workspace)随时能重新分,不要 git;说一句它是从哪分出来的
|
|
585
|
+
if (ws?.sandbox)
|
|
586
|
+
push({ name: 'sandbox', ok: true, required: false, detail: `试用 workspace:${ws.sandbox.forkedAt.slice(0, 10)} 从 ${redactHome(ws.sandbox.from)} 分出来;--sandbox --fresh 重新分` });
|
|
587
|
+
else {
|
|
588
|
+
const gitThere = (await statOrNull(join(root, '.git'))) !== null;
|
|
589
|
+
push({ name: 'git', ok: gitThere, required: false, detail: gitThere ? 'workspace是 git 仓' : 'workspace不在 git 里', fix: gitThere ? undefined : '建议 git init:老师记忆、账本、政策都是不可再生状态', cmd: gitThere ? undefined : `git init ${quoted}` });
|
|
590
|
+
}
|
|
591
|
+
return finish({ ok: checks.filter((c) => c.required).every((c) => c.ok), root, source, checks });
|
|
617
592
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 查到的样子;配置名是 Domain / Private / Public,规则上的 Any 覆盖全部 */
|
|
2
|
+
export interface FirewallState {
|
|
3
|
+
/** 连着的网络的类型(Get-NetConnectionProfile 的 NetworkCategory:Private / Public / DomainAuthenticated) */
|
|
4
|
+
networks: string[];
|
|
5
|
+
/** 防火墙开着的配置 */
|
|
6
|
+
enabled: string[];
|
|
7
|
+
/** 放行的规则(入站、允许、启用,按这个 node 或这个端口)各覆盖哪些配置,如 "Private"、"Private, Public"、"Any" */
|
|
8
|
+
allowed: string[];
|
|
9
|
+
/** 拦着这个 node 的规则覆盖哪些配置(家长在「允许访问」那一步点了取消,Windows 会建这种规则) */
|
|
10
|
+
blocked: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface FirewallVerdict {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
detail: string;
|
|
15
|
+
fix?: string;
|
|
16
|
+
}
|
|
17
|
+
/** 查一次的 PowerShell 脚本;node 的路径与端口写进去(单引号里的单引号写两个) */
|
|
18
|
+
export declare function firewallScript(node: string, port: number): string;
|
|
19
|
+
/** PowerShell 回的 JSON → 样子;一个元素的数组可能被拆成字符串,null 当空 */
|
|
20
|
+
export declare function parseFirewall(text: string): FirewallState | null;
|
|
21
|
+
/** 平板连不连得进来,怎么修 */
|
|
22
|
+
export declare function judgeFirewall(s: FirewallState, port: number, node: string): FirewallVerdict;
|
|
23
|
+
/** 真查一次(只在 Windows 上);查不了是 null */
|
|
24
|
+
export declare function probeFirewall(port: number, node?: string): Promise<FirewallState | null>;
|