@ynhcj/xiaoyi-channel 0.0.217-beta → 0.0.217-next
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -3
- package/dist/index.js +81 -3
- package/dist/src/bot.js +56 -8
- package/dist/src/compaction-provider.d.ts +50 -0
- package/dist/src/compaction-provider.js +202 -0
- package/dist/src/cron-query-handler.d.ts +20 -0
- package/dist/src/cron-query-handler.js +481 -91
- package/dist/src/cron-recovery.d.ts +25 -0
- package/dist/src/cron-recovery.js +587 -0
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +17 -12
- package/dist/src/cspl/config.d.ts +1 -0
- package/dist/src/cspl/config.js +59 -65
- package/dist/src/cspl/constants.d.ts +1 -36
- package/dist/src/cspl/constants.js +0 -9
- package/dist/src/cspl/sentinel_hook.js +30 -35
- package/dist/src/cspl/upload_file.js +12 -6
- package/dist/src/cspl/utils.d.ts +9 -20
- package/dist/src/cspl/utils.js +75 -119
- package/dist/src/monitor.js +49 -8
- package/dist/src/provider.js +89 -2
- package/dist/src/skill-retriever/hooks.js +10 -1
- package/dist/src/skill-retriever/tool-search.js +13 -11
- package/dist/src/steer-queue.d.ts +1 -0
- package/dist/src/steer-queue.js +9 -0
- package/dist/src/tools/call-phone-tool.d.ts +0 -1
- package/dist/src/tools/call-phone-tool.js +5 -8
- package/package.json +3 -2
|
@@ -3,16 +3,96 @@
|
|
|
3
3
|
// calls Gateway cron RPC via callGatewayTool, and sends the
|
|
4
4
|
// result back to the client via sendCommand as a System.CronQuery
|
|
5
5
|
// command with the result in payload.ans.
|
|
6
|
+
//
|
|
7
|
+
// This module adapts between the XY Channel device-facing RPC schema
|
|
8
|
+
// and the OpenClaw Gateway native cron RPC (protocol v4).
|
|
9
|
+
// Each action transforms request params from device format → gateway format,
|
|
10
|
+
// then transforms the gateway response → device format before sending back.
|
|
6
11
|
import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
7
|
-
import
|
|
12
|
+
import fsp from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
8
15
|
import { sendCommand } from "./formatter.js";
|
|
9
16
|
import { resolveXYConfig } from "./config.js";
|
|
10
17
|
import { configManager } from "./utils/config-manager.js";
|
|
11
18
|
import { setJobPushId } from "./utils/cron-push-map.js";
|
|
12
19
|
import { logger } from "./utils/logger.js";
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
// ── Local run-log constants, types, and parsers (moved from cron-recovery) ───
|
|
21
|
+
const ROOT_DIR = path.join(os.homedir(), ".openclaw");
|
|
22
|
+
/** Path to the legacy cron run-log directory. */
|
|
23
|
+
const LEGACY_CRON_RUNS_DIR = path.join(ROOT_DIR, "cron", "runs");
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function readString(record, key) {
|
|
28
|
+
const val = record[key];
|
|
29
|
+
return typeof val === "string" && val.trim() ? val.trim() : undefined;
|
|
30
|
+
}
|
|
31
|
+
function readOptionalString(record, key) {
|
|
32
|
+
const val = record[key];
|
|
33
|
+
return typeof val === "string" ? val : undefined;
|
|
34
|
+
}
|
|
35
|
+
function readNumber(record, key) {
|
|
36
|
+
const val = record[key];
|
|
37
|
+
return typeof val === "number" && Number.isFinite(val) ? val : undefined;
|
|
38
|
+
}
|
|
39
|
+
function readBoolean(record, key) {
|
|
40
|
+
const val = record[key];
|
|
41
|
+
return typeof val === "boolean" ? val : undefined;
|
|
42
|
+
}
|
|
43
|
+
function parseRunLogEntry(obj, opts) {
|
|
44
|
+
if (!isRecord(obj))
|
|
45
|
+
return null;
|
|
46
|
+
const ts = readNumber(obj, "ts");
|
|
47
|
+
if (ts === undefined)
|
|
48
|
+
return null;
|
|
49
|
+
const jobId = opts?.jobId ?? readString(obj, "jobId");
|
|
50
|
+
if (!jobId)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
jobId,
|
|
54
|
+
ts,
|
|
55
|
+
runId: readOptionalString(obj, "runId"),
|
|
56
|
+
status: readOptionalString(obj, "status"),
|
|
57
|
+
error: readOptionalString(obj, "error"),
|
|
58
|
+
summary: readOptionalString(obj, "summary"),
|
|
59
|
+
diagnosticsSummary: readOptionalString(obj, "diagnosticsSummary"),
|
|
60
|
+
deliveryStatus: readOptionalString(obj, "deliveryStatus"),
|
|
61
|
+
deliveryError: readOptionalString(obj, "deliveryError"),
|
|
62
|
+
delivered: readBoolean(obj, "delivered"),
|
|
63
|
+
sessionId: readOptionalString(obj, "sessionId"),
|
|
64
|
+
sessionKey: readOptionalString(obj, "sessionKey"),
|
|
65
|
+
runAtMs: readNumber(obj, "runAtMs"),
|
|
66
|
+
durationMs: readNumber(obj, "durationMs"),
|
|
67
|
+
nextRunAtMs: readNumber(obj, "nextRunAtMs"),
|
|
68
|
+
model: readOptionalString(obj, "model"),
|
|
69
|
+
provider: readOptionalString(obj, "provider"),
|
|
70
|
+
totalTokens: readNumber(obj, "totalTokens"),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parseCronRunLogEntriesFromJsonl(raw, opts) {
|
|
74
|
+
if (!raw.trim())
|
|
75
|
+
return [];
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const line of raw.split("\n")) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed)
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
const entry = parseRunLogEntry(JSON.parse(trimmed), opts);
|
|
83
|
+
if (entry)
|
|
84
|
+
entries.push(entry);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Skip malformed historical rows
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
15
92
|
const GATEWAY_TIMEOUT_MS = 60_000;
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Main handler
|
|
95
|
+
// ============================================================================
|
|
16
96
|
/**
|
|
17
97
|
* Handle a cron-query-event.
|
|
18
98
|
*
|
|
@@ -27,47 +107,111 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
27
107
|
let error;
|
|
28
108
|
try {
|
|
29
109
|
switch (action) {
|
|
30
|
-
|
|
31
|
-
|
|
110
|
+
// ── list ──────────────────────────────────────────────────────
|
|
111
|
+
case "list": {
|
|
112
|
+
const gatewayParams = buildListParams(params);
|
|
113
|
+
let gatewayResult = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
|
|
114
|
+
// Scan for systemEvent jobs and migrate them to agentTurn
|
|
115
|
+
// Skip main-session jobs — gateway requires payload.kind="systemEvent" for those
|
|
116
|
+
const jobs = gatewayResult?.jobs ?? [];
|
|
117
|
+
const systemEventJobs = jobs.filter((j) => j?.payload?.kind === "systemEvent");
|
|
118
|
+
if (systemEventJobs.length > 0) {
|
|
119
|
+
log.log(`[CRON-QUERY] list: found ${systemEventJobs.length} systemEvent jobs, migrating to agentTurn`);
|
|
120
|
+
for (const job of systemEventJobs) {
|
|
121
|
+
const updatePatch = {
|
|
122
|
+
sessionTarget: "isolated",
|
|
123
|
+
description: job.payload?.text ?? job.payload?.message ?? "",
|
|
124
|
+
payload: {
|
|
125
|
+
kind: "agentTurn",
|
|
126
|
+
message: job.payload?.text ?? job.payload?.message ?? "",
|
|
127
|
+
},
|
|
128
|
+
delivery: {
|
|
129
|
+
channel: "xiaoyi-channel",
|
|
130
|
+
mode: "announce",
|
|
131
|
+
to: "default"
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, { id: job.id, patch: updatePatch });
|
|
136
|
+
log.log(`[CRON-QUERY] list: migrated job ${job.id} (${job.name ?? ""}) from systemEvent → agentTurn`);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
log.warn(`[CRON-QUERY] list: failed to migrate job ${job.id}:`, err);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Re-fetch list after migration
|
|
143
|
+
gatewayResult = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
|
|
144
|
+
}
|
|
145
|
+
result = transformListResponse(gatewayResult, params);
|
|
32
146
|
break;
|
|
33
|
-
|
|
34
|
-
|
|
147
|
+
}
|
|
148
|
+
// ── status ────────────────────────────────────────────────────
|
|
149
|
+
case "status": {
|
|
150
|
+
const gatewayResult = await callGatewayTool("cron.status", { timeoutMs: GATEWAY_TIMEOUT_MS }, {});
|
|
151
|
+
result = transformStatusResponse(gatewayResult);
|
|
35
152
|
break;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
153
|
+
}
|
|
154
|
+
// ── runs ─────────────────────────────────────────────────────
|
|
155
|
+
case "runs": {
|
|
156
|
+
const { offset = 0, limit = 50, ...otherParams } = params ?? {};
|
|
157
|
+
// Fetch from local runs folder and gateway RPC in parallel
|
|
158
|
+
const [localEntries, gatewayResult] = await Promise.all([
|
|
159
|
+
readLocalRunLogsForJob(jobId).catch(() => []),
|
|
160
|
+
callGatewayTool("cron.runs", { timeoutMs: GATEWAY_TIMEOUT_MS }, { jobId, scope: "job", ...otherParams }).catch(() => ({ entries: [] })),
|
|
161
|
+
]);
|
|
162
|
+
const gatewayEntries = gatewayResult?.entries ?? [];
|
|
163
|
+
// Merge, deduplicate, and sort by time (newest first)
|
|
164
|
+
const merged = mergeAndDedupeRunEntries(localEntries, gatewayEntries);
|
|
165
|
+
// Apply pagination after merge
|
|
166
|
+
const total = merged.length;
|
|
167
|
+
const paged = merged.slice(offset, offset + limit);
|
|
168
|
+
const hasMore = total > offset + limit;
|
|
169
|
+
result = {
|
|
170
|
+
entries: paged,
|
|
171
|
+
total,
|
|
172
|
+
offset,
|
|
173
|
+
limit,
|
|
174
|
+
hasMore,
|
|
175
|
+
nextOffset: hasMore ? offset + limit : null,
|
|
176
|
+
};
|
|
41
177
|
break;
|
|
42
|
-
|
|
43
|
-
|
|
178
|
+
}
|
|
179
|
+
// ── add ──────────────────────────────────────────────────────
|
|
180
|
+
case "add": {
|
|
181
|
+
const gatewayParams = buildAddParams(params);
|
|
182
|
+
const gatewayResult = await callGatewayTool("cron.add", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
|
|
44
183
|
// 捕获 jobId↔pushId:cron-query 路径由 channel 自己建 job,
|
|
45
184
|
// 此处 context 握着 sessionId,configManager 有对应设备 pushId。
|
|
46
|
-
await persistCronPushMap(context.sessionId,
|
|
185
|
+
await persistCronPushMap(context.sessionId, gatewayResult).catch((err) => {
|
|
47
186
|
logger.error(`[CRON-QUERY] Failed to persist cron-push-map:`, err);
|
|
48
187
|
});
|
|
188
|
+
result = transformAddResponse(gatewayResult);
|
|
49
189
|
break;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
190
|
+
}
|
|
191
|
+
// ── update ───────────────────────────────────────────────────
|
|
192
|
+
case "update": {
|
|
193
|
+
const gatewayParams = buildUpdateParams(jobId, params);
|
|
194
|
+
const gatewayResult = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, gatewayParams);
|
|
195
|
+
result = transformUpdateResponse(gatewayResult);
|
|
55
196
|
break;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
197
|
+
}
|
|
198
|
+
// ── remove ───────────────────────────────────────────────────
|
|
199
|
+
case "remove": {
|
|
200
|
+
const gatewayResult = await callGatewayTool("cron.remove", { timeoutMs: GATEWAY_TIMEOUT_MS }, { id: jobId });
|
|
201
|
+
result = transformRemoveResponse(gatewayResult);
|
|
60
202
|
break;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
203
|
+
}
|
|
204
|
+
// ── run ──────────────────────────────────────────────────────
|
|
205
|
+
case "run": {
|
|
206
|
+
const gatewayResult = await callGatewayTool("cron.run", { timeoutMs: GATEWAY_TIMEOUT_MS }, { id: jobId, mode: "force" });
|
|
207
|
+
result = transformRunResponse(gatewayResult);
|
|
67
208
|
break;
|
|
68
|
-
|
|
69
|
-
|
|
209
|
+
}
|
|
210
|
+
// ── queryTimeList ────────────────────────────────────────────
|
|
211
|
+
case "queryTimeList": {
|
|
212
|
+
result = await queryTimeListFromGateway(params);
|
|
70
213
|
break;
|
|
214
|
+
}
|
|
71
215
|
default:
|
|
72
216
|
error = `Unknown action: ${context.action}`;
|
|
73
217
|
log.error(`[CRON-QUERY] ${error}`);
|
|
@@ -81,7 +225,7 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
81
225
|
}
|
|
82
226
|
// Log the result
|
|
83
227
|
log.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
|
|
84
|
-
// Send result back via sendCommand as
|
|
228
|
+
// Send result back via sendCommand as AgentEvent.CronQuery with payload.ans
|
|
85
229
|
if (cfg && sessionId && taskId && messageId) {
|
|
86
230
|
try {
|
|
87
231
|
const config = resolveXYConfig(cfg);
|
|
@@ -92,6 +236,7 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
92
236
|
},
|
|
93
237
|
payload: {
|
|
94
238
|
action,
|
|
239
|
+
status: !error,
|
|
95
240
|
ans: result,
|
|
96
241
|
},
|
|
97
242
|
};
|
|
@@ -113,6 +258,114 @@ export async function handleCronQueryEvent(context, cfg) {
|
|
|
113
258
|
log.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
|
|
114
259
|
}
|
|
115
260
|
}
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Request builders — device params → gateway params
|
|
263
|
+
// ============================================================================
|
|
264
|
+
/** list: device params pass through (gateway-compatible). */
|
|
265
|
+
function buildListParams(params) {
|
|
266
|
+
return params ?? {};
|
|
267
|
+
}
|
|
268
|
+
/** add: unwrap params.job → top-level gateway params. */
|
|
269
|
+
function buildAddParams(params) {
|
|
270
|
+
const job = params?.job ?? params ?? {};
|
|
271
|
+
return {
|
|
272
|
+
name: job.name,
|
|
273
|
+
schedule: job.schedule,
|
|
274
|
+
sessionTarget: job.sessionTarget,
|
|
275
|
+
wakeMode: job.wakeMode,
|
|
276
|
+
payload: job.payload,
|
|
277
|
+
enabled: job.enabled,
|
|
278
|
+
deleteAfterRun: job.deleteAfterRun,
|
|
279
|
+
delivery: job.delivery,
|
|
280
|
+
description: job.description,
|
|
281
|
+
failureAlert: job.failureAlert,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** update: unwrap params.patch → patch, add jobId. */
|
|
285
|
+
function buildUpdateParams(jobId, params) {
|
|
286
|
+
const patch = params?.patch ?? params ?? {};
|
|
287
|
+
return {
|
|
288
|
+
id: jobId,
|
|
289
|
+
patch
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Response transformers — gateway result → device format
|
|
294
|
+
// ============================================================================
|
|
295
|
+
/** Compute pagination metadata from request params and total. */
|
|
296
|
+
function computePagination(params, total) {
|
|
297
|
+
const offset = params?.offset ?? 0;
|
|
298
|
+
const limit = params?.limit ?? total;
|
|
299
|
+
const hasMore = total > offset + limit;
|
|
300
|
+
const nextOffset = hasMore ? offset + limit : null;
|
|
301
|
+
return { offset, limit, hasMore, nextOffset };
|
|
302
|
+
}
|
|
303
|
+
/** list: add pagination fields + status. */
|
|
304
|
+
function transformListResponse(gatewayResult, params) {
|
|
305
|
+
const jobs = gatewayResult?.jobs ?? [];
|
|
306
|
+
const total = gatewayResult?.total ?? jobs.length;
|
|
307
|
+
const pagination = computePagination(params, total);
|
|
308
|
+
return {
|
|
309
|
+
jobs,
|
|
310
|
+
total,
|
|
311
|
+
offset: pagination.offset,
|
|
312
|
+
limit: pagination.limit,
|
|
313
|
+
hasMore: pagination.hasMore,
|
|
314
|
+
nextOffset: pagination.nextOffset,
|
|
315
|
+
deliveryPreviews: gatewayResult?.deliveryPreviews ?? {},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** status: rename nextRunAtMs → nextWakeAtMs, add storePath. */
|
|
319
|
+
function transformStatusResponse(gatewayResult) {
|
|
320
|
+
return {
|
|
321
|
+
enabled: gatewayResult?.enabled ?? false,
|
|
322
|
+
storePath: ".openclaw/cron/jobs.json",
|
|
323
|
+
jobs: gatewayResult?.jobs ?? 0,
|
|
324
|
+
nextWakeAtMs: gatewayResult?.nextRunAtMs ?? null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/** add: pick relevant fields from gateway CronJob response. */
|
|
328
|
+
function transformAddResponse(gatewayResult) {
|
|
329
|
+
return {
|
|
330
|
+
id: gatewayResult?.id,
|
|
331
|
+
name: gatewayResult?.name,
|
|
332
|
+
enabled: gatewayResult?.enabled,
|
|
333
|
+
description: gatewayResult?.payload?.message,
|
|
334
|
+
createdAtMs: gatewayResult?.createdAtMs,
|
|
335
|
+
updatedAtMs: gatewayResult?.updatedAtMs,
|
|
336
|
+
schedule: gatewayResult?.schedule,
|
|
337
|
+
sessionTarget: gatewayResult?.sessionTarget,
|
|
338
|
+
wakeMode: gatewayResult?.wakeMode,
|
|
339
|
+
payload: gatewayResult?.payload,
|
|
340
|
+
delivery: gatewayResult?.delivery,
|
|
341
|
+
state: {
|
|
342
|
+
nextRunAtMs: gatewayResult?.state?.nextRunAtMs ?? null,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/** update: same structure as add. */
|
|
347
|
+
function transformUpdateResponse(gatewayResult) {
|
|
348
|
+
return transformAddResponse(gatewayResult);
|
|
349
|
+
}
|
|
350
|
+
/** remove: wrap with ok flag. */
|
|
351
|
+
function transformRemoveResponse(gatewayResult) {
|
|
352
|
+
return {
|
|
353
|
+
ok: gatewayResult?.removed ?? false,
|
|
354
|
+
removed: gatewayResult?.removed ?? false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/** run: map ran → enqueued. */
|
|
358
|
+
function transformRunResponse(gatewayResult) {
|
|
359
|
+
return {
|
|
360
|
+
ok: gatewayResult?.ok ?? false,
|
|
361
|
+
runId: gatewayResult?.runId ?? null,
|
|
362
|
+
enqueued: gatewayResult?.ran ?? false,
|
|
363
|
+
error: gatewayResult?.error ?? undefined,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// Supporting functions
|
|
368
|
+
// ============================================================================
|
|
116
369
|
/**
|
|
117
370
|
* 从 cron.add 结果中提取 jobId,配合 sessionId 对应的 pushId 写入映射。
|
|
118
371
|
*/
|
|
@@ -129,7 +382,9 @@ async function persistCronPushMap(sessionId, result) {
|
|
|
129
382
|
jobId = id.trim();
|
|
130
383
|
}
|
|
131
384
|
if (!jobId) {
|
|
132
|
-
const preview = typeof result === "string"
|
|
385
|
+
const preview = typeof result === "string"
|
|
386
|
+
? result.slice(0, 200)
|
|
387
|
+
: JSON.stringify(result)?.slice(0, 200);
|
|
133
388
|
logger.log(`[CRONMAP] cron-query skip: no jobId in result. preview=${preview ?? "(empty)"}`);
|
|
134
389
|
return;
|
|
135
390
|
}
|
|
@@ -143,72 +398,46 @@ async function persistCronPushMap(sessionId, result) {
|
|
|
143
398
|
logger.log(`[CRONMAP] cron-query map written OK`);
|
|
144
399
|
}
|
|
145
400
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* Data sources:
|
|
150
|
-
* - state/cron/jobs.json → job id → name mapping
|
|
151
|
-
* - state/cron/runs/*.jsonl → run records (one JSON per line)
|
|
401
|
+
* Query run history from the last 7 days from both local runs folder
|
|
402
|
+
* and the gateway's native cron.runs RPC, merged and grouped by date.
|
|
152
403
|
*
|
|
153
404
|
* Return format:
|
|
154
405
|
* [ { "YYYY-MM-DD": [ { run record with .name }, ... ] }, ... ]
|
|
155
406
|
*/
|
|
156
|
-
async function
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
407
|
+
async function queryTimeListFromGateway(params) {
|
|
408
|
+
// Fetch from local runs folder and gateway RPC in parallel
|
|
409
|
+
const [localEntries, rpcResult] = await Promise.all([
|
|
410
|
+
readAllLocalRunLogs().catch(() => []),
|
|
411
|
+
callGatewayTool("cron.runs", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
412
|
+
scope: "all",
|
|
413
|
+
limit: 200,
|
|
414
|
+
sortDir: "desc",
|
|
415
|
+
...(params ?? {}),
|
|
416
|
+
}).catch(() => ({ entries: [] })),
|
|
417
|
+
]);
|
|
418
|
+
const gatewayEntries = rpcResult?.entries ?? [];
|
|
419
|
+
// Merge and deduplicate
|
|
420
|
+
const merged = mergeAndDedupeRunEntries(localEntries, gatewayEntries);
|
|
421
|
+
if (merged.length === 0) {
|
|
422
|
+
return [];
|
|
171
423
|
}
|
|
172
|
-
//
|
|
424
|
+
// Filter to last 7 days
|
|
173
425
|
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
catch {
|
|
180
|
-
files = [];
|
|
181
|
-
}
|
|
182
|
-
for (const file of files) {
|
|
183
|
-
if (!file.endsWith(".jsonl"))
|
|
184
|
-
continue;
|
|
185
|
-
try {
|
|
186
|
-
const content = readFileSync(join(runsDir, file), "utf-8");
|
|
187
|
-
const lines = content.trim().split("\n");
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
if (!line.trim())
|
|
190
|
-
continue;
|
|
191
|
-
try {
|
|
192
|
-
const run = JSON.parse(line);
|
|
193
|
-
if (run.ts && run.ts >= sevenDaysAgo) {
|
|
194
|
-
run.name = jobNameMap[run.jobId] || run.jobId || "";
|
|
195
|
-
allRuns.push(run);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
// skip malformed line
|
|
200
|
-
}
|
|
201
|
-
}
|
|
426
|
+
const recent = merged.filter((e) => e.ts && e.ts >= sevenDaysAgo);
|
|
427
|
+
// Ensure .name is populated (gateway returns jobName; normalize to .name)
|
|
428
|
+
for (const run of recent) {
|
|
429
|
+
if (!run.name && run.jobName) {
|
|
430
|
+
run.name = run.jobName;
|
|
202
431
|
}
|
|
203
|
-
|
|
204
|
-
|
|
432
|
+
else if (!run.name) {
|
|
433
|
+
run.name = run.jobId || "";
|
|
205
434
|
}
|
|
206
435
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
//
|
|
436
|
+
// Sort by ts ascending for chronological display
|
|
437
|
+
recent.sort((a, b) => a.ts - b.ts);
|
|
438
|
+
// Group by date (YYYY-MM-DD in local time)
|
|
210
439
|
const grouped = new Map();
|
|
211
|
-
for (const run of
|
|
440
|
+
for (const run of recent) {
|
|
212
441
|
const d = new Date(run.ts);
|
|
213
442
|
const label = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
214
443
|
if (!grouped.has(label)) {
|
|
@@ -216,10 +445,171 @@ async function queryTimeListLocal() {
|
|
|
216
445
|
}
|
|
217
446
|
grouped.get(label).push(run);
|
|
218
447
|
}
|
|
219
|
-
//
|
|
448
|
+
// Convert to ordered array of single-key objects
|
|
220
449
|
const result = [];
|
|
221
450
|
for (const [date, runs] of grouped) {
|
|
222
451
|
result.push({ [date]: runs });
|
|
223
452
|
}
|
|
224
453
|
return result;
|
|
225
454
|
}
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// Local run-log helpers — read legacy runs/*.jsonl files
|
|
457
|
+
// ============================================================================
|
|
458
|
+
/**
|
|
459
|
+
* Filename patterns that may contain legacy run-log data.
|
|
460
|
+
*
|
|
461
|
+
* After cron-recovery migration (gateway_start hook), .jsonl files are
|
|
462
|
+
* archived to .jsonl.migrated (or .migrated.2, .migrated.3, …).
|
|
463
|
+
* We must match all of these so that historical runs are still visible
|
|
464
|
+
* in queryTimeList after migration.
|
|
465
|
+
*/
|
|
466
|
+
const RUN_LOG_SUFFIXES = [
|
|
467
|
+
".jsonl", // active / pre-migration
|
|
468
|
+
".jsonl.migrated", // first migration
|
|
469
|
+
];
|
|
470
|
+
/**
|
|
471
|
+
* Check whether a filename looks like a run-log file (any generation).
|
|
472
|
+
*/
|
|
473
|
+
function isRunLogFile(name) {
|
|
474
|
+
return RUN_LOG_SUFFIXES.some((suffix) => name.endsWith(suffix))
|
|
475
|
+
// Also catch .migrated.2, .migrated.3, …
|
|
476
|
+
|| /\.jsonl\.migrated\.\d+$/.test(name);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Strip known run-log suffixes to extract the bare jobId from a filename.
|
|
480
|
+
* e.g. "abc123.jsonl.migrated.2" → "abc123"
|
|
481
|
+
*/
|
|
482
|
+
function extractJobIdFromRunLogName(name) {
|
|
483
|
+
// Try each known suffix first
|
|
484
|
+
for (const suffix of RUN_LOG_SUFFIXES) {
|
|
485
|
+
if (name.endsWith(suffix)) {
|
|
486
|
+
return name.slice(0, -suffix.length);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Fallback: strip .jsonl and everything after
|
|
490
|
+
const dotJsonl = name.indexOf(".jsonl");
|
|
491
|
+
if (dotJsonl >= 0) {
|
|
492
|
+
return name.slice(0, dotJsonl);
|
|
493
|
+
}
|
|
494
|
+
// Last resort: just use the basename without extension
|
|
495
|
+
return path.basename(name, path.extname(name));
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Read local run-log entries for a single job from the legacy runs folder.
|
|
499
|
+
*
|
|
500
|
+
* Tries multiple filename suffixes (active + archived) since migration
|
|
501
|
+
* may have renamed the file to .jsonl.migrated.
|
|
502
|
+
*/
|
|
503
|
+
async function readLocalRunLogsForJob(jobId) {
|
|
504
|
+
// Try active file first, then archived variants
|
|
505
|
+
const candidates = [
|
|
506
|
+
`${jobId}.jsonl`,
|
|
507
|
+
`${jobId}.jsonl.migrated`,
|
|
508
|
+
];
|
|
509
|
+
for (const candidate of candidates) {
|
|
510
|
+
const filePath = path.join(LEGACY_CRON_RUNS_DIR, candidate);
|
|
511
|
+
try {
|
|
512
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
513
|
+
if (raw.trim()) {
|
|
514
|
+
return parseCronRunLogEntriesFromJsonl(raw, { jobId });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Try next candidate
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Also scan for .migrated.N variants
|
|
522
|
+
try {
|
|
523
|
+
const dirEntries = await fsp.readdir(LEGACY_CRON_RUNS_DIR, { withFileTypes: true });
|
|
524
|
+
const migratedRe = new RegExp(`^${escapeRegex(jobId)}\\.jsonl\\.migrated\\.\\d+$`);
|
|
525
|
+
for (const entry of dirEntries) {
|
|
526
|
+
if (entry.isFile() && migratedRe.test(entry.name)) {
|
|
527
|
+
try {
|
|
528
|
+
const raw = await fsp.readFile(path.join(LEGACY_CRON_RUNS_DIR, entry.name), "utf-8");
|
|
529
|
+
if (raw.trim()) {
|
|
530
|
+
return parseCronRunLogEntriesFromJsonl(raw, { jobId });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Keep trying other candidates
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// Directory listing failed, give up
|
|
541
|
+
}
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Read all local run-log entries from the legacy runs folder.
|
|
546
|
+
*
|
|
547
|
+
* Matches .jsonl files as well as post-migration archived copies
|
|
548
|
+
* (.jsonl.migrated, .jsonl.migrated.2, …).
|
|
549
|
+
*/
|
|
550
|
+
async function readAllLocalRunLogs() {
|
|
551
|
+
try {
|
|
552
|
+
const entries = await fsp.readdir(LEGACY_CRON_RUNS_DIR, { withFileTypes: true });
|
|
553
|
+
const runLogFiles = entries.filter((e) => e.isFile() && isRunLogFile(e.name));
|
|
554
|
+
const allEntries = [];
|
|
555
|
+
for (const file of runLogFiles) {
|
|
556
|
+
const jobId = extractJobIdFromRunLogName(file.name);
|
|
557
|
+
try {
|
|
558
|
+
const raw = await fsp.readFile(path.join(LEGACY_CRON_RUNS_DIR, file.name), "utf-8");
|
|
559
|
+
const parsed = parseCronRunLogEntriesFromJsonl(raw, { jobId });
|
|
560
|
+
allEntries.push(...parsed);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Skip unreadable files
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return allEntries;
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/** Escape special regex characters in a string. */
|
|
573
|
+
function escapeRegex(s) {
|
|
574
|
+
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Build a dedup key for a run-log entry.
|
|
578
|
+
*/
|
|
579
|
+
function makeRunEntryKey(entry) {
|
|
580
|
+
return [
|
|
581
|
+
entry.jobId,
|
|
582
|
+
String(entry.ts),
|
|
583
|
+
entry.runId ?? "",
|
|
584
|
+
entry.status ?? "",
|
|
585
|
+
entry.summary ?? "",
|
|
586
|
+
entry.error ?? "",
|
|
587
|
+
].join("\0");
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Merge local and gateway run entries, deduplicate, and sort by ts descending.
|
|
591
|
+
* Gateway entries take precedence over local entries with the same key.
|
|
592
|
+
*/
|
|
593
|
+
function mergeAndDedupeRunEntries(localEntries, gatewayEntries) {
|
|
594
|
+
const seen = new Set();
|
|
595
|
+
const merged = [];
|
|
596
|
+
// Add gateway entries first (they take precedence)
|
|
597
|
+
for (const entry of gatewayEntries) {
|
|
598
|
+
const key = makeRunEntryKey(entry);
|
|
599
|
+
if (!seen.has(key)) {
|
|
600
|
+
seen.add(key);
|
|
601
|
+
merged.push(entry);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Add local entries that aren't already in gateway results
|
|
605
|
+
for (const entry of localEntries) {
|
|
606
|
+
const key = makeRunEntryKey(entry);
|
|
607
|
+
if (!seen.has(key)) {
|
|
608
|
+
seen.add(key);
|
|
609
|
+
merged.push(entry);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Sort by ts descending (newest first)
|
|
613
|
+
merged.sort((a, b) => b.ts - a.ts);
|
|
614
|
+
return merged;
|
|
615
|
+
}
|