opencode-supertask 0.1.21 → 0.1.23
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 +30 -5
- package/dist/cli/index.js +617 -134
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.d.ts +3 -1
- package/dist/gateway/index.js +480 -101
- package/dist/gateway/index.js.map +1 -1
- package/dist/plugin/supertask.js +19 -2
- package/dist/plugin/supertask.js.map +1 -1
- package/dist/web/index.js +400 -36
- package/dist/web/index.js.map +1 -1
- package/dist/worker/index.js +17 -0
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/web/index.js
CHANGED
|
@@ -7247,26 +7247,26 @@ var require_CronDate = __commonJS({
|
|
|
7247
7247
|
* @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
|
|
7248
7248
|
* @param {string} [tz] - The timezone to use for the CronDate.
|
|
7249
7249
|
*/
|
|
7250
|
-
constructor(
|
|
7250
|
+
constructor(timestamp2, tz) {
|
|
7251
7251
|
const dateOpts = { zone: tz };
|
|
7252
|
-
if (!
|
|
7252
|
+
if (!timestamp2) {
|
|
7253
7253
|
this.#date = luxon_1.DateTime.local();
|
|
7254
|
-
} else if (
|
|
7255
|
-
this.#date =
|
|
7256
|
-
this.#dstStart =
|
|
7257
|
-
this.#dstEnd =
|
|
7258
|
-
} else if (
|
|
7259
|
-
this.#date = luxon_1.DateTime.fromJSDate(
|
|
7260
|
-
} else if (typeof
|
|
7261
|
-
this.#date = luxon_1.DateTime.fromMillis(
|
|
7254
|
+
} else if (timestamp2 instanceof _CronDate) {
|
|
7255
|
+
this.#date = timestamp2.#date;
|
|
7256
|
+
this.#dstStart = timestamp2.#dstStart;
|
|
7257
|
+
this.#dstEnd = timestamp2.#dstEnd;
|
|
7258
|
+
} else if (timestamp2 instanceof Date) {
|
|
7259
|
+
this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
|
|
7260
|
+
} else if (typeof timestamp2 === "number") {
|
|
7261
|
+
this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
|
|
7262
7262
|
} else {
|
|
7263
|
-
this.#date = luxon_1.DateTime.fromISO(
|
|
7264
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(
|
|
7265
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(
|
|
7266
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(
|
|
7263
|
+
this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
|
|
7264
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
|
|
7265
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
|
|
7266
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
|
|
7267
7267
|
}
|
|
7268
7268
|
if (!this.#date.isValid) {
|
|
7269
|
-
throw new Error(`CronDate: unhandled timestamp: ${
|
|
7269
|
+
throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
|
|
7270
7270
|
}
|
|
7271
7271
|
if (tz && tz !== this.#date.zoneName) {
|
|
7272
7272
|
this.#date = this.#date.setZone(tz);
|
|
@@ -16650,6 +16650,14 @@ function getSqlite() {
|
|
|
16650
16650
|
if (!_sqlite) initDb();
|
|
16651
16651
|
return _sqlite;
|
|
16652
16652
|
}
|
|
16653
|
+
function closeDb() {
|
|
16654
|
+
if (_sqlite) {
|
|
16655
|
+
_sqlite.close();
|
|
16656
|
+
_sqlite = null;
|
|
16657
|
+
_db = null;
|
|
16658
|
+
_migrationRan = false;
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16653
16661
|
var db = new Proxy({}, {
|
|
16654
16662
|
get(_, prop) {
|
|
16655
16663
|
const target = getDb();
|
|
@@ -16840,6 +16848,23 @@ var TaskService = class {
|
|
|
16840
16848
|
).returning();
|
|
16841
16849
|
return result.length;
|
|
16842
16850
|
}
|
|
16851
|
+
static async resetOrphanRunningToPending() {
|
|
16852
|
+
const result = await db.update(tasks2).set({
|
|
16853
|
+
status: "pending",
|
|
16854
|
+
startedAt: null,
|
|
16855
|
+
finishedAt: null
|
|
16856
|
+
}).where(
|
|
16857
|
+
and(
|
|
16858
|
+
eq(tasks2.status, "running"),
|
|
16859
|
+
sql`NOT EXISTS (
|
|
16860
|
+
SELECT 1 FROM ${taskRuns2}
|
|
16861
|
+
WHERE ${taskRuns2.taskId} = ${tasks2.id}
|
|
16862
|
+
AND ${taskRuns2.status} = 'running'
|
|
16863
|
+
)`
|
|
16864
|
+
)
|
|
16865
|
+
).returning();
|
|
16866
|
+
return result.length;
|
|
16867
|
+
}
|
|
16843
16868
|
static async cancel(id, scope = {}) {
|
|
16844
16869
|
const conditions = [
|
|
16845
16870
|
eq(tasks2.id, id),
|
|
@@ -17190,8 +17215,345 @@ var TaskTemplateService = class {
|
|
|
17190
17215
|
}
|
|
17191
17216
|
};
|
|
17192
17217
|
|
|
17218
|
+
// src/core/services/database-maintenance.service.ts
|
|
17219
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
17220
|
+
import { randomUUID } from "crypto";
|
|
17221
|
+
import {
|
|
17222
|
+
chmodSync,
|
|
17223
|
+
constants,
|
|
17224
|
+
copyFileSync,
|
|
17225
|
+
existsSync as existsSync2,
|
|
17226
|
+
mkdirSync as mkdirSync2,
|
|
17227
|
+
renameSync,
|
|
17228
|
+
statSync,
|
|
17229
|
+
unlinkSync,
|
|
17230
|
+
writeFileSync
|
|
17231
|
+
} from "fs";
|
|
17232
|
+
import { tmpdir } from "os";
|
|
17233
|
+
import { basename, dirname as dirname2, resolve } from "path";
|
|
17234
|
+
|
|
17235
|
+
// src/core/process-control.ts
|
|
17236
|
+
function isProcessAlive(pid) {
|
|
17237
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
17238
|
+
try {
|
|
17239
|
+
process.kill(pid, 0);
|
|
17240
|
+
return true;
|
|
17241
|
+
} catch (error) {
|
|
17242
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
17243
|
+
}
|
|
17244
|
+
}
|
|
17245
|
+
|
|
17246
|
+
// src/core/services/database-maintenance.service.ts
|
|
17247
|
+
var REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
|
|
17248
|
+
var DatabaseMaintenanceConflictError = class extends Error {
|
|
17249
|
+
constructor(message) {
|
|
17250
|
+
super(message);
|
|
17251
|
+
this.name = "DatabaseMaintenanceConflictError";
|
|
17252
|
+
}
|
|
17253
|
+
};
|
|
17254
|
+
function timestamp() {
|
|
17255
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
17256
|
+
}
|
|
17257
|
+
function normalizedPath(path) {
|
|
17258
|
+
return path === ":memory:" ? path : resolve(path);
|
|
17259
|
+
}
|
|
17260
|
+
function safeUnlink(path) {
|
|
17261
|
+
try {
|
|
17262
|
+
unlinkSync(path);
|
|
17263
|
+
} catch (error) {
|
|
17264
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
17265
|
+
if (code !== "ENOENT") throw error;
|
|
17266
|
+
}
|
|
17267
|
+
}
|
|
17268
|
+
function errorMessage(error) {
|
|
17269
|
+
return error instanceof Error ? error.message : String(error);
|
|
17270
|
+
}
|
|
17271
|
+
var DatabaseMaintenanceService = class {
|
|
17272
|
+
static check() {
|
|
17273
|
+
return this.inspect(getSqlite(), DB_FILE_PATH);
|
|
17274
|
+
}
|
|
17275
|
+
static backup(outputPath) {
|
|
17276
|
+
const sqlite2 = getSqlite();
|
|
17277
|
+
try {
|
|
17278
|
+
sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
|
|
17279
|
+
} catch {
|
|
17280
|
+
}
|
|
17281
|
+
return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
|
|
17282
|
+
}
|
|
17283
|
+
static clear(options = {}) {
|
|
17284
|
+
const sqlite2 = getSqlite();
|
|
17285
|
+
this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
|
|
17286
|
+
this.assertNoRunningWork(sqlite2);
|
|
17287
|
+
sqlite2.exec("BEGIN IMMEDIATE");
|
|
17288
|
+
let backup = null;
|
|
17289
|
+
try {
|
|
17290
|
+
this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
|
|
17291
|
+
this.assertNoRunningWork(sqlite2);
|
|
17292
|
+
const before = this.readCounts(sqlite2);
|
|
17293
|
+
backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
|
|
17294
|
+
sqlite2.exec("DELETE FROM task_runs");
|
|
17295
|
+
sqlite2.exec("DELETE FROM tasks");
|
|
17296
|
+
sqlite2.exec("DELETE FROM task_templates");
|
|
17297
|
+
sqlite2.exec("COMMIT");
|
|
17298
|
+
return {
|
|
17299
|
+
backupPath: backup.path,
|
|
17300
|
+
deleted: before,
|
|
17301
|
+
check: this.inspect(sqlite2, DB_FILE_PATH)
|
|
17302
|
+
};
|
|
17303
|
+
} catch (error) {
|
|
17304
|
+
if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
|
|
17305
|
+
if (error instanceof DatabaseMaintenanceConflictError) throw error;
|
|
17306
|
+
const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
|
|
17307
|
+
throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
|
|
17308
|
+
}
|
|
17309
|
+
}
|
|
17310
|
+
static restore(sourcePath) {
|
|
17311
|
+
if (DB_FILE_PATH === ":memory:") {
|
|
17312
|
+
throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
|
|
17313
|
+
}
|
|
17314
|
+
const source = normalizedPath(sourcePath);
|
|
17315
|
+
const livePath = normalizedPath(DB_FILE_PATH);
|
|
17316
|
+
if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
|
|
17317
|
+
const current = getSqlite();
|
|
17318
|
+
this.assertGatewaySafe(current, false);
|
|
17319
|
+
this.assertNoRunningWork(current);
|
|
17320
|
+
if (!existsSync2(source) || !statSync(source).isFile()) {
|
|
17321
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
|
|
17322
|
+
}
|
|
17323
|
+
let sourceCheck;
|
|
17324
|
+
try {
|
|
17325
|
+
sourceCheck = this.inspectFile(source);
|
|
17326
|
+
} catch (error) {
|
|
17327
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
|
|
17328
|
+
}
|
|
17329
|
+
if (!sourceCheck.ok) {
|
|
17330
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
|
|
17331
|
+
}
|
|
17332
|
+
const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
|
|
17333
|
+
const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
|
|
17334
|
+
let safetyBackup = null;
|
|
17335
|
+
let liveMoved = false;
|
|
17336
|
+
try {
|
|
17337
|
+
copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
|
|
17338
|
+
chmodSync(stagePath, 384);
|
|
17339
|
+
const staged = new Database3(stagePath);
|
|
17340
|
+
try {
|
|
17341
|
+
const stagedCheck = this.inspect(staged, stagePath);
|
|
17342
|
+
if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
|
|
17343
|
+
const now = Date.now();
|
|
17344
|
+
staged.query(
|
|
17345
|
+
"INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
|
|
17346
|
+
).run(process.pid, now, now);
|
|
17347
|
+
} finally {
|
|
17348
|
+
staged.close();
|
|
17349
|
+
}
|
|
17350
|
+
const sqlite2 = getSqlite();
|
|
17351
|
+
sqlite2.exec("BEGIN IMMEDIATE");
|
|
17352
|
+
try {
|
|
17353
|
+
this.assertGatewaySafe(sqlite2, false);
|
|
17354
|
+
this.assertNoRunningWork(sqlite2);
|
|
17355
|
+
safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
|
|
17356
|
+
const now = Date.now();
|
|
17357
|
+
sqlite2.query(
|
|
17358
|
+
"INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
|
|
17359
|
+
).run(process.pid, now, now);
|
|
17360
|
+
sqlite2.exec("COMMIT");
|
|
17361
|
+
} catch (error) {
|
|
17362
|
+
if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
|
|
17363
|
+
throw error;
|
|
17364
|
+
}
|
|
17365
|
+
const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
17366
|
+
if (checkpoint && checkpoint.busy !== 0) {
|
|
17367
|
+
throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
|
|
17368
|
+
}
|
|
17369
|
+
closeDb();
|
|
17370
|
+
safeUnlink(`${livePath}-wal`);
|
|
17371
|
+
safeUnlink(`${livePath}-shm`);
|
|
17372
|
+
renameSync(livePath, rollbackPath);
|
|
17373
|
+
liveMoved = true;
|
|
17374
|
+
renameSync(stagePath, livePath);
|
|
17375
|
+
const restored = getSqlite();
|
|
17376
|
+
restored.exec("BEGIN IMMEDIATE");
|
|
17377
|
+
let recoveredRunningTasks = 0;
|
|
17378
|
+
let closedRunningRuns = 0;
|
|
17379
|
+
try {
|
|
17380
|
+
recoveredRunningTasks = this.scalar(
|
|
17381
|
+
restored,
|
|
17382
|
+
"SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
|
|
17383
|
+
);
|
|
17384
|
+
closedRunningRuns = this.scalar(
|
|
17385
|
+
restored,
|
|
17386
|
+
"SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
|
|
17387
|
+
);
|
|
17388
|
+
const finishedAt = Math.floor(Date.now() / 1e3);
|
|
17389
|
+
restored.query(`
|
|
17390
|
+
UPDATE task_runs
|
|
17391
|
+
SET status = 'failed', finished_at = ?,
|
|
17392
|
+
log = CASE
|
|
17393
|
+
WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
|
|
17394
|
+
ELSE log || '
|
|
17395
|
+
\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
|
|
17396
|
+
END
|
|
17397
|
+
WHERE status = 'running'
|
|
17398
|
+
`).run(finishedAt);
|
|
17399
|
+
restored.exec(`
|
|
17400
|
+
UPDATE tasks
|
|
17401
|
+
SET status = 'pending', started_at = NULL, finished_at = NULL
|
|
17402
|
+
WHERE status = 'running'
|
|
17403
|
+
`);
|
|
17404
|
+
restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
17405
|
+
restored.exec("COMMIT");
|
|
17406
|
+
} catch (error) {
|
|
17407
|
+
if (restored.inTransaction) restored.exec("ROLLBACK");
|
|
17408
|
+
throw error;
|
|
17409
|
+
}
|
|
17410
|
+
const check = this.inspect(restored, livePath);
|
|
17411
|
+
if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
|
|
17412
|
+
safeUnlink(rollbackPath);
|
|
17413
|
+
return {
|
|
17414
|
+
sourcePath: source,
|
|
17415
|
+
safetyBackupPath: safetyBackup.path,
|
|
17416
|
+
recoveredRunningTasks,
|
|
17417
|
+
closedRunningRuns,
|
|
17418
|
+
check
|
|
17419
|
+
};
|
|
17420
|
+
} catch (error) {
|
|
17421
|
+
closeDb();
|
|
17422
|
+
safeUnlink(`${livePath}-wal`);
|
|
17423
|
+
safeUnlink(`${livePath}-shm`);
|
|
17424
|
+
safeUnlink(stagePath);
|
|
17425
|
+
safeUnlink(`${stagePath}-journal`);
|
|
17426
|
+
safeUnlink(`${stagePath}-wal`);
|
|
17427
|
+
safeUnlink(`${stagePath}-shm`);
|
|
17428
|
+
if (liveMoved && existsSync2(rollbackPath)) {
|
|
17429
|
+
safeUnlink(livePath);
|
|
17430
|
+
renameSync(rollbackPath, livePath);
|
|
17431
|
+
try {
|
|
17432
|
+
const original = getSqlite();
|
|
17433
|
+
original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
17434
|
+
} catch {
|
|
17435
|
+
}
|
|
17436
|
+
} else if (existsSync2(rollbackPath)) {
|
|
17437
|
+
safeUnlink(rollbackPath);
|
|
17438
|
+
} else {
|
|
17439
|
+
try {
|
|
17440
|
+
getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
17441
|
+
} catch {
|
|
17442
|
+
}
|
|
17443
|
+
}
|
|
17444
|
+
const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
|
|
17445
|
+
throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
|
|
17446
|
+
}
|
|
17447
|
+
}
|
|
17448
|
+
static inspectFile(path) {
|
|
17449
|
+
let sqlite2;
|
|
17450
|
+
try {
|
|
17451
|
+
sqlite2 = new Database3(path, { readonly: true, strict: true });
|
|
17452
|
+
} catch (error) {
|
|
17453
|
+
throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
|
|
17454
|
+
}
|
|
17455
|
+
try {
|
|
17456
|
+
return this.inspect(sqlite2, path);
|
|
17457
|
+
} finally {
|
|
17458
|
+
sqlite2.close();
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
17461
|
+
static inspect(sqlite2, path) {
|
|
17462
|
+
const tableRows = sqlite2.query(
|
|
17463
|
+
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
17464
|
+
).all();
|
|
17465
|
+
const tableNames = new Set(tableRows.map((row) => row.name));
|
|
17466
|
+
const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
|
|
17467
|
+
const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
|
|
17468
|
+
const integrityMessages = integrityRows.map((row) => row.integrity_check);
|
|
17469
|
+
const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
|
|
17470
|
+
const journal = sqlite2.query("PRAGMA journal_mode").get();
|
|
17471
|
+
const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
|
|
17472
|
+
const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
|
|
17473
|
+
const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
|
|
17474
|
+
const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync(path).size;
|
|
17475
|
+
return {
|
|
17476
|
+
ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
|
|
17477
|
+
path: normalizedPath(path),
|
|
17478
|
+
sizeBytes,
|
|
17479
|
+
journalMode: journal?.journal_mode ?? "unknown",
|
|
17480
|
+
integrityMessages,
|
|
17481
|
+
foreignKeyViolations,
|
|
17482
|
+
missingTables,
|
|
17483
|
+
counts,
|
|
17484
|
+
runningTasks,
|
|
17485
|
+
runningRuns
|
|
17486
|
+
};
|
|
17487
|
+
}
|
|
17488
|
+
static writeSnapshot(sqlite2, outputPath) {
|
|
17489
|
+
const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
|
|
17490
|
+
if (!liveCheck.ok) throw new Error("\u5F53\u524D\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C\uFF0C\u5DF2\u62D2\u7EDD\u521B\u5EFA\u5907\u4EFD");
|
|
17491
|
+
const output = normalizedPath(outputPath);
|
|
17492
|
+
if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
|
|
17493
|
+
throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
|
|
17494
|
+
}
|
|
17495
|
+
if (existsSync2(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
|
|
17496
|
+
mkdirSync2(dirname2(output), { recursive: true });
|
|
17497
|
+
const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
|
|
17498
|
+
try {
|
|
17499
|
+
writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
|
|
17500
|
+
const standalone = new Database3(temporary, { readwrite: true, create: false });
|
|
17501
|
+
try {
|
|
17502
|
+
standalone.query("PRAGMA journal_mode = DELETE").get();
|
|
17503
|
+
} finally {
|
|
17504
|
+
standalone.close();
|
|
17505
|
+
}
|
|
17506
|
+
safeUnlink(`${temporary}-wal`);
|
|
17507
|
+
safeUnlink(`${temporary}-shm`);
|
|
17508
|
+
const check = this.inspectFile(temporary);
|
|
17509
|
+
if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
|
|
17510
|
+
renameSync(temporary, output);
|
|
17511
|
+
const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
|
|
17512
|
+
return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
|
|
17513
|
+
} catch (error) {
|
|
17514
|
+
safeUnlink(temporary);
|
|
17515
|
+
safeUnlink(`${temporary}-wal`);
|
|
17516
|
+
safeUnlink(`${temporary}-shm`);
|
|
17517
|
+
throw error;
|
|
17518
|
+
}
|
|
17519
|
+
}
|
|
17520
|
+
static createBackupPath(kind) {
|
|
17521
|
+
const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
|
|
17522
|
+
const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename(DB_FILE_PATH, ".db");
|
|
17523
|
+
return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
|
|
17524
|
+
}
|
|
17525
|
+
static readCounts(sqlite2) {
|
|
17526
|
+
return {
|
|
17527
|
+
tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
|
|
17528
|
+
taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
|
|
17529
|
+
taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
|
|
17530
|
+
};
|
|
17531
|
+
}
|
|
17532
|
+
static scalar(sqlite2, statement) {
|
|
17533
|
+
const row = sqlite2.query(statement).get();
|
|
17534
|
+
return Number(row?.count ?? 0);
|
|
17535
|
+
}
|
|
17536
|
+
static assertGatewaySafe(sqlite2, allowCurrentGateway) {
|
|
17537
|
+
const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
|
|
17538
|
+
if (!lock || !isProcessAlive(lock.pid)) return;
|
|
17539
|
+
if (allowCurrentGateway && lock.pid === process.pid) return;
|
|
17540
|
+
throw new DatabaseMaintenanceConflictError(
|
|
17541
|
+
`Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
|
|
17542
|
+
);
|
|
17543
|
+
}
|
|
17544
|
+
static assertNoRunningWork(sqlite2) {
|
|
17545
|
+
const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
|
|
17546
|
+
const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
|
|
17547
|
+
if (runningTasks > 0 || runningRuns > 0) {
|
|
17548
|
+
throw new DatabaseMaintenanceConflictError(
|
|
17549
|
+
`\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
|
|
17550
|
+
);
|
|
17551
|
+
}
|
|
17552
|
+
}
|
|
17553
|
+
};
|
|
17554
|
+
|
|
17193
17555
|
// src/gateway/config.ts
|
|
17194
|
-
import { existsSync as
|
|
17556
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
17195
17557
|
import { homedir as homedir2 } from "os";
|
|
17196
17558
|
import { join as join2 } from "path";
|
|
17197
17559
|
var DEFAULT_CONFIG = {
|
|
@@ -17296,7 +17658,7 @@ function validateConfig(input) {
|
|
|
17296
17658
|
};
|
|
17297
17659
|
}
|
|
17298
17660
|
function loadConfig(path = getConfigPath()) {
|
|
17299
|
-
if (!
|
|
17661
|
+
if (!existsSync3(path)) return validateConfig({ configVersion: 2 });
|
|
17300
17662
|
try {
|
|
17301
17663
|
return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
17302
17664
|
} catch (error) {
|
|
@@ -17306,8 +17668,8 @@ function loadConfig(path = getConfigPath()) {
|
|
|
17306
17668
|
}
|
|
17307
17669
|
|
|
17308
17670
|
// src/web/index.tsx
|
|
17309
|
-
import { existsSync as
|
|
17310
|
-
import { dirname as
|
|
17671
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
|
|
17672
|
+
import { dirname as dirname3 } from "path";
|
|
17311
17673
|
|
|
17312
17674
|
// src/gateway/health.ts
|
|
17313
17675
|
var LOCK_STALE_MS = 3e4;
|
|
@@ -17459,7 +17821,7 @@ function esc(s) {
|
|
|
17459
17821
|
}
|
|
17460
17822
|
function readCurrentConfig() {
|
|
17461
17823
|
const configPath = getConfigPath();
|
|
17462
|
-
if (!
|
|
17824
|
+
if (!existsSync4(configPath)) return {};
|
|
17463
17825
|
try {
|
|
17464
17826
|
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
17465
17827
|
} catch {
|
|
@@ -17468,11 +17830,11 @@ function readCurrentConfig() {
|
|
|
17468
17830
|
}
|
|
17469
17831
|
function writeConfig(cfg) {
|
|
17470
17832
|
const configPath = getConfigPath();
|
|
17471
|
-
const dir =
|
|
17472
|
-
if (!
|
|
17833
|
+
const dir = dirname3(configPath);
|
|
17834
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
17473
17835
|
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
17474
|
-
|
|
17475
|
-
|
|
17836
|
+
writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
17837
|
+
renameSync2(tempPath, configPath);
|
|
17476
17838
|
}
|
|
17477
17839
|
var SHARED_STYLES = html`
|
|
17478
17840
|
<style>
|
|
@@ -17564,12 +17926,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
|
|
|
17564
17926
|
function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
|
|
17565
17927
|
|
|
17566
17928
|
async function clearDatabase(){
|
|
17567
|
-
if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\
|
|
17568
|
-
if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
|
|
17929
|
+
if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u521B\u5EFA\u5907\u4EFD\u3002'))return;
|
|
17930
|
+
if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u4E8B\u52A1\u6027\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
|
|
17569
17931
|
try{
|
|
17570
|
-
const r=await fetch('/api/database/clear',{method:'POST'});
|
|
17932
|
+
const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
|
|
17571
17933
|
const d=await r.json();
|
|
17572
|
-
if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
|
|
17934
|
+
if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
|
|
17573
17935
|
else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
|
|
17574
17936
|
}catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
|
|
17575
17937
|
}
|
|
@@ -17810,7 +18172,7 @@ app.get("/system", async (c) => {
|
|
|
17810
18172
|
const stats = await TaskService.stats({});
|
|
17811
18173
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
17812
18174
|
const templates = await TaskTemplateService.list(100);
|
|
17813
|
-
const configExists =
|
|
18175
|
+
const configExists = existsSync4(configPath);
|
|
17814
18176
|
let runRows = "";
|
|
17815
18177
|
if (runningRuns.length > 0) {
|
|
17816
18178
|
for (const run of runningRuns) {
|
|
@@ -17884,7 +18246,7 @@ app.get("/system", async (c) => {
|
|
|
17884
18246
|
|
|
17885
18247
|
<div class="card mt16" style="border-color:var(--red)">
|
|
17886
18248
|
<h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
|
|
17887
|
-
<p class="sm mu" style="margin:0 0 12px">\
|
|
18249
|
+
<p class="sm mu" style="margin:0 0 12px">\u81EA\u52A8\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A tasks\u3001task_runs \u548C task_templates\uFF1B\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u6267\u884C\u3002</p>
|
|
17888
18250
|
<button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
|
|
17889
18251
|
</div>`;
|
|
17890
18252
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
@@ -17989,14 +18351,16 @@ app.put("/api/config", async (c) => {
|
|
|
17989
18351
|
}
|
|
17990
18352
|
});
|
|
17991
18353
|
app.post("/api/database/clear", async (c) => {
|
|
18354
|
+
const body = await c.req.json().catch(() => ({}));
|
|
18355
|
+
if (body.confirmation !== "CLEAR") {
|
|
18356
|
+
return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
|
|
18357
|
+
}
|
|
17992
18358
|
try {
|
|
17993
|
-
const
|
|
17994
|
-
|
|
17995
|
-
await db.delete(taskTemplates3);
|
|
17996
|
-
await db.delete(tasks3);
|
|
17997
|
-
return c.json({ success: true });
|
|
18359
|
+
const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
|
|
18360
|
+
return c.json({ success: true, ...result });
|
|
17998
18361
|
} catch (err) {
|
|
17999
|
-
|
|
18362
|
+
const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
|
|
18363
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
|
|
18000
18364
|
}
|
|
18001
18365
|
});
|
|
18002
18366
|
var dashboardApp = app;
|