opencode-supertask 0.1.19 → 0.1.21
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 +51 -25
- package/dist/cli/index.js +1256 -587
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.js +849 -302
- package/dist/gateway/index.js.map +1 -1
- package/dist/plugin/supertask.js +585 -233
- package/dist/plugin/supertask.js.map +1 -1
- package/dist/web/index.js +546 -213
- package/dist/web/index.js.map +1 -1
- package/dist/worker/index.d.ts +19 -7
- package/dist/worker/index.js +434 -152
- package/dist/worker/index.js.map +1 -1
- package/drizzle/0004_reliability_fields.sql +31 -0
- package/drizzle/meta/0004_snapshot.json +85 -13
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2353,11 +2353,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2353
2353
|
};
|
|
2354
2354
|
this._checkNumberOfArguments();
|
|
2355
2355
|
const processedArgs = [];
|
|
2356
|
-
this.registeredArguments.forEach((declaredArg,
|
|
2356
|
+
this.registeredArguments.forEach((declaredArg, index2) => {
|
|
2357
2357
|
let value = declaredArg.defaultValue;
|
|
2358
2358
|
if (declaredArg.variadic) {
|
|
2359
|
-
if (
|
|
2360
|
-
value = this.args.slice(
|
|
2359
|
+
if (index2 < this.args.length) {
|
|
2360
|
+
value = this.args.slice(index2);
|
|
2361
2361
|
if (declaredArg.parseArg) {
|
|
2362
2362
|
value = value.reduce((processed, v) => {
|
|
2363
2363
|
return myParseArg(declaredArg, v, processed);
|
|
@@ -2366,13 +2366,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2366
2366
|
} else if (value === void 0) {
|
|
2367
2367
|
value = [];
|
|
2368
2368
|
}
|
|
2369
|
-
} else if (
|
|
2370
|
-
value = this.args[
|
|
2369
|
+
} else if (index2 < this.args.length) {
|
|
2370
|
+
value = this.args[index2];
|
|
2371
2371
|
if (declaredArg.parseArg) {
|
|
2372
2372
|
value = myParseArg(declaredArg, value, declaredArg.defaultValue);
|
|
2373
2373
|
}
|
|
2374
2374
|
}
|
|
2375
|
-
processedArgs[
|
|
2375
|
+
processedArgs[index2] = value;
|
|
2376
2376
|
});
|
|
2377
2377
|
this.processedArgs = processedArgs;
|
|
2378
2378
|
}
|
|
@@ -2658,10 +2658,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2658
2658
|
}
|
|
2659
2659
|
}
|
|
2660
2660
|
if (/^--[^=]+=/.test(arg)) {
|
|
2661
|
-
const
|
|
2662
|
-
const option = this._findOption(arg.slice(0,
|
|
2661
|
+
const index2 = arg.indexOf("=");
|
|
2662
|
+
const option = this._findOption(arg.slice(0, index2));
|
|
2663
2663
|
if (option && (option.required || option.optional)) {
|
|
2664
|
-
this.emit(`option:${option.name()}`, arg.slice(
|
|
2664
|
+
this.emit(`option:${option.name()}`, arg.slice(index2 + 1));
|
|
2665
2665
|
continue;
|
|
2666
2666
|
}
|
|
2667
2667
|
}
|
|
@@ -4506,7 +4506,7 @@ var init_sql = __esm({
|
|
|
4506
4506
|
return new SQL([new StringChunk(str)]);
|
|
4507
4507
|
}
|
|
4508
4508
|
sql2.raw = raw2;
|
|
4509
|
-
function
|
|
4509
|
+
function join5(chunks, separator) {
|
|
4510
4510
|
const result = [];
|
|
4511
4511
|
for (const [i, chunk] of chunks.entries()) {
|
|
4512
4512
|
if (i > 0 && separator !== void 0) {
|
|
@@ -4516,7 +4516,7 @@ var init_sql = __esm({
|
|
|
4516
4516
|
}
|
|
4517
4517
|
return new SQL(result);
|
|
4518
4518
|
}
|
|
4519
|
-
sql2.join =
|
|
4519
|
+
sql2.join = join5;
|
|
4520
4520
|
function identifier(value) {
|
|
4521
4521
|
return new Name(value);
|
|
4522
4522
|
}
|
|
@@ -4660,8 +4660,8 @@ function haveSameKeys(left, right) {
|
|
|
4660
4660
|
if (leftKeys.length !== rightKeys.length) {
|
|
4661
4661
|
return false;
|
|
4662
4662
|
}
|
|
4663
|
-
for (const [
|
|
4664
|
-
if (key !== rightKeys[
|
|
4663
|
+
for (const [index2, key] of leftKeys.entries()) {
|
|
4664
|
+
if (key !== rightKeys[index2]) {
|
|
4665
4665
|
return false;
|
|
4666
4666
|
}
|
|
4667
4667
|
}
|
|
@@ -6522,8 +6522,8 @@ var init_dialect = __esm({
|
|
|
6522
6522
|
}
|
|
6523
6523
|
const joinsArray = [];
|
|
6524
6524
|
if (joins) {
|
|
6525
|
-
for (const [
|
|
6526
|
-
if (
|
|
6525
|
+
for (const [index2, joinMeta] of joins.entries()) {
|
|
6526
|
+
if (index2 === 0) {
|
|
6527
6527
|
joinsArray.push(sql` `);
|
|
6528
6528
|
}
|
|
6529
6529
|
const table = joinMeta.table;
|
|
@@ -6540,7 +6540,7 @@ var init_dialect = __esm({
|
|
|
6540
6540
|
sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`
|
|
6541
6541
|
);
|
|
6542
6542
|
}
|
|
6543
|
-
if (
|
|
6543
|
+
if (index2 < joins.length - 1) {
|
|
6544
6544
|
joinsArray.push(sql` `);
|
|
6545
6545
|
}
|
|
6546
6546
|
}
|
|
@@ -6553,9 +6553,9 @@ var init_dialect = __esm({
|
|
|
6553
6553
|
buildOrderBy(orderBy) {
|
|
6554
6554
|
const orderByList = [];
|
|
6555
6555
|
if (orderBy) {
|
|
6556
|
-
for (const [
|
|
6556
|
+
for (const [index2, orderByValue] of orderBy.entries()) {
|
|
6557
6557
|
orderByList.push(orderByValue);
|
|
6558
|
-
if (
|
|
6558
|
+
if (index2 < orderBy.length - 1) {
|
|
6559
6559
|
orderByList.push(sql`, `);
|
|
6560
6560
|
}
|
|
6561
6561
|
}
|
|
@@ -6604,9 +6604,9 @@ var init_dialect = __esm({
|
|
|
6604
6604
|
const havingSql = having ? sql` having ${having}` : void 0;
|
|
6605
6605
|
const groupByList = [];
|
|
6606
6606
|
if (groupBy) {
|
|
6607
|
-
for (const [
|
|
6607
|
+
for (const [index2, groupByValue] of groupBy.entries()) {
|
|
6608
6608
|
groupByList.push(groupByValue);
|
|
6609
|
-
if (
|
|
6609
|
+
if (index2 < groupBy.length - 1) {
|
|
6610
6610
|
groupByList.push(sql`, `);
|
|
6611
6611
|
}
|
|
6612
6612
|
}
|
|
@@ -7132,7 +7132,7 @@ var init_select2 = __esm({
|
|
|
7132
7132
|
return (table, on) => {
|
|
7133
7133
|
const baseTableName = this.tableName;
|
|
7134
7134
|
const tableName = getTableLikeName(table);
|
|
7135
|
-
if (typeof tableName === "string" && this.config.joins?.some((
|
|
7135
|
+
if (typeof tableName === "string" && this.config.joins?.some((join5) => join5.alias === tableName)) {
|
|
7136
7136
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
7137
7137
|
}
|
|
7138
7138
|
if (!this.isPartialSelect) {
|
|
@@ -7974,7 +7974,7 @@ var init_update = __esm({
|
|
|
7974
7974
|
createJoin(joinType) {
|
|
7975
7975
|
return (table, on) => {
|
|
7976
7976
|
const tableName = getTableLikeName(table);
|
|
7977
|
-
if (typeof tableName === "string" && this.config.joins.some((
|
|
7977
|
+
if (typeof tableName === "string" && this.config.joins.some((join5) => join5.alias === tableName)) {
|
|
7978
7978
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
7979
7979
|
}
|
|
7980
7980
|
if (typeof on === "function") {
|
|
@@ -8695,6 +8695,9 @@ var init_checks = __esm({
|
|
|
8695
8695
|
});
|
|
8696
8696
|
|
|
8697
8697
|
// node_modules/drizzle-orm/sqlite-core/indexes.js
|
|
8698
|
+
function index(name) {
|
|
8699
|
+
return new IndexBuilderOn(name, false);
|
|
8700
|
+
}
|
|
8698
8701
|
var IndexBuilderOn, IndexBuilder, Index;
|
|
8699
8702
|
var init_indexes = __esm({
|
|
8700
8703
|
"node_modules/drizzle-orm/sqlite-core/indexes.js"() {
|
|
@@ -9274,10 +9277,6 @@ var init_migrator = __esm({
|
|
|
9274
9277
|
});
|
|
9275
9278
|
|
|
9276
9279
|
// node_modules/drizzle-orm/bun-sqlite/migrator.js
|
|
9277
|
-
var migrator_exports = {};
|
|
9278
|
-
__export(migrator_exports, {
|
|
9279
|
-
migrate: () => migrate
|
|
9280
|
-
});
|
|
9281
9280
|
function migrate(db2, config) {
|
|
9282
9281
|
const migrations = readMigrationFiles(config);
|
|
9283
9282
|
db2.dialect.migrate(migrations, db2.session, config);
|
|
@@ -9327,12 +9326,17 @@ var init_schema = __esm({
|
|
|
9327
9326
|
resultLog: text("result_log"),
|
|
9328
9327
|
retryCount: integer("retry_count").default(0),
|
|
9329
9328
|
maxRetries: integer("max_retries").default(3),
|
|
9329
|
+
retryBackoffMs: integer("retry_backoff_ms").default(3e4),
|
|
9330
9330
|
// Gateway 扩展字段(毫秒)
|
|
9331
9331
|
retryAfter: integer("retry_after"),
|
|
9332
9332
|
timeoutMs: integer("timeout_ms"),
|
|
9333
9333
|
templateId: integer("template_id"),
|
|
9334
9334
|
scheduledAt: integer("scheduled_at")
|
|
9335
|
-
})
|
|
9335
|
+
}, (table) => [
|
|
9336
|
+
index("tasks_queue_idx").on(table.status, table.retryAfter, table.urgency, table.importance, table.createdAt, table.id),
|
|
9337
|
+
index("tasks_batch_status_idx").on(table.batchId, table.status),
|
|
9338
|
+
index("tasks_template_status_idx").on(table.templateId, table.status)
|
|
9339
|
+
]);
|
|
9336
9340
|
TASK_CATEGORIES = [
|
|
9337
9341
|
"translate",
|
|
9338
9342
|
"generate",
|
|
@@ -9342,7 +9346,7 @@ var init_schema = __esm({
|
|
|
9342
9346
|
];
|
|
9343
9347
|
taskRuns = sqliteTable("task_runs", {
|
|
9344
9348
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
9345
|
-
taskId: integer("task_id").notNull().references(() => tasks.id),
|
|
9349
|
+
taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
|
|
9346
9350
|
sessionId: text("session_id"),
|
|
9347
9351
|
model: text("model"),
|
|
9348
9352
|
status: text("status").default("running"),
|
|
@@ -9355,7 +9359,10 @@ var init_schema = __esm({
|
|
|
9355
9359
|
heartbeatAt: integer("heartbeat_at"),
|
|
9356
9360
|
workerPid: integer("worker_pid"),
|
|
9357
9361
|
childPid: integer("child_pid")
|
|
9358
|
-
})
|
|
9362
|
+
}, (table) => [
|
|
9363
|
+
index("task_runs_task_started_idx").on(table.taskId, table.startedAt, table.id),
|
|
9364
|
+
index("task_runs_status_heartbeat_idx").on(table.status, table.heartbeatAt)
|
|
9365
|
+
]);
|
|
9359
9366
|
taskTemplates = sqliteTable("task_templates", {
|
|
9360
9367
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
9361
9368
|
name: text("name").notNull(),
|
|
@@ -9366,6 +9373,7 @@ var init_schema = __esm({
|
|
|
9366
9373
|
category: text("category").default("general"),
|
|
9367
9374
|
importance: integer("importance").default(3),
|
|
9368
9375
|
urgency: integer("urgency").default(3),
|
|
9376
|
+
batchId: text("batch_id"),
|
|
9369
9377
|
scheduleType: text("schedule_type").notNull(),
|
|
9370
9378
|
cronExpr: text("cron_expr"),
|
|
9371
9379
|
intervalMs: integer("interval_ms"),
|
|
@@ -9373,12 +9381,15 @@ var init_schema = __esm({
|
|
|
9373
9381
|
maxInstances: integer("max_instances").default(1),
|
|
9374
9382
|
maxRetries: integer("max_retries").default(3),
|
|
9375
9383
|
retryBackoffMs: integer("retry_backoff_ms").default(3e4),
|
|
9384
|
+
timeoutMs: integer("timeout_ms"),
|
|
9376
9385
|
lastRunAt: integer("last_run_at"),
|
|
9377
9386
|
nextRunAt: integer("next_run_at"),
|
|
9378
9387
|
enabled: integer("enabled", { mode: "boolean" }).default(true),
|
|
9379
9388
|
createdAt: integer("created_at").default(0),
|
|
9380
9389
|
updatedAt: integer("updated_at").default(0)
|
|
9381
|
-
})
|
|
9390
|
+
}, (table) => [
|
|
9391
|
+
index("task_templates_due_idx").on(table.enabled, table.nextRunAt, table.id)
|
|
9392
|
+
]);
|
|
9382
9393
|
}
|
|
9383
9394
|
});
|
|
9384
9395
|
|
|
@@ -9400,7 +9411,15 @@ import { join, dirname } from "path";
|
|
|
9400
9411
|
import { fileURLToPath } from "url";
|
|
9401
9412
|
function getMigrationsFolder() {
|
|
9402
9413
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
9403
|
-
|
|
9414
|
+
let dir = __dirname2;
|
|
9415
|
+
for (let i = 0; i < 5; i++) {
|
|
9416
|
+
const candidate = join(dir, "drizzle", "meta", "_journal.json");
|
|
9417
|
+
if (existsSync(candidate)) {
|
|
9418
|
+
return join(dir, "drizzle");
|
|
9419
|
+
}
|
|
9420
|
+
dir = dirname(dir);
|
|
9421
|
+
}
|
|
9422
|
+
return join(__dirname2, "../../drizzle");
|
|
9404
9423
|
}
|
|
9405
9424
|
function initDb() {
|
|
9406
9425
|
const dataDir = dirname(DB_FILE_PATH);
|
|
@@ -9415,16 +9434,30 @@ function initDb() {
|
|
|
9415
9434
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
9416
9435
|
pid INTEGER NOT NULL,
|
|
9417
9436
|
acquired_at INTEGER NOT NULL,
|
|
9418
|
-
heartbeat_at INTEGER NOT NULL
|
|
9437
|
+
heartbeat_at INTEGER NOT NULL,
|
|
9438
|
+
ready_at INTEGER
|
|
9419
9439
|
);
|
|
9420
9440
|
`);
|
|
9441
|
+
const gatewayLockColumns = _sqlite.query("PRAGMA table_info(gateway_lock)").all();
|
|
9442
|
+
if (!gatewayLockColumns.some((column) => column.name === "ready_at")) {
|
|
9443
|
+
_sqlite.exec("ALTER TABLE gateway_lock ADD COLUMN ready_at INTEGER;");
|
|
9444
|
+
}
|
|
9421
9445
|
_db = drizzle(_sqlite, { schema: schema_exports });
|
|
9422
9446
|
if (!_migrationRan) {
|
|
9423
|
-
_migrationRan = true;
|
|
9424
9447
|
try {
|
|
9425
9448
|
migrate(_db, { migrationsFolder: getMigrationsFolder() });
|
|
9449
|
+
_sqlite.exec("PRAGMA foreign_keys = ON;");
|
|
9450
|
+
const violations = _sqlite.query("PRAGMA foreign_key_check;").all();
|
|
9451
|
+
if (violations.length > 0) {
|
|
9452
|
+
throw new Error(`\u68C0\u6D4B\u5230 ${violations.length} \u6761\u5B64\u7ACB\u5173\u8054\u8BB0\u5F55\uFF0C\u8BF7\u5148\u4FEE\u590D\u6570\u636E\u518D\u542F\u52A8`);
|
|
9453
|
+
}
|
|
9454
|
+
_migrationRan = true;
|
|
9426
9455
|
} catch (err) {
|
|
9427
9456
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9457
|
+
_migrationRan = false;
|
|
9458
|
+
_sqlite.close();
|
|
9459
|
+
_sqlite = null;
|
|
9460
|
+
_db = null;
|
|
9428
9461
|
console.error(`[supertask] migration failed: ${msg}`);
|
|
9429
9462
|
throw new Error(`[supertask] DB migration failed: ${msg}`);
|
|
9430
9463
|
}
|
|
@@ -9444,6 +9477,7 @@ function closeDb() {
|
|
|
9444
9477
|
_sqlite.close();
|
|
9445
9478
|
_sqlite = null;
|
|
9446
9479
|
_db = null;
|
|
9480
|
+
_migrationRan = false;
|
|
9447
9481
|
}
|
|
9448
9482
|
}
|
|
9449
9483
|
var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
|
|
@@ -9527,14 +9561,14 @@ var init_backoff = __esm({
|
|
|
9527
9561
|
});
|
|
9528
9562
|
|
|
9529
9563
|
// src/core/services/task.service.ts
|
|
9530
|
-
var tasks2, TaskService;
|
|
9564
|
+
var tasks2, taskRuns2, TaskService;
|
|
9531
9565
|
var init_task_service = __esm({
|
|
9532
9566
|
"src/core/services/task.service.ts"() {
|
|
9533
9567
|
"use strict";
|
|
9534
9568
|
init_db2();
|
|
9535
9569
|
init_drizzle_orm();
|
|
9536
9570
|
init_backoff();
|
|
9537
|
-
({ tasks: tasks2 } = schema_exports);
|
|
9571
|
+
({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
|
|
9538
9572
|
TaskService = class {
|
|
9539
9573
|
static buildScopeWhere(scope) {
|
|
9540
9574
|
const conditions = [];
|
|
@@ -9544,9 +9578,27 @@ var init_task_service = __esm({
|
|
|
9544
9578
|
return conditions;
|
|
9545
9579
|
}
|
|
9546
9580
|
static async add(data) {
|
|
9581
|
+
this.validateNewTask(data);
|
|
9547
9582
|
const result = await db.insert(tasks2).values(data).returning();
|
|
9548
9583
|
return result[0];
|
|
9549
9584
|
}
|
|
9585
|
+
static validateNewTask(data) {
|
|
9586
|
+
if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
|
|
9587
|
+
if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
|
|
9588
|
+
if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
|
|
9589
|
+
this.validateInteger("importance", data.importance, 1, 5);
|
|
9590
|
+
this.validateInteger("urgency", data.urgency, 1, 5);
|
|
9591
|
+
this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
|
|
9592
|
+
this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
|
|
9593
|
+
this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
|
|
9594
|
+
this.validateInteger("dependsOn", data.dependsOn, 1, Number.MAX_SAFE_INTEGER);
|
|
9595
|
+
}
|
|
9596
|
+
static validateInteger(name, value, min, max) {
|
|
9597
|
+
if (value === void 0 || value === null) return;
|
|
9598
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
9599
|
+
throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
9600
|
+
}
|
|
9601
|
+
}
|
|
9550
9602
|
static async next(scope = {}) {
|
|
9551
9603
|
const baseConditions = [...this.buildScopeWhere(scope)];
|
|
9552
9604
|
const nowMs = Date.now();
|
|
@@ -9569,7 +9621,7 @@ var init_task_service = __esm({
|
|
|
9569
9621
|
),
|
|
9570
9622
|
and(
|
|
9571
9623
|
eq(tasks2.status, "failed"),
|
|
9572
|
-
sql`${tasks2.retryCount}
|
|
9624
|
+
sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
|
|
9573
9625
|
retryAfterFilter
|
|
9574
9626
|
)
|
|
9575
9627
|
);
|
|
@@ -9583,7 +9635,8 @@ var init_task_service = __esm({
|
|
|
9583
9635
|
const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
|
|
9584
9636
|
desc(tasks2.urgency),
|
|
9585
9637
|
desc(tasks2.importance),
|
|
9586
|
-
asc(tasks2.createdAt)
|
|
9638
|
+
asc(tasks2.createdAt),
|
|
9639
|
+
asc(tasks2.id)
|
|
9587
9640
|
);
|
|
9588
9641
|
for (const task of allTasks) {
|
|
9589
9642
|
if (task.dependsOn) {
|
|
@@ -9604,7 +9657,7 @@ var init_task_service = __esm({
|
|
|
9604
9657
|
eq(tasks2.status, "pending"),
|
|
9605
9658
|
and(
|
|
9606
9659
|
eq(tasks2.status, "failed"),
|
|
9607
|
-
sql`${tasks2.retryCount}
|
|
9660
|
+
sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
|
|
9608
9661
|
)
|
|
9609
9662
|
),
|
|
9610
9663
|
...this.buildScopeWhere(scope)
|
|
@@ -9617,7 +9670,11 @@ var init_task_service = __esm({
|
|
|
9617
9670
|
return result[0] || null;
|
|
9618
9671
|
}
|
|
9619
9672
|
static async done(id, log, scope = {}) {
|
|
9620
|
-
const conditions = [
|
|
9673
|
+
const conditions = [
|
|
9674
|
+
eq(tasks2.id, id),
|
|
9675
|
+
eq(tasks2.status, "running"),
|
|
9676
|
+
...this.buildScopeWhere(scope)
|
|
9677
|
+
];
|
|
9621
9678
|
const result = await db.update(tasks2).set({
|
|
9622
9679
|
status: "done",
|
|
9623
9680
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
@@ -9628,17 +9685,24 @@ var init_task_service = __esm({
|
|
|
9628
9685
|
}
|
|
9629
9686
|
static async fail(id, log, scope = {}, options) {
|
|
9630
9687
|
const current = await this.getById(id, scope);
|
|
9631
|
-
if (!current) return null;
|
|
9688
|
+
if (!current || current.status !== "running") return null;
|
|
9632
9689
|
const newRetryCount = (current.retryCount ?? 0) + 1;
|
|
9633
9690
|
const maxRetries = current.maxRetries ?? 3;
|
|
9634
|
-
const isDeadLetter = options?.setDeadLetter ?? newRetryCount
|
|
9635
|
-
const conditions = [
|
|
9691
|
+
const isDeadLetter = options?.setDeadLetter ?? newRetryCount > maxRetries;
|
|
9692
|
+
const conditions = [
|
|
9693
|
+
eq(tasks2.id, id),
|
|
9694
|
+
eq(tasks2.status, "running"),
|
|
9695
|
+
...this.buildScopeWhere(scope)
|
|
9696
|
+
];
|
|
9636
9697
|
const result = await db.update(tasks2).set({
|
|
9637
9698
|
status: isDeadLetter ? "dead_letter" : "failed",
|
|
9638
9699
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
9639
9700
|
resultLog: log,
|
|
9640
9701
|
retryCount: newRetryCount,
|
|
9641
|
-
retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
|
|
9702
|
+
retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
|
|
9703
|
+
newRetryCount,
|
|
9704
|
+
current.retryBackoffMs ?? 3e4
|
|
9705
|
+
)
|
|
9642
9706
|
}).where(and(...conditions)).returning();
|
|
9643
9707
|
return result[0] || null;
|
|
9644
9708
|
}
|
|
@@ -9671,8 +9735,20 @@ var init_task_service = __esm({
|
|
|
9671
9735
|
return result.length;
|
|
9672
9736
|
}
|
|
9673
9737
|
static async cancel(id, scope = {}) {
|
|
9674
|
-
const conditions = [
|
|
9675
|
-
|
|
9738
|
+
const conditions = [
|
|
9739
|
+
eq(tasks2.id, id),
|
|
9740
|
+
or(
|
|
9741
|
+
eq(tasks2.status, "pending"),
|
|
9742
|
+
eq(tasks2.status, "running"),
|
|
9743
|
+
eq(tasks2.status, "failed")
|
|
9744
|
+
),
|
|
9745
|
+
...this.buildScopeWhere(scope)
|
|
9746
|
+
];
|
|
9747
|
+
const result = await db.update(tasks2).set({
|
|
9748
|
+
status: "cancelled",
|
|
9749
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
9750
|
+
retryAfter: null
|
|
9751
|
+
}).where(and(...conditions)).returning();
|
|
9676
9752
|
return result[0] || null;
|
|
9677
9753
|
}
|
|
9678
9754
|
static async retry(id, scope = {}) {
|
|
@@ -9684,7 +9760,8 @@ var init_task_service = __esm({
|
|
|
9684
9760
|
status: "pending",
|
|
9685
9761
|
startedAt: null,
|
|
9686
9762
|
finishedAt: null,
|
|
9687
|
-
retryAfter: null
|
|
9763
|
+
retryAfter: null,
|
|
9764
|
+
retryCount: 0
|
|
9688
9765
|
}).where(and(...conditions)).returning();
|
|
9689
9766
|
return result[0] || null;
|
|
9690
9767
|
}
|
|
@@ -9698,7 +9775,8 @@ var init_task_service = __esm({
|
|
|
9698
9775
|
status: "pending",
|
|
9699
9776
|
startedAt: null,
|
|
9700
9777
|
finishedAt: null,
|
|
9701
|
-
retryAfter: null
|
|
9778
|
+
retryAfter: null,
|
|
9779
|
+
retryCount: 0
|
|
9702
9780
|
}).where(and(...conditions)).returning();
|
|
9703
9781
|
return result.length;
|
|
9704
9782
|
}
|
|
@@ -9766,6 +9844,9 @@ var init_task_service = __esm({
|
|
|
9766
9844
|
}
|
|
9767
9845
|
static async delete(id, scope = {}) {
|
|
9768
9846
|
const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
|
|
9847
|
+
const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
|
|
9848
|
+
if (!existing[0]) return false;
|
|
9849
|
+
await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
|
|
9769
9850
|
const result = await db.delete(tasks2).where(and(...conditions)).returning();
|
|
9770
9851
|
return result.length > 0;
|
|
9771
9852
|
}
|
|
@@ -9984,7 +10065,7 @@ var require_CronField = __commonJS({
|
|
|
9984
10065
|
if (!isValidRange) {
|
|
9985
10066
|
throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
|
|
9986
10067
|
}
|
|
9987
|
-
const duplicate = this.#values.find((value,
|
|
10068
|
+
const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
|
|
9988
10069
|
if (duplicate) {
|
|
9989
10070
|
throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
|
|
9990
10071
|
}
|
|
@@ -17829,11 +17910,11 @@ var require_CronFieldCollection = __commonJS({
|
|
|
17829
17910
|
throw new Error("Unexpected range end");
|
|
17830
17911
|
}
|
|
17831
17912
|
if (step * multiplier > range.end) {
|
|
17832
|
-
const mapFn = (_,
|
|
17913
|
+
const mapFn = (_, index2) => {
|
|
17833
17914
|
if (typeof range.start !== "number") {
|
|
17834
17915
|
throw new Error("Unexpected range start");
|
|
17835
17916
|
}
|
|
17836
|
-
return
|
|
17917
|
+
return index2 % step === 0 ? range.start + index2 : null;
|
|
17837
17918
|
};
|
|
17838
17919
|
if (typeof range.start !== "number") {
|
|
17839
17920
|
throw new Error("Unexpected range start");
|
|
@@ -18725,9 +18806,9 @@ var require_CronExpressionParser = __commonJS({
|
|
|
18725
18806
|
if (field === CronUnit.DayOfWeek && max % 7 === 0) {
|
|
18726
18807
|
stack.push(0);
|
|
18727
18808
|
}
|
|
18728
|
-
for (let
|
|
18729
|
-
if (stack.indexOf(
|
|
18730
|
-
stack.push(
|
|
18809
|
+
for (let index2 = min; index2 <= max; index2 += repeatInterval) {
|
|
18810
|
+
if (stack.indexOf(index2) === -1) {
|
|
18811
|
+
stack.push(index2);
|
|
18731
18812
|
}
|
|
18732
18813
|
}
|
|
18733
18814
|
return stack;
|
|
@@ -18850,8 +18931,8 @@ var require_CronFileParser = __commonJS({
|
|
|
18850
18931
|
* @throws If file cannot be read
|
|
18851
18932
|
*/
|
|
18852
18933
|
static parseFileSync(filePath) {
|
|
18853
|
-
const { readFileSync:
|
|
18854
|
-
const data =
|
|
18934
|
+
const { readFileSync: readFileSync5 } = __require("fs");
|
|
18935
|
+
const data = readFileSync5(filePath, "utf8");
|
|
18855
18936
|
return _CronFileParser.#parseContent(data);
|
|
18856
18937
|
}
|
|
18857
18938
|
/**
|
|
@@ -18950,11 +19031,6 @@ var require_dist = __commonJS({
|
|
|
18950
19031
|
});
|
|
18951
19032
|
|
|
18952
19033
|
// src/core/cron-parser.ts
|
|
18953
|
-
var cron_parser_exports = {};
|
|
18954
|
-
__export(cron_parser_exports, {
|
|
18955
|
-
getNextCronRun: () => getNextCronRun,
|
|
18956
|
-
isValidCronExpr: () => isValidCronExpr
|
|
18957
|
-
});
|
|
18958
19034
|
function getNextCronRun(expr, afterMs) {
|
|
18959
19035
|
try {
|
|
18960
19036
|
const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
|
|
@@ -18992,6 +19068,7 @@ var init_task_template_service = __esm({
|
|
|
18992
19068
|
({ taskTemplates: taskTemplates2 } = schema_exports);
|
|
18993
19069
|
TaskTemplateService = class {
|
|
18994
19070
|
static async create(data) {
|
|
19071
|
+
this.validate(data);
|
|
18995
19072
|
const now = Date.now();
|
|
18996
19073
|
const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
|
|
18997
19074
|
const tmpl = result[0];
|
|
@@ -19007,8 +19084,38 @@ var init_task_template_service = __esm({
|
|
|
19007
19084
|
}
|
|
19008
19085
|
return tmpl;
|
|
19009
19086
|
}
|
|
19087
|
+
static validate(data) {
|
|
19088
|
+
if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
|
|
19089
|
+
if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
|
|
19090
|
+
if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
|
|
19091
|
+
const scheduleType = data.scheduleType;
|
|
19092
|
+
if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
|
|
19093
|
+
throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
|
|
19094
|
+
}
|
|
19095
|
+
if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
|
|
19096
|
+
throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
|
|
19097
|
+
}
|
|
19098
|
+
if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
|
|
19099
|
+
throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
|
|
19100
|
+
}
|
|
19101
|
+
if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
|
|
19102
|
+
throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
|
|
19103
|
+
}
|
|
19104
|
+
this.validateInteger("importance", data.importance, 1, 5);
|
|
19105
|
+
this.validateInteger("urgency", data.urgency, 1, 5);
|
|
19106
|
+
this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
|
|
19107
|
+
this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
|
|
19108
|
+
this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
|
|
19109
|
+
this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
|
|
19110
|
+
}
|
|
19111
|
+
static validateInteger(name, value, min, max) {
|
|
19112
|
+
if (value === void 0 || value === null) return;
|
|
19113
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
19114
|
+
throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
19115
|
+
}
|
|
19116
|
+
}
|
|
19010
19117
|
static async list(limit = 50) {
|
|
19011
|
-
return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
|
|
19118
|
+
return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
|
|
19012
19119
|
}
|
|
19013
19120
|
static async getById(id) {
|
|
19014
19121
|
const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
|
|
@@ -19051,137 +19158,466 @@ var init_task_template_service = __esm({
|
|
|
19051
19158
|
// src/gateway/config.ts
|
|
19052
19159
|
var config_exports = {};
|
|
19053
19160
|
__export(config_exports, {
|
|
19054
|
-
|
|
19055
|
-
|
|
19161
|
+
DEFAULT_CONFIG: () => DEFAULT_CONFIG,
|
|
19162
|
+
DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
|
|
19163
|
+
getConfigPath: () => getConfigPath,
|
|
19164
|
+
loadConfig: () => loadConfig,
|
|
19165
|
+
validateConfig: () => validateConfig
|
|
19056
19166
|
});
|
|
19057
|
-
import {
|
|
19167
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
19058
19168
|
import { homedir as homedir2 } from "os";
|
|
19059
19169
|
import { join as join2 } from "path";
|
|
19060
|
-
function
|
|
19061
|
-
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
result[key] = deepMerge(baseVal, val);
|
|
19068
|
-
continue;
|
|
19069
|
-
}
|
|
19070
|
-
}
|
|
19071
|
-
if (val !== void 0) {
|
|
19072
|
-
result[key] = val;
|
|
19073
|
-
}
|
|
19170
|
+
function getConfigPath() {
|
|
19171
|
+
return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
|
|
19172
|
+
}
|
|
19173
|
+
function objectAt(value, path) {
|
|
19174
|
+
if (value === void 0) return {};
|
|
19175
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
19176
|
+
throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
19074
19177
|
}
|
|
19075
|
-
return
|
|
19178
|
+
return value;
|
|
19076
19179
|
}
|
|
19077
|
-
function
|
|
19078
|
-
|
|
19079
|
-
|
|
19180
|
+
function integerAt(source, key, fallback, min, max, path) {
|
|
19181
|
+
const value = source[key];
|
|
19182
|
+
if (value === void 0) return fallback;
|
|
19183
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
19184
|
+
throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
19080
19185
|
}
|
|
19186
|
+
return value;
|
|
19187
|
+
}
|
|
19188
|
+
function booleanAt(source, key, fallback, path) {
|
|
19189
|
+
const value = source[key];
|
|
19190
|
+
if (value === void 0) return fallback;
|
|
19191
|
+
if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
19192
|
+
return value;
|
|
19193
|
+
}
|
|
19194
|
+
function validateConfig(input) {
|
|
19195
|
+
const root = objectAt(input, "config");
|
|
19196
|
+
const version2 = root.configVersion ?? 1;
|
|
19197
|
+
if (version2 !== 1 && version2 !== 2) {
|
|
19198
|
+
throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
|
|
19199
|
+
}
|
|
19200
|
+
const worker = objectAt(root.worker, "worker");
|
|
19201
|
+
const scheduler = objectAt(root.scheduler, "scheduler");
|
|
19202
|
+
const watchdog = objectAt(root.watchdog, "watchdog");
|
|
19203
|
+
const dashboard = objectAt(root.dashboard, "dashboard");
|
|
19204
|
+
const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
|
|
19205
|
+
const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
|
|
19206
|
+
let checkIntervalMs;
|
|
19207
|
+
let cleanupIntervalMs;
|
|
19208
|
+
if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
|
|
19209
|
+
checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
|
|
19210
|
+
cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
|
|
19211
|
+
} else {
|
|
19212
|
+
checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
|
|
19213
|
+
cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
|
|
19214
|
+
}
|
|
19215
|
+
if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
|
|
19216
|
+
throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
|
|
19217
|
+
}
|
|
19218
|
+
if (checkIntervalMs > heartbeatTimeoutMs) {
|
|
19219
|
+
throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
|
|
19220
|
+
}
|
|
19221
|
+
return {
|
|
19222
|
+
configVersion: 2,
|
|
19223
|
+
worker: {
|
|
19224
|
+
maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
|
|
19225
|
+
pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
|
|
19226
|
+
heartbeatIntervalMs,
|
|
19227
|
+
taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
|
|
19228
|
+
shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
|
|
19229
|
+
},
|
|
19230
|
+
scheduler: {
|
|
19231
|
+
enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
|
|
19232
|
+
checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
|
|
19233
|
+
},
|
|
19234
|
+
watchdog: {
|
|
19235
|
+
heartbeatTimeoutMs,
|
|
19236
|
+
checkIntervalMs,
|
|
19237
|
+
cleanupIntervalMs,
|
|
19238
|
+
retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
|
|
19239
|
+
},
|
|
19240
|
+
dashboard: {
|
|
19241
|
+
enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
|
|
19242
|
+
port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
|
|
19243
|
+
}
|
|
19244
|
+
};
|
|
19245
|
+
}
|
|
19246
|
+
function loadConfig(path = getConfigPath()) {
|
|
19247
|
+
if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
|
|
19081
19248
|
try {
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
19087
|
-
console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
|
|
19088
|
-
return DEFAULT_CONFIG;
|
|
19249
|
+
return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
19250
|
+
} catch (error) {
|
|
19251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19252
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
|
|
19089
19253
|
}
|
|
19090
19254
|
}
|
|
19091
|
-
var DEFAULT_CONFIG,
|
|
19255
|
+
var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
|
|
19092
19256
|
var init_config = __esm({
|
|
19093
19257
|
"src/gateway/config.ts"() {
|
|
19094
19258
|
"use strict";
|
|
19095
19259
|
DEFAULT_CONFIG = {
|
|
19260
|
+
configVersion: 2,
|
|
19096
19261
|
worker: {
|
|
19097
19262
|
maxConcurrency: 2,
|
|
19098
19263
|
pollIntervalMs: 1e3,
|
|
19099
19264
|
heartbeatIntervalMs: 3e4,
|
|
19100
|
-
taskTimeoutMs: 18e5
|
|
19265
|
+
taskTimeoutMs: 18e5,
|
|
19266
|
+
shutdownGracePeriodMs: 3e4
|
|
19101
19267
|
},
|
|
19102
19268
|
scheduler: {
|
|
19103
19269
|
enabled: true,
|
|
19104
|
-
checkIntervalMs: 1e3
|
|
19105
|
-
catchUp: "next"
|
|
19270
|
+
checkIntervalMs: 1e3
|
|
19106
19271
|
},
|
|
19107
19272
|
watchdog: {
|
|
19108
19273
|
heartbeatTimeoutMs: 6e5,
|
|
19109
|
-
|
|
19274
|
+
checkIntervalMs: 6e4,
|
|
19275
|
+
cleanupIntervalMs: 864e5,
|
|
19110
19276
|
retentionDays: 30
|
|
19111
19277
|
},
|
|
19112
19278
|
dashboard: {
|
|
19113
19279
|
enabled: true,
|
|
19114
19280
|
port: 4680
|
|
19115
|
-
},
|
|
19116
|
-
logging: {
|
|
19117
|
-
level: "info",
|
|
19118
|
-
format: "json"
|
|
19119
19281
|
}
|
|
19120
19282
|
};
|
|
19121
|
-
|
|
19283
|
+
DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
|
|
19284
|
+
}
|
|
19285
|
+
});
|
|
19286
|
+
|
|
19287
|
+
// src/daemon/pm2.ts
|
|
19288
|
+
var pm2_exports = {};
|
|
19289
|
+
__export(pm2_exports, {
|
|
19290
|
+
ensureGateway: () => ensureGateway,
|
|
19291
|
+
getPackageVersion: () => getPackageVersion,
|
|
19292
|
+
install: () => install,
|
|
19293
|
+
isGatewayReady: () => isGatewayReady,
|
|
19294
|
+
isGatewayRunning: () => isGatewayRunning,
|
|
19295
|
+
isPm2Installed: () => isPm2Installed,
|
|
19296
|
+
resolveGatewayEntry: () => resolveGatewayEntry,
|
|
19297
|
+
uninstall: () => uninstall,
|
|
19298
|
+
upgrade: () => upgrade
|
|
19299
|
+
});
|
|
19300
|
+
import { execSync, spawnSync } from "child_process";
|
|
19301
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
19302
|
+
import { homedir as homedir3 } from "os";
|
|
19303
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
19304
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
19305
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
19306
|
+
function getPackageVersion() {
|
|
19307
|
+
const envVersion = process.env.npm_package_version;
|
|
19308
|
+
if (envVersion) return envVersion;
|
|
19309
|
+
try {
|
|
19310
|
+
const pkgPath = join3(__dirname, "../../package.json");
|
|
19311
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
19312
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
19313
|
+
} catch {
|
|
19314
|
+
return "0.0.0";
|
|
19315
|
+
}
|
|
19316
|
+
}
|
|
19317
|
+
function resolveGatewayEntry() {
|
|
19318
|
+
const override = process.env.SUPERTASK_GATEWAY_ENTRY;
|
|
19319
|
+
if (override) {
|
|
19320
|
+
if (!existsSync3(override)) throw new Error(`[supertask] Gateway entry not found: ${override}`);
|
|
19321
|
+
return override;
|
|
19322
|
+
}
|
|
19323
|
+
const candidates = [
|
|
19324
|
+
join3(__dirname, "../gateway/index.js"),
|
|
19325
|
+
join3(__dirname, "../gateway/index.ts")
|
|
19326
|
+
];
|
|
19327
|
+
const entry = candidates.find((candidate) => existsSync3(candidate));
|
|
19328
|
+
if (!entry) throw new Error(`[supertask] Gateway entry not found. Checked: ${candidates.join(", ")}`);
|
|
19329
|
+
return entry;
|
|
19330
|
+
}
|
|
19331
|
+
function versionFile() {
|
|
19332
|
+
return process.env.SUPERTASK_VERSION_FILE ?? join3(homedir3(), ".local/share/opencode/supertask-gateway-version");
|
|
19333
|
+
}
|
|
19334
|
+
function getRunningVersion() {
|
|
19335
|
+
try {
|
|
19336
|
+
const path = versionFile();
|
|
19337
|
+
if (!existsSync3(path)) return null;
|
|
19338
|
+
return readFileSync2(path, "utf-8").trim() || null;
|
|
19339
|
+
} catch {
|
|
19340
|
+
return null;
|
|
19341
|
+
}
|
|
19342
|
+
}
|
|
19343
|
+
function writeRunningVersion(version2) {
|
|
19344
|
+
const path = versionFile();
|
|
19345
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
19346
|
+
writeFileSync(path, version2, "utf-8");
|
|
19347
|
+
}
|
|
19348
|
+
function pm2Bin() {
|
|
19349
|
+
return process.env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
|
|
19350
|
+
}
|
|
19351
|
+
function isPm2Installed() {
|
|
19352
|
+
const result = spawnSync(pm2Bin(), ["--version"], {
|
|
19353
|
+
stdio: "ignore",
|
|
19354
|
+
shell: process.platform === "win32"
|
|
19355
|
+
});
|
|
19356
|
+
return result.status === 0;
|
|
19357
|
+
}
|
|
19358
|
+
function installPm2() {
|
|
19359
|
+
console.log("[supertask] Installing pm2...");
|
|
19360
|
+
try {
|
|
19361
|
+
execSync("npm install -g pm2", { stdio: "inherit" });
|
|
19362
|
+
return true;
|
|
19363
|
+
} catch {
|
|
19364
|
+
try {
|
|
19365
|
+
execSync("bun install -g pm2", { stdio: "inherit" });
|
|
19366
|
+
return true;
|
|
19367
|
+
} catch {
|
|
19368
|
+
return false;
|
|
19369
|
+
}
|
|
19370
|
+
}
|
|
19371
|
+
}
|
|
19372
|
+
function pm2Exec(args) {
|
|
19373
|
+
const result = spawnSync(pm2Bin(), args, {
|
|
19374
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19375
|
+
encoding: "utf-8",
|
|
19376
|
+
env: process.env,
|
|
19377
|
+
shell: process.platform === "win32"
|
|
19378
|
+
});
|
|
19379
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
19380
|
+
if (result.error) return { ok: false, output: result.error.message };
|
|
19381
|
+
return { ok: result.status === 0, output };
|
|
19382
|
+
}
|
|
19383
|
+
function requirePm2(args, action) {
|
|
19384
|
+
const result = pm2Exec(args);
|
|
19385
|
+
if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
|
|
19386
|
+
return result.output;
|
|
19387
|
+
}
|
|
19388
|
+
function pm2JsonList() {
|
|
19389
|
+
const output = requirePm2(["jlist"], "pm2 jlist");
|
|
19390
|
+
try {
|
|
19391
|
+
const parsed = JSON.parse(output);
|
|
19392
|
+
if (!Array.isArray(parsed)) throw new Error("result is not an array");
|
|
19393
|
+
return parsed;
|
|
19394
|
+
} catch (error) {
|
|
19395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19396
|
+
throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
|
|
19397
|
+
}
|
|
19398
|
+
}
|
|
19399
|
+
function isGatewayRunning() {
|
|
19400
|
+
if (!isPm2Installed()) return false;
|
|
19401
|
+
const proc = pm2JsonList().find((item) => item.name === PROCESS_NAME);
|
|
19402
|
+
return proc?.pm2_env?.status === "online" && typeof proc.pid === "number" && isGatewayReady(proc.pid);
|
|
19403
|
+
}
|
|
19404
|
+
function databasePath() {
|
|
19405
|
+
return process.env.SUPERTASK_DB_PATH ?? join3(homedir3(), ".local/share/opencode/tasks.db");
|
|
19406
|
+
}
|
|
19407
|
+
function isGatewayReady(expectedPid) {
|
|
19408
|
+
const path = databasePath();
|
|
19409
|
+
if (!existsSync3(path)) return false;
|
|
19410
|
+
let database = null;
|
|
19411
|
+
try {
|
|
19412
|
+
database = new Database3(path, { readonly: true });
|
|
19413
|
+
const row = database.query(
|
|
19414
|
+
"SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
|
|
19415
|
+
).get();
|
|
19416
|
+
if (!row || row.ready_at == null) return false;
|
|
19417
|
+
if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
|
|
19418
|
+
const ageMs = Date.now() - row.heartbeat_at;
|
|
19419
|
+
return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
|
|
19420
|
+
} catch {
|
|
19421
|
+
return false;
|
|
19422
|
+
} finally {
|
|
19423
|
+
database?.close();
|
|
19424
|
+
}
|
|
19425
|
+
}
|
|
19426
|
+
function readyTimeoutMs() {
|
|
19427
|
+
const value = Number(process.env.SUPERTASK_GATEWAY_READY_TIMEOUT_MS ?? 3e4);
|
|
19428
|
+
return Number.isFinite(value) && value > 0 ? value : 3e4;
|
|
19429
|
+
}
|
|
19430
|
+
function waitForGatewayReady(pid) {
|
|
19431
|
+
const deadline = Date.now() + readyTimeoutMs();
|
|
19432
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
19433
|
+
while (Date.now() < deadline) {
|
|
19434
|
+
if (isGatewayReady(pid)) return true;
|
|
19435
|
+
Atomics.wait(sleeper, 0, 0, 100);
|
|
19436
|
+
}
|
|
19437
|
+
return isGatewayReady(pid);
|
|
19438
|
+
}
|
|
19439
|
+
function findBunPath() {
|
|
19440
|
+
const override = process.env.SUPERTASK_BUN_BIN;
|
|
19441
|
+
if (override) return override;
|
|
19442
|
+
try {
|
|
19443
|
+
const command = process.platform === "win32" ? "where bun" : "which bun";
|
|
19444
|
+
return execSync(command, { stdio: "pipe" }).toString().trim().split("\n")[0];
|
|
19445
|
+
} catch {
|
|
19446
|
+
return process.execPath;
|
|
19447
|
+
}
|
|
19448
|
+
}
|
|
19449
|
+
function pm2StartGateway(gatewayEntry = resolveGatewayEntry()) {
|
|
19450
|
+
const configuredKillTimeout = Number(process.env.SUPERTASK_PM2_KILL_TIMEOUT_MS);
|
|
19451
|
+
const killTimeoutMs = Number.isInteger(configuredKillTimeout) && configuredKillTimeout >= 5e3 ? configuredKillTimeout : loadConfig().worker.shutdownGracePeriodMs + 5e3;
|
|
19452
|
+
requirePm2([
|
|
19453
|
+
"start",
|
|
19454
|
+
findBunPath(),
|
|
19455
|
+
"--name",
|
|
19456
|
+
PROCESS_NAME,
|
|
19457
|
+
"--interpreter",
|
|
19458
|
+
"none",
|
|
19459
|
+
"--restart-delay",
|
|
19460
|
+
"5000",
|
|
19461
|
+
"--max-restarts",
|
|
19462
|
+
"30",
|
|
19463
|
+
"--kill-timeout",
|
|
19464
|
+
String(killTimeoutMs),
|
|
19465
|
+
"--",
|
|
19466
|
+
gatewayEntry
|
|
19467
|
+
], "pm2 start");
|
|
19468
|
+
const started = pm2JsonList().find((item) => item.name === PROCESS_NAME);
|
|
19469
|
+
if (started?.pm2_env?.status !== "online") {
|
|
19470
|
+
throw new Error(`[supertask] Gateway did not become online (status: ${started?.pm2_env?.status ?? "missing"})`);
|
|
19471
|
+
}
|
|
19472
|
+
if (typeof started.pid !== "number" || !waitForGatewayReady(started.pid)) {
|
|
19473
|
+
throw new Error("[supertask] Gateway \u8FDB\u7A0B online\uFF0C\u4F46\u672A\u5728\u9650\u5B9A\u65F6\u95F4\u5185\u5C31\u7EEA\uFF1B\u8BF7\u67E5\u770B pm2 logs supertask-gateway");
|
|
19474
|
+
}
|
|
19475
|
+
}
|
|
19476
|
+
function savePm2State() {
|
|
19477
|
+
requirePm2(["save"], "pm2 save");
|
|
19478
|
+
}
|
|
19479
|
+
function install() {
|
|
19480
|
+
if (!isPm2Installed() && !installPm2()) {
|
|
19481
|
+
throw new Error("[supertask] Failed to install pm2. Please install it manually: npm install -g pm2");
|
|
19482
|
+
}
|
|
19483
|
+
const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
|
|
19484
|
+
if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete existing Gateway");
|
|
19485
|
+
const version2 = getPackageVersion();
|
|
19486
|
+
pm2StartGateway();
|
|
19487
|
+
writeRunningVersion(version2);
|
|
19488
|
+
savePm2State();
|
|
19489
|
+
const startup = pm2Exec(["startup"]);
|
|
19490
|
+
if (startup.output) console.log(startup.output);
|
|
19491
|
+
if (!startup.ok) {
|
|
19492
|
+
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");
|
|
19493
|
+
}
|
|
19494
|
+
console.log("[supertask] Gateway installed and running.");
|
|
19495
|
+
console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
|
|
19496
|
+
}
|
|
19497
|
+
function uninstall() {
|
|
19498
|
+
if (!isPm2Installed()) throw new Error("[supertask] pm2 is not installed");
|
|
19499
|
+
const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
|
|
19500
|
+
if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete Gateway");
|
|
19501
|
+
savePm2State();
|
|
19502
|
+
console.log("[supertask] Gateway removed from pm2. Other pm2 startup entries were preserved.");
|
|
19503
|
+
}
|
|
19504
|
+
function upgrade(target) {
|
|
19505
|
+
if (!isPm2Installed()) {
|
|
19506
|
+
throw new Error("[supertask] pm2 is not installed. Run `supertask install` first.");
|
|
19507
|
+
}
|
|
19508
|
+
const before = getRunningVersion();
|
|
19509
|
+
const oldGatewayEntry = resolveGatewayEntry();
|
|
19510
|
+
const currentVersion = target?.version ?? getPackageVersion();
|
|
19511
|
+
const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
|
|
19512
|
+
if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete old Gateway");
|
|
19513
|
+
try {
|
|
19514
|
+
pm2StartGateway(target?.gatewayEntry ?? oldGatewayEntry);
|
|
19515
|
+
writeRunningVersion(currentVersion);
|
|
19516
|
+
savePm2State();
|
|
19517
|
+
return { before, after: currentVersion, restarted: true };
|
|
19518
|
+
} catch (error) {
|
|
19519
|
+
const failed = pm2JsonList().find((item) => item.name === PROCESS_NAME);
|
|
19520
|
+
if (failed) requirePm2(["delete", PROCESS_NAME], "pm2 delete failed Gateway");
|
|
19521
|
+
try {
|
|
19522
|
+
pm2StartGateway(oldGatewayEntry);
|
|
19523
|
+
if (before) writeRunningVersion(before);
|
|
19524
|
+
savePm2State();
|
|
19525
|
+
} catch (rollbackError) {
|
|
19526
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
19527
|
+
const rollback = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
19528
|
+
throw new Error(`${original}; \u65E7 Gateway \u56DE\u6EDA\u4E5F\u5931\u8D25: ${rollback}`);
|
|
19529
|
+
}
|
|
19530
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19531
|
+
throw new Error(`${message}; \u5DF2\u56DE\u6EDA\u5230\u65E7 Gateway`);
|
|
19532
|
+
}
|
|
19533
|
+
}
|
|
19534
|
+
function ensureGateway() {
|
|
19535
|
+
if (!isPm2Installed()) {
|
|
19536
|
+
return { ok: false, reason: "pm2-not-installed" };
|
|
19537
|
+
}
|
|
19538
|
+
const currentVersion = getPackageVersion();
|
|
19539
|
+
const processList = pm2JsonList();
|
|
19540
|
+
const existing = processList.find((item) => item.name === PROCESS_NAME);
|
|
19541
|
+
if (existing?.pm2_env?.status === "online" && typeof existing.pid === "number" && isGatewayReady(existing.pid) && getRunningVersion() === currentVersion) {
|
|
19542
|
+
return { ok: true, action: "already-running" };
|
|
19543
|
+
}
|
|
19544
|
+
if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete stale Gateway");
|
|
19545
|
+
pm2StartGateway();
|
|
19546
|
+
writeRunningVersion(currentVersion);
|
|
19547
|
+
savePm2State();
|
|
19548
|
+
return { ok: true, action: existing ? "restarted" : "started" };
|
|
19549
|
+
}
|
|
19550
|
+
var __dirname, PROCESS_NAME, GATEWAY_LOCK_STALE_MS;
|
|
19551
|
+
var init_pm2 = __esm({
|
|
19552
|
+
"src/daemon/pm2.ts"() {
|
|
19553
|
+
"use strict";
|
|
19554
|
+
init_config();
|
|
19555
|
+
__dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
19556
|
+
PROCESS_NAME = "supertask-gateway";
|
|
19557
|
+
GATEWAY_LOCK_STALE_MS = 3e4;
|
|
19122
19558
|
}
|
|
19123
19559
|
});
|
|
19124
19560
|
|
|
19125
19561
|
// src/core/services/task-run.service.ts
|
|
19126
|
-
var
|
|
19562
|
+
var taskRuns3, TaskRunService;
|
|
19127
19563
|
var init_task_run_service = __esm({
|
|
19128
19564
|
"src/core/services/task-run.service.ts"() {
|
|
19129
19565
|
"use strict";
|
|
19130
19566
|
init_db2();
|
|
19131
19567
|
init_drizzle_orm();
|
|
19132
|
-
({ taskRuns:
|
|
19568
|
+
({ taskRuns: taskRuns3 } = schema_exports);
|
|
19133
19569
|
TaskRunService = class {
|
|
19134
19570
|
static async create(data) {
|
|
19135
|
-
const result = await db.insert(
|
|
19571
|
+
const result = await db.insert(taskRuns3).values(data).returning();
|
|
19136
19572
|
return result[0];
|
|
19137
19573
|
}
|
|
19138
19574
|
static async updateSessionId(id, sessionId) {
|
|
19139
|
-
const result = await db.update(
|
|
19575
|
+
const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
19140
19576
|
return result[0] || null;
|
|
19141
19577
|
}
|
|
19142
19578
|
static async done(id, log) {
|
|
19143
|
-
const result = await db.update(
|
|
19579
|
+
const result = await db.update(taskRuns3).set({
|
|
19144
19580
|
status: "done",
|
|
19145
19581
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
19146
19582
|
log
|
|
19147
|
-
}).where(eq(
|
|
19583
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
19148
19584
|
return result[0] || null;
|
|
19149
19585
|
}
|
|
19150
19586
|
static async fail(id, log) {
|
|
19151
|
-
const result = await db.update(
|
|
19587
|
+
const result = await db.update(taskRuns3).set({
|
|
19152
19588
|
status: "failed",
|
|
19153
19589
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
19154
19590
|
log
|
|
19155
|
-
}).where(eq(
|
|
19591
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
19156
19592
|
return result[0] || null;
|
|
19157
19593
|
}
|
|
19158
19594
|
static async heartbeat(id) {
|
|
19159
|
-
const result = await db.update(
|
|
19595
|
+
const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
19160
19596
|
return result[0] || null;
|
|
19161
19597
|
}
|
|
19162
19598
|
static async updatePid(id, workerPid, childPid) {
|
|
19163
|
-
const result = await db.update(
|
|
19599
|
+
const result = await db.update(taskRuns3).set({
|
|
19164
19600
|
workerPid,
|
|
19165
19601
|
childPid,
|
|
19166
19602
|
lockedAt: Date.now(),
|
|
19167
19603
|
lockedBy: `gateway-${process.pid}`
|
|
19168
|
-
}).where(eq(
|
|
19604
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
19169
19605
|
return result[0] || null;
|
|
19170
19606
|
}
|
|
19171
19607
|
static async getById(id) {
|
|
19172
|
-
const result = await db.select().from(
|
|
19608
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
|
|
19173
19609
|
return result[0] || null;
|
|
19174
19610
|
}
|
|
19175
19611
|
static async listByTaskId(taskId) {
|
|
19176
|
-
return await db.select().from(
|
|
19612
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
19177
19613
|
}
|
|
19178
19614
|
static async getLatestByTaskId(taskId) {
|
|
19179
|
-
const result = await db.select().from(
|
|
19615
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
|
|
19180
19616
|
return result[0] || null;
|
|
19181
19617
|
}
|
|
19182
19618
|
static async getLatestByTaskIds(taskIds) {
|
|
19183
19619
|
if (taskIds.length === 0) return /* @__PURE__ */ new Map();
|
|
19184
|
-
const latestRuns = await db.select().from(
|
|
19620
|
+
const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
19185
19621
|
const result = /* @__PURE__ */ new Map();
|
|
19186
19622
|
for (const run of latestRuns) {
|
|
19187
19623
|
if (!result.has(run.taskId)) {
|
|
@@ -19195,22 +19631,23 @@ var init_task_run_service = __esm({
|
|
|
19195
19631
|
const cutoffSec = Math.floor(cutoffMs / 1e3);
|
|
19196
19632
|
const { tasks: tasksTable } = schema_exports;
|
|
19197
19633
|
const result = await db.select({
|
|
19198
|
-
runId:
|
|
19199
|
-
taskId:
|
|
19200
|
-
childPid:
|
|
19634
|
+
runId: taskRuns3.id,
|
|
19635
|
+
taskId: taskRuns3.taskId,
|
|
19636
|
+
childPid: taskRuns3.childPid,
|
|
19201
19637
|
taskRetryCount: tasksTable.retryCount,
|
|
19202
|
-
taskMaxRetries: tasksTable.maxRetries
|
|
19203
|
-
|
|
19638
|
+
taskMaxRetries: tasksTable.maxRetries,
|
|
19639
|
+
taskRetryBackoffMs: tasksTable.retryBackoffMs
|
|
19640
|
+
}).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
|
|
19204
19641
|
and(
|
|
19205
|
-
eq(
|
|
19642
|
+
eq(taskRuns3.status, "running"),
|
|
19206
19643
|
or(
|
|
19207
19644
|
and(
|
|
19208
|
-
sql`${
|
|
19209
|
-
sql`${
|
|
19645
|
+
sql`${taskRuns3.heartbeatAt} IS NULL`,
|
|
19646
|
+
sql`${taskRuns3.startedAt} < ${cutoffSec}`
|
|
19210
19647
|
),
|
|
19211
19648
|
and(
|
|
19212
|
-
sql`${
|
|
19213
|
-
sql`${
|
|
19649
|
+
sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
|
|
19650
|
+
sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
|
|
19214
19651
|
)
|
|
19215
19652
|
)
|
|
19216
19653
|
)
|
|
@@ -19220,65 +19657,234 @@ var init_task_run_service = __esm({
|
|
|
19220
19657
|
taskId: row.taskId,
|
|
19221
19658
|
childPid: row.childPid,
|
|
19222
19659
|
taskRetryCount: row.taskRetryCount ?? 0,
|
|
19223
|
-
taskMaxRetries: row.taskMaxRetries ?? 3
|
|
19660
|
+
taskMaxRetries: row.taskMaxRetries ?? 3,
|
|
19661
|
+
taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
|
|
19224
19662
|
}));
|
|
19225
19663
|
}
|
|
19226
19664
|
static async getRunningRunByTaskId(taskId) {
|
|
19227
|
-
const result = await db.select().from(
|
|
19665
|
+
const result = await db.select().from(taskRuns3).where(and(eq(taskRuns3.taskId, taskId), eq(taskRuns3.status, "running"))).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
|
|
19228
19666
|
return result[0] || null;
|
|
19229
19667
|
}
|
|
19230
19668
|
static async deleteByTaskIds(taskIds) {
|
|
19231
19669
|
if (taskIds.length === 0) return 0;
|
|
19232
|
-
const result = await db.delete(
|
|
19670
|
+
const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
|
|
19233
19671
|
return result.length;
|
|
19234
19672
|
}
|
|
19235
19673
|
static async getAllRunningRuns() {
|
|
19236
|
-
return await db.select().from(
|
|
19674
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
|
|
19237
19675
|
}
|
|
19238
19676
|
};
|
|
19239
19677
|
}
|
|
19240
19678
|
});
|
|
19241
19679
|
|
|
19680
|
+
// src/gateway/health.ts
|
|
19681
|
+
function initializeGatewayHealth(config) {
|
|
19682
|
+
const now = Date.now();
|
|
19683
|
+
state = {
|
|
19684
|
+
startedAt: now,
|
|
19685
|
+
config,
|
|
19686
|
+
lastActivityAt: {
|
|
19687
|
+
worker: now,
|
|
19688
|
+
scheduler: now,
|
|
19689
|
+
watchdog: now
|
|
19690
|
+
}
|
|
19691
|
+
};
|
|
19692
|
+
}
|
|
19693
|
+
function markGatewayActivity(component) {
|
|
19694
|
+
if (state) state.lastActivityAt[component] = Date.now();
|
|
19695
|
+
}
|
|
19696
|
+
function resetGatewayHealth() {
|
|
19697
|
+
state = null;
|
|
19698
|
+
}
|
|
19699
|
+
function componentStatus(component, enabled, maxAgeMs, now) {
|
|
19700
|
+
const lastActivityAt = state?.lastActivityAt[component] ?? null;
|
|
19701
|
+
const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
|
|
19702
|
+
return {
|
|
19703
|
+
enabled,
|
|
19704
|
+
healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
|
|
19705
|
+
lastActivityAt,
|
|
19706
|
+
ageMs,
|
|
19707
|
+
maxAgeMs
|
|
19708
|
+
};
|
|
19709
|
+
}
|
|
19710
|
+
function getGatewayHealth(now = Date.now()) {
|
|
19711
|
+
const worker = componentStatus(
|
|
19712
|
+
"worker",
|
|
19713
|
+
true,
|
|
19714
|
+
Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
|
|
19715
|
+
now
|
|
19716
|
+
);
|
|
19717
|
+
const scheduler = componentStatus(
|
|
19718
|
+
"scheduler",
|
|
19719
|
+
state?.config.schedulerEnabled ?? false,
|
|
19720
|
+
Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
|
|
19721
|
+
now
|
|
19722
|
+
);
|
|
19723
|
+
const watchdog = componentStatus(
|
|
19724
|
+
"watchdog",
|
|
19725
|
+
true,
|
|
19726
|
+
Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
|
|
19727
|
+
now
|
|
19728
|
+
);
|
|
19729
|
+
let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
|
|
19730
|
+
try {
|
|
19731
|
+
const row = sqlite.prepare(
|
|
19732
|
+
"SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
|
|
19733
|
+
).get();
|
|
19734
|
+
if (row) {
|
|
19735
|
+
const ageMs = Math.max(0, now - row.heartbeat_at);
|
|
19736
|
+
lock = {
|
|
19737
|
+
pid: row.pid,
|
|
19738
|
+
heartbeatAt: row.heartbeat_at,
|
|
19739
|
+
readyAt: row.ready_at,
|
|
19740
|
+
ageMs,
|
|
19741
|
+
healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
|
|
19742
|
+
};
|
|
19743
|
+
}
|
|
19744
|
+
} catch {
|
|
19745
|
+
}
|
|
19746
|
+
const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
|
|
19747
|
+
return {
|
|
19748
|
+
status: healthy ? "ok" : "degraded",
|
|
19749
|
+
pid: process.pid,
|
|
19750
|
+
uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
|
|
19751
|
+
lock,
|
|
19752
|
+
components: { worker, scheduler, watchdog }
|
|
19753
|
+
};
|
|
19754
|
+
}
|
|
19755
|
+
var LOCK_STALE_MS, state;
|
|
19756
|
+
var init_health = __esm({
|
|
19757
|
+
"src/gateway/health.ts"() {
|
|
19758
|
+
"use strict";
|
|
19759
|
+
init_db2();
|
|
19760
|
+
LOCK_STALE_MS = 3e4;
|
|
19761
|
+
state = null;
|
|
19762
|
+
}
|
|
19763
|
+
});
|
|
19764
|
+
|
|
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
|
+
|
|
19242
19830
|
// src/worker/index.ts
|
|
19243
19831
|
import { spawn } from "child_process";
|
|
19244
|
-
var WorkerEngine;
|
|
19832
|
+
var DEFAULT_MAX_OUTPUT_CHARS, FORBIDDEN_AGENT, WorkerEngine;
|
|
19245
19833
|
var init_worker = __esm({
|
|
19246
19834
|
"src/worker/index.ts"() {
|
|
19247
19835
|
"use strict";
|
|
19248
19836
|
init_task_service();
|
|
19249
19837
|
init_task_run_service();
|
|
19838
|
+
init_health();
|
|
19839
|
+
init_process_control();
|
|
19840
|
+
DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
|
|
19841
|
+
FORBIDDEN_AGENT = "supertask-runner";
|
|
19250
19842
|
WorkerEngine = class {
|
|
19251
19843
|
activeBatchIds = /* @__PURE__ */ new Set();
|
|
19252
19844
|
runningTasks = /* @__PURE__ */ new Map();
|
|
19253
19845
|
stopped = false;
|
|
19254
19846
|
pollTimer = null;
|
|
19255
19847
|
heartbeatTimer = null;
|
|
19848
|
+
pollCyclePromise = null;
|
|
19256
19849
|
cfg;
|
|
19257
|
-
|
|
19850
|
+
opencodeBin;
|
|
19851
|
+
maxOutputChars;
|
|
19852
|
+
constructor(cfg, options = {}) {
|
|
19258
19853
|
this.cfg = cfg.worker;
|
|
19854
|
+
this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
19855
|
+
this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
19259
19856
|
}
|
|
19260
19857
|
start() {
|
|
19261
19858
|
this.stopped = false;
|
|
19859
|
+
markGatewayActivity("worker");
|
|
19262
19860
|
this.poll();
|
|
19263
19861
|
this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
|
|
19264
19862
|
}
|
|
19265
|
-
stop() {
|
|
19863
|
+
async stop(gracePeriodMs = 0) {
|
|
19266
19864
|
this.stopped = true;
|
|
19267
19865
|
if (this.pollTimer) {
|
|
19268
19866
|
clearTimeout(this.pollTimer);
|
|
19269
19867
|
this.pollTimer = null;
|
|
19270
19868
|
}
|
|
19869
|
+
if (this.pollCyclePromise) await this.pollCyclePromise;
|
|
19870
|
+
if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
|
|
19871
|
+
const deadline = Date.now() + gracePeriodMs;
|
|
19872
|
+
while (this.runningTasks.size > 0 && Date.now() < deadline) {
|
|
19873
|
+
await Bun.sleep(Math.min(50, deadline - Date.now()));
|
|
19874
|
+
}
|
|
19875
|
+
}
|
|
19271
19876
|
if (this.heartbeatTimer) {
|
|
19272
19877
|
clearInterval(this.heartbeatTimer);
|
|
19273
19878
|
this.heartbeatTimer = null;
|
|
19274
19879
|
}
|
|
19880
|
+
const interruptedTaskIds = [...this.runningTasks.keys()];
|
|
19275
19881
|
const killPromises = [];
|
|
19276
|
-
for (const
|
|
19882
|
+
for (const entry of this.runningTasks.values()) {
|
|
19277
19883
|
entry.shutdown = true;
|
|
19278
19884
|
killPromises.push(this.killEntry(entry));
|
|
19279
19885
|
}
|
|
19280
|
-
|
|
19281
|
-
|
|
19886
|
+
await Promise.allSettled(killPromises);
|
|
19887
|
+
return interruptedTaskIds;
|
|
19282
19888
|
}
|
|
19283
19889
|
getRunningTaskIds() {
|
|
19284
19890
|
return [...this.runningTasks.keys()];
|
|
@@ -19288,136 +19894,244 @@ var init_worker = __esm({
|
|
|
19288
19894
|
}
|
|
19289
19895
|
poll() {
|
|
19290
19896
|
if (this.stopped) return;
|
|
19291
|
-
|
|
19897
|
+
markGatewayActivity("worker");
|
|
19898
|
+
this.pollCyclePromise = this.tryDispatch().finally(() => {
|
|
19899
|
+
this.pollCyclePromise = null;
|
|
19292
19900
|
if (this.stopped) return;
|
|
19293
19901
|
this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
|
|
19294
19902
|
});
|
|
19295
19903
|
}
|
|
19296
19904
|
async tryDispatch() {
|
|
19905
|
+
await this.reconcileCancelledTasks();
|
|
19297
19906
|
while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
|
|
19907
|
+
let task;
|
|
19908
|
+
try {
|
|
19909
|
+
task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
|
|
19910
|
+
} catch (err) {
|
|
19911
|
+
this.logError("task claim failed", err);
|
|
19912
|
+
break;
|
|
19913
|
+
}
|
|
19914
|
+
if (!task) break;
|
|
19915
|
+
if (this.stopped) break;
|
|
19916
|
+
if (!await TaskService.start(task.id)) continue;
|
|
19917
|
+
if (task.batchId) this.activeBatchIds.add(task.batchId);
|
|
19918
|
+
if (this.stopped) {
|
|
19919
|
+
await TaskService.resetRunningToPending([task.id]);
|
|
19920
|
+
this.releaseBatch(task);
|
|
19921
|
+
break;
|
|
19922
|
+
}
|
|
19298
19923
|
try {
|
|
19299
|
-
const excludedBatchIds = [...this.activeBatchIds];
|
|
19300
|
-
const task = await TaskService.next({ excludedBatchIds });
|
|
19301
|
-
if (!task) break;
|
|
19302
|
-
if (!await TaskService.start(task.id)) continue;
|
|
19303
|
-
if (task.batchId) {
|
|
19304
|
-
this.activeBatchIds.add(task.batchId);
|
|
19305
|
-
}
|
|
19306
19924
|
const run = await TaskRunService.create({
|
|
19307
19925
|
taskId: task.id,
|
|
19308
19926
|
model: this.resolveModel(task.model),
|
|
19309
19927
|
status: "running"
|
|
19310
19928
|
});
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19929
|
+
if (this.stopped) {
|
|
19930
|
+
await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
|
|
19931
|
+
await TaskService.resetRunningToPending([task.id]);
|
|
19932
|
+
this.releaseBatch(task);
|
|
19933
|
+
break;
|
|
19315
19934
|
}
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
19321
|
-
|
|
19322
|
-
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19328
|
-
|
|
19329
|
-
|
|
19330
|
-
|
|
19331
|
-
}
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
|
|
19338
|
-
|
|
19339
|
-
|
|
19340
|
-
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19364
|
-
|
|
19365
|
-
|
|
19366
|
-
}
|
|
19367
|
-
console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
|
|
19935
|
+
if (task.agent === FORBIDDEN_AGENT) {
|
|
19936
|
+
const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
|
|
19937
|
+
await TaskRunService.fail(run.id, message);
|
|
19938
|
+
await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
|
|
19939
|
+
this.releaseBatch(task);
|
|
19940
|
+
continue;
|
|
19941
|
+
}
|
|
19942
|
+
this.spawnTask(task, run.id);
|
|
19943
|
+
} catch (err) {
|
|
19944
|
+
this.releaseBatch(task);
|
|
19945
|
+
const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
|
|
19946
|
+
try {
|
|
19947
|
+
await TaskService.fail(task.id, message);
|
|
19948
|
+
} catch (failErr) {
|
|
19949
|
+
this.logError("failed to compensate task startup", failErr, task.id);
|
|
19950
|
+
}
|
|
19951
|
+
this.logError("task dispatch failed", err, task.id);
|
|
19952
|
+
}
|
|
19953
|
+
}
|
|
19954
|
+
}
|
|
19955
|
+
spawnTask(task, runId) {
|
|
19956
|
+
const model = this.resolveModel(task.model);
|
|
19957
|
+
const args = ["run", "--agent", task.agent, "--format", "json"];
|
|
19958
|
+
if (model) args.push("-m", model);
|
|
19959
|
+
args.push(task.prompt);
|
|
19960
|
+
const child = spawn(this.opencodeBin, args, {
|
|
19961
|
+
cwd: task.cwd || process.cwd(),
|
|
19962
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19963
|
+
detached: process.platform !== "win32"
|
|
19964
|
+
});
|
|
19965
|
+
const entry = {
|
|
19966
|
+
task,
|
|
19967
|
+
runId,
|
|
19968
|
+
child,
|
|
19969
|
+
output: "",
|
|
19970
|
+
sessionId: null,
|
|
19971
|
+
timeoutTimer: null,
|
|
19972
|
+
shutdown: false,
|
|
19973
|
+
settled: false
|
|
19974
|
+
};
|
|
19975
|
+
this.runningTasks.set(task.id, entry);
|
|
19976
|
+
const handleData = (data) => {
|
|
19977
|
+
const text2 = data.toString();
|
|
19978
|
+
entry.output = (entry.output + text2).slice(-this.maxOutputChars);
|
|
19979
|
+
process.stdout.write(text2);
|
|
19980
|
+
const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
|
|
19981
|
+
if (match2?.[1] && match2[1] !== entry.sessionId) {
|
|
19982
|
+
entry.sessionId = match2[1];
|
|
19983
|
+
TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
|
|
19984
|
+
this.logError("sessionId update failed", err, task.id);
|
|
19368
19985
|
});
|
|
19986
|
+
}
|
|
19987
|
+
};
|
|
19988
|
+
child.stdout?.on("data", handleData);
|
|
19989
|
+
child.stderr?.on("data", handleData);
|
|
19990
|
+
child.once("error", (err) => {
|
|
19991
|
+
void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
|
|
19992
|
+
});
|
|
19993
|
+
child.once("close", (code, signal) => {
|
|
19994
|
+
const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
|
|
19995
|
+
void this.finishEntry(entry, code, failure);
|
|
19996
|
+
});
|
|
19997
|
+
const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
|
|
19998
|
+
if (timeoutMs > 0) {
|
|
19999
|
+
entry.timeoutTimer = setTimeout(() => {
|
|
20000
|
+
this.signalEntry(entry, "SIGKILL");
|
|
20001
|
+
void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
|
|
20002
|
+
}, timeoutMs);
|
|
20003
|
+
}
|
|
20004
|
+
TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
|
|
20005
|
+
this.signalEntry(entry, "SIGKILL");
|
|
20006
|
+
void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
20007
|
+
});
|
|
20008
|
+
}
|
|
20009
|
+
async finishEntry(entry, code, failure) {
|
|
20010
|
+
if (entry.settled) return;
|
|
20011
|
+
entry.settled = true;
|
|
20012
|
+
if (entry.timeoutTimer) {
|
|
20013
|
+
clearTimeout(entry.timeoutTimer);
|
|
20014
|
+
entry.timeoutTimer = null;
|
|
20015
|
+
}
|
|
20016
|
+
try {
|
|
20017
|
+
if (entry.shutdown) return;
|
|
20018
|
+
const currentRun = await TaskRunService.getById(entry.runId);
|
|
20019
|
+
if (!currentRun || currentRun.status !== "running") return;
|
|
20020
|
+
const output = entry.output.trim();
|
|
20021
|
+
const log = failure ? `${failure}${output ? `
|
|
20022
|
+
${output}` : ""}` : output;
|
|
20023
|
+
if (code === 0 && !failure) {
|
|
20024
|
+
const completed = await TaskService.done(entry.task.id, log);
|
|
20025
|
+
if (completed) {
|
|
20026
|
+
await TaskRunService.done(entry.runId, log);
|
|
20027
|
+
console.log(JSON.stringify({
|
|
20028
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20029
|
+
level: "info",
|
|
20030
|
+
msg: "task done",
|
|
20031
|
+
taskId: entry.task.id
|
|
20032
|
+
}));
|
|
20033
|
+
return;
|
|
20034
|
+
}
|
|
20035
|
+
await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
|
|
20036
|
+
return;
|
|
20037
|
+
}
|
|
20038
|
+
await TaskRunService.fail(entry.runId, log);
|
|
20039
|
+
const failed = await TaskService.fail(entry.task.id, log);
|
|
20040
|
+
if (!failed) {
|
|
20041
|
+
this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
|
|
20042
|
+
}
|
|
20043
|
+
console.error(JSON.stringify({
|
|
20044
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20045
|
+
level: "error",
|
|
20046
|
+
msg: "task failed",
|
|
20047
|
+
taskId: entry.task.id,
|
|
20048
|
+
error: failure
|
|
20049
|
+
}));
|
|
20050
|
+
} finally {
|
|
20051
|
+
this.runningTasks.delete(entry.task.id);
|
|
20052
|
+
this.releaseBatch(entry.task);
|
|
20053
|
+
}
|
|
20054
|
+
}
|
|
20055
|
+
async reconcileCancelledTasks() {
|
|
20056
|
+
for (const entry of [...this.runningTasks.values()]) {
|
|
20057
|
+
try {
|
|
20058
|
+
const task = await TaskService.getById(entry.task.id);
|
|
20059
|
+
if (task?.status === "cancelled") await this.cancelEntry(entry);
|
|
19369
20060
|
} catch (err) {
|
|
19370
|
-
|
|
19371
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19372
|
-
level: "error",
|
|
19373
|
-
msg: "tryDispatch iteration failed",
|
|
19374
|
-
error: err instanceof Error ? err.message : String(err)
|
|
19375
|
-
}));
|
|
19376
|
-
break;
|
|
20061
|
+
this.logError("cancel reconciliation failed", err, entry.task.id);
|
|
19377
20062
|
}
|
|
19378
20063
|
}
|
|
19379
20064
|
}
|
|
20065
|
+
async cancelEntry(entry) {
|
|
20066
|
+
if (entry.settled) return;
|
|
20067
|
+
entry.settled = true;
|
|
20068
|
+
if (entry.timeoutTimer) {
|
|
20069
|
+
clearTimeout(entry.timeoutTimer);
|
|
20070
|
+
entry.timeoutTimer = null;
|
|
20071
|
+
}
|
|
20072
|
+
try {
|
|
20073
|
+
await this.killEntry(entry);
|
|
20074
|
+
const output = entry.output.trim();
|
|
20075
|
+
const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
|
|
20076
|
+
${output}` : ""}`;
|
|
20077
|
+
await TaskRunService.fail(entry.runId, log);
|
|
20078
|
+
console.log(JSON.stringify({
|
|
20079
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20080
|
+
level: "info",
|
|
20081
|
+
msg: "running task cancelled",
|
|
20082
|
+
taskId: entry.task.id
|
|
20083
|
+
}));
|
|
20084
|
+
} finally {
|
|
20085
|
+
this.runningTasks.delete(entry.task.id);
|
|
20086
|
+
this.releaseBatch(entry.task);
|
|
20087
|
+
}
|
|
20088
|
+
}
|
|
19380
20089
|
async updateHeartbeats() {
|
|
19381
|
-
for (const
|
|
20090
|
+
for (const entry of this.runningTasks.values()) {
|
|
19382
20091
|
try {
|
|
19383
20092
|
await TaskRunService.heartbeat(entry.runId);
|
|
19384
|
-
} catch {
|
|
20093
|
+
} catch (err) {
|
|
20094
|
+
this.logError("heartbeat update failed", err, entry.task.id);
|
|
19385
20095
|
}
|
|
19386
20096
|
}
|
|
19387
20097
|
}
|
|
19388
20098
|
killEntry(entry) {
|
|
19389
|
-
if (entry.child.exitCode !== null) {
|
|
20099
|
+
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
19390
20100
|
return Promise.resolve();
|
|
19391
20101
|
}
|
|
19392
20102
|
return new Promise((resolve) => {
|
|
19393
20103
|
const timeout = setTimeout(() => {
|
|
19394
|
-
|
|
19395
|
-
if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
|
|
19396
|
-
} catch {
|
|
19397
|
-
}
|
|
20104
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19398
20105
|
resolve();
|
|
19399
20106
|
}, 5e3);
|
|
19400
|
-
entry.child.
|
|
20107
|
+
entry.child.once("close", () => {
|
|
19401
20108
|
clearTimeout(timeout);
|
|
19402
20109
|
resolve();
|
|
19403
20110
|
});
|
|
19404
|
-
|
|
19405
|
-
if (entry.child.pid) {
|
|
19406
|
-
entry.child.kill("SIGTERM");
|
|
19407
|
-
} else {
|
|
19408
|
-
clearTimeout(timeout);
|
|
19409
|
-
resolve();
|
|
19410
|
-
}
|
|
19411
|
-
} catch {
|
|
19412
|
-
clearTimeout(timeout);
|
|
19413
|
-
resolve();
|
|
19414
|
-
}
|
|
20111
|
+
this.signalEntry(entry, "SIGTERM");
|
|
19415
20112
|
});
|
|
19416
20113
|
}
|
|
20114
|
+
signalEntry(entry, signal) {
|
|
20115
|
+
const pid = entry.child.pid;
|
|
20116
|
+
if (!pid) return;
|
|
20117
|
+
signalSpawnedProcessTree(pid, signal);
|
|
20118
|
+
}
|
|
20119
|
+
releaseBatch(task) {
|
|
20120
|
+
if (task.batchId) this.activeBatchIds.delete(task.batchId);
|
|
20121
|
+
}
|
|
19417
20122
|
resolveModel(taskModel) {
|
|
19418
20123
|
if (!taskModel || taskModel === "default") return null;
|
|
19419
20124
|
return taskModel;
|
|
19420
20125
|
}
|
|
20126
|
+
logError(message, error, taskId) {
|
|
20127
|
+
console.error(JSON.stringify({
|
|
20128
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20129
|
+
level: "error",
|
|
20130
|
+
msg: message,
|
|
20131
|
+
taskId,
|
|
20132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20133
|
+
}));
|
|
20134
|
+
}
|
|
19421
20135
|
};
|
|
19422
20136
|
}
|
|
19423
20137
|
});
|
|
@@ -19429,15 +20143,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19429
20143
|
for (const run of staleRuns) {
|
|
19430
20144
|
try {
|
|
19431
20145
|
if (run.childPid != null && run.childPid > 0) {
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
20146
|
+
const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
20147
|
+
const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
|
|
20148
|
+
if (!killed) {
|
|
20149
|
+
console.warn(JSON.stringify({
|
|
20150
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20151
|
+
level: "warn",
|
|
20152
|
+
msg: "stale child process was not killed because identity validation failed",
|
|
20153
|
+
taskId: run.taskId,
|
|
20154
|
+
runId: run.runId,
|
|
20155
|
+
childPid: run.childPid
|
|
20156
|
+
}));
|
|
19435
20157
|
}
|
|
19436
20158
|
}
|
|
19437
20159
|
await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
|
|
19438
20160
|
const newRetryCount = run.taskRetryCount + 1;
|
|
19439
20161
|
const maxRetries = run.taskMaxRetries;
|
|
19440
|
-
if (newRetryCount
|
|
20162
|
+
if (newRetryCount > maxRetries) {
|
|
19441
20163
|
await TaskService.markDeadLetter(run.taskId, newRetryCount);
|
|
19442
20164
|
console.log(JSON.stringify({
|
|
19443
20165
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19449,7 +20171,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19449
20171
|
maxRetries
|
|
19450
20172
|
}));
|
|
19451
20173
|
} else {
|
|
19452
|
-
const backoffMs = computeBackoff(newRetryCount);
|
|
20174
|
+
const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
|
|
19453
20175
|
const retryAfter = Date.now() + backoffMs;
|
|
19454
20176
|
await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
|
|
19455
20177
|
console.log(JSON.stringify({
|
|
@@ -19480,6 +20202,7 @@ var init_heartbeat = __esm({
|
|
|
19480
20202
|
init_task_run_service();
|
|
19481
20203
|
init_task_service();
|
|
19482
20204
|
init_backoff();
|
|
20205
|
+
init_process_control();
|
|
19483
20206
|
}
|
|
19484
20207
|
});
|
|
19485
20208
|
|
|
@@ -19524,6 +20247,7 @@ var init_watchdog = __esm({
|
|
|
19524
20247
|
"use strict";
|
|
19525
20248
|
init_heartbeat();
|
|
19526
20249
|
init_cleanup();
|
|
20250
|
+
init_health();
|
|
19527
20251
|
Watchdog = class {
|
|
19528
20252
|
constructor(cfg) {
|
|
19529
20253
|
this.cfg = cfg;
|
|
@@ -19534,13 +20258,14 @@ var init_watchdog = __esm({
|
|
|
19534
20258
|
cleanupTimer = null;
|
|
19535
20259
|
start() {
|
|
19536
20260
|
this.stopped = false;
|
|
20261
|
+
markGatewayActivity("watchdog");
|
|
19537
20262
|
this.heartbeatTimer = setInterval(
|
|
19538
20263
|
() => this.runHeartbeatCheck(),
|
|
19539
|
-
this.cfg.watchdog.
|
|
20264
|
+
this.cfg.watchdog.checkIntervalMs
|
|
19540
20265
|
);
|
|
19541
20266
|
this.cleanupTimer = setInterval(
|
|
19542
20267
|
() => this.runCleanup(),
|
|
19543
|
-
this.cfg.watchdog.cleanupIntervalMs
|
|
20268
|
+
this.cfg.watchdog.cleanupIntervalMs
|
|
19544
20269
|
);
|
|
19545
20270
|
}
|
|
19546
20271
|
stop() {
|
|
@@ -19556,6 +20281,7 @@ var init_watchdog = __esm({
|
|
|
19556
20281
|
}
|
|
19557
20282
|
async runHeartbeatCheck() {
|
|
19558
20283
|
if (this.stopped) return;
|
|
20284
|
+
markGatewayActivity("watchdog");
|
|
19559
20285
|
try {
|
|
19560
20286
|
await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
|
|
19561
20287
|
} catch (err) {
|
|
@@ -19588,7 +20314,7 @@ var init_watchdog = __esm({
|
|
|
19588
20314
|
async function cloneTaskFromTemplate(templateId) {
|
|
19589
20315
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
19590
20316
|
const tmpl = rows[0];
|
|
19591
|
-
if (!tmpl) return null;
|
|
20317
|
+
if (!tmpl || !tmpl.enabled) return null;
|
|
19592
20318
|
const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
|
|
19593
20319
|
and(
|
|
19594
20320
|
eq(schema_exports.tasks.templateId, templateId),
|
|
@@ -19597,7 +20323,8 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19597
20323
|
).then((r) => r[0].count);
|
|
19598
20324
|
if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
|
|
19599
20325
|
const nowMs = Date.now();
|
|
19600
|
-
const
|
|
20326
|
+
const isDelayed = tmpl.scheduleType === "delayed";
|
|
20327
|
+
const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
|
|
19601
20328
|
tmpl.scheduleType,
|
|
19602
20329
|
tmpl,
|
|
19603
20330
|
nowMs
|
|
@@ -19611,13 +20338,17 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19611
20338
|
category: tmpl.category ?? "general",
|
|
19612
20339
|
importance: tmpl.importance ?? 3,
|
|
19613
20340
|
urgency: tmpl.urgency ?? 3,
|
|
20341
|
+
batchId: tmpl.batchId,
|
|
19614
20342
|
maxRetries: tmpl.maxRetries ?? 3,
|
|
20343
|
+
retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
|
|
20344
|
+
timeoutMs: tmpl.timeoutMs,
|
|
19615
20345
|
templateId: tmpl.id,
|
|
19616
20346
|
scheduledAt: nowMs
|
|
19617
20347
|
});
|
|
19618
20348
|
await db.update(taskTemplates3).set({
|
|
19619
20349
|
lastRunAt: nowMs,
|
|
19620
20350
|
nextRunAt,
|
|
20351
|
+
enabled: isDelayed ? false : tmpl.enabled,
|
|
19621
20352
|
updatedAt: nowMs
|
|
19622
20353
|
}).where(eq(taskTemplates3.id, templateId));
|
|
19623
20354
|
return task;
|
|
@@ -19630,7 +20361,7 @@ async function getDueTemplates() {
|
|
|
19630
20361
|
sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
|
|
19631
20362
|
sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
|
|
19632
20363
|
)
|
|
19633
|
-
);
|
|
20364
|
+
).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
|
|
19634
20365
|
}
|
|
19635
20366
|
async function initializeNextRunAt(templateId) {
|
|
19636
20367
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
@@ -19664,6 +20395,7 @@ var init_scheduler = __esm({
|
|
|
19664
20395
|
init_job_templates();
|
|
19665
20396
|
init_db2();
|
|
19666
20397
|
init_drizzle_orm();
|
|
20398
|
+
init_health();
|
|
19667
20399
|
Scheduler = class {
|
|
19668
20400
|
constructor(cfg) {
|
|
19669
20401
|
this.cfg = cfg;
|
|
@@ -19675,6 +20407,7 @@ var init_scheduler = __esm({
|
|
|
19675
20407
|
async start() {
|
|
19676
20408
|
if (!this.cfg.scheduler.enabled) return;
|
|
19677
20409
|
this.stopped = false;
|
|
20410
|
+
markGatewayActivity("scheduler");
|
|
19678
20411
|
await this.initializeTemplates();
|
|
19679
20412
|
this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
|
|
19680
20413
|
}
|
|
@@ -19687,6 +20420,7 @@ var init_scheduler = __esm({
|
|
|
19687
20420
|
}
|
|
19688
20421
|
async tick() {
|
|
19689
20422
|
if (this.stopped || this.ticking) return;
|
|
20423
|
+
markGatewayActivity("scheduler");
|
|
19690
20424
|
this.ticking = true;
|
|
19691
20425
|
try {
|
|
19692
20426
|
const dueTemplates = await getDueTemplates();
|
|
@@ -19741,13 +20475,13 @@ var init_compose = __esm({
|
|
|
19741
20475
|
"use strict";
|
|
19742
20476
|
compose = (middleware, onError, onNotFound) => {
|
|
19743
20477
|
return (context, next) => {
|
|
19744
|
-
let
|
|
20478
|
+
let index2 = -1;
|
|
19745
20479
|
return dispatch(0);
|
|
19746
20480
|
async function dispatch(i) {
|
|
19747
|
-
if (i <=
|
|
20481
|
+
if (i <= index2) {
|
|
19748
20482
|
throw new Error("next() called multiple times");
|
|
19749
20483
|
}
|
|
19750
|
-
|
|
20484
|
+
index2 = i;
|
|
19751
20485
|
let res;
|
|
19752
20486
|
let isError = false;
|
|
19753
20487
|
let handler;
|
|
@@ -19862,8 +20596,8 @@ var init_body = __esm({
|
|
|
19862
20596
|
handleParsingNestedValues = (form, key, value) => {
|
|
19863
20597
|
let nestedForm = form;
|
|
19864
20598
|
const keys = key.split(".");
|
|
19865
|
-
keys.forEach((key2,
|
|
19866
|
-
if (
|
|
20599
|
+
keys.forEach((key2, index2) => {
|
|
20600
|
+
if (index2 === keys.length - 1) {
|
|
19867
20601
|
nestedForm[key2] = value;
|
|
19868
20602
|
} else {
|
|
19869
20603
|
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
@@ -19895,8 +20629,8 @@ var init_url = __esm({
|
|
|
19895
20629
|
};
|
|
19896
20630
|
extractGroupsFromPath = (path) => {
|
|
19897
20631
|
const groups = [];
|
|
19898
|
-
path = path.replace(/\{[^}]+\}/g, (match2,
|
|
19899
|
-
const mark = `@${
|
|
20632
|
+
path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
|
|
20633
|
+
const mark = `@${index2}`;
|
|
19900
20634
|
groups.push([mark, match2]);
|
|
19901
20635
|
return mark;
|
|
19902
20636
|
});
|
|
@@ -20415,10 +21149,10 @@ var init_html = __esm({
|
|
|
20415
21149
|
return;
|
|
20416
21150
|
}
|
|
20417
21151
|
let escape;
|
|
20418
|
-
let
|
|
21152
|
+
let index2;
|
|
20419
21153
|
let lastIndex = 0;
|
|
20420
|
-
for (
|
|
20421
|
-
switch (str.charCodeAt(
|
|
21154
|
+
for (index2 = match2; index2 < str.length; index2++) {
|
|
21155
|
+
switch (str.charCodeAt(index2)) {
|
|
20422
21156
|
case 34:
|
|
20423
21157
|
escape = """;
|
|
20424
21158
|
break;
|
|
@@ -20437,10 +21171,10 @@ var init_html = __esm({
|
|
|
20437
21171
|
default:
|
|
20438
21172
|
continue;
|
|
20439
21173
|
}
|
|
20440
|
-
buffer[0] += str.substring(lastIndex,
|
|
20441
|
-
lastIndex =
|
|
21174
|
+
buffer[0] += str.substring(lastIndex, index2) + escape;
|
|
21175
|
+
lastIndex = index2 + 1;
|
|
20442
21176
|
}
|
|
20443
|
-
buffer[0] += str.substring(lastIndex,
|
|
21177
|
+
buffer[0] += str.substring(lastIndex, index2);
|
|
20444
21178
|
};
|
|
20445
21179
|
resolveCallbackSync = (str) => {
|
|
20446
21180
|
const callbacks = str.callbacks;
|
|
@@ -21316,8 +22050,8 @@ function match(method, path) {
|
|
|
21316
22050
|
if (!match3) {
|
|
21317
22051
|
return [[], emptyParam];
|
|
21318
22052
|
}
|
|
21319
|
-
const
|
|
21320
|
-
return [matcher[1][
|
|
22053
|
+
const index2 = match3.indexOf("", 1);
|
|
22054
|
+
return [matcher[1][index2], match3];
|
|
21321
22055
|
});
|
|
21322
22056
|
this.match = match2;
|
|
21323
22057
|
return match2(method, path);
|
|
@@ -21364,7 +22098,7 @@ var init_node = __esm({
|
|
|
21364
22098
|
#index;
|
|
21365
22099
|
#varIndex;
|
|
21366
22100
|
#children = /* @__PURE__ */ Object.create(null);
|
|
21367
|
-
insert(tokens,
|
|
22101
|
+
insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
|
|
21368
22102
|
if (tokens.length === 0) {
|
|
21369
22103
|
if (this.#index !== void 0) {
|
|
21370
22104
|
throw PATH_ERROR;
|
|
@@ -21372,7 +22106,7 @@ var init_node = __esm({
|
|
|
21372
22106
|
if (pathErrorCheckOnly) {
|
|
21373
22107
|
return;
|
|
21374
22108
|
}
|
|
21375
|
-
this.#index =
|
|
22109
|
+
this.#index = index2;
|
|
21376
22110
|
return;
|
|
21377
22111
|
}
|
|
21378
22112
|
const [token, ...restTokens] = tokens;
|
|
@@ -21422,7 +22156,7 @@ var init_node = __esm({
|
|
|
21422
22156
|
node = this.#children[token] = new _Node();
|
|
21423
22157
|
}
|
|
21424
22158
|
}
|
|
21425
|
-
node.insert(restTokens,
|
|
22159
|
+
node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
|
|
21426
22160
|
}
|
|
21427
22161
|
buildRegExpStr() {
|
|
21428
22162
|
const childKeys = Object.keys(this.#children).sort(compareKey);
|
|
@@ -21454,7 +22188,7 @@ var init_trie = __esm({
|
|
|
21454
22188
|
Trie = class {
|
|
21455
22189
|
#context = { varIndex: 0 };
|
|
21456
22190
|
#root = new Node();
|
|
21457
|
-
insert(path,
|
|
22191
|
+
insert(path, index2, pathErrorCheckOnly) {
|
|
21458
22192
|
const paramAssoc = [];
|
|
21459
22193
|
const groups = [];
|
|
21460
22194
|
for (let i = 0; ; ) {
|
|
@@ -21480,7 +22214,7 @@ var init_trie = __esm({
|
|
|
21480
22214
|
}
|
|
21481
22215
|
}
|
|
21482
22216
|
}
|
|
21483
|
-
this.#root.insert(tokens,
|
|
22217
|
+
this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
|
|
21484
22218
|
return paramAssoc;
|
|
21485
22219
|
}
|
|
21486
22220
|
buildRegExp() {
|
|
@@ -22074,8 +22808,19 @@ __export(web_exports, {
|
|
|
22074
22808
|
dashboardApp: () => dashboardApp,
|
|
22075
22809
|
default: () => web_default
|
|
22076
22810
|
});
|
|
22077
|
-
import { existsSync as
|
|
22078
|
-
import { dirname as
|
|
22811
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync } from "fs";
|
|
22812
|
+
import { dirname as dirname3 } from "path";
|
|
22813
|
+
function parsePositiveInteger(value) {
|
|
22814
|
+
if (!/^\d+$/.test(value)) return null;
|
|
22815
|
+
const parsed = Number(value);
|
|
22816
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
22817
|
+
}
|
|
22818
|
+
function parseTaskStatus(value) {
|
|
22819
|
+
return TASK_STATUSES.has(value) ? value : null;
|
|
22820
|
+
}
|
|
22821
|
+
function safeStatus(value) {
|
|
22822
|
+
return value && TASK_STATUSES.has(value) ? value : "unknown";
|
|
22823
|
+
}
|
|
22079
22824
|
function formatDuration(startAt, endAt) {
|
|
22080
22825
|
if (!startAt) return "-";
|
|
22081
22826
|
const start = new Date(startAt).getTime();
|
|
@@ -22113,17 +22858,21 @@ function esc(s) {
|
|
|
22113
22858
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
22114
22859
|
}
|
|
22115
22860
|
function readCurrentConfig() {
|
|
22116
|
-
|
|
22861
|
+
const configPath = getConfigPath();
|
|
22862
|
+
if (!existsSync4(configPath)) return {};
|
|
22117
22863
|
try {
|
|
22118
|
-
return JSON.parse(
|
|
22864
|
+
return JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
22119
22865
|
} catch {
|
|
22120
22866
|
return {};
|
|
22121
22867
|
}
|
|
22122
22868
|
}
|
|
22123
22869
|
function writeConfig(cfg) {
|
|
22124
|
-
const
|
|
22125
|
-
|
|
22126
|
-
|
|
22870
|
+
const configPath = getConfigPath();
|
|
22871
|
+
const dir = dirname3(configPath);
|
|
22872
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
22873
|
+
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
22874
|
+
writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
22875
|
+
renameSync(tempPath, configPath);
|
|
22127
22876
|
}
|
|
22128
22877
|
function renderLayout(title, activeTab, body) {
|
|
22129
22878
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -22162,11 +22911,11 @@ async function saveConfig(){
|
|
|
22162
22911
|
scheduler:{
|
|
22163
22912
|
enabled:form.se.checked,
|
|
22164
22913
|
checkIntervalMs:Number(form.si.value),
|
|
22165
|
-
catchUp:form.cu.value,
|
|
22166
22914
|
},
|
|
22167
22915
|
watchdog:{
|
|
22168
22916
|
heartbeatTimeoutMs:Number(form.wt.value)*1000,
|
|
22169
|
-
|
|
22917
|
+
checkIntervalMs:Number(form.wci.value)*1000,
|
|
22918
|
+
cleanupIntervalMs:Number(form.wcl.value)*3600000,
|
|
22170
22919
|
retentionDays:Number(form.rd.value),
|
|
22171
22920
|
}
|
|
22172
22921
|
};
|
|
@@ -22197,7 +22946,7 @@ async function saveConfig(){
|
|
|
22197
22946
|
<dialog id="dd"><div class="dh"><h3 style="margin:0">\u8BE6\u60C5</h3><button class="cb" onclick="document.getElementById('dd').close()">×</button></div><div class="db"><pre id="dc"></pre></div></dialog>
|
|
22198
22947
|
</body></html>`;
|
|
22199
22948
|
}
|
|
22200
|
-
var app, SHARED_STYLES, dashboardApp, web_default;
|
|
22949
|
+
var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
|
|
22201
22950
|
var init_web = __esm({
|
|
22202
22951
|
"src/web/index.tsx"() {
|
|
22203
22952
|
"use strict";
|
|
@@ -22209,7 +22958,47 @@ var init_web = __esm({
|
|
|
22209
22958
|
init_drizzle_orm();
|
|
22210
22959
|
init_db2();
|
|
22211
22960
|
init_config();
|
|
22961
|
+
init_health();
|
|
22212
22962
|
app = new Hono2();
|
|
22963
|
+
TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
22964
|
+
"pending",
|
|
22965
|
+
"running",
|
|
22966
|
+
"done",
|
|
22967
|
+
"failed",
|
|
22968
|
+
"dead_letter",
|
|
22969
|
+
"cancelled"
|
|
22970
|
+
]);
|
|
22971
|
+
app.use("*", async (c, next) => {
|
|
22972
|
+
await next();
|
|
22973
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
22974
|
+
c.header("X-Frame-Options", "DENY");
|
|
22975
|
+
c.header("Referrer-Policy", "no-referrer");
|
|
22976
|
+
});
|
|
22977
|
+
app.use("/api/*", async (c, next) => {
|
|
22978
|
+
if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
|
|
22979
|
+
const fetchSite = c.req.header("Sec-Fetch-Site");
|
|
22980
|
+
if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
|
|
22981
|
+
return c.json({ error: "cross-site request rejected" }, 403);
|
|
22982
|
+
}
|
|
22983
|
+
const origin = c.req.header("Origin");
|
|
22984
|
+
if (origin) {
|
|
22985
|
+
try {
|
|
22986
|
+
const originUrl = new URL(origin);
|
|
22987
|
+
const requestUrl = new URL(c.req.url);
|
|
22988
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
|
|
22989
|
+
if (!loopback || originUrl.origin !== requestUrl.origin) {
|
|
22990
|
+
return c.json({ error: "cross-site request rejected" }, 403);
|
|
22991
|
+
}
|
|
22992
|
+
} catch {
|
|
22993
|
+
return c.json({ error: "invalid origin" }, 403);
|
|
22994
|
+
}
|
|
22995
|
+
}
|
|
22996
|
+
return next();
|
|
22997
|
+
});
|
|
22998
|
+
app.get("/health", (c) => {
|
|
22999
|
+
const health = getGatewayHealth();
|
|
23000
|
+
return c.json(health, health.status === "ok" ? 200 : 503);
|
|
23001
|
+
});
|
|
22213
23002
|
SHARED_STYLES = html`
|
|
22214
23003
|
<style>
|
|
22215
23004
|
:root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
|
|
@@ -22286,12 +23075,16 @@ var init_web = __esm({
|
|
|
22286
23075
|
</style>
|
|
22287
23076
|
`;
|
|
22288
23077
|
app.get("/", async (c) => {
|
|
22289
|
-
const
|
|
23078
|
+
const pageParam = c.req.query("page") || "1";
|
|
23079
|
+
const page = parsePositiveInteger(pageParam);
|
|
23080
|
+
if (page === null) return c.text("invalid page", 400);
|
|
22290
23081
|
const statusFilter = c.req.query("status") || "";
|
|
23082
|
+
const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
|
|
23083
|
+
if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
|
|
22291
23084
|
const limit = 50;
|
|
22292
23085
|
const offset = (page - 1) * limit;
|
|
22293
23086
|
const [tasks3, statsData] = await Promise.all([
|
|
22294
|
-
TaskService.list({ limit, offset, ...
|
|
23087
|
+
TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
|
|
22295
23088
|
TaskService.stats({})
|
|
22296
23089
|
]);
|
|
22297
23090
|
const taskIds = tasks3.map((t) => t.id);
|
|
@@ -22314,12 +23107,13 @@ var init_web = __esm({
|
|
|
22314
23107
|
</div>`;
|
|
22315
23108
|
let rows = "";
|
|
22316
23109
|
for (const task of tasks3) {
|
|
22317
|
-
const
|
|
23110
|
+
const status = safeStatus(task.status);
|
|
23111
|
+
const st = status.toUpperCase();
|
|
22318
23112
|
rows += `<tr>
|
|
22319
23113
|
<td class="mu">#${task.id}</td>
|
|
22320
23114
|
<td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
|
|
22321
23115
|
<td><span class="tag">${esc(task.agent)}</span></td>
|
|
22322
|
-
<td><span class="badge b-${
|
|
23116
|
+
<td><span class="badge b-${status}">${st}</span></td>
|
|
22323
23117
|
<td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
|
|
22324
23118
|
<td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
|
|
22325
23119
|
<td>
|
|
@@ -22351,21 +23145,13 @@ var init_web = __esm({
|
|
|
22351
23145
|
});
|
|
22352
23146
|
app.get("/templates", async (c) => {
|
|
22353
23147
|
const templates = await TaskTemplateService.list(100);
|
|
22354
|
-
for (const tmpl of templates) {
|
|
22355
|
-
if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
|
|
22356
|
-
try {
|
|
22357
|
-
const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
|
|
22358
|
-
tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
|
|
22359
|
-
} catch {
|
|
22360
|
-
}
|
|
22361
|
-
}
|
|
22362
|
-
}
|
|
22363
23148
|
const enabled = templates.filter((t) => t.enabled).length;
|
|
22364
23149
|
const disabled = templates.length - enabled;
|
|
22365
23150
|
let rows = "";
|
|
22366
23151
|
for (const t of templates) {
|
|
22367
|
-
const
|
|
22368
|
-
const
|
|
23152
|
+
const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
|
|
23153
|
+
const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
|
|
23154
|
+
const typeClass = "tag t-" + scheduleType;
|
|
22369
23155
|
let rule = "-";
|
|
22370
23156
|
if (t.scheduleType === "cron") rule = t.cronExpr || "-";
|
|
22371
23157
|
else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
|
|
@@ -22377,7 +23163,7 @@ var init_web = __esm({
|
|
|
22377
23163
|
<td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
|
|
22378
23164
|
<div class="sm" style="margin-top:2px"><span class="tag">${esc(t.agent)}</span>${t.model && t.model !== "default" ? ` <span class="tag">${esc(t.model)}</span>` : ""}</div></td>
|
|
22379
23165
|
<td><span class="${typeClass}">${typeLabel}</span></td>
|
|
22380
|
-
<td class="m sm">${rule}</td>
|
|
23166
|
+
<td class="m sm">${esc(rule)}</td>
|
|
22381
23167
|
<td>${statusBadge}</td>
|
|
22382
23168
|
<td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
|
|
22383
23169
|
<td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
|
|
@@ -22405,7 +23191,8 @@ var init_web = __esm({
|
|
|
22405
23191
|
return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
|
|
22406
23192
|
});
|
|
22407
23193
|
app.get("/runs", async (c) => {
|
|
22408
|
-
const page =
|
|
23194
|
+
const page = parsePositiveInteger(c.req.query("page") || "1");
|
|
23195
|
+
if (page === null) return c.text("invalid page", 400);
|
|
22409
23196
|
const limit = 50;
|
|
22410
23197
|
const offset = (page - 1) * limit;
|
|
22411
23198
|
const { taskRuns: tr, tasks: tk } = schema_exports;
|
|
@@ -22423,13 +23210,14 @@ var init_web = __esm({
|
|
|
22423
23210
|
childPid: tr.childPid,
|
|
22424
23211
|
taskName: tk.name,
|
|
22425
23212
|
taskAgent: tk.agent
|
|
22426
|
-
}).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
|
|
23213
|
+
}).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
|
|
22427
23214
|
const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
|
|
22428
23215
|
const total = Number(totalResult[0]?.count ?? 0);
|
|
22429
23216
|
const totalPages = Math.ceil(total / limit);
|
|
22430
23217
|
let rows = "";
|
|
22431
23218
|
const logsHtml = [];
|
|
22432
23219
|
for (const run of runs) {
|
|
23220
|
+
const status = safeStatus(run.status);
|
|
22433
23221
|
const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
22434
23222
|
const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
|
|
22435
23223
|
rows += `<tr>
|
|
@@ -22437,15 +23225,15 @@ var init_web = __esm({
|
|
|
22437
23225
|
<td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
|
|
22438
23226
|
${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
|
|
22439
23227
|
<td><span class="tag">${esc(run.taskAgent)}</span></td>
|
|
22440
|
-
<td><span class="badge b-${
|
|
23228
|
+
<td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
|
|
22441
23229
|
<td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
|
|
22442
23230
|
<td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
22443
23231
|
<td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
|
|
22444
23232
|
</tr>`;
|
|
22445
23233
|
if (run.log) {
|
|
22446
23234
|
logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
|
|
22447
|
-
<div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
|
|
22448
|
-
<div class="log-box">${run.log
|
|
23235
|
+
<div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
|
|
23236
|
+
<div class="log-box">${esc(run.log)}</div></div></div>`);
|
|
22449
23237
|
}
|
|
22450
23238
|
}
|
|
22451
23239
|
let paging = `<div class="pn">`;
|
|
@@ -22471,17 +23259,18 @@ var init_web = __esm({
|
|
|
22471
23259
|
});
|
|
22472
23260
|
app.get("/system", async (c) => {
|
|
22473
23261
|
const config = loadConfig();
|
|
23262
|
+
const configPath = getConfigPath();
|
|
22474
23263
|
const stats = await TaskService.stats({});
|
|
22475
23264
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
22476
23265
|
const templates = await TaskTemplateService.list(100);
|
|
22477
|
-
const configExists =
|
|
23266
|
+
const configExists = existsSync4(configPath);
|
|
22478
23267
|
let runRows = "";
|
|
22479
23268
|
if (runningRuns.length > 0) {
|
|
22480
23269
|
for (const run of runningRuns) {
|
|
22481
23270
|
const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
22482
23271
|
runRows += `<tr>
|
|
22483
23272
|
<td class="mu">#${run.id}</td><td>#${run.taskId}</td>
|
|
22484
|
-
<td class="m sm">${shortS}</td>
|
|
23273
|
+
<td class="m sm">${esc(shortS)}</td>
|
|
22485
23274
|
<td class="sm">${esc(run.model) || "-"}</td>
|
|
22486
23275
|
<td class="sm">${formatDate(run.startedAt)}</td>
|
|
22487
23276
|
<td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
@@ -22506,19 +23295,14 @@ var init_web = __esm({
|
|
|
22506
23295
|
<h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
|
|
22507
23296
|
<div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
|
|
22508
23297
|
<div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(ms)</label><input type="number" name="si" value="${config.scheduler.checkIntervalMs}" min="100" style="width:100px"></div>
|
|
22509
|
-
<div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
|
|
22510
|
-
<option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
|
|
22511
|
-
<option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
|
|
22512
|
-
<option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
|
|
22513
|
-
</select></div>
|
|
22514
23298
|
<div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
|
|
22515
23299
|
</div>
|
|
22516
23300
|
<div class="card">
|
|
22517
23301
|
<h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
|
|
22518
23302
|
<div class="form-row"><label>\u5FC3\u8DF3\u8D85\u65F6(\u79D2)</label><input type="number" name="wt" value="${config.watchdog.heartbeatTimeoutMs / 1e3}" min="10" style="width:100px"></div>
|
|
22519
|
-
<div class="form-row"><label>\
|
|
23303
|
+
<div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(\u79D2)</label><input type="number" name="wci" value="${config.watchdog.checkIntervalMs / 1e3}" min="1" style="width:100px"></div>
|
|
23304
|
+
<div class="form-row"><label>\u6E05\u7406\u95F4\u9694(\u5C0F\u65F6)</label><input type="number" name="wcl" value="${config.watchdog.cleanupIntervalMs / 36e5}" min="1" style="width:100px"></div>
|
|
22520
23305
|
<div class="form-row"><label>\u6570\u636E\u4FDD\u7559(\u5929)</label><input type="number" name="rd" value="${config.watchdog.retentionDays}" min="1" style="width:100px"></div>
|
|
22521
|
-
<div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
|
|
22522
23306
|
</div>
|
|
22523
23307
|
</div>
|
|
22524
23308
|
<div style="text-align:center;margin-bottom:24px">
|
|
@@ -22547,7 +23331,7 @@ var init_web = __esm({
|
|
|
22547
23331
|
|
|
22548
23332
|
<div class="card mt16">
|
|
22549
23333
|
<h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
|
|
22550
|
-
<div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${
|
|
23334
|
+
<div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
|
|
22551
23335
|
<div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
|
|
22552
23336
|
</div>
|
|
22553
23337
|
|
|
@@ -22559,46 +23343,61 @@ var init_web = __esm({
|
|
|
22559
23343
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
22560
23344
|
});
|
|
22561
23345
|
app.get("/api/tasks/:id", async (c) => {
|
|
22562
|
-
const id =
|
|
23346
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23347
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
22563
23348
|
const task = await TaskService.getById(id);
|
|
22564
23349
|
if (!task) return c.json({ error: "not found" }, 404);
|
|
22565
23350
|
const runs = await TaskRunService.listByTaskId(id);
|
|
22566
23351
|
return c.json({ ...task, _runs: runs });
|
|
22567
23352
|
});
|
|
22568
23353
|
app.get("/api/runs/:id", async (c) => {
|
|
22569
|
-
const id =
|
|
23354
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23355
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
22570
23356
|
const run = await TaskRunService.getById(id);
|
|
22571
23357
|
if (!run) return c.json({ error: "not found" }, 404);
|
|
22572
23358
|
return c.json(run);
|
|
22573
23359
|
});
|
|
22574
23360
|
app.get("/api/templates/:id", async (c) => {
|
|
22575
|
-
const id =
|
|
23361
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23362
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
22576
23363
|
const tmpl = await TaskTemplateService.getById(id);
|
|
22577
23364
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
22578
23365
|
return c.json(tmpl);
|
|
22579
23366
|
});
|
|
22580
23367
|
app.post("/api/tasks/:id/retry", async (c) => {
|
|
22581
|
-
|
|
22582
|
-
return c.json({
|
|
23368
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23369
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
23370
|
+
const task = await TaskService.retry(id);
|
|
23371
|
+
if (task) return c.json({ success: true });
|
|
23372
|
+
return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
|
|
22583
23373
|
});
|
|
22584
23374
|
app.delete("/api/tasks/:id", async (c) => {
|
|
22585
|
-
|
|
22586
|
-
return c.json({
|
|
23375
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23376
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
23377
|
+
const deleted = await TaskService.delete(id);
|
|
23378
|
+
return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
22587
23379
|
});
|
|
22588
23380
|
app.post("/api/templates/:id/enable", async (c) => {
|
|
22589
|
-
const
|
|
22590
|
-
return c.json({
|
|
23381
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23382
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
23383
|
+
const result = await TaskTemplateService.enable(id);
|
|
23384
|
+
return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
22591
23385
|
});
|
|
22592
23386
|
app.post("/api/templates/:id/disable", async (c) => {
|
|
22593
|
-
const
|
|
22594
|
-
return c.json({
|
|
23387
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23388
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
23389
|
+
const result = await TaskTemplateService.disable(id);
|
|
23390
|
+
return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
22595
23391
|
});
|
|
22596
23392
|
app.delete("/api/templates/:id", async (c) => {
|
|
22597
|
-
const
|
|
22598
|
-
return c.json({
|
|
23393
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23394
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
23395
|
+
const ok = await TaskTemplateService.delete(id);
|
|
23396
|
+
return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
22599
23397
|
});
|
|
22600
23398
|
app.post("/api/templates/:id/trigger", async (c) => {
|
|
22601
|
-
const id =
|
|
23399
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
23400
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
22602
23401
|
const tmpl = await TaskTemplateService.getById(id);
|
|
22603
23402
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
22604
23403
|
const task = await TaskService.add({
|
|
@@ -22610,7 +23409,10 @@ var init_web = __esm({
|
|
|
22610
23409
|
category: tmpl.category,
|
|
22611
23410
|
importance: tmpl.importance,
|
|
22612
23411
|
urgency: tmpl.urgency,
|
|
23412
|
+
batchId: tmpl.batchId,
|
|
22613
23413
|
maxRetries: tmpl.maxRetries,
|
|
23414
|
+
retryBackoffMs: tmpl.retryBackoffMs,
|
|
23415
|
+
timeoutMs: tmpl.timeoutMs,
|
|
22614
23416
|
templateId: tmpl.id
|
|
22615
23417
|
});
|
|
22616
23418
|
return c.json({ success: true, taskId: task.id });
|
|
@@ -22625,22 +23427,29 @@ var init_web = __esm({
|
|
|
22625
23427
|
const bW = body.worker ?? {};
|
|
22626
23428
|
const bS = body.scheduler ?? {};
|
|
22627
23429
|
const bD = body.watchdog ?? {};
|
|
22628
|
-
const merged = {
|
|
22629
|
-
|
|
23430
|
+
const merged = {
|
|
23431
|
+
...current,
|
|
23432
|
+
...body,
|
|
23433
|
+
configVersion: 2,
|
|
23434
|
+
worker: { ...curW, ...bW },
|
|
23435
|
+
scheduler: { ...curS, ...bS },
|
|
23436
|
+
watchdog: { ...curD, ...bD }
|
|
23437
|
+
};
|
|
23438
|
+
writeConfig(validateConfig(merged));
|
|
22630
23439
|
return c.json({ success: true });
|
|
22631
23440
|
} catch (err) {
|
|
22632
|
-
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
23441
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
|
|
22633
23442
|
}
|
|
22634
23443
|
});
|
|
22635
23444
|
app.post("/api/database/clear", async (c) => {
|
|
22636
23445
|
try {
|
|
22637
|
-
const { tasks: tasks3, taskRuns:
|
|
22638
|
-
await db.delete(
|
|
23446
|
+
const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
|
|
23447
|
+
await db.delete(taskRuns4);
|
|
22639
23448
|
await db.delete(taskTemplates4);
|
|
22640
23449
|
await db.delete(tasks3);
|
|
22641
23450
|
return c.json({ success: true });
|
|
22642
23451
|
} catch (err) {
|
|
22643
|
-
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
23452
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
22644
23453
|
}
|
|
22645
23454
|
});
|
|
22646
23455
|
dashboardApp = app;
|
|
@@ -22676,7 +23485,7 @@ function acquireLock() {
|
|
|
22676
23485
|
sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
|
|
22677
23486
|
}
|
|
22678
23487
|
sqlite.exec(
|
|
22679
|
-
"INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?,
|
|
23488
|
+
"INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
|
|
22680
23489
|
[pid, now, now]
|
|
22681
23490
|
);
|
|
22682
23491
|
sqlite.exec("COMMIT");
|
|
@@ -22710,6 +23519,18 @@ function updateLockHeartbeat() {
|
|
|
22710
23519
|
} catch {
|
|
22711
23520
|
}
|
|
22712
23521
|
}
|
|
23522
|
+
function markGatewayReady() {
|
|
23523
|
+
sqlite.exec(
|
|
23524
|
+
"UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
|
|
23525
|
+
[Date.now(), Date.now(), process.pid]
|
|
23526
|
+
);
|
|
23527
|
+
}
|
|
23528
|
+
function markGatewayNotReady() {
|
|
23529
|
+
try {
|
|
23530
|
+
sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
|
|
23531
|
+
} catch {
|
|
23532
|
+
}
|
|
23533
|
+
}
|
|
22713
23534
|
async function main() {
|
|
22714
23535
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
|
|
22715
23536
|
if (!acquireLock()) {
|
|
@@ -22721,20 +23542,21 @@ async function main() {
|
|
|
22721
23542
|
const worker = new WorkerEngine(cfg);
|
|
22722
23543
|
const watchdog = new Watchdog(cfg);
|
|
22723
23544
|
const scheduler = new Scheduler(cfg);
|
|
23545
|
+
initializeGatewayHealth({
|
|
23546
|
+
workerPollIntervalMs: cfg.worker.pollIntervalMs,
|
|
23547
|
+
schedulerEnabled: cfg.scheduler.enabled,
|
|
23548
|
+
schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
|
|
23549
|
+
watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
|
|
23550
|
+
});
|
|
22724
23551
|
worker.start();
|
|
22725
23552
|
watchdog.start();
|
|
22726
23553
|
await scheduler.start();
|
|
22727
23554
|
if (cfg.dashboard.enabled) {
|
|
22728
23555
|
const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
|
|
22729
23556
|
Bun.serve({
|
|
23557
|
+
hostname: "127.0.0.1",
|
|
22730
23558
|
port: cfg.dashboard.port,
|
|
22731
|
-
fetch
|
|
22732
|
-
const url = new URL(req.url);
|
|
22733
|
-
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "::1") {
|
|
22734
|
-
return new Response("Forbidden", { status: 403 });
|
|
22735
|
-
}
|
|
22736
|
-
return dashboardApp2.fetch(req);
|
|
22737
|
-
}
|
|
23559
|
+
fetch: dashboardApp2.fetch
|
|
22738
23560
|
});
|
|
22739
23561
|
console.log(JSON.stringify({
|
|
22740
23562
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -22743,6 +23565,7 @@ async function main() {
|
|
|
22743
23565
|
url: `http://localhost:${cfg.dashboard.port}`
|
|
22744
23566
|
}));
|
|
22745
23567
|
}
|
|
23568
|
+
markGatewayReady();
|
|
22746
23569
|
console.log(JSON.stringify({
|
|
22747
23570
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22748
23571
|
level: "info",
|
|
@@ -22756,10 +23579,10 @@ async function main() {
|
|
|
22756
23579
|
shuttingDown = true;
|
|
22757
23580
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
|
|
22758
23581
|
clearInterval(heartbeatTimer);
|
|
23582
|
+
markGatewayNotReady();
|
|
22759
23583
|
scheduler.stop();
|
|
22760
23584
|
watchdog.stop();
|
|
22761
|
-
const runningIds = worker.
|
|
22762
|
-
await worker.stop();
|
|
23585
|
+
const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
|
|
22763
23586
|
if (runningIds.length > 0) {
|
|
22764
23587
|
const resetCount = await TaskService.resetRunningToPending(runningIds);
|
|
22765
23588
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
|
|
@@ -22769,6 +23592,7 @@ async function main() {
|
|
|
22769
23592
|
await TaskRunService.fail(run.id, "Gateway shutdown");
|
|
22770
23593
|
}
|
|
22771
23594
|
releaseLock();
|
|
23595
|
+
resetGatewayHealth();
|
|
22772
23596
|
closeDb();
|
|
22773
23597
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
|
|
22774
23598
|
process.exit(0);
|
|
@@ -22796,6 +23620,7 @@ var init_gateway = __esm({
|
|
|
22796
23620
|
init_db2();
|
|
22797
23621
|
init_task_service();
|
|
22798
23622
|
init_task_run_service();
|
|
23623
|
+
init_health();
|
|
22799
23624
|
STALE_THRESHOLD_MS = 3e4;
|
|
22800
23625
|
if (import.meta.main) {
|
|
22801
23626
|
main();
|
|
@@ -22803,247 +23628,87 @@ var init_gateway = __esm({
|
|
|
22803
23628
|
}
|
|
22804
23629
|
});
|
|
22805
23630
|
|
|
22806
|
-
// src/daemon/
|
|
22807
|
-
var
|
|
22808
|
-
__export(
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
isGatewayRunning: () => isGatewayRunning,
|
|
22812
|
-
uninstall: () => uninstall,
|
|
22813
|
-
upgrade: () => upgrade
|
|
23631
|
+
// src/daemon/update.ts
|
|
23632
|
+
var update_exports = {};
|
|
23633
|
+
__export(update_exports, {
|
|
23634
|
+
installLatestPlugin: () => installLatestPlugin,
|
|
23635
|
+
resolveInstalledPlugin: () => resolveInstalledPlugin
|
|
22814
23636
|
});
|
|
22815
|
-
import {
|
|
22816
|
-
import {
|
|
22817
|
-
import {
|
|
22818
|
-
import {
|
|
22819
|
-
|
|
22820
|
-
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
22824
|
-
return pkg.version || "0.0.0";
|
|
22825
|
-
} catch {
|
|
22826
|
-
return "0.0.0";
|
|
22827
|
-
}
|
|
22828
|
-
}
|
|
22829
|
-
function getRunningVersion() {
|
|
23637
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
23638
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync } from "fs";
|
|
23639
|
+
import { homedir as homedir4 } from "os";
|
|
23640
|
+
import { join as join4 } from "path";
|
|
23641
|
+
function pluginAt(packageDir) {
|
|
23642
|
+
const packageJson = join4(packageDir, "package.json");
|
|
23643
|
+
const gatewayEntry = join4(packageDir, "dist/gateway/index.js");
|
|
23644
|
+
if (!existsSync5(packageJson) || !existsSync5(gatewayEntry)) return null;
|
|
22830
23645
|
try {
|
|
22831
|
-
|
|
22832
|
-
|
|
23646
|
+
const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
|
|
23647
|
+
if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
|
|
23648
|
+
return { packageDir, gatewayEntry, version: pkg.version };
|
|
22833
23649
|
} catch {
|
|
22834
23650
|
return null;
|
|
22835
23651
|
}
|
|
22836
23652
|
}
|
|
22837
|
-
function
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
} catch {
|
|
22841
|
-
}
|
|
22842
|
-
}
|
|
22843
|
-
function pm2Bin() {
|
|
22844
|
-
return process.platform === "win32" ? "pm2.cmd" : "pm2";
|
|
22845
|
-
}
|
|
22846
|
-
function isPm2Installed() {
|
|
22847
|
-
try {
|
|
22848
|
-
const cmd = process.platform === "win32" ? "where pm2" : "which pm2";
|
|
22849
|
-
execSync(cmd, { stdio: "pipe" });
|
|
22850
|
-
return true;
|
|
22851
|
-
} catch {
|
|
22852
|
-
return false;
|
|
22853
|
-
}
|
|
22854
|
-
}
|
|
22855
|
-
function installPm2() {
|
|
22856
|
-
console.log("[supertask] Installing pm2...");
|
|
22857
|
-
try {
|
|
22858
|
-
execSync("npm install -g pm2", { stdio: "inherit" });
|
|
22859
|
-
return true;
|
|
22860
|
-
} catch {
|
|
22861
|
-
try {
|
|
22862
|
-
execSync("bun install -g pm2", { stdio: "inherit" });
|
|
22863
|
-
return true;
|
|
22864
|
-
} catch {
|
|
22865
|
-
return false;
|
|
22866
|
-
}
|
|
22867
|
-
}
|
|
22868
|
-
}
|
|
22869
|
-
function pm2Exec(args) {
|
|
22870
|
-
const bin = pm2Bin();
|
|
22871
|
-
try {
|
|
22872
|
-
const result = spawnSync(bin, args, {
|
|
22873
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
22874
|
-
encoding: "utf-8",
|
|
22875
|
-
shell: process.platform === "win32"
|
|
22876
|
-
});
|
|
22877
|
-
const output = (result.stdout ?? "") + (result.stderr ?? "");
|
|
22878
|
-
return { ok: result.status === 0, output };
|
|
22879
|
-
} catch (err) {
|
|
22880
|
-
return { ok: false, output: err instanceof Error ? err.message : String(err) };
|
|
22881
|
-
}
|
|
22882
|
-
}
|
|
22883
|
-
function pm2JsonList() {
|
|
22884
|
-
const { ok, output } = pm2Exec(["jlist"]);
|
|
22885
|
-
if (!ok) return [];
|
|
22886
|
-
try {
|
|
22887
|
-
return JSON.parse(output);
|
|
22888
|
-
} catch {
|
|
22889
|
-
return [];
|
|
22890
|
-
}
|
|
22891
|
-
}
|
|
22892
|
-
function isGatewayRunning() {
|
|
22893
|
-
const list = pm2JsonList();
|
|
22894
|
-
const proc = list.find((p) => p.name === PROCESS_NAME);
|
|
22895
|
-
if (!proc) return false;
|
|
22896
|
-
return proc.pm2_env?.status === "online";
|
|
23653
|
+
function versionParts(version2) {
|
|
23654
|
+
const match2 = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version2);
|
|
23655
|
+
return match2 ? match2.slice(1).map(Number) : null;
|
|
22897
23656
|
}
|
|
22898
|
-
function
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
return
|
|
23657
|
+
function compareVersions(left, right) {
|
|
23658
|
+
const a = versionParts(left);
|
|
23659
|
+
const b = versionParts(right);
|
|
23660
|
+
if (!a || !b) return left.localeCompare(right);
|
|
23661
|
+
for (let index2 = 0; index2 < 3; index2 += 1) {
|
|
23662
|
+
if (a[index2] !== b[index2]) return a[index2] - b[index2];
|
|
22904
23663
|
}
|
|
23664
|
+
return 0;
|
|
22905
23665
|
}
|
|
22906
|
-
function
|
|
22907
|
-
|
|
22908
|
-
return pm2Exec([
|
|
22909
|
-
"start",
|
|
22910
|
-
bunPath,
|
|
22911
|
-
"--name",
|
|
22912
|
-
PROCESS_NAME,
|
|
22913
|
-
"--interpreter",
|
|
22914
|
-
"none",
|
|
22915
|
-
"--restart-delay",
|
|
22916
|
-
"5000",
|
|
22917
|
-
"--max-restarts",
|
|
22918
|
-
"30",
|
|
22919
|
-
"--",
|
|
22920
|
-
GATEWAY_ENTRY
|
|
22921
|
-
]);
|
|
23666
|
+
function cacheRoot() {
|
|
23667
|
+
return process.env.SUPERTASK_OPENCODE_CACHE_DIR ?? join4(homedir4(), ".cache/opencode/packages");
|
|
22922
23668
|
}
|
|
22923
|
-
function
|
|
22924
|
-
|
|
22925
|
-
|
|
22926
|
-
|
|
22927
|
-
}
|
|
23669
|
+
function resolveInstalledPlugin() {
|
|
23670
|
+
const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
|
|
23671
|
+
if (override) {
|
|
23672
|
+
const plugin = pluginAt(override);
|
|
23673
|
+
if (!plugin) throw new Error(`[supertask] \u5B89\u88C5\u5305\u65E0\u6548\u6216\u7F3A\u5C11 Gateway \u6784\u5EFA\u4EA7\u7269: ${override}`);
|
|
23674
|
+
return plugin;
|
|
22928
23675
|
}
|
|
22929
|
-
|
|
22930
|
-
const
|
|
22931
|
-
const
|
|
22932
|
-
if (
|
|
22933
|
-
|
|
22934
|
-
const { ok } = pm2Exec(["reload", PROCESS_NAME]);
|
|
22935
|
-
if (!ok) {
|
|
22936
|
-
console.error("[supertask] pm2 reload failed, trying restart...");
|
|
22937
|
-
pm2Exec(["restart", PROCESS_NAME]);
|
|
22938
|
-
}
|
|
22939
|
-
} else {
|
|
22940
|
-
console.log("[supertask] Starting Gateway with pm2...");
|
|
22941
|
-
const version2 = getPackageVersion();
|
|
22942
|
-
const { ok, output } = pm2StartGateway(version2);
|
|
22943
|
-
if (!ok) {
|
|
22944
|
-
throw new Error(`[supertask] pm2 start failed: ${output}`);
|
|
22945
|
-
}
|
|
22946
|
-
writeRunningVersion(version2);
|
|
22947
|
-
}
|
|
22948
|
-
pm2Exec(["save"]);
|
|
22949
|
-
console.log("[supertask] Configuring startup...");
|
|
22950
|
-
const { ok: startupOk, output: startupOutput } = pm2Exec(["startup"]);
|
|
22951
|
-
if (!startupOk) {
|
|
22952
|
-
if (startupOutput.includes("sudo") || startupOutput.includes("run as root")) {
|
|
22953
|
-
console.log("[supertask] pm2 startup requires elevated permissions.");
|
|
22954
|
-
console.log("[supertask] Run the command shown above, or manually execute:");
|
|
22955
|
-
console.log(` pm2 startup`);
|
|
22956
|
-
console.log(` pm2 save`);
|
|
22957
|
-
} else if (process.platform === "win32") {
|
|
22958
|
-
console.log("[supertask] On Windows, use pm2-installer for startup:");
|
|
22959
|
-
console.log(" npm install -g pm2-windows-startup");
|
|
22960
|
-
console.log(" pm2-startup install");
|
|
22961
|
-
}
|
|
23676
|
+
const root = cacheRoot();
|
|
23677
|
+
const packageDirs = existsSync5(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
|
+
const installed = packageDirs.map(pluginAt).filter((plugin) => plugin !== null).sort((left, right) => compareVersions(right.version, left.version));
|
|
23679
|
+
if (!installed[0]) {
|
|
23680
|
+
throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
|
|
22962
23681
|
}
|
|
22963
|
-
|
|
22964
|
-
console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
|
|
23682
|
+
return installed[0];
|
|
22965
23683
|
}
|
|
22966
|
-
function
|
|
22967
|
-
|
|
22968
|
-
pm2Exec(["stop", PROCESS_NAME]);
|
|
22969
|
-
console.log("[supertask] Removing Gateway from pm2...");
|
|
22970
|
-
pm2Exec(["delete", PROCESS_NAME]);
|
|
22971
|
-
pm2Exec(["save"]);
|
|
22972
|
-
console.log("\n[supertask] Gateway removed from pm2.");
|
|
22973
|
-
console.log("[supertask] Note: pm2 startup config was not removed (you may have other pm2 processes).");
|
|
22974
|
-
console.log("[supertask] To fully remove pm2 startup: pm2 unstartup");
|
|
23684
|
+
function opencodeBin() {
|
|
23685
|
+
return process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
22975
23686
|
}
|
|
22976
|
-
function
|
|
22977
|
-
const
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
console.log(`[supertask] Gateway upgraded: ${before ?? "unknown"} \u2192 ${currentVersion}`);
|
|
22991
|
-
} else {
|
|
22992
|
-
throw new Error(`[supertask] Failed to start Gateway after upgrade`);
|
|
22993
|
-
}
|
|
22994
|
-
return { before, after: currentVersion, restarted: true };
|
|
22995
|
-
}
|
|
22996
|
-
function ensureGateway() {
|
|
22997
|
-
const currentVersion = getPackageVersion();
|
|
22998
|
-
try {
|
|
22999
|
-
const list = pm2JsonList();
|
|
23000
|
-
const proc = list.find((p) => p.name === PROCESS_NAME);
|
|
23001
|
-
if (proc && proc.pm2_env?.status === "online") {
|
|
23002
|
-
const runningVersion = getRunningVersion();
|
|
23003
|
-
if (runningVersion === currentVersion) {
|
|
23004
|
-
return;
|
|
23005
|
-
}
|
|
23006
|
-
console.log(`[supertask] Version changed: ${runningVersion ?? "unknown"} \u2192 ${currentVersion}, reloading Gateway...`);
|
|
23007
|
-
pm2Exec(["delete", PROCESS_NAME]);
|
|
23008
|
-
const { ok } = pm2StartGateway(currentVersion);
|
|
23009
|
-
if (ok) writeRunningVersion(currentVersion);
|
|
23010
|
-
pm2Exec(["save"]);
|
|
23011
|
-
return;
|
|
23012
|
-
}
|
|
23013
|
-
} catch {
|
|
23014
|
-
}
|
|
23015
|
-
if (!isPm2Installed()) {
|
|
23016
|
-
console.log("[supertask] Installing pm2 for Gateway process management...");
|
|
23017
|
-
try {
|
|
23018
|
-
execSync("npm install -g pm2", { stdio: "pipe" });
|
|
23019
|
-
} catch {
|
|
23020
|
-
try {
|
|
23021
|
-
execSync("bun install -g pm2", { stdio: "pipe" });
|
|
23022
|
-
} catch {
|
|
23023
|
-
console.warn("[supertask] Could not install pm2. Gateway will not auto-start. Run `supertask install` manually.");
|
|
23024
|
-
return;
|
|
23025
|
-
}
|
|
23026
|
-
}
|
|
23687
|
+
function installLatestPlugin() {
|
|
23688
|
+
const result = spawnSync3(opencodeBin(), [
|
|
23689
|
+
"plugin",
|
|
23690
|
+
`${PACKAGE_NAME}@latest`,
|
|
23691
|
+
"--global",
|
|
23692
|
+
"--force"
|
|
23693
|
+
], {
|
|
23694
|
+
encoding: "utf8",
|
|
23695
|
+
env: process.env,
|
|
23696
|
+
timeout: 12e4
|
|
23697
|
+
});
|
|
23698
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
23699
|
+
if (result.error) {
|
|
23700
|
+
throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${result.error.message}`);
|
|
23027
23701
|
}
|
|
23028
|
-
|
|
23029
|
-
|
|
23030
|
-
if (existing) {
|
|
23031
|
-
pm2Exec(["restart", PROCESS_NAME]);
|
|
23032
|
-
} else {
|
|
23033
|
-
const version2 = getPackageVersion();
|
|
23034
|
-
const { ok } = pm2StartGateway(version2);
|
|
23035
|
-
if (ok) writeRunningVersion(version2);
|
|
23702
|
+
if (result.status !== 0) {
|
|
23703
|
+
throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${result.status}`}`);
|
|
23036
23704
|
}
|
|
23037
|
-
|
|
23705
|
+
return resolveInstalledPlugin();
|
|
23038
23706
|
}
|
|
23039
|
-
var
|
|
23040
|
-
var
|
|
23041
|
-
"src/daemon/
|
|
23707
|
+
var PACKAGE_NAME;
|
|
23708
|
+
var init_update2 = __esm({
|
|
23709
|
+
"src/daemon/update.ts"() {
|
|
23042
23710
|
"use strict";
|
|
23043
|
-
|
|
23044
|
-
GATEWAY_ENTRY = join3(__dirname, "../gateway/index.js");
|
|
23045
|
-
PROCESS_NAME = "supertask-gateway";
|
|
23046
|
-
VERSION_FILE = join3(homedir3(), ".local/share/opencode/supertask-gateway-version");
|
|
23711
|
+
PACKAGE_NAME = "opencode-supertask";
|
|
23047
23712
|
}
|
|
23048
23713
|
});
|
|
23049
23714
|
|
|
@@ -23099,6 +23764,7 @@ function parseDuration(input) {
|
|
|
23099
23764
|
}
|
|
23100
23765
|
|
|
23101
23766
|
// src/cli/index.ts
|
|
23767
|
+
init_pm2();
|
|
23102
23768
|
async function withDb(fn) {
|
|
23103
23769
|
try {
|
|
23104
23770
|
return await fn();
|
|
@@ -23111,9 +23777,14 @@ async function withDb(fn) {
|
|
|
23111
23777
|
}
|
|
23112
23778
|
}
|
|
23113
23779
|
var program2 = new Command();
|
|
23114
|
-
program2.name("supertask").description("\u901A\u7528\u4EFB\u52A1\u7BA1\u7406\u7CFB\u7EDF - AI Agent \u4EFB\u52A1\u8C03\u5EA6\u5668").version(
|
|
23115
|
-
program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOption("-n, --name <name>", "\u4EFB\u52A1\u540D\u79F0").requiredOption("-a, --agent <agent>", "\u4E3B Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 (1-5)", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 (1-5)", "3").option("-b, --batch <batchId>", "\u6279\u6B21 ID").option("-d, --depends <taskId>", "\u4F9D\u8D56\u7684\u4EFB\u52A1 ID").option("-w, --cwd <path>", "(\u5DF2\u5E9F\u5F03) \u5DE5\u4F5C\u76EE\u5F55\u3002\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u8BB0\u5F55\u63D0\u4EA4\u4EFB\u52A1\u65F6\u7684\u5F53\u524D\u76EE\u5F55").action(async (options) => withDb(async () => {
|
|
23780
|
+
program2.name("supertask").description("\u901A\u7528\u4EFB\u52A1\u7BA1\u7406\u7CFB\u7EDF - AI Agent \u4EFB\u52A1\u8C03\u5EA6\u5668").version(getPackageVersion());
|
|
23781
|
+
program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOption("-n, --name <name>", "\u4EFB\u52A1\u540D\u79F0").requiredOption("-a, --agent <agent>", "\u4E3B Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 (1-5)", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 (1-5)", "3").option("-b, --batch <batchId>", "\u6279\u6B21 ID").option("-d, --depends <taskId>", "\u4F9D\u8D56\u7684\u4EFB\u52A1 ID").option("--max-retries <number>", "\u9996\u6B21\u6267\u884C\u4E4B\u5916\u5141\u8BB8\u7684\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <duration>", "\u91CD\u8BD5\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min", "30s").option("--timeout <duration>", "\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").option("-w, --cwd <path>", "(\u5DF2\u5E9F\u5F03) \u5DE5\u4F5C\u76EE\u5F55\u3002\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u8BB0\u5F55\u63D0\u4EA4\u4EFB\u52A1\u65F6\u7684\u5F53\u524D\u76EE\u5F55").action(async (options) => withDb(async () => {
|
|
23116
23782
|
const submitCwd = process.cwd();
|
|
23783
|
+
const retryBackoffMs = parseDuration(options.retryBackoff);
|
|
23784
|
+
const timeoutMs = options.timeout ? parseDuration(options.timeout) : null;
|
|
23785
|
+
if (retryBackoffMs === null || options.timeout && timeoutMs === null) {
|
|
23786
|
+
throw new Error("retry-backoff \u6216 timeout \u683C\u5F0F\u65E0\u6548");
|
|
23787
|
+
}
|
|
23117
23788
|
const task = await TaskService.add({
|
|
23118
23789
|
name: options.name,
|
|
23119
23790
|
agent: options.agent,
|
|
@@ -23124,7 +23795,10 @@ program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOp
|
|
|
23124
23795
|
urgency: parseInt(options.urgency),
|
|
23125
23796
|
batchId: options.batch,
|
|
23126
23797
|
dependsOn: options.depends ? parseInt(options.depends) : void 0,
|
|
23127
|
-
cwd: submitCwd
|
|
23798
|
+
cwd: submitCwd,
|
|
23799
|
+
maxRetries: parseInt(options.maxRetries),
|
|
23800
|
+
retryBackoffMs,
|
|
23801
|
+
timeoutMs
|
|
23128
23802
|
});
|
|
23129
23803
|
console.log(JSON.stringify({ id: task.id, status: "created" }, null, 2));
|
|
23130
23804
|
}));
|
|
@@ -23143,7 +23817,7 @@ program2.command("next").description("\u83B7\u53D6\u4E0B\u4E00\u4E2A\u5F85\u6267
|
|
|
23143
23817
|
urgency: task.urgency
|
|
23144
23818
|
}, null, 2));
|
|
23145
23819
|
} else {
|
|
23146
|
-
console.log(JSON.stringify({ id: null, message: "No
|
|
23820
|
+
console.log(JSON.stringify({ id: null, message: "No executable tasks" }));
|
|
23147
23821
|
}
|
|
23148
23822
|
}));
|
|
23149
23823
|
program2.command("start").description("\u5F00\u59CB\u6267\u884C\u4EFB\u52A1\uFF08\u6807\u8BB0\u4E3A running\uFF09").requiredOption("--id <id>", "\u4EFB\u52A1 ID").action(async (options) => withDb(async () => {
|
|
@@ -23231,9 +23905,14 @@ program2.command("delete").description("\u5220\u9664\u4EFB\u52A1").requiredOptio
|
|
|
23231
23905
|
console.log(JSON.stringify({ deleted, id: parseInt(options.id) }));
|
|
23232
23906
|
}));
|
|
23233
23907
|
program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u6A21\u677F").addCommand(
|
|
23234
|
-
new Command("add").description("\u521B\u5EFA\u8C03\u5EA6\u6A21\u677F").requiredOption("-n, --name <name>", "\u6A21\u677F\u540D\u79F0").requiredOption("-a, --agent <agent>", "Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").requiredOption("-t, --type <type>", "\u8C03\u5EA6\u7C7B\u578B\uFF1Acron/delayed/recurring").option("--cron <expr>", "cron \u8868\u8FBE\u5F0F\uFF08cron \u7C7B\u578B\u5FC5\u586B\uFF09").option("--delay <duration>", "\u5EF6\u8FDF\u65F6\u95F4\uFF08delayed \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 30s / 5min / 1h / 2d").option("--interval <duration>", "\u5FAA\u73AF\u95F4\u9694\uFF08recurring \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 1h / 30min / 5s").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 1-5", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 1-5", "3").option("--max-instances <number>", "\u6700\u5927\u5E76\u53D1\u5B9E\u4F8B\u6570", "1").option("--max-retries <number>", "\u6700\u5927\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <
|
|
23908
|
+
new Command("add").description("\u521B\u5EFA\u8C03\u5EA6\u6A21\u677F").requiredOption("-n, --name <name>", "\u6A21\u677F\u540D\u79F0").requiredOption("-a, --agent <agent>", "Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").requiredOption("-t, --type <type>", "\u8C03\u5EA6\u7C7B\u578B\uFF1Acron/delayed/recurring").option("--cron <expr>", "cron \u8868\u8FBE\u5F0F\uFF08cron \u7C7B\u578B\u5FC5\u586B\uFF09").option("--delay <duration>", "\u5EF6\u8FDF\u65F6\u95F4\uFF08delayed \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 30s / 5min / 1h / 2d").option("--interval <duration>", "\u5FAA\u73AF\u95F4\u9694\uFF08recurring \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 1h / 30min / 5s").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 1-5", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 1-5", "3").option("-b, --batch <batchId>", "\u6A21\u677F\u751F\u6210\u4EFB\u52A1\u7684\u6279\u6B21 ID").option("--max-instances <number>", "\u6700\u5927\u5E76\u53D1\u5B9E\u4F8B\u6570", "1").option("--max-retries <number>", "\u6700\u5927\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <duration>", "\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min", "30s").option("--timeout <duration>", "\u6BCF\u6B21\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").action(async (options) => withDb(async () => {
|
|
23235
23909
|
let intervalMs = null;
|
|
23236
23910
|
let runAt = null;
|
|
23911
|
+
const retryBackoffMs = parseDuration(options.retryBackoff);
|
|
23912
|
+
const timeoutMs = options.timeout ? parseDuration(options.timeout) : null;
|
|
23913
|
+
if (retryBackoffMs === null || options.timeout && timeoutMs === null) {
|
|
23914
|
+
throw new Error("retry-backoff \u6216 timeout \u683C\u5F0F\u65E0\u6548");
|
|
23915
|
+
}
|
|
23237
23916
|
if (options.interval) {
|
|
23238
23917
|
intervalMs = parseDuration(options.interval);
|
|
23239
23918
|
if (intervalMs === null) {
|
|
@@ -23257,13 +23936,16 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
|
|
|
23257
23936
|
category: options.category,
|
|
23258
23937
|
importance: parseInt(options.importance),
|
|
23259
23938
|
urgency: parseInt(options.urgency),
|
|
23939
|
+
cwd: process.cwd(),
|
|
23940
|
+
batchId: options.batch,
|
|
23260
23941
|
scheduleType: options.type,
|
|
23261
23942
|
cronExpr: options.cron,
|
|
23262
23943
|
intervalMs,
|
|
23263
23944
|
runAt,
|
|
23264
23945
|
maxInstances: parseInt(options.maxInstances),
|
|
23265
23946
|
maxRetries: parseInt(options.maxRetries),
|
|
23266
|
-
retryBackoffMs
|
|
23947
|
+
retryBackoffMs,
|
|
23948
|
+
timeoutMs
|
|
23267
23949
|
});
|
|
23268
23950
|
console.log(JSON.stringify({ id: tmpl.id, status: "created", nextRunAt: tmpl.nextRunAt }, null, 2));
|
|
23269
23951
|
}))
|
|
@@ -23299,36 +23981,29 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
|
|
|
23299
23981
|
}))
|
|
23300
23982
|
);
|
|
23301
23983
|
program2.command("init").description("Initialize SuperTask (create config + run migrations)").action(async () => withDb(async () => {
|
|
23302
|
-
const { existsSync:
|
|
23303
|
-
const {
|
|
23304
|
-
const {
|
|
23305
|
-
const
|
|
23306
|
-
if (!
|
|
23307
|
-
const dir = dirname4(
|
|
23308
|
-
if (!
|
|
23309
|
-
writeFileSync3(
|
|
23984
|
+
const { existsSync: existsSync6, mkdirSync: mkdirSync4, writeFileSync: writeFileSync3 } = await import("fs");
|
|
23985
|
+
const { dirname: dirname4 } = await import("path");
|
|
23986
|
+
const { getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
23987
|
+
const configPath = getConfigPath2();
|
|
23988
|
+
if (!existsSync6(configPath)) {
|
|
23989
|
+
const dir = dirname4(configPath);
|
|
23990
|
+
if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
|
|
23991
|
+
writeFileSync3(configPath, JSON.stringify({
|
|
23992
|
+
configVersion: 2,
|
|
23310
23993
|
worker: { maxConcurrency: 2 },
|
|
23311
23994
|
scheduler: { enabled: true }
|
|
23312
23995
|
}, null, 2) + "\n");
|
|
23313
|
-
console.log(JSON.stringify({ created:
|
|
23996
|
+
console.log(JSON.stringify({ created: configPath }));
|
|
23314
23997
|
} else {
|
|
23315
|
-
console.log(JSON.stringify({ exists:
|
|
23998
|
+
console.log(JSON.stringify({ exists: configPath }));
|
|
23316
23999
|
}
|
|
23317
24000
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db2(), db_exports));
|
|
23318
|
-
|
|
23319
|
-
const { join: pJoin, dirname: pDirname } = await import("path");
|
|
23320
|
-
const { fileURLToPath: fileURLToPath3 } = await import("url");
|
|
23321
|
-
const __dirname2 = pDirname(fileURLToPath3(import.meta.url));
|
|
23322
|
-
migrate2(getDb2(), { migrationsFolder: pJoin(__dirname2, "../../drizzle") });
|
|
24001
|
+
getDb2();
|
|
23323
24002
|
console.log(JSON.stringify({ migrated: true }));
|
|
23324
24003
|
}));
|
|
23325
24004
|
program2.command("migrate").description("Run database migrations").action(async () => withDb(async () => {
|
|
23326
24005
|
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db2(), db_exports));
|
|
23327
|
-
|
|
23328
|
-
const { join: join4, dirname: dirname4 } = await import("path");
|
|
23329
|
-
const { fileURLToPath: fileURLToPath3 } = await import("url");
|
|
23330
|
-
const __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
23331
|
-
migrate2(getDb2(), { migrationsFolder: join4(__dirname2, "../../drizzle") });
|
|
24006
|
+
getDb2();
|
|
23332
24007
|
console.log(JSON.stringify({ migrated: true }));
|
|
23333
24008
|
}));
|
|
23334
24009
|
program2.command("gateway").description("Start the Gateway process (foreground)").action(async () => {
|
|
@@ -23370,26 +24045,20 @@ program2.command("uninstall").description("Stop and remove Gateway pm2 service")
|
|
|
23370
24045
|
process.exit(1);
|
|
23371
24046
|
}
|
|
23372
24047
|
});
|
|
23373
|
-
program2.command("upgrade").description("Update
|
|
23374
|
-
const { execSync: execSync2 } = await import("child_process");
|
|
23375
|
-
const { homedir: homedir4 } = await import("os");
|
|
23376
|
-
const { join: join4 } = await import("path");
|
|
23377
|
-
const configDir = join4(homedir4(), ".config/opencode");
|
|
24048
|
+
program2.command("upgrade").description("Update OpenCode plugin cache and restart Gateway").action(async () => {
|
|
23378
24049
|
console.log("Updating opencode-supertask...");
|
|
24050
|
+
let installed;
|
|
23379
24051
|
try {
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
stdio: "inherit",
|
|
23383
|
-
timeout: 6e4
|
|
23384
|
-
});
|
|
24052
|
+
const { installLatestPlugin: installLatestPlugin2 } = await Promise.resolve().then(() => (init_update2(), update_exports));
|
|
24053
|
+
installed = installLatestPlugin2();
|
|
23385
24054
|
} catch (err) {
|
|
23386
|
-
console.error(
|
|
23387
|
-
console.error("Try manually:
|
|
24055
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
24056
|
+
console.error("Try manually: opencode plugin opencode-supertask@latest --global --force");
|
|
23388
24057
|
process.exit(1);
|
|
23389
24058
|
}
|
|
23390
24059
|
try {
|
|
23391
24060
|
const { upgrade: pm2Upgrade } = await Promise.resolve().then(() => (init_pm2(), pm2_exports));
|
|
23392
|
-
const result = pm2Upgrade();
|
|
24061
|
+
const result = pm2Upgrade(installed);
|
|
23393
24062
|
console.log(`
|
|
23394
24063
|
SuperTask upgraded: ${result.before ?? "unknown"} \u2192 ${result.after}`);
|
|
23395
24064
|
console.log("Gateway restarted. Please restart opencode to load the new plugin.");
|