@lawrenceliang-btc/atel-sdk 1.2.11 → 1.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/atel.mjs +773 -134
- package/bin/notification-action-helpers.mjs +18 -1
- package/bin/order-cancel-helpers.mjs +77 -0
- package/package.json +2 -2
- package/skill/atel-agent/SKILL.md +3 -3
- package/skill/atel-agent/setup.sh +1 -1
- package/skill/references/quickstart.md +1 -1
package/bin/atel.mjs
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
} from '@lawrenceliang-btc/atel-sdk';
|
|
68
68
|
import { TunnelManager, HeartbeatManager } from './tunnel-manager.mjs';
|
|
69
69
|
import { buildAgentCallbackAction, getDirectExecutableActions, normalizeGatewayBind, shouldSkipAgentHook, shouldUseGatewaySession } from './notification-action-helpers.mjs';
|
|
70
|
+
import { parseOrderCancelArgs, preflightOrderCancel } from './order-cancel-helpers.mjs';
|
|
70
71
|
// ollama-manager removed — SDK does not run local models
|
|
71
72
|
const initializeOllama = async () => {};
|
|
72
73
|
const getOllamaStatus = async () => ({ running: false, models: [] });
|
|
@@ -74,9 +75,69 @@ import { parseAttachmentFlags, processAttachments } from './atel-attachment-help
|
|
|
74
75
|
|
|
75
76
|
const ATEL_DIR = resolve(process.env.ATEL_DIR || '.atel');
|
|
76
77
|
const IDENTITY_FILE = resolve(ATEL_DIR, 'identity.json');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
78
|
+
|
|
79
|
+
function readDefaultPlatformBase() {
|
|
80
|
+
const explicit = process.env.ATEL_PLATFORM || process.env.ATEL_API || process.env.ATEL_REGISTRY || '';
|
|
81
|
+
if (String(explicit).trim()) return String(explicit).trim().replace(/\/+$/, '');
|
|
82
|
+
try {
|
|
83
|
+
const networkFile = resolve(ATEL_DIR, 'network.json');
|
|
84
|
+
if (existsSync(networkFile)) {
|
|
85
|
+
const network = JSON.parse(readFileSync(networkFile, 'utf-8'));
|
|
86
|
+
const candidate = [network?.relayUrl, network?.platformUrl, network?.registryUrl].find((value) => typeof value === 'string' && value.trim());
|
|
87
|
+
if (candidate) return String(candidate).trim().replace(/\/+$/, '');
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
return 'https://api.atelai.org';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DEFAULT_PLATFORM_BASE = readDefaultPlatformBase();
|
|
94
|
+
const ATEL_PLATFORM = process.env.ATEL_PLATFORM || process.env.ATEL_API || process.env.ATEL_REGISTRY || DEFAULT_PLATFORM_BASE;
|
|
95
|
+
const REGISTRY_URL = process.env.ATEL_REGISTRY || ATEL_PLATFORM || DEFAULT_PLATFORM_BASE;
|
|
96
|
+
const ATEL_RELAY = process.env.ATEL_RELAY || DEFAULT_PLATFORM_BASE;
|
|
97
|
+
|
|
98
|
+
function normalizeUrl(value = '') {
|
|
99
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getEnvironmentProfile(url = '') {
|
|
103
|
+
const normalized = normalizeUrl(url);
|
|
104
|
+
if (!normalized) return { name: 'unknown', label: 'unknown', url: normalized };
|
|
105
|
+
try {
|
|
106
|
+
const parsed = new URL(normalized);
|
|
107
|
+
const host = String(parsed.hostname || '').toLowerCase();
|
|
108
|
+
if (host === 'api.atelai.org') return { name: 'production', label: 'production', url: normalized };
|
|
109
|
+
if (host === '127.0.0.1' || host === 'localhost') return { name: 'test', label: 'local-test', url: normalized };
|
|
110
|
+
if (host === '43.160.230.129' || host === '43.160.211.180') return { name: 'test', label: 'host-test', url: normalized };
|
|
111
|
+
return { name: 'custom', label: host || 'custom', url: normalized };
|
|
112
|
+
} catch {
|
|
113
|
+
return { name: 'custom', label: normalized, url: normalized };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getCurrentEnvironmentSummary() {
|
|
118
|
+
const platform = getEnvironmentProfile(ATEL_PLATFORM);
|
|
119
|
+
const registry = getEnvironmentProfile(REGISTRY_URL);
|
|
120
|
+
const relay = getEnvironmentProfile(ATEL_RELAY);
|
|
121
|
+
const explicit = Boolean(String(process.env.ATEL_PLATFORM || process.env.ATEL_API || process.env.ATEL_REGISTRY || process.env.ATEL_RELAY || '').trim());
|
|
122
|
+
return {
|
|
123
|
+
profile: platform.name,
|
|
124
|
+
platform,
|
|
125
|
+
registry,
|
|
126
|
+
relay,
|
|
127
|
+
explicit,
|
|
128
|
+
atelDir: ATEL_DIR,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ensureProductionAuthTarget() {
|
|
133
|
+
const env = getCurrentEnvironmentSummary();
|
|
134
|
+
if (env.platform.name === 'production') return env;
|
|
135
|
+
console.error(`Authorization blocked: current ATEL_DIR is targeting ${env.platform.label} (${env.platform.url || 'unknown'}), not production.`);
|
|
136
|
+
console.error(`ATEL_DIR: ${env.atelDir}`);
|
|
137
|
+
console.error('To authorize a code from https://atelai.org/login, run with explicit production endpoints:');
|
|
138
|
+
console.error(' ATEL_PLATFORM=https://api.atelai.org ATEL_REGISTRY=https://api.atelai.org ATEL_RELAY=https://api.atelai.org atel auth <code>');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
80
141
|
const ATEL_NOTIFY_GATEWAY = process.env.ATEL_NOTIFY_GATEWAY || process.env.OPENCLAW_GATEWAY_URL || '';
|
|
81
142
|
const ATEL_NOTIFY_TARGET = process.env.ATEL_NOTIFY_TARGET || '';
|
|
82
143
|
const ATEL_NOTIFY_AUTO_BIND = /^(1|true|yes)$/i.test(String(process.env.ATEL_NOTIFY_AUTO_BIND || ''));
|
|
@@ -92,8 +153,13 @@ const PENDING_FILE = resolve(ATEL_DIR, 'pending-tasks.json');
|
|
|
92
153
|
const RESULT_PUSH_QUEUE_FILE = resolve(ATEL_DIR, 'pending-result-pushes.json');
|
|
93
154
|
const NOTIFY_TARGETS_FILE = resolve(ATEL_DIR, 'notify-targets.json');
|
|
94
155
|
const NOTIFY_ROUTES_FILE = resolve(ATEL_DIR, 'notify-routes.json');
|
|
156
|
+
const NOTIFY_CONSENTS_FILE = resolve(ATEL_DIR, 'notify-consents.json');
|
|
95
157
|
const TELEGRAM_UPDATES_STATE_FILE = resolve(process.env.HOME || '/root', '.openclaw', 'deploy', 'sdk-telegram-updates.json');
|
|
96
158
|
const TELEGRAM_BINDINGS_FILE = resolve(process.env.HOME || '/root', '.openclaw', 'deploy', 'sdk-telegram-bindings.json');
|
|
159
|
+
const OPENCLAW_CONFIG_FILE = resolve(process.env.HOME || '/root', '.openclaw', 'openclaw.json');
|
|
160
|
+
const OPENCLAW_TG_OFFSET_FILE = resolve(process.env.HOME || '/root', '.openclaw', 'telegram', 'update-offset-default.json');
|
|
161
|
+
const OPENCLAW_GATEWAY_SYSTEMD_FILE = resolve(process.env.HOME || '/root', '.config', 'systemd', 'user', 'openclaw-gateway.service');
|
|
162
|
+
const OPENCLAW_GATEWAY_SYSTEMD_OVERRIDE_FILE = resolve(process.env.HOME || '/root', '.config', 'systemd', 'user', 'openclaw-gateway.service.d', 'env.conf');
|
|
97
163
|
const TRADE_TRACK_FILE = resolve(ATEL_DIR, 'tracked-orders.json');
|
|
98
164
|
const P2P_STATUS_FILE = resolve(ATEL_DIR, 'p2p-task-status.jsonl');
|
|
99
165
|
const PENDING_AGENT_CALLBACKS_FILE = resolve(ATEL_DIR, 'pending-agent-callbacks.json');
|
|
@@ -107,6 +173,47 @@ const DEFAULT_POLICY = { rateLimit: 60, maxPayloadBytes: 1048576, maxConcurrent:
|
|
|
107
173
|
|
|
108
174
|
function ensureDir() { if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true }); }
|
|
109
175
|
|
|
176
|
+
function isPidAlive(pid) {
|
|
177
|
+
const n = Number(pid || 0);
|
|
178
|
+
if (!Number.isFinite(n) || n <= 0) return false;
|
|
179
|
+
try { process.kill(n, 0); return true; }
|
|
180
|
+
catch { return false; }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getStartInstanceLockFile(port) {
|
|
184
|
+
return resolve(ATEL_DIR, `start-instance-${String(port || '3100')}.lock.json`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function acquireStartInstanceLock(port) {
|
|
188
|
+
ensureDir();
|
|
189
|
+
const file = getStartInstanceLockFile(port);
|
|
190
|
+
let existing = null;
|
|
191
|
+
if (existsSync(file)) {
|
|
192
|
+
try { existing = JSON.parse(readFileSync(file, 'utf-8')); } catch {}
|
|
193
|
+
}
|
|
194
|
+
if (existing?.pid && existing.pid !== process.pid && isPidAlive(existing.pid)) {
|
|
195
|
+
return { ok: false, file, existing };
|
|
196
|
+
}
|
|
197
|
+
const payload = {
|
|
198
|
+
pid: process.pid,
|
|
199
|
+
port: Number(port || 0),
|
|
200
|
+
did: (() => { try { return requireIdentity().did; } catch { return ''; } })(),
|
|
201
|
+
startedAt: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
writeFileSync(file, JSON.stringify(payload, null, 2));
|
|
204
|
+
return { ok: true, file, existing };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function releaseStartInstanceLock(port) {
|
|
208
|
+
const file = getStartInstanceLockFile(port);
|
|
209
|
+
if (!existsSync(file)) return false;
|
|
210
|
+
try {
|
|
211
|
+
const current = JSON.parse(readFileSync(file, 'utf-8'));
|
|
212
|
+
if (Number(current?.pid || 0) !== process.pid) return false;
|
|
213
|
+
} catch {}
|
|
214
|
+
try { unlinkSync(file); return true; } catch { return false; }
|
|
215
|
+
}
|
|
216
|
+
|
|
110
217
|
function ensureOrderWorkspace(orderId, context = {}) {
|
|
111
218
|
ensureDir();
|
|
112
219
|
const safeOrderId = String(orderId || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
@@ -212,6 +319,18 @@ function summarizeAgentOutput(text, maxChars = 300) {
|
|
|
212
319
|
return trimmed.substring(0, maxChars);
|
|
213
320
|
}
|
|
214
321
|
|
|
322
|
+
function isKnownInvalidLocalAgentStdout(text) {
|
|
323
|
+
const value = String(text || '').trim();
|
|
324
|
+
if (!value) return false;
|
|
325
|
+
const lowered = value.toLowerCase();
|
|
326
|
+
return lowered.includes('401 该令牌已过期')
|
|
327
|
+
|| lowered.includes('token expired')
|
|
328
|
+
|| lowered.includes('plugin register() called')
|
|
329
|
+
|| lowered.includes('plugin registration complete')
|
|
330
|
+
|| lowered.includes('session file locked')
|
|
331
|
+
|| lowered.includes('session locked');
|
|
332
|
+
}
|
|
333
|
+
|
|
215
334
|
// ═══════════════════════════════════════════════════════════════════
|
|
216
335
|
// Notification Target System — auto-discover gateway, manage targets
|
|
217
336
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -226,6 +345,86 @@ function saveNotifyTargets(data) {
|
|
|
226
345
|
writeFileSync(NOTIFY_TARGETS_FILE, JSON.stringify(data, null, 2));
|
|
227
346
|
}
|
|
228
347
|
|
|
348
|
+
function markNotifyTargetUsed(targets, deliveredTarget, usedAt) {
|
|
349
|
+
const list = Array.isArray(targets?.targets) ? targets.targets : [];
|
|
350
|
+
const id = String(deliveredTarget?.id || '').trim();
|
|
351
|
+
const channel = String(deliveredTarget?.channel || '').trim();
|
|
352
|
+
const targetValue = String(deliveredTarget?.target || '').trim();
|
|
353
|
+
for (const item of list) {
|
|
354
|
+
if (!item || typeof item !== 'object') continue;
|
|
355
|
+
if (id && String(item.id || '').trim() === id) {
|
|
356
|
+
item.lastUsedAt = usedAt;
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
if (channel && targetValue && String(item.channel || '').trim() === channel && String(item.target || '').trim() === targetValue) {
|
|
360
|
+
item.lastUsedAt = usedAt;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function loadNotifyConsents() {
|
|
368
|
+
try { return JSON.parse(readFileSync(NOTIFY_CONSENTS_FILE, 'utf-8')); }
|
|
369
|
+
catch { return { version: 1, consents: [] }; }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function saveNotifyConsents(data) {
|
|
373
|
+
ensureDir();
|
|
374
|
+
writeFileSync(NOTIFY_CONSENTS_FILE, JSON.stringify(data, null, 2));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function extractTelegramBotId(botToken = '') {
|
|
378
|
+
const token = String(botToken || '').trim();
|
|
379
|
+
return token ? (token.split(':', 1)[0] || '') : '';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function upsertNotifyConsent({ did, channel, target, botToken, source = 'notify_bind', status = 'active' }) {
|
|
383
|
+
const ownerDid = String(did || '').trim();
|
|
384
|
+
const normalizedChannel = String(channel || '').trim();
|
|
385
|
+
const normalizedTarget = String(target || '').trim();
|
|
386
|
+
if (!ownerDid || !normalizedChannel || !normalizedTarget) return false;
|
|
387
|
+
const data = loadNotifyConsents();
|
|
388
|
+
if (!Array.isArray(data.consents)) data.consents = [];
|
|
389
|
+
const now = new Date().toISOString();
|
|
390
|
+
const botId = normalizedChannel === 'telegram' ? extractTelegramBotId(botToken) : '';
|
|
391
|
+
const existing = data.consents.find((item) => item && item.did === ownerDid && item.channel === normalizedChannel && item.target === normalizedTarget);
|
|
392
|
+
if (existing) {
|
|
393
|
+
existing.status = status;
|
|
394
|
+
existing.updatedAt = now;
|
|
395
|
+
existing.revokedAt = status === 'revoked' ? (existing.revokedAt || now) : null;
|
|
396
|
+
if (status !== 'revoked') existing.consentedAt = existing.consentedAt || now;
|
|
397
|
+
existing.source = source || existing.source || 'notify_bind';
|
|
398
|
+
if (botId) existing.botId = botId;
|
|
399
|
+
} else {
|
|
400
|
+
data.consents.push({
|
|
401
|
+
did: ownerDid,
|
|
402
|
+
channel: normalizedChannel,
|
|
403
|
+
target: normalizedTarget,
|
|
404
|
+
botId: botId || undefined,
|
|
405
|
+
source,
|
|
406
|
+
status,
|
|
407
|
+
consentedAt: status === 'revoked' ? null : now,
|
|
408
|
+
revokedAt: status === 'revoked' ? now : null,
|
|
409
|
+
updatedAt: now,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
saveNotifyConsents(data);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function revokeNotifyConsent({ did, channel, target, reason = 'notify_remove' }) {
|
|
417
|
+
return upsertNotifyConsent({ did, channel, target, source: reason, status: 'revoked' });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function listNotifyConsentsForDid(did) {
|
|
421
|
+
const ownerDid = String(did || '').trim();
|
|
422
|
+
if (!ownerDid) return [];
|
|
423
|
+
const data = loadNotifyConsents();
|
|
424
|
+
const items = Array.isArray(data.consents) ? data.consents : [];
|
|
425
|
+
return items.filter((item) => item && item.did === ownerDid);
|
|
426
|
+
}
|
|
427
|
+
|
|
229
428
|
function loadNotifyRoutes() {
|
|
230
429
|
try { return JSON.parse(readFileSync(NOTIFY_ROUTES_FILE, 'utf-8')); }
|
|
231
430
|
catch { return { version: 1, defaultTelegram: null, orderBindings: {} }; }
|
|
@@ -289,24 +488,26 @@ function resolveNotifyTargets(orderId = '') {
|
|
|
289
488
|
const primary = getPrimaryTelegramTarget();
|
|
290
489
|
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
490
|
if (!preferred?.chatId) return { targets, enabled };
|
|
292
|
-
|
|
491
|
+
const bindingTarget = resolveNotifyBindTarget(preferred.chatId, preferred.botToken || discoverTelegramBot() || '');
|
|
492
|
+
let hasBoundTarget = false;
|
|
293
493
|
enabled = enabled.map((target) => {
|
|
294
|
-
if (target.channel !==
|
|
295
|
-
|
|
494
|
+
if (target.channel !== bindingTarget.channel) return target;
|
|
495
|
+
if (String(target.target) !== String(preferred.chatId)) return target;
|
|
496
|
+
hasBoundTarget = true;
|
|
296
497
|
return {
|
|
297
498
|
...target,
|
|
298
499
|
target: String(preferred.chatId),
|
|
299
|
-
botToken: preferred.botToken || target.botToken,
|
|
500
|
+
botToken: bindingTarget.channel === 'telegram' ? (preferred.botToken || target.botToken) : undefined,
|
|
300
501
|
routeSource: orderId && routes.orderBindings && routes.orderBindings[orderId] ? 'order' : 'default',
|
|
301
502
|
};
|
|
302
503
|
});
|
|
303
|
-
if (!
|
|
504
|
+
if (!hasBoundTarget) {
|
|
304
505
|
enabled = [{
|
|
305
|
-
id:
|
|
306
|
-
channel:
|
|
506
|
+
id: bindingTarget.id,
|
|
507
|
+
channel: bindingTarget.channel,
|
|
307
508
|
target: String(preferred.chatId),
|
|
308
|
-
botToken: preferred.botToken || discoverTelegramBot() || undefined,
|
|
309
|
-
label: orderId ?
|
|
509
|
+
botToken: bindingTarget.channel === 'telegram' ? (preferred.botToken || discoverTelegramBot() || undefined) : undefined,
|
|
510
|
+
label: orderId ? 'order:' + orderId : 'owner',
|
|
310
511
|
enabled: true,
|
|
311
512
|
createdAt: preferred.updatedAt || new Date().toISOString(),
|
|
312
513
|
lastUsedAt: null,
|
|
@@ -351,45 +552,308 @@ function autoBindNotifications() {
|
|
|
351
552
|
if (!chatId) return;
|
|
352
553
|
|
|
353
554
|
const botToken = routes.defaultTelegram?.botToken || discoverTelegramBot();
|
|
354
|
-
const
|
|
555
|
+
const bindingTarget = resolveNotifyBindTarget(chatId, botToken || '');
|
|
355
556
|
targets.targets.push({
|
|
356
|
-
id
|
|
357
|
-
|
|
557
|
+
id: bindingTarget.id,
|
|
558
|
+
channel: bindingTarget.channel,
|
|
559
|
+
target: String(chatId),
|
|
560
|
+
botToken: bindingTarget.channel === 'telegram' ? (botToken || undefined) : undefined,
|
|
358
561
|
label: 'owner', enabled: true,
|
|
359
562
|
createdAt: new Date().toISOString(), lastUsedAt: null,
|
|
360
563
|
});
|
|
361
564
|
saveNotifyTargets(targets);
|
|
362
565
|
rememberTelegramRoute(chatId, botToken || undefined);
|
|
363
|
-
|
|
566
|
+
try { upsertNotifyConsent({ did: requireIdentity().did, channel: 'telegram', target: String(chatId), botToken, source: 'auto_bind', status: 'active' }); } catch {}
|
|
567
|
+
console.log(`🔔 Auto-bound ${bindingTarget.channel} notifications to chat ${chatId}`);
|
|
364
568
|
}
|
|
365
569
|
|
|
366
570
|
// Auto-discover OpenClaw gateway: read token + find port
|
|
571
|
+
function loadOpenClawGatewayRuntimeHints() {
|
|
572
|
+
const hints = { ports: [], binds: [], urls: [] };
|
|
573
|
+
const addPort = (value) => {
|
|
574
|
+
const port = Number.parseInt(String(value || '').trim(), 10);
|
|
575
|
+
if (Number.isFinite(port) && port > 0 && !hints.ports.includes(port)) hints.ports.push(port);
|
|
576
|
+
};
|
|
577
|
+
const addBind = (value) => {
|
|
578
|
+
const bind = normalizeGatewayBind(String(value || '').trim() || '127.0.0.1');
|
|
579
|
+
if (bind && !hints.binds.includes(bind)) hints.binds.push(bind);
|
|
580
|
+
};
|
|
581
|
+
const addUrl = (value) => {
|
|
582
|
+
const url = String(value || '').trim();
|
|
583
|
+
if (url && !hints.urls.includes(url)) hints.urls.push(url);
|
|
584
|
+
};
|
|
585
|
+
const parseRuntimeText = (content) => {
|
|
586
|
+
if (!content) return;
|
|
587
|
+
for (const match of content.matchAll(/(?:--port|OPENCLAW_GATEWAY_PORT=)(\d+)/g)) addPort(match[1]);
|
|
588
|
+
for (const match of content.matchAll(/(?:--host|--bind)\s+([^\s"']+)/g)) addBind(match[1]);
|
|
589
|
+
for (const match of content.matchAll(/OPENCLAW_GATEWAY_URL=([^\s"']+)/g)) addUrl(match[1]);
|
|
590
|
+
};
|
|
591
|
+
addPort(process.env.OPENCLAW_GATEWAY_PORT);
|
|
592
|
+
addBind(process.env.OPENCLAW_GATEWAY_BIND || process.env.OPENCLAW_GATEWAY_HOST);
|
|
593
|
+
addUrl(process.env.OPENCLAW_GATEWAY_URL || process.env.ATEL_NOTIFY_GATEWAY);
|
|
594
|
+
for (const candidate of [OPENCLAW_GATEWAY_SYSTEMD_FILE, OPENCLAW_GATEWAY_SYSTEMD_OVERRIDE_FILE]) {
|
|
595
|
+
try {
|
|
596
|
+
if (existsSync(candidate)) parseRuntimeText(readFileSync(candidate, 'utf-8'));
|
|
597
|
+
} catch {}
|
|
598
|
+
}
|
|
599
|
+
return hints;
|
|
600
|
+
}
|
|
601
|
+
|
|
367
602
|
function discoverGateway() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
603
|
+
const candidateUrls = [];
|
|
604
|
+
const seen = new Set();
|
|
605
|
+
const addCandidate = (url, source) => {
|
|
606
|
+
const normalized = String(url || '').trim();
|
|
607
|
+
if (!normalized || seen.has(normalized)) return;
|
|
608
|
+
seen.add(normalized);
|
|
609
|
+
candidateUrls.push({ url: normalized, source });
|
|
610
|
+
};
|
|
611
|
+
const addPortCandidates = (port, bind, source) => {
|
|
612
|
+
const parsedPort = Number.parseInt(String(port || '').trim(), 10);
|
|
613
|
+
if (!Number.isFinite(parsedPort) || parsedPort <= 0) return;
|
|
614
|
+
const hosts = new Set([
|
|
615
|
+
normalizeGatewayBind(bind || '127.0.0.1'),
|
|
616
|
+
'127.0.0.1',
|
|
617
|
+
'localhost',
|
|
618
|
+
]);
|
|
619
|
+
for (const host of hosts) addCandidate(`http://${host}:${parsedPort}`, source);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
let token = '';
|
|
623
|
+
let cfg = null;
|
|
376
624
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
625
|
+
cfg = JSON.parse(readFileSync(OPENCLAW_CONFIG_FILE, 'utf-8'));
|
|
626
|
+
token = String(cfg?.gateway?.auth?.token || '').trim();
|
|
627
|
+
} catch {}
|
|
628
|
+
|
|
629
|
+
const explicitUrl = process.env.ATEL_NOTIFY_GATEWAY || process.env.OPENCLAW_GATEWAY_URL;
|
|
630
|
+
if (explicitUrl) addCandidate(explicitUrl, 'env_url');
|
|
631
|
+
|
|
632
|
+
const hints = loadOpenClawGatewayRuntimeHints();
|
|
633
|
+
for (const url of hints.urls) addCandidate(url, 'runtime_url');
|
|
634
|
+
for (const port of hints.ports) addPortCandidates(port, hints.binds[0], 'runtime_port');
|
|
635
|
+
|
|
636
|
+
if (cfg?.gateway?.port) addPortCandidates(cfg.gateway.port, cfg.gateway?.bind || '127.0.0.1', 'config');
|
|
637
|
+
|
|
638
|
+
const fallbackPort = Number.parseInt(String(process.env.OPENCLAW_GATEWAY_PORT || '').trim(), 10);
|
|
639
|
+
if (Number.isFinite(fallbackPort) && fallbackPort > 0) addPortCandidates(fallbackPort, process.env.OPENCLAW_GATEWAY_BIND || process.env.OPENCLAW_GATEWAY_HOST || '127.0.0.1', 'env_port');
|
|
640
|
+
|
|
641
|
+
if (candidateUrls.length === 0) addPortCandidates(18789, '127.0.0.1', 'default');
|
|
642
|
+
|
|
643
|
+
if (!token && candidateUrls.length === 0) return null;
|
|
644
|
+
return { url: candidateUrls[0]?.url || '', token, candidates: candidateUrls };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function invokeGatewayTelegramMessage(target, message, timeoutMs = 5000) {
|
|
648
|
+
const gw = discoverGateway();
|
|
649
|
+
const candidates = Array.isArray(gw?.candidates) && gw.candidates.length > 0 ? gw.candidates : (gw?.url ? [{ url: gw.url, source: 'legacy' }] : []);
|
|
650
|
+
let last = { ok: false, reason: 'missing_gateway', error: 'missing_gateway', status: 0, url: '' };
|
|
651
|
+
if (gw?.token && candidates.length > 0) {
|
|
652
|
+
for (const candidate of candidates) {
|
|
653
|
+
try {
|
|
654
|
+
const resp = await fetch(`${candidate.url}/tools/invoke`, {
|
|
655
|
+
method: 'POST',
|
|
656
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
|
|
657
|
+
body: JSON.stringify({ tool: 'message', args: { action: 'send', channel: 'telegram', message, target } }),
|
|
658
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
659
|
+
});
|
|
660
|
+
if (resp.ok) return { ok: true, status: resp.status, url: candidate.url, source: candidate.source };
|
|
661
|
+
let detail = `http_${resp.status}`;
|
|
662
|
+
try {
|
|
663
|
+
const payload = await resp.json();
|
|
664
|
+
detail = payload?.error?.message || payload?.message || detail;
|
|
665
|
+
} catch {}
|
|
666
|
+
last = { ok: false, reason: 'http_error', error: detail, status: resp.status, url: candidate.url, source: candidate.source };
|
|
667
|
+
} catch (e) {
|
|
668
|
+
last = { ok: false, reason: 'fetch_failed', error: e.message || 'fetch_failed', status: 0, url: candidate.url, source: candidate.source };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const oc = loadOpenClawConfig();
|
|
674
|
+
const botToken = String(oc?.channels?.telegram?.botToken || '').trim();
|
|
675
|
+
if (botToken) {
|
|
676
|
+
try {
|
|
677
|
+
const data = await sendTelegramBotRequest(botToken, 'sendMessage', { chat_id: target, text: message }, timeoutMs);
|
|
678
|
+
if (data?.ok !== false) {
|
|
679
|
+
return { ok: true, status: 200, url: 'telegram://openclaw-bot', source: 'openclaw_bot_fallback' };
|
|
680
|
+
}
|
|
681
|
+
last = { ok: false, reason: 'telegram_http_error', error: data?.description || 'http_200', status: 200, url: 'telegram://openclaw-bot', source: 'openclaw_bot_fallback' };
|
|
682
|
+
} catch (e) {
|
|
683
|
+
last = { ok: false, reason: 'telegram_fetch_failed', error: e.message || 'fetch_failed', status: 0, url: 'telegram://openclaw-bot', source: 'openclaw_bot_fallback' };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return last;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function sendTelegramBotRequest(botToken, method, payload, timeoutMs = 5000) {
|
|
690
|
+
const url = `https://api.telegram.org/bot${botToken}/${method}`;
|
|
691
|
+
const options = {
|
|
692
|
+
method: 'POST',
|
|
693
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
694
|
+
};
|
|
695
|
+
if (payload instanceof FormData) {
|
|
696
|
+
options.body = payload;
|
|
697
|
+
} else {
|
|
698
|
+
options.headers = { 'Content-Type': 'application/json' };
|
|
699
|
+
options.body = JSON.stringify(payload);
|
|
700
|
+
}
|
|
701
|
+
const resp = await fetch(url, options);
|
|
702
|
+
const data = await resp.json().catch(() => null);
|
|
703
|
+
if (!resp.ok || data?.ok === false) {
|
|
704
|
+
throw new Error(data?.description || `http_${resp.status}`);
|
|
705
|
+
}
|
|
706
|
+
return data;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function decodeDataUrl(dataUrl = '') {
|
|
710
|
+
const match = String(dataUrl || '').match(/^data:([^;]+);base64,(.+)$/);
|
|
711
|
+
if (!match) return null;
|
|
712
|
+
const [, mimeType, base64] = match;
|
|
713
|
+
try {
|
|
714
|
+
return { mimeType, buffer: Buffer.from(base64, 'base64') };
|
|
715
|
+
} catch {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function buildMediaCaption(message = '', payload = {}, index = 0) {
|
|
721
|
+
const base = String(message || '').trim();
|
|
722
|
+
const sender = String(payload.peerDid || '').trim();
|
|
723
|
+
const extra = index === 0 && sender ? `来自: ${sender}` : '';
|
|
724
|
+
return [base, extra].filter(Boolean).join('\n').slice(0, 900);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function fetchTelegramUploadSource(item, defaultName, defaultMimeType, timeoutMs = 5000) {
|
|
728
|
+
if (item?.kind === 'inline' && item?.data) {
|
|
729
|
+
const decoded = decodeDataUrl(item.data);
|
|
730
|
+
if (decoded?.buffer) {
|
|
731
|
+
return {
|
|
732
|
+
name: item.name || defaultName,
|
|
733
|
+
mimeType: decoded.mimeType || item.mimeType || defaultMimeType,
|
|
734
|
+
buffer: decoded.buffer,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const mediaUrl = String(item?.downloadUrl || item?.url || '').trim();
|
|
739
|
+
if (!mediaUrl) throw new Error('missing_media_url');
|
|
740
|
+
const resp = await fetch(mediaUrl, { signal: AbortSignal.timeout(timeoutMs) });
|
|
741
|
+
if (!resp.ok) throw new Error(`attachment_fetch_http_${resp.status}`);
|
|
742
|
+
const arrayBuffer = await resp.arrayBuffer();
|
|
743
|
+
return {
|
|
744
|
+
name: item?.name || defaultName,
|
|
745
|
+
mimeType: item?.mimeType || resp.headers.get('content-type') || defaultMimeType,
|
|
746
|
+
buffer: Buffer.from(arrayBuffer),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function sendTelegramPhoto(botToken, chatId, image, caption = '', timeoutMs = 5000) {
|
|
751
|
+
const source = await fetchTelegramUploadSource(image, 'image', image?.mimeType || 'image/png', timeoutMs);
|
|
752
|
+
const form = new FormData();
|
|
753
|
+
form.append('chat_id', String(chatId));
|
|
754
|
+
if (caption) form.append('caption', caption);
|
|
755
|
+
form.append('photo', new Blob([source.buffer], { type: source.mimeType || 'image/png' }), source.name || 'image');
|
|
756
|
+
return await sendTelegramBotRequest(botToken, 'sendPhoto', form, timeoutMs);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function sendTelegramDocumentLike(botToken, method, fieldName, chatId, attachment, caption = '', timeoutMs = 5000) {
|
|
760
|
+
const source = await fetchTelegramUploadSource(attachment, attachment?.name || fieldName, attachment?.mimeType || 'application/octet-stream', timeoutMs);
|
|
761
|
+
const form = new FormData();
|
|
762
|
+
form.append('chat_id', String(chatId));
|
|
763
|
+
if (caption) form.append('caption', caption);
|
|
764
|
+
form.append(fieldName, new Blob([source.buffer], { type: source.mimeType || 'application/octet-stream' }), source.name || fieldName);
|
|
765
|
+
return await sendTelegramBotRequest(botToken, method, form, timeoutMs);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function sendTelegramRichMedia(botToken, chatId, message, payload = {}, timeoutMs = 5000) {
|
|
769
|
+
const images = Array.isArray(payload.images) ? payload.images : [];
|
|
770
|
+
const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
|
|
771
|
+
let sent = false;
|
|
772
|
+
for (let i = 0; i < images.length; i++) {
|
|
773
|
+
await sendTelegramPhoto(botToken, chatId, images[i], buildMediaCaption(message, payload, i), timeoutMs);
|
|
774
|
+
sent = true;
|
|
775
|
+
}
|
|
776
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
777
|
+
const attachment = attachments[i] || {};
|
|
778
|
+
const kind = String(attachment.kind || '').toLowerCase();
|
|
779
|
+
const mimeType = String(attachment.mimeType || '').toLowerCase();
|
|
780
|
+
const caption = buildMediaCaption(message, payload, images.length + i);
|
|
781
|
+
if (kind === 'audio' || mimeType.startsWith('audio/')) {
|
|
782
|
+
try {
|
|
783
|
+
await sendTelegramDocumentLike(botToken, 'sendAudio', 'audio', chatId, attachment, caption, timeoutMs);
|
|
784
|
+
} catch {
|
|
785
|
+
await sendTelegramDocumentLike(botToken, 'sendDocument', 'document', chatId, attachment, caption, timeoutMs);
|
|
786
|
+
}
|
|
787
|
+
} else if (kind === 'video' || mimeType.startsWith('video/')) {
|
|
788
|
+
try {
|
|
789
|
+
await sendTelegramDocumentLike(botToken, 'sendVideo', 'video', chatId, attachment, caption, timeoutMs);
|
|
790
|
+
} catch {
|
|
791
|
+
await sendTelegramDocumentLike(botToken, 'sendDocument', 'document', chatId, attachment, caption, timeoutMs);
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
await sendTelegramDocumentLike(botToken, 'sendDocument', 'document', chatId, attachment, caption, timeoutMs);
|
|
795
|
+
}
|
|
796
|
+
sent = true;
|
|
797
|
+
}
|
|
798
|
+
return sent;
|
|
383
799
|
}
|
|
384
800
|
|
|
385
801
|
function loadOpenClawConfig() {
|
|
386
802
|
try {
|
|
387
|
-
return JSON.parse(readFileSync(
|
|
803
|
+
return JSON.parse(readFileSync(OPENCLAW_CONFIG_FILE, 'utf-8'));
|
|
388
804
|
} catch {
|
|
389
805
|
return null;
|
|
390
806
|
}
|
|
391
807
|
}
|
|
392
808
|
|
|
809
|
+
function loadOpenClawTelegramOffset() {
|
|
810
|
+
try {
|
|
811
|
+
return JSON.parse(readFileSync(OPENCLAW_TG_OFFSET_FILE, 'utf-8'));
|
|
812
|
+
} catch {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function getTelegramInboundHostMode(config) {
|
|
818
|
+
const explicit = String(process.env.ATEL_NOTIFY_TG_INGRESS || '').trim().toLowerCase();
|
|
819
|
+
if (explicit === 'sdk') return { mode: 'sdk', reason: 'env_override_sdk' };
|
|
820
|
+
if (explicit === 'openclaw') return { mode: 'openclaw', reason: 'env_override_openclaw' };
|
|
821
|
+
const oc = loadOpenClawConfig();
|
|
822
|
+
const enabled = oc?.channels?.telegram?.enabled === true;
|
|
823
|
+
const botToken = String(oc?.channels?.telegram?.botToken || '').trim();
|
|
824
|
+
const offset = loadOpenClawTelegramOffset();
|
|
825
|
+
const offsetBotId = String(offset?.botId || '').trim();
|
|
826
|
+
if (enabled && botToken && config?.botToken && botToken === config.botToken) {
|
|
827
|
+
return { mode: 'openclaw', reason: offsetBotId && offsetBotId === String(config.botId || '') ? 'openclaw_same_bot_active' : 'openclaw_same_bot_configured' };
|
|
828
|
+
}
|
|
829
|
+
return { mode: 'sdk', reason: 'sdk_default' };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function resolveNotifyBindTarget(chatId, botToken) {
|
|
833
|
+
const normalizedChatId = String(chatId || '').trim();
|
|
834
|
+
const normalizedBotToken = String(botToken || '').trim();
|
|
835
|
+
const botId = normalizedBotToken ? (normalizedBotToken.split(':', 1)[0] || 'telegram') : 'telegram';
|
|
836
|
+
const ingress = getTelegramInboundHostMode({ chatId: normalizedChatId, botToken: normalizedBotToken, botId });
|
|
837
|
+
if (ingress.mode === 'openclaw') {
|
|
838
|
+
return {
|
|
839
|
+
channel: 'gateway',
|
|
840
|
+
id: 'gw_' + normalizedChatId,
|
|
841
|
+
target: normalizedChatId,
|
|
842
|
+
label: 'owner',
|
|
843
|
+
ingress,
|
|
844
|
+
botToken: undefined,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
channel: 'telegram',
|
|
849
|
+
id: 'tg_' + normalizedChatId,
|
|
850
|
+
target: normalizedChatId,
|
|
851
|
+
label: 'owner',
|
|
852
|
+
ingress,
|
|
853
|
+
botToken: normalizedBotToken || undefined,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
393
857
|
// Read TG bot token from openclaw config
|
|
394
858
|
function discoverTelegramBot() {
|
|
395
859
|
try {
|
|
@@ -441,6 +905,45 @@ function registerTelegramInboundBinding(did, config) {
|
|
|
441
905
|
return { ownerDid: did, rebound: true };
|
|
442
906
|
}
|
|
443
907
|
|
|
908
|
+
function getTelegramBindingKey(chatId, botToken) {
|
|
909
|
+
const normalizedChatId = String(chatId || '').trim();
|
|
910
|
+
const normalizedBotToken = String(botToken || '').trim();
|
|
911
|
+
if (!normalizedChatId || !normalizedBotToken) return null;
|
|
912
|
+
const botId = normalizedBotToken.split(':', 1)[0] || 'telegram';
|
|
913
|
+
return { key: `${botId}:${normalizedChatId}`, botId, chatId: normalizedChatId };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function ensureTelegramBindingOwnership(did, chatId, botToken) {
|
|
917
|
+
const binding = getTelegramBindingKey(chatId, botToken);
|
|
918
|
+
if (!did || !binding) return { ok: true, ownerDid: String(did || '').trim(), key: binding?.key || '' };
|
|
919
|
+
const data = loadTelegramBindings();
|
|
920
|
+
if (!data.bindings || typeof data.bindings !== 'object') data.bindings = {};
|
|
921
|
+
const current = data.bindings[binding.key];
|
|
922
|
+
const ownerDid = String(current?.ownerDid || '').trim();
|
|
923
|
+
if (ownerDid && ownerDid !== did) {
|
|
924
|
+
return { ok: false, ownerDid, key: binding.key, botId: binding.botId, chatId: binding.chatId };
|
|
925
|
+
}
|
|
926
|
+
return { ok: true, ownerDid: ownerDid || String(did).trim(), key: binding.key, botId: binding.botId, chatId: binding.chatId };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function releaseTelegramBindingOwnership(did, chatId, botToken) {
|
|
930
|
+
const binding = getTelegramBindingKey(chatId, botToken);
|
|
931
|
+
if (!did || !binding) return false;
|
|
932
|
+
const data = loadTelegramBindings();
|
|
933
|
+
if (!data.bindings || typeof data.bindings !== 'object') data.bindings = {};
|
|
934
|
+
const current = data.bindings[binding.key];
|
|
935
|
+
if (!current || String(current.ownerDid || '').trim() !== String(did).trim()) return false;
|
|
936
|
+
delete data.bindings[binding.key];
|
|
937
|
+
saveTelegramBindings(data);
|
|
938
|
+
const state = loadTelegramUpdatesState();
|
|
939
|
+
if (state.leaders && state.leaders[binding.key] && String(state.leaders[binding.key].did || '').trim() === String(did).trim()) {
|
|
940
|
+
delete state.leaders[binding.key];
|
|
941
|
+
saveTelegramUpdatesState(state);
|
|
942
|
+
}
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
|
|
444
947
|
function acquireTelegramInboundLeader(did, config) {
|
|
445
948
|
const state = loadTelegramUpdatesState();
|
|
446
949
|
if (!state.leaders || typeof state.leaders !== 'object') state.leaders = {};
|
|
@@ -535,6 +1038,11 @@ async function pollTelegramInboundUpdates(targetDid) {
|
|
|
535
1038
|
const config = getTelegramInboundConfig();
|
|
536
1039
|
if (!config) return { status: 'noop' };
|
|
537
1040
|
|
|
1041
|
+
const ingressMode = getTelegramInboundHostMode(config);
|
|
1042
|
+
if (ingressMode.mode === 'openclaw') {
|
|
1043
|
+
return { status: 'hosted_by_openclaw', reason: ingressMode.reason, botId: config.botId, chatId: config.chatId };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
538
1046
|
const binding = registerTelegramInboundBinding(targetDid, config);
|
|
539
1047
|
if (binding.ownerDid && binding.ownerDid !== targetDid) {
|
|
540
1048
|
return { status: 'follower', ownerDid: binding.ownerDid };
|
|
@@ -652,9 +1160,14 @@ async function pushTradeNotification(eventType, payload, body) {
|
|
|
652
1160
|
'milestone_rejected': (p) => {
|
|
653
1161
|
const desc = p.milestoneDescription ? `
|
|
654
1162
|
目标: ${p.milestoneDescription}` : '';
|
|
1163
|
+
const submitCount = Number(p.submitCount || 0);
|
|
1164
|
+
const arbitrationNote = submitCount >= 3 || p.maxReached
|
|
1165
|
+
? `
|
|
1166
|
+
由于连续 3 次被拒,已进入仲裁待决状态,等待人工决定是否发起仲裁`
|
|
1167
|
+
: '';
|
|
655
1168
|
return `❌ 里程碑 M${p.milestoneIndex ?? '?'} 被拒绝
|
|
656
1169
|
订单: ${p.orderId || body?.orderId || '?'}${desc}
|
|
657
|
-
原因: ${p.rejectReason || '未说明'}`;
|
|
1170
|
+
原因: ${p.rejectReason || '未说明'}${arbitrationNote}`;
|
|
658
1171
|
},
|
|
659
1172
|
'order_completed': (p) => `📦 订单已提交完成
|
|
660
1173
|
订单: ${p.orderId || body?.orderId || '?'}
|
|
@@ -704,24 +1217,15 @@ USDC 已支付`;
|
|
|
704
1217
|
continue;
|
|
705
1218
|
}
|
|
706
1219
|
} else if (target.channel === 'gateway') {
|
|
707
|
-
const
|
|
708
|
-
if (
|
|
709
|
-
|
|
710
|
-
method: 'POST',
|
|
711
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
|
|
712
|
-
body: JSON.stringify({ tool: 'message', args: { action: 'send', message, target: target.target } }),
|
|
713
|
-
signal: AbortSignal.timeout(5000),
|
|
714
|
-
});
|
|
715
|
-
if (!resp.ok) {
|
|
716
|
-
log({ event: 'trade_notify_delivery_error', eventType, channel: 'gateway', target: target.id || target.target, status: resp.status, error: `http_${resp.status}` });
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
} else {
|
|
720
|
-
log({ event: 'trade_notify_target_skipped', eventType, channel: 'gateway', target: target.id || target.target, reason: 'missing_gateway' });
|
|
1220
|
+
const delivery = await invokeGatewayTelegramMessage(target.target, message, 5000);
|
|
1221
|
+
if (!delivery.ok) {
|
|
1222
|
+
log({ event: 'trade_notify_delivery_error', eventType, channel: 'gateway', target: target.id || target.target, status: delivery.status || 0, error: delivery.error || delivery.reason || 'gateway_failed', gatewayUrl: delivery.url || null });
|
|
721
1223
|
continue;
|
|
722
1224
|
}
|
|
723
1225
|
}
|
|
724
1226
|
target.lastUsedAt = new Date().toISOString();
|
|
1227
|
+
markNotifyTargetUsed(targets, target, target.lastUsedAt);
|
|
1228
|
+
log({ event: 'trade_notify_delivered', eventType, channel: target.channel, target: target.id || target.target, lastUsedAt: target.lastUsedAt });
|
|
725
1229
|
} catch (e) {
|
|
726
1230
|
log({ event: 'trade_notify_delivery_error', eventType, channel: target.channel, target: target.id || target.target, error: e.message || 'unknown_error' });
|
|
727
1231
|
}
|
|
@@ -771,41 +1275,33 @@ async function pushP2PNotification(eventType, payload = {}) {
|
|
|
771
1275
|
|
|
772
1276
|
for (const target of enabled) {
|
|
773
1277
|
try {
|
|
1278
|
+
const hasRichMedia = (Array.isArray(payload.images) && payload.images.length > 0) || (Array.isArray(payload.attachments) && payload.attachments.length > 0);
|
|
774
1279
|
if (target.channel === 'telegram') {
|
|
775
1280
|
if (!target.botToken) {
|
|
776
1281
|
log({ event: 'p2p_notify_target_skipped', eventType, channel: 'telegram', target: target.id || target.target, reason: 'missing_bot_token' });
|
|
777
1282
|
continue;
|
|
778
1283
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
signal: AbortSignal.timeout(5000),
|
|
784
|
-
});
|
|
785
|
-
const data = await resp.json().catch(() => null);
|
|
786
|
-
if (!resp.ok || data?.ok === false) {
|
|
787
|
-
log({ event: 'p2p_notify_delivery_error', eventType, channel: 'telegram', target: target.id || target.target, status: resp.status, error: data?.description || `http_${resp.status}` });
|
|
788
|
-
continue;
|
|
1284
|
+
if (hasRichMedia) {
|
|
1285
|
+
await sendTelegramRichMedia(target.botToken, target.target, message, payload, 5000);
|
|
1286
|
+
} else {
|
|
1287
|
+
await sendTelegramBotRequest(target.botToken, 'sendMessage', { chat_id: target.target, text: message }, 5000);
|
|
789
1288
|
}
|
|
790
1289
|
} else if (target.channel === 'gateway') {
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (!resp.ok) {
|
|
800
|
-
log({ event: 'p2p_notify_delivery_error', eventType, channel: 'gateway', target: target.id || target.target, status: resp.status, error: `http_${resp.status}` });
|
|
1290
|
+
const oc = loadOpenClawConfig();
|
|
1291
|
+
const botToken = String(oc?.channels?.telegram?.botToken || '').trim();
|
|
1292
|
+
if (hasRichMedia && botToken) {
|
|
1293
|
+
await sendTelegramRichMedia(botToken, target.target, message, payload, 5000);
|
|
1294
|
+
} else {
|
|
1295
|
+
const delivery = await invokeGatewayTelegramMessage(target.target, message, 5000);
|
|
1296
|
+
if (!delivery.ok) {
|
|
1297
|
+
log({ event: 'p2p_notify_delivery_error', eventType, channel: 'gateway', target: target.id || target.target, status: delivery.status || 0, error: delivery.error || delivery.reason || 'gateway_failed', gatewayUrl: delivery.url || null });
|
|
801
1298
|
continue;
|
|
802
1299
|
}
|
|
803
|
-
} else {
|
|
804
|
-
log({ event: 'p2p_notify_target_skipped', eventType, channel: 'gateway', target: target.id || target.target, reason: 'missing_gateway' });
|
|
805
|
-
continue;
|
|
806
1300
|
}
|
|
807
1301
|
}
|
|
808
1302
|
target.lastUsedAt = new Date().toISOString();
|
|
1303
|
+
markNotifyTargetUsed(targets, target, target.lastUsedAt);
|
|
1304
|
+
log({ event: 'p2p_notify_delivered', eventType, channel: target.channel, target: target.id || target.target, lastUsedAt: target.lastUsedAt });
|
|
809
1305
|
} catch (e) {
|
|
810
1306
|
log({ event: 'p2p_notify_delivery_error', eventType, channel: target.channel, target: target.id || target.target, error: e.message || 'unknown_error' });
|
|
811
1307
|
}
|
|
@@ -1704,9 +2200,9 @@ async function getWalletAddresses() {
|
|
|
1704
2200
|
function detectPreferredChain() {
|
|
1705
2201
|
const config = loadAnchorConfig();
|
|
1706
2202
|
if (config?.preferredChain) return config.preferredChain;
|
|
1707
|
-
if (getChainPrivateKey('solana')) return 'solana';
|
|
1708
|
-
if (getChainPrivateKey('base')) return 'base';
|
|
1709
2203
|
if (getChainPrivateKey('bsc')) return 'bsc';
|
|
2204
|
+
if (getChainPrivateKey('base')) return 'base';
|
|
2205
|
+
if (getChainPrivateKey('solana')) return 'solana';
|
|
1710
2206
|
return null;
|
|
1711
2207
|
}
|
|
1712
2208
|
|
|
@@ -2738,11 +3234,12 @@ You are an ATEL agent (${name}) processing tasks from other agents via the ATEL
|
|
|
2738
3234
|
}
|
|
2739
3235
|
}
|
|
2740
3236
|
|
|
2741
|
-
|
|
2742
|
-
status: 'created',
|
|
2743
|
-
agent_id: identity.agent_id,
|
|
2744
|
-
did: identity.did,
|
|
3237
|
+
const output = {
|
|
3238
|
+
status: 'created',
|
|
3239
|
+
agent_id: identity.agent_id,
|
|
3240
|
+
did: identity.did,
|
|
2745
3241
|
policy: POLICY_FILE,
|
|
3242
|
+
anchor: anchorConfigured ? 'configured' : 'disabled',
|
|
2746
3243
|
nextSteps: [
|
|
2747
3244
|
'atel start — bring the agent online (network + auto-register)',
|
|
2748
3245
|
`atel register ${name} "<capability1>,<capability2>" — advertise what you can do`,
|
|
@@ -2751,7 +3248,6 @@ You are an ATEL agent (${name}) processing tasks from other agents via the ATEL
|
|
|
2751
3248
|
],
|
|
2752
3249
|
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',
|
|
2753
3250
|
};
|
|
2754
|
-
if (smartWallet) output.smartWallet = smartWallet;
|
|
2755
3251
|
console.log(JSON.stringify(output, null, 2));
|
|
2756
3252
|
|
|
2757
3253
|
try {
|
|
@@ -2864,7 +3360,7 @@ async function cmdAnchor(subcommand) {
|
|
|
2864
3360
|
|
|
2865
3361
|
async function cmdInfo() {
|
|
2866
3362
|
const id = requireIdentity();
|
|
2867
|
-
const info = { agent_id: id.agent_id, did: id.did, capabilities: loadCapabilities(), policy: loadPolicy(), network: loadNetwork(), executor: EXECUTOR_URL || 'not configured' };
|
|
3363
|
+
const info = { agent_id: id.agent_id, did: id.did, capabilities: loadCapabilities(), policy: loadPolicy(), network: loadNetwork(), executor: EXECUTOR_URL || 'not configured', environment: getCurrentEnvironmentSummary() };
|
|
2868
3364
|
|
|
2869
3365
|
// Fetch wallet info from Platform
|
|
2870
3366
|
try {
|
|
@@ -2983,11 +3479,11 @@ async function cmdSetup(port) {
|
|
|
2983
3479
|
const net = await autoNetworkSetup(p, ATEL_RELAY);
|
|
2984
3480
|
for (const step of net.steps) console.log(JSON.stringify({ event: 'step', message: step }));
|
|
2985
3481
|
if (net.endpoint) {
|
|
2986
|
-
saveNetwork({ publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
|
|
3482
|
+
saveNetwork({ ...net, publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
|
|
2987
3483
|
console.log(JSON.stringify({ status: 'ready', endpoint: net.endpoint }));
|
|
2988
3484
|
} else if (net.publicIP) {
|
|
2989
3485
|
const ep = `http://${net.publicIP}:${p}`;
|
|
2990
|
-
saveNetwork({ publicIP: net.publicIP, port: p, endpoint: ep, upnp: false, reachable: false, needsManualPortForward: true, configuredAt: new Date().toISOString() });
|
|
3486
|
+
saveNetwork({ ...net, publicIP: net.publicIP, port: p, endpoint: ep, upnp: false, reachable: false, needsManualPortForward: true, configuredAt: new Date().toISOString() });
|
|
2991
3487
|
console.log(JSON.stringify({ status: 'needs_port_forward', publicIP: net.publicIP, port: p, instruction: `Forward external TCP port ${p} to this machine's port ${p} on your router. Then run: atel verify` }));
|
|
2992
3488
|
} else {
|
|
2993
3489
|
console.log(JSON.stringify({ status: 'failed', error: 'Could not determine public IP' }));
|
|
@@ -3180,12 +3676,25 @@ async function cmdStart(port) {
|
|
|
3180
3676
|
}
|
|
3181
3677
|
|
|
3182
3678
|
const p = parseInt(port || '3100');
|
|
3679
|
+
const startLock = acquireStartInstanceLock(p);
|
|
3680
|
+
if (!startLock.ok) {
|
|
3681
|
+
console.error(`Another atel start instance is already running for this ATEL_DIR on port ${p} (pid: ${startLock.existing?.pid || 'unknown'}).`);
|
|
3682
|
+
process.exit(1);
|
|
3683
|
+
}
|
|
3684
|
+
process.on('exit', () => { releaseStartInstanceLock(p); });
|
|
3183
3685
|
const caps = loadCapabilities();
|
|
3184
3686
|
const capTypes = caps.map(c => c.type || c);
|
|
3185
3687
|
const policy = loadPolicy();
|
|
3186
3688
|
const enforcer = new PolicyEnforcer(policy);
|
|
3187
3689
|
const pendingTasks = loadTasks();
|
|
3188
3690
|
let resultPushQueue = loadResultPushQueue();
|
|
3691
|
+
const notifyTargets = loadNotifyTargets();
|
|
3692
|
+
const enabledNotifyTargets = notifyTargets.targets.filter(t => t.enabled !== false);
|
|
3693
|
+
if (notifyTargets.targets.length === 0) {
|
|
3694
|
+
console.warn('⚠️ No notification targets are bound. Run `atel notify bind <chatId>` or `atel notify add telegram <chatId>` before expecting callbacks.');
|
|
3695
|
+
} else if (enabledNotifyTargets.length === 0) {
|
|
3696
|
+
console.warn('⚠️ Notification targets exist, but all are disabled. Run `atel notify enable <id>` to restore callbacks.');
|
|
3697
|
+
}
|
|
3189
3698
|
|
|
3190
3699
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
3191
3700
|
|
|
@@ -3194,7 +3703,7 @@ async function cmdStart(port) {
|
|
|
3194
3703
|
if (process.env.ATEL_DEBUG) console.error('[DEBUG] verifyAnchorFromChain input:', { chain, txHash, traceRoot });
|
|
3195
3704
|
|
|
3196
3705
|
if (!txHash || !traceRoot) return { checked: false, verified: false, reason: 'missing_anchor_or_root' };
|
|
3197
|
-
const c = (chain || '
|
|
3706
|
+
const c = (chain || 'base').toLowerCase();
|
|
3198
3707
|
|
|
3199
3708
|
if (c === 'solana') {
|
|
3200
3709
|
const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
|
|
@@ -3510,8 +4019,9 @@ async function cmdStart(port) {
|
|
|
3510
4019
|
const processedEvents = new Set();
|
|
3511
4020
|
const pendingAgentCallbacks = new Map();
|
|
3512
4021
|
|
|
3513
|
-
// ── Agent hook queue (
|
|
3514
|
-
let
|
|
4022
|
+
// ── Agent hook queue (bounded concurrency, still guarded by recovery keys) ──
|
|
4023
|
+
let activeHookWorkers = 0;
|
|
4024
|
+
const MAX_HOOK_CONCURRENCY = Math.max(1, Math.min(4, Number.parseInt(process.env.ATEL_HOOK_CONCURRENCY || '3', 10) || 3));
|
|
3515
4025
|
const hookQueue = [];
|
|
3516
4026
|
const activeRecoveryKeys = new Set();
|
|
3517
4027
|
|
|
@@ -3837,9 +4347,17 @@ For rejection:
|
|
|
3837
4347
|
Important: you are not chatting with the user.
|
|
3838
4348
|
Do not output markdown, headings, bullet points, explanations, or your reasoning.
|
|
3839
4349
|
You must output exactly one line of JSON.
|
|
4350
|
+
Do not return a plan-only answer.
|
|
4351
|
+
Do not return vague wording like “可定义为/应当/建议/可以/后续将”.
|
|
4352
|
+
You must prefer factual evidence:
|
|
4353
|
+
- what you actually checked
|
|
4354
|
+
- what command or file/path or interface you actually touched
|
|
4355
|
+
- what concrete output / state / observation you actually got
|
|
4356
|
+
- whether that evidence satisfies the current milestone
|
|
4357
|
+
If you could not find evidence, say that explicitly in the JSON result, including what you checked.
|
|
3840
4358
|
|
|
3841
4359
|
Format:
|
|
3842
|
-
{"result":"
|
|
4360
|
+
{"result":"factual deliverable with concrete evidence and observations only"}`;
|
|
3843
4361
|
}
|
|
3844
4362
|
|
|
3845
4363
|
return promptText;
|
|
@@ -3965,6 +4483,9 @@ Format:
|
|
|
3965
4483
|
const parsed = JSON.parse(cleaned);
|
|
3966
4484
|
return buildAgentCallbackAction(eventType, payload, parsed);
|
|
3967
4485
|
} catch {
|
|
4486
|
+
if (isKnownInvalidLocalAgentStdout(cleaned)) {
|
|
4487
|
+
return { ok: false, error: 'invalid_local_agent_stdout_known_failure' };
|
|
4488
|
+
}
|
|
3968
4489
|
return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
|
|
3969
4490
|
}
|
|
3970
4491
|
}
|
|
@@ -4094,7 +4615,7 @@ Format:
|
|
|
4094
4615
|
activeRecoveryKeys.add(recoveryKey);
|
|
4095
4616
|
}
|
|
4096
4617
|
hookQueue.push({ event: eventType, dedupeKey, cmd: parsedCmd[0], args: parsedCmd.slice(1), cwd, payload, recoveryKey });
|
|
4097
|
-
|
|
4618
|
+
processHookQueue();
|
|
4098
4619
|
return true;
|
|
4099
4620
|
}
|
|
4100
4621
|
|
|
@@ -4186,6 +4707,10 @@ Format:
|
|
|
4186
4707
|
orderDescription,
|
|
4187
4708
|
previousApprovedOutputs,
|
|
4188
4709
|
});
|
|
4710
|
+
if (isResubmission && Number(currentMilestone.submitCount || 0) >= 3) {
|
|
4711
|
+
log({ event: 'trade_reconcile_executor_skip_manual_arbitration', orderId, currentMilestone: currentIndex, phase: ms.phase, submitCount: Number(currentMilestone.submitCount || 0) });
|
|
4712
|
+
return;
|
|
4713
|
+
}
|
|
4189
4714
|
const promptText = isResubmission
|
|
4190
4715
|
? `You are the ATEL executor agent. Your previous submission for milestone M${currentIndex} was rejected.
|
|
4191
4716
|
Original order requirements: ${orderDescription || 'not provided'}
|
|
@@ -4238,7 +4763,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4238
4763
|
orderDescription,
|
|
4239
4764
|
previousApprovedOutputs,
|
|
4240
4765
|
};
|
|
4241
|
-
const promptText = `You are the ATEL requester agent. You need to review the work submitted by the executor.\nOriginal order requirements: ${orderDescription || 'not provided'}\nMilestone goal: ${submittedMilestone.title || ''}\nPreviously approved outputs:\n${previousApprovedOutputs || 'none'}\nSubmission: ${submittedMilestone.resultSummary || ''}\nDecide carefully whether to pass or reject based only on the order requirements and the previously approved outputs, then return decision=pass or decision=reject via the callback.`;
|
|
4766
|
+
const promptText = `You are the ATEL requester agent. You need to review the work submitted by the executor.\nOriginal order requirements: ${orderDescription || 'not provided'}\nMilestone goal: ${submittedMilestone.title || ''}\nPreviously approved outputs:\n${previousApprovedOutputs || 'none'}\nSubmission: ${submittedMilestone.resultSummary || ''}\nReview based on the submitted evidence, command outputs, file contents, and the previously approved outputs. Do not try to inspect executor-local filesystem paths like /tmp on your own machine unless the submission includes a reproducible proof that makes such a check valid.\nDecide carefully whether to pass or reject based only on the order requirements and the previously approved outputs, then return decision=pass or decision=reject via the callback.`;
|
|
4242
4767
|
const recoveryKey = buildMilestoneHookRecoveryKey('milestone_submitted', payload);
|
|
4243
4768
|
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
4244
4769
|
if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
|
|
@@ -4290,7 +4815,13 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4290
4815
|
const order = await fetchOrderState(orderId);
|
|
4291
4816
|
await reconcileSingleTradeOrder(order);
|
|
4292
4817
|
} catch (e) {
|
|
4293
|
-
|
|
4818
|
+
const message = String(e?.message || '');
|
|
4819
|
+
if (message.includes('order_info_http_404')) {
|
|
4820
|
+
untrackOrder(orderId);
|
|
4821
|
+
log({ event: 'trade_reconcile_tracked_removed', orderId, reason: 'order_info_http_404' });
|
|
4822
|
+
continue;
|
|
4823
|
+
}
|
|
4824
|
+
log({ event: 'trade_reconcile_tracked_error', orderId, error: message });
|
|
4294
4825
|
}
|
|
4295
4826
|
}
|
|
4296
4827
|
}
|
|
@@ -4391,7 +4922,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4391
4922
|
if (kind === 'contact_added') {
|
|
4392
4923
|
pushP2PNotification('p2p_contact_added', { peerDid: messageSenderDid || senderDid, alias: body.alias || '', text }).catch((e) => log({ event: 'p2p_notify_error', kind, error: e.message }));
|
|
4393
4924
|
} else if (kind !== 'telegram_message') {
|
|
4394
|
-
pushP2PNotification('p2p_message_received', { peerDid: messageSenderDid || senderDid, text }).catch((e) => log({ event: 'p2p_notify_error', kind, error: e.message }));
|
|
4925
|
+
pushP2PNotification('p2p_message_received', { peerDid: messageSenderDid || senderDid, text, images: body.images, attachments: body.attachments }).catch((e) => log({ event: 'p2p_notify_error', kind, error: e.message }));
|
|
4395
4926
|
}
|
|
4396
4927
|
|
|
4397
4928
|
res.json({ status: 'ok', kind });
|
|
@@ -4538,7 +5069,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4538
5069
|
+ `## 原任务\n${fullDesc}\n\n`
|
|
4539
5070
|
+ `## 当前里程碑\nM${mIdx}: ${mDesc}\n\n`
|
|
4540
5071
|
+ `## 执行方提交(第 ${submitCount}/3 次)\n${subSummary}\n\n`
|
|
4541
|
-
+ `请基于你自己的判断,评估这次提交是否真实地完成了原任务在当前里程碑应有的部分。如果符合原任务的要求和意图,PASS;如果偏离原任务、违反原任务的约束、或没有真正交付要求的内容,REJECT
|
|
5072
|
+
+ `请基于你自己的判断,评估这次提交是否真实地完成了原任务在当前里程碑应有的部分。如果符合原任务的要求和意图,PASS;如果偏离原任务、违反原任务的约束、或没有真正交付要求的内容,REJECT。评审时优先依据执行方提交的证据、命令输出、文件内容与前序已批准结果;不要在你自己的机器上直接检查执行方本地 /tmp 等路径,除非提交里已经给出可复现且合理的远端证明。\n\n`
|
|
4542
5073
|
+ `通过:\ncd ~/atel-workspace && atel milestone-verify ${orderIdForCwd} ${mIdx} --pass\n\n`
|
|
4543
5074
|
+ `不通过(必须给出具体理由):\ncd ~/atel-workspace && atel milestone-verify ${orderIdForCwd} ${mIdx} --reject '具体原因'\n`;
|
|
4544
5075
|
log({ event: 'verifier_prompt_overridden', orderId: orderIdForCwd, milestoneIndex: mIdx });
|
|
@@ -4610,7 +5141,11 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4610
5141
|
// to "say" it will run a command. This is the reliable path for state-transition actions
|
|
4611
5142
|
// such as approving the milestone plan on order acceptance.
|
|
4612
5143
|
let directExecutionSucceeded = false;
|
|
4613
|
-
const
|
|
5144
|
+
const rejectLimitReached = event === 'milestone_rejected' && Number(payload?.submitCount || body?.submitCount || 0) >= 3;
|
|
5145
|
+
const directActions = rejectLimitReached ? [] : getDirectExecutableActions(event, recommendedActions, payload, currentPolicy);
|
|
5146
|
+
if (rejectLimitReached) {
|
|
5147
|
+
log({ event: 'direct_action_skip_manual_arbitration', eventType: event, dedupeKey, orderId: payload?.orderId || body?.orderId || '', milestoneIndex: payload?.milestoneIndex ?? body?.milestoneIndex ?? null, reason: 'rejection_limit_reached' });
|
|
5148
|
+
}
|
|
4614
5149
|
for (const action of directActions) {
|
|
4615
5150
|
const result = await executeRecommendedActionDirect(event, action, atelCwd, dedupeKey);
|
|
4616
5151
|
if (result.ok) directExecutionSucceeded = true;
|
|
@@ -4623,6 +5158,18 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4623
5158
|
const autoTriggerEvents = ['order_accepted', 'milestone_plan_confirmed', 'milestone_submitted', 'milestone_verified', 'milestone_rejected'];
|
|
4624
5159
|
const hasGatewayAction = Array.isArray(recommendedActions) && recommendedActions.some((action) => Array.isArray(action?.command) && action.command[0] === 'atel');
|
|
4625
5160
|
if (agentCmd && prompt && autoTriggerEvents.includes(event) && !shouldSkipAgentHook(event, directExecutionSucceeded)) {
|
|
5161
|
+
if (rejectLimitReached) {
|
|
5162
|
+
log({
|
|
5163
|
+
event: 'agent_hook_skip_manual_arbitration',
|
|
5164
|
+
eventType: event,
|
|
5165
|
+
dedupeKey,
|
|
5166
|
+
orderId: payload?.orderId || body?.orderId || '',
|
|
5167
|
+
milestoneIndex: payload?.milestoneIndex ?? body?.milestoneIndex ?? null,
|
|
5168
|
+
reason: 'rejection_limit_reached',
|
|
5169
|
+
});
|
|
5170
|
+
res.json({ status: 'received', eventId, eventType: event, skipped: true, requiresManualArbitration: true });
|
|
5171
|
+
return;
|
|
5172
|
+
}
|
|
4626
5173
|
const needsActionablePayload = event !== 'milestone_submitted';
|
|
4627
5174
|
if (needsActionablePayload && !hasGatewayAction) {
|
|
4628
5175
|
log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
|
|
@@ -4667,13 +5214,16 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4667
5214
|
|
|
4668
5215
|
// Process hook queue serially
|
|
4669
5216
|
async function processHookQueue() {
|
|
4670
|
-
if (
|
|
4671
|
-
|
|
5217
|
+
if (activeHookWorkers >= MAX_HOOK_CONCURRENCY || hookQueue.length === 0) return;
|
|
5218
|
+
activeHookWorkers += 1;
|
|
4672
5219
|
const { event: hookEvent, dedupeKey: hookKey, cmd: spawnCmd, args: spawnArgs, cwd: hookCwd, payload: hookPayload, recoveryKey } = hookQueue.shift();
|
|
5220
|
+
if (activeHookWorkers < MAX_HOOK_CONCURRENCY && hookQueue.length > 0) {
|
|
5221
|
+
processHookQueue();
|
|
5222
|
+
}
|
|
4673
5223
|
const { execFile } = await import('child_process');
|
|
4674
5224
|
const finishHook = () => {
|
|
4675
5225
|
if (recoveryKey) activeRecoveryKeys.delete(recoveryKey);
|
|
4676
|
-
|
|
5226
|
+
activeHookWorkers = Math.max(0, activeHookWorkers - 1);
|
|
4677
5227
|
processHookQueue();
|
|
4678
5228
|
};
|
|
4679
5229
|
log({ event: 'agent_cmd_trigger', eventType: hookEvent, dedupeKey: hookKey, cmd: spawnCmd, argsCount: spawnArgs.length });
|
|
@@ -4695,7 +5245,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4695
5245
|
|
|
4696
5246
|
const MAX_ATTEMPTS = 5;
|
|
4697
5247
|
const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
|
|
4698
|
-
const localHookTimeoutMs = isMilestoneHook ?
|
|
5248
|
+
const localHookTimeoutMs = isMilestoneHook ? 90000 : 600000;
|
|
4699
5249
|
const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
|
|
4700
5250
|
const runHook = (attempt, invocation = preparedInvocation) => {
|
|
4701
5251
|
const hookStartedAt = Date.now();
|
|
@@ -4707,7 +5257,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4707
5257
|
|
|
4708
5258
|
if (isSessionLock && attempt < MAX_ATTEMPTS) {
|
|
4709
5259
|
// OpenClaw session still held by previous call — wait for lock release then retry
|
|
4710
|
-
const delay =
|
|
5260
|
+
const delay = 3000 + (attempt * 2000); // 3s, 5s, 7s, 9s
|
|
4711
5261
|
log({ event: 'agent_cmd_session_lock', eventType: hookEvent, attempt: attempt + 1, delayMs: delay, duration_ms: durationMs });
|
|
4712
5262
|
setTimeout(() => runHook(attempt + 1), delay);
|
|
4713
5263
|
} else if (isNetworkError && attempt < 2) {
|
|
@@ -4901,7 +5451,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
4901
5451
|
const proofRecord = {
|
|
4902
5452
|
traceRoot: proof.trace_root,
|
|
4903
5453
|
txHash: anchor.txHash,
|
|
4904
|
-
chain: anchor.chain || '
|
|
5454
|
+
chain: anchor.chain || 'base',
|
|
4905
5455
|
executor: id.did,
|
|
4906
5456
|
taskFrom: task.from,
|
|
4907
5457
|
action: task.action,
|
|
@@ -5004,7 +5554,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5004
5554
|
const anchorTx = anchor?.txHash || null;
|
|
5005
5555
|
let anchorAudit = { checked: false, verified: false, chain: task?.chain || anchor?.chain || null, reason: null };
|
|
5006
5556
|
if (anchorTx) {
|
|
5007
|
-
anchorAudit = await verifyAnchorFromChain(task?.chain || anchor?.chain || '
|
|
5557
|
+
anchorAudit = await verifyAnchorFromChain(task?.chain || anchor?.chain || 'base', anchorTx, proof.trace_root);
|
|
5008
5558
|
}
|
|
5009
5559
|
|
|
5010
5560
|
const auditReasons = [];
|
|
@@ -5038,7 +5588,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5038
5588
|
proofBundle: proof,
|
|
5039
5589
|
traceRoot: proof.trace_root,
|
|
5040
5590
|
anchorTx: anchor?.txHash || null,
|
|
5041
|
-
chain: anchor?.chain || task?.chain || '
|
|
5591
|
+
chain: anchor?.chain || task?.chain || 'base',
|
|
5042
5592
|
traceEvents: trace.events, // Include trace events for verification
|
|
5043
5593
|
audit: auditSummary,
|
|
5044
5594
|
};
|
|
@@ -5088,7 +5638,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5088
5638
|
fetch(`${ATEL_NOTIFY_GATEWAY}/tools/invoke`, {
|
|
5089
5639
|
method: 'POST',
|
|
5090
5640
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
|
5091
|
-
body: JSON.stringify({ tool: 'message', args: { action: 'send', message: msg, target: ATEL_NOTIFY_TARGET } }),
|
|
5641
|
+
body: JSON.stringify({ tool: 'message', args: { action: 'send', channel: 'telegram', message: msg, target: ATEL_NOTIFY_TARGET } }),
|
|
5092
5642
|
signal: AbortSignal.timeout(5000),
|
|
5093
5643
|
}).then(() => log({ event: 'notify_sent', taskId })).catch(e => log({ event: 'notify_failed', taskId, error: e.message }));
|
|
5094
5644
|
}
|
|
@@ -5120,7 +5670,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5120
5670
|
status: (success !== false && auditPassed) ? 'completed' : 'failed',
|
|
5121
5671
|
result,
|
|
5122
5672
|
proof: { proof_id: proof.proof_id, trace_root: proof.trace_root, events_count: trace.events.length },
|
|
5123
|
-
anchor: anchor ? { chain: anchor.chain || '
|
|
5673
|
+
anchor: anchor ? { chain: anchor.chain || 'base', txHash: anchor.txHash, block: anchor.blockNumber } : null,
|
|
5124
5674
|
execution: { duration_ms: durationMs, encrypted: task.encrypted },
|
|
5125
5675
|
audit: auditSummary,
|
|
5126
5676
|
rollback: rollbackReport ? { total: rollbackReport.total, succeeded: rollbackReport.succeeded, failed: rollbackReport.failed } : null,
|
|
@@ -5371,7 +5921,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5371
5921
|
text,
|
|
5372
5922
|
timestamp: new Date().toISOString(),
|
|
5373
5923
|
});
|
|
5374
|
-
pushP2PNotification('p2p_message_received', { peerDid: message.from, text }).catch((e) => log({ event: 'p2p_notify_error', kind: 'portal_message', error: e.message }));
|
|
5924
|
+
pushP2PNotification('p2p_message_received', { peerDid: message.from, text, images: payload.images, attachments: payload.attachments }).catch((e) => log({ event: 'p2p_notify_error', kind: 'portal_message', error: e.message }));
|
|
5375
5925
|
return {
|
|
5376
5926
|
status: 'ok',
|
|
5377
5927
|
kind: 'portal_message',
|
|
@@ -5641,7 +6191,7 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5641
6191
|
const echoDurationMs = Date.now() - new Date(echoAcceptedAt || Date.now()).getTime();
|
|
5642
6192
|
if (anchor?.txHash) {
|
|
5643
6193
|
const proofRecord = {
|
|
5644
|
-
traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || '
|
|
6194
|
+
traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'base',
|
|
5645
6195
|
executor: id.did, taskFrom: message.from, action, success: echoSuccess,
|
|
5646
6196
|
durationMs: echoDurationMs, riskLevel: 'low', policyViolations: 0,
|
|
5647
6197
|
proofId: proof.proof_id, timestamp: new Date().toISOString(), verified: true,
|
|
@@ -5677,10 +6227,8 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5677
6227
|
|
|
5678
6228
|
await endpoint.start();
|
|
5679
6229
|
|
|
5680
|
-
//
|
|
5681
|
-
|
|
5682
|
-
try { autoBindNotifications(); } catch (e) { /* never block startup */ }
|
|
5683
|
-
}
|
|
6230
|
+
// In OpenClaw-hosted mode, self-heal notification binding automatically so TG callbacks survive reinstall / port drift.
|
|
6231
|
+
try { autoBindNotifications(); } catch (e) { /* never block startup */ }
|
|
5684
6232
|
|
|
5685
6233
|
// Background retry for failed result pushes (durable queue)
|
|
5686
6234
|
const flushResultPushQueue = async () => {
|
|
@@ -5721,12 +6269,12 @@ Advance the current milestone strictly based on these approved results. Do not i
|
|
|
5721
6269
|
flushResultPushQueue().catch((e) => log({ event: 'result_push_flush_error', error: e.message }));
|
|
5722
6270
|
setInterval(() => {
|
|
5723
6271
|
flushResultPushQueue().catch((e) => log({ event: 'result_push_flush_error', error: e.message }));
|
|
5724
|
-
},
|
|
6272
|
+
}, 5000);
|
|
5725
6273
|
|
|
5726
6274
|
reconcileActiveTradeOrders().catch((e) => log({ event: 'trade_reconcile_bootstrap_error', error: e.message }));
|
|
5727
6275
|
setInterval(() => {
|
|
5728
6276
|
reconcileActiveTradeOrders().catch((e) => log({ event: 'trade_reconcile_interval_error', error: e.message }));
|
|
5729
|
-
},
|
|
6277
|
+
}, 3000);
|
|
5730
6278
|
|
|
5731
6279
|
// Auto-register to Registry with candidates
|
|
5732
6280
|
if (capTypes.length > 0 && networkConfig.candidates && networkConfig.candidates.length > 0) {
|
|
@@ -6736,14 +7284,14 @@ async function cmdRotate() {
|
|
|
6736
7284
|
newDid: newIdentity.did,
|
|
6737
7285
|
backup: backupFile,
|
|
6738
7286
|
proof_valid: verifyKeyRotation(proof),
|
|
6739
|
-
anchor: anchor ? { chain: anchor.chain || '
|
|
7287
|
+
anchor: anchor ? { chain: anchor.chain || 'base', txHash: anchor.txHash } : null,
|
|
6740
7288
|
next: 'Restart endpoint: atel start [port]',
|
|
6741
7289
|
}, null, 2));
|
|
6742
7290
|
}
|
|
6743
7291
|
|
|
6744
7292
|
// ─── Platform API Helpers ────────────────────────────────────────
|
|
6745
7293
|
|
|
6746
|
-
const PLATFORM_URL =
|
|
7294
|
+
const PLATFORM_URL = ATEL_PLATFORM;
|
|
6747
7295
|
|
|
6748
7296
|
async function signedFetch(method, path, payload = {}) {
|
|
6749
7297
|
const id = requireIdentity();
|
|
@@ -6769,6 +7317,7 @@ async function cmdAuth(code) {
|
|
|
6769
7317
|
console.error(' Authorize a Dashboard session using the code displayed on the login page.');
|
|
6770
7318
|
process.exit(1);
|
|
6771
7319
|
}
|
|
7320
|
+
ensureProductionAuthTarget();
|
|
6772
7321
|
const id = requireIdentity();
|
|
6773
7322
|
const { default: nacl } = await import('tweetnacl');
|
|
6774
7323
|
const { serializePayload } = await import('@lawrenceliang-btc/atel-sdk');
|
|
@@ -7268,10 +7817,39 @@ async function cmdReject(orderId) {
|
|
|
7268
7817
|
console.log(JSON.stringify(data, null, 2));
|
|
7269
7818
|
}
|
|
7270
7819
|
|
|
7271
|
-
async function cmdOrderCancel(orderId,
|
|
7272
|
-
if (!orderId) { console.error('Usage: atel order-cancel <orderId> [reason]'); process.exit(1); }
|
|
7820
|
+
async function cmdOrderCancel(orderId, restArgs = []) {
|
|
7821
|
+
if (!orderId) { console.error('Usage: atel order-cancel <orderId> [reason] [--dry-run]'); process.exit(1); }
|
|
7822
|
+
|
|
7823
|
+
const { dryRun, reason } = parseOrderCancelArgs(restArgs);
|
|
7824
|
+
const cancelReason = reason || 'reset regression environment';
|
|
7825
|
+
|
|
7826
|
+
const res = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, { signal: AbortSignal.timeout(10000) });
|
|
7827
|
+
const orderInfo = await res.json();
|
|
7828
|
+
if (!res.ok) { console.error('Failed to get order:', orderInfo.error || `http_${res.status}`); process.exit(1); }
|
|
7829
|
+
|
|
7830
|
+
const currentDid = requireIdentity().did;
|
|
7831
|
+
const preflight = preflightOrderCancel(orderInfo, currentDid);
|
|
7832
|
+
if (!preflight.ok) {
|
|
7833
|
+
console.error(preflight.error);
|
|
7834
|
+
process.exit(1);
|
|
7835
|
+
}
|
|
7836
|
+
|
|
7837
|
+
if (dryRun) {
|
|
7838
|
+
console.log(JSON.stringify({
|
|
7839
|
+
status: 'dry_run',
|
|
7840
|
+
orderId: preflight.orderId,
|
|
7841
|
+
orderStatus: preflight.orderStatus,
|
|
7842
|
+
requesterDid: preflight.requesterDid,
|
|
7843
|
+
executorDid: preflight.executorDid,
|
|
7844
|
+
currentDid: preflight.currentDid,
|
|
7845
|
+
reason: cancelReason,
|
|
7846
|
+
canCancel: true,
|
|
7847
|
+
}, null, 2));
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
|
|
7273
7851
|
const data = await signedFetch('POST', `/trade/v1/order/${orderId}/cancel`, {
|
|
7274
|
-
reason:
|
|
7852
|
+
reason: cancelReason,
|
|
7275
7853
|
});
|
|
7276
7854
|
if (data?.status === 'cancelled') untrackOrder(orderId);
|
|
7277
7855
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -7574,7 +8152,7 @@ async function cmdComplete(orderId, taskId) {
|
|
|
7574
8152
|
|
|
7575
8153
|
if (anchor?.txHash) {
|
|
7576
8154
|
const proofRecord = {
|
|
7577
|
-
traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || '
|
|
8155
|
+
traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'base',
|
|
7578
8156
|
executor: id.did, taskFrom: requesterDid, action: 'cli-complete',
|
|
7579
8157
|
success: true, durationMs: 0, riskLevel: 'low', policyViolations: 0,
|
|
7580
8158
|
proofId: proof.proof_id, timestamp: new Date().toISOString(), verified: true,
|
|
@@ -7621,7 +8199,7 @@ async function cmdComplete(orderId, taskId) {
|
|
|
7621
8199
|
const orderPrice = Number(orderInfo?.priceAmount ?? 0);
|
|
7622
8200
|
const isPaidOrder = orderId.startsWith('ord-') && orderPrice > 0;
|
|
7623
8201
|
const anchorTx = anchor?.txHash || null;
|
|
7624
|
-
const anchorChain = anchorTx ? (anchor?.chain || orderInfo?.chain || '
|
|
8202
|
+
const anchorChain = anchorTx ? (anchor?.chain || orderInfo?.chain || 'base') : (orderInfo?.chain || null);
|
|
7625
8203
|
const auditReasons = [];
|
|
7626
8204
|
if (!traceAudit.valid) auditReasons.push('trace_hash_chain_invalid');
|
|
7627
8205
|
if (isPaidOrder && !anchorTx) auditReasons.push('paid_order_anchor_missing');
|
|
@@ -9597,7 +10175,7 @@ const commands = {
|
|
|
9597
10175
|
// Trade
|
|
9598
10176
|
'trade-task': () => cmdTradeTask(args[0], args.slice(1).join(' ')),
|
|
9599
10177
|
order: () => cmdOrder(args[0], args[1], args[2]),
|
|
9600
|
-
'order-cancel': () => cmdOrderCancel(args[0], args.slice(1)
|
|
10178
|
+
'order-cancel': () => cmdOrderCancel(args[0], args.slice(1)),
|
|
9601
10179
|
'order-info': () => cmdOrderInfo(args[0]),
|
|
9602
10180
|
accept: () => cmdAccept(args[0]),
|
|
9603
10181
|
reject: () => _origCmdReject(args[0]),
|
|
@@ -9745,13 +10323,28 @@ const commands = {
|
|
|
9745
10323
|
console.log('Gateway:', gw ? `${gw.url} (token: ${gw.token ? '✅' : '❌'})` : '❌ not found');
|
|
9746
10324
|
const botToken = discoverTelegramBot();
|
|
9747
10325
|
console.log('TG Bot:', botToken ? `✅ (${botToken.split(':')[0]})` : '❌ not configured');
|
|
10326
|
+
const inboundConfig = getTelegramInboundConfig();
|
|
10327
|
+
const inboundMode = inboundConfig ? getTelegramInboundHostMode(inboundConfig) : { mode: 'disabled', reason: 'no_inbound_config' };
|
|
10328
|
+
console.log('TG Ingress:', `${inboundMode.mode}${inboundMode.reason ? ` (${inboundMode.reason})` : ''}`);
|
|
10329
|
+
let currentDid = '';
|
|
10330
|
+
try { currentDid = requireIdentity().did; } catch {}
|
|
10331
|
+
const consents = listNotifyConsentsForDid(currentDid);
|
|
9748
10332
|
console.log(`\nTargets (${targets.targets.length}):`);
|
|
9749
10333
|
if (targets.targets.length === 0) {
|
|
9750
10334
|
console.log(' (none) — use "atel notify bind <chatId>" to add');
|
|
9751
10335
|
}
|
|
9752
10336
|
for (const t of targets.targets) {
|
|
9753
10337
|
const status = t.enabled !== false ? '✅' : '🔇';
|
|
9754
|
-
|
|
10338
|
+
const consent = consents.find((item) => ((item.channel === t.channel) || (t.channel === 'gateway' && item.channel === 'telegram')) && item.target === String(t.target));
|
|
10339
|
+
const consentLabel = consent ? `${consent.status || 'active'} @ ${consent.consentedAt || consent.updatedAt || 'unknown'}` : 'missing';
|
|
10340
|
+
console.log(` ${status} [${t.id}] ${t.channel}:${t.target} label=${t.label || '-'} last=${t.lastUsedAt || 'never'} consent=${consentLabel}`);
|
|
10341
|
+
}
|
|
10342
|
+
if (currentDid) {
|
|
10343
|
+
console.log(`\nConsent Records (${consents.length}) for ${currentDid}:`);
|
|
10344
|
+
if (consents.length === 0) console.log(' (none)');
|
|
10345
|
+
for (const item of consents) {
|
|
10346
|
+
console.log(` - ${item.channel}:${item.target} status=${item.status || 'active'} source=${item.source || '-'} updated=${item.updatedAt || item.consentedAt || 'unknown'}`);
|
|
10347
|
+
}
|
|
9755
10348
|
}
|
|
9756
10349
|
return;
|
|
9757
10350
|
}
|
|
@@ -9772,18 +10365,32 @@ const commands = {
|
|
|
9772
10365
|
let botToken = rawArgs.includes('--bot-token') ? rawArgs[rawArgs.indexOf('--bot-token') + 1] : '';
|
|
9773
10366
|
if (!botToken) botToken = process.env.TELEGRAM_BOT_TOKEN || '';
|
|
9774
10367
|
if (!botToken) botToken = discoverTelegramBot() || '';
|
|
9775
|
-
const
|
|
9776
|
-
|
|
9777
|
-
|
|
10368
|
+
const currentDid = requireIdentity().did;
|
|
10369
|
+
const bindingTarget = resolveNotifyBindTarget(chatId, botToken);
|
|
10370
|
+
if (bindingTarget.channel === 'telegram') {
|
|
10371
|
+
const ownership = ensureTelegramBindingOwnership(currentDid, chatId, botToken);
|
|
10372
|
+
if (!ownership.ok) {
|
|
10373
|
+
console.error(`Telegram chat ${chatId} for bot ${ownership.botId || 'telegram'} is already owned by DID ${ownership.ownerDid}. Remove/disable it there before rebinding.`);
|
|
10374
|
+
process.exit(1);
|
|
10375
|
+
}
|
|
10376
|
+
}
|
|
10377
|
+
const id = bindingTarget.id;
|
|
10378
|
+
// Remove existing notify targets for the same chat before rebinding
|
|
10379
|
+
targets.targets = targets.targets.filter(t => !(String(t.target) === String(chatId) && (t.channel === 'telegram' || t.channel === 'gateway')));
|
|
9778
10380
|
targets.targets.push({
|
|
9779
|
-
id, channel:
|
|
9780
|
-
botToken: botToken
|
|
9781
|
-
label:
|
|
10381
|
+
id, channel: bindingTarget.channel, target: String(bindingTarget.target),
|
|
10382
|
+
botToken: bindingTarget.botToken,
|
|
10383
|
+
label: bindingTarget.label, enabled: true,
|
|
9782
10384
|
createdAt: new Date().toISOString(), lastUsedAt: null,
|
|
9783
10385
|
});
|
|
9784
10386
|
saveNotifyTargets(targets);
|
|
9785
10387
|
rememberTelegramRoute(chatId, botToken || undefined);
|
|
9786
|
-
|
|
10388
|
+
try { upsertNotifyConsent({ did: requireIdentity().did, channel: 'telegram', target: String(chatId), botToken, source: 'notify_bind', status: 'active' }); } catch {}
|
|
10389
|
+
if (bindingTarget.channel === 'gateway') {
|
|
10390
|
+
console.log(`✅ Bound OpenClaw gateway chat ${chatId} as notification target (id: ${id})`);
|
|
10391
|
+
} else {
|
|
10392
|
+
console.log(`✅ Bound TG chat ${chatId} as notification target (id: ${id})`);
|
|
10393
|
+
}
|
|
9787
10394
|
if (!botToken) console.log('⚠️ No bot token found. Set TELEGRAM_BOT_TOKEN or use --bot-token');
|
|
9788
10395
|
return;
|
|
9789
10396
|
}
|
|
@@ -9795,6 +10402,17 @@ const commands = {
|
|
|
9795
10402
|
if (rawArgs.includes('--label')) label = rawArgs[rawArgs.indexOf('--label') + 1] || '';
|
|
9796
10403
|
let botToken = rawArgs.includes('--bot-token') ? rawArgs[rawArgs.indexOf('--bot-token') + 1] : '';
|
|
9797
10404
|
if (!botToken && channel === 'telegram') botToken = process.env.TELEGRAM_BOT_TOKEN || discoverTelegramBot() || '';
|
|
10405
|
+
const currentDid = requireIdentity().did;
|
|
10406
|
+
if (channel === 'telegram') {
|
|
10407
|
+
const bindingTarget = resolveNotifyBindTarget(target, botToken);
|
|
10408
|
+
if (bindingTarget.channel === 'telegram') {
|
|
10409
|
+
const ownership = ensureTelegramBindingOwnership(currentDid, target, botToken);
|
|
10410
|
+
if (!ownership.ok) {
|
|
10411
|
+
console.error(`Telegram chat ${target} for bot ${ownership.botId || 'telegram'} is already owned by DID ${ownership.ownerDid}. Remove/disable it there before rebinding.`);
|
|
10412
|
+
process.exit(1);
|
|
10413
|
+
}
|
|
10414
|
+
}
|
|
10415
|
+
}
|
|
9798
10416
|
const id = `${channel.substring(0,2)}_${target}`;
|
|
9799
10417
|
targets.targets = targets.targets.filter(t => t.id !== id);
|
|
9800
10418
|
targets.targets.push({
|
|
@@ -9804,6 +10422,7 @@ const commands = {
|
|
|
9804
10422
|
createdAt: new Date().toISOString(), lastUsedAt: null,
|
|
9805
10423
|
});
|
|
9806
10424
|
saveNotifyTargets(targets);
|
|
10425
|
+
try { upsertNotifyConsent({ did: requireIdentity().did, channel, target: String(target), botToken, source: 'notify_add', status: 'active' }); } catch {}
|
|
9807
10426
|
console.log(`✅ Added ${channel}:${target} (id: ${id})`);
|
|
9808
10427
|
return;
|
|
9809
10428
|
}
|
|
@@ -9811,9 +10430,17 @@ const commands = {
|
|
|
9811
10430
|
if (subCmd === 'remove') {
|
|
9812
10431
|
const id = args[1];
|
|
9813
10432
|
if (!id) { console.error('Usage: atel notify remove <id>'); process.exit(1); }
|
|
10433
|
+
const removedTarget = targets.targets.find(t => t.id === id) || null;
|
|
9814
10434
|
const before = targets.targets.length;
|
|
9815
10435
|
targets.targets = targets.targets.filter(t => t.id !== id);
|
|
9816
10436
|
saveNotifyTargets(targets);
|
|
10437
|
+
if (removedTarget) {
|
|
10438
|
+
try {
|
|
10439
|
+
const did = requireIdentity().did;
|
|
10440
|
+
if (removedTarget.channel === 'telegram') releaseTelegramBindingOwnership(did, removedTarget.target, removedTarget.botToken);
|
|
10441
|
+
revokeNotifyConsent({ did, channel: removedTarget.channel, target: String(removedTarget.target), reason: 'notify_remove' });
|
|
10442
|
+
} catch {}
|
|
10443
|
+
}
|
|
9817
10444
|
console.log(targets.targets.length < before ? `✅ Removed ${id}` : `⚠️ Target ${id} not found`);
|
|
9818
10445
|
return;
|
|
9819
10446
|
}
|
|
@@ -9823,8 +10450,26 @@ const commands = {
|
|
|
9823
10450
|
if (!id) { console.error(`Usage: atel notify ${subCmd} <id>`); process.exit(1); }
|
|
9824
10451
|
const t = targets.targets.find(t => t.id === id);
|
|
9825
10452
|
if (!t) { console.error(`Target ${id} not found`); process.exit(1); }
|
|
10453
|
+
const did = requireIdentity().did;
|
|
10454
|
+
if (subCmd === 'enable' && t.channel === 'telegram') {
|
|
10455
|
+
const bindingTarget = resolveNotifyBindTarget(t.target, t.botToken || '');
|
|
10456
|
+
if (bindingTarget.channel === 'telegram') {
|
|
10457
|
+
const ownership = ensureTelegramBindingOwnership(did, t.target, t.botToken);
|
|
10458
|
+
if (!ownership.ok) {
|
|
10459
|
+
console.error(`Telegram chat ${t.target} for bot ${ownership.botId || 'telegram'} is already owned by DID ${ownership.ownerDid}. Remove/disable it there before rebinding.`);
|
|
10460
|
+
process.exit(1);
|
|
10461
|
+
}
|
|
10462
|
+
}
|
|
10463
|
+
}
|
|
9826
10464
|
t.enabled = subCmd === 'enable';
|
|
9827
10465
|
saveNotifyTargets(targets);
|
|
10466
|
+
try {
|
|
10467
|
+
if (subCmd === 'enable') upsertNotifyConsent({ did, channel: t.channel, target: String(t.target), botToken: t.botToken, source: 'notify_enable', status: 'active' });
|
|
10468
|
+
else {
|
|
10469
|
+
if (t.channel === 'telegram') releaseTelegramBindingOwnership(did, t.target, t.botToken);
|
|
10470
|
+
revokeNotifyConsent({ did, channel: t.channel, target: String(t.target), reason: 'notify_disable' });
|
|
10471
|
+
}
|
|
10472
|
+
} catch {}
|
|
9828
10473
|
console.log(`✅ ${id} ${subCmd}d`);
|
|
9829
10474
|
return;
|
|
9830
10475
|
}
|
|
@@ -9845,17 +10490,11 @@ const commands = {
|
|
|
9845
10490
|
const data = await resp.json();
|
|
9846
10491
|
console.log(` ${target.id}: ${data.ok ? '✅ sent' : '❌ ' + (data.description || 'failed')}`);
|
|
9847
10492
|
} else if (target.channel === 'gateway') {
|
|
9848
|
-
const
|
|
9849
|
-
if (
|
|
9850
|
-
|
|
9851
|
-
method: 'POST',
|
|
9852
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
|
|
9853
|
-
body: JSON.stringify({ tool: 'message', args: { action: 'send', message: '🔔 ATEL notification test', target: target.target } }),
|
|
9854
|
-
signal: AbortSignal.timeout(10000),
|
|
9855
|
-
});
|
|
9856
|
-
console.log(` ${target.id}: ${resp.ok ? '✅ sent' : '❌ status ' + resp.status}`);
|
|
10493
|
+
const delivery = await invokeGatewayTelegramMessage(target.target, '🔔 ATEL notification test', 10000);
|
|
10494
|
+
if (delivery.ok) {
|
|
10495
|
+
console.log(` ${target.id}: ✅ sent via ${delivery.url}`);
|
|
9857
10496
|
} else {
|
|
9858
|
-
console.log(` ${target.id}: ❌
|
|
10497
|
+
console.log(` ${target.id}: ❌ ${delivery.error || delivery.reason || 'gateway_failed'}${delivery.url ? ' @ ' + delivery.url : ''}`);
|
|
9859
10498
|
}
|
|
9860
10499
|
} else {
|
|
9861
10500
|
console.log(` ${target.id}: ❌ unsupported channel ${target.channel}`);
|
|
@@ -9946,7 +10585,7 @@ Hub Commands:
|
|
|
9946
10585
|
Trade Commands:
|
|
9947
10586
|
trade-task <cap> <desc> [--budget N] One-shot: search → order → wait → confirm (requester)
|
|
9948
10587
|
order <executorDid> <cap> <price> Create a trade order
|
|
9949
|
-
order-cancel <orderId> [reason]
|
|
10588
|
+
order-cancel <orderId> [reason] [--dry-run] Cancel an order
|
|
9950
10589
|
order-info <orderId> Get order details
|
|
9951
10590
|
accept <orderId> Accept an order (executor)
|
|
9952
10591
|
reject <orderId> Reject an order (executor)
|