agent-hub-runner 0.2.0 → 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 +4 -0
- package/autostart.mjs +21 -4
- package/package.json +1 -1
- package/runner.mjs +44 -8
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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 起新版自动顶掉旧进程) -->
|
|
4
|
+
<!-- @意图#R731(2026-11-04): 常见问题加开机自启「Bootstrap failed: 5」(0.2.1 起自动处理) -->
|
|
3
5
|
<!-- @意图#R666(2026-10-29): 改成不用全局安装的 npx 一条命令(码@地址)、Mac 开机自启、stop / uninstall、常见报错 -->
|
|
4
6
|
<!-- @意图#R664(2026-10-29): npm 包 agent-hub-runner 的说明(随包发布):给客户看的安装、配对、再连、查看、断开、卸载和安全提示 -->
|
|
5
7
|
# agent-hub-runner
|
|
@@ -71,6 +73,8 @@ npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
|
71
73
|
- 「找不到服务器」「连不上服务器」「超时」:检查网络;命令要整条原样复制。
|
|
72
74
|
- 「配对码过期了 / 已经用过了 / 已经作废了」:网页上点「换一个码」再运行新命令。
|
|
73
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` 就行,它会先停掉正在跑的那个,等新版本真连上了才说「已设开机自启」。
|
|
74
78
|
|
|
75
79
|
## 断开、卸载
|
|
76
80
|
|
package/autostart.mjs
CHANGED
|
@@ -124,11 +124,22 @@ export function runLaunchctl(args, env = process.env) {
|
|
|
124
124
|
}
|
|
125
125
|
const uid = () => (typeof process.getuid === "function" ? process.getuid() : 0);
|
|
126
126
|
|
|
127
|
+
// @意图#R731(2026-11-04): plist 的 PATH 用终端里的,但去掉 npx 临时缓存、各级 node_modules/.bin、npm 自带的 node-gyp-bin 这些会变或会被清的目录;去重保序
|
|
128
|
+
export function cleanPath(p) {
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
return String(p || "").split(":").filter((d) => d && !d.includes("/_npx/") && !/\/node_modules\/\.bin\/?$/.test(d) && !d.includes("/node-gyp-bin") && !seen.has(d) && seen.add(d)).join(":") || "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 加载失败时给小白看的中文说明(带一条能直接复制的修复命令) */
|
|
134
|
+
export const bootstrapHelp = (label, raw) => `开机自启没设成:macOS 拒绝加载后台任务(${raw})。\n可以在终端里复制运行下面这条命令后,再运行一次 autostart on:\n launchctl enable gui/$(id -u)/${label}\n还不行就重启一下电脑再试;仍然失败请把这段话截图发给我们。`;
|
|
135
|
+
|
|
127
136
|
/**
|
|
128
137
|
* 开启开机自启:复制自己、写 plist、(先卸掉旧的)再加载。加载后 launchd 马上起一个后台运行器(RunAtLoad)。
|
|
129
138
|
* 返回 { label, plist, script, node, log }。
|
|
130
139
|
*/
|
|
131
|
-
|
|
140
|
+
// @意图#R731(2026-11-04): 小白遇到「任务被 macOS 记成已禁用」(以前 launchctl disable 过,删 plist 也清不掉)时 bootstrap 报 5,自己处理掉:
|
|
141
|
+
// 加载前先 launchctl enable(没禁用也无害);还失败就等 1 秒再 enable + bootstrap 一次(刚 bootout 马上 bootstrap 偶尔也报 5);仍失败给中文说明和修复命令
|
|
142
|
+
export async function autostartOn({ configFile, version, srcDir, execPath = process.execPath, env = process.env, launchctl, platform = process.platform, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
132
143
|
launchctl ??= (a) => runLaunchctl(a, env);
|
|
133
144
|
if (platform !== "darwin") throw new Error(ONLY_MAC);
|
|
134
145
|
const cfg = path.resolve(configFile);
|
|
@@ -138,14 +149,20 @@ export async function autostartOn({ configFile, version, srcDir, execPath = proc
|
|
|
138
149
|
const label = labelFor(cfg, env);
|
|
139
150
|
const log = logFileFor(cfg, env);
|
|
140
151
|
mkdirSync(path.dirname(log), { recursive: true, mode: 0o700 });
|
|
141
|
-
const vars = { AGENT_RUNNER_CONFIG: cfg, AGENT_RUNNER_LAUNCHD: "1", PATH: env.PATH
|
|
152
|
+
const vars = { AGENT_RUNNER_CONFIG: cfg, AGENT_RUNNER_LAUNCHD: "1", PATH: cleanPath(env.PATH), LANG: env.LANG || "en_US.UTF-8" };
|
|
142
153
|
if (env.AGENT_RUNNER_HOME) vars.AGENT_RUNNER_HOME = env.AGENT_RUNNER_HOME;
|
|
143
154
|
const plist = plistPathFor(cfg, env);
|
|
144
155
|
mkdirSync(path.dirname(plist), { recursive: true });
|
|
145
156
|
writeFileSync(plist, plistXml({ label, args: [node, script, "start"], env: vars, log }), { mode: 0o644 });
|
|
146
157
|
await launchctl(["bootout", `gui/${uid()}/${label}`]); // 以前加载过(升级):先卸掉,没有也不要紧
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
let r;
|
|
159
|
+
for (let i = 0; i < 2; i++) {
|
|
160
|
+
if (i) await sleep(1000);
|
|
161
|
+
await launchctl(["enable", `gui/${uid()}/${label}`]);
|
|
162
|
+
r = await launchctl(["bootstrap", `gui/${uid()}`, plist]);
|
|
163
|
+
if (r.code === 0) break;
|
|
164
|
+
}
|
|
165
|
+
if (r.code !== 0) throw new Error(bootstrapHelp(label, `launchctl bootstrap 失败:${(r.stderr || r.stdout).trim() || r.code}`));
|
|
149
166
|
return { label, plist, script, node, log };
|
|
150
167
|
}
|
|
151
168
|
|
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
|
}
|