weacpx 0.4.8 → 0.4.10

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/dist/cli.js CHANGED
@@ -1999,6 +1999,7 @@ var require_lib = __commonJS((exports, module) => {
1999
1999
 
2000
2000
  // src/util/private-file.ts
2001
2001
  import { chmod, mkdir, writeFile } from "node:fs/promises";
2002
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2002
2003
  import { dirname } from "node:path";
2003
2004
  async function writePrivateFileAtomic(path, content) {
2004
2005
  await mkdir(dirname(path), { recursive: true });
@@ -2031,6 +2032,25 @@ async function writePrivateFileAtomic(path, content) {
2031
2032
  await release();
2032
2033
  }
2033
2034
  }
2035
+ function writePrivateFileSync(path, content, deps = {}) {
2036
+ mkdirSync(dirname(path), { recursive: true });
2037
+ const platform = deps.platform ?? process.platform;
2038
+ const atomicWrite = deps.atomicWrite ?? ((p, c) => import_write_file_atomic.default.sync(p, c, { mode: PRIVATE_FILE_MODE, encoding: "utf8", fsync: true }));
2039
+ try {
2040
+ atomicWrite(path, content);
2041
+ } catch (error) {
2042
+ if (!isTransientWriteError(error, platform)) {
2043
+ throw error;
2044
+ }
2045
+ const directWrite = deps.directWrite ?? ((p, c) => {
2046
+ writeFileSync(p, c, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
2047
+ try {
2048
+ chmodSync(p, PRIVATE_FILE_MODE);
2049
+ } catch {}
2050
+ });
2051
+ directWrite(path, content);
2052
+ }
2053
+ }
2034
2054
  async function retryTransientWriteErrors(run, options = {}) {
2035
2055
  const platform = options.platform ?? process.platform;
2036
2056
  const maxAttempts = options.maxAttempts ?? WRITE_RETRY_MAX_ATTEMPTS;
@@ -2206,6 +2226,9 @@ function parseConfig(raw, options = {}) {
2206
2226
  throw new Error("transport.permissionPolicy must be a non-empty string");
2207
2227
  }
2208
2228
  }
2229
+ if ("queueOwnerTtlSeconds" in transport && (typeof transport.queueOwnerTtlSeconds !== "number" || !Number.isFinite(transport.queueOwnerTtlSeconds) || transport.queueOwnerTtlSeconds < 0)) {
2230
+ throw new Error("transport.queueOwnerTtlSeconds must be a non-negative number (0 = keep alive forever)");
2231
+ }
2209
2232
  if (!isRecord(raw.agents)) {
2210
2233
  throw new Error("agents must be an object");
2211
2234
  }
@@ -2302,7 +2325,8 @@ function parseConfig(raw, options = {}) {
2302
2325
  ...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
2303
2326
  type: transportType,
2304
2327
  permissionMode,
2305
- nonInteractivePermissions
2328
+ nonInteractivePermissions,
2329
+ queueOwnerTtlSeconds: typeof transport.queueOwnerTtlSeconds === "number" ? transport.queueOwnerTtlSeconds : DEFAULT_QUEUE_OWNER_TTL_SECONDS
2306
2330
  },
2307
2331
  logging: {
2308
2332
  level: resolvedLoggingLevel,
@@ -2424,10 +2448,11 @@ function parseOrchestrationConfig(raw) {
2424
2448
  allowWorkerChainedRequests: raw.allowWorkerChainedRequests === true,
2425
2449
  allowedAgentRequestTargets: Array.isArray(raw.allowedAgentRequestTargets) ? raw.allowedAgentRequestTargets.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestTargets],
2426
2450
  allowedAgentRequestRoles: Array.isArray(raw.allowedAgentRequestRoles) ? raw.allowedAgentRequestRoles.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestRoles],
2427
- progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds
2451
+ progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds,
2452
+ maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
2428
2453
  };
2429
2454
  }
2430
- var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2455
+ var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_QUEUE_OWNER_TTL_SECONDS = 1800, DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2431
2456
  var init_load_config = __esm(() => {
2432
2457
  init_workspace_path();
2433
2458
  DEFAULT_PERF_LOG_CONFIG = {
@@ -2452,7 +2477,8 @@ var init_load_config = __esm(() => {
2452
2477
  allowWorkerChainedRequests: false,
2453
2478
  allowedAgentRequestTargets: [],
2454
2479
  allowedAgentRequestRoles: [],
2455
- progressHeartbeatSeconds: 300
2480
+ progressHeartbeatSeconds: 300,
2481
+ maxParallelTasksPerAgent: 3
2456
2482
  };
2457
2483
  });
2458
2484
 
@@ -2727,7 +2753,7 @@ class DaemonStatusStore {
2727
2753
  var init_daemon_status = () => {};
2728
2754
 
2729
2755
  // src/daemon/daemon-controller.ts
2730
- import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
2756
+ import { mkdir as mkdir3, open, readFile as readFile4, rm as rm2 } from "node:fs/promises";
2731
2757
  import { dirname as dirname3 } from "node:path";
2732
2758
 
2733
2759
  class DaemonController {
@@ -2786,9 +2812,19 @@ class DaemonController {
2786
2812
  if (current.state === "indeterminate") {
2787
2813
  throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
2788
2814
  }
2789
- await this.statusStore.clear();
2790
- const pid = await this.deps.spawnDetached(options);
2791
- await this.writePid(pid);
2815
+ const pidHandle = await this.openPidFileExclusive();
2816
+ let pid;
2817
+ try {
2818
+ await this.statusStore.clear();
2819
+ pid = await this.deps.spawnDetached(options);
2820
+ await pidHandle.write(`${pid}
2821
+ `);
2822
+ } catch (error) {
2823
+ await pidHandle.close().catch(() => {});
2824
+ await rm2(this.paths.pidFile, { force: true }).catch(() => {});
2825
+ throw error;
2826
+ }
2827
+ await pidHandle.close();
2792
2828
  await this.waitForStartupMetadata(pid, options.firstRunOnboarding ? this.onboardingStartupTimeoutMs : this.startupTimeoutMs, options.startupWait);
2793
2829
  return { state: "started", pid };
2794
2830
  }
@@ -2816,10 +2852,16 @@ class DaemonController {
2816
2852
  throw error;
2817
2853
  }
2818
2854
  }
2819
- async writePid(pid) {
2855
+ async openPidFileExclusive() {
2820
2856
  await mkdir3(dirname3(this.paths.pidFile), { recursive: true });
2821
- await writeFile3(this.paths.pidFile, `${pid}
2822
- `);
2857
+ try {
2858
+ return await open(this.paths.pidFile, "wx", 384);
2859
+ } catch (error) {
2860
+ if (error.code === "EEXIST") {
2861
+ throw new Error(`weacpx daemon pid file already exists (${this.paths.pidFile}); another start may be in progress`);
2862
+ }
2863
+ throw error;
2864
+ }
2823
2865
  }
2824
2866
  async clearRuntimeFiles() {
2825
2867
  await rm2(this.paths.pidFile, { force: true });
@@ -2916,15 +2958,17 @@ async function defaultRunProcessCommand(command, args) {
2916
2958
  var init_terminate_process_tree = () => {};
2917
2959
 
2918
2960
  // src/daemon/create-daemon-controller.ts
2919
- import { mkdir as mkdir4, open } from "node:fs/promises";
2961
+ import { mkdir as mkdir4, open as open2 } from "node:fs/promises";
2920
2962
  import { spawn as spawn2 } from "node:child_process";
2921
2963
  function createDaemonController(paths, options) {
2922
2964
  return new DaemonController(paths, {
2923
2965
  isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning2,
2924
2966
  spawnDetached: async (spawnOptions) => {
2925
2967
  await mkdir4(paths.runtimeDir, { recursive: true });
2926
- const stdoutHandle = await open(paths.stdoutLog, "a");
2927
- const stderrHandle = await open(paths.stderrLog, "a");
2968
+ const stdoutHandle = await open2(paths.stdoutLog, "a", 384);
2969
+ const stderrHandle = await open2(paths.stderrLog, "a", 384);
2970
+ await stdoutHandle.chmod(384).catch(() => {});
2971
+ await stderrHandle.chmod(384).catch(() => {});
2928
2972
  try {
2929
2973
  return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd, spawnOptions));
2930
2974
  } finally {
@@ -9645,6 +9689,34 @@ var init_quota_errors = __esm(() => {
9645
9689
  };
9646
9690
  });
9647
9691
 
9692
+ // src/commands/workspace-name.ts
9693
+ function sanitizeWorkspaceName(input, fallback = "workspace") {
9694
+ const sanitized = input.trim().replace(UNSAFE_RUN_RE, "-").replace(TRIM_DASHES_RE, "");
9695
+ return sanitized.length > 0 ? sanitized : fallback;
9696
+ }
9697
+ function allocateWorkspaceName(base, existing) {
9698
+ if (!Object.prototype.hasOwnProperty.call(existing, base))
9699
+ return base;
9700
+ let suffix = 2;
9701
+ while (Object.prototype.hasOwnProperty.call(existing, `${base}-${suffix}`))
9702
+ suffix += 1;
9703
+ return `${base}-${suffix}`;
9704
+ }
9705
+ function isWorkspaceNameValid(input) {
9706
+ return VALID_WORKSPACE_NAME_RE.test(input);
9707
+ }
9708
+ function quoteWorkspaceNameIfNeeded(input) {
9709
+ if (isWorkspaceNameValid(input))
9710
+ return input;
9711
+ return `"${input.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
9712
+ }
9713
+ var VALID_WORKSPACE_NAME_RE, UNSAFE_RUN_RE, TRIM_DASHES_RE;
9714
+ var init_workspace_name = __esm(() => {
9715
+ VALID_WORKSPACE_NAME_RE = /^[a-zA-Z0-9._-]+$/;
9716
+ UNSAFE_RUN_RE = /[^a-zA-Z0-9._-]+/g;
9717
+ TRIM_DASHES_RE = /^-+|-+$/g;
9718
+ });
9719
+
9648
9720
  // src/orchestration/orchestration-types.ts
9649
9721
  function createEmptyOrchestrationState() {
9650
9722
  return {
@@ -9683,7 +9755,7 @@ function isOptionalBoolean(value) {
9683
9755
  return value === undefined || typeof value === "boolean";
9684
9756
  }
9685
9757
  function isTaskStatus(value) {
9686
- return value === "needs_confirmation" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
9758
+ return value === "needs_confirmation" || value === "queued" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
9687
9759
  }
9688
9760
  function isSourceKind(value) {
9689
9761
  return value === "human" || value === "coordinator" || value === "worker";
@@ -9719,7 +9791,7 @@ function isTaskRecord(value) {
9719
9791
  if (!isRecord2(value)) {
9720
9792
  return false;
9721
9793
  }
9722
- return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isString(value.task) && isTaskStatus(value.status) && isString(value.summary) && isString(value.resultText) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.chatKey) && isOptionalString(value.replyContextToken) && isOptionalString(value.accountId) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.coordinatorInjectedAt) && isOptionalString(value.cancelRequestedAt) && isOptionalString(value.cancelCompletedAt) && isOptionalString(value.lastCancelError) && isOptionalBoolean(value.noticePending) && isOptionalString(value.noticeSentAt) && isOptionalString(value.lastNoticeError) && isOptionalBoolean(value.injectionPending) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError) && isOptionalString(value.lastProgressAt) && isOptionalString(value.lastProgressSummary) && isOptionalString(value.groupId) && (value.openQuestion === undefined || isOpenQuestionRecord(value.openQuestion)) && (value.reviewPending === undefined || isReviewPendingRecord(value.reviewPending)) && (value.correctionPending === undefined || isCorrectionPendingRecord(value.correctionPending)) && isOptionalNumber(value.eventSeq) && (value.events === undefined || Array.isArray(value.events) && value.events.every(isTaskEventRecord));
9794
+ return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isString(value.task) && isTaskStatus(value.status) && isString(value.summary) && isString(value.resultText) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.chatKey) && isOptionalString(value.replyContextToken) && isOptionalString(value.accountId) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.coordinatorInjectedAt) && isOptionalString(value.cancelRequestedAt) && isOptionalString(value.cancelCompletedAt) && isOptionalString(value.lastCancelError) && isOptionalBoolean(value.noticePending) && isOptionalString(value.noticeSentAt) && isOptionalString(value.lastNoticeError) && isOptionalBoolean(value.injectionPending) && isOptionalBoolean(value.ephemeralWorkerSession) && isOptionalBoolean(value.ephemeralWorkerSessionClosed) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError) && isOptionalString(value.lastProgressAt) && isOptionalString(value.lastProgressSummary) && isOptionalString(value.groupId) && (value.openQuestion === undefined || isOpenQuestionRecord(value.openQuestion)) && (value.reviewPending === undefined || isReviewPendingRecord(value.reviewPending)) && (value.correctionPending === undefined || isCorrectionPendingRecord(value.correctionPending)) && isOptionalNumber(value.eventSeq) && (value.events === undefined || Array.isArray(value.events) && value.events.every(isTaskEventRecord));
9723
9795
  }
9724
9796
  function isExternalCoordinatorRecord(value) {
9725
9797
  if (!isRecord2(value)) {
@@ -9731,7 +9803,7 @@ function isWorkerBindingRecord(value) {
9731
9803
  if (!isRecord2(value)) {
9732
9804
  return false;
9733
9805
  }
9734
- return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role);
9806
+ return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isOptionalBoolean(value.ephemeral);
9735
9807
  }
9736
9808
  function isGroupRecord(value) {
9737
9809
  if (!isRecord2(value)) {
@@ -9984,20 +10056,33 @@ var init_state_store = __esm(() => {
9984
10056
  });
9985
10057
 
9986
10058
  // src/plugins/plugin-home.ts
9987
- import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
10059
+ import { mkdir as mkdir6, writeFile as writeFile4 } from "node:fs/promises";
9988
10060
  import { homedir as homedir3 } from "node:os";
9989
10061
  import { join as join3 } from "node:path";
10062
+ function coerceMissing(value) {
10063
+ if (value === undefined)
10064
+ return;
10065
+ const trimmed = value.trim();
10066
+ if (!trimmed)
10067
+ return;
10068
+ const lower = trimmed.toLowerCase();
10069
+ if (lower === "undefined" || lower === "null")
10070
+ return;
10071
+ return trimmed;
10072
+ }
9990
10073
  function resolvePluginHome(input = {}) {
9991
- if (input.pluginHome?.trim())
9992
- return input.pluginHome;
9993
- if (process.env.WEACPX_PLUGIN_HOME?.trim())
9994
- return process.env.WEACPX_PLUGIN_HOME;
9995
- const home = input.home ?? process.env.HOME ?? homedir3();
10074
+ const explicit = coerceMissing(input.pluginHome);
10075
+ if (explicit)
10076
+ return explicit;
10077
+ const envOverride = coerceMissing(process.env.WEACPX_PLUGIN_HOME);
10078
+ if (envOverride)
10079
+ return envOverride;
10080
+ const home = coerceMissing(input.home) ?? coerceMissing(process.env.HOME) ?? homedir3();
9996
10081
  return join3(home, ".weacpx", "plugins");
9997
10082
  }
9998
10083
  async function ensurePluginHome(pluginHome) {
9999
10084
  await mkdir6(pluginHome, { recursive: true, mode: 448 });
10000
- await writeFile5(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
10085
+ await writeFile4(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
10001
10086
  `, { flag: "wx" }).catch((error2) => {
10002
10087
  if (error2.code !== "EEXIST")
10003
10088
  throw error2;
@@ -10136,8 +10221,6 @@ function loadWeixinAccount(accountId) {
10136
10221
  return null;
10137
10222
  }
10138
10223
  function saveWeixinAccount(accountId, update) {
10139
- const dir = resolveAccountsDir();
10140
- ensureDirSync(dir);
10141
10224
  const existing = loadWeixinAccount(accountId) ?? {};
10142
10225
  const token = update.token?.trim() || existing.token;
10143
10226
  const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
@@ -10147,11 +10230,7 @@ function saveWeixinAccount(accountId, update) {
10147
10230
  ...baseUrl ? { baseUrl } : {},
10148
10231
  ...userId ? { userId } : {}
10149
10232
  };
10150
- const filePath = resolveAccountPath(accountId);
10151
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
10152
- try {
10153
- fs3.chmodSync(filePath, 384);
10154
- } catch {}
10233
+ writePrivateFileSync(resolveAccountPath(accountId), JSON.stringify(data, null, 2));
10155
10234
  }
10156
10235
  function clearWeixinAccount(accountId) {
10157
10236
  try {
@@ -10202,6 +10281,28 @@ function loadConfigRouteTag(accountId) {
10202
10281
  return;
10203
10282
  }
10204
10283
  }
10284
+ function loadConfigBotAgent(accountId) {
10285
+ try {
10286
+ const configPath = resolveConfigPath();
10287
+ if (!fs3.existsSync(configPath))
10288
+ return;
10289
+ const raw = fs3.readFileSync(configPath, "utf-8");
10290
+ const cfg = JSON.parse(raw);
10291
+ const channels = cfg.channels;
10292
+ const section = channels?.["openclaw-weixin"];
10293
+ if (!section)
10294
+ return;
10295
+ if (accountId) {
10296
+ const accounts = section.accounts;
10297
+ const agent = accounts?.[accountId]?.botAgent;
10298
+ if (typeof agent === "string" && agent.trim())
10299
+ return agent.trim();
10300
+ }
10301
+ return typeof section.botAgent === "string" && section.botAgent.trim() ? section.botAgent.trim() : undefined;
10302
+ } catch {
10303
+ return;
10304
+ }
10305
+ }
10205
10306
  function listWeixinAccountIds() {
10206
10307
  const indexed = listIndexedWeixinAccountIds();
10207
10308
  if (indexed.length > 0)
@@ -10230,6 +10331,7 @@ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com", CDN_BASE_URL = "https://
10230
10331
  var init_accounts = __esm(() => {
10231
10332
  init_ensure_dir();
10232
10333
  init_state_dir();
10334
+ init_private_file();
10233
10335
  });
10234
10336
 
10235
10337
  // src/weixin/util/logger.ts
@@ -10490,8 +10592,83 @@ var init_send_errors = __esm(() => {
10490
10592
 
10491
10593
  // src/weixin/api/api.ts
10492
10594
  import crypto from "node:crypto";
10595
+ function buildClientVersion(version2) {
10596
+ const parts = version2.split(".").map((p) => parseInt(p, 10));
10597
+ const major = parts[0] ?? 0;
10598
+ const minor = parts[1] ?? 0;
10599
+ const patch = parts[2] ?? 0;
10600
+ return (major & 255) << 16 | (minor & 255) << 8 | patch & 255;
10601
+ }
10602
+ function sanitizeBotAgent(raw) {
10603
+ if (!raw || typeof raw !== "string")
10604
+ return DEFAULT_BOT_AGENT;
10605
+ const trimmed = raw.trim();
10606
+ if (!trimmed)
10607
+ return DEFAULT_BOT_AGENT;
10608
+ const productRe = /^[A-Za-z0-9_.\-]{1,32}\/[A-Za-z0-9_.+\-]{1,32}$/;
10609
+ const commentCharRe = /^[\x20-\x27\x2A-\x7E]{1,64}$/;
10610
+ const rawTokens = trimmed.split(/\s+/);
10611
+ const tokens = [];
10612
+ for (let i = 0;i < rawTokens.length; i += 1) {
10613
+ const tok = rawTokens[i];
10614
+ if (tok.startsWith("(") && !tok.endsWith(")")) {
10615
+ let acc = tok;
10616
+ while (i + 1 < rawTokens.length && !acc.endsWith(")")) {
10617
+ i += 1;
10618
+ acc += " " + rawTokens[i];
10619
+ }
10620
+ tokens.push(acc);
10621
+ } else {
10622
+ tokens.push(tok);
10623
+ }
10624
+ }
10625
+ const accepted = [];
10626
+ let pendingProduct = null;
10627
+ for (const tok of tokens) {
10628
+ if (tok.startsWith("(") && tok.endsWith(")")) {
10629
+ const inner = tok.slice(1, -1);
10630
+ if (pendingProduct && commentCharRe.test(inner)) {
10631
+ accepted.push(`${pendingProduct} (${inner})`);
10632
+ pendingProduct = null;
10633
+ } else {
10634
+ if (pendingProduct) {
10635
+ accepted.push(pendingProduct);
10636
+ pendingProduct = null;
10637
+ }
10638
+ }
10639
+ continue;
10640
+ }
10641
+ if (pendingProduct) {
10642
+ accepted.push(pendingProduct);
10643
+ pendingProduct = null;
10644
+ }
10645
+ if (productRe.test(tok)) {
10646
+ pendingProduct = tok;
10647
+ }
10648
+ }
10649
+ if (pendingProduct)
10650
+ accepted.push(pendingProduct);
10651
+ if (accepted.length === 0)
10652
+ return DEFAULT_BOT_AGENT;
10653
+ const joined = accepted.join(" ");
10654
+ if (Buffer.byteLength(joined, "utf-8") <= BOT_AGENT_MAX_LEN)
10655
+ return joined;
10656
+ const truncated = [];
10657
+ let len = 0;
10658
+ for (const t of accepted) {
10659
+ const add = (truncated.length === 0 ? 0 : 1) + Buffer.byteLength(t, "utf-8");
10660
+ if (len + add > BOT_AGENT_MAX_LEN)
10661
+ break;
10662
+ truncated.push(t);
10663
+ len += add;
10664
+ }
10665
+ return truncated.length > 0 ? truncated.join(" ") : DEFAULT_BOT_AGENT;
10666
+ }
10493
10667
  function buildBaseInfo() {
10494
- return { channel_version: CHANNEL_VERSION };
10668
+ return {
10669
+ channel_version: CHANNEL_VERSION,
10670
+ bot_agent: sanitizeBotAgent(loadConfigBotAgent())
10671
+ };
10495
10672
  }
10496
10673
  function ensureTrailingSlash(url) {
10497
10674
  return url.endsWith("/") ? url : `${url}/`;
@@ -10502,6 +10679,9 @@ function randomWechatUin() {
10502
10679
  }
10503
10680
  function buildCommonHeaders() {
10504
10681
  const headers = {};
10682
+ if (ILINK_APP_ID)
10683
+ headers["iLink-App-Id"] = ILINK_APP_ID;
10684
+ headers["iLink-App-ClientVersion"] = String(ILINK_APP_CLIENT_VERSION);
10505
10685
  const routeTag = loadConfigRouteTag();
10506
10686
  if (routeTag) {
10507
10687
  headers.SKRouteTag = routeTag;
@@ -10547,6 +10727,35 @@ async function apiGetFetch(params) {
10547
10727
  throw err;
10548
10728
  }
10549
10729
  }
10730
+ async function apiPostFetch(params) {
10731
+ const base = ensureTrailingSlash(params.baseUrl);
10732
+ const url = new URL(params.endpoint, base);
10733
+ const hdrs = buildCommonHeaders();
10734
+ hdrs["Content-Type"] = "application/json";
10735
+ logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
10736
+ const controller = new AbortController;
10737
+ const t = params.timeoutMs !== undefined ? setTimeout(() => controller.abort(), params.timeoutMs) : undefined;
10738
+ const onAbort = () => controller.abort();
10739
+ params.abortSignal?.addEventListener("abort", onAbort, { once: true });
10740
+ try {
10741
+ const res = await fetch(url.toString(), {
10742
+ method: "POST",
10743
+ headers: hdrs,
10744
+ body: params.body,
10745
+ signal: controller.signal
10746
+ });
10747
+ const rawText = await res.text();
10748
+ logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
10749
+ if (!res.ok) {
10750
+ throw new Error(`${params.label} ${res.status}: ${rawText}`);
10751
+ }
10752
+ return rawText;
10753
+ } finally {
10754
+ if (t !== undefined)
10755
+ clearTimeout(t);
10756
+ params.abortSignal?.removeEventListener("abort", onAbort);
10757
+ }
10758
+ }
10550
10759
  async function apiFetch(params) {
10551
10760
  const base = ensureTrailingSlash(params.baseUrl);
10552
10761
  const url = new URL(params.endpoint, base);
@@ -10705,7 +10914,7 @@ async function sendTyping(params) {
10705
10914
  label: "sendTyping"
10706
10915
  });
10707
10916
  }
10708
- var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
10917
+ var CHANNEL_VERSION, ILINK_APP_CLIENT_VERSION, ILINK_APP_ID, DEFAULT_BOT_AGENT = "weacpx", BOT_AGENT_MAX_LEN = 256, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
10709
10918
  var init_api = __esm(() => {
10710
10919
  init_version();
10711
10920
  init_accounts();
@@ -10713,6 +10922,8 @@ var init_api = __esm(() => {
10713
10922
  init_redact();
10714
10923
  init_send_errors();
10715
10924
  CHANNEL_VERSION = readVersion();
10925
+ ILINK_APP_CLIENT_VERSION = buildClientVersion(CHANNEL_VERSION);
10926
+ ILINK_APP_ID = (process.env.WEACPX_ILINK_APP_ID ?? "").trim();
10716
10927
  });
10717
10928
 
10718
10929
  // node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -11744,22 +11955,47 @@ function purgeExpiredLogins() {
11744
11955
  }
11745
11956
  }
11746
11957
  }
11958
+ function getLocalBotTokenList() {
11959
+ const accountIds = listIndexedWeixinAccountIds();
11960
+ const tokens = [];
11961
+ for (let i = accountIds.length - 1;i >= 0 && tokens.length < 10; i--) {
11962
+ const accountId = accountIds[i];
11963
+ if (!accountId)
11964
+ continue;
11965
+ const data = loadWeixinAccount(accountId);
11966
+ const token = data?.token?.trim();
11967
+ if (token) {
11968
+ tokens.push(token);
11969
+ }
11970
+ }
11971
+ return tokens;
11972
+ }
11747
11973
  async function fetchQRCode(apiBaseUrl, botType) {
11748
11974
  logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
11749
- const rawText = await apiGetFetch({
11975
+ const localTokenList = getLocalBotTokenList();
11976
+ logger.info(`fetchQRCode: local_token_list count=${localTokenList.length}`);
11977
+ const rawText = await apiPostFetch({
11750
11978
  baseUrl: apiBaseUrl,
11751
11979
  endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
11752
- timeoutMs: GET_QRCODE_TIMEOUT_MS,
11980
+ body: JSON.stringify({ local_token_list: localTokenList }),
11753
11981
  label: "fetchQRCode"
11754
11982
  });
11755
11983
  return JSON.parse(rawText);
11756
11984
  }
11757
- async function pollQRStatus(apiBaseUrl, qrcode) {
11758
- logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=***`);
11985
+ function buildPollQRStatusEndpoint(qrcode, verifyCode) {
11986
+ let endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
11987
+ if (verifyCode) {
11988
+ endpoint += `&verify_code=${encodeURIComponent(verifyCode)}`;
11989
+ }
11990
+ return endpoint;
11991
+ }
11992
+ async function pollQRStatus(apiBaseUrl, qrcode, verifyCode) {
11993
+ logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=*** hasVerifyCode=${Boolean(verifyCode)}`);
11994
+ const endpoint = buildPollQRStatusEndpoint(qrcode, verifyCode);
11759
11995
  try {
11760
11996
  const rawText = await apiGetFetch({
11761
11997
  baseUrl: apiBaseUrl,
11762
- endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
11998
+ endpoint,
11763
11999
  timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
11764
12000
  label: "pollQRStatus"
11765
12001
  });
@@ -11774,6 +12010,28 @@ async function pollQRStatus(apiBaseUrl, qrcode) {
11774
12010
  return { status: "wait" };
11775
12011
  }
11776
12012
  }
12013
+ async function readVerifyCodeFromStdin(prompt) {
12014
+ if (!process.stdin.isTTY) {
12015
+ throw new Error("verify code requested but stdin is not a TTY (running in daemon mode?)");
12016
+ }
12017
+ process.stdout.write(prompt);
12018
+ return new Promise((resolve) => {
12019
+ let input = "";
12020
+ const onData = (chunk) => {
12021
+ const str = chunk.toString();
12022
+ input += str;
12023
+ if (input.includes(`
12024
+ `)) {
12025
+ process.stdin.removeListener("data", onData);
12026
+ process.stdin.pause();
12027
+ resolve(input.trim());
12028
+ }
12029
+ };
12030
+ process.stdin.resume();
12031
+ process.stdin.setEncoding("utf-8");
12032
+ process.stdin.on("data", onData);
12033
+ });
12034
+ }
11777
12035
  async function startWeixinLoginWithQr(opts) {
11778
12036
  const sessionKey = opts.accountId || randomUUID2();
11779
12037
  purgeExpiredLogins();
@@ -11812,6 +12070,36 @@ async function startWeixinLoginWithQr(opts) {
11812
12070
  };
11813
12071
  }
11814
12072
  }
12073
+ async function refreshQRCode(activeLogin, botType, qrRefreshCount, onScannedReset) {
12074
+ try {
12075
+ const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
12076
+ activeLogin.qrcode = qrResponse.qrcode;
12077
+ activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
12078
+ activeLogin.startedAt = Date.now();
12079
+ onScannedReset();
12080
+ logger.info(`refreshQRCode: new QR code obtained (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT}) qrcode=${redactToken(qrResponse.qrcode)}`);
12081
+ process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
12082
+
12083
+ `);
12084
+ try {
12085
+ const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
12086
+ qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
12087
+ process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
12088
+ `);
12089
+ process.stdout.write(`${qrResponse.qrcode_img_content}
12090
+ `);
12091
+ } catch {
12092
+ process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
12093
+ `);
12094
+ process.stdout.write(`${qrResponse.qrcode_img_content}
12095
+ `);
12096
+ }
12097
+ return { success: true };
12098
+ } catch (refreshErr) {
12099
+ logger.error(`refreshQRCode: failed to refresh QR code: ${String(refreshErr)}`);
12100
+ return { success: false, message: `刷新二维码失败: ${String(refreshErr)}` };
12101
+ }
12102
+ }
11815
12103
  async function waitForWeixinLogin(opts) {
11816
12104
  let activeLogin = activeLogins.get(opts.sessionKey);
11817
12105
  if (!activeLogin) {
@@ -11838,7 +12126,7 @@ async function waitForWeixinLogin(opts) {
11838
12126
  while (Date.now() < deadline) {
11839
12127
  try {
11840
12128
  const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
11841
- const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
12129
+ const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode, activeLogin.pendingVerifyCode);
11842
12130
  logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
11843
12131
  activeLogin.status = statusResponse.status;
11844
12132
  switch (statusResponse.status) {
@@ -11848,6 +12136,10 @@ async function waitForWeixinLogin(opts) {
11848
12136
  }
11849
12137
  break;
11850
12138
  case "scaned":
12139
+ if (activeLogin.pendingVerifyCode) {
12140
+ logger.info("verify code accepted, resuming polling");
12141
+ activeLogin.pendingVerifyCode = undefined;
12142
+ }
11851
12143
  if (!scannedPrinted) {
11852
12144
  process.stdout.write(`
11853
12145
  \uD83D\uDC40 已扫码,在微信继续操作...
@@ -11856,6 +12148,7 @@ async function waitForWeixinLogin(opts) {
11856
12148
  }
11857
12149
  break;
11858
12150
  case "expired": {
12151
+ activeLogin.pendingVerifyCode = undefined;
11859
12152
  qrRefreshCount++;
11860
12153
  if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
11861
12154
  logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
@@ -11869,36 +12162,14 @@ async function waitForWeixinLogin(opts) {
11869
12162
  ⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
11870
12163
  `);
11871
12164
  logger.info(`waitForWeixinLogin: QR expired, refreshing (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
11872
- try {
11873
- const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
11874
- const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
11875
- activeLogin.qrcode = qrResponse.qrcode;
11876
- activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
11877
- activeLogin.startedAt = Date.now();
12165
+ const expiredRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
11878
12166
  scannedPrinted = false;
11879
- logger.info(`waitForWeixinLogin: new QR code obtained qrcode=${redactToken(qrResponse.qrcode)}`);
11880
- process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
11881
-
11882
- `);
11883
- try {
11884
- const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
11885
- qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
11886
- process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
11887
- `);
11888
- process.stdout.write(`${qrResponse.qrcode_img_content}
11889
- `);
11890
- } catch {
11891
- process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
11892
- `);
11893
- process.stdout.write(`${qrResponse.qrcode_img_content}
11894
- `);
11895
- }
11896
- } catch (refreshErr) {
11897
- logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
12167
+ });
12168
+ if (!expiredRefreshResult.success) {
11898
12169
  activeLogins.delete(opts.sessionKey);
11899
12170
  return {
11900
12171
  connected: false,
11901
- message: `刷新二维码失败: ${String(refreshErr)}`
12172
+ message: expiredRefreshResult.message
11902
12173
  };
11903
12174
  }
11904
12175
  break;
@@ -11914,6 +12185,49 @@ async function waitForWeixinLogin(opts) {
11914
12185
  }
11915
12186
  break;
11916
12187
  }
12188
+ case "need_verifycode": {
12189
+ const verifyPrompt = activeLogin.pendingVerifyCode ? "❌ 你输入的数字不匹配,请重新输入:" : "输入手机微信显示的数字,以继续连接:";
12190
+ let code;
12191
+ try {
12192
+ code = await readVerifyCodeFromStdin(verifyPrompt);
12193
+ } catch (err) {
12194
+ logger.error(`waitForWeixinLogin: cannot read verify code (no TTY): ${String(err)}`);
12195
+ activeLogins.delete(opts.sessionKey);
12196
+ return {
12197
+ connected: false,
12198
+ message: "需要输入配对码,但当前环境没有交互式终端。请在前台运行 `weacpx login` 完成登录。"
12199
+ };
12200
+ }
12201
+ activeLogin.pendingVerifyCode = code;
12202
+ continue;
12203
+ }
12204
+ case "verify_code_blocked": {
12205
+ logger.warn(`waitForWeixinLogin: verify code blocked, qrRefreshCount=${qrRefreshCount} sessionKey=${opts.sessionKey}`);
12206
+ process.stdout.write(`
12207
+ ⛔ 多次输入错误,请稍后再试。
12208
+ `);
12209
+ activeLogin.pendingVerifyCode = undefined;
12210
+ qrRefreshCount++;
12211
+ if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
12212
+ logger.warn(`waitForWeixinLogin: verify_code_blocked and QR refresh limit reached, giving up sessionKey=${opts.sessionKey}`);
12213
+ activeLogins.delete(opts.sessionKey);
12214
+ return {
12215
+ connected: false,
12216
+ message: "多次输入错误,连接流程已停止。请稍后再试。"
12217
+ };
12218
+ }
12219
+ const blockedRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
12220
+ scannedPrinted = false;
12221
+ });
12222
+ if (!blockedRefreshResult.success) {
12223
+ activeLogins.delete(opts.sessionKey);
12224
+ return {
12225
+ connected: false,
12226
+ message: blockedRefreshResult.message
12227
+ };
12228
+ }
12229
+ break;
12230
+ }
11917
12231
  case "confirmed": {
11918
12232
  if (!statusResponse.ilink_bot_id) {
11919
12233
  activeLogins.delete(opts.sessionKey);
@@ -11953,140 +12267,22 @@ async function waitForWeixinLogin(opts) {
11953
12267
  message: "登录超时,请重试。"
11954
12268
  };
11955
12269
  }
11956
- var ACTIVE_LOGIN_TTL_MS, GET_QRCODE_TIMEOUT_MS = 5000, QR_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_ILINK_BOT_TYPE = "3", FIXED_BASE_URL = "https://ilinkai.weixin.qq.com", activeLogins, MAX_QR_REFRESH_COUNT = 3;
12270
+ var ACTIVE_LOGIN_TTL_MS, QR_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_ILINK_BOT_TYPE = "3", FIXED_BASE_URL = "https://ilinkai.weixin.qq.com", activeLogins, MAX_QR_REFRESH_COUNT = 3;
11957
12271
  var init_login_qr = __esm(() => {
11958
12272
  init_api();
12273
+ init_accounts();
11959
12274
  init_logger();
11960
12275
  init_redact();
11961
12276
  ACTIVE_LOGIN_TTL_MS = 5 * 60000;
11962
12277
  activeLogins = new Map;
11963
12278
  });
11964
12279
 
11965
- // src/weixin/api/config-cache.ts
11966
- class WeixinConfigManager {
11967
- apiOpts;
11968
- log;
11969
- cache = new Map;
11970
- constructor(apiOpts, log) {
11971
- this.apiOpts = apiOpts;
11972
- this.log = log;
11973
- }
11974
- async getForUser(userId, contextToken) {
11975
- const now = Date.now();
11976
- const entry = this.cache.get(userId);
11977
- const shouldFetch = !entry || now >= entry.nextFetchAt;
11978
- if (shouldFetch) {
11979
- let fetchOk = false;
11980
- try {
11981
- const resp = await getConfig({
11982
- baseUrl: this.apiOpts.baseUrl,
11983
- token: this.apiOpts.token,
11984
- ilinkUserId: userId,
11985
- contextToken
11986
- });
11987
- if (resp.ret === 0) {
11988
- this.cache.set(userId, {
11989
- config: { typingTicket: resp.typing_ticket ?? "" },
11990
- everSucceeded: true,
11991
- nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
11992
- retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
11993
- });
11994
- this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
11995
- fetchOk = true;
11996
- }
11997
- } catch (err) {
11998
- this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
11999
- }
12000
- if (!fetchOk) {
12001
- const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
12002
- const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
12003
- if (entry) {
12004
- entry.nextFetchAt = now + nextDelay;
12005
- entry.retryDelayMs = nextDelay;
12006
- } else {
12007
- this.cache.set(userId, {
12008
- config: { typingTicket: "" },
12009
- everSucceeded: false,
12010
- nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
12011
- retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
12012
- });
12013
- }
12014
- }
12015
- }
12016
- return this.cache.get(userId)?.config ?? { typingTicket: "" };
12017
- }
12018
- }
12019
- var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
12020
- var init_config_cache = __esm(() => {
12021
- init_api();
12022
- CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12023
- CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
12024
- });
12025
-
12026
- // src/weixin/api/session-guard.ts
12027
- function pauseSession(accountId) {
12028
- const until = Date.now() + SESSION_PAUSE_DURATION_MS;
12029
- pauseUntilMap.set(accountId, until);
12030
- logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
12031
- }
12032
- function getRemainingPauseMs(accountId) {
12033
- const until = pauseUntilMap.get(accountId);
12034
- if (until === undefined)
12035
- return 0;
12036
- const remaining = until - Date.now();
12037
- if (remaining <= 0) {
12038
- pauseUntilMap.delete(accountId);
12039
- return 0;
12040
- }
12041
- return remaining;
12042
- }
12043
- var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
12044
- var init_session_guard = __esm(() => {
12045
- init_logger();
12046
- SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
12047
- pauseUntilMap = new Map;
12048
- });
12049
-
12050
- // src/weixin/messaging/conversation-executor.ts
12051
- function createConversationExecutor() {
12052
- const states = new Map;
12053
- const getState = (conversationId) => {
12054
- const existing = states.get(conversationId);
12055
- if (existing)
12056
- return existing;
12057
- const created = { activeControls: 0 };
12058
- states.set(conversationId, created);
12059
- return created;
12060
- };
12061
- const cleanupState = (conversationId, state) => {
12062
- if (!state.normalTail && state.activeControls === 0) {
12063
- states.delete(conversationId);
12064
- }
12065
- };
12066
- return {
12067
- run(conversationId, lane, task) {
12068
- const state = getState(conversationId);
12069
- if (lane === "control") {
12070
- state.activeControls += 1;
12071
- return Promise.resolve().then(task).finally(() => {
12072
- state.activeControls -= 1;
12073
- cleanupState(conversationId, state);
12074
- });
12075
- }
12076
- const previous = state.normalTail ?? Promise.resolve();
12077
- const next = previous.catch(() => {
12078
- return;
12079
- }).then(task);
12080
- state.normalTail = next;
12081
- return next.finally(() => {
12082
- if (state.normalTail === next) {
12083
- state.normalTail = undefined;
12084
- }
12085
- cleanupState(conversationId, state);
12086
- });
12087
- }
12088
- };
12280
+ // src/weixin/util/random.ts
12281
+ import crypto2 from "node:crypto";
12282
+ function generateId(prefix) {
12283
+ return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
12089
12284
  }
12285
+ var init_random = () => {};
12090
12286
 
12091
12287
  // src/weixin/api/types.ts
12092
12288
  var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
@@ -12121,9 +12317,272 @@ var init_types2 = __esm(() => {
12121
12317
  };
12122
12318
  });
12123
12319
 
12124
- // src/channels/media-store.ts
12125
- import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile6 } from "node:fs/promises";
12320
+ // src/weixin/messaging/inbound.ts
12321
+ import fs5 from "node:fs";
12126
12322
  import path6 from "node:path";
12323
+ function contextTokenKey(accountId, userId) {
12324
+ return `${accountId}:${userId}`;
12325
+ }
12326
+ function resolveContextTokenFilePath(accountId) {
12327
+ return path6.join(resolveStateDir(), "openclaw-weixin", "accounts", `${accountId}.context-tokens.json`);
12328
+ }
12329
+ function persistContextTokens(accountId) {
12330
+ const prefix = `${accountId}:`;
12331
+ const tokens = {};
12332
+ for (const [k, v] of contextTokenStore) {
12333
+ if (k.startsWith(prefix))
12334
+ tokens[k.slice(prefix.length)] = v;
12335
+ }
12336
+ const filePath = resolveContextTokenFilePath(accountId);
12337
+ try {
12338
+ writePrivateFileSync(filePath, JSON.stringify(tokens));
12339
+ } catch (err) {
12340
+ logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
12341
+ }
12342
+ }
12343
+ function restoreContextTokens(accountId) {
12344
+ const filePath = resolveContextTokenFilePath(accountId);
12345
+ try {
12346
+ if (!fs5.existsSync(filePath))
12347
+ return;
12348
+ const raw = fs5.readFileSync(filePath, "utf-8");
12349
+ const tokens = JSON.parse(raw);
12350
+ let count = 0;
12351
+ for (const [userId, token] of Object.entries(tokens)) {
12352
+ if (typeof token === "string" && token) {
12353
+ contextTokenStore.set(contextTokenKey(accountId, userId), token);
12354
+ count++;
12355
+ }
12356
+ }
12357
+ logger.info(`restoreContextTokens: restored ${count} tokens for account=${accountId}`);
12358
+ } catch (err) {
12359
+ logger.warn(`restoreContextTokens: failed to read ${filePath}: ${String(err)}`);
12360
+ }
12361
+ }
12362
+ function clearContextTokensForAccount(accountId) {
12363
+ const prefix = `${accountId}:`;
12364
+ for (const k of [...contextTokenStore.keys()]) {
12365
+ if (k.startsWith(prefix))
12366
+ contextTokenStore.delete(k);
12367
+ }
12368
+ const filePath = resolveContextTokenFilePath(accountId);
12369
+ try {
12370
+ if (fs5.existsSync(filePath))
12371
+ fs5.unlinkSync(filePath);
12372
+ } catch (err) {
12373
+ logger.warn(`clearContextTokensForAccount: failed to remove ${filePath}: ${String(err)}`);
12374
+ }
12375
+ logger.info(`clearContextTokensForAccount: cleared tokens for account=${accountId}`);
12376
+ }
12377
+ function setContextToken(accountId, userId, token) {
12378
+ const k = contextTokenKey(accountId, userId);
12379
+ logger.debug(`setContextToken: key=${k}`);
12380
+ contextTokenStore.set(k, token);
12381
+ persistContextTokens(accountId);
12382
+ }
12383
+ function getContextToken(accountId, userId) {
12384
+ const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
12385
+ const val = contextTokenStore.get(k);
12386
+ logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
12387
+ return val;
12388
+ }
12389
+ function normalizeWeixinUserIdFromChatKey(chatKey) {
12390
+ const parts = chatKey.split(":");
12391
+ if (parts[0] === "weixin" && parts[2]) {
12392
+ return parts.slice(2).join(":");
12393
+ }
12394
+ return chatKey;
12395
+ }
12396
+ function isMediaItem(item) {
12397
+ return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
12398
+ }
12399
+ function bodyFromItemList(itemList) {
12400
+ if (!itemList?.length)
12401
+ return "";
12402
+ for (const item of itemList) {
12403
+ if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
12404
+ const text = String(item.text_item.text);
12405
+ const ref = item.ref_msg;
12406
+ if (!ref)
12407
+ return text;
12408
+ if (ref.message_item && isMediaItem(ref.message_item))
12409
+ return text;
12410
+ const parts = [];
12411
+ if (ref.title)
12412
+ parts.push(ref.title);
12413
+ if (ref.message_item) {
12414
+ const refBody = bodyFromItemList([ref.message_item]);
12415
+ if (refBody)
12416
+ parts.push(refBody);
12417
+ }
12418
+ if (!parts.length)
12419
+ return text;
12420
+ return `[引用: ${parts.join(" | ")}]
12421
+ ${text}`;
12422
+ }
12423
+ if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
12424
+ return item.voice_item.text;
12425
+ }
12426
+ }
12427
+ return "";
12428
+ }
12429
+ function extractWeixinMediaDescriptors(itemList) {
12430
+ const out = [];
12431
+ for (const item of itemList ?? []) {
12432
+ const descriptor = descriptorFromItem(item);
12433
+ if (descriptor)
12434
+ out.push(descriptor);
12435
+ const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
12436
+ const refDescriptor = descriptorFromItem(ref);
12437
+ if (refDescriptor)
12438
+ out.push(refDescriptor);
12439
+ }
12440
+ return out;
12441
+ }
12442
+ function descriptorFromItem(item) {
12443
+ if (!item)
12444
+ return;
12445
+ if (item.type === MessageItemType.IMAGE)
12446
+ return { item, kind: "image" };
12447
+ if (item.type === MessageItemType.VIDEO)
12448
+ return { item, kind: "video" };
12449
+ if (item.type === MessageItemType.FILE)
12450
+ return { item, kind: "file", fileName: item.file_item?.file_name };
12451
+ if (item.type === MessageItemType.VOICE)
12452
+ return { item, kind: "audio" };
12453
+ return;
12454
+ }
12455
+ var contextTokenStore;
12456
+ var init_inbound = __esm(() => {
12457
+ init_logger();
12458
+ init_random();
12459
+ init_types2();
12460
+ init_state_dir();
12461
+ init_private_file();
12462
+ contextTokenStore = new Map;
12463
+ });
12464
+
12465
+ // src/weixin/api/config-cache.ts
12466
+ class WeixinConfigManager {
12467
+ apiOpts;
12468
+ log;
12469
+ cache = new Map;
12470
+ constructor(apiOpts, log) {
12471
+ this.apiOpts = apiOpts;
12472
+ this.log = log;
12473
+ }
12474
+ async getForUser(userId, contextToken) {
12475
+ const now = Date.now();
12476
+ const entry = this.cache.get(userId);
12477
+ const shouldFetch = !entry || now >= entry.nextFetchAt;
12478
+ if (shouldFetch) {
12479
+ let fetchOk = false;
12480
+ try {
12481
+ const resp = await getConfig({
12482
+ baseUrl: this.apiOpts.baseUrl,
12483
+ token: this.apiOpts.token,
12484
+ ilinkUserId: userId,
12485
+ contextToken
12486
+ });
12487
+ if (resp.ret === 0) {
12488
+ this.cache.set(userId, {
12489
+ config: { typingTicket: resp.typing_ticket ?? "" },
12490
+ everSucceeded: true,
12491
+ nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
12492
+ retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
12493
+ });
12494
+ this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
12495
+ fetchOk = true;
12496
+ }
12497
+ } catch (err) {
12498
+ this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
12499
+ }
12500
+ if (!fetchOk) {
12501
+ const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
12502
+ const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
12503
+ if (entry) {
12504
+ entry.nextFetchAt = now + nextDelay;
12505
+ entry.retryDelayMs = nextDelay;
12506
+ } else {
12507
+ this.cache.set(userId, {
12508
+ config: { typingTicket: "" },
12509
+ everSucceeded: false,
12510
+ nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
12511
+ retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
12512
+ });
12513
+ }
12514
+ }
12515
+ }
12516
+ return this.cache.get(userId)?.config ?? { typingTicket: "" };
12517
+ }
12518
+ }
12519
+ var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
12520
+ var init_config_cache = __esm(() => {
12521
+ init_api();
12522
+ CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12523
+ CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
12524
+ });
12525
+
12526
+ // src/weixin/api/session-guard.ts
12527
+ function pauseSession(accountId) {
12528
+ const until = Date.now() + SESSION_PAUSE_DURATION_MS;
12529
+ pauseUntilMap.set(accountId, until);
12530
+ logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
12531
+ }
12532
+ function resetSessionPause(accountId) {
12533
+ pauseUntilMap.delete(accountId);
12534
+ }
12535
+ var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
12536
+ var init_session_guard = __esm(() => {
12537
+ init_logger();
12538
+ SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
12539
+ pauseUntilMap = new Map;
12540
+ });
12541
+
12542
+ // src/weixin/messaging/conversation-executor.ts
12543
+ function createConversationExecutor() {
12544
+ const states = new Map;
12545
+ const getState = (conversationId) => {
12546
+ const existing = states.get(conversationId);
12547
+ if (existing)
12548
+ return existing;
12549
+ const created = { activeControls: 0 };
12550
+ states.set(conversationId, created);
12551
+ return created;
12552
+ };
12553
+ const cleanupState = (conversationId, state) => {
12554
+ if (!state.normalTail && state.activeControls === 0) {
12555
+ states.delete(conversationId);
12556
+ }
12557
+ };
12558
+ return {
12559
+ run(conversationId, lane, task) {
12560
+ const state = getState(conversationId);
12561
+ if (lane === "control") {
12562
+ state.activeControls += 1;
12563
+ return Promise.resolve().then(task).finally(() => {
12564
+ state.activeControls -= 1;
12565
+ cleanupState(conversationId, state);
12566
+ });
12567
+ }
12568
+ const previous = state.normalTail ?? Promise.resolve();
12569
+ const next = previous.catch(() => {
12570
+ return;
12571
+ }).then(task);
12572
+ state.normalTail = next;
12573
+ return next.finally(() => {
12574
+ if (state.normalTail === next) {
12575
+ state.normalTail = undefined;
12576
+ }
12577
+ cleanupState(conversationId, state);
12578
+ });
12579
+ }
12580
+ };
12581
+ }
12582
+
12583
+ // src/channels/media-store.ts
12584
+ import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile5 } from "node:fs/promises";
12585
+ import path7 from "node:path";
12127
12586
 
12128
12587
  class RuntimeMediaStore {
12129
12588
  rootDir;
@@ -12139,19 +12598,19 @@ class RuntimeMediaStore {
12139
12598
  const safeChatKey = safePathSegment(input.chatKey);
12140
12599
  const safeMessageId = safePathSegment(input.messageId || "message");
12141
12600
  const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
12142
- const dir = path6.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
12601
+ const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
12143
12602
  await mkdir7(dir, { recursive: true });
12144
- const resolvedRoot = path6.resolve(this.rootDir);
12145
- const resolvedFile = path6.resolve(path6.join(dir, await uniqueFileName(dir, baseFileName)));
12603
+ const resolvedRoot = path7.resolve(this.rootDir);
12604
+ const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
12146
12605
  if (!isPathInside(resolvedFile, resolvedRoot)) {
12147
12606
  throw new Error("media path escapes runtime media root");
12148
12607
  }
12149
- await writeFile6(resolvedFile, input.buffer);
12608
+ await writeFile5(resolvedFile, input.buffer);
12150
12609
  return {
12151
12610
  kind: input.kind,
12152
12611
  filePath: resolvedFile,
12153
12612
  mimeType: input.mimeType,
12154
- fileName: path6.basename(resolvedFile),
12613
+ fileName: path7.basename(resolvedFile),
12155
12614
  sizeBytes: input.buffer.byteLength,
12156
12615
  source: {
12157
12616
  channelId: input.channelId,
@@ -12167,10 +12626,10 @@ class RuntimeMediaStore {
12167
12626
  }
12168
12627
  }
12169
12628
  function sanitizeMediaFileName(fileName, mimeType) {
12170
- const base = path6.basename(fileName.trim() || "attachment");
12629
+ const base = path7.basename(fileName.trim() || "attachment");
12171
12630
  const replaced = base.replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
12172
12631
  const safe = replaced || "attachment";
12173
- const ext = path6.extname(safe);
12632
+ const ext = path7.extname(safe);
12174
12633
  if (ext)
12175
12634
  return safe;
12176
12635
  return `${safe}${extensionFromMime(mimeType)}`;
@@ -12180,13 +12639,13 @@ function safePathSegment(value) {
12180
12639
  return safe || "unknown";
12181
12640
  }
12182
12641
  async function uniqueFileName(dir, baseName) {
12183
- const ext = path6.extname(baseName);
12642
+ const ext = path7.extname(baseName);
12184
12643
  const stem = ext ? baseName.slice(0, -ext.length) : baseName;
12185
12644
  let candidate = baseName;
12186
12645
  let counter = 2;
12187
12646
  while (true) {
12188
12647
  try {
12189
- await access2(path6.join(dir, candidate));
12648
+ await access2(path7.join(dir, candidate));
12190
12649
  candidate = `${stem}-${counter}${ext}`;
12191
12650
  counter += 1;
12192
12651
  } catch {
@@ -12217,8 +12676,8 @@ function extensionFromMime(mimeType) {
12217
12676
  return ".bin";
12218
12677
  }
12219
12678
  function isPathInside(candidate, root) {
12220
- const relative = path6.relative(root, candidate);
12221
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
12679
+ const relative = path7.relative(root, candidate);
12680
+ return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
12222
12681
  }
12223
12682
  async function cleanupDir(dir, cutoffMs) {
12224
12683
  let entries;
@@ -12229,7 +12688,7 @@ async function cleanupDir(dir, cutoffMs) {
12229
12688
  }
12230
12689
  let empty = true;
12231
12690
  for (const entry of entries) {
12232
- const full = path6.join(dir, entry.name);
12691
+ const full = path7.join(dir, entry.name);
12233
12692
  if (entry.isDirectory()) {
12234
12693
  const childEmpty = await cleanupDir(full, cutoffMs);
12235
12694
  if (childEmpty) {
@@ -12258,18 +12717,18 @@ var init_media_store = __esm(() => {
12258
12717
  });
12259
12718
 
12260
12719
  // src/channels/outbound-media-safety.ts
12261
- import fs5 from "node:fs/promises";
12262
- import path7 from "node:path";
12720
+ import fs6 from "node:fs/promises";
12721
+ import path8 from "node:path";
12263
12722
  async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
12264
12723
  if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) {
12265
12724
  return null;
12266
12725
  }
12267
- const candidate = path7.isAbsolute(mediaPath) ? mediaPath : path7.resolve(mediaPath);
12726
+ const candidate = path8.isAbsolute(mediaPath) ? mediaPath : path8.resolve(mediaPath);
12268
12727
  const realCandidate = await realpathOrNull(candidate);
12269
12728
  if (!realCandidate) {
12270
12729
  return null;
12271
12730
  }
12272
- const stat2 = await fs5.stat(realCandidate).catch(() => null);
12731
+ const stat2 = await fs6.stat(realCandidate).catch(() => null);
12273
12732
  if (!stat2?.isFile()) {
12274
12733
  return null;
12275
12734
  }
@@ -12283,21 +12742,21 @@ async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
12283
12742
  }
12284
12743
  async function realpathOrNull(filePath) {
12285
12744
  try {
12286
- return await fs5.realpath(filePath);
12745
+ return await fs6.realpath(filePath);
12287
12746
  } catch {
12288
12747
  return null;
12289
12748
  }
12290
12749
  }
12291
12750
  function isPathInside2(candidate, root) {
12292
- const relative = path7.relative(root, candidate);
12293
- return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
12751
+ const relative = path8.relative(root, candidate);
12752
+ return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
12294
12753
  }
12295
12754
  var init_outbound_media_safety = () => {};
12296
12755
 
12297
12756
  // src/weixin/media/mime.ts
12298
- import path8 from "node:path";
12757
+ import path9 from "node:path";
12299
12758
  function getMimeFromFilename(filename) {
12300
- const ext = path8.extname(filename).toLowerCase();
12759
+ const ext = path9.extname(filename).toLowerCase();
12301
12760
  return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
12302
12761
  }
12303
12762
  function getExtensionFromMime(mimeType) {
@@ -12635,115 +13094,361 @@ function buildFinalHeadsUp(input) {
12635
13094
  \uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`;
12636
13095
  }
12637
13096
 
12638
- // src/weixin/util/random.ts
12639
- import crypto2 from "node:crypto";
12640
- function generateId(prefix) {
12641
- return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
12642
- }
12643
- var init_random = () => {};
12644
-
12645
- // src/weixin/messaging/inbound.ts
12646
- function contextTokenKey(accountId, userId) {
12647
- return `${accountId}:${userId}`;
12648
- }
12649
- function setContextToken(accountId, userId, token) {
12650
- const k = contextTokenKey(accountId, userId);
12651
- logger.debug(`setContextToken: key=${k}`);
12652
- contextTokenStore.set(k, token);
12653
- }
12654
- function getContextToken(accountId, userId) {
12655
- const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
12656
- const val = contextTokenStore.get(k);
12657
- logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
12658
- return val;
12659
- }
12660
- function normalizeWeixinUserIdFromChatKey(chatKey) {
12661
- const parts = chatKey.split(":");
12662
- if (parts[0] === "weixin" && parts[2]) {
12663
- return parts.slice(2).join(":");
13097
+ // src/weixin/messaging/markdown-filter.ts
13098
+ class StreamingMarkdownFilter {
13099
+ buf = "";
13100
+ fence = false;
13101
+ sol = true;
13102
+ inl = null;
13103
+ feed(delta) {
13104
+ this.buf += delta;
13105
+ return this.pump(false);
12664
13106
  }
12665
- return chatKey;
12666
- }
12667
- function isMediaItem(item) {
12668
- return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
12669
- }
12670
- function bodyFromItemList(itemList) {
12671
- if (!itemList?.length)
13107
+ flush() {
13108
+ return this.pump(true);
13109
+ }
13110
+ pump(eof) {
13111
+ let out = "";
13112
+ while (this.buf) {
13113
+ const sLen = this.buf.length;
13114
+ const sSol = this.sol;
13115
+ const sFence = this.fence;
13116
+ const sInl = this.inl;
13117
+ if (this.fence)
13118
+ out += this.pumpFence(eof);
13119
+ else if (this.inl)
13120
+ out += this.pumpInline(eof);
13121
+ else if (this.sol)
13122
+ out += this.pumpSOL(eof);
13123
+ else
13124
+ out += this.pumpBody(eof);
13125
+ if (this.buf.length === sLen && this.sol === sSol && this.fence === sFence && this.inl === sInl)
13126
+ break;
13127
+ }
13128
+ if (eof && this.inl) {
13129
+ const markers = { image: "![", bold3: "***", italic: "*", ubold3: "___", uitalic: "_" };
13130
+ out += (markers[this.inl.type] ?? "") + this.inl.acc;
13131
+ this.inl = null;
13132
+ }
13133
+ return out;
13134
+ }
13135
+ pumpFence(eof) {
13136
+ if (this.sol) {
13137
+ if (this.buf.length < 3 && !eof)
13138
+ return "";
13139
+ if (this.buf.startsWith("```")) {
13140
+ const nl2 = this.buf.indexOf(`
13141
+ `, 3);
13142
+ if (nl2 !== -1) {
13143
+ this.fence = false;
13144
+ const line = this.buf.slice(0, nl2 + 1);
13145
+ this.buf = this.buf.slice(nl2 + 1);
13146
+ this.sol = true;
13147
+ return line;
13148
+ }
13149
+ if (eof) {
13150
+ this.fence = false;
13151
+ const line = this.buf;
13152
+ this.buf = "";
13153
+ return line;
13154
+ }
13155
+ return "";
13156
+ }
13157
+ this.sol = false;
13158
+ }
13159
+ const nl = this.buf.indexOf(`
13160
+ `);
13161
+ if (nl !== -1) {
13162
+ const chunk2 = this.buf.slice(0, nl + 1);
13163
+ this.buf = this.buf.slice(nl + 1);
13164
+ this.sol = true;
13165
+ return chunk2;
13166
+ }
13167
+ const chunk = this.buf;
13168
+ this.buf = "";
13169
+ return chunk;
13170
+ }
13171
+ pumpSOL(eof) {
13172
+ const b = this.buf;
13173
+ if (b[0] === `
13174
+ `) {
13175
+ this.buf = b.slice(1);
13176
+ return `
13177
+ `;
13178
+ }
13179
+ if (b[0] === "`") {
13180
+ if (b.length < 3 && !eof)
13181
+ return "";
13182
+ if (b.startsWith("```")) {
13183
+ const nl = b.indexOf(`
13184
+ `, 3);
13185
+ if (nl !== -1) {
13186
+ this.fence = true;
13187
+ const line = b.slice(0, nl + 1);
13188
+ this.buf = b.slice(nl + 1);
13189
+ this.sol = true;
13190
+ return line;
13191
+ }
13192
+ if (eof) {
13193
+ this.buf = "";
13194
+ return b;
13195
+ }
13196
+ return "";
13197
+ }
13198
+ this.sol = false;
13199
+ return "";
13200
+ }
13201
+ if (b[0] === ">") {
13202
+ this.sol = false;
13203
+ return "";
13204
+ }
13205
+ if (b[0] === "#") {
13206
+ let n = 0;
13207
+ while (n < b.length && b[n] === "#")
13208
+ n++;
13209
+ if (n === b.length && !eof)
13210
+ return "";
13211
+ if (n >= 5 && n <= 6 && n < b.length && b[n] === " ") {
13212
+ this.buf = b.slice(n + 1);
13213
+ this.sol = false;
13214
+ return "";
13215
+ }
13216
+ this.sol = false;
13217
+ return "";
13218
+ }
13219
+ if (b[0] === " " || b[0] === "\t") {
13220
+ if (b.search(/[^ \t]/) === -1 && !eof)
13221
+ return "";
13222
+ this.sol = false;
13223
+ return "";
13224
+ }
13225
+ if (b[0] === "-" || b[0] === "*" || b[0] === "_") {
13226
+ const ch = b[0];
13227
+ let j = 0;
13228
+ while (j < b.length && (b[j] === ch || b[j] === " "))
13229
+ j++;
13230
+ if (j === b.length && !eof)
13231
+ return "";
13232
+ if (j === b.length || b[j] === `
13233
+ `) {
13234
+ let count = 0;
13235
+ for (let k = 0;k < j; k++)
13236
+ if (b[k] === ch)
13237
+ count++;
13238
+ if (count >= 3) {
13239
+ if (j < b.length) {
13240
+ this.buf = b.slice(j + 1);
13241
+ this.sol = true;
13242
+ return b.slice(0, j + 1);
13243
+ }
13244
+ this.buf = "";
13245
+ return b;
13246
+ }
13247
+ }
13248
+ this.sol = false;
13249
+ return "";
13250
+ }
13251
+ this.sol = false;
12672
13252
  return "";
12673
- for (const item of itemList) {
12674
- if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
12675
- const text = String(item.text_item.text);
12676
- const ref = item.ref_msg;
12677
- if (!ref)
12678
- return text;
12679
- if (ref.message_item && isMediaItem(ref.message_item))
12680
- return text;
12681
- const parts = [];
12682
- if (ref.title)
12683
- parts.push(ref.title);
12684
- if (ref.message_item) {
12685
- const refBody = bodyFromItemList([ref.message_item]);
12686
- if (refBody)
12687
- parts.push(refBody);
13253
+ }
13254
+ pumpBody(eof) {
13255
+ let out = "";
13256
+ let i = 0;
13257
+ while (i < this.buf.length) {
13258
+ const c = this.buf[i];
13259
+ if (c === `
13260
+ `) {
13261
+ out += this.buf.slice(0, i + 1);
13262
+ this.buf = this.buf.slice(i + 1);
13263
+ this.sol = true;
13264
+ return out;
13265
+ }
13266
+ if (c === "!" && i + 1 < this.buf.length && this.buf[i + 1] === "[") {
13267
+ out += this.buf.slice(0, i);
13268
+ this.buf = this.buf.slice(i + 2);
13269
+ this.inl = { type: "image", acc: "" };
13270
+ return out;
13271
+ }
13272
+ if (c === "~") {
13273
+ i++;
13274
+ continue;
12688
13275
  }
12689
- if (!parts.length)
12690
- return text;
12691
- return `[引用: ${parts.join(" | ")}]
12692
- ${text}`;
13276
+ if (c === "*") {
13277
+ if (i + 2 < this.buf.length && this.buf[i + 1] === "*" && this.buf[i + 2] === "*") {
13278
+ out += this.buf.slice(0, i);
13279
+ this.buf = this.buf.slice(i + 3);
13280
+ this.inl = { type: "bold3", acc: "" };
13281
+ return out;
13282
+ }
13283
+ if (i + 1 < this.buf.length && this.buf[i + 1] === "*") {
13284
+ i += 2;
13285
+ continue;
13286
+ }
13287
+ if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
13288
+ `) {
13289
+ out += this.buf.slice(0, i);
13290
+ this.buf = this.buf.slice(i + 1);
13291
+ this.inl = { type: "italic", acc: "" };
13292
+ return out;
13293
+ }
13294
+ i++;
13295
+ continue;
13296
+ }
13297
+ if (c === "_") {
13298
+ if (i + 2 < this.buf.length && this.buf[i + 1] === "_" && this.buf[i + 2] === "_") {
13299
+ out += this.buf.slice(0, i);
13300
+ this.buf = this.buf.slice(i + 3);
13301
+ this.inl = { type: "ubold3", acc: "" };
13302
+ return out;
13303
+ }
13304
+ if (i + 1 < this.buf.length && this.buf[i + 1] === "_") {
13305
+ i += 2;
13306
+ continue;
13307
+ }
13308
+ if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
13309
+ `) {
13310
+ out += this.buf.slice(0, i);
13311
+ this.buf = this.buf.slice(i + 1);
13312
+ this.inl = { type: "uitalic", acc: "" };
13313
+ return out;
13314
+ }
13315
+ i++;
13316
+ continue;
13317
+ }
13318
+ i++;
12693
13319
  }
12694
- if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
12695
- return item.voice_item.text;
13320
+ let hold = 0;
13321
+ if (!eof) {
13322
+ if (this.buf.endsWith("**"))
13323
+ hold = 2;
13324
+ else if (this.buf.endsWith("__"))
13325
+ hold = 2;
13326
+ else if (this.buf.endsWith("*"))
13327
+ hold = 1;
13328
+ else if (this.buf.endsWith("_"))
13329
+ hold = 1;
13330
+ else if (this.buf.endsWith("!"))
13331
+ hold = 1;
13332
+ }
13333
+ out += this.buf.slice(0, this.buf.length - hold);
13334
+ this.buf = hold > 0 ? this.buf.slice(-hold) : "";
13335
+ return out;
13336
+ }
13337
+ pumpInline(_eof) {
13338
+ if (!this.inl)
13339
+ return "";
13340
+ this.inl.acc += this.buf;
13341
+ this.buf = "";
13342
+ switch (this.inl.type) {
13343
+ case "bold3": {
13344
+ const idx = this.inl.acc.indexOf("***");
13345
+ if (idx !== -1) {
13346
+ const content = this.inl.acc.slice(0, idx);
13347
+ this.buf = this.inl.acc.slice(idx + 3);
13348
+ this.inl = null;
13349
+ if (StreamingMarkdownFilter.containsCJK(content))
13350
+ return content;
13351
+ return `***${content}***`;
13352
+ }
13353
+ return "";
13354
+ }
13355
+ case "ubold3": {
13356
+ const idx = this.inl.acc.indexOf("___");
13357
+ if (idx !== -1) {
13358
+ const content = this.inl.acc.slice(0, idx);
13359
+ this.buf = this.inl.acc.slice(idx + 3);
13360
+ this.inl = null;
13361
+ if (StreamingMarkdownFilter.containsCJK(content))
13362
+ return content;
13363
+ return `___${content}___`;
13364
+ }
13365
+ return "";
13366
+ }
13367
+ case "italic": {
13368
+ for (let j = 0;j < this.inl.acc.length; j++) {
13369
+ if (this.inl.acc[j] === `
13370
+ `) {
13371
+ const r = "*" + this.inl.acc.slice(0, j + 1);
13372
+ this.buf = this.inl.acc.slice(j + 1);
13373
+ this.inl = null;
13374
+ this.sol = true;
13375
+ return r;
13376
+ }
13377
+ if (this.inl.acc[j] === "*") {
13378
+ if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "*") {
13379
+ j++;
13380
+ continue;
13381
+ }
13382
+ const content = this.inl.acc.slice(0, j);
13383
+ this.buf = this.inl.acc.slice(j + 1);
13384
+ this.inl = null;
13385
+ if (StreamingMarkdownFilter.containsCJK(content))
13386
+ return content;
13387
+ return `*${content}*`;
13388
+ }
13389
+ }
13390
+ return "";
13391
+ }
13392
+ case "uitalic": {
13393
+ for (let j = 0;j < this.inl.acc.length; j++) {
13394
+ if (this.inl.acc[j] === `
13395
+ `) {
13396
+ const r = "_" + this.inl.acc.slice(0, j + 1);
13397
+ this.buf = this.inl.acc.slice(j + 1);
13398
+ this.inl = null;
13399
+ this.sol = true;
13400
+ return r;
13401
+ }
13402
+ if (this.inl.acc[j] === "_") {
13403
+ if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "_") {
13404
+ j++;
13405
+ continue;
13406
+ }
13407
+ const content = this.inl.acc.slice(0, j);
13408
+ this.buf = this.inl.acc.slice(j + 1);
13409
+ this.inl = null;
13410
+ if (StreamingMarkdownFilter.containsCJK(content))
13411
+ return content;
13412
+ return `_${content}_`;
13413
+ }
13414
+ }
13415
+ return "";
13416
+ }
13417
+ case "image": {
13418
+ const cb = this.inl.acc.indexOf("]");
13419
+ if (cb === -1)
13420
+ return "";
13421
+ if (cb + 1 >= this.inl.acc.length)
13422
+ return "";
13423
+ if (this.inl.acc[cb + 1] !== "(") {
13424
+ const r = "![" + this.inl.acc.slice(0, cb + 1);
13425
+ this.buf = this.inl.acc.slice(cb + 1);
13426
+ this.inl = null;
13427
+ return r;
13428
+ }
13429
+ const cp = this.inl.acc.indexOf(")", cb + 2);
13430
+ if (cp !== -1) {
13431
+ this.buf = this.inl.acc.slice(cp + 1);
13432
+ this.inl = null;
13433
+ return "";
13434
+ }
13435
+ return "";
13436
+ }
12696
13437
  }
13438
+ return "";
12697
13439
  }
12698
- return "";
12699
- }
12700
- function extractWeixinMediaDescriptors(itemList) {
12701
- const out = [];
12702
- for (const item of itemList ?? []) {
12703
- const descriptor = descriptorFromItem(item);
12704
- if (descriptor)
12705
- out.push(descriptor);
12706
- const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
12707
- const refDescriptor = descriptorFromItem(ref);
12708
- if (refDescriptor)
12709
- out.push(refDescriptor);
13440
+ static containsCJK(text) {
13441
+ return /[\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(text);
12710
13442
  }
12711
- return out;
12712
13443
  }
12713
- function descriptorFromItem(item) {
12714
- if (!item)
12715
- return;
12716
- if (item.type === MessageItemType.IMAGE)
12717
- return { item, kind: "image" };
12718
- if (item.type === MessageItemType.VIDEO)
12719
- return { item, kind: "video" };
12720
- if (item.type === MessageItemType.FILE)
12721
- return { item, kind: "file", fileName: item.file_item?.file_name };
12722
- if (item.type === MessageItemType.VOICE)
12723
- return { item, kind: "audio" };
12724
- return;
12725
- }
12726
- var contextTokenStore;
12727
- var init_inbound = __esm(() => {
12728
- init_logger();
12729
- init_random();
12730
- init_types2();
12731
- contextTokenStore = new Map;
12732
- });
12733
13444
 
12734
13445
  // src/weixin/messaging/send.ts
12735
13446
  function generateClientId() {
12736
13447
  return generateId("openclaw-weixin");
12737
13448
  }
12738
13449
  function markdownToPlainText(text) {
12739
- let result = text;
12740
- result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
12741
- result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
12742
- result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
12743
- result = result.replace(/^\|[\s:|-]+\|$/gm, "");
12744
- result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
12745
- result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
12746
- return result;
13450
+ const f = new StreamingMarkdownFilter;
13451
+ return f.feed(text) + f.flush();
12747
13452
  }
12748
13453
  function buildTextMessageReq(params) {
12749
13454
  const { to, text, contextToken, clientId } = params;
@@ -12979,10 +13684,10 @@ var init_cdn_upload = __esm(() => {
12979
13684
 
12980
13685
  // src/weixin/cdn/upload.ts
12981
13686
  import crypto3 from "node:crypto";
12982
- import fs6 from "node:fs/promises";
13687
+ import fs7 from "node:fs/promises";
12983
13688
  async function uploadMediaToCdn(params) {
12984
13689
  const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
12985
- const plaintext = await fs6.readFile(filePath);
13690
+ const plaintext = await fs7.readFile(filePath);
12986
13691
  const rawsize = plaintext.length;
12987
13692
  const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
12988
13693
  const filesize = aesEcbPaddedSize(rawsize);
@@ -13055,7 +13760,7 @@ var init_upload = __esm(() => {
13055
13760
  });
13056
13761
 
13057
13762
  // src/weixin/messaging/send-media.ts
13058
- import path9 from "node:path";
13763
+ import path10 from "node:path";
13059
13764
  async function sendWeixinMediaFile(params) {
13060
13765
  const { media, filePath, to, text, opts, cdnBaseUrl } = params;
13061
13766
  const mime = media?.mimeType ?? getMimeFromFilename(filePath);
@@ -13082,7 +13787,7 @@ async function sendWeixinMediaFile(params) {
13082
13787
  logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
13083
13788
  return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
13084
13789
  }
13085
- const fileName = media?.fileName ?? path9.basename(filePath);
13790
+ const fileName = media?.fileName ?? path10.basename(filePath);
13086
13791
  logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
13087
13792
  const uploaded = await uploadFileAttachmentToWeixin({
13088
13793
  filePath,
@@ -13102,14 +13807,14 @@ var init_send_media = __esm(() => {
13102
13807
  });
13103
13808
 
13104
13809
  // src/weixin/messaging/debug-mode.ts
13105
- import fs7 from "node:fs";
13106
- import path10 from "node:path";
13810
+ import fs8 from "node:fs";
13811
+ import path11 from "node:path";
13107
13812
  function resolveDebugModePath() {
13108
- return path10.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
13813
+ return path11.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
13109
13814
  }
13110
13815
  function loadState() {
13111
13816
  try {
13112
- const raw = fs7.readFileSync(resolveDebugModePath(), "utf-8");
13817
+ const raw = fs8.readFileSync(resolveDebugModePath(), "utf-8");
13113
13818
  const parsed = JSON.parse(raw);
13114
13819
  if (parsed && typeof parsed.accounts === "object")
13115
13820
  return parsed;
@@ -13118,8 +13823,8 @@ function loadState() {
13118
13823
  }
13119
13824
  function saveState(state) {
13120
13825
  const filePath = resolveDebugModePath();
13121
- ensureDirSync(path10.dirname(filePath));
13122
- fs7.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
13826
+ ensureDirSync(path11.dirname(filePath));
13827
+ fs8.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
13123
13828
  }
13124
13829
  function toggleDebugMode(accountId) {
13125
13830
  const state = loadState();
@@ -13576,9 +14281,9 @@ var init_perf_tracer = __esm(() => {
13576
14281
 
13577
14282
  // src/weixin/messaging/handle-weixin-message-turn.ts
13578
14283
  import crypto4 from "node:crypto";
13579
- import fs8 from "node:fs/promises";
14284
+ import fs9 from "node:fs/promises";
13580
14285
  import { tmpdir } from "node:os";
13581
- import path11 from "node:path";
14286
+ import path12 from "node:path";
13582
14287
  function utf8ByteLength(s) {
13583
14288
  return Buffer.byteLength(s, "utf8");
13584
14289
  }
@@ -13668,24 +14373,24 @@ function hardCutByCodepoint(s, maxBytes) {
13668
14373
  return out;
13669
14374
  }
13670
14375
  function resolveMediaTempDir(customRoot) {
13671
- return customRoot ?? path11.join(tmpdir(), "weacpx", "media");
14376
+ return customRoot ?? path12.join(tmpdir(), "weacpx", "media");
13672
14377
  }
13673
14378
  function createSaveMediaBuffer(mediaTempDir) {
13674
14379
  return async function saveMediaBuffer(buffer, contentType, subdir, maxBytes, originalFilename) {
13675
14380
  if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
13676
14381
  throw new Error(`media exceeds ${maxBytes} bytes`);
13677
14382
  }
13678
- const dir = path11.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
13679
- await fs8.mkdir(dir, { recursive: true });
14383
+ const dir = path12.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
14384
+ await fs9.mkdir(dir, { recursive: true });
13680
14385
  let ext = ".bin";
13681
14386
  if (originalFilename) {
13682
- ext = path11.extname(originalFilename) || ".bin";
14387
+ ext = path12.extname(originalFilename) || ".bin";
13683
14388
  } else if (contentType) {
13684
14389
  ext = getExtensionFromMime(contentType);
13685
14390
  }
13686
14391
  const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
13687
- const filePath = path11.join(dir, name);
13688
- await fs8.writeFile(filePath, buffer);
14392
+ const filePath = path12.join(dir, name);
14393
+ await fs9.writeFile(filePath, buffer);
13689
14394
  return { path: filePath };
13690
14395
  };
13691
14396
  }
@@ -13837,7 +14542,7 @@ async function handleWeixinMessageTurn(full, deps) {
13837
14542
  continue;
13838
14543
  }
13839
14544
  try {
13840
- const buffer = await fs8.readFile(filePath);
14545
+ const buffer = await fs9.readFile(filePath);
13841
14546
  const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
13842
14547
  media.push(await mediaStore.saveMediaBuffer({
13843
14548
  channelId: "weixin",
@@ -13851,7 +14556,7 @@ async function handleWeixinMessageTurn(full, deps) {
13851
14556
  maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
13852
14557
  }));
13853
14558
  } finally {
13854
- await fs8.rm(filePath, { force: true }).catch(() => {});
14559
+ await fs9.rm(filePath, { force: true }).catch(() => {});
13855
14560
  }
13856
14561
  } catch (err) {
13857
14562
  deps.errLog(`media download failed: ${String(err)}`);
@@ -14085,20 +14790,20 @@ var init_handle_weixin_message_turn = __esm(() => {
14085
14790
  });
14086
14791
 
14087
14792
  // src/weixin/storage/sync-buf.ts
14088
- import fs9 from "node:fs";
14089
- import path12 from "node:path";
14793
+ import fs10 from "node:fs";
14794
+ import path13 from "node:path";
14090
14795
  function resolveAccountsDir2() {
14091
- return path12.join(resolveStateDir(), "openclaw-weixin", "accounts");
14796
+ return path13.join(resolveStateDir(), "openclaw-weixin", "accounts");
14092
14797
  }
14093
14798
  function getSyncBufFilePath(accountId) {
14094
- return path12.join(resolveAccountsDir2(), `${accountId}.sync.json`);
14799
+ return path13.join(resolveAccountsDir2(), `${accountId}.sync.json`);
14095
14800
  }
14096
14801
  function getLegacySyncBufDefaultJsonPath() {
14097
- return path12.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
14802
+ return path13.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
14098
14803
  }
14099
14804
  function readSyncBufFile(filePath) {
14100
14805
  try {
14101
- const raw = fs9.readFileSync(filePath, "utf-8");
14806
+ const raw = fs10.readFileSync(filePath, "utf-8");
14102
14807
  const data = JSON.parse(raw);
14103
14808
  if (typeof data.get_updates_buf === "string") {
14104
14809
  return data.get_updates_buf;
@@ -14110,10 +14815,10 @@ function loadGetUpdatesBuf(filePath) {
14110
14815
  const value = readSyncBufFile(filePath);
14111
14816
  if (value !== undefined)
14112
14817
  return value;
14113
- const accountId = path12.basename(filePath, ".sync.json");
14818
+ const accountId = path13.basename(filePath, ".sync.json");
14114
14819
  const rawId = deriveRawAccountId(accountId);
14115
14820
  if (rawId) {
14116
- const compatPath = path12.join(resolveAccountsDir2(), `${rawId}.sync.json`);
14821
+ const compatPath = path13.join(resolveAccountsDir2(), `${rawId}.sync.json`);
14117
14822
  const compatValue = readSyncBufFile(compatPath);
14118
14823
  if (compatValue !== undefined)
14119
14824
  return compatValue;
@@ -14121,13 +14826,11 @@ function loadGetUpdatesBuf(filePath) {
14121
14826
  return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
14122
14827
  }
14123
14828
  function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
14124
- const dir = path12.dirname(filePath);
14125
- ensureDirSync(dir);
14126
- fs9.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
14829
+ writePrivateFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0));
14127
14830
  }
14128
14831
  var init_sync_buf = __esm(() => {
14129
14832
  init_accounts();
14130
- init_ensure_dir();
14833
+ init_private_file();
14131
14834
  init_state_dir();
14132
14835
  });
14133
14836
 
@@ -14157,23 +14860,23 @@ function shouldFetchTypingConfig(textBody) {
14157
14860
  }
14158
14861
  async function monitorWeixinProvider(opts) {
14159
14862
  const {
14160
- baseUrl,
14161
- cdnBaseUrl,
14162
- token,
14163
- accountId,
14164
14863
  agent,
14165
14864
  abortSignal,
14166
14865
  longPollTimeoutMs
14167
14866
  } = opts;
14867
+ let baseUrl = opts.baseUrl;
14868
+ let cdnBaseUrl = opts.cdnBaseUrl;
14869
+ let token = opts.token;
14870
+ let accountId = opts.accountId;
14168
14871
  const log = opts.log ?? ((msg) => console.log(msg));
14169
14872
  const errLog = (msg) => {
14170
14873
  log(msg);
14171
14874
  logger.error(msg);
14172
14875
  };
14173
- const aLog = logger.withAccount(accountId);
14876
+ let aLog = logger.withAccount(accountId);
14174
14877
  log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
14175
14878
  aLog.info(`Monitor started: baseUrl=${baseUrl}`);
14176
- const syncFilePath = getSyncBufFilePath(accountId);
14879
+ let syncFilePath = getSyncBufFilePath(accountId);
14177
14880
  const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
14178
14881
  let getUpdatesBuf = previousGetUpdatesBuf ?? "";
14179
14882
  if (previousGetUpdatesBuf) {
@@ -14181,7 +14884,7 @@ async function monitorWeixinProvider(opts) {
14181
14884
  } else {
14182
14885
  log(`[weixin] no previous sync buf, starting fresh`);
14183
14886
  }
14184
- const configManager = new WeixinConfigManager({ baseUrl, token }, log);
14887
+ let configManager = new WeixinConfigManager({ baseUrl, token }, log);
14185
14888
  const conversationExecutor = createConversationExecutor();
14186
14889
  const seenMessageIds = new Set;
14187
14890
  const messageIdOrder = [];
@@ -14204,11 +14907,37 @@ async function monitorWeixinProvider(opts) {
14204
14907
  if (isApiError) {
14205
14908
  const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
14206
14909
  if (isSessionExpired) {
14910
+ const staleToken = token;
14911
+ const staleAccountId = accountId;
14912
+ errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), entering credential recovery. Please run \`weacpx login\` to re-login.`);
14207
14913
  pauseSession(accountId);
14208
- const pauseMs = getRemainingPauseMs(accountId);
14209
- errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing for ${Math.ceil(pauseMs / 60000)} min. Please run \`npx weixin-acp login\` to re-login.`);
14210
14914
  consecutiveFailures = 0;
14211
- await sleep(pauseMs, abortSignal);
14915
+ const recovered = await pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal);
14916
+ if (recovered === null) {
14917
+ aLog.info("Monitor stopped (aborted during credential recovery)");
14918
+ return;
14919
+ }
14920
+ const oldAccountId = accountId;
14921
+ accountId = recovered.accountId;
14922
+ baseUrl = recovered.baseUrl;
14923
+ cdnBaseUrl = recovered.cdnBaseUrl;
14924
+ token = recovered.token;
14925
+ aLog = logger.withAccount(accountId);
14926
+ syncFilePath = getSyncBufFilePath(accountId);
14927
+ const previousBuf = loadGetUpdatesBuf(syncFilePath);
14928
+ getUpdatesBuf = previousBuf ?? "";
14929
+ configManager = new WeixinConfigManager({ baseUrl, token }, log);
14930
+ seenMessageIds.clear();
14931
+ messageIdOrder.length = 0;
14932
+ consecutiveFailures = 0;
14933
+ nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
14934
+ resetSessionPause(oldAccountId);
14935
+ resetSessionPause(accountId);
14936
+ if (oldAccountId !== accountId) {
14937
+ clearContextTokensForAccount(oldAccountId);
14938
+ restoreContextTokens(accountId);
14939
+ }
14940
+ log(`[weixin] credential recovered, resuming monitor with account=${accountId}`);
14212
14941
  continue;
14213
14942
  }
14214
14943
  consecutiveFailures += 1;
@@ -14302,7 +15031,43 @@ function sleep(ms, signal) {
14302
15031
  }, { once: true });
14303
15032
  });
14304
15033
  }
14305
- var DEFAULT_LONG_POLL_TIMEOUT_MS2 = 35000, MAX_CONSECUTIVE_FAILURES = 3, BACKOFF_DELAY_MS = 30000, RETRY_DELAY_MS = 2000;
15034
+ async function pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal) {
15035
+ let attempt = 0;
15036
+ while (!abortSignal?.aborted) {
15037
+ attempt += 1;
15038
+ const currentAccount = resolveWeixinAccount(staleAccountId);
15039
+ if (currentAccount.token && currentAccount.token !== staleToken) {
15040
+ log(`[weixin] credential recovery: fresh token detected for account=${staleAccountId}`);
15041
+ return {
15042
+ accountId: currentAccount.accountId,
15043
+ baseUrl: currentAccount.baseUrl,
15044
+ cdnBaseUrl: currentAccount.cdnBaseUrl,
15045
+ token: currentAccount.token
15046
+ };
15047
+ }
15048
+ const ids = listWeixinAccountIds();
15049
+ for (const id of ids) {
15050
+ if (id === staleAccountId)
15051
+ continue;
15052
+ const account = resolveWeixinAccount(id);
15053
+ if (account.configured && account.token) {
15054
+ log(`[weixin] credential recovery: new account detected, switching to account=${id}`);
15055
+ return {
15056
+ accountId: account.accountId,
15057
+ baseUrl: account.baseUrl,
15058
+ cdnBaseUrl: account.cdnBaseUrl,
15059
+ token: account.token
15060
+ };
15061
+ }
15062
+ }
15063
+ if (attempt % 10 === 0) {
15064
+ log(`[weixin] credential recovery: still waiting for fresh credentials (checked ${attempt} times)`);
15065
+ }
15066
+ await sleep(CREDENTIAL_RECOVERY_POLL_INTERVAL_MS, abortSignal);
15067
+ }
15068
+ return null;
15069
+ }
15070
+ var DEFAULT_LONG_POLL_TIMEOUT_MS2 = 35000, MAX_CONSECUTIVE_FAILURES = 3, BACKOFF_DELAY_MS = 30000, RETRY_DELAY_MS = 2000, CREDENTIAL_RECOVERY_POLL_INTERVAL_MS = 30000;
14306
15071
  var init_monitor = __esm(() => {
14307
15072
  init_api();
14308
15073
  init_config_cache();
@@ -14311,6 +15076,9 @@ var init_monitor = __esm(() => {
14311
15076
  init_types2();
14312
15077
  init_sync_buf();
14313
15078
  init_logger();
15079
+ init_accounts();
15080
+ init_session_guard();
15081
+ init_inbound();
14314
15082
  });
14315
15083
 
14316
15084
  // src/weixin/bot.ts
@@ -14369,6 +15137,8 @@ function logout(opts) {
14369
15137
  log("当前没有已登录的账号");
14370
15138
  return;
14371
15139
  }
15140
+ for (const id of ids)
15141
+ clearContextTokensForAccount(id);
14372
15142
  clearAllWeixinAccounts();
14373
15143
  log("✅ 已退出登录");
14374
15144
  }
@@ -14396,6 +15166,7 @@ async function start(agent, opts) {
14396
15166
  if (!account.configured) {
14397
15167
  throw new Error(`账号 ${accountId} 未配置 (缺少 token),请先运行 login`);
14398
15168
  }
15169
+ restoreContextTokens(account.accountId);
14399
15170
  log(`[weixin] 启动 bot, account=${account.accountId}`);
14400
15171
  await monitorWeixinProvider({
14401
15172
  baseUrl: account.baseUrl,
@@ -14421,6 +15192,7 @@ async function start(agent, opts) {
14421
15192
  var init_bot = __esm(() => {
14422
15193
  init_accounts();
14423
15194
  init_login_qr();
15195
+ init_inbound();
14424
15196
  init_monitor();
14425
15197
  });
14426
15198
 
@@ -14671,7 +15443,7 @@ var init_deliver_coordinator_message = __esm(() => {
14671
15443
  });
14672
15444
 
14673
15445
  // src/weixin/monitor/consumer-lock.ts
14674
- import { mkdir as mkdir8, open as open2, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15446
+ import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
14675
15447
  import { dirname as dirname8, join as join5 } from "node:path";
14676
15448
  import { homedir as homedir4 } from "node:os";
14677
15449
  function createWeixinConsumerLock(options = {}) {
@@ -14683,7 +15455,7 @@ function createWeixinConsumerLock(options = {}) {
14683
15455
  await mkdir8(dirname8(lockFilePath), { recursive: true });
14684
15456
  while (true) {
14685
15457
  try {
14686
- const handle = await open2(lockFilePath, "wx");
15458
+ const handle = await open3(lockFilePath, "wx");
14687
15459
  try {
14688
15460
  await handle.writeFile(`${JSON.stringify(meta2, null, 2)}
14689
15461
  `, "utf8");
@@ -14751,9 +15523,9 @@ function createWeixinConsumerLock(options = {}) {
14751
15523
  }
14752
15524
  };
14753
15525
  }
14754
- async function loadLockMetadata(path13) {
15526
+ async function loadLockMetadata(path14) {
14755
15527
  try {
14756
- const raw = await readFile6(path13, "utf8");
15528
+ const raw = await readFile6(path14, "utf8");
14757
15529
  const parsed = JSON.parse(raw);
14758
15530
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
14759
15531
  return null;
@@ -15368,21 +16140,27 @@ async function loadConfiguredPlugins(input) {
15368
16140
  const importPlugin = input.importPlugin ?? importPluginFromHome;
15369
16141
  const loaded = [];
15370
16142
  for (const config2 of enabled) {
15371
- let moduleValue;
15372
16143
  try {
15373
- moduleValue = await importPlugin(config2.name, pluginHome);
16144
+ let moduleValue;
16145
+ try {
16146
+ moduleValue = await importPlugin(config2.name, pluginHome);
16147
+ } catch (error2) {
16148
+ const message = error2 instanceof Error ? error2.message : String(error2);
16149
+ throw new Error(`failed to load plugin ${config2.name}: ${message}`);
16150
+ }
16151
+ const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
16152
+ ...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
16153
+ });
16154
+ const channels = plugin.channels ?? [];
16155
+ for (const channel of channels) {
16156
+ registerChannelPlugin(channel);
16157
+ }
16158
+ loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
15374
16159
  } catch (error2) {
15375
- const message = error2 instanceof Error ? error2.message : String(error2);
15376
- throw new Error(`failed to load plugin ${config2.name}: ${message}`);
15377
- }
15378
- const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
15379
- ...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
15380
- });
15381
- const channels = plugin.channels ?? [];
15382
- for (const channel of channels) {
15383
- registerChannelPlugin(channel);
16160
+ if (!input.onPluginError)
16161
+ throw error2;
16162
+ input.onPluginError({ name: config2.name, error: error2 });
15384
16163
  }
15385
- loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
15386
16164
  }
15387
16165
  return loaded;
15388
16166
  }
@@ -15403,7 +16181,7 @@ var init_bootstrap = __esm(() => {
15403
16181
  });
15404
16182
 
15405
16183
  // src/logging/app-logger.ts
15406
- import { appendFile, mkdir as mkdir9 } from "node:fs/promises";
16184
+ import { appendFile, chmod as chmod2, mkdir as mkdir9 } from "node:fs/promises";
15407
16185
  import { dirname as dirname10 } from "node:path";
15408
16186
  function createNoopAppLogger() {
15409
16187
  return {
@@ -15417,6 +16195,7 @@ function createNoopAppLogger() {
15417
16195
  function createAppLogger(options) {
15418
16196
  const now = options.now ?? (() => new Date);
15419
16197
  let writeChain = Promise.resolve();
16198
+ let modeEnsured = false;
15420
16199
  return {
15421
16200
  debug: async (event, message, context) => {
15422
16201
  await enqueueWrite("debug", event, message, context);
@@ -15445,8 +16224,12 @@ function createAppLogger(options) {
15445
16224
  }
15446
16225
  const line = formatLogLine(now(), level, event, message, context);
15447
16226
  await mkdir9(dirname10(options.filePath), { recursive: true });
16227
+ if (!modeEnsured) {
16228
+ modeEnsured = true;
16229
+ await chmod2(options.filePath, 384).catch(() => {});
16230
+ }
15448
16231
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
15449
- await appendFile(options.filePath, line, "utf8");
16232
+ await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
15450
16233
  }
15451
16234
  }
15452
16235
  function formatLogLine(time3, level, event, message, context) {
@@ -15823,6 +16606,7 @@ function parseCommand(input) {
15823
16606
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
15824
16607
  const name = parts[2];
15825
16608
  let cwd = "";
16609
+ let raw = false;
15826
16610
  let invalid = false;
15827
16611
  for (let index = 3;index < parts.length; index += 1) {
15828
16612
  if (parts[index] === "--cwd" || parts[index] === "-d") {
@@ -15834,11 +16618,19 @@ function parseCommand(input) {
15834
16618
  index += 1;
15835
16619
  continue;
15836
16620
  }
16621
+ if (parts[index] === "--raw") {
16622
+ if (raw) {
16623
+ invalid = true;
16624
+ break;
16625
+ }
16626
+ raw = true;
16627
+ continue;
16628
+ }
15837
16629
  invalid = true;
15838
16630
  break;
15839
16631
  }
15840
16632
  if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
15841
- return { kind: "workspace.new", name, cwd };
16633
+ return { kind: "workspace.new", name, cwd, ...raw ? { raw: true } : {} };
15842
16634
  }
15843
16635
  }
15844
16636
  if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
@@ -16298,26 +17090,26 @@ var init_permission_handler = __esm(() => {
16298
17090
 
16299
17091
  // src/commands/handlers/config-handler.ts
16300
17092
  function handleConfigShow(context) {
16301
- const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path13) => `- ${path13}`)];
16302
- lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((path13) => `- ${path13}`));
17093
+ const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path14) => `- ${path14}`)];
17094
+ lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((path14) => `- ${path14}`));
16303
17095
  if (context.config) {
16304
17096
  lines.push("", "示例:", "- /config set channel.replyMode final", "- /config set logging.level debug");
16305
17097
  }
16306
17098
  return { text: lines.join(`
16307
17099
  `) };
16308
17100
  }
16309
- async function handleConfigSet(context, path13, rawValue) {
17101
+ async function handleConfigSet(context, path14, rawValue) {
16310
17102
  if (!context.config || !context.configStore) {
16311
17103
  return { text: "当前没有加载可写入的配置。" };
16312
17104
  }
16313
17105
  const previous = cloneAppConfig(context.config);
16314
17106
  const updated = cloneAppConfig(context.config);
16315
- const result = applySupportedConfigUpdate(updated, path13, rawValue);
17107
+ const result = applySupportedConfigUpdate(updated, path14, rawValue);
16316
17108
  if ("error" in result) {
16317
17109
  return { text: result.error };
16318
17110
  }
16319
17111
  await context.configStore.save(updated);
16320
- if (path13 === "transport.permissionMode" || path13 === "transport.nonInteractivePermissions" || path13 === "transport.permissionPolicy") {
17112
+ if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
16321
17113
  try {
16322
17114
  await context.transport.updatePermissionPolicy?.(updated.transport);
16323
17115
  } catch (error2) {
@@ -16327,10 +17119,10 @@ async function handleConfigSet(context, path13, rawValue) {
16327
17119
  }
16328
17120
  }
16329
17121
  context.replaceConfig(updated);
16330
- return { text: `配置已更新:${path13} = ${result.renderedValue}` };
17122
+ return { text: `配置已更新:${path14} = ${result.renderedValue}` };
16331
17123
  }
16332
- function applySupportedConfigUpdate(config2, path13, rawValue) {
16333
- switch (path13) {
17124
+ function applySupportedConfigUpdate(config2, path14, rawValue) {
17125
+ switch (path14) {
16334
17126
  case "transport.type": {
16335
17127
  const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
16336
17128
  if (!parsed)
@@ -16418,18 +17210,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16418
17210
  };
16419
17211
  }
16420
17212
  }
16421
- const agentMatch = path13.match(/^agents\.([^.]+)\.(driver|command)$/);
17213
+ const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
16422
17214
  if (agentMatch) {
16423
17215
  const [, name, field] = agentMatch;
16424
17216
  if (!name || !field) {
16425
- return { error: `不支持修改这个配置路径:${path13}` };
17217
+ return { error: `不支持修改这个配置路径:${path14}` };
16426
17218
  }
16427
17219
  const agent = config2.agents[name];
16428
17220
  if (!agent) {
16429
17221
  return { error: `Agent「${name}」不存在,请先创建。` };
16430
17222
  }
16431
17223
  if (!rawValue.trim()) {
16432
- return { error: `${path13} 不能为空。` };
17224
+ return { error: `${path14} 不能为空。` };
16433
17225
  }
16434
17226
  if (field === "driver") {
16435
17227
  agent.driver = rawValue;
@@ -16438,18 +17230,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16438
17230
  }
16439
17231
  return { renderedValue: rawValue };
16440
17232
  }
16441
- const workspaceMatch = path13.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
17233
+ const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
16442
17234
  if (workspaceMatch) {
16443
17235
  const [, name, field] = workspaceMatch;
16444
17236
  if (!name || !field) {
16445
- return { error: `不支持修改这个配置路径:${path13}` };
17237
+ return { error: `不支持修改这个配置路径:${path14}` };
16446
17238
  }
16447
17239
  const workspace = config2.workspaces[name];
16448
17240
  if (!workspace) {
16449
17241
  return { error: `工作区「${name}」不存在,请先创建。` };
16450
17242
  }
16451
17243
  if (!rawValue.trim()) {
16452
- return { error: `${path13} 不能为空。` };
17244
+ return { error: `${path14} 不能为空。` };
16453
17245
  }
16454
17246
  if (field === "cwd") {
16455
17247
  workspace.cwd = rawValue;
@@ -16458,15 +17250,15 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16458
17250
  }
16459
17251
  return { renderedValue: rawValue };
16460
17252
  }
16461
- return { error: `不支持修改这个配置路径:${path13}` };
17253
+ return { error: `不支持修改这个配置路径:${path14}` };
16462
17254
  }
16463
17255
  function parseEnum(value, allowed) {
16464
17256
  return allowed.includes(value) ? value : null;
16465
17257
  }
16466
- function parsePositiveNumber(rawValue, path13) {
17258
+ function parsePositiveNumber(rawValue, path14) {
16467
17259
  const value = Number(rawValue);
16468
17260
  if (!Number.isFinite(value) || value <= 0) {
16469
- return { error: `${path13} 必须是正数。` };
17261
+ return { error: `${path14} 必须是正数。` };
16470
17262
  }
16471
17263
  return { value };
16472
17264
  }
@@ -16901,7 +17693,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
16901
17693
  return {
16902
17694
  text: [
16903
17695
  "没有找到可绑定的已有会话。",
16904
- `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
17696
+ `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${quoteWorkspaceNameIfNeeded(workspace)} --name <会话名>`
16905
17697
  ].join(`
16906
17698
  `)
16907
17699
  };
@@ -17238,6 +18030,7 @@ var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /se
17238
18030
  var init_session_handler = __esm(() => {
17239
18031
  init_build_coordinator_prompt();
17240
18032
  init_channel_scope();
18033
+ init_workspace_name();
17241
18034
  sessionHelp = {
17242
18035
  topic: "session",
17243
18036
  aliases: ["ss", "sessions"],
@@ -17885,7 +18678,7 @@ var init_agent_handler = __esm(() => {
17885
18678
  function handleWorkspaces(context) {
17886
18679
  return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
17887
18680
  }
17888
- async function handleWorkspaceCreate(context, workspaceName, cwd) {
18681
+ async function handleWorkspaceCreate(context, workspaceName, cwd, options = {}) {
17889
18682
  if (!context.config || !context.configStore) {
17890
18683
  return { text: "当前没有加载可写入的配置。" };
17891
18684
  }
@@ -17893,9 +18686,18 @@ async function handleWorkspaceCreate(context, workspaceName, cwd) {
17893
18686
  if (!await pathExists(normalizedCwd)) {
17894
18687
  return { text: `工作区路径不存在:${cwd}` };
17895
18688
  }
17896
- const updated = await context.configStore.upsertWorkspace(workspaceName, normalizedCwd);
18689
+ let name = workspaceName;
18690
+ let notice;
18691
+ if (!options.raw && !isWorkspaceNameValid(workspaceName)) {
18692
+ const base = sanitizeWorkspaceName(workspaceName);
18693
+ name = allocateWorkspaceName(base, context.config.workspaces);
18694
+ notice = `名称 ${JSON.stringify(workspaceName)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`;
18695
+ }
18696
+ const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
17897
18697
  context.replaceConfig(updated);
17898
- return { text: `工作区「${workspaceName}」已保存` };
18698
+ const savedLine = `工作区「${name}」已保存`;
18699
+ return { text: notice ? `${notice}
18700
+ ${savedLine}` : savedLine };
17899
18701
  }
17900
18702
  async function handleWorkspaceRemove(context, workspaceName) {
17901
18703
  if (!context.config || !context.configStore) {
@@ -17907,6 +18709,7 @@ async function handleWorkspaceRemove(context, workspaceName) {
17907
18709
  }
17908
18710
  var workspaceHelp;
17909
18711
  var init_workspace_handler = __esm(() => {
18712
+ init_workspace_name();
17910
18713
  init_workspace_path();
17911
18714
  workspaceHelp = {
17912
18715
  topic: "workspace",
@@ -17915,7 +18718,7 @@ var init_workspace_handler = __esm(() => {
17915
18718
  commands: [
17916
18719
  { usage: "/workspaces", description: "查看当前已注册的工作区" },
17917
18720
  { usage: "/workspace 或 /ws", description: "查看工作区列表" },
17918
- { usage: "/ws new <name> -d <path>", description: "添加工作区" },
18721
+ { usage: "/ws new <name> -d <path> [--raw]", description: "添加工作区;含特殊字符的名称会被自动规范化,--raw 保留原名" },
17919
18722
  { usage: "/workspace rm <name>", description: "删除工作区" }
17920
18723
  ],
17921
18724
  examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
@@ -18152,7 +18955,7 @@ async function resolveShortcutWorkspace(context, target) {
18152
18955
  reused: true
18153
18956
  };
18154
18957
  }
18155
- const workspaceName = allocateWorkspaceName(context, basenameForWorkspacePath(cwd));
18958
+ const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), context.config?.workspaces ?? {});
18156
18959
  const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
18157
18960
  context.replaceConfig(updated);
18158
18961
  return {
@@ -18161,16 +18964,6 @@ async function resolveShortcutWorkspace(context, target) {
18161
18964
  reused: false
18162
18965
  };
18163
18966
  }
18164
- function allocateWorkspaceName(context, baseName) {
18165
- if (!context.config?.workspaces[baseName]) {
18166
- return baseName;
18167
- }
18168
- let suffix = 2;
18169
- while (context.config.workspaces[`${baseName}-${suffix}`]) {
18170
- suffix += 1;
18171
- }
18172
- return `${baseName}-${suffix}`;
18173
- }
18174
18967
  async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
18175
18968
  if (!await hasLogicalSession(context, baseAlias, chatKey)) {
18176
18969
  return baseAlias;
@@ -18196,6 +18989,7 @@ function renderShortcutSessionCreationError(workspace, alias) {
18196
18989
  };
18197
18990
  }
18198
18991
  var init_session_shortcut_handler = __esm(() => {
18992
+ init_workspace_name();
18199
18993
  init_workspace_path();
18200
18994
  init_errors();
18201
18995
  init_channel_scope();
@@ -18208,8 +19002,8 @@ function renderTransportError(session, error2) {
18208
19002
  return {
18209
19003
  text: [
18210
19004
  `当前会话「${session.alias}」暂时不可用。`,
18211
- `请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
18212
- `如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
19005
+ `请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)}`,
19006
+ `如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
18213
19007
  ].join(`
18214
19008
  `)
18215
19009
  };
@@ -18271,7 +19065,7 @@ function renderSessionCreationFailure(session, detail) {
18271
19065
  text: [
18272
19066
  "会话创建失败。",
18273
19067
  `错误信息:${summarizeTransportError(detail)}`,
18274
- `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
19068
+ `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
18275
19069
  ].join(`
18276
19070
  `)
18277
19071
  };
@@ -18290,6 +19084,7 @@ async function tryRecoverMissingSession(ops, session, error2) {
18290
19084
  }
18291
19085
  var init_session_recovery_handler = __esm(() => {
18292
19086
  init_errors();
19087
+ init_workspace_name();
18293
19088
  });
18294
19089
 
18295
19090
  // src/recovery/auto-install-optional-dep.ts
@@ -18415,10 +19210,10 @@ ${err.message}`, reason: "spawn" });
18415
19210
  const dir = join10(homedir6(), ".weacpx", "logs");
18416
19211
  await mkdir10(dir, { recursive: true });
18417
19212
  const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
18418
- const path13 = join10(dir, `auto-install-${timestamp}.log`);
18419
- const stream = createWriteStream(path13, { flags: "a" });
19213
+ const path14 = join10(dir, `auto-install-${timestamp}.log`);
19214
+ const stream = createWriteStream(path14, { flags: "a" });
18420
19215
  return {
18421
- path: path13,
19216
+ path: path14,
18422
19217
  append: async (chunk) => {
18423
19218
  await new Promise((resolve3, reject) => stream.write(chunk, (err) => err ? reject(err) : resolve3()));
18424
19219
  },
@@ -18501,9 +19296,9 @@ function isUnder(child, parent) {
18501
19296
  const p = parent.replace(/[\\/]+$/, "");
18502
19297
  return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
18503
19298
  }
18504
- async function defaultFsExists(path13) {
19299
+ async function defaultFsExists(path14) {
18505
19300
  try {
18506
- await access3(path13);
19301
+ await access3(path14);
18507
19302
  return true;
18508
19303
  } catch {
18509
19304
  return false;
@@ -18731,7 +19526,7 @@ class CommandRouter {
18731
19526
  case "workspaces":
18732
19527
  return handleWorkspaces(this.createHandlerContext());
18733
19528
  case "workspace.new":
18734
- return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
19529
+ return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd, command.raw ? { raw: true } : {});
18735
19530
  case "workspace.rm":
18736
19531
  return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
18737
19532
  case "sessions":
@@ -19193,7 +19988,7 @@ function resolveAcpxCommandMetadata(options = {}) {
19193
19988
  }
19194
19989
  const platform = options.platform ?? process.platform;
19195
19990
  const resolvePackageJson = options.resolvePackageJson ?? ((id) => require3.resolve(id));
19196
- const readPackageJson = options.readPackageJson ?? ((path13) => JSON.parse(readFileSync(path13, "utf8")));
19991
+ const readPackageJson = options.readPackageJson ?? ((path14) => JSON.parse(readFileSync(path14, "utf8")));
19197
19992
  try {
19198
19993
  const packageJsonPath = resolvePackageJson("acpx/package.json");
19199
19994
  const pkg = readPackageJson(packageJsonPath);
@@ -19570,8 +20365,8 @@ class OrchestrationServer {
19570
20365
  if (this.endpoint.kind !== "unix") {
19571
20366
  return;
19572
20367
  }
19573
- const removeFile = this.deps.removeFile ?? (async (path13) => {
19574
- await rm7(path13, { force: true });
20368
+ const removeFile = this.deps.removeFile ?? (async (path14) => {
20369
+ await rm7(path14, { force: true });
19575
20370
  });
19576
20371
  await removeFile(this.endpoint.path);
19577
20372
  }
@@ -19732,9 +20527,9 @@ function requireTaskQuestions(params, key) {
19732
20527
  };
19733
20528
  });
19734
20529
  }
19735
- async function canConnectToEndpoint(path13) {
20530
+ async function canConnectToEndpoint(path14) {
19736
20531
  return await new Promise((resolve3) => {
19737
- const socket = createConnection2(path13);
20532
+ const socket = createConnection2(path14);
19738
20533
  let settled = false;
19739
20534
  const finish = (result) => {
19740
20535
  if (settled) {
@@ -19755,7 +20550,7 @@ async function canConnectToEndpoint(path13) {
19755
20550
  });
19756
20551
  });
19757
20552
  }
19758
- async function listen(server, path13) {
20553
+ async function listen(server, path14) {
19759
20554
  await new Promise((resolve3, reject) => {
19760
20555
  const onError = (error2) => {
19761
20556
  server.off("listening", onListening);
@@ -19767,7 +20562,7 @@ async function listen(server, path13) {
19767
20562
  };
19768
20563
  server.once("error", onError);
19769
20564
  server.once("listening", onListening);
19770
- server.listen(path13);
20565
+ server.listen(path14);
19771
20566
  });
19772
20567
  }
19773
20568
  function isServerNotRunningError(error2) {
@@ -19891,6 +20686,7 @@ class OrchestrationService {
19891
20686
  stateMutex;
19892
20687
  pendingWorkerSessions = new Map;
19893
20688
  pendingLogicalTransportSessions = new Map;
20689
+ pendingParallelStarts = new Map;
19894
20690
  constructor(deps) {
19895
20691
  this.deps = deps;
19896
20692
  this.stateMutex = deps.stateMutex ?? new AsyncMutex;
@@ -20055,84 +20851,139 @@ class OrchestrationService {
20055
20851
  const normalizedGroupId = this.normalizeGroupId(input.groupId);
20056
20852
  const taskId = this.deps.createId();
20057
20853
  const workerSession = await this.resolveWorkerSession(input);
20058
- const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
20059
- let ensuredWorkerSession = workerSession;
20060
- let prepared;
20061
- try {
20062
- ensuredWorkerSession = await this.ensureReservedWorkerSession({
20063
- workerSession,
20064
- sourceHandle: input.sourceHandle,
20065
- sourceKind: input.sourceKind,
20066
- coordinatorSession: input.coordinatorSession,
20067
- workspace: input.workspace,
20068
- ...input.cwd ? { cwd: input.cwd } : {},
20069
- targetAgent: input.targetAgent,
20070
- role
20071
- });
20072
- prepared = await this.mutate(async () => {
20854
+ if (input.parallel) {
20855
+ const queuedResult = await this.mutate(async () => {
20073
20856
  const state = await this.deps.loadState();
20074
- const now = this.deps.now().toISOString();
20075
- if (normalizedGroupId) {
20076
- this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
20857
+ if (this.canStartParallelTask(state, input.targetAgent)) {
20858
+ this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
20859
+ return null;
20077
20860
  }
20078
- const task = {
20861
+ const now = this.deps.now().toISOString();
20862
+ const queuedTask = {
20079
20863
  taskId,
20080
20864
  sourceHandle: input.sourceHandle,
20081
20865
  sourceKind: input.sourceKind,
20082
20866
  coordinatorSession: input.coordinatorSession,
20083
- workerSession: ensuredWorkerSession,
20867
+ workerSession,
20084
20868
  workspace: input.workspace,
20085
20869
  ...input.cwd ? { cwd: input.cwd } : {},
20086
20870
  targetAgent: input.targetAgent,
20087
20871
  ...role ? { role } : {},
20088
20872
  ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
20089
20873
  task: input.task,
20090
- status: "running",
20874
+ status: "queued",
20875
+ ephemeralWorkerSession: true,
20091
20876
  summary: "",
20092
20877
  resultText: "",
20093
20878
  createdAt: now,
20094
20879
  updatedAt: now,
20095
20880
  eventSeq: 1,
20096
- events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
20881
+ events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }],
20097
20882
  ...input.chatKey ? { chatKey: input.chatKey } : {},
20098
20883
  ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
20099
20884
  ...input.accountId ? { accountId: input.accountId } : {}
20100
20885
  };
20101
- let previousGroup;
20102
- if (normalizedGroupId) {
20103
- const group = this.ensureGroups(state)[normalizedGroupId];
20104
- previousGroup = { ...group };
20105
- group.updatedAt = now;
20106
- group.coordinatorInjectedAt = undefined;
20107
- group.injectionPending = undefined;
20108
- group.injectionAppliedAt = undefined;
20109
- group.lastInjectionError = undefined;
20110
- }
20111
- const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
20112
- this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
20113
- this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
20114
- state.orchestration.tasks[taskId] = task;
20115
- state.orchestration.workerBindings[ensuredWorkerSession] = {
20116
- sourceHandle: ensuredWorkerSession,
20886
+ state.orchestration.tasks[taskId] = queuedTask;
20887
+ await this.deps.saveState(state);
20888
+ return { taskId, status: "queued", workerSession };
20889
+ });
20890
+ if (queuedResult) {
20891
+ this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
20892
+ return queuedResult;
20893
+ }
20894
+ }
20895
+ const releasePendingParallelStart = input.parallel ? () => {
20896
+ const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
20897
+ if (count <= 1) {
20898
+ this.pendingParallelStarts.delete(input.targetAgent);
20899
+ } else {
20900
+ this.pendingParallelStarts.set(input.targetAgent, count - 1);
20901
+ }
20902
+ } : undefined;
20903
+ let ensuredWorkerSession = workerSession;
20904
+ let prepared;
20905
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
20906
+ try {
20907
+ try {
20908
+ ensuredWorkerSession = await this.ensureReservedWorkerSession({
20909
+ workerSession,
20910
+ sourceHandle: input.sourceHandle,
20911
+ sourceKind: input.sourceKind,
20117
20912
  coordinatorSession: input.coordinatorSession,
20118
20913
  workspace: input.workspace,
20119
20914
  ...input.cwd ? { cwd: input.cwd } : {},
20120
20915
  targetAgent: input.targetAgent,
20121
20916
  role
20122
- };
20123
- await this.deps.saveState(state);
20124
- return {
20125
- task: { ...task },
20126
- previousBinding,
20127
- previousGroup,
20128
- normalizedGroupId
20129
- };
20130
- });
20131
- } catch (error2) {
20917
+ });
20918
+ prepared = await this.mutate(async () => {
20919
+ const state = await this.deps.loadState();
20920
+ const now = this.deps.now().toISOString();
20921
+ if (normalizedGroupId) {
20922
+ this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
20923
+ }
20924
+ const task = {
20925
+ taskId,
20926
+ sourceHandle: input.sourceHandle,
20927
+ sourceKind: input.sourceKind,
20928
+ coordinatorSession: input.coordinatorSession,
20929
+ workerSession: ensuredWorkerSession,
20930
+ workspace: input.workspace,
20931
+ ...input.cwd ? { cwd: input.cwd } : {},
20932
+ targetAgent: input.targetAgent,
20933
+ ...role ? { role } : {},
20934
+ ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
20935
+ task: input.task,
20936
+ status: "running",
20937
+ summary: "",
20938
+ resultText: "",
20939
+ createdAt: now,
20940
+ updatedAt: now,
20941
+ eventSeq: 1,
20942
+ events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
20943
+ ...input.chatKey ? { chatKey: input.chatKey } : {},
20944
+ ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
20945
+ ...input.accountId ? { accountId: input.accountId } : {},
20946
+ ...input.parallel ? { ephemeralWorkerSession: true } : {}
20947
+ };
20948
+ let previousGroup;
20949
+ if (normalizedGroupId) {
20950
+ const group = this.ensureGroups(state)[normalizedGroupId];
20951
+ previousGroup = { ...group };
20952
+ group.updatedAt = now;
20953
+ group.coordinatorInjectedAt = undefined;
20954
+ group.injectionPending = undefined;
20955
+ group.injectionAppliedAt = undefined;
20956
+ group.lastInjectionError = undefined;
20957
+ }
20958
+ const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
20959
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
20960
+ this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
20961
+ state.orchestration.tasks[taskId] = task;
20962
+ state.orchestration.workerBindings[ensuredWorkerSession] = {
20963
+ sourceHandle: ensuredWorkerSession,
20964
+ coordinatorSession: input.coordinatorSession,
20965
+ workspace: input.workspace,
20966
+ ...input.cwd ? { cwd: input.cwd } : {},
20967
+ targetAgent: input.targetAgent,
20968
+ role,
20969
+ ...input.parallel ? { ephemeral: true } : {}
20970
+ };
20971
+ await this.deps.saveState(state);
20972
+ return {
20973
+ task: { ...task },
20974
+ previousBinding,
20975
+ previousGroup,
20976
+ normalizedGroupId
20977
+ };
20978
+ });
20979
+ } catch (error2) {
20980
+ await releaseWorkerReservation();
20981
+ throw error2;
20982
+ }
20132
20983
  await releaseWorkerReservation();
20133
- throw error2;
20984
+ } finally {
20985
+ releasePendingParallelStart?.();
20134
20986
  }
20135
- await releaseWorkerReservation();
20136
20987
  try {
20137
20988
  await this.deps.dispatchWorkerTask({
20138
20989
  taskId,
@@ -20182,6 +21033,7 @@ class OrchestrationService {
20182
21033
  return { sourceContext, targetLocation, role, normalizedGroupId };
20183
21034
  });
20184
21035
  const autoRun = preflight.sourceContext.sourceKind === "coordinator";
21036
+ const taskId = this.deps.createId();
20185
21037
  const workerSessionName = await this.resolveWorkerSession({
20186
21038
  sourceHandle: input.sourceHandle,
20187
21039
  sourceKind: preflight.sourceContext.sourceKind,
@@ -20190,18 +21042,18 @@ class OrchestrationService {
20190
21042
  ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
20191
21043
  targetAgent: input.targetAgent,
20192
21044
  task: input.task,
20193
- ...preflight.role ? { role: preflight.role } : {}
21045
+ ...preflight.role ? { role: preflight.role } : {},
21046
+ ...input.parallel ? { parallel: true } : {}
20194
21047
  });
20195
- const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
20196
- let prepared;
20197
- try {
20198
- prepared = await this.mutate(async () => {
21048
+ if (input.parallel && autoRun) {
21049
+ const queuedResult = await this.mutate(async () => {
20199
21050
  const state = await this.deps.loadState();
20200
- this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
21051
+ if (this.canStartParallelTask(state, input.targetAgent)) {
21052
+ this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
21053
+ return null;
21054
+ }
20201
21055
  const now = this.deps.now().toISOString();
20202
- const taskId = this.deps.createId();
20203
- const status = autoRun ? "running" : "needs_confirmation";
20204
- const task = {
21056
+ const queuedTask = {
20205
21057
  taskId,
20206
21058
  sourceHandle: input.sourceHandle,
20207
21059
  sourceKind: preflight.sourceContext.sourceKind,
@@ -20213,49 +21065,101 @@ class OrchestrationService {
20213
21065
  ...preflight.role ? { role: preflight.role } : {},
20214
21066
  ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
20215
21067
  task: input.task,
20216
- status,
21068
+ status: "queued",
21069
+ ephemeralWorkerSession: true,
20217
21070
  summary: "",
20218
21071
  resultText: "",
20219
21072
  createdAt: now,
20220
21073
  updatedAt: now,
20221
21074
  eventSeq: 1,
20222
- events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }]
21075
+ events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }]
20223
21076
  };
20224
- if (preflight.normalizedGroupId) {
20225
- const group = this.ensureGroups(state)[preflight.normalizedGroupId];
20226
- group.updatedAt = now;
20227
- group.coordinatorInjectedAt = undefined;
20228
- group.injectionPending = undefined;
20229
- group.injectionAppliedAt = undefined;
20230
- group.lastInjectionError = undefined;
20231
- }
20232
- let previousBinding;
20233
- if (autoRun) {
20234
- previousBinding = state.orchestration.workerBindings[workerSessionName];
20235
- this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
20236
- this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
20237
- state.orchestration.tasks[taskId] = task;
20238
- state.orchestration.workerBindings[workerSessionName] = {
20239
- sourceHandle: workerSessionName,
21077
+ state.orchestration.tasks[taskId] = queuedTask;
21078
+ await this.deps.saveState(state);
21079
+ return { taskId, status: "queued", workerSession: workerSessionName };
21080
+ });
21081
+ if (queuedResult) {
21082
+ this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
21083
+ return queuedResult;
21084
+ }
21085
+ }
21086
+ const releasePendingParallelStart = input.parallel && autoRun ? () => {
21087
+ const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
21088
+ if (count <= 1) {
21089
+ this.pendingParallelStarts.delete(input.targetAgent);
21090
+ } else {
21091
+ this.pendingParallelStarts.set(input.targetAgent, count - 1);
21092
+ }
21093
+ } : undefined;
21094
+ let prepared;
21095
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
21096
+ try {
21097
+ try {
21098
+ prepared = await this.mutate(async () => {
21099
+ const state = await this.deps.loadState();
21100
+ this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
21101
+ const now = this.deps.now().toISOString();
21102
+ const status = autoRun ? "running" : "needs_confirmation";
21103
+ const task = {
21104
+ taskId,
21105
+ sourceHandle: input.sourceHandle,
21106
+ sourceKind: preflight.sourceContext.sourceKind,
20240
21107
  coordinatorSession: preflight.sourceContext.coordinatorSession,
21108
+ workerSession: workerSessionName,
20241
21109
  workspace: preflight.targetLocation.workspace,
20242
21110
  ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
20243
21111
  targetAgent: input.targetAgent,
20244
- role: preflight.role
21112
+ ...preflight.role ? { role: preflight.role } : {},
21113
+ ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
21114
+ task: input.task,
21115
+ status,
21116
+ summary: "",
21117
+ resultText: "",
21118
+ createdAt: now,
21119
+ updatedAt: now,
21120
+ eventSeq: 1,
21121
+ events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }],
21122
+ ...input.parallel ? { ephemeralWorkerSession: true } : {}
20245
21123
  };
20246
- } else {
20247
- this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
20248
- this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
20249
- state.orchestration.tasks[taskId] = task;
20250
- }
20251
- await this.deps.saveState(state);
20252
- return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
20253
- });
20254
- } catch (error2) {
21124
+ if (preflight.normalizedGroupId) {
21125
+ const group = this.ensureGroups(state)[preflight.normalizedGroupId];
21126
+ group.updatedAt = now;
21127
+ group.coordinatorInjectedAt = undefined;
21128
+ group.injectionPending = undefined;
21129
+ group.injectionAppliedAt = undefined;
21130
+ group.lastInjectionError = undefined;
21131
+ }
21132
+ let previousBinding;
21133
+ if (autoRun) {
21134
+ previousBinding = state.orchestration.workerBindings[workerSessionName];
21135
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
21136
+ this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
21137
+ state.orchestration.tasks[taskId] = task;
21138
+ state.orchestration.workerBindings[workerSessionName] = {
21139
+ sourceHandle: workerSessionName,
21140
+ coordinatorSession: preflight.sourceContext.coordinatorSession,
21141
+ workspace: preflight.targetLocation.workspace,
21142
+ ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
21143
+ targetAgent: input.targetAgent,
21144
+ role: preflight.role,
21145
+ ...input.parallel ? { ephemeral: true } : {}
21146
+ };
21147
+ } else {
21148
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
21149
+ this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
21150
+ state.orchestration.tasks[taskId] = task;
21151
+ }
21152
+ await this.deps.saveState(state);
21153
+ return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
21154
+ });
21155
+ } catch (error2) {
21156
+ await releaseWorkerReservation();
21157
+ throw error2;
21158
+ }
20255
21159
  await releaseWorkerReservation();
20256
- throw error2;
21160
+ } finally {
21161
+ releasePendingParallelStart?.();
20257
21162
  }
20258
- await releaseWorkerReservation();
20259
21163
  if (autoRun) {
20260
21164
  this.runAutoRunRpcWorkerTask({
20261
21165
  task: prepared.task,
@@ -21224,6 +22128,16 @@ class OrchestrationService {
21224
22128
  await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
21225
22129
  }
21226
22130
  }
22131
+ if (input.decision === "accept") {
22132
+ try {
22133
+ await this.reconcileParallelSlots();
22134
+ } catch (error2) {
22135
+ this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after contested result accepted", {
22136
+ taskId: prepared.task.taskId,
22137
+ message: error2 instanceof Error ? error2.message : String(error2)
22138
+ });
22139
+ }
22140
+ }
21227
22141
  return prepared.task;
21228
22142
  }
21229
22143
  async listTasks(filter) {
@@ -21690,6 +22604,16 @@ class OrchestrationService {
21690
22604
  if (prepared.closedPackageId) {
21691
22605
  await this.handoffQueuedQuestions(prepared.task.coordinatorSession, prepared.closedPackageId);
21692
22606
  }
22607
+ if (!prepared.shouldPropagate && this.isTerminalStatus(prepared.task.status)) {
22608
+ try {
22609
+ await this.reconcileParallelSlots();
22610
+ } catch (error2) {
22611
+ this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after non-running cancel", {
22612
+ taskId: prepared.task.taskId,
22613
+ message: error2 instanceof Error ? error2.message : String(error2)
22614
+ });
22615
+ }
22616
+ }
21693
22617
  return prepared.task;
21694
22618
  }
21695
22619
  async completeTaskCancellation(taskId) {
@@ -21752,6 +22676,14 @@ class OrchestrationService {
21752
22676
  return prepared.task;
21753
22677
  }
21754
22678
  this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", this.taskContext(prepared.task));
22679
+ try {
22680
+ await this.reconcileParallelSlots();
22681
+ } catch (error2) {
22682
+ this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after cancel completion", {
22683
+ taskId: prepared.task.taskId,
22684
+ message: error2 instanceof Error ? error2.message : String(error2)
22685
+ });
22686
+ }
21755
22687
  return prepared.task;
21756
22688
  }
21757
22689
  async failTaskCancellation(taskId, errorMessage) {
@@ -21796,6 +22728,34 @@ class OrchestrationService {
21796
22728
  task: currentTask.task,
21797
22729
  ...currentTask.role ? { role: currentTask.role } : {}
21798
22730
  });
22731
+ if (currentTask.ephemeralWorkerSession === true) {
22732
+ const queuedResult = await this.mutate(async () => {
22733
+ const state = await this.deps.loadState();
22734
+ const task = state.orchestration.tasks[input.taskId];
22735
+ if (!task) {
22736
+ throw new Error(`task "${input.taskId}" does not exist`);
22737
+ }
22738
+ this.assertCoordinatorOwnership(task, input.coordinatorSession);
22739
+ this.assertNeedsConfirmation(task);
22740
+ if (this.canStartParallelTask(state, task.targetAgent)) {
22741
+ return null;
22742
+ }
22743
+ const now = this.deps.now().toISOString();
22744
+ task.workerSession = workerSession;
22745
+ task.status = "queued";
22746
+ task.updatedAt = now;
22747
+ this.appendTaskEvent(task, now, "status_changed", {
22748
+ status: "queued",
22749
+ message: "Task queued at parallel capacity"
22750
+ });
22751
+ await this.deps.saveState(state);
22752
+ return { ...task };
22753
+ });
22754
+ if (queuedResult) {
22755
+ this.logEvent("orchestration.task.queued", "parallel task queued at capacity on approve", { taskId: input.taskId, targetAgent: currentTask.targetAgent });
22756
+ return queuedResult;
22757
+ }
22758
+ }
21799
22759
  const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession, input.taskId);
21800
22760
  let ensuredWorkerSession = workerSession;
21801
22761
  let prepared;
@@ -21837,7 +22797,8 @@ class OrchestrationService {
21837
22797
  workspace: task.workspace,
21838
22798
  ...task.cwd ? { cwd: task.cwd } : {},
21839
22799
  targetAgent: task.targetAgent,
21840
- role: task.role
22800
+ role: task.role,
22801
+ ...task.ephemeralWorkerSession ? { ephemeral: true } : {}
21841
22802
  };
21842
22803
  await this.deps.saveState(state);
21843
22804
  return {
@@ -21891,6 +22852,10 @@ class OrchestrationService {
21891
22852
  }
21892
22853
  async resolveWorkerSession(input) {
21893
22854
  const role = this.normalizeRole(input.role);
22855
+ const baseName = [input.workspace, input.cwd ? this.cwdWorkerSessionPart(input.cwd) : undefined, input.targetAgent, role, input.coordinatorSession].filter((part) => typeof part === "string" && part.trim().length > 0).map((part) => part.trim()).join(":");
22856
+ if (input.parallel) {
22857
+ return `${baseName}:p-${this.deps.createId()}`;
22858
+ }
21894
22859
  const reusable = await this.deps.findReusableWorkerSession?.({
21895
22860
  sourceHandle: input.sourceHandle,
21896
22861
  sourceKind: input.sourceKind,
@@ -21903,7 +22868,7 @@ class OrchestrationService {
21903
22868
  if (reusable && reusable.trim().length > 0) {
21904
22869
  return reusable.trim();
21905
22870
  }
21906
- return [input.workspace, input.cwd ? this.cwdWorkerSessionPart(input.cwd) : undefined, input.targetAgent, role, input.coordinatorSession].filter((part) => typeof part === "string" && part.trim().length > 0).map((part) => part.trim()).join(":");
22871
+ return baseName;
21907
22872
  }
21908
22873
  async reserveProposedWorkerSession(workerSession, excludingTaskId) {
21909
22874
  await this.mutate(async () => {
@@ -22061,7 +23026,7 @@ class OrchestrationService {
22061
23026
  if (role && policy.allowedAgentRequestRoles.length > 0 && !policy.allowedAgentRequestRoles.includes(role)) {
22062
23027
  throw new Error(`role "${role}" is not allowed for agent-requested delegation`);
22063
23028
  }
22064
- const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running"));
23029
+ const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running" || task.status === "queued"));
22065
23030
  if (outstandingRequests.length >= policy.maxPendingAgentRequestsPerCoordinator) {
22066
23031
  throw new Error("agent-requested delegation quota exceeded for this coordinator");
22067
23032
  }
@@ -22226,6 +23191,137 @@ class OrchestrationService {
22226
23191
  hasActiveTaskWorkerSession(state, workerSession, excludingTaskId) {
22227
23192
  return Object.values(state.orchestration.tasks).some((task) => task.taskId !== excludingTaskId && task.workerSession === workerSession && (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined));
22228
23193
  }
23194
+ countActiveParallelSlots(state, targetAgent) {
23195
+ const persisted = Object.values(state.orchestration.tasks).filter((task) => task.ephemeralWorkerSession === true && task.targetAgent === targetAgent && (task.status === "running" || task.status === "blocked" || task.status === "waiting_for_human")).length;
23196
+ const pending = this.pendingParallelStarts.get(targetAgent) ?? 0;
23197
+ return persisted + pending;
23198
+ }
23199
+ canStartParallelTask(state, targetAgent) {
23200
+ const cap = this.deps.config.orchestration.maxParallelTasksPerAgent;
23201
+ return this.countActiveParallelSlots(state, targetAgent) < cap;
23202
+ }
23203
+ async reconcileParallelSlots() {
23204
+ const toClose = await this.mutate(async () => {
23205
+ const state = await this.deps.loadState();
23206
+ const collected = [];
23207
+ for (const task of Object.values(state.orchestration.tasks)) {
23208
+ if (task.ephemeralWorkerSession === true && task.ephemeralWorkerSessionClosed !== true && task.workerSession && task.reviewPending === undefined && this.isTerminalStatus(task.status)) {
23209
+ task.ephemeralWorkerSessionClosed = true;
23210
+ if (state.orchestration.workerBindings[task.workerSession] !== undefined) {
23211
+ delete state.orchestration.workerBindings[task.workerSession];
23212
+ collected.push({
23213
+ workerSession: task.workerSession,
23214
+ coordinatorSession: task.coordinatorSession,
23215
+ workspace: task.workspace,
23216
+ ...task.cwd ? { cwd: task.cwd } : {},
23217
+ targetAgent: task.targetAgent,
23218
+ ...task.role ? { role: task.role } : {}
23219
+ });
23220
+ }
23221
+ }
23222
+ }
23223
+ if (collected.length > 0) {
23224
+ await this.deps.saveState(state);
23225
+ }
23226
+ return collected;
23227
+ });
23228
+ for (const req of toClose) {
23229
+ try {
23230
+ await this.deps.closeWorkerSession?.(req);
23231
+ } catch (error2) {
23232
+ this.logEvent("orchestration.parallel.close_failed", "failed to close ephemeral worker session", {
23233
+ workerSession: req.workerSession,
23234
+ message: error2 instanceof Error ? error2.message : String(error2)
23235
+ });
23236
+ }
23237
+ }
23238
+ for (;; ) {
23239
+ const next = await this.mutate(async () => {
23240
+ const state = await this.deps.loadState();
23241
+ const queued = Object.values(state.orchestration.tasks).filter((t) => t.status === "queued" && t.ephemeralWorkerSession === true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
23242
+ for (const task of queued) {
23243
+ if (!this.canStartParallelTask(state, task.targetAgent)) {
23244
+ continue;
23245
+ }
23246
+ task.status = "running";
23247
+ task.updatedAt = this.deps.now().toISOString();
23248
+ state.orchestration.workerBindings[task.workerSession] = {
23249
+ sourceHandle: task.workerSession,
23250
+ coordinatorSession: task.coordinatorSession,
23251
+ workspace: task.workspace,
23252
+ ...task.cwd ? { cwd: task.cwd } : {},
23253
+ targetAgent: task.targetAgent,
23254
+ ...task.role ? { role: task.role } : {},
23255
+ ephemeral: true
23256
+ };
23257
+ await this.deps.saveState(state);
23258
+ return { ...task };
23259
+ }
23260
+ return null;
23261
+ });
23262
+ if (!next) {
23263
+ break;
23264
+ }
23265
+ try {
23266
+ await this.ensureReservedWorkerSession({
23267
+ workerSession: next.workerSession,
23268
+ sourceHandle: next.sourceHandle,
23269
+ sourceKind: next.sourceKind,
23270
+ coordinatorSession: next.coordinatorSession,
23271
+ workspace: next.workspace,
23272
+ ...next.cwd ? { cwd: next.cwd } : {},
23273
+ targetAgent: next.targetAgent,
23274
+ ...next.role ? { role: next.role } : {}
23275
+ });
23276
+ await this.deps.dispatchWorkerTask({
23277
+ taskId: next.taskId,
23278
+ workerSession: next.workerSession,
23279
+ coordinatorSession: next.coordinatorSession,
23280
+ workspace: next.workspace,
23281
+ ...next.cwd ? { cwd: next.cwd } : {},
23282
+ targetAgent: next.targetAgent,
23283
+ ...next.role ? { role: next.role } : {},
23284
+ task: next.task
23285
+ });
23286
+ } catch (error2) {
23287
+ await this.mutate(async () => {
23288
+ const state = await this.deps.loadState();
23289
+ const task = state.orchestration.tasks[next.taskId];
23290
+ if (task && task.status === "running") {
23291
+ task.status = "queued";
23292
+ task.updatedAt = this.deps.now().toISOString();
23293
+ delete state.orchestration.workerBindings[next.workerSession];
23294
+ this.appendTaskEvent(task, task.updatedAt, "status_changed", {
23295
+ status: "queued",
23296
+ message: "Task re-queued after drain failure"
23297
+ });
23298
+ await this.deps.saveState(state);
23299
+ }
23300
+ });
23301
+ this.logEvent("orchestration.parallel.drain_failed", "failed to drain queued parallel task", {
23302
+ taskId: next.taskId,
23303
+ workerSession: next.workerSession,
23304
+ message: error2 instanceof Error ? error2.message : String(error2)
23305
+ });
23306
+ break;
23307
+ }
23308
+ await this.mutate(async () => {
23309
+ const state = await this.deps.loadState();
23310
+ const task = state.orchestration.tasks[next.taskId];
23311
+ if (task && task.status === "running") {
23312
+ this.appendTaskEvent(task, task.updatedAt, "status_changed", {
23313
+ status: "running",
23314
+ message: "Task drained from parallel queue"
23315
+ });
23316
+ await this.deps.saveState(state);
23317
+ }
23318
+ });
23319
+ this.logEvent("orchestration.task.drained", "parallel task drained from queue", {
23320
+ taskId: next.taskId,
23321
+ targetAgent: next.targetAgent
23322
+ });
23323
+ }
23324
+ }
22229
23325
  async assertProposedWorkerSessionDoesNotConflictExternalCoordinator(workerSession) {
22230
23326
  const state = await this.deps.loadState();
22231
23327
  this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
@@ -22884,6 +23980,20 @@ class SessionService {
22884
23980
  async createSession(alias, agent, workspace) {
22885
23981
  return await this.createLogicalSession(alias, agent, workspace, `${workspace}:${alias}`);
22886
23982
  }
23983
+ listAllResolvedSessions() {
23984
+ const seen = new Set;
23985
+ const resolved = [];
23986
+ for (const session of Object.values(this.state.sessions)) {
23987
+ if (seen.has(session.transport_session)) {
23988
+ continue;
23989
+ }
23990
+ seen.add(session.transport_session);
23991
+ try {
23992
+ resolved.push(this.toResolvedSession(session));
23993
+ } catch {}
23994
+ }
23995
+ return resolved;
23996
+ }
22887
23997
  resolveSession(alias, agent, workspace, transportSession) {
22888
23998
  this.validateSession(alias, agent, workspace);
22889
23999
  return this.toResolvedSession({
@@ -23236,6 +24346,13 @@ async function runConsole(paths, deps) {
23236
24346
  trigger: "startup"
23237
24347
  });
23238
24348
  } catch {}
24349
+ try {
24350
+ await runtime.orchestration.service.reconcileParallelSlots();
24351
+ } catch (reconcileError) {
24352
+ await runtime.logger.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots at startup", {
24353
+ message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
24354
+ });
24355
+ }
23239
24356
  consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
23240
24357
  if (consumerLock) {
23241
24358
  const lockMeta = {
@@ -23564,7 +24681,8 @@ async function spawnAcpxBridgeClient(options = {}) {
23564
24681
  ...process.env,
23565
24682
  WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
23566
24683
  WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
23567
- WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
24684
+ WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
24685
+ ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { WEACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {}
23568
24686
  },
23569
24687
  stdio: ["pipe", "pipe", "inherit"]
23570
24688
  });
@@ -23992,9 +25110,9 @@ var init_spawn_command = __esm(() => {
23992
25110
  });
23993
25111
 
23994
25112
  // src/transport/prompt-media.ts
23995
- import { mkdtemp, open as open3, rm as rm8, writeFile as writeFile8 } from "node:fs/promises";
25113
+ import { mkdtemp, open as open4, rm as rm8, writeFile as writeFile7 } from "node:fs/promises";
23996
25114
  import { tmpdir as defaultTmpdir } from "node:os";
23997
- import path13 from "node:path";
25115
+ import path14 from "node:path";
23998
25116
  import { pathToFileURL as pathToFileURL2 } from "node:url";
23999
25117
  async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
24000
25118
  const mediaList = normalizePromptMedia(media);
@@ -24028,7 +25146,7 @@ async function createStructuredPromptFile(text, media, deps = defaultStructuredP
24028
25146
  type: "resource",
24029
25147
  resource: {
24030
25148
  uri: pathToFileURL2(item.filePath).toString(),
24031
- text: `${item.fileName ?? path13.basename(item.filePath)} ${item.mimeType} ${item.type}`
25149
+ text: `${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.type}`
24032
25150
  }
24033
25151
  });
24034
25152
  }
@@ -24042,7 +25160,7 @@ function normalizePromptMedia(media) {
24042
25160
  function buildAttachmentSummary(items) {
24043
25161
  const lines = ["Attachments available as local files:"];
24044
25162
  for (const [index, item] of items.entries()) {
24045
- lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path13.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
25163
+ lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
24046
25164
  }
24047
25165
  return lines.join(`
24048
25166
  `);
@@ -24050,8 +25168,8 @@ function buildAttachmentSummary(items) {
24050
25168
  async function writeStructuredPromptBlocks(blocks, deps) {
24051
25169
  let dir = "";
24052
25170
  try {
24053
- dir = await deps.mkdtemp(path13.join(deps.tmpdir(), "weacpx-acp-prompt-"));
24054
- const filePath = path13.join(dir, "prompt.json");
25171
+ dir = await deps.mkdtemp(path14.join(deps.tmpdir(), "weacpx-acp-prompt-"));
25172
+ const filePath = path14.join(dir, "prompt.json");
24055
25173
  await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
24056
25174
  return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
24057
25175
  } catch (error2) {
@@ -24061,7 +25179,7 @@ async function writeStructuredPromptBlocks(blocks, deps) {
24061
25179
  }
24062
25180
  }
24063
25181
  async function readImageFileBounded(filePath, maxBytes) {
24064
- const handle = await open3(filePath, "r");
25182
+ const handle = await open4(filePath, "r");
24065
25183
  try {
24066
25184
  const imageStats = await handle.stat();
24067
25185
  if (!imageStats.isFile()) {
@@ -24116,7 +25234,7 @@ var init_prompt_media = __esm(() => {
24116
25234
  defaultStructuredPromptFileDeps = {
24117
25235
  readImageFile: readImageFileBounded,
24118
25236
  mkdtemp,
24119
- writeFile: writeFile8,
25237
+ writeFile: writeFile7,
24120
25238
  rm: rm8,
24121
25239
  tmpdir: defaultTmpdir
24122
25240
  };
@@ -24423,12 +25541,12 @@ function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
24423
25541
  }
24424
25542
  return join12(dirname12(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
24425
25543
  }
24426
- async function ensureNodePtyHelperExecutable(helperPath, chmod2 = chmodFs) {
25544
+ async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
24427
25545
  if (!helperPath) {
24428
25546
  return;
24429
25547
  }
24430
25548
  try {
24431
- await chmod2(helperPath, 493);
25549
+ await chmod3(helperPath, 493);
24432
25550
  } catch (error2) {
24433
25551
  if (error2.code === "ENOENT") {
24434
25552
  return;
@@ -24721,6 +25839,7 @@ class AcpxCliTransport {
24721
25839
  permissionMode;
24722
25840
  nonInteractivePermissions;
24723
25841
  permissionPolicy;
25842
+ queueOwnerTtlSeconds;
24724
25843
  runCommand;
24725
25844
  runPtyCommand;
24726
25845
  queueOwnerLauncher;
@@ -24731,10 +25850,12 @@ class AcpxCliTransport {
24731
25850
  this.permissionMode = options.permissionMode ?? "approve-all";
24732
25851
  this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
24733
25852
  this.permissionPolicy = options.permissionPolicy;
25853
+ this.queueOwnerTtlSeconds = options.queueOwnerTtlSeconds;
24734
25854
  this.runCommand = runCommand;
24735
25855
  this.runPtyCommand = runPtyCommand;
24736
25856
  this.queueOwnerLauncher = queueOwnerLauncher ?? new AcpxQueueOwnerLauncher({
24737
- acpxCommand: this.command
25857
+ acpxCommand: this.command,
25858
+ ...typeof this.queueOwnerTtlSeconds === "number" && Number.isFinite(this.queueOwnerTtlSeconds) ? { ttlMs: this.queueOwnerTtlSeconds * 1000 } : {}
24738
25859
  });
24739
25860
  this.streamingHooks = streamingHooks;
24740
25861
  }
@@ -25058,7 +26179,8 @@ ${baseText}` : "" };
25058
26179
  "--json-strict",
25059
26180
  "--cwd",
25060
26181
  session.cwd,
25061
- ...this.buildPermissionArgs()
26182
+ ...this.buildPermissionArgs(),
26183
+ ...this.buildQueueOwnerTtlArgs()
25062
26184
  ];
25063
26185
  const tail2 = promptFile ? ["prompt", "-s", session.transportSession, "--file", promptFile] : ["prompt", "-s", session.transportSession, text];
25064
26186
  if (session.agentCommand) {
@@ -25066,6 +26188,12 @@ ${baseText}` : "" };
25066
26188
  }
25067
26189
  return [...prefix, session.agent, ...tail2];
25068
26190
  }
26191
+ buildQueueOwnerTtlArgs() {
26192
+ if (typeof this.queueOwnerTtlSeconds !== "number" || !Number.isFinite(this.queueOwnerTtlSeconds)) {
26193
+ return [];
26194
+ }
26195
+ return ["--ttl", String(this.queueOwnerTtlSeconds)];
26196
+ }
25069
26197
  buildPermissionArgs() {
25070
26198
  const modeFlag = permissionModeToFlag(this.permissionMode);
25071
26199
  const args = [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
@@ -25112,6 +26240,146 @@ var init_acpx_cli_transport = __esm(() => {
25112
26240
  require4 = createRequire5(import.meta.url);
25113
26241
  });
25114
26242
 
26243
+ // src/transport/queue-owner-reaper.ts
26244
+ import { spawn as spawn10 } from "node:child_process";
26245
+ async function reapQueueOwners(acpxCommand, targets, deps = {}) {
26246
+ const resolveRecordId = deps.resolveRecordId ?? defaultResolveRecordId;
26247
+ const terminate = deps.terminate ?? terminateAcpxQueueOwner;
26248
+ const timeoutMs = deps.timeoutMs ?? 5000;
26249
+ const seen = new Set;
26250
+ const unique = targets.filter((target) => {
26251
+ if (seen.has(target.transportSession)) {
26252
+ return false;
26253
+ }
26254
+ seen.add(target.transportSession);
26255
+ return true;
26256
+ });
26257
+ let terminated = 0;
26258
+ const reapOne = async (target) => {
26259
+ try {
26260
+ const recordId = await resolveRecordId(acpxCommand, target);
26261
+ if (!recordId) {
26262
+ return;
26263
+ }
26264
+ await terminate(recordId);
26265
+ terminated += 1;
26266
+ } catch (error2) {
26267
+ deps.onError?.(target, error2);
26268
+ }
26269
+ };
26270
+ await settleWithinTimeout(Promise.all(unique.map(reapOne)), timeoutMs);
26271
+ return { terminated, attempted: unique.length };
26272
+ }
26273
+ function settleWithinTimeout(work, timeoutMs) {
26274
+ return new Promise((resolve3) => {
26275
+ let settled = false;
26276
+ const finish = () => {
26277
+ if (!settled) {
26278
+ settled = true;
26279
+ resolve3();
26280
+ }
26281
+ };
26282
+ const timer = setTimeout(finish, timeoutMs);
26283
+ if (typeof timer.unref === "function") {
26284
+ timer.unref();
26285
+ }
26286
+ work.then(() => {
26287
+ clearTimeout(timer);
26288
+ finish();
26289
+ }, () => {
26290
+ clearTimeout(timer);
26291
+ finish();
26292
+ });
26293
+ });
26294
+ }
26295
+ async function defaultResolveRecordId(acpxCommand, target) {
26296
+ const args = [
26297
+ "--format",
26298
+ "quiet",
26299
+ "--cwd",
26300
+ target.cwd,
26301
+ ...target.agentCommand ? ["--agent", target.agentCommand] : [target.agent],
26302
+ "sessions",
26303
+ "show",
26304
+ target.transportSession
26305
+ ];
26306
+ const spawnSpec = resolveSpawnCommand(acpxCommand, args);
26307
+ const result = await runCapture2(spawnSpec.command, spawnSpec.args, 4000);
26308
+ if (result.code !== 0) {
26309
+ return null;
26310
+ }
26311
+ return parseRecordId(result.stdout);
26312
+ }
26313
+ function parseRecordId(stdout2) {
26314
+ try {
26315
+ const parsed = JSON.parse(stdout2);
26316
+ if (typeof parsed.acpxRecordId === "string") {
26317
+ return parsed.acpxRecordId;
26318
+ }
26319
+ if (typeof parsed.id === "string") {
26320
+ return parsed.id;
26321
+ }
26322
+ } catch {
26323
+ const firstLine = stdout2.trim().split(/\r?\n/, 1)[0];
26324
+ if (firstLine && /^[\w.:-]+$/.test(firstLine) && firstLine.length >= 8) {
26325
+ return firstLine;
26326
+ }
26327
+ }
26328
+ return null;
26329
+ }
26330
+ function runCapture2(command, args, timeoutMs) {
26331
+ return new Promise((resolve3) => {
26332
+ const child = spawn10(command, args, { stdio: ["ignore", "pipe", "ignore"] });
26333
+ let stdout2 = "";
26334
+ let done = false;
26335
+ const finish = (code) => {
26336
+ if (done) {
26337
+ return;
26338
+ }
26339
+ done = true;
26340
+ clearTimeout(timer);
26341
+ resolve3({ code, stdout: stdout2 });
26342
+ };
26343
+ const timer = setTimeout(() => {
26344
+ child.kill("SIGKILL");
26345
+ finish(1);
26346
+ }, timeoutMs);
26347
+ child.stdout?.on("data", (chunk) => {
26348
+ stdout2 += String(chunk);
26349
+ });
26350
+ child.once("error", () => finish(1));
26351
+ child.once("close", (code) => finish(code ?? 1));
26352
+ });
26353
+ }
26354
+ var init_queue_owner_reaper = __esm(() => {
26355
+ init_spawn_command();
26356
+ init_acpx_queue_owner_launcher();
26357
+ });
26358
+
26359
+ // src/transport/collect-reap-targets.ts
26360
+ function workerBindingReapTargets(orchestration, config2) {
26361
+ const targets = [];
26362
+ for (const [workerSession, binding] of Object.entries(orchestration.workerBindings)) {
26363
+ const agentConfig = config2.agents[binding.targetAgent];
26364
+ if (!agentConfig) {
26365
+ continue;
26366
+ }
26367
+ const cwd = binding.cwd ?? config2.workspaces[binding.workspace]?.cwd;
26368
+ if (!cwd) {
26369
+ continue;
26370
+ }
26371
+ const agentCommand = resolveAgentCommand(agentConfig.driver, agentConfig.command);
26372
+ targets.push({
26373
+ agent: binding.targetAgent,
26374
+ ...agentCommand ? { agentCommand } : {},
26375
+ cwd,
26376
+ transportSession: workerSession
26377
+ });
26378
+ }
26379
+ return targets;
26380
+ }
26381
+ var init_collect_reap_targets = () => {};
26382
+
25115
26383
  // src/channels/channel-registry.ts
25116
26384
  var exports_channel_registry = {};
25117
26385
  __export(exports_channel_registry, {
@@ -25307,7 +26575,11 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
25307
26575
  if (thresholdSeconds <= 0) {
25308
26576
  return;
25309
26577
  }
26578
+ let ticking = false;
25310
26579
  return setInterval(async () => {
26580
+ if (ticking)
26581
+ return;
26582
+ ticking = true;
25311
26583
  try {
25312
26584
  const tasks = await orchestration.listHeartbeatTasks(thresholdSeconds);
25313
26585
  for (const task of tasks) {
@@ -25328,6 +26600,8 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
25328
26600
  await logger2.error("orchestration.heartbeat.check_failed", "heartbeat check failed", {
25329
26601
  message: error2 instanceof Error ? error2.message : String(error2)
25330
26602
  });
26603
+ } finally {
26604
+ ticking = false;
25331
26605
  }
25332
26606
  }, 60000);
25333
26607
  }
@@ -25380,7 +26654,8 @@ async function buildApp(paths, deps = {}) {
25380
26654
  acpxCommand,
25381
26655
  bridgeEntryPath: resolveBridgeEntryPath(),
25382
26656
  permissionMode: config2.transport.permissionMode,
25383
- nonInteractivePermissions: config2.transport.nonInteractivePermissions
26657
+ nonInteractivePermissions: config2.transport.nonInteractivePermissions,
26658
+ ...typeof config2.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config2.transport.queueOwnerTtlSeconds } : {}
25384
26659
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config2.transport, command: acpxCommand });
25385
26660
  const quota = new QuotaManager({
25386
26661
  onInbound: (chatKey) => {
@@ -25595,6 +26870,14 @@ async function buildApp(paths, deps = {}) {
25595
26870
  resultText: ""
25596
26871
  });
25597
26872
  }
26873
+ try {
26874
+ await orchestration.reconcileParallelSlots();
26875
+ } catch (reconcileError) {
26876
+ await logger2.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots after worker turn", {
26877
+ taskId: input.taskId,
26878
+ message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
26879
+ });
26880
+ }
25598
26881
  if (taskRecord && shouldNotifyTaskCompletion(taskRecord)) {
25599
26882
  try {
25600
26883
  await sendCompletionNotice(taskRecord);
@@ -25662,6 +26945,13 @@ async function buildApp(paths, deps = {}) {
25662
26945
  throw new Error(result.message || "worker task cancel was not acknowledged");
25663
26946
  }
25664
26947
  },
26948
+ closeWorkerSession: async ({ workerSession, targetAgent, workspace, cwd }) => {
26949
+ if (!transport.removeSession) {
26950
+ return;
26951
+ }
26952
+ const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
26953
+ await transport.removeSession(session);
26954
+ },
25665
26955
  resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, cwd, answer }) => {
25666
26956
  launchWorkerTurn({
25667
26957
  taskId,
@@ -25687,7 +26977,7 @@ async function buildApp(paths, deps = {}) {
25687
26977
  }
25688
26978
  },
25689
26979
  findReusableWorkerSession: async ({ coordinatorSession, workspace, cwd, targetAgent, role }) => {
25690
- const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.coordinatorSession === coordinatorSession && current.workspace === workspace && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
26980
+ const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.ephemeral !== true && current.coordinatorSession === coordinatorSession && current.workspace === workspace && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
25691
26981
  return binding?.[0] ?? null;
25692
26982
  },
25693
26983
  logger: logger2
@@ -25740,6 +27030,35 @@ async function buildApp(paths, deps = {}) {
25740
27030
  clearInterval(progressHeartbeatInterval);
25741
27031
  }
25742
27032
  await Promise.allSettled([...pendingWorkerDispatches]);
27033
+ try {
27034
+ const targets = [
27035
+ ...sessions.listAllResolvedSessions().map((session) => ({
27036
+ agent: session.agent,
27037
+ ...session.agentCommand ? { agentCommand: session.agentCommand } : {},
27038
+ cwd: session.cwd,
27039
+ transportSession: session.transportSession
27040
+ })),
27041
+ ...workerBindingReapTargets(state.orchestration, config2)
27042
+ ];
27043
+ if (targets.length > 0) {
27044
+ const { terminated, attempted } = await reapQueueOwners(acpxCommand, targets, {
27045
+ onError: (target, error2) => {
27046
+ logger2.info("transport.queue_owner_reap.failed", "failed to reap queue owner on shutdown", {
27047
+ transport_session: target.transportSession,
27048
+ error: error2 instanceof Error ? error2.message : String(error2)
27049
+ }).catch(() => {});
27050
+ }
27051
+ });
27052
+ await logger2.info("transport.queue_owner_reap.completed", "reaped warm queue owners on shutdown", {
27053
+ terminated,
27054
+ attempted
27055
+ }).catch(() => {});
27056
+ }
27057
+ } catch (err) {
27058
+ await logger2.error("transport.queue_owner_reap.error", "queue owner reap failed during shutdown", {
27059
+ error: err instanceof Error ? err.message : String(err)
27060
+ }).catch(() => {});
27061
+ }
25743
27062
  await debouncedStateStore.dispose();
25744
27063
  if ("dispose" in transport && typeof transport.dispose === "function") {
25745
27064
  await transport.dispose();
@@ -25770,7 +27089,12 @@ async function main() {
25770
27089
  await ensureConfigExists(paths.configPath);
25771
27090
  const startupConfig = await loadConfig(paths.configPath);
25772
27091
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
25773
- await loadConfiguredPlugins2({ plugins: startupConfig.plugins });
27092
+ await loadConfiguredPlugins2({
27093
+ plugins: startupConfig.plugins,
27094
+ onPluginError: ({ name, error: error2 }) => {
27095
+ console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
27096
+ }
27097
+ });
25774
27098
  const { channelDeps } = await prepareChannelMedia(paths.configPath, startupConfig);
25775
27099
  const channelRegistry = new MessageChannelRegistry(createMessageChannels2(startupConfig.channels, channelDeps));
25776
27100
  await runConsole(paths, {
@@ -25854,6 +27178,8 @@ var init_main = __esm(async () => {
25854
27178
  init_acpx_bridge_client();
25855
27179
  init_acpx_bridge_transport();
25856
27180
  init_acpx_cli_transport();
27181
+ init_queue_owner_reaper();
27182
+ init_collect_reap_targets();
25857
27183
  init_channel_registry();
25858
27184
  init_media_store();
25859
27185
  init_quota_errors();
@@ -25864,7 +27190,7 @@ var init_main = __esm(async () => {
25864
27190
  });
25865
27191
 
25866
27192
  // src/doctor/checks/acpx-check.ts
25867
- import { spawn as spawn10 } from "node:child_process";
27193
+ import { spawn as spawn11 } from "node:child_process";
25868
27194
  async function checkAcpx(options = {}) {
25869
27195
  const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
25870
27196
  try {
@@ -25911,7 +27237,7 @@ function buildDetails(metadata, version2, verbose) {
25911
27237
  async function defaultRunVersion(command) {
25912
27238
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
25913
27239
  return await new Promise((resolve3, reject) => {
25914
- const child = spawn10(spawnSpec.command, spawnSpec.args, {
27240
+ const child = spawn11(spawnSpec.command, spawnSpec.args, {
25915
27241
  stdio: ["ignore", "pipe", "pipe"]
25916
27242
  });
25917
27243
  let stdout2 = "";
@@ -26247,107 +27573,107 @@ async function checkRuntime(options = {}) {
26247
27573
  }
26248
27574
  function createRuntimeFsProbe() {
26249
27575
  return {
26250
- stat: async (path14) => await stat3(path14),
26251
- access: async (path14, mode) => await access4(path14, mode)
27576
+ stat: async (path15) => await stat3(path15),
27577
+ access: async (path15, mode) => await access4(path15, mode)
26252
27578
  };
26253
27579
  }
26254
- async function checkDirectoryCreatable(label, path14, probe, platform) {
27580
+ async function checkDirectoryCreatable(label, path15, probe, platform) {
26255
27581
  try {
26256
- const stats = await probe.stat(path14);
27582
+ const stats = await probe.stat(path15);
26257
27583
  if (!stats.isDirectory()) {
26258
27584
  return {
26259
27585
  ok: false,
26260
- detail: `${label}: ${path14} (exists but is not a directory)`
27586
+ detail: `${label}: ${path15} (exists but is not a directory)`
26261
27587
  };
26262
27588
  }
26263
- await probe.access(path14, directoryAccessMode(platform));
27589
+ await probe.access(path15, directoryAccessMode(platform));
26264
27590
  return {
26265
27591
  ok: true,
26266
- detail: `${label}: ${path14} (writable)`
27592
+ detail: `${label}: ${path15} (writable)`
26267
27593
  };
26268
27594
  } catch (error2) {
26269
27595
  if (!isMissingPathError(error2)) {
26270
27596
  return {
26271
27597
  ok: false,
26272
- detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
27598
+ detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
26273
27599
  };
26274
27600
  }
26275
- const parentCheck = await checkCreatableAncestorDirectory(path14, probe, platform);
27601
+ const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
26276
27602
  if (!parentCheck.ok) {
26277
27603
  return {
26278
27604
  ok: false,
26279
- detail: `${label}: ${path14} (parent not writable: ${parentCheck.blockingPath})`
27605
+ detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
26280
27606
  };
26281
27607
  }
26282
27608
  return {
26283
27609
  ok: true,
26284
- detail: `${label}: ${path14} (creatable via ${parentCheck.creatableFrom})`
27610
+ detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
26285
27611
  };
26286
27612
  }
26287
27613
  }
26288
- async function checkFileCreatable(label, path14, probe, platform) {
27614
+ async function checkFileCreatable(label, path15, probe, platform) {
26289
27615
  try {
26290
- const stats = await probe.stat(path14);
27616
+ const stats = await probe.stat(path15);
26291
27617
  if (stats.isDirectory()) {
26292
27618
  return {
26293
27619
  ok: false,
26294
- detail: `${label}: ${path14} (exists but is a directory)`
27620
+ detail: `${label}: ${path15} (exists but is a directory)`
26295
27621
  };
26296
27622
  }
26297
- await probe.access(path14, constants.W_OK);
27623
+ await probe.access(path15, constants.W_OK);
26298
27624
  return {
26299
27625
  ok: true,
26300
- detail: `${label}: ${path14} (writable)`
27626
+ detail: `${label}: ${path15} (writable)`
26301
27627
  };
26302
27628
  } catch (error2) {
26303
27629
  if (!isMissingPathError(error2)) {
26304
27630
  return {
26305
27631
  ok: false,
26306
- detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
27632
+ detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
26307
27633
  };
26308
27634
  }
26309
- const parentCheck = await checkCreatableAncestorDirectory(dirname14(path14), probe, platform);
27635
+ const parentCheck = await checkCreatableAncestorDirectory(dirname14(path15), probe, platform);
26310
27636
  if (!parentCheck.ok) {
26311
27637
  return {
26312
27638
  ok: false,
26313
- detail: `${label}: ${path14} (parent not writable: ${parentCheck.blockingPath})`
27639
+ detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
26314
27640
  };
26315
27641
  }
26316
27642
  return {
26317
27643
  ok: true,
26318
- detail: `${label}: ${path14} (creatable via ${parentCheck.creatableFrom})`
27644
+ detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
26319
27645
  };
26320
27646
  }
26321
27647
  }
26322
- async function checkCreatableAncestorDirectory(path14, probe, platform) {
27648
+ async function checkCreatableAncestorDirectory(path15, probe, platform) {
26323
27649
  try {
26324
- const stats = await probe.stat(path14);
27650
+ const stats = await probe.stat(path15);
26325
27651
  if (!stats.isDirectory()) {
26326
27652
  return {
26327
27653
  ok: false,
26328
- creatableFrom: path14,
26329
- blockingPath: path14
27654
+ creatableFrom: path15,
27655
+ blockingPath: path15
26330
27656
  };
26331
27657
  }
26332
- await probe.access(path14, directoryAccessMode(platform));
27658
+ await probe.access(path15, directoryAccessMode(platform));
26333
27659
  return {
26334
27660
  ok: true,
26335
- creatableFrom: path14
27661
+ creatableFrom: path15
26336
27662
  };
26337
27663
  } catch (error2) {
26338
27664
  if (!isMissingPathError(error2)) {
26339
27665
  return {
26340
27666
  ok: false,
26341
- creatableFrom: path14,
26342
- blockingPath: path14
27667
+ creatableFrom: path15,
27668
+ blockingPath: path15
26343
27669
  };
26344
27670
  }
26345
- const parent = dirname14(path14);
26346
- if (parent === path14) {
27671
+ const parent = dirname14(path15);
27672
+ if (parent === path15) {
26347
27673
  return {
26348
27674
  ok: false,
26349
- creatableFrom: path14,
26350
- blockingPath: path14
27675
+ creatableFrom: path15,
27676
+ blockingPath: path15
26351
27677
  };
26352
27678
  }
26353
27679
  const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
@@ -26932,7 +28258,7 @@ import { fileURLToPath as fileURLToPath6 } from "node:url";
26932
28258
 
26933
28259
  // src/daemon/daemon-runtime.ts
26934
28260
  init_daemon_status();
26935
- import { mkdir as mkdir5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
28261
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
26936
28262
  import { dirname as dirname5 } from "node:path";
26937
28263
 
26938
28264
  class DaemonRuntime {
@@ -26960,7 +28286,7 @@ class DaemonRuntime {
26960
28286
  stderr_log: this.paths.stderrLog
26961
28287
  };
26962
28288
  await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
26963
- await writeFile4(this.paths.pidFile, `${this.options.pid}
28289
+ await writeFile3(this.paths.pidFile, `${this.options.pid}
26964
28290
  `);
26965
28291
  await this.statusStore.save(this.currentStatus);
26966
28292
  }
@@ -39393,7 +40719,8 @@ function buildWeacpxMcpToolRegistry(input) {
39393
40719
  task: exports_external.string().min(1),
39394
40720
  workingDirectory: exports_external.string().min(1).optional(),
39395
40721
  role: exports_external.string().min(1).optional(),
39396
- groupId: exports_external.string().min(1).optional()
40722
+ groupId: exports_external.string().min(1).optional(),
40723
+ parallel: exports_external.boolean().describe("Set to true to run this task in its own ephemeral session, concurrently with other in-flight tasks for the same agent.").optional()
39397
40724
  }).strict(),
39398
40725
  handler: async (args) => await asToolResult(async () => {
39399
40726
  const input2 = args;
@@ -39414,7 +40741,8 @@ function buildWeacpxMcpToolRegistry(input) {
39414
40741
  targetAgent: exports_external.string().min(1),
39415
40742
  task: exports_external.string().min(1),
39416
40743
  workingDirectory: exports_external.string().min(1).optional(),
39417
- role: exports_external.string().min(1).optional()
40744
+ role: exports_external.string().min(1).optional(),
40745
+ parallel: exports_external.boolean().describe("Set to true to run this task in its own ephemeral session, concurrently with other in-flight tasks for the same agent.").optional()
39418
40746
  }).strict()).min(1)
39419
40747
  }).strict(),
39420
40748
  handler: async (args) => await asToolResult(async () => {
@@ -39433,7 +40761,8 @@ function buildWeacpxMcpToolRegistry(input) {
39433
40761
  task: entry.task,
39434
40762
  ...entry.workingDirectory ? { workingDirectory: entry.workingDirectory } : {},
39435
40763
  ...entry.role ? { role: entry.role } : {},
39436
- ...groupId ? { groupId } : {}
40764
+ ...groupId ? { groupId } : {},
40765
+ ...entry.parallel !== undefined ? { parallel: entry.parallel } : {}
39437
40766
  });
39438
40767
  results.push({ index, taskId: result.taskId, status: result.status });
39439
40768
  } catch (error2) {
@@ -39657,7 +40986,7 @@ function createErrorResult(message) {
39657
40986
  };
39658
40987
  }
39659
40988
  function renderDelegateSuccess(result) {
39660
- const next = result.status === "needs_confirmation" ? `Next: this delegation requires user approval. Tell the user, then call task_approve or task_cancel based on their response.` : `Next: task "${result.taskId}" is running. Return this taskId to the user, call task_get/task_list for non-blocking progress snapshots, or task_watch to long-poll for the next event or terminal state.`;
40989
+ const next = result.status === "needs_confirmation" ? `Next: this delegation requires user approval. Tell the user, then call task_approve or task_cancel based on their response.` : result.status === "queued" ? `Next: task "${result.taskId}" is queued (agent at parallel capacity). It will start automatically when a slot frees. Call task_watch to long-poll for the transition to running, or task_get/task_list for non-blocking snapshots.` : `Next: task "${result.taskId}" is running. Return this taskId to the user, call task_get/task_list for non-blocking progress snapshots, or task_watch to long-poll for the next event or terminal state.`;
39661
40990
  return [`Delegation task "${result.taskId}" created.`, `- Status: ${result.status}`, next].join(`
39662
40991
  `);
39663
40992
  }
@@ -39928,7 +41257,8 @@ function createOrchestrationTransport(endpoint, deps = {}) {
39928
41257
  task: input.task,
39929
41258
  ...input.workingDirectory !== undefined ? { cwd: input.workingDirectory } : {},
39930
41259
  ...input.role !== undefined ? { role: input.role } : {},
39931
- ...input.groupId !== undefined ? { groupId: input.groupId } : {}
41260
+ ...input.groupId !== undefined ? { groupId: input.groupId } : {},
41261
+ ...input.parallel !== undefined ? { parallel: input.parallel } : {}
39932
41262
  }),
39933
41263
  createGroup: async (input) => await client.createGroup(input),
39934
41264
  getTask: async (input) => await client.getTaskForCoordinator(input),
@@ -40641,10 +41971,12 @@ function parseSourceHandle(args, env = process.env) {
40641
41971
 
40642
41972
  // src/cli.ts
40643
41973
  init_workspace_path();
41974
+ init_workspace_name();
40644
41975
  init_state_store();
40645
41976
 
40646
41977
  // src/onboarding.ts
40647
41978
  init_workspace_path();
41979
+ init_workspace_name();
40648
41980
  init_agent_templates();
40649
41981
  function isFirstUse(config2, state) {
40650
41982
  return Object.keys(state.sessions ?? {}).length === 0 && Object.keys(config2.workspaces ?? {}).length === 0 && (config2.plugins ?? []).length === 0;
@@ -40655,7 +41987,7 @@ async function maybeRunFirstUseOnboarding(input) {
40655
41987
  if (!input.deps.isInteractive())
40656
41988
  return { created: false };
40657
41989
  const cwd = normalizeWorkspacePath(input.deps.cwd());
40658
- const workspaceName = allocateName(sanitizeName(basenameForWorkspacePath(cwd), "workspace"), input.config.workspaces);
41990
+ const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), input.config.workspaces);
40659
41991
  const yes = (await input.deps.promptText(`检测到首次使用 weacpx。是否将当前目录创建为工作区「${workspaceName}」?[Y/n] `)).trim().toLowerCase();
40660
41992
  if (yes === "n" || yes === "no")
40661
41993
  return { created: false };
@@ -40696,18 +42028,6 @@ function resolveTemplateChoice(answer, names) {
40696
42028
  return names[index - 1];
40697
42029
  return names.includes(answer) ? answer : null;
40698
42030
  }
40699
- function allocateName(base, existing) {
40700
- if (!existing[base])
40701
- return base;
40702
- let suffix = 2;
40703
- while (existing[`${base}-${suffix}`])
40704
- suffix += 1;
40705
- return `${base}-${suffix}`;
40706
- }
40707
- function sanitizeName(input, fallback) {
40708
- const sanitized = input.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
40709
- return sanitized || fallback;
40710
- }
40711
42031
 
40712
42032
  // src/cli-update.ts
40713
42033
  init_plugin_home();
@@ -42371,7 +43691,7 @@ var HELP_LINES = [
42371
43691
  "weacpx doctor - 运行诊断",
42372
43692
  "weacpx version - 查看版本",
42373
43693
  "weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
42374
- "weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
43694
+ "weacpx workspace list|add [name] [--raw]|rm <name> - 管理本机工作区(别名:ws)",
42375
43695
  "weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
42376
43696
  ];
42377
43697
  function getUsageText() {
@@ -42635,10 +43955,23 @@ async function handleWorkspaceCli(args, deps) {
42635
43955
  if (args.length !== 1)
42636
43956
  return null;
42637
43957
  return await workspaceList(deps.print);
42638
- case "add":
42639
- if (args.length > 2)
42640
- return null;
42641
- return await workspaceAdd(args[1], deps);
43958
+ case "add": {
43959
+ const rest = args.slice(1);
43960
+ let rawFlag = false;
43961
+ let explicit;
43962
+ for (const token of rest) {
43963
+ if (token === "--raw") {
43964
+ if (rawFlag)
43965
+ return null;
43966
+ rawFlag = true;
43967
+ continue;
43968
+ }
43969
+ if (explicit !== undefined)
43970
+ return null;
43971
+ explicit = token;
43972
+ }
43973
+ return await workspaceAdd(explicit, { ...deps, raw: rawFlag });
43974
+ }
42642
43975
  case "rm":
42643
43976
  if (args.length !== 2 || !args[1])
42644
43977
  return null;
@@ -42663,13 +43996,20 @@ async function workspaceList(print) {
42663
43996
  }
42664
43997
  async function workspaceAdd(rawName, deps) {
42665
43998
  const cwd = normalizeWorkspacePath(deps.cwd());
42666
- const name = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
42667
- if (name.trim().length === 0) {
43999
+ const input = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
44000
+ if (input.length === 0) {
42668
44001
  deps.print("工作区名称不能为空。");
42669
44002
  return 1;
42670
44003
  }
42671
44004
  const store = await createCliConfigStore();
42672
44005
  const config2 = await store.load();
44006
+ let name = input;
44007
+ if (!deps.raw && !isWorkspaceNameValid(input)) {
44008
+ const base = sanitizeWorkspaceName(input);
44009
+ name = allocateWorkspaceName(base, config2.workspaces);
44010
+ const sourceLabel = rawName === undefined ? "目录名" : "名称";
44011
+ deps.print(`${sourceLabel} ${JSON.stringify(input)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`);
44012
+ }
42673
44013
  const existing = config2.workspaces[name];
42674
44014
  if (existing) {
42675
44015
  if (sameWorkspacePath(existing.cwd, cwd)) {
@@ -42677,7 +44017,7 @@ async function workspaceAdd(rawName, deps) {
42677
44017
  return 0;
42678
44018
  }
42679
44019
  deps.print(`工作区「${name}」已存在,但路径不同:${existing.cwd}`);
42680
- deps.print(`请换一个名称,或先执行:weacpx workspace rm ${name}`);
44020
+ deps.print(`请换一个名称,或先执行:weacpx workspace rm ${quoteWorkspaceNameIfNeeded(name)}`);
42681
44021
  return 1;
42682
44022
  }
42683
44023
  await store.upsertWorkspace(name, cwd);
@@ -42830,7 +44170,12 @@ async function defaultRun(options = {}) {
42830
44170
  await ensureConfigExists(runtimePaths.configPath);
42831
44171
  const config2 = await loadConfig(runtimePaths.configPath);
42832
44172
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
42833
- await loadConfiguredPlugins2({ plugins: config2.plugins });
44173
+ await loadConfiguredPlugins2({
44174
+ plugins: config2.plugins,
44175
+ onPluginError: ({ name, error: error2 }) => {
44176
+ console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
44177
+ }
44178
+ });
42834
44179
  const { createMessageChannels: createMessageChannels2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
42835
44180
  const { MessageChannelRegistry: MessageChannelRegistry2 } = await Promise.resolve().then(() => (init_channel_registry(), exports_channel_registry));
42836
44181
  const daemonPaths = resolveDaemonPathsForCurrentConfig();