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/cli/index.js
CHANGED
|
@@ -9734,6 +9734,23 @@ var init_task_service = __esm({
|
|
|
9734
9734
|
).returning();
|
|
9735
9735
|
return result.length;
|
|
9736
9736
|
}
|
|
9737
|
+
static async resetOrphanRunningToPending() {
|
|
9738
|
+
const result = await db.update(tasks2).set({
|
|
9739
|
+
status: "pending",
|
|
9740
|
+
startedAt: null,
|
|
9741
|
+
finishedAt: null
|
|
9742
|
+
}).where(
|
|
9743
|
+
and(
|
|
9744
|
+
eq(tasks2.status, "running"),
|
|
9745
|
+
sql`NOT EXISTS (
|
|
9746
|
+
SELECT 1 FROM ${taskRuns2}
|
|
9747
|
+
WHERE ${taskRuns2.taskId} = ${tasks2.id}
|
|
9748
|
+
AND ${taskRuns2.status} = 'running'
|
|
9749
|
+
)`
|
|
9750
|
+
)
|
|
9751
|
+
).returning();
|
|
9752
|
+
return result.length;
|
|
9753
|
+
}
|
|
9737
9754
|
static async cancel(id, scope = {}) {
|
|
9738
9755
|
const conditions = [
|
|
9739
9756
|
eq(tasks2.id, id),
|
|
@@ -17078,26 +17095,26 @@ var require_CronDate = __commonJS({
|
|
|
17078
17095
|
* @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
|
|
17079
17096
|
* @param {string} [tz] - The timezone to use for the CronDate.
|
|
17080
17097
|
*/
|
|
17081
|
-
constructor(
|
|
17098
|
+
constructor(timestamp2, tz) {
|
|
17082
17099
|
const dateOpts = { zone: tz };
|
|
17083
|
-
if (!
|
|
17100
|
+
if (!timestamp2) {
|
|
17084
17101
|
this.#date = luxon_1.DateTime.local();
|
|
17085
|
-
} else if (
|
|
17086
|
-
this.#date =
|
|
17087
|
-
this.#dstStart =
|
|
17088
|
-
this.#dstEnd =
|
|
17089
|
-
} else if (
|
|
17090
|
-
this.#date = luxon_1.DateTime.fromJSDate(
|
|
17091
|
-
} else if (typeof
|
|
17092
|
-
this.#date = luxon_1.DateTime.fromMillis(
|
|
17102
|
+
} else if (timestamp2 instanceof _CronDate) {
|
|
17103
|
+
this.#date = timestamp2.#date;
|
|
17104
|
+
this.#dstStart = timestamp2.#dstStart;
|
|
17105
|
+
this.#dstEnd = timestamp2.#dstEnd;
|
|
17106
|
+
} else if (timestamp2 instanceof Date) {
|
|
17107
|
+
this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
|
|
17108
|
+
} else if (typeof timestamp2 === "number") {
|
|
17109
|
+
this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
|
|
17093
17110
|
} else {
|
|
17094
|
-
this.#date = luxon_1.DateTime.fromISO(
|
|
17095
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(
|
|
17096
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(
|
|
17097
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(
|
|
17111
|
+
this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
|
|
17112
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
|
|
17113
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
|
|
17114
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
|
|
17098
17115
|
}
|
|
17099
17116
|
if (!this.#date.isValid) {
|
|
17100
|
-
throw new Error(`CronDate: unhandled timestamp: ${
|
|
17117
|
+
throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
|
|
17101
17118
|
}
|
|
17102
17119
|
if (tz && tz !== this.#date.zoneName) {
|
|
17103
17120
|
this.#date = this.#date.setZone(tz);
|
|
@@ -19155,6 +19172,412 @@ var init_task_template_service = __esm({
|
|
|
19155
19172
|
}
|
|
19156
19173
|
});
|
|
19157
19174
|
|
|
19175
|
+
// src/core/process-control.ts
|
|
19176
|
+
import { spawnSync } from "child_process";
|
|
19177
|
+
import { basename } from "path";
|
|
19178
|
+
function isSafePid(pid) {
|
|
19179
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
19180
|
+
}
|
|
19181
|
+
function isProcessAlive(pid) {
|
|
19182
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
19183
|
+
try {
|
|
19184
|
+
process.kill(pid, 0);
|
|
19185
|
+
return true;
|
|
19186
|
+
} catch (error) {
|
|
19187
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
19188
|
+
}
|
|
19189
|
+
}
|
|
19190
|
+
function inspectUnixProcess(pid) {
|
|
19191
|
+
const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
19192
|
+
encoding: "utf8",
|
|
19193
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19194
|
+
});
|
|
19195
|
+
if (result.status !== 0) return null;
|
|
19196
|
+
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
19197
|
+
if (!match2) return null;
|
|
19198
|
+
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
19199
|
+
}
|
|
19200
|
+
function commandMatches(command, expectedExecutable) {
|
|
19201
|
+
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
19202
|
+
if (!expectedName) return false;
|
|
19203
|
+
return command.toLowerCase().includes(expectedName);
|
|
19204
|
+
}
|
|
19205
|
+
function signalSpawnedProcessTree(pid, signal) {
|
|
19206
|
+
if (!isSafePid(pid)) return false;
|
|
19207
|
+
if (process.platform !== "win32") {
|
|
19208
|
+
try {
|
|
19209
|
+
process.kill(-pid, signal);
|
|
19210
|
+
return true;
|
|
19211
|
+
} catch {
|
|
19212
|
+
}
|
|
19213
|
+
}
|
|
19214
|
+
try {
|
|
19215
|
+
process.kill(pid, signal);
|
|
19216
|
+
return true;
|
|
19217
|
+
} catch {
|
|
19218
|
+
return false;
|
|
19219
|
+
}
|
|
19220
|
+
}
|
|
19221
|
+
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
19222
|
+
if (!isSafePid(pid)) return false;
|
|
19223
|
+
if (process.platform === "win32") {
|
|
19224
|
+
const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
19225
|
+
encoding: "utf8",
|
|
19226
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19227
|
+
});
|
|
19228
|
+
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
19229
|
+
const args = ["/PID", String(pid), "/T"];
|
|
19230
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
19231
|
+
return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
19232
|
+
}
|
|
19233
|
+
const info = inspectUnixProcess(pid);
|
|
19234
|
+
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
19235
|
+
try {
|
|
19236
|
+
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
19237
|
+
else process.kill(pid, signal);
|
|
19238
|
+
return true;
|
|
19239
|
+
} catch {
|
|
19240
|
+
return false;
|
|
19241
|
+
}
|
|
19242
|
+
}
|
|
19243
|
+
var init_process_control = __esm({
|
|
19244
|
+
"src/core/process-control.ts"() {
|
|
19245
|
+
"use strict";
|
|
19246
|
+
}
|
|
19247
|
+
});
|
|
19248
|
+
|
|
19249
|
+
// src/core/services/database-maintenance.service.ts
|
|
19250
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
19251
|
+
import { randomUUID } from "crypto";
|
|
19252
|
+
import {
|
|
19253
|
+
chmodSync,
|
|
19254
|
+
constants,
|
|
19255
|
+
copyFileSync,
|
|
19256
|
+
existsSync as existsSync2,
|
|
19257
|
+
mkdirSync as mkdirSync2,
|
|
19258
|
+
renameSync,
|
|
19259
|
+
statSync,
|
|
19260
|
+
unlinkSync,
|
|
19261
|
+
writeFileSync
|
|
19262
|
+
} from "fs";
|
|
19263
|
+
import { tmpdir } from "os";
|
|
19264
|
+
import { basename as basename2, dirname as dirname2, resolve } from "path";
|
|
19265
|
+
function timestamp() {
|
|
19266
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
19267
|
+
}
|
|
19268
|
+
function normalizedPath(path) {
|
|
19269
|
+
return path === ":memory:" ? path : resolve(path);
|
|
19270
|
+
}
|
|
19271
|
+
function safeUnlink(path) {
|
|
19272
|
+
try {
|
|
19273
|
+
unlinkSync(path);
|
|
19274
|
+
} catch (error) {
|
|
19275
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
19276
|
+
if (code !== "ENOENT") throw error;
|
|
19277
|
+
}
|
|
19278
|
+
}
|
|
19279
|
+
function errorMessage(error) {
|
|
19280
|
+
return error instanceof Error ? error.message : String(error);
|
|
19281
|
+
}
|
|
19282
|
+
var REQUIRED_TABLES, DatabaseMaintenanceConflictError, DatabaseMaintenanceService;
|
|
19283
|
+
var init_database_maintenance_service = __esm({
|
|
19284
|
+
"src/core/services/database-maintenance.service.ts"() {
|
|
19285
|
+
"use strict";
|
|
19286
|
+
init_db2();
|
|
19287
|
+
init_process_control();
|
|
19288
|
+
REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
|
|
19289
|
+
DatabaseMaintenanceConflictError = class extends Error {
|
|
19290
|
+
constructor(message) {
|
|
19291
|
+
super(message);
|
|
19292
|
+
this.name = "DatabaseMaintenanceConflictError";
|
|
19293
|
+
}
|
|
19294
|
+
};
|
|
19295
|
+
DatabaseMaintenanceService = class {
|
|
19296
|
+
static check() {
|
|
19297
|
+
return this.inspect(getSqlite(), DB_FILE_PATH);
|
|
19298
|
+
}
|
|
19299
|
+
static backup(outputPath) {
|
|
19300
|
+
const sqlite2 = getSqlite();
|
|
19301
|
+
try {
|
|
19302
|
+
sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
|
|
19303
|
+
} catch {
|
|
19304
|
+
}
|
|
19305
|
+
return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
|
|
19306
|
+
}
|
|
19307
|
+
static clear(options = {}) {
|
|
19308
|
+
const sqlite2 = getSqlite();
|
|
19309
|
+
this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
|
|
19310
|
+
this.assertNoRunningWork(sqlite2);
|
|
19311
|
+
sqlite2.exec("BEGIN IMMEDIATE");
|
|
19312
|
+
let backup = null;
|
|
19313
|
+
try {
|
|
19314
|
+
this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
|
|
19315
|
+
this.assertNoRunningWork(sqlite2);
|
|
19316
|
+
const before = this.readCounts(sqlite2);
|
|
19317
|
+
backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
|
|
19318
|
+
sqlite2.exec("DELETE FROM task_runs");
|
|
19319
|
+
sqlite2.exec("DELETE FROM tasks");
|
|
19320
|
+
sqlite2.exec("DELETE FROM task_templates");
|
|
19321
|
+
sqlite2.exec("COMMIT");
|
|
19322
|
+
return {
|
|
19323
|
+
backupPath: backup.path,
|
|
19324
|
+
deleted: before,
|
|
19325
|
+
check: this.inspect(sqlite2, DB_FILE_PATH)
|
|
19326
|
+
};
|
|
19327
|
+
} catch (error) {
|
|
19328
|
+
if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
|
|
19329
|
+
if (error instanceof DatabaseMaintenanceConflictError) throw error;
|
|
19330
|
+
const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
|
|
19331
|
+
throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
|
|
19332
|
+
}
|
|
19333
|
+
}
|
|
19334
|
+
static restore(sourcePath) {
|
|
19335
|
+
if (DB_FILE_PATH === ":memory:") {
|
|
19336
|
+
throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
|
|
19337
|
+
}
|
|
19338
|
+
const source = normalizedPath(sourcePath);
|
|
19339
|
+
const livePath = normalizedPath(DB_FILE_PATH);
|
|
19340
|
+
if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
|
|
19341
|
+
const current = getSqlite();
|
|
19342
|
+
this.assertGatewaySafe(current, false);
|
|
19343
|
+
this.assertNoRunningWork(current);
|
|
19344
|
+
if (!existsSync2(source) || !statSync(source).isFile()) {
|
|
19345
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
|
|
19346
|
+
}
|
|
19347
|
+
let sourceCheck;
|
|
19348
|
+
try {
|
|
19349
|
+
sourceCheck = this.inspectFile(source);
|
|
19350
|
+
} catch (error) {
|
|
19351
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
|
|
19352
|
+
}
|
|
19353
|
+
if (!sourceCheck.ok) {
|
|
19354
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
|
|
19355
|
+
}
|
|
19356
|
+
const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
|
|
19357
|
+
const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
|
|
19358
|
+
let safetyBackup = null;
|
|
19359
|
+
let liveMoved = false;
|
|
19360
|
+
try {
|
|
19361
|
+
copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
|
|
19362
|
+
chmodSync(stagePath, 384);
|
|
19363
|
+
const staged = new Database3(stagePath);
|
|
19364
|
+
try {
|
|
19365
|
+
const stagedCheck = this.inspect(staged, stagePath);
|
|
19366
|
+
if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
|
|
19367
|
+
const now = Date.now();
|
|
19368
|
+
staged.query(
|
|
19369
|
+
"INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
|
|
19370
|
+
).run(process.pid, now, now);
|
|
19371
|
+
} finally {
|
|
19372
|
+
staged.close();
|
|
19373
|
+
}
|
|
19374
|
+
const sqlite2 = getSqlite();
|
|
19375
|
+
sqlite2.exec("BEGIN IMMEDIATE");
|
|
19376
|
+
try {
|
|
19377
|
+
this.assertGatewaySafe(sqlite2, false);
|
|
19378
|
+
this.assertNoRunningWork(sqlite2);
|
|
19379
|
+
safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
|
|
19380
|
+
const now = Date.now();
|
|
19381
|
+
sqlite2.query(
|
|
19382
|
+
"INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
|
|
19383
|
+
).run(process.pid, now, now);
|
|
19384
|
+
sqlite2.exec("COMMIT");
|
|
19385
|
+
} catch (error) {
|
|
19386
|
+
if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
|
|
19387
|
+
throw error;
|
|
19388
|
+
}
|
|
19389
|
+
const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
19390
|
+
if (checkpoint && checkpoint.busy !== 0) {
|
|
19391
|
+
throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
|
|
19392
|
+
}
|
|
19393
|
+
closeDb();
|
|
19394
|
+
safeUnlink(`${livePath}-wal`);
|
|
19395
|
+
safeUnlink(`${livePath}-shm`);
|
|
19396
|
+
renameSync(livePath, rollbackPath);
|
|
19397
|
+
liveMoved = true;
|
|
19398
|
+
renameSync(stagePath, livePath);
|
|
19399
|
+
const restored = getSqlite();
|
|
19400
|
+
restored.exec("BEGIN IMMEDIATE");
|
|
19401
|
+
let recoveredRunningTasks = 0;
|
|
19402
|
+
let closedRunningRuns = 0;
|
|
19403
|
+
try {
|
|
19404
|
+
recoveredRunningTasks = this.scalar(
|
|
19405
|
+
restored,
|
|
19406
|
+
"SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
|
|
19407
|
+
);
|
|
19408
|
+
closedRunningRuns = this.scalar(
|
|
19409
|
+
restored,
|
|
19410
|
+
"SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
|
|
19411
|
+
);
|
|
19412
|
+
const finishedAt = Math.floor(Date.now() / 1e3);
|
|
19413
|
+
restored.query(`
|
|
19414
|
+
UPDATE task_runs
|
|
19415
|
+
SET status = 'failed', finished_at = ?,
|
|
19416
|
+
log = CASE
|
|
19417
|
+
WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
|
|
19418
|
+
ELSE log || '
|
|
19419
|
+
\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
|
|
19420
|
+
END
|
|
19421
|
+
WHERE status = 'running'
|
|
19422
|
+
`).run(finishedAt);
|
|
19423
|
+
restored.exec(`
|
|
19424
|
+
UPDATE tasks
|
|
19425
|
+
SET status = 'pending', started_at = NULL, finished_at = NULL
|
|
19426
|
+
WHERE status = 'running'
|
|
19427
|
+
`);
|
|
19428
|
+
restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
19429
|
+
restored.exec("COMMIT");
|
|
19430
|
+
} catch (error) {
|
|
19431
|
+
if (restored.inTransaction) restored.exec("ROLLBACK");
|
|
19432
|
+
throw error;
|
|
19433
|
+
}
|
|
19434
|
+
const check = this.inspect(restored, livePath);
|
|
19435
|
+
if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
|
|
19436
|
+
safeUnlink(rollbackPath);
|
|
19437
|
+
return {
|
|
19438
|
+
sourcePath: source,
|
|
19439
|
+
safetyBackupPath: safetyBackup.path,
|
|
19440
|
+
recoveredRunningTasks,
|
|
19441
|
+
closedRunningRuns,
|
|
19442
|
+
check
|
|
19443
|
+
};
|
|
19444
|
+
} catch (error) {
|
|
19445
|
+
closeDb();
|
|
19446
|
+
safeUnlink(`${livePath}-wal`);
|
|
19447
|
+
safeUnlink(`${livePath}-shm`);
|
|
19448
|
+
safeUnlink(stagePath);
|
|
19449
|
+
safeUnlink(`${stagePath}-journal`);
|
|
19450
|
+
safeUnlink(`${stagePath}-wal`);
|
|
19451
|
+
safeUnlink(`${stagePath}-shm`);
|
|
19452
|
+
if (liveMoved && existsSync2(rollbackPath)) {
|
|
19453
|
+
safeUnlink(livePath);
|
|
19454
|
+
renameSync(rollbackPath, livePath);
|
|
19455
|
+
try {
|
|
19456
|
+
const original = getSqlite();
|
|
19457
|
+
original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
19458
|
+
} catch {
|
|
19459
|
+
}
|
|
19460
|
+
} else if (existsSync2(rollbackPath)) {
|
|
19461
|
+
safeUnlink(rollbackPath);
|
|
19462
|
+
} else {
|
|
19463
|
+
try {
|
|
19464
|
+
getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
19465
|
+
} catch {
|
|
19466
|
+
}
|
|
19467
|
+
}
|
|
19468
|
+
const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
|
|
19469
|
+
throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
|
|
19470
|
+
}
|
|
19471
|
+
}
|
|
19472
|
+
static inspectFile(path) {
|
|
19473
|
+
let sqlite2;
|
|
19474
|
+
try {
|
|
19475
|
+
sqlite2 = new Database3(path, { readonly: true, strict: true });
|
|
19476
|
+
} catch (error) {
|
|
19477
|
+
throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
|
|
19478
|
+
}
|
|
19479
|
+
try {
|
|
19480
|
+
return this.inspect(sqlite2, path);
|
|
19481
|
+
} finally {
|
|
19482
|
+
sqlite2.close();
|
|
19483
|
+
}
|
|
19484
|
+
}
|
|
19485
|
+
static inspect(sqlite2, path) {
|
|
19486
|
+
const tableRows = sqlite2.query(
|
|
19487
|
+
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
19488
|
+
).all();
|
|
19489
|
+
const tableNames = new Set(tableRows.map((row) => row.name));
|
|
19490
|
+
const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
|
|
19491
|
+
const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
|
|
19492
|
+
const integrityMessages = integrityRows.map((row) => row.integrity_check);
|
|
19493
|
+
const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
|
|
19494
|
+
const journal = sqlite2.query("PRAGMA journal_mode").get();
|
|
19495
|
+
const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
|
|
19496
|
+
const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
|
|
19497
|
+
const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
|
|
19498
|
+
const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync(path).size;
|
|
19499
|
+
return {
|
|
19500
|
+
ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
|
|
19501
|
+
path: normalizedPath(path),
|
|
19502
|
+
sizeBytes,
|
|
19503
|
+
journalMode: journal?.journal_mode ?? "unknown",
|
|
19504
|
+
integrityMessages,
|
|
19505
|
+
foreignKeyViolations,
|
|
19506
|
+
missingTables,
|
|
19507
|
+
counts,
|
|
19508
|
+
runningTasks,
|
|
19509
|
+
runningRuns
|
|
19510
|
+
};
|
|
19511
|
+
}
|
|
19512
|
+
static writeSnapshot(sqlite2, outputPath) {
|
|
19513
|
+
const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
|
|
19514
|
+
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");
|
|
19515
|
+
const output = normalizedPath(outputPath);
|
|
19516
|
+
if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
|
|
19517
|
+
throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
|
|
19518
|
+
}
|
|
19519
|
+
if (existsSync2(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
|
|
19520
|
+
mkdirSync2(dirname2(output), { recursive: true });
|
|
19521
|
+
const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
|
|
19522
|
+
try {
|
|
19523
|
+
writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
|
|
19524
|
+
const standalone = new Database3(temporary, { readwrite: true, create: false });
|
|
19525
|
+
try {
|
|
19526
|
+
standalone.query("PRAGMA journal_mode = DELETE").get();
|
|
19527
|
+
} finally {
|
|
19528
|
+
standalone.close();
|
|
19529
|
+
}
|
|
19530
|
+
safeUnlink(`${temporary}-wal`);
|
|
19531
|
+
safeUnlink(`${temporary}-shm`);
|
|
19532
|
+
const check = this.inspectFile(temporary);
|
|
19533
|
+
if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
|
|
19534
|
+
renameSync(temporary, output);
|
|
19535
|
+
const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
|
|
19536
|
+
return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
|
|
19537
|
+
} catch (error) {
|
|
19538
|
+
safeUnlink(temporary);
|
|
19539
|
+
safeUnlink(`${temporary}-wal`);
|
|
19540
|
+
safeUnlink(`${temporary}-shm`);
|
|
19541
|
+
throw error;
|
|
19542
|
+
}
|
|
19543
|
+
}
|
|
19544
|
+
static createBackupPath(kind) {
|
|
19545
|
+
const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
|
|
19546
|
+
const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename2(DB_FILE_PATH, ".db");
|
|
19547
|
+
return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
|
|
19548
|
+
}
|
|
19549
|
+
static readCounts(sqlite2) {
|
|
19550
|
+
return {
|
|
19551
|
+
tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
|
|
19552
|
+
taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
|
|
19553
|
+
taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
|
|
19554
|
+
};
|
|
19555
|
+
}
|
|
19556
|
+
static scalar(sqlite2, statement) {
|
|
19557
|
+
const row = sqlite2.query(statement).get();
|
|
19558
|
+
return Number(row?.count ?? 0);
|
|
19559
|
+
}
|
|
19560
|
+
static assertGatewaySafe(sqlite2, allowCurrentGateway) {
|
|
19561
|
+
const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
|
|
19562
|
+
if (!lock || !isProcessAlive(lock.pid)) return;
|
|
19563
|
+
if (allowCurrentGateway && lock.pid === process.pid) return;
|
|
19564
|
+
throw new DatabaseMaintenanceConflictError(
|
|
19565
|
+
`Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
|
|
19566
|
+
);
|
|
19567
|
+
}
|
|
19568
|
+
static assertNoRunningWork(sqlite2) {
|
|
19569
|
+
const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
|
|
19570
|
+
const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
|
|
19571
|
+
if (runningTasks > 0 || runningRuns > 0) {
|
|
19572
|
+
throw new DatabaseMaintenanceConflictError(
|
|
19573
|
+
`\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
|
|
19574
|
+
);
|
|
19575
|
+
}
|
|
19576
|
+
}
|
|
19577
|
+
};
|
|
19578
|
+
}
|
|
19579
|
+
});
|
|
19580
|
+
|
|
19158
19581
|
// src/gateway/config.ts
|
|
19159
19582
|
var config_exports = {};
|
|
19160
19583
|
__export(config_exports, {
|
|
@@ -19164,7 +19587,7 @@ __export(config_exports, {
|
|
|
19164
19587
|
loadConfig: () => loadConfig,
|
|
19165
19588
|
validateConfig: () => validateConfig
|
|
19166
19589
|
});
|
|
19167
|
-
import { existsSync as
|
|
19590
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
19168
19591
|
import { homedir as homedir2 } from "os";
|
|
19169
19592
|
import { join as join2 } from "path";
|
|
19170
19593
|
function getConfigPath() {
|
|
@@ -19244,7 +19667,7 @@ function validateConfig(input) {
|
|
|
19244
19667
|
};
|
|
19245
19668
|
}
|
|
19246
19669
|
function loadConfig(path = getConfigPath()) {
|
|
19247
|
-
if (!
|
|
19670
|
+
if (!existsSync3(path)) return validateConfig({ configVersion: 2 });
|
|
19248
19671
|
try {
|
|
19249
19672
|
return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
19250
19673
|
} catch (error) {
|
|
@@ -19290,6 +19713,7 @@ __export(pm2_exports, {
|
|
|
19290
19713
|
ensureGateway: () => ensureGateway,
|
|
19291
19714
|
getPackageVersion: () => getPackageVersion,
|
|
19292
19715
|
install: () => install,
|
|
19716
|
+
installMacLaunchAgent: () => installMacLaunchAgent,
|
|
19293
19717
|
isGatewayReady: () => isGatewayReady,
|
|
19294
19718
|
isGatewayRunning: () => isGatewayRunning,
|
|
19295
19719
|
isPm2Installed: () => isPm2Installed,
|
|
@@ -19297,12 +19721,12 @@ __export(pm2_exports, {
|
|
|
19297
19721
|
uninstall: () => uninstall,
|
|
19298
19722
|
upgrade: () => upgrade
|
|
19299
19723
|
});
|
|
19300
|
-
import { execSync, spawnSync } from "child_process";
|
|
19301
|
-
import { existsSync as
|
|
19724
|
+
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
19725
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
19302
19726
|
import { homedir as homedir3 } from "os";
|
|
19303
|
-
import { dirname as
|
|
19727
|
+
import { dirname as dirname3, isAbsolute, join as join3 } from "path";
|
|
19304
19728
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
19305
|
-
import { Database as
|
|
19729
|
+
import { Database as Database4 } from "bun:sqlite";
|
|
19306
19730
|
function getPackageVersion() {
|
|
19307
19731
|
const envVersion = process.env.npm_package_version;
|
|
19308
19732
|
if (envVersion) return envVersion;
|
|
@@ -19317,14 +19741,14 @@ function getPackageVersion() {
|
|
|
19317
19741
|
function resolveGatewayEntry() {
|
|
19318
19742
|
const override = process.env.SUPERTASK_GATEWAY_ENTRY;
|
|
19319
19743
|
if (override) {
|
|
19320
|
-
if (!
|
|
19744
|
+
if (!existsSync4(override)) throw new Error(`[supertask] Gateway entry not found: ${override}`);
|
|
19321
19745
|
return override;
|
|
19322
19746
|
}
|
|
19323
19747
|
const candidates = [
|
|
19324
19748
|
join3(__dirname, "../gateway/index.js"),
|
|
19325
19749
|
join3(__dirname, "../gateway/index.ts")
|
|
19326
19750
|
];
|
|
19327
|
-
const entry = candidates.find((candidate) =>
|
|
19751
|
+
const entry = candidates.find((candidate) => existsSync4(candidate));
|
|
19328
19752
|
if (!entry) throw new Error(`[supertask] Gateway entry not found. Checked: ${candidates.join(", ")}`);
|
|
19329
19753
|
return entry;
|
|
19330
19754
|
}
|
|
@@ -19334,7 +19758,7 @@ function versionFile() {
|
|
|
19334
19758
|
function getRunningVersion() {
|
|
19335
19759
|
try {
|
|
19336
19760
|
const path = versionFile();
|
|
19337
|
-
if (!
|
|
19761
|
+
if (!existsSync4(path)) return null;
|
|
19338
19762
|
return readFileSync2(path, "utf-8").trim() || null;
|
|
19339
19763
|
} catch {
|
|
19340
19764
|
return null;
|
|
@@ -19342,14 +19766,91 @@ function getRunningVersion() {
|
|
|
19342
19766
|
}
|
|
19343
19767
|
function writeRunningVersion(version2) {
|
|
19344
19768
|
const path = versionFile();
|
|
19345
|
-
|
|
19346
|
-
|
|
19769
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
19770
|
+
writeFileSync2(path, version2, "utf-8");
|
|
19347
19771
|
}
|
|
19348
19772
|
function pm2Bin() {
|
|
19349
19773
|
return process.env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
|
|
19350
19774
|
}
|
|
19775
|
+
function xmlEscape(value) {
|
|
19776
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
19777
|
+
}
|
|
19778
|
+
function resolvePm2Bin() {
|
|
19779
|
+
const configured = pm2Bin();
|
|
19780
|
+
if (isAbsolute(configured)) return configured;
|
|
19781
|
+
const result = spawnSync2("which", [configured], { encoding: "utf8" });
|
|
19782
|
+
const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
|
|
19783
|
+
if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
|
|
19784
|
+
return resolved;
|
|
19785
|
+
}
|
|
19786
|
+
function launchAgentPath() {
|
|
19787
|
+
return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join3(homedir3(), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
|
|
19788
|
+
}
|
|
19789
|
+
function launchctlBin() {
|
|
19790
|
+
return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
|
|
19791
|
+
}
|
|
19792
|
+
function installMacLaunchAgent() {
|
|
19793
|
+
if (typeof process.getuid !== "function") {
|
|
19794
|
+
throw new Error("[supertask] \u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u83B7\u53D6 macOS \u7528\u6237 ID");
|
|
19795
|
+
}
|
|
19796
|
+
const path = launchAgentPath();
|
|
19797
|
+
const home = homedir3();
|
|
19798
|
+
const pm2Home = process.env.PM2_HOME ?? join3(home, ".pm2");
|
|
19799
|
+
const environmentPath = process.env.PATH ?? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
19800
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
19801
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
19802
|
+
<plist version="1.0">
|
|
19803
|
+
<dict>
|
|
19804
|
+
<key>Label</key>
|
|
19805
|
+
<string>${MAC_LAUNCH_AGENT_LABEL}</string>
|
|
19806
|
+
<key>ProgramArguments</key>
|
|
19807
|
+
<array>
|
|
19808
|
+
<string>${xmlEscape(resolvePm2Bin())}</string>
|
|
19809
|
+
<string>resurrect</string>
|
|
19810
|
+
</array>
|
|
19811
|
+
<key>RunAtLoad</key>
|
|
19812
|
+
<true/>
|
|
19813
|
+
<key>KeepAlive</key>
|
|
19814
|
+
<dict>
|
|
19815
|
+
<key>SuccessfulExit</key>
|
|
19816
|
+
<false/>
|
|
19817
|
+
</dict>
|
|
19818
|
+
<key>ThrottleInterval</key>
|
|
19819
|
+
<integer>10</integer>
|
|
19820
|
+
<key>EnvironmentVariables</key>
|
|
19821
|
+
<dict>
|
|
19822
|
+
<key>HOME</key>
|
|
19823
|
+
<string>${xmlEscape(home)}</string>
|
|
19824
|
+
<key>PATH</key>
|
|
19825
|
+
<string>${xmlEscape(environmentPath)}</string>
|
|
19826
|
+
<key>PM2_HOME</key>
|
|
19827
|
+
<string>${xmlEscape(pm2Home)}</string>
|
|
19828
|
+
</dict>
|
|
19829
|
+
<key>StandardErrorPath</key>
|
|
19830
|
+
<string>${xmlEscape(join3(pm2Home, "supertask-launchd-error.log"))}</string>
|
|
19831
|
+
<key>StandardOutPath</key>
|
|
19832
|
+
<string>${xmlEscape(join3(pm2Home, "supertask-launchd-output.log"))}</string>
|
|
19833
|
+
</dict>
|
|
19834
|
+
</plist>
|
|
19835
|
+
`;
|
|
19836
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
19837
|
+
writeFileSync2(path, plist, { mode: 384 });
|
|
19838
|
+
chmodSync2(path, 384);
|
|
19839
|
+
const domain = `gui/${process.getuid()}`;
|
|
19840
|
+
spawnSync2(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
|
|
19841
|
+
stdio: "ignore"
|
|
19842
|
+
});
|
|
19843
|
+
const loaded = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
|
|
19844
|
+
encoding: "utf8"
|
|
19845
|
+
});
|
|
19846
|
+
if (loaded.status !== 0) {
|
|
19847
|
+
const output = `${loaded.stdout ?? ""}${loaded.stderr ?? ""}`.trim();
|
|
19848
|
+
throw new Error(`[supertask] macOS LaunchAgent \u52A0\u8F7D\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${loaded.status}`}`);
|
|
19849
|
+
}
|
|
19850
|
+
return path;
|
|
19851
|
+
}
|
|
19351
19852
|
function isPm2Installed() {
|
|
19352
|
-
const result =
|
|
19853
|
+
const result = spawnSync2(pm2Bin(), ["--version"], {
|
|
19353
19854
|
stdio: "ignore",
|
|
19354
19855
|
shell: process.platform === "win32"
|
|
19355
19856
|
});
|
|
@@ -19370,7 +19871,7 @@ function installPm2() {
|
|
|
19370
19871
|
}
|
|
19371
19872
|
}
|
|
19372
19873
|
function pm2Exec(args) {
|
|
19373
|
-
const result =
|
|
19874
|
+
const result = spawnSync2(pm2Bin(), args, {
|
|
19374
19875
|
stdio: ["pipe", "pipe", "pipe"],
|
|
19375
19876
|
encoding: "utf-8",
|
|
19376
19877
|
env: process.env,
|
|
@@ -19406,10 +19907,10 @@ function databasePath() {
|
|
|
19406
19907
|
}
|
|
19407
19908
|
function isGatewayReady(expectedPid) {
|
|
19408
19909
|
const path = databasePath();
|
|
19409
|
-
if (!
|
|
19910
|
+
if (!existsSync4(path)) return false;
|
|
19410
19911
|
let database = null;
|
|
19411
19912
|
try {
|
|
19412
|
-
database = new
|
|
19913
|
+
database = new Database4(path, { readonly: true });
|
|
19413
19914
|
const row = database.query(
|
|
19414
19915
|
"SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
|
|
19415
19916
|
).get();
|
|
@@ -19486,10 +19987,19 @@ function install() {
|
|
|
19486
19987
|
pm2StartGateway();
|
|
19487
19988
|
writeRunningVersion(version2);
|
|
19488
19989
|
savePm2State();
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19990
|
+
if (process.platform === "darwin") {
|
|
19991
|
+
try {
|
|
19992
|
+
const path = installMacLaunchAgent();
|
|
19993
|
+
console.log(`[supertask] macOS LaunchAgent installed: ${path}`);
|
|
19994
|
+
} catch (error) {
|
|
19995
|
+
console.warn(error instanceof Error ? error.message : String(error));
|
|
19996
|
+
}
|
|
19997
|
+
} else {
|
|
19998
|
+
const startup = pm2Exec(["startup"]);
|
|
19999
|
+
if (startup.output) console.log(startup.output);
|
|
20000
|
+
if (!startup.ok) {
|
|
20001
|
+
console.warn("[supertask] pm2 startup \u672A\u5B8C\u6210\uFF1B\u8BF7\u6309 pm2 \u8F93\u51FA\u6267\u884C\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650\u7684\u547D\u4EE4\uFF0C\u7136\u540E\u8FD0\u884C `pm2 save`\u3002");
|
|
20002
|
+
}
|
|
19493
20003
|
}
|
|
19494
20004
|
console.log("[supertask] Gateway installed and running.");
|
|
19495
20005
|
console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
|
|
@@ -19547,13 +20057,14 @@ function ensureGateway() {
|
|
|
19547
20057
|
savePm2State();
|
|
19548
20058
|
return { ok: true, action: existing ? "restarted" : "started" };
|
|
19549
20059
|
}
|
|
19550
|
-
var __dirname, PROCESS_NAME, GATEWAY_LOCK_STALE_MS;
|
|
20060
|
+
var __dirname, PROCESS_NAME, MAC_LAUNCH_AGENT_LABEL, GATEWAY_LOCK_STALE_MS;
|
|
19551
20061
|
var init_pm2 = __esm({
|
|
19552
20062
|
"src/daemon/pm2.ts"() {
|
|
19553
20063
|
"use strict";
|
|
19554
20064
|
init_config();
|
|
19555
|
-
__dirname =
|
|
20065
|
+
__dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
19556
20066
|
PROCESS_NAME = "supertask-gateway";
|
|
20067
|
+
MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
|
|
19557
20068
|
GATEWAY_LOCK_STALE_MS = 3e4;
|
|
19558
20069
|
}
|
|
19559
20070
|
});
|
|
@@ -19762,71 +20273,6 @@ var init_health = __esm({
|
|
|
19762
20273
|
}
|
|
19763
20274
|
});
|
|
19764
20275
|
|
|
19765
|
-
// src/core/process-control.ts
|
|
19766
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
19767
|
-
import { basename } from "path";
|
|
19768
|
-
function isSafePid(pid) {
|
|
19769
|
-
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
19770
|
-
}
|
|
19771
|
-
function inspectUnixProcess(pid) {
|
|
19772
|
-
const result = spawnSync2("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
19773
|
-
encoding: "utf8",
|
|
19774
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
19775
|
-
});
|
|
19776
|
-
if (result.status !== 0) return null;
|
|
19777
|
-
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
19778
|
-
if (!match2) return null;
|
|
19779
|
-
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
19780
|
-
}
|
|
19781
|
-
function commandMatches(command, expectedExecutable) {
|
|
19782
|
-
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
19783
|
-
if (!expectedName) return false;
|
|
19784
|
-
return command.toLowerCase().includes(expectedName);
|
|
19785
|
-
}
|
|
19786
|
-
function signalSpawnedProcessTree(pid, signal) {
|
|
19787
|
-
if (!isSafePid(pid)) return false;
|
|
19788
|
-
if (process.platform !== "win32") {
|
|
19789
|
-
try {
|
|
19790
|
-
process.kill(-pid, signal);
|
|
19791
|
-
return true;
|
|
19792
|
-
} catch {
|
|
19793
|
-
}
|
|
19794
|
-
}
|
|
19795
|
-
try {
|
|
19796
|
-
process.kill(pid, signal);
|
|
19797
|
-
return true;
|
|
19798
|
-
} catch {
|
|
19799
|
-
return false;
|
|
19800
|
-
}
|
|
19801
|
-
}
|
|
19802
|
-
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
19803
|
-
if (!isSafePid(pid)) return false;
|
|
19804
|
-
if (process.platform === "win32") {
|
|
19805
|
-
const list = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
19806
|
-
encoding: "utf8",
|
|
19807
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
19808
|
-
});
|
|
19809
|
-
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
19810
|
-
const args = ["/PID", String(pid), "/T"];
|
|
19811
|
-
if (signal === "SIGKILL") args.push("/F");
|
|
19812
|
-
return spawnSync2("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
19813
|
-
}
|
|
19814
|
-
const info = inspectUnixProcess(pid);
|
|
19815
|
-
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
19816
|
-
try {
|
|
19817
|
-
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
19818
|
-
else process.kill(pid, signal);
|
|
19819
|
-
return true;
|
|
19820
|
-
} catch {
|
|
19821
|
-
return false;
|
|
19822
|
-
}
|
|
19823
|
-
}
|
|
19824
|
-
var init_process_control = __esm({
|
|
19825
|
-
"src/core/process-control.ts"() {
|
|
19826
|
-
"use strict";
|
|
19827
|
-
}
|
|
19828
|
-
});
|
|
19829
|
-
|
|
19830
20276
|
// src/worker/index.ts
|
|
19831
20277
|
import { spawn } from "child_process";
|
|
19832
20278
|
var DEFAULT_MAX_OUTPUT_CHARS, FORBIDDEN_AGENT, WorkerEngine;
|
|
@@ -20099,14 +20545,14 @@ ${output}` : ""}`;
|
|
|
20099
20545
|
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
20100
20546
|
return Promise.resolve();
|
|
20101
20547
|
}
|
|
20102
|
-
return new Promise((
|
|
20548
|
+
return new Promise((resolve2) => {
|
|
20103
20549
|
const timeout = setTimeout(() => {
|
|
20104
20550
|
this.signalEntry(entry, "SIGKILL");
|
|
20105
|
-
|
|
20551
|
+
resolve2();
|
|
20106
20552
|
}, 5e3);
|
|
20107
20553
|
entry.child.once("close", () => {
|
|
20108
20554
|
clearTimeout(timeout);
|
|
20109
|
-
|
|
20555
|
+
resolve2();
|
|
20110
20556
|
});
|
|
20111
20557
|
this.signalEntry(entry, "SIGTERM");
|
|
20112
20558
|
});
|
|
@@ -22808,8 +23254,8 @@ __export(web_exports, {
|
|
|
22808
23254
|
dashboardApp: () => dashboardApp,
|
|
22809
23255
|
default: () => web_default
|
|
22810
23256
|
});
|
|
22811
|
-
import { existsSync as
|
|
22812
|
-
import { dirname as
|
|
23257
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, renameSync as renameSync2 } from "fs";
|
|
23258
|
+
import { dirname as dirname4 } from "path";
|
|
22813
23259
|
function parsePositiveInteger(value) {
|
|
22814
23260
|
if (!/^\d+$/.test(value)) return null;
|
|
22815
23261
|
const parsed = Number(value);
|
|
@@ -22859,7 +23305,7 @@ function esc(s) {
|
|
|
22859
23305
|
}
|
|
22860
23306
|
function readCurrentConfig() {
|
|
22861
23307
|
const configPath = getConfigPath();
|
|
22862
|
-
if (!
|
|
23308
|
+
if (!existsSync5(configPath)) return {};
|
|
22863
23309
|
try {
|
|
22864
23310
|
return JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
22865
23311
|
} catch {
|
|
@@ -22868,11 +23314,11 @@ function readCurrentConfig() {
|
|
|
22868
23314
|
}
|
|
22869
23315
|
function writeConfig(cfg) {
|
|
22870
23316
|
const configPath = getConfigPath();
|
|
22871
|
-
const dir =
|
|
22872
|
-
if (!
|
|
23317
|
+
const dir = dirname4(configPath);
|
|
23318
|
+
if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
|
|
22873
23319
|
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
22874
|
-
|
|
22875
|
-
|
|
23320
|
+
writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
23321
|
+
renameSync2(tempPath, configPath);
|
|
22876
23322
|
}
|
|
22877
23323
|
function renderLayout(title, activeTab, body) {
|
|
22878
23324
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -22889,12 +23335,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
|
|
|
22889
23335
|
function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
|
|
22890
23336
|
|
|
22891
23337
|
async function clearDatabase(){
|
|
22892
|
-
if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\
|
|
22893
|
-
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;
|
|
23338
|
+
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;
|
|
23339
|
+
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;
|
|
22894
23340
|
try{
|
|
22895
|
-
const r=await fetch('/api/database/clear',{method:'POST'});
|
|
23341
|
+
const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
|
|
22896
23342
|
const d=await r.json();
|
|
22897
|
-
if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
|
|
23343
|
+
if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
|
|
22898
23344
|
else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
|
|
22899
23345
|
}catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
|
|
22900
23346
|
}
|
|
@@ -22955,6 +23401,7 @@ var init_web = __esm({
|
|
|
22955
23401
|
init_task_service();
|
|
22956
23402
|
init_task_run_service();
|
|
22957
23403
|
init_task_template_service();
|
|
23404
|
+
init_database_maintenance_service();
|
|
22958
23405
|
init_drizzle_orm();
|
|
22959
23406
|
init_db2();
|
|
22960
23407
|
init_config();
|
|
@@ -23263,7 +23710,7 @@ var init_web = __esm({
|
|
|
23263
23710
|
const stats = await TaskService.stats({});
|
|
23264
23711
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
23265
23712
|
const templates = await TaskTemplateService.list(100);
|
|
23266
|
-
const configExists =
|
|
23713
|
+
const configExists = existsSync5(configPath);
|
|
23267
23714
|
let runRows = "";
|
|
23268
23715
|
if (runningRuns.length > 0) {
|
|
23269
23716
|
for (const run of runningRuns) {
|
|
@@ -23337,7 +23784,7 @@ var init_web = __esm({
|
|
|
23337
23784
|
|
|
23338
23785
|
<div class="card mt16" style="border-color:var(--red)">
|
|
23339
23786
|
<h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
|
|
23340
|
-
<p class="sm mu" style="margin:0 0 12px">\
|
|
23787
|
+
<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>
|
|
23341
23788
|
<button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
|
|
23342
23789
|
</div>`;
|
|
23343
23790
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
@@ -23442,14 +23889,16 @@ var init_web = __esm({
|
|
|
23442
23889
|
}
|
|
23443
23890
|
});
|
|
23444
23891
|
app.post("/api/database/clear", async (c) => {
|
|
23892
|
+
const body = await c.req.json().catch(() => ({}));
|
|
23893
|
+
if (body.confirmation !== "CLEAR") {
|
|
23894
|
+
return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
|
|
23895
|
+
}
|
|
23445
23896
|
try {
|
|
23446
|
-
const
|
|
23447
|
-
|
|
23448
|
-
await db.delete(taskTemplates4);
|
|
23449
|
-
await db.delete(tasks3);
|
|
23450
|
-
return c.json({ success: true });
|
|
23897
|
+
const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
|
|
23898
|
+
return c.json({ success: true, ...result });
|
|
23451
23899
|
} catch (err) {
|
|
23452
|
-
|
|
23900
|
+
const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
|
|
23901
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
|
|
23453
23902
|
}
|
|
23454
23903
|
});
|
|
23455
23904
|
dashboardApp = app;
|
|
@@ -23463,7 +23912,9 @@ var init_web = __esm({
|
|
|
23463
23912
|
// src/gateway/index.ts
|
|
23464
23913
|
var gateway_exports = {};
|
|
23465
23914
|
__export(gateway_exports, {
|
|
23466
|
-
|
|
23915
|
+
acquireLock: () => acquireLock,
|
|
23916
|
+
main: () => main,
|
|
23917
|
+
releaseLock: () => releaseLock
|
|
23467
23918
|
});
|
|
23468
23919
|
function acquireLock() {
|
|
23469
23920
|
const now = Date.now();
|
|
@@ -23472,7 +23923,8 @@ function acquireLock() {
|
|
|
23472
23923
|
sqlite.exec("BEGIN IMMEDIATE");
|
|
23473
23924
|
const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
|
|
23474
23925
|
if (existing) {
|
|
23475
|
-
|
|
23926
|
+
const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
|
|
23927
|
+
if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
|
|
23476
23928
|
sqlite.exec("ROLLBACK");
|
|
23477
23929
|
console.error(JSON.stringify({
|
|
23478
23930
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -23542,6 +23994,15 @@ async function main() {
|
|
|
23542
23994
|
const worker = new WorkerEngine(cfg);
|
|
23543
23995
|
const watchdog = new Watchdog(cfg);
|
|
23544
23996
|
const scheduler = new Scheduler(cfg);
|
|
23997
|
+
const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
|
|
23998
|
+
if (recoveredOrphans > 0) {
|
|
23999
|
+
console.log(JSON.stringify({
|
|
24000
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24001
|
+
level: "warn",
|
|
24002
|
+
msg: "reset orphan running tasks to pending",
|
|
24003
|
+
count: recoveredOrphans
|
|
24004
|
+
}));
|
|
24005
|
+
}
|
|
23545
24006
|
initializeGatewayHealth({
|
|
23546
24007
|
workerPollIntervalMs: cfg.worker.pollIntervalMs,
|
|
23547
24008
|
schedulerEnabled: cfg.scheduler.enabled,
|
|
@@ -23621,6 +24082,7 @@ var init_gateway = __esm({
|
|
|
23621
24082
|
init_task_service();
|
|
23622
24083
|
init_task_run_service();
|
|
23623
24084
|
init_health();
|
|
24085
|
+
init_process_control();
|
|
23624
24086
|
STALE_THRESHOLD_MS = 3e4;
|
|
23625
24087
|
if (import.meta.main) {
|
|
23626
24088
|
main();
|
|
@@ -23635,13 +24097,13 @@ __export(update_exports, {
|
|
|
23635
24097
|
resolveInstalledPlugin: () => resolveInstalledPlugin
|
|
23636
24098
|
});
|
|
23637
24099
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
23638
|
-
import { existsSync as
|
|
24100
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync } from "fs";
|
|
23639
24101
|
import { homedir as homedir4 } from "os";
|
|
23640
24102
|
import { join as join4 } from "path";
|
|
23641
24103
|
function pluginAt(packageDir) {
|
|
23642
24104
|
const packageJson = join4(packageDir, "package.json");
|
|
23643
24105
|
const gatewayEntry = join4(packageDir, "dist/gateway/index.js");
|
|
23644
|
-
if (!
|
|
24106
|
+
if (!existsSync6(packageJson) || !existsSync6(gatewayEntry)) return null;
|
|
23645
24107
|
try {
|
|
23646
24108
|
const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
|
|
23647
24109
|
if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
|
|
@@ -23674,7 +24136,7 @@ function resolveInstalledPlugin() {
|
|
|
23674
24136
|
return plugin;
|
|
23675
24137
|
}
|
|
23676
24138
|
const root = cacheRoot();
|
|
23677
|
-
const packageDirs =
|
|
24139
|
+
const packageDirs = existsSync6(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === PACKAGE_NAME || entry.name.startsWith(`${PACKAGE_NAME}@`))).map((entry) => join4(root, entry.name, "node_modules", PACKAGE_NAME)) : [];
|
|
23678
24140
|
const installed = packageDirs.map(pluginAt).filter((plugin) => plugin !== null).sort((left, right) => compareVersions(right.version, left.version));
|
|
23679
24141
|
if (!installed[0]) {
|
|
23680
24142
|
throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
|
|
@@ -23732,6 +24194,7 @@ var {
|
|
|
23732
24194
|
// src/cli/index.ts
|
|
23733
24195
|
init_task_service();
|
|
23734
24196
|
init_task_template_service();
|
|
24197
|
+
init_database_maintenance_service();
|
|
23735
24198
|
init_db2();
|
|
23736
24199
|
|
|
23737
24200
|
// src/core/duration.ts
|
|
@@ -23980,15 +24443,35 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
|
|
|
23980
24443
|
console.log(JSON.stringify({ deleted, id: parseInt(options.id) }));
|
|
23981
24444
|
}))
|
|
23982
24445
|
);
|
|
24446
|
+
var databaseCommand = new Command("db").description("\u6570\u636E\u5E93\u68C0\u67E5\u3001\u5907\u4EFD\u3001\u6E05\u7A7A\u4E0E\u6062\u590D");
|
|
24447
|
+
databaseCommand.command("check").description("\u68C0\u67E5\u6570\u636E\u5E93\u5B8C\u6574\u6027\u3001\u5916\u952E\u548C\u4E1A\u52A1\u8868\u7EDF\u8BA1").action(async () => withDb(async () => {
|
|
24448
|
+
console.log(JSON.stringify(DatabaseMaintenanceService.check(), null, 2));
|
|
24449
|
+
}));
|
|
24450
|
+
databaseCommand.command("backup").description("\u521B\u5EFA\u7ECF\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C\u7684\u4E00\u81F4\u6027\u5907\u4EFD").option("-o, --output <path>", "\u5907\u4EFD\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u5199\u5165\u6570\u636E\u5E93\u76EE\u5F55\uFF09").action(async (options) => withDb(async () => {
|
|
24451
|
+
console.log(JSON.stringify(DatabaseMaintenanceService.backup(options.output), null, 2));
|
|
24452
|
+
}));
|
|
24453
|
+
databaseCommand.command("clear").description("\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F").option("--confirm <word>", "\u5371\u9669\u64CD\u4F5C\u786E\u8BA4\uFF0C\u5FC5\u987B\u586B\u5199 CLEAR").action(async (options) => withDb(async () => {
|
|
24454
|
+
if (options.confirm !== "CLEAR") {
|
|
24455
|
+
throw new Error("\u6E05\u7A7A\u6570\u636E\u5E93\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 --confirm CLEAR");
|
|
24456
|
+
}
|
|
24457
|
+
console.log(JSON.stringify(DatabaseMaintenanceService.clear(), null, 2));
|
|
24458
|
+
}));
|
|
24459
|
+
databaseCommand.command("restore").description("\u81EA\u52A8\u5907\u4EFD\u5F53\u524D\u5E93\u540E\uFF0C\u4ECE\u6307\u5B9A\u5907\u4EFD\u6062\u590D\u6570\u636E\u5E93").requiredOption("--from <path>", "\u8981\u6062\u590D\u7684 SQLite \u5907\u4EFD\u6587\u4EF6").option("--confirm <word>", "\u5371\u9669\u64CD\u4F5C\u786E\u8BA4\uFF0C\u5FC5\u987B\u586B\u5199 RESTORE").action(async (options) => withDb(async () => {
|
|
24460
|
+
if (options.confirm !== "RESTORE") {
|
|
24461
|
+
throw new Error("\u6062\u590D\u6570\u636E\u5E93\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 --confirm RESTORE");
|
|
24462
|
+
}
|
|
24463
|
+
console.log(JSON.stringify(DatabaseMaintenanceService.restore(options.from), null, 2));
|
|
24464
|
+
}));
|
|
24465
|
+
program2.addCommand(databaseCommand);
|
|
23983
24466
|
program2.command("init").description("Initialize SuperTask (create config + run migrations)").action(async () => withDb(async () => {
|
|
23984
|
-
const { existsSync:
|
|
23985
|
-
const { dirname:
|
|
24467
|
+
const { existsSync: existsSync7, mkdirSync: mkdirSync5, writeFileSync: writeFileSync4 } = await import("fs");
|
|
24468
|
+
const { dirname: dirname5 } = await import("path");
|
|
23986
24469
|
const { getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
23987
24470
|
const configPath = getConfigPath2();
|
|
23988
|
-
if (!
|
|
23989
|
-
const dir =
|
|
23990
|
-
if (!
|
|
23991
|
-
|
|
24471
|
+
if (!existsSync7(configPath)) {
|
|
24472
|
+
const dir = dirname5(configPath);
|
|
24473
|
+
if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
|
|
24474
|
+
writeFileSync4(configPath, JSON.stringify({
|
|
23992
24475
|
configVersion: 2,
|
|
23993
24476
|
worker: { maxConcurrency: 2 },
|
|
23994
24477
|
scheduler: { enabled: true }
|