@lawrenceliang-btc/atel-sdk 1.1.7 → 1.1.9
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 +25 -0
- package/bin/atel.mjs +249 -26
- package/package.json +1 -1
- package/skill/atel-agent/SKILL.md +84 -4
package/README.md
CHANGED
|
@@ -21,6 +21,10 @@ ATEL provides the cryptographic primitives and protocol building blocks that ena
|
|
|
21
21
|
- ATEL handles DID identity, relay, inbox, callback, notification, and paid order state
|
|
22
22
|
- OpenClaw or your own runtime handles reasoning and tool use
|
|
23
23
|
- Cross-platform CLI (Linux/macOS/Windows)
|
|
24
|
+
- Paid Platform orders currently support two settlement chains:
|
|
25
|
+
- `Base`
|
|
26
|
+
- `BSC`
|
|
27
|
+
- For paid orders, the chain truth source is always `order.chain`
|
|
24
28
|
|
|
25
29
|
### P2P Friend System
|
|
26
30
|
- Relationship-based access control (friends-only mode)
|
|
@@ -59,6 +63,14 @@ atel register "My Agent" "assistant,research"
|
|
|
59
63
|
atel start 3100
|
|
60
64
|
```
|
|
61
65
|
|
|
66
|
+
If you want to support paid Platform orders on EVM chains, configure at least one paid-order chain key before or after registering:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export ATEL_BASE_PRIVATE_KEY=...
|
|
70
|
+
# or
|
|
71
|
+
export ATEL_BSC_PRIVATE_KEY=...
|
|
72
|
+
```
|
|
73
|
+
|
|
62
74
|
### Recommended Runtime
|
|
63
75
|
|
|
64
76
|
ATEL is not a built-in general-purpose LLM executor. The recommended setup is:
|
|
@@ -76,6 +88,8 @@ atel start 3100
|
|
|
76
88
|
|
|
77
89
|
For custom runtimes, point `ATEL_EXECUTOR_URL` at your own service.
|
|
78
90
|
|
|
91
|
+
For paid orders, do not hardcode Base as the only chain. Runtime actions that touch escrow, release, refund, milestone anchoring, chain-record inspection, or balance interpretation must follow `order.chain`.
|
|
92
|
+
|
|
79
93
|
## Architecture
|
|
80
94
|
|
|
81
95
|
ATEL is organized into protocol and runtime layers:
|
|
@@ -170,6 +184,17 @@ atel milestone-submit <orderId> <index> --result # Submit milestone result
|
|
|
170
184
|
atel milestone-verify <orderId> <index> --pass # Verify submitted milestone
|
|
171
185
|
```
|
|
172
186
|
|
|
187
|
+
Notes:
|
|
188
|
+
|
|
189
|
+
- Paid Platform orders are currently supported on `Base` and `BSC`
|
|
190
|
+
- Before acting on a paid order, inspect `atel order-info <orderId>` or `atel milestone-status <orderId>`
|
|
191
|
+
- Treat `order.chain` as the only source of truth for:
|
|
192
|
+
- smart wallet
|
|
193
|
+
- escrow
|
|
194
|
+
- release / refund
|
|
195
|
+
- chain-records
|
|
196
|
+
- chain-side balance interpretation
|
|
197
|
+
|
|
173
198
|
## API Examples
|
|
174
199
|
|
|
175
200
|
### Identity & Signing
|
package/bin/atel.mjs
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
54
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
55
|
-
import { resolve, join } from 'node:path';
|
|
55
|
+
import { resolve, join, dirname } from 'node:path';
|
|
56
56
|
import crypto from 'node:crypto';
|
|
57
57
|
import {
|
|
58
58
|
AgentIdentity, AgentEndpoint, AgentClient, HandshakeManager,
|
|
@@ -91,6 +91,7 @@ const NOTIFY_TARGETS_FILE = resolve(ATEL_DIR, 'notify-targets.json');
|
|
|
91
91
|
const TRADE_TRACK_FILE = resolve(ATEL_DIR, 'tracked-orders.json');
|
|
92
92
|
const P2P_STATUS_FILE = resolve(ATEL_DIR, 'p2p-task-status.jsonl');
|
|
93
93
|
const PENDING_AGENT_CALLBACKS_FILE = resolve(ATEL_DIR, 'pending-agent-callbacks.json');
|
|
94
|
+
const ORDER_WORK_DIR = resolve(ATEL_DIR, 'order-workspaces');
|
|
94
95
|
const KEYS_DIR = resolve(ATEL_DIR, 'keys');
|
|
95
96
|
const ANCHOR_FILE = resolve(KEYS_DIR, 'anchor.json');
|
|
96
97
|
|
|
@@ -100,6 +101,97 @@ const DEFAULT_POLICY = { rateLimit: 60, maxPayloadBytes: 1048576, maxConcurrent:
|
|
|
100
101
|
|
|
101
102
|
function ensureDir() { if (!existsSync(ATEL_DIR)) mkdirSync(ATEL_DIR, { recursive: true }); }
|
|
102
103
|
|
|
104
|
+
function ensureOrderWorkspace(orderId, context = {}) {
|
|
105
|
+
ensureDir();
|
|
106
|
+
const safeOrderId = String(orderId || 'unknown').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
107
|
+
const dir = resolve(ORDER_WORK_DIR, safeOrderId);
|
|
108
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
109
|
+
const contextFile = join(dir, 'ORDER_CONTEXT.md');
|
|
110
|
+
const lines = [
|
|
111
|
+
`# ATEL Order Context`,
|
|
112
|
+
``,
|
|
113
|
+
`Order ID: ${orderId || ''}`,
|
|
114
|
+
`Chain: ${context.chain || ''}`,
|
|
115
|
+
`Role: ${context.role || ''}`,
|
|
116
|
+
`Status: ${context.status || ''}`,
|
|
117
|
+
`Phase: ${context.phase || ''}`,
|
|
118
|
+
`Current Milestone: ${context.currentMilestone ?? ''}`,
|
|
119
|
+
`Milestone Title: ${context.milestoneTitle || ''}`,
|
|
120
|
+
``,
|
|
121
|
+
`## Order Description`,
|
|
122
|
+
context.orderDescription || '',
|
|
123
|
+
``,
|
|
124
|
+
`## Milestone Objective`,
|
|
125
|
+
context.milestoneObjective || '',
|
|
126
|
+
``,
|
|
127
|
+
`## Submission Content`,
|
|
128
|
+
context.resultSummary || '',
|
|
129
|
+
``,
|
|
130
|
+
`## Previous Approved Outputs`,
|
|
131
|
+
context.previousApprovedOutputs || '',
|
|
132
|
+
``,
|
|
133
|
+
`## Hard Rules`,
|
|
134
|
+
`- Only work from the order description and milestone objective in this file.`,
|
|
135
|
+
`- Do not inspect unrelated local projects or repository content unless the order explicitly asks for repo analysis.`,
|
|
136
|
+
`- Do not infer a different project from stray files in the machine workspace.`,
|
|
137
|
+
`- Return only content that directly satisfies this order.`,
|
|
138
|
+
``,
|
|
139
|
+
];
|
|
140
|
+
writeFileSync(contextFile, lines.join('\n'));
|
|
141
|
+
return { dir, contextFile };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getOrderWorkspace(orderId, context = {}) {
|
|
145
|
+
if (!orderId) return { dir: process.cwd(), contextFile: '' };
|
|
146
|
+
return ensureOrderWorkspace(orderId, context);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getAtelWorkspaceRoot() {
|
|
150
|
+
return dirname(ATEL_DIR);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function shouldAllowRepoAccess(context = {}) {
|
|
154
|
+
const description = String(context?.orderDescription || '').toLowerCase();
|
|
155
|
+
const objective = String(context?.milestoneObjective || '').toLowerCase();
|
|
156
|
+
const resultSummary = String(context?.resultSummary || '').toLowerCase();
|
|
157
|
+
const previousApprovedOutputs = String(context?.previousApprovedOutputs || '').toLowerCase();
|
|
158
|
+
const combined = `${description}\n${objective}\n${resultSummary}\n${previousApprovedOutputs}`;
|
|
159
|
+
return /(repo|repository|codebase|仓库|代码库|项目代码|source code|read files|analyze code|修改代码|修复代码|实现功能)/i.test(combined);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function summarizeApprovedMilestones(milestones = [], beforeIndex = Number.MAX_SAFE_INTEGER) {
|
|
163
|
+
return (Array.isArray(milestones) ? milestones : [])
|
|
164
|
+
.filter((m) => m && m.status === 'verified' && Number.isFinite(m.index) && m.index < beforeIndex)
|
|
165
|
+
.sort((a, b) => a.index - b.index)
|
|
166
|
+
.map((m) => `M${m.index}: ${m.title || ''}\nResult: ${m.resultSummary || ''}`.trim())
|
|
167
|
+
.join('\n\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sanitizeAgentPrompt(promptText, meta = {}) {
|
|
171
|
+
const raw = typeof promptText === 'string' ? promptText : '';
|
|
172
|
+
const trimmed = raw.trim();
|
|
173
|
+
if (!trimmed) {
|
|
174
|
+
log({ event: 'agent_prompt_skip_empty', eventType: meta.eventType || 'unknown', dedupeKey: meta.dedupeKey || '' });
|
|
175
|
+
return '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Keep a large but bounded margin below upstream model limits. We only need
|
|
179
|
+
// concise task prompts here; oversized prompts add noise and can trigger
|
|
180
|
+
// upstream length validation errors.
|
|
181
|
+
const maxChars = 16000;
|
|
182
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
183
|
+
|
|
184
|
+
const truncated = `${trimmed.slice(0, maxChars)}\n\n[Prompt truncated by ATEL SDK to stay within model input limits.]`;
|
|
185
|
+
log({
|
|
186
|
+
event: 'agent_prompt_truncated',
|
|
187
|
+
eventType: meta.eventType || 'unknown',
|
|
188
|
+
dedupeKey: meta.dedupeKey || '',
|
|
189
|
+
originalChars: trimmed.length,
|
|
190
|
+
finalChars: truncated.length,
|
|
191
|
+
});
|
|
192
|
+
return truncated;
|
|
193
|
+
}
|
|
194
|
+
|
|
103
195
|
// ═══════════════════════════════════════════════════════════════════
|
|
104
196
|
// Notification Target System — auto-discover gateway, manage targets
|
|
105
197
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -204,6 +296,7 @@ async function pushTradeNotification(eventType, payload, body) {
|
|
|
204
296
|
if (enabled.length === 0) return;
|
|
205
297
|
|
|
206
298
|
const templates = {
|
|
299
|
+
'order_created': (p) => `📥 收到新订单\n订单: ${p.orderId || body?.orderId || '?'}\n金额: $${p.priceAmount ?? '?'} USDC\n来自: ${p.requesterDid || '未知请求方'}\n请审核后决定是否接单`,
|
|
207
300
|
'order_accepted': (p) => `📋 订单已被接单\n订单: ${p.orderId || body?.orderId || '?'}\n执行方已开始处理,进入里程碑阶段`,
|
|
208
301
|
'milestone_submitted': (p) => `📝 里程碑 M${p.milestoneIndex ?? '?'} 已提交\n订单: ${p.orderId || body?.orderId || '?'}\n等待审核`,
|
|
209
302
|
'milestone_verified': (p) => `✅ 里程碑 M${p.milestoneIndex ?? '?'} 审核通过\n订单: ${p.orderId || body?.orderId || '?'}`,
|
|
@@ -413,11 +506,12 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
413
506
|
const { execFile } = await import('child_process');
|
|
414
507
|
const childCmd = command[0] === 'atel' ? process.execPath : command[0];
|
|
415
508
|
const childArgs = command[0] === 'atel' ? [process.argv[1], ...command.slice(1)] : command.slice(1);
|
|
509
|
+
const childCwd = command[0] === 'atel' ? getAtelWorkspaceRoot() : cwd;
|
|
416
510
|
|
|
417
511
|
log({ event: 'recommended_action_direct_trigger', eventType, dedupeKey, action: action.action, command });
|
|
418
512
|
|
|
419
513
|
return await new Promise((resolve) => {
|
|
420
|
-
execFile(childCmd, childArgs, { timeout: 180000, cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
514
|
+
execFile(childCmd, childArgs, { timeout: 180000, cwd: childCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
421
515
|
if (err) {
|
|
422
516
|
log({
|
|
423
517
|
event: 'recommended_action_direct_error',
|
|
@@ -2573,7 +2667,9 @@ async function cmdStart(port) {
|
|
|
2573
2667
|
return;
|
|
2574
2668
|
}
|
|
2575
2669
|
}
|
|
2576
|
-
|
|
2670
|
+
// Treat late/duplicate/expired callbacks as idempotent skips rather than hard errors.
|
|
2671
|
+
// The callback source has already completed, timed out, or been recovered elsewhere.
|
|
2672
|
+
res.json({ status: 'ok', skipped: true, reason: 'unknown_or_expired_dedupeKey' });
|
|
2577
2673
|
return;
|
|
2578
2674
|
}
|
|
2579
2675
|
log({
|
|
@@ -2587,6 +2683,67 @@ async function cmdStart(port) {
|
|
|
2587
2683
|
});
|
|
2588
2684
|
|
|
2589
2685
|
if (body.status === 'failed') {
|
|
2686
|
+
// Some subagents pessimistically send `failed` after already producing a usable
|
|
2687
|
+
// summary/result because they observed a callback transport error on their side.
|
|
2688
|
+
// If the payload is still actionable, recover it here instead of dropping the flow.
|
|
2689
|
+
const failedAction = buildAgentCallbackAction(pending.eventType, pending.payload || {}, body);
|
|
2690
|
+
if (failedAction.ok && !failedAction.skipped) {
|
|
2691
|
+
log({
|
|
2692
|
+
event: 'agent_callback_failed_recovered',
|
|
2693
|
+
eventType: pending.eventType,
|
|
2694
|
+
dedupeKey,
|
|
2695
|
+
childSessionKey: pending.childSessionKey,
|
|
2696
|
+
summary: body.summary,
|
|
2697
|
+
error: body.error,
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
if (failedAction.action?.type === 'local_result') {
|
|
2701
|
+
try {
|
|
2702
|
+
const localResp = await fetch(`http://127.0.0.1:${p}/atel/v1/result`, {
|
|
2703
|
+
method: 'POST',
|
|
2704
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2705
|
+
body: JSON.stringify({
|
|
2706
|
+
taskId: failedAction.action.taskId,
|
|
2707
|
+
result: failedAction.action.result,
|
|
2708
|
+
success: true,
|
|
2709
|
+
}),
|
|
2710
|
+
signal: AbortSignal.timeout(15000),
|
|
2711
|
+
});
|
|
2712
|
+
const localBody = await localResp.json().catch(() => ({}));
|
|
2713
|
+
pendingAgentCallbacks.delete(dedupeKey);
|
|
2714
|
+
if (localResp.ok) {
|
|
2715
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: failedAction.action, localBody, recoveredFromFailed: true });
|
|
2716
|
+
pending.resolve({ ok: true, recovered: true, body, action: failedAction.action, localBody });
|
|
2717
|
+
res.json({ status: 'ok', recovered: true });
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2721
|
+
pending.resolve({ ok: false, body, action: failedAction.action, localBody, error: localBody.error || 'local_result_callback_failed' });
|
|
2722
|
+
res.status(500).json({ error: localBody.error || 'local_result_callback_failed' });
|
|
2723
|
+
return;
|
|
2724
|
+
} catch (e) {
|
|
2725
|
+
pendingAgentCallbacks.delete(dedupeKey);
|
|
2726
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2727
|
+
pending.resolve({ ok: false, body, action: failedAction.action, error: e.message });
|
|
2728
|
+
res.status(500).json({ error: e.message || 'local_result_callback_failed' });
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const execResult = await executeRecommendedActionDirect(pending.eventType, failedAction.action, pending.cwd || process.cwd(), dedupeKey);
|
|
2734
|
+
pendingAgentCallbacks.delete(dedupeKey);
|
|
2735
|
+
if (execResult.ok) {
|
|
2736
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: failedAction.action, recoveredFromFailed: true });
|
|
2737
|
+
pending.resolve({ ok: true, recovered: true, body, action: failedAction.action, execResult });
|
|
2738
|
+
res.json({ status: 'ok', recovered: true });
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2742
|
+
pending.resolve({ ok: false, body, action: failedAction.action, execResult, error: execResult.error || 'callback_action_failed' });
|
|
2743
|
+
res.status(500).json({ error: execResult.error || 'callback_action_failed' });
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2590
2747
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2591
2748
|
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2592
2749
|
pending.resolve({ ok: false, body, error: body.error || 'agent_reported_failed' });
|
|
@@ -2660,7 +2817,7 @@ async function cmdStart(port) {
|
|
|
2660
2817
|
res.json({ status: 'ok' });
|
|
2661
2818
|
});
|
|
2662
2819
|
|
|
2663
|
-
function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd) {
|
|
2820
|
+
function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd, payload = {}) {
|
|
2664
2821
|
const callbackExamples = {
|
|
2665
2822
|
milestone_submitted: [
|
|
2666
2823
|
'通过时执行:',
|
|
@@ -2700,18 +2857,30 @@ async function cmdStart(port) {
|
|
|
2700
2857
|
].join('\n');
|
|
2701
2858
|
|
|
2702
2859
|
const callbackDone = eventType === 'milestone_submitted' ? callbackExamples.milestone_submitted : callbackExamples.default;
|
|
2860
|
+
const contextFile = join(cwd, 'ORDER_CONTEXT.md');
|
|
2861
|
+
const allowRepoAccess = shouldAllowRepoAccess(payload);
|
|
2862
|
+
const fileAccessRule = allowRepoAccess
|
|
2863
|
+
? `3. 仅允许使用目录 ${cwd} 下与当前订单直接相关的内容;如果订单明确要求读仓库,也只能读取该订单工作区中明确提供的路径。`
|
|
2864
|
+
: `3. 本单禁止读取任何本地文件、共享草稿、仓库或其他项目。不要使用文件搜索、目录浏览、读文件等方式扩展上下文;只允许依据本条消息中的订单描述、里程碑目标、提交内容来工作。`;
|
|
2865
|
+
const contextRule = allowRepoAccess
|
|
2866
|
+
? `4. 优先读取 ${contextFile},严格以其中的订单描述、里程碑目标、提交内容为准。`
|
|
2867
|
+
: `4. 不要扫描本机其他目录,不要读取 /root/atel-workspace 下的共享文件,不要根据历史项目或 stray files 推断任务。`;
|
|
2868
|
+
const repoRule = allowRepoAccess
|
|
2869
|
+
? `5. 只有当订单明确要求分析仓库/代码时,才允许读取订单工作区里显式提供的代码路径;禁止顺带读取其他目录。`
|
|
2870
|
+
: `5. 本单不是 repo/code 任务。禁止把任务扩展成代码分析、工程改造或共享草稿筛选。`;
|
|
2703
2871
|
if (eventType === 'p2p_task') {
|
|
2704
2872
|
return `${promptText}
|
|
2705
2873
|
|
|
2706
2874
|
重要要求:
|
|
2707
2875
|
1. 这是一个 P2P 任务。不要调用 atel result;本地 SDK 会在你回调后自动提交结果。
|
|
2708
2876
|
2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
|
|
2709
|
-
|
|
2710
|
-
|
|
2877
|
+
${fileAccessRule}
|
|
2878
|
+
${contextRule}
|
|
2879
|
+
5. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
|
|
2711
2880
|
|
|
2712
2881
|
${callbackDone}
|
|
2713
2882
|
|
|
2714
|
-
|
|
2883
|
+
6. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
|
|
2715
2884
|
|
|
2716
2885
|
${callbackFailed}
|
|
2717
2886
|
`;
|
|
@@ -2722,12 +2891,14 @@ ${callbackFailed}
|
|
|
2722
2891
|
重要要求:
|
|
2723
2892
|
1. 不要执行 atel milestone-submit / milestone-verify / milestone-feedback 命令;这些命令会由本地 SDK 在你回调后代为执行。
|
|
2724
2893
|
2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
|
|
2725
|
-
|
|
2726
|
-
|
|
2894
|
+
${fileAccessRule}
|
|
2895
|
+
${contextRule}
|
|
2896
|
+
${repoRule}
|
|
2897
|
+
6. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
|
|
2727
2898
|
|
|
2728
2899
|
${callbackDone}
|
|
2729
2900
|
|
|
2730
|
-
|
|
2901
|
+
7. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
|
|
2731
2902
|
|
|
2732
2903
|
${callbackFailed}
|
|
2733
2904
|
`;
|
|
@@ -2743,7 +2914,9 @@ ${callbackFailed}
|
|
|
2743
2914
|
}
|
|
2744
2915
|
|
|
2745
2916
|
const callbackUrl = `http://127.0.0.1:${p}/atel/v1/agent-callback`;
|
|
2746
|
-
const
|
|
2917
|
+
const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
|
|
2918
|
+
if (!safePrompt) return { ok: false, error: 'empty_agent_prompt' };
|
|
2919
|
+
const taskPrompt = buildGatewayCallbackPrompt(eventType, safePrompt, callbackUrl, dedupeKey, cwd, payload);
|
|
2747
2920
|
const timeoutMs = 10 * 60 * 1000;
|
|
2748
2921
|
|
|
2749
2922
|
return await new Promise(async (resolve) => {
|
|
@@ -2809,9 +2982,11 @@ ${callbackFailed}
|
|
|
2809
2982
|
}
|
|
2810
2983
|
|
|
2811
2984
|
function queueAgentHook(eventType, dedupeKey, promptText, cwd, payload = {}, options = {}) {
|
|
2812
|
-
if (!detectedAgentCmd
|
|
2985
|
+
if (!detectedAgentCmd) return false;
|
|
2986
|
+
const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
|
|
2987
|
+
if (!safePrompt) return false;
|
|
2813
2988
|
const parsedCmd = detectedAgentCmd.trim().split(/\s+/);
|
|
2814
|
-
parsedCmd.push(
|
|
2989
|
+
parsedCmd.push(safePrompt);
|
|
2815
2990
|
const recoveryKey = options.recoveryKey || '';
|
|
2816
2991
|
if (recoveryKey) {
|
|
2817
2992
|
if (activeRecoveryKeys.has(recoveryKey)) return false;
|
|
@@ -2841,6 +3016,8 @@ ${callbackFailed}
|
|
|
2841
3016
|
const requesterDid = order?.requesterDid || order?.RequesterDID || '';
|
|
2842
3017
|
const executorDid = order?.executorDid || order?.ExecutorDID || '';
|
|
2843
3018
|
const orderStatus = order?.status || order?.Status || '';
|
|
3019
|
+
const orderDescription = order?.description || order?.Description || order?.taskRequest?.description || order?.TaskRequest?.description || '';
|
|
3020
|
+
const chain = order?.chain || order?.Chain || '';
|
|
2844
3021
|
|
|
2845
3022
|
if (['cancelled', 'settled', 'rejected', 'expired'].includes(orderStatus)) {
|
|
2846
3023
|
untrackOrder(orderId);
|
|
@@ -2849,7 +3026,7 @@ ${callbackFailed}
|
|
|
2849
3026
|
|
|
2850
3027
|
if (orderStatus === 'milestone_review') {
|
|
2851
3028
|
const approveAction = { type: 'cli', action: 'approve_plan', command: ['atel', 'milestone-feedback', orderId, '--approve'] };
|
|
2852
|
-
const result = await executeRecommendedActionDirect('order_accepted', approveAction,
|
|
3029
|
+
const result = await executeRecommendedActionDirect('order_accepted', approveAction, getAtelWorkspaceRoot(), `reconcile:${orderId}:approve_plan`);
|
|
2853
3030
|
log({ event: 'trade_reconcile_plan', orderId, ok: result.ok, role: requesterDid === id.did ? 'requester' : 'executor' });
|
|
2854
3031
|
return;
|
|
2855
3032
|
}
|
|
@@ -2862,6 +3039,18 @@ ${callbackFailed}
|
|
|
2862
3039
|
if (executorDid === id.did && ms.phase === 'waiting_executor_submission') {
|
|
2863
3040
|
const currentIndex = Number.isFinite(ms.currentMilestone) ? ms.currentMilestone : 0;
|
|
2864
3041
|
const currentMilestone = (ms.milestones || []).find(m => m.index === currentIndex) || {};
|
|
3042
|
+
const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], currentIndex);
|
|
3043
|
+
const workspace = getOrderWorkspace(orderId, {
|
|
3044
|
+
chain,
|
|
3045
|
+
role: 'executor',
|
|
3046
|
+
status: orderStatus,
|
|
3047
|
+
phase: ms.phase,
|
|
3048
|
+
currentMilestone: currentIndex,
|
|
3049
|
+
milestoneTitle: currentMilestone.title || '',
|
|
3050
|
+
orderDescription,
|
|
3051
|
+
milestoneObjective: currentMilestone.title || '',
|
|
3052
|
+
previousApprovedOutputs,
|
|
3053
|
+
});
|
|
2865
3054
|
const eventType = currentIndex === 0 ? 'milestone_plan_confirmed' : 'milestone_verified';
|
|
2866
3055
|
const payload = currentIndex === 0
|
|
2867
3056
|
? {
|
|
@@ -2869,7 +3058,8 @@ ${callbackFailed}
|
|
|
2869
3058
|
milestoneIndex: 0,
|
|
2870
3059
|
totalMilestones: ms.totalMilestones || 5,
|
|
2871
3060
|
milestoneDescription: currentMilestone.title || '',
|
|
2872
|
-
orderDescription
|
|
3061
|
+
orderDescription,
|
|
3062
|
+
previousApprovedOutputs,
|
|
2873
3063
|
}
|
|
2874
3064
|
: {
|
|
2875
3065
|
orderId,
|
|
@@ -2878,13 +3068,14 @@ ${callbackFailed}
|
|
|
2878
3068
|
totalMilestones: ms.totalMilestones || 5,
|
|
2879
3069
|
allComplete: false,
|
|
2880
3070
|
nextMilestoneDescription: currentMilestone.title || '',
|
|
2881
|
-
orderDescription
|
|
3071
|
+
orderDescription,
|
|
3072
|
+
previousApprovedOutputs,
|
|
2882
3073
|
};
|
|
2883
3074
|
const promptText = currentIndex === 0
|
|
2884
|
-
? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n当前里程碑 M0:${currentMilestone.title || ''}\n
|
|
2885
|
-
: `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n
|
|
3075
|
+
? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n订单原始要求:${orderDescription || '未提供'}\n当前里程碑 M0:${currentMilestone.title || ''}\n请只围绕这个订单要求完成当前里程碑,并通过回调返回最终交付内容。`
|
|
3076
|
+
: `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n订单原始要求:${orderDescription || '未提供'}\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n\n请严格基于这些已通过结果推进当前里程碑,不要自行假设缺失材料,也不要读取本地共享文件来补上下文。完成后通过回调返回最终交付内容。`;
|
|
2886
3077
|
const recoveryKey = `reconcile:${orderId}:executor:${currentIndex}`;
|
|
2887
|
-
const queued = queueAgentHook(eventType, recoveryKey, promptText,
|
|
3078
|
+
const queued = queueAgentHook(eventType, recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
2888
3079
|
if (queued) log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
2889
3080
|
return;
|
|
2890
3081
|
}
|
|
@@ -2892,16 +3083,31 @@ ${callbackFailed}
|
|
|
2892
3083
|
if (requesterDid === id.did && ms.phase === 'waiting_requester_verification') {
|
|
2893
3084
|
const submittedMilestone = (ms.milestones || []).find(m => m.status === 'submitted');
|
|
2894
3085
|
if (!submittedMilestone) return;
|
|
3086
|
+
const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], submittedMilestone.index);
|
|
3087
|
+
const workspace = getOrderWorkspace(orderId, {
|
|
3088
|
+
chain,
|
|
3089
|
+
role: 'requester',
|
|
3090
|
+
status: orderStatus,
|
|
3091
|
+
phase: ms.phase,
|
|
3092
|
+
currentMilestone: submittedMilestone.index,
|
|
3093
|
+
milestoneTitle: submittedMilestone.title || '',
|
|
3094
|
+
orderDescription,
|
|
3095
|
+
milestoneObjective: submittedMilestone.title || '',
|
|
3096
|
+
resultSummary: submittedMilestone.resultSummary || '',
|
|
3097
|
+
previousApprovedOutputs,
|
|
3098
|
+
});
|
|
2895
3099
|
const payload = {
|
|
2896
3100
|
orderId,
|
|
2897
3101
|
milestoneIndex: submittedMilestone.index,
|
|
2898
3102
|
milestoneDescription: submittedMilestone.title || '',
|
|
2899
3103
|
resultSummary: submittedMilestone.resultSummary || '',
|
|
2900
3104
|
submitCount: submittedMilestone.submitCount || 0,
|
|
3105
|
+
orderDescription,
|
|
3106
|
+
previousApprovedOutputs,
|
|
2901
3107
|
};
|
|
2902
|
-
const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n里程碑目标:${submittedMilestone.title || ''}\n提交内容:${submittedMilestone.resultSummary || ''}\n
|
|
3108
|
+
const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n订单原始要求:${orderDescription || '未提供'}\n里程碑目标:${submittedMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n提交内容:${submittedMilestone.resultSummary || ''}\n请只按该订单要求和前序已通过结果审慎决定通过还是拒绝,并通过回调返回 decision=pass 或 decision=reject。`;
|
|
2903
3109
|
const recoveryKey = `reconcile:${orderId}:requester:${submittedMilestone.index}:${submittedMilestone.submitCount || 0}`;
|
|
2904
|
-
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText,
|
|
3110
|
+
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
2905
3111
|
if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
|
|
2906
3112
|
}
|
|
2907
3113
|
}
|
|
@@ -3004,7 +3210,20 @@ ${callbackFailed}
|
|
|
3004
3210
|
});
|
|
3005
3211
|
|
|
3006
3212
|
const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
|
|
3007
|
-
const
|
|
3213
|
+
const orderIdForCwd = body.orderId || payload.orderId || '';
|
|
3214
|
+
const workspace = getOrderWorkspace(orderIdForCwd, {
|
|
3215
|
+
chain: payload.chain || body.chain || '',
|
|
3216
|
+
role: payload.executorDid === id.did ? 'executor' : (payload.requesterDid === id.did ? 'requester' : ''),
|
|
3217
|
+
status: payload.orderStatus || body.orderStatus || '',
|
|
3218
|
+
phase: payload.phase || body.phase || '',
|
|
3219
|
+
currentMilestone: payload.currentMilestone ?? payload.milestoneIndex ?? '',
|
|
3220
|
+
milestoneTitle: payload.milestoneDescription || payload.nextMilestoneDescription || '',
|
|
3221
|
+
orderDescription: payload.orderDescription || payload.description || '',
|
|
3222
|
+
milestoneObjective: payload.milestoneDescription || payload.nextMilestoneDescription || '',
|
|
3223
|
+
resultSummary: payload.resultSummary || '',
|
|
3224
|
+
});
|
|
3225
|
+
const hookCwd = workspace.dir;
|
|
3226
|
+
const atelCwd = getAtelWorkspaceRoot();
|
|
3008
3227
|
|
|
3009
3228
|
// 3. Policy mode: auto-execute deterministic operations (not thinking/work)
|
|
3010
3229
|
const currentPolicy = loadPolicy();
|
|
@@ -3069,7 +3288,7 @@ ${callbackFailed}
|
|
|
3069
3288
|
let directExecutionSucceeded = false;
|
|
3070
3289
|
const directActions = getDirectExecutableActions(event, recommendedActions);
|
|
3071
3290
|
for (const action of directActions) {
|
|
3072
|
-
const result = await executeRecommendedActionDirect(event, action,
|
|
3291
|
+
const result = await executeRecommendedActionDirect(event, action, atelCwd, dedupeKey);
|
|
3073
3292
|
if (result.ok) directExecutionSucceeded = true;
|
|
3074
3293
|
}
|
|
3075
3294
|
|
|
@@ -3086,8 +3305,12 @@ ${callbackFailed}
|
|
|
3086
3305
|
return;
|
|
3087
3306
|
}
|
|
3088
3307
|
// Add working directory context so agent runs atel commands in the right place
|
|
3089
|
-
const cwdNote = `\n\n
|
|
3090
|
-
const enrichedPrompt = prompt + cwdNote;
|
|
3308
|
+
const cwdNote = `\n\n重要:OpenClaw 的分析工作目录是 ${hookCwd}。所有 atel 命令必须在目录 ${atelCwd} 下执行(cd ${atelCwd} && atel ...)。`;
|
|
3309
|
+
const enrichedPrompt = sanitizeAgentPrompt(prompt + cwdNote, { eventType: event, dedupeKey });
|
|
3310
|
+
if (!enrichedPrompt) {
|
|
3311
|
+
res.json({ status: 'received', eventId, eventType: event, skipped: true });
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3091
3314
|
|
|
3092
3315
|
// Skip if already triggered for this dedupeKey
|
|
3093
3316
|
if (processedEvents.has('hook:' + dedupeKey)) {
|
|
@@ -3100,7 +3323,7 @@ ${callbackFailed}
|
|
|
3100
3323
|
parsedCmd.push(enrichedPrompt);
|
|
3101
3324
|
|
|
3102
3325
|
// Queue the hook (serialize to avoid session lock conflicts)
|
|
3103
|
-
hookQueue.push({ event, dedupeKey, cmd: parsedCmd[0], args: parsedCmd.slice(1), cwd, payload, recoveryKey: '' });
|
|
3326
|
+
hookQueue.push({ event, dedupeKey, cmd: parsedCmd[0], args: parsedCmd.slice(1), cwd: hookCwd, payload, recoveryKey: '' });
|
|
3104
3327
|
if (!hookBusy) processHookQueue();
|
|
3105
3328
|
}
|
|
3106
3329
|
}
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ ATEL 负责:
|
|
|
18
18
|
- relay / endpoint / inbox
|
|
19
19
|
- paid order 状态机
|
|
20
20
|
- 通知与回调
|
|
21
|
+
- paid order 的链字段与链上记录
|
|
21
22
|
|
|
22
23
|
OpenClaw 负责:
|
|
23
24
|
|
|
@@ -123,6 +124,32 @@ echo "========================================="
|
|
|
123
124
|
- `atel start` 会启动 ATEL 本地 endpoint、relay 轮询、通知、回调处理
|
|
124
125
|
- 具体“怎么思考、怎么写内容、怎么调用工具”由 OpenClaw 完成
|
|
125
126
|
- 不要把 ATEL 理解成内置了一个通用 LLM 执行器
|
|
127
|
+
- paid order 目前按正式支持链处理:
|
|
128
|
+
- `Base`
|
|
129
|
+
- `BSC`
|
|
130
|
+
- **paid order 的唯一链真相源是 `order.chain`**
|
|
131
|
+
- 订单在哪条链,你就按哪条链理解:
|
|
132
|
+
- smart wallet
|
|
133
|
+
- USDC 余额
|
|
134
|
+
- gas
|
|
135
|
+
- escrow
|
|
136
|
+
- chain-records
|
|
137
|
+
|
|
138
|
+
### 双链使用规则
|
|
139
|
+
|
|
140
|
+
处理 paid order 时,必须遵守:
|
|
141
|
+
|
|
142
|
+
1. 不要默认所有订单都在 Base
|
|
143
|
+
2. 先用 `atel order-info <orderId>` 或 `atel milestone-status <orderId>` 看 `chain`
|
|
144
|
+
3. 后续所有跟链有关的判断都跟 `order.chain`
|
|
145
|
+
4. 如果订单是 `bsc`,就不要再按 `base` 钱包、`base` gas、`base` 浏览器去理解
|
|
146
|
+
|
|
147
|
+
常见含义:
|
|
148
|
+
|
|
149
|
+
- `base`:
|
|
150
|
+
- Base 主网 USDC / gas / chain record
|
|
151
|
+
- `bsc`:
|
|
152
|
+
- BSC 主网 USDC / gas / chain record
|
|
126
153
|
|
|
127
154
|
---
|
|
128
155
|
|
|
@@ -137,6 +164,16 @@ atel order <executor-DID> general <金额> --desc "任务描述"
|
|
|
137
164
|
|
|
138
165
|
发单前确保钱包有 USDC(`atel balance` 查看)。
|
|
139
166
|
|
|
167
|
+
⚠️ 双链说明:
|
|
168
|
+
|
|
169
|
+
- paid order 不是默认只有 Base
|
|
170
|
+
- 发单时要明确知道自己准备用哪条链完成交易
|
|
171
|
+
- 后续 accept / escrow / submit / verify / settle 都会跟该订单的 `chain`
|
|
172
|
+
- 如果要做双链 paid order,发单方和接单方都需要在对应链准备:
|
|
173
|
+
- smart wallet
|
|
174
|
+
- USDC
|
|
175
|
+
- gas
|
|
176
|
+
|
|
140
177
|
### 接单(Executor)
|
|
141
178
|
|
|
142
179
|
收到新订单通知时,人类确认后:
|
|
@@ -154,6 +191,14 @@ atel orders executor # 只看接的单
|
|
|
154
191
|
atel order-info <orderId> # 查看某个订单详情
|
|
155
192
|
```
|
|
156
193
|
|
|
194
|
+
看订单时一定注意:
|
|
195
|
+
|
|
196
|
+
- `order.chain`
|
|
197
|
+
- `escrow.chain`
|
|
198
|
+
- `chain-records`
|
|
199
|
+
|
|
200
|
+
这几个字段会直接决定后续链上动作在哪条链执行。
|
|
201
|
+
|
|
157
202
|
### 一键下单(搜索 + 下单 + 等待)
|
|
158
203
|
|
|
159
204
|
```bash
|
|
@@ -178,16 +223,27 @@ atel trade-task <capability> "任务描述" --budget 5
|
|
|
178
223
|
|
|
179
224
|
```bash
|
|
180
225
|
cd ~/atel-workspace
|
|
226
|
+
atel order-info <orderId>
|
|
181
227
|
atel milestone-status <orderId>
|
|
182
228
|
atel milestone-feedback <orderId> --approve
|
|
183
229
|
```
|
|
184
230
|
|
|
231
|
+
注意:
|
|
232
|
+
|
|
233
|
+
- 先确认 `order.chain`
|
|
234
|
+
- 不要把 paid order 默认当成 Base 单
|
|
235
|
+
|
|
185
236
|
**`milestone_plan_confirmed` — 方案确认了(你是接单方):**
|
|
186
237
|
- prompt 里有里程碑描述,用你的 AI 能力完成工作
|
|
187
238
|
```bash
|
|
188
239
|
cd ~/atel-workspace && atel milestone-submit <orderId> <index> --result '<你的交付内容>'
|
|
189
240
|
```
|
|
190
241
|
|
|
242
|
+
提交前要明确:
|
|
243
|
+
|
|
244
|
+
- 当前订单在哪条链
|
|
245
|
+
- 后续 anchor / settle / chain-records 都会落在这条链
|
|
246
|
+
|
|
191
247
|
**`milestone_submitted` — 对方提交了(你是发单方):**
|
|
192
248
|
- prompt 里有里程碑目标和提交内容,认真审核
|
|
193
249
|
- 质量达标就通过,不达标就写清楚具体原因
|
|
@@ -221,6 +277,12 @@ cd ~/atel-workspace && atel balance
|
|
|
221
277
|
cd ~/atel-workspace && atel chain-records <orderId>
|
|
222
278
|
```
|
|
223
279
|
|
|
280
|
+
结算后检查时,不要只看 Base:
|
|
281
|
+
|
|
282
|
+
- `atel balance` 会显示链上钱包情况
|
|
283
|
+
- `atel chain-records <orderId>` 要确认该订单对应链上的记录
|
|
284
|
+
- 如果这是 `bsc` 单,就按 `bsc` 的链上记录理解结果
|
|
285
|
+
|
|
224
286
|
---
|
|
225
287
|
|
|
226
288
|
## 三、P2P 与消息
|
|
@@ -244,6 +306,16 @@ ATEL 有两种轻量协作方式,不要混淆:
|
|
|
244
306
|
如果用户想“直接让对方做一个轻任务”,用 `atel task`。
|
|
245
307
|
如果用户想“带付款、验收、结算”,用 `atel order`。
|
|
246
308
|
|
|
309
|
+
补充:
|
|
310
|
+
|
|
311
|
+
- `atel task` 和 `atel send` 不走 paid order 双链结算流
|
|
312
|
+
- `atel order` 才会进入:
|
|
313
|
+
- escrow
|
|
314
|
+
- milestone
|
|
315
|
+
- chain-records
|
|
316
|
+
- dispute
|
|
317
|
+
- 所以只有 `atel order` 需要严格理解 `Base / BSC`
|
|
318
|
+
|
|
247
319
|
---
|
|
248
320
|
|
|
249
321
|
## 四、社交通信
|
|
@@ -324,14 +396,22 @@ atel offer-close <offerId>
|
|
|
324
396
|
## 五、账户管理
|
|
325
397
|
|
|
326
398
|
```bash
|
|
327
|
-
atel balance
|
|
328
|
-
atel deposit 10 crypto_base
|
|
329
|
-
atel
|
|
330
|
-
atel
|
|
399
|
+
atel balance # 查余额(会看到 Base / BSC)
|
|
400
|
+
atel deposit 10 crypto_base # 充值 10 USDC(Base)
|
|
401
|
+
atel deposit 10 crypto_bsc # 充值 10 USDC(BSC)
|
|
402
|
+
atel withdraw 5 crypto_base <钱包地址> # 从 Base 提现
|
|
403
|
+
atel withdraw 5 crypto_bsc <钱包地址> # 从 BSC 提现
|
|
404
|
+
atel transactions # 交易记录
|
|
331
405
|
```
|
|
332
406
|
|
|
333
407
|
支持的充值渠道:`crypto_solana`、`crypto_base`、`crypto_bsc`、`stripe`、`alipay`、`manual`
|
|
334
408
|
|
|
409
|
+
注意:
|
|
410
|
+
|
|
411
|
+
- 双链 paid order 场景下,余额检查不能只看 Base
|
|
412
|
+
- 你要确认订单实际在哪条链,再决定看哪条链的钱包与 USDC
|
|
413
|
+
- 如果订单是 `bsc`,就不要只用 `crypto_base` 的心智理解充值、提现和结算
|
|
414
|
+
|
|
335
415
|
---
|
|
336
416
|
|
|
337
417
|
## 六、信任与安全
|