@actiondock/core 2.0.12 → 2.2.1
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 +61 -30
- package/dist/app/app.d.ts +73 -0
- package/dist/app/app.js +700 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/types.d.ts +283 -0
- package/dist/app/types.js +1 -0
- package/dist/catalog/action-index.d.ts +1 -1
- package/dist/catalog/action-index.js +93 -0
- package/dist/catalog/action-resolver.d.ts +2 -2
- package/dist/catalog/action-resolver.js +95 -0
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +5 -0
- package/dist/catalog/location-registry.d.ts +5 -5
- package/dist/catalog/location-registry.js +138 -0
- package/dist/catalog/package-catalog.d.ts +2 -2
- package/dist/catalog/package-catalog.js +76 -0
- package/dist/catalog/types.d.ts +2 -2
- package/dist/catalog/types.js +1 -0
- package/dist/doctor/doctor.d.ts +2 -1
- package/dist/doctor/doctor.js +613 -0
- package/dist/doctor/index.d.ts +2 -2
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/types.js +1 -0
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +123 -0
- package/dist/execution/index.d.ts +2 -2
- package/dist/execution/index.js +2 -0
- package/dist/execution/service.d.ts +46 -41
- package/dist/execution/service.js +633 -0
- package/dist/execution/types.d.ts +59 -2
- package/dist/execution/types.js +1 -0
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +1 -0
- package/dist/export/templates.d.ts +45 -7
- package/dist/export/templates.js +631 -0
- package/dist/filter/index.d.ts +1 -1
- package/dist/filter/index.js +1 -0
- package/dist/filter/intent.js +123 -0
- package/dist/host/host.d.ts +55 -0
- package/dist/host/host.js +740 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.js +2 -0
- package/dist/host/types.d.ts +81 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +21 -16
- package/dist/index.js +20 -8458
- package/dist/ipc/diagnostic.d.ts +65 -0
- package/dist/ipc/diagnostic.js +129 -0
- package/dist/ipc/host.d.ts +7 -0
- package/dist/ipc/host.js +93 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +4 -0
- package/dist/ipc/target.d.ts +56 -0
- package/dist/ipc/target.js +297 -0
- package/dist/ipc/types.d.ts +42 -0
- package/dist/ipc/types.js +1 -0
- package/dist/platform/default.d.ts +32 -0
- package/dist/platform/default.js +40 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/node-fs.d.ts +37 -0
- package/dist/platform/node-fs.js +81 -0
- package/dist/platform/types.d.ts +104 -0
- package/dist/platform/types.js +1 -0
- package/dist/profile/client.d.ts +58 -12
- package/dist/profile/client.js +467 -0
- package/dist/profile/index.d.ts +3 -3
- package/dist/profile/index.js +3 -0
- package/dist/profile/manager.d.ts +2 -2
- package/dist/profile/manager.js +294 -0
- package/dist/profile/types.d.ts +1 -1
- package/dist/profile/types.js +1 -0
- package/dist/project/closure.js +68 -0
- package/dist/project/digest.d.ts +29 -0
- package/dist/project/digest.js +324 -0
- package/dist/project/index.d.ts +10 -5
- package/dist/project/index.js +10 -0
- package/dist/project/init.d.ts +3 -4
- package/dist/project/init.js +175 -0
- package/dist/project/loader.d.ts +21 -27
- package/dist/project/loader.js +484 -0
- package/dist/project/lockfile.d.ts +55 -0
- package/dist/project/lockfile.js +175 -0
- package/dist/project/manifest.d.ts +6 -28
- package/dist/project/manifest.js +220 -0
- package/dist/project/resolver.d.ts +116 -0
- package/dist/project/resolver.js +376 -0
- package/dist/project/transactions.d.ts +103 -0
- package/dist/project/transactions.js +1203 -0
- package/dist/project/types-generator.d.ts +33 -0
- package/dist/project/types-generator.js +168 -0
- package/dist/project/types.d.ts +58 -82
- package/dist/project/types.js +1 -0
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +5 -0
- package/dist/registry/lock.d.ts +27 -0
- package/dist/registry/lock.js +93 -0
- package/dist/registry/registry.d.ts +32 -11
- package/dist/registry/registry.js +611 -0
- package/dist/registry/resolve.d.ts +62 -0
- package/dist/registry/resolve.js +195 -0
- package/dist/registry/scan.d.ts +38 -0
- package/dist/registry/scan.js +135 -0
- package/dist/registry/types.d.ts +1 -1
- package/dist/registry/types.js +1 -0
- package/dist/runtime/action-collection.d.ts +27 -0
- package/dist/runtime/action-collection.js +57 -0
- package/dist/runtime/clock.d.ts +0 -2
- package/dist/runtime/clock.js +17 -0
- package/dist/runtime/context.d.ts +16 -5
- package/dist/runtime/context.js +242 -0
- package/dist/runtime/env.d.ts +3 -1
- package/dist/runtime/env.js +159 -0
- package/dist/runtime/events.d.ts +46 -14
- package/dist/runtime/events.js +604 -0
- package/dist/runtime/execution-manager.d.ts +1 -1
- package/dist/runtime/execution-manager.js +66 -0
- package/dist/runtime/index.d.ts +10 -9
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/module-loader.d.ts +0 -8
- package/dist/runtime/module-loader.js +16 -0
- package/dist/runtime/process.d.ts +2 -4
- package/dist/runtime/process.js +146 -0
- package/dist/runtime/runner.d.ts +117 -10
- package/dist/runtime/runner.js +984 -0
- package/dist/runtime/standalone.d.ts +68 -19
- package/dist/runtime/standalone.js +536 -0
- package/dist/schema/validator.js +122 -0
- package/dist/server/body.js +93 -0
- package/dist/server/index.d.ts +5 -6
- package/dist/server/index.js +5 -0
- package/dist/server/routes/actions.d.ts +2 -2
- package/dist/server/routes/actions.js +298 -0
- package/dist/server/routes/common.d.ts +28 -17
- package/dist/server/routes/common.js +104 -0
- package/dist/server/routes/config.d.ts +2 -1
- package/dist/server/routes/config.js +173 -0
- package/dist/server/routes/doctor.d.ts +2 -2
- package/dist/server/routes/doctor.js +63 -0
- package/dist/server/routes/health.d.ts +2 -2
- package/dist/server/routes/health.js +35 -0
- package/dist/server/routes/index.d.ts +9 -9
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/info.d.ts +4 -2
- package/dist/server/routes/info.js +158 -0
- package/dist/server/routes/playbooks.d.ts +2 -2
- package/dist/server/routes/playbooks.js +134 -0
- package/dist/server/routes/runs.d.ts +2 -2
- package/dist/server/routes/runs.js +343 -0
- package/dist/server/routes/state.d.ts +2 -1
- package/dist/server/routes/state.js +169 -0
- package/dist/server/security.d.ts +11 -4
- package/dist/server/security.js +101 -0
- package/dist/server/server.d.ts +6 -6
- package/dist/server/server.js +355 -0
- package/dist/server/types.d.ts +27 -8
- package/dist/server/types.js +1 -0
- package/dist/storage/data-dir-lock.d.ts +119 -0
- package/dist/storage/data-dir-lock.js +1016 -0
- package/dist/storage/driver.d.ts +4 -10
- package/dist/storage/driver.js +59 -0
- package/dist/storage/index.d.ts +30 -10
- package/dist/storage/index.js +98 -0
- package/dist/storage/lazy.d.ts +8 -0
- package/dist/storage/lazy.js +44 -0
- package/dist/storage/mask.d.ts +12 -5
- package/dist/storage/mask.js +48 -0
- package/dist/storage/params.d.ts +13 -0
- package/dist/storage/params.js +32 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +689 -0
- package/dist/storage/types.d.ts +57 -6
- package/dist/storage/types.js +14 -0
- package/dist/target/index.d.ts +4 -0
- package/dist/target/index.js +4 -0
- package/dist/target/local.d.ts +52 -0
- package/dist/target/local.js +343 -0
- package/dist/target/remote.d.ts +76 -0
- package/dist/target/remote.js +839 -0
- package/dist/target/target.d.ts +6 -0
- package/dist/target/target.js +35 -0
- package/dist/target/types.d.ts +246 -0
- package/dist/target/types.js +34 -0
- package/dist/utils/index.d.ts +22 -2
- package/dist/utils/index.js +180 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +4 -0
- package/package.json +6 -7
- package/dist/build/index.d.ts +0 -1
- package/dist/build/templates.d.ts +0 -19
- package/dist/server/runtime-registry.d.ts +0 -78
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, } from "@actiondock/sdk";
|
|
4
|
+
import { SystemClock } from "../runtime/clock.js";
|
|
5
|
+
import { createDefaultSqliteDriver } from "./driver.js";
|
|
6
|
+
import { STORAGE_SCHEMA_VERSION, } from "./types.js";
|
|
7
|
+
export { decodeStateKey, encodeStateKey, escapeStateSegment, unescapeStateSegment, };
|
|
8
|
+
/**
|
|
9
|
+
* 统一 SQLite 运行时存储实现。
|
|
10
|
+
* 通过 SqliteDriver 抽象驱动,解耦底层具体运行时引擎。
|
|
11
|
+
*/
|
|
12
|
+
export class SqliteRuntimeStorage {
|
|
13
|
+
driver;
|
|
14
|
+
packageId;
|
|
15
|
+
clock;
|
|
16
|
+
isClosed = false;
|
|
17
|
+
statementCache = new Map();
|
|
18
|
+
dbPath;
|
|
19
|
+
get isOpen() {
|
|
20
|
+
return !this.isClosed;
|
|
21
|
+
}
|
|
22
|
+
get closed() {
|
|
23
|
+
return this.isClosed;
|
|
24
|
+
}
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.packageId = options.packageId;
|
|
27
|
+
this.clock = options.clock ?? new SystemClock();
|
|
28
|
+
const dbPath = options.dbPath || ":memory:";
|
|
29
|
+
this.dbPath = dbPath;
|
|
30
|
+
if (dbPath !== ":memory:") {
|
|
31
|
+
const dir = dirname(dbPath);
|
|
32
|
+
if (!existsSync(dir)) {
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
35
|
+
chmodSync(dir, 0o700);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// 忽略系统权限设置失败
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.driver = options.driver ?? createDefaultSqliteDriver(dbPath);
|
|
43
|
+
if (dbPath !== ":memory:" && existsSync(dbPath)) {
|
|
44
|
+
try {
|
|
45
|
+
chmodSync(dbPath, 0o600);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// 忽略文件权限设置异常
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.init();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 初始化数据库结构并执行严格版本校验。
|
|
55
|
+
*
|
|
56
|
+
* 规则:
|
|
57
|
+
* - 空数据目录首次初始化在单个事务中完成完整 Schema 构建。
|
|
58
|
+
* - 若创建事务失败则拒绝启动且不留下残缺表。
|
|
59
|
+
* - 打开旧版本或不兼容版本的数据库时,在写事务前直接抛出 UNSUPPORTED_STORAGE_SCHEMA 异常并拒绝启动。
|
|
60
|
+
*/
|
|
61
|
+
init() {
|
|
62
|
+
this.driver.exec("PRAGMA journal_mode = WAL;");
|
|
63
|
+
this.driver.exec("PRAGMA synchronous = NORMAL;");
|
|
64
|
+
// 读取 Schema 版本号
|
|
65
|
+
const versionRes = this.driver.prepare("PRAGMA user_version;").get();
|
|
66
|
+
const version = Number(versionRes?.user_version ?? 0);
|
|
67
|
+
if (version === 0) {
|
|
68
|
+
this.driver.transaction(() => {
|
|
69
|
+
this.driver.exec(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
71
|
+
package_id TEXT NOT NULL,
|
|
72
|
+
key TEXT NOT NULL,
|
|
73
|
+
value_json TEXT,
|
|
74
|
+
updated_at TEXT NOT NULL,
|
|
75
|
+
PRIMARY KEY (package_id, key)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS state (
|
|
79
|
+
package_id TEXT NOT NULL,
|
|
80
|
+
namespace TEXT NOT NULL,
|
|
81
|
+
key TEXT NOT NULL,
|
|
82
|
+
value_json TEXT,
|
|
83
|
+
updated_at TEXT NOT NULL,
|
|
84
|
+
expires_at TEXT,
|
|
85
|
+
PRIMARY KEY (package_id, namespace, key)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
root_run_id TEXT NOT NULL,
|
|
91
|
+
parent_run_id TEXT,
|
|
92
|
+
package_id TEXT NOT NULL,
|
|
93
|
+
package_instance_id TEXT NOT NULL,
|
|
94
|
+
action_id TEXT NOT NULL,
|
|
95
|
+
generation_id TEXT NOT NULL,
|
|
96
|
+
owner_id TEXT NOT NULL,
|
|
97
|
+
host_session_id TEXT,
|
|
98
|
+
status TEXT NOT NULL,
|
|
99
|
+
input_json TEXT,
|
|
100
|
+
output_json TEXT,
|
|
101
|
+
error_json TEXT,
|
|
102
|
+
started_at TEXT NOT NULL,
|
|
103
|
+
finished_at TEXT,
|
|
104
|
+
duration_ms INTEGER
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_runs_action ON runs(package_id, action_id);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_runs_root ON runs(root_run_id);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_runs_host_session ON runs(host_session_id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_state_expires ON state(expires_at);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS idempotency_keys (
|
|
114
|
+
owner_id TEXT NOT NULL,
|
|
115
|
+
action_ref TEXT NOT NULL,
|
|
116
|
+
request_id TEXT NOT NULL,
|
|
117
|
+
input_digest TEXT NOT NULL,
|
|
118
|
+
run_id TEXT NOT NULL,
|
|
119
|
+
created_at TEXT NOT NULL,
|
|
120
|
+
PRIMARY KEY (owner_id, action_ref, request_id)
|
|
121
|
+
);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_idemp_run ON idempotency_keys(run_id);
|
|
123
|
+
|
|
124
|
+
PRAGMA user_version = ${STORAGE_SCHEMA_VERSION};
|
|
125
|
+
`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (version !== STORAGE_SCHEMA_VERSION) {
|
|
129
|
+
const err = new Error(`UNSUPPORTED_STORAGE_SCHEMA: Database schema version ${version} is incompatible (expected ${STORAGE_SCHEMA_VERSION}). Silent upgrade and database overwriting are strictly prohibited.`);
|
|
130
|
+
err.code = "UNSUPPORTED_STORAGE_SCHEMA";
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
// 确保已有历史数据库补充迁移 host_session_id 列
|
|
134
|
+
if (version !== 0) {
|
|
135
|
+
try {
|
|
136
|
+
const columns = this.driver.prepare("PRAGMA table_info(runs);").all();
|
|
137
|
+
if (Array.isArray(columns) && !columns.some((c) => c.name === "host_session_id")) {
|
|
138
|
+
this.driver.exec("ALTER TABLE runs ADD COLUMN host_session_id TEXT;");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// 忽略非结构化表或兼容驱动异常
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 重启恢复:将未正常结算的 running 与 pending 状态自动收敛为 interrupted
|
|
146
|
+
this.recoverRunningRuns();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 重启恢复:将未正常结算的非终态(running/pending)记录自动收敛为 interrupted。
|
|
150
|
+
* 若指定了当前新 Host 会话标识,将收敛不属于该会话(包括旧会话或空会话)的死亡任务。
|
|
151
|
+
*/
|
|
152
|
+
recoverRunningRuns(currentHostSessionId) {
|
|
153
|
+
return this.recoverDeadSessionRuns(currentHostSessionId);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 收敛死亡会话遗留的非终态运行任务。
|
|
157
|
+
*/
|
|
158
|
+
recoverDeadSessionRuns(currentHostSessionId) {
|
|
159
|
+
if (this.isClosed)
|
|
160
|
+
return 0;
|
|
161
|
+
try {
|
|
162
|
+
const now = this.clock.now().toISOString();
|
|
163
|
+
let sql = `
|
|
164
|
+
UPDATE runs
|
|
165
|
+
SET status = 'interrupted',
|
|
166
|
+
finished_at = ?,
|
|
167
|
+
error_json = '{"code":"RUN_INTERRUPTED","message":"Execution interrupted by system shutdown or restart"}'
|
|
168
|
+
WHERE status IN ('running', 'pending')
|
|
169
|
+
`;
|
|
170
|
+
const params = [now];
|
|
171
|
+
if (currentHostSessionId) {
|
|
172
|
+
sql += " AND (host_session_id IS NULL OR host_session_id != ?)";
|
|
173
|
+
params.push(currentHostSessionId);
|
|
174
|
+
}
|
|
175
|
+
const stmt = this.getStatement(sql);
|
|
176
|
+
const res = stmt.run(...params);
|
|
177
|
+
return res.changes;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 获取或复用预编译 SQL 语句缓存。
|
|
185
|
+
*/
|
|
186
|
+
getStatement(sql) {
|
|
187
|
+
let stmt = this.statementCache.get(sql);
|
|
188
|
+
if (!stmt) {
|
|
189
|
+
stmt = this.driver.prepare(sql);
|
|
190
|
+
this.statementCache.set(sql, stmt);
|
|
191
|
+
}
|
|
192
|
+
return stmt;
|
|
193
|
+
}
|
|
194
|
+
// --- Config 配置管理 ---
|
|
195
|
+
getConfig(key) {
|
|
196
|
+
const stmt = this.getStatement("SELECT value_json FROM config WHERE package_id = ? AND key = ?");
|
|
197
|
+
const row = stmt.get(this.packageId, key);
|
|
198
|
+
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(row.value_json);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return row.value_json;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
listConfig() {
|
|
209
|
+
const stmt = this.getStatement("SELECT key, value_json FROM config WHERE package_id = ?");
|
|
210
|
+
const rows = stmt.all(this.packageId);
|
|
211
|
+
const result = {};
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
try {
|
|
214
|
+
result[row.key] = JSON.parse(row.value_json);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
result[row.key] = row.value_json;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
setConfig(key, value) {
|
|
223
|
+
const stmt = this.getStatement(`
|
|
224
|
+
INSERT INTO config (package_id, key, value_json, updated_at)
|
|
225
|
+
VALUES (?, ?, ?, ?)
|
|
226
|
+
ON CONFLICT(package_id, key) DO UPDATE SET
|
|
227
|
+
value_json = excluded.value_json,
|
|
228
|
+
updated_at = excluded.updated_at
|
|
229
|
+
`);
|
|
230
|
+
const valJson = JSON.stringify(value);
|
|
231
|
+
const now = this.clock.now().toISOString();
|
|
232
|
+
stmt.run(this.packageId, key, valJson, now);
|
|
233
|
+
}
|
|
234
|
+
deleteConfig(key) {
|
|
235
|
+
const stmt = this.getStatement("DELETE FROM config WHERE package_id = ? AND key = ?");
|
|
236
|
+
const res = stmt.run(this.packageId, key);
|
|
237
|
+
return res.changes > 0;
|
|
238
|
+
}
|
|
239
|
+
// --- State 状态管理 ---
|
|
240
|
+
async getState(namespace, key) {
|
|
241
|
+
const stmt = this.getStatement("SELECT value_json, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?");
|
|
242
|
+
const row = stmt.get(this.packageId, namespace, key);
|
|
243
|
+
if (!row || row.value_json === undefined || row.value_json === null) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (row.expires_at) {
|
|
247
|
+
const expires = new Date(row.expires_at).getTime();
|
|
248
|
+
if (this.clock.now().getTime() >= expires) {
|
|
249
|
+
this.deleteState(namespace, key).catch(() => { });
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(row.value_json);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return row.value_json;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async findMatchingStateRows(targetKey) {
|
|
261
|
+
let decoded;
|
|
262
|
+
try {
|
|
263
|
+
decoded = decodeStateKey(targetKey);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// 键名包含多个未转义冒号
|
|
267
|
+
}
|
|
268
|
+
const candidateStmt = this.getStatement(`
|
|
269
|
+
SELECT namespace, key, value_json, updated_at, expires_at FROM state
|
|
270
|
+
WHERE package_id = ? AND (
|
|
271
|
+
(CASE WHEN ? IS NOT NULL THEN (namespace = ? AND key = ?) ELSE 0 END)
|
|
272
|
+
OR (CASE WHEN namespace = '' THEN key ELSE namespace || ':' || key END) = ?
|
|
273
|
+
OR key = ?
|
|
274
|
+
)
|
|
275
|
+
`);
|
|
276
|
+
const rawResult = candidateStmt.all(this.packageId, decoded ? 1 : null, decoded?.namespace ?? "", decoded?.key ?? "", targetKey, targetKey);
|
|
277
|
+
const rawRows = Array.isArray(rawResult) ? rawResult : [];
|
|
278
|
+
const now = this.clock.now().getTime();
|
|
279
|
+
const uniqueMap = new Map();
|
|
280
|
+
for (const r of rawRows) {
|
|
281
|
+
if (r.expires_at && now >= new Date(r.expires_at).getTime()) {
|
|
282
|
+
this.deleteState(r.namespace, r.key).catch(() => { });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
uniqueMap.set(`${r.namespace}\0${r.key}`, r);
|
|
286
|
+
}
|
|
287
|
+
return Array.from(uniqueMap.values());
|
|
288
|
+
}
|
|
289
|
+
async findState(targetKey, namespace) {
|
|
290
|
+
if (namespace !== undefined) {
|
|
291
|
+
const val = await this.getState(namespace, targetKey);
|
|
292
|
+
if (val === undefined)
|
|
293
|
+
return undefined;
|
|
294
|
+
const stmt = this.getStatement("SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?");
|
|
295
|
+
const row = stmt.get(this.packageId, namespace, targetKey);
|
|
296
|
+
return {
|
|
297
|
+
packageId: this.packageId,
|
|
298
|
+
namespace,
|
|
299
|
+
key: targetKey,
|
|
300
|
+
fullKey: encodeStateKey(namespace, targetKey),
|
|
301
|
+
value: val,
|
|
302
|
+
updatedAt: row?.updated_at || this.clock.now().toISOString(),
|
|
303
|
+
expiresAt: row?.expires_at,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const matchingRows = await this.findMatchingStateRows(targetKey);
|
|
307
|
+
if (matchingRows.length > 1) {
|
|
308
|
+
throw new Error(`Ambiguous state key '${targetKey}': matches ${matchingRows.length} entries (${matchingRows.map((r) => `${r.namespace}:${r.key}`).join(", ")})`);
|
|
309
|
+
}
|
|
310
|
+
if (matchingRows.length === 0) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const row = matchingRows[0];
|
|
314
|
+
let parsedVal;
|
|
315
|
+
try {
|
|
316
|
+
parsedVal = JSON.parse(row.value_json);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
parsedVal = row.value_json;
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
packageId: this.packageId,
|
|
323
|
+
namespace: row.namespace,
|
|
324
|
+
key: row.key,
|
|
325
|
+
fullKey: encodeStateKey(row.namespace, row.key),
|
|
326
|
+
value: parsedVal,
|
|
327
|
+
updatedAt: row.updated_at,
|
|
328
|
+
expiresAt: row.expires_at,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async setState(namespace, key, value, ttl) {
|
|
332
|
+
const stmt = this.getStatement(`
|
|
333
|
+
INSERT INTO state (package_id, namespace, key, value_json, updated_at, expires_at)
|
|
334
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
335
|
+
ON CONFLICT(package_id, namespace, key) DO UPDATE SET
|
|
336
|
+
value_json = excluded.value_json,
|
|
337
|
+
updated_at = excluded.updated_at,
|
|
338
|
+
expires_at = excluded.expires_at
|
|
339
|
+
`);
|
|
340
|
+
const valJson = JSON.stringify(value);
|
|
341
|
+
const now = this.clock.now();
|
|
342
|
+
const updatedAt = now.toISOString();
|
|
343
|
+
let expiresAt = null;
|
|
344
|
+
if (typeof ttl === "number" && ttl > 0) {
|
|
345
|
+
expiresAt = new Date(now.getTime() + ttl * 1000).toISOString();
|
|
346
|
+
}
|
|
347
|
+
stmt.run(this.packageId, namespace, key, valJson, updatedAt, expiresAt);
|
|
348
|
+
}
|
|
349
|
+
async deleteState(namespace, key) {
|
|
350
|
+
const stmt = this.getStatement("DELETE FROM state WHERE package_id = ? AND namespace = ? AND key = ?");
|
|
351
|
+
const res = stmt.run(this.packageId, namespace, key);
|
|
352
|
+
return res.changes > 0;
|
|
353
|
+
}
|
|
354
|
+
async deleteStateSmart(targetKey, namespace) {
|
|
355
|
+
if (namespace !== undefined) {
|
|
356
|
+
return this.deleteState(namespace, targetKey);
|
|
357
|
+
}
|
|
358
|
+
const matchingRows = await this.findMatchingStateRows(targetKey);
|
|
359
|
+
if (matchingRows.length > 1) {
|
|
360
|
+
throw new Error(`Ambiguous state key '${targetKey}': matches ${matchingRows.length} entries for deletion (${matchingRows.map((r) => `${r.namespace}:${r.key}`).join(", ")})`);
|
|
361
|
+
}
|
|
362
|
+
if (matchingRows.length === 1) {
|
|
363
|
+
return this.deleteState(matchingRows[0].namespace, matchingRows[0].key);
|
|
364
|
+
}
|
|
365
|
+
return this.deleteState("", targetKey);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 清理已过期的状态记录
|
|
369
|
+
*/
|
|
370
|
+
async cleanExpiredState() {
|
|
371
|
+
if (this.isClosed)
|
|
372
|
+
return 0;
|
|
373
|
+
try {
|
|
374
|
+
const now = this.clock.now().toISOString();
|
|
375
|
+
const stmt = this.getStatement("DELETE FROM state WHERE package_id = ? AND expires_at IS NOT NULL AND expires_at <= ?");
|
|
376
|
+
const res = stmt.run(this.packageId, now);
|
|
377
|
+
return res.changes;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async clearState(options = {}) {
|
|
384
|
+
let sql = "DELETE FROM state WHERE package_id = ?";
|
|
385
|
+
const params = [this.packageId];
|
|
386
|
+
if (options.namespace !== undefined) {
|
|
387
|
+
sql += " AND namespace = ?";
|
|
388
|
+
params.push(options.namespace);
|
|
389
|
+
}
|
|
390
|
+
if (options.prefix) {
|
|
391
|
+
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
392
|
+
const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
|
|
393
|
+
params.push(`${escapedPrefix}%`);
|
|
394
|
+
}
|
|
395
|
+
const stmt = this.getStatement(sql);
|
|
396
|
+
const res = stmt.run(...params);
|
|
397
|
+
return res.changes;
|
|
398
|
+
}
|
|
399
|
+
async listStateKeys(namespace, prefix) {
|
|
400
|
+
await this.cleanExpiredState();
|
|
401
|
+
let sql = "SELECT namespace, key, expires_at FROM state WHERE package_id = ?";
|
|
402
|
+
const params = [this.packageId];
|
|
403
|
+
if (namespace !== null && namespace !== undefined) {
|
|
404
|
+
sql += " AND namespace = ?";
|
|
405
|
+
params.push(namespace);
|
|
406
|
+
}
|
|
407
|
+
if (prefix) {
|
|
408
|
+
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
409
|
+
const escapedPrefix = prefix.replace(/([%_\\])/g, "\\$1");
|
|
410
|
+
params.push(`${escapedPrefix}%`);
|
|
411
|
+
}
|
|
412
|
+
const stmt = this.getStatement(sql);
|
|
413
|
+
const rawResult = stmt.all(...params);
|
|
414
|
+
const rows = Array.isArray(rawResult) ? rawResult : [];
|
|
415
|
+
const now = this.clock.now().getTime();
|
|
416
|
+
const result = [];
|
|
417
|
+
for (const row of rows) {
|
|
418
|
+
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (namespace !== null && namespace !== undefined) {
|
|
422
|
+
result.push(row.key);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
result.push(encodeStateKey(row.namespace, row.key));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
async listStateEntries(options = {}) {
|
|
431
|
+
await this.cleanExpiredState();
|
|
432
|
+
let sql = "SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ?";
|
|
433
|
+
const params = [this.packageId];
|
|
434
|
+
if (options.namespace !== undefined) {
|
|
435
|
+
sql += " AND namespace = ?";
|
|
436
|
+
params.push(options.namespace);
|
|
437
|
+
}
|
|
438
|
+
if (options.prefix) {
|
|
439
|
+
sql += " AND key LIKE ? ESCAPE '\\'";
|
|
440
|
+
const escapedPrefix = options.prefix.replace(/([%_\\])/g, "\\$1");
|
|
441
|
+
params.push(`${escapedPrefix}%`);
|
|
442
|
+
}
|
|
443
|
+
const stmt = this.getStatement(sql);
|
|
444
|
+
const rawResult = stmt.all(...params);
|
|
445
|
+
const rows = Array.isArray(rawResult) ? rawResult : [];
|
|
446
|
+
const now = this.clock.now().getTime();
|
|
447
|
+
const results = [];
|
|
448
|
+
for (const row of rows) {
|
|
449
|
+
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
let parsedVal;
|
|
453
|
+
try {
|
|
454
|
+
parsedVal = JSON.parse(row.value_json);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
parsedVal = row.value_json;
|
|
458
|
+
}
|
|
459
|
+
results.push({
|
|
460
|
+
packageId: this.packageId,
|
|
461
|
+
namespace: row.namespace,
|
|
462
|
+
key: row.key,
|
|
463
|
+
fullKey: encodeStateKey(row.namespace, row.key),
|
|
464
|
+
value: parsedVal,
|
|
465
|
+
updatedAt: row.updated_at,
|
|
466
|
+
expiresAt: row.expires_at,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return results;
|
|
470
|
+
}
|
|
471
|
+
// --- Runs 运行记录管理 ---
|
|
472
|
+
createRun(record) {
|
|
473
|
+
const stmt = this.getStatement(`
|
|
474
|
+
INSERT INTO runs (
|
|
475
|
+
id, root_run_id, parent_run_id, package_id, package_instance_id,
|
|
476
|
+
action_id, generation_id, owner_id, host_session_id, status, input_json, output_json,
|
|
477
|
+
error_json, started_at, finished_at, duration_ms
|
|
478
|
+
)
|
|
479
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
480
|
+
`);
|
|
481
|
+
const rootRunId = record.rootRunId || record.id;
|
|
482
|
+
const parentRunId = record.parentRunId || null;
|
|
483
|
+
const packageInstanceId = record.packageInstanceId || record.packageId || this.packageId;
|
|
484
|
+
const generationId = record.generationId || "1";
|
|
485
|
+
const ownerId = record.ownerId || "local";
|
|
486
|
+
const hostSessionId = record.hostSessionId || null;
|
|
487
|
+
const inputJson = record.input !== undefined ? JSON.stringify(record.input) : null;
|
|
488
|
+
const outputJson = record.output !== undefined ? JSON.stringify(record.output) : null;
|
|
489
|
+
const errorJson = record.error ? JSON.stringify(record.error) : null;
|
|
490
|
+
const finishedAt = record.finishedAt || null;
|
|
491
|
+
const durationMs = typeof record.durationMs === "number" ? record.durationMs : null;
|
|
492
|
+
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);
|
|
493
|
+
}
|
|
494
|
+
updateRun(id, status, output, error, finishedAt) {
|
|
495
|
+
if (this.isClosed)
|
|
496
|
+
return;
|
|
497
|
+
try {
|
|
498
|
+
const finishIso = finishedAt || this.clock.now().toISOString();
|
|
499
|
+
const stmt = this.getStatement(`
|
|
500
|
+
UPDATE runs
|
|
501
|
+
SET status = ?, output_json = ?, error_json = ?, finished_at = ?,
|
|
502
|
+
duration_ms = CASE
|
|
503
|
+
WHEN started_at IS NOT NULL THEN MAX(0, CAST(ROUND((julianday(?) - julianday(started_at)) * 86400000) AS INTEGER))
|
|
504
|
+
ELSE NULL
|
|
505
|
+
END
|
|
506
|
+
WHERE id = ? AND status = 'running'
|
|
507
|
+
`);
|
|
508
|
+
stmt.run(status, output !== undefined ? JSON.stringify(output) : null, error ? JSON.stringify(error) : null, finishIso, finishIso, id);
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
if (this.isClosed)
|
|
512
|
+
return;
|
|
513
|
+
console.warn(`[SqliteRuntimeStorage] Failed to update run "${id}":`, err);
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
getRun(id) {
|
|
518
|
+
const stmt = this.getStatement("SELECT * FROM runs WHERE id = ? AND package_id = ?");
|
|
519
|
+
const row = stmt.get(id, this.packageId);
|
|
520
|
+
if (!row)
|
|
521
|
+
return null;
|
|
522
|
+
return this.mapRunRecord(row);
|
|
523
|
+
}
|
|
524
|
+
listRuns(options = {}) {
|
|
525
|
+
const limit = options.limit || 50;
|
|
526
|
+
let rows;
|
|
527
|
+
if (options.actionId) {
|
|
528
|
+
const stmt = this.getStatement(`
|
|
529
|
+
SELECT * FROM runs
|
|
530
|
+
WHERE package_id = ? AND action_id = ?
|
|
531
|
+
ORDER BY started_at DESC
|
|
532
|
+
LIMIT ?
|
|
533
|
+
`);
|
|
534
|
+
rows = stmt.all(this.packageId, options.actionId, limit);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const stmt = this.getStatement(`
|
|
538
|
+
SELECT * FROM runs
|
|
539
|
+
WHERE package_id = ?
|
|
540
|
+
ORDER BY started_at DESC
|
|
541
|
+
LIMIT ?
|
|
542
|
+
`);
|
|
543
|
+
rows = stmt.all(this.packageId, limit);
|
|
544
|
+
}
|
|
545
|
+
return rows.map((r) => this.mapRunRecord(r));
|
|
546
|
+
}
|
|
547
|
+
clearRuns(options = {}) {
|
|
548
|
+
let sql = "DELETE FROM runs WHERE package_id = ?";
|
|
549
|
+
const params = [this.packageId];
|
|
550
|
+
if (options.actionId) {
|
|
551
|
+
sql += " AND action_id = ?";
|
|
552
|
+
params.push(options.actionId);
|
|
553
|
+
}
|
|
554
|
+
if (options.status) {
|
|
555
|
+
sql += " AND status = ?";
|
|
556
|
+
params.push(options.status);
|
|
557
|
+
}
|
|
558
|
+
const stmt = this.getStatement(sql);
|
|
559
|
+
const res = stmt.run(...params);
|
|
560
|
+
// 运行记录清理后,级联清理已无对应运行的孤立去重索引记录
|
|
561
|
+
try {
|
|
562
|
+
this.getStatement("DELETE FROM idempotency_keys WHERE run_id NOT IN (SELECT id FROM runs)").run();
|
|
563
|
+
}
|
|
564
|
+
catch { }
|
|
565
|
+
return res.changes;
|
|
566
|
+
}
|
|
567
|
+
// --- Idempotency 幂等去重管理 ---
|
|
568
|
+
checkAndRecordIdempotency(record) {
|
|
569
|
+
if (this.isClosed) {
|
|
570
|
+
throw new Error("SqliteRuntimeStorage is closed");
|
|
571
|
+
}
|
|
572
|
+
// 事务外预读:同步契约下驱动的事务录制器仅允许写语句,
|
|
573
|
+
// 幂等检查所需的读操作全部前移至事务开始之前执行。
|
|
574
|
+
// 同步单连接串行执行保证预读与事务提交之间不存在并发窗口。
|
|
575
|
+
const checkStmt = this.getStatement(`
|
|
576
|
+
SELECT input_digest, run_id FROM idempotency_keys
|
|
577
|
+
WHERE owner_id = ? AND action_ref = ? AND request_id = ?
|
|
578
|
+
`);
|
|
579
|
+
const existing = checkStmt.get(record.ownerId, record.actionRef, record.requestId);
|
|
580
|
+
if (existing) {
|
|
581
|
+
// 检查关联的运行记录是否仍存在于运行表中
|
|
582
|
+
const runStmt = this.getStatement("SELECT id FROM runs WHERE id = ?");
|
|
583
|
+
const runExists = runStmt.get(existing.run_id);
|
|
584
|
+
if (!runExists) {
|
|
585
|
+
// 原运行记录已被清理淘汰,事务内仅执行删除与重登记写操作
|
|
586
|
+
this.driver.transaction(() => {
|
|
587
|
+
const delStmt = this.getStatement(`
|
|
588
|
+
DELETE FROM idempotency_keys
|
|
589
|
+
WHERE owner_id = ? AND action_ref = ? AND request_id = ?
|
|
590
|
+
`);
|
|
591
|
+
delStmt.run(record.ownerId, record.actionRef, record.requestId);
|
|
592
|
+
const insertStmt = this.getStatement(`
|
|
593
|
+
INSERT INTO idempotency_keys (owner_id, action_ref, request_id, input_digest, run_id, created_at)
|
|
594
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
595
|
+
`);
|
|
596
|
+
const createdAt = record.createdAt || this.clock.now().toISOString();
|
|
597
|
+
insertStmt.run(record.ownerId, record.actionRef, record.requestId, record.inputDigest, record.runId, createdAt);
|
|
598
|
+
});
|
|
599
|
+
return { outcome: "new" };
|
|
600
|
+
}
|
|
601
|
+
if (existing.input_digest !== record.inputDigest) {
|
|
602
|
+
return { outcome: "conflict", existingDigest: existing.input_digest };
|
|
603
|
+
}
|
|
604
|
+
return { outcome: "duplicate", runId: existing.run_id };
|
|
605
|
+
}
|
|
606
|
+
this.driver.transaction(() => {
|
|
607
|
+
const insertStmt = this.getStatement(`
|
|
608
|
+
INSERT INTO idempotency_keys (owner_id, action_ref, request_id, input_digest, run_id, created_at)
|
|
609
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
610
|
+
`);
|
|
611
|
+
const createdAt = record.createdAt || this.clock.now().toISOString();
|
|
612
|
+
insertStmt.run(record.ownerId, record.actionRef, record.requestId, record.inputDigest, record.runId, createdAt);
|
|
613
|
+
});
|
|
614
|
+
return { outcome: "new" };
|
|
615
|
+
}
|
|
616
|
+
getIdempotencyRecord(ownerId, actionRef, requestId) {
|
|
617
|
+
if (this.isClosed)
|
|
618
|
+
return undefined;
|
|
619
|
+
const stmt = this.getStatement(`
|
|
620
|
+
SELECT owner_id, action_ref, request_id, input_digest, run_id, created_at
|
|
621
|
+
FROM idempotency_keys
|
|
622
|
+
WHERE owner_id = ? AND action_ref = ? AND request_id = ?
|
|
623
|
+
`);
|
|
624
|
+
const row = stmt.get(ownerId, actionRef, requestId);
|
|
625
|
+
if (!row)
|
|
626
|
+
return undefined;
|
|
627
|
+
return {
|
|
628
|
+
ownerId: row.owner_id,
|
|
629
|
+
actionRef: row.action_ref,
|
|
630
|
+
requestId: row.request_id,
|
|
631
|
+
inputDigest: row.input_digest,
|
|
632
|
+
runId: row.run_id,
|
|
633
|
+
createdAt: row.created_at,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
mapRunRecord(row) {
|
|
637
|
+
let input;
|
|
638
|
+
let output;
|
|
639
|
+
let error;
|
|
640
|
+
try {
|
|
641
|
+
input = row.input_json ? JSON.parse(row.input_json) : undefined;
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
input = row.input_json;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
output = row.output_json ? JSON.parse(row.output_json) : undefined;
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
output = row.output_json;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
error = row.error_json ? JSON.parse(row.error_json) : undefined;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
error = undefined;
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
id: row.id,
|
|
660
|
+
rootRunId: row.root_run_id || row.id,
|
|
661
|
+
parentRunId: row.parent_run_id || undefined,
|
|
662
|
+
packageId: row.package_id,
|
|
663
|
+
packageInstanceId: row.package_instance_id || row.package_id,
|
|
664
|
+
actionId: row.action_id,
|
|
665
|
+
generationId: row.generation_id || "1",
|
|
666
|
+
ownerId: row.owner_id || "local",
|
|
667
|
+
hostSessionId: row.host_session_id || undefined,
|
|
668
|
+
status: row.status,
|
|
669
|
+
input,
|
|
670
|
+
output,
|
|
671
|
+
error,
|
|
672
|
+
startedAt: row.started_at,
|
|
673
|
+
finishedAt: row.finished_at || undefined,
|
|
674
|
+
durationMs: typeof row.duration_ms === "number" ? row.duration_ms : undefined,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async close() {
|
|
678
|
+
if (this.isClosed)
|
|
679
|
+
return;
|
|
680
|
+
this.isClosed = true;
|
|
681
|
+
this.statementCache.clear();
|
|
682
|
+
try {
|
|
683
|
+
this.driver.close();
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
// 忽略重复关闭异常
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|