weacpx 0.4.8 → 0.4.9

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
@@ -2424,7 +2424,8 @@ function parseOrchestrationConfig(raw) {
2424
2424
  allowWorkerChainedRequests: raw.allowWorkerChainedRequests === true,
2425
2425
  allowedAgentRequestTargets: Array.isArray(raw.allowedAgentRequestTargets) ? raw.allowedAgentRequestTargets.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestTargets],
2426
2426
  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
2427
+ progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds,
2428
+ maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
2428
2429
  };
2429
2430
  }
2430
2431
  var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
@@ -2452,7 +2453,8 @@ var init_load_config = __esm(() => {
2452
2453
  allowWorkerChainedRequests: false,
2453
2454
  allowedAgentRequestTargets: [],
2454
2455
  allowedAgentRequestRoles: [],
2455
- progressHeartbeatSeconds: 300
2456
+ progressHeartbeatSeconds: 300,
2457
+ maxParallelTasksPerAgent: 3
2456
2458
  };
2457
2459
  });
2458
2460
 
@@ -9645,6 +9647,34 @@ var init_quota_errors = __esm(() => {
9645
9647
  };
9646
9648
  });
9647
9649
 
9650
+ // src/commands/workspace-name.ts
9651
+ function sanitizeWorkspaceName(input, fallback = "workspace") {
9652
+ const sanitized = input.trim().replace(UNSAFE_RUN_RE, "-").replace(TRIM_DASHES_RE, "");
9653
+ return sanitized.length > 0 ? sanitized : fallback;
9654
+ }
9655
+ function allocateWorkspaceName(base, existing) {
9656
+ if (!Object.prototype.hasOwnProperty.call(existing, base))
9657
+ return base;
9658
+ let suffix = 2;
9659
+ while (Object.prototype.hasOwnProperty.call(existing, `${base}-${suffix}`))
9660
+ suffix += 1;
9661
+ return `${base}-${suffix}`;
9662
+ }
9663
+ function isWorkspaceNameValid(input) {
9664
+ return VALID_WORKSPACE_NAME_RE.test(input);
9665
+ }
9666
+ function quoteWorkspaceNameIfNeeded(input) {
9667
+ if (isWorkspaceNameValid(input))
9668
+ return input;
9669
+ return `"${input.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
9670
+ }
9671
+ var VALID_WORKSPACE_NAME_RE, UNSAFE_RUN_RE, TRIM_DASHES_RE;
9672
+ var init_workspace_name = __esm(() => {
9673
+ VALID_WORKSPACE_NAME_RE = /^[a-zA-Z0-9._-]+$/;
9674
+ UNSAFE_RUN_RE = /[^a-zA-Z0-9._-]+/g;
9675
+ TRIM_DASHES_RE = /^-+|-+$/g;
9676
+ });
9677
+
9648
9678
  // src/orchestration/orchestration-types.ts
9649
9679
  function createEmptyOrchestrationState() {
9650
9680
  return {
@@ -9683,7 +9713,7 @@ function isOptionalBoolean(value) {
9683
9713
  return value === undefined || typeof value === "boolean";
9684
9714
  }
9685
9715
  function isTaskStatus(value) {
9686
- return value === "needs_confirmation" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
9716
+ return value === "needs_confirmation" || value === "queued" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
9687
9717
  }
9688
9718
  function isSourceKind(value) {
9689
9719
  return value === "human" || value === "coordinator" || value === "worker";
@@ -9719,7 +9749,7 @@ function isTaskRecord(value) {
9719
9749
  if (!isRecord2(value)) {
9720
9750
  return false;
9721
9751
  }
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));
9752
+ 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
9753
  }
9724
9754
  function isExternalCoordinatorRecord(value) {
9725
9755
  if (!isRecord2(value)) {
@@ -9731,7 +9761,7 @@ function isWorkerBindingRecord(value) {
9731
9761
  if (!isRecord2(value)) {
9732
9762
  return false;
9733
9763
  }
9734
- return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role);
9764
+ return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isOptionalBoolean(value.ephemeral);
9735
9765
  }
9736
9766
  function isGroupRecord(value) {
9737
9767
  if (!isRecord2(value)) {
@@ -9987,12 +10017,25 @@ var init_state_store = __esm(() => {
9987
10017
  import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
9988
10018
  import { homedir as homedir3 } from "node:os";
9989
10019
  import { join as join3 } from "node:path";
10020
+ function coerceMissing(value) {
10021
+ if (value === undefined)
10022
+ return;
10023
+ const trimmed = value.trim();
10024
+ if (!trimmed)
10025
+ return;
10026
+ const lower = trimmed.toLowerCase();
10027
+ if (lower === "undefined" || lower === "null")
10028
+ return;
10029
+ return trimmed;
10030
+ }
9990
10031
  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();
10032
+ const explicit = coerceMissing(input.pluginHome);
10033
+ if (explicit)
10034
+ return explicit;
10035
+ const envOverride = coerceMissing(process.env.WEACPX_PLUGIN_HOME);
10036
+ if (envOverride)
10037
+ return envOverride;
10038
+ const home = coerceMissing(input.home) ?? coerceMissing(process.env.HOME) ?? homedir3();
9996
10039
  return join3(home, ".weacpx", "plugins");
9997
10040
  }
9998
10041
  async function ensurePluginHome(pluginHome) {
@@ -10202,6 +10245,28 @@ function loadConfigRouteTag(accountId) {
10202
10245
  return;
10203
10246
  }
10204
10247
  }
10248
+ function loadConfigBotAgent(accountId) {
10249
+ try {
10250
+ const configPath = resolveConfigPath();
10251
+ if (!fs3.existsSync(configPath))
10252
+ return;
10253
+ const raw = fs3.readFileSync(configPath, "utf-8");
10254
+ const cfg = JSON.parse(raw);
10255
+ const channels = cfg.channels;
10256
+ const section = channels?.["openclaw-weixin"];
10257
+ if (!section)
10258
+ return;
10259
+ if (accountId) {
10260
+ const accounts = section.accounts;
10261
+ const agent = accounts?.[accountId]?.botAgent;
10262
+ if (typeof agent === "string" && agent.trim())
10263
+ return agent.trim();
10264
+ }
10265
+ return typeof section.botAgent === "string" && section.botAgent.trim() ? section.botAgent.trim() : undefined;
10266
+ } catch {
10267
+ return;
10268
+ }
10269
+ }
10205
10270
  function listWeixinAccountIds() {
10206
10271
  const indexed = listIndexedWeixinAccountIds();
10207
10272
  if (indexed.length > 0)
@@ -10490,8 +10555,83 @@ var init_send_errors = __esm(() => {
10490
10555
 
10491
10556
  // src/weixin/api/api.ts
10492
10557
  import crypto from "node:crypto";
10558
+ function buildClientVersion(version2) {
10559
+ const parts = version2.split(".").map((p) => parseInt(p, 10));
10560
+ const major = parts[0] ?? 0;
10561
+ const minor = parts[1] ?? 0;
10562
+ const patch = parts[2] ?? 0;
10563
+ return (major & 255) << 16 | (minor & 255) << 8 | patch & 255;
10564
+ }
10565
+ function sanitizeBotAgent(raw) {
10566
+ if (!raw || typeof raw !== "string")
10567
+ return DEFAULT_BOT_AGENT;
10568
+ const trimmed = raw.trim();
10569
+ if (!trimmed)
10570
+ return DEFAULT_BOT_AGENT;
10571
+ const productRe = /^[A-Za-z0-9_.\-]{1,32}\/[A-Za-z0-9_.+\-]{1,32}$/;
10572
+ const commentCharRe = /^[\x20-\x27\x2A-\x7E]{1,64}$/;
10573
+ const rawTokens = trimmed.split(/\s+/);
10574
+ const tokens = [];
10575
+ for (let i = 0;i < rawTokens.length; i += 1) {
10576
+ const tok = rawTokens[i];
10577
+ if (tok.startsWith("(") && !tok.endsWith(")")) {
10578
+ let acc = tok;
10579
+ while (i + 1 < rawTokens.length && !acc.endsWith(")")) {
10580
+ i += 1;
10581
+ acc += " " + rawTokens[i];
10582
+ }
10583
+ tokens.push(acc);
10584
+ } else {
10585
+ tokens.push(tok);
10586
+ }
10587
+ }
10588
+ const accepted = [];
10589
+ let pendingProduct = null;
10590
+ for (const tok of tokens) {
10591
+ if (tok.startsWith("(") && tok.endsWith(")")) {
10592
+ const inner = tok.slice(1, -1);
10593
+ if (pendingProduct && commentCharRe.test(inner)) {
10594
+ accepted.push(`${pendingProduct} (${inner})`);
10595
+ pendingProduct = null;
10596
+ } else {
10597
+ if (pendingProduct) {
10598
+ accepted.push(pendingProduct);
10599
+ pendingProduct = null;
10600
+ }
10601
+ }
10602
+ continue;
10603
+ }
10604
+ if (pendingProduct) {
10605
+ accepted.push(pendingProduct);
10606
+ pendingProduct = null;
10607
+ }
10608
+ if (productRe.test(tok)) {
10609
+ pendingProduct = tok;
10610
+ }
10611
+ }
10612
+ if (pendingProduct)
10613
+ accepted.push(pendingProduct);
10614
+ if (accepted.length === 0)
10615
+ return DEFAULT_BOT_AGENT;
10616
+ const joined = accepted.join(" ");
10617
+ if (Buffer.byteLength(joined, "utf-8") <= BOT_AGENT_MAX_LEN)
10618
+ return joined;
10619
+ const truncated = [];
10620
+ let len = 0;
10621
+ for (const t of accepted) {
10622
+ const add = (truncated.length === 0 ? 0 : 1) + Buffer.byteLength(t, "utf-8");
10623
+ if (len + add > BOT_AGENT_MAX_LEN)
10624
+ break;
10625
+ truncated.push(t);
10626
+ len += add;
10627
+ }
10628
+ return truncated.length > 0 ? truncated.join(" ") : DEFAULT_BOT_AGENT;
10629
+ }
10493
10630
  function buildBaseInfo() {
10494
- return { channel_version: CHANNEL_VERSION };
10631
+ return {
10632
+ channel_version: CHANNEL_VERSION,
10633
+ bot_agent: sanitizeBotAgent(loadConfigBotAgent())
10634
+ };
10495
10635
  }
10496
10636
  function ensureTrailingSlash(url) {
10497
10637
  return url.endsWith("/") ? url : `${url}/`;
@@ -10502,6 +10642,9 @@ function randomWechatUin() {
10502
10642
  }
10503
10643
  function buildCommonHeaders() {
10504
10644
  const headers = {};
10645
+ if (ILINK_APP_ID)
10646
+ headers["iLink-App-Id"] = ILINK_APP_ID;
10647
+ headers["iLink-App-ClientVersion"] = String(ILINK_APP_CLIENT_VERSION);
10505
10648
  const routeTag = loadConfigRouteTag();
10506
10649
  if (routeTag) {
10507
10650
  headers.SKRouteTag = routeTag;
@@ -10547,6 +10690,35 @@ async function apiGetFetch(params) {
10547
10690
  throw err;
10548
10691
  }
10549
10692
  }
10693
+ async function apiPostFetch(params) {
10694
+ const base = ensureTrailingSlash(params.baseUrl);
10695
+ const url = new URL(params.endpoint, base);
10696
+ const hdrs = buildCommonHeaders();
10697
+ hdrs["Content-Type"] = "application/json";
10698
+ logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
10699
+ const controller = new AbortController;
10700
+ const t = params.timeoutMs !== undefined ? setTimeout(() => controller.abort(), params.timeoutMs) : undefined;
10701
+ const onAbort = () => controller.abort();
10702
+ params.abortSignal?.addEventListener("abort", onAbort, { once: true });
10703
+ try {
10704
+ const res = await fetch(url.toString(), {
10705
+ method: "POST",
10706
+ headers: hdrs,
10707
+ body: params.body,
10708
+ signal: controller.signal
10709
+ });
10710
+ const rawText = await res.text();
10711
+ logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
10712
+ if (!res.ok) {
10713
+ throw new Error(`${params.label} ${res.status}: ${rawText}`);
10714
+ }
10715
+ return rawText;
10716
+ } finally {
10717
+ if (t !== undefined)
10718
+ clearTimeout(t);
10719
+ params.abortSignal?.removeEventListener("abort", onAbort);
10720
+ }
10721
+ }
10550
10722
  async function apiFetch(params) {
10551
10723
  const base = ensureTrailingSlash(params.baseUrl);
10552
10724
  const url = new URL(params.endpoint, base);
@@ -10705,7 +10877,7 @@ async function sendTyping(params) {
10705
10877
  label: "sendTyping"
10706
10878
  });
10707
10879
  }
10708
- var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
10880
+ 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
10881
  var init_api = __esm(() => {
10710
10882
  init_version();
10711
10883
  init_accounts();
@@ -10713,6 +10885,8 @@ var init_api = __esm(() => {
10713
10885
  init_redact();
10714
10886
  init_send_errors();
10715
10887
  CHANNEL_VERSION = readVersion();
10888
+ ILINK_APP_CLIENT_VERSION = buildClientVersion(CHANNEL_VERSION);
10889
+ ILINK_APP_ID = (process.env.WEACPX_ILINK_APP_ID ?? "").trim();
10716
10890
  });
10717
10891
 
10718
10892
  // node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -11744,22 +11918,47 @@ function purgeExpiredLogins() {
11744
11918
  }
11745
11919
  }
11746
11920
  }
11921
+ function getLocalBotTokenList() {
11922
+ const accountIds = listIndexedWeixinAccountIds();
11923
+ const tokens = [];
11924
+ for (let i = accountIds.length - 1;i >= 0 && tokens.length < 10; i--) {
11925
+ const accountId = accountIds[i];
11926
+ if (!accountId)
11927
+ continue;
11928
+ const data = loadWeixinAccount(accountId);
11929
+ const token = data?.token?.trim();
11930
+ if (token) {
11931
+ tokens.push(token);
11932
+ }
11933
+ }
11934
+ return tokens;
11935
+ }
11747
11936
  async function fetchQRCode(apiBaseUrl, botType) {
11748
11937
  logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
11749
- const rawText = await apiGetFetch({
11938
+ const localTokenList = getLocalBotTokenList();
11939
+ logger.info(`fetchQRCode: local_token_list count=${localTokenList.length}`);
11940
+ const rawText = await apiPostFetch({
11750
11941
  baseUrl: apiBaseUrl,
11751
11942
  endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
11752
- timeoutMs: GET_QRCODE_TIMEOUT_MS,
11943
+ body: JSON.stringify({ local_token_list: localTokenList }),
11753
11944
  label: "fetchQRCode"
11754
11945
  });
11755
11946
  return JSON.parse(rawText);
11756
11947
  }
11757
- async function pollQRStatus(apiBaseUrl, qrcode) {
11758
- logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=***`);
11948
+ function buildPollQRStatusEndpoint(qrcode, verifyCode) {
11949
+ let endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
11950
+ if (verifyCode) {
11951
+ endpoint += `&verify_code=${encodeURIComponent(verifyCode)}`;
11952
+ }
11953
+ return endpoint;
11954
+ }
11955
+ async function pollQRStatus(apiBaseUrl, qrcode, verifyCode) {
11956
+ logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=*** hasVerifyCode=${Boolean(verifyCode)}`);
11957
+ const endpoint = buildPollQRStatusEndpoint(qrcode, verifyCode);
11759
11958
  try {
11760
11959
  const rawText = await apiGetFetch({
11761
11960
  baseUrl: apiBaseUrl,
11762
- endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
11961
+ endpoint,
11763
11962
  timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
11764
11963
  label: "pollQRStatus"
11765
11964
  });
@@ -11774,6 +11973,28 @@ async function pollQRStatus(apiBaseUrl, qrcode) {
11774
11973
  return { status: "wait" };
11775
11974
  }
11776
11975
  }
11976
+ async function readVerifyCodeFromStdin(prompt) {
11977
+ if (!process.stdin.isTTY) {
11978
+ throw new Error("verify code requested but stdin is not a TTY (running in daemon mode?)");
11979
+ }
11980
+ process.stdout.write(prompt);
11981
+ return new Promise((resolve) => {
11982
+ let input = "";
11983
+ const onData = (chunk) => {
11984
+ const str = chunk.toString();
11985
+ input += str;
11986
+ if (input.includes(`
11987
+ `)) {
11988
+ process.stdin.removeListener("data", onData);
11989
+ process.stdin.pause();
11990
+ resolve(input.trim());
11991
+ }
11992
+ };
11993
+ process.stdin.resume();
11994
+ process.stdin.setEncoding("utf-8");
11995
+ process.stdin.on("data", onData);
11996
+ });
11997
+ }
11777
11998
  async function startWeixinLoginWithQr(opts) {
11778
11999
  const sessionKey = opts.accountId || randomUUID2();
11779
12000
  purgeExpiredLogins();
@@ -11812,6 +12033,36 @@ async function startWeixinLoginWithQr(opts) {
11812
12033
  };
11813
12034
  }
11814
12035
  }
12036
+ async function refreshQRCode(activeLogin, botType, qrRefreshCount, onScannedReset) {
12037
+ try {
12038
+ const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
12039
+ activeLogin.qrcode = qrResponse.qrcode;
12040
+ activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
12041
+ activeLogin.startedAt = Date.now();
12042
+ onScannedReset();
12043
+ logger.info(`refreshQRCode: new QR code obtained (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT}) qrcode=${redactToken(qrResponse.qrcode)}`);
12044
+ process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
12045
+
12046
+ `);
12047
+ try {
12048
+ const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
12049
+ qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
12050
+ process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
12051
+ `);
12052
+ process.stdout.write(`${qrResponse.qrcode_img_content}
12053
+ `);
12054
+ } catch {
12055
+ process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
12056
+ `);
12057
+ process.stdout.write(`${qrResponse.qrcode_img_content}
12058
+ `);
12059
+ }
12060
+ return { success: true };
12061
+ } catch (refreshErr) {
12062
+ logger.error(`refreshQRCode: failed to refresh QR code: ${String(refreshErr)}`);
12063
+ return { success: false, message: `刷新二维码失败: ${String(refreshErr)}` };
12064
+ }
12065
+ }
11815
12066
  async function waitForWeixinLogin(opts) {
11816
12067
  let activeLogin = activeLogins.get(opts.sessionKey);
11817
12068
  if (!activeLogin) {
@@ -11838,7 +12089,7 @@ async function waitForWeixinLogin(opts) {
11838
12089
  while (Date.now() < deadline) {
11839
12090
  try {
11840
12091
  const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
11841
- const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
12092
+ const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode, activeLogin.pendingVerifyCode);
11842
12093
  logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
11843
12094
  activeLogin.status = statusResponse.status;
11844
12095
  switch (statusResponse.status) {
@@ -11848,6 +12099,10 @@ async function waitForWeixinLogin(opts) {
11848
12099
  }
11849
12100
  break;
11850
12101
  case "scaned":
12102
+ if (activeLogin.pendingVerifyCode) {
12103
+ logger.info("verify code accepted, resuming polling");
12104
+ activeLogin.pendingVerifyCode = undefined;
12105
+ }
11851
12106
  if (!scannedPrinted) {
11852
12107
  process.stdout.write(`
11853
12108
  \uD83D\uDC40 已扫码,在微信继续操作...
@@ -11856,6 +12111,7 @@ async function waitForWeixinLogin(opts) {
11856
12111
  }
11857
12112
  break;
11858
12113
  case "expired": {
12114
+ activeLogin.pendingVerifyCode = undefined;
11859
12115
  qrRefreshCount++;
11860
12116
  if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
11861
12117
  logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
@@ -11869,36 +12125,14 @@ async function waitForWeixinLogin(opts) {
11869
12125
  ⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
11870
12126
  `);
11871
12127
  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();
12128
+ const expiredRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
11878
12129
  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)}`);
12130
+ });
12131
+ if (!expiredRefreshResult.success) {
11898
12132
  activeLogins.delete(opts.sessionKey);
11899
12133
  return {
11900
12134
  connected: false,
11901
- message: `刷新二维码失败: ${String(refreshErr)}`
12135
+ message: expiredRefreshResult.message
11902
12136
  };
11903
12137
  }
11904
12138
  break;
@@ -11914,6 +12148,49 @@ async function waitForWeixinLogin(opts) {
11914
12148
  }
11915
12149
  break;
11916
12150
  }
12151
+ case "need_verifycode": {
12152
+ const verifyPrompt = activeLogin.pendingVerifyCode ? "❌ 你输入的数字不匹配,请重新输入:" : "输入手机微信显示的数字,以继续连接:";
12153
+ let code;
12154
+ try {
12155
+ code = await readVerifyCodeFromStdin(verifyPrompt);
12156
+ } catch (err) {
12157
+ logger.error(`waitForWeixinLogin: cannot read verify code (no TTY): ${String(err)}`);
12158
+ activeLogins.delete(opts.sessionKey);
12159
+ return {
12160
+ connected: false,
12161
+ message: "需要输入配对码,但当前环境没有交互式终端。请在前台运行 `weacpx login` 完成登录。"
12162
+ };
12163
+ }
12164
+ activeLogin.pendingVerifyCode = code;
12165
+ continue;
12166
+ }
12167
+ case "verify_code_blocked": {
12168
+ logger.warn(`waitForWeixinLogin: verify code blocked, qrRefreshCount=${qrRefreshCount} sessionKey=${opts.sessionKey}`);
12169
+ process.stdout.write(`
12170
+ ⛔ 多次输入错误,请稍后再试。
12171
+ `);
12172
+ activeLogin.pendingVerifyCode = undefined;
12173
+ qrRefreshCount++;
12174
+ if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
12175
+ logger.warn(`waitForWeixinLogin: verify_code_blocked and QR refresh limit reached, giving up sessionKey=${opts.sessionKey}`);
12176
+ activeLogins.delete(opts.sessionKey);
12177
+ return {
12178
+ connected: false,
12179
+ message: "多次输入错误,连接流程已停止。请稍后再试。"
12180
+ };
12181
+ }
12182
+ const blockedRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
12183
+ scannedPrinted = false;
12184
+ });
12185
+ if (!blockedRefreshResult.success) {
12186
+ activeLogins.delete(opts.sessionKey);
12187
+ return {
12188
+ connected: false,
12189
+ message: blockedRefreshResult.message
12190
+ };
12191
+ }
12192
+ break;
12193
+ }
11917
12194
  case "confirmed": {
11918
12195
  if (!statusResponse.ilink_bot_id) {
11919
12196
  activeLogins.delete(opts.sessionKey);
@@ -11953,140 +12230,22 @@ async function waitForWeixinLogin(opts) {
11953
12230
  message: "登录超时,请重试。"
11954
12231
  };
11955
12232
  }
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;
12233
+ 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
12234
  var init_login_qr = __esm(() => {
11958
12235
  init_api();
12236
+ init_accounts();
11959
12237
  init_logger();
11960
12238
  init_redact();
11961
12239
  ACTIVE_LOGIN_TTL_MS = 5 * 60000;
11962
12240
  activeLogins = new Map;
11963
12241
  });
11964
12242
 
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
- };
12243
+ // src/weixin/util/random.ts
12244
+ import crypto2 from "node:crypto";
12245
+ function generateId(prefix) {
12246
+ return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
12089
12247
  }
12248
+ var init_random = () => {};
12090
12249
 
12091
12250
  // src/weixin/api/types.ts
12092
12251
  var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
@@ -12121,9 +12280,272 @@ var init_types2 = __esm(() => {
12121
12280
  };
12122
12281
  });
12123
12282
 
12283
+ // src/weixin/messaging/inbound.ts
12284
+ import fs5 from "node:fs";
12285
+ import path6 from "node:path";
12286
+ function contextTokenKey(accountId, userId) {
12287
+ return `${accountId}:${userId}`;
12288
+ }
12289
+ function resolveContextTokenFilePath(accountId) {
12290
+ return path6.join(resolveStateDir(), "openclaw-weixin", "accounts", `${accountId}.context-tokens.json`);
12291
+ }
12292
+ function persistContextTokens(accountId) {
12293
+ const prefix = `${accountId}:`;
12294
+ const tokens = {};
12295
+ for (const [k, v] of contextTokenStore) {
12296
+ if (k.startsWith(prefix))
12297
+ tokens[k.slice(prefix.length)] = v;
12298
+ }
12299
+ const filePath = resolveContextTokenFilePath(accountId);
12300
+ try {
12301
+ fs5.mkdirSync(path6.dirname(filePath), { recursive: true });
12302
+ fs5.writeFileSync(filePath, JSON.stringify(tokens), "utf-8");
12303
+ } catch (err) {
12304
+ logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
12305
+ }
12306
+ }
12307
+ function restoreContextTokens(accountId) {
12308
+ const filePath = resolveContextTokenFilePath(accountId);
12309
+ try {
12310
+ if (!fs5.existsSync(filePath))
12311
+ return;
12312
+ const raw = fs5.readFileSync(filePath, "utf-8");
12313
+ const tokens = JSON.parse(raw);
12314
+ let count = 0;
12315
+ for (const [userId, token] of Object.entries(tokens)) {
12316
+ if (typeof token === "string" && token) {
12317
+ contextTokenStore.set(contextTokenKey(accountId, userId), token);
12318
+ count++;
12319
+ }
12320
+ }
12321
+ logger.info(`restoreContextTokens: restored ${count} tokens for account=${accountId}`);
12322
+ } catch (err) {
12323
+ logger.warn(`restoreContextTokens: failed to read ${filePath}: ${String(err)}`);
12324
+ }
12325
+ }
12326
+ function clearContextTokensForAccount(accountId) {
12327
+ const prefix = `${accountId}:`;
12328
+ for (const k of [...contextTokenStore.keys()]) {
12329
+ if (k.startsWith(prefix))
12330
+ contextTokenStore.delete(k);
12331
+ }
12332
+ const filePath = resolveContextTokenFilePath(accountId);
12333
+ try {
12334
+ if (fs5.existsSync(filePath))
12335
+ fs5.unlinkSync(filePath);
12336
+ } catch (err) {
12337
+ logger.warn(`clearContextTokensForAccount: failed to remove ${filePath}: ${String(err)}`);
12338
+ }
12339
+ logger.info(`clearContextTokensForAccount: cleared tokens for account=${accountId}`);
12340
+ }
12341
+ function setContextToken(accountId, userId, token) {
12342
+ const k = contextTokenKey(accountId, userId);
12343
+ logger.debug(`setContextToken: key=${k}`);
12344
+ contextTokenStore.set(k, token);
12345
+ persistContextTokens(accountId);
12346
+ }
12347
+ function getContextToken(accountId, userId) {
12348
+ const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
12349
+ const val = contextTokenStore.get(k);
12350
+ logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
12351
+ return val;
12352
+ }
12353
+ function normalizeWeixinUserIdFromChatKey(chatKey) {
12354
+ const parts = chatKey.split(":");
12355
+ if (parts[0] === "weixin" && parts[2]) {
12356
+ return parts.slice(2).join(":");
12357
+ }
12358
+ return chatKey;
12359
+ }
12360
+ function isMediaItem(item) {
12361
+ return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
12362
+ }
12363
+ function bodyFromItemList(itemList) {
12364
+ if (!itemList?.length)
12365
+ return "";
12366
+ for (const item of itemList) {
12367
+ if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
12368
+ const text = String(item.text_item.text);
12369
+ const ref = item.ref_msg;
12370
+ if (!ref)
12371
+ return text;
12372
+ if (ref.message_item && isMediaItem(ref.message_item))
12373
+ return text;
12374
+ const parts = [];
12375
+ if (ref.title)
12376
+ parts.push(ref.title);
12377
+ if (ref.message_item) {
12378
+ const refBody = bodyFromItemList([ref.message_item]);
12379
+ if (refBody)
12380
+ parts.push(refBody);
12381
+ }
12382
+ if (!parts.length)
12383
+ return text;
12384
+ return `[引用: ${parts.join(" | ")}]
12385
+ ${text}`;
12386
+ }
12387
+ if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
12388
+ return item.voice_item.text;
12389
+ }
12390
+ }
12391
+ return "";
12392
+ }
12393
+ function extractWeixinMediaDescriptors(itemList) {
12394
+ const out = [];
12395
+ for (const item of itemList ?? []) {
12396
+ const descriptor = descriptorFromItem(item);
12397
+ if (descriptor)
12398
+ out.push(descriptor);
12399
+ const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
12400
+ const refDescriptor = descriptorFromItem(ref);
12401
+ if (refDescriptor)
12402
+ out.push(refDescriptor);
12403
+ }
12404
+ return out;
12405
+ }
12406
+ function descriptorFromItem(item) {
12407
+ if (!item)
12408
+ return;
12409
+ if (item.type === MessageItemType.IMAGE)
12410
+ return { item, kind: "image" };
12411
+ if (item.type === MessageItemType.VIDEO)
12412
+ return { item, kind: "video" };
12413
+ if (item.type === MessageItemType.FILE)
12414
+ return { item, kind: "file", fileName: item.file_item?.file_name };
12415
+ if (item.type === MessageItemType.VOICE)
12416
+ return { item, kind: "audio" };
12417
+ return;
12418
+ }
12419
+ var contextTokenStore;
12420
+ var init_inbound = __esm(() => {
12421
+ init_logger();
12422
+ init_random();
12423
+ init_types2();
12424
+ init_state_dir();
12425
+ contextTokenStore = new Map;
12426
+ });
12427
+
12428
+ // src/weixin/api/config-cache.ts
12429
+ class WeixinConfigManager {
12430
+ apiOpts;
12431
+ log;
12432
+ cache = new Map;
12433
+ constructor(apiOpts, log) {
12434
+ this.apiOpts = apiOpts;
12435
+ this.log = log;
12436
+ }
12437
+ async getForUser(userId, contextToken) {
12438
+ const now = Date.now();
12439
+ const entry = this.cache.get(userId);
12440
+ const shouldFetch = !entry || now >= entry.nextFetchAt;
12441
+ if (shouldFetch) {
12442
+ let fetchOk = false;
12443
+ try {
12444
+ const resp = await getConfig({
12445
+ baseUrl: this.apiOpts.baseUrl,
12446
+ token: this.apiOpts.token,
12447
+ ilinkUserId: userId,
12448
+ contextToken
12449
+ });
12450
+ if (resp.ret === 0) {
12451
+ this.cache.set(userId, {
12452
+ config: { typingTicket: resp.typing_ticket ?? "" },
12453
+ everSucceeded: true,
12454
+ nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
12455
+ retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
12456
+ });
12457
+ this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
12458
+ fetchOk = true;
12459
+ }
12460
+ } catch (err) {
12461
+ this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
12462
+ }
12463
+ if (!fetchOk) {
12464
+ const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
12465
+ const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
12466
+ if (entry) {
12467
+ entry.nextFetchAt = now + nextDelay;
12468
+ entry.retryDelayMs = nextDelay;
12469
+ } else {
12470
+ this.cache.set(userId, {
12471
+ config: { typingTicket: "" },
12472
+ everSucceeded: false,
12473
+ nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
12474
+ retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
12475
+ });
12476
+ }
12477
+ }
12478
+ }
12479
+ return this.cache.get(userId)?.config ?? { typingTicket: "" };
12480
+ }
12481
+ }
12482
+ var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
12483
+ var init_config_cache = __esm(() => {
12484
+ init_api();
12485
+ CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12486
+ CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
12487
+ });
12488
+
12489
+ // src/weixin/api/session-guard.ts
12490
+ function pauseSession(accountId) {
12491
+ const until = Date.now() + SESSION_PAUSE_DURATION_MS;
12492
+ pauseUntilMap.set(accountId, until);
12493
+ logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
12494
+ }
12495
+ function resetSessionPause(accountId) {
12496
+ pauseUntilMap.delete(accountId);
12497
+ }
12498
+ var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
12499
+ var init_session_guard = __esm(() => {
12500
+ init_logger();
12501
+ SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
12502
+ pauseUntilMap = new Map;
12503
+ });
12504
+
12505
+ // src/weixin/messaging/conversation-executor.ts
12506
+ function createConversationExecutor() {
12507
+ const states = new Map;
12508
+ const getState = (conversationId) => {
12509
+ const existing = states.get(conversationId);
12510
+ if (existing)
12511
+ return existing;
12512
+ const created = { activeControls: 0 };
12513
+ states.set(conversationId, created);
12514
+ return created;
12515
+ };
12516
+ const cleanupState = (conversationId, state) => {
12517
+ if (!state.normalTail && state.activeControls === 0) {
12518
+ states.delete(conversationId);
12519
+ }
12520
+ };
12521
+ return {
12522
+ run(conversationId, lane, task) {
12523
+ const state = getState(conversationId);
12524
+ if (lane === "control") {
12525
+ state.activeControls += 1;
12526
+ return Promise.resolve().then(task).finally(() => {
12527
+ state.activeControls -= 1;
12528
+ cleanupState(conversationId, state);
12529
+ });
12530
+ }
12531
+ const previous = state.normalTail ?? Promise.resolve();
12532
+ const next = previous.catch(() => {
12533
+ return;
12534
+ }).then(task);
12535
+ state.normalTail = next;
12536
+ return next.finally(() => {
12537
+ if (state.normalTail === next) {
12538
+ state.normalTail = undefined;
12539
+ }
12540
+ cleanupState(conversationId, state);
12541
+ });
12542
+ }
12543
+ };
12544
+ }
12545
+
12124
12546
  // src/channels/media-store.ts
12125
12547
  import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile6 } from "node:fs/promises";
12126
- import path6 from "node:path";
12548
+ import path7 from "node:path";
12127
12549
 
12128
12550
  class RuntimeMediaStore {
12129
12551
  rootDir;
@@ -12139,10 +12561,10 @@ class RuntimeMediaStore {
12139
12561
  const safeChatKey = safePathSegment(input.chatKey);
12140
12562
  const safeMessageId = safePathSegment(input.messageId || "message");
12141
12563
  const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
12142
- const dir = path6.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
12564
+ const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
12143
12565
  await mkdir7(dir, { recursive: true });
12144
- const resolvedRoot = path6.resolve(this.rootDir);
12145
- const resolvedFile = path6.resolve(path6.join(dir, await uniqueFileName(dir, baseFileName)));
12566
+ const resolvedRoot = path7.resolve(this.rootDir);
12567
+ const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
12146
12568
  if (!isPathInside(resolvedFile, resolvedRoot)) {
12147
12569
  throw new Error("media path escapes runtime media root");
12148
12570
  }
@@ -12151,7 +12573,7 @@ class RuntimeMediaStore {
12151
12573
  kind: input.kind,
12152
12574
  filePath: resolvedFile,
12153
12575
  mimeType: input.mimeType,
12154
- fileName: path6.basename(resolvedFile),
12576
+ fileName: path7.basename(resolvedFile),
12155
12577
  sizeBytes: input.buffer.byteLength,
12156
12578
  source: {
12157
12579
  channelId: input.channelId,
@@ -12167,10 +12589,10 @@ class RuntimeMediaStore {
12167
12589
  }
12168
12590
  }
12169
12591
  function sanitizeMediaFileName(fileName, mimeType) {
12170
- const base = path6.basename(fileName.trim() || "attachment");
12592
+ const base = path7.basename(fileName.trim() || "attachment");
12171
12593
  const replaced = base.replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
12172
12594
  const safe = replaced || "attachment";
12173
- const ext = path6.extname(safe);
12595
+ const ext = path7.extname(safe);
12174
12596
  if (ext)
12175
12597
  return safe;
12176
12598
  return `${safe}${extensionFromMime(mimeType)}`;
@@ -12180,13 +12602,13 @@ function safePathSegment(value) {
12180
12602
  return safe || "unknown";
12181
12603
  }
12182
12604
  async function uniqueFileName(dir, baseName) {
12183
- const ext = path6.extname(baseName);
12605
+ const ext = path7.extname(baseName);
12184
12606
  const stem = ext ? baseName.slice(0, -ext.length) : baseName;
12185
12607
  let candidate = baseName;
12186
12608
  let counter = 2;
12187
12609
  while (true) {
12188
12610
  try {
12189
- await access2(path6.join(dir, candidate));
12611
+ await access2(path7.join(dir, candidate));
12190
12612
  candidate = `${stem}-${counter}${ext}`;
12191
12613
  counter += 1;
12192
12614
  } catch {
@@ -12217,8 +12639,8 @@ function extensionFromMime(mimeType) {
12217
12639
  return ".bin";
12218
12640
  }
12219
12641
  function isPathInside(candidate, root) {
12220
- const relative = path6.relative(root, candidate);
12221
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
12642
+ const relative = path7.relative(root, candidate);
12643
+ return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
12222
12644
  }
12223
12645
  async function cleanupDir(dir, cutoffMs) {
12224
12646
  let entries;
@@ -12229,7 +12651,7 @@ async function cleanupDir(dir, cutoffMs) {
12229
12651
  }
12230
12652
  let empty = true;
12231
12653
  for (const entry of entries) {
12232
- const full = path6.join(dir, entry.name);
12654
+ const full = path7.join(dir, entry.name);
12233
12655
  if (entry.isDirectory()) {
12234
12656
  const childEmpty = await cleanupDir(full, cutoffMs);
12235
12657
  if (childEmpty) {
@@ -12258,18 +12680,18 @@ var init_media_store = __esm(() => {
12258
12680
  });
12259
12681
 
12260
12682
  // src/channels/outbound-media-safety.ts
12261
- import fs5 from "node:fs/promises";
12262
- import path7 from "node:path";
12683
+ import fs6 from "node:fs/promises";
12684
+ import path8 from "node:path";
12263
12685
  async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
12264
12686
  if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) {
12265
12687
  return null;
12266
12688
  }
12267
- const candidate = path7.isAbsolute(mediaPath) ? mediaPath : path7.resolve(mediaPath);
12689
+ const candidate = path8.isAbsolute(mediaPath) ? mediaPath : path8.resolve(mediaPath);
12268
12690
  const realCandidate = await realpathOrNull(candidate);
12269
12691
  if (!realCandidate) {
12270
12692
  return null;
12271
12693
  }
12272
- const stat2 = await fs5.stat(realCandidate).catch(() => null);
12694
+ const stat2 = await fs6.stat(realCandidate).catch(() => null);
12273
12695
  if (!stat2?.isFile()) {
12274
12696
  return null;
12275
12697
  }
@@ -12283,21 +12705,21 @@ async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
12283
12705
  }
12284
12706
  async function realpathOrNull(filePath) {
12285
12707
  try {
12286
- return await fs5.realpath(filePath);
12708
+ return await fs6.realpath(filePath);
12287
12709
  } catch {
12288
12710
  return null;
12289
12711
  }
12290
12712
  }
12291
12713
  function isPathInside2(candidate, root) {
12292
- const relative = path7.relative(root, candidate);
12293
- return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
12714
+ const relative = path8.relative(root, candidate);
12715
+ return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
12294
12716
  }
12295
12717
  var init_outbound_media_safety = () => {};
12296
12718
 
12297
12719
  // src/weixin/media/mime.ts
12298
- import path8 from "node:path";
12720
+ import path9 from "node:path";
12299
12721
  function getMimeFromFilename(filename) {
12300
- const ext = path8.extname(filename).toLowerCase();
12722
+ const ext = path9.extname(filename).toLowerCase();
12301
12723
  return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
12302
12724
  }
12303
12725
  function getExtensionFromMime(mimeType) {
@@ -12635,115 +13057,361 @@ function buildFinalHeadsUp(input) {
12635
13057
  \uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`;
12636
13058
  }
12637
13059
 
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(":");
13060
+ // src/weixin/messaging/markdown-filter.ts
13061
+ class StreamingMarkdownFilter {
13062
+ buf = "";
13063
+ fence = false;
13064
+ sol = true;
13065
+ inl = null;
13066
+ feed(delta) {
13067
+ this.buf += delta;
13068
+ return this.pump(false);
12664
13069
  }
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)
13070
+ flush() {
13071
+ return this.pump(true);
13072
+ }
13073
+ pump(eof) {
13074
+ let out = "";
13075
+ while (this.buf) {
13076
+ const sLen = this.buf.length;
13077
+ const sSol = this.sol;
13078
+ const sFence = this.fence;
13079
+ const sInl = this.inl;
13080
+ if (this.fence)
13081
+ out += this.pumpFence(eof);
13082
+ else if (this.inl)
13083
+ out += this.pumpInline(eof);
13084
+ else if (this.sol)
13085
+ out += this.pumpSOL(eof);
13086
+ else
13087
+ out += this.pumpBody(eof);
13088
+ if (this.buf.length === sLen && this.sol === sSol && this.fence === sFence && this.inl === sInl)
13089
+ break;
13090
+ }
13091
+ if (eof && this.inl) {
13092
+ const markers = { image: "![", bold3: "***", italic: "*", ubold3: "___", uitalic: "_" };
13093
+ out += (markers[this.inl.type] ?? "") + this.inl.acc;
13094
+ this.inl = null;
13095
+ }
13096
+ return out;
13097
+ }
13098
+ pumpFence(eof) {
13099
+ if (this.sol) {
13100
+ if (this.buf.length < 3 && !eof)
13101
+ return "";
13102
+ if (this.buf.startsWith("```")) {
13103
+ const nl2 = this.buf.indexOf(`
13104
+ `, 3);
13105
+ if (nl2 !== -1) {
13106
+ this.fence = false;
13107
+ const line = this.buf.slice(0, nl2 + 1);
13108
+ this.buf = this.buf.slice(nl2 + 1);
13109
+ this.sol = true;
13110
+ return line;
13111
+ }
13112
+ if (eof) {
13113
+ this.fence = false;
13114
+ const line = this.buf;
13115
+ this.buf = "";
13116
+ return line;
13117
+ }
13118
+ return "";
13119
+ }
13120
+ this.sol = false;
13121
+ }
13122
+ const nl = this.buf.indexOf(`
13123
+ `);
13124
+ if (nl !== -1) {
13125
+ const chunk2 = this.buf.slice(0, nl + 1);
13126
+ this.buf = this.buf.slice(nl + 1);
13127
+ this.sol = true;
13128
+ return chunk2;
13129
+ }
13130
+ const chunk = this.buf;
13131
+ this.buf = "";
13132
+ return chunk;
13133
+ }
13134
+ pumpSOL(eof) {
13135
+ const b = this.buf;
13136
+ if (b[0] === `
13137
+ `) {
13138
+ this.buf = b.slice(1);
13139
+ return `
13140
+ `;
13141
+ }
13142
+ if (b[0] === "`") {
13143
+ if (b.length < 3 && !eof)
13144
+ return "";
13145
+ if (b.startsWith("```")) {
13146
+ const nl = b.indexOf(`
13147
+ `, 3);
13148
+ if (nl !== -1) {
13149
+ this.fence = true;
13150
+ const line = b.slice(0, nl + 1);
13151
+ this.buf = b.slice(nl + 1);
13152
+ this.sol = true;
13153
+ return line;
13154
+ }
13155
+ if (eof) {
13156
+ this.buf = "";
13157
+ return b;
13158
+ }
13159
+ return "";
13160
+ }
13161
+ this.sol = false;
13162
+ return "";
13163
+ }
13164
+ if (b[0] === ">") {
13165
+ this.sol = false;
13166
+ return "";
13167
+ }
13168
+ if (b[0] === "#") {
13169
+ let n = 0;
13170
+ while (n < b.length && b[n] === "#")
13171
+ n++;
13172
+ if (n === b.length && !eof)
13173
+ return "";
13174
+ if (n >= 5 && n <= 6 && n < b.length && b[n] === " ") {
13175
+ this.buf = b.slice(n + 1);
13176
+ this.sol = false;
13177
+ return "";
13178
+ }
13179
+ this.sol = false;
13180
+ return "";
13181
+ }
13182
+ if (b[0] === " " || b[0] === "\t") {
13183
+ if (b.search(/[^ \t]/) === -1 && !eof)
13184
+ return "";
13185
+ this.sol = false;
13186
+ return "";
13187
+ }
13188
+ if (b[0] === "-" || b[0] === "*" || b[0] === "_") {
13189
+ const ch = b[0];
13190
+ let j = 0;
13191
+ while (j < b.length && (b[j] === ch || b[j] === " "))
13192
+ j++;
13193
+ if (j === b.length && !eof)
13194
+ return "";
13195
+ if (j === b.length || b[j] === `
13196
+ `) {
13197
+ let count = 0;
13198
+ for (let k = 0;k < j; k++)
13199
+ if (b[k] === ch)
13200
+ count++;
13201
+ if (count >= 3) {
13202
+ if (j < b.length) {
13203
+ this.buf = b.slice(j + 1);
13204
+ this.sol = true;
13205
+ return b.slice(0, j + 1);
13206
+ }
13207
+ this.buf = "";
13208
+ return b;
13209
+ }
13210
+ }
13211
+ this.sol = false;
13212
+ return "";
13213
+ }
13214
+ this.sol = false;
12672
13215
  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);
13216
+ }
13217
+ pumpBody(eof) {
13218
+ let out = "";
13219
+ let i = 0;
13220
+ while (i < this.buf.length) {
13221
+ const c = this.buf[i];
13222
+ if (c === `
13223
+ `) {
13224
+ out += this.buf.slice(0, i + 1);
13225
+ this.buf = this.buf.slice(i + 1);
13226
+ this.sol = true;
13227
+ return out;
13228
+ }
13229
+ if (c === "!" && i + 1 < this.buf.length && this.buf[i + 1] === "[") {
13230
+ out += this.buf.slice(0, i);
13231
+ this.buf = this.buf.slice(i + 2);
13232
+ this.inl = { type: "image", acc: "" };
13233
+ return out;
13234
+ }
13235
+ if (c === "~") {
13236
+ i++;
13237
+ continue;
12688
13238
  }
12689
- if (!parts.length)
12690
- return text;
12691
- return `[引用: ${parts.join(" | ")}]
12692
- ${text}`;
13239
+ if (c === "*") {
13240
+ if (i + 2 < this.buf.length && this.buf[i + 1] === "*" && this.buf[i + 2] === "*") {
13241
+ out += this.buf.slice(0, i);
13242
+ this.buf = this.buf.slice(i + 3);
13243
+ this.inl = { type: "bold3", acc: "" };
13244
+ return out;
13245
+ }
13246
+ if (i + 1 < this.buf.length && this.buf[i + 1] === "*") {
13247
+ i += 2;
13248
+ continue;
13249
+ }
13250
+ if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
13251
+ `) {
13252
+ out += this.buf.slice(0, i);
13253
+ this.buf = this.buf.slice(i + 1);
13254
+ this.inl = { type: "italic", acc: "" };
13255
+ return out;
13256
+ }
13257
+ i++;
13258
+ continue;
13259
+ }
13260
+ if (c === "_") {
13261
+ if (i + 2 < this.buf.length && this.buf[i + 1] === "_" && this.buf[i + 2] === "_") {
13262
+ out += this.buf.slice(0, i);
13263
+ this.buf = this.buf.slice(i + 3);
13264
+ this.inl = { type: "ubold3", acc: "" };
13265
+ return out;
13266
+ }
13267
+ if (i + 1 < this.buf.length && this.buf[i + 1] === "_") {
13268
+ i += 2;
13269
+ continue;
13270
+ }
13271
+ if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
13272
+ `) {
13273
+ out += this.buf.slice(0, i);
13274
+ this.buf = this.buf.slice(i + 1);
13275
+ this.inl = { type: "uitalic", acc: "" };
13276
+ return out;
13277
+ }
13278
+ i++;
13279
+ continue;
13280
+ }
13281
+ i++;
12693
13282
  }
12694
- if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
12695
- return item.voice_item.text;
13283
+ let hold = 0;
13284
+ if (!eof) {
13285
+ if (this.buf.endsWith("**"))
13286
+ hold = 2;
13287
+ else if (this.buf.endsWith("__"))
13288
+ hold = 2;
13289
+ else if (this.buf.endsWith("*"))
13290
+ hold = 1;
13291
+ else if (this.buf.endsWith("_"))
13292
+ hold = 1;
13293
+ else if (this.buf.endsWith("!"))
13294
+ hold = 1;
13295
+ }
13296
+ out += this.buf.slice(0, this.buf.length - hold);
13297
+ this.buf = hold > 0 ? this.buf.slice(-hold) : "";
13298
+ return out;
13299
+ }
13300
+ pumpInline(_eof) {
13301
+ if (!this.inl)
13302
+ return "";
13303
+ this.inl.acc += this.buf;
13304
+ this.buf = "";
13305
+ switch (this.inl.type) {
13306
+ case "bold3": {
13307
+ const idx = this.inl.acc.indexOf("***");
13308
+ if (idx !== -1) {
13309
+ const content = this.inl.acc.slice(0, idx);
13310
+ this.buf = this.inl.acc.slice(idx + 3);
13311
+ this.inl = null;
13312
+ if (StreamingMarkdownFilter.containsCJK(content))
13313
+ return content;
13314
+ return `***${content}***`;
13315
+ }
13316
+ return "";
13317
+ }
13318
+ case "ubold3": {
13319
+ const idx = this.inl.acc.indexOf("___");
13320
+ if (idx !== -1) {
13321
+ const content = this.inl.acc.slice(0, idx);
13322
+ this.buf = this.inl.acc.slice(idx + 3);
13323
+ this.inl = null;
13324
+ if (StreamingMarkdownFilter.containsCJK(content))
13325
+ return content;
13326
+ return `___${content}___`;
13327
+ }
13328
+ return "";
13329
+ }
13330
+ case "italic": {
13331
+ for (let j = 0;j < this.inl.acc.length; j++) {
13332
+ if (this.inl.acc[j] === `
13333
+ `) {
13334
+ const r = "*" + this.inl.acc.slice(0, j + 1);
13335
+ this.buf = this.inl.acc.slice(j + 1);
13336
+ this.inl = null;
13337
+ this.sol = true;
13338
+ return r;
13339
+ }
13340
+ if (this.inl.acc[j] === "*") {
13341
+ if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "*") {
13342
+ j++;
13343
+ continue;
13344
+ }
13345
+ const content = this.inl.acc.slice(0, j);
13346
+ this.buf = this.inl.acc.slice(j + 1);
13347
+ this.inl = null;
13348
+ if (StreamingMarkdownFilter.containsCJK(content))
13349
+ return content;
13350
+ return `*${content}*`;
13351
+ }
13352
+ }
13353
+ return "";
13354
+ }
13355
+ case "uitalic": {
13356
+ for (let j = 0;j < this.inl.acc.length; j++) {
13357
+ if (this.inl.acc[j] === `
13358
+ `) {
13359
+ const r = "_" + this.inl.acc.slice(0, j + 1);
13360
+ this.buf = this.inl.acc.slice(j + 1);
13361
+ this.inl = null;
13362
+ this.sol = true;
13363
+ return r;
13364
+ }
13365
+ if (this.inl.acc[j] === "_") {
13366
+ if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "_") {
13367
+ j++;
13368
+ continue;
13369
+ }
13370
+ const content = this.inl.acc.slice(0, j);
13371
+ this.buf = this.inl.acc.slice(j + 1);
13372
+ this.inl = null;
13373
+ if (StreamingMarkdownFilter.containsCJK(content))
13374
+ return content;
13375
+ return `_${content}_`;
13376
+ }
13377
+ }
13378
+ return "";
13379
+ }
13380
+ case "image": {
13381
+ const cb = this.inl.acc.indexOf("]");
13382
+ if (cb === -1)
13383
+ return "";
13384
+ if (cb + 1 >= this.inl.acc.length)
13385
+ return "";
13386
+ if (this.inl.acc[cb + 1] !== "(") {
13387
+ const r = "![" + this.inl.acc.slice(0, cb + 1);
13388
+ this.buf = this.inl.acc.slice(cb + 1);
13389
+ this.inl = null;
13390
+ return r;
13391
+ }
13392
+ const cp = this.inl.acc.indexOf(")", cb + 2);
13393
+ if (cp !== -1) {
13394
+ this.buf = this.inl.acc.slice(cp + 1);
13395
+ this.inl = null;
13396
+ return "";
13397
+ }
13398
+ return "";
13399
+ }
12696
13400
  }
13401
+ return "";
12697
13402
  }
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);
13403
+ static containsCJK(text) {
13404
+ return /[\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(text);
12710
13405
  }
12711
- return out;
12712
13406
  }
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
13407
 
12734
13408
  // src/weixin/messaging/send.ts
12735
13409
  function generateClientId() {
12736
13410
  return generateId("openclaw-weixin");
12737
13411
  }
12738
13412
  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;
13413
+ const f = new StreamingMarkdownFilter;
13414
+ return f.feed(text) + f.flush();
12747
13415
  }
12748
13416
  function buildTextMessageReq(params) {
12749
13417
  const { to, text, contextToken, clientId } = params;
@@ -12979,10 +13647,10 @@ var init_cdn_upload = __esm(() => {
12979
13647
 
12980
13648
  // src/weixin/cdn/upload.ts
12981
13649
  import crypto3 from "node:crypto";
12982
- import fs6 from "node:fs/promises";
13650
+ import fs7 from "node:fs/promises";
12983
13651
  async function uploadMediaToCdn(params) {
12984
13652
  const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
12985
- const plaintext = await fs6.readFile(filePath);
13653
+ const plaintext = await fs7.readFile(filePath);
12986
13654
  const rawsize = plaintext.length;
12987
13655
  const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
12988
13656
  const filesize = aesEcbPaddedSize(rawsize);
@@ -13055,7 +13723,7 @@ var init_upload = __esm(() => {
13055
13723
  });
13056
13724
 
13057
13725
  // src/weixin/messaging/send-media.ts
13058
- import path9 from "node:path";
13726
+ import path10 from "node:path";
13059
13727
  async function sendWeixinMediaFile(params) {
13060
13728
  const { media, filePath, to, text, opts, cdnBaseUrl } = params;
13061
13729
  const mime = media?.mimeType ?? getMimeFromFilename(filePath);
@@ -13082,7 +13750,7 @@ async function sendWeixinMediaFile(params) {
13082
13750
  logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
13083
13751
  return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
13084
13752
  }
13085
- const fileName = media?.fileName ?? path9.basename(filePath);
13753
+ const fileName = media?.fileName ?? path10.basename(filePath);
13086
13754
  logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
13087
13755
  const uploaded = await uploadFileAttachmentToWeixin({
13088
13756
  filePath,
@@ -13102,14 +13770,14 @@ var init_send_media = __esm(() => {
13102
13770
  });
13103
13771
 
13104
13772
  // src/weixin/messaging/debug-mode.ts
13105
- import fs7 from "node:fs";
13106
- import path10 from "node:path";
13773
+ import fs8 from "node:fs";
13774
+ import path11 from "node:path";
13107
13775
  function resolveDebugModePath() {
13108
- return path10.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
13776
+ return path11.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
13109
13777
  }
13110
13778
  function loadState() {
13111
13779
  try {
13112
- const raw = fs7.readFileSync(resolveDebugModePath(), "utf-8");
13780
+ const raw = fs8.readFileSync(resolveDebugModePath(), "utf-8");
13113
13781
  const parsed = JSON.parse(raw);
13114
13782
  if (parsed && typeof parsed.accounts === "object")
13115
13783
  return parsed;
@@ -13118,8 +13786,8 @@ function loadState() {
13118
13786
  }
13119
13787
  function saveState(state) {
13120
13788
  const filePath = resolveDebugModePath();
13121
- ensureDirSync(path10.dirname(filePath));
13122
- fs7.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
13789
+ ensureDirSync(path11.dirname(filePath));
13790
+ fs8.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
13123
13791
  }
13124
13792
  function toggleDebugMode(accountId) {
13125
13793
  const state = loadState();
@@ -13576,9 +14244,9 @@ var init_perf_tracer = __esm(() => {
13576
14244
 
13577
14245
  // src/weixin/messaging/handle-weixin-message-turn.ts
13578
14246
  import crypto4 from "node:crypto";
13579
- import fs8 from "node:fs/promises";
14247
+ import fs9 from "node:fs/promises";
13580
14248
  import { tmpdir } from "node:os";
13581
- import path11 from "node:path";
14249
+ import path12 from "node:path";
13582
14250
  function utf8ByteLength(s) {
13583
14251
  return Buffer.byteLength(s, "utf8");
13584
14252
  }
@@ -13668,24 +14336,24 @@ function hardCutByCodepoint(s, maxBytes) {
13668
14336
  return out;
13669
14337
  }
13670
14338
  function resolveMediaTempDir(customRoot) {
13671
- return customRoot ?? path11.join(tmpdir(), "weacpx", "media");
14339
+ return customRoot ?? path12.join(tmpdir(), "weacpx", "media");
13672
14340
  }
13673
14341
  function createSaveMediaBuffer(mediaTempDir) {
13674
14342
  return async function saveMediaBuffer(buffer, contentType, subdir, maxBytes, originalFilename) {
13675
14343
  if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
13676
14344
  throw new Error(`media exceeds ${maxBytes} bytes`);
13677
14345
  }
13678
- const dir = path11.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
13679
- await fs8.mkdir(dir, { recursive: true });
14346
+ const dir = path12.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
14347
+ await fs9.mkdir(dir, { recursive: true });
13680
14348
  let ext = ".bin";
13681
14349
  if (originalFilename) {
13682
- ext = path11.extname(originalFilename) || ".bin";
14350
+ ext = path12.extname(originalFilename) || ".bin";
13683
14351
  } else if (contentType) {
13684
14352
  ext = getExtensionFromMime(contentType);
13685
14353
  }
13686
14354
  const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
13687
- const filePath = path11.join(dir, name);
13688
- await fs8.writeFile(filePath, buffer);
14355
+ const filePath = path12.join(dir, name);
14356
+ await fs9.writeFile(filePath, buffer);
13689
14357
  return { path: filePath };
13690
14358
  };
13691
14359
  }
@@ -13837,7 +14505,7 @@ async function handleWeixinMessageTurn(full, deps) {
13837
14505
  continue;
13838
14506
  }
13839
14507
  try {
13840
- const buffer = await fs8.readFile(filePath);
14508
+ const buffer = await fs9.readFile(filePath);
13841
14509
  const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
13842
14510
  media.push(await mediaStore.saveMediaBuffer({
13843
14511
  channelId: "weixin",
@@ -13851,7 +14519,7 @@ async function handleWeixinMessageTurn(full, deps) {
13851
14519
  maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
13852
14520
  }));
13853
14521
  } finally {
13854
- await fs8.rm(filePath, { force: true }).catch(() => {});
14522
+ await fs9.rm(filePath, { force: true }).catch(() => {});
13855
14523
  }
13856
14524
  } catch (err) {
13857
14525
  deps.errLog(`media download failed: ${String(err)}`);
@@ -14085,20 +14753,20 @@ var init_handle_weixin_message_turn = __esm(() => {
14085
14753
  });
14086
14754
 
14087
14755
  // src/weixin/storage/sync-buf.ts
14088
- import fs9 from "node:fs";
14089
- import path12 from "node:path";
14756
+ import fs10 from "node:fs";
14757
+ import path13 from "node:path";
14090
14758
  function resolveAccountsDir2() {
14091
- return path12.join(resolveStateDir(), "openclaw-weixin", "accounts");
14759
+ return path13.join(resolveStateDir(), "openclaw-weixin", "accounts");
14092
14760
  }
14093
14761
  function getSyncBufFilePath(accountId) {
14094
- return path12.join(resolveAccountsDir2(), `${accountId}.sync.json`);
14762
+ return path13.join(resolveAccountsDir2(), `${accountId}.sync.json`);
14095
14763
  }
14096
14764
  function getLegacySyncBufDefaultJsonPath() {
14097
- return path12.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
14765
+ return path13.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
14098
14766
  }
14099
14767
  function readSyncBufFile(filePath) {
14100
14768
  try {
14101
- const raw = fs9.readFileSync(filePath, "utf-8");
14769
+ const raw = fs10.readFileSync(filePath, "utf-8");
14102
14770
  const data = JSON.parse(raw);
14103
14771
  if (typeof data.get_updates_buf === "string") {
14104
14772
  return data.get_updates_buf;
@@ -14110,10 +14778,10 @@ function loadGetUpdatesBuf(filePath) {
14110
14778
  const value = readSyncBufFile(filePath);
14111
14779
  if (value !== undefined)
14112
14780
  return value;
14113
- const accountId = path12.basename(filePath, ".sync.json");
14781
+ const accountId = path13.basename(filePath, ".sync.json");
14114
14782
  const rawId = deriveRawAccountId(accountId);
14115
14783
  if (rawId) {
14116
- const compatPath = path12.join(resolveAccountsDir2(), `${rawId}.sync.json`);
14784
+ const compatPath = path13.join(resolveAccountsDir2(), `${rawId}.sync.json`);
14117
14785
  const compatValue = readSyncBufFile(compatPath);
14118
14786
  if (compatValue !== undefined)
14119
14787
  return compatValue;
@@ -14121,9 +14789,9 @@ function loadGetUpdatesBuf(filePath) {
14121
14789
  return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
14122
14790
  }
14123
14791
  function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
14124
- const dir = path12.dirname(filePath);
14792
+ const dir = path13.dirname(filePath);
14125
14793
  ensureDirSync(dir);
14126
- fs9.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
14794
+ fs10.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
14127
14795
  }
14128
14796
  var init_sync_buf = __esm(() => {
14129
14797
  init_accounts();
@@ -14157,23 +14825,23 @@ function shouldFetchTypingConfig(textBody) {
14157
14825
  }
14158
14826
  async function monitorWeixinProvider(opts) {
14159
14827
  const {
14160
- baseUrl,
14161
- cdnBaseUrl,
14162
- token,
14163
- accountId,
14164
14828
  agent,
14165
14829
  abortSignal,
14166
14830
  longPollTimeoutMs
14167
14831
  } = opts;
14832
+ let baseUrl = opts.baseUrl;
14833
+ let cdnBaseUrl = opts.cdnBaseUrl;
14834
+ let token = opts.token;
14835
+ let accountId = opts.accountId;
14168
14836
  const log = opts.log ?? ((msg) => console.log(msg));
14169
14837
  const errLog = (msg) => {
14170
14838
  log(msg);
14171
14839
  logger.error(msg);
14172
14840
  };
14173
- const aLog = logger.withAccount(accountId);
14841
+ let aLog = logger.withAccount(accountId);
14174
14842
  log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
14175
14843
  aLog.info(`Monitor started: baseUrl=${baseUrl}`);
14176
- const syncFilePath = getSyncBufFilePath(accountId);
14844
+ let syncFilePath = getSyncBufFilePath(accountId);
14177
14845
  const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
14178
14846
  let getUpdatesBuf = previousGetUpdatesBuf ?? "";
14179
14847
  if (previousGetUpdatesBuf) {
@@ -14181,7 +14849,7 @@ async function monitorWeixinProvider(opts) {
14181
14849
  } else {
14182
14850
  log(`[weixin] no previous sync buf, starting fresh`);
14183
14851
  }
14184
- const configManager = new WeixinConfigManager({ baseUrl, token }, log);
14852
+ let configManager = new WeixinConfigManager({ baseUrl, token }, log);
14185
14853
  const conversationExecutor = createConversationExecutor();
14186
14854
  const seenMessageIds = new Set;
14187
14855
  const messageIdOrder = [];
@@ -14204,11 +14872,37 @@ async function monitorWeixinProvider(opts) {
14204
14872
  if (isApiError) {
14205
14873
  const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
14206
14874
  if (isSessionExpired) {
14875
+ const staleToken = token;
14876
+ const staleAccountId = accountId;
14877
+ errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), entering credential recovery. Please run \`weacpx login\` to re-login.`);
14207
14878
  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
14879
  consecutiveFailures = 0;
14211
- await sleep(pauseMs, abortSignal);
14880
+ const recovered = await pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal);
14881
+ if (recovered === null) {
14882
+ aLog.info("Monitor stopped (aborted during credential recovery)");
14883
+ return;
14884
+ }
14885
+ const oldAccountId = accountId;
14886
+ accountId = recovered.accountId;
14887
+ baseUrl = recovered.baseUrl;
14888
+ cdnBaseUrl = recovered.cdnBaseUrl;
14889
+ token = recovered.token;
14890
+ aLog = logger.withAccount(accountId);
14891
+ syncFilePath = getSyncBufFilePath(accountId);
14892
+ const previousBuf = loadGetUpdatesBuf(syncFilePath);
14893
+ getUpdatesBuf = previousBuf ?? "";
14894
+ configManager = new WeixinConfigManager({ baseUrl, token }, log);
14895
+ seenMessageIds.clear();
14896
+ messageIdOrder.length = 0;
14897
+ consecutiveFailures = 0;
14898
+ nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
14899
+ resetSessionPause(oldAccountId);
14900
+ resetSessionPause(accountId);
14901
+ if (oldAccountId !== accountId) {
14902
+ clearContextTokensForAccount(oldAccountId);
14903
+ restoreContextTokens(accountId);
14904
+ }
14905
+ log(`[weixin] credential recovered, resuming monitor with account=${accountId}`);
14212
14906
  continue;
14213
14907
  }
14214
14908
  consecutiveFailures += 1;
@@ -14302,7 +14996,43 @@ function sleep(ms, signal) {
14302
14996
  }, { once: true });
14303
14997
  });
14304
14998
  }
14305
- var DEFAULT_LONG_POLL_TIMEOUT_MS2 = 35000, MAX_CONSECUTIVE_FAILURES = 3, BACKOFF_DELAY_MS = 30000, RETRY_DELAY_MS = 2000;
14999
+ async function pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal) {
15000
+ let attempt = 0;
15001
+ while (!abortSignal?.aborted) {
15002
+ attempt += 1;
15003
+ const currentAccount = resolveWeixinAccount(staleAccountId);
15004
+ if (currentAccount.token && currentAccount.token !== staleToken) {
15005
+ log(`[weixin] credential recovery: fresh token detected for account=${staleAccountId}`);
15006
+ return {
15007
+ accountId: currentAccount.accountId,
15008
+ baseUrl: currentAccount.baseUrl,
15009
+ cdnBaseUrl: currentAccount.cdnBaseUrl,
15010
+ token: currentAccount.token
15011
+ };
15012
+ }
15013
+ const ids = listWeixinAccountIds();
15014
+ for (const id of ids) {
15015
+ if (id === staleAccountId)
15016
+ continue;
15017
+ const account = resolveWeixinAccount(id);
15018
+ if (account.configured && account.token) {
15019
+ log(`[weixin] credential recovery: new account detected, switching to account=${id}`);
15020
+ return {
15021
+ accountId: account.accountId,
15022
+ baseUrl: account.baseUrl,
15023
+ cdnBaseUrl: account.cdnBaseUrl,
15024
+ token: account.token
15025
+ };
15026
+ }
15027
+ }
15028
+ if (attempt % 10 === 0) {
15029
+ log(`[weixin] credential recovery: still waiting for fresh credentials (checked ${attempt} times)`);
15030
+ }
15031
+ await sleep(CREDENTIAL_RECOVERY_POLL_INTERVAL_MS, abortSignal);
15032
+ }
15033
+ return null;
15034
+ }
15035
+ 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
15036
  var init_monitor = __esm(() => {
14307
15037
  init_api();
14308
15038
  init_config_cache();
@@ -14311,6 +15041,9 @@ var init_monitor = __esm(() => {
14311
15041
  init_types2();
14312
15042
  init_sync_buf();
14313
15043
  init_logger();
15044
+ init_accounts();
15045
+ init_session_guard();
15046
+ init_inbound();
14314
15047
  });
14315
15048
 
14316
15049
  // src/weixin/bot.ts
@@ -14369,6 +15102,8 @@ function logout(opts) {
14369
15102
  log("当前没有已登录的账号");
14370
15103
  return;
14371
15104
  }
15105
+ for (const id of ids)
15106
+ clearContextTokensForAccount(id);
14372
15107
  clearAllWeixinAccounts();
14373
15108
  log("✅ 已退出登录");
14374
15109
  }
@@ -14396,6 +15131,7 @@ async function start(agent, opts) {
14396
15131
  if (!account.configured) {
14397
15132
  throw new Error(`账号 ${accountId} 未配置 (缺少 token),请先运行 login`);
14398
15133
  }
15134
+ restoreContextTokens(account.accountId);
14399
15135
  log(`[weixin] 启动 bot, account=${account.accountId}`);
14400
15136
  await monitorWeixinProvider({
14401
15137
  baseUrl: account.baseUrl,
@@ -14421,6 +15157,7 @@ async function start(agent, opts) {
14421
15157
  var init_bot = __esm(() => {
14422
15158
  init_accounts();
14423
15159
  init_login_qr();
15160
+ init_inbound();
14424
15161
  init_monitor();
14425
15162
  });
14426
15163
 
@@ -14751,9 +15488,9 @@ function createWeixinConsumerLock(options = {}) {
14751
15488
  }
14752
15489
  };
14753
15490
  }
14754
- async function loadLockMetadata(path13) {
15491
+ async function loadLockMetadata(path14) {
14755
15492
  try {
14756
- const raw = await readFile6(path13, "utf8");
15493
+ const raw = await readFile6(path14, "utf8");
14757
15494
  const parsed = JSON.parse(raw);
14758
15495
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
14759
15496
  return null;
@@ -15823,6 +16560,7 @@ function parseCommand(input) {
15823
16560
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
15824
16561
  const name = parts[2];
15825
16562
  let cwd = "";
16563
+ let raw = false;
15826
16564
  let invalid = false;
15827
16565
  for (let index = 3;index < parts.length; index += 1) {
15828
16566
  if (parts[index] === "--cwd" || parts[index] === "-d") {
@@ -15834,11 +16572,19 @@ function parseCommand(input) {
15834
16572
  index += 1;
15835
16573
  continue;
15836
16574
  }
16575
+ if (parts[index] === "--raw") {
16576
+ if (raw) {
16577
+ invalid = true;
16578
+ break;
16579
+ }
16580
+ raw = true;
16581
+ continue;
16582
+ }
15837
16583
  invalid = true;
15838
16584
  break;
15839
16585
  }
15840
16586
  if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
15841
- return { kind: "workspace.new", name, cwd };
16587
+ return { kind: "workspace.new", name, cwd, ...raw ? { raw: true } : {} };
15842
16588
  }
15843
16589
  }
15844
16590
  if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
@@ -16298,26 +17044,26 @@ var init_permission_handler = __esm(() => {
16298
17044
 
16299
17045
  // src/commands/handlers/config-handler.ts
16300
17046
  function handleConfigShow(context) {
16301
- const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path13) => `- ${path13}`)];
16302
- lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((path13) => `- ${path13}`));
17047
+ const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path14) => `- ${path14}`)];
17048
+ lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((path14) => `- ${path14}`));
16303
17049
  if (context.config) {
16304
17050
  lines.push("", "示例:", "- /config set channel.replyMode final", "- /config set logging.level debug");
16305
17051
  }
16306
17052
  return { text: lines.join(`
16307
17053
  `) };
16308
17054
  }
16309
- async function handleConfigSet(context, path13, rawValue) {
17055
+ async function handleConfigSet(context, path14, rawValue) {
16310
17056
  if (!context.config || !context.configStore) {
16311
17057
  return { text: "当前没有加载可写入的配置。" };
16312
17058
  }
16313
17059
  const previous = cloneAppConfig(context.config);
16314
17060
  const updated = cloneAppConfig(context.config);
16315
- const result = applySupportedConfigUpdate(updated, path13, rawValue);
17061
+ const result = applySupportedConfigUpdate(updated, path14, rawValue);
16316
17062
  if ("error" in result) {
16317
17063
  return { text: result.error };
16318
17064
  }
16319
17065
  await context.configStore.save(updated);
16320
- if (path13 === "transport.permissionMode" || path13 === "transport.nonInteractivePermissions" || path13 === "transport.permissionPolicy") {
17066
+ if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
16321
17067
  try {
16322
17068
  await context.transport.updatePermissionPolicy?.(updated.transport);
16323
17069
  } catch (error2) {
@@ -16327,10 +17073,10 @@ async function handleConfigSet(context, path13, rawValue) {
16327
17073
  }
16328
17074
  }
16329
17075
  context.replaceConfig(updated);
16330
- return { text: `配置已更新:${path13} = ${result.renderedValue}` };
17076
+ return { text: `配置已更新:${path14} = ${result.renderedValue}` };
16331
17077
  }
16332
- function applySupportedConfigUpdate(config2, path13, rawValue) {
16333
- switch (path13) {
17078
+ function applySupportedConfigUpdate(config2, path14, rawValue) {
17079
+ switch (path14) {
16334
17080
  case "transport.type": {
16335
17081
  const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
16336
17082
  if (!parsed)
@@ -16418,18 +17164,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16418
17164
  };
16419
17165
  }
16420
17166
  }
16421
- const agentMatch = path13.match(/^agents\.([^.]+)\.(driver|command)$/);
17167
+ const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
16422
17168
  if (agentMatch) {
16423
17169
  const [, name, field] = agentMatch;
16424
17170
  if (!name || !field) {
16425
- return { error: `不支持修改这个配置路径:${path13}` };
17171
+ return { error: `不支持修改这个配置路径:${path14}` };
16426
17172
  }
16427
17173
  const agent = config2.agents[name];
16428
17174
  if (!agent) {
16429
17175
  return { error: `Agent「${name}」不存在,请先创建。` };
16430
17176
  }
16431
17177
  if (!rawValue.trim()) {
16432
- return { error: `${path13} 不能为空。` };
17178
+ return { error: `${path14} 不能为空。` };
16433
17179
  }
16434
17180
  if (field === "driver") {
16435
17181
  agent.driver = rawValue;
@@ -16438,18 +17184,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16438
17184
  }
16439
17185
  return { renderedValue: rawValue };
16440
17186
  }
16441
- const workspaceMatch = path13.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
17187
+ const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
16442
17188
  if (workspaceMatch) {
16443
17189
  const [, name, field] = workspaceMatch;
16444
17190
  if (!name || !field) {
16445
- return { error: `不支持修改这个配置路径:${path13}` };
17191
+ return { error: `不支持修改这个配置路径:${path14}` };
16446
17192
  }
16447
17193
  const workspace = config2.workspaces[name];
16448
17194
  if (!workspace) {
16449
17195
  return { error: `工作区「${name}」不存在,请先创建。` };
16450
17196
  }
16451
17197
  if (!rawValue.trim()) {
16452
- return { error: `${path13} 不能为空。` };
17198
+ return { error: `${path14} 不能为空。` };
16453
17199
  }
16454
17200
  if (field === "cwd") {
16455
17201
  workspace.cwd = rawValue;
@@ -16458,15 +17204,15 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16458
17204
  }
16459
17205
  return { renderedValue: rawValue };
16460
17206
  }
16461
- return { error: `不支持修改这个配置路径:${path13}` };
17207
+ return { error: `不支持修改这个配置路径:${path14}` };
16462
17208
  }
16463
17209
  function parseEnum(value, allowed) {
16464
17210
  return allowed.includes(value) ? value : null;
16465
17211
  }
16466
- function parsePositiveNumber(rawValue, path13) {
17212
+ function parsePositiveNumber(rawValue, path14) {
16467
17213
  const value = Number(rawValue);
16468
17214
  if (!Number.isFinite(value) || value <= 0) {
16469
- return { error: `${path13} 必须是正数。` };
17215
+ return { error: `${path14} 必须是正数。` };
16470
17216
  }
16471
17217
  return { value };
16472
17218
  }
@@ -16901,7 +17647,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
16901
17647
  return {
16902
17648
  text: [
16903
17649
  "没有找到可绑定的已有会话。",
16904
- `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
17650
+ `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${quoteWorkspaceNameIfNeeded(workspace)} --name <会话名>`
16905
17651
  ].join(`
16906
17652
  `)
16907
17653
  };
@@ -17238,6 +17984,7 @@ var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /se
17238
17984
  var init_session_handler = __esm(() => {
17239
17985
  init_build_coordinator_prompt();
17240
17986
  init_channel_scope();
17987
+ init_workspace_name();
17241
17988
  sessionHelp = {
17242
17989
  topic: "session",
17243
17990
  aliases: ["ss", "sessions"],
@@ -17885,7 +18632,7 @@ var init_agent_handler = __esm(() => {
17885
18632
  function handleWorkspaces(context) {
17886
18633
  return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
17887
18634
  }
17888
- async function handleWorkspaceCreate(context, workspaceName, cwd) {
18635
+ async function handleWorkspaceCreate(context, workspaceName, cwd, options = {}) {
17889
18636
  if (!context.config || !context.configStore) {
17890
18637
  return { text: "当前没有加载可写入的配置。" };
17891
18638
  }
@@ -17893,9 +18640,18 @@ async function handleWorkspaceCreate(context, workspaceName, cwd) {
17893
18640
  if (!await pathExists(normalizedCwd)) {
17894
18641
  return { text: `工作区路径不存在:${cwd}` };
17895
18642
  }
17896
- const updated = await context.configStore.upsertWorkspace(workspaceName, normalizedCwd);
18643
+ let name = workspaceName;
18644
+ let notice;
18645
+ if (!options.raw && !isWorkspaceNameValid(workspaceName)) {
18646
+ const base = sanitizeWorkspaceName(workspaceName);
18647
+ name = allocateWorkspaceName(base, context.config.workspaces);
18648
+ notice = `名称 ${JSON.stringify(workspaceName)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`;
18649
+ }
18650
+ const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
17897
18651
  context.replaceConfig(updated);
17898
- return { text: `工作区「${workspaceName}」已保存` };
18652
+ const savedLine = `工作区「${name}」已保存`;
18653
+ return { text: notice ? `${notice}
18654
+ ${savedLine}` : savedLine };
17899
18655
  }
17900
18656
  async function handleWorkspaceRemove(context, workspaceName) {
17901
18657
  if (!context.config || !context.configStore) {
@@ -17907,6 +18663,7 @@ async function handleWorkspaceRemove(context, workspaceName) {
17907
18663
  }
17908
18664
  var workspaceHelp;
17909
18665
  var init_workspace_handler = __esm(() => {
18666
+ init_workspace_name();
17910
18667
  init_workspace_path();
17911
18668
  workspaceHelp = {
17912
18669
  topic: "workspace",
@@ -17915,7 +18672,7 @@ var init_workspace_handler = __esm(() => {
17915
18672
  commands: [
17916
18673
  { usage: "/workspaces", description: "查看当前已注册的工作区" },
17917
18674
  { usage: "/workspace 或 /ws", description: "查看工作区列表" },
17918
- { usage: "/ws new <name> -d <path>", description: "添加工作区" },
18675
+ { usage: "/ws new <name> -d <path> [--raw]", description: "添加工作区;含特殊字符的名称会被自动规范化,--raw 保留原名" },
17919
18676
  { usage: "/workspace rm <name>", description: "删除工作区" }
17920
18677
  ],
17921
18678
  examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
@@ -18152,7 +18909,7 @@ async function resolveShortcutWorkspace(context, target) {
18152
18909
  reused: true
18153
18910
  };
18154
18911
  }
18155
- const workspaceName = allocateWorkspaceName(context, basenameForWorkspacePath(cwd));
18912
+ const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), context.config?.workspaces ?? {});
18156
18913
  const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
18157
18914
  context.replaceConfig(updated);
18158
18915
  return {
@@ -18161,16 +18918,6 @@ async function resolveShortcutWorkspace(context, target) {
18161
18918
  reused: false
18162
18919
  };
18163
18920
  }
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
18921
  async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
18175
18922
  if (!await hasLogicalSession(context, baseAlias, chatKey)) {
18176
18923
  return baseAlias;
@@ -18196,6 +18943,7 @@ function renderShortcutSessionCreationError(workspace, alias) {
18196
18943
  };
18197
18944
  }
18198
18945
  var init_session_shortcut_handler = __esm(() => {
18946
+ init_workspace_name();
18199
18947
  init_workspace_path();
18200
18948
  init_errors();
18201
18949
  init_channel_scope();
@@ -18208,8 +18956,8 @@ function renderTransportError(session, error2) {
18208
18956
  return {
18209
18957
  text: [
18210
18958
  `当前会话「${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 <会话名>`
18959
+ `请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)}`,
18960
+ `如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
18213
18961
  ].join(`
18214
18962
  `)
18215
18963
  };
@@ -18271,7 +19019,7 @@ function renderSessionCreationFailure(session, detail) {
18271
19019
  text: [
18272
19020
  "会话创建失败。",
18273
19021
  `错误信息:${summarizeTransportError(detail)}`,
18274
- `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
19022
+ `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
18275
19023
  ].join(`
18276
19024
  `)
18277
19025
  };
@@ -18290,6 +19038,7 @@ async function tryRecoverMissingSession(ops, session, error2) {
18290
19038
  }
18291
19039
  var init_session_recovery_handler = __esm(() => {
18292
19040
  init_errors();
19041
+ init_workspace_name();
18293
19042
  });
18294
19043
 
18295
19044
  // src/recovery/auto-install-optional-dep.ts
@@ -18415,10 +19164,10 @@ ${err.message}`, reason: "spawn" });
18415
19164
  const dir = join10(homedir6(), ".weacpx", "logs");
18416
19165
  await mkdir10(dir, { recursive: true });
18417
19166
  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" });
19167
+ const path14 = join10(dir, `auto-install-${timestamp}.log`);
19168
+ const stream = createWriteStream(path14, { flags: "a" });
18420
19169
  return {
18421
- path: path13,
19170
+ path: path14,
18422
19171
  append: async (chunk) => {
18423
19172
  await new Promise((resolve3, reject) => stream.write(chunk, (err) => err ? reject(err) : resolve3()));
18424
19173
  },
@@ -18501,9 +19250,9 @@ function isUnder(child, parent) {
18501
19250
  const p = parent.replace(/[\\/]+$/, "");
18502
19251
  return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
18503
19252
  }
18504
- async function defaultFsExists(path13) {
19253
+ async function defaultFsExists(path14) {
18505
19254
  try {
18506
- await access3(path13);
19255
+ await access3(path14);
18507
19256
  return true;
18508
19257
  } catch {
18509
19258
  return false;
@@ -18731,7 +19480,7 @@ class CommandRouter {
18731
19480
  case "workspaces":
18732
19481
  return handleWorkspaces(this.createHandlerContext());
18733
19482
  case "workspace.new":
18734
- return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
19483
+ return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd, command.raw ? { raw: true } : {});
18735
19484
  case "workspace.rm":
18736
19485
  return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
18737
19486
  case "sessions":
@@ -19193,7 +19942,7 @@ function resolveAcpxCommandMetadata(options = {}) {
19193
19942
  }
19194
19943
  const platform = options.platform ?? process.platform;
19195
19944
  const resolvePackageJson = options.resolvePackageJson ?? ((id) => require3.resolve(id));
19196
- const readPackageJson = options.readPackageJson ?? ((path13) => JSON.parse(readFileSync(path13, "utf8")));
19945
+ const readPackageJson = options.readPackageJson ?? ((path14) => JSON.parse(readFileSync(path14, "utf8")));
19197
19946
  try {
19198
19947
  const packageJsonPath = resolvePackageJson("acpx/package.json");
19199
19948
  const pkg = readPackageJson(packageJsonPath);
@@ -19570,8 +20319,8 @@ class OrchestrationServer {
19570
20319
  if (this.endpoint.kind !== "unix") {
19571
20320
  return;
19572
20321
  }
19573
- const removeFile = this.deps.removeFile ?? (async (path13) => {
19574
- await rm7(path13, { force: true });
20322
+ const removeFile = this.deps.removeFile ?? (async (path14) => {
20323
+ await rm7(path14, { force: true });
19575
20324
  });
19576
20325
  await removeFile(this.endpoint.path);
19577
20326
  }
@@ -19732,9 +20481,9 @@ function requireTaskQuestions(params, key) {
19732
20481
  };
19733
20482
  });
19734
20483
  }
19735
- async function canConnectToEndpoint(path13) {
20484
+ async function canConnectToEndpoint(path14) {
19736
20485
  return await new Promise((resolve3) => {
19737
- const socket = createConnection2(path13);
20486
+ const socket = createConnection2(path14);
19738
20487
  let settled = false;
19739
20488
  const finish = (result) => {
19740
20489
  if (settled) {
@@ -19755,7 +20504,7 @@ async function canConnectToEndpoint(path13) {
19755
20504
  });
19756
20505
  });
19757
20506
  }
19758
- async function listen(server, path13) {
20507
+ async function listen(server, path14) {
19759
20508
  await new Promise((resolve3, reject) => {
19760
20509
  const onError = (error2) => {
19761
20510
  server.off("listening", onListening);
@@ -19767,7 +20516,7 @@ async function listen(server, path13) {
19767
20516
  };
19768
20517
  server.once("error", onError);
19769
20518
  server.once("listening", onListening);
19770
- server.listen(path13);
20519
+ server.listen(path14);
19771
20520
  });
19772
20521
  }
19773
20522
  function isServerNotRunningError(error2) {
@@ -19891,6 +20640,7 @@ class OrchestrationService {
19891
20640
  stateMutex;
19892
20641
  pendingWorkerSessions = new Map;
19893
20642
  pendingLogicalTransportSessions = new Map;
20643
+ pendingParallelStarts = new Map;
19894
20644
  constructor(deps) {
19895
20645
  this.deps = deps;
19896
20646
  this.stateMutex = deps.stateMutex ?? new AsyncMutex;
@@ -20055,84 +20805,139 @@ class OrchestrationService {
20055
20805
  const normalizedGroupId = this.normalizeGroupId(input.groupId);
20056
20806
  const taskId = this.deps.createId();
20057
20807
  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 () => {
20808
+ if (input.parallel) {
20809
+ const queuedResult = await this.mutate(async () => {
20073
20810
  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);
20811
+ if (this.canStartParallelTask(state, input.targetAgent)) {
20812
+ this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
20813
+ return null;
20077
20814
  }
20078
- const task = {
20815
+ const now = this.deps.now().toISOString();
20816
+ const queuedTask = {
20079
20817
  taskId,
20080
20818
  sourceHandle: input.sourceHandle,
20081
20819
  sourceKind: input.sourceKind,
20082
20820
  coordinatorSession: input.coordinatorSession,
20083
- workerSession: ensuredWorkerSession,
20821
+ workerSession,
20084
20822
  workspace: input.workspace,
20085
20823
  ...input.cwd ? { cwd: input.cwd } : {},
20086
20824
  targetAgent: input.targetAgent,
20087
20825
  ...role ? { role } : {},
20088
20826
  ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
20089
20827
  task: input.task,
20090
- status: "running",
20828
+ status: "queued",
20829
+ ephemeralWorkerSession: true,
20091
20830
  summary: "",
20092
20831
  resultText: "",
20093
20832
  createdAt: now,
20094
20833
  updatedAt: now,
20095
20834
  eventSeq: 1,
20096
- events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
20835
+ events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }],
20097
20836
  ...input.chatKey ? { chatKey: input.chatKey } : {},
20098
20837
  ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
20099
20838
  ...input.accountId ? { accountId: input.accountId } : {}
20100
20839
  };
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,
20840
+ state.orchestration.tasks[taskId] = queuedTask;
20841
+ await this.deps.saveState(state);
20842
+ return { taskId, status: "queued", workerSession };
20843
+ });
20844
+ if (queuedResult) {
20845
+ this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
20846
+ return queuedResult;
20847
+ }
20848
+ }
20849
+ const releasePendingParallelStart = input.parallel ? () => {
20850
+ const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
20851
+ if (count <= 1) {
20852
+ this.pendingParallelStarts.delete(input.targetAgent);
20853
+ } else {
20854
+ this.pendingParallelStarts.set(input.targetAgent, count - 1);
20855
+ }
20856
+ } : undefined;
20857
+ let ensuredWorkerSession = workerSession;
20858
+ let prepared;
20859
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
20860
+ try {
20861
+ try {
20862
+ ensuredWorkerSession = await this.ensureReservedWorkerSession({
20863
+ workerSession,
20864
+ sourceHandle: input.sourceHandle,
20865
+ sourceKind: input.sourceKind,
20117
20866
  coordinatorSession: input.coordinatorSession,
20118
20867
  workspace: input.workspace,
20119
20868
  ...input.cwd ? { cwd: input.cwd } : {},
20120
20869
  targetAgent: input.targetAgent,
20121
20870
  role
20122
- };
20123
- await this.deps.saveState(state);
20124
- return {
20125
- task: { ...task },
20126
- previousBinding,
20127
- previousGroup,
20128
- normalizedGroupId
20129
- };
20130
- });
20131
- } catch (error2) {
20871
+ });
20872
+ prepared = await this.mutate(async () => {
20873
+ const state = await this.deps.loadState();
20874
+ const now = this.deps.now().toISOString();
20875
+ if (normalizedGroupId) {
20876
+ this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
20877
+ }
20878
+ const task = {
20879
+ taskId,
20880
+ sourceHandle: input.sourceHandle,
20881
+ sourceKind: input.sourceKind,
20882
+ coordinatorSession: input.coordinatorSession,
20883
+ workerSession: ensuredWorkerSession,
20884
+ workspace: input.workspace,
20885
+ ...input.cwd ? { cwd: input.cwd } : {},
20886
+ targetAgent: input.targetAgent,
20887
+ ...role ? { role } : {},
20888
+ ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
20889
+ task: input.task,
20890
+ status: "running",
20891
+ summary: "",
20892
+ resultText: "",
20893
+ createdAt: now,
20894
+ updatedAt: now,
20895
+ eventSeq: 1,
20896
+ events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
20897
+ ...input.chatKey ? { chatKey: input.chatKey } : {},
20898
+ ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
20899
+ ...input.accountId ? { accountId: input.accountId } : {},
20900
+ ...input.parallel ? { ephemeralWorkerSession: true } : {}
20901
+ };
20902
+ let previousGroup;
20903
+ if (normalizedGroupId) {
20904
+ const group = this.ensureGroups(state)[normalizedGroupId];
20905
+ previousGroup = { ...group };
20906
+ group.updatedAt = now;
20907
+ group.coordinatorInjectedAt = undefined;
20908
+ group.injectionPending = undefined;
20909
+ group.injectionAppliedAt = undefined;
20910
+ group.lastInjectionError = undefined;
20911
+ }
20912
+ const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
20913
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
20914
+ this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
20915
+ state.orchestration.tasks[taskId] = task;
20916
+ state.orchestration.workerBindings[ensuredWorkerSession] = {
20917
+ sourceHandle: ensuredWorkerSession,
20918
+ coordinatorSession: input.coordinatorSession,
20919
+ workspace: input.workspace,
20920
+ ...input.cwd ? { cwd: input.cwd } : {},
20921
+ targetAgent: input.targetAgent,
20922
+ role,
20923
+ ...input.parallel ? { ephemeral: true } : {}
20924
+ };
20925
+ await this.deps.saveState(state);
20926
+ return {
20927
+ task: { ...task },
20928
+ previousBinding,
20929
+ previousGroup,
20930
+ normalizedGroupId
20931
+ };
20932
+ });
20933
+ } catch (error2) {
20934
+ await releaseWorkerReservation();
20935
+ throw error2;
20936
+ }
20132
20937
  await releaseWorkerReservation();
20133
- throw error2;
20938
+ } finally {
20939
+ releasePendingParallelStart?.();
20134
20940
  }
20135
- await releaseWorkerReservation();
20136
20941
  try {
20137
20942
  await this.deps.dispatchWorkerTask({
20138
20943
  taskId,
@@ -20182,6 +20987,7 @@ class OrchestrationService {
20182
20987
  return { sourceContext, targetLocation, role, normalizedGroupId };
20183
20988
  });
20184
20989
  const autoRun = preflight.sourceContext.sourceKind === "coordinator";
20990
+ const taskId = this.deps.createId();
20185
20991
  const workerSessionName = await this.resolveWorkerSession({
20186
20992
  sourceHandle: input.sourceHandle,
20187
20993
  sourceKind: preflight.sourceContext.sourceKind,
@@ -20190,18 +20996,18 @@ class OrchestrationService {
20190
20996
  ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
20191
20997
  targetAgent: input.targetAgent,
20192
20998
  task: input.task,
20193
- ...preflight.role ? { role: preflight.role } : {}
20999
+ ...preflight.role ? { role: preflight.role } : {},
21000
+ ...input.parallel ? { parallel: true } : {}
20194
21001
  });
20195
- const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
20196
- let prepared;
20197
- try {
20198
- prepared = await this.mutate(async () => {
21002
+ if (input.parallel && autoRun) {
21003
+ const queuedResult = await this.mutate(async () => {
20199
21004
  const state = await this.deps.loadState();
20200
- this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
21005
+ if (this.canStartParallelTask(state, input.targetAgent)) {
21006
+ this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
21007
+ return null;
21008
+ }
20201
21009
  const now = this.deps.now().toISOString();
20202
- const taskId = this.deps.createId();
20203
- const status = autoRun ? "running" : "needs_confirmation";
20204
- const task = {
21010
+ const queuedTask = {
20205
21011
  taskId,
20206
21012
  sourceHandle: input.sourceHandle,
20207
21013
  sourceKind: preflight.sourceContext.sourceKind,
@@ -20213,49 +21019,101 @@ class OrchestrationService {
20213
21019
  ...preflight.role ? { role: preflight.role } : {},
20214
21020
  ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
20215
21021
  task: input.task,
20216
- status,
21022
+ status: "queued",
21023
+ ephemeralWorkerSession: true,
20217
21024
  summary: "",
20218
21025
  resultText: "",
20219
21026
  createdAt: now,
20220
21027
  updatedAt: now,
20221
21028
  eventSeq: 1,
20222
- events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }]
21029
+ events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }]
20223
21030
  };
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,
21031
+ state.orchestration.tasks[taskId] = queuedTask;
21032
+ await this.deps.saveState(state);
21033
+ return { taskId, status: "queued", workerSession: workerSessionName };
21034
+ });
21035
+ if (queuedResult) {
21036
+ this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
21037
+ return queuedResult;
21038
+ }
21039
+ }
21040
+ const releasePendingParallelStart = input.parallel && autoRun ? () => {
21041
+ const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
21042
+ if (count <= 1) {
21043
+ this.pendingParallelStarts.delete(input.targetAgent);
21044
+ } else {
21045
+ this.pendingParallelStarts.set(input.targetAgent, count - 1);
21046
+ }
21047
+ } : undefined;
21048
+ let prepared;
21049
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
21050
+ try {
21051
+ try {
21052
+ prepared = await this.mutate(async () => {
21053
+ const state = await this.deps.loadState();
21054
+ this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
21055
+ const now = this.deps.now().toISOString();
21056
+ const status = autoRun ? "running" : "needs_confirmation";
21057
+ const task = {
21058
+ taskId,
21059
+ sourceHandle: input.sourceHandle,
21060
+ sourceKind: preflight.sourceContext.sourceKind,
20240
21061
  coordinatorSession: preflight.sourceContext.coordinatorSession,
21062
+ workerSession: workerSessionName,
20241
21063
  workspace: preflight.targetLocation.workspace,
20242
21064
  ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
20243
21065
  targetAgent: input.targetAgent,
20244
- role: preflight.role
21066
+ ...preflight.role ? { role: preflight.role } : {},
21067
+ ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
21068
+ task: input.task,
21069
+ status,
21070
+ summary: "",
21071
+ resultText: "",
21072
+ createdAt: now,
21073
+ updatedAt: now,
21074
+ eventSeq: 1,
21075
+ events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }],
21076
+ ...input.parallel ? { ephemeralWorkerSession: true } : {}
20245
21077
  };
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) {
21078
+ if (preflight.normalizedGroupId) {
21079
+ const group = this.ensureGroups(state)[preflight.normalizedGroupId];
21080
+ group.updatedAt = now;
21081
+ group.coordinatorInjectedAt = undefined;
21082
+ group.injectionPending = undefined;
21083
+ group.injectionAppliedAt = undefined;
21084
+ group.lastInjectionError = undefined;
21085
+ }
21086
+ let previousBinding;
21087
+ if (autoRun) {
21088
+ previousBinding = state.orchestration.workerBindings[workerSessionName];
21089
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
21090
+ this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
21091
+ state.orchestration.tasks[taskId] = task;
21092
+ state.orchestration.workerBindings[workerSessionName] = {
21093
+ sourceHandle: workerSessionName,
21094
+ coordinatorSession: preflight.sourceContext.coordinatorSession,
21095
+ workspace: preflight.targetLocation.workspace,
21096
+ ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
21097
+ targetAgent: input.targetAgent,
21098
+ role: preflight.role,
21099
+ ...input.parallel ? { ephemeral: true } : {}
21100
+ };
21101
+ } else {
21102
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
21103
+ this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
21104
+ state.orchestration.tasks[taskId] = task;
21105
+ }
21106
+ await this.deps.saveState(state);
21107
+ return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
21108
+ });
21109
+ } catch (error2) {
21110
+ await releaseWorkerReservation();
21111
+ throw error2;
21112
+ }
20255
21113
  await releaseWorkerReservation();
20256
- throw error2;
21114
+ } finally {
21115
+ releasePendingParallelStart?.();
20257
21116
  }
20258
- await releaseWorkerReservation();
20259
21117
  if (autoRun) {
20260
21118
  this.runAutoRunRpcWorkerTask({
20261
21119
  task: prepared.task,
@@ -21224,6 +22082,16 @@ class OrchestrationService {
21224
22082
  await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
21225
22083
  }
21226
22084
  }
22085
+ if (input.decision === "accept") {
22086
+ try {
22087
+ await this.reconcileParallelSlots();
22088
+ } catch (error2) {
22089
+ this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after contested result accepted", {
22090
+ taskId: prepared.task.taskId,
22091
+ message: error2 instanceof Error ? error2.message : String(error2)
22092
+ });
22093
+ }
22094
+ }
21227
22095
  return prepared.task;
21228
22096
  }
21229
22097
  async listTasks(filter) {
@@ -21690,6 +22558,16 @@ class OrchestrationService {
21690
22558
  if (prepared.closedPackageId) {
21691
22559
  await this.handoffQueuedQuestions(prepared.task.coordinatorSession, prepared.closedPackageId);
21692
22560
  }
22561
+ if (!prepared.shouldPropagate && this.isTerminalStatus(prepared.task.status)) {
22562
+ try {
22563
+ await this.reconcileParallelSlots();
22564
+ } catch (error2) {
22565
+ this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after non-running cancel", {
22566
+ taskId: prepared.task.taskId,
22567
+ message: error2 instanceof Error ? error2.message : String(error2)
22568
+ });
22569
+ }
22570
+ }
21693
22571
  return prepared.task;
21694
22572
  }
21695
22573
  async completeTaskCancellation(taskId) {
@@ -21752,6 +22630,14 @@ class OrchestrationService {
21752
22630
  return prepared.task;
21753
22631
  }
21754
22632
  this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", this.taskContext(prepared.task));
22633
+ try {
22634
+ await this.reconcileParallelSlots();
22635
+ } catch (error2) {
22636
+ this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after cancel completion", {
22637
+ taskId: prepared.task.taskId,
22638
+ message: error2 instanceof Error ? error2.message : String(error2)
22639
+ });
22640
+ }
21755
22641
  return prepared.task;
21756
22642
  }
21757
22643
  async failTaskCancellation(taskId, errorMessage) {
@@ -21796,6 +22682,34 @@ class OrchestrationService {
21796
22682
  task: currentTask.task,
21797
22683
  ...currentTask.role ? { role: currentTask.role } : {}
21798
22684
  });
22685
+ if (currentTask.ephemeralWorkerSession === true) {
22686
+ const queuedResult = await this.mutate(async () => {
22687
+ const state = await this.deps.loadState();
22688
+ const task = state.orchestration.tasks[input.taskId];
22689
+ if (!task) {
22690
+ throw new Error(`task "${input.taskId}" does not exist`);
22691
+ }
22692
+ this.assertCoordinatorOwnership(task, input.coordinatorSession);
22693
+ this.assertNeedsConfirmation(task);
22694
+ if (this.canStartParallelTask(state, task.targetAgent)) {
22695
+ return null;
22696
+ }
22697
+ const now = this.deps.now().toISOString();
22698
+ task.workerSession = workerSession;
22699
+ task.status = "queued";
22700
+ task.updatedAt = now;
22701
+ this.appendTaskEvent(task, now, "status_changed", {
22702
+ status: "queued",
22703
+ message: "Task queued at parallel capacity"
22704
+ });
22705
+ await this.deps.saveState(state);
22706
+ return { ...task };
22707
+ });
22708
+ if (queuedResult) {
22709
+ this.logEvent("orchestration.task.queued", "parallel task queued at capacity on approve", { taskId: input.taskId, targetAgent: currentTask.targetAgent });
22710
+ return queuedResult;
22711
+ }
22712
+ }
21799
22713
  const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession, input.taskId);
21800
22714
  let ensuredWorkerSession = workerSession;
21801
22715
  let prepared;
@@ -21837,7 +22751,8 @@ class OrchestrationService {
21837
22751
  workspace: task.workspace,
21838
22752
  ...task.cwd ? { cwd: task.cwd } : {},
21839
22753
  targetAgent: task.targetAgent,
21840
- role: task.role
22754
+ role: task.role,
22755
+ ...task.ephemeralWorkerSession ? { ephemeral: true } : {}
21841
22756
  };
21842
22757
  await this.deps.saveState(state);
21843
22758
  return {
@@ -21891,6 +22806,10 @@ class OrchestrationService {
21891
22806
  }
21892
22807
  async resolveWorkerSession(input) {
21893
22808
  const role = this.normalizeRole(input.role);
22809
+ 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(":");
22810
+ if (input.parallel) {
22811
+ return `${baseName}:p-${this.deps.createId()}`;
22812
+ }
21894
22813
  const reusable = await this.deps.findReusableWorkerSession?.({
21895
22814
  sourceHandle: input.sourceHandle,
21896
22815
  sourceKind: input.sourceKind,
@@ -21903,7 +22822,7 @@ class OrchestrationService {
21903
22822
  if (reusable && reusable.trim().length > 0) {
21904
22823
  return reusable.trim();
21905
22824
  }
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(":");
22825
+ return baseName;
21907
22826
  }
21908
22827
  async reserveProposedWorkerSession(workerSession, excludingTaskId) {
21909
22828
  await this.mutate(async () => {
@@ -22061,7 +22980,7 @@ class OrchestrationService {
22061
22980
  if (role && policy.allowedAgentRequestRoles.length > 0 && !policy.allowedAgentRequestRoles.includes(role)) {
22062
22981
  throw new Error(`role "${role}" is not allowed for agent-requested delegation`);
22063
22982
  }
22064
- const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running"));
22983
+ 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
22984
  if (outstandingRequests.length >= policy.maxPendingAgentRequestsPerCoordinator) {
22066
22985
  throw new Error("agent-requested delegation quota exceeded for this coordinator");
22067
22986
  }
@@ -22226,6 +23145,137 @@ class OrchestrationService {
22226
23145
  hasActiveTaskWorkerSession(state, workerSession, excludingTaskId) {
22227
23146
  return Object.values(state.orchestration.tasks).some((task) => task.taskId !== excludingTaskId && task.workerSession === workerSession && (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined));
22228
23147
  }
23148
+ countActiveParallelSlots(state, targetAgent) {
23149
+ 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;
23150
+ const pending = this.pendingParallelStarts.get(targetAgent) ?? 0;
23151
+ return persisted + pending;
23152
+ }
23153
+ canStartParallelTask(state, targetAgent) {
23154
+ const cap = this.deps.config.orchestration.maxParallelTasksPerAgent;
23155
+ return this.countActiveParallelSlots(state, targetAgent) < cap;
23156
+ }
23157
+ async reconcileParallelSlots() {
23158
+ const toClose = await this.mutate(async () => {
23159
+ const state = await this.deps.loadState();
23160
+ const collected = [];
23161
+ for (const task of Object.values(state.orchestration.tasks)) {
23162
+ if (task.ephemeralWorkerSession === true && task.ephemeralWorkerSessionClosed !== true && task.workerSession && task.reviewPending === undefined && this.isTerminalStatus(task.status)) {
23163
+ task.ephemeralWorkerSessionClosed = true;
23164
+ if (state.orchestration.workerBindings[task.workerSession] !== undefined) {
23165
+ delete state.orchestration.workerBindings[task.workerSession];
23166
+ collected.push({
23167
+ workerSession: task.workerSession,
23168
+ coordinatorSession: task.coordinatorSession,
23169
+ workspace: task.workspace,
23170
+ ...task.cwd ? { cwd: task.cwd } : {},
23171
+ targetAgent: task.targetAgent,
23172
+ ...task.role ? { role: task.role } : {}
23173
+ });
23174
+ }
23175
+ }
23176
+ }
23177
+ if (collected.length > 0) {
23178
+ await this.deps.saveState(state);
23179
+ }
23180
+ return collected;
23181
+ });
23182
+ for (const req of toClose) {
23183
+ try {
23184
+ await this.deps.closeWorkerSession?.(req);
23185
+ } catch (error2) {
23186
+ this.logEvent("orchestration.parallel.close_failed", "failed to close ephemeral worker session", {
23187
+ workerSession: req.workerSession,
23188
+ message: error2 instanceof Error ? error2.message : String(error2)
23189
+ });
23190
+ }
23191
+ }
23192
+ for (;; ) {
23193
+ const next = await this.mutate(async () => {
23194
+ const state = await this.deps.loadState();
23195
+ const queued = Object.values(state.orchestration.tasks).filter((t) => t.status === "queued" && t.ephemeralWorkerSession === true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
23196
+ for (const task of queued) {
23197
+ if (!this.canStartParallelTask(state, task.targetAgent)) {
23198
+ continue;
23199
+ }
23200
+ task.status = "running";
23201
+ task.updatedAt = this.deps.now().toISOString();
23202
+ state.orchestration.workerBindings[task.workerSession] = {
23203
+ sourceHandle: task.workerSession,
23204
+ coordinatorSession: task.coordinatorSession,
23205
+ workspace: task.workspace,
23206
+ ...task.cwd ? { cwd: task.cwd } : {},
23207
+ targetAgent: task.targetAgent,
23208
+ ...task.role ? { role: task.role } : {},
23209
+ ephemeral: true
23210
+ };
23211
+ await this.deps.saveState(state);
23212
+ return { ...task };
23213
+ }
23214
+ return null;
23215
+ });
23216
+ if (!next) {
23217
+ break;
23218
+ }
23219
+ try {
23220
+ await this.ensureReservedWorkerSession({
23221
+ workerSession: next.workerSession,
23222
+ sourceHandle: next.sourceHandle,
23223
+ sourceKind: next.sourceKind,
23224
+ coordinatorSession: next.coordinatorSession,
23225
+ workspace: next.workspace,
23226
+ ...next.cwd ? { cwd: next.cwd } : {},
23227
+ targetAgent: next.targetAgent,
23228
+ ...next.role ? { role: next.role } : {}
23229
+ });
23230
+ await this.deps.dispatchWorkerTask({
23231
+ taskId: next.taskId,
23232
+ workerSession: next.workerSession,
23233
+ coordinatorSession: next.coordinatorSession,
23234
+ workspace: next.workspace,
23235
+ ...next.cwd ? { cwd: next.cwd } : {},
23236
+ targetAgent: next.targetAgent,
23237
+ ...next.role ? { role: next.role } : {},
23238
+ task: next.task
23239
+ });
23240
+ } catch (error2) {
23241
+ await this.mutate(async () => {
23242
+ const state = await this.deps.loadState();
23243
+ const task = state.orchestration.tasks[next.taskId];
23244
+ if (task && task.status === "running") {
23245
+ task.status = "queued";
23246
+ task.updatedAt = this.deps.now().toISOString();
23247
+ delete state.orchestration.workerBindings[next.workerSession];
23248
+ this.appendTaskEvent(task, task.updatedAt, "status_changed", {
23249
+ status: "queued",
23250
+ message: "Task re-queued after drain failure"
23251
+ });
23252
+ await this.deps.saveState(state);
23253
+ }
23254
+ });
23255
+ this.logEvent("orchestration.parallel.drain_failed", "failed to drain queued parallel task", {
23256
+ taskId: next.taskId,
23257
+ workerSession: next.workerSession,
23258
+ message: error2 instanceof Error ? error2.message : String(error2)
23259
+ });
23260
+ break;
23261
+ }
23262
+ await this.mutate(async () => {
23263
+ const state = await this.deps.loadState();
23264
+ const task = state.orchestration.tasks[next.taskId];
23265
+ if (task && task.status === "running") {
23266
+ this.appendTaskEvent(task, task.updatedAt, "status_changed", {
23267
+ status: "running",
23268
+ message: "Task drained from parallel queue"
23269
+ });
23270
+ await this.deps.saveState(state);
23271
+ }
23272
+ });
23273
+ this.logEvent("orchestration.task.drained", "parallel task drained from queue", {
23274
+ taskId: next.taskId,
23275
+ targetAgent: next.targetAgent
23276
+ });
23277
+ }
23278
+ }
22229
23279
  async assertProposedWorkerSessionDoesNotConflictExternalCoordinator(workerSession) {
22230
23280
  const state = await this.deps.loadState();
22231
23281
  this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
@@ -23236,6 +24286,13 @@ async function runConsole(paths, deps) {
23236
24286
  trigger: "startup"
23237
24287
  });
23238
24288
  } catch {}
24289
+ try {
24290
+ await runtime.orchestration.service.reconcileParallelSlots();
24291
+ } catch (reconcileError) {
24292
+ await runtime.logger.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots at startup", {
24293
+ message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
24294
+ });
24295
+ }
23239
24296
  consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
23240
24297
  if (consumerLock) {
23241
24298
  const lockMeta = {
@@ -23994,7 +25051,7 @@ var init_spawn_command = __esm(() => {
23994
25051
  // src/transport/prompt-media.ts
23995
25052
  import { mkdtemp, open as open3, rm as rm8, writeFile as writeFile8 } from "node:fs/promises";
23996
25053
  import { tmpdir as defaultTmpdir } from "node:os";
23997
- import path13 from "node:path";
25054
+ import path14 from "node:path";
23998
25055
  import { pathToFileURL as pathToFileURL2 } from "node:url";
23999
25056
  async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
24000
25057
  const mediaList = normalizePromptMedia(media);
@@ -24028,7 +25085,7 @@ async function createStructuredPromptFile(text, media, deps = defaultStructuredP
24028
25085
  type: "resource",
24029
25086
  resource: {
24030
25087
  uri: pathToFileURL2(item.filePath).toString(),
24031
- text: `${item.fileName ?? path13.basename(item.filePath)} ${item.mimeType} ${item.type}`
25088
+ text: `${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.type}`
24032
25089
  }
24033
25090
  });
24034
25091
  }
@@ -24042,7 +25099,7 @@ function normalizePromptMedia(media) {
24042
25099
  function buildAttachmentSummary(items) {
24043
25100
  const lines = ["Attachments available as local files:"];
24044
25101
  for (const [index, item] of items.entries()) {
24045
- lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path13.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
25102
+ lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
24046
25103
  }
24047
25104
  return lines.join(`
24048
25105
  `);
@@ -24050,8 +25107,8 @@ function buildAttachmentSummary(items) {
24050
25107
  async function writeStructuredPromptBlocks(blocks, deps) {
24051
25108
  let dir = "";
24052
25109
  try {
24053
- dir = await deps.mkdtemp(path13.join(deps.tmpdir(), "weacpx-acp-prompt-"));
24054
- const filePath = path13.join(dir, "prompt.json");
25110
+ dir = await deps.mkdtemp(path14.join(deps.tmpdir(), "weacpx-acp-prompt-"));
25111
+ const filePath = path14.join(dir, "prompt.json");
24055
25112
  await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
24056
25113
  return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
24057
25114
  } catch (error2) {
@@ -25595,6 +26652,14 @@ async function buildApp(paths, deps = {}) {
25595
26652
  resultText: ""
25596
26653
  });
25597
26654
  }
26655
+ try {
26656
+ await orchestration.reconcileParallelSlots();
26657
+ } catch (reconcileError) {
26658
+ await logger2.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots after worker turn", {
26659
+ taskId: input.taskId,
26660
+ message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
26661
+ });
26662
+ }
25598
26663
  if (taskRecord && shouldNotifyTaskCompletion(taskRecord)) {
25599
26664
  try {
25600
26665
  await sendCompletionNotice(taskRecord);
@@ -25662,6 +26727,13 @@ async function buildApp(paths, deps = {}) {
25662
26727
  throw new Error(result.message || "worker task cancel was not acknowledged");
25663
26728
  }
25664
26729
  },
26730
+ closeWorkerSession: async ({ workerSession, targetAgent, workspace, cwd }) => {
26731
+ if (!transport.removeSession) {
26732
+ return;
26733
+ }
26734
+ const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
26735
+ await transport.removeSession(session);
26736
+ },
25665
26737
  resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, cwd, answer }) => {
25666
26738
  launchWorkerTurn({
25667
26739
  taskId,
@@ -25687,7 +26759,7 @@ async function buildApp(paths, deps = {}) {
25687
26759
  }
25688
26760
  },
25689
26761
  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);
26762
+ 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
26763
  return binding?.[0] ?? null;
25692
26764
  },
25693
26765
  logger: logger2
@@ -26247,107 +27319,107 @@ async function checkRuntime(options = {}) {
26247
27319
  }
26248
27320
  function createRuntimeFsProbe() {
26249
27321
  return {
26250
- stat: async (path14) => await stat3(path14),
26251
- access: async (path14, mode) => await access4(path14, mode)
27322
+ stat: async (path15) => await stat3(path15),
27323
+ access: async (path15, mode) => await access4(path15, mode)
26252
27324
  };
26253
27325
  }
26254
- async function checkDirectoryCreatable(label, path14, probe, platform) {
27326
+ async function checkDirectoryCreatable(label, path15, probe, platform) {
26255
27327
  try {
26256
- const stats = await probe.stat(path14);
27328
+ const stats = await probe.stat(path15);
26257
27329
  if (!stats.isDirectory()) {
26258
27330
  return {
26259
27331
  ok: false,
26260
- detail: `${label}: ${path14} (exists but is not a directory)`
27332
+ detail: `${label}: ${path15} (exists but is not a directory)`
26261
27333
  };
26262
27334
  }
26263
- await probe.access(path14, directoryAccessMode(platform));
27335
+ await probe.access(path15, directoryAccessMode(platform));
26264
27336
  return {
26265
27337
  ok: true,
26266
- detail: `${label}: ${path14} (writable)`
27338
+ detail: `${label}: ${path15} (writable)`
26267
27339
  };
26268
27340
  } catch (error2) {
26269
27341
  if (!isMissingPathError(error2)) {
26270
27342
  return {
26271
27343
  ok: false,
26272
- detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
27344
+ detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
26273
27345
  };
26274
27346
  }
26275
- const parentCheck = await checkCreatableAncestorDirectory(path14, probe, platform);
27347
+ const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
26276
27348
  if (!parentCheck.ok) {
26277
27349
  return {
26278
27350
  ok: false,
26279
- detail: `${label}: ${path14} (parent not writable: ${parentCheck.blockingPath})`
27351
+ detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
26280
27352
  };
26281
27353
  }
26282
27354
  return {
26283
27355
  ok: true,
26284
- detail: `${label}: ${path14} (creatable via ${parentCheck.creatableFrom})`
27356
+ detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
26285
27357
  };
26286
27358
  }
26287
27359
  }
26288
- async function checkFileCreatable(label, path14, probe, platform) {
27360
+ async function checkFileCreatable(label, path15, probe, platform) {
26289
27361
  try {
26290
- const stats = await probe.stat(path14);
27362
+ const stats = await probe.stat(path15);
26291
27363
  if (stats.isDirectory()) {
26292
27364
  return {
26293
27365
  ok: false,
26294
- detail: `${label}: ${path14} (exists but is a directory)`
27366
+ detail: `${label}: ${path15} (exists but is a directory)`
26295
27367
  };
26296
27368
  }
26297
- await probe.access(path14, constants.W_OK);
27369
+ await probe.access(path15, constants.W_OK);
26298
27370
  return {
26299
27371
  ok: true,
26300
- detail: `${label}: ${path14} (writable)`
27372
+ detail: `${label}: ${path15} (writable)`
26301
27373
  };
26302
27374
  } catch (error2) {
26303
27375
  if (!isMissingPathError(error2)) {
26304
27376
  return {
26305
27377
  ok: false,
26306
- detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
27378
+ detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
26307
27379
  };
26308
27380
  }
26309
- const parentCheck = await checkCreatableAncestorDirectory(dirname14(path14), probe, platform);
27381
+ const parentCheck = await checkCreatableAncestorDirectory(dirname14(path15), probe, platform);
26310
27382
  if (!parentCheck.ok) {
26311
27383
  return {
26312
27384
  ok: false,
26313
- detail: `${label}: ${path14} (parent not writable: ${parentCheck.blockingPath})`
27385
+ detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
26314
27386
  };
26315
27387
  }
26316
27388
  return {
26317
27389
  ok: true,
26318
- detail: `${label}: ${path14} (creatable via ${parentCheck.creatableFrom})`
27390
+ detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
26319
27391
  };
26320
27392
  }
26321
27393
  }
26322
- async function checkCreatableAncestorDirectory(path14, probe, platform) {
27394
+ async function checkCreatableAncestorDirectory(path15, probe, platform) {
26323
27395
  try {
26324
- const stats = await probe.stat(path14);
27396
+ const stats = await probe.stat(path15);
26325
27397
  if (!stats.isDirectory()) {
26326
27398
  return {
26327
27399
  ok: false,
26328
- creatableFrom: path14,
26329
- blockingPath: path14
27400
+ creatableFrom: path15,
27401
+ blockingPath: path15
26330
27402
  };
26331
27403
  }
26332
- await probe.access(path14, directoryAccessMode(platform));
27404
+ await probe.access(path15, directoryAccessMode(platform));
26333
27405
  return {
26334
27406
  ok: true,
26335
- creatableFrom: path14
27407
+ creatableFrom: path15
26336
27408
  };
26337
27409
  } catch (error2) {
26338
27410
  if (!isMissingPathError(error2)) {
26339
27411
  return {
26340
27412
  ok: false,
26341
- creatableFrom: path14,
26342
- blockingPath: path14
27413
+ creatableFrom: path15,
27414
+ blockingPath: path15
26343
27415
  };
26344
27416
  }
26345
- const parent = dirname14(path14);
26346
- if (parent === path14) {
27417
+ const parent = dirname14(path15);
27418
+ if (parent === path15) {
26347
27419
  return {
26348
27420
  ok: false,
26349
- creatableFrom: path14,
26350
- blockingPath: path14
27421
+ creatableFrom: path15,
27422
+ blockingPath: path15
26351
27423
  };
26352
27424
  }
26353
27425
  const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
@@ -39393,7 +40465,8 @@ function buildWeacpxMcpToolRegistry(input) {
39393
40465
  task: exports_external.string().min(1),
39394
40466
  workingDirectory: exports_external.string().min(1).optional(),
39395
40467
  role: exports_external.string().min(1).optional(),
39396
- groupId: exports_external.string().min(1).optional()
40468
+ groupId: exports_external.string().min(1).optional(),
40469
+ 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
40470
  }).strict(),
39398
40471
  handler: async (args) => await asToolResult(async () => {
39399
40472
  const input2 = args;
@@ -39414,7 +40487,8 @@ function buildWeacpxMcpToolRegistry(input) {
39414
40487
  targetAgent: exports_external.string().min(1),
39415
40488
  task: exports_external.string().min(1),
39416
40489
  workingDirectory: exports_external.string().min(1).optional(),
39417
- role: exports_external.string().min(1).optional()
40490
+ role: exports_external.string().min(1).optional(),
40491
+ 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
40492
  }).strict()).min(1)
39419
40493
  }).strict(),
39420
40494
  handler: async (args) => await asToolResult(async () => {
@@ -39433,7 +40507,8 @@ function buildWeacpxMcpToolRegistry(input) {
39433
40507
  task: entry.task,
39434
40508
  ...entry.workingDirectory ? { workingDirectory: entry.workingDirectory } : {},
39435
40509
  ...entry.role ? { role: entry.role } : {},
39436
- ...groupId ? { groupId } : {}
40510
+ ...groupId ? { groupId } : {},
40511
+ ...entry.parallel !== undefined ? { parallel: entry.parallel } : {}
39437
40512
  });
39438
40513
  results.push({ index, taskId: result.taskId, status: result.status });
39439
40514
  } catch (error2) {
@@ -39657,7 +40732,7 @@ function createErrorResult(message) {
39657
40732
  };
39658
40733
  }
39659
40734
  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.`;
40735
+ 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
40736
  return [`Delegation task "${result.taskId}" created.`, `- Status: ${result.status}`, next].join(`
39662
40737
  `);
39663
40738
  }
@@ -39928,7 +41003,8 @@ function createOrchestrationTransport(endpoint, deps = {}) {
39928
41003
  task: input.task,
39929
41004
  ...input.workingDirectory !== undefined ? { cwd: input.workingDirectory } : {},
39930
41005
  ...input.role !== undefined ? { role: input.role } : {},
39931
- ...input.groupId !== undefined ? { groupId: input.groupId } : {}
41006
+ ...input.groupId !== undefined ? { groupId: input.groupId } : {},
41007
+ ...input.parallel !== undefined ? { parallel: input.parallel } : {}
39932
41008
  }),
39933
41009
  createGroup: async (input) => await client.createGroup(input),
39934
41010
  getTask: async (input) => await client.getTaskForCoordinator(input),
@@ -40641,10 +41717,12 @@ function parseSourceHandle(args, env = process.env) {
40641
41717
 
40642
41718
  // src/cli.ts
40643
41719
  init_workspace_path();
41720
+ init_workspace_name();
40644
41721
  init_state_store();
40645
41722
 
40646
41723
  // src/onboarding.ts
40647
41724
  init_workspace_path();
41725
+ init_workspace_name();
40648
41726
  init_agent_templates();
40649
41727
  function isFirstUse(config2, state) {
40650
41728
  return Object.keys(state.sessions ?? {}).length === 0 && Object.keys(config2.workspaces ?? {}).length === 0 && (config2.plugins ?? []).length === 0;
@@ -40655,7 +41733,7 @@ async function maybeRunFirstUseOnboarding(input) {
40655
41733
  if (!input.deps.isInteractive())
40656
41734
  return { created: false };
40657
41735
  const cwd = normalizeWorkspacePath(input.deps.cwd());
40658
- const workspaceName = allocateName(sanitizeName(basenameForWorkspacePath(cwd), "workspace"), input.config.workspaces);
41736
+ const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), input.config.workspaces);
40659
41737
  const yes = (await input.deps.promptText(`检测到首次使用 weacpx。是否将当前目录创建为工作区「${workspaceName}」?[Y/n] `)).trim().toLowerCase();
40660
41738
  if (yes === "n" || yes === "no")
40661
41739
  return { created: false };
@@ -40696,18 +41774,6 @@ function resolveTemplateChoice(answer, names) {
40696
41774
  return names[index - 1];
40697
41775
  return names.includes(answer) ? answer : null;
40698
41776
  }
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
41777
 
40712
41778
  // src/cli-update.ts
40713
41779
  init_plugin_home();
@@ -42371,7 +43437,7 @@ var HELP_LINES = [
42371
43437
  "weacpx doctor - 运行诊断",
42372
43438
  "weacpx version - 查看版本",
42373
43439
  "weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
42374
- "weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
43440
+ "weacpx workspace list|add [name] [--raw]|rm <name> - 管理本机工作区(别名:ws)",
42375
43441
  "weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
42376
43442
  ];
42377
43443
  function getUsageText() {
@@ -42635,10 +43701,23 @@ async function handleWorkspaceCli(args, deps) {
42635
43701
  if (args.length !== 1)
42636
43702
  return null;
42637
43703
  return await workspaceList(deps.print);
42638
- case "add":
42639
- if (args.length > 2)
42640
- return null;
42641
- return await workspaceAdd(args[1], deps);
43704
+ case "add": {
43705
+ const rest = args.slice(1);
43706
+ let rawFlag = false;
43707
+ let explicit;
43708
+ for (const token of rest) {
43709
+ if (token === "--raw") {
43710
+ if (rawFlag)
43711
+ return null;
43712
+ rawFlag = true;
43713
+ continue;
43714
+ }
43715
+ if (explicit !== undefined)
43716
+ return null;
43717
+ explicit = token;
43718
+ }
43719
+ return await workspaceAdd(explicit, { ...deps, raw: rawFlag });
43720
+ }
42642
43721
  case "rm":
42643
43722
  if (args.length !== 2 || !args[1])
42644
43723
  return null;
@@ -42663,13 +43742,20 @@ async function workspaceList(print) {
42663
43742
  }
42664
43743
  async function workspaceAdd(rawName, deps) {
42665
43744
  const cwd = normalizeWorkspacePath(deps.cwd());
42666
- const name = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
42667
- if (name.trim().length === 0) {
43745
+ const input = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
43746
+ if (input.length === 0) {
42668
43747
  deps.print("工作区名称不能为空。");
42669
43748
  return 1;
42670
43749
  }
42671
43750
  const store = await createCliConfigStore();
42672
43751
  const config2 = await store.load();
43752
+ let name = input;
43753
+ if (!deps.raw && !isWorkspaceNameValid(input)) {
43754
+ const base = sanitizeWorkspaceName(input);
43755
+ name = allocateWorkspaceName(base, config2.workspaces);
43756
+ const sourceLabel = rawName === undefined ? "目录名" : "名称";
43757
+ deps.print(`${sourceLabel} ${JSON.stringify(input)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`);
43758
+ }
42673
43759
  const existing = config2.workspaces[name];
42674
43760
  if (existing) {
42675
43761
  if (sameWorkspacePath(existing.cwd, cwd)) {
@@ -42677,7 +43763,7 @@ async function workspaceAdd(rawName, deps) {
42677
43763
  return 0;
42678
43764
  }
42679
43765
  deps.print(`工作区「${name}」已存在,但路径不同:${existing.cwd}`);
42680
- deps.print(`请换一个名称,或先执行:weacpx workspace rm ${name}`);
43766
+ deps.print(`请换一个名称,或先执行:weacpx workspace rm ${quoteWorkspaceNameIfNeeded(name)}`);
42681
43767
  return 1;
42682
43768
  }
42683
43769
  await store.upsertWorkspace(name, cwd);