@actiondock/core 2.0.2 → 2.0.3
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 +59 -27
- package/package.json +2 -2
- package/src/build/builder.ts +20 -7
- package/src/catalog/action-index.ts +92 -0
- package/src/catalog/action-resolver.ts +120 -0
- package/src/catalog/index.ts +5 -0
- package/src/catalog/location-registry.ts +134 -0
- package/src/catalog/package-catalog.ts +87 -0
- package/src/catalog/types.ts +83 -0
- package/src/doctor/doctor.ts +59 -33
- package/src/execution/index.ts +2 -0
- package/src/execution/service.ts +282 -0
- package/src/execution/types.ts +73 -0
- package/src/export/skill.ts +7 -6
- package/src/export/templates.ts +1 -1
- package/src/index.ts +2 -0
- package/src/project/index.ts +1 -0
- package/src/project/init.ts +80 -33
- package/src/project/loader.ts +11 -12
- package/src/project/manifest.ts +96 -0
- package/src/project/types.ts +30 -0
- package/src/runtime/clock.ts +41 -0
- package/src/runtime/context.ts +24 -2
- package/src/runtime/events.ts +142 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/process.ts +244 -0
- package/src/runtime/runner.ts +37 -3
- package/src/server/server.ts +102 -8
- package/src/server/types.ts +11 -0
- package/src/storage/driver.ts +126 -0
- package/src/storage/index.ts +4 -3
- package/src/storage/sqlite.ts +331 -251
- package/src/storage/types.ts +41 -23
package/src/storage/sqlite.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { Database } from "bun:sqlite";
|
|
2
1
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
3
2
|
import { dirname } from "node:path";
|
|
4
|
-
import type { RuntimeError, RunRecord } from "@actiondock/sdk";
|
|
5
|
-
import
|
|
3
|
+
import type { JsonValue, RuntimeError, RunRecord } from "@actiondock/sdk";
|
|
4
|
+
import { createDefaultSqliteDriver } from "./driver";
|
|
5
|
+
import type {
|
|
6
|
+
RuntimeStorage,
|
|
7
|
+
SqliteDriver,
|
|
8
|
+
StateEntry,
|
|
9
|
+
StorageOptions,
|
|
10
|
+
TerminalRunStatus,
|
|
11
|
+
} from "./types";
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 关键特性:
|
|
11
|
-
* 1. 零网络与外部进程开销:直接通过 Bun C-API 高速操作 SQLite 数据库文件。
|
|
12
|
-
* 2. 高并发优化:开启 `WAL`(Write-Ahead Logging)模式与 `synchronous = NORMAL`,支持高并发读写。
|
|
13
|
-
* 3. 严格的安全权限:自动配置目录权限为 0700,数据库文件权限为 0600。
|
|
14
|
-
* 4. 自动版本迁移:基于 SQLite `user_version` PRAGMA 实现无损自动 Schema 迁移。
|
|
15
|
-
* 5. TTL 自动过期:支持状态的过期清理。
|
|
14
|
+
* 统一 SQLite 运行时存储实现。
|
|
15
|
+
* 通过 SqliteDriver 抽象驱动,解耦底层具体运行时引擎(Node.js / Bun)。
|
|
16
16
|
*/
|
|
17
17
|
export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
18
|
-
private
|
|
18
|
+
private driver: SqliteDriver;
|
|
19
19
|
private packageId: string;
|
|
20
20
|
private isClosed = false;
|
|
21
21
|
|
|
@@ -30,12 +30,13 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
30
30
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
31
31
|
chmodSync(dir, 0o700);
|
|
32
32
|
} catch {
|
|
33
|
-
//
|
|
33
|
+
// 忽略系统权限设置失败
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
this.
|
|
38
|
+
this.driver = options.driver ?? createDefaultSqliteDriver(dbPath);
|
|
39
|
+
|
|
39
40
|
if (dbPath !== ":memory:" && existsSync(dbPath)) {
|
|
40
41
|
try {
|
|
41
42
|
chmodSync(dbPath, 0o600);
|
|
@@ -47,21 +48,21 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
+
* 初始化数据库结构并执行无损版本升级。
|
|
51
52
|
*/
|
|
52
53
|
private init(): void {
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
54
|
+
this.driver.exec("PRAGMA journal_mode = WAL;");
|
|
55
|
+
this.driver.exec("PRAGMA synchronous = NORMAL;");
|
|
55
56
|
|
|
56
57
|
// 读取 Schema 版本号
|
|
57
|
-
const versionRes = this.
|
|
58
|
+
const versionRes = this.driver.prepare("PRAGMA user_version;").get<{
|
|
58
59
|
user_version: number;
|
|
59
|
-
};
|
|
60
|
+
}>();
|
|
60
61
|
const version = versionRes?.user_version ?? 0;
|
|
61
62
|
|
|
62
63
|
if (version === 0) {
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
64
|
+
this.driver.transaction(() => {
|
|
65
|
+
this.driver.exec(`
|
|
65
66
|
CREATE TABLE IF NOT EXISTS config (
|
|
66
67
|
package_id TEXT NOT NULL,
|
|
67
68
|
key TEXT NOT NULL,
|
|
@@ -82,52 +83,91 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
82
83
|
|
|
83
84
|
CREATE TABLE IF NOT EXISTS runs (
|
|
84
85
|
id TEXT PRIMARY KEY,
|
|
86
|
+
root_run_id TEXT NOT NULL,
|
|
87
|
+
parent_run_id TEXT,
|
|
85
88
|
package_id TEXT NOT NULL,
|
|
89
|
+
package_instance_id TEXT NOT NULL,
|
|
86
90
|
action_id TEXT NOT NULL,
|
|
87
|
-
|
|
91
|
+
generation_id TEXT NOT NULL,
|
|
92
|
+
owner_id TEXT NOT NULL,
|
|
88
93
|
status TEXT NOT NULL,
|
|
89
94
|
input_json TEXT,
|
|
90
95
|
output_json TEXT,
|
|
91
96
|
error_json TEXT,
|
|
92
97
|
started_at TEXT NOT NULL,
|
|
93
|
-
finished_at TEXT
|
|
98
|
+
finished_at TEXT,
|
|
99
|
+
duration_ms INTEGER
|
|
94
100
|
);
|
|
95
101
|
|
|
96
102
|
CREATE INDEX IF NOT EXISTS idx_runs_action ON runs(package_id, action_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_runs_root ON runs(root_run_id);
|
|
97
104
|
CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
|
|
98
105
|
CREATE INDEX IF NOT EXISTS idx_state_expires ON state(expires_at);
|
|
99
106
|
|
|
100
|
-
PRAGMA user_version =
|
|
107
|
+
PRAGMA user_version = 3;
|
|
101
108
|
`);
|
|
102
|
-
})
|
|
109
|
+
});
|
|
103
110
|
} else if (version < 2) {
|
|
104
|
-
this.
|
|
111
|
+
this.driver.transaction(() => {
|
|
105
112
|
try {
|
|
106
|
-
const columns = this.
|
|
107
|
-
.
|
|
108
|
-
.all
|
|
113
|
+
const columns = this.driver
|
|
114
|
+
.prepare("PRAGMA table_info(state);")
|
|
115
|
+
.all<{ name: string }>();
|
|
109
116
|
const hasExpiresAt = columns.some((c) => c.name === "expires_at");
|
|
110
117
|
if (!hasExpiresAt) {
|
|
111
|
-
this.
|
|
118
|
+
this.driver.exec("ALTER TABLE state ADD COLUMN expires_at TEXT;");
|
|
112
119
|
}
|
|
113
120
|
} catch {
|
|
114
|
-
//
|
|
121
|
+
// 忽略表已存在或字段已添加
|
|
115
122
|
}
|
|
116
|
-
this.
|
|
123
|
+
this.driver.exec(
|
|
117
124
|
"CREATE INDEX IF NOT EXISTS idx_state_expires ON state(expires_at);"
|
|
118
125
|
);
|
|
119
|
-
this.
|
|
120
|
-
})
|
|
126
|
+
this.driver.exec("PRAGMA user_version = 2;");
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 升级至版本 3:补齐运行记录调用链与实例字段
|
|
131
|
+
if (version < 3) {
|
|
132
|
+
this.driver.transaction(() => {
|
|
133
|
+
try {
|
|
134
|
+
const columns = this.driver
|
|
135
|
+
.prepare("PRAGMA table_info(runs);")
|
|
136
|
+
.all<{ name: string }>();
|
|
137
|
+
const columnNames = new Set(columns.map((c) => c.name));
|
|
138
|
+
|
|
139
|
+
if (!columnNames.has("root_run_id")) {
|
|
140
|
+
this.driver.exec("ALTER TABLE runs ADD COLUMN root_run_id TEXT DEFAULT '';");
|
|
141
|
+
this.driver.exec("UPDATE runs SET root_run_id = id WHERE root_run_id = '' OR root_run_id IS NULL;");
|
|
142
|
+
}
|
|
143
|
+
if (!columnNames.has("package_instance_id")) {
|
|
144
|
+
this.driver.exec("ALTER TABLE runs ADD COLUMN package_instance_id TEXT DEFAULT '';");
|
|
145
|
+
}
|
|
146
|
+
if (!columnNames.has("generation_id")) {
|
|
147
|
+
this.driver.exec("ALTER TABLE runs ADD COLUMN generation_id TEXT DEFAULT '1';");
|
|
148
|
+
}
|
|
149
|
+
if (!columnNames.has("owner_id")) {
|
|
150
|
+
this.driver.exec("ALTER TABLE runs ADD COLUMN owner_id TEXT DEFAULT 'local';");
|
|
151
|
+
}
|
|
152
|
+
if (!columnNames.has("duration_ms")) {
|
|
153
|
+
this.driver.exec("ALTER TABLE runs ADD COLUMN duration_ms INTEGER;");
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// 忽略已存在的字段
|
|
157
|
+
}
|
|
158
|
+
this.driver.exec("CREATE INDEX IF NOT EXISTS idx_runs_root ON runs(root_run_id);");
|
|
159
|
+
this.driver.exec("PRAGMA user_version = 3;");
|
|
160
|
+
});
|
|
121
161
|
}
|
|
122
162
|
}
|
|
123
163
|
|
|
124
|
-
// --- Config ---
|
|
164
|
+
// --- Config 配置管理 ---
|
|
125
165
|
|
|
126
166
|
getConfig<T = unknown>(key: string): T | undefined {
|
|
127
|
-
const stmt = this.
|
|
167
|
+
const stmt = this.driver.prepare(
|
|
128
168
|
"SELECT value_json FROM config WHERE package_id = ? AND key = ?"
|
|
129
169
|
);
|
|
130
|
-
const row = stmt.get
|
|
170
|
+
const row = stmt.get<{ value_json: string }>(this.packageId, key);
|
|
131
171
|
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
132
172
|
return undefined;
|
|
133
173
|
}
|
|
@@ -139,13 +179,10 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
listConfig(): Record<string, unknown> {
|
|
142
|
-
const stmt = this.
|
|
182
|
+
const stmt = this.driver.prepare(
|
|
143
183
|
"SELECT key, value_json FROM config WHERE package_id = ?"
|
|
144
184
|
);
|
|
145
|
-
const rows = stmt.all(this.packageId)
|
|
146
|
-
key: string;
|
|
147
|
-
value_json: string;
|
|
148
|
-
}>;
|
|
185
|
+
const rows = stmt.all<{ key: string; value_json: string }>(this.packageId);
|
|
149
186
|
const result: Record<string, unknown> = {};
|
|
150
187
|
for (const row of rows) {
|
|
151
188
|
try {
|
|
@@ -158,7 +195,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
setConfig(key: string, value: unknown): void {
|
|
161
|
-
const stmt = this.
|
|
198
|
+
const stmt = this.driver.prepare(`
|
|
162
199
|
INSERT INTO config (package_id, key, value_json, updated_at)
|
|
163
200
|
VALUES (?, ?, ?, ?)
|
|
164
201
|
ON CONFLICT(package_id, key) DO UPDATE SET
|
|
@@ -171,36 +208,36 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
deleteConfig(key: string): boolean {
|
|
174
|
-
const stmt = this.
|
|
211
|
+
const stmt = this.driver.prepare(
|
|
175
212
|
"DELETE FROM config WHERE package_id = ? AND key = ?"
|
|
176
213
|
);
|
|
177
214
|
const res = stmt.run(this.packageId, key);
|
|
178
215
|
return res.changes > 0;
|
|
179
216
|
}
|
|
180
217
|
|
|
181
|
-
// --- State ---
|
|
218
|
+
// --- State 状态管理 ---
|
|
182
219
|
|
|
183
|
-
async getState<T = unknown>(
|
|
184
|
-
|
|
185
|
-
key: string
|
|
186
|
-
): Promise<T | undefined> {
|
|
187
|
-
const stmt = this.db.prepare(
|
|
220
|
+
async getState<T = unknown>(namespace: string, key: string): Promise<T | undefined> {
|
|
221
|
+
const stmt = this.driver.prepare(
|
|
188
222
|
"SELECT value_json, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
189
223
|
);
|
|
190
|
-
const row = stmt.get
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
224
|
+
const row = stmt.get<{ value_json: string; expires_at?: string }>(
|
|
225
|
+
this.packageId,
|
|
226
|
+
namespace,
|
|
227
|
+
key
|
|
228
|
+
);
|
|
194
229
|
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
195
230
|
return undefined;
|
|
196
231
|
}
|
|
232
|
+
|
|
197
233
|
if (row.expires_at) {
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
234
|
+
const expires = new Date(row.expires_at).getTime();
|
|
235
|
+
if (Date.now() >= expires) {
|
|
236
|
+
this.deleteState(namespace, key).catch(() => {});
|
|
201
237
|
return undefined;
|
|
202
238
|
}
|
|
203
239
|
}
|
|
240
|
+
|
|
204
241
|
try {
|
|
205
242
|
return JSON.parse(row.value_json) as T;
|
|
206
243
|
} catch {
|
|
@@ -212,60 +249,94 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
212
249
|
targetKey: string,
|
|
213
250
|
namespace?: string
|
|
214
251
|
): Promise<StateEntry | undefined> {
|
|
215
|
-
let
|
|
252
|
+
let ns = namespace;
|
|
253
|
+
let actualKey = targetKey;
|
|
216
254
|
|
|
217
|
-
if (
|
|
218
|
-
const
|
|
219
|
-
|
|
255
|
+
if (!ns && targetKey.includes(":")) {
|
|
256
|
+
const parts = targetKey.split(":");
|
|
257
|
+
ns = parts[0];
|
|
258
|
+
actualKey = parts.slice(1).join(":");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (ns) {
|
|
262
|
+
const val = await this.getState<T>(ns, actualKey);
|
|
263
|
+
if (val === undefined) return undefined;
|
|
264
|
+
|
|
265
|
+
const stmt = this.driver.prepare(
|
|
266
|
+
"SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
220
267
|
);
|
|
221
|
-
row = stmt.get
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"SELECT * FROM state WHERE package_id = ? AND namespace = '' AND key = ?"
|
|
268
|
+
const row = stmt.get<{ updated_at: string; expires_at?: string }>(
|
|
269
|
+
this.packageId,
|
|
270
|
+
ns,
|
|
271
|
+
actualKey
|
|
226
272
|
);
|
|
227
|
-
row = rootStmt.get(this.packageId, targetKey);
|
|
228
273
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
274
|
+
return {
|
|
275
|
+
packageId: this.packageId,
|
|
276
|
+
namespace: ns,
|
|
277
|
+
key: actualKey,
|
|
278
|
+
fullKey: targetKey,
|
|
279
|
+
value: val,
|
|
280
|
+
updatedAt: row?.updated_at || new Date().toISOString(),
|
|
281
|
+
expiresAt: row?.expires_at,
|
|
282
|
+
};
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
285
|
+
const val = await this.getState<T>("", actualKey);
|
|
286
|
+
if (val !== undefined) {
|
|
287
|
+
const stmt = this.driver.prepare(
|
|
288
|
+
"SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
289
|
+
);
|
|
290
|
+
const row = stmt.get<{ updated_at: string; expires_at?: string }>(
|
|
291
|
+
this.packageId,
|
|
292
|
+
"",
|
|
293
|
+
actualKey
|
|
294
|
+
);
|
|
295
|
+
return {
|
|
296
|
+
packageId: this.packageId,
|
|
297
|
+
namespace: "",
|
|
298
|
+
key: actualKey,
|
|
299
|
+
fullKey: actualKey,
|
|
300
|
+
value: val,
|
|
301
|
+
updatedAt: row?.updated_at || new Date().toISOString(),
|
|
302
|
+
expiresAt: row?.expires_at,
|
|
303
|
+
};
|
|
247
304
|
}
|
|
248
305
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
306
|
+
const stmt = this.driver.prepare(
|
|
307
|
+
"SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ? AND key = ?"
|
|
308
|
+
);
|
|
309
|
+
const rows = stmt.all<{
|
|
310
|
+
namespace: string;
|
|
311
|
+
key: string;
|
|
312
|
+
value_json: string;
|
|
313
|
+
updated_at: string;
|
|
314
|
+
expires_at?: string;
|
|
315
|
+
}>(this.packageId, actualKey);
|
|
316
|
+
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
for (const row of rows) {
|
|
319
|
+
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
let parsedVal: unknown;
|
|
323
|
+
try {
|
|
324
|
+
parsedVal = JSON.parse(row.value_json);
|
|
325
|
+
} catch {
|
|
326
|
+
parsedVal = row.value_json;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
packageId: this.packageId,
|
|
330
|
+
namespace: row.namespace,
|
|
331
|
+
key: row.key,
|
|
332
|
+
fullKey: row.namespace ? `${row.namespace}:${row.key}` : row.key,
|
|
333
|
+
value: parsedVal,
|
|
334
|
+
updatedAt: row.updated_at,
|
|
335
|
+
expiresAt: row.expires_at,
|
|
336
|
+
};
|
|
257
337
|
}
|
|
258
338
|
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
packageId: row.package_id,
|
|
262
|
-
namespace: row.namespace,
|
|
263
|
-
key: row.key,
|
|
264
|
-
fullKey,
|
|
265
|
-
value: parsedVal as T,
|
|
266
|
-
updatedAt: row.updated_at,
|
|
267
|
-
expiresAt: row.expires_at || undefined,
|
|
268
|
-
};
|
|
339
|
+
return undefined;
|
|
269
340
|
}
|
|
270
341
|
|
|
271
342
|
async setState<T = unknown>(
|
|
@@ -274,12 +345,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
274
345
|
value: T,
|
|
275
346
|
ttl?: number
|
|
276
347
|
): Promise<void> {
|
|
277
|
-
const
|
|
278
|
-
typeof ttl === "number" && ttl > 0
|
|
279
|
-
? new Date(Date.now() + ttl * 1000).toISOString()
|
|
280
|
-
: null;
|
|
281
|
-
|
|
282
|
-
const stmt = this.db.prepare(`
|
|
348
|
+
const stmt = this.driver.prepare(`
|
|
283
349
|
INSERT INTO state (package_id, namespace, key, value_json, updated_at, expires_at)
|
|
284
350
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
285
351
|
ON CONFLICT(package_id, namespace, key) DO UPDATE SET
|
|
@@ -288,195 +354,207 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
288
354
|
expires_at = excluded.expires_at
|
|
289
355
|
`);
|
|
290
356
|
const valJson = JSON.stringify(value);
|
|
291
|
-
const now = new Date()
|
|
292
|
-
|
|
357
|
+
const now = new Date();
|
|
358
|
+
const updatedAt = now.toISOString();
|
|
359
|
+
|
|
360
|
+
let expiresAt: string | null = null;
|
|
361
|
+
if (typeof ttl === "number" && ttl > 0) {
|
|
362
|
+
expiresAt = new Date(now.getTime() + ttl * 1000).toISOString();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
stmt.run(this.packageId, namespace, key, valJson, updatedAt, expiresAt);
|
|
293
366
|
}
|
|
294
367
|
|
|
295
368
|
async deleteState(namespace: string, key: string): Promise<boolean> {
|
|
296
|
-
const stmt = this.
|
|
369
|
+
const stmt = this.driver.prepare(
|
|
297
370
|
"DELETE FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
298
371
|
);
|
|
299
372
|
const res = stmt.run(this.packageId, namespace, key);
|
|
300
373
|
return res.changes > 0;
|
|
301
374
|
}
|
|
302
375
|
|
|
303
|
-
async deleteStateSmart(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const rootDeleted = await this.deleteState("", targetKey);
|
|
312
|
-
if (rootDeleted) return true;
|
|
313
|
-
|
|
314
|
-
// 2. 若 targetKey 包含 ':',尝试按 (namespace || ':' || key) 复合键删除
|
|
315
|
-
if (targetKey.includes(":")) {
|
|
316
|
-
const stmt = this.db.prepare(
|
|
317
|
-
"DELETE FROM state WHERE package_id = ? AND (namespace || ':' || key) = ?"
|
|
318
|
-
);
|
|
319
|
-
const res = stmt.run(this.packageId, targetKey);
|
|
320
|
-
return res.changes > 0;
|
|
376
|
+
async deleteStateSmart(targetKey: string, namespace?: string): Promise<boolean> {
|
|
377
|
+
let ns = namespace;
|
|
378
|
+
let actualKey = targetKey;
|
|
379
|
+
|
|
380
|
+
if (!ns && targetKey.includes(":")) {
|
|
381
|
+
const parts = targetKey.split(":");
|
|
382
|
+
ns = parts[0];
|
|
383
|
+
actualKey = parts.slice(1).join(":");
|
|
321
384
|
}
|
|
322
385
|
|
|
323
|
-
|
|
386
|
+
if (ns !== undefined) {
|
|
387
|
+
return this.deleteState(ns, actualKey);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const deletedRoot = await this.deleteState("", actualKey);
|
|
391
|
+
if (deletedRoot) return true;
|
|
392
|
+
|
|
393
|
+
const stmt = this.driver.prepare(
|
|
394
|
+
"DELETE FROM state WHERE package_id = ? AND key = ?"
|
|
395
|
+
);
|
|
396
|
+
const res = stmt.run(this.packageId, actualKey);
|
|
397
|
+
return res.changes > 0;
|
|
324
398
|
}
|
|
325
399
|
|
|
326
400
|
async clearState(
|
|
327
401
|
options: { namespace?: string; all?: boolean; prefix?: string } = {}
|
|
328
402
|
): Promise<number> {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const res = stmt.run(this.packageId);
|
|
332
|
-
return res.changes;
|
|
333
|
-
}
|
|
403
|
+
let sql = "DELETE FROM state WHERE package_id = ?";
|
|
404
|
+
const params: any[] = [this.packageId];
|
|
334
405
|
|
|
335
406
|
if (options.namespace !== undefined) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
"DELETE FROM state WHERE package_id = ? AND namespace = ? AND key LIKE ?"
|
|
339
|
-
);
|
|
340
|
-
const res = stmt.run(
|
|
341
|
-
this.packageId,
|
|
342
|
-
options.namespace,
|
|
343
|
-
`${options.prefix}%`
|
|
344
|
-
);
|
|
345
|
-
return res.changes;
|
|
346
|
-
} else {
|
|
347
|
-
const stmt = this.db.prepare(
|
|
348
|
-
"DELETE FROM state WHERE package_id = ? AND namespace = ?"
|
|
349
|
-
);
|
|
350
|
-
const res = stmt.run(this.packageId, options.namespace);
|
|
351
|
-
return res.changes;
|
|
352
|
-
}
|
|
407
|
+
sql += " AND namespace = ?";
|
|
408
|
+
params.push(options.namespace);
|
|
353
409
|
}
|
|
354
410
|
|
|
355
411
|
if (options.prefix) {
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
const res = stmt.run(this.packageId, pattern);
|
|
361
|
-
return res.changes;
|
|
412
|
+
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
413
|
+
const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
|
|
414
|
+
params.push(`${escapedPrefix}%`);
|
|
362
415
|
}
|
|
363
416
|
|
|
364
|
-
const stmt = this.
|
|
365
|
-
|
|
366
|
-
);
|
|
367
|
-
const res = stmt.run(this.packageId);
|
|
417
|
+
const stmt = this.driver.prepare(sql);
|
|
418
|
+
const res = stmt.run(...params);
|
|
368
419
|
return res.changes;
|
|
369
420
|
}
|
|
370
421
|
|
|
371
422
|
async listStateKeys(
|
|
372
423
|
namespace?: string | null,
|
|
373
|
-
prefix
|
|
424
|
+
prefix?: string
|
|
374
425
|
): Promise<string[]> {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
} catch {
|
|
382
|
-
// Best-effort cleanup
|
|
426
|
+
let sql = "SELECT namespace, key, expires_at FROM state WHERE package_id = ?";
|
|
427
|
+
const params: any[] = [this.packageId];
|
|
428
|
+
|
|
429
|
+
if (namespace !== null && namespace !== undefined) {
|
|
430
|
+
sql += " AND namespace = ?";
|
|
431
|
+
params.push(namespace);
|
|
383
432
|
}
|
|
384
433
|
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
);
|
|
390
|
-
const rows = stmt.all(this.packageId, nowStr, pattern) as Array<{
|
|
391
|
-
namespace: string;
|
|
392
|
-
key: string;
|
|
393
|
-
}>;
|
|
394
|
-
return rows.map((r) => (r.namespace ? `${r.namespace}:${r.key}` : r.key));
|
|
395
|
-
} else {
|
|
396
|
-
const pattern = prefix ? `${prefix}%` : "%";
|
|
397
|
-
const stmt = this.db.prepare(
|
|
398
|
-
"SELECT key FROM state WHERE package_id = ? AND namespace = ? AND key LIKE ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY key ASC"
|
|
399
|
-
);
|
|
400
|
-
const rows = stmt.all(
|
|
401
|
-
this.packageId,
|
|
402
|
-
namespace,
|
|
403
|
-
pattern,
|
|
404
|
-
nowStr
|
|
405
|
-
) as Array<{
|
|
406
|
-
key: string;
|
|
407
|
-
}>;
|
|
408
|
-
return rows.map((r) => r.key);
|
|
434
|
+
if (prefix) {
|
|
435
|
+
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
436
|
+
const escapedPrefix = prefix.replace(/([%_\\])/g, "\\$1");
|
|
437
|
+
params.push(`${escapedPrefix}%`);
|
|
409
438
|
}
|
|
439
|
+
|
|
440
|
+
const stmt = this.driver.prepare(sql);
|
|
441
|
+
const rows = stmt.all<{
|
|
442
|
+
namespace: string;
|
|
443
|
+
key: string;
|
|
444
|
+
expires_at?: string;
|
|
445
|
+
}>(...params);
|
|
446
|
+
|
|
447
|
+
const now = Date.now();
|
|
448
|
+
const result: string[] = [];
|
|
449
|
+
|
|
450
|
+
for (const row of rows) {
|
|
451
|
+
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (namespace !== null && namespace !== undefined) {
|
|
455
|
+
result.push(row.key);
|
|
456
|
+
} else {
|
|
457
|
+
result.push(row.namespace ? `${row.namespace}:${row.key}` : row.key);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return result;
|
|
410
462
|
}
|
|
411
463
|
|
|
412
464
|
async listStateEntries(
|
|
413
465
|
options: { namespace?: string; prefix?: string } = {}
|
|
414
466
|
): Promise<StateEntry[]> {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const cleanupStmt = this.db.prepare(
|
|
418
|
-
"DELETE FROM state WHERE package_id = ? AND expires_at IS NOT NULL AND expires_at <= ?"
|
|
419
|
-
);
|
|
420
|
-
cleanupStmt.run(this.packageId, nowStr);
|
|
421
|
-
} catch {}
|
|
422
|
-
|
|
423
|
-
const pattern = options.prefix ? `${options.prefix}%` : "%";
|
|
424
|
-
let rows: any[] = [];
|
|
467
|
+
let sql = "SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ?";
|
|
468
|
+
const params: any[] = [this.packageId];
|
|
425
469
|
|
|
426
470
|
if (options.namespace !== undefined) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
430
|
-
rows = stmt.all(this.packageId, options.namespace, pattern, nowStr);
|
|
431
|
-
} else {
|
|
432
|
-
const stmt = this.db.prepare(
|
|
433
|
-
"SELECT * FROM state WHERE package_id = ? AND (expires_at IS NULL OR expires_at > ?) AND (CASE WHEN namespace = '' THEN key ELSE (namespace || ':' || key) END) LIKE ? ORDER BY namespace ASC, key ASC"
|
|
434
|
-
);
|
|
435
|
-
rows = stmt.all(this.packageId, nowStr, pattern);
|
|
471
|
+
sql += " AND namespace = ?";
|
|
472
|
+
params.push(options.namespace);
|
|
436
473
|
}
|
|
437
474
|
|
|
438
|
-
|
|
475
|
+
if (options.prefix) {
|
|
476
|
+
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
477
|
+
const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
|
|
478
|
+
params.push(`${escapedPrefix}%`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const stmt = this.driver.prepare(sql);
|
|
482
|
+
const rows = stmt.all<{
|
|
483
|
+
namespace: string;
|
|
484
|
+
key: string;
|
|
485
|
+
value_json: string;
|
|
486
|
+
updated_at: string;
|
|
487
|
+
expires_at?: string;
|
|
488
|
+
}>(...params);
|
|
489
|
+
|
|
490
|
+
const now = Date.now();
|
|
491
|
+
const results: StateEntry[] = [];
|
|
492
|
+
|
|
493
|
+
for (const row of rows) {
|
|
494
|
+
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
439
498
|
let parsedVal: unknown;
|
|
440
499
|
try {
|
|
441
|
-
parsedVal =
|
|
442
|
-
row.value_json !== null && row.value_json !== undefined
|
|
443
|
-
? JSON.parse(row.value_json)
|
|
444
|
-
: row.value_json;
|
|
500
|
+
parsedVal = JSON.parse(row.value_json);
|
|
445
501
|
} catch {
|
|
446
502
|
parsedVal = row.value_json;
|
|
447
503
|
}
|
|
448
|
-
|
|
449
|
-
|
|
504
|
+
|
|
505
|
+
results.push({
|
|
506
|
+
packageId: this.packageId,
|
|
450
507
|
namespace: row.namespace,
|
|
451
508
|
key: row.key,
|
|
452
509
|
fullKey: row.namespace ? `${row.namespace}:${row.key}` : row.key,
|
|
453
510
|
value: parsedVal,
|
|
454
511
|
updatedAt: row.updated_at,
|
|
455
|
-
expiresAt: row.expires_at
|
|
456
|
-
};
|
|
457
|
-
}
|
|
512
|
+
expiresAt: row.expires_at,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return results;
|
|
458
517
|
}
|
|
459
518
|
|
|
460
|
-
// --- Runs ---
|
|
519
|
+
// --- Runs 运行记录管理 ---
|
|
461
520
|
|
|
462
|
-
createRun(record: RunRecord): void {
|
|
463
|
-
const stmt = this.
|
|
521
|
+
createRun(record: RunRecord | any): void {
|
|
522
|
+
const stmt = this.driver.prepare(`
|
|
464
523
|
INSERT INTO runs (
|
|
465
|
-
id,
|
|
466
|
-
|
|
467
|
-
|
|
524
|
+
id, root_run_id, parent_run_id, package_id, package_instance_id,
|
|
525
|
+
action_id, generation_id, owner_id, status, input_json, output_json,
|
|
526
|
+
error_json, started_at, finished_at, duration_ms
|
|
527
|
+
)
|
|
528
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
468
529
|
`);
|
|
530
|
+
|
|
531
|
+
const rootRunId = record.rootRunId || record.id;
|
|
532
|
+
const parentRunId = record.parentRunId || null;
|
|
533
|
+
const packageInstanceId = record.packageInstanceId || record.packageId || this.packageId;
|
|
534
|
+
const generationId = record.generationId || "1";
|
|
535
|
+
const ownerId = record.ownerId || "local";
|
|
536
|
+
const inputJson = record.input !== undefined ? JSON.stringify(record.input) : null;
|
|
537
|
+
const outputJson = record.output !== undefined ? JSON.stringify(record.output) : null;
|
|
538
|
+
const errorJson = record.error ? JSON.stringify(record.error) : null;
|
|
539
|
+
const finishedAt = record.finishedAt || null;
|
|
540
|
+
const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
|
|
541
|
+
|
|
469
542
|
stmt.run(
|
|
470
543
|
record.id,
|
|
471
|
-
|
|
544
|
+
rootRunId,
|
|
545
|
+
parentRunId,
|
|
546
|
+
record.packageId || this.packageId,
|
|
547
|
+
packageInstanceId,
|
|
472
548
|
record.actionId,
|
|
473
|
-
|
|
549
|
+
generationId,
|
|
550
|
+
ownerId,
|
|
474
551
|
record.status,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
552
|
+
inputJson,
|
|
553
|
+
outputJson,
|
|
554
|
+
errorJson,
|
|
478
555
|
record.startedAt,
|
|
479
|
-
|
|
556
|
+
finishedAt,
|
|
557
|
+
durationMs
|
|
480
558
|
);
|
|
481
559
|
}
|
|
482
560
|
|
|
@@ -489,12 +567,9 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
489
567
|
): void {
|
|
490
568
|
if (this.isClosed) return;
|
|
491
569
|
try {
|
|
492
|
-
const stmt = this.
|
|
493
|
-
UPDATE runs
|
|
494
|
-
|
|
495
|
-
output_json = ?,
|
|
496
|
-
error_json = ?,
|
|
497
|
-
finished_at = ?
|
|
570
|
+
const stmt = this.driver.prepare(`
|
|
571
|
+
UPDATE runs
|
|
572
|
+
SET status = ?, output_json = ?, error_json = ?, finished_at = ?
|
|
498
573
|
WHERE id = ?
|
|
499
574
|
`);
|
|
500
575
|
stmt.run(
|
|
@@ -505,15 +580,15 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
505
580
|
id
|
|
506
581
|
);
|
|
507
582
|
} catch {
|
|
508
|
-
//
|
|
583
|
+
// 数据库已关闭或操作异常,安全忽略
|
|
509
584
|
}
|
|
510
585
|
}
|
|
511
586
|
|
|
512
587
|
getRun(id: string): RunRecord | null {
|
|
513
|
-
const stmt = this.
|
|
588
|
+
const stmt = this.driver.prepare(
|
|
514
589
|
"SELECT * FROM runs WHERE id = ? AND package_id = ?"
|
|
515
590
|
);
|
|
516
|
-
const row = stmt.get(id, this.packageId)
|
|
591
|
+
const row = stmt.get<any>(id, this.packageId);
|
|
517
592
|
if (!row) return null;
|
|
518
593
|
return this.mapRunRecord(row);
|
|
519
594
|
}
|
|
@@ -522,7 +597,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
522
597
|
const limit = options.limit || 50;
|
|
523
598
|
let rows: any[];
|
|
524
599
|
if (options.actionId) {
|
|
525
|
-
const stmt = this.
|
|
600
|
+
const stmt = this.driver.prepare(`
|
|
526
601
|
SELECT * FROM runs
|
|
527
602
|
WHERE package_id = ? AND action_id = ?
|
|
528
603
|
ORDER BY started_at DESC
|
|
@@ -530,7 +605,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
530
605
|
`);
|
|
531
606
|
rows = stmt.all(this.packageId, options.actionId, limit);
|
|
532
607
|
} else {
|
|
533
|
-
const stmt = this.
|
|
608
|
+
const stmt = this.driver.prepare(`
|
|
534
609
|
SELECT * FROM runs
|
|
535
610
|
WHERE package_id = ?
|
|
536
611
|
ORDER BY started_at DESC
|
|
@@ -552,14 +627,14 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
552
627
|
sql += " AND status = ?";
|
|
553
628
|
params.push(options.status);
|
|
554
629
|
}
|
|
555
|
-
const stmt = this.
|
|
630
|
+
const stmt = this.driver.prepare(sql);
|
|
556
631
|
const res = stmt.run(...params);
|
|
557
632
|
return res.changes;
|
|
558
633
|
}
|
|
559
634
|
|
|
560
635
|
private mapRunRecord(row: any): RunRecord {
|
|
561
|
-
let input:
|
|
562
|
-
let output:
|
|
636
|
+
let input: JsonValue | undefined;
|
|
637
|
+
let output: JsonValue | undefined;
|
|
563
638
|
let error: RuntimeError | undefined;
|
|
564
639
|
|
|
565
640
|
try {
|
|
@@ -582,22 +657,27 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
582
657
|
|
|
583
658
|
return {
|
|
584
659
|
id: row.id,
|
|
660
|
+
rootRunId: row.root_run_id || row.id,
|
|
661
|
+
parentRunId: row.parent_run_id || undefined,
|
|
585
662
|
packageId: row.package_id,
|
|
663
|
+
packageInstanceId: row.package_instance_id || row.package_id,
|
|
586
664
|
actionId: row.action_id,
|
|
587
|
-
|
|
665
|
+
generationId: row.generation_id || "1",
|
|
666
|
+
ownerId: row.owner_id || "local",
|
|
588
667
|
status: row.status,
|
|
589
668
|
input,
|
|
590
669
|
output,
|
|
591
670
|
error,
|
|
592
671
|
startedAt: row.started_at,
|
|
593
672
|
finishedAt: row.finished_at || undefined,
|
|
673
|
+
durationMs: typeof row.duration_ms === "number" ? row.duration_ms : undefined,
|
|
594
674
|
};
|
|
595
675
|
}
|
|
596
676
|
|
|
597
677
|
close(): void {
|
|
598
678
|
this.isClosed = true;
|
|
599
679
|
try {
|
|
600
|
-
this.
|
|
680
|
+
this.driver.close();
|
|
601
681
|
} catch {
|
|
602
682
|
// 忽略重复关闭异常
|
|
603
683
|
}
|