opencode-supertask 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -25
- package/dist/cli/index.js +1245 -579
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.js +838 -294
- package/dist/gateway/index.js.map +1 -1
- package/dist/plugin/supertask.js +576 -232
- package/dist/plugin/supertask.js.map +1 -1
- package/dist/web/index.js +537 -212
- package/dist/web/index.js.map +1 -1
- package/dist/worker/index.d.ts +19 -7
- package/dist/worker/index.js +425 -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;
|
|
6215
|
+
}
|
|
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`);
|
|
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;
|
|
6192
6229
|
}
|
|
6193
|
-
function
|
|
6194
|
-
|
|
6195
|
-
|
|
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");
|
|
6196
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
|
}
|
|
@@ -6432,8 +6544,20 @@ var init_task_service = __esm({
|
|
|
6432
6544
|
return result.length;
|
|
6433
6545
|
}
|
|
6434
6546
|
static async cancel(id, scope = {}) {
|
|
6435
|
-
const conditions = [
|
|
6436
|
-
|
|
6547
|
+
const conditions = [
|
|
6548
|
+
eq(tasks2.id, id),
|
|
6549
|
+
or(
|
|
6550
|
+
eq(tasks2.status, "pending"),
|
|
6551
|
+
eq(tasks2.status, "running"),
|
|
6552
|
+
eq(tasks2.status, "failed")
|
|
6553
|
+
),
|
|
6554
|
+
...this.buildScopeWhere(scope)
|
|
6555
|
+
];
|
|
6556
|
+
const result = await db.update(tasks2).set({
|
|
6557
|
+
status: "cancelled",
|
|
6558
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
6559
|
+
retryAfter: null
|
|
6560
|
+
}).where(and(...conditions)).returning();
|
|
6437
6561
|
return result[0] || null;
|
|
6438
6562
|
}
|
|
6439
6563
|
static async retry(id, scope = {}) {
|
|
@@ -6445,7 +6569,8 @@ var init_task_service = __esm({
|
|
|
6445
6569
|
status: "pending",
|
|
6446
6570
|
startedAt: null,
|
|
6447
6571
|
finishedAt: null,
|
|
6448
|
-
retryAfter: null
|
|
6572
|
+
retryAfter: null,
|
|
6573
|
+
retryCount: 0
|
|
6449
6574
|
}).where(and(...conditions)).returning();
|
|
6450
6575
|
return result[0] || null;
|
|
6451
6576
|
}
|
|
@@ -6459,7 +6584,8 @@ var init_task_service = __esm({
|
|
|
6459
6584
|
status: "pending",
|
|
6460
6585
|
startedAt: null,
|
|
6461
6586
|
finishedAt: null,
|
|
6462
|
-
retryAfter: null
|
|
6587
|
+
retryAfter: null,
|
|
6588
|
+
retryCount: 0
|
|
6463
6589
|
}).where(and(...conditions)).returning();
|
|
6464
6590
|
return result.length;
|
|
6465
6591
|
}
|
|
@@ -6527,6 +6653,9 @@ var init_task_service = __esm({
|
|
|
6527
6653
|
}
|
|
6528
6654
|
static async delete(id, scope = {}) {
|
|
6529
6655
|
const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
|
|
6656
|
+
const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
|
|
6657
|
+
if (!existing[0]) return false;
|
|
6658
|
+
await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
|
|
6530
6659
|
const result = await db.delete(tasks2).where(and(...conditions)).returning();
|
|
6531
6660
|
return result.length > 0;
|
|
6532
6661
|
}
|
|
@@ -6546,65 +6675,65 @@ var init_task_service = __esm({
|
|
|
6546
6675
|
});
|
|
6547
6676
|
|
|
6548
6677
|
// src/core/services/task-run.service.ts
|
|
6549
|
-
var
|
|
6678
|
+
var taskRuns3, TaskRunService;
|
|
6550
6679
|
var init_task_run_service = __esm({
|
|
6551
6680
|
"src/core/services/task-run.service.ts"() {
|
|
6552
6681
|
"use strict";
|
|
6553
6682
|
init_db2();
|
|
6554
6683
|
init_drizzle_orm();
|
|
6555
|
-
({ taskRuns:
|
|
6684
|
+
({ taskRuns: taskRuns3 } = schema_exports);
|
|
6556
6685
|
TaskRunService = class {
|
|
6557
6686
|
static async create(data) {
|
|
6558
|
-
const result = await db.insert(
|
|
6687
|
+
const result = await db.insert(taskRuns3).values(data).returning();
|
|
6559
6688
|
return result[0];
|
|
6560
6689
|
}
|
|
6561
6690
|
static async updateSessionId(id, sessionId) {
|
|
6562
|
-
const result = await db.update(
|
|
6691
|
+
const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6563
6692
|
return result[0] || null;
|
|
6564
6693
|
}
|
|
6565
6694
|
static async done(id, log) {
|
|
6566
|
-
const result = await db.update(
|
|
6695
|
+
const result = await db.update(taskRuns3).set({
|
|
6567
6696
|
status: "done",
|
|
6568
6697
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6569
6698
|
log
|
|
6570
|
-
}).where(eq(
|
|
6699
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6571
6700
|
return result[0] || null;
|
|
6572
6701
|
}
|
|
6573
6702
|
static async fail(id, log) {
|
|
6574
|
-
const result = await db.update(
|
|
6703
|
+
const result = await db.update(taskRuns3).set({
|
|
6575
6704
|
status: "failed",
|
|
6576
6705
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6577
6706
|
log
|
|
6578
|
-
}).where(eq(
|
|
6707
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6579
6708
|
return result[0] || null;
|
|
6580
6709
|
}
|
|
6581
6710
|
static async heartbeat(id) {
|
|
6582
|
-
const result = await db.update(
|
|
6711
|
+
const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6583
6712
|
return result[0] || null;
|
|
6584
6713
|
}
|
|
6585
6714
|
static async updatePid(id, workerPid, childPid) {
|
|
6586
|
-
const result = await db.update(
|
|
6715
|
+
const result = await db.update(taskRuns3).set({
|
|
6587
6716
|
workerPid,
|
|
6588
6717
|
childPid,
|
|
6589
6718
|
lockedAt: Date.now(),
|
|
6590
6719
|
lockedBy: `gateway-${process.pid}`
|
|
6591
|
-
}).where(eq(
|
|
6720
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6592
6721
|
return result[0] || null;
|
|
6593
6722
|
}
|
|
6594
6723
|
static async getById(id) {
|
|
6595
|
-
const result = await db.select().from(
|
|
6724
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
|
|
6596
6725
|
return result[0] || null;
|
|
6597
6726
|
}
|
|
6598
6727
|
static async listByTaskId(taskId) {
|
|
6599
|
-
return await db.select().from(
|
|
6728
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
6600
6729
|
}
|
|
6601
6730
|
static async getLatestByTaskId(taskId) {
|
|
6602
|
-
const result = await db.select().from(
|
|
6731
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
|
|
6603
6732
|
return result[0] || null;
|
|
6604
6733
|
}
|
|
6605
6734
|
static async getLatestByTaskIds(taskIds) {
|
|
6606
6735
|
if (taskIds.length === 0) return /* @__PURE__ */ new Map();
|
|
6607
|
-
const latestRuns = await db.select().from(
|
|
6736
|
+
const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
6608
6737
|
const result = /* @__PURE__ */ new Map();
|
|
6609
6738
|
for (const run of latestRuns) {
|
|
6610
6739
|
if (!result.has(run.taskId)) {
|
|
@@ -6618,22 +6747,23 @@ var init_task_run_service = __esm({
|
|
|
6618
6747
|
const cutoffSec = Math.floor(cutoffMs / 1e3);
|
|
6619
6748
|
const { tasks: tasksTable } = schema_exports;
|
|
6620
6749
|
const result = await db.select({
|
|
6621
|
-
runId:
|
|
6622
|
-
taskId:
|
|
6623
|
-
childPid:
|
|
6750
|
+
runId: taskRuns3.id,
|
|
6751
|
+
taskId: taskRuns3.taskId,
|
|
6752
|
+
childPid: taskRuns3.childPid,
|
|
6624
6753
|
taskRetryCount: tasksTable.retryCount,
|
|
6625
|
-
taskMaxRetries: tasksTable.maxRetries
|
|
6626
|
-
|
|
6754
|
+
taskMaxRetries: tasksTable.maxRetries,
|
|
6755
|
+
taskRetryBackoffMs: tasksTable.retryBackoffMs
|
|
6756
|
+
}).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
|
|
6627
6757
|
and(
|
|
6628
|
-
eq(
|
|
6758
|
+
eq(taskRuns3.status, "running"),
|
|
6629
6759
|
or(
|
|
6630
6760
|
and(
|
|
6631
|
-
sql`${
|
|
6632
|
-
sql`${
|
|
6761
|
+
sql`${taskRuns3.heartbeatAt} IS NULL`,
|
|
6762
|
+
sql`${taskRuns3.startedAt} < ${cutoffSec}`
|
|
6633
6763
|
),
|
|
6634
6764
|
and(
|
|
6635
|
-
sql`${
|
|
6636
|
-
sql`${
|
|
6765
|
+
sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
|
|
6766
|
+
sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
|
|
6637
6767
|
)
|
|
6638
6768
|
)
|
|
6639
6769
|
)
|
|
@@ -6643,25 +6773,111 @@ var init_task_run_service = __esm({
|
|
|
6643
6773
|
taskId: row.taskId,
|
|
6644
6774
|
childPid: row.childPid,
|
|
6645
6775
|
taskRetryCount: row.taskRetryCount ?? 0,
|
|
6646
|
-
taskMaxRetries: row.taskMaxRetries ?? 3
|
|
6776
|
+
taskMaxRetries: row.taskMaxRetries ?? 3,
|
|
6777
|
+
taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
|
|
6647
6778
|
}));
|
|
6648
6779
|
}
|
|
6649
6780
|
static async getRunningRunByTaskId(taskId) {
|
|
6650
|
-
const result = await db.select().from(
|
|
6781
|
+
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
6782
|
return result[0] || null;
|
|
6652
6783
|
}
|
|
6653
6784
|
static async deleteByTaskIds(taskIds) {
|
|
6654
6785
|
if (taskIds.length === 0) return 0;
|
|
6655
|
-
const result = await db.delete(
|
|
6786
|
+
const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
|
|
6656
6787
|
return result.length;
|
|
6657
6788
|
}
|
|
6658
6789
|
static async getAllRunningRuns() {
|
|
6659
|
-
return await db.select().from(
|
|
6790
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
|
|
6660
6791
|
}
|
|
6661
6792
|
};
|
|
6662
6793
|
}
|
|
6663
6794
|
});
|
|
6664
6795
|
|
|
6796
|
+
// src/gateway/health.ts
|
|
6797
|
+
function initializeGatewayHealth(config) {
|
|
6798
|
+
const now = Date.now();
|
|
6799
|
+
state = {
|
|
6800
|
+
startedAt: now,
|
|
6801
|
+
config,
|
|
6802
|
+
lastActivityAt: {
|
|
6803
|
+
worker: now,
|
|
6804
|
+
scheduler: now,
|
|
6805
|
+
watchdog: now
|
|
6806
|
+
}
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6809
|
+
function markGatewayActivity(component) {
|
|
6810
|
+
if (state) state.lastActivityAt[component] = Date.now();
|
|
6811
|
+
}
|
|
6812
|
+
function resetGatewayHealth() {
|
|
6813
|
+
state = null;
|
|
6814
|
+
}
|
|
6815
|
+
function componentStatus(component, enabled, maxAgeMs, now) {
|
|
6816
|
+
const lastActivityAt = state?.lastActivityAt[component] ?? null;
|
|
6817
|
+
const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
|
|
6818
|
+
return {
|
|
6819
|
+
enabled,
|
|
6820
|
+
healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
|
|
6821
|
+
lastActivityAt,
|
|
6822
|
+
ageMs,
|
|
6823
|
+
maxAgeMs
|
|
6824
|
+
};
|
|
6825
|
+
}
|
|
6826
|
+
function getGatewayHealth(now = Date.now()) {
|
|
6827
|
+
const worker = componentStatus(
|
|
6828
|
+
"worker",
|
|
6829
|
+
true,
|
|
6830
|
+
Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
|
|
6831
|
+
now
|
|
6832
|
+
);
|
|
6833
|
+
const scheduler = componentStatus(
|
|
6834
|
+
"scheduler",
|
|
6835
|
+
state?.config.schedulerEnabled ?? false,
|
|
6836
|
+
Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
|
|
6837
|
+
now
|
|
6838
|
+
);
|
|
6839
|
+
const watchdog = componentStatus(
|
|
6840
|
+
"watchdog",
|
|
6841
|
+
true,
|
|
6842
|
+
Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
|
|
6843
|
+
now
|
|
6844
|
+
);
|
|
6845
|
+
let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
|
|
6846
|
+
try {
|
|
6847
|
+
const row = sqlite.prepare(
|
|
6848
|
+
"SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
|
|
6849
|
+
).get();
|
|
6850
|
+
if (row) {
|
|
6851
|
+
const ageMs = Math.max(0, now - row.heartbeat_at);
|
|
6852
|
+
lock = {
|
|
6853
|
+
pid: row.pid,
|
|
6854
|
+
heartbeatAt: row.heartbeat_at,
|
|
6855
|
+
readyAt: row.ready_at,
|
|
6856
|
+
ageMs,
|
|
6857
|
+
healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
|
|
6858
|
+
};
|
|
6859
|
+
}
|
|
6860
|
+
} catch {
|
|
6861
|
+
}
|
|
6862
|
+
const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
|
|
6863
|
+
return {
|
|
6864
|
+
status: healthy ? "ok" : "degraded",
|
|
6865
|
+
pid: process.pid,
|
|
6866
|
+
uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
|
|
6867
|
+
lock,
|
|
6868
|
+
components: { worker, scheduler, watchdog }
|
|
6869
|
+
};
|
|
6870
|
+
}
|
|
6871
|
+
var LOCK_STALE_MS, state;
|
|
6872
|
+
var init_health = __esm({
|
|
6873
|
+
"src/gateway/health.ts"() {
|
|
6874
|
+
"use strict";
|
|
6875
|
+
init_db2();
|
|
6876
|
+
LOCK_STALE_MS = 3e4;
|
|
6877
|
+
state = null;
|
|
6878
|
+
}
|
|
6879
|
+
});
|
|
6880
|
+
|
|
6665
6881
|
// node_modules/cron-parser/dist/fields/types.js
|
|
6666
6882
|
var require_types = __commonJS({
|
|
6667
6883
|
"node_modules/cron-parser/dist/fields/types.js"(exports) {
|
|
@@ -6862,7 +7078,7 @@ var require_CronField = __commonJS({
|
|
|
6862
7078
|
if (!isValidRange) {
|
|
6863
7079
|
throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
|
|
6864
7080
|
}
|
|
6865
|
-
const duplicate = this.#values.find((value,
|
|
7081
|
+
const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
|
|
6866
7082
|
if (duplicate) {
|
|
6867
7083
|
throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
|
|
6868
7084
|
}
|
|
@@ -14707,11 +14923,11 @@ var require_CronFieldCollection = __commonJS({
|
|
|
14707
14923
|
throw new Error("Unexpected range end");
|
|
14708
14924
|
}
|
|
14709
14925
|
if (step * multiplier > range.end) {
|
|
14710
|
-
const mapFn = (_,
|
|
14926
|
+
const mapFn = (_, index2) => {
|
|
14711
14927
|
if (typeof range.start !== "number") {
|
|
14712
14928
|
throw new Error("Unexpected range start");
|
|
14713
14929
|
}
|
|
14714
|
-
return
|
|
14930
|
+
return index2 % step === 0 ? range.start + index2 : null;
|
|
14715
14931
|
};
|
|
14716
14932
|
if (typeof range.start !== "number") {
|
|
14717
14933
|
throw new Error("Unexpected range start");
|
|
@@ -15603,9 +15819,9 @@ var require_CronExpressionParser = __commonJS({
|
|
|
15603
15819
|
if (field === CronUnit.DayOfWeek && max % 7 === 0) {
|
|
15604
15820
|
stack.push(0);
|
|
15605
15821
|
}
|
|
15606
|
-
for (let
|
|
15607
|
-
if (stack.indexOf(
|
|
15608
|
-
stack.push(
|
|
15822
|
+
for (let index2 = min; index2 <= max; index2 += repeatInterval) {
|
|
15823
|
+
if (stack.indexOf(index2) === -1) {
|
|
15824
|
+
stack.push(index2);
|
|
15609
15825
|
}
|
|
15610
15826
|
}
|
|
15611
15827
|
return stack;
|
|
@@ -15828,11 +16044,6 @@ var require_dist = __commonJS({
|
|
|
15828
16044
|
});
|
|
15829
16045
|
|
|
15830
16046
|
// src/core/cron-parser.ts
|
|
15831
|
-
var cron_parser_exports = {};
|
|
15832
|
-
__export(cron_parser_exports, {
|
|
15833
|
-
getNextCronRun: () => getNextCronRun,
|
|
15834
|
-
isValidCronExpr: () => isValidCronExpr
|
|
15835
|
-
});
|
|
15836
16047
|
function getNextCronRun(expr, afterMs) {
|
|
15837
16048
|
try {
|
|
15838
16049
|
const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
|
|
@@ -15870,6 +16081,7 @@ var init_task_template_service = __esm({
|
|
|
15870
16081
|
({ taskTemplates: taskTemplates2 } = schema_exports);
|
|
15871
16082
|
TaskTemplateService = class {
|
|
15872
16083
|
static async create(data) {
|
|
16084
|
+
this.validate(data);
|
|
15873
16085
|
const now = Date.now();
|
|
15874
16086
|
const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
|
|
15875
16087
|
const tmpl = result[0];
|
|
@@ -15885,8 +16097,38 @@ var init_task_template_service = __esm({
|
|
|
15885
16097
|
}
|
|
15886
16098
|
return tmpl;
|
|
15887
16099
|
}
|
|
16100
|
+
static validate(data) {
|
|
16101
|
+
if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16102
|
+
if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16103
|
+
if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
|
|
16104
|
+
const scheduleType = data.scheduleType;
|
|
16105
|
+
if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
|
|
16106
|
+
throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
|
|
16107
|
+
}
|
|
16108
|
+
if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
|
|
16109
|
+
throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
|
|
16110
|
+
}
|
|
16111
|
+
if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
|
|
16112
|
+
throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
|
|
16113
|
+
}
|
|
16114
|
+
if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
|
|
16115
|
+
throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
|
|
16116
|
+
}
|
|
16117
|
+
this.validateInteger("importance", data.importance, 1, 5);
|
|
16118
|
+
this.validateInteger("urgency", data.urgency, 1, 5);
|
|
16119
|
+
this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
|
|
16120
|
+
this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
|
|
16121
|
+
this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
|
|
16122
|
+
this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
|
|
16123
|
+
}
|
|
16124
|
+
static validateInteger(name, value, min, max) {
|
|
16125
|
+
if (value === void 0 || value === null) return;
|
|
16126
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
16127
|
+
throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
|
|
16128
|
+
}
|
|
16129
|
+
}
|
|
15888
16130
|
static async list(limit = 50) {
|
|
15889
|
-
return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
|
|
16131
|
+
return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
|
|
15890
16132
|
}
|
|
15891
16133
|
static async getById(id) {
|
|
15892
16134
|
const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
|
|
@@ -15933,13 +16175,13 @@ var init_compose = __esm({
|
|
|
15933
16175
|
"use strict";
|
|
15934
16176
|
compose = (middleware, onError, onNotFound) => {
|
|
15935
16177
|
return (context, next) => {
|
|
15936
|
-
let
|
|
16178
|
+
let index2 = -1;
|
|
15937
16179
|
return dispatch(0);
|
|
15938
16180
|
async function dispatch(i) {
|
|
15939
|
-
if (i <=
|
|
16181
|
+
if (i <= index2) {
|
|
15940
16182
|
throw new Error("next() called multiple times");
|
|
15941
16183
|
}
|
|
15942
|
-
|
|
16184
|
+
index2 = i;
|
|
15943
16185
|
let res;
|
|
15944
16186
|
let isError = false;
|
|
15945
16187
|
let handler;
|
|
@@ -16054,8 +16296,8 @@ var init_body = __esm({
|
|
|
16054
16296
|
handleParsingNestedValues = (form, key, value) => {
|
|
16055
16297
|
let nestedForm = form;
|
|
16056
16298
|
const keys = key.split(".");
|
|
16057
|
-
keys.forEach((key2,
|
|
16058
|
-
if (
|
|
16299
|
+
keys.forEach((key2, index2) => {
|
|
16300
|
+
if (index2 === keys.length - 1) {
|
|
16059
16301
|
nestedForm[key2] = value;
|
|
16060
16302
|
} else {
|
|
16061
16303
|
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
@@ -16087,8 +16329,8 @@ var init_url = __esm({
|
|
|
16087
16329
|
};
|
|
16088
16330
|
extractGroupsFromPath = (path) => {
|
|
16089
16331
|
const groups = [];
|
|
16090
|
-
path = path.replace(/\{[^}]+\}/g, (match2,
|
|
16091
|
-
const mark = `@${
|
|
16332
|
+
path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
|
|
16333
|
+
const mark = `@${index2}`;
|
|
16092
16334
|
groups.push([mark, match2]);
|
|
16093
16335
|
return mark;
|
|
16094
16336
|
});
|
|
@@ -16607,10 +16849,10 @@ var init_html = __esm({
|
|
|
16607
16849
|
return;
|
|
16608
16850
|
}
|
|
16609
16851
|
let escape;
|
|
16610
|
-
let
|
|
16852
|
+
let index2;
|
|
16611
16853
|
let lastIndex = 0;
|
|
16612
|
-
for (
|
|
16613
|
-
switch (str.charCodeAt(
|
|
16854
|
+
for (index2 = match2; index2 < str.length; index2++) {
|
|
16855
|
+
switch (str.charCodeAt(index2)) {
|
|
16614
16856
|
case 34:
|
|
16615
16857
|
escape = """;
|
|
16616
16858
|
break;
|
|
@@ -16629,10 +16871,10 @@ var init_html = __esm({
|
|
|
16629
16871
|
default:
|
|
16630
16872
|
continue;
|
|
16631
16873
|
}
|
|
16632
|
-
buffer[0] += str.substring(lastIndex,
|
|
16633
|
-
lastIndex =
|
|
16874
|
+
buffer[0] += str.substring(lastIndex, index2) + escape;
|
|
16875
|
+
lastIndex = index2 + 1;
|
|
16634
16876
|
}
|
|
16635
|
-
buffer[0] += str.substring(lastIndex,
|
|
16877
|
+
buffer[0] += str.substring(lastIndex, index2);
|
|
16636
16878
|
};
|
|
16637
16879
|
resolveCallbackSync = (str) => {
|
|
16638
16880
|
const callbacks = str.callbacks;
|
|
@@ -17508,8 +17750,8 @@ function match(method, path) {
|
|
|
17508
17750
|
if (!match3) {
|
|
17509
17751
|
return [[], emptyParam];
|
|
17510
17752
|
}
|
|
17511
|
-
const
|
|
17512
|
-
return [matcher[1][
|
|
17753
|
+
const index2 = match3.indexOf("", 1);
|
|
17754
|
+
return [matcher[1][index2], match3];
|
|
17513
17755
|
});
|
|
17514
17756
|
this.match = match2;
|
|
17515
17757
|
return match2(method, path);
|
|
@@ -17556,7 +17798,7 @@ var init_node = __esm({
|
|
|
17556
17798
|
#index;
|
|
17557
17799
|
#varIndex;
|
|
17558
17800
|
#children = /* @__PURE__ */ Object.create(null);
|
|
17559
|
-
insert(tokens,
|
|
17801
|
+
insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
|
|
17560
17802
|
if (tokens.length === 0) {
|
|
17561
17803
|
if (this.#index !== void 0) {
|
|
17562
17804
|
throw PATH_ERROR;
|
|
@@ -17564,7 +17806,7 @@ var init_node = __esm({
|
|
|
17564
17806
|
if (pathErrorCheckOnly) {
|
|
17565
17807
|
return;
|
|
17566
17808
|
}
|
|
17567
|
-
this.#index =
|
|
17809
|
+
this.#index = index2;
|
|
17568
17810
|
return;
|
|
17569
17811
|
}
|
|
17570
17812
|
const [token, ...restTokens] = tokens;
|
|
@@ -17614,7 +17856,7 @@ var init_node = __esm({
|
|
|
17614
17856
|
node = this.#children[token] = new _Node();
|
|
17615
17857
|
}
|
|
17616
17858
|
}
|
|
17617
|
-
node.insert(restTokens,
|
|
17859
|
+
node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
|
|
17618
17860
|
}
|
|
17619
17861
|
buildRegExpStr() {
|
|
17620
17862
|
const childKeys = Object.keys(this.#children).sort(compareKey);
|
|
@@ -17646,7 +17888,7 @@ var init_trie = __esm({
|
|
|
17646
17888
|
Trie = class {
|
|
17647
17889
|
#context = { varIndex: 0 };
|
|
17648
17890
|
#root = new Node();
|
|
17649
|
-
insert(path,
|
|
17891
|
+
insert(path, index2, pathErrorCheckOnly) {
|
|
17650
17892
|
const paramAssoc = [];
|
|
17651
17893
|
const groups = [];
|
|
17652
17894
|
for (let i = 0; ; ) {
|
|
@@ -17672,7 +17914,7 @@ var init_trie = __esm({
|
|
|
17672
17914
|
}
|
|
17673
17915
|
}
|
|
17674
17916
|
}
|
|
17675
|
-
this.#root.insert(tokens,
|
|
17917
|
+
this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
|
|
17676
17918
|
return paramAssoc;
|
|
17677
17919
|
}
|
|
17678
17920
|
buildRegExp() {
|
|
@@ -18266,8 +18508,19 @@ __export(web_exports, {
|
|
|
18266
18508
|
dashboardApp: () => dashboardApp,
|
|
18267
18509
|
default: () => web_default
|
|
18268
18510
|
});
|
|
18269
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
18511
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
|
|
18270
18512
|
import { dirname as dirname2 } from "path";
|
|
18513
|
+
function parsePositiveInteger(value) {
|
|
18514
|
+
if (!/^\d+$/.test(value)) return null;
|
|
18515
|
+
const parsed = Number(value);
|
|
18516
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
18517
|
+
}
|
|
18518
|
+
function parseTaskStatus(value) {
|
|
18519
|
+
return TASK_STATUSES.has(value) ? value : null;
|
|
18520
|
+
}
|
|
18521
|
+
function safeStatus(value) {
|
|
18522
|
+
return value && TASK_STATUSES.has(value) ? value : "unknown";
|
|
18523
|
+
}
|
|
18271
18524
|
function formatDuration(startAt, endAt) {
|
|
18272
18525
|
if (!startAt) return "-";
|
|
18273
18526
|
const start = new Date(startAt).getTime();
|
|
@@ -18305,17 +18558,21 @@ function esc(s) {
|
|
|
18305
18558
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
18306
18559
|
}
|
|
18307
18560
|
function readCurrentConfig() {
|
|
18308
|
-
|
|
18561
|
+
const configPath = getConfigPath();
|
|
18562
|
+
if (!existsSync3(configPath)) return {};
|
|
18309
18563
|
try {
|
|
18310
|
-
return JSON.parse(readFileSync2(
|
|
18564
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
18311
18565
|
} catch {
|
|
18312
18566
|
return {};
|
|
18313
18567
|
}
|
|
18314
18568
|
}
|
|
18315
18569
|
function writeConfig(cfg) {
|
|
18316
|
-
const
|
|
18570
|
+
const configPath = getConfigPath();
|
|
18571
|
+
const dir = dirname2(configPath);
|
|
18317
18572
|
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
18318
|
-
|
|
18573
|
+
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
18574
|
+
writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
18575
|
+
renameSync(tempPath, configPath);
|
|
18319
18576
|
}
|
|
18320
18577
|
function renderLayout(title, activeTab, body) {
|
|
18321
18578
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -18354,11 +18611,11 @@ async function saveConfig(){
|
|
|
18354
18611
|
scheduler:{
|
|
18355
18612
|
enabled:form.se.checked,
|
|
18356
18613
|
checkIntervalMs:Number(form.si.value),
|
|
18357
|
-
catchUp:form.cu.value,
|
|
18358
18614
|
},
|
|
18359
18615
|
watchdog:{
|
|
18360
18616
|
heartbeatTimeoutMs:Number(form.wt.value)*1000,
|
|
18361
|
-
|
|
18617
|
+
checkIntervalMs:Number(form.wci.value)*1000,
|
|
18618
|
+
cleanupIntervalMs:Number(form.wcl.value)*3600000,
|
|
18362
18619
|
retentionDays:Number(form.rd.value),
|
|
18363
18620
|
}
|
|
18364
18621
|
};
|
|
@@ -18389,7 +18646,7 @@ async function saveConfig(){
|
|
|
18389
18646
|
<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
18647
|
</body></html>`;
|
|
18391
18648
|
}
|
|
18392
|
-
var app, SHARED_STYLES, dashboardApp, web_default;
|
|
18649
|
+
var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
|
|
18393
18650
|
var init_web = __esm({
|
|
18394
18651
|
"src/web/index.tsx"() {
|
|
18395
18652
|
"use strict";
|
|
@@ -18401,7 +18658,47 @@ var init_web = __esm({
|
|
|
18401
18658
|
init_drizzle_orm();
|
|
18402
18659
|
init_db2();
|
|
18403
18660
|
init_config();
|
|
18661
|
+
init_health();
|
|
18404
18662
|
app = new Hono2();
|
|
18663
|
+
TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
18664
|
+
"pending",
|
|
18665
|
+
"running",
|
|
18666
|
+
"done",
|
|
18667
|
+
"failed",
|
|
18668
|
+
"dead_letter",
|
|
18669
|
+
"cancelled"
|
|
18670
|
+
]);
|
|
18671
|
+
app.use("*", async (c, next) => {
|
|
18672
|
+
await next();
|
|
18673
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
18674
|
+
c.header("X-Frame-Options", "DENY");
|
|
18675
|
+
c.header("Referrer-Policy", "no-referrer");
|
|
18676
|
+
});
|
|
18677
|
+
app.use("/api/*", async (c, next) => {
|
|
18678
|
+
if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
|
|
18679
|
+
const fetchSite = c.req.header("Sec-Fetch-Site");
|
|
18680
|
+
if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
|
|
18681
|
+
return c.json({ error: "cross-site request rejected" }, 403);
|
|
18682
|
+
}
|
|
18683
|
+
const origin = c.req.header("Origin");
|
|
18684
|
+
if (origin) {
|
|
18685
|
+
try {
|
|
18686
|
+
const originUrl = new URL(origin);
|
|
18687
|
+
const requestUrl = new URL(c.req.url);
|
|
18688
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
|
|
18689
|
+
if (!loopback || originUrl.origin !== requestUrl.origin) {
|
|
18690
|
+
return c.json({ error: "cross-site request rejected" }, 403);
|
|
18691
|
+
}
|
|
18692
|
+
} catch {
|
|
18693
|
+
return c.json({ error: "invalid origin" }, 403);
|
|
18694
|
+
}
|
|
18695
|
+
}
|
|
18696
|
+
return next();
|
|
18697
|
+
});
|
|
18698
|
+
app.get("/health", (c) => {
|
|
18699
|
+
const health = getGatewayHealth();
|
|
18700
|
+
return c.json(health, health.status === "ok" ? 200 : 503);
|
|
18701
|
+
});
|
|
18405
18702
|
SHARED_STYLES = html`
|
|
18406
18703
|
<style>
|
|
18407
18704
|
:root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
|
|
@@ -18478,12 +18775,16 @@ var init_web = __esm({
|
|
|
18478
18775
|
</style>
|
|
18479
18776
|
`;
|
|
18480
18777
|
app.get("/", async (c) => {
|
|
18481
|
-
const
|
|
18778
|
+
const pageParam = c.req.query("page") || "1";
|
|
18779
|
+
const page = parsePositiveInteger(pageParam);
|
|
18780
|
+
if (page === null) return c.text("invalid page", 400);
|
|
18482
18781
|
const statusFilter = c.req.query("status") || "";
|
|
18782
|
+
const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
|
|
18783
|
+
if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
|
|
18483
18784
|
const limit = 50;
|
|
18484
18785
|
const offset = (page - 1) * limit;
|
|
18485
18786
|
const [tasks3, statsData] = await Promise.all([
|
|
18486
|
-
TaskService.list({ limit, offset, ...
|
|
18787
|
+
TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
|
|
18487
18788
|
TaskService.stats({})
|
|
18488
18789
|
]);
|
|
18489
18790
|
const taskIds = tasks3.map((t) => t.id);
|
|
@@ -18506,12 +18807,13 @@ var init_web = __esm({
|
|
|
18506
18807
|
</div>`;
|
|
18507
18808
|
let rows = "";
|
|
18508
18809
|
for (const task of tasks3) {
|
|
18509
|
-
const
|
|
18810
|
+
const status = safeStatus(task.status);
|
|
18811
|
+
const st = status.toUpperCase();
|
|
18510
18812
|
rows += `<tr>
|
|
18511
18813
|
<td class="mu">#${task.id}</td>
|
|
18512
18814
|
<td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
|
|
18513
18815
|
<td><span class="tag">${esc(task.agent)}</span></td>
|
|
18514
|
-
<td><span class="badge b-${
|
|
18816
|
+
<td><span class="badge b-${status}">${st}</span></td>
|
|
18515
18817
|
<td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
|
|
18516
18818
|
<td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
|
|
18517
18819
|
<td>
|
|
@@ -18543,21 +18845,13 @@ var init_web = __esm({
|
|
|
18543
18845
|
});
|
|
18544
18846
|
app.get("/templates", async (c) => {
|
|
18545
18847
|
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
18848
|
const enabled = templates.filter((t) => t.enabled).length;
|
|
18556
18849
|
const disabled = templates.length - enabled;
|
|
18557
18850
|
let rows = "";
|
|
18558
18851
|
for (const t of templates) {
|
|
18559
|
-
const
|
|
18560
|
-
const
|
|
18852
|
+
const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
|
|
18853
|
+
const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
|
|
18854
|
+
const typeClass = "tag t-" + scheduleType;
|
|
18561
18855
|
let rule = "-";
|
|
18562
18856
|
if (t.scheduleType === "cron") rule = t.cronExpr || "-";
|
|
18563
18857
|
else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
|
|
@@ -18569,7 +18863,7 @@ var init_web = __esm({
|
|
|
18569
18863
|
<td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
|
|
18570
18864
|
<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
18865
|
<td><span class="${typeClass}">${typeLabel}</span></td>
|
|
18572
|
-
<td class="m sm">${rule}</td>
|
|
18866
|
+
<td class="m sm">${esc(rule)}</td>
|
|
18573
18867
|
<td>${statusBadge}</td>
|
|
18574
18868
|
<td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
|
|
18575
18869
|
<td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
|
|
@@ -18597,7 +18891,8 @@ var init_web = __esm({
|
|
|
18597
18891
|
return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
|
|
18598
18892
|
});
|
|
18599
18893
|
app.get("/runs", async (c) => {
|
|
18600
|
-
const page =
|
|
18894
|
+
const page = parsePositiveInteger(c.req.query("page") || "1");
|
|
18895
|
+
if (page === null) return c.text("invalid page", 400);
|
|
18601
18896
|
const limit = 50;
|
|
18602
18897
|
const offset = (page - 1) * limit;
|
|
18603
18898
|
const { taskRuns: tr, tasks: tk } = schema_exports;
|
|
@@ -18615,13 +18910,14 @@ var init_web = __esm({
|
|
|
18615
18910
|
childPid: tr.childPid,
|
|
18616
18911
|
taskName: tk.name,
|
|
18617
18912
|
taskAgent: tk.agent
|
|
18618
|
-
}).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
|
|
18913
|
+
}).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
|
|
18619
18914
|
const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
|
|
18620
18915
|
const total = Number(totalResult[0]?.count ?? 0);
|
|
18621
18916
|
const totalPages = Math.ceil(total / limit);
|
|
18622
18917
|
let rows = "";
|
|
18623
18918
|
const logsHtml = [];
|
|
18624
18919
|
for (const run of runs) {
|
|
18920
|
+
const status = safeStatus(run.status);
|
|
18625
18921
|
const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
18626
18922
|
const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
|
|
18627
18923
|
rows += `<tr>
|
|
@@ -18629,15 +18925,15 @@ var init_web = __esm({
|
|
|
18629
18925
|
<td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
|
|
18630
18926
|
${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
|
|
18631
18927
|
<td><span class="tag">${esc(run.taskAgent)}</span></td>
|
|
18632
|
-
<td><span class="badge b-${
|
|
18928
|
+
<td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
|
|
18633
18929
|
<td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
|
|
18634
18930
|
<td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
18635
18931
|
<td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
|
|
18636
18932
|
</tr>`;
|
|
18637
18933
|
if (run.log) {
|
|
18638
18934
|
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
|
|
18935
|
+
<div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
|
|
18936
|
+
<div class="log-box">${esc(run.log)}</div></div></div>`);
|
|
18641
18937
|
}
|
|
18642
18938
|
}
|
|
18643
18939
|
let paging = `<div class="pn">`;
|
|
@@ -18663,17 +18959,18 @@ var init_web = __esm({
|
|
|
18663
18959
|
});
|
|
18664
18960
|
app.get("/system", async (c) => {
|
|
18665
18961
|
const config = loadConfig();
|
|
18962
|
+
const configPath = getConfigPath();
|
|
18666
18963
|
const stats = await TaskService.stats({});
|
|
18667
18964
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
18668
18965
|
const templates = await TaskTemplateService.list(100);
|
|
18669
|
-
const configExists = existsSync3(
|
|
18966
|
+
const configExists = existsSync3(configPath);
|
|
18670
18967
|
let runRows = "";
|
|
18671
18968
|
if (runningRuns.length > 0) {
|
|
18672
18969
|
for (const run of runningRuns) {
|
|
18673
18970
|
const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
18674
18971
|
runRows += `<tr>
|
|
18675
18972
|
<td class="mu">#${run.id}</td><td>#${run.taskId}</td>
|
|
18676
|
-
<td class="m sm">${shortS}</td>
|
|
18973
|
+
<td class="m sm">${esc(shortS)}</td>
|
|
18677
18974
|
<td class="sm">${esc(run.model) || "-"}</td>
|
|
18678
18975
|
<td class="sm">${formatDate(run.startedAt)}</td>
|
|
18679
18976
|
<td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
@@ -18698,19 +18995,14 @@ var init_web = __esm({
|
|
|
18698
18995
|
<h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
|
|
18699
18996
|
<div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
|
|
18700
18997
|
<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
18998
|
<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
18999
|
</div>
|
|
18708
19000
|
<div class="card">
|
|
18709
19001
|
<h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
|
|
18710
19002
|
<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>\
|
|
19003
|
+
<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>
|
|
19004
|
+
<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
19005
|
<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
19006
|
</div>
|
|
18715
19007
|
</div>
|
|
18716
19008
|
<div style="text-align:center;margin-bottom:24px">
|
|
@@ -18739,7 +19031,7 @@ var init_web = __esm({
|
|
|
18739
19031
|
|
|
18740
19032
|
<div class="card mt16">
|
|
18741
19033
|
<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">${
|
|
19034
|
+
<div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
|
|
18743
19035
|
<div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
|
|
18744
19036
|
</div>
|
|
18745
19037
|
|
|
@@ -18751,46 +19043,61 @@ var init_web = __esm({
|
|
|
18751
19043
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
18752
19044
|
});
|
|
18753
19045
|
app.get("/api/tasks/:id", async (c) => {
|
|
18754
|
-
const id =
|
|
19046
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19047
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18755
19048
|
const task = await TaskService.getById(id);
|
|
18756
19049
|
if (!task) return c.json({ error: "not found" }, 404);
|
|
18757
19050
|
const runs = await TaskRunService.listByTaskId(id);
|
|
18758
19051
|
return c.json({ ...task, _runs: runs });
|
|
18759
19052
|
});
|
|
18760
19053
|
app.get("/api/runs/:id", async (c) => {
|
|
18761
|
-
const id =
|
|
19054
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19055
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18762
19056
|
const run = await TaskRunService.getById(id);
|
|
18763
19057
|
if (!run) return c.json({ error: "not found" }, 404);
|
|
18764
19058
|
return c.json(run);
|
|
18765
19059
|
});
|
|
18766
19060
|
app.get("/api/templates/:id", async (c) => {
|
|
18767
|
-
const id =
|
|
19061
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19062
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18768
19063
|
const tmpl = await TaskTemplateService.getById(id);
|
|
18769
19064
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
18770
19065
|
return c.json(tmpl);
|
|
18771
19066
|
});
|
|
18772
19067
|
app.post("/api/tasks/:id/retry", async (c) => {
|
|
18773
|
-
|
|
18774
|
-
return c.json({
|
|
19068
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19069
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19070
|
+
const task = await TaskService.retry(id);
|
|
19071
|
+
if (task) return c.json({ success: true });
|
|
19072
|
+
return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
|
|
18775
19073
|
});
|
|
18776
19074
|
app.delete("/api/tasks/:id", async (c) => {
|
|
18777
|
-
|
|
18778
|
-
return c.json({
|
|
19075
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19076
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19077
|
+
const deleted = await TaskService.delete(id);
|
|
19078
|
+
return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18779
19079
|
});
|
|
18780
19080
|
app.post("/api/templates/:id/enable", async (c) => {
|
|
18781
|
-
const
|
|
18782
|
-
return c.json({
|
|
19081
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19082
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19083
|
+
const result = await TaskTemplateService.enable(id);
|
|
19084
|
+
return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18783
19085
|
});
|
|
18784
19086
|
app.post("/api/templates/:id/disable", async (c) => {
|
|
18785
|
-
const
|
|
18786
|
-
return c.json({
|
|
19087
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19088
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19089
|
+
const result = await TaskTemplateService.disable(id);
|
|
19090
|
+
return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18787
19091
|
});
|
|
18788
19092
|
app.delete("/api/templates/:id", async (c) => {
|
|
18789
|
-
const
|
|
18790
|
-
return c.json({
|
|
19093
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19094
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
19095
|
+
const ok = await TaskTemplateService.delete(id);
|
|
19096
|
+
return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
|
|
18791
19097
|
});
|
|
18792
19098
|
app.post("/api/templates/:id/trigger", async (c) => {
|
|
18793
|
-
const id =
|
|
19099
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19100
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18794
19101
|
const tmpl = await TaskTemplateService.getById(id);
|
|
18795
19102
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
18796
19103
|
const task = await TaskService.add({
|
|
@@ -18802,7 +19109,10 @@ var init_web = __esm({
|
|
|
18802
19109
|
category: tmpl.category,
|
|
18803
19110
|
importance: tmpl.importance,
|
|
18804
19111
|
urgency: tmpl.urgency,
|
|
19112
|
+
batchId: tmpl.batchId,
|
|
18805
19113
|
maxRetries: tmpl.maxRetries,
|
|
19114
|
+
retryBackoffMs: tmpl.retryBackoffMs,
|
|
19115
|
+
timeoutMs: tmpl.timeoutMs,
|
|
18806
19116
|
templateId: tmpl.id
|
|
18807
19117
|
});
|
|
18808
19118
|
return c.json({ success: true, taskId: task.id });
|
|
@@ -18817,22 +19127,29 @@ var init_web = __esm({
|
|
|
18817
19127
|
const bW = body.worker ?? {};
|
|
18818
19128
|
const bS = body.scheduler ?? {};
|
|
18819
19129
|
const bD = body.watchdog ?? {};
|
|
18820
|
-
const merged = {
|
|
18821
|
-
|
|
19130
|
+
const merged = {
|
|
19131
|
+
...current,
|
|
19132
|
+
...body,
|
|
19133
|
+
configVersion: 2,
|
|
19134
|
+
worker: { ...curW, ...bW },
|
|
19135
|
+
scheduler: { ...curS, ...bS },
|
|
19136
|
+
watchdog: { ...curD, ...bD }
|
|
19137
|
+
};
|
|
19138
|
+
writeConfig(validateConfig(merged));
|
|
18822
19139
|
return c.json({ success: true });
|
|
18823
19140
|
} catch (err) {
|
|
18824
|
-
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
19141
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
|
|
18825
19142
|
}
|
|
18826
19143
|
});
|
|
18827
19144
|
app.post("/api/database/clear", async (c) => {
|
|
18828
19145
|
try {
|
|
18829
|
-
const { tasks: tasks3, taskRuns:
|
|
18830
|
-
await db.delete(
|
|
19146
|
+
const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
|
|
19147
|
+
await db.delete(taskRuns4);
|
|
18831
19148
|
await db.delete(taskTemplates4);
|
|
18832
19149
|
await db.delete(tasks3);
|
|
18833
19150
|
return c.json({ success: true });
|
|
18834
19151
|
} catch (err) {
|
|
18835
|
-
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
19152
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
|
|
18836
19153
|
}
|
|
18837
19154
|
});
|
|
18838
19155
|
dashboardApp = app;
|
|
@@ -18850,39 +19167,118 @@ init_config();
|
|
|
18850
19167
|
// src/worker/index.ts
|
|
18851
19168
|
init_task_service();
|
|
18852
19169
|
init_task_run_service();
|
|
19170
|
+
init_health();
|
|
18853
19171
|
import { spawn } from "child_process";
|
|
19172
|
+
|
|
19173
|
+
// src/core/process-control.ts
|
|
19174
|
+
import { spawnSync } from "child_process";
|
|
19175
|
+
import { basename } from "path";
|
|
19176
|
+
function isSafePid(pid) {
|
|
19177
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
19178
|
+
}
|
|
19179
|
+
function inspectUnixProcess(pid) {
|
|
19180
|
+
const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
19181
|
+
encoding: "utf8",
|
|
19182
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19183
|
+
});
|
|
19184
|
+
if (result.status !== 0) return null;
|
|
19185
|
+
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
19186
|
+
if (!match2) return null;
|
|
19187
|
+
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
19188
|
+
}
|
|
19189
|
+
function commandMatches(command, expectedExecutable) {
|
|
19190
|
+
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
19191
|
+
if (!expectedName) return false;
|
|
19192
|
+
return command.toLowerCase().includes(expectedName);
|
|
19193
|
+
}
|
|
19194
|
+
function signalSpawnedProcessTree(pid, signal) {
|
|
19195
|
+
if (!isSafePid(pid)) return false;
|
|
19196
|
+
if (process.platform !== "win32") {
|
|
19197
|
+
try {
|
|
19198
|
+
process.kill(-pid, signal);
|
|
19199
|
+
return true;
|
|
19200
|
+
} catch {
|
|
19201
|
+
}
|
|
19202
|
+
}
|
|
19203
|
+
try {
|
|
19204
|
+
process.kill(pid, signal);
|
|
19205
|
+
return true;
|
|
19206
|
+
} catch {
|
|
19207
|
+
return false;
|
|
19208
|
+
}
|
|
19209
|
+
}
|
|
19210
|
+
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
19211
|
+
if (!isSafePid(pid)) return false;
|
|
19212
|
+
if (process.platform === "win32") {
|
|
19213
|
+
const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
19214
|
+
encoding: "utf8",
|
|
19215
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19216
|
+
});
|
|
19217
|
+
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
19218
|
+
const args = ["/PID", String(pid), "/T"];
|
|
19219
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
19220
|
+
return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
19221
|
+
}
|
|
19222
|
+
const info = inspectUnixProcess(pid);
|
|
19223
|
+
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
19224
|
+
try {
|
|
19225
|
+
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
19226
|
+
else process.kill(pid, signal);
|
|
19227
|
+
return true;
|
|
19228
|
+
} catch {
|
|
19229
|
+
return false;
|
|
19230
|
+
}
|
|
19231
|
+
}
|
|
19232
|
+
|
|
19233
|
+
// src/worker/index.ts
|
|
19234
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
|
|
19235
|
+
var FORBIDDEN_AGENT = "supertask-runner";
|
|
18854
19236
|
var WorkerEngine = class {
|
|
18855
19237
|
activeBatchIds = /* @__PURE__ */ new Set();
|
|
18856
19238
|
runningTasks = /* @__PURE__ */ new Map();
|
|
18857
19239
|
stopped = false;
|
|
18858
19240
|
pollTimer = null;
|
|
18859
19241
|
heartbeatTimer = null;
|
|
19242
|
+
pollCyclePromise = null;
|
|
18860
19243
|
cfg;
|
|
18861
|
-
|
|
19244
|
+
opencodeBin;
|
|
19245
|
+
maxOutputChars;
|
|
19246
|
+
constructor(cfg, options = {}) {
|
|
18862
19247
|
this.cfg = cfg.worker;
|
|
19248
|
+
this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
19249
|
+
this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
18863
19250
|
}
|
|
18864
19251
|
start() {
|
|
18865
19252
|
this.stopped = false;
|
|
19253
|
+
markGatewayActivity("worker");
|
|
18866
19254
|
this.poll();
|
|
18867
19255
|
this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
|
|
18868
19256
|
}
|
|
18869
|
-
stop() {
|
|
19257
|
+
async stop(gracePeriodMs = 0) {
|
|
18870
19258
|
this.stopped = true;
|
|
18871
19259
|
if (this.pollTimer) {
|
|
18872
19260
|
clearTimeout(this.pollTimer);
|
|
18873
19261
|
this.pollTimer = null;
|
|
18874
19262
|
}
|
|
19263
|
+
if (this.pollCyclePromise) await this.pollCyclePromise;
|
|
19264
|
+
if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
|
|
19265
|
+
const deadline = Date.now() + gracePeriodMs;
|
|
19266
|
+
while (this.runningTasks.size > 0 && Date.now() < deadline) {
|
|
19267
|
+
await Bun.sleep(Math.min(50, deadline - Date.now()));
|
|
19268
|
+
}
|
|
19269
|
+
}
|
|
18875
19270
|
if (this.heartbeatTimer) {
|
|
18876
19271
|
clearInterval(this.heartbeatTimer);
|
|
18877
19272
|
this.heartbeatTimer = null;
|
|
18878
19273
|
}
|
|
19274
|
+
const interruptedTaskIds = [...this.runningTasks.keys()];
|
|
18879
19275
|
const killPromises = [];
|
|
18880
|
-
for (const
|
|
19276
|
+
for (const entry of this.runningTasks.values()) {
|
|
18881
19277
|
entry.shutdown = true;
|
|
18882
19278
|
killPromises.push(this.killEntry(entry));
|
|
18883
19279
|
}
|
|
18884
|
-
|
|
18885
|
-
|
|
19280
|
+
await Promise.allSettled(killPromises);
|
|
19281
|
+
return interruptedTaskIds;
|
|
18886
19282
|
}
|
|
18887
19283
|
getRunningTaskIds() {
|
|
18888
19284
|
return [...this.runningTasks.keys()];
|
|
@@ -18892,136 +19288,244 @@ var WorkerEngine = class {
|
|
|
18892
19288
|
}
|
|
18893
19289
|
poll() {
|
|
18894
19290
|
if (this.stopped) return;
|
|
18895
|
-
|
|
19291
|
+
markGatewayActivity("worker");
|
|
19292
|
+
this.pollCyclePromise = this.tryDispatch().finally(() => {
|
|
19293
|
+
this.pollCyclePromise = null;
|
|
18896
19294
|
if (this.stopped) return;
|
|
18897
19295
|
this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
|
|
18898
19296
|
});
|
|
18899
19297
|
}
|
|
18900
19298
|
async tryDispatch() {
|
|
19299
|
+
await this.reconcileCancelledTasks();
|
|
18901
19300
|
while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
|
|
19301
|
+
let task;
|
|
19302
|
+
try {
|
|
19303
|
+
task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
|
|
19304
|
+
} catch (err) {
|
|
19305
|
+
this.logError("task claim failed", err);
|
|
19306
|
+
break;
|
|
19307
|
+
}
|
|
19308
|
+
if (!task) break;
|
|
19309
|
+
if (this.stopped) break;
|
|
19310
|
+
if (!await TaskService.start(task.id)) continue;
|
|
19311
|
+
if (task.batchId) this.activeBatchIds.add(task.batchId);
|
|
19312
|
+
if (this.stopped) {
|
|
19313
|
+
await TaskService.resetRunningToPending([task.id]);
|
|
19314
|
+
this.releaseBatch(task);
|
|
19315
|
+
break;
|
|
19316
|
+
}
|
|
18902
19317
|
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
19318
|
const run = await TaskRunService.create({
|
|
18911
19319
|
taskId: task.id,
|
|
18912
19320
|
model: this.resolveModel(task.model),
|
|
18913
19321
|
status: "running"
|
|
18914
19322
|
});
|
|
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 }));
|
|
19323
|
+
if (this.stopped) {
|
|
19324
|
+
await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
|
|
19325
|
+
await TaskService.resetRunningToPending([task.id]);
|
|
19326
|
+
this.releaseBatch(task);
|
|
19327
|
+
break;
|
|
19328
|
+
}
|
|
19329
|
+
if (task.agent === FORBIDDEN_AGENT) {
|
|
19330
|
+
const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
|
|
19331
|
+
await TaskRunService.fail(run.id, message);
|
|
19332
|
+
await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
|
|
19333
|
+
this.releaseBatch(task);
|
|
19334
|
+
continue;
|
|
19335
|
+
}
|
|
19336
|
+
this.spawnTask(task, run.id);
|
|
19337
|
+
} catch (err) {
|
|
19338
|
+
this.releaseBatch(task);
|
|
19339
|
+
const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
|
|
19340
|
+
try {
|
|
19341
|
+
await TaskService.fail(task.id, message);
|
|
19342
|
+
} catch (failErr) {
|
|
19343
|
+
this.logError("failed to compensate task startup", failErr, task.id);
|
|
19344
|
+
}
|
|
19345
|
+
this.logError("task dispatch failed", err, task.id);
|
|
19346
|
+
}
|
|
19347
|
+
}
|
|
19348
|
+
}
|
|
19349
|
+
spawnTask(task, runId) {
|
|
19350
|
+
const model = this.resolveModel(task.model);
|
|
19351
|
+
const args = ["run", "--agent", task.agent, "--format", "json"];
|
|
19352
|
+
if (model) args.push("-m", model);
|
|
19353
|
+
args.push(task.prompt);
|
|
19354
|
+
const child = spawn(this.opencodeBin, args, {
|
|
19355
|
+
cwd: task.cwd || process.cwd(),
|
|
19356
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19357
|
+
detached: process.platform !== "win32"
|
|
19358
|
+
});
|
|
19359
|
+
const entry = {
|
|
19360
|
+
task,
|
|
19361
|
+
runId,
|
|
19362
|
+
child,
|
|
19363
|
+
output: "",
|
|
19364
|
+
sessionId: null,
|
|
19365
|
+
timeoutTimer: null,
|
|
19366
|
+
shutdown: false,
|
|
19367
|
+
settled: false
|
|
19368
|
+
};
|
|
19369
|
+
this.runningTasks.set(task.id, entry);
|
|
19370
|
+
const handleData = (data) => {
|
|
19371
|
+
const text2 = data.toString();
|
|
19372
|
+
entry.output = (entry.output + text2).slice(-this.maxOutputChars);
|
|
19373
|
+
process.stdout.write(text2);
|
|
19374
|
+
const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
|
|
19375
|
+
if (match2?.[1] && match2[1] !== entry.sessionId) {
|
|
19376
|
+
entry.sessionId = match2[1];
|
|
19377
|
+
TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
|
|
19378
|
+
this.logError("sessionId update failed", err, task.id);
|
|
18972
19379
|
});
|
|
19380
|
+
}
|
|
19381
|
+
};
|
|
19382
|
+
child.stdout?.on("data", handleData);
|
|
19383
|
+
child.stderr?.on("data", handleData);
|
|
19384
|
+
child.once("error", (err) => {
|
|
19385
|
+
void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
|
|
19386
|
+
});
|
|
19387
|
+
child.once("close", (code, signal) => {
|
|
19388
|
+
const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
|
|
19389
|
+
void this.finishEntry(entry, code, failure);
|
|
19390
|
+
});
|
|
19391
|
+
const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
|
|
19392
|
+
if (timeoutMs > 0) {
|
|
19393
|
+
entry.timeoutTimer = setTimeout(() => {
|
|
19394
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19395
|
+
void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
|
|
19396
|
+
}, timeoutMs);
|
|
19397
|
+
}
|
|
19398
|
+
TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
|
|
19399
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19400
|
+
void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
|
|
19401
|
+
});
|
|
19402
|
+
}
|
|
19403
|
+
async finishEntry(entry, code, failure) {
|
|
19404
|
+
if (entry.settled) return;
|
|
19405
|
+
entry.settled = true;
|
|
19406
|
+
if (entry.timeoutTimer) {
|
|
19407
|
+
clearTimeout(entry.timeoutTimer);
|
|
19408
|
+
entry.timeoutTimer = null;
|
|
19409
|
+
}
|
|
19410
|
+
try {
|
|
19411
|
+
if (entry.shutdown) return;
|
|
19412
|
+
const currentRun = await TaskRunService.getById(entry.runId);
|
|
19413
|
+
if (!currentRun || currentRun.status !== "running") return;
|
|
19414
|
+
const output = entry.output.trim();
|
|
19415
|
+
const log = failure ? `${failure}${output ? `
|
|
19416
|
+
${output}` : ""}` : output;
|
|
19417
|
+
if (code === 0 && !failure) {
|
|
19418
|
+
const completed = await TaskService.done(entry.task.id, log);
|
|
19419
|
+
if (completed) {
|
|
19420
|
+
await TaskRunService.done(entry.runId, log);
|
|
19421
|
+
console.log(JSON.stringify({
|
|
19422
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19423
|
+
level: "info",
|
|
19424
|
+
msg: "task done",
|
|
19425
|
+
taskId: entry.task.id
|
|
19426
|
+
}));
|
|
19427
|
+
return;
|
|
19428
|
+
}
|
|
19429
|
+
await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
|
|
19430
|
+
return;
|
|
19431
|
+
}
|
|
19432
|
+
await TaskRunService.fail(entry.runId, log);
|
|
19433
|
+
const failed = await TaskService.fail(entry.task.id, log);
|
|
19434
|
+
if (!failed) {
|
|
19435
|
+
this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
|
|
19436
|
+
}
|
|
19437
|
+
console.error(JSON.stringify({
|
|
19438
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19439
|
+
level: "error",
|
|
19440
|
+
msg: "task failed",
|
|
19441
|
+
taskId: entry.task.id,
|
|
19442
|
+
error: failure
|
|
19443
|
+
}));
|
|
19444
|
+
} finally {
|
|
19445
|
+
this.runningTasks.delete(entry.task.id);
|
|
19446
|
+
this.releaseBatch(entry.task);
|
|
19447
|
+
}
|
|
19448
|
+
}
|
|
19449
|
+
async reconcileCancelledTasks() {
|
|
19450
|
+
for (const entry of [...this.runningTasks.values()]) {
|
|
19451
|
+
try {
|
|
19452
|
+
const task = await TaskService.getById(entry.task.id);
|
|
19453
|
+
if (task?.status === "cancelled") await this.cancelEntry(entry);
|
|
18973
19454
|
} 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;
|
|
19455
|
+
this.logError("cancel reconciliation failed", err, entry.task.id);
|
|
18981
19456
|
}
|
|
18982
19457
|
}
|
|
18983
19458
|
}
|
|
19459
|
+
async cancelEntry(entry) {
|
|
19460
|
+
if (entry.settled) return;
|
|
19461
|
+
entry.settled = true;
|
|
19462
|
+
if (entry.timeoutTimer) {
|
|
19463
|
+
clearTimeout(entry.timeoutTimer);
|
|
19464
|
+
entry.timeoutTimer = null;
|
|
19465
|
+
}
|
|
19466
|
+
try {
|
|
19467
|
+
await this.killEntry(entry);
|
|
19468
|
+
const output = entry.output.trim();
|
|
19469
|
+
const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
|
|
19470
|
+
${output}` : ""}`;
|
|
19471
|
+
await TaskRunService.fail(entry.runId, log);
|
|
19472
|
+
console.log(JSON.stringify({
|
|
19473
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19474
|
+
level: "info",
|
|
19475
|
+
msg: "running task cancelled",
|
|
19476
|
+
taskId: entry.task.id
|
|
19477
|
+
}));
|
|
19478
|
+
} finally {
|
|
19479
|
+
this.runningTasks.delete(entry.task.id);
|
|
19480
|
+
this.releaseBatch(entry.task);
|
|
19481
|
+
}
|
|
19482
|
+
}
|
|
18984
19483
|
async updateHeartbeats() {
|
|
18985
|
-
for (const
|
|
19484
|
+
for (const entry of this.runningTasks.values()) {
|
|
18986
19485
|
try {
|
|
18987
19486
|
await TaskRunService.heartbeat(entry.runId);
|
|
18988
|
-
} catch {
|
|
19487
|
+
} catch (err) {
|
|
19488
|
+
this.logError("heartbeat update failed", err, entry.task.id);
|
|
18989
19489
|
}
|
|
18990
19490
|
}
|
|
18991
19491
|
}
|
|
18992
19492
|
killEntry(entry) {
|
|
18993
|
-
if (entry.child.exitCode !== null) {
|
|
19493
|
+
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
18994
19494
|
return Promise.resolve();
|
|
18995
19495
|
}
|
|
18996
19496
|
return new Promise((resolve) => {
|
|
18997
19497
|
const timeout = setTimeout(() => {
|
|
18998
|
-
|
|
18999
|
-
if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
|
|
19000
|
-
} catch {
|
|
19001
|
-
}
|
|
19498
|
+
this.signalEntry(entry, "SIGKILL");
|
|
19002
19499
|
resolve();
|
|
19003
19500
|
}, 5e3);
|
|
19004
|
-
entry.child.
|
|
19501
|
+
entry.child.once("close", () => {
|
|
19005
19502
|
clearTimeout(timeout);
|
|
19006
19503
|
resolve();
|
|
19007
19504
|
});
|
|
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
|
-
}
|
|
19505
|
+
this.signalEntry(entry, "SIGTERM");
|
|
19019
19506
|
});
|
|
19020
19507
|
}
|
|
19508
|
+
signalEntry(entry, signal) {
|
|
19509
|
+
const pid = entry.child.pid;
|
|
19510
|
+
if (!pid) return;
|
|
19511
|
+
signalSpawnedProcessTree(pid, signal);
|
|
19512
|
+
}
|
|
19513
|
+
releaseBatch(task) {
|
|
19514
|
+
if (task.batchId) this.activeBatchIds.delete(task.batchId);
|
|
19515
|
+
}
|
|
19021
19516
|
resolveModel(taskModel) {
|
|
19022
19517
|
if (!taskModel || taskModel === "default") return null;
|
|
19023
19518
|
return taskModel;
|
|
19024
19519
|
}
|
|
19520
|
+
logError(message, error, taskId) {
|
|
19521
|
+
console.error(JSON.stringify({
|
|
19522
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19523
|
+
level: "error",
|
|
19524
|
+
msg: message,
|
|
19525
|
+
taskId,
|
|
19526
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19527
|
+
}));
|
|
19528
|
+
}
|
|
19025
19529
|
};
|
|
19026
19530
|
|
|
19027
19531
|
// src/gateway/watchdog/heartbeat.ts
|
|
@@ -19034,15 +19538,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19034
19538
|
for (const run of staleRuns) {
|
|
19035
19539
|
try {
|
|
19036
19540
|
if (run.childPid != null && run.childPid > 0) {
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19541
|
+
const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
|
|
19542
|
+
const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
|
|
19543
|
+
if (!killed) {
|
|
19544
|
+
console.warn(JSON.stringify({
|
|
19545
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19546
|
+
level: "warn",
|
|
19547
|
+
msg: "stale child process was not killed because identity validation failed",
|
|
19548
|
+
taskId: run.taskId,
|
|
19549
|
+
runId: run.runId,
|
|
19550
|
+
childPid: run.childPid
|
|
19551
|
+
}));
|
|
19040
19552
|
}
|
|
19041
19553
|
}
|
|
19042
19554
|
await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
|
|
19043
19555
|
const newRetryCount = run.taskRetryCount + 1;
|
|
19044
19556
|
const maxRetries = run.taskMaxRetries;
|
|
19045
|
-
if (newRetryCount
|
|
19557
|
+
if (newRetryCount > maxRetries) {
|
|
19046
19558
|
await TaskService.markDeadLetter(run.taskId, newRetryCount);
|
|
19047
19559
|
console.log(JSON.stringify({
|
|
19048
19560
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19054,7 +19566,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19054
19566
|
maxRetries
|
|
19055
19567
|
}));
|
|
19056
19568
|
} else {
|
|
19057
|
-
const backoffMs = computeBackoff(newRetryCount);
|
|
19569
|
+
const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
|
|
19058
19570
|
const retryAfter = Date.now() + backoffMs;
|
|
19059
19571
|
await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
|
|
19060
19572
|
console.log(JSON.stringify({
|
|
@@ -19110,6 +19622,7 @@ async function cleanupOldRecords(retentionDays) {
|
|
|
19110
19622
|
}
|
|
19111
19623
|
|
|
19112
19624
|
// src/gateway/watchdog/index.ts
|
|
19625
|
+
init_health();
|
|
19113
19626
|
var Watchdog = class {
|
|
19114
19627
|
constructor(cfg) {
|
|
19115
19628
|
this.cfg = cfg;
|
|
@@ -19120,13 +19633,14 @@ var Watchdog = class {
|
|
|
19120
19633
|
cleanupTimer = null;
|
|
19121
19634
|
start() {
|
|
19122
19635
|
this.stopped = false;
|
|
19636
|
+
markGatewayActivity("watchdog");
|
|
19123
19637
|
this.heartbeatTimer = setInterval(
|
|
19124
19638
|
() => this.runHeartbeatCheck(),
|
|
19125
|
-
this.cfg.watchdog.
|
|
19639
|
+
this.cfg.watchdog.checkIntervalMs
|
|
19126
19640
|
);
|
|
19127
19641
|
this.cleanupTimer = setInterval(
|
|
19128
19642
|
() => this.runCleanup(),
|
|
19129
|
-
this.cfg.watchdog.cleanupIntervalMs
|
|
19643
|
+
this.cfg.watchdog.cleanupIntervalMs
|
|
19130
19644
|
);
|
|
19131
19645
|
}
|
|
19132
19646
|
stop() {
|
|
@@ -19142,6 +19656,7 @@ var Watchdog = class {
|
|
|
19142
19656
|
}
|
|
19143
19657
|
async runHeartbeatCheck() {
|
|
19144
19658
|
if (this.stopped) return;
|
|
19659
|
+
markGatewayActivity("watchdog");
|
|
19145
19660
|
try {
|
|
19146
19661
|
await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
|
|
19147
19662
|
} catch (err) {
|
|
@@ -19177,7 +19692,7 @@ var { taskTemplates: taskTemplates3 } = schema_exports;
|
|
|
19177
19692
|
async function cloneTaskFromTemplate(templateId) {
|
|
19178
19693
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
19179
19694
|
const tmpl = rows[0];
|
|
19180
|
-
if (!tmpl) return null;
|
|
19695
|
+
if (!tmpl || !tmpl.enabled) return null;
|
|
19181
19696
|
const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
|
|
19182
19697
|
and(
|
|
19183
19698
|
eq(schema_exports.tasks.templateId, templateId),
|
|
@@ -19186,7 +19701,8 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19186
19701
|
).then((r) => r[0].count);
|
|
19187
19702
|
if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
|
|
19188
19703
|
const nowMs = Date.now();
|
|
19189
|
-
const
|
|
19704
|
+
const isDelayed = tmpl.scheduleType === "delayed";
|
|
19705
|
+
const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
|
|
19190
19706
|
tmpl.scheduleType,
|
|
19191
19707
|
tmpl,
|
|
19192
19708
|
nowMs
|
|
@@ -19200,13 +19716,17 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19200
19716
|
category: tmpl.category ?? "general",
|
|
19201
19717
|
importance: tmpl.importance ?? 3,
|
|
19202
19718
|
urgency: tmpl.urgency ?? 3,
|
|
19719
|
+
batchId: tmpl.batchId,
|
|
19203
19720
|
maxRetries: tmpl.maxRetries ?? 3,
|
|
19721
|
+
retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
|
|
19722
|
+
timeoutMs: tmpl.timeoutMs,
|
|
19204
19723
|
templateId: tmpl.id,
|
|
19205
19724
|
scheduledAt: nowMs
|
|
19206
19725
|
});
|
|
19207
19726
|
await db.update(taskTemplates3).set({
|
|
19208
19727
|
lastRunAt: nowMs,
|
|
19209
19728
|
nextRunAt,
|
|
19729
|
+
enabled: isDelayed ? false : tmpl.enabled,
|
|
19210
19730
|
updatedAt: nowMs
|
|
19211
19731
|
}).where(eq(taskTemplates3.id, templateId));
|
|
19212
19732
|
return task;
|
|
@@ -19219,7 +19739,7 @@ async function getDueTemplates() {
|
|
|
19219
19739
|
sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
|
|
19220
19740
|
sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
|
|
19221
19741
|
)
|
|
19222
|
-
);
|
|
19742
|
+
).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
|
|
19223
19743
|
}
|
|
19224
19744
|
async function initializeNextRunAt(templateId) {
|
|
19225
19745
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
@@ -19237,6 +19757,7 @@ async function initializeNextRunAt(templateId) {
|
|
|
19237
19757
|
// src/gateway/scheduler/index.ts
|
|
19238
19758
|
init_db2();
|
|
19239
19759
|
init_drizzle_orm();
|
|
19760
|
+
init_health();
|
|
19240
19761
|
var Scheduler = class {
|
|
19241
19762
|
constructor(cfg) {
|
|
19242
19763
|
this.cfg = cfg;
|
|
@@ -19248,6 +19769,7 @@ var Scheduler = class {
|
|
|
19248
19769
|
async start() {
|
|
19249
19770
|
if (!this.cfg.scheduler.enabled) return;
|
|
19250
19771
|
this.stopped = false;
|
|
19772
|
+
markGatewayActivity("scheduler");
|
|
19251
19773
|
await this.initializeTemplates();
|
|
19252
19774
|
this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
|
|
19253
19775
|
}
|
|
@@ -19260,6 +19782,7 @@ var Scheduler = class {
|
|
|
19260
19782
|
}
|
|
19261
19783
|
async tick() {
|
|
19262
19784
|
if (this.stopped || this.ticking) return;
|
|
19785
|
+
markGatewayActivity("scheduler");
|
|
19263
19786
|
this.ticking = true;
|
|
19264
19787
|
try {
|
|
19265
19788
|
const dueTemplates = await getDueTemplates();
|
|
@@ -19309,6 +19832,7 @@ var Scheduler = class {
|
|
|
19309
19832
|
init_db2();
|
|
19310
19833
|
init_task_service();
|
|
19311
19834
|
init_task_run_service();
|
|
19835
|
+
init_health();
|
|
19312
19836
|
var STALE_THRESHOLD_MS = 3e4;
|
|
19313
19837
|
function acquireLock() {
|
|
19314
19838
|
const now = Date.now();
|
|
@@ -19330,7 +19854,7 @@ function acquireLock() {
|
|
|
19330
19854
|
sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
|
|
19331
19855
|
}
|
|
19332
19856
|
sqlite.exec(
|
|
19333
|
-
"INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?,
|
|
19857
|
+
"INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
|
|
19334
19858
|
[pid, now, now]
|
|
19335
19859
|
);
|
|
19336
19860
|
sqlite.exec("COMMIT");
|
|
@@ -19364,6 +19888,18 @@ function updateLockHeartbeat() {
|
|
|
19364
19888
|
} catch {
|
|
19365
19889
|
}
|
|
19366
19890
|
}
|
|
19891
|
+
function markGatewayReady() {
|
|
19892
|
+
sqlite.exec(
|
|
19893
|
+
"UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
|
|
19894
|
+
[Date.now(), Date.now(), process.pid]
|
|
19895
|
+
);
|
|
19896
|
+
}
|
|
19897
|
+
function markGatewayNotReady() {
|
|
19898
|
+
try {
|
|
19899
|
+
sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
|
|
19900
|
+
} catch {
|
|
19901
|
+
}
|
|
19902
|
+
}
|
|
19367
19903
|
async function main() {
|
|
19368
19904
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
|
|
19369
19905
|
if (!acquireLock()) {
|
|
@@ -19375,6 +19911,12 @@ async function main() {
|
|
|
19375
19911
|
const worker = new WorkerEngine(cfg);
|
|
19376
19912
|
const watchdog = new Watchdog(cfg);
|
|
19377
19913
|
const scheduler = new Scheduler(cfg);
|
|
19914
|
+
initializeGatewayHealth({
|
|
19915
|
+
workerPollIntervalMs: cfg.worker.pollIntervalMs,
|
|
19916
|
+
schedulerEnabled: cfg.scheduler.enabled,
|
|
19917
|
+
schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
|
|
19918
|
+
watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
|
|
19919
|
+
});
|
|
19378
19920
|
worker.start();
|
|
19379
19921
|
watchdog.start();
|
|
19380
19922
|
await scheduler.start();
|
|
@@ -19392,6 +19934,7 @@ async function main() {
|
|
|
19392
19934
|
url: `http://localhost:${cfg.dashboard.port}`
|
|
19393
19935
|
}));
|
|
19394
19936
|
}
|
|
19937
|
+
markGatewayReady();
|
|
19395
19938
|
console.log(JSON.stringify({
|
|
19396
19939
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19397
19940
|
level: "info",
|
|
@@ -19405,10 +19948,10 @@ async function main() {
|
|
|
19405
19948
|
shuttingDown = true;
|
|
19406
19949
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
|
|
19407
19950
|
clearInterval(heartbeatTimer);
|
|
19951
|
+
markGatewayNotReady();
|
|
19408
19952
|
scheduler.stop();
|
|
19409
19953
|
watchdog.stop();
|
|
19410
|
-
const runningIds = worker.
|
|
19411
|
-
await worker.stop();
|
|
19954
|
+
const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
|
|
19412
19955
|
if (runningIds.length > 0) {
|
|
19413
19956
|
const resetCount = await TaskService.resetRunningToPending(runningIds);
|
|
19414
19957
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
|
|
@@ -19418,6 +19961,7 @@ async function main() {
|
|
|
19418
19961
|
await TaskRunService.fail(run.id, "Gateway shutdown");
|
|
19419
19962
|
}
|
|
19420
19963
|
releaseLock();
|
|
19964
|
+
resetGatewayHealth();
|
|
19421
19965
|
closeDb();
|
|
19422
19966
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
|
|
19423
19967
|
process.exit(0);
|