arona-agent 1.0.9 → 1.1.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/README.md +3 -0
- package/README_en.md +3 -0
- package/bin/arona.mjs +3 -2
- package/package.json +1 -1
- package/src/doctor.ts +385 -0
package/README.md
CHANGED
package/README_en.md
CHANGED
package/bin/arona.mjs
CHANGED
|
@@ -14,8 +14,9 @@ const args = process.argv.slice(2);
|
|
|
14
14
|
|
|
15
15
|
const isSetup = args[0] === 'setup';
|
|
16
16
|
const isVoice = args[0] === 'voice';
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const isDoctor = args[0] === 'doctor';
|
|
18
|
+
const target = isSetup ? 'src/setup.ts' : isVoice ? 'src/voice_cli.ts' : isDoctor ? 'src/doctor.ts' : 'src/index.ts';
|
|
19
|
+
const passArgs = (isSetup || isVoice || isDoctor) ? args.slice(1) : args;
|
|
19
20
|
|
|
20
21
|
// tsx ships a CLI binary alongside the package. We prefer the local install.
|
|
21
22
|
const tsxBin = process.platform === 'win32'
|
package/package.json
CHANGED
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// arona doctor:独立环境自检命令(bin/arona.mjs 路由到本文件,不走 REPL/斜杠命令)。
|
|
2
|
+
// 颜色语义:绿 = 正常;黄 = 可略过(未配置/某功能不可用);红 = 可能影响使用。
|
|
3
|
+
// 注意:勿 import tts_provider / gpt_sovits_local——后者模块级 setInterval 会让本进程无法退出,
|
|
4
|
+
// GPT-SoVITS 配置在此处直接解析 ttsConfig["gpt-sovits"] 原始字段。
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import net from "net";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { config, settingsExist, PROJECT_ROOT } from "./config.ts";
|
|
11
|
+
import { t } from "./locale.ts";
|
|
12
|
+
import { AGENT_IDS, getMainAgent } from "./agent_registry.ts";
|
|
13
|
+
import { getVoiceId, getGptSovitsVoice } from "./voices.ts";
|
|
14
|
+
import { spawnCompat } from "./utils/spawn.ts";
|
|
15
|
+
|
|
16
|
+
type Level = "ok" | "warn" | "fail";
|
|
17
|
+
|
|
18
|
+
let failCount = 0;
|
|
19
|
+
let warnCount = 0;
|
|
20
|
+
|
|
21
|
+
function item(level: Level, text: string): void {
|
|
22
|
+
const mark = level === "ok" ? chalk.green("✓") : level === "warn" ? chalk.yellow("!") : chalk.red("✗");
|
|
23
|
+
if (level === "warn") warnCount++;
|
|
24
|
+
if (level === "fail") failCount++;
|
|
25
|
+
console.log(` ${mark} ${text}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function section(title: string): void {
|
|
29
|
+
console.log(`\n${chalk.bold(title)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ProbeResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
stdout: string;
|
|
35
|
+
stderr: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 运行一个命令并收集输出(超时/启动失败均视为不可用,不抛异常)。 */
|
|
39
|
+
function probe(bin: string, args: string[], timeoutMs: number): Promise<ProbeResult> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let proc;
|
|
42
|
+
try {
|
|
43
|
+
proc = spawnCompat(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
44
|
+
} catch {
|
|
45
|
+
resolve({ ok: false, stdout: "", stderr: "spawn failed" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let stdout = "";
|
|
49
|
+
let stderr = "";
|
|
50
|
+
let settled = false;
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
if (!settled) {
|
|
53
|
+
settled = true;
|
|
54
|
+
try { proc.kill("SIGKILL"); } catch {}
|
|
55
|
+
resolve({ ok: false, stdout, stderr: "timeout" });
|
|
56
|
+
}
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
proc.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
59
|
+
proc.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
60
|
+
proc.on("error", (err) => {
|
|
61
|
+
if (!settled) {
|
|
62
|
+
settled = true;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve({ ok: false, stdout, stderr: err.message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
proc.on("close", (code) => {
|
|
68
|
+
if (!settled) {
|
|
69
|
+
settled = true;
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
resolve({ ok: code === 0, stdout, stderr });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tcpReachable(host: string, port: number, timeoutMs = 2000): Promise<boolean> {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const socket = new net.Socket();
|
|
80
|
+
let done = false;
|
|
81
|
+
const finish = (v: boolean) => {
|
|
82
|
+
if (!done) {
|
|
83
|
+
done = true;
|
|
84
|
+
socket.destroy();
|
|
85
|
+
resolve(v);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
socket.setTimeout(timeoutMs);
|
|
89
|
+
socket.once("connect", () => finish(true));
|
|
90
|
+
socket.once("timeout", () => finish(false));
|
|
91
|
+
socket.once("error", () => finish(false));
|
|
92
|
+
socket.connect(port, host);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 主程序 Python 的版本要求:3.12 / 3.13(3.14 起 pydantic-core 不支持)。 */
|
|
97
|
+
const PY_REQUIRE = "3.12 / 3.13";
|
|
98
|
+
|
|
99
|
+
function pyVersionFrom(out: string): string | null {
|
|
100
|
+
const m = out.match(/Python\s+(\d+\.\d+(?:\.\d+)?)/);
|
|
101
|
+
return m ? m[1] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 主程序 Python 依赖(requirements.txt):一次子进程探测全部 import。 */
|
|
105
|
+
const DEPS = ["cua", "websockets", "pyaudio", "numpy", "pynput", "dashscope"] as const;
|
|
106
|
+
|
|
107
|
+
const DEPS_SCRIPT = [
|
|
108
|
+
"import json",
|
|
109
|
+
`mods = ${JSON.stringify([...DEPS])}`,
|
|
110
|
+
"r = {}",
|
|
111
|
+
"for m in mods:",
|
|
112
|
+
" try:",
|
|
113
|
+
" __import__(m)",
|
|
114
|
+
" r[m] = 1",
|
|
115
|
+
" except Exception:",
|
|
116
|
+
" r[m] = 0",
|
|
117
|
+
"print(json.dumps(r))",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
interface GsvInfo {
|
|
121
|
+
mode: "local" | "cloud";
|
|
122
|
+
baseUrl: string;
|
|
123
|
+
/** 显式配置的 GPT-SoVITS 专用 Python;空 = 回退主程序 Python。 */
|
|
124
|
+
pythonPath: string;
|
|
125
|
+
apiScriptPath: string;
|
|
126
|
+
gptModelPath: string;
|
|
127
|
+
sovitsModelPath: string;
|
|
128
|
+
bertPath: string;
|
|
129
|
+
cnhubertPath: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 解析 ttsConfig["gpt-sovits"] 原始字段;无任何有效键 = 暂未配置。 */
|
|
133
|
+
function readGsvConfig(): GsvInfo | null {
|
|
134
|
+
const raw = config.ttsConfig?.["gpt-sovits"];
|
|
135
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
136
|
+
const obj = raw as Record<string, unknown>;
|
|
137
|
+
const s = (k: string) => (typeof obj[k] === "string" ? (obj[k] as string).trim() : "");
|
|
138
|
+
const configured = ["mode", "pythonPath", "apiScriptPath", "gptModelPath", "sovitsModelPath", "bertPath", "cnhubertPath", "baseUrl", "apiKey"].some((k) => s(k));
|
|
139
|
+
if (!configured) return null;
|
|
140
|
+
return {
|
|
141
|
+
mode: obj.mode === "cloud" ? "cloud" : "local",
|
|
142
|
+
baseUrl: s("baseUrl") || "http://127.0.0.1:9880",
|
|
143
|
+
pythonPath: s("pythonPath"),
|
|
144
|
+
apiScriptPath: s("apiScriptPath"),
|
|
145
|
+
gptModelPath: s("gptModelPath"),
|
|
146
|
+
sovitsModelPath: s("sovitsModelPath"),
|
|
147
|
+
bertPath: s("bertPath"),
|
|
148
|
+
cnhubertPath: s("cnhubertPath"),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseBaseUrl(baseUrl: string): { host: string; port: number } {
|
|
153
|
+
try {
|
|
154
|
+
const u = new URL(baseUrl);
|
|
155
|
+
return { host: u.hostname || "127.0.0.1", port: u.port ? Number(u.port) : 9880 };
|
|
156
|
+
} catch {
|
|
157
|
+
return { host: "127.0.0.1", port: 9880 };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function main(): Promise<void> {
|
|
162
|
+
console.log(chalk.bold("ARONA doctor"));
|
|
163
|
+
|
|
164
|
+
const mainPy = config.pythonPath;
|
|
165
|
+
const gsv = readGsvConfig();
|
|
166
|
+
const gsvPy = gsv?.pythonPath || mainPy;
|
|
167
|
+
|
|
168
|
+
// 并行探测:主程序 Python 版本 + 依赖;GPT-SoVITS Python 版本 + torch
|
|
169
|
+
const pyVerP = probe(mainPy, ["--version"], 10000);
|
|
170
|
+
const pyDepsP = probe(mainPy, ["-c", DEPS_SCRIPT], 60000);
|
|
171
|
+
let gsvVerP: Promise<ProbeResult> | null = null;
|
|
172
|
+
let gsvTorchP: Promise<ProbeResult> | null = null;
|
|
173
|
+
if (gsv) {
|
|
174
|
+
gsvVerP = probe(gsvPy, ["--version"], 10000);
|
|
175
|
+
gsvTorchP = probe(gsvPy, ["-c", "import torch;print(torch.__version__)"], 90000);
|
|
176
|
+
}
|
|
177
|
+
const [pyVer, pyDeps, gsvVer, gsvTorch] = await Promise.all([pyVerP, pyDepsP, gsvVerP, pyTorch(gsvTorchP)]);
|
|
178
|
+
|
|
179
|
+
// ---------------- 核心环境 ----------------
|
|
180
|
+
section(t("核心环境", "Core"));
|
|
181
|
+
|
|
182
|
+
const nodeVer = process.versions.node;
|
|
183
|
+
const [nodeMajor, nodeMinor] = nodeVer.split(".").map(Number);
|
|
184
|
+
if (nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 19)) {
|
|
185
|
+
item("ok", `Node.js v${nodeVer}`);
|
|
186
|
+
} else {
|
|
187
|
+
item("fail", t(`Node.js v${nodeVer},需 ≥ 22.19.0`, `Node.js v${nodeVer}, need >= 22.19.0`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (settingsExist()) {
|
|
191
|
+
item("ok", t("配置文件 ~/.arona/settings.json", "Config file ~/.arona/settings.json"));
|
|
192
|
+
} else {
|
|
193
|
+
item("fail", t("配置文件不存在,请先运行 arona setup", "Config file missing. Run `arona setup` first"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
item(
|
|
197
|
+
config.apiKey ? "ok" : "fail",
|
|
198
|
+
config.apiKey
|
|
199
|
+
? t("LLM API Key 已配置", "LLM API Key configured")
|
|
200
|
+
: t("LLM API Key 未配置", "LLM API Key not configured"),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!pyVer.ok) {
|
|
204
|
+
item("fail", t(`主程序 Python 未找到:${mainPy}`, `Main Python not found: ${mainPy}`));
|
|
205
|
+
} else {
|
|
206
|
+
const ver = pyVersionFrom(pyVer.stdout);
|
|
207
|
+
const mm = ver?.split(".").slice(0, 2).join(".");
|
|
208
|
+
if (!ver) {
|
|
209
|
+
item("fail", t("无法获取主程序 Python 版本", "Failed to detect main Python version"));
|
|
210
|
+
} else if (mm !== "3.12" && mm !== "3.13") {
|
|
211
|
+
item("fail", t(`主程序 Python ${ver} 版本不受支持,需 ${PY_REQUIRE}`, `Main Python ${ver} unsupported, need ${PY_REQUIRE}`));
|
|
212
|
+
} else {
|
|
213
|
+
item("ok", t(`主程序 Python ${ver}`, `Main Python ${ver}`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (pyVer.ok) {
|
|
218
|
+
let missing: string[] | null = null;
|
|
219
|
+
try {
|
|
220
|
+
const r = JSON.parse(pyDeps.stdout) as Record<string, unknown>;
|
|
221
|
+
missing = DEPS.filter((d) => !r[d]);
|
|
222
|
+
} catch {
|
|
223
|
+
missing = null;
|
|
224
|
+
}
|
|
225
|
+
if (missing === null) {
|
|
226
|
+
item("warn", t("Python 依赖检查失败", "Python dependency check failed"));
|
|
227
|
+
} else if (missing.length > 0) {
|
|
228
|
+
item("warn", t(
|
|
229
|
+
`缺少 Python 依赖:${missing.join("、")},运行 pip install -r requirements.txt 补装`,
|
|
230
|
+
`Missing Python deps: ${missing.join(", ")}. Run pip install -r requirements.txt`,
|
|
231
|
+
));
|
|
232
|
+
} else {
|
|
233
|
+
item("ok", t("Python 依赖完整", "Python dependencies installed"));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------- 语音 ----------------
|
|
238
|
+
if (config.noVoice) {
|
|
239
|
+
console.log(t("\n语音功能已禁用(--no-voice),跳过语音检查。", "\nVoice disabled (--no-voice), voice checks skipped."));
|
|
240
|
+
} else {
|
|
241
|
+
section(t("语音", "Voice"));
|
|
242
|
+
console.log(` TTS Provider:${config.ttsProvider === "gpt-sovits" ? "GPT-SoVITS" : t("阿里云百炼", "Aliyun Bailian")}`);
|
|
243
|
+
|
|
244
|
+
if (config.ttsProvider === "aliyun") {
|
|
245
|
+
item(
|
|
246
|
+
config.ttsApiKey ? "ok" : "warn",
|
|
247
|
+
config.ttsApiKey
|
|
248
|
+
? t("TTS API Key 已配置", "TTS API Key configured")
|
|
249
|
+
: t("TTS API Key 未配置,语音合成不可用", "TTS API Key not configured, speech synthesis unavailable"),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const mainAgent = getMainAgent();
|
|
254
|
+
if (config.ttsProvider === "aliyun") {
|
|
255
|
+
item(
|
|
256
|
+
getVoiceId(mainAgent) ? "ok" : "warn",
|
|
257
|
+
getVoiceId(mainAgent)
|
|
258
|
+
? t(`当前角色 ${mainAgent} 已配置音色`, `Current agent ${mainAgent} has a cloned voice`)
|
|
259
|
+
: t(`当前角色 ${mainAgent} 未克隆音色,TTS 静音;arona voice add 可补全`, `Current agent ${mainAgent} has no cloned voice, TTS muted. Run \`arona voice add\``),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// GPT-SoVITS(无论当前 provider 是否为它,配置了就体检)
|
|
264
|
+
if (!gsv) {
|
|
265
|
+
item("warn", t("GPT-SoVITS:暂未配置", "GPT-SoVITS: not configured"));
|
|
266
|
+
} else {
|
|
267
|
+
if (gsv.pythonPath) {
|
|
268
|
+
if (!gsvVer?.ok) {
|
|
269
|
+
item("fail", t(`GPT-SoVITS Python 未找到:${gsv.pythonPath}`, `GPT-SoVITS Python not found: ${gsv.pythonPath}`));
|
|
270
|
+
} else {
|
|
271
|
+
const ver = pyVersionFrom(gsvVer.stdout) ?? "";
|
|
272
|
+
item("ok", t(`GPT-SoVITS Python ${ver}`, `GPT-SoVITS Python ${ver}`));
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// 未单独配置专用 Python:按设计回退主程序 Python(是否可用由 torch 检查揭示)
|
|
276
|
+
if (!pyVer.ok) {
|
|
277
|
+
item("fail", t("GPT-SoVITS Python 未配置,且主程序 Python 不可用", "GPT-SoVITS Python not configured and main Python unavailable"));
|
|
278
|
+
} else {
|
|
279
|
+
item("warn", t(
|
|
280
|
+
`GPT-SoVITS Python 未单独配置,将使用主程序 Python ${pyVersionFrom(pyVer.stdout) ?? ""}`,
|
|
281
|
+
`GPT-SoVITS Python not set; falls back to main Python ${pyVersionFrom(pyVer.stdout) ?? ""}`,
|
|
282
|
+
));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (gsvTorch?.ok && gsvTorch.stdout.trim()) {
|
|
287
|
+
item("ok", t(`PyTorch ${gsvTorch.stdout.trim()}`, `PyTorch ${gsvTorch.stdout.trim()}`));
|
|
288
|
+
} else {
|
|
289
|
+
item("warn", t("PyTorch 未安装,GPT-SoVITS 合成不可用", "PyTorch not installed, GPT-SoVITS synthesis unavailable"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (gsv.mode === "local") {
|
|
293
|
+
if (!gsv.apiScriptPath) {
|
|
294
|
+
item("warn", t("api_v2 脚本未配置,无法自动启动本地服务", "api_v2 script not configured, cannot auto-start local server"));
|
|
295
|
+
} else if (!existsSync(gsv.apiScriptPath)) {
|
|
296
|
+
item("warn", t(`api_v2 脚本不存在:${gsv.apiScriptPath}`, `api_v2 script not found: ${gsv.apiScriptPath}`));
|
|
297
|
+
} else {
|
|
298
|
+
item("ok", t("api_v2 脚本就绪", "api_v2 script ready"));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 配置了但路径失效的权重
|
|
302
|
+
const badPaths: string[] = [];
|
|
303
|
+
if (gsv.gptModelPath && !existsSync(gsv.gptModelPath)) badPaths.push("gptModelPath");
|
|
304
|
+
if (gsv.sovitsModelPath && !existsSync(gsv.sovitsModelPath)) badPaths.push("sovitsModelPath");
|
|
305
|
+
if (badPaths.length > 0) {
|
|
306
|
+
item("warn", t(`模型权重路径不存在:${badPaths.join("、")}`, `Model weight path(s) not found: ${badPaths.join(", ")}`));
|
|
307
|
+
} else {
|
|
308
|
+
const hasGpt = !!gsv.gptModelPath || AGENT_IDS.some((id) => !!getGptSovitsVoice(id)?.gptWeightsPath?.trim());
|
|
309
|
+
const hasSovits = !!gsv.sovitsModelPath || AGENT_IDS.some((id) => !!getGptSovitsVoice(id)?.sovitsWeightsPath?.trim());
|
|
310
|
+
if (hasGpt && hasSovits) {
|
|
311
|
+
item("ok", t("模型权重已配置", "Model weights configured"));
|
|
312
|
+
} else {
|
|
313
|
+
item("warn", t("未配置模型权重,本地合成不可用", "No model weights configured, local synthesis unavailable"));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const bertOk = !!gsv.bertPath && existsSync(gsv.bertPath);
|
|
318
|
+
const hubOk = !!gsv.cnhubertPath && existsSync(gsv.cnhubertPath);
|
|
319
|
+
if (bertOk && hubOk) {
|
|
320
|
+
item("ok", t("BERT / CNHubert 就绪", "BERT / CNHubert ready"));
|
|
321
|
+
} else {
|
|
322
|
+
item("warn", t("BERT 或 CNHubert 未配置或不存在,自动启动本地服务需要它们", "BERT or CNHubert missing; both are required to auto-start the local server"));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const { host, port } = parseBaseUrl(gsv.baseUrl);
|
|
326
|
+
const alive = await tcpReachable(host, port);
|
|
327
|
+
item(
|
|
328
|
+
alive ? "ok" : "warn",
|
|
329
|
+
alive
|
|
330
|
+
? t(`本地服务运行中 ${host}:${port}`, `Local server running at ${host}:${port}`)
|
|
331
|
+
: t("本地服务未运行,首次合成时自动启动", "Local server not running; it will auto-start on first synthesis"),
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
const { host, port } = parseBaseUrl(gsv.baseUrl);
|
|
335
|
+
const alive = await tcpReachable(host, port, 3000);
|
|
336
|
+
item(
|
|
337
|
+
alive ? "ok" : "warn",
|
|
338
|
+
alive
|
|
339
|
+
? t(`云端服务可达 ${gsv.baseUrl}`, `Cloud service reachable at ${gsv.baseUrl}`)
|
|
340
|
+
: t(`云端服务不可达:${gsv.baseUrl}`, `Cloud service unreachable: ${gsv.baseUrl}`),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (config.sttEnabled) {
|
|
346
|
+
item(
|
|
347
|
+
config.sttApiKey ? "ok" : "warn",
|
|
348
|
+
config.sttApiKey
|
|
349
|
+
? t("STT API Key 已配置", "STT API Key configured")
|
|
350
|
+
: t("STT API Key 未配置,语音识别不可用", "STT API Key not configured, speech recognition unavailable"),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------- 桌宠 ----------------
|
|
356
|
+
section(t("桌宠", "Pet"));
|
|
357
|
+
const electronOk = existsSync(join(PROJECT_ROOT, "node_modules", "electron"));
|
|
358
|
+
item(
|
|
359
|
+
electronOk ? "ok" : "fail",
|
|
360
|
+
electronOk
|
|
361
|
+
? t("Electron 已安装", "Electron installed")
|
|
362
|
+
: t(`Electron 未安装,桌宠不可用;在 ${PROJECT_ROOT} 运行 npm install`, `Electron not installed, pet unavailable. Run npm install in ${PROJECT_ROOT}`),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// ---------------- 汇总 ----------------
|
|
366
|
+
console.log();
|
|
367
|
+
if (failCount > 0) {
|
|
368
|
+
console.log(chalk.red(t(`发现 ${failCount} 项可能影响使用的问题`, `${failCount} issue(s) that may affect usage`)));
|
|
369
|
+
} else if (warnCount > 0) {
|
|
370
|
+
console.log(chalk.yellow(t(`有 ${warnCount} 项可略过(未配置或功能受限)`, `${warnCount} item(s) skippable (not configured or limited)`)));
|
|
371
|
+
} else {
|
|
372
|
+
console.log(chalk.green(t("一切正常", "All checks passed")));
|
|
373
|
+
}
|
|
374
|
+
process.exitCode = failCount > 0 ? 1 : 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 占位透传:让 Promise.all 的元素类型统一(null 探测保持 null)。 */
|
|
378
|
+
function pyTorch(p: Promise<ProbeResult> | null): Promise<ProbeResult | null> {
|
|
379
|
+
return p ?? Promise.resolve(null);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
main().catch((err) => {
|
|
383
|
+
console.error(chalk.red(t(`doctor 执行失败:`, `doctor failed: `) + (err instanceof Error ? err.message : String(err))));
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
});
|