@actiondock/core 2.9.0 → 2.10.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 +1 -1
- package/dist/catalog/graph.js +5 -2
- package/dist/doctor/checks/project.js +4 -4
- package/dist/doctor/checks/registry.js +1 -1
- package/dist/errors.d.ts +32 -1
- package/dist/errors.js +62 -2
- package/dist/host/host.d.ts +2 -0
- package/dist/host/host.js +13 -1
- package/dist/host/types.d.ts +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/input/describe.d.ts +6 -0
- package/dist/input/describe.js +26 -1
- package/dist/input/input-resolver.js +1 -1
- package/dist/invocation/policy.js +2 -0
- package/dist/ipc/host.js +1 -0
- package/dist/ipc/service.js +3 -0
- package/dist/package/runtime.d.ts +3 -0
- package/dist/package/runtime.js +8 -0
- package/dist/package/types.d.ts +4 -0
- package/dist/platform/node.js +1 -0
- package/dist/platform/types.d.ts +3 -1
- package/dist/profile/client-runs.d.ts +10 -1
- package/dist/profile/client-runs.js +14 -1
- package/dist/profile/client.d.ts +1 -1
- package/dist/profile/client.js +1 -1
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +1 -1
- package/dist/runtime/standalone.js +50 -4
- package/dist/server/routes/runs.js +50 -1
- package/dist/server/server.js +15 -0
- package/dist/service/local.js +3 -0
- package/dist/service/remote.js +16 -1
- package/dist/service/types.d.ts +4 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +6 -1
- package/dist/storage/sqlite.d.ts +18 -1
- package/dist/storage/sqlite.js +159 -2
- package/dist/storage/types.d.ts +29 -0
- package/dist/storage/types.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeStateKey } from "@actiondock/sdk";
|
|
2
2
|
import { filterWithFallbackInfo } from "../filter/index.js";
|
|
3
3
|
import { createActionDock } from "../service/factory.js";
|
|
4
|
-
import { STANDALONE_ASYNC_UNSUPPORTED } from "../errors.js";
|
|
4
|
+
import { STANDALONE_ASYNC_UNSUPPORTED, resolveExecutionHint, } from "../errors.js";
|
|
5
5
|
import { resolveActionInput, formatActionDetail, buildActionDescribePayload, mapInputValidationFailure, } from "../input/index.js";
|
|
6
6
|
import { validateActionInputValue } from "../json/value-validator.js";
|
|
7
7
|
import { normalizeActionCollection } from "./action-collection.js";
|
|
@@ -62,6 +62,10 @@ export class StandaloneDispatcher {
|
|
|
62
62
|
emitError(isJson, code, message, exitCode, options) {
|
|
63
63
|
if (isJson) {
|
|
64
64
|
const details = options?.details;
|
|
65
|
+
const hint = options?.hint ??
|
|
66
|
+
(details && typeof details === "object" && typeof details.hint === "string"
|
|
67
|
+
? details.hint
|
|
68
|
+
: undefined);
|
|
65
69
|
this.writeOut(JSON.stringify({
|
|
66
70
|
ok: false,
|
|
67
71
|
error: {
|
|
@@ -69,6 +73,7 @@ export class StandaloneDispatcher {
|
|
|
69
73
|
message,
|
|
70
74
|
...(details !== undefined ? { details } : {}),
|
|
71
75
|
},
|
|
76
|
+
...(hint !== undefined ? { hint } : {}),
|
|
72
77
|
}, null, 2));
|
|
73
78
|
}
|
|
74
79
|
else {
|
|
@@ -249,8 +254,11 @@ export class StandaloneDispatcher {
|
|
|
249
254
|
description: a.description || "",
|
|
250
255
|
}));
|
|
251
256
|
const filterRes = filterWithFallbackInfo(list, effectiveIntent, [(a) => a.id, (a) => a.description], !noFallback);
|
|
257
|
+
const hints = [
|
|
258
|
+
"Tip: For composite or multi-step tasks, check 'ad playbook list' for standard operating procedures.",
|
|
259
|
+
];
|
|
252
260
|
if (isJson) {
|
|
253
|
-
this.writeOut(JSON.stringify(filterRes.items, null, 2));
|
|
261
|
+
this.writeOut(JSON.stringify({ items: filterRes.items, hints }, null, 2));
|
|
254
262
|
}
|
|
255
263
|
else {
|
|
256
264
|
let text = `Actions in ${this.options.packageId} (v${this.options.version}):\n\n`;
|
|
@@ -272,7 +280,11 @@ export class StandaloneDispatcher {
|
|
|
272
280
|
action = await service.discovery.describeAction(id);
|
|
273
281
|
}
|
|
274
282
|
catch {
|
|
275
|
-
|
|
283
|
+
const hint = "Tip: Run 'ad list' to discover available actions.";
|
|
284
|
+
return this.emitError(isJson, "INVALID_ARGUMENT", `Action '${id}' not found`, ExitCode.INVALID_ARGUMENT, {
|
|
285
|
+
textMessage: `Error: Action '${id}' not found\n${hint}`,
|
|
286
|
+
hint,
|
|
287
|
+
});
|
|
276
288
|
}
|
|
277
289
|
const payload = buildActionDescribePayload(action, {
|
|
278
290
|
packageId: this.options.packageId,
|
|
@@ -294,8 +306,12 @@ export class StandaloneDispatcher {
|
|
|
294
306
|
let inputStr;
|
|
295
307
|
let inputFile;
|
|
296
308
|
let timeoutMs;
|
|
309
|
+
const recognizedFlags = new Set(["--json", "--async"]);
|
|
297
310
|
for (let i = 0; i < subArgs.length; i++) {
|
|
298
311
|
const arg = subArgs[i];
|
|
312
|
+
if (arg === id || recognizedFlags.has(arg)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
299
315
|
if (arg === "--timeout" && i + 1 < subArgs.length) {
|
|
300
316
|
try {
|
|
301
317
|
timeoutMs = parseDuration(subArgs[++i]);
|
|
@@ -318,12 +334,31 @@ export class StandaloneDispatcher {
|
|
|
318
334
|
else if (arg.startsWith("--input=")) {
|
|
319
335
|
inputStr = arg.slice(8);
|
|
320
336
|
}
|
|
337
|
+
else if (arg === "-i" && i + 1 < subArgs.length) {
|
|
338
|
+
inputStr = subArgs[++i];
|
|
339
|
+
}
|
|
340
|
+
else if (arg.startsWith("-i=")) {
|
|
341
|
+
inputStr = arg.slice(3);
|
|
342
|
+
}
|
|
321
343
|
else if (arg === "--input-file" && i + 1 < subArgs.length) {
|
|
322
344
|
inputFile = subArgs[++i];
|
|
323
345
|
}
|
|
324
346
|
else if (arg.startsWith("--input-file=")) {
|
|
325
347
|
inputFile = arg.slice(13);
|
|
326
348
|
}
|
|
349
|
+
else if (arg === "-f" && i + 1 < subArgs.length) {
|
|
350
|
+
inputFile = subArgs[++i];
|
|
351
|
+
}
|
|
352
|
+
else if (arg.startsWith("-f=")) {
|
|
353
|
+
inputFile = arg.slice(3);
|
|
354
|
+
}
|
|
355
|
+
else if (arg.startsWith("-")) {
|
|
356
|
+
const hint = "Hint: Separate action inputs from CLI options using '--', e.g.: ad run <id> [options] -- <param>=<val> or <param>:=<json>.";
|
|
357
|
+
return this.emitError(isJson, "INVALID_ARGUMENT", `error: unknown option '${arg}'`, ExitCode.INVALID_ARGUMENT, {
|
|
358
|
+
textMessage: `error: unknown option '${arg}'\n${hint}`,
|
|
359
|
+
hint,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
327
362
|
}
|
|
328
363
|
let input;
|
|
329
364
|
try {
|
|
@@ -348,7 +383,14 @@ export class StandaloneDispatcher {
|
|
|
348
383
|
timeoutMs,
|
|
349
384
|
});
|
|
350
385
|
if (isJson) {
|
|
351
|
-
|
|
386
|
+
if (!result.ok) {
|
|
387
|
+
const hint = resolveExecutionHint(id, result.error);
|
|
388
|
+
const machineOutput = hint !== undefined ? { ...result, hint } : result;
|
|
389
|
+
this.writeOut(JSON.stringify(machineOutput, null, 2));
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
this.writeOut(JSON.stringify(result, null, 2));
|
|
393
|
+
}
|
|
352
394
|
}
|
|
353
395
|
else {
|
|
354
396
|
if (result.ok) {
|
|
@@ -428,6 +470,10 @@ export class StandaloneDispatcher {
|
|
|
428
470
|
? result.error.details
|
|
429
471
|
: JSON.stringify(result.error.details, null, 2));
|
|
430
472
|
}
|
|
473
|
+
const hint = resolveExecutionHint(id, result.error);
|
|
474
|
+
if (hint) {
|
|
475
|
+
this.writeErr(hint);
|
|
476
|
+
}
|
|
431
477
|
}
|
|
432
478
|
}
|
|
433
479
|
return result.ok ? ExitCode.SUCCESS : ExitCode.FAILURE;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ACTION_FORBIDDEN, CAPABILITY_UNAVAILABLE, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED, EXECUTION_FAILED, PACKAGE_FORBIDDEN, RUN_ALREADY_FINISHED, RUN_INTERRUPTED, RUN_NOT_FOUND, RUNS_CLEAR_ERROR, RUNS_LIST_ERROR, } from "../../errors.js";
|
|
2
2
|
import { isTerminalRunStatus } from "../../storage/types.js";
|
|
3
|
+
import { parseDuration } from "../../utils/index.js";
|
|
3
4
|
import { readJsonBody } from "../body.js";
|
|
4
5
|
import { getSubPath, isActionAllowedByPolicy, isPackageAllowedByPolicy, jsonResponse } from "./common.js";
|
|
5
6
|
/**
|
|
@@ -69,6 +70,31 @@ export async function handleRunsRoutes(ctx) {
|
|
|
69
70
|
const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
|
|
70
71
|
const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
|
|
71
72
|
const status = url.searchParams.get("status") || body.status || undefined;
|
|
73
|
+
const rawOlderThan = url.searchParams.get("olderThan") ||
|
|
74
|
+
url.searchParams.get("olderThanMs") ||
|
|
75
|
+
body.olderThan ||
|
|
76
|
+
body.olderThanMs;
|
|
77
|
+
let olderThanMs;
|
|
78
|
+
if (typeof rawOlderThan === "number" && rawOlderThan > 0) {
|
|
79
|
+
olderThanMs = rawOlderThan;
|
|
80
|
+
}
|
|
81
|
+
else if (typeof rawOlderThan === "string" && rawOlderThan.trim()) {
|
|
82
|
+
try {
|
|
83
|
+
olderThanMs = parseDuration(rawOlderThan);
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
}
|
|
87
|
+
const rawKeep = url.searchParams.get("keep") || body.keep;
|
|
88
|
+
let keep;
|
|
89
|
+
if (typeof rawKeep === "number" && rawKeep >= 0) {
|
|
90
|
+
keep = rawKeep;
|
|
91
|
+
}
|
|
92
|
+
else if (typeof rawKeep === "string" && rawKeep.trim()) {
|
|
93
|
+
const parsed = parseInt(rawKeep.trim(), 10);
|
|
94
|
+
if (!isNaN(parsed) && parsed >= 0) {
|
|
95
|
+
keep = parsed;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
72
98
|
if (packageId && !isPackageAllowedByPolicy(packageId, policy)) {
|
|
73
99
|
return jsonResponse({
|
|
74
100
|
ok: false,
|
|
@@ -80,7 +106,13 @@ export async function handleRunsRoutes(ctx) {
|
|
|
80
106
|
}
|
|
81
107
|
let clearedCount = 0;
|
|
82
108
|
if (service.runs.clear) {
|
|
83
|
-
clearedCount = await service.runs.clear({
|
|
109
|
+
clearedCount = await service.runs.clear({
|
|
110
|
+
packageId,
|
|
111
|
+
actionId,
|
|
112
|
+
status,
|
|
113
|
+
olderThanMs,
|
|
114
|
+
keep,
|
|
115
|
+
});
|
|
84
116
|
}
|
|
85
117
|
return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
|
|
86
118
|
}
|
|
@@ -88,6 +120,23 @@ export async function handleRunsRoutes(ctx) {
|
|
|
88
120
|
return jsonResponse({ ok: false, error: { code: RUNS_CLEAR_ERROR, message: err.message } }, 500, corsHeaders);
|
|
89
121
|
}
|
|
90
122
|
}
|
|
123
|
+
// 2.1 Runs Clean Expired: POST /api/v2/runs/clean-expired, POST /runs/clean-expired
|
|
124
|
+
if (subpath === "/runs/clean-expired" && req.method === "POST") {
|
|
125
|
+
try {
|
|
126
|
+
let body = {};
|
|
127
|
+
if (req.headers.get("content-type")?.includes("json")) {
|
|
128
|
+
body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
|
|
129
|
+
}
|
|
130
|
+
let cleanedCount = 0;
|
|
131
|
+
if (service.runs.cleanExpired) {
|
|
132
|
+
cleanedCount = await service.runs.cleanExpired(body);
|
|
133
|
+
}
|
|
134
|
+
return jsonResponse({ ok: true, cleanedCount }, 200, corsHeaders);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return jsonResponse({ ok: false, error: { code: RUNS_CLEAR_ERROR, message: err.message } }, 500, corsHeaders);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
91
140
|
// 3. Run Events SSE: GET /api/v2/runs/:runId/events (and stream alias)
|
|
92
141
|
const runEventsMatch = subpath.match(/^\/runs\/([^/]+)\/(events|stream)$/);
|
|
93
142
|
if (runEventsMatch && req.method === "GET") {
|
package/dist/server/server.js
CHANGED
|
@@ -340,6 +340,17 @@ export async function startActionDockServer(options = {}) {
|
|
|
340
340
|
}
|
|
341
341
|
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
342
342
|
const protocol = options.tls ? "https" : "http";
|
|
343
|
+
// 启动后台运行记录定期自动清理巡检定时器(默认 1 小时巡检一次)
|
|
344
|
+
const runsCleanupIntervalMs = 3600_000;
|
|
345
|
+
let runsCleanupTimer;
|
|
346
|
+
if (serviceInstance.runs?.cleanExpired) {
|
|
347
|
+
runsCleanupTimer = setInterval(() => {
|
|
348
|
+
serviceInstance?.runs?.cleanExpired?.().catch((err) => {
|
|
349
|
+
console.warn(`[ActionDockServer] Periodic runs cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
|
+
});
|
|
351
|
+
}, runsCleanupIntervalMs);
|
|
352
|
+
runsCleanupTimer.unref();
|
|
353
|
+
}
|
|
343
354
|
const instance = {
|
|
344
355
|
get port() {
|
|
345
356
|
return server.port ?? port;
|
|
@@ -353,6 +364,10 @@ export async function startActionDockServer(options = {}) {
|
|
|
353
364
|
},
|
|
354
365
|
ready: Promise.resolve(),
|
|
355
366
|
stop: async (stopOptions) => {
|
|
367
|
+
if (runsCleanupTimer) {
|
|
368
|
+
clearInterval(runsCleanupTimer);
|
|
369
|
+
runsCleanupTimer = undefined;
|
|
370
|
+
}
|
|
356
371
|
await server.stop(true);
|
|
357
372
|
if (serviceInstance) {
|
|
358
373
|
try {
|
package/dist/service/local.js
CHANGED
package/dist/service/remote.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
2
2
|
import { ActionDockError, ACTION_CANCELLED, CAPABILITY_UNAVAILABLE, INVALID_ARGUMENT, TIMEOUT, } from "../errors.js";
|
|
3
|
-
import { cancelRemoteRun, clearRemoteRuns, clearRemoteState, deleteRemoteConfig, deleteRemoteStateKey, executeRemoteAction, fetchRemoteActionShow, fetchRemoteActions, fetchRemoteConfig, fetchRemoteInfo, fetchRemotePlaybookShow, fetchRemotePlaybooks, fetchRemoteRun, fetchRemoteRuns, fetchRemoteStateList, getRemoteStateKey, setRemoteConfig, setRemoteStateKey, } from "../profile/client.js";
|
|
3
|
+
import { cancelRemoteRun, clearRemoteRuns, cleanExpiredRemoteRuns, clearRemoteState, deleteRemoteConfig, deleteRemoteStateKey, executeRemoteAction, fetchRemoteActionShow, fetchRemoteActions, fetchRemoteConfig, fetchRemoteInfo, fetchRemotePlaybookShow, fetchRemotePlaybooks, fetchRemoteRun, fetchRemoteRuns, fetchRemoteStateList, getRemoteStateKey, setRemoteConfig, setRemoteStateKey, } from "../profile/client.js";
|
|
4
4
|
import { isRemoteStateKeyNotFound, wrapRemoteError } from "./remote-errors.js";
|
|
5
5
|
import { formatTerminalRunResult, pollRunCompletion } from "./remote-polling.js";
|
|
6
6
|
import { streamRemoteEvents } from "./sse-stream.js";
|
|
@@ -306,6 +306,21 @@ export class RemoteActionDockService {
|
|
|
306
306
|
wrapRemoteError(err);
|
|
307
307
|
}
|
|
308
308
|
},
|
|
309
|
+
async cleanExpired(policy) {
|
|
310
|
+
self.assertNotClosed();
|
|
311
|
+
try {
|
|
312
|
+
const res = await cleanExpiredRemoteRuns(self.serverUrl, self.token, {
|
|
313
|
+
...policy,
|
|
314
|
+
allowInsecureHttp: self.allowInsecureHttp,
|
|
315
|
+
insecure: self.insecure,
|
|
316
|
+
dispatcher: self.dispatcher,
|
|
317
|
+
});
|
|
318
|
+
return res.cleanedCount ?? 0;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
wrapRemoteError(err);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
309
324
|
};
|
|
310
325
|
this.events = {
|
|
311
326
|
events(runId, opts) {
|
package/dist/service/types.d.ts
CHANGED
|
@@ -170,7 +170,11 @@ export interface RunsPort {
|
|
|
170
170
|
packageId?: string;
|
|
171
171
|
actionId?: string;
|
|
172
172
|
status?: string;
|
|
173
|
+
olderThanMs?: number;
|
|
174
|
+
keep?: number;
|
|
173
175
|
}): Promise<number>;
|
|
176
|
+
/** 按保留策略清理过期及超额的终态运行记录(基于时间与数量策略) */
|
|
177
|
+
cleanExpired?(policy?: import("../storage/types.js").RunsRetentionPolicy): Promise<number>;
|
|
174
178
|
}
|
|
175
179
|
/**
|
|
176
180
|
* 配置项管理服务端口。
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export declare function createStorage(packageId: string, options?: {
|
|
|
42
42
|
inMemory?: boolean;
|
|
43
43
|
customHome?: string;
|
|
44
44
|
recoverOrphans?: boolean;
|
|
45
|
+
retentionPolicy?: import("./types.js").RunsRetentionPolicy;
|
|
45
46
|
}): RuntimeStorage;
|
|
46
47
|
/**
|
|
47
48
|
* 解析全局共享数据库(global.db)文件的绝对路径。
|
package/dist/storage/index.js
CHANGED
|
@@ -59,7 +59,12 @@ export function resolveDatabasePath(packageId, options = {}) {
|
|
|
59
59
|
*/
|
|
60
60
|
export function createStorage(packageId, options = {}) {
|
|
61
61
|
const dbPath = resolveDatabasePath(packageId, options);
|
|
62
|
-
return new SqliteRuntimeStorage({
|
|
62
|
+
return new SqliteRuntimeStorage({
|
|
63
|
+
dbPath,
|
|
64
|
+
packageId,
|
|
65
|
+
recoverOrphans: options.recoverOrphans,
|
|
66
|
+
retentionPolicy: options.retentionPolicy,
|
|
67
|
+
});
|
|
63
68
|
}
|
|
64
69
|
/**
|
|
65
70
|
* 解析全局共享数据库(global.db)文件的绝对路径。
|
package/dist/storage/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, type RuntimeError, type RunRecord } from "@actiondock/sdk";
|
|
2
|
-
import { type IdempotencyCheckResult, type IdempotencyRecord, type RuntimeStorage, type StateEntry, type StorageOptions, type TerminalRunStatus } from "./types.js";
|
|
2
|
+
import { type IdempotencyCheckResult, type IdempotencyRecord, type RunsRetentionPolicy, type RuntimeStorage, type StateEntry, type StorageOptions, type TerminalRunStatus } from "./types.js";
|
|
3
3
|
export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, };
|
|
4
4
|
/**
|
|
5
5
|
* 统一 SQLite 运行时存储实现。
|
|
@@ -25,6 +25,11 @@ export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
25
25
|
private statementCache;
|
|
26
26
|
private dbPath;
|
|
27
27
|
private recoverOrphans;
|
|
28
|
+
private retentionPolicy?;
|
|
29
|
+
private lastRunsCleanupAt;
|
|
30
|
+
private runInsertCount;
|
|
31
|
+
private static readonly RUNS_CLEANUP_INTERVAL_MS;
|
|
32
|
+
private static readonly RUNS_CLEANUP_BATCH_THRESHOLD;
|
|
28
33
|
get isOpen(): boolean;
|
|
29
34
|
get closed(): boolean;
|
|
30
35
|
constructor(options: StorageOptions);
|
|
@@ -84,7 +89,19 @@ export declare class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
84
89
|
clearRuns(options?: {
|
|
85
90
|
actionId?: string;
|
|
86
91
|
status?: string;
|
|
92
|
+
olderThanMs?: number;
|
|
93
|
+
keep?: number;
|
|
87
94
|
}): number;
|
|
95
|
+
/**
|
|
96
|
+
* 按保留策略清理过期的终态运行记录与超出容量限制的最旧运行记录。
|
|
97
|
+
*
|
|
98
|
+
* 包含:
|
|
99
|
+
* - 基于时间:删除早于 maxAgeMs 的终态记录(保留 minRetainRuns 保底数量)。
|
|
100
|
+
* - 基于数量:若终态记录总数超过 maxRuns,按时间由旧至新淘汰超出部分。
|
|
101
|
+
* - 级联清理孤立的幂等去重记录。
|
|
102
|
+
*/
|
|
103
|
+
cleanExpiredRuns(policy?: RunsRetentionPolicy): number;
|
|
104
|
+
private resolveRetentionPolicy;
|
|
88
105
|
checkAndRecordIdempotency(record: IdempotencyRecord): IdempotencyCheckResult;
|
|
89
106
|
getIdempotencyRecord(ownerId: string, actionRef: string, requestId: string): IdempotencyRecord | undefined;
|
|
90
107
|
private mapRunRecord;
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -5,7 +5,7 @@ import { SystemClock } from "./clock.js";
|
|
|
5
5
|
import { createDefaultSqliteDriver } from "./driver.js";
|
|
6
6
|
import { safeParseStoredJson } from "./utils.js";
|
|
7
7
|
import { ActionDockError, AMBIGUOUS_STATE_KEY, STORED_ERROR_DECODE_FAILED, UNSUPPORTED_STORAGE_SCHEMA } from "../errors.js";
|
|
8
|
-
import { IDEMPOTENCY_RETENTION_MS, STORAGE_SCHEMA_VERSION, isTerminalRunStatus, } from "./types.js";
|
|
8
|
+
import { DEFAULT_MAX_RUNS_PER_PACKAGE, DEFAULT_MIN_RETAIN_RUNS, DEFAULT_RUNS_RETENTION_MS, IDEMPOTENCY_RETENTION_MS, STORAGE_SCHEMA_VERSION, isTerminalRunStatus, } from "./types.js";
|
|
9
9
|
export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, };
|
|
10
10
|
/**
|
|
11
11
|
* 统一 SQLite 运行时存储实现。
|
|
@@ -31,6 +31,11 @@ export class SqliteRuntimeStorage {
|
|
|
31
31
|
statementCache = new Map();
|
|
32
32
|
dbPath;
|
|
33
33
|
recoverOrphans;
|
|
34
|
+
retentionPolicy;
|
|
35
|
+
lastRunsCleanupAt = 0;
|
|
36
|
+
runInsertCount = 0;
|
|
37
|
+
static RUNS_CLEANUP_INTERVAL_MS = 3600_000;
|
|
38
|
+
static RUNS_CLEANUP_BATCH_THRESHOLD = 50;
|
|
34
39
|
get isOpen() {
|
|
35
40
|
return !this.isClosed;
|
|
36
41
|
}
|
|
@@ -43,6 +48,7 @@ export class SqliteRuntimeStorage {
|
|
|
43
48
|
const dbPath = options.dbPath || ":memory:";
|
|
44
49
|
this.dbPath = dbPath;
|
|
45
50
|
this.recoverOrphans = options.recoverOrphans === true;
|
|
51
|
+
this.retentionPolicy = options.retentionPolicy;
|
|
46
52
|
if (dbPath !== ":memory:") {
|
|
47
53
|
const dir = dirname(dbPath);
|
|
48
54
|
if (!existsSync(dir)) {
|
|
@@ -162,10 +168,11 @@ export class SqliteRuntimeStorage {
|
|
|
162
168
|
// 忽略非结构化表或兼容驱动异常
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
|
-
//
|
|
171
|
+
// 重启恢复:仅持有者身份的打开方在构造阶段收割死亡会话遗留的非终态运行并执行历史记录清理;
|
|
166
172
|
// 旁观查询打开保持只读语义,绝不触碰其他进程的在途记录
|
|
167
173
|
if (this.recoverOrphans) {
|
|
168
174
|
this.recoverDeadSessionRuns();
|
|
175
|
+
this.cleanExpiredRuns();
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
/**
|
|
@@ -504,6 +511,19 @@ export class SqliteRuntimeStorage {
|
|
|
504
511
|
const finishedAt = record.finishedAt || null;
|
|
505
512
|
const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
|
|
506
513
|
stmt.run(record.id, rootRunId, parentRunId, record.packageId || this.packageId, packageInstanceId, record.actionId ?? "", generationId, ownerId, hostSessionId, record.status, inputJson, outputJson, errorJson, record.startedAt || new Date().toISOString(), finishedAt, durationMs);
|
|
514
|
+
this.runInsertCount++;
|
|
515
|
+
const nowMs = this.clock.now().getTime();
|
|
516
|
+
if (this.runInsertCount >= SqliteRuntimeStorage.RUNS_CLEANUP_BATCH_THRESHOLD &&
|
|
517
|
+
nowMs - this.lastRunsCleanupAt >= SqliteRuntimeStorage.RUNS_CLEANUP_INTERVAL_MS) {
|
|
518
|
+
this.lastRunsCleanupAt = nowMs;
|
|
519
|
+
this.runInsertCount = 0;
|
|
520
|
+
try {
|
|
521
|
+
this.cleanExpiredRuns();
|
|
522
|
+
}
|
|
523
|
+
catch (cleanupErr) {
|
|
524
|
+
console.warn(`[actiondock] opportunistic cleanExpiredRuns failed: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
507
527
|
}
|
|
508
528
|
updateRun(id, status, output, error, finishedAt) {
|
|
509
529
|
if (this.isClosed)
|
|
@@ -569,6 +589,27 @@ export class SqliteRuntimeStorage {
|
|
|
569
589
|
sql += " AND status = ?";
|
|
570
590
|
params.push(options.status);
|
|
571
591
|
}
|
|
592
|
+
if (typeof options.olderThanMs === "number" && options.olderThanMs > 0) {
|
|
593
|
+
const cutoff = new Date(this.clock.now().getTime() - options.olderThanMs).toISOString();
|
|
594
|
+
sql += " AND started_at < ?";
|
|
595
|
+
params.push(cutoff);
|
|
596
|
+
}
|
|
597
|
+
if (typeof options.keep === "number" && options.keep > 0) {
|
|
598
|
+
let subQuery = "SELECT id FROM runs WHERE package_id = ?";
|
|
599
|
+
const subParams = [this.packageId];
|
|
600
|
+
if (options.actionId) {
|
|
601
|
+
subQuery += " AND action_id = ?";
|
|
602
|
+
subParams.push(options.actionId);
|
|
603
|
+
}
|
|
604
|
+
if (options.status) {
|
|
605
|
+
subQuery += " AND status = ?";
|
|
606
|
+
subParams.push(options.status);
|
|
607
|
+
}
|
|
608
|
+
subQuery += " ORDER BY started_at DESC LIMIT ?";
|
|
609
|
+
subParams.push(options.keep);
|
|
610
|
+
sql += ` AND id NOT IN (${subQuery})`;
|
|
611
|
+
params.push(...subParams);
|
|
612
|
+
}
|
|
572
613
|
const stmt = this.getStatement(sql);
|
|
573
614
|
const res = stmt.run(...params);
|
|
574
615
|
// 运行记录清理后,级联清理已无对应运行的孤立去重索引记录
|
|
@@ -581,6 +622,122 @@ export class SqliteRuntimeStorage {
|
|
|
581
622
|
}
|
|
582
623
|
return res.changes;
|
|
583
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* 按保留策略清理过期的终态运行记录与超出容量限制的最旧运行记录。
|
|
627
|
+
*
|
|
628
|
+
* 包含:
|
|
629
|
+
* - 基于时间:删除早于 maxAgeMs 的终态记录(保留 minRetainRuns 保底数量)。
|
|
630
|
+
* - 基于数量:若终态记录总数超过 maxRuns,按时间由旧至新淘汰超出部分。
|
|
631
|
+
* - 级联清理孤立的幂等去重记录。
|
|
632
|
+
*/
|
|
633
|
+
cleanExpiredRuns(policy) {
|
|
634
|
+
if (this.isClosed)
|
|
635
|
+
return 0;
|
|
636
|
+
try {
|
|
637
|
+
const resolved = this.resolveRetentionPolicy(policy);
|
|
638
|
+
const { maxAgeMs, maxRuns, minRetainRuns } = resolved;
|
|
639
|
+
let totalDeleted = 0;
|
|
640
|
+
// 1. 基于时间策略清理(仅针对终态记录)
|
|
641
|
+
if (maxAgeMs > 0) {
|
|
642
|
+
const now = this.clock.now().getTime();
|
|
643
|
+
const cutoff = new Date(now - maxAgeMs).toISOString();
|
|
644
|
+
if (minRetainRuns > 0) {
|
|
645
|
+
const stmt = this.getStatement(`
|
|
646
|
+
DELETE FROM runs
|
|
647
|
+
WHERE package_id = ?
|
|
648
|
+
AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
|
|
649
|
+
AND started_at < ?
|
|
650
|
+
AND id NOT IN (
|
|
651
|
+
SELECT id FROM runs
|
|
652
|
+
WHERE package_id = ?
|
|
653
|
+
AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
|
|
654
|
+
ORDER BY started_at DESC
|
|
655
|
+
LIMIT ?
|
|
656
|
+
)
|
|
657
|
+
`);
|
|
658
|
+
const res = stmt.run(this.packageId, cutoff, this.packageId, minRetainRuns);
|
|
659
|
+
totalDeleted += res.changes;
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
const stmt = this.getStatement(`
|
|
663
|
+
DELETE FROM runs
|
|
664
|
+
WHERE package_id = ?
|
|
665
|
+
AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
|
|
666
|
+
AND started_at < ?
|
|
667
|
+
`);
|
|
668
|
+
const res = stmt.run(this.packageId, cutoff);
|
|
669
|
+
totalDeleted += res.changes;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// 2. 基于数量策略清理(若当前终态记录数超过 maxRuns,淘汰最旧的记录)
|
|
673
|
+
if (maxRuns > 0) {
|
|
674
|
+
const stmt = this.getStatement(`
|
|
675
|
+
DELETE FROM runs
|
|
676
|
+
WHERE package_id = ?
|
|
677
|
+
AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
|
|
678
|
+
AND id NOT IN (
|
|
679
|
+
SELECT id FROM runs
|
|
680
|
+
WHERE package_id = ?
|
|
681
|
+
AND status IN ('success', 'failed', 'cancelled', 'timed_out', 'interrupted')
|
|
682
|
+
ORDER BY started_at DESC
|
|
683
|
+
LIMIT ?
|
|
684
|
+
)
|
|
685
|
+
`);
|
|
686
|
+
const res = stmt.run(this.packageId, this.packageId, maxRuns);
|
|
687
|
+
totalDeleted += res.changes;
|
|
688
|
+
}
|
|
689
|
+
// 3. 级联清理无对应运行记录的孤立幂等去重索引
|
|
690
|
+
if (totalDeleted > 0) {
|
|
691
|
+
try {
|
|
692
|
+
this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
console.warn(`[actiondock] orphaned idempotency keys cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return totalDeleted;
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
console.warn(`[actiondock] cleanExpiredRuns failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
resolveRetentionPolicy(policy) {
|
|
706
|
+
let configDays;
|
|
707
|
+
let configMaxRuns;
|
|
708
|
+
let configMinRetain;
|
|
709
|
+
try {
|
|
710
|
+
configDays = this.getConfig("runs.retentionDays");
|
|
711
|
+
configMaxRuns = this.getConfig("runs.maxRuns");
|
|
712
|
+
configMinRetain = this.getConfig("runs.minRetainRuns");
|
|
713
|
+
}
|
|
714
|
+
catch { }
|
|
715
|
+
const envDays = process.env.ACTIONDOCK_RUNS_RETENTION_DAYS
|
|
716
|
+
? parseInt(process.env.ACTIONDOCK_RUNS_RETENTION_DAYS, 10)
|
|
717
|
+
: undefined;
|
|
718
|
+
const envMaxRuns = process.env.ACTIONDOCK_RUNS_MAX_COUNT
|
|
719
|
+
? parseInt(process.env.ACTIONDOCK_RUNS_MAX_COUNT, 10)
|
|
720
|
+
: undefined;
|
|
721
|
+
const envMinRetain = process.env.ACTIONDOCK_RUNS_MIN_RETAIN
|
|
722
|
+
? parseInt(process.env.ACTIONDOCK_RUNS_MIN_RETAIN, 10)
|
|
723
|
+
: undefined;
|
|
724
|
+
const maxAgeMs = policy?.maxAgeMs ??
|
|
725
|
+
this.retentionPolicy?.maxAgeMs ??
|
|
726
|
+
(typeof configDays === "number" && configDays > 0 ? configDays * 86_400_000 : undefined) ??
|
|
727
|
+
(typeof envDays === "number" && !isNaN(envDays) && envDays > 0 ? envDays * 86_400_000 : undefined) ??
|
|
728
|
+
DEFAULT_RUNS_RETENTION_MS;
|
|
729
|
+
const maxRuns = policy?.maxRuns ??
|
|
730
|
+
this.retentionPolicy?.maxRuns ??
|
|
731
|
+
(typeof configMaxRuns === "number" && configMaxRuns > 0 ? configMaxRuns : undefined) ??
|
|
732
|
+
(typeof envMaxRuns === "number" && !isNaN(envMaxRuns) && envMaxRuns > 0 ? envMaxRuns : undefined) ??
|
|
733
|
+
DEFAULT_MAX_RUNS_PER_PACKAGE;
|
|
734
|
+
const minRetainRuns = policy?.minRetainRuns ??
|
|
735
|
+
this.retentionPolicy?.minRetainRuns ??
|
|
736
|
+
(typeof configMinRetain === "number" && configMinRetain >= 0 ? configMinRetain : undefined) ??
|
|
737
|
+
(typeof envMinRetain === "number" && !isNaN(envMinRetain) && envMinRetain >= 0 ? envMinRetain : undefined) ??
|
|
738
|
+
DEFAULT_MIN_RETAIN_RUNS;
|
|
739
|
+
return { maxAgeMs, maxRuns, minRetainRuns };
|
|
740
|
+
}
|
|
584
741
|
// --- Idempotency 幂等去重管理 ---
|
|
585
742
|
checkAndRecordIdempotency(record) {
|
|
586
743
|
if (this.isClosed) {
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -9,6 +9,29 @@ export declare const STORAGE_SCHEMA_VERSION = 2;
|
|
|
9
9
|
* 中的 idempotencyPolicy.retentionMs 保持单一事实源。
|
|
10
10
|
*/
|
|
11
11
|
export declare const IDEMPOTENCY_RETENTION_MS = 86400000;
|
|
12
|
+
/**
|
|
13
|
+
* 运行记录默认保留时长常量(14 天)。
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_RUNS_RETENTION_MS: number;
|
|
16
|
+
/**
|
|
17
|
+
* 运行记录单个包默认最大终态记录保留条数(5000 条)。
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_MAX_RUNS_PER_PACKAGE = 5000;
|
|
20
|
+
/**
|
|
21
|
+
* 运行记录时间过期清理默认保底保留条数(50 条)。
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_MIN_RETAIN_RUNS = 50;
|
|
24
|
+
/**
|
|
25
|
+
* 运行记录双重保留策略契约(基于时间与基于数量)。
|
|
26
|
+
*/
|
|
27
|
+
export interface RunsRetentionPolicy {
|
|
28
|
+
/** 最大保留时长(毫秒,默认 14 天) */
|
|
29
|
+
maxAgeMs?: number;
|
|
30
|
+
/** 单个包最大保留终态记录数(默认 5000,超出按最旧先淘汰) */
|
|
31
|
+
maxRuns?: number;
|
|
32
|
+
/** 最小保底保留终态记录数(默认 50,防止全清空) */
|
|
33
|
+
minRetainRuns?: number;
|
|
34
|
+
}
|
|
12
35
|
/**
|
|
13
36
|
* SQLite 基础参数值类型。
|
|
14
37
|
*/
|
|
@@ -95,6 +118,8 @@ export interface StorageOptions {
|
|
|
95
118
|
* 避免跨进程互毁在途运行。
|
|
96
119
|
*/
|
|
97
120
|
recoverOrphans?: boolean;
|
|
121
|
+
/** 运行记录保留策略配置 */
|
|
122
|
+
retentionPolicy?: RunsRetentionPolicy;
|
|
98
123
|
}
|
|
99
124
|
/**
|
|
100
125
|
* 数据库中存储的幂等请求去重记录实体。
|
|
@@ -179,7 +204,11 @@ export interface RuntimeStorage {
|
|
|
179
204
|
clearRuns(options?: {
|
|
180
205
|
actionId?: string;
|
|
181
206
|
status?: string;
|
|
207
|
+
olderThanMs?: number;
|
|
208
|
+
keep?: number;
|
|
182
209
|
}): number;
|
|
210
|
+
/** 按保留策略清理过期及超额的终态运行记录(基于时间与数量策略) */
|
|
211
|
+
cleanExpiredRuns?(policy?: RunsRetentionPolicy): number;
|
|
183
212
|
/**
|
|
184
213
|
* 收敛死亡会话遗留的非终态运行任务(含无会话标识的遗留非终态记录),
|
|
185
214
|
* 统一收敛为 interrupted。历史别名 recoverRunningRuns 已合并至本方法。
|
package/dist/storage/types.js
CHANGED
|
@@ -8,6 +8,18 @@ export const STORAGE_SCHEMA_VERSION = 2;
|
|
|
8
8
|
* 中的 idempotencyPolicy.retentionMs 保持单一事实源。
|
|
9
9
|
*/
|
|
10
10
|
export const IDEMPOTENCY_RETENTION_MS = 86_400_000;
|
|
11
|
+
/**
|
|
12
|
+
* 运行记录默认保留时长常量(14 天)。
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_RUNS_RETENTION_MS = 14 * 86_400_000;
|
|
15
|
+
/**
|
|
16
|
+
* 运行记录单个包默认最大终态记录保留条数(5000 条)。
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_MAX_RUNS_PER_PACKAGE = 5000;
|
|
19
|
+
/**
|
|
20
|
+
* 运行记录时间过期清理默认保底保留条数(50 条)。
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_MIN_RETAIN_RUNS = 50;
|
|
11
23
|
/**
|
|
12
24
|
* 判断指定运行状态是否为终态(收敛 success, failed, cancelled, timed_out, interrupted)。
|
|
13
25
|
*/
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "ActionDock Core Engine - Project loader, runtime execution, SQLite storage, standalone builder, and skill exporter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"test": "node ../../scripts/run-tests.ts"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@actiondock/sdk": "^2.
|
|
69
|
+
"@actiondock/sdk": "^2.10.0",
|
|
70
70
|
"ajv": "^8.17.1",
|
|
71
71
|
"ajv-formats": "^3.0.1",
|
|
72
72
|
"undici": "^7.29.1"
|