@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 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
  ![访问边界:本地配对与管理接口只允许 loopback;远程访问使用私有网络或受访问密钥保护的隧道。](./docs/assets/security-boundaries.svg)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenacc/agentbridge",
3
- "version": "0.1.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
- return json(res, shuttingDown ? 503 : 200, { ok: !shuttingDown, shuttingDown, lastSeq: eventStore.lastSeq });
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
- const active = sessions.list().filter((session) => ["starting", "running", "stopping"].includes(session.status));
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(() => {