@aiagenta2z/onekey-gateway 0.1.4 → 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.
- package/README.md +48 -22
- package/dist/cli.js +161 -1
- package/dist/gateway.d.ts +97 -0
- package/dist/gateway.js +1049 -0
- package/dist/monitor.d.ts +9 -0
- package/dist/monitor.js +112 -0
- package/package.json +8 -3
package/dist/gateway.js
ADDED
|
@@ -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
|
+
}
|