opencode-supertask 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -25
- package/dist/cli/index.js +1256 -587
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.js +849 -302
- package/dist/gateway/index.js.map +1 -1
- package/dist/plugin/supertask.js +585 -233
- package/dist/plugin/supertask.js.map +1 -1
- package/dist/web/index.js +546 -213
- package/dist/web/index.js.map +1 -1
- package/dist/worker/index.d.ts +19 -7
- package/dist/worker/index.js +434 -152
- package/dist/worker/index.js.map +1 -1
- package/drizzle/0004_reliability_fields.sql +31 -0
- package/drizzle/meta/0004_snapshot.json +85 -13
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
package/dist/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
|
|
|
@@ -6084,7 +6099,15 @@ import { join, dirname } from "path";
|
|
|
6084
6099
|
import { fileURLToPath } from "url";
|
|
6085
6100
|
function getMigrationsFolder() {
|
|
6086
6101
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6087
|
-
|
|
6102
|
+
let dir = __dirname;
|
|
6103
|
+
for (let i = 0; i < 5; i++) {
|
|
6104
|
+
const candidate = join(dir, "drizzle", "meta", "_journal.json");
|
|
6105
|
+
if (existsSync(candidate)) {
|
|
6106
|
+
return join(dir, "drizzle");
|
|
6107
|
+
}
|
|
6108
|
+
dir = dirname(dir);
|
|
6109
|
+
}
|
|
6110
|
+
return join(__dirname, "../../drizzle");
|
|
6088
6111
|
}
|
|
6089
6112
|
function initDb() {
|
|
6090
6113
|
const dataDir = dirname(DB_FILE_PATH);
|
|
@@ -6099,16 +6122,30 @@ function initDb() {
|
|
|
6099
6122
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
6100
6123
|
pid INTEGER NOT NULL,
|
|
6101
6124
|
acquired_at INTEGER NOT NULL,
|
|
6102
|
-
heartbeat_at INTEGER NOT NULL
|
|
6125
|
+
heartbeat_at INTEGER NOT NULL,
|
|
6126
|
+
ready_at INTEGER
|
|
6103
6127
|
);
|
|
6104
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
|
+
}
|
|
6105
6133
|
_db = drizzle(_sqlite, { schema: schema_exports });
|
|
6106
6134
|
if (!_migrationRan) {
|
|
6107
|
-
_migrationRan = true;
|
|
6108
6135
|
try {
|
|
6109
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;
|
|
6110
6143
|
} catch (err) {
|
|
6111
6144
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6145
|
+
_migrationRan = false;
|
|
6146
|
+
_sqlite.close();
|
|
6147
|
+
_sqlite = null;
|
|
6148
|
+
_db = null;
|
|
6112
6149
|
console.error(`[supertask] migration failed: ${msg}`);
|
|
6113
6150
|
throw new Error(`[supertask] DB migration failed: ${msg}`);
|
|
6114
6151
|
}
|
|
@@ -6128,6 +6165,7 @@ function closeDb() {
|
|
|
6128
6165
|
_sqlite.close();
|
|
6129
6166
|
_sqlite = null;
|
|
6130
6167
|
_db = null;
|
|
6168
|
+
_migrationRan = false;
|
|
6131
6169
|
}
|
|
6132
6170
|
}
|
|
6133
6171
|
var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
|
|
@@ -6162,71 +6200,123 @@ var init_db2 = __esm({
|
|
|
6162
6200
|
});
|
|
6163
6201
|
|
|
6164
6202
|
// src/gateway/config.ts
|
|
6165
|
-
import {
|
|
6203
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
6166
6204
|
import { homedir as homedir2 } from "os";
|
|
6167
6205
|
import { join as join2 } from "path";
|
|
6168
|
-
function
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
result[key] = deepMerge(baseVal, val);
|
|
6176
|
-
continue;
|
|
6177
|
-
}
|
|
6178
|
-
}
|
|
6179
|
-
if (val !== void 0) {
|
|
6180
|
-
result[key] = val;
|
|
6181
|
-
}
|
|
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`);
|
|
6182
6213
|
}
|
|
6183
|
-
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;
|
|
6184
6229
|
}
|
|
6185
|
-
function
|
|
6186
|
-
|
|
6187
|
-
|
|
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");
|
|
6188
6235
|
}
|
|
6236
|
+
const worker = objectAt(root.worker, "worker");
|
|
6237
|
+
const scheduler = objectAt(root.scheduler, "scheduler");
|
|
6238
|
+
const watchdog = objectAt(root.watchdog, "watchdog");
|
|
6239
|
+
const dashboard = objectAt(root.dashboard, "dashboard");
|
|
6240
|
+
const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
|
|
6241
|
+
const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
|
|
6242
|
+
let checkIntervalMs;
|
|
6243
|
+
let cleanupIntervalMs;
|
|
6244
|
+
if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
|
|
6245
|
+
checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
|
|
6246
|
+
cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
|
|
6247
|
+
} else {
|
|
6248
|
+
checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
|
|
6249
|
+
cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
|
|
6250
|
+
}
|
|
6251
|
+
if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
|
|
6252
|
+
throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
|
|
6253
|
+
}
|
|
6254
|
+
if (checkIntervalMs > heartbeatTimeoutMs) {
|
|
6255
|
+
throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
|
|
6256
|
+
}
|
|
6257
|
+
return {
|
|
6258
|
+
configVersion: 2,
|
|
6259
|
+
worker: {
|
|
6260
|
+
maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
|
|
6261
|
+
pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
|
|
6262
|
+
heartbeatIntervalMs,
|
|
6263
|
+
taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
|
|
6264
|
+
shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
|
|
6265
|
+
},
|
|
6266
|
+
scheduler: {
|
|
6267
|
+
enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
|
|
6268
|
+
checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
|
|
6269
|
+
},
|
|
6270
|
+
watchdog: {
|
|
6271
|
+
heartbeatTimeoutMs,
|
|
6272
|
+
checkIntervalMs,
|
|
6273
|
+
cleanupIntervalMs,
|
|
6274
|
+
retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
|
|
6275
|
+
},
|
|
6276
|
+
dashboard: {
|
|
6277
|
+
enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
|
|
6278
|
+
port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
|
|
6279
|
+
}
|
|
6280
|
+
};
|
|
6281
|
+
}
|
|
6282
|
+
function loadConfig(path = getConfigPath()) {
|
|
6283
|
+
if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
|
|
6189
6284
|
try {
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6195
|
-
console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
|
|
6196
|
-
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}`);
|
|
6197
6289
|
}
|
|
6198
6290
|
}
|
|
6199
|
-
var DEFAULT_CONFIG,
|
|
6291
|
+
var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
|
|
6200
6292
|
var init_config = __esm({
|
|
6201
6293
|
"src/gateway/config.ts"() {
|
|
6202
6294
|
"use strict";
|
|
6203
6295
|
DEFAULT_CONFIG = {
|
|
6296
|
+
configVersion: 2,
|
|
6204
6297
|
worker: {
|
|
6205
6298
|
maxConcurrency: 2,
|
|
6206
6299
|
pollIntervalMs: 1e3,
|
|
6207
6300
|
heartbeatIntervalMs: 3e4,
|
|
6208
|
-
taskTimeoutMs: 18e5
|
|
6301
|
+
taskTimeoutMs: 18e5,
|
|
6302
|
+
shutdownGracePeriodMs: 3e4
|
|
6209
6303
|
},
|
|
6210
6304
|
scheduler: {
|
|
6211
6305
|
enabled: true,
|
|
6212
|
-
checkIntervalMs: 1e3
|
|
6213
|
-
catchUp: "next"
|
|
6306
|
+
checkIntervalMs: 1e3
|
|
6214
6307
|
},
|
|
6215
6308
|
watchdog: {
|
|
6216
6309
|
heartbeatTimeoutMs: 6e5,
|
|
6217
|
-
|
|
6310
|
+
checkIntervalMs: 6e4,
|
|
6311
|
+
cleanupIntervalMs: 864e5,
|
|
6218
6312
|
retentionDays: 30
|
|
6219
6313
|
},
|
|
6220
6314
|
dashboard: {
|
|
6221
6315
|
enabled: true,
|
|
6222
6316
|
port: 4680
|
|
6223
|
-
},
|
|
6224
|
-
logging: {
|
|
6225
|
-
level: "info",
|
|
6226
|
-
format: "json"
|
|
6227
6317
|
}
|
|
6228
6318
|
};
|
|
6229
|
-
|
|
6319
|
+
DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
|
|
6230
6320
|
}
|
|
6231
6321
|
});
|
|
6232
6322
|
|
|
@@ -6280,14 +6370,14 @@ var init_backoff = __esm({
|
|
|
6280
6370
|
});
|
|
6281
6371
|
|
|
6282
6372
|
// src/core/services/task.service.ts
|
|
6283
|
-
var tasks2, TaskService;
|
|
6373
|
+
var tasks2, taskRuns2, TaskService;
|
|
6284
6374
|
var init_task_service = __esm({
|
|
6285
6375
|
"src/core/services/task.service.ts"() {
|
|
6286
6376
|
"use strict";
|
|
6287
6377
|
init_db2();
|
|
6288
6378
|
init_drizzle_orm();
|
|
6289
6379
|
init_backoff();
|
|
6290
|
-
({ tasks: tasks2 } = schema_exports);
|
|
6380
|
+
({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
|
|
6291
6381
|
TaskService = class {
|
|
6292
6382
|
static buildScopeWhere(scope) {
|
|
6293
6383
|
const conditions = [];
|
|
@@ -6297,9 +6387,27 @@ var init_task_service = __esm({
|
|
|
6297
6387
|
return conditions;
|
|
6298
6388
|
}
|
|
6299
6389
|
static async add(data) {
|
|
6390
|
+
this.validateNewTask(data);
|
|
6300
6391
|
const result = await db.insert(tasks2).values(data).returning();
|
|
6301
6392
|
return result[0];
|
|
6302
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
|
+
}
|
|
6303
6411
|
static async next(scope = {}) {
|
|
6304
6412
|
const baseConditions = [...this.buildScopeWhere(scope)];
|
|
6305
6413
|
const nowMs = Date.now();
|
|
@@ -6322,7 +6430,7 @@ var init_task_service = __esm({
|
|
|
6322
6430
|
),
|
|
6323
6431
|
and(
|
|
6324
6432
|
eq(tasks2.status, "failed"),
|
|
6325
|
-
sql`${tasks2.retryCount}
|
|
6433
|
+
sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
|
|
6326
6434
|
retryAfterFilter
|
|
6327
6435
|
)
|
|
6328
6436
|
);
|
|
@@ -6336,7 +6444,8 @@ var init_task_service = __esm({
|
|
|
6336
6444
|
const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
|
|
6337
6445
|
desc(tasks2.urgency),
|
|
6338
6446
|
desc(tasks2.importance),
|
|
6339
|
-
asc(tasks2.createdAt)
|
|
6447
|
+
asc(tasks2.createdAt),
|
|
6448
|
+
asc(tasks2.id)
|
|
6340
6449
|
);
|
|
6341
6450
|
for (const task of allTasks) {
|
|
6342
6451
|
if (task.dependsOn) {
|
|
@@ -6357,7 +6466,7 @@ var init_task_service = __esm({
|
|
|
6357
6466
|
eq(tasks2.status, "pending"),
|
|
6358
6467
|
and(
|
|
6359
6468
|
eq(tasks2.status, "failed"),
|
|
6360
|
-
sql`${tasks2.retryCount}
|
|
6469
|
+
sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
|
|
6361
6470
|
)
|
|
6362
6471
|
),
|
|
6363
6472
|
...this.buildScopeWhere(scope)
|
|
@@ -6370,7 +6479,11 @@ var init_task_service = __esm({
|
|
|
6370
6479
|
return result[0] || null;
|
|
6371
6480
|
}
|
|
6372
6481
|
static async done(id, log, scope = {}) {
|
|
6373
|
-
const conditions = [
|
|
6482
|
+
const conditions = [
|
|
6483
|
+
eq(tasks2.id, id),
|
|
6484
|
+
eq(tasks2.status, "running"),
|
|
6485
|
+
...this.buildScopeWhere(scope)
|
|
6486
|
+
];
|
|
6374
6487
|
const result = await db.update(tasks2).set({
|
|
6375
6488
|
status: "done",
|
|
6376
6489
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
@@ -6381,17 +6494,24 @@ var init_task_service = __esm({
|
|
|
6381
6494
|
}
|
|
6382
6495
|
static async fail(id, log, scope = {}, options) {
|
|
6383
6496
|
const current = await this.getById(id, scope);
|
|
6384
|
-
if (!current) return null;
|
|
6497
|
+
if (!current || current.status !== "running") return null;
|
|
6385
6498
|
const newRetryCount = (current.retryCount ?? 0) + 1;
|
|
6386
6499
|
const maxRetries = current.maxRetries ?? 3;
|
|
6387
|
-
const isDeadLetter = options?.setDeadLetter ?? newRetryCount
|
|
6388
|
-
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
|
+
];
|
|
6389
6506
|
const result = await db.update(tasks2).set({
|
|
6390
6507
|
status: isDeadLetter ? "dead_letter" : "failed",
|
|
6391
6508
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6392
6509
|
resultLog: log,
|
|
6393
6510
|
retryCount: newRetryCount,
|
|
6394
|
-
retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
|
|
6511
|
+
retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
|
|
6512
|
+
newRetryCount,
|
|
6513
|
+
current.retryBackoffMs ?? 3e4
|
|
6514
|
+
)
|
|
6395
6515
|
}).where(and(...conditions)).returning();
|
|
6396
6516
|
return result[0] || null;
|
|
6397
6517
|
}
|
|
@@ -6424,8 +6544,20 @@ var init_task_service = __esm({
|
|
|
6424
6544
|
return result.length;
|
|
6425
6545
|
}
|
|
6426
6546
|
static async cancel(id, scope = {}) {
|
|
6427
|
-
const conditions = [
|
|
6428
|
-
|
|
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();
|
|
6429
6561
|
return result[0] || null;
|
|
6430
6562
|
}
|
|
6431
6563
|
static async retry(id, scope = {}) {
|
|
@@ -6437,7 +6569,8 @@ var init_task_service = __esm({
|
|
|
6437
6569
|
status: "pending",
|
|
6438
6570
|
startedAt: null,
|
|
6439
6571
|
finishedAt: null,
|
|
6440
|
-
retryAfter: null
|
|
6572
|
+
retryAfter: null,
|
|
6573
|
+
retryCount: 0
|
|
6441
6574
|
}).where(and(...conditions)).returning();
|
|
6442
6575
|
return result[0] || null;
|
|
6443
6576
|
}
|
|
@@ -6451,7 +6584,8 @@ var init_task_service = __esm({
|
|
|
6451
6584
|
status: "pending",
|
|
6452
6585
|
startedAt: null,
|
|
6453
6586
|
finishedAt: null,
|
|
6454
|
-
retryAfter: null
|
|
6587
|
+
retryAfter: null,
|
|
6588
|
+
retryCount: 0
|
|
6455
6589
|
}).where(and(...conditions)).returning();
|
|
6456
6590
|
return result.length;
|
|
6457
6591
|
}
|
|
@@ -6519,6 +6653,9 @@ var init_task_service = __esm({
|
|
|
6519
6653
|
}
|
|
6520
6654
|
static async delete(id, scope = {}) {
|
|
6521
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));
|
|
6522
6659
|
const result = await db.delete(tasks2).where(and(...conditions)).returning();
|
|
6523
6660
|
return result.length > 0;
|
|
6524
6661
|
}
|
|
@@ -6538,65 +6675,65 @@ var init_task_service = __esm({
|
|
|
6538
6675
|
});
|
|
6539
6676
|
|
|
6540
6677
|
// src/core/services/task-run.service.ts
|
|
6541
|
-
var
|
|
6678
|
+
var taskRuns3, TaskRunService;
|
|
6542
6679
|
var init_task_run_service = __esm({
|
|
6543
6680
|
"src/core/services/task-run.service.ts"() {
|
|
6544
6681
|
"use strict";
|
|
6545
6682
|
init_db2();
|
|
6546
6683
|
init_drizzle_orm();
|
|
6547
|
-
({ taskRuns:
|
|
6684
|
+
({ taskRuns: taskRuns3 } = schema_exports);
|
|
6548
6685
|
TaskRunService = class {
|
|
6549
6686
|
static async create(data) {
|
|
6550
|
-
const result = await db.insert(
|
|
6687
|
+
const result = await db.insert(taskRuns3).values(data).returning();
|
|
6551
6688
|
return result[0];
|
|
6552
6689
|
}
|
|
6553
6690
|
static async updateSessionId(id, sessionId) {
|
|
6554
|
-
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();
|
|
6555
6692
|
return result[0] || null;
|
|
6556
6693
|
}
|
|
6557
6694
|
static async done(id, log) {
|
|
6558
|
-
const result = await db.update(
|
|
6695
|
+
const result = await db.update(taskRuns3).set({
|
|
6559
6696
|
status: "done",
|
|
6560
6697
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6561
6698
|
log
|
|
6562
|
-
}).where(eq(
|
|
6699
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6563
6700
|
return result[0] || null;
|
|
6564
6701
|
}
|
|
6565
6702
|
static async fail(id, log) {
|
|
6566
|
-
const result = await db.update(
|
|
6703
|
+
const result = await db.update(taskRuns3).set({
|
|
6567
6704
|
status: "failed",
|
|
6568
6705
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
6569
6706
|
log
|
|
6570
|
-
}).where(eq(
|
|
6707
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6571
6708
|
return result[0] || null;
|
|
6572
6709
|
}
|
|
6573
6710
|
static async heartbeat(id) {
|
|
6574
|
-
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();
|
|
6575
6712
|
return result[0] || null;
|
|
6576
6713
|
}
|
|
6577
6714
|
static async updatePid(id, workerPid, childPid) {
|
|
6578
|
-
const result = await db.update(
|
|
6715
|
+
const result = await db.update(taskRuns3).set({
|
|
6579
6716
|
workerPid,
|
|
6580
6717
|
childPid,
|
|
6581
6718
|
lockedAt: Date.now(),
|
|
6582
6719
|
lockedBy: `gateway-${process.pid}`
|
|
6583
|
-
}).where(eq(
|
|
6720
|
+
}).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
|
|
6584
6721
|
return result[0] || null;
|
|
6585
6722
|
}
|
|
6586
6723
|
static async getById(id) {
|
|
6587
|
-
const result = await db.select().from(
|
|
6724
|
+
const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
|
|
6588
6725
|
return result[0] || null;
|
|
6589
6726
|
}
|
|
6590
6727
|
static async listByTaskId(taskId) {
|
|
6591
|
-
return await db.select().from(
|
|
6728
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
|
|
6592
6729
|
}
|
|
6593
6730
|
static async getLatestByTaskId(taskId) {
|
|
6594
|
-
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);
|
|
6595
6732
|
return result[0] || null;
|
|
6596
6733
|
}
|
|
6597
6734
|
static async getLatestByTaskIds(taskIds) {
|
|
6598
6735
|
if (taskIds.length === 0) return /* @__PURE__ */ new Map();
|
|
6599
|
-
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));
|
|
6600
6737
|
const result = /* @__PURE__ */ new Map();
|
|
6601
6738
|
for (const run of latestRuns) {
|
|
6602
6739
|
if (!result.has(run.taskId)) {
|
|
@@ -6610,22 +6747,23 @@ var init_task_run_service = __esm({
|
|
|
6610
6747
|
const cutoffSec = Math.floor(cutoffMs / 1e3);
|
|
6611
6748
|
const { tasks: tasksTable } = schema_exports;
|
|
6612
6749
|
const result = await db.select({
|
|
6613
|
-
runId:
|
|
6614
|
-
taskId:
|
|
6615
|
-
childPid:
|
|
6750
|
+
runId: taskRuns3.id,
|
|
6751
|
+
taskId: taskRuns3.taskId,
|
|
6752
|
+
childPid: taskRuns3.childPid,
|
|
6616
6753
|
taskRetryCount: tasksTable.retryCount,
|
|
6617
|
-
taskMaxRetries: tasksTable.maxRetries
|
|
6618
|
-
|
|
6754
|
+
taskMaxRetries: tasksTable.maxRetries,
|
|
6755
|
+
taskRetryBackoffMs: tasksTable.retryBackoffMs
|
|
6756
|
+
}).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
|
|
6619
6757
|
and(
|
|
6620
|
-
eq(
|
|
6758
|
+
eq(taskRuns3.status, "running"),
|
|
6621
6759
|
or(
|
|
6622
6760
|
and(
|
|
6623
|
-
sql`${
|
|
6624
|
-
sql`${
|
|
6761
|
+
sql`${taskRuns3.heartbeatAt} IS NULL`,
|
|
6762
|
+
sql`${taskRuns3.startedAt} < ${cutoffSec}`
|
|
6625
6763
|
),
|
|
6626
6764
|
and(
|
|
6627
|
-
sql`${
|
|
6628
|
-
sql`${
|
|
6765
|
+
sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
|
|
6766
|
+
sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
|
|
6629
6767
|
)
|
|
6630
6768
|
)
|
|
6631
6769
|
)
|
|
@@ -6635,25 +6773,111 @@ var init_task_run_service = __esm({
|
|
|
6635
6773
|
taskId: row.taskId,
|
|
6636
6774
|
childPid: row.childPid,
|
|
6637
6775
|
taskRetryCount: row.taskRetryCount ?? 0,
|
|
6638
|
-
taskMaxRetries: row.taskMaxRetries ?? 3
|
|
6776
|
+
taskMaxRetries: row.taskMaxRetries ?? 3,
|
|
6777
|
+
taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
|
|
6639
6778
|
}));
|
|
6640
6779
|
}
|
|
6641
6780
|
static async getRunningRunByTaskId(taskId) {
|
|
6642
|
-
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);
|
|
6643
6782
|
return result[0] || null;
|
|
6644
6783
|
}
|
|
6645
6784
|
static async deleteByTaskIds(taskIds) {
|
|
6646
6785
|
if (taskIds.length === 0) return 0;
|
|
6647
|
-
const result = await db.delete(
|
|
6786
|
+
const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
|
|
6648
6787
|
return result.length;
|
|
6649
6788
|
}
|
|
6650
6789
|
static async getAllRunningRuns() {
|
|
6651
|
-
return await db.select().from(
|
|
6790
|
+
return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
|
|
6652
6791
|
}
|
|
6653
6792
|
};
|
|
6654
6793
|
}
|
|
6655
6794
|
});
|
|
6656
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
|
+
|
|
6657
6881
|
// node_modules/cron-parser/dist/fields/types.js
|
|
6658
6882
|
var require_types = __commonJS({
|
|
6659
6883
|
"node_modules/cron-parser/dist/fields/types.js"(exports) {
|
|
@@ -6854,7 +7078,7 @@ var require_CronField = __commonJS({
|
|
|
6854
7078
|
if (!isValidRange) {
|
|
6855
7079
|
throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
|
|
6856
7080
|
}
|
|
6857
|
-
const duplicate = this.#values.find((value,
|
|
7081
|
+
const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
|
|
6858
7082
|
if (duplicate) {
|
|
6859
7083
|
throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
|
|
6860
7084
|
}
|
|
@@ -14699,11 +14923,11 @@ var require_CronFieldCollection = __commonJS({
|
|
|
14699
14923
|
throw new Error("Unexpected range end");
|
|
14700
14924
|
}
|
|
14701
14925
|
if (step * multiplier > range.end) {
|
|
14702
|
-
const mapFn = (_,
|
|
14926
|
+
const mapFn = (_, index2) => {
|
|
14703
14927
|
if (typeof range.start !== "number") {
|
|
14704
14928
|
throw new Error("Unexpected range start");
|
|
14705
14929
|
}
|
|
14706
|
-
return
|
|
14930
|
+
return index2 % step === 0 ? range.start + index2 : null;
|
|
14707
14931
|
};
|
|
14708
14932
|
if (typeof range.start !== "number") {
|
|
14709
14933
|
throw new Error("Unexpected range start");
|
|
@@ -15595,9 +15819,9 @@ var require_CronExpressionParser = __commonJS({
|
|
|
15595
15819
|
if (field === CronUnit.DayOfWeek && max % 7 === 0) {
|
|
15596
15820
|
stack.push(0);
|
|
15597
15821
|
}
|
|
15598
|
-
for (let
|
|
15599
|
-
if (stack.indexOf(
|
|
15600
|
-
stack.push(
|
|
15822
|
+
for (let index2 = min; index2 <= max; index2 += repeatInterval) {
|
|
15823
|
+
if (stack.indexOf(index2) === -1) {
|
|
15824
|
+
stack.push(index2);
|
|
15601
15825
|
}
|
|
15602
15826
|
}
|
|
15603
15827
|
return stack;
|
|
@@ -15820,11 +16044,6 @@ var require_dist = __commonJS({
|
|
|
15820
16044
|
});
|
|
15821
16045
|
|
|
15822
16046
|
// src/core/cron-parser.ts
|
|
15823
|
-
var cron_parser_exports = {};
|
|
15824
|
-
__export(cron_parser_exports, {
|
|
15825
|
-
getNextCronRun: () => getNextCronRun,
|
|
15826
|
-
isValidCronExpr: () => isValidCronExpr
|
|
15827
|
-
});
|
|
15828
16047
|
function getNextCronRun(expr, afterMs) {
|
|
15829
16048
|
try {
|
|
15830
16049
|
const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
|
|
@@ -15862,6 +16081,7 @@ var init_task_template_service = __esm({
|
|
|
15862
16081
|
({ taskTemplates: taskTemplates2 } = schema_exports);
|
|
15863
16082
|
TaskTemplateService = class {
|
|
15864
16083
|
static async create(data) {
|
|
16084
|
+
this.validate(data);
|
|
15865
16085
|
const now = Date.now();
|
|
15866
16086
|
const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
|
|
15867
16087
|
const tmpl = result[0];
|
|
@@ -15877,8 +16097,38 @@ var init_task_template_service = __esm({
|
|
|
15877
16097
|
}
|
|
15878
16098
|
return tmpl;
|
|
15879
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
|
+
}
|
|
15880
16130
|
static async list(limit = 50) {
|
|
15881
|
-
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);
|
|
15882
16132
|
}
|
|
15883
16133
|
static async getById(id) {
|
|
15884
16134
|
const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
|
|
@@ -15925,13 +16175,13 @@ var init_compose = __esm({
|
|
|
15925
16175
|
"use strict";
|
|
15926
16176
|
compose = (middleware, onError, onNotFound) => {
|
|
15927
16177
|
return (context, next) => {
|
|
15928
|
-
let
|
|
16178
|
+
let index2 = -1;
|
|
15929
16179
|
return dispatch(0);
|
|
15930
16180
|
async function dispatch(i) {
|
|
15931
|
-
if (i <=
|
|
16181
|
+
if (i <= index2) {
|
|
15932
16182
|
throw new Error("next() called multiple times");
|
|
15933
16183
|
}
|
|
15934
|
-
|
|
16184
|
+
index2 = i;
|
|
15935
16185
|
let res;
|
|
15936
16186
|
let isError = false;
|
|
15937
16187
|
let handler;
|
|
@@ -16046,8 +16296,8 @@ var init_body = __esm({
|
|
|
16046
16296
|
handleParsingNestedValues = (form, key, value) => {
|
|
16047
16297
|
let nestedForm = form;
|
|
16048
16298
|
const keys = key.split(".");
|
|
16049
|
-
keys.forEach((key2,
|
|
16050
|
-
if (
|
|
16299
|
+
keys.forEach((key2, index2) => {
|
|
16300
|
+
if (index2 === keys.length - 1) {
|
|
16051
16301
|
nestedForm[key2] = value;
|
|
16052
16302
|
} else {
|
|
16053
16303
|
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
@@ -16079,8 +16329,8 @@ var init_url = __esm({
|
|
|
16079
16329
|
};
|
|
16080
16330
|
extractGroupsFromPath = (path) => {
|
|
16081
16331
|
const groups = [];
|
|
16082
|
-
path = path.replace(/\{[^}]+\}/g, (match2,
|
|
16083
|
-
const mark = `@${
|
|
16332
|
+
path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
|
|
16333
|
+
const mark = `@${index2}`;
|
|
16084
16334
|
groups.push([mark, match2]);
|
|
16085
16335
|
return mark;
|
|
16086
16336
|
});
|
|
@@ -16599,10 +16849,10 @@ var init_html = __esm({
|
|
|
16599
16849
|
return;
|
|
16600
16850
|
}
|
|
16601
16851
|
let escape;
|
|
16602
|
-
let
|
|
16852
|
+
let index2;
|
|
16603
16853
|
let lastIndex = 0;
|
|
16604
|
-
for (
|
|
16605
|
-
switch (str.charCodeAt(
|
|
16854
|
+
for (index2 = match2; index2 < str.length; index2++) {
|
|
16855
|
+
switch (str.charCodeAt(index2)) {
|
|
16606
16856
|
case 34:
|
|
16607
16857
|
escape = """;
|
|
16608
16858
|
break;
|
|
@@ -16621,10 +16871,10 @@ var init_html = __esm({
|
|
|
16621
16871
|
default:
|
|
16622
16872
|
continue;
|
|
16623
16873
|
}
|
|
16624
|
-
buffer[0] += str.substring(lastIndex,
|
|
16625
|
-
lastIndex =
|
|
16874
|
+
buffer[0] += str.substring(lastIndex, index2) + escape;
|
|
16875
|
+
lastIndex = index2 + 1;
|
|
16626
16876
|
}
|
|
16627
|
-
buffer[0] += str.substring(lastIndex,
|
|
16877
|
+
buffer[0] += str.substring(lastIndex, index2);
|
|
16628
16878
|
};
|
|
16629
16879
|
resolveCallbackSync = (str) => {
|
|
16630
16880
|
const callbacks = str.callbacks;
|
|
@@ -17500,8 +17750,8 @@ function match(method, path) {
|
|
|
17500
17750
|
if (!match3) {
|
|
17501
17751
|
return [[], emptyParam];
|
|
17502
17752
|
}
|
|
17503
|
-
const
|
|
17504
|
-
return [matcher[1][
|
|
17753
|
+
const index2 = match3.indexOf("", 1);
|
|
17754
|
+
return [matcher[1][index2], match3];
|
|
17505
17755
|
});
|
|
17506
17756
|
this.match = match2;
|
|
17507
17757
|
return match2(method, path);
|
|
@@ -17548,7 +17798,7 @@ var init_node = __esm({
|
|
|
17548
17798
|
#index;
|
|
17549
17799
|
#varIndex;
|
|
17550
17800
|
#children = /* @__PURE__ */ Object.create(null);
|
|
17551
|
-
insert(tokens,
|
|
17801
|
+
insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
|
|
17552
17802
|
if (tokens.length === 0) {
|
|
17553
17803
|
if (this.#index !== void 0) {
|
|
17554
17804
|
throw PATH_ERROR;
|
|
@@ -17556,7 +17806,7 @@ var init_node = __esm({
|
|
|
17556
17806
|
if (pathErrorCheckOnly) {
|
|
17557
17807
|
return;
|
|
17558
17808
|
}
|
|
17559
|
-
this.#index =
|
|
17809
|
+
this.#index = index2;
|
|
17560
17810
|
return;
|
|
17561
17811
|
}
|
|
17562
17812
|
const [token, ...restTokens] = tokens;
|
|
@@ -17606,7 +17856,7 @@ var init_node = __esm({
|
|
|
17606
17856
|
node = this.#children[token] = new _Node();
|
|
17607
17857
|
}
|
|
17608
17858
|
}
|
|
17609
|
-
node.insert(restTokens,
|
|
17859
|
+
node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
|
|
17610
17860
|
}
|
|
17611
17861
|
buildRegExpStr() {
|
|
17612
17862
|
const childKeys = Object.keys(this.#children).sort(compareKey);
|
|
@@ -17638,7 +17888,7 @@ var init_trie = __esm({
|
|
|
17638
17888
|
Trie = class {
|
|
17639
17889
|
#context = { varIndex: 0 };
|
|
17640
17890
|
#root = new Node();
|
|
17641
|
-
insert(path,
|
|
17891
|
+
insert(path, index2, pathErrorCheckOnly) {
|
|
17642
17892
|
const paramAssoc = [];
|
|
17643
17893
|
const groups = [];
|
|
17644
17894
|
for (let i = 0; ; ) {
|
|
@@ -17664,7 +17914,7 @@ var init_trie = __esm({
|
|
|
17664
17914
|
}
|
|
17665
17915
|
}
|
|
17666
17916
|
}
|
|
17667
|
-
this.#root.insert(tokens,
|
|
17917
|
+
this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
|
|
17668
17918
|
return paramAssoc;
|
|
17669
17919
|
}
|
|
17670
17920
|
buildRegExp() {
|
|
@@ -18258,8 +18508,19 @@ __export(web_exports, {
|
|
|
18258
18508
|
dashboardApp: () => dashboardApp,
|
|
18259
18509
|
default: () => web_default
|
|
18260
18510
|
});
|
|
18261
|
-
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";
|
|
18262
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
|
+
}
|
|
18263
18524
|
function formatDuration(startAt, endAt) {
|
|
18264
18525
|
if (!startAt) return "-";
|
|
18265
18526
|
const start = new Date(startAt).getTime();
|
|
@@ -18297,17 +18558,21 @@ function esc(s) {
|
|
|
18297
18558
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
18298
18559
|
}
|
|
18299
18560
|
function readCurrentConfig() {
|
|
18300
|
-
|
|
18561
|
+
const configPath = getConfigPath();
|
|
18562
|
+
if (!existsSync3(configPath)) return {};
|
|
18301
18563
|
try {
|
|
18302
|
-
return JSON.parse(readFileSync2(
|
|
18564
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
18303
18565
|
} catch {
|
|
18304
18566
|
return {};
|
|
18305
18567
|
}
|
|
18306
18568
|
}
|
|
18307
18569
|
function writeConfig(cfg) {
|
|
18308
|
-
const
|
|
18570
|
+
const configPath = getConfigPath();
|
|
18571
|
+
const dir = dirname2(configPath);
|
|
18309
18572
|
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
18310
|
-
|
|
18573
|
+
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
18574
|
+
writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
18575
|
+
renameSync(tempPath, configPath);
|
|
18311
18576
|
}
|
|
18312
18577
|
function renderLayout(title, activeTab, body) {
|
|
18313
18578
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -18346,11 +18611,11 @@ async function saveConfig(){
|
|
|
18346
18611
|
scheduler:{
|
|
18347
18612
|
enabled:form.se.checked,
|
|
18348
18613
|
checkIntervalMs:Number(form.si.value),
|
|
18349
|
-
catchUp:form.cu.value,
|
|
18350
18614
|
},
|
|
18351
18615
|
watchdog:{
|
|
18352
18616
|
heartbeatTimeoutMs:Number(form.wt.value)*1000,
|
|
18353
|
-
|
|
18617
|
+
checkIntervalMs:Number(form.wci.value)*1000,
|
|
18618
|
+
cleanupIntervalMs:Number(form.wcl.value)*3600000,
|
|
18354
18619
|
retentionDays:Number(form.rd.value),
|
|
18355
18620
|
}
|
|
18356
18621
|
};
|
|
@@ -18381,7 +18646,7 @@ async function saveConfig(){
|
|
|
18381
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>
|
|
18382
18647
|
</body></html>`;
|
|
18383
18648
|
}
|
|
18384
|
-
var app, SHARED_STYLES, dashboardApp, web_default;
|
|
18649
|
+
var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
|
|
18385
18650
|
var init_web = __esm({
|
|
18386
18651
|
"src/web/index.tsx"() {
|
|
18387
18652
|
"use strict";
|
|
@@ -18393,7 +18658,47 @@ var init_web = __esm({
|
|
|
18393
18658
|
init_drizzle_orm();
|
|
18394
18659
|
init_db2();
|
|
18395
18660
|
init_config();
|
|
18661
|
+
init_health();
|
|
18396
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
|
+
});
|
|
18397
18702
|
SHARED_STYLES = html`
|
|
18398
18703
|
<style>
|
|
18399
18704
|
:root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
|
|
@@ -18470,12 +18775,16 @@ var init_web = __esm({
|
|
|
18470
18775
|
</style>
|
|
18471
18776
|
`;
|
|
18472
18777
|
app.get("/", async (c) => {
|
|
18473
|
-
const
|
|
18778
|
+
const pageParam = c.req.query("page") || "1";
|
|
18779
|
+
const page = parsePositiveInteger(pageParam);
|
|
18780
|
+
if (page === null) return c.text("invalid page", 400);
|
|
18474
18781
|
const statusFilter = c.req.query("status") || "";
|
|
18782
|
+
const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
|
|
18783
|
+
if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
|
|
18475
18784
|
const limit = 50;
|
|
18476
18785
|
const offset = (page - 1) * limit;
|
|
18477
18786
|
const [tasks3, statsData] = await Promise.all([
|
|
18478
|
-
TaskService.list({ limit, offset, ...
|
|
18787
|
+
TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
|
|
18479
18788
|
TaskService.stats({})
|
|
18480
18789
|
]);
|
|
18481
18790
|
const taskIds = tasks3.map((t) => t.id);
|
|
@@ -18498,12 +18807,13 @@ var init_web = __esm({
|
|
|
18498
18807
|
</div>`;
|
|
18499
18808
|
let rows = "";
|
|
18500
18809
|
for (const task of tasks3) {
|
|
18501
|
-
const
|
|
18810
|
+
const status = safeStatus(task.status);
|
|
18811
|
+
const st = status.toUpperCase();
|
|
18502
18812
|
rows += `<tr>
|
|
18503
18813
|
<td class="mu">#${task.id}</td>
|
|
18504
18814
|
<td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
|
|
18505
18815
|
<td><span class="tag">${esc(task.agent)}</span></td>
|
|
18506
|
-
<td><span class="badge b-${
|
|
18816
|
+
<td><span class="badge b-${status}">${st}</span></td>
|
|
18507
18817
|
<td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
|
|
18508
18818
|
<td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
|
|
18509
18819
|
<td>
|
|
@@ -18535,21 +18845,13 @@ var init_web = __esm({
|
|
|
18535
18845
|
});
|
|
18536
18846
|
app.get("/templates", async (c) => {
|
|
18537
18847
|
const templates = await TaskTemplateService.list(100);
|
|
18538
|
-
for (const tmpl of templates) {
|
|
18539
|
-
if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
|
|
18540
|
-
try {
|
|
18541
|
-
const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
|
|
18542
|
-
tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
|
|
18543
|
-
} catch {
|
|
18544
|
-
}
|
|
18545
|
-
}
|
|
18546
|
-
}
|
|
18547
18848
|
const enabled = templates.filter((t) => t.enabled).length;
|
|
18548
18849
|
const disabled = templates.length - enabled;
|
|
18549
18850
|
let rows = "";
|
|
18550
18851
|
for (const t of templates) {
|
|
18551
|
-
const
|
|
18552
|
-
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;
|
|
18553
18855
|
let rule = "-";
|
|
18554
18856
|
if (t.scheduleType === "cron") rule = t.cronExpr || "-";
|
|
18555
18857
|
else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
|
|
@@ -18561,7 +18863,7 @@ var init_web = __esm({
|
|
|
18561
18863
|
<td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
|
|
18562
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>
|
|
18563
18865
|
<td><span class="${typeClass}">${typeLabel}</span></td>
|
|
18564
|
-
<td class="m sm">${rule}</td>
|
|
18866
|
+
<td class="m sm">${esc(rule)}</td>
|
|
18565
18867
|
<td>${statusBadge}</td>
|
|
18566
18868
|
<td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
|
|
18567
18869
|
<td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
|
|
@@ -18589,7 +18891,8 @@ var init_web = __esm({
|
|
|
18589
18891
|
return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
|
|
18590
18892
|
});
|
|
18591
18893
|
app.get("/runs", async (c) => {
|
|
18592
|
-
const page =
|
|
18894
|
+
const page = parsePositiveInteger(c.req.query("page") || "1");
|
|
18895
|
+
if (page === null) return c.text("invalid page", 400);
|
|
18593
18896
|
const limit = 50;
|
|
18594
18897
|
const offset = (page - 1) * limit;
|
|
18595
18898
|
const { taskRuns: tr, tasks: tk } = schema_exports;
|
|
@@ -18607,13 +18910,14 @@ var init_web = __esm({
|
|
|
18607
18910
|
childPid: tr.childPid,
|
|
18608
18911
|
taskName: tk.name,
|
|
18609
18912
|
taskAgent: tk.agent
|
|
18610
|
-
}).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);
|
|
18611
18914
|
const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
|
|
18612
18915
|
const total = Number(totalResult[0]?.count ?? 0);
|
|
18613
18916
|
const totalPages = Math.ceil(total / limit);
|
|
18614
18917
|
let rows = "";
|
|
18615
18918
|
const logsHtml = [];
|
|
18616
18919
|
for (const run of runs) {
|
|
18920
|
+
const status = safeStatus(run.status);
|
|
18617
18921
|
const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
18618
18922
|
const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
|
|
18619
18923
|
rows += `<tr>
|
|
@@ -18621,15 +18925,15 @@ var init_web = __esm({
|
|
|
18621
18925
|
<td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
|
|
18622
18926
|
${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
|
|
18623
18927
|
<td><span class="tag">${esc(run.taskAgent)}</span></td>
|
|
18624
|
-
<td><span class="badge b-${
|
|
18928
|
+
<td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
|
|
18625
18929
|
<td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
|
|
18626
18930
|
<td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
18627
18931
|
<td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
|
|
18628
18932
|
</tr>`;
|
|
18629
18933
|
if (run.log) {
|
|
18630
18934
|
logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
|
|
18631
|
-
<div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
|
|
18632
|
-
<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>`);
|
|
18633
18937
|
}
|
|
18634
18938
|
}
|
|
18635
18939
|
let paging = `<div class="pn">`;
|
|
@@ -18655,17 +18959,18 @@ var init_web = __esm({
|
|
|
18655
18959
|
});
|
|
18656
18960
|
app.get("/system", async (c) => {
|
|
18657
18961
|
const config = loadConfig();
|
|
18962
|
+
const configPath = getConfigPath();
|
|
18658
18963
|
const stats = await TaskService.stats({});
|
|
18659
18964
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
18660
18965
|
const templates = await TaskTemplateService.list(100);
|
|
18661
|
-
const configExists = existsSync3(
|
|
18966
|
+
const configExists = existsSync3(configPath);
|
|
18662
18967
|
let runRows = "";
|
|
18663
18968
|
if (runningRuns.length > 0) {
|
|
18664
18969
|
for (const run of runningRuns) {
|
|
18665
18970
|
const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
|
|
18666
18971
|
runRows += `<tr>
|
|
18667
18972
|
<td class="mu">#${run.id}</td><td>#${run.taskId}</td>
|
|
18668
|
-
<td class="m sm">${shortS}</td>
|
|
18973
|
+
<td class="m sm">${esc(shortS)}</td>
|
|
18669
18974
|
<td class="sm">${esc(run.model) || "-"}</td>
|
|
18670
18975
|
<td class="sm">${formatDate(run.startedAt)}</td>
|
|
18671
18976
|
<td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
|
|
@@ -18690,19 +18995,14 @@ var init_web = __esm({
|
|
|
18690
18995
|
<h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
|
|
18691
18996
|
<div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
|
|
18692
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>
|
|
18693
|
-
<div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
|
|
18694
|
-
<option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
|
|
18695
|
-
<option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
|
|
18696
|
-
<option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
|
|
18697
|
-
</select></div>
|
|
18698
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>
|
|
18699
18999
|
</div>
|
|
18700
19000
|
<div class="card">
|
|
18701
19001
|
<h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
|
|
18702
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>
|
|
18703
|
-
<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>
|
|
18704
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>
|
|
18705
|
-
<div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
|
|
18706
19006
|
</div>
|
|
18707
19007
|
</div>
|
|
18708
19008
|
<div style="text-align:center;margin-bottom:24px">
|
|
@@ -18731,7 +19031,7 @@ var init_web = __esm({
|
|
|
18731
19031
|
|
|
18732
19032
|
<div class="card mt16">
|
|
18733
19033
|
<h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
|
|
18734
|
-
<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>
|
|
18735
19035
|
<div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
|
|
18736
19036
|
</div>
|
|
18737
19037
|
|
|
@@ -18743,46 +19043,61 @@ var init_web = __esm({
|
|
|
18743
19043
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
18744
19044
|
});
|
|
18745
19045
|
app.get("/api/tasks/:id", async (c) => {
|
|
18746
|
-
const id =
|
|
19046
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19047
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18747
19048
|
const task = await TaskService.getById(id);
|
|
18748
19049
|
if (!task) return c.json({ error: "not found" }, 404);
|
|
18749
19050
|
const runs = await TaskRunService.listByTaskId(id);
|
|
18750
19051
|
return c.json({ ...task, _runs: runs });
|
|
18751
19052
|
});
|
|
18752
19053
|
app.get("/api/runs/:id", async (c) => {
|
|
18753
|
-
const id =
|
|
19054
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19055
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18754
19056
|
const run = await TaskRunService.getById(id);
|
|
18755
19057
|
if (!run) return c.json({ error: "not found" }, 404);
|
|
18756
19058
|
return c.json(run);
|
|
18757
19059
|
});
|
|
18758
19060
|
app.get("/api/templates/:id", async (c) => {
|
|
18759
|
-
const id =
|
|
19061
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19062
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18760
19063
|
const tmpl = await TaskTemplateService.getById(id);
|
|
18761
19064
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
18762
19065
|
return c.json(tmpl);
|
|
18763
19066
|
});
|
|
18764
19067
|
app.post("/api/tasks/:id/retry", async (c) => {
|
|
18765
|
-
|
|
18766
|
-
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);
|
|
18767
19073
|
});
|
|
18768
19074
|
app.delete("/api/tasks/:id", async (c) => {
|
|
18769
|
-
|
|
18770
|
-
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);
|
|
18771
19079
|
});
|
|
18772
19080
|
app.post("/api/templates/:id/enable", async (c) => {
|
|
18773
|
-
const
|
|
18774
|
-
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);
|
|
18775
19085
|
});
|
|
18776
19086
|
app.post("/api/templates/:id/disable", async (c) => {
|
|
18777
|
-
const
|
|
18778
|
-
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);
|
|
18779
19091
|
});
|
|
18780
19092
|
app.delete("/api/templates/:id", async (c) => {
|
|
18781
|
-
const
|
|
18782
|
-
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);
|
|
18783
19097
|
});
|
|
18784
19098
|
app.post("/api/templates/:id/trigger", async (c) => {
|
|
18785
|
-
const id =
|
|
19099
|
+
const id = parsePositiveInteger(c.req.param("id"));
|
|
19100
|
+
if (id === null) return c.json({ error: "invalid id" }, 400);
|
|
18786
19101
|
const tmpl = await TaskTemplateService.getById(id);
|
|
18787
19102
|
if (!tmpl) return c.json({ error: "not found" }, 404);
|
|
18788
19103
|
const task = await TaskService.add({
|
|
@@ -18794,7 +19109,10 @@ var init_web = __esm({
|
|
|
18794
19109
|
category: tmpl.category,
|
|
18795
19110
|
importance: tmpl.importance,
|
|
18796
19111
|
urgency: tmpl.urgency,
|
|
19112
|
+
batchId: tmpl.batchId,
|
|
18797
19113
|
maxRetries: tmpl.maxRetries,
|
|
19114
|
+
retryBackoffMs: tmpl.retryBackoffMs,
|
|
19115
|
+
timeoutMs: tmpl.timeoutMs,
|
|
18798
19116
|
templateId: tmpl.id
|
|
18799
19117
|
});
|
|
18800
19118
|
return c.json({ success: true, taskId: task.id });
|
|
@@ -18809,22 +19127,29 @@ var init_web = __esm({
|
|
|
18809
19127
|
const bW = body.worker ?? {};
|
|
18810
19128
|
const bS = body.scheduler ?? {};
|
|
18811
19129
|
const bD = body.watchdog ?? {};
|
|
18812
|
-
const merged = {
|
|
18813
|
-
|
|
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));
|
|
18814
19139
|
return c.json({ success: true });
|
|
18815
19140
|
} catch (err) {
|
|
18816
|
-
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);
|
|
18817
19142
|
}
|
|
18818
19143
|
});
|
|
18819
19144
|
app.post("/api/database/clear", async (c) => {
|
|
18820
19145
|
try {
|
|
18821
|
-
const { tasks: tasks3, taskRuns:
|
|
18822
|
-
await db.delete(
|
|
19146
|
+
const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
|
|
19147
|
+
await db.delete(taskRuns4);
|
|
18823
19148
|
await db.delete(taskTemplates4);
|
|
18824
19149
|
await db.delete(tasks3);
|
|
18825
19150
|
return c.json({ success: true });
|
|
18826
19151
|
} catch (err) {
|
|
18827
|
-
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);
|
|
18828
19153
|
}
|
|
18829
19154
|
});
|
|
18830
19155
|
dashboardApp = app;
|
|
@@ -18842,39 +19167,118 @@ init_config();
|
|
|
18842
19167
|
// src/worker/index.ts
|
|
18843
19168
|
init_task_service();
|
|
18844
19169
|
init_task_run_service();
|
|
19170
|
+
init_health();
|
|
18845
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";
|
|
18846
19236
|
var WorkerEngine = class {
|
|
18847
19237
|
activeBatchIds = /* @__PURE__ */ new Set();
|
|
18848
19238
|
runningTasks = /* @__PURE__ */ new Map();
|
|
18849
19239
|
stopped = false;
|
|
18850
19240
|
pollTimer = null;
|
|
18851
19241
|
heartbeatTimer = null;
|
|
19242
|
+
pollCyclePromise = null;
|
|
18852
19243
|
cfg;
|
|
18853
|
-
|
|
19244
|
+
opencodeBin;
|
|
19245
|
+
maxOutputChars;
|
|
19246
|
+
constructor(cfg, options = {}) {
|
|
18854
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;
|
|
18855
19250
|
}
|
|
18856
19251
|
start() {
|
|
18857
19252
|
this.stopped = false;
|
|
19253
|
+
markGatewayActivity("worker");
|
|
18858
19254
|
this.poll();
|
|
18859
19255
|
this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
|
|
18860
19256
|
}
|
|
18861
|
-
stop() {
|
|
19257
|
+
async stop(gracePeriodMs = 0) {
|
|
18862
19258
|
this.stopped = true;
|
|
18863
19259
|
if (this.pollTimer) {
|
|
18864
19260
|
clearTimeout(this.pollTimer);
|
|
18865
19261
|
this.pollTimer = null;
|
|
18866
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
|
+
}
|
|
18867
19270
|
if (this.heartbeatTimer) {
|
|
18868
19271
|
clearInterval(this.heartbeatTimer);
|
|
18869
19272
|
this.heartbeatTimer = null;
|
|
18870
19273
|
}
|
|
19274
|
+
const interruptedTaskIds = [...this.runningTasks.keys()];
|
|
18871
19275
|
const killPromises = [];
|
|
18872
|
-
for (const
|
|
19276
|
+
for (const entry of this.runningTasks.values()) {
|
|
18873
19277
|
entry.shutdown = true;
|
|
18874
19278
|
killPromises.push(this.killEntry(entry));
|
|
18875
19279
|
}
|
|
18876
|
-
|
|
18877
|
-
|
|
19280
|
+
await Promise.allSettled(killPromises);
|
|
19281
|
+
return interruptedTaskIds;
|
|
18878
19282
|
}
|
|
18879
19283
|
getRunningTaskIds() {
|
|
18880
19284
|
return [...this.runningTasks.keys()];
|
|
@@ -18884,136 +19288,244 @@ var WorkerEngine = class {
|
|
|
18884
19288
|
}
|
|
18885
19289
|
poll() {
|
|
18886
19290
|
if (this.stopped) return;
|
|
18887
|
-
|
|
19291
|
+
markGatewayActivity("worker");
|
|
19292
|
+
this.pollCyclePromise = this.tryDispatch().finally(() => {
|
|
19293
|
+
this.pollCyclePromise = null;
|
|
18888
19294
|
if (this.stopped) return;
|
|
18889
19295
|
this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
|
|
18890
19296
|
});
|
|
18891
19297
|
}
|
|
18892
19298
|
async tryDispatch() {
|
|
19299
|
+
await this.reconcileCancelledTasks();
|
|
18893
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
|
+
}
|
|
18894
19317
|
try {
|
|
18895
|
-
const excludedBatchIds = [...this.activeBatchIds];
|
|
18896
|
-
const task = await TaskService.next({ excludedBatchIds });
|
|
18897
|
-
if (!task) break;
|
|
18898
|
-
if (!await TaskService.start(task.id)) continue;
|
|
18899
|
-
if (task.batchId) {
|
|
18900
|
-
this.activeBatchIds.add(task.batchId);
|
|
18901
|
-
}
|
|
18902
19318
|
const run = await TaskRunService.create({
|
|
18903
19319
|
taskId: task.id,
|
|
18904
19320
|
model: this.resolveModel(task.model),
|
|
18905
19321
|
status: "running"
|
|
18906
19322
|
});
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
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
|
-
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);
|
|
18964
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);
|
|
18965
19454
|
} catch (err) {
|
|
18966
|
-
|
|
18967
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18968
|
-
level: "error",
|
|
18969
|
-
msg: "tryDispatch iteration failed",
|
|
18970
|
-
error: err instanceof Error ? err.message : String(err)
|
|
18971
|
-
}));
|
|
18972
|
-
break;
|
|
19455
|
+
this.logError("cancel reconciliation failed", err, entry.task.id);
|
|
18973
19456
|
}
|
|
18974
19457
|
}
|
|
18975
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
|
+
}
|
|
18976
19483
|
async updateHeartbeats() {
|
|
18977
|
-
for (const
|
|
19484
|
+
for (const entry of this.runningTasks.values()) {
|
|
18978
19485
|
try {
|
|
18979
19486
|
await TaskRunService.heartbeat(entry.runId);
|
|
18980
|
-
} catch {
|
|
19487
|
+
} catch (err) {
|
|
19488
|
+
this.logError("heartbeat update failed", err, entry.task.id);
|
|
18981
19489
|
}
|
|
18982
19490
|
}
|
|
18983
19491
|
}
|
|
18984
19492
|
killEntry(entry) {
|
|
18985
|
-
if (entry.child.exitCode !== null) {
|
|
19493
|
+
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
18986
19494
|
return Promise.resolve();
|
|
18987
19495
|
}
|
|
18988
19496
|
return new Promise((resolve) => {
|
|
18989
19497
|
const timeout = setTimeout(() => {
|
|
18990
|
-
|
|
18991
|
-
if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
|
|
18992
|
-
} catch {
|
|
18993
|
-
}
|
|
19498
|
+
this.signalEntry(entry, "SIGKILL");
|
|
18994
19499
|
resolve();
|
|
18995
19500
|
}, 5e3);
|
|
18996
|
-
entry.child.
|
|
19501
|
+
entry.child.once("close", () => {
|
|
18997
19502
|
clearTimeout(timeout);
|
|
18998
19503
|
resolve();
|
|
18999
19504
|
});
|
|
19000
|
-
|
|
19001
|
-
if (entry.child.pid) {
|
|
19002
|
-
entry.child.kill("SIGTERM");
|
|
19003
|
-
} else {
|
|
19004
|
-
clearTimeout(timeout);
|
|
19005
|
-
resolve();
|
|
19006
|
-
}
|
|
19007
|
-
} catch {
|
|
19008
|
-
clearTimeout(timeout);
|
|
19009
|
-
resolve();
|
|
19010
|
-
}
|
|
19505
|
+
this.signalEntry(entry, "SIGTERM");
|
|
19011
19506
|
});
|
|
19012
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
|
+
}
|
|
19013
19516
|
resolveModel(taskModel) {
|
|
19014
19517
|
if (!taskModel || taskModel === "default") return null;
|
|
19015
19518
|
return taskModel;
|
|
19016
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
|
+
}
|
|
19017
19529
|
};
|
|
19018
19530
|
|
|
19019
19531
|
// src/gateway/watchdog/heartbeat.ts
|
|
@@ -19026,15 +19538,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19026
19538
|
for (const run of staleRuns) {
|
|
19027
19539
|
try {
|
|
19028
19540
|
if (run.childPid != null && run.childPid > 0) {
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
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
|
+
}));
|
|
19032
19552
|
}
|
|
19033
19553
|
}
|
|
19034
19554
|
await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
|
|
19035
19555
|
const newRetryCount = run.taskRetryCount + 1;
|
|
19036
19556
|
const maxRetries = run.taskMaxRetries;
|
|
19037
|
-
if (newRetryCount
|
|
19557
|
+
if (newRetryCount > maxRetries) {
|
|
19038
19558
|
await TaskService.markDeadLetter(run.taskId, newRetryCount);
|
|
19039
19559
|
console.log(JSON.stringify({
|
|
19040
19560
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19046,7 +19566,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
|
19046
19566
|
maxRetries
|
|
19047
19567
|
}));
|
|
19048
19568
|
} else {
|
|
19049
|
-
const backoffMs = computeBackoff(newRetryCount);
|
|
19569
|
+
const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
|
|
19050
19570
|
const retryAfter = Date.now() + backoffMs;
|
|
19051
19571
|
await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
|
|
19052
19572
|
console.log(JSON.stringify({
|
|
@@ -19102,6 +19622,7 @@ async function cleanupOldRecords(retentionDays) {
|
|
|
19102
19622
|
}
|
|
19103
19623
|
|
|
19104
19624
|
// src/gateway/watchdog/index.ts
|
|
19625
|
+
init_health();
|
|
19105
19626
|
var Watchdog = class {
|
|
19106
19627
|
constructor(cfg) {
|
|
19107
19628
|
this.cfg = cfg;
|
|
@@ -19112,13 +19633,14 @@ var Watchdog = class {
|
|
|
19112
19633
|
cleanupTimer = null;
|
|
19113
19634
|
start() {
|
|
19114
19635
|
this.stopped = false;
|
|
19636
|
+
markGatewayActivity("watchdog");
|
|
19115
19637
|
this.heartbeatTimer = setInterval(
|
|
19116
19638
|
() => this.runHeartbeatCheck(),
|
|
19117
|
-
this.cfg.watchdog.
|
|
19639
|
+
this.cfg.watchdog.checkIntervalMs
|
|
19118
19640
|
);
|
|
19119
19641
|
this.cleanupTimer = setInterval(
|
|
19120
19642
|
() => this.runCleanup(),
|
|
19121
|
-
this.cfg.watchdog.cleanupIntervalMs
|
|
19643
|
+
this.cfg.watchdog.cleanupIntervalMs
|
|
19122
19644
|
);
|
|
19123
19645
|
}
|
|
19124
19646
|
stop() {
|
|
@@ -19134,6 +19656,7 @@ var Watchdog = class {
|
|
|
19134
19656
|
}
|
|
19135
19657
|
async runHeartbeatCheck() {
|
|
19136
19658
|
if (this.stopped) return;
|
|
19659
|
+
markGatewayActivity("watchdog");
|
|
19137
19660
|
try {
|
|
19138
19661
|
await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
|
|
19139
19662
|
} catch (err) {
|
|
@@ -19169,7 +19692,7 @@ var { taskTemplates: taskTemplates3 } = schema_exports;
|
|
|
19169
19692
|
async function cloneTaskFromTemplate(templateId) {
|
|
19170
19693
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
19171
19694
|
const tmpl = rows[0];
|
|
19172
|
-
if (!tmpl) return null;
|
|
19695
|
+
if (!tmpl || !tmpl.enabled) return null;
|
|
19173
19696
|
const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
|
|
19174
19697
|
and(
|
|
19175
19698
|
eq(schema_exports.tasks.templateId, templateId),
|
|
@@ -19178,7 +19701,8 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19178
19701
|
).then((r) => r[0].count);
|
|
19179
19702
|
if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
|
|
19180
19703
|
const nowMs = Date.now();
|
|
19181
|
-
const
|
|
19704
|
+
const isDelayed = tmpl.scheduleType === "delayed";
|
|
19705
|
+
const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
|
|
19182
19706
|
tmpl.scheduleType,
|
|
19183
19707
|
tmpl,
|
|
19184
19708
|
nowMs
|
|
@@ -19192,13 +19716,17 @@ async function cloneTaskFromTemplate(templateId) {
|
|
|
19192
19716
|
category: tmpl.category ?? "general",
|
|
19193
19717
|
importance: tmpl.importance ?? 3,
|
|
19194
19718
|
urgency: tmpl.urgency ?? 3,
|
|
19719
|
+
batchId: tmpl.batchId,
|
|
19195
19720
|
maxRetries: tmpl.maxRetries ?? 3,
|
|
19721
|
+
retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
|
|
19722
|
+
timeoutMs: tmpl.timeoutMs,
|
|
19196
19723
|
templateId: tmpl.id,
|
|
19197
19724
|
scheduledAt: nowMs
|
|
19198
19725
|
});
|
|
19199
19726
|
await db.update(taskTemplates3).set({
|
|
19200
19727
|
lastRunAt: nowMs,
|
|
19201
19728
|
nextRunAt,
|
|
19729
|
+
enabled: isDelayed ? false : tmpl.enabled,
|
|
19202
19730
|
updatedAt: nowMs
|
|
19203
19731
|
}).where(eq(taskTemplates3.id, templateId));
|
|
19204
19732
|
return task;
|
|
@@ -19211,7 +19739,7 @@ async function getDueTemplates() {
|
|
|
19211
19739
|
sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
|
|
19212
19740
|
sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
|
|
19213
19741
|
)
|
|
19214
|
-
);
|
|
19742
|
+
).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
|
|
19215
19743
|
}
|
|
19216
19744
|
async function initializeNextRunAt(templateId) {
|
|
19217
19745
|
const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
|
|
@@ -19229,6 +19757,7 @@ async function initializeNextRunAt(templateId) {
|
|
|
19229
19757
|
// src/gateway/scheduler/index.ts
|
|
19230
19758
|
init_db2();
|
|
19231
19759
|
init_drizzle_orm();
|
|
19760
|
+
init_health();
|
|
19232
19761
|
var Scheduler = class {
|
|
19233
19762
|
constructor(cfg) {
|
|
19234
19763
|
this.cfg = cfg;
|
|
@@ -19240,6 +19769,7 @@ var Scheduler = class {
|
|
|
19240
19769
|
async start() {
|
|
19241
19770
|
if (!this.cfg.scheduler.enabled) return;
|
|
19242
19771
|
this.stopped = false;
|
|
19772
|
+
markGatewayActivity("scheduler");
|
|
19243
19773
|
await this.initializeTemplates();
|
|
19244
19774
|
this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
|
|
19245
19775
|
}
|
|
@@ -19252,6 +19782,7 @@ var Scheduler = class {
|
|
|
19252
19782
|
}
|
|
19253
19783
|
async tick() {
|
|
19254
19784
|
if (this.stopped || this.ticking) return;
|
|
19785
|
+
markGatewayActivity("scheduler");
|
|
19255
19786
|
this.ticking = true;
|
|
19256
19787
|
try {
|
|
19257
19788
|
const dueTemplates = await getDueTemplates();
|
|
@@ -19301,6 +19832,7 @@ var Scheduler = class {
|
|
|
19301
19832
|
init_db2();
|
|
19302
19833
|
init_task_service();
|
|
19303
19834
|
init_task_run_service();
|
|
19835
|
+
init_health();
|
|
19304
19836
|
var STALE_THRESHOLD_MS = 3e4;
|
|
19305
19837
|
function acquireLock() {
|
|
19306
19838
|
const now = Date.now();
|
|
@@ -19322,7 +19854,7 @@ function acquireLock() {
|
|
|
19322
19854
|
sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
|
|
19323
19855
|
}
|
|
19324
19856
|
sqlite.exec(
|
|
19325
|
-
"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)",
|
|
19326
19858
|
[pid, now, now]
|
|
19327
19859
|
);
|
|
19328
19860
|
sqlite.exec("COMMIT");
|
|
@@ -19356,6 +19888,18 @@ function updateLockHeartbeat() {
|
|
|
19356
19888
|
} catch {
|
|
19357
19889
|
}
|
|
19358
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
|
+
}
|
|
19359
19903
|
async function main() {
|
|
19360
19904
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
|
|
19361
19905
|
if (!acquireLock()) {
|
|
@@ -19367,20 +19911,21 @@ async function main() {
|
|
|
19367
19911
|
const worker = new WorkerEngine(cfg);
|
|
19368
19912
|
const watchdog = new Watchdog(cfg);
|
|
19369
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
|
+
});
|
|
19370
19920
|
worker.start();
|
|
19371
19921
|
watchdog.start();
|
|
19372
19922
|
await scheduler.start();
|
|
19373
19923
|
if (cfg.dashboard.enabled) {
|
|
19374
19924
|
const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
|
|
19375
19925
|
Bun.serve({
|
|
19926
|
+
hostname: "127.0.0.1",
|
|
19376
19927
|
port: cfg.dashboard.port,
|
|
19377
|
-
fetch
|
|
19378
|
-
const url = new URL(req.url);
|
|
19379
|
-
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "::1") {
|
|
19380
|
-
return new Response("Forbidden", { status: 403 });
|
|
19381
|
-
}
|
|
19382
|
-
return dashboardApp2.fetch(req);
|
|
19383
|
-
}
|
|
19928
|
+
fetch: dashboardApp2.fetch
|
|
19384
19929
|
});
|
|
19385
19930
|
console.log(JSON.stringify({
|
|
19386
19931
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19389,6 +19934,7 @@ async function main() {
|
|
|
19389
19934
|
url: `http://localhost:${cfg.dashboard.port}`
|
|
19390
19935
|
}));
|
|
19391
19936
|
}
|
|
19937
|
+
markGatewayReady();
|
|
19392
19938
|
console.log(JSON.stringify({
|
|
19393
19939
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19394
19940
|
level: "info",
|
|
@@ -19402,10 +19948,10 @@ async function main() {
|
|
|
19402
19948
|
shuttingDown = true;
|
|
19403
19949
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
|
|
19404
19950
|
clearInterval(heartbeatTimer);
|
|
19951
|
+
markGatewayNotReady();
|
|
19405
19952
|
scheduler.stop();
|
|
19406
19953
|
watchdog.stop();
|
|
19407
|
-
const runningIds = worker.
|
|
19408
|
-
await worker.stop();
|
|
19954
|
+
const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
|
|
19409
19955
|
if (runningIds.length > 0) {
|
|
19410
19956
|
const resetCount = await TaskService.resetRunningToPending(runningIds);
|
|
19411
19957
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
|
|
@@ -19415,6 +19961,7 @@ async function main() {
|
|
|
19415
19961
|
await TaskRunService.fail(run.id, "Gateway shutdown");
|
|
19416
19962
|
}
|
|
19417
19963
|
releaseLock();
|
|
19964
|
+
resetGatewayHealth();
|
|
19418
19965
|
closeDb();
|
|
19419
19966
|
console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
|
|
19420
19967
|
process.exit(0);
|