weacpx 0.1.2 → 0.1.4

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
@@ -28,17 +28,10 @@ bun add -g weacpx
28
28
 
29
29
  第一次使用建议按这个顺序:
30
30
 
31
- 1. 登录微信
32
- 2. 启动服务
31
+ 1. 登录微信 `weacpx login`
32
+ 2. 启动服务 `weacpx start`
33
33
  3. 在微信里创建会话并开始对话
34
34
 
35
- 如果你是全局安装版本:
36
-
37
- ```bash
38
- weacpx login
39
- weacpx start
40
- ```
41
-
42
35
  `weacpx login` 会在终端里显示二维码,使用微信扫描登录。`weacpx start` 启动后,在微信里发:
43
36
 
44
37
  ```text
@@ -57,6 +50,8 @@ weacpx start
57
50
  hello
58
51
  ```
59
52
 
53
+ 如果任务比较长,`weacpx` 会优先把 Agent 的中间回复分段发回微信,而不是一直等到最后一条结果。
54
+
60
55
  如果你是从源码仓库直接使用:
61
56
 
62
57
  ```bash
@@ -75,6 +70,7 @@ bun run dev
75
70
  常用命令:
76
71
 
77
72
  - `weacpx login`
73
+ - `weacpx logout`
78
74
  - `weacpx run`
79
75
  - `weacpx start`
80
76
  - `weacpx status`
@@ -86,6 +82,12 @@ bun run dev
86
82
  - `start` 后台启动
87
83
  - `status` 查看后台状态、PID、配置路径和日志路径
88
84
  - `stop` 停止后台实例
85
+ - `logout` 清除本机已保存的微信登录凭证;如果当前没有已登录账号,会直接提示
86
+
87
+ 说明:
88
+
89
+ - `weacpx logout` 只清理已保存的微信账号凭证
90
+ - 它不会停止当前 daemon,也不会删除 `weacpx` 的 session/state 配置
89
91
 
90
92
  ## 微信中使用说明
91
93
 
@@ -163,6 +165,29 @@ bun run dev
163
165
  - `/use <alias>` 用来切换当前会话
164
166
  - 非 `/` 开头的文本会发送到当前 session
165
167
 
168
+ ### 权限策略
169
+
170
+ `weacpx` 支持直接在微信里查看和切换 `acpx` 的权限策略。
171
+
172
+ | 命令 | 说明 |
173
+ |------|------|
174
+ | `/pm` / `/permission` | 查看当前权限模式 |
175
+ | `/pm set allow` | 切到 `approve-all` |
176
+ | `/pm set read` | 切到 `approve-reads` |
177
+ | `/pm set deny` | 切到 `deny-all` |
178
+ | `/pm auto` | 查看当前非交互策略 |
179
+ | `/pm auto allow` | 切到 `allow` |
180
+ | `/pm auto deny` | 切到 `deny` |
181
+ | `/pm auto fail` | 切到 `fail` |
182
+
183
+ 说明:
184
+
185
+ - `allow` 对应 `approve-all`
186
+ - `read` 对应 `approve-reads`
187
+ - `deny` 对应 `deny-all`
188
+ - `/pm auto ...` 修改的是 `transport.nonInteractivePermissions`
189
+ - 这些命令会把结果写回 `config.json`
190
+
166
191
  ### 推荐工作流
167
192
 
168
193
  新建一个可聊天的会话:
@@ -190,6 +215,21 @@ bun run dev
190
215
  /workspace rm old-repo
191
216
  ```
192
217
 
218
+ ### 微信内置登录相关指令
219
+
220
+ 除了 `weacpx login` / `weacpx logout` 这类 CLI 命令外,微信通道里还支持少量内置 slash 指令:
221
+
222
+ | 命令 | 说明 |
223
+ |------|------|
224
+ | `/clear` | 清除当前聊天会话,上下文重新开始 |
225
+ | `/logout` | 清除当前机器上已保存的所有微信账号凭证 |
226
+
227
+ 说明:
228
+
229
+ - `/logout` 的语义和 CLI 的 `weacpx logout` 一致,都是清凭证
230
+ - 如果当前没有已登录账号,会提示“当前没有已登录的账号”
231
+ - `/logout` 不会停止后台服务,也不会删除 `weacpx` 的工作区、agent、session 状态
232
+
193
233
  ## 配置与运行文件
194
234
 
195
235
  默认文件位置:
@@ -211,6 +251,28 @@ bun run dev
211
251
  - `WEACPX_STATE`
212
252
  - `WEACPX_WEIXIN_SDK`
213
253
 
254
+ ### Transport 权限配置
255
+
256
+ `config.json` 中的 `transport` 支持以下权限字段:
257
+
258
+ ```json
259
+ {
260
+ "transport": {
261
+ "type": "acpx-bridge",
262
+ "sessionInitTimeoutMs": 120000,
263
+ "permissionMode": "approve-all",
264
+ "nonInteractivePermissions": "fail"
265
+ }
266
+ }
267
+ ```
268
+
269
+ 说明:
270
+
271
+ - `permissionMode`: `approve-all`、`approve-reads`、`deny-all`
272
+ - `nonInteractivePermissions`: `allow`、`deny`、`fail`
273
+ - 默认值分别是 `approve-all` 和 `fail`
274
+ - 也可以直接在微信里通过 `/pm` 和 `/pm auto` 修改
275
+
214
276
  ### 日志配置
215
277
 
216
278
  `config.json` 支持可选的 `logging` 配置:
@@ -28,6 +28,138 @@ var __export = (target, all) => {
28
28
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
29
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
30
 
31
+ // src/transport/prompt-output.ts
32
+ function getPromptText(result) {
33
+ const stdoutOutput = extractPromptOutput(result.stdout);
34
+ if (result.code === 0) {
35
+ return sanitizePromptText(stdoutOutput.text);
36
+ }
37
+ const preferredError = extractPromptFailureMessage(result);
38
+ if (preferredError) {
39
+ throw new PromptCommandError(preferredError, result);
40
+ }
41
+ const stderrOutput = extractPromptOutput(result.stderr);
42
+ const partialReply = [stdoutOutput, stderrOutput].filter((output) => output.hasAgentMessage && output.text.length > 0).map((output) => sanitizePromptText(output.text)).find((text) => text.length > 0);
43
+ if (partialReply) {
44
+ return partialReply;
45
+ }
46
+ throw new PromptCommandError(`command failed with exit code ${result.code}`, result);
47
+ }
48
+ function normalizeCommandError(result) {
49
+ const preferredError = extractPromptFailureMessage(result);
50
+ if (preferredError) {
51
+ return preferredError;
52
+ }
53
+ return result.stdout.trim() || null;
54
+ }
55
+ function extractPromptFailureMessage(result) {
56
+ const rpcMessages = extractJsonRpcErrorMessages(result.stderr).concat(extractJsonRpcErrorMessages(result.stdout)).filter((message) => message.length > 0);
57
+ const preferredMessage = [...rpcMessages].reverse().find((message) => message !== "Resource not found");
58
+ if (preferredMessage) {
59
+ return preferredMessage;
60
+ }
61
+ if (rpcMessages.length > 0) {
62
+ return rpcMessages[rpcMessages.length - 1] ?? null;
63
+ }
64
+ const stderrText = result.stderr.trim();
65
+ if (stderrText.length > 0) {
66
+ return stderrText;
67
+ }
68
+ return null;
69
+ }
70
+ function extractPromptOutput(output) {
71
+ const lines = output.split(`
72
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
73
+ const messageSegments = [];
74
+ let currentSegment = "";
75
+ let hasAgentMessage = false;
76
+ for (const line of lines) {
77
+ try {
78
+ const event = JSON.parse(line);
79
+ const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
80
+ if (isMessageChunk) {
81
+ hasAgentMessage = true;
82
+ const chunk = event.params.update.content.text ?? "";
83
+ if (chunk.length > 0) {
84
+ currentSegment += chunk;
85
+ }
86
+ continue;
87
+ }
88
+ if (currentSegment.trim().length > 0) {
89
+ messageSegments.push(currentSegment.trim());
90
+ }
91
+ currentSegment = "";
92
+ } catch {
93
+ if (currentSegment.trim().length > 0) {
94
+ messageSegments.push(currentSegment.trim());
95
+ currentSegment = "";
96
+ }
97
+ }
98
+ }
99
+ if (currentSegment.trim().length > 0) {
100
+ messageSegments.push(currentSegment.trim());
101
+ }
102
+ if (messageSegments.length > 0) {
103
+ return {
104
+ text: messageSegments[messageSegments.length - 1],
105
+ hasAgentMessage
106
+ };
107
+ }
108
+ return {
109
+ text: output.trim(),
110
+ hasAgentMessage
111
+ };
112
+ }
113
+ function sanitizePromptText(text) {
114
+ const trimmed = text.trim();
115
+ const paragraphs = trimmed.split(/\n\s*\n/);
116
+ if (paragraphs.length < 2) {
117
+ return trimmed;
118
+ }
119
+ const firstParagraph = paragraphs[0].trim().replace(/\s+/g, " ").toLowerCase();
120
+ if (!looksLikeWorkflowPreamble(firstParagraph)) {
121
+ return trimmed;
122
+ }
123
+ return paragraphs.slice(1).join(`
124
+
125
+ `).trim();
126
+ }
127
+ function looksLikeWorkflowPreamble(paragraph) {
128
+ if (!paragraph.startsWith("using ")) {
129
+ return false;
130
+ }
131
+ return paragraph.includes("using-superpowers") || paragraph.includes("repo workflow requirement") || paragraph.includes("workflow requirement") || paragraph.includes("before responding") || paragraph.includes("skill check");
132
+ }
133
+ function extractJsonRpcErrorMessages(output) {
134
+ return output.split(`
135
+ `).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
136
+ try {
137
+ const payload = JSON.parse(line);
138
+ if (typeof payload.error?.message === "string" && payload.error.message.length > 0) {
139
+ return [payload.error.message];
140
+ }
141
+ } catch {
142
+ return [];
143
+ }
144
+ return [];
145
+ });
146
+ }
147
+ var PromptCommandError;
148
+ var init_prompt_output = __esm(() => {
149
+ PromptCommandError = class PromptCommandError extends Error {
150
+ exitCode;
151
+ stdout;
152
+ stderr;
153
+ constructor(message, result) {
154
+ super(message);
155
+ this.name = "PromptCommandError";
156
+ this.exitCode = result.code;
157
+ this.stdout = result.stdout;
158
+ this.stderr = result.stderr;
159
+ }
160
+ };
161
+ });
162
+
31
163
  // src/process/spawn-command.ts
32
164
  function resolveSpawnCommand(command, args) {
33
165
  if (SCRIPT_FILE_PATTERN.test(command)) {
@@ -47,6 +179,8 @@ var init_spawn_command = __esm(() => {
47
179
  import { createInterface } from "node:readline";
48
180
 
49
181
  // src/bridge/bridge-server.ts
182
+ init_prompt_output();
183
+
50
184
  class BridgeServer {
51
185
  runtime;
52
186
  constructor(runtime) {
@@ -69,7 +203,14 @@ class BridgeServer {
69
203
  ok: false,
70
204
  error: {
71
205
  code: "BRIDGE_INTERNAL_ERROR",
72
- message
206
+ message,
207
+ ...error instanceof PromptCommandError ? {
208
+ details: {
209
+ exitCode: error.exitCode,
210
+ stdout: error.stdout,
211
+ stderr: error.stderr
212
+ }
213
+ } : {}
73
214
  }
74
215
  })}
75
216
  `;
@@ -124,6 +265,7 @@ function asOptionalString(value) {
124
265
 
125
266
  // src/bridge/bridge-runtime.ts
126
267
  init_spawn_command();
268
+ init_prompt_output();
127
269
  import { spawn } from "node:child_process";
128
270
  import { fileURLToPath } from "node:url";
129
271
 
@@ -131,10 +273,12 @@ class BridgeRuntime {
131
273
  command;
132
274
  run;
133
275
  runSessionCreate;
134
- constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner) {
276
+ options;
277
+ constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}) {
135
278
  this.command = command;
136
279
  this.run = run;
137
280
  this.runSessionCreate = runSessionCreate;
281
+ this.options = options;
138
282
  }
139
283
  async hasSession(input) {
140
284
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
@@ -169,17 +313,14 @@ class BridgeRuntime {
169
313
  return {};
170
314
  }
171
315
  async prompt(input) {
172
- const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
316
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
173
317
  "prompt",
174
318
  "-s",
175
319
  input.name,
176
320
  input.text
177
321
  ]));
178
322
  const result = await this.run(spawnSpec.command, spawnSpec.args);
179
- if (result.code !== 0) {
180
- throw new Error(result.stderr || result.stdout || "prompt failed");
181
- }
182
- return { text: result.stdout.trim() };
323
+ return { text: getPromptText(result) };
183
324
  }
184
325
  async cancel(input) {
185
326
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
@@ -200,10 +341,37 @@ class BridgeRuntime {
200
341
  return {};
201
342
  }
202
343
  buildSessionArgs(input, tail) {
344
+ const prefix = [
345
+ "--format",
346
+ "quiet",
347
+ "--cwd",
348
+ input.cwd,
349
+ ...this.buildPermissionArgs()
350
+ ];
203
351
  if (input.agentCommand) {
204
- return ["--format", "quiet", "--cwd", input.cwd, "--agent", input.agentCommand, ...tail];
352
+ return [...prefix, "--agent", input.agentCommand, ...tail];
205
353
  }
206
- return ["--format", "quiet", "--cwd", input.cwd, input.agent, ...tail];
354
+ return [...prefix, input.agent, ...tail];
355
+ }
356
+ buildPromptArgs(input, tail) {
357
+ const prefix = [
358
+ "--format",
359
+ "json",
360
+ "--json-strict",
361
+ "--cwd",
362
+ input.cwd,
363
+ ...this.buildPermissionArgs()
364
+ ];
365
+ if (input.agentCommand) {
366
+ return [...prefix, "--agent", input.agentCommand, ...tail];
367
+ }
368
+ return [...prefix, input.agent, ...tail];
369
+ }
370
+ buildPermissionArgs() {
371
+ const permissionMode = this.options.permissionMode ?? "approve-all";
372
+ const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "fail";
373
+ const modeFlag = permissionMode === "approve-reads" ? "--approve-reads" : permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
374
+ return [modeFlag, "--non-interactive-permissions", nonInteractivePermissions];
207
375
  }
208
376
  }
209
377
  async function defaultRunner(command, args) {
@@ -245,7 +413,10 @@ async function shellSessionCreateRunner(command, args, cwd) {
245
413
  }
246
414
 
247
415
  // src/bridge/bridge-main.ts
248
- var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx"));
416
+ var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
417
+ permissionMode: process.env.WEACPX_BRIDGE_PERMISSION_MODE === "approve-reads" || process.env.WEACPX_BRIDGE_PERMISSION_MODE === "deny-all" ? process.env.WEACPX_BRIDGE_PERMISSION_MODE : "approve-all",
418
+ nonInteractivePermissions: process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS === "deny" ? "deny" : "fail"
419
+ }));
249
420
  var input = createInterface({
250
421
  input: process.stdin,
251
422
  crlfDelay: Infinity