ellm-proxy 0.0.2 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +60 -60
  2. package/bin/commands/autostart.js +37 -213
  3. package/bin/commands/config.js +8 -8
  4. package/bin/commands/pm2.js +28 -28
  5. package/bin/commands/service.js +107 -107
  6. package/bin/ellm-proxy.js +61 -61
  7. package/dist/server.js +33 -2
  8. package/dist/web-ui/assets/DashboardView-BHG4xIdN.css +1 -0
  9. package/dist/web-ui/assets/{DashboardView-CQdlgirS.js → DashboardView-C3Hw-HpG.js} +1 -1
  10. package/dist/web-ui/assets/LayoutView-BUA8-IzK.css +1 -0
  11. package/dist/web-ui/assets/LayoutView-C9phf7in.js +1 -0
  12. package/dist/web-ui/assets/LoginView-DrhfslIY.css +1 -0
  13. package/dist/web-ui/assets/LoginView-es6pSPgt.js +1 -0
  14. package/dist/web-ui/assets/LogsView-BlXITEJH.js +9 -0
  15. package/dist/web-ui/assets/LogsView-Dl50XQhG.css +1 -0
  16. package/dist/web-ui/assets/{ProcessView-0x_ShSwV.js → ProcessView-Bgf7-MLQ.js} +1 -1
  17. package/dist/web-ui/assets/ProcessView-DK0TvDpR.css +1 -0
  18. package/dist/web-ui/assets/ProvidersView-Dpr8MI-p.css +1 -0
  19. package/dist/web-ui/assets/ProvidersView-f8aZkMdw.js +31 -0
  20. package/dist/web-ui/assets/SettingsView-BkwyoOUb.css +1 -0
  21. package/dist/web-ui/assets/SettingsView-GvAA2vLp.js +1 -0
  22. package/dist/web-ui/assets/config-Cs3_z63C.js +1 -0
  23. package/dist/web-ui/assets/{index-DFlUx18F.js → index-GohB4JDa.js} +2 -2
  24. package/dist/web-ui/assets/{providers-CGEmOxne.js → providers-Djtdy2tv.js} +1 -1
  25. package/dist/web-ui/assets/{useApi-BRPinb_2.js → useApi-D_iJ2Mca.js} +1 -1
  26. package/dist/web-ui/assets/useManagerApi-C1_COxKg.js +1 -0
  27. package/dist/web-ui/assets/{useToast-D7xlwSg6.js → useToast-C0UMTbkx.js} +1 -1
  28. package/dist/web-ui/config.js +10 -10
  29. package/dist/web-ui/index.html +13 -13
  30. package/docs/DESIGN.md +99 -99
  31. package/manager/api/harness/backup.js +20 -20
  32. package/manager/api/harness/claude-code.js +54 -54
  33. package/manager/api/harness/codex.js +123 -123
  34. package/manager/api/harness/dsh.js +175 -175
  35. package/manager/api/harness/index.js +118 -118
  36. package/manager/api/service-settings.js +98 -0
  37. package/manager/api/service.js +3 -13
  38. package/manager/index.js +182 -170
  39. package/package.json +44 -40
  40. package/utils/autostart.js +219 -0
  41. package/utils/interpreter.js +14 -14
  42. package/utils/paths.js +45 -44
  43. package/utils/pm2.js +130 -130
  44. package/utils/service.js +205 -189
  45. package/utils/updater.js +210 -0
  46. package/dist/web-ui/assets/DashboardView-qrQrkgLA.css +0 -1
  47. package/dist/web-ui/assets/LayoutView-BaY4Pz9W.js +0 -1
  48. package/dist/web-ui/assets/LoginView-CPvz1Ylx.js +0 -1
  49. package/dist/web-ui/assets/LoginView-Dg0mB6ok.css +0 -1
  50. package/dist/web-ui/assets/LogsView-B-bIj36j.js +0 -9
  51. package/dist/web-ui/assets/LogsView-Wl3_adHm.css +0 -1
  52. package/dist/web-ui/assets/ProcessView-DzMqBJmd.css +0 -1
  53. package/dist/web-ui/assets/ProvidersView-BxAxDRFH.css +0 -1
  54. package/dist/web-ui/assets/ProvidersView-GQENcvJS.js +0 -31
  55. package/dist/web-ui/assets/SettingsView-BDkymsrS.css +0 -1
  56. package/dist/web-ui/assets/SettingsView-BrAiaskZ.js +0 -1
  57. package/dist/web-ui/assets/config-DNn5cjYY.js +0 -1
  58. package/dist/web-ui/assets/useManagerApi-BDWJz5o6.js +0 -1
@@ -1,175 +1,175 @@
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
+ // 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 +1,118 @@
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
+ // 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";