@matelink/cli 2026.4.9 → 2026.4.11
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 +484 -22
- 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,22 @@ 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 resolveMacLaunchAgentPath() {
|
|
444
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resolveLinuxUserUnitPath() {
|
|
448
|
+
return path.join(os.homedir(), ".config", "systemd", "user", SERVICE_UNIT_NAME);
|
|
449
|
+
}
|
|
450
|
+
|
|
402
451
|
function resolveBridgeLockPath({ relayUrl, gatewayId, gatewayBaseUrl }) {
|
|
403
452
|
const digest = createHash("sha256")
|
|
404
453
|
.update(
|
|
@@ -440,6 +489,39 @@ function readOptionalJsonFile(filePath) {
|
|
|
440
489
|
}
|
|
441
490
|
}
|
|
442
491
|
|
|
492
|
+
function readJsonFileIfExists(filePath) {
|
|
493
|
+
try {
|
|
494
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function commandToString(command, args) {
|
|
501
|
+
return [command, ...args]
|
|
502
|
+
.map((part) => {
|
|
503
|
+
const value = String(part);
|
|
504
|
+
return /[\s"]/u.test(value) ? `"${value.replaceAll("\"", "\\\"")}"` : value;
|
|
505
|
+
})
|
|
506
|
+
.join(" ");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function quoteWindowsCommandArg(value) {
|
|
510
|
+
const text = String(value ?? "");
|
|
511
|
+
if (!/[\s"]/u.test(text)) {
|
|
512
|
+
return text;
|
|
513
|
+
}
|
|
514
|
+
return `"${text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function runCommand(command, args, options = {}) {
|
|
518
|
+
return spawnSync(command, args, {
|
|
519
|
+
encoding: "utf8",
|
|
520
|
+
windowsHide: process.platform === "win32",
|
|
521
|
+
...options,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
443
525
|
function isPidAlive(pid) {
|
|
444
526
|
const normalized = Number(pid);
|
|
445
527
|
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
@@ -774,6 +856,34 @@ function formatHostForUrl(host) {
|
|
|
774
856
|
return value;
|
|
775
857
|
}
|
|
776
858
|
|
|
859
|
+
function resolveGatewayHost(config) {
|
|
860
|
+
const fromEnv = String(process.env.OPENCLAW_GATEWAY_HOST ?? "").trim();
|
|
861
|
+
if (fromEnv) {
|
|
862
|
+
return fromEnv;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const gateway = ensureObject(config?.gateway);
|
|
866
|
+
const http = ensureObject(gateway.http);
|
|
867
|
+
const bindMode = String(gateway.bind ?? "").trim().toLowerCase();
|
|
868
|
+
|
|
869
|
+
const configuredHostCandidates = [
|
|
870
|
+
gateway.host,
|
|
871
|
+
gateway.hostname,
|
|
872
|
+
http.host,
|
|
873
|
+
http.hostname,
|
|
874
|
+
bindMode === "custom" ? gateway.customBindHost : "",
|
|
875
|
+
];
|
|
876
|
+
|
|
877
|
+
for (const candidate of configuredHostCandidates) {
|
|
878
|
+
const value = String(candidate ?? "").trim();
|
|
879
|
+
if (value) {
|
|
880
|
+
return value;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return DEFAULT_GATEWAY_HOST;
|
|
885
|
+
}
|
|
886
|
+
|
|
777
887
|
function resolveGatewayPort(config) {
|
|
778
888
|
const fromEnv = parseGatewayPortEnvValue(process.env.OPENCLAW_GATEWAY_PORT);
|
|
779
889
|
if (fromEnv != null) {
|
|
@@ -809,11 +919,7 @@ function resolveGatewayBaseUrl(options, config) {
|
|
|
809
919
|
].join("\n"),
|
|
810
920
|
);
|
|
811
921
|
}
|
|
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);
|
|
922
|
+
const host = resolveGatewayHost(config);
|
|
817
923
|
return `http://${formatHostForUrl(host)}:${gatewayPort}`;
|
|
818
924
|
}
|
|
819
925
|
|
|
@@ -868,7 +974,7 @@ function extractErrorMessage(decoded, fallback) {
|
|
|
868
974
|
return fallback;
|
|
869
975
|
}
|
|
870
976
|
|
|
871
|
-
async function fetchWithTimeout(url, options = {}, timeoutMs =
|
|
977
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_NETWORK_TIMEOUT_MS) {
|
|
872
978
|
const controller = new AbortController();
|
|
873
979
|
const timer = setTimeout(() => controller.abort(new Error(`fetch timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
874
980
|
try {
|
|
@@ -1137,6 +1243,242 @@ function restartOpenClaw() {
|
|
|
1137
1243
|
};
|
|
1138
1244
|
}
|
|
1139
1245
|
|
|
1246
|
+
function bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }) {
|
|
1247
|
+
return [CLI_ENTRY, "bridge", "--relay", relayUrl, "--gateway", gatewayBaseUrl];
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl }) {
|
|
1251
|
+
if (!relayUrl) {
|
|
1252
|
+
fail("relay URL is required for bridge service");
|
|
1253
|
+
}
|
|
1254
|
+
if (!gatewayBaseUrl) {
|
|
1255
|
+
fail("gateway base URL is required for bridge service");
|
|
1256
|
+
}
|
|
1257
|
+
const logDir = resolveBridgeLogsDir();
|
|
1258
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1259
|
+
return {
|
|
1260
|
+
relayUrl,
|
|
1261
|
+
gatewayBaseUrl,
|
|
1262
|
+
logDir,
|
|
1263
|
+
stdoutLog: path.join(logDir, "bridge.stdout.log"),
|
|
1264
|
+
stderrLog: path.join(logDir, "bridge.stderr.log"),
|
|
1265
|
+
nodePath: process.execPath,
|
|
1266
|
+
scriptPath: CLI_ENTRY,
|
|
1267
|
+
args: bridgeInvocationArgs({ relayUrl, gatewayBaseUrl }),
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function installBridgeServiceMac(installConfig) {
|
|
1272
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1273
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
1274
|
+
const argsXml = installConfig.args
|
|
1275
|
+
.map((arg) => ` <string>${String(arg).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")}</string>`)
|
|
1276
|
+
.join("\n");
|
|
1277
|
+
const plist = [
|
|
1278
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
1279
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
1280
|
+
"<plist version=\"1.0\">",
|
|
1281
|
+
"<dict>",
|
|
1282
|
+
` <key>Label</key><string>${SERVICE_LABEL}</string>`,
|
|
1283
|
+
" <key>ProgramArguments</key>",
|
|
1284
|
+
" <array>",
|
|
1285
|
+
argsXml,
|
|
1286
|
+
" </array>",
|
|
1287
|
+
" <key>RunAtLoad</key><true/>",
|
|
1288
|
+
" <key>KeepAlive</key><true/>",
|
|
1289
|
+
` <key>WorkingDirectory</key><string>${resolveOpenClawHome()}</string>`,
|
|
1290
|
+
` <key>StandardOutPath</key><string>${installConfig.stdoutLog}</string>`,
|
|
1291
|
+
` <key>StandardErrorPath</key><string>${installConfig.stderrLog}</string>`,
|
|
1292
|
+
"</dict>",
|
|
1293
|
+
"</plist>",
|
|
1294
|
+
"",
|
|
1295
|
+
].join("\n");
|
|
1296
|
+
fs.writeFileSync(plistPath, plist, "utf8");
|
|
1297
|
+
|
|
1298
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1299
|
+
const domains = uid == null ? [] : [`gui/${uid}`, `user/${uid}`];
|
|
1300
|
+
let selectedDomain = null;
|
|
1301
|
+
for (const domain of domains) {
|
|
1302
|
+
runCommand("launchctl", ["bootout", domain, plistPath]);
|
|
1303
|
+
const bootstrap = runCommand("launchctl", ["bootstrap", domain, plistPath]);
|
|
1304
|
+
if (bootstrap.status === 0) {
|
|
1305
|
+
selectedDomain = domain;
|
|
1306
|
+
runCommand("launchctl", ["enable", `${domain}/${SERVICE_LABEL}`]);
|
|
1307
|
+
runCommand("launchctl", ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`]);
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (!selectedDomain) {
|
|
1312
|
+
const fallback = runCommand("launchctl", ["load", "-w", plistPath]);
|
|
1313
|
+
if (fallback.status !== 0) {
|
|
1314
|
+
throw new Error((fallback.stderr || fallback.stdout || "launchctl bootstrap failed").trim());
|
|
1315
|
+
}
|
|
1316
|
+
selectedDomain = "launchd";
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
manager: "launchd",
|
|
1320
|
+
installed: true,
|
|
1321
|
+
running: true,
|
|
1322
|
+
target: plistPath,
|
|
1323
|
+
logDir: installConfig.logDir,
|
|
1324
|
+
ref: `${selectedDomain}/${SERVICE_LABEL}`,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function installBridgeServiceLinux(installConfig) {
|
|
1329
|
+
const unitPath = resolveLinuxUserUnitPath();
|
|
1330
|
+
fs.mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
1331
|
+
const execStart = commandToString(installConfig.nodePath, installConfig.args);
|
|
1332
|
+
const unit = [
|
|
1333
|
+
"[Unit]",
|
|
1334
|
+
"Description=Matelink relay bridge",
|
|
1335
|
+
"After=network-online.target",
|
|
1336
|
+
"Wants=network-online.target",
|
|
1337
|
+
"",
|
|
1338
|
+
"[Service]",
|
|
1339
|
+
"Type=simple",
|
|
1340
|
+
`WorkingDirectory=${resolveOpenClawHome()}`,
|
|
1341
|
+
`ExecStart=${execStart}`,
|
|
1342
|
+
"Restart=always",
|
|
1343
|
+
"RestartSec=5",
|
|
1344
|
+
`StandardOutput=append:${installConfig.stdoutLog}`,
|
|
1345
|
+
`StandardError=append:${installConfig.stderrLog}`,
|
|
1346
|
+
"",
|
|
1347
|
+
"[Install]",
|
|
1348
|
+
"WantedBy=default.target",
|
|
1349
|
+
"",
|
|
1350
|
+
].join("\n");
|
|
1351
|
+
fs.writeFileSync(unitPath, unit, "utf8");
|
|
1352
|
+
const commands = [
|
|
1353
|
+
["systemctl", ["--user", "daemon-reload"]],
|
|
1354
|
+
["systemctl", ["--user", "enable", "--now", SERVICE_UNIT_NAME]],
|
|
1355
|
+
];
|
|
1356
|
+
for (const [command, args] of commands) {
|
|
1357
|
+
const result = runCommand(command, args);
|
|
1358
|
+
if (result.status !== 0) {
|
|
1359
|
+
throw new Error((result.stderr || result.stdout || `${command} failed`).trim());
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
manager: "systemd-user",
|
|
1364
|
+
installed: true,
|
|
1365
|
+
running: true,
|
|
1366
|
+
target: unitPath,
|
|
1367
|
+
logDir: installConfig.logDir,
|
|
1368
|
+
ref: SERVICE_UNIT_NAME,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function installBridgeServiceWindows(installConfig) {
|
|
1373
|
+
const commandLine = installConfig.args.map(quoteWindowsCommandArg).join(" ");
|
|
1374
|
+
const script = [
|
|
1375
|
+
`$Action = New-ScheduledTaskAction -Execute '${installConfig.nodePath.replaceAll("'", "''")}' -Argument '${commandLine.replaceAll("'", "''")}'`,
|
|
1376
|
+
"$Trigger = New-ScheduledTaskTrigger -AtLogOn",
|
|
1377
|
+
"$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)",
|
|
1378
|
+
`Register-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Action $Action -Trigger $Trigger -Settings $Settings -Description 'Matelink relay bridge' -Force | Out-Null`,
|
|
1379
|
+
`Start-ScheduledTask -TaskName '${SERVICE_TASK_NAME}'`,
|
|
1380
|
+
].join("; ");
|
|
1381
|
+
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", script]);
|
|
1382
|
+
if (result.status !== 0) {
|
|
1383
|
+
throw new Error((result.stderr || result.stdout || "scheduled task install failed").trim());
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
manager: "task-scheduler",
|
|
1387
|
+
installed: true,
|
|
1388
|
+
running: true,
|
|
1389
|
+
target: SERVICE_TASK_NAME,
|
|
1390
|
+
logDir: installConfig.logDir,
|
|
1391
|
+
ref: SERVICE_TASK_NAME,
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function installBridgeService({ relayUrl, gatewayBaseUrl }) {
|
|
1396
|
+
const installConfig = ensureBridgeServiceInstallConfig({ relayUrl, gatewayBaseUrl });
|
|
1397
|
+
if (process.platform === "darwin") {
|
|
1398
|
+
return installBridgeServiceMac(installConfig);
|
|
1399
|
+
}
|
|
1400
|
+
if (process.platform === "linux") {
|
|
1401
|
+
return installBridgeServiceLinux(installConfig);
|
|
1402
|
+
}
|
|
1403
|
+
if (process.platform === "win32") {
|
|
1404
|
+
return installBridgeServiceWindows(installConfig);
|
|
1405
|
+
}
|
|
1406
|
+
throw new Error(`unsupported platform for service install: ${process.platform}`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function uninstallBridgeService() {
|
|
1410
|
+
if (process.platform === "darwin") {
|
|
1411
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1412
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1413
|
+
if (uid != null) {
|
|
1414
|
+
runCommand("launchctl", ["bootout", `gui/${uid}`, plistPath]);
|
|
1415
|
+
runCommand("launchctl", ["bootout", `user/${uid}`, plistPath]);
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
fs.unlinkSync(plistPath);
|
|
1419
|
+
} catch {}
|
|
1420
|
+
return { manager: "launchd", installed: false, running: false, target: plistPath, logDir: resolveBridgeLogsDir() };
|
|
1421
|
+
}
|
|
1422
|
+
if (process.platform === "linux") {
|
|
1423
|
+
const unitPath = resolveLinuxUserUnitPath();
|
|
1424
|
+
runCommand("systemctl", ["--user", "disable", "--now", SERVICE_UNIT_NAME]);
|
|
1425
|
+
try {
|
|
1426
|
+
fs.unlinkSync(unitPath);
|
|
1427
|
+
} catch {}
|
|
1428
|
+
runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
1429
|
+
return { manager: "systemd-user", installed: false, running: false, target: unitPath, logDir: resolveBridgeLogsDir() };
|
|
1430
|
+
}
|
|
1431
|
+
if (process.platform === "win32") {
|
|
1432
|
+
runCommand("powershell.exe", ["-NoProfile", "-Command", `Unregister-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' -Confirm:$false -ErrorAction SilentlyContinue`]);
|
|
1433
|
+
return { manager: "task-scheduler", installed: false, running: false, target: SERVICE_TASK_NAME, logDir: resolveBridgeLogsDir() };
|
|
1434
|
+
}
|
|
1435
|
+
throw new Error(`unsupported platform for service uninstall: ${process.platform}`);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function restartBridgeService({ relayUrl, gatewayBaseUrl }) {
|
|
1439
|
+
const installed = installBridgeService({ relayUrl, gatewayBaseUrl });
|
|
1440
|
+
return installed;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function getBridgeServiceStatus() {
|
|
1444
|
+
if (process.platform === "darwin") {
|
|
1445
|
+
const plistPath = resolveMacLaunchAgentPath();
|
|
1446
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1447
|
+
const refs = uid == null ? [] : [`gui/${uid}/${SERVICE_LABEL}`, `user/${uid}/${SERVICE_LABEL}`];
|
|
1448
|
+
for (const ref of refs) {
|
|
1449
|
+
const result = runCommand("launchctl", ["print", ref]);
|
|
1450
|
+
if (result.status === 0) {
|
|
1451
|
+
return { manager: "launchd", installed: true, running: true, target: plistPath, logDir: resolveBridgeLogsDir(), ref };
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return { manager: "launchd", installed: fs.existsSync(plistPath), running: false, target: plistPath, logDir: resolveBridgeLogsDir(), ref: SERVICE_LABEL };
|
|
1455
|
+
}
|
|
1456
|
+
if (process.platform === "linux") {
|
|
1457
|
+
const enabled = runCommand("systemctl", ["--user", "is-enabled", SERVICE_UNIT_NAME]);
|
|
1458
|
+
const active = runCommand("systemctl", ["--user", "is-active", SERVICE_UNIT_NAME]);
|
|
1459
|
+
return {
|
|
1460
|
+
manager: "systemd-user",
|
|
1461
|
+
installed: enabled.status === 0 || fs.existsSync(resolveLinuxUserUnitPath()),
|
|
1462
|
+
running: active.status === 0,
|
|
1463
|
+
target: resolveLinuxUserUnitPath(),
|
|
1464
|
+
logDir: resolveBridgeLogsDir(),
|
|
1465
|
+
ref: SERVICE_UNIT_NAME,
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
if (process.platform === "win32") {
|
|
1469
|
+
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", `Get-ScheduledTask -TaskName '${SERVICE_TASK_NAME}' | Select-Object -ExpandProperty State`]);
|
|
1470
|
+
return {
|
|
1471
|
+
manager: "task-scheduler",
|
|
1472
|
+
installed: result.status === 0,
|
|
1473
|
+
running: result.status === 0 && String(result.stdout ?? "").trim().toLowerCase() === "ready",
|
|
1474
|
+
target: SERVICE_TASK_NAME,
|
|
1475
|
+
logDir: resolveBridgeLogsDir(),
|
|
1476
|
+
ref: SERVICE_TASK_NAME,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
return { manager: process.platform, installed: false, running: false, target: null, logDir: resolveBridgeLogsDir(), ref: null };
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1140
1482
|
function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
|
|
1141
1483
|
let result;
|
|
1142
1484
|
if (process.platform === "win32") {
|
|
@@ -1173,7 +1515,11 @@ function listBridgeProcesses({ relayUrl, gatewayBaseUrl }) {
|
|
|
1173
1515
|
(entry) =>
|
|
1174
1516
|
Number.isFinite(entry.pid) &&
|
|
1175
1517
|
entry.pid !== process.pid &&
|
|
1176
|
-
|
|
1518
|
+
(
|
|
1519
|
+
entry.command.includes(CLI_COMMAND_NAME) ||
|
|
1520
|
+
entry.command.includes(CLI_PACKAGE_NAME) ||
|
|
1521
|
+
entry.command.includes(path.basename(CLI_ENTRY))
|
|
1522
|
+
) &&
|
|
1177
1523
|
entry.command.includes("bridge") &&
|
|
1178
1524
|
entry.command.includes(relayUrl) &&
|
|
1179
1525
|
entry.command.includes(gatewayBaseUrl),
|
|
@@ -1644,7 +1990,7 @@ async function publishRelayGatewayEvent({
|
|
|
1644
1990
|
...relayGatewayHeaders(gatewayToken),
|
|
1645
1991
|
},
|
|
1646
1992
|
body: JSON.stringify(payload),
|
|
1647
|
-
},
|
|
1993
|
+
}, DEFAULT_NETWORK_TIMEOUT_MS);
|
|
1648
1994
|
|
|
1649
1995
|
const text = await response.text();
|
|
1650
1996
|
if (!response.ok) {
|
|
@@ -1671,7 +2017,7 @@ async function publishRelayGatewayChatEvent({
|
|
|
1671
2017
|
...relayGatewayHeaders(gatewayToken),
|
|
1672
2018
|
},
|
|
1673
2019
|
body: JSON.stringify(payload ?? {}),
|
|
1674
|
-
},
|
|
2020
|
+
}, DEFAULT_NETWORK_TIMEOUT_MS);
|
|
1675
2021
|
|
|
1676
2022
|
const text = await response.text();
|
|
1677
2023
|
if (!response.ok) {
|
|
@@ -2071,7 +2417,7 @@ function createGatewayWsClient({ gatewayBaseUrl, gatewayAuthToken }) {
|
|
|
2071
2417
|
const timer = setTimeout(() => {
|
|
2072
2418
|
pending.delete(id);
|
|
2073
2419
|
reject(new Error(`gateway rpc timeout: ${method}`));
|
|
2074
|
-
},
|
|
2420
|
+
}, DEFAULT_NETWORK_TIMEOUT_MS);
|
|
2075
2421
|
pending.set(id, {
|
|
2076
2422
|
resolve: (v) => { clearTimeout(timer); resolve(v); },
|
|
2077
2423
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
@@ -2109,7 +2455,7 @@ async function callGatewayRpcLocal({
|
|
|
2109
2455
|
if (method === "__gateway.features") {
|
|
2110
2456
|
return {
|
|
2111
2457
|
ok: true,
|
|
2112
|
-
methods: ["sessions.list", "sessions.abort", "sessions.usage", "sessions.patch",
|
|
2458
|
+
methods: ["sessions.list", "sessions.abort", "sessions.delete", "sessions.usage", "sessions.patch",
|
|
2113
2459
|
"usage.cost", "agents.files.list", "agents.files.get", "agents.files.set",
|
|
2114
2460
|
"skills.status", "models.list", "chat.history", "chat.abort",
|
|
2115
2461
|
"config.get", "config.patch"],
|
|
@@ -2488,7 +2834,7 @@ async function runRelayBridge({
|
|
|
2488
2834
|
relayUrl,
|
|
2489
2835
|
gatewayId,
|
|
2490
2836
|
gatewayToken,
|
|
2491
|
-
waitSeconds:
|
|
2837
|
+
waitSeconds: DEFAULT_RELAY_WORKER_WAIT_SECONDS,
|
|
2492
2838
|
});
|
|
2493
2839
|
if (!request) {
|
|
2494
2840
|
continue;
|
|
@@ -2571,6 +2917,18 @@ function startRelayBridgeDetached({
|
|
|
2571
2917
|
return { started: true, alreadyRunning: false, mode: "detached" };
|
|
2572
2918
|
}
|
|
2573
2919
|
|
|
2920
|
+
function formatServiceStatusOutput(result) {
|
|
2921
|
+
return [
|
|
2922
|
+
t("service_status_manager", { value: result.manager }),
|
|
2923
|
+
t("service_status_installed", { value: result.installed ? t("yes") : t("no") }),
|
|
2924
|
+
t("service_status_running", { value: result.running ? t("yes") : t("no") }),
|
|
2925
|
+
result.target ? t("service_status_target", { value: result.target }) : null,
|
|
2926
|
+
result.logDir ? t("service_status_logs", { value: result.logDir }) : null,
|
|
2927
|
+
]
|
|
2928
|
+
.filter(Boolean)
|
|
2929
|
+
.join("\n");
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2574
2932
|
async function runPair({
|
|
2575
2933
|
json,
|
|
2576
2934
|
account,
|
|
@@ -2808,18 +3166,38 @@ async function runPair({
|
|
|
2808
3166
|
|
|
2809
3167
|
if (serveRelay && !json) {
|
|
2810
3168
|
try {
|
|
2811
|
-
const
|
|
3169
|
+
const serviceResult = installBridgeService({
|
|
2812
3170
|
relayUrl,
|
|
2813
3171
|
gatewayBaseUrl: localGatewayBaseUrl,
|
|
2814
3172
|
});
|
|
2815
|
-
output.relayWorkerResult = bridgeResult;
|
|
2816
|
-
} catch (error) {
|
|
2817
3173
|
output.relayWorkerResult = {
|
|
2818
|
-
|
|
2819
|
-
started:
|
|
2820
|
-
|
|
3174
|
+
...serviceResult,
|
|
3175
|
+
started: true,
|
|
3176
|
+
alreadyRunning: serviceResult.running,
|
|
3177
|
+
mode: "service",
|
|
2821
3178
|
};
|
|
2822
|
-
console.
|
|
3179
|
+
console.log(t("service_installed"));
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
const serviceError = String(error instanceof Error ? error.message : error);
|
|
3182
|
+
console.warn(t("service_install_failed"));
|
|
3183
|
+
console.warn(serviceError);
|
|
3184
|
+
try {
|
|
3185
|
+
const bridgeResult = startRelayBridgeDetached({
|
|
3186
|
+
relayUrl,
|
|
3187
|
+
gatewayBaseUrl: localGatewayBaseUrl,
|
|
3188
|
+
});
|
|
3189
|
+
output.relayWorkerResult = {
|
|
3190
|
+
...bridgeResult,
|
|
3191
|
+
error: serviceError,
|
|
3192
|
+
};
|
|
3193
|
+
} catch (fallbackError) {
|
|
3194
|
+
output.relayWorkerResult = {
|
|
3195
|
+
mode: "detached",
|
|
3196
|
+
started: false,
|
|
3197
|
+
error: String(fallbackError instanceof Error ? fallbackError.message : fallbackError),
|
|
3198
|
+
};
|
|
3199
|
+
console.warn(`Failed to start relay bridge worker: ${output.relayWorkerResult.error}`);
|
|
3200
|
+
}
|
|
2823
3201
|
}
|
|
2824
3202
|
}
|
|
2825
3203
|
|
|
@@ -2866,6 +3244,14 @@ async function runReset({ json, relay, noRelay, gateway }) {
|
|
|
2866
3244
|
relayUrl && gatewayBaseUrl
|
|
2867
3245
|
? stopBridgeProcesses({ relayUrl, gatewayBaseUrl })
|
|
2868
3246
|
: { count: 0, pids: [] };
|
|
3247
|
+
let serviceReset = null;
|
|
3248
|
+
if (relayUrl && gatewayBaseUrl) {
|
|
3249
|
+
try {
|
|
3250
|
+
serviceReset = uninstallBridgeService();
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
serviceReset = { error: String(error instanceof Error ? error.message : error) };
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
2869
3255
|
const nextState = resetLocalBindingState({ accountId });
|
|
2870
3256
|
|
|
2871
3257
|
const output = {
|
|
@@ -2883,6 +3269,7 @@ async function runReset({ json, relay, noRelay, gateway }) {
|
|
|
2883
3269
|
clearedCodes: relayResetResult.clearedCodes,
|
|
2884
3270
|
},
|
|
2885
3271
|
stoppedBridgeCount: stoppedBridges.count,
|
|
3272
|
+
serviceReset,
|
|
2886
3273
|
nextGatewayId: nextState.state.relayGatewayId ?? null,
|
|
2887
3274
|
statePath: nextState.statePath,
|
|
2888
3275
|
};
|
|
@@ -2917,6 +3304,59 @@ async function runReset({ json, relay, noRelay, gateway }) {
|
|
|
2917
3304
|
console.log(t("reset_ready"));
|
|
2918
3305
|
}
|
|
2919
3306
|
|
|
3307
|
+
async function runService({ json, relay, noRelay, gateway, serviceAction }) {
|
|
3308
|
+
const configPath = resolveOpenClawConfigPath();
|
|
3309
|
+
const action = String(serviceAction ?? "status").trim().toLowerCase() || "status";
|
|
3310
|
+
const config = readJsonFileIfExists(configPath);
|
|
3311
|
+
const section = config ? readChannelSection(config) : {};
|
|
3312
|
+
const relayUrl =
|
|
3313
|
+
action === "install" || action === "restart"
|
|
3314
|
+
? resolveRelayUrl({ relay, noRelay }, section)
|
|
3315
|
+
: normalizeBaseUrl(relay) || (config ? resolveRelayUrl({ relay: null, noRelay }, section) : null);
|
|
3316
|
+
const gatewayBaseUrl =
|
|
3317
|
+
action === "install" || action === "restart"
|
|
3318
|
+
? resolveGatewayBaseUrl({ gateway }, config)
|
|
3319
|
+
: normalizeBaseUrl(gateway) || (config ? resolveGatewayBaseUrl({ gateway: null }, config) : null);
|
|
3320
|
+
|
|
3321
|
+
let result;
|
|
3322
|
+
switch (action) {
|
|
3323
|
+
case "status":
|
|
3324
|
+
result = getBridgeServiceStatus();
|
|
3325
|
+
break;
|
|
3326
|
+
case "install":
|
|
3327
|
+
result = installBridgeService({ relayUrl, gatewayBaseUrl });
|
|
3328
|
+
break;
|
|
3329
|
+
case "restart":
|
|
3330
|
+
result = restartBridgeService({ relayUrl, gatewayBaseUrl });
|
|
3331
|
+
break;
|
|
3332
|
+
case "uninstall":
|
|
3333
|
+
result = uninstallBridgeService();
|
|
3334
|
+
break;
|
|
3335
|
+
default:
|
|
3336
|
+
fail(`unknown service action: ${action}`);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
const output = {
|
|
3340
|
+
command: "service",
|
|
3341
|
+
action,
|
|
3342
|
+
relayUrl: relayUrl ?? null,
|
|
3343
|
+
gatewayBaseUrl: gatewayBaseUrl ?? null,
|
|
3344
|
+
...result,
|
|
3345
|
+
};
|
|
3346
|
+
|
|
3347
|
+
if (json) {
|
|
3348
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
if (action === "uninstall") {
|
|
3353
|
+
console.log(t("service_uninstalled"));
|
|
3354
|
+
} else if (action === "install" || action === "restart") {
|
|
3355
|
+
console.log(t("service_installed"));
|
|
3356
|
+
}
|
|
3357
|
+
console.log(formatServiceStatusOutput(output));
|
|
3358
|
+
}
|
|
3359
|
+
|
|
2920
3360
|
function runSetup({
|
|
2921
3361
|
json,
|
|
2922
3362
|
account,
|
|
@@ -3192,6 +3632,7 @@ function runStatus({ json }) {
|
|
|
3192
3632
|
const config = readJsonFile(configPath);
|
|
3193
3633
|
const section = readChannelSection(config);
|
|
3194
3634
|
const creds = resolveRelayCredentialsFromSection(section);
|
|
3635
|
+
const service = getBridgeServiceStatus();
|
|
3195
3636
|
|
|
3196
3637
|
const status = {
|
|
3197
3638
|
configPath,
|
|
@@ -3213,6 +3654,10 @@ function runStatus({ json }) {
|
|
|
3213
3654
|
relayGatewayId: creds.gatewayId || null,
|
|
3214
3655
|
hasRelayClientToken: Boolean(creds.clientToken),
|
|
3215
3656
|
hasRelayGatewayToken: Boolean(creds.gatewayToken),
|
|
3657
|
+
serviceManager: service.manager,
|
|
3658
|
+
serviceInstalled: service.installed,
|
|
3659
|
+
serviceRunning: service.running,
|
|
3660
|
+
serviceTarget: service.target,
|
|
3216
3661
|
linkedUserId:
|
|
3217
3662
|
typeof section?.linkedUserId === "string" && section.linkedUserId.trim()
|
|
3218
3663
|
? section.linkedUserId.trim()
|
|
@@ -3254,6 +3699,20 @@ function runStatus({ json }) {
|
|
|
3254
3699
|
value: status.hasAccessToken ? t("set") : t("missing"),
|
|
3255
3700
|
}),
|
|
3256
3701
|
);
|
|
3702
|
+
console.log(t("service_status_manager", { value: status.serviceManager }));
|
|
3703
|
+
console.log(
|
|
3704
|
+
t("service_status_installed", {
|
|
3705
|
+
value: status.serviceInstalled ? t("yes") : t("no"),
|
|
3706
|
+
}),
|
|
3707
|
+
);
|
|
3708
|
+
console.log(
|
|
3709
|
+
t("service_status_running", {
|
|
3710
|
+
value: status.serviceRunning ? t("yes") : t("no"),
|
|
3711
|
+
}),
|
|
3712
|
+
);
|
|
3713
|
+
if (status.serviceTarget) {
|
|
3714
|
+
console.log(t("service_status_target", { value: status.serviceTarget }));
|
|
3715
|
+
}
|
|
3257
3716
|
console.log(t("status_linked_user", { value: status.linkedUserId ?? t("not_linked") }));
|
|
3258
3717
|
}
|
|
3259
3718
|
|
|
@@ -3273,6 +3732,9 @@ async function main() {
|
|
|
3273
3732
|
case "bridge":
|
|
3274
3733
|
await runBridge(options);
|
|
3275
3734
|
return;
|
|
3735
|
+
case "service":
|
|
3736
|
+
await runService(options);
|
|
3737
|
+
return;
|
|
3276
3738
|
case "pair-url":
|
|
3277
3739
|
runPairUrl(options);
|
|
3278
3740
|
return;
|