weacpx 0.3.0 → 0.3.2
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 +93 -0
- package/dist/bridge/bridge-main.js +173 -13
- package/dist/cli.js +3041 -1471
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -145,6 +145,50 @@ hello
|
|
|
145
145
|
| `weacpx stop` | 停止后台实例 |
|
|
146
146
|
| `weacpx doctor` | 运行环境诊断 |
|
|
147
147
|
| `weacpx version` | 查看当前版本 |
|
|
148
|
+
| `weacpx workspace list` | 查看本机已注册的 workspace |
|
|
149
|
+
| `weacpx workspace add [name]` | 把当前目录注册成 workspace;不传 `name` 时使用当前目录名 |
|
|
150
|
+
| `weacpx workspace rm <name>` | 删除 workspace |
|
|
151
|
+
|
|
152
|
+
`workspace` 也可以简写为 `ws`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
weacpx ws add
|
|
156
|
+
weacpx ws list
|
|
157
|
+
weacpx ws rm backend
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `workspace` CLI 怎么用
|
|
161
|
+
|
|
162
|
+
`weacpx workspace` 用来在电脑本机维护 `~/.weacpx/config.json` 里的 `workspaces` 配置。它适合先在终端里注册常用项目目录,然后在微信里用 `--ws <name>` 直接引用。
|
|
163
|
+
|
|
164
|
+
| 命令 | 说明 |
|
|
165
|
+
|------|------|
|
|
166
|
+
| `weacpx workspace list` | 列出已注册的 workspace 及其路径 |
|
|
167
|
+
| `weacpx workspace add` | 把当前目录注册为 workspace,名称默认取当前目录名 |
|
|
168
|
+
| `weacpx workspace add <name>` | 把当前目录注册为指定名称 |
|
|
169
|
+
| `weacpx workspace rm <name>` | 删除指定 workspace |
|
|
170
|
+
|
|
171
|
+
常见用法:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
cd /absolute/path/to/backend
|
|
175
|
+
weacpx workspace add backend
|
|
176
|
+
|
|
177
|
+
cd /absolute/path/to/frontend
|
|
178
|
+
weacpx ws add frontend
|
|
179
|
+
|
|
180
|
+
weacpx ws list
|
|
181
|
+
weacpx ws rm frontend
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
注册后,你可以在微信里直接使用:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
/ss codex --ws backend
|
|
188
|
+
/ss new claude --ws frontend
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
注意:`workspace add` 总是注册**当前终端所在目录**;如果不传名称,会用当前目录名作为 workspace 名称。
|
|
148
192
|
|
|
149
193
|
### `doctor` 怎么用
|
|
150
194
|
|
|
@@ -274,6 +318,55 @@ README 里只保留用户视角的最常用命令。
|
|
|
274
318
|
|
|
275
319
|
- [docs/weacpx-group-usage-guide.md](./docs/weacpx-group-usage-guide.md)
|
|
276
320
|
|
|
321
|
+
|
|
322
|
+
### MCP 集成:外部 coordinator
|
|
323
|
+
|
|
324
|
+
如果你想让 Codex、Claude Code 等外部 MCP host 直接使用 weacpx 的多 Agent 编排能力,可以把 `weacpx mcp-stdio` 配成一个 stdio MCP server。
|
|
325
|
+
|
|
326
|
+
先启动 daemon:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
weacpx start
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
MCP 配置推荐保持简单,不要在启动参数里绑定 workspace:
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"mcpServers": {
|
|
337
|
+
"weacpx": {
|
|
338
|
+
"command": "weacpx",
|
|
339
|
+
"args": ["mcp-stdio"]
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
外部 host 调用 `delegate_request` 时传 `workingDirectory`,weacpx 会让被委派的 worker 在这个目录工作:
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"targetAgent": "claude",
|
|
350
|
+
"task": "审查这个改动的风险点",
|
|
351
|
+
"workingDirectory": "/absolute/path/to/your/repo"
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Windows 上如果 MCP host 不会帮你解析带参数的 `command`,把 `node.exe` 放在 `command`,把 weacpx 脚本和参数放在 `args`:
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"type": "stdio",
|
|
360
|
+
"command": "D:\\Users\\you\\.nvmd\\versions\\22.19.0\\node.exe",
|
|
361
|
+
"args": [
|
|
362
|
+
"E:\\projects\\weacpx\\dist\\cli.js",
|
|
363
|
+
"mcp-stdio"
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
更多身份规则、`workingDirectory` 语义、工具列表、流程图和故障排查见:[docs/external-mcp.md](./docs/external-mcp.md)。
|
|
369
|
+
|
|
277
370
|
## 常见场景
|
|
278
371
|
|
|
279
372
|
### 在手机上继续盯一个本地项目
|
|
@@ -212,6 +212,115 @@ var init_spawn_command = __esm(() => {
|
|
|
212
212
|
SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
// src/transport/prompt-media.ts
|
|
216
|
+
import { mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
217
|
+
import { tmpdir as defaultTmpdir } from "node:os";
|
|
218
|
+
import path from "node:path";
|
|
219
|
+
async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
|
|
220
|
+
if (!media) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (media.type !== "image") {
|
|
224
|
+
throw new Error("prompt media type is not supported; only image media is supported");
|
|
225
|
+
}
|
|
226
|
+
const imageData = await deps.readImageFile(media.filePath, MAX_STRUCTURED_IMAGE_BYTES);
|
|
227
|
+
if (imageData.byteLength === 0) {
|
|
228
|
+
throw new Error("image prompt must not be empty");
|
|
229
|
+
}
|
|
230
|
+
if (imageData.byteLength > MAX_STRUCTURED_IMAGE_BYTES) {
|
|
231
|
+
throw new Error(`image prompt exceeds ${MAX_STRUCTURED_IMAGE_BYTES} bytes`);
|
|
232
|
+
}
|
|
233
|
+
const blocks = [];
|
|
234
|
+
if (text.trim().length > 0) {
|
|
235
|
+
blocks.push({ type: "text", text });
|
|
236
|
+
}
|
|
237
|
+
blocks.push({
|
|
238
|
+
type: "image",
|
|
239
|
+
mimeType: resolveImageMimeType(imageData, media.mimeType),
|
|
240
|
+
data: imageData.toString("base64")
|
|
241
|
+
});
|
|
242
|
+
let dir = "";
|
|
243
|
+
try {
|
|
244
|
+
dir = await deps.mkdtemp(path.join(deps.tmpdir(), "weacpx-acp-prompt-"));
|
|
245
|
+
const filePath = path.join(dir, "prompt.json");
|
|
246
|
+
await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
|
|
247
|
+
return {
|
|
248
|
+
filePath,
|
|
249
|
+
cleanup: async () => {
|
|
250
|
+
await deps.rm(dir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (dir) {
|
|
255
|
+
try {
|
|
256
|
+
await deps.rm(dir, { recursive: true, force: true });
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function readImageFileBounded(filePath, maxBytes) {
|
|
263
|
+
const handle = await open(filePath, "r");
|
|
264
|
+
try {
|
|
265
|
+
const imageStats = await handle.stat();
|
|
266
|
+
if (!imageStats.isFile()) {
|
|
267
|
+
throw new Error("image prompt path must be a regular file");
|
|
268
|
+
}
|
|
269
|
+
if (imageStats.size > maxBytes) {
|
|
270
|
+
throw new Error(`image prompt exceeds ${maxBytes} bytes`);
|
|
271
|
+
}
|
|
272
|
+
const chunks = [];
|
|
273
|
+
let total = 0;
|
|
274
|
+
let position = 0;
|
|
275
|
+
const chunkSize = 1024 * 1024;
|
|
276
|
+
while (total <= maxBytes) {
|
|
277
|
+
const buffer = Buffer.allocUnsafe(Math.min(chunkSize, maxBytes + 1 - total));
|
|
278
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
279
|
+
if (bytesRead === 0)
|
|
280
|
+
break;
|
|
281
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
282
|
+
total += bytesRead;
|
|
283
|
+
position += bytesRead;
|
|
284
|
+
}
|
|
285
|
+
return Buffer.concat(chunks, total);
|
|
286
|
+
} finally {
|
|
287
|
+
await handle.close();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function resolveImageMimeType(buffer, declaredMimeType) {
|
|
291
|
+
if (/^image\/[A-Za-z0-9.+-]+$/.test(declaredMimeType) && declaredMimeType !== "image/*") {
|
|
292
|
+
return declaredMimeType;
|
|
293
|
+
}
|
|
294
|
+
if (buffer.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) {
|
|
295
|
+
return "image/png";
|
|
296
|
+
}
|
|
297
|
+
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
|
|
298
|
+
return "image/jpeg";
|
|
299
|
+
}
|
|
300
|
+
const header6 = buffer.subarray(0, 6).toString("ascii");
|
|
301
|
+
if (header6 === "GIF87a" || header6 === "GIF89a") {
|
|
302
|
+
return "image/gif";
|
|
303
|
+
}
|
|
304
|
+
if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
305
|
+
return "image/webp";
|
|
306
|
+
}
|
|
307
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).toString("ascii") === "BM") {
|
|
308
|
+
return "image/bmp";
|
|
309
|
+
}
|
|
310
|
+
return "image/png";
|
|
311
|
+
}
|
|
312
|
+
var MAX_STRUCTURED_IMAGE_BYTES, defaultStructuredPromptFileDeps;
|
|
313
|
+
var init_prompt_media = __esm(() => {
|
|
314
|
+
MAX_STRUCTURED_IMAGE_BYTES = 100 * 1024 * 1024;
|
|
315
|
+
defaultStructuredPromptFileDeps = {
|
|
316
|
+
readImageFile: readImageFileBounded,
|
|
317
|
+
mkdtemp,
|
|
318
|
+
writeFile,
|
|
319
|
+
rm,
|
|
320
|
+
tmpdir: defaultTmpdir
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
|
|
215
324
|
// src/transport/streaming-prompt.ts
|
|
216
325
|
function createStreamingPromptState(formatToolCalls = false) {
|
|
217
326
|
return {
|
|
@@ -354,6 +463,7 @@ var init_streaming_prompt = __esm(() => {
|
|
|
354
463
|
|
|
355
464
|
// src/recovery/discover-parent-package-paths.ts
|
|
356
465
|
import { spawn } from "node:child_process";
|
|
466
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
357
467
|
import { access } from "node:fs/promises";
|
|
358
468
|
import { homedir } from "node:os";
|
|
359
469
|
import { dirname, join } from "node:path";
|
|
@@ -416,9 +526,9 @@ function isUnder(child, parent) {
|
|
|
416
526
|
const p = parent.replace(/[\\/]+$/, "");
|
|
417
527
|
return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
|
|
418
528
|
}
|
|
419
|
-
async function defaultFsExists(
|
|
529
|
+
async function defaultFsExists(path2) {
|
|
420
530
|
try {
|
|
421
|
-
await access(
|
|
531
|
+
await access(path2);
|
|
422
532
|
return true;
|
|
423
533
|
} catch {
|
|
424
534
|
return false;
|
|
@@ -426,8 +536,8 @@ async function defaultFsExists(path) {
|
|
|
426
536
|
}
|
|
427
537
|
function defaultResolveFromCwd(name, cwd) {
|
|
428
538
|
try {
|
|
429
|
-
const pkgJson =
|
|
430
|
-
paths: [cwd, ...
|
|
539
|
+
const pkgJson = require2.resolve(`${name}/package.json`, {
|
|
540
|
+
paths: [cwd, ...require2.resolve.paths(name) ?? []]
|
|
431
541
|
});
|
|
432
542
|
return dirname(pkgJson);
|
|
433
543
|
} catch {
|
|
@@ -480,11 +590,14 @@ async function defaultQueryPackageManagerRoot(tool) {
|
|
|
480
590
|
});
|
|
481
591
|
});
|
|
482
592
|
}
|
|
483
|
-
var
|
|
593
|
+
var require2;
|
|
594
|
+
var init_discover_parent_package_paths = __esm(() => {
|
|
595
|
+
require2 = createRequire2(import.meta.url);
|
|
596
|
+
});
|
|
484
597
|
|
|
485
598
|
// src/process/terminate-process-tree.ts
|
|
486
599
|
import { spawn as spawn2 } from "node:child_process";
|
|
487
|
-
async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
600
|
+
async function terminateProcessTree(pid, options = {}, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
488
601
|
process.kill(targetPid, signal);
|
|
489
602
|
}, isProcessRunning = defaultIsProcessRunning) {
|
|
490
603
|
if (pid <= 0) {
|
|
@@ -496,7 +609,7 @@ async function terminateProcessTree(pid, platform = process.platform, runCommand
|
|
|
496
609
|
} catch {}
|
|
497
610
|
return;
|
|
498
611
|
}
|
|
499
|
-
const targetPid =
|
|
612
|
+
const targetPid = options.detachedProcessGroup ? -pid : pid;
|
|
500
613
|
try {
|
|
501
614
|
killProcess(targetPid, "SIGTERM");
|
|
502
615
|
} catch {
|
|
@@ -692,7 +805,7 @@ async function terminateAcpxQueueOwner(sessionId) {
|
|
|
692
805
|
return;
|
|
693
806
|
}
|
|
694
807
|
if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
695
|
-
await terminateProcessTree(owner.pid);
|
|
808
|
+
await terminateProcessTree(owner.pid, { detachedProcessGroup: true });
|
|
696
809
|
}
|
|
697
810
|
await unlink(lockPath).catch(() => {});
|
|
698
811
|
}
|
|
@@ -783,6 +896,7 @@ class BridgeRequestScheduler {
|
|
|
783
896
|
// src/bridge/bridge-runtime.ts
|
|
784
897
|
init_spawn_command();
|
|
785
898
|
init_prompt_output();
|
|
899
|
+
init_prompt_media();
|
|
786
900
|
init_streaming_prompt();
|
|
787
901
|
import { copyFile, readdir } from "node:fs/promises";
|
|
788
902
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -929,15 +1043,22 @@ class BridgeRuntime {
|
|
|
929
1043
|
}
|
|
930
1044
|
async prompt(input, onEvent) {
|
|
931
1045
|
await this.launchMcpQueueOwnerIfNeeded(input);
|
|
1046
|
+
const structuredPrompt = await createStructuredPromptFile(input.text, input.media);
|
|
932
1047
|
const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
|
|
933
1048
|
"prompt",
|
|
934
1049
|
"-s",
|
|
935
1050
|
input.name,
|
|
936
|
-
input.text
|
|
1051
|
+
...structuredPrompt ? ["--file", structuredPrompt.filePath] : [input.text]
|
|
937
1052
|
]));
|
|
938
1053
|
const formatToolCalls = input.replyMode === "verbose";
|
|
939
|
-
|
|
940
|
-
|
|
1054
|
+
try {
|
|
1055
|
+
const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent, { formatToolCalls }) : await this.run(spawnSpec.command, spawnSpec.args);
|
|
1056
|
+
return { text: getPromptText(result) };
|
|
1057
|
+
} finally {
|
|
1058
|
+
try {
|
|
1059
|
+
await structuredPrompt?.cleanup();
|
|
1060
|
+
} catch {}
|
|
1061
|
+
}
|
|
941
1062
|
}
|
|
942
1063
|
async launchMcpQueueOwnerIfNeeded(input) {
|
|
943
1064
|
if (!input.mcpCoordinatorSession) {
|
|
@@ -1331,6 +1452,7 @@ class BridgeServer {
|
|
|
1331
1452
|
}
|
|
1332
1453
|
});
|
|
1333
1454
|
case "prompt":
|
|
1455
|
+
const media = asOptionalPromptMedia(params.media);
|
|
1334
1456
|
return await this.runtime.prompt({
|
|
1335
1457
|
agent: requireString(params, "agent"),
|
|
1336
1458
|
agentCommand: asOptionalString(params.agentCommand),
|
|
@@ -1338,8 +1460,9 @@ class BridgeServer {
|
|
|
1338
1460
|
name: requireString(params, "name"),
|
|
1339
1461
|
mcpCoordinatorSession: asOptionalString(params.mcpCoordinatorSession),
|
|
1340
1462
|
mcpSourceHandle: asOptionalString(params.mcpSourceHandle),
|
|
1341
|
-
text:
|
|
1342
|
-
replyMode: asOptionalReplyMode(params.replyMode)
|
|
1463
|
+
text: requirePromptText(params, media),
|
|
1464
|
+
replyMode: asOptionalReplyMode(params.replyMode),
|
|
1465
|
+
media
|
|
1343
1466
|
}, (event) => {
|
|
1344
1467
|
if (event.type === "prompt.segment") {
|
|
1345
1468
|
writeLine?.(encodeBridgePromptSegmentEvent({
|
|
@@ -1445,6 +1568,16 @@ function requireString(params, key) {
|
|
|
1445
1568
|
}
|
|
1446
1569
|
return value;
|
|
1447
1570
|
}
|
|
1571
|
+
function requirePromptText(params, media) {
|
|
1572
|
+
const value = params.text;
|
|
1573
|
+
if (typeof value !== "string") {
|
|
1574
|
+
throw new BridgeInvalidRequestError("text must be a non-empty string");
|
|
1575
|
+
}
|
|
1576
|
+
if (value.length === 0 && media?.type !== "image") {
|
|
1577
|
+
throw new BridgeInvalidRequestError("text must be a non-empty string unless image media is provided");
|
|
1578
|
+
}
|
|
1579
|
+
return value;
|
|
1580
|
+
}
|
|
1448
1581
|
function requirePermissionMode(params, key) {
|
|
1449
1582
|
const value = params[key];
|
|
1450
1583
|
if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
|
|
@@ -1465,6 +1598,33 @@ function asOptionalString(value) {
|
|
|
1465
1598
|
}
|
|
1466
1599
|
return value;
|
|
1467
1600
|
}
|
|
1601
|
+
function asOptionalPromptMedia(value) {
|
|
1602
|
+
if (value === undefined) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1606
|
+
throw new BridgeInvalidRequestError("media must be an object when provided");
|
|
1607
|
+
}
|
|
1608
|
+
const record = value;
|
|
1609
|
+
const type = record.type;
|
|
1610
|
+
const filePath = record.filePath;
|
|
1611
|
+
const mimeType = record.mimeType;
|
|
1612
|
+
if (type !== "image") {
|
|
1613
|
+
throw new BridgeInvalidRequestError("media.type must be image");
|
|
1614
|
+
}
|
|
1615
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
1616
|
+
throw new BridgeInvalidRequestError("media.filePath must be a non-empty string");
|
|
1617
|
+
}
|
|
1618
|
+
if (typeof mimeType !== "string" || mimeType.length === 0) {
|
|
1619
|
+
throw new BridgeInvalidRequestError("media.mimeType must be a non-empty string");
|
|
1620
|
+
}
|
|
1621
|
+
return {
|
|
1622
|
+
type,
|
|
1623
|
+
filePath,
|
|
1624
|
+
mimeType,
|
|
1625
|
+
...typeof record.fileName === "string" && record.fileName.length > 0 ? { fileName: record.fileName } : {}
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1468
1628
|
var VALID_REPLY_MODES = new Set(["stream", "final", "verbose"]);
|
|
1469
1629
|
function asOptionalReplyMode(value) {
|
|
1470
1630
|
if (typeof value !== "string" || !VALID_REPLY_MODES.has(value)) {
|