agent-hub-runner 0.4.0 → 0.5.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 +17 -6
- package/ops.mjs +4 -8
- package/package.json +1 -1
- package/runner.mjs +31 -7
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ agent-hub 的电脑运行器。装在你自己的电脑上,配对之后,你
|
|
|
16
16
|
## 需要
|
|
17
17
|
|
|
18
18
|
- **Node 22 或更新**(终端里运行 `node -v` 看版本)。还没装:去 [nodejs.org](https://nodejs.org) 下载 LTS 版(Mac 选 .pkg 安装包),或者 `brew install node`。
|
|
19
|
-
- **macOS**:给 bot
|
|
20
|
-
- bot
|
|
19
|
+
- **macOS / Windows**:给 bot 选自定义目录时会弹系统选文件夹窗口;macOS 和 Windows 都支持开机自启。
|
|
20
|
+
- bot 跑命令时 macOS / Linux 用 `bash`,Windows 用 PowerShell。
|
|
21
21
|
|
|
22
22
|
## 第一次连接
|
|
23
23
|
|
|
@@ -30,7 +30,15 @@ agent-hub 的电脑运行器。装在你自己的电脑上,配对之后,你
|
|
|
30
30
|
|
|
31
31
|
配对码 5 分钟有效、只能用一次;过期了在抽屉里点「换一个码」。配对成功后直接连上。
|
|
32
32
|
3. Mac 上会问「开机自动连上?(Y/n)」:回车就是「是」,以后开机自己连上,终端可以关;选「否」的话终端窗口别关。
|
|
33
|
-
4. 抽屉里第 2
|
|
33
|
+
4. 抽屉里第 2 步会自动打勾。之后到网页右侧「bot设置 → 执行环境」给这个 bot 选电脑;默认使用这台电脑的共享资料库,需要时可点「选择自定义目录」在本机弹窗里选文件夹。
|
|
34
|
+
|
|
35
|
+
## 多台电脑、共享资料库和整台电脑访问(0.5.0 起)
|
|
36
|
+
|
|
37
|
+
- 同一个账号可以连多台电脑;每台运行器有自己的配置和令牌。
|
|
38
|
+
- 每台电脑有一个**固定的共享资料库**:`~/.agent-runner/library`(Windows 是当前用户目录下的 `.agent-runner\library`)。程序自动创建,不能改成别的路径;升级、重启、自更新都不会清空。它是应用数据,不是临时缓存。
|
|
39
|
+
- bot 绑定这台电脑后默认就使用共享资料库;单个 bot 可以在网页 bot 设置里改成别的目录。
|
|
40
|
+
- 绑定后 bot 可以读写整台电脑,**不再限制在某个授权文件夹**;实际仍受运行器所在系统账号本身的权限限制。read 不弹批准,write/edit 自动允许并留记录,跑命令仍按「帮我批准命令行」决定是否要你点。只绑定你信任的电脑。
|
|
41
|
+
- 后台任务创建时冻结当时的电脑和目录;中途在网页切绑不影响旧任务,新消息和新任务用新绑定。
|
|
34
42
|
|
|
35
43
|
`@` 后面的地址默认是 https;`localhost`、`127.x` 默认 http;也可以写全 `码@http://…`。老写法 `pair <配对码> --server <地址>` 照样能用。只配对不连:加 `--no-start`。
|
|
36
44
|
|
|
@@ -63,14 +71,15 @@ npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
|
63
71
|
|
|
64
72
|
## 配置文件
|
|
65
73
|
|
|
66
|
-
- 默认在 `~/.agent-runner/config.json`(权限 600),里面有服务器地址、设备 id、长期令牌、电脑名和每个 bot
|
|
74
|
+
- 默认在 `~/.agent-runner/config.json`(权限 600),里面有服务器地址、设备 id、长期令牌、电脑名和每个 bot 的工作目录。别发给别人。
|
|
75
|
+
- 共享资料库固定在同一应用目录下的 `library/`,不写进配置,也没有修改命令。
|
|
67
76
|
- 想放别处:设环境变量 `AGENT_RUNNER_CONFIG=/某个路径/config.json`。
|
|
68
77
|
- 文件坏了:删掉它,回网页拿新配对码重新配对。
|
|
69
78
|
|
|
70
79
|
|
|
71
80
|
## 项目上下文快照(0.3.0;`.claude/skills` 起 0.3.1)
|
|
72
81
|
|
|
73
|
-
服务器在每轮主对话开始时可能调用只读操作 `context_snapshot`(没有路径参数):在**当前 bot
|
|
82
|
+
服务器在每轮主对话开始时可能调用只读操作 `context_snapshot`(没有路径参数):在**当前 bot 的默认工作目录**里读取一份 AGENTS/CLAUDE 规则,并列出 `.pi/skills`、`.agents/skills`、`.claude/skills` 下技能的名称与说明(不上传技能全文)。R758 起目录不再是权限边界,但项目规则仍只从默认工作目录开始找。基础快照需要运行器 **0.3.0+**;发现 `.claude/skills` 需要 **0.3.1+**。
|
|
74
83
|
|
|
75
84
|
## 常见问题
|
|
76
85
|
|
|
@@ -79,11 +88,13 @@ npx -y agent-hub-runner@latest browser status # 看浏览器装没装
|
|
|
79
88
|
- 「权限不够」/ `EACCES`:别用 sudo;运行 `sudo chown -R "$(whoami)" ~/.npm ~/.agent-runner` 修好再试。
|
|
80
89
|
- 「找不到服务器」「连不上服务器」「超时」:检查网络;命令要整条原样复制。
|
|
81
90
|
- 「配对码过期了 / 已经用过了 / 已经作废了」:网页上点「换一个码」再运行新命令。
|
|
82
|
-
-
|
|
91
|
+
- 「这个账号已经连着一台电脑了」:这是旧版提示;0.5.0 起一个账号可以连多台,旧版可以升级后再配。
|
|
92
|
+
- 想再加一台电脑:网页「我的电脑」抽屉里点「添加电脑」,拿新码到另一台电脑运行。
|
|
83
93
|
- 开机自启报「Bootstrap failed: 5: Input/output error」:这个后台任务以前被 macOS 记成了「已禁用」。0.2.1 起 `autostart on` 会自己解除并重试,直接再运行一次 `npx -y agent-hub-runner@latest autostart on` 就行;还不行就按它给的那条 `launchctl enable …` 命令运行后再试,或重启电脑再试。
|
|
84
94
|
- 升级后网页还说「运行器版本太旧」:旧版本的运行器还在后台跑着(升级前开的终端,或者旧的开机自启),新版一看有人连着就退出了。0.2.2 起新版会自己把旧版本停掉、接上服务器;从 0.2.1 或更早升上来的,运行一次 `npx -y agent-hub-runner@latest autostart on` 就行,它会先停掉正在跑的那个,等新版本真连上了才说「已设开机自启」。
|
|
85
95
|
- 网页上点「升级」(0.3.2 起):抽屉里有新版本时点一下,运行器自己去 npm 拉最新版、校验后换上并重启,不用开终端。0.3.1 及更早不认这个按钮,仍用上面那条命令升一次。
|
|
86
96
|
- 联网搜索(0.4.0 起):bot 设置里打开「允许联网搜索」后,bot 可以上网查资料。默认用 DuckDuckGo,不用注册、不用 key。想更稳可以在「我的电脑」抽屉里换成 Tavily 或 Brave(去官网注册,把 key 粘进来),或者填一个 SearXNG 地址。key 只存在你电脑上,服务器不留。0.3.x 及更早没有这项,抽屉里会给升级命令。
|
|
97
|
+
- 共享资料库和整台电脑访问需要 **0.5.0**;旧运行器仍能连接,但不知道默认资料库,也不会按新规则放开目录。
|
|
87
98
|
|
|
88
99
|
## 断开、卸载
|
|
89
100
|
|
package/ops.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// @意图#R636(2026-10-27): 运行器在你电脑上真正执行 read / write / edit / bash。路径只在这里判断(S6
|
|
2
|
-
//
|
|
1
|
+
// @意图#R636(2026-10-27): 运行器在你电脑上真正执行 read / write / edit / bash。路径只在这里判断(S6 结论:服务器不解析用户电脑的路径)。
|
|
2
|
+
// @意图#R758(2026-11-08): 绑定后整台电脑都可读写,工作目录只是默认 cwd(相对路径起点);仍然解开符号链接,不越过 OS 账号权限。
|
|
3
3
|
// 第一轮没有沙箱(king 定的范围):bash 里的命令本身能碰到目录外的东西,只能靠你看清批准卡片再点。
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
@@ -44,14 +44,10 @@ export async function realOf(abs, depth = 0) {
|
|
|
44
44
|
|
|
45
45
|
export const inside = (real, root) => real === root || real.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
// @意图#R636(2026-10-27): 授权目录外的路径一律拒绝
|
|
47
|
+
/** 换算成真实路径;R758 起不再限制在 root 内,root 只用来解析相对路径。 */
|
|
49
48
|
export async function resolveInside(p, root) {
|
|
50
49
|
if (typeof p !== "string" || !p.trim()) throw new Error("路径是空的");
|
|
51
|
-
|
|
52
|
-
try { real = await realOf(expandPath(p, root)); } catch (e) { throw new Error(`无法解析路径:${p}(${e?.code ?? e?.message ?? e})`); }
|
|
53
|
-
if (!inside(real, root)) throw new Error(`目录外路径已拒绝:${p}(授权目录是 ${root})`);
|
|
54
|
-
return real;
|
|
50
|
+
try { return await realOf(expandPath(p, root)); } catch (e) { throw new Error(`无法解析路径:${p}(${e?.code ?? e?.message ?? e})`); }
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
function imageMime(buf) {
|
package/package.json
CHANGED
package/runner.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { browserStatus, createBrowserManager, installBrowser } from "./browser.m
|
|
|
31
31
|
import { searchWeb, cleanSearchConfig, searchConfigStatus } from "./search.mjs"; // R751
|
|
32
32
|
import { autostartOff, autostartOffWindows, autostartOn, autostartOnWindows, autostartStatus, autostartStatusWindows, COPY_FILES, installCopy, integrityOk, logFileFor, ONLY_MAC, otherAutostarts, runnerHome, truncateLogIfBig } from "./autostart.mjs";
|
|
33
33
|
|
|
34
|
-
export const PROTOCOL_VERSION =
|
|
34
|
+
export const PROTOCOL_VERSION = 2;
|
|
35
35
|
/** npm 包名,也是装好后的命令名 */
|
|
36
36
|
export const PKG_BIN = "agent-hub-runner";
|
|
37
37
|
/** R666:抽屉里给的、npx 运行时提示里写的命令前缀(@latest:每次都用最新版,也不会被电脑上旧的全局安装盖掉) */
|
|
@@ -50,6 +50,8 @@ export const CLOSE_REPLACED = 4409;
|
|
|
50
50
|
|
|
51
51
|
// @意图#R666(2026-10-29): 默认目录可用 AGENT_RUNNER_HOME 换(测试、开机自启的副本用);不设时和原来一样是 ~/.agent-runner/config.json
|
|
52
52
|
export const configPath = () => process.env.AGENT_RUNNER_CONFIG || path.join(runnerHome(), "config.json");
|
|
53
|
+
/** R758:这台电脑固定的共享资料库(程序自己管理,不提供修改入口);测试可用 AGENT_RUNNER_HOME 隔离。 */
|
|
54
|
+
export const libraryDir = (env = process.env) => path.join(runnerHome(env), "library");
|
|
53
55
|
|
|
54
56
|
const stamp = () => new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
55
57
|
const say = (s) => console.log(`[${stamp()}] ${s}`);
|
|
@@ -188,9 +190,8 @@ export function pickDirWindows({ signal, spawn: spawnFn = spawn } = {}) {
|
|
|
188
190
|
|
|
189
191
|
// @意图#R639(2026-10-27): 配对时的 --dir 和网页上选的目录走同一套检查
|
|
190
192
|
export async function checkDir(dir) {
|
|
191
|
-
const real = await fs.realpath(path.resolve(dir)).catch(() => { throw new Error(
|
|
192
|
-
if (!statSync(real).isDirectory()) throw new Error(
|
|
193
|
-
if (real === path.parse(real).root) throw new Error("不能把整个磁盘根目录授权出去");
|
|
193
|
+
const real = await fs.realpath(path.resolve(dir)).catch(() => { throw new Error(`工作目录不存在:${dir}`); });
|
|
194
|
+
if (!statSync(real).isDirectory()) throw new Error(`工作目录不是目录:${dir}`);
|
|
194
195
|
return real;
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -309,7 +310,8 @@ const wsUrlOf = (server) => `${server.replace(/^http/, "ws")}/devices/ws`;
|
|
|
309
310
|
// 浏览器不看这个 bot 有没有选目录(资料目录在 ~/.agent-runner/browser-profiles/<bot_id>/,和授权目录无关);断线不关浏览器(重连后接着用)
|
|
310
311
|
export function createSession(cfg, { log = say, platform = process.platform, pickDir = platform === "win32" ? pickDirWindows : pickDirMac, file = configPath(), changes, browser, search } = {}) {
|
|
311
312
|
const searchImpl = search ?? searchWeb; // R751:测试注入,默认真搜
|
|
312
|
-
|
|
313
|
+
const sharedLibrary = libraryDir();
|
|
314
|
+
let dirs = botDirsOf(cfg); // bot_id → 工作目录
|
|
313
315
|
let browsers = browser; // R729:每个 bot 一只浏览器,用到时才建
|
|
314
316
|
const browserMgr = () => (browsers ??= createBrowserManager({ log }));
|
|
315
317
|
const jobs = new Map(); // 调用 id → AbortController
|
|
@@ -332,8 +334,11 @@ export function createSession(cfg, { log = say, platform = process.platform, pic
|
|
|
332
334
|
async function handleCall(send, msg) {
|
|
333
335
|
const { id, op, args } = msg;
|
|
334
336
|
const bot = typeof msg.bot_id === "string" ? msg.bot_id : "";
|
|
335
|
-
|
|
337
|
+
// R758:任务/这一轮冻结的目录优先;没有就按 bot 当前目录。中途换目录不影响已经发出去的调用。
|
|
338
|
+
const frozen = typeof msg.dir === "string" && msg.dir.trim() ? msg.dir.trim() : undefined;
|
|
339
|
+
let dir = bot ? (frozen ?? dirs[bot]) : undefined;
|
|
336
340
|
if (!dir) { send({ type: "result", id, ok: false, error: NO_DIR }); return; }
|
|
341
|
+
try { dir = await checkDir(dir); } catch (e) { send({ type: "result", id, ok: false, error: e?.message ?? String(e) }); return; }
|
|
337
342
|
const ac = new AbortController();
|
|
338
343
|
jobs.set(id, ac);
|
|
339
344
|
log(`[${bot}] ${op === "bash" ? `bash:${args?.command}` : `${op}:${args?.path}`}`);
|
|
@@ -403,6 +408,24 @@ export function createSession(cfg, { log = say, platform = process.platform, pic
|
|
|
403
408
|
}
|
|
404
409
|
}
|
|
405
410
|
|
|
411
|
+
// R758:服务器同步 bot 当前绑定。dir 空 = 用固定共享资料库;自定义路径来自本机选目录或服务器已保存的值。
|
|
412
|
+
async function handleSetBinding(send, msg) {
|
|
413
|
+
const { id } = msg;
|
|
414
|
+
const bot = typeof msg.bot_id === "string" ? msg.bot_id : "";
|
|
415
|
+
if (!bot) { send({ type: "result", id, ok: false, error: "缺 bot_id" }); return; }
|
|
416
|
+
try {
|
|
417
|
+
const wanted = typeof msg.dir === "string" && msg.dir.trim() ? msg.dir.trim() : sharedLibrary;
|
|
418
|
+
if (wanted === sharedLibrary) mkdirSync(sharedLibrary, { recursive: true, mode: 0o700 });
|
|
419
|
+
const real = await checkDir(wanted);
|
|
420
|
+
dirs = { ...dirs, [bot]: real };
|
|
421
|
+
cfg.bot_dirs = { ...dirs };
|
|
422
|
+
saveConfig(cfg, file);
|
|
423
|
+
send({ type: "result", id, ok: true, bot_id: bot, dir: real });
|
|
424
|
+
} catch (e) {
|
|
425
|
+
send({ type: "result", id, ok: false, error: e?.message ?? String(e) });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
406
429
|
async function handlePick(send, msg) {
|
|
407
430
|
const { id } = msg;
|
|
408
431
|
const bot = typeof msg.bot_id === "string" ? msg.bot_id : "";
|
|
@@ -436,6 +459,7 @@ export function createSession(cfg, { log = say, platform = process.platform, pic
|
|
|
436
459
|
cleanChanges() { return changeStore().clean(true).catch(() => 0); },
|
|
437
460
|
onMessage(msg, send) {
|
|
438
461
|
if (msg.type === "call") return handleCall(send, msg);
|
|
462
|
+
if (msg.type === "set_binding") return handleSetBinding(send, msg);
|
|
439
463
|
if (msg.type === "pick_dir") return handlePick(send, msg);
|
|
440
464
|
if (msg.type === "change_get") return handleChangeGet(send, msg);
|
|
441
465
|
if (msg.type === "browser") return handleBrowser(send, msg); // R729
|
|
@@ -491,7 +515,7 @@ export function startRunner(cfg, { log = say, retryMs = 2000, platform = process
|
|
|
491
515
|
const sock = new WebSocket(wsUrlOf(cfg.server));
|
|
492
516
|
ws = sock;
|
|
493
517
|
sock.addEventListener("open", () => {
|
|
494
|
-
send(sock, { type: "hello", version: PROTOCOL_VERSION, device_id: cfg.device_id, token: cfg.token, bot_dirs: session.botDirs,
|
|
518
|
+
send(sock, { type: "hello", version: PROTOCOL_VERSION, device_id: cfg.device_id, token: cfg.token, bot_dirs: session.botDirs, library_dir: libraryDir(),
|
|
495
519
|
name: cfg.name ?? os.hostname(), os: `${process.platform} ${os.release()}`, runner_version: RUNNER_VERSION, autostart: background, browser: browserInfo(), search: searchConfigStatus(cfg.search) });
|
|
496
520
|
beat = setInterval(() => send(sock, { type: "ping", browser: browserInfo() }), HEARTBEAT_MS);
|
|
497
521
|
});
|