ellm-proxy 0.0.7 → 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 -63
- package/dist/cli.js +35780 -0
- package/dist/server.js +16723 -6451
- package/dist/web-ui/assets/{DashboardView-BCwrFrE2.js → DashboardView-3c_i_3VF.js} +1 -1
- 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-Dp7NmXtf.js → LoginView-J23vsMu2.js} +1 -1
- package/dist/web-ui/assets/{LogsView-Dyy_zkgg.js → LogsView-2h2Y0XE8.js} +1 -1
- 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-CYFs76wJ.js → ProvidersView-bE1Hv9D-.js} +1 -1
- 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-DX6S_KpK.js → SetupView-Cgj7ubNz.js} +1 -1
- package/dist/web-ui/assets/{config-CRWaUFQc.js → config-nvSp8hHp.js} +1 -1
- package/dist/web-ui/assets/{index-X5PdkJlA.js → index-sPrBBnf5.js} +2 -2
- package/dist/web-ui/assets/{providers-H_9x778D.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/package.json +2 -17
- package/bin/commands/autostart.js +0 -37
- package/bin/commands/config.js +0 -8
- package/bin/commands/credentials.js +0 -100
- package/bin/commands/pm2.js +0 -28
- package/bin/commands/service.js +0 -107
- package/dist/web-ui/assets/LayoutView-BUA8-IzK.css +0 -1
- package/dist/web-ui/assets/LayoutView-BePJfgia.js +0 -1
- package/dist/web-ui/assets/ProcessView-CpPpMtFg.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-mPcaVAD2.js +0 -1
- package/dist/web-ui/assets/useManagerApi-C1_COxKg.js +0 -1
- package/dist/web-ui/assets/useToast-MtbkGJiM.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 -233
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ellm-proxy",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Self-hosted LLM gateway with admin web UI (
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "Self-hosted LLM gateway with admin web UI (single process, built-in process management)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
|
-
"manager",
|
|
16
|
-
"utils",
|
|
17
15
|
"dist",
|
|
18
16
|
"docs",
|
|
19
17
|
"ellm.config.example.json",
|
|
@@ -25,19 +23,6 @@
|
|
|
25
23
|
"prepack": "node scripts/build-package.mjs",
|
|
26
24
|
"release": "node scripts/release.mjs"
|
|
27
25
|
},
|
|
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
26
|
"publishConfig": {
|
|
42
27
|
"registry": "https://registry.npmjs.org/"
|
|
43
28
|
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// autostart on|off 命令(CLI 薄壳):平台实现与状态探测在 utils/autostart.js(被管理服务复用),
|
|
2
|
-
// 本文件只做命令装配与人读输出。
|
|
3
|
-
import { autostartStatus, enableAutostart, disableAutostart } from "../../utils/autostart.js";
|
|
4
|
-
|
|
5
|
-
async function autostartOn() {
|
|
6
|
-
const { itemPath } = await enableAutostart();
|
|
7
|
-
console.log("开机自启已开启(登录时触发):");
|
|
8
|
-
console.log(` 自启项 : ${itemPath}(pm2 resurrect 恢复服务)`);
|
|
9
|
-
console.log(" 禁用方式 : ellm-proxy autostart off");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function autostartOff() {
|
|
13
|
-
const { removed } = await disableAutostart();
|
|
14
|
-
if (!removed) {
|
|
15
|
-
console.log("未找到自启项(可能未开启开机自启),无需操作。");
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
console.log("开机自启已关闭(自启项已移除)。");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function register(cli, run) {
|
|
22
|
-
cli
|
|
23
|
-
.command(
|
|
24
|
-
"autostart <action>",
|
|
25
|
-
"开机自启(登录时触发):on=注册自启项(Win 启动文件夹/mac LaunchAgent/Linux systemd user),off=移除",
|
|
26
|
-
)
|
|
27
|
-
.action((action) => {
|
|
28
|
-
if (action !== "on" && action !== "off") {
|
|
29
|
-
console.error(`未知参数: ${action}(用法: ellm-proxy autostart on|off)`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
return run(action === "on" ? autostartOn : autostartOff);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 状态探测保留导出(潜在 CLI status 输出用;当前 manager 走 utils 直连)
|
|
37
|
-
export { autostartStatus };
|
package/bin/commands/config.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// 凭据维护 CLI:主密钥与管理台登录账号的查看/设置/清除/重置(忘记凭据时的自救通道)。
|
|
2
|
-
// ellm-proxy master-key 查看当前主密钥
|
|
3
|
-
// ellm-proxy master-key <key> 设置自定义主密钥(≥8 位)
|
|
4
|
-
// ellm-proxy master-key --random 随机生成主密钥
|
|
5
|
-
// ellm-proxy master-key --clear 清除主密钥(/v1 恢复免鉴权,仅建议本机使用)
|
|
6
|
-
// ellm-proxy account 查看当前登录账号
|
|
7
|
-
// ellm-proxy account <username> <password> 设置登录账号(密码 ≥6 位)
|
|
8
|
-
// ellm-proxy account --reset 重置为默认账号 admin/admin
|
|
9
|
-
// 直接改写 ellm.config.json(与后端同一查找链);后端每请求动态读取该文件,改完即时生效无需重启。
|
|
10
|
-
import { randomBytes } from "node:crypto";
|
|
11
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { configPath, ensureConfig } from "../../utils/paths.js";
|
|
13
|
-
|
|
14
|
-
const MIN_MASTER_KEY = 8;
|
|
15
|
-
const MIN_PASSWORD = 6;
|
|
16
|
-
|
|
17
|
-
function loadConfig(override) {
|
|
18
|
-
const path = configPath(override);
|
|
19
|
-
return { path, config: JSON.parse(readFileSync(path, "utf-8")) };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function saveConfig(path, config) {
|
|
23
|
-
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function register(cli, run) {
|
|
27
|
-
cli
|
|
28
|
-
.command("master-key [key]", "查看/设置主密钥(--random 随机生成 · --clear 清除;设置后 /v1/* 需携带 Bearer <key>)")
|
|
29
|
-
.option("--random", "随机生成 43 位强密钥")
|
|
30
|
-
.option("--clear", "清除主密钥(/v1/* 恢复免鉴权,仅建议本机使用)")
|
|
31
|
-
.option("--config <path>", "配置文件路径(默认 ~/.ellm-proxy/ellm.config.json)")
|
|
32
|
-
.action((key, opts) =>
|
|
33
|
-
run(() => {
|
|
34
|
-
ensureConfig(opts.config);
|
|
35
|
-
const { path, config } = loadConfig(opts.config);
|
|
36
|
-
if (opts.random) {
|
|
37
|
-
const generated = randomBytes(32).toString("base64url");
|
|
38
|
-
saveConfig(path, { ...config, masterKey: generated });
|
|
39
|
-
console.log(`已生成随机主密钥:\n${generated}\n(请妥善保存,调用 /v1/* 时作为 Bearer token 使用)`);
|
|
40
|
-
} else if (opts.clear) {
|
|
41
|
-
saveConfig(path, { ...config, masterKey: "" });
|
|
42
|
-
console.log("已清除主密钥:/v1/* 恢复免鉴权(本地个人使用模式)。");
|
|
43
|
-
} else if (key) {
|
|
44
|
-
const trimmed = String(key).trim();
|
|
45
|
-
if (trimmed.length < MIN_MASTER_KEY) throw new Error(`主密钥至少 ${MIN_MASTER_KEY} 位`);
|
|
46
|
-
saveConfig(path, { ...config, masterKey: trimmed });
|
|
47
|
-
console.log("主密钥已更新,调用 /v1/* 时使用 Bearer <新密钥>。");
|
|
48
|
-
} else {
|
|
49
|
-
const current = typeof config.masterKey === "string" ? config.masterKey.trim() : "";
|
|
50
|
-
if (current === "") {
|
|
51
|
-
console.log("当前未设置主密钥:/v1/* 免鉴权(本地个人使用模式)。");
|
|
52
|
-
console.log(`设置方式:ellm-proxy master-key <key>(≥${MIN_MASTER_KEY} 位)或 --random`);
|
|
53
|
-
} else {
|
|
54
|
-
console.log(`当前主密钥: ${current}`);
|
|
55
|
-
}
|
|
56
|
-
if (process.env["ELLM_MASTER_KEY"]) {
|
|
57
|
-
console.log("注意:检测到环境变量 ELLM_MASTER_KEY(配置文件未设置时才生效)。");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
console.log(`配置文件: ${path}(后端动态读取,无需重启即时生效)`);
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
cli
|
|
65
|
-
.command("account [username] [password]", "查看/设置管理台登录账号(--reset 重置为 admin/admin;改后旧登录态立即失效)")
|
|
66
|
-
.option("--reset", "重置为默认账号 admin/admin")
|
|
67
|
-
.option("--config <path>", "配置文件路径(默认 ~/.ellm-proxy/ellm.config.json)")
|
|
68
|
-
.action((username, password, opts) =>
|
|
69
|
-
run(() => {
|
|
70
|
-
ensureConfig(opts.config);
|
|
71
|
-
const { path, config } = loadConfig(opts.config);
|
|
72
|
-
const webUi = typeof config["web-ui"] === "object" && config["web-ui"] !== null ? config["web-ui"] : {};
|
|
73
|
-
if (opts.reset) {
|
|
74
|
-
saveConfig(path, { ...config, "web-ui": { ...webUi, "login-account": {} } });
|
|
75
|
-
console.log("已重置为默认账号 admin/admin(旧登录 token 立即失效)。");
|
|
76
|
-
} else if (username && password) {
|
|
77
|
-
const name = String(username).trim();
|
|
78
|
-
const pass = String(password);
|
|
79
|
-
if (name === "") throw new Error("用户名不能为空");
|
|
80
|
-
if (pass.length < MIN_PASSWORD) throw new Error(`密码至少 ${MIN_PASSWORD} 位`);
|
|
81
|
-
saveConfig(path, {
|
|
82
|
-
...config,
|
|
83
|
-
"web-ui": { ...webUi, "login-account": { username: name, password: pass } },
|
|
84
|
-
});
|
|
85
|
-
console.log(`登录账号已更新为 ${name}(旧登录 token 立即失效)。`);
|
|
86
|
-
} else {
|
|
87
|
-
const account = webUi["login-account"] ?? {};
|
|
88
|
-
const name = typeof account.username === "string" && account.username.trim() !== "" ? account.username.trim() : "";
|
|
89
|
-
if (name === "") {
|
|
90
|
-
console.log("当前未在配置文件中设置账号:使用 admin/admin(或 WEBUI_USERNAME/WEBUI_PASSWORD 环境变量)。");
|
|
91
|
-
} else {
|
|
92
|
-
console.log(`当前账号: ${name}`);
|
|
93
|
-
console.log(`当前密码: ${account.password}`);
|
|
94
|
-
}
|
|
95
|
-
console.log("设置方式:ellm-proxy account <username> <password>,重置:ellm-proxy account --reset");
|
|
96
|
-
}
|
|
97
|
-
console.log(`配置文件: ${path}(后端动态读取,无需重启即时生效)`);
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
100
|
-
}
|
package/bin/commands/pm2.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
package/bin/commands/service.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.mt-12[data-v-e326b723]{margin-top:12px}.check-body[data-v-e326b723]{min-height:80px}.version-row[data-v-e326b723]{display:flex;align-items:center;gap:8px;margin-bottom:8px}.version-label[data-v-e326b723]{color:var(--el-text-color-secondary);font-size:13px}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as E,u as N,c as b,w as e,r as f,a,o as _,b as W,e as D,F as z,f as i,g as o,h as u,t as x,i as $,E as I,j as L,_ as S,k as F,l as R,m as j,n as O}from"./index-X5PdkJlA.js";import{m as T}from"./useManagerApi-C1_COxKg.js";import{u as q}from"./useToast-MtbkGJiM.js";const G={class:"check-body"},H={class:"version-row"},J={class:"version-row"},K=E({__name:"UpdateCheckDialog",props:{modelValue:{type:Boolean,default:!1},modelModifiers:{}},emits:["update:modelValue"],setup(B){const{toast:w}=q(),g=N(B,"modelValue"),d=f(!1),c=f(!1),n=f(null),v=f(null),p=f(null),r=async()=>{n.value=null,v.value=null,p.value=null,d.value=!0;try{n.value=await T.checkUpdate()}catch(l){v.value=l.message||"检查更新失败"}finally{d.value=!1}},t=async()=>{if(n.value){try{await I.confirm(`确定更新到 ${n.value.latestVersion}?更新期间主服务将短暂中断(停服安装后自动重启),管理服务随后自重启。`,"更新确认",{confirmButtonText:"立即更新",cancelButtonText:"取消",type:"warning"})}catch{return}c.value=!0;try{const l=await T.applyUpdate();p.value=l.note??`已更新 ${l.fromVersion} → ${l.toVersion}`,w("更新完成","success"),n.value={hasUpdate:!1,currentVersion:l.toVersion,latestVersion:l.toVersion,checkedAt:Date.now()}}catch(l){w(l.message||"更新失败","error")}finally{c.value=!1}}};return(l,s)=>{const k=a("el-tag"),V=a("el-alert"),y=a("el-button"),h=a("el-empty"),C=a("el-dialog"),U=L("loading");return _(),b(C,{modelValue:g.value,"onUpdate:modelValue":s[2]||(s[2]=m=>g.value=m),title:"检查更新",width:"420px","close-on-click-modal":!c.value,onOpen:r},{footer:e(()=>{var m;return[(m=n.value)!=null&&m.hasUpdate?(_(),b(y,{key:0,type:"primary",loading:c.value,onClick:t},{default:e(()=>[u(x(c.value?"更新中...":"立即更新"),1)]),_:1},8,["loading"])):$("",!0),o(y,{onClick:s[1]||(s[1]=A=>g.value=!1)},{default:e(()=>[...s[6]||(s[6]=[u("关闭",-1)])]),_:1})]}),default:e(()=>[W((_(),D("div",G,[n.value?(_(),D(z,{key:0},[i("div",H,[s[3]||(s[3]=i("span",{class:"version-label"},"当前版本",-1)),o(k,{size:"small"},{default:e(()=>[u(x(n.value.currentVersion),1)]),_:1})]),i("div",J,[s[4]||(s[4]=i("span",{class:"version-label"},"最新版本",-1)),o(k,{type:n.value.hasUpdate?"warning":"success",size:"small","disable-transitions":""},{default:e(()=>[u(x(n.value.latestVersion),1)]),_:1},8,["type"])]),o(V,{title:n.value.hasUpdate?`发现新版本 ${n.value.latestVersion}`:"已是最新版本",type:n.value.hasUpdate?"warning":"success","show-icon":"",closable:!1,class:"mt-12"},null,8,["title","type"])],64)):!d.value&&v.value?(_(),b(h,{key:1,description:v.value},{default:e(()=>[o(y,{type:"primary",onClick:r},{default:e(()=>[...s[5]||(s[5]=[u("重试",-1)])]),_:1})]),_:1},8,["description"])):$("",!0)])),[[U,d.value]]),p.value?(_(),b(V,{key:0,title:p.value,type:"info","show-icon":"",class:"mt-12",onClose:s[0]||(s[0]=m=>p.value=null)},null,8,["title"])):$("",!0)]),_:1},8,["modelValue","close-on-click-modal"])}}}),P=S(K,[["__scopeId","data-v-e326b723"]]),Q={class:"layout"},X={class:"header-nav"},Y={class:"header-actions"},Z={class:"main"},ee={class:"page"},se=E({__name:"LayoutView",setup(B){const w=O(),g=j(),d=f(!1),c=(()=>{const r=localStorage.getItem("webui_token")||"";try{return atob(r).split(":")[0]||"用户"}catch{return"用户"}})(),n=R(()=>{const r=w.path;return r==="/"?"/":r.startsWith("/providers")?"/providers":r.startsWith("/settings")?"/settings":r.startsWith("/logs")?"/logs":r.startsWith("/process")?"/process":"/"}),v=()=>{localStorage.removeItem("webui_token"),g.push("/login")},p=r=>{r==="logout"&&v(),r==="check-update"&&(d.value=!0)};return(r,t)=>{const l=a("el-menu-item"),s=a("el-menu"),k=a("ArrowDown"),V=a("el-icon"),y=a("el-button"),h=a("el-dropdown-item"),C=a("el-dropdown-menu"),U=a("el-dropdown"),m=a("el-header"),A=a("router-view");return _(),D("div",Q,[o(m,{class:"header"},{default:e(()=>[t[8]||(t[8]=i("h1",null,[i("span",null,"E"),u("LLM")],-1)),i("nav",X,[o(s,{mode:"horizontal","default-active":n.value,ellipsis:!1,router:"",class:"header-menu"},{default:e(()=>[o(l,{index:"/"},{default:e(()=>[...t[1]||(t[1]=[u("首页",-1)])]),_:1}),o(l,{index:"/providers"},{default:e(()=>[...t[2]||(t[2]=[u("供应商管理",-1)])]),_:1}),o(l,{index:"/logs"},{default:e(()=>[...t[3]||(t[3]=[u("日志",-1)])]),_:1}),o(l,{index:"/settings"},{default:e(()=>[...t[4]||(t[4]=[u("设置",-1)])]),_:1}),o(l,{index:"/process"},{default:e(()=>[...t[5]||(t[5]=[u("进程管理",-1)])]),_:1})]),_:1},8,["default-active"])]),i("div",Y,[o(U,{trigger:"click",onCommand:p,class:"ml-12"},{dropdown:e(()=>[o(C,null,{default:e(()=>[o(h,{command:"check-update"},{default:e(()=>[...t[6]||(t[6]=[u("检查更新",-1)])]),_:1}),o(h,{command:"logout",divided:""},{default:e(()=>[...t[7]||(t[7]=[u("退出",-1)])]),_:1})]),_:1})]),default:e(()=>[o(y,null,{default:e(()=>[u(x(F(c))+" ",1),o(V,{class:"el-icon--right"},{default:e(()=>[o(k)]),_:1})]),_:1})]),_:1})])]),_:1}),i("main",Z,[i("div",ee,[o(A)])]),o(P,{modelValue:d.value,"onUpdate:modelValue":t[0]||(t[0]=M=>d.value=M)},null,8,["modelValue"])])}}});export{se as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as X,p as q,a8 as H,b as J,e as D,g as s,w as l,r as i,a as c,j as Q,o as w,f as d,$ as W,h as o,t as r,c as Y,E as Z,_ as ee}from"./index-X5PdkJlA.js";import{m as E}from"./useManagerApi-C1_COxKg.js";import{u as te}from"./useToast-MtbkGJiM.js";const ae={class:"flex-col-gap"},le={class:"flex-between"},se={class:"proc-name"},ne={class:"proc-sub"},oe=["onClick"],re={key:1,class:"muted-sm"},ie={class:"desc-full"},de=X({__name:"ProcessView",setup(ue){const{toast:b}=te(),v=i(!0),g=i(!1),y=i([]),x=i(null),p=i(null),h=i(Date.now()),I={daemon:"pm2 守护进程",manager:"管理服务",main:"代理服务"},C=e=>I[e.role]??e.role,k={daemon:"pm2 守护进程:托管代理服务与管理服务的后台守护,进程崩溃时自动拉起;自身不监听端口、不处理业务请求,不允许远程操控(停止它将失去自动恢复能力)。",manager:"管理服务(ellm-proxy-manager):独立常驻的轻量服务,提供进程启停接口与代理开关接口;代理服务停止时保持存活,保证还能在本页远程拉起代理服务。",main:"代理服务(ellm-proxy):网关本体,转发 /v1/* 模型请求,同时承载管理台页面与 API;本页的重启操作即作用于该进程,重启期间网关与管理台 API 短暂不可用(代理服务停止时重启等效于启动)。"},P=e=>k[e.role],_=i(!1),T=i(""),$=i(""),A=e=>{T.value=`${C(e)}(${M(e)})`,$.value=k[e.role],_.value=!0},M=e=>e.role==="daemon"?"pm2":e.role==="manager"?"ellm-proxy-manager":"ellm-proxy",O=e=>e.status==="online",N=e=>e.status?e.status==="online"?"running":e.status==="errored"?"abnormal":"stopped":"stopped",V=e=>{switch(e.status){case"online":return{text:"运行中",type:"success"};case"stopped":return{text:"已停止",type:"info"};case"errored":return{text:"异常",type:"danger"};case"waiting restart":case"launching":return{text:"重启中",type:"warning"};default:return{text:e.status||"未知",type:"info"}}},R=e=>{if(!O(e)||!e.startedAt)return"-";let t=Math.max(0,Math.floor((h.value-e.startedAt)/1e3));const m=Math.floor(t/86400);t-=m*86400;const u=Math.floor(t/3600);t-=u*3600;const n=Math.floor(t/60);return t-=n*60,[m?`${m}天`:"",u?`${u}时`:"",n?`${n}分`:"",`${t}秒`].filter(Boolean).join(" ")},S=e=>e?e>=1024**3?(e/1024**3).toFixed(1)+"G":e>=1024**2?(e/1024**2).toFixed(1)+"M":Math.round(e/1024)+"K":"-",f=async e=>{e||(y.value.length?g.value=!0:v.value=!0);try{y.value=await E.processes(),h.value=Date.now(),x.value=null}catch(t){e||(x.value=t.message)}finally{e||(v.value=!1,g.value=!1)}};let B;const j=()=>{p.value||document.hidden||f(!0)};q(()=>{f(!1),B=window.setInterval(j,5e3)}),H(()=>window.clearInterval(B));const z=async()=>{try{await Z.confirm("确定重启 ellm-proxy 代理服务?重启期间网关与管理台 API 将短暂不可用(代理服务已停止时重启等效于启动)。","重启确认",{confirmButtonText:"重启",cancelButtonText:"取消",type:"warning"})}catch{return}p.value="restart";try{await E.restart(),b("重启完成","success")}catch(e){b(e.message||"操作失败","error")}finally{p.value=null,await f(!0)}};return(e,t)=>{const m=c("el-tag"),u=c("el-button"),n=c("el-table-column"),F=c("el-empty"),L=c("el-table"),U=c("el-card"),G=c("el-dialog"),K=Q("loading");return J((w(),D("div",ae,[s(U,{shadow:"never"},{header:l(()=>[d("div",le,[t[6]||(t[6]=d("span",{class:"fw-600"},"进程管理",-1)),d("div",null,[s(m,{type:"info",class:"mr-8"},{default:l(()=>[...t[4]||(t[4]=[o("刷新间隔 5s",-1)])]),_:1}),s(u,{loading:g.value,onClick:t[0]||(t[0]=a=>f(!1))},{default:l(()=>[...t[5]||(t[5]=[o("刷新",-1)])]),_:1},8,["loading"])])])]),default:l(()=>[s(L,{data:y.value,border:"",stripe:"",class:"full-width"},{empty:l(()=>[s(F,{description:x.value||"暂无进程数据"},{default:l(()=>[s(u,{type:"primary",onClick:t[1]||(t[1]=a=>f(!1))},{default:l(()=>[...t[8]||(t[8]=[o("重试",-1)])]),_:1})]),_:1},8,["description"])]),default:l(()=>[s(n,{label:"进程","min-width":"180"},{default:l(({row:a})=>[d("div",se,[d("span",{class:W(["status-dot",N(a)])},null,2),o(" "+r(C(a))+" ",1),d("span",ne,r(M(a)),1)])]),_:1}),s(n,{label:"运行状态",width:"100",align:"center"},{default:l(({row:a})=>[s(m,{type:V(a).type,"disable-transitions":""},{default:l(()=>[o(r(V(a).text),1)]),_:2},1032,["type"])]),_:1}),s(n,{label:"PID",width:"90",align:"center"},{default:l(({row:a})=>[o(r(a.pid??"-"),1)]),_:1}),s(n,{label:"端口",width:"90",align:"center"},{default:l(({row:a})=>[o(r(a.port??"-"),1)]),_:1}),s(n,{label:"运行时长",width:"130",align:"center"},{default:l(({row:a})=>[o(r(R(a)),1)]),_:1}),s(n,{label:"地址","min-width":"170"},{default:l(({row:a})=>[o(r(a.address??"-"),1)]),_:1}),s(n,{label:"内存",width:"90",align:"center"},{default:l(({row:a})=>[o(r(S(a.memory)),1)]),_:1}),s(n,{label:"描述","min-width":"220"},{default:l(({row:a})=>[d("span",{class:"desc-cell",title:"点击查看详情",onClick:ce=>A(a)},r(P(a)),9,oe)]),_:1}),s(n,{label:"操作",width:"90",align:"center"},{default:l(({row:a})=>[a.role==="main"?(w(),Y(u,{key:0,type:"primary",size:"small",loading:p.value==="restart",disabled:p.value!==null,onClick:z},{default:l(()=>[...t[7]||(t[7]=[o(" 重启 ",-1)])]),_:1},8,["loading","disabled"])):(w(),D("span",re,"不可操作"))]),_:1})]),_:1},8,["data"]),t[9]||(t[9]=d("div",{class:"muted-sm"}," 进程控制由管理服务(manager)执行,仅代理服务(ellm-proxy)可重启(已停止时重启等效于启动): 重启期间网关与管理台 API 短暂不可用。 pm2 守护进程与管理服务不允许远程操控(停止它们将失去远程恢复能力)。 ",-1))]),_:1}),s(G,{modelValue:_.value,"onUpdate:modelValue":t[3]||(t[3]=a=>_.value=a),title:T.value,width:"520px"},{footer:l(()=>[s(u,{type:"primary",onClick:t[2]||(t[2]=a=>_.value=!1)},{default:l(()=>[...t[10]||(t[10]=[o("知道了",-1)])]),_:1})]),default:l(()=>[d("p",ie,r($.value),1)]),_:1},8,["modelValue","title"])])),[[K,v.value]])}}}),_e=ee(de,[["__scopeId","data-v-c87eb134"]]);export{_e as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@charset "UTF-8";.proc-name[data-v-c87eb134]{display:flex;align-items:center;gap:8px;font-weight:600}.proc-name .proc-sub[data-v-c87eb134]{font-weight:400;color:var(--el-text-color-secondary);font-size:12px}.status-dot[data-v-c87eb134]{display:inline-block;width:8px;height:8px;border-radius:50%}.status-dot.running[data-v-c87eb134]{background:var(--el-color-success);box-shadow:0 0 4px var(--el-color-success)}.status-dot.stopped[data-v-c87eb134]{background:var(--el-color-info)}.status-dot.abnormal[data-v-c87eb134]{background:var(--el-color-danger)}.desc-cell[data-v-c87eb134]{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.desc-full[data-v-c87eb134]{margin:0;line-height:1.8}.mr-8[data-v-c87eb134]{margin-right:8px}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.switch-row[data-v-65e65156]{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:4px 0}.switch-title[data-v-65e65156]{display:flex;align-items:center;gap:8px;font-weight:600;margin-bottom:4px}.switch-desc[data-v-65e65156]{color:var(--el-text-color-secondary);font-size:13px;line-height:1.6}.switch-desc code[data-v-65e65156]{background:var(--el-fill-color-light);padding:1px 6px;border-radius:4px;font-size:12px}.switch-divider[data-v-65e65156]{margin:16px 0}.mb-16[data-v-03f482d3]{margin-bottom:16px}.mt-8[data-v-03f482d3]{margin-top:8px}.switch-row[data-v-03f482d3]{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:4px 0}.switch-title[data-v-03f482d3]{display:flex;align-items:center;gap:8px;font-weight:600;margin-bottom:4px}.switch-desc[data-v-03f482d3]{color:var(--el-text-color-secondary);font-size:13px;line-height:1.6}.switch-desc code[data-v-03f482d3]{background:var(--el-fill-color-light);padding:1px 6px;border-radius:4px;font-size:12px}.version-row[data-v-03f482d3]{display:flex;align-items:center;gap:8px;margin-bottom:8px}.version-label[data-v-03f482d3]{color:var(--el-text-color-secondary);font-size:13px}.version-actions[data-v-03f482d3]{margin-top:12px;display:flex;gap:8px}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as Y}from"./useToast-MtbkGJiM.js";import{d as F,p as X,b as q,o as m,c as B,w as n,e as $,f as s,t as h,i as D,h as u,g as a,F as H,r as d,E as W,a as c,j as G,_ as Q,l as j,A as ee}from"./index-X5PdkJlA.js";import{u as le}from"./config-CRWaUFQc.js";import{m as K}from"./useManagerApi-C1_COxKg.js";const te={class:"flex-between"},se={class:"muted-sm mb-8"},ae={class:"switch-row"},oe={class:"switch-info"},ne={class:"switch-title"},ue={class:"switch-row"},ie={class:"switch-info"},de={class:"switch-title"},re={class:"switch-row"},ce={class:"switch-info"},ve={class:"switch-title"},pe=F({__name:"HarnessSwitches",setup(J){const{toast:g}=Y(),b=d(!0),E=d(!1),v=d(null),V=d(null),i=d(null),x=d(null),_=d("success"),C=async f=>{f||(v.value?E.value=!0:b.value=!0);try{v.value=await K.harness(),V.value=null}catch(o){f||(V.value=o.message)}finally{f||(b.value=!1,E.value=!1)}};X(()=>C(!1));const S={claudeCode:{label:"Claude Code",file:"~/.claude/settings.json",toggle:K.setClaudeCode},codex:{label:"Codex CLI",file:"~/.codex/config.toml",toggle:K.setCodex},dsh:{label:"DeepSeek Harness",file:"~/.dsh/settings.yaml",toggle:K.setDsh}},k=async(f,o)=>{if(!v.value||i.value)return;const y=S[f],p=o?`确定开启 ${y.label} 代理?将改写 ${y.file}(开启前自动备份,关闭时从备份还原)。`:`确定关闭 ${y.label} 代理?将从备份还原 ${y.file}(无备份时移除代理相关配置)。`;try{await W.confirm(p,o?"开启确认":"关闭确认",{confirmButtonText:o?"开启":"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}i.value=f;try{const l=await y.toggle(o);x.value=l.note??null,_.value=o?"success":"info",g(o?"已开启代理":"已关闭代理","success")}catch(l){g(l.message||"操作失败","error")}finally{i.value=null,await C(!0)}};return(f,o)=>{const y=c("el-button"),p=c("el-alert"),l=c("el-tag"),I=c("el-switch"),z=c("el-divider"),P=c("el-empty"),R=c("el-card"),N=G("loading");return q((m(),B(R,{shadow:"never"},{header:n(()=>[s("div",te,[o[7]||(o[7]=s("span",{class:"fw-600"},"代理开关",-1)),a(y,{loading:E.value,onClick:o[0]||(o[0]=T=>C(!1))},{default:n(()=>[...o[6]||(o[6]=[u("刷新",-1)])]),_:1},8,["loading"])])]),default:n(()=>[v.value?(m(),$(H,{key:0},[s("div",se," 将本机 CLI 工具的流量一键指向 ellm-proxy 网关("+h(v.value.gatewayBase)+")。 开启前自动备份原配置为 *.ellm-bak(保留最早的原始备份),关闭时从备份还原。 ",1),x.value?(m(),B(p,{key:0,title:x.value,type:_.value,"show-icon":"",class:"mb-8",onClose:o[1]||(o[1]=T=>x.value=null)},null,8,["title","type"])):D("",!0),s("div",ae,[s("div",oe,[s("div",ne,[o[8]||(o[8]=u(" Claude Code ",-1)),a(l,{type:v.value.claudeCode?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.claudeCode?"已开启":"未开启"),1)]),_:1},8,["type"])]),o[9]||(o[9]=s("div",{class:"switch-desc"},[u(" 写入 "),s("code",null,"~/.claude/settings.json"),u(" 的 env.ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN, 鉴权取网关配置的 masterKey(未设置将开启失败);其余字段原样保留,重启 Claude Code 会话后生效。 ")],-1))]),a(I,{"model-value":v.value.claudeCode,loading:i.value==="claudeCode",onChange:o[2]||(o[2]=T=>k("claudeCode",!!T))},null,8,["model-value","loading"])]),a(z,{class:"switch-divider"}),s("div",ue,[s("div",ie,[s("div",de,[o[10]||(o[10]=u(" Codex CLI ",-1)),a(l,{type:v.value.codex?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.codex?"已开启":"未开启"),1)]),_:1},8,["type"])]),o[11]||(o[11]=s("div",{class:"switch-desc"},[u(" 写入 "),s("code",null,"~/.codex/config.toml"),u(" 的 model_provider 与 [model_providers.ellm-proxy] 段 (保留用户注释与排版);开启后需按提示设置环境变量 ELLM_PROXY_API_KEY 供 Codex 鉴权。 ")],-1))]),a(I,{"model-value":v.value.codex,loading:i.value==="codex",onChange:o[3]||(o[3]=T=>k("codex",!!T))},null,8,["model-value","loading"])]),a(z,{class:"switch-divider"}),s("div",re,[s("div",ce,[s("div",ve,[o[12]||(o[12]=u(" DeepSeek Harness ",-1)),a(l,{type:v.value.dsh?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.dsh?"已开启":"未开启"),1)]),_:1},8,["type"])]),o[13]||(o[13]=s("div",{class:"switch-desc"},[u(" 写入 "),s("code",null,"~/.dsh/settings.yaml"),u(" 的 llm-deepseek.baseURL 与 "),s("code",null,"~/.dsh/.credentials.yaml"),u(" 的 DEEPSEEK_API_KEY(取网关 masterKey,未设置则写占位值);模型请在 dsh 内选择与网关部署名一致的模型。 dsh 为 developer preview,配置格式可能随版本变化。 ")],-1))]),a(I,{"model-value":v.value.dsh,loading:i.value==="dsh",onChange:o[4]||(o[4]=T=>k("dsh",!!T))},null,8,["model-value","loading"])])],64)):(m(),B(P,{key:1,description:V.value||"暂无开关状态"},{default:n(()=>[a(y,{type:"primary",onClick:o[5]||(o[5]=T=>C(!1))},{default:n(()=>[...o[14]||(o[14]=[u("重试",-1)])]),_:1})]),_:1},8,["description"]))]),_:1})),[[N,b.value]])}}}),me=Q(pe,[["__scopeId","data-v-65e65156"]]),fe={class:"service-settings"},_e={class:"flex-between"},ge={key:0,class:"switch-row"},ye={class:"switch-info"},we={class:"switch-title"},be={key:0,class:"switch-row"},Ce={class:"switch-info"},ke={class:"switch-title"},he={class:"switch-desc"},xe={key:0,class:"version-block"},$e={class:"version-row"},Ve={class:"muted-sm"},Te={key:0,class:"muted-sm"},Ae={class:"version-actions"},Ee=F({__name:"ServiceUpdateSettings",setup(J){const{toast:g}=Y(),b=d(!0),E=d(!1),v=d(null),V=d(null),i=d(null),x=d(null),_=d(null),C=d(!1),S=d(!1),k=d(null),f=async t=>{var e,r;if(!t){if(E.value)return;E.value=!0}try{const[A,U]=await Promise.allSettled([K.autostart(),K.update()]);v.value=A.status==="fulfilled"?A.value:null,V.value=A.status==="rejected"?(e=A.reason)==null?void 0:e.message:null,i.value=U.status==="fulfilled"?U.value:null,x.value=U.status==="rejected"?(r=U.reason)==null?void 0:r.message:null}finally{b.value=!1,E.value=!1}};X(()=>f(!1));const o=j(()=>{var t,e;return((e=(t=i.value)==null?void 0:t.lastCheck)==null?void 0:e.currentVersion)??null}),y=j(()=>{var t,e;return((e=(t=i.value)==null?void 0:t.lastCheck)==null?void 0:e.latestVersion)??null}),p=j(()=>{var t,e;return((e=(t=i.value)==null?void 0:t.lastCheck)==null?void 0:e.hasUpdate)===!0}),l=j(()=>{var e,r;const t=(r=(e=i.value)==null?void 0:e.lastCheck)==null?void 0:r.checkedAt;return t?new Date(t).toLocaleString():null}),I=j(()=>{var r;const t=(r=i.value)==null?void 0:r.lastApply;if(!t)return null;const e=t.appliedAt?new Date(t.appliedAt).toLocaleString():"";return`${t.fromVersion} → ${t.toVersion}${e?`(${e})`:""}`}),z=t=>t?new Date(t).toLocaleString():"",P=async t=>{if(!v.value||_.value)return;const e=t?"确定开启开机自启?将注册登录触发的自启项(登录时自动恢复服务)。":"确定关闭开机自启?将移除自启项,且此后首次启动不再自动注册。";try{await W.confirm(e,t?"开启确认":"关闭确认",{confirmButtonText:t?"开启":"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}_.value="autostart";try{const r=await K.setAutostart(t);r.note?g(r.note,"success"):g(t?"开机自启已开启":"开机自启已关闭","success")}catch(r){g(r.message||"操作失败","error")}finally{_.value=null,await f(!0)}},R=async t=>{if(!i.value||_.value)return;const e=t?"确定开启自动更新?将每小时检查私有源,发现新版本自动安装并重启服务。":"确定关闭自动更新?不再自动检查(手动检查更新不受影响)。";try{await W.confirm(e,t?"开启确认":"关闭确认",{confirmButtonText:t?"开启":"关闭",cancelButtonText:"取消",type:"warning"})}catch{return}_.value="updateEnabled";try{const r=await K.setUpdateEnabled(t);r.note&&g(r.note,"success")}catch(r){g(r.message||"操作失败","error")}finally{_.value=null,await f(!0)}},N=async()=>{C.value=!0;try{const t=await K.checkUpdate();k.value=t.hasUpdate?`发现新版本 ${t.latestVersion}(当前 ${t.currentVersion}),可点击「立即更新」安装`:`已是最新版本 ${t.currentVersion}(检查于 ${z(t.checkedAt)})`,await f(!0)}catch(t){g(t.message||"检查更新失败","error")}finally{C.value=!1}},T=async()=>{try{await W.confirm(`确定更新到 ${y.value}?更新期间主服务将短暂中断(停服安装后自动重启),管理服务随后自重启。`,"更新确认",{confirmButtonText:"立即更新",cancelButtonText:"取消",type:"warning"})}catch{return}S.value=!0;try{const t=await K.applyUpdate();k.value=t.note??`已更新 ${t.fromVersion} → ${t.toVersion}`,g("更新完成","success"),await f(!0)}catch(t){g(t.message||"更新失败","error"),await f(!0)}finally{S.value=!1}};return(t,e)=>{const r=c("el-button"),A=c("el-tag"),U=c("el-switch"),M=c("el-empty"),O=c("el-card"),w=c("el-alert"),Z=G("loading");return q((m(),$("div",fe,[a(O,{shadow:"never",class:"mb-16"},{header:n(()=>[s("div",_e,[e[7]||(e[7]=s("span",{class:"fw-600"},"开机自启",-1)),a(r,{loading:E.value,onClick:e[0]||(e[0]=L=>f(!1))},{default:n(()=>[...e[6]||(e[6]=[u("刷新",-1)])]),_:1},8,["loading"])])]),default:n(()=>[v.value?(m(),$("div",ge,[s("div",ye,[s("div",we,[e[8]||(e[8]=u(" 登录时自动启动服务 ",-1)),a(A,{type:v.value.enabled?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(v.value.enabled?"已开启":"未开启"),1)]),_:1},8,["type"])]),e[9]||(e[9]=s("div",{class:"switch-desc"},[u(" 登录操作系统时自动执行 "),s("code",null,"pm2 resurrect"),u(" 恢复服务(无需管理员权限)。 关闭后首次启动 "),s("code",null,"ellm-proxy"),u(" 也不会再自动注册,重新开启即可解除。 ")],-1))]),a(U,{"model-value":v.value.enabled,loading:_.value==="autostart",onChange:e[1]||(e[1]=L=>P(!!L))},null,8,["model-value","loading"])])):(m(),B(M,{key:1,description:V.value||"暂无自启状态"},{default:n(()=>[a(r,{type:"primary",onClick:e[2]||(e[2]=L=>f(!1))},{default:n(()=>[...e[10]||(e[10]=[u("重试",-1)])]),_:1})]),_:1},8,["description"]))]),_:1}),a(O,{shadow:"never",class:"mb-16"},{header:n(()=>[...e[11]||(e[11]=[s("div",{class:"flex-between"},[s("span",{class:"fw-600"},"自动更新")],-1)])]),default:n(()=>[i.value?(m(),$("div",be,[s("div",Ce,[s("div",ke,[e[12]||(e[12]=u(" 自动检查并安装新版本 ",-1)),a(A,{type:i.value.enabled?"success":"info",size:"small","disable-transitions":""},{default:n(()=>[u(h(i.value.enabled?"已开启":"未开启"),1)]),_:1},8,["type"])]),s("div",he,[i.value.supported?(m(),$(H,{key:1},[u(" 每小时检查私有 npm 源,发现新版本自动安装并重启服务(更新期间服务短暂中断); 关闭后仍可在下方手动检查与更新。 ")],64)):(m(),$(H,{key:0},[u(" 当前为源码运行模式,更新功能不可用(仅 npm 全局安装的包支持自动更新)。 ")],64))])]),i.value.supported?(m(),B(U,{key:0,"model-value":i.value.enabled,loading:_.value==="updateEnabled",onChange:e[3]||(e[3]=L=>R(!!L))},null,8,["model-value","loading"])):D("",!0)])):(m(),B(M,{key:1,description:x.value||"暂无更新状态"},{default:n(()=>[a(r,{type:"primary",onClick:e[4]||(e[4]=L=>f(!1))},{default:n(()=>[...e[13]||(e[13]=[u("重试",-1)])]),_:1})]),_:1},8,["description"]))]),_:1}),a(O,{shadow:"never"},{header:n(()=>[...e[14]||(e[14]=[s("div",{class:"flex-between"},[s("span",{class:"fw-600"},"版本信息")],-1)])]),default:n(()=>[i.value?(m(),$("div",xe,[s("div",$e,[e[17]||(e[17]=s("span",{class:"version-label"},"当前版本",-1)),a(A,{size:"small"},{default:n(()=>[u(h(o.value||"未知"),1)]),_:1}),y.value?(m(),$(H,{key:0},[e[16]||(e[16]=s("span",{class:"version-label"},"最新版本",-1)),a(A,{type:p.value?"warning":"success",size:"small","disable-transitions":""},{default:n(()=>[u(h(y.value),1)]),_:1},8,["type"]),p.value?(m(),B(A,{key:0,type:"warning",size:"small","disable-transitions":""},{default:n(()=>[...e[15]||(e[15]=[u("发现新版本",-1)])]),_:1})):D("",!0)],64)):D("",!0)]),s("div",Ve,[u(" 上次检查:"+h(l.value||"尚未检查")+" ",1),i.value.updating?(m(),$(H,{key:0},[u(" · 更新执行中...")],64)):D("",!0)]),I.value?(m(),$("div",Te,"上次更新:"+h(I.value),1)):D("",!0),s("div",Ae,[a(r,{loading:C.value,disabled:i.value.updating,onClick:N},{default:n(()=>[u(h(C.value?"检查中...":"检查更新"),1)]),_:1},8,["loading","disabled"]),p.value?(m(),B(r,{key:0,type:"primary",loading:S.value,disabled:i.value.updating,onClick:T},{default:n(()=>[u(h(S.value?"更新中...":`立即更新到 ${y.value}`),1)]),_:1},8,["loading","disabled"])):D("",!0)]),k.value?(m(),B(w,{key:1,title:k.value,type:"info","show-icon":"",class:"mt-8",onClose:e[5]||(e[5]=L=>k.value=null)},null,8,["title"])):D("",!0)])):(m(),B(M,{key:1,description:"更新状态不可用"}))]),_:1})])),[[Z,b.value]])}}}),Se=Q(Ee,[["__scopeId","data-v-03f482d3"]]),Ue={class:"flex-col-gap"},Be={class:"flex-between"},Ie={class:"muted-sm"},Pe={key:0},De={class:"flex-end"},He=F({__name:"SettingsView",setup(J){const{toast:g}=Y(),b=le(),E=d("basic"),v=d(!0),V=d(!1),i=d(""),x=d(29770),_=d(null),C=d("passthrough"),S=d(!1),k=d(""),f=d(""),o=async()=>{var p;try{await b.load(),i.value=b.config.masterKey||"",x.value=b.config.port??29770,C.value=b.config.compression==="plain"?"plain":"passthrough",S.value=b.config.logContent===!0;const l=(p=b.config["web-ui"])==null?void 0:p["login-account"];k.value=(l==null?void 0:l.username)||"",f.value=(l==null?void 0:l.password)||"";try{_.value=await ee.get("/api/status").then(I=>I.listenPort)}catch{_.value=null}}catch(l){g(l.message,"error")}finally{v.value=!1}},y=async()=>{V.value=!0;try{await b.update(p=>{var l;p.masterKey=i.value.trim(),p.port=x.value,p.compression=C.value,p.logContent=S.value,p["web-ui"]??(p["web-ui"]={}),(l=p["web-ui"])["login-account"]??(l["login-account"]={}),p["web-ui"]["login-account"].username=k.value.trim(),p["web-ui"]["login-account"].password=f.value}),g("设置已保存并生效","success")}catch(p){g(p.message,"error")}finally{V.value=!1}};return X(o),(p,l)=>{const I=c("el-tag"),z=c("el-input"),P=c("el-form-item"),R=c("el-input-number"),N=c("el-option"),T=c("el-select"),t=c("el-switch"),e=c("el-form"),r=c("el-card"),A=c("el-button"),U=c("el-tab-pane"),M=c("el-tabs"),O=G("loading");return m(),B(M,{modelValue:E.value,"onUpdate:modelValue":l[6]||(l[6]=w=>E.value=w),class:"settings-tabs"},{default:n(()=>[a(U,{label:"基本设置",name:"basic"},{default:n(()=>[q((m(),$("div",Ue,[a(r,{shadow:"never"},{header:n(()=>[s("div",Be,[l[8]||(l[8]=s("span",{class:"fw-600"},"基本设置",-1)),a(I,{type:"success"},{default:n(()=>[...l[7]||(l[7]=[u("运行中",-1)])]),_:1})])]),default:n(()=>[a(e,{"label-position":"top"},{default:n(()=>[a(P,{label:"Master Key"},{default:n(()=>[a(z,{modelValue:i.value,"onUpdate:modelValue":l[0]||(l[0]=w=>i.value=w),type:"password",placeholder:"sk-1234","show-password":""},null,8,["modelValue"]),l[9]||(l[9]=s("div",{class:"muted-sm"}," 网关 API 认证密钥(/v1/* 请求需携带 Bearer <masterKey>;留空 = 本地放行) ",-1))]),_:1}),a(P,{label:"监听端口 (port)"},{default:n(()=>[a(R,{modelValue:x.value,"onUpdate:modelValue":l[1]||(l[1]=w=>x.value=w),min:1,max:65535,step:1,class:"full-width"},null,8,["modelValue"]),s("div",Ie,[l[11]||(l[11]=u(" 网关与管理台 API 的监听端口,保存后下次重启生效 ",-1)),_.value!==null?(m(),$(H,{key:0},[l[10]||(l[10]=u(" ;当前实际监听 ",-1)),s("b",null,h(_.value),1),_.value!==x.value?(m(),$("span",Pe,"(由 PORT 环境变量覆盖,此配置值不生效)")):D("",!0)],64)):D("",!0)])]),_:1}),a(P,{label:"压缩语义 (compression)"},{default:n(()=>[a(T,{modelValue:C.value,"onUpdate:modelValue":l[2]||(l[2]=w=>C.value=w),class:"full-width"},{default:n(()=>[a(N,{label:"透传 (passthrough)",value:"passthrough"}),a(N,{label:"明文 (plain)",value:"plain"})]),_:1},8,["modelValue"]),l[12]||(l[12]=s("div",{class:"muted-sm"},"上游响应压缩处理方式,保存后热更新生效",-1))]),_:1}),a(P,{label:"日志内容捕获 (logContent)"},{default:n(()=>[a(t,{modelValue:S.value,"onUpdate:modelValue":l[3]||(l[3]=w=>S.value=w)},null,8,["modelValue"]),l[13]||(l[13]=s("div",{class:"muted-sm"}," 记录完整请求/响应(含上游原始与转换后内容)进日志详情,用于排查定位问题;开启后有少量内存与存储开销,保存后热更新生效 ",-1))]),_:1}),a(P,{label:"管理台用户名 (web-ui.login-account.username)"},{default:n(()=>[a(z,{modelValue:k.value,"onUpdate:modelValue":l[4]||(l[4]=w=>k.value=w),placeholder:"admin"},null,8,["modelValue"]),l[14]||(l[14]=s("div",{class:"muted-sm"}," 管理台登录用户名,留空回退 WEBUI_USERNAME 环境变量或默认 admin ",-1))]),_:1}),a(P,{label:"管理台密码 (web-ui.login-account.password)"},{default:n(()=>[a(z,{modelValue:f.value,"onUpdate:modelValue":l[5]||(l[5]=w=>f.value=w),type:"password",placeholder:"admin","show-password":""},null,8,["modelValue"]),l[15]||(l[15]=s("div",{class:"muted-sm"}," 管理台登录密码,留空回退 WEBUI_PASSWORD 环境变量或默认 admin;修改保存后立即生效,需用新账号重新登录 ",-1))]),_:1})]),_:1})]),_:1}),s("div",De,[a(A,{type:"primary",loading:V.value,disabled:V.value,onClick:y},{default:n(()=>[u(h(V.value?"保存中...":"保存设置"),1)]),_:1},8,["loading","disabled"])])])),[[O,v.value]])]),_:1}),a(U,{label:"代理开关",name:"harness",lazy:""},{default:n(()=>[a(me)]),_:1}),a(U,{label:"服务与更新",name:"service",lazy:""},{default:n(()=>[a(Se)]),_:1})]),_:1},8,["modelValue"])}}});export{He as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var i;const c=((i=window.__ELLM_CONFIG__)==null?void 0:i.API_BASE)??"";function h(){try{const t=c?new URL(c):new URL(location.href);return t.port?`${t.protocol}//${t.hostname}:${Number(t.port)+1}`:null}catch{return null}}var p;const l=((p=window.__ELLM_CONFIG__)==null?void 0:p.MANAGER_BASE)??h(),E=3e4;async function e(t,u,s,d=E){if(l===null)throw new Error("无法确定管理服务地址:config.js 未配置 MANAGER_BASE,且页面经 80/443 端口访问(无法按端口+1 推导)");const a=new AbortController,T=setTimeout(()=>a.abort(),d);try{const r=await fetch(`${l}${u}`,{method:t,headers:{"Content-Type":"application/json"},body:s!==void 0?JSON.stringify(s):void 0,signal:a.signal}),n=await r.text();let o;try{o=n?JSON.parse(n):{}}catch{throw new Error(`管理服务响应格式错误 (HTTP ${r.status})`)}if(!r.ok||o.ok===!1)throw new Error(o.error||`HTTP ${r.status}`);return o}catch(r){throw r.name==="AbortError"?new Error("请求超时"):r instanceof TypeError?new Error("管理服务不可达(请确认 ellm-proxy 已启动,且本页面通过 localhost/127.0.0.1 访问)"):r}finally{clearTimeout(T)}}const w={status:()=>e("GET","/status"),processes:()=>e("GET","/processes").then(t=>t.processes),start:()=>e("POST","/start"),stop:()=>e("POST","/stop"),restart:()=>e("POST","/restart"),autostart:()=>e("GET","/autostart"),setAutostart:t=>e("POST","/autostart",{enabled:t}),update:()=>e("GET","/update"),setUpdateEnabled:t=>e("POST","/update/enabled",{enabled:t}),checkUpdate:()=>e("POST","/update/check"),applyUpdate:()=>e("POST","/update/apply",void 0,6e5),harness:()=>e("GET","/harness"),setClaudeCode:t=>e("POST","/harness/claude-code",{enabled:t}),setCodex:t=>e("POST","/harness/codex",{enabled:t}),setDsh:t=>e("POST","/harness/dsh",{enabled:t})};export{w as m};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a9 as a}from"./index-X5PdkJlA.js";function u(){function s(t,o="success"){a({message:t,type:o,duration:3e3})}return{toast:s}}export{u};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// harness 配置文件备份/还原(claude-code 与 codex 共用):
|
|
2
|
-
// 开启前备份原文件为 <file>.ellm-bak(已存在则保留最早原始配置不覆盖);关闭 = 从备份还原并删除备份。
|
|
3
|
-
import { existsSync, copyFileSync, rmSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
/** 备份原文件(<file>.ellm-bak;备份已存在则保留最早原始配置不覆盖)。返回是否新建了备份 */
|
|
6
|
-
export function backupOnce(file) {
|
|
7
|
-
const bak = `${file}.ellm-bak`;
|
|
8
|
-
if (existsSync(bak)) return false;
|
|
9
|
-
if (existsSync(file)) copyFileSync(file, bak);
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** 从备份还原并删除备份;无备份返回 false */
|
|
14
|
-
export function restoreFromBackup(file) {
|
|
15
|
-
const bak = `${file}.ellm-bak`;
|
|
16
|
-
if (!existsSync(bak)) return false;
|
|
17
|
-
copyFileSync(bak, file);
|
|
18
|
-
rmSync(bak, { force: true });
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Claude Code 代理开关:~/.claude/settings.json 的 env.ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN
|
|
2
|
-
// 指向本机 ellm-proxy 网关。JSON 字段级修改,其余字段原样保留。
|
|
3
|
-
// 开关状态以实际文件内容判定(env.ANTHROPIC_BASE_URL 是否已指向网关);不落额外状态文件。
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
7
|
-
import { backupOnce, restoreFromBackup } from "./backup.js";
|
|
8
|
-
|
|
9
|
-
const CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
|
|
10
|
-
|
|
11
|
-
/** 去尾斜杠比较,容忍 http://x:1/ 与 http://x:1 视为同一地址 */
|
|
12
|
-
function sameUrl(a, b) {
|
|
13
|
-
return String(a || "").replace(/\/+$/, "") === String(b || "").replace(/\/+$/, "");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function readClaudeSettings() {
|
|
17
|
-
if (!existsSync(CLAUDE_SETTINGS)) return null;
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(readFileSync(CLAUDE_SETTINGS, "utf-8"));
|
|
20
|
-
} catch (err) {
|
|
21
|
-
throw new Error(`${CLAUDE_SETTINGS} 不是合法 JSON,拒绝修改(请先手工修复): ${err.message}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Claude Code 开关状态:env.ANTHROPIC_BASE_URL 是否已指向网关 */
|
|
26
|
-
export function claudeCodeStatus(gatewayBase) {
|
|
27
|
-
const settings = readClaudeSettings();
|
|
28
|
-
if (!settings) return false;
|
|
29
|
-
return sameUrl(settings?.env?.ANTHROPIC_BASE_URL, gatewayBase);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** 开启 Claude Code 代理:备份 → 字段级写入 env.ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN(其余字段原样保留) */
|
|
33
|
-
export function claudeCodeEnable({ gatewayBase, token }) {
|
|
34
|
-
mkdirSync(dirname(CLAUDE_SETTINGS), { recursive: true });
|
|
35
|
-
const settings = readClaudeSettings() ?? {};
|
|
36
|
-
backupOnce(CLAUDE_SETTINGS);
|
|
37
|
-
settings.env = { ...(settings.env ?? {}) };
|
|
38
|
-
settings.env.ANTHROPIC_BASE_URL = gatewayBase;
|
|
39
|
-
settings.env.ANTHROPIC_AUTH_TOKEN = token;
|
|
40
|
-
writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 关闭 Claude Code 代理:从备份还原(无备份时仅移除代理相关字段) */
|
|
44
|
-
export function claudeCodeDisable(gatewayBase) {
|
|
45
|
-
if (restoreFromBackup(CLAUDE_SETTINGS)) return true;
|
|
46
|
-
const settings = readClaudeSettings();
|
|
47
|
-
if (!settings) return false;
|
|
48
|
-
if (!sameUrl(settings?.env?.ANTHROPIC_BASE_URL, gatewayBase)) return false;
|
|
49
|
-
delete settings.env.ANTHROPIC_BASE_URL;
|
|
50
|
-
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
51
|
-
if (settings.env && Object.keys(settings.env).length === 0) delete settings.env;
|
|
52
|
-
writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// Codex 代理开关:~/.codex/config.toml 顶层 model_provider + [model_providers.ellm-proxy] 段
|
|
2
|
-
// 指向本机 ellm-proxy 网关(语法见 openai/codex docs/config.md;wire_api 仅支持 chat|responses,
|
|
3
|
-
// 此处用 chat 走 /v1/chat/completions)。
|
|
4
|
-
// TOML 解析/校验用 smol-toml 库(不手写解析器);写入采用文本级编辑以保留用户注释与排版,
|
|
5
|
-
// 且写盘前必须通过库校验(结果文本可解析且 model_provider 正确),否则抛错保留原文件。
|
|
6
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
|
-
import { join, dirname } from "node:path";
|
|
9
|
-
import { parse as parseToml } from "smol-toml";
|
|
10
|
-
import { backupOnce, restoreFromBackup } from "./backup.js";
|
|
11
|
-
|
|
12
|
-
const PROVIDER_ID = "ellm-proxy"; // Codex provider id(避开官方保留 id: openai/ollama/lmstudio)
|
|
13
|
-
const CODEX_CONFIG = join(homedir(), ".codex", "config.toml");
|
|
14
|
-
|
|
15
|
-
function readCodexConfig() {
|
|
16
|
-
return existsSync(CODEX_CONFIG) ? readFileSync(CODEX_CONFIG, "utf-8") : null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 是否已写顶层 model_provider = "ellm-proxy"(库解析;文本不合法按未开启处理,enable 时会给出明确报错) */
|
|
20
|
-
function codexProviderActive(text) {
|
|
21
|
-
try {
|
|
22
|
-
return parseToml(text)?.model_provider === PROVIDER_ID;
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 库校验:文本必须可解析,且 expectProvider 为 true 时顶层 model_provider 必须是本 provider(写盘前强制调用) */
|
|
29
|
-
function validateTomlText(text, expectProvider) {
|
|
30
|
-
let parsed;
|
|
31
|
-
try {
|
|
32
|
-
parsed = parseToml(text);
|
|
33
|
-
} catch (err) {
|
|
34
|
-
throw new Error(`编辑后的 config.toml 不是合法 TOML,已保留原文件: ${err.message}`);
|
|
35
|
-
}
|
|
36
|
-
if (expectProvider && parsed?.model_provider !== PROVIDER_ID) {
|
|
37
|
-
throw new Error(`编辑后的 config.toml 顶层 model_provider 不是 "${PROVIDER_ID}",已保留原文件`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Codex 开关状态:顶层 model_provider 是否为 ellm-proxy */
|
|
42
|
-
export function codexStatus() {
|
|
43
|
-
const text = readCodexConfig();
|
|
44
|
-
return text ? codexProviderActive(text) : false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 移除已有的 [model_providers.ellm-proxy] 段(从段头到下一个段头/文件尾),供重写为最新 base_url */
|
|
48
|
-
function removeProviderSection(text) {
|
|
49
|
-
const lines = text.split("\n");
|
|
50
|
-
const out = [];
|
|
51
|
-
let skipping = false;
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
if (/^\s*\[model_providers\.ellm-proxy\]\s*$/.test(line)) {
|
|
54
|
-
skipping = true;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (skipping && /^\s*\[/.test(line)) skipping = false; // 下一个段头,恢复收集
|
|
58
|
-
if (!skipping) out.push(line);
|
|
59
|
-
}
|
|
60
|
-
return out.join("\n").replace(/\n{3,}$/, "\n\n");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 设置顶层 model_provider = "ellm-proxy":已有该键则替换(忽略注释行),否则插到首个段头前(TOML 顶层键必须在任何 [section] 之前) */
|
|
64
|
-
function setTopLevelProvider(text) {
|
|
65
|
-
const lines = text.split("\n");
|
|
66
|
-
const keyLine = `model_provider = "${PROVIDER_ID}"`;
|
|
67
|
-
let replaced = false;
|
|
68
|
-
const out = lines.map((line) => {
|
|
69
|
-
if (!replaced && /^\s*model_provider\s*=/.test(line) && !line.trim().startsWith("#")) {
|
|
70
|
-
replaced = true;
|
|
71
|
-
return keyLine;
|
|
72
|
-
}
|
|
73
|
-
return line;
|
|
74
|
-
});
|
|
75
|
-
if (replaced) return out.join("\n");
|
|
76
|
-
const firstSection = out.findIndex((line) => /^\s*\[/.test(line));
|
|
77
|
-
if (firstSection === -1) return [...out, keyLine].join("\n");
|
|
78
|
-
out.splice(firstSection, 0, keyLine, "");
|
|
79
|
-
return out.join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** 开启 Codex 代理:备份 → 写 model_provider + [model_providers.ellm-proxy] 段(重复开启幂等刷新 base_url)。
|
|
83
|
-
* 原文本不合法时直接报错(不覆盖坏文件);编辑结果写盘前经库校验 */
|
|
84
|
-
export function codexEnable({ gatewayBase }) {
|
|
85
|
-
mkdirSync(dirname(CODEX_CONFIG), { recursive: true });
|
|
86
|
-
const original = readCodexConfig();
|
|
87
|
-
if (original !== null) {
|
|
88
|
-
try {
|
|
89
|
-
parseToml(original);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
throw new Error(`${CODEX_CONFIG} 不是合法 TOML,拒绝修改(请先手工修复): ${err.message}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
let text = original ?? "";
|
|
95
|
-
backupOnce(CODEX_CONFIG);
|
|
96
|
-
text = removeProviderSection(text);
|
|
97
|
-
text = setTopLevelProvider(text);
|
|
98
|
-
const section = [
|
|
99
|
-
`[model_providers.${PROVIDER_ID}]`,
|
|
100
|
-
`name = "ellm-proxy gateway"`,
|
|
101
|
-
`base_url = "${gatewayBase}/v1"`,
|
|
102
|
-
`wire_api = "chat"`,
|
|
103
|
-
`env_key = "ELLM_PROXY_API_KEY"`,
|
|
104
|
-
].join("\n");
|
|
105
|
-
text = text.replace(/\s*$/, "") + "\n\n" + section + "\n";
|
|
106
|
-
validateTomlText(text, true);
|
|
107
|
-
writeFileSync(CODEX_CONFIG, text, "utf-8");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** 关闭 Codex 代理:从备份还原(无备份时移除 provider 段与顶层键,写盘前经库校验) */
|
|
111
|
-
export function codexDisable() {
|
|
112
|
-
if (restoreFromBackup(CODEX_CONFIG)) return true;
|
|
113
|
-
const text = readCodexConfig();
|
|
114
|
-
if (!text || !codexProviderActive(text)) return false;
|
|
115
|
-
let out = removeProviderSection(text);
|
|
116
|
-
out = out
|
|
117
|
-
.split("\n")
|
|
118
|
-
.filter((line) => !/^\s*model_provider\s*=\s*["']?ellm-proxy["']?\s*$/.test(line))
|
|
119
|
-
.join("\n");
|
|
120
|
-
validateTomlText(out, false);
|
|
121
|
-
writeFileSync(CODEX_CONFIG, out, "utf-8");
|
|
122
|
-
return true;
|
|
123
|
-
}
|