weacpx 0.4.10 → 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/README.md CHANGED
@@ -323,6 +323,30 @@ opencode, qoder, qwen, trae
323
323
  /cancel
324
324
  ```
325
325
 
326
+ ### 定时任务(/later)
327
+
328
+ 让 agent 在未来某个时间自动收到一条消息。任务绑定「创建时的当前会话」,到点后把这条消息作为普通 prompt 发给那个会话。
329
+
330
+ | 命令 | 说明 |
331
+ |------|------|
332
+ | `/lt <时间> <消息>` | 创建一次性定时任务(`/later` 同义) |
333
+ | `/lt list` | 查看全局待执行任务 |
334
+ | `/lt cancel <id>` | 取消待执行任务 |
335
+
336
+ 最常见例子:
337
+
338
+ ```text
339
+ /lt in 2h 检查 CI 是否通过
340
+ /lt 明天 09:00 看 PR
341
+ /lt list
342
+ ```
343
+
344
+ 说明:
345
+
346
+ - 只支持一次性任务,时间必须在 10 秒之后、7 天之内
347
+ - 时间格式是固定白名单(相对时间 / 今天·明天·后天 / 星期几 + 时刻),不支持自然语言
348
+ - 完整时间格式、任务状态与限制见 [docs/later-command.md](./docs/later-command.md)
349
+
326
350
  ### 配置与权限
327
351
 
328
352
  | 命令 | 说明 |
@@ -514,6 +538,7 @@ bun run dev
514
538
  ### 日常使用
515
539
 
516
540
  - 想查看完整聊天命令参考:[docs/commands.md](./docs/commands.md)
541
+ - 想用定时任务(`/later`)安排一次性的未来消息:[docs/later-command.md](./docs/later-command.md)
517
542
  - 想理解什么时候该用 delegate、什么时候该开 group:[docs/weacpx-group-usage-guide.md](./docs/weacpx-group-usage-guide.md)
518
543
 
519
544
  ### 排错与验证
@@ -22,6 +22,16 @@ export interface CoordinatorMessageInput {
22
22
  replyContextToken?: string;
23
23
  text: string;
24
24
  }
25
+ export interface ScheduledChannelMessageInput {
26
+ chatKey: string;
27
+ taskId?: string;
28
+ sessionAlias: string;
29
+ accountId?: string;
30
+ replyContextToken?: string;
31
+ noticeText: string;
32
+ promptText: string;
33
+ abortSignal?: AbortSignal;
34
+ }
25
35
  export interface ChannelStartInput {
26
36
  agent: ChatAgent;
27
37
  abortSignal: AbortSignal;
@@ -60,6 +70,7 @@ export interface MessageChannelRuntime {
60
70
  notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
61
71
  notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
62
72
  sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
73
+ sendScheduledMessage?(input: ScheduledChannelMessageInput): Promise<void>;
63
74
  }
64
75
  export type ToolUseStatus = "running" | "success" | "error";
65
76
  export type ToolUseKind = "read" | "search" | "execute" | "edit" | "think" | "other";
@@ -1,8 +1,9 @@
1
- import type { MessageChannelRuntime, ChannelStartInput, CoordinatorMessageInput, OrchestrationDeliveryCallbacks, ConsumerLock, ConsumerLockOptions } from "./types.js";
1
+ import type { MessageChannelRuntime, ChannelStartInput, CoordinatorMessageInput, OrchestrationDeliveryCallbacks, ConsumerLock, ConsumerLockOptions, ScheduledChannelMessageInput } from "./types.js";
2
2
  import type { RuntimeMediaStore } from "./media-store.js";
3
3
  import type { OrchestrationTaskRecord } from "../orchestration/orchestration-types.js";
4
4
  export declare class WeixinChannel implements MessageChannelRuntime {
5
5
  readonly id = "weixin";
6
+ private agent;
6
7
  private quota;
7
8
  private logger;
8
9
  private markDelivered;
@@ -19,4 +20,5 @@ export declare class WeixinChannel implements MessageChannelRuntime {
19
20
  notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
20
21
  notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
21
22
  sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
23
+ sendScheduledMessage(input: ScheduledChannelMessageInput): Promise<void>;
22
24
  }
package/dist/cli.js CHANGED
@@ -9735,7 +9735,8 @@ function createEmptyState() {
9735
9735
  return {
9736
9736
  sessions: {},
9737
9737
  chat_contexts: {},
9738
- orchestration: createEmptyOrchestrationState()
9738
+ orchestration: createEmptyOrchestrationState(),
9739
+ scheduled_tasks: {}
9739
9740
  };
9740
9741
  }
9741
9742
  var init_types = () => {};
@@ -9981,6 +9982,29 @@ function parseChatContexts(raw, path3) {
9981
9982
  }
9982
9983
  return chatContexts;
9983
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
+ }
9984
10008
  function parseState(raw, path3) {
9985
10009
  if (!isRecord2(raw)) {
9986
10010
  throw new Error(`state file "${path3}" must contain a JSON object`);
@@ -9999,7 +10023,8 @@ function parseState(raw, path3) {
9999
10023
  return {
10000
10024
  sessions: parsedSessions,
10001
10025
  chat_contexts: parseChatContexts(chatContexts, path3),
10002
- orchestration
10026
+ orchestration,
10027
+ scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
10003
10028
  };
10004
10029
  }
10005
10030
  function validateExternalCoordinatorIdentityCollisions(sessions, orchestration, path3) {
@@ -15442,6 +15467,177 @@ var init_deliver_coordinator_message = __esm(() => {
15442
15467
  init_send();
15443
15468
  });
15444
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
+
15445
15641
  // src/weixin/monitor/consumer-lock.ts
15446
15642
  import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15447
15643
  import { dirname as dirname8, join as join5 } from "node:path";
@@ -15568,6 +15764,7 @@ var init_consumer_lock = __esm(() => {
15568
15764
  // src/channels/weixin-channel.ts
15569
15765
  class WeixinChannel {
15570
15766
  id = "weixin";
15767
+ agent = null;
15571
15768
  quota = null;
15572
15769
  logger = null;
15573
15770
  markDelivered = null;
@@ -15598,6 +15795,7 @@ class WeixinChannel {
15598
15795
  this.markFailed = callbacks.markTaskNoticeFailed;
15599
15796
  }
15600
15797
  async start(input) {
15798
+ this.agent = input.agent;
15601
15799
  this.quota = input.quota;
15602
15800
  this.logger = input.logger;
15603
15801
  if (!this.isLoggedIn()) {
@@ -15658,6 +15856,23 @@ class WeixinChannel {
15658
15856
  logger: this.logger
15659
15857
  });
15660
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
+ }
15661
15876
  }
15662
15877
  var init_weixin_channel = __esm(() => {
15663
15878
  init_weixin();
@@ -15665,6 +15880,7 @@ var init_weixin_channel = __esm(() => {
15665
15880
  init_deliver_orchestration_task_notice();
15666
15881
  init_deliver_orchestration_task_progress();
15667
15882
  init_deliver_coordinator_message();
15883
+ init_scheduled_turn();
15668
15884
  init_consumer_lock();
15669
15885
  });
15670
15886
 
@@ -16049,7 +16265,7 @@ function validatePluginCompatibility(metadata, context) {
16049
16265
  }
16050
16266
  }
16051
16267
  }
16052
- 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;
16053
16269
  var init_compatibility = __esm(() => {
16054
16270
  WEACPX_PLUGIN_API_SUPPORTED_VERSIONS = [1];
16055
16271
  SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
@@ -16435,7 +16651,9 @@ var init_command_list = __esm(() => {
16435
16651
  "/dg",
16436
16652
  "/group",
16437
16653
  "/groups",
16438
- "/task"
16654
+ "/task",
16655
+ "/later",
16656
+ "/lt"
16439
16657
  ];
16440
16658
  KNOWN_COMMAND_PREFIX_SET = new Set(WEACPX_KNOWN_COMMAND_PREFIXES);
16441
16659
  });
@@ -16603,6 +16821,16 @@ function parseCommand(input) {
16603
16821
  } else if (command === "/task" && parts[1] && parts.length === 2) {
16604
16822
  return { kind: "task.get", taskId: parts[1] };
16605
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
+ }
16606
16834
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
16607
16835
  const name = parts[2];
16608
16836
  let cwd = "";
@@ -16771,6 +16999,8 @@ function normalizeCommand(command) {
16771
16999
  return "/permission";
16772
17000
  if (command === "/stop")
16773
17001
  return "/cancel";
17002
+ if (command === "/lt")
17003
+ return "/later";
16774
17004
  return command;
16775
17005
  }
16776
17006
  function isRecognizedCommand(command) {
@@ -16966,6 +17196,7 @@ var init_command_policy = __esm(() => {
16966
17196
  "group.get",
16967
17197
  "tasks",
16968
17198
  "task.get",
17199
+ "later.help",
16969
17200
  "invalid",
16970
17201
  "prompt"
16971
17202
  ]);
@@ -16995,7 +17226,10 @@ var init_command_policy = __esm(() => {
16995
17226
  "session.new": "/session new",
16996
17227
  "session.shortcut": "/session",
16997
17228
  "session.shortcut.new": "/session",
16998
- "session.attach": "/session attach"
17229
+ "session.attach": "/session attach",
17230
+ "later.create": "/later",
17231
+ "later.list": "/later list",
17232
+ "later.cancel": "/later cancel"
16999
17233
  };
17000
17234
  });
17001
17235
 
@@ -17949,11 +18183,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17949
18183
  throw error2;
17950
18184
  }
17951
18185
  }
17952
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
17953
- const session = await context.sessions.getCurrentSession(chatKey);
17954
- if (!session) {
17955
- return { text: NO_CURRENT_SESSION_TEXT };
17956
- }
18186
+ async function handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
17957
18187
  try {
17958
18188
  return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
17959
18189
  } catch (error2) {
@@ -17964,6 +18194,13 @@ async function handlePrompt(context, chatKey, text, reply, replyContextToken, ac
17964
18194
  return context.recovery.renderTransportError(session, error2);
17965
18195
  }
17966
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
+ }
17967
18204
  async function preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId) {
17968
18205
  const orchestration = context.orchestration;
17969
18206
  if (!orchestration) {
@@ -18725,6 +18962,333 @@ var init_workspace_handler = __esm(() => {
18725
18962
  };
18726
18963
  });
18727
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
+
18728
19292
  // src/commands/help/help-registry.ts
18729
19293
  function getHelpTopic(topic) {
18730
19294
  return HELP_TOPIC_MAP.get(topic) ?? null;
@@ -18740,6 +19304,7 @@ var init_help_registry = __esm(() => {
18740
19304
  init_permission_handler();
18741
19305
  init_session_handler();
18742
19306
  init_workspace_handler();
19307
+ init_later_handler();
18743
19308
  HELP_TOPICS = [
18744
19309
  sessionHelp,
18745
19310
  workspaceHelp,
@@ -18750,7 +19315,8 @@ var init_help_registry = __esm(() => {
18750
19315
  modeHelp,
18751
19316
  replyModeHelp,
18752
19317
  statusHelp,
18753
- cancelHelp
19318
+ cancelHelp,
19319
+ laterHelpMetadata
18754
19320
  ];
18755
19321
  HELP_TOPIC_MAP = new Map;
18756
19322
  for (const topic of HELP_TOPICS) {
@@ -18797,7 +19363,8 @@ function renderHelpTopic(topic) {
18797
19363
  "",
18798
19364
  "命令:",
18799
19365
  ...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
18800
- ...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}`)] : []
18801
19368
  ].join(`
18802
19369
  `);
18803
19370
  }
@@ -19448,6 +20015,8 @@ class CommandRouter {
19448
20015
  resolveSessionAgentCommand;
19449
20016
  orchestration;
19450
20017
  quota;
20018
+ scheduled;
20019
+ scheduledDelivery;
19451
20020
  logger;
19452
20021
  autoInstall = autoInstallOptionalDep;
19453
20022
  discoverPaths = discoverParentPackagePaths;
@@ -19457,7 +20026,7 @@ class CommandRouter {
19457
20026
  __setDiscoverPathsForTest(fn) {
19458
20027
  this.discoverPaths = fn;
19459
20028
  }
19460
- constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota) {
20029
+ constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota, scheduled, scheduledDelivery) {
19461
20030
  this.sessions = sessions;
19462
20031
  this.transport = transport;
19463
20032
  this.config = config2;
@@ -19465,6 +20034,8 @@ class CommandRouter {
19465
20034
  this.resolveSessionAgentCommand = resolveSessionAgentCommand;
19466
20035
  this.orchestration = orchestration;
19467
20036
  this.quota = quota;
20037
+ this.scheduled = scheduled;
20038
+ this.scheduledDelivery = scheduledDelivery;
19468
20039
  this.logger = logger2 ?? createNoopAppLogger();
19469
20040
  }
19470
20041
  async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan) {
@@ -19585,8 +20156,38 @@ class CommandRouter {
19585
20156
  return await handleTaskReject(this.createHandlerContext(), chatKey, command.taskId);
19586
20157
  case "task.cancel":
19587
20158
  return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
19588
- case "prompt":
19589
- 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
+ }
19590
20191
  }
19591
20192
  });
19592
20193
  }
@@ -19964,11 +20565,13 @@ var init_command_router = __esm(() => {
19964
20565
  init_agent_handler();
19965
20566
  init_workspace_handler();
19966
20567
  init_session_shortcut_handler();
20568
+ init_later_handler();
19967
20569
  init_session_recovery_handler();
19968
20570
  init_auto_install_optional_dep();
19969
20571
  init_discover_parent_package_paths();
19970
20572
  init_errors();
19971
20573
  init_session_reset_handler();
20574
+ init_scheduled_render();
19972
20575
  });
19973
20576
 
19974
20577
  // src/config/resolve-acpx-command.ts
@@ -23965,6 +24568,220 @@ function buildWorkerAnswerPrompt(answer) {
23965
24568
  `);
23966
24569
  }
23967
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
+
23968
24785
  // src/sessions/session-service.ts
23969
24786
  class SessionService {
23970
24787
  config;
@@ -24419,20 +25236,47 @@ async function runConsole(paths, deps) {
24419
25236
  runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
24420
25237
  }, 86400000);
24421
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
+ }
24422
25264
  try {
24423
- await deps.channels.startAll({
24424
- agent: runtime.agent,
24425
- abortSignal: shutdownController.signal,
24426
- quota: runtime.quota,
24427
- logger: runtime.logger,
24428
- perfTracer: runtime.perfTracer
24429
- });
25265
+ await runtime.scheduled.scheduler.start();
25266
+ } catch (error2) {
25267
+ shutdownController.abort();
25268
+ throw error2;
25269
+ }
25270
+ try {
25271
+ await channelStartPromise;
24430
25272
  } catch (error2) {
25273
+ runtime.scheduled.scheduler.stop();
24431
25274
  if (deps.channelStartupPolicy !== "best-effort") {
24432
25275
  throw error2;
24433
25276
  }
24434
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) });
24435
25278
  await waitForShutdown(shutdownController.signal);
25279
+ return;
24436
25280
  }
24437
25281
  } finally {
24438
25282
  await runCleanupSequence({
@@ -26435,6 +27279,21 @@ class MessageChannelRegistry {
26435
27279
  async sendCoordinatorMessage(input) {
26436
27280
  await this.requireByChatKey(input.chatKey).sendCoordinatorMessage(input);
26437
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
+ }
26438
27297
  createConsumerLocks() {
26439
27298
  const result = [];
26440
27299
  for (const channel of this.channels.values()) {
@@ -26649,6 +27508,7 @@ async function buildApp(paths, deps = {}) {
26649
27508
  }
26650
27509
  });
26651
27510
  const sessions = new SessionService(config2, debouncedStateStore, state, { stateMutex });
27511
+ const scheduledService = new ScheduledTaskService(state, debouncedStateStore, { stateMutex });
26652
27512
  const pendingWorkerDispatches = new Set;
26653
27513
  const transport = config2.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
26654
27514
  acpxCommand,
@@ -27008,8 +27868,33 @@ async function buildApp(paths, deps = {}) {
27008
27868
  const progressHeartbeatInterval = startProgressHeartbeat(orchestration, config2, logger2, deps.channel ?? null);
27009
27869
  const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
27010
27870
  const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration);
27011
- 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);
27012
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
+ });
27013
27898
  return {
27014
27899
  agent,
27015
27900
  router,
@@ -27025,7 +27910,12 @@ async function buildApp(paths, deps = {}) {
27025
27910
  server: orchestrationServer,
27026
27911
  endpoint: orchestrationEndpoint
27027
27912
  },
27913
+ scheduled: {
27914
+ service: scheduledService,
27915
+ scheduler: scheduledScheduler
27916
+ },
27028
27917
  dispose: async () => {
27918
+ scheduledScheduler.stop();
27029
27919
  if (progressHeartbeatInterval !== undefined) {
27030
27920
  clearInterval(progressHeartbeatInterval);
27031
27921
  }
@@ -27078,6 +27968,7 @@ function replaceRuntimeState(target, source) {
27078
27968
  target.sessions = source.sessions;
27079
27969
  target.chat_contexts = source.chat_contexts;
27080
27970
  target.orchestration = source.orchestration;
27971
+ target.scheduled_tasks = source.scheduled_tasks;
27081
27972
  }
27082
27973
  function replaceRuntimeConfig(target, source) {
27083
27974
  Object.assign(target, source);
@@ -27172,6 +28063,10 @@ var init_main = __esm(async () => {
27172
28063
  init_orchestration_server();
27173
28064
  init_orchestration_service();
27174
28065
  init_build_coordinator_prompt();
28066
+ init_scheduled_scheduler();
28067
+ init_scheduled_service();
28068
+ init_scheduled_render();
28069
+ init_channel_scope();
27175
28070
  init_session_service();
27176
28071
  init_state_store();
27177
28072
  init_run_console();
@@ -1,6 +1,6 @@
1
1
  export type { ChannelPluginDefinition } from "./channels/plugin.js";
2
2
  export type { ChannelFactory, CreateChannelDeps } from "./channels/create-channel.js";
3
- export type { ChannelStartInput, ConsumerLock, ConsumerLockMetadata, ConsumerLockOptions, CoordinatorMessageInput, MessageChannelRuntime, OrchestrationDeliveryCallbacks, OutboundQuota, ToolUseEvent, ToolUseKind, ToolUseStatus, } from "./channels/types.js";
3
+ export type { ChannelStartInput, ConsumerLock, ConsumerLockMetadata, ConsumerLockOptions, CoordinatorMessageInput, MessageChannelRuntime, ScheduledChannelMessageInput, OrchestrationDeliveryCallbacks, OutboundQuota, ToolUseEvent, ToolUseKind, ToolUseStatus, } from "./channels/types.js";
4
4
  export type { ChannelCliInput, ChannelCliIo, ChannelCliParseResult, ChannelCliProvider, ChannelCliValidationIssue, } from "./channels/cli/provider.js";
5
5
  export type { ChannelRuntimeConfig } from "./config/types.js";
6
6
  export type { AppLogger } from "./logging/app-logger.js";
@@ -160,7 +160,7 @@ function validatePluginCompatibility(metadata, context) {
160
160
  }
161
161
  }
162
162
  }
163
- var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.4.0", SEMVER_RE;
163
+ var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.5.0", SEMVER_RE;
164
164
  var init_compatibility = __esm(() => {
165
165
  WEACPX_PLUGIN_API_SUPPORTED_VERSIONS = [1];
166
166
  SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
@@ -1,6 +1,6 @@
1
1
  export declare const WEACPX_PLUGIN_API_VERSION: 1;
2
2
  export declare const WEACPX_PLUGIN_API_SUPPORTED_VERSIONS: readonly number[];
3
- export declare const WEACPX_PLUGIN_MIN_CORE_VERSION: "0.4.0";
3
+ export declare const WEACPX_PLUGIN_MIN_CORE_VERSION: "0.5.0";
4
4
  export declare function normalizeCoreVersionForCompat(version: string): string;
5
5
  export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
6
6
  export declare function isVersionSatisfied(current: string, requirement: string): boolean;
@@ -62,6 +62,8 @@ export interface ChatRequestMetadata {
62
62
  senderName?: string;
63
63
  groupId?: string;
64
64
  isOwner?: boolean;
65
+ /** Internal weacpx session alias to use for non-interactive scheduled prompts. */
66
+ scheduledSessionAlias?: string;
65
67
  }
66
68
  export interface ChatResponse {
67
69
  /**
@@ -0,0 +1,22 @@
1
+ import type { AppLogger } from "../../logging/app-logger";
2
+ import type { ScheduledChannelMessageInput } from "../../channels/types";
3
+ import type { Agent } from "../agent/interface";
4
+ import type { PendingFinalChunk } from "./quota-manager";
5
+ import { sendMessageWeixin } from "./send";
6
+ export interface ScheduledTurnDeps {
7
+ agent: Agent;
8
+ listAccountIds: () => string[];
9
+ resolveAccount: (accountId: string) => {
10
+ accountId: string;
11
+ baseUrl: string;
12
+ token?: string;
13
+ };
14
+ getContextToken: (accountId: string, userId: string) => string | undefined;
15
+ reserveMidSegment: (chatKey: string) => boolean;
16
+ reserveFinal: (chatKey: string) => boolean;
17
+ finalRemaining?: (chatKey: string) => number;
18
+ enqueuePendingFinal?: (chatKey: string, chunks: PendingFinalChunk[]) => void;
19
+ sendMessage?: typeof sendMessageWeixin;
20
+ logger: AppLogger;
21
+ }
22
+ export declare function executeScheduledTurn(input: ScheduledChannelMessageInput, deps: ScheduledTurnDeps): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weacpx",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "使用微信 ClawBot 随时随地通过 `acpx` 控制 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",