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/manager/api/service.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// 主服务进程控制 api 子应用:状态查询与启停。
|
|
2
|
-
// 注意 stop 语义 = 只停主服务(与 CLI ellm-proxy stop 的全停语义不同)——
|
|
3
|
-
// 管理服务若自杀,再无人能远程拉起主服务。
|
|
4
|
-
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import { Hono } from "hono";
|
|
10
|
-
import { APP_NAME, MANAGER_APP_NAME } from "../../utils/paths.js";
|
|
11
|
-
import { withPm2, findApp, pm2cb, safeDump } from "../../utils/pm2.js";
|
|
12
|
-
import { doStart, stopMainOnly, resolveGatewayBase, resolvePort } from "../../utils/service.js";
|
|
13
|
-
|
|
14
|
-
// 本文件在 manager/api/ 下,包根为上两级
|
|
15
|
-
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
16
|
-
|
|
17
|
-
/** ellm-proxy 进程状态(pm2 视角) */
|
|
18
|
-
async function statusPayload(startOpts) {
|
|
19
|
-
const proc = await withPm2(() => findApp());
|
|
20
|
-
const { base } = resolveGatewayBase(startOpts.config);
|
|
21
|
-
return {
|
|
22
|
-
running: !!proc,
|
|
23
|
-
pid: proc?.pid ?? null,
|
|
24
|
-
pm2Status: proc?.pm2_env?.status ?? null,
|
|
25
|
-
uptime: proc?.pm2_env?.pm_uptime ?? null,
|
|
26
|
-
gatewayBase: base,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** pm2 依赖树内的 pidusage(realpath 穿透 pnpm 符号链接):pm2 守护进程自身的内存/CPU 采样 */
|
|
31
|
-
function loadPidusage() {
|
|
32
|
-
const pm2PkgJson = realpathSync(join(pkgRoot, "node_modules", "pm2", "package.json"));
|
|
33
|
-
return createRequire(pm2PkgJson)("pidusage");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 统一行形状:role = daemon | manager | main(前端按 main 判定可操作行) */
|
|
37
|
-
function row(role, { pid = null, status = "stopped", port = null, startedAt = null, memory = null, cpu = null, restarts = null }) {
|
|
38
|
-
return {
|
|
39
|
-
role,
|
|
40
|
-
pid,
|
|
41
|
-
status,
|
|
42
|
-
port,
|
|
43
|
-
address: port ? `http://127.0.0.1:${port}` : null,
|
|
44
|
-
startedAt,
|
|
45
|
-
memory,
|
|
46
|
-
cpu,
|
|
47
|
-
restarts,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** pm2 守护进程行:PID 读 PM2_HOME/pm2.pid(文件只在 daemon 启动时写,mtime 即启动时刻);
|
|
52
|
-
* 内存/CPU 用 pidusage 采样(pm2 jlist 不含 daemon 自身) */
|
|
53
|
-
async function daemonRow() {
|
|
54
|
-
const pm2Home = process.env.PM2_HOME || join(homedir(), ".pm2");
|
|
55
|
-
const pidFile = join(pm2Home, "pm2.pid");
|
|
56
|
-
let pid = null;
|
|
57
|
-
let startedAt = null;
|
|
58
|
-
try {
|
|
59
|
-
pid = Number(readFileSync(pidFile, "utf8").trim()) || null;
|
|
60
|
-
startedAt = statSync(pidFile).mtimeMs;
|
|
61
|
-
} catch {
|
|
62
|
-
/* 无 pm2.pid = daemon 未运行 */
|
|
63
|
-
}
|
|
64
|
-
if (!pid) return row("daemon");
|
|
65
|
-
let memory = null;
|
|
66
|
-
let cpu = null;
|
|
67
|
-
try {
|
|
68
|
-
const stat = await loadPidusage()(pid);
|
|
69
|
-
memory = stat.memory ?? null;
|
|
70
|
-
cpu = stat.cpu != null ? Math.round(stat.cpu * 10) / 10 : null;
|
|
71
|
-
} catch {
|
|
72
|
-
/* pid 文件存在但进程已死(采样 reject):按未运行处理 */
|
|
73
|
-
pid = null;
|
|
74
|
-
}
|
|
75
|
-
return row("daemon", { pid, status: pid ? "online" : "stopped", port: null, startedAt, memory, cpu });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** pm2 托管进程行:monit(内存/CPU,pm2 周期采样)+ pm_uptime(启动时刻)+ restart_time */
|
|
79
|
-
function appRow(proc, role, { port }) {
|
|
80
|
-
if (!proc) return row(role);
|
|
81
|
-
const env = proc.pm2_env ?? {};
|
|
82
|
-
const monit = proc.monit ?? {};
|
|
83
|
-
return row(role, {
|
|
84
|
-
pid: proc.pid ?? null,
|
|
85
|
-
status: env.status ?? "unknown",
|
|
86
|
-
port: port ?? null,
|
|
87
|
-
startedAt: env.pm_uptime ?? null,
|
|
88
|
-
memory: monit.memory ?? null,
|
|
89
|
-
cpu: monit.cpu != null ? Math.round(monit.cpu * 10) / 10 : null,
|
|
90
|
-
restarts: env.restart_time ?? null,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 全进程列表:pm2 守护进程 / 管理服务 / 主服务。
|
|
95
|
-
* 端口口径:主服务 = pm2 注入的 ELLM_PORT(含占用顺延后的实际值)> env > 配置文件;
|
|
96
|
-
* 管理服务 = ELLM_MANAGER_PORT env > 主端口 + 1(与 manager/index.js 的预算口径一致)。 */
|
|
97
|
-
async function processesPayload(startOpts) {
|
|
98
|
-
const list = (await withPm2(() => pm2cb("list"))) || [];
|
|
99
|
-
const main = list.find((p) => p.name === APP_NAME);
|
|
100
|
-
const manager = list.find((p) => p.name === MANAGER_APP_NAME);
|
|
101
|
-
const mainPort =
|
|
102
|
-
Number(main?.pm2_env?.env?.ELLM_PORT) || resolvePort(process.env.ELLM_PORT, startOpts.config) || null;
|
|
103
|
-
const managerPort = Number(process.env.ELLM_MANAGER_PORT) || (mainPort ? mainPort + 1 : null);
|
|
104
|
-
return [
|
|
105
|
-
await daemonRow(),
|
|
106
|
-
appRow(manager, "manager", { port: managerPort }),
|
|
107
|
-
appRow(main, "main", { port: mainPort }),
|
|
108
|
-
];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** pm2 启停:start / stop / restart(restart = 停主服务 + start,
|
|
112
|
-
* start 时管理服务已在线会被 doStart 的幂等检查跳过)。
|
|
113
|
-
* stopMainOnly 已抽到 utils/service.js(updater 更新前停主服务复用同语义)。 */
|
|
114
|
-
async function runStartStop(action, startOpts) {
|
|
115
|
-
if (action === "stop") return stopMainOnly();
|
|
116
|
-
if (action === "start") return doStart(startOpts);
|
|
117
|
-
await stopMainOnly();
|
|
118
|
-
await doStart(startOpts);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 进程控制子应用(挂载于根路径):GET /status · GET /processes · POST /start|stop|restart */
|
|
122
|
-
export function createServiceApp({ startOpts }) {
|
|
123
|
-
const app = new Hono();
|
|
124
|
-
|
|
125
|
-
app.get("/status", async (c) => c.json({ ok: true, ...(await statusPayload(startOpts)) }));
|
|
126
|
-
|
|
127
|
-
// 全进程列表(三行固定:pm2 守护进程 / 管理服务 / 主服务);启停操作仍只走
|
|
128
|
-
// /start|stop|restart 的主服务语义——daemon 与管理服务不允许被远程操控(自杀失联风险)
|
|
129
|
-
app.get("/processes", async (c) =>
|
|
130
|
-
c.json({ ok: true, processes: await processesPayload(startOpts) }),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
app.post("/start", async (c) => {
|
|
134
|
-
await runStartStop("start", startOpts);
|
|
135
|
-
return c.json({ ok: true, action: "start" });
|
|
136
|
-
});
|
|
137
|
-
app.post("/stop", async (c) => {
|
|
138
|
-
await runStartStop("stop", startOpts);
|
|
139
|
-
return c.json({ ok: true, action: "stop" });
|
|
140
|
-
});
|
|
141
|
-
app.post("/restart", async (c) => {
|
|
142
|
-
await runStartStop("restart", startOpts);
|
|
143
|
-
return c.json({ ok: true, action: "restart" });
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
return app;
|
|
147
|
-
}
|
package/manager/index.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// ellm-proxy 管理服务入口(独立常驻脚本):随 `ellm-proxy start` 一并由 pm2 托管(name=ellm-proxy-manager);
|
|
2
|
-
// 也可手动 `node manager/index.js` 运行。本文件 = 创建 app(中间件 + api 聚合挂载)+ 启动 HTTP 服务;
|
|
3
|
-
// 各 api 域实现在 api/ 下:service.js(进程控制,命名对齐 backend/src/api/service.ts)、harness/(代理开关域)。
|
|
4
|
-
//
|
|
5
|
-
// 为什么独立于 backend:ellm-proxy 与 backend 同进程,进程自杀后管理台与转发层同时失联,
|
|
6
|
-
// 无法返回操作结果——启停必须由活在外面的本服务执行。
|
|
7
|
-
//
|
|
8
|
-
// 管理界面托管:本服务同进程托管 web-ui 构建产物(根路径 /,hash 路由),并反代 /api/* 到主服务——
|
|
9
|
-
// web-ui 全部接口(/api/* 管理台 + /status /restart /harness 等进程/开关域)同源可达,config.js 留空零配置。
|
|
10
|
-
//
|
|
11
|
-
// 接口(CORS 全放开,均返回 {ok, ...} 或 {ok:false, error}):
|
|
12
|
-
// GET / web-ui 管理界面(静态托管,产物缺失时仅告警跳过)
|
|
13
|
-
// GET /api/* 反代主服务管理台 API(端口 = ELLM_PORT env > 配置文件 port,每请求解析)
|
|
14
|
-
// GET /status ellm-proxy 运行状态(running/pid/status/uptime/网关端口)
|
|
15
|
-
// GET /processes 全进程列表(pm2 守护进程/管理服务/主服务:状态/PID/端口/内存/CPU/运行时长)
|
|
16
|
-
// POST /start | /stop | /restart pm2 启动/停止/重启 ellm-proxy(复用 utils/service.js,含探活)
|
|
17
|
-
// GET|POST /autostart 开机自启状态 / 开关(body {enabled};utils/autostart.js,opt-out 尊重用户选择)
|
|
18
|
-
// GET /update 自动更新状态(开关/上次检查/上次更新/supported)
|
|
19
|
-
// POST /update/enabled body {enabled};自动更新开关(<appDir>/updater.json)
|
|
20
|
-
// POST /update/check 手动检查更新(npm view 私有源)
|
|
21
|
-
// POST /update/apply 手动执行更新(停主服务 → npm i -g → 重启主服务 → manager 延迟自重启)
|
|
22
|
-
// GET /harness Claude Code / Codex / DeepSeek Harness 代理开关状态
|
|
23
|
-
// POST /harness/claude-code body {enabled};开 = 写 ~/.claude/settings.json(备份),关 = 还原备份
|
|
24
|
-
// POST /harness/codex body {enabled};开 = 写 ~/.codex/config.toml(备份),关 = 还原备份
|
|
25
|
-
// POST /harness/dsh body {enabled};开 = 写 ~/.dsh/settings.yaml + .credentials.yaml(备份),关 = 还原
|
|
26
|
-
//
|
|
27
|
-
// 配置来源(env 优先,argv 可覆盖,缺省兜底):
|
|
28
|
-
// 监听端口 ELLM_MANAGER_PORT(doStart 统一预算 = 主服务端口+1)> argv --port > 29771
|
|
29
|
-
// web-ui 产物 ELLM_WEBUI_DIR(可选覆盖)> 按本文件位置推导:包模式 <pkgRoot>/dist/web-ui → 仓库模式 <repoRoot>/web-ui/dist
|
|
30
|
-
// 主服务参数 与 ellm-proxy start 注入的 env 同源:ELLM_CONFIG / ELLM_DATA_DIR / ELLM_APP_DIR / ELLM_PORT /
|
|
31
|
-
// ELLM_SERVER_JS(主服务 dist/server.js 路径,restart 重启主服务时使用,保证参数不漂移)
|
|
32
|
-
import { existsSync } from "node:fs";
|
|
33
|
-
import { readFile } from "node:fs/promises";
|
|
34
|
-
import { Hono } from "hono";
|
|
35
|
-
import { cors } from "hono/cors";
|
|
36
|
-
import { proxy } from "hono/proxy";
|
|
37
|
-
import { serve } from "@hono/node-server";
|
|
38
|
-
import { serveStatic } from "@hono/node-server/serve-static";
|
|
39
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
40
|
-
import { fileURLToPath } from "node:url";
|
|
41
|
-
import mri from "mri";
|
|
42
|
-
import { absolutize } from "../utils/paths.js";
|
|
43
|
-
import { resolveGatewayBase } from "../utils/service.js";
|
|
44
|
-
import { startUpdateWatchdog } from "../utils/updater.js";
|
|
45
|
-
import { ICONS, say } from "../utils/ui.js";
|
|
46
|
-
import { createServiceApp } from "./api/service.js";
|
|
47
|
-
import { createServiceSettingsApp } from "./api/service-settings.js";
|
|
48
|
-
import { createHarnessApp } from "./api/harness/index.js";
|
|
49
|
-
|
|
50
|
-
const DEFAULT_SERVER_PORT = 29771; // 手动运行且无 env 时的兜底(默认网关 29770 + 1)
|
|
51
|
-
// 本文件在 manager/ 下,包根为上一级
|
|
52
|
-
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
|
-
|
|
54
|
-
// web-ui 构建产物目录:ELLM_WEBUI_DIR 优先;缺省按本文件位置推导(包模式 dist/web-ui 与 server.js 同级,
|
|
55
|
-
// 仓库模式落到 monorepo 的 web-ui/dist)。启动时判定一次,缺失则跳过静态托管(接口不受影响)。
|
|
56
|
-
const WEBUI_DIST_DIR = process.env.ELLM_WEBUI_DIR
|
|
57
|
-
? resolve(process.cwd(), process.env.ELLM_WEBUI_DIR)
|
|
58
|
-
: ([join(pkgRoot, "dist", "web-ui"), resolve(pkgRoot, "..", "web-ui", "dist")].find((p) =>
|
|
59
|
-
existsSync(join(p, "index.html")),
|
|
60
|
-
) ?? join(pkgRoot, "dist", "web-ui"));
|
|
61
|
-
const HAS_WEBUI = existsSync(join(WEBUI_DIST_DIR, "index.html"));
|
|
62
|
-
|
|
63
|
-
/** web-ui 管理界面静态托管:/ 出 index.html(hash 路由唯一服务端入口),assets/ 补长缓存头。
|
|
64
|
-
* 须在 api 聚合与 /api 反代之后注册——serve-static 未命中才放行,不会遮蔽接口。 */
|
|
65
|
-
function registerWebUi(app) {
|
|
66
|
-
if (!HAS_WEBUI) return;
|
|
67
|
-
// serve-static 的 root 只接受相对 cwd 的路径:把绝对定位换算成当前 cwd 表达(仓库/发布包/任意 cwd 均成立)
|
|
68
|
-
const root = relative(process.cwd(), WEBUI_DIST_DIR);
|
|
69
|
-
app.get("/", async (c) => {
|
|
70
|
-
try {
|
|
71
|
-
const buf = await readFile(join(WEBUI_DIST_DIR, "index.html"));
|
|
72
|
-
return c.body(new Uint8Array(buf), 200, {
|
|
73
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
74
|
-
"Cache-Control": "no-cache",
|
|
75
|
-
});
|
|
76
|
-
} catch {
|
|
77
|
-
return c.text("Not Found", 404);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
app.use("/*", (c, next) => {
|
|
81
|
-
if (c.req.path === "/") return next(); // 根路径由上面的 serveIndex 处理
|
|
82
|
-
return serveStatic({
|
|
83
|
-
root,
|
|
84
|
-
onFound: (_p, ctx) => {
|
|
85
|
-
if (ctx.req.path.startsWith("/assets/")) ctx.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
86
|
-
},
|
|
87
|
-
})(c, next);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** 创建管理服务 app:CORS 全放开 + api 聚合挂载 + /api 反代主服务 + web-ui 静态托管 + 统一 404/500 */
|
|
92
|
-
function createApp({ startOpts }) {
|
|
93
|
-
const app = new Hono();
|
|
94
|
-
|
|
95
|
-
// CORS 全放开(OPTIONS 预检由中间件自动处理)
|
|
96
|
-
app.use("*", cors());
|
|
97
|
-
|
|
98
|
-
// api 聚合:进程控制(GET /status · POST /start|stop|restart)、服务设置(autostart / update 域)
|
|
99
|
-
// 与 harness 开关(GET /harness · POST /harness/claude-code|codex|dsh)
|
|
100
|
-
app.route("/", createServiceApp({ startOpts }));
|
|
101
|
-
app.route("/", createServiceSettingsApp({ startOpts }));
|
|
102
|
-
app.route("/harness", createHarnessApp({ startOpts }));
|
|
103
|
-
|
|
104
|
-
// 反代主服务管理台 API(web-ui 同源访问的 /api/*):端口与 start 口径一致(ELLM_PORT env > 配置文件),
|
|
105
|
-
// 每请求解析——主服务端口顺延漂移无感知
|
|
106
|
-
app.all("/api/*", (c) => {
|
|
107
|
-
const { base } = resolveGatewayBase();
|
|
108
|
-
const port = Number(new URL(base).port);
|
|
109
|
-
if (!port) {
|
|
110
|
-
return c.json(
|
|
111
|
-
{ ok: false, error: "主服务端口无法解析(ELLM_PORT env 与配置文件 port 均缺失),无法转发 /api/*" },
|
|
112
|
-
502,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
// 目标 URL 必须带上原始查询串:c.req.path 只有 pathname,漏拼 search 会把
|
|
116
|
-
// ?id=... / ?page=... 等查询参数全部丢掉(主服务按缺参返回 400 或默认分页)
|
|
117
|
-
const incoming = new URL(c.req.raw.url);
|
|
118
|
-
return proxy(new URL(incoming.pathname + incoming.search, base), {
|
|
119
|
-
method: c.req.method,
|
|
120
|
-
headers: c.req.raw.headers,
|
|
121
|
-
body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : c.req.raw.body,
|
|
122
|
-
duplex: "half", // body 为流时 undici 必需
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
registerWebUi(app);
|
|
127
|
-
|
|
128
|
-
app.notFound((c) =>
|
|
129
|
-
c.json(
|
|
130
|
-
{
|
|
131
|
-
ok: false,
|
|
132
|
-
error: `未知接口: ${c.req.method} ${c.req.path}(可用: GET /(管理界面), GET /api/*(反代主服务), GET /status, GET /processes, POST /start|stop|restart, GET|POST /autostart, GET /update, POST /update/enabled|check|apply, GET /harness, POST /harness/claude-code|codex|dsh)`,
|
|
133
|
-
},
|
|
134
|
-
404,
|
|
135
|
-
),
|
|
136
|
-
);
|
|
137
|
-
app.onError((err, c) => {
|
|
138
|
-
console.error(`[ellm-proxy-manager] ${c.req.method} ${c.req.path} 失败:`, err?.message ?? err);
|
|
139
|
-
return c.json({ ok: false, error: err?.message ?? String(err) }, 500);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
return app;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function main() {
|
|
146
|
-
// argv 解析(mri):仅 --port(其余主服务参数一律走 env,与 start 注入链同源)
|
|
147
|
-
const argv = mri(process.argv.slice(2));
|
|
148
|
-
const listenPort = Number(argv.port) || Number(process.env.ELLM_MANAGER_PORT) || DEFAULT_SERVER_PORT;
|
|
149
|
-
// 主服务启动参数(start/restart 时透传给 doStart):ELLM_* env 与 CLI start 注入链同源
|
|
150
|
-
const startOpts = absolutize({
|
|
151
|
-
config: process.env.ELLM_CONFIG,
|
|
152
|
-
dataDir: process.env.ELLM_DATA_DIR,
|
|
153
|
-
workingDir: process.env.ELLM_APP_DIR,
|
|
154
|
-
serverJs: process.env.ELLM_SERVER_JS || join(pkgRoot, "dist", "server.js"),
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const app = createApp({ startOpts });
|
|
158
|
-
|
|
159
|
-
const server = serve({ fetch: app.fetch, port: listenPort, hostname: "127.0.0.1" });
|
|
160
|
-
server.on("error", (err) => {
|
|
161
|
-
console.error(
|
|
162
|
-
`错误:管理服务监听 ${listenPort} 失败(${err.code ?? err.message})${err.code === "EADDRINUSE" ? ",端口被占用" : ""}`,
|
|
163
|
-
);
|
|
164
|
-
process.exit(1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
say(`ellm-proxy 管理服务已启动: http://127.0.0.1:${listenPort}`, ICONS.rocket);
|
|
168
|
-
say(
|
|
169
|
-
HAS_WEBUI
|
|
170
|
-
? ` 管理界面: http://127.0.0.1:${listenPort}/(web-ui,/api/* 反代主服务)`
|
|
171
|
-
: ` 管理界面未托管:web-ui 构建产物缺失(${WEBUI_DIST_DIR}),仓库根执行 pnpm build:web 后重启本服务`,
|
|
172
|
-
);
|
|
173
|
-
say(
|
|
174
|
-
" 接口: GET /status /processes · POST /start /stop /restart · GET|POST /autostart · GET /update · POST /update/enabled|check|apply · GET /harness · POST /harness/claude-code /harness/codex /harness/dsh",
|
|
175
|
-
);
|
|
176
|
-
say(" CORS 已全放开(仅监听 127.0.0.1,不对外)");
|
|
177
|
-
|
|
178
|
-
// 自动更新看门狗:源码运行时内部直接返回(不轮询);开关默认开启,每小时检查私有源新版本
|
|
179
|
-
startUpdateWatchdog(startOpts);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
main();
|
package/utils/autostart.js
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// 开机自启核心(跨平台,登录时触发,均无需管理员权限):状态探测 / 注册 / 移除 / 首启自愈。
|
|
2
|
-
// 被 CLI(bin/commands/autostart.js)与管理服务(manager/api/service-settings.js)复用。
|
|
3
|
-
// 本模块只做平台操作并返回数据,人读文案由调用方输出;错误以 throw 抛出(文案是契约的一部分)。
|
|
4
|
-
// 自启项内容三平台一致:登录时执行 <nodePath> <包内 pm2 bin> resurrect(恢复最后一次 pm2 dump):
|
|
5
|
-
// Windows: 启动文件夹 ellm-proxy-autostart.cmd(shell:startup——schtasks onlogon 需管理员,实测被拒)
|
|
6
|
-
// macOS: LaunchAgent ~/Library/LaunchAgents/com.ellm-proxy.autostart.plist(launchctl load)
|
|
7
|
-
// Linux: systemd user unit ~/.config/systemd/user/ellm-proxy.service(systemctl --user enable)
|
|
8
|
-
// opt-out 标记(<appDir>/autostart.opt-out):用户显式关闭过自启后,首启自愈不再自动重开
|
|
9
|
-
// (尊重用户选择);enableAutostart 成功时清除,disableAutostart 成功时写入。
|
|
10
|
-
import { writeFileSync, rmSync, existsSync, mkdirSync } from "node:fs";
|
|
11
|
-
import { homedir, platform } from "node:os";
|
|
12
|
-
import { spawn } from "node:child_process";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { defaultAppDir } from "./paths.js";
|
|
15
|
-
import { resolveInterpreter } from "./interpreter.js";
|
|
16
|
-
import { pm2BinJs } from "./pm2.js";
|
|
17
|
-
|
|
18
|
-
const AUTOSTART_CMD = "ellm-proxy-autostart.cmd"; // Windows
|
|
19
|
-
const PLIST_LABEL = "com.ellm-proxy.autostart"; // macOS
|
|
20
|
-
const LINUX_UNIT = "ellm-proxy.service"; // Linux
|
|
21
|
-
const OPT_OUT_FILE = "autostart.opt-out"; // appDir 下的 opt-out 标记文件名
|
|
22
|
-
|
|
23
|
-
/** opt-out 标记路径(用户显式关闭过自启的持久标记,首启自愈据此跳过) */
|
|
24
|
-
function optOutPath() {
|
|
25
|
-
return join(defaultAppDir, OPT_OUT_FILE);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 用户启动文件夹(Windows shell:startup):登录时自动执行其中的脚本 */
|
|
29
|
-
function windowsStartupDir() {
|
|
30
|
-
const dir = process.env["APPDATA"];
|
|
31
|
-
if (!dir) throw new Error("无法定位启动文件夹(APPDATA 环境变量未设置)。");
|
|
32
|
-
return join(dir, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** spawn 执行外部命令,返回退出码;进程无法启动(ENOENT 等)返回 127 */
|
|
36
|
-
function runExit(cmd, args) {
|
|
37
|
-
return new Promise((resolvePromise) => {
|
|
38
|
-
const child = spawn(cmd, args, { stdio: "ignore", shell: false });
|
|
39
|
-
child.on("error", () => resolvePromise(127));
|
|
40
|
-
child.on("exit", (code) => resolvePromise(code ?? 1));
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 各平台自启项路径(探测用;未知平台为 null) */
|
|
45
|
-
function platformItemPath() {
|
|
46
|
-
switch (platform()) {
|
|
47
|
-
case "win32":
|
|
48
|
-
return join(windowsStartupDir(), AUTOSTART_CMD);
|
|
49
|
-
case "darwin":
|
|
50
|
-
return join(homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
51
|
-
case "linux":
|
|
52
|
-
return join(homedir(), ".config", "systemd", "user", LINUX_UNIT);
|
|
53
|
-
default:
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// —— 注册(各平台) ————————————————————————————————————————————————
|
|
59
|
-
|
|
60
|
-
async function windowsOn(nodePath, pm2Js) {
|
|
61
|
-
// cmd 内容全 ASCII(含中文的注释交给外部帮助输出,避免 cmd 在 GBK 代码页下乱码)
|
|
62
|
-
const body = [
|
|
63
|
-
"@echo off",
|
|
64
|
-
`rem Auto-generated by "ellm-proxy autostart on". Disable with "ellm-proxy autostart off".`,
|
|
65
|
-
`"${nodePath}" "${pm2Js}" resurrect >nul 2>&1`,
|
|
66
|
-
"",
|
|
67
|
-
].join("\r\n");
|
|
68
|
-
const itemPath = join(windowsStartupDir(), AUTOSTART_CMD);
|
|
69
|
-
writeFileSync(itemPath, body, "utf-8");
|
|
70
|
-
return itemPath;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function darwinOn(nodePath, pm2Js) {
|
|
74
|
-
const itemPath = join(homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
75
|
-
const logPath = join(defaultAppDir, "logs", "autostart.log");
|
|
76
|
-
mkdirSync(join(defaultAppDir, "logs"), { recursive: true });
|
|
77
|
-
const body = [
|
|
78
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
79
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
80
|
-
'<plist version="1.0">',
|
|
81
|
-
"<dict>",
|
|
82
|
-
` <key>Label</key><string>${PLIST_LABEL}</string>`,
|
|
83
|
-
" <key>ProgramArguments</key>",
|
|
84
|
-
" <array>",
|
|
85
|
-
` <string>${nodePath}</string>`,
|
|
86
|
-
` <string>${pm2Js}</string>`,
|
|
87
|
-
" <string>resurrect</string>",
|
|
88
|
-
" </array>",
|
|
89
|
-
" <key>RunAtLoad</key><true/>",
|
|
90
|
-
` <key>StandardOutPath</key><string>${logPath}</string>`,
|
|
91
|
-
` <key>StandardErrorPath</key><string>${logPath}</string>`,
|
|
92
|
-
"</dict>",
|
|
93
|
-
"</plist>",
|
|
94
|
-
"",
|
|
95
|
-
].join("\n");
|
|
96
|
-
writeFileSync(itemPath, body, "utf-8");
|
|
97
|
-
// 先卸载旧定义再加载(幂等;未加载过时 unload 报错属正常)
|
|
98
|
-
await runExit("launchctl", ["unload", itemPath]);
|
|
99
|
-
const code = await runExit("launchctl", ["load", itemPath]);
|
|
100
|
-
if (code !== 0) {
|
|
101
|
-
throw new Error(`launchctl load 失败(退出码 ${code})。请手动执行: launchctl load "${itemPath}"`);
|
|
102
|
-
}
|
|
103
|
-
return itemPath;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function linuxOn(nodePath, pm2Js) {
|
|
107
|
-
const itemPath = join(homedir(), ".config", "systemd", "user", LINUX_UNIT);
|
|
108
|
-
mkdirSync(join(homedir(), ".config", "systemd", "user"), { recursive: true });
|
|
109
|
-
const body = [
|
|
110
|
-
"[Unit]",
|
|
111
|
-
"Description=ellm-proxy autostart (pm2 resurrect)",
|
|
112
|
-
"After=default.target",
|
|
113
|
-
"",
|
|
114
|
-
"[Service]",
|
|
115
|
-
"Type=oneshot",
|
|
116
|
-
`ExecStart="${nodePath}" "${pm2Js}" resurrect`,
|
|
117
|
-
"",
|
|
118
|
-
"[Install]",
|
|
119
|
-
"WantedBy=default.target",
|
|
120
|
-
"",
|
|
121
|
-
].join("\n");
|
|
122
|
-
writeFileSync(itemPath, body, "utf-8");
|
|
123
|
-
const reload = await runExit("systemctl", ["--user", "daemon-reload"]);
|
|
124
|
-
if (reload !== 0) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
`systemctl --user daemon-reload 失败(退出码 ${reload})。当前环境可能无 systemd(WSL/容器),请手动启用或删除: ${itemPath}`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
const enable = await runExit("systemctl", ["--user", "enable", LINUX_UNIT]);
|
|
130
|
-
if (enable !== 0) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`systemctl --user enable 失败(退出码 ${enable})。请手动执行: systemctl --user enable ${LINUX_UNIT}`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
return itemPath;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// —— 移除(各平台) ————————————————————————————————————————————————
|
|
139
|
-
|
|
140
|
-
async function windowsOff() {
|
|
141
|
-
const itemPath = join(windowsStartupDir(), AUTOSTART_CMD);
|
|
142
|
-
if (!existsSync(itemPath)) return false;
|
|
143
|
-
rmSync(itemPath, { force: true });
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function darwinOff() {
|
|
148
|
-
const itemPath = join(homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
149
|
-
if (!existsSync(itemPath)) return false;
|
|
150
|
-
await runExit("launchctl", ["unload", itemPath]);
|
|
151
|
-
rmSync(itemPath, { force: true });
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function linuxOff() {
|
|
156
|
-
const itemPath = join(homedir(), ".config", "systemd", "user", LINUX_UNIT);
|
|
157
|
-
if (!existsSync(itemPath)) return false;
|
|
158
|
-
await runExit("systemctl", ["--user", "disable", LINUX_UNIT]); // 容错:未启用过时报错不影响删除
|
|
159
|
-
rmSync(itemPath, { force: true });
|
|
160
|
-
await runExit("systemctl", ["--user", "daemon-reload"]);
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// —— 对外 API ——————————————————————————————————————————————————————
|
|
165
|
-
|
|
166
|
-
/** 自启状态:enabled = 平台自启项是否存在;itemPath 未知平台为 null */
|
|
167
|
-
export async function autostartStatus() {
|
|
168
|
-
const itemPath = platformItemPath();
|
|
169
|
-
return {
|
|
170
|
-
platform: platform(),
|
|
171
|
-
enabled: itemPath ? existsSync(itemPath) : false,
|
|
172
|
-
itemPath,
|
|
173
|
-
/** 用户是否显式关闭过(首启自愈据此跳过) */
|
|
174
|
-
optOut: existsSync(optOutPath()),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** 注册自启项(幂等:重复注册覆盖旧文件;macOS 先 unload 再 load)。成功返回 { itemPath } */
|
|
179
|
-
export async function enableAutostart() {
|
|
180
|
-
const pf = platform();
|
|
181
|
-
if (pf !== "win32" && pf !== "darwin" && pf !== "linux") {
|
|
182
|
-
throw new Error(`暂不支持的平台: ${pf}。可改用 pm2 自带的 pm2 startup。`);
|
|
183
|
-
}
|
|
184
|
-
const nodePath = await resolveInterpreter();
|
|
185
|
-
const pm2Js = pm2BinJs();
|
|
186
|
-
const itemPath = await (pf === "win32" ? windowsOn : pf === "darwin" ? darwinOn : linuxOn)(nodePath, pm2Js);
|
|
187
|
-
rmSync(optOutPath(), { force: true }); // 显式开启 = 清除 opt-out,首启自愈不再被视为已关闭
|
|
188
|
-
return { itemPath };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** 移除自启项。返回 removed(此前是否存在);成功后写 opt-out 标记(首启自愈不再自动重开) */
|
|
192
|
-
export async function disableAutostart() {
|
|
193
|
-
const pf = platform();
|
|
194
|
-
if (pf !== "win32" && pf !== "darwin" && pf !== "linux") {
|
|
195
|
-
throw new Error(`暂不支持的平台: ${pf}。可改用 pm2 自带的 pm2 startup。`);
|
|
196
|
-
}
|
|
197
|
-
const removed = await (pf === "win32" ? windowsOff : pf === "darwin" ? darwinOff : linuxOff)();
|
|
198
|
-
mkdirSync(defaultAppDir, { recursive: true });
|
|
199
|
-
writeFileSync(optOutPath(), new Date().toISOString(), "utf-8");
|
|
200
|
-
return { removed };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** 首启自愈(doStart 末尾静默调用):自启项已存在或用户 opt-out 过则跳过,否则注册并提示一句。
|
|
204
|
-
* 失败不抛出(不阻塞启动,console.warn 提示手动路径)。 */
|
|
205
|
-
export async function ensureAutostartOnStart() {
|
|
206
|
-
try {
|
|
207
|
-
const status = await autostartStatus();
|
|
208
|
-
if (!status.itemPath || status.enabled) return; // 未知平台 / 已注册
|
|
209
|
-
if (status.optOut) {
|
|
210
|
-
console.log("已按此前设置保持开机自启关闭(如需开启:ellm-proxy autostart on 或管理台设置页)。");
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const { itemPath } = await enableAutostart();
|
|
214
|
-
console.log(`已自动注册开机自启(登录时 pm2 resurrect): ${itemPath}`);
|
|
215
|
-
console.log("禁用方式: ellm-proxy autostart off 或管理台设置页。");
|
|
216
|
-
} catch (err) {
|
|
217
|
-
console.warn(`开机自启注册失败(不影响本次启动): ${err?.message ?? err}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
package/utils/interpreter.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// 服务解释器解析:保证 dist/server.js(node:sqlite)运行在 Node >= MIN_NODE 上。
|
|
2
|
-
// CLI 自身 node 满足 → process.execPath;否则 get-node 解析(缺失时下载一次并缓存),
|
|
3
|
-
// pm2 daemon 的 node 版本无关紧要(interpreter 由本模块保证)。
|
|
4
|
-
import getNode from "get-node";
|
|
5
|
-
import { MIN_NODE, FALLBACK_NODE } from "./paths.js";
|
|
6
|
-
|
|
7
|
-
export async function resolveInterpreter() {
|
|
8
|
-
const [maj, min] = process.version.replace(/^v/, "").split(".").map(Number);
|
|
9
|
-
if (maj > 22 || (maj === 22 && min >= 13)) return process.execPath;
|
|
10
|
-
console.log(`当前 node ${process.version} 不满足 ${MIN_NODE},正在解析可用版本(缺失时自动下载一次并缓存)...`);
|
|
11
|
-
const { path, version } = await getNode(FALLBACK_NODE, { progress: true });
|
|
12
|
-
console.log(`服务将使用 node ${version}: ${path}`);
|
|
13
|
-
return path;
|
|
14
|
-
}
|
package/utils/paths.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// 共享常量与路径工具:包根/应用目录常量、配置查找链、首次配置自愈、选项绝对化。
|
|
2
|
-
// 供 CLI(bin/)与管理服务(manager/)两侧复用;工具一律返回数据,人读文案由调用方输出。
|
|
3
|
-
import { readFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
/** 本文件在 utils/ 下,包根为上一级 */
|
|
9
|
-
export const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
-
export const defaultAppDir = join(homedir(), ".ellm-proxy");
|
|
11
|
-
export const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf-8"));
|
|
12
|
-
export const APP_NAME = "ellm-proxy"; // 主服务(dist/server.js)的 pm2 进程名
|
|
13
|
-
export const MANAGER_APP_NAME = "ellm-proxy-manager"; // 管理服务(manager/index.js)的 pm2 进程名
|
|
14
|
-
export const MIN_NODE = ">=22.13.0"; // node:sqlite 最低版(与包 engines 一致)
|
|
15
|
-
export const FALLBACK_NODE = "22"; // 兜底解释器锁定的主版本(满足 MIN_NODE,与 nve 22 口径一致)
|
|
16
|
-
|
|
17
|
-
/** 配置文件路径:--config > ELLM_CONFIG 环境变量 > ~/.ellm-proxy/ellm.config.json(与 server 端查找链一致) */
|
|
18
|
-
export function configPath(override) {
|
|
19
|
-
if (override) return resolve(override);
|
|
20
|
-
return process.env["ELLM_CONFIG"] ? resolve(process.env["ELLM_CONFIG"]) : join(defaultAppDir, "ellm.config.json");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 首次配置自愈:不存在则从包内示例复制创建,并提示两种修改方式 */
|
|
24
|
-
export function ensureConfig(override) {
|
|
25
|
-
const config = configPath(override);
|
|
26
|
-
if (existsSync(config)) return config;
|
|
27
|
-
const example = join(pkgRoot, "ellm.config.example.json");
|
|
28
|
-
if (!existsSync(example)) {
|
|
29
|
-
throw new Error(`缺少示例配置 ${example},无法自动创建配置文件`);
|
|
30
|
-
}
|
|
31
|
-
mkdirSync(dirname(config), { recursive: true });
|
|
32
|
-
copyFileSync(example, config);
|
|
33
|
-
console.log(`已创建配置文件: ${config}`);
|
|
34
|
-
console.log("可直接编辑该文件(填入供应商 apiKey),或经管理台在线修改(实时热更新)。");
|
|
35
|
-
return config;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 路径类选项统一转绝对路径(相对按调用方当前目录展开;CLI cwd 与 node 进程工作目录基准不同,透传相对串会错位) */
|
|
39
|
-
export function absolutize(opts) {
|
|
40
|
-
const out = { ...opts };
|
|
41
|
-
for (const key of ["config", "dataDir", "workingDir", "serverJs"]) {
|
|
42
|
-
if (out[key]) out[key] = resolve(out[key]);
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|