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