@lawrenceliang-btc/atel-sdk 1.1.8 → 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 +248 -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
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -414,11 +506,12 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
414
506
|
const { execFile } = await import('child_process');
|
|
415
507
|
const childCmd = command[0] === 'atel' ? process.execPath : command[0];
|
|
416
508
|
const childArgs = command[0] === 'atel' ? [process.argv[1], ...command.slice(1)] : command.slice(1);
|
|
509
|
+
const childCwd = command[0] === 'atel' ? getAtelWorkspaceRoot() : cwd;
|
|
417
510
|
|
|
418
511
|
log({ event: 'recommended_action_direct_trigger', eventType, dedupeKey, action: action.action, command });
|
|
419
512
|
|
|
420
513
|
return await new Promise((resolve) => {
|
|
421
|
-
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) => {
|
|
422
515
|
if (err) {
|
|
423
516
|
log({
|
|
424
517
|
event: 'recommended_action_direct_error',
|
|
@@ -2574,7 +2667,9 @@ async function cmdStart(port) {
|
|
|
2574
2667
|
return;
|
|
2575
2668
|
}
|
|
2576
2669
|
}
|
|
2577
|
-
|
|
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' });
|
|
2578
2673
|
return;
|
|
2579
2674
|
}
|
|
2580
2675
|
log({
|
|
@@ -2588,6 +2683,67 @@ async function cmdStart(port) {
|
|
|
2588
2683
|
});
|
|
2589
2684
|
|
|
2590
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
|
+
|
|
2591
2747
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2592
2748
|
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2593
2749
|
pending.resolve({ ok: false, body, error: body.error || 'agent_reported_failed' });
|
|
@@ -2661,7 +2817,7 @@ async function cmdStart(port) {
|
|
|
2661
2817
|
res.json({ status: 'ok' });
|
|
2662
2818
|
});
|
|
2663
2819
|
|
|
2664
|
-
function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd) {
|
|
2820
|
+
function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd, payload = {}) {
|
|
2665
2821
|
const callbackExamples = {
|
|
2666
2822
|
milestone_submitted: [
|
|
2667
2823
|
'通过时执行:',
|
|
@@ -2701,18 +2857,30 @@ async function cmdStart(port) {
|
|
|
2701
2857
|
].join('\n');
|
|
2702
2858
|
|
|
2703
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 任务。禁止把任务扩展成代码分析、工程改造或共享草稿筛选。`;
|
|
2704
2871
|
if (eventType === 'p2p_task') {
|
|
2705
2872
|
return `${promptText}
|
|
2706
2873
|
|
|
2707
2874
|
重要要求:
|
|
2708
2875
|
1. 这是一个 P2P 任务。不要调用 atel result;本地 SDK 会在你回调后自动提交结果。
|
|
2709
2876
|
2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
|
|
2710
|
-
|
|
2711
|
-
|
|
2877
|
+
${fileAccessRule}
|
|
2878
|
+
${contextRule}
|
|
2879
|
+
5. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
|
|
2712
2880
|
|
|
2713
2881
|
${callbackDone}
|
|
2714
2882
|
|
|
2715
|
-
|
|
2883
|
+
6. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
|
|
2716
2884
|
|
|
2717
2885
|
${callbackFailed}
|
|
2718
2886
|
`;
|
|
@@ -2723,12 +2891,14 @@ ${callbackFailed}
|
|
|
2723
2891
|
重要要求:
|
|
2724
2892
|
1. 不要执行 atel milestone-submit / milestone-verify / milestone-feedback 命令;这些命令会由本地 SDK 在你回调后代为执行。
|
|
2725
2893
|
2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
|
|
2726
|
-
|
|
2727
|
-
|
|
2894
|
+
${fileAccessRule}
|
|
2895
|
+
${contextRule}
|
|
2896
|
+
${repoRule}
|
|
2897
|
+
6. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
|
|
2728
2898
|
|
|
2729
2899
|
${callbackDone}
|
|
2730
2900
|
|
|
2731
|
-
|
|
2901
|
+
7. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
|
|
2732
2902
|
|
|
2733
2903
|
${callbackFailed}
|
|
2734
2904
|
`;
|
|
@@ -2744,7 +2914,9 @@ ${callbackFailed}
|
|
|
2744
2914
|
}
|
|
2745
2915
|
|
|
2746
2916
|
const callbackUrl = `http://127.0.0.1:${p}/atel/v1/agent-callback`;
|
|
2747
|
-
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);
|
|
2748
2920
|
const timeoutMs = 10 * 60 * 1000;
|
|
2749
2921
|
|
|
2750
2922
|
return await new Promise(async (resolve) => {
|
|
@@ -2810,9 +2982,11 @@ ${callbackFailed}
|
|
|
2810
2982
|
}
|
|
2811
2983
|
|
|
2812
2984
|
function queueAgentHook(eventType, dedupeKey, promptText, cwd, payload = {}, options = {}) {
|
|
2813
|
-
if (!detectedAgentCmd
|
|
2985
|
+
if (!detectedAgentCmd) return false;
|
|
2986
|
+
const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
|
|
2987
|
+
if (!safePrompt) return false;
|
|
2814
2988
|
const parsedCmd = detectedAgentCmd.trim().split(/\s+/);
|
|
2815
|
-
parsedCmd.push(
|
|
2989
|
+
parsedCmd.push(safePrompt);
|
|
2816
2990
|
const recoveryKey = options.recoveryKey || '';
|
|
2817
2991
|
if (recoveryKey) {
|
|
2818
2992
|
if (activeRecoveryKeys.has(recoveryKey)) return false;
|
|
@@ -2842,6 +3016,8 @@ ${callbackFailed}
|
|
|
2842
3016
|
const requesterDid = order?.requesterDid || order?.RequesterDID || '';
|
|
2843
3017
|
const executorDid = order?.executorDid || order?.ExecutorDID || '';
|
|
2844
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 || '';
|
|
2845
3021
|
|
|
2846
3022
|
if (['cancelled', 'settled', 'rejected', 'expired'].includes(orderStatus)) {
|
|
2847
3023
|
untrackOrder(orderId);
|
|
@@ -2850,7 +3026,7 @@ ${callbackFailed}
|
|
|
2850
3026
|
|
|
2851
3027
|
if (orderStatus === 'milestone_review') {
|
|
2852
3028
|
const approveAction = { type: 'cli', action: 'approve_plan', command: ['atel', 'milestone-feedback', orderId, '--approve'] };
|
|
2853
|
-
const result = await executeRecommendedActionDirect('order_accepted', approveAction,
|
|
3029
|
+
const result = await executeRecommendedActionDirect('order_accepted', approveAction, getAtelWorkspaceRoot(), `reconcile:${orderId}:approve_plan`);
|
|
2854
3030
|
log({ event: 'trade_reconcile_plan', orderId, ok: result.ok, role: requesterDid === id.did ? 'requester' : 'executor' });
|
|
2855
3031
|
return;
|
|
2856
3032
|
}
|
|
@@ -2863,6 +3039,18 @@ ${callbackFailed}
|
|
|
2863
3039
|
if (executorDid === id.did && ms.phase === 'waiting_executor_submission') {
|
|
2864
3040
|
const currentIndex = Number.isFinite(ms.currentMilestone) ? ms.currentMilestone : 0;
|
|
2865
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
|
+
});
|
|
2866
3054
|
const eventType = currentIndex === 0 ? 'milestone_plan_confirmed' : 'milestone_verified';
|
|
2867
3055
|
const payload = currentIndex === 0
|
|
2868
3056
|
? {
|
|
@@ -2870,7 +3058,8 @@ ${callbackFailed}
|
|
|
2870
3058
|
milestoneIndex: 0,
|
|
2871
3059
|
totalMilestones: ms.totalMilestones || 5,
|
|
2872
3060
|
milestoneDescription: currentMilestone.title || '',
|
|
2873
|
-
orderDescription
|
|
3061
|
+
orderDescription,
|
|
3062
|
+
previousApprovedOutputs,
|
|
2874
3063
|
}
|
|
2875
3064
|
: {
|
|
2876
3065
|
orderId,
|
|
@@ -2879,13 +3068,14 @@ ${callbackFailed}
|
|
|
2879
3068
|
totalMilestones: ms.totalMilestones || 5,
|
|
2880
3069
|
allComplete: false,
|
|
2881
3070
|
nextMilestoneDescription: currentMilestone.title || '',
|
|
2882
|
-
orderDescription
|
|
3071
|
+
orderDescription,
|
|
3072
|
+
previousApprovedOutputs,
|
|
2883
3073
|
};
|
|
2884
3074
|
const promptText = currentIndex === 0
|
|
2885
|
-
? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n当前里程碑 M0:${currentMilestone.title || ''}\n
|
|
2886
|
-
: `你是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请严格基于这些已通过结果推进当前里程碑,不要自行假设缺失材料,也不要读取本地共享文件来补上下文。完成后通过回调返回最终交付内容。`;
|
|
2887
3077
|
const recoveryKey = `reconcile:${orderId}:executor:${currentIndex}`;
|
|
2888
|
-
const queued = queueAgentHook(eventType, recoveryKey, promptText,
|
|
3078
|
+
const queued = queueAgentHook(eventType, recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
2889
3079
|
if (queued) log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
2890
3080
|
return;
|
|
2891
3081
|
}
|
|
@@ -2893,16 +3083,31 @@ ${callbackFailed}
|
|
|
2893
3083
|
if (requesterDid === id.did && ms.phase === 'waiting_requester_verification') {
|
|
2894
3084
|
const submittedMilestone = (ms.milestones || []).find(m => m.status === 'submitted');
|
|
2895
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
|
+
});
|
|
2896
3099
|
const payload = {
|
|
2897
3100
|
orderId,
|
|
2898
3101
|
milestoneIndex: submittedMilestone.index,
|
|
2899
3102
|
milestoneDescription: submittedMilestone.title || '',
|
|
2900
3103
|
resultSummary: submittedMilestone.resultSummary || '',
|
|
2901
3104
|
submitCount: submittedMilestone.submitCount || 0,
|
|
3105
|
+
orderDescription,
|
|
3106
|
+
previousApprovedOutputs,
|
|
2902
3107
|
};
|
|
2903
|
-
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。`;
|
|
2904
3109
|
const recoveryKey = `reconcile:${orderId}:requester:${submittedMilestone.index}:${submittedMilestone.submitCount || 0}`;
|
|
2905
|
-
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText,
|
|
3110
|
+
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
2906
3111
|
if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
|
|
2907
3112
|
}
|
|
2908
3113
|
}
|
|
@@ -3005,7 +3210,20 @@ ${callbackFailed}
|
|
|
3005
3210
|
});
|
|
3006
3211
|
|
|
3007
3212
|
const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
|
|
3008
|
-
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();
|
|
3009
3227
|
|
|
3010
3228
|
// 3. Policy mode: auto-execute deterministic operations (not thinking/work)
|
|
3011
3229
|
const currentPolicy = loadPolicy();
|
|
@@ -3070,7 +3288,7 @@ ${callbackFailed}
|
|
|
3070
3288
|
let directExecutionSucceeded = false;
|
|
3071
3289
|
const directActions = getDirectExecutableActions(event, recommendedActions);
|
|
3072
3290
|
for (const action of directActions) {
|
|
3073
|
-
const result = await executeRecommendedActionDirect(event, action,
|
|
3291
|
+
const result = await executeRecommendedActionDirect(event, action, atelCwd, dedupeKey);
|
|
3074
3292
|
if (result.ok) directExecutionSucceeded = true;
|
|
3075
3293
|
}
|
|
3076
3294
|
|
|
@@ -3087,8 +3305,12 @@ ${callbackFailed}
|
|
|
3087
3305
|
return;
|
|
3088
3306
|
}
|
|
3089
3307
|
// Add working directory context so agent runs atel commands in the right place
|
|
3090
|
-
const cwdNote = `\n\n
|
|
3091
|
-
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
|
+
}
|
|
3092
3314
|
|
|
3093
3315
|
// Skip if already triggered for this dedupeKey
|
|
3094
3316
|
if (processedEvents.has('hook:' + dedupeKey)) {
|
|
@@ -3101,7 +3323,7 @@ ${callbackFailed}
|
|
|
3101
3323
|
parsedCmd.push(enrichedPrompt);
|
|
3102
3324
|
|
|
3103
3325
|
// Queue the hook (serialize to avoid session lock conflicts)
|
|
3104
|
-
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: '' });
|
|
3105
3327
|
if (!hookBusy) processHookQueue();
|
|
3106
3328
|
}
|
|
3107
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
|
## 六、信任与安全
|