weacpx 0.4.6 → 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
@@ -2201,6 +2201,11 @@ function parseConfig(raw, options = {}) {
2201
2201
  if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
2202
2202
  throw new Error("transport.nonInteractivePermissions must be deny or fail");
2203
2203
  }
2204
+ if ("permissionPolicy" in transport && transport.permissionPolicy !== undefined) {
2205
+ if (typeof transport.permissionPolicy !== "string" || transport.permissionPolicy.trim().length === 0) {
2206
+ throw new Error("transport.permissionPolicy must be a non-empty string");
2207
+ }
2208
+ }
2204
2209
  if (!isRecord(raw.agents)) {
2205
2210
  throw new Error("agents must be an object");
2206
2211
  }
@@ -2294,6 +2299,7 @@ function parseConfig(raw, options = {}) {
2294
2299
  transport: {
2295
2300
  ...typeof transport.command === "string" ? { command: transport.command } : {},
2296
2301
  ...typeof transport.sessionInitTimeoutMs === "number" ? { sessionInitTimeoutMs: transport.sessionInitTimeoutMs } : {},
2302
+ ...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
2297
2303
  type: transportType,
2298
2304
  permissionMode,
2299
2305
  nonInteractivePermissions
@@ -2418,7 +2424,8 @@ function parseOrchestrationConfig(raw) {
2418
2424
  allowWorkerChainedRequests: raw.allowWorkerChainedRequests === true,
2419
2425
  allowedAgentRequestTargets: Array.isArray(raw.allowedAgentRequestTargets) ? raw.allowedAgentRequestTargets.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestTargets],
2420
2426
  allowedAgentRequestRoles: Array.isArray(raw.allowedAgentRequestRoles) ? raw.allowedAgentRequestRoles.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestRoles],
2421
- 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
2422
2429
  };
2423
2430
  }
2424
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;
@@ -2446,7 +2453,8 @@ var init_load_config = __esm(() => {
2446
2453
  allowWorkerChainedRequests: false,
2447
2454
  allowedAgentRequestTargets: [],
2448
2455
  allowedAgentRequestRoles: [],
2449
- progressHeartbeatSeconds: 300
2456
+ progressHeartbeatSeconds: 300,
2457
+ maxParallelTasksPerAgent: 3
2450
2458
  };
2451
2459
  });
2452
2460
 
@@ -3096,7 +3104,7 @@ var init_orchestration_ipc = () => {};
3096
3104
  // src/daemon/daemon-files.ts
3097
3105
  import { dirname as dirname4, join as join2 } from "node:path";
3098
3106
  function resolveDaemonPaths(options) {
3099
- const runtimeDir = options.runtimeDir ?? join2(options.home, ".weacpx", "runtime");
3107
+ const runtimeDir = options.runtimeDir ?? (options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : join2(options.home, ".weacpx", "runtime"));
3100
3108
  return {
3101
3109
  runtimeDir,
3102
3110
  pidFile: join2(runtimeDir, "daemon.pid"),
@@ -9639,6 +9647,34 @@ var init_quota_errors = __esm(() => {
9639
9647
  };
9640
9648
  });
9641
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
+
9642
9678
  // src/orchestration/orchestration-types.ts
9643
9679
  function createEmptyOrchestrationState() {
9644
9680
  return {
@@ -9677,7 +9713,7 @@ function isOptionalBoolean(value) {
9677
9713
  return value === undefined || typeof value === "boolean";
9678
9714
  }
9679
9715
  function isTaskStatus(value) {
9680
- 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";
9681
9717
  }
9682
9718
  function isSourceKind(value) {
9683
9719
  return value === "human" || value === "coordinator" || value === "worker";
@@ -9713,7 +9749,7 @@ function isTaskRecord(value) {
9713
9749
  if (!isRecord2(value)) {
9714
9750
  return false;
9715
9751
  }
9716
- 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));
9717
9753
  }
9718
9754
  function isExternalCoordinatorRecord(value) {
9719
9755
  if (!isRecord2(value)) {
@@ -9725,7 +9761,7 @@ function isWorkerBindingRecord(value) {
9725
9761
  if (!isRecord2(value)) {
9726
9762
  return false;
9727
9763
  }
9728
- 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);
9729
9765
  }
9730
9766
  function isGroupRecord(value) {
9731
9767
  if (!isRecord2(value)) {
@@ -9981,12 +10017,25 @@ var init_state_store = __esm(() => {
9981
10017
  import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
9982
10018
  import { homedir as homedir3 } from "node:os";
9983
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
+ }
9984
10031
  function resolvePluginHome(input = {}) {
9985
- if (input.pluginHome?.trim())
9986
- return input.pluginHome;
9987
- if (process.env.WEACPX_PLUGIN_HOME?.trim())
9988
- return process.env.WEACPX_PLUGIN_HOME;
9989
- 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();
9990
10039
  return join3(home, ".weacpx", "plugins");
9991
10040
  }
9992
10041
  async function ensurePluginHome(pluginHome) {
@@ -10044,6 +10093,31 @@ function resolveWeixinStateDir() {
10044
10093
  function resolveAccountIndexPath() {
10045
10094
  return path4.join(resolveWeixinStateDir(), "accounts.json");
10046
10095
  }
10096
+ function listAccountFileIds() {
10097
+ const dir = resolveAccountsDir();
10098
+ try {
10099
+ if (!fs3.existsSync(dir))
10100
+ return [];
10101
+ return fs3.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => ({
10102
+ id: entry.name.slice(0, -5),
10103
+ data: readAccountFile(path4.join(dir, entry.name))
10104
+ })).filter((entry) => entry.id.trim() !== "" && Boolean(entry.data?.token?.trim())).map((entry) => entry.id).sort();
10105
+ } catch {
10106
+ return [];
10107
+ }
10108
+ }
10109
+ function uniqueAccountIds(ids) {
10110
+ const seen = new Set;
10111
+ const result = [];
10112
+ for (const raw of ids) {
10113
+ const id = raw.trim();
10114
+ if (!id || seen.has(id))
10115
+ continue;
10116
+ seen.add(id);
10117
+ result.push(id);
10118
+ }
10119
+ return result;
10120
+ }
10047
10121
  function listIndexedWeixinAccountIds() {
10048
10122
  const filePath = resolveAccountIndexPath();
10049
10123
  try {
@@ -10053,7 +10127,7 @@ function listIndexedWeixinAccountIds() {
10053
10127
  const parsed = JSON.parse(raw);
10054
10128
  if (!Array.isArray(parsed))
10055
10129
  return [];
10056
- return parsed.filter((id) => typeof id === "string" && id.trim() !== "");
10130
+ return uniqueAccountIds(parsed.filter((id) => typeof id === "string" && id.trim() !== ""));
10057
10131
  } catch {
10058
10132
  return [];
10059
10133
  }
@@ -10128,7 +10202,10 @@ function clearWeixinAccount(accountId) {
10128
10202
  } catch {}
10129
10203
  }
10130
10204
  function clearAllWeixinAccounts() {
10131
- const ids = listIndexedWeixinAccountIds();
10205
+ const ids = uniqueAccountIds([
10206
+ ...listIndexedWeixinAccountIds(),
10207
+ ...listAccountFileIds()
10208
+ ]);
10132
10209
  for (const id of ids) {
10133
10210
  clearWeixinAccount(id);
10134
10211
  }
@@ -10168,8 +10245,33 @@ function loadConfigRouteTag(accountId) {
10168
10245
  return;
10169
10246
  }
10170
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
+ }
10171
10270
  function listWeixinAccountIds() {
10172
- return listIndexedWeixinAccountIds();
10271
+ const indexed = listIndexedWeixinAccountIds();
10272
+ if (indexed.length > 0)
10273
+ return indexed;
10274
+ return listAccountFileIds();
10173
10275
  }
10174
10276
  function resolveWeixinAccount(accountId) {
10175
10277
  const raw = accountId?.trim();
@@ -10453,8 +10555,83 @@ var init_send_errors = __esm(() => {
10453
10555
 
10454
10556
  // src/weixin/api/api.ts
10455
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
+ }
10456
10630
  function buildBaseInfo() {
10457
- return { channel_version: CHANNEL_VERSION };
10631
+ return {
10632
+ channel_version: CHANNEL_VERSION,
10633
+ bot_agent: sanitizeBotAgent(loadConfigBotAgent())
10634
+ };
10458
10635
  }
10459
10636
  function ensureTrailingSlash(url) {
10460
10637
  return url.endsWith("/") ? url : `${url}/`;
@@ -10465,6 +10642,9 @@ function randomWechatUin() {
10465
10642
  }
10466
10643
  function buildCommonHeaders() {
10467
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);
10468
10648
  const routeTag = loadConfigRouteTag();
10469
10649
  if (routeTag) {
10470
10650
  headers.SKRouteTag = routeTag;
@@ -10510,6 +10690,35 @@ async function apiGetFetch(params) {
10510
10690
  throw err;
10511
10691
  }
10512
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
+ }
10513
10722
  async function apiFetch(params) {
10514
10723
  const base = ensureTrailingSlash(params.baseUrl);
10515
10724
  const url = new URL(params.endpoint, base);
@@ -10668,7 +10877,7 @@ async function sendTyping(params) {
10668
10877
  label: "sendTyping"
10669
10878
  });
10670
10879
  }
10671
- 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;
10672
10881
  var init_api = __esm(() => {
10673
10882
  init_version();
10674
10883
  init_accounts();
@@ -10676,6 +10885,8 @@ var init_api = __esm(() => {
10676
10885
  init_redact();
10677
10886
  init_send_errors();
10678
10887
  CHANNEL_VERSION = readVersion();
10888
+ ILINK_APP_CLIENT_VERSION = buildClientVersion(CHANNEL_VERSION);
10889
+ ILINK_APP_ID = (process.env.WEACPX_ILINK_APP_ID ?? "").trim();
10679
10890
  });
10680
10891
 
10681
10892
  // node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -11707,22 +11918,47 @@ function purgeExpiredLogins() {
11707
11918
  }
11708
11919
  }
11709
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
+ }
11710
11936
  async function fetchQRCode(apiBaseUrl, botType) {
11711
11937
  logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
11712
- const rawText = await apiGetFetch({
11938
+ const localTokenList = getLocalBotTokenList();
11939
+ logger.info(`fetchQRCode: local_token_list count=${localTokenList.length}`);
11940
+ const rawText = await apiPostFetch({
11713
11941
  baseUrl: apiBaseUrl,
11714
11942
  endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
11715
- timeoutMs: GET_QRCODE_TIMEOUT_MS,
11943
+ body: JSON.stringify({ local_token_list: localTokenList }),
11716
11944
  label: "fetchQRCode"
11717
11945
  });
11718
11946
  return JSON.parse(rawText);
11719
11947
  }
11720
- async function pollQRStatus(apiBaseUrl, qrcode) {
11721
- 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);
11722
11958
  try {
11723
11959
  const rawText = await apiGetFetch({
11724
11960
  baseUrl: apiBaseUrl,
11725
- endpoint: `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
11961
+ endpoint,
11726
11962
  timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
11727
11963
  label: "pollQRStatus"
11728
11964
  });
@@ -11737,6 +11973,28 @@ async function pollQRStatus(apiBaseUrl, qrcode) {
11737
11973
  return { status: "wait" };
11738
11974
  }
11739
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
+ }
11740
11998
  async function startWeixinLoginWithQr(opts) {
11741
11999
  const sessionKey = opts.accountId || randomUUID2();
11742
12000
  purgeExpiredLogins();
@@ -11775,6 +12033,36 @@ async function startWeixinLoginWithQr(opts) {
11775
12033
  };
11776
12034
  }
11777
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
+ }
11778
12066
  async function waitForWeixinLogin(opts) {
11779
12067
  let activeLogin = activeLogins.get(opts.sessionKey);
11780
12068
  if (!activeLogin) {
@@ -11801,7 +12089,7 @@ async function waitForWeixinLogin(opts) {
11801
12089
  while (Date.now() < deadline) {
11802
12090
  try {
11803
12091
  const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
11804
- const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
12092
+ const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode, activeLogin.pendingVerifyCode);
11805
12093
  logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
11806
12094
  activeLogin.status = statusResponse.status;
11807
12095
  switch (statusResponse.status) {
@@ -11811,6 +12099,10 @@ async function waitForWeixinLogin(opts) {
11811
12099
  }
11812
12100
  break;
11813
12101
  case "scaned":
12102
+ if (activeLogin.pendingVerifyCode) {
12103
+ logger.info("verify code accepted, resuming polling");
12104
+ activeLogin.pendingVerifyCode = undefined;
12105
+ }
11814
12106
  if (!scannedPrinted) {
11815
12107
  process.stdout.write(`
11816
12108
  \uD83D\uDC40 已扫码,在微信继续操作...
@@ -11819,6 +12111,7 @@ async function waitForWeixinLogin(opts) {
11819
12111
  }
11820
12112
  break;
11821
12113
  case "expired": {
12114
+ activeLogin.pendingVerifyCode = undefined;
11822
12115
  qrRefreshCount++;
11823
12116
  if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
11824
12117
  logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
@@ -11832,36 +12125,14 @@ async function waitForWeixinLogin(opts) {
11832
12125
  ⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
11833
12126
  `);
11834
12127
  logger.info(`waitForWeixinLogin: QR expired, refreshing (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
11835
- try {
11836
- const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
11837
- const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
11838
- activeLogin.qrcode = qrResponse.qrcode;
11839
- activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
11840
- activeLogin.startedAt = Date.now();
12128
+ const expiredRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
11841
12129
  scannedPrinted = false;
11842
- logger.info(`waitForWeixinLogin: new QR code obtained qrcode=${redactToken(qrResponse.qrcode)}`);
11843
- process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
11844
-
11845
- `);
11846
- try {
11847
- const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
11848
- qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
11849
- process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
11850
- `);
11851
- process.stdout.write(`${qrResponse.qrcode_img_content}
11852
- `);
11853
- } catch {
11854
- process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
11855
- `);
11856
- process.stdout.write(`${qrResponse.qrcode_img_content}
11857
- `);
11858
- }
11859
- } catch (refreshErr) {
11860
- logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
12130
+ });
12131
+ if (!expiredRefreshResult.success) {
11861
12132
  activeLogins.delete(opts.sessionKey);
11862
12133
  return {
11863
12134
  connected: false,
11864
- message: `刷新二维码失败: ${String(refreshErr)}`
12135
+ message: expiredRefreshResult.message
11865
12136
  };
11866
12137
  }
11867
12138
  break;
@@ -11877,6 +12148,49 @@ async function waitForWeixinLogin(opts) {
11877
12148
  }
11878
12149
  break;
11879
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
+ }
11880
12194
  case "confirmed": {
11881
12195
  if (!statusResponse.ilink_bot_id) {
11882
12196
  activeLogins.delete(opts.sessionKey);
@@ -11916,15 +12230,201 @@ async function waitForWeixinLogin(opts) {
11916
12230
  message: "登录超时,请重试。"
11917
12231
  };
11918
12232
  }
11919
- 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;
11920
12234
  var init_login_qr = __esm(() => {
11921
12235
  init_api();
12236
+ init_accounts();
11922
12237
  init_logger();
11923
12238
  init_redact();
11924
12239
  ACTIVE_LOGIN_TTL_MS = 5 * 60000;
11925
12240
  activeLogins = new Map;
11926
12241
  });
11927
12242
 
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")}`;
12247
+ }
12248
+ var init_random = () => {};
12249
+
12250
+ // src/weixin/api/types.ts
12251
+ var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
12252
+ var init_types2 = __esm(() => {
12253
+ UploadMediaType = {
12254
+ IMAGE: 1,
12255
+ VIDEO: 2,
12256
+ FILE: 3,
12257
+ VOICE: 4
12258
+ };
12259
+ MessageType = {
12260
+ NONE: 0,
12261
+ USER: 1,
12262
+ BOT: 2
12263
+ };
12264
+ MessageItemType = {
12265
+ NONE: 0,
12266
+ TEXT: 1,
12267
+ IMAGE: 2,
12268
+ VOICE: 3,
12269
+ FILE: 4,
12270
+ VIDEO: 5
12271
+ };
12272
+ MessageState = {
12273
+ NEW: 0,
12274
+ GENERATING: 1,
12275
+ FINISH: 2
12276
+ };
12277
+ TypingStatus = {
12278
+ TYPING: 1,
12279
+ CANCEL: 2
12280
+ };
12281
+ });
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
+
11928
12428
  // src/weixin/api/config-cache.ts
11929
12429
  class WeixinConfigManager {
11930
12430
  apiOpts;
@@ -11992,16 +12492,8 @@ function pauseSession(accountId) {
11992
12492
  pauseUntilMap.set(accountId, until);
11993
12493
  logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
11994
12494
  }
11995
- function getRemainingPauseMs(accountId) {
11996
- const until = pauseUntilMap.get(accountId);
11997
- if (until === undefined)
11998
- return 0;
11999
- const remaining = until - Date.now();
12000
- if (remaining <= 0) {
12001
- pauseUntilMap.delete(accountId);
12002
- return 0;
12003
- }
12004
- return remaining;
12495
+ function resetSessionPause(accountId) {
12496
+ pauseUntilMap.delete(accountId);
12005
12497
  }
12006
12498
  var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
12007
12499
  var init_session_guard = __esm(() => {
@@ -12051,42 +12543,9 @@ function createConversationExecutor() {
12051
12543
  };
12052
12544
  }
12053
12545
 
12054
- // src/weixin/api/types.ts
12055
- var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
12056
- var init_types2 = __esm(() => {
12057
- UploadMediaType = {
12058
- IMAGE: 1,
12059
- VIDEO: 2,
12060
- FILE: 3,
12061
- VOICE: 4
12062
- };
12063
- MessageType = {
12064
- NONE: 0,
12065
- USER: 1,
12066
- BOT: 2
12067
- };
12068
- MessageItemType = {
12069
- NONE: 0,
12070
- TEXT: 1,
12071
- IMAGE: 2,
12072
- VOICE: 3,
12073
- FILE: 4,
12074
- VIDEO: 5
12075
- };
12076
- MessageState = {
12077
- NEW: 0,
12078
- GENERATING: 1,
12079
- FINISH: 2
12080
- };
12081
- TypingStatus = {
12082
- TYPING: 1,
12083
- CANCEL: 2
12084
- };
12085
- });
12086
-
12087
12546
  // src/channels/media-store.ts
12088
12547
  import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile6 } from "node:fs/promises";
12089
- import path6 from "node:path";
12548
+ import path7 from "node:path";
12090
12549
 
12091
12550
  class RuntimeMediaStore {
12092
12551
  rootDir;
@@ -12102,10 +12561,10 @@ class RuntimeMediaStore {
12102
12561
  const safeChatKey = safePathSegment(input.chatKey);
12103
12562
  const safeMessageId = safePathSegment(input.messageId || "message");
12104
12563
  const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
12105
- const dir = path6.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
12564
+ const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
12106
12565
  await mkdir7(dir, { recursive: true });
12107
- const resolvedRoot = path6.resolve(this.rootDir);
12108
- 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)));
12109
12568
  if (!isPathInside(resolvedFile, resolvedRoot)) {
12110
12569
  throw new Error("media path escapes runtime media root");
12111
12570
  }
@@ -12114,7 +12573,7 @@ class RuntimeMediaStore {
12114
12573
  kind: input.kind,
12115
12574
  filePath: resolvedFile,
12116
12575
  mimeType: input.mimeType,
12117
- fileName: path6.basename(resolvedFile),
12576
+ fileName: path7.basename(resolvedFile),
12118
12577
  sizeBytes: input.buffer.byteLength,
12119
12578
  source: {
12120
12579
  channelId: input.channelId,
@@ -12130,10 +12589,10 @@ class RuntimeMediaStore {
12130
12589
  }
12131
12590
  }
12132
12591
  function sanitizeMediaFileName(fileName, mimeType) {
12133
- const base = path6.basename(fileName.trim() || "attachment");
12592
+ const base = path7.basename(fileName.trim() || "attachment");
12134
12593
  const replaced = base.replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
12135
12594
  const safe = replaced || "attachment";
12136
- const ext = path6.extname(safe);
12595
+ const ext = path7.extname(safe);
12137
12596
  if (ext)
12138
12597
  return safe;
12139
12598
  return `${safe}${extensionFromMime(mimeType)}`;
@@ -12143,13 +12602,13 @@ function safePathSegment(value) {
12143
12602
  return safe || "unknown";
12144
12603
  }
12145
12604
  async function uniqueFileName(dir, baseName) {
12146
- const ext = path6.extname(baseName);
12605
+ const ext = path7.extname(baseName);
12147
12606
  const stem = ext ? baseName.slice(0, -ext.length) : baseName;
12148
12607
  let candidate = baseName;
12149
12608
  let counter = 2;
12150
12609
  while (true) {
12151
12610
  try {
12152
- await access2(path6.join(dir, candidate));
12611
+ await access2(path7.join(dir, candidate));
12153
12612
  candidate = `${stem}-${counter}${ext}`;
12154
12613
  counter += 1;
12155
12614
  } catch {
@@ -12180,8 +12639,8 @@ function extensionFromMime(mimeType) {
12180
12639
  return ".bin";
12181
12640
  }
12182
12641
  function isPathInside(candidate, root) {
12183
- const relative = path6.relative(root, candidate);
12184
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
12642
+ const relative = path7.relative(root, candidate);
12643
+ return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
12185
12644
  }
12186
12645
  async function cleanupDir(dir, cutoffMs) {
12187
12646
  let entries;
@@ -12192,7 +12651,7 @@ async function cleanupDir(dir, cutoffMs) {
12192
12651
  }
12193
12652
  let empty = true;
12194
12653
  for (const entry of entries) {
12195
- const full = path6.join(dir, entry.name);
12654
+ const full = path7.join(dir, entry.name);
12196
12655
  if (entry.isDirectory()) {
12197
12656
  const childEmpty = await cleanupDir(full, cutoffMs);
12198
12657
  if (childEmpty) {
@@ -12221,18 +12680,18 @@ var init_media_store = __esm(() => {
12221
12680
  });
12222
12681
 
12223
12682
  // src/channels/outbound-media-safety.ts
12224
- import fs5 from "node:fs/promises";
12225
- import path7 from "node:path";
12683
+ import fs6 from "node:fs/promises";
12684
+ import path8 from "node:path";
12226
12685
  async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
12227
12686
  if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) {
12228
12687
  return null;
12229
12688
  }
12230
- const candidate = path7.isAbsolute(mediaPath) ? mediaPath : path7.resolve(mediaPath);
12689
+ const candidate = path8.isAbsolute(mediaPath) ? mediaPath : path8.resolve(mediaPath);
12231
12690
  const realCandidate = await realpathOrNull(candidate);
12232
12691
  if (!realCandidate) {
12233
12692
  return null;
12234
12693
  }
12235
- const stat2 = await fs5.stat(realCandidate).catch(() => null);
12694
+ const stat2 = await fs6.stat(realCandidate).catch(() => null);
12236
12695
  if (!stat2?.isFile()) {
12237
12696
  return null;
12238
12697
  }
@@ -12246,21 +12705,21 @@ async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
12246
12705
  }
12247
12706
  async function realpathOrNull(filePath) {
12248
12707
  try {
12249
- return await fs5.realpath(filePath);
12708
+ return await fs6.realpath(filePath);
12250
12709
  } catch {
12251
12710
  return null;
12252
12711
  }
12253
12712
  }
12254
12713
  function isPathInside2(candidate, root) {
12255
- const relative = path7.relative(root, candidate);
12256
- return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
12714
+ const relative = path8.relative(root, candidate);
12715
+ return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
12257
12716
  }
12258
12717
  var init_outbound_media_safety = () => {};
12259
12718
 
12260
12719
  // src/weixin/media/mime.ts
12261
- import path8 from "node:path";
12720
+ import path9 from "node:path";
12262
12721
  function getMimeFromFilename(filename) {
12263
- const ext = path8.extname(filename).toLowerCase();
12722
+ const ext = path9.extname(filename).toLowerCase();
12264
12723
  return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
12265
12724
  }
12266
12725
  function getExtensionFromMime(mimeType) {
@@ -12598,115 +13057,361 @@ function buildFinalHeadsUp(input) {
12598
13057
  \uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`;
12599
13058
  }
12600
13059
 
12601
- // src/weixin/util/random.ts
12602
- import crypto2 from "node:crypto";
12603
- function generateId(prefix) {
12604
- return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
12605
- }
12606
- var init_random = () => {};
12607
-
12608
- // src/weixin/messaging/inbound.ts
12609
- function contextTokenKey(accountId, userId) {
12610
- return `${accountId}:${userId}`;
12611
- }
12612
- function setContextToken(accountId, userId, token) {
12613
- const k = contextTokenKey(accountId, userId);
12614
- logger.debug(`setContextToken: key=${k}`);
12615
- contextTokenStore.set(k, token);
12616
- }
12617
- function getContextToken(accountId, userId) {
12618
- const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
12619
- const val = contextTokenStore.get(k);
12620
- logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
12621
- return val;
12622
- }
12623
- function normalizeWeixinUserIdFromChatKey(chatKey) {
12624
- const parts = chatKey.split(":");
12625
- if (parts[0] === "weixin" && parts[2]) {
12626
- 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);
12627
13069
  }
12628
- return chatKey;
12629
- }
12630
- function isMediaItem(item) {
12631
- return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
12632
- }
12633
- function bodyFromItemList(itemList) {
12634
- 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;
12635
13215
  return "";
12636
- for (const item of itemList) {
12637
- if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
12638
- const text = String(item.text_item.text);
12639
- const ref = item.ref_msg;
12640
- if (!ref)
12641
- return text;
12642
- if (ref.message_item && isMediaItem(ref.message_item))
12643
- return text;
12644
- const parts = [];
12645
- if (ref.title)
12646
- parts.push(ref.title);
12647
- if (ref.message_item) {
12648
- const refBody = bodyFromItemList([ref.message_item]);
12649
- if (refBody)
12650
- 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;
12651
13238
  }
12652
- if (!parts.length)
12653
- return text;
12654
- return `[引用: ${parts.join(" | ")}]
12655
- ${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++;
12656
13282
  }
12657
- if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
12658
- 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
+ }
12659
13400
  }
13401
+ return "";
12660
13402
  }
12661
- return "";
12662
- }
12663
- function extractWeixinMediaDescriptors(itemList) {
12664
- const out = [];
12665
- for (const item of itemList ?? []) {
12666
- const descriptor = descriptorFromItem(item);
12667
- if (descriptor)
12668
- out.push(descriptor);
12669
- const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
12670
- const refDescriptor = descriptorFromItem(ref);
12671
- if (refDescriptor)
12672
- out.push(refDescriptor);
13403
+ static containsCJK(text) {
13404
+ return /[\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(text);
12673
13405
  }
12674
- return out;
12675
- }
12676
- function descriptorFromItem(item) {
12677
- if (!item)
12678
- return;
12679
- if (item.type === MessageItemType.IMAGE)
12680
- return { item, kind: "image" };
12681
- if (item.type === MessageItemType.VIDEO)
12682
- return { item, kind: "video" };
12683
- if (item.type === MessageItemType.FILE)
12684
- return { item, kind: "file", fileName: item.file_item?.file_name };
12685
- if (item.type === MessageItemType.VOICE)
12686
- return { item, kind: "audio" };
12687
- return;
12688
13406
  }
12689
- var contextTokenStore;
12690
- var init_inbound = __esm(() => {
12691
- init_logger();
12692
- init_random();
12693
- init_types2();
12694
- contextTokenStore = new Map;
12695
- });
12696
13407
 
12697
13408
  // src/weixin/messaging/send.ts
12698
13409
  function generateClientId() {
12699
13410
  return generateId("openclaw-weixin");
12700
13411
  }
12701
13412
  function markdownToPlainText(text) {
12702
- let result = text;
12703
- result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
12704
- result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
12705
- result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
12706
- result = result.replace(/^\|[\s:|-]+\|$/gm, "");
12707
- result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
12708
- result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
12709
- return result;
13413
+ const f = new StreamingMarkdownFilter;
13414
+ return f.feed(text) + f.flush();
12710
13415
  }
12711
13416
  function buildTextMessageReq(params) {
12712
13417
  const { to, text, contextToken, clientId } = params;
@@ -12942,10 +13647,10 @@ var init_cdn_upload = __esm(() => {
12942
13647
 
12943
13648
  // src/weixin/cdn/upload.ts
12944
13649
  import crypto3 from "node:crypto";
12945
- import fs6 from "node:fs/promises";
13650
+ import fs7 from "node:fs/promises";
12946
13651
  async function uploadMediaToCdn(params) {
12947
13652
  const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
12948
- const plaintext = await fs6.readFile(filePath);
13653
+ const plaintext = await fs7.readFile(filePath);
12949
13654
  const rawsize = plaintext.length;
12950
13655
  const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
12951
13656
  const filesize = aesEcbPaddedSize(rawsize);
@@ -13018,7 +13723,7 @@ var init_upload = __esm(() => {
13018
13723
  });
13019
13724
 
13020
13725
  // src/weixin/messaging/send-media.ts
13021
- import path9 from "node:path";
13726
+ import path10 from "node:path";
13022
13727
  async function sendWeixinMediaFile(params) {
13023
13728
  const { media, filePath, to, text, opts, cdnBaseUrl } = params;
13024
13729
  const mime = media?.mimeType ?? getMimeFromFilename(filePath);
@@ -13045,7 +13750,7 @@ async function sendWeixinMediaFile(params) {
13045
13750
  logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
13046
13751
  return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
13047
13752
  }
13048
- const fileName = media?.fileName ?? path9.basename(filePath);
13753
+ const fileName = media?.fileName ?? path10.basename(filePath);
13049
13754
  logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
13050
13755
  const uploaded = await uploadFileAttachmentToWeixin({
13051
13756
  filePath,
@@ -13065,14 +13770,14 @@ var init_send_media = __esm(() => {
13065
13770
  });
13066
13771
 
13067
13772
  // src/weixin/messaging/debug-mode.ts
13068
- import fs7 from "node:fs";
13069
- import path10 from "node:path";
13773
+ import fs8 from "node:fs";
13774
+ import path11 from "node:path";
13070
13775
  function resolveDebugModePath() {
13071
- return path10.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
13776
+ return path11.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
13072
13777
  }
13073
13778
  function loadState() {
13074
13779
  try {
13075
- const raw = fs7.readFileSync(resolveDebugModePath(), "utf-8");
13780
+ const raw = fs8.readFileSync(resolveDebugModePath(), "utf-8");
13076
13781
  const parsed = JSON.parse(raw);
13077
13782
  if (parsed && typeof parsed.accounts === "object")
13078
13783
  return parsed;
@@ -13081,8 +13786,8 @@ function loadState() {
13081
13786
  }
13082
13787
  function saveState(state) {
13083
13788
  const filePath = resolveDebugModePath();
13084
- ensureDirSync(path10.dirname(filePath));
13085
- 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");
13086
13791
  }
13087
13792
  function toggleDebugMode(accountId) {
13088
13793
  const state = loadState();
@@ -13539,9 +14244,9 @@ var init_perf_tracer = __esm(() => {
13539
14244
 
13540
14245
  // src/weixin/messaging/handle-weixin-message-turn.ts
13541
14246
  import crypto4 from "node:crypto";
13542
- import fs8 from "node:fs/promises";
14247
+ import fs9 from "node:fs/promises";
13543
14248
  import { tmpdir } from "node:os";
13544
- import path11 from "node:path";
14249
+ import path12 from "node:path";
13545
14250
  function utf8ByteLength(s) {
13546
14251
  return Buffer.byteLength(s, "utf8");
13547
14252
  }
@@ -13631,24 +14336,24 @@ function hardCutByCodepoint(s, maxBytes) {
13631
14336
  return out;
13632
14337
  }
13633
14338
  function resolveMediaTempDir(customRoot) {
13634
- return customRoot ?? path11.join(tmpdir(), "weacpx", "media");
14339
+ return customRoot ?? path12.join(tmpdir(), "weacpx", "media");
13635
14340
  }
13636
14341
  function createSaveMediaBuffer(mediaTempDir) {
13637
14342
  return async function saveMediaBuffer(buffer, contentType, subdir, maxBytes, originalFilename) {
13638
14343
  if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
13639
14344
  throw new Error(`media exceeds ${maxBytes} bytes`);
13640
14345
  }
13641
- const dir = path11.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
13642
- await fs8.mkdir(dir, { recursive: true });
14346
+ const dir = path12.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
14347
+ await fs9.mkdir(dir, { recursive: true });
13643
14348
  let ext = ".bin";
13644
14349
  if (originalFilename) {
13645
- ext = path11.extname(originalFilename) || ".bin";
14350
+ ext = path12.extname(originalFilename) || ".bin";
13646
14351
  } else if (contentType) {
13647
14352
  ext = getExtensionFromMime(contentType);
13648
14353
  }
13649
14354
  const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
13650
- const filePath = path11.join(dir, name);
13651
- await fs8.writeFile(filePath, buffer);
14355
+ const filePath = path12.join(dir, name);
14356
+ await fs9.writeFile(filePath, buffer);
13652
14357
  return { path: filePath };
13653
14358
  };
13654
14359
  }
@@ -13800,7 +14505,7 @@ async function handleWeixinMessageTurn(full, deps) {
13800
14505
  continue;
13801
14506
  }
13802
14507
  try {
13803
- const buffer = await fs8.readFile(filePath);
14508
+ const buffer = await fs9.readFile(filePath);
13804
14509
  const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
13805
14510
  media.push(await mediaStore.saveMediaBuffer({
13806
14511
  channelId: "weixin",
@@ -13814,7 +14519,7 @@ async function handleWeixinMessageTurn(full, deps) {
13814
14519
  maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
13815
14520
  }));
13816
14521
  } finally {
13817
- await fs8.rm(filePath, { force: true }).catch(() => {});
14522
+ await fs9.rm(filePath, { force: true }).catch(() => {});
13818
14523
  }
13819
14524
  } catch (err) {
13820
14525
  deps.errLog(`media download failed: ${String(err)}`);
@@ -14048,20 +14753,20 @@ var init_handle_weixin_message_turn = __esm(() => {
14048
14753
  });
14049
14754
 
14050
14755
  // src/weixin/storage/sync-buf.ts
14051
- import fs9 from "node:fs";
14052
- import path12 from "node:path";
14756
+ import fs10 from "node:fs";
14757
+ import path13 from "node:path";
14053
14758
  function resolveAccountsDir2() {
14054
- return path12.join(resolveStateDir(), "openclaw-weixin", "accounts");
14759
+ return path13.join(resolveStateDir(), "openclaw-weixin", "accounts");
14055
14760
  }
14056
14761
  function getSyncBufFilePath(accountId) {
14057
- return path12.join(resolveAccountsDir2(), `${accountId}.sync.json`);
14762
+ return path13.join(resolveAccountsDir2(), `${accountId}.sync.json`);
14058
14763
  }
14059
14764
  function getLegacySyncBufDefaultJsonPath() {
14060
- 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");
14061
14766
  }
14062
14767
  function readSyncBufFile(filePath) {
14063
14768
  try {
14064
- const raw = fs9.readFileSync(filePath, "utf-8");
14769
+ const raw = fs10.readFileSync(filePath, "utf-8");
14065
14770
  const data = JSON.parse(raw);
14066
14771
  if (typeof data.get_updates_buf === "string") {
14067
14772
  return data.get_updates_buf;
@@ -14073,10 +14778,10 @@ function loadGetUpdatesBuf(filePath) {
14073
14778
  const value = readSyncBufFile(filePath);
14074
14779
  if (value !== undefined)
14075
14780
  return value;
14076
- const accountId = path12.basename(filePath, ".sync.json");
14781
+ const accountId = path13.basename(filePath, ".sync.json");
14077
14782
  const rawId = deriveRawAccountId(accountId);
14078
14783
  if (rawId) {
14079
- const compatPath = path12.join(resolveAccountsDir2(), `${rawId}.sync.json`);
14784
+ const compatPath = path13.join(resolveAccountsDir2(), `${rawId}.sync.json`);
14080
14785
  const compatValue = readSyncBufFile(compatPath);
14081
14786
  if (compatValue !== undefined)
14082
14787
  return compatValue;
@@ -14084,9 +14789,9 @@ function loadGetUpdatesBuf(filePath) {
14084
14789
  return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
14085
14790
  }
14086
14791
  function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
14087
- const dir = path12.dirname(filePath);
14792
+ const dir = path13.dirname(filePath);
14088
14793
  ensureDirSync(dir);
14089
- 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");
14090
14795
  }
14091
14796
  var init_sync_buf = __esm(() => {
14092
14797
  init_accounts();
@@ -14120,23 +14825,23 @@ function shouldFetchTypingConfig(textBody) {
14120
14825
  }
14121
14826
  async function monitorWeixinProvider(opts) {
14122
14827
  const {
14123
- baseUrl,
14124
- cdnBaseUrl,
14125
- token,
14126
- accountId,
14127
14828
  agent,
14128
14829
  abortSignal,
14129
14830
  longPollTimeoutMs
14130
14831
  } = opts;
14832
+ let baseUrl = opts.baseUrl;
14833
+ let cdnBaseUrl = opts.cdnBaseUrl;
14834
+ let token = opts.token;
14835
+ let accountId = opts.accountId;
14131
14836
  const log = opts.log ?? ((msg) => console.log(msg));
14132
14837
  const errLog = (msg) => {
14133
14838
  log(msg);
14134
14839
  logger.error(msg);
14135
14840
  };
14136
- const aLog = logger.withAccount(accountId);
14841
+ let aLog = logger.withAccount(accountId);
14137
14842
  log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
14138
14843
  aLog.info(`Monitor started: baseUrl=${baseUrl}`);
14139
- const syncFilePath = getSyncBufFilePath(accountId);
14844
+ let syncFilePath = getSyncBufFilePath(accountId);
14140
14845
  const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
14141
14846
  let getUpdatesBuf = previousGetUpdatesBuf ?? "";
14142
14847
  if (previousGetUpdatesBuf) {
@@ -14144,7 +14849,7 @@ async function monitorWeixinProvider(opts) {
14144
14849
  } else {
14145
14850
  log(`[weixin] no previous sync buf, starting fresh`);
14146
14851
  }
14147
- const configManager = new WeixinConfigManager({ baseUrl, token }, log);
14852
+ let configManager = new WeixinConfigManager({ baseUrl, token }, log);
14148
14853
  const conversationExecutor = createConversationExecutor();
14149
14854
  const seenMessageIds = new Set;
14150
14855
  const messageIdOrder = [];
@@ -14167,11 +14872,37 @@ async function monitorWeixinProvider(opts) {
14167
14872
  if (isApiError) {
14168
14873
  const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
14169
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.`);
14170
14878
  pauseSession(accountId);
14171
- const pauseMs = getRemainingPauseMs(accountId);
14172
- errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing for ${Math.ceil(pauseMs / 60000)} min. Please run \`npx weixin-acp login\` to re-login.`);
14173
14879
  consecutiveFailures = 0;
14174
- 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}`);
14175
14906
  continue;
14176
14907
  }
14177
14908
  consecutiveFailures += 1;
@@ -14265,7 +14996,43 @@ function sleep(ms, signal) {
14265
14996
  }, { once: true });
14266
14997
  });
14267
14998
  }
14268
- 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;
14269
15036
  var init_monitor = __esm(() => {
14270
15037
  init_api();
14271
15038
  init_config_cache();
@@ -14274,6 +15041,9 @@ var init_monitor = __esm(() => {
14274
15041
  init_types2();
14275
15042
  init_sync_buf();
14276
15043
  init_logger();
15044
+ init_accounts();
15045
+ init_session_guard();
15046
+ init_inbound();
14277
15047
  });
14278
15048
 
14279
15049
  // src/weixin/bot.ts
@@ -14332,6 +15102,8 @@ function logout(opts) {
14332
15102
  log("当前没有已登录的账号");
14333
15103
  return;
14334
15104
  }
15105
+ for (const id of ids)
15106
+ clearContextTokensForAccount(id);
14335
15107
  clearAllWeixinAccounts();
14336
15108
  log("✅ 已退出登录");
14337
15109
  }
@@ -14359,6 +15131,7 @@ async function start(agent, opts) {
14359
15131
  if (!account.configured) {
14360
15132
  throw new Error(`账号 ${accountId} 未配置 (缺少 token),请先运行 login`);
14361
15133
  }
15134
+ restoreContextTokens(account.accountId);
14362
15135
  log(`[weixin] 启动 bot, account=${account.accountId}`);
14363
15136
  await monitorWeixinProvider({
14364
15137
  baseUrl: account.baseUrl,
@@ -14384,6 +15157,7 @@ async function start(agent, opts) {
14384
15157
  var init_bot = __esm(() => {
14385
15158
  init_accounts();
14386
15159
  init_login_qr();
15160
+ init_inbound();
14387
15161
  init_monitor();
14388
15162
  });
14389
15163
 
@@ -14714,9 +15488,9 @@ function createWeixinConsumerLock(options = {}) {
14714
15488
  }
14715
15489
  };
14716
15490
  }
14717
- async function loadLockMetadata(path13) {
15491
+ async function loadLockMetadata(path14) {
14718
15492
  try {
14719
- const raw = await readFile6(path13, "utf8");
15493
+ const raw = await readFile6(path14, "utf8");
14720
15494
  const parsed = JSON.parse(raw);
14721
15495
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
14722
15496
  return null;
@@ -15672,6 +16446,18 @@ function parseCommand(input) {
15672
16446
  return { kind: "workspaces" };
15673
16447
  if (command === "/session" && parts[1] === "reset" && parts.length === 2)
15674
16448
  return { kind: "session.reset" };
16449
+ if (command === "/session" && parts[1] === "tail") {
16450
+ if (parts.length === 2) {
16451
+ return { kind: "session.tail" };
16452
+ }
16453
+ if (parts.length === 3) {
16454
+ const lines = parsePositiveInt(parts[2]);
16455
+ if (lines !== null) {
16456
+ return { kind: "session.tail", lines };
16457
+ }
16458
+ }
16459
+ return { kind: "invalid", text: trimmed, recognizedCommand: "/session" };
16460
+ }
15675
16461
  if (command === "/session" && parts[1] === "rm" && parts[2] && parts.length === 3) {
15676
16462
  return { kind: "session.rm", alias: parts[2] };
15677
16463
  }
@@ -15774,6 +16560,7 @@ function parseCommand(input) {
15774
16560
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
15775
16561
  const name = parts[2];
15776
16562
  let cwd = "";
16563
+ let raw = false;
15777
16564
  let invalid = false;
15778
16565
  for (let index = 3;index < parts.length; index += 1) {
15779
16566
  if (parts[index] === "--cwd" || parts[index] === "-d") {
@@ -15785,11 +16572,19 @@ function parseCommand(input) {
15785
16572
  index += 1;
15786
16573
  continue;
15787
16574
  }
16575
+ if (parts[index] === "--raw") {
16576
+ if (raw) {
16577
+ invalid = true;
16578
+ break;
16579
+ }
16580
+ raw = true;
16581
+ continue;
16582
+ }
15788
16583
  invalid = true;
15789
16584
  break;
15790
16585
  }
15791
16586
  if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
15792
- return { kind: "workspace.new", name, cwd };
16587
+ return { kind: "workspace.new", name, cwd, ...raw ? { raw: true } : {} };
15793
16588
  }
15794
16589
  }
15795
16590
  if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
@@ -15981,6 +16776,16 @@ function tokenizeCommand(input) {
15981
16776
  }
15982
16777
  return tokens;
15983
16778
  }
16779
+ function parsePositiveInt(value) {
16780
+ if (!value)
16781
+ return null;
16782
+ if (!/^\d+$/.test(value))
16783
+ return null;
16784
+ const parsed = Number(value);
16785
+ if (!Number.isFinite(parsed) || parsed <= 0)
16786
+ return null;
16787
+ return parsed;
16788
+ }
15984
16789
  function parseListFilterFlags(parts, validStatuses) {
15985
16790
  const filter = {};
15986
16791
  let i = 1;
@@ -16104,6 +16909,7 @@ var init_command_policy = __esm(() => {
16104
16909
  "agents",
16105
16910
  "workspaces",
16106
16911
  "sessions",
16912
+ "session.tail",
16107
16913
  "status",
16108
16914
  "mode.show",
16109
16915
  "replymode.show",
@@ -16120,6 +16926,7 @@ var init_command_policy = __esm(() => {
16120
16926
  COMMAND_KIND_TO_LABEL = {
16121
16927
  "session.reset": "/clear",
16122
16928
  "session.rm": "/session rm",
16929
+ "session.tail": "/session tail",
16123
16930
  "replymode.set": "/replymode",
16124
16931
  "replymode.reset": "/replymode reset",
16125
16932
  "mode.set": "/mode",
@@ -16237,26 +17044,26 @@ var init_permission_handler = __esm(() => {
16237
17044
 
16238
17045
  // src/commands/handlers/config-handler.ts
16239
17046
  function handleConfigShow(context) {
16240
- const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path13) => `- ${path13}`)];
16241
- 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}`));
16242
17049
  if (context.config) {
16243
17050
  lines.push("", "示例:", "- /config set channel.replyMode final", "- /config set logging.level debug");
16244
17051
  }
16245
17052
  return { text: lines.join(`
16246
17053
  `) };
16247
17054
  }
16248
- async function handleConfigSet(context, path13, rawValue) {
17055
+ async function handleConfigSet(context, path14, rawValue) {
16249
17056
  if (!context.config || !context.configStore) {
16250
17057
  return { text: "当前没有加载可写入的配置。" };
16251
17058
  }
16252
17059
  const previous = cloneAppConfig(context.config);
16253
17060
  const updated = cloneAppConfig(context.config);
16254
- const result = applySupportedConfigUpdate(updated, path13, rawValue);
17061
+ const result = applySupportedConfigUpdate(updated, path14, rawValue);
16255
17062
  if ("error" in result) {
16256
17063
  return { text: result.error };
16257
17064
  }
16258
17065
  await context.configStore.save(updated);
16259
- if (path13 === "transport.permissionMode" || path13 === "transport.nonInteractivePermissions") {
17066
+ if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
16260
17067
  try {
16261
17068
  await context.transport.updatePermissionPolicy?.(updated.transport);
16262
17069
  } catch (error2) {
@@ -16266,10 +17073,10 @@ async function handleConfigSet(context, path13, rawValue) {
16266
17073
  }
16267
17074
  }
16268
17075
  context.replaceConfig(updated);
16269
- return { text: `配置已更新:${path13} = ${result.renderedValue}` };
17076
+ return { text: `配置已更新:${path14} = ${result.renderedValue}` };
16270
17077
  }
16271
- function applySupportedConfigUpdate(config2, path13, rawValue) {
16272
- switch (path13) {
17078
+ function applySupportedConfigUpdate(config2, path14, rawValue) {
17079
+ switch (path14) {
16273
17080
  case "transport.type": {
16274
17081
  const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
16275
17082
  if (!parsed)
@@ -16303,6 +17110,11 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16303
17110
  config2.transport.nonInteractivePermissions = parsed;
16304
17111
  return { renderedValue: parsed };
16305
17112
  }
17113
+ case "transport.permissionPolicy":
17114
+ if (!rawValue.trim())
17115
+ return { error: "transport.permissionPolicy 不能为空。" };
17116
+ config2.transport.permissionPolicy = rawValue;
17117
+ return { renderedValue: rawValue };
16306
17118
  case "logging.level": {
16307
17119
  const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
16308
17120
  if (!parsed)
@@ -16352,18 +17164,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16352
17164
  };
16353
17165
  }
16354
17166
  }
16355
- const agentMatch = path13.match(/^agents\.([^.]+)\.(driver|command)$/);
17167
+ const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
16356
17168
  if (agentMatch) {
16357
17169
  const [, name, field] = agentMatch;
16358
17170
  if (!name || !field) {
16359
- return { error: `不支持修改这个配置路径:${path13}` };
17171
+ return { error: `不支持修改这个配置路径:${path14}` };
16360
17172
  }
16361
17173
  const agent = config2.agents[name];
16362
17174
  if (!agent) {
16363
17175
  return { error: `Agent「${name}」不存在,请先创建。` };
16364
17176
  }
16365
17177
  if (!rawValue.trim()) {
16366
- return { error: `${path13} 不能为空。` };
17178
+ return { error: `${path14} 不能为空。` };
16367
17179
  }
16368
17180
  if (field === "driver") {
16369
17181
  agent.driver = rawValue;
@@ -16372,18 +17184,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16372
17184
  }
16373
17185
  return { renderedValue: rawValue };
16374
17186
  }
16375
- const workspaceMatch = path13.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
17187
+ const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
16376
17188
  if (workspaceMatch) {
16377
17189
  const [, name, field] = workspaceMatch;
16378
17190
  if (!name || !field) {
16379
- return { error: `不支持修改这个配置路径:${path13}` };
17191
+ return { error: `不支持修改这个配置路径:${path14}` };
16380
17192
  }
16381
17193
  const workspace = config2.workspaces[name];
16382
17194
  if (!workspace) {
16383
17195
  return { error: `工作区「${name}」不存在,请先创建。` };
16384
17196
  }
16385
17197
  if (!rawValue.trim()) {
16386
- return { error: `${path13} 不能为空。` };
17198
+ return { error: `${path14} 不能为空。` };
16387
17199
  }
16388
17200
  if (field === "cwd") {
16389
17201
  workspace.cwd = rawValue;
@@ -16392,15 +17204,15 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
16392
17204
  }
16393
17205
  return { renderedValue: rawValue };
16394
17206
  }
16395
- return { error: `不支持修改这个配置路径:${path13}` };
17207
+ return { error: `不支持修改这个配置路径:${path14}` };
16396
17208
  }
16397
17209
  function parseEnum(value, allowed) {
16398
17210
  return allowed.includes(value) ? value : null;
16399
17211
  }
16400
- function parsePositiveNumber(rawValue, path13) {
17212
+ function parsePositiveNumber(rawValue, path14) {
16401
17213
  const value = Number(rawValue);
16402
17214
  if (!Number.isFinite(value) || value <= 0) {
16403
- return { error: `${path13} 必须是正数。` };
17215
+ return { error: `${path14} 必须是正数。` };
16404
17216
  }
16405
17217
  return { value };
16406
17218
  }
@@ -16412,6 +17224,7 @@ var init_config_handler = __esm(() => {
16412
17224
  "transport.sessionInitTimeoutMs",
16413
17225
  "transport.permissionMode",
16414
17226
  "transport.nonInteractivePermissions",
17227
+ "transport.permissionPolicy",
16415
17228
  "logging.level",
16416
17229
  "logging.maxSizeBytes",
16417
17230
  "logging.maxFiles",
@@ -16834,7 +17647,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
16834
17647
  return {
16835
17648
  text: [
16836
17649
  "没有找到可绑定的已有会话。",
16837
- `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
17650
+ `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${quoteWorkspaceNameIfNeeded(workspace)} --name <会话名>`
16838
17651
  ].join(`
16839
17652
  `)
16840
17653
  };
@@ -16966,6 +17779,15 @@ async function handleCancel(context, chatKey) {
16966
17779
  async function handleSessionReset(context, chatKey) {
16967
17780
  return await context.lifecycle.resetCurrentSession(chatKey);
16968
17781
  }
17782
+ async function handleSessionTail(context, chatKey, lines) {
17783
+ const session = await context.sessions.getCurrentSession(chatKey);
17784
+ if (!session) {
17785
+ return { text: NO_CURRENT_SESSION_TEXT };
17786
+ }
17787
+ const resolvedLines = Math.min(Math.max(lines ?? DEFAULT_SESSION_TAIL_LINES, 1), MAX_SESSION_TAIL_LINES);
17788
+ const result = await context.transport.tailSessionHistory(session, resolvedLines);
17789
+ return { text: result.text };
17790
+ }
16969
17791
  async function handleSessionRemove(context, chatKey, alias) {
16970
17792
  const internalAlias = await context.sessions.resolveAliasForChat(chatKey, alias);
16971
17793
  const session = await context.sessions.getSession(internalAlias);
@@ -17036,7 +17858,7 @@ async function handleSessionRemove(context, chatKey, alias) {
17036
17858
  return { text: lines.join(`
17037
17859
  `) };
17038
17860
  }
17039
- async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan) {
17861
+ async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
17040
17862
  const effectiveReplyMode = session.replyMode ?? context.config?.channel.replyMode ?? "verbose";
17041
17863
  if (!session.replyMode)
17042
17864
  session.replyMode = effectiveReplyMode;
@@ -17061,7 +17883,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17061
17883
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId);
17062
17884
  try {
17063
17885
  const replyContext = transportReply && context.quota && getChannelIdFromChatKey(chatKey) === "weixin" ? { chatKey, quota: context.quota } : undefined;
17064
- const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, perfSpan);
17886
+ const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan);
17065
17887
  if (claimHumanReply) {
17066
17888
  try {
17067
17889
  await context.orchestration?.claimActiveHumanReply?.(claimHumanReply);
@@ -17081,17 +17903,17 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17081
17903
  throw error2;
17082
17904
  }
17083
17905
  }
17084
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan) {
17906
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
17085
17907
  const session = await context.sessions.getCurrentSession(chatKey);
17086
17908
  if (!session) {
17087
17909
  return { text: NO_CURRENT_SESSION_TEXT };
17088
17910
  }
17089
17911
  try {
17090
- return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
17912
+ return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
17091
17913
  } catch (error2) {
17092
17914
  const recovered = await context.recovery.tryRecoverMissingSession(session, error2);
17093
17915
  if (recovered) {
17094
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
17916
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
17095
17917
  }
17096
17918
  return context.recovery.renderTransportError(session, error2);
17097
17919
  }
@@ -17158,10 +17980,11 @@ async function markCoordinatorResultsInjectionFailed(context, taskIds, groupIds,
17158
17980
  });
17159
17981
  }
17160
17982
  }
17161
- var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。", sessionHelp, modeHelp, replyModeHelp, statusHelp, cancelHelp;
17983
+ var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。", DEFAULT_SESSION_TAIL_LINES = 50, MAX_SESSION_TAIL_LINES = 500, sessionHelp, modeHelp, replyModeHelp, statusHelp, cancelHelp;
17162
17984
  var init_session_handler = __esm(() => {
17163
17985
  init_build_coordinator_prompt();
17164
17986
  init_channel_scope();
17987
+ init_workspace_name();
17165
17988
  sessionHelp = {
17166
17989
  topic: "session",
17167
17990
  aliases: ["ss", "sessions"],
@@ -17173,6 +17996,7 @@ var init_session_handler = __esm(() => {
17173
17996
  { usage: "/ss new <agent> (-d <path> | --ws <name>)", description: "强制新建会话" },
17174
17997
  { usage: "/ss new <alias> -a <name> --ws <name>", description: "按指定配置新建会话" },
17175
17998
  { usage: "/ss attach <alias> -a <name> --ws <name> --name <transport-session>", description: "绑定已有会话" },
17999
+ { usage: "/session tail [N]", description: "补拉当前会话的历史输出(默认 50 行)" },
17176
18000
  { usage: "/session rm <alias>", description: "删除逻辑会话" },
17177
18001
  { usage: "/use <alias>", description: "切换当前会话" },
17178
18002
  { usage: "/session reset 或 /clear", description: "重置当前会话上下文" }
@@ -17808,7 +18632,7 @@ var init_agent_handler = __esm(() => {
17808
18632
  function handleWorkspaces(context) {
17809
18633
  return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
17810
18634
  }
17811
- async function handleWorkspaceCreate(context, workspaceName, cwd) {
18635
+ async function handleWorkspaceCreate(context, workspaceName, cwd, options = {}) {
17812
18636
  if (!context.config || !context.configStore) {
17813
18637
  return { text: "当前没有加载可写入的配置。" };
17814
18638
  }
@@ -17816,9 +18640,18 @@ async function handleWorkspaceCreate(context, workspaceName, cwd) {
17816
18640
  if (!await pathExists(normalizedCwd)) {
17817
18641
  return { text: `工作区路径不存在:${cwd}` };
17818
18642
  }
17819
- 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);
17820
18651
  context.replaceConfig(updated);
17821
- return { text: `工作区「${workspaceName}」已保存` };
18652
+ const savedLine = `工作区「${name}」已保存`;
18653
+ return { text: notice ? `${notice}
18654
+ ${savedLine}` : savedLine };
17822
18655
  }
17823
18656
  async function handleWorkspaceRemove(context, workspaceName) {
17824
18657
  if (!context.config || !context.configStore) {
@@ -17830,6 +18663,7 @@ async function handleWorkspaceRemove(context, workspaceName) {
17830
18663
  }
17831
18664
  var workspaceHelp;
17832
18665
  var init_workspace_handler = __esm(() => {
18666
+ init_workspace_name();
17833
18667
  init_workspace_path();
17834
18668
  workspaceHelp = {
17835
18669
  topic: "workspace",
@@ -17838,7 +18672,7 @@ var init_workspace_handler = __esm(() => {
17838
18672
  commands: [
17839
18673
  { usage: "/workspaces", description: "查看当前已注册的工作区" },
17840
18674
  { usage: "/workspace 或 /ws", description: "查看工作区列表" },
17841
- { usage: "/ws new <name> -d <path>", description: "添加工作区" },
18675
+ { usage: "/ws new <name> -d <path> [--raw]", description: "添加工作区;含特殊字符的名称会被自动规范化,--raw 保留原名" },
17842
18676
  { usage: "/workspace rm <name>", description: "删除工作区" }
17843
18677
  ],
17844
18678
  examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
@@ -18075,7 +18909,7 @@ async function resolveShortcutWorkspace(context, target) {
18075
18909
  reused: true
18076
18910
  };
18077
18911
  }
18078
- const workspaceName = allocateWorkspaceName(context, basenameForWorkspacePath(cwd));
18912
+ const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), context.config?.workspaces ?? {});
18079
18913
  const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
18080
18914
  context.replaceConfig(updated);
18081
18915
  return {
@@ -18084,16 +18918,6 @@ async function resolveShortcutWorkspace(context, target) {
18084
18918
  reused: false
18085
18919
  };
18086
18920
  }
18087
- function allocateWorkspaceName(context, baseName) {
18088
- if (!context.config?.workspaces[baseName]) {
18089
- return baseName;
18090
- }
18091
- let suffix = 2;
18092
- while (context.config.workspaces[`${baseName}-${suffix}`]) {
18093
- suffix += 1;
18094
- }
18095
- return `${baseName}-${suffix}`;
18096
- }
18097
18921
  async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
18098
18922
  if (!await hasLogicalSession(context, baseAlias, chatKey)) {
18099
18923
  return baseAlias;
@@ -18119,6 +18943,7 @@ function renderShortcutSessionCreationError(workspace, alias) {
18119
18943
  };
18120
18944
  }
18121
18945
  var init_session_shortcut_handler = __esm(() => {
18946
+ init_workspace_name();
18122
18947
  init_workspace_path();
18123
18948
  init_errors();
18124
18949
  init_channel_scope();
@@ -18131,8 +18956,8 @@ function renderTransportError(session, error2) {
18131
18956
  return {
18132
18957
  text: [
18133
18958
  `当前会话「${session.alias}」暂时不可用。`,
18134
- `请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
18135
- `如果你要绑定一个已有会话,再执行:/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 <会话名>`
18136
18961
  ].join(`
18137
18962
  `)
18138
18963
  };
@@ -18194,7 +19019,7 @@ function renderSessionCreationFailure(session, detail) {
18194
19019
  text: [
18195
19020
  "会话创建失败。",
18196
19021
  `错误信息:${summarizeTransportError(detail)}`,
18197
- `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
19022
+ `如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
18198
19023
  ].join(`
18199
19024
  `)
18200
19025
  };
@@ -18213,6 +19038,7 @@ async function tryRecoverMissingSession(ops, session, error2) {
18213
19038
  }
18214
19039
  var init_session_recovery_handler = __esm(() => {
18215
19040
  init_errors();
19041
+ init_workspace_name();
18216
19042
  });
18217
19043
 
18218
19044
  // src/recovery/auto-install-optional-dep.ts
@@ -18338,10 +19164,10 @@ ${err.message}`, reason: "spawn" });
18338
19164
  const dir = join10(homedir6(), ".weacpx", "logs");
18339
19165
  await mkdir10(dir, { recursive: true });
18340
19166
  const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
18341
- const path13 = join10(dir, `auto-install-${timestamp}.log`);
18342
- const stream = createWriteStream(path13, { flags: "a" });
19167
+ const path14 = join10(dir, `auto-install-${timestamp}.log`);
19168
+ const stream = createWriteStream(path14, { flags: "a" });
18343
19169
  return {
18344
- path: path13,
19170
+ path: path14,
18345
19171
  append: async (chunk) => {
18346
19172
  await new Promise((resolve3, reject) => stream.write(chunk, (err) => err ? reject(err) : resolve3()));
18347
19173
  },
@@ -18424,9 +19250,9 @@ function isUnder(child, parent) {
18424
19250
  const p = parent.replace(/[\\/]+$/, "");
18425
19251
  return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
18426
19252
  }
18427
- async function defaultFsExists(path13) {
19253
+ async function defaultFsExists(path14) {
18428
19254
  try {
18429
- await access3(path13);
19255
+ await access3(path14);
18430
19256
  return true;
18431
19257
  } catch {
18432
19258
  return false;
@@ -18595,7 +19421,7 @@ class CommandRouter {
18595
19421
  this.quota = quota;
18596
19422
  this.logger = logger2 ?? createNoopAppLogger();
18597
19423
  }
18598
- async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, perfSpan) {
19424
+ async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan) {
18599
19425
  const startedAt = Date.now();
18600
19426
  const command = parseCommand(input);
18601
19427
  await this.logger.debug("command.parsed", "parsed inbound command", {
@@ -18654,7 +19480,7 @@ class CommandRouter {
18654
19480
  case "workspaces":
18655
19481
  return handleWorkspaces(this.createHandlerContext());
18656
19482
  case "workspace.new":
18657
- return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
19483
+ return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd, command.raw ? { raw: true } : {});
18658
19484
  case "workspace.rm":
18659
19485
  return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
18660
19486
  case "sessions":
@@ -18685,6 +19511,8 @@ class CommandRouter {
18685
19511
  return await handleCancel(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18686
19512
  case "session.reset":
18687
19513
  return await handleSessionReset(this.createSessionHandlerContext(reply, perfSpan), chatKey);
19514
+ case "session.tail":
19515
+ return await handleSessionTail(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.lines);
18688
19516
  case "session.rm":
18689
19517
  return await handleSessionRemove(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
18690
19518
  case "groups":
@@ -18712,7 +19540,7 @@ class CommandRouter {
18712
19540
  case "task.cancel":
18713
19541
  return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
18714
19542
  case "prompt":
18715
- return await handlePrompt(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
19543
+ return await handlePrompt(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
18716
19544
  }
18717
19545
  });
18718
19546
  }
@@ -18765,7 +19593,7 @@ class CommandRouter {
18765
19593
  return {
18766
19594
  setModeTransportSession: (session, modeId) => this.setModeTransportSession(session, modeId),
18767
19595
  cancelTransportSession: (session) => this.cancelTransportSession(session),
18768
- promptTransportSession: (session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpanOverride) => this.promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpanOverride ?? perfSpan)
19596
+ promptTransportSession: (session, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride) => this.promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride ?? perfSpan)
18769
19597
  };
18770
19598
  }
18771
19599
  createSessionRenderRecoveryOps() {
@@ -18943,7 +19771,7 @@ class CommandRouter {
18943
19771
  async checkTransportSession(session) {
18944
19772
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
18945
19773
  }
18946
- async promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpan) {
19774
+ async promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan) {
18947
19775
  session.mcpCoordinatorSession ??= session.transportSession;
18948
19776
  let done = false;
18949
19777
  let abortRequested = false;
@@ -18999,7 +19827,8 @@ class CommandRouter {
18999
19827
  return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply, replyContext, {
19000
19828
  ...media ? { media } : {},
19001
19829
  ...reply ? { onSegment } : {},
19002
- ...onToolEvent ? { onToolEvent } : {}
19830
+ ...onToolEvent ? { onToolEvent } : {},
19831
+ ...onThought ? { onThought } : {}
19003
19832
  }));
19004
19833
  } catch (error2) {
19005
19834
  localOutcome = isAbortError2(error2) || abortRequested ? "aborted" : "error";
@@ -19113,7 +19942,7 @@ function resolveAcpxCommandMetadata(options = {}) {
19113
19942
  }
19114
19943
  const platform = options.platform ?? process.platform;
19115
19944
  const resolvePackageJson = options.resolvePackageJson ?? ((id) => require3.resolve(id));
19116
- const readPackageJson = options.readPackageJson ?? ((path13) => JSON.parse(readFileSync(path13, "utf8")));
19945
+ const readPackageJson = options.readPackageJson ?? ((path14) => JSON.parse(readFileSync(path14, "utf8")));
19117
19946
  try {
19118
19947
  const packageJsonPath = resolvePackageJson("acpx/package.json");
19119
19948
  const pkg = readPackageJson(packageJsonPath);
@@ -19165,7 +19994,7 @@ class ConsoleAgent {
19165
19994
  ...m.fileName ? { fileName: m.fileName } : {}
19166
19995
  })) : undefined;
19167
19996
  request.perfSpan?.mark("agent.dispatched");
19168
- return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.perfSpan);
19997
+ return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.onThought, request.perfSpan);
19169
19998
  }
19170
19999
  isKnownCommand(text) {
19171
20000
  return isKnownWeacpxCommandText(text);
@@ -19490,8 +20319,8 @@ class OrchestrationServer {
19490
20319
  if (this.endpoint.kind !== "unix") {
19491
20320
  return;
19492
20321
  }
19493
- const removeFile = this.deps.removeFile ?? (async (path13) => {
19494
- await rm7(path13, { force: true });
20322
+ const removeFile = this.deps.removeFile ?? (async (path14) => {
20323
+ await rm7(path14, { force: true });
19495
20324
  });
19496
20325
  await removeFile(this.endpoint.path);
19497
20326
  }
@@ -19652,9 +20481,9 @@ function requireTaskQuestions(params, key) {
19652
20481
  };
19653
20482
  });
19654
20483
  }
19655
- async function canConnectToEndpoint(path13) {
20484
+ async function canConnectToEndpoint(path14) {
19656
20485
  return await new Promise((resolve3) => {
19657
- const socket = createConnection2(path13);
20486
+ const socket = createConnection2(path14);
19658
20487
  let settled = false;
19659
20488
  const finish = (result) => {
19660
20489
  if (settled) {
@@ -19675,7 +20504,7 @@ async function canConnectToEndpoint(path13) {
19675
20504
  });
19676
20505
  });
19677
20506
  }
19678
- async function listen(server, path13) {
20507
+ async function listen(server, path14) {
19679
20508
  await new Promise((resolve3, reject) => {
19680
20509
  const onError = (error2) => {
19681
20510
  server.off("listening", onListening);
@@ -19687,7 +20516,7 @@ async function listen(server, path13) {
19687
20516
  };
19688
20517
  server.once("error", onError);
19689
20518
  server.once("listening", onListening);
19690
- server.listen(path13);
20519
+ server.listen(path14);
19691
20520
  });
19692
20521
  }
19693
20522
  function isServerNotRunningError(error2) {
@@ -19811,6 +20640,7 @@ class OrchestrationService {
19811
20640
  stateMutex;
19812
20641
  pendingWorkerSessions = new Map;
19813
20642
  pendingLogicalTransportSessions = new Map;
20643
+ pendingParallelStarts = new Map;
19814
20644
  constructor(deps) {
19815
20645
  this.deps = deps;
19816
20646
  this.stateMutex = deps.stateMutex ?? new AsyncMutex;
@@ -19975,84 +20805,139 @@ class OrchestrationService {
19975
20805
  const normalizedGroupId = this.normalizeGroupId(input.groupId);
19976
20806
  const taskId = this.deps.createId();
19977
20807
  const workerSession = await this.resolveWorkerSession(input);
19978
- const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
19979
- let ensuredWorkerSession = workerSession;
19980
- let prepared;
19981
- try {
19982
- ensuredWorkerSession = await this.ensureReservedWorkerSession({
19983
- workerSession,
19984
- sourceHandle: input.sourceHandle,
19985
- sourceKind: input.sourceKind,
19986
- coordinatorSession: input.coordinatorSession,
19987
- workspace: input.workspace,
19988
- ...input.cwd ? { cwd: input.cwd } : {},
19989
- targetAgent: input.targetAgent,
19990
- role
19991
- });
19992
- prepared = await this.mutate(async () => {
20808
+ if (input.parallel) {
20809
+ const queuedResult = await this.mutate(async () => {
19993
20810
  const state = await this.deps.loadState();
19994
- const now = this.deps.now().toISOString();
19995
- if (normalizedGroupId) {
19996
- 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;
19997
20814
  }
19998
- const task = {
20815
+ const now = this.deps.now().toISOString();
20816
+ const queuedTask = {
19999
20817
  taskId,
20000
20818
  sourceHandle: input.sourceHandle,
20001
20819
  sourceKind: input.sourceKind,
20002
20820
  coordinatorSession: input.coordinatorSession,
20003
- workerSession: ensuredWorkerSession,
20821
+ workerSession,
20004
20822
  workspace: input.workspace,
20005
20823
  ...input.cwd ? { cwd: input.cwd } : {},
20006
20824
  targetAgent: input.targetAgent,
20007
20825
  ...role ? { role } : {},
20008
20826
  ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
20009
20827
  task: input.task,
20010
- status: "running",
20828
+ status: "queued",
20829
+ ephemeralWorkerSession: true,
20011
20830
  summary: "",
20012
20831
  resultText: "",
20013
20832
  createdAt: now,
20014
20833
  updatedAt: now,
20015
20834
  eventSeq: 1,
20016
- 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" }],
20017
20836
  ...input.chatKey ? { chatKey: input.chatKey } : {},
20018
20837
  ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
20019
20838
  ...input.accountId ? { accountId: input.accountId } : {}
20020
20839
  };
20021
- let previousGroup;
20022
- if (normalizedGroupId) {
20023
- const group = this.ensureGroups(state)[normalizedGroupId];
20024
- previousGroup = { ...group };
20025
- group.updatedAt = now;
20026
- group.coordinatorInjectedAt = undefined;
20027
- group.injectionPending = undefined;
20028
- group.injectionAppliedAt = undefined;
20029
- group.lastInjectionError = undefined;
20030
- }
20031
- const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
20032
- this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
20033
- this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
20034
- state.orchestration.tasks[taskId] = task;
20035
- state.orchestration.workerBindings[ensuredWorkerSession] = {
20036
- 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,
20037
20866
  coordinatorSession: input.coordinatorSession,
20038
20867
  workspace: input.workspace,
20039
20868
  ...input.cwd ? { cwd: input.cwd } : {},
20040
20869
  targetAgent: input.targetAgent,
20041
20870
  role
20042
- };
20043
- await this.deps.saveState(state);
20044
- return {
20045
- task: { ...task },
20046
- previousBinding,
20047
- previousGroup,
20048
- normalizedGroupId
20049
- };
20050
- });
20051
- } 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
+ }
20052
20937
  await releaseWorkerReservation();
20053
- throw error2;
20938
+ } finally {
20939
+ releasePendingParallelStart?.();
20054
20940
  }
20055
- await releaseWorkerReservation();
20056
20941
  try {
20057
20942
  await this.deps.dispatchWorkerTask({
20058
20943
  taskId,
@@ -20102,6 +20987,7 @@ class OrchestrationService {
20102
20987
  return { sourceContext, targetLocation, role, normalizedGroupId };
20103
20988
  });
20104
20989
  const autoRun = preflight.sourceContext.sourceKind === "coordinator";
20990
+ const taskId = this.deps.createId();
20105
20991
  const workerSessionName = await this.resolveWorkerSession({
20106
20992
  sourceHandle: input.sourceHandle,
20107
20993
  sourceKind: preflight.sourceContext.sourceKind,
@@ -20110,18 +20996,18 @@ class OrchestrationService {
20110
20996
  ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
20111
20997
  targetAgent: input.targetAgent,
20112
20998
  task: input.task,
20113
- ...preflight.role ? { role: preflight.role } : {}
20999
+ ...preflight.role ? { role: preflight.role } : {},
21000
+ ...input.parallel ? { parallel: true } : {}
20114
21001
  });
20115
- const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
20116
- let prepared;
20117
- try {
20118
- prepared = await this.mutate(async () => {
21002
+ if (input.parallel && autoRun) {
21003
+ const queuedResult = await this.mutate(async () => {
20119
21004
  const state = await this.deps.loadState();
20120
- 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
+ }
20121
21009
  const now = this.deps.now().toISOString();
20122
- const taskId = this.deps.createId();
20123
- const status = autoRun ? "running" : "needs_confirmation";
20124
- const task = {
21010
+ const queuedTask = {
20125
21011
  taskId,
20126
21012
  sourceHandle: input.sourceHandle,
20127
21013
  sourceKind: preflight.sourceContext.sourceKind,
@@ -20133,49 +21019,101 @@ class OrchestrationService {
20133
21019
  ...preflight.role ? { role: preflight.role } : {},
20134
21020
  ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
20135
21021
  task: input.task,
20136
- status,
21022
+ status: "queued",
21023
+ ephemeralWorkerSession: true,
20137
21024
  summary: "",
20138
21025
  resultText: "",
20139
21026
  createdAt: now,
20140
21027
  updatedAt: now,
20141
21028
  eventSeq: 1,
20142
- 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" }]
20143
21030
  };
20144
- if (preflight.normalizedGroupId) {
20145
- const group = this.ensureGroups(state)[preflight.normalizedGroupId];
20146
- group.updatedAt = now;
20147
- group.coordinatorInjectedAt = undefined;
20148
- group.injectionPending = undefined;
20149
- group.injectionAppliedAt = undefined;
20150
- group.lastInjectionError = undefined;
20151
- }
20152
- let previousBinding;
20153
- if (autoRun) {
20154
- previousBinding = state.orchestration.workerBindings[workerSessionName];
20155
- this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
20156
- this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
20157
- state.orchestration.tasks[taskId] = task;
20158
- state.orchestration.workerBindings[workerSessionName] = {
20159
- 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,
20160
21061
  coordinatorSession: preflight.sourceContext.coordinatorSession,
21062
+ workerSession: workerSessionName,
20161
21063
  workspace: preflight.targetLocation.workspace,
20162
21064
  ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
20163
21065
  targetAgent: input.targetAgent,
20164
- 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 } : {}
20165
21077
  };
20166
- } else {
20167
- this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
20168
- this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
20169
- state.orchestration.tasks[taskId] = task;
20170
- }
20171
- await this.deps.saveState(state);
20172
- return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
20173
- });
20174
- } 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
+ }
20175
21113
  await releaseWorkerReservation();
20176
- throw error2;
21114
+ } finally {
21115
+ releasePendingParallelStart?.();
20177
21116
  }
20178
- await releaseWorkerReservation();
20179
21117
  if (autoRun) {
20180
21118
  this.runAutoRunRpcWorkerTask({
20181
21119
  task: prepared.task,
@@ -21144,6 +22082,16 @@ class OrchestrationService {
21144
22082
  await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
21145
22083
  }
21146
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
+ }
21147
22095
  return prepared.task;
21148
22096
  }
21149
22097
  async listTasks(filter) {
@@ -21610,6 +22558,16 @@ class OrchestrationService {
21610
22558
  if (prepared.closedPackageId) {
21611
22559
  await this.handoffQueuedQuestions(prepared.task.coordinatorSession, prepared.closedPackageId);
21612
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
+ }
21613
22571
  return prepared.task;
21614
22572
  }
21615
22573
  async completeTaskCancellation(taskId) {
@@ -21672,6 +22630,14 @@ class OrchestrationService {
21672
22630
  return prepared.task;
21673
22631
  }
21674
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
+ }
21675
22641
  return prepared.task;
21676
22642
  }
21677
22643
  async failTaskCancellation(taskId, errorMessage) {
@@ -21716,6 +22682,34 @@ class OrchestrationService {
21716
22682
  task: currentTask.task,
21717
22683
  ...currentTask.role ? { role: currentTask.role } : {}
21718
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
+ }
21719
22713
  const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession, input.taskId);
21720
22714
  let ensuredWorkerSession = workerSession;
21721
22715
  let prepared;
@@ -21757,7 +22751,8 @@ class OrchestrationService {
21757
22751
  workspace: task.workspace,
21758
22752
  ...task.cwd ? { cwd: task.cwd } : {},
21759
22753
  targetAgent: task.targetAgent,
21760
- role: task.role
22754
+ role: task.role,
22755
+ ...task.ephemeralWorkerSession ? { ephemeral: true } : {}
21761
22756
  };
21762
22757
  await this.deps.saveState(state);
21763
22758
  return {
@@ -21811,6 +22806,10 @@ class OrchestrationService {
21811
22806
  }
21812
22807
  async resolveWorkerSession(input) {
21813
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
+ }
21814
22813
  const reusable = await this.deps.findReusableWorkerSession?.({
21815
22814
  sourceHandle: input.sourceHandle,
21816
22815
  sourceKind: input.sourceKind,
@@ -21823,7 +22822,7 @@ class OrchestrationService {
21823
22822
  if (reusable && reusable.trim().length > 0) {
21824
22823
  return reusable.trim();
21825
22824
  }
21826
- 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;
21827
22826
  }
21828
22827
  async reserveProposedWorkerSession(workerSession, excludingTaskId) {
21829
22828
  await this.mutate(async () => {
@@ -21981,7 +22980,7 @@ class OrchestrationService {
21981
22980
  if (role && policy.allowedAgentRequestRoles.length > 0 && !policy.allowedAgentRequestRoles.includes(role)) {
21982
22981
  throw new Error(`role "${role}" is not allowed for agent-requested delegation`);
21983
22982
  }
21984
- 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"));
21985
22984
  if (outstandingRequests.length >= policy.maxPendingAgentRequestsPerCoordinator) {
21986
22985
  throw new Error("agent-requested delegation quota exceeded for this coordinator");
21987
22986
  }
@@ -22146,6 +23145,137 @@ class OrchestrationService {
22146
23145
  hasActiveTaskWorkerSession(state, workerSession, excludingTaskId) {
22147
23146
  return Object.values(state.orchestration.tasks).some((task) => task.taskId !== excludingTaskId && task.workerSession === workerSession && (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined));
22148
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
+ }
22149
23279
  async assertProposedWorkerSessionDoesNotConflictExternalCoordinator(workerSession) {
22150
23280
  const state = await this.deps.loadState();
22151
23281
  this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
@@ -23156,6 +24286,13 @@ async function runConsole(paths, deps) {
23156
24286
  trigger: "startup"
23157
24287
  });
23158
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
+ }
23159
24296
  consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
23160
24297
  if (consumerLock) {
23161
24298
  const lockMeta = {
@@ -23333,6 +24470,10 @@ function encodeBridgePromptToolEvent(event) {
23333
24470
  return `${JSON.stringify(event)}
23334
24471
  `;
23335
24472
  }
24473
+ function encodeBridgePromptThoughtEvent(event) {
24474
+ return `${JSON.stringify(event)}
24475
+ `;
24476
+ }
23336
24477
  function encodeBridgeSessionProgressEvent(event) {
23337
24478
  return `${JSON.stringify(event)}
23338
24479
  `;
@@ -23405,6 +24546,11 @@ class AcpxBridgeClient {
23405
24546
  type: "prompt.tool_event",
23406
24547
  event: message.toolEvent
23407
24548
  });
24549
+ } else if (message.event === "prompt.thought") {
24550
+ pending.onEvent?.({
24551
+ type: "prompt.thought",
24552
+ text: message.text
24553
+ });
23408
24554
  } else if (message.event === "session.progress") {
23409
24555
  pending.onEvent?.({
23410
24556
  type: "session.progress",
@@ -23754,6 +24900,12 @@ class AcpxBridgeTransport {
23754
24900
  }
23755
24901
  } : undefined);
23756
24902
  }
24903
+ async tailSessionHistory(session, lines) {
24904
+ return await this.client.request("tailSessionHistory", {
24905
+ ...this.toParams(session),
24906
+ lines
24907
+ });
24908
+ }
23757
24909
  async prompt(session, text, reply, replyContext, options) {
23758
24910
  const sink = reply ? createQuotaGatedReplySink({
23759
24911
  reply,
@@ -23763,6 +24915,8 @@ class AcpxBridgeTransport {
23763
24915
  let segmentChain = Promise.resolve();
23764
24916
  let toolEventError;
23765
24917
  let toolEventChain = Promise.resolve();
24918
+ let thoughtError;
24919
+ let thoughtChain = Promise.resolve();
23766
24920
  let toolEventMode = resolveToolEventMode(options);
23767
24921
  if ((toolEventMode === "structured" || toolEventMode === "both") && !options?.onToolEvent) {
23768
24922
  toolEventMode = "text";
@@ -23795,9 +24949,20 @@ class AcpxBridgeTransport {
23795
24949
  }
23796
24950
  return;
23797
24951
  }
24952
+ if (event.type === "prompt.thought") {
24953
+ const onThought = options?.onThought;
24954
+ if (onThought) {
24955
+ const thoughtText = event.text;
24956
+ thoughtChain = thoughtChain.then(() => onThought(thoughtText)).catch((error2) => {
24957
+ thoughtError ??= error2;
24958
+ });
24959
+ }
24960
+ return;
24961
+ }
23798
24962
  });
23799
24963
  await segmentChain;
23800
24964
  await toolEventChain;
24965
+ await thoughtChain;
23801
24966
  if (sink) {
23802
24967
  const { overflowCount } = sink.finalize();
23803
24968
  await sink.drain({ timeoutMs: 30000 });
@@ -23812,6 +24977,9 @@ class AcpxBridgeTransport {
23812
24977
  if (toolEventError) {
23813
24978
  throw toolEventError;
23814
24979
  }
24980
+ if (thoughtError) {
24981
+ throw thoughtError;
24982
+ }
23815
24983
  return { text: summary ? `${summary}
23816
24984
 
23817
24985
  ${result.text}` : "" };
@@ -23822,6 +24990,9 @@ ${result.text}` : "" };
23822
24990
  if (toolEventError) {
23823
24991
  throw toolEventError;
23824
24992
  }
24993
+ if (thoughtError) {
24994
+ throw thoughtError;
24995
+ }
23825
24996
  return result;
23826
24997
  }
23827
24998
  async setMode(session, modeId) {
@@ -23880,7 +25051,7 @@ var init_spawn_command = __esm(() => {
23880
25051
  // src/transport/prompt-media.ts
23881
25052
  import { mkdtemp, open as open3, rm as rm8, writeFile as writeFile8 } from "node:fs/promises";
23882
25053
  import { tmpdir as defaultTmpdir } from "node:os";
23883
- import path13 from "node:path";
25054
+ import path14 from "node:path";
23884
25055
  import { pathToFileURL as pathToFileURL2 } from "node:url";
23885
25056
  async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
23886
25057
  const mediaList = normalizePromptMedia(media);
@@ -23914,7 +25085,7 @@ async function createStructuredPromptFile(text, media, deps = defaultStructuredP
23914
25085
  type: "resource",
23915
25086
  resource: {
23916
25087
  uri: pathToFileURL2(item.filePath).toString(),
23917
- text: `${item.fileName ?? path13.basename(item.filePath)} ${item.mimeType} ${item.type}`
25088
+ text: `${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.type}`
23918
25089
  }
23919
25090
  });
23920
25091
  }
@@ -23928,7 +25099,7 @@ function normalizePromptMedia(media) {
23928
25099
  function buildAttachmentSummary(items) {
23929
25100
  const lines = ["Attachments available as local files:"];
23930
25101
  for (const [index, item] of items.entries()) {
23931
- 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}`);
23932
25103
  }
23933
25104
  return lines.join(`
23934
25105
  `);
@@ -23936,8 +25107,8 @@ function buildAttachmentSummary(items) {
23936
25107
  async function writeStructuredPromptBlocks(blocks, deps) {
23937
25108
  let dir = "";
23938
25109
  try {
23939
- dir = await deps.mkdtemp(path13.join(deps.tmpdir(), "weacpx-acp-prompt-"));
23940
- 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");
23941
25112
  await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
23942
25113
  return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
23943
25114
  } catch (error2) {
@@ -24026,6 +25197,7 @@ var init_tool_kind_emoji = __esm(() => {
24026
25197
  function createStreamingPromptState(formatToolCalls = false, options) {
24027
25198
  let toolEventMode;
24028
25199
  let onToolEvent;
25200
+ let onThought;
24029
25201
  if (options === undefined) {
24030
25202
  toolEventMode = "text";
24031
25203
  onToolEvent = undefined;
@@ -24034,6 +25206,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
24034
25206
  toolEventMode = "structured";
24035
25207
  } else {
24036
25208
  onToolEvent = options.onToolEvent;
25209
+ onThought = options.onThought;
24037
25210
  toolEventMode = resolveToolEventMode({
24038
25211
  toolEventMode: options.mode,
24039
25212
  onToolEvent
@@ -24048,6 +25221,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
24048
25221
  emittedToolCallIds: new Set,
24049
25222
  toolEventMode,
24050
25223
  onToolEvent,
25224
+ onThought,
24051
25225
  finalize() {
24052
25226
  if (this.pendingLine.trim().length > 0) {
24053
25227
  parseStreamingChunks(this, this.pendingLine);
@@ -24106,6 +25280,14 @@ function parseStreamingChunks(state, line) {
24106
25280
  }
24107
25281
  return;
24108
25282
  }
25283
+ const isThoughtChunk = update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
25284
+ if (isThoughtChunk) {
25285
+ const chunk2 = update.content.text;
25286
+ if (chunk2.length > 0) {
25287
+ state.onThought?.(chunk2);
25288
+ }
25289
+ return;
25290
+ }
24109
25291
  const isMessageChunk = update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
24110
25292
  if (!isMessageChunk)
24111
25293
  return;
@@ -24133,7 +25315,7 @@ function formatToolCallEvent(update, sessionUpdate) {
24133
25315
  if (title.length === 0)
24134
25316
  return null;
24135
25317
  const emoji2 = TOOL_KIND_EMOJI[kind] ?? DEFAULT_TOOL_EMOJI;
24136
- const inputSummary = summarizeToolInput(update.rawInput, title);
25318
+ const inputSummary = summarizeToolInput(update.rawInput, title) || summarizeToolInput(update.rawOutput, title);
24137
25319
  const status = readString(update, "status");
24138
25320
  if (!inputSummary && status === "pending")
24139
25321
  return null;
@@ -24164,15 +25346,23 @@ function buildToolUseEvent(update) {
24164
25346
  })();
24165
25347
  const title = (update.title ?? "").trim();
24166
25348
  const toolName = title || "Tool";
24167
- const summaryRaw = summarizeToolInput(update.rawInput, title);
25349
+ const summaryRaw = summarizeToolInput(update.rawInput, title) || summarizeToolInput(update.rawOutput, title);
24168
25350
  const summary = summaryRaw && summaryRaw !== title ? summaryRaw : undefined;
24169
25351
  const statusRaw = readString(update, "status");
24170
25352
  const status = statusRaw === "completed" || statusRaw === "success" ? "success" : statusRaw === "failed" || statusRaw === "error" ? "error" : "running";
25353
+ const rawInput = update.rawInput;
25354
+ const content = update.content;
25355
+ const rawOutput = update.rawOutput;
25356
+ const locations = update.locations;
24171
25357
  return {
24172
25358
  toolCallId,
24173
25359
  toolName,
24174
25360
  kind,
24175
25361
  ...summary ? { summary } : {},
25362
+ ...rawInput !== undefined ? { rawInput } : {},
25363
+ ...content !== undefined ? { content } : {},
25364
+ ...rawOutput !== undefined ? { rawOutput } : {},
25365
+ ...locations !== undefined ? { locations } : {},
24176
25366
  status
24177
25367
  };
24178
25368
  }
@@ -24333,6 +25523,8 @@ function buildQueueOwnerPayload(input) {
24333
25523
  nonInteractivePermissions: input.nonInteractivePermissions,
24334
25524
  ttlMs: input.ttlMs ?? 300000,
24335
25525
  maxQueueDepth: input.maxQueueDepth ?? 16,
25526
+ ...Number.isFinite(input.promptRetries) ? { promptRetries: input.promptRetries } : {},
25527
+ ...input.sessionOptions ? { sessionOptions: input.sessionOptions } : {},
24336
25528
  mcpServers: input.mcpServers
24337
25529
  };
24338
25530
  }
@@ -24585,6 +25777,7 @@ class AcpxCliTransport {
24585
25777
  sessionInitTimeoutMs;
24586
25778
  permissionMode;
24587
25779
  nonInteractivePermissions;
25780
+ permissionPolicy;
24588
25781
  runCommand;
24589
25782
  runPtyCommand;
24590
25783
  queueOwnerLauncher;
@@ -24594,6 +25787,7 @@ class AcpxCliTransport {
24594
25787
  this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
24595
25788
  this.permissionMode = options.permissionMode ?? "approve-all";
24596
25789
  this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
25790
+ this.permissionPolicy = options.permissionPolicy;
24597
25791
  this.runCommand = runCommand;
24598
25792
  this.runPtyCommand = runPtyCommand;
24599
25793
  this.queueOwnerLauncher = queueOwnerLauncher ?? new AcpxQueueOwnerLauncher({
@@ -24613,18 +25807,38 @@ class AcpxCliTransport {
24613
25807
  timeoutMs: this.sessionInitTimeoutMs
24614
25808
  });
24615
25809
  }
25810
+ async tailSessionHistory(session, lines) {
25811
+ const candidates = [
25812
+ ["sessions", "history", "quiet", "-s", session.transportSession, String(lines)],
25813
+ ["sessions", "history", "quiet", session.transportSession, String(lines)],
25814
+ ["sessions", "history", "-s", session.transportSession, "--tail", String(lines)],
25815
+ ["sessions", "history", session.transportSession, "--tail", String(lines)],
25816
+ ["sessions", "history", "--name", session.transportSession, "--tail", String(lines)]
25817
+ ];
25818
+ let lastResult;
25819
+ for (const tail2 of candidates) {
25820
+ const args = this.buildArgs(session, tail2);
25821
+ const result = await this.runCommandWithTimeout(this.runCommand, args);
25822
+ if (result.code === 0) {
25823
+ return { text: result.stdout.trimEnd() };
25824
+ }
25825
+ lastResult = result;
25826
+ }
25827
+ const detail = lastResult ? normalizeCommandError(lastResult) ?? `command failed with exit code ${lastResult.code}` : "command failed";
25828
+ throw new Error(detail);
25829
+ }
24616
25830
  async prompt(session, text, reply, replyContext, options) {
24617
25831
  await this.launchMcpQueueOwnerIfNeeded(session);
24618
25832
  const structuredPrompt = await createStructuredPromptFile(text, options?.media);
24619
25833
  const args = this.buildPromptArgs(session, text, structuredPrompt?.filePath);
24620
25834
  try {
24621
- if (reply || options?.onSegment || options?.onToolEvent) {
25835
+ if (reply || options?.onSegment || options?.onToolEvent || options?.onThought) {
24622
25836
  const formatToolCalls = (session.replyMode ?? "verbose") === "verbose";
24623
25837
  let toolEventMode = resolveToolEventMode(options);
24624
25838
  if ((toolEventMode === "structured" || toolEventMode === "both") && !options?.onToolEvent) {
24625
25839
  toolEventMode = "text";
24626
25840
  }
24627
- const { result: result2, overflowCount } = await this.runStreamingPrompt(this.command, args, reply, formatToolCalls, toolEventMode, replyContext, options?.onSegment, options?.onToolEvent);
25841
+ const { result: result2, overflowCount } = await this.runStreamingPrompt(this.command, args, reply, formatToolCalls, toolEventMode, replyContext, options?.onSegment, options?.onToolEvent, options?.onThought);
24628
25842
  const baseText = getPromptText(result2);
24629
25843
  if (!reply) {
24630
25844
  return { text: baseText };
@@ -24664,6 +25878,7 @@ ${baseText}` : "" };
24664
25878
  async updatePermissionPolicy(policy) {
24665
25879
  this.permissionMode = policy.permissionMode;
24666
25880
  this.nonInteractivePermissions = policy.nonInteractivePermissions;
25881
+ this.permissionPolicy = policy.permissionPolicy;
24667
25882
  }
24668
25883
  async removeSession(session) {
24669
25884
  const result = await this.runCommand(this.command, this.buildArgs(session, [
@@ -24761,7 +25976,7 @@ ${baseText}` : "" };
24761
25976
  })
24762
25977
  ]);
24763
25978
  }
24764
- async runStreamingPrompt(command, args, reply, formatToolCalls = false, toolEventMode = "text", replyContext, onSegment, onToolEvent) {
25979
+ async runStreamingPrompt(command, args, reply, formatToolCalls = false, toolEventMode = "text", replyContext, onSegment, onToolEvent, onThought) {
24765
25980
  const hooks = this.streamingHooks;
24766
25981
  const doSpawn = hooks.spawnPrompt ?? ((cmd, spawnArgs) => spawn9(cmd, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
24767
25982
  const setIntervalFn = hooks.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
@@ -24779,7 +25994,10 @@ ${baseText}` : "" };
24779
25994
  let segmentError;
24780
25995
  let toolEventChain = Promise.resolve();
24781
25996
  let toolEventError;
25997
+ let thoughtChain = Promise.resolve();
25998
+ let thoughtError;
24782
25999
  const userOnToolEvent = onToolEvent;
26000
+ const userOnThought = onThought;
24783
26001
  const state = createStreamingPromptState(formatToolCalls, {
24784
26002
  mode: toolEventMode,
24785
26003
  ...userOnToolEvent ? {
@@ -24788,6 +26006,13 @@ ${baseText}` : "" };
24788
26006
  toolEventError ??= error2;
24789
26007
  });
24790
26008
  }
26009
+ } : {},
26010
+ ...userOnThought ? {
26011
+ onThought: (chunk) => {
26012
+ thoughtChain = thoughtChain.then(() => userOnThought(chunk)).catch((error2) => {
26013
+ thoughtError ??= error2;
26014
+ });
26015
+ }
24791
26016
  } : {}
24792
26017
  });
24793
26018
  const sink = reply ? createQuotaGatedReplySink({
@@ -24840,7 +26065,8 @@ ${baseText}` : "" };
24840
26065
  Promise.all([
24841
26066
  sink?.drain({ timeoutMs: 30000 }) ?? Promise.resolve(),
24842
26067
  segmentChain,
24843
- toolEventChain
26068
+ toolEventChain,
26069
+ thoughtChain
24844
26070
  ]).then(() => {
24845
26071
  const deferred = sink?.getPendingError();
24846
26072
  if (deferred) {
@@ -24855,6 +26081,10 @@ ${baseText}` : "" };
24855
26081
  reject(toolEventError);
24856
26082
  return;
24857
26083
  }
26084
+ if (thoughtError) {
26085
+ reject(thoughtError);
26086
+ return;
26087
+ }
24858
26088
  resolve3({
24859
26089
  result: { code: code ?? 1, stdout: stdout2, stderr },
24860
26090
  overflowCount
@@ -24895,7 +26125,11 @@ ${baseText}` : "" };
24895
26125
  }
24896
26126
  buildPermissionArgs() {
24897
26127
  const modeFlag = permissionModeToFlag(this.permissionMode);
24898
- return [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
26128
+ const args = [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
26129
+ if (typeof this.permissionPolicy === "string" && this.permissionPolicy.trim().length > 0) {
26130
+ args.push("--permission-policy", this.permissionPolicy);
26131
+ }
26132
+ return args;
24899
26133
  }
24900
26134
  }
24901
26135
  function isMissingAcpxSessionError(stderr, stdout2) {
@@ -25418,6 +26652,14 @@ async function buildApp(paths, deps = {}) {
25418
26652
  resultText: ""
25419
26653
  });
25420
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
+ }
25421
26663
  if (taskRecord && shouldNotifyTaskCompletion(taskRecord)) {
25422
26664
  try {
25423
26665
  await sendCompletionNotice(taskRecord);
@@ -25485,6 +26727,13 @@ async function buildApp(paths, deps = {}) {
25485
26727
  throw new Error(result.message || "worker task cancel was not acknowledged");
25486
26728
  }
25487
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
+ },
25488
26737
  resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, cwd, answer }) => {
25489
26738
  launchWorkerTurn({
25490
26739
  taskId,
@@ -25510,7 +26759,7 @@ async function buildApp(paths, deps = {}) {
25510
26759
  }
25511
26760
  },
25512
26761
  findReusableWorkerSession: async ({ coordinatorSession, workspace, cwd, targetAgent, role }) => {
25513
- 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);
25514
26763
  return binding?.[0] ?? null;
25515
26764
  },
25516
26765
  logger: logger2
@@ -25876,7 +27125,11 @@ import { fileURLToPath as fileURLToPath5 } from "node:url";
25876
27125
  import { homedir as homedir10 } from "node:os";
25877
27126
  async function checkDaemon(options = {}) {
25878
27127
  const home = options.home ?? process.env.HOME ?? homedir10();
25879
- const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
27128
+ const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
27129
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
27130
+ home,
27131
+ ...runtimeDir ? { runtimeDir } : {}
27132
+ });
25880
27133
  const controller = createDaemonController(paths, {
25881
27134
  processExecPath: options.processExecPath ?? process.execPath,
25882
27135
  cliEntryPath: options.cliEntryPath ?? resolveCliEntryPath(),
@@ -26028,7 +27281,11 @@ import { dirname as dirname14 } from "node:path";
26028
27281
  import { homedir as homedir11 } from "node:os";
26029
27282
  async function checkRuntime(options = {}) {
26030
27283
  const home = options.home ?? process.env.HOME ?? homedir11();
26031
- const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
27284
+ const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
27285
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
27286
+ home,
27287
+ ...runtimeDir ? { runtimeDir } : {}
27288
+ });
26032
27289
  const probe = options.probe ?? createRuntimeFsProbe();
26033
27290
  const platform = options.platform ?? process.platform;
26034
27291
  const checks3 = [
@@ -26062,107 +27319,107 @@ async function checkRuntime(options = {}) {
26062
27319
  }
26063
27320
  function createRuntimeFsProbe() {
26064
27321
  return {
26065
- stat: async (path14) => await stat3(path14),
26066
- access: async (path14, mode) => await access4(path14, mode)
27322
+ stat: async (path15) => await stat3(path15),
27323
+ access: async (path15, mode) => await access4(path15, mode)
26067
27324
  };
26068
27325
  }
26069
- async function checkDirectoryCreatable(label, path14, probe, platform) {
27326
+ async function checkDirectoryCreatable(label, path15, probe, platform) {
26070
27327
  try {
26071
- const stats = await probe.stat(path14);
27328
+ const stats = await probe.stat(path15);
26072
27329
  if (!stats.isDirectory()) {
26073
27330
  return {
26074
27331
  ok: false,
26075
- detail: `${label}: ${path14} (exists but is not a directory)`
27332
+ detail: `${label}: ${path15} (exists but is not a directory)`
26076
27333
  };
26077
27334
  }
26078
- await probe.access(path14, directoryAccessMode(platform));
27335
+ await probe.access(path15, directoryAccessMode(platform));
26079
27336
  return {
26080
27337
  ok: true,
26081
- detail: `${label}: ${path14} (writable)`
27338
+ detail: `${label}: ${path15} (writable)`
26082
27339
  };
26083
27340
  } catch (error2) {
26084
27341
  if (!isMissingPathError(error2)) {
26085
27342
  return {
26086
27343
  ok: false,
26087
- detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
27344
+ detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
26088
27345
  };
26089
27346
  }
26090
- const parentCheck = await checkCreatableAncestorDirectory(path14, probe, platform);
27347
+ const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
26091
27348
  if (!parentCheck.ok) {
26092
27349
  return {
26093
27350
  ok: false,
26094
- detail: `${label}: ${path14} (parent not writable: ${parentCheck.blockingPath})`
27351
+ detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
26095
27352
  };
26096
27353
  }
26097
27354
  return {
26098
27355
  ok: true,
26099
- detail: `${label}: ${path14} (creatable via ${parentCheck.creatableFrom})`
27356
+ detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
26100
27357
  };
26101
27358
  }
26102
27359
  }
26103
- async function checkFileCreatable(label, path14, probe, platform) {
27360
+ async function checkFileCreatable(label, path15, probe, platform) {
26104
27361
  try {
26105
- const stats = await probe.stat(path14);
27362
+ const stats = await probe.stat(path15);
26106
27363
  if (stats.isDirectory()) {
26107
27364
  return {
26108
27365
  ok: false,
26109
- detail: `${label}: ${path14} (exists but is a directory)`
27366
+ detail: `${label}: ${path15} (exists but is a directory)`
26110
27367
  };
26111
27368
  }
26112
- await probe.access(path14, constants.W_OK);
27369
+ await probe.access(path15, constants.W_OK);
26113
27370
  return {
26114
27371
  ok: true,
26115
- detail: `${label}: ${path14} (writable)`
27372
+ detail: `${label}: ${path15} (writable)`
26116
27373
  };
26117
27374
  } catch (error2) {
26118
27375
  if (!isMissingPathError(error2)) {
26119
27376
  return {
26120
27377
  ok: false,
26121
- detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
27378
+ detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
26122
27379
  };
26123
27380
  }
26124
- const parentCheck = await checkCreatableAncestorDirectory(dirname14(path14), probe, platform);
27381
+ const parentCheck = await checkCreatableAncestorDirectory(dirname14(path15), probe, platform);
26125
27382
  if (!parentCheck.ok) {
26126
27383
  return {
26127
27384
  ok: false,
26128
- detail: `${label}: ${path14} (parent not writable: ${parentCheck.blockingPath})`
27385
+ detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
26129
27386
  };
26130
27387
  }
26131
27388
  return {
26132
27389
  ok: true,
26133
- detail: `${label}: ${path14} (creatable via ${parentCheck.creatableFrom})`
27390
+ detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
26134
27391
  };
26135
27392
  }
26136
27393
  }
26137
- async function checkCreatableAncestorDirectory(path14, probe, platform) {
27394
+ async function checkCreatableAncestorDirectory(path15, probe, platform) {
26138
27395
  try {
26139
- const stats = await probe.stat(path14);
27396
+ const stats = await probe.stat(path15);
26140
27397
  if (!stats.isDirectory()) {
26141
27398
  return {
26142
27399
  ok: false,
26143
- creatableFrom: path14,
26144
- blockingPath: path14
27400
+ creatableFrom: path15,
27401
+ blockingPath: path15
26145
27402
  };
26146
27403
  }
26147
- await probe.access(path14, directoryAccessMode(platform));
27404
+ await probe.access(path15, directoryAccessMode(platform));
26148
27405
  return {
26149
27406
  ok: true,
26150
- creatableFrom: path14
27407
+ creatableFrom: path15
26151
27408
  };
26152
27409
  } catch (error2) {
26153
27410
  if (!isMissingPathError(error2)) {
26154
27411
  return {
26155
27412
  ok: false,
26156
- creatableFrom: path14,
26157
- blockingPath: path14
27413
+ creatableFrom: path15,
27414
+ blockingPath: path15
26158
27415
  };
26159
27416
  }
26160
- const parent = dirname14(path14);
26161
- if (parent === path14) {
27417
+ const parent = dirname14(path15);
27418
+ if (parent === path15) {
26162
27419
  return {
26163
27420
  ok: false,
26164
- creatableFrom: path14,
26165
- blockingPath: path14
27421
+ creatableFrom: path15,
27422
+ blockingPath: path15
26166
27423
  };
26167
27424
  }
26168
27425
  const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
@@ -26591,10 +27848,12 @@ async function runDoctor(options = {}, deps = {}) {
26591
27848
  resolveRuntimePaths: () => runtimePaths
26592
27849
  }));
26593
27850
  checks3.push(await (deps.checkRuntime ?? checkRuntime)({
26594
- home
27851
+ home,
27852
+ configPath: runtimePaths.configPath
26595
27853
  }));
26596
27854
  checks3.push(await (deps.checkDaemon ?? checkDaemon)({
26597
- home
27855
+ home,
27856
+ configPath: runtimePaths.configPath
26598
27857
  }));
26599
27858
  checks3.push(await (deps.checkWechat ?? checkWechat)({
26600
27859
  verbose: options.verbose
@@ -39206,7 +40465,8 @@ function buildWeacpxMcpToolRegistry(input) {
39206
40465
  task: exports_external.string().min(1),
39207
40466
  workingDirectory: exports_external.string().min(1).optional(),
39208
40467
  role: exports_external.string().min(1).optional(),
39209
- 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()
39210
40470
  }).strict(),
39211
40471
  handler: async (args) => await asToolResult(async () => {
39212
40472
  const input2 = args;
@@ -39227,7 +40487,8 @@ function buildWeacpxMcpToolRegistry(input) {
39227
40487
  targetAgent: exports_external.string().min(1),
39228
40488
  task: exports_external.string().min(1),
39229
40489
  workingDirectory: exports_external.string().min(1).optional(),
39230
- 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()
39231
40492
  }).strict()).min(1)
39232
40493
  }).strict(),
39233
40494
  handler: async (args) => await asToolResult(async () => {
@@ -39246,7 +40507,8 @@ function buildWeacpxMcpToolRegistry(input) {
39246
40507
  task: entry.task,
39247
40508
  ...entry.workingDirectory ? { workingDirectory: entry.workingDirectory } : {},
39248
40509
  ...entry.role ? { role: entry.role } : {},
39249
- ...groupId ? { groupId } : {}
40510
+ ...groupId ? { groupId } : {},
40511
+ ...entry.parallel !== undefined ? { parallel: entry.parallel } : {}
39250
40512
  });
39251
40513
  results.push({ index, taskId: result.taskId, status: result.status });
39252
40514
  } catch (error2) {
@@ -39470,7 +40732,7 @@ function createErrorResult(message) {
39470
40732
  };
39471
40733
  }
39472
40734
  function renderDelegateSuccess(result) {
39473
- 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.`;
39474
40736
  return [`Delegation task "${result.taskId}" created.`, `- Status: ${result.status}`, next].join(`
39475
40737
  `);
39476
40738
  }
@@ -39741,7 +41003,8 @@ function createOrchestrationTransport(endpoint, deps = {}) {
39741
41003
  task: input.task,
39742
41004
  ...input.workingDirectory !== undefined ? { cwd: input.workingDirectory } : {},
39743
41005
  ...input.role !== undefined ? { role: input.role } : {},
39744
- ...input.groupId !== undefined ? { groupId: input.groupId } : {}
41006
+ ...input.groupId !== undefined ? { groupId: input.groupId } : {},
41007
+ ...input.parallel !== undefined ? { parallel: input.parallel } : {}
39745
41008
  }),
39746
41009
  createGroup: async (input) => await client.createGroup(input),
39747
41010
  getTask: async (input) => await client.getTaskForCoordinator(input),
@@ -40454,10 +41717,12 @@ function parseSourceHandle(args, env = process.env) {
40454
41717
 
40455
41718
  // src/cli.ts
40456
41719
  init_workspace_path();
41720
+ init_workspace_name();
40457
41721
  init_state_store();
40458
41722
 
40459
41723
  // src/onboarding.ts
40460
41724
  init_workspace_path();
41725
+ init_workspace_name();
40461
41726
  init_agent_templates();
40462
41727
  function isFirstUse(config2, state) {
40463
41728
  return Object.keys(state.sessions ?? {}).length === 0 && Object.keys(config2.workspaces ?? {}).length === 0 && (config2.plugins ?? []).length === 0;
@@ -40468,7 +41733,7 @@ async function maybeRunFirstUseOnboarding(input) {
40468
41733
  if (!input.deps.isInteractive())
40469
41734
  return { created: false };
40470
41735
  const cwd = normalizeWorkspacePath(input.deps.cwd());
40471
- const workspaceName = allocateName(sanitizeName(basenameForWorkspacePath(cwd), "workspace"), input.config.workspaces);
41736
+ const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), input.config.workspaces);
40472
41737
  const yes = (await input.deps.promptText(`检测到首次使用 weacpx。是否将当前目录创建为工作区「${workspaceName}」?[Y/n] `)).trim().toLowerCase();
40473
41738
  if (yes === "n" || yes === "no")
40474
41739
  return { created: false };
@@ -40509,18 +41774,6 @@ function resolveTemplateChoice(answer, names) {
40509
41774
  return names[index - 1];
40510
41775
  return names.includes(answer) ? answer : null;
40511
41776
  }
40512
- function allocateName(base, existing) {
40513
- if (!existing[base])
40514
- return base;
40515
- let suffix = 2;
40516
- while (existing[`${base}-${suffix}`])
40517
- suffix += 1;
40518
- return `${base}-${suffix}`;
40519
- }
40520
- function sanitizeName(input, fallback) {
40521
- const sanitized = input.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
40522
- return sanitized || fallback;
40523
- }
40524
41777
 
40525
41778
  // src/cli-update.ts
40526
41779
  init_plugin_home();
@@ -42184,7 +43437,7 @@ var HELP_LINES = [
42184
43437
  "weacpx doctor - 运行诊断",
42185
43438
  "weacpx version - 查看版本",
42186
43439
  "weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
42187
- "weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
43440
+ "weacpx workspace list|add [name] [--raw]|rm <name> - 管理本机工作区(别名:ws)",
42188
43441
  "weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
42189
43442
  ];
42190
43443
  function getUsageText() {
@@ -42311,7 +43564,7 @@ async function runCli(args, deps = {}) {
42311
43564
  case "mcp-stdio":
42312
43565
  return await (deps.mcpStdio ?? ((subArgs) => defaultMcpStdio(subArgs, { stderr: deps.stderr })))(args.slice(1));
42313
43566
  case "start": {
42314
- const controller = deps.controller ?? createDefaultController();
43567
+ const controller = deps.controller ?? createDefaultController(deps);
42315
43568
  try {
42316
43569
  const isInteractive = deps.isInteractive ?? defaultIsInteractive;
42317
43570
  const status = await controller.getStatus();
@@ -42354,7 +43607,7 @@ async function runCli(args, deps = {}) {
42354
43607
  }
42355
43608
  }
42356
43609
  case "status": {
42357
- const controller = deps.controller ?? createDefaultController();
43610
+ const controller = deps.controller ?? createDefaultController(deps);
42358
43611
  const status = await controller.getStatus();
42359
43612
  if (status.state === "indeterminate") {
42360
43613
  print("weacpx 进程仍在运行,但状态元数据缺失");
@@ -42377,7 +43630,7 @@ async function runCli(args, deps = {}) {
42377
43630
  return 0;
42378
43631
  }
42379
43632
  case "stop": {
42380
- const controller = deps.controller ?? createDefaultController();
43633
+ const controller = deps.controller ?? createDefaultController(deps);
42381
43634
  const result = await controller.stop();
42382
43635
  if (result.detail === "not-running") {
42383
43636
  print("weacpx 未运行");
@@ -42387,7 +43640,7 @@ async function runCli(args, deps = {}) {
42387
43640
  return 0;
42388
43641
  }
42389
43642
  case "restart": {
42390
- const controller = deps.controller ?? createDefaultController();
43643
+ const controller = deps.controller ?? createDefaultController(deps);
42391
43644
  try {
42392
43645
  return await restartDaemonCli(controller, print);
42393
43646
  } catch (error2) {
@@ -42448,10 +43701,23 @@ async function handleWorkspaceCli(args, deps) {
42448
43701
  if (args.length !== 1)
42449
43702
  return null;
42450
43703
  return await workspaceList(deps.print);
42451
- case "add":
42452
- if (args.length > 2)
42453
- return null;
42454
- 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
+ }
42455
43721
  case "rm":
42456
43722
  if (args.length !== 2 || !args[1])
42457
43723
  return null;
@@ -42476,13 +43742,20 @@ async function workspaceList(print) {
42476
43742
  }
42477
43743
  async function workspaceAdd(rawName, deps) {
42478
43744
  const cwd = normalizeWorkspacePath(deps.cwd());
42479
- const name = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
42480
- if (name.trim().length === 0) {
43745
+ const input = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
43746
+ if (input.length === 0) {
42481
43747
  deps.print("工作区名称不能为空。");
42482
43748
  return 1;
42483
43749
  }
42484
43750
  const store = await createCliConfigStore();
42485
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
+ }
42486
43759
  const existing = config2.workspaces[name];
42487
43760
  if (existing) {
42488
43761
  if (sameWorkspacePath(existing.cwd, cwd)) {
@@ -42490,7 +43763,7 @@ async function workspaceAdd(rawName, deps) {
42490
43763
  return 0;
42491
43764
  }
42492
43765
  deps.print(`工作区「${name}」已存在,但路径不同:${existing.cwd}`);
42493
- deps.print(`请换一个名称,或先执行:weacpx workspace rm ${name}`);
43766
+ deps.print(`请换一个名称,或先执行:weacpx workspace rm ${quoteWorkspaceNameIfNeeded(name)}`);
42494
43767
  return 1;
42495
43768
  }
42496
43769
  await store.upsertWorkspace(name, cwd);
@@ -42600,8 +43873,18 @@ async function agentRemove(rawName, print) {
42600
43873
  print(`Agent「${name}」已删除`);
42601
43874
  return 0;
42602
43875
  }
43876
+ function resolveConfigPathForCurrentEnv() {
43877
+ return process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
43878
+ }
43879
+ function resolveDaemonPathsForCurrentConfig() {
43880
+ const configPath = resolveConfigPathForCurrentEnv();
43881
+ return resolveDaemonPaths({
43882
+ home: requireHome2(),
43883
+ runtimeDir: resolveRuntimeDirFromConfigPath(configPath)
43884
+ });
43885
+ }
42603
43886
  async function createCliConfigStore() {
42604
- const configPath = process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
43887
+ const configPath = resolveConfigPathForCurrentEnv();
42605
43888
  await ensureConfigExists(configPath);
42606
43889
  return new ConfigStore(configPath);
42607
43890
  }
@@ -42636,7 +43919,7 @@ async function defaultRun(options = {}) {
42636
43919
  await loadConfiguredPlugins2({ plugins: config2.plugins });
42637
43920
  const { createMessageChannels: createMessageChannels2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
42638
43921
  const { MessageChannelRegistry: MessageChannelRegistry2 } = await Promise.resolve().then(() => (init_channel_registry(), exports_channel_registry));
42639
- const daemonPaths = resolveDaemonPaths({ home: requireHome2() });
43922
+ const daemonPaths = resolveDaemonPathsForCurrentConfig();
42640
43923
  const daemonRuntime = new DaemonRuntime(daemonPaths, { pid: process.pid });
42641
43924
  const { channelDeps } = await prepareChannelMedia2(runtimePaths.configPath, config2);
42642
43925
  const channelRegistry = new MessageChannelRegistry2(createMessageChannels2(config2.channels, channelDeps));
@@ -42882,13 +44165,14 @@ async function defaultPromptSecret(message) {
42882
44165
  process.stdin.on("data", onData);
42883
44166
  });
42884
44167
  }
42885
- function createDefaultController() {
42886
- const daemonPaths = resolveDaemonPaths({ home: requireHome2() });
44168
+ function createDefaultController(deps = {}) {
44169
+ const daemonPaths = resolveDaemonPathsForCurrentConfig();
42887
44170
  const controller = createDaemonController(daemonPaths, {
42888
44171
  processExecPath: process.execPath,
42889
44172
  cliEntryPath: resolveCliEntryPath2(),
42890
44173
  cwd: process.cwd(),
42891
- env: process.env
44174
+ env: process.env,
44175
+ ...deps.isProcessRunning ? { isProcessRunning: deps.isProcessRunning } : {}
42892
44176
  });
42893
44177
  return {
42894
44178
  getStatus: () => controller.getStatus(),
@@ -42944,8 +44228,8 @@ function printDaemonLogHints(print) {
42944
44228
  }
42945
44229
  function safeDaemonLogPaths() {
42946
44230
  try {
42947
- const configPath = process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
42948
- const paths = resolveDaemonPaths({ home: requireHome2() });
44231
+ const configPath = resolveConfigPathForCurrentEnv();
44232
+ const paths = resolveDaemonPathsForCurrentConfig();
42949
44233
  return {
42950
44234
  appLog: join16(dirname15(configPath), "runtime", "app.log"),
42951
44235
  stderrLog: paths.stderrLog