@aiagenta2z/onekey-gateway 0.1.3 → 0.1.5

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.
@@ -0,0 +1,1049 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_LOCAL_GATEWAY = exports.COACHOWL_TIMETABLE_GATEWAY = void 0;
7
+ exports.loadConfigFile = loadConfigFile;
8
+ exports.loadGatewayEndpointConfigSync = loadGatewayEndpointConfigSync;
9
+ exports.resolveGatewayProfile = resolveGatewayProfile;
10
+ exports.getAgentTasks = getAgentTasks;
11
+ exports.claimAgentTask = claimAgentTask;
12
+ exports.runLocalAgent = runLocalAgent;
13
+ exports.runCodex = runCodex;
14
+ exports.runGemini = runGemini;
15
+ exports.runClaude = runClaude;
16
+ exports.runGenericCLI = runGenericCLI;
17
+ exports.runGenericCLIWithMonitor = runGenericCLIWithMonitor;
18
+ exports.postAgentTaskResult = postAgentTaskResult;
19
+ exports.startPullWorker = startPullWorker;
20
+ exports.startCallbackServer = startCallbackServer;
21
+ exports.runGateway = runGateway;
22
+ // =========================
23
+ // OneKey Gateway Runtime
24
+ // coachowl/coachowl
25
+ // =========================
26
+ const fs_1 = __importDefault(require("fs"));
27
+ const http_1 = __importDefault(require("http"));
28
+ const os_1 = __importDefault(require("os"));
29
+ const path_1 = __importDefault(require("path"));
30
+ const child_process_1 = require("child_process");
31
+ const WORKSPACE_ROOT = process.cwd(); // this is your codebase root
32
+ function createTaskWorkspace(taskId) {
33
+ const base = path_1.default.join(WORKSPACE_ROOT, ".onekey", "tmp", taskId);
34
+ fs_1.default.mkdirSync(base, { recursive: true });
35
+ return {
36
+ root: base,
37
+ logs: path_1.default.join(base, "logs.txt"),
38
+ output: path_1.default.join(base, "output.txt"),
39
+ };
40
+ }
41
+ const DEBUG_ENABLE = false;
42
+ const DEFAULT_AGENT_RUN_TIMEOUT = 600000; // 10 minutes default
43
+ const HEALTH_INTERVAL = 1 * 60 * 1000; // /health/post endpoint
44
+ const HEARTBEAT_TIMER_INTERVAL = 1 * 60 * 1000; // heartbeat timer interval
45
+ const DEFAULT_CALLBACK_ENDPOINT_PORT = 18000; // call back port of onekey gateway
46
+ const EVENT_LOG_STATUS_RUNNING = "running";
47
+ const EVENT_LOG_STATUS_STALLED = "stalled";
48
+ const EVENT_LOG_STATUS_COMPLETED = "completed";
49
+ const EVENT_LOG_STATUS_ERROR = "error";
50
+ exports.COACHOWL_TIMETABLE_GATEWAY = "https://coachowl.aiagenta2z.com";
51
+ exports.DEFAULT_LOCAL_GATEWAY = "http://0.0.0.0:7115";
52
+ const PROJECT_LOCAL_DIR = process.cwd();
53
+ const DEFAULT_LOCAL_AGENT_CLI = "claude";
54
+ // ------------------------------
55
+ // Load JSON file safely
56
+ // ------------------------------
57
+ function loadConfigFile(filePath) {
58
+ if (!filePath || typeof filePath !== "string") {
59
+ console.warn("[loadConfigFile] invalid filePath");
60
+ return null;
61
+ }
62
+ if (!fs_1.default.existsSync(filePath)) {
63
+ console.warn(`[loadConfigFile] file not found: ${filePath}`);
64
+ return null;
65
+ }
66
+ try {
67
+ const raw = fs_1.default.readFileSync(filePath, "utf8");
68
+ if (!raw?.trim()) {
69
+ console.warn("[loadConfigFile] empty file");
70
+ return null;
71
+ }
72
+ const parsed = JSON.parse(raw);
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
74
+ console.warn("[loadConfigFile] invalid JSON structure");
75
+ return null;
76
+ }
77
+ return parsed;
78
+ }
79
+ catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ console.error("[loadConfigFile] failed:", message);
82
+ return null;
83
+ }
84
+ }
85
+ function findDataConfigDir() {
86
+ const candidates = [path_1.default.resolve(PROJECT_LOCAL_DIR, "data", "config")];
87
+ for (const candidate of candidates) {
88
+ if (fs_1.default.existsSync(candidate))
89
+ return candidate;
90
+ }
91
+ return null;
92
+ }
93
+ function getDefaultGatewayConfig() {
94
+ return {
95
+ "coachowl/coachowl": {
96
+ local: {
97
+ endpoint: exports.DEFAULT_LOCAL_GATEWAY,
98
+ auth_header: "X-OneKey",
99
+ routes: {
100
+ get_tasks: "/api/v1/agent/tasks/get",
101
+ claim_task: "/api/v1/agent/tasks/claim",
102
+ post_result: "/api/v1/agent/tasks/post",
103
+ // agent_execution_state upsert + heartbeat
104
+ update_health: "/api/v1/agent/tasks/health/post",
105
+ // selective important logs
106
+ update_log: "/api/v1/agent/tasks/log/post"
107
+ },
108
+ agents_clis: ["codex", "gemini", "claude", "openclaw"]
109
+ },
110
+ production: {
111
+ endpoint: exports.COACHOWL_TIMETABLE_GATEWAY,
112
+ auth_header: "X-OneKey",
113
+ routes: {
114
+ get_tasks: "/api/v1/agent/tasks/get",
115
+ claim_task: "/api/v1/agent/tasks/claim",
116
+ post_result: "/api/v1/agent/tasks/post",
117
+ update_health: "/api/v1/agent/tasks/health/post",
118
+ update_log: "/api/v1/agent/tasks/log/post"
119
+ },
120
+ agents_clis: ["codex", "gemini", "claude", "openclaw"]
121
+ }
122
+ }
123
+ };
124
+ }
125
+ function normalizeGatewayConfig(config) {
126
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
127
+ return getDefaultGatewayConfig();
128
+ }
129
+ return config;
130
+ }
131
+ function loadGatewayEndpointConfigSync() {
132
+ const configDir = findDataConfigDir();
133
+ if (!configDir) {
134
+ console.warn("[gateway] config dir not found, using defaults");
135
+ return getDefaultGatewayConfig();
136
+ }
137
+ const configPath = path_1.default.join(configDir, "gateway_endpoint.json");
138
+ const config = loadConfigFile(configPath);
139
+ if (!config) {
140
+ console.warn("[gateway] invalid config, fallback to defaults");
141
+ return getDefaultGatewayConfig();
142
+ }
143
+ return normalizeGatewayConfig(config);
144
+ }
145
+ const gatewayEndpointConfig = loadGatewayEndpointConfigSync();
146
+ // -------------------------
147
+ // Utilities
148
+ // -------------------------
149
+ function today() {
150
+ return new Date().toISOString().slice(0, 10);
151
+ }
152
+ function safeJson(res) {
153
+ return res.json().catch(async (err) => {
154
+ const raw = await res.text().catch(() => "<unreadable body>");
155
+ console.error("❌ safeJson parse failed");
156
+ console.error("Status:", res.status);
157
+ console.error("StatusText:", res.statusText);
158
+ console.error("Error:", err);
159
+ console.error("Raw response body:", raw);
160
+ return {};
161
+ });
162
+ }
163
+ function toBaseUrl(url) {
164
+ return url.replace(/\/+$/, "");
165
+ }
166
+ function isCliAvailable(cli) {
167
+ try {
168
+ (0, child_process_1.execSync)(process.platform === "win32" ? `where ${cli}` : `command -v ${cli}`, {
169
+ stdio: "ignore"
170
+ });
171
+ return true;
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }
177
+ function pickAvailableCli(profile) {
178
+ const list = profile.agents_clis?.length ? profile.agents_clis : [DEFAULT_LOCAL_AGENT_CLI];
179
+ for (const cli of list) {
180
+ if (isCliAvailable(cli))
181
+ return cli;
182
+ }
183
+ return null;
184
+ }
185
+ function makeAgentId(cli) {
186
+ const host = os_1.default.hostname().replace(/[^a-zA-Z0-9_.-]+/g, "_");
187
+ return `${cli}_${host}_${process.pid}`;
188
+ }
189
+ function sleep(ms) {
190
+ return new Promise((resolve) => setTimeout(resolve, ms));
191
+ }
192
+ function parseJsonBody(req) {
193
+ return new Promise((resolve, reject) => {
194
+ const chunks = [];
195
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
196
+ req.on("end", () => {
197
+ const raw = Buffer.concat(chunks).toString("utf8");
198
+ if (!raw.trim())
199
+ return resolve({});
200
+ try {
201
+ resolve(JSON.parse(raw));
202
+ }
203
+ catch (error) {
204
+ reject(error);
205
+ }
206
+ });
207
+ req.on("error", reject);
208
+ });
209
+ }
210
+ function sendJson(res, statusCode, data) {
211
+ const body = JSON.stringify(data);
212
+ res.statusCode = statusCode;
213
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
214
+ res.setHeader("Content-Length", Buffer.byteLength(body));
215
+ res.end(body);
216
+ }
217
+ // -------------------------
218
+ // Gateway profile resolution
219
+ // -------------------------
220
+ function resolveGatewayProfile(uniqueId, mode = "local") {
221
+ const entry = gatewayEndpointConfig?.[uniqueId];
222
+ const profile = entry?.[mode];
223
+ if (!profile?.endpoint || !profile?.auth_header || !profile?.routes) {
224
+ const fallback = getDefaultGatewayConfig()?.[uniqueId]?.[mode];
225
+ if (fallback)
226
+ return fallback;
227
+ throw new Error(`Gateway profile not found for ${uniqueId} (${mode})`);
228
+ }
229
+ return profile;
230
+ }
231
+ // -------------------------
232
+ // 1) GET TASKS
233
+ // -------------------------
234
+ /**
235
+ "local": {
236
+ "endpoint": "http://0.0.0.0:7115",
237
+ "auth_header": "X-OneKey",
238
+ "routes": {
239
+ "get_tasks": "/api/v1/agent/tasks/get",
240
+ "claim_task": "/api/v1/agent/tasks/claim",
241
+ "post_result": "/api/v1/agent/tasks/post"
242
+ },
243
+ "agents_clis": ["codex","gemini","claude","openclaw"]
244
+ }
245
+ */
246
+ async function getAgentTasks(accessKey, profileOrGatewayUrl = exports.DEFAULT_LOCAL_GATEWAY) {
247
+ const profile = typeof profileOrGatewayUrl === "string"
248
+ ? {
249
+ endpoint: profileOrGatewayUrl,
250
+ auth_header: "X-OneKey",
251
+ routes: {
252
+ get_tasks: "/api/v1/agent/tasks/get",
253
+ claim_task: "/api/v1/agent/tasks/claim",
254
+ post_result: "/api/v1/agent/tasks/post",
255
+ update_health: "/api/v1/agent/tasks/health/post",
256
+ update_log: "/api/v1/agent/tasks/log/post"
257
+ }
258
+ }
259
+ : profileOrGatewayUrl;
260
+ const url = `${toBaseUrl(profile.endpoint)}${profile.routes.get_tasks}`;
261
+ if (DEBUG_ENABLE) {
262
+ const headers = { [profile.auth_header]: accessKey };
263
+ console.log(`url ${url} and headers ${headers}`);
264
+ }
265
+ const res = await fetch(url, {
266
+ method: "GET",
267
+ headers: {
268
+ [profile.auth_header]: accessKey
269
+ }
270
+ });
271
+ const data = (await safeJson(res));
272
+ if (!data?.success) {
273
+ throw new Error("[onekey gateway] getAgentTasks failed to get tasks");
274
+ }
275
+ return data;
276
+ }
277
+ // -------------------------
278
+ // 2) CLAIM TASK
279
+ // -------------------------
280
+ async function claimAgentTask(accessKey, payload, profileOrGatewayUrl = exports.DEFAULT_LOCAL_GATEWAY) {
281
+ const profile = typeof profileOrGatewayUrl === "string"
282
+ ? {
283
+ endpoint: profileOrGatewayUrl,
284
+ auth_header: "X-OneKey",
285
+ routes: {
286
+ get_tasks: "/api/v1/agent/tasks/get",
287
+ claim_task: "/api/v1/agent/tasks/claim",
288
+ post_result: "/api/v1/agent/tasks/post",
289
+ update_health: "/api/v1/agent/tasks/health/post",
290
+ update_log: "/api/v1/agent/tasks/log/post"
291
+ }
292
+ }
293
+ : profileOrGatewayUrl;
294
+ const url = `${toBaseUrl(profile.endpoint)}${profile.routes.claim_task}`;
295
+ const routed_payload = {
296
+ method: "POST",
297
+ headers: {
298
+ [profile.auth_header]: accessKey,
299
+ "Content-Type": "application/json"
300
+ },
301
+ body: JSON.stringify({
302
+ agent_id: payload.agent_id,
303
+ agent_name: payload.agent_name || payload.agent_id?.split("_")[0] || "unknown",
304
+ habit_id: payload.habit_id,
305
+ execution_date: payload.execution_date
306
+ })
307
+ };
308
+ if (DEBUG_ENABLE) {
309
+ console.log(`/claim claimAgentTask url ${url} and payload ${JSON.stringify(routed_payload)}`);
310
+ }
311
+ const res = await fetch(url, routed_payload);
312
+ const data = await safeJson(res);
313
+ if (DEBUG_ENABLE) {
314
+ console.log(`/claim claimAgentTask result ${JSON.stringify(data)}`);
315
+ }
316
+ if (!data?.success)
317
+ return { success: false, error: data?.message || "claim_failed" };
318
+ return data;
319
+ }
320
+ function isAgentCLI(cli) {
321
+ return ["codex", "gemini", "claude", "claude-code", "openclaw"].includes(cli);
322
+ }
323
+ async function runLocalAgent(cli, prompt, accessKey, profile, executionContext, timeoutMs = DEFAULT_AGENT_RUN_TIMEOUT, opts) {
324
+ const cwd = opts?.cwd ?? process.cwd();
325
+ const taskDir = opts?.taskDir;
326
+ console.log("\n==============================");
327
+ console.log("[onekey gateway] runLocalAgent ROUTER START");
328
+ console.log("CLI:", cli);
329
+ console.log("CWD:", cwd);
330
+ console.log("TASK_DIR:", taskDir);
331
+ console.log("Prompt:", prompt);
332
+ console.log("==============================\n");
333
+ switch (cli) {
334
+ case "codex":
335
+ return runCodex(prompt, accessKey, profile, executionContext, timeoutMs, opts);
336
+ case "gemini":
337
+ return runGemini(prompt, accessKey, profile, executionContext, timeoutMs, opts);
338
+ case "claude":
339
+ return runClaude(prompt, accessKey, profile, executionContext, timeoutMs, opts);
340
+ default:
341
+ return runGenericCLIWithMonitor(cli, ["-p", prompt], accessKey, profile, executionContext, timeoutMs, cli, cwd, taskDir);
342
+ }
343
+ }
344
+ function runProcess(label, cmd, args, timeoutMs) {
345
+ return new Promise((resolve) => {
346
+ const child = (0, child_process_1.spawn)(cmd, args, {
347
+ stdio: ["ignore", "pipe", "pipe"],
348
+ env: {
349
+ ...process.env,
350
+ TERM: "dumb",
351
+ CI: "true",
352
+ },
353
+ });
354
+ let stdout = "";
355
+ let stderr = "";
356
+ let finished = false;
357
+ const kill = (reason) => {
358
+ if (finished)
359
+ return;
360
+ finished = true;
361
+ try {
362
+ child.kill("SIGKILL");
363
+ }
364
+ catch { }
365
+ resolve({
366
+ success: false,
367
+ content: `${label} killed: ${reason}\n${stdout}\n${stderr}`.trim(),
368
+ exitCode: null,
369
+ });
370
+ };
371
+ const timer = timeoutMs > 0 ? setTimeout(() => kill("timeout"), timeoutMs) : null;
372
+ child.stdout.on("data", (d) => {
373
+ const text = d.toString();
374
+ stdout += text;
375
+ if (DEBUG_ENABLE)
376
+ console.log(`[${label} stdout]`, text);
377
+ });
378
+ child.stderr.on("data", (d) => {
379
+ const text = d.toString();
380
+ stderr += text;
381
+ if (DEBUG_ENABLE)
382
+ console.log(`[${label} stderr]`, text);
383
+ });
384
+ child.on("error", (err) => {
385
+ if (timer)
386
+ clearTimeout(timer);
387
+ kill(`spawn error: ${err.message}`);
388
+ });
389
+ child.on("close", (code) => {
390
+ if (timer)
391
+ clearTimeout(timer);
392
+ if (finished)
393
+ return;
394
+ finished = true;
395
+ resolve({
396
+ success: code === 0,
397
+ content: (stdout + "\n" + stderr).trim() || "(no output)",
398
+ exitCode: code,
399
+ });
400
+ });
401
+ });
402
+ }
403
+ function runCodex(prompt, accessKey, profile, executionContext, timeoutMs = 120000, opts) {
404
+ return runGenericCLIWithMonitor("codex", [
405
+ "exec",
406
+ "--skip-git-repo-check",
407
+ "--full-auto",
408
+ "-C",
409
+ opts?.cwd ?? process.cwd(),
410
+ prompt
411
+ ], accessKey, profile, executionContext, timeoutMs, "codex", opts?.cwd, opts?.taskDir);
412
+ }
413
+ function runGemini(prompt, accessKey, profile, executionContext, timeoutMs = 60000, opts) {
414
+ return runGenericCLIWithMonitor("gemini", ["-p", prompt], accessKey, profile, executionContext, timeoutMs, "gemini", opts?.cwd, opts?.taskDir);
415
+ }
416
+ function runClaude(prompt, accessKey, profile, executionContext, timeoutMs = 60000, opts) {
417
+ return runGenericCLIWithMonitor("claude", ["-p", prompt], accessKey, profile, executionContext, timeoutMs, "claude", opts?.cwd, opts?.taskDir);
418
+ }
419
+ async function runGenericCLI(cli, args, timeoutMs = 60000, label = cli, cwd = process.cwd(), taskDir) {
420
+ return new Promise((resolve) => {
421
+ const child = (0, child_process_1.spawn)(cli, args, {
422
+ cwd, // WORKING DIRECTORY CONTROL
423
+ stdio: ["ignore", "pipe", "pipe"],
424
+ env: {
425
+ ...process.env,
426
+ TERM: "dumb",
427
+ CI: "true",
428
+ NO_COLOR: "1",
429
+ // optional per-task sandbox context
430
+ TASK_DIR: taskDir || "",
431
+ },
432
+ });
433
+ console.info(`[onekey gateway] INFO: Process ID Started: ${child.pid}, CLI: ${cli} ${args.join(" ")}`);
434
+ let stdout = "";
435
+ let stderr = "";
436
+ let finished = false;
437
+ const kill = (reason) => {
438
+ if (finished)
439
+ return;
440
+ finished = true;
441
+ try {
442
+ child.kill("SIGKILL");
443
+ }
444
+ catch { }
445
+ resolve({
446
+ success: false,
447
+ content: `${label} killed: ${reason}\n${stdout}\n${stderr}`.trim(),
448
+ exitCode: null,
449
+ });
450
+ };
451
+ const timer = timeoutMs > 0 ? setTimeout(() => kill("timeout"), timeoutMs) : null;
452
+ child.stdout.on("data", (d) => {
453
+ const text = d.toString("utf8");
454
+ stdout += text;
455
+ if (DEBUG_ENABLE) {
456
+ console.log(`[${label} stdout]`, text);
457
+ }
458
+ });
459
+ child.stderr.on("data", (d) => {
460
+ const text = d.toString("utf8");
461
+ stderr += text;
462
+ if (DEBUG_ENABLE) {
463
+ console.log(`[${label} stderr]`, text);
464
+ }
465
+ });
466
+ child.on("error", (err) => {
467
+ if (timer)
468
+ clearTimeout(timer);
469
+ kill(`spawn error: ${err.message}`);
470
+ });
471
+ child.on("close", (code) => {
472
+ if (timer)
473
+ clearTimeout(timer);
474
+ if (finished)
475
+ return;
476
+ finished = true;
477
+ resolve({
478
+ success: code === 0,
479
+ content: (stdout + "\n" + stderr).trim() || "(no output)",
480
+ exitCode: code,
481
+ });
482
+ });
483
+ });
484
+ }
485
+ async function runGenericCLIWithMonitor(cli, args, accessKey, profile, executionContext, timeoutMs = DEFAULT_AGENT_RUN_TIMEOUT, label = cli, cwd = process.cwd(), taskDir) {
486
+ return new Promise((resolve) => {
487
+ const child = (0, child_process_1.spawn)(cli, args, {
488
+ cwd,
489
+ stdio: ["ignore", "pipe", "pipe"],
490
+ env: {
491
+ ...process.env,
492
+ TERM: "dumb",
493
+ CI: "true",
494
+ NO_COLOR: "1",
495
+ TASK_DIR: taskDir || "",
496
+ },
497
+ });
498
+ const pid = child.pid;
499
+ const baseUrl = profile.endpoint;
500
+ const routes = profile.routes;
501
+ const updateHealthRoute = routes?.update_health || "/api/v1/agent/tasks/health/post";
502
+ const updateLogRoute = routes?.update_log || "/api/v1/agent/tasks/log/post";
503
+ function nowUtcSqlString() {
504
+ // backend expects/produces "%Y-%m-%d %H:%M:%S" (UTC)
505
+ const iso = new Date().toISOString(); // 2026-01-01T00:00:00.000Z
506
+ return iso.replace("T", " ").slice(0, 19);
507
+ }
508
+ async function post(urlPath, body) {
509
+ try {
510
+ const controller = new AbortController();
511
+ const t = setTimeout(() => controller.abort(), 8000);
512
+ await fetch(`${toBaseUrl(baseUrl)}${urlPath}`, {
513
+ method: "POST",
514
+ headers: {
515
+ [profile.auth_header]: accessKey,
516
+ "Content-Type": "application/json",
517
+ },
518
+ body: JSON.stringify(body),
519
+ signal: controller.signal,
520
+ });
521
+ clearTimeout(t);
522
+ }
523
+ catch (e) {
524
+ console.error(`[gateway monitor] post failed`, e);
525
+ }
526
+ }
527
+ let stdout = "";
528
+ let stderr = "";
529
+ let finished = false;
530
+ let lastOutputTime = Date.now();
531
+ let heartbeatTimer = null;
532
+ const startedAt = nowUtcSqlString();
533
+ let lastHealthSentAt = 0;
534
+ let lastImportantLogSentAt = 0;
535
+ let lastImportantMessage;
536
+ let lastReportedStatus;
537
+ const logLineBuffer = {
538
+ stdout: "",
539
+ stderr: "",
540
+ };
541
+ // Aggregate Batched Buffered Log Lines and Post Back
542
+ const batchedLogBuffer = {
543
+ stdout: [],
544
+ stderr: [],
545
+ };
546
+ const BATCH_SIZE = 5;
547
+ function makeLogBody(kind, message, chunk, timestamp, status) {
548
+ return {
549
+ ...executionContext,
550
+ event_type: kind,
551
+ status: status,
552
+ message,
553
+ metadata: {
554
+ pid,
555
+ label,
556
+ chunk,
557
+ timestamp
558
+ },
559
+ };
560
+ }
561
+ async function postStatusLog(status, message) {
562
+ if (lastReportedStatus === status)
563
+ return;
564
+ lastReportedStatus = status;
565
+ const now = Date.now();
566
+ const clipped = message.slice(0, 2000);
567
+ await post(updateLogRoute, makeLogBody("stdout", clipped, clipped, now, status));
568
+ }
569
+ async function flushLogBuffer(kind, status) {
570
+ const buffer = batchedLogBuffer[kind];
571
+ if (buffer.length === 0)
572
+ return;
573
+ const combined = buffer.join("\n");
574
+ batchedLogBuffer[kind] = []; // clear BEFORE await (avoid race)
575
+ const now = Date.now();
576
+ const clipped = combined.slice(0, 4000);
577
+ // const status = "running";
578
+ await post(updateLogRoute, makeLogBody(kind, clipped, clipped, now, status));
579
+ }
580
+ function pushLog(kind, line) {
581
+ batchedLogBuffer[kind].push(line);
582
+ if (batchedLogBuffer[kind].length >= BATCH_SIZE) {
583
+ flushLogBuffer(kind, EVENT_LOG_STATUS_RUNNING); // fire async (don’t await to avoid blocking stream)
584
+ }
585
+ }
586
+ function isImportantLogLine(line) {
587
+ const s = line.trim();
588
+ if (!s)
589
+ return false;
590
+ if (s.length < 3)
591
+ return false;
592
+ if (s.startsWith("{") && s.endsWith("}"))
593
+ return false; // avoid raw JSON spam
594
+ // Keep high-signal lines (errors, warnings, progress, summaries)
595
+ if (/(^|\b)(error|failed|failure|exception|traceback|panic)\b/i.test(s))
596
+ return true;
597
+ if (/(^|\b)(warn|warning)\b/i.test(s))
598
+ return true;
599
+ if (/(^|\b)(done|complete|completed|success|succeed|finished)\b/i.test(s))
600
+ return true;
601
+ if (/(^|\b)(progress|step|phase|running|starting)\b/i.test(s))
602
+ return true;
603
+ if (/(^|\b)(patch|apply_patch|diff|files changed|tests?|build)\b/i.test(s))
604
+ return true;
605
+ if (/(^|\b)(http|https):\/\//i.test(s))
606
+ return true; // usually important links
607
+ return false;
608
+ }
609
+ function normalizeLogChunkToLines(kind, chunk) {
610
+ logLineBuffer[kind] += chunk;
611
+ const parts = logLineBuffer[kind].split(/\r?\n/);
612
+ logLineBuffer[kind] = parts.pop() ?? "";
613
+ return parts;
614
+ }
615
+ async function postHealth(fields) {
616
+ const now = Date.now();
617
+ if (now - lastHealthSentAt < HEALTH_INTERVAL)
618
+ return; // avoid bursts when stdout is noisy
619
+ lastHealthSentAt = now;
620
+ await post(updateHealthRoute, {
621
+ ...executionContext,
622
+ pid,
623
+ label,
624
+ is_alive: true,
625
+ started_at: startedAt,
626
+ heartbeat_at: nowUtcSqlString(),
627
+ last_message: lastImportantMessage,
628
+ ...fields,
629
+ });
630
+ }
631
+ async function postImportantLog(kind, line, status) {
632
+ const now = Date.now();
633
+ if (now - lastImportantLogSentAt < 1200)
634
+ return; // cap log posts
635
+ lastImportantLogSentAt = now;
636
+ const clipped = line.slice(0, 2000);
637
+ // const status = "running";
638
+ await post(updateLogRoute, makeLogBody(kind, clipped, clipped, now, status));
639
+ }
640
+ console.info(`[onekey gateway] Process Started PID=${pid}, CLI=${cli} ${args.join(" ")}`);
641
+ // 🚀 START HEALTH
642
+ postHealth({ status: "starting", progress: 0 });
643
+ postStatusLog(EVENT_LOG_STATUS_RUNNING, `[gateway] process started pid=${pid} cli=${cli}`);
644
+ // 🧠 HEARTBEAT
645
+ heartbeatTimer = setInterval(() => {
646
+ const idleSeconds = Math.floor((Date.now() - lastOutputTime) / 1000);
647
+ const nextStatus = idleSeconds > 120 ? EVENT_LOG_STATUS_STALLED : EVENT_LOG_STATUS_RUNNING;
648
+ postHealth({ status: nextStatus, idle_seconds: idleSeconds });
649
+ postStatusLog(nextStatus, `[gateway] heartbeat idle_seconds=${idleSeconds}`);
650
+ }, HEARTBEAT_TIMER_INTERVAL);
651
+ const kill = (reason) => {
652
+ if (finished)
653
+ return;
654
+ finished = true;
655
+ if (heartbeatTimer)
656
+ clearInterval(heartbeatTimer);
657
+ try {
658
+ child.kill("SIGKILL");
659
+ }
660
+ catch { }
661
+ postHealth({ status: "killed", reason, is_alive: false });
662
+ postStatusLog(EVENT_LOG_STATUS_ERROR, `[gateway] killed: ${reason}`);
663
+ resolve({
664
+ success: false,
665
+ content: `${label} killed: ${reason}\n${stdout}\n${stderr}`.trim(),
666
+ exitCode: null,
667
+ });
668
+ };
669
+ const timer = timeoutMs > 0 ? setTimeout(() => kill("timeout"), timeoutMs) : null;
670
+ // 📡 STDOUT LOG STREAM
671
+ child.stdout.on("data", (d) => {
672
+ const text = d.toString("utf8");
673
+ stdout += text;
674
+ lastOutputTime = Date.now();
675
+ const lines = normalizeLogChunkToLines("stdout", text);
676
+ // Chunking and Post Logs
677
+ for (const line of lines) {
678
+ if (!isImportantLogLine(line))
679
+ continue;
680
+ const trimmed = line.trim();
681
+ lastImportantMessage = trimmed.slice(0, 500);
682
+ // buffered instead of immediate
683
+ pushLog("stdout", trimmed);
684
+ postHealth({ status: EVENT_LOG_STATUS_RUNNING });
685
+ }
686
+ console.log(`[${label} stdout]`, text);
687
+ // if (DEBUG_ENABLE) {
688
+ // console.log(`[${label} stdout]`, text);
689
+ // }
690
+ });
691
+ // 📡 STDERR LOG STREAM
692
+ child.stderr.on("data", (d) => {
693
+ const text = d.toString("utf8");
694
+ stderr += text;
695
+ lastOutputTime = Date.now();
696
+ const lines = normalizeLogChunkToLines("stderr", text);
697
+ for (const line of lines) {
698
+ if (!isImportantLogLine(line))
699
+ continue;
700
+ lastImportantMessage = line.trim().slice(0, 500);
701
+ postImportantLog("stderr", line, EVENT_LOG_STATUS_RUNNING);
702
+ postHealth({ status: EVENT_LOG_STATUS_RUNNING });
703
+ }
704
+ console.log(`[${label} stderr]`, text);
705
+ // if (DEBUG_ENABLE) {
706
+ // console.log(`[${label} stderr]`, text);
707
+ // }
708
+ });
709
+ child.on("error", (err) => {
710
+ if (timer)
711
+ clearTimeout(timer);
712
+ kill(`spawn error: ${err.message}`);
713
+ });
714
+ child.on("close", (code) => {
715
+ if (timer)
716
+ clearTimeout(timer);
717
+ if (heartbeatTimer)
718
+ clearInterval(heartbeatTimer);
719
+ if (finished)
720
+ return;
721
+ finished = true;
722
+ // flush trailing buffered lines
723
+ for (const kind of ["stdout", "stderr"]) {
724
+ const tail = logLineBuffer[kind].trim();
725
+ if (tail && isImportantLogLine(tail)) {
726
+ lastImportantMessage = tail.slice(0, 500);
727
+ postImportantLog(kind, tail, code === 0 ? EVENT_LOG_STATUS_COMPLETED : EVENT_LOG_STATUS_ERROR);
728
+ }
729
+ }
730
+ const finalStatus = code === 0 ? EVENT_LOG_STATUS_COMPLETED : EVENT_LOG_STATUS_ERROR;
731
+ flushLogBuffer("stdout", finalStatus);
732
+ flushLogBuffer("stderr", finalStatus);
733
+ postHealth({
734
+ status: finalStatus,
735
+ exitCode: code,
736
+ success: code === 0,
737
+ is_alive: false,
738
+ progress: 100,
739
+ });
740
+ postStatusLog(finalStatus, `[gateway] process exited code=${code ?? "null"}`);
741
+ post(routes.post_result, {
742
+ results: [
743
+ {
744
+ task_id: executionContext.habit_id,
745
+ execution_id: executionContext.execution_id,
746
+ root_execution_id: executionContext.root_execution_id,
747
+ content: (stdout + "\n" + stderr).trim() || "(no output)",
748
+ agent_id: executionContext.agent_id,
749
+ status: "completed",
750
+ agent_name: executionContext.agent_name,
751
+ },
752
+ ],
753
+ });
754
+ resolve({
755
+ success: code === 0,
756
+ content: (stdout + "\n" + stderr).trim() || "(no output)",
757
+ exitCode: code,
758
+ });
759
+ });
760
+ });
761
+ }
762
+ // -------------------------
763
+ // 4) POST RESULTS
764
+ // -------------------------
765
+ async function postAgentTaskResult(accessKey, result, profileOrGatewayUrl = exports.DEFAULT_LOCAL_GATEWAY) {
766
+ const profile = typeof profileOrGatewayUrl === "string"
767
+ ? {
768
+ endpoint: profileOrGatewayUrl,
769
+ auth_header: "X-OneKey",
770
+ routes: {
771
+ get_tasks: "/api/v1/agent/tasks/get",
772
+ claim_task: "/api/v1/agent/tasks/claim",
773
+ post_result: "/api/v1/agent/tasks/post",
774
+ update_health: "/api/v1/agent/tasks/health/post",
775
+ update_log: "/api/v1/agent/tasks/log/post"
776
+ }
777
+ }
778
+ : profileOrGatewayUrl;
779
+ const url = `${toBaseUrl(profile.endpoint)}${profile.routes.post_result}`;
780
+ const payload = {
781
+ method: "POST",
782
+ headers: {
783
+ [profile.auth_header]: accessKey,
784
+ "Content-Type": "application/json"
785
+ },
786
+ body: JSON.stringify({
787
+ results: [
788
+ {
789
+ task_id: result.habit_id,
790
+ execution_id: result.execution_id,
791
+ root_execution_id: result.root_execution_id,
792
+ content: result.content,
793
+ images: result.images || [],
794
+ agent_id: result.agent_id,
795
+ status: result.status,
796
+ agent_name: result.agent_name
797
+ }
798
+ ]
799
+ })
800
+ };
801
+ if (DEBUG_ENABLE) {
802
+ console.log(`/post postAgentTaskResult url ${url} and payload ${JSON.stringify(payload)}`);
803
+ }
804
+ const res = await fetch(url, payload);
805
+ if (DEBUG_ENABLE) {
806
+ console.log(`/post postAgentTaskResult url ${url} and payload ${JSON.stringify(payload)}`);
807
+ }
808
+ return safeJson(res);
809
+ }
810
+ const claimedInProcess = new Set();
811
+ function taskKey(task) {
812
+ return `${task.habit_id}:${task.execution_date}`;
813
+ }
814
+ function taskPrompt(task) {
815
+ const raw = task.raw || {};
816
+ const taskName = raw.name ??
817
+ raw.title ??
818
+ raw.task ??
819
+ "Default Task";
820
+ const taskContent = raw.content ??
821
+ raw.note ??
822
+ "";
823
+ return [
824
+ `Task Name: ${taskName}`,
825
+ `Description: ${taskContent}`
826
+ ].join("\n");
827
+ }
828
+ async function processTask(task, ctx) {
829
+ const key = taskKey(task);
830
+ if (claimedInProcess.has(key))
831
+ return;
832
+ const cli = pickAvailableCli(ctx.profile);
833
+ if (!cli) {
834
+ console.warn(`[onekey gateway] no local agent CLI available for task ${task.habit_id}`);
835
+ return;
836
+ }
837
+ const agentId = makeAgentId(cli);
838
+ const agentName = cli;
839
+ const claim = await claimAgentTask(ctx.accessKey, {
840
+ agent_id: agentId,
841
+ agent_name: agentName,
842
+ habit_id: task.habit_id,
843
+ execution_date: task.execution_date
844
+ }, ctx.profile);
845
+ if (!claim?.success) {
846
+ var error = claim?.error;
847
+ console.log(`/claim new task is not successful! Error ${error}`);
848
+ return;
849
+ }
850
+ claimedInProcess.add(key);
851
+ console.log(`[onekey gateway] pull /claim Task ID: ${task.habit_id} agent=${agentName}`);
852
+ // Running Message 1: Status: Idle -> Running
853
+ await postAgentTaskResult(ctx.accessKey, {
854
+ habit_id: task.habit_id,
855
+ execution_id: claim.execution_id,
856
+ root_execution_id: claim.root_execution_id,
857
+ content: `Agent starting: ${agentName}`,
858
+ images: [],
859
+ agent_id: agentId,
860
+ status: "starting",
861
+ agent_name: agentName
862
+ }, ctx.profile);
863
+ console.log(`[onekey gateway] pull /post status=starting Task ID: ${task.habit_id}`);
864
+ const prompt = taskPrompt(task);
865
+ console.log(`[onekey gateway] pull /post status=running Task ID: ${task.habit_id} cli "${cli}" and prompt "${prompt}" ctx ${JSON.stringify(ctx)}...`);
866
+ const taskWorkspace = createTaskWorkspace(task.habit_id);
867
+ // const executionResult = await runLocalAgent(cli, prompt, ctx.timeoutMs, {cwd: WORKSPACE_ROOT, taskDir: taskWorkspace.root});
868
+ const executionResult = await runLocalAgent(cli, prompt, ctx.accessKey, ctx.profile, {
869
+ habit_id: task.habit_id,
870
+ execution_id: claim.execution_id,
871
+ root_execution_id: claim.root_execution_id,
872
+ agent_id: agentId,
873
+ agent_name: cli
874
+ }, ctx.timeoutMs, {
875
+ cwd: WORKSPACE_ROOT,
876
+ taskDir: taskWorkspace.root
877
+ });
878
+ console.log(`[onekey gateway] pull /post task status=completed Task ID: ${task.habit_id} executionResult "${JSON.stringify(executionResult)}"...`);
879
+ await postAgentTaskResult(ctx.accessKey, {
880
+ habit_id: task.habit_id,
881
+ execution_id: claim.execution_id,
882
+ root_execution_id: claim.root_execution_id,
883
+ content: executionResult.content,
884
+ images: [],
885
+ agent_id: agentId,
886
+ status: "completed",
887
+ agent_name: agentName
888
+ }, ctx.profile);
889
+ console.log(`[onekey gateway] pull /post status=completed Task ID: ${task.habit_id}`);
890
+ }
891
+ async function runWithConcurrency(items, limit, fn) {
892
+ const queue = [...items];
893
+ const workers = new Array(Math.max(1, limit)).fill(null).map(async () => {
894
+ while (queue.length) {
895
+ const item = queue.shift();
896
+ if (!item)
897
+ return;
898
+ await fn(item);
899
+ }
900
+ });
901
+ await Promise.all(workers);
902
+ }
903
+ // -------------------------
904
+ // 2.1 Pull mode worker
905
+ // -------------------------
906
+ async function startPullWorker(ctx, intervalMs = 10 * 60 * 1000) {
907
+ let running = false;
908
+ async function runBatch() {
909
+ if (running)
910
+ return;
911
+ running = true;
912
+ try {
913
+ if (!ctx.accessKey || !ctx.accessKey.trim()) {
914
+ throw new Error("Missing access key: set DEEPNLP_ONEKEY_ROUTER_ACCESS or pass --key");
915
+ }
916
+ console.log("[onekey gateway] pull /get tasks ...");
917
+ const data = await getAgentTasks(ctx.accessKey, ctx.profile);
918
+ // const connectedAgents: ConnectedAgent[] = Array.isArray(data?.connected_agents) ? data.connected_agents : [];
919
+ const connectedAgents = Array.isArray(data?.connected_agents)
920
+ ? data.connected_agents.map((a) => ({
921
+ id: a.id ?? "",
922
+ name: a.name ?? "",
923
+ cli: a.cli ?? "",
924
+ description: a.description ?? "",
925
+ }))
926
+ : [];
927
+ const tasks = (data?.tasks || []).map((t) => ({
928
+ habit_id: t.id,
929
+ execution_date: t.execution_date || today(),
930
+ raw: t
931
+ }));
932
+ // Get Agents
933
+ const localAgents = (ctx.profile.agents_clis || [])
934
+ .map(a => a.trim())
935
+ .filter(Boolean);
936
+ // 'connected_agents': [{'id': '', 'name': 'claude', 'description': 'Agent for claude', 'cli': 'claude'}, {'id': '', 'name': 'codex', 'description': 'Agent for codex', 'cli': 'codex'}, {'id': '', 'name': 'gemini', 'description': 'Agent for gemini', 'cli': 'gemini'}]}
937
+ const remoteAgents = connectedAgents.map(a => a.cli);
938
+ // fallback logic
939
+ const selectedAgents = remoteAgents.length > 0 ? remoteAgents : localAgents;
940
+ console.log(`[onekey gateway] Available agents |remote setting="${remoteAgents.join(",")}" | selected="${selectedAgents.join(",")}"`);
941
+ console.log(`[onekey gateway] pull /get Task IDs: ${tasks.map((t) => t.habit_id).join(",") || "(none)"}`);
942
+ await runWithConcurrency(tasks, ctx.concurrency, async (task) => processTask(task, ctx));
943
+ }
944
+ catch (error) {
945
+ console.error("[gateway] pull_worker_error", error);
946
+ }
947
+ finally {
948
+ running = false;
949
+ }
950
+ }
951
+ await runBatch();
952
+ for (;;) {
953
+ await sleep(intervalMs);
954
+ await runBatch();
955
+ }
956
+ }
957
+ // -------------------------
958
+ // 2.2 Callback mode server
959
+ // -------------------------
960
+ async function startCallbackServer(ctx, port) {
961
+ const server = http_1.default.createServer(async (req, res) => {
962
+ const url = req.url ? req.url.split("?", 1)[0] : "/";
963
+ if (req.method === "POST" && url === "/api/v1/agent/tasks/callback") {
964
+ try {
965
+ const headerName = (ctx.profile.auth_header || "X-OneKey").toLowerCase();
966
+ const headerValue = req.headers[headerName];
967
+ const accessKeyFromHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue;
968
+ const accessKey = String(accessKeyFromHeader || ctx.accessKey || "").trim();
969
+ if (!accessKey) {
970
+ return sendJson(res, 401, { success: false, error: "missing_access_key" });
971
+ }
972
+ const body = await parseJsonBody(req);
973
+ const rawTasks = Array.isArray(body?.tasks) ? body.tasks : [];
974
+ console.log(`[onekey gateway] AGENT EVENT PUSH callback received ${rawTasks.length} tasks`);
975
+ const tasks = rawTasks
976
+ .map((t) => ({
977
+ habit_id: t?.id || t?.habit_id,
978
+ execution_date: t?.execution_date || today(),
979
+ raw: t
980
+ }))
981
+ .filter((t) => Boolean(t?.habit_id));
982
+ const ctxWithKey = { ...ctx, accessKey };
983
+ await runWithConcurrency(tasks, ctx.concurrency, async (task) => processTask(task, ctxWithKey));
984
+ return sendJson(res, 200, { success: true });
985
+ }
986
+ catch (error) {
987
+ const message = error instanceof Error ? error.message : String(error);
988
+ return sendJson(res, 400, { success: false, error: message });
989
+ }
990
+ }
991
+ if (req.method === "GET" && url === "/health") {
992
+ return sendJson(res, 200, { ok: true });
993
+ }
994
+ sendJson(res, 404, { success: false, error: "not_found" });
995
+ });
996
+ await new Promise((resolve) => {
997
+ server.listen(port, "0.0.0.0", () => resolve());
998
+ });
999
+ console.log(`[onekey gateway] Agent Event Push Listen | Callback listening on http://127.0.0.1:${port}/api/v1/agent/tasks/callback`);
1000
+ }
1001
+ // -------------------------
1002
+ // Entry (CLI)
1003
+ // -------------------------
1004
+ async function runGateway(positionals, flags = {}) {
1005
+ const [uniqueId] = positionals;
1006
+ if (!uniqueId)
1007
+ throw new Error("gateway requires unique_id (e.g. coachowl/coachowl)");
1008
+ const modeFlag = typeof flags.mode === "string" ? flags.mode : "pull";
1009
+ const mode = modeFlag === "callback" ? "callback" : "pull";
1010
+ const accessKey = typeof flags.key === "string" ? flags.key : process.env.DEEPNLP_ONEKEY_ROUTER_ACCESS;
1011
+ if (mode === "pull" && (!accessKey || !accessKey.trim())) {
1012
+ throw new Error("Missing access key: set DEEPNLP_ONEKEY_ROUTER_ACCESS or pass --key");
1013
+ }
1014
+ // default to local tasks
1015
+ const envFlag = typeof flags.env === "string" ? flags.env : "production";
1016
+ const env = envFlag === "production" ? "production" : "local";
1017
+ const profile = resolveGatewayProfile(uniqueId, env);
1018
+ const timeoutMsRaw = flags.timeout;
1019
+ const timeoutMs = typeof timeoutMsRaw === "string" && Number.isFinite(Number(timeoutMsRaw)) ? Number(timeoutMsRaw) : 0;
1020
+ const concurrencyRaw = flags.concurrency;
1021
+ const concurrency = typeof concurrencyRaw === "string" && Number.isFinite(Number(concurrencyRaw)) ? Number(concurrencyRaw) : 1;
1022
+ const intervalSecondsRaw = flags.interval;
1023
+ const intervalMsRaw = flags["interval-ms"] ?? flags.intervalMs;
1024
+ const intervalMs = typeof intervalSecondsRaw === "string" && Number.isFinite(Number(intervalSecondsRaw))
1025
+ ? Number(intervalSecondsRaw) * 1000
1026
+ : typeof intervalMsRaw === "string" && Number.isFinite(Number(intervalMsRaw))
1027
+ ? Number(intervalMsRaw)
1028
+ : 10 * 60 * 1000;
1029
+ const portRaw = flags.port;
1030
+ const port = typeof portRaw === "string" && Number.isFinite(Number(portRaw)) ? Number(portRaw) : DEFAULT_CALLBACK_ENDPOINT_PORT;
1031
+ const ctx = {
1032
+ uniqueId,
1033
+ accessKey,
1034
+ profile,
1035
+ timeoutMs,
1036
+ concurrency
1037
+ };
1038
+ console.log(`[onekey gateway] Agent Pull Event|unique_id=${uniqueId} env=${env} mode=${mode}`);
1039
+ console.log(`[onekey gateway] Agent Pull Event|endpoint=${profile.endpoint}`);
1040
+ console.log(`[onekey gateway] Agent Pull Event|agents_clis=${(profile.agents_clis || []).join(",")}`);
1041
+ if (mode === "callback") {
1042
+ await startCallbackServer(ctx, port);
1043
+ // keep process alive
1044
+ for (;;) {
1045
+ await sleep(60000);
1046
+ }
1047
+ }
1048
+ await startPullWorker(ctx, intervalMs);
1049
+ }