@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/README.md +1 -1
- package/bin/atel.mjs +874 -135
- package/bin/notification-action-helpers.mjs +41 -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
|
@@ -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
|
-
|
|
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 };
|
|
@@ -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
|
|
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' });
|
|
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
|
-
|
|
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;
|
|
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
|
|
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}` });
|
|
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
|
-
['
|
|
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 || '
|
|
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 (
|
|
3514
|
-
let
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
4671
|
-
|
|
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)
|
|
4676
|
-
|
|
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 ?
|
|
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 =
|
|
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 || '
|
|
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 || '
|
|
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 || '
|
|
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 || '
|
|
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 || '
|
|
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
|
-
//
|
|
5681
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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 || '
|
|
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 =
|
|
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,
|
|
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:
|
|
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 || '
|
|
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 || '
|
|
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)
|
|
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
|
-
|
|
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
|
|
9776
|
-
|
|
9777
|
-
|
|
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:
|
|
9780
|
-
botToken: botToken
|
|
9781
|
-
label:
|
|
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
|
-
|
|
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
|
|
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}`);
|
|
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}: ❌
|
|
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|
|
|
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]
|
|
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)
|