opencode-supertask 0.1.20 → 0.1.22
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 +1374 -581
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.d.ts +3 -1
- package/dist/gateway/index.js +878 -296
- package/dist/gateway/index.js.map +1 -1
- package/dist/plugin/supertask.js +593 -232
- package/dist/plugin/supertask.js.map +1 -1
- package/dist/web/index.js +554 -212
- package/dist/web/index.js.map +1 -1
- package/dist/worker/index.d.ts +19 -7
- package/dist/worker/index.js +442 -151
- 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/gateway/index.js
CHANGED
|
@@ -1358,8 +1358,8 @@ function haveSameKeys(left, right) {
|
|
|
1358
1358
|
if (leftKeys.length !== rightKeys.length) {
|
|
1359
1359
|
return false;
|
|
1360
1360
|
}
|
|
1361
|
-
for (const [
|
|
1362
|
-
if (key !== rightKeys[
|
|
1361
|
+
for (const [index2, key] of leftKeys.entries()) {
|
|
1362
|
+
if (key !== rightKeys[index2]) {
|
|
1363
1363
|
return false;
|
|
1364
1364
|
}
|
|
1365
1365
|
}
|
|
@@ -3220,8 +3220,8 @@ var init_dialect = __esm({
|
|
|
3220
3220
|
}
|
|
3221
3221
|
const joinsArray = [];
|
|
3222
3222
|
if (joins) {
|
|
3223
|
-
for (const [
|
|
3224
|
-
if (
|
|
3223
|
+
for (const [index2, joinMeta] of joins.entries()) {
|
|
3224
|
+
if (index2 === 0) {
|
|
3225
3225
|
joinsArray.push(sql` `);
|
|
3226
3226
|
}
|
|
3227
3227
|
const table = joinMeta.table;
|
|
@@ -3238,7 +3238,7 @@ var init_dialect = __esm({
|
|
|
3238
3238
|
sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`
|
|
3239
3239
|
);
|
|
3240
3240
|
}
|
|
3241
|
-
if (
|
|
3241
|
+
if (index2 < joins.length - 1) {
|
|
3242
3242
|
joinsArray.push(sql` `);
|
|
3243
3243
|
}
|
|
3244
3244
|
}
|
|
@@ -3251,9 +3251,9 @@ var init_dialect = __esm({
|
|
|
3251
3251
|
buildOrderBy(orderBy) {
|
|
3252
3252
|
const orderByList = [];
|
|
3253
3253
|
if (orderBy) {
|
|
3254
|
-
for (const [
|
|
3254
|
+
for (const [index2, orderByValue] of orderBy.entries()) {
|
|
3255
3255
|
orderByList.push(orderByValue);
|
|
3256
|
-
if (
|
|
3256
|
+
if (index2 < orderBy.length - 1) {
|
|
3257
3257
|
orderByList.push(sql`, `);
|
|
3258
3258
|
}
|
|
3259
3259
|
}
|
|
@@ -3302,9 +3302,9 @@ var init_dialect = __esm({
|
|
|
3302
3302
|
const havingSql = having ? sql` having ${having}` : void 0;
|
|
3303
3303
|
const groupByList = [];
|
|
3304
3304
|
if (groupBy) {
|
|
3305
|
-
for (const [
|
|
3305
|
+
for (const [index2, groupByValue] of groupBy.entries()) {
|
|
3306
3306
|
groupByList.push(groupByValue);
|
|
3307
|
-
if (
|
|
3307
|
+
if (index2 < groupBy.length - 1) {
|
|
3308
3308
|
groupByList.push(sql`, `);
|
|
3309
3309
|
}
|
|
3310
3310
|
}
|
|
@@ -5393,6 +5393,9 @@ var init_checks = __esm({
|
|
|
5393
5393
|
});
|
|
5394
5394
|
|
|
5395
5395
|
// node_modules/drizzle-orm/sqlite-core/indexes.js
|
|
5396
|
+
function index(name) {
|
|
5397
|
+
return new IndexBuilderOn(name, false);
|
|
5398
|
+
}
|
|
5396
5399
|
var IndexBuilderOn, IndexBuilder, Index;
|
|
5397
5400
|
var init_indexes = __esm({
|
|
5398
5401
|
"node_modules/drizzle-orm/sqlite-core/indexes.js"() {
|
|
@@ -6021,12 +6024,17 @@ var init_schema = __esm({
|
|
|
6021
6024
|
resultLog: text("result_log"),
|
|
6022
6025
|
retryCount: integer("retry_count").default(0),
|
|
6023
6026
|
maxRetries: integer("max_retries").default(3),
|
|
6027
|
+
retryBackoffMs: integer("retry_backoff_ms").default(3e4),
|
|
6024
6028
|
// Gateway 扩展字段(毫秒)
|
|
6025
6029
|
retryAfter: integer("retry_after"),
|
|
6026
6030
|
timeoutMs: integer("timeout_ms"),
|
|
6027
6031
|
templateId: integer("template_id"),
|
|
6028
6032
|
scheduledAt: integer("scheduled_at")
|
|
6029
|
-
})
|
|
6033
|
+
}, (table) => [
|
|
6034
|
+
index("tasks_queue_idx").on(table.status, table.retryAfter, table.urgency, table.importance, table.createdAt, table.id),
|
|
6035
|
+
index("tasks_batch_status_idx").on(table.batchId, table.status),
|
|
6036
|
+
index("tasks_template_status_idx").on(table.templateId, table.status)
|
|
6037
|
+
]);
|
|
6030
6038
|
TASK_CATEGORIES = [
|
|
6031
6039
|
"translate",
|
|
6032
6040
|
"generate",
|
|
@@ -6036,7 +6044,7 @@ var init_schema = __esm({
|
|
|
6036
6044
|
];
|
|
6037
6045
|
taskRuns = sqliteTable("task_runs", {
|
|
6038
6046
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
6039
|
-
taskId: integer("task_id").notNull().references(() => tasks.id),
|
|
6047
|
+
taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
|
|
6040
6048
|
sessionId: text("session_id"),
|
|
6041
6049
|
model: text("model"),
|
|
6042
6050
|
status: text("status").default("running"),
|
|
@@ -6049,7 +6057,10 @@ var init_schema = __esm({
|
|
|
6049
6057
|
heartbeatAt: integer("heartbeat_at"),
|
|
6050
6058
|
workerPid: integer("worker_pid"),
|
|
6051
6059
|
childPid: integer("child_pid")
|
|
6052
|
-
})
|
|
6060
|
+
}, (table) => [
|
|
6061
|
+
index("task_runs_task_started_idx").on(table.taskId, table.startedAt, table.id),
|
|
6062
|
+
index("task_runs_status_heartbeat_idx").on(table.status, table.heartbeatAt)
|
|
6063
|
+
]);
|
|
6053
6064
|
taskTemplates = sqliteTable("task_templates", {
|
|
6054
6065
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
6055
6066
|
name: text("name").notNull(),
|
|
@@ -6060,6 +6071,7 @@ var init_schema = __esm({
|
|
|
6060
6071
|
category: text("category").default("general"),
|
|
6061
6072
|
importance: integer("importance").default(3),
|
|
6062
6073
|
urgency: integer("urgency").default(3),
|
|
6074
|
+
batchId: text("batch_id"),
|
|
6063
6075
|
scheduleType: text("schedule_type").notNull(),
|
|
6064
6076
|
cronExpr: text("cron_expr"),
|
|
6065
6077
|
intervalMs: integer("interval_ms"),
|
|
@@ -6067,12 +6079,15 @@ var init_schema = __esm({
|
|
|
6067
6079
|
maxInstances: integer("max_instances").default(1),
|
|
6068
6080
|
maxRetries: integer("max_retries").default(3),
|
|
6069
6081
|
retryBackoffMs: integer("retry_backoff_ms").default(3e4),
|
|
6082
|
+
timeoutMs: integer("timeout_ms"),
|
|
6070
6083
|
lastRunAt: integer("last_run_at"),
|
|
6071
6084
|
nextRunAt: integer("next_run_at"),
|
|
6072
6085
|
enabled: integer("enabled", { mode: "boolean" }).default(true),
|
|
6073
6086
|
createdAt: integer("created_at").default(0),
|
|
6074
6087
|
updatedAt: integer("updated_at").default(0)
|
|
6075
|
-
})
|
|
6088
|
+
}, (table) => [
|
|
6089
|
+
index("task_templates_due_idx").on(table.enabled, table.nextRunAt, table.id)
|
|
6090
|
+
]);
|
|
6076
6091
|
}
|
|
6077
6092
|
});
|
|
6078
6093
|
|
|
@@ -6107,16 +6122,30 @@ function initDb() {
|
|
|
6107
6122
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
6108
6123
|
pid INTEGER NOT NULL,
|
|
6109
6124
|
acquired_at INTEGER NOT NULL,
|
|
6110
|
-
heartbeat_at INTEGER NOT NULL
|
|
6125
|
+
heartbeat_at INTEGER NOT NULL,
|
|
6126
|
+
ready_at INTEGER
|
|
6111
6127
|
);
|
|
6112
6128
|
`);
|
|
6129
|
+
const gatewayLockColumns = _sqlite.query("PRAGMA table_info(gateway_lock)").all();
|
|
6130
|
+
if (!gatewayLockColumns.some((column) => column.name === "ready_at")) {
|
|
6131
|
+
_sqlite.exec("ALTER TABLE gateway_lock ADD COLUMN ready_at INTEGER;");
|
|
6132
|
+
}
|
|
6113
6133
|
_db = drizzle(_sqlite, { schema: schema_exports });
|
|
6114
6134
|
if (!_migrationRan) {
|
|
6115
|
-
_migrationRan = true;
|
|
6116
6135
|
try {
|
|
6117
6136
|
migrate(_db, { migrationsFolder: getMigrationsFolder() });
|
|
6137
|
+
_sqlite.exec("PRAGMA foreign_keys = ON;");
|
|
6138
|
+
const violations = _sqlite.query("PRAGMA foreign_key_check;").all();
|
|
6139
|
+
if (violations.length > 0) {
|
|
6140
|
+
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`);
|
|
6141
|
+
}
|
|
6142
|
+
_migrationRan = true;
|
|
6118
6143
|
} catch (err) {
|
|
6119
6144
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6145
|
+
_migrationRan = false;
|
|
6146
|
+
_sqlite.close();
|
|
6147
|
+
_sqlite = null;
|
|
6148
|
+
_db = null;
|
|
6120
6149
|
console.error(`[supertask] migration failed: ${msg}`);
|
|
6121
6150
|
throw new Error(`[supertask] DB migration failed: ${msg}`);
|
|
6122
6151
|
}
|
|
@@ -6136,6 +6165,7 @@ function closeDb() {
|
|
|
6136
6165
|
_sqlite.close();
|
|
6137
6166
|
_sqlite = null;
|
|
6138
6167
|
_db = null;
|
|
6168
|
+
_migrationRan = false;
|
|
6139
6169
|
}
|
|
6140
6170
|
}
|
|
6141
6171
|
var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
|
|
@@ -6170,71 +6200,123 @@ var init_db2 = __esm({
|
|
|
6170
6200
|
});
|
|
6171
6201
|
|
|
6172
6202
|
// src/gateway/config.ts
|
|
6173
|
-
import {
|
|
6203
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
6174
6204
|
import { homedir as homedir2 } from "os";
|
|
6175
6205
|
import { join as join2 } from "path";
|
|
6176
|
-
function
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
result[key] = deepMerge(baseVal, val);
|
|
6184
|
-
continue;
|
|
6185
|
-
}
|
|
6186
|
-
}
|
|
6187
|
-
if (val !== void 0) {
|
|
6188
|
-
result[key] = val;
|
|
6189
|
-
}
|
|
6206
|
+
function getConfigPath() {
|
|
6207
|
+
return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
|
|
6208
|
+
}
|
|
6209
|
+
function objectAt(value, path) {
|
|
6210
|
+
if (value === void 0) return {};
|
|
6211
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
6212
|
+
throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
6190
6213
|
}
|
|
6191
|
-
return
|
|
6214
|
+
return value;
|
|
6192
6215
|
}
|
|
6193
|
-
function
|
|
6194
|
-
|
|
6195
|
-
|
|
6216
|
+
function integerAt(source, key, fallback, min, max, path) {
|
|
6217
|
+
const value = source[key];
|
|
6218
|
+
if (value === void 0) return fallback;
|
|
6219
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
6220
|
+
throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
6196
6221
|
}
|
|
6222
|
+
return value;
|
|
6223
|
+
}
|
|
6224
|
+
function booleanAt(source, key, fallback, path) {
|
|
6225
|
+
const value = source[key];
|
|
6226
|
+
if (value === void 0) return fallback;
|
|
6227
|
+
if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
6228
|
+
return value;
|
|
6229
|
+
}
|
|
6230
|
+
function validateConfig(input) {
|
|
6231
|
+
const root = objectAt(input, "config");
|
|
6232
|
+
const version2 = root.configVersion ?? 1;
|
|
6233
|
+
if (version2 !== 1 && version2 !== 2) {
|
|
6234
|
+
throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
|
|
6235
|
+
}
|
|
6236
|
+
const worker = objectAt(root.worker, "worker");
|
|
6237
|
+
const scheduler = objectAt(root.scheduler, "scheduler");
|
|
6238
|
+
const watchdog = objectAt(root.watchdog, "watchdog");
|
|
6239
|
+
const dashboard = objectAt(root.dashboard, "dashboard");
|
|
6240
|
+
const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
|
|
6241
|
+
const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
|
|
6242
|
+
let checkIntervalMs;
|
|
6243
|
+
let cleanupIntervalMs;
|
|
6244
|
+
if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
|
|
6245
|
+
checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
|
|
6246
|
+
cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
|
|
6247
|
+
} else {
|
|
6248
|
+
checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
|
|
6249
|
+
cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
|
|
6250
|
+
}
|
|
6251
|
+
if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
|
|
6252
|
+
throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
|
|
6253
|
+
}
|
|
6254
|
+
if (checkIntervalMs > heartbeatTimeoutMs) {
|
|
6255
|
+
throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
|
|
6256
|
+
}
|
|
6257
|
+
return {
|
|
6258
|
+
configVersion: 2,
|
|
6259
|
+
worker: {
|
|
6260
|
+
maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
|
|
6261
|
+
pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
|
|
6262
|
+
heartbeatIntervalMs,
|
|
6263
|
+
taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
|
|
6264
|
+
shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
|
|
6265
|
+
},
|
|
6266
|
+
scheduler: {
|
|
6267
|
+
enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
|
|
6268
|
+
checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
|
|
6269
|
+
},
|
|
6270
|
+
watchdog: {
|
|
6271
|
+
heartbeatTimeoutMs,
|
|
6272
|
+
checkIntervalMs,
|
|
6273
|
+
cleanupIntervalMs,
|
|
6274
|
+
retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
|
|
6275
|
+
},
|
|
6276
|
+
dashboard: {
|
|
6277
|
+
enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
|
|
6278
|
+
port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
|
|
6279
|
+
}
|
|
6280
|
+
};
|
|
6281
|
+
}
|
|
6282
|
+
function loadConfig(path = getConfigPath()) {
|
|
6283
|
+
if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
|
|
6197
6284
|
try {
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6203
|
-
console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
|
|
6204
|
-
return DEFAULT_CONFIG;
|
|
6285
|
+
return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
6286
|
+
} catch (error) {
|
|
6287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6288
|
+
throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
|
|
6205
6289
|
}
|
|
6206
6290
|
}
|
|
6207
|
-
var DEFAULT_CONFIG,
|
|
6291
|
+
var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
|
|
6208
6292
|
var init_config = __esm({
|
|
6209
6293
|
"src/gateway/config.ts"() {
|
|
6210
6294
|
"use strict";
|
|
6211
6295
|
DEFAULT_CONFIG = {
|
|
6296
|
+
configVersion: 2,
|
|
6212
6297
|
worker: {
|
|
6213
6298
|
maxConcurrency: 2,
|
|
6214
6299
|
pollIntervalMs: 1e3,
|
|
6215
6300
|
heartbeatIntervalMs: 3e4,
|
|
6216
|
-
taskTimeoutMs: 18e5
|
|
6301
|
+
taskTimeoutMs: 18e5,
|
|
6302
|
+
shutdownGracePeriodMs: 3e4
|
|
6217
6303
|
},
|
|
6218
6304
|
scheduler: {
|
|
6219
6305
|
enabled: true,
|
|
6220
|
-
checkIntervalMs: 1e3
|
|
6221
|
-
catchUp: "next"
|
|
6306
|
+
checkIntervalMs: 1e3
|
|
6222
6307
|
},
|
|
6223
6308
|
watchdog: {
|
|
6224
6309
|
heartbeatTimeoutMs: 6e5,
|
|
6225
|
-
|
|
6310
|
+
checkIntervalMs: 6e4,
|
|
6311
|
+
cleanupIntervalMs: 864e5,
|
|
6226
6312
|
retentionDays: 30
|
|
6227
6313
|
},
|
|
6228
6314
|
dashboard: {
|
|
6229
6315
|
enabled: true,
|
|
6230
6316
|
port: 4680
|
|
6231
|
-
},
|
|
6232
|
-
logging: {
|
|
6233
|
-
level: "info",
|
|
6234
|
-
format: "json"
|
|
6235
6317
|
}
|
|
6236
6318
|
};
|
|
6237
|
-
|
|
6319
|
+
DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
|
|
6238
6320
|
}
|
|
6239
6321
|
});
|
|
6240
6322
|
|
|
@@ -6288,14 +6370,14 @@ var init_backoff = __esm({
|
|
|
6288
6370
|
});
|
|
6289
6371
|
|
|
6290
6372
|
// src/core/services/task.service.ts
|
|
6291
|
-
var tasks2, TaskService;
|
|
6373
|
+
var tasks2, taskRuns2, TaskService;
|
|
6292
6374
|
var init_task_service = __esm({
|
|
6293
6375
|
"src/core/services/task.service.ts"() {
|
|
6294
6376
|
"use strict";
|
|
6295
6377
|
init_db2();
|
|
6296
6378
|
init_drizzle_orm();
|
|
6297
6379
|
init_backoff();
|
|
6298
|
-
({ tasks: tasks2 } = schema_exports);
|
|
6380
|
+
({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
|
|
6299
6381
|
TaskService = class {
|
|
6300
6382
|
static buildScopeWhere(scope) {
|
|
6301
6383
|
const conditions = [];
|
|
@@ -6305,9 +6387,27 @@ var init_task_service = __esm({
|
|
|
6305
6387
|
return conditions;
|
|
6306
6388
|
}
|
|
6307
6389
|
static async add(data) {
|
|
6390
|
+
this.validateNewTask(data);
|
|
6308
6391
|
const result = await db.insert(tasks2).values(data).returning();
|
|
6309
6392
|
return result[0];
|
|
6310
6393
|
}
|
|
6394
|
+
static validateNewTask(data) {
|
|
6395
|
+
if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
|
|
6396
|
+
if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
|
|
6397
|
+
if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
|
|
6398
|
+
this.validateInteger("importance", data.importance, 1, 5);
|
|
6399
|
+
this.validateInteger("urgency", data.urgency, 1, 5);
|
|
6400
|
+
this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
|
|
6401
|
+
this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
|
|
6402
|
+
this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
|
|
6403
|
+
this.validateInteger("dependsOn", data.dependsOn, 1, Number.MAX_SAFE_INTEGER);
|
|
6404
|
+
}
|
|
6405
|
+
static validateInteger(name, value, min, max) {
|
|
6406
|
+
if (value === void 0 || value === null) return;
|
|
6407
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
6408
|
+
throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6311
6411
|
static async next(scope = {}) {
|
|
6312
6412
|
const baseConditions = [...this.buildScopeWhere(scope)];
|
|
6313
6413
|
const nowMs = Date.now();
|
|
@@ -6330,7 +6430,7 @@ var init_task_service = __esm({
|
|
|
6330
6430
|
),
|
|
6331
6431
|
and(
|
|
6332
6432
|
eq(tasks2.status, "failed"),
|
|
6333
|
-
sql`${tasks2.retryCount}
|
|
6433
|
+
sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
|
|
6334
6434
|
retryAfterFilter
|
|
6335
6435
|
)
|
|
6336
6436
|
);
|
|
@@ -6344,7 +6444,8 @@ var init_task_service = __esm({
|
|
|
6344
6444
|
const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
|
|
6345
6445
|
desc(tasks2.urgency),
|
|
6346
6446
|
desc(tasks2.importance),
|
|
6347
|
-
asc(tasks2.createdAt)
|
|
6447
|
+
asc(tasks2.createdAt),
|
|
6448
|
+
asc(tasks2.id)
|
|
6348
6449
|
);
|
|
6349
6450
|
for (const task of allTasks) {
|
|
6350
6451
|
if (task.dependsOn) {
|
|
@@ -6365,7 +6466,7 @@ var init_task_service = __esm({
|
|
|
6365
6466
|
eq(tasks2.status, "pending"),
|
|
6366
6467
|
and(
|
|
6367
6468
|
eq(tasks2.status, "failed"),
|
|
6368
|
-
sql`${tasks2.retryCount}
|
|
6469
|
+
sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
|
|
6369
6470
|
)
|
|
6370
6471
|
),
|
|
6371
6472
|
...this.buildScopeWhere(scope)
|
|
@@ -6378,7 +6479,11 @@ var init_task_service = __esm({
|
|
|
6378
6479
|
return result[0] || null;
|
|
6379
6480
|
}
|
|
6380
6481
|
static async done(id, log, scope = {}) {
|
|
6381
|
-
const conditions = [
|
|
6482
|
+
const conditions = [
|
|
6483
|
+
eq(tasks2.id, id),
|
|
6484
|
+
eq(tasks2.status, "running"),
|
|
6485
|
+
...this.buildScopeWhere(scope)
|
|
6486
|
+
];
|
|
6382
6487
|
const result = await db.update(tasks2).set({
|
|
6383
6488
|
status: "done",
|
|
6384
6489
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
@@ -6389,17 +6494,24 @@ var init_task_service = __esm({
|
|
|
6389
6494
|
}
|
|
6390
6495
|
static async fail(id, log, scope = {}, options) {
|
|
6391
6496
|
const current = await this.getById(id, scope);
|
|
6392
|
-
if (!current) return null;
|
|
6497
|
+
if (!current || current.status !== "running") return null;
|
|
6393
6498
|
const newRetryCount = (current.retryCount ?? 0) + 1;
|
|
6394
6499
|
const maxRetries = current.maxRetries ?? 3;
|
|
6395
|
-
const isDeadLetter = options?.setDeadLetter ?? newRetryCount
|
|
6396
|
-
const conditions = [
|
|
6500
|
+
const isDeadLetter = options?.setDeadLetter ?? newRetryCount > maxRetries;
|
|
6501
|
+
const conditions = [
|
|
6502
|
+
eq(tasks2.id, id),
|
|
6503
|
+
eq(tasks2.status, "running"),
|
|
6504
|
+
...this.buildScopeWhere(scope)
|
|
6505
|
+
];
|
|
6397
6506
|
const result = await db.update(tasks2).set({
|
|
6398
6507
|
status: isDeadLetter ? "dead_letter" : "failed",
|
|
6399
6508
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6400
6509
|
resultLog: log,
|
|
6401
6510
|
retryCount: newRetryCount,
|
|
6402
|
-
retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
|
|
6511
|
+
retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
|
|
6512
|
+
newRetryCount,
|
|
6513
|
+
current.retryBackoffMs ?? 3e4
|
|
6514
|
+
)
|
|
6403
6515
|
}).where(and(...conditions)).returning();
|
|
6404
6516
|
return result[0] || null;
|
|
6405
6517
|
}
|
|
@@ -6431,9 +6543,38 @@ var init_task_service = __esm({
|
|
|
6431
6543
|
).returning();
|
|
6432
6544
|
return result.length;
|
|
6433
6545
|
}
|
|
6546
|
+
static async resetOrphanRunningToPending() {
|
|
6547
|
+
const result = await db.update(tasks2).set({
|
|
6548
|
+
status: "pending",
|
|
6549
|
+
startedAt: null,
|
|
6550
|
+
finishedAt: null
|
|
6551
|
+
}).where(
|
|
6552
|
+
and(
|
|
6553
|
+
eq(tasks2.status, "running"),
|
|
6554
|
+
sql`NOT EXISTS (
|
|
6555
|
+
SELECT 1 FROM ${taskRuns2}
|
|
6556
|
+
WHERE ${taskRuns2.taskId} = ${tasks2.id}
|
|
6557
|
+
AND ${taskRuns2.status} = 'running'
|
|
6558
|
+
)`
|
|
6559
|
+
)
|
|
6560
|
+
).returning();
|
|
6561
|
+
return result.length;
|
|
6562
|
+
}
|
|
6434
6563
|
static async cancel(id, scope = {}) {
|
|
6435
|
-
const conditions = [
|
|
6436
|
-
|
|
6564
|
+
const conditions = [
|
|
6565
|
+
eq(tasks2.id, id),
|
|
6566
|
+
or(
|
|
6567
|
+
eq(tasks2.status, "pending"),
|
|
6568
|
+
eq(tasks2.status, "running"),
|
|
6569
|
+
eq(tasks2.status, "failed")
|
|
6570
|
+
),
|
|
6571
|
+
...this.buildScopeWhere(scope)
|
|
6572
|
+
];
|
|
6573
|
+
const result = await db.update(tasks2).set({
|
|
6574
|
+
status: "cancelled",
|
|
6575
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
6576
|
+
retryAfter: null
|
|
6577
|
+
}).where(and(...conditions)).returning();
|
|
6437
6578
|
return result[0] || null;
|
|
6438
6579
|
}
|
|
6439
6580
|
static async retry(id, scope = {}) {
|
|
@@ -6445,7 +6586,8 @@ var init_task_service = __esm({
|
|
|
6445
6586
|
status: "pending",
|
|
6446
6587
|
startedAt: null,
|
|
6447
6588
|
finishedAt: null,
|
|
6448
|
-
retryAfter: null
|
|
6589
|
+
retryAfter: null,
|
|
6590
|
+
retryCount: 0
|
|
6449
6591
|
}).where(and(...conditions)).returning();
|
|
6450
6592
|
return result[0] || null;
|
|
6451
6593
|
}
|
|
@@ -6459,7 +6601,8 @@ var init_task_service = __esm({
|
|
|
6459
6601
|
status: "pending",
|
|
6460
6602
|
startedAt: null,
|
|
6461
6603
|
finishedAt: null,
|
|
6462
|
-
retryAfter: null
|
|
6604
|
+
retryAfter: null,
|
|
6605
|
+
retryCount: 0
|
|
6463
6606
|
}).where(and(...conditions)).returning();
|
|
6464
6607
|
return result.length;
|
|
6465
6608
|
}
|
|
@@ -6527,6 +6670,9 @@ var init_task_service = __esm({
|
|
|
6527
6670
|
}
|
|
6528
6671
|
static async delete(id, scope = {}) {
|
|
6529
6672
|
const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
|
|
6673
|
+
const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
|
|
6674
|
+
if (!existing[0]) return false;
|
|
6675
|
+
await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
|
|
6530
6676
|
const result = await db.delete(tasks2).where(and(...conditions)).returning();
|
|
6531
6677
|
return result.length > 0;
|
|
6532
6678
|
}
|
|
@@ -6546,65 +6692,65 @@ var init_task_service = __esm({
|
|
|
6546
6692
|
});
|
|
6547
6693
|
|
|
6548
6694
|
// src/core/services/task-run.service.ts
|
|
6549
|
-
var
|
|
6695
|
+
var taskRuns3, TaskRunService;
|
|
6550
6696
|
var init_task_run_service = __esm({
|
|
6551
6697
|
"src/core/services/task-run.service.ts"() {
|
|
6552
6698
|
"use strict";
|
|
6553
6699
|
init_db2();
|
|
6554
6700
|
init_drizzle_orm();
|
|
6555
|
-
({ taskRuns:
|
|
6701
|
+
({ taskRuns: taskRuns3 } = schema_exports);
|
|
6556
6702
|
TaskRunService = class {
|
|
6557
6703
|
static async create(data) {
|
|
6558
|
-
const result = await db.insert(
|
|
6704
|
+
const result = await db.insert(taskRuns3).values(data).returning();
|
|
6559
6705
|
return result[0];
|
|
6560
6706
|
}
|
|
6561
6707
|
static async updateSessionId(id, sessionId) {
|
|
6562
|
-
const result = await db.update(
|
|
6708
|
+
const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6563
6709
|
return result[0] || null;
|
|
6564
6710
|
}
|
|
6565
6711
|
static async done(id, log) {
|
|
6566
|
-
const result = await db.update(
|
|
6712
|
+
const result = await db.update(taskRuns3).set({
|
|
6567
6713
|
status: "done",
|
|
6568
6714
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6569
6715
|
log
|
|
6570
|
-
}).where(eq(
|
|
6716
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6571
6717
|
return result[0] || null;
|
|
6572
6718
|
}
|
|
6573
6719
|
static async fail(id, log) {
|
|
6574
|
-
const result = await db.update(
|
|
6720
|
+
const result = await db.update(taskRuns3).set({
|
|
6575
6721
|
status: "failed",
|
|
6576
6722
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6577
6723
|
log
|
|
6578
|
-
}).where(eq(
|
|
6724
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6579
6725
|
return result[0] || null;
|
|
6580
6726
|
}
|
|
6581
6727
|
static async heartbeat(id) {
|
|
6582
|
-
const result = await db.update(
|
|
6728
|
+
const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6583
6729
|
return result[0] || null;
|
|
6584
6730
|
}
|
|
6585
6731
|
static async updatePid(id, workerPid, childPid) {
|
|
6586
|
-
const result = await db.update(
|
|
6732
|
+
const result = await db.update(taskRuns3).set({
|
|
6587
6733
|
workerPid,
|
|
6588
6734
|
childPid,
|
|
6589
6735
|
lockedAt: Date.now(),
|
|
6590
6736
|
lockedBy: `gateway-${process.pid}`
|
|
6591
|
-
}).where(eq(
|
|
6737
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6592
6738
|
return result[0] || null;
|
|
6593
6739
|
}
|
|
6594
6740
|
static async getById(id) {
|
|
6595
|
-
const result = await db.select().from(
|
|
6741
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
|
|
6596
6742
|
return result[0] || null;
|
|
6597
6743
|
}
|
|
6598
6744
|
static async listByTaskId(taskId) {
|
|
6599
|
-
return await db.select().from(
|
|
6745
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
6600
6746
|
}
|
|
6601
6747
|
static async getLatestByTaskId(taskId) {
|
|
6602
|
-
const result = await db.select().from(
|
|
6748
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
|
|
6603
6749
|
return result[0] || null;
|
|
6604
6750
|
}
|
|
6605
6751
|
static async getLatestByTaskIds(taskIds) {
|
|
6606
6752
|
if (taskIds.length === 0) return /* @__PURE__ */ new Map();
|
|
6607
|
-
const latestRuns = await db.select().from(
|
|
6753
|
+
const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
6608
6754
|
const result = /* @__PURE__ */ new Map();
|
|
6609
6755
|
for (const run of latestRuns) {
|
|
6610
6756
|
if (!result.has(run.taskId)) {
|
|
@@ -6618,22 +6764,23 @@ var init_task_run_service = __esm({
|
|
|
6618
6764
|
const cutoffSec = Math.floor(cutoffMs / 1e3);
|
|
6619
6765
|
const { tasks: tasksTable } = schema_exports;
|
|
6620
6766
|
const result = await db.select({
|
|
6621
|
-
runId:
|
|
6622
|
-
taskId:
|
|
6623
|
-
childPid:
|
|
6767
|
+
runId: taskRuns3.id,
|
|
6768
|
+
taskId: taskRuns3.taskId,
|
|
6769
|
+
childPid: taskRuns3.childPid,
|
|
6624
6770
|
taskRetryCount: tasksTable.retryCount,
|
|
6625
|
-
taskMaxRetries: tasksTable.maxRetries
|
|
6626
|
-
|
|
6771
|
+
taskMaxRetries: tasksTable.maxRetries,
|
|
6772
|
+
taskRetryBackoffMs: tasksTable.retryBackoffMs
|
|
6773
|
+
}).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
|
|
6627
6774
|
and(
|
|
6628
|
-
eq(
|
|
6775
|
+
eq(taskRuns3.status, "running"),
|
|
6629
6776
|
or(
|
|
6630
6777
|
and(
|
|
6631
|
-
sql`${
|
|
6632
|
-
sql`${
|
|
6778
|
+
sql`${taskRuns3.heartbeatAt} IS NULL`,
|
|
6779
|
+
sql`${taskRuns3.startedAt} < ${cutoffSec}`
|
|
6633
6780
|
),
|
|
6634
6781
|
and(
|
|
6635
|
-
sql`${
|
|
6636
|
-
sql`${
|
|
6782
|
+
sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
|
|
6783
|
+
sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
|
|
6637
6784
|
)
|
|
6638
6785
|
)
|
|
6639
6786
|
)
|
|
@@ -6643,25 +6790,111 @@ var init_task_run_service = __esm({
|
|
|
6643
6790
|
taskId: row.taskId,
|
|
6644
6791
|
childPid: row.childPid,
|
|
6645
6792
|
taskRetryCount: row.taskRetryCount ?? 0,
|
|
6646
|
-
taskMaxRetries: row.taskMaxRetries ?? 3
|
|
6793
|
+
taskMaxRetries: row.taskMaxRetries ?? 3,
|
|
6794
|
+
taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
|
|
6647
6795
|
}));
|
|
6648
6796
|
}
|
|
6649
6797
|
static async getRunningRunByTaskId(taskId) {
|
|
6650
|
-
const result = await db.select().from(
|
|
6798
|
+
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);
|
|
6651
6799
|
return result[0] || null;
|
|
6652
6800
|
}
|
|
6653
6801
|
static async deleteByTaskIds(taskIds) {
|
|
6654
6802
|
if (taskIds.length === 0) return 0;
|
|
6655
|
-
const result = await db.delete(
|
|
6803
|
+
const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
|
|
6656
6804
|
return result.length;
|
|
6657
6805
|
}
|
|
6658
6806
|
static async getAllRunningRuns() {
|
|
6659
|
-
return await db.select().from(
|
|
6807
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
|
|
6660
6808
|
}
|
|
6661
6809
|
};
|
|
6662
6810
|
}
|
|
6663
6811
|
});
|
|
6664
6812
|
|
|
6813
|
+
// src/gateway/health.ts
|
|
6814
|
+
function initializeGatewayHealth(config) {
|
|
6815
|
+
const now = Date.now();
|
|
6816
|
+
state = {
|
|
6817
|
+
startedAt: now,
|
|
6818
|
+
config,
|
|
6819
|
+
lastActivityAt: {
|
|
6820
|
+
worker: now,
|
|
6821
|
+
scheduler: now,
|
|
6822
|
+
watchdog: now
|
|
6823
|
+
}
|
|
6824
|
+
};
|
|
6825
|
+
}
|
|
6826
|
+
function markGatewayActivity(component) {
|
|
6827
|
+
if (state) state.lastActivityAt[component] = Date.now();
|
|
6828
|
+
}
|
|
6829
|
+
function resetGatewayHealth() {
|
|
6830
|
+
state = null;
|
|
6831
|
+
}
|
|
6832
|
+
function componentStatus(component, enabled, maxAgeMs, now) {
|
|
6833
|
+
const lastActivityAt = state?.lastActivityAt[component] ?? null;
|
|
6834
|
+
const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
|
|
6835
|
+
return {
|
|
6836
|
+
enabled,
|
|
6837
|
+
healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
|
|
6838
|
+
lastActivityAt,
|
|
6839
|
+
ageMs,
|
|
6840
|
+
maxAgeMs
|
|
6841
|
+
};
|
|
6842
|
+
}
|
|
6843
|
+
function getGatewayHealth(now = Date.now()) {
|
|
6844
|
+
const worker = componentStatus(
|
|
6845
|
+
"worker",
|
|
6846
|
+
true,
|
|
6847
|
+
Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
|
|
6848
|
+
now
|
|
6849
|
+
);
|
|
6850
|
+
const scheduler = componentStatus(
|
|
6851
|
+
"scheduler",
|
|
6852
|
+
state?.config.schedulerEnabled ?? false,
|
|
6853
|
+
Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
|
|
6854
|
+
now
|
|
6855
|
+
);
|
|
6856
|
+
const watchdog = componentStatus(
|
|
6857
|
+
"watchdog",
|
|
6858
|
+
true,
|
|
6859
|
+
Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
|
|
6860
|
+
now
|
|
6861
|
+
);
|
|
6862
|
+
let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
|
|
6863
|
+
try {
|
|
6864
|
+
const row = sqlite.prepare(
|
|
6865
|
+
"SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
|
|
6866
|
+
).get();
|
|
6867
|
+
if (row) {
|
|
6868
|
+
const ageMs = Math.max(0, now - row.heartbeat_at);
|
|
6869
|
+
lock = {
|
|
6870
|
+
pid: row.pid,
|
|
6871
|
+
heartbeatAt: row.heartbeat_at,
|
|
6872
|
+
readyAt: row.ready_at,
|
|
6873
|
+
ageMs,
|
|
6874
|
+
healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
|
|
6875
|
+
};
|
|
6876
|
+
}
|
|
6877
|
+
} catch {
|
|
6878
|
+
}
|
|
6879
|
+
const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
|
|
6880
|
+
return {
|
|
6881
|
+
status: healthy ? "ok" : "degraded",
|
|
6882
|
+
pid: process.pid,
|
|
6883
|
+
uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
|
|
6884
|
+
lock,
|
|
6885
|
+
components: { worker, scheduler, watchdog }
|
|
6886
|
+
};
|
|
6887
|
+
}
|
|
6888
|
+
var LOCK_STALE_MS, state;
|
|
6889
|
+
var init_health = __esm({
|
|
6890
|
+
"src/gateway/health.ts"() {
|
|
6891
|
+
"use strict";
|
|
6892
|
+
init_db2();
|
|
6893
|
+
LOCK_STALE_MS = 3e4;
|
|
6894
|
+
state = null;
|
|
6895
|
+
}
|
|
6896
|
+
});
|
|
6897
|
+
|
|
6665
6898
|
// node_modules/cron-parser/dist/fields/types.js
|
|
6666
6899
|
var require_types = __commonJS({
|
|
6667
6900
|
"node_modules/cron-parser/dist/fields/types.js"(exports) {
|
|
@@ -6862,7 +7095,7 @@ var require_CronField = __commonJS({
|
|
|
6862
7095
|
if (!isValidRange) {
|
|
6863
7096
|
throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
|
|
6864
7097
|
}
|
|
6865
|
-
const duplicate = this.#values.find((value,
|
|
7098
|
+
const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
|
|
6866
7099
|
if (duplicate) {
|
|
6867
7100
|
throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
|
|
6868
7101
|
}
|
|
@@ -14707,11 +14940,11 @@ var require_CronFieldCollection = __commonJS({
|
|
|
14707
14940
|
throw new Error("Unexpected range end");
|
|
14708
14941
|
}
|
|
14709
14942
|
if (step * multiplier > range.end) {
|
|
14710
|
-
const mapFn = (_,
|
|
14943
|
+
const mapFn = (_, index2) => {
|
|
14711
14944
|
if (typeof range.start !== "number") {
|
|
14712
14945
|
throw new Error("Unexpected range start");
|
|
14713
14946
|
}
|
|
14714
|
-
return
|
|
14947
|
+
return index2 % step === 0 ? range.start + index2 : null;
|
|
14715
14948
|
};
|
|
14716
14949
|
if (typeof range.start !== "number") {
|
|
14717
14950
|
throw new Error("Unexpected range start");
|
|
@@ -15603,9 +15836,9 @@ var require_CronExpressionParser = __commonJS({
|
|
|
15603
15836
|
if (field === CronUnit.DayOfWeek && max % 7 === 0) {
|
|
15604
15837
|
stack.push(0);
|
|
15605
15838
|
}
|
|
15606
|
-
for (let
|
|
15607
|
-
if (stack.indexOf(
|
|
15608
|
-
stack.push(
|
|
15839
|
+
for (let index2 = min; index2 <= max; index2 += repeatInterval) {
|
|
15840
|
+
if (stack.indexOf(index2) === -1) {
|
|
15841
|
+
stack.push(index2);
|
|
15609
15842
|
}
|
|
15610
15843
|
}
|
|
15611
15844
|
return stack;
|
|
@@ -15828,11 +16061,6 @@ var require_dist = __commonJS({
|
|
|
15828
16061
|
});
|
|
15829
16062
|
|
|
15830
16063
|
// src/core/cron-parser.ts
|
|
15831
|
-
var cron_parser_exports = {};
|
|
15832
|
-
__export(cron_parser_exports, {
|
|
15833
|
-
getNextCronRun: () => getNextCronRun,
|
|
15834
|
-
isValidCronExpr: () => isValidCronExpr
|
|
15835
|
-
});
|
|
15836
16064
|
function getNextCronRun(expr, afterMs) {
|
|
15837
16065
|
try {
|
|
15838
16066
|
const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
|
|
@@ -15870,6 +16098,7 @@ var init_task_template_service = __esm({
|
|
|
15870
16098
|
({ taskTemplates: taskTemplates2 } = schema_exports);
|
|
15871
16099
|
TaskTemplateService = class {
|
|
15872
16100
|
static async create(data) {
|
|
16101
|
+
this.validate(data);
|
|
15873
16102
|
const now = Date.now();
|
|
15874
16103
|
const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
|
|
15875
16104
|
const tmpl = result[0];
|
|
@@ -15885,8 +16114,38 @@ var init_task_template_service = __esm({
|
|
|
15885
16114
|
}
|
|
15886
16115
|
return tmpl;
|
|
15887
16116
|
}
|
|
16117
|
+
static validate(data) {
|
|
16118
|
+
if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16119
|
+
if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16120
|
+
if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16121
|
+
const scheduleType = data.scheduleType;
|
|
16122
|
+
if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
|
|
16123
|
+
throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
|
|
16124
|
+
}
|
|
16125
|
+
if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
|
|
16126
|
+
throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
|
|
16127
|
+
}
|
|
16128
|
+
if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
|
|
16129
|
+
throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
|
|
16130
|
+
}
|
|
16131
|
+
if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
|
|
16132
|
+
throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
|
|
16133
|
+
}
|
|
16134
|
+
this.validateInteger("importance", data.importance, 1, 5);
|
|
16135
|
+
this.validateInteger("urgency", data.urgency, 1, 5);
|
|
16136
|
+
this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
|
|
16137
|
+
this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
|
|
16138
|
+
this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
|
|
16139
|
+
this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
|
|
16140
|
+
}
|
|
16141
|
+
static validateInteger(name, value, min, max) {
|
|
16142
|
+
if (value === void 0 || value === null) return;
|
|
16143
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
16144
|
+
throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
15888
16147
|
static async list(limit = 50) {
|
|
15889
|
-
return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
|
|
16148
|
+
return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
|
|
15890
16149
|
}
|
|
15891
16150
|
static async getById(id) {
|
|
15892
16151
|
const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
|
|
@@ -15933,13 +16192,13 @@ var init_compose = __esm({
|
|
|
15933
16192
|
"use strict";
|
|
15934
16193
|
compose = (middleware, onError, onNotFound) => {
|
|
15935
16194
|
return (context, next) => {
|
|
15936
|
-
let
|
|
16195
|
+
let index2 = -1;
|
|
15937
16196
|
return dispatch(0);
|
|
15938
16197
|
async function dispatch(i) {
|
|
15939
|
-
if (i <=
|
|
16198
|
+
if (i <= index2) {
|
|
15940
16199
|
throw new Error("next() called multiple times");
|
|
15941
16200
|
}
|
|
15942
|
-
|
|
16201
|
+
index2 = i;
|
|
15943
16202
|
let res;
|
|
15944
16203
|
let isError = false;
|
|
15945
16204
|
let handler;
|
|
@@ -16054,8 +16313,8 @@ var init_body = __esm({
|
|
|
16054
16313
|
handleParsingNestedValues = (form, key, value) => {
|
|
16055
16314
|
let nestedForm = form;
|
|
16056
16315
|
const keys = key.split(".");
|
|
16057
|
-
keys.forEach((key2,
|
|
16058
|
-
if (
|
|
16316
|
+
keys.forEach((key2, index2) => {
|
|
16317
|
+
if (index2 === keys.length - 1) {
|
|
16059
16318
|
nestedForm[key2] = value;
|
|
16060
16319
|
} else {
|
|
16061
16320
|
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
@@ -16087,8 +16346,8 @@ var init_url = __esm({
|
|
|
16087
16346
|
};
|
|
16088
16347
|
extractGroupsFromPath = (path) => {
|
|
16089
16348
|
const groups = [];
|
|
16090
|
-
path = path.replace(/\{[^}]+\}/g, (match2,
|
|
16091
|
-
const mark = `@${
|
|
16349
|
+
path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
|
|
16350
|
+
const mark = `@${index2}`;
|
|
16092
16351
|
groups.push([mark, match2]);
|
|
16093
16352
|
return mark;
|
|
16094
16353
|
});
|
|
@@ -16607,10 +16866,10 @@ var init_html = __esm({
|
|
|
16607
16866
|
return;
|
|
16608
16867
|
}
|
|
16609
16868
|
let escape;
|
|
16610
|
-
let
|
|
16869
|
+
let index2;
|
|
16611
16870
|
let lastIndex = 0;
|
|
16612
|
-
for (
|
|
16613
|
-
switch (str.charCodeAt(
|
|
16871
|
+
for (index2 = match2; index2 < str.length; index2++) {
|
|
16872
|
+
switch (str.charCodeAt(index2)) {
|
|
16614
16873
|
case 34:
|
|
16615
16874
|
escape = """;
|
|
16616
16875
|
break;
|
|
@@ -16629,10 +16888,10 @@ var init_html = __esm({
|
|
|
16629
16888
|
default:
|
|
16630
16889
|
continue;
|
|
16631
16890
|
}
|
|
16632
|
-
buffer[0] += str.substring(lastIndex,
|
|
16633
|
-
lastIndex =
|
|
16891
|
+
buffer[0] += str.substring(lastIndex, index2) + escape;
|
|
16892
|
+
lastIndex = index2 + 1;
|
|
16634
16893
|
}
|
|
16635
|
-
buffer[0] += str.substring(lastIndex,
|
|
16894
|
+
buffer[0] += str.substring(lastIndex, index2);
|
|
16636
16895
|
};
|
|
16637
16896
|
resolveCallbackSync = (str) => {
|
|
16638
16897
|
const callbacks = str.callbacks;
|
|
@@ -17508,8 +17767,8 @@ function match(method, path) {
|
|
|
17508
17767
|
if (!match3) {
|
|
17509
17768
|
return [[], emptyParam];
|
|
17510
17769
|
}
|
|
17511
|
-
const
|
|
17512
|
-
return [matcher[1][
|
|
17770
|
+
const index2 = match3.indexOf("", 1);
|
|
17771
|
+
return [matcher[1][index2], match3];
|
|
17513
17772
|
});
|
|
17514
17773
|
this.match = match2;
|
|
17515
17774
|
return match2(method, path);
|
|
@@ -17556,7 +17815,7 @@ var init_node = __esm({
|
|
|
17556
17815
|
#index;
|
|
17557
17816
|
#varIndex;
|
|
17558
17817
|
#children = /* @__PURE__ */ Object.create(null);
|
|
17559
|
-
insert(tokens,
|
|
17818
|
+
insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
|
|
17560
17819
|
if (tokens.length === 0) {
|
|
17561
17820
|
if (this.#index !== void 0) {
|
|
17562
17821
|
throw PATH_ERROR;
|
|
@@ -17564,7 +17823,7 @@ var init_node = __esm({
|
|
|
17564
17823
|
if (pathErrorCheckOnly) {
|
|
17565
17824
|
return;
|
|
17566
17825
|
}
|
|
17567
|
-
this.#index =
|
|
17826
|
+
this.#index = index2;
|
|
17568
17827
|
return;
|
|
17569
17828
|
}
|
|
17570
17829
|
const [token, ...restTokens] = tokens;
|
|
@@ -17614,7 +17873,7 @@ var init_node = __esm({
|
|
|
17614
17873
|
node = this.#children[token] = new _Node();
|
|
17615
17874
|
}
|
|
17616
17875
|
}
|
|
17617
|
-
node.insert(restTokens,
|
|
17876
|
+
node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
|
|
17618
17877
|
}
|
|
17619
17878
|
buildRegExpStr() {
|
|
17620
17879
|
const childKeys = Object.keys(this.#children).sort(compareKey);
|
|
@@ -17646,7 +17905,7 @@ var init_trie = __esm({
|
|
|
17646
17905
|
Trie = class {
|
|
17647
17906
|
#context = { varIndex: 0 };
|
|
17648
17907
|
#root = new Node();
|
|
17649
|
-
insert(path,
|
|
17908
|
+
insert(path, index2, pathErrorCheckOnly) {
|
|
17650
17909
|
const paramAssoc = [];
|
|
17651
17910
|
const groups = [];
|
|
17652
17911
|
for (let i = 0; ; ) {
|
|
@@ -17672,7 +17931,7 @@ var init_trie = __esm({
|
|
|
17672
17931
|
}
|
|
17673
17932
|
}
|
|
17674
17933
|
}
|
|
17675
|
-
this.#root.insert(tokens,
|
|
17934
|
+
this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
|
|
17676
17935
|
return paramAssoc;
|
|
17677
17936
|
}
|
|
17678
17937
|
buildRegExp() {
|
|
@@ -18266,8 +18525,19 @@ __export(web_exports, {
|
|
|
18266
18525
|
dashboardApp: () => dashboardApp,
|
|
18267
18526
|
default: () => web_default
|
|
18268
18527
|
});
|
|
18269
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
18528
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
|
|
18270
18529
|
import { dirname as dirname2 } from "path";
|
|
18530
|
+
function parsePositiveInteger(value) {
|
|
18531
|
+
if (!/^\d+$/.test(value)) return null;
|
|
18532
|
+
const parsed = Number(value);
|
|
18533
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
18534
|
+
}
|
|
18535
|
+
function parseTaskStatus(value) {
|
|
18536
|
+
return TASK_STATUSES.has(value) ? value : null;
|
|
18537
|
+
}
|
|
18538
|
+
function safeStatus(value) {
|
|
18539
|
+
return value && TASK_STATUSES.has(value) ? value : "unknown";
|
|
18540
|
+
}
|
|
18271
18541
|
function formatDuration(startAt, endAt) {
|
|
18272
18542
|
if (!startAt) return "-";
|
|
18273
18543
|
const start = new Date(startAt).getTime();
|
|
@@ -18305,17 +18575,21 @@ function esc(s) {
|
|
|
18305
18575
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
18306
18576
|
}
|
|
18307
18577
|
function readCurrentConfig() {
|
|
18308
|
-
|
|
18578
|
+
const configPath = getConfigPath();
|
|
18579
|
+
if (!existsSync3(configPath)) return {};
|
|
18309
18580
|
try {
|
|
18310
|
-
return JSON.parse(readFileSync2(
|
|
18581
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
18311
18582
|
} catch {
|
|
18312
18583
|
return {};
|
|
18313
18584
|
}
|
|
18314
18585
|
}
|
|
18315
18586
|
function writeConfig(cfg) {
|
|
18316
|
-
const
|
|
18587
|
+
const configPath = getConfigPath();
|
|
18588
|
+
const dir = dirname2(configPath);
|
|
18317
18589
|
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
18318
|
-
|
|
18590
|
+
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
18591
|
+
writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
18592
|
+
renameSync(tempPath, configPath);
|
|
18319
18593
|
}
|
|
18320
18594
|
function renderLayout(title, activeTab, body) {
|
|
18321
18595
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -18354,11 +18628,11 @@ async function saveConfig(){
|
|
|
18354
18628
|
scheduler:{
|
|
18355
18629
|
enabled:form.se.checked,
|
|
18356
18630
|
checkIntervalMs:Number(form.si.value),
|
|
18357
|
-
catchUp:form.cu.value,
|
|
18358
18631
|
},
|
|
18359
18632
|
watchdog:{
|
|
18360
18633
|
heartbeatTimeoutMs:Number(form.wt.value)*1000,
|
|
18361
|
-
|
|
18634
|
+
checkIntervalMs:Number(form.wci.value)*1000,
|
|
18635
|
+
cleanupIntervalMs:Number(form.wcl.value)*3600000,
|
|
18362
18636
|
retentionDays:Number(form.rd.value),
|
|
18363
18637
|
}
|
|
18364
18638
|
};
|
|
@@ -18389,7 +18663,7 @@ async function saveConfig(){
|
|
|
18389
18663
|
<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>
|
|
18390
18664
|
</body></html>`;
|
|
18391
18665
|
}
|
|
18392
|
-
var app, SHARED_STYLES, dashboardApp, web_default;
|
|
18666
|
+
var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
|
|
18393
18667
|
var init_web = __esm({
|
|
18394
18668
|
"src/web/index.tsx"() {
|
|
18395
18669
|
"use strict";
|
|
@@ -18401,7 +18675,47 @@ var init_web = __esm({
|
|
|
18401
18675
|
init_drizzle_orm();
|
|
18402
18676
|
init_db2();
|
|
18403
18677
|
init_config();
|
|
18678
|
+
init_health();
|
|
18404
18679
|
app = new Hono2();
|
|
18680
|
+
TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
18681
|
+
"pending",
|
|
18682
|
+
"running",
|
|
18683
|
+
"done",
|
|
18684
|
+
"failed",
|
|
18685
|
+
"dead_letter",
|
|
18686
|
+
"cancelled"
|
|
18687
|
+
]);
|
|
18688
|
+
app.use("*", async (c, next) => {
|
|
18689
|
+
await next();
|
|
18690
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
18691
|
+
c.header("X-Frame-Options", "DENY");
|
|
18692
|
+
c.header("Referrer-Policy", "no-referrer");
|
|
18693
|
+
});
|
|
18694
|
+
app.use("/api/*", async (c, next) => {
|
|
18695
|
+
if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
|
|
18696
|
+
const fetchSite = c.req.header("Sec-Fetch-Site");
|
|
18697
|
+
if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
|
|
18698
|
+
return c.json({ error: "cross-site request rejected" }, 403);
|
|
18699
|
+
}
|
|
18700
|
+
const origin = c.req.header("Origin");
|
|
18701
|
+
if (origin) {
|
|
18702
|
+
try {
|
|
18703
|
+
const originUrl = new URL(origin);
|
|
18704
|
+
const requestUrl = new URL(c.req.url);
|
|
18705
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
|
|
18706
|
+
if (!loopback || originUrl.origin !== requestUrl.origin) {
|
|
18707
|
+
return c.json({ error: "cross-site request rejected" }, 403);
|
|
18708
|
+
}
|
|
18709
|
+
} catch {
|
|
18710
|
+
return c.json({ error: "invalid origin" }, 403);
|
|
18711
|
+
}
|
|
18712
|
+
}
|
|
18713
|
+
return next();
|
|
18714
|
+
});
|
|
18715
|
+
app.get("/health", (c) => {
|
|
18716
|
+
const health = getGatewayHealth();
|
|
18717
|
+
return c.json(health, health.status === "ok" ? 200 : 503);
|
|
18718
|
+
});
|
|
18405
18719
|
SHARED_STYLES = html`
|
|
18406
18720
|
<style>
|
|
18407
18721
|
:root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
|
|
@@ -18478,12 +18792,16 @@ var init_web = __esm({
|
|
|
18478
18792
|
</style>
|
|
18479
18793
|
`;
|
|
18480
18794
|
app.get("/", async (c) => {
|
|
18481
|
-
const
|
|
18795
|
+
const pageParam = c.req.query("page") || "1";
|
|
18796
|
+
const page = parsePositiveInteger(pageParam);
|
|
18797
|
+
if (page === null) return c.text("invalid page", 400);
|
|
18482
18798
|
const statusFilter = c.req.query("status") || "";
|
|
18799
|
+
const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
|
|
18800
|
+
if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
|
|
18483
18801
|
const limit = 50;
|
|
18484
18802
|
const offset = (page - 1) * limit;
|
|
18485
18803
|
const [tasks3, statsData] = await Promise.all([
|
|
18486
|
-
TaskService.list({ limit, offset, ...
|
|
18804
|
+
TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
|
|
18487
18805
|
TaskService.stats({})
|
|
18488
18806
|
]);
|
|
18489
18807
|
const taskIds = tasks3.map((t) => t.id);
|
|
@@ -18506,12 +18824,13 @@ var init_web = __esm({
|
|
|
18506
18824
|
</div>`;
|
|
18507
18825
|
let rows = "";
|
|
18508
18826
|
for (const task of tasks3) {
|
|
18509
|
-
const
|
|
18827
|
+
const status = safeStatus(task.status);
|
|
18828
|
+
const st = status.toUpperCase();
|
|
18510
18829
|
rows += `<tr>
|
|
18511
18830
|
<td class="mu">#${task.id}</td>
|
|
18512
18831
|
<td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
|
|
18513
18832
|
<td><span class="tag">${esc(task.agent)}</span></td>
|
|
18514
|
-
<td><span class="badge b-${
|
|
18833
|
+
<td><span class="badge b-${status}">${st}</span></td>
|
|
18515
18834
|
<td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
|
|
18516
18835
|
<td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
|
|
18517
18836
|
<td>
|
|
@@ -18543,21 +18862,13 @@ var init_web = __esm({
|
|
|
18543
18862
|
});
|
|
18544
18863
|
app.get("/templates", async (c) => {
|
|
18545
18864
|
const templates = await TaskTemplateService.list(100);
|
|
18546
|
-
for (const tmpl of templates) {
|
|
18547
|
-
if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
|
|
18548
|
-
try {
|
|
18549
|
-
const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
|
|
18550
|
-
tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
|
|
18551
|
-
} catch {
|
|
18552
|
-
}
|
|
18553
|
-
}
|
|
18554
|
-
}
|
|
18555
18865
|
const enabled = templates.filter((t) => t.enabled).length;
|
|
18556
18866
|
const disabled = templates.length - enabled;
|
|
18557
18867
|
let rows = "";
|
|
18558
18868
|
for (const t of templates) {
|
|
18559
|
-
const
|
|
18560
|
-
const
|
|
18869
|
+
const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
|
|
18870
|
+
const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
|
|
18871
|
+
const typeClass = "tag t-" + scheduleType;
|
|
18561
18872
|
let rule = "-";
|
|
18562
18873
|
if (t.scheduleType === "cron") rule = t.cronExpr || "-";
|
|
18563
18874
|
else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
|
|
@@ -18569,7 +18880,7 @@ var init_web = __esm({
|
|
|
18569
18880
|
<td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
|
|
18570
18881
|
<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>
|
|
18571
18882
|
<td><span class="${typeClass}">${typeLabel}</span></td>
|
|
18572
|
-
<td class="m sm">${rule}</td>
|
|
18883
|
+
<td class="m sm">${esc(rule)}</td>
|
|
18573
18884
|
<td>${statusBadge}</td>
|
|
18574
18885
|
<td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
|
|
18575
18886
|
<td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
|
|
@@ -18597,7 +18908,8 @@ var init_web = __esm({
|
|
|
18597
18908
|
return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
|
|
18598
18909
|
});
|
|
18599
18910
|
app.get("/runs", async (c) => {
|
|
18600
|
-
const page =
|
|
18911
|
+
const page = parsePositiveInteger(c.req.query("page") || "1");
|
|
18912
|
+
if (page === null) return c.text("invalid page", 400);
|
|
18601
18913
|
const limit = 50;
|
|
18602
18914
|
const offset = (page - 1) * limit;
|
|
18603
18915
|
const { taskRuns: tr, tasks: tk } = schema_exports;
|
|
@@ -18615,13 +18927,14 @@ var init_web = __esm({
|
|
|
18615
18927
|
childPid: tr.childPid,
|
|
18616
18928
|
taskName: tk.name,
|
|
18617
18929
|
taskAgent: tk.agent
|
|
18618
|
-
}).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
|
|
18930
|
+
}).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
|
|
18619
18931
|
const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
|
|
18620
18932
|
const total = Number(totalResult[0]?.count ?? 0);
|
|
18621
18933
|
const totalPages = Math.ceil(total / limit);
|
|
18622
18934
|
let rows = "";
|
|
18623
18935
|
const logsHtml = [];
|
|
18624
18936
|
for (const run of runs) {
|
|
18937
|
+
const status = safeStatus(run.status);
|
|
18625
18938
|
const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
18626
18939
|
const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
|
|
18627
18940
|
rows += `<tr>
|
|
@@ -18629,15 +18942,15 @@ var init_web = __esm({
|
|
|
18629
18942
|
<td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
|
|
18630
18943
|
${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
|
|
18631
18944
|
<td><span class="tag">${esc(run.taskAgent)}</span></td>
|
|
18632
|
-
<td><span class="badge b-${
|
|
18945
|
+
<td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
|
|
18633
18946
|
<td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
|
|
18634
18947
|
<td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
18635
18948
|
<td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
|
|
18636
18949
|
</tr>`;
|
|
18637
18950
|
if (run.log) {
|
|
18638
18951
|
logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
|
|
18639
|
-
<div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
|
|
18640
|
-
<div class="log-box">${run.log
|
|
18952
|
+
<div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
|
|
18953
|
+
<div class="log-box">${esc(run.log)}</div></div></div>`);
|
|
18641
18954
|
}
|
|
18642
18955
|
}
|
|
18643
18956
|
let paging = `<div class="pn">`;
|
|
@@ -18663,17 +18976,18 @@ var init_web = __esm({
|
|
|
18663
18976
|
});
|
|
18664
18977
|
app.get("/system", async (c) => {
|
|
18665
18978
|
const config = loadConfig();
|
|
18979
|
+
const configPath = getConfigPath();
|
|
18666
18980
|
const stats = await TaskService.stats({});
|
|
18667
18981
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
18668
18982
|
const templates = await TaskTemplateService.list(100);
|
|
18669
|
-
const configExists = existsSync3(
|
|
18983
|
+
const configExists = existsSync3(configPath);
|
|
18670
18984
|
let runRows = "";
|
|
18671
18985
|
if (runningRuns.length > 0) {
|
|
18672
18986
|
for (const run of runningRuns) {
|
|
18673
18987
|
const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
18674
18988
|
runRows += `<tr>
|
|
18675
18989
|
<td class="mu">#${run.id}</td><td>#${run.taskId}</td>
|
|
18676
|
-
<td class="m sm">${shortS}</td>
|
|
18990
|
+
<td class="m sm">${esc(shortS)}</td>
|
|
18677
18991
|
<td class="sm">${esc(run.model) || "-"}</td>
|
|
18678
18992
|
<td class="sm">${formatDate(run.startedAt)}</td>
|
|
18679
18993
|
<td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
@@ -18698,19 +19012,14 @@ var init_web = __esm({
|
|
|
18698
19012
|
<h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
|
|
18699
19013
|
<div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
|
|
18700
19014
|
<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>
|
|
18701
|
-
<div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
|
|
18702
|
-
<option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
|
|
18703
|
-
<option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
|
|
18704
|
-
<option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
|
|
18705
|
-
</select></div>
|
|
18706
19015
|
<div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
|
|
18707
19016
|
</div>
|
|
18708
19017
|
<div class="card">
|
|
18709
19018
|
<h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
|
|
18710
19019
|
<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>
|
|
18711
|
-
<div class="form-row"><label>\
|
|
19020
|
+
<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>
|
|
19021
|
+
<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>
|
|
18712
19022
|
<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>
|
|
18713
|
-
<div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
|
|
18714
19023
|
</div>
|
|
18715
19024
|
</div>
|
|
18716
19025
|
<div style="text-align:center;margin-bottom:24px">
|
|
@@ -18739,7 +19048,7 @@ var init_web = __esm({
|
|
|
18739
19048
|
|
|
18740
19049
|
<div class="card mt16">
|
|
18741
19050
|
<h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
|
|
18742
|
-
<div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${
|
|
19051
|
+
<div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
|
|
18743
19052
|
<div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
|
|
18744
19053
|
</div>
|
|
18745
19054
|
|
|
@@ -18751,46 +19060,61 @@ var init_web = __esm({
|
|
|
18751
19060
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
18752
19061
|
});
|
|
18753
19062
|
app.get("/api/tasks/:id", async (c) => {
|
|
18754
|
-
const id =
|
|
19063
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19064
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18755
19065
|
const task = await TaskService.getById(id);
|
|
18756
19066
|
if (!task) return c.json({ error: "not found" }, 404);
|
|
18757
19067
|
const runs = await TaskRunService.listByTaskId(id);
|
|
18758
19068
|
return c.json({ ...task, _runs: runs });
|
|
18759
19069
|
});
|
|
18760
19070
|
app.get("/api/runs/:id", async (c) => {
|
|
18761
|
-
const id =
|
|
19071
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19072
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18762
19073
|
const run = await TaskRunService.getById(id);
|
|
18763
19074
|
if (!run) return c.json({ error: "not found" }, 404);
|
|
18764
19075
|
return c.json(run);
|
|
18765
19076
|
});
|
|
18766
19077
|
app.get("/api/templates/:id", async (c) => {
|
|
18767
|
-
const id =
|
|
19078
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19079
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18768
19080
|
const tmpl = await TaskTemplateService.getById(id);
|
|
18769
19081
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
18770
19082
|
return c.json(tmpl);
|
|
18771
19083
|
});
|
|
18772
19084
|
app.post("/api/tasks/:id/retry", async (c) => {
|
|
18773
|
-
|
|
18774
|
-
return c.json({
|
|
19085
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19086
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19087
|
+
const task = await TaskService.retry(id);
|
|
19088
|
+
if (task) return c.json({ success: true });
|
|
19089
|
+
return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
|
|
18775
19090
|
});
|
|
18776
19091
|
app.delete("/api/tasks/:id", async (c) => {
|
|
18777
|
-
|
|
18778
|
-
return c.json({
|
|
19092
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19093
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19094
|
+
const deleted = await TaskService.delete(id);
|
|
19095
|
+
return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18779
19096
|
});
|
|
18780
19097
|
app.post("/api/templates/:id/enable", async (c) => {
|
|
18781
|
-
const
|
|
18782
|
-
return c.json({
|
|
19098
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19099
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19100
|
+
const result = await TaskTemplateService.enable(id);
|
|
19101
|
+
return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18783
19102
|
});
|
|
18784
19103
|
app.post("/api/templates/:id/disable", async (c) => {
|
|
18785
|
-
const
|
|
18786
|
-
return c.json({
|
|
19104
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19105
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19106
|
+
const result = await TaskTemplateService.disable(id);
|
|
19107
|
+
return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18787
19108
|
});
|
|
18788
19109
|
app.delete("/api/templates/:id", async (c) => {
|
|
18789
|
-
const
|
|
18790
|
-
return c.json({
|
|
19110
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19111
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19112
|
+
const ok = await TaskTemplateService.delete(id);
|
|
19113
|
+
return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18791
19114
|
});
|
|
18792
19115
|
app.post("/api/templates/:id/trigger", async (c) => {
|
|
18793
|
-
const id =
|
|
19116
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19117
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18794
19118
|
const tmpl = await TaskTemplateService.getById(id);
|
|
18795
19119
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
18796
19120
|
const task = await TaskService.add({
|
|
@@ -18802,7 +19126,10 @@ var init_web = __esm({
|
|
|
18802
19126
|
category: tmpl.category,
|
|
18803
19127
|
importance: tmpl.importance,
|
|
18804
19128
|
urgency: tmpl.urgency,
|
|
19129
|
+
batchId: tmpl.batchId,
|
|
18805
19130
|
maxRetries: tmpl.maxRetries,
|
|
19131
|
+
retryBackoffMs: tmpl.retryBackoffMs,
|
|
19132
|
+
timeoutMs: tmpl.timeoutMs,
|
|
18806
19133
|
templateId: tmpl.id
|
|
18807
19134
|
});
|
|
18808
19135
|
return c.json({ success: true, taskId: task.id });
|
|
@@ -18817,22 +19144,29 @@ var init_web = __esm({
|
|
|
18817
19144
|
const bW = body.worker ?? {};
|
|
18818
19145
|
const bS = body.scheduler ?? {};
|
|
18819
19146
|
const bD = body.watchdog ?? {};
|
|
18820
|
-
const merged = {
|
|
18821
|
-
|
|
19147
|
+
const merged = {
|
|
19148
|
+
...current,
|
|
19149
|
+
...body,
|
|
19150
|
+
configVersion: 2,
|
|
19151
|
+
worker: { ...curW, ...bW },
|
|
19152
|
+
scheduler: { ...curS, ...bS },
|
|
19153
|
+
watchdog: { ...curD, ...bD }
|
|
19154
|
+
};
|
|
19155
|
+
writeConfig(validateConfig(merged));
|
|
18822
19156
|
return c.json({ success: true });
|
|
18823
19157
|
} catch (err) {
|
|
18824
|
-
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
19158
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
|
|
18825
19159
|
}
|
|
18826
19160
|
});
|
|
18827
19161
|
app.post("/api/database/clear", async (c) => {
|
|
18828
19162
|
try {
|
|
18829
|
-
const { tasks: tasks3, taskRuns:
|
|
18830
|
-
await db.delete(
|
|
19163
|
+
const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
|
|
19164
|
+
await db.delete(taskRuns4);
|
|
18831
19165
|
await db.delete(taskTemplates4);
|
|
18832
19166
|
await db.delete(tasks3);
|
|
18833
19167
|
return c.json({ success: true });
|
|
18834
19168
|
} catch (err) {
|
|
18835
|
-
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
19169
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
18836
19170
|
}
|
|
18837
19171
|
});
|
|
18838
19172
|
dashboardApp = app;
|
|
@@ -18850,39 +19184,127 @@ init_config();
|
|
|
18850
19184
|
// src/worker/index.ts
|
|
18851
19185
|
init_task_service();
|
|
18852
19186
|
init_task_run_service();
|
|
19187
|
+
init_health();
|
|
18853
19188
|
import { spawn } from "child_process";
|
|
19189
|
+
|
|
19190
|
+
// src/core/process-control.ts
|
|
19191
|
+
import { spawnSync } from "child_process";
|
|
19192
|
+
import { basename } from "path";
|
|
19193
|
+
function isSafePid(pid) {
|
|
19194
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
19195
|
+
}
|
|
19196
|
+
function isProcessAlive(pid) {
|
|
19197
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
19198
|
+
try {
|
|
19199
|
+
process.kill(pid, 0);
|
|
19200
|
+
return true;
|
|
19201
|
+
} catch (error) {
|
|
19202
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
19203
|
+
}
|
|
19204
|
+
}
|
|
19205
|
+
function inspectUnixProcess(pid) {
|
|
19206
|
+
const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
19207
|
+
encoding: "utf8",
|
|
19208
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19209
|
+
});
|
|
19210
|
+
if (result.status !== 0) return null;
|
|
19211
|
+
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
19212
|
+
if (!match2) return null;
|
|
19213
|
+
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
19214
|
+
}
|
|
19215
|
+
function commandMatches(command, expectedExecutable) {
|
|
19216
|
+
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
19217
|
+
if (!expectedName) return false;
|
|
19218
|
+
return command.toLowerCase().includes(expectedName);
|
|
19219
|
+
}
|
|
19220
|
+
function signalSpawnedProcessTree(pid, signal) {
|
|
19221
|
+
if (!isSafePid(pid)) return false;
|
|
19222
|
+
if (process.platform !== "win32") {
|
|
19223
|
+
try {
|
|
19224
|
+
process.kill(-pid, signal);
|
|
19225
|
+
return true;
|
|
19226
|
+
} catch {
|
|
19227
|
+
}
|
|
19228
|
+
}
|
|
19229
|
+
try {
|
|
19230
|
+
process.kill(pid, signal);
|
|
19231
|
+
return true;
|
|
19232
|
+
} catch {
|
|
19233
|
+
return false;
|
|
19234
|
+
}
|
|
19235
|
+
}
|
|
19236
|
+
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
19237
|
+
if (!isSafePid(pid)) return false;
|
|
19238
|
+
if (process.platform === "win32") {
|
|
19239
|
+
const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
19240
|
+
encoding: "utf8",
|
|
19241
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19242
|
+
});
|
|
19243
|
+
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
19244
|
+
const args = ["/PID", String(pid), "/T"];
|
|
19245
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
19246
|
+
return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
19247
|
+
}
|
|
19248
|
+
const info = inspectUnixProcess(pid);
|
|
19249
|
+
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
19250
|
+
try {
|
|
19251
|
+
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
19252
|
+
else process.kill(pid, signal);
|
|
19253
|
+
return true;
|
|
19254
|
+
} catch {
|
|
19255
|
+
return false;
|
|
19256
|
+
}
|
|
19257
|
+
}
|
|
19258
|
+
|
|
19259
|
+
// src/worker/index.ts
|
|
19260
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
|
|
19261
|
+
var FORBIDDEN_AGENT = "supertask-runner";
|
|
18854
19262
|
var WorkerEngine = class {
|
|
18855
19263
|
activeBatchIds = /* @__PURE__ */ new Set();
|
|
18856
19264
|
runningTasks = /* @__PURE__ */ new Map();
|
|
18857
19265
|
stopped = false;
|
|
18858
19266
|
pollTimer = null;
|
|
18859
19267
|
heartbeatTimer = null;
|
|
19268
|
+
pollCyclePromise = null;
|
|
18860
19269
|
cfg;
|
|
18861
|
-
|
|
19270
|
+
opencodeBin;
|
|
19271
|
+
maxOutputChars;
|
|
19272
|
+
constructor(cfg, options = {}) {
|
|
18862
19273
|
this.cfg = cfg.worker;
|
|
19274
|
+
this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
19275
|
+
this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
18863
19276
|
}
|
|
18864
19277
|
start() {
|
|
18865
19278
|
this.stopped = false;
|
|
19279
|
+
markGatewayActivity("worker");
|
|
18866
19280
|
this.poll();
|
|
18867
19281
|
this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
|
|
18868
19282
|
}
|
|
18869
|
-
stop() {
|
|
19283
|
+
async stop(gracePeriodMs = 0) {
|
|
18870
19284
|
this.stopped = true;
|
|
18871
19285
|
if (this.pollTimer) {
|
|
18872
19286
|
clearTimeout(this.pollTimer);
|
|
18873
19287
|
this.pollTimer = null;
|
|
18874
19288
|
}
|
|
19289
|
+
if (this.pollCyclePromise) await this.pollCyclePromise;
|
|
19290
|
+
if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
|
|
19291
|
+
const deadline = Date.now() + gracePeriodMs;
|
|
19292
|
+
while (this.runningTasks.size > 0 && Date.now() < deadline) {
|
|
19293
|
+
await Bun.sleep(Math.min(50, deadline - Date.now()));
|
|
19294
|
+
}
|
|
19295
|
+
}
|
|
18875
19296
|
if (this.heartbeatTimer) {
|
|
18876
19297
|
clearInterval(this.heartbeatTimer);
|
|
18877
19298
|
this.heartbeatTimer = null;
|
|
18878
19299
|
}
|
|
19300
|
+
const interruptedTaskIds = [...this.runningTasks.keys()];
|
|
18879
19301
|
const killPromises = [];
|
|
18880
|
-
for (const
|
|
19302
|
+
for (const entry of this.runningTasks.values()) {
|
|
18881
19303
|
entry.shutdown = true;
|
|
18882
19304
|
killPromises.push(this.killEntry(entry));
|
|
18883
19305
|
}
|
|
18884
|
-
|
|
18885
|
-
|
|
19306
|
+
await Promise.allSettled(killPromises);
|
|
19307
|
+
return interruptedTaskIds;
|
|
18886
19308
|
}
|
|
18887
19309
|
getRunningTaskIds() {
|
|
18888
19310
|
return [...this.runningTasks.keys()];
|
|
@@ -18892,136 +19314,244 @@ var WorkerEngine = class {
|
|
|
18892
19314
|
}
|
|
18893
19315
|
poll() {
|
|
18894
19316
|
if (this.stopped) return;
|
|
18895
|
-
|
|
19317
|
+
markGatewayActivity("worker");
|
|
19318
|
+
this.pollCyclePromise = this.tryDispatch().finally(() => {
|
|
19319
|
+
this.pollCyclePromise = null;
|
|
18896
19320
|
if (this.stopped) return;
|
|
18897
19321
|
this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
|
|
18898
19322
|
});
|
|
18899
19323
|
}
|
|
18900
19324
|
async tryDispatch() {
|
|
19325
|
+
await this.reconcileCancelledTasks();
|
|
18901
19326
|
while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
|
|
19327
|
+
let task;
|
|
19328
|
+
try {
|
|
19329
|
+
task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
|
|
19330
|
+
} catch (err) {
|
|
19331
|
+
this.logError("task claim failed", err);
|
|
19332
|
+
break;
|
|
19333
|
+
}
|
|
19334
|
+
if (!task) break;
|
|
19335
|
+
if (this.stopped) break;
|
|
19336
|
+
if (!await TaskService.start(task.id)) continue;
|
|
19337
|
+
if (task.batchId) this.activeBatchIds.add(task.batchId);
|
|
19338
|
+
if (this.stopped) {
|
|
19339
|
+
await TaskService.resetRunningToPending([task.id]);
|
|
19340
|
+
this.releaseBatch(task);
|
|
19341
|
+
break;
|
|
19342
|
+
}
|
|
18902
19343
|
try {
|
|
18903
|
-
const excludedBatchIds = [...this.activeBatchIds];
|
|
18904
|
-
const task = await TaskService.next({ excludedBatchIds });
|
|
18905
|
-
if (!task) break;
|
|
18906
|
-
if (!await TaskService.start(task.id)) continue;
|
|
18907
|
-
if (task.batchId) {
|
|
18908
|
-
this.activeBatchIds.add(task.batchId);
|
|
18909
|
-
}
|
|
18910
19344
|
const run = await TaskRunService.create({
|
|
18911
19345
|
taskId: task.id,
|
|
18912
19346
|
model: this.resolveModel(task.model),
|
|
18913
19347
|
status: "running"
|
|
18914
19348
|
});
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
|
|
18926
|
-
|
|
18927
|
-
|
|
18928
|
-
|
|
18929
|
-
|
|
18930
|
-
|
|
18931
|
-
|
|
18932
|
-
|
|
18933
|
-
|
|
18934
|
-
|
|
18935
|
-
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
18944
|
-
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
18971
|
-
console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
|
|
19349
|
+
if (this.stopped) {
|
|
19350
|
+
await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
|
|
19351
|
+
await TaskService.resetRunningToPending([task.id]);
|
|
19352
|
+
this.releaseBatch(task);
|
|
19353
|
+
break;
|
|
19354
|
+
}
|
|
19355
|
+
if (task.agent === FORBIDDEN_AGENT) {
|
|
19356
|
+
const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
|
|
19357
|
+
await TaskRunService.fail(run.id, message);
|
|
19358
|
+
await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
|
|
19359
|
+
this.releaseBatch(task);
|
|
19360
|
+
continue;
|
|
19361
|
+
}
|
|
19362
|
+
this.spawnTask(task, run.id);
|
|
19363
|
+
} catch (err) {
|
|
19364
|
+
this.releaseBatch(task);
|
|
19365
|
+
const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
|
|
19366
|
+
try {
|
|
19367
|
+
await TaskService.fail(task.id, message);
|
|
19368
|
+
} catch (failErr) {
|
|
19369
|
+
this.logError("failed to compensate task startup", failErr, task.id);
|
|
19370
|
+
}
|
|
19371
|
+
this.logError("task dispatch failed", err, task.id);
|
|
19372
|
+
}
|
|
19373
|
+
}
|
|
19374
|
+
}
|
|
19375
|
+
spawnTask(task, runId) {
|
|
19376
|
+
const model = this.resolveModel(task.model);
|
|
19377
|
+
const args = ["run", "--agent", task.agent, "--format", "json"];
|
|
19378
|
+
if (model) args.push("-m", model);
|
|
19379
|
+
args.push(task.prompt);
|
|
19380
|
+
const child = spawn(this.opencodeBin, args, {
|
|
19381
|
+
cwd: task.cwd || process.cwd(),
|
|
19382
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19383
|
+
detached: process.platform !== "win32"
|
|
19384
|
+
});
|
|
19385
|
+
const entry = {
|
|
19386
|
+
task,
|
|
19387
|
+
runId,
|
|
19388
|
+
child,
|
|
19389
|
+
output: "",
|
|
19390
|
+
sessionId: null,
|
|
19391
|
+
timeoutTimer: null,
|
|
19392
|
+
shutdown: false,
|
|
19393
|
+
settled: false
|
|
19394
|
+
};
|
|
19395
|
+
this.runningTasks.set(task.id, entry);
|
|
19396
|
+
const handleData = (data) => {
|
|
19397
|
+
const text2 = data.toString();
|
|
19398
|
+
entry.output = (entry.output + text2).slice(-this.maxOutputChars);
|
|
19399
|
+
process.stdout.write(text2);
|
|
19400
|
+
const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
|
|
19401
|
+
if (match2?.[1] && match2[1] !== entry.sessionId) {
|
|
19402
|
+
entry.sessionId = match2[1];
|
|
19403
|
+
TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
|
|
19404
|
+
this.logError("sessionId update failed", err, task.id);
|
|
18972
19405
|
});
|
|
19406
|
+
}
|
|
19407
|
+
};
|
|
19408
|
+
child.stdout?.on("data", handleData);
|
|
19409
|
+
child.stderr?.on("data", handleData);
|
|
19410
|
+
child.once("error", (err) => {
|
|
19411
|
+
void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
|
|
19412
|
+
});
|
|
19413
|
+
child.once("close", (code, signal) => {
|
|
19414
|
+
const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
|
|
19415
|
+
void this.finishEntry(entry, code, failure);
|
|
19416
|
+
});
|
|
19417
|
+
const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
|
|
19418
|
+
if (timeoutMs > 0) {
|
|
19419
|
+
entry.timeoutTimer = setTimeout(() => {
|
|
19420
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19421
|
+
void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
|
|
19422
|
+
}, timeoutMs);
|
|
19423
|
+
}
|
|
19424
|
+
TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
|
|
19425
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19426
|
+
void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
19427
|
+
});
|
|
19428
|
+
}
|
|
19429
|
+
async finishEntry(entry, code, failure) {
|
|
19430
|
+
if (entry.settled) return;
|
|
19431
|
+
entry.settled = true;
|
|
19432
|
+
if (entry.timeoutTimer) {
|
|
19433
|
+
clearTimeout(entry.timeoutTimer);
|
|
19434
|
+
entry.timeoutTimer = null;
|
|
19435
|
+
}
|
|
19436
|
+
try {
|
|
19437
|
+
if (entry.shutdown) return;
|
|
19438
|
+
const currentRun = await TaskRunService.getById(entry.runId);
|
|
19439
|
+
if (!currentRun || currentRun.status !== "running") return;
|
|
19440
|
+
const output = entry.output.trim();
|
|
19441
|
+
const log = failure ? `${failure}${output ? `
|
|
19442
|
+
${output}` : ""}` : output;
|
|
19443
|
+
if (code === 0 && !failure) {
|
|
19444
|
+
const completed = await TaskService.done(entry.task.id, log);
|
|
19445
|
+
if (completed) {
|
|
19446
|
+
await TaskRunService.done(entry.runId, log);
|
|
19447
|
+
console.log(JSON.stringify({
|
|
19448
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19449
|
+
level: "info",
|
|
19450
|
+
msg: "task done",
|
|
19451
|
+
taskId: entry.task.id
|
|
19452
|
+
}));
|
|
19453
|
+
return;
|
|
19454
|
+
}
|
|
19455
|
+
await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
|
|
19456
|
+
return;
|
|
19457
|
+
}
|
|
19458
|
+
await TaskRunService.fail(entry.runId, log);
|
|
19459
|
+
const failed = await TaskService.fail(entry.task.id, log);
|
|
19460
|
+
if (!failed) {
|
|
19461
|
+
this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
|
|
19462
|
+
}
|
|
19463
|
+
console.error(JSON.stringify({
|
|
19464
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19465
|
+
level: "error",
|
|
19466
|
+
msg: "task failed",
|
|
19467
|
+
taskId: entry.task.id,
|
|
19468
|
+
error: failure
|
|
19469
|
+
}));
|
|
19470
|
+
} finally {
|
|
19471
|
+
this.runningTasks.delete(entry.task.id);
|
|
19472
|
+
this.releaseBatch(entry.task);
|
|
19473
|
+
}
|
|
19474
|
+
}
|
|
19475
|
+
async reconcileCancelledTasks() {
|
|
19476
|
+
for (const entry of [...this.runningTasks.values()]) {
|
|
19477
|
+
try {
|
|
19478
|
+
const task = await TaskService.getById(entry.task.id);
|
|
19479
|
+
if (task?.status === "cancelled") await this.cancelEntry(entry);
|
|
18973
19480
|
} catch (err) {
|
|
18974
|
-
|
|
18975
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18976
|
-
level: "error",
|
|
18977
|
-
msg: "tryDispatch iteration failed",
|
|
18978
|
-
error: err instanceof Error ? err.message : String(err)
|
|
18979
|
-
}));
|
|
18980
|
-
break;
|
|
19481
|
+
this.logError("cancel reconciliation failed", err, entry.task.id);
|
|
18981
19482
|
}
|
|
18982
19483
|
}
|
|
18983
19484
|
}
|
|
19485
|
+
async cancelEntry(entry) {
|
|
19486
|
+
if (entry.settled) return;
|
|
19487
|
+
entry.settled = true;
|
|
19488
|
+
if (entry.timeoutTimer) {
|
|
19489
|
+
clearTimeout(entry.timeoutTimer);
|
|
19490
|
+
entry.timeoutTimer = null;
|
|
19491
|
+
}
|
|
19492
|
+
try {
|
|
19493
|
+
await this.killEntry(entry);
|
|
19494
|
+
const output = entry.output.trim();
|
|
19495
|
+
const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
|
|
19496
|
+
${output}` : ""}`;
|
|
19497
|
+
await TaskRunService.fail(entry.runId, log);
|
|
19498
|
+
console.log(JSON.stringify({
|
|
19499
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19500
|
+
level: "info",
|
|
19501
|
+
msg: "running task cancelled",
|
|
19502
|
+
taskId: entry.task.id
|
|
19503
|
+
}));
|
|
19504
|
+
} finally {
|
|
19505
|
+
this.runningTasks.delete(entry.task.id);
|
|
19506
|
+
this.releaseBatch(entry.task);
|
|
19507
|
+
}
|
|
19508
|
+
}
|
|
18984
19509
|
async updateHeartbeats() {
|
|
18985
|
-
for (const
|
|
19510
|
+
for (const entry of this.runningTasks.values()) {
|
|
18986
19511
|
try {
|
|
18987
19512
|
await TaskRunService.heartbeat(entry.runId);
|
|
18988
|
-
} catch {
|
|
19513
|
+
} catch (err) {
|
|
19514
|
+
this.logError("heartbeat update failed", err, entry.task.id);
|
|
18989
19515
|
}
|
|
18990
19516
|
}
|
|
18991
19517
|
}
|
|
18992
19518
|
killEntry(entry) {
|
|
18993
|
-
if (entry.child.exitCode !== null) {
|
|
19519
|
+
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
18994
19520
|
return Promise.resolve();
|
|
18995
19521
|
}
|
|
18996
19522
|
return new Promise((resolve) => {
|
|
18997
19523
|
const timeout = setTimeout(() => {
|
|
18998
|
-
|
|
18999
|
-
if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
|
|
19000
|
-
} catch {
|
|
19001
|
-
}
|
|
19524
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19002
19525
|
resolve();
|
|
19003
19526
|
}, 5e3);
|
|
19004
|
-
entry.child.
|
|
19527
|
+
entry.child.once("close", () => {
|
|
19005
19528
|
clearTimeout(timeout);
|
|
19006
19529
|
resolve();
|
|
19007
19530
|
});
|
|
19008
|
-
|
|
19009
|
-
if (entry.child.pid) {
|
|
19010
|
-
entry.child.kill("SIGTERM");
|
|
19011
|
-
} else {
|
|
19012
|
-
clearTimeout(timeout);
|
|
19013
|
-
resolve();
|
|
19014
|
-
}
|
|
19015
|
-
} catch {
|
|
19016
|
-
clearTimeout(timeout);
|
|
19017
|
-
resolve();
|
|
19018
|
-
}
|
|
19531
|
+
this.signalEntry(entry, "SIGTERM");
|
|
19019
19532
|
});
|
|
19020
19533
|
}
|
|
19534
|
+
signalEntry(entry, signal) {
|
|
19535
|
+
const pid = entry.child.pid;
|
|
19536
|
+
if (!pid) return;
|
|
19537
|
+
signalSpawnedProcessTree(pid, signal);
|
|
19538
|
+
}
|
|
19539
|
+
releaseBatch(task) {
|
|
19540
|
+
if (task.batchId) this.activeBatchIds.delete(task.batchId);
|
|
19541
|
+
}
|
|
19021
19542
|
resolveModel(taskModel) {
|
|
19022
19543
|
if (!taskModel || taskModel === "default") return null;
|
|
19023
19544
|
return taskModel;
|
|
19024
19545
|
}
|
|
19546
|
+
logError(message, error, taskId) {
|
|
19547
|
+
console.error(JSON.stringify({
|
|
19548
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19549
|
+
level: "error",
|
|
19550
|
+
msg: message,
|
|
19551
|
+
taskId,
|
|
19552
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19553
|
+
}));
|
|
19554
|
+
}
|
|
19025
19555
|
};
|
|
19026
19556
|
|
|
19027
19557
|
// src/gateway/watchdog/heartbeat.ts
|
|
@@ -19034,15 +19564,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19034
19564
|
for (const run of staleRuns) {
|
|
19035
19565
|
try {
|
|
19036
19566
|
if (run.childPid != null && run.childPid > 0) {
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19567
|
+
const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
19568
|
+
const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
|
|
19569
|
+
if (!killed) {
|
|
19570
|
+
console.warn(JSON.stringify({
|
|
19571
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19572
|
+
level: "warn",
|
|
19573
|
+
msg: "stale child process was not killed because identity validation failed",
|
|
19574
|
+
taskId: run.taskId,
|
|
19575
|
+
runId: run.runId,
|
|
19576
|
+
childPid: run.childPid
|
|
19577
|
+
}));
|
|
19040
19578
|
}
|
|
19041
19579
|
}
|
|
19042
19580
|
await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
|
|
19043
19581
|
const newRetryCount = run.taskRetryCount + 1;
|
|
19044
19582
|
const maxRetries = run.taskMaxRetries;
|
|
19045
|
-
if (newRetryCount
|
|
19583
|
+
if (newRetryCount > maxRetries) {
|
|
19046
19584
|
await TaskService.markDeadLetter(run.taskId, newRetryCount);
|
|
19047
19585
|
console.log(JSON.stringify({
|
|
19048
19586
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19054,7 +19592,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19054
19592
|
maxRetries
|
|
19055
19593
|
}));
|
|
19056
19594
|
} else {
|
|
19057
|
-
const backoffMs = computeBackoff(newRetryCount);
|
|
19595
|
+
const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
|
|
19058
19596
|
const retryAfter = Date.now() + backoffMs;
|
|
19059
19597
|
await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
|
|
19060
19598
|
console.log(JSON.stringify({
|
|
@@ -19110,6 +19648,7 @@ async function cleanupOldRecords(retentionDays) {
|
|
|
19110
19648
|
}
|
|
19111
19649
|
|
|
19112
19650
|
// src/gateway/watchdog/index.ts
|
|
19651
|
+
init_health();
|
|
19113
19652
|
var Watchdog = class {
|
|
19114
19653
|
constructor(cfg) {
|
|
19115
19654
|
this.cfg = cfg;
|
|
@@ -19120,13 +19659,14 @@ var Watchdog = class {
|
|
|
19120
19659
|
cleanupTimer = null;
|
|
19121
19660
|
start() {
|
|
19122
19661
|
this.stopped = false;
|
|
19662
|
+
markGatewayActivity("watchdog");
|
|
19123
19663
|
this.heartbeatTimer = setInterval(
|
|
19124
19664
|
() => this.runHeartbeatCheck(),
|
|
19125
|
-
this.cfg.watchdog.
|
|
19665
|
+
this.cfg.watchdog.checkIntervalMs
|
|
19126
19666
|
);
|
|
19127
19667
|
this.cleanupTimer = setInterval(
|
|
19128
19668
|
() => this.runCleanup(),
|
|
19129
|
-
this.cfg.watchdog.cleanupIntervalMs
|
|
19669
|
+
this.cfg.watchdog.cleanupIntervalMs
|
|
19130
19670
|
);
|
|
19131
19671
|
}
|
|
19132
19672
|
stop() {
|
|
@@ -19142,6 +19682,7 @@ var Watchdog = class {
|
|
|
19142
19682
|
}
|
|
19143
19683
|
async runHeartbeatCheck() {
|
|
19144
19684
|
if (this.stopped) return;
|
|
19685
|
+
markGatewayActivity("watchdog");
|
|
19145
19686
|
try {
|
|
19146
19687
|
await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
|
|
19147
19688
|
} catch (err) {
|
|
@@ -19177,7 +19718,7 @@ var { taskTemplates: taskTemplates3 } = schema_exports;
|
|
|
19177
19718
|
async function cloneTaskFromTemplate(templateId) {
|
|
19178
19719
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
19179
19720
|
const tmpl = rows[0];
|
|
19180
|
-
if (!tmpl) return null;
|
|
19721
|
+
if (!tmpl || !tmpl.enabled) return null;
|
|
19181
19722
|
const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
|
|
19182
19723
|
and(
|
|
19183
19724
|
eq(schema_exports.tasks.templateId, templateId),
|
|
@@ -19186,7 +19727,8 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19186
19727
|
).then((r) => r[0].count);
|
|
19187
19728
|
if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
|
|
19188
19729
|
const nowMs = Date.now();
|
|
19189
|
-
const
|
|
19730
|
+
const isDelayed = tmpl.scheduleType === "delayed";
|
|
19731
|
+
const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
|
|
19190
19732
|
tmpl.scheduleType,
|
|
19191
19733
|
tmpl,
|
|
19192
19734
|
nowMs
|
|
@@ -19200,13 +19742,17 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19200
19742
|
category: tmpl.category ?? "general",
|
|
19201
19743
|
importance: tmpl.importance ?? 3,
|
|
19202
19744
|
urgency: tmpl.urgency ?? 3,
|
|
19745
|
+
batchId: tmpl.batchId,
|
|
19203
19746
|
maxRetries: tmpl.maxRetries ?? 3,
|
|
19747
|
+
retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
|
|
19748
|
+
timeoutMs: tmpl.timeoutMs,
|
|
19204
19749
|
templateId: tmpl.id,
|
|
19205
19750
|
scheduledAt: nowMs
|
|
19206
19751
|
});
|
|
19207
19752
|
await db.update(taskTemplates3).set({
|
|
19208
19753
|
lastRunAt: nowMs,
|
|
19209
19754
|
nextRunAt,
|
|
19755
|
+
enabled: isDelayed ? false : tmpl.enabled,
|
|
19210
19756
|
updatedAt: nowMs
|
|
19211
19757
|
}).where(eq(taskTemplates3.id, templateId));
|
|
19212
19758
|
return task;
|
|
@@ -19219,7 +19765,7 @@ async function getDueTemplates() {
|
|
|
19219
19765
|
sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
|
|
19220
19766
|
sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
|
|
19221
19767
|
)
|
|
19222
|
-
);
|
|
19768
|
+
).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
|
|
19223
19769
|
}
|
|
19224
19770
|
async function initializeNextRunAt(templateId) {
|
|
19225
19771
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
@@ -19237,6 +19783,7 @@ async function initializeNextRunAt(templateId) {
|
|
|
19237
19783
|
// src/gateway/scheduler/index.ts
|
|
19238
19784
|
init_db2();
|
|
19239
19785
|
init_drizzle_orm();
|
|
19786
|
+
init_health();
|
|
19240
19787
|
var Scheduler = class {
|
|
19241
19788
|
constructor(cfg) {
|
|
19242
19789
|
this.cfg = cfg;
|
|
@@ -19248,6 +19795,7 @@ var Scheduler = class {
|
|
|
19248
19795
|
async start() {
|
|
19249
19796
|
if (!this.cfg.scheduler.enabled) return;
|
|
19250
19797
|
this.stopped = false;
|
|
19798
|
+
markGatewayActivity("scheduler");
|
|
19251
19799
|
await this.initializeTemplates();
|
|
19252
19800
|
this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
|
|
19253
19801
|
}
|
|
@@ -19260,6 +19808,7 @@ var Scheduler = class {
|
|
|
19260
19808
|
}
|
|
19261
19809
|
async tick() {
|
|
19262
19810
|
if (this.stopped || this.ticking) return;
|
|
19811
|
+
markGatewayActivity("scheduler");
|
|
19263
19812
|
this.ticking = true;
|
|
19264
19813
|
try {
|
|
19265
19814
|
const dueTemplates = await getDueTemplates();
|
|
@@ -19309,6 +19858,7 @@ var Scheduler = class {
|
|
|
19309
19858
|
init_db2();
|
|
19310
19859
|
init_task_service();
|
|
19311
19860
|
init_task_run_service();
|
|
19861
|
+
init_health();
|
|
19312
19862
|
var STALE_THRESHOLD_MS = 3e4;
|
|
19313
19863
|
function acquireLock() {
|
|
19314
19864
|
const now = Date.now();
|
|
@@ -19317,7 +19867,8 @@ function acquireLock() {
|
|
|
19317
19867
|
sqlite.exec("BEGIN IMMEDIATE");
|
|
19318
19868
|
const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
|
|
19319
19869
|
if (existing) {
|
|
19320
|
-
|
|
19870
|
+
const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
|
|
19871
|
+
if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
|
|
19321
19872
|
sqlite.exec("ROLLBACK");
|
|
19322
19873
|
console.error(JSON.stringify({
|
|
19323
19874
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19330,7 +19881,7 @@ function acquireLock() {
|
|
|
19330
19881
|
sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
|
|
19331
19882
|
}
|
|
19332
19883
|
sqlite.exec(
|
|
19333
|
-
"INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?,
|
|
19884
|
+
"INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
|
|
19334
19885
|
[pid, now, now]
|
|
19335
19886
|
);
|
|
19336
19887
|
sqlite.exec("COMMIT");
|
|
@@ -19364,6 +19915,18 @@ function updateLockHeartbeat() {
|
|
|
19364
19915
|
} catch {
|
|
19365
19916
|
}
|
|
19366
19917
|
}
|
|
19918
|
+
function markGatewayReady() {
|
|
19919
|
+
sqlite.exec(
|
|
19920
|
+
"UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
|
|
19921
|
+
[Date.now(), Date.now(), process.pid]
|
|
19922
|
+
);
|
|
19923
|
+
}
|
|
19924
|
+
function markGatewayNotReady() {
|
|
19925
|
+
try {
|
|
19926
|
+
sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
|
|
19927
|
+
} catch {
|
|
19928
|
+
}
|
|
19929
|
+
}
|
|
19367
19930
|
async function main() {
|
|
19368
19931
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
|
|
19369
19932
|
if (!acquireLock()) {
|
|
@@ -19375,6 +19938,21 @@ async function main() {
|
|
|
19375
19938
|
const worker = new WorkerEngine(cfg);
|
|
19376
19939
|
const watchdog = new Watchdog(cfg);
|
|
19377
19940
|
const scheduler = new Scheduler(cfg);
|
|
19941
|
+
const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
|
|
19942
|
+
if (recoveredOrphans > 0) {
|
|
19943
|
+
console.log(JSON.stringify({
|
|
19944
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19945
|
+
level: "warn",
|
|
19946
|
+
msg: "reset orphan running tasks to pending",
|
|
19947
|
+
count: recoveredOrphans
|
|
19948
|
+
}));
|
|
19949
|
+
}
|
|
19950
|
+
initializeGatewayHealth({
|
|
19951
|
+
workerPollIntervalMs: cfg.worker.pollIntervalMs,
|
|
19952
|
+
schedulerEnabled: cfg.scheduler.enabled,
|
|
19953
|
+
schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
|
|
19954
|
+
watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
|
|
19955
|
+
});
|
|
19378
19956
|
worker.start();
|
|
19379
19957
|
watchdog.start();
|
|
19380
19958
|
await scheduler.start();
|
|
@@ -19392,6 +19970,7 @@ async function main() {
|
|
|
19392
19970
|
url: `http://localhost:${cfg.dashboard.port}`
|
|
19393
19971
|
}));
|
|
19394
19972
|
}
|
|
19973
|
+
markGatewayReady();
|
|
19395
19974
|
console.log(JSON.stringify({
|
|
19396
19975
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19397
19976
|
level: "info",
|
|
@@ -19405,10 +19984,10 @@ async function main() {
|
|
|
19405
19984
|
shuttingDown = true;
|
|
19406
19985
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
|
|
19407
19986
|
clearInterval(heartbeatTimer);
|
|
19987
|
+
markGatewayNotReady();
|
|
19408
19988
|
scheduler.stop();
|
|
19409
19989
|
watchdog.stop();
|
|
19410
|
-
const runningIds = worker.
|
|
19411
|
-
await worker.stop();
|
|
19990
|
+
const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
|
|
19412
19991
|
if (runningIds.length > 0) {
|
|
19413
19992
|
const resetCount = await TaskService.resetRunningToPending(runningIds);
|
|
19414
19993
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
|
|
@@ -19418,6 +19997,7 @@ async function main() {
|
|
|
19418
19997
|
await TaskRunService.fail(run.id, "Gateway shutdown");
|
|
19419
19998
|
}
|
|
19420
19999
|
releaseLock();
|
|
20000
|
+
resetGatewayHealth();
|
|
19421
20001
|
closeDb();
|
|
19422
20002
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
|
|
19423
20003
|
process.exit(0);
|
|
@@ -19437,6 +20017,8 @@ if (import.meta.main) {
|
|
|
19437
20017
|
main();
|
|
19438
20018
|
}
|
|
19439
20019
|
export {
|
|
19440
|
-
|
|
20020
|
+
acquireLock,
|
|
20021
|
+
main,
|
|
20022
|
+
releaseLock
|
|
19441
20023
|
};
|
|
19442
20024
|
//# sourceMappingURL=index.js.map
|