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
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
// DeepSeek Harness(dsh)代理开关:~/.dsh/settings.yaml 的 llm-deepseek.baseURL 指向本机网关,
|
|
2
|
-
// 密钥写入官方托管凭据库 ~/.dsh/.credentials.yaml(refs.DEEPSEEK_API_KEY)。
|
|
3
|
-
// dsh 解析口径(0.1.x developer preview):llm-deepseek 适配器拼 {baseURL}/chat/completions,
|
|
4
|
-
// 故 baseURL 需带 /v1;密钥解析优先级 = 进程环境变量 > .credentials.yaml > .env(进程已设
|
|
5
|
-
// DEEPSEEK_API_KEY 时本开关写入的值不生效,需用户自行调整)。DSH_HOME env 可重定向 ~/.dsh。
|
|
6
|
-
// YAML 解析/校验用 yaml 库 Document API(保留用户注释),不手写解析器;写盘前经库回读校验。
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
|
-
import { join, dirname } from "node:path";
|
|
10
|
-
import { parseDocument, isMap } from "yaml";
|
|
11
|
-
import { backupOnce, restoreFromBackup } from "./backup.js";
|
|
12
|
-
|
|
13
|
-
/** dsh 密钥引用名(llm-deepseek 的 apiKeyEnv 默认值) */
|
|
14
|
-
const KEY_REF = "DEEPSEEK_API_KEY";
|
|
15
|
-
/** 凭据文档 schema 版本(dsh credentials-local 的 version: 1) */
|
|
16
|
-
const CREDENTIALS_VERSION = 1;
|
|
17
|
-
/** 网关未配 masterKey 时写入的占位密钥(值任意:网关未设 masterKey 时放行,但 dsh 本地要求非空) */
|
|
18
|
-
const PLACEHOLDER_KEY = "ellm-proxy";
|
|
19
|
-
|
|
20
|
-
const dshHome = () => process.env.DSH_HOME || join(homedir(), ".dsh");
|
|
21
|
-
const settingsFile = () => join(dshHome(), "settings.yaml");
|
|
22
|
-
const credentialsFile = () => join(dshHome(), ".credentials.yaml");
|
|
23
|
-
|
|
24
|
-
/** 去尾斜杠比较,容忍 http://x:1/ 与 http://x:1 视为同一地址 */
|
|
25
|
-
function sameUrl(a, b) {
|
|
26
|
-
return String(a || "").replace(/\/+$/, "") === String(b || "").replace(/\/+$/, "");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function readTextIfExists(file) {
|
|
30
|
-
if (!existsSync(file)) return null;
|
|
31
|
-
const text = readFileSync(file, "utf-8");
|
|
32
|
-
return text.trim() === "" ? null : text; // 空文件按不存在处理(dsh:absent = empty store)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 解析 YAML 文档(不合法抛错,保留原文件语义同 claude-code 的 JSON 处理) */
|
|
36
|
-
function parseYamlDoc(text, file) {
|
|
37
|
-
const doc = parseDocument(text);
|
|
38
|
-
if (doc.errors.length > 0) {
|
|
39
|
-
throw new Error(`${file} 不是合法 YAML,拒绝修改(请先手工修复): ${doc.errors[0].message}`);
|
|
40
|
-
}
|
|
41
|
-
return doc;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 开关目标端点:dsh 适配器在 baseURL 后拼 /chat/completions,故带 /v1 走网关正式路由 */
|
|
45
|
-
function targetBase(gatewayBase) {
|
|
46
|
-
return `${gatewayBase}/v1`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** dsh 开关状态:settings.yaml 的 llm-deepseek.baseURL 是否已指向网关(文件缺失/不合法按未开启) */
|
|
50
|
-
export function dshStatus(gatewayBase) {
|
|
51
|
-
const text = readTextIfExists(settingsFile());
|
|
52
|
-
if (!text) return false;
|
|
53
|
-
try {
|
|
54
|
-
const data = parseYamlDoc(text, settingsFile()).toJS();
|
|
55
|
-
return sameUrl(data?.["llm-deepseek"]?.baseURL, targetBase(gatewayBase));
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 开启 dsh 代理:备份 → settings.yaml 写 llm-deepseek.baseURL(幂等刷新)→
|
|
63
|
-
* .credentials.yaml 写 refs.DEEPSEEK_API_KEY(masterKey 缺省时写占位值)。
|
|
64
|
-
* 原文件不合法时直接报错(不覆盖坏文件);凭据文档仅支持 version:1 与旧版扁平两种形态。
|
|
65
|
-
*/
|
|
66
|
-
export function dshEnable({ gatewayBase, masterKey }) {
|
|
67
|
-
const target = targetBase(gatewayBase);
|
|
68
|
-
|
|
69
|
-
// 1) settings.yaml:先解析校验,后备份写入
|
|
70
|
-
mkdirSync(dshHome(), { recursive: true });
|
|
71
|
-
const settingsPath = settingsFile();
|
|
72
|
-
const origSettings = readTextIfExists(settingsPath);
|
|
73
|
-
const doc = origSettings === null ? parseYamlDoc("", settingsPath) : parseYamlDoc(origSettings, settingsPath);
|
|
74
|
-
const settingsData = doc.toJS();
|
|
75
|
-
if (settingsData && typeof settingsData["llm-deepseek"] !== "undefined" && typeof settingsData["llm-deepseek"] !== "object") {
|
|
76
|
-
throw new Error(`${settingsPath} 的 llm-deepseek 不是映射结构,拒绝修改`);
|
|
77
|
-
}
|
|
78
|
-
backupOnce(settingsPath);
|
|
79
|
-
doc.setIn(["llm-deepseek", "baseURL"], target);
|
|
80
|
-
writeValidatedYaml(settingsPath, doc);
|
|
81
|
-
|
|
82
|
-
// 2) .credentials.yaml:官方托管凭据库(优先级低于进程环境变量,高于 .env 两层)
|
|
83
|
-
const credPath = credentialsFile();
|
|
84
|
-
const key = masterKey || PLACEHOLDER_KEY;
|
|
85
|
-
const origCred = readTextIfExists(credPath);
|
|
86
|
-
backupOnce(credPath);
|
|
87
|
-
if (origCred === null) {
|
|
88
|
-
const credDoc = parseYamlDoc("", credPath);
|
|
89
|
-
credDoc.set("version", CREDENTIALS_VERSION);
|
|
90
|
-
credDoc.setIn(["refs", KEY_REF], key);
|
|
91
|
-
writeValidatedYaml(credPath, credDoc);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const credDoc = parseYamlDoc(origCred, credPath);
|
|
95
|
-
const credData = credDoc.toJS();
|
|
96
|
-
if (!isMap(credDoc.contents)) throw new Error(`${credPath} 根节点不是映射结构,拒绝修改`);
|
|
97
|
-
if (typeof credData.version !== "undefined" && credData.version !== CREDENTIALS_VERSION) {
|
|
98
|
-
throw new Error(`${credPath} 的 version 为 ${credData.version},本开关仅支持 version: ${CREDENTIALS_VERSION}`);
|
|
99
|
-
}
|
|
100
|
-
if (typeof credData.version === "undefined") {
|
|
101
|
-
// 旧版扁平文档(dsh 启动时会自行迁移为 version:1):保持扁平形态写入
|
|
102
|
-
const flat = Object.values(credData).every((v) => typeof v === "string" && v.length > 0);
|
|
103
|
-
if (!flat) throw new Error(`${credPath} 既非 version:1 也非旧版扁平结构,请手工整理后重试`);
|
|
104
|
-
credDoc.set(KEY_REF, key);
|
|
105
|
-
} else {
|
|
106
|
-
if (typeof credData.refs !== "undefined" && (typeof credData.refs !== "object" || credData.refs === null)) {
|
|
107
|
-
throw new Error(`${credPath} 的 refs 不是映射结构,拒绝修改`);
|
|
108
|
-
}
|
|
109
|
-
credDoc.setIn(["refs", KEY_REF], key);
|
|
110
|
-
}
|
|
111
|
-
writeValidatedYaml(credPath, credDoc);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 关闭 dsh 代理:优先从备份还原两个文件(各自独立备份);无备份时移除本开关写入的字段——
|
|
116
|
-
* settings.yaml 仅在 baseURL 仍指向网关时移除(连带清理空 llm-deepseek 段);
|
|
117
|
-
* 凭据库仅在 DEEPSEEK_API_KEY 取值为本开关写入值(masterKey 或占位值)时移除,避免误删用户自己的官方 key。
|
|
118
|
-
* 返回是否实际发生了还原/移除。
|
|
119
|
-
*/
|
|
120
|
-
export function dshDisable(gatewayBase, masterKey) {
|
|
121
|
-
const settingsPath = settingsFile();
|
|
122
|
-
const credPath = credentialsFile();
|
|
123
|
-
let changed = false;
|
|
124
|
-
|
|
125
|
-
if (restoreFromBackup(settingsPath)) {
|
|
126
|
-
changed = true;
|
|
127
|
-
} else {
|
|
128
|
-
const text = readTextIfExists(settingsPath);
|
|
129
|
-
if (text) {
|
|
130
|
-
const doc = parseYamlDoc(text, settingsPath);
|
|
131
|
-
const data = doc.toJS();
|
|
132
|
-
if (sameUrl(data?.["llm-deepseek"]?.baseURL, targetBase(gatewayBase))) {
|
|
133
|
-
doc.deleteIn(["llm-deepseek", "baseURL"]);
|
|
134
|
-
const rest = doc.toJS()?.["llm-deepseek"];
|
|
135
|
-
if (!rest || typeof rest !== "object" || Object.keys(rest).length === 0) doc.deleteIn(["llm-deepseek"]);
|
|
136
|
-
writeValidatedYaml(settingsPath, doc);
|
|
137
|
-
changed = true;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (restoreFromBackup(credPath)) {
|
|
143
|
-
changed = true;
|
|
144
|
-
} else {
|
|
145
|
-
const text = readTextIfExists(credPath);
|
|
146
|
-
if (text) {
|
|
147
|
-
const doc = parseYamlDoc(text, credPath);
|
|
148
|
-
const data = doc.toJS();
|
|
149
|
-
const ours = (v) => v === masterKey || v === PLACEHOLDER_KEY;
|
|
150
|
-
if (data?.version === CREDENTIALS_VERSION && ours(data?.refs?.[KEY_REF])) {
|
|
151
|
-
doc.deleteIn(["refs", KEY_REF]);
|
|
152
|
-
if (Object.keys(doc.toJS()?.refs ?? {}).length === 0) doc.deleteIn(["refs"]);
|
|
153
|
-
writeValidatedYaml(credPath, doc);
|
|
154
|
-
changed = true;
|
|
155
|
-
} else if (typeof data?.version === "undefined" && ours(data?.[KEY_REF])) {
|
|
156
|
-
doc.deleteIn([KEY_REF]);
|
|
157
|
-
writeValidatedYaml(credPath, doc);
|
|
158
|
-
changed = true;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return changed;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** 写盘前经库回读校验(结果必须可解析),失败保留原文件 */
|
|
166
|
-
function writeValidatedYaml(file, doc) {
|
|
167
|
-
const text = doc.toString();
|
|
168
|
-
try {
|
|
169
|
-
parseDocument(text);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
throw new Error(`编辑后的 ${file} 不是合法 YAML,已保留原文件: ${err.message}`);
|
|
172
|
-
}
|
|
173
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
174
|
-
writeFileSync(file, text, "utf-8");
|
|
175
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// harness 代理开关聚合:Claude Code / Codex CLI / DeepSeek Harness 流量指向本机 ellm-proxy 网关。
|
|
2
|
-
// 纯开关实现:claude-code.js(~/.claude/settings.json)/ codex.js(~/.codex/config.toml)/
|
|
3
|
-
// dsh.js(~/.dsh/settings.yaml + ~/.dsh/.credentials.yaml)/ backup.js(共用备份还原)
|
|
4
|
-
// createHarnessApp = HTTP 路由子应用(挂载于 /harness):状态查询 + 开关切换(含 masterKey 校验与提示文案)
|
|
5
|
-
import { Hono } from "hono";
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
7
|
-
import { resolveGatewayBase } from "../../../utils/service.js";
|
|
8
|
-
import { claudeCodeStatus, claudeCodeEnable, claudeCodeDisable } from "./claude-code.js";
|
|
9
|
-
import { codexStatus, codexEnable, codexDisable } from "./codex.js";
|
|
10
|
-
import { dshStatus, dshEnable, dshDisable } from "./dsh.js";
|
|
11
|
-
|
|
12
|
-
/** 配置文件顶层 masterKey(Claude Code 代理的 AUTH_TOKEN 取值;网关侧用它做鉴权) */
|
|
13
|
-
function readMasterKey(configFile) {
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(readFileSync(configFile, "utf-8")).masterKey || "";
|
|
16
|
-
} catch {
|
|
17
|
-
return "";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** harness 开关子应用(挂载于 /harness):GET / · POST /claude-code · POST /codex · POST /dsh(body {enabled}) */
|
|
22
|
-
export function createHarnessApp({ startOpts }) {
|
|
23
|
-
const app = new Hono();
|
|
24
|
-
|
|
25
|
-
// GET /harness:三个开关状态
|
|
26
|
-
app.get("/", (c) => {
|
|
27
|
-
const { base } = resolveGatewayBase(startOpts.config);
|
|
28
|
-
return c.json({ ok: true, claudeCode: claudeCodeStatus(base), codex: codexStatus(), dsh: dshStatus(base), gatewayBase: base });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
/** 读 body 并校验 enabled;合法返回 boolean,非法返回 400 Response(调用方直接 return 该响应) */
|
|
32
|
-
const requireEnabled = async (c) => {
|
|
33
|
-
let enabled;
|
|
34
|
-
try {
|
|
35
|
-
enabled = (await c.req.json())?.enabled;
|
|
36
|
-
} catch {
|
|
37
|
-
return c.json({ ok: false, error: "请求体不是合法 JSON" }, 400);
|
|
38
|
-
}
|
|
39
|
-
if (typeof enabled !== "boolean") {
|
|
40
|
-
return c.json({ ok: false, error: 'body 需为 {"enabled": true|false}' }, 400);
|
|
41
|
-
}
|
|
42
|
-
return enabled;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
app.post("/claude-code", async (c) => {
|
|
46
|
-
const enabled = await requireEnabled(c);
|
|
47
|
-
if (enabled instanceof Response) return enabled;
|
|
48
|
-
const { base, configFile } = resolveGatewayBase(startOpts.config);
|
|
49
|
-
if (enabled) {
|
|
50
|
-
const masterKey = readMasterKey(configFile);
|
|
51
|
-
if (!masterKey) {
|
|
52
|
-
return c.json(
|
|
53
|
-
{
|
|
54
|
-
ok: false,
|
|
55
|
-
error: `配置未设置 masterKey,Claude Code 代理无法鉴权。请先在配置文件中设置 masterKey: ${configFile}`,
|
|
56
|
-
},
|
|
57
|
-
400,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
claudeCodeEnable({ gatewayBase: base, token: masterKey });
|
|
61
|
-
return c.json({
|
|
62
|
-
ok: true,
|
|
63
|
-
claudeCode: true,
|
|
64
|
-
file: "~/.claude/settings.json",
|
|
65
|
-
note: "已备份原文件为 settings.json.ellm-bak,重启 Claude Code 会话后生效",
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
const changed = claudeCodeDisable(base);
|
|
69
|
-
return c.json({ ok: true, claudeCode: false, changed, note: changed ? "已从备份还原" : "原本未开启,无操作" });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
app.post("/codex", async (c) => {
|
|
73
|
-
const enabled = await requireEnabled(c);
|
|
74
|
-
if (enabled instanceof Response) return enabled;
|
|
75
|
-
const { base, configFile } = resolveGatewayBase(startOpts.config);
|
|
76
|
-
if (enabled) {
|
|
77
|
-
codexEnable({ gatewayBase: base });
|
|
78
|
-
const masterKey = readMasterKey(configFile);
|
|
79
|
-
return c.json({
|
|
80
|
-
ok: true,
|
|
81
|
-
codex: true,
|
|
82
|
-
file: "~/.codex/config.toml",
|
|
83
|
-
note: masterKey
|
|
84
|
-
? `已备份原文件为 config.toml.ellm-bak;请设置环境变量 ELLM_PROXY_API_KEY=${masterKey}(Codex 以 env_key 读取鉴权)`
|
|
85
|
-
: "已备份原文件为 config.toml.ellm-bak;请设置环境变量 ELLM_PROXY_API_KEY(网关未配 masterKey 时取值任意)",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const changed = codexDisable();
|
|
89
|
-
return c.json({ ok: true, codex: false, changed, note: changed ? "已从备份还原" : "原本未开启,无操作" });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
app.post("/dsh", async (c) => {
|
|
93
|
-
const enabled = await requireEnabled(c);
|
|
94
|
-
if (enabled instanceof Response) return enabled;
|
|
95
|
-
const { base, configFile } = resolveGatewayBase(startOpts.config);
|
|
96
|
-
const masterKey = readMasterKey(configFile);
|
|
97
|
-
if (enabled) {
|
|
98
|
-
dshEnable({ gatewayBase: base, masterKey });
|
|
99
|
-
return c.json({
|
|
100
|
-
ok: true,
|
|
101
|
-
dsh: true,
|
|
102
|
-
file: "~/.dsh/settings.yaml",
|
|
103
|
-
note: masterKey
|
|
104
|
-
? "已写入 settings.yaml(llm-deepseek.baseURL)与 .credentials.yaml(refs.DEEPSEEK_API_KEY),原文件备份为 *.ellm-bak。注意:进程环境里已设 DEEPSEEK_API_KEY 时其优先级更高,需自行改用网关 masterKey;模型请在 dsh 内选择与网关部署名一致的模型"
|
|
105
|
-
: "已写入 settings.yaml 与 .credentials.yaml(网关未配 masterKey,密钥为占位值即可放行),原文件备份为 *.ellm-bak。模型请在 dsh 内选择与网关部署名一致的模型",
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
const changed = dshDisable(base, masterKey);
|
|
109
|
-
return c.json({ ok: true, dsh: false, changed, note: changed ? "已从备份还原(或移除本开关写入的字段)" : "原本未开启,无操作" });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
return app;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 纯开关函数同时聚合导出(供不经过 HTTP 的调用方直接使用)
|
|
116
|
-
export * from "./claude-code.js";
|
|
117
|
-
export * from "./codex.js";
|
|
118
|
-
export * from "./dsh.js";
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
}
|
package/manager/api/service.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// 主服务进程控制 api 子应用:状态查询与启停。
|
|
2
|
-
// 注意 stop 语义 = 只停主服务(与 CLI ellm-proxy stop 的全停语义不同)——
|
|
3
|
-
// 管理服务若自杀,再无人能远程拉起主服务。
|
|
4
|
-
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import { Hono } from "hono";
|
|
10
|
-
import { APP_NAME, MANAGER_APP_NAME } from "../../utils/paths.js";
|
|
11
|
-
import { withPm2, findApp, pm2cb, safeDump } from "../../utils/pm2.js";
|
|
12
|
-
import { doStart, stopMainOnly, resolveGatewayBase, resolvePort } from "../../utils/service.js";
|
|
13
|
-
|
|
14
|
-
// 本文件在 manager/api/ 下,包根为上两级
|
|
15
|
-
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
16
|
-
|
|
17
|
-
/** ellm-proxy 进程状态(pm2 视角) */
|
|
18
|
-
async function statusPayload(startOpts) {
|
|
19
|
-
const proc = await withPm2(() => findApp());
|
|
20
|
-
const { base } = resolveGatewayBase(startOpts.config);
|
|
21
|
-
return {
|
|
22
|
-
running: !!proc,
|
|
23
|
-
pid: proc?.pid ?? null,
|
|
24
|
-
pm2Status: proc?.pm2_env?.status ?? null,
|
|
25
|
-
uptime: proc?.pm2_env?.pm_uptime ?? null,
|
|
26
|
-
gatewayBase: base,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** pm2 依赖树内的 pidusage(realpath 穿透 pnpm 符号链接):pm2 守护进程自身的内存/CPU 采样 */
|
|
31
|
-
function loadPidusage() {
|
|
32
|
-
const pm2PkgJson = realpathSync(join(pkgRoot, "node_modules", "pm2", "package.json"));
|
|
33
|
-
return createRequire(pm2PkgJson)("pidusage");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 统一行形状:role = daemon | manager | main(前端按 main 判定可操作行) */
|
|
37
|
-
function row(role, { pid = null, status = "stopped", port = null, startedAt = null, memory = null, cpu = null, restarts = null }) {
|
|
38
|
-
return {
|
|
39
|
-
role,
|
|
40
|
-
pid,
|
|
41
|
-
status,
|
|
42
|
-
port,
|
|
43
|
-
address: port ? `http://127.0.0.1:${port}` : null,
|
|
44
|
-
startedAt,
|
|
45
|
-
memory,
|
|
46
|
-
cpu,
|
|
47
|
-
restarts,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** pm2 守护进程行:PID 读 PM2_HOME/pm2.pid(文件只在 daemon 启动时写,mtime 即启动时刻);
|
|
52
|
-
* 内存/CPU 用 pidusage 采样(pm2 jlist 不含 daemon 自身) */
|
|
53
|
-
async function daemonRow() {
|
|
54
|
-
const pm2Home = process.env.PM2_HOME || join(homedir(), ".pm2");
|
|
55
|
-
const pidFile = join(pm2Home, "pm2.pid");
|
|
56
|
-
let pid = null;
|
|
57
|
-
let startedAt = null;
|
|
58
|
-
try {
|
|
59
|
-
pid = Number(readFileSync(pidFile, "utf8").trim()) || null;
|
|
60
|
-
startedAt = statSync(pidFile).mtimeMs;
|
|
61
|
-
} catch {
|
|
62
|
-
/* 无 pm2.pid = daemon 未运行 */
|
|
63
|
-
}
|
|
64
|
-
if (!pid) return row("daemon");
|
|
65
|
-
let memory = null;
|
|
66
|
-
let cpu = null;
|
|
67
|
-
try {
|
|
68
|
-
const stat = await loadPidusage()(pid);
|
|
69
|
-
memory = stat.memory ?? null;
|
|
70
|
-
cpu = stat.cpu != null ? Math.round(stat.cpu * 10) / 10 : null;
|
|
71
|
-
} catch {
|
|
72
|
-
/* pid 文件存在但进程已死(采样 reject):按未运行处理 */
|
|
73
|
-
pid = null;
|
|
74
|
-
}
|
|
75
|
-
return row("daemon", { pid, status: pid ? "online" : "stopped", port: null, startedAt, memory, cpu });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** pm2 托管进程行:monit(内存/CPU,pm2 周期采样)+ pm_uptime(启动时刻)+ restart_time */
|
|
79
|
-
function appRow(proc, role, { port }) {
|
|
80
|
-
if (!proc) return row(role);
|
|
81
|
-
const env = proc.pm2_env ?? {};
|
|
82
|
-
const monit = proc.monit ?? {};
|
|
83
|
-
return row(role, {
|
|
84
|
-
pid: proc.pid ?? null,
|
|
85
|
-
status: env.status ?? "unknown",
|
|
86
|
-
port: port ?? null,
|
|
87
|
-
startedAt: env.pm_uptime ?? null,
|
|
88
|
-
memory: monit.memory ?? null,
|
|
89
|
-
cpu: monit.cpu != null ? Math.round(monit.cpu * 10) / 10 : null,
|
|
90
|
-
restarts: env.restart_time ?? null,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 全进程列表:pm2 守护进程 / 管理服务 / 主服务。
|
|
95
|
-
* 端口口径:主服务 = pm2 注入的 ELLM_PORT(含占用顺延后的实际值)> env > 配置文件;
|
|
96
|
-
* 管理服务 = ELLM_MANAGER_PORT env > 主端口 + 1(与 manager/index.js 的预算口径一致)。 */
|
|
97
|
-
async function processesPayload(startOpts) {
|
|
98
|
-
const list = (await withPm2(() => pm2cb("list"))) || [];
|
|
99
|
-
const main = list.find((p) => p.name === APP_NAME);
|
|
100
|
-
const manager = list.find((p) => p.name === MANAGER_APP_NAME);
|
|
101
|
-
const mainPort =
|
|
102
|
-
Number(main?.pm2_env?.env?.ELLM_PORT) || resolvePort(process.env.ELLM_PORT, startOpts.config) || null;
|
|
103
|
-
const managerPort = Number(process.env.ELLM_MANAGER_PORT) || (mainPort ? mainPort + 1 : null);
|
|
104
|
-
return [
|
|
105
|
-
await daemonRow(),
|
|
106
|
-
appRow(manager, "manager", { port: managerPort }),
|
|
107
|
-
appRow(main, "main", { port: mainPort }),
|
|
108
|
-
];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** pm2 启停:start / stop / restart(restart = 停主服务 + start,
|
|
112
|
-
* start 时管理服务已在线会被 doStart 的幂等检查跳过)。
|
|
113
|
-
* stopMainOnly 已抽到 utils/service.js(updater 更新前停主服务复用同语义)。 */
|
|
114
|
-
async function runStartStop(action, startOpts) {
|
|
115
|
-
if (action === "stop") return stopMainOnly();
|
|
116
|
-
if (action === "start") return doStart(startOpts);
|
|
117
|
-
await stopMainOnly();
|
|
118
|
-
await doStart(startOpts);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 进程控制子应用(挂载于根路径):GET /status · GET /processes · POST /start|stop|restart */
|
|
122
|
-
export function createServiceApp({ startOpts }) {
|
|
123
|
-
const app = new Hono();
|
|
124
|
-
|
|
125
|
-
app.get("/status", async (c) => c.json({ ok: true, ...(await statusPayload(startOpts)) }));
|
|
126
|
-
|
|
127
|
-
// 全进程列表(三行固定:pm2 守护进程 / 管理服务 / 主服务);启停操作仍只走
|
|
128
|
-
// /start|stop|restart 的主服务语义——daemon 与管理服务不允许被远程操控(自杀失联风险)
|
|
129
|
-
app.get("/processes", async (c) =>
|
|
130
|
-
c.json({ ok: true, processes: await processesPayload(startOpts) }),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
app.post("/start", async (c) => {
|
|
134
|
-
await runStartStop("start", startOpts);
|
|
135
|
-
return c.json({ ok: true, action: "start" });
|
|
136
|
-
});
|
|
137
|
-
app.post("/stop", async (c) => {
|
|
138
|
-
await runStartStop("stop", startOpts);
|
|
139
|
-
return c.json({ ok: true, action: "stop" });
|
|
140
|
-
});
|
|
141
|
-
app.post("/restart", async (c) => {
|
|
142
|
-
await runStartStop("restart", startOpts);
|
|
143
|
-
return c.json({ ok: true, action: "restart" });
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
return app;
|
|
147
|
-
}
|