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,946 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw-specific HTTP routes — physically migrated from routes/instances.ts
|
|
3
|
+
* as part of §32.2.5 full decoupling.
|
|
4
|
+
*
|
|
5
|
+
* These endpoints know about the OpenClaw workspace layout (mcporter.json,
|
|
6
|
+
* .openclaw/workspace/skills/, sessions.json), OpenClaw's Feishu/WeChat
|
|
7
|
+
* channel plugins, and the OpenClaw gateway's control-UI HTML that needs
|
|
8
|
+
* boot-script injection. None of them are generic enough for the framework
|
|
9
|
+
* to host, so they live next to the OpenClaw adapter and are registered
|
|
10
|
+
* via `RuntimeAdapter.registerRoutes()`.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
import { existsSync, realpathSync } from "fs";
|
|
14
|
+
import { readFile, stat } from "fs/promises";
|
|
15
|
+
import { request as httpRequest } from "http";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { PROXY_IDENTITY_HEADERS } from "../../../constants.js";
|
|
18
|
+
import { assertNotLocked } from "../../backup-manager.js";
|
|
19
|
+
import * as instanceManager from "../../instance-manager.js";
|
|
20
|
+
import * as pluginInstaller from "../../plugin-installer.js";
|
|
21
|
+
import { supportsGatewayChatPanel } from "../instance.js";
|
|
22
|
+
import { getAdapter, resolveAgentType } from "../index.js";
|
|
23
|
+
import { writeConfigFile, ensureDirContainer } from "../../../utils/fs.js";
|
|
24
|
+
import { HOP_BY_HOP, ensureControlUiAllowedOrigin, getSvc, inferRequestOrigin, restartRunningInstanceForControlUiOrigin, validateId, } from "../../../routes/instances.js";
|
|
25
|
+
/**
|
|
26
|
+
* Register all OpenClaw-specific HTTP endpoints on the given Fastify app.
|
|
27
|
+
* Called via `OpenClawAdapter.registerRoutes(app)` at server startup.
|
|
28
|
+
*/
|
|
29
|
+
export function registerOpenclawRoutes(app) {
|
|
30
|
+
// ── Plugin check & install — host-side, no container dependency ────
|
|
31
|
+
app.get("/api/instances/:id/plugins/check/:channelId", async (req, reply) => {
|
|
32
|
+
const idErr = validateId(req.params.id);
|
|
33
|
+
if (idErr)
|
|
34
|
+
return reply.status(400).send({ detail: idErr });
|
|
35
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
36
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
37
|
+
}
|
|
38
|
+
const inst = instanceManager.getInstance(req.params.id);
|
|
39
|
+
const agentType = resolveAgentType(inst);
|
|
40
|
+
const adapter = getAdapter(agentType);
|
|
41
|
+
const installed = typeof adapter.isChannelPluginInstalled === "function"
|
|
42
|
+
? adapter.isChannelPluginInstalled(req.params.id, req.params.channelId)
|
|
43
|
+
: false;
|
|
44
|
+
return { channelId: req.params.channelId, installed };
|
|
45
|
+
});
|
|
46
|
+
app.get("/api/instances/:id/plugins/status", async (req, reply) => {
|
|
47
|
+
const idErr = validateId(req.params.id);
|
|
48
|
+
if (idErr)
|
|
49
|
+
return reply.status(400).send({ detail: idErr });
|
|
50
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
51
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
52
|
+
}
|
|
53
|
+
return { plugins: pluginInstaller.getAllPluginStatuses(req.params.id) };
|
|
54
|
+
});
|
|
55
|
+
app.get("/api/instances/:id/quick-status", async (req, reply) => {
|
|
56
|
+
const idErr = validateId(req.params.id);
|
|
57
|
+
if (idErr)
|
|
58
|
+
return reply.status(400).send({ detail: idErr });
|
|
59
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
60
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
61
|
+
}
|
|
62
|
+
const id = req.params.id;
|
|
63
|
+
const cfg = instanceManager.getConfig(id) ?? {};
|
|
64
|
+
const channels = cfg.channels ?? {};
|
|
65
|
+
const feishuCh = channels["feishu"] ?? channels["lark"] ?? {};
|
|
66
|
+
const weixinCh = channels["openclaw-weixin"] ?? {};
|
|
67
|
+
const feishuBound = !!(feishuCh.enabled &&
|
|
68
|
+
(feishuCh.appId || feishuCh.token || feishuCh.deviceToken || feishuCh.accessToken));
|
|
69
|
+
const weixinBound = !!(weixinCh.enabled &&
|
|
70
|
+
weixinCh.accounts &&
|
|
71
|
+
Object.keys(weixinCh.accounts).length > 0);
|
|
72
|
+
const { readdirSync: fsReaddir, readFileSync: fsRead } = await import("fs");
|
|
73
|
+
const { join: fsJoin } = await import("path");
|
|
74
|
+
const workspaceDir = fsJoin(instanceManager.getOpenclawHome(id), ".openclaw", "workspace");
|
|
75
|
+
const stateDir = fsJoin(workspaceDir, "skills");
|
|
76
|
+
let installedSkillDirs = [];
|
|
77
|
+
try {
|
|
78
|
+
installedSkillDirs = fsReaddir(stateDir, { withFileTypes: true })
|
|
79
|
+
.filter((e) => e.isDirectory())
|
|
80
|
+
.map((e) => e.name);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* no skills dir */
|
|
84
|
+
}
|
|
85
|
+
const mcporterInstalled = installedSkillDirs.some((d) => d.toLowerCase() === "mcporter");
|
|
86
|
+
let mcporterServers = {};
|
|
87
|
+
const mcporterCfgPath = fsJoin(workspaceDir, "config", "mcporter.json");
|
|
88
|
+
try {
|
|
89
|
+
const raw = JSON.parse(fsRead(mcporterCfgPath, "utf8"));
|
|
90
|
+
mcporterServers = raw.mcpServers ?? {};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* no mcporter config */
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
im: { feishu: feishuBound, weixin: weixinBound },
|
|
97
|
+
installedSkillDirs,
|
|
98
|
+
mcporterInstalled,
|
|
99
|
+
mcporterServers,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
// ── MCPorter server CRUD ──────────────────────────────────────────
|
|
103
|
+
app.get("/api/instances/:id/mcporter/list", async (req, reply) => {
|
|
104
|
+
const idErr = validateId(req.params.id);
|
|
105
|
+
if (idErr)
|
|
106
|
+
return reply.status(400).send({ detail: idErr });
|
|
107
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
108
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
109
|
+
}
|
|
110
|
+
const id = req.params.id;
|
|
111
|
+
const openclawHome = instanceManager.getOpenclawHome(id);
|
|
112
|
+
const workspaceDir = join(openclawHome, ".openclaw", "workspace");
|
|
113
|
+
const mcporterBinPath = join(workspaceDir, ".npm-global", "bin", "mcporter");
|
|
114
|
+
const mcporterCfg = join(workspaceDir, "config", "mcporter.json");
|
|
115
|
+
if (!existsSync(mcporterBinPath)) {
|
|
116
|
+
return { servers: [], installed: false };
|
|
117
|
+
}
|
|
118
|
+
const { execFile } = await import("child_process");
|
|
119
|
+
const { promisify } = await import("util");
|
|
120
|
+
const execFileAsync = promisify(execFile);
|
|
121
|
+
try {
|
|
122
|
+
const { stdout } = await execFileAsync(mcporterBinPath, ["list", "--json"], {
|
|
123
|
+
env: { ...process.env, HOME: openclawHome, MCPORTER_CONFIG: mcporterCfg },
|
|
124
|
+
timeout: 60_000,
|
|
125
|
+
});
|
|
126
|
+
const parsed = JSON.parse(stdout);
|
|
127
|
+
return { servers: parsed.servers ?? [], installed: true };
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const raw = err?.stdout ?? "";
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(raw);
|
|
133
|
+
return { servers: parsed.servers ?? [], installed: true };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* fall through */
|
|
137
|
+
}
|
|
138
|
+
return { servers: [], installed: true, error: err?.message ?? "unknown" };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
app.post("/api/instances/:id/mcporter/add", async (req, reply) => {
|
|
142
|
+
const idErr = validateId(req.params.id);
|
|
143
|
+
if (idErr)
|
|
144
|
+
return reply.status(400).send({ detail: idErr });
|
|
145
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
146
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
147
|
+
}
|
|
148
|
+
const { servers } = req.body;
|
|
149
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
150
|
+
return reply.status(400).send({ detail: "servers must be an object" });
|
|
151
|
+
}
|
|
152
|
+
const openclawHome = instanceManager.getOpenclawHome(req.params.id);
|
|
153
|
+
const workspaceDir = join(openclawHome, ".openclaw", "workspace");
|
|
154
|
+
const mcporterCfgPath = join(workspaceDir, "config", "mcporter.json");
|
|
155
|
+
const { readFileSync } = await import("fs");
|
|
156
|
+
let cfg = { mcpServers: {}, imports: [] };
|
|
157
|
+
try {
|
|
158
|
+
cfg = JSON.parse(readFileSync(mcporterCfgPath, "utf8"));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* start fresh */
|
|
162
|
+
}
|
|
163
|
+
if (!cfg.mcpServers)
|
|
164
|
+
cfg.mcpServers = {};
|
|
165
|
+
// Prototype-pollution guard.
|
|
166
|
+
const PROTO_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
167
|
+
for (const [k, v] of Object.entries(servers)) {
|
|
168
|
+
if (!PROTO_KEYS.has(k))
|
|
169
|
+
cfg.mcpServers[k] = v;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
ensureDirContainer(join(workspaceDir, "config"));
|
|
173
|
+
writeConfigFile(mcporterCfgPath, JSON.stringify(cfg, null, 2));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return reply.status(500).send({ detail: `Write failed: ${err.message}` });
|
|
177
|
+
}
|
|
178
|
+
return { ok: true, mcpServers: cfg.mcpServers };
|
|
179
|
+
});
|
|
180
|
+
app.delete("/api/instances/:id/mcporter/:serverName", async (req, reply) => {
|
|
181
|
+
const idErr = validateId(req.params.id);
|
|
182
|
+
if (idErr)
|
|
183
|
+
return reply.status(400).send({ detail: idErr });
|
|
184
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
185
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
186
|
+
}
|
|
187
|
+
const { serverName } = req.params;
|
|
188
|
+
if (!serverName || typeof serverName !== "string") {
|
|
189
|
+
return reply.status(400).send({ detail: "serverName is required" });
|
|
190
|
+
}
|
|
191
|
+
const openclawHome = instanceManager.getOpenclawHome(req.params.id);
|
|
192
|
+
const workspaceDir = join(openclawHome, ".openclaw", "workspace");
|
|
193
|
+
const mcporterCfgPath = join(workspaceDir, "config", "mcporter.json");
|
|
194
|
+
const { readFileSync } = await import("fs");
|
|
195
|
+
let cfg = { mcpServers: {}, imports: [] };
|
|
196
|
+
try {
|
|
197
|
+
cfg = JSON.parse(readFileSync(mcporterCfgPath, "utf8"));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* start fresh */
|
|
201
|
+
}
|
|
202
|
+
if (!cfg.mcpServers || !(serverName in cfg.mcpServers)) {
|
|
203
|
+
return reply.status(404).send({ detail: `Server '${serverName}' not found` });
|
|
204
|
+
}
|
|
205
|
+
delete cfg.mcpServers[serverName];
|
|
206
|
+
try {
|
|
207
|
+
writeConfigFile(mcporterCfgPath, JSON.stringify(cfg, null, 2));
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
return reply.status(500).send({ detail: `Write failed: ${err.message}` });
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, mcpServers: cfg.mcpServers };
|
|
213
|
+
});
|
|
214
|
+
// ── Skills CRUD ───────────────────────────────────────────────────
|
|
215
|
+
app.delete("/api/instances/:id/skills/:skillDir", async (req, reply) => {
|
|
216
|
+
const idErr = validateId(req.params.id);
|
|
217
|
+
if (idErr)
|
|
218
|
+
return reply.status(400).send({ detail: idErr });
|
|
219
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
220
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
221
|
+
}
|
|
222
|
+
const { skillDir } = req.params;
|
|
223
|
+
if (!skillDir || skillDir.includes("/") || skillDir.includes("..") || skillDir.startsWith(".")) {
|
|
224
|
+
return reply.status(400).send({ detail: "Invalid skill directory name" });
|
|
225
|
+
}
|
|
226
|
+
const openclawHome = instanceManager.getOpenclawHome(req.params.id);
|
|
227
|
+
const skillPath = join(openclawHome, ".openclaw", "workspace", "skills", skillDir);
|
|
228
|
+
const { existsSync: fsEx, rmSync } = await import("fs");
|
|
229
|
+
if (!fsEx(skillPath)) {
|
|
230
|
+
return reply.status(404).send({ detail: `Skill '${skillDir}' not found` });
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
return reply.status(500).send({ detail: `Delete failed: ${err.message}` });
|
|
237
|
+
}
|
|
238
|
+
return { ok: true };
|
|
239
|
+
});
|
|
240
|
+
// ── Plugin install ────────────────────────────────────────────────
|
|
241
|
+
app.post("/api/instances/:id/plugins/install", async (req, reply) => {
|
|
242
|
+
const idErr = validateId(req.params.id);
|
|
243
|
+
if (idErr)
|
|
244
|
+
return reply.status(400).send({ detail: idErr });
|
|
245
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
246
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
assertNotLocked(req.params.id);
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return reply.status(e.statusCode || 409).send({ detail: e.message });
|
|
253
|
+
}
|
|
254
|
+
const { channelId } = req.body;
|
|
255
|
+
if (!channelId || typeof channelId !== "string") {
|
|
256
|
+
return reply.status(400).send({ detail: "channelId is required" });
|
|
257
|
+
}
|
|
258
|
+
const inst2 = instanceManager.getInstance(req.params.id);
|
|
259
|
+
const agentType2 = resolveAgentType(inst2);
|
|
260
|
+
const pkg = getAdapter(agentType2).channelPluginMap?.[channelId];
|
|
261
|
+
if (!pkg) {
|
|
262
|
+
return reply.status(400).send({ detail: `Unknown channel: ${channelId}` });
|
|
263
|
+
}
|
|
264
|
+
const pStatus = pluginInstaller.getPluginStatus(req.params.id, channelId);
|
|
265
|
+
if (pStatus.status === "installed")
|
|
266
|
+
return { ok: true, status: "already_installed" };
|
|
267
|
+
if (pStatus.status === "installing")
|
|
268
|
+
return { ok: true, status: "installing" };
|
|
269
|
+
pluginInstaller.enqueueInstall(req.params.id, channelId);
|
|
270
|
+
return { ok: true, status: "queued" };
|
|
271
|
+
});
|
|
272
|
+
// Check-only helper; throws if the plugin needed by an IM login is missing.
|
|
273
|
+
async function ensurePluginInstalled(instanceId, channelId) {
|
|
274
|
+
const inst = instanceManager.getInstance(instanceId);
|
|
275
|
+
const agentType = resolveAgentType(inst);
|
|
276
|
+
const adapter = getAdapter(agentType);
|
|
277
|
+
if (!adapter.channelPluginMap?.[channelId])
|
|
278
|
+
return;
|
|
279
|
+
const installed = typeof adapter.isChannelPluginInstalled === "function"
|
|
280
|
+
? adapter.isChannelPluginInstalled(instanceId, channelId)
|
|
281
|
+
: false;
|
|
282
|
+
if (!installed) {
|
|
283
|
+
throw new Error(`Plugin ${channelId} is not installed. Please install it from the config page.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ── Feishu/Lark OAuth Device Code Login ───────────────────────────
|
|
287
|
+
const FEISHU_AUTH_URL = "https://accounts.feishu.cn";
|
|
288
|
+
const MAX_LOGIN_SESSIONS = 100;
|
|
289
|
+
const feishuLogins = new Map();
|
|
290
|
+
app.post("/api/instances/:id/feishu/login", async (req, reply) => {
|
|
291
|
+
const channelKey = req.body?.channelKey || "feishu";
|
|
292
|
+
const idErr = validateId(req.params.id);
|
|
293
|
+
if (idErr)
|
|
294
|
+
return reply.status(400).send({ detail: idErr });
|
|
295
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
296
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
297
|
+
}
|
|
298
|
+
const svc = await getSvc();
|
|
299
|
+
const svcStatus = await svc.getStatus(req.params.id);
|
|
300
|
+
if (svcStatus.status !== "running") {
|
|
301
|
+
return reply.status(400).send({ detail: "Instance must be running first" });
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
await ensurePluginInstalled(req.params.id, channelKey);
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
return reply.status(400).send({ detail: e.message });
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await fetch(`${FEISHU_AUTH_URL}/oauth/v1/app/registration`, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
313
|
+
body: "action=init",
|
|
314
|
+
signal: AbortSignal.timeout(30_000),
|
|
315
|
+
});
|
|
316
|
+
const beginResp = await fetch(`${FEISHU_AUTH_URL}/oauth/v1/app/registration`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
319
|
+
body: "action=begin&archetype=PersonalAgent&auth_method=client_secret&request_user_info=open_id",
|
|
320
|
+
signal: AbortSignal.timeout(30_000),
|
|
321
|
+
});
|
|
322
|
+
if (!beginResp.ok)
|
|
323
|
+
throw new Error(`Feishu API error: ${beginResp.status}`);
|
|
324
|
+
const beginData = (await beginResp.json());
|
|
325
|
+
const sessionKey = `${req.params.id}-${channelKey}-${Date.now()}`;
|
|
326
|
+
feishuLogins.set(sessionKey, {
|
|
327
|
+
instanceId: req.params.id,
|
|
328
|
+
deviceCode: beginData.device_code,
|
|
329
|
+
startedAt: Date.now(),
|
|
330
|
+
interval: beginData.interval || 5,
|
|
331
|
+
expireIn: beginData.expire_in || 600,
|
|
332
|
+
channelKey,
|
|
333
|
+
});
|
|
334
|
+
for (const [k, v] of feishuLogins) {
|
|
335
|
+
if (Date.now() - v.startedAt > v.expireIn * 1000)
|
|
336
|
+
feishuLogins.delete(k);
|
|
337
|
+
}
|
|
338
|
+
while (feishuLogins.size > MAX_LOGIN_SESSIONS) {
|
|
339
|
+
feishuLogins.delete(feishuLogins.keys().next().value);
|
|
340
|
+
}
|
|
341
|
+
return { qrcodeUrl: beginData.verification_uri_complete, sessionKey };
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
return reply.status(502).send({ detail: e.message || "Failed to start Feishu login" });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
app.get("/api/instances/:id/feishu/login/:sessionKey", async (req, reply) => {
|
|
348
|
+
const idErr = validateId(req.params.id);
|
|
349
|
+
if (idErr)
|
|
350
|
+
return reply.status(400).send({ detail: idErr });
|
|
351
|
+
const login = feishuLogins.get(req.params.sessionKey);
|
|
352
|
+
if (!login)
|
|
353
|
+
return reply.status(404).send({ detail: "Login session not found or expired" });
|
|
354
|
+
if (login.instanceId !== req.params.id) {
|
|
355
|
+
return reply.status(403).send({ detail: "Session belongs to a different instance" });
|
|
356
|
+
}
|
|
357
|
+
if (Date.now() - login.startedAt > login.expireIn * 1000) {
|
|
358
|
+
feishuLogins.delete(req.params.sessionKey);
|
|
359
|
+
return { status: "expired", connected: false, message: "QR code expired, please regenerate." };
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const resp = await fetch(`${FEISHU_AUTH_URL}/oauth/v1/app/registration`, {
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
365
|
+
body: `action=poll&device_code=${encodeURIComponent(login.deviceCode)}`,
|
|
366
|
+
signal: AbortSignal.timeout(10_000),
|
|
367
|
+
});
|
|
368
|
+
const data = (await resp.json());
|
|
369
|
+
if (data.client_id && data.client_secret) {
|
|
370
|
+
const storedChannelKey = login.channelKey || "feishu";
|
|
371
|
+
feishuLogins.delete(req.params.sessionKey);
|
|
372
|
+
const domain = data.user_info?.tenant_brand === "lark" ? "lark" : "feishu";
|
|
373
|
+
instanceManager.saveFeishuCredentials(req.params.id, {
|
|
374
|
+
appId: data.client_id,
|
|
375
|
+
appSecret: data.client_secret,
|
|
376
|
+
domain,
|
|
377
|
+
channelKey: storedChannelKey,
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
status: "confirmed",
|
|
381
|
+
connected: true,
|
|
382
|
+
domain,
|
|
383
|
+
message: domain === "lark" ? "Lark bot configured!" : "Feishu bot configured!",
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (data.error === "authorization_pending") {
|
|
387
|
+
return { status: "waiting", connected: false, message: "Waiting for scan..." };
|
|
388
|
+
}
|
|
389
|
+
if (data.error === "expired_token") {
|
|
390
|
+
feishuLogins.delete(req.params.sessionKey);
|
|
391
|
+
return { status: "expired", connected: false, message: "QR code expired, please regenerate." };
|
|
392
|
+
}
|
|
393
|
+
return { status: "waiting", connected: false, message: "Waiting for scan..." };
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
return reply.status(502).send({ detail: e.message || "Poll failed" });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
// ── WeChat accounts + QR Login ────────────────────────────────────
|
|
400
|
+
app.get("/api/instances/:id/weixin/accounts", async (req, reply) => {
|
|
401
|
+
const idErr = validateId(req.params.id);
|
|
402
|
+
if (idErr)
|
|
403
|
+
return reply.status(400).send({ detail: idErr });
|
|
404
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
405
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
406
|
+
}
|
|
407
|
+
return { accounts: instanceManager.getWeixinAccounts(req.params.id) };
|
|
408
|
+
});
|
|
409
|
+
const WEIXIN_API_BASE = "https://ilinkai.weixin.qq.com";
|
|
410
|
+
const WEIXIN_BOT_TYPE = "3";
|
|
411
|
+
const weixinLogins = new Map();
|
|
412
|
+
app.post("/api/instances/:id/weixin/login", async (req, reply) => {
|
|
413
|
+
const idErr = validateId(req.params.id);
|
|
414
|
+
if (idErr)
|
|
415
|
+
return reply.status(400).send({ detail: idErr });
|
|
416
|
+
if (!instanceManager.getInstance(req.params.id)) {
|
|
417
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
418
|
+
}
|
|
419
|
+
const svc = await getSvc();
|
|
420
|
+
const svcStatus = await svc.getStatus(req.params.id);
|
|
421
|
+
if (svcStatus.status !== "running") {
|
|
422
|
+
return reply.status(400).send({ detail: "Instance must be running first" });
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
await ensurePluginInstalled(req.params.id, "openclaw-weixin");
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
return reply.status(400).send({ detail: e.message });
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const resp = await fetch(`${WEIXIN_API_BASE}/ilink/bot/get_bot_qrcode?bot_type=${WEIXIN_BOT_TYPE}`, { signal: AbortSignal.timeout(30_000) });
|
|
432
|
+
if (!resp.ok)
|
|
433
|
+
throw new Error(`WeChat API error: ${resp.status}`);
|
|
434
|
+
const data = (await resp.json());
|
|
435
|
+
const sessionKey = `${req.params.id}-${Date.now()}`;
|
|
436
|
+
weixinLogins.set(sessionKey, {
|
|
437
|
+
instanceId: req.params.id,
|
|
438
|
+
qrcode: data.qrcode,
|
|
439
|
+
qrcodeUrl: data.qrcode_img_content,
|
|
440
|
+
startedAt: Date.now(),
|
|
441
|
+
});
|
|
442
|
+
for (const [k, v] of weixinLogins) {
|
|
443
|
+
if (Date.now() - v.startedAt > 5 * 60_000)
|
|
444
|
+
weixinLogins.delete(k);
|
|
445
|
+
}
|
|
446
|
+
while (weixinLogins.size > MAX_LOGIN_SESSIONS) {
|
|
447
|
+
weixinLogins.delete(weixinLogins.keys().next().value);
|
|
448
|
+
}
|
|
449
|
+
return { qrcodeUrl: data.qrcode_img_content, sessionKey };
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
return reply.status(502).send({ detail: e.message || "Failed to get QR code" });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
app.get("/api/instances/:id/weixin/login/:sessionKey", async (req, reply) => {
|
|
456
|
+
const idErr = validateId(req.params.id);
|
|
457
|
+
if (idErr)
|
|
458
|
+
return reply.status(400).send({ detail: idErr });
|
|
459
|
+
const login = weixinLogins.get(req.params.sessionKey);
|
|
460
|
+
if (!login)
|
|
461
|
+
return reply.status(404).send({ detail: "Login session not found or expired" });
|
|
462
|
+
if (login.instanceId !== req.params.id) {
|
|
463
|
+
return reply.status(403).send({ detail: "Session belongs to a different instance" });
|
|
464
|
+
}
|
|
465
|
+
if (Date.now() - login.startedAt > 5 * 60_000) {
|
|
466
|
+
weixinLogins.delete(req.params.sessionKey);
|
|
467
|
+
return { status: "expired", connected: false, message: "QR code expired, please regenerate." };
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const resp = await fetch(`${WEIXIN_API_BASE}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(login.qrcode)}`, { headers: { "iLink-App-ClientVersion": "1" }, signal: AbortSignal.timeout(35_000) });
|
|
471
|
+
if (!resp.ok)
|
|
472
|
+
throw new Error(`Status poll failed: ${resp.status}`);
|
|
473
|
+
const data = (await resp.json());
|
|
474
|
+
if (data.status === "confirmed") {
|
|
475
|
+
// iLink occasionally returns `confirmed` with an empty payload
|
|
476
|
+
// (seen when the upstream QR session raced an internal refresh
|
|
477
|
+
// on their side). Without all four fields the runtime can't
|
|
478
|
+
// actually pair — write it anyway and the next `docker pull` of
|
|
479
|
+
// updates 401s with a cryptic "token missing" error. Reject at
|
|
480
|
+
// the route layer so the UI surfaces a clear failure instead.
|
|
481
|
+
if (!data.ilink_bot_id || !data.bot_token) {
|
|
482
|
+
console.error(`[weixin-login] iLink returned confirmed but payload incomplete: ` +
|
|
483
|
+
`bot_id=${data.ilink_bot_id ? "<present>" : "<missing>"}, ` +
|
|
484
|
+
`bot_token=${data.bot_token ? "<present>" : "<missing>"}`);
|
|
485
|
+
weixinLogins.delete(req.params.sessionKey);
|
|
486
|
+
return reply.status(502).send({
|
|
487
|
+
status: "error",
|
|
488
|
+
connected: false,
|
|
489
|
+
detail: "WeChat confirmed the scan but returned an incomplete credential payload. " +
|
|
490
|
+
"Please retry the QR login.",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
weixinLogins.delete(req.params.sessionKey);
|
|
494
|
+
try {
|
|
495
|
+
instanceManager.saveWeixinCredentials(req.params.id, {
|
|
496
|
+
accountId: data.ilink_bot_id,
|
|
497
|
+
token: data.bot_token,
|
|
498
|
+
baseUrl: data.baseurl || WEIXIN_API_BASE,
|
|
499
|
+
userId: data.ilink_user_id || "",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
console.error(`[weixin-login] Failed to save credentials: ${e.message}`);
|
|
504
|
+
return reply.status(500).send({
|
|
505
|
+
status: "confirmed",
|
|
506
|
+
connected: false,
|
|
507
|
+
detail: "WeChat authenticated but failed to save credentials: " + e.message,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
status: "confirmed",
|
|
512
|
+
connected: true,
|
|
513
|
+
accountId: data.ilink_bot_id,
|
|
514
|
+
message: "WeChat connected!",
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (data.status === "expired") {
|
|
518
|
+
try {
|
|
519
|
+
const refreshResp = await fetch(`${WEIXIN_API_BASE}/ilink/bot/get_bot_qrcode?bot_type=${WEIXIN_BOT_TYPE}`, { signal: AbortSignal.timeout(30_000) });
|
|
520
|
+
if (refreshResp.ok) {
|
|
521
|
+
const refreshData = (await refreshResp.json());
|
|
522
|
+
login.qrcode = refreshData.qrcode;
|
|
523
|
+
login.qrcodeUrl = refreshData.qrcode_img_content;
|
|
524
|
+
login.startedAt = Date.now();
|
|
525
|
+
return {
|
|
526
|
+
status: "refreshed",
|
|
527
|
+
connected: false,
|
|
528
|
+
qrcodeUrl: refreshData.qrcode_img_content,
|
|
529
|
+
message: "QR code refreshed, please scan again.",
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
/* fall through */
|
|
535
|
+
}
|
|
536
|
+
weixinLogins.delete(req.params.sessionKey);
|
|
537
|
+
return { status: "expired", connected: false, message: "QR code expired, please regenerate." };
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
status: data.status,
|
|
541
|
+
connected: false,
|
|
542
|
+
message: data.status === "scaned" ? "Scanned, please confirm on WeChat..." : "Waiting for scan...",
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
return reply.status(502).send({ detail: e.message || "Status poll failed" });
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
// ── Usage (reads OpenClaw sessions.json) ──────────────────────────
|
|
550
|
+
app.get("/api/instances/:id/usage", async (req, reply) => {
|
|
551
|
+
const idErr = validateId(req.params.id);
|
|
552
|
+
if (idErr)
|
|
553
|
+
return reply.status(400).send({ detail: idErr });
|
|
554
|
+
const inst = instanceManager.getInstance(req.params.id);
|
|
555
|
+
if (!inst)
|
|
556
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
557
|
+
const openclawHome = instanceManager.getOpenclawHome(req.params.id);
|
|
558
|
+
const sessionsIndex = join(openclawHome, ".openclaw", "agents", "main", "sessions", "sessions.json");
|
|
559
|
+
const emptyTotals = {
|
|
560
|
+
input: 0,
|
|
561
|
+
output: 0,
|
|
562
|
+
cacheRead: 0,
|
|
563
|
+
cacheWrite: 0,
|
|
564
|
+
totalTokens: 0,
|
|
565
|
+
costTotal: 0,
|
|
566
|
+
messages: 0,
|
|
567
|
+
};
|
|
568
|
+
if (!existsSync(sessionsIndex))
|
|
569
|
+
return { sessions: [], totals: emptyTotals };
|
|
570
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
571
|
+
const indexStat = await stat(sessionsIndex);
|
|
572
|
+
if (indexStat.size > MAX_FILE_SIZE) {
|
|
573
|
+
return reply.status(400).send({ detail: "sessions.json exceeds 10MB size limit" });
|
|
574
|
+
}
|
|
575
|
+
let sessionsMap;
|
|
576
|
+
try {
|
|
577
|
+
const raw = await readFile(sessionsIndex, "utf-8");
|
|
578
|
+
sessionsMap = JSON.parse(raw);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return reply.status(500).send({ detail: "Failed to parse sessions.json" });
|
|
582
|
+
}
|
|
583
|
+
const sessions = [];
|
|
584
|
+
const totals = { ...emptyTotals };
|
|
585
|
+
for (const [sessionKey, sessionMeta] of Object.entries(sessionsMap)) {
|
|
586
|
+
const sessionFile = sessionMeta?.sessionFile;
|
|
587
|
+
if (!sessionFile || !existsSync(sessionFile))
|
|
588
|
+
continue;
|
|
589
|
+
try {
|
|
590
|
+
const resolvedSession = realpathSync(sessionFile);
|
|
591
|
+
const resolvedHome = realpathSync(openclawHome);
|
|
592
|
+
if (!resolvedSession.startsWith(resolvedHome + "/") && resolvedSession !== resolvedHome)
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
let sessionStat;
|
|
599
|
+
try {
|
|
600
|
+
sessionStat = await stat(sessionFile);
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (sessionStat.size > MAX_FILE_SIZE)
|
|
606
|
+
continue;
|
|
607
|
+
const sessionUsage = {
|
|
608
|
+
input: 0,
|
|
609
|
+
output: 0,
|
|
610
|
+
cacheRead: 0,
|
|
611
|
+
cacheWrite: 0,
|
|
612
|
+
totalTokens: 0,
|
|
613
|
+
costTotal: 0,
|
|
614
|
+
messages: 0,
|
|
615
|
+
};
|
|
616
|
+
let model = "";
|
|
617
|
+
let firstTs = "";
|
|
618
|
+
let lastTs = "";
|
|
619
|
+
const originLabel = sessionMeta?.origin?.label || "";
|
|
620
|
+
const channel = sessionMeta?.origin?.provider || "";
|
|
621
|
+
let sessionContent;
|
|
622
|
+
try {
|
|
623
|
+
sessionContent = await readFile(sessionFile, "utf-8");
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
for (const line of sessionContent.split("\n")) {
|
|
629
|
+
let entry;
|
|
630
|
+
try {
|
|
631
|
+
entry = JSON.parse(line.trim());
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (entry.type !== "message")
|
|
637
|
+
continue;
|
|
638
|
+
const msg = entry.message || {};
|
|
639
|
+
const ts = entry.timestamp || "";
|
|
640
|
+
if (ts && !firstTs)
|
|
641
|
+
firstTs = ts;
|
|
642
|
+
if (ts)
|
|
643
|
+
lastTs = ts;
|
|
644
|
+
if (msg.role === "assistant") {
|
|
645
|
+
if (!model && msg.model)
|
|
646
|
+
model = msg.model;
|
|
647
|
+
const usage = msg.usage;
|
|
648
|
+
if (usage) {
|
|
649
|
+
sessionUsage.input += usage.input || 0;
|
|
650
|
+
sessionUsage.output += usage.output || 0;
|
|
651
|
+
sessionUsage.cacheRead += usage.cacheRead || 0;
|
|
652
|
+
sessionUsage.cacheWrite += usage.cacheWrite || 0;
|
|
653
|
+
sessionUsage.totalTokens += usage.totalTokens || 0;
|
|
654
|
+
if (typeof usage.cost === "object")
|
|
655
|
+
sessionUsage.costTotal += usage.cost.total || 0;
|
|
656
|
+
}
|
|
657
|
+
sessionUsage.messages++;
|
|
658
|
+
}
|
|
659
|
+
else if (msg.role === "user") {
|
|
660
|
+
sessionUsage.messages++;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
sessions.push({
|
|
664
|
+
key: sessionKey,
|
|
665
|
+
model,
|
|
666
|
+
channel,
|
|
667
|
+
origin: originLabel,
|
|
668
|
+
firstMessage: firstTs,
|
|
669
|
+
lastMessage: lastTs,
|
|
670
|
+
usage: sessionUsage,
|
|
671
|
+
});
|
|
672
|
+
for (const k of ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "messages"]) {
|
|
673
|
+
totals[k] += sessionUsage[k];
|
|
674
|
+
}
|
|
675
|
+
totals.costTotal += sessionUsage.costTotal;
|
|
676
|
+
}
|
|
677
|
+
sessions.sort((a, b) => (b.lastMessage || "").localeCompare(a.lastMessage || ""));
|
|
678
|
+
return { sessions, totals };
|
|
679
|
+
});
|
|
680
|
+
// ── Gateway launch (OpenClaw control-UI iframe boot) ──────────────
|
|
681
|
+
app.get("/api/instances/:id/gateway-launch", async (req, reply) => {
|
|
682
|
+
const idErr = validateId(req.params.id);
|
|
683
|
+
if (idErr)
|
|
684
|
+
return reply.status(400).send({ detail: idErr });
|
|
685
|
+
const inst = instanceManager.getInstance(req.params.id);
|
|
686
|
+
if (!inst)
|
|
687
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
688
|
+
if (!(await supportsGatewayChatPanel(req.params.id, inst))) {
|
|
689
|
+
return reply
|
|
690
|
+
.status(501)
|
|
691
|
+
.send({ detail: "Gateway chat panel is not supported for this runtime" });
|
|
692
|
+
}
|
|
693
|
+
const panelOrigin = inferRequestOrigin(req);
|
|
694
|
+
if (panelOrigin) {
|
|
695
|
+
let addedAllowedOrigin = false;
|
|
696
|
+
try {
|
|
697
|
+
addedAllowedOrigin = await ensureControlUiAllowedOrigin(req.params.id, panelOrigin);
|
|
698
|
+
}
|
|
699
|
+
catch (err) {
|
|
700
|
+
console.warn(`[gateway-launch] failed to add allowed origin for ${req.params.id}:`, err.message || err);
|
|
701
|
+
}
|
|
702
|
+
if (addedAllowedOrigin) {
|
|
703
|
+
await restartRunningInstanceForControlUiOrigin(req.params.id, panelOrigin);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const baseUrl = `/api/instances/${req.params.id}/gateway/`;
|
|
707
|
+
const cfg = instanceManager.getStoredConfig(req.params.id);
|
|
708
|
+
const token = cfg?.gateway?.auth?.token;
|
|
709
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
710
|
+
return { url: baseUrl };
|
|
711
|
+
}
|
|
712
|
+
return { url: `${baseUrl}#token=${encodeURIComponent(token.trim())}` };
|
|
713
|
+
});
|
|
714
|
+
// ── Gateway HTTP proxy (with control-UI HTML injection) ───────────
|
|
715
|
+
const gatewayProxy = async (request, reply) => {
|
|
716
|
+
const { id } = request.params;
|
|
717
|
+
const idErr = validateId(id);
|
|
718
|
+
if (idErr)
|
|
719
|
+
return reply.status(400).send({ detail: idErr });
|
|
720
|
+
const inst = instanceManager.getInstance(id);
|
|
721
|
+
if (!inst)
|
|
722
|
+
return reply.status(404).send({ detail: "Instance not found" });
|
|
723
|
+
if (!(await supportsGatewayChatPanel(id, inst))) {
|
|
724
|
+
return reply.status(501).send({ detail: "Gateway proxy is not supported for this runtime" });
|
|
725
|
+
}
|
|
726
|
+
const port = instanceManager.getGatewayPort(id);
|
|
727
|
+
const gwHost = await instanceManager.getGatewayHost(id);
|
|
728
|
+
const suffix = request.params["*"] || "";
|
|
729
|
+
const qs = request.url.includes("?") ? request.url.slice(request.url.indexOf("?")) : "";
|
|
730
|
+
const urlGwHost = instanceManager.urlHost(gwHost);
|
|
731
|
+
const targetUrl = `http://${urlGwHost}:${port}/${suffix}${qs}`;
|
|
732
|
+
try {
|
|
733
|
+
const fwdHeaders = {};
|
|
734
|
+
const rawHeaders = request.raw.headers;
|
|
735
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
736
|
+
if (v === undefined)
|
|
737
|
+
continue;
|
|
738
|
+
const lk = k.toLowerCase();
|
|
739
|
+
if (HOP_BY_HOP.has(lk) ||
|
|
740
|
+
PROXY_IDENTITY_HEADERS.has(lk) ||
|
|
741
|
+
lk === "host" ||
|
|
742
|
+
lk === "cookie" ||
|
|
743
|
+
lk === "authorization")
|
|
744
|
+
continue;
|
|
745
|
+
fwdHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
746
|
+
}
|
|
747
|
+
fwdHeaders["host"] = `${urlGwHost}:${port}`;
|
|
748
|
+
// NOTE: the cli branch (commit 94163bc) stripped the browser's Origin
|
|
749
|
+
// and rewrote it to `http://<gwHost>:<gwPort>`. In multi-agents the
|
|
750
|
+
// panel instead auto-adds the browser's origin to
|
|
751
|
+
// `gateway.controlUi.allowedOrigins` and restarts the instance (see
|
|
752
|
+
// ensureControlUiAllowedOrigin in routes/instances.ts). Forwarding the
|
|
753
|
+
// real Origin is what lets that whitelist entry match — rewriting
|
|
754
|
+
// breaks the configured allowlist.
|
|
755
|
+
fwdHeaders["accept-encoding"] = "identity";
|
|
756
|
+
const upstreamRes = await new Promise((resolve, reject) => {
|
|
757
|
+
const proxyReq = httpRequest(targetUrl, { method: request.method, headers: fwdHeaders }, resolve);
|
|
758
|
+
proxyReq.on("error", reject);
|
|
759
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
760
|
+
request.raw.pipe(proxyReq);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
proxyReq.end();
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
reply.status(upstreamRes.statusCode || 502);
|
|
767
|
+
for (const [k, v] of Object.entries(upstreamRes.headers)) {
|
|
768
|
+
if (v === undefined)
|
|
769
|
+
continue;
|
|
770
|
+
const lk = k.toLowerCase();
|
|
771
|
+
if (HOP_BY_HOP.has(lk))
|
|
772
|
+
continue;
|
|
773
|
+
if (lk === "x-frame-options") {
|
|
774
|
+
reply.header("x-frame-options", "SAMEORIGIN");
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (lk === "content-security-policy") {
|
|
778
|
+
const csp = Array.isArray(v) ? v.join(", ") : v;
|
|
779
|
+
const rewritten = csp.replace(/frame-ancestors\s+[^;]*/i, "frame-ancestors 'self'");
|
|
780
|
+
reply.header("content-security-policy", rewritten === csp ? `${csp}; frame-ancestors 'self'` : rewritten);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
reply.header(k, Array.isArray(v) ? v.join(", ") : v);
|
|
784
|
+
}
|
|
785
|
+
const respCt = upstreamRes.headers["content-type"] || "";
|
|
786
|
+
if (suffix === "__openclaw/control-ui-config.json" && respCt.includes("application/json")) {
|
|
787
|
+
const chunks = [];
|
|
788
|
+
for await (const chunk of upstreamRes)
|
|
789
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
790
|
+
try {
|
|
791
|
+
const config = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
792
|
+
config.basePath = `/api/instances/${id}/gateway`;
|
|
793
|
+
const buf = Buffer.from(JSON.stringify(config));
|
|
794
|
+
reply.header("content-length", buf.length);
|
|
795
|
+
reply.removeHeader("content-encoding");
|
|
796
|
+
return reply.send(buf);
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
/* fall through to stream */
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (respCt.includes("text/html")) {
|
|
803
|
+
const chunks = [];
|
|
804
|
+
for await (const chunk of upstreamRes)
|
|
805
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
806
|
+
let html = Buffer.concat(chunks).toString("utf-8");
|
|
807
|
+
const basePath = `/api/instances/${id}/gateway`;
|
|
808
|
+
const injectScript = [
|
|
809
|
+
`window.__OPENCLAW_CONTROL_UI_BASE_PATH__=${JSON.stringify(basePath)};`,
|
|
810
|
+
`(()=>{`,
|
|
811
|
+
` try {`,
|
|
812
|
+
` const settingsKey='openclaw.control.settings.v1';`,
|
|
813
|
+
` const tokenStoragePrefix='openclaw.control.token.v1:';`,
|
|
814
|
+
` const normalizeGatewayScope=(gatewayUrl)=>{`,
|
|
815
|
+
` const raw=(gatewayUrl||'').trim();`,
|
|
816
|
+
` if(!raw) return 'default';`,
|
|
817
|
+
` try {`,
|
|
818
|
+
` const base=\`\${window.location.protocol}//\${window.location.host}\${window.location.pathname||'/'}\`;`,
|
|
819
|
+
` const parsed=new URL(raw, base);`,
|
|
820
|
+
` const pathname=parsed.pathname==='/'?'':(parsed.pathname.replace(/\\/+$/,'')||parsed.pathname);`,
|
|
821
|
+
` return \`\${parsed.protocol}//\${parsed.host}\${pathname}\`;`,
|
|
822
|
+
` } catch {`,
|
|
823
|
+
` return raw;`,
|
|
824
|
+
` }`,
|
|
825
|
+
` };`,
|
|
826
|
+
` const proto=window.location.protocol==='https:'?'wss':'ws';`,
|
|
827
|
+
` const gatewayUrl=\`\${proto}://\${window.location.host}${basePath}\`;`,
|
|
828
|
+
` const tokenSessionKey=\`\${tokenStoragePrefix}\${normalizeGatewayScope(gatewayUrl)}\`;`,
|
|
829
|
+
` const raw=window.localStorage.getItem(settingsKey);`,
|
|
830
|
+
` let next={};`,
|
|
831
|
+
` try { next=raw ? JSON.parse(raw) : {}; } catch { next={}; }`,
|
|
832
|
+
` next.gatewayUrl=gatewayUrl;`,
|
|
833
|
+
` if('token' in next) delete next.token;`,
|
|
834
|
+
` const hashParams=new URLSearchParams(window.location.hash.startsWith('#')?window.location.hash.slice(1):window.location.hash);`,
|
|
835
|
+
` const searchParams=new URLSearchParams(window.location.search);`,
|
|
836
|
+
` const launchToken=(hashParams.get('token')||searchParams.get('token')||'').trim();`,
|
|
837
|
+
` if(launchToken){`,
|
|
838
|
+
` window.sessionStorage.setItem(tokenSessionKey, launchToken);`,
|
|
839
|
+
` }`,
|
|
840
|
+
` window.localStorage.setItem(settingsKey, JSON.stringify(next));`,
|
|
841
|
+
` const autoConnect=()=>{`,
|
|
842
|
+
` const attempt=()=>{`,
|
|
843
|
+
` const app=document.querySelector('openclaw-app');`,
|
|
844
|
+
` if(!app||typeof app.connect!=='function'||typeof app.applySettings!=='function'||!app.settings||typeof app.settings!=='object') return false;`,
|
|
845
|
+
` const sessionToken=(window.sessionStorage.getItem(tokenSessionKey)||'').trim();`,
|
|
846
|
+
` const token=(sessionToken||launchToken||app.settings.token||'').trim();`,
|
|
847
|
+
` if(!token) return false;`,
|
|
848
|
+
` const nextSettings={...app.settings, gatewayUrl, token};`,
|
|
849
|
+
` if(nextSettings.gatewayUrl!==app.settings.gatewayUrl||nextSettings.token!==app.settings.token){`,
|
|
850
|
+
` app.applySettings(nextSettings);`,
|
|
851
|
+
` }`,
|
|
852
|
+
` if(app.connected) return true;`,
|
|
853
|
+
` const wsState=app.client&&app.client.ws?app.client.ws.readyState:null;`,
|
|
854
|
+
` const connecting=wsState===0||wsState===1;`,
|
|
855
|
+
` if(!connecting){`,
|
|
856
|
+
` window.setTimeout(()=>{`,
|
|
857
|
+
` try { if(!app.connected) app.connect(); } catch {}`,
|
|
858
|
+
` }, 0);`,
|
|
859
|
+
` }`,
|
|
860
|
+
` return false;`,
|
|
861
|
+
` };`,
|
|
862
|
+
` const start=()=>{`,
|
|
863
|
+
` let tries=0;`,
|
|
864
|
+
` let timer=0;`,
|
|
865
|
+
` const tick=()=>{`,
|
|
866
|
+
` tries+=1;`,
|
|
867
|
+
` if(attempt()||tries>=120){`,
|
|
868
|
+
` window.clearInterval(timer);`,
|
|
869
|
+
` }`,
|
|
870
|
+
` };`,
|
|
871
|
+
` tick();`,
|
|
872
|
+
` timer=window.setInterval(()=>{`,
|
|
873
|
+
` tick();`,
|
|
874
|
+
` },500);`,
|
|
875
|
+
` };`,
|
|
876
|
+
` if(window.customElements&&typeof window.customElements.whenDefined==='function'){`,
|
|
877
|
+
` window.customElements.whenDefined('openclaw-app').then(start).catch(()=>{});`,
|
|
878
|
+
` }else{`,
|
|
879
|
+
` start();`,
|
|
880
|
+
` }`,
|
|
881
|
+
` };`,
|
|
882
|
+
` autoConnect();`,
|
|
883
|
+
` } catch {}`,
|
|
884
|
+
`})();`,
|
|
885
|
+
].join("");
|
|
886
|
+
const inject = `<script>${injectScript}</script>`;
|
|
887
|
+
const injectCmdScript = [
|
|
888
|
+
`(function(){`,
|
|
889
|
+
` var _jishuInject=function(cmd,send){`,
|
|
890
|
+
` var app=document.querySelector('openclaw-app');`,
|
|
891
|
+
` if(!app)return false;`,
|
|
892
|
+
` if(send){`,
|
|
893
|
+
` if(!app.connected)return false;`,
|
|
894
|
+
` try{app.handleSendChat(cmd);return true;}catch(e){}`,
|
|
895
|
+
` return false;`,
|
|
896
|
+
` }else{`,
|
|
897
|
+
` try{app.chatMessage=cmd;return true;}catch(e){}`,
|
|
898
|
+
` return false;`,
|
|
899
|
+
` }`,
|
|
900
|
+
` };`,
|
|
901
|
+
` window.addEventListener('message',function(e){`,
|
|
902
|
+
` if(!e.data||e.data.type!=='jishu:inject-cmd')return;`,
|
|
903
|
+
` var cmd=e.data.cmd,send=!!e.data.send,tries=0;`,
|
|
904
|
+
` var poll=function(){if(_jishuInject(cmd,send)||++tries>=50)return;setTimeout(poll,200);};`,
|
|
905
|
+
` poll();`,
|
|
906
|
+
` },false);`,
|
|
907
|
+
`})();`,
|
|
908
|
+
].join("");
|
|
909
|
+
const injectCmdScriptHash = createHash("sha256").update(injectCmdScript, "utf8").digest("base64");
|
|
910
|
+
const fullHtmlInject = `${inject}<script>${injectCmdScript}</script>`;
|
|
911
|
+
html = html.replace(/<head\b[^>]*>/i, (match) => `${match}${fullHtmlInject}`);
|
|
912
|
+
const inlineScriptHash = createHash("sha256").update(injectScript, "utf8").digest("base64");
|
|
913
|
+
const cspHeader = reply.getHeader("content-security-policy");
|
|
914
|
+
if (typeof cspHeader === "string" && cspHeader) {
|
|
915
|
+
const hashToken = `'sha256-${inlineScriptHash}'`;
|
|
916
|
+
const hashToken2 = `'sha256-${injectCmdScriptHash}'`;
|
|
917
|
+
const addHashes = (src) => {
|
|
918
|
+
let s = src;
|
|
919
|
+
if (!s.includes(hashToken))
|
|
920
|
+
s = s + ` ${hashToken}`;
|
|
921
|
+
if (!s.includes(hashToken2))
|
|
922
|
+
s = s + ` ${hashToken2}`;
|
|
923
|
+
return s;
|
|
924
|
+
};
|
|
925
|
+
const nextCsp = /\bscript-src\b/i.test(cspHeader)
|
|
926
|
+
? cspHeader.replace(/\bscript-src\b([^;]*)/i, (_m, value) => `script-src${addHashes(value)}`)
|
|
927
|
+
: `${cspHeader}; script-src 'self' ${hashToken} ${hashToken2}`;
|
|
928
|
+
reply.header("content-security-policy", nextCsp);
|
|
929
|
+
}
|
|
930
|
+
const buf = Buffer.from(html, "utf-8");
|
|
931
|
+
reply.header("cache-control", "no-store");
|
|
932
|
+
reply.header("content-length", buf.length);
|
|
933
|
+
reply.removeHeader("content-encoding");
|
|
934
|
+
return reply.send(buf);
|
|
935
|
+
}
|
|
936
|
+
return reply.send(upstreamRes);
|
|
937
|
+
}
|
|
938
|
+
catch (err) {
|
|
939
|
+
console.error(`[gateway-proxy] ${id}:`, err.message || err);
|
|
940
|
+
return reply.status(502).send({ detail: "Cannot reach OpenClaw gateway" });
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
app.all("/api/instances/:id/gateway/*", gatewayProxy);
|
|
944
|
+
app.all("/api/instances/:id/gateway", gatewayProxy);
|
|
945
|
+
}
|
|
946
|
+
//# sourceMappingURL=openclaw-routes.js.map
|