ellm-proxy 0.0.6 → 0.0.8
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 +29 -11
- package/bin/ellm-proxy.js +2 -60
- package/dist/cli.js +35780 -0
- package/dist/server.js +16714 -6336
- package/dist/web-ui/assets/DashboardView-3c_i_3VF.js +1 -0
- package/dist/web-ui/assets/LayoutView-BhN1f8wo.js +1 -0
- package/dist/web-ui/assets/LayoutView-CNd_MJt2.css +1 -0
- package/dist/web-ui/assets/LoginView-J23vsMu2.js +1 -0
- package/dist/web-ui/assets/{LogsView-BlXITEJH.js → LogsView-2h2Y0XE8.js} +2 -2
- package/dist/web-ui/assets/ProcessView-Bn8Ph7rh.css +1 -0
- package/dist/web-ui/assets/ProcessView-Bs2k6aAf.js +1 -0
- package/dist/web-ui/assets/{ProvidersView-f8aZkMdw.js → ProvidersView-bE1Hv9D-.js} +2 -2
- package/dist/web-ui/assets/SettingsView-BmLmC-_v.js +1 -0
- package/dist/web-ui/assets/SettingsView-Ceyj-ZDZ.css +1 -0
- package/dist/web-ui/assets/SetupView-Cgj7ubNz.js +1 -0
- package/dist/web-ui/assets/SetupView-kPJXJ7dX.css +1 -0
- package/dist/web-ui/assets/config-nvSp8hHp.js +1 -0
- package/dist/web-ui/assets/{index-GohB4JDa.js → index-sPrBBnf5.js} +28 -28
- package/dist/web-ui/assets/{providers-Djtdy2tv.js → providers-di7L8jy_.js} +1 -1
- package/dist/web-ui/assets/useManagerApi-qPjxfB0X.js +1 -0
- package/dist/web-ui/assets/useToast-D76qyWZg.js +1 -0
- package/dist/web-ui/config.js +3 -6
- package/dist/web-ui/index.html +1 -1
- package/docs/DESIGN.md +43 -14
- package/ellm.config.example.json +2 -0
- package/package.json +2 -17
- package/bin/commands/autostart.js +0 -37
- package/bin/commands/config.js +0 -8
- package/bin/commands/pm2.js +0 -28
- package/bin/commands/service.js +0 -107
- package/dist/web-ui/assets/DashboardView-C3Hw-HpG.js +0 -1
- package/dist/web-ui/assets/LayoutView-BUA8-IzK.css +0 -1
- package/dist/web-ui/assets/LayoutView-C9phf7in.js +0 -1
- package/dist/web-ui/assets/LoginView-es6pSPgt.js +0 -1
- package/dist/web-ui/assets/ProcessView-Bgf7-MLQ.js +0 -1
- package/dist/web-ui/assets/ProcessView-DK0TvDpR.css +0 -1
- package/dist/web-ui/assets/SettingsView-BkwyoOUb.css +0 -1
- package/dist/web-ui/assets/SettingsView-GvAA2vLp.js +0 -1
- package/dist/web-ui/assets/config-Cs3_z63C.js +0 -1
- package/dist/web-ui/assets/useApi-D_iJ2Mca.js +0 -1
- package/dist/web-ui/assets/useManagerApi-C1_COxKg.js +0 -1
- package/dist/web-ui/assets/useToast-C0UMTbkx.js +0 -1
- package/manager/api/harness/backup.js +0 -20
- package/manager/api/harness/claude-code.js +0 -54
- package/manager/api/harness/codex.js +0 -123
- package/manager/api/harness/dsh.js +0 -175
- package/manager/api/harness/index.js +0 -118
- package/manager/api/service-settings.js +0 -98
- package/manager/api/service.js +0 -147
- package/manager/index.js +0 -182
- package/utils/autostart.js +0 -219
- package/utils/interpreter.js +0 -14
- package/utils/paths.js +0 -45
- package/utils/pm2.js +0 -130
- package/utils/port.js +0 -35
- package/utils/service.js +0 -205
- package/utils/ui.js +0 -59
- package/utils/updater.js +0 -210
package/utils/pm2.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// pm2 JS API 封装:连接包裹/回调转 promise/应用查询/探活轮询/安全 dump/包内 pm2 CLI 定位/env 白名单。
|
|
2
|
-
import { rmSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { createRequire } from "node:module";
|
|
6
|
-
import pm2 from "pm2";
|
|
7
|
-
import { APP_NAME, MANAGER_APP_NAME } from "./paths.js";
|
|
8
|
-
import { dot, endOk, endFail } from "./ui.js";
|
|
9
|
-
|
|
10
|
-
/** pm2.connect 包裹:fn 内可调 pm2.xxx(回调式),结束后自动 disconnect */
|
|
11
|
-
export function withPm2(fn) {
|
|
12
|
-
return new Promise((resolvePromise, reject) => {
|
|
13
|
-
pm2.connect((connectErr) => {
|
|
14
|
-
if (connectErr) return reject(connectErr);
|
|
15
|
-
Promise.resolve(fn(pm2)).then(
|
|
16
|
-
(value) => pm2.disconnect(() => resolvePromise(value)),
|
|
17
|
-
(err) => {
|
|
18
|
-
try {
|
|
19
|
-
pm2.disconnect();
|
|
20
|
-
} catch {
|
|
21
|
-
/* 已断开 */
|
|
22
|
-
}
|
|
23
|
-
reject(err);
|
|
24
|
-
},
|
|
25
|
-
);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** pm2 回调式方法转 promise */
|
|
31
|
-
export function pm2cb(method, ...args) {
|
|
32
|
-
return new Promise((resolvePromise, reject) => {
|
|
33
|
-
pm2[method](...args, (err, ...out) => (err ? reject(err) : resolvePromise(...out)));
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 查询应用当前进程记录(list 中按 name 匹配;不存在返回 undefined)。
|
|
38
|
-
* name 缺省 = 主服务 ellm-proxy;管理服务传 MANAGER_APP_NAME */
|
|
39
|
-
export async function findApp(name = APP_NAME) {
|
|
40
|
-
const list = (await pm2cb("list")) || [];
|
|
41
|
-
return list.find((p) => p.name === name);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 启动探活:轮询 status 至 online(每 pollMs 一次,全程打进度点)——比固定等待更快成功
|
|
45
|
-
* (online 即返回),也能容忍慢启动(默认 15s 上限,固定 1.5s 在慢机器上会误报);errored/stopped
|
|
46
|
-
* 提前失败(脚本秒崩不必等满超时)。failHint 是失败文案的线索部分(文案是契约的一部分)。
|
|
47
|
-
* quiet = 静默轮询不打点(多服务并行等待时输出由调用方合并,避免多组进度点交错)。 */
|
|
48
|
-
export async function waitOnline(name, failHint, { timeoutMs = 15000, pollMs = 300, quiet = false } = {}) {
|
|
49
|
-
const deadline = Date.now() + timeoutMs;
|
|
50
|
-
let last = null;
|
|
51
|
-
for (;;) {
|
|
52
|
-
last = await findApp(name);
|
|
53
|
-
const status = last?.pm2_env?.status;
|
|
54
|
-
if (status === "online") {
|
|
55
|
-
if (!quiet) endOk();
|
|
56
|
-
return last;
|
|
57
|
-
}
|
|
58
|
-
if (status === "errored" || status === "stopped" || Date.now() >= deadline) {
|
|
59
|
-
if (!quiet) endFail();
|
|
60
|
-
throw new Error(`${name} 启动后状态异常(${status ?? "已退出"})。${failHint}`);
|
|
61
|
-
}
|
|
62
|
-
if (!quiet) dot();
|
|
63
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 安全 dump(start/stop 成功后调用):pm2 不允许 dump 空列表会报错——此时说明全部进程已删,
|
|
68
|
-
* 清掉旧 dump.pm2 与滚动备份 dump.pm2.bak(resurrect 无 dump.pm2 时会回落 .bak),防止开机
|
|
69
|
-
* resurrect 把已停止的服务拉起来 */
|
|
70
|
-
export async function safeDump() {
|
|
71
|
-
try {
|
|
72
|
-
await pm2cb("dump");
|
|
73
|
-
} catch {
|
|
74
|
-
try {
|
|
75
|
-
rmSync(join(homedir(), ".pm2", "dump.pm2"), { force: true });
|
|
76
|
-
rmSync(join(homedir(), ".pm2", "dump.pm2.bak"), { force: true });
|
|
77
|
-
} catch {
|
|
78
|
-
/* 清理失败不影响 stop 语义 */
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** 当前包内 pm2 CLI 入口绝对路径(autostart cmd 用;resolve 兼容 npm 全局平铺与 pnpm 布局) */
|
|
84
|
-
export function pm2BinJs() {
|
|
85
|
-
const pm2PkgRoot = dirname(createRequire(import.meta.url).resolve("pm2/package.json"));
|
|
86
|
-
return join(pm2PkgRoot, "bin", "pm2");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** 清洗 pm2 子进程透传的 env:pm2 托管的进程自身 env 带 pm_id/pm2_env/unique_id 等
|
|
90
|
-
* 内部变量与 bash 注入的 `_`,原样传回 pm2.start 会污染新进程注册——实测主服务
|
|
91
|
-
* (name=ellm-proxy)被注册成管理服务的名字,导致探活误判「已退出」。
|
|
92
|
-
* 黑名单清洗实测仍有漏网(多键组合触发),故改用白名单:
|
|
93
|
-
* 只透传主服务必需的业务键(ELLM_* 前缀,含 ELLM_PORT / ELLM_MANAGER_PORT)与 Windows node 运行所需的系统键。
|
|
94
|
-
* PM2_HOME 保留(pm2 API 连接 daemon 需要,主服务自身不用)。 */
|
|
95
|
-
const PM2_ENV_WHITELIST = new Set([
|
|
96
|
-
"PATH",
|
|
97
|
-
"SYSTEMROOT",
|
|
98
|
-
"WINDIR",
|
|
99
|
-
"SYSTEMDRIVE",
|
|
100
|
-
"COMSPEC",
|
|
101
|
-
"PATHEXT",
|
|
102
|
-
"OS",
|
|
103
|
-
"TEMP",
|
|
104
|
-
"TMP",
|
|
105
|
-
"USERPROFILE",
|
|
106
|
-
"HOMEDRIVE",
|
|
107
|
-
"HOMEPATH",
|
|
108
|
-
"APPDATA",
|
|
109
|
-
"LOCALAPPDATA",
|
|
110
|
-
"PROGRAMDATA",
|
|
111
|
-
"PROGRAMFILES",
|
|
112
|
-
"ALLUSERSPROFILE",
|
|
113
|
-
"PUBLIC",
|
|
114
|
-
"USERNAME",
|
|
115
|
-
"USERDOMAIN",
|
|
116
|
-
"COMPUTERNAME",
|
|
117
|
-
"NUMBER_OF_PROCESSORS",
|
|
118
|
-
"HTTP_PROXY",
|
|
119
|
-
"HTTPS_PROXY",
|
|
120
|
-
"NO_PROXY",
|
|
121
|
-
]);
|
|
122
|
-
|
|
123
|
-
export function sanitizePm2Env(env) {
|
|
124
|
-
const out = {};
|
|
125
|
-
for (const [key, value] of Object.entries(env)) {
|
|
126
|
-
if (key === "PM2_HOME" || key.startsWith("ELLM_")) out[key] = value;
|
|
127
|
-
else if (PM2_ENV_WHITELIST.has(key.toUpperCase())) out[key] = value;
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
package/utils/port.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// 端口可用性探测:试绑 listen(全部成功=空闲,任一 EADDRINUSE 等错误=占用),供 service.js 启动前预算端口。
|
|
2
|
-
// Windows 的 wildcard/specific 共存语义(bind 0.0.0.0 与 bind 127.0.0.1 可同端口并存,实测)会让单维度
|
|
3
|
-
// 探测漏报"bind 成功但 IPv4 流量被占用者抢走"的假活,故主服务探测 0.0.0.0 + 127.0.0.1 双维度;
|
|
4
|
-
// IPv6-only 占用极罕见(漏报由 pm2 探活失败兜底)。管理服务固定监听 127.0.0.1 → 探测 ["127.0.0.1"]。
|
|
5
|
-
// 探测与实际 bind 之间存在毫秒级竞态窗口(TOCTOU),极端时被抢占 → 仍由 pm2 探活失败兜底。
|
|
6
|
-
import { createServer } from "node:net";
|
|
7
|
-
|
|
8
|
-
/** 单 host 试绑:成功=空闲(等 close 回调 socket 释放后才返回 true,避免误占探测端口) */
|
|
9
|
-
function isHostFree(port, host) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
const srv = createServer();
|
|
12
|
-
srv.once("error", () => resolve(false)); // EADDRINUSE / EACCES 等一律视为不可用
|
|
13
|
-
srv.once("listening", () => srv.close(() => resolve(true)));
|
|
14
|
-
srv.listen(port, host);
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 端口是否空闲:逐个 host 试绑,全部成功才空闲(hosts 传数组多维探测,传字符串退化为单 host) */
|
|
19
|
-
export function isPortFree(port, hosts = ["0.0.0.0", "127.0.0.1"]) {
|
|
20
|
-
const list = Array.isArray(hosts) ? hosts : [hosts];
|
|
21
|
-
return (async () => {
|
|
22
|
-
for (const host of list) {
|
|
23
|
-
if (!(await isHostFree(port, host))) return false;
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
})();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 从 start 起向上找第一个空闲端口(含 start;maxTry 个内全占返回 0,由调用方决定兜底行为) */
|
|
30
|
-
export async function findFreePort(start, hosts = ["0.0.0.0", "127.0.0.1"], maxTry = 100) {
|
|
31
|
-
for (let p = start; p < start + maxTry; p++) {
|
|
32
|
-
if (await isPortFree(p, hosts)) return p;
|
|
33
|
-
}
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
package/utils/service.js
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// 服务进程管理核心:start/stop(restart = stop + start 由调用方组合,保证最新参数生效)。
|
|
2
|
-
// 被 CLI(bin/commands/service.js)与管理服务(manager/index.js)复用。
|
|
3
|
-
// 本模块只做进程编排并返回数据,人读文案/摘要由调用方输出;错误以 throw 抛出(文案是契约的一部分)。
|
|
4
|
-
// start 流程:配置自愈 → env 注入(含端口预算:主服务 ELLM_PORT / 管理服务 ELLM_MANAGER_PORT = 主端口+1,
|
|
5
|
-
// 占用时自动顺延空闲端口)→ 两个服务一并提交 pm2(数组形式)→ 并行轮询探活 → dump;
|
|
6
|
-
// 管理服务已在线(部分恢复场景)则只补启主服务。
|
|
7
|
-
// 全程分步输出进度(「→ 启动 ...」+ 探活进度点),等待期间不留静默空窗。
|
|
8
|
-
import { readFileSync, mkdirSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { APP_NAME, MANAGER_APP_NAME, pkgRoot, defaultAppDir, ensureConfig, configPath } from "./paths.js";
|
|
11
|
-
import { withPm2, pm2cb, findApp, waitOnline, safeDump, sanitizePm2Env } from "./pm2.js";
|
|
12
|
-
import { resolveInterpreter } from "./interpreter.js";
|
|
13
|
-
import { isPortFree, findFreePort } from "./port.js";
|
|
14
|
-
import { ensureAutostartOnStart } from "./autostart.js";
|
|
15
|
-
import { ICONS, say, warn, begin, endOk, endFail } from "./ui.js";
|
|
16
|
-
|
|
17
|
-
/** 管理服务脚本(随包分发,start 时随主服务一并 pm2 托管) */
|
|
18
|
-
const MANAGER_INDEX_JS = join(pkgRoot, "manager", "index.js");
|
|
19
|
-
const DEFAULT_MANAGER_PORT = 29771; // 主端口无法解析时 manager 的最终兜底(默认网关 29770 + 1,与 manager/index.js 缺省一致)
|
|
20
|
-
|
|
21
|
-
/** 端口解析口径:portEnv(ELLM_PORT env)> 配置文件 port 字段 */
|
|
22
|
-
export function resolvePort(portEnv, configFile) {
|
|
23
|
-
let port = Number(portEnv) || 0;
|
|
24
|
-
if (!port) {
|
|
25
|
-
try {
|
|
26
|
-
port = JSON.parse(readFileSync(configFile, "utf-8")).port || 0;
|
|
27
|
-
} catch {
|
|
28
|
-
port = 0;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return port;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 网关地址与配置文件定位(harness 代理指向目标):ELLM_PORT env / 配置文件 port,与 start 命令端口口径一致 */
|
|
35
|
-
export function resolveGatewayBase(configOverride) {
|
|
36
|
-
const configFile = configPath(configOverride);
|
|
37
|
-
const port = resolvePort(process.env.ELLM_PORT, configFile);
|
|
38
|
-
return { base: `http://127.0.0.1:${port}`, configFile };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 启动服务(CLI restart 与管理服务 restart 复用):
|
|
42
|
-
* 配置自愈 → env 注入 → 两服务一并提交 pm2(数组形式)→ 并行轮询探活 → dump,
|
|
43
|
-
* 管理服务已在线则只补启主服务。全程分步进度输出。返回 { proc, managerProc, appDir, config } 供调用方输出摘要。 */
|
|
44
|
-
export async function doStart(opts) {
|
|
45
|
-
say(`正在启动 ${APP_NAME}(pm2 后台守护)...`, ICONS.rocket);
|
|
46
|
-
const config = ensureConfig(opts.config);
|
|
47
|
-
const appDir = opts.workingDir || defaultAppDir;
|
|
48
|
-
mkdirSync(join(appDir, "logs"), { recursive: true });
|
|
49
|
-
|
|
50
|
-
// server 端读取的路径基变量:显式值优先(CLI 参数 > 会话已有 > 默认推导)
|
|
51
|
-
process.env.ELLM_APP_DIR = process.env.ELLM_APP_DIR ?? appDir;
|
|
52
|
-
process.env.ELLM_CONFIG = config;
|
|
53
|
-
if (opts.dataDir) process.env.ELLM_DATA_DIR = opts.dataDir;
|
|
54
|
-
|
|
55
|
-
// 端口基准预算:主服务 = --port > ELLM_PORT env > 配置文件 port 字段(与 server 端读取口径一致)。
|
|
56
|
-
// 占用探测与注入在 withPm2 内做(管理服务是否在线影响其端口预算,findApp 需要 pm2 连接)。
|
|
57
|
-
const port = resolvePort(opts.port ?? process.env.ELLM_PORT, config);
|
|
58
|
-
|
|
59
|
-
const interpreter = await resolveInterpreter();
|
|
60
|
-
const script = opts.serverJs || join(pkgRoot, "dist", "server.js");
|
|
61
|
-
// ELLM_SERVER_JS:管理服务 restart 主服务时按同一路径重启,保证 --server-js 等参数不漂移
|
|
62
|
-
process.env.ELLM_SERVER_JS = script;
|
|
63
|
-
|
|
64
|
-
return withPm2(async () => {
|
|
65
|
-
if (await findApp()) {
|
|
66
|
-
throw new Error(`${APP_NAME} 已在运行。请先停止: ellm-proxy stop`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 主服务端口:基准被占(其他应用 / 孤儿进程)则自动顺延到空闲端口(默认 0.0.0.0 + 127.0.0.1 双维度,
|
|
70
|
-
// 见 utils/port.js),避免把注定冲突的端口交给 pm2 后陷入崩溃循环;顺延仅本次运行生效,不改配置文件。
|
|
71
|
-
let mainPort = port;
|
|
72
|
-
if (mainPort && !(await isPortFree(mainPort))) {
|
|
73
|
-
const wanted = mainPort;
|
|
74
|
-
mainPort = (await findFreePort(mainPort + 1)) || mainPort;
|
|
75
|
-
warn(
|
|
76
|
-
mainPort === wanted
|
|
77
|
-
? `端口 ${wanted} 已被占用且未找到空闲端口,仍按原端口启动(可能失败,请释放端口或修改配置)`
|
|
78
|
-
: `端口 ${wanted} 已被占用,主服务自动改用空闲端口 ${mainPort}(仅本次运行生效,不改配置文件)`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
if (mainPort) process.env.ELLM_PORT = String(mainPort);
|
|
82
|
-
|
|
83
|
-
// 管理服务随主服务一并托管(活在外面:主服务被 stop/重启后由它执行拉起;resurrect 时一并恢复)。
|
|
84
|
-
// 已在线则跳过启动(部分恢复场景幂等),并保持其现有端口(重新预算换端口会造成访问地址漂移)。
|
|
85
|
-
const existingManager = await findApp(MANAGER_APP_NAME);
|
|
86
|
-
let managerPort = 0;
|
|
87
|
-
if (existingManager) {
|
|
88
|
-
const runningPort = Number(existingManager.pm2_env?.env?.ELLM_MANAGER_PORT);
|
|
89
|
-
if (runningPort) process.env.ELLM_MANAGER_PORT = String(runningPort);
|
|
90
|
-
managerPort = Number(process.env.ELLM_MANAGER_PORT) || 0;
|
|
91
|
-
say(`管理服务 ${MANAGER_APP_NAME} 已在线(端口 ${managerPort || "?"}),跳过启动`, ICONS.skip);
|
|
92
|
-
} else {
|
|
93
|
-
// 管理服务端口 = 主服务端口 + 1(用户显式设置过 ELLM_MANAGER_PORT 则尊重);
|
|
94
|
-
// 被占则自动顺延(固定监听 127.0.0.1,单维度探测)。探测注入必须在 pm2 start 前,env 随之透传。
|
|
95
|
-
managerPort = Number(process.env.ELLM_MANAGER_PORT) || (mainPort ? mainPort + 1 : 0);
|
|
96
|
-
if (managerPort && !(await isPortFree(managerPort, ["127.0.0.1"]))) {
|
|
97
|
-
const wanted = managerPort;
|
|
98
|
-
managerPort = (await findFreePort(managerPort + 1, ["127.0.0.1"])) || managerPort;
|
|
99
|
-
warn(
|
|
100
|
-
managerPort === wanted
|
|
101
|
-
? `管理服务端口 ${wanted} 已被占用且未找到空闲端口,仍按原端口启动(可能失败,可用 ELLM_MANAGER_PORT 调整)`
|
|
102
|
-
: `管理服务端口 ${wanted} 已被占用,自动改用空闲端口 ${managerPort}(仅本次运行生效)`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
if (managerPort) process.env.ELLM_MANAGER_PORT = String(managerPort);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 两份 pm2 start 配置(单启主服务 / 双启一并提交两条路径共用);env 在端口注入后快照
|
|
109
|
-
const mainOpts = {
|
|
110
|
-
name: APP_NAME,
|
|
111
|
-
script,
|
|
112
|
-
interpreter,
|
|
113
|
-
cwd: appDir,
|
|
114
|
-
// 崩溃自动重启但封顶,防端口占用/配置损坏陷入无限重启
|
|
115
|
-
autorestart: true,
|
|
116
|
-
max_restarts: 10,
|
|
117
|
-
restart_delay: 3000,
|
|
118
|
-
merge_logs: true,
|
|
119
|
-
out_file: join(appDir, "logs", "ellm-out.log"),
|
|
120
|
-
error_file: join(appDir, "logs", "ellm-err.log"),
|
|
121
|
-
env: sanitizePm2Env(process.env),
|
|
122
|
-
};
|
|
123
|
-
const managerOpts = {
|
|
124
|
-
name: MANAGER_APP_NAME,
|
|
125
|
-
script: MANAGER_INDEX_JS,
|
|
126
|
-
interpreter,
|
|
127
|
-
cwd: appDir,
|
|
128
|
-
autorestart: true,
|
|
129
|
-
max_restarts: 10,
|
|
130
|
-
restart_delay: 3000,
|
|
131
|
-
merge_logs: true,
|
|
132
|
-
out_file: join(appDir, "logs", "manager-out.log"),
|
|
133
|
-
error_file: join(appDir, "logs", "manager-err.log"),
|
|
134
|
-
env: sanitizePm2Env(process.env),
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
if (existingManager) {
|
|
138
|
-
// 仅补启主服务(单服务,保持打点进度样式)
|
|
139
|
-
begin(`启动主服务 ${APP_NAME}${mainPort ? ` (端口 ${mainPort})` : ""} `);
|
|
140
|
-
await pm2cb("start", mainOpts);
|
|
141
|
-
const proc = await waitOnline(
|
|
142
|
-
APP_NAME,
|
|
143
|
-
`最可能是端口被占用或配置无效,请查看日志: ${join(appDir, "logs", "ellm-err.log")}`,
|
|
144
|
-
);
|
|
145
|
-
await safeDump();
|
|
146
|
-
return { proc, managerProc: existingManager, appDir, config };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 双启:两个服务一并提交 pm2(数组形式,daemon 逐个拉起),等待期并行轮询、输出合并为一行。
|
|
150
|
-
// 主服务失败时管理服务往往已在线——其职责正是远程拉起主服务,故失败后不回收进程,文案给出重试路径。
|
|
151
|
-
begin(
|
|
152
|
-
`启动 ${APP_NAME}${mainPort ? ` (端口 ${mainPort})` : ""} + ${MANAGER_APP_NAME}${managerPort ? ` (端口 ${managerPort})` : ""} ...`,
|
|
153
|
-
);
|
|
154
|
-
await pm2cb("start", [mainOpts, managerOpts]);
|
|
155
|
-
const [mainRes, managerRes] = await Promise.allSettled([
|
|
156
|
-
waitOnline(APP_NAME, `最可能是端口被占用或配置无效,请查看日志: ${join(appDir, "logs", "ellm-err.log")}`, {
|
|
157
|
-
quiet: true,
|
|
158
|
-
}),
|
|
159
|
-
waitOnline(
|
|
160
|
-
MANAGER_APP_NAME,
|
|
161
|
-
`最可能是管理服务端口被占用(端口 ${process.env.ELLM_MANAGER_PORT || DEFAULT_MANAGER_PORT},可用 ELLM_MANAGER_PORT 调整),请查看日志: ${join(appDir, "logs", "manager-err.log")}`,
|
|
162
|
-
{ quiet: true },
|
|
163
|
-
),
|
|
164
|
-
]);
|
|
165
|
-
const failures = [mainRes, managerRes].filter((r) => r.status === "rejected");
|
|
166
|
-
if (failures.length) {
|
|
167
|
-
endFail();
|
|
168
|
-
throw new Error(failures.map((f) => f.reason?.message ?? String(f.reason)).join(";"));
|
|
169
|
-
}
|
|
170
|
-
endOk();
|
|
171
|
-
|
|
172
|
-
// dump 进程列表(~/.pm2/dump.pm2),供开机自启 pm2 resurrect 恢复(主服务 + 管理服务一并恢复)
|
|
173
|
-
await safeDump();
|
|
174
|
-
// 首启自愈:从未注册过自启项且用户未显式关闭过 → 静默注册(opt-out 标记尊重用户选择)
|
|
175
|
-
await ensureAutostartOnStart();
|
|
176
|
-
return { proc: mainRes.value, managerProc: managerRes.value, appDir, config };
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** 停止服务:主服务与管理服务一并删除(幂等)。返回 { main, manager } 表示各自此前是否在运行。 */
|
|
181
|
-
export async function doStop() {
|
|
182
|
-
return withPm2(async () => {
|
|
183
|
-
const proc = await findApp();
|
|
184
|
-
const managerProc = await findApp(MANAGER_APP_NAME);
|
|
185
|
-
if (!proc && !managerProc) return { main: false, manager: false };
|
|
186
|
-
// 先删管理服务(避免它在主服务停止后仍对外提供启停接口造成状态误判),再删主服务
|
|
187
|
-
if (managerProc) await pm2cb("delete", MANAGER_APP_NAME);
|
|
188
|
-
if (proc) await pm2cb("delete", APP_NAME);
|
|
189
|
-
await safeDump();
|
|
190
|
-
return { main: !!proc, manager: !!managerProc };
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** 仅停止主服务(管理服务保持存活;幂等)。管理服务仍在 dump 列表中,resurrect 后它会先起来
|
|
195
|
-
* 继续提供远程启停。管理服务的 stop 语义与 CLI 全停不同——管理服务若自杀,再无人能远程拉起主服务。
|
|
196
|
-
* 被 manager /stop 与 updater(更新前停主服务,manager 自身保持存活执行后续流程)复用。 */
|
|
197
|
-
export async function stopMainOnly() {
|
|
198
|
-
return withPm2(async () => {
|
|
199
|
-
const proc = await findApp();
|
|
200
|
-
if (!proc) return "already-stopped";
|
|
201
|
-
await pm2cb("delete", APP_NAME);
|
|
202
|
-
await safeDump();
|
|
203
|
-
return "stopped";
|
|
204
|
-
});
|
|
205
|
-
}
|
package/utils/ui.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// 统一输出工具:emoji 图标 + 进度点 + 摘要字段行,CLI(bin/)与管理服务(manager/)共用。
|
|
2
|
-
// 图标约定:一律选无 VS16(变体选择符)的码点——🖥️/🛠️ 等带 VS16 的在不同终端宽度不稳,实测弃用。
|
|
3
|
-
// 摘要字段行按「两空格缩进 + 图标 + 四字标签 + 三空格 + 内容」对齐。
|
|
4
|
-
export const ICONS = {
|
|
5
|
-
rocket: "🚀", // 开始
|
|
6
|
-
pending: "⏳", // 进行中(探活等待等)
|
|
7
|
-
success: "✅", // 成功完成
|
|
8
|
-
warn: "❗", // 警告(端口顺延等降级提示)
|
|
9
|
-
skip: "⏭️", // 已在线跳过
|
|
10
|
-
running: "🟢", // 服务运行中
|
|
11
|
-
stopped: "🛑", // 已停止
|
|
12
|
-
idle: "💤", // 未在运行
|
|
13
|
-
error: "❌", // 失败
|
|
14
|
-
gateway: "🌐", // 网关端点
|
|
15
|
-
webui: "💻", // 管理界面
|
|
16
|
-
api: "🔧", // 管理服务接口
|
|
17
|
-
folder: "📂", // 应用目录
|
|
18
|
-
status: "📋", // 运行状态
|
|
19
|
-
question: "❓", // 未知(端口未知等兜底展示)
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/** 一行信息(icon 省略 = 纯文本行) */
|
|
23
|
-
export function say(text, icon) {
|
|
24
|
-
console.log(icon ? `${icon} ${text}` : text);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 警告行 */
|
|
28
|
-
export function warn(text) {
|
|
29
|
-
say(text, ICONS.warn);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 错误行(stderr,供 CLI 统一错误出口) */
|
|
33
|
-
export function error(text) {
|
|
34
|
-
console.error(`${ICONS.error} ${text}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 行内片段(不换行):⏳ 前缀 + 文本,供后续追加进度点 / 行尾状态;text 需自带尾部空格或省略号 */
|
|
38
|
-
export function begin(text) {
|
|
39
|
-
process.stdout.write(`${ICONS.pending} ${text}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 进度点(探活轮询期间追加) */
|
|
43
|
-
export function dot() {
|
|
44
|
-
process.stdout.write(".");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 行尾成功 / 失败标记并换行(与 begin/dot 配套) */
|
|
48
|
-
export function endOk() {
|
|
49
|
-
process.stdout.write(" ✓\n");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function endFail() {
|
|
53
|
-
process.stdout.write(" ✗\n");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 摘要字段行:缩进 + 图标 + 标签 + 内容 */
|
|
57
|
-
export function field(icon, label, value) {
|
|
58
|
-
console.log(` ${icon} ${label} ${value}`);
|
|
59
|
-
}
|
package/utils/updater.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
// 自动更新核心:npm 全局包的检查 / 安装 / 看门狗轮询。
|
|
2
|
-
// 被 manager(manager/api/service-settings.js 挂载 + startUpdateWatchdog 常驻轮询)复用。
|
|
3
|
-
// 检查源 = npm 官方 registry(ELLM_UPDATE_REGISTRY 可覆盖);包名取 pkg.name(ellm-proxy)。
|
|
4
|
-
// 更新流程:stopMainOnly(停主服务释放文件句柄,manager 保持存活)→ npm i -g <pkg>@latest →
|
|
5
|
-
// doStart(startOpts)(主服务即新代码)→ 延迟 3s pm2 restart manager 自身(daemon 负责拉起,不依赖自身存活)。
|
|
6
|
-
// 开关状态 <appDir>/updater.json:{ enabled?, lastCheck?, lastApply? };文件缺失 = 默认开启。
|
|
7
|
-
// 仓库 dev 模式防护:pkgRoot 父目录不是 node_modules(源码运行)时更新域 unsupported,看门狗不启动。
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { basename, dirname, join } from "node:path";
|
|
11
|
-
import { pkg, pkgRoot, defaultAppDir, MANAGER_APP_NAME } from "./paths.js";
|
|
12
|
-
import { doStart, stopMainOnly } from "./service.js";
|
|
13
|
-
import { pm2cb, withPm2 } from "./pm2.js";
|
|
14
|
-
|
|
15
|
-
const UPDATER_SETTINGS_FILE = "updater.json";
|
|
16
|
-
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
17
|
-
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查
|
|
18
|
-
const FIRST_CHECK_DELAY_MS = 30 * 1000; // 启动后 30s 首查(错开启动高峰)
|
|
19
|
-
|
|
20
|
-
/** 更新开关配置路径 */
|
|
21
|
-
function settingsPath() {
|
|
22
|
-
return join(defaultAppDir, UPDATER_SETTINGS_FILE);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 读取开关设置(文件缺失/损坏 = 默认开启);enabled 显式 false 才视为关闭 */
|
|
26
|
-
export function readUpdaterSettings() {
|
|
27
|
-
try {
|
|
28
|
-
const raw = JSON.parse(readFileSync(settingsPath(), "utf-8"));
|
|
29
|
-
return { enabled: raw.enabled !== false, lastCheck: raw.lastCheck ?? null, lastApply: raw.lastApply ?? null };
|
|
30
|
-
} catch {
|
|
31
|
-
return { enabled: true, lastCheck: null, lastApply: null };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 合并写回开关设置(patch 浅合并;写盘失败不 throw——检查结果落盘失败不影响主流程) */
|
|
36
|
-
export function writeUpdaterSettings(patch) {
|
|
37
|
-
const next = { ...readUpdaterSettings(), ...patch };
|
|
38
|
-
try {
|
|
39
|
-
mkdirSync(defaultAppDir, { recursive: true });
|
|
40
|
-
writeFileSync(settingsPath(), JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
41
|
-
} catch {
|
|
42
|
-
/* 只读盘等场景容忍:内存态仍可用 */
|
|
43
|
-
}
|
|
44
|
-
return next;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 仓库 dev 模式判定:包根(或 scoped 包的上一级 @scope 目录)不在 node_modules 下 = 源码运行,
|
|
48
|
-
* 更新域不可用(防 dev 会话误更新全局包)。全局安装路径形如 <prefix>/node_modules/<pkg>,
|
|
49
|
-
* scoped 包为 <prefix>/node_modules/@scope/<pkg>,故向上最多探两层。 */
|
|
50
|
-
export function isUpdateSupported() {
|
|
51
|
-
const parent = dirname(pkgRoot);
|
|
52
|
-
return basename(parent) === "node_modules" || basename(dirname(parent)) === "node_modules";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** x.y.z 逐段数字比较(prerelease/脏串按 0 段处理:私有源版本号由 release.mjs 保证三段式) */
|
|
56
|
-
export function isNewerVersion(latest, current) {
|
|
57
|
-
const parse = (v) => String(v || "").split(".").map((n) => Number(n) || 0);
|
|
58
|
-
const a = parse(latest);
|
|
59
|
-
const b = parse(current);
|
|
60
|
-
for (let i = 0; i < 3; i++) {
|
|
61
|
-
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** 更新源 registry(env ELLM_UPDATE_REGISTRY 可覆盖,默认私有 group 源) */
|
|
67
|
-
export function updateRegistry() {
|
|
68
|
-
return process.env["ELLM_UPDATE_REGISTRY"] || DEFAULT_REGISTRY;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** spawn 执行 npm 命令并收集 stdout(shell:true 兼容 Windows npm.cmd;先例 scripts/release.mjs) */
|
|
72
|
-
function runNpm(args, timeoutMs) {
|
|
73
|
-
return new Promise((resolvePromise, reject) => {
|
|
74
|
-
// windowsHide: true 必须带——manager 是无控制台的 pm2 后台进程,shell:true 经 cmd.exe 跑 npm.cmd
|
|
75
|
-
// 时 Windows 会给命令链分配新控制台(web-ui 点更新/看门狗 tick 都会弹 cmd 黑窗),此选项全程隐藏
|
|
76
|
-
const child = spawn(`npm ${args.join(" ")}`, { shell: true, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
77
|
-
let stdout = "";
|
|
78
|
-
let stderr = "";
|
|
79
|
-
const timer = setTimeout(() => {
|
|
80
|
-
child.kill();
|
|
81
|
-
reject(new Error(`npm ${args[0]} 超时(${Math.round(timeoutMs / 1000)}s)`));
|
|
82
|
-
}, timeoutMs);
|
|
83
|
-
child.stdout.on("data", (d) => (stdout += d));
|
|
84
|
-
child.stderr.on("data", (d) => (stderr += d));
|
|
85
|
-
child.on("error", (err) => {
|
|
86
|
-
clearTimeout(timer);
|
|
87
|
-
reject(err);
|
|
88
|
-
});
|
|
89
|
-
child.on("exit", (code) => {
|
|
90
|
-
clearTimeout(timer);
|
|
91
|
-
if (code === 0) resolvePromise(stdout.trim());
|
|
92
|
-
else reject(new Error(`npm ${args[0]} 失败(exit ${code}):${stderr.trim().slice(-400) || "无错误输出"}`));
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 查询 registry 上该包的最新版本(npm view) */
|
|
98
|
-
async function fetchLatestVersion() {
|
|
99
|
-
const out = await runNpm(["view", pkg.name, "version", "--registry", updateRegistry()], 60 * 1000);
|
|
100
|
-
const version = out.split("\n").filter(Boolean).pop()?.trim();
|
|
101
|
-
if (!version || !/^\d+\.\d+\.\d+/.test(version)) {
|
|
102
|
-
throw new Error(`无法解析 npm view 输出版本号: ${out.slice(0, 200)}`);
|
|
103
|
-
}
|
|
104
|
-
return version;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** 检查更新:查询最新版本并与当前比较,结果落 lastCheck。返回 { hasUpdate, currentVersion, latestVersion, checkedAt } */
|
|
108
|
-
export async function checkForUpdate() {
|
|
109
|
-
const latestVersion = await fetchLatestVersion();
|
|
110
|
-
const result = {
|
|
111
|
-
hasUpdate: isNewerVersion(latestVersion, pkg.version),
|
|
112
|
-
currentVersion: pkg.version,
|
|
113
|
-
latestVersion,
|
|
114
|
-
checkedAt: Date.now(),
|
|
115
|
-
};
|
|
116
|
-
writeUpdaterSettings({ lastCheck: result });
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// —— 安装与重启 ————————————————————————————————————————————————————————
|
|
121
|
-
|
|
122
|
-
/** 更新执行互斥锁:手动触发与看门狗 tick 并发时只放行一个(其余收到明确报错) */
|
|
123
|
-
let updating = false;
|
|
124
|
-
|
|
125
|
-
export function isUpdating() {
|
|
126
|
-
return updating;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 执行更新(内部先查版本,无新版直接返回 updated:false):停主服务 → npm i -g →
|
|
130
|
-
* 重启主服务(新代码)→ 3s 后重启 manager 自身换新代码。安装失败 → 回滚为主服务旧版本继续服务。
|
|
131
|
-
* startOpts = 主服务启动参数(与 manager/index.js 的 startOpts 同源,保证重启后参数不漂移)。
|
|
132
|
-
* 返回 { fromVersion, toVersion, updated }。 */
|
|
133
|
-
export async function performUpdate(startOpts) {
|
|
134
|
-
if (updating) throw new Error("已有更新在执行中,请稍后再试。");
|
|
135
|
-
updating = true;
|
|
136
|
-
const fromVersion = pkg.version;
|
|
137
|
-
try {
|
|
138
|
-
const latest = await fetchLatestVersion();
|
|
139
|
-
if (!isNewerVersion(latest, fromVersion)) {
|
|
140
|
-
const result = { hasUpdate: false, currentVersion: fromVersion, latestVersion: latest, checkedAt: Date.now() };
|
|
141
|
-
writeUpdaterSettings({ lastCheck: result });
|
|
142
|
-
return { fromVersion, toVersion: fromVersion, updated: false };
|
|
143
|
-
}
|
|
144
|
-
// 停主服务释放 dist 文件句柄(Windows 上 npm 替换全局目录时句柄占用会致安装失败);
|
|
145
|
-
// manager 保持存活执行后续流程
|
|
146
|
-
await stopMainOnly();
|
|
147
|
-
try {
|
|
148
|
-
await runNpm(["install", "-g", `${pkg.name}@latest`, "--registry", updateRegistry()], 10 * 60 * 1000);
|
|
149
|
-
} catch (err) {
|
|
150
|
-
// 安装失败:旧版本继续服务(doStart 用的仍是当前包代码)
|
|
151
|
-
await doStart(startOpts).catch(() => {});
|
|
152
|
-
throw new Error(`安装新版本失败,已恢复旧版本 ${fromVersion} 继续服务。${err?.message ?? err}`);
|
|
153
|
-
}
|
|
154
|
-
writeUpdaterSettings({ lastApply: { fromVersion, toVersion: latest, appliedAt: Date.now() } });
|
|
155
|
-
await doStart(startOpts);
|
|
156
|
-
// 延迟重启 manager 自身:让本次 HTTP 响应先送达,pm2 daemon 负责拉起新代码
|
|
157
|
-
setTimeout(() => {
|
|
158
|
-
withPm2(() => pm2cb("restart", MANAGER_APP_NAME)).catch(() => {});
|
|
159
|
-
}, 3000);
|
|
160
|
-
return { fromVersion, toVersion: latest, updated: true };
|
|
161
|
-
} finally {
|
|
162
|
-
updating = false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** 检查到新版后的一步式自动处理(看门狗 tick 用):返回是否执行了更新 */
|
|
167
|
-
async function autoUpdateIfNeeded(startOpts) {
|
|
168
|
-
const check = await checkForUpdate();
|
|
169
|
-
if (!check.hasUpdate) return false;
|
|
170
|
-
console.log(`[updater] 发现新版本 ${check.latestVersion}(当前 ${check.currentVersion}),开始自动更新...`);
|
|
171
|
-
const result = await performUpdate(startOpts);
|
|
172
|
-
if (result.updated) {
|
|
173
|
-
console.log(`[updater] 已更新 ${result.fromVersion} → ${result.toVersion},主服务已重启,manager 将延迟自重启`);
|
|
174
|
-
}
|
|
175
|
-
return result.updated;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// —— 看门狗 ————————————————————————————————————————————————————————
|
|
179
|
-
|
|
180
|
-
let watchdogTimer = null;
|
|
181
|
-
|
|
182
|
-
/** 启动更新看门狗(manager 启动时调用):更新域受支持且开关开启时,
|
|
183
|
-
* 启动后 30s 首查 + 每小时轮询;每次 tick 重读开关,已关闭则停表(下次 manager 启动再评估)。
|
|
184
|
-
* tick 内检查/更新失败只记日志(下个周期再试),绝不 throw——看门狗不能把 manager 打崩。
|
|
185
|
-
* startOpts = 主服务启动参数(透传 performUpdate → doStart,与 manager/index.js 同源)。 */
|
|
186
|
-
export async function startUpdateWatchdog(startOpts) {
|
|
187
|
-
if (!isUpdateSupported()) return; // 源码运行:不轮询不检查
|
|
188
|
-
const { enabled } = readUpdaterSettings();
|
|
189
|
-
if (!enabled) return;
|
|
190
|
-
if (watchdogTimer) return; // 已在跑(幂等)
|
|
191
|
-
|
|
192
|
-
const tick = async () => {
|
|
193
|
-
const { enabled: nowEnabled } = readUpdaterSettings();
|
|
194
|
-
if (!nowEnabled) {
|
|
195
|
-
clearInterval(watchdogTimer);
|
|
196
|
-
watchdogTimer = null;
|
|
197
|
-
console.log("[updater] 自动更新已关闭,看门狗停止");
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
try {
|
|
201
|
-
await autoUpdateIfNeeded(startOpts);
|
|
202
|
-
} catch (err) {
|
|
203
|
-
console.warn(`[updater] 自动检查/更新失败(下个周期重试): ${err?.message ?? err}`);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
setTimeout(tick, FIRST_CHECK_DELAY_MS).unref?.();
|
|
208
|
-
watchdogTimer = setInterval(tick, CHECK_INTERVAL_MS);
|
|
209
|
-
console.log(`[updater] 更新看门狗已启动(每 ${CHECK_INTERVAL_MS / 60000} 分钟检查,源 ${updateRegistry()})`);
|
|
210
|
-
}
|