weacpx 0.2.1 → 0.2.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.
@@ -276,6 +276,39 @@ function normalizeBridgeNonInteractivePermissions(value) {
276
276
  // src/bridge/bridge-server.ts
277
277
  init_prompt_output();
278
278
 
279
+ // src/bridge/bridge-request-scheduler.ts
280
+ class BridgeRequestScheduler {
281
+ sessions = new Map;
282
+ run(sessionName, lane, task) {
283
+ if (lane === "control") {
284
+ return Promise.resolve().then(task);
285
+ }
286
+ const state = this.sessions.get(sessionName) ?? this.createSessionState(sessionName);
287
+ state.pendingNormals += 1;
288
+ const result = state.tail.then(() => task());
289
+ state.tail = result.then(() => {
290
+ return;
291
+ }, () => {
292
+ return;
293
+ });
294
+ return result.finally(() => {
295
+ state.pendingNormals -= 1;
296
+ if (state.pendingNormals === 0 && this.sessions.get(sessionName) === state) {
297
+ this.sessions.delete(sessionName);
298
+ }
299
+ });
300
+ }
301
+ createSessionState(sessionName) {
302
+ const state = {
303
+ pendingNormals: 0,
304
+ tail: Promise.resolve()
305
+ };
306
+ this.sessions.set(sessionName, state);
307
+ return state;
308
+ }
309
+ }
310
+
311
+ // src/bridge/bridge-server.ts
279
312
  class BridgeInvalidRequestError extends Error {
280
313
  }
281
314
  var BRIDGE_METHODS = new Set([
@@ -288,9 +321,11 @@ var BRIDGE_METHODS = new Set([
288
321
  "setMode",
289
322
  "cancel"
290
323
  ]);
324
+ var SESSION_SCOPED_METHODS = new Set(["hasSession", "ensureSession", "prompt", "setMode", "cancel"]);
291
325
 
292
326
  class BridgeServer {
293
327
  runtime;
328
+ scheduler = new BridgeRequestScheduler;
294
329
  constructor(runtime) {
295
330
  this.runtime = runtime;
296
331
  }
@@ -299,7 +334,7 @@ class BridgeServer {
299
334
  try {
300
335
  const request = parseBridgeRequest(line);
301
336
  requestId = request.id;
302
- const result = await this.dispatch(request.id, request.method, request.params, writeLine);
337
+ const result = await this.dispatchRequest(request.id, request.method, request.params, writeLine);
303
338
  return `${JSON.stringify({
304
339
  id: request.id,
305
340
  ok: true,
@@ -326,6 +361,21 @@ class BridgeServer {
326
361
  `;
327
362
  }
328
363
  }
364
+ async dispatchRequest(requestId, method, params, writeLine) {
365
+ if (!SESSION_SCOPED_METHODS.has(method)) {
366
+ return await this.dispatch(requestId, method, params, writeLine);
367
+ }
368
+ const sessionName = getSessionName(params);
369
+ if (!sessionName) {
370
+ return await this.dispatch(requestId, method, params, writeLine);
371
+ }
372
+ const sessionKey = getSessionScheduleKey(params);
373
+ if (!sessionKey) {
374
+ return await this.dispatch(requestId, method, params, writeLine);
375
+ }
376
+ const lane = method === "cancel" ? "control" : "normal";
377
+ return await this.scheduler.run(sessionKey, lane, () => this.dispatch(requestId, method, params, writeLine));
378
+ }
329
379
  async dispatch(requestId, method, params, writeLine) {
330
380
  switch (method) {
331
381
  case "ping":
@@ -431,6 +481,24 @@ function parseBridgeRequest(line) {
431
481
  params
432
482
  };
433
483
  }
484
+ function getSessionName(params) {
485
+ return asNonEmptyString(params.name);
486
+ }
487
+ function getSessionScheduleKey(params) {
488
+ const name = asNonEmptyString(params.name);
489
+ const cwd = asNonEmptyString(params.cwd);
490
+ const agentIdentity = asNonEmptyString(params.agentCommand) ?? asNonEmptyString(params.agent);
491
+ if (!name || !cwd || !agentIdentity) {
492
+ return;
493
+ }
494
+ return JSON.stringify([agentIdentity, cwd, name]);
495
+ }
496
+ function asNonEmptyString(value) {
497
+ if (typeof value !== "string" || value.length === 0) {
498
+ return;
499
+ }
500
+ return value;
501
+ }
434
502
  function requireString(params, key) {
435
503
  const value = params[key];
436
504
  if (typeof value !== "string" || value.length === 0) {
@@ -733,17 +801,53 @@ async function tryRepairAcpxSessionIndex() {
733
801
  }
734
802
 
735
803
  // src/bridge/bridge-main.ts
736
- var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
737
- permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
738
- nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
739
- }));
740
- var input = createInterface({
741
- input: process.stdin,
742
- crlfDelay: Infinity
743
- });
744
- for await (const line of input) {
745
- const response = await server.handleLine(line, (chunk) => {
746
- process.stdout.write(chunk);
804
+ async function processBridgeInput(options) {
805
+ const pendingWrites = new Set;
806
+ let firstError;
807
+ for await (const line of options.input) {
808
+ const pendingWrite = (async () => {
809
+ const response = await options.server.handleLine(line, (chunk) => {
810
+ options.write(chunk);
811
+ });
812
+ options.write(response);
813
+ })();
814
+ const observedPendingWrite = pendingWrite.catch((error) => {
815
+ if (firstError === undefined) {
816
+ firstError = error;
817
+ options.input.close();
818
+ }
819
+ });
820
+ pendingWrites.add(pendingWrite);
821
+ observedPendingWrite.finally(() => {
822
+ pendingWrites.delete(pendingWrite);
823
+ });
824
+ }
825
+ await Promise.allSettled(pendingWrites);
826
+ if (firstError !== undefined) {
827
+ throw firstError;
828
+ }
829
+ }
830
+ async function runBridgeMain() {
831
+ const server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
832
+ permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
833
+ nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
834
+ }));
835
+ const input = createInterface({
836
+ input: process.stdin,
837
+ crlfDelay: Infinity
838
+ });
839
+ await processBridgeInput({
840
+ input,
841
+ server,
842
+ write: (chunk) => {
843
+ process.stdout.write(chunk);
844
+ }
747
845
  });
748
- process.stdout.write(response);
749
846
  }
847
+ if (__require.main == __require.module) {
848
+ await runBridgeMain();
849
+ }
850
+ export {
851
+ runBridgeMain,
852
+ processBridgeInput
853
+ };
package/dist/cli.js CHANGED
@@ -2423,6 +2423,47 @@ var init_session_guard = __esm(() => {
2423
2423
  pauseUntilMap = new Map;
2424
2424
  });
2425
2425
 
2426
+ // src/weixin/messaging/conversation-executor.ts
2427
+ function createConversationExecutor() {
2428
+ const states = new Map;
2429
+ const getState = (conversationId) => {
2430
+ const existing = states.get(conversationId);
2431
+ if (existing)
2432
+ return existing;
2433
+ const created = { activeControls: 0 };
2434
+ states.set(conversationId, created);
2435
+ return created;
2436
+ };
2437
+ const cleanupState = (conversationId, state) => {
2438
+ if (!state.normalTail && state.activeControls === 0) {
2439
+ states.delete(conversationId);
2440
+ }
2441
+ };
2442
+ return {
2443
+ run(conversationId, lane, task) {
2444
+ const state = getState(conversationId);
2445
+ if (lane === "control") {
2446
+ state.activeControls += 1;
2447
+ return Promise.resolve().then(task).finally(() => {
2448
+ state.activeControls -= 1;
2449
+ cleanupState(conversationId, state);
2450
+ });
2451
+ }
2452
+ const previous = state.normalTail ?? Promise.resolve();
2453
+ const next = previous.catch(() => {
2454
+ return;
2455
+ }).then(task);
2456
+ state.normalTail = next;
2457
+ return next.finally(() => {
2458
+ if (state.normalTail === next) {
2459
+ state.normalTail = undefined;
2460
+ }
2461
+ cleanupState(conversationId, state);
2462
+ });
2463
+ }
2464
+ };
2465
+ }
2466
+
2426
2467
  // src/weixin/api/types.ts
2427
2468
  var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
2428
2469
  var init_types = __esm(() => {
@@ -3360,6 +3401,10 @@ function extractTextBody(itemList) {
3360
3401
  }
3361
3402
  return "";
3362
3403
  }
3404
+ function getWeixinMessageTurnLane(full) {
3405
+ const textBody = extractTextBody(full.item_list).trim().toLowerCase();
3406
+ return textBody === "/cancel" || textBody === "/stop" ? "control" : "normal";
3407
+ }
3363
3408
  function findMediaItem(itemList) {
3364
3409
  if (!itemList?.length)
3365
3410
  return;
@@ -3611,6 +3656,7 @@ async function monitorWeixinProvider(opts) {
3611
3656
  log(`[weixin] no previous sync buf, starting fresh`);
3612
3657
  }
3613
3658
  const configManager = new WeixinConfigManager({ baseUrl, token }, log);
3659
+ const conversationExecutor = createConversationExecutor();
3614
3660
  const seenMessageIds = new Set;
3615
3661
  const messageIdOrder = [];
3616
3662
  const DEDUP_WINDOW = 100;
@@ -3672,7 +3718,7 @@ async function monitorWeixinProvider(opts) {
3672
3718
  aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
3673
3719
  const fromUserId = full.from_user_id ?? "";
3674
3720
  const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
3675
- await handleWeixinMessageTurn(full, {
3721
+ conversationExecutor.run(full.from_user_id ?? "", getWeixinMessageTurnLane(full), () => handleWeixinMessageTurn(full, {
3676
3722
  accountId,
3677
3723
  agent,
3678
3724
  baseUrl,
@@ -3681,6 +3727,8 @@ async function monitorWeixinProvider(opts) {
3681
3727
  typingTicket: cachedConfig.typingTicket,
3682
3728
  log,
3683
3729
  errLog
3730
+ })).catch((err) => {
3731
+ errLog(`[weixin] message turn failed: ${String(err)}`);
3684
3732
  });
3685
3733
  }
3686
3734
  } catch (err) {
@@ -4852,6 +4900,11 @@ async function handleCancel(context, chatKey) {
4852
4900
  const result = await context.interaction.cancelTransportSession(session);
4853
4901
  return { text: result.message || "cancelled" };
4854
4902
  } catch (error) {
4903
+ const recovered = await context.recovery.tryRecoverMissingSession(session, error);
4904
+ if (recovered) {
4905
+ const result = await context.interaction.cancelTransportSession(recovered);
4906
+ return { text: result.message || "cancelled" };
4907
+ }
4855
4908
  return context.recovery.renderTransportError(session, error);
4856
4909
  }
4857
4910
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weacpx",
3
3
  "description": "使用微信 ClawBot 随时随地通过 `acpx` 控制 Claude Code、Codex 等 Agents。",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "main": "index.js",
6
6
  "directories": {
7
7
  "doc": "docs",