@chenacc/agentbridge 0.2.1 → 0.2.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/.env.example +5 -0
- package/README.md +14 -5
- package/package.json +4 -3
- package/src/cli.js +56 -8
- package/src/config.js +41 -5
- package/src/server.js +11 -0
package/.env.example
CHANGED
|
@@ -63,3 +63,8 @@ AGENTBRIDGE_PROJECTS=agentbridge=D:\codex_project\手机远程操控agent
|
|
|
63
63
|
# AGENTBRIDGE_NOTIFY_TOKEN=your-provider-token
|
|
64
64
|
# AGENTBRIDGE_NOTIFY_CHANNEL=clawbot
|
|
65
65
|
# AGENTBRIDGE_NOTIFY_LINK=1
|
|
66
|
+
|
|
67
|
+
# The bridge reads notify.json from its data directory. If that directory moved
|
|
68
|
+
# (the DSH plugin gives the bridge its own state folder), an older folder can be
|
|
69
|
+
# adopted once instead of re-entering the token by hand:
|
|
70
|
+
# AGENTBRIDGE_NOTIFY_IMPORT=D:\codex_project\agentbridge\.agentbridge
|
package/README.md
CHANGED
|
@@ -8,23 +8,29 @@
|
|
|
8
8
|
|
|
9
9
|
**一条命令,把手机变成这台电脑上 Agent 的遥控器。** 微信扫码即用,不用装 App,也不用和电脑连同一个 Wi-Fi。
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
<img src="./docs/assets/hero-phone.jpg" alt="手机端控制台:语音/文字发指令,Agent 流式回复,与桌面同一份对话" width="240" align="right" />
|
|
12
|
+
|
|
13
|
+
在电脑上跑着的 Claude Code / Codex / DeepSeek Harness / DSH Desktop 会话,执行 `/phone` 会出现一次性二维码:
|
|
14
|
+
|
|
15
|
+
- 手机扫码后**接管的就是屏幕上那一条对话**,不是新建一个"类似的"会话;
|
|
16
|
+
- 消息、工具调用、审批卡片、模型切换、停止任务,两端看到的是同一份事件流;
|
|
17
|
+
- 电脑始终是唯一执行端,凭据和文件访问不出本机。
|
|
14
18
|
|
|
15
19
|
```powershell
|
|
16
20
|
npx @chenacc/agentbridge # 起桥并直接打印二维码
|
|
17
21
|
```
|
|
18
22
|
|
|
19
|
-
然后在 Claude Code / Codex / DSH Desktop 里输入 `/phone
|
|
23
|
+
然后在 Claude Code / Codex / DSH Desktop 里输入 `/phone`,扫二维码即可。
|
|
20
24
|
|
|
21
25
|
> 觉得有用就给个 star:[github.com/chenacc1/agentbridge](https://github.com/chenacc1/agentbridge) —— 不点也不影响任何功能,只影响我有没有动力继续写。
|
|
22
26
|
|
|
27
|
+

|
|
28
|
+
|
|
23
29
|
手机与桌面共享同一份可追加事件流,因此消息、工具活动、审批和任务状态始终同步。电脑是唯一的执行端:手机只运行界面,不运行 Agent,也拿不到任何供应商凭据。
|
|
24
30
|
|
|
25
31
|
默认走 Cloudflare 隧道:手机用流量、在外地都能打开;隧道对外开放,因此自动带一道访问密钥门禁。
|
|
26
32
|
|
|
27
|
-
**当前版本:** 桥 `@chenacc/agentbridge` **0.2.
|
|
33
|
+
**当前版本:** 桥 `@chenacc/agentbridge` **0.2.1** · DSH 插件 `@chenacc/dsh-agentbridge` **0.4.1**(两个包各自发布,见文末「版本与发布」)。
|
|
28
34
|
|
|
29
35
|
## 它是怎么工作的
|
|
30
36
|
|
|
@@ -380,6 +386,9 @@ agentbridge phone --agent deepseek-harness
|
|
|
380
386
|
| `AGENTBRIDGE_SESSION_TTL_MS` | `259200000` | 设备会话有效期(72 小时) |
|
|
381
387
|
| `AGENTBRIDGE_CLAUDE_MODEL` | 供应商默认 | 可选的 Claude / Kimi 模型 ID |
|
|
382
388
|
| `AGENTBRIDGE_NOTIFY_WEBHOOK` | 未设置 | 通知 Webhook;未设置时该功能静默 |
|
|
389
|
+
| `AGENTBRIDGE_NOTIFY_IMPORT` | 未设置 | 通知设置存在旧数据目录时,指向它一次即可自动迁过来 |
|
|
390
|
+
|
|
391
|
+
> 通知设置(webhook / token / 静默开关)保存在**桥的数据目录**下的 `notify.json`,默认是 `%USERPROFILE%\.agentbridge`。DSH 插件接管网关后数据目录换过位置,老配置如果留在原来的 `.agentbridge` 里,用 `AGENTBRIDGE_NOTIFY_IMPORT=<旧目录>` 启动一次就会自动迁过去;也可以直接在桌面控制台的「通知」面板重填一次。桥启动日志里有一行 `Notifications: pushplus/clawbot` 或 `Notifications: off — no webhook configured in …`,能直接看出它到底读到了哪个文件。
|
|
383
392
|
|
|
384
393
|
**跨网络(CLI 与插件都认):**
|
|
385
394
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chenacc/agentbridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -65,8 +65,9 @@
|
|
|
65
65
|
"verify:responsive": "node scripts/verify-responsive.mjs",
|
|
66
66
|
"verify:guide": "node scripts/verify-product-guide.mjs",
|
|
67
67
|
"verify:release": "node scripts/verify-plugin-release.mjs",
|
|
68
|
-
"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 src/tunnel.js && node --check src/remote.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",
|
|
69
|
-
"verify:remote": "node scripts/verify-remote-tunnel.mjs"
|
|
68
|
+
"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 src/tunnel.js && node --check src/remote.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 && node --check scripts/capture-screenshots.mjs",
|
|
69
|
+
"verify:remote": "node scripts/verify-remote-tunnel.mjs",
|
|
70
|
+
"capture:shots": "node scripts/capture-screenshots.mjs"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
73
|
"katex": "0.19.1",
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -52,23 +52,37 @@ Tell the user to scan the QR with WeChat. While the phone controls this session,
|
|
|
52
52
|
`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The Codex skill is named `phone-codex`, not `phone`.
|
|
57
|
+
*
|
|
58
|
+
* `~/.agents/skills` is shared: Codex reads it, and so does DSH Desktop. A skill
|
|
59
|
+
* called `phone` therefore shadows the DSH plugin's `/phone` command, and typing
|
|
60
|
+
* `/phone` in DSH Desktop runs `phone --agent codex` — pairing a Codex
|
|
61
|
+
* conversation instead of the one on screen. One name per client: the plugin owns
|
|
62
|
+
* `/phone`, this skill answers `/phone-codex`.
|
|
63
|
+
*/
|
|
64
|
+
export const CODEX_SKILL_NAME = "phone-codex";
|
|
65
|
+
const CODEX_SKILL_MARKER = "# Send a Codex project to AgentBridge";
|
|
66
|
+
|
|
55
67
|
export function renderCodexPhoneSkill(entryPath = cliEntry) {
|
|
56
68
|
const portableEntry = entryPath.replaceAll("\\", "/").replaceAll('"', '\\"');
|
|
57
69
|
return `---
|
|
58
|
-
name:
|
|
59
|
-
description: Generate a one-time WeChat QR code for a Codex project conversation. Use only when the user explicitly invokes
|
|
70
|
+
name: ${CODEX_SKILL_NAME}
|
|
71
|
+
description: Generate a one-time WeChat QR code for a Codex project conversation. Use only when the user explicitly invokes /${CODEX_SKILL_NAME}.
|
|
60
72
|
disable-model-invocation: true
|
|
61
73
|
---
|
|
62
74
|
|
|
63
|
-
|
|
75
|
+
${CODEX_SKILL_MARKER}
|
|
64
76
|
|
|
65
|
-
When the user invokes
|
|
77
|
+
When the user invokes \`/${CODEX_SKILL_NAME}\`, you must immediately execute the following command in the terminal from the current project directory. Do not merely describe the command or ask the user to run it:
|
|
66
78
|
|
|
67
79
|
\`node "${portableEntry}" phone --agent codex --cwd .\`
|
|
68
80
|
|
|
69
81
|
After a successful command, reply with the exact QR image Markdown emitted by the command (for example, \`\`) so Codex Desktop renders a scannable image. The temporary QR image URL and pairing URL must be shown as clickable links; then tell the user to scan the 微信扫码二维码 with WeChat. Do not invent or alter either URL. If the command fails, show its exact safe error and do not claim that a QR code was generated.
|
|
70
82
|
|
|
71
83
|
If the user supplies a known persisted Codex thread ID, append \`--session <thread-id>\` to resume that exact thread. Do not claim that the command attached the current desktop conversation when no thread ID was supplied: Codex does not expose a documented current-thread substitution to this local skill.
|
|
84
|
+
|
|
85
|
+
> Named \`${CODEX_SKILL_NAME}\` on purpose: DSH Desktop reads the same skills folder and owns \`/phone\` for the conversation on screen. Running \`/phone\` here would pair the wrong Agent.
|
|
72
86
|
`;
|
|
73
87
|
}
|
|
74
88
|
|
|
@@ -271,17 +285,51 @@ async function installClaudeSkill(options) {
|
|
|
271
285
|
return target;
|
|
272
286
|
}
|
|
273
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Remove a `phone` skill this installer used to create.
|
|
290
|
+
*
|
|
291
|
+
* Earlier releases wrote the Codex skill as `phone`, which shadowed the DSH
|
|
292
|
+
* plugin's `/phone` in DSH Desktop because both clients read `~/.agents/skills`.
|
|
293
|
+
* Re-running the installer has to clean that up, or the wrong pairing keeps
|
|
294
|
+
* happening after an upgrade. Only a file carrying our own marker is touched —
|
|
295
|
+
* a hand-written `phone` skill belongs to whoever wrote it.
|
|
296
|
+
*/
|
|
297
|
+
async function removeLegacyCodexSkill(root) {
|
|
298
|
+
const legacy = path.join(root, "phone", "SKILL.md");
|
|
299
|
+
let content;
|
|
300
|
+
try {
|
|
301
|
+
content = await readFile(legacy, "utf8");
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (!content.includes(CODEX_SKILL_MARKER)) return null;
|
|
306
|
+
try {
|
|
307
|
+
await rm(path.join(root, "phone"), { recursive: true, force: true });
|
|
308
|
+
return legacy;
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
274
314
|
async function installCodexSkill(options) {
|
|
275
315
|
if (options.user && options.project) throw new Error("Choose only one of --user or --project");
|
|
276
316
|
const projectCwd = path.resolve(options.cwd ?? process.cwd());
|
|
277
317
|
const root = options.project
|
|
278
318
|
? path.join(projectCwd, ".agents", "skills")
|
|
279
319
|
: path.join(os.homedir(), ".agents", "skills");
|
|
280
|
-
|
|
320
|
+
|
|
321
|
+
const removed = await removeLegacyCodexSkill(root);
|
|
322
|
+
if (removed) {
|
|
323
|
+
console.log(`已移除会抢占 /phone 的旧技能: ${removed}`);
|
|
324
|
+
console.log("(DSH Desktop 也读这个目录,旧的 phone 技能会让 /phone 绑到 Codex 会话。)");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const target = path.join(root, CODEX_SKILL_NAME, "SKILL.md");
|
|
281
328
|
await mkdir(path.dirname(target), { recursive: true });
|
|
282
329
|
await writeFile(target, renderCodexPhoneSkill(), { encoding: "utf8", flag: "w" });
|
|
283
|
-
console.log(`Codex
|
|
284
|
-
console.log(
|
|
330
|
+
console.log(`Codex /${CODEX_SKILL_NAME} 已安装到: ${target}`);
|
|
331
|
+
console.log(`重新打开 Codex 桌面项目后,在斜杠命令列表选择 /${CODEX_SKILL_NAME}。未提供 thread ID 时,它会创建新的项目会话并清楚标注。`);
|
|
332
|
+
console.log("DSH Desktop 里的 /phone 保持归插件所有,绑定的就是屏幕上那条会话。");
|
|
285
333
|
return target;
|
|
286
334
|
}
|
|
287
335
|
|
package/src/config.js
CHANGED
|
@@ -105,16 +105,52 @@ function parseProjects(raw, workspace) {
|
|
|
105
105
|
* end up in a tracked file. The bridge's own data directory is git-ignored, so a
|
|
106
106
|
* small JSON file there is the honest place for it; environment variables still win
|
|
107
107
|
* when both are present, which keeps `setx` and one-off experiments working.
|
|
108
|
+
*
|
|
109
|
+
* `legacyFiles` exists because the data directory can move under the user's feet:
|
|
110
|
+
* the DSH plugin started hosting the bridge and gave it its own state directory,
|
|
111
|
+
* which silently left notifications configured in the old one. When the current
|
|
112
|
+
* directory has no settings but an old one does, adopt them once and say so — the
|
|
113
|
+
* alternative is a feature that stops working with no explanation.
|
|
108
114
|
*/
|
|
109
|
-
function readNotifyFile(dataDir) {
|
|
115
|
+
function readNotifyFile(dataDir, legacyFiles = []) {
|
|
116
|
+
const own = path.join(dataDir, "notify.json");
|
|
117
|
+
const parsed = readJsonObject(own);
|
|
118
|
+
if (parsed) return parsed;
|
|
119
|
+
|
|
120
|
+
for (const candidate of legacyFiles) {
|
|
121
|
+
const adopted = readJsonObject(candidate);
|
|
122
|
+
if (!adopted) continue;
|
|
123
|
+
try {
|
|
124
|
+
mkdirSync(dataDir, { recursive: true });
|
|
125
|
+
writeFileSync(own, JSON.stringify(adopted, null, 2), "utf8");
|
|
126
|
+
console.error(`Notifications: adopted settings from ${candidate} into ${own}`);
|
|
127
|
+
} catch {
|
|
128
|
+
// A read-only data directory still gets to use the settings for this run.
|
|
129
|
+
}
|
|
130
|
+
return adopted;
|
|
131
|
+
}
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readJsonObject(file) {
|
|
110
136
|
try {
|
|
111
|
-
const parsed = JSON.parse(readFileSync(
|
|
112
|
-
return parsed && typeof parsed === "object" ? parsed :
|
|
137
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
138
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
113
139
|
} catch {
|
|
114
|
-
return
|
|
140
|
+
return null;
|
|
115
141
|
}
|
|
116
142
|
}
|
|
117
143
|
|
|
144
|
+
/** Candidate files a moved data directory may have left behind. */
|
|
145
|
+
export function legacyNotifyPaths(env = process.env, workspace = process.cwd()) {
|
|
146
|
+
const fromEnv = String(env.AGENTBRIDGE_NOTIFY_IMPORT ?? "")
|
|
147
|
+
.split(";")
|
|
148
|
+
.map((entry) => entry.trim())
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.map((entry) => (/notify\.json$/i.test(entry) ? path.resolve(entry) : path.join(path.resolve(entry), "notify.json")));
|
|
151
|
+
return [...fromEnv, path.join(path.resolve(workspace), ".agentbridge", "notify.json")];
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
function pick(env, key, fallback) {
|
|
119
155
|
const value = String(env[key] ?? "").trim();
|
|
120
156
|
return value || fallback || null;
|
|
@@ -153,7 +189,7 @@ export function loadConfig(env = process.env, workspace = process.cwd()) {
|
|
|
153
189
|
const advertisedOrigin = publicOrigin || `http://${firstLanAddress()}:${port}`;
|
|
154
190
|
const accessKey = (env.AGENTBRIDGE_ACCESS_KEY ?? "").trim() || null;
|
|
155
191
|
const dataDir = path.resolve(env.AGENTBRIDGE_DATA_DIR ?? path.join(workspace, ".agentbridge"));
|
|
156
|
-
const notifyFile = readNotifyFile(dataDir);
|
|
192
|
+
const notifyFile = readNotifyFile(dataDir, legacyNotifyPaths(env, workspace));
|
|
157
193
|
|
|
158
194
|
return {
|
|
159
195
|
host,
|
package/src/server.js
CHANGED
|
@@ -994,6 +994,17 @@ export async function startBridge() {
|
|
|
994
994
|
console.log(`Remote path: ${bridge.config.remoteProvider} → ${bridge.config.advertisedOrigin}`);
|
|
995
995
|
}
|
|
996
996
|
console.log(`WeChat scan target: ${bridge.getCurrentPairing().url}`);
|
|
997
|
+
// Notifications are silent by design, which once hid the fact that the config
|
|
998
|
+
// was being read from a directory that did not have it (the data directory moved
|
|
999
|
+
// when the DSH plugin started hosting the bridge). Say where it looked.
|
|
1000
|
+
if (bridge.notifier.enabled) {
|
|
1001
|
+
console.log(`Notifications: ${bridge.notifier.format}${bridge.notifier.channel ? `/${bridge.notifier.channel}` : ""}${bridge.notifier.muted ? " (muted)" : ""}`);
|
|
1002
|
+
} else {
|
|
1003
|
+
console.log(
|
|
1004
|
+
`Notifications: off — no webhook configured in ${path.join(bridge.config.dataDir, "notify.json")} `
|
|
1005
|
+
+ "(set it in the desktop console, or point AGENTBRIDGE_NOTIFY_IMPORT at an older .agentbridge folder)",
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
997
1008
|
// Silence here used to mean a QR encoding an address the phone could never reach.
|
|
998
1009
|
if (new URL(bridge.config.advertisedOrigin).hostname === "127.0.0.1") {
|
|
999
1010
|
console.warn(
|