weacpx 0.4.10 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/channels/types.d.ts +11 -0
- package/dist/channels/weixin-channel.d.ts +3 -1
- package/dist/cli.js +918 -23
- package/dist/plugin-api.d.ts +1 -1
- package/dist/plugin-api.js +1 -1
- package/dist/plugins/compatibility.d.ts +1 -1
- package/dist/weixin/agent/interface.d.ts +2 -0
- package/dist/weixin/messaging/scheduled-turn.d.ts +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -323,6 +323,30 @@ opencode, qoder, qwen, trae
|
|
|
323
323
|
/cancel
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
+
### 定时任务(/later)
|
|
327
|
+
|
|
328
|
+
让 agent 在未来某个时间自动收到一条消息。任务绑定「创建时的当前会话」,到点后把这条消息作为普通 prompt 发给那个会话。
|
|
329
|
+
|
|
330
|
+
| 命令 | 说明 |
|
|
331
|
+
|------|------|
|
|
332
|
+
| `/lt <时间> <消息>` | 创建一次性定时任务(`/later` 同义) |
|
|
333
|
+
| `/lt list` | 查看全局待执行任务 |
|
|
334
|
+
| `/lt cancel <id>` | 取消待执行任务 |
|
|
335
|
+
|
|
336
|
+
最常见例子:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
/lt in 2h 检查 CI 是否通过
|
|
340
|
+
/lt 明天 09:00 看 PR
|
|
341
|
+
/lt list
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
说明:
|
|
345
|
+
|
|
346
|
+
- 只支持一次性任务,时间必须在 10 秒之后、7 天之内
|
|
347
|
+
- 时间格式是固定白名单(相对时间 / 今天·明天·后天 / 星期几 + 时刻),不支持自然语言
|
|
348
|
+
- 完整时间格式、任务状态与限制见 [docs/later-command.md](./docs/later-command.md)
|
|
349
|
+
|
|
326
350
|
### 配置与权限
|
|
327
351
|
|
|
328
352
|
| 命令 | 说明 |
|
|
@@ -514,6 +538,7 @@ bun run dev
|
|
|
514
538
|
### 日常使用
|
|
515
539
|
|
|
516
540
|
- 想查看完整聊天命令参考:[docs/commands.md](./docs/commands.md)
|
|
541
|
+
- 想用定时任务(`/later`)安排一次性的未来消息:[docs/later-command.md](./docs/later-command.md)
|
|
517
542
|
- 想理解什么时候该用 delegate、什么时候该开 group:[docs/weacpx-group-usage-guide.md](./docs/weacpx-group-usage-guide.md)
|
|
518
543
|
|
|
519
544
|
### 排错与验证
|
package/dist/channels/types.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface CoordinatorMessageInput {
|
|
|
22
22
|
replyContextToken?: string;
|
|
23
23
|
text: string;
|
|
24
24
|
}
|
|
25
|
+
export interface ScheduledChannelMessageInput {
|
|
26
|
+
chatKey: string;
|
|
27
|
+
taskId?: string;
|
|
28
|
+
sessionAlias: string;
|
|
29
|
+
accountId?: string;
|
|
30
|
+
replyContextToken?: string;
|
|
31
|
+
noticeText: string;
|
|
32
|
+
promptText: string;
|
|
33
|
+
abortSignal?: AbortSignal;
|
|
34
|
+
}
|
|
25
35
|
export interface ChannelStartInput {
|
|
26
36
|
agent: ChatAgent;
|
|
27
37
|
abortSignal: AbortSignal;
|
|
@@ -60,6 +70,7 @@ export interface MessageChannelRuntime {
|
|
|
60
70
|
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
|
|
61
71
|
notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
|
|
62
72
|
sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
|
|
73
|
+
sendScheduledMessage?(input: ScheduledChannelMessageInput): Promise<void>;
|
|
63
74
|
}
|
|
64
75
|
export type ToolUseStatus = "running" | "success" | "error";
|
|
65
76
|
export type ToolUseKind = "read" | "search" | "execute" | "edit" | "think" | "other";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { MessageChannelRuntime, ChannelStartInput, CoordinatorMessageInput, OrchestrationDeliveryCallbacks, ConsumerLock, ConsumerLockOptions } from "./types.js";
|
|
1
|
+
import type { MessageChannelRuntime, ChannelStartInput, CoordinatorMessageInput, OrchestrationDeliveryCallbacks, ConsumerLock, ConsumerLockOptions, ScheduledChannelMessageInput } from "./types.js";
|
|
2
2
|
import type { RuntimeMediaStore } from "./media-store.js";
|
|
3
3
|
import type { OrchestrationTaskRecord } from "../orchestration/orchestration-types.js";
|
|
4
4
|
export declare class WeixinChannel implements MessageChannelRuntime {
|
|
5
5
|
readonly id = "weixin";
|
|
6
|
+
private agent;
|
|
6
7
|
private quota;
|
|
7
8
|
private logger;
|
|
8
9
|
private markDelivered;
|
|
@@ -19,4 +20,5 @@ export declare class WeixinChannel implements MessageChannelRuntime {
|
|
|
19
20
|
notifyTaskCompletion(task: OrchestrationTaskRecord): Promise<void>;
|
|
20
21
|
notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
|
|
21
22
|
sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
|
|
23
|
+
sendScheduledMessage(input: ScheduledChannelMessageInput): Promise<void>;
|
|
22
24
|
}
|
package/dist/cli.js
CHANGED
|
@@ -9735,7 +9735,8 @@ function createEmptyState() {
|
|
|
9735
9735
|
return {
|
|
9736
9736
|
sessions: {},
|
|
9737
9737
|
chat_contexts: {},
|
|
9738
|
-
orchestration: createEmptyOrchestrationState()
|
|
9738
|
+
orchestration: createEmptyOrchestrationState(),
|
|
9739
|
+
scheduled_tasks: {}
|
|
9739
9740
|
};
|
|
9740
9741
|
}
|
|
9741
9742
|
var init_types = () => {};
|
|
@@ -9981,6 +9982,29 @@ function parseChatContexts(raw, path3) {
|
|
|
9981
9982
|
}
|
|
9982
9983
|
return chatContexts;
|
|
9983
9984
|
}
|
|
9985
|
+
function isScheduledTaskStatus(value) {
|
|
9986
|
+
return value === "pending" || value === "triggering" || value === "executed" || value === "cancelled" || value === "missed" || value === "failed";
|
|
9987
|
+
}
|
|
9988
|
+
function isScheduledTaskRecord(value) {
|
|
9989
|
+
if (!isRecord2(value))
|
|
9990
|
+
return false;
|
|
9991
|
+
return isString(value.id) && isString(value.chat_key) && isString(value.session_alias) && isString(value.execute_at) && isString(value.message) && isScheduledTaskStatus(value.status) && isString(value.created_at) && isOptionalString(value.account_id) && isOptionalString(value.reply_context_token) && isOptionalString(value.source_label) && isOptionalString(value.triggered_at) && isOptionalString(value.executed_at) && isOptionalString(value.cancelled_at) && isOptionalString(value.missed_at) && isOptionalString(value.failed_at) && isOptionalString(value.last_error);
|
|
9992
|
+
}
|
|
9993
|
+
function parseScheduledTasks(raw, path3) {
|
|
9994
|
+
if (raw === undefined)
|
|
9995
|
+
return {};
|
|
9996
|
+
if (!isRecord2(raw)) {
|
|
9997
|
+
throw new Error(`state file "${path3}" must contain an object field "scheduled_tasks"`);
|
|
9998
|
+
}
|
|
9999
|
+
const tasks = {};
|
|
10000
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
10001
|
+
if (!isScheduledTaskRecord(value) || value.id !== id) {
|
|
10002
|
+
throw new Error(`state file "${path3}" contains malformed scheduled task record "${id}"`);
|
|
10003
|
+
}
|
|
10004
|
+
tasks[id] = value;
|
|
10005
|
+
}
|
|
10006
|
+
return tasks;
|
|
10007
|
+
}
|
|
9984
10008
|
function parseState(raw, path3) {
|
|
9985
10009
|
if (!isRecord2(raw)) {
|
|
9986
10010
|
throw new Error(`state file "${path3}" must contain a JSON object`);
|
|
@@ -9999,7 +10023,8 @@ function parseState(raw, path3) {
|
|
|
9999
10023
|
return {
|
|
10000
10024
|
sessions: parsedSessions,
|
|
10001
10025
|
chat_contexts: parseChatContexts(chatContexts, path3),
|
|
10002
|
-
orchestration
|
|
10026
|
+
orchestration,
|
|
10027
|
+
scheduled_tasks: parseScheduledTasks(raw.scheduled_tasks, path3)
|
|
10003
10028
|
};
|
|
10004
10029
|
}
|
|
10005
10030
|
function validateExternalCoordinatorIdentityCollisions(sessions, orchestration, path3) {
|
|
@@ -15442,6 +15467,177 @@ var init_deliver_coordinator_message = __esm(() => {
|
|
|
15442
15467
|
init_send();
|
|
15443
15468
|
});
|
|
15444
15469
|
|
|
15470
|
+
// src/weixin/messaging/scheduled-turn.ts
|
|
15471
|
+
async function executeScheduledTurn(input, deps) {
|
|
15472
|
+
const userId = normalizeWeixinUserIdFromChatKey(input.chatKey);
|
|
15473
|
+
const quotaKey = userId;
|
|
15474
|
+
const sendMessage2 = deps.sendMessage ?? sendMessageWeixin;
|
|
15475
|
+
const candidateAccountIds = input.accountId ? [input.accountId] : deps.listAccountIds();
|
|
15476
|
+
if (candidateAccountIds.length === 0) {
|
|
15477
|
+
throw new Error(`no weixin account is available for scheduled message on chatKey: ${input.chatKey}`);
|
|
15478
|
+
}
|
|
15479
|
+
let noticeSent = false;
|
|
15480
|
+
let lastNoticeError;
|
|
15481
|
+
let deliveryAccountId;
|
|
15482
|
+
let deliveryContextToken;
|
|
15483
|
+
let deliverableAccountId;
|
|
15484
|
+
let deliverableContextToken;
|
|
15485
|
+
const resolveContextToken = (candidateAccountId) => deps.getContextToken(candidateAccountId, userId) ?? (candidateAccountId === input.accountId ? input.replyContextToken : undefined);
|
|
15486
|
+
for (const candidateAccountId of candidateAccountIds) {
|
|
15487
|
+
const contextToken = resolveContextToken(candidateAccountId);
|
|
15488
|
+
if (!contextToken)
|
|
15489
|
+
continue;
|
|
15490
|
+
const account = deps.resolveAccount(candidateAccountId);
|
|
15491
|
+
if (!account.token)
|
|
15492
|
+
continue;
|
|
15493
|
+
if (!deliverableAccountId) {
|
|
15494
|
+
deliverableAccountId = candidateAccountId;
|
|
15495
|
+
deliverableContextToken = contextToken;
|
|
15496
|
+
}
|
|
15497
|
+
try {
|
|
15498
|
+
if (!deps.reserveMidSegment(quotaKey)) {
|
|
15499
|
+
throw new Error("mid segment quota exhausted");
|
|
15500
|
+
}
|
|
15501
|
+
await sendMessage2({
|
|
15502
|
+
to: userId,
|
|
15503
|
+
text: input.noticeText,
|
|
15504
|
+
opts: { baseUrl: account.baseUrl, token: account.token, contextToken }
|
|
15505
|
+
});
|
|
15506
|
+
noticeSent = true;
|
|
15507
|
+
deliveryAccountId = candidateAccountId;
|
|
15508
|
+
deliveryContextToken = contextToken;
|
|
15509
|
+
break;
|
|
15510
|
+
} catch (error2) {
|
|
15511
|
+
lastNoticeError = error2;
|
|
15512
|
+
await deps.logger.error("scheduled.notice_send_failed", "failed to send scheduled notice", { chatKey: input.chatKey, accountId: candidateAccountId, error: String(error2) });
|
|
15513
|
+
}
|
|
15514
|
+
}
|
|
15515
|
+
if (!noticeSent) {
|
|
15516
|
+
if (!deliverableAccountId || !deliverableContextToken) {
|
|
15517
|
+
const message = lastNoticeError instanceof Error ? lastNoticeError.message : `no deliverable weixin context for scheduled message on chatKey: ${input.chatKey}`;
|
|
15518
|
+
throw new Error(message);
|
|
15519
|
+
}
|
|
15520
|
+
deliveryAccountId = deliverableAccountId;
|
|
15521
|
+
deliveryContextToken = deliverableContextToken;
|
|
15522
|
+
await deps.logger.info("scheduled.notice_skipped", "scheduled trigger notice was not delivered; proceeding with agent turn", {
|
|
15523
|
+
chatKey: input.chatKey,
|
|
15524
|
+
accountId: deliveryAccountId,
|
|
15525
|
+
reason: lastNoticeError instanceof Error ? lastNoticeError.message : "notice_undelivered"
|
|
15526
|
+
});
|
|
15527
|
+
}
|
|
15528
|
+
const sendReplySegment = async (text) => {
|
|
15529
|
+
const plainText = markdownToPlainText(text).trim();
|
|
15530
|
+
if (plainText.length === 0)
|
|
15531
|
+
return false;
|
|
15532
|
+
return await sendTextViaAvailableAccount(plainText, "scheduled.mid_send_failed");
|
|
15533
|
+
};
|
|
15534
|
+
const sendReservedMidText = async (text) => {
|
|
15535
|
+
const plainText = markdownToPlainText(text).trim();
|
|
15536
|
+
if (plainText.length === 0)
|
|
15537
|
+
return false;
|
|
15538
|
+
if (!deps.reserveMidSegment(quotaKey)) {
|
|
15539
|
+
await deps.logger.info("scheduled.mid_dropped", "scheduled turn intermediate response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted" });
|
|
15540
|
+
return false;
|
|
15541
|
+
}
|
|
15542
|
+
return await sendTextViaAvailableAccount(plainText, "scheduled.mid_send_failed");
|
|
15543
|
+
};
|
|
15544
|
+
const resolvedAccountId = deliveryAccountId ?? input.accountId ?? candidateAccountIds[0];
|
|
15545
|
+
let turn;
|
|
15546
|
+
try {
|
|
15547
|
+
turn = await executeChatTurn({
|
|
15548
|
+
agent: deps.agent,
|
|
15549
|
+
request: {
|
|
15550
|
+
accountId: resolvedAccountId,
|
|
15551
|
+
conversationId: input.chatKey,
|
|
15552
|
+
text: input.promptText,
|
|
15553
|
+
...deliveryContextToken ? { replyContextToken: deliveryContextToken } : {},
|
|
15554
|
+
...input.abortSignal ? { abortSignal: input.abortSignal } : {},
|
|
15555
|
+
metadata: { channel: "weixin", scheduledSessionAlias: input.sessionAlias }
|
|
15556
|
+
},
|
|
15557
|
+
onReplySegment: sendReplySegment
|
|
15558
|
+
});
|
|
15559
|
+
} catch (error2) {
|
|
15560
|
+
await sendReservedMidText(`定时任务执行失败:${error2 instanceof Error ? error2.message : String(error2)}`).catch(() => false);
|
|
15561
|
+
throw error2;
|
|
15562
|
+
}
|
|
15563
|
+
if (turn.text) {
|
|
15564
|
+
const finalText = markdownToPlainText(turn.text).trim();
|
|
15565
|
+
if (finalText.length > 0) {
|
|
15566
|
+
await sendFinalText(finalText);
|
|
15567
|
+
}
|
|
15568
|
+
}
|
|
15569
|
+
async function sendFinalText(finalText) {
|
|
15570
|
+
const rawChunks = chunkFinalText(finalText, 1800);
|
|
15571
|
+
if (rawChunks.length === 0)
|
|
15572
|
+
return;
|
|
15573
|
+
const total = rawChunks.length;
|
|
15574
|
+
const chunks = total === 1 ? rawChunks : rawChunks.map((body, index) => `(${index + 1}/${total}) ${body}`);
|
|
15575
|
+
const available = total === 1 ? 1 : Math.max(Math.min(deps.finalRemaining?.(quotaKey) ?? total, total), 0);
|
|
15576
|
+
const wave = chunks.slice(0, available);
|
|
15577
|
+
if (wave.length > 0 && wave.length < total) {
|
|
15578
|
+
wave[wave.length - 1] = `${wave[wave.length - 1]}
|
|
15579
|
+
|
|
15580
|
+
${buildFinalHeadsUp({
|
|
15581
|
+
total,
|
|
15582
|
+
sentSoFar: wave.length
|
|
15583
|
+
})}`;
|
|
15584
|
+
}
|
|
15585
|
+
let sent = 0;
|
|
15586
|
+
for (let index = 0;index < wave.length; index += 1) {
|
|
15587
|
+
if (!deps.reserveFinal(quotaKey)) {
|
|
15588
|
+
await deps.logger.info("scheduled.final_dropped", "scheduled turn final response dropped due to quota", { chatKey: input.chatKey, reason: "quota_exhausted", chunk: index + 1, total });
|
|
15589
|
+
break;
|
|
15590
|
+
}
|
|
15591
|
+
const delivered = await sendTextViaAvailableAccount(wave[index], "scheduled.final_send_failed");
|
|
15592
|
+
if (!delivered)
|
|
15593
|
+
break;
|
|
15594
|
+
sent += 1;
|
|
15595
|
+
}
|
|
15596
|
+
const restToPark = chunks.slice(sent);
|
|
15597
|
+
if (total > 1 && restToPark.length > 0 && deps.enqueuePendingFinal) {
|
|
15598
|
+
const pending = restToPark.map((text, index) => {
|
|
15599
|
+
const entry = { text, seq: sent + index + 1, total };
|
|
15600
|
+
if (deliveryContextToken)
|
|
15601
|
+
entry.contextToken = deliveryContextToken;
|
|
15602
|
+
if (deliveryAccountId)
|
|
15603
|
+
entry.accountId = deliveryAccountId;
|
|
15604
|
+
return entry;
|
|
15605
|
+
});
|
|
15606
|
+
deps.enqueuePendingFinal(quotaKey, pending);
|
|
15607
|
+
}
|
|
15608
|
+
}
|
|
15609
|
+
async function sendTextViaAvailableAccount(text, errorEvent) {
|
|
15610
|
+
const orderedAccountIds = [
|
|
15611
|
+
...deliveryAccountId ? [deliveryAccountId] : [],
|
|
15612
|
+
...candidateAccountIds.filter((accountId) => accountId !== deliveryAccountId)
|
|
15613
|
+
];
|
|
15614
|
+
for (const candidateAccountId of orderedAccountIds) {
|
|
15615
|
+
const contextToken = candidateAccountId === deliveryAccountId && deliveryContextToken ? deliveryContextToken : resolveContextToken(candidateAccountId);
|
|
15616
|
+
if (!contextToken)
|
|
15617
|
+
continue;
|
|
15618
|
+
const account = deps.resolveAccount(candidateAccountId);
|
|
15619
|
+
if (!account.token)
|
|
15620
|
+
continue;
|
|
15621
|
+
try {
|
|
15622
|
+
await sendMessage2({
|
|
15623
|
+
to: userId,
|
|
15624
|
+
text,
|
|
15625
|
+
opts: { baseUrl: account.baseUrl, token: account.token, contextToken }
|
|
15626
|
+
});
|
|
15627
|
+
return true;
|
|
15628
|
+
} catch (error2) {
|
|
15629
|
+
await deps.logger.error(errorEvent, "failed to send scheduled response text", { chatKey: input.chatKey, accountId: candidateAccountId, error: String(error2) });
|
|
15630
|
+
}
|
|
15631
|
+
}
|
|
15632
|
+
return false;
|
|
15633
|
+
}
|
|
15634
|
+
}
|
|
15635
|
+
var init_scheduled_turn = __esm(() => {
|
|
15636
|
+
init_handle_weixin_message_turn();
|
|
15637
|
+
init_send();
|
|
15638
|
+
init_inbound();
|
|
15639
|
+
});
|
|
15640
|
+
|
|
15445
15641
|
// src/weixin/monitor/consumer-lock.ts
|
|
15446
15642
|
import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
|
|
15447
15643
|
import { dirname as dirname8, join as join5 } from "node:path";
|
|
@@ -15568,6 +15764,7 @@ var init_consumer_lock = __esm(() => {
|
|
|
15568
15764
|
// src/channels/weixin-channel.ts
|
|
15569
15765
|
class WeixinChannel {
|
|
15570
15766
|
id = "weixin";
|
|
15767
|
+
agent = null;
|
|
15571
15768
|
quota = null;
|
|
15572
15769
|
logger = null;
|
|
15573
15770
|
markDelivered = null;
|
|
@@ -15598,6 +15795,7 @@ class WeixinChannel {
|
|
|
15598
15795
|
this.markFailed = callbacks.markTaskNoticeFailed;
|
|
15599
15796
|
}
|
|
15600
15797
|
async start(input) {
|
|
15798
|
+
this.agent = input.agent;
|
|
15601
15799
|
this.quota = input.quota;
|
|
15602
15800
|
this.logger = input.logger;
|
|
15603
15801
|
if (!this.isLoggedIn()) {
|
|
@@ -15658,6 +15856,23 @@ class WeixinChannel {
|
|
|
15658
15856
|
logger: this.logger
|
|
15659
15857
|
});
|
|
15660
15858
|
}
|
|
15859
|
+
async sendScheduledMessage(input) {
|
|
15860
|
+
if (!this.agent || !this.quota || !this.logger) {
|
|
15861
|
+
throw new Error("WeixinChannel.start() must be called before scheduled message delivery");
|
|
15862
|
+
}
|
|
15863
|
+
await executeScheduledTurn(input, {
|
|
15864
|
+
agent: this.agent,
|
|
15865
|
+
listAccountIds: () => listWeixinAccountIds(),
|
|
15866
|
+
resolveAccount: (accountId) => resolveWeixinAccount(accountId),
|
|
15867
|
+
getContextToken: (accountId, userId) => getContextToken(accountId, userId),
|
|
15868
|
+
reserveMidSegment: (chatKey) => this.quota.reserveMidSegment(chatKey),
|
|
15869
|
+
reserveFinal: (chatKey) => this.quota.reserveFinal(chatKey),
|
|
15870
|
+
finalRemaining: (chatKey) => this.quota.finalRemaining(chatKey),
|
|
15871
|
+
enqueuePendingFinal: (chatKey, chunks) => this.quota.enqueuePendingFinal(chatKey, chunks),
|
|
15872
|
+
sendMessage: sendMessageWeixin,
|
|
15873
|
+
logger: this.logger
|
|
15874
|
+
});
|
|
15875
|
+
}
|
|
15661
15876
|
}
|
|
15662
15877
|
var init_weixin_channel = __esm(() => {
|
|
15663
15878
|
init_weixin();
|
|
@@ -15665,6 +15880,7 @@ var init_weixin_channel = __esm(() => {
|
|
|
15665
15880
|
init_deliver_orchestration_task_notice();
|
|
15666
15881
|
init_deliver_orchestration_task_progress();
|
|
15667
15882
|
init_deliver_coordinator_message();
|
|
15883
|
+
init_scheduled_turn();
|
|
15668
15884
|
init_consumer_lock();
|
|
15669
15885
|
});
|
|
15670
15886
|
|
|
@@ -16049,7 +16265,7 @@ function validatePluginCompatibility(metadata, context) {
|
|
|
16049
16265
|
}
|
|
16050
16266
|
}
|
|
16051
16267
|
}
|
|
16052
|
-
var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.
|
|
16268
|
+
var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.5.0", SEMVER_RE;
|
|
16053
16269
|
var init_compatibility = __esm(() => {
|
|
16054
16270
|
WEACPX_PLUGIN_API_SUPPORTED_VERSIONS = [1];
|
|
16055
16271
|
SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
@@ -16435,7 +16651,9 @@ var init_command_list = __esm(() => {
|
|
|
16435
16651
|
"/dg",
|
|
16436
16652
|
"/group",
|
|
16437
16653
|
"/groups",
|
|
16438
|
-
"/task"
|
|
16654
|
+
"/task",
|
|
16655
|
+
"/later",
|
|
16656
|
+
"/lt"
|
|
16439
16657
|
];
|
|
16440
16658
|
KNOWN_COMMAND_PREFIX_SET = new Set(WEACPX_KNOWN_COMMAND_PREFIXES);
|
|
16441
16659
|
});
|
|
@@ -16603,6 +16821,16 @@ function parseCommand(input) {
|
|
|
16603
16821
|
} else if (command === "/task" && parts[1] && parts.length === 2) {
|
|
16604
16822
|
return { kind: "task.get", taskId: parts[1] };
|
|
16605
16823
|
}
|
|
16824
|
+
if (command === "/later") {
|
|
16825
|
+
if (parts.length === 1)
|
|
16826
|
+
return { kind: "later.help" };
|
|
16827
|
+
if (parts[1] === "list" && parts.length === 2)
|
|
16828
|
+
return { kind: "later.list" };
|
|
16829
|
+
if (parts[1] === "cancel" && parts[2] && parts.length === 3) {
|
|
16830
|
+
return { kind: "later.cancel", id: parts[2] };
|
|
16831
|
+
}
|
|
16832
|
+
return { kind: "later.create", tokens: parts.slice(1) };
|
|
16833
|
+
}
|
|
16606
16834
|
if (command === "/workspace" && parts[1] === "new" && parts[2]) {
|
|
16607
16835
|
const name = parts[2];
|
|
16608
16836
|
let cwd = "";
|
|
@@ -16771,6 +16999,8 @@ function normalizeCommand(command) {
|
|
|
16771
16999
|
return "/permission";
|
|
16772
17000
|
if (command === "/stop")
|
|
16773
17001
|
return "/cancel";
|
|
17002
|
+
if (command === "/lt")
|
|
17003
|
+
return "/later";
|
|
16774
17004
|
return command;
|
|
16775
17005
|
}
|
|
16776
17006
|
function isRecognizedCommand(command) {
|
|
@@ -16966,6 +17196,7 @@ var init_command_policy = __esm(() => {
|
|
|
16966
17196
|
"group.get",
|
|
16967
17197
|
"tasks",
|
|
16968
17198
|
"task.get",
|
|
17199
|
+
"later.help",
|
|
16969
17200
|
"invalid",
|
|
16970
17201
|
"prompt"
|
|
16971
17202
|
]);
|
|
@@ -16995,7 +17226,10 @@ var init_command_policy = __esm(() => {
|
|
|
16995
17226
|
"session.new": "/session new",
|
|
16996
17227
|
"session.shortcut": "/session",
|
|
16997
17228
|
"session.shortcut.new": "/session",
|
|
16998
|
-
"session.attach": "/session attach"
|
|
17229
|
+
"session.attach": "/session attach",
|
|
17230
|
+
"later.create": "/later",
|
|
17231
|
+
"later.list": "/later list",
|
|
17232
|
+
"later.cancel": "/later cancel"
|
|
16999
17233
|
};
|
|
17000
17234
|
});
|
|
17001
17235
|
|
|
@@ -17949,11 +18183,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
|
|
|
17949
18183
|
throw error2;
|
|
17950
18184
|
}
|
|
17951
18185
|
}
|
|
17952
|
-
async function
|
|
17953
|
-
const session = await context.sessions.getCurrentSession(chatKey);
|
|
17954
|
-
if (!session) {
|
|
17955
|
-
return { text: NO_CURRENT_SESSION_TEXT };
|
|
17956
|
-
}
|
|
18186
|
+
async function handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
|
|
17957
18187
|
try {
|
|
17958
18188
|
return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
|
|
17959
18189
|
} catch (error2) {
|
|
@@ -17964,6 +18194,13 @@ async function handlePrompt(context, chatKey, text, reply, replyContextToken, ac
|
|
|
17964
18194
|
return context.recovery.renderTransportError(session, error2);
|
|
17965
18195
|
}
|
|
17966
18196
|
}
|
|
18197
|
+
async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan) {
|
|
18198
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
18199
|
+
if (!session) {
|
|
18200
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
18201
|
+
}
|
|
18202
|
+
return await handlePromptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
|
|
18203
|
+
}
|
|
17967
18204
|
async function preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId) {
|
|
17968
18205
|
const orchestration = context.orchestration;
|
|
17969
18206
|
if (!orchestration) {
|
|
@@ -18725,6 +18962,333 @@ var init_workspace_handler = __esm(() => {
|
|
|
18725
18962
|
};
|
|
18726
18963
|
});
|
|
18727
18964
|
|
|
18965
|
+
// src/scheduled/scheduled-types.ts
|
|
18966
|
+
var LATER_MIN_DELAY_MS = 1e4, LATER_MAX_DELAY_MS, LATER_MESSAGE_PREVIEW_CHARS = 120;
|
|
18967
|
+
var init_scheduled_types = __esm(() => {
|
|
18968
|
+
LATER_MAX_DELAY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18969
|
+
});
|
|
18970
|
+
|
|
18971
|
+
// src/scheduled/parse-later-time.ts
|
|
18972
|
+
function parseLaterTime(tokens, now = new Date) {
|
|
18973
|
+
if (tokens.length === 0)
|
|
18974
|
+
return { ok: false, code: "missing_time" };
|
|
18975
|
+
const relative = parseRelative(tokens, now);
|
|
18976
|
+
if (relative)
|
|
18977
|
+
return validateResult(relative.executeAt, relative.messageStartIndex, tokens, now);
|
|
18978
|
+
const absolute = parseAbsolute(tokens, now);
|
|
18979
|
+
if (absolute)
|
|
18980
|
+
return validateResult(absolute.executeAt, absolute.messageStartIndex, tokens, now, absolute.pastTodayValue);
|
|
18981
|
+
return { ok: false, code: "unrecognized_time" };
|
|
18982
|
+
}
|
|
18983
|
+
function parseRelative(tokens, now) {
|
|
18984
|
+
if (tokens[0] === "in" && tokens[1]) {
|
|
18985
|
+
const ms = parseDuration(tokens[1]);
|
|
18986
|
+
if (ms !== null)
|
|
18987
|
+
return { executeAt: new Date(now.getTime() + ms), messageStartIndex: 2 };
|
|
18988
|
+
}
|
|
18989
|
+
const zh = /^(\d+)(分钟|小时|天)后$/.exec(tokens[0] ?? "");
|
|
18990
|
+
if (zh) {
|
|
18991
|
+
const amount = Number(zh[1]);
|
|
18992
|
+
const unit = zh[2];
|
|
18993
|
+
const ms = unit === "分钟" ? amount * 60000 : unit === "小时" ? amount * 3600000 : amount * 86400000;
|
|
18994
|
+
return { executeAt: new Date(now.getTime() + ms), messageStartIndex: 1 };
|
|
18995
|
+
}
|
|
18996
|
+
return null;
|
|
18997
|
+
}
|
|
18998
|
+
function parseDuration(value) {
|
|
18999
|
+
const match = /^(\d+)(m|min|minute|minutes|h|hour|hours|d|day|days)$/.exec(value.toLowerCase());
|
|
19000
|
+
if (!match)
|
|
19001
|
+
return null;
|
|
19002
|
+
const amount = Number(match[1]);
|
|
19003
|
+
const unit = match[2];
|
|
19004
|
+
if (unit === "m" || unit === "min" || unit === "minute" || unit === "minutes")
|
|
19005
|
+
return amount * 60000;
|
|
19006
|
+
if (unit === "h" || unit === "hour" || unit === "hours")
|
|
19007
|
+
return amount * 3600000;
|
|
19008
|
+
return amount * 86400000;
|
|
19009
|
+
}
|
|
19010
|
+
function parseAbsolute(tokens, now) {
|
|
19011
|
+
if (tokens[0] === "at" && tokens[1]) {
|
|
19012
|
+
const parsed = parseClock(tokens[1]);
|
|
19013
|
+
if (!parsed)
|
|
19014
|
+
return null;
|
|
19015
|
+
const executeAt = atLocalDate(now, 0, parsed.hour, parsed.minute);
|
|
19016
|
+
if (executeAt.getTime() <= now.getTime())
|
|
19017
|
+
return { executeAt, messageStartIndex: 2, pastTodayValue: tokens[1] };
|
|
19018
|
+
return { executeAt, messageStartIndex: 2 };
|
|
19019
|
+
}
|
|
19020
|
+
const dayWord = tokens[0]?.toLowerCase();
|
|
19021
|
+
const dayOffset = dayWord === "today" || dayWord === "今天" ? 0 : dayWord === "tomorrow" || dayWord === "明天" ? 1 : dayWord === "后天" ? 2 : null;
|
|
19022
|
+
if (dayOffset !== null && tokens[1]) {
|
|
19023
|
+
const parsed = parseClock(tokens[1]);
|
|
19024
|
+
if (!parsed)
|
|
19025
|
+
return null;
|
|
19026
|
+
const executeAt = atLocalDate(now, dayOffset, parsed.hour, parsed.minute);
|
|
19027
|
+
if (dayOffset === 0 && executeAt.getTime() <= now.getTime())
|
|
19028
|
+
return { executeAt, messageStartIndex: 2, pastTodayValue: tokens[1] };
|
|
19029
|
+
return { executeAt, messageStartIndex: 2 };
|
|
19030
|
+
}
|
|
19031
|
+
const weekday = WEEKDAYS.get(tokens[0]?.toLowerCase() ?? "");
|
|
19032
|
+
if (weekday !== undefined && tokens[1]) {
|
|
19033
|
+
const parsed = parseClock(tokens[1]);
|
|
19034
|
+
if (!parsed)
|
|
19035
|
+
return null;
|
|
19036
|
+
let days = (weekday - now.getDay() + 7) % 7;
|
|
19037
|
+
let executeAt = atLocalDate(now, days, parsed.hour, parsed.minute);
|
|
19038
|
+
if (days === 0 && executeAt.getTime() <= now.getTime()) {
|
|
19039
|
+
days = 7;
|
|
19040
|
+
executeAt = atLocalDate(now, days, parsed.hour, parsed.minute);
|
|
19041
|
+
}
|
|
19042
|
+
return { executeAt, messageStartIndex: 2 };
|
|
19043
|
+
}
|
|
19044
|
+
return null;
|
|
19045
|
+
}
|
|
19046
|
+
function parseClock(value) {
|
|
19047
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value);
|
|
19048
|
+
if (!match)
|
|
19049
|
+
return null;
|
|
19050
|
+
const hour = Number(match[1]);
|
|
19051
|
+
const minute = Number(match[2]);
|
|
19052
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
19053
|
+
return null;
|
|
19054
|
+
return { hour, minute };
|
|
19055
|
+
}
|
|
19056
|
+
function atLocalDate(now, dayOffset, hour, minute) {
|
|
19057
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayOffset, hour, minute, 0, 0);
|
|
19058
|
+
}
|
|
19059
|
+
function validateResult(executeAt, messageStartIndex, tokens, now, pastTodayValue) {
|
|
19060
|
+
if (pastTodayValue)
|
|
19061
|
+
return { ok: false, code: "past_today_time", value: pastTodayValue };
|
|
19062
|
+
if (tokens.slice(messageStartIndex).join(" ").trim().length === 0)
|
|
19063
|
+
return { ok: false, code: "missing_message" };
|
|
19064
|
+
const delta = executeAt.getTime() - now.getTime();
|
|
19065
|
+
if (delta < LATER_MIN_DELAY_MS)
|
|
19066
|
+
return { ok: false, code: "too_soon" };
|
|
19067
|
+
if (delta > LATER_MAX_DELAY_MS)
|
|
19068
|
+
return { ok: false, code: "out_of_range" };
|
|
19069
|
+
return { ok: true, executeAt, messageStartIndex };
|
|
19070
|
+
}
|
|
19071
|
+
var WEEKDAYS;
|
|
19072
|
+
var init_parse_later_time = __esm(() => {
|
|
19073
|
+
init_scheduled_types();
|
|
19074
|
+
WEEKDAYS = new Map([
|
|
19075
|
+
["周日", 0],
|
|
19076
|
+
["周天", 0],
|
|
19077
|
+
["星期日", 0],
|
|
19078
|
+
["星期天", 0],
|
|
19079
|
+
["sun", 0],
|
|
19080
|
+
["sunday", 0],
|
|
19081
|
+
["周一", 1],
|
|
19082
|
+
["星期一", 1],
|
|
19083
|
+
["mon", 1],
|
|
19084
|
+
["monday", 1],
|
|
19085
|
+
["周二", 2],
|
|
19086
|
+
["星期二", 2],
|
|
19087
|
+
["tue", 2],
|
|
19088
|
+
["tuesday", 2],
|
|
19089
|
+
["周三", 3],
|
|
19090
|
+
["星期三", 3],
|
|
19091
|
+
["wed", 3],
|
|
19092
|
+
["wednesday", 3],
|
|
19093
|
+
["周四", 4],
|
|
19094
|
+
["星期四", 4],
|
|
19095
|
+
["thu", 4],
|
|
19096
|
+
["thursday", 4],
|
|
19097
|
+
["周五", 5],
|
|
19098
|
+
["星期五", 5],
|
|
19099
|
+
["fri", 5],
|
|
19100
|
+
["friday", 5],
|
|
19101
|
+
["周六", 6],
|
|
19102
|
+
["星期六", 6],
|
|
19103
|
+
["sat", 6],
|
|
19104
|
+
["saturday", 6]
|
|
19105
|
+
]);
|
|
19106
|
+
});
|
|
19107
|
+
|
|
19108
|
+
// src/scheduled/scheduled-render.ts
|
|
19109
|
+
function renderLaterHelp() {
|
|
19110
|
+
return [
|
|
19111
|
+
"定时任务用法:",
|
|
19112
|
+
"",
|
|
19113
|
+
"创建:",
|
|
19114
|
+
"/lt in 2h 检查 CI",
|
|
19115
|
+
"/lt 30分钟后 总结进展",
|
|
19116
|
+
"/lt tomorrow 09:00 看 PR",
|
|
19117
|
+
"/lt 周五 09:00 继续处理",
|
|
19118
|
+
"",
|
|
19119
|
+
"查看:",
|
|
19120
|
+
"/lt list",
|
|
19121
|
+
"",
|
|
19122
|
+
"取消:",
|
|
19123
|
+
"/lt cancel <id>",
|
|
19124
|
+
"",
|
|
19125
|
+
"说明:",
|
|
19126
|
+
"- 只支持一次性任务",
|
|
19127
|
+
"- 时间必须在 10 秒之后、7 天之内",
|
|
19128
|
+
"- 到点后会把消息发送到创建时绑定的会话",
|
|
19129
|
+
"- 触发通知和 agent 回复复用现有频道路由;微信回复额度由现有路由控制",
|
|
19130
|
+
"- 不支持延迟执行 / 开头的 weacpx 命令",
|
|
19131
|
+
"- 完整时间格式与说明见 docs/later-command.md"
|
|
19132
|
+
].join(`
|
|
19133
|
+
`);
|
|
19134
|
+
}
|
|
19135
|
+
function renderLaterUnsupportedChannel() {
|
|
19136
|
+
return [
|
|
19137
|
+
"当前频道暂不支持定时任务,未创建任务。",
|
|
19138
|
+
"",
|
|
19139
|
+
"原因:这个频道还没有实现定时消息投递能力,任务到点后无法把结果发回原聊天。",
|
|
19140
|
+
"请切换到支持定时任务的频道后再使用 /lt。"
|
|
19141
|
+
].join(`
|
|
19142
|
+
`);
|
|
19143
|
+
}
|
|
19144
|
+
function renderTaskCreated(task, displaySession) {
|
|
19145
|
+
return [
|
|
19146
|
+
`已创建定时任务 #${task.id}`,
|
|
19147
|
+
`执行时间:${formatLocalDateTime(new Date(task.execute_at))}`,
|
|
19148
|
+
`会话:${displaySession}`,
|
|
19149
|
+
`内容:${preview(task.message)}`
|
|
19150
|
+
].join(`
|
|
19151
|
+
`);
|
|
19152
|
+
}
|
|
19153
|
+
function renderLaterList(tasks, displaySession) {
|
|
19154
|
+
if (tasks.length === 0)
|
|
19155
|
+
return "当前没有待执行定时任务。";
|
|
19156
|
+
return [
|
|
19157
|
+
"待执行定时任务:",
|
|
19158
|
+
"",
|
|
19159
|
+
...tasks.flatMap((task) => [
|
|
19160
|
+
`#${task.id} ${formatLocalDateTime(new Date(task.execute_at))} 会话:${displaySession(task.session_alias)}`,
|
|
19161
|
+
preview(task.message),
|
|
19162
|
+
""
|
|
19163
|
+
])
|
|
19164
|
+
].join(`
|
|
19165
|
+
`).trimEnd();
|
|
19166
|
+
}
|
|
19167
|
+
function preview(text) {
|
|
19168
|
+
return text.length <= LATER_MESSAGE_PREVIEW_CHARS ? text : `${text.slice(0, LATER_MESSAGE_PREVIEW_CHARS - 1)}…`;
|
|
19169
|
+
}
|
|
19170
|
+
function formatLocalDateTime(date4) {
|
|
19171
|
+
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
19172
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
19173
|
+
return `${date4.getFullYear()}-${pad(date4.getMonth() + 1)}-${pad(date4.getDate())} ${weekdays[date4.getDay()]} ${pad(date4.getHours())}:${pad(date4.getMinutes())}`;
|
|
19174
|
+
}
|
|
19175
|
+
var init_scheduled_render = __esm(() => {
|
|
19176
|
+
init_scheduled_types();
|
|
19177
|
+
});
|
|
19178
|
+
|
|
19179
|
+
// src/commands/handlers/later-handler.ts
|
|
19180
|
+
function handleLaterHelp() {
|
|
19181
|
+
return { text: renderLaterHelp() };
|
|
19182
|
+
}
|
|
19183
|
+
async function handleLaterCreate(tokens, scheduled, chatKey, currentSessionAlias, accountId, replyContextToken) {
|
|
19184
|
+
if (!currentSessionAlias) {
|
|
19185
|
+
return {
|
|
19186
|
+
text: [
|
|
19187
|
+
"当前没有会话,无法创建定时任务。",
|
|
19188
|
+
"",
|
|
19189
|
+
"请先创建或切换到一个会话:",
|
|
19190
|
+
"- /ss codex --ws backend(新建并切换)",
|
|
19191
|
+
"- /use backend-codex(切换到已有会话)"
|
|
19192
|
+
].join(`
|
|
19193
|
+
`)
|
|
19194
|
+
};
|
|
19195
|
+
}
|
|
19196
|
+
const result = parseLaterTime(tokens);
|
|
19197
|
+
if (!result.ok) {
|
|
19198
|
+
return { text: renderTimeParseError(result.code, result.value) };
|
|
19199
|
+
}
|
|
19200
|
+
const message = tokens.slice(result.messageStartIndex).join(" ").trim();
|
|
19201
|
+
if (message.startsWith("/")) {
|
|
19202
|
+
return {
|
|
19203
|
+
text: [
|
|
19204
|
+
"不支持延迟执行 / 开头的命令。",
|
|
19205
|
+
"",
|
|
19206
|
+
"如果需要让 agent 解释命令,可以用自然语言描述:",
|
|
19207
|
+
"例如:/lt in 1h 请解释 /status 的作用"
|
|
19208
|
+
].join(`
|
|
19209
|
+
`)
|
|
19210
|
+
};
|
|
19211
|
+
}
|
|
19212
|
+
const task = await scheduled.createTask({
|
|
19213
|
+
chatKey,
|
|
19214
|
+
sessionAlias: currentSessionAlias,
|
|
19215
|
+
executeAt: result.executeAt,
|
|
19216
|
+
message,
|
|
19217
|
+
...accountId ? { accountId } : {},
|
|
19218
|
+
...replyContextToken ? { replyContextToken } : {}
|
|
19219
|
+
});
|
|
19220
|
+
return { text: renderTaskCreated(task, toDisplaySessionAlias(currentSessionAlias)) };
|
|
19221
|
+
}
|
|
19222
|
+
function handleLaterList(scheduled) {
|
|
19223
|
+
const tasks = scheduled.listPending();
|
|
19224
|
+
return { text: renderLaterList(tasks, (alias) => toDisplaySessionAlias(alias)) };
|
|
19225
|
+
}
|
|
19226
|
+
async function handleLaterCancel(id, scheduled) {
|
|
19227
|
+
const ok = await scheduled.cancelPending(id);
|
|
19228
|
+
if (ok) {
|
|
19229
|
+
return { text: `已取消定时任务 #${id.replace(/^#/, "").toLowerCase()}` };
|
|
19230
|
+
}
|
|
19231
|
+
const displayId = id.replace(/^#/, "").toLowerCase();
|
|
19232
|
+
return { text: [`未找到待执行的定时任务 #${displayId}。`, "可以用 /lt list 查看当前待执行任务。"].join(`
|
|
19233
|
+
`) };
|
|
19234
|
+
}
|
|
19235
|
+
function renderTimeParseError(code, value) {
|
|
19236
|
+
switch (code) {
|
|
19237
|
+
case "missing_message":
|
|
19238
|
+
return "定时任务需要消息内容,请在时间后附上要发送的内容。";
|
|
19239
|
+
case "too_soon":
|
|
19240
|
+
return "定时任务执行时间必须在 10 秒之后。";
|
|
19241
|
+
case "out_of_range":
|
|
19242
|
+
return "定时任务执行时间不能超过 7 天。";
|
|
19243
|
+
case "past_today_time":
|
|
19244
|
+
return `今天 ${value} 已经过了,请指定一个未来的时间,或使用「明天」。`;
|
|
19245
|
+
case "unrecognized_time":
|
|
19246
|
+
case "missing_time":
|
|
19247
|
+
default:
|
|
19248
|
+
return [
|
|
19249
|
+
"无法识别时间格式。",
|
|
19250
|
+
"",
|
|
19251
|
+
"支持的格式:",
|
|
19252
|
+
"- /lt in 2h 消息(2小时后)",
|
|
19253
|
+
"- /lt 30分钟后 消息",
|
|
19254
|
+
"- /lt tomorrow 09:00 消息",
|
|
19255
|
+
"- /lt 周五 09:00 消息"
|
|
19256
|
+
].join(`
|
|
19257
|
+
`);
|
|
19258
|
+
}
|
|
19259
|
+
}
|
|
19260
|
+
var laterHelpMetadata;
|
|
19261
|
+
var init_later_handler = __esm(() => {
|
|
19262
|
+
init_parse_later_time();
|
|
19263
|
+
init_scheduled_render();
|
|
19264
|
+
init_channel_scope();
|
|
19265
|
+
laterHelpMetadata = {
|
|
19266
|
+
topic: "later",
|
|
19267
|
+
aliases: ["lt"],
|
|
19268
|
+
summary: "定时任务:延时发送消息到当前会话",
|
|
19269
|
+
commands: [
|
|
19270
|
+
{ usage: "/lt <时间> <消息>", description: "创建定时任务" },
|
|
19271
|
+
{ usage: "/lt list", description: "查看待执行定时任务" },
|
|
19272
|
+
{ usage: "/lt cancel <id>", description: "取消定时任务" }
|
|
19273
|
+
],
|
|
19274
|
+
examples: [
|
|
19275
|
+
"/lt in 2h 检查 CI",
|
|
19276
|
+
"/lt 30分钟后 总结进展",
|
|
19277
|
+
"/lt tomorrow 09:00 看 PR",
|
|
19278
|
+
"/lt 今天 21:30 继续处理",
|
|
19279
|
+
"/lt 周五 09:00 继续处理"
|
|
19280
|
+
],
|
|
19281
|
+
notes: [
|
|
19282
|
+
"只支持一次性任务,不支持重复执行",
|
|
19283
|
+
"时间必须在 10 秒之后、7 天之内",
|
|
19284
|
+
"到点后会把消息发送到创建时绑定的会话(不随之后 /use 切换而改变)",
|
|
19285
|
+
"/lt list 显示全局待执行任务;群聊中只有群主可取消",
|
|
19286
|
+
"不支持延迟执行 / 开头的 weacpx 命令",
|
|
19287
|
+
"完整时间格式与说明见 docs/later-command.md"
|
|
19288
|
+
]
|
|
19289
|
+
};
|
|
19290
|
+
});
|
|
19291
|
+
|
|
18728
19292
|
// src/commands/help/help-registry.ts
|
|
18729
19293
|
function getHelpTopic(topic) {
|
|
18730
19294
|
return HELP_TOPIC_MAP.get(topic) ?? null;
|
|
@@ -18740,6 +19304,7 @@ var init_help_registry = __esm(() => {
|
|
|
18740
19304
|
init_permission_handler();
|
|
18741
19305
|
init_session_handler();
|
|
18742
19306
|
init_workspace_handler();
|
|
19307
|
+
init_later_handler();
|
|
18743
19308
|
HELP_TOPICS = [
|
|
18744
19309
|
sessionHelp,
|
|
18745
19310
|
workspaceHelp,
|
|
@@ -18750,7 +19315,8 @@ var init_help_registry = __esm(() => {
|
|
|
18750
19315
|
modeHelp,
|
|
18751
19316
|
replyModeHelp,
|
|
18752
19317
|
statusHelp,
|
|
18753
|
-
cancelHelp
|
|
19318
|
+
cancelHelp,
|
|
19319
|
+
laterHelpMetadata
|
|
18754
19320
|
];
|
|
18755
19321
|
HELP_TOPIC_MAP = new Map;
|
|
18756
19322
|
for (const topic of HELP_TOPICS) {
|
|
@@ -18797,7 +19363,8 @@ function renderHelpTopic(topic) {
|
|
|
18797
19363
|
"",
|
|
18798
19364
|
"命令:",
|
|
18799
19365
|
...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
|
|
18800
|
-
...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : []
|
|
19366
|
+
...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : [],
|
|
19367
|
+
...topic.notes && topic.notes.length > 0 ? ["", "注意:", ...topic.notes.map((note) => `- ${note}`)] : []
|
|
18801
19368
|
].join(`
|
|
18802
19369
|
`);
|
|
18803
19370
|
}
|
|
@@ -19448,6 +20015,8 @@ class CommandRouter {
|
|
|
19448
20015
|
resolveSessionAgentCommand;
|
|
19449
20016
|
orchestration;
|
|
19450
20017
|
quota;
|
|
20018
|
+
scheduled;
|
|
20019
|
+
scheduledDelivery;
|
|
19451
20020
|
logger;
|
|
19452
20021
|
autoInstall = autoInstallOptionalDep;
|
|
19453
20022
|
discoverPaths = discoverParentPackagePaths;
|
|
@@ -19457,7 +20026,7 @@ class CommandRouter {
|
|
|
19457
20026
|
__setDiscoverPathsForTest(fn) {
|
|
19458
20027
|
this.discoverPaths = fn;
|
|
19459
20028
|
}
|
|
19460
|
-
constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota) {
|
|
20029
|
+
constructor(sessions, transport, config2, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex, orchestration, quota, scheduled, scheduledDelivery) {
|
|
19461
20030
|
this.sessions = sessions;
|
|
19462
20031
|
this.transport = transport;
|
|
19463
20032
|
this.config = config2;
|
|
@@ -19465,6 +20034,8 @@ class CommandRouter {
|
|
|
19465
20034
|
this.resolveSessionAgentCommand = resolveSessionAgentCommand;
|
|
19466
20035
|
this.orchestration = orchestration;
|
|
19467
20036
|
this.quota = quota;
|
|
20037
|
+
this.scheduled = scheduled;
|
|
20038
|
+
this.scheduledDelivery = scheduledDelivery;
|
|
19468
20039
|
this.logger = logger2 ?? createNoopAppLogger();
|
|
19469
20040
|
}
|
|
19470
20041
|
async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan) {
|
|
@@ -19585,8 +20156,38 @@ class CommandRouter {
|
|
|
19585
20156
|
return await handleTaskReject(this.createHandlerContext(), chatKey, command.taskId);
|
|
19586
20157
|
case "task.cancel":
|
|
19587
20158
|
return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
|
|
19588
|
-
case "
|
|
19589
|
-
|
|
20159
|
+
case "later.help":
|
|
20160
|
+
if (!this.scheduled)
|
|
20161
|
+
return { text: "定时任务服务未启用。" };
|
|
20162
|
+
return handleLaterHelp();
|
|
20163
|
+
case "later.list":
|
|
20164
|
+
if (!this.scheduled)
|
|
20165
|
+
return { text: "定时任务服务未启用。" };
|
|
20166
|
+
return handleLaterList(this.scheduled);
|
|
20167
|
+
case "later.create": {
|
|
20168
|
+
if (!this.scheduled)
|
|
20169
|
+
return { text: "定时任务服务未启用。" };
|
|
20170
|
+
if (this.scheduledDelivery && !this.scheduledDelivery.supportsScheduledMessages(chatKey)) {
|
|
20171
|
+
return { text: renderLaterUnsupportedChannel() };
|
|
20172
|
+
}
|
|
20173
|
+
const currentSession = await this.sessions.getCurrentSession(chatKey);
|
|
20174
|
+
return await handleLaterCreate(command.tokens, this.scheduled, chatKey, currentSession?.alias ?? null, accountId, replyContextToken);
|
|
20175
|
+
}
|
|
20176
|
+
case "later.cancel":
|
|
20177
|
+
if (!this.scheduled)
|
|
20178
|
+
return { text: "定时任务服务未启用。" };
|
|
20179
|
+
return await handleLaterCancel(command.id, this.scheduled);
|
|
20180
|
+
case "prompt": {
|
|
20181
|
+
const sessionContext = this.createSessionHandlerContext(undefined, perfSpan);
|
|
20182
|
+
if (metadata?.scheduledSessionAlias) {
|
|
20183
|
+
const scheduledSession = await this.sessions.getSession(metadata.scheduledSessionAlias);
|
|
20184
|
+
if (!scheduledSession) {
|
|
20185
|
+
throw new Error(`session "${metadata.scheduledSessionAlias}" not found for scheduled prompt`);
|
|
20186
|
+
}
|
|
20187
|
+
return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
|
|
20188
|
+
}
|
|
20189
|
+
return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan);
|
|
20190
|
+
}
|
|
19590
20191
|
}
|
|
19591
20192
|
});
|
|
19592
20193
|
}
|
|
@@ -19964,11 +20565,13 @@ var init_command_router = __esm(() => {
|
|
|
19964
20565
|
init_agent_handler();
|
|
19965
20566
|
init_workspace_handler();
|
|
19966
20567
|
init_session_shortcut_handler();
|
|
20568
|
+
init_later_handler();
|
|
19967
20569
|
init_session_recovery_handler();
|
|
19968
20570
|
init_auto_install_optional_dep();
|
|
19969
20571
|
init_discover_parent_package_paths();
|
|
19970
20572
|
init_errors();
|
|
19971
20573
|
init_session_reset_handler();
|
|
20574
|
+
init_scheduled_render();
|
|
19972
20575
|
});
|
|
19973
20576
|
|
|
19974
20577
|
// src/config/resolve-acpx-command.ts
|
|
@@ -23965,6 +24568,220 @@ function buildWorkerAnswerPrompt(answer) {
|
|
|
23965
24568
|
`);
|
|
23966
24569
|
}
|
|
23967
24570
|
|
|
24571
|
+
// src/scheduled/scheduled-scheduler.ts
|
|
24572
|
+
class ScheduledTaskScheduler {
|
|
24573
|
+
service;
|
|
24574
|
+
intervalMs;
|
|
24575
|
+
dispatchTimeoutMs;
|
|
24576
|
+
setIntervalFn;
|
|
24577
|
+
clearIntervalFn;
|
|
24578
|
+
dispatchTask;
|
|
24579
|
+
logger;
|
|
24580
|
+
intervalHandle = null;
|
|
24581
|
+
ticking = false;
|
|
24582
|
+
constructor(service, deps) {
|
|
24583
|
+
this.service = service;
|
|
24584
|
+
this.dispatchTask = deps.dispatchTask;
|
|
24585
|
+
this.intervalMs = deps.intervalMs ?? 5000;
|
|
24586
|
+
this.dispatchTimeoutMs = deps.dispatchTimeoutMs ?? DEFAULT_DISPATCH_TIMEOUT_MS;
|
|
24587
|
+
this.setIntervalFn = deps.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
|
|
24588
|
+
this.clearIntervalFn = deps.clearIntervalFn ?? ((timer) => clearInterval(timer));
|
|
24589
|
+
this.logger = deps.logger;
|
|
24590
|
+
}
|
|
24591
|
+
async start() {
|
|
24592
|
+
if (this.intervalHandle !== null)
|
|
24593
|
+
return;
|
|
24594
|
+
await this.service.markStartupMissed();
|
|
24595
|
+
this.intervalHandle = this.setIntervalFn(() => {
|
|
24596
|
+
this.tick();
|
|
24597
|
+
}, this.intervalMs);
|
|
24598
|
+
await this.tick();
|
|
24599
|
+
}
|
|
24600
|
+
stop() {
|
|
24601
|
+
if (this.intervalHandle !== null) {
|
|
24602
|
+
this.clearIntervalFn(this.intervalHandle);
|
|
24603
|
+
this.intervalHandle = null;
|
|
24604
|
+
}
|
|
24605
|
+
}
|
|
24606
|
+
async tick() {
|
|
24607
|
+
if (this.ticking)
|
|
24608
|
+
return;
|
|
24609
|
+
this.ticking = true;
|
|
24610
|
+
try {
|
|
24611
|
+
const dueTasks = await this.service.claimDueTasks();
|
|
24612
|
+
for (const task of dueTasks) {
|
|
24613
|
+
try {
|
|
24614
|
+
await this.dispatchWithTimeout(task);
|
|
24615
|
+
await this.service.markExecuted(task.id);
|
|
24616
|
+
} catch (error2) {
|
|
24617
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
24618
|
+
await this.logger?.error("scheduled.dispatch.failed", "failed to dispatch scheduled task", {
|
|
24619
|
+
taskId: task.id,
|
|
24620
|
+
message
|
|
24621
|
+
});
|
|
24622
|
+
await this.service.markFailed(task.id, error2);
|
|
24623
|
+
}
|
|
24624
|
+
}
|
|
24625
|
+
} finally {
|
|
24626
|
+
this.ticking = false;
|
|
24627
|
+
}
|
|
24628
|
+
}
|
|
24629
|
+
async dispatchWithTimeout(task) {
|
|
24630
|
+
const controller = new AbortController;
|
|
24631
|
+
let timer;
|
|
24632
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
24633
|
+
timer = setTimeout(() => {
|
|
24634
|
+
reject(new Error(`scheduled task dispatch timed out after ${this.dispatchTimeoutMs}ms`));
|
|
24635
|
+
controller.abort();
|
|
24636
|
+
}, this.dispatchTimeoutMs);
|
|
24637
|
+
});
|
|
24638
|
+
const dispatch = this.dispatchTask(task, controller.signal);
|
|
24639
|
+
dispatch.catch(() => {});
|
|
24640
|
+
try {
|
|
24641
|
+
await Promise.race([dispatch, timeout]);
|
|
24642
|
+
} finally {
|
|
24643
|
+
if (timer !== undefined)
|
|
24644
|
+
clearTimeout(timer);
|
|
24645
|
+
}
|
|
24646
|
+
}
|
|
24647
|
+
}
|
|
24648
|
+
var DEFAULT_DISPATCH_TIMEOUT_MS;
|
|
24649
|
+
var init_scheduled_scheduler = __esm(() => {
|
|
24650
|
+
DEFAULT_DISPATCH_TIMEOUT_MS = 10 * 60 * 1000;
|
|
24651
|
+
});
|
|
24652
|
+
|
|
24653
|
+
// src/scheduled/scheduled-service.ts
|
|
24654
|
+
class ScheduledTaskService {
|
|
24655
|
+
state;
|
|
24656
|
+
stateStore;
|
|
24657
|
+
now;
|
|
24658
|
+
generateId;
|
|
24659
|
+
stateMutex;
|
|
24660
|
+
claimedInThisSession = new Set;
|
|
24661
|
+
constructor(state, stateStore, options) {
|
|
24662
|
+
this.state = state;
|
|
24663
|
+
this.stateStore = stateStore;
|
|
24664
|
+
this.now = options?.now ?? (() => new Date);
|
|
24665
|
+
this.generateId = options?.generateId ?? (() => Math.random().toString(36).slice(2, 6));
|
|
24666
|
+
this.stateMutex = options?.stateMutex ?? new AsyncMutex;
|
|
24667
|
+
}
|
|
24668
|
+
async createTask(input) {
|
|
24669
|
+
return await this.mutate(async () => {
|
|
24670
|
+
const id = this.nextId();
|
|
24671
|
+
const task = {
|
|
24672
|
+
id,
|
|
24673
|
+
chat_key: input.chatKey,
|
|
24674
|
+
session_alias: input.sessionAlias,
|
|
24675
|
+
execute_at: input.executeAt.toISOString(),
|
|
24676
|
+
message: input.message,
|
|
24677
|
+
status: "pending",
|
|
24678
|
+
created_at: this.now().toISOString(),
|
|
24679
|
+
...input.accountId ? { account_id: input.accountId } : {},
|
|
24680
|
+
...input.replyContextToken ? { reply_context_token: input.replyContextToken } : {},
|
|
24681
|
+
...input.sourceLabel ? { source_label: input.sourceLabel } : {}
|
|
24682
|
+
};
|
|
24683
|
+
this.state.scheduled_tasks[id] = task;
|
|
24684
|
+
await this.save();
|
|
24685
|
+
return task;
|
|
24686
|
+
});
|
|
24687
|
+
}
|
|
24688
|
+
listPending() {
|
|
24689
|
+
return Object.values(this.state.scheduled_tasks).filter((task) => task.status === "pending").sort((left, right) => left.execute_at.localeCompare(right.execute_at));
|
|
24690
|
+
}
|
|
24691
|
+
async cancelPending(inputId) {
|
|
24692
|
+
return await this.mutate(async () => {
|
|
24693
|
+
const id = normalizeId(inputId);
|
|
24694
|
+
const task = this.state.scheduled_tasks[id];
|
|
24695
|
+
if (!task || task.status !== "pending")
|
|
24696
|
+
return false;
|
|
24697
|
+
task.status = "cancelled";
|
|
24698
|
+
task.cancelled_at = this.now().toISOString();
|
|
24699
|
+
await this.save();
|
|
24700
|
+
return true;
|
|
24701
|
+
});
|
|
24702
|
+
}
|
|
24703
|
+
async markStartupMissed() {
|
|
24704
|
+
await this.mutate(async () => {
|
|
24705
|
+
const nowMs = this.now().getTime();
|
|
24706
|
+
let changed = false;
|
|
24707
|
+
for (const task of Object.values(this.state.scheduled_tasks)) {
|
|
24708
|
+
if (task.status === "pending" && Date.parse(task.execute_at) < nowMs) {
|
|
24709
|
+
task.status = "missed";
|
|
24710
|
+
task.missed_at = this.now().toISOString();
|
|
24711
|
+
changed = true;
|
|
24712
|
+
}
|
|
24713
|
+
if (task.status === "triggering" && !this.claimedInThisSession.has(task.id)) {
|
|
24714
|
+
task.status = "failed";
|
|
24715
|
+
task.failed_at = this.now().toISOString();
|
|
24716
|
+
task.last_error = "process stopped while task was triggering";
|
|
24717
|
+
changed = true;
|
|
24718
|
+
}
|
|
24719
|
+
}
|
|
24720
|
+
if (changed)
|
|
24721
|
+
await this.save();
|
|
24722
|
+
});
|
|
24723
|
+
}
|
|
24724
|
+
async claimDueTasks() {
|
|
24725
|
+
return await this.mutate(async () => {
|
|
24726
|
+
const nowMs = this.now().getTime();
|
|
24727
|
+
const due = this.listPending().filter((task) => Date.parse(task.execute_at) <= nowMs);
|
|
24728
|
+
if (due.length === 0)
|
|
24729
|
+
return [];
|
|
24730
|
+
const at = this.now().toISOString();
|
|
24731
|
+
for (const task of due) {
|
|
24732
|
+
task.status = "triggering";
|
|
24733
|
+
task.triggered_at = at;
|
|
24734
|
+
this.claimedInThisSession.add(task.id);
|
|
24735
|
+
}
|
|
24736
|
+
await this.save();
|
|
24737
|
+
return due.map((task) => ({ ...task }));
|
|
24738
|
+
});
|
|
24739
|
+
}
|
|
24740
|
+
async markExecuted(id) {
|
|
24741
|
+
await this.mutate(async () => {
|
|
24742
|
+
const taskId = normalizeId(id);
|
|
24743
|
+
const task = this.state.scheduled_tasks[taskId];
|
|
24744
|
+
if (!task)
|
|
24745
|
+
return;
|
|
24746
|
+
task.status = "executed";
|
|
24747
|
+
task.executed_at = this.now().toISOString();
|
|
24748
|
+
this.claimedInThisSession.delete(taskId);
|
|
24749
|
+
await this.save();
|
|
24750
|
+
});
|
|
24751
|
+
}
|
|
24752
|
+
async markFailed(id, error2) {
|
|
24753
|
+
await this.mutate(async () => {
|
|
24754
|
+
const taskId = normalizeId(id);
|
|
24755
|
+
const task = this.state.scheduled_tasks[taskId];
|
|
24756
|
+
if (!task)
|
|
24757
|
+
return;
|
|
24758
|
+
task.status = "failed";
|
|
24759
|
+
task.failed_at = this.now().toISOString();
|
|
24760
|
+
task.last_error = error2 instanceof Error ? error2.message : String(error2);
|
|
24761
|
+
this.claimedInThisSession.delete(taskId);
|
|
24762
|
+
await this.save();
|
|
24763
|
+
});
|
|
24764
|
+
}
|
|
24765
|
+
nextId() {
|
|
24766
|
+
for (let attempt = 0;attempt < 20; attempt += 1) {
|
|
24767
|
+
const id = normalizeId(this.generateId()).replace(/[^0-9a-z]/g, "").slice(0, 6);
|
|
24768
|
+
if (id.length >= 4 && !this.state.scheduled_tasks[id])
|
|
24769
|
+
return id;
|
|
24770
|
+
}
|
|
24771
|
+
throw new Error("failed to generate unique scheduled task id");
|
|
24772
|
+
}
|
|
24773
|
+
async mutate(critical) {
|
|
24774
|
+
return await this.stateMutex.run(critical);
|
|
24775
|
+
}
|
|
24776
|
+
async save() {
|
|
24777
|
+
await this.stateStore.save(this.state);
|
|
24778
|
+
}
|
|
24779
|
+
}
|
|
24780
|
+
function normalizeId(input) {
|
|
24781
|
+
return input.trim().replace(/^#/, "").toLowerCase();
|
|
24782
|
+
}
|
|
24783
|
+
var init_scheduled_service = () => {};
|
|
24784
|
+
|
|
23968
24785
|
// src/sessions/session-service.ts
|
|
23969
24786
|
class SessionService {
|
|
23970
24787
|
config;
|
|
@@ -24419,20 +25236,47 @@ async function runConsole(paths, deps) {
|
|
|
24419
25236
|
runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
|
|
24420
25237
|
}, 86400000);
|
|
24421
25238
|
}
|
|
25239
|
+
const channelStartPromise = deps.channels.startAll({
|
|
25240
|
+
agent: runtime.agent,
|
|
25241
|
+
abortSignal: shutdownController.signal,
|
|
25242
|
+
quota: runtime.quota,
|
|
25243
|
+
logger: runtime.logger,
|
|
25244
|
+
perfTracer: runtime.perfTracer
|
|
25245
|
+
});
|
|
25246
|
+
channelStartPromise.catch(() => {});
|
|
25247
|
+
let channelStartSettled = false;
|
|
25248
|
+
let channelStartError;
|
|
25249
|
+
channelStartPromise.then(() => {
|
|
25250
|
+
channelStartSettled = true;
|
|
25251
|
+
}, (error2) => {
|
|
25252
|
+
channelStartSettled = true;
|
|
25253
|
+
channelStartError = error2;
|
|
25254
|
+
});
|
|
25255
|
+
await Promise.resolve();
|
|
25256
|
+
if (channelStartSettled && channelStartError) {
|
|
25257
|
+
if (deps.channelStartupPolicy !== "best-effort") {
|
|
25258
|
+
throw channelStartError;
|
|
25259
|
+
}
|
|
25260
|
+
await runtime.logger.error("daemon.channels.start_failed", "all channels failed to start; daemon remains alive for orchestration IPC", { error: channelStartError instanceof Error ? channelStartError.message : String(channelStartError) });
|
|
25261
|
+
await waitForShutdown(shutdownController.signal);
|
|
25262
|
+
return;
|
|
25263
|
+
}
|
|
24422
25264
|
try {
|
|
24423
|
-
await
|
|
24424
|
-
|
|
24425
|
-
|
|
24426
|
-
|
|
24427
|
-
|
|
24428
|
-
|
|
24429
|
-
|
|
25265
|
+
await runtime.scheduled.scheduler.start();
|
|
25266
|
+
} catch (error2) {
|
|
25267
|
+
shutdownController.abort();
|
|
25268
|
+
throw error2;
|
|
25269
|
+
}
|
|
25270
|
+
try {
|
|
25271
|
+
await channelStartPromise;
|
|
24430
25272
|
} catch (error2) {
|
|
25273
|
+
runtime.scheduled.scheduler.stop();
|
|
24431
25274
|
if (deps.channelStartupPolicy !== "best-effort") {
|
|
24432
25275
|
throw error2;
|
|
24433
25276
|
}
|
|
24434
25277
|
await runtime.logger.error("daemon.channels.start_failed", "all channels failed to start; daemon remains alive for orchestration IPC", { error: error2 instanceof Error ? error2.message : String(error2) });
|
|
24435
25278
|
await waitForShutdown(shutdownController.signal);
|
|
25279
|
+
return;
|
|
24436
25280
|
}
|
|
24437
25281
|
} finally {
|
|
24438
25282
|
await runCleanupSequence({
|
|
@@ -26435,6 +27279,21 @@ class MessageChannelRegistry {
|
|
|
26435
27279
|
async sendCoordinatorMessage(input) {
|
|
26436
27280
|
await this.requireByChatKey(input.chatKey).sendCoordinatorMessage(input);
|
|
26437
27281
|
}
|
|
27282
|
+
supportsScheduledMessages(chatKey) {
|
|
27283
|
+
const [candidateChannelId] = chatKey.split(":", 1);
|
|
27284
|
+
if (chatKey.includes(":") && candidateChannelId && !this.channels.has(candidateChannelId)) {
|
|
27285
|
+
return false;
|
|
27286
|
+
}
|
|
27287
|
+
const channel = this.getByChatKey(chatKey);
|
|
27288
|
+
return !!channel?.sendScheduledMessage;
|
|
27289
|
+
}
|
|
27290
|
+
async sendScheduledMessage(input) {
|
|
27291
|
+
const channel = this.requireByChatKey(input.chatKey);
|
|
27292
|
+
if (!channel.sendScheduledMessage) {
|
|
27293
|
+
throw new Error(`channel '${channel.id}' does not support scheduled messages`);
|
|
27294
|
+
}
|
|
27295
|
+
await channel.sendScheduledMessage(input);
|
|
27296
|
+
}
|
|
26438
27297
|
createConsumerLocks() {
|
|
26439
27298
|
const result = [];
|
|
26440
27299
|
for (const channel of this.channels.values()) {
|
|
@@ -26649,6 +27508,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
26649
27508
|
}
|
|
26650
27509
|
});
|
|
26651
27510
|
const sessions = new SessionService(config2, debouncedStateStore, state, { stateMutex });
|
|
27511
|
+
const scheduledService = new ScheduledTaskService(state, debouncedStateStore, { stateMutex });
|
|
26652
27512
|
const pendingWorkerDispatches = new Set;
|
|
26653
27513
|
const transport = config2.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
|
|
26654
27514
|
acpxCommand,
|
|
@@ -27008,8 +27868,33 @@ async function buildApp(paths, deps = {}) {
|
|
|
27008
27868
|
const progressHeartbeatInterval = startProgressHeartbeat(orchestration, config2, logger2, deps.channel ?? null);
|
|
27009
27869
|
const orchestrationEndpoint = createOrchestrationEndpoint(paths.orchestrationSocketPath ?? resolveOrchestrationSocketPathFromConfigPath(paths.configPath));
|
|
27010
27870
|
const orchestrationServer = new OrchestrationServer(orchestrationEndpoint, orchestration);
|
|
27011
|
-
const router = new CommandRouter(sessions, transport, config2, configStore, logger2, undefined, orchestration, quota);
|
|
27871
|
+
const router = new CommandRouter(sessions, transport, config2, configStore, logger2, undefined, orchestration, quota, scheduledService, deps.channel?.supportsScheduledMessages ? { supportsScheduledMessages: deps.channel.supportsScheduledMessages.bind(deps.channel) } : undefined);
|
|
27012
27872
|
const agent = new ConsoleAgent(router, logger2);
|
|
27873
|
+
const scheduledScheduler = new ScheduledTaskScheduler(scheduledService, {
|
|
27874
|
+
dispatchTask: async (task, abortSignal) => {
|
|
27875
|
+
const session = await sessions.getSession(task.session_alias);
|
|
27876
|
+
if (!session) {
|
|
27877
|
+
throw new Error(`session "${task.session_alias}" not found for scheduled task`);
|
|
27878
|
+
}
|
|
27879
|
+
const noticeText = `执行定时任务 #${task.id}
|
|
27880
|
+
会话:${toDisplaySessionAlias(task.session_alias)}
|
|
27881
|
+
内容:${preview(task.message)}`;
|
|
27882
|
+
if (!deps.channel?.sendScheduledMessage) {
|
|
27883
|
+
throw new Error("no channel runtime available for scheduled task dispatch");
|
|
27884
|
+
}
|
|
27885
|
+
await deps.channel.sendScheduledMessage({
|
|
27886
|
+
chatKey: task.chat_key,
|
|
27887
|
+
taskId: task.id,
|
|
27888
|
+
sessionAlias: task.session_alias,
|
|
27889
|
+
noticeText,
|
|
27890
|
+
promptText: task.message,
|
|
27891
|
+
abortSignal,
|
|
27892
|
+
...task.account_id ? { accountId: task.account_id } : {},
|
|
27893
|
+
...task.reply_context_token ? { replyContextToken: task.reply_context_token } : {}
|
|
27894
|
+
});
|
|
27895
|
+
},
|
|
27896
|
+
logger: logger2
|
|
27897
|
+
});
|
|
27013
27898
|
return {
|
|
27014
27899
|
agent,
|
|
27015
27900
|
router,
|
|
@@ -27025,7 +27910,12 @@ async function buildApp(paths, deps = {}) {
|
|
|
27025
27910
|
server: orchestrationServer,
|
|
27026
27911
|
endpoint: orchestrationEndpoint
|
|
27027
27912
|
},
|
|
27913
|
+
scheduled: {
|
|
27914
|
+
service: scheduledService,
|
|
27915
|
+
scheduler: scheduledScheduler
|
|
27916
|
+
},
|
|
27028
27917
|
dispose: async () => {
|
|
27918
|
+
scheduledScheduler.stop();
|
|
27029
27919
|
if (progressHeartbeatInterval !== undefined) {
|
|
27030
27920
|
clearInterval(progressHeartbeatInterval);
|
|
27031
27921
|
}
|
|
@@ -27078,6 +27968,7 @@ function replaceRuntimeState(target, source) {
|
|
|
27078
27968
|
target.sessions = source.sessions;
|
|
27079
27969
|
target.chat_contexts = source.chat_contexts;
|
|
27080
27970
|
target.orchestration = source.orchestration;
|
|
27971
|
+
target.scheduled_tasks = source.scheduled_tasks;
|
|
27081
27972
|
}
|
|
27082
27973
|
function replaceRuntimeConfig(target, source) {
|
|
27083
27974
|
Object.assign(target, source);
|
|
@@ -27172,6 +28063,10 @@ var init_main = __esm(async () => {
|
|
|
27172
28063
|
init_orchestration_server();
|
|
27173
28064
|
init_orchestration_service();
|
|
27174
28065
|
init_build_coordinator_prompt();
|
|
28066
|
+
init_scheduled_scheduler();
|
|
28067
|
+
init_scheduled_service();
|
|
28068
|
+
init_scheduled_render();
|
|
28069
|
+
init_channel_scope();
|
|
27175
28070
|
init_session_service();
|
|
27176
28071
|
init_state_store();
|
|
27177
28072
|
init_run_console();
|
package/dist/plugin-api.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { ChannelPluginDefinition } from "./channels/plugin.js";
|
|
2
2
|
export type { ChannelFactory, CreateChannelDeps } from "./channels/create-channel.js";
|
|
3
|
-
export type { ChannelStartInput, ConsumerLock, ConsumerLockMetadata, ConsumerLockOptions, CoordinatorMessageInput, MessageChannelRuntime, OrchestrationDeliveryCallbacks, OutboundQuota, ToolUseEvent, ToolUseKind, ToolUseStatus, } from "./channels/types.js";
|
|
3
|
+
export type { ChannelStartInput, ConsumerLock, ConsumerLockMetadata, ConsumerLockOptions, CoordinatorMessageInput, MessageChannelRuntime, ScheduledChannelMessageInput, OrchestrationDeliveryCallbacks, OutboundQuota, ToolUseEvent, ToolUseKind, ToolUseStatus, } from "./channels/types.js";
|
|
4
4
|
export type { ChannelCliInput, ChannelCliIo, ChannelCliParseResult, ChannelCliProvider, ChannelCliValidationIssue, } from "./channels/cli/provider.js";
|
|
5
5
|
export type { ChannelRuntimeConfig } from "./config/types.js";
|
|
6
6
|
export type { AppLogger } from "./logging/app-logger.js";
|
package/dist/plugin-api.js
CHANGED
|
@@ -160,7 +160,7 @@ function validatePluginCompatibility(metadata, context) {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.
|
|
163
|
+
var WEACPX_PLUGIN_API_VERSION = 1, WEACPX_PLUGIN_API_SUPPORTED_VERSIONS, WEACPX_PLUGIN_MIN_CORE_VERSION = "0.5.0", SEMVER_RE;
|
|
164
164
|
var init_compatibility = __esm(() => {
|
|
165
165
|
WEACPX_PLUGIN_API_SUPPORTED_VERSIONS = [1];
|
|
166
166
|
SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const WEACPX_PLUGIN_API_VERSION: 1;
|
|
2
2
|
export declare const WEACPX_PLUGIN_API_SUPPORTED_VERSIONS: readonly number[];
|
|
3
|
-
export declare const WEACPX_PLUGIN_MIN_CORE_VERSION: "0.
|
|
3
|
+
export declare const WEACPX_PLUGIN_MIN_CORE_VERSION: "0.5.0";
|
|
4
4
|
export declare function normalizeCoreVersionForCompat(version: string): string;
|
|
5
5
|
export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
|
|
6
6
|
export declare function isVersionSatisfied(current: string, requirement: string): boolean;
|
|
@@ -62,6 +62,8 @@ export interface ChatRequestMetadata {
|
|
|
62
62
|
senderName?: string;
|
|
63
63
|
groupId?: string;
|
|
64
64
|
isOwner?: boolean;
|
|
65
|
+
/** Internal weacpx session alias to use for non-interactive scheduled prompts. */
|
|
66
|
+
scheduledSessionAlias?: string;
|
|
65
67
|
}
|
|
66
68
|
export interface ChatResponse {
|
|
67
69
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AppLogger } from "../../logging/app-logger";
|
|
2
|
+
import type { ScheduledChannelMessageInput } from "../../channels/types";
|
|
3
|
+
import type { Agent } from "../agent/interface";
|
|
4
|
+
import type { PendingFinalChunk } from "./quota-manager";
|
|
5
|
+
import { sendMessageWeixin } from "./send";
|
|
6
|
+
export interface ScheduledTurnDeps {
|
|
7
|
+
agent: Agent;
|
|
8
|
+
listAccountIds: () => string[];
|
|
9
|
+
resolveAccount: (accountId: string) => {
|
|
10
|
+
accountId: string;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
token?: string;
|
|
13
|
+
};
|
|
14
|
+
getContextToken: (accountId: string, userId: string) => string | undefined;
|
|
15
|
+
reserveMidSegment: (chatKey: string) => boolean;
|
|
16
|
+
reserveFinal: (chatKey: string) => boolean;
|
|
17
|
+
finalRemaining?: (chatKey: string) => number;
|
|
18
|
+
enqueuePendingFinal?: (chatKey: string, chunks: PendingFinalChunk[]) => void;
|
|
19
|
+
sendMessage?: typeof sendMessageWeixin;
|
|
20
|
+
logger: AppLogger;
|
|
21
|
+
}
|
|
22
|
+
export declare function executeScheduledTurn(input: ScheduledChannelMessageInput, deps: ScheduledTurnDeps): Promise<void>;
|