jishushell 0.4.30 → 0.5.22
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 +2 -5
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +18 -6
- package/apps/filebrowser-container.yaml +164 -0
- package/apps/ollama-binary.yaml +44 -0
- package/apps/ollama-with-hollama-binary.yaml +45 -1
- package/apps/openclaw-binary.yaml +8 -0
- package/apps/openclaw-container.yaml +9 -1
- package/apps/openclaw-with-searxng-container.yaml +4 -0
- package/apps/searxng-container.yaml +5 -4
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/doctor.js +144 -16
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/install.js +4 -4
- package/dist/install.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +87 -12
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +29 -0
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +9 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.js +358 -6
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +3 -0
- package/dist/services/agent-apps/catalog.js +40 -13
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +19 -2
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +3 -0
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +9 -0
- package/dist/services/app/app-manager.js +248 -43
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +1 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +37 -5
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +220 -8
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.js +26 -7
- package/dist/services/capability-endpoint-validator.js.map +1 -1
- package/dist/services/connection-apply.d.ts +2 -0
- package/dist/services/connection-apply.js +55 -1
- package/dist/services/connection-apply.js.map +1 -1
- package/dist/services/connection-resolver.js +1 -1
- package/dist/services/connection-resolver.js.map +1 -1
- package/dist/services/connection-transactor.d.ts +2 -0
- package/dist/services/connection-transactor.js +12 -2
- package/dist/services/connection-transactor.js.map +1 -1
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.js +90 -32
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +28 -0
- package/dist/services/llm-proxy/index.js +76 -3
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +6 -2
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/llm-proxy/validate-key.d.ts +41 -0
- package/dist/services/llm-proxy/validate-key.js +672 -0
- package/dist/services/llm-proxy/validate-key.js.map +1 -0
- package/dist/services/macos-launchd.d.ts +89 -0
- package/dist/services/macos-launchd.js +273 -0
- package/dist/services/macos-launchd.js.map +1 -0
- package/dist/services/nomad-manager.d.ts +11 -0
- package/dist/services/nomad-manager.js +343 -98
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +40 -11
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +3 -2
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.js +56 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +4 -3
- package/dist/services/runtime/adapters/hermes.js +166 -64
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +118 -0
- package/dist/services/runtime/adapters/openclaw.js +1459 -49
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/types.d.ts +31 -0
- package/dist/services/setup-manager.js +190 -68
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.js.map +1 -1
- package/dist/services/update-manager.js +32 -14
- package/dist/services/update-manager.js.map +1 -1
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +247 -35
- package/install/jishu-uninstall.sh +45 -5
- package/package.json +20 -2
- package/public/assets/ApiKeyField-CvyAOcJS.js +1 -0
- package/public/assets/Dashboard-AuJESBlJ.js +1 -0
- package/public/assets/{HermesChatPanel-_GHoklgo.js → HermesChatPanel-CByPREwb.js} +1 -1
- package/public/assets/HermesConfigForm-DRda8FKX.js +4 -0
- package/public/assets/InitPassword-ka4wNpM5.js +1 -0
- package/public/assets/InstanceDetail-Cg1nS8HX.js +92 -0
- package/public/assets/Login-aPajuQzf.js +1 -0
- package/public/assets/NewInstance-Dd1ebNIx.js +1 -0
- package/public/assets/ProviderRecommendations-DFmADQ7V.js +1 -0
- package/public/assets/Settings-BYQnbLYL.js +1 -0
- package/public/assets/Setup-D05lwDOV.js +1 -0
- package/public/assets/WeixinLoginPanel-D89kdhP4.js +9 -0
- package/public/assets/index-HSXCsceK.css +1 -0
- package/public/assets/index-bnBu0nlQ.js +19 -0
- package/public/assets/registry-C_qeFTkZ.js +2 -0
- package/public/assets/usePolling-Bn93fe7M.js +1 -0
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-flxcMVeP.js} +2 -2
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-ZC5T_huj.js} +7 -7
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +18 -4
- package/scripts/check-colima-launchd.mjs +230 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-rkWp-CXd.js +0 -1
- package/public/assets/HermesConfigForm-anDnwUp_.js +0 -4
- package/public/assets/InitPassword-ZU9_-hDr.js +0 -1
- package/public/assets/InstanceDetail-CN0FH1aw.js +0 -92
- package/public/assets/Login-BItXqYAJ.js +0 -1
- package/public/assets/NewInstance-BousE6kY.js +0 -1
- package/public/assets/ProviderRecommendations-DFYj7Fb6.js +0 -1
- package/public/assets/Settings-Bttc6QmM.js +0 -1
- package/public/assets/Setup-Bsxx1zgj.js +0 -1
- package/public/assets/WeixinLoginPanel-DPZpAKgO.js +0 -9
- package/public/assets/index-8xZy1z5k.css +0 -1
- package/public/assets/index-Dw3HhUYE.js +0 -19
- package/public/assets/input-paste-CrNVAyOy.js +0 -1
- package/public/assets/providers-DtNXh9JD.js +0 -1
- package/public/assets/registry-5s2UB6is.js +0 -2
- package/public/assets/usePolling-Do5Erqm_.js +0 -1
|
@@ -14,7 +14,35 @@ import { createTask, emitTask, getRunningTasks, getTask } from "../task-registry
|
|
|
14
14
|
import { compileTaskRuntime } from "./app-compiler.js";
|
|
15
15
|
import * as capabilityRegistry from "../capability-registry.js";
|
|
16
16
|
import { resolveProvideEndpoint } from "./provide-resolver.js";
|
|
17
|
-
|
|
17
|
+
import { platformTransformSpec } from "./platform-transform.js";
|
|
18
|
+
const DEFAULT_LIFECYCLE_PATH_ENTRIES = [
|
|
19
|
+
"/opt/homebrew/bin",
|
|
20
|
+
"/opt/homebrew/sbin",
|
|
21
|
+
"/usr/local/bin",
|
|
22
|
+
"/usr/local/sbin",
|
|
23
|
+
"/usr/bin",
|
|
24
|
+
"/bin",
|
|
25
|
+
"/usr/sbin",
|
|
26
|
+
"/sbin",
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_LIFECYCLE_PATH = DEFAULT_LIFECYCLE_PATH_ENTRIES.join(":");
|
|
29
|
+
const MACOS_LIFECYCLE_PATH_PROBES = [
|
|
30
|
+
"/Applications/Docker.app/Contents/Resources/bin",
|
|
31
|
+
];
|
|
32
|
+
const ANONYMOUS_DOWNLOAD_IMAGE_ALLOWLIST = new Set([
|
|
33
|
+
"filebrowser/filebrowser:latest",
|
|
34
|
+
"ghcr.io/browserless/chromium:latest",
|
|
35
|
+
"ghcr.io/fmaclen/hollama:latest",
|
|
36
|
+
"ghcr.io/open-webui/open-webui:main",
|
|
37
|
+
"mcr.microsoft.com/playwright:v1.55.0-noble",
|
|
38
|
+
"mintplexlabs/anythingllm:latest",
|
|
39
|
+
"paradedb/paradedb:v0.22.2-pg17",
|
|
40
|
+
"redis:7.0-alpine",
|
|
41
|
+
"searxng/searxng:latest",
|
|
42
|
+
"wechatopenai/weknora-app:latest",
|
|
43
|
+
"wechatopenai/weknora-docreader:latest",
|
|
44
|
+
"wechatopenai/weknora-ui:latest",
|
|
45
|
+
]);
|
|
18
46
|
function getConfigValue(name) {
|
|
19
47
|
return name in config ? config[name] : undefined;
|
|
20
48
|
}
|
|
@@ -81,7 +109,6 @@ function parseBuiltinTemplate(fileName, yamlText) {
|
|
|
81
109
|
const runtime = typeof task?.runtime === "string" ? task.runtime.trim() : "";
|
|
82
110
|
return runtime === "container" || runtime === "process";
|
|
83
111
|
})
|
|
84
|
-
&& (tasks.length === 1 || isOllamaTemplate)
|
|
85
112
|
&& (serviceRuntime === "container" || serviceRuntime === "process"),
|
|
86
113
|
suggestedAppType,
|
|
87
114
|
yaml: yamlText,
|
|
@@ -169,15 +196,44 @@ function expandPath(p) {
|
|
|
169
196
|
}
|
|
170
197
|
return p.replace(/^~(?=\/|$)/, process.env.HOME ?? homedir());
|
|
171
198
|
}
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
.split(":")
|
|
199
|
+
function buildDeterministicPath(basePath, extraPaths = []) {
|
|
200
|
+
return [basePath ?? "", DEFAULT_LIFECYCLE_PATH, dirname(process.execPath), ...extraPaths]
|
|
201
|
+
.flatMap((entry) => entry.split(":"))
|
|
175
202
|
.map((entry) => entry.trim())
|
|
176
|
-
.filter(Boolean)
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.filter((entry, index, entries) => entries.indexOf(entry) === index)
|
|
205
|
+
.join(":");
|
|
206
|
+
}
|
|
207
|
+
function buildLifecycleEnv() {
|
|
208
|
+
const extraPaths = process.platform === "darwin"
|
|
209
|
+
? MACOS_LIFECYCLE_PATH_PROBES.filter((entry) => existsSync(entry))
|
|
210
|
+
: [];
|
|
211
|
+
const mergedPath = buildDeterministicPath(process.env.PATH, extraPaths);
|
|
212
|
+
// Surface panel callback hooks to lifecycle scripts. post_start can curl
|
|
213
|
+
// ${JISHUSHELL_PANEL_URL}/api/internal/* with the internal token to read
|
|
214
|
+
// panel-managed state (default provider creds etc.) and self-configure
|
|
215
|
+
// without going through user JWT. Best-effort: if the token file or
|
|
216
|
+
// port lookup fails we just omit the vars and the script gets a clean
|
|
217
|
+
// "missing env" failure path.
|
|
218
|
+
const panelHooks = {};
|
|
219
|
+
try {
|
|
220
|
+
panelHooks.JISHUSHELL_PANEL_URL = `http://127.0.0.1:${config.getPanelPort()}`;
|
|
221
|
+
panelHooks.JISHUSHELL_INTERNAL_TOKEN = config.getInternalMcpToken();
|
|
222
|
+
// LAN host that *other* services (and the post_start script's curls into
|
|
223
|
+
// its own service) can reach. AnythingLLM binds eth0 via Nomad's
|
|
224
|
+
// `external` host_network, so the panel-host loopback (127.0.0.1) won't
|
|
225
|
+
// reach 18097 — post_start needs to hit the LAN IP to check its own
|
|
226
|
+
// health. getPanelLanHost() returns the same IP Nomad publishes ports on.
|
|
227
|
+
panelHooks.JISHUSHELL_LAN_HOST = config.getPanelLanHost();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// tolerate — only post_start cares
|
|
231
|
+
}
|
|
177
232
|
return {
|
|
178
233
|
...process.env,
|
|
179
234
|
HOME: process.env.HOME ?? homedir(),
|
|
180
|
-
PATH:
|
|
235
|
+
PATH: mergedPath,
|
|
236
|
+
...panelHooks,
|
|
181
237
|
};
|
|
182
238
|
}
|
|
183
239
|
const SUDO_PASSTHROUGH_ENV_KEYS = ["HOME", "PATH", "TMPDIR", "TMP", "TEMP", "XDG_RUNTIME_DIR"];
|
|
@@ -325,7 +381,7 @@ export async function validateSudoPassword(sudoPassword) {
|
|
|
325
381
|
}
|
|
326
382
|
resolve();
|
|
327
383
|
});
|
|
328
|
-
child.on("error", (
|
|
384
|
+
child.on("error", (_error) => {
|
|
329
385
|
resolve();
|
|
330
386
|
});
|
|
331
387
|
});
|
|
@@ -334,8 +390,11 @@ export async function validateSudoPassword(sudoPassword) {
|
|
|
334
390
|
preparedEnv.cleanup();
|
|
335
391
|
}
|
|
336
392
|
}
|
|
337
|
-
function prepareLifecycleExecEnv(execOptions) {
|
|
338
|
-
const env =
|
|
393
|
+
function prepareLifecycleExecEnv(execOptions, envOverrides) {
|
|
394
|
+
const env = {
|
|
395
|
+
...buildLifecycleEnv(),
|
|
396
|
+
...(envOverrides ?? {}),
|
|
397
|
+
};
|
|
339
398
|
const sudoPassword = execOptions?.sudoPassword;
|
|
340
399
|
if (!sudoPassword) {
|
|
341
400
|
return { env, cleanup: () => undefined };
|
|
@@ -364,6 +423,26 @@ function prepareLifecycleExecEnv(execOptions) {
|
|
|
364
423
|
},
|
|
365
424
|
};
|
|
366
425
|
}
|
|
426
|
+
function shouldBypassDockerCredentialHelperForDownloadImage(image) {
|
|
427
|
+
return ANONYMOUS_DOWNLOAD_IMAGE_ALLOWLIST.has(image.trim());
|
|
428
|
+
}
|
|
429
|
+
function createAnonymousDockerConfig() {
|
|
430
|
+
const dockerConfigDir = mkdtempSync(join(tmpdir(), "jishushell-docker-config-"));
|
|
431
|
+
writeFileSync(join(dockerConfigDir, "config.json"), `${JSON.stringify({ auths: {} }, null, 2)}\n`, { mode: 0o600 });
|
|
432
|
+
return {
|
|
433
|
+
envOverrides: {
|
|
434
|
+
DOCKER_CONFIG: dockerConfigDir,
|
|
435
|
+
},
|
|
436
|
+
cleanup: () => {
|
|
437
|
+
try {
|
|
438
|
+
rmSync(dockerConfigDir, { recursive: true, force: true });
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// best effort cleanup for one-shot anonymous docker config files
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
367
446
|
const ANSI_ESCAPE_RE = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
368
447
|
function sanitizeTaskLine(line) {
|
|
369
448
|
return line
|
|
@@ -687,11 +766,16 @@ function materializeInstalledSpec(spec, appId, offset) {
|
|
|
687
766
|
const portShiftMap = buildPortShiftMap(spec, offset);
|
|
688
767
|
const rewritten = rewriteInstalledSpecValue(spec, spec.id, appId, portShiftMap);
|
|
689
768
|
const derivedName = deriveInstalledDisplayName(spec, appId);
|
|
690
|
-
|
|
769
|
+
const normalized = normalizeAppSpec({
|
|
691
770
|
...rewritten,
|
|
692
771
|
id: spec.id,
|
|
693
772
|
...(derivedName ? { name: derivedName } : {}),
|
|
694
773
|
});
|
|
774
|
+
// Final step: drop spec fields that work on the spec author's Linux
|
|
775
|
+
// baseline but break on the host platform (e.g. host_network:
|
|
776
|
+
// docker_bridge / user: "host" on darwin). Identity pass-through on
|
|
777
|
+
// Linux — see platform-transform.ts for the rule list.
|
|
778
|
+
return platformTransformSpec(normalized);
|
|
695
779
|
}
|
|
696
780
|
function deriveInstalledDisplayName(spec, appId) {
|
|
697
781
|
const baseName = typeof spec.name === "string" && spec.name.trim() ? spec.name.trim() : spec.id;
|
|
@@ -801,7 +885,15 @@ async function resolveInstallTarget(spec, originalSpecYaml, requestedAppId) {
|
|
|
801
885
|
return {
|
|
802
886
|
appId,
|
|
803
887
|
installedSpec,
|
|
804
|
-
|
|
888
|
+
// Always re-serialize from `installedSpec` so the cached yaml
|
|
889
|
+
// reflects every transform `materializeInstalledSpec` ran —
|
|
890
|
+
// including the platform pass that strips host_network/user fields
|
|
891
|
+
// on darwin. Using `originalSpecYaml` for offset=0 (the previous
|
|
892
|
+
// behavior) bypassed the transformer for multi-instance apps'
|
|
893
|
+
// first install slot and re-introduced the Linux-only fields on
|
|
894
|
+
// disk; subsequent panel restarts then re-read the raw source via
|
|
895
|
+
// `loadInstalledAppSpec` and broke macOS placement again.
|
|
896
|
+
installedSpecYaml: stringify(installedSpec),
|
|
805
897
|
};
|
|
806
898
|
}
|
|
807
899
|
throw new Error(`App '${baseId}' 没有可用安装槽位,目录名或端口已全部占用`);
|
|
@@ -819,37 +911,57 @@ const DOCKER_PULL_TIMEOUT_MS = 1_800_000;
|
|
|
819
911
|
// Separate from the total timeout above: if docker pull stops producing any
|
|
820
912
|
// stdout/stderr for long enough, treat it as stalled and retry rather than
|
|
821
913
|
// waiting the full 30 minutes.
|
|
822
|
-
|
|
823
|
-
|
|
914
|
+
//
|
|
915
|
+
// Why 600s (not 180s): on slow links (especially Docker Hub from China to
|
|
916
|
+
// edge devices), large single layers — AnythingLLM ~500MB, Playwright/Hermes
|
|
917
|
+
// ~2.3GB — can spend 5-8 minutes between progress lines without TTY. 180s
|
|
918
|
+
// idle was killing pulls that were actually making progress, then rolling
|
|
919
|
+
// back the partially-completed image. Layer cache is preserved across
|
|
920
|
+
// retries (docker dedupes by layer sha), so a higher idle ceiling lets each
|
|
921
|
+
// big layer finish without throwing away the layers already on disk.
|
|
922
|
+
const DOCKER_PULL_IDLE_TIMEOUT_MS = 600_000;
|
|
923
|
+
async function pullDockerImageStep(label, image, display, task, timeoutMs = DOCKER_PULL_TIMEOUT_MS, idleTimeoutMs) {
|
|
824
924
|
if (await dockerImageExists(image)) {
|
|
825
925
|
const skipMessage = `[lifecycle:${label}] docker image '${image}' already exists locally; skipping pull`;
|
|
826
926
|
process.stdout.write(` ${skipMessage}\n`);
|
|
827
927
|
emitInstallTaskLog(task, skipMessage);
|
|
828
928
|
return;
|
|
829
929
|
}
|
|
930
|
+
const anonymousDockerConfig = shouldBypassDockerCredentialHelperForDownloadImage(image)
|
|
931
|
+
? createAnonymousDockerConfig()
|
|
932
|
+
: null;
|
|
830
933
|
let lastError;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
process.stdout.write(` ${recoveredMessage}\n`);
|
|
840
|
-
emitInstallTaskLog(task, recoveredMessage);
|
|
934
|
+
try {
|
|
935
|
+
for (let attempt = 1; attempt <= DOCKER_PULL_RETRY_ATTEMPTS; attempt++) {
|
|
936
|
+
try {
|
|
937
|
+
await spawnStepWithTimeout(label, display, display, "docker", ["pull", image], timeoutMs, task, undefined, undefined, {
|
|
938
|
+
idleTimeoutMs: idleTimeoutMs ?? Math.min(DOCKER_PULL_IDLE_TIMEOUT_MS, timeoutMs),
|
|
939
|
+
envOverrides: anonymousDockerConfig?.envOverrides,
|
|
940
|
+
stallMessageHint: "Docker credential resolution (for example docker-credential-desktop) may be involved.",
|
|
941
|
+
});
|
|
841
942
|
return;
|
|
842
943
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (await dockerImageExists(image)) {
|
|
946
|
+
const recoveredMessage = `[lifecycle:${label}] docker image '${image}' is present locally after pull failure/timeout; treating step as successful`;
|
|
947
|
+
process.stdout.write(` ${recoveredMessage}\n`);
|
|
948
|
+
emitInstallTaskLog(task, recoveredMessage);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
lastError = error;
|
|
952
|
+
if (attempt === DOCKER_PULL_RETRY_ATTEMPTS) {
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
956
|
+
const retryMessage = `[lifecycle:${label}] docker pull failed for ${image} (attempt ${attempt}/${DOCKER_PULL_RETRY_ATTEMPTS}): ${reason}; retrying`;
|
|
957
|
+
process.stdout.write(` ${retryMessage}\n`);
|
|
958
|
+
emitInstallTaskLog(task, retryMessage);
|
|
846
959
|
}
|
|
847
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
848
|
-
const retryMessage = `[lifecycle:${label}] docker pull failed for ${image} (attempt ${attempt}/${DOCKER_PULL_RETRY_ATTEMPTS}): ${reason}; retrying`;
|
|
849
|
-
process.stdout.write(` ${retryMessage}\n`);
|
|
850
|
-
emitInstallTaskLog(task, retryMessage);
|
|
851
960
|
}
|
|
852
961
|
}
|
|
962
|
+
finally {
|
|
963
|
+
anonymousDockerConfig?.cleanup();
|
|
964
|
+
}
|
|
853
965
|
throw (lastError instanceof Error ? lastError : new Error(String(lastError)));
|
|
854
966
|
}
|
|
855
967
|
async function dockerImageExists(image) {
|
|
@@ -866,7 +978,7 @@ function spawnStepWithTimeout(label, display, taskDisplay, cmd, args, timeoutMs,
|
|
|
866
978
|
process.stdout.write(` [lifecycle:${label}] ${display}\n`);
|
|
867
979
|
emitInstallTaskLog(task, `[lifecycle:${label}] ${taskDisplay}`);
|
|
868
980
|
return new Promise((resolve, reject) => {
|
|
869
|
-
const preparedEnv = prepareLifecycleExecEnv(sudo ? execOptions : undefined);
|
|
981
|
+
const preparedEnv = prepareLifecycleExecEnv(sudo ? execOptions : undefined, runOptions?.envOverrides);
|
|
870
982
|
let cleaned = false;
|
|
871
983
|
let heartbeatTimer = null;
|
|
872
984
|
let idleTimer = null;
|
|
@@ -904,10 +1016,11 @@ function spawnStepWithTimeout(label, display, taskDisplay, cmd, args, timeoutMs,
|
|
|
904
1016
|
clearTimeout(idleTimer);
|
|
905
1017
|
idleTimer = setTimeout(() => {
|
|
906
1018
|
const idleSeconds = Math.max(1, Math.round(runOptions.idleTimeoutMs / 1000));
|
|
907
|
-
const
|
|
1019
|
+
const stallMessageSuffix = runOptions.stallMessageHint ? ` ${runOptions.stallMessageHint}` : "";
|
|
1020
|
+
const stallMessage = `[lifecycle:${label}] no output for ${idleSeconds}s; terminating stalled step: ${taskDisplay}${stallMessageSuffix}`;
|
|
908
1021
|
process.stdout.write(` ${stallMessage}\n`);
|
|
909
1022
|
emitInstallTaskLog(task, stallMessage);
|
|
910
|
-
forcedError = new Error(`lifecycle '${label}' step stalled after ${idleSeconds}s with no output: ${display}`);
|
|
1023
|
+
forcedError = new Error(`lifecycle '${label}' step stalled after ${idleSeconds}s with no output: ${display}${stallMessageSuffix}`);
|
|
911
1024
|
child.kill("SIGTERM");
|
|
912
1025
|
}, runOptions.idleTimeoutMs);
|
|
913
1026
|
idleTimer.unref?.();
|
|
@@ -1108,7 +1221,7 @@ async function runLifecycleSteps(steps, label, artifacts, task, execOptions) {
|
|
|
1108
1221
|
}
|
|
1109
1222
|
}
|
|
1110
1223
|
else if ("downloadImage" in step) {
|
|
1111
|
-
await pullDockerImageStep(label, step.downloadImage, `downloadImage: ${step.downloadImage}`, task, step.timeout_ms);
|
|
1224
|
+
await pullDockerImageStep(label, step.downloadImage, `downloadImage: ${step.downloadImage}`, task, step.timeout_ms, step.idle_timeout_ms);
|
|
1112
1225
|
artifacts?.push({ type: "image", path: step.downloadImage });
|
|
1113
1226
|
}
|
|
1114
1227
|
else if ("deleteImage" in step) {
|
|
@@ -1208,7 +1321,7 @@ function cleanupArtifacts(artifacts, task) {
|
|
|
1208
1321
|
}
|
|
1209
1322
|
const DOCKER_IMAGE_RE = /^[a-zA-Z0-9][a-zA-Z0-9\-_.:/@]*$/;
|
|
1210
1323
|
const APP_ID_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
1211
|
-
const
|
|
1324
|
+
const _REGISTRY_PATH = join(APPS_DIR, "capability-registry.json");
|
|
1212
1325
|
const INSTALL_LOCK_FILENAME = "install.lock";
|
|
1213
1326
|
// ── Directory helpers ─────────────────────────────────────────────────────
|
|
1214
1327
|
function appDirForId(appId) {
|
|
@@ -1723,16 +1836,66 @@ function readRegistry() {
|
|
|
1723
1836
|
const file = capabilityRegistry.readRegistry();
|
|
1724
1837
|
return { capabilities: file.capabilities ?? {}, providersByCapability: file.providersByCapability };
|
|
1725
1838
|
}
|
|
1726
|
-
function installedProvidersForCapability(capability) {
|
|
1727
|
-
return listApps()
|
|
1728
|
-
.filter((app) => app.spec.provides?.some((provide) => provide.capability === capability))
|
|
1729
|
-
.map((app) => app.manifest.id);
|
|
1730
|
-
}
|
|
1731
1839
|
// `ensureRequiredCapabilitiesAvailable` was removed in PR 3 sub-step 3c.
|
|
1732
1840
|
// install never blocks on missing required providers any more —
|
|
1733
1841
|
// resolveConnections(..., "preCreate") collects them into the `pending`
|
|
1734
1842
|
// list and the UI surfaces them after install completes.
|
|
1843
|
+
// `listProvidedCapabilities()` does a full filesystem app scan (listApps →
|
|
1844
|
+
// readdir + per-app spec parse) plus a registry read. `augmentInstanceMetadata`
|
|
1845
|
+
// calls it ~2× per instance (getProvidedCapabilitiesForApp +
|
|
1846
|
+
// getEmbeddedUiHintForApp), so `GET /api/instances` over N instances did ~2N
|
|
1847
|
+
// full disk scans per request — the dashboard warm-path cost.
|
|
1848
|
+
//
|
|
1849
|
+
// Cache the result, but key validity on a CHEAP fingerprint of the
|
|
1850
|
+
// underlying state, not on time alone. A time-only TTL left a staleness
|
|
1851
|
+
// window where a freshly installed/uninstalled app (or created/deleted
|
|
1852
|
+
// instance — INSTANCES_DIR also feeds listApps) was missing from / lingered
|
|
1853
|
+
// in the list until the TTL expired, which broke the Connections UI and the
|
|
1854
|
+
// embedded-UI hint. The fingerprint (sorted dir entries of APPS_DIR +
|
|
1855
|
+
// INSTANCES_DIR, plus the registry file mtime) changes the instant the app
|
|
1856
|
+
// set or registry changes — including external mutations — so the cache
|
|
1857
|
+
// self-busts without having to hook every mutation site. It costs two
|
|
1858
|
+
// readdir + one stat, still vastly cheaper than re-parsing every app spec.
|
|
1859
|
+
// The short TTL stays only as a safety net for in-place spec edits that
|
|
1860
|
+
// don't change the dir set; the explicit register/unregister/markStopped
|
|
1861
|
+
// invalidations remain as a precise fast-path for sub-millisecond registry
|
|
1862
|
+
// writes that could share an mtime tick.
|
|
1863
|
+
const PROVIDED_CAPS_TTL_MS = 5_000;
|
|
1864
|
+
let _providedCapsEntry = null;
|
|
1865
|
+
function providedCapabilitiesFingerprint() {
|
|
1866
|
+
const dirEntries = (dir) => {
|
|
1867
|
+
try {
|
|
1868
|
+
return existsSync(dir) ? readdirSync(dir).sort().join(",") : "";
|
|
1869
|
+
}
|
|
1870
|
+
catch {
|
|
1871
|
+
return "";
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
let registryMtime = 0;
|
|
1875
|
+
try {
|
|
1876
|
+
registryMtime = lstatSync(_REGISTRY_PATH).mtimeMs;
|
|
1877
|
+
}
|
|
1878
|
+
catch {
|
|
1879
|
+
/* registry file not written yet → 0 */
|
|
1880
|
+
}
|
|
1881
|
+
return `${dirEntries(APPS_DIR)}|${dirEntries(INSTANCES_DIR)}|${registryMtime}`;
|
|
1882
|
+
}
|
|
1883
|
+
export function invalidateProvidedCapabilitiesCache() {
|
|
1884
|
+
_providedCapsEntry = null;
|
|
1885
|
+
}
|
|
1735
1886
|
export function listProvidedCapabilities() {
|
|
1887
|
+
const fp = providedCapabilitiesFingerprint();
|
|
1888
|
+
const now = Date.now();
|
|
1889
|
+
if (_providedCapsEntry &&
|
|
1890
|
+
_providedCapsEntry.fp === fp &&
|
|
1891
|
+
now - _providedCapsEntry.ts < PROVIDED_CAPS_TTL_MS) {
|
|
1892
|
+
return _providedCapsEntry.data;
|
|
1893
|
+
}
|
|
1894
|
+
const data = computeProvidedCapabilities();
|
|
1895
|
+
_providedCapsEntry = { data, fp, ts: now };
|
|
1896
|
+
return data;
|
|
1897
|
+
}
|
|
1898
|
+
function computeProvidedCapabilities() {
|
|
1736
1899
|
const reg = readRegistry();
|
|
1737
1900
|
return listApps().flatMap((app) => (app.spec.provides ?? []).map((provide) => {
|
|
1738
1901
|
const url = getProvideUrl(provide) ?? undefined;
|
|
@@ -1753,6 +1916,7 @@ export function listProvidedCapabilities() {
|
|
|
1753
1916
|
...(provide.visibility ? { visibility: provide.visibility } : {}),
|
|
1754
1917
|
...(provide.description ? { description: provide.description } : {}),
|
|
1755
1918
|
...(provide.terminal ? { terminal: provide.terminal } : {}),
|
|
1919
|
+
...(provide.embedded ? { embedded: provide.embedded } : {}),
|
|
1756
1920
|
...(address ? { address } : {}),
|
|
1757
1921
|
registered: Boolean(registered),
|
|
1758
1922
|
...(registered?.address ? { registeredAddress: registered.address } : {}),
|
|
@@ -1763,11 +1927,38 @@ export function listProvidedCapabilities() {
|
|
|
1763
1927
|
export function getProvidedCapabilitiesForApp(appId) {
|
|
1764
1928
|
return listProvidedCapabilities().filter((entry) => entry.appId === appId);
|
|
1765
1929
|
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Resolve the runtime endpoint (host + port) for a given app's capability.
|
|
1932
|
+
* Returns the runtime-resolved port when available, falling back to the
|
|
1933
|
+
* declared AppSpec port. This is used by the capability proxy to target the
|
|
1934
|
+
* correct live endpoint after potential port reallocation.
|
|
1935
|
+
*/
|
|
1936
|
+
export function resolveRuntimeCapabilityPort(appId, capabilityName) {
|
|
1937
|
+
const app = getApp(appId);
|
|
1938
|
+
if (!app)
|
|
1939
|
+
return null;
|
|
1940
|
+
const provide = (app.spec.provides ?? []).find((p) => p.capability === capabilityName);
|
|
1941
|
+
if (!provide)
|
|
1942
|
+
return null;
|
|
1943
|
+
const resolved = resolveProvideEndpoint(appId, app.spec, provide);
|
|
1944
|
+
return resolved?.hostPort ?? getProvidePort(app.spec, provide);
|
|
1945
|
+
}
|
|
1766
1946
|
export function getEmbeddedUiHintForApp(appId) {
|
|
1767
1947
|
const provides = getProvidedCapabilitiesForApp(appId);
|
|
1768
1948
|
if (!provides.length)
|
|
1769
1949
|
return null;
|
|
1770
|
-
|
|
1950
|
+
// Selection priority for which provide becomes the embedded UI:
|
|
1951
|
+
// 1. browserless-debugger (special — dev console, not the API)
|
|
1952
|
+
// 2. any capability whose name ends in "-ui" (the canonical Web UI slot,
|
|
1953
|
+
// by convention served at "/"). Apps like AnythingLLM provide both an
|
|
1954
|
+
// API capability (`knowledge-anythingllm`, path `/api/v1`) AND a UI
|
|
1955
|
+
// capability (`anythingllm-ui`, path `/`). Without this preference
|
|
1956
|
+
// the first-iterated provide wins and the iframe ends up pointing
|
|
1957
|
+
// at `/api/v1` → 404.
|
|
1958
|
+
// 3. fall back to natural order for legacy single-capability apps.
|
|
1959
|
+
const browserlessPreferred = provides.find((provide) => provide.capability === BROWSERLESS_DEBUGGER_CAPABILITY);
|
|
1960
|
+
const uiPreferred = provides.find((provide) => provide.capability.endsWith("-ui"));
|
|
1961
|
+
const preferred = browserlessPreferred ?? uiPreferred ?? null;
|
|
1771
1962
|
const orderedProvides = preferred
|
|
1772
1963
|
? [preferred, ...provides.filter((provide) => provide !== preferred)]
|
|
1773
1964
|
: provides;
|
|
@@ -1787,6 +1978,14 @@ export function getEmbeddedUiHintForApp(appId) {
|
|
|
1787
1978
|
}
|
|
1788
1979
|
if (typeof provide.port !== "number" || provide.port < 1)
|
|
1789
1980
|
continue;
|
|
1981
|
+
// Honor explicit `embedded` opt-in/out on the provide before the
|
|
1982
|
+
// auto-detection logic runs. `"proxy"` short-circuits to the
|
|
1983
|
+
// same-origin proxy path (needed when upstream is firewall-blocked,
|
|
1984
|
+
// emits X-Frame-Options, or otherwise can't be reached by the
|
|
1985
|
+
// browser directly). `"direct"` forces the direct URL even when
|
|
1986
|
+
// listening only on loopback — caller asserts they know what
|
|
1987
|
+
// they're doing.
|
|
1988
|
+
const embeddedMode = provide.embedded ?? "auto";
|
|
1790
1989
|
// Prefer a direct upstream URL when the container port is published to
|
|
1791
1990
|
// a LAN-reachable address (Pi with host_network "external", etc.). The
|
|
1792
1991
|
// same-origin reverse-proxy path is necessary only when the container
|
|
@@ -1797,7 +1996,10 @@ export function getEmbeddedUiHintForApp(appId) {
|
|
|
1797
1996
|
// absolute URLs starting with `/api/...` (e.g. OpenWebUI), because
|
|
1798
1997
|
// those calls bypass `<base href>` and hit the panel API instead.
|
|
1799
1998
|
const listeningHost = legacyInstanceManager.getListeningHostForPort(provide.port);
|
|
1800
|
-
const
|
|
1999
|
+
const isLoopback = listeningHost === "127.0.0.1" || listeningHost === "::1";
|
|
2000
|
+
const directlyReachable = embeddedMode !== "proxy"
|
|
2001
|
+
&& listeningHost
|
|
2002
|
+
&& (!isLoopback || embeddedMode === "direct");
|
|
1801
2003
|
if (directlyReachable) {
|
|
1802
2004
|
const advertised = legacyInstanceManager.getAdvertisedHostForPort(provide.port);
|
|
1803
2005
|
const directUrl = `${protocol}://${advertised}:${provide.port}${provide.path ?? ""}`;
|
|
@@ -2162,6 +2364,7 @@ export function registerCapabilities(instanceId, spec, portOverride) {
|
|
|
2162
2364
|
};
|
|
2163
2365
|
capabilityRegistry.registerProvider(entry);
|
|
2164
2366
|
}
|
|
2367
|
+
invalidateProvidedCapabilitiesCache();
|
|
2165
2368
|
}
|
|
2166
2369
|
/**
|
|
2167
2370
|
* Mark an instance's providers as `stopped` (preferred for stop) or
|
|
@@ -2171,9 +2374,11 @@ export function registerCapabilities(instanceId, spec, portOverride) {
|
|
|
2171
2374
|
*/
|
|
2172
2375
|
export function unregisterCapabilities(instanceId) {
|
|
2173
2376
|
capabilityRegistry.unregisterProviders(instanceId);
|
|
2377
|
+
invalidateProvidedCapabilitiesCache();
|
|
2174
2378
|
}
|
|
2175
2379
|
export function markCapabilitiesStopped(instanceId) {
|
|
2176
2380
|
capabilityRegistry.setProviderStatus(instanceId, "stopped");
|
|
2381
|
+
invalidateProvidedCapabilitiesCache();
|
|
2177
2382
|
}
|
|
2178
2383
|
export function resolveRequires(spec) {
|
|
2179
2384
|
if (!spec.requires || spec.requires.length === 0)
|