ellm-proxy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/bin/commands/autostart.js +213 -0
  4. package/bin/commands/config.js +8 -0
  5. package/bin/commands/pm2.js +28 -0
  6. package/bin/commands/service.js +107 -0
  7. package/bin/ellm-proxy.js +61 -0
  8. package/dist/package.json +1 -0
  9. package/dist/server.js +12176 -0
  10. package/dist/web-ui/assets/DashboardView-CQdlgirS.js +1 -0
  11. package/dist/web-ui/assets/DashboardView-qrQrkgLA.css +1 -0
  12. package/dist/web-ui/assets/LayoutView-BaY4Pz9W.js +1 -0
  13. package/dist/web-ui/assets/LoginView-CPvz1Ylx.js +1 -0
  14. package/dist/web-ui/assets/LoginView-Dg0mB6ok.css +1 -0
  15. package/dist/web-ui/assets/LogsView-B-bIj36j.js +9 -0
  16. package/dist/web-ui/assets/LogsView-Wl3_adHm.css +1 -0
  17. package/dist/web-ui/assets/ProcessView-0x_ShSwV.js +1 -0
  18. package/dist/web-ui/assets/ProcessView-DzMqBJmd.css +1 -0
  19. package/dist/web-ui/assets/ProvidersView-BxAxDRFH.css +1 -0
  20. package/dist/web-ui/assets/ProvidersView-GQENcvJS.js +31 -0
  21. package/dist/web-ui/assets/SettingsView-BDkymsrS.css +1 -0
  22. package/dist/web-ui/assets/SettingsView-BrAiaskZ.js +1 -0
  23. package/dist/web-ui/assets/config-DNn5cjYY.js +1 -0
  24. package/dist/web-ui/assets/index-BecfPmag.css +1 -0
  25. package/dist/web-ui/assets/index-DFlUx18F.js +96 -0
  26. package/dist/web-ui/assets/providers-CGEmOxne.js +1 -0
  27. package/dist/web-ui/assets/useApi-BRPinb_2.js +1 -0
  28. package/dist/web-ui/assets/useManagerApi-BDWJz5o6.js +1 -0
  29. package/dist/web-ui/assets/useToast-D7xlwSg6.js +1 -0
  30. package/dist/web-ui/config.js +10 -0
  31. package/dist/web-ui/index.html +15 -0
  32. package/docs/DESIGN.md +99 -0
  33. package/ellm.config.example.json +33 -0
  34. package/manager/api/harness/backup.js +20 -0
  35. package/manager/api/harness/claude-code.js +54 -0
  36. package/manager/api/harness/codex.js +123 -0
  37. package/manager/api/harness/dsh.js +175 -0
  38. package/manager/api/harness/index.js +118 -0
  39. package/manager/api/service.js +157 -0
  40. package/manager/index.js +170 -0
  41. package/package.json +40 -0
  42. package/utils/interpreter.js +14 -0
  43. package/utils/paths.js +44 -0
  44. package/utils/pm2.js +130 -0
  45. package/utils/port.js +35 -0
  46. package/utils/service.js +189 -0
  47. package/utils/ui.js +59 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 doyzheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # ellm-proxy
2
+
3
+ 自研 LLM 网关(行为基准 litellm 1.100.0),pm2 后台守护一键启停(跨平台)。主服务(`ellm-proxy`)单进程提供网关与管理台 API;管理服务(`ellm-proxy-manager`,端口 = 主端口 + 1)托管管理界面 web-ui 并反代 `/api/*` 到主服务,主服务停止时仍可远程拉起。
4
+
5
+ > 需求决策与架构设计详见 [docs/DESIGN.md](docs/DESIGN.md)。
6
+
7
+ - `/v1/*` — LLM 网关端点(OpenAI / Anthropic 协议互转,多供应商路由、压缩、鉴权)
8
+ - `/api/*` — 管理台 API(登录鉴权、配置热更新、用量/日志;主服务全局 CORS,供独立部署的 web-ui 跨源调用)
9
+ - `http://127.0.0.1:<主端口+1>/` — 管理界面(manager 托管 web-ui,同源反代 `/api/*`)
10
+
11
+ ## 安装
12
+
13
+ 要求 Node.js **>= 22.13.0**(内置 `node:sqlite`,无需 flag)。
14
+
15
+ ```bash
16
+ npm i -g ellm-proxy
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ ```bash
22
+ ellm-proxy -h # 查看帮助
23
+ ellm-proxy start # pm2 后台守护启动(崩溃自动重启;首次自动创建配置)
24
+ ellm-proxy stop # 停止服务
25
+ ellm-proxy restart # 重启(先 stop 后 start,保证最新参数生效,选项同 start)
26
+ ellm-proxy autostart on # Windows 开机自启(注册登录触发的计划任务)
27
+ ellm-proxy autostart off # 关闭开机自启
28
+ ellm-proxy config # 显示配置文件路径
29
+ ```
30
+
31
+ `start/stop/restart` 基于包内自带的 pm2(局部安装,进程名 `ellm-proxy`):服务由 pm2 守护常驻,关闭终端不影响运行,崩溃自动重启(封顶 10 次,防端口占用无限循环),日志写 `<appDir>/logs/`。服务要求的 Node ≥22.13 由 CLI 解析(当前 node 满足则直接用,否则经 get-node 自动下载一次并缓存),终端默认 node 版本无关。start/stop 成功后自动 `pm2 dump`,可随时用 `pm2 resurrect` 恢复。Windows、Linux、macOS 均可用;运行状态用 `pm2 ls` 查看。
32
+
33
+ **开机自启(跨平台)**:`ellm-proxy autostart on` 注册登录触发的自启项(内容为 `pm2 resurrect`),三平台均无需管理员权限——Windows 写启动文件夹 `.cmd`、macOS 写 LaunchAgent(`launchctl load`)、Linux 写 systemd user unit(`systemctl --user enable`,WSL/无 systemd 环境会报错提示);`autostart off` 移除。
34
+
35
+ ## 运行时布局(默认 `~/.ellm-proxy/`,均可被环境变量覆盖)
36
+
37
+ | 内容 | 默认路径 | 环境变量覆盖 |
38
+ | --- | --- | --- |
39
+ | 配置文件 | `~/.ellm-proxy/ellm.config.json` | `ELLM_CONFIG`(或 `start --config <path>`) |
40
+ | 数据目录(sqlite 日志/缓存) | `~/.ellm-proxy/data/` | `ELLM_DATA_DIR`(或 `start --data-dir <path>`) |
41
+ | 应用目录(配置/数据基准) | `~/.ellm-proxy/` | `ELLM_APP_DIR`(CLI 按 `--working-dir` 自动注入) |
42
+ | web-ui 静态资源 | 包内 `dist/web-ui/`(manager 托管) | `ELLM_WEBUI_DIR`(可选覆盖;manager 按自身位置自动推导) |
43
+ | 运行日志 | `~/.ellm-proxy/logs/` | `start --working-dir <path>`(见 `ellm-proxy -h`) |
44
+
45
+ 所有路径均支持相对路径(按当前工作目录展开)与绝对路径。服务端自身不加载任何 `.env` 文件;`ELLM_MASTER_KEY` 等环境变量直接在会话中导出即可。
46
+
47
+ 首次执行 `ellm-proxy start` 会自动创建示例配置,之后两种方式修改:
48
+
49
+ 1. 直接编辑配置文件;
50
+ 2. 启动服务后访问管理界面 `http://127.0.0.1:<主端口+1>/` 在线修改(实时热更新)。
51
+
52
+ 其他环境变量:`ELLM_PORT`(网关监听端口,或 `start --port`;管理服务固定 `ELLM_PORT + 1`,可用 `ELLM_MANAGER_PORT` 覆盖)、`HOST`(监听地址)、`ELLM_MASTER_KEY`(网关主密钥,低于配置文件 `masterKey` 字段)。启动时端口若被占用会自动顺延到下一个空闲端口(仅本次运行生效,不改配置文件),顺延结果会打印在启动摘要中。
53
+
54
+ ## 本地开发(仓库内)
55
+
56
+ 本包是 `ellm` monorepo 的发布载体:发布版 `dist/server.js` 由 `scripts/build-server.mjs` 从 `backend` 源码直接构建(esbuild 插件在构建期把路径模块替换为 `release/runtime-dir.ts` 包模式实现,backend 源码保持单模式零分支),`web-ui` 产物由 `pnpm build:web` 生成后拷入 `dist/web-ui/`(manager 托管):
57
+
58
+ ```bash
59
+ pnpm pack:proxy # 构建 web-ui → 构建发布版 + 聚合 → npm pack 生成 tgz
60
+ ```
@@ -0,0 +1,213 @@
1
+ // autostart on|off 命令(跨平台开机自启,登录时触发,均无需管理员权限):
2
+ // Windows: 启动文件夹 ellm-proxy-autostart.cmd(shell:startup——schtasks onlogon 需管理员,实测被拒)
3
+ // macOS: LaunchAgent ~/Library/LaunchAgents/com.ellm-proxy.autostart.plist(launchctl load)
4
+ // Linux: systemd user unit ~/.config/systemd/user/ellm-proxy.service(systemctl --user enable)
5
+ // 三者内容一致:登录时执行 <nodePath> <包内 pm2 bin> resurrect(恢复最后一次 pm2 dump)。
6
+ import { writeFileSync, rmSync, existsSync, mkdirSync } from "node:fs";
7
+ import { homedir, platform } from "node:os";
8
+ import { spawn } from "node:child_process";
9
+ import { join } from "node:path";
10
+ import { resolveInterpreter } from "../../utils/interpreter.js";
11
+ import { pm2BinJs } from "../../utils/pm2.js";
12
+ import { defaultAppDir } from "../../utils/paths.js";
13
+
14
+ const AUTOSTART_CMD = "ellm-proxy-autostart.cmd"; // Windows
15
+ const PLIST_LABEL = "com.ellm-proxy.autostart"; // macOS
16
+ const LINUX_UNIT = "ellm-proxy.service"; // Linux
17
+
18
+ /** 用户启动文件夹(Windows shell:startup):登录时自动执行其中的脚本 */
19
+ function windowsStartupDir() {
20
+ const dir = process.env["APPDATA"];
21
+ if (!dir) throw new Error("无法定位启动文件夹(APPDATA 环境变量未设置)。");
22
+ return join(dir, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
23
+ }
24
+
25
+ /** spawn 执行外部命令,返回退出码;进程无法启动(ENOENT 等)返回 127 */
26
+ function runExit(cmd, args) {
27
+ return new Promise((resolvePromise) => {
28
+ const child = spawn(cmd, args, { stdio: "ignore", shell: false });
29
+ child.on("error", () => resolvePromise(127));
30
+ child.on("exit", (code) => resolvePromise(code ?? 1));
31
+ });
32
+ }
33
+
34
+ // —— Windows:启动文件夹 .cmd ————————————————————————————————————————
35
+
36
+ function windowsPaths() {
37
+ return { itemPath: join(windowsStartupDir(), AUTOSTART_CMD) };
38
+ }
39
+
40
+ async function windowsOn(nodePath, pm2Js) {
41
+ // cmd 内容全 ASCII(含中文的注释交给外部帮助输出,避免 cmd 在 GBK 代码页下乱码)
42
+ const body = [
43
+ "@echo off",
44
+ `rem Auto-generated by "ellm-proxy autostart on". Disable with "ellm-proxy autostart off".`,
45
+ `"${nodePath}" "${pm2Js}" resurrect >nul 2>&1`,
46
+ "",
47
+ ].join("\r\n");
48
+ const { itemPath } = windowsPaths();
49
+ writeFileSync(itemPath, body, "utf-8");
50
+ }
51
+
52
+ async function windowsOff() {
53
+ const { itemPath } = windowsPaths();
54
+ if (!existsSync(itemPath)) return false;
55
+ rmSync(itemPath, { force: true });
56
+ return true;
57
+ }
58
+
59
+ // —— macOS:LaunchAgent plist ——————————————————————————————————————————
60
+
61
+ function darwinPaths() {
62
+ return { itemPath: join(homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`) };
63
+ }
64
+
65
+ async function darwinOn(nodePath, pm2Js) {
66
+ const { itemPath } = darwinPaths();
67
+ const logPath = join(defaultAppDir, "logs", "autostart.log");
68
+ mkdirSync(join(defaultAppDir, "logs"), { recursive: true });
69
+ const body = [
70
+ '<?xml version="1.0" encoding="UTF-8"?>',
71
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
72
+ '<plist version="1.0">',
73
+ "<dict>",
74
+ ` <key>Label</key><string>${PLIST_LABEL}</string>`,
75
+ " <key>ProgramArguments</key>",
76
+ " <array>",
77
+ ` <string>${nodePath}</string>`,
78
+ ` <string>${pm2Js}</string>`,
79
+ " <string>resurrect</string>",
80
+ " </array>",
81
+ " <key>RunAtLoad</key><true/>",
82
+ ` <key>StandardOutPath</key><string>${logPath}</string>`,
83
+ ` <key>StandardErrorPath</key><string>${logPath}</string>`,
84
+ "</dict>",
85
+ "</plist>",
86
+ "",
87
+ ].join("\n");
88
+ writeFileSync(itemPath, body, "utf-8");
89
+ // 先卸载旧定义再加载(幂等;未加载过时 unload 报错属正常)
90
+ await runExit("launchctl", ["unload", itemPath]);
91
+ const code = await runExit("launchctl", ["load", itemPath]);
92
+ if (code !== 0) {
93
+ throw new Error(`launchctl load 失败(退出码 ${code})。请手动执行: launchctl load "${itemPath}"`);
94
+ }
95
+ }
96
+
97
+ async function darwinOff() {
98
+ const { itemPath } = darwinPaths();
99
+ if (!existsSync(itemPath)) return false;
100
+ await runExit("launchctl", ["unload", itemPath]);
101
+ rmSync(itemPath, { force: true });
102
+ return true;
103
+ }
104
+
105
+ // —— Linux:systemd user unit ——————————————————————————————————————————
106
+
107
+ function linuxPaths() {
108
+ return { itemPath: join(homedir(), ".config", "systemd", "user", LINUX_UNIT) };
109
+ }
110
+
111
+ async function linuxOn(nodePath, pm2Js) {
112
+ const { itemPath } = linuxPaths();
113
+ mkdirSync(join(homedir(), ".config", "systemd", "user"), { recursive: true });
114
+ const body = [
115
+ "[Unit]",
116
+ "Description=ellm-proxy autostart (pm2 resurrect)",
117
+ "After=default.target",
118
+ "",
119
+ "[Service]",
120
+ "Type=oneshot",
121
+ `ExecStart="${nodePath}" "${pm2Js}" resurrect`,
122
+ "",
123
+ "[Install]",
124
+ "WantedBy=default.target",
125
+ "",
126
+ ].join("\n");
127
+ writeFileSync(itemPath, body, "utf-8");
128
+ const reload = await runExit("systemctl", ["--user", "daemon-reload"]);
129
+ if (reload !== 0) {
130
+ throw new Error(
131
+ `systemctl --user daemon-reload 失败(退出码 ${reload})。当前环境可能无 systemd(WSL/容器),请手动启用或删除: ${itemPath}`,
132
+ );
133
+ }
134
+ const enable = await runExit("systemctl", ["--user", "enable", LINUX_UNIT]);
135
+ if (enable !== 0) {
136
+ throw new Error(
137
+ `systemctl --user enable 失败(退出码 ${enable})。请手动执行: systemctl --user enable ${LINUX_UNIT}`,
138
+ );
139
+ }
140
+ }
141
+
142
+ async function linuxOff() {
143
+ const { itemPath } = linuxPaths();
144
+ if (!existsSync(itemPath)) return false;
145
+ await runExit("systemctl", ["--user", "disable", LINUX_UNIT]); // 容错:未启用过时报错不影响删除
146
+ rmSync(itemPath, { force: true });
147
+ await runExit("systemctl", ["--user", "daemon-reload"]);
148
+ return true;
149
+ }
150
+
151
+ // —— 命令入口:平台分支 ————————————————————————————————————————————————
152
+
153
+ async function autostartOn() {
154
+ const nodePath = await resolveInterpreter();
155
+ const pm2Js = pm2BinJs();
156
+ let itemPath;
157
+ switch (platform()) {
158
+ case "win32":
159
+ itemPath = windowsPaths().itemPath;
160
+ await windowsOn(nodePath, pm2Js);
161
+ break;
162
+ case "darwin":
163
+ itemPath = darwinPaths().itemPath;
164
+ await darwinOn(nodePath, pm2Js);
165
+ break;
166
+ case "linux":
167
+ itemPath = linuxPaths().itemPath;
168
+ await linuxOn(nodePath, pm2Js);
169
+ break;
170
+ default:
171
+ throw new Error(`暂不支持的平台: ${platform()}。可改用 pm2 自带的 pm2 startup。`);
172
+ }
173
+ console.log("开机自启已开启(登录时触发):");
174
+ console.log(` 自启项 : ${itemPath}(pm2 resurrect 恢复服务)`);
175
+ console.log(" 禁用方式 : ellm-proxy autostart off");
176
+ }
177
+
178
+ async function autostartOff() {
179
+ let removed;
180
+ switch (platform()) {
181
+ case "win32":
182
+ removed = await windowsOff();
183
+ break;
184
+ case "darwin":
185
+ removed = await darwinOff();
186
+ break;
187
+ case "linux":
188
+ removed = await linuxOff();
189
+ break;
190
+ default:
191
+ throw new Error(`暂不支持的平台: ${platform()}。可改用 pm2 自带的 pm2 startup。`);
192
+ }
193
+ if (!removed) {
194
+ console.log("未找到自启项(可能未开启开机自启),无需操作。");
195
+ return;
196
+ }
197
+ console.log("开机自启已关闭(自启项已移除)。");
198
+ }
199
+
200
+ export function register(cli, run) {
201
+ cli
202
+ .command(
203
+ "autostart <action>",
204
+ "开机自启(登录时触发):on=注册自启项(Win 启动文件夹/mac LaunchAgent/Linux systemd user),off=移除",
205
+ )
206
+ .action((action) => {
207
+ if (action !== "on" && action !== "off") {
208
+ console.error(`未知参数: ${action}(用法: ellm-proxy autostart on|off)`);
209
+ process.exit(1);
210
+ }
211
+ return run(action === "on" ? autostartOn : autostartOff);
212
+ });
213
+ }
@@ -0,0 +1,8 @@
1
+ // config 命令:显示配置文件路径(不存在时自动创建示例配置)。
2
+ import { ensureConfig } from "../../utils/paths.js";
3
+
4
+ export function register(cli) {
5
+ cli.command("config", "显示配置文件路径(不存在时自动创建示例配置)").action((opts) => {
6
+ console.log(ensureConfig(opts.config));
7
+ });
8
+ }
@@ -0,0 +1,28 @@
1
+ // pm2 透传命令:ellm-proxy pm2 <任意 pm2 参数>(如 list/ls/logs/monit/describe/flush)。
2
+ // 用包内局部 pm2 执行,用户无需自装全局 pm2;stdio 直接继承,logs/monit 等交互命令可实时输出、
3
+ // Ctrl-C 退出。透传参数不经 cac 解析(在 CLI 入口先于 cac 拦截),pm2 的 -- 选项原样可达。
4
+ import { spawn } from "node:child_process";
5
+ import { pm2BinJs } from "../../utils/pm2.js";
6
+
7
+ /** 以包内 pm2 CLI 执行任意参数(node 版本无关紧要:pm2 bin 脚本兼容;daemon 连接与 interpreter 无关) */
8
+ export async function doPassthrough(args) {
9
+ if (args.length === 0) {
10
+ args = ["list"]; // 无参数 = 默认展示进程列表
11
+ }
12
+ return new Promise((resolve, reject) => {
13
+ const child = spawn(process.execPath, [pm2BinJs(), ...args], { stdio: "inherit", shell: false });
14
+ child.on("error", reject);
15
+ // pm2 退出码原样透传;exit 回调内 process.exit 结束进程(promise 永不 resolve)
16
+ child.on("exit", (code) => process.exit(code ?? 1));
17
+ });
18
+ }
19
+
20
+ export function register(cli, run) {
21
+ // cac 仅声明命令用于 -h 展示;实际执行走 CLI 入口的 pm2 拦截(保证 pm2 选项不被 cac 解析吞掉)
22
+ cli
23
+ .command(
24
+ "pm2 [args...]",
25
+ "透传执行包内 pm2 任意子命令(如 list/ls/logs/monit/describe/flush;pm2 的 -- 选项原样透传)",
26
+ )
27
+ .action(() => {});
28
+ }
@@ -0,0 +1,107 @@
1
+ // 服务生命周期 CLI 命令:start / stop / restart(restart = stop + start,保证最新参数生效)。
2
+ // 进程编排核心在 utils/service.js,本文件只做命令装配与人读输出(摘要文案)。
3
+ import { join } from "node:path";
4
+ import { APP_NAME, MANAGER_APP_NAME, pkgRoot, defaultAppDir, absolutize } from "../../utils/paths.js";
5
+ import { withPm2, findApp } from "../../utils/pm2.js";
6
+ import { doStart, doStop, resolvePort } from "../../utils/service.js";
7
+ import { ICONS, say, field } from "../../utils/ui.js";
8
+
9
+ /** 摘要块(start/restart 成功 与 默认命令已运行 共用的输出形态,图标 + 对齐标签) */
10
+ function printInfo({ appDir, port, managerPort, logsHint }) {
11
+ if (port) {
12
+ field(ICONS.gateway, "网关端点", `http://localhost:${port}`);
13
+ field(ICONS.api, "管理台 API", `http://localhost:${port}/api/*`);
14
+ if (managerPort) field(ICONS.webui, "管理界面", `http://127.0.0.1:${managerPort}/`);
15
+ } else {
16
+ field(
17
+ ICONS.question,
18
+ "访问入口",
19
+ "端口未知(请检查配置):网关 = http://localhost:<端口> · 管理界面 = http://127.0.0.1:<主端口+1>/",
20
+ );
21
+ }
22
+ field(
23
+ ICONS.api,
24
+ "管理服务",
25
+ `${managerPort ? `http://127.0.0.1:${managerPort}` : "端口未知(= 网关端口 + 1)"}(启停/代理开关接口,进程名 ${MANAGER_APP_NAME})`,
26
+ );
27
+ field(ICONS.folder, "应用目录", appDir);
28
+ field(ICONS.status, "运行状态", `pm2 ls(进程名 ${APP_NAME})· 日志 ${logsHint ?? "<应用目录>/logs/"}`);
29
+ }
30
+
31
+ /** 启动摘要(start/restart 成功后输出) */
32
+ function printSummary(appDir, config) {
33
+ printInfo({
34
+ appDir,
35
+ port: resolvePort(process.env.ELLM_PORT, config),
36
+ managerPort: process.env.ELLM_MANAGER_PORT,
37
+ });
38
+ }
39
+
40
+ /** 运行中服务的信息(默认命令:ellm-proxy 不带参数时已运行则展示此信息)。
41
+ * 端口/路径以 pm2 记录的进程 env 为准(与当前 CLI 会话 env 无关) */
42
+ function printRunningInfo(proc) {
43
+ const pmEnv = proc.pm2_env?.env ?? {};
44
+ const appDir = pmEnv.ELLM_APP_DIR || proc.pm2_env?.pm_cwd || defaultAppDir;
45
+ const uptime = proc.pm2_env?.pm_uptime ? new Date(proc.pm2_env.pm_uptime).toLocaleString() : "";
46
+ say(`${APP_NAME} 已在运行(pid ${proc.pid}${uptime ? ` · 自 ${uptime} 起` : ""})`, ICONS.running);
47
+ printInfo({
48
+ appDir,
49
+ port: resolvePort(pmEnv.ELLM_PORT, pmEnv.ELLM_CONFIG),
50
+ managerPort: pmEnv.ELLM_MANAGER_PORT,
51
+ logsHint: join(appDir, "logs"),
52
+ });
53
+ }
54
+
55
+ /** start 成功后的统一输出 */
56
+ function printStarted({ proc, managerProc, appDir, config }) {
57
+ console.log();
58
+ say(`启动完成(主服务 pid ${proc.pid}${managerProc ? ` · 管理服务 pid ${managerProc.pid}` : ""})`, ICONS.success);
59
+ console.log();
60
+ printSummary(appDir, config);
61
+ }
62
+
63
+ /** 默认命令(ellm-proxy 不带参数):已运行则打印服务信息,未运行则启动(同 start) */
64
+ export async function doStartOrInfo(opts) {
65
+ const existing = await withPm2(() => findApp());
66
+ if (existing) return printRunningInfo(existing);
67
+ printStarted(await doStart(absolutize(opts)));
68
+ }
69
+
70
+ /** start/restart/默认命令共用的选项集 */
71
+ export function withStartOptions(cmd) {
72
+ return cmd
73
+ .option("--port <n>", "监听端口(也可用 ELLM_PORT 环境变量)")
74
+ .option("--config <path>", "配置文件路径(也可用 ELLM_CONFIG 环境变量)")
75
+ .option("--data-dir <path>", "数据目录(也可用 ELLM_DATA_DIR 环境变量;默认 <appDir>/data)")
76
+ .option("--working-dir <path>", "应用目录(配置/数据/日志基目录 ELLM_APP_DIR,默认 ~/.ellm-proxy)")
77
+ .option("--server-js <path>", "显式指定 dist/server.js 路径(默认本包 dist/server.js)");
78
+ }
79
+
80
+ export function register(cli, run) {
81
+ withStartOptions(cli.command("start", "pm2 后台守护启动(崩溃自动重启;首次自动创建配置)")).action((opts) =>
82
+ run(() => doStart(absolutize(opts)).then(printStarted)),
83
+ );
84
+
85
+ withStartOptions(cli.command("restart", "重启服务(先 stop 后 start,保证最新参数生效,选项同 start)")).action(
86
+ (opts) =>
87
+ run(async () => {
88
+ const stopped = await doStop();
89
+ if (!stopped.main && !stopped.manager) say(`${APP_NAME} 未在运行(可能已停止)。`, ICONS.idle);
90
+ printStarted(await doStart(absolutize(opts)));
91
+ }),
92
+ );
93
+
94
+ cli.command("stop", "停止服务(pm2 删除守护进程,不影响其他 node 进程)").action(() =>
95
+ run(async () => {
96
+ const stopped = await doStop();
97
+ if (!stopped.main && !stopped.manager) {
98
+ say(`${APP_NAME} 未在运行(可能已停止)。`, ICONS.idle);
99
+ return;
100
+ }
101
+ say(
102
+ stopped.manager ? `${APP_NAME} 已停止(管理服务 ${MANAGER_APP_NAME} 一并停止)。` : `${APP_NAME} 已停止。`,
103
+ ICONS.stopped,
104
+ );
105
+ }),
106
+ );
107
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // ellm-proxy CLI 入口(bin,cac 框架):仅做命令装配,实现在 commands/(命令)与 utils/(共享工具)。
3
+ //
4
+ // 命令:(无参数 = 默认启动)start / stop / restart / pm2 <args...> / autostart on|off / config / -h / --version(选项详见 -h)
5
+ //
6
+ // 运行时布局(默认 ~/.ellm-proxy/,均可被选项/环境变量覆盖):
7
+ // 配置: --config > ELLM_CONFIG 环境变量 > ~/.ellm-proxy/ellm.config.json(首次自动创建)
8
+ // 数据: ELLM_DATA_DIR 环境变量 > ~/.ellm-proxy/data
9
+ // 日志: <appDir>/logs/ellm-out.log · ellm-err.log(主服务)+ manager-out.log · manager-err.log(管理服务)
10
+ //
11
+ // 进程管理(跨平台,pm2 JS API 封装见 utils/pm2.js,编排核心见 utils/service.js):
12
+ // start 会一并托管两个进程——主服务 dist/server.js(name=ellm-proxy)与管理服务 manager/index.js
13
+ // (name=ellm-proxy-manager,启停/重启主服务 + Claude Code/Codex 代理开关)。
14
+ // 崩溃自动重启、日志落盘、dump/resurrect(开机自启一并恢复)。服务要求的 Node >=22.13.0 通过
15
+ // interpreter 保证(utils/interpreter.js),pm2 daemon 的 node 版本无关紧要。
16
+ // start/stop 成功后自动 pm2 dump,供开机自启 resurrect 恢复。`ellm-proxy pm2 <args>` 可透传
17
+ // 任意 pm2 子命令(list/logs/monit/...,用包内局部 pm2,无需自装全局)。
18
+ //
19
+ // 开机自启(commands/autostart.js,跨平台):登录时触发、无需管理员——Windows 写启动文件夹
20
+ // .cmd(shell:startup)、macOS 写 LaunchAgent plist(launchctl load)、Linux 写 systemd user
21
+ // unit(systemctl --user enable);内容一致 = pm2 resurrect 恢复最后一次 dump。
22
+
23
+ import { cac } from "cac";
24
+ import { pkg } from "../utils/paths.js";
25
+ import { error as uiError } from "../utils/ui.js";
26
+ import * as serviceCmd from "./commands/service.js";
27
+ import * as autostartCmd from "./commands/autostart.js";
28
+ import * as configCmd from "./commands/config.js";
29
+ import * as pm2Cmd from "./commands/pm2.js";
30
+
31
+ const cli = cac("ellm-proxy");
32
+
33
+ /** 统一错误出口:命令实现抛错 → 打印中文错误信息并以退出码 1 结束 */
34
+ async function run(action) {
35
+ try {
36
+ await action();
37
+ } catch (err) {
38
+ console.error();
39
+ uiError(`错误:${err?.message ?? err}`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ serviceCmd.register(cli, run);
45
+ autostartCmd.register(cli, run);
46
+ configCmd.register(cli);
47
+ pm2Cmd.register(cli, run);
48
+
49
+ // pm2 透传:在 cac 解析前拦截 `ellm-proxy pm2 <...args>`,pm2 的 -- 选项原样可达(cac 会吞掉未知 -- 选项)
50
+ if (process.argv[2] === "pm2") {
51
+ run(() => pm2Cmd.doPassthrough(process.argv.slice(3)));
52
+ } else {
53
+ // 默认命令(无参数):启动服务,已运行则显示服务信息(选项同 start)
54
+ serviceCmd
55
+ .withStartOptions(cli.command("", "启动服务(已运行则显示服务信息,选项同 start)"))
56
+ .action((opts) => run(() => serviceCmd.doStartOrInfo(opts)));
57
+
58
+ cli.help();
59
+ cli.version(pkg.version);
60
+ cli.parse();
61
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }