agent-hub-runner 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/runner.mjs +44 -8
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!-- @意图#R729(2026-11-04): 加 browser install / status 和「bot 的浏览器」一节(0.2.0) -->
|
|
2
2
|
<!-- @意图#R690(2026-11-01): 加「改动记录」一节:存在哪、打码、64 KB、30 天、怎么删 -->
|
|
3
|
+
<!-- @意图#R732(2026-11-04): 常见问题加「升级了网页还说版本太旧」(0.2.2 起新版自动顶掉旧进程) -->
|
|
3
4
|
<!-- @意图#R731(2026-11-04): 常见问题加开机自启「Bootstrap failed: 5」(0.2.1 起自动处理) -->
|
|
4
5
|
<!-- @意图#R666(2026-10-29): 改成不用全局安装的 npx 一条命令(码@地址)、Mac 开机自启、stop / uninstall、常见报错 -->
|
|
5
6
|
<!-- @意图#R664(2026-10-29): npm 包 agent-hub-runner 的说明(随包发布):给客户看的安装、配对、再连、查看、断开、卸载和安全提示 -->
|
|
@@ -73,6 +74,7 @@ npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
|
73
74
|
- 「配对码过期了 / 已经用过了 / 已经作废了」:网页上点「换一个码」再运行新命令。
|
|
74
75
|
- 「这个账号已经连着一台电脑了」:一个账号同时只能连一台,先在网页抽屉「连着的电脑」里点「断开」。
|
|
75
76
|
- 开机自启报「Bootstrap failed: 5: Input/output error」:这个后台任务以前被 macOS 记成了「已禁用」。0.2.1 起 `autostart on` 会自己解除并重试,直接再运行一次 `npx -y agent-hub-runner@latest autostart on` 就行;还不行就按它给的那条 `launchctl enable …` 命令运行后再试,或重启电脑再试。
|
|
77
|
+
- 升级后网页还说「运行器版本太旧」:旧版本的运行器还在后台跑着(升级前开的终端,或者旧的开机自启),新版一看有人连着就退出了。0.2.2 起新版会自己把旧版本停掉、接上服务器;从 0.2.1 或更早升上来的,运行一次 `npx -y agent-hub-runner@latest autostart on` 就行,它会先停掉正在跑的那个,等新版本真连上了才说「已设开机自启」。
|
|
76
78
|
|
|
77
79
|
## 断开、卸载
|
|
78
80
|
|
package/package.json
CHANGED
package/runner.mjs
CHANGED
|
@@ -510,12 +510,22 @@ export const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } ca
|
|
|
510
510
|
* 拿锁:别的活着的进程拿着就返回 { ok: false, pid, launchd };否则写上自己(600)返回 { ok: true, release }。
|
|
511
511
|
* 进程死了留下的旧锁直接覆盖。
|
|
512
512
|
*/
|
|
513
|
+
/** R732:a 比 b 新(只比 x.y.z;带预发布后缀的算旧一点;b 不是版本号 = 很旧) */
|
|
514
|
+
export function newerVersion(a, b) {
|
|
515
|
+
const re = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/;
|
|
516
|
+
const pa = re.exec(String(a ?? "")); const pb = re.exec(String(b ?? ""));
|
|
517
|
+
if (!pa) return false;
|
|
518
|
+
if (!pb) return true;
|
|
519
|
+
for (let i = 1; i <= 3; i++) { const d = Number(pa[i]) - Number(pb[i]); if (d) return d > 0; }
|
|
520
|
+
return !String(a).includes("-") && String(b).includes("-");
|
|
521
|
+
}
|
|
522
|
+
// @意图#R732(2026-11-04): 锁里记版本号;拿不到锁时告诉调用方对方是不是旧版本(older:锁里没版本号或比我旧),旧的由 runStart 顶掉
|
|
513
523
|
// @意图#R666(2026-10-29): 防互踢(king 定必须做):开机自启的在后台跑着,再在终端里 start 就提示「已经在后台连着」退出,不再两个互相顶
|
|
514
|
-
export function takeLock(file = configPath(), { pid = process.pid, launchd = process.env.AGENT_RUNNER_LAUNCHD === "1" } = {}) {
|
|
524
|
+
export function takeLock(file = configPath(), { pid = process.pid, launchd = process.env.AGENT_RUNNER_LAUNCHD === "1", version = RUNNER_VERSION } = {}) {
|
|
515
525
|
const cur = readLock(file);
|
|
516
|
-
if (cur && cur.pid !== pid && pidAlive(cur.pid)) return { ok: false, pid: cur.pid, launchd: !!cur.launchd };
|
|
526
|
+
if (cur && cur.pid !== pid && pidAlive(cur.pid)) return { ok: false, pid: cur.pid, launchd: !!cur.launchd, version: cur.version ?? null, older: newerVersion(version, cur.version) };
|
|
517
527
|
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
518
|
-
writeFileSync(lockPath(file), `${JSON.stringify({ pid, launchd, started_at: new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
528
|
+
writeFileSync(lockPath(file), `${JSON.stringify({ pid, launchd, version, started_at: new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
519
529
|
return { ok: true, release: () => { if (readLock(file)?.pid === pid) rmSync(lockPath(file), { force: true }); } };
|
|
520
530
|
}
|
|
521
531
|
|
|
@@ -532,6 +542,7 @@ async function autostartOffQuietly(file, deps) {
|
|
|
532
542
|
} catch { return false; }
|
|
533
543
|
}
|
|
534
544
|
|
|
545
|
+
// @意图#R732(2026-11-04): 拿锁时发现拿着锁的是旧版本(或没记版本的老运行器)就先让它退出再接管,客户升级后不用自己停旧进程;同版本或更新的照旧提示退出
|
|
535
546
|
// @意图#R666(2026-10-29): 先拿锁(后台已在跑就提示退出 0);开机自启时启动前日志超 5MB 清空、配置坏了/没配对也退出 0(不让 launchd 反复拉起);
|
|
536
547
|
// 结束一律退出码 0(Ctrl-C、被网页断开、令牌失效、被顶掉),被断开 / 令牌失效时顺手关掉自启
|
|
537
548
|
// @意图#R663(2026-10-29): 前台跑 start,Ctrl-C 断开(pair --start 和 start 共用)
|
|
@@ -544,7 +555,14 @@ async function runStart(prog = progName(), deps = {}) {
|
|
|
544
555
|
if (background) { console.error(explainError(e)); process.exit(0); }
|
|
545
556
|
throw e;
|
|
546
557
|
}
|
|
547
|
-
|
|
558
|
+
let lock = takeLock(file);
|
|
559
|
+
if (!lock.ok && lock.older) {
|
|
560
|
+
// R732:后台是旧版本(升级前起的那个还在内存里):让它退出,我来接
|
|
561
|
+
const old = lock;
|
|
562
|
+
await stopPid(old.pid);
|
|
563
|
+
lock = takeLock(file);
|
|
564
|
+
if (lock.ok) say(`已顶掉旧版本 ${old.version ?? "(0.2.1 以前)"}(进程 ${old.pid}),现在是 ${RUNNER_VERSION}`);
|
|
565
|
+
}
|
|
548
566
|
if (!lock.ok) {
|
|
549
567
|
console.log(`已经在后台连着,不用再开(进程 ${lock.pid}${lock.launchd ? ",开机自启起的" : ""})。要停掉它:${prog} stop`);
|
|
550
568
|
process.exit(0);
|
|
@@ -586,22 +604,40 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
586
604
|
async function stopHolder(file) {
|
|
587
605
|
const l = readLock(file);
|
|
588
606
|
if (!l || l.pid === process.pid || !pidAlive(l.pid)) return null;
|
|
589
|
-
|
|
590
|
-
for (let i = 0; i < 50 && pidAlive(l.pid); i++) await sleep(100);
|
|
607
|
+
await stopPid(l.pid);
|
|
591
608
|
return l;
|
|
592
609
|
}
|
|
610
|
+
/** R732:发 SIGTERM 让它正常退出(退出码 0,launchd 不会再拉起),等最多 5 秒 */
|
|
611
|
+
async function stopPid(pid) {
|
|
612
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* 已经没了 */ }
|
|
613
|
+
for (let i = 0; i < 50 && pidAlive(pid); i++) await sleep(100);
|
|
614
|
+
}
|
|
615
|
+
/** R732:等新起的后台运行器拿到锁(锁里是 version 且进程活着),最多 ms 毫秒;拿到返回锁信息,超时 null */
|
|
616
|
+
export async function waitNewHolder(file, { version = RUNNER_VERSION, ms = 10000, step = 200 } = {}) {
|
|
617
|
+
for (let t = 0; ; t += step) {
|
|
618
|
+
const l = readLock(file);
|
|
619
|
+
if (l && l.pid !== process.pid && l.version === version && pidAlive(l.pid)) return l;
|
|
620
|
+
if (t >= ms) return null;
|
|
621
|
+
await sleep(step);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
593
624
|
|
|
594
625
|
const here = () => path.dirname(fileURLToPath(import.meta.url));
|
|
595
626
|
// @意图#R666(2026-10-29): 开启自启:前台有一个在跑就先停掉它(不然后台那个起来就因为锁退出了),再交给 autostart.mjs(复制自己、写 plist、加载)
|
|
596
627
|
// @意图#R725(2026-11-03): Windows 走计划任务(autostartOnWindows),Mac 走 launchd。提示里的「这台 Mac」按平台写。
|
|
628
|
+
// @意图#R732(2026-11-04): 不管拿着锁的是终端里的还是开机自启起的(包括别的标签起的旧版本),一律先停掉;装好后等新版本真的拿到锁才报「已设开机自启」,没接上就说清楚、给日志位置
|
|
597
629
|
async function turnAutostartOn(file, prog, deps = {}) {
|
|
598
|
-
|
|
599
|
-
if (held && !held.launchd) await stopHolder(file);
|
|
630
|
+
await stopHolder(file);
|
|
600
631
|
const platform = deps.platform ?? process.platform;
|
|
601
632
|
const r = platform === "win32"
|
|
602
633
|
? await autostartOnWindows({ configFile: file, version: RUNNER_VERSION, srcDir: here(), execPath: deps.execPath, schtasks: deps.schtasks })
|
|
603
634
|
: await autostartOn({ configFile: file, version: RUNNER_VERSION, srcDir: here(), launchctl: deps.launchctl, platform });
|
|
604
635
|
const where = platform === "win32" ? "这台电脑" : "这台 Mac";
|
|
636
|
+
const got = await (deps.waitNewHolder ?? waitNewHolder)(file, { version: RUNNER_VERSION });
|
|
637
|
+
if (!got) {
|
|
638
|
+
console.log(`开机自启已经装好(${RUNNER_VERSION}),但等了 10 秒后台还没连上。可以看日志找原因:${r.log}\n过一会儿可以再运行一次:${prog} autostart on`);
|
|
639
|
+
return r;
|
|
640
|
+
}
|
|
605
641
|
console.log(`已设开机自启:以后登录${where}会自动连上,不用开终端。现在已经在后台连着,可以关掉这个终端窗口了。\n日志:${r.log}\n关掉开机自启:${prog} autostart off`);
|
|
606
642
|
return r;
|
|
607
643
|
}
|