@lawrenceliang-btc/atel-sdk 1.1.5 → 1.1.7
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 +44 -37
- package/bin/atel.mjs +271 -15
- package/package.json +3 -3
- package/skill/atel-agent/SKILL.md +52 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ATEL SDK
|
|
2
2
|
|
|
3
|
-
**Agent Trust & Exchange Layer** — A
|
|
3
|
+
**Agent Trust & Exchange Layer** — A protocol SDK and CLI for trustworthy, auditable multi-agent collaboration.
|
|
4
4
|
|
|
5
5
|
## Core Capabilities
|
|
6
6
|
|
|
@@ -12,16 +12,15 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
|
|
|
12
12
|
- **✅ Proof Generation** — Merkle-tree proof bundles with multi-check verification
|
|
13
13
|
- **⚓ On-Chain Anchoring** — Multi-chain proof anchoring (Solana/Base/BSC)
|
|
14
14
|
- **📊 Trust Scoring** — Local trust computation based on execution history
|
|
15
|
-
-
|
|
15
|
+
- **🔔 Notification & Callback Runtime** — Local notify, callback, inbox, and recovery flow
|
|
16
16
|
- **👥 P2P Access Control** — Relationship-based friend system with temporary sessions
|
|
17
17
|
|
|
18
18
|
## Key Features
|
|
19
19
|
|
|
20
|
-
###
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- Cross-platform (Linux/macOS/Windows)
|
|
20
|
+
### Runtime Model
|
|
21
|
+
- ATEL handles DID identity, relay, inbox, callback, notification, and paid order state
|
|
22
|
+
- OpenClaw or your own runtime handles reasoning and tool use
|
|
23
|
+
- Cross-platform CLI (Linux/macOS/Windows)
|
|
25
24
|
|
|
26
25
|
### P2P Friend System
|
|
27
26
|
- Relationship-based access control (friends-only mode)
|
|
@@ -35,14 +34,14 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
|
|
|
35
34
|
- Merkle-tree proof generation
|
|
36
35
|
- On-chain anchoring (Solana/Base/BSC)
|
|
37
36
|
- Local trust score computation
|
|
38
|
-
-
|
|
37
|
+
- Callback-driven execution and recovery
|
|
39
38
|
|
|
40
39
|
### Developer Experience
|
|
41
40
|
- Comprehensive CLI with detailed help
|
|
42
41
|
- Unified output format (human/json/quiet)
|
|
43
42
|
- Status commands for system overview
|
|
44
43
|
- Confirmation prompts for destructive operations
|
|
45
|
-
-
|
|
44
|
+
- Skill-first onboarding path
|
|
46
45
|
|
|
47
46
|
## Quick Start
|
|
48
47
|
|
|
@@ -60,47 +59,52 @@ atel register "My Agent" "assistant,research"
|
|
|
60
59
|
atel start 3100
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
###
|
|
62
|
+
### Recommended Runtime
|
|
63
|
+
|
|
64
|
+
ATEL is not a built-in general-purpose LLM executor. The recommended setup is:
|
|
65
|
+
|
|
66
|
+
- OpenClaw handles agent reasoning and tool execution
|
|
67
|
+
- `atel start` handles endpoint, relay, callback, inbox, and notifications
|
|
68
|
+
- the provided `SKILL.md` handles setup and runtime conventions
|
|
69
|
+
|
|
70
|
+
For OpenClaw, enable `sessions_spawn` in Gateway and start the ATEL runtime:
|
|
64
71
|
|
|
65
72
|
```bash
|
|
73
|
+
openclaw gateway restart
|
|
66
74
|
atel start 3100
|
|
67
|
-
# 📦 Downloading model (first time only, ~400MB)...
|
|
68
|
-
# Progress: 100% (408.9/408.9 MB)
|
|
69
|
-
# ✅ Model ready
|
|
70
|
-
# 🚀 Agent started on port 3100
|
|
71
75
|
```
|
|
72
76
|
|
|
77
|
+
For custom runtimes, point `ATEL_EXECUTOR_URL` at your own service.
|
|
78
|
+
|
|
73
79
|
## Architecture
|
|
74
80
|
|
|
75
|
-
ATEL is organized into
|
|
81
|
+
ATEL is organized into protocol and runtime layers:
|
|
76
82
|
|
|
77
83
|
```
|
|
78
84
|
┌──────────────────────────────────────────────────────────────┐
|
|
79
|
-
│ ATEL SDK
|
|
85
|
+
│ ATEL CLI / SDK │
|
|
80
86
|
├──────────┬──────────┬──────────┬──────────┬─────────────────┤
|
|
81
|
-
│ Identity │
|
|
87
|
+
│ Identity │ Registry │ Policy │ Relay │ Trace │
|
|
82
88
|
├──────────┴──────────┴──────────┴──────────┴─────────────────┤
|
|
83
|
-
│ Proof │
|
|
89
|
+
│ Proof │ Notify │ Callback │ Trade │ Anchor │ Trust/Score │
|
|
84
90
|
├───────────────────────────────┬──────────────────────────────┤
|
|
85
|
-
│
|
|
91
|
+
│ Local Runtime State │ External Agent Runtime │
|
|
86
92
|
└───────────────────────────────┴──────────────────────────────┘
|
|
87
93
|
```
|
|
88
94
|
|
|
89
95
|
| Module | Description |
|
|
90
96
|
|--------|-------------|
|
|
91
97
|
| **Identity** | Ed25519 keypairs, DID creation, signing & verification |
|
|
92
|
-
| **
|
|
93
|
-
| **Policy** |
|
|
94
|
-
| **
|
|
98
|
+
| **Registry** | Agent registration, discovery, metadata |
|
|
99
|
+
| **Policy** | Access control and task acceptance policy |
|
|
100
|
+
| **Relay** | Message delivery, inbox, connectivity fallback |
|
|
95
101
|
| **Trace** | Append-only, hash-chained execution log |
|
|
96
102
|
| **Proof** | Merkle-tree proof bundles with verification |
|
|
97
|
-
| **
|
|
98
|
-
| **
|
|
99
|
-
| **
|
|
100
|
-
| **Rollback** | Compensation and rollback execution |
|
|
103
|
+
| **Notify** | Local user notifications and target fan-out |
|
|
104
|
+
| **Callback** | Runtime callback, recovery, and dedupe handling |
|
|
105
|
+
| **Trade** | Paid order flow, milestone state, settlement hooks |
|
|
101
106
|
| **Anchor** | Multi-chain proof anchoring |
|
|
102
|
-
| **
|
|
103
|
-
| **Service** | HTTP API for trust queries |
|
|
107
|
+
| **Trust/Score** | Local trust-score computation and risk checks |
|
|
104
108
|
|
|
105
109
|
## CLI Commands
|
|
106
110
|
|
|
@@ -140,10 +144,11 @@ atel friend add @alice --notes "Met at conference"
|
|
|
140
144
|
atel temp-session allow @bob --duration 120
|
|
141
145
|
```
|
|
142
146
|
|
|
143
|
-
###
|
|
147
|
+
### P2P Collaboration
|
|
144
148
|
```bash
|
|
145
|
-
atel task <target> <json>
|
|
146
|
-
atel result <taskId> <json>
|
|
149
|
+
atel task <target> <json> # Direct P2P task
|
|
150
|
+
atel result <taskId> <json> # Submit execution result
|
|
151
|
+
atel inbox # Inspect pending direct tasks/messages
|
|
147
152
|
```
|
|
148
153
|
|
|
149
154
|
### Trust & Verification
|
|
@@ -155,12 +160,14 @@ atel verify-proof <tx> <root> # Verify on-chain proof
|
|
|
155
160
|
|
|
156
161
|
### Registry & Trading
|
|
157
162
|
```bash
|
|
158
|
-
atel register [name] [caps]
|
|
159
|
-
atel search <capability>
|
|
160
|
-
atel order <did> <cap> <price>
|
|
161
|
-
atel accept <orderId>
|
|
162
|
-
atel
|
|
163
|
-
atel
|
|
163
|
+
atel register [name] [caps] # Register on public registry
|
|
164
|
+
atel search <capability> # Search for agents
|
|
165
|
+
atel order <did> <cap> <price> # Create paid order
|
|
166
|
+
atel accept <orderId> # Accept order
|
|
167
|
+
atel milestone-status <orderId> # Inspect current plan/progress
|
|
168
|
+
atel milestone-feedback <orderId> --approve # Approve plan
|
|
169
|
+
atel milestone-submit <orderId> <index> --result # Submit milestone result
|
|
170
|
+
atel milestone-verify <orderId> <index> --pass # Verify submitted milestone
|
|
164
171
|
```
|
|
165
172
|
|
|
166
173
|
## API Examples
|
package/bin/atel.mjs
CHANGED
|
@@ -89,6 +89,8 @@ const PENDING_FILE = resolve(ATEL_DIR, 'pending-tasks.json');
|
|
|
89
89
|
const RESULT_PUSH_QUEUE_FILE = resolve(ATEL_DIR, 'pending-result-pushes.json');
|
|
90
90
|
const NOTIFY_TARGETS_FILE = resolve(ATEL_DIR, 'notify-targets.json');
|
|
91
91
|
const TRADE_TRACK_FILE = resolve(ATEL_DIR, 'tracked-orders.json');
|
|
92
|
+
const P2P_STATUS_FILE = resolve(ATEL_DIR, 'p2p-task-status.jsonl');
|
|
93
|
+
const PENDING_AGENT_CALLBACKS_FILE = resolve(ATEL_DIR, 'pending-agent-callbacks.json');
|
|
92
94
|
const KEYS_DIR = resolve(ATEL_DIR, 'keys');
|
|
93
95
|
const ANCHOR_FILE = resolve(KEYS_DIR, 'anchor.json');
|
|
94
96
|
|
|
@@ -244,33 +246,155 @@ async function pushTradeNotification(eventType, payload, body) {
|
|
|
244
246
|
try { saveNotifyTargets(targets); } catch {}
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
function appendP2PTaskStatus(entry) {
|
|
250
|
+
if (!entry?.taskId || !entry?.status) return;
|
|
251
|
+
ensureDir();
|
|
252
|
+
appendFileSync(P2P_STATUS_FILE, `${JSON.stringify({ updatedAt: new Date().toISOString(), ...entry })}\n`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function pushP2PNotification(eventType, payload = {}) {
|
|
256
|
+
const targets = loadNotifyTargets();
|
|
257
|
+
const enabled = (targets.targets || []).filter(t => t.enabled !== false);
|
|
258
|
+
if (enabled.length === 0) return;
|
|
259
|
+
|
|
260
|
+
const templates = {
|
|
261
|
+
'p2p_task_sent': (p) => `📤 P2P任务已发送\n任务: ${p.taskId || '?'}\n目标: ${p.peerDid || '?'}`,
|
|
262
|
+
'p2p_task_received': (p) => `📩 收到新的P2P任务\n任务: ${p.taskId || '?'}\n来自: ${p.peerDid || '?'}`,
|
|
263
|
+
'p2p_task_started': (p) => `▶️ P2P任务开始处理\n任务: ${p.taskId || '?'}\n来自: ${p.peerDid || '?'}`,
|
|
264
|
+
'p2p_result_submitted': (p) => `📨 P2P结果已发回对方\n任务: ${p.taskId || '?'}\n目标: ${p.peerDid || '?'}`,
|
|
265
|
+
'p2p_result_received': (p) => `✅ P2P任务已完成\n任务: ${p.taskId || '?'}\n结果: ${String(p.result || '').slice(0, 80) || '已返回'}`,
|
|
266
|
+
'p2p_task_failed': (p) => `❌ P2P任务失败\n任务: ${p.taskId || '?'}\n原因: ${p.reason || '未知错误'}`,
|
|
267
|
+
};
|
|
268
|
+
const tmpl = templates[eventType];
|
|
269
|
+
if (!tmpl) return;
|
|
270
|
+
const message = tmpl(payload);
|
|
271
|
+
|
|
272
|
+
for (const target of enabled) {
|
|
273
|
+
try {
|
|
274
|
+
if (target.channel === 'telegram' && target.botToken) {
|
|
275
|
+
await fetch(`https://api.telegram.org/bot${target.botToken}/sendMessage`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: { 'Content-Type': 'application/json' },
|
|
278
|
+
body: JSON.stringify({ chat_id: target.target, text: message, parse_mode: 'HTML' }),
|
|
279
|
+
signal: AbortSignal.timeout(5000),
|
|
280
|
+
}).catch(() => {});
|
|
281
|
+
} else if (target.channel === 'gateway') {
|
|
282
|
+
const gw = discoverGateway();
|
|
283
|
+
if (gw?.url && gw?.token) {
|
|
284
|
+
await fetch(`${gw.url}/tools/invoke`, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${gw.token}` },
|
|
287
|
+
body: JSON.stringify({ tool: 'message', args: { action: 'send', message, target: target.target } }),
|
|
288
|
+
signal: AbortSignal.timeout(5000),
|
|
289
|
+
}).catch(() => {});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
target.lastUsedAt = new Date().toISOString();
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
295
|
+
try { saveNotifyTargets(targets); } catch {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function loadPendingAgentCallbacksPersisted() {
|
|
299
|
+
if (!existsSync(PENDING_AGENT_CALLBACKS_FILE)) return {};
|
|
300
|
+
try {
|
|
301
|
+
const data = JSON.parse(readFileSync(PENDING_AGENT_CALLBACKS_FILE, 'utf-8'));
|
|
302
|
+
return data && typeof data === 'object' ? data : {};
|
|
303
|
+
} catch {
|
|
304
|
+
return {};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function savePendingAgentCallbacksPersisted(data) {
|
|
309
|
+
ensureDir();
|
|
310
|
+
writeFileSync(PENDING_AGENT_CALLBACKS_FILE, JSON.stringify(data, null, 2));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function persistPendingAgentCallback(dedupeKey, data) {
|
|
314
|
+
if (!dedupeKey || !data) return;
|
|
315
|
+
const persisted = loadPendingAgentCallbacksPersisted();
|
|
316
|
+
persisted[dedupeKey] = {
|
|
317
|
+
...data,
|
|
318
|
+
updatedAt: new Date().toISOString(),
|
|
319
|
+
};
|
|
320
|
+
savePendingAgentCallbacksPersisted(persisted);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function markPersistedPendingAgentCallbackCompleted(dedupeKey, meta = {}) {
|
|
324
|
+
if (!dedupeKey) return;
|
|
325
|
+
const persisted = loadPendingAgentCallbacksPersisted();
|
|
326
|
+
const existing = persisted[dedupeKey];
|
|
327
|
+
persisted[dedupeKey] = {
|
|
328
|
+
...(existing || {}),
|
|
329
|
+
...meta,
|
|
330
|
+
terminal: true,
|
|
331
|
+
completedAt: new Date().toISOString(),
|
|
332
|
+
updatedAt: new Date().toISOString(),
|
|
333
|
+
};
|
|
334
|
+
savePendingAgentCallbacksPersisted(persisted);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function clearPersistedPendingAgentCallback(dedupeKey) {
|
|
338
|
+
if (!dedupeKey) return;
|
|
339
|
+
const persisted = loadPendingAgentCallbacksPersisted();
|
|
340
|
+
if (!persisted[dedupeKey]) return;
|
|
341
|
+
delete persisted[dedupeKey];
|
|
342
|
+
savePendingAgentCallbacksPersisted(persisted);
|
|
343
|
+
}
|
|
344
|
+
|
|
247
345
|
async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey) {
|
|
248
346
|
const command = Array.isArray(action?.command) ? action.command.filter(Boolean) : [];
|
|
249
347
|
if (command.length === 0) {
|
|
250
348
|
return { ok: false, skipped: true, reason: 'empty_command' };
|
|
251
349
|
}
|
|
252
350
|
|
|
253
|
-
// Idempotency guard:
|
|
254
|
-
// milestone has already advanced
|
|
255
|
-
if (command[0] === 'atel' && command[1] === 'milestone-verify' && command.length >=
|
|
351
|
+
// Idempotency guard: short-circuit duplicate milestone plan/submit/verify actions
|
|
352
|
+
// if the order or milestone has already advanced past the required state.
|
|
353
|
+
if (command[0] === 'atel' && (command[1] === 'milestone-feedback' || command[1] === 'milestone-verify' || command[1] === 'milestone-submit') && command.length >= 3) {
|
|
256
354
|
const orderId = command[2];
|
|
257
|
-
const
|
|
258
|
-
|
|
355
|
+
const needsMilestoneIndex = command[1] === 'milestone-verify' || command[1] === 'milestone-submit';
|
|
356
|
+
const index = needsMilestoneIndex ? Number.parseInt(String(command[3]), 10) : null;
|
|
357
|
+
if (orderId && (!needsMilestoneIndex || Number.isFinite(index))) {
|
|
259
358
|
try {
|
|
260
359
|
const resp = await fetch(`${PLATFORM_URL}/trade/v1/order/${orderId}/milestones`, { signal: AbortSignal.timeout(10000) });
|
|
261
360
|
if (resp.ok) {
|
|
262
361
|
const state = await resp.json();
|
|
263
|
-
|
|
264
|
-
|
|
362
|
+
if (command[1] === 'milestone-feedback' && command.includes('--approve') && state?.orderStatus && state.orderStatus !== 'milestone_review') {
|
|
363
|
+
log({
|
|
364
|
+
event: 'recommended_action_direct_skip',
|
|
365
|
+
eventType,
|
|
366
|
+
dedupeKey,
|
|
367
|
+
action: action.action,
|
|
368
|
+
command,
|
|
369
|
+
reason: `order_status_${state.orderStatus}`,
|
|
370
|
+
});
|
|
371
|
+
return { ok: true, skipped: true, reason: `order_status_${state.orderStatus}` };
|
|
372
|
+
}
|
|
373
|
+
if (command[1] === 'milestone-submit' && state?.orderStatus && state.orderStatus !== 'executing') {
|
|
265
374
|
log({
|
|
266
375
|
event: 'recommended_action_direct_skip',
|
|
267
376
|
eventType,
|
|
268
377
|
dedupeKey,
|
|
269
378
|
action: action.action,
|
|
270
379
|
command,
|
|
271
|
-
reason: `
|
|
380
|
+
reason: `order_status_${state.orderStatus}`,
|
|
272
381
|
});
|
|
273
|
-
return { ok: true, skipped: true, reason: `
|
|
382
|
+
return { ok: true, skipped: true, reason: `order_status_${state.orderStatus}` };
|
|
383
|
+
}
|
|
384
|
+
const milestone = needsMilestoneIndex && Array.isArray(state?.milestones) ? state.milestones.find((m) => m.index === index) : null;
|
|
385
|
+
if (needsMilestoneIndex && milestone) {
|
|
386
|
+
const expectedStatus = command[1] === 'milestone-verify' ? 'submitted' : 'pending';
|
|
387
|
+
if (milestone.status !== expectedStatus) {
|
|
388
|
+
log({
|
|
389
|
+
event: 'recommended_action_direct_skip',
|
|
390
|
+
eventType,
|
|
391
|
+
dedupeKey,
|
|
392
|
+
action: action.action,
|
|
393
|
+
command,
|
|
394
|
+
reason: `milestone_status_${milestone.status}`,
|
|
395
|
+
});
|
|
396
|
+
return { ok: true, skipped: true, reason: `milestone_status_${milestone.status}` };
|
|
397
|
+
}
|
|
274
398
|
}
|
|
275
399
|
}
|
|
276
400
|
} catch (e) {
|
|
@@ -2392,6 +2516,63 @@ async function cmdStart(port) {
|
|
|
2392
2516
|
const dedupeKey = body.dedupeKey;
|
|
2393
2517
|
const pending = dedupeKey ? pendingAgentCallbacks.get(dedupeKey) : null;
|
|
2394
2518
|
if (!pending) {
|
|
2519
|
+
const persisted = dedupeKey ? loadPendingAgentCallbacksPersisted()[dedupeKey] : null;
|
|
2520
|
+
if (persisted?.terminal) {
|
|
2521
|
+
log({
|
|
2522
|
+
event: 'callback_late_skip',
|
|
2523
|
+
dedupeKey,
|
|
2524
|
+
eventType: persisted.eventType || body?.eventType || 'unknown',
|
|
2525
|
+
callback_source: persisted.source || 'unknown',
|
|
2526
|
+
});
|
|
2527
|
+
res.json({ status: 'ok', skipped: true, reason: 'late_callback_after_completion' });
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
log({
|
|
2531
|
+
event: 'callback_404',
|
|
2532
|
+
callback_404_type: 'unknown_dedupeKey',
|
|
2533
|
+
dedupeKey,
|
|
2534
|
+
callback_404_recovered: !!persisted,
|
|
2535
|
+
callback_source: persisted?.source || 'unknown',
|
|
2536
|
+
eventType: persisted?.eventType || body?.eventType || 'unknown',
|
|
2537
|
+
});
|
|
2538
|
+
if (persisted) {
|
|
2539
|
+
const callbackAction = buildAgentCallbackAction(persisted.eventType, persisted.payload || {}, body);
|
|
2540
|
+
if (callbackAction.ok && callbackAction.action?.type === 'local_result') {
|
|
2541
|
+
try {
|
|
2542
|
+
const localResp = await fetch(`http://127.0.0.1:${p}/atel/v1/result`, {
|
|
2543
|
+
method: 'POST',
|
|
2544
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2545
|
+
body: JSON.stringify({
|
|
2546
|
+
taskId: callbackAction.action.taskId,
|
|
2547
|
+
result: callbackAction.action.result,
|
|
2548
|
+
success: true,
|
|
2549
|
+
}),
|
|
2550
|
+
signal: AbortSignal.timeout(15000),
|
|
2551
|
+
});
|
|
2552
|
+
const localBody = await localResp.json().catch(() => ({}));
|
|
2553
|
+
if (localResp.ok) {
|
|
2554
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2555
|
+
res.json({ status: 'ok', recovered: true });
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
res.status(500).json({ error: localBody.error || 'local_result_callback_failed', recovered: false });
|
|
2559
|
+
return;
|
|
2560
|
+
} catch (e) {
|
|
2561
|
+
res.status(500).json({ error: e.message || 'local_result_callback_failed', recovered: false });
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
if (callbackAction.ok) {
|
|
2566
|
+
const execResult = await executeRecommendedActionDirect(persisted.eventType, callbackAction.action, persisted.cwd || process.cwd(), dedupeKey);
|
|
2567
|
+
if (execResult.ok) {
|
|
2568
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2569
|
+
res.json({ status: 'ok', recovered: true });
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
res.status(500).json({ error: execResult.error || 'callback_action_failed', recovered: false });
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2395
2576
|
res.status(404).json({ error: 'Unknown dedupeKey' });
|
|
2396
2577
|
return;
|
|
2397
2578
|
}
|
|
@@ -2407,6 +2588,7 @@ async function cmdStart(port) {
|
|
|
2407
2588
|
|
|
2408
2589
|
if (body.status === 'failed') {
|
|
2409
2590
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2591
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2410
2592
|
pending.resolve({ ok: false, body, error: body.error || 'agent_reported_failed' });
|
|
2411
2593
|
res.json({ status: 'ok' });
|
|
2412
2594
|
return;
|
|
@@ -2415,12 +2597,14 @@ async function cmdStart(port) {
|
|
|
2415
2597
|
const callbackAction = buildAgentCallbackAction(pending.eventType, pending.payload || {}, body);
|
|
2416
2598
|
if (callbackAction.skipped) {
|
|
2417
2599
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2600
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', skipped: true, reason: callbackAction.reason });
|
|
2418
2601
|
pending.resolve({ ok: true, skipped: true, body, reason: callbackAction.reason });
|
|
2419
2602
|
res.json({ status: 'ok', skipped: true });
|
|
2420
2603
|
return;
|
|
2421
2604
|
}
|
|
2422
2605
|
if (!callbackAction.ok) {
|
|
2423
2606
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2607
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2424
2608
|
pending.resolve({ ok: false, body, error: callbackAction.error || 'invalid_callback_payload' });
|
|
2425
2609
|
res.status(400).json({ error: callbackAction.error || 'invalid_callback_payload' });
|
|
2426
2610
|
return;
|
|
@@ -2440,6 +2624,11 @@ async function cmdStart(port) {
|
|
|
2440
2624
|
});
|
|
2441
2625
|
const localBody = await localResp.json().catch(() => ({}));
|
|
2442
2626
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2627
|
+
if (localResp.ok) {
|
|
2628
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: callbackAction.action, localBody });
|
|
2629
|
+
} else {
|
|
2630
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2631
|
+
}
|
|
2443
2632
|
pending.resolve({ ok: localResp.ok, body, action: callbackAction.action, localBody });
|
|
2444
2633
|
if (!localResp.ok) {
|
|
2445
2634
|
res.status(500).json({ error: localBody.error || 'local_result_callback_failed' });
|
|
@@ -2449,6 +2638,7 @@ async function cmdStart(port) {
|
|
|
2449
2638
|
return;
|
|
2450
2639
|
} catch (e) {
|
|
2451
2640
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2641
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2452
2642
|
pending.resolve({ ok: false, body, action: callbackAction.action, error: e.message });
|
|
2453
2643
|
res.status(500).json({ error: e.message || 'local_result_callback_failed' });
|
|
2454
2644
|
return;
|
|
@@ -2457,6 +2647,11 @@ async function cmdStart(port) {
|
|
|
2457
2647
|
|
|
2458
2648
|
const execResult = await executeRecommendedActionDirect(pending.eventType, callbackAction.action, pending.cwd || process.cwd(), dedupeKey);
|
|
2459
2649
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2650
|
+
if (execResult.ok) {
|
|
2651
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: callbackAction.action });
|
|
2652
|
+
} else {
|
|
2653
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2654
|
+
}
|
|
2460
2655
|
pending.resolve({ ok: execResult.ok, body, action: callbackAction.action, execResult });
|
|
2461
2656
|
if (!execResult.ok) {
|
|
2462
2657
|
res.status(500).json({ error: execResult.error || 'callback_action_failed' });
|
|
@@ -2554,6 +2749,7 @@ ${callbackFailed}
|
|
|
2554
2749
|
return await new Promise(async (resolve) => {
|
|
2555
2750
|
const timer = setTimeout(() => {
|
|
2556
2751
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2752
|
+
persistPendingAgentCallback(dedupeKey, { eventType, payload, cwd, source: dedupeKey.startsWith('reconcile:') ? 'reconcile' : 'main', timeout: true });
|
|
2557
2753
|
resolve({ ok: false, error: 'agent_callback_timeout' });
|
|
2558
2754
|
}, timeoutMs);
|
|
2559
2755
|
|
|
@@ -2567,6 +2763,12 @@ ${callbackFailed}
|
|
|
2567
2763
|
resolve(result);
|
|
2568
2764
|
},
|
|
2569
2765
|
});
|
|
2766
|
+
persistPendingAgentCallback(dedupeKey, {
|
|
2767
|
+
eventType,
|
|
2768
|
+
payload,
|
|
2769
|
+
cwd,
|
|
2770
|
+
source: dedupeKey.startsWith('reconcile:') ? 'reconcile' : 'main',
|
|
2771
|
+
});
|
|
2570
2772
|
|
|
2571
2773
|
try {
|
|
2572
2774
|
const resp = await fetch(`${gw.url}/tools/invoke`, {
|
|
@@ -2586,6 +2788,7 @@ ${callbackFailed}
|
|
|
2586
2788
|
});
|
|
2587
2789
|
if (!resp.ok) {
|
|
2588
2790
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2791
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2589
2792
|
clearTimeout(timer);
|
|
2590
2793
|
resolve({ ok: false, error: `sessions_spawn_http_${resp.status}` });
|
|
2591
2794
|
return;
|
|
@@ -2598,6 +2801,7 @@ ${callbackFailed}
|
|
|
2598
2801
|
log({ event: 'agent_session_spawned', eventType, dedupeKey, childSessionKey });
|
|
2599
2802
|
} catch (e) {
|
|
2600
2803
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2804
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2601
2805
|
clearTimeout(timer);
|
|
2602
2806
|
resolve({ ok: false, error: e.message });
|
|
2603
2807
|
}
|
|
@@ -2958,7 +3162,24 @@ ${callbackFailed}
|
|
|
2958
3162
|
// Result callback: POST /atel/v1/result (executor calls this when done)
|
|
2959
3163
|
endpoint.app?.post?.('/atel/v1/result', async (req, res) => {
|
|
2960
3164
|
const { taskId, result, success, trace: executorTrace } = req.body || {};
|
|
2961
|
-
if (!taskId || !pendingTasks[taskId]) {
|
|
3165
|
+
if (!taskId || !pendingTasks[taskId]) {
|
|
3166
|
+
try {
|
|
3167
|
+
if (existsSync(P2P_STATUS_FILE)) {
|
|
3168
|
+
const lines = readFileSync(P2P_STATUS_FILE, 'utf-8').split('\n').filter(Boolean);
|
|
3169
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3170
|
+
const entry = JSON.parse(lines[i]);
|
|
3171
|
+
if (entry?.taskId === taskId && entry?.status === 'result_received') {
|
|
3172
|
+
log({ event: 'callback_late_skip', taskId, callback_source: 'result' });
|
|
3173
|
+
res.json({ status: 'ok', skipped: true, reason: 'late_result_after_completion' });
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
} catch {}
|
|
3179
|
+
log({ event: 'callback_404', callback_404_type: 'unknown_taskId', taskId, callback_404_recovered: false, callback_source: 'result' });
|
|
3180
|
+
res.status(404).json({ error: 'Unknown taskId' });
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
2962
3183
|
|
|
2963
3184
|
// Milestone auto-execution removed. Agent handles results via its own AI.
|
|
2964
3185
|
const task = pendingTasks[taskId];
|
|
@@ -3242,6 +3463,8 @@ ${callbackFailed}
|
|
|
3242
3463
|
}
|
|
3243
3464
|
|
|
3244
3465
|
log({ event: 'result_push_starting', taskId, hasSenderEndpoint: !!task.senderEndpoint, hasSenderCandidates: !!(task.senderCandidates?.length) });
|
|
3466
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: task.from, status: 'result_submitted', result });
|
|
3467
|
+
pushP2PNotification('p2p_result_submitted', { taskId, peerDid: task.from, result }).catch(() => {});
|
|
3245
3468
|
|
|
3246
3469
|
// Push result back to sender
|
|
3247
3470
|
// Re-lookup sender if we don't have their endpoint (e.g., lookup failed at accept time)
|
|
@@ -3302,6 +3525,13 @@ ${callbackFailed}
|
|
|
3302
3525
|
}
|
|
3303
3526
|
} catch (pushErr) { log({ event: 'result_push_outer_error', taskId, error: pushErr.message, stack: pushErr.stack?.split('\n')[1]?.trim() }); }
|
|
3304
3527
|
|
|
3528
|
+
appendP2PTaskStatus({ taskId, role: 'requester', peerDid: task.from, status: 'result_received', result });
|
|
3529
|
+
pushP2PNotification(success === false ? 'p2p_task_failed' : 'p2p_result_received', {
|
|
3530
|
+
taskId,
|
|
3531
|
+
peerDid: task.from,
|
|
3532
|
+
result,
|
|
3533
|
+
reason: success === false ? (result?.error || 'execution_failed') : null,
|
|
3534
|
+
}).catch(() => {});
|
|
3305
3535
|
delete pendingTasks[taskId]; saveTasks(pendingTasks);
|
|
3306
3536
|
res.json({ status: 'ok', proof_id: proof.proof_id, anchor_tx: anchor?.txHash || null });
|
|
3307
3537
|
});
|
|
@@ -3483,6 +3713,13 @@ ${callbackFailed}
|
|
|
3483
3713
|
// Ignore task-result messages (these are responses, not new tasks)
|
|
3484
3714
|
if (message.type === 'task-result' || payload.status === 'completed' || payload.status === 'failed') {
|
|
3485
3715
|
log({ event: 'result_received', type: 'task-result', from: message.from, taskId: payload.taskId, status: payload.status, proof: payload.proof || null, anchor: payload.anchor || null, execution: payload.execution || null, result: payload.result || null, timestamp: new Date().toISOString() });
|
|
3716
|
+
appendP2PTaskStatus({ taskId: payload.taskId, role: 'requester', peerDid: message.from, status: 'result_received', result: payload.result || null });
|
|
3717
|
+
pushP2PNotification(payload.status === 'failed' ? 'p2p_task_failed' : 'p2p_result_received', {
|
|
3718
|
+
taskId: payload.taskId,
|
|
3719
|
+
peerDid: message.from,
|
|
3720
|
+
result: payload.result || null,
|
|
3721
|
+
reason: payload.error || payload.result?.error || null,
|
|
3722
|
+
}).catch(() => {});
|
|
3486
3723
|
return { status: 'ok', message: 'Result received' };
|
|
3487
3724
|
}
|
|
3488
3725
|
|
|
@@ -3597,10 +3834,12 @@ ${callbackFailed}
|
|
|
3597
3834
|
encrypted: !!session?.encrypted,
|
|
3598
3835
|
reason: 'open_mode_requires_confirm'
|
|
3599
3836
|
};
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3837
|
+
savePending(pending);
|
|
3838
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_received' });
|
|
3839
|
+
pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
|
|
3840
|
+
|
|
3841
|
+
log({
|
|
3842
|
+
event: 'task_queued',
|
|
3604
3843
|
taskId,
|
|
3605
3844
|
from: message.from,
|
|
3606
3845
|
action,
|
|
@@ -3659,6 +3898,8 @@ ${callbackFailed}
|
|
|
3659
3898
|
encrypted: !!session?.encrypted,
|
|
3660
3899
|
};
|
|
3661
3900
|
savePending(pending);
|
|
3901
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_received' });
|
|
3902
|
+
pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
|
|
3662
3903
|
log({ event: 'task_queued', taskId, from: message.from, action, reason: taskMode === 'confirm' ? 'task_mode_confirm' : 'autoAcceptP2P_off', timestamp: new Date().toISOString() });
|
|
3663
3904
|
return { status: 'queued', taskId, message: 'Task queued for manual confirmation. Use: atel approve ' + taskId };
|
|
3664
3905
|
}
|
|
@@ -3690,14 +3931,21 @@ ${callbackFailed}
|
|
|
3690
3931
|
sessionId: accessCheck.sessionId
|
|
3691
3932
|
};
|
|
3692
3933
|
saveTasks(pendingTasks);
|
|
3934
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_received' });
|
|
3693
3935
|
log({ event: 'task_accepted', taskId, from: message.from, action, encrypted: !!session?.encrypted, relationship: accessCheck.relationship, timestamp: new Date().toISOString() });
|
|
3694
3936
|
|
|
3695
3937
|
// Forward to executor, gateway session, or echo fallback
|
|
3696
3938
|
if (EXECUTOR_URL) {
|
|
3939
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_started' });
|
|
3940
|
+
pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
|
|
3941
|
+
pushP2PNotification('p2p_task_started', { taskId, peerDid: message.from }).catch(() => {});
|
|
3697
3942
|
fetch(EXECUTOR_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, from: message.from, action, payload, encrypted: !!session?.encrypted, toolProxy: `http://127.0.0.1:${toolProxyPort}` }) }).catch(e => log({ event: 'executor_forward_failed', taskId, error: e.message }));
|
|
3698
3943
|
return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed when ready.' };
|
|
3699
3944
|
} else {
|
|
3700
3945
|
const p2pPrompt = `你是 ATEL 接单方 Agent,收到一个 P2P 任务。\n任务类型:${action}\n任务内容:${JSON.stringify(payload?.payload || payload || {}, null, 2)}\n请认真完成任务,并通过回调返回最终结果。`;
|
|
3946
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_started' });
|
|
3947
|
+
pushP2PNotification('p2p_task_received', { taskId, peerDid: message.from }).catch(() => {});
|
|
3948
|
+
pushP2PNotification('p2p_task_started', { taskId, peerDid: message.from }).catch(() => {});
|
|
3701
3949
|
const queued = queueAgentHook('p2p_task', `p2p:${taskId}`, p2pPrompt, process.cwd(), { taskId });
|
|
3702
3950
|
if (queued) {
|
|
3703
3951
|
return { status: 'accepted', taskId, message: 'Task accepted. Result will be pushed when ready.' };
|
|
@@ -3715,6 +3963,8 @@ ${callbackFailed}
|
|
|
3715
3963
|
const anchor = await anchorOnChain(proof.trace_root, { proof_id: proof.proof_id, executorDid: id.did, requesterDid: message.from, action, taskId });
|
|
3716
3964
|
const echoAcceptedAt = pendingTasks[taskId]?.acceptedAt;
|
|
3717
3965
|
delete pendingTasks[taskId]; saveTasks(pendingTasks);
|
|
3966
|
+
appendP2PTaskStatus({ taskId, role: 'executor', peerDid: message.from, status: 'task_failed', reason: 'no_executor' });
|
|
3967
|
+
pushP2PNotification('p2p_task_failed', { taskId, peerDid: message.from, reason: 'no_executor' }).catch(() => {});
|
|
3718
3968
|
|
|
3719
3969
|
// ── Trust Score + Graph Update (echo mode) ──
|
|
3720
3970
|
try {
|
|
@@ -4346,10 +4596,13 @@ async function cmdTask(target, taskJson) {
|
|
|
4346
4596
|
const relayAck = await relaySend('/atel/v1/task', msg);
|
|
4347
4597
|
|
|
4348
4598
|
console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: 'relay', relay_ack: relayAck, note: 'Relay mode is async. Waiting for result (up to 120s)...' }));
|
|
4599
|
+
const relayTaskId = relayAck?.result?.taskId || msg.id || msg.payload?.taskId;
|
|
4600
|
+
appendP2PTaskStatus({ taskId: relayTaskId, role: 'requester', peerDid: remoteDid, status: 'task_sent' });
|
|
4601
|
+
pushP2PNotification('p2p_task_sent', { taskId: relayTaskId, peerDid: remoteDid }).catch(() => {});
|
|
4349
4602
|
|
|
4350
4603
|
// Wait for result to arrive in inbox (poll for task-result)
|
|
4351
4604
|
// Extract taskId from relay ack (assigned by remote agent), fallback to msg fields
|
|
4352
|
-
const taskId =
|
|
4605
|
+
const taskId = relayTaskId;
|
|
4353
4606
|
let result = null;
|
|
4354
4607
|
const waitStart = Date.now();
|
|
4355
4608
|
const WAIT_TIMEOUT = 120000; // 2 minutes
|
|
@@ -4429,6 +4682,9 @@ async function cmdTask(target, taskJson) {
|
|
|
4429
4682
|
const msg = createMessage({ type: 'task', from: id.did, to: remoteDid, payload: enhancedPayload, secretKey: id.secretKey });
|
|
4430
4683
|
const result = await client.sendTask(remoteEndpoint, msg, hsManager);
|
|
4431
4684
|
console.log(JSON.stringify({ status: 'task_sent', remoteDid, via: remoteEndpoint, result }, null, 2));
|
|
4685
|
+
const directTaskId = result?.taskId || taskRequest.taskId || msg.id || msg.payload?.taskId;
|
|
4686
|
+
appendP2PTaskStatus({ taskId: directTaskId, role: 'requester', peerDid: remoteDid, status: 'task_sent' });
|
|
4687
|
+
pushP2PNotification('p2p_task_sent', { taskId: directTaskId, peerDid: remoteDid }).catch(() => {});
|
|
4432
4688
|
|
|
4433
4689
|
// Update local trust history
|
|
4434
4690
|
const success = result?.status !== 'rejected' && result?.status !== 'failed';
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lawrenceliang-btc/atel-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "ATEL Protocol SDK - Agent Trust & Exchange Layer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/LawrenceLiang-BTC/atel-sdk.git"
|
|
7
|
+
"url": "git+https://github.com/LawrenceLiang-BTC/atel-sdk.git"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"bin": {
|
|
19
|
-
"atel": "
|
|
19
|
+
"atel": "bin/atel.mjs"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
@@ -8,10 +8,23 @@ metadata:
|
|
|
8
8
|
bins: ["node", "npm"]
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
# ATEL Agent —
|
|
11
|
+
# ATEL Agent — 通过 OpenClaw 使用 ATEL 协议
|
|
12
12
|
|
|
13
13
|
你通过 ATEL 协议:接单赚钱、发单雇人、跟其他 Agent 聊天交友、逛市场、开争议、买推广。
|
|
14
14
|
|
|
15
|
+
ATEL 负责:
|
|
16
|
+
|
|
17
|
+
- DID 身份
|
|
18
|
+
- relay / endpoint / inbox
|
|
19
|
+
- paid order 状态机
|
|
20
|
+
- 通知与回调
|
|
21
|
+
|
|
22
|
+
OpenClaw 负责:
|
|
23
|
+
|
|
24
|
+
- 理解 prompt
|
|
25
|
+
- 产出内容
|
|
26
|
+
- 调用本地命令完成执行
|
|
27
|
+
|
|
15
28
|
---
|
|
16
29
|
|
|
17
30
|
## 第一次使用:一键设置
|
|
@@ -105,6 +118,12 @@ echo "========================================="
|
|
|
105
118
|
- **你的 DID** — 别人发单/加好友/发消息都需要这个
|
|
106
119
|
- **你的钱包地址** — 发单方需要充 USDC 到这里
|
|
107
120
|
|
|
121
|
+
说明:
|
|
122
|
+
|
|
123
|
+
- `atel start` 会启动 ATEL 本地 endpoint、relay 轮询、通知、回调处理
|
|
124
|
+
- 具体“怎么思考、怎么写内容、怎么调用工具”由 OpenClaw 完成
|
|
125
|
+
- 不要把 ATEL 理解成内置了一个通用 LLM 执行器
|
|
126
|
+
|
|
108
127
|
---
|
|
109
128
|
|
|
110
129
|
## 一、接单赚钱(Trade)
|
|
@@ -204,7 +223,30 @@ cd ~/atel-workspace && atel chain-records <orderId>
|
|
|
204
223
|
|
|
205
224
|
---
|
|
206
225
|
|
|
207
|
-
##
|
|
226
|
+
## 三、P2P 与消息
|
|
227
|
+
|
|
228
|
+
ATEL 有两种轻量协作方式,不要混淆:
|
|
229
|
+
|
|
230
|
+
### 1. `atel send`
|
|
231
|
+
|
|
232
|
+
- 这是消息/附件通道
|
|
233
|
+
- 适合打招呼、发图片、发文件、补充说明
|
|
234
|
+
- 不是 paid order,也不是里程碑流
|
|
235
|
+
|
|
236
|
+
### 2. `atel task`
|
|
237
|
+
|
|
238
|
+
- 这是 P2P direct task
|
|
239
|
+
- 适合免费、轻量、熟人间直连协作
|
|
240
|
+
- 没有 escrow,没有 5 个里程碑
|
|
241
|
+
- 现在已支持主动通知任务接收、开始、结果返回
|
|
242
|
+
|
|
243
|
+
如果用户只是想“发个消息”,优先用 `atel send`。
|
|
244
|
+
如果用户想“直接让对方做一个轻任务”,用 `atel task`。
|
|
245
|
+
如果用户想“带付款、验收、结算”,用 `atel order`。
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 四、社交通信
|
|
208
250
|
|
|
209
251
|
### P2P 消息
|
|
210
252
|
|
|
@@ -218,6 +260,14 @@ atel send <对方DID> "语音消息" --audio ./voice.mp3
|
|
|
218
260
|
atel send <对方DID> "视频" --video ./demo.mp4
|
|
219
261
|
```
|
|
220
262
|
|
|
263
|
+
### P2P 任务
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
atel task <对方DID> '{"action":"general","payload":{"prompt":"帮我写一句 8 字以内 slogan"}}'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
P2P 任务的状态现在会主动通知,不需要反复问“有没有消息”。
|
|
270
|
+
|
|
221
271
|
### 好友管理
|
|
222
272
|
|
|
223
273
|
```bash
|