jishushell 0.4.10 → 0.4.24-beta.2
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/Dockerfile.hermes-slim +193 -0
- package/INSTALL-NOTICE +10 -12
- package/apps/hermes-container.yaml +35 -0
- package/apps/ollama-binary.yaml +164 -0
- package/apps/ollama-cpu-container.yaml +37 -0
- package/apps/ollama-with-hollama-binary.yaml +159 -0
- package/apps/openclaw-binary.yaml +69 -0
- package/apps/openclaw-container.yaml +37 -0
- package/apps/openclaw-with-ollama-container.yaml +42 -0
- package/apps/openclaw-with-searxng-container.yaml +136 -0
- package/apps/openwebui-container.yaml +53 -0
- package/apps/playwright-container.yaml +120 -0
- package/apps/searxng-container.yaml +115 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +15 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli/app.d.ts +4 -0
- package/dist/cli/app.js +874 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/backup.d.ts +3 -0
- package/dist/cli/backup.js +434 -0
- package/dist/cli/backup.js.map +1 -0
- package/dist/{doctor.d.ts → cli/doctor.d.ts} +7 -1
- package/dist/{doctor.js → cli/doctor.js} +377 -22
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/helpers.d.ts +4 -0
- package/dist/cli/helpers.js +32 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/job.d.ts +4 -0
- package/dist/cli/job.js +198 -0
- package/dist/cli/job.js.map +1 -0
- package/dist/cli/llm.d.ts +25 -0
- package/dist/cli/llm.js +599 -0
- package/dist/cli/llm.js.map +1 -0
- package/dist/cli/managed-list.d.ts +30 -0
- package/dist/cli/managed-list.js +129 -0
- package/dist/cli/managed-list.js.map +1 -0
- package/dist/cli/panel.d.ts +26 -0
- package/dist/cli/panel.js +804 -0
- package/dist/cli/panel.js.map +1 -0
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +12 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +48 -776
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +69 -0
- package/dist/config.js +268 -7
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +17 -41
- package/dist/control.js +61 -1323
- package/dist/control.js.map +1 -1
- package/dist/install.d.ts +16 -0
- package/dist/install.js +75 -26
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +15 -0
- package/dist/routes/agent-apps.js +78 -0
- package/dist/routes/agent-apps.js.map +1 -0
- package/dist/routes/apps.d.ts +3 -0
- package/dist/routes/apps.js +278 -0
- package/dist/routes/apps.js.map +1 -0
- package/dist/routes/backup.js +3 -3
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/instances.d.ts +6 -0
- package/dist/routes/instances.js +863 -874
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.d.ts +15 -0
- package/dist/routes/llm.js +247 -0
- package/dist/routes/llm.js.map +1 -0
- package/dist/routes/runtime.d.ts +15 -0
- package/dist/routes/runtime.js +69 -0
- package/dist/routes/runtime.js.map +1 -0
- package/dist/routes/setup.js +131 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +56 -9
- package/dist/routes/system.js.map +1 -1
- package/dist/server.js +107 -7
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +30 -0
- package/dist/services/agent-apps/catalog.js +60 -0
- package/dist/services/agent-apps/catalog.js.map +1 -0
- package/dist/services/agent-apps/index.d.ts +36 -0
- package/dist/services/agent-apps/index.js +171 -0
- package/dist/services/agent-apps/index.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter-probes.d.ts +49 -0
- package/dist/services/agent-apps/installers/adapter-probes.js +223 -0
- package/dist/services/agent-apps/installers/adapter-probes.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter.d.ts +30 -0
- package/dist/services/agent-apps/installers/adapter.js +171 -0
- package/dist/services/agent-apps/installers/adapter.js.map +1 -0
- package/dist/services/agent-apps/installers/registry-probe.d.ts +38 -0
- package/dist/services/agent-apps/installers/registry-probe.js +183 -0
- package/dist/services/agent-apps/installers/registry-probe.js.map +1 -0
- package/dist/services/agent-apps/installers/shell-script.d.ts +47 -0
- package/dist/services/agent-apps/installers/shell-script.js +471 -0
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -0
- package/dist/services/agent-apps/types.d.ts +125 -0
- package/dist/services/agent-apps/types.js +17 -0
- package/dist/services/agent-apps/types.js.map +1 -0
- package/dist/services/app/app-compiler.d.ts +15 -0
- package/dist/services/app/app-compiler.js +172 -0
- package/dist/services/app/app-compiler.js.map +1 -0
- package/dist/services/app/app-manager.d.ts +142 -0
- package/dist/services/app/app-manager.js +2148 -0
- package/dist/services/app/app-manager.js.map +1 -0
- package/dist/services/app/custom-manager.d.ts +27 -0
- package/dist/services/app/custom-manager.js +285 -0
- package/dist/services/app/custom-manager.js.map +1 -0
- package/dist/services/app/hermes-agent-manager.d.ts +20 -0
- package/dist/services/app/hermes-agent-manager.js +289 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -0
- package/dist/services/app/id-normalizer.d.ts +27 -0
- package/dist/services/app/id-normalizer.js +77 -0
- package/dist/services/app/id-normalizer.js.map +1 -0
- package/dist/services/app/ollama-manager.d.ts +18 -0
- package/dist/services/app/ollama-manager.js +207 -0
- package/dist/services/app/ollama-manager.js.map +1 -0
- package/dist/services/app/openclaw-manager.d.ts +63 -0
- package/dist/services/app/openclaw-manager.js +1178 -0
- package/dist/services/app/openclaw-manager.js.map +1 -0
- package/dist/services/app/paths.d.ts +47 -0
- package/dist/services/app/paths.js +68 -0
- package/dist/services/app/paths.js.map +1 -0
- package/dist/services/app/registry.d.ts +17 -0
- package/dist/services/app/registry.js +31 -0
- package/dist/services/app/registry.js.map +1 -0
- package/dist/services/app/remote-spec.d.ts +14 -0
- package/dist/services/app/remote-spec.js +58 -0
- package/dist/services/app/remote-spec.js.map +1 -0
- package/dist/services/app/terminal-session-manager.d.ts +27 -0
- package/dist/services/app/terminal-session-manager.js +157 -0
- package/dist/services/app/terminal-session-manager.js.map +1 -0
- package/dist/services/app/types.d.ts +72 -0
- package/dist/services/app/types.js +16 -0
- package/dist/services/app/types.js.map +1 -0
- package/dist/services/backup-manager.js +60 -22
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/instance-manager.d.ts +125 -34
- package/dist/services/instance-manager.js +679 -1043
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/adapters.js +5 -1
- package/dist/services/llm-proxy/adapters.js.map +1 -1
- package/dist/services/llm-proxy/circuit-breaker.js +10 -2
- package/dist/services/llm-proxy/circuit-breaker.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +43 -0
- package/dist/services/llm-proxy/index.js +120 -5
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +1 -1
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +260 -3
- package/dist/services/nomad-manager.js +2921 -341
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/panel-manager.d.ts +50 -0
- package/dist/services/panel-manager.js +443 -0
- package/dist/services/panel-manager.js.map +1 -0
- package/dist/services/plugin-installer.js +28 -2
- package/dist/services/plugin-installer.js.map +1 -1
- package/dist/services/process-manager.js +42 -7
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.d.ts +20 -0
- package/dist/services/runtime/adapters/custom.js +90 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -0
- package/dist/services/runtime/adapters/hermes.d.ts +174 -0
- package/dist/services/runtime/adapters/hermes.js +1316 -0
- package/dist/services/runtime/adapters/hermes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +17 -0
- package/dist/services/runtime/adapters/openclaw-routes.js +946 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw.d.ts +188 -0
- package/dist/services/runtime/adapters/openclaw.js +2195 -0
- package/dist/services/runtime/adapters/openclaw.js.map +1 -0
- package/dist/services/runtime/errors.d.ts +28 -0
- package/dist/services/runtime/errors.js +31 -0
- package/dist/services/runtime/errors.js.map +1 -0
- package/dist/services/runtime/index.d.ts +34 -0
- package/dist/services/runtime/index.js +51 -0
- package/dist/services/runtime/index.js.map +1 -0
- package/dist/services/runtime/instance.d.ts +24 -0
- package/dist/services/runtime/instance.js +143 -0
- package/dist/services/runtime/instance.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +15 -0
- package/dist/services/runtime/migrations.js +25 -0
- package/dist/services/runtime/migrations.js.map +1 -0
- package/dist/services/runtime/registry.d.ts +13 -0
- package/dist/services/runtime/registry.js +32 -0
- package/dist/services/runtime/registry.js.map +1 -0
- package/dist/services/runtime/types.d.ts +545 -0
- package/dist/services/runtime/types.js +14 -0
- package/dist/services/runtime/types.js.map +1 -0
- package/dist/services/setup-manager.d.ts +70 -29
- package/dist/services/setup-manager.js +591 -625
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/task-registry.d.ts +44 -0
- package/dist/services/task-registry.js +74 -0
- package/dist/services/task-registry.js.map +1 -0
- package/dist/services/telemetry/heartbeat.d.ts +6 -6
- package/dist/services/telemetry/heartbeat.js +29 -30
- package/dist/services/telemetry/heartbeat.js.map +1 -1
- package/dist/services/update-manager.d.ts +47 -0
- package/dist/services/update-manager.js +305 -0
- package/dist/services/update-manager.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/utils/docker-host.d.ts +15 -0
- package/dist/utils/docker-host.js +64 -0
- package/dist/utils/docker-host.js.map +1 -0
- package/install/jishu-install.sh +303 -38
- package/install/post-install.sh +64 -5
- package/package.json +19 -5
- package/public/assets/Dashboard-rh9qpYRR.js +1 -0
- package/public/assets/HermesChatPanel-D6JI6lLY.js +1 -0
- package/public/assets/HermesConfigForm-DcbSemaj.js +4 -0
- package/public/assets/InitPassword-CFTKsED4.js +1 -0
- package/public/assets/InstanceDetail-BhNIKA6Z.js +91 -0
- package/public/assets/{Login-CUoEZOWR.js → Login-KB9qrtM0.js} +1 -1
- package/public/assets/NewInstance-CxkO8Hlq.js +1 -0
- package/public/assets/Settings-BVWJvOkU.js +1 -0
- package/public/assets/Setup-X-lzuaUT.js +1 -0
- package/public/assets/WeixinLoginPanel-gca0QTic.js +9 -0
- package/public/assets/index-C8B0cFJM.js +19 -0
- package/public/assets/index-CPhVFEsx.css +1 -0
- package/public/assets/input-paste-CrNVAyOy.js +1 -0
- package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
- package/public/assets/registry-fVUSujib.js +2 -0
- package/public/assets/{usePolling-CK0DfI4h.js → usePolling-Do5Erqm_.js} +1 -1
- package/public/assets/vendor-i18n-ucpM0OR0.js +9 -0
- package/public/assets/{vendor-react-B1-3Yrt-.js → vendor-react-Bk1hRGiY.js} +1 -1
- package/public/favicon.png +0 -0
- package/public/index.html +9 -4
- package/public/logos/hermes.png +0 -0
- package/public/logos/ollama.png +0 -0
- package/public/logos/openclaw.svg +60 -0
- package/scripts/build-hermes-image.sh +21 -0
- package/scripts/build-local.sh +54 -0
- package/scripts/check-adapter-isolation.ts +293 -0
- package/scripts/fixtures/instances/hermes-sample/instance.json +37 -0
- package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +7 -0
- package/scripts/smoke/hermes-bootstrap.sh +195 -0
- package/templates/hermes-entrypoint.sh +154 -0
- package/dist/doctor.js.map +0 -1
- package/install/jishu-install-china.sh +0 -3092
- package/public/assets/Dashboard-DhsrzJ4F.js +0 -1
- package/public/assets/InitPassword-BjubiVdd.js +0 -1
- package/public/assets/InstanceDetail-DMcywsof.js +0 -17
- package/public/assets/NewInstance-Bk0G4EiJ.js +0 -1
- package/public/assets/Settings-D5tHL_h5.js +0 -1
- package/public/assets/Setup-4t6E3Rut.js +0 -1
- package/public/assets/index-BJ47MWpF.css +0 -1
- package/public/assets/index-DbX85irc.js +0 -16
- package/public/assets/vendor-i18n-CfW0RvgE.js +0 -9
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw App-type Manager
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates ALL logic specific to the "openclaw" app type:
|
|
5
|
+
* - Instance directory layout (openclaw-home/.openclaw/openclaw.json)
|
|
6
|
+
* - defaultRuntime / starterConfig builders
|
|
7
|
+
* - Config load / save (openclaw.json + env files)
|
|
8
|
+
* - IM channel plugin installation (Feishu, WeChat, …)
|
|
9
|
+
* - Feishu / WeChat credential persistence
|
|
10
|
+
* - Pre-start validation and Docker config patching
|
|
11
|
+
*
|
|
12
|
+
* nomad-manager.ts owns the Nomad task/job spec builders; it imports
|
|
13
|
+
* getOpenclawHome / getOpenclawConfigPath etc. from here.
|
|
14
|
+
*
|
|
15
|
+
* Generic utilities (port allocation, listInstances, …) are imported
|
|
16
|
+
* from instance-manager.ts which is the public facade kept for backward
|
|
17
|
+
* compatibility with all existing callers.
|
|
18
|
+
*/
|
|
19
|
+
import { execFile, execFileSync } from "child_process";
|
|
20
|
+
import { randomBytes } from "crypto";
|
|
21
|
+
import { chmodSync, chownSync, copyFileSync, cpSync, existsSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, } from "fs";
|
|
22
|
+
import { userInfo } from "os";
|
|
23
|
+
import { dirname, join, resolve } from "path";
|
|
24
|
+
import { INSTANCES_DIR, JISHUSHELL_HOME, getInstanceDir, getPanelConfig, instanceMetaPath, } from "../../config.js";
|
|
25
|
+
import { LEGACY_PROVIDER_API_ALIASES } from "../../constants.js";
|
|
26
|
+
import { safeReadJson, safeWriteJson } from "../../utils/safe-json.js";
|
|
27
|
+
import { ensureDirContainer, writeConfigFile } from "../../utils/fs.js";
|
|
28
|
+
import { compileTaskRuntime } from "./app-compiler.js";
|
|
29
|
+
import { getInstance, listInstances, resolveServiceUser, chownToServiceUser, getRuntimeEnvFiles, getRuntimeEnv, updateEnvFile, defaultGatewayPort, releasePort, inferProviderApiKeyEnvName, notifyConfigChange, getGatewayPort, getInstanceRuntime, } from "../instance-manager.js";
|
|
30
|
+
// ── OpenClaw-specific constants ──────────────────────────────────────────────
|
|
31
|
+
export const INSTANCE_OPENCLAW_HOME_DIRNAME = "openclaw-home";
|
|
32
|
+
export const INSTANCE_MODEL_ENV_FILENAME = "model.env";
|
|
33
|
+
const OPENCLAW_STATE_DIRNAME = ".openclaw";
|
|
34
|
+
const OPENCLAW_CONFIG_FILENAME = "openclaw.json";
|
|
35
|
+
// ── Private utilities ────────────────────────────────────────────────────────
|
|
36
|
+
function normalizePath(p) {
|
|
37
|
+
return resolve(p.replace(/^~/, userInfo().homedir));
|
|
38
|
+
}
|
|
39
|
+
function deepMerge(base, overlay) {
|
|
40
|
+
if (typeof base !== "object" || base === null ||
|
|
41
|
+
typeof overlay !== "object" || overlay === null ||
|
|
42
|
+
Array.isArray(base) || Array.isArray(overlay)) {
|
|
43
|
+
return structuredClone(overlay);
|
|
44
|
+
}
|
|
45
|
+
const merged = structuredClone(base);
|
|
46
|
+
for (const key of Object.keys(overlay)) {
|
|
47
|
+
merged[key] = key in merged ? deepMerge(merged[key], overlay[key]) : structuredClone(overlay[key]);
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
function loadJson(path) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
console.warn(`[openclaw] Failed to parse ${path}: ${e.message}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ── Path helpers ─────────────────────────────────────────────────────────────
|
|
61
|
+
export function defaultOpenclawHome(instanceId) {
|
|
62
|
+
return join(getInstanceDir(instanceId), INSTANCE_OPENCLAW_HOME_DIRNAME);
|
|
63
|
+
}
|
|
64
|
+
export function defaultModelEnvFile(instanceId) {
|
|
65
|
+
return join(getInstanceDir(instanceId), INSTANCE_MODEL_ENV_FILENAME);
|
|
66
|
+
}
|
|
67
|
+
export function getOpenclawHome(instanceId) {
|
|
68
|
+
const meta = getInstance(instanceId);
|
|
69
|
+
return meta?.openclaw_home || defaultOpenclawHome(instanceId);
|
|
70
|
+
}
|
|
71
|
+
export function openclawStateDir(instanceId) {
|
|
72
|
+
return join(getOpenclawHome(instanceId), OPENCLAW_STATE_DIRNAME);
|
|
73
|
+
}
|
|
74
|
+
function legacyOpenclawConfigPath(instanceId) {
|
|
75
|
+
return join(getOpenclawHome(instanceId), OPENCLAW_CONFIG_FILENAME);
|
|
76
|
+
}
|
|
77
|
+
function openclawConfigPathInternal(instanceId) {
|
|
78
|
+
return join(openclawStateDir(instanceId), OPENCLAW_CONFIG_FILENAME);
|
|
79
|
+
}
|
|
80
|
+
export function getOpenclawConfigPath(instanceId) {
|
|
81
|
+
return openclawConfigPathInternal(instanceId);
|
|
82
|
+
}
|
|
83
|
+
export function getLegacyOpenclawConfigPath(instanceId) {
|
|
84
|
+
return legacyOpenclawConfigPath(instanceId);
|
|
85
|
+
}
|
|
86
|
+
function resolveExistingConfigPath(instanceId) {
|
|
87
|
+
const runtimePath = openclawConfigPathInternal(instanceId);
|
|
88
|
+
if (existsSync(runtimePath))
|
|
89
|
+
return runtimePath;
|
|
90
|
+
const legacyPath = legacyOpenclawConfigPath(instanceId);
|
|
91
|
+
if (existsSync(legacyPath))
|
|
92
|
+
return legacyPath;
|
|
93
|
+
return runtimePath;
|
|
94
|
+
}
|
|
95
|
+
export function getChannelExtensionsDir(instanceId) {
|
|
96
|
+
return join(getOpenclawHome(instanceId), OPENCLAW_STATE_DIRNAME, "extensions");
|
|
97
|
+
}
|
|
98
|
+
export function getStockExtensionsDir() {
|
|
99
|
+
return join(JISHUSHELL_HOME, "packages", "openclaw", "lib", "node_modules", "openclaw", "extensions");
|
|
100
|
+
}
|
|
101
|
+
// ── OpenClaw binary resolution ───────────────────────────────────────────────
|
|
102
|
+
function resolveOpenclawBin() {
|
|
103
|
+
const candidates = [
|
|
104
|
+
join(JISHUSHELL_HOME, "packages", "openclaw", "bin", "openclaw"),
|
|
105
|
+
"/usr/local/bin/openclaw",
|
|
106
|
+
"/usr/bin/openclaw",
|
|
107
|
+
];
|
|
108
|
+
for (const p of candidates) {
|
|
109
|
+
if (existsSync(p)) {
|
|
110
|
+
try {
|
|
111
|
+
execFileSync("chmod", ["+x", p], { timeout: 3000 });
|
|
112
|
+
}
|
|
113
|
+
catch { /* best effort */ }
|
|
114
|
+
try {
|
|
115
|
+
const real = realpathSync(p);
|
|
116
|
+
if (real !== p) {
|
|
117
|
+
try {
|
|
118
|
+
execFileSync("chmod", ["+x", real], { timeout: 3000 });
|
|
119
|
+
}
|
|
120
|
+
catch { /* best effort */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch { /* best effort */ }
|
|
124
|
+
return p;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return candidates[0];
|
|
128
|
+
}
|
|
129
|
+
export function getResolvedOpenclawBin() {
|
|
130
|
+
return resolveOpenclawBin();
|
|
131
|
+
}
|
|
132
|
+
// ── Runtime / config builders ────────────────────────────────────────────────
|
|
133
|
+
async function buildDefaultRuntime(instanceId, openclawHome) {
|
|
134
|
+
const port = await defaultGatewayPort(instanceId);
|
|
135
|
+
const svcUser = resolveServiceUser();
|
|
136
|
+
return {
|
|
137
|
+
runtime: {
|
|
138
|
+
command: resolveOpenclawBin(),
|
|
139
|
+
args: ["gateway", "run", "--port", String(port), "--allow-unconfigured"],
|
|
140
|
+
cwd: openclawHome,
|
|
141
|
+
user: svcUser?.username ?? userInfo().username,
|
|
142
|
+
env_files: [defaultModelEnvFile(instanceId)],
|
|
143
|
+
env: {
|
|
144
|
+
OPENCLAW_GATEWAY_PORT: String(port),
|
|
145
|
+
NODE_OPTIONS: "--max-old-space-size=2048",
|
|
146
|
+
},
|
|
147
|
+
resources: { CPU: 1000, MemoryMB: 2048 },
|
|
148
|
+
},
|
|
149
|
+
allocatedPort: port,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function buildStarterConfig() {
|
|
153
|
+
const dp = getPanelConfig().default_provider;
|
|
154
|
+
let providerName = "minimax";
|
|
155
|
+
let providerConfig = {
|
|
156
|
+
baseUrl: "https://api.minimaxi.com/v1",
|
|
157
|
+
api: "openai-completions",
|
|
158
|
+
models: [{ id: "MiniMax-M2.7", name: "MiniMax M2.7", contextWindow: 204800 }],
|
|
159
|
+
};
|
|
160
|
+
let defaultModel = "minimax/MiniMax-M2.7";
|
|
161
|
+
if (dp?.providerId) {
|
|
162
|
+
providerName = dp.providerId;
|
|
163
|
+
providerConfig = {
|
|
164
|
+
baseUrl: dp.baseUrl,
|
|
165
|
+
api: dp.api,
|
|
166
|
+
...(dp.authHeader ? { authHeader: true } : {}),
|
|
167
|
+
models: dp.models || [],
|
|
168
|
+
};
|
|
169
|
+
const modelId = dp.selectedModelId || dp.models?.[0]?.id || "";
|
|
170
|
+
defaultModel = `${providerName}/${modelId}`;
|
|
171
|
+
}
|
|
172
|
+
const config = {
|
|
173
|
+
models: { providers: { [providerName]: providerConfig } },
|
|
174
|
+
agents: { defaults: { model: defaultModel, models: { [defaultModel]: {} } } },
|
|
175
|
+
channels: {},
|
|
176
|
+
gateway: {
|
|
177
|
+
mode: "local",
|
|
178
|
+
auth: { mode: "token", token: randomBytes(24).toString("hex") },
|
|
179
|
+
controlUi: { dangerouslyDisableDeviceAuth: true },
|
|
180
|
+
},
|
|
181
|
+
plugins: { entries: { feishu: { enabled: false } } },
|
|
182
|
+
};
|
|
183
|
+
if (dp?.providerId) {
|
|
184
|
+
config["x-jishushell"] = {
|
|
185
|
+
proxy: {
|
|
186
|
+
upstream: {
|
|
187
|
+
providerId: dp.providerId,
|
|
188
|
+
baseUrl: dp.baseUrl,
|
|
189
|
+
api: dp.api,
|
|
190
|
+
authHeader: dp.authHeader || false,
|
|
191
|
+
models: dp.models || [],
|
|
192
|
+
selectedModelId: dp.selectedModelId || dp.models?.[0]?.id || "",
|
|
193
|
+
hasApiKey: !!dp.apiKey,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return config;
|
|
199
|
+
}
|
|
200
|
+
// backward-compat alias used by existing callers
|
|
201
|
+
export { buildStarterConfig as starterConfig };
|
|
202
|
+
// ── Config loading ───────────────────────────────────────────────────────────
|
|
203
|
+
function loadEffectiveConfig(instanceId) {
|
|
204
|
+
const runtimePath = openclawConfigPathInternal(instanceId);
|
|
205
|
+
const legacyPath = legacyOpenclawConfigPath(instanceId);
|
|
206
|
+
const rExists = existsSync(runtimePath);
|
|
207
|
+
const lExists = existsSync(legacyPath);
|
|
208
|
+
if (rExists && lExists) {
|
|
209
|
+
const legacy = loadJson(legacyPath);
|
|
210
|
+
const runtime = loadJson(runtimePath);
|
|
211
|
+
if (legacy && runtime)
|
|
212
|
+
return deepMerge(legacy, runtime);
|
|
213
|
+
return runtime || legacy || null;
|
|
214
|
+
}
|
|
215
|
+
if (rExists)
|
|
216
|
+
return loadJson(runtimePath);
|
|
217
|
+
if (lExists)
|
|
218
|
+
return loadJson(legacyPath);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
// ── Provider key helpers ─────────────────────────────────────────────────────
|
|
222
|
+
function hasConfiguredValue(value) {
|
|
223
|
+
if (typeof value === "string")
|
|
224
|
+
return value.trim().length > 0;
|
|
225
|
+
if (typeof value === "object" && value !== null)
|
|
226
|
+
return Object.keys(value).length > 0;
|
|
227
|
+
return value != null;
|
|
228
|
+
}
|
|
229
|
+
function injectProviderApiKeys(instanceId, config) {
|
|
230
|
+
const merged = structuredClone(config);
|
|
231
|
+
const runtimeEnv = getRuntimeEnv(instanceId);
|
|
232
|
+
const providers = merged.models?.providers || {};
|
|
233
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
234
|
+
if (typeof provider !== "object" || provider === null)
|
|
235
|
+
continue;
|
|
236
|
+
const p = provider;
|
|
237
|
+
const api = p.api;
|
|
238
|
+
if (typeof api === "string" && api in LEGACY_PROVIDER_API_ALIASES) {
|
|
239
|
+
p.api = LEGACY_PROVIDER_API_ALIASES[api];
|
|
240
|
+
}
|
|
241
|
+
const apiKey = runtimeEnv[inferProviderApiKeyEnvName(providerId)];
|
|
242
|
+
if (apiKey)
|
|
243
|
+
p.apiKey = apiKey;
|
|
244
|
+
}
|
|
245
|
+
return merged;
|
|
246
|
+
}
|
|
247
|
+
function applyFeishuDebugAccessDefaults(channel) {
|
|
248
|
+
if (channel.enabled === false)
|
|
249
|
+
return;
|
|
250
|
+
if (!hasConfiguredValue(channel.appId))
|
|
251
|
+
return;
|
|
252
|
+
if (!hasConfiguredValue(channel.appSecret))
|
|
253
|
+
return;
|
|
254
|
+
let dmPolicy = channel.dmPolicy;
|
|
255
|
+
if (typeof dmPolicy !== "string" || !dmPolicy.trim()) {
|
|
256
|
+
channel.dmPolicy = "open";
|
|
257
|
+
dmPolicy = "open";
|
|
258
|
+
}
|
|
259
|
+
if (dmPolicy !== "open")
|
|
260
|
+
return;
|
|
261
|
+
if (!("resolveSenderNames" in channel))
|
|
262
|
+
channel.resolveSenderNames = false;
|
|
263
|
+
let accounts = channel.accounts;
|
|
264
|
+
if (typeof accounts !== "object" || accounts === null) {
|
|
265
|
+
accounts = {};
|
|
266
|
+
channel.accounts = accounts;
|
|
267
|
+
}
|
|
268
|
+
let defaultAccount = accounts.default;
|
|
269
|
+
if (typeof defaultAccount !== "object" || defaultAccount === null) {
|
|
270
|
+
defaultAccount = {};
|
|
271
|
+
accounts.default = defaultAccount;
|
|
272
|
+
}
|
|
273
|
+
if (!("resolveSenderNames" in defaultAccount))
|
|
274
|
+
defaultAccount.resolveSenderNames = false;
|
|
275
|
+
const allowFrom = channel.allowFrom;
|
|
276
|
+
if (Array.isArray(allowFrom)) {
|
|
277
|
+
const normalized = allowFrom.map((e) => String(e).trim()).filter(Boolean);
|
|
278
|
+
if (!normalized.includes("*"))
|
|
279
|
+
normalized.push("*");
|
|
280
|
+
channel.allowFrom = normalized;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
channel.allowFrom = ["*"];
|
|
284
|
+
}
|
|
285
|
+
function prepareConfigForSave(instanceId, config) {
|
|
286
|
+
const configToWrite = structuredClone(config);
|
|
287
|
+
delete configToWrite["x-jishushell"];
|
|
288
|
+
const envUpdates = {};
|
|
289
|
+
const providers = configToWrite.models?.providers || {};
|
|
290
|
+
const envFiles = getRuntimeEnvFiles(instanceId);
|
|
291
|
+
const channels = configToWrite.channels || {};
|
|
292
|
+
const plugins = configToWrite.plugins ??= {};
|
|
293
|
+
const pluginEntries = plugins.entries ??= {};
|
|
294
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
295
|
+
if (typeof provider !== "object" || provider === null)
|
|
296
|
+
continue;
|
|
297
|
+
const p = provider;
|
|
298
|
+
if (typeof p.api === "string" && p.api in LEGACY_PROVIDER_API_ALIASES) {
|
|
299
|
+
p.api = LEGACY_PROVIDER_API_ALIASES[p.api];
|
|
300
|
+
}
|
|
301
|
+
if (!("apiKey" in p))
|
|
302
|
+
continue;
|
|
303
|
+
if (typeof p.baseUrl === "string" && p.baseUrl.includes("/proxy/"))
|
|
304
|
+
continue;
|
|
305
|
+
const apiKey = p.apiKey;
|
|
306
|
+
delete p.apiKey;
|
|
307
|
+
if (envFiles.length) {
|
|
308
|
+
envUpdates[inferProviderApiKeyEnvName(providerId)] = String(apiKey || "");
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
p.apiKey = apiKey;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const [channelId, channel] of Object.entries(channels)) {
|
|
315
|
+
if (typeof channel !== "object" || channel === null)
|
|
316
|
+
continue;
|
|
317
|
+
const ch = channel;
|
|
318
|
+
if (channelId === "feishu" || channelId === "lark")
|
|
319
|
+
applyFeishuDebugAccessDefaults(ch);
|
|
320
|
+
let pluginEntry = pluginEntries[channelId];
|
|
321
|
+
if (pluginEntry == null) {
|
|
322
|
+
pluginEntry = {};
|
|
323
|
+
pluginEntries[channelId] = pluginEntry;
|
|
324
|
+
}
|
|
325
|
+
if (typeof pluginEntry === "object") {
|
|
326
|
+
pluginEntry.enabled = ch.enabled !== false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return [configToWrite, envUpdates];
|
|
330
|
+
}
|
|
331
|
+
// ── IM channel plugin helpers ────────────────────────────────────────────────
|
|
332
|
+
const CHANNEL_EXT_DIR_ALIAS = {
|
|
333
|
+
feishu: "openclaw-lark",
|
|
334
|
+
lark: "openclaw-lark",
|
|
335
|
+
};
|
|
336
|
+
export const CHANNEL_PLUGIN_MAP = {
|
|
337
|
+
feishu: "@larksuite/openclaw-lark",
|
|
338
|
+
lark: "@larksuite/openclaw-lark",
|
|
339
|
+
telegram: "@openclaw/telegram",
|
|
340
|
+
discord: "@openclaw/discord",
|
|
341
|
+
slack: "@openclaw/slack",
|
|
342
|
+
whatsapp: "@openclaw/whatsapp",
|
|
343
|
+
signal: "@openclaw/signal",
|
|
344
|
+
line: "@openclaw/line",
|
|
345
|
+
msteams: "@openclaw/msteams",
|
|
346
|
+
"openclaw-weixin": "@tencent-weixin/openclaw-weixin",
|
|
347
|
+
};
|
|
348
|
+
const IM_PLUGIN_ENTRY_IDS = new Set([
|
|
349
|
+
...Object.keys(CHANNEL_PLUGIN_MAP),
|
|
350
|
+
...Object.values(CHANNEL_EXT_DIR_ALIAS),
|
|
351
|
+
]);
|
|
352
|
+
export function stripImBindings(config) {
|
|
353
|
+
if (config?.channels)
|
|
354
|
+
delete config.channels;
|
|
355
|
+
const entries = config?.plugins?.entries;
|
|
356
|
+
if (entries && typeof entries === "object") {
|
|
357
|
+
for (const key of Object.keys(entries)) {
|
|
358
|
+
if (IM_PLUGIN_ENTRY_IDS.has(key))
|
|
359
|
+
delete entries[key];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
export function isChannelPluginInstalled(instanceId, channelId) {
|
|
364
|
+
const extDirName = CHANNEL_EXT_DIR_ALIAS[channelId] || channelId;
|
|
365
|
+
const stockExtDir = getStockExtensionsDir();
|
|
366
|
+
return existsSync(join(getChannelExtensionsDir(instanceId), extDirName))
|
|
367
|
+
|| existsSync(join(stockExtDir, extDirName))
|
|
368
|
+
|| (extDirName !== channelId && existsSync(join(stockExtDir, channelId)));
|
|
369
|
+
}
|
|
370
|
+
export async function installChannelPlugin(instanceId, channelId) {
|
|
371
|
+
const pkg = CHANNEL_PLUGIN_MAP[channelId];
|
|
372
|
+
if (!pkg)
|
|
373
|
+
throw new Error(`Unknown channel: ${channelId}`);
|
|
374
|
+
if (isChannelPluginInstalled(instanceId, channelId))
|
|
375
|
+
return;
|
|
376
|
+
const openclawHome = getOpenclawHome(instanceId);
|
|
377
|
+
const extensionsDir = getChannelExtensionsDir(instanceId);
|
|
378
|
+
const { getNomadDriver } = await import("../../config.js");
|
|
379
|
+
if (getNomadDriver() === "docker") {
|
|
380
|
+
await installChannelPluginViaDocker(instanceId, channelId, pkg, extensionsDir);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const openclawBin = resolveOpenclawBin();
|
|
384
|
+
const nodeBinDir = dirname(process.execPath);
|
|
385
|
+
const childPath = [nodeBinDir, process.env.PATH].filter(Boolean).join(":");
|
|
386
|
+
const proxyEnvKeys = [
|
|
387
|
+
"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY",
|
|
388
|
+
"no_proxy", "NO_PROXY", "NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED",
|
|
389
|
+
];
|
|
390
|
+
const proxyEnv = {};
|
|
391
|
+
for (const key of proxyEnvKeys) {
|
|
392
|
+
if (process.env[key])
|
|
393
|
+
proxyEnv[key] = process.env[key];
|
|
394
|
+
}
|
|
395
|
+
const childEnv = {
|
|
396
|
+
PATH: childPath,
|
|
397
|
+
HOME: process.env.HOME,
|
|
398
|
+
LANG: process.env.LANG,
|
|
399
|
+
OPENCLAW_HOME: openclawHome,
|
|
400
|
+
...proxyEnv,
|
|
401
|
+
};
|
|
402
|
+
const MAX_ATTEMPTS = 3;
|
|
403
|
+
const RETRY_DELAY_MS = 5_000;
|
|
404
|
+
const attemptInstall = () => new Promise((res, rej) => {
|
|
405
|
+
execFile(openclawBin, ["plugins", "install", pkg], { cwd: openclawHome, env: childEnv, timeout: 300_000 }, (err, stdout, stderr) => {
|
|
406
|
+
if (err && !isChannelPluginInstalled(instanceId, channelId)) {
|
|
407
|
+
const msg = [stderr?.trim(), stdout?.trim(), err.message].filter(Boolean).join(" | ");
|
|
408
|
+
console.error(`[openclaw-plugins] ${pkg} exit code ${err.code ?? "?"}`);
|
|
409
|
+
try {
|
|
410
|
+
if (existsSync(extensionsDir)) {
|
|
411
|
+
for (const entry of readdirSync(extensionsDir)) {
|
|
412
|
+
if (entry.startsWith(".openclaw-install-stage-")) {
|
|
413
|
+
rmSync(join(extensionsDir, entry), { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch { /* best effort */ }
|
|
419
|
+
rej(new Error(msg));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
res();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
console.log(`[openclaw-plugins] Installing ${pkg} for ${channelId} (host)...`);
|
|
427
|
+
let lastErr;
|
|
428
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
429
|
+
try {
|
|
430
|
+
await attemptInstall();
|
|
431
|
+
const extDirName = CHANNEL_EXT_DIR_ALIAS[channelId] || channelId;
|
|
432
|
+
const installedExtDir = join(extensionsDir, extDirName);
|
|
433
|
+
if (existsSync(installedExtDir)) {
|
|
434
|
+
ensureDirContainer(installedExtDir);
|
|
435
|
+
try {
|
|
436
|
+
for (const entry of readdirSync(installedExtDir, { withFileTypes: true })) {
|
|
437
|
+
if (entry.isDirectory())
|
|
438
|
+
ensureDirContainer(join(installedExtDir, entry.name));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch { /* best effort */ }
|
|
442
|
+
}
|
|
443
|
+
ensureDirContainer(extensionsDir);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
lastErr = err;
|
|
448
|
+
const isFetchError = /fetch failed/i.test(err.message ?? "");
|
|
449
|
+
if (isFetchError && attempt < MAX_ATTEMPTS) {
|
|
450
|
+
console.warn(`[openclaw-plugins] ${pkg} install attempt ${attempt}/${MAX_ATTEMPTS} failed, retrying...`);
|
|
451
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
throw lastErr;
|
|
458
|
+
}
|
|
459
|
+
async function installChannelPluginViaDocker(instanceId, channelId, pkg, extensionsDir) {
|
|
460
|
+
const { exec } = await import("../nomad-manager.js");
|
|
461
|
+
const MAX_ATTEMPTS = 3;
|
|
462
|
+
const RETRY_DELAY_MS = 5_000;
|
|
463
|
+
console.log(`[openclaw-plugins] Installing ${pkg} for ${channelId} via docker exec (${instanceId})...`);
|
|
464
|
+
let lastErr;
|
|
465
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
466
|
+
try {
|
|
467
|
+
const result = await exec(instanceId, ["openclaw", "plugins", "install", pkg], 300_000);
|
|
468
|
+
if (result.exitCode !== 0 && !isChannelPluginInstalled(instanceId, channelId)) {
|
|
469
|
+
const msg = [result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join(" | ");
|
|
470
|
+
throw new Error(msg || `openclaw plugins install exited with code ${result.exitCode}`);
|
|
471
|
+
}
|
|
472
|
+
console.log(`[openclaw-plugins] ${pkg} installed via docker`);
|
|
473
|
+
const extDirName = CHANNEL_EXT_DIR_ALIAS[channelId] || channelId;
|
|
474
|
+
const installedExtDir = join(extensionsDir, extDirName);
|
|
475
|
+
if (existsSync(installedExtDir)) {
|
|
476
|
+
ensureDirContainer(installedExtDir);
|
|
477
|
+
try {
|
|
478
|
+
for (const entry of readdirSync(installedExtDir, { withFileTypes: true })) {
|
|
479
|
+
if (entry.isDirectory())
|
|
480
|
+
ensureDirContainer(join(installedExtDir, entry.name));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch { /* best effort */ }
|
|
484
|
+
}
|
|
485
|
+
ensureDirContainer(extensionsDir);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
lastErr = err;
|
|
490
|
+
if (/not running/i.test(err.message ?? "")) {
|
|
491
|
+
throw new Error("请先启动实例后再安装插件(Docker 模式下插件需在容器内安装)");
|
|
492
|
+
}
|
|
493
|
+
const isTransient = /fetch failed|ECONNREFUSED/i.test(err.message ?? "");
|
|
494
|
+
if (isTransient && attempt < MAX_ATTEMPTS) {
|
|
495
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
throw lastErr;
|
|
502
|
+
}
|
|
503
|
+
// ── Public config API ────────────────────────────────────────────────────────
|
|
504
|
+
export function getConfig(instanceId) {
|
|
505
|
+
const config = loadEffectiveConfig(instanceId);
|
|
506
|
+
if (!config)
|
|
507
|
+
return null;
|
|
508
|
+
const meta = getInstance(instanceId);
|
|
509
|
+
if (meta?.["x-jishushell"])
|
|
510
|
+
config["x-jishushell"] = meta["x-jishushell"];
|
|
511
|
+
return injectProviderApiKeys(instanceId, config);
|
|
512
|
+
}
|
|
513
|
+
export function getStoredConfig(instanceId) {
|
|
514
|
+
const config = loadEffectiveConfig(instanceId);
|
|
515
|
+
if (!config)
|
|
516
|
+
return null;
|
|
517
|
+
const meta = getInstance(instanceId);
|
|
518
|
+
if (meta?.["x-jishushell"])
|
|
519
|
+
config["x-jishushell"] = meta["x-jishushell"];
|
|
520
|
+
return config;
|
|
521
|
+
}
|
|
522
|
+
export function saveConfig(instanceId, config) {
|
|
523
|
+
const configPath = openclawConfigPathInternal(instanceId);
|
|
524
|
+
if (!existsSync(getInstanceDir(instanceId)))
|
|
525
|
+
return false;
|
|
526
|
+
if (!existsSync(configPath)) {
|
|
527
|
+
const legacyPath = legacyOpenclawConfigPath(instanceId);
|
|
528
|
+
ensureDirContainer(dirname(configPath));
|
|
529
|
+
if (existsSync(legacyPath))
|
|
530
|
+
copyFileSync(legacyPath, configPath);
|
|
531
|
+
}
|
|
532
|
+
if (config["x-jishushell"]) {
|
|
533
|
+
const metaPath = instanceMetaPath(instanceId);
|
|
534
|
+
if (existsSync(metaPath)) {
|
|
535
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
536
|
+
meta["x-jishushell"] = config["x-jishushell"];
|
|
537
|
+
safeWriteJson(metaPath, meta);
|
|
538
|
+
chownToServiceUser(metaPath);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const [configToWrite, envUpdates] = prepareConfigForSave(instanceId, config);
|
|
542
|
+
if (configToWrite.plugins?.entries?.["openclaw-lark"]?.enabled) {
|
|
543
|
+
const stockExtDir = getStockExtensionsDir();
|
|
544
|
+
const stockFeishu = join(stockExtDir, "feishu");
|
|
545
|
+
const stockOcl = join(stockExtDir, "openclaw-lark");
|
|
546
|
+
const instanceOcl = join(getChannelExtensionsDir(instanceId), "openclaw-lark");
|
|
547
|
+
if (existsSync(stockFeishu) && !existsSync(stockOcl) && !existsSync(instanceOcl)) {
|
|
548
|
+
configToWrite.plugins.entries.feishu = { enabled: true };
|
|
549
|
+
delete configToWrite.plugins.entries["openclaw-lark"];
|
|
550
|
+
}
|
|
551
|
+
else if (existsSync(stockFeishu)) {
|
|
552
|
+
configToWrite.plugins.entries.feishu = { enabled: false };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (existsSync(configPath)) {
|
|
556
|
+
try {
|
|
557
|
+
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
558
|
+
if (existing.plugins?.installs) {
|
|
559
|
+
configToWrite.plugins ??= {};
|
|
560
|
+
configToWrite.plugins.installs = { ...existing.plugins.installs, ...configToWrite.plugins?.installs };
|
|
561
|
+
}
|
|
562
|
+
if (existing.plugins?.entries && configToWrite.plugins?.entries) {
|
|
563
|
+
for (const [key, val] of Object.entries(configToWrite.plugins.entries)) {
|
|
564
|
+
const old = existing.plugins.entries[key];
|
|
565
|
+
if (val && typeof val === "object" && !Array.isArray(val) && old && typeof old === "object") {
|
|
566
|
+
configToWrite.plugins.entries[key] = { ...old, ...val };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (existing.channels && configToWrite.channels) {
|
|
571
|
+
for (const [key, val] of Object.entries(configToWrite.channels)) {
|
|
572
|
+
const old = existing.channels[key];
|
|
573
|
+
if (val && typeof val === "object" && !Array.isArray(val) && old && typeof old === "object") {
|
|
574
|
+
configToWrite.channels[key] = { ...old, ...val };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch { /* best effort */ }
|
|
580
|
+
}
|
|
581
|
+
if (existsSync(configPath))
|
|
582
|
+
copyFileSync(configPath, configPath + ".bak");
|
|
583
|
+
const configJson = JSON.stringify(configToWrite, null, 2);
|
|
584
|
+
ensureDirContainer(dirname(configPath));
|
|
585
|
+
writeConfigFile(configPath + ".tmp", configJson);
|
|
586
|
+
JSON.parse(readFileSync(configPath + ".tmp", "utf-8"));
|
|
587
|
+
renameSync(configPath + ".tmp", configPath);
|
|
588
|
+
chownToServiceUser(configPath);
|
|
589
|
+
const legacyPath = legacyOpenclawConfigPath(instanceId);
|
|
590
|
+
if (existsSync(legacyPath))
|
|
591
|
+
copyFileSync(legacyPath, legacyPath + ".bak");
|
|
592
|
+
writeConfigFile(legacyPath + ".tmp", configJson);
|
|
593
|
+
JSON.parse(readFileSync(legacyPath + ".tmp", "utf-8"));
|
|
594
|
+
renameSync(legacyPath + ".tmp", legacyPath);
|
|
595
|
+
chownToServiceUser(legacyPath);
|
|
596
|
+
if (Object.keys(envUpdates).length) {
|
|
597
|
+
const envFiles = getRuntimeEnvFiles(instanceId);
|
|
598
|
+
if (envFiles.length)
|
|
599
|
+
updateEnvFile(envFiles[0], envUpdates);
|
|
600
|
+
}
|
|
601
|
+
notifyConfigChange(instanceId);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
// ── Credential management ────────────────────────────────────────────────────
|
|
605
|
+
const FEISHU_APP_ID_RE = /^cli_[a-zA-Z0-9]{8,64}$/;
|
|
606
|
+
export function saveFeishuCredentials(instanceId, creds) {
|
|
607
|
+
if (!FEISHU_APP_ID_RE.test(creds.appId)) {
|
|
608
|
+
throw new Error(`Invalid Feishu appId format: expected cli_<alnum> (got "${creds.appId}")`);
|
|
609
|
+
}
|
|
610
|
+
if (!creds.appSecret || typeof creds.appSecret !== "string" || creds.appSecret.length < 4) {
|
|
611
|
+
throw new Error("Invalid Feishu appSecret: must be a non-empty string");
|
|
612
|
+
}
|
|
613
|
+
const configPath = openclawConfigPathInternal(instanceId);
|
|
614
|
+
const config = safeReadJson(configPath, "feishu-creds") || {};
|
|
615
|
+
config.plugins ??= {};
|
|
616
|
+
config.plugins.entries ??= {};
|
|
617
|
+
config.plugins.entries.feishu = { enabled: false };
|
|
618
|
+
config.plugins.entries["openclaw-lark"] = { enabled: true };
|
|
619
|
+
config.channels ??= {};
|
|
620
|
+
config.channels.feishu = {
|
|
621
|
+
...config.channels.feishu,
|
|
622
|
+
enabled: true,
|
|
623
|
+
appId: creds.appId,
|
|
624
|
+
appSecret: creds.appSecret,
|
|
625
|
+
domain: creds.domain,
|
|
626
|
+
dmPolicy: "open",
|
|
627
|
+
allowFrom: ["*"],
|
|
628
|
+
};
|
|
629
|
+
safeWriteJson(configPath, config);
|
|
630
|
+
chownToServiceUser(configPath);
|
|
631
|
+
console.log(`[openclaw] Feishu credentials saved for ${instanceId}, domain=${creds.domain}`);
|
|
632
|
+
}
|
|
633
|
+
const SAFE_ACCOUNT_ID_RE = /^[a-zA-Z0-9@._-]{1,128}$/;
|
|
634
|
+
export function saveWeixinCredentials(instanceId, creds) {
|
|
635
|
+
if (!creds.accountId || !SAFE_ACCOUNT_ID_RE.test(creds.accountId) || creds.accountId.includes("..")) {
|
|
636
|
+
throw new Error(`Invalid accountId: must be 1-128 chars of [a-zA-Z0-9@._-] without '..'`);
|
|
637
|
+
}
|
|
638
|
+
const home = getOpenclawHome(instanceId);
|
|
639
|
+
const stateDir = join(home, OPENCLAW_STATE_DIRNAME, "openclaw-weixin");
|
|
640
|
+
const accountsDir = join(stateDir, "accounts");
|
|
641
|
+
ensureDirContainer(accountsDir);
|
|
642
|
+
const credObj = {
|
|
643
|
+
token: creds.token,
|
|
644
|
+
baseUrl: creds.baseUrl,
|
|
645
|
+
userId: creds.userId,
|
|
646
|
+
savedAt: new Date().toISOString(),
|
|
647
|
+
};
|
|
648
|
+
safeWriteJson(join(accountsDir, `${creds.accountId}.json`), credObj);
|
|
649
|
+
safeWriteJson(join(accountsDir, "default.json"), credObj);
|
|
650
|
+
chownToServiceUser(join(accountsDir, `${creds.accountId}.json`), join(accountsDir, "default.json"));
|
|
651
|
+
const indexPath = join(stateDir, "accounts.json");
|
|
652
|
+
let index = [];
|
|
653
|
+
try {
|
|
654
|
+
index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
655
|
+
}
|
|
656
|
+
catch { /* start fresh */ }
|
|
657
|
+
if (!Array.isArray(index))
|
|
658
|
+
index = [];
|
|
659
|
+
if (!index.includes(creds.accountId))
|
|
660
|
+
index.push(creds.accountId);
|
|
661
|
+
safeWriteJson(indexPath, index);
|
|
662
|
+
const configPath = openclawConfigPathInternal(instanceId);
|
|
663
|
+
const config = safeReadJson(configPath, "weixin-creds") || {};
|
|
664
|
+
config.plugins ??= {};
|
|
665
|
+
config.plugins.entries ??= {};
|
|
666
|
+
config.plugins.entries["openclaw-weixin"] ??= {};
|
|
667
|
+
config.plugins.entries["openclaw-weixin"].enabled = true;
|
|
668
|
+
config.channels ??= {};
|
|
669
|
+
config.channels["openclaw-weixin"] ??= {};
|
|
670
|
+
config.channels["openclaw-weixin"].enabled = true;
|
|
671
|
+
const normalizedId = creds.accountId.replace(/[@.]/g, "-");
|
|
672
|
+
const accounts = config.channels["openclaw-weixin"].accounts ??= {};
|
|
673
|
+
accounts[creds.accountId] = { enabled: true };
|
|
674
|
+
if (normalizedId !== creds.accountId)
|
|
675
|
+
accounts[normalizedId] = { enabled: true };
|
|
676
|
+
accounts["default"] = { enabled: true };
|
|
677
|
+
if (!config.channels["openclaw-weixin"].defaultAccount) {
|
|
678
|
+
config.channels["openclaw-weixin"].defaultAccount = "default";
|
|
679
|
+
}
|
|
680
|
+
safeWriteJson(configPath, config);
|
|
681
|
+
chownToServiceUser(configPath);
|
|
682
|
+
console.log(`[openclaw] WeChat credentials saved for ${instanceId}, account=${creds.accountId}`);
|
|
683
|
+
}
|
|
684
|
+
export function getWeixinAccounts(instanceId) {
|
|
685
|
+
const home = getOpenclawHome(instanceId);
|
|
686
|
+
const stateDir = join(home, OPENCLAW_STATE_DIRNAME, "openclaw-weixin");
|
|
687
|
+
const accountsDir = join(stateDir, "accounts");
|
|
688
|
+
if (!existsSync(accountsDir))
|
|
689
|
+
return [];
|
|
690
|
+
let indexedIds = [];
|
|
691
|
+
try {
|
|
692
|
+
indexedIds = JSON.parse(readFileSync(join(stateDir, "accounts.json"), "utf-8"));
|
|
693
|
+
}
|
|
694
|
+
catch { /* fallback */ }
|
|
695
|
+
const results = [];
|
|
696
|
+
for (const f of readdirSync(accountsDir)) {
|
|
697
|
+
if (!f.endsWith(".json"))
|
|
698
|
+
continue;
|
|
699
|
+
const id = f.replace(/\.json$/, "");
|
|
700
|
+
if (indexedIds.length > 0 && !indexedIds.includes(id))
|
|
701
|
+
continue;
|
|
702
|
+
if (id === "default")
|
|
703
|
+
continue;
|
|
704
|
+
try {
|
|
705
|
+
const data = JSON.parse(readFileSync(join(accountsDir, f), "utf-8"));
|
|
706
|
+
results.push({ accountId: id, userId: data.userId, savedAt: data.savedAt });
|
|
707
|
+
}
|
|
708
|
+
catch { /* skip */ }
|
|
709
|
+
}
|
|
710
|
+
return results;
|
|
711
|
+
}
|
|
712
|
+
// ── Docker config patching ───────────────────────────────────────────────────
|
|
713
|
+
export function patchJsproxyBaseUrl(configPath) {
|
|
714
|
+
try {
|
|
715
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
716
|
+
const patched = raw.replace(/http:\/\/127\.0\.0\.1:(\d+)\/proxy/g, `http://host.docker.internal:$1/proxy`);
|
|
717
|
+
if (patched !== raw) {
|
|
718
|
+
writeConfigFile(configPath, patched);
|
|
719
|
+
console.log(`[openclaw] Patched jsproxy baseUrl in ${configPath}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch (e) {
|
|
723
|
+
console.warn(`[openclaw] Failed to patch jsproxy baseUrl in ${configPath}: ${e.message}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
export function patchDockerBridgeGatewayBind(configPath) {
|
|
727
|
+
try {
|
|
728
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
729
|
+
const parsed = JSON.parse(raw);
|
|
730
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
731
|
+
return;
|
|
732
|
+
const gatewayRaw = parsed.gateway;
|
|
733
|
+
const gateway = gatewayRaw && typeof gatewayRaw === "object" && !Array.isArray(gatewayRaw)
|
|
734
|
+
? gatewayRaw
|
|
735
|
+
: (parsed.gateway = {});
|
|
736
|
+
const bind = typeof gateway.bind === "string" ? gateway.bind.trim() : "";
|
|
737
|
+
if (bind && bind !== "loopback")
|
|
738
|
+
return;
|
|
739
|
+
gateway.bind = "lan";
|
|
740
|
+
const next = JSON.stringify(parsed, null, 2);
|
|
741
|
+
const output = raw.endsWith("\n") ? `${next}\n` : next;
|
|
742
|
+
if (output === raw)
|
|
743
|
+
return;
|
|
744
|
+
writeConfigFile(configPath, output);
|
|
745
|
+
console.log(`[openclaw] Normalized gateway.bind to "lan" in ${configPath}`);
|
|
746
|
+
}
|
|
747
|
+
catch (e) {
|
|
748
|
+
console.warn(`[openclaw] Failed to patch gateway.bind in ${configPath}: ${e.message}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// ── prepareStart ─────────────────────────────────────────────────────────────
|
|
752
|
+
export async function prepareStartOpenClaw(instanceId) {
|
|
753
|
+
const { getNomadDriver, getOpenclawDockerImage } = await import("../../config.js");
|
|
754
|
+
const { DOCKER_IMAGE_RE, MAX_DOCKER_IMAGE_NAME_LEN } = await import("../nomad-manager.js");
|
|
755
|
+
const configPath = getOpenclawConfigPath(instanceId);
|
|
756
|
+
if (!existsSync(configPath)) {
|
|
757
|
+
return { ok: false, error: "Config file not found" };
|
|
758
|
+
}
|
|
759
|
+
if (getNomadDriver() === "docker") {
|
|
760
|
+
const stateDir = dirname(configPath);
|
|
761
|
+
ensureDirContainer(stateDir);
|
|
762
|
+
try {
|
|
763
|
+
for (const entry of readdirSync(stateDir, { withFileTypes: true })) {
|
|
764
|
+
if (entry.isDirectory()) {
|
|
765
|
+
const sub = join(stateDir, entry.name);
|
|
766
|
+
ensureDirContainer(sub);
|
|
767
|
+
try {
|
|
768
|
+
for (const child of readdirSync(sub, { withFileTypes: true })) {
|
|
769
|
+
if (child.isDirectory())
|
|
770
|
+
ensureDirContainer(join(sub, child.name));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
catch { /* ignore */ }
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch { /* ignore */ }
|
|
778
|
+
if (existsSync(configPath))
|
|
779
|
+
chmodSync(configPath, 0o644);
|
|
780
|
+
patchDockerBridgeGatewayBind(configPath);
|
|
781
|
+
patchJsproxyBaseUrl(configPath);
|
|
782
|
+
const image = getOpenclawDockerImage();
|
|
783
|
+
if (!DOCKER_IMAGE_RE.test(image) || image.length > MAX_DOCKER_IMAGE_NAME_LEN) {
|
|
784
|
+
return { ok: false, error: `Invalid Docker image name: "${image}"` };
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
execFileSync("docker", ["image", "inspect", image], { timeout: 10000, stdio: "ignore" });
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
console.log(`[openclaw] Docker image ${image} not found, starting background pull...`);
|
|
791
|
+
try {
|
|
792
|
+
const setupManager = await import("../setup-manager.js");
|
|
793
|
+
const result = setupManager.startBuildSlimOpenclawImage(image);
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
error: `Docker image ${image} not found. Pull started in background.`,
|
|
797
|
+
building: true,
|
|
798
|
+
taskId: result.taskId,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
catch (e) {
|
|
802
|
+
return { ok: false, error: `Docker image ${image} not available: ${e.message}` };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return { ok: true };
|
|
807
|
+
}
|
|
808
|
+
// ── createOpenclawInstance ────────────────────────────────────────────────────
|
|
809
|
+
export async function createOpenclawInstance(instanceId, name, description, options) {
|
|
810
|
+
const { appSpec, cloneFrom, openclawHome: customHome, cloneOptions } = options;
|
|
811
|
+
const d = getInstanceDir(instanceId);
|
|
812
|
+
if (existsSync(d))
|
|
813
|
+
throw new Error(`Instance '${instanceId}' already exists`);
|
|
814
|
+
const home = customHome ? normalizePath(customHome) : defaultOpenclawHome(instanceId);
|
|
815
|
+
if (customHome) {
|
|
816
|
+
const resolved = resolve(home);
|
|
817
|
+
if (!resolved.startsWith(JISHUSHELL_HOME) && !resolved.startsWith("/home/")) {
|
|
818
|
+
throw new Error(`openclaw_home must be under ${JISHUSHELL_HOME} or /home/`);
|
|
819
|
+
}
|
|
820
|
+
const parentDir = dirname(resolved);
|
|
821
|
+
if (existsSync(parentDir)) {
|
|
822
|
+
const realParent = realpathSync(parentDir);
|
|
823
|
+
if (!realParent.startsWith(JISHUSHELL_HOME) && !realParent.startsWith("/home/")) {
|
|
824
|
+
throw new Error(`openclaw_home parent resolves outside allowed paths (symlink detected)`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const shared = listInstances().filter((inst) => normalizePath(inst.openclaw_home || defaultOpenclawHome(inst.id)) === normalizePath(home));
|
|
828
|
+
if (shared.length) {
|
|
829
|
+
throw new Error(`OpenClaw home '${home}' is already used by instance(s): ${shared.map((i) => i.id).join(", ")}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (existsSync(home)) {
|
|
833
|
+
try {
|
|
834
|
+
const entries = readdirSync(home);
|
|
835
|
+
if (entries.length > 0) {
|
|
836
|
+
throw new Error(`OpenClaw home directory '${home}' already exists and is not empty. Remove it manually or choose a different path.`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch (e) {
|
|
840
|
+
if (e.message.includes("not empty"))
|
|
841
|
+
throw e;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
ensureDirContainer(d);
|
|
845
|
+
try {
|
|
846
|
+
const parentGid = statSync(INSTANCES_DIR).gid;
|
|
847
|
+
chownSync(d, -1, parentGid);
|
|
848
|
+
}
|
|
849
|
+
catch { /* non-root */ }
|
|
850
|
+
ensureDirContainer(home);
|
|
851
|
+
ensureDirContainer(join(home, OPENCLAW_STATE_DIRNAME));
|
|
852
|
+
const { runtime: baseRuntime, allocatedPort } = await buildDefaultRuntime(instanceId, home);
|
|
853
|
+
let runtime = baseRuntime;
|
|
854
|
+
if (appSpec) {
|
|
855
|
+
const serviceTask = appSpec.tasks.find((t) => t.role === "service");
|
|
856
|
+
if (serviceTask) {
|
|
857
|
+
const compiled = compileTaskRuntime(serviceTask, instanceId);
|
|
858
|
+
runtime = { ...baseRuntime, ...compiled };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const meta = {
|
|
863
|
+
id: instanceId,
|
|
864
|
+
name,
|
|
865
|
+
description,
|
|
866
|
+
openclaw_home: home,
|
|
867
|
+
runtime,
|
|
868
|
+
created_at: new Date().toISOString(),
|
|
869
|
+
...(appSpec ? { app_id: appSpec.app_id ?? appSpec.id } : {}),
|
|
870
|
+
};
|
|
871
|
+
safeWriteJson(instanceMetaPath(instanceId), meta);
|
|
872
|
+
const envFiles = (runtime.env_files || []).map((p) => normalizePath(p));
|
|
873
|
+
for (const ef of envFiles) {
|
|
874
|
+
if (!existsSync(ef))
|
|
875
|
+
writeConfigFile(ef, "");
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
const runtimeUser = runtime.user;
|
|
879
|
+
if (runtimeUser && runtimeUser !== userInfo().username) {
|
|
880
|
+
for (const ef of envFiles) {
|
|
881
|
+
execFileSync("chown", [runtimeUser, ef], { timeout: 5000 });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
catch { /* ignore */ }
|
|
886
|
+
const configPath = openclawConfigPathInternal(instanceId);
|
|
887
|
+
ensureDirContainer(dirname(configPath));
|
|
888
|
+
if (cloneFrom && !existsSync(configPath)) {
|
|
889
|
+
const srcConfig = resolveExistingConfigPath(cloneFrom);
|
|
890
|
+
if (existsSync(srcConfig)) {
|
|
891
|
+
try {
|
|
892
|
+
const cloned = JSON.parse(readFileSync(srcConfig, "utf-8"));
|
|
893
|
+
const providers = cloned?.models?.providers;
|
|
894
|
+
if (providers) {
|
|
895
|
+
for (const [pid, prov] of Object.entries(providers)) {
|
|
896
|
+
if (typeof prov?.baseUrl === "string" && prov.baseUrl.includes("/proxy/")) {
|
|
897
|
+
delete providers[pid];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const dm = cloned?.agents?.defaults?.model;
|
|
902
|
+
if (typeof dm === "string" && (dm.startsWith("jsproxy/") || dm.startsWith("js-"))) {
|
|
903
|
+
delete cloned.agents.defaults.model;
|
|
904
|
+
}
|
|
905
|
+
stripImBindings(cloned);
|
|
906
|
+
const subdirs = ["extensions", "workspace"];
|
|
907
|
+
if (cloneOptions?.include_memory !== false) {
|
|
908
|
+
const memDir = join(dirname(srcConfig), "memory");
|
|
909
|
+
if (existsSync(memDir))
|
|
910
|
+
subdirs.push("memory");
|
|
911
|
+
}
|
|
912
|
+
if (cloneOptions?.include_sessions) {
|
|
913
|
+
const sessDir = join(dirname(srcConfig), "agents");
|
|
914
|
+
if (existsSync(sessDir))
|
|
915
|
+
subdirs.push("agents");
|
|
916
|
+
}
|
|
917
|
+
for (const subdir of subdirs) {
|
|
918
|
+
const srcDir = join(dirname(srcConfig), subdir);
|
|
919
|
+
const dstDir = join(dirname(configPath), subdir);
|
|
920
|
+
if (existsSync(srcDir) && !existsSync(dstDir)) {
|
|
921
|
+
try {
|
|
922
|
+
cpSync(srcDir, dstDir, { recursive: true });
|
|
923
|
+
}
|
|
924
|
+
catch { /* best effort */ }
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
writeConfigFile(configPath, JSON.stringify(cloned, null, 2));
|
|
928
|
+
const srcMetaPath = join(getInstanceDir(cloneFrom), "instance.json");
|
|
929
|
+
if (existsSync(srcMetaPath)) {
|
|
930
|
+
try {
|
|
931
|
+
const srcMeta = JSON.parse(readFileSync(srcMetaPath, "utf-8"));
|
|
932
|
+
const srcXj = srcMeta?.["x-jishushell"];
|
|
933
|
+
if (srcXj?.proxy?.upstream) {
|
|
934
|
+
const dstXj = { proxy: { upstream: { ...srcXj.proxy.upstream } } };
|
|
935
|
+
delete dstXj.proxy.upstream.apiKey;
|
|
936
|
+
const metaPathDst = instanceMetaPath(instanceId);
|
|
937
|
+
if (existsSync(metaPathDst)) {
|
|
938
|
+
const dstMeta = JSON.parse(readFileSync(metaPathDst, "utf-8"));
|
|
939
|
+
dstMeta["x-jishushell"] = dstXj;
|
|
940
|
+
writeConfigFile(metaPathDst, JSON.stringify(dstMeta, null, 2));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
catch { /* ignore */ }
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
copyFileSync(srcConfig, configPath);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!existsSync(configPath)) {
|
|
953
|
+
writeConfigFile(configPath, JSON.stringify(buildStarterConfig(), null, 2));
|
|
954
|
+
const dp = getPanelConfig().default_provider;
|
|
955
|
+
if (dp?.apiKey && dp?.providerId && envFiles.length) {
|
|
956
|
+
const envKey = inferProviderApiKeyEnvName(dp.providerId);
|
|
957
|
+
updateEnvFile(envFiles[0], { [envKey]: dp.apiKey });
|
|
958
|
+
const providerEnv = join(dirname(envFiles[0]), "provider.env");
|
|
959
|
+
updateEnvFile(providerEnv, { UPSTREAM_API_KEY: dp.apiKey });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (appSpec?.openclaw?.config_defaults && existsSync(configPath)) {
|
|
963
|
+
try {
|
|
964
|
+
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
965
|
+
const defaults = appSpec.openclaw.config_defaults;
|
|
966
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
967
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof existing[key] === "object" && existing[key] !== null) {
|
|
968
|
+
existing[key] = { ...existing[key], ...value };
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
existing[key] = value;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
writeConfigFile(configPath, JSON.stringify(existing, null, 2));
|
|
975
|
+
}
|
|
976
|
+
catch { /* ignore */ }
|
|
977
|
+
}
|
|
978
|
+
if (appSpec?.openclaw?.skills && Array.isArray(appSpec.openclaw.skills)) {
|
|
979
|
+
try {
|
|
980
|
+
const skillsDir = join(dirname(configPath), "skills");
|
|
981
|
+
ensureDirContainer(skillsDir);
|
|
982
|
+
const skillMeta = join(skillsDir, ".app-skills.json");
|
|
983
|
+
safeWriteJson(skillMeta, { app_id: appSpec.app_id ?? appSpec.id, skills: appSpec.openclaw.skills });
|
|
984
|
+
}
|
|
985
|
+
catch { /* ignore */ }
|
|
986
|
+
}
|
|
987
|
+
if (cloneFrom && envFiles.length) {
|
|
988
|
+
const srcEnvFiles = getRuntimeEnvFiles(cloneFrom);
|
|
989
|
+
const srcEnvFile = srcEnvFiles[0];
|
|
990
|
+
const dstEnvFile = envFiles[0];
|
|
991
|
+
if (srcEnvFile) {
|
|
992
|
+
const srcProvider = join(dirname(srcEnvFile), "provider.env");
|
|
993
|
+
const dstProvider = join(dirname(dstEnvFile), "provider.env");
|
|
994
|
+
if (existsSync(srcProvider) && !existsSync(dstProvider)) {
|
|
995
|
+
copyFileSync(srcProvider, dstProvider);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
const { bootstrapInstanceProxy } = await import("../llm-proxy/index.js");
|
|
1001
|
+
await bootstrapInstanceProxy(instanceId);
|
|
1002
|
+
}
|
|
1003
|
+
catch (e) {
|
|
1004
|
+
console.warn(`[openclaw] Proxy bootstrap for ${instanceId} deferred: ${e.message}`);
|
|
1005
|
+
}
|
|
1006
|
+
const svcUser = resolveServiceUser();
|
|
1007
|
+
if (svcUser) {
|
|
1008
|
+
try {
|
|
1009
|
+
execFileSync("chown", ["-R", `${svcUser.uid}:${svcUser.gid}`, d], { timeout: 10_000 });
|
|
1010
|
+
if (!home.startsWith(d + "/") && existsSync(home)) {
|
|
1011
|
+
execFileSync("chown", ["-R", `${svcUser.uid}:${svcUser.gid}`, home], { timeout: 10_000 });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch (e) {
|
|
1015
|
+
console.warn(`[openclaw] chown for ${instanceId} failed:`, e.message);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return meta;
|
|
1019
|
+
}
|
|
1020
|
+
finally {
|
|
1021
|
+
releasePort(allocatedPort);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
import { getOpenclawDockerImage, } from "../../config.js";
|
|
1025
|
+
import { DEFAULT_PIDS_LIMIT, DEFAULT_RESOURCES, DEFAULT_ARGS, DEFAULT_USER, DEFAULT_CWD, DEFAULT_ENV, MAX_CPU_MHZ, MAX_MEMORY_MB, VALID_USER_RE, resolveUidGid, normalizeDockerResources, nomadGet, nomadPut, jobId, } from "../nomad-manager.js";
|
|
1026
|
+
function buildOpenclawRuntime(instanceId) {
|
|
1027
|
+
const runtime = getInstanceRuntime(instanceId);
|
|
1028
|
+
const openclawHome = getOpenclawHome(instanceId);
|
|
1029
|
+
if (runtime.user && !VALID_USER_RE.test(runtime.user)) {
|
|
1030
|
+
throw new Error(`Invalid runtime user: ${runtime.user}`);
|
|
1031
|
+
}
|
|
1032
|
+
const command = runtime.command || "/usr/bin/openclaw";
|
|
1033
|
+
let args = runtime.args;
|
|
1034
|
+
if (!Array.isArray(args))
|
|
1035
|
+
args = [...DEFAULT_ARGS];
|
|
1036
|
+
else
|
|
1037
|
+
args = args.map(String);
|
|
1038
|
+
const env = { ...DEFAULT_ENV };
|
|
1039
|
+
Object.assign(env, getRuntimeEnv(instanceId));
|
|
1040
|
+
delete env.JSPROXY_API_KEY;
|
|
1041
|
+
env.OPENCLAW_HOME = openclawHome;
|
|
1042
|
+
env.OPENCLAW_INSTANCE_ID = instanceId;
|
|
1043
|
+
const resources = { ...DEFAULT_RESOURCES };
|
|
1044
|
+
for (const [key, value] of Object.entries(runtime.resources || {})) {
|
|
1045
|
+
if (value != null)
|
|
1046
|
+
resources[key] = Number(value);
|
|
1047
|
+
}
|
|
1048
|
+
resources.CPU = Math.max(1, Math.min(resources.CPU, MAX_CPU_MHZ));
|
|
1049
|
+
resources.MemoryMB = Math.max(1, Math.min(resources.MemoryMB, MAX_MEMORY_MB));
|
|
1050
|
+
return {
|
|
1051
|
+
command: String(command),
|
|
1052
|
+
args,
|
|
1053
|
+
user: runtime.user || DEFAULT_USER,
|
|
1054
|
+
cwd: runtime.cwd || DEFAULT_CWD,
|
|
1055
|
+
env,
|
|
1056
|
+
resources,
|
|
1057
|
+
image: runtime.image ?? null,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
export function buildNomadTaskOpenClaw(instanceId, runtime, safeJobId) {
|
|
1061
|
+
const openclawHome = getOpenclawHome(instanceId);
|
|
1062
|
+
const image = runtime.image || getOpenclawDockerImage();
|
|
1063
|
+
const volumes = [`${openclawHome}:${openclawHome}:rw`];
|
|
1064
|
+
const containerEnv = { ...runtime.env };
|
|
1065
|
+
containerEnv.HOME = openclawHome;
|
|
1066
|
+
if (!containerEnv.OPENCLAW_STATE_DIR) {
|
|
1067
|
+
containerEnv.OPENCLAW_STATE_DIR = `${openclawHome}/.openclaw`;
|
|
1068
|
+
}
|
|
1069
|
+
containerEnv.npm_config_prefix = `${openclawHome}/.npm-global`;
|
|
1070
|
+
containerEnv.PIP_USER = "1";
|
|
1071
|
+
containerEnv.PYTHONUSERBASE = `${openclawHome}/.local`;
|
|
1072
|
+
containerEnv.NODE_ENV = "production";
|
|
1073
|
+
containerEnv.NODE_PATH = [
|
|
1074
|
+
`${openclawHome}/.npm-global/lib/node_modules`,
|
|
1075
|
+
"/app/node_modules",
|
|
1076
|
+
].join(":");
|
|
1077
|
+
containerEnv.PATH = [
|
|
1078
|
+
`${openclawHome}/.npm-global/bin`,
|
|
1079
|
+
`${openclawHome}/.local/bin`,
|
|
1080
|
+
`${openclawHome}/go/bin`,
|
|
1081
|
+
`${openclawHome}/.cargo/bin`,
|
|
1082
|
+
"/usr/local/sbin",
|
|
1083
|
+
"/usr/local/bin",
|
|
1084
|
+
"/usr/sbin",
|
|
1085
|
+
"/usr/bin",
|
|
1086
|
+
"/sbin",
|
|
1087
|
+
"/bin",
|
|
1088
|
+
].join(":");
|
|
1089
|
+
const runtimeArgs = [...(runtime.args || [])];
|
|
1090
|
+
const gatewayPort = getGatewayPort(instanceId);
|
|
1091
|
+
const normalizedResources = normalizeDockerResources(instanceId, runtime);
|
|
1092
|
+
return {
|
|
1093
|
+
Name: "gateway",
|
|
1094
|
+
Driver: "docker",
|
|
1095
|
+
User: resolveUidGid(runtime.user),
|
|
1096
|
+
Config: {
|
|
1097
|
+
image,
|
|
1098
|
+
force_pull: false,
|
|
1099
|
+
args: runtimeArgs,
|
|
1100
|
+
work_dir: openclawHome,
|
|
1101
|
+
volumes,
|
|
1102
|
+
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
1103
|
+
cap_drop: ["ALL"],
|
|
1104
|
+
security_opt: ["no-new-privileges"],
|
|
1105
|
+
pids_limit: DEFAULT_PIDS_LIMIT,
|
|
1106
|
+
readonly_rootfs: true,
|
|
1107
|
+
mounts: [
|
|
1108
|
+
{ type: "tmpfs", target: "/tmp", tmpfs_options: { size: 536870912 } },
|
|
1109
|
+
{ type: "tmpfs", target: "/var/tmp", tmpfs_options: { size: 67108864 } },
|
|
1110
|
+
{ type: "tmpfs", target: "/run", tmpfs_options: { size: 52428800 } },
|
|
1111
|
+
],
|
|
1112
|
+
},
|
|
1113
|
+
Env: containerEnv,
|
|
1114
|
+
Resources: {
|
|
1115
|
+
...normalizedResources,
|
|
1116
|
+
Networks: [{ ReservedPorts: [{ Label: "gateway", Value: gatewayPort }] }],
|
|
1117
|
+
},
|
|
1118
|
+
LogConfig: { MaxFiles: 3, MaxFileSizeMB: 10 },
|
|
1119
|
+
Templates: [{
|
|
1120
|
+
DestPath: "secrets/instance.env",
|
|
1121
|
+
Envvars: true,
|
|
1122
|
+
EmbeddedTmpl: [
|
|
1123
|
+
`{{ if nomadVarExists "nomad/jobs/${safeJobId}/openclaw/gateway" }}`,
|
|
1124
|
+
`JSPROXY_API_KEY={{ with nomadVar "nomad/jobs/${safeJobId}/openclaw/gateway" }}{{ .JSPROXY_API_KEY }}{{ end }}`,
|
|
1125
|
+
`{{ end }}`,
|
|
1126
|
+
].join("\n"),
|
|
1127
|
+
ChangeMode: "restart",
|
|
1128
|
+
}],
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
export async function writeNomadSecretsOpenClaw(instanceId) {
|
|
1132
|
+
const jid = jobId(instanceId);
|
|
1133
|
+
const ns = "default";
|
|
1134
|
+
const varPath = `nomad/jobs/${jid}/openclaw/gateway`;
|
|
1135
|
+
const encodedPath = encodeURIComponent(varPath);
|
|
1136
|
+
const env = getRuntimeEnv(instanceId);
|
|
1137
|
+
const proxyToken = env.JSPROXY_API_KEY || "";
|
|
1138
|
+
if (!proxyToken)
|
|
1139
|
+
return;
|
|
1140
|
+
const items = { JSPROXY_API_KEY: proxyToken };
|
|
1141
|
+
const MAX_ATTEMPTS = 3;
|
|
1142
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1143
|
+
let cas = 0;
|
|
1144
|
+
try {
|
|
1145
|
+
const existing = await nomadGet(`/v1/var/${encodedPath}?namespace=${ns}`);
|
|
1146
|
+
if (existing.ok) {
|
|
1147
|
+
const data = await existing.json();
|
|
1148
|
+
cas = data.ModifyIndex || 0;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
catch { /* variable may not exist yet — cas=0 creates a new one */ }
|
|
1152
|
+
const resp = await nomadPut(`/v1/var/${encodedPath}?cas=${cas}&namespace=${ns}`, {
|
|
1153
|
+
Namespace: ns,
|
|
1154
|
+
Path: varPath,
|
|
1155
|
+
Items: items,
|
|
1156
|
+
});
|
|
1157
|
+
if (resp.ok)
|
|
1158
|
+
return;
|
|
1159
|
+
const text = await resp.text();
|
|
1160
|
+
if (resp.status === 409 && attempt < MAX_ATTEMPTS - 1) {
|
|
1161
|
+
await new Promise(r => setTimeout(r, 100 * Math.pow(2, attempt)));
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
throw new Error(`Failed to write Nomad Variables for ${instanceId}` +
|
|
1165
|
+
` (attempt ${attempt + 1}/${MAX_ATTEMPTS}): HTTP ${resp.status} ${text}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// ── AppTypeManager export ──────────────────────────────────────────────────
|
|
1169
|
+
export const openclawManager = {
|
|
1170
|
+
appType: "openclaw",
|
|
1171
|
+
createInstance: createOpenclawInstance,
|
|
1172
|
+
prepareStart: prepareStartOpenClaw,
|
|
1173
|
+
writeNomadSecrets: writeNomadSecretsOpenClaw,
|
|
1174
|
+
buildNomadTask: buildNomadTaskOpenClaw,
|
|
1175
|
+
nomadTaskGroupName: () => "openclaw",
|
|
1176
|
+
buildRuntime: buildOpenclawRuntime,
|
|
1177
|
+
};
|
|
1178
|
+
//# sourceMappingURL=openclaw-manager.js.map
|