nodus-wechat 0.5.1 → 0.6.0
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 +29 -4
- package/bin/nodus-wechat.js +341 -22
- package/package.json +1 -1
- package/templates/wechat-agent-poc/poc-webhook/server.py +3 -1
- package/templates/wechat-agent-poc/scripts/logs.sh +1 -1
- package/templates/wechat-agent-poc/scripts/start.sh +48 -2
- package/templates/wechat-agent-poc/scripts/status.sh +15 -2
- package/templates/wechat-agent-poc/scripts/stop.sh +10 -1
package/README.md
CHANGED
|
@@ -14,12 +14,15 @@ npx nodus-wechat
|
|
|
14
14
|
npx nodus-wechat setup
|
|
15
15
|
npx nodus-wechat setup --install-hermes
|
|
16
16
|
npx nodus-wechat install-hermes
|
|
17
|
+
npx nodus-wechat install-openilink
|
|
17
18
|
npx nodus-wechat doctor
|
|
18
19
|
npx nodus-wechat start
|
|
20
|
+
npx nodus-wechat start --docker
|
|
19
21
|
npx nodus-wechat status
|
|
20
22
|
npx nodus-wechat logs
|
|
21
23
|
npx nodus-wechat stop
|
|
22
24
|
npx nodus-wechat uninstall --yes
|
|
25
|
+
npx nodus-wechat clean --yes
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
For a server-bound OpeniLink origin:
|
|
@@ -34,23 +37,45 @@ npx nodus-wechat setup \
|
|
|
34
37
|
The default gateway base URL is `https://api.nodus.sbs/`.
|
|
35
38
|
Use `--install-hermes` or `install-hermes` to run the official Hermes installer
|
|
36
39
|
with `--skip-setup` and the same Hermes home used by this CLI.
|
|
40
|
+
Use `install-openilink` to install the native OpeniLink Hub CLI (`oih`) through
|
|
41
|
+
the official OpeniLink installer.
|
|
37
42
|
|
|
38
43
|
## Current behavior
|
|
39
44
|
|
|
40
45
|
- Creates local configuration at `~/.nodus-wechat/config.json`.
|
|
41
46
|
- Can install Hermes Agent CLI through the official NousResearch installer.
|
|
47
|
+
- Can install OpeniLink Hub native CLI through the official OpeniLink installer.
|
|
42
48
|
- Installs the OpeniLink + webhook POC runtime at `~/.nodus-wechat/runtime`.
|
|
43
49
|
- Writes Hermes common settings to `~/.hermes/config.yaml`.
|
|
44
50
|
- Writes the AstraGate key to `~/.hermes/.env` as `ASTRAGATE_API_KEY`.
|
|
45
51
|
- Writes runtime `.env`, Docker Compose, webhook server, helper scripts, and the OpeniLink reply plugin.
|
|
46
52
|
- Stores gateway base URL, api key, model, Hermes paths, OpeniLink origin, webhook port, and runtime path.
|
|
47
|
-
- Checks Node.js, local configuration, Hermes files, runtime files, Docker Compose availability, Hermes CLI availability, and WeChat app detection with `doctor`.
|
|
48
|
-
- Starts/stops the local runtime
|
|
49
|
-
-
|
|
53
|
+
- Checks Node.js, local configuration, Hermes files, runtime files, Python, OpeniLink CLI, optional Docker Compose availability, Hermes CLI availability, and WeChat app detection with `doctor`.
|
|
54
|
+
- Starts/stops the local runtime with native local processes by default.
|
|
55
|
+
- Keeps Docker Compose available only when `--docker` is passed.
|
|
56
|
+
- Removes Nodus WeChat config/runtime files with `uninstall --yes`.
|
|
57
|
+
- Cleans Nodus WeChat config/runtime plus generated Hermes settings with `clean --yes`.
|
|
58
|
+
|
|
59
|
+
## Uninstall and clean
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx nodus-wechat stop
|
|
63
|
+
npx nodus-wechat uninstall --yes
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use `clean --yes` when you also want to remove the generated Hermes settings:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx nodus-wechat clean --yes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`clean --yes` removes `~/.nodus-wechat`, removes `ASTRAGATE_API_KEY` from
|
|
73
|
+
`~/.hermes/.env`, and removes `~/.hermes/config.yaml` only when it still matches
|
|
74
|
+
the config generated by this CLI.
|
|
50
75
|
|
|
51
76
|
## Current non-goals
|
|
52
77
|
|
|
53
|
-
- Does not run a third-party installer unless `--install-hermes` or `install-
|
|
78
|
+
- Does not run a third-party installer unless `--install-hermes`, `install-hermes`, or `install-openilink` is explicitly requested.
|
|
54
79
|
- Does not automate, inject into, read, or control WeChat directly.
|
|
55
80
|
- Does not start a daemon, LaunchAgent, or background worker outside Docker Compose.
|
|
56
81
|
- Does not redeem real CDKs or mutate sub2api accounts; the bundled webhook keeps the existing dry-run POC boundary.
|
package/bin/nodus-wechat.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const fs = require("node:fs");
|
|
6
|
+
const http = require("node:http");
|
|
6
7
|
const os = require("node:os");
|
|
7
8
|
const path = require("node:path");
|
|
8
9
|
const childProcess = require("node:child_process");
|
|
9
10
|
|
|
10
|
-
const VERSION = "0.
|
|
11
|
+
const VERSION = "0.6.0";
|
|
11
12
|
const DEFAULT_BASE_URL = "https://api.nodus.sbs/";
|
|
12
13
|
const DEFAULT_MODEL = "gpt-5.5";
|
|
13
14
|
const DEFAULT_OPENILINK_ORIGIN = "http://localhost:9800";
|
|
@@ -40,22 +41,27 @@ Usage:
|
|
|
40
41
|
[--openilink-rp-id <id>] [--webhook-port <port>]
|
|
41
42
|
[--webhook-token <token>] [--install-hermes]
|
|
42
43
|
nodus-wechat install-hermes
|
|
44
|
+
nodus-wechat install-openilink
|
|
43
45
|
nodus-wechat doctor
|
|
44
|
-
nodus-wechat start
|
|
45
|
-
nodus-wechat status
|
|
46
|
-
nodus-wechat logs
|
|
47
|
-
nodus-wechat stop
|
|
46
|
+
nodus-wechat start [--docker]
|
|
47
|
+
nodus-wechat status [--docker]
|
|
48
|
+
nodus-wechat logs [--docker]
|
|
49
|
+
nodus-wechat stop [--docker]
|
|
48
50
|
nodus-wechat uninstall --yes
|
|
51
|
+
nodus-wechat clean --yes
|
|
49
52
|
|
|
50
53
|
Commands:
|
|
51
54
|
setup Create or update local configuration and runtime files. This is the default.
|
|
52
55
|
install-hermes Install Hermes Agent CLI with the official installer.
|
|
56
|
+
install-openilink
|
|
57
|
+
Install OpeniLink Hub native CLI with the official installer.
|
|
53
58
|
doctor Check local prerequisites and configuration.
|
|
54
|
-
start Start
|
|
55
|
-
status Show
|
|
56
|
-
logs Follow
|
|
59
|
+
start Start OpeniLink + webhook with local processes by default.
|
|
60
|
+
status Show local process status.
|
|
61
|
+
logs Follow local runtime logs.
|
|
57
62
|
stop Stop the local runtime.
|
|
58
|
-
uninstall Remove
|
|
63
|
+
uninstall Remove Nodus WeChat config and runtime files.
|
|
64
|
+
clean Stop runtime if possible, then remove Nodus WeChat files and generated Hermes settings.
|
|
59
65
|
|
|
60
66
|
This version installs an OpeniLink webhook POC runtime. It does not inject into,
|
|
61
67
|
read, or control WeChat directly.`);
|
|
@@ -72,7 +78,7 @@ function parseArgs(argv) {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
const key = item.slice(2);
|
|
75
|
-
if (key === "help" || key === "yes" || key === "install-hermes") {
|
|
81
|
+
if (key === "help" || key === "yes" || key === "install-hermes" || key === "docker") {
|
|
76
82
|
result[key] = true;
|
|
77
83
|
continue;
|
|
78
84
|
}
|
|
@@ -233,6 +239,13 @@ function hermesInstallCommand() {
|
|
|
233
239
|
);
|
|
234
240
|
}
|
|
235
241
|
|
|
242
|
+
function openiLinkInstallCommand() {
|
|
243
|
+
return (
|
|
244
|
+
process.env.NODUS_WECHAT_OPENILINK_INSTALL_COMMAND ||
|
|
245
|
+
"curl -fsSL https://raw.githubusercontent.com/openilink/openilink-hub/main/install.sh | sh"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
function runHermesInstaller(hermesDir) {
|
|
237
250
|
const args = ["--skip-setup", "--hermes-home", hermesDir];
|
|
238
251
|
const command = `${hermesInstallCommand()} ${args.map(shellQuote).join(" ")}`;
|
|
@@ -376,6 +389,21 @@ function installHermes() {
|
|
|
376
389
|
return 0;
|
|
377
390
|
}
|
|
378
391
|
|
|
392
|
+
function installOpeniLink() {
|
|
393
|
+
const result = childProcess.spawnSync(openiLinkInstallCommand(), {
|
|
394
|
+
shell: true,
|
|
395
|
+
stdio: "inherit",
|
|
396
|
+
});
|
|
397
|
+
if (result.error) {
|
|
398
|
+
throw result.error;
|
|
399
|
+
}
|
|
400
|
+
if (result.status !== 0) {
|
|
401
|
+
throw new Error(`OpeniLink installer failed with exit code ${result.status}`);
|
|
402
|
+
}
|
|
403
|
+
console.log("OpeniLink installer completed. Run `nodus-wechat start`.");
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
379
407
|
function doctor() {
|
|
380
408
|
let ok = true;
|
|
381
409
|
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
@@ -415,11 +443,24 @@ function doctor() {
|
|
|
415
443
|
ok = false;
|
|
416
444
|
console.log(`runtime: missing (${config.runtime?.dir || path.join(configHome(), "runtime")})`);
|
|
417
445
|
}
|
|
446
|
+
const python = commandPath("python3") || commandPath("python");
|
|
447
|
+
if (python) {
|
|
448
|
+
console.log(`python: ok (${python})`);
|
|
449
|
+
} else {
|
|
450
|
+
ok = false;
|
|
451
|
+
console.log("python: failed (needed for local webhook runtime)");
|
|
452
|
+
}
|
|
453
|
+
const oih = commandPath("oih");
|
|
454
|
+
if (oih) {
|
|
455
|
+
console.log(`openilink cli: ok (${oih})`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log("openilink cli: missing (run `nodus-wechat install-openilink`)");
|
|
458
|
+
}
|
|
418
459
|
const docker = dockerComposeAvailable();
|
|
419
460
|
if (docker.ok) {
|
|
420
|
-
console.log(`docker compose: ok (${docker.version})`);
|
|
461
|
+
console.log(`docker compose: ok (${docker.version}; optional with --docker)`);
|
|
421
462
|
} else {
|
|
422
|
-
console.log("docker compose: missing (needed for `
|
|
463
|
+
console.log("docker compose: missing (optional; only needed for `--docker`)");
|
|
423
464
|
}
|
|
424
465
|
console.log(`openilink: ${config.openilink?.publicOrigin || DEFAULT_OPENILINK_ORIGIN}`);
|
|
425
466
|
console.log(`webhook: http://127.0.0.1:${config.webhook?.port || DEFAULT_WEBHOOK_PORT}/health`);
|
|
@@ -467,6 +508,116 @@ function dockerComposeAvailable() {
|
|
|
467
508
|
};
|
|
468
509
|
}
|
|
469
510
|
|
|
511
|
+
function commandPath(name) {
|
|
512
|
+
const result = childProcess.spawnSync("/bin/sh", ["-c", `command -v ${shellQuote(name)}`], {
|
|
513
|
+
encoding: "utf8",
|
|
514
|
+
});
|
|
515
|
+
if (result.error || result.status !== 0) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return result.stdout.trim() || null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function runtimePath(config, name) {
|
|
522
|
+
return path.join(config.runtime.dir, name);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function pidPath(config, name) {
|
|
526
|
+
return runtimePath(config, `.nodus-${name}.pid`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function logPath(config, name) {
|
|
530
|
+
return runtimePath(config, `${name}.log`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function readPid(filePath) {
|
|
534
|
+
if (!fs.existsSync(filePath)) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const pid = Number.parseInt(fs.readFileSync(filePath, "utf8"), 10);
|
|
538
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function processRunning(pid) {
|
|
542
|
+
if (!pid) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
process.kill(pid, 0);
|
|
547
|
+
return true;
|
|
548
|
+
} catch (_error) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function startManagedProcess(config, name, command, args, env) {
|
|
554
|
+
const filePath = pidPath(config, name);
|
|
555
|
+
const existingPid = readPid(filePath);
|
|
556
|
+
if (processRunning(existingPid)) {
|
|
557
|
+
console.log(`${name}: already running (pid ${existingPid})`);
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fs.mkdirSync(config.runtime.dir, { recursive: true, mode: 0o700 });
|
|
562
|
+
const out = fs.openSync(logPath(config, name), "a");
|
|
563
|
+
const child = childProcess.spawn(command, args, {
|
|
564
|
+
cwd: config.runtime.dir,
|
|
565
|
+
env,
|
|
566
|
+
detached: true,
|
|
567
|
+
stdio: ["ignore", out, out],
|
|
568
|
+
});
|
|
569
|
+
child.unref();
|
|
570
|
+
fs.writeFileSync(filePath, `${child.pid}\n`, { mode: 0o600 });
|
|
571
|
+
console.log(`${name}: started (pid ${child.pid}, log ${logPath(config, name)})`);
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function stopManagedProcess(config, name) {
|
|
576
|
+
const filePath = pidPath(config, name);
|
|
577
|
+
const pid = readPid(filePath);
|
|
578
|
+
if (!processRunning(pid)) {
|
|
579
|
+
fs.rmSync(filePath, { force: true });
|
|
580
|
+
console.log(`${name}: stopped`);
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
process.kill(-pid, "SIGTERM");
|
|
586
|
+
} catch (_error) {
|
|
587
|
+
process.kill(pid, "SIGTERM");
|
|
588
|
+
}
|
|
589
|
+
fs.rmSync(filePath, { force: true });
|
|
590
|
+
console.log(`${name}: stopped (pid ${pid})`);
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function localRuntimeEnv(config) {
|
|
595
|
+
const env = parseDotEnv(path.join(config.runtime.dir, ".env"));
|
|
596
|
+
return {
|
|
597
|
+
...process.env,
|
|
598
|
+
...env,
|
|
599
|
+
LISTEN: `:${config.openilink?.port || DEFAULT_OPENILINK_PORT}`,
|
|
600
|
+
RP_ORIGIN: config.openilink?.publicOrigin || DEFAULT_OPENILINK_ORIGIN,
|
|
601
|
+
RP_ID: config.openilink?.rpId || DEFAULT_OPENILINK_RP_ID,
|
|
602
|
+
POC_WEBHOOK_BIND: config.webhook?.bind || "127.0.0.1",
|
|
603
|
+
POC_WEBHOOK_PORT: String(config.webhook?.port || DEFAULT_WEBHOOK_PORT),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function httpGet(url, timeoutMs = 1200) {
|
|
608
|
+
return new Promise((resolve) => {
|
|
609
|
+
const request = http.get(url, { timeout: timeoutMs }, (response) => {
|
|
610
|
+
response.resume();
|
|
611
|
+
response.on("end", () => resolve({ ok: response.statusCode >= 200 && response.statusCode < 500, statusCode: response.statusCode }));
|
|
612
|
+
});
|
|
613
|
+
request.on("timeout", () => {
|
|
614
|
+
request.destroy();
|
|
615
|
+
resolve({ ok: false });
|
|
616
|
+
});
|
|
617
|
+
request.on("error", () => resolve({ ok: false }));
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
470
621
|
function loadRuntimeConfig() {
|
|
471
622
|
if (!fs.existsSync(configPath())) {
|
|
472
623
|
console.error(`Config missing: ${configPath()}`);
|
|
@@ -509,7 +660,7 @@ function runDockerCompose(config, args, stdio = "inherit") {
|
|
|
509
660
|
return result.status || 0;
|
|
510
661
|
}
|
|
511
662
|
|
|
512
|
-
function
|
|
663
|
+
function startDocker() {
|
|
513
664
|
const config = loadRuntimeConfig();
|
|
514
665
|
if (!config) {
|
|
515
666
|
return 1;
|
|
@@ -518,7 +669,39 @@ function start() {
|
|
|
518
669
|
return runDockerCompose(config, ["up", "-d"]);
|
|
519
670
|
}
|
|
520
671
|
|
|
521
|
-
function
|
|
672
|
+
function startLocal() {
|
|
673
|
+
const config = loadRuntimeConfig();
|
|
674
|
+
if (!config) {
|
|
675
|
+
return 1;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const python = commandPath("python3") || commandPath("python");
|
|
679
|
+
if (!python) {
|
|
680
|
+
console.error("Python is required for the local webhook runtime.");
|
|
681
|
+
return 1;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const oih = commandPath("oih");
|
|
685
|
+
if (!oih) {
|
|
686
|
+
console.error("OpeniLink Hub CLI `oih` is not installed.");
|
|
687
|
+
console.error("Run: nodus-wechat install-openilink");
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const env = localRuntimeEnv(config);
|
|
692
|
+
const webhookPath = path.join(config.runtime.dir, "poc-webhook", "server.py");
|
|
693
|
+
startManagedProcess(config, "webhook", python, [webhookPath], env);
|
|
694
|
+
startManagedProcess(config, "openilink", oih, [], env);
|
|
695
|
+
console.log(`OpeniLink Hub: ${config.openilink?.publicOrigin || DEFAULT_OPENILINK_ORIGIN}`);
|
|
696
|
+
console.log(`Webhook health: http://127.0.0.1:${config.webhook?.port || DEFAULT_WEBHOOK_PORT}/health`);
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function start(options) {
|
|
701
|
+
return options.docker ? startDocker() : startLocal();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function statusDocker() {
|
|
522
705
|
const config = loadRuntimeConfig();
|
|
523
706
|
if (!config) {
|
|
524
707
|
return 1;
|
|
@@ -527,7 +710,26 @@ function status() {
|
|
|
527
710
|
return runDockerCompose(config, ["ps"]);
|
|
528
711
|
}
|
|
529
712
|
|
|
530
|
-
function
|
|
713
|
+
async function statusLocal() {
|
|
714
|
+
const config = loadRuntimeConfig();
|
|
715
|
+
if (!config) {
|
|
716
|
+
return 1;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
for (const name of ["openilink", "webhook"]) {
|
|
720
|
+
const pid = readPid(pidPath(config, name));
|
|
721
|
+
console.log(`${name}: ${processRunning(pid) ? `running (pid ${pid})` : "stopped"}`);
|
|
722
|
+
}
|
|
723
|
+
const health = await httpGet(`http://127.0.0.1:${config.webhook?.port || DEFAULT_WEBHOOK_PORT}/health`);
|
|
724
|
+
console.log(`webhook health: ${health.ok ? `ok (${health.statusCode})` : "unreachable"}`);
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function status(options) {
|
|
729
|
+
return options.docker ? statusDocker() : statusLocal();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function logsDocker() {
|
|
531
733
|
const config = loadRuntimeConfig();
|
|
532
734
|
if (!config) {
|
|
533
735
|
return 1;
|
|
@@ -536,7 +738,36 @@ function logs() {
|
|
|
536
738
|
return runDockerCompose(config, ["logs", "-f", "poc-webhook"]);
|
|
537
739
|
}
|
|
538
740
|
|
|
539
|
-
function
|
|
741
|
+
function logsLocal() {
|
|
742
|
+
const config = loadRuntimeConfig();
|
|
743
|
+
if (!config) {
|
|
744
|
+
return 1;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const files = ["openilink", "webhook"].map((name) => logPath(config, name)).filter((filePath) => fs.existsSync(filePath));
|
|
748
|
+
if (files.length === 0) {
|
|
749
|
+
console.error("No local runtime logs found.");
|
|
750
|
+
return 1;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const tail = commandPath("tail");
|
|
754
|
+
if (!tail) {
|
|
755
|
+
for (const filePath of files) {
|
|
756
|
+
console.log(`==> ${filePath} <==`);
|
|
757
|
+
console.log(fs.readFileSync(filePath, "utf8"));
|
|
758
|
+
}
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const result = childProcess.spawnSync(tail, ["-f", ...files], { stdio: "inherit" });
|
|
763
|
+
return result.status || 0;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function logs(options) {
|
|
767
|
+
return options.docker ? logsDocker() : logsLocal();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function stopDocker() {
|
|
540
771
|
const config = loadRuntimeConfig();
|
|
541
772
|
if (!config) {
|
|
542
773
|
return 1;
|
|
@@ -545,6 +776,21 @@ function stop() {
|
|
|
545
776
|
return runDockerCompose(config, ["down"]);
|
|
546
777
|
}
|
|
547
778
|
|
|
779
|
+
function stopLocal() {
|
|
780
|
+
const config = loadRuntimeConfig();
|
|
781
|
+
if (!config) {
|
|
782
|
+
return 1;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
stopManagedProcess(config, "webhook");
|
|
786
|
+
stopManagedProcess(config, "openilink");
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function stop(options) {
|
|
791
|
+
return options.docker ? stopDocker() : stopLocal();
|
|
792
|
+
}
|
|
793
|
+
|
|
548
794
|
function uninstall(options) {
|
|
549
795
|
if (!options.yes) {
|
|
550
796
|
console.error("Refusing to uninstall without --yes.");
|
|
@@ -556,7 +802,65 @@ function uninstall(options) {
|
|
|
556
802
|
return 0;
|
|
557
803
|
}
|
|
558
804
|
|
|
559
|
-
function
|
|
805
|
+
function removeHermesApiKey(envPath) {
|
|
806
|
+
if (!fs.existsSync(envPath)) {
|
|
807
|
+
return "missing";
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const lines = fs.readFileSync(envPath, "utf8").split(/\r?\n/);
|
|
811
|
+
const kept = lines.filter((line) => line && !line.startsWith("ASTRAGATE_API_KEY="));
|
|
812
|
+
if (kept.length === 0) {
|
|
813
|
+
fs.rmSync(envPath, { force: true });
|
|
814
|
+
return "removed";
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
fs.writeFileSync(envPath, `${kept.join("\n")}\n`, { mode: 0o600 });
|
|
818
|
+
return "updated";
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function removeGeneratedHermesConfig(config) {
|
|
822
|
+
const hermesConfigPath = config.hermes?.configPath || path.join(hermesHome(), "config.yaml");
|
|
823
|
+
if (!fs.existsSync(hermesConfigPath)) {
|
|
824
|
+
return "missing";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const current = fs.readFileSync(hermesConfigPath, "utf8");
|
|
828
|
+
if (current !== buildHermesConfig(config)) {
|
|
829
|
+
return "kept";
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
fs.rmSync(hermesConfigPath, { force: true });
|
|
833
|
+
return "removed";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function clean(options) {
|
|
837
|
+
if (!options.yes) {
|
|
838
|
+
console.error("Refusing to clean without --yes.");
|
|
839
|
+
return 1;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const config = fs.existsSync(configPath()) ? readConfig() : null;
|
|
843
|
+
if (config) {
|
|
844
|
+
for (const name of ["webhook", "openilink"]) {
|
|
845
|
+
stopManagedProcess({ ...config, runtime: { ...config.runtime, dir: config.runtime?.dir || path.join(configHome(), "runtime") } }, name);
|
|
846
|
+
}
|
|
847
|
+
const docker = dockerComposeAvailable();
|
|
848
|
+
if (docker.ok && config.runtime?.dir && fs.existsSync(path.join(config.runtime.dir, "docker-compose.yml"))) {
|
|
849
|
+
runDockerCompose(config, ["down"], "pipe");
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const hermesConfigStatus = removeGeneratedHermesConfig(config);
|
|
853
|
+
const hermesEnvStatus = removeHermesApiKey(config.hermes?.envPath || path.join(hermesHome(), ".env"));
|
|
854
|
+
console.log(`Hermes config: ${hermesConfigStatus}`);
|
|
855
|
+
console.log(`Hermes env: ${hermesEnvStatus}`);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
fs.rmSync(configHome(), { recursive: true, force: true });
|
|
859
|
+
console.log(`Removed: ${configHome()}`);
|
|
860
|
+
return 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function main() {
|
|
560
864
|
let args;
|
|
561
865
|
try {
|
|
562
866
|
args = parseArgs(process.argv.slice(2));
|
|
@@ -581,29 +885,37 @@ function main() {
|
|
|
581
885
|
return installHermes();
|
|
582
886
|
}
|
|
583
887
|
|
|
888
|
+
if (command === "install-openilink") {
|
|
889
|
+
return installOpeniLink();
|
|
890
|
+
}
|
|
891
|
+
|
|
584
892
|
if (command === "doctor") {
|
|
585
893
|
return doctor();
|
|
586
894
|
}
|
|
587
895
|
|
|
588
896
|
if (command === "start") {
|
|
589
|
-
return start();
|
|
897
|
+
return start(args);
|
|
590
898
|
}
|
|
591
899
|
|
|
592
900
|
if (command === "status") {
|
|
593
|
-
return status();
|
|
901
|
+
return await status(args);
|
|
594
902
|
}
|
|
595
903
|
|
|
596
904
|
if (command === "logs") {
|
|
597
|
-
return logs();
|
|
905
|
+
return logs(args);
|
|
598
906
|
}
|
|
599
907
|
|
|
600
908
|
if (command === "stop") {
|
|
601
|
-
return stop();
|
|
909
|
+
return stop(args);
|
|
602
910
|
}
|
|
603
911
|
|
|
604
912
|
if (command === "uninstall") {
|
|
605
913
|
return uninstall(args);
|
|
606
914
|
}
|
|
915
|
+
|
|
916
|
+
if (command === "clean") {
|
|
917
|
+
return clean(args);
|
|
918
|
+
}
|
|
607
919
|
} catch (error) {
|
|
608
920
|
console.error(error.message);
|
|
609
921
|
return 1;
|
|
@@ -614,4 +926,11 @@ function main() {
|
|
|
614
926
|
return 1;
|
|
615
927
|
}
|
|
616
928
|
|
|
617
|
-
|
|
929
|
+
main()
|
|
930
|
+
.then((code) => {
|
|
931
|
+
process.exitCode = code;
|
|
932
|
+
})
|
|
933
|
+
.catch((error) => {
|
|
934
|
+
console.error(error.message);
|
|
935
|
+
process.exitCode = 1;
|
|
936
|
+
});
|
package/package.json
CHANGED
|
@@ -115,4 +115,6 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
if __name__ == "__main__":
|
|
118
|
-
|
|
118
|
+
bind = os.environ.get("POC_WEBHOOK_BIND", "127.0.0.1")
|
|
119
|
+
port = int(os.environ.get("POC_WEBHOOK_PORT", "9811"))
|
|
120
|
+
HTTPServer((bind, port), Handler).serve_forever()
|
|
@@ -7,5 +7,51 @@ if [ ! -f .env ]; then
|
|
|
7
7
|
cp .env.example .env
|
|
8
8
|
fi
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
set -a
|
|
11
|
+
. ./.env
|
|
12
|
+
set +a
|
|
13
|
+
|
|
14
|
+
if ! command -v oih >/dev/null 2>&1; then
|
|
15
|
+
echo "OpeniLink Hub CLI 'oih' is not installed. Run: npx nodus-wechat install-openilink" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
PYTHON_BIN="${PYTHON_BIN:-}"
|
|
20
|
+
if [ -z "$PYTHON_BIN" ]; then
|
|
21
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
22
|
+
PYTHON_BIN=python3
|
|
23
|
+
elif command -v python >/dev/null 2>&1; then
|
|
24
|
+
PYTHON_BIN=python
|
|
25
|
+
else
|
|
26
|
+
echo "Python is required for the local webhook runtime." >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
OPENILINK_PID=".nodus-openilink.pid"
|
|
32
|
+
WEBHOOK_PID=".nodus-webhook.pid"
|
|
33
|
+
|
|
34
|
+
if [ -f "$WEBHOOK_PID" ] && kill -0 "$(cat "$WEBHOOK_PID")" >/dev/null 2>&1; then
|
|
35
|
+
echo "webhook: already running (pid $(cat "$WEBHOOK_PID"))"
|
|
36
|
+
else
|
|
37
|
+
POC_WEBHOOK_BIND="${POC_WEBHOOK_BIND:-127.0.0.1}" \
|
|
38
|
+
POC_WEBHOOK_PORT="${POC_WEBHOOK_PORT:-9811}" \
|
|
39
|
+
POC_WEBHOOK_TOKEN="${POC_WEBHOOK_TOKEN:-}" \
|
|
40
|
+
"$PYTHON_BIN" poc-webhook/server.py >>webhook.log 2>&1 &
|
|
41
|
+
echo $! >"$WEBHOOK_PID"
|
|
42
|
+
echo "webhook: started (pid $(cat "$WEBHOOK_PID"))"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ -f "$OPENILINK_PID" ] && kill -0 "$(cat "$OPENILINK_PID")" >/dev/null 2>&1; then
|
|
46
|
+
echo "openilink: already running (pid $(cat "$OPENILINK_PID"))"
|
|
47
|
+
else
|
|
48
|
+
LISTEN=":${OPENILINK_PORT:-9800}" \
|
|
49
|
+
RP_ORIGIN="${OPENILINK_PUBLIC_ORIGIN:-http://localhost:9800}" \
|
|
50
|
+
RP_ID="${OPENILINK_RP_ID:-localhost}" \
|
|
51
|
+
oih >>openilink.log 2>&1 &
|
|
52
|
+
echo $! >"$OPENILINK_PID"
|
|
53
|
+
echo "openilink: started (pid $(cat "$OPENILINK_PID"))"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo "OpeniLink Hub: ${OPENILINK_PUBLIC_ORIGIN:-http://localhost:9800}"
|
|
57
|
+
echo "Webhook health: http://127.0.0.1:${POC_WEBHOOK_PORT:-9811}/health"
|
|
@@ -3,8 +3,21 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
cd "$(dirname "$0")/.."
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
for name in openilink webhook; do
|
|
7
|
+
pid_file=".nodus-${name}.pid"
|
|
8
|
+
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then
|
|
9
|
+
echo "$name: running (pid $(cat "$pid_file"))"
|
|
10
|
+
else
|
|
11
|
+
echo "$name: stopped"
|
|
12
|
+
fi
|
|
13
|
+
done
|
|
14
|
+
|
|
15
|
+
if [ -f .env ]; then
|
|
16
|
+
set -a
|
|
17
|
+
. ./.env
|
|
18
|
+
set +a
|
|
19
|
+
fi
|
|
7
20
|
printf "\nWebhook health:\n"
|
|
8
21
|
curl -fsS "http://127.0.0.1:${POC_WEBHOOK_PORT:-9811}/health" || true
|
|
9
22
|
printf "\n\nRecent webhook logs:\n"
|
|
10
|
-
|
|
23
|
+
tail -n 80 webhook.log 2>/dev/null || true
|
|
@@ -2,4 +2,13 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
cd "$(dirname "$0")/.."
|
|
5
|
-
|
|
5
|
+
for name in webhook openilink; do
|
|
6
|
+
pid_file=".nodus-${name}.pid"
|
|
7
|
+
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then
|
|
8
|
+
kill "$(cat "$pid_file")" || true
|
|
9
|
+
echo "$name: stopped (pid $(cat "$pid_file"))"
|
|
10
|
+
else
|
|
11
|
+
echo "$name: stopped"
|
|
12
|
+
fi
|
|
13
|
+
rm -f "$pid_file"
|
|
14
|
+
done
|