@lawrenceliang-btc/atel-sdk 1.2.12 → 1.2.14

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 CHANGED
@@ -61,12 +61,13 @@ import {
61
61
  createMessage, verifyMessage, parseDID, RegistryClient, ExecutionTrace, ProofGenerator,
62
62
  SolanaAnchorProvider, BaseAnchorProvider, BSCAnchorProvider,
63
63
  autoNetworkSetup, collectCandidates, connectToAgent,
64
- discoverPublicIP, checkReachable, ContentAuditor, TrustScoreClient,
64
+ discoverPublicIP, checkReachable, verifyPortReachable, ContentAuditor, TrustScoreClient,
65
65
  RollbackManager, rotateKey, verifyKeyRotation, ToolGateway, PolicyEngine, mintConsentToken, sign,
66
66
  TrustGraph, calculateTaskWeight,
67
67
  } from '@lawrenceliang-btc/atel-sdk';
68
68
  import { TunnelManager, HeartbeatManager } from './tunnel-manager.mjs';
69
- import { buildAgentCallbackAction, getDirectExecutableActions, normalizeGatewayBind, shouldSkipAgentHook, shouldUseGatewaySession } from './notification-action-helpers.mjs';
69
+ import { buildAgentCallbackAction, explainDirectExecutionSkip, 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
- const ATEL_PLATFORM = process.env.ATEL_PLATFORM || process.env.ATEL_API || process.env.ATEL_REGISTRY || 'https://api.atelai.org';
78
- const REGISTRY_URL = process.env.ATEL_REGISTRY || ATEL_PLATFORM || 'https://api.atelai.org';
79
- const ATEL_RELAY = process.env.ATEL_RELAY || 'https://api.atelai.org';
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
- let hasTelegram = false;
491
+ const bindingTarget = resolveNotifyBindTarget(preferred.chatId, preferred.botToken || discoverTelegramBot() || '');
492
+ let hasBoundTarget = false;
293
493
  enabled = enabled.map((target) => {
294
- if (target.channel !== 'telegram') return target;
295
- hasTelegram = true;
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 (!hasTelegram) {
504
+ if (!hasBoundTarget) {
304
505
  enabled = [{
305
- id: `tg_${preferred.chatId}`,
306
- channel: 'telegram',
506
+ id: bindingTarget.id,
507
+ channel: bindingTarget.channel,
307
508
  target: String(preferred.chatId),
308
- botToken: preferred.botToken || discoverTelegramBot() || undefined,
309
- label: orderId ? `order:${orderId}` : 'owner',
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 id = `tg_${chatId}`;
555
+ const bindingTarget = resolveNotifyBindTarget(chatId, botToken || '');
355
556
  targets.targets.push({
356
- id, channel: 'telegram', target: chatId,
357
- botToken: botToken || undefined,
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
- console.log(`🔔 Auto-bound TG notifications to chat ${chatId}`);
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
- // 1. Env override
369
- if (process.env.ATEL_NOTIFY_GATEWAY || process.env.OPENCLAW_GATEWAY_URL) {
370
- const url = process.env.ATEL_NOTIFY_GATEWAY || process.env.OPENCLAW_GATEWAY_URL;
371
- let token = '';
372
- try { token = JSON.parse(readFileSync(join(process.env.HOME || '', '.openclaw/openclaw.json'), 'utf-8')).gateway?.auth?.token || ''; } catch {}
373
- return { url, token };
374
- }
375
- // 2. Read from ~/.openclaw/openclaw.json
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
- const cfg = JSON.parse(readFileSync(join(process.env.HOME || '', '.openclaw/openclaw.json'), 'utf-8'));
378
- const token = cfg.gateway?.auth?.token || '';
379
- const port = cfg.gateway?.port || 18789;
380
- const bind = normalizeGatewayBind(cfg.gateway?.bind || '127.0.0.1');
381
- return { url: `http://${bind}:${port}`, token };
382
- } catch { return null; }
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(join(process.env.HOME || '', '.openclaw/openclaw.json'), 'utf-8'));
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 };
@@ -610,12 +1118,26 @@ async function pushTradeNotification(eventType, payload, body) {
610
1118
  if (c === 'bsc') return ' (BSC)';
611
1119
  return '';
612
1120
  };
1121
+ const autoAcceptReasonText = (reason) => {
1122
+ if (reason === 'missing_recommended_actions') return '平台未提供可执行接单动作';
1123
+ if (reason === 'missing_accept_action') return '事件里没有 accept 动作';
1124
+ if (reason === 'task_mode_not_auto') return '当前 taskMode 不是 auto';
1125
+ if (reason === 'auto_accept_platform_disabled') return '当前未启用免费单自动接单';
1126
+ if (reason === 'paid_auto_accept_disabled') return '当前未启用付费单自动接单';
1127
+ if (reason === 'price_exceeds_accept_max') return '订单金额超过自动接单上限';
1128
+ return reason || '未说明';
1129
+ };
1130
+
613
1131
  const templates = {
614
1132
  'order_created': (p) => `📥 收到新订单
615
1133
  订单: ${p.orderId || body?.orderId || '?'}
616
1134
  金额: $${p.priceAmount ?? '?'} USDC
617
1135
  来自: ${p.requesterDid || '未知请求方'}
618
1136
  请审核后决定是否接单`,
1137
+ 'order_created_auto_accept_skipped': (p) => `⏸️ 未自动接单
1138
+ 订单: ${p.orderId || body?.orderId || '?'}
1139
+ 原因: ${autoAcceptReasonText(p.reasonCode)}
1140
+ 请人工判断是否接单`,
619
1141
  'order_accepted': (p) => `📋 订单已被接单
620
1142
  订单: ${p.orderId || body?.orderId || '?'}
621
1143
  执行方已开始处理,进入里程碑阶段`,
@@ -652,9 +1174,14 @@ async function pushTradeNotification(eventType, payload, body) {
652
1174
  'milestone_rejected': (p) => {
653
1175
  const desc = p.milestoneDescription ? `
654
1176
  目标: ${p.milestoneDescription}` : '';
1177
+ const submitCount = Number(p.submitCount || 0);
1178
+ const arbitrationNote = submitCount >= 3 || p.maxReached
1179
+ ? `
1180
+ 由于连续 3 次被拒,已进入仲裁待决状态,等待人工决定是否发起仲裁`
1181
+ : '';
655
1182
  return `❌ 里程碑 M${p.milestoneIndex ?? '?'} 被拒绝
656
1183
  订单: ${p.orderId || body?.orderId || '?'}${desc}
657
- 原因: ${p.rejectReason || '未说明'}`;
1184
+ 原因: ${p.rejectReason || '未说明'}${arbitrationNote}`;
658
1185
  },
659
1186
  'order_completed': (p) => `📦 订单已提交完成
660
1187
  订单: ${p.orderId || body?.orderId || '?'}
@@ -663,7 +1190,8 @@ async function pushTradeNotification(eventType, payload, body) {
663
1190
  订单: ${p.orderId || body?.orderId || '?'}
664
1191
  原因: ${p.reason || '未说明'}`,
665
1192
  'order_expired': (p) => `⌛ 订单已过期
666
- 订单: ${p.orderId || body?.orderId || '?'}
1193
+ 订单: ${p.orderId || p.order_id || body?.orderId || body?.order_id || '?'}
1194
+ 原因: ${p.reason || '未说明'}
667
1195
  系统已自动结束该订单`,
668
1196
  'dispute_created': (p) => `⚖️ 争议已创建
669
1197
  订单: ${p.orderId || body?.orderId || '?'}
@@ -704,24 +1232,15 @@ USDC 已支付`;
704
1232
  continue;
705
1233
  }
706
1234
  } else if (target.channel === 'gateway') {
707
- const gw = discoverGateway();
708
- if (gw?.url && gw?.token) {
709
- const resp = await fetch(`${gw.url}/tools/invoke`, {
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' });
1235
+ const delivery = await invokeGatewayTelegramMessage(target.target, message, 5000);
1236
+ if (!delivery.ok) {
1237
+ 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
1238
  continue;
722
1239
  }
723
1240
  }
724
1241
  target.lastUsedAt = new Date().toISOString();
1242
+ markNotifyTargetUsed(targets, target, target.lastUsedAt);
1243
+ log({ event: 'trade_notify_delivered', eventType, channel: target.channel, target: target.id || target.target, lastUsedAt: target.lastUsedAt });
725
1244
  } catch (e) {
726
1245
  log({ event: 'trade_notify_delivery_error', eventType, channel: target.channel, target: target.id || target.target, error: e.message || 'unknown_error' });
727
1246
  }
@@ -771,41 +1290,33 @@ async function pushP2PNotification(eventType, payload = {}) {
771
1290
 
772
1291
  for (const target of enabled) {
773
1292
  try {
1293
+ const hasRichMedia = (Array.isArray(payload.images) && payload.images.length > 0) || (Array.isArray(payload.attachments) && payload.attachments.length > 0);
774
1294
  if (target.channel === 'telegram') {
775
1295
  if (!target.botToken) {
776
1296
  log({ event: 'p2p_notify_target_skipped', eventType, channel: 'telegram', target: target.id || target.target, reason: 'missing_bot_token' });
777
1297
  continue;
778
1298
  }
779
- const resp = await fetch(`https://api.telegram.org/bot${target.botToken}/sendMessage`, {
780
- method: 'POST',
781
- headers: { 'Content-Type': 'application/json' },
782
- body: JSON.stringify({ chat_id: target.target, text: message }),
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;
1299
+ if (hasRichMedia) {
1300
+ await sendTelegramRichMedia(target.botToken, target.target, message, payload, 5000);
1301
+ } else {
1302
+ await sendTelegramBotRequest(target.botToken, 'sendMessage', { chat_id: target.target, text: message }, 5000);
789
1303
  }
790
1304
  } else if (target.channel === 'gateway') {
791
- const gw = discoverGateway();
792
- if (gw?.url && gw?.token) {
793
- const resp = await fetch(`${gw.url}/tools/invoke`, {
794
- method: 'POST',
795
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
796
- body: JSON.stringify({ tool: 'message', args: { action: 'send', message, target: target.target } }),
797
- signal: AbortSignal.timeout(5000),
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}` });
1305
+ const oc = loadOpenClawConfig();
1306
+ const botToken = String(oc?.channels?.telegram?.botToken || '').trim();
1307
+ if (hasRichMedia && botToken) {
1308
+ await sendTelegramRichMedia(botToken, target.target, message, payload, 5000);
1309
+ } else {
1310
+ const delivery = await invokeGatewayTelegramMessage(target.target, message, 5000);
1311
+ if (!delivery.ok) {
1312
+ 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
1313
  continue;
802
1314
  }
803
- } else {
804
- log({ event: 'p2p_notify_target_skipped', eventType, channel: 'gateway', target: target.id || target.target, reason: 'missing_gateway' });
805
- continue;
806
1315
  }
807
1316
  }
808
1317
  target.lastUsedAt = new Date().toISOString();
1318
+ markNotifyTargetUsed(targets, target, target.lastUsedAt);
1319
+ log({ event: 'p2p_notify_delivered', eventType, channel: target.channel, target: target.id || target.target, lastUsedAt: target.lastUsedAt });
809
1320
  } catch (e) {
810
1321
  log({ event: 'p2p_notify_delivery_error', eventType, channel: target.channel, target: target.id || target.target, error: e.message || 'unknown_error' });
811
1322
  }
@@ -1704,7 +2215,6 @@ async function getWalletAddresses() {
1704
2215
  function detectPreferredChain() {
1705
2216
  const config = loadAnchorConfig();
1706
2217
  if (config?.preferredChain) return config.preferredChain;
1707
- if (getChainPrivateKey('solana')) return 'solana';
1708
2218
  if (getChainPrivateKey('base')) return 'base';
1709
2219
  if (getChainPrivateKey('bsc')) return 'bsc';
1710
2220
  return null;
@@ -1816,6 +2326,12 @@ function addFriend(did, options = {}) {
1816
2326
 
1817
2327
  saveFriends(data);
1818
2328
  log({ event: 'friend_added', did, addedBy: options.addedBy });
2329
+ syncContactToPlatform(did, { alias: options.alias || '', notes: options.notes || '' }).catch(() => {});
2330
+ pushP2PNotification('p2p_contact_added', {
2331
+ peerDid: did,
2332
+ alias: options.alias || '',
2333
+ text: options.notes || ''
2334
+ }).catch((e) => log({ event: 'p2p_notify_error', kind: 'friend_added', error: e.message }));
1819
2335
  return true;
1820
2336
  }
1821
2337
 
@@ -2629,7 +3145,7 @@ async function configureAnchor() {
2629
3145
  // 1. Select chain
2630
3146
  const chain = await promptChoice(
2631
3147
  'Select blockchain for anchoring:',
2632
- ['solana', 'bsc', 'base']
3148
+ ['base', 'bsc']
2633
3149
  );
2634
3150
 
2635
3151
  // 2. Input private key
@@ -2864,7 +3380,7 @@ async function cmdAnchor(subcommand) {
2864
3380
 
2865
3381
  async function cmdInfo() {
2866
3382
  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' };
3383
+ const info = { agent_id: id.agent_id, did: id.did, capabilities: loadCapabilities(), policy: loadPolicy(), network: loadNetwork(), executor: EXECUTOR_URL || 'not configured', environment: getCurrentEnvironmentSummary() };
2868
3384
 
2869
3385
  // Fetch wallet info from Platform
2870
3386
  try {
@@ -2983,11 +3499,11 @@ async function cmdSetup(port) {
2983
3499
  const net = await autoNetworkSetup(p, ATEL_RELAY);
2984
3500
  for (const step of net.steps) console.log(JSON.stringify({ event: 'step', message: step }));
2985
3501
  if (net.endpoint) {
2986
- saveNetwork({ publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
3502
+ saveNetwork({ ...net, publicIP: net.publicIP, port: p, endpoint: net.endpoint, upnp: net.upnpSuccess, reachable: net.reachable, configuredAt: new Date().toISOString() });
2987
3503
  console.log(JSON.stringify({ status: 'ready', endpoint: net.endpoint }));
2988
3504
  } else if (net.publicIP) {
2989
3505
  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() });
3506
+ saveNetwork({ ...net, publicIP: net.publicIP, port: p, endpoint: ep, upnp: false, reachable: false, needsManualPortForward: true, configuredAt: new Date().toISOString() });
2991
3507
  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
3508
  } else {
2993
3509
  console.log(JSON.stringify({ status: 'failed', error: 'Could not determine public IP' }));
@@ -3171,6 +3687,15 @@ async function cmdStart(port) {
3171
3687
  log({ event: 'atel_skill_sync_error', error: e.message });
3172
3688
  }
3173
3689
 
3690
+ try {
3691
+ const syncedContacts = await syncAllFriendsToPlatform();
3692
+ if (syncedContacts.attempted > 0) {
3693
+ log({ event: 'contacts_backfill_sync_done', ...syncedContacts });
3694
+ }
3695
+ } catch (e) {
3696
+ log({ event: 'contacts_backfill_sync_error', error: e.message });
3697
+ }
3698
+
3174
3699
  // Initialize Ollama only if explicitly enabled (optional local AI audit)
3175
3700
  if (process.env.ATEL_OLLAMA_ENABLED === 'true') {
3176
3701
  await initializeOllama().catch(err => {
@@ -3180,12 +3705,25 @@ async function cmdStart(port) {
3180
3705
  }
3181
3706
 
3182
3707
  const p = parseInt(port || '3100');
3708
+ const startLock = acquireStartInstanceLock(p);
3709
+ if (!startLock.ok) {
3710
+ console.error(`Another atel start instance is already running for this ATEL_DIR on port ${p} (pid: ${startLock.existing?.pid || 'unknown'}).`);
3711
+ process.exit(1);
3712
+ }
3713
+ process.on('exit', () => { releaseStartInstanceLock(p); });
3183
3714
  const caps = loadCapabilities();
3184
3715
  const capTypes = caps.map(c => c.type || c);
3185
3716
  const policy = loadPolicy();
3186
3717
  const enforcer = new PolicyEnforcer(policy);
3187
3718
  const pendingTasks = loadTasks();
3188
3719
  let resultPushQueue = loadResultPushQueue();
3720
+ const notifyTargets = loadNotifyTargets();
3721
+ const enabledNotifyTargets = notifyTargets.targets.filter(t => t.enabled !== false);
3722
+ if (notifyTargets.targets.length === 0) {
3723
+ console.warn('⚠️ No notification targets are bound. Run `atel notify bind <chatId>` or `atel notify add telegram <chatId>` before expecting callbacks.');
3724
+ } else if (enabledNotifyTargets.length === 0) {
3725
+ console.warn('⚠️ Notification targets exist, but all are disabled. Run `atel notify enable <id>` to restore callbacks.');
3726
+ }
3189
3727
 
3190
3728
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
3191
3729
 
@@ -3194,7 +3732,7 @@ async function cmdStart(port) {
3194
3732
  if (process.env.ATEL_DEBUG) console.error('[DEBUG] verifyAnchorFromChain input:', { chain, txHash, traceRoot });
3195
3733
 
3196
3734
  if (!txHash || !traceRoot) return { checked: false, verified: false, reason: 'missing_anchor_or_root' };
3197
- const c = (chain || 'solana').toLowerCase();
3735
+ const c = (chain || 'base').toLowerCase();
3198
3736
 
3199
3737
  if (c === 'solana') {
3200
3738
  const rpcUrl = process.env.ATEL_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
@@ -3510,10 +4048,27 @@ async function cmdStart(port) {
3510
4048
  const processedEvents = new Set();
3511
4049
  const pendingAgentCallbacks = new Map();
3512
4050
 
3513
- // ── Agent hook queue (serialize hook executions to avoid session lock conflicts) ──
3514
- let hookBusy = false;
4051
+ // ── Agent hook queue (bounded concurrency, still guarded by recovery keys) ──
4052
+ let activeHookWorkers = 0;
4053
+ const MAX_HOOK_CONCURRENCY = Math.max(1, Math.min(4, Number.parseInt(process.env.ATEL_HOOK_CONCURRENCY || '3', 10) || 3));
3515
4054
  const hookQueue = [];
3516
4055
  const activeRecoveryKeys = new Set();
4056
+ const recoveryKeyLogState = new Map();
4057
+
4058
+ function logRecoveryKeyActive(eventType, dedupeKey, recoveryKey) {
4059
+ if (!recoveryKey) return;
4060
+ const now = Date.now();
4061
+ const previous = recoveryKeyLogState.get(recoveryKey) || { lastLoggedAt: 0, suppressed: 0 };
4062
+ if (now - previous.lastLoggedAt < 15000) {
4063
+ previous.suppressed += 1;
4064
+ recoveryKeyLogState.set(recoveryKey, previous);
4065
+ return;
4066
+ }
4067
+ const payload = { event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'recovery_key_active', recoveryKey };
4068
+ if (previous.suppressed > 0) payload.suppressed = previous.suppressed;
4069
+ log(payload);
4070
+ recoveryKeyLogState.set(recoveryKey, { lastLoggedAt: now, suppressed: 0 });
4071
+ }
3517
4072
 
3518
4073
  endpoint.app?.post?.('/atel/v1/agent-callback', async (req, res) => {
3519
4074
  const body = req.body || {};
@@ -3837,9 +4392,17 @@ For rejection:
3837
4392
  Important: you are not chatting with the user.
3838
4393
  Do not output markdown, headings, bullet points, explanations, or your reasoning.
3839
4394
  You must output exactly one line of JSON.
4395
+ Do not return a plan-only answer.
4396
+ Do not return vague wording like “可定义为/应当/建议/可以/后续将”.
4397
+ You must prefer factual evidence:
4398
+ - what you actually checked
4399
+ - what command or file/path or interface you actually touched
4400
+ - what concrete output / state / observation you actually got
4401
+ - whether that evidence satisfies the current milestone
4402
+ If you could not find evidence, say that explicitly in the JSON result, including what you checked.
3840
4403
 
3841
4404
  Format:
3842
- {"result":"the actual deliverable for the current milestone"}`;
4405
+ {"result":"factual deliverable with concrete evidence and observations only"}`;
3843
4406
  }
3844
4407
 
3845
4408
  return promptText;
@@ -3965,6 +4528,9 @@ Format:
3965
4528
  const parsed = JSON.parse(cleaned);
3966
4529
  return buildAgentCallbackAction(eventType, payload, parsed);
3967
4530
  } catch {
4531
+ if (isKnownInvalidLocalAgentStdout(cleaned)) {
4532
+ return { ok: false, error: 'invalid_local_agent_stdout_known_failure' };
4533
+ }
3968
4534
  return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
3969
4535
  }
3970
4536
  }
@@ -4088,13 +4654,13 @@ Format:
4088
4654
  const recoveryKey = options.recoveryKey || '';
4089
4655
  if (recoveryKey) {
4090
4656
  if (activeRecoveryKeys.has(recoveryKey)) {
4091
- log({ event: 'agent_hook_not_queued', eventType, dedupeKey, reason: 'recovery_key_active', recoveryKey });
4657
+ logRecoveryKeyActive(eventType, dedupeKey, recoveryKey);
4092
4658
  return false;
4093
4659
  }
4094
4660
  activeRecoveryKeys.add(recoveryKey);
4095
4661
  }
4096
4662
  hookQueue.push({ event: eventType, dedupeKey, cmd: parsedCmd[0], args: parsedCmd.slice(1), cwd, payload, recoveryKey });
4097
- if (!hookBusy) processHookQueue();
4663
+ processHookQueue();
4098
4664
  return true;
4099
4665
  }
4100
4666
 
@@ -4186,6 +4752,10 @@ Format:
4186
4752
  orderDescription,
4187
4753
  previousApprovedOutputs,
4188
4754
  });
4755
+ if (isResubmission && Number(currentMilestone.submitCount || 0) >= 3) {
4756
+ log({ event: 'trade_reconcile_executor_skip_manual_arbitration', orderId, currentMilestone: currentIndex, phase: ms.phase, submitCount: Number(currentMilestone.submitCount || 0) });
4757
+ return;
4758
+ }
4189
4759
  const promptText = isResubmission
4190
4760
  ? `You are the ATEL executor agent. Your previous submission for milestone M${currentIndex} was rejected.
4191
4761
  Original order requirements: ${orderDescription || 'not provided'}
@@ -4238,7 +4808,7 @@ Advance the current milestone strictly based on these approved results. Do not i
4238
4808
  orderDescription,
4239
4809
  previousApprovedOutputs,
4240
4810
  };
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.`;
4811
+ 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
4812
  const recoveryKey = buildMilestoneHookRecoveryKey('milestone_submitted', payload);
4243
4813
  const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
4244
4814
  if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
@@ -4290,7 +4860,13 @@ Advance the current milestone strictly based on these approved results. Do not i
4290
4860
  const order = await fetchOrderState(orderId);
4291
4861
  await reconcileSingleTradeOrder(order);
4292
4862
  } catch (e) {
4293
- log({ event: 'trade_reconcile_tracked_error', orderId, error: e.message });
4863
+ const message = String(e?.message || '');
4864
+ if (message.includes('order_info_http_404')) {
4865
+ untrackOrder(orderId);
4866
+ log({ event: 'trade_reconcile_tracked_removed', orderId, reason: 'order_info_http_404' });
4867
+ continue;
4868
+ }
4869
+ log({ event: 'trade_reconcile_tracked_error', orderId, error: message });
4294
4870
  }
4295
4871
  }
4296
4872
  }
@@ -4391,7 +4967,7 @@ Advance the current milestone strictly based on these approved results. Do not i
4391
4967
  if (kind === 'contact_added') {
4392
4968
  pushP2PNotification('p2p_contact_added', { peerDid: messageSenderDid || senderDid, alias: body.alias || '', text }).catch((e) => log({ event: 'p2p_notify_error', kind, error: e.message }));
4393
4969
  } 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 }));
4970
+ 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
4971
  }
4396
4972
 
4397
4973
  res.json({ status: 'ok', kind });
@@ -4538,7 +5114,7 @@ Advance the current milestone strictly based on these approved results. Do not i
4538
5114
  + `## 原任务\n${fullDesc}\n\n`
4539
5115
  + `## 当前里程碑\nM${mIdx}: ${mDesc}\n\n`
4540
5116
  + `## 执行方提交(第 ${submitCount}/3 次)\n${subSummary}\n\n`
4541
- + `请基于你自己的判断,评估这次提交是否真实地完成了原任务在当前里程碑应有的部分。如果符合原任务的要求和意图,PASS;如果偏离原任务、违反原任务的约束、或没有真正交付要求的内容,REJECT。\n\n`
5117
+ + `请基于你自己的判断,评估这次提交是否真实地完成了原任务在当前里程碑应有的部分。如果符合原任务的要求和意图,PASS;如果偏离原任务、违反原任务的约束、或没有真正交付要求的内容,REJECT。评审时优先依据执行方提交的证据、命令输出、文件内容与前序已批准结果;不要在你自己的机器上直接检查执行方本地 /tmp 等路径,除非提交里已经给出可复现且合理的远端证明。\n\n`
4542
5118
  + `通过:\ncd ~/atel-workspace && atel milestone-verify ${orderIdForCwd} ${mIdx} --pass\n\n`
4543
5119
  + `不通过(必须给出具体理由):\ncd ~/atel-workspace && atel milestone-verify ${orderIdForCwd} ${mIdx} --reject '具体原因'\n`;
4544
5120
  log({ event: 'verifier_prompt_overridden', orderId: orderIdForCwd, milestoneIndex: mIdx });
@@ -4610,7 +5186,27 @@ Advance the current milestone strictly based on these approved results. Do not i
4610
5186
  // to "say" it will run a command. This is the reliable path for state-transition actions
4611
5187
  // such as approving the milestone plan on order acceptance.
4612
5188
  let directExecutionSucceeded = false;
4613
- const directActions = getDirectExecutableActions(event, recommendedActions);
5189
+ const rejectLimitReached = event === 'milestone_rejected' && Number(payload?.submitCount || body?.submitCount || 0) >= 3;
5190
+ const directActions = rejectLimitReached ? [] : getDirectExecutableActions(event, recommendedActions, payload, currentPolicy);
5191
+ if (event === 'order_created' && directActions.length === 0 && !rejectLimitReached) {
5192
+ const skipReason = explainDirectExecutionSkip(event, recommendedActions, payload, currentPolicy);
5193
+ if (skipReason) {
5194
+ log({
5195
+ event: 'order_created_auto_accept_skipped',
5196
+ orderId: payload?.orderId || body?.orderId || '',
5197
+ amount: Number(payload?.priceAmount || 0),
5198
+ reason: skipReason,
5199
+ });
5200
+ pushTradeNotification('order_created_auto_accept_skipped', {
5201
+ ...payload,
5202
+ orderId: payload?.orderId || body?.orderId || '',
5203
+ reasonCode: skipReason,
5204
+ }, body).catch(e => log({ event: 'trade_notify_error', error: e.message }));
5205
+ }
5206
+ }
5207
+ if (rejectLimitReached) {
5208
+ 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' });
5209
+ }
4614
5210
  for (const action of directActions) {
4615
5211
  const result = await executeRecommendedActionDirect(event, action, atelCwd, dedupeKey);
4616
5212
  if (result.ok) directExecutionSucceeded = true;
@@ -4623,6 +5219,18 @@ Advance the current milestone strictly based on these approved results. Do not i
4623
5219
  const autoTriggerEvents = ['order_accepted', 'milestone_plan_confirmed', 'milestone_submitted', 'milestone_verified', 'milestone_rejected'];
4624
5220
  const hasGatewayAction = Array.isArray(recommendedActions) && recommendedActions.some((action) => Array.isArray(action?.command) && action.command[0] === 'atel');
4625
5221
  if (agentCmd && prompt && autoTriggerEvents.includes(event) && !shouldSkipAgentHook(event, directExecutionSucceeded)) {
5222
+ if (rejectLimitReached) {
5223
+ log({
5224
+ event: 'agent_hook_skip_manual_arbitration',
5225
+ eventType: event,
5226
+ dedupeKey,
5227
+ orderId: payload?.orderId || body?.orderId || '',
5228
+ milestoneIndex: payload?.milestoneIndex ?? body?.milestoneIndex ?? null,
5229
+ reason: 'rejection_limit_reached',
5230
+ });
5231
+ res.json({ status: 'received', eventId, eventType: event, skipped: true, requiresManualArbitration: true });
5232
+ return;
5233
+ }
4626
5234
  const needsActionablePayload = event !== 'milestone_submitted';
4627
5235
  if (needsActionablePayload && !hasGatewayAction) {
4628
5236
  log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
@@ -4667,13 +5275,19 @@ Advance the current milestone strictly based on these approved results. Do not i
4667
5275
 
4668
5276
  // Process hook queue serially
4669
5277
  async function processHookQueue() {
4670
- if (hookBusy || hookQueue.length === 0) return;
4671
- hookBusy = true;
5278
+ if (activeHookWorkers >= MAX_HOOK_CONCURRENCY || hookQueue.length === 0) return;
5279
+ activeHookWorkers += 1;
4672
5280
  const { event: hookEvent, dedupeKey: hookKey, cmd: spawnCmd, args: spawnArgs, cwd: hookCwd, payload: hookPayload, recoveryKey } = hookQueue.shift();
5281
+ if (activeHookWorkers < MAX_HOOK_CONCURRENCY && hookQueue.length > 0) {
5282
+ processHookQueue();
5283
+ }
4673
5284
  const { execFile } = await import('child_process');
4674
5285
  const finishHook = () => {
4675
- if (recoveryKey) activeRecoveryKeys.delete(recoveryKey);
4676
- hookBusy = false;
5286
+ if (recoveryKey) {
5287
+ activeRecoveryKeys.delete(recoveryKey);
5288
+ recoveryKeyLogState.delete(recoveryKey);
5289
+ }
5290
+ activeHookWorkers = Math.max(0, activeHookWorkers - 1);
4677
5291
  processHookQueue();
4678
5292
  };
4679
5293
  log({ event: 'agent_cmd_trigger', eventType: hookEvent, dedupeKey: hookKey, cmd: spawnCmd, argsCount: spawnArgs.length });
@@ -4695,7 +5309,7 @@ Advance the current milestone strictly based on these approved results. Do not i
4695
5309
 
4696
5310
  const MAX_ATTEMPTS = 5;
4697
5311
  const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
4698
- const localHookTimeoutMs = isMilestoneHook ? 180000 : 600000;
5312
+ const localHookTimeoutMs = isMilestoneHook ? 90000 : 600000;
4699
5313
  const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
4700
5314
  const runHook = (attempt, invocation = preparedInvocation) => {
4701
5315
  const hookStartedAt = Date.now();
@@ -4707,7 +5321,7 @@ Advance the current milestone strictly based on these approved results. Do not i
4707
5321
 
4708
5322
  if (isSessionLock && attempt < MAX_ATTEMPTS) {
4709
5323
  // OpenClaw session still held by previous call — wait for lock release then retry
4710
- const delay = 15000 + (attempt * 5000); // 15s, 20s, 25s, 30s
5324
+ const delay = 3000 + (attempt * 2000); // 3s, 5s, 7s, 9s
4711
5325
  log({ event: 'agent_cmd_session_lock', eventType: hookEvent, attempt: attempt + 1, delayMs: delay, duration_ms: durationMs });
4712
5326
  setTimeout(() => runHook(attempt + 1), delay);
4713
5327
  } else if (isNetworkError && attempt < 2) {
@@ -4901,7 +5515,7 @@ Advance the current milestone strictly based on these approved results. Do not i
4901
5515
  const proofRecord = {
4902
5516
  traceRoot: proof.trace_root,
4903
5517
  txHash: anchor.txHash,
4904
- chain: anchor.chain || 'solana',
5518
+ chain: anchor.chain || 'base',
4905
5519
  executor: id.did,
4906
5520
  taskFrom: task.from,
4907
5521
  action: task.action,
@@ -5004,7 +5618,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5004
5618
  const anchorTx = anchor?.txHash || null;
5005
5619
  let anchorAudit = { checked: false, verified: false, chain: task?.chain || anchor?.chain || null, reason: null };
5006
5620
  if (anchorTx) {
5007
- anchorAudit = await verifyAnchorFromChain(task?.chain || anchor?.chain || 'solana', anchorTx, proof.trace_root);
5621
+ anchorAudit = await verifyAnchorFromChain(task?.chain || anchor?.chain || 'base', anchorTx, proof.trace_root);
5008
5622
  }
5009
5623
 
5010
5624
  const auditReasons = [];
@@ -5038,7 +5652,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5038
5652
  proofBundle: proof,
5039
5653
  traceRoot: proof.trace_root,
5040
5654
  anchorTx: anchor?.txHash || null,
5041
- chain: anchor?.chain || task?.chain || 'solana',
5655
+ chain: anchor?.chain || task?.chain || 'base',
5042
5656
  traceEvents: trace.events, // Include trace events for verification
5043
5657
  audit: auditSummary,
5044
5658
  };
@@ -5088,7 +5702,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5088
5702
  fetch(`${ATEL_NOTIFY_GATEWAY}/tools/invoke`, {
5089
5703
  method: 'POST',
5090
5704
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
5091
- body: JSON.stringify({ tool: 'message', args: { action: 'send', message: msg, target: ATEL_NOTIFY_TARGET } }),
5705
+ body: JSON.stringify({ tool: 'message', args: { action: 'send', channel: 'telegram', message: msg, target: ATEL_NOTIFY_TARGET } }),
5092
5706
  signal: AbortSignal.timeout(5000),
5093
5707
  }).then(() => log({ event: 'notify_sent', taskId })).catch(e => log({ event: 'notify_failed', taskId, error: e.message }));
5094
5708
  }
@@ -5120,7 +5734,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5120
5734
  status: (success !== false && auditPassed) ? 'completed' : 'failed',
5121
5735
  result,
5122
5736
  proof: { proof_id: proof.proof_id, trace_root: proof.trace_root, events_count: trace.events.length },
5123
- anchor: anchor ? { chain: anchor.chain || 'solana', txHash: anchor.txHash, block: anchor.blockNumber } : null,
5737
+ anchor: anchor ? { chain: anchor.chain || 'base', txHash: anchor.txHash, block: anchor.blockNumber } : null,
5124
5738
  execution: { duration_ms: durationMs, encrypted: task.encrypted },
5125
5739
  audit: auditSummary,
5126
5740
  rollback: rollbackReport ? { total: rollbackReport.total, succeeded: rollbackReport.succeeded, failed: rollbackReport.failed } : null,
@@ -5371,7 +5985,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5371
5985
  text,
5372
5986
  timestamp: new Date().toISOString(),
5373
5987
  });
5374
- pushP2PNotification('p2p_message_received', { peerDid: message.from, text }).catch((e) => log({ event: 'p2p_notify_error', kind: 'portal_message', error: e.message }));
5988
+ 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
5989
  return {
5376
5990
  status: 'ok',
5377
5991
  kind: 'portal_message',
@@ -5641,7 +6255,7 @@ Advance the current milestone strictly based on these approved results. Do not i
5641
6255
  const echoDurationMs = Date.now() - new Date(echoAcceptedAt || Date.now()).getTime();
5642
6256
  if (anchor?.txHash) {
5643
6257
  const proofRecord = {
5644
- traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'solana',
6258
+ traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'base',
5645
6259
  executor: id.did, taskFrom: message.from, action, success: echoSuccess,
5646
6260
  durationMs: echoDurationMs, riskLevel: 'low', policyViolations: 0,
5647
6261
  proofId: proof.proof_id, timestamp: new Date().toISOString(), verified: true,
@@ -5677,10 +6291,8 @@ Advance the current milestone strictly based on these approved results. Do not i
5677
6291
 
5678
6292
  await endpoint.start();
5679
6293
 
5680
- // Telegram notifications are opt-in by default. Only auto-bind when explicit consent was already collected.
5681
- if (ATEL_NOTIFY_AUTO_BIND) {
5682
- try { autoBindNotifications(); } catch (e) { /* never block startup */ }
5683
- }
6294
+ // In OpenClaw-hosted mode, self-heal notification binding automatically so TG callbacks survive reinstall / port drift.
6295
+ try { autoBindNotifications(); } catch (e) { /* never block startup */ }
5684
6296
 
5685
6297
  // Background retry for failed result pushes (durable queue)
5686
6298
  const flushResultPushQueue = async () => {
@@ -5721,12 +6333,12 @@ Advance the current milestone strictly based on these approved results. Do not i
5721
6333
  flushResultPushQueue().catch((e) => log({ event: 'result_push_flush_error', error: e.message }));
5722
6334
  setInterval(() => {
5723
6335
  flushResultPushQueue().catch((e) => log({ event: 'result_push_flush_error', error: e.message }));
5724
- }, 15000);
6336
+ }, 5000);
5725
6337
 
5726
6338
  reconcileActiveTradeOrders().catch((e) => log({ event: 'trade_reconcile_bootstrap_error', error: e.message }));
5727
6339
  setInterval(() => {
5728
6340
  reconcileActiveTradeOrders().catch((e) => log({ event: 'trade_reconcile_interval_error', error: e.message }));
5729
- }, 15000);
6341
+ }, 3000);
5730
6342
 
5731
6343
  // Auto-register to Registry with candidates
5732
6344
  if (capTypes.length > 0 && networkConfig.candidates && networkConfig.candidates.length > 0) {
@@ -6736,14 +7348,50 @@ async function cmdRotate() {
6736
7348
  newDid: newIdentity.did,
6737
7349
  backup: backupFile,
6738
7350
  proof_valid: verifyKeyRotation(proof),
6739
- anchor: anchor ? { chain: anchor.chain || 'solana', txHash: anchor.txHash } : null,
7351
+ anchor: anchor ? { chain: anchor.chain || 'base', txHash: anchor.txHash } : null,
6740
7352
  next: 'Restart endpoint: atel start [port]',
6741
7353
  }, null, 2));
6742
7354
  }
6743
7355
 
6744
7356
  // ─── Platform API Helpers ────────────────────────────────────────
6745
7357
 
6746
- const PLATFORM_URL = process.env.ATEL_PLATFORM || process.env.ATEL_API || process.env.ATEL_REGISTRY || 'https://api.atelai.org';
7358
+ const PLATFORM_URL = ATEL_PLATFORM;
7359
+
7360
+ async function syncContactToPlatform(contactDid, options = {}) {
7361
+ const id = requireIdentity();
7362
+ const normalized = String(contactDid || '').trim();
7363
+ if (!normalized || normalized === id.did) return { ok: false, reason: 'invalid_contact' };
7364
+ try {
7365
+ await signedFetch('POST', '/contacts/v1/sync', {
7366
+ contactDid: normalized,
7367
+ alias: String(options.alias || '').trim(),
7368
+ notes: String(options.notes || '').trim(),
7369
+ });
7370
+ log({ event: 'contact_sync_ok', contactDid: normalized });
7371
+ return { ok: true };
7372
+ } catch (e) {
7373
+ log({ event: 'contact_sync_failed', contactDid: normalized, error: e.message || 'unknown_error' });
7374
+ return { ok: false, reason: e.message || 'sync_failed' };
7375
+ }
7376
+ }
7377
+
7378
+ async function syncAllFriendsToPlatform() {
7379
+ const friends = loadFriends();
7380
+ const accepted = Array.isArray(friends?.friends) ? friends.friends : [];
7381
+ if (accepted.length === 0) return { attempted: 0, synced: 0, failed: 0 };
7382
+ let synced = 0;
7383
+ let failed = 0;
7384
+ for (const friend of accepted) {
7385
+ const result = await syncContactToPlatform(friend.did, {
7386
+ alias: friend.alias || '',
7387
+ notes: friend.notes || ''
7388
+ });
7389
+ if (result.ok) synced += 1;
7390
+ else failed += 1;
7391
+ }
7392
+ log({ event: 'contacts_backfill_sync', attempted: accepted.length, synced, failed });
7393
+ return { attempted: accepted.length, synced, failed };
7394
+ }
6747
7395
 
6748
7396
  async function signedFetch(method, path, payload = {}) {
6749
7397
  const id = requireIdentity();
@@ -6769,6 +7417,7 @@ async function cmdAuth(code) {
6769
7417
  console.error(' Authorize a Dashboard session using the code displayed on the login page.');
6770
7418
  process.exit(1);
6771
7419
  }
7420
+ ensureProductionAuthTarget();
6772
7421
  const id = requireIdentity();
6773
7422
  const { default: nacl } = await import('tweetnacl');
6774
7423
  const { serializePayload } = await import('@lawrenceliang-btc/atel-sdk');
@@ -7268,10 +7917,39 @@ async function cmdReject(orderId) {
7268
7917
  console.log(JSON.stringify(data, null, 2));
7269
7918
  }
7270
7919
 
7271
- async function cmdOrderCancel(orderId, reason) {
7272
- if (!orderId) { console.error('Usage: atel order-cancel <orderId> [reason]'); process.exit(1); }
7920
+ async function cmdOrderCancel(orderId, restArgs = []) {
7921
+ if (!orderId) { console.error('Usage: atel order-cancel <orderId> [reason] [--dry-run]'); process.exit(1); }
7922
+
7923
+ const { dryRun, reason } = parseOrderCancelArgs(restArgs);
7924
+ const cancelReason = reason || 'reset regression environment';
7925
+
7926
+ const res = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}`, { signal: AbortSignal.timeout(10000) });
7927
+ const orderInfo = await res.json();
7928
+ if (!res.ok) { console.error('Failed to get order:', orderInfo.error || `http_${res.status}`); process.exit(1); }
7929
+
7930
+ const currentDid = requireIdentity().did;
7931
+ const preflight = preflightOrderCancel(orderInfo, currentDid);
7932
+ if (!preflight.ok) {
7933
+ console.error(preflight.error);
7934
+ process.exit(1);
7935
+ }
7936
+
7937
+ if (dryRun) {
7938
+ console.log(JSON.stringify({
7939
+ status: 'dry_run',
7940
+ orderId: preflight.orderId,
7941
+ orderStatus: preflight.orderStatus,
7942
+ requesterDid: preflight.requesterDid,
7943
+ executorDid: preflight.executorDid,
7944
+ currentDid: preflight.currentDid,
7945
+ reason: cancelReason,
7946
+ canCancel: true,
7947
+ }, null, 2));
7948
+ return;
7949
+ }
7950
+
7273
7951
  const data = await signedFetch('POST', `/trade/v1/order/${orderId}/cancel`, {
7274
- reason: reason || 'reset regression environment',
7952
+ reason: cancelReason,
7275
7953
  });
7276
7954
  if (data?.status === 'cancelled') untrackOrder(orderId);
7277
7955
  console.log(JSON.stringify(data, null, 2));
@@ -7574,7 +8252,7 @@ async function cmdComplete(orderId, taskId) {
7574
8252
 
7575
8253
  if (anchor?.txHash) {
7576
8254
  const proofRecord = {
7577
- traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'solana',
8255
+ traceRoot: proof.trace_root, txHash: anchor.txHash, chain: anchor.chain || 'base',
7578
8256
  executor: id.did, taskFrom: requesterDid, action: 'cli-complete',
7579
8257
  success: true, durationMs: 0, riskLevel: 'low', policyViolations: 0,
7580
8258
  proofId: proof.proof_id, timestamp: new Date().toISOString(), verified: true,
@@ -7621,7 +8299,7 @@ async function cmdComplete(orderId, taskId) {
7621
8299
  const orderPrice = Number(orderInfo?.priceAmount ?? 0);
7622
8300
  const isPaidOrder = orderId.startsWith('ord-') && orderPrice > 0;
7623
8301
  const anchorTx = anchor?.txHash || null;
7624
- const anchorChain = anchorTx ? (anchor?.chain || orderInfo?.chain || 'solana') : (orderInfo?.chain || null);
8302
+ const anchorChain = anchorTx ? (anchor?.chain || orderInfo?.chain || 'base') : (orderInfo?.chain || null);
7625
8303
  const auditReasons = [];
7626
8304
  if (!traceAudit.valid) auditReasons.push('trace_hash_chain_invalid');
7627
8305
  if (isPaidOrder && !anchorTx) auditReasons.push('paid_order_anchor_missing');
@@ -9597,7 +10275,7 @@ const commands = {
9597
10275
  // Trade
9598
10276
  'trade-task': () => cmdTradeTask(args[0], args.slice(1).join(' ')),
9599
10277
  order: () => cmdOrder(args[0], args[1], args[2]),
9600
- 'order-cancel': () => cmdOrderCancel(args[0], args.slice(1).join(' ').trim()),
10278
+ 'order-cancel': () => cmdOrderCancel(args[0], args.slice(1)),
9601
10279
  'order-info': () => cmdOrderInfo(args[0]),
9602
10280
  accept: () => cmdAccept(args[0]),
9603
10281
  reject: () => _origCmdReject(args[0]),
@@ -9745,13 +10423,28 @@ const commands = {
9745
10423
  console.log('Gateway:', gw ? `${gw.url} (token: ${gw.token ? '✅' : '❌'})` : '❌ not found');
9746
10424
  const botToken = discoverTelegramBot();
9747
10425
  console.log('TG Bot:', botToken ? `✅ (${botToken.split(':')[0]})` : '❌ not configured');
10426
+ const inboundConfig = getTelegramInboundConfig();
10427
+ const inboundMode = inboundConfig ? getTelegramInboundHostMode(inboundConfig) : { mode: 'disabled', reason: 'no_inbound_config' };
10428
+ console.log('TG Ingress:', `${inboundMode.mode}${inboundMode.reason ? ` (${inboundMode.reason})` : ''}`);
10429
+ let currentDid = '';
10430
+ try { currentDid = requireIdentity().did; } catch {}
10431
+ const consents = listNotifyConsentsForDid(currentDid);
9748
10432
  console.log(`\nTargets (${targets.targets.length}):`);
9749
10433
  if (targets.targets.length === 0) {
9750
10434
  console.log(' (none) — use "atel notify bind <chatId>" to add');
9751
10435
  }
9752
10436
  for (const t of targets.targets) {
9753
10437
  const status = t.enabled !== false ? '✅' : '🔇';
9754
- console.log(` ${status} [${t.id}] ${t.channel}:${t.target} label=${t.label || '-'} last=${t.lastUsedAt || 'never'}`);
10438
+ const consent = consents.find((item) => ((item.channel === t.channel) || (t.channel === 'gateway' && item.channel === 'telegram')) && item.target === String(t.target));
10439
+ const consentLabel = consent ? `${consent.status || 'active'} @ ${consent.consentedAt || consent.updatedAt || 'unknown'}` : 'missing';
10440
+ console.log(` ${status} [${t.id}] ${t.channel}:${t.target} label=${t.label || '-'} last=${t.lastUsedAt || 'never'} consent=${consentLabel}`);
10441
+ }
10442
+ if (currentDid) {
10443
+ console.log(`\nConsent Records (${consents.length}) for ${currentDid}:`);
10444
+ if (consents.length === 0) console.log(' (none)');
10445
+ for (const item of consents) {
10446
+ console.log(` - ${item.channel}:${item.target} status=${item.status || 'active'} source=${item.source || '-'} updated=${item.updatedAt || item.consentedAt || 'unknown'}`);
10447
+ }
9755
10448
  }
9756
10449
  return;
9757
10450
  }
@@ -9772,18 +10465,32 @@ const commands = {
9772
10465
  let botToken = rawArgs.includes('--bot-token') ? rawArgs[rawArgs.indexOf('--bot-token') + 1] : '';
9773
10466
  if (!botToken) botToken = process.env.TELEGRAM_BOT_TOKEN || '';
9774
10467
  if (!botToken) botToken = discoverTelegramBot() || '';
9775
- const id = `tg_${chatId}`;
9776
- // Remove existing with same id
9777
- targets.targets = targets.targets.filter(t => t.id !== id);
10468
+ const currentDid = requireIdentity().did;
10469
+ const bindingTarget = resolveNotifyBindTarget(chatId, botToken);
10470
+ if (bindingTarget.channel === 'telegram') {
10471
+ const ownership = ensureTelegramBindingOwnership(currentDid, chatId, botToken);
10472
+ if (!ownership.ok) {
10473
+ console.error(`Telegram chat ${chatId} for bot ${ownership.botId || 'telegram'} is already owned by DID ${ownership.ownerDid}. Remove/disable it there before rebinding.`);
10474
+ process.exit(1);
10475
+ }
10476
+ }
10477
+ const id = bindingTarget.id;
10478
+ // Remove existing notify targets for the same chat before rebinding
10479
+ targets.targets = targets.targets.filter(t => !(String(t.target) === String(chatId) && (t.channel === 'telegram' || t.channel === 'gateway')));
9778
10480
  targets.targets.push({
9779
- id, channel: 'telegram', target: String(chatId),
9780
- botToken: botToken || undefined,
9781
- label: 'owner', enabled: true,
10481
+ id, channel: bindingTarget.channel, target: String(bindingTarget.target),
10482
+ botToken: bindingTarget.botToken,
10483
+ label: bindingTarget.label, enabled: true,
9782
10484
  createdAt: new Date().toISOString(), lastUsedAt: null,
9783
10485
  });
9784
10486
  saveNotifyTargets(targets);
9785
10487
  rememberTelegramRoute(chatId, botToken || undefined);
9786
- console.log(`✅ Bound TG chat ${chatId} as notification target (id: ${id})`);
10488
+ try { upsertNotifyConsent({ did: requireIdentity().did, channel: 'telegram', target: String(chatId), botToken, source: 'notify_bind', status: 'active' }); } catch {}
10489
+ if (bindingTarget.channel === 'gateway') {
10490
+ console.log(`✅ Bound OpenClaw gateway chat ${chatId} as notification target (id: ${id})`);
10491
+ } else {
10492
+ console.log(`✅ Bound TG chat ${chatId} as notification target (id: ${id})`);
10493
+ }
9787
10494
  if (!botToken) console.log('⚠️ No bot token found. Set TELEGRAM_BOT_TOKEN or use --bot-token');
9788
10495
  return;
9789
10496
  }
@@ -9795,6 +10502,17 @@ const commands = {
9795
10502
  if (rawArgs.includes('--label')) label = rawArgs[rawArgs.indexOf('--label') + 1] || '';
9796
10503
  let botToken = rawArgs.includes('--bot-token') ? rawArgs[rawArgs.indexOf('--bot-token') + 1] : '';
9797
10504
  if (!botToken && channel === 'telegram') botToken = process.env.TELEGRAM_BOT_TOKEN || discoverTelegramBot() || '';
10505
+ const currentDid = requireIdentity().did;
10506
+ if (channel === 'telegram') {
10507
+ const bindingTarget = resolveNotifyBindTarget(target, botToken);
10508
+ if (bindingTarget.channel === 'telegram') {
10509
+ const ownership = ensureTelegramBindingOwnership(currentDid, target, botToken);
10510
+ if (!ownership.ok) {
10511
+ console.error(`Telegram chat ${target} for bot ${ownership.botId || 'telegram'} is already owned by DID ${ownership.ownerDid}. Remove/disable it there before rebinding.`);
10512
+ process.exit(1);
10513
+ }
10514
+ }
10515
+ }
9798
10516
  const id = `${channel.substring(0,2)}_${target}`;
9799
10517
  targets.targets = targets.targets.filter(t => t.id !== id);
9800
10518
  targets.targets.push({
@@ -9804,6 +10522,7 @@ const commands = {
9804
10522
  createdAt: new Date().toISOString(), lastUsedAt: null,
9805
10523
  });
9806
10524
  saveNotifyTargets(targets);
10525
+ try { upsertNotifyConsent({ did: requireIdentity().did, channel, target: String(target), botToken, source: 'notify_add', status: 'active' }); } catch {}
9807
10526
  console.log(`✅ Added ${channel}:${target} (id: ${id})`);
9808
10527
  return;
9809
10528
  }
@@ -9811,9 +10530,17 @@ const commands = {
9811
10530
  if (subCmd === 'remove') {
9812
10531
  const id = args[1];
9813
10532
  if (!id) { console.error('Usage: atel notify remove <id>'); process.exit(1); }
10533
+ const removedTarget = targets.targets.find(t => t.id === id) || null;
9814
10534
  const before = targets.targets.length;
9815
10535
  targets.targets = targets.targets.filter(t => t.id !== id);
9816
10536
  saveNotifyTargets(targets);
10537
+ if (removedTarget) {
10538
+ try {
10539
+ const did = requireIdentity().did;
10540
+ if (removedTarget.channel === 'telegram') releaseTelegramBindingOwnership(did, removedTarget.target, removedTarget.botToken);
10541
+ revokeNotifyConsent({ did, channel: removedTarget.channel, target: String(removedTarget.target), reason: 'notify_remove' });
10542
+ } catch {}
10543
+ }
9817
10544
  console.log(targets.targets.length < before ? `✅ Removed ${id}` : `⚠️ Target ${id} not found`);
9818
10545
  return;
9819
10546
  }
@@ -9823,8 +10550,26 @@ const commands = {
9823
10550
  if (!id) { console.error(`Usage: atel notify ${subCmd} <id>`); process.exit(1); }
9824
10551
  const t = targets.targets.find(t => t.id === id);
9825
10552
  if (!t) { console.error(`Target ${id} not found`); process.exit(1); }
10553
+ const did = requireIdentity().did;
10554
+ if (subCmd === 'enable' && t.channel === 'telegram') {
10555
+ const bindingTarget = resolveNotifyBindTarget(t.target, t.botToken || '');
10556
+ if (bindingTarget.channel === 'telegram') {
10557
+ const ownership = ensureTelegramBindingOwnership(did, t.target, t.botToken);
10558
+ if (!ownership.ok) {
10559
+ console.error(`Telegram chat ${t.target} for bot ${ownership.botId || 'telegram'} is already owned by DID ${ownership.ownerDid}. Remove/disable it there before rebinding.`);
10560
+ process.exit(1);
10561
+ }
10562
+ }
10563
+ }
9826
10564
  t.enabled = subCmd === 'enable';
9827
10565
  saveNotifyTargets(targets);
10566
+ try {
10567
+ if (subCmd === 'enable') upsertNotifyConsent({ did, channel: t.channel, target: String(t.target), botToken: t.botToken, source: 'notify_enable', status: 'active' });
10568
+ else {
10569
+ if (t.channel === 'telegram') releaseTelegramBindingOwnership(did, t.target, t.botToken);
10570
+ revokeNotifyConsent({ did, channel: t.channel, target: String(t.target), reason: 'notify_disable' });
10571
+ }
10572
+ } catch {}
9828
10573
  console.log(`✅ ${id} ${subCmd}d`);
9829
10574
  return;
9830
10575
  }
@@ -9845,17 +10590,11 @@ const commands = {
9845
10590
  const data = await resp.json();
9846
10591
  console.log(` ${target.id}: ${data.ok ? '✅ sent' : '❌ ' + (data.description || 'failed')}`);
9847
10592
  } else if (target.channel === 'gateway') {
9848
- const gw = discoverGateway();
9849
- if (gw?.url && gw?.token) {
9850
- const resp = await fetch(`${gw.url}/tools/invoke`, {
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}`);
10593
+ const delivery = await invokeGatewayTelegramMessage(target.target, '🔔 ATEL notification test', 10000);
10594
+ if (delivery.ok) {
10595
+ console.log(` ${target.id}: sent via ${delivery.url}`);
9857
10596
  } else {
9858
- console.log(` ${target.id}: ❌ gateway not found`);
10597
+ console.log(` ${target.id}: ❌ ${delivery.error || delivery.reason || 'gateway_failed'}${delivery.url ? ' @ ' + delivery.url : ''}`);
9859
10598
  }
9860
10599
  } else {
9861
10600
  console.log(` ${target.id}: ❌ unsupported channel ${target.channel}`);
@@ -9919,7 +10658,7 @@ Auth Commands:
9919
10658
 
9920
10659
  Account Commands:
9921
10660
  balance Show platform account balance
9922
- deposit <amount> [channel] Deposit funds (channel: manual|crypto_solana|crypto_base|crypto_bsc|stripe|alipay)
10661
+ deposit <amount> [channel] Deposit funds (channel: manual|crypto_base|crypto_bsc|stripe|alipay)
9923
10662
  withdraw <amount> [channel] [address] Withdraw funds (address required for crypto)
9924
10663
  transactions List payment history
9925
10664
 
@@ -9946,7 +10685,7 @@ Hub Commands:
9946
10685
  Trade Commands:
9947
10686
  trade-task <cap> <desc> [--budget N] One-shot: search → order → wait → confirm (requester)
9948
10687
  order <executorDid> <cap> <price> Create a trade order
9949
- order-cancel <orderId> [reason] Cancel an order
10688
+ order-cancel <orderId> [reason] [--dry-run] Cancel an order
9950
10689
  order-info <orderId> Get order details
9951
10690
  accept <orderId> Accept an order (executor)
9952
10691
  reject <orderId> Reject an order (executor)