weacpx 0.4.10 → 0.5.1

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
@@ -2317,6 +2317,7 @@ function parseConfig(raw, options = {}) {
2317
2317
  const channelConfig = parseChannelConfig(channel, legacyWechat);
2318
2318
  const channelsConfig = parseRuntimeChannels(raw.channels, channelConfig);
2319
2319
  const orchestrationConfig = parseOrchestrationConfig(orchestration);
2320
+ const laterConfig = parseLaterConfig(raw.later);
2320
2321
  const plugins = parsePlugins(raw.plugins);
2321
2322
  return {
2322
2323
  transport: {
@@ -2348,7 +2349,8 @@ function parseConfig(raw, options = {}) {
2348
2349
  plugins,
2349
2350
  agents,
2350
2351
  workspaces,
2351
- orchestration: orchestrationConfig
2352
+ orchestration: orchestrationConfig,
2353
+ later: laterConfig
2352
2354
  };
2353
2355
  }
2354
2356
  function parsePluginConfig(raw, index) {
@@ -2437,6 +2439,11 @@ function parseRuntimeChannels(rawChannels, channel) {
2437
2439
  }
2438
2440
  ];
2439
2441
  }
2442
+ function parseLaterConfig(raw) {
2443
+ if (!isRecord(raw))
2444
+ return { ...DEFAULT_LATER_CONFIG };
2445
+ return raw.defaultMode === "bind" ? { defaultMode: "bind" } : { ...DEFAULT_LATER_CONFIG };
2446
+ }
2440
2447
  function parseOrchestrationConfig(raw) {
2441
2448
  if (!isRecord(raw)) {
2442
2449
  return {
@@ -2452,7 +2459,7 @@ function parseOrchestrationConfig(raw) {
2452
2459
  maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
2453
2460
  };
2454
2461
  }
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;
2462
+ 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, DEFAULT_LATER_CONFIG;
2456
2463
  var init_load_config = __esm(() => {
2457
2464
  init_workspace_path();
2458
2465
  DEFAULT_PERF_LOG_CONFIG = {
@@ -2480,6 +2487,9 @@ var init_load_config = __esm(() => {
2480
2487
  progressHeartbeatSeconds: 300,
2481
2488
  maxParallelTasksPerAgent: 3
2482
2489
  };
2490
+ DEFAULT_LATER_CONFIG = {
2491
+ defaultMode: "temp"
2492
+ };
2483
2493
  });
2484
2494
 
2485
2495
  // src/config/config-store.ts
@@ -2547,6 +2557,15 @@ var init_config_store = __esm(() => {
2547
2557
  init_load_config();
2548
2558
  });
2549
2559
 
2560
+ // src/config/default-workspace.ts
2561
+ var DEFAULT_HOME_WORKSPACE_NAME = "home", DEFAULT_HOME_WORKSPACE;
2562
+ var init_default_workspace = __esm(() => {
2563
+ DEFAULT_HOME_WORKSPACE = {
2564
+ cwd: "~",
2565
+ description: "用户主目录"
2566
+ };
2567
+ });
2568
+
2550
2569
  // src/config/ensure-config.ts
2551
2570
  import { readFile as readFile2 } from "node:fs/promises";
2552
2571
  async function ensureConfigExists(path2, options = {}) {
@@ -2601,7 +2620,9 @@ function normalizeDefaultConfigTemplate(raw) {
2601
2620
  ...resolveAgentCommand(agent.driver, agent.command) ? { command: resolveAgentCommand(agent.driver, agent.command) } : {}
2602
2621
  }
2603
2622
  ])),
2604
- workspaces: {}
2623
+ workspaces: {
2624
+ [DEFAULT_HOME_WORKSPACE_NAME]: { ...DEFAULT_HOME_WORKSPACE }
2625
+ }
2605
2626
  };
2606
2627
  }
2607
2628
  function isMissingFileError(error) {
@@ -2610,6 +2631,7 @@ function isMissingFileError(error) {
2610
2631
  var BUILTIN_DEFAULT_CONFIG_TEMPLATE;
2611
2632
  var init_ensure_config = __esm(() => {
2612
2633
  init_config_store();
2634
+ init_default_workspace();
2613
2635
  init_load_config();
2614
2636
  BUILTIN_DEFAULT_CONFIG_TEMPLATE = {
2615
2637
  transport: {
@@ -2638,7 +2660,9 @@ var init_ensure_config = __esm(() => {
2638
2660
  codex: { driver: "codex" },
2639
2661
  claude: { driver: "claude" }
2640
2662
  },
2641
- workspaces: {},
2663
+ workspaces: {
2664
+ [DEFAULT_HOME_WORKSPACE_NAME]: { ...DEFAULT_HOME_WORKSPACE }
2665
+ },
2642
2666
  plugins: []
2643
2667
  };
2644
2668
  });
@@ -9735,7 +9759,8 @@ function createEmptyState() {
9735
9759
  return {
9736
9760
  sessions: {},
9737
9761
  chat_contexts: {},
9738
- orchestration: createEmptyOrchestrationState()
9762
+ orchestration: createEmptyOrchestrationState(),
9763
+ scheduled_tasks: {}
9739
9764
  };
9740
9765
  }
9741
9766
  var init_types = () => {};
@@ -9831,7 +9856,7 @@ function isCoordinatorRouteContextRecord(value) {
9831
9856
  if (!isRecord2(value)) {
9832
9857
  return false;
9833
9858
  }
9834
- return isString(value.coordinatorSession) && isString(value.chatKey) && isOptionalString(value.accountId) && isOptionalString(value.replyContextToken) && isString(value.updatedAt);
9859
+ return isString(value.coordinatorSession) && isString(value.chatKey) && isOptionalString(value.sessionAlias) && isOptionalString(value.accountId) && isOptionalString(value.replyContextToken) && isOptionalString(value.channel) && (value.chatType === undefined || value.chatType === "direct" || value.chatType === "group") && isOptionalString(value.senderId) && isOptionalString(value.senderName) && isOptionalString(value.groupId) && isOptionalBoolean(value.isOwner) && isString(value.updatedAt);
9835
9860
  }
9836
9861
  function isHumanQuestionPackageMessageRecord(value) {
9837
9862
  if (!isRecord2(value)) {
@@ -9981,6 +10006,32 @@ function parseChatContexts(raw, path3) {
9981
10006
  }
9982
10007
  return chatContexts;
9983
10008
  }
10009
+ function isScheduledTaskStatus(value) {
10010
+ return value === "pending" || value === "triggering" || value === "executed" || value === "cancelled" || value === "missed" || value === "failed";
10011
+ }
10012
+ function isOptionalScheduledSessionMode(value) {
10013
+ return value === undefined || value === "temp" || value === "bound";
10014
+ }
10015
+ function isScheduledTaskRecord(value) {
10016
+ if (!isRecord2(value))
10017
+ return false;
10018
+ 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) && isOptionalScheduledSessionMode(value.session_mode) && isOptionalString(value.agent) && isOptionalString(value.workspace);
10019
+ }
10020
+ function parseScheduledTasks(raw, path3) {
10021
+ if (raw === undefined)
10022
+ return {};
10023
+ if (!isRecord2(raw)) {
10024
+ throw new Error(`state file "${path3}" must contain an object field "scheduled_tasks"`);
10025
+ }
10026
+ const tasks = {};
10027
+ for (const [id, value] of Object.entries(raw)) {
10028
+ if (!isScheduledTaskRecord(value) || value.id !== id) {
10029
+ throw new Error(`state file "${path3}" contains malformed scheduled task record "${id}"`);
10030
+ }
10031
+ tasks[id] = value;
10032
+ }
10033
+ return tasks;
10034
+ }
9984
10035
  function parseState(raw, path3) {
9985
10036
  if (!isRecord2(raw)) {
9986
10037
  throw new Error(`state file "${path3}" must contain a JSON object`);
@@ -9999,7 +10050,8 @@ function parseState(raw, path3) {
9999
10050
  return {
10000
10051
  sessions: parsedSessions,
10001
10052
  chat_contexts: parseChatContexts(chatContexts, path3),
10002
- orchestration
10053
+ orchestration,
10054
+ scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
10003
10055
  };
10004
10056
  }
10005
10057
  function validateExternalCoordinatorIdentityCollisions(sessions, orchestration, path3) {
@@ -10055,6 +10107,315 @@ var init_state_store = __esm(() => {
10055
10107
  init_types();
10056
10108
  });
10057
10109
 
10110
+ // src/channels/channel-scope.ts
10111
+ function registerKnownChannelId(channelId) {
10112
+ const normalized = channelId.trim();
10113
+ if (!normalized || normalized.includes(":")) {
10114
+ throw new Error("channel id must be non-empty and must not contain ':'");
10115
+ }
10116
+ KNOWN_CHANNEL_IDS.add(normalized);
10117
+ }
10118
+ function listKnownChannelIds() {
10119
+ return Array.from(KNOWN_CHANNEL_IDS);
10120
+ }
10121
+ function getChannelIdFromChatKey(chatKey) {
10122
+ const first = chatKey.split(":", 1)[0];
10123
+ return first && KNOWN_CHANNEL_IDS.has(first) ? first : "weixin";
10124
+ }
10125
+ function toInternalSessionAlias(channelId, displayAlias) {
10126
+ const normalized = displayAlias.trim();
10127
+ if (normalized.length === 0) {
10128
+ throw new Error("display session alias must be non-empty");
10129
+ }
10130
+ if (normalized.startsWith(`${channelId}:`)) {
10131
+ return normalized;
10132
+ }
10133
+ return `${channelId}:${normalized}`;
10134
+ }
10135
+ function toDisplaySessionAlias(internalAlias) {
10136
+ const [first, ...rest] = internalAlias.split(":");
10137
+ if (first && KNOWN_CHANNEL_IDS.has(first) && rest.length > 0) {
10138
+ return rest.join(":");
10139
+ }
10140
+ return internalAlias;
10141
+ }
10142
+ function isSessionAliasVisibleInChannel(alias, channelId) {
10143
+ const [first] = alias.split(":", 1);
10144
+ if (first && KNOWN_CHANNEL_IDS.has(first)) {
10145
+ return first === channelId;
10146
+ }
10147
+ return channelId === "weixin";
10148
+ }
10149
+ function resolveSessionAliasForInput(channelId, displayAlias, existingAliases) {
10150
+ const normalized = displayAlias.trim();
10151
+ if (normalized.length === 0) {
10152
+ throw new Error("display session alias must be non-empty");
10153
+ }
10154
+ if (normalized.startsWith(`${channelId}:`)) {
10155
+ return normalized;
10156
+ }
10157
+ const scopedAlias = toInternalSessionAlias(channelId, normalized);
10158
+ for (const alias of existingAliases) {
10159
+ if (alias === scopedAlias)
10160
+ return scopedAlias;
10161
+ }
10162
+ if (channelId === "weixin") {
10163
+ for (const alias of existingAliases) {
10164
+ if (alias === normalized)
10165
+ return alias;
10166
+ }
10167
+ }
10168
+ return scopedAlias;
10169
+ }
10170
+ function buildDefaultTransportSession(channelId, displayAlias) {
10171
+ const normalized = displayAlias.trim();
10172
+ if (normalized.length === 0) {
10173
+ throw new Error("display session alias must be non-empty");
10174
+ }
10175
+ return channelId === "weixin" ? normalized : toInternalSessionAlias(channelId, normalized);
10176
+ }
10177
+ var KNOWN_CHANNEL_IDS;
10178
+ var init_channel_scope = __esm(() => {
10179
+ KNOWN_CHANNEL_IDS = new Set(["weixin"]);
10180
+ });
10181
+
10182
+ // src/scheduled/scheduled-types.ts
10183
+ var LATER_MIN_DELAY_MS = 1e4, LATER_MAX_DELAY_MS, LATER_MESSAGE_PREVIEW_CHARS = 120;
10184
+ var init_scheduled_types = __esm(() => {
10185
+ LATER_MAX_DELAY_MS = 7 * 24 * 60 * 60 * 1000;
10186
+ });
10187
+
10188
+ // src/scheduled/scheduled-render.ts
10189
+ function sessionLabel(task, displaySession) {
10190
+ if (task.session_mode === "temp") {
10191
+ return `临时会话(${task.workspace ?? "?"} · ${task.agent ?? "?"})`;
10192
+ }
10193
+ return `会话:${displaySession}`;
10194
+ }
10195
+ function renderLaterHelp() {
10196
+ return [
10197
+ "定时任务用法:",
10198
+ "",
10199
+ "创建:",
10200
+ "/lt in 2h 检查 CI",
10201
+ "/lt 30分钟后 总结进展",
10202
+ "/lt tomorrow 09:00 看 PR",
10203
+ "/lt 周五 09:00 继续处理",
10204
+ "",
10205
+ "查看:",
10206
+ "/lt list",
10207
+ "",
10208
+ "取消:",
10209
+ "/lt cancel <id>",
10210
+ "",
10211
+ "说明:",
10212
+ "- 只支持一次性任务",
10213
+ "- 时间必须在 10 秒之后、7 天之内",
10214
+ "- 默认在为本次任务新建的临时会话里执行(跑完即销毁)",
10215
+ "- 加 --bind 改为发送到创建时绑定的当前会话",
10216
+ "- 触发通知和 agent 回复复用现有频道路由;微信回复额度由现有路由控制",
10217
+ "- 不支持延迟执行 / 开头的 weacpx 命令",
10218
+ "- 完整时间格式与说明见 docs/later-command.md"
10219
+ ].join(`
10220
+ `);
10221
+ }
10222
+ function renderLaterUnsupportedChannel() {
10223
+ return [
10224
+ "当前频道暂不支持定时任务,未创建任务。",
10225
+ "",
10226
+ "原因:这个频道还没有实现定时消息投递能力,任务到点后无法把结果发回原聊天。",
10227
+ "请切换到支持定时任务的频道后再使用 /lt。"
10228
+ ].join(`
10229
+ `);
10230
+ }
10231
+ function renderTaskCreated(task, displaySession) {
10232
+ return [
10233
+ `已创建定时任务 #${task.id}`,
10234
+ `执行时间:${formatLocalDateTime(new Date(task.execute_at))}`,
10235
+ sessionLabel(task, displaySession),
10236
+ `内容:${preview(task.message)}`
10237
+ ].join(`
10238
+ `);
10239
+ }
10240
+ function renderLaterList(tasks, displaySession) {
10241
+ if (tasks.length === 0)
10242
+ return "当前没有待执行定时任务。";
10243
+ return [
10244
+ "待执行定时任务:",
10245
+ "",
10246
+ ...tasks.flatMap((task) => [
10247
+ `#${task.id} ${formatLocalDateTime(new Date(task.execute_at))} ${sessionLabel(task, displaySession(task.session_alias))}`,
10248
+ preview(task.message),
10249
+ ""
10250
+ ])
10251
+ ].join(`
10252
+ `).trimEnd();
10253
+ }
10254
+ function preview(text) {
10255
+ return text.length <= LATER_MESSAGE_PREVIEW_CHARS ? text : `${text.slice(0, LATER_MESSAGE_PREVIEW_CHARS - 1)}…`;
10256
+ }
10257
+ function formatLocalDateTime(date4) {
10258
+ const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
10259
+ const pad = (value) => String(value).padStart(2, "0");
10260
+ return `${date4.getFullYear()}-${pad(date4.getMonth() + 1)}-${pad(date4.getDate())} ${weekdays[date4.getDay()]} ${pad(date4.getHours())}:${pad(date4.getMinutes())}`;
10261
+ }
10262
+ var init_scheduled_render = __esm(() => {
10263
+ init_scheduled_types();
10264
+ });
10265
+
10266
+ // src/orchestration/async-mutex.ts
10267
+ class AsyncMutex {
10268
+ tail = Promise.resolve();
10269
+ async run(critical) {
10270
+ const previous = this.tail;
10271
+ let release;
10272
+ this.tail = new Promise((resolve) => {
10273
+ release = resolve;
10274
+ });
10275
+ await previous;
10276
+ try {
10277
+ return await critical();
10278
+ } finally {
10279
+ release();
10280
+ }
10281
+ }
10282
+ }
10283
+
10284
+ // src/scheduled/scheduled-service.ts
10285
+ class ScheduledTaskService {
10286
+ state;
10287
+ stateStore;
10288
+ now;
10289
+ generateId;
10290
+ stateMutex;
10291
+ claimedInThisSession = new Set;
10292
+ constructor(state, stateStore, options) {
10293
+ this.state = state;
10294
+ this.stateStore = stateStore;
10295
+ this.now = options?.now ?? (() => new Date);
10296
+ this.generateId = options?.generateId ?? (() => Math.random().toString(36).slice(2, 6));
10297
+ this.stateMutex = options?.stateMutex ?? new AsyncMutex;
10298
+ }
10299
+ async createTask(input) {
10300
+ return await this.mutate(async () => {
10301
+ const id = this.nextId();
10302
+ const task = {
10303
+ id,
10304
+ chat_key: input.chatKey,
10305
+ session_alias: input.sessionAlias,
10306
+ execute_at: input.executeAt.toISOString(),
10307
+ message: input.message,
10308
+ status: "pending",
10309
+ created_at: this.now().toISOString(),
10310
+ ...input.sessionMode ? { session_mode: input.sessionMode } : {},
10311
+ ...input.agent ? { agent: input.agent } : {},
10312
+ ...input.workspace ? { workspace: input.workspace } : {},
10313
+ ...input.accountId ? { account_id: input.accountId } : {},
10314
+ ...input.replyContextToken ? { reply_context_token: input.replyContextToken } : {},
10315
+ ...input.sourceLabel ? { source_label: input.sourceLabel } : {}
10316
+ };
10317
+ this.state.scheduled_tasks[id] = task;
10318
+ await this.save();
10319
+ return task;
10320
+ });
10321
+ }
10322
+ listPending() {
10323
+ return Object.values(this.state.scheduled_tasks).filter((task) => task.status === "pending").sort((left, right) => left.execute_at.localeCompare(right.execute_at));
10324
+ }
10325
+ async cancelPending(inputId) {
10326
+ return await this.mutate(async () => {
10327
+ const id = normalizeId(inputId);
10328
+ const task = this.state.scheduled_tasks[id];
10329
+ if (!task || task.status !== "pending")
10330
+ return false;
10331
+ task.status = "cancelled";
10332
+ task.cancelled_at = this.now().toISOString();
10333
+ await this.save();
10334
+ return true;
10335
+ });
10336
+ }
10337
+ async markStartupMissed() {
10338
+ await this.mutate(async () => {
10339
+ const nowMs = this.now().getTime();
10340
+ let changed = false;
10341
+ for (const task of Object.values(this.state.scheduled_tasks)) {
10342
+ if (task.status === "pending" && Date.parse(task.execute_at) < nowMs) {
10343
+ task.status = "missed";
10344
+ task.missed_at = this.now().toISOString();
10345
+ changed = true;
10346
+ }
10347
+ if (task.status === "triggering" && !this.claimedInThisSession.has(task.id)) {
10348
+ task.status = "failed";
10349
+ task.failed_at = this.now().toISOString();
10350
+ task.last_error = "process stopped while task was triggering";
10351
+ changed = true;
10352
+ }
10353
+ }
10354
+ if (changed)
10355
+ await this.save();
10356
+ });
10357
+ }
10358
+ async claimDueTasks() {
10359
+ return await this.mutate(async () => {
10360
+ const nowMs = this.now().getTime();
10361
+ const due = this.listPending().filter((task) => Date.parse(task.execute_at) <= nowMs);
10362
+ if (due.length === 0)
10363
+ return [];
10364
+ const at = this.now().toISOString();
10365
+ for (const task of due) {
10366
+ task.status = "triggering";
10367
+ task.triggered_at = at;
10368
+ this.claimedInThisSession.add(task.id);
10369
+ }
10370
+ await this.save();
10371
+ return due.map((task) => ({ ...task }));
10372
+ });
10373
+ }
10374
+ async markExecuted(id) {
10375
+ await this.mutate(async () => {
10376
+ const taskId = normalizeId(id);
10377
+ const task = this.state.scheduled_tasks[taskId];
10378
+ if (!task)
10379
+ return;
10380
+ task.status = "executed";
10381
+ task.executed_at = this.now().toISOString();
10382
+ this.claimedInThisSession.delete(taskId);
10383
+ await this.save();
10384
+ });
10385
+ }
10386
+ async markFailed(id, error2) {
10387
+ await this.mutate(async () => {
10388
+ const taskId = normalizeId(id);
10389
+ const task = this.state.scheduled_tasks[taskId];
10390
+ if (!task)
10391
+ return;
10392
+ task.status = "failed";
10393
+ task.failed_at = this.now().toISOString();
10394
+ task.last_error = error2 instanceof Error ? error2.message : String(error2);
10395
+ this.claimedInThisSession.delete(taskId);
10396
+ await this.save();
10397
+ });
10398
+ }
10399
+ nextId() {
10400
+ for (let attempt = 0;attempt < 20; attempt += 1) {
10401
+ const id = normalizeId(this.generateId()).replace(/[^0-9a-z]/g, "").slice(0, 6);
10402
+ if (id.length >= 4 && !this.state.scheduled_tasks[id])
10403
+ return id;
10404
+ }
10405
+ throw new Error("failed to generate unique scheduled task id");
10406
+ }
10407
+ async mutate(critical) {
10408
+ return await this.stateMutex.run(critical);
10409
+ }
10410
+ async save() {
10411
+ await this.stateStore.save(this.state);
10412
+ }
10413
+ }
10414
+ function normalizeId(input) {
10415
+ return input.trim().replace(/^#/, "").toLowerCase();
10416
+ }
10417
+ var init_scheduled_service = () => {};
10418
+
10058
10419
  // src/plugins/plugin-home.ts
10059
10420
  import { mkdir as mkdir6, writeFile as writeFile4 } from "node:fs/promises";
10060
10421
  import { homedir as homedir3 } from "node:os";
@@ -15442,6 +15803,181 @@ var init_deliver_coordinator_message = __esm(() => {
15442
15803
  init_send();
15443
15804
  });
15444
15805
 
15806
+ // src/weixin/messaging/scheduled-turn.ts
15807
+ async function executeScheduledTurn(input, deps) {
15808
+ const userId = normalizeWeixinUserIdFromChatKey(input.chatKey);
15809
+ const quotaKey = userId;
15810
+ const sendMessage2 = deps.sendMessage ?? sendMessageWeixin;
15811
+ const candidateAccountIds = input.accountId ? [input.accountId] : deps.listAccountIds();
15812
+ if (candidateAccountIds.length === 0) {
15813
+ throw new Error(`no weixin account is available for scheduled message on chatKey: ${input.chatKey}`);
15814
+ }
15815
+ let noticeSent = false;
15816
+ let lastNoticeError;
15817
+ let deliveryAccountId;
15818
+ let deliveryContextToken;
15819
+ let deliverableAccountId;
15820
+ let deliverableContextToken;
15821
+ const resolveContextToken = (candidateAccountId) => deps.getContextToken(candidateAccountId, userId) ?? (candidateAccountId === input.accountId ? input.replyContextToken : undefined);
15822
+ for (const candidateAccountId of candidateAccountIds) {
15823
+ const contextToken = resolveContextToken(candidateAccountId);
15824
+ if (!contextToken)
15825
+ continue;
15826
+ const account = deps.resolveAccount(candidateAccountId);
15827
+ if (!account.token)
15828
+ continue;
15829
+ if (!deliverableAccountId) {
15830
+ deliverableAccountId = candidateAccountId;
15831
+ deliverableContextToken = contextToken;
15832
+ }
15833
+ try {
15834
+ if (!deps.reserveMidSegment(quotaKey)) {
15835
+ throw new Error("mid segment quota exhausted");
15836
+ }
15837
+ await sendMessage2({
15838
+ to: userId,
15839
+ text: input.noticeText,
15840
+ opts: { baseUrl: account.baseUrl, token: account.token, contextToken }
15841
+ });
15842
+ noticeSent = true;
15843
+ deliveryAccountId = candidateAccountId;
15844
+ deliveryContextToken = contextToken;
15845
+ break;
15846
+ } catch (error2) {
15847
+ lastNoticeError = error2;
15848
+ await deps.logger.error("scheduled.notice_send_failed", "failed to send scheduled notice", { chatKey: input.chatKey, accountId: candidateAccountId, error: String(error2) });
15849
+ }
15850
+ }
15851
+ if (!noticeSent) {
15852
+ if (!deliverableAccountId || !deliverableContextToken) {
15853
+ const message = lastNoticeError instanceof Error ? lastNoticeError.message : `no deliverable weixin context for scheduled message on chatKey: ${input.chatKey}`;
15854
+ throw new Error(message);
15855
+ }
15856
+ deliveryAccountId = deliverableAccountId;
15857
+ deliveryContextToken = deliverableContextToken;
15858
+ await deps.logger.info("scheduled.notice_skipped", "scheduled trigger notice was not delivered; proceeding with agent turn", {
15859
+ chatKey: input.chatKey,
15860
+ accountId: deliveryAccountId,
15861
+ reason: lastNoticeError instanceof Error ? lastNoticeError.message : "notice_undelivered"
15862
+ });
15863
+ }
15864
+ const sendReplySegment = async (text) => {
15865
+ const plainText = markdownToPlainText(text).trim();
15866
+ if (plainText.length === 0)
15867
+ return false;
15868
+ return await sendTextViaAvailableAccount(plainText, "scheduled.mid_send_failed");
15869
+ };
15870
+ const sendReservedMidText = async (text) => {
15871
+ const plainText = markdownToPlainText(text).trim();
15872
+ if (plainText.length === 0)
15873
+ return false;
15874
+ if (!deps.reserveMidSegment(quotaKey)) {
15875
+ await deps.logger.info("scheduled.mid_dropped", "scheduled turn intermediate response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted" });
15876
+ return false;
15877
+ }
15878
+ return await sendTextViaAvailableAccount(plainText, "scheduled.mid_send_failed");
15879
+ };
15880
+ const resolvedAccountId = deliveryAccountId ?? input.accountId ?? candidateAccountIds[0];
15881
+ let turn;
15882
+ try {
15883
+ turn = await executeChatTurn({
15884
+ agent: deps.agent,
15885
+ request: {
15886
+ accountId: resolvedAccountId,
15887
+ conversationId: input.chatKey,
15888
+ text: input.promptText,
15889
+ ...deliveryContextToken ? { replyContextToken: deliveryContextToken } : {},
15890
+ ...input.abortSignal ? { abortSignal: input.abortSignal } : {},
15891
+ metadata: {
15892
+ channel: "weixin",
15893
+ scheduledSessionAlias: input.sessionAlias,
15894
+ ...input.sessionDescriptor ? { scheduledSessionDescriptor: input.sessionDescriptor } : {}
15895
+ }
15896
+ },
15897
+ onReplySegment: sendReplySegment
15898
+ });
15899
+ } catch (error2) {
15900
+ await sendReservedMidText(`定时任务执行失败:${error2 instanceof Error ? error2.message : String(error2)}`).catch(() => false);
15901
+ throw error2;
15902
+ }
15903
+ if (turn.text) {
15904
+ const finalText = markdownToPlainText(turn.text).trim();
15905
+ if (finalText.length > 0) {
15906
+ await sendFinalText(finalText);
15907
+ }
15908
+ }
15909
+ async function sendFinalText(finalText) {
15910
+ const rawChunks = chunkFinalText(finalText, 1800);
15911
+ if (rawChunks.length === 0)
15912
+ return;
15913
+ const total = rawChunks.length;
15914
+ const chunks = total === 1 ? rawChunks : rawChunks.map((body, index) => `(${index + 1}/${total}) ${body}`);
15915
+ const available = total === 1 ? 1 : Math.max(Math.min(deps.finalRemaining?.(quotaKey) ?? total, total), 0);
15916
+ const wave = chunks.slice(0, available);
15917
+ if (wave.length > 0 && wave.length < total) {
15918
+ wave[wave.length - 1] = `${wave[wave.length - 1]}
15919
+
15920
+ ${buildFinalHeadsUp({
15921
+ total,
15922
+ sentSoFar: wave.length
15923
+ })}`;
15924
+ }
15925
+ let sent = 0;
15926
+ for (let index = 0;index < wave.length; index += 1) {
15927
+ if (!deps.reserveFinal(quotaKey)) {
15928
+ 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 });
15929
+ break;
15930
+ }
15931
+ const delivered = await sendTextViaAvailableAccount(wave[index], "scheduled.final_send_failed");
15932
+ if (!delivered)
15933
+ break;
15934
+ sent += 1;
15935
+ }
15936
+ const restToPark = chunks.slice(sent);
15937
+ if (total > 1 && restToPark.length > 0 && deps.enqueuePendingFinal) {
15938
+ const pending = restToPark.map((text, index) => {
15939
+ const entry = { text, seq: sent + index + 1, total };
15940
+ if (deliveryContextToken)
15941
+ entry.contextToken = deliveryContextToken;
15942
+ if (deliveryAccountId)
15943
+ entry.accountId = deliveryAccountId;
15944
+ return entry;
15945
+ });
15946
+ deps.enqueuePendingFinal(quotaKey, pending);
15947
+ }
15948
+ }
15949
+ async function sendTextViaAvailableAccount(text, errorEvent) {
15950
+ const orderedAccountIds = [
15951
+ ...deliveryAccountId ? [deliveryAccountId] : [],
15952
+ ...candidateAccountIds.filter((accountId) => accountId !== deliveryAccountId)
15953
+ ];
15954
+ for (const candidateAccountId of orderedAccountIds) {
15955
+ const contextToken = candidateAccountId === deliveryAccountId && deliveryContextToken ? deliveryContextToken : resolveContextToken(candidateAccountId);
15956
+ if (!contextToken)
15957
+ continue;
15958
+ const account = deps.resolveAccount(candidateAccountId);
15959
+ if (!account.token)
15960
+ continue;
15961
+ try {
15962
+ await sendMessage2({
15963
+ to: userId,
15964
+ text,
15965
+ opts: { baseUrl: account.baseUrl, token: account.token, contextToken }
15966
+ });
15967
+ return true;
15968
+ } catch (error2) {
15969
+ await deps.logger.error(errorEvent, "failed to send scheduled response text", { chatKey: input.chatKey, accountId: candidateAccountId, error: String(error2) });
15970
+ }
15971
+ }
15972
+ return false;
15973
+ }
15974
+ }
15975
+ var init_scheduled_turn = __esm(() => {
15976
+ init_handle_weixin_message_turn();
15977
+ init_send();
15978
+ init_inbound();
15979
+ });
15980
+
15445
15981
  // src/weixin/monitor/consumer-lock.ts
15446
15982
  import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
15447
15983
  import { dirname as dirname8, join as join5 } from "node:path";
@@ -15568,6 +16104,7 @@ var init_consumer_lock = __esm(() => {
15568
16104
  // src/channels/weixin-channel.ts
15569
16105
  class WeixinChannel {
15570
16106
  id = "weixin";
16107
+ agent = null;
15571
16108
  quota = null;
15572
16109
  logger = null;
15573
16110
  markDelivered = null;
@@ -15598,6 +16135,7 @@ class WeixinChannel {
15598
16135
  this.markFailed = callbacks.markTaskNoticeFailed;
15599
16136
  }
15600
16137
  async start(input) {
16138
+ this.agent = input.agent;
15601
16139
  this.quota = input.quota;
15602
16140
  this.logger = input.logger;
15603
16141
  if (!this.isLoggedIn()) {
@@ -15658,6 +16196,23 @@ class WeixinChannel {
15658
16196
  logger: this.logger
15659
16197
  });
15660
16198
  }
16199
+ async sendScheduledMessage(input) {
16200
+ if (!this.agent || !this.quota || !this.logger) {
16201
+ throw new Error("WeixinChannel.start() must be called before scheduled message delivery");
16202
+ }
16203
+ await executeScheduledTurn(input, {
16204
+ agent: this.agent,
16205
+ listAccountIds: () => listWeixinAccountIds(),
16206
+ resolveAccount: (accountId) => resolveWeixinAccount(accountId),
16207
+ getContextToken: (accountId, userId) => getContextToken(accountId, userId),
16208
+ reserveMidSegment: (chatKey) => this.quota.reserveMidSegment(chatKey),
16209
+ reserveFinal: (chatKey) => this.quota.reserveFinal(chatKey),
16210
+ finalRemaining: (chatKey) => this.quota.finalRemaining(chatKey),
16211
+ enqueuePendingFinal: (chatKey, chunks) => this.quota.enqueuePendingFinal(chatKey, chunks),
16212
+ sendMessage: sendMessageWeixin,
16213
+ logger: this.logger
16214
+ });
16215
+ }
15661
16216
  }
15662
16217
  var init_weixin_channel = __esm(() => {
15663
16218
  init_weixin();
@@ -15665,81 +16220,10 @@ var init_weixin_channel = __esm(() => {
15665
16220
  init_deliver_orchestration_task_notice();
15666
16221
  init_deliver_orchestration_task_progress();
15667
16222
  init_deliver_coordinator_message();
16223
+ init_scheduled_turn();
15668
16224
  init_consumer_lock();
15669
16225
  });
15670
16226
 
15671
- // src/channels/channel-scope.ts
15672
- function registerKnownChannelId(channelId) {
15673
- const normalized = channelId.trim();
15674
- if (!normalized || normalized.includes(":")) {
15675
- throw new Error("channel id must be non-empty and must not contain ':'");
15676
- }
15677
- KNOWN_CHANNEL_IDS.add(normalized);
15678
- }
15679
- function listKnownChannelIds() {
15680
- return Array.from(KNOWN_CHANNEL_IDS);
15681
- }
15682
- function getChannelIdFromChatKey(chatKey) {
15683
- const first = chatKey.split(":", 1)[0];
15684
- return first && KNOWN_CHANNEL_IDS.has(first) ? first : "weixin";
15685
- }
15686
- function toInternalSessionAlias(channelId, displayAlias) {
15687
- const normalized = displayAlias.trim();
15688
- if (normalized.length === 0) {
15689
- throw new Error("display session alias must be non-empty");
15690
- }
15691
- if (normalized.startsWith(`${channelId}:`)) {
15692
- return normalized;
15693
- }
15694
- return `${channelId}:${normalized}`;
15695
- }
15696
- function toDisplaySessionAlias(internalAlias) {
15697
- const [first, ...rest] = internalAlias.split(":");
15698
- if (first && KNOWN_CHANNEL_IDS.has(first) && rest.length > 0) {
15699
- return rest.join(":");
15700
- }
15701
- return internalAlias;
15702
- }
15703
- function isSessionAliasVisibleInChannel(alias, channelId) {
15704
- const [first] = alias.split(":", 1);
15705
- if (first && KNOWN_CHANNEL_IDS.has(first)) {
15706
- return first === channelId;
15707
- }
15708
- return channelId === "weixin";
15709
- }
15710
- function resolveSessionAliasForInput(channelId, displayAlias, existingAliases) {
15711
- const normalized = displayAlias.trim();
15712
- if (normalized.length === 0) {
15713
- throw new Error("display session alias must be non-empty");
15714
- }
15715
- if (normalized.startsWith(`${channelId}:`)) {
15716
- return normalized;
15717
- }
15718
- const scopedAlias = toInternalSessionAlias(channelId, normalized);
15719
- for (const alias of existingAliases) {
15720
- if (alias === scopedAlias)
15721
- return scopedAlias;
15722
- }
15723
- if (channelId === "weixin") {
15724
- for (const alias of existingAliases) {
15725
- if (alias === normalized)
15726
- return alias;
15727
- }
15728
- }
15729
- return scopedAlias;
15730
- }
15731
- function buildDefaultTransportSession(channelId, displayAlias) {
15732
- const normalized = displayAlias.trim();
15733
- if (normalized.length === 0) {
15734
- throw new Error("display session alias must be non-empty");
15735
- }
15736
- return channelId === "weixin" ? normalized : toInternalSessionAlias(channelId, normalized);
15737
- }
15738
- var KNOWN_CHANNEL_IDS;
15739
- var init_channel_scope = __esm(() => {
15740
- KNOWN_CHANNEL_IDS = new Set(["weixin"]);
15741
- });
15742
-
15743
16227
  // src/plugins/known-plugins.ts
15744
16228
  function listKnownPlugins() {
15745
16229
  return KNOWN_PLUGINS.map((plugin) => ({ ...plugin, channels: [...plugin.channels] }));
@@ -16049,7 +16533,7 @@ function validatePluginCompatibility(metadata, context) {
16049
16533
  }
16050
16534
  }
16051
16535
  }
16052
- var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.4.0", SEMVER_RE;
16536
+ var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.5.0", SEMVER_RE;
16053
16537
  var init_compatibility = __esm(() => {
16054
16538
  WEACPX_PLUGIN_API_SUPPORTED_VERSIONS = [1];
16055
16539
  SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
@@ -16435,7 +16919,9 @@ var init_command_list = __esm(() => {
16435
16919
  "/dg",
16436
16920
  "/group",
16437
16921
  "/groups",
16438
- "/task"
16922
+ "/task",
16923
+ "/later",
16924
+ "/lt"
16439
16925
  ];
16440
16926
  KNOWN_COMMAND_PREFIX_SET = new Set(WEACPX_KNOWN_COMMAND_PREFIXES);
16441
16927
  });
@@ -16603,6 +17089,16 @@ function parseCommand(input) {
16603
17089
  } else if (command === "/task" && parts[1] && parts.length === 2) {
16604
17090
  return { kind: "task.get", taskId: parts[1] };
16605
17091
  }
17092
+ if (command === "/later") {
17093
+ if (parts.length === 1)
17094
+ return { kind: "later.help" };
17095
+ if (parts[1] === "list" && parts.length === 2)
17096
+ return { kind: "later.list" };
17097
+ if (parts[1] === "cancel" && parts[2] && parts.length === 3) {
17098
+ return { kind: "later.cancel", id: parts[2] };
17099
+ }
17100
+ return { kind: "later.create", tokens: parts.slice(1) };
17101
+ }
16606
17102
  if (command === "/workspace" && parts[1] === "new" && parts[2]) {
16607
17103
  const name = parts[2];
16608
17104
  let cwd = "";
@@ -16771,6 +17267,8 @@ function normalizeCommand(command) {
16771
17267
  return "/permission";
16772
17268
  if (command === "/stop")
16773
17269
  return "/cancel";
17270
+ if (command === "/lt")
17271
+ return "/later";
16774
17272
  return command;
16775
17273
  }
16776
17274
  function isRecognizedCommand(command) {
@@ -16966,6 +17464,7 @@ var init_command_policy = __esm(() => {
16966
17464
  "group.get",
16967
17465
  "tasks",
16968
17466
  "task.get",
17467
+ "later.help",
16969
17468
  "invalid",
16970
17469
  "prompt"
16971
17470
  ]);
@@ -16995,7 +17494,10 @@ var init_command_policy = __esm(() => {
16995
17494
  "session.new": "/session new",
16996
17495
  "session.shortcut": "/session",
16997
17496
  "session.shortcut.new": "/session",
16998
- "session.attach": "/session attach"
17497
+ "session.attach": "/session attach",
17498
+ "later.create": "/later",
17499
+ "later.list": "/later list",
17500
+ "later.cancel": "/later cancel"
16999
17501
  };
17000
17502
  });
17001
17503
 
@@ -17904,7 +18406,7 @@ async function handleSessionRemove(context, chatKey, alias) {
17904
18406
  return { text: lines.join(`
17905
18407
  `) };
17906
18408
  }
17907
- async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
18409
+ async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata) {
17908
18410
  const effectiveReplyMode = session.replyMode ?? context.config?.channel.replyMode ?? "verbose";
17909
18411
  if (!session.replyMode)
17910
18412
  session.replyMode = effectiveReplyMode;
@@ -17914,8 +18416,10 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17914
18416
  await context.orchestration.recordCoordinatorRouteContext?.({
17915
18417
  coordinatorSession: session.transportSession,
17916
18418
  chatKey,
18419
+ sessionAlias: session.alias,
17917
18420
  ...replyContextToken ? { replyContextToken } : {},
17918
- ...accountId ? { accountId } : {}
18421
+ ...accountId ? { accountId } : {},
18422
+ ...toCoordinatorRouteChatMetadata(metadata)
17919
18423
  });
17920
18424
  } catch (error2) {
17921
18425
  await context.logger.error("orchestration.coordinator_route_context.record_failed", "failed to record coordinator route context", {
@@ -17924,6 +18428,13 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17924
18428
  chatKey,
17925
18429
  error: error2 instanceof Error ? error2.message : String(error2)
17926
18430
  });
18431
+ return {
18432
+ text: [
18433
+ "无法记录当前会话路由,已取消本次发送。",
18434
+ "请稍后重试;如果问题持续存在,请检查 weacpx 运行日志和 state.json 写入权限。"
18435
+ ].join(`
18436
+ `)
18437
+ };
17927
18438
  }
17928
18439
  }
17929
18440
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId);
@@ -17949,21 +18460,37 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
17949
18460
  throw error2;
17950
18461
  }
17951
18462
  }
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
- }
18463
+ async function handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata) {
17957
18464
  try {
17958
- return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
18465
+ return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata);
17959
18466
  } catch (error2) {
17960
18467
  const recovered = await context.recovery.tryRecoverMissingSession(session, error2);
17961
18468
  if (recovered) {
17962
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
18469
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata);
17963
18470
  }
17964
18471
  return context.recovery.renderTransportError(session, error2);
17965
18472
  }
17966
18473
  }
18474
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata) {
18475
+ const session = await context.sessions.getCurrentSession(chatKey);
18476
+ if (!session) {
18477
+ return { text: NO_CURRENT_SESSION_TEXT };
18478
+ }
18479
+ return await handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata);
18480
+ }
18481
+ function toCoordinatorRouteChatMetadata(metadata) {
18482
+ if (!metadata) {
18483
+ return {};
18484
+ }
18485
+ return {
18486
+ ...metadata.channel ? { channel: metadata.channel } : {},
18487
+ ...metadata.chatType ? { chatType: metadata.chatType } : {},
18488
+ ...metadata.senderId ? { senderId: metadata.senderId } : {},
18489
+ ...metadata.senderName ? { senderName: metadata.senderName } : {},
18490
+ ...metadata.groupId ? { groupId: metadata.groupId } : {},
18491
+ ...metadata.isOwner !== undefined ? { isOwner: metadata.isOwner } : {}
18492
+ };
18493
+ }
17967
18494
  async function preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId) {
17968
18495
  const orchestration = context.orchestration;
17969
18496
  if (!orchestration) {
@@ -18725,6 +19252,273 @@ var init_workspace_handler = __esm(() => {
18725
19252
  };
18726
19253
  });
18727
19254
 
19255
+ // src/scheduled/parse-later-time.ts
19256
+ function parseLaterTime(tokens, now = new Date) {
19257
+ if (tokens.length === 0)
19258
+ return { ok: false, code: "missing_time" };
19259
+ const relative = parseRelative(tokens, now);
19260
+ if (relative)
19261
+ return validateResult(relative.executeAt, relative.messageStartIndex, tokens, now);
19262
+ const absolute = parseAbsolute(tokens, now);
19263
+ if (absolute)
19264
+ return validateResult(absolute.executeAt, absolute.messageStartIndex, tokens, now, absolute.pastTodayValue);
19265
+ return { ok: false, code: "unrecognized_time" };
19266
+ }
19267
+ function parseRelative(tokens, now) {
19268
+ if (tokens[0] === "in" && tokens[1]) {
19269
+ const ms = parseDuration(tokens[1]);
19270
+ if (ms !== null)
19271
+ return { executeAt: new Date(now.getTime() + ms), messageStartIndex: 2 };
19272
+ }
19273
+ const zh = /^(\d+)(分钟|小时|天)后$/.exec(tokens[0] ?? "");
19274
+ if (zh) {
19275
+ const amount = Number(zh[1]);
19276
+ const unit = zh[2];
19277
+ const ms = unit === "分钟" ? amount * 60000 : unit === "小时" ? amount * 3600000 : amount * 86400000;
19278
+ return { executeAt: new Date(now.getTime() + ms), messageStartIndex: 1 };
19279
+ }
19280
+ return null;
19281
+ }
19282
+ function parseDuration(value) {
19283
+ const match = /^(\d+)(m|min|minute|minutes|h|hour|hours|d|day|days)$/.exec(value.toLowerCase());
19284
+ if (!match)
19285
+ return null;
19286
+ const amount = Number(match[1]);
19287
+ const unit = match[2];
19288
+ if (unit === "m" || unit === "min" || unit === "minute" || unit === "minutes")
19289
+ return amount * 60000;
19290
+ if (unit === "h" || unit === "hour" || unit === "hours")
19291
+ return amount * 3600000;
19292
+ return amount * 86400000;
19293
+ }
19294
+ function parseAbsolute(tokens, now) {
19295
+ if (tokens[0] === "at" && tokens[1]) {
19296
+ const parsed = parseClock(tokens[1]);
19297
+ if (!parsed)
19298
+ return null;
19299
+ const executeAt = atLocalDate(now, 0, parsed.hour, parsed.minute);
19300
+ if (executeAt.getTime() <= now.getTime())
19301
+ return { executeAt, messageStartIndex: 2, pastTodayValue: tokens[1] };
19302
+ return { executeAt, messageStartIndex: 2 };
19303
+ }
19304
+ const dayWord = tokens[0]?.toLowerCase();
19305
+ const dayOffset = dayWord === "today" || dayWord === "今天" ? 0 : dayWord === "tomorrow" || dayWord === "明天" ? 1 : dayWord === "后天" ? 2 : null;
19306
+ if (dayOffset !== null && tokens[1]) {
19307
+ const parsed = parseClock(tokens[1]);
19308
+ if (!parsed)
19309
+ return null;
19310
+ const executeAt = atLocalDate(now, dayOffset, parsed.hour, parsed.minute);
19311
+ if (dayOffset === 0 && executeAt.getTime() <= now.getTime())
19312
+ return { executeAt, messageStartIndex: 2, pastTodayValue: tokens[1] };
19313
+ return { executeAt, messageStartIndex: 2 };
19314
+ }
19315
+ const weekday = WEEKDAYS.get(tokens[0]?.toLowerCase() ?? "");
19316
+ if (weekday !== undefined && tokens[1]) {
19317
+ const parsed = parseClock(tokens[1]);
19318
+ if (!parsed)
19319
+ return null;
19320
+ let days = (weekday - now.getDay() + 7) % 7;
19321
+ let executeAt = atLocalDate(now, days, parsed.hour, parsed.minute);
19322
+ if (days === 0 && executeAt.getTime() <= now.getTime()) {
19323
+ days = 7;
19324
+ executeAt = atLocalDate(now, days, parsed.hour, parsed.minute);
19325
+ }
19326
+ return { executeAt, messageStartIndex: 2 };
19327
+ }
19328
+ return null;
19329
+ }
19330
+ function parseClock(value) {
19331
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value);
19332
+ if (!match)
19333
+ return null;
19334
+ const hour = Number(match[1]);
19335
+ const minute = Number(match[2]);
19336
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
19337
+ return null;
19338
+ return { hour, minute };
19339
+ }
19340
+ function atLocalDate(now, dayOffset, hour, minute) {
19341
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayOffset, hour, minute, 0, 0);
19342
+ }
19343
+ function validateResult(executeAt, messageStartIndex, tokens, now, pastTodayValue) {
19344
+ if (pastTodayValue)
19345
+ return { ok: false, code: "past_today_time", value: pastTodayValue };
19346
+ if (tokens.slice(messageStartIndex).join(" ").trim().length === 0)
19347
+ return { ok: false, code: "missing_message" };
19348
+ const delta = executeAt.getTime() - now.getTime();
19349
+ if (delta < LATER_MIN_DELAY_MS)
19350
+ return { ok: false, code: "too_soon" };
19351
+ if (delta > LATER_MAX_DELAY_MS)
19352
+ return { ok: false, code: "out_of_range" };
19353
+ return { ok: true, executeAt, messageStartIndex };
19354
+ }
19355
+ var WEEKDAYS;
19356
+ var init_parse_later_time = __esm(() => {
19357
+ init_scheduled_types();
19358
+ WEEKDAYS = new Map([
19359
+ ["周日", 0],
19360
+ ["周天", 0],
19361
+ ["星期日", 0],
19362
+ ["星期天", 0],
19363
+ ["sun", 0],
19364
+ ["sunday", 0],
19365
+ ["周一", 1],
19366
+ ["星期一", 1],
19367
+ ["mon", 1],
19368
+ ["monday", 1],
19369
+ ["周二", 2],
19370
+ ["星期二", 2],
19371
+ ["tue", 2],
19372
+ ["tuesday", 2],
19373
+ ["周三", 3],
19374
+ ["星期三", 3],
19375
+ ["wed", 3],
19376
+ ["wednesday", 3],
19377
+ ["周四", 4],
19378
+ ["星期四", 4],
19379
+ ["thu", 4],
19380
+ ["thursday", 4],
19381
+ ["周五", 5],
19382
+ ["星期五", 5],
19383
+ ["fri", 5],
19384
+ ["friday", 5],
19385
+ ["周六", 6],
19386
+ ["星期六", 6],
19387
+ ["sat", 6],
19388
+ ["saturday", 6]
19389
+ ]);
19390
+ });
19391
+
19392
+ // src/commands/handlers/later-handler.ts
19393
+ function handleLaterHelp() {
19394
+ return { text: renderLaterHelp() };
19395
+ }
19396
+ async function handleLaterCreate(tokens, scheduled, chatKey, currentSession, defaultMode, accountId, replyContextToken) {
19397
+ let rest = tokens;
19398
+ const seenFlags = new Set;
19399
+ let flagMode;
19400
+ while (rest.length > 0 && (rest[0] === "--bind" || rest[0] === "--temp")) {
19401
+ seenFlags.add(rest[0]);
19402
+ flagMode = rest[0] === "--bind" ? "bound" : "temp";
19403
+ rest = rest.slice(1);
19404
+ }
19405
+ if (seenFlags.size > 1) {
19406
+ return { text: "定时任务的 --bind 与 --temp 不能同时使用。" };
19407
+ }
19408
+ const mode = flagMode ?? defaultMode;
19409
+ if (!currentSession) {
19410
+ return {
19411
+ text: [
19412
+ "当前没有会话,无法创建定时任务。",
19413
+ "",
19414
+ "请先创建或切换到一个会话:",
19415
+ "- /ss codex --ws backend(新建并切换)",
19416
+ "- /use backend-codex(切换到已有会话)"
19417
+ ].join(`
19418
+ `)
19419
+ };
19420
+ }
19421
+ const result = parseLaterTime(rest);
19422
+ if (!result.ok) {
19423
+ return { text: renderTimeParseError(result.code, result.value) };
19424
+ }
19425
+ const message = rest.slice(result.messageStartIndex).join(" ").trim();
19426
+ if (message.startsWith("/")) {
19427
+ return {
19428
+ text: [
19429
+ "不支持延迟执行 / 开头的命令。",
19430
+ "",
19431
+ "如果需要让 agent 解释命令,可以用自然语言描述:",
19432
+ "例如:/lt in 1h 请解释 /status 的作用"
19433
+ ].join(`
19434
+ `)
19435
+ };
19436
+ }
19437
+ const task = await scheduled.createTask({
19438
+ chatKey,
19439
+ sessionAlias: currentSession.alias,
19440
+ executeAt: result.executeAt,
19441
+ message,
19442
+ sessionMode: mode,
19443
+ ...mode === "temp" ? { agent: currentSession.agent, workspace: currentSession.workspace } : {},
19444
+ ...accountId ? { accountId } : {},
19445
+ ...replyContextToken ? { replyContextToken } : {}
19446
+ });
19447
+ return { text: renderTaskCreated(task, toDisplaySessionAlias(currentSession.alias)) };
19448
+ }
19449
+ function handleLaterList(scheduled) {
19450
+ const tasks = scheduled.listPending();
19451
+ return { text: renderLaterList(tasks, (alias) => toDisplaySessionAlias(alias)) };
19452
+ }
19453
+ async function handleLaterCancel(id, scheduled) {
19454
+ const ok = await scheduled.cancelPending(id);
19455
+ if (ok) {
19456
+ return { text: `已取消定时任务 #${id.replace(/^#/, "").toLowerCase()}` };
19457
+ }
19458
+ const displayId = id.replace(/^#/, "").toLowerCase();
19459
+ return { text: [`未找到待执行的定时任务 #${displayId}。`, "可以用 /lt list 查看当前待执行任务。"].join(`
19460
+ `) };
19461
+ }
19462
+ function renderTimeParseError(code, value) {
19463
+ switch (code) {
19464
+ case "missing_message":
19465
+ return "定时任务需要消息内容,请在时间后附上要发送的内容。";
19466
+ case "too_soon":
19467
+ return "定时任务执行时间必须在 10 秒之后。";
19468
+ case "out_of_range":
19469
+ return "定时任务执行时间不能超过 7 天。";
19470
+ case "past_today_time":
19471
+ return `今天 ${value} 已经过了,请指定一个未来的时间,或使用「明天」。`;
19472
+ case "unrecognized_time":
19473
+ case "missing_time":
19474
+ default:
19475
+ return [
19476
+ "无法识别时间格式。",
19477
+ "",
19478
+ "支持的格式:",
19479
+ "- /lt in 2h 消息(2小时后)",
19480
+ "- /lt 30分钟后 消息",
19481
+ "- /lt tomorrow 09:00 消息",
19482
+ "- /lt 周五 09:00 消息"
19483
+ ].join(`
19484
+ `);
19485
+ }
19486
+ }
19487
+ var laterHelpMetadata;
19488
+ var init_later_handler = __esm(() => {
19489
+ init_parse_later_time();
19490
+ init_scheduled_render();
19491
+ init_channel_scope();
19492
+ laterHelpMetadata = {
19493
+ topic: "later",
19494
+ aliases: ["lt"],
19495
+ summary: "定时任务:到点在临时会话执行(或 --bind 发到当前会话)",
19496
+ commands: [
19497
+ { usage: "/lt <时间> <消息>", description: "创建定时任务" },
19498
+ { usage: "/lt --bind <时间> <消息>", description: "改为发送到当前会话" },
19499
+ { usage: "/lt --temp <时间> <消息>", description: "强制使用临时会话" },
19500
+ { usage: "/lt list", description: "查看待执行定时任务" },
19501
+ { usage: "/lt cancel <id>", description: "取消定时任务" }
19502
+ ],
19503
+ examples: [
19504
+ "/lt in 2h 检查 CI",
19505
+ "/lt 30分钟后 总结进展",
19506
+ "/lt tomorrow 09:00 看 PR",
19507
+ "/lt 今天 21:30 继续处理",
19508
+ "/lt 周五 09:00 继续处理"
19509
+ ],
19510
+ notes: [
19511
+ "只支持一次性任务,不支持重复执行",
19512
+ "时间必须在 10 秒之后、7 天之内",
19513
+ "默认在为本次任务新建的临时会话里执行,跑完即销毁",
19514
+ "加 --bind 改为发送到创建时绑定的当前会话(默认模式可用 later.defaultMode 配置)",
19515
+ "/lt list 显示全局待执行任务;群聊中只有群主可取消",
19516
+ "不支持延迟执行 / 开头的 weacpx 命令",
19517
+ "完整时间格式与说明见 docs/later-command.md"
19518
+ ]
19519
+ };
19520
+ });
19521
+
18728
19522
  // src/commands/help/help-registry.ts
18729
19523
  function getHelpTopic(topic) {
18730
19524
  return HELP_TOPIC_MAP.get(topic) ?? null;
@@ -18740,6 +19534,7 @@ var init_help_registry = __esm(() => {
18740
19534
  init_permission_handler();
18741
19535
  init_session_handler();
18742
19536
  init_workspace_handler();
19537
+ init_later_handler();
18743
19538
  HELP_TOPICS = [
18744
19539
  sessionHelp,
18745
19540
  workspaceHelp,
@@ -18750,7 +19545,8 @@ var init_help_registry = __esm(() => {
18750
19545
  modeHelp,
18751
19546
  replyModeHelp,
18752
19547
  statusHelp,
18753
- cancelHelp
19548
+ cancelHelp,
19549
+ laterHelpMetadata
18754
19550
  ];
18755
19551
  HELP_TOPIC_MAP = new Map;
18756
19552
  for (const topic of HELP_TOPICS) {
@@ -18797,7 +19593,8 @@ function renderHelpTopic(topic) {
18797
19593
  "",
18798
19594
  "命令:",
18799
19595
  ...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
18800
- ...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : []
19596
+ ...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : [],
19597
+ ...topic.notes && topic.notes.length > 0 ? ["", "注意:", ...topic.notes.map((note) => `- ${note}`)] : []
18801
19598
  ].join(`
18802
19599
  `);
18803
19600
  }
@@ -18999,6 +19796,15 @@ var init_session_shortcut_handler = __esm(() => {
18999
19796
  function renderTransportError(session, error2) {
19000
19797
  const message = error2 instanceof Error ? error2.message : String(error2);
19001
19798
  if (message.includes("No acpx session found")) {
19799
+ if (session.transient) {
19800
+ return {
19801
+ text: [
19802
+ "定时任务的临时会话启动失败,本次任务未能执行。",
19803
+ "临时会话由系统在执行时自动创建,无需手动操作;如需重排,请用 /lt 重新安排。"
19804
+ ].join(`
19805
+ `)
19806
+ };
19807
+ }
19002
19808
  return {
19003
19809
  text: [
19004
19810
  `当前会话「${session.alias}」暂时不可用。`,
@@ -19075,6 +19881,9 @@ async function tryRecoverMissingSession(ops, session, error2) {
19075
19881
  if (!message.includes("No acpx session found")) {
19076
19882
  return null;
19077
19883
  }
19884
+ if (session.transient) {
19885
+ return null;
19886
+ }
19078
19887
  const transportAgentCommand = await ops.resolveSessionAgentCommand(session);
19079
19888
  if (!transportAgentCommand || transportAgentCommand === session.agentCommand) {
19080
19889
  return null;
@@ -19448,6 +20257,8 @@ class CommandRouter {
19448
20257
  resolveSessionAgentCommand;
19449
20258
  orchestration;
19450
20259
  quota;
20260
+ scheduled;
20261
+ scheduledDelivery;
19451
20262
  logger;
19452
20263
  autoInstall = autoInstallOptionalDep;
19453
20264
  discoverPaths = discoverParentPackagePaths;
@@ -19457,7 +20268,7 @@ class CommandRouter {
19457
20268
  __setDiscoverPathsForTest(fn) {
19458
20269
  this.discoverPaths = fn;
19459
20270
  }
19460
- constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota) {
20271
+ constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota, scheduled, scheduledDelivery) {
19461
20272
  this.sessions = sessions;
19462
20273
  this.transport = transport;
19463
20274
  this.config = config2;
@@ -19465,6 +20276,8 @@ class CommandRouter {
19465
20276
  this.resolveSessionAgentCommand = resolveSessionAgentCommand;
19466
20277
  this.orchestration = orchestration;
19467
20278
  this.quota = quota;
20279
+ this.scheduled = scheduled;
20280
+ this.scheduledDelivery = scheduledDelivery;
19468
20281
  this.logger = logger2 ?? createNoopAppLogger();
19469
20282
  }
19470
20283
  async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan) {
@@ -19585,8 +20398,46 @@ class CommandRouter {
19585
20398
  return await handleTaskReject(this.createHandlerContext(), chatKey, command.taskId);
19586
20399
  case "task.cancel":
19587
20400
  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);
20401
+ case "later.help":
20402
+ if (!this.scheduled)
20403
+ return { text: "定时任务服务未启用。" };
20404
+ return handleLaterHelp();
20405
+ case "later.list":
20406
+ if (!this.scheduled)
20407
+ return { text: "定时任务服务未启用。" };
20408
+ return handleLaterList(this.scheduled);
20409
+ case "later.create": {
20410
+ if (!this.scheduled)
20411
+ return { text: "定时任务服务未启用。" };
20412
+ if (this.scheduledDelivery && !this.scheduledDelivery.supportsScheduledMessages(chatKey)) {
20413
+ return { text: renderLaterUnsupportedChannel() };
20414
+ }
20415
+ const currentSession = await this.sessions.getCurrentSession(chatKey);
20416
+ return await handleLaterCreate(command.tokens, this.scheduled, chatKey, currentSession ? { alias: currentSession.alias, agent: currentSession.agent, workspace: currentSession.workspace } : null, this.config?.later?.defaultMode === "bind" ? "bound" : "temp", accountId, replyContextToken);
20417
+ }
20418
+ case "later.cancel":
20419
+ if (!this.scheduled)
20420
+ return { text: "定时任务服务未启用。" };
20421
+ return await handleLaterCancel(command.id, this.scheduled);
20422
+ case "prompt": {
20423
+ const sessionContext = this.createSessionHandlerContext(undefined, perfSpan);
20424
+ if (metadata?.scheduledSessionDescriptor) {
20425
+ const descriptor = metadata.scheduledSessionDescriptor;
20426
+ const transientSession = {
20427
+ ...this.sessions.resolveSession(descriptor.alias, descriptor.agent, descriptor.workspace, descriptor.transportSession),
20428
+ transient: true
20429
+ };
20430
+ return await handlePromptWithSession(sessionContext, transientSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata);
20431
+ }
20432
+ if (metadata?.scheduledSessionAlias) {
20433
+ const scheduledSession = await this.sessions.getSession(metadata.scheduledSessionAlias);
20434
+ if (!scheduledSession) {
20435
+ throw new Error(`session "${metadata.scheduledSessionAlias}" not found for scheduled prompt`);
20436
+ }
20437
+ return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata);
20438
+ }
20439
+ return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata);
20440
+ }
19590
20441
  }
19591
20442
  });
19592
20443
  }
@@ -19964,11 +20815,13 @@ var init_command_router = __esm(() => {
19964
20815
  init_agent_handler();
19965
20816
  init_workspace_handler();
19966
20817
  init_session_shortcut_handler();
20818
+ init_later_handler();
19967
20819
  init_session_recovery_handler();
19968
20820
  init_auto_install_optional_dep();
19969
20821
  init_discover_parent_package_paths();
19970
20822
  init_errors();
19971
20823
  init_session_reset_handler();
20824
+ init_scheduled_render();
19972
20825
  });
19973
20826
 
19974
20827
  // src/config/resolve-acpx-command.ts
@@ -20061,24 +20914,6 @@ var init_console_agent = __esm(() => {
20061
20914
  init_command_list();
20062
20915
  });
20063
20916
 
20064
- // src/orchestration/async-mutex.ts
20065
- class AsyncMutex {
20066
- tail = Promise.resolve();
20067
- async run(critical) {
20068
- const previous = this.tail;
20069
- let release;
20070
- this.tail = new Promise((resolve3) => {
20071
- release = resolve3;
20072
- });
20073
- await previous;
20074
- try {
20075
- return await critical();
20076
- } finally {
20077
- release();
20078
- }
20079
- }
20080
- }
20081
-
20082
20917
  // src/orchestration/orchestration-server.ts
20083
20918
  import { rm as rm7 } from "node:fs/promises";
20084
20919
  import { createConnection as createConnection2, createServer } from "node:net";
@@ -20234,6 +21069,12 @@ class OrchestrationServer {
20234
21069
  reviewId: requireString(params, "reviewId"),
20235
21070
  decision: requireEnum(params, "decision", ["accept", "discard"])
20236
21071
  });
21072
+ case "scheduled.create":
21073
+ return await this.dispatchScheduledCreate(params);
21074
+ case "scheduled.list":
21075
+ return await this.dispatchScheduledList(params);
21076
+ case "scheduled.cancel":
21077
+ return await this.dispatchScheduledCancel(params);
20237
21078
  case "group.new":
20238
21079
  requireOnlyKeys(params, ["coordinatorSession", "title"], "params");
20239
21080
  return await this.handlers.createGroup({
@@ -20244,6 +21085,47 @@ class OrchestrationServer {
20244
21085
  throw new OrchestrationInvalidRequestError(`unsupported orchestration method: ${method}`);
20245
21086
  }
20246
21087
  }
21088
+ async dispatchScheduledCreate(params) {
21089
+ const input = this.parseScheduledCreateInput(params);
21090
+ const handler = this.deps.createScheduledTaskFromRoute ?? this.handlers.createScheduledTaskFromRoute;
21091
+ if (!handler) {
21092
+ throw new Error("scheduled task creation is not configured");
21093
+ }
21094
+ return await handler(input);
21095
+ }
21096
+ parseScheduledCreateInput(params) {
21097
+ requireOnlyKeys(params, ["coordinatorSession", "timeText", "message", "mode"], "params");
21098
+ const mode = requireOptionalEnum(params, "mode", ["temp", "bound"]);
21099
+ return {
21100
+ coordinatorSession: requireString(params, "coordinatorSession"),
21101
+ timeText: requireString(params, "timeText"),
21102
+ message: requireString(params, "message"),
21103
+ ...mode !== undefined ? { mode } : {}
21104
+ };
21105
+ }
21106
+ async dispatchScheduledList(params) {
21107
+ requireOnlyKeys(params, ["coordinatorSession"], "params");
21108
+ const input = {
21109
+ coordinatorSession: requireString(params, "coordinatorSession")
21110
+ };
21111
+ const handler = this.deps.listScheduledTasksFromRoute ?? this.handlers.listScheduledTasksFromRoute;
21112
+ if (!handler) {
21113
+ throw new Error("scheduled task listing is not configured");
21114
+ }
21115
+ return await handler(input);
21116
+ }
21117
+ async dispatchScheduledCancel(params) {
21118
+ requireOnlyKeys(params, ["coordinatorSession", "id"], "params");
21119
+ const input = {
21120
+ coordinatorSession: requireString(params, "coordinatorSession"),
21121
+ id: requireString(params, "id")
21122
+ };
21123
+ const handler = this.deps.cancelScheduledTaskFromRoute ?? this.handlers.cancelScheduledTaskFromRoute;
21124
+ if (!handler) {
21125
+ throw new Error("scheduled task cancellation is not configured");
21126
+ }
21127
+ return await handler(input);
21128
+ }
20247
21129
  parseRegisterExternalCoordinatorInput(params) {
20248
21130
  requireOnlyKeys(params, ["coordinatorSession", "workspace", "defaultTargetAgent"], "params");
20249
21131
  const workspace = requireOptionalString(params, "workspace");
@@ -20588,6 +21470,9 @@ var init_orchestration_server = __esm(() => {
20588
21470
  "coordinator.retract_answer",
20589
21471
  "coordinator.request_human_input",
20590
21472
  "coordinator.review_contested_result",
21473
+ "scheduled.create",
21474
+ "scheduled.list",
21475
+ "scheduled.cancel",
20591
21476
  "group.new"
20592
21477
  ]);
20593
21478
  });
@@ -21581,10 +22466,11 @@ class OrchestrationService {
21581
22466
  const state = await this.deps.loadState();
21582
22467
  const now = this.deps.now().toISOString();
21583
22468
  const existing = this.ensureCoordinatorRoutes(state)[input.coordinatorSession];
22469
+ const sameChat = existing?.chatKey === input.chatKey;
21584
22470
  const hasAccountId = input.accountId !== undefined;
21585
22471
  const hasReplyContextToken = input.replyContextToken !== undefined;
21586
22472
  const hasCompleteReplyRoute = hasAccountId && hasReplyContextToken;
21587
- const shouldPreserveExistingReplyRoute = !hasAccountId && !hasReplyContextToken && existing?.chatKey === input.chatKey;
22473
+ const shouldPreserveExistingReplyRoute = !hasAccountId && !hasReplyContextToken && sameChat;
21588
22474
  const replyRoute = hasCompleteReplyRoute ? {
21589
22475
  accountId: input.accountId,
21590
22476
  replyContextToken: input.replyContextToken
@@ -21595,7 +22481,9 @@ class OrchestrationService {
21595
22481
  const route = {
21596
22482
  coordinatorSession: input.coordinatorSession,
21597
22483
  chatKey: input.chatKey,
22484
+ ...input.sessionAlias ? { sessionAlias: input.sessionAlias } : {},
21598
22485
  ...replyRoute ? replyRoute : {},
22486
+ ...buildCoordinatorRouteChatMetadata(input, sameChat ? existing : undefined),
21599
22487
  updatedAt: now
21600
22488
  };
21601
22489
  this.ensureCoordinatorRoutes(state)[input.coordinatorSession] = route;
@@ -23922,6 +24810,22 @@ class OrchestrationService {
23922
24810
  task.events = events.slice(-MAX_TASK_EVENTS_PER_TASK);
23923
24811
  }
23924
24812
  }
24813
+ function buildCoordinatorRouteChatMetadata(input, existing) {
24814
+ const channel = input.channel ?? existing?.channel;
24815
+ const chatType = input.chatType ?? existing?.chatType;
24816
+ const senderId = input.senderId ?? existing?.senderId;
24817
+ const senderName = input.senderName ?? existing?.senderName;
24818
+ const groupId = input.groupId ?? existing?.groupId;
24819
+ const isOwner = input.isOwner ?? existing?.isOwner;
24820
+ return {
24821
+ ...channel !== undefined ? { channel } : {},
24822
+ ...chatType !== undefined ? { chatType } : {},
24823
+ ...senderId !== undefined ? { senderId } : {},
24824
+ ...senderName !== undefined ? { senderName } : {},
24825
+ ...groupId !== undefined ? { groupId } : {},
24826
+ ...isOwner !== undefined ? { isOwner } : {}
24827
+ };
24828
+ }
23925
24829
  function isTerminalTaskStatus2(status) {
23926
24830
  return status === "completed" || status === "failed" || status === "cancelled";
23927
24831
  }
@@ -23965,6 +24869,269 @@ function buildWorkerAnswerPrompt(answer) {
23965
24869
  `);
23966
24870
  }
23967
24871
 
24872
+ // src/scheduled/scheduled-scheduler.ts
24873
+ class ScheduledTaskScheduler {
24874
+ service;
24875
+ intervalMs;
24876
+ dispatchTimeoutMs;
24877
+ setIntervalFn;
24878
+ clearIntervalFn;
24879
+ dispatchTask;
24880
+ logger;
24881
+ intervalHandle = null;
24882
+ ticking = false;
24883
+ constructor(service, deps) {
24884
+ this.service = service;
24885
+ this.dispatchTask = deps.dispatchTask;
24886
+ this.intervalMs = deps.intervalMs ?? 5000;
24887
+ this.dispatchTimeoutMs = deps.dispatchTimeoutMs ?? DEFAULT_DISPATCH_TIMEOUT_MS;
24888
+ this.setIntervalFn = deps.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
24889
+ this.clearIntervalFn = deps.clearIntervalFn ?? ((timer) => clearInterval(timer));
24890
+ this.logger = deps.logger;
24891
+ }
24892
+ async start() {
24893
+ if (this.intervalHandle !== null)
24894
+ return;
24895
+ await this.service.markStartupMissed();
24896
+ this.intervalHandle = this.setIntervalFn(() => {
24897
+ this.tick();
24898
+ }, this.intervalMs);
24899
+ await this.tick();
24900
+ }
24901
+ stop() {
24902
+ if (this.intervalHandle !== null) {
24903
+ this.clearIntervalFn(this.intervalHandle);
24904
+ this.intervalHandle = null;
24905
+ }
24906
+ }
24907
+ async tick() {
24908
+ if (this.ticking)
24909
+ return;
24910
+ this.ticking = true;
24911
+ try {
24912
+ const dueTasks = await this.service.claimDueTasks();
24913
+ for (const task of dueTasks) {
24914
+ try {
24915
+ await this.dispatchWithTimeout(task);
24916
+ await this.service.markExecuted(task.id);
24917
+ } catch (error2) {
24918
+ const message = error2 instanceof Error ? error2.message : String(error2);
24919
+ await this.logger?.error("scheduled.dispatch.failed", "failed to dispatch scheduled task", {
24920
+ taskId: task.id,
24921
+ message
24922
+ });
24923
+ await this.service.markFailed(task.id, error2);
24924
+ }
24925
+ }
24926
+ } finally {
24927
+ this.ticking = false;
24928
+ }
24929
+ }
24930
+ async dispatchWithTimeout(task) {
24931
+ const controller = new AbortController;
24932
+ let timer;
24933
+ const timeout = new Promise((_resolve, reject) => {
24934
+ timer = setTimeout(() => {
24935
+ reject(new Error(`scheduled task dispatch timed out after ${this.dispatchTimeoutMs}ms`));
24936
+ controller.abort();
24937
+ }, this.dispatchTimeoutMs);
24938
+ });
24939
+ const dispatch = this.dispatchTask(task, controller.signal);
24940
+ dispatch.catch(() => {});
24941
+ try {
24942
+ await Promise.race([dispatch, timeout]);
24943
+ } finally {
24944
+ if (timer !== undefined)
24945
+ clearTimeout(timer);
24946
+ }
24947
+ }
24948
+ }
24949
+ var DEFAULT_DISPATCH_TIMEOUT_MS;
24950
+ var init_scheduled_scheduler = __esm(() => {
24951
+ DEFAULT_DISPATCH_TIMEOUT_MS = 10 * 60 * 1000;
24952
+ });
24953
+
24954
+ // src/scheduled/scheduled-dispatch.ts
24955
+ function buildScheduledDispatchTask(deps) {
24956
+ return async (task, abortSignal) => {
24957
+ if (task.session_mode === "temp") {
24958
+ await dispatchTemp(task, abortSignal, deps);
24959
+ return;
24960
+ }
24961
+ await dispatchBound(task, abortSignal, deps);
24962
+ };
24963
+ }
24964
+ async function dispatchBound(task, abortSignal, deps) {
24965
+ const session = await deps.getSession(task.session_alias);
24966
+ if (!session) {
24967
+ throw new Error(`session "${task.session_alias}" not found for scheduled task`);
24968
+ }
24969
+ const noticeText = `执行定时任务 #${task.id}
24970
+ 会话:${toDisplaySessionAlias(task.session_alias)}
24971
+ 内容:${preview(task.message)}`;
24972
+ await deps.sendScheduledMessage({
24973
+ chatKey: task.chat_key,
24974
+ taskId: task.id,
24975
+ sessionAlias: task.session_alias,
24976
+ noticeText,
24977
+ promptText: task.message,
24978
+ abortSignal,
24979
+ ...task.account_id ? { accountId: task.account_id } : {},
24980
+ ...task.reply_context_token ? { replyContextToken: task.reply_context_token } : {}
24981
+ });
24982
+ }
24983
+ async function dispatchTemp(task, abortSignal, deps) {
24984
+ if (!task.agent || !task.workspace) {
24985
+ throw new Error(`temp scheduled task #${task.id} is missing its agent/workspace snapshot`);
24986
+ }
24987
+ const alias = `later-${task.id}`;
24988
+ const transportSession = `${task.workspace}:${alias}`;
24989
+ const session = deps.resolveSession(alias, task.agent, task.workspace, transportSession);
24990
+ const noticeText = `执行定时任务 #${task.id}
24991
+ 会话:临时会话(${task.workspace} · ${task.agent})
24992
+ 内容:${preview(task.message)}`;
24993
+ try {
24994
+ await deps.sendScheduledMessage({
24995
+ chatKey: task.chat_key,
24996
+ taskId: task.id,
24997
+ sessionAlias: task.session_alias,
24998
+ sessionDescriptor: { alias, agent: task.agent, workspace: task.workspace, transportSession },
24999
+ noticeText,
25000
+ promptText: task.message,
25001
+ abortSignal,
25002
+ ...task.account_id ? { accountId: task.account_id } : {},
25003
+ ...task.reply_context_token ? { replyContextToken: task.reply_context_token } : {}
25004
+ });
25005
+ } finally {
25006
+ if (deps.removeSession) {
25007
+ try {
25008
+ await deps.removeSession(session);
25009
+ } catch (error2) {
25010
+ await deps.logger?.error("scheduled.temp_session_close_failed", "failed to close temp scheduled session", { taskId: task.id, transportSession, error: String(error2) });
25011
+ }
25012
+ }
25013
+ }
25014
+ }
25015
+ var init_scheduled_dispatch = __esm(() => {
25016
+ init_channel_scope();
25017
+ init_scheduled_render();
25018
+ });
25019
+
25020
+ // src/scheduled/scheduled-route-create.ts
25021
+ async function createScheduledTaskFromRoute(input, deps) {
25022
+ const coordinatorSession = input.coordinatorSession.trim();
25023
+ if (coordinatorSession.length === 0) {
25024
+ throw new Error("coordinatorSession must be a non-empty string");
25025
+ }
25026
+ const route = deps.state.orchestration.coordinatorRoutes[coordinatorSession];
25027
+ if (!route) {
25028
+ throw new Error(`no chat route is recorded for coordinator session "${coordinatorSession}"`);
25029
+ }
25030
+ if (route.chatType !== "direct" && route.chatType !== "group") {
25031
+ throw new Error("scheduled_create requires current chat route metadata");
25032
+ }
25033
+ if (route.chatType === "group" && route.isOwner !== true) {
25034
+ throw new Error("scheduled_create is owner-only in group chats");
25035
+ }
25036
+ if (deps.supportsScheduledMessages && !deps.supportsScheduledMessages(route.chatKey)) {
25037
+ throw new Error("current channel does not support scheduled tasks");
25038
+ }
25039
+ const message = input.message.trim();
25040
+ if (message.length === 0) {
25041
+ throw new Error("message must be a non-empty string");
25042
+ }
25043
+ if (message.startsWith("/")) {
25044
+ throw new Error("scheduled_create does not support slash-prefixed weacpx commands");
25045
+ }
25046
+ if (!route.sessionAlias) {
25047
+ throw new Error("scheduled_create requires current session route metadata");
25048
+ }
25049
+ const session = await deps.sessions.getSession(route.sessionAlias);
25050
+ if (!session) {
25051
+ throw new Error(`session "${route.sessionAlias}" recorded for coordinator session "${coordinatorSession}" was not found`);
25052
+ }
25053
+ if (session.transportSession !== coordinatorSession) {
25054
+ throw new Error(`session "${route.sessionAlias}" is no longer attached to coordinator session "${coordinatorSession}"`);
25055
+ }
25056
+ const executeAt = parseRouteScheduledTime(input.timeText, deps.now?.() ?? new Date);
25057
+ const mode = input.mode ?? (deps.config.later?.defaultMode === "bind" ? "bound" : "temp");
25058
+ return await deps.scheduled.createTask({
25059
+ chatKey: route.chatKey,
25060
+ sessionAlias: session.alias,
25061
+ executeAt,
25062
+ message,
25063
+ sessionMode: mode,
25064
+ ...mode === "temp" ? { agent: session.agent, workspace: session.workspace } : {},
25065
+ ...route.accountId ? { accountId: route.accountId } : {},
25066
+ ...route.replyContextToken ? { replyContextToken: route.replyContextToken } : {},
25067
+ sourceLabel: "mcp:scheduled_create"
25068
+ });
25069
+ }
25070
+ function parseRouteScheduledTime(timeText, now) {
25071
+ const timeTokens = timeText.trim().split(/\s+/).filter((token) => token.length > 0);
25072
+ if (timeTokens.length === 0) {
25073
+ throw new Error(formatLaterTimeParseError("missing_time"));
25074
+ }
25075
+ const parsed = parseLaterTime([...timeTokens, "__scheduled_create_message__"], now);
25076
+ if (!parsed.ok) {
25077
+ throw new Error(formatLaterTimeParseError(parsed.code, parsed.value));
25078
+ }
25079
+ if (parsed.messageStartIndex !== timeTokens.length) {
25080
+ throw new Error("timeText must contain only the time expression; put the scheduled content in message");
25081
+ }
25082
+ return parsed.executeAt;
25083
+ }
25084
+ function formatLaterTimeParseError(code, value) {
25085
+ switch (code) {
25086
+ case "missing_message":
25087
+ return "message must be provided separately from timeText";
25088
+ case "too_soon":
25089
+ return "scheduled task time must be at least 10 seconds in the future";
25090
+ case "out_of_range":
25091
+ return "scheduled task time must be within 7 days";
25092
+ case "past_today_time":
25093
+ return `today at ${value} has already passed; choose a future time or use tomorrow`;
25094
+ case "unrecognized_time":
25095
+ case "missing_time":
25096
+ default:
25097
+ return "unrecognized timeText; supported examples: in 2h, 30分钟后, tomorrow 09:00, 周五 09:00";
25098
+ }
25099
+ }
25100
+ var init_scheduled_route_create = __esm(() => {
25101
+ init_parse_later_time();
25102
+ });
25103
+
25104
+ // src/scheduled/scheduled-route-manage.ts
25105
+ async function listScheduledTasksFromRoute(input, deps) {
25106
+ resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_list");
25107
+ return deps.scheduled.listPending();
25108
+ }
25109
+ async function cancelScheduledTaskFromRoute(input, deps) {
25110
+ resolveOwnedCoordinatorRoute(input.coordinatorSession, deps.state, "scheduled_cancel");
25111
+ const cancelled = await deps.scheduled.cancelPending(input.id);
25112
+ return { id: normalizeId(input.id), cancelled };
25113
+ }
25114
+ function resolveOwnedCoordinatorRoute(coordinatorSession, state, label) {
25115
+ const session = coordinatorSession.trim();
25116
+ if (session.length === 0) {
25117
+ throw new Error("coordinatorSession must be a non-empty string");
25118
+ }
25119
+ const route = state.orchestration.coordinatorRoutes[session];
25120
+ if (!route) {
25121
+ throw new Error(`no chat route is recorded for coordinator session "${session}"`);
25122
+ }
25123
+ if (route.chatType !== "direct" && route.chatType !== "group") {
25124
+ throw new Error(`${label} requires current chat route metadata`);
25125
+ }
25126
+ if (route.chatType === "group" && route.isOwner !== true) {
25127
+ throw new Error(`${label} is owner-only in group chats`);
25128
+ }
25129
+ return route;
25130
+ }
25131
+ var init_scheduled_route_manage = __esm(() => {
25132
+ init_scheduled_service();
25133
+ });
25134
+
23968
25135
  // src/sessions/session-service.ts
23969
25136
  class SessionService {
23970
25137
  config;
@@ -24419,20 +25586,47 @@ async function runConsole(paths, deps) {
24419
25586
  runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
24420
25587
  }, 86400000);
24421
25588
  }
25589
+ const channelStartPromise = deps.channels.startAll({
25590
+ agent: runtime.agent,
25591
+ abortSignal: shutdownController.signal,
25592
+ quota: runtime.quota,
25593
+ logger: runtime.logger,
25594
+ perfTracer: runtime.perfTracer
25595
+ });
25596
+ channelStartPromise.catch(() => {});
25597
+ let channelStartSettled = false;
25598
+ let channelStartError;
25599
+ channelStartPromise.then(() => {
25600
+ channelStartSettled = true;
25601
+ }, (error2) => {
25602
+ channelStartSettled = true;
25603
+ channelStartError = error2;
25604
+ });
25605
+ await Promise.resolve();
25606
+ if (channelStartSettled && channelStartError) {
25607
+ if (deps.channelStartupPolicy !== "best-effort") {
25608
+ throw channelStartError;
25609
+ }
25610
+ 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) });
25611
+ await waitForShutdown(shutdownController.signal);
25612
+ return;
25613
+ }
24422
25614
  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
- });
25615
+ await runtime.scheduled.scheduler.start();
25616
+ } catch (error2) {
25617
+ shutdownController.abort();
25618
+ throw error2;
25619
+ }
25620
+ try {
25621
+ await channelStartPromise;
24430
25622
  } catch (error2) {
25623
+ runtime.scheduled.scheduler.stop();
24431
25624
  if (deps.channelStartupPolicy !== "best-effort") {
24432
25625
  throw error2;
24433
25626
  }
24434
25627
  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
25628
  await waitForShutdown(shutdownController.signal);
25629
+ return;
24436
25630
  }
24437
25631
  } finally {
24438
25632
  await runCleanupSequence({
@@ -25573,7 +26767,7 @@ function buildWeacpxMcpServerSpec(input) {
25573
26767
  "mcp-stdio",
25574
26768
  "--coordinator-session",
25575
26769
  input.coordinatorSession,
25576
- ...input.sourceHandle ? ["--source-handle", input.sourceHandle] : []
26770
+ ...input.sourceHandle ? ["--source-handle", input.sourceHandle] : ["--internal-session-tools"]
25577
26771
  ]
25578
26772
  };
25579
26773
  }
@@ -26435,6 +27629,21 @@ class MessageChannelRegistry {
26435
27629
  async sendCoordinatorMessage(input) {
26436
27630
  await this.requireByChatKey(input.chatKey).sendCoordinatorMessage(input);
26437
27631
  }
27632
+ supportsScheduledMessages(chatKey) {
27633
+ const [candidateChannelId] = chatKey.split(":", 1);
27634
+ if (chatKey.includes(":") && candidateChannelId && !this.channels.has(candidateChannelId)) {
27635
+ return false;
27636
+ }
27637
+ const channel = this.getByChatKey(chatKey);
27638
+ return !!channel?.sendScheduledMessage;
27639
+ }
27640
+ async sendScheduledMessage(input) {
27641
+ const channel = this.requireByChatKey(input.chatKey);
27642
+ if (!channel.sendScheduledMessage) {
27643
+ throw new Error(`channel '${channel.id}' does not support scheduled messages`);
27644
+ }
27645
+ await channel.sendScheduledMessage(input);
27646
+ }
26438
27647
  createConsumerLocks() {
26439
27648
  const result = [];
26440
27649
  for (const channel of this.channels.values()) {
@@ -26649,6 +27858,7 @@ async function buildApp(paths, deps = {}) {
26649
27858
  }
26650
27859
  });
26651
27860
  const sessions = new SessionService(config2, debouncedStateStore, state, { stateMutex });
27861
+ const scheduledService = new ScheduledTaskService(state, debouncedStateStore, { stateMutex });
26652
27862
  const pendingWorkerDispatches = new Set;
26653
27863
  const transport = config2.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
26654
27864
  acpxCommand,
@@ -27007,9 +28217,34 @@ async function buildApp(paths, deps = {}) {
27007
28217
  }
27008
28218
  const progressHeartbeatInterval = startProgressHeartbeat(orchestration, config2, logger2, deps.channel ?? null);
27009
28219
  const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
27010
- const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration);
27011
- const router = new CommandRouter(sessions, transport, config2, configStore, logger2, undefined, orchestration, quota);
28220
+ const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration, {
28221
+ createScheduledTaskFromRoute: async (input) => await createScheduledTaskFromRoute(input, {
28222
+ state,
28223
+ config: config2,
28224
+ sessions,
28225
+ scheduled: scheduledService,
28226
+ ...deps.channel?.supportsScheduledMessages ? { supportsScheduledMessages: deps.channel.supportsScheduledMessages.bind(deps.channel) } : {}
28227
+ }),
28228
+ listScheduledTasksFromRoute: async (input) => await listScheduledTasksFromRoute(input, { state, scheduled: scheduledService }),
28229
+ cancelScheduledTaskFromRoute: async (input) => await cancelScheduledTaskFromRoute(input, { state, scheduled: scheduledService })
28230
+ });
28231
+ 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
28232
  const agent = new ConsoleAgent(router, logger2);
28233
+ const scheduledScheduler = new ScheduledTaskScheduler(scheduledService, {
28234
+ dispatchTask: buildScheduledDispatchTask({
28235
+ getSession: (alias) => sessions.getSession(alias),
28236
+ resolveSession: (alias, agent2, workspace, transportSession) => sessions.resolveSession(alias, agent2, workspace, transportSession),
28237
+ sendScheduledMessage: async (input) => {
28238
+ if (!deps.channel?.sendScheduledMessage) {
28239
+ throw new Error("no channel runtime available for scheduled task dispatch");
28240
+ }
28241
+ await deps.channel.sendScheduledMessage(input);
28242
+ },
28243
+ ...transport.removeSession ? { removeSession: (session) => transport.removeSession(session) } : {},
28244
+ logger: logger2
28245
+ }),
28246
+ logger: logger2
28247
+ });
27013
28248
  return {
27014
28249
  agent,
27015
28250
  router,
@@ -27025,7 +28260,12 @@ async function buildApp(paths, deps = {}) {
27025
28260
  server: orchestrationServer,
27026
28261
  endpoint: orchestrationEndpoint
27027
28262
  },
28263
+ scheduled: {
28264
+ service: scheduledService,
28265
+ scheduler: scheduledScheduler
28266
+ },
27028
28267
  dispose: async () => {
28268
+ scheduledScheduler.stop();
27029
28269
  if (progressHeartbeatInterval !== undefined) {
27030
28270
  clearInterval(progressHeartbeatInterval);
27031
28271
  }
@@ -27078,6 +28318,7 @@ function replaceRuntimeState(target, source) {
27078
28318
  target.sessions = source.sessions;
27079
28319
  target.chat_contexts = source.chat_contexts;
27080
28320
  target.orchestration = source.orchestration;
28321
+ target.scheduled_tasks = source.scheduled_tasks;
27081
28322
  }
27082
28323
  function replaceRuntimeConfig(target, source) {
27083
28324
  Object.assign(target, source);
@@ -27172,6 +28413,11 @@ var init_main = __esm(async () => {
27172
28413
  init_orchestration_server();
27173
28414
  init_orchestration_service();
27174
28415
  init_build_coordinator_prompt();
28416
+ init_scheduled_scheduler();
28417
+ init_scheduled_service();
28418
+ init_scheduled_dispatch();
28419
+ init_scheduled_route_create();
28420
+ init_scheduled_route_manage();
27175
28421
  init_session_service();
27176
28422
  init_state_store();
27177
28423
  init_run_console();
@@ -40703,12 +41949,13 @@ var sortSchema = exports_external.enum(["updatedAt", "createdAt"]);
40703
41949
  var orderSchema = exports_external.enum(["asc", "desc"]);
40704
41950
  var contestedDecisionSchema = exports_external.enum(["accept", "discard"]);
40705
41951
  var taskWatchModeSchema = exports_external.enum(["next_event", "until_attention_or_terminal"]);
41952
+ var scheduledModeSchema = exports_external.enum(["temp", "bound"]);
40706
41953
  var taskQuestionSchema = exports_external.object({
40707
41954
  taskId: exports_external.string().min(1),
40708
41955
  questionId: exports_external.string().min(1)
40709
41956
  }).strict();
40710
41957
  function buildWeacpxMcpToolRegistry(input) {
40711
- const { transport, coordinatorSession, sourceHandle, isExternalCoordinator, availableAgents } = input;
41958
+ const { transport, coordinatorSession, sourceHandle, isExternalCoordinator, internalSessionTools, availableAgents } = input;
40712
41959
  const tools = [
40713
41960
  {
40714
41961
  name: "delegate_request",
@@ -40926,6 +42173,63 @@ function buildWeacpxMcpToolRegistry(input) {
40926
42173
  })
40927
42174
  }
40928
42175
  ];
42176
+ if (internalSessionTools && !isExternalCoordinator && !sourceHandle) {
42177
+ tools.push({
42178
+ name: "scheduled_create",
42179
+ description: "Create a one-shot scheduled task for the current conversation session using the recorded chat route. Provide only the time expression and message; routing/session/account details are resolved by weacpx.",
42180
+ inputSchema: exports_external.object({
42181
+ timeText: exports_external.string().min(1).describe("Time expression, e.g. 'in 2h', '30分钟后', 'tomorrow 09:00', or '周五 09:00'."),
42182
+ message: exports_external.string().min(1).describe("Natural-language message to send to the current session at the scheduled time."),
42183
+ mode: scheduledModeSchema.describe("'temp' creates a temporary one-shot session; 'bound' sends to the current bound session.").optional()
42184
+ }).strict(),
42185
+ handler: async (args) => await asToolResult(async () => {
42186
+ const input2 = args;
42187
+ const task = await transport.scheduledCreate({
42188
+ coordinatorSession,
42189
+ timeText: input2.timeText,
42190
+ message: input2.message,
42191
+ ...input2.mode ? { mode: input2.mode } : {}
42192
+ });
42193
+ return createSuccessResult(`Scheduled task #${task.id} created for ${task.execute_at}.`, {
42194
+ id: task.id,
42195
+ status: task.status,
42196
+ executeAt: task.execute_at,
42197
+ sessionAlias: task.session_alias,
42198
+ sessionMode: task.session_mode ?? "bound"
42199
+ });
42200
+ })
42201
+ });
42202
+ tools.push({
42203
+ name: "scheduled_list",
42204
+ description: "List pending one-shot scheduled tasks (global). Use to recover task ids before cancelling, or to see what is scheduled. Owner-only in group chats. Routing and account are resolved from the current session; pass no other arguments.",
42205
+ inputSchema: exports_external.object({}).strict(),
42206
+ handler: async () => await asToolResult(async () => {
42207
+ const tasks = await transport.scheduledList({ coordinatorSession });
42208
+ return createSuccessResult(renderScheduledList(tasks), {
42209
+ tasks: tasks.map((task) => ({
42210
+ id: task.id,
42211
+ executeAt: task.execute_at,
42212
+ message: task.message,
42213
+ sessionAlias: task.session_alias,
42214
+ sessionMode: task.session_mode ?? "bound",
42215
+ chatKey: task.chat_key
42216
+ }))
42217
+ });
42218
+ })
42219
+ });
42220
+ tools.push({
42221
+ name: "scheduled_cancel",
42222
+ description: "Cancel a pending scheduled task by id. Owner-only in group chats. Returns whether a pending task with that id was found and cancelled. Routing is resolved from the current session.",
42223
+ inputSchema: exports_external.object({
42224
+ id: exports_external.string().min(1).describe("The scheduled task id, e.g. 'k8f2' (a leading # is allowed).")
42225
+ }).strict(),
42226
+ handler: async (args) => await asToolResult(async () => {
42227
+ const { id } = args;
42228
+ const result = await transport.scheduledCancel({ coordinatorSession, id });
42229
+ return createSuccessResult(renderScheduledCancel(result), { id: result.id, cancelled: result.cancelled });
42230
+ })
42231
+ });
42232
+ }
40929
42233
  if (isExternalCoordinator) {
40930
42234
  const externalCoordinatorIncompatibleTools = new Set([
40931
42235
  "coordinator_request_human_input"
@@ -41005,6 +42309,19 @@ function renderTaskList(tasks) {
41005
42309
  return ["Tasks for the current coordinator:", ...tasks.map((task) => renderTaskListItem(task))].join(`
41006
42310
  `);
41007
42311
  }
42312
+ function renderScheduledList(tasks) {
42313
+ if (tasks.length === 0) {
42314
+ return "There are no pending scheduled tasks.";
42315
+ }
42316
+ return [
42317
+ "Pending scheduled tasks:",
42318
+ ...tasks.map((task) => `- #${task.id} at ${task.execute_at} [${task.session_mode ?? "bound"}] -> ${task.session_alias}: ${task.message}`)
42319
+ ].join(`
42320
+ `);
42321
+ }
42322
+ function renderScheduledCancel(result) {
42323
+ return result.cancelled ? `Scheduled task #${result.id} cancelled.` : `No pending scheduled task #${result.id} found.`;
42324
+ }
41008
42325
  function renderTaskListItem(task) {
41009
42326
  const role = task.role ? ` / ${task.role}` : "";
41010
42327
  const group = task.groupId ? `; group: ${task.groupId}` : "";
@@ -41175,6 +42492,15 @@ class OrchestrationClient {
41175
42492
  async createGroup(input) {
41176
42493
  return await this.request("group.new", input);
41177
42494
  }
42495
+ async scheduledCreate(input) {
42496
+ return await this.request("scheduled.create", input);
42497
+ }
42498
+ async scheduledList(input) {
42499
+ return await this.request("scheduled.list", input);
42500
+ }
42501
+ async scheduledCancel(input) {
42502
+ return await this.request("scheduled.cancel", input);
42503
+ }
41178
42504
  async request(method, params, timeoutMs = this.timeoutMs) {
41179
42505
  const id = this.createId();
41180
42506
  return await new Promise((resolve, reject) => {
@@ -41287,7 +42613,25 @@ function createOrchestrationTransport(endpoint, deps = {}) {
41287
42613
  },
41288
42614
  coordinatorAnswerQuestion: async (input) => await client.coordinatorAnswerQuestion(input),
41289
42615
  coordinatorRequestHumanInput: async (input) => await client.coordinatorRequestHumanInput(input),
41290
- coordinatorReviewContestedResult: async (input) => await client.coordinatorReviewContestedResult(input)
42616
+ coordinatorReviewContestedResult: async (input) => await client.coordinatorReviewContestedResult(input),
42617
+ scheduledCreate: async (input) => {
42618
+ if (!client.scheduledCreate) {
42619
+ throw new Error("orchestration client scheduledCreate is not configured");
42620
+ }
42621
+ return await client.scheduledCreate(input);
42622
+ },
42623
+ scheduledList: async (input) => {
42624
+ if (!client.scheduledList) {
42625
+ throw new Error("orchestration client scheduledList is not configured");
42626
+ }
42627
+ return await client.scheduledList(input);
42628
+ },
42629
+ scheduledCancel: async (input) => {
42630
+ if (!client.scheduledCancel) {
42631
+ throw new Error("orchestration client scheduledCancel is not configured");
42632
+ }
42633
+ return await client.scheduledCancel(input);
42634
+ }
41291
42635
  };
41292
42636
  }
41293
42637
 
@@ -41343,6 +42687,7 @@ function createWeacpxMcpServer(options) {
41343
42687
  coordinatorSession: identity.coordinatorSession,
41344
42688
  ...identity.sourceHandle ? { sourceHandle: identity.sourceHandle } : {},
41345
42689
  ...identity.isExternalCoordinator ? { isExternalCoordinator: true } : {},
42690
+ ...identity.internalSessionTools ? { internalSessionTools: true } : {},
41346
42691
  ...options.availableAgents ? { availableAgents: options.availableAgents } : {}
41347
42692
  });
41348
42693
  return toolState;
@@ -41778,7 +43123,8 @@ async function resolveMcpIdentity(server, options) {
41778
43123
  return {
41779
43124
  coordinatorSession: options.coordinatorSession,
41780
43125
  ...options.sourceHandle ? { sourceHandle: options.sourceHandle } : {},
41781
- ...options.isExternalCoordinator ? { isExternalCoordinator: true } : {}
43126
+ ...options.isExternalCoordinator ? { isExternalCoordinator: true } : {},
43127
+ ...options.internalSessionTools ? { internalSessionTools: true } : {}
41782
43128
  };
41783
43129
  }
41784
43130
  throw new McpError(ErrorCode.InvalidRequest, "weacpx MCP identity is not configured; run through `weacpx mcp-stdio` or provide --coordinator-session");
@@ -41867,6 +43213,7 @@ async function runWeacpxMcpServer(options) {
41867
43213
  transport,
41868
43214
  ...options.coordinatorSession ? { coordinatorSession: options.coordinatorSession } : {},
41869
43215
  ...options.sourceHandle ? { sourceHandle: options.sourceHandle } : {},
43216
+ ...options.internalSessionTools ? { internalSessionTools: true } : {},
41870
43217
  ...options.resolveIdentity ? { resolveIdentity: options.resolveIdentity } : {},
41871
43218
  ...options.availableAgents ? { availableAgents: options.availableAgents } : {}
41872
43219
  });
@@ -41961,6 +43308,11 @@ function parseCoordinatorSession(args, env = process.env) {
41961
43308
  });
41962
43309
  }
41963
43310
 
43311
+ // src/mcp/parse-internal-session-tools.ts
43312
+ function parseInternalSessionToolsFlag(args, _env = process.env) {
43313
+ return args.includes("--internal-session-tools");
43314
+ }
43315
+
41964
43316
  // src/mcp/parse-source-handle.ts
41965
43317
  function parseSourceHandle(args, env = process.env) {
41966
43318
  return parseStringFlag(args, env, {
@@ -41973,13 +43325,19 @@ function parseSourceHandle(args, env = process.env) {
41973
43325
  init_workspace_path();
41974
43326
  init_workspace_name();
41975
43327
  init_state_store();
43328
+ init_channel_scope();
43329
+ init_scheduled_render();
43330
+ init_scheduled_service();
41976
43331
 
41977
43332
  // src/onboarding.ts
41978
43333
  init_workspace_path();
41979
43334
  init_workspace_name();
43335
+ init_default_workspace();
41980
43336
  init_agent_templates();
41981
43337
  function isFirstUse(config2, state) {
41982
- return Object.keys(state.sessions ?? {}).length === 0 && Object.keys(config2.workspaces ?? {}).length === 0 && (config2.plugins ?? []).length === 0;
43338
+ const workspaceNames = Object.keys(config2.workspaces ?? {});
43339
+ const onlyDefaultOrEmpty = workspaceNames.length === 0 || workspaceNames.length === 1 && workspaceNames[0] === DEFAULT_HOME_WORKSPACE_NAME;
43340
+ return Object.keys(state.sessions ?? {}).length === 0 && onlyDefaultOrEmpty && (config2.plugins ?? []).length === 0;
41983
43341
  }
41984
43342
  async function maybeRunFirstUseOnboarding(input) {
41985
43343
  if (!isFirstUse(input.config, input.state))
@@ -43647,7 +45005,8 @@ function createMcpStdioIdentityResolver(input) {
43647
45005
  return {
43648
45006
  coordinatorSession: resolvedCoordinatorSession,
43649
45007
  ...sourceHandle ? { sourceHandle } : {},
43650
- ...startup.kind === "external-coordinator" ? { isExternalCoordinator: true } : {}
45008
+ ...startup.kind === "external-coordinator" ? { isExternalCoordinator: true } : {},
45009
+ ...input.internalSessionTools && startup.kind === "existing-session" && !sourceHandle ? { internalSessionTools: true } : {}
43651
45010
  };
43652
45011
  };
43653
45012
  }
@@ -43692,6 +45051,7 @@ var HELP_LINES = [
43692
45051
  "weacpx version - 查看版本",
43693
45052
  "weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
43694
45053
  "weacpx workspace list|add [name] [--raw]|rm <name> - 管理本机工作区(别名:ws)",
45054
+ "weacpx later|lt list|cancel <id> - 管理本机待执行定时任务",
43695
45055
  "weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
43696
45056
  ];
43697
45057
  function getUsageText() {
@@ -43779,6 +45139,17 @@ async function runCli(args, deps = {}) {
43779
45139
  }
43780
45140
  return result;
43781
45141
  }
45142
+ case "later":
45143
+ case "lt": {
45144
+ const result = await handleLaterCli(args.slice(1), { print });
45145
+ if (result === null) {
45146
+ for (const line of HELP_LINES) {
45147
+ print(line);
45148
+ }
45149
+ return 1;
45150
+ }
45151
+ return result;
45152
+ }
43782
45153
  case "plugin": {
43783
45154
  const result = await handlePluginCli(args.slice(1), await createPluginCliDeps({
43784
45155
  print,
@@ -44127,6 +45498,48 @@ async function agentRemove(rawName, print) {
44127
45498
  print(`Agent「${name}」已删除`);
44128
45499
  return 0;
44129
45500
  }
45501
+ async function handleLaterCli(args, deps) {
45502
+ const subcommand = args[0];
45503
+ switch (subcommand) {
45504
+ case "list":
45505
+ if (args.length !== 1)
45506
+ return null;
45507
+ return await laterList(deps.print);
45508
+ case "cancel":
45509
+ if (args.length !== 2 || !args[1])
45510
+ return null;
45511
+ return await laterCancel(args[1], deps.print);
45512
+ default:
45513
+ return null;
45514
+ }
45515
+ }
45516
+ async function laterList(print) {
45517
+ const scheduled = await createCliScheduledTaskService();
45518
+ print(renderLaterList(scheduled.listPending(), (alias) => toDisplaySessionAlias(alias)));
45519
+ return 0;
45520
+ }
45521
+ async function laterCancel(rawId, print) {
45522
+ const id = normalizeId(rawId);
45523
+ if (id.length === 0) {
45524
+ print("定时任务 ID 不能为空。");
45525
+ return 1;
45526
+ }
45527
+ const scheduled = await createCliScheduledTaskService();
45528
+ const ok = await scheduled.cancelPending(id);
45529
+ if (!ok) {
45530
+ print(`未找到待执行的定时任务 #${id}。`);
45531
+ print("可以用 weacpx later list 查看当前待执行任务。");
45532
+ return 1;
45533
+ }
45534
+ print(`已取消定时任务 #${id}`);
45535
+ return 0;
45536
+ }
45537
+ async function createCliScheduledTaskService() {
45538
+ const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
45539
+ const stateStore = new StateStore(runtimePaths.statePath);
45540
+ const state = await stateStore.load();
45541
+ return new ScheduledTaskService(state, stateStore);
45542
+ }
44130
45543
  function resolveConfigPathForCurrentEnv() {
44131
45544
  return process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
44132
45545
  }
@@ -44253,10 +45666,12 @@ async function defaultMcpStdio(args, deps = {}) {
44253
45666
  let transport;
44254
45667
  let identityResolver;
44255
45668
  let availableAgents;
45669
+ let internalSessionTools = false;
44256
45670
  try {
44257
45671
  const parsedCoordinatorSession = parseCoordinatorSession(args, process.env);
44258
45672
  sourceHandle = parseSourceHandle(args, process.env);
44259
45673
  const workspace = parseCoordinatorWorkspace(args, process.env);
45674
+ const requestedInternalSessionTools = parseInternalSessionToolsFlag(args, process.env);
44260
45675
  endpoint = resolveDefaultOrchestrationEndpoint(process.env, process.platform);
44261
45676
  const client = new OrchestrationClient(endpoint);
44262
45677
  transport = createOrchestrationTransport(endpoint, { client });
@@ -44271,10 +45686,12 @@ async function defaultMcpStdio(args, deps = {}) {
44271
45686
  workspace,
44272
45687
  config: config2,
44273
45688
  state,
44274
- client
45689
+ client,
45690
+ internalSessionTools: requestedInternalSessionTools
44275
45691
  });
44276
45692
  const eagerIdentity = parsedCoordinatorSession ? await resolveIdentity({ clientName: undefined, listRoots: async () => [] }) : null;
44277
45693
  coordinatorSession = eagerIdentity?.coordinatorSession ?? "";
45694
+ internalSessionTools = eagerIdentity?.internalSessionTools ?? false;
44278
45695
  identityResolver = eagerIdentity ? undefined : resolveIdentity;
44279
45696
  } catch (error2) {
44280
45697
  (deps.stderr ?? ((text) => process.stderr.write(text)))(`${error2 instanceof Error ? error2.message : String(error2)}
@@ -44285,6 +45702,7 @@ async function defaultMcpStdio(args, deps = {}) {
44285
45702
  transport,
44286
45703
  ...coordinatorSession ? { coordinatorSession } : {},
44287
45704
  ...sourceHandle ? { sourceHandle } : {},
45705
+ ...internalSessionTools ? { internalSessionTools: true } : {},
44288
45706
  ...identityResolver ? { resolveIdentity: identityResolver } : {},
44289
45707
  ...availableAgents ? { availableAgents } : {},
44290
45708
  onDiagnostic: (event, context) => {