@chenacc/agentbridge 0.1.1 → 0.1.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 +11 -0
- package/package.json +3 -2
- package/src/config.js +8 -0
- package/src/server.js +56 -12
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ Echo 适配器不接触任何 AI 工具,只验证「CLI → 本机 Bridge →
|
|
|
99
99
|
|
|
100
100
|
| 场景 | 模式 | 手机要求 |
|
|
101
101
|
|---|---|---|
|
|
102
|
+
| 什么都不装、也不连同一个 Wi-Fi(DSH 插件默认) | Cloudflare Tunnel(自动) | 只需浏览器 |
|
|
102
103
|
| 同一可信 Wi-Fi | 局域网 | 浏览器 / 微信 |
|
|
103
104
|
| 跨网络,但属同一私有网络 | Tailscale | 同一 tailnet |
|
|
104
105
|
| Tailscale 不稳定 | ZeroTier | 同一私有网络 |
|
|
@@ -177,6 +178,14 @@ node .\bin\agentbridge.js phone --agent codex --session <thread-id>
|
|
|
177
178
|
|
|
178
179
|
**第 0 步:桥。** 插件是遥控器,桥是基站。从 0.2.0 起桥随插件一起安装,**DSH Desktop 启动时插件会自己在后台把它拉起来**,正常情况你什么都不用敲。
|
|
179
180
|
|
|
181
|
+
从 0.3.0 起,插件默认再开一条 **Cloudflare 隧道**,把桥映射成公网 https 域名,于是手机不必和电脑在同一个 Wi-Fi——扫描、出差、用流量都能开。这一步需要一个 Cloudflare 官方小工具:
|
|
182
|
+
|
|
183
|
+
```powershell
|
|
184
|
+
winget install --id Cloudflare.cloudflared
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
没装也不影响使用,只是退回"手机与电脑同一 Wi-Fi"。想一直走局域网,设 `AGENTBRIDGE_NO_TUNNEL=1`;想用固定域名,去 Cloudflare 建命名隧道并把 `AGENTBRIDGE_PUBLIC_ORIGIN` 指向它。
|
|
188
|
+
|
|
180
189
|
只有自动启动失败(沙箱限制、端口被占)才需要手动来一条:
|
|
181
190
|
|
|
182
191
|
```powershell
|
|
@@ -283,6 +292,8 @@ node .\bin\agentbridge.js phone --agent deepseek-harness
|
|
|
283
292
|
|
|
284
293
|
若自动选择的局域网地址手机访问不到,显式指定 `AGENTBRIDGE_PUBLIC_ORIGIN`;Windows 防火墙提示时只放行「专用网络」。
|
|
285
294
|
|
|
295
|
+
自动选地址时会跳过 `169.254.x.x`(网卡没拿到租约时的临时地址,手机根本路由不到)和 Hyper-V / WSL / 蓝牙这类虚拟网卡,优先真实的 Wi-Fi 或以太网地址。扫码后如果微信弹出「该地址为 IP 地址,请使用域名访问网站」,点「继续访问」即可;手机上不了网、或者只有手机流量没连同一个 Wi-Fi 时,二维码里的地址是打不开的。
|
|
296
|
+
|
|
286
297
|
## 安全边界
|
|
287
298
|
|
|
288
299
|

|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chenacc/agentbridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Control local coding agents from a phone through a secure WeChat QR web console.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"verify:responsive": "node scripts/verify-responsive.mjs",
|
|
59
59
|
"verify:guide": "node scripts/verify-product-guide.mjs",
|
|
60
60
|
"verify:release": "node scripts/verify-plugin-release.mjs",
|
|
61
|
-
"check": "node --check src/server.js && node --check src/cli.js && node --check src/cc-switch-models.js && node --check src/workspaces/workspace-catalog.js && node --check src/conversations/conversation-catalog.js && node --check src/adapters/capabilities.js && node --check src/adapters/claude.js && node --check src/adapters/codex.js && node --check src/adapters/deepseek-harness.js && node --check src/adapters/dsh-desktop.js && node --check src/dsh-desktop-channels.js && node --check src/event-store.js && node --check src/notifier.js && node --check src/attachments.js && node --check src/session-manager.js && node --check integrations/dsh-phone/lib/index.js && node --check src/adapters/zcode-native.js && node --check public/adapter-capabilities.js && node --check public/workspace-navigation.js && node --check public/markdown.js && node --check public/highlight.js && node --check public/transcript.js && node --check public/app.js && node --check scripts/verify-workspace-ui.mjs && node --check scripts/verify-message-blocks.mjs && node --check scripts/verify-background-scrim.mjs && node --check scripts/verify-model-control.mjs && node --check scripts/verify-notifications.mjs && node --check scripts/verify-attachments.mjs && node --check scripts/verify-features.mjs && node --check scripts/verify-quick-nav.mjs && node --check scripts/verify-questions.mjs && node --check scripts/verify-responsive.mjs && node --check scripts/verify-product-guide.mjs && node --check scripts/verify-plugin-release.mjs"
|
|
61
|
+
"check": "node --check src/server.js && node --check src/cli.js && node --check src/config.js && node --check src/cc-switch-models.js && node --check src/workspaces/workspace-catalog.js && node --check src/conversations/conversation-catalog.js && node --check src/adapters/capabilities.js && node --check src/adapters/claude.js && node --check src/adapters/codex.js && node --check src/adapters/deepseek-harness.js && node --check src/adapters/dsh-desktop.js && node --check src/dsh-desktop-channels.js && node --check src/event-store.js && node --check src/notifier.js && node --check src/attachments.js && node --check src/session-manager.js && node --check integrations/dsh-phone/lib/index.js && node --check integrations/dsh-phone/lib/tunnel.js && node --check src/adapters/zcode-native.js && node --check public/adapter-capabilities.js && node --check public/workspace-navigation.js && node --check public/markdown.js && node --check public/highlight.js && node --check public/transcript.js && node --check public/app.js && node --check scripts/verify-workspace-ui.mjs && node --check scripts/verify-message-blocks.mjs && node --check scripts/verify-background-scrim.mjs && node --check scripts/verify-model-control.mjs && node --check scripts/verify-notifications.mjs && node --check scripts/verify-attachments.mjs && node --check scripts/verify-features.mjs && node --check scripts/verify-quick-nav.mjs && node --check scripts/verify-questions.mjs && node --check scripts/verify-responsive.mjs && node --check scripts/verify-product-guide.mjs && node --check scripts/verify-plugin-release.mjs",
|
|
62
|
+
"verify:remote": "node scripts/verify-remote-tunnel.mjs"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
65
|
"katex": "0.19.1",
|
package/src/config.js
CHANGED
|
@@ -166,6 +166,14 @@ export function loadConfig(env = process.env, workspace = process.cwd()) {
|
|
|
166
166
|
pairingTtlMs: Number.parseInt(env.AGENTBRIDGE_PAIRING_TTL_MS ?? "300000", 10),
|
|
167
167
|
sessionTtlMs: Number.parseInt(env.AGENTBRIDGE_SESSION_TTL_MS ?? "259200000", 10),
|
|
168
168
|
machineName: env.AGENTBRIDGE_MACHINE_NAME ?? os.hostname(),
|
|
169
|
+
// Set by the DSH plugin when it spawns the bridge itself. It lets the plugin
|
|
170
|
+
// recognise its own child in /api/health and restart it to change the public
|
|
171
|
+
// address, without ever touching a bridge the user started by hand.
|
|
172
|
+
autostart: env.AGENTBRIDGE_AUTOSTART === "1",
|
|
173
|
+
// Shared secret between the plugin that spawned this bridge and the bridge
|
|
174
|
+
// itself, so the plugin may ask for a restart to change the advertised address
|
|
175
|
+
// without handing that power to anything else that can reach loopback.
|
|
176
|
+
autostartToken: (env.AGENTBRIDGE_AUTOSTART_TOKEN ?? "").trim() || null,
|
|
169
177
|
// Outbound notifications: unset webhook means the feature is simply off.
|
|
170
178
|
notifyWebhook: pick(env, "AGENTBRIDGE_NOTIFY_WEBHOOK", notifyFile.webhook),
|
|
171
179
|
notifyFormat: pick(env, "AGENTBRIDGE_NOTIFY_FORMAT", notifyFile.format ?? "json").toLowerCase(),
|
package/src/server.js
CHANGED
|
@@ -386,11 +386,40 @@ export async function createBridge(options = {}) {
|
|
|
386
386
|
return shutdownPromise;
|
|
387
387
|
};
|
|
388
388
|
|
|
389
|
+
/**
|
|
390
|
+
* A restart requested by the plugin must not cut a running Agent turn in half.
|
|
391
|
+
*
|
|
392
|
+
* The plugin retries on the next desktop launch in that case; refusing is the
|
|
393
|
+
* safer answer than silently killing work the user is waiting on.
|
|
394
|
+
*/
|
|
395
|
+
const refuseWhenBusy = () => {
|
|
396
|
+
const active = sessions.list().filter((session) => ["starting", "running", "stopping"].includes(session.status));
|
|
397
|
+
if (!active.length) return;
|
|
398
|
+
throw Object.assign(new Error("仍有 Agent 任务正在运行,请先停止任务再关闭 Bridge"), {
|
|
399
|
+
statusCode: 409,
|
|
400
|
+
code: "ACTIVE_SESSIONS",
|
|
401
|
+
diagnostics: {
|
|
402
|
+
count: active.length,
|
|
403
|
+
sessions: active.map(({ id, title, status, adapterId }) => ({ id, title, status, adapterId })),
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
|
|
389
408
|
const server = http.createServer(async (req, res) => {
|
|
390
409
|
const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
|
|
391
410
|
try {
|
|
392
411
|
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
393
|
-
|
|
412
|
+
// The advertised origin is public information (it is printed inside every
|
|
413
|
+
// QR code), and exposing it here is what lets the DSH plugin notice that a
|
|
414
|
+
// running bridge still advertises a stale address.
|
|
415
|
+
return json(res, shuttingDown ? 503 : 200, {
|
|
416
|
+
ok: !shuttingDown,
|
|
417
|
+
shuttingDown,
|
|
418
|
+
lastSeq: eventStore.lastSeq,
|
|
419
|
+
advertisedOrigin: config.advertisedOrigin,
|
|
420
|
+
accessKeyRequired: Boolean(config.accessKey),
|
|
421
|
+
autostart: config.autostart === true,
|
|
422
|
+
});
|
|
394
423
|
}
|
|
395
424
|
|
|
396
425
|
if (shuttingDown) throw Object.assign(new Error("AgentBridge is shutting down"), { statusCode: 503, code: "BRIDGE_SHUTTING_DOWN" });
|
|
@@ -644,6 +673,31 @@ export async function createBridge(options = {}) {
|
|
|
644
673
|
// point remain authenticated (and gated by the access key when enabled).
|
|
645
674
|
if (req.method === "GET" && await serveStatic(res, url.pathname, url.search)) return;
|
|
646
675
|
|
|
676
|
+
// The DSH plugin starts this bridge itself and sometimes has to restart it,
|
|
677
|
+
// because the address it advertises (cloudflared hostname) only exists after
|
|
678
|
+
// the tunnel is up. Restarting needs a stop, and a stop needs an
|
|
679
|
+
// authorisation: loopback alone would let any local page POST here, so the
|
|
680
|
+
// caller must also present the token the plugin generated when it spawned
|
|
681
|
+
// this process. The token lives in the bridge's data directory, which keeps
|
|
682
|
+
// it out of the command line and out of the logs.
|
|
683
|
+
if (req.method === "POST" && url.pathname === "/api/local/autostart/shutdown") {
|
|
684
|
+
if (!isLoopback(req.socket.remoteAddress)) throw Object.assign(new Error("Local access only"), { statusCode: 403 });
|
|
685
|
+
if (!config.autostartToken || !keyMatches(req.headers["x-agentbridge-autostart"], config.autostartToken)) {
|
|
686
|
+
throw Object.assign(new Error("Autostart token required"), { statusCode: 401 });
|
|
687
|
+
}
|
|
688
|
+
refuseWhenBusy();
|
|
689
|
+
shuttingDown = true;
|
|
690
|
+
json(res, 202, { accepted: true, message: "AgentBridge is restarting to change its public address" });
|
|
691
|
+
setImmediate(() => {
|
|
692
|
+
void shutdown().catch((error) => {
|
|
693
|
+
console.error("AgentBridge safe shutdown failed", error);
|
|
694
|
+
shuttingDown = false;
|
|
695
|
+
shutdownPromise = null;
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
647
701
|
requireAccessKey(req, url, config);
|
|
648
702
|
const device = pairing.authenticate(requestToken(req));
|
|
649
703
|
if (!device) throw Object.assign(new Error("Authentication required"), { statusCode: 401 });
|
|
@@ -784,17 +838,7 @@ export async function createBridge(options = {}) {
|
|
|
784
838
|
let match;
|
|
785
839
|
if (req.method === "POST" && url.pathname === "/api/local/shutdown") {
|
|
786
840
|
if (!isLoopback(req.socket.remoteAddress)) throw Object.assign(new Error("Local access only"), { statusCode: 403 });
|
|
787
|
-
|
|
788
|
-
if (active.length) {
|
|
789
|
-
throw Object.assign(new Error("仍有 Agent 任务正在运行,请先停止任务再关闭 Bridge"), {
|
|
790
|
-
statusCode: 409,
|
|
791
|
-
code: "ACTIVE_SESSIONS",
|
|
792
|
-
diagnostics: {
|
|
793
|
-
count: active.length,
|
|
794
|
-
sessions: active.map(({ id, title, status, adapterId }) => ({ id, title, status, adapterId })),
|
|
795
|
-
},
|
|
796
|
-
});
|
|
797
|
-
}
|
|
841
|
+
refuseWhenBusy();
|
|
798
842
|
shuttingDown = true;
|
|
799
843
|
json(res, 202, { accepted: true, message: "AgentBridge 正在安全停止" });
|
|
800
844
|
setImmediate(() => {
|