






















11import path from "node:path";
2-import type { DatabaseSync } from "node:sqlite";
32import {
43normalizeLowercaseStringOrEmpty,
54normalizeOptionalString,
65normalizeStringifiedOptionalString,
76} from "@openclaw/normalization-core/string-coerce";
87import { uniqueValues } from "@openclaw/normalization-core/string-normalization";
9-import type { Insertable, Selectable } from "kysely";
108import { parseByteSize } from "../cli/parse-bytes.js";
119import type { CronConfig } from "../config/types.cron.js";
12-import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
13-import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
1410import {
1511openOpenClawStateDatabase,
1612runOpenClawStateWriteTransaction,
1713} from "../state/openclaw-state-db.js";
1814import type { CronRunLogEntry } from "./run-log-types.js";
19-import { parseCronRunLogEntryObject } from "./run-log/entry-codec.js";
15+import {
16+countCronRunLogRows,
17+insertCronRunLogEntry,
18+parseStoredRunLogEntry,
19+pruneCronRunLogRows,
20+readCronRunLogRows,
21+readCronRunLogRowsPage,
22+} from "./run-log/sqlite-store.js";
2023import type { CronDeliveryStatus, CronRunStatus } from "./types.js";
21242225export type { CronRunLogEntry } from "./run-log-types.js";
@@ -55,11 +58,6 @@ type AppendCronRunLogOptions = {
5558keepLines?: number | false;
5659};
576058-type CronRunLogsTable = OpenClawStateKyselyDatabase["cron_run_logs"];
59-type CronRunLogDatabase = Pick<OpenClawStateKyselyDatabase, "cron_run_logs">;
60-type CronRunLogRow = Selectable<CronRunLogsTable>;
61-type CronRunLogInsert = Insertable<CronRunLogsTable>;
62-6361const INVALID_CRON_RUN_LOG_JOB_ID_MESSAGE = "invalid cron run log job id";
64626563function assertSafeCronRunLogJobId(jobId: string): string {
@@ -132,207 +130,6 @@ function cronStoreKey(storePath: string): string {
132130return path.resolve(storePath);
133131}
134132135-function getCronRunLogKysely(db: DatabaseSync) {
136-return getNodeSqliteKysely<CronRunLogDatabase>(db);
137-}
138-139-function normalizeNumber(value: number | bigint | null): number | undefined {
140-if (typeof value === "bigint") {
141-return Number(value);
142-}
143-return typeof value === "number" ? value : undefined;
144-}
145-146-function booleanToInteger(value: boolean | undefined): number | null {
147-return typeof value === "boolean" ? (value ? 1 : 0) : null;
148-}
149-150-function integerToBoolean(value: number | bigint | null): boolean | undefined {
151-const normalized = normalizeNumber(value);
152-return normalized == null ? undefined : normalized !== 0;
153-}
154-155-function bindCronRunLogRow(params: {
156-storeKey: string;
157-seq: number;
158-entry: CronRunLogEntry;
159-}): CronRunLogInsert {
160-const entry = params.entry;
161-return {
162-store_key: params.storeKey,
163-job_id: entry.jobId,
164-seq: params.seq,
165-ts: entry.ts,
166-status: entry.status ?? null,
167-error: entry.error ?? null,
168-summary: entry.summary ?? null,
169-diagnostics_summary: entry.diagnostics?.summary ?? null,
170-delivery_status: entry.deliveryStatus ?? null,
171-delivery_error: entry.deliveryError ?? null,
172-delivered: booleanToInteger(entry.delivered),
173-session_id: entry.sessionId ?? null,
174-session_key: entry.sessionKey ?? null,
175-run_id: entry.runId ?? null,
176-run_at_ms: entry.runAtMs ?? null,
177-duration_ms: entry.durationMs ?? null,
178-next_run_at_ms: entry.nextRunAtMs ?? null,
179-model: entry.model ?? null,
180-provider: entry.provider ?? null,
181-total_tokens: entry.usage?.total_tokens ?? null,
182-entry_json: JSON.stringify(entry),
183-created_at: Date.now(),
184-};
185-}
186-187-function parseStoredRunLogEntry(row: CronRunLogRow): CronRunLogEntry | null {
188-let rawEntry: unknown;
189-try {
190-rawEntry = JSON.parse(row.entry_json);
191-} catch {
192-return null;
193-}
194-const parsed = parseCronRunLogEntryObject(rawEntry, { jobId: row.job_id });
195-if (!parsed) {
196-return null;
197-}
198-return {
199- ...parsed,
200-ts: normalizeNumber(row.ts) ?? parsed.ts,
201-jobId: row.job_id,
202-status: (row.status as CronRunStatus | null) ?? parsed.status,
203-error: row.error ?? parsed.error,
204-summary: row.summary ?? parsed.summary,
205-delivered: integerToBoolean(row.delivered) ?? parsed.delivered,
206-deliveryStatus: (row.delivery_status as CronDeliveryStatus | null) ?? parsed.deliveryStatus,
207-deliveryError: row.delivery_error ?? parsed.deliveryError,
208-sessionId: row.session_id ?? parsed.sessionId,
209-sessionKey: row.session_key ?? parsed.sessionKey,
210-runId: row.run_id ?? parsed.runId,
211-runAtMs: normalizeNumber(row.run_at_ms) ?? parsed.runAtMs,
212-durationMs: normalizeNumber(row.duration_ms) ?? parsed.durationMs,
213-nextRunAtMs: normalizeNumber(row.next_run_at_ms) ?? parsed.nextRunAtMs,
214-model: row.model ?? parsed.model,
215-provider: row.provider ?? parsed.provider,
216-};
217-}
218-219-function readCronRunLogRows(db: DatabaseSync, storeKey: string, jobId?: string): CronRunLogRow[] {
220-let query = getCronRunLogKysely(db)
221-.selectFrom("cron_run_logs")
222-.selectAll()
223-.where("store_key", "=", storeKey);
224-if (jobId) {
225-query = query.where("job_id", "=", jobId);
226-}
227-return executeSqliteQuerySync(db, query.orderBy("ts", "asc").orderBy("seq", "asc")).rows;
228-}
229-230-function buildRunLogWhereClause(params: {
231-storeKey: string;
232-jobId?: string;
233-statuses: CronRunStatus[] | null;
234-deliveryStatuses: CronDeliveryStatus[] | null;
235-runId?: string;
236-}): { whereSql: string; values: Array<string | number> } {
237-const clauses = ["store_key = ?"];
238-const values: Array<string | number> = [params.storeKey];
239-if (params.jobId) {
240-clauses.push("job_id = ?");
241-values.push(params.jobId);
242-}
243-if (params.statuses?.length) {
244-clauses.push(`status IN (${params.statuses.map(() => "?").join(", ")})`);
245-values.push(...params.statuses);
246-}
247-if (params.deliveryStatuses?.length) {
248-clauses.push(
249-`COALESCE(delivery_status, 'not-requested') IN (${params.deliveryStatuses
250- .map(() => "?")
251- .join(", ")})`,
252-);
253-values.push(...params.deliveryStatuses);
254-}
255-const runId = normalizeOptionalString(params.runId);
256-if (runId) {
257-clauses.push("run_id = ?");
258-values.push(runId);
259-}
260-return { whereSql: clauses.join(" AND "), values };
261-}
262-263-function countCronRunLogRows(
264-db: DatabaseSync,
265-whereSql: string,
266-values: Array<string | number>,
267-): number {
268-const row = db
269-.prepare(`SELECT COUNT(*) AS count FROM cron_run_logs WHERE ${whereSql}`)
270-.get(...values) as { count?: number | bigint } | undefined;
271-return normalizeNumber(row?.count ?? null) ?? 0;
272-}
273-274-function readCronRunLogRowsPage(params: {
275-db: DatabaseSync;
276-storeKey: string;
277-jobId?: string;
278-statuses: CronRunStatus[] | null;
279-deliveryStatuses: CronDeliveryStatus[] | null;
280-runId?: string;
281-sortDir: CronRunLogSortDir;
282-offset?: number;
283-limit?: number;
284-}): CronRunLogRow[] {
285-const { whereSql, values } = buildRunLogWhereClause(params);
286-const order = params.sortDir === "asc" ? "ASC" : "DESC";
287-const limitSql =
288-params.limit === undefined || params.offset === undefined ? "" : " LIMIT ? OFFSET ?";
289-const limitValues =
290-params.limit === undefined || params.offset === undefined ? [] : [params.limit, params.offset];
291-return params.db
292-.prepare(
293-`SELECT * FROM cron_run_logs WHERE ${whereSql} ORDER BY ts ${order}, seq ${order}${limitSql}`,
294-)
295-.all(...values, ...limitValues) as CronRunLogRow[];
296-}
297-298-function nextCronRunLogSeq(db: DatabaseSync, storeKey: string, jobId: string): number {
299-const row = db
300-.prepare(
301-"SELECT COALESCE(MAX(seq), 0) AS seq FROM cron_run_logs WHERE store_key = ? AND job_id = ?",
302-)
303-.get(storeKey, jobId) as { seq?: number | bigint } | undefined;
304-return (normalizeNumber(row?.seq ?? null) ?? 0) + 1;
305-}
306-307-function insertCronRunLogEntry(db: DatabaseSync, storeKey: string, entry: CronRunLogEntry): void {
308-const seq = nextCronRunLogSeq(db, storeKey, entry.jobId);
309-executeSqliteQuerySync(
310-db,
311-getCronRunLogKysely(db)
312-.insertInto("cron_run_logs")
313-.values(bindCronRunLogRow({ storeKey, seq, entry })),
314-);
315-}
316-317-function pruneCronRunLogRows(
318-db: DatabaseSync,
319-storeKey: string,
320-jobId: string,
321-keepLines: number,
322-): void {
323-const keep = Math.max(1, Math.floor(keepLines));
324-db.prepare(
325-`DELETE FROM cron_run_logs
326- WHERE store_key = ? AND job_id = ?
327- AND seq NOT IN (
328- SELECT seq FROM cron_run_logs
329- WHERE store_key = ? AND job_id = ?
330- ORDER BY seq DESC
331- LIMIT ?
332- )`,
333-).run(storeKey, jobId, storeKey, jobId, keep);
334-}
335-336133export async function appendCronRunLog(params: {
337134storePath: string;
338135entry: CronRunLogEntry;
@@ -503,14 +300,14 @@ export async function readCronRunLogEntriesPage(
503300const offset = Math.max(0, Math.floor(opts.offset ?? 0));
504301505302if (!query) {
506-const { whereSql, values } = buildRunLogWhereClause({
303+const total = countCronRunLogRows({
304+ db,
507305 storeKey,
508306 jobId,
509307 statuses,
510308 deliveryStatuses,
511309runId: opts.runId,
512310});
513-const total = countCronRunLogRows(db, whereSql, values);
514311const boundedOffset = Math.min(total, offset);
515312const entries = readCronRunLogRowsPage({
516313 db,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。