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 +71 -9
- package/dist/bridge/bridge-main.js +181 -10
- package/dist/cli.js +2853 -231
- package/package.json +8 -3
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 [
|
|
352
|
+
return [...prefix, "--agent", input.agentCommand, ...tail];
|
|
205
353
|
}
|
|
206
|
-
return [
|
|
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
|