@csdwd/ai-teams-server 0.1.5 → 0.3.0
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/dist/index.js +653 -43
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import path2 from "node:path";
|
|
6
|
+
import fs2 from "node:fs";
|
|
7
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import Fastify from "fastify";
|
|
9
10
|
import swagger from "@fastify/swagger";
|
|
@@ -230,6 +231,171 @@ function parseEncryptionKey(hex) {
|
|
|
230
231
|
return key;
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
// ../../packages/shared/dist/daemon.js
|
|
235
|
+
import fs from "node:fs";
|
|
236
|
+
import path from "node:path";
|
|
237
|
+
import { spawn } from "node:child_process";
|
|
238
|
+
function readPidFile(pidFile) {
|
|
239
|
+
try {
|
|
240
|
+
const content = fs.readFileSync(pidFile, "utf-8").trim();
|
|
241
|
+
const pid = Number(content);
|
|
242
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function writePidFile(pidFile, pid) {
|
|
248
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
249
|
+
fs.writeFileSync(pidFile, String(pid), "utf-8");
|
|
250
|
+
}
|
|
251
|
+
function removePidFile(pidFile) {
|
|
252
|
+
try {
|
|
253
|
+
fs.unlinkSync(pidFile);
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function isProcessRunning(pid) {
|
|
258
|
+
try {
|
|
259
|
+
process.kill(pid, 0);
|
|
260
|
+
return true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function spawnWorker(script, args, logDir) {
|
|
266
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
267
|
+
const logFile = path.join(logDir, "worker.log");
|
|
268
|
+
const logStream = fs.openSync(logFile, "a");
|
|
269
|
+
const child = spawn(process.execPath, [script, ...args], {
|
|
270
|
+
stdio: ["ignore", logStream, logStream],
|
|
271
|
+
env: {
|
|
272
|
+
...process.env,
|
|
273
|
+
__AI_TEAMS_DAEMON_WORKER: "1",
|
|
274
|
+
__AI_TEAMS_DAEMON_WATCHDOG: void 0
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
fs.closeSync(logStream);
|
|
278
|
+
return child;
|
|
279
|
+
}
|
|
280
|
+
var MAX_RESTARTS_PER_MINUTE = 10;
|
|
281
|
+
var RESTART_DELAY_MS = 3e3;
|
|
282
|
+
async function runWatchdog(opts) {
|
|
283
|
+
const { name, pidFile, logDir, workerScript, workerArgs } = opts;
|
|
284
|
+
writePidFile(pidFile, process.pid);
|
|
285
|
+
let restartTimestamps = [];
|
|
286
|
+
function cleanupAndExit(code) {
|
|
287
|
+
removePidFile(pidFile);
|
|
288
|
+
process.exit(code);
|
|
289
|
+
}
|
|
290
|
+
let worker = null;
|
|
291
|
+
let shuttingDown = false;
|
|
292
|
+
function shutdown() {
|
|
293
|
+
if (shuttingDown)
|
|
294
|
+
return;
|
|
295
|
+
shuttingDown = true;
|
|
296
|
+
if (worker)
|
|
297
|
+
worker.kill("SIGTERM");
|
|
298
|
+
}
|
|
299
|
+
process.on("SIGTERM", shutdown);
|
|
300
|
+
process.on("SIGINT", shutdown);
|
|
301
|
+
while (!shuttingDown) {
|
|
302
|
+
await new Promise((resolve) => {
|
|
303
|
+
worker = spawnWorker(workerScript, workerArgs, logDir);
|
|
304
|
+
worker.on("exit", (code, signal) => {
|
|
305
|
+
worker = null;
|
|
306
|
+
if (shuttingDown) {
|
|
307
|
+
cleanupAndExit(0);
|
|
308
|
+
}
|
|
309
|
+
if (code === 0) {
|
|
310
|
+
console.log(`${name}: worker exited cleanly, daemon stopping.`);
|
|
311
|
+
cleanupAndExit(0);
|
|
312
|
+
}
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
restartTimestamps = restartTimestamps.filter((t) => now - t < 6e4);
|
|
315
|
+
restartTimestamps.push(now);
|
|
316
|
+
if (restartTimestamps.length > MAX_RESTARTS_PER_MINUTE) {
|
|
317
|
+
console.error(`${name}: exceeded ${MAX_RESTARTS_PER_MINUTE} restarts/minute, daemon stopping.`);
|
|
318
|
+
cleanupAndExit(1);
|
|
319
|
+
}
|
|
320
|
+
console.log(`${name}: worker crashed (code=${code}, signal=${signal}), restarting in ${RESTART_DELAY_MS}ms...`);
|
|
321
|
+
setTimeout(resolve, RESTART_DELAY_MS);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
cleanupAndExit(0);
|
|
326
|
+
}
|
|
327
|
+
function getDaemonStatus(pidFile) {
|
|
328
|
+
const pid = readPidFile(pidFile);
|
|
329
|
+
if (pid === null) {
|
|
330
|
+
return { running: false, pid: null };
|
|
331
|
+
}
|
|
332
|
+
if (isProcessRunning(pid)) {
|
|
333
|
+
return { running: true, pid };
|
|
334
|
+
}
|
|
335
|
+
removePidFile(pidFile);
|
|
336
|
+
return { running: false, pid: null };
|
|
337
|
+
}
|
|
338
|
+
async function stopDaemon(pidFile) {
|
|
339
|
+
const pid = readPidFile(pidFile);
|
|
340
|
+
if (pid === null) {
|
|
341
|
+
console.log("Not running (no PID file found).");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!isProcessRunning(pid)) {
|
|
345
|
+
removePidFile(pidFile);
|
|
346
|
+
console.log("Not running (stale PID file cleaned).");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
process.kill(pid, "SIGTERM");
|
|
350
|
+
for (let i = 0; i < 20; i++) {
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
352
|
+
if (!isProcessRunning(pid)) {
|
|
353
|
+
removePidFile(pidFile);
|
|
354
|
+
console.log(`Stopped (PID ${pid}).`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
process.kill(pid, "SIGKILL");
|
|
359
|
+
removePidFile(pidFile);
|
|
360
|
+
console.log(`Force killed (PID ${pid}).`);
|
|
361
|
+
}
|
|
362
|
+
function daemonize(opts) {
|
|
363
|
+
if (process.env.__AI_TEAMS_DAEMON_WORKER === "1") {
|
|
364
|
+
return opts.run();
|
|
365
|
+
}
|
|
366
|
+
if (process.env.__AI_TEAMS_DAEMON_WATCHDOG === "1") {
|
|
367
|
+
return runWatchdog({
|
|
368
|
+
name: opts.name,
|
|
369
|
+
pidFile: opts.pidFile,
|
|
370
|
+
logDir: path.dirname(opts.logFile),
|
|
371
|
+
workerScript: process.argv[1],
|
|
372
|
+
workerArgs: process.argv.slice(2)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
fs.mkdirSync(path.dirname(opts.logFile), { recursive: true });
|
|
376
|
+
const child = spawn(process.execPath, [process.argv[1], ...process.argv.slice(2)], {
|
|
377
|
+
detached: true,
|
|
378
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
379
|
+
env: {
|
|
380
|
+
...process.env,
|
|
381
|
+
__AI_TEAMS_DAEMON_WATCHDOG: "1"
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
child.unref();
|
|
385
|
+
return new Promise((resolve) => {
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
const pid = readPidFile(opts.pidFile);
|
|
388
|
+
if (pid) {
|
|
389
|
+
console.log(`${opts.name} started (PID ${pid})`);
|
|
390
|
+
} else {
|
|
391
|
+
console.error(`${opts.name}: failed to start \u2014 PID file not found.`);
|
|
392
|
+
}
|
|
393
|
+
resolve();
|
|
394
|
+
process.exit(pid ? 0 : 1);
|
|
395
|
+
}, 500);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
233
399
|
// src/db.ts
|
|
234
400
|
import { DatabaseSync } from "node:sqlite";
|
|
235
401
|
import pg from "pg";
|
|
@@ -373,6 +539,25 @@ async function initDb(db) {
|
|
|
373
539
|
webhook_url TEXT NOT NULL
|
|
374
540
|
)
|
|
375
541
|
`);
|
|
542
|
+
await db.run(`
|
|
543
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
544
|
+
id TEXT PRIMARY KEY,
|
|
545
|
+
name TEXT NOT NULL,
|
|
546
|
+
cron_expr TEXT NOT NULL,
|
|
547
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
548
|
+
target_mode TEXT NOT NULL DEFAULT 'queue',
|
|
549
|
+
target_agents TEXT NOT NULL DEFAULT '[]',
|
|
550
|
+
prompt TEXT NOT NULL,
|
|
551
|
+
workspace TEXT,
|
|
552
|
+
timeout_sec INTEGER,
|
|
553
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
554
|
+
required_labels TEXT DEFAULT '[]',
|
|
555
|
+
last_run_at TEXT,
|
|
556
|
+
next_run_at TEXT,
|
|
557
|
+
created_at TEXT NOT NULL,
|
|
558
|
+
updated_at TEXT NOT NULL
|
|
559
|
+
)
|
|
560
|
+
`);
|
|
376
561
|
}
|
|
377
562
|
async function hydrateState(db, state, defaultTimeoutSec, maxLogChunksPerTask) {
|
|
378
563
|
const employeeRows = await db.all("SELECT payload_json FROM employees");
|
|
@@ -609,6 +794,90 @@ function taskToDbValues(task) {
|
|
|
609
794
|
function toSnakeCase(str) {
|
|
610
795
|
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
611
796
|
}
|
|
797
|
+
var SCHEDULE_COLUMNS = [
|
|
798
|
+
"id",
|
|
799
|
+
"name",
|
|
800
|
+
"cron_expr",
|
|
801
|
+
"enabled",
|
|
802
|
+
"target_mode",
|
|
803
|
+
"target_agents",
|
|
804
|
+
"prompt",
|
|
805
|
+
"workspace",
|
|
806
|
+
"timeout_sec",
|
|
807
|
+
"priority",
|
|
808
|
+
"required_labels",
|
|
809
|
+
"last_run_at",
|
|
810
|
+
"next_run_at",
|
|
811
|
+
"created_at",
|
|
812
|
+
"updated_at"
|
|
813
|
+
];
|
|
814
|
+
var SCHEDULE_PLACEHOLDERS = SCHEDULE_COLUMNS.map((_, i) => `$${i + 1}`).join(", ");
|
|
815
|
+
var SCHEDULE_UPDATE_SET = SCHEDULE_COLUMNS.slice(1).map((col) => `${col} = excluded.${col}`).join(", ");
|
|
816
|
+
function dbRowToSchedule(row) {
|
|
817
|
+
return {
|
|
818
|
+
id: row.id,
|
|
819
|
+
name: row.name,
|
|
820
|
+
cronExpr: row.cron_expr,
|
|
821
|
+
enabled: row.enabled === 1,
|
|
822
|
+
targetMode: row.target_mode,
|
|
823
|
+
targetAgents: JSON.parse(row.target_agents || "[]"),
|
|
824
|
+
prompt: row.prompt,
|
|
825
|
+
workspace: row.workspace,
|
|
826
|
+
timeoutSec: row.timeout_sec,
|
|
827
|
+
priority: row.priority,
|
|
828
|
+
requiredLabels: row.required_labels ? JSON.parse(row.required_labels) : null,
|
|
829
|
+
lastRunAt: row.last_run_at,
|
|
830
|
+
nextRunAt: row.next_run_at,
|
|
831
|
+
createdAt: row.created_at,
|
|
832
|
+
updatedAt: row.updated_at
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function scheduleToDbValues(s) {
|
|
836
|
+
return [
|
|
837
|
+
s.id,
|
|
838
|
+
s.name,
|
|
839
|
+
s.cronExpr,
|
|
840
|
+
s.enabled ? 1 : 0,
|
|
841
|
+
s.targetMode,
|
|
842
|
+
JSON.stringify(s.targetAgents),
|
|
843
|
+
s.prompt,
|
|
844
|
+
s.workspace,
|
|
845
|
+
s.timeoutSec,
|
|
846
|
+
s.priority,
|
|
847
|
+
s.requiredLabels ? JSON.stringify(s.requiredLabels) : null,
|
|
848
|
+
s.lastRunAt,
|
|
849
|
+
s.nextRunAt,
|
|
850
|
+
s.createdAt,
|
|
851
|
+
s.updatedAt
|
|
852
|
+
];
|
|
853
|
+
}
|
|
854
|
+
async function upsertSchedule(db, schedule) {
|
|
855
|
+
await db.run(
|
|
856
|
+
`INSERT INTO schedules (${SCHEDULE_COLUMNS.join(", ")}) VALUES (${SCHEDULE_PLACEHOLDERS}) ON CONFLICT(id) DO UPDATE SET ${SCHEDULE_UPDATE_SET}`,
|
|
857
|
+
scheduleToDbValues(schedule)
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
async function getAllSchedules(db) {
|
|
861
|
+
const rows = await db.all("SELECT * FROM schedules ORDER BY created_at");
|
|
862
|
+
return rows.map(dbRowToSchedule);
|
|
863
|
+
}
|
|
864
|
+
async function getScheduleById(db, id) {
|
|
865
|
+
const row = await db.get("SELECT * FROM schedules WHERE id = $1", [id]);
|
|
866
|
+
return row ? dbRowToSchedule(row) : void 0;
|
|
867
|
+
}
|
|
868
|
+
async function deleteScheduleRow(db, id) {
|
|
869
|
+
const row = await db.get("SELECT id FROM schedules WHERE id = $1", [id]);
|
|
870
|
+
if (!row) return false;
|
|
871
|
+
await db.run("DELETE FROM schedules WHERE id = $1", [id]);
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
async function updateScheduleFields(db, id, fields) {
|
|
875
|
+
const existing = await getScheduleById(db, id);
|
|
876
|
+
if (!existing) return void 0;
|
|
877
|
+
const updated = { ...existing, ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
878
|
+
await upsertSchedule(db, updated);
|
|
879
|
+
return updated;
|
|
880
|
+
}
|
|
612
881
|
|
|
613
882
|
// src/schemas.ts
|
|
614
883
|
var errorResponseSchema = {
|
|
@@ -890,6 +1159,65 @@ function parseWebhookUrl(value) {
|
|
|
890
1159
|
}
|
|
891
1160
|
return url.toString();
|
|
892
1161
|
}
|
|
1162
|
+
var createScheduleRequestSchema = {
|
|
1163
|
+
type: "object",
|
|
1164
|
+
required: ["name", "cron", "prompt"],
|
|
1165
|
+
properties: {
|
|
1166
|
+
name: { type: "string", minLength: 1 },
|
|
1167
|
+
cron: { type: "string", minLength: 9 },
|
|
1168
|
+
enabled: { type: "boolean", default: true },
|
|
1169
|
+
targetMode: { type: "string", enum: ["queue", "direct", "broadcast"], default: "queue" },
|
|
1170
|
+
targetAgents: { type: "array", items: { type: "string" } },
|
|
1171
|
+
prompt: { type: "string", minLength: 1 },
|
|
1172
|
+
workspace: { type: "string" },
|
|
1173
|
+
timeoutSec: { type: "number", minimum: 1 },
|
|
1174
|
+
priority: { type: "integer", minimum: 0, maximum: 3, default: 0 },
|
|
1175
|
+
requiredLabels: { type: "array", items: { type: "string" } }
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
var updateScheduleRequestSchema = {
|
|
1179
|
+
type: "object",
|
|
1180
|
+
properties: {
|
|
1181
|
+
name: { type: "string", minLength: 1 },
|
|
1182
|
+
cron: { type: "string", minLength: 9 },
|
|
1183
|
+
enabled: { type: "boolean" },
|
|
1184
|
+
targetMode: { type: "string", enum: ["queue", "direct", "broadcast"] },
|
|
1185
|
+
targetAgents: { type: "array", items: { type: "string" } },
|
|
1186
|
+
prompt: { type: "string", minLength: 1 },
|
|
1187
|
+
workspace: { type: "string" },
|
|
1188
|
+
timeoutSec: { type: "number", minimum: 1 },
|
|
1189
|
+
priority: { type: "integer", minimum: 0, maximum: 3 },
|
|
1190
|
+
requiredLabels: { type: "array", items: { type: "string" } }
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
var scheduleResponseSchema = {
|
|
1194
|
+
type: "object",
|
|
1195
|
+
required: ["id", "name", "cron", "enabled", "targetMode", "prompt", "createdAt", "updatedAt"],
|
|
1196
|
+
properties: {
|
|
1197
|
+
id: { type: "string" },
|
|
1198
|
+
name: { type: "string" },
|
|
1199
|
+
cron: { type: "string" },
|
|
1200
|
+
enabled: { type: "boolean" },
|
|
1201
|
+
targetMode: { type: "string", enum: ["queue", "direct", "broadcast"] },
|
|
1202
|
+
targetAgents: { type: "array", items: { type: "string" } },
|
|
1203
|
+
prompt: { type: "string" },
|
|
1204
|
+
workspace: { type: "string" },
|
|
1205
|
+
timeoutSec: { type: "number" },
|
|
1206
|
+
priority: { type: "integer" },
|
|
1207
|
+
requiredLabels: { type: "array", items: { type: "string" } },
|
|
1208
|
+
lastRunAt: { type: "string" },
|
|
1209
|
+
nextRunAt: { type: "string" },
|
|
1210
|
+
createdAt: { type: "string" },
|
|
1211
|
+
updatedAt: { type: "string" }
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
var scheduleListResponseSchema = {
|
|
1215
|
+
type: "object",
|
|
1216
|
+
required: ["schedules"],
|
|
1217
|
+
properties: {
|
|
1218
|
+
schedules: { type: "array", items: scheduleResponseSchema }
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
893
1221
|
|
|
894
1222
|
// src/dispatch.ts
|
|
895
1223
|
import { createHmac, randomUUID } from "node:crypto";
|
|
@@ -1655,7 +1983,8 @@ function createInMemoryStateStore() {
|
|
|
1655
1983
|
taskQueues: /* @__PURE__ */ new Map(),
|
|
1656
1984
|
mainTaskQueues: /* @__PURE__ */ new Map(),
|
|
1657
1985
|
sharedTaskQueue: [],
|
|
1658
|
-
sharedQueueCursor: 0
|
|
1986
|
+
sharedQueueCursor: 0,
|
|
1987
|
+
scheduleJobs: /* @__PURE__ */ new Map()
|
|
1659
1988
|
};
|
|
1660
1989
|
}
|
|
1661
1990
|
|
|
@@ -1705,6 +2034,41 @@ function createEncryptor(encryptionKeyHex) {
|
|
|
1705
2034
|
};
|
|
1706
2035
|
}
|
|
1707
2036
|
|
|
2037
|
+
// src/scheduler.ts
|
|
2038
|
+
import { CronJob } from "cron";
|
|
2039
|
+
function startScheduleJob(schedule, dispatchFn, jobs, onFire) {
|
|
2040
|
+
const atAgents = schedule.targetMode === "queue" ? "queue" : schedule.targetMode === "broadcast" ? "all" : schedule.targetAgents;
|
|
2041
|
+
const job = new CronJob(
|
|
2042
|
+
schedule.cronExpr,
|
|
2043
|
+
() => {
|
|
2044
|
+
dispatchFn(
|
|
2045
|
+
{
|
|
2046
|
+
type: "command.dispatch",
|
|
2047
|
+
atAgents,
|
|
2048
|
+
prompt: schedule.prompt,
|
|
2049
|
+
workspace: schedule.workspace ?? void 0,
|
|
2050
|
+
timeoutSec: schedule.timeoutSec ?? void 0
|
|
2051
|
+
},
|
|
2052
|
+
null,
|
|
2053
|
+
void 0,
|
|
2054
|
+
schedule.priority,
|
|
2055
|
+
schedule.requiredLabels
|
|
2056
|
+
);
|
|
2057
|
+
onFire(schedule.id);
|
|
2058
|
+
},
|
|
2059
|
+
null,
|
|
2060
|
+
true
|
|
2061
|
+
);
|
|
2062
|
+
jobs.set(schedule.id, job);
|
|
2063
|
+
}
|
|
2064
|
+
function stopScheduleJob(jobs, scheduleId) {
|
|
2065
|
+
const job = jobs.get(scheduleId);
|
|
2066
|
+
if (job) {
|
|
2067
|
+
job.stop();
|
|
2068
|
+
jobs.delete(scheduleId);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1708
2072
|
// src/index.ts
|
|
1709
2073
|
var DEFAULT_PORT = 3789;
|
|
1710
2074
|
function isAuthorized(authToken, rawUrl, headers) {
|
|
@@ -1720,8 +2084,8 @@ async function createAiTeamsServer(options) {
|
|
|
1720
2084
|
const defaultTimeoutSec = options.defaultTimeoutSec ?? 1800;
|
|
1721
2085
|
const disconnectGraceMs = options.disconnectGraceMs ?? 15e3;
|
|
1722
2086
|
const maxLogChunksPerTask = options.maxLogChunksPerTask ?? 400;
|
|
1723
|
-
const dataDir = options.dataDir ??
|
|
1724
|
-
const dbPath = options.dbPath ??
|
|
2087
|
+
const dataDir = options.dataDir ?? path2.join(process.cwd(), "data");
|
|
2088
|
+
const dbPath = options.dbPath ?? path2.join(dataDir, "ai-teams.db");
|
|
1725
2089
|
let closing = false;
|
|
1726
2090
|
const state = createInMemoryStateStore();
|
|
1727
2091
|
const db = await createDatabaseFromEnv({
|
|
@@ -1736,13 +2100,13 @@ async function createAiTeamsServer(options) {
|
|
|
1736
2100
|
const logDir = options.logDir || process.env.LOG_DIR;
|
|
1737
2101
|
let loggerConfig = { level: logLevel };
|
|
1738
2102
|
if (logDir && options.logger !== false) {
|
|
1739
|
-
|
|
2103
|
+
fs2.mkdirSync(logDir, { recursive: true });
|
|
1740
2104
|
loggerConfig = {
|
|
1741
2105
|
level: logLevel,
|
|
1742
2106
|
transport: {
|
|
1743
2107
|
targets: [
|
|
1744
2108
|
{ target: "pino/file", options: { destination: 1 }, level: logLevel },
|
|
1745
|
-
{ target: "pino/file", options: { destination:
|
|
2109
|
+
{ target: "pino/file", options: { destination: path2.join(logDir, "server.log") }, level: logLevel }
|
|
1746
2110
|
]
|
|
1747
2111
|
}
|
|
1748
2112
|
};
|
|
@@ -1775,8 +2139,8 @@ async function createAiTeamsServer(options) {
|
|
|
1775
2139
|
},
|
|
1776
2140
|
staticCSP: true
|
|
1777
2141
|
});
|
|
1778
|
-
const webDir =
|
|
1779
|
-
if (
|
|
2142
|
+
const webDir = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "web");
|
|
2143
|
+
if (fs2.existsSync(webDir)) {
|
|
1780
2144
|
await app.register(fastifyStatic, { root: webDir, prefix: "/" });
|
|
1781
2145
|
app.setNotFoundHandler((_, reply) => {
|
|
1782
2146
|
reply.sendFile("index.html");
|
|
@@ -1794,6 +2158,29 @@ async function createAiTeamsServer(options) {
|
|
|
1794
2158
|
encryptor: createEncryptor(process.env.AI_TEAMS_ENCRYPTION_KEY)
|
|
1795
2159
|
};
|
|
1796
2160
|
const { dispatchLeaderCommand, handleAgentMessage, handleLeaderMessage, cancelTaskById } = createDispatch(dispatchCtx);
|
|
2161
|
+
const scheduleDispatchFn = (message, webhookUrl, cliConfig, priority, requiredLabels) => {
|
|
2162
|
+
return dispatchLeaderCommand(message, webhookUrl, cliConfig, priority, requiredLabels);
|
|
2163
|
+
};
|
|
2164
|
+
async function onScheduleFire(scheduleId) {
|
|
2165
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2166
|
+
const job = state.scheduleJobs.get(scheduleId);
|
|
2167
|
+
const nextRun = job?.nextDate()?.toISO() ?? null;
|
|
2168
|
+
await updateScheduleFields(db, scheduleId, { lastRunAt: now, nextRunAt: nextRun });
|
|
2169
|
+
}
|
|
2170
|
+
async function loadAndStartSchedules() {
|
|
2171
|
+
const schedules = await getAllSchedules(db);
|
|
2172
|
+
for (const schedule of schedules) {
|
|
2173
|
+
if (schedule.enabled) {
|
|
2174
|
+
try {
|
|
2175
|
+
startScheduleJob(schedule, scheduleDispatchFn, state.scheduleJobs, onScheduleFire);
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
app.log.error({ scheduleId: schedule.id, cronExpr: schedule.cronExpr, err }, "Failed to start schedule");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
app.log.info({ count: schedules.filter((s) => s.enabled).length }, "Schedules loaded");
|
|
2182
|
+
}
|
|
2183
|
+
await loadAndStartSchedules();
|
|
1797
2184
|
function buildSnapshot() {
|
|
1798
2185
|
return {
|
|
1799
2186
|
employees: [...state.employees.values()],
|
|
@@ -1864,19 +2251,19 @@ async function createAiTeamsServer(options) {
|
|
|
1864
2251
|
return { employeeId, workspace: null, activeSessionId: null, sessions: [] };
|
|
1865
2252
|
}
|
|
1866
2253
|
const encodedPath = workspace.replace(/\//g, "-");
|
|
1867
|
-
const claudeProjectsDir =
|
|
2254
|
+
const claudeProjectsDir = path2.join(os.homedir(), ".claude", "projects", encodedPath);
|
|
1868
2255
|
let entries;
|
|
1869
2256
|
try {
|
|
1870
|
-
entries =
|
|
2257
|
+
entries = fs2.readdirSync(claudeProjectsDir, { withFileTypes: true });
|
|
1871
2258
|
} catch {
|
|
1872
2259
|
return { employeeId, workspace, activeSessionId: null, sessions: [] };
|
|
1873
2260
|
}
|
|
1874
2261
|
const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).sort((a, b) => b.name.localeCompare(a.name));
|
|
1875
2262
|
const sessions = jsonlFiles.map((entry) => {
|
|
1876
|
-
const filePath =
|
|
1877
|
-
const stat =
|
|
2263
|
+
const filePath = path2.join(claudeProjectsDir, entry.name);
|
|
2264
|
+
const stat = fs2.statSync(filePath);
|
|
1878
2265
|
const sessionId = entry.name.replace(/\.jsonl$/, "");
|
|
1879
|
-
const content =
|
|
2266
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
1880
2267
|
const lines = content.split("\n").filter(Boolean);
|
|
1881
2268
|
const lineCount = lines.length;
|
|
1882
2269
|
let firstUserMessage = null;
|
|
@@ -1889,9 +2276,9 @@ async function createAiTeamsServer(options) {
|
|
|
1889
2276
|
return { id: sessionId, sizeBytes: stat.size, modifiedAt: stat.mtime.toISOString(), lineCount, firstUserMessage, latestUserMessage };
|
|
1890
2277
|
});
|
|
1891
2278
|
let activeSessionId = null;
|
|
1892
|
-
const agentStatePath =
|
|
2279
|
+
const agentStatePath = path2.join(workspace, ".ai-teams", "agents", employeeId, "session-state.json");
|
|
1893
2280
|
try {
|
|
1894
|
-
const raw =
|
|
2281
|
+
const raw = fs2.readFileSync(agentStatePath, "utf8");
|
|
1895
2282
|
activeSessionId = JSON.parse(raw).claudeSessionId ?? null;
|
|
1896
2283
|
} catch {
|
|
1897
2284
|
}
|
|
@@ -1902,7 +2289,7 @@ async function createAiTeamsServer(options) {
|
|
|
1902
2289
|
if (request.url.startsWith("/ws/") || request.url.startsWith("/docs")) {
|
|
1903
2290
|
return;
|
|
1904
2291
|
}
|
|
1905
|
-
const ext =
|
|
2292
|
+
const ext = path2.extname(request.url.split("?")[0]);
|
|
1906
2293
|
if (request.url === "/" || staticExts.has(ext)) {
|
|
1907
2294
|
return;
|
|
1908
2295
|
}
|
|
@@ -2204,6 +2591,163 @@ async function createAiTeamsServer(options) {
|
|
|
2204
2591
|
return { deleted: true };
|
|
2205
2592
|
}
|
|
2206
2593
|
);
|
|
2594
|
+
app.get(
|
|
2595
|
+
"/api/schedules",
|
|
2596
|
+
{
|
|
2597
|
+
schema: {
|
|
2598
|
+
tags: ["schedules"],
|
|
2599
|
+
summary: "List all schedules",
|
|
2600
|
+
response: { 200: scheduleListResponseSchema, 401: errorResponseSchema }
|
|
2601
|
+
}
|
|
2602
|
+
},
|
|
2603
|
+
async () => {
|
|
2604
|
+
const schedules = await getAllSchedules(db);
|
|
2605
|
+
return { schedules };
|
|
2606
|
+
}
|
|
2607
|
+
);
|
|
2608
|
+
app.get(
|
|
2609
|
+
"/api/schedules/:scheduleId",
|
|
2610
|
+
{
|
|
2611
|
+
schema: {
|
|
2612
|
+
tags: ["schedules"],
|
|
2613
|
+
summary: "Get a schedule by ID",
|
|
2614
|
+
params: { type: "object", required: ["scheduleId"], properties: { scheduleId: { type: "string", minLength: 1 } } },
|
|
2615
|
+
response: { 200: scheduleResponseSchema, 404: errorResponseSchema, 401: errorResponseSchema }
|
|
2616
|
+
}
|
|
2617
|
+
},
|
|
2618
|
+
async (request, reply) => {
|
|
2619
|
+
const schedule = await getScheduleById(db, request.params.scheduleId);
|
|
2620
|
+
if (!schedule) return reply.code(404).send({ error: "Schedule not found." });
|
|
2621
|
+
return schedule;
|
|
2622
|
+
}
|
|
2623
|
+
);
|
|
2624
|
+
app.post(
|
|
2625
|
+
"/api/schedules",
|
|
2626
|
+
{
|
|
2627
|
+
schema: {
|
|
2628
|
+
tags: ["schedules"],
|
|
2629
|
+
summary: "Create a schedule",
|
|
2630
|
+
body: createScheduleRequestSchema,
|
|
2631
|
+
response: { 201: scheduleResponseSchema, 400: errorResponseSchema, 401: errorResponseSchema }
|
|
2632
|
+
}
|
|
2633
|
+
},
|
|
2634
|
+
async (request, reply) => {
|
|
2635
|
+
const body = request.body;
|
|
2636
|
+
const id = randomUUID2();
|
|
2637
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2638
|
+
const schedule = {
|
|
2639
|
+
id,
|
|
2640
|
+
name: body.name,
|
|
2641
|
+
cronExpr: body.cron,
|
|
2642
|
+
enabled: body.enabled ?? true,
|
|
2643
|
+
targetMode: body.targetMode ?? "queue",
|
|
2644
|
+
targetAgents: body.targetAgents ?? [],
|
|
2645
|
+
prompt: body.prompt,
|
|
2646
|
+
workspace: body.workspace ?? null,
|
|
2647
|
+
timeoutSec: body.timeoutSec ?? null,
|
|
2648
|
+
priority: body.priority ?? 0,
|
|
2649
|
+
requiredLabels: body.requiredLabels ?? null,
|
|
2650
|
+
lastRunAt: null,
|
|
2651
|
+
nextRunAt: null,
|
|
2652
|
+
createdAt: now,
|
|
2653
|
+
updatedAt: now
|
|
2654
|
+
};
|
|
2655
|
+
try {
|
|
2656
|
+
if (schedule.enabled) {
|
|
2657
|
+
startScheduleJob(schedule, scheduleDispatchFn, state.scheduleJobs, onScheduleFire);
|
|
2658
|
+
schedule.nextRunAt = state.scheduleJobs.get(id)?.nextDate()?.toISO() ?? null;
|
|
2659
|
+
}
|
|
2660
|
+
await upsertSchedule(db, schedule);
|
|
2661
|
+
return reply.code(201).send(schedule);
|
|
2662
|
+
} catch (err) {
|
|
2663
|
+
stopScheduleJob(state.scheduleJobs, id);
|
|
2664
|
+
return reply.code(400).send({ error: err instanceof Error ? err.message : "Invalid schedule." });
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
);
|
|
2668
|
+
app.patch(
|
|
2669
|
+
"/api/schedules/:scheduleId",
|
|
2670
|
+
{
|
|
2671
|
+
schema: {
|
|
2672
|
+
tags: ["schedules"],
|
|
2673
|
+
summary: "Update a schedule",
|
|
2674
|
+
params: { type: "object", required: ["scheduleId"], properties: { scheduleId: { type: "string", minLength: 1 } } },
|
|
2675
|
+
body: updateScheduleRequestSchema,
|
|
2676
|
+
response: { 200: scheduleResponseSchema, 404: errorResponseSchema, 401: errorResponseSchema }
|
|
2677
|
+
}
|
|
2678
|
+
},
|
|
2679
|
+
async (request, reply) => {
|
|
2680
|
+
const existing = await getScheduleById(db, request.params.scheduleId);
|
|
2681
|
+
if (!existing) return reply.code(404).send({ error: "Schedule not found." });
|
|
2682
|
+
const fields = {};
|
|
2683
|
+
if (request.body.name !== void 0) fields.name = request.body.name;
|
|
2684
|
+
if (request.body.cron !== void 0) fields.cronExpr = request.body.cron;
|
|
2685
|
+
if (request.body.enabled !== void 0) fields.enabled = request.body.enabled;
|
|
2686
|
+
if (request.body.targetMode !== void 0) fields.targetMode = request.body.targetMode;
|
|
2687
|
+
if (request.body.targetAgents !== void 0) fields.targetAgents = request.body.targetAgents;
|
|
2688
|
+
if (request.body.prompt !== void 0) fields.prompt = request.body.prompt;
|
|
2689
|
+
if (request.body.workspace !== void 0) fields.workspace = request.body.workspace;
|
|
2690
|
+
if (request.body.timeoutSec !== void 0) fields.timeoutSec = request.body.timeoutSec;
|
|
2691
|
+
if (request.body.priority !== void 0) fields.priority = request.body.priority;
|
|
2692
|
+
if (request.body.requiredLabels !== void 0) fields.requiredLabels = request.body.requiredLabels;
|
|
2693
|
+
const updated = await updateScheduleFields(db, request.params.scheduleId, fields);
|
|
2694
|
+
if (!updated) return reply.code(404).send({ error: "Schedule not found." });
|
|
2695
|
+
stopScheduleJob(state.scheduleJobs, request.params.scheduleId);
|
|
2696
|
+
if (updated.enabled) {
|
|
2697
|
+
startScheduleJob(updated, scheduleDispatchFn, state.scheduleJobs, onScheduleFire);
|
|
2698
|
+
updated.nextRunAt = state.scheduleJobs.get(request.params.scheduleId)?.nextDate()?.toISO() ?? null;
|
|
2699
|
+
await updateScheduleFields(db, request.params.scheduleId, { nextRunAt: updated.nextRunAt });
|
|
2700
|
+
}
|
|
2701
|
+
return updated;
|
|
2702
|
+
}
|
|
2703
|
+
);
|
|
2704
|
+
app.delete(
|
|
2705
|
+
"/api/schedules/:scheduleId",
|
|
2706
|
+
{
|
|
2707
|
+
schema: {
|
|
2708
|
+
tags: ["schedules"],
|
|
2709
|
+
summary: "Delete a schedule",
|
|
2710
|
+
params: { type: "object", required: ["scheduleId"], properties: { scheduleId: { type: "string", minLength: 1 } } },
|
|
2711
|
+
response: {
|
|
2712
|
+
200: { type: "object", required: ["deleted"], properties: { deleted: { type: "boolean" } } },
|
|
2713
|
+
404: errorResponseSchema,
|
|
2714
|
+
401: errorResponseSchema
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
},
|
|
2718
|
+
async (request, reply) => {
|
|
2719
|
+
stopScheduleJob(state.scheduleJobs, request.params.scheduleId);
|
|
2720
|
+
const deleted = await deleteScheduleRow(db, request.params.scheduleId);
|
|
2721
|
+
if (!deleted) return reply.code(404).send({ error: "Schedule not found." });
|
|
2722
|
+
return { deleted: true };
|
|
2723
|
+
}
|
|
2724
|
+
);
|
|
2725
|
+
app.post(
|
|
2726
|
+
"/api/schedules/:scheduleId/trigger",
|
|
2727
|
+
{
|
|
2728
|
+
schema: {
|
|
2729
|
+
tags: ["schedules"],
|
|
2730
|
+
summary: "Manually trigger a schedule",
|
|
2731
|
+
params: { type: "object", required: ["scheduleId"], properties: { scheduleId: { type: "string", minLength: 1 } } },
|
|
2732
|
+
response: { 200: scheduleResponseSchema, 404: errorResponseSchema, 401: errorResponseSchema }
|
|
2733
|
+
}
|
|
2734
|
+
},
|
|
2735
|
+
async (request, reply) => {
|
|
2736
|
+
const schedule = await getScheduleById(db, request.params.scheduleId);
|
|
2737
|
+
if (!schedule) return reply.code(404).send({ error: "Schedule not found." });
|
|
2738
|
+
const atAgents = schedule.targetMode === "queue" ? "queue" : schedule.targetMode === "broadcast" ? "all" : schedule.targetAgents;
|
|
2739
|
+
const result = dispatchLeaderCommand(
|
|
2740
|
+
{ type: "command.dispatch", atAgents, prompt: schedule.prompt, workspace: schedule.workspace ?? void 0, timeoutSec: schedule.timeoutSec ?? void 0 },
|
|
2741
|
+
null,
|
|
2742
|
+
void 0,
|
|
2743
|
+
schedule.priority,
|
|
2744
|
+
schedule.requiredLabels
|
|
2745
|
+
);
|
|
2746
|
+
if (!result.ok) return reply.code(400).send({ error: result.message });
|
|
2747
|
+
await onScheduleFire(request.params.scheduleId);
|
|
2748
|
+
return await getScheduleById(db, request.params.scheduleId);
|
|
2749
|
+
}
|
|
2750
|
+
);
|
|
2207
2751
|
app.get("/ws/agent", { websocket: true }, (socket, request) => {
|
|
2208
2752
|
if (!isAuthorized(options.authToken, request.url, request.headers)) {
|
|
2209
2753
|
socket.close(1008, "unauthorized");
|
|
@@ -2278,6 +2822,9 @@ async function createAiTeamsServer(options) {
|
|
|
2278
2822
|
buildSnapshot,
|
|
2279
2823
|
close: async () => {
|
|
2280
2824
|
closing = true;
|
|
2825
|
+
for (const job of state.scheduleJobs.values()) {
|
|
2826
|
+
job.stop();
|
|
2827
|
+
}
|
|
2281
2828
|
for (const timer of state.taskTimeouts.values()) {
|
|
2282
2829
|
clearTimeout(timer);
|
|
2283
2830
|
}
|
|
@@ -2311,62 +2858,125 @@ async function startServer(options = readOptionsFromEnv()) {
|
|
|
2311
2858
|
await server.app.listen({ port: options.port ?? DEFAULT_PORT, host: options.host ?? "0.0.0.0" });
|
|
2312
2859
|
return server;
|
|
2313
2860
|
}
|
|
2314
|
-
var isCli = process.argv[1] &&
|
|
2861
|
+
var isCli = process.argv[1] && fs2.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
2315
2862
|
if (isCli) {
|
|
2316
2863
|
let getArgValue = function(name) {
|
|
2317
2864
|
const idx = args.indexOf(name);
|
|
2318
2865
|
if (idx === -1) return void 0;
|
|
2319
2866
|
return args[idx + 1];
|
|
2867
|
+
}, resolveDataDir = function() {
|
|
2868
|
+
return getArgValue("--data-dir") || process.env.DATA_DIR || path2.join(process.cwd(), "data");
|
|
2869
|
+
}, resolvePidFile = function() {
|
|
2870
|
+
return path2.join(resolveDataDir(), ".ai-teams-server.pid");
|
|
2871
|
+
}, resolveLogDir = function() {
|
|
2872
|
+
return getArgValue("--log-dir") || process.env.LOG_DIR || path2.join(resolveDataDir(), "logs");
|
|
2873
|
+
}, applyCliArgsToEnv = function() {
|
|
2874
|
+
const cliToken = getArgValue("--token");
|
|
2875
|
+
const cliPort = getArgValue("--port");
|
|
2876
|
+
const cliHost = getArgValue("--host");
|
|
2877
|
+
const cliDataDir = getArgValue("--data-dir");
|
|
2878
|
+
const cliDatabaseUrl = getArgValue("--database-url");
|
|
2879
|
+
const cliDbPath = getArgValue("--db-path");
|
|
2880
|
+
const cliLogLevel = getArgValue("--log-level");
|
|
2881
|
+
const cliLogDir = getArgValue("--log-dir");
|
|
2882
|
+
if (cliToken) process.env.AI_TEAMS_AUTH_TOKEN = cliToken;
|
|
2883
|
+
if (cliPort) process.env.AI_TEAMS_SERVER_PORT = cliPort;
|
|
2884
|
+
if (cliHost) process.env.HOST = cliHost;
|
|
2885
|
+
if (cliDataDir) process.env.DATA_DIR = cliDataDir;
|
|
2886
|
+
if (cliDatabaseUrl) process.env.DATABASE_URL = cliDatabaseUrl;
|
|
2887
|
+
if (cliDbPath) process.env.DB_PATH = cliDbPath;
|
|
2888
|
+
if (cliLogLevel) process.env.LOG_LEVEL = cliLogLevel;
|
|
2889
|
+
if (cliLogDir) process.env.LOG_DIR = cliLogDir;
|
|
2320
2890
|
};
|
|
2321
|
-
getArgValue2 = getArgValue;
|
|
2891
|
+
getArgValue2 = getArgValue, resolveDataDir2 = resolveDataDir, resolvePidFile2 = resolvePidFile, resolveLogDir2 = resolveLogDir, applyCliArgsToEnv2 = applyCliArgsToEnv;
|
|
2322
2892
|
const args = process.argv.slice(2);
|
|
2323
2893
|
if (args.includes("--version") || args.includes("-v")) {
|
|
2324
|
-
console.log("0.
|
|
2894
|
+
console.log("0.3.0");
|
|
2325
2895
|
process.exit(0);
|
|
2326
2896
|
}
|
|
2327
2897
|
if (args.includes("--help") || args.includes("-h")) {
|
|
2328
2898
|
console.log(`ai-teams-server \u2014 AI Teams \u4E2D\u592E\u670D\u52A1\u5668
|
|
2329
2899
|
|
|
2330
|
-
\u7528\u6CD5: ai-teams-server [\u9009\u9879]
|
|
2900
|
+
\u7528\u6CD5: ai-teams-server <command> [\u9009\u9879]
|
|
2901
|
+
|
|
2902
|
+
\u547D\u4EE4:
|
|
2903
|
+
start [\u9009\u9879] \u540E\u53F0\u542F\u52A8\u5B88\u62A4\u8FDB\u7A0B
|
|
2904
|
+
stop \u505C\u6B62\u5B88\u62A4\u8FDB\u7A0B
|
|
2905
|
+
restart [\u9009\u9879] \u91CD\u542F\u5B88\u62A4\u8FDB\u7A0B
|
|
2906
|
+
status \u67E5\u770B\u8FD0\u884C\u72B6\u6001
|
|
2331
2907
|
|
|
2332
2908
|
\u9009\u9879:
|
|
2333
2909
|
--token <token> \u8BA4\u8BC1 Token (\u5FC5\u586B\uFF0C\u6216\u8BBE AI_TEAMS_AUTH_TOKEN)
|
|
2334
2910
|
--port <port> \u670D\u52A1\u7AEF\u53E3 (\u9ED8\u8BA4 3789)
|
|
2335
2911
|
--host <host> \u7ED1\u5B9A\u5730\u5740 (\u9ED8\u8BA4 0.0.0.0)
|
|
2336
2912
|
--data-dir <dir> \u6570\u636E\u76EE\u5F55
|
|
2913
|
+
--database-url <url> PostgreSQL \u8FDE\u63A5\u5B57\u7B26\u4E32 (\u8BBE\u7F6E\u540E\u4F7F\u7528 PostgreSQL \u800C\u975E SQLite)
|
|
2337
2914
|
--db-path <path> \u6570\u636E\u5E93\u8DEF\u5F84
|
|
2338
2915
|
--log-level <level> \u65E5\u5FD7\u7EA7\u522B trace/debug/info/warn/error (\u9ED8\u8BA4 info)
|
|
2339
2916
|
--log-dir <dir> \u65E5\u5FD7\u6587\u4EF6\u76EE\u5F55 (\u4E0D\u8BBE\u5219\u4EC5\u8F93\u51FA\u5230 stdout)
|
|
2340
2917
|
-v, --version \u663E\u793A\u7248\u672C\u53F7
|
|
2341
2918
|
-h, --help \u663E\u793A\u5E2E\u52A9
|
|
2919
|
+
|
|
2920
|
+
\u4E0D\u5E26\u547D\u4EE4\u76F4\u63A5\u8FD0\u884C\u65F6\u4E3A\u524D\u53F0\u6A21\u5F0F\u3002
|
|
2342
2921
|
`);
|
|
2343
2922
|
process.exit(0);
|
|
2344
2923
|
}
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2924
|
+
const subcommand = args[0];
|
|
2925
|
+
if (subcommand === "start" || subcommand === "restart") {
|
|
2926
|
+
void (async () => {
|
|
2927
|
+
if (subcommand === "restart") {
|
|
2928
|
+
const status = getDaemonStatus(resolvePidFile());
|
|
2929
|
+
if (status.running) {
|
|
2930
|
+
await stopDaemon(resolvePidFile());
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
applyCliArgsToEnv();
|
|
2934
|
+
if (!process.env.AI_TEAMS_AUTH_TOKEN) {
|
|
2935
|
+
console.error("\u9519\u8BEF: \u9700\u8981\u8BA4\u8BC1 Token\u3002\u4F7F\u7528 --token <token> \u6216\u8BBE\u7F6E AI_TEAMS_AUTH_TOKEN \u73AF\u5883\u53D8\u91CF\u3002");
|
|
2936
|
+
process.exit(1);
|
|
2937
|
+
}
|
|
2938
|
+
if (!process.env.LOG_DIR) {
|
|
2939
|
+
process.env.LOG_DIR = resolveLogDir();
|
|
2940
|
+
}
|
|
2941
|
+
await daemonize({
|
|
2942
|
+
name: "ai-teams-server",
|
|
2943
|
+
pidFile: resolvePidFile(),
|
|
2944
|
+
logFile: path2.join(resolveLogDir(), "server.log"),
|
|
2945
|
+
run: async () => {
|
|
2946
|
+
await startServer(readOptionsFromEnv());
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
})();
|
|
2950
|
+
} else if (subcommand === "stop") {
|
|
2951
|
+
void (async () => {
|
|
2952
|
+
await stopDaemon(resolvePidFile());
|
|
2953
|
+
})();
|
|
2954
|
+
} else if (subcommand === "status") {
|
|
2955
|
+
const status = getDaemonStatus(resolvePidFile());
|
|
2956
|
+
if (status.running) {
|
|
2957
|
+
console.log(`ai-teams-server is running (PID ${status.pid})`);
|
|
2958
|
+
console.log(`Log: ${path2.join(resolveLogDir(), "server.log")}`);
|
|
2959
|
+
} else {
|
|
2960
|
+
console.log("ai-teams-server is not running.");
|
|
2961
|
+
}
|
|
2962
|
+
} else {
|
|
2963
|
+
applyCliArgsToEnv();
|
|
2964
|
+
const options = readOptionsFromEnv();
|
|
2965
|
+
if (!options.authToken) {
|
|
2966
|
+
console.error("\u9519\u8BEF: \u9700\u8981\u8BA4\u8BC1 Token\u3002\u4F7F\u7528 --token <token> \u6216\u8BBE\u7F6E AI_TEAMS_AUTH_TOKEN \u73AF\u5883\u53D8\u91CF\u3002");
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
}
|
|
2969
|
+
startServer(options).catch((error) => {
|
|
2970
|
+
console.error(error);
|
|
2971
|
+
process.exit(1);
|
|
2972
|
+
});
|
|
2363
2973
|
}
|
|
2364
|
-
startServer(options).catch((error) => {
|
|
2365
|
-
console.error(error);
|
|
2366
|
-
process.exit(1);
|
|
2367
|
-
});
|
|
2368
2974
|
}
|
|
2369
2975
|
var getArgValue2;
|
|
2976
|
+
var resolveDataDir2;
|
|
2977
|
+
var resolvePidFile2;
|
|
2978
|
+
var resolveLogDir2;
|
|
2979
|
+
var applyCliArgsToEnv2;
|
|
2370
2980
|
export {
|
|
2371
2981
|
createAiTeamsServer,
|
|
2372
2982
|
readOptionsFromEnv,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@csdwd/ai-teams-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI Teams central server — Fastify HTTP + WebSocket server for task dispatch, employee management, and web UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"@fastify/swagger": "^9.7.0",
|
|
21
21
|
"@fastify/swagger-ui": "^5.2.5",
|
|
22
22
|
"@fastify/websocket": "^11.0.0",
|
|
23
|
+
"cron": "^4.4.0",
|
|
23
24
|
"fastify": "^5.2.0",
|
|
24
25
|
"pg": "^8.20.0",
|
|
25
26
|
"ws": "^8.18.3"
|