ellm-proxy 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +29 -11
  2. package/bin/ellm-proxy.js +2 -60
  3. package/dist/cli.js +35780 -0
  4. package/dist/server.js +16714 -6336
  5. package/dist/web-ui/assets/DashboardView-3c_i_3VF.js +1 -0
  6. package/dist/web-ui/assets/LayoutView-BhN1f8wo.js +1 -0
  7. package/dist/web-ui/assets/LayoutView-CNd_MJt2.css +1 -0
  8. package/dist/web-ui/assets/LoginView-J23vsMu2.js +1 -0
  9. package/dist/web-ui/assets/{LogsView-BlXITEJH.js → LogsView-2h2Y0XE8.js} +2 -2
  10. package/dist/web-ui/assets/ProcessView-Bn8Ph7rh.css +1 -0
  11. package/dist/web-ui/assets/ProcessView-Bs2k6aAf.js +1 -0
  12. package/dist/web-ui/assets/{ProvidersView-f8aZkMdw.js → ProvidersView-bE1Hv9D-.js} +2 -2
  13. package/dist/web-ui/assets/SettingsView-BmLmC-_v.js +1 -0
  14. package/dist/web-ui/assets/SettingsView-Ceyj-ZDZ.css +1 -0
  15. package/dist/web-ui/assets/SetupView-Cgj7ubNz.js +1 -0
  16. package/dist/web-ui/assets/SetupView-kPJXJ7dX.css +1 -0
  17. package/dist/web-ui/assets/config-nvSp8hHp.js +1 -0
  18. package/dist/web-ui/assets/{index-GohB4JDa.js → index-sPrBBnf5.js} +28 -28
  19. package/dist/web-ui/assets/{providers-Djtdy2tv.js → providers-di7L8jy_.js} +1 -1
  20. package/dist/web-ui/assets/useManagerApi-qPjxfB0X.js +1 -0
  21. package/dist/web-ui/assets/useToast-D76qyWZg.js +1 -0
  22. package/dist/web-ui/config.js +3 -6
  23. package/dist/web-ui/index.html +1 -1
  24. package/docs/DESIGN.md +43 -14
  25. package/ellm.config.example.json +2 -0
  26. package/package.json +2 -17
  27. package/bin/commands/autostart.js +0 -37
  28. package/bin/commands/config.js +0 -8
  29. package/bin/commands/pm2.js +0 -28
  30. package/bin/commands/service.js +0 -107
  31. package/dist/web-ui/assets/DashboardView-C3Hw-HpG.js +0 -1
  32. package/dist/web-ui/assets/LayoutView-BUA8-IzK.css +0 -1
  33. package/dist/web-ui/assets/LayoutView-C9phf7in.js +0 -1
  34. package/dist/web-ui/assets/LoginView-es6pSPgt.js +0 -1
  35. package/dist/web-ui/assets/ProcessView-Bgf7-MLQ.js +0 -1
  36. package/dist/web-ui/assets/ProcessView-DK0TvDpR.css +0 -1
  37. package/dist/web-ui/assets/SettingsView-BkwyoOUb.css +0 -1
  38. package/dist/web-ui/assets/SettingsView-GvAA2vLp.js +0 -1
  39. package/dist/web-ui/assets/config-Cs3_z63C.js +0 -1
  40. package/dist/web-ui/assets/useApi-D_iJ2Mca.js +0 -1
  41. package/dist/web-ui/assets/useManagerApi-C1_COxKg.js +0 -1
  42. package/dist/web-ui/assets/useToast-C0UMTbkx.js +0 -1
  43. package/manager/api/harness/backup.js +0 -20
  44. package/manager/api/harness/claude-code.js +0 -54
  45. package/manager/api/harness/codex.js +0 -123
  46. package/manager/api/harness/dsh.js +0 -175
  47. package/manager/api/harness/index.js +0 -118
  48. package/manager/api/service-settings.js +0 -98
  49. package/manager/api/service.js +0 -147
  50. package/manager/index.js +0 -182
  51. package/utils/autostart.js +0 -219
  52. package/utils/interpreter.js +0 -14
  53. package/utils/paths.js +0 -45
  54. package/utils/pm2.js +0 -130
  55. package/utils/port.js +0 -35
  56. package/utils/service.js +0 -205
  57. package/utils/ui.js +0 -59
  58. package/utils/updater.js +0 -210
@@ -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
- }
@@ -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
- }