ellm-proxy 0.0.2 → 0.0.6

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.
Files changed (58) hide show
  1. package/README.md +60 -60
  2. package/bin/commands/autostart.js +37 -213
  3. package/bin/commands/config.js +8 -8
  4. package/bin/commands/pm2.js +28 -28
  5. package/bin/commands/service.js +107 -107
  6. package/bin/ellm-proxy.js +61 -61
  7. package/dist/server.js +33 -2
  8. package/dist/web-ui/assets/DashboardView-BHG4xIdN.css +1 -0
  9. package/dist/web-ui/assets/{DashboardView-CQdlgirS.js → DashboardView-C3Hw-HpG.js} +1 -1
  10. package/dist/web-ui/assets/LayoutView-BUA8-IzK.css +1 -0
  11. package/dist/web-ui/assets/LayoutView-C9phf7in.js +1 -0
  12. package/dist/web-ui/assets/LoginView-DrhfslIY.css +1 -0
  13. package/dist/web-ui/assets/LoginView-es6pSPgt.js +1 -0
  14. package/dist/web-ui/assets/LogsView-BlXITEJH.js +9 -0
  15. package/dist/web-ui/assets/LogsView-Dl50XQhG.css +1 -0
  16. package/dist/web-ui/assets/{ProcessView-0x_ShSwV.js → ProcessView-Bgf7-MLQ.js} +1 -1
  17. package/dist/web-ui/assets/ProcessView-DK0TvDpR.css +1 -0
  18. package/dist/web-ui/assets/ProvidersView-Dpr8MI-p.css +1 -0
  19. package/dist/web-ui/assets/ProvidersView-f8aZkMdw.js +31 -0
  20. package/dist/web-ui/assets/SettingsView-BkwyoOUb.css +1 -0
  21. package/dist/web-ui/assets/SettingsView-GvAA2vLp.js +1 -0
  22. package/dist/web-ui/assets/config-Cs3_z63C.js +1 -0
  23. package/dist/web-ui/assets/{index-DFlUx18F.js → index-GohB4JDa.js} +2 -2
  24. package/dist/web-ui/assets/{providers-CGEmOxne.js → providers-Djtdy2tv.js} +1 -1
  25. package/dist/web-ui/assets/{useApi-BRPinb_2.js → useApi-D_iJ2Mca.js} +1 -1
  26. package/dist/web-ui/assets/useManagerApi-C1_COxKg.js +1 -0
  27. package/dist/web-ui/assets/{useToast-D7xlwSg6.js → useToast-C0UMTbkx.js} +1 -1
  28. package/dist/web-ui/config.js +10 -10
  29. package/dist/web-ui/index.html +13 -13
  30. package/docs/DESIGN.md +99 -99
  31. package/manager/api/harness/backup.js +20 -20
  32. package/manager/api/harness/claude-code.js +54 -54
  33. package/manager/api/harness/codex.js +123 -123
  34. package/manager/api/harness/dsh.js +175 -175
  35. package/manager/api/harness/index.js +118 -118
  36. package/manager/api/service-settings.js +98 -0
  37. package/manager/api/service.js +3 -13
  38. package/manager/index.js +182 -170
  39. package/package.json +44 -40
  40. package/utils/autostart.js +219 -0
  41. package/utils/interpreter.js +14 -14
  42. package/utils/paths.js +45 -44
  43. package/utils/pm2.js +130 -130
  44. package/utils/service.js +205 -189
  45. package/utils/updater.js +210 -0
  46. package/dist/web-ui/assets/DashboardView-qrQrkgLA.css +0 -1
  47. package/dist/web-ui/assets/LayoutView-BaY4Pz9W.js +0 -1
  48. package/dist/web-ui/assets/LoginView-CPvz1Ylx.js +0 -1
  49. package/dist/web-ui/assets/LoginView-Dg0mB6ok.css +0 -1
  50. package/dist/web-ui/assets/LogsView-B-bIj36j.js +0 -9
  51. package/dist/web-ui/assets/LogsView-Wl3_adHm.css +0 -1
  52. package/dist/web-ui/assets/ProcessView-DzMqBJmd.css +0 -1
  53. package/dist/web-ui/assets/ProvidersView-BxAxDRFH.css +0 -1
  54. package/dist/web-ui/assets/ProvidersView-GQENcvJS.js +0 -31
  55. package/dist/web-ui/assets/SettingsView-BDkymsrS.css +0 -1
  56. package/dist/web-ui/assets/SettingsView-BrAiaskZ.js +0 -1
  57. package/dist/web-ui/assets/config-DNn5cjYY.js +0 -1
  58. package/dist/web-ui/assets/useManagerApi-BDWJz5o6.js +0 -1
@@ -0,0 +1,98 @@
1
+ // 服务设置 api 子应用:开机自启开关 + 自动更新(检查/开关/手动触发)。
2
+ // 与 api/service.js(进程控制)、api/harness/(代理开关)平级的第三域,路由形状/响应约定一致
3
+ // ({ok,...} 或 {ok:false,error});实现在 utils/autostart.js 与 utils/updater.js(CLI 侧复用)。
4
+ import { Hono } from "hono";
5
+ import { autostartStatus, enableAutostart, disableAutostart } from "../../utils/autostart.js";
6
+ import {
7
+ readUpdaterSettings,
8
+ writeUpdaterSettings,
9
+ checkForUpdate,
10
+ performUpdate,
11
+ isUpdateSupported,
12
+ isUpdating,
13
+ } from "../../utils/updater.js";
14
+
15
+ /** 读 body 并校验 enabled;合法返回 boolean,非法返回 400 Response(调用方直接 return 该响应) */
16
+ async function requireEnabled(c) {
17
+ let enabled;
18
+ try {
19
+ enabled = (await c.req.json())?.enabled;
20
+ } catch {
21
+ return c.json({ ok: false, error: "请求体不是合法 JSON" }, 400);
22
+ }
23
+ if (typeof enabled !== "boolean") {
24
+ return c.json({ ok: false, error: 'body 需为 {"enabled": true|false}' }, 400);
25
+ }
26
+ return enabled;
27
+ }
28
+
29
+ /** 服务设置子应用:GET/POST /autostart · GET /update · POST /update/enabled|check|apply */
30
+ export function createServiceSettingsApp({ startOpts }) {
31
+ const app = new Hono();
32
+
33
+ // —— 开机自启 ——————————————————————————————————————————————
34
+ app.get("/autostart", async (c) => c.json({ ok: true, ...(await autostartStatus()) }));
35
+
36
+ app.post("/autostart", async (c) => {
37
+ const enabled = await requireEnabled(c);
38
+ if (enabled instanceof Response) return enabled;
39
+ if (enabled) {
40
+ const { itemPath } = await enableAutostart();
41
+ return c.json({ ok: true, enabled: true, itemPath, note: "已注册自启项,下次登录时 pm2 resurrect 恢复服务" });
42
+ }
43
+ const { removed } = await disableAutostart();
44
+ return c.json({
45
+ ok: true,
46
+ enabled: false,
47
+ removed,
48
+ note: removed ? "已移除自启项,且首次启动不再自动注册(重新开启即可解除)" : "原本未开启,无操作",
49
+ });
50
+ });
51
+
52
+ // —— 自动更新 ——————————————————————————————————————————————
53
+ app.get("/update", (c) => {
54
+ const settings = readUpdaterSettings();
55
+ return c.json({
56
+ ok: true,
57
+ enabled: settings.enabled,
58
+ lastCheck: settings.lastCheck,
59
+ lastApply: settings.lastApply,
60
+ supported: isUpdateSupported(),
61
+ updating: isUpdating(),
62
+ });
63
+ });
64
+
65
+ app.post("/update/enabled", async (c) => {
66
+ const enabled = await requireEnabled(c);
67
+ if (enabled instanceof Response) return enabled;
68
+ writeUpdaterSettings({ enabled });
69
+ return c.json({
70
+ ok: true,
71
+ enabled,
72
+ note: enabled
73
+ ? "自动更新已开启(看门狗随下次 manager 启动生效;当前周期内手动检查不受影响)"
74
+ : "自动更新已关闭(看门狗在下次 tick 时停止)",
75
+ });
76
+ });
77
+
78
+ app.post("/update/check", async (c) => {
79
+ const result = await checkForUpdate();
80
+ return c.json({ ok: true, ...result });
81
+ });
82
+
83
+ app.post("/update/apply", async (c) => {
84
+ if (!isUpdateSupported()) {
85
+ return c.json({ ok: false, error: "当前为源码运行模式,更新功能不可用(仅 npm 全局安装的包支持自动更新)" }, 400);
86
+ }
87
+ const result = await performUpdate(startOpts);
88
+ return c.json({
89
+ ok: true,
90
+ ...result,
91
+ note: result.updated
92
+ ? `已更新 ${result.fromVersion} → ${result.toVersion}:主服务已重启,管理服务将在数秒后自重启(期间界面请求可能瞬时失败)`
93
+ : `已是最新版本 ${result.toVersion}`,
94
+ });
95
+ });
96
+
97
+ return app;
98
+ }
@@ -9,7 +9,7 @@ import { createRequire } from "node:module";
9
9
  import { Hono } from "hono";
10
10
  import { APP_NAME, MANAGER_APP_NAME } from "../../utils/paths.js";
11
11
  import { withPm2, findApp, pm2cb, safeDump } from "../../utils/pm2.js";
12
- import { doStart, resolveGatewayBase, resolvePort } from "../../utils/service.js";
12
+ import { doStart, stopMainOnly, resolveGatewayBase, resolvePort } from "../../utils/service.js";
13
13
 
14
14
  // 本文件在 manager/api/ 下,包根为上两级
15
15
  const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
@@ -108,19 +108,9 @@ async function processesPayload(startOpts) {
108
108
  ];
109
109
  }
110
110
 
111
- /** 仅停止主服务(管理服务保持存活);管理服务仍在 dump 列表中,resurrect 后它会先起来继续提供远程启停 */
112
- async function stopMainOnly() {
113
- return withPm2(async () => {
114
- const proc = await findApp();
115
- if (!proc) return "already-stopped";
116
- await pm2cb("delete", APP_NAME);
117
- await safeDump();
118
- return "stopped";
119
- });
120
- }
121
-
122
111
  /** pm2 启停:start / stop / restart(restart = 停主服务 + start,
123
- * start 时管理服务已在线会被 doStart 的幂等检查跳过) */
112
+ * start 时管理服务已在线会被 doStart 的幂等检查跳过)。
113
+ * stopMainOnly 已抽到 utils/service.js(updater 更新前停主服务复用同语义)。 */
124
114
  async function runStartStop(action, startOpts) {
125
115
  if (action === "stop") return stopMainOnly();
126
116
  if (action === "start") return doStart(startOpts);
package/manager/index.js CHANGED
@@ -1,170 +1,182 @@
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 /harness Claude Code / Codex / DeepSeek Harness 代理开关状态
18
- // POST /harness/claude-code body {enabled};开 = 写 ~/.claude/settings.json(备份),关 = 还原备份
19
- // POST /harness/codex body {enabled};开 = 写 ~/.codex/config.toml(备份),关 = 还原备份
20
- // POST /harness/dsh body {enabled};开 = 写 ~/.dsh/settings.yaml + .credentials.yaml(备份),关 = 还原
21
- //
22
- // 配置来源(env 优先,argv 可覆盖,缺省兜底):
23
- // 监听端口 ELLM_MANAGER_PORT(doStart 统一预算 = 主服务端口+1)> argv --port > 29771
24
- // web-ui 产物 ELLM_WEBUI_DIR(可选覆盖)> 按本文件位置推导:包模式 <pkgRoot>/dist/web-ui 仓库模式 <repoRoot>/web-ui/dist
25
- // 主服务参数 ellm-proxy start 注入的 env 同源:ELLM_CONFIG / ELLM_DATA_DIR / ELLM_APP_DIR / ELLM_PORT /
26
- // ELLM_SERVER_JS(主服务 dist/server.js 路径,restart 重启主服务时使用,保证参数不漂移)
27
- import { existsSync } from "node:fs";
28
- import { readFile } from "node:fs/promises";
29
- import { Hono } from "hono";
30
- import { cors } from "hono/cors";
31
- import { proxy } from "hono/proxy";
32
- import { serve } from "@hono/node-server";
33
- import { serveStatic } from "@hono/node-server/serve-static";
34
- import { dirname, join, relative, resolve } from "node:path";
35
- import { fileURLToPath } from "node:url";
36
- import mri from "mri";
37
- import { absolutize } from "../utils/paths.js";
38
- import { resolveGatewayBase } from "../utils/service.js";
39
- import { ICONS, say } from "../utils/ui.js";
40
- import { createServiceApp } from "./api/service.js";
41
- import { createHarnessApp } from "./api/harness/index.js";
42
-
43
- const DEFAULT_SERVER_PORT = 29771; // 手动运行且无 env 时的兜底(默认网关 29770 + 1)
44
- // 本文件在 manager/ 下,包根为上一级
45
- const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
46
-
47
- // web-ui 构建产物目录:ELLM_WEBUI_DIR 优先;缺省按本文件位置推导(包模式 dist/web-ui 与 server.js 同级,
48
- // 仓库模式落到 monorepo web-ui/dist)。启动时判定一次,缺失则跳过静态托管(接口不受影响)。
49
- const WEBUI_DIST_DIR = process.env.ELLM_WEBUI_DIR
50
- ? resolve(process.cwd(), process.env.ELLM_WEBUI_DIR)
51
- : ([join(pkgRoot, "dist", "web-ui"), resolve(pkgRoot, "..", "web-ui", "dist")].find((p) =>
52
- existsSync(join(p, "index.html")),
53
- ) ?? join(pkgRoot, "dist", "web-ui"));
54
- const HAS_WEBUI = existsSync(join(WEBUI_DIST_DIR, "index.html"));
55
-
56
- /** web-ui 管理界面静态托管:/ 出 index.html(hash 路由唯一服务端入口),assets/ 补长缓存头。
57
- * 须在 api 聚合与 /api 反代之后注册——serve-static 未命中才放行,不会遮蔽接口。 */
58
- function registerWebUi(app) {
59
- if (!HAS_WEBUI) return;
60
- // serve-static root 只接受相对 cwd 的路径:把绝对定位换算成当前 cwd 表达(仓库/发布包/任意 cwd 均成立)
61
- const root = relative(process.cwd(), WEBUI_DIST_DIR);
62
- app.get("/", async (c) => {
63
- try {
64
- const buf = await readFile(join(WEBUI_DIST_DIR, "index.html"));
65
- return c.body(new Uint8Array(buf), 200, {
66
- "Content-Type": "text/html; charset=utf-8",
67
- "Cache-Control": "no-cache",
68
- });
69
- } catch {
70
- return c.text("Not Found", 404);
71
- }
72
- });
73
- app.use("/*", (c, next) => {
74
- if (c.req.path === "/") return next(); // 根路径由上面的 serveIndex 处理
75
- return serveStatic({
76
- root,
77
- onFound: (_p, ctx) => {
78
- if (ctx.req.path.startsWith("/assets/")) ctx.header("Cache-Control", "public, max-age=31536000, immutable");
79
- },
80
- })(c, next);
81
- });
82
- }
83
-
84
- /** 创建管理服务 app:CORS 全放开 + api 聚合挂载 + /api 反代主服务 + web-ui 静态托管 + 统一 404/500 */
85
- function createApp({ startOpts }) {
86
- const app = new Hono();
87
-
88
- // CORS 全放开(OPTIONS 预检由中间件自动处理)
89
- app.use("*", cors());
90
-
91
- // api 聚合:进程控制(GET /status · POST /start|stop|restart)与 harness 开关(GET /harness · POST /harness/claude-code|codex|dsh)
92
- app.route("/", createServiceApp({ startOpts }));
93
- app.route("/harness", createHarnessApp({ startOpts }));
94
-
95
- // 反代主服务管理台 API(web-ui 同源访问的 /api/*):端口与 start 口径一致(ELLM_PORT env > 配置文件),
96
- // 每请求解析——主服务端口顺延漂移无感知
97
- app.all("/api/*", (c) => {
98
- const { base } = resolveGatewayBase();
99
- const port = Number(new URL(base).port);
100
- if (!port) {
101
- return c.json(
102
- { ok: false, error: "主服务端口无法解析(ELLM_PORT env 与配置文件 port 均缺失),无法转发 /api/*" },
103
- 502,
104
- );
105
- }
106
- // 目标 URL 必须带上原始查询串:c.req.path 只有 pathname,漏拼 search 会把
107
- // ?id=... / ?page=... 等查询参数全部丢掉(主服务按缺参返回 400 或默认分页)
108
- const incoming = new URL(c.req.raw.url);
109
- return proxy(new URL(incoming.pathname + incoming.search, base), {
110
- method: c.req.method,
111
- headers: c.req.raw.headers,
112
- body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : c.req.raw.body,
113
- duplex: "half", // body 为流时 undici 必需
114
- });
115
- });
116
-
117
- registerWebUi(app);
118
-
119
- app.notFound((c) =>
120
- c.json(
121
- {
122
- ok: false,
123
- error: `未知接口: ${c.req.method} ${c.req.path}(可用: GET /(管理界面), GET /api/*(反代主服务), GET /status, GET /processes, POST /start|stop|restart, GET /harness, POST /harness/claude-code|codex|dsh)`,
124
- },
125
- 404,
126
- ),
127
- );
128
- app.onError((err, c) => {
129
- console.error(`[ellm-proxy-manager] ${c.req.method} ${c.req.path} 失败:`, err?.message ?? err);
130
- return c.json({ ok: false, error: err?.message ?? String(err) }, 500);
131
- });
132
-
133
- return app;
134
- }
135
-
136
- async function main() {
137
- // argv 解析(mri):仅 --port(其余主服务参数一律走 env,与 start 注入链同源)
138
- const argv = mri(process.argv.slice(2));
139
- const listenPort = Number(argv.port) || Number(process.env.ELLM_MANAGER_PORT) || DEFAULT_SERVER_PORT;
140
- // 主服务启动参数(start/restart 时透传给 doStart):ELLM_* env 与 CLI start 注入链同源
141
- const startOpts = absolutize({
142
- config: process.env.ELLM_CONFIG,
143
- dataDir: process.env.ELLM_DATA_DIR,
144
- workingDir: process.env.ELLM_APP_DIR,
145
- serverJs: process.env.ELLM_SERVER_JS || join(pkgRoot, "dist", "server.js"),
146
- });
147
-
148
- const app = createApp({ startOpts });
149
-
150
- const server = serve({ fetch: app.fetch, port: listenPort, hostname: "127.0.0.1" });
151
- server.on("error", (err) => {
152
- console.error(
153
- `错误:管理服务监听 ${listenPort} 失败(${err.code ?? err.message})${err.code === "EADDRINUSE" ? ",端口被占用" : ""}`,
154
- );
155
- process.exit(1);
156
- });
157
-
158
- say(`ellm-proxy 管理服务已启动: http://127.0.0.1:${listenPort}`, ICONS.rocket);
159
- say(
160
- HAS_WEBUI
161
- ? ` 管理界面: http://127.0.0.1:${listenPort}/(web-ui,/api/* 反代主服务)`
162
- : ` 管理界面未托管:web-ui 构建产物缺失(${WEBUI_DIST_DIR}),仓库根执行 pnpm build:web 后重启本服务`,
163
- );
164
- say(
165
- " 接口: GET /status /processes · POST /start /stop /restart · GET /harness · POST /harness/claude-code /harness/codex /harness/dsh",
166
- );
167
- say(" CORS 已全放开(仅监听 127.0.0.1,不对外)");
168
- }
169
-
170
- main();
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/package.json CHANGED
@@ -1,40 +1,44 @@
1
- {
2
- "name": "ellm-proxy",
3
- "version": "0.0.2",
4
- "description": "Self-hosted LLM gateway with admin web UI (pm2-managed)",
5
- "license": "MIT",
6
- "type": "module",
7
- "engines": {
8
- "node": ">=22.13.0"
9
- },
10
- "bin": {
11
- "ellm-proxy": "bin/ellm-proxy.js"
12
- },
13
- "files": [
14
- "bin",
15
- "manager",
16
- "utils",
17
- "dist",
18
- "docs",
19
- "ellm.config.example.json",
20
- "README.md",
21
- "LICENSE"
22
- ],
23
- "scripts": {
24
- "build": "node scripts/build-package.mjs",
25
- "prepack": "node scripts/build-package.mjs"
26
- },
27
- "devDependencies": {
28
- "@types/node": "^22.12.1"
29
- },
30
- "dependencies": {
31
- "@hono/node-server": "^1.19.17",
32
- "cac": "^6.7.14",
33
- "get-node": "^15.0.4",
34
- "hono": "^4.13.5",
35
- "mri": "^1.2.0",
36
- "pm2": "^7.0.4",
37
- "smol-toml": "^1.8.0",
38
- "yaml": "^2.9.0"
39
- }
40
- }
1
+ {
2
+ "name": "ellm-proxy",
3
+ "version": "0.0.6",
4
+ "description": "Self-hosted LLM gateway with admin web UI (pm2-managed)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.13.0"
9
+ },
10
+ "bin": {
11
+ "ellm-proxy": "bin/ellm-proxy.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "manager",
16
+ "utils",
17
+ "dist",
18
+ "docs",
19
+ "ellm.config.example.json",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "node scripts/build-package.mjs",
25
+ "prepack": "node scripts/build-package.mjs",
26
+ "release": "node scripts/release.mjs"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.12.1"
30
+ },
31
+ "dependencies": {
32
+ "@hono/node-server": "^1.19.17",
33
+ "cac": "^6.7.14",
34
+ "get-node": "^15.0.4",
35
+ "hono": "^4.13.5",
36
+ "mri": "^1.2.0",
37
+ "pm2": "^7.0.4",
38
+ "smol-toml": "^1.8.0",
39
+ "yaml": "^2.9.0"
40
+ },
41
+ "publishConfig": {
42
+ "registry": "https://registry.npmjs.org/"
43
+ }
44
+ }