@matelink/cli 2026.4.10 → 2026.4.12
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/README.md +1 -1
- package/bin/matecli.mjs +745 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ of `openclaw.json`.
|
|
|
78
78
|
Start OpenClaw first, then pair from the app:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
openclaw gateway run --bind loopback --port
|
|
81
|
+
openclaw gateway run --bind loopback --port <gateway.port in openclaw.json> --force
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
Use the mobile app to generate a pairing code, then run:
|
package/bin/matecli.mjs
CHANGED
|
@@ -19,13 +19,17 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
const NUMERIC_CODE_LENGTH = 4;
|
|
20
20
|
const GROUP_SIZE = 4;
|
|
21
21
|
const DEFAULT_RELAY_WORKER_WAIT_SECONDS = 600;
|
|
22
|
+
const DEFAULT_NETWORK_TIMEOUT_MS = 600000;
|
|
22
23
|
const DEFAULT_RELAY_URL = "http://43.134.64.199:8090";
|
|
23
24
|
const DEFAULT_GATEWAY_HOST = "127.0.0.1";
|
|
24
25
|
const DEFAULT_WEBHOOK_PATH = "/testnextim/webhook";
|
|
25
26
|
const DEFAULT_BIND_PATH = "/testnextim/bind";
|
|
26
|
-
const GATEWAY_RPC_CLI_TIMEOUT_MS =
|
|
27
|
+
const GATEWAY_RPC_CLI_TIMEOUT_MS = DEFAULT_NETWORK_TIMEOUT_MS;
|
|
27
28
|
const CLI_PACKAGE_NAME = "@matelink/cli";
|
|
28
29
|
const CLI_COMMAND_NAME = "matecli";
|
|
30
|
+
const SERVICE_LABEL = "com.matelink.matecli.bridge";
|
|
31
|
+
const SERVICE_UNIT_NAME = "matelink-matecli-bridge.service";
|
|
32
|
+
const SERVICE_TASK_NAME = "Matelink MateCLI Bridge";
|
|
29
33
|
// Relay worker defaults to full operator scope so gateway HTTP routes and RPC-adjacent
|
|
30
34
|
// compatibility endpoints remain available without per-method scope juggling.
|
|
31
35
|
const DEFAULT_GATEWAY_SCOPES = "operator.admin";
|
|
@@ -38,8 +42,9 @@ const CLI_I18N = {
|
|
|
38
42
|
help_environment: "环境变量:",
|
|
39
43
|
help_package: `包名: ${CLI_PACKAGE_NAME}`,
|
|
40
44
|
help_tip: "提示:",
|
|
41
|
-
help_tip_pair: " pair
|
|
45
|
+
help_tip_pair: " pair 命令会自动安装并启动系统后台服务,保持 relay bridge 在线。",
|
|
42
46
|
help_tip_no_serve: " 如果不希望自动启动 bridge worker,可使用 --no-serve-relay。",
|
|
47
|
+
help_tip_service: ` 可使用 \`${CLI_COMMAND_NAME} service status|install|uninstall|restart\` 管理后台服务。`,
|
|
43
48
|
help_env_home: " OPENCLAW_HOME 覆盖 OpenClaw 主目录(默认: ~/.openclaw)",
|
|
44
49
|
help_env_openim: " OPENIM_RELAY_URL / OPENIM_RELAY_TOKEN",
|
|
45
50
|
help_env_relay_url: ` TESTNEXTIM_RELAY_URL 覆盖 relay 地址(默认: ${DEFAULT_RELAY_URL})`,
|
|
@@ -74,6 +79,14 @@ const CLI_I18N = {
|
|
|
74
79
|
gateway_ready_hint: "请确认 OpenClaw daemon 正在运行,并且 responses endpoint 已启用。",
|
|
75
80
|
pair_success: "配对已就绪,请回到 App 完成确认。",
|
|
76
81
|
pair_worker_failed: "配对初始化失败,relay bridge worker 启动失败。",
|
|
82
|
+
service_install_failed: "后台服务安装失败,将回退到临时后台进程。",
|
|
83
|
+
service_installed: "后台服务已安装并启动。",
|
|
84
|
+
service_uninstalled: "后台服务已卸载。",
|
|
85
|
+
service_status_manager: "服务管理器: {value}",
|
|
86
|
+
service_status_installed: "服务已安装: {value}",
|
|
87
|
+
service_status_running: "服务运行中: {value}",
|
|
88
|
+
service_status_target: "服务目标: {value}",
|
|
89
|
+
service_status_logs: "日志目录: {value}",
|
|
77
90
|
setup_updated: "配置已更新: {path}",
|
|
78
91
|
setup_ready: "配置已准备好: {path}",
|
|
79
92
|
setup_state_file: "状态文件: {path}",
|
|
@@ -115,8 +128,9 @@ const CLI_I18N = {
|
|
|
115
128
|
help_environment: "Environment:",
|
|
116
129
|
help_package: `Package: ${CLI_PACKAGE_NAME}`,
|
|
117
130
|
help_tip: "Tip:",
|
|
118
|
-
help_tip_pair: " The pair command auto-starts the relay bridge
|
|
131
|
+
help_tip_pair: " The pair command auto-installs and starts a background service to keep the relay bridge online.",
|
|
119
132
|
help_tip_no_serve: " Use --no-serve-relay if you do not want the bridge worker to start automatically.",
|
|
133
|
+
help_tip_service: ` Use \`${CLI_COMMAND_NAME} service status|install|uninstall|restart\` to manage the background service.`,
|
|
120
134
|
help_env_home: " OPENCLAW_HOME Override OpenClaw home directory (default: ~/.openclaw)",
|
|
121
135
|
help_env_openim: " OPENIM_RELAY_URL / OPENIM_RELAY_TOKEN",
|
|
122
136
|
help_env_relay_url: ` TESTNEXTIM_RELAY_URL Override relay base URL (default: ${DEFAULT_RELAY_URL})`,
|
|
@@ -152,6 +166,14 @@ const CLI_I18N = {
|
|
|
152
166
|
"Make sure OpenClaw daemon is running and the responses endpoint is enabled.",
|
|
153
167
|
pair_success: "Pairing is ready. Return to the app to finish confirmation.",
|
|
154
168
|
pair_worker_failed: "Pairing initialization failed because the relay bridge worker could not start.",
|
|
169
|
+
service_install_failed: "Background service install failed. Falling back to a temporary detached process.",
|
|
170
|
+
service_installed: "Background service installed and started.",
|
|
171
|
+
service_uninstalled: "Background service uninstalled.",
|
|
172
|
+
service_status_manager: "Service Manager: {value}",
|
|
173
|
+
service_status_installed: "Service Installed: {value}",
|
|
174
|
+
service_status_running: "Service Running: {value}",
|
|
175
|
+
service_status_target: "Service Target: {value}",
|
|
176
|
+
service_status_logs: "Log Directory: {value}",
|
|
155
177
|
setup_updated: "Updated config: {path}",
|
|
156
178
|
setup_ready: "Config already prepared: {path}",
|
|
157
179
|
setup_state_file: "State file: {path}",
|
|
@@ -233,6 +255,7 @@ function printHelp() {
|
|
|
233
255
|
` ${CLI_COMMAND_NAME} pair <1234|123456|XXXX-XXXX> [--mode <relay>]`,
|
|
234
256
|
` ${CLI_COMMAND_NAME} reset [--relay <url>] [--gateway <url>] [--json]`,
|
|
235
257
|
` ${CLI_COMMAND_NAME} bridge [--relay <url>] [--gateway <url>] [--json] (Deprecated)`,
|
|
258
|
+
` ${CLI_COMMAND_NAME} service <status|install|uninstall|restart> [--relay <url>] [--gateway <url>] [--json]`,
|
|
236
259
|
` ${CLI_COMMAND_NAME} pair-url [<1234|123456|XXXX-XXXX>] [--account <id>] [--code <1234|123456|XXXX-XXXX>] [--json]`,
|
|
237
260
|
` ${CLI_COMMAND_NAME} status [--json]`,
|
|
238
261
|
"",
|
|
@@ -249,6 +272,7 @@ function printHelp() {
|
|
|
249
272
|
t("help_tip"),
|
|
250
273
|
t("help_tip_pair"),
|
|
251
274
|
t("help_tip_no_serve"),
|
|
275
|
+
t("help_tip_service"),
|
|
252
276
|
].join("\n"),
|
|
253
277
|
);
|
|
254
278
|
}
|
|
@@ -273,6 +297,7 @@ function parseArgs(argv) {
|
|
|
273
297
|
waitBind: false,
|
|
274
298
|
waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
|
|
275
299
|
serveRelay: true,
|
|
300
|
+
serviceAction: "status",
|
|
276
301
|
mode: undefined,
|
|
277
302
|
};
|
|
278
303
|
|
|
@@ -369,6 +394,14 @@ function parseArgs(argv) {
|
|
|
369
394
|
fail(`unknown option: ${token}`);
|
|
370
395
|
}
|
|
371
396
|
|
|
397
|
+
if (command === "service") {
|
|
398
|
+
if (!options.serviceAction || options.serviceAction === "status") {
|
|
399
|
+
options.serviceAction = token.trim().toLowerCase();
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
fail(`unexpected argument: ${token}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
372
405
|
if (command === "pair" || command === "pair-url") {
|
|
373
406
|
if (!options.code) {
|
|
374
407
|
options.code = token.trim();
|
|
@@ -399,6 +432,26 @@ function resolveTestNextIMStatePath() {
|
|
|
399
432
|
return path.join(resolveOpenClawHome(), "testnextim.state.json");
|
|
400
433
|
}
|
|
401
434
|
|
|
435
|
+
function resolveMatecliRuntimeDir() {
|
|
436
|
+
return path.join(resolveOpenClawHome(), "matecli");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function resolveBridgeLogsDir() {
|
|
440
|
+
return path.join(resolveMatecliRuntimeDir(), "logs");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function resolveBridgeLaunchScriptPath() {
|
|
444
|
+
return path.join(resolveMatecliRuntimeDir(), "bridge-launch.sh");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resolveMacLaunchAgentPath() {
|
|
448
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function resolveLinuxUserUnitPath() {
|
|
452
|
+
return path.join(os.homedir(), ".config", "systemd", "user", SERVICE_UNIT_NAME);
|
|
453
|
+
}
|
|
454
|
+
|
|
402
455
|
function resolveBridgeLockPath({ relayUrl, gatewayId, gatewayBaseUrl }) {
|
|
403
456
|
const digest = createHash("sha256")
|
|
404
457
|
.update(
|
|
@@ -440,6 +493,39 @@ function readOptionalJsonFile(filePath) {
|
|
|
440
493
|
}
|
|
441
494
|
}
|
|
442
495
|
|
|
496
|
+
function readJsonFileIfExists(filePath) {
|
|
497
|
+
try {
|
|
498
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
499
|
+
} catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function commandToString(command, args) {
|
|
505
|
+
return [command, ...args]
|
|
506
|
+
.map((part) => {
|
|
507
|
+
const value = String(part);
|
|
508
|
+
return /[\s"]/u.test(value) ? `"${value.replaceAll("\"", "\\\"")}"` : value;
|
|
509
|
+
})
|
|
510
|
+
.join(" ");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function quoteWindowsCommandArg(value) {
|
|
514
|
+
const text = String(value ?? "");
|
|
515
|
+
if (!/[\s"]/u.test(text)) {
|
|
516
|
+
return text;
|
|
517
|
+
}
|
|
518
|
+
return `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function runCommand(command, args, options = {}) {
|
|
522
|
+
return spawnSync(command, args, {
|
|
523
|
+
encoding: "utf8",
|
|
524
|
+
windowsHide: process.platform === "win32",
|
|
525
|
+
...options,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
443
529
|
function isPidAlive(pid) {
|
|
444
530
|
const normalized = Number(pid);
|
|
445
531
|
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
@@ -774,6 +860,34 @@ function formatHostForUrl(host) {
|
|
|
774
860
|
return value;
|
|
775
861
|
}
|
|
776
862
|
|
|
863
|
+
function resolveGatewayHost(config) {
|
|
864
|
+
const fromEnv = String(process.env.OPENCLAW_GATEWAY_HOST ?? "").trim();
|
|
865
|
+
if (fromEnv) {
|
|
866
|
+
return fromEnv;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const gateway = ensureObject(config?.gateway);
|
|
870
|
+
const http = ensureObject(gateway.http);
|
|
871
|
+
const bindMode = String(gateway.bind ?? "").trim().toLowerCase();
|
|
872
|
+
|
|
873
|
+
const configuredHostCandidates = [
|
|
874
|
+
gateway.host,
|
|
875
|
+
gateway.hostname,
|
|
876
|
+
http.host,
|
|
877
|
+
http.hostname,
|
|
878
|
+
bindMode === "custom" ? gateway.customBindHost : "",
|
|
879
|
+
];
|
|
880
|
+
|
|
881
|
+
for (const candidate of configuredHostCandidates) {
|
|
882
|
+
const value = String(candidate ?? "").trim();
|
|
883
|
+
if (value) {
|
|
884
|
+
return value;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return DEFAULT_GATEWAY_HOST;
|
|
889
|
+
}
|
|
890
|
+
|
|
777
891
|
function resolveGatewayPort(config) {
|
|
778
892
|
const fromEnv = parseGatewayPortEnvValue(process.env.OPENCLAW_GATEWAY_PORT);
|
|
779
893
|
if (fromEnv != null) {
|
|
@@ -809,11 +923,7 @@ function resolveGatewayBaseUrl(options, config) {
|
|
|
809
923
|
].join("\n"),
|
|
810
924
|
);
|
|
811
925
|
}
|
|
812
|
-
const
|
|
813
|
-
const customBindHost = String(config?.gateway?.customBindHost ?? "").trim();
|
|
814
|
-
const host =
|
|
815
|
-
String(process.env.OPENCLAW_GATEWAY_HOST ?? "").trim() ||
|
|
816
|
-
(bindMode === "custom" && customBindHost ? customBindHost : DEFAULT_GATEWAY_HOST);
|
|
926
|
+
const host = resolveGatewayHost(config);
|
|
817
927
|
return `http://${formatHostForUrl(host)}:${gatewayPort}`;
|
|
818
928
|
}
|
|
819
929
|
|
|
@@ -868,7 +978,7 @@ function extractErrorMessage(decoded, fallback) {
|
|
|
868
978
|
return fallback;
|
|
869
979
|
}
|
|
870
980
|
|
|
871
|
-
async function fetchWithTimeout(url, options = {}, timeoutMs =
|
|
981
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS) {
|
|
872
982
|
const controller = new AbortController();
|
|
873
983
|
const timer = setTimeout(() => controller.abort(new Error(`fetch timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
874
984
|
try {
|
|
@@ -1137,6 +1247,462 @@ function restartOpenClaw() {
|
|
|
1137
1247
|
};
|
|
1138
1248
|
}
|
|
1139
1249
|
|
|
1250
|
+
function bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }) {
|
|
1251
|
+
return [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl];
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function escapePosixShellSingleQuoted(value) {
|
|
1255
|
+
return String(value ?? "").replaceAll("'", "'\"'\"'");
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function buildPosixShellExec(command, args) {
|
|
1259
|
+
return [command, ...args]
|
|
1260
|
+
.map((part) => `'${escapePosixShellSingleQuoted(part)}'`)
|
|
1261
|
+
.join(" ");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function resolveNodeCandidates() {
|
|
1265
|
+
const homeDir = os.homedir();
|
|
1266
|
+
const candidates = new Set();
|
|
1267
|
+
const addCandidate = (candidate) => {
|
|
1268
|
+
const normalized = String(candidate ?? "").trim();
|
|
1269
|
+
if (!normalized) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
if (fs.existsSync(normalized)) {
|
|
1274
|
+
candidates.add(fs.realpathSync(normalized));
|
|
1275
|
+
}
|
|
1276
|
+
} catch {
|
|
1277
|
+
if (fs.existsSync(normalized)) {
|
|
1278
|
+
candidates.add(normalized);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
addCandidate(process.execPath);
|
|
1284
|
+
addCandidate(path.join(homeDir, ".nvm", "current", "bin", "node"));
|
|
1285
|
+
addCandidate(path.join(homeDir, ".volta", "bin", "node"));
|
|
1286
|
+
addCandidate("/opt/homebrew/bin/node");
|
|
1287
|
+
addCandidate("/usr/local/bin/node");
|
|
1288
|
+
addCandidate("/usr/bin/node");
|
|
1289
|
+
|
|
1290
|
+
const nvmVersionsDir = path.join(homeDir, ".nvm", "versions", "node");
|
|
1291
|
+
try {
|
|
1292
|
+
const nvmEntries = fs
|
|
1293
|
+
.readdirSync(nvmVersionsDir, { withFileTypes: true })
|
|
1294
|
+
.filter((entry) => entry.isDirectory())
|
|
1295
|
+
.map((entry) => entry.name)
|
|
1296
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
|
|
1297
|
+
for (const version of nvmEntries) {
|
|
1298
|
+
addCandidate(path.join(nvmVersionsDir, version, "bin", "node"));
|
|
1299
|
+
}
|
|
1300
|
+
} catch {}
|
|
1301
|
+
|
|
1302
|
+
const localNodeRoots = [
|
|
1303
|
+
path.join(homeDir, ".local"),
|
|
1304
|
+
path.join(homeDir, ".local", "node"),
|
|
1305
|
+
];
|
|
1306
|
+
for (const root of localNodeRoots) {
|
|
1307
|
+
try {
|
|
1308
|
+
const entries = fs
|
|
1309
|
+
.readdirSync(root, { withFileTypes: true })
|
|
1310
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("node-"))
|
|
1311
|
+
.map((entry) => entry.name)
|
|
1312
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
|
|
1313
|
+
for (const name of entries) {
|
|
1314
|
+
addCandidate(path.join(root, name, "bin", "node"));
|
|
1315
|
+
}
|
|
1316
|
+
} catch {}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
return [...candidates];
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function resolveBridgeServiceNodePath() {
|
|
1323
|
+
for (const candidate of resolveNodeCandidates()) {
|
|
1324
|
+
if (candidate && fs.existsSync(candidate)) {
|
|
1325
|
+
return candidate;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const whichNode = runCommand("which", ["node"]);
|
|
1329
|
+
if (whichNode.status === 0) {
|
|
1330
|
+
const resolved = String(whichNode.stdout ?? "").trim();
|
|
1331
|
+
if (resolved && fs.existsSync(resolved)) {
|
|
1332
|
+
return resolved;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
fail("node runtime is required for bridge service install");
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function writeBridgeLaunchScript(installConfig) {
|
|
1339
|
+
const scriptPath = resolveBridgeLaunchScriptPath();
|
|
1340
|
+
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
1341
|
+
const defaultPath = [
|
|
1342
|
+
"/opt/homebrew/bin",
|
|
1343
|
+
"/usr/local/bin",
|
|
1344
|
+
"/usr/bin",
|
|
1345
|
+
"/bin",
|
|
1346
|
+
"/usr/sbin",
|
|
1347
|
+
"/sbin",
|
|
1348
|
+
path.join(os.homedir(), ".volta", "bin"),
|
|
1349
|
+
path.join(os.homedir(), ".nvm", "current", "bin"),
|
|
1350
|
+
].join(":");
|
|
1351
|
+
const knownNodeCandidates = resolveNodeCandidates();
|
|
1352
|
+
const candidateChecks = knownNodeCandidates
|
|
1353
|
+
.map((candidate) => `if [ -x '${escapePosixShellSingleQuoted(candidate)}' ]; then NODE_BIN='${escapePosixShellSingleQuoted(candidate)}'; fi`)
|
|
1354
|
+
.join("\n");
|
|
1355
|
+
const fallbackLine = buildPosixShellExec("node", installConfig.args);
|
|
1356
|
+
const script = [
|
|
1357
|
+
"#!/bin/sh",
|
|
1358
|
+
"set -eu",
|
|
1359
|
+
"export PATH='" + escapePosixShellSingleQuoted(defaultPath) + ":'\"${PATH:-}\"",
|
|
1360
|
+
"NODE_BIN=''",
|
|
1361
|
+
candidateChecks,
|
|
1362
|
+
"if [ -z \"$NODE_BIN\" ] && command -v node >/dev/null 2>&1; then",
|
|
1363
|
+
" NODE_BIN=\"$(command -v node)\"",
|
|
1364
|
+
"fi",
|
|
1365
|
+
"if [ -n \"$NODE_BIN\" ]; then",
|
|
1366
|
+
` exec "$NODE_BIN" ${installConfig.args.map((arg) => `'${escapePosixShellSingleQuoted(arg)}'`).join(" ")}`,
|
|
1367
|
+
"fi",
|
|
1368
|
+
`exec ${fallbackLine}`,
|
|
1369
|
+
"",
|
|
1370
|
+
].join("\n");
|
|
1371
|
+
fs.writeFileSync(scriptPath, script, { encoding: "utf8", mode: 0o755 });
|
|
1372
|
+
return scriptPath;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
|
|
1376
|
+
if (!relayUrl) {
|
|
1377
|
+
fail("relay URL is required for bridge service");
|
|
1378
|
+
}
|
|
1379
|
+
if (!gatewayBaseUrl) {
|
|
1380
|
+
fail("gateway base URL is required for bridge service");
|
|
1381
|
+
}
|
|
1382
|
+
const logDir = resolveBridgeLogsDir();
|
|
1383
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1384
|
+
return {
|
|
1385
|
+
relayUrl,
|
|
1386
|
+
gatewayBaseUrl,
|
|
1387
|
+
logDir,
|
|
1388
|
+
stdoutLog: path.join(logDir, "bridge.stdout.log"),
|
|
1389
|
+
stderrLog: path.join(logDir, "bridge.stderr.log"),
|
|
1390
|
+
nodePath: resolveBridgeServiceNodePath(),
|
|
1391
|
+
scriptPath: CLI_ENTRY,
|
|
1392
|
+
args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function escapeXmlText(value) {
|
|
1397
|
+
return String(value)
|
|
1398
|
+
.replaceAll("&", "&")
|
|
1399
|
+
.replaceAll("<", "<")
|
|
1400
|
+
.replaceAll(">", ">");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function readMacLaunchdServiceStatus(uid, plistPath) {
|
|
1404
|
+
const refs = uid == null ? [] : [`gui/${uid}/${SERVICE_LABEL}`, `user/${uid}/${SERVICE_LABEL}`];
|
|
1405
|
+
for (const ref of refs) {
|
|
1406
|
+
const result = runCommand("launchctl", ["print", ref]);
|
|
1407
|
+
if (result.status !== 0) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
1411
|
+
const stateMatch = output.match(/state = ([^\n]+)/u);
|
|
1412
|
+
const activeCountMatch = output.match(/active count = (\d+)/u);
|
|
1413
|
+
const pidMatch = output.match(/\bpid = (\d+)/u);
|
|
1414
|
+
const exitCodeMatch = output.match(/last exit code = (\d+)/u);
|
|
1415
|
+
const state = stateMatch?.[1]?.trim() ?? "";
|
|
1416
|
+
const activeCount = Number.parseInt(activeCountMatch?.[1] ?? "0", 10) || 0;
|
|
1417
|
+
const pid = Number.parseInt(pidMatch?.[1] ?? "0", 10) || 0;
|
|
1418
|
+
const lastExitCode = Number.parseInt(exitCodeMatch?.[1] ?? "0", 10) || 0;
|
|
1419
|
+
const running =
|
|
1420
|
+
state.toLowerCase() === "running" ||
|
|
1421
|
+
activeCount > 0 ||
|
|
1422
|
+
(pid > 0 && isPidAlive(pid));
|
|
1423
|
+
return {
|
|
1424
|
+
manager: "launchd",
|
|
1425
|
+
installed: true,
|
|
1426
|
+
running,
|
|
1427
|
+
target: plistPath,
|
|
1428
|
+
logDir: resolveBridgeLogsDir(),
|
|
1429
|
+
ref,
|
|
1430
|
+
state,
|
|
1431
|
+
activeCount,
|
|
1432
|
+
pid: pid || null,
|
|
1433
|
+
lastExitCode,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
return {
|
|
1437
|
+
manager: "launchd",
|
|
1438
|
+
installed: fs.existsSync(plistPath),
|
|
1439
|
+
running: false,
|
|
1440
|
+
target: plistPath,
|
|
1441
|
+
logDir: resolveBridgeLogsDir(),
|
|
1442
|
+
ref: SERVICE_LABEL,
|
|
1443
|
+
state: "not-loaded",
|
|
1444
|
+
activeCount: 0,
|
|
1445
|
+
pid: null,
|
|
1446
|
+
lastExitCode: 0,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function installBridgeServiceMac(installConfig) {
|
|
1451
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1452
|
+
const launchScriptPath = writeBridgeLaunchScript(installConfig);
|
|
1453
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
1454
|
+
const argsXml = [launchScriptPath]
|
|
1455
|
+
.map((arg) => ` <string>${escapeXmlText(arg)}</string>`)
|
|
1456
|
+
.join("\n");
|
|
1457
|
+
const plist = [
|
|
1458
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
1459
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
1460
|
+
"<plist version=\"1.0\">",
|
|
1461
|
+
"<dict>",
|
|
1462
|
+
` <key>Label</key><string>${SERVICE_LABEL}</string>`,
|
|
1463
|
+
" <key>ProgramArguments</key>",
|
|
1464
|
+
" <array>",
|
|
1465
|
+
argsXml,
|
|
1466
|
+
" </array>",
|
|
1467
|
+
" <key>RunAtLoad</key><true/>",
|
|
1468
|
+
" <key>KeepAlive</key><true/>",
|
|
1469
|
+
` <key>WorkingDirectory</key><string>${resolveOpenClawHome()}</string>`,
|
|
1470
|
+
` <key>StandardOutPath</key><string>${installConfig.stdoutLog}</string>`,
|
|
1471
|
+
` <key>StandardErrorPath</key><string>${installConfig.stderrLog}</string>`,
|
|
1472
|
+
"</dict>",
|
|
1473
|
+
"</plist>",
|
|
1474
|
+
"",
|
|
1475
|
+
].join("\n");
|
|
1476
|
+
fs.writeFileSync(plistPath, plist, "utf8");
|
|
1477
|
+
|
|
1478
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1479
|
+
const domains = uid == null ? [] : [`gui/${uid}`, `user/${uid}`];
|
|
1480
|
+
let selectedDomain = null;
|
|
1481
|
+
for (const domain of domains) {
|
|
1482
|
+
runCommand("launchctl", ["bootout", domain, plistPath]);
|
|
1483
|
+
const bootstrap = runCommand("launchctl", ["bootstrap", domain, plistPath]);
|
|
1484
|
+
if (bootstrap.status === 0) {
|
|
1485
|
+
selectedDomain = domain;
|
|
1486
|
+
runCommand("launchctl", ["enable", `${domain}/${SERVICE_LABEL}`]);
|
|
1487
|
+
runCommand("launchctl", ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`]);
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (!selectedDomain) {
|
|
1492
|
+
const fallback = runCommand("launchctl", ["load", "-w", plistPath]);
|
|
1493
|
+
if (fallback.status !== 0) {
|
|
1494
|
+
throw new Error((fallback.stderr || fallback.stdout || "launchctl bootstrap failed").trim());
|
|
1495
|
+
}
|
|
1496
|
+
selectedDomain = "launchd";
|
|
1497
|
+
}
|
|
1498
|
+
const status = readMacLaunchdServiceStatus(uid, plistPath);
|
|
1499
|
+
if (!status.running) {
|
|
1500
|
+
const details = status.lastExitCode
|
|
1501
|
+
? `launchd last exit code: ${status.lastExitCode}`
|
|
1502
|
+
: "launchd service failed to stay running";
|
|
1503
|
+
throw new Error(details);
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
manager: "launchd",
|
|
1507
|
+
installed: true,
|
|
1508
|
+
running: true,
|
|
1509
|
+
target: plistPath,
|
|
1510
|
+
logDir: installConfig.logDir,
|
|
1511
|
+
ref: `${selectedDomain}/${SERVICE_LABEL}`,
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function installBridgeServiceLinux(installConfig) {
|
|
1516
|
+
const unitPath = resolveLinuxUserUnitPath();
|
|
1517
|
+
fs.mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
1518
|
+
const execStart = commandToString(installConfig.nodePath, installConfig.args);
|
|
1519
|
+
const unit = [
|
|
1520
|
+
"[Unit]",
|
|
1521
|
+
"Description=Matelink relay bridge",
|
|
1522
|
+
"After=network-online.target",
|
|
1523
|
+
"Wants=network-online.target",
|
|
1524
|
+
"",
|
|
1525
|
+
"[Service]",
|
|
1526
|
+
"Type=simple",
|
|
1527
|
+
`WorkingDirectory=${resolveOpenClawHome()}`,
|
|
1528
|
+
`ExecStart=${execStart}`,
|
|
1529
|
+
"Restart=always",
|
|
1530
|
+
"RestartSec=5",
|
|
1531
|
+
`StandardOutput=append:${installConfig.stdoutLog}`,
|
|
1532
|
+
`StandardError=append:${installConfig.stderrLog}`,
|
|
1533
|
+
"",
|
|
1534
|
+
"[Install]",
|
|
1535
|
+
"WantedBy=default.target",
|
|
1536
|
+
"",
|
|
1537
|
+
].join("\n");
|
|
1538
|
+
fs.writeFileSync(unitPath, unit, "utf8");
|
|
1539
|
+
const commands = [
|
|
1540
|
+
["systemctl", ["--user", "daemon-reload"]],
|
|
1541
|
+
["systemctl", ["--user", "enable", "--now", SERVICE_UNIT_NAME]],
|
|
1542
|
+
];
|
|
1543
|
+
for (const [command, args] of commands) {
|
|
1544
|
+
const result = runCommand(command, args);
|
|
1545
|
+
if (result.status !== 0) {
|
|
1546
|
+
throw new Error((result.stderr || result.stdout || `${command} failed`).trim());
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return {
|
|
1550
|
+
manager: "systemd-user",
|
|
1551
|
+
installed: true,
|
|
1552
|
+
running: true,
|
|
1553
|
+
target: unitPath,
|
|
1554
|
+
logDir: installConfig.logDir,
|
|
1555
|
+
ref: SERVICE_UNIT_NAME,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function installBridgeServiceWindows(installConfig) {
|
|
1560
|
+
const commandLine = installConfig.args.map(quoteWindowsCommandArg).join(" ");
|
|
1561
|
+
const script = [
|
|
1562
|
+
`$Action = New-ScheduledTaskAction -Execute '${installConfig.nodePath.replaceAll("'", "''")}' -Argument '${commandLine.replaceAll("'", "''")}'`,
|
|
1563
|
+
"$Trigger = New-ScheduledTaskTrigger -AtLogOn",
|
|
1564
|
+
"$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)",
|
|
1565
|
+
`Register-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Action $Action -Trigger $Trigger -Settings $Settings -Description 'Matelink relay bridge' -Force | Out-Null`,
|
|
1566
|
+
`Start-ScheduledTask -TaskName '${SERVICE_TASK_NAME}'`,
|
|
1567
|
+
].join("; ");
|
|
1568
|
+
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", script]);
|
|
1569
|
+
if (result.status !== 0) {
|
|
1570
|
+
throw new Error((result.stderr || result.stdout || "scheduled task install failed").trim());
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
manager: "task-scheduler",
|
|
1574
|
+
installed: true,
|
|
1575
|
+
running: true,
|
|
1576
|
+
target: SERVICE_TASK_NAME,
|
|
1577
|
+
logDir: installConfig.logDir,
|
|
1578
|
+
ref: SERVICE_TASK_NAME,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function installBridgeService({ relayUrl, gatewayBaseUrl }) {
|
|
1583
|
+
const installConfig = ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl });
|
|
1584
|
+
if (process.platform === "darwin") {
|
|
1585
|
+
return installBridgeServiceMac(installConfig);
|
|
1586
|
+
}
|
|
1587
|
+
if (process.platform === "linux") {
|
|
1588
|
+
return installBridgeServiceLinux(installConfig);
|
|
1589
|
+
}
|
|
1590
|
+
if (process.platform === "win32") {
|
|
1591
|
+
return installBridgeServiceWindows(installConfig);
|
|
1592
|
+
}
|
|
1593
|
+
throw new Error(`unsupported platform for service install: ${process.platform}`);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function uninstallBridgeService() {
|
|
1597
|
+
if (process.platform === "darwin") {
|
|
1598
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1599
|
+
const launchScriptPath = resolveBridgeLaunchScriptPath();
|
|
1600
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1601
|
+
if (uid != null) {
|
|
1602
|
+
runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
|
|
1603
|
+
runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
|
|
1604
|
+
}
|
|
1605
|
+
try {
|
|
1606
|
+
fs.unlinkSync(plistPath);
|
|
1607
|
+
} catch {}
|
|
1608
|
+
try {
|
|
1609
|
+
fs.unlinkSync(launchScriptPath);
|
|
1610
|
+
} catch {}
|
|
1611
|
+
return { manager: "launchd", installed: false, running: false, target: plistPath, logDir: resolveBridgeLogsDir() };
|
|
1612
|
+
}
|
|
1613
|
+
if (process.platform === "linux") {
|
|
1614
|
+
const unitPath = resolveLinuxUserUnitPath();
|
|
1615
|
+
runCommand("systemctl", ["--user", "disable", "--now", SERVICE_UNIT_NAME]);
|
|
1616
|
+
try {
|
|
1617
|
+
fs.unlinkSync(unitPath);
|
|
1618
|
+
} catch {}
|
|
1619
|
+
runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
1620
|
+
return { manager: "systemd-user", installed: false, running: false, target: unitPath, logDir: resolveBridgeLogsDir() };
|
|
1621
|
+
}
|
|
1622
|
+
if (process.platform === "win32") {
|
|
1623
|
+
runCommand("powershell.exe", ["-NoProfile", "-Command", `Unregister-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Confirm:$false -ErrorAction SilentlyContinue`]);
|
|
1624
|
+
return { manager: "task-scheduler", installed: false, running: false, target: SERVICE_TASK_NAME, logDir: resolveBridgeLogsDir() };
|
|
1625
|
+
}
|
|
1626
|
+
throw new Error(`unsupported platform for service uninstall: ${process.platform}`);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function stopBridgeService() {
|
|
1630
|
+
if (process.platform === "darwin") {
|
|
1631
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1632
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1633
|
+
if (uid != null) {
|
|
1634
|
+
runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
|
|
1635
|
+
runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
manager: "launchd",
|
|
1639
|
+
installed: fs.existsSync(plistPath),
|
|
1640
|
+
running: false,
|
|
1641
|
+
target: plistPath,
|
|
1642
|
+
logDir: resolveBridgeLogsDir(),
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
if (process.platform === "linux") {
|
|
1646
|
+
const unitPath = resolveLinuxUserUnitPath();
|
|
1647
|
+
runCommand("systemctl", ["--user", "stop", SERVICE_UNIT_NAME]);
|
|
1648
|
+
return {
|
|
1649
|
+
manager: "systemd-user",
|
|
1650
|
+
installed: fs.existsSync(unitPath),
|
|
1651
|
+
running: false,
|
|
1652
|
+
target: unitPath,
|
|
1653
|
+
logDir: resolveBridgeLogsDir(),
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
if (process.platform === "win32") {
|
|
1657
|
+
runCommand("powershell.exe", ["-NoProfile", "-Command", `Stop-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -ErrorAction SilentlyContinue`]);
|
|
1658
|
+
return {
|
|
1659
|
+
manager: "task-scheduler",
|
|
1660
|
+
installed: true,
|
|
1661
|
+
running: false,
|
|
1662
|
+
target: SERVICE_TASK_NAME,
|
|
1663
|
+
logDir: resolveBridgeLogsDir(),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir() };
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function restartBridgeService({ relayUrl, gatewayBaseUrl }) {
|
|
1670
|
+
stopBridgeService();
|
|
1671
|
+
return installBridgeService({ relayUrl, gatewayBaseUrl });
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function getBridgeServiceStatus() {
|
|
1675
|
+
if (process.platform === "darwin") {
|
|
1676
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1677
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1678
|
+
return readMacLaunchdServiceStatus(uid, plistPath);
|
|
1679
|
+
}
|
|
1680
|
+
if (process.platform === "linux") {
|
|
1681
|
+
const enabled = runCommand("systemctl", ["--user", "is-enabled", SERVICE_UNIT_NAME]);
|
|
1682
|
+
const active = runCommand("systemctl", ["--user", "is-active", SERVICE_UNIT_NAME]);
|
|
1683
|
+
return {
|
|
1684
|
+
manager: "systemd-user",
|
|
1685
|
+
installed: enabled.status === 0 || fs.existsSync(resolveLinuxUserUnitPath()),
|
|
1686
|
+
running: active.status === 0,
|
|
1687
|
+
target: resolveLinuxUserUnitPath(),
|
|
1688
|
+
logDir: resolveBridgeLogsDir(),
|
|
1689
|
+
ref: SERVICE_UNIT_NAME,
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
if (process.platform === "win32") {
|
|
1693
|
+
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", `Get-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' | Select-Object -ExpandProperty State`]);
|
|
1694
|
+
return {
|
|
1695
|
+
manager: "task-scheduler",
|
|
1696
|
+
installed: result.status === 0,
|
|
1697
|
+
running: result.status === 0 && String(result.stdout ?? "").trim().toLowerCase() === "ready",
|
|
1698
|
+
target: SERVICE_TASK_NAME,
|
|
1699
|
+
logDir: resolveBridgeLogsDir(),
|
|
1700
|
+
ref: SERVICE_TASK_NAME,
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir(), ref: null };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1140
1706
|
function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
|
|
1141
1707
|
let result;
|
|
1142
1708
|
if (process.platform === "win32") {
|
|
@@ -1173,7 +1739,11 @@ function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
|
|
|
1173
1739
|
(entry) =>
|
|
1174
1740
|
Number.isFinite(entry.pid) &&
|
|
1175
1741
|
entry.pid !== process.pid &&
|
|
1176
|
-
|
|
1742
|
+
(
|
|
1743
|
+
entry.command.includes(CLI_COMMAND_NAME) ||
|
|
1744
|
+
entry.command.includes(CLI_PACKAGE_NAME) ||
|
|
1745
|
+
entry.command.includes(path.basename(CLI_ENTRY))
|
|
1746
|
+
) &&
|
|
1177
1747
|
entry.command.includes("bridge") &&
|
|
1178
1748
|
entry.command.includes(relayUrl) &&
|
|
1179
1749
|
entry.command.includes(gatewayBaseUrl),
|
|
@@ -1195,6 +1765,29 @@ function stopBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
|
|
|
1195
1765
|
};
|
|
1196
1766
|
}
|
|
1197
1767
|
|
|
1768
|
+
function clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl }) {
|
|
1769
|
+
const serviceStatus = getBridgeServiceStatus();
|
|
1770
|
+
let serviceStopped = null;
|
|
1771
|
+
if (serviceStatus.installed) {
|
|
1772
|
+
try {
|
|
1773
|
+
serviceStopped = stopBridgeService();
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
serviceStopped = {
|
|
1776
|
+
error: String(error instanceof Error ? error.message : error),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const stoppedProcesses =
|
|
1781
|
+
relayUrl && gatewayBaseUrl
|
|
1782
|
+
? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
|
|
1783
|
+
: { count: 0, pids: [] };
|
|
1784
|
+
return {
|
|
1785
|
+
serviceStatus,
|
|
1786
|
+
serviceStopped,
|
|
1787
|
+
stoppedProcesses,
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1198
1791
|
function resolvePublishedGatewayBaseUrl({
|
|
1199
1792
|
relayUrl,
|
|
1200
1793
|
bindPublicBaseUrl,
|
|
@@ -1644,7 +2237,7 @@ async function publishRelayGatewayEvent({
|
|
|
1644
2237
|
...relayGatewayHeaders(gatewayToken),
|
|
1645
2238
|
},
|
|
1646
2239
|
body: JSON.stringify(payload),
|
|
1647
|
-
},
|
|
2240
|
+
}, DEFAULT_NETWORK_TIMEOUT_MS);
|
|
1648
2241
|
|
|
1649
2242
|
const text = await response.text();
|
|
1650
2243
|
if (!response.ok) {
|
|
@@ -1671,7 +2264,7 @@ async function publishRelayGatewayChatEvent({
|
|
|
1671
2264
|
...relayGatewayHeaders(gatewayToken),
|
|
1672
2265
|
},
|
|
1673
2266
|
body: JSON.stringify(payload ?? {}),
|
|
1674
|
-
},
|
|
2267
|
+
}, DEFAULT_NETWORK_TIMEOUT_MS);
|
|
1675
2268
|
|
|
1676
2269
|
const text = await response.text();
|
|
1677
2270
|
if (!response.ok) {
|
|
@@ -2071,7 +2664,7 @@ function createGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken }) {
|
|
|
2071
2664
|
const timer = setTimeout(() => {
|
|
2072
2665
|
pending.delete(id);
|
|
2073
2666
|
reject(new Error(`gateway rpc timeout: ${method}`));
|
|
2074
|
-
},
|
|
2667
|
+
}, DEFAULT_NETWORK_TIMEOUT_MS);
|
|
2075
2668
|
pending.set(id, {
|
|
2076
2669
|
resolve: (v) => { clearTimeout(timer); resolve(v); },
|
|
2077
2670
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
@@ -2488,7 +3081,7 @@ async function runRelayBridge({
|
|
|
2488
3081
|
relayUrl,
|
|
2489
3082
|
gatewayId,
|
|
2490
3083
|
gatewayToken,
|
|
2491
|
-
waitSeconds:
|
|
3084
|
+
waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
|
|
2492
3085
|
});
|
|
2493
3086
|
if (!request) {
|
|
2494
3087
|
continue;
|
|
@@ -2571,6 +3164,18 @@ function startRelayBridgeDetached({
|
|
|
2571
3164
|
return { started: true, alreadyRunning: false, mode: "detached" };
|
|
2572
3165
|
}
|
|
2573
3166
|
|
|
3167
|
+
function formatServiceStatusOutput(result) {
|
|
3168
|
+
return [
|
|
3169
|
+
t("service_status_manager", { value: result.manager }),
|
|
3170
|
+
t("service_status_installed", { value: result.installed ? t("yes") : t("no") }),
|
|
3171
|
+
t("service_status_running", { value: result.running ? t("yes") : t("no") }),
|
|
3172
|
+
result.target ? t("service_status_target", { value: result.target }) : null,
|
|
3173
|
+
result.logDir ? t("service_status_logs", { value: result.logDir }) : null,
|
|
3174
|
+
]
|
|
3175
|
+
.filter(Boolean)
|
|
3176
|
+
.join("\n");
|
|
3177
|
+
}
|
|
3178
|
+
|
|
2574
3179
|
async function runPair({
|
|
2575
3180
|
json,
|
|
2576
3181
|
account,
|
|
@@ -2804,22 +3409,55 @@ async function runPair({
|
|
|
2804
3409
|
publishedGatewayBaseUrl,
|
|
2805
3410
|
gatewayRestarted: Boolean(restartResult?.ok),
|
|
2806
3411
|
relayGatewayId: relayCredentials?.gatewayId ?? null,
|
|
3412
|
+
bridgeCleanup: null,
|
|
2807
3413
|
};
|
|
2808
3414
|
|
|
2809
3415
|
if (serveRelay && !json) {
|
|
3416
|
+
const bridgeCleanup = clearExistingBridgeRuntime({
|
|
3417
|
+
relayUrl,
|
|
3418
|
+
gatewayBaseUrl: localGatewayBaseUrl,
|
|
3419
|
+
});
|
|
3420
|
+
output.bridgeCleanup = {
|
|
3421
|
+
serviceInstalled: bridgeCleanup.serviceStatus?.installed === true,
|
|
3422
|
+
serviceRunning: bridgeCleanup.serviceStatus?.running === true,
|
|
3423
|
+
serviceStopError: bridgeCleanup.serviceStopped?.error ?? null,
|
|
3424
|
+
stoppedProcessCount: bridgeCleanup.stoppedProcesses?.count ?? 0,
|
|
3425
|
+
stoppedPids: bridgeCleanup.stoppedProcesses?.pids ?? [],
|
|
3426
|
+
};
|
|
2810
3427
|
try {
|
|
2811
|
-
const
|
|
3428
|
+
const serviceStatus = getBridgeServiceStatus();
|
|
3429
|
+
const serviceResult = (serviceStatus.installed ? restartBridgeService : installBridgeService)({
|
|
2812
3430
|
relayUrl,
|
|
2813
3431
|
gatewayBaseUrl: localGatewayBaseUrl,
|
|
2814
3432
|
});
|
|
2815
|
-
output.relayWorkerResult = bridgeResult;
|
|
2816
|
-
} catch (error) {
|
|
2817
3433
|
output.relayWorkerResult = {
|
|
2818
|
-
|
|
2819
|
-
started:
|
|
2820
|
-
|
|
3434
|
+
...serviceResult,
|
|
3435
|
+
started: true,
|
|
3436
|
+
alreadyRunning: serviceResult.running,
|
|
3437
|
+
mode: "service",
|
|
2821
3438
|
};
|
|
2822
|
-
console.
|
|
3439
|
+
console.log(t("service_installed"));
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
const serviceError = String(error instanceof Error ? error.message : error);
|
|
3442
|
+
console.warn(t("service_install_failed"));
|
|
3443
|
+
console.warn(serviceError);
|
|
3444
|
+
try {
|
|
3445
|
+
const bridgeResult = startRelayBridgeDetached({
|
|
3446
|
+
relayUrl,
|
|
3447
|
+
gatewayBaseUrl: localGatewayBaseUrl,
|
|
3448
|
+
});
|
|
3449
|
+
output.relayWorkerResult = {
|
|
3450
|
+
...bridgeResult,
|
|
3451
|
+
error: serviceError,
|
|
3452
|
+
};
|
|
3453
|
+
} catch (fallbackError) {
|
|
3454
|
+
output.relayWorkerResult = {
|
|
3455
|
+
mode: "detached",
|
|
3456
|
+
started: false,
|
|
3457
|
+
error: String(fallbackError instanceof Error ? fallbackError.message : fallbackError),
|
|
3458
|
+
};
|
|
3459
|
+
console.warn(`Failed to start relay bridge worker: ${output.relayWorkerResult.error}`);
|
|
3460
|
+
}
|
|
2823
3461
|
}
|
|
2824
3462
|
}
|
|
2825
3463
|
|
|
@@ -2862,6 +3500,14 @@ async function runReset({ json, relay, noRelay, gateway }) {
|
|
|
2862
3500
|
});
|
|
2863
3501
|
}
|
|
2864
3502
|
|
|
3503
|
+
let serviceReset = null;
|
|
3504
|
+
if (relayUrl && gatewayBaseUrl) {
|
|
3505
|
+
try {
|
|
3506
|
+
serviceReset = uninstallBridgeService();
|
|
3507
|
+
} catch (error) {
|
|
3508
|
+
serviceReset = { error: String(error instanceof Error ? error.message : error) };
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
2865
3511
|
const stoppedBridges =
|
|
2866
3512
|
relayUrl && gatewayBaseUrl
|
|
2867
3513
|
? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
|
|
@@ -2883,6 +3529,7 @@ async function runReset({ json, relay, noRelay, gateway }) {
|
|
|
2883
3529
|
clearedCodes: relayResetResult.clearedCodes,
|
|
2884
3530
|
},
|
|
2885
3531
|
stoppedBridgeCount: stoppedBridges.count,
|
|
3532
|
+
serviceReset,
|
|
2886
3533
|
nextGatewayId: nextState.state.relayGatewayId ?? null,
|
|
2887
3534
|
statePath: nextState.statePath,
|
|
2888
3535
|
};
|
|
@@ -2917,6 +3564,61 @@ async function runReset({ json, relay, noRelay, gateway }) {
|
|
|
2917
3564
|
console.log(t("reset_ready"));
|
|
2918
3565
|
}
|
|
2919
3566
|
|
|
3567
|
+
async function runService({ json, relay, noRelay, gateway, serviceAction }) {
|
|
3568
|
+
const configPath = resolveOpenClawConfigPath();
|
|
3569
|
+
const action = String(serviceAction ?? "status").trim().toLowerCase() || "status";
|
|
3570
|
+
const config = readJsonFileIfExists(configPath);
|
|
3571
|
+
const section = config ? readChannelSection(config) : {};
|
|
3572
|
+
const relayUrl =
|
|
3573
|
+
action === "install" || action === "restart"
|
|
3574
|
+
? resolveRelayUrl({ relay, noRelay }, section)
|
|
3575
|
+
: normalizeBaseUrl(relay) || (config ? resolveRelayUrl({ relay: null, noRelay }, section) : null);
|
|
3576
|
+
const gatewayBaseUrl =
|
|
3577
|
+
action === "install" || action === "restart"
|
|
3578
|
+
? resolveGatewayBaseUrl({ gateway }, config)
|
|
3579
|
+
: normalizeBaseUrl(gateway) || (config ? resolveGatewayBaseUrl({ gateway: null }, config) : null);
|
|
3580
|
+
|
|
3581
|
+
let result;
|
|
3582
|
+
switch (action) {
|
|
3583
|
+
case "status":
|
|
3584
|
+
result = getBridgeServiceStatus();
|
|
3585
|
+
break;
|
|
3586
|
+
case "install":
|
|
3587
|
+
clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
|
|
3588
|
+
result = installBridgeService({ relayUrl, gatewayBaseUrl });
|
|
3589
|
+
break;
|
|
3590
|
+
case "restart":
|
|
3591
|
+
clearExistingBridgeRuntime({ relayUrl, gatewayBaseUrl });
|
|
3592
|
+
result = restartBridgeService({ relayUrl, gatewayBaseUrl });
|
|
3593
|
+
break;
|
|
3594
|
+
case "uninstall":
|
|
3595
|
+
result = uninstallBridgeService();
|
|
3596
|
+
break;
|
|
3597
|
+
default:
|
|
3598
|
+
fail(`unknown service action: ${action}`);
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
const output = {
|
|
3602
|
+
command: "service",
|
|
3603
|
+
action,
|
|
3604
|
+
relayUrl: relayUrl ?? null,
|
|
3605
|
+
gatewayBaseUrl: gatewayBaseUrl ?? null,
|
|
3606
|
+
...result,
|
|
3607
|
+
};
|
|
3608
|
+
|
|
3609
|
+
if (json) {
|
|
3610
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
if (action === "uninstall") {
|
|
3615
|
+
console.log(t("service_uninstalled"));
|
|
3616
|
+
} else if (action === "install" || action === "restart") {
|
|
3617
|
+
console.log(t("service_installed"));
|
|
3618
|
+
}
|
|
3619
|
+
console.log(formatServiceStatusOutput(output));
|
|
3620
|
+
}
|
|
3621
|
+
|
|
2920
3622
|
function runSetup({
|
|
2921
3623
|
json,
|
|
2922
3624
|
account,
|
|
@@ -3192,6 +3894,7 @@ function runStatus({ json }) {
|
|
|
3192
3894
|
const config = readJsonFile(configPath);
|
|
3193
3895
|
const section = readChannelSection(config);
|
|
3194
3896
|
const creds = resolveRelayCredentialsFromSection(section);
|
|
3897
|
+
const service = getBridgeServiceStatus();
|
|
3195
3898
|
|
|
3196
3899
|
const status = {
|
|
3197
3900
|
configPath,
|
|
@@ -3213,6 +3916,10 @@ function runStatus({ json }) {
|
|
|
3213
3916
|
relayGatewayId: creds.gatewayId || null,
|
|
3214
3917
|
hasRelayClientToken: Boolean(creds.clientToken),
|
|
3215
3918
|
hasRelayGatewayToken: Boolean(creds.gatewayToken),
|
|
3919
|
+
serviceManager: service.manager,
|
|
3920
|
+
serviceInstalled: service.installed,
|
|
3921
|
+
serviceRunning: service.running,
|
|
3922
|
+
serviceTarget: service.target,
|
|
3216
3923
|
linkedUserId:
|
|
3217
3924
|
typeof section?.linkedUserId === "string" && section.linkedUserId.trim()
|
|
3218
3925
|
? section.linkedUserId.trim()
|
|
@@ -3254,6 +3961,20 @@ function runStatus({ json }) {
|
|
|
3254
3961
|
value: status.hasAccessToken ? t("set") : t("missing"),
|
|
3255
3962
|
}),
|
|
3256
3963
|
);
|
|
3964
|
+
console.log(t("service_status_manager", { value: status.serviceManager }));
|
|
3965
|
+
console.log(
|
|
3966
|
+
t("service_status_installed", {
|
|
3967
|
+
value: status.serviceInstalled ? t("yes") : t("no"),
|
|
3968
|
+
}),
|
|
3969
|
+
);
|
|
3970
|
+
console.log(
|
|
3971
|
+
t("service_status_running", {
|
|
3972
|
+
value: status.serviceRunning ? t("yes") : t("no"),
|
|
3973
|
+
}),
|
|
3974
|
+
);
|
|
3975
|
+
if (status.serviceTarget) {
|
|
3976
|
+
console.log(t("service_status_target", { value: status.serviceTarget }));
|
|
3977
|
+
}
|
|
3257
3978
|
console.log(t("status_linked_user", { value: status.linkedUserId ?? t("not_linked") }));
|
|
3258
3979
|
}
|
|
3259
3980
|
|
|
@@ -3273,6 +3994,9 @@ async function main() {
|
|
|
3273
3994
|
case "bridge":
|
|
3274
3995
|
await runBridge(options);
|
|
3275
3996
|
return;
|
|
3997
|
+
case "service":
|
|
3998
|
+
await runService(options);
|
|
3999
|
+
return;
|
|
3276
4000
|
case "pair-url":
|
|
3277
4001
|
runPairUrl(options);
|
|
3278
4002
|
return;
|