@lawrenceliang-btc/atel-sdk 1.1.42 → 1.1.43
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/bin/atel.mjs +1223 -416
- package/package.json +1 -1
- package/skill/SKILL.md +624 -0
- package/skill/atel-agent/SKILL.md +61 -36
- package/skill/atel-agent/setup.sh +62 -29
- package/skill/references/executor.md +13 -13
- package/skill/references/quickstart.md +7 -16
- package/skill/references/workflows.md +9 -20
package/bin/atel.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* atel start [port] Start endpoint (auto network + auto register)
|
|
10
10
|
* atel setup [port] Network setup only (detect IP, UPnP, verify)
|
|
11
11
|
* atel verify Verify port reachability
|
|
12
|
-
* atel inbox [count] Show received messages
|
|
12
|
+
* atel inbox [count] Show received messages / notifications
|
|
13
13
|
* atel register [name] [caps] [url] Register on public registry
|
|
14
14
|
* atel search <capability> Search registry for agents
|
|
15
15
|
* atel handshake <endpoint> [did] Handshake with a remote agent
|
|
@@ -79,6 +79,7 @@ const REGISTRY_URL = process.env.ATEL_REGISTRY || ATEL_PLATFORM || 'https://api.
|
|
|
79
79
|
const ATEL_RELAY = process.env.ATEL_RELAY || 'https://api.atelai.org';
|
|
80
80
|
const ATEL_NOTIFY_GATEWAY = process.env.ATEL_NOTIFY_GATEWAY || process.env.OPENCLAW_GATEWAY_URL || '';
|
|
81
81
|
const ATEL_NOTIFY_TARGET = process.env.ATEL_NOTIFY_TARGET || '';
|
|
82
|
+
const ATEL_NOTIFY_AUTO_BIND = /^(1|true|yes)$/i.test(String(process.env.ATEL_NOTIFY_AUTO_BIND || ''));
|
|
82
83
|
let EXECUTOR_URL = process.env.ATEL_EXECUTOR_URL || '';
|
|
83
84
|
const INBOX_FILE = resolve(ATEL_DIR, 'inbox.jsonl');
|
|
84
85
|
const POLICY_FILE = resolve(ATEL_DIR, 'policy.json');
|
|
@@ -90,6 +91,9 @@ const TRACES_DIR = resolve(ATEL_DIR, 'traces');
|
|
|
90
91
|
const PENDING_FILE = resolve(ATEL_DIR, 'pending-tasks.json');
|
|
91
92
|
const RESULT_PUSH_QUEUE_FILE = resolve(ATEL_DIR, 'pending-result-pushes.json');
|
|
92
93
|
const NOTIFY_TARGETS_FILE = resolve(ATEL_DIR, 'notify-targets.json');
|
|
94
|
+
const NOTIFY_ROUTES_FILE = resolve(ATEL_DIR, 'notify-routes.json');
|
|
95
|
+
const TELEGRAM_UPDATES_STATE_FILE = resolve(process.env.HOME || '/root', '.openclaw', 'deploy', 'sdk-telegram-updates.json');
|
|
96
|
+
const TELEGRAM_BINDINGS_FILE = resolve(process.env.HOME || '/root', '.openclaw', 'deploy', 'sdk-telegram-bindings.json');
|
|
93
97
|
const TRADE_TRACK_FILE = resolve(ATEL_DIR, 'tracked-orders.json');
|
|
94
98
|
const P2P_STATUS_FILE = resolve(ATEL_DIR, 'p2p-task-status.jsonl');
|
|
95
99
|
const PENDING_AGENT_CALLBACKS_FILE = resolve(ATEL_DIR, 'pending-agent-callbacks.json');
|
|
@@ -222,6 +226,96 @@ function saveNotifyTargets(data) {
|
|
|
222
226
|
writeFileSync(NOTIFY_TARGETS_FILE, JSON.stringify(data, null, 2));
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
function loadNotifyRoutes() {
|
|
230
|
+
try { return JSON.parse(readFileSync(NOTIFY_ROUTES_FILE, 'utf-8')); }
|
|
231
|
+
catch { return { version: 1, defaultTelegram: null, orderBindings: {} }; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function saveNotifyRoutes(data) {
|
|
235
|
+
ensureDir();
|
|
236
|
+
writeFileSync(NOTIFY_ROUTES_FILE, JSON.stringify(data, null, 2));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getPrimaryTelegramTarget() {
|
|
240
|
+
const targets = loadNotifyTargets();
|
|
241
|
+
return (targets.targets || []).find((target) => target && target.enabled !== false && target.channel === 'telegram' && target.target) || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function rememberTelegramRoute(chatId, botToken, orderId = '') {
|
|
245
|
+
const normalizedChatId = String(chatId || '').trim();
|
|
246
|
+
if (!normalizedChatId) return false;
|
|
247
|
+
const routes = loadNotifyRoutes();
|
|
248
|
+
const updatedAt = new Date().toISOString();
|
|
249
|
+
const nextDefault = { chatId: normalizedChatId, updatedAt };
|
|
250
|
+
if (botToken) nextDefault.botToken = botToken;
|
|
251
|
+
routes.defaultTelegram = nextDefault;
|
|
252
|
+
if (!routes.orderBindings || typeof routes.orderBindings !== 'object') routes.orderBindings = {};
|
|
253
|
+
if (orderId) {
|
|
254
|
+
routes.orderBindings[orderId] = { chatId: normalizedChatId, updatedAt };
|
|
255
|
+
if (botToken) routes.orderBindings[orderId].botToken = botToken;
|
|
256
|
+
}
|
|
257
|
+
saveNotifyRoutes(routes);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function rememberCurrentTelegramRoute(orderId = '') {
|
|
262
|
+
if (!String(process.env.OPENCLAW_SHELL || '').trim()) return false;
|
|
263
|
+
let chatId = discoverTelegramChat();
|
|
264
|
+
let botToken = discoverTelegramBot();
|
|
265
|
+
if (!chatId) {
|
|
266
|
+
const primary = getPrimaryTelegramTarget();
|
|
267
|
+
if (primary?.target) {
|
|
268
|
+
chatId = primary.target;
|
|
269
|
+
botToken = botToken || primary.botToken;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!chatId) return false;
|
|
273
|
+
return rememberTelegramRoute(chatId, botToken, orderId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function dropTelegramRoute(orderId = '') {
|
|
277
|
+
if (!orderId) return false;
|
|
278
|
+
const routes = loadNotifyRoutes();
|
|
279
|
+
if (!routes.orderBindings || !routes.orderBindings[orderId]) return false;
|
|
280
|
+
delete routes.orderBindings[orderId];
|
|
281
|
+
saveNotifyRoutes(routes);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function resolveNotifyTargets(orderId = '') {
|
|
286
|
+
const targets = loadNotifyTargets();
|
|
287
|
+
let enabled = (targets.targets || []).filter(t => t.enabled !== false);
|
|
288
|
+
const routes = loadNotifyRoutes();
|
|
289
|
+
const primary = getPrimaryTelegramTarget();
|
|
290
|
+
const preferred = (orderId && routes.orderBindings && routes.orderBindings[orderId]) || routes.defaultTelegram || (primary ? { chatId: String(primary.target), botToken: primary.botToken, updatedAt: primary.lastUsedAt || primary.createdAt || new Date().toISOString() } : null);
|
|
291
|
+
if (!preferred?.chatId) return { targets, enabled };
|
|
292
|
+
let hasTelegram = false;
|
|
293
|
+
enabled = enabled.map((target) => {
|
|
294
|
+
if (target.channel !== 'telegram') return target;
|
|
295
|
+
hasTelegram = true;
|
|
296
|
+
return {
|
|
297
|
+
...target,
|
|
298
|
+
target: String(preferred.chatId),
|
|
299
|
+
botToken: preferred.botToken || target.botToken,
|
|
300
|
+
routeSource: orderId && routes.orderBindings && routes.orderBindings[orderId] ? 'order' : 'default',
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
if (!hasTelegram) {
|
|
304
|
+
enabled = [{
|
|
305
|
+
id: `tg_${preferred.chatId}`,
|
|
306
|
+
channel: 'telegram',
|
|
307
|
+
target: String(preferred.chatId),
|
|
308
|
+
botToken: preferred.botToken || discoverTelegramBot() || undefined,
|
|
309
|
+
label: orderId ? `order:${orderId}` : 'owner',
|
|
310
|
+
enabled: true,
|
|
311
|
+
createdAt: preferred.updatedAt || new Date().toISOString(),
|
|
312
|
+
lastUsedAt: null,
|
|
313
|
+
routeSource: orderId && routes.orderBindings && routes.orderBindings[orderId] ? 'order' : 'default',
|
|
314
|
+
}, ...enabled];
|
|
315
|
+
}
|
|
316
|
+
return { targets, enabled };
|
|
317
|
+
}
|
|
318
|
+
|
|
225
319
|
// Auto-detect current TG chat from OpenClaw session state
|
|
226
320
|
function discoverTelegramChat() {
|
|
227
321
|
const sessionPaths = [
|
|
@@ -252,10 +346,11 @@ function autoBindNotifications() {
|
|
|
252
346
|
const targets = loadNotifyTargets();
|
|
253
347
|
if (targets.targets.length > 0) return; // already has targets
|
|
254
348
|
|
|
255
|
-
const
|
|
349
|
+
const routes = loadNotifyRoutes();
|
|
350
|
+
const chatId = routes.defaultTelegram?.chatId || discoverTelegramChat();
|
|
256
351
|
if (!chatId) return;
|
|
257
352
|
|
|
258
|
-
const botToken = discoverTelegramBot();
|
|
353
|
+
const botToken = routes.defaultTelegram?.botToken || discoverTelegramBot();
|
|
259
354
|
const id = `tg_${chatId}`;
|
|
260
355
|
targets.targets.push({
|
|
261
356
|
id, channel: 'telegram', target: chatId,
|
|
@@ -264,6 +359,7 @@ function autoBindNotifications() {
|
|
|
264
359
|
createdAt: new Date().toISOString(), lastUsedAt: null,
|
|
265
360
|
});
|
|
266
361
|
saveNotifyTargets(targets);
|
|
362
|
+
rememberTelegramRoute(chatId, botToken || undefined);
|
|
267
363
|
console.log(`🔔 Auto-bound TG notifications to chat ${chatId}`);
|
|
268
364
|
}
|
|
269
365
|
|
|
@@ -305,80 +401,324 @@ function discoverTelegramBot() {
|
|
|
305
401
|
// Also check env
|
|
306
402
|
}
|
|
307
403
|
|
|
404
|
+
function loadTelegramUpdatesState() {
|
|
405
|
+
try { return JSON.parse(readFileSync(TELEGRAM_UPDATES_STATE_FILE, 'utf-8')); }
|
|
406
|
+
catch { return { version: 1, cursors: {}, leaders: {} }; }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function saveTelegramUpdatesState(data) {
|
|
410
|
+
ensureDir();
|
|
411
|
+
writeFileSync(TELEGRAM_UPDATES_STATE_FILE, JSON.stringify(data, null, 2));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function loadTelegramBindings() {
|
|
415
|
+
try { return JSON.parse(readFileSync(TELEGRAM_BINDINGS_FILE, 'utf-8')); }
|
|
416
|
+
catch { return { version: 1, bindings: {} }; }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function saveTelegramBindings(data) {
|
|
420
|
+
mkdirSync(dirname(TELEGRAM_BINDINGS_FILE), { recursive: true });
|
|
421
|
+
writeFileSync(TELEGRAM_BINDINGS_FILE, JSON.stringify(data, null, 2));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function registerTelegramInboundBinding(did, config) {
|
|
425
|
+
if (!did || !config?.cursorKey || !config?.chatId) return { ownerDid: '', rebound: false };
|
|
426
|
+
const data = loadTelegramBindings();
|
|
427
|
+
if (!data.bindings || typeof data.bindings !== 'object') data.bindings = {};
|
|
428
|
+
const key = config.cursorKey;
|
|
429
|
+
const current = data.bindings[key];
|
|
430
|
+
const ownerDid = String(current?.ownerDid || '').trim();
|
|
431
|
+
if (ownerDid && ownerDid !== did) {
|
|
432
|
+
return { ownerDid, rebound: false };
|
|
433
|
+
}
|
|
434
|
+
data.bindings[key] = {
|
|
435
|
+
chatId: config.chatId,
|
|
436
|
+
botId: config.botId,
|
|
437
|
+
ownerDid: did,
|
|
438
|
+
updatedAt: new Date().toISOString(),
|
|
439
|
+
};
|
|
440
|
+
saveTelegramBindings(data);
|
|
441
|
+
return { ownerDid: did, rebound: true };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function acquireTelegramInboundLeader(did, config) {
|
|
445
|
+
const state = loadTelegramUpdatesState();
|
|
446
|
+
if (!state.leaders || typeof state.leaders !== 'object') state.leaders = {};
|
|
447
|
+
const now = Date.now();
|
|
448
|
+
const leaseMs = 15000;
|
|
449
|
+
const current = state.leaders[config.cursorKey];
|
|
450
|
+
if (current && current.did && current.did !== did && Number(current.expiresAt || 0) > now) {
|
|
451
|
+
return { leader: current.did, acquired: false };
|
|
452
|
+
}
|
|
453
|
+
state.leaders[config.cursorKey] = { did, expiresAt: now + leaseMs, updatedAt: new Date().toISOString() };
|
|
454
|
+
saveTelegramUpdatesState(state);
|
|
455
|
+
return { leader: did, acquired: true };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function getTelegramInboundConfig() {
|
|
459
|
+
const routes = loadNotifyRoutes();
|
|
460
|
+
const primary = getPrimaryTelegramTarget();
|
|
461
|
+
const chatId = String(routes.defaultTelegram?.chatId || primary?.target || '').trim();
|
|
462
|
+
const botToken = String(routes.defaultTelegram?.botToken || primary?.botToken || discoverTelegramBot() || '').trim();
|
|
463
|
+
if (!chatId || !botToken) return null;
|
|
464
|
+
const botId = botToken.split(':', 1)[0] || 'telegram';
|
|
465
|
+
return { chatId, botToken, botId, cursorKey: `${botId}:${chatId}` };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function extractTelegramInboundPayload(update) {
|
|
469
|
+
const msg = update?.message || update?.edited_message;
|
|
470
|
+
if (!msg || msg?.from?.is_bot) return null;
|
|
471
|
+
const chatId = String(msg?.chat?.id || '').trim();
|
|
472
|
+
const text = typeof msg?.text === 'string' && msg.text.trim()
|
|
473
|
+
? msg.text.trim()
|
|
474
|
+
: (typeof msg?.caption === 'string' && msg.caption.trim() ? msg.caption.trim() : '');
|
|
475
|
+
if (!chatId || !text) return null;
|
|
476
|
+
const nameParts = [];
|
|
477
|
+
if (typeof msg?.from?.username === 'string' && msg.from.username.trim()) nameParts.push(`@${msg.from.username.trim()}`);
|
|
478
|
+
if (typeof msg?.from?.first_name === 'string' && msg.from.first_name.trim()) nameParts.push(msg.from.first_name.trim());
|
|
479
|
+
if (typeof msg?.from?.last_name === 'string' && msg.from.last_name.trim()) nameParts.push(msg.from.last_name.trim());
|
|
480
|
+
const unixTs = Number(msg?.date || update?.edited_message?.date || 0);
|
|
481
|
+
const occurredAt = unixTs > 0 ? new Date(unixTs * 1000).toISOString() : new Date().toISOString();
|
|
482
|
+
return {
|
|
483
|
+
chatId,
|
|
484
|
+
text,
|
|
485
|
+
occurredAt,
|
|
486
|
+
messageId: msg?.message_id ?? null,
|
|
487
|
+
senderName: nameParts.join(' ').trim(),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function queueTelegramInboundMirror(targetDid, config, update) {
|
|
492
|
+
const payload = extractTelegramInboundPayload(update);
|
|
493
|
+
if (!payload || payload.chatId !== config.chatId) return false;
|
|
494
|
+
const sender = `telegram:${payload.chatId}`;
|
|
495
|
+
const relayMessage = {
|
|
496
|
+
method: 'POST',
|
|
497
|
+
path: '/atel/v1/relay-message',
|
|
498
|
+
body: {
|
|
499
|
+
kind: 'telegram_message',
|
|
500
|
+
channel: 'telegram',
|
|
501
|
+
text: payload.text,
|
|
502
|
+
senderDid: sender,
|
|
503
|
+
timestamp: payload.occurredAt,
|
|
504
|
+
telegramChatId: payload.chatId,
|
|
505
|
+
telegramUpdateId: update?.update_id ?? null,
|
|
506
|
+
telegramMessageId: payload.messageId,
|
|
507
|
+
telegramSender: payload.senderName || '',
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
const resp = await fetch(`${ATEL_PLATFORM}/relay/v1/respond/${encodeURIComponent(targetDid)}`, {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
headers: { 'Content-Type': 'application/json' },
|
|
513
|
+
body: JSON.stringify({ sender, message: relayMessage }),
|
|
514
|
+
signal: AbortSignal.timeout(10000),
|
|
515
|
+
});
|
|
516
|
+
const raw = await resp.text().catch(() => '');
|
|
517
|
+
if (!resp.ok) throw new Error(`telegram_mirror_http_${resp.status}:${raw || 'unknown'}`);
|
|
518
|
+
log({ event: 'telegram_inbound_mirrored', did: targetDid, chatId: payload.chatId, updateId: update?.update_id ?? null, text: payload.text.slice(0, 120) });
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function mirrorTelegramInboundToBindings(config, update) {
|
|
523
|
+
const bindings = loadTelegramBindings();
|
|
524
|
+
const ownerDid = String(bindings.bindings?.[config.cursorKey]?.ownerDid || '').trim();
|
|
525
|
+
if (!ownerDid) return 0;
|
|
526
|
+
try {
|
|
527
|
+
return (await queueTelegramInboundMirror(ownerDid, config, update)) ? 1 : 0;
|
|
528
|
+
} catch (e) {
|
|
529
|
+
log({ event: 'telegram_inbound_mirror_error', did: ownerDid, chatId: config.chatId, updateId: update?.update_id ?? null, error: e.message || 'unknown_error' });
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function pollTelegramInboundUpdates(targetDid) {
|
|
535
|
+
const config = getTelegramInboundConfig();
|
|
536
|
+
if (!config) return { status: 'noop' };
|
|
537
|
+
|
|
538
|
+
const binding = registerTelegramInboundBinding(targetDid, config);
|
|
539
|
+
if (binding.ownerDid && binding.ownerDid !== targetDid) {
|
|
540
|
+
return { status: 'follower', ownerDid: binding.ownerDid };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const leadership = acquireTelegramInboundLeader(targetDid, config);
|
|
544
|
+
if (!leadership.acquired) {
|
|
545
|
+
return { status: 'follower', leader: leadership.leader };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const state = loadTelegramUpdatesState();
|
|
549
|
+
if (!state.cursors || typeof state.cursors !== 'object') state.cursors = {};
|
|
550
|
+
const cursor = state.cursors[config.cursorKey] || {};
|
|
551
|
+
const nextUpdateId = Number(cursor.nextUpdateId || 0);
|
|
552
|
+
const params = new URLSearchParams({ timeout: '0', limit: '20' });
|
|
553
|
+
if (nextUpdateId > 0) params.set('offset', String(nextUpdateId));
|
|
554
|
+
|
|
555
|
+
const resp = await fetch(`https://api.telegram.org/bot${config.botToken}/getUpdates?${params.toString()}`, {
|
|
556
|
+
signal: AbortSignal.timeout(12000),
|
|
557
|
+
});
|
|
558
|
+
const data = await resp.json().catch(() => null);
|
|
559
|
+
if (!resp.ok || data?.ok === false) {
|
|
560
|
+
throw new Error(data?.description || `telegram_getUpdates_http_${resp.status}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const updates = Array.isArray(data?.result) ? data.result : [];
|
|
564
|
+
if (updates.length === 0) return { status: 'idle' };
|
|
565
|
+
|
|
566
|
+
let nextCursor = nextUpdateId;
|
|
567
|
+
for (const update of updates) {
|
|
568
|
+
const updateId = Number(update?.update_id || 0);
|
|
569
|
+
if (updateId > 0) nextCursor = Math.max(nextCursor, updateId + 1);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let effectiveUpdates = updates;
|
|
573
|
+
if (nextUpdateId === 0) {
|
|
574
|
+
effectiveUpdates = updates.slice(-1);
|
|
575
|
+
state.cursors[config.cursorKey] = {
|
|
576
|
+
nextUpdateId: nextCursor,
|
|
577
|
+
chatId: config.chatId,
|
|
578
|
+
botId: config.botId,
|
|
579
|
+
updatedAt: new Date().toISOString(),
|
|
580
|
+
};
|
|
581
|
+
saveTelegramUpdatesState(state);
|
|
582
|
+
log({ event: 'telegram_inbound_bootstrap', did: targetDid, chatId: config.chatId, skipped: Math.max(0, updates.length - effectiveUpdates.length), mirroredOnBootstrap: effectiveUpdates.length, nextUpdateId: nextCursor });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let mirrored = 0;
|
|
586
|
+
for (const update of effectiveUpdates) {
|
|
587
|
+
mirrored += await mirrorTelegramInboundToBindings(config, update);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
state.cursors[config.cursorKey] = {
|
|
591
|
+
nextUpdateId: nextCursor,
|
|
592
|
+
chatId: config.chatId,
|
|
593
|
+
botId: config.botId,
|
|
594
|
+
updatedAt: new Date().toISOString(),
|
|
595
|
+
};
|
|
596
|
+
saveTelegramUpdatesState(state);
|
|
597
|
+
return { status: 'ok', mirrored, nextUpdateId: nextCursor };
|
|
598
|
+
}
|
|
599
|
+
|
|
308
600
|
// Send notification to all enabled targets (fire-and-forget, never blocks)
|
|
309
601
|
async function pushTradeNotification(eventType, payload, body) {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
602
|
+
const orderId = payload?.orderId || body?.orderId || '';
|
|
603
|
+
const { targets, enabled } = resolveNotifyTargets(orderId);
|
|
312
604
|
if (enabled.length === 0) return;
|
|
313
605
|
|
|
314
|
-
const chainLabel = (p) => {
|
|
315
|
-
const c = p?.chain || body?.chain || '';
|
|
316
|
-
if (c === 'fast-coop') return ' (Fast)';
|
|
317
|
-
if (c === 'bsc') return ' (BSC)';
|
|
318
|
-
return '';
|
|
319
|
-
};
|
|
320
606
|
const templates = {
|
|
321
|
-
'order_created': (p) => `📥
|
|
322
|
-
|
|
607
|
+
'order_created': (p) => `📥 收到新订单
|
|
608
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
609
|
+
金额: $${p.priceAmount ?? '?'} USDC
|
|
610
|
+
来自: ${p.requesterDid || '未知请求方'}
|
|
611
|
+
请审核后决定是否接单`,
|
|
612
|
+
'order_accepted': (p) => `📋 订单已被接单
|
|
613
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
614
|
+
执行方已开始处理,进入里程碑阶段`,
|
|
615
|
+
'escrow_confirmed': (p) => `🔒 托管已确认
|
|
616
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
617
|
+
链上托管已完成,等待里程碑方案确认`,
|
|
618
|
+
'milestone_plan_confirmed': (p) => {
|
|
619
|
+
const desc = p.milestoneDescription || p.nextMilestoneDescription || '';
|
|
620
|
+
const progress = p.totalMilestones ? `
|
|
621
|
+
进度: 1/${p.totalMilestones}` : '';
|
|
622
|
+
return `🚀 里程碑方案已确认
|
|
623
|
+
订单: ${p.orderId || body?.orderId || '?'}${desc ? `
|
|
624
|
+
当前里程碑: ${desc}` : ''}${progress}`;
|
|
625
|
+
},
|
|
323
626
|
'milestone_submitted': (p) => {
|
|
324
|
-
const desc = p.milestoneDescription ?
|
|
325
|
-
|
|
326
|
-
|
|
627
|
+
const desc = p.milestoneDescription ? `
|
|
628
|
+
目标: ${p.milestoneDescription}` : '';
|
|
629
|
+
const content = p.resultSummary ? `
|
|
630
|
+
提交内容: ${String(p.resultSummary).substring(0, 200)}` : '';
|
|
631
|
+
return `📝 里程碑 M${p.milestoneIndex ?? '?'} 已提交
|
|
632
|
+
订单: ${p.orderId || body?.orderId || '?'}${desc}${content}
|
|
633
|
+
等待审核`;
|
|
327
634
|
},
|
|
328
635
|
'milestone_verified': (p) => {
|
|
329
|
-
const desc = p.milestoneDescription ?
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
636
|
+
const desc = p.milestoneDescription ? `
|
|
637
|
+
目标: ${p.milestoneDescription}` : '';
|
|
638
|
+
const content = p.resultSummary ? `
|
|
639
|
+
提交内容: ${String(p.resultSummary).substring(0, 200)}` : '';
|
|
640
|
+
const progress = p.totalMilestones ? `
|
|
641
|
+
进度: ${(p.milestoneIndex ?? 0) + 1}/${p.totalMilestones}` : '';
|
|
642
|
+
return `✅ 里程碑 M${p.milestoneIndex ?? '?'} 审核通过
|
|
643
|
+
订单: ${p.orderId || body?.orderId || '?'}${desc}${content}${progress}`;
|
|
333
644
|
},
|
|
334
645
|
'milestone_rejected': (p) => {
|
|
335
|
-
const desc = p.milestoneDescription ?
|
|
336
|
-
|
|
646
|
+
const desc = p.milestoneDescription ? `
|
|
647
|
+
目标: ${p.milestoneDescription}` : '';
|
|
648
|
+
return `❌ 里程碑 M${p.milestoneIndex ?? '?'} 被拒绝
|
|
649
|
+
订单: ${p.orderId || body?.orderId || '?'}${desc}
|
|
650
|
+
原因: ${p.rejectReason || '未说明'}`;
|
|
337
651
|
},
|
|
652
|
+
'order_completed': (p) => `📦 订单已提交完成
|
|
653
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
654
|
+
等待对方确认或系统自动确认`,
|
|
655
|
+
'order_cancelled': (p) => `🛑 订单已取消
|
|
656
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
657
|
+
原因: ${p.reason || '未说明'}`,
|
|
658
|
+
'order_expired': (p) => `⌛ 订单已过期
|
|
659
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
660
|
+
系统已自动结束该订单`,
|
|
661
|
+
'dispute_created': (p) => `⚖️ 争议已创建
|
|
662
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
663
|
+
请尽快补充证据`,
|
|
664
|
+
'dispute_resolved': (p) => `🧾 争议已处理完成
|
|
665
|
+
订单: ${p.orderId || body?.orderId || '?'}
|
|
666
|
+
结果: ${p.result || p.resolution || '已裁定'}`,
|
|
338
667
|
'order_settled': (p) => {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
668
|
+
const amount = p.priceAmount ? `
|
|
669
|
+
金额: $${p.priceAmount} USDC` : '';
|
|
670
|
+
return `💰 订单已结算完成
|
|
671
|
+
订单: ${p.orderId || body?.orderId || '?'}${amount}
|
|
672
|
+
USDC 已支付`;
|
|
343
673
|
},
|
|
344
674
|
};
|
|
345
675
|
const tmpl = templates[eventType];
|
|
346
|
-
|
|
347
|
-
const message = tmpl(payload)
|
|
676
|
+
const orderIdText = orderId || '?';
|
|
677
|
+
const message = tmpl ? tmpl(payload) : `🔔 平台事件更新
|
|
678
|
+
事件: ${eventType}
|
|
679
|
+
订单: ${orderIdText}`;
|
|
348
680
|
|
|
349
681
|
for (const target of enabled) {
|
|
350
682
|
try {
|
|
351
|
-
if (target.channel === 'telegram'
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
await fetch(`https://api.telegram.org/bot${target.botToken}/sendMessage`, {
|
|
683
|
+
if (target.channel === 'telegram') {
|
|
684
|
+
if (!target.botToken) {
|
|
685
|
+
log({ event: 'trade_notify_target_skipped', eventType, channel: 'telegram', target: target.id || target.target, reason: 'missing_bot_token' });
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const resp = await fetch(`https://api.telegram.org/bot${target.botToken}/sendMessage`, {
|
|
358
689
|
method: 'POST',
|
|
359
690
|
headers: { 'Content-Type': 'application/json' },
|
|
360
691
|
body: JSON.stringify({ chat_id: target.target, text: message }),
|
|
361
692
|
signal: AbortSignal.timeout(5000),
|
|
362
|
-
})
|
|
693
|
+
});
|
|
694
|
+
const data = await resp.json().catch(() => null);
|
|
695
|
+
if (!resp.ok || data?.ok === false) {
|
|
696
|
+
log({ event: 'trade_notify_delivery_error', eventType, channel: 'telegram', target: target.id || target.target, status: resp.status, error: data?.description || `http_${resp.status}` });
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
363
699
|
} else if (target.channel === 'gateway') {
|
|
364
|
-
// OpenClaw gateway
|
|
365
700
|
const gw = discoverGateway();
|
|
366
701
|
if (gw?.url && gw?.token) {
|
|
367
|
-
await fetch(`${gw.url}/tools/invoke`, {
|
|
702
|
+
const resp = await fetch(`${gw.url}/tools/invoke`, {
|
|
368
703
|
method: 'POST',
|
|
369
704
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
|
|
370
705
|
body: JSON.stringify({ tool: 'message', args: { action: 'send', message, target: target.target } }),
|
|
371
706
|
signal: AbortSignal.timeout(5000),
|
|
372
|
-
})
|
|
707
|
+
});
|
|
708
|
+
if (!resp.ok) {
|
|
709
|
+
log({ event: 'trade_notify_delivery_error', eventType, channel: 'gateway', target: target.id || target.target, status: resp.status, error: `http_${resp.status}` });
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
log({ event: 'trade_notify_target_skipped', eventType, channel: 'gateway', target: target.id || target.target, reason: 'missing_gateway' });
|
|
714
|
+
continue;
|
|
373
715
|
}
|
|
374
716
|
}
|
|
375
|
-
// Update lastUsedAt
|
|
376
717
|
target.lastUsedAt = new Date().toISOString();
|
|
377
718
|
} catch (e) {
|
|
378
|
-
|
|
719
|
+
log({ event: 'trade_notify_delivery_error', eventType, channel: target.channel, target: target.id || target.target, error: e.message || 'unknown_error' });
|
|
379
720
|
}
|
|
380
721
|
}
|
|
381
|
-
// Best-effort save lastUsedAt
|
|
382
722
|
try { saveNotifyTargets(targets); } catch {}
|
|
383
723
|
}
|
|
384
724
|
|
|
@@ -389,17 +729,34 @@ function appendP2PTaskStatus(entry) {
|
|
|
389
729
|
}
|
|
390
730
|
|
|
391
731
|
async function pushP2PNotification(eventType, payload = {}) {
|
|
392
|
-
const targets =
|
|
393
|
-
const enabled = (targets.targets || []).filter(t => t.enabled !== false);
|
|
732
|
+
const { targets, enabled } = resolveNotifyTargets('');
|
|
394
733
|
if (enabled.length === 0) return;
|
|
395
734
|
|
|
396
735
|
const templates = {
|
|
397
|
-
'p2p_task_sent': (p) => `📤 P2P
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
'
|
|
401
|
-
|
|
402
|
-
|
|
736
|
+
'p2p_task_sent': (p) => `📤 P2P任务已发送
|
|
737
|
+
任务: ${p.taskId || '?'}
|
|
738
|
+
目标: ${p.peerDid || '?'}`,
|
|
739
|
+
'p2p_task_received': (p) => `📩 收到新的P2P任务
|
|
740
|
+
任务: ${p.taskId || '?'}
|
|
741
|
+
来自: ${p.peerDid || '?'}`,
|
|
742
|
+
'p2p_task_started': (p) => `▶️ P2P任务开始处理
|
|
743
|
+
任务: ${p.taskId || '?'}
|
|
744
|
+
来自: ${p.peerDid || '?'}`,
|
|
745
|
+
'p2p_result_submitted': (p) => `📨 P2P结果已发回对方
|
|
746
|
+
任务: ${p.taskId || '?'}
|
|
747
|
+
目标: ${p.peerDid || '?'}`,
|
|
748
|
+
'p2p_result_received': (p) => `✅ P2P任务已完成
|
|
749
|
+
任务: ${p.taskId || '?'}
|
|
750
|
+
结果: ${String(p.result || '').slice(0, 80) || '已返回'}`,
|
|
751
|
+
'p2p_task_failed': (p) => `❌ P2P任务失败
|
|
752
|
+
任务: ${p.taskId || '?'}
|
|
753
|
+
原因: ${p.reason || '未知错误'}`,
|
|
754
|
+
'p2p_message_received': (p) => `💬 收到新消息
|
|
755
|
+
来自: ${p.peerDid || '?'}
|
|
756
|
+
内容: ${String(p.text || '').slice(0, 120) || '[message]'}`,
|
|
757
|
+
'p2p_contact_added': (p) => `👤 联系人提醒
|
|
758
|
+
来自: ${p.peerDid || '?'}${p.alias ? `
|
|
759
|
+
备注: ${p.alias}` : ''}`,
|
|
403
760
|
};
|
|
404
761
|
const tmpl = templates[eventType];
|
|
405
762
|
if (!tmpl) return;
|
|
@@ -407,27 +764,44 @@ async function pushP2PNotification(eventType, payload = {}) {
|
|
|
407
764
|
|
|
408
765
|
for (const target of enabled) {
|
|
409
766
|
try {
|
|
410
|
-
if (target.channel === 'telegram'
|
|
411
|
-
|
|
412
|
-
|
|
767
|
+
if (target.channel === 'telegram') {
|
|
768
|
+
if (!target.botToken) {
|
|
769
|
+
log({ event: 'p2p_notify_target_skipped', eventType, channel: 'telegram', target: target.id || target.target, reason: 'missing_bot_token' });
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const resp = await fetch(`https://api.telegram.org/bot${target.botToken}/sendMessage`, {
|
|
413
773
|
method: 'POST',
|
|
414
774
|
headers: { 'Content-Type': 'application/json' },
|
|
415
775
|
body: JSON.stringify({ chat_id: target.target, text: message }),
|
|
416
776
|
signal: AbortSignal.timeout(5000),
|
|
417
|
-
})
|
|
777
|
+
});
|
|
778
|
+
const data = await resp.json().catch(() => null);
|
|
779
|
+
if (!resp.ok || data?.ok === false) {
|
|
780
|
+
log({ event: 'p2p_notify_delivery_error', eventType, channel: 'telegram', target: target.id || target.target, status: resp.status, error: data?.description || `http_${resp.status}` });
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
418
783
|
} else if (target.channel === 'gateway') {
|
|
419
784
|
const gw = discoverGateway();
|
|
420
785
|
if (gw?.url && gw?.token) {
|
|
421
|
-
await fetch(`${gw.url}/tools/invoke`, {
|
|
786
|
+
const resp = await fetch(`${gw.url}/tools/invoke`, {
|
|
422
787
|
method: 'POST',
|
|
423
788
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
|
|
424
789
|
body: JSON.stringify({ tool: 'message', args: { action: 'send', message, target: target.target } }),
|
|
425
790
|
signal: AbortSignal.timeout(5000),
|
|
426
|
-
})
|
|
791
|
+
});
|
|
792
|
+
if (!resp.ok) {
|
|
793
|
+
log({ event: 'p2p_notify_delivery_error', eventType, channel: 'gateway', target: target.id || target.target, status: resp.status, error: `http_${resp.status}` });
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
log({ event: 'p2p_notify_target_skipped', eventType, channel: 'gateway', target: target.id || target.target, reason: 'missing_gateway' });
|
|
798
|
+
continue;
|
|
427
799
|
}
|
|
428
800
|
}
|
|
429
801
|
target.lastUsedAt = new Date().toISOString();
|
|
430
|
-
} catch {
|
|
802
|
+
} catch (e) {
|
|
803
|
+
log({ event: 'p2p_notify_delivery_error', eventType, channel: target.channel, target: target.id || target.target, error: e.message || 'unknown_error' });
|
|
804
|
+
}
|
|
431
805
|
}
|
|
432
806
|
try { saveNotifyTargets(targets); } catch {}
|
|
433
807
|
}
|
|
@@ -1082,6 +1456,7 @@ function saveTrackedOrders(orders) {
|
|
|
1082
1456
|
}
|
|
1083
1457
|
function trackOrder(orderId, role) {
|
|
1084
1458
|
if (!orderId || !role) return;
|
|
1459
|
+
rememberCurrentTelegramRoute(orderId);
|
|
1085
1460
|
const now = new Date().toISOString();
|
|
1086
1461
|
const orders = loadTrackedOrders().filter((o) => o && o.orderId);
|
|
1087
1462
|
const existing = orders.find((o) => o.orderId === orderId);
|
|
@@ -1096,11 +1471,146 @@ function trackOrder(orderId, role) {
|
|
|
1096
1471
|
function untrackOrder(orderId) {
|
|
1097
1472
|
if (!orderId) return;
|
|
1098
1473
|
saveTrackedOrders(loadTrackedOrders().filter((o) => o?.orderId !== orderId));
|
|
1474
|
+
dropTelegramRoute(orderId);
|
|
1099
1475
|
}
|
|
1100
1476
|
function loadTasks() { if (!existsSync(TASKS_FILE)) return {}; try { return JSON.parse(readFileSync(TASKS_FILE, 'utf-8')); } catch { return {}; } }
|
|
1101
1477
|
function saveTasks(t) { ensureDir(); writeFileSync(TASKS_FILE, JSON.stringify(t, null, 2)); }
|
|
1102
1478
|
function loadNetwork() { if (!existsSync(NETWORK_FILE)) return null; try { return JSON.parse(readFileSync(NETWORK_FILE, 'utf-8')); } catch { return null; } }
|
|
1103
1479
|
function saveNetwork(n) { ensureDir(); writeFileSync(NETWORK_FILE, JSON.stringify(n, null, 2)); }
|
|
1480
|
+
function syncNetworkConfigToPort(networkConfig, port) {
|
|
1481
|
+
if (!networkConfig || typeof networkConfig !== 'object') return { config: networkConfig, changed: false };
|
|
1482
|
+
let changed = false;
|
|
1483
|
+
const next = { ...networkConfig };
|
|
1484
|
+
if (next.port !== port) { next.port = port; changed = true; }
|
|
1485
|
+
const rewriteUrlPort = (value) => {
|
|
1486
|
+
if (typeof value !== 'string' || !value.trim()) return value;
|
|
1487
|
+
try {
|
|
1488
|
+
const parsed = new URL(value);
|
|
1489
|
+
const desiredPort = String(port);
|
|
1490
|
+
if (parsed.port === desiredPort) return value;
|
|
1491
|
+
parsed.port = desiredPort;
|
|
1492
|
+
changed = true;
|
|
1493
|
+
return parsed.toString();
|
|
1494
|
+
} catch {
|
|
1495
|
+
return value;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
next.endpoint = rewriteUrlPort(next.endpoint);
|
|
1499
|
+
if (Array.isArray(next.candidates)) {
|
|
1500
|
+
next.candidates = next.candidates.map((candidate) => {
|
|
1501
|
+
if (!candidate || typeof candidate !== 'object') return candidate;
|
|
1502
|
+
const updated = { ...candidate };
|
|
1503
|
+
if (updated.type === 'relay') {
|
|
1504
|
+
if (typeof next.relayUrl === 'string' && next.relayUrl.trim() && updated.url !== next.relayUrl) {
|
|
1505
|
+
updated.url = next.relayUrl;
|
|
1506
|
+
changed = true;
|
|
1507
|
+
}
|
|
1508
|
+
if (updated.port && updated.port !== 18204) {
|
|
1509
|
+
updated.port = 18204;
|
|
1510
|
+
changed = true;
|
|
1511
|
+
}
|
|
1512
|
+
return updated;
|
|
1513
|
+
}
|
|
1514
|
+
if (updated.port !== port) { updated.port = port; changed = true; }
|
|
1515
|
+
updated.url = rewriteUrlPort(updated.url);
|
|
1516
|
+
return updated;
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
if (changed) next.configuredAt = new Date().toISOString();
|
|
1520
|
+
return { config: next, changed };
|
|
1521
|
+
}
|
|
1522
|
+
function isLegacyPassiveRelayMessage(req) {
|
|
1523
|
+
if (!req || typeof req !== 'object' || Array.isArray(req)) return false;
|
|
1524
|
+
const path = typeof req.path === 'string' ? req.path.trim() : '';
|
|
1525
|
+
const method = typeof req.method === 'string' ? req.method.trim().toUpperCase() : '';
|
|
1526
|
+
if (path || method) return false;
|
|
1527
|
+
const kind = typeof req.kind === 'string' ? req.kind.trim() : '';
|
|
1528
|
+
const text = typeof req.text === 'string' ? req.text.trim() : '';
|
|
1529
|
+
const msgType = typeof req.msgType === 'string' ? req.msgType.trim() : '';
|
|
1530
|
+
const p2pType = typeof req.type === 'string' ? req.type.trim() : '';
|
|
1531
|
+
return Boolean(text || msgType === 'message' || kind === 'portal_message' || kind === 'contact_added' || ['friend_request','friend_accept','friend_reject'].includes(p2pType));
|
|
1532
|
+
}
|
|
1533
|
+
function buildPassiveRelayRequest(req, senderDid = '') {
|
|
1534
|
+
const normalizedSender = String(req?.senderDid || req?.from || senderDid || '').trim();
|
|
1535
|
+
const normalizedKind = typeof req?.kind === 'string' && req.kind.trim()
|
|
1536
|
+
? req.kind.trim()
|
|
1537
|
+
: ((typeof req?.msgType === 'string' && req.msgType.trim() === 'message') || typeof req?.text === 'string'
|
|
1538
|
+
? 'portal_message'
|
|
1539
|
+
: 'relay_message');
|
|
1540
|
+
return {
|
|
1541
|
+
method: 'POST',
|
|
1542
|
+
path: '/atel/v1/relay-message',
|
|
1543
|
+
body: {
|
|
1544
|
+
...req,
|
|
1545
|
+
senderDid: normalizedSender,
|
|
1546
|
+
kind: normalizedKind,
|
|
1547
|
+
timestamp: req?.timestamp || new Date().toISOString(),
|
|
1548
|
+
},
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
function extractSignedRelayEnvelope(message) {
|
|
1552
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) return {};
|
|
1553
|
+
if (
|
|
1554
|
+
message.envelope === 'atel.msg.v1' &&
|
|
1555
|
+
typeof message.from === 'string' &&
|
|
1556
|
+
typeof message.to === 'string' &&
|
|
1557
|
+
typeof message.type === 'string' &&
|
|
1558
|
+
message.payload !== undefined &&
|
|
1559
|
+
typeof message.signature === 'string'
|
|
1560
|
+
) {
|
|
1561
|
+
return {
|
|
1562
|
+
envelope: message.envelope,
|
|
1563
|
+
type: message.type,
|
|
1564
|
+
from: message.from,
|
|
1565
|
+
to: message.to,
|
|
1566
|
+
timestamp: message.timestamp,
|
|
1567
|
+
nonce: message.nonce,
|
|
1568
|
+
payload: message.payload,
|
|
1569
|
+
signature: message.signature,
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const signed = {};
|
|
1573
|
+
for (const key of ['to', 'from', 'type', 'action', 'msgType', 'msgId', 'text', 'images', 'attachments', 'summary', 'kind', 'nonce', 'payload', 'envelope', 'signature', 'timestamp']) {
|
|
1574
|
+
if (message[key] !== undefined) signed[key] = message[key];
|
|
1575
|
+
}
|
|
1576
|
+
return signed;
|
|
1577
|
+
}
|
|
1578
|
+
function isAckableMalformedRelayMessage(req, inner) {
|
|
1579
|
+
if (typeof inner === 'string') {
|
|
1580
|
+
const raw = inner.trim();
|
|
1581
|
+
if (raw === '' || raw === '{}') return true;
|
|
1582
|
+
}
|
|
1583
|
+
if (!req || typeof req !== 'object' || Array.isArray(req)) return false;
|
|
1584
|
+
const keys = Object.keys(req);
|
|
1585
|
+
if (keys.length === 0) return true;
|
|
1586
|
+
const path = typeof req.path === 'string' ? req.path.trim() : '';
|
|
1587
|
+
const method = typeof req.method === 'string' ? req.method.trim().toUpperCase() : '';
|
|
1588
|
+
const body = req.body;
|
|
1589
|
+
const hasBodyObject = !!body && typeof body === 'object' && !Array.isArray(body) && Object.keys(body).length > 0;
|
|
1590
|
+
const hasBodyString = typeof body === 'string' && body.trim() !== '';
|
|
1591
|
+
if ((!path || path === '/') && !hasBodyObject && !hasBodyString) return true;
|
|
1592
|
+
return !path && !method && !hasBodyObject && !hasBodyString;
|
|
1593
|
+
}
|
|
1594
|
+
const HUMAN_INBOX_EVENT_PREFIXES = ['p2p_task_', 'p2p_result_'];
|
|
1595
|
+
const HUMAN_INBOX_EVENTS = new Set([
|
|
1596
|
+
'notification',
|
|
1597
|
+
'result_received',
|
|
1598
|
+
'rejection_proof',
|
|
1599
|
+
'friend_added',
|
|
1600
|
+
'friend_removed',
|
|
1601
|
+
'friend_request_received',
|
|
1602
|
+
'friend_request_accepted_by_peer',
|
|
1603
|
+
'friend_request_rejected_by_peer',
|
|
1604
|
+
'relay_message_received',
|
|
1605
|
+
]);
|
|
1606
|
+
function isHumanInboxEntry(entry) {
|
|
1607
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
1608
|
+
if (entry.type === 'task-result') return true;
|
|
1609
|
+
const event = typeof entry.event === 'string' ? entry.event : '';
|
|
1610
|
+
if (!event) return false;
|
|
1611
|
+
if (HUMAN_INBOX_EVENTS.has(event)) return true;
|
|
1612
|
+
return HUMAN_INBOX_EVENT_PREFIXES.some((prefix) => event.startsWith(prefix));
|
|
1613
|
+
}
|
|
1104
1614
|
function saveTrace(taskId, trace) { if (!existsSync(TRACES_DIR)) mkdirSync(TRACES_DIR, { recursive: true }); writeFileSync(resolve(TRACES_DIR, `${taskId}.jsonl`), trace.export()); }
|
|
1105
1615
|
function loadTrace(taskId) { const f = resolve(TRACES_DIR, `${taskId}.jsonl`); if (!existsSync(f)) return null; return readFileSync(f, 'utf-8'); }
|
|
1106
1616
|
function loadPending() { if (!existsSync(PENDING_FILE)) return {}; try { return JSON.parse(readFileSync(PENDING_FILE, 'utf-8')); } catch { return {}; } }
|
|
@@ -1319,7 +1829,9 @@ function removeFriend(did) {
|
|
|
1319
1829
|
|
|
1320
1830
|
// Check if DID is a friend
|
|
1321
1831
|
function isFriend(did) {
|
|
1322
|
-
|
|
1832
|
+
// Friendship can be updated by a separate CLI process (for example `atel friend accept`).
|
|
1833
|
+
// Read from disk here so the long-running agent process does not enforce a stale cache.
|
|
1834
|
+
const data = loadFriends();
|
|
1323
1835
|
return data.friends.some(f => f.did === did && f.status === 'accepted');
|
|
1324
1836
|
}
|
|
1325
1837
|
|
|
@@ -1759,6 +2271,121 @@ function checkP2PAccess(from, action, payload, currentPolicy) {
|
|
|
1759
2271
|
};
|
|
1760
2272
|
}
|
|
1761
2273
|
|
|
2274
|
+
function checkP2PMessageAccess(from, currentPolicy) {
|
|
2275
|
+
if (!from) {
|
|
2276
|
+
return {
|
|
2277
|
+
allowed: false,
|
|
2278
|
+
reason: 'UNKNOWN_SENDER',
|
|
2279
|
+
message: 'Missing sender DID',
|
|
2280
|
+
code: 'INVALID_SENDER'
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
if (currentPolicy.blockedDIDs && currentPolicy.blockedDIDs.length > 0 && currentPolicy.blockedDIDs.includes(from)) {
|
|
2285
|
+
log({
|
|
2286
|
+
event: 'p2p_access_denied',
|
|
2287
|
+
from,
|
|
2288
|
+
action: 'message',
|
|
2289
|
+
reason: 'DID_BLOCKED',
|
|
2290
|
+
timestamp: new Date().toISOString()
|
|
2291
|
+
});
|
|
2292
|
+
return {
|
|
2293
|
+
allowed: false,
|
|
2294
|
+
reason: 'DID_BLOCKED',
|
|
2295
|
+
message: 'You are blocked by this agent',
|
|
2296
|
+
code: 'BLOCKED'
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
const relPolicy = currentPolicy.relationshipPolicy || getDefaultRelationshipPolicy();
|
|
2301
|
+
|
|
2302
|
+
if (isFriend(from)) {
|
|
2303
|
+
log({
|
|
2304
|
+
event: 'p2p_access_granted',
|
|
2305
|
+
from,
|
|
2306
|
+
action: 'message',
|
|
2307
|
+
reason: 'FRIEND',
|
|
2308
|
+
relationship: 'friend',
|
|
2309
|
+
timestamp: new Date().toISOString()
|
|
2310
|
+
});
|
|
2311
|
+
return {
|
|
2312
|
+
allowed: true,
|
|
2313
|
+
reason: 'FRIEND',
|
|
2314
|
+
relationship: 'friend'
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const tempSession = getActiveTempSession(from);
|
|
2319
|
+
if (tempSession) {
|
|
2320
|
+
if (Date.now() > new Date(tempSession.expiresAt).getTime()) {
|
|
2321
|
+
removeTempSession(tempSession.sessionId);
|
|
2322
|
+
log({
|
|
2323
|
+
event: 'p2p_access_denied',
|
|
2324
|
+
from,
|
|
2325
|
+
action: 'message',
|
|
2326
|
+
reason: 'TEMP_SESSION_EXPIRED',
|
|
2327
|
+
sessionId: tempSession.sessionId,
|
|
2328
|
+
timestamp: new Date().toISOString()
|
|
2329
|
+
});
|
|
2330
|
+
return {
|
|
2331
|
+
allowed: false,
|
|
2332
|
+
reason: 'TEMP_SESSION_EXPIRED',
|
|
2333
|
+
message: 'Your temporary session has expired. Please request a new one.',
|
|
2334
|
+
code: 'TEMP_EXPIRED'
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
log({
|
|
2339
|
+
event: 'p2p_access_granted',
|
|
2340
|
+
from,
|
|
2341
|
+
action: 'message',
|
|
2342
|
+
reason: 'TEMP_SESSION',
|
|
2343
|
+
relationship: 'temporary',
|
|
2344
|
+
sessionId: tempSession.sessionId,
|
|
2345
|
+
timestamp: new Date().toISOString()
|
|
2346
|
+
});
|
|
2347
|
+
return {
|
|
2348
|
+
allowed: true,
|
|
2349
|
+
reason: 'TEMP_SESSION',
|
|
2350
|
+
relationship: 'temporary',
|
|
2351
|
+
sessionId: tempSession.sessionId
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (relPolicy.defaultMode === 'open') {
|
|
2356
|
+
log({
|
|
2357
|
+
event: 'p2p_access_granted',
|
|
2358
|
+
from,
|
|
2359
|
+
action: 'message',
|
|
2360
|
+
reason: 'OPEN_MODE',
|
|
2361
|
+
relationship: 'stranger',
|
|
2362
|
+
timestamp: new Date().toISOString()
|
|
2363
|
+
});
|
|
2364
|
+
return {
|
|
2365
|
+
allowed: true,
|
|
2366
|
+
reason: 'OPEN_MODE',
|
|
2367
|
+
relationship: 'stranger'
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
log({
|
|
2372
|
+
event: 'p2p_access_denied',
|
|
2373
|
+
from,
|
|
2374
|
+
action: 'message',
|
|
2375
|
+
reason: 'NOT_FRIEND',
|
|
2376
|
+
defaultMode: relPolicy.defaultMode || 'unknown',
|
|
2377
|
+
timestamp: new Date().toISOString()
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
return {
|
|
2381
|
+
allowed: false,
|
|
2382
|
+
reason: 'NOT_FRIEND',
|
|
2383
|
+
message: 'You are not a friend. Please send a friend request first.',
|
|
2384
|
+
hint: 'Send friend request: atel friend request <your-did>',
|
|
2385
|
+
code: 'NOT_FRIEND'
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
|
|
1762
2389
|
// ─── Unified Trust Score & Level System ──────────────────────────
|
|
1763
2390
|
// Single source of truth: computeTrustScore() calculates score,
|
|
1764
2391
|
// trustLevel is derived from score. No independent logic.
|
|
@@ -1979,19 +2606,8 @@ async function promptInput(question) {
|
|
|
1979
2606
|
// ─── Anchor Configuration ────────────────────────────────────────
|
|
1980
2607
|
|
|
1981
2608
|
async function configureAnchor() {
|
|
1982
|
-
console.log('\n🔗 Configure On-Chain Anchoring
|
|
1983
|
-
|
|
1984
|
-
console.log(' executor wallets — you do NOT need this for paid orders.');
|
|
1985
|
-
console.log(' Your smart wallet addresses (derived from your DID) already receive');
|
|
1986
|
-
console.log(' USDC and the platform pays gas for you. Only continue if you are');
|
|
1987
|
-
console.log(' running a legacy V1 self-anchoring agent.');
|
|
1988
|
-
console.log('');
|
|
1989
|
-
const go = await promptYesNo('Continue anyway?');
|
|
1990
|
-
if (!go) {
|
|
1991
|
-
console.log('Aborted. Your identity is unchanged.');
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
|
|
2609
|
+
console.log('\n🔗 Configure On-Chain Anchoring\n');
|
|
2610
|
+
|
|
1995
2611
|
// 1. Select chain
|
|
1996
2612
|
const chain = await promptChoice(
|
|
1997
2613
|
'Select blockchain for anchoring:',
|
|
@@ -2048,108 +2664,99 @@ async function configureAnchor() {
|
|
|
2048
2664
|
// ─── Commands ────────────────────────────────────────────────────
|
|
2049
2665
|
|
|
2050
2666
|
async function cmdInit(agentId) {
|
|
2051
|
-
const
|
|
2667
|
+
const existingIdentity = loadIdentity();
|
|
2668
|
+
if (existingIdentity && process.env.ATEL_REINIT != '1') {
|
|
2669
|
+
console.log(JSON.stringify({
|
|
2670
|
+
status: 'exists',
|
|
2671
|
+
agent_id: existingIdentity.agent_id,
|
|
2672
|
+
did: existingIdentity.did,
|
|
2673
|
+
next: 'Reuse existing identity. Run: atel start [port]'
|
|
2674
|
+
}, null, 2));
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
let name = (agentId || '').trim();
|
|
2679
|
+
if (!name && process.stdin.isTTY) {
|
|
2680
|
+
const chosen = await promptInput('Choose your agent name (required):');
|
|
2681
|
+
name = (chosen || '').trim();
|
|
2682
|
+
}
|
|
2683
|
+
if (!name) {
|
|
2684
|
+
console.error('Agent name is required. Pass a name explicitly, or run `atel init` interactively and enter one.');
|
|
2685
|
+
process.exit(1);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2052
2688
|
const identity = new AgentIdentity({ agent_id: name });
|
|
2053
2689
|
saveIdentity(identity);
|
|
2054
2690
|
savePolicy(DEFAULT_POLICY);
|
|
2055
|
-
|
|
2691
|
+
|
|
2056
2692
|
// Create default agent-context.md for built-in executor
|
|
2057
2693
|
const ctxFile = resolve(ATEL_DIR, 'agent-context.md');
|
|
2058
2694
|
if (!existsSync(ctxFile)) {
|
|
2059
|
-
writeFileSync(ctxFile, `# Agent Context
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
// NOTE (2026-04-09): We intentionally do NOT prompt for an on-chain anchor
|
|
2063
|
-
// private key here any more. That prompt is a V1 leftover — in V2 the
|
|
2064
|
-
// Platform handles all on-chain anchoring on behalf of agents using its own
|
|
2065
|
-
// registered executor wallets, and the user's USDC lives in a smart wallet
|
|
2066
|
-
// (AA) whose address is derived from the ATEL identity key. Asking users to
|
|
2067
|
-
// paste a raw Base/BSC/Solana private key at install time is both
|
|
2068
|
-
// unnecessary and intimidating. If someone genuinely needs legacy
|
|
2069
|
-
// self-anchoring they can opt in later via `atel anchor config`.
|
|
2695
|
+
writeFileSync(ctxFile, `# Agent Context
|
|
2070
2696
|
|
|
2071
|
-
|
|
2072
|
-
// where to top up USDC for paid orders.
|
|
2073
|
-
let smartWallet = null;
|
|
2074
|
-
try {
|
|
2075
|
-
const info = identity.toJSON?.() || {};
|
|
2076
|
-
smartWallet = info.smart_wallet || info.wallet || null;
|
|
2077
|
-
} catch { /* best-effort */ }
|
|
2697
|
+
You are an ATEL agent (${name}) processing tasks from other agents via the ATEL protocol.
|
|
2078
2698
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2699
|
+
## Guidelines
|
|
2700
|
+
- Complete the task accurately and concisely
|
|
2701
|
+
- Return only the requested result, no extra commentary
|
|
2702
|
+
- If the task is unclear, do your best interpretation
|
|
2703
|
+
- Do not access private files or sensitive data
|
|
2704
|
+
- Do not make external network requests unless the task requires it
|
|
2705
|
+
`);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
console.log('');
|
|
2709
|
+
const providePaidOrders = await promptYesNo(
|
|
2710
|
+
'Do you want to provide paid order services? (requires on-chain anchoring)'
|
|
2711
|
+
);
|
|
2712
|
+
|
|
2713
|
+
let anchorConfigured = false;
|
|
2714
|
+
if (providePaidOrders) {
|
|
2715
|
+
try {
|
|
2716
|
+
await configureAnchor();
|
|
2717
|
+
anchorConfigured = true;
|
|
2718
|
+
} catch (e) {
|
|
2719
|
+
console.error(`❌ Failed to configure anchor: ${e.message}`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
console.log(JSON.stringify({
|
|
2724
|
+
status: 'created',
|
|
2725
|
+
agent_id: identity.agent_id,
|
|
2726
|
+
did: identity.did,
|
|
2083
2727
|
policy: POLICY_FILE,
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
'atel info — show your DID, wallets, and policy',
|
|
2088
|
-
'atel balance — check USDC balance on Base/BSC smart wallets',
|
|
2089
|
-
],
|
|
2090
|
-
note: 'Paid orders work out of the box in V2 — no on-chain private key needed. The Platform anchors on your behalf using its own executor wallets. If you specifically need legacy self-anchoring, run: atel anchor config',
|
|
2091
|
-
};
|
|
2092
|
-
if (smartWallet) output.smartWallet = smartWallet;
|
|
2093
|
-
console.log(JSON.stringify(output, null, 2));
|
|
2728
|
+
anchor: anchorConfigured ? 'configured' : 'disabled',
|
|
2729
|
+
next: 'Run: atel start [port] — auto-configures network and registers'
|
|
2730
|
+
}, null, 2));
|
|
2094
2731
|
|
|
2095
|
-
// Auto-install SKILL.md to OpenClaw skills directory
|
|
2096
2732
|
try {
|
|
2097
|
-
const
|
|
2098
|
-
if (
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
//
|
|
2122
|
-
// We intentionally sync to *every* candidate dir where an `atel-agent` sibling
|
|
2123
|
-
// is present (not just the first one), because different openclaw versions
|
|
2124
|
-
// read from different roots and we want all of them to end up coherent.
|
|
2125
|
-
// Returns the list of destinations actually written.
|
|
2126
|
-
function syncAtelSkillToOpenClaw({ verbose = false } = {}) {
|
|
2127
|
-
const sdkSkillPath = resolveSdkSkillPath();
|
|
2128
|
-
if (!existsSync(sdkSkillPath)) return [];
|
|
2129
|
-
const home = process.env.HOME || '';
|
|
2130
|
-
// Candidate parent dirs, in the order we want to probe. We sync to any
|
|
2131
|
-
// that exist, creating the leaf `atel-agent/` dir if its parent is present.
|
|
2132
|
-
const parents = [
|
|
2133
|
-
join(home, '.openclaw', 'workspace', 'skills'),
|
|
2134
|
-
join(home, '.openclaw', 'skills'),
|
|
2135
|
-
];
|
|
2136
|
-
const written = [];
|
|
2137
|
-
for (const parent of parents) {
|
|
2138
|
-
if (!existsSync(parent)) continue;
|
|
2139
|
-
const dir = join(parent, 'atel-agent');
|
|
2140
|
-
try {
|
|
2141
|
-
mkdirSync(dir, { recursive: true });
|
|
2142
|
-
const dest = join(dir, 'SKILL.md');
|
|
2143
|
-
copyFileSync(sdkSkillPath, dest);
|
|
2144
|
-
written.push(dest);
|
|
2145
|
-
if (verbose) {
|
|
2146
|
-
console.log(`✅ ATEL skill synced → ${dest}`);
|
|
2733
|
+
const sdkSkillPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'atel-agent', 'SKILL.md');
|
|
2734
|
+
if (existsSync(sdkSkillPath)) {
|
|
2735
|
+
const home = process.env.HOME || '';
|
|
2736
|
+
const candidates = [
|
|
2737
|
+
join(home, '.openclaw', 'skills', 'atel-agent'),
|
|
2738
|
+
join(home, '.openclaw', 'workspace', 'skills', 'atel-agent'),
|
|
2739
|
+
];
|
|
2740
|
+
let installed = false;
|
|
2741
|
+
for (const dir of candidates) {
|
|
2742
|
+
if (existsSync(dirname(dir))) {
|
|
2743
|
+
mkdirSync(dir, { recursive: true });
|
|
2744
|
+
copyFileSync(sdkSkillPath, join(dir, 'SKILL.md'));
|
|
2745
|
+
console.log(`\n✅ ATEL Skill auto-installed to ${dir}`);
|
|
2746
|
+
console.log(' Your AI agent will automatically know how to use ATEL.');
|
|
2747
|
+
console.log('');
|
|
2748
|
+
console.log('📌 Tell your agent this message to get started:');
|
|
2749
|
+
console.log(' "Read the atel-agent skill at ~/.openclaw/skills/atel-agent/SKILL.md carefully, then help me set up ATEL and start earning."');
|
|
2750
|
+
installed = true;
|
|
2751
|
+
break;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (!installed) {
|
|
2755
|
+
console.log('\n📋 To teach your AI agent about ATEL, send it this message:');
|
|
2756
|
+
console.log(` "Read ${sdkSkillPath} carefully, then help me set up ATEL and start earning."`);
|
|
2147
2757
|
}
|
|
2148
|
-
} catch (e) {
|
|
2149
|
-
if (verbose) console.error(` (skipped ${dir}: ${e.message})`);
|
|
2150
2758
|
}
|
|
2151
|
-
}
|
|
2152
|
-
return written;
|
|
2759
|
+
} catch (e) { /* skill install is best-effort */ }
|
|
2153
2760
|
}
|
|
2154
2761
|
|
|
2155
2762
|
async function cmdAnchor(subcommand) {
|
|
@@ -2319,7 +2926,7 @@ async function cmdStatus() {
|
|
|
2319
2926
|
async function cmdSetup(port) {
|
|
2320
2927
|
const p = parseInt(port || '3100');
|
|
2321
2928
|
console.log(JSON.stringify({ event: 'network_setup', port: p }));
|
|
2322
|
-
const net = await autoNetworkSetup(p);
|
|
2929
|
+
const net = await autoNetworkSetup(p, ATEL_RELAY);
|
|
2323
2930
|
for (const step of net.steps) console.log(JSON.stringify({ event: 'step', message: step }));
|
|
2324
2931
|
if (net.endpoint) {
|
|
2325
2932
|
saveNetwork({ publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
|
|
@@ -2495,21 +3102,6 @@ async function cmdStart(port) {
|
|
|
2495
3102
|
log({ event: 'openclaw_lock_cleanup_error', error: e.message });
|
|
2496
3103
|
}
|
|
2497
3104
|
|
|
2498
|
-
// Refresh workspace SKILL.md from the currently-installed SDK package.
|
|
2499
|
-
// Without this, `npm i -g` upgrades leave a stale copy at
|
|
2500
|
-
// ~/.openclaw/workspace/skills/atel-agent/SKILL.md that openclaw keeps
|
|
2501
|
-
// reading — the documented behaviour of the upgrade then silently lags
|
|
2502
|
-
// behind the code. Fixes incident 2026-04-09 where a ~12-day-old workspace
|
|
2503
|
-
// skill copy was missing "--desc is required" guidance.
|
|
2504
|
-
try {
|
|
2505
|
-
const synced = syncAtelSkillToOpenClaw();
|
|
2506
|
-
if (synced.length > 0) {
|
|
2507
|
-
log({ event: 'atel_skill_synced', count: synced.length, destinations: synced });
|
|
2508
|
-
}
|
|
2509
|
-
} catch (e) {
|
|
2510
|
-
log({ event: 'atel_skill_sync_error', error: e.message });
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
3105
|
// Initialize Ollama only if explicitly enabled (optional local AI audit)
|
|
2514
3106
|
if (process.env.ATEL_OLLAMA_ENABLED === 'true') {
|
|
2515
3107
|
await initializeOllama().catch(err => {
|
|
@@ -2646,12 +3238,19 @@ async function cmdStart(port) {
|
|
|
2646
3238
|
let networkConfig = loadNetwork();
|
|
2647
3239
|
if (!networkConfig) {
|
|
2648
3240
|
log({ event: 'network_setup', status: 'auto-detecting' });
|
|
2649
|
-
networkConfig = await autoNetworkSetup(p);
|
|
3241
|
+
networkConfig = await autoNetworkSetup(p, ATEL_RELAY);
|
|
2650
3242
|
for (const step of networkConfig.steps) log({ event: 'network_step', message: step });
|
|
2651
3243
|
delete networkConfig.steps;
|
|
2652
3244
|
saveNetwork(networkConfig);
|
|
2653
3245
|
} else {
|
|
2654
|
-
|
|
3246
|
+
const synced = syncNetworkConfigToPort(networkConfig, p);
|
|
3247
|
+
networkConfig = synced.config;
|
|
3248
|
+
if (synced.changed) {
|
|
3249
|
+
saveNetwork(networkConfig);
|
|
3250
|
+
log({ event: 'network_port_aligned', port: p, endpoint: networkConfig.endpoint, candidates: networkConfig.candidates?.length || 0 });
|
|
3251
|
+
} else {
|
|
3252
|
+
log({ event: 'network_loaded', candidates: networkConfig.candidates?.length || 0 });
|
|
3253
|
+
}
|
|
2655
3254
|
}
|
|
2656
3255
|
|
|
2657
3256
|
// ── Start endpoint ──
|
|
@@ -2922,6 +3521,7 @@ async function cmdStart(port) {
|
|
|
2922
3521
|
summary: body.summary,
|
|
2923
3522
|
error: body.error,
|
|
2924
3523
|
childSessionKey: pending.childSessionKey,
|
|
3524
|
+
duration_ms: pending.startedAt ? Date.now() - pending.startedAt : undefined,
|
|
2925
3525
|
});
|
|
2926
3526
|
|
|
2927
3527
|
if (body.status === 'failed') {
|
|
@@ -2937,6 +3537,7 @@ async function cmdStart(port) {
|
|
|
2937
3537
|
childSessionKey: pending.childSessionKey,
|
|
2938
3538
|
summary: body.summary,
|
|
2939
3539
|
error: body.error,
|
|
3540
|
+
duration_ms: pending.startedAt ? Date.now() - pending.startedAt : undefined,
|
|
2940
3541
|
});
|
|
2941
3542
|
|
|
2942
3543
|
if (failedAction.action?.type === 'local_result') {
|
|
@@ -3115,7 +3716,7 @@ async function cmdStart(port) {
|
|
|
3115
3716
|
|
|
3116
3717
|
Important requirements:
|
|
3117
3718
|
1. This is a P2P task. Do not call atel result; the local SDK will submit the result automatically after your callback.
|
|
3118
|
-
2. Your job is to complete the AI work carefully and send the final conclusion back to the local SDK through the callback.
|
|
3719
|
+
2. Your job is to complete the AI work carefully and send the final conclusion back to the local SDK through the callback. Writing text alone does NOT deliver the result. Only the callback counts.
|
|
3119
3720
|
${fileAccessRule}
|
|
3120
3721
|
${contextRule}
|
|
3121
3722
|
5. After finishing, you must immediately run the success callback command template below and replace its content with your real result:
|
|
@@ -3132,7 +3733,7 @@ ${callbackFailed}
|
|
|
3132
3733
|
|
|
3133
3734
|
Important requirements:
|
|
3134
3735
|
1. Do not run atel milestone-submit / milestone-verify / milestone-feedback. The local SDK will execute those commands on your behalf after the callback.
|
|
3135
|
-
2. Your job is to complete the AI work carefully and send the final conclusion back to the local SDK through the callback.
|
|
3736
|
+
2. Your job is to complete the AI work carefully and send the final conclusion back to the local SDK through the callback. Writing text alone does NOT deliver the result. Only the callback counts.
|
|
3136
3737
|
${fileAccessRule}
|
|
3137
3738
|
${contextRule}
|
|
3138
3739
|
${repoRule}
|
|
@@ -3333,11 +3934,14 @@ Format:
|
|
|
3333
3934
|
if (!safePrompt) return { ok: false, error: 'empty_agent_prompt' };
|
|
3334
3935
|
const taskPrompt = buildGatewayCallbackPrompt(eventType, safePrompt, callbackUrl, dedupeKey, cwd, payload);
|
|
3335
3936
|
const timeoutMs = 10 * 60 * 1000;
|
|
3937
|
+
const startedAt = Date.now();
|
|
3336
3938
|
|
|
3337
3939
|
return await new Promise(async (resolve) => {
|
|
3338
3940
|
const timer = setTimeout(() => {
|
|
3941
|
+
const active = pendingAgentCallbacks.get(dedupeKey);
|
|
3339
3942
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
3340
3943
|
persistPendingAgentCallback(dedupeKey, { eventType, payload, cwd, source: dedupeKey.startsWith('reconcile:') ? 'reconcile' : 'main', timeout: true });
|
|
3944
|
+
log({ event: 'agent_callback_timeout', eventType, dedupeKey, childSessionKey: active?.childSessionKey || null, duration_ms: Date.now() - startedAt });
|
|
3341
3945
|
resolve({ ok: false, error: 'agent_callback_timeout' });
|
|
3342
3946
|
}, timeoutMs);
|
|
3343
3947
|
|
|
@@ -3346,6 +3950,7 @@ Format:
|
|
|
3346
3950
|
payload,
|
|
3347
3951
|
cwd,
|
|
3348
3952
|
childSessionKey: null,
|
|
3953
|
+
startedAt,
|
|
3349
3954
|
resolve: (result) => {
|
|
3350
3955
|
clearTimeout(timer);
|
|
3351
3956
|
resolve(result);
|
|
@@ -3375,9 +3980,11 @@ Format:
|
|
|
3375
3980
|
signal: AbortSignal.timeout(15000),
|
|
3376
3981
|
});
|
|
3377
3982
|
if (!resp.ok) {
|
|
3983
|
+
const errorText = await resp.text().catch(() => '');
|
|
3378
3984
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
3379
3985
|
clearPersistedPendingAgentCallback(dedupeKey);
|
|
3380
3986
|
clearTimeout(timer);
|
|
3987
|
+
log({ event: 'agent_session_spawn_failed', eventType, dedupeKey, status: resp.status, error: errorText.slice(0, 300), duration_ms: Date.now() - startedAt });
|
|
3381
3988
|
resolve({ ok: false, error: `sessions_spawn_http_${resp.status}` });
|
|
3382
3989
|
return;
|
|
3383
3990
|
}
|
|
@@ -3386,25 +3993,35 @@ Format:
|
|
|
3386
3993
|
const childSessionKey = data.result?.details?.childSessionKey || data.result?.childSessionKey || '';
|
|
3387
3994
|
const pending = pendingAgentCallbacks.get(dedupeKey);
|
|
3388
3995
|
if (pending) pending.childSessionKey = childSessionKey;
|
|
3389
|
-
log({ event: 'agent_session_spawned', eventType, dedupeKey, childSessionKey });
|
|
3996
|
+
log({ event: 'agent_session_spawned', eventType, dedupeKey, childSessionKey, spawn_duration_ms: Date.now() - startedAt });
|
|
3390
3997
|
} catch (e) {
|
|
3391
3998
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
3392
3999
|
clearPersistedPendingAgentCallback(dedupeKey);
|
|
3393
4000
|
clearTimeout(timer);
|
|
4001
|
+
log({ event: 'agent_session_spawn_failed', eventType, dedupeKey, error: e.message, duration_ms: Date.now() - startedAt });
|
|
3394
4002
|
resolve({ ok: false, error: e.message });
|
|
3395
4003
|
}
|
|
3396
4004
|
});
|
|
3397
4005
|
}
|
|
3398
4006
|
|
|
3399
4007
|
function queueAgentHook(eventType, dedupeKey, promptText, cwd, payload = {}, options = {}) {
|
|
3400
|
-
if (!detectedAgentCmd)
|
|
4008
|
+
if (!detectedAgentCmd) {
|
|
4009
|
+
log({ event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'missing_agent_cmd' });
|
|
4010
|
+
return false;
|
|
4011
|
+
}
|
|
3401
4012
|
const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
|
|
3402
|
-
if (!safePrompt)
|
|
4013
|
+
if (!safePrompt) {
|
|
4014
|
+
log({ event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'empty_prompt' });
|
|
4015
|
+
return false;
|
|
4016
|
+
}
|
|
3403
4017
|
const parsedCmd = detectedAgentCmd.trim().split(/\s+/);
|
|
3404
4018
|
parsedCmd.push(safePrompt);
|
|
3405
4019
|
const recoveryKey = options.recoveryKey || '';
|
|
3406
4020
|
if (recoveryKey) {
|
|
3407
|
-
if (activeRecoveryKeys.has(recoveryKey))
|
|
4021
|
+
if (activeRecoveryKeys.has(recoveryKey)) {
|
|
4022
|
+
log({ event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'recovery_key_active', recoveryKey });
|
|
4023
|
+
return false;
|
|
4024
|
+
}
|
|
3408
4025
|
activeRecoveryKeys.add(recoveryKey);
|
|
3409
4026
|
}
|
|
3410
4027
|
hookQueue.push({ event: eventType, dedupeKey, cmd: parsedCmd[0], args: parsedCmd.slice(1), cwd, payload, recoveryKey });
|
|
@@ -3451,7 +4068,7 @@ Format:
|
|
|
3451
4068
|
const ms = await fetchMilestoneState(orderId);
|
|
3452
4069
|
if (ms.orderStatus !== 'executing') return;
|
|
3453
4070
|
|
|
3454
|
-
if (executorDid === id.did && ms.phase
|
|
4071
|
+
if (executorDid === id.did && ['waiting_executor_submission', 'waiting_executor_resubmission'].includes(ms.phase)) {
|
|
3455
4072
|
const currentIndex = Number.isFinite(ms.currentMilestone) ? ms.currentMilestone : 0;
|
|
3456
4073
|
const currentMilestone = (ms.milestones || []).find(m => m.index === currentIndex) || {};
|
|
3457
4074
|
const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], currentIndex);
|
|
@@ -3466,33 +4083,64 @@ Format:
|
|
|
3466
4083
|
milestoneObjective: currentMilestone.title || '',
|
|
3467
4084
|
previousApprovedOutputs,
|
|
3468
4085
|
});
|
|
3469
|
-
const
|
|
3470
|
-
const
|
|
4086
|
+
const isResubmission = ms.phase === 'waiting_executor_resubmission' || currentMilestone.status === 'rejected';
|
|
4087
|
+
const eventType = isResubmission
|
|
4088
|
+
? 'milestone_rejected'
|
|
4089
|
+
: (currentIndex === 0 ? 'milestone_plan_confirmed' : 'milestone_verified');
|
|
4090
|
+
const payload = isResubmission
|
|
3471
4091
|
? {
|
|
3472
4092
|
orderId,
|
|
3473
|
-
milestoneIndex:
|
|
4093
|
+
milestoneIndex: currentIndex,
|
|
3474
4094
|
totalMilestones: ms.totalMilestones || 5,
|
|
3475
4095
|
milestoneDescription: currentMilestone.title || '',
|
|
3476
4096
|
orderDescription,
|
|
3477
4097
|
previousApprovedOutputs,
|
|
4098
|
+
rejectReason: currentMilestone.rejectReason || '',
|
|
4099
|
+
submitCount: currentMilestone.submitCount || 0,
|
|
3478
4100
|
}
|
|
3479
|
-
:
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
4101
|
+
: (currentIndex === 0
|
|
4102
|
+
? {
|
|
4103
|
+
orderId,
|
|
4104
|
+
milestoneIndex: 0,
|
|
4105
|
+
totalMilestones: ms.totalMilestones || 5,
|
|
4106
|
+
milestoneDescription: currentMilestone.title || '',
|
|
4107
|
+
orderDescription,
|
|
4108
|
+
previousApprovedOutputs,
|
|
4109
|
+
}
|
|
4110
|
+
: {
|
|
4111
|
+
orderId,
|
|
4112
|
+
milestoneIndex: currentIndex - 1,
|
|
4113
|
+
currentMilestone: currentIndex,
|
|
4114
|
+
totalMilestones: ms.totalMilestones || 5,
|
|
4115
|
+
allComplete: false,
|
|
4116
|
+
nextMilestoneDescription: currentMilestone.title || '',
|
|
4117
|
+
orderDescription,
|
|
4118
|
+
previousApprovedOutputs,
|
|
4119
|
+
});
|
|
4120
|
+
const promptText = isResubmission
|
|
4121
|
+
? `You are the ATEL executor agent. Your previous submission for milestone M${currentIndex} was rejected.
|
|
4122
|
+
Original order requirements: ${orderDescription || 'not provided'}
|
|
4123
|
+
Current milestone M${currentIndex}: ${currentMilestone.title || ''}
|
|
4124
|
+
Previously approved outputs:
|
|
4125
|
+
${previousApprovedOutputs || 'none'}
|
|
4126
|
+
|
|
4127
|
+
Revise only the rejected milestone based on the rejection feedback and submit an improved deliverable.`
|
|
4128
|
+
: (currentIndex === 0
|
|
4129
|
+
? `You are the ATEL executor agent. The plan has been confirmed and execution begins.
|
|
4130
|
+
Original order requirements: ${orderDescription || 'not provided'}
|
|
4131
|
+
Current milestone M0: ${currentMilestone.title || ''}
|
|
4132
|
+
Complete only this milestone for the current order and return the final deliverable via the callback.`
|
|
4133
|
+
: `You are the ATEL executor agent. M${currentIndex - 1} has been approved.
|
|
4134
|
+
Original order requirements: ${orderDescription || 'not provided'}
|
|
4135
|
+
Next milestone M${currentIndex}: ${currentMilestone.title || ''}
|
|
4136
|
+
Previously approved outputs:
|
|
4137
|
+
${previousApprovedOutputs || 'none'}
|
|
4138
|
+
|
|
4139
|
+
Advance the current milestone strictly based on these approved results. Do not invent missing materials or read local shared files to fill missing context. After completion, return the final deliverable via the callback.`);
|
|
3492
4140
|
const recoveryKey = buildMilestoneHookRecoveryKey(eventType, payload);
|
|
3493
|
-
log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
4141
|
+
log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, phase: ms.phase, recoveryKey });
|
|
3494
4142
|
const queued = queueAgentHook(eventType, recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
3495
|
-
if (queued) log({ event: 'trade_reconcile_executor_queued', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
4143
|
+
if (queued) log({ event: 'trade_reconcile_executor_queued', orderId, currentMilestone: currentIndex, phase: ms.phase, recoveryKey });
|
|
3496
4144
|
return;
|
|
3497
4145
|
}
|
|
3498
4146
|
|
|
@@ -3578,6 +4226,108 @@ Format:
|
|
|
3578
4226
|
}
|
|
3579
4227
|
}
|
|
3580
4228
|
|
|
4229
|
+
endpoint.app?.post?.('/atel/v1/relay-message', async (req, res) => {
|
|
4230
|
+
const body = req.body || {};
|
|
4231
|
+
const senderDid = String(body.senderDid || body.from || '').trim();
|
|
4232
|
+
const p2pType = typeof body.type === 'string' ? body.type.trim() : '';
|
|
4233
|
+
const relaySignedMessage = extractSignedRelayEnvelope(body);
|
|
4234
|
+
const relaySignedSenderDid = String(relaySignedMessage.from || senderDid || '').trim();
|
|
4235
|
+
|
|
4236
|
+
if (p2pType === 'friend_accept') {
|
|
4237
|
+
const { requestId } = relaySignedMessage.payload || {};
|
|
4238
|
+
const verified = relaySignedSenderDid ? verifyMessage(relaySignedMessage, parseDID(relaySignedSenderDid)) : { valid: false };
|
|
4239
|
+
if (!verified.valid) {
|
|
4240
|
+
log({ event: 'friend_accept_rejected', from: relaySignedSenderDid || body.from, reason: 'invalid_signature_via_relay' });
|
|
4241
|
+
return res.json({ status: 'rejected', error: 'Invalid signature' });
|
|
4242
|
+
}
|
|
4243
|
+
const requests = loadFriendRequests();
|
|
4244
|
+
if (!requests.outgoing) requests.outgoing = [];
|
|
4245
|
+
const request = requests.outgoing.find(r => r.requestId === requestId);
|
|
4246
|
+
if (!request) {
|
|
4247
|
+
log({ event: 'friend_accept_ignored', from: relaySignedSenderDid, requestId, reason: 'request_not_found_via_relay' });
|
|
4248
|
+
return res.json({ status: 'error', error: 'Request not found' });
|
|
4249
|
+
}
|
|
4250
|
+
request.status = 'accepted';
|
|
4251
|
+
request.acceptedAt = new Date().toISOString();
|
|
4252
|
+
saveFriendRequests(requests);
|
|
4253
|
+
addFriend(relaySignedSenderDid, { addedBy: 'request', alias: '', notes: `Accepted our request ${requestId}` });
|
|
4254
|
+
log({ event: 'friend_request_accepted_by_peer', from: relaySignedSenderDid, requestId, via: 'relay' });
|
|
4255
|
+
return res.json({ status: 'ok', type: 'friend_accept' });
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
if (p2pType === 'friend_reject') {
|
|
4259
|
+
const { requestId, reason } = relaySignedMessage.payload || {};
|
|
4260
|
+
const verified = relaySignedSenderDid ? verifyMessage(relaySignedMessage, parseDID(relaySignedSenderDid)) : { valid: false };
|
|
4261
|
+
if (!verified.valid) {
|
|
4262
|
+
log({ event: 'friend_reject_ignored', from: relaySignedSenderDid || body.from, reason: 'invalid_signature_via_relay' });
|
|
4263
|
+
return res.json({ status: 'rejected', error: 'Invalid signature' });
|
|
4264
|
+
}
|
|
4265
|
+
const requests = loadFriendRequests();
|
|
4266
|
+
if (!requests.outgoing) requests.outgoing = [];
|
|
4267
|
+
const request = requests.outgoing.find(r => r.requestId === requestId);
|
|
4268
|
+
if (request) {
|
|
4269
|
+
request.status = 'rejected';
|
|
4270
|
+
request.rejectedAt = new Date().toISOString();
|
|
4271
|
+
request.reason = reason || '';
|
|
4272
|
+
saveFriendRequests(requests);
|
|
4273
|
+
}
|
|
4274
|
+
log({ event: 'friend_request_rejected_by_peer', from: relaySignedSenderDid, requestId, reason, via: 'relay' });
|
|
4275
|
+
return res.json({ status: 'ok', type: 'friend_reject' });
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
const kind = String(body.kind || ((typeof body.msgType === 'string' && body.msgType === 'message') || typeof body.text === 'string' ? 'portal_message' : 'relay_message')).trim();
|
|
4279
|
+
const text = typeof body.text === 'string' && body.text.trim()
|
|
4280
|
+
? body.text.trim()
|
|
4281
|
+
: (typeof body.summary === 'string' && body.summary.trim()
|
|
4282
|
+
? body.summary.trim()
|
|
4283
|
+
: (kind === 'contact_added' && senderDid ? `${senderDid} added you as a contact` : '[message]'));
|
|
4284
|
+
const messageSenderDid = relaySignedSenderDid || senderDid;
|
|
4285
|
+
|
|
4286
|
+
if (kind === 'portal_message' && messageSenderDid && messageSenderDid !== 'did:atel:platform') {
|
|
4287
|
+
if (relaySignedMessage.signature) {
|
|
4288
|
+
const verified = verifyMessage(relaySignedMessage, parseDID(messageSenderDid));
|
|
4289
|
+
if (!verified.valid) {
|
|
4290
|
+
log({ event: 'relay_message_rejected', kind, from: messageSenderDid, reason: 'invalid_signature' });
|
|
4291
|
+
return res.json({ status: 'rejected', error: 'Invalid signature', code: 'INVALID_SIGNATURE' });
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
const currentPolicy = loadPolicy();
|
|
4296
|
+
const accessCheck = checkP2PMessageAccess(messageSenderDid, currentPolicy);
|
|
4297
|
+
if (!accessCheck.allowed) {
|
|
4298
|
+
log({
|
|
4299
|
+
event: 'p2p_message_rejected',
|
|
4300
|
+
from: messageSenderDid,
|
|
4301
|
+
reason: accessCheck.reason,
|
|
4302
|
+
code: accessCheck.code,
|
|
4303
|
+
timestamp: new Date().toISOString()
|
|
4304
|
+
});
|
|
4305
|
+
return res.json({
|
|
4306
|
+
status: 'rejected',
|
|
4307
|
+
error: accessCheck.message || 'Access denied',
|
|
4308
|
+
code: accessCheck.code,
|
|
4309
|
+
hint: accessCheck.hint
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
log({
|
|
4315
|
+
event: 'relay_message_received',
|
|
4316
|
+
kind,
|
|
4317
|
+
from: messageSenderDid || senderDid,
|
|
4318
|
+
text,
|
|
4319
|
+
timestamp: body.timestamp || new Date().toISOString(),
|
|
4320
|
+
});
|
|
4321
|
+
|
|
4322
|
+
if (kind === 'contact_added') {
|
|
4323
|
+
pushP2PNotification('p2p_contact_added', { peerDid: messageSenderDid || senderDid, alias: body.alias || '', text }).catch((e) => log({ event: 'p2p_notify_error', kind, error: e.message }));
|
|
4324
|
+
} else if (kind !== 'telegram_message') {
|
|
4325
|
+
pushP2PNotification('p2p_message_received', { peerDid: messageSenderDid || senderDid, text }).catch((e) => log({ event: 'p2p_notify_error', kind, error: e.message }));
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
res.json({ status: 'ok', kind });
|
|
4329
|
+
});
|
|
4330
|
+
|
|
3581
4331
|
// Webhook notification: POST /atel/v1/notify
|
|
3582
4332
|
// SDK only: logs, writes inbox, prints prompt. Does NOT execute any actions.
|
|
3583
4333
|
// Agent reads inbox or webhook to decide what to do.
|
|
@@ -3638,29 +4388,6 @@ Format:
|
|
|
3638
4388
|
dedupeKey: body.dedupeKey,
|
|
3639
4389
|
});
|
|
3640
4390
|
|
|
3641
|
-
// ── Fast-coop: auto-submit deliverable when all milestones verified ──
|
|
3642
|
-
// When executor receives the final milestone_verified for a fast-coop order,
|
|
3643
|
-
// sign and submit Escrow::Submit on Fast staging so the Platform can Complete.
|
|
3644
|
-
if (event === 'milestone_verified' && payload.allComplete === true) {
|
|
3645
|
-
const fastOrderId = body.orderId || payload.orderId || '';
|
|
3646
|
-
if (fastOrderId) {
|
|
3647
|
-
// Payload may not include chain — fetch order to check
|
|
3648
|
-
try {
|
|
3649
|
-
const _resp = await fetch(`${PLATFORM_URL}/trade/v1/order/${fastOrderId}`, { signal: AbortSignal.timeout(10000) });
|
|
3650
|
-
const orderInfo = _resp.ok ? await _resp.json() : null;
|
|
3651
|
-
const orderChain = orderInfo?.chain || orderInfo?.Chain || '';
|
|
3652
|
-
if (orderChain === 'fast-coop') {
|
|
3653
|
-
log({ event: 'fast_submit_triggered', orderId: fastOrderId, chain: orderChain });
|
|
3654
|
-
submitFastDeliverable(fastOrderId, id).catch(e => {
|
|
3655
|
-
log({ event: 'fast_submit_error', orderId: fastOrderId, error: e.message });
|
|
3656
|
-
});
|
|
3657
|
-
}
|
|
3658
|
-
} catch (e) {
|
|
3659
|
-
log({ event: 'fast_submit_check_error', orderId: fastOrderId, error: e.message });
|
|
3660
|
-
}
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
|
|
3664
4391
|
const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
|
|
3665
4392
|
const orderIdForCwd = body.orderId || payload.orderId || '';
|
|
3666
4393
|
let workspace = getOrderWorkspace(orderIdForCwd, {
|
|
@@ -3875,27 +4602,34 @@ Format:
|
|
|
3875
4602
|
const localHookTimeoutMs = isMilestoneHook ? 180000 : 600000;
|
|
3876
4603
|
const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
|
|
3877
4604
|
const runHook = (attempt, invocation = preparedInvocation) => {
|
|
4605
|
+
const hookStartedAt = Date.now();
|
|
3878
4606
|
execFile(invocation.cmd, invocation.args, { timeout: localHookTimeoutMs, cwd: hookCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3879
4607
|
const errMsg = (err?.message || '') + (stderr || '');
|
|
4608
|
+
const durationMs = Date.now() - hookStartedAt;
|
|
3880
4609
|
const isSessionLock = errMsg.includes('session file locked') || errMsg.includes('session locked');
|
|
3881
4610
|
const isNetworkError = err && (err.killed || err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET');
|
|
3882
4611
|
|
|
3883
4612
|
if (isSessionLock && attempt < MAX_ATTEMPTS) {
|
|
3884
4613
|
// OpenClaw session still held by previous call — wait for lock release then retry
|
|
3885
4614
|
const delay = 15000 + (attempt * 5000); // 15s, 20s, 25s, 30s
|
|
3886
|
-
log({ event: 'agent_cmd_session_lock', eventType: hookEvent, attempt: attempt + 1, delayMs: delay });
|
|
4615
|
+
log({ event: 'agent_cmd_session_lock', eventType: hookEvent, attempt: attempt + 1, delayMs: delay, duration_ms: durationMs });
|
|
3887
4616
|
setTimeout(() => runHook(attempt + 1), delay);
|
|
3888
4617
|
} else if (isNetworkError && attempt < 2) {
|
|
3889
|
-
log({ event: 'agent_cmd_retry', eventType: hookEvent, attempt: attempt + 1, error: err.message });
|
|
4618
|
+
log({ event: 'agent_cmd_retry', eventType: hookEvent, attempt: attempt + 1, error: err.message, duration_ms: durationMs });
|
|
3890
4619
|
setTimeout(() => runHook(attempt + 1), 10000);
|
|
3891
4620
|
} else if (err) {
|
|
3892
|
-
|
|
4621
|
+
if (err.killed || err.code === 'ETIMEDOUT') {
|
|
4622
|
+
log({ event: 'agent_cmd_timeout', eventType: hookEvent, dedupeKey: hookKey, timeout_ms: localHookTimeoutMs, duration_ms: durationMs, error: err.message, stderr: (stderr || '').substring(0, 200) });
|
|
4623
|
+
} else {
|
|
4624
|
+
log({ event: 'agent_cmd_error', eventType: hookEvent, dedupeKey: hookKey, error: err.message, duration_ms: durationMs, stderr: (stderr || '').substring(0, 200) });
|
|
4625
|
+
}
|
|
3893
4626
|
finishHook();
|
|
3894
4627
|
} else if (isKnownUpstreamModelInputError(stdout)) {
|
|
3895
4628
|
log({
|
|
3896
4629
|
event: 'agent_cmd_upstream_input_error',
|
|
3897
4630
|
eventType: hookEvent,
|
|
3898
4631
|
dedupeKey: hookKey,
|
|
4632
|
+
duration_ms: durationMs,
|
|
3899
4633
|
note: 'suppressed known upstream model input-length error',
|
|
3900
4634
|
});
|
|
3901
4635
|
finishHook();
|
|
@@ -3926,21 +4660,21 @@ Format:
|
|
|
3926
4660
|
: Promise.resolve(false);
|
|
3927
4661
|
guardCheck.then(alreadySubmitted => {
|
|
3928
4662
|
if (alreadySubmitted) {
|
|
3929
|
-
log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'already_submitted_by_agent', dedupeKey: hookKey });
|
|
4663
|
+
log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'already_submitted_by_agent', dedupeKey: hookKey, duration_ms: durationMs });
|
|
3930
4664
|
finishHook();
|
|
3931
4665
|
return;
|
|
3932
4666
|
}
|
|
3933
4667
|
executeRecommendedActionDirect(hookEvent, localAction.action, hookCwd || process.cwd(), hookKey)
|
|
3934
4668
|
.then((execResult) => {
|
|
3935
4669
|
if (execResult.ok) {
|
|
3936
|
-
log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'local_stdout_action', dedupeKey: hookKey, stdout: summarizeAgentOutput(stdout, 200) });
|
|
4670
|
+
log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'local_stdout_action', dedupeKey: hookKey, duration_ms: durationMs, stdout: summarizeAgentOutput(stdout, 200) });
|
|
3937
4671
|
} else {
|
|
3938
|
-
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: execResult.error || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
|
|
4672
|
+
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: execResult.error || 'local_action_failed', duration_ms: durationMs, stdout: summarizeAgentOutput(stdout, 200) });
|
|
3939
4673
|
}
|
|
3940
4674
|
finishHook();
|
|
3941
4675
|
})
|
|
3942
4676
|
.catch((e) => {
|
|
3943
|
-
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: e.message || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
|
|
4677
|
+
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: e.message || 'local_action_failed', duration_ms: durationMs, stdout: summarizeAgentOutput(stdout, 200) });
|
|
3944
4678
|
finishHook();
|
|
3945
4679
|
});
|
|
3946
4680
|
return;
|
|
@@ -3948,7 +4682,13 @@ Format:
|
|
|
3948
4682
|
return;
|
|
3949
4683
|
}
|
|
3950
4684
|
|
|
3951
|
-
|
|
4685
|
+
if (localAction.skipped) {
|
|
4686
|
+
log({ event: 'agent_cmd_skipped', eventType: hookEvent, dedupeKey: hookKey, reason: localAction.reason || 'skipped', duration_ms: durationMs, stdout: summarizeAgentOutput(stdout, 200) });
|
|
4687
|
+
finishHook();
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4691
|
+
log({ event: 'agent_cmd_local_parse_error', eventType: hookEvent, dedupeKey: hookKey, error: localAction.error || 'invalid_local_agent_stdout', duration_ms: durationMs, stdout: summarizeAgentOutput(stdout, 300) });
|
|
3952
4692
|
finishHook();
|
|
3953
4693
|
}
|
|
3954
4694
|
});
|
|
@@ -4507,6 +5247,42 @@ Format:
|
|
|
4507
5247
|
};
|
|
4508
5248
|
}
|
|
4509
5249
|
|
|
5250
|
+
if (payload.msgType === 'message') {
|
|
5251
|
+
const text = typeof payload.text === 'string' && payload.text.trim()
|
|
5252
|
+
? payload.text.trim()
|
|
5253
|
+
: (typeof payload.summary === 'string' && payload.summary.trim() ? payload.summary.trim() : '[message]');
|
|
5254
|
+
const currentPolicy = loadPolicy();
|
|
5255
|
+
const accessCheck = checkP2PMessageAccess(message.from, currentPolicy);
|
|
5256
|
+
if (!accessCheck.allowed) {
|
|
5257
|
+
log({
|
|
5258
|
+
event: 'p2p_message_rejected',
|
|
5259
|
+
from: message.from,
|
|
5260
|
+
reason: accessCheck.reason,
|
|
5261
|
+
code: accessCheck.code,
|
|
5262
|
+
timestamp: new Date().toISOString()
|
|
5263
|
+
});
|
|
5264
|
+
return {
|
|
5265
|
+
status: 'rejected',
|
|
5266
|
+
error: accessCheck.message || 'Access denied',
|
|
5267
|
+
code: accessCheck.code,
|
|
5268
|
+
hint: accessCheck.hint
|
|
5269
|
+
};
|
|
5270
|
+
}
|
|
5271
|
+
log({
|
|
5272
|
+
event: 'relay_message_received',
|
|
5273
|
+
kind: 'portal_message',
|
|
5274
|
+
from: message.from,
|
|
5275
|
+
text,
|
|
5276
|
+
timestamp: new Date().toISOString(),
|
|
5277
|
+
});
|
|
5278
|
+
pushP2PNotification('p2p_message_received', { peerDid: message.from, text }).catch((e) => log({ event: 'p2p_notify_error', kind: 'portal_message', error: e.message }));
|
|
5279
|
+
return {
|
|
5280
|
+
status: 'ok',
|
|
5281
|
+
kind: 'portal_message',
|
|
5282
|
+
msgId: payload.msgId || null,
|
|
5283
|
+
};
|
|
5284
|
+
}
|
|
5285
|
+
|
|
4510
5286
|
// Ignore task-result messages (these are responses, not new tasks)
|
|
4511
5287
|
if (message.type === 'task-result' || payload.status === 'completed' || payload.status === 'failed') {
|
|
4512
5288
|
log({ event: 'result_received', type: 'task-result', from: message.from, taskId: payload.taskId, status: payload.status, proof: payload.proof || null, anchor: payload.anchor || null, execution: payload.execution || null, result: payload.result || null, timestamp: new Date().toISOString() });
|
|
@@ -4805,8 +5581,10 @@ Format:
|
|
|
4805
5581
|
|
|
4806
5582
|
await endpoint.start();
|
|
4807
5583
|
|
|
4808
|
-
//
|
|
4809
|
-
|
|
5584
|
+
// Telegram notifications are opt-in by default. Only auto-bind when explicit consent was already collected.
|
|
5585
|
+
if (ATEL_NOTIFY_AUTO_BIND) {
|
|
5586
|
+
try { autoBindNotifications(); } catch (e) { /* never block startup */ }
|
|
5587
|
+
}
|
|
4810
5588
|
|
|
4811
5589
|
// Background retry for failed result pushes (durable queue)
|
|
4812
5590
|
const flushResultPushQueue = async () => {
|
|
@@ -4945,21 +5723,30 @@ Format:
|
|
|
4945
5723
|
try {
|
|
4946
5724
|
req = typeof inner === 'string' ? JSON.parse(inner) : inner;
|
|
4947
5725
|
} catch (e) {
|
|
4948
|
-
if (m.id)
|
|
4949
|
-
log({ event: '
|
|
5726
|
+
if (m.id) ackedIds.push(m.id);
|
|
5727
|
+
log({ event: 'relay_message_parse_error_acked', id: m.id, error: e.message, rawType: typeof inner });
|
|
4950
5728
|
continue;
|
|
4951
5729
|
}
|
|
4952
5730
|
|
|
4953
5731
|
try {
|
|
5732
|
+
if (isLegacyPassiveRelayMessage(req)) {
|
|
5733
|
+
req = buildPassiveRelayRequest(req, m.sender || '');
|
|
5734
|
+
}
|
|
5735
|
+
if (isAckableMalformedRelayMessage(req, inner)) {
|
|
5736
|
+
if (m.id) ackedIds.push(m.id);
|
|
5737
|
+
log({ event: 'relay_message_invalid_acked', id: m.id, path: typeof req?.path === 'string' && req.path.trim() ? req.path.trim() : '/', note: 'empty_or_invalid_relay_message' });
|
|
5738
|
+
continue;
|
|
5739
|
+
}
|
|
4954
5740
|
const method = req.method || 'POST';
|
|
4955
|
-
const path = req.path
|
|
5741
|
+
const path = typeof req.path === 'string' ? req.path.trim() : '';
|
|
5742
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
4956
5743
|
const fetchOpts = {
|
|
4957
5744
|
method,
|
|
4958
5745
|
headers: { 'Content-Type': 'application/json' },
|
|
4959
5746
|
signal: AbortSignal.timeout(30000),
|
|
4960
5747
|
};
|
|
4961
|
-
if (method !== 'GET' && method !== 'HEAD') fetchOpts.body = JSON.stringify(
|
|
4962
|
-
const localUrl = `http://127.0.0.1:${p}${path}`;
|
|
5748
|
+
if (method !== 'GET' && method !== 'HEAD') fetchOpts.body = JSON.stringify(body);
|
|
5749
|
+
const localUrl = `http://127.0.0.1:${p}${path || '/'}`;
|
|
4963
5750
|
const localResp = await fetch(localUrl, fetchOpts);
|
|
4964
5751
|
const rawText = await localResp.text();
|
|
4965
5752
|
let parsed;
|
|
@@ -5020,6 +5807,23 @@ Format:
|
|
|
5020
5807
|
}, 120000);
|
|
5021
5808
|
}
|
|
5022
5809
|
|
|
5810
|
+
let telegramInboundBusy = false;
|
|
5811
|
+
const pollTelegramInbound = async () => {
|
|
5812
|
+
if (telegramInboundBusy) return;
|
|
5813
|
+
telegramInboundBusy = true;
|
|
5814
|
+
try {
|
|
5815
|
+
await pollTelegramInboundUpdates(id.did);
|
|
5816
|
+
} catch (e) {
|
|
5817
|
+
log({ event: 'telegram_inbound_poll_error', did: id.did, error: e.message || 'unknown_error' });
|
|
5818
|
+
} finally {
|
|
5819
|
+
telegramInboundBusy = false;
|
|
5820
|
+
}
|
|
5821
|
+
};
|
|
5822
|
+
pollTelegramInbound().catch((e) => log({ event: 'telegram_inbound_poll_bootstrap_error', did: id.did, error: e.message || 'unknown_error' }));
|
|
5823
|
+
setInterval(() => {
|
|
5824
|
+
pollTelegramInbound().catch((e) => log({ event: 'telegram_inbound_poll_interval_error', did: id.did, error: e.message || 'unknown_error' }));
|
|
5825
|
+
}, 4000);
|
|
5826
|
+
|
|
5023
5827
|
console.log(JSON.stringify({
|
|
5024
5828
|
status: 'listening', agent_id: id.agent_id, did: id.did,
|
|
5025
5829
|
port: p, candidates: networkConfig.candidates || [], capabilities: capTypes,
|
|
@@ -5099,10 +5903,16 @@ Format:
|
|
|
5099
5903
|
|
|
5100
5904
|
async function cmdInbox(count) {
|
|
5101
5905
|
const n = parseInt(count || '20');
|
|
5102
|
-
if (!existsSync(INBOX_FILE)) {
|
|
5906
|
+
if (!existsSync(INBOX_FILE)) {
|
|
5907
|
+
console.log(JSON.stringify({ messages: [], count: 0, total: 0, filteredOut: 0 }, null, 2));
|
|
5908
|
+
return;
|
|
5909
|
+
}
|
|
5103
5910
|
const lines = readFileSync(INBOX_FILE, 'utf-8').trim().split('\n').filter(Boolean);
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5911
|
+
const parsed = lines.map((line) => {
|
|
5912
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
5913
|
+
}).filter(Boolean);
|
|
5914
|
+
const messages = parsed.filter(isHumanInboxEntry).slice(-n);
|
|
5915
|
+
console.log(JSON.stringify({ messages, count: messages.length, total: parsed.length, filteredOut: parsed.length - messages.length }, null, 2));
|
|
5106
5916
|
}
|
|
5107
5917
|
|
|
5108
5918
|
async function cmdRegister(name, capabilities, endpointUrl) {
|
|
@@ -5857,7 +6667,8 @@ async function signedFetch(method, path, payload = {}) {
|
|
|
5857
6667
|
// ─── Auth Command ────────────────────────────────────────────────
|
|
5858
6668
|
|
|
5859
6669
|
async function cmdAuth(code) {
|
|
5860
|
-
|
|
6670
|
+
const normalizedCode = (code || '').trim().toUpperCase();
|
|
6671
|
+
if (!normalizedCode) {
|
|
5861
6672
|
console.error('Usage: atel auth <code>');
|
|
5862
6673
|
console.error(' Authorize a Dashboard session using the code displayed on the login page.');
|
|
5863
6674
|
process.exit(1);
|
|
@@ -5867,7 +6678,7 @@ async function cmdAuth(code) {
|
|
|
5867
6678
|
const { serializePayload } = await import('@lawrenceliang-btc/atel-sdk');
|
|
5868
6679
|
|
|
5869
6680
|
const ts = new Date().toISOString();
|
|
5870
|
-
const payload = { code:
|
|
6681
|
+
const payload = { code: normalizedCode, did: id.did, timestamp: ts };
|
|
5871
6682
|
const signable = serializePayload({ payload, did: id.did, timestamp: ts });
|
|
5872
6683
|
const sig = Buffer.from(nacl.sign.detached(Buffer.from(signable), id.secretKey)).toString('base64');
|
|
5873
6684
|
|
|
@@ -5875,7 +6686,7 @@ async function cmdAuth(code) {
|
|
|
5875
6686
|
const resp = await fetch(`${ATEL_PLATFORM}/auth/v1/verify`, {
|
|
5876
6687
|
method: 'POST',
|
|
5877
6688
|
headers: { 'Content-Type': 'application/json' },
|
|
5878
|
-
body: JSON.stringify({ code:
|
|
6689
|
+
body: JSON.stringify({ code: normalizedCode, did: id.did, signature: sig, timestamp: ts }),
|
|
5879
6690
|
});
|
|
5880
6691
|
const data = await resp.json();
|
|
5881
6692
|
if (resp.ok) {
|
|
@@ -6014,19 +6825,11 @@ async function cmdTransactions() {
|
|
|
6014
6825
|
// ─── Trade Task: High-level one-shot command ────────────────────
|
|
6015
6826
|
async function cmdTradeTask(capability, description) {
|
|
6016
6827
|
if (!capability) { console.error('Usage: atel trade-task <capability> <description> [--budget N] [--executor DID] [--timeout 300]'); process.exit(1); }
|
|
6017
|
-
if (!description || !description.trim()) {
|
|
6018
|
-
console.error('Error: <description> is required and must contain the full task description.');
|
|
6019
|
-
console.error(' The executor can only understand what to do via this field.');
|
|
6020
|
-
console.error(' Pass the user\'s original task text verbatim, e.g.:');
|
|
6021
|
-
console.error(' atel trade-task <capability> "<full user message>" --budget 5');
|
|
6022
|
-
console.error(' Do NOT summarize, translate, or shorten the user\'s request.');
|
|
6023
|
-
process.exit(2);
|
|
6024
|
-
}
|
|
6025
6828
|
const id = requireIdentity();
|
|
6026
6829
|
const budget = parseFloat(rawArgs.find((a, i) => rawArgs[i-1] === '--budget') || '0');
|
|
6027
6830
|
const executorArg = rawArgs.find((a, i) => rawArgs[i-1] === '--executor') || '';
|
|
6028
6831
|
const timeout = parseInt(rawArgs.find((a, i) => rawArgs[i-1] === '--timeout') || '300') * 1000;
|
|
6029
|
-
const desc = description;
|
|
6832
|
+
const desc = description || capability;
|
|
6030
6833
|
|
|
6031
6834
|
// Step 1: Find executor
|
|
6032
6835
|
let executorDid = executorArg;
|
|
@@ -6048,7 +6851,6 @@ async function cmdTradeTask(capability, description) {
|
|
|
6048
6851
|
console.error(`[trade-task] Creating order: ${capability}, budget: $${budget}...`);
|
|
6049
6852
|
const orderData = await signedFetch('POST', '/trade/v1/order', {
|
|
6050
6853
|
executorDid, capabilityType: capability, priceAmount: budget, priceCurrency: 'USD', pricingModel: 'per_task',
|
|
6051
|
-
description: desc,
|
|
6052
6854
|
});
|
|
6053
6855
|
const orderId = orderData.orderId;
|
|
6054
6856
|
console.error(`[trade-task] Order created: ${orderId}`);
|
|
@@ -6096,27 +6898,12 @@ async function cmdTradeTask(capability, description) {
|
|
|
6096
6898
|
}
|
|
6097
6899
|
|
|
6098
6900
|
async function cmdOrder(executorDid, capType, price) {
|
|
6099
|
-
if (!executorDid || !capType || !price) { console.error('Usage: atel order <executorDid> <capabilityType> <price> --desc "task description"'); process.exit(1); }
|
|
6901
|
+
if (!executorDid || !capType || !price) { console.error('Usage: atel order <executorDid> <capabilityType> <price> [--desc "task description"]'); process.exit(1); }
|
|
6100
6902
|
// Resolve @alias to full DID
|
|
6101
6903
|
if (executorDid.startsWith('@')) {
|
|
6102
6904
|
try { executorDid = resolveDID(executorDid); } catch (e) { console.error(e.message); process.exit(1); }
|
|
6103
6905
|
}
|
|
6104
6906
|
const description = rawArgs.find((a, i) => rawArgs[i-1] === '--desc') || '';
|
|
6105
|
-
// --desc is MANDATORY: the executor can only see the task via this field.
|
|
6106
|
-
// An empty description would make the executor work from its own imagination
|
|
6107
|
-
// (see production incident 2026-04-09, order ord-4c12a03d-ea8 where the
|
|
6108
|
-
// executor generated an off-topic "system validation" milestone because
|
|
6109
|
-
// orderDescription arrived empty). We fail loudly here so the caller
|
|
6110
|
-
// (human or ReAct agent loop) gets an actionable error and can retry
|
|
6111
|
-
// with the real task text from the user's message.
|
|
6112
|
-
if (!description || !description.trim()) {
|
|
6113
|
-
console.error('Error: --desc is required and must contain the full task description.');
|
|
6114
|
-
console.error(' The executor can only understand what to do via --desc.');
|
|
6115
|
-
console.error(' Pass the user\'s original task text verbatim, e.g.:');
|
|
6116
|
-
console.error(' atel order <executorDid> <capability> <price> --desc "<full user message>"');
|
|
6117
|
-
console.error(' Do NOT summarize, translate, or shorten the user\'s request.');
|
|
6118
|
-
process.exit(2);
|
|
6119
|
-
}
|
|
6120
6907
|
const chainArg = rawArgs.find((a, i) => rawArgs[i-1] === '--chain') || '';
|
|
6121
6908
|
const id = requireIdentity();
|
|
6122
6909
|
|
|
@@ -6191,6 +6978,7 @@ async function cmdOrder(executorDid, capType, price) {
|
|
|
6191
6978
|
};
|
|
6192
6979
|
if (chainArg) { orderBody.chain = chainArg; }
|
|
6193
6980
|
const data = await signedFetch('POST', '/trade/v1/order', orderBody);
|
|
6981
|
+
if (data?.orderId) trackOrder(data.orderId, 'requester');
|
|
6194
6982
|
|
|
6195
6983
|
// For paid orders: show escrow info (chain escrow creation handled by Platform backend)
|
|
6196
6984
|
if (data.orderId && parseFloat(price) > 0 && data.escrow?.escrowContract) {
|
|
@@ -6766,6 +7554,7 @@ async function cmdComplete(orderId, taskId) {
|
|
|
6766
7554
|
async function cmdConfirm(orderId) {
|
|
6767
7555
|
if (!orderId) { console.error('Usage: atel confirm <orderId>'); process.exit(1); }
|
|
6768
7556
|
const data = await signedFetch('POST', `/trade/v1/order/${orderId}/confirm`);
|
|
7557
|
+
rememberCurrentTelegramRoute(orderId);
|
|
6769
7558
|
console.log(JSON.stringify(data, null, 2));
|
|
6770
7559
|
}
|
|
6771
7560
|
|
|
@@ -6823,6 +7612,7 @@ async function cmdMilestoneFeedback(orderId) {
|
|
|
6823
7612
|
|
|
6824
7613
|
const payload = approve ? { approved: true } : { approved: false, feedback };
|
|
6825
7614
|
const data = await signedFetch('POST', `/trade/v1/order/${orderId}/milestones/feedback`, payload);
|
|
7615
|
+
rememberCurrentTelegramRoute(orderId);
|
|
6826
7616
|
console.log(JSON.stringify(data, null, 2));
|
|
6827
7617
|
}
|
|
6828
7618
|
|
|
@@ -6894,6 +7684,7 @@ async function cmdMilestoneSubmit(orderId, indexStr) {
|
|
|
6894
7684
|
resultSummary: resultText,
|
|
6895
7685
|
resultHash,
|
|
6896
7686
|
});
|
|
7687
|
+
rememberCurrentTelegramRoute(orderId);
|
|
6897
7688
|
console.log(JSON.stringify(data, null, 2));
|
|
6898
7689
|
}
|
|
6899
7690
|
|
|
@@ -6943,9 +7734,11 @@ async function cmdMilestoneVerify(orderId, indexStr) {
|
|
|
6943
7734
|
process.exit(1);
|
|
6944
7735
|
}
|
|
6945
7736
|
const data = await signedFetch('POST', `/trade/v1/order/${orderId}/milestone/${indexStr}/verify`, { passed: false, rejectReason });
|
|
7737
|
+
rememberCurrentTelegramRoute(orderId);
|
|
6946
7738
|
console.log(JSON.stringify(data, null, 2));
|
|
6947
7739
|
} else {
|
|
6948
7740
|
const data = await signedFetch('POST', `/trade/v1/order/${orderId}/milestone/${indexStr}/verify`, { passed: true });
|
|
7741
|
+
rememberCurrentTelegramRoute(orderId);
|
|
6949
7742
|
console.log(JSON.stringify(data, null, 2));
|
|
6950
7743
|
}
|
|
6951
7744
|
}
|
|
@@ -7849,35 +8642,130 @@ async function cmdFriendStatus(args) {
|
|
|
7849
8642
|
// ─── P2P Send Helper ────────────────────────────────────────────
|
|
7850
8643
|
// Resolves DID → endpoint via registry, then sends a signed message.
|
|
7851
8644
|
// Replaces the previously undefined sendP2PMessage.
|
|
8645
|
+
function extractCapabilityTypes(capabilities) {
|
|
8646
|
+
if (!Array.isArray(capabilities)) return [];
|
|
8647
|
+
return capabilities.map((cap) => {
|
|
8648
|
+
if (typeof cap === 'string') return cap.trim().toLowerCase();
|
|
8649
|
+
if (cap && typeof cap === 'object' && typeof cap.type === 'string') return cap.type.trim().toLowerCase();
|
|
8650
|
+
return '';
|
|
8651
|
+
}).filter(Boolean);
|
|
8652
|
+
}
|
|
8653
|
+
|
|
7852
8654
|
async function sendP2PMessage(targetDid, signedMsg) {
|
|
7853
8655
|
const id = loadIdentity();
|
|
7854
8656
|
if (!id) throw new Error('No identity found');
|
|
7855
8657
|
|
|
7856
8658
|
let endpoint = null;
|
|
8659
|
+
let relayEndpoint = '';
|
|
7857
8660
|
try {
|
|
7858
8661
|
const resp = await fetch(`${REGISTRY_URL}/registry/v1/agent/${encodeURIComponent(targetDid)}`, { signal: AbortSignal.timeout(5000) });
|
|
7859
8662
|
if (resp.ok) {
|
|
7860
8663
|
const entry = await resp.json();
|
|
7861
|
-
const candidates = entry.endpoint_candidates || [];
|
|
8664
|
+
const candidates = entry.endpoint_candidates || entry.candidates || [];
|
|
8665
|
+
const local = candidates.find(c => c.type === 'local');
|
|
7862
8666
|
const direct = candidates.find(c => c.type === 'direct');
|
|
7863
8667
|
const relay = candidates.find(c => c.type === 'relay');
|
|
7864
|
-
endpoint = direct?.url || relay?.url || entry.endpoint;
|
|
8668
|
+
endpoint = local?.url || direct?.url || relay?.url || entry.endpoint;
|
|
8669
|
+
relayEndpoint = relay?.url || '';
|
|
7865
8670
|
}
|
|
7866
8671
|
} catch (e) {
|
|
7867
|
-
|
|
8672
|
+
log({ event: 'p2p_registry_lookup_failed', to: targetDid, error: e.message });
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8675
|
+
const tryRelayFallback = async (reason) => {
|
|
8676
|
+
const base = relayEndpoint || process.env.ATEL_RELAY || process.env.ATEL_PLATFORM || REGISTRY_URL;
|
|
8677
|
+
if (!base) throw new Error(reason || `No reachable endpoint for ${targetDid}`);
|
|
8678
|
+
const relayUrl = `${String(base).replace(/\/+$/, '')}/relay/v1/respond/${encodeURIComponent(targetDid)}`;
|
|
8679
|
+
const resp = await fetch(relayUrl, {
|
|
8680
|
+
method: 'POST',
|
|
8681
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8682
|
+
body: JSON.stringify({ sender: id.did, message: signedMsg }),
|
|
8683
|
+
signal: AbortSignal.timeout(8000),
|
|
8684
|
+
});
|
|
8685
|
+
if (!resp.ok) {
|
|
8686
|
+
const text = await resp.text().catch(() => '');
|
|
8687
|
+
throw new Error(`${reason || 'direct_send_failed'}; relay fallback ${resp.status}: ${text || 'unknown error'}`);
|
|
8688
|
+
}
|
|
8689
|
+
log({ event: 'p2p_relay_fallback_ok', to: targetDid, relay: relayUrl, reason });
|
|
8690
|
+
return { status: 'queued_via_relay', relay: relayUrl };
|
|
8691
|
+
};
|
|
8692
|
+
|
|
8693
|
+
if (!endpoint) {
|
|
8694
|
+
return await tryRelayFallback(`no_reachable_endpoint_for_${targetDid}`);
|
|
7868
8695
|
}
|
|
7869
|
-
if (!endpoint) throw new Error(`No reachable endpoint for ${targetDid}`);
|
|
7870
8696
|
|
|
7871
8697
|
const hsManager = new HandshakeManager(id);
|
|
7872
8698
|
const client = new AgentClient(id);
|
|
7873
|
-
|
|
8699
|
+
try {
|
|
8700
|
+
return await client.sendTask(endpoint, signedMsg, hsManager);
|
|
8701
|
+
} catch (e) {
|
|
8702
|
+
log({ event: 'p2p_direct_send_failed', to: targetDid, endpoint, error: e.message });
|
|
8703
|
+
return await tryRelayFallback(`direct_send_failed:${e.message}`);
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8706
|
+
|
|
8707
|
+
async function sendPortalMessage(targetDid, messageBody, options = {}) {
|
|
8708
|
+
const id = loadIdentity();
|
|
8709
|
+
if (!id) throw new Error('No identity found');
|
|
8710
|
+
|
|
8711
|
+
const relayEndpoint = String(options.relayEndpoint || '').trim();
|
|
8712
|
+
const directEndpoint = String(options.directEndpoint || '').trim();
|
|
8713
|
+
const signedEnvelope = options.signedEnvelope || null;
|
|
8714
|
+
|
|
8715
|
+
const tryRelayFallback = async (reason) => {
|
|
8716
|
+
const base = relayEndpoint || process.env.ATEL_RELAY || process.env.ATEL_PLATFORM || REGISTRY_URL;
|
|
8717
|
+
if (!base || !targetDid) throw new Error(reason || `No reachable endpoint for ${targetDid || 'target'}`);
|
|
8718
|
+
const relayUrl = `${String(base).replace(/\/+$/, '')}/relay/v1/respond/${encodeURIComponent(targetDid)}`;
|
|
8719
|
+
const resp = await fetch(relayUrl, {
|
|
8720
|
+
method: 'POST',
|
|
8721
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8722
|
+
body: JSON.stringify({ sender: id.did, message: messageBody }),
|
|
8723
|
+
signal: AbortSignal.timeout(8000),
|
|
8724
|
+
});
|
|
8725
|
+
const raw = await resp.text().catch(() => '');
|
|
8726
|
+
let parsed = null;
|
|
8727
|
+
try { parsed = raw ? JSON.parse(raw) : null; } catch {}
|
|
8728
|
+
if (!resp.ok) throw new Error(`${reason || 'direct_send_failed'}; relay fallback ${resp.status}: ${raw || 'unknown error'}`);
|
|
8729
|
+
log({ event: 'p2p_message_relay_fallback_ok', to: targetDid, relay: relayUrl, reason });
|
|
8730
|
+
return { status: 'queued_via_relay', relay: relayUrl, result: parsed };
|
|
8731
|
+
};
|
|
8732
|
+
|
|
8733
|
+
if (directEndpoint) {
|
|
8734
|
+
const directUrl = `${String(directEndpoint).replace(/\/+$/, '')}/atel/v1/task`;
|
|
8735
|
+
try {
|
|
8736
|
+
const resp = await fetch(directUrl, {
|
|
8737
|
+
method: 'POST',
|
|
8738
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8739
|
+
body: JSON.stringify(signedEnvelope || messageBody),
|
|
8740
|
+
signal: AbortSignal.timeout(8000),
|
|
8741
|
+
});
|
|
8742
|
+
const raw = await resp.text().catch(() => '');
|
|
8743
|
+
let parsed = null;
|
|
8744
|
+
try { parsed = raw ? JSON.parse(raw) : null; } catch {}
|
|
8745
|
+
if (!resp.ok) {
|
|
8746
|
+
throw new Error(`direct_send ${resp.status}: ${raw || 'unknown error'}`);
|
|
8747
|
+
}
|
|
8748
|
+
if (parsed && (parsed.status === 'rejected' || parsed.status === 'error')) {
|
|
8749
|
+
return { status: parsed.status, direct: directUrl, result: parsed };
|
|
8750
|
+
}
|
|
8751
|
+
return { status: 'delivered', direct: directUrl, result: parsed };
|
|
8752
|
+
} catch (e) {
|
|
8753
|
+
log({ event: 'p2p_message_direct_send_failed', to: targetDid || directEndpoint, endpoint: directEndpoint, error: e.message });
|
|
8754
|
+
if (targetDid) {
|
|
8755
|
+
return await tryRelayFallback(`direct_send_failed:${e.message}`);
|
|
8756
|
+
}
|
|
8757
|
+
throw e;
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
|
|
8761
|
+
return await tryRelayFallback(`no_reachable_endpoint_for_${targetDid}`);
|
|
7874
8762
|
}
|
|
7875
8763
|
|
|
7876
8764
|
// ─── Send Command (Rich Media P2P Messaging) ─────────────────────
|
|
7877
8765
|
|
|
7878
8766
|
function showSendHelp() {
|
|
7879
8767
|
console.log(`
|
|
7880
|
-
|
|
8768
|
+
atel send — Send a message (with optional rich media) to another agent
|
|
7881
8769
|
|
|
7882
8770
|
Usage:
|
|
7883
8771
|
atel send <did|alias|endpoint> "message text"
|
|
@@ -7900,6 +8788,10 @@ Notes:
|
|
|
7900
8788
|
and ask the other side to accept with:
|
|
7901
8789
|
atel friend accept <request-id>
|
|
7902
8790
|
|
|
8791
|
+
Some agents do not expose the "general" capability needed for free-form
|
|
8792
|
+
chat messages. If you get an "outside capability boundary" rejection,
|
|
8793
|
+
enable "general" on the recipient or use atel task / atel order instead.
|
|
8794
|
+
|
|
7903
8795
|
To disable the friend requirement on your own agent, set in policy.json:
|
|
7904
8796
|
{ "relationshipPolicy": { "defaultMode": "open" } }
|
|
7905
8797
|
|
|
@@ -7962,6 +8854,8 @@ async function cmdSend(args) {
|
|
|
7962
8854
|
// Resolve target → endpoint + remoteDid
|
|
7963
8855
|
let remoteEndpoint = target;
|
|
7964
8856
|
let remoteDid;
|
|
8857
|
+
let registryEntry = null;
|
|
8858
|
+
let remoteRelayEndpoint = '';
|
|
7965
8859
|
|
|
7966
8860
|
// Resolve @alias
|
|
7967
8861
|
try { const r = resolveDID(target); if (r !== target) remoteEndpoint = r; } catch (_) {}
|
|
@@ -7979,11 +8873,14 @@ async function cmdSend(args) {
|
|
|
7979
8873
|
if (results.length > 0) entry = results[0];
|
|
7980
8874
|
}
|
|
7981
8875
|
if (!entry) { console.error(`Agent not found: ${target}`); process.exit(1); }
|
|
8876
|
+
registryEntry = entry;
|
|
7982
8877
|
remoteDid = entry.did;
|
|
7983
|
-
const candidates = entry.endpoint_candidates || [];
|
|
8878
|
+
const candidates = entry.endpoint_candidates || entry.candidates || [];
|
|
8879
|
+
const local = candidates.find(c => c.type === 'local');
|
|
7984
8880
|
const direct = candidates.find(c => c.type === 'direct');
|
|
7985
8881
|
const relay = candidates.find(c => c.type === 'relay');
|
|
7986
|
-
|
|
8882
|
+
remoteRelayEndpoint = relay?.url || '';
|
|
8883
|
+
remoteEndpoint = local?.url || direct?.url || (entry.endpoint && entry.endpoint !== remoteRelayEndpoint ? entry.endpoint : '');
|
|
7987
8884
|
} else {
|
|
7988
8885
|
remoteDid = target.startsWith('did:') ? target : undefined;
|
|
7989
8886
|
// Try to get remoteDid from handshake cache for direct endpoints
|
|
@@ -8001,14 +8898,38 @@ async function cmdSend(args) {
|
|
|
8001
8898
|
process.exit(1);
|
|
8002
8899
|
}
|
|
8003
8900
|
|
|
8901
|
+
// IM-style messages are relationship-gated, not capability-gated.
|
|
8902
|
+
|
|
8004
8903
|
// Send
|
|
8005
8904
|
try {
|
|
8006
|
-
const
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8905
|
+
const messagePayload = {
|
|
8906
|
+
msgType: 'message',
|
|
8907
|
+
msgId,
|
|
8908
|
+
text,
|
|
8909
|
+
images: payload.images,
|
|
8910
|
+
attachments: payload.attachments,
|
|
8911
|
+
timestamp: payload.timestamp,
|
|
8912
|
+
};
|
|
8913
|
+
const msg = createMessage({
|
|
8914
|
+
type: 'task_delegate',
|
|
8915
|
+
from: id.did,
|
|
8916
|
+
to: remoteDid || remoteEndpoint,
|
|
8917
|
+
payload: messagePayload,
|
|
8918
|
+
secretKey: id.secretKey,
|
|
8919
|
+
});
|
|
8920
|
+
const messageBody = {
|
|
8921
|
+
...messagePayload,
|
|
8922
|
+
from: msg.from,
|
|
8923
|
+
to: msg.to,
|
|
8924
|
+
type: msg.type,
|
|
8925
|
+
payload: msg.payload,
|
|
8926
|
+
nonce: msg.nonce,
|
|
8927
|
+
envelope: msg.envelope,
|
|
8928
|
+
signature: msg.signature,
|
|
8929
|
+
timestamp: msg.timestamp,
|
|
8930
|
+
};
|
|
8931
|
+
const result = await sendPortalMessage(remoteDid, messageBody, { directEndpoint: remoteEndpoint, relayEndpoint: remoteRelayEndpoint, signedEnvelope: msg });
|
|
8010
8932
|
|
|
8011
|
-
// Surface friendly errors
|
|
8012
8933
|
const inner = result?.result;
|
|
8013
8934
|
if (inner?.code === 'NOT_FRIEND' || inner?.error?.includes('NOT_FRIEND') || inner?.error?.includes('not a friend')) {
|
|
8014
8935
|
if (isJson) {
|
|
@@ -8020,19 +8941,28 @@ async function cmdSend(args) {
|
|
|
8020
8941
|
}
|
|
8021
8942
|
process.exit(1);
|
|
8022
8943
|
}
|
|
8023
|
-
if (inner?.status === 'rejected') {
|
|
8944
|
+
if (inner?.status === 'rejected' || result?.status === 'rejected') {
|
|
8945
|
+
const rejectionMessage = inner?.error || 'Message rejected';
|
|
8024
8946
|
if (isJson) {
|
|
8025
|
-
console.log(JSON.stringify({
|
|
8947
|
+
console.log(JSON.stringify({
|
|
8948
|
+
status: 'error',
|
|
8949
|
+
code: inner?.code,
|
|
8950
|
+
message: rejectionMessage,
|
|
8951
|
+
hint: inner?.hint,
|
|
8952
|
+
result
|
|
8953
|
+
}));
|
|
8026
8954
|
} else {
|
|
8027
|
-
console.error(
|
|
8955
|
+
console.error('✗ Message rejected: ' + rejectionMessage);
|
|
8956
|
+
if (inner?.hint) console.error(' ' + inner.hint);
|
|
8028
8957
|
}
|
|
8029
8958
|
process.exit(1);
|
|
8030
8959
|
}
|
|
8031
8960
|
|
|
8032
8961
|
if (isJson) {
|
|
8033
|
-
console.log(JSON.stringify({ status: 'sent', msgId, to: remoteDid || remoteEndpoint, attachments: (payload.images?.length || 0) + (payload.attachments?.length || 0), result }, null, 2));
|
|
8962
|
+
console.log(JSON.stringify({ status: 'sent', msgId, to: remoteDid || remoteEndpoint, attachments: (payload.images?.length || 0) + (payload.attachments?.length || 0), delivery: result }, null, 2));
|
|
8034
8963
|
} else {
|
|
8035
|
-
|
|
8964
|
+
const deliveryLabel = result?.status === 'queued_via_relay' ? 'queued via relay' : 'sent';
|
|
8965
|
+
console.log(`✓ Message ${deliveryLabel} to ${remoteDid || remoteEndpoint}`);
|
|
8036
8966
|
if (payload.images?.length) console.log(` Images: ${payload.images.length}`);
|
|
8037
8967
|
if (payload.attachments?.length) console.log(` Attachments: ${payload.attachments.length}`);
|
|
8038
8968
|
}
|
|
@@ -8392,130 +9322,6 @@ async function cmdAliasRemove(args) {
|
|
|
8392
9322
|
|
|
8393
9323
|
// ─── Main ────────────────────────────────────────────────────────
|
|
8394
9324
|
|
|
8395
|
-
// ── Fast-coop: Executor auto-submit deliverable to Fast escrow ──────
|
|
8396
|
-
// Called when executor receives milestone_verified with allComplete=true
|
|
8397
|
-
// for a fast-coop order. Signs Escrow::Submit with the agent's DID
|
|
8398
|
-
// private key (= Fast Ed25519 account key).
|
|
8399
|
-
async function submitFastDeliverable(orderId, identity) {
|
|
8400
|
-
try {
|
|
8401
|
-
// 1. Get order info
|
|
8402
|
-
const orderResp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, { signal: AbortSignal.timeout(10000) });
|
|
8403
|
-
if (!orderResp.ok) throw new Error(`order fetch failed: ${orderResp.status}`);
|
|
8404
|
-
const orderInfo = await orderResp.json();
|
|
8405
|
-
const chain = orderInfo.chain || orderInfo.Chain || '';
|
|
8406
|
-
if (chain !== 'fast-coop') return;
|
|
8407
|
-
|
|
8408
|
-
// 2. Find escrow job_id from chain-records
|
|
8409
|
-
const recordsResp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, {
|
|
8410
|
-
signal: AbortSignal.timeout(10000),
|
|
8411
|
-
});
|
|
8412
|
-
// The job_id is stored in the on_chain_records by the Platform.
|
|
8413
|
-
// We need to get it from the order or from the chain-records endpoint.
|
|
8414
|
-
// For now, compute it the same way Platform does: keccak256(orderId)
|
|
8415
|
-
const { createHash } = await import('node:crypto');
|
|
8416
|
-
|
|
8417
|
-
// 3. Get the agent's Fast account info
|
|
8418
|
-
const FAST_RPC = process.env.ATEL_FASTCOOP_RPC_URL || 'https://staging.api.fast.xyz/proxy-rest';
|
|
8419
|
-
const pubKeyHex = Buffer.from(identity.publicKey).toString('hex');
|
|
8420
|
-
|
|
8421
|
-
const accountResp = await fetch(`${FAST_RPC}/v1/accounts/${pubKeyHex}`, {
|
|
8422
|
-
signal: AbortSignal.timeout(10000),
|
|
8423
|
-
});
|
|
8424
|
-
if (!accountResp.ok) throw new Error(`Fast account query failed: ${accountResp.status}`);
|
|
8425
|
-
const accountData = (await accountResp.json()).data;
|
|
8426
|
-
const nonce = accountData.next_nonce;
|
|
8427
|
-
|
|
8428
|
-
// 4. Find escrow job for this order (query by provider)
|
|
8429
|
-
const jobsResp = await fetch(`${FAST_RPC}/v1/escrow-jobs?provider=${pubKeyHex}&status=Funded`, {
|
|
8430
|
-
signal: AbortSignal.timeout(10000),
|
|
8431
|
-
});
|
|
8432
|
-
if (!jobsResp.ok) throw new Error(`escrow jobs query failed: ${jobsResp.status}`);
|
|
8433
|
-
const jobs = (await jobsResp.json()).data || [];
|
|
8434
|
-
const job = jobs.find(j => j.description && j.description.includes(orderId));
|
|
8435
|
-
if (!job) {
|
|
8436
|
-
log({ event: 'fast_submit_no_job', orderId, note: 'no Funded escrow job found for this order' });
|
|
8437
|
-
return;
|
|
8438
|
-
}
|
|
8439
|
-
|
|
8440
|
-
// 5. Build Escrow::Submit transaction
|
|
8441
|
-
// deliverable = keccak256(orderId) as 32-byte hash
|
|
8442
|
-
const deliverableHash = createHash('sha3-256').update(orderId).digest('hex');
|
|
8443
|
-
|
|
8444
|
-
// BCS encode: Operation::Escrow(12) → Escrow::Submit(2) → jobId(32) + deliverable(32)
|
|
8445
|
-
const jobIdBytes = Buffer.from(job.job_id, 'hex');
|
|
8446
|
-
const deliverableBytes = Buffer.from(deliverableHash, 'hex');
|
|
8447
|
-
|
|
8448
|
-
// Build minimal BCS by hand (matching Go implementation):
|
|
8449
|
-
// ULEB128(12) = 0x0c, ULEB128(2) = 0x02, then 32+32 bytes
|
|
8450
|
-
const claimBytes = Buffer.concat([
|
|
8451
|
-
Buffer.from([0x0c]), // Operation::Escrow variant 12
|
|
8452
|
-
Buffer.from([0x02]), // Escrow::Submit variant 2
|
|
8453
|
-
jobIdBytes.subarray(0, 32), // job_id 32 bytes
|
|
8454
|
-
deliverableBytes.subarray(0, 32), // deliverable 32 bytes
|
|
8455
|
-
]);
|
|
8456
|
-
|
|
8457
|
-
// Transaction BCS: VersionedTransaction variant 1 (Release20260407)
|
|
8458
|
-
const networkId = process.env.ATEL_FASTCOOP_NETWORK_ID || 'fast:devnet';
|
|
8459
|
-
const senderBytes = Buffer.from(identity.publicKey);
|
|
8460
|
-
const tsNanos = BigInt(Date.now()) * 1000000n;
|
|
8461
|
-
|
|
8462
|
-
// ULEB128 encoder
|
|
8463
|
-
const uleb128 = (v) => {
|
|
8464
|
-
const buf = [];
|
|
8465
|
-
do { let b = Number(v & 0x7fn); v >>= 7n; if (v > 0n) b |= 0x80; buf.push(b); } while (v > 0n);
|
|
8466
|
-
return Buffer.from(buf.length ? buf : [0]);
|
|
8467
|
-
};
|
|
8468
|
-
const u64le = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(v)); return b; };
|
|
8469
|
-
const u128le = (v) => { const b = Buffer.alloc(16); b.writeBigUInt64LE(v & 0xffffffffffffffffn); b.writeBigUInt64LE(v >> 64n, 8); return b; };
|
|
8470
|
-
const bcsString = (s) => { const b = Buffer.from(s, 'utf-8'); return Buffer.concat([uleb128(BigInt(b.length)), b]); };
|
|
8471
|
-
|
|
8472
|
-
// Transaction body
|
|
8473
|
-
const txBody = Buffer.concat([
|
|
8474
|
-
bcsString(networkId),
|
|
8475
|
-
senderBytes.subarray(0, 32),
|
|
8476
|
-
u64le(nonce),
|
|
8477
|
-
u128le(tsNanos),
|
|
8478
|
-
uleb128(1n), // claims vector length = 1
|
|
8479
|
-
claimBytes,
|
|
8480
|
-
Buffer.from([0x00]), // archival = false
|
|
8481
|
-
Buffer.from([0x00]), // fee_token = None
|
|
8482
|
-
]);
|
|
8483
|
-
|
|
8484
|
-
// VersionedTransaction = enum variant 1
|
|
8485
|
-
const versionedTx = Buffer.concat([uleb128(1n), txBody]);
|
|
8486
|
-
|
|
8487
|
-
// 6. Sign
|
|
8488
|
-
const { default: nacl } = await import('tweetnacl');
|
|
8489
|
-
const sigMessage = Buffer.concat([Buffer.from('VersionedTransaction::'), versionedTx]);
|
|
8490
|
-
const signature = nacl.sign.detached(sigMessage, identity.secretKey);
|
|
8491
|
-
|
|
8492
|
-
// SignatureOrMultiSig::Signature = variant 0 + 64 bytes
|
|
8493
|
-
const bcsSig = Buffer.concat([Buffer.from([0x00]), Buffer.from(signature)]);
|
|
8494
|
-
|
|
8495
|
-
// 7. Submit to Fast
|
|
8496
|
-
const submitResp = await fetch(`${FAST_RPC}/v1/submit-transaction`, {
|
|
8497
|
-
method: 'POST',
|
|
8498
|
-
headers: { 'Content-Type': 'application/json' },
|
|
8499
|
-
body: JSON.stringify({
|
|
8500
|
-
transaction: versionedTx.toString('hex'),
|
|
8501
|
-
signature: bcsSig.toString('hex'),
|
|
8502
|
-
}),
|
|
8503
|
-
signal: AbortSignal.timeout(15000),
|
|
8504
|
-
});
|
|
8505
|
-
|
|
8506
|
-
const submitResult = await submitResp.json();
|
|
8507
|
-
if (submitResult.error) {
|
|
8508
|
-
throw new Error(`Fast submit failed: ${submitResult.error.message || JSON.stringify(submitResult.error)}`);
|
|
8509
|
-
}
|
|
8510
|
-
|
|
8511
|
-
log({ event: 'fast_submit_success', orderId, jobId: job.job_id, nonce });
|
|
8512
|
-
console.log(`⚡ [Fast] Deliverable submitted for ${orderId} (job: ${job.job_id.substring(0, 16)}...)`);
|
|
8513
|
-
} catch (e) {
|
|
8514
|
-
log({ event: 'fast_submit_error', orderId, error: e.message });
|
|
8515
|
-
console.error(`⚠️ [Fast] Submit failed for ${orderId}: ${e.message}`);
|
|
8516
|
-
}
|
|
8517
|
-
}
|
|
8518
|
-
|
|
8519
9325
|
const [,, cmd, ...rawArgs] = process.argv;
|
|
8520
9326
|
const args = rawArgs.filter(a => !a.startsWith('--'));
|
|
8521
9327
|
const commands = {
|
|
@@ -8726,6 +9532,7 @@ const commands = {
|
|
|
8726
9532
|
createdAt: new Date().toISOString(), lastUsedAt: null,
|
|
8727
9533
|
});
|
|
8728
9534
|
saveNotifyTargets(targets);
|
|
9535
|
+
rememberTelegramRoute(chatId, botToken || undefined);
|
|
8729
9536
|
console.log(`✅ Bound TG chat ${chatId} as notification target (id: ${id})`);
|
|
8730
9537
|
if (!botToken) console.log('⚠️ No bot token found. Set TELEGRAM_BOT_TOKEN or use --bot-token');
|
|
8731
9538
|
return;
|
|
@@ -8845,7 +9652,7 @@ Protocol Commands:
|
|
|
8845
9652
|
setup [port] Configure network (detect IP, UPnP, verify)
|
|
8846
9653
|
verify Verify port reachability
|
|
8847
9654
|
start [port] Start endpoint (auto network + auto register)
|
|
8848
|
-
inbox [count] Show received messages (default: 20)
|
|
9655
|
+
inbox [count] Show received messages / notifications (default: 20)
|
|
8849
9656
|
register [name] [caps] [endpoint] Register on public registry (caps: "type1:price1,type2:price2" or "type1,type2" for free)
|
|
8850
9657
|
search <capability> Search registry for agents (shows pricing info)
|
|
8851
9658
|
handshake <endpoint> [did] Handshake with remote agent
|