agent-hub-runner 0.3.1 → 0.3.3
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 +2 -1
- package/autostart.mjs +11 -0
- package/package.json +1 -1
- package/runner.mjs +92 -2
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ npx -y agent-hub-runner@latest browser install # 给 bot 装它自己的浏览
|
|
|
57
57
|
npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
开机自启时,程序复制到 `~/.agent-runner/bin/<版本>/`,日志在 `~/.agent-runner/logs/runner.log`(超过 5MB
|
|
60
|
+
开机自启时,程序复制到 `~/.agent-runner/bin/<版本>/`,日志在 `~/.agent-runner/logs/runner.log`(超过 5MB 自动清空)。网页上提示「有新版本」时,0.3.2 起可以直接点「升级」;更早的版本按提示运行那一条升级命令。
|
|
61
61
|
|
|
62
62
|
仓库开发者(R699):要让仓库里的源码 `runner/runner.mjs` 直接连测试服、开机自启,用 `agent-hub/scripts/runner-test.sh autostart on|off | start | stop | status | log`(launchd 服务 `com.agenthub.runner.src`,日志 `~/.agent-runner/logs/runner-src.log`,不复制程序,改了代码 `start` 一下就生效)。普通用户不用管这个。
|
|
63
63
|
|
|
@@ -82,6 +82,7 @@ npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
|
82
82
|
- 「这个账号已经连着一台电脑了」:一个账号同时只能连一台,先在网页抽屉「连着的电脑」里点「断开」。
|
|
83
83
|
- 开机自启报「Bootstrap failed: 5: Input/output error」:这个后台任务以前被 macOS 记成了「已禁用」。0.2.1 起 `autostart on` 会自己解除并重试,直接再运行一次 `npx -y agent-hub-runner@latest autostart on` 就行;还不行就按它给的那条 `launchctl enable …` 命令运行后再试,或重启电脑再试。
|
|
84
84
|
- 升级后网页还说「运行器版本太旧」:旧版本的运行器还在后台跑着(升级前开的终端,或者旧的开机自启),新版一看有人连着就退出了。0.2.2 起新版会自己把旧版本停掉、接上服务器;从 0.2.1 或更早升上来的,运行一次 `npx -y agent-hub-runner@latest autostart on` 就行,它会先停掉正在跑的那个,等新版本真连上了才说「已设开机自启」。
|
|
85
|
+
- 网页上点「升级」(0.3.2 起):抽屉里有新版本时点一下,运行器自己去 npm 拉最新版、校验后换上并重启,不用开终端。0.3.1 及更早不认这个按钮,仍用上面那条命令升一次。
|
|
85
86
|
|
|
86
87
|
## 断开、卸载
|
|
87
88
|
|
package/autostart.mjs
CHANGED
|
@@ -116,6 +116,17 @@ export function installCopy({ srcDir, version, env = process.env, platform = pro
|
|
|
116
116
|
return path.join(cur, "runner.mjs");
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* R750:校验 npm 包的 integrity。npm 给的是 "sha512-<base64>",对不上返回 false,调用方不得替换正在跑的版本。
|
|
121
|
+
* 只认 sha512。别的算法(sha1、没有 integrity)一律 false,不将就。
|
|
122
|
+
*/
|
|
123
|
+
export function integrityOk(buf, integrity) {
|
|
124
|
+
const m = /^sha512-(.+)$/.exec(String(integrity ?? "").trim());
|
|
125
|
+
if (!m) return false;
|
|
126
|
+
const got = createHash("sha512").update(buf).digest("base64");
|
|
127
|
+
return got === m[1];
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
/** 默认的 launchctl:返回 { code, stdout, stderr },不抛错。AGENT_RUNNER_LAUNCHCTL 可换成别的程序(测试用假的,不真加载) */
|
|
120
131
|
export function runLaunchctl(args, env = process.env) {
|
|
121
132
|
return new Promise((resolve) => {
|
package/package.json
CHANGED
package/runner.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { fileURLToPath } from "node:url";
|
|
|
28
28
|
import { runOp } from "./ops.mjs";
|
|
29
29
|
import { buildChangeRecord, changesDirFor, CHANGE_ID_RE, createChangeStore } from "./changes.mjs";
|
|
30
30
|
import { browserStatus, createBrowserManager, installBrowser } from "./browser.mjs";
|
|
31
|
-
import { autostartOff, autostartOffWindows, autostartOn, autostartOnWindows, autostartStatus, autostartStatusWindows, logFileFor, ONLY_MAC, otherAutostarts, runnerHome, truncateLogIfBig } from "./autostart.mjs";
|
|
31
|
+
import { autostartOff, autostartOffWindows, autostartOn, autostartOnWindows, autostartStatus, autostartStatusWindows, COPY_FILES, installCopy, integrityOk, logFileFor, ONLY_MAC, otherAutostarts, runnerHome, truncateLogIfBig } from "./autostart.mjs";
|
|
32
32
|
|
|
33
33
|
export const PROTOCOL_VERSION = 1;
|
|
34
34
|
/** npm 包名,也是装好后的命令名 */
|
|
@@ -438,7 +438,7 @@ export function createSession(cfg, { log = say, platform = process.platform, pic
|
|
|
438
438
|
// @意图#R640(2026-10-27): hello 报 bot→目录 对照表(bot_dirs),不再报单一 dir
|
|
439
439
|
// @意图#R639(2026-10-27): 没目录也能连上;目录由 createSession 管(网页上选完马上生效),重连时 hello 带当前目录
|
|
440
440
|
// @意图#R636(2026-10-27): 前台运行、心跳、执行工具调用;断线时杀掉正在跑的命令,重连后不重放
|
|
441
|
-
export function startRunner(cfg, { log = say, retryMs = 2000, platform = process.platform, pickDir = platform === "win32" ? pickDirWindows : pickDirMac, file = configPath(), background = process.env.AGENT_RUNNER_LAUNCHD === "1" } = {}) {
|
|
441
|
+
export function startRunner(cfg, { log = say, retryMs = 2000, platform = process.platform, pickDir = platform === "win32" ? pickDirWindows : pickDirMac, file = configPath(), background = process.env.AGENT_RUNNER_LAUNCHD === "1", selfUpdate: selfUpdateFn } = {}) {
|
|
442
442
|
checkNode();
|
|
443
443
|
let fails = 0;
|
|
444
444
|
const session = createSession(cfg, { log, pickDir, platform, file });
|
|
@@ -446,12 +446,16 @@ export function startRunner(cfg, { log = say, retryMs = 2000, platform = process
|
|
|
446
446
|
let ws = null;
|
|
447
447
|
let stopped = false;
|
|
448
448
|
let beat = null;
|
|
449
|
+
let retry = null;
|
|
449
450
|
let resolveDone;
|
|
450
451
|
const done = new Promise((r) => { resolveDone = r; });
|
|
451
452
|
const finish = (why) => { if (!stopped) { stopped = true; } resolveDone(why); };
|
|
452
453
|
const send = (sock, msg) => { if (sock.readyState === WebSocket.OPEN) sock.send(JSON.stringify(msg)); };
|
|
453
454
|
const browserInfo = () => { try { const b = browserStatus(); return { installed: b.installed, version: b.version }; } catch { return { installed: false, version: null }; } };
|
|
454
455
|
|
|
456
|
+
// @意图#R750(2026-11-07): 网页点「升级」发来 self_update(不带参数)。自己查最新版、校验、换上、拉起新版,回一条结果后正常退出,
|
|
457
|
+
// 让新版接上。失败了回原因,旧版继续跑。同时只跑一个,重复发来的忽略。
|
|
458
|
+
let updating = false;
|
|
455
459
|
function connect() {
|
|
456
460
|
if (stopped) return;
|
|
457
461
|
const sock = new WebSocket(wsUrlOf(cfg.server));
|
|
@@ -471,6 +475,7 @@ export function startRunner(cfg, { log = say, retryMs = 2000, platform = process
|
|
|
471
475
|
log(n ? `已连上服务器 ${cfg.server},${n} 个 bot 选了目录。${tail}`
|
|
472
476
|
: `已连上服务器 ${cfg.server},还没有 bot 选目录:请在网页上每个 bot 的私聊里点输入框下面「我的电脑」那一行,再点「选择目录」。${tail}`);
|
|
473
477
|
} else if (msg.type === "revoked") { session.killAll(); log("网页上已断开这台电脑,令牌已作废。要再连,请在网页上重新拿配对码配对。"); }
|
|
478
|
+
else if (msg.type === "self_update") { void runSelfUpdate(sock); }
|
|
474
479
|
else void session.onMessage(msg, (m) => send(sock, m));
|
|
475
480
|
});
|
|
476
481
|
sock.addEventListener("close", (ev) => {
|
|
@@ -494,6 +499,7 @@ export function startRunner(cfg, { log = say, retryMs = 2000, platform = process
|
|
|
494
499
|
session.killAll();
|
|
495
500
|
void session.closeBrowsers(); // R729
|
|
496
501
|
clearInterval(beat);
|
|
502
|
+
clearTimeout(retry);
|
|
497
503
|
try { ws?.close(1000, "runner stopped"); } catch { /* ignore */ }
|
|
498
504
|
resolveDone("stopped");
|
|
499
505
|
};
|
|
@@ -622,6 +628,90 @@ export async function waitNewHolder(file, { version = RUNNER_VERSION, ms = 10000
|
|
|
622
628
|
}
|
|
623
629
|
}
|
|
624
630
|
|
|
631
|
+
/**
|
|
632
|
+
* R750:自己升级。版本号自己去 npm 查,不接收调用方给的版本或命令。
|
|
633
|
+
* 下载的包按 integrity 校验 sha512,对不上就不替换。
|
|
634
|
+
* fetchLatest / download / extract / spawnNext 都可以换成假的(测试用),默认走真的 npm。
|
|
635
|
+
* 返回 { ok:true, version } 或 { ok:false, error }。成功时新进程已经拉起,调用方负责退出自己。
|
|
636
|
+
*/
|
|
637
|
+
// @意图#R750(2026-11-07): 网页点「升级」,运行器自己拉最新版换上,用户不用开终端。指令不带参数,目标版本自己查,
|
|
638
|
+
// 校验不过就不动旧版。没开开机自启的也拉一个后台新进程接上,不用先 Ctrl-C。
|
|
639
|
+
export const NPM_PACK_URL = (version) => `https://registry.npmjs.org/agent-hub-runner/-/agent-hub-runner-${version}.tgz`;
|
|
640
|
+
export async function fetchLatestPack(fetchImpl = globalThis.fetch, timeoutMs = 8000) {
|
|
641
|
+
const r = await fetchImpl("https://registry.npmjs.org/agent-hub-runner/latest", { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
|
|
642
|
+
if (!r.ok) return null;
|
|
643
|
+
const b = await r.json().catch(() => null);
|
|
644
|
+
const version = typeof b?.version === "string" ? b.version : "";
|
|
645
|
+
const integrity = typeof b?.dist?.integrity === "string" ? b.dist.integrity : "";
|
|
646
|
+
if (!/^\d+\.\d+\.\d+/.test(version) || !integrity.startsWith("sha512-")) return null;
|
|
647
|
+
return { version, integrity, tarball: typeof b?.dist?.tarball === "string" ? b.dist.tarball : NPM_PACK_URL(version) };
|
|
648
|
+
}
|
|
649
|
+
/** 下载 tgz。地址只接受 registry.npmjs.org,别的一律拒绝,不跟随调用方给的地址。 */
|
|
650
|
+
export async function downloadPack(latest, fetchImpl = globalThis.fetch, timeoutMs = 20000) {
|
|
651
|
+
const url = new URL(latest.tarball);
|
|
652
|
+
if (url.protocol !== "https:" || url.hostname !== "registry.npmjs.org") throw new Error("只从 registry.npmjs.org 下载");
|
|
653
|
+
const r = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
654
|
+
if (!r.ok) throw new Error(`下载失败 ${r.status}`);
|
|
655
|
+
return Buffer.from(await r.arrayBuffer());
|
|
656
|
+
}
|
|
657
|
+
/** 解压 tgz 到临时目录,返回 package/ 子目录(npm 包的根)。只取运行器自己那几份文件,不执行包里的任何东西。 */
|
|
658
|
+
export async function extractPack(buf) {
|
|
659
|
+
const { mkdtempSync } = await import("node:fs");
|
|
660
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "agent-runner-upd-"));
|
|
661
|
+
const tar = spawn("tar", ["-xzf", "-", "-C", dir], { stdio: ["pipe", "ignore", "ignore"] });
|
|
662
|
+
const done = new Promise((resolve, reject) => { tar.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`解压失败 ${code}`))); tar.on("error", reject); });
|
|
663
|
+
tar.stdin.end(buf);
|
|
664
|
+
await done;
|
|
665
|
+
const pkg = path.join(dir, "package");
|
|
666
|
+
const { readdirSync } = await import("node:fs");
|
|
667
|
+
for (const name of readdirSync(pkg)) if (!COPY_FILES.includes(name)) rmSync(path.join(pkg, name), { recursive: true, force: true });
|
|
668
|
+
return pkg;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* 拉起新版后台进程。新版从刚装好的副本起,不从下载的临时目录起。
|
|
672
|
+
* 开机自启的交给 launchd / 计划任务重新加载(新版按 R732 顶掉旧的);没开的自己起一个后台进程,等它拿到锁。
|
|
673
|
+
*/
|
|
674
|
+
export async function spawnUpdated({ version, file, env = process.env, platform = process.platform, launchctl, schtasks, spawnFn = spawn, wait = waitNewHolder }) {
|
|
675
|
+
const script = path.join(runnerHome(env), "bin", version, "runner.mjs");
|
|
676
|
+
if (platform === "win32" && env.AGENT_RUNNER_LAUNCHD === "1") {
|
|
677
|
+
await autostartOnWindows({ configFile: file, version, srcDir: path.dirname(script), execPath: process.execPath, env, schtasks });
|
|
678
|
+
} else if (platform === "darwin" && env.AGENT_RUNNER_LAUNCHD === "1") {
|
|
679
|
+
await autostartOn({ configFile: file, version, srcDir: path.dirname(script), execPath: process.execPath, env, launchctl, platform });
|
|
680
|
+
} else {
|
|
681
|
+
const child = spawnFn(process.execPath, [script, "start"], { detached: true, stdio: "ignore", env: { ...env, AGENT_RUNNER_LAUNCHD: "" } });
|
|
682
|
+
child.unref();
|
|
683
|
+
}
|
|
684
|
+
const got = await wait(file, { version });
|
|
685
|
+
if (!got) throw new Error("新版没有接上");
|
|
686
|
+
return got;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* R750:处理一条 self_update。不看消息内容(版本号、命令都忽略),只跑 selfUpdate。
|
|
690
|
+
* 回一条 self_update_result。成功返回 true,调用方退出让新版接上;失败返回 false,旧版继续跑。
|
|
691
|
+
*/
|
|
692
|
+
export async function handleSelfUpdate({ file, platform, selfUpdate: run = selfUpdate, log = () => {} }, send) {
|
|
693
|
+
const r = await run({ file, env: process.env, platform }).catch((e) => ({ ok: false, error: e?.message ?? String(e) }));
|
|
694
|
+
send({ type: "self_update_result", ok: r.ok === true, ...(r.ok ? { version: r.version } : { error: r.error ?? "升级失败" }) });
|
|
695
|
+
if (r.ok) { log(`已换到 ${r.version},这个进程退出,新版接上`); return true; }
|
|
696
|
+
log(`升级没成:${r.error}`);
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
export async function selfUpdate({ file = configPath(), env = process.env, platform = process.platform, fetchLatest = () => fetchLatestPack(), download = downloadPack, extract = extractPack, spawnNext = spawnUpdated, currentVersion = RUNNER_VERSION } = {}) {
|
|
700
|
+
const latest = await fetchLatest().catch(() => null);
|
|
701
|
+
if (!latest) return { ok: false, error: "查不到最新版,没有更换" };
|
|
702
|
+
if (!newerVersion(latest.version, currentVersion)) return { ok: false, error: `已经是最新版(${currentVersion})` };
|
|
703
|
+
const buf = await download(latest).catch(() => null);
|
|
704
|
+
if (!buf) return { ok: false, error: "下载失败,没有更换" };
|
|
705
|
+
if (!integrityOk(buf, latest.integrity)) return { ok: false, error: "下载的包校验对不上,没有更换" };
|
|
706
|
+
let srcDir;
|
|
707
|
+
try { srcDir = await extract(buf, latest.version); } catch (e) { return { ok: false, error: `解压失败,没有更换:${e?.message ?? e}` }; }
|
|
708
|
+
installCopy({ srcDir, version: latest.version, env, platform });
|
|
709
|
+
try {
|
|
710
|
+
await spawnNext({ version: latest.version, file, env });
|
|
711
|
+
} catch (e) { return { ok: false, error: `新版没接上,没有换掉正在跑的:${e?.message ?? e}` }; }
|
|
712
|
+
return { ok: true, version: latest.version };
|
|
713
|
+
}
|
|
714
|
+
|
|
625
715
|
const here = () => path.dirname(fileURLToPath(import.meta.url));
|
|
626
716
|
// @意图#R666(2026-10-29): 开启自启:前台有一个在跑就先停掉它(不然后台那个起来就因为锁退出了),再交给 autostart.mjs(复制自己、写 plist、加载)
|
|
627
717
|
// @意图#R725(2026-11-03): Windows 走计划任务(autostartOnWindows),Mac 走 launchd。提示里的「这台 Mac」按平台写。
|