weacpx 0.4.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1999,6 +1999,7 @@ var require_lib = __commonJS((exports, module) => {
1999
1999
 
2000
2000
  // src/util/private-file.ts
2001
2001
  import { chmod, mkdir, writeFile } from "node:fs/promises";
2002
+ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
2002
2003
  import { dirname } from "node:path";
2003
2004
  async function writePrivateFileAtomic(path, content) {
2004
2005
  await mkdir(dirname(path), { recursive: true });
@@ -2031,6 +2032,25 @@ async function writePrivateFileAtomic(path, content) {
2031
2032
  await release();
2032
2033
  }
2033
2034
  }
2035
+ function writePrivateFileSync(path, content, deps = {}) {
2036
+ mkdirSync(dirname(path), { recursive: true });
2037
+ const platform = deps.platform ?? process.platform;
2038
+ const atomicWrite = deps.atomicWrite ?? ((p, c) => import_write_file_atomic.default.sync(p, c, { mode: PRIVATE_FILE_MODE, encoding: "utf8", fsync: true }));
2039
+ try {
2040
+ atomicWrite(path, content);
2041
+ } catch (error) {
2042
+ if (!isTransientWriteError(error, platform)) {
2043
+ throw error;
2044
+ }
2045
+ const directWrite = deps.directWrite ?? ((p, c) => {
2046
+ writeFileSync(p, c, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
2047
+ try {
2048
+ chmodSync(p, PRIVATE_FILE_MODE);
2049
+ } catch {}
2050
+ });
2051
+ directWrite(path, content);
2052
+ }
2053
+ }
2034
2054
  async function retryTransientWriteErrors(run, options = {}) {
2035
2055
  const platform = options.platform ?? process.platform;
2036
2056
  const maxAttempts = options.maxAttempts ?? WRITE_RETRY_MAX_ATTEMPTS;
@@ -2206,6 +2226,9 @@ function parseConfig(raw, options = {}) {
2206
2226
  throw new Error("transport.permissionPolicy must be a non-empty string");
2207
2227
  }
2208
2228
  }
2229
+ if ("queueOwnerTtlSeconds" in transport && (typeof transport.queueOwnerTtlSeconds !== "number" || !Number.isFinite(transport.queueOwnerTtlSeconds) || transport.queueOwnerTtlSeconds < 0)) {
2230
+ throw new Error("transport.queueOwnerTtlSeconds must be a non-negative number (0 = keep alive forever)");
2231
+ }
2209
2232
  if (!isRecord(raw.agents)) {
2210
2233
  throw new Error("agents must be an object");
2211
2234
  }
@@ -2302,7 +2325,8 @@ function parseConfig(raw, options = {}) {
2302
2325
  ...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
2303
2326
  type: transportType,
2304
2327
  permissionMode,
2305
- nonInteractivePermissions
2328
+ nonInteractivePermissions,
2329
+ queueOwnerTtlSeconds: typeof transport.queueOwnerTtlSeconds === "number" ? transport.queueOwnerTtlSeconds : DEFAULT_QUEUE_OWNER_TTL_SECONDS
2306
2330
  },
2307
2331
  logging: {
2308
2332
  level: resolvedLoggingLevel,
@@ -2428,7 +2452,7 @@ function parseOrchestrationConfig(raw) {
2428
2452
  maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
2429
2453
  };
2430
2454
  }
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;
2455
+ var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_QUEUE_OWNER_TTL_SECONDS = 1800, DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2432
2456
  var init_load_config = __esm(() => {
2433
2457
  init_workspace_path();
2434
2458
  DEFAULT_PERF_LOG_CONFIG = {
@@ -2729,7 +2753,7 @@ class DaemonStatusStore {
2729
2753
  var init_daemon_status = () => {};
2730
2754
 
2731
2755
  // src/daemon/daemon-controller.ts
2732
- import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
2756
+ import { mkdir as mkdir3, open, readFile as readFile4, rm as rm2 } from "node:fs/promises";
2733
2757
  import { dirname as dirname3 } from "node:path";
2734
2758
 
2735
2759
  class DaemonController {
@@ -2788,9 +2812,19 @@ class DaemonController {
2788
2812
  if (current.state === "indeterminate") {
2789
2813
  throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
2790
2814
  }
2791
- await this.statusStore.clear();
2792
- const pid = await this.deps.spawnDetached(options);
2793
- await this.writePid(pid);
2815
+ const pidHandle = await this.openPidFileExclusive();
2816
+ let pid;
2817
+ try {
2818
+ await this.statusStore.clear();
2819
+ pid = await this.deps.spawnDetached(options);
2820
+ await pidHandle.write(`${pid}
2821
+ `);
2822
+ } catch (error) {
2823
+ await pidHandle.close().catch(() => {});
2824
+ await rm2(this.paths.pidFile, { force: true }).catch(() => {});
2825
+ throw error;
2826
+ }
2827
+ await pidHandle.close();
2794
2828
  await this.waitForStartupMetadata(pid, options.firstRunOnboarding ? this.onboardingStartupTimeoutMs : this.startupTimeoutMs, options.startupWait);
2795
2829
  return { state: "started", pid };
2796
2830
  }
@@ -2818,10 +2852,16 @@ class DaemonController {
2818
2852
  throw error;
2819
2853
  }
2820
2854
  }
2821
- async writePid(pid) {
2855
+ async openPidFileExclusive() {
2822
2856
  await mkdir3(dirname3(this.paths.pidFile), { recursive: true });
2823
- await writeFile3(this.paths.pidFile, `${pid}
2824
- `);
2857
+ try {
2858
+ return await open(this.paths.pidFile, "wx", 384);
2859
+ } catch (error) {
2860
+ if (error.code === "EEXIST") {
2861
+ throw new Error(`weacpx daemon pid file already exists (${this.paths.pidFile}); another start may be in progress`);
2862
+ }
2863
+ throw error;
2864
+ }
2825
2865
  }
2826
2866
  async clearRuntimeFiles() {
2827
2867
  await rm2(this.paths.pidFile, { force: true });
@@ -2918,15 +2958,17 @@ async function defaultRunProcessCommand(command, args) {
2918
2958
  var init_terminate_process_tree = () => {};
2919
2959
 
2920
2960
  // src/daemon/create-daemon-controller.ts
2921
- import { mkdir as mkdir4, open } from "node:fs/promises";
2961
+ import { mkdir as mkdir4, open as open2 } from "node:fs/promises";
2922
2962
  import { spawn as spawn2 } from "node:child_process";
2923
2963
  function createDaemonController(paths, options) {
2924
2964
  return new DaemonController(paths, {
2925
2965
  isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning2,
2926
2966
  spawnDetached: async (spawnOptions) => {
2927
2967
  await mkdir4(paths.runtimeDir, { recursive: true });
2928
- const stdoutHandle = await open(paths.stdoutLog, "a");
2929
- const stderrHandle = await open(paths.stderrLog, "a");
2968
+ const stdoutHandle = await open2(paths.stdoutLog, "a", 384);
2969
+ const stderrHandle = await open2(paths.stderrLog, "a", 384);
2970
+ await stdoutHandle.chmod(384).catch(() => {});
2971
+ await stderrHandle.chmod(384).catch(() => {});
2930
2972
  try {
2931
2973
  return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd, spawnOptions));
2932
2974
  } finally {
@@ -9693,7 +9735,8 @@ function createEmptyState() {
9693
9735
  return {
9694
9736
  sessions: {},
9695
9737
  chat_contexts: {},
9696
- orchestration: createEmptyOrchestrationState()
9738
+ orchestration: createEmptyOrchestrationState(),
9739
+ scheduled_tasks: {}
9697
9740
  };
9698
9741
  }
9699
9742
  var init_types = () => {};
@@ -9939,6 +9982,29 @@ function parseChatContexts(raw, path3) {
9939
9982
  }
9940
9983
  return chatContexts;
9941
9984
  }
9985
+ function isScheduledTaskStatus(value) {
9986
+ return value === "pending" || value === "triggering" || value === "executed" || value === "cancelled" || value === "missed" || value === "failed";
9987
+ }
9988
+ function isScheduledTaskRecord(value) {
9989
+ if (!isRecord2(value))
9990
+ return false;
9991
+ return isString(value.id) && isString(value.chat_key) && isString(value.session_alias) && isString(value.execute_at) && isString(value.message) && isScheduledTaskStatus(value.status) && isString(value.created_at) && isOptionalString(value.account_id) && isOptionalString(value.reply_context_token) && isOptionalString(value.source_label) && isOptionalString(value.triggered_at) && isOptionalString(value.executed_at) && isOptionalString(value.cancelled_at) && isOptionalString(value.missed_at) && isOptionalString(value.failed_at) && isOptionalString(value.last_error);
9992
+ }
9993
+ function parseScheduledTasks(raw, path3) {
9994
+ if (raw === undefined)
9995
+ return {};
9996
+ if (!isRecord2(raw)) {
9997
+ throw new Error(`state file "${path3}" must contain an object field "scheduled_tasks"`);
9998
+ }
9999
+ const tasks = {};
10000
+ for (const [id, value] of Object.entries(raw)) {
10001
+ if (!isScheduledTaskRecord(value) || value.id !== id) {
10002
+ throw new Error(`state file "${path3}" contains malformed scheduled task record "${id}"`);
10003
+ }
10004
+ tasks[id] = value;
10005
+ }
10006
+ return tasks;
10007
+ }
9942
10008
  function parseState(raw, path3) {
9943
10009
  if (!isRecord2(raw)) {
9944
10010
  throw new Error(`state file "${path3}" must contain a JSON object`);
@@ -9957,7 +10023,8 @@ function parseState(raw, path3) {
9957
10023
  return {
9958
10024
  sessions: parsedSessions,
9959
10025
  chat_contexts: parseChatContexts(chatContexts, path3),
9960
- orchestration
10026
+ orchestration,
10027
+ scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
9961
10028
  };
9962
10029
  }
9963
10030
  function validateExternalCoordinatorIdentityCollisions(sessions, orchestration, path3) {
@@ -10014,7 +10081,7 @@ var init_state_store = __esm(() => {
10014
10081
  });
10015
10082
 
10016
10083
  // src/plugins/plugin-home.ts
10017
- import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
10084
+ import { mkdir as mkdir6, writeFile as writeFile4 } from "node:fs/promises";
10018
10085
  import { homedir as homedir3 } from "node:os";
10019
10086
  import { join as join3 } from "node:path";
10020
10087
  function coerceMissing(value) {
@@ -10040,7 +10107,7 @@ function resolvePluginHome(input = {}) {
10040
10107
  }
10041
10108
  async function ensurePluginHome(pluginHome) {
10042
10109
  await mkdir6(pluginHome, { recursive: true, mode: 448 });
10043
- await writeFile5(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
10110
+ await writeFile4(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
10044
10111
  `, { flag: "wx" }).catch((error2) => {
10045
10112
  if (error2.code !== "EEXIST")
10046
10113
  throw error2;
@@ -10179,8 +10246,6 @@ function loadWeixinAccount(accountId) {
10179
10246
  return null;
10180
10247
  }
10181
10248
  function saveWeixinAccount(accountId, update) {
10182
- const dir = resolveAccountsDir();
10183
- ensureDirSync(dir);
10184
10249
  const existing = loadWeixinAccount(accountId) ?? {};
10185
10250
  const token = update.token?.trim() || existing.token;
10186
10251
  const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
@@ -10190,11 +10255,7 @@ function saveWeixinAccount(accountId, update) {
10190
10255
  ...baseUrl ? { baseUrl } : {},
10191
10256
  ...userId ? { userId } : {}
10192
10257
  };
10193
- const filePath = resolveAccountPath(accountId);
10194
- fs3.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
10195
- try {
10196
- fs3.chmodSync(filePath, 384);
10197
- } catch {}
10258
+ writePrivateFileSync(resolveAccountPath(accountId), JSON.stringify(data, null, 2));
10198
10259
  }
10199
10260
  function clearWeixinAccount(accountId) {
10200
10261
  try {
@@ -10295,6 +10356,7 @@ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com", CDN_BASE_URL = "https://
10295
10356
  var init_accounts = __esm(() => {
10296
10357
  init_ensure_dir();
10297
10358
  init_state_dir();
10359
+ init_private_file();
10298
10360
  });
10299
10361
 
10300
10362
  // src/weixin/util/logger.ts
@@ -12298,8 +12360,7 @@ function persistContextTokens(accountId) {
12298
12360
  }
12299
12361
  const filePath = resolveContextTokenFilePath(accountId);
12300
12362
  try {
12301
- fs5.mkdirSync(path6.dirname(filePath), { recursive: true });
12302
- fs5.writeFileSync(filePath, JSON.stringify(tokens), "utf-8");
12363
+ writePrivateFileSync(filePath, JSON.stringify(tokens));
12303
12364
  } catch (err) {
12304
12365
  logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
12305
12366
  }
@@ -12422,6 +12483,7 @@ var init_inbound = __esm(() => {
12422
12483
  init_random();
12423
12484
  init_types2();
12424
12485
  init_state_dir();
12486
+ init_private_file();
12425
12487
  contextTokenStore = new Map;
12426
12488
  });
12427
12489
 
@@ -12544,7 +12606,7 @@ function createConversationExecutor() {
12544
12606
  }
12545
12607
 
12546
12608
  // src/channels/media-store.ts
12547
- import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile6 } from "node:fs/promises";
12609
+ import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile5 } from "node:fs/promises";
12548
12610
  import path7 from "node:path";
12549
12611
 
12550
12612
  class RuntimeMediaStore {
@@ -12568,7 +12630,7 @@ class RuntimeMediaStore {
12568
12630
  if (!isPathInside(resolvedFile, resolvedRoot)) {
12569
12631
  throw new Error("media path escapes runtime media root");
12570
12632
  }
12571
- await writeFile6(resolvedFile, input.buffer);
12633
+ await writeFile5(resolvedFile, input.buffer);
12572
12634
  return {
12573
12635
  kind: input.kind,
12574
12636
  filePath: resolvedFile,
@@ -14789,13 +14851,11 @@ function loadGetUpdatesBuf(filePath) {
14789
14851
  return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
14790
14852
  }
14791
14853
  function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
14792
- const dir = path13.dirname(filePath);
14793
- ensureDirSync(dir);
14794
- fs10.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
14854
+ writePrivateFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0));
14795
14855
  }
14796
14856
  var init_sync_buf = __esm(() => {
14797
14857
  init_accounts();
14798
- init_ensure_dir();
14858
+ init_private_file();
14799
14859
  init_state_dir();
14800
14860
  });
14801
14861
 
@@ -15407,8 +15467,179 @@ var init_deliver_coordinator_message = __esm(() => {
15407
15467
  init_send();
15408
15468
  });
15409
15469
 
15470
+ // src/weixin/messaging/scheduled-turn.ts
15471
+ async function executeScheduledTurn(input, deps) {
15472
+ const userId = normalizeWeixinUserIdFromChatKey(input.chatKey);
15473
+ const quotaKey = userId;
15474
+ const sendMessage2 = deps.sendMessage ?? sendMessageWeixin;
15475
+ const candidateAccountIds = input.accountId ? [input.accountId] : deps.listAccountIds();
15476
+ if (candidateAccountIds.length === 0) {
15477
+ throw new Error(`no weixin account is available for scheduled message on chatKey: ${input.chatKey}`);
15478
+ }
15479
+ let noticeSent = false;
15480
+ let lastNoticeError;
15481
+ let deliveryAccountId;
15482
+ let deliveryContextToken;
15483
+ let deliverableAccountId;
15484
+ let deliverableContextToken;
15485
+ const resolveContextToken = (candidateAccountId) => deps.getContextToken(candidateAccountId, userId) ?? (candidateAccountId === input.accountId ? input.replyContextToken : undefined);
15486
+ for (const candidateAccountId of candidateAccountIds) {
15487
+ const contextToken = resolveContextToken(candidateAccountId);
15488
+ if (!contextToken)
15489
+ continue;
15490
+ const account = deps.resolveAccount(candidateAccountId);
15491
+ if (!account.token)
15492
+ continue;
15493
+ if (!deliverableAccountId) {
15494
+ deliverableAccountId = candidateAccountId;
15495
+ deliverableContextToken = contextToken;
15496
+ }
15497
+ try {
15498
+ if (!deps.reserveMidSegment(quotaKey)) {
15499
+ throw new Error("mid segment quota exhausted");
15500
+ }
15501
+ await sendMessage2({
15502
+ to: userId,
15503
+ text: input.noticeText,
15504
+ opts: { baseUrl: account.baseUrl, token: account.token, contextToken }
15505
+ });
15506
+ noticeSent = true;
15507
+ deliveryAccountId = candidateAccountId;
15508
+ deliveryContextToken = contextToken;
15509
+ break;
15510
+ } catch (error2) {
15511
+ lastNoticeError = error2;
15512
+ await deps.logger.error("scheduled.notice_send_failed", "failed to send scheduled notice", { chatKey: input.chatKey, accountId: candidateAccountId, error: String(error2) });
15513
+ }
15514
+ }
15515
+ if (!noticeSent) {
15516
+ if (!deliverableAccountId || !deliverableContextToken) {
15517
+ const message = lastNoticeError instanceof Error ? lastNoticeError.message : `no deliverable weixin context for scheduled message on chatKey: ${input.chatKey}`;
15518
+ throw new Error(message);
15519
+ }
15520
+ deliveryAccountId = deliverableAccountId;
15521
+ deliveryContextToken = deliverableContextToken;
15522
+ await deps.logger.info("scheduled.notice_skipped", "scheduled trigger notice was not delivered; proceeding with agent turn", {
15523
+ chatKey: input.chatKey,
15524
+ accountId: deliveryAccountId,
15525
+ reason: lastNoticeError instanceof Error ? lastNoticeError.message : "notice_undelivered"
15526
+ });
15527
+ }
15528
+ const sendReplySegment = async (text) => {
15529
+ const plainText = markdownToPlainText(text).trim();
15530
+ if (plainText.length === 0)
15531
+ return false;
15532
+ return await sendTextViaAvailableAccount(plainText, "scheduled.mid_send_failed");
15533
+ };
15534
+ const sendReservedMidText = async (text) => {
15535
+ const plainText = markdownToPlainText(text).trim();
15536
+ if (plainText.length === 0)
15537
+ return false;
15538
+ if (!deps.reserveMidSegment(quotaKey)) {
15539
+ await deps.logger.info("scheduled.mid_dropped", "scheduled turn intermediate response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted" });
15540
+ return false;
15541
+ }
15542
+ return await sendTextViaAvailableAccount(plainText, "scheduled.mid_send_failed");
15543
+ };
15544
+ const resolvedAccountId = deliveryAccountId ?? input.accountId ?? candidateAccountIds[0];
15545
+ let turn;
15546
+ try {
15547
+ turn = await executeChatTurn({
15548
+ agent: deps.agent,
15549
+ request: {
15550
+ accountId: resolvedAccountId,
15551
+ conversationId: input.chatKey,
15552
+ text: input.promptText,
15553
+ ...deliveryContextToken ? { replyContextToken: deliveryContextToken } : {},
15554
+ ...input.abortSignal ? { abortSignal: input.abortSignal } : {},
15555
+ metadata: { channel: "weixin", scheduledSessionAlias: input.sessionAlias }
15556
+ },
15557
+ onReplySegment: sendReplySegment
15558
+ });
15559
+ } catch (error2) {
15560
+ await sendReservedMidText(`定时任务执行失败:${error2 instanceof Error ? error2.message : String(error2)}`).catch(() => false);
15561
+ throw error2;
15562
+ }
15563
+ if (turn.text) {
15564
+ const finalText = markdownToPlainText(turn.text).trim();
15565
+ if (finalText.length > 0) {
15566
+ await sendFinalText(finalText);
15567
+ }
15568
+ }
15569
+ async function sendFinalText(finalText) {
15570
+ const rawChunks = chunkFinalText(finalText, 1800);
15571
+ if (rawChunks.length === 0)
15572
+ return;
15573
+ const total = rawChunks.length;
15574
+ const chunks = total === 1 ? rawChunks : rawChunks.map((body, index) => `(${index + 1}/${total}) ${body}`);
15575
+ const available = total === 1 ? 1 : Math.max(Math.min(deps.finalRemaining?.(quotaKey) ?? total, total), 0);
15576
+ const wave = chunks.slice(0, available);
15577
+ if (wave.length > 0 && wave.length < total) {
15578
+ wave[wave.length - 1] = `${wave[wave.length - 1]}
15579
+
15580
+ ${buildFinalHeadsUp({
15581
+ total,
15582
+ sentSoFar: wave.length
15583
+ })}`;
15584
+ }
15585
+ let sent = 0;
15586
+ for (let index = 0;index < wave.length; index += 1) {
15587
+ if (!deps.reserveFinal(quotaKey)) {
15588
+ await deps.logger.info("scheduled.final_dropped", "scheduled turn final response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted", chunk: index + 1, total });
15589
+ break;
15590
+ }
15591
+ const delivered = await sendTextViaAvailableAccount(wave[index], "scheduled.final_send_failed");
15592
+ if (!delivered)
15593
+ break;
15594
+ sent += 1;
15595
+ }
15596
+ const restToPark = chunks.slice(sent);
15597
+ if (total > 1 && restToPark.length > 0 && deps.enqueuePendingFinal) {
15598
+ const pending = restToPark.map((text, index) => {
15599
+ const entry = { text, seq: sent + index + 1, total };
15600
+ if (deliveryContextToken)
15601
+ entry.contextToken = deliveryContextToken;
15602
+ if (deliveryAccountId)
15603
+ entry.accountId = deliveryAccountId;
15604
+ return entry;
15605
+ });
15606
+ deps.enqueuePendingFinal(quotaKey, pending);
15607
+ }
15608
+ }
15609
+ async function sendTextViaAvailableAccount(text, errorEvent) {
15610
+ const orderedAccountIds = [
15611
+ ...deliveryAccountId ? [deliveryAccountId] : [],
15612
+ ...candidateAccountIds.filter((accountId) => accountId !== deliveryAccountId)
15613
+ ];
15614
+ for (const candidateAccountId of orderedAccountIds) {
15615
+ const contextToken = candidateAccountId === deliveryAccountId && deliveryContextToken ? deliveryContextToken : resolveContextToken(candidateAccountId);
15616
+ if (!contextToken)
15617
+ continue;
15618
+ const account = deps.resolveAccount(candidateAccountId);
15619
+ if (!account.token)
15620
+ continue;
15621
+ try {
15622
+ await sendMessage2({
15623
+ to: userId,
15624
+ text,
15625
+ opts: { baseUrl: account.baseUrl, token: account.token, contextToken }
15626
+ });
15627
+ return true;
15628
+ } catch (error2) {
15629
+ await deps.logger.error(errorEvent, "failed to send scheduled response text", { chatKey: input.chatKey, accountId: candidateAccountId, error: String(error2) });
15630
+ }
15631
+ }
15632
+ return false;
15633
+ }
15634
+ }
15635
+ var init_scheduled_turn = __esm(() => {
15636
+ init_handle_weixin_message_turn();
15637
+ init_send();
15638
+ init_inbound();
15639
+ });
15640
+
15410
15641
  // src/weixin/monitor/consumer-lock.ts
15411
- import { mkdir as mkdir8, open as open2, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15642
+ import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15412
15643
  import { dirname as dirname8, join as join5 } from "node:path";
15413
15644
  import { homedir as homedir4 } from "node:os";
15414
15645
  function createWeixinConsumerLock(options = {}) {
@@ -15420,7 +15651,7 @@ function createWeixinConsumerLock(options = {}) {
15420
15651
  await mkdir8(dirname8(lockFilePath), { recursive: true });
15421
15652
  while (true) {
15422
15653
  try {
15423
- const handle = await open2(lockFilePath, "wx");
15654
+ const handle = await open3(lockFilePath, "wx");
15424
15655
  try {
15425
15656
  await handle.writeFile(`${JSON.stringify(meta2, null, 2)}
15426
15657
  `, "utf8");
@@ -15533,6 +15764,7 @@ var init_consumer_lock = __esm(() => {
15533
15764
  // src/channels/weixin-channel.ts
15534
15765
  class WeixinChannel {
15535
15766
  id = "weixin";
15767
+ agent = null;
15536
15768
  quota = null;
15537
15769
  logger = null;
15538
15770
  markDelivered = null;
@@ -15563,6 +15795,7 @@ class WeixinChannel {
15563
15795
  this.markFailed = callbacks.markTaskNoticeFailed;
15564
15796
  }
15565
15797
  async start(input) {
15798
+ this.agent = input.agent;
15566
15799
  this.quota = input.quota;
15567
15800
  this.logger = input.logger;
15568
15801
  if (!this.isLoggedIn()) {
@@ -15623,6 +15856,23 @@ class WeixinChannel {
15623
15856
  logger: this.logger
15624
15857
  });
15625
15858
  }
15859
+ async sendScheduledMessage(input) {
15860
+ if (!this.agent || !this.quota || !this.logger) {
15861
+ throw new Error("WeixinChannel.start() must be called before scheduled message delivery");
15862
+ }
15863
+ await executeScheduledTurn(input, {
15864
+ agent: this.agent,
15865
+ listAccountIds: () => listWeixinAccountIds(),
15866
+ resolveAccount: (accountId) => resolveWeixinAccount(accountId),
15867
+ getContextToken: (accountId, userId) => getContextToken(accountId, userId),
15868
+ reserveMidSegment: (chatKey) => this.quota.reserveMidSegment(chatKey),
15869
+ reserveFinal: (chatKey) => this.quota.reserveFinal(chatKey),
15870
+ finalRemaining: (chatKey) => this.quota.finalRemaining(chatKey),
15871
+ enqueuePendingFinal: (chatKey, chunks) => this.quota.enqueuePendingFinal(chatKey, chunks),
15872
+ sendMessage: sendMessageWeixin,
15873
+ logger: this.logger
15874
+ });
15875
+ }
15626
15876
  }
15627
15877
  var init_weixin_channel = __esm(() => {
15628
15878
  init_weixin();
@@ -15630,6 +15880,7 @@ var init_weixin_channel = __esm(() => {
15630
15880
  init_deliver_orchestration_task_notice();
15631
15881
  init_deliver_orchestration_task_progress();
15632
15882
  init_deliver_coordinator_message();
15883
+ init_scheduled_turn();
15633
15884
  init_consumer_lock();
15634
15885
  });
15635
15886
 
@@ -16014,7 +16265,7 @@ function validatePluginCompatibility(metadata, context) {
16014
16265
  }
16015
16266
  }
16016
16267
  }
16017
- var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.4.0", SEMVER_RE;
16268
+ var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.5.0", SEMVER_RE;
16018
16269
  var init_compatibility = __esm(() => {
16019
16270
  WEACPX_PLUGIN_API_SUPPORTED_VERSIONS = [1];
16020
16271
  SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
@@ -16105,21 +16356,27 @@ async function loadConfiguredPlugins(input) {
16105
16356
  const importPlugin = input.importPlugin ?? importPluginFromHome;
16106
16357
  const loaded = [];
16107
16358
  for (const config2 of enabled) {
16108
- let moduleValue;
16109
16359
  try {
16110
- moduleValue = await importPlugin(config2.name, pluginHome);
16360
+ let moduleValue;
16361
+ try {
16362
+ moduleValue = await importPlugin(config2.name, pluginHome);
16363
+ } catch (error2) {
16364
+ const message = error2 instanceof Error ? error2.message : String(error2);
16365
+ throw new Error(`failed to load plugin ${config2.name}: ${message}`);
16366
+ }
16367
+ const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
16368
+ ...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
16369
+ });
16370
+ const channels = plugin.channels ?? [];
16371
+ for (const channel of channels) {
16372
+ registerChannelPlugin(channel);
16373
+ }
16374
+ loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
16111
16375
  } catch (error2) {
16112
- const message = error2 instanceof Error ? error2.message : String(error2);
16113
- throw new Error(`failed to load plugin ${config2.name}: ${message}`);
16114
- }
16115
- const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
16116
- ...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
16117
- });
16118
- const channels = plugin.channels ?? [];
16119
- for (const channel of channels) {
16120
- registerChannelPlugin(channel);
16376
+ if (!input.onPluginError)
16377
+ throw error2;
16378
+ input.onPluginError({ name: config2.name, error: error2 });
16121
16379
  }
16122
- loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
16123
16380
  }
16124
16381
  return loaded;
16125
16382
  }
@@ -16140,7 +16397,7 @@ var init_bootstrap = __esm(() => {
16140
16397
  });
16141
16398
 
16142
16399
  // src/logging/app-logger.ts
16143
- import { appendFile, mkdir as mkdir9 } from "node:fs/promises";
16400
+ import { appendFile, chmod as chmod2, mkdir as mkdir9 } from "node:fs/promises";
16144
16401
  import { dirname as dirname10 } from "node:path";
16145
16402
  function createNoopAppLogger() {
16146
16403
  return {
@@ -16154,6 +16411,7 @@ function createNoopAppLogger() {
16154
16411
  function createAppLogger(options) {
16155
16412
  const now = options.now ?? (() => new Date);
16156
16413
  let writeChain = Promise.resolve();
16414
+ let modeEnsured = false;
16157
16415
  return {
16158
16416
  debug: async (event, message, context) => {
16159
16417
  await enqueueWrite("debug", event, message, context);
@@ -16182,8 +16440,12 @@ function createAppLogger(options) {
16182
16440
  }
16183
16441
  const line = formatLogLine(now(), level, event, message, context);
16184
16442
  await mkdir9(dirname10(options.filePath), { recursive: true });
16443
+ if (!modeEnsured) {
16444
+ modeEnsured = true;
16445
+ await chmod2(options.filePath, 384).catch(() => {});
16446
+ }
16185
16447
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
16186
- await appendFile(options.filePath, line, "utf8");
16448
+ await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
16187
16449
  }
16188
16450
  }
16189
16451
  function formatLogLine(time3, level, event, message, context) {
@@ -16389,7 +16651,9 @@ var init_command_list = __esm(() => {
16389
16651
  "/dg",
16390
16652
  "/group",
16391
16653
  "/groups",
16392
- "/task"
16654
+ "/task",
16655
+ "/later",
16656
+ "/lt"
16393
16657
  ];
16394
16658
  KNOWN_COMMAND_PREFIX_SET = new Set(WEACPX_KNOWN_COMMAND_PREFIXES);
16395
16659
  });
@@ -16557,6 +16821,16 @@ function parseCommand(input) {
16557
16821
  } else if (command === "/task" && parts[1] && parts.length === 2) {
16558
16822
  return { kind: "task.get", taskId: parts[1] };
16559
16823
  }
16824
+ if (command === "/later") {
16825
+ if (parts.length === 1)
16826
+ return { kind: "later.help" };
16827
+ if (parts[1] === "list" && parts.length === 2)
16828
+ return { kind: "later.list" };
16829
+ if (parts[1] === "cancel" && parts[2] && parts.length === 3) {
16830
+ return { kind: "later.cancel", id: parts[2] };
16831
+ }
16832
+ return { kind: "later.create", tokens: parts.slice(1) };
16833
+ }
16560
16834
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
16561
16835
  const name = parts[2];
16562
16836
  let cwd = "";
@@ -16725,6 +16999,8 @@ function normalizeCommand(command) {
16725
16999
  return "/permission";
16726
17000
  if (command === "/stop")
16727
17001
  return "/cancel";
17002
+ if (command === "/lt")
17003
+ return "/later";
16728
17004
  return command;
16729
17005
  }
16730
17006
  function isRecognizedCommand(command) {
@@ -16920,6 +17196,7 @@ var init_command_policy = __esm(() => {
16920
17196
  "group.get",
16921
17197
  "tasks",
16922
17198
  "task.get",
17199
+ "later.help",
16923
17200
  "invalid",
16924
17201
  "prompt"
16925
17202
  ]);
@@ -16949,7 +17226,10 @@ var init_command_policy = __esm(() => {
16949
17226
  "session.new": "/session new",
16950
17227
  "session.shortcut": "/session",
16951
17228
  "session.shortcut.new": "/session",
16952
- "session.attach": "/session attach"
17229
+ "session.attach": "/session attach",
17230
+ "later.create": "/later",
17231
+ "later.list": "/later list",
17232
+ "later.cancel": "/later cancel"
16953
17233
  };
16954
17234
  });
16955
17235
 
@@ -17903,11 +18183,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17903
18183
  throw error2;
17904
18184
  }
17905
18185
  }
17906
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
17907
- const session = await context.sessions.getCurrentSession(chatKey);
17908
- if (!session) {
17909
- return { text: NO_CURRENT_SESSION_TEXT };
17910
- }
18186
+ async function handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
17911
18187
  try {
17912
18188
  return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
17913
18189
  } catch (error2) {
@@ -17918,6 +18194,13 @@ async function handlePrompt(context, chatKey, text, reply, replyContextToken, ac
17918
18194
  return context.recovery.renderTransportError(session, error2);
17919
18195
  }
17920
18196
  }
18197
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
18198
+ const session = await context.sessions.getCurrentSession(chatKey);
18199
+ if (!session) {
18200
+ return { text: NO_CURRENT_SESSION_TEXT };
18201
+ }
18202
+ return await handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
18203
+ }
17921
18204
  async function preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId) {
17922
18205
  const orchestration = context.orchestration;
17923
18206
  if (!orchestration) {
@@ -18679,6 +18962,333 @@ var init_workspace_handler = __esm(() => {
18679
18962
  };
18680
18963
  });
18681
18964
 
18965
+ // src/scheduled/scheduled-types.ts
18966
+ var LATER_MIN_DELAY_MS = 1e4, LATER_MAX_DELAY_MS, LATER_MESSAGE_PREVIEW_CHARS = 120;
18967
+ var init_scheduled_types = __esm(() => {
18968
+ LATER_MAX_DELAY_MS = 7 * 24 * 60 * 60 * 1000;
18969
+ });
18970
+
18971
+ // src/scheduled/parse-later-time.ts
18972
+ function parseLaterTime(tokens, now = new Date) {
18973
+ if (tokens.length === 0)
18974
+ return { ok: false, code: "missing_time" };
18975
+ const relative = parseRelative(tokens, now);
18976
+ if (relative)
18977
+ return validateResult(relative.executeAt, relative.messageStartIndex, tokens, now);
18978
+ const absolute = parseAbsolute(tokens, now);
18979
+ if (absolute)
18980
+ return validateResult(absolute.executeAt, absolute.messageStartIndex, tokens, now, absolute.pastTodayValue);
18981
+ return { ok: false, code: "unrecognized_time" };
18982
+ }
18983
+ function parseRelative(tokens, now) {
18984
+ if (tokens[0] === "in" && tokens[1]) {
18985
+ const ms = parseDuration(tokens[1]);
18986
+ if (ms !== null)
18987
+ return { executeAt: new Date(now.getTime() + ms), messageStartIndex: 2 };
18988
+ }
18989
+ const zh = /^(\d+)(分钟|小时|天)后$/.exec(tokens[0] ?? "");
18990
+ if (zh) {
18991
+ const amount = Number(zh[1]);
18992
+ const unit = zh[2];
18993
+ const ms = unit === "分钟" ? amount * 60000 : unit === "小时" ? amount * 3600000 : amount * 86400000;
18994
+ return { executeAt: new Date(now.getTime() + ms), messageStartIndex: 1 };
18995
+ }
18996
+ return null;
18997
+ }
18998
+ function parseDuration(value) {
18999
+ const match = /^(\d+)(m|min|minute|minutes|h|hour|hours|d|day|days)$/.exec(value.toLowerCase());
19000
+ if (!match)
19001
+ return null;
19002
+ const amount = Number(match[1]);
19003
+ const unit = match[2];
19004
+ if (unit === "m" || unit === "min" || unit === "minute" || unit === "minutes")
19005
+ return amount * 60000;
19006
+ if (unit === "h" || unit === "hour" || unit === "hours")
19007
+ return amount * 3600000;
19008
+ return amount * 86400000;
19009
+ }
19010
+ function parseAbsolute(tokens, now) {
19011
+ if (tokens[0] === "at" && tokens[1]) {
19012
+ const parsed = parseClock(tokens[1]);
19013
+ if (!parsed)
19014
+ return null;
19015
+ const executeAt = atLocalDate(now, 0, parsed.hour, parsed.minute);
19016
+ if (executeAt.getTime() <= now.getTime())
19017
+ return { executeAt, messageStartIndex: 2, pastTodayValue: tokens[1] };
19018
+ return { executeAt, messageStartIndex: 2 };
19019
+ }
19020
+ const dayWord = tokens[0]?.toLowerCase();
19021
+ const dayOffset = dayWord === "today" || dayWord === "今天" ? 0 : dayWord === "tomorrow" || dayWord === "明天" ? 1 : dayWord === "后天" ? 2 : null;
19022
+ if (dayOffset !== null && tokens[1]) {
19023
+ const parsed = parseClock(tokens[1]);
19024
+ if (!parsed)
19025
+ return null;
19026
+ const executeAt = atLocalDate(now, dayOffset, parsed.hour, parsed.minute);
19027
+ if (dayOffset === 0 && executeAt.getTime() <= now.getTime())
19028
+ return { executeAt, messageStartIndex: 2, pastTodayValue: tokens[1] };
19029
+ return { executeAt, messageStartIndex: 2 };
19030
+ }
19031
+ const weekday = WEEKDAYS.get(tokens[0]?.toLowerCase() ?? "");
19032
+ if (weekday !== undefined && tokens[1]) {
19033
+ const parsed = parseClock(tokens[1]);
19034
+ if (!parsed)
19035
+ return null;
19036
+ let days = (weekday - now.getDay() + 7) % 7;
19037
+ let executeAt = atLocalDate(now, days, parsed.hour, parsed.minute);
19038
+ if (days === 0 && executeAt.getTime() <= now.getTime()) {
19039
+ days = 7;
19040
+ executeAt = atLocalDate(now, days, parsed.hour, parsed.minute);
19041
+ }
19042
+ return { executeAt, messageStartIndex: 2 };
19043
+ }
19044
+ return null;
19045
+ }
19046
+ function parseClock(value) {
19047
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value);
19048
+ if (!match)
19049
+ return null;
19050
+ const hour = Number(match[1]);
19051
+ const minute = Number(match[2]);
19052
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
19053
+ return null;
19054
+ return { hour, minute };
19055
+ }
19056
+ function atLocalDate(now, dayOffset, hour, minute) {
19057
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayOffset, hour, minute, 0, 0);
19058
+ }
19059
+ function validateResult(executeAt, messageStartIndex, tokens, now, pastTodayValue) {
19060
+ if (pastTodayValue)
19061
+ return { ok: false, code: "past_today_time", value: pastTodayValue };
19062
+ if (tokens.slice(messageStartIndex).join(" ").trim().length === 0)
19063
+ return { ok: false, code: "missing_message" };
19064
+ const delta = executeAt.getTime() - now.getTime();
19065
+ if (delta < LATER_MIN_DELAY_MS)
19066
+ return { ok: false, code: "too_soon" };
19067
+ if (delta > LATER_MAX_DELAY_MS)
19068
+ return { ok: false, code: "out_of_range" };
19069
+ return { ok: true, executeAt, messageStartIndex };
19070
+ }
19071
+ var WEEKDAYS;
19072
+ var init_parse_later_time = __esm(() => {
19073
+ init_scheduled_types();
19074
+ WEEKDAYS = new Map([
19075
+ ["周日", 0],
19076
+ ["周天", 0],
19077
+ ["星期日", 0],
19078
+ ["星期天", 0],
19079
+ ["sun", 0],
19080
+ ["sunday", 0],
19081
+ ["周一", 1],
19082
+ ["星期一", 1],
19083
+ ["mon", 1],
19084
+ ["monday", 1],
19085
+ ["周二", 2],
19086
+ ["星期二", 2],
19087
+ ["tue", 2],
19088
+ ["tuesday", 2],
19089
+ ["周三", 3],
19090
+ ["星期三", 3],
19091
+ ["wed", 3],
19092
+ ["wednesday", 3],
19093
+ ["周四", 4],
19094
+ ["星期四", 4],
19095
+ ["thu", 4],
19096
+ ["thursday", 4],
19097
+ ["周五", 5],
19098
+ ["星期五", 5],
19099
+ ["fri", 5],
19100
+ ["friday", 5],
19101
+ ["周六", 6],
19102
+ ["星期六", 6],
19103
+ ["sat", 6],
19104
+ ["saturday", 6]
19105
+ ]);
19106
+ });
19107
+
19108
+ // src/scheduled/scheduled-render.ts
19109
+ function renderLaterHelp() {
19110
+ return [
19111
+ "定时任务用法:",
19112
+ "",
19113
+ "创建:",
19114
+ "/lt in 2h 检查 CI",
19115
+ "/lt 30分钟后 总结进展",
19116
+ "/lt tomorrow 09:00 看 PR",
19117
+ "/lt 周五 09:00 继续处理",
19118
+ "",
19119
+ "查看:",
19120
+ "/lt list",
19121
+ "",
19122
+ "取消:",
19123
+ "/lt cancel <id>",
19124
+ "",
19125
+ "说明:",
19126
+ "- 只支持一次性任务",
19127
+ "- 时间必须在 10 秒之后、7 天之内",
19128
+ "- 到点后会把消息发送到创建时绑定的会话",
19129
+ "- 触发通知和 agent 回复复用现有频道路由;微信回复额度由现有路由控制",
19130
+ "- 不支持延迟执行 / 开头的 weacpx 命令",
19131
+ "- 完整时间格式与说明见 docs/later-command.md"
19132
+ ].join(`
19133
+ `);
19134
+ }
19135
+ function renderLaterUnsupportedChannel() {
19136
+ return [
19137
+ "当前频道暂不支持定时任务,未创建任务。",
19138
+ "",
19139
+ "原因:这个频道还没有实现定时消息投递能力,任务到点后无法把结果发回原聊天。",
19140
+ "请切换到支持定时任务的频道后再使用 /lt。"
19141
+ ].join(`
19142
+ `);
19143
+ }
19144
+ function renderTaskCreated(task, displaySession) {
19145
+ return [
19146
+ `已创建定时任务 #${task.id}`,
19147
+ `执行时间:${formatLocalDateTime(new Date(task.execute_at))}`,
19148
+ `会话:${displaySession}`,
19149
+ `内容:${preview(task.message)}`
19150
+ ].join(`
19151
+ `);
19152
+ }
19153
+ function renderLaterList(tasks, displaySession) {
19154
+ if (tasks.length === 0)
19155
+ return "当前没有待执行定时任务。";
19156
+ return [
19157
+ "待执行定时任务:",
19158
+ "",
19159
+ ...tasks.flatMap((task) => [
19160
+ `#${task.id} ${formatLocalDateTime(new Date(task.execute_at))} 会话:${displaySession(task.session_alias)}`,
19161
+ preview(task.message),
19162
+ ""
19163
+ ])
19164
+ ].join(`
19165
+ `).trimEnd();
19166
+ }
19167
+ function preview(text) {
19168
+ return text.length <= LATER_MESSAGE_PREVIEW_CHARS ? text : `${text.slice(0, LATER_MESSAGE_PREVIEW_CHARS - 1)}…`;
19169
+ }
19170
+ function formatLocalDateTime(date4) {
19171
+ const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
19172
+ const pad = (value) => String(value).padStart(2, "0");
19173
+ return `${date4.getFullYear()}-${pad(date4.getMonth() + 1)}-${pad(date4.getDate())} ${weekdays[date4.getDay()]} ${pad(date4.getHours())}:${pad(date4.getMinutes())}`;
19174
+ }
19175
+ var init_scheduled_render = __esm(() => {
19176
+ init_scheduled_types();
19177
+ });
19178
+
19179
+ // src/commands/handlers/later-handler.ts
19180
+ function handleLaterHelp() {
19181
+ return { text: renderLaterHelp() };
19182
+ }
19183
+ async function handleLaterCreate(tokens, scheduled, chatKey, currentSessionAlias, accountId, replyContextToken) {
19184
+ if (!currentSessionAlias) {
19185
+ return {
19186
+ text: [
19187
+ "当前没有会话,无法创建定时任务。",
19188
+ "",
19189
+ "请先创建或切换到一个会话:",
19190
+ "- /ss codex --ws backend(新建并切换)",
19191
+ "- /use backend-codex(切换到已有会话)"
19192
+ ].join(`
19193
+ `)
19194
+ };
19195
+ }
19196
+ const result = parseLaterTime(tokens);
19197
+ if (!result.ok) {
19198
+ return { text: renderTimeParseError(result.code, result.value) };
19199
+ }
19200
+ const message = tokens.slice(result.messageStartIndex).join(" ").trim();
19201
+ if (message.startsWith("/")) {
19202
+ return {
19203
+ text: [
19204
+ "不支持延迟执行 / 开头的命令。",
19205
+ "",
19206
+ "如果需要让 agent 解释命令,可以用自然语言描述:",
19207
+ "例如:/lt in 1h 请解释 /status 的作用"
19208
+ ].join(`
19209
+ `)
19210
+ };
19211
+ }
19212
+ const task = await scheduled.createTask({
19213
+ chatKey,
19214
+ sessionAlias: currentSessionAlias,
19215
+ executeAt: result.executeAt,
19216
+ message,
19217
+ ...accountId ? { accountId } : {},
19218
+ ...replyContextToken ? { replyContextToken } : {}
19219
+ });
19220
+ return { text: renderTaskCreated(task, toDisplaySessionAlias(currentSessionAlias)) };
19221
+ }
19222
+ function handleLaterList(scheduled) {
19223
+ const tasks = scheduled.listPending();
19224
+ return { text: renderLaterList(tasks, (alias) => toDisplaySessionAlias(alias)) };
19225
+ }
19226
+ async function handleLaterCancel(id, scheduled) {
19227
+ const ok = await scheduled.cancelPending(id);
19228
+ if (ok) {
19229
+ return { text: `已取消定时任务 #${id.replace(/^#/, "").toLowerCase()}` };
19230
+ }
19231
+ const displayId = id.replace(/^#/, "").toLowerCase();
19232
+ return { text: [`未找到待执行的定时任务 #${displayId}。`, "可以用 /lt list 查看当前待执行任务。"].join(`
19233
+ `) };
19234
+ }
19235
+ function renderTimeParseError(code, value) {
19236
+ switch (code) {
19237
+ case "missing_message":
19238
+ return "定时任务需要消息内容,请在时间后附上要发送的内容。";
19239
+ case "too_soon":
19240
+ return "定时任务执行时间必须在 10 秒之后。";
19241
+ case "out_of_range":
19242
+ return "定时任务执行时间不能超过 7 天。";
19243
+ case "past_today_time":
19244
+ return `今天 ${value} 已经过了,请指定一个未来的时间,或使用「明天」。`;
19245
+ case "unrecognized_time":
19246
+ case "missing_time":
19247
+ default:
19248
+ return [
19249
+ "无法识别时间格式。",
19250
+ "",
19251
+ "支持的格式:",
19252
+ "- /lt in 2h 消息(2小时后)",
19253
+ "- /lt 30分钟后 消息",
19254
+ "- /lt tomorrow 09:00 消息",
19255
+ "- /lt 周五 09:00 消息"
19256
+ ].join(`
19257
+ `);
19258
+ }
19259
+ }
19260
+ var laterHelpMetadata;
19261
+ var init_later_handler = __esm(() => {
19262
+ init_parse_later_time();
19263
+ init_scheduled_render();
19264
+ init_channel_scope();
19265
+ laterHelpMetadata = {
19266
+ topic: "later",
19267
+ aliases: ["lt"],
19268
+ summary: "定时任务:延时发送消息到当前会话",
19269
+ commands: [
19270
+ { usage: "/lt <时间> <消息>", description: "创建定时任务" },
19271
+ { usage: "/lt list", description: "查看待执行定时任务" },
19272
+ { usage: "/lt cancel <id>", description: "取消定时任务" }
19273
+ ],
19274
+ examples: [
19275
+ "/lt in 2h 检查 CI",
19276
+ "/lt 30分钟后 总结进展",
19277
+ "/lt tomorrow 09:00 看 PR",
19278
+ "/lt 今天 21:30 继续处理",
19279
+ "/lt 周五 09:00 继续处理"
19280
+ ],
19281
+ notes: [
19282
+ "只支持一次性任务,不支持重复执行",
19283
+ "时间必须在 10 秒之后、7 天之内",
19284
+ "到点后会把消息发送到创建时绑定的会话(不随之后 /use 切换而改变)",
19285
+ "/lt list 显示全局待执行任务;群聊中只有群主可取消",
19286
+ "不支持延迟执行 / 开头的 weacpx 命令",
19287
+ "完整时间格式与说明见 docs/later-command.md"
19288
+ ]
19289
+ };
19290
+ });
19291
+
18682
19292
  // src/commands/help/help-registry.ts
18683
19293
  function getHelpTopic(topic) {
18684
19294
  return HELP_TOPIC_MAP.get(topic) ?? null;
@@ -18694,6 +19304,7 @@ var init_help_registry = __esm(() => {
18694
19304
  init_permission_handler();
18695
19305
  init_session_handler();
18696
19306
  init_workspace_handler();
19307
+ init_later_handler();
18697
19308
  HELP_TOPICS = [
18698
19309
  sessionHelp,
18699
19310
  workspaceHelp,
@@ -18704,7 +19315,8 @@ var init_help_registry = __esm(() => {
18704
19315
  modeHelp,
18705
19316
  replyModeHelp,
18706
19317
  statusHelp,
18707
- cancelHelp
19318
+ cancelHelp,
19319
+ laterHelpMetadata
18708
19320
  ];
18709
19321
  HELP_TOPIC_MAP = new Map;
18710
19322
  for (const topic of HELP_TOPICS) {
@@ -18751,7 +19363,8 @@ function renderHelpTopic(topic) {
18751
19363
  "",
18752
19364
  "命令:",
18753
19365
  ...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
18754
- ...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : []
19366
+ ...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : [],
19367
+ ...topic.notes && topic.notes.length > 0 ? ["", "注意:", ...topic.notes.map((note) => `- ${note}`)] : []
18755
19368
  ].join(`
18756
19369
  `);
18757
19370
  }
@@ -19402,6 +20015,8 @@ class CommandRouter {
19402
20015
  resolveSessionAgentCommand;
19403
20016
  orchestration;
19404
20017
  quota;
20018
+ scheduled;
20019
+ scheduledDelivery;
19405
20020
  logger;
19406
20021
  autoInstall = autoInstallOptionalDep;
19407
20022
  discoverPaths = discoverParentPackagePaths;
@@ -19411,7 +20026,7 @@ class CommandRouter {
19411
20026
  __setDiscoverPathsForTest(fn) {
19412
20027
  this.discoverPaths = fn;
19413
20028
  }
19414
- constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota) {
20029
+ constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota, scheduled, scheduledDelivery) {
19415
20030
  this.sessions = sessions;
19416
20031
  this.transport = transport;
19417
20032
  this.config = config2;
@@ -19419,6 +20034,8 @@ class CommandRouter {
19419
20034
  this.resolveSessionAgentCommand = resolveSessionAgentCommand;
19420
20035
  this.orchestration = orchestration;
19421
20036
  this.quota = quota;
20037
+ this.scheduled = scheduled;
20038
+ this.scheduledDelivery = scheduledDelivery;
19422
20039
  this.logger = logger2 ?? createNoopAppLogger();
19423
20040
  }
19424
20041
  async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan) {
@@ -19539,8 +20156,38 @@ class CommandRouter {
19539
20156
  return await handleTaskReject(this.createHandlerContext(), chatKey, command.taskId);
19540
20157
  case "task.cancel":
19541
20158
  return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
19542
- case "prompt":
19543
- return await handlePrompt(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
20159
+ case "later.help":
20160
+ if (!this.scheduled)
20161
+ return { text: "定时任务服务未启用。" };
20162
+ return handleLaterHelp();
20163
+ case "later.list":
20164
+ if (!this.scheduled)
20165
+ return { text: "定时任务服务未启用。" };
20166
+ return handleLaterList(this.scheduled);
20167
+ case "later.create": {
20168
+ if (!this.scheduled)
20169
+ return { text: "定时任务服务未启用。" };
20170
+ if (this.scheduledDelivery && !this.scheduledDelivery.supportsScheduledMessages(chatKey)) {
20171
+ return { text: renderLaterUnsupportedChannel() };
20172
+ }
20173
+ const currentSession = await this.sessions.getCurrentSession(chatKey);
20174
+ return await handleLaterCreate(command.tokens, this.scheduled, chatKey, currentSession?.alias ?? null, accountId, replyContextToken);
20175
+ }
20176
+ case "later.cancel":
20177
+ if (!this.scheduled)
20178
+ return { text: "定时任务服务未启用。" };
20179
+ return await handleLaterCancel(command.id, this.scheduled);
20180
+ case "prompt": {
20181
+ const sessionContext = this.createSessionHandlerContext(undefined, perfSpan);
20182
+ if (metadata?.scheduledSessionAlias) {
20183
+ const scheduledSession = await this.sessions.getSession(metadata.scheduledSessionAlias);
20184
+ if (!scheduledSession) {
20185
+ throw new Error(`session "${metadata.scheduledSessionAlias}" not found for scheduled prompt`);
20186
+ }
20187
+ return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
20188
+ }
20189
+ return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
20190
+ }
19544
20191
  }
19545
20192
  });
19546
20193
  }
@@ -19918,11 +20565,13 @@ var init_command_router = __esm(() => {
19918
20565
  init_agent_handler();
19919
20566
  init_workspace_handler();
19920
20567
  init_session_shortcut_handler();
20568
+ init_later_handler();
19921
20569
  init_session_recovery_handler();
19922
20570
  init_auto_install_optional_dep();
19923
20571
  init_discover_parent_package_paths();
19924
20572
  init_errors();
19925
20573
  init_session_reset_handler();
20574
+ init_scheduled_render();
19926
20575
  });
19927
20576
 
19928
20577
  // src/config/resolve-acpx-command.ts
@@ -23919,6 +24568,220 @@ function buildWorkerAnswerPrompt(answer) {
23919
24568
  `);
23920
24569
  }
23921
24570
 
24571
+ // src/scheduled/scheduled-scheduler.ts
24572
+ class ScheduledTaskScheduler {
24573
+ service;
24574
+ intervalMs;
24575
+ dispatchTimeoutMs;
24576
+ setIntervalFn;
24577
+ clearIntervalFn;
24578
+ dispatchTask;
24579
+ logger;
24580
+ intervalHandle = null;
24581
+ ticking = false;
24582
+ constructor(service, deps) {
24583
+ this.service = service;
24584
+ this.dispatchTask = deps.dispatchTask;
24585
+ this.intervalMs = deps.intervalMs ?? 5000;
24586
+ this.dispatchTimeoutMs = deps.dispatchTimeoutMs ?? DEFAULT_DISPATCH_TIMEOUT_MS;
24587
+ this.setIntervalFn = deps.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
24588
+ this.clearIntervalFn = deps.clearIntervalFn ?? ((timer) => clearInterval(timer));
24589
+ this.logger = deps.logger;
24590
+ }
24591
+ async start() {
24592
+ if (this.intervalHandle !== null)
24593
+ return;
24594
+ await this.service.markStartupMissed();
24595
+ this.intervalHandle = this.setIntervalFn(() => {
24596
+ this.tick();
24597
+ }, this.intervalMs);
24598
+ await this.tick();
24599
+ }
24600
+ stop() {
24601
+ if (this.intervalHandle !== null) {
24602
+ this.clearIntervalFn(this.intervalHandle);
24603
+ this.intervalHandle = null;
24604
+ }
24605
+ }
24606
+ async tick() {
24607
+ if (this.ticking)
24608
+ return;
24609
+ this.ticking = true;
24610
+ try {
24611
+ const dueTasks = await this.service.claimDueTasks();
24612
+ for (const task of dueTasks) {
24613
+ try {
24614
+ await this.dispatchWithTimeout(task);
24615
+ await this.service.markExecuted(task.id);
24616
+ } catch (error2) {
24617
+ const message = error2 instanceof Error ? error2.message : String(error2);
24618
+ await this.logger?.error("scheduled.dispatch.failed", "failed to dispatch scheduled task", {
24619
+ taskId: task.id,
24620
+ message
24621
+ });
24622
+ await this.service.markFailed(task.id, error2);
24623
+ }
24624
+ }
24625
+ } finally {
24626
+ this.ticking = false;
24627
+ }
24628
+ }
24629
+ async dispatchWithTimeout(task) {
24630
+ const controller = new AbortController;
24631
+ let timer;
24632
+ const timeout = new Promise((_resolve, reject) => {
24633
+ timer = setTimeout(() => {
24634
+ reject(new Error(`scheduled task dispatch timed out after ${this.dispatchTimeoutMs}ms`));
24635
+ controller.abort();
24636
+ }, this.dispatchTimeoutMs);
24637
+ });
24638
+ const dispatch = this.dispatchTask(task, controller.signal);
24639
+ dispatch.catch(() => {});
24640
+ try {
24641
+ await Promise.race([dispatch, timeout]);
24642
+ } finally {
24643
+ if (timer !== undefined)
24644
+ clearTimeout(timer);
24645
+ }
24646
+ }
24647
+ }
24648
+ var DEFAULT_DISPATCH_TIMEOUT_MS;
24649
+ var init_scheduled_scheduler = __esm(() => {
24650
+ DEFAULT_DISPATCH_TIMEOUT_MS = 10 * 60 * 1000;
24651
+ });
24652
+
24653
+ // src/scheduled/scheduled-service.ts
24654
+ class ScheduledTaskService {
24655
+ state;
24656
+ stateStore;
24657
+ now;
24658
+ generateId;
24659
+ stateMutex;
24660
+ claimedInThisSession = new Set;
24661
+ constructor(state, stateStore, options) {
24662
+ this.state = state;
24663
+ this.stateStore = stateStore;
24664
+ this.now = options?.now ?? (() => new Date);
24665
+ this.generateId = options?.generateId ?? (() => Math.random().toString(36).slice(2, 6));
24666
+ this.stateMutex = options?.stateMutex ?? new AsyncMutex;
24667
+ }
24668
+ async createTask(input) {
24669
+ return await this.mutate(async () => {
24670
+ const id = this.nextId();
24671
+ const task = {
24672
+ id,
24673
+ chat_key: input.chatKey,
24674
+ session_alias: input.sessionAlias,
24675
+ execute_at: input.executeAt.toISOString(),
24676
+ message: input.message,
24677
+ status: "pending",
24678
+ created_at: this.now().toISOString(),
24679
+ ...input.accountId ? { account_id: input.accountId } : {},
24680
+ ...input.replyContextToken ? { reply_context_token: input.replyContextToken } : {},
24681
+ ...input.sourceLabel ? { source_label: input.sourceLabel } : {}
24682
+ };
24683
+ this.state.scheduled_tasks[id] = task;
24684
+ await this.save();
24685
+ return task;
24686
+ });
24687
+ }
24688
+ listPending() {
24689
+ return Object.values(this.state.scheduled_tasks).filter((task) => task.status === "pending").sort((left, right) => left.execute_at.localeCompare(right.execute_at));
24690
+ }
24691
+ async cancelPending(inputId) {
24692
+ return await this.mutate(async () => {
24693
+ const id = normalizeId(inputId);
24694
+ const task = this.state.scheduled_tasks[id];
24695
+ if (!task || task.status !== "pending")
24696
+ return false;
24697
+ task.status = "cancelled";
24698
+ task.cancelled_at = this.now().toISOString();
24699
+ await this.save();
24700
+ return true;
24701
+ });
24702
+ }
24703
+ async markStartupMissed() {
24704
+ await this.mutate(async () => {
24705
+ const nowMs = this.now().getTime();
24706
+ let changed = false;
24707
+ for (const task of Object.values(this.state.scheduled_tasks)) {
24708
+ if (task.status === "pending" && Date.parse(task.execute_at) < nowMs) {
24709
+ task.status = "missed";
24710
+ task.missed_at = this.now().toISOString();
24711
+ changed = true;
24712
+ }
24713
+ if (task.status === "triggering" && !this.claimedInThisSession.has(task.id)) {
24714
+ task.status = "failed";
24715
+ task.failed_at = this.now().toISOString();
24716
+ task.last_error = "process stopped while task was triggering";
24717
+ changed = true;
24718
+ }
24719
+ }
24720
+ if (changed)
24721
+ await this.save();
24722
+ });
24723
+ }
24724
+ async claimDueTasks() {
24725
+ return await this.mutate(async () => {
24726
+ const nowMs = this.now().getTime();
24727
+ const due = this.listPending().filter((task) => Date.parse(task.execute_at) <= nowMs);
24728
+ if (due.length === 0)
24729
+ return [];
24730
+ const at = this.now().toISOString();
24731
+ for (const task of due) {
24732
+ task.status = "triggering";
24733
+ task.triggered_at = at;
24734
+ this.claimedInThisSession.add(task.id);
24735
+ }
24736
+ await this.save();
24737
+ return due.map((task) => ({ ...task }));
24738
+ });
24739
+ }
24740
+ async markExecuted(id) {
24741
+ await this.mutate(async () => {
24742
+ const taskId = normalizeId(id);
24743
+ const task = this.state.scheduled_tasks[taskId];
24744
+ if (!task)
24745
+ return;
24746
+ task.status = "executed";
24747
+ task.executed_at = this.now().toISOString();
24748
+ this.claimedInThisSession.delete(taskId);
24749
+ await this.save();
24750
+ });
24751
+ }
24752
+ async markFailed(id, error2) {
24753
+ await this.mutate(async () => {
24754
+ const taskId = normalizeId(id);
24755
+ const task = this.state.scheduled_tasks[taskId];
24756
+ if (!task)
24757
+ return;
24758
+ task.status = "failed";
24759
+ task.failed_at = this.now().toISOString();
24760
+ task.last_error = error2 instanceof Error ? error2.message : String(error2);
24761
+ this.claimedInThisSession.delete(taskId);
24762
+ await this.save();
24763
+ });
24764
+ }
24765
+ nextId() {
24766
+ for (let attempt = 0;attempt < 20; attempt += 1) {
24767
+ const id = normalizeId(this.generateId()).replace(/[^0-9a-z]/g, "").slice(0, 6);
24768
+ if (id.length >= 4 && !this.state.scheduled_tasks[id])
24769
+ return id;
24770
+ }
24771
+ throw new Error("failed to generate unique scheduled task id");
24772
+ }
24773
+ async mutate(critical) {
24774
+ return await this.stateMutex.run(critical);
24775
+ }
24776
+ async save() {
24777
+ await this.stateStore.save(this.state);
24778
+ }
24779
+ }
24780
+ function normalizeId(input) {
24781
+ return input.trim().replace(/^#/, "").toLowerCase();
24782
+ }
24783
+ var init_scheduled_service = () => {};
24784
+
23922
24785
  // src/sessions/session-service.ts
23923
24786
  class SessionService {
23924
24787
  config;
@@ -23934,6 +24797,20 @@ class SessionService {
23934
24797
  async createSession(alias, agent, workspace) {
23935
24798
  return await this.createLogicalSession(alias, agent, workspace, `${workspace}:${alias}`);
23936
24799
  }
24800
+ listAllResolvedSessions() {
24801
+ const seen = new Set;
24802
+ const resolved = [];
24803
+ for (const session of Object.values(this.state.sessions)) {
24804
+ if (seen.has(session.transport_session)) {
24805
+ continue;
24806
+ }
24807
+ seen.add(session.transport_session);
24808
+ try {
24809
+ resolved.push(this.toResolvedSession(session));
24810
+ } catch {}
24811
+ }
24812
+ return resolved;
24813
+ }
23937
24814
  resolveSession(alias, agent, workspace, transportSession) {
23938
24815
  this.validateSession(alias, agent, workspace);
23939
24816
  return this.toResolvedSession({
@@ -24359,20 +25236,47 @@ async function runConsole(paths, deps) {
24359
25236
  runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
24360
25237
  }, 86400000);
24361
25238
  }
25239
+ const channelStartPromise = deps.channels.startAll({
25240
+ agent: runtime.agent,
25241
+ abortSignal: shutdownController.signal,
25242
+ quota: runtime.quota,
25243
+ logger: runtime.logger,
25244
+ perfTracer: runtime.perfTracer
25245
+ });
25246
+ channelStartPromise.catch(() => {});
25247
+ let channelStartSettled = false;
25248
+ let channelStartError;
25249
+ channelStartPromise.then(() => {
25250
+ channelStartSettled = true;
25251
+ }, (error2) => {
25252
+ channelStartSettled = true;
25253
+ channelStartError = error2;
25254
+ });
25255
+ await Promise.resolve();
25256
+ if (channelStartSettled && channelStartError) {
25257
+ if (deps.channelStartupPolicy !== "best-effort") {
25258
+ throw channelStartError;
25259
+ }
25260
+ await runtime.logger.error("daemon.channels.start_failed", "all channels failed to start; daemon remains alive for orchestration IPC", { error: channelStartError instanceof Error ? channelStartError.message : String(channelStartError) });
25261
+ await waitForShutdown(shutdownController.signal);
25262
+ return;
25263
+ }
24362
25264
  try {
24363
- await deps.channels.startAll({
24364
- agent: runtime.agent,
24365
- abortSignal: shutdownController.signal,
24366
- quota: runtime.quota,
24367
- logger: runtime.logger,
24368
- perfTracer: runtime.perfTracer
24369
- });
25265
+ await runtime.scheduled.scheduler.start();
25266
+ } catch (error2) {
25267
+ shutdownController.abort();
25268
+ throw error2;
25269
+ }
25270
+ try {
25271
+ await channelStartPromise;
24370
25272
  } catch (error2) {
25273
+ runtime.scheduled.scheduler.stop();
24371
25274
  if (deps.channelStartupPolicy !== "best-effort") {
24372
25275
  throw error2;
24373
25276
  }
24374
25277
  await runtime.logger.error("daemon.channels.start_failed", "all channels failed to start; daemon remains alive for orchestration IPC", { error: error2 instanceof Error ? error2.message : String(error2) });
24375
25278
  await waitForShutdown(shutdownController.signal);
25279
+ return;
24376
25280
  }
24377
25281
  } finally {
24378
25282
  await runCleanupSequence({
@@ -24621,7 +25525,8 @@ async function spawnAcpxBridgeClient(options = {}) {
24621
25525
  ...process.env,
24622
25526
  WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
24623
25527
  WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
24624
- WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
25528
+ WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
25529
+ ...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { WEACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {}
24625
25530
  },
24626
25531
  stdio: ["pipe", "pipe", "inherit"]
24627
25532
  });
@@ -25049,7 +25954,7 @@ var init_spawn_command = __esm(() => {
25049
25954
  });
25050
25955
 
25051
25956
  // src/transport/prompt-media.ts
25052
- import { mkdtemp, open as open3, rm as rm8, writeFile as writeFile8 } from "node:fs/promises";
25957
+ import { mkdtemp, open as open4, rm as rm8, writeFile as writeFile7 } from "node:fs/promises";
25053
25958
  import { tmpdir as defaultTmpdir } from "node:os";
25054
25959
  import path14 from "node:path";
25055
25960
  import { pathToFileURL as pathToFileURL2 } from "node:url";
@@ -25118,7 +26023,7 @@ async function writeStructuredPromptBlocks(blocks, deps) {
25118
26023
  }
25119
26024
  }
25120
26025
  async function readImageFileBounded(filePath, maxBytes) {
25121
- const handle = await open3(filePath, "r");
26026
+ const handle = await open4(filePath, "r");
25122
26027
  try {
25123
26028
  const imageStats = await handle.stat();
25124
26029
  if (!imageStats.isFile()) {
@@ -25173,7 +26078,7 @@ var init_prompt_media = __esm(() => {
25173
26078
  defaultStructuredPromptFileDeps = {
25174
26079
  readImageFile: readImageFileBounded,
25175
26080
  mkdtemp,
25176
- writeFile: writeFile8,
26081
+ writeFile: writeFile7,
25177
26082
  rm: rm8,
25178
26083
  tmpdir: defaultTmpdir
25179
26084
  };
@@ -25480,12 +26385,12 @@ function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
25480
26385
  }
25481
26386
  return join12(dirname12(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
25482
26387
  }
25483
- async function ensureNodePtyHelperExecutable(helperPath, chmod2 = chmodFs) {
26388
+ async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
25484
26389
  if (!helperPath) {
25485
26390
  return;
25486
26391
  }
25487
26392
  try {
25488
- await chmod2(helperPath, 493);
26393
+ await chmod3(helperPath, 493);
25489
26394
  } catch (error2) {
25490
26395
  if (error2.code === "ENOENT") {
25491
26396
  return;
@@ -25778,6 +26683,7 @@ class AcpxCliTransport {
25778
26683
  permissionMode;
25779
26684
  nonInteractivePermissions;
25780
26685
  permissionPolicy;
26686
+ queueOwnerTtlSeconds;
25781
26687
  runCommand;
25782
26688
  runPtyCommand;
25783
26689
  queueOwnerLauncher;
@@ -25788,10 +26694,12 @@ class AcpxCliTransport {
25788
26694
  this.permissionMode = options.permissionMode ?? "approve-all";
25789
26695
  this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
25790
26696
  this.permissionPolicy = options.permissionPolicy;
26697
+ this.queueOwnerTtlSeconds = options.queueOwnerTtlSeconds;
25791
26698
  this.runCommand = runCommand;
25792
26699
  this.runPtyCommand = runPtyCommand;
25793
26700
  this.queueOwnerLauncher = queueOwnerLauncher ?? new AcpxQueueOwnerLauncher({
25794
- acpxCommand: this.command
26701
+ acpxCommand: this.command,
26702
+ ...typeof this.queueOwnerTtlSeconds === "number" && Number.isFinite(this.queueOwnerTtlSeconds) ? { ttlMs: this.queueOwnerTtlSeconds * 1000 } : {}
25795
26703
  });
25796
26704
  this.streamingHooks = streamingHooks;
25797
26705
  }
@@ -26115,7 +27023,8 @@ ${baseText}` : "" };
26115
27023
  "--json-strict",
26116
27024
  "--cwd",
26117
27025
  session.cwd,
26118
- ...this.buildPermissionArgs()
27026
+ ...this.buildPermissionArgs(),
27027
+ ...this.buildQueueOwnerTtlArgs()
26119
27028
  ];
26120
27029
  const tail2 = promptFile ? ["prompt", "-s", session.transportSession, "--file", promptFile] : ["prompt", "-s", session.transportSession, text];
26121
27030
  if (session.agentCommand) {
@@ -26123,6 +27032,12 @@ ${baseText}` : "" };
26123
27032
  }
26124
27033
  return [...prefix, session.agent, ...tail2];
26125
27034
  }
27035
+ buildQueueOwnerTtlArgs() {
27036
+ if (typeof this.queueOwnerTtlSeconds !== "number" || !Number.isFinite(this.queueOwnerTtlSeconds)) {
27037
+ return [];
27038
+ }
27039
+ return ["--ttl", String(this.queueOwnerTtlSeconds)];
27040
+ }
26126
27041
  buildPermissionArgs() {
26127
27042
  const modeFlag = permissionModeToFlag(this.permissionMode);
26128
27043
  const args = [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
@@ -26169,6 +27084,146 @@ var init_acpx_cli_transport = __esm(() => {
26169
27084
  require4 = createRequire5(import.meta.url);
26170
27085
  });
26171
27086
 
27087
+ // src/transport/queue-owner-reaper.ts
27088
+ import { spawn as spawn10 } from "node:child_process";
27089
+ async function reapQueueOwners(acpxCommand, targets, deps = {}) {
27090
+ const resolveRecordId = deps.resolveRecordId ?? defaultResolveRecordId;
27091
+ const terminate = deps.terminate ?? terminateAcpxQueueOwner;
27092
+ const timeoutMs = deps.timeoutMs ?? 5000;
27093
+ const seen = new Set;
27094
+ const unique = targets.filter((target) => {
27095
+ if (seen.has(target.transportSession)) {
27096
+ return false;
27097
+ }
27098
+ seen.add(target.transportSession);
27099
+ return true;
27100
+ });
27101
+ let terminated = 0;
27102
+ const reapOne = async (target) => {
27103
+ try {
27104
+ const recordId = await resolveRecordId(acpxCommand, target);
27105
+ if (!recordId) {
27106
+ return;
27107
+ }
27108
+ await terminate(recordId);
27109
+ terminated += 1;
27110
+ } catch (error2) {
27111
+ deps.onError?.(target, error2);
27112
+ }
27113
+ };
27114
+ await settleWithinTimeout(Promise.all(unique.map(reapOne)), timeoutMs);
27115
+ return { terminated, attempted: unique.length };
27116
+ }
27117
+ function settleWithinTimeout(work, timeoutMs) {
27118
+ return new Promise((resolve3) => {
27119
+ let settled = false;
27120
+ const finish = () => {
27121
+ if (!settled) {
27122
+ settled = true;
27123
+ resolve3();
27124
+ }
27125
+ };
27126
+ const timer = setTimeout(finish, timeoutMs);
27127
+ if (typeof timer.unref === "function") {
27128
+ timer.unref();
27129
+ }
27130
+ work.then(() => {
27131
+ clearTimeout(timer);
27132
+ finish();
27133
+ }, () => {
27134
+ clearTimeout(timer);
27135
+ finish();
27136
+ });
27137
+ });
27138
+ }
27139
+ async function defaultResolveRecordId(acpxCommand, target) {
27140
+ const args = [
27141
+ "--format",
27142
+ "quiet",
27143
+ "--cwd",
27144
+ target.cwd,
27145
+ ...target.agentCommand ? ["--agent", target.agentCommand] : [target.agent],
27146
+ "sessions",
27147
+ "show",
27148
+ target.transportSession
27149
+ ];
27150
+ const spawnSpec = resolveSpawnCommand(acpxCommand, args);
27151
+ const result = await runCapture2(spawnSpec.command, spawnSpec.args, 4000);
27152
+ if (result.code !== 0) {
27153
+ return null;
27154
+ }
27155
+ return parseRecordId(result.stdout);
27156
+ }
27157
+ function parseRecordId(stdout2) {
27158
+ try {
27159
+ const parsed = JSON.parse(stdout2);
27160
+ if (typeof parsed.acpxRecordId === "string") {
27161
+ return parsed.acpxRecordId;
27162
+ }
27163
+ if (typeof parsed.id === "string") {
27164
+ return parsed.id;
27165
+ }
27166
+ } catch {
27167
+ const firstLine = stdout2.trim().split(/\r?\n/, 1)[0];
27168
+ if (firstLine && /^[\w.:-]+$/.test(firstLine) && firstLine.length >= 8) {
27169
+ return firstLine;
27170
+ }
27171
+ }
27172
+ return null;
27173
+ }
27174
+ function runCapture2(command, args, timeoutMs) {
27175
+ return new Promise((resolve3) => {
27176
+ const child = spawn10(command, args, { stdio: ["ignore", "pipe", "ignore"] });
27177
+ let stdout2 = "";
27178
+ let done = false;
27179
+ const finish = (code) => {
27180
+ if (done) {
27181
+ return;
27182
+ }
27183
+ done = true;
27184
+ clearTimeout(timer);
27185
+ resolve3({ code, stdout: stdout2 });
27186
+ };
27187
+ const timer = setTimeout(() => {
27188
+ child.kill("SIGKILL");
27189
+ finish(1);
27190
+ }, timeoutMs);
27191
+ child.stdout?.on("data", (chunk) => {
27192
+ stdout2 += String(chunk);
27193
+ });
27194
+ child.once("error", () => finish(1));
27195
+ child.once("close", (code) => finish(code ?? 1));
27196
+ });
27197
+ }
27198
+ var init_queue_owner_reaper = __esm(() => {
27199
+ init_spawn_command();
27200
+ init_acpx_queue_owner_launcher();
27201
+ });
27202
+
27203
+ // src/transport/collect-reap-targets.ts
27204
+ function workerBindingReapTargets(orchestration, config2) {
27205
+ const targets = [];
27206
+ for (const [workerSession, binding] of Object.entries(orchestration.workerBindings)) {
27207
+ const agentConfig = config2.agents[binding.targetAgent];
27208
+ if (!agentConfig) {
27209
+ continue;
27210
+ }
27211
+ const cwd = binding.cwd ?? config2.workspaces[binding.workspace]?.cwd;
27212
+ if (!cwd) {
27213
+ continue;
27214
+ }
27215
+ const agentCommand = resolveAgentCommand(agentConfig.driver, agentConfig.command);
27216
+ targets.push({
27217
+ agent: binding.targetAgent,
27218
+ ...agentCommand ? { agentCommand } : {},
27219
+ cwd,
27220
+ transportSession: workerSession
27221
+ });
27222
+ }
27223
+ return targets;
27224
+ }
27225
+ var init_collect_reap_targets = () => {};
27226
+
26172
27227
  // src/channels/channel-registry.ts
26173
27228
  var exports_channel_registry = {};
26174
27229
  __export(exports_channel_registry, {
@@ -26224,6 +27279,21 @@ class MessageChannelRegistry {
26224
27279
  async sendCoordinatorMessage(input) {
26225
27280
  await this.requireByChatKey(input.chatKey).sendCoordinatorMessage(input);
26226
27281
  }
27282
+ supportsScheduledMessages(chatKey) {
27283
+ const [candidateChannelId] = chatKey.split(":", 1);
27284
+ if (chatKey.includes(":") && candidateChannelId && !this.channels.has(candidateChannelId)) {
27285
+ return false;
27286
+ }
27287
+ const channel = this.getByChatKey(chatKey);
27288
+ return !!channel?.sendScheduledMessage;
27289
+ }
27290
+ async sendScheduledMessage(input) {
27291
+ const channel = this.requireByChatKey(input.chatKey);
27292
+ if (!channel.sendScheduledMessage) {
27293
+ throw new Error(`channel '${channel.id}' does not support scheduled messages`);
27294
+ }
27295
+ await channel.sendScheduledMessage(input);
27296
+ }
26227
27297
  createConsumerLocks() {
26228
27298
  const result = [];
26229
27299
  for (const channel of this.channels.values()) {
@@ -26364,7 +27434,11 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
26364
27434
  if (thresholdSeconds <= 0) {
26365
27435
  return;
26366
27436
  }
27437
+ let ticking = false;
26367
27438
  return setInterval(async () => {
27439
+ if (ticking)
27440
+ return;
27441
+ ticking = true;
26368
27442
  try {
26369
27443
  const tasks = await orchestration.listHeartbeatTasks(thresholdSeconds);
26370
27444
  for (const task of tasks) {
@@ -26385,6 +27459,8 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
26385
27459
  await logger2.error("orchestration.heartbeat.check_failed", "heartbeat check failed", {
26386
27460
  message: error2 instanceof Error ? error2.message : String(error2)
26387
27461
  });
27462
+ } finally {
27463
+ ticking = false;
26388
27464
  }
26389
27465
  }, 60000);
26390
27466
  }
@@ -26432,12 +27508,14 @@ async function buildApp(paths, deps = {}) {
26432
27508
  }
26433
27509
  });
26434
27510
  const sessions = new SessionService(config2, debouncedStateStore, state, { stateMutex });
27511
+ const scheduledService = new ScheduledTaskService(state, debouncedStateStore, { stateMutex });
26435
27512
  const pendingWorkerDispatches = new Set;
26436
27513
  const transport = config2.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
26437
27514
  acpxCommand,
26438
27515
  bridgeEntryPath: resolveBridgeEntryPath(),
26439
27516
  permissionMode: config2.transport.permissionMode,
26440
- nonInteractivePermissions: config2.transport.nonInteractivePermissions
27517
+ nonInteractivePermissions: config2.transport.nonInteractivePermissions,
27518
+ ...typeof config2.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config2.transport.queueOwnerTtlSeconds } : {}
26441
27519
  })))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config2.transport, command: acpxCommand });
26442
27520
  const quota = new QuotaManager({
26443
27521
  onInbound: (chatKey) => {
@@ -26790,8 +27868,33 @@ async function buildApp(paths, deps = {}) {
26790
27868
  const progressHeartbeatInterval = startProgressHeartbeat(orchestration, config2, logger2, deps.channel ?? null);
26791
27869
  const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
26792
27870
  const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration);
26793
- const router = new CommandRouter(sessions, transport, config2, configStore, logger2, undefined, orchestration, quota);
27871
+ const router = new CommandRouter(sessions, transport, config2, configStore, logger2, undefined, orchestration, quota, scheduledService, deps.channel?.supportsScheduledMessages ? { supportsScheduledMessages: deps.channel.supportsScheduledMessages.bind(deps.channel) } : undefined);
26794
27872
  const agent = new ConsoleAgent(router, logger2);
27873
+ const scheduledScheduler = new ScheduledTaskScheduler(scheduledService, {
27874
+ dispatchTask: async (task, abortSignal) => {
27875
+ const session = await sessions.getSession(task.session_alias);
27876
+ if (!session) {
27877
+ throw new Error(`session "${task.session_alias}" not found for scheduled task`);
27878
+ }
27879
+ const noticeText = `执行定时任务 #${task.id}
27880
+ 会话:${toDisplaySessionAlias(task.session_alias)}
27881
+ 内容:${preview(task.message)}`;
27882
+ if (!deps.channel?.sendScheduledMessage) {
27883
+ throw new Error("no channel runtime available for scheduled task dispatch");
27884
+ }
27885
+ await deps.channel.sendScheduledMessage({
27886
+ chatKey: task.chat_key,
27887
+ taskId: task.id,
27888
+ sessionAlias: task.session_alias,
27889
+ noticeText,
27890
+ promptText: task.message,
27891
+ abortSignal,
27892
+ ...task.account_id ? { accountId: task.account_id } : {},
27893
+ ...task.reply_context_token ? { replyContextToken: task.reply_context_token } : {}
27894
+ });
27895
+ },
27896
+ logger: logger2
27897
+ });
26795
27898
  return {
26796
27899
  agent,
26797
27900
  router,
@@ -26807,11 +27910,45 @@ async function buildApp(paths, deps = {}) {
26807
27910
  server: orchestrationServer,
26808
27911
  endpoint: orchestrationEndpoint
26809
27912
  },
27913
+ scheduled: {
27914
+ service: scheduledService,
27915
+ scheduler: scheduledScheduler
27916
+ },
26810
27917
  dispose: async () => {
27918
+ scheduledScheduler.stop();
26811
27919
  if (progressHeartbeatInterval !== undefined) {
26812
27920
  clearInterval(progressHeartbeatInterval);
26813
27921
  }
26814
27922
  await Promise.allSettled([...pendingWorkerDispatches]);
27923
+ try {
27924
+ const targets = [
27925
+ ...sessions.listAllResolvedSessions().map((session) => ({
27926
+ agent: session.agent,
27927
+ ...session.agentCommand ? { agentCommand: session.agentCommand } : {},
27928
+ cwd: session.cwd,
27929
+ transportSession: session.transportSession
27930
+ })),
27931
+ ...workerBindingReapTargets(state.orchestration, config2)
27932
+ ];
27933
+ if (targets.length > 0) {
27934
+ const { terminated, attempted } = await reapQueueOwners(acpxCommand, targets, {
27935
+ onError: (target, error2) => {
27936
+ logger2.info("transport.queue_owner_reap.failed", "failed to reap queue owner on shutdown", {
27937
+ transport_session: target.transportSession,
27938
+ error: error2 instanceof Error ? error2.message : String(error2)
27939
+ }).catch(() => {});
27940
+ }
27941
+ });
27942
+ await logger2.info("transport.queue_owner_reap.completed", "reaped warm queue owners on shutdown", {
27943
+ terminated,
27944
+ attempted
27945
+ }).catch(() => {});
27946
+ }
27947
+ } catch (err) {
27948
+ await logger2.error("transport.queue_owner_reap.error", "queue owner reap failed during shutdown", {
27949
+ error: err instanceof Error ? err.message : String(err)
27950
+ }).catch(() => {});
27951
+ }
26815
27952
  await debouncedStateStore.dispose();
26816
27953
  if ("dispose" in transport && typeof transport.dispose === "function") {
26817
27954
  await transport.dispose();
@@ -26831,6 +27968,7 @@ function replaceRuntimeState(target, source) {
26831
27968
  target.sessions = source.sessions;
26832
27969
  target.chat_contexts = source.chat_contexts;
26833
27970
  target.orchestration = source.orchestration;
27971
+ target.scheduled_tasks = source.scheduled_tasks;
26834
27972
  }
26835
27973
  function replaceRuntimeConfig(target, source) {
26836
27974
  Object.assign(target, source);
@@ -26842,7 +27980,12 @@ async function main() {
26842
27980
  await ensureConfigExists(paths.configPath);
26843
27981
  const startupConfig = await loadConfig(paths.configPath);
26844
27982
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
26845
- await loadConfiguredPlugins2({ plugins: startupConfig.plugins });
27983
+ await loadConfiguredPlugins2({
27984
+ plugins: startupConfig.plugins,
27985
+ onPluginError: ({ name, error: error2 }) => {
27986
+ console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
27987
+ }
27988
+ });
26846
27989
  const { channelDeps } = await prepareChannelMedia(paths.configPath, startupConfig);
26847
27990
  const channelRegistry = new MessageChannelRegistry(createMessageChannels2(startupConfig.channels, channelDeps));
26848
27991
  await runConsole(paths, {
@@ -26920,12 +28063,18 @@ var init_main = __esm(async () => {
26920
28063
  init_orchestration_server();
26921
28064
  init_orchestration_service();
26922
28065
  init_build_coordinator_prompt();
28066
+ init_scheduled_scheduler();
28067
+ init_scheduled_service();
28068
+ init_scheduled_render();
28069
+ init_channel_scope();
26923
28070
  init_session_service();
26924
28071
  init_state_store();
26925
28072
  init_run_console();
26926
28073
  init_acpx_bridge_client();
26927
28074
  init_acpx_bridge_transport();
26928
28075
  init_acpx_cli_transport();
28076
+ init_queue_owner_reaper();
28077
+ init_collect_reap_targets();
26929
28078
  init_channel_registry();
26930
28079
  init_media_store();
26931
28080
  init_quota_errors();
@@ -26936,7 +28085,7 @@ var init_main = __esm(async () => {
26936
28085
  });
26937
28086
 
26938
28087
  // src/doctor/checks/acpx-check.ts
26939
- import { spawn as spawn10 } from "node:child_process";
28088
+ import { spawn as spawn11 } from "node:child_process";
26940
28089
  async function checkAcpx(options = {}) {
26941
28090
  const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
26942
28091
  try {
@@ -26983,7 +28132,7 @@ function buildDetails(metadata, version2, verbose) {
26983
28132
  async function defaultRunVersion(command) {
26984
28133
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
26985
28134
  return await new Promise((resolve3, reject) => {
26986
- const child = spawn10(spawnSpec.command, spawnSpec.args, {
28135
+ const child = spawn11(spawnSpec.command, spawnSpec.args, {
26987
28136
  stdio: ["ignore", "pipe", "pipe"]
26988
28137
  });
26989
28138
  let stdout2 = "";
@@ -28004,7 +29153,7 @@ import { fileURLToPath as fileURLToPath6 } from "node:url";
28004
29153
 
28005
29154
  // src/daemon/daemon-runtime.ts
28006
29155
  init_daemon_status();
28007
- import { mkdir as mkdir5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
29156
+ import { mkdir as mkdir5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
28008
29157
  import { dirname as dirname5 } from "node:path";
28009
29158
 
28010
29159
  class DaemonRuntime {
@@ -28032,7 +29181,7 @@ class DaemonRuntime {
28032
29181
  stderr_log: this.paths.stderrLog
28033
29182
  };
28034
29183
  await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
28035
- await writeFile4(this.paths.pidFile, `${this.options.pid}
29184
+ await writeFile3(this.paths.pidFile, `${this.options.pid}
28036
29185
  `);
28037
29186
  await this.statusStore.save(this.currentStatus);
28038
29187
  }
@@ -43916,7 +45065,12 @@ async function defaultRun(options = {}) {
43916
45065
  await ensureConfigExists(runtimePaths.configPath);
43917
45066
  const config2 = await loadConfig(runtimePaths.configPath);
43918
45067
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
43919
- await loadConfiguredPlugins2({ plugins: config2.plugins });
45068
+ await loadConfiguredPlugins2({
45069
+ plugins: config2.plugins,
45070
+ onPluginError: ({ name, error: error2 }) => {
45071
+ console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
45072
+ }
45073
+ });
43920
45074
  const { createMessageChannels: createMessageChannels2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
43921
45075
  const { MessageChannelRegistry: MessageChannelRegistry2 } = await Promise.resolve().then(() => (init_channel_registry(), exports_channel_registry));
43922
45076
  const daemonPaths = resolveDaemonPathsForCurrentConfig();