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 CHANGED
@@ -1,14 +1,34 @@
1
- # weacpx
1
+ # Weacpx
2
2
 
3
- 使用微信 ClawBot 随时随地通过 `acpx` 控制 Claude CodeCodex Agents。
3
+ 连接微信与 acpx 协议,让 Claude Code / Codex 成为你口袋里的 24/7 伙伴。
4
+
5
+ [![npm](https://img.shields.io/npm/v/weacpx?style=flat-square)](https://www.npmjs.com/package/weacpx)
6
+ [![Node.js Version](https://img.shields.io/node/v/weacpx?style=flat-square)](https://nodejs.org)
7
+ [![License](https://img.shields.io/npm/l/weacpx?style=flat-square)](./LICENSE)
8
+
9
+ ![weacpx logo](assets/weacpx.jpg)
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
- const request = JSON.parse(line);
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: request.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: String(params.agent),
259
+ agent: requireString(params, "agent"),
246
260
  agentCommand: asOptionalString(params.agentCommand),
247
- cwd: String(params.cwd),
248
- name: String(params.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: String(params.agent),
266
+ agent: requireString(params, "agent"),
253
267
  agentCommand: asOptionalString(params.agentCommand),
254
- cwd: String(params.cwd),
255
- name: String(params.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: String(params.agent),
273
+ agent: requireString(params, "agent"),
260
274
  agentCommand: asOptionalString(params.agentCommand),
261
- cwd: String(params.cwd),
262
- name: String(params.name),
263
- text: String(params.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: String(params.agent),
281
+ agent: requireString(params, "agent"),
268
282
  agentCommand: asOptionalString(params.agentCommand),
269
- cwd: String(params.cwd),
270
- name: String(params.name),
271
- modeId: String(params.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: String(params.agent),
289
+ agent: requireString(params, "agent"),
276
290
  agentCommand: asOptionalString(params.agentCommand),
277
- cwd: String(params.cwd),
278
- name: String(params.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;