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