weacpx 0.1.6 → 0.1.7
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 +39 -39
- package/dist/bridge/bridge-main.js +85 -20
- package/dist/cli.js +766 -599
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weacpx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
连接微信与 acpx 协议,让 Claude Code / Codex 成为你口袋里的 24/7 伙伴。
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/weacpx)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Why Weacpx?
|
|
12
|
+
|
|
13
|
+
在 Agent-First 的开发模式下,编码任务必须依托顶级 Agents,🙅♀️不要通过 openclaw 去开发,现在有一个更好的方案。Weacpx 通过微信提供一个轻量化的远程入口,随时随地通过手机驱动你的顶级 Agents。
|
|
14
|
+
|
|
15
|
+
Weacpx 的核心价值主张很简单:
|
|
16
|
+
|
|
17
|
+
**随时随地访问** — 只要你有微信,就能控制你的 Agent。无需 VPN、Web 界面或复杂的云服务配置。
|
|
18
|
+
|
|
19
|
+
**统一的会话管理** — 通过 acpx 协议,weacpx 让你在微信里管理多个 Agent 会话(Codex、Claude Code 等),就像在本地终端一样。创建、切换、查询状态,全部通过简单的斜杠命令完成,这是其它简单基于 ACP 实现的远控 agent 所不具备的。
|
|
20
|
+
|
|
21
|
+
**轻量守护进程** — weacpx 作为后台守护进程运行,资源占用极低。不用启动一个臃肿 openclaw,不用担心在工作机器上使用会占用资源。启动、停止、查看状态都通过简单的 CLI 命令完成。
|
|
22
|
+
|
|
23
|
+
**权限可控** — 可以即时通过微信修改 agent 的权限,无论是 YOLO 还是只读。
|
|
4
24
|
|
|
5
25
|
## 安装前准备
|
|
6
26
|
|
|
7
27
|
开始前,至少需要:
|
|
8
28
|
|
|
9
29
|
- Node.js 22+ 或 Bun
|
|
10
|
-
- 一个可用的微信登录环境
|
|
11
30
|
- Claude Code 或 Codex
|
|
31
|
+
- 装了微信的手机
|
|
12
32
|
|
|
13
33
|
> `weacpx` 基于 `weixin-agent-sdk` 与 `acpx` 实现。
|
|
14
34
|
> 正常情况下,不需要再额外全局安装 `acpx`。
|
|
@@ -252,12 +272,6 @@ bun run dev
|
|
|
252
272
|
- `~/.weacpx/runtime/stdout.log`
|
|
253
273
|
- `~/.weacpx/runtime/stderr.log`
|
|
254
274
|
|
|
255
|
-
常用环境变量:
|
|
256
|
-
|
|
257
|
-
- `WEACPX_CONFIG`
|
|
258
|
-
- `WEACPX_STATE`
|
|
259
|
-
- `WEACPX_WEIXIN_SDK`
|
|
260
|
-
|
|
261
275
|
### Transport 权限配置
|
|
262
276
|
|
|
263
277
|
`config.json` 中的 `transport` 支持以下权限字段:
|
|
@@ -304,36 +318,6 @@ bun run dev
|
|
|
304
318
|
|
|
305
319
|
## 注意事项
|
|
306
320
|
|
|
307
|
-
### `dry-run`
|
|
308
|
-
|
|
309
|
-
`dry-run` 会复用同一套 router、session service、transport,只是把微信消息换成终端输入,适合本地排查。
|
|
310
|
-
|
|
311
|
-
示例:
|
|
312
|
-
|
|
313
|
-
```bash
|
|
314
|
-
bun run dry-run --chat-key wx:test -- \
|
|
315
|
-
"/agent add codex" \
|
|
316
|
-
"/ws new backend -d /absolute/path/to/backend" \
|
|
317
|
-
"/ss new demo -a codex --ws backend" \
|
|
318
|
-
"/status"
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### 如果 `/ss new` 失败
|
|
322
|
-
|
|
323
|
-
当前最常见的问题仍然是底层 `acpx` named session 的运行时恢复,不一定是 `weacpx` 本身的逻辑问题。
|
|
324
|
-
|
|
325
|
-
可以先在本地创建一个 named session,再在微信里 attach:
|
|
326
|
-
|
|
327
|
-
```bash
|
|
328
|
-
./node_modules/.bin/acpx --verbose --cwd /absolute/workspace/path codex sessions new --name existing-demo
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
然后在微信里:
|
|
332
|
-
|
|
333
|
-
```text
|
|
334
|
-
/ss attach demo -a codex --ws backend --name existing-demo
|
|
335
|
-
```
|
|
336
|
-
|
|
337
321
|
### Adapter mode 参考
|
|
338
322
|
|
|
339
323
|
`acpx set-mode` / 计划中的 `/mode <id>` 本质上都是给底层 ACP session 发送 `session/set_mode`。
|
|
@@ -354,6 +338,22 @@ bun run dry-run --chat-key wx:test -- \
|
|
|
354
338
|
- 对其他 adapter,不要在 `weacpx` 里写死候选值;最好把 `/mode <id>` 设计成透传,由 adapter 自己决定是否接受。
|
|
355
339
|
- 如果某个 adapter 后续补充了官方 mode 文档,再把它们补进这里。
|
|
356
340
|
|
|
341
|
+
### 如果 `/ss new` 失败
|
|
342
|
+
|
|
343
|
+
当前最常见的问题仍然是底层 `acpx` named session 的运行时恢复,不一定是 `weacpx` 本身的逻辑问题。
|
|
344
|
+
|
|
345
|
+
可以先在本地创建一个 named session,再在微信里 attach:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
./node_modules/.bin/acpx --verbose --cwd /absolute/workspace/path codex sessions new --name existing-demo
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
然后在微信里:
|
|
352
|
+
|
|
353
|
+
```text
|
|
354
|
+
/ss attach demo -a codex --ws backend --name existing-demo
|
|
355
|
+
```
|
|
356
|
+
|
|
357
357
|
## 更多文档
|
|
358
358
|
|
|
359
359
|
- 配置参考:[docs/config-reference.md](./docs/config-reference.md)
|
|
@@ -199,14 +199,28 @@ import { createInterface } from "node:readline";
|
|
|
199
199
|
// src/bridge/bridge-server.ts
|
|
200
200
|
init_prompt_output();
|
|
201
201
|
|
|
202
|
+
class BridgeInvalidRequestError extends Error {
|
|
203
|
+
}
|
|
204
|
+
var BRIDGE_METHODS = new Set([
|
|
205
|
+
"ping",
|
|
206
|
+
"shutdown",
|
|
207
|
+
"hasSession",
|
|
208
|
+
"ensureSession",
|
|
209
|
+
"prompt",
|
|
210
|
+
"setMode",
|
|
211
|
+
"cancel"
|
|
212
|
+
]);
|
|
213
|
+
|
|
202
214
|
class BridgeServer {
|
|
203
215
|
runtime;
|
|
204
216
|
constructor(runtime) {
|
|
205
217
|
this.runtime = runtime;
|
|
206
218
|
}
|
|
207
219
|
async handleLine(line) {
|
|
208
|
-
|
|
220
|
+
let requestId = extractRequestId(line);
|
|
209
221
|
try {
|
|
222
|
+
const request = parseBridgeRequest(line);
|
|
223
|
+
requestId = request.id;
|
|
210
224
|
const result = await this.dispatch(request.method, request.params);
|
|
211
225
|
return `${JSON.stringify({
|
|
212
226
|
id: request.id,
|
|
@@ -217,10 +231,10 @@ class BridgeServer {
|
|
|
217
231
|
} catch (error) {
|
|
218
232
|
const message = error instanceof Error ? error.message : String(error);
|
|
219
233
|
return `${JSON.stringify({
|
|
220
|
-
id:
|
|
234
|
+
id: requestId,
|
|
221
235
|
ok: false,
|
|
222
236
|
error: {
|
|
223
|
-
code: "BRIDGE_INTERNAL_ERROR",
|
|
237
|
+
code: error instanceof BridgeInvalidRequestError ? "BRIDGE_INVALID_REQUEST" : "BRIDGE_INTERNAL_ERROR",
|
|
224
238
|
message,
|
|
225
239
|
...error instanceof PromptCommandError ? {
|
|
226
240
|
details: {
|
|
@@ -242,46 +256,97 @@ class BridgeServer {
|
|
|
242
256
|
return await this.runtime.shutdown();
|
|
243
257
|
case "hasSession":
|
|
244
258
|
return await this.runtime.hasSession({
|
|
245
|
-
agent:
|
|
259
|
+
agent: requireString(params, "agent"),
|
|
246
260
|
agentCommand: asOptionalString(params.agentCommand),
|
|
247
|
-
cwd:
|
|
248
|
-
name:
|
|
261
|
+
cwd: requireString(params, "cwd"),
|
|
262
|
+
name: requireString(params, "name")
|
|
249
263
|
});
|
|
250
264
|
case "ensureSession":
|
|
251
265
|
return await this.runtime.ensureSession({
|
|
252
|
-
agent:
|
|
266
|
+
agent: requireString(params, "agent"),
|
|
253
267
|
agentCommand: asOptionalString(params.agentCommand),
|
|
254
|
-
cwd:
|
|
255
|
-
name:
|
|
268
|
+
cwd: requireString(params, "cwd"),
|
|
269
|
+
name: requireString(params, "name")
|
|
256
270
|
});
|
|
257
271
|
case "prompt":
|
|
258
272
|
return await this.runtime.prompt({
|
|
259
|
-
agent:
|
|
273
|
+
agent: requireString(params, "agent"),
|
|
260
274
|
agentCommand: asOptionalString(params.agentCommand),
|
|
261
|
-
cwd:
|
|
262
|
-
name:
|
|
263
|
-
text:
|
|
275
|
+
cwd: requireString(params, "cwd"),
|
|
276
|
+
name: requireString(params, "name"),
|
|
277
|
+
text: requireString(params, "text")
|
|
264
278
|
});
|
|
265
279
|
case "setMode":
|
|
266
280
|
return await this.runtime.setMode({
|
|
267
|
-
agent:
|
|
281
|
+
agent: requireString(params, "agent"),
|
|
268
282
|
agentCommand: asOptionalString(params.agentCommand),
|
|
269
|
-
cwd:
|
|
270
|
-
name:
|
|
271
|
-
modeId:
|
|
283
|
+
cwd: requireString(params, "cwd"),
|
|
284
|
+
name: requireString(params, "name"),
|
|
285
|
+
modeId: requireString(params, "modeId")
|
|
272
286
|
});
|
|
273
287
|
case "cancel":
|
|
274
288
|
return await this.runtime.cancel({
|
|
275
|
-
agent:
|
|
289
|
+
agent: requireString(params, "agent"),
|
|
276
290
|
agentCommand: asOptionalString(params.agentCommand),
|
|
277
|
-
cwd:
|
|
278
|
-
name:
|
|
291
|
+
cwd: requireString(params, "cwd"),
|
|
292
|
+
name: requireString(params, "name")
|
|
279
293
|
});
|
|
280
294
|
default:
|
|
281
295
|
throw new Error(`unsupported bridge method: ${method}`);
|
|
282
296
|
}
|
|
283
297
|
}
|
|
284
298
|
}
|
|
299
|
+
function extractRequestId(line) {
|
|
300
|
+
try {
|
|
301
|
+
const raw = JSON.parse(line);
|
|
302
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
303
|
+
return "unknown";
|
|
304
|
+
}
|
|
305
|
+
const id = raw.id;
|
|
306
|
+
return typeof id === "string" && id.length > 0 ? id : "unknown";
|
|
307
|
+
} catch {
|
|
308
|
+
return "unknown";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function parseBridgeRequest(line) {
|
|
312
|
+
let raw;
|
|
313
|
+
try {
|
|
314
|
+
raw = JSON.parse(line);
|
|
315
|
+
} catch {
|
|
316
|
+
throw new BridgeInvalidRequestError("request must be valid JSON");
|
|
317
|
+
}
|
|
318
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
319
|
+
throw new BridgeInvalidRequestError("request must be a JSON object");
|
|
320
|
+
}
|
|
321
|
+
const request = raw;
|
|
322
|
+
const id = request.id;
|
|
323
|
+
const method = request.method;
|
|
324
|
+
const params = request.params;
|
|
325
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
326
|
+
throw new BridgeInvalidRequestError("id must be a non-empty string");
|
|
327
|
+
}
|
|
328
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
329
|
+
throw new BridgeInvalidRequestError("method must be a non-empty string");
|
|
330
|
+
}
|
|
331
|
+
if (!BRIDGE_METHODS.has(method)) {
|
|
332
|
+
throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
|
|
333
|
+
}
|
|
334
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
335
|
+
throw new BridgeInvalidRequestError("params must be an object");
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
id,
|
|
339
|
+
method,
|
|
340
|
+
params
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function requireString(params, key) {
|
|
344
|
+
const value = params[key];
|
|
345
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
346
|
+
throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
285
350
|
function asOptionalString(value) {
|
|
286
351
|
if (typeof value !== "string" || value.length === 0) {
|
|
287
352
|
return;
|