@lawrenceliang-btc/atel-sdk 1.1.8 → 1.1.10
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 +568 -41
- package/bin/notification-action-helpers.mjs +4 -7
- 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,111 @@ 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
|
+
|
|
195
|
+
function isKnownUpstreamModelInputError(text) {
|
|
196
|
+
const value = String(text || '');
|
|
197
|
+
return value.includes('InternalError.Algo.InvalidParameter')
|
|
198
|
+
|| value.includes('Range of input length should be [1, 258048]');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function summarizeAgentOutput(text, maxChars = 300) {
|
|
202
|
+
const raw = String(text || '');
|
|
203
|
+
const trimmed = raw.trim();
|
|
204
|
+
if (!trimmed) return '';
|
|
205
|
+
if (isKnownUpstreamModelInputError(trimmed)) return '[suppressed upstream model input-length error]';
|
|
206
|
+
return trimmed.substring(0, maxChars);
|
|
207
|
+
}
|
|
208
|
+
|
|
103
209
|
// ═══════════════════════════════════════════════════════════════════
|
|
104
210
|
// Notification Target System — auto-discover gateway, manage targets
|
|
105
211
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -348,6 +454,18 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
348
454
|
if (command.length === 0) {
|
|
349
455
|
return { ok: false, skipped: true, reason: 'empty_command' };
|
|
350
456
|
}
|
|
457
|
+
const actionKey = JSON.stringify(command);
|
|
458
|
+
if (globalThis.__atelActiveDirectActionKeys?.has(actionKey)) {
|
|
459
|
+
log({
|
|
460
|
+
event: 'recommended_action_direct_skip',
|
|
461
|
+
eventType,
|
|
462
|
+
dedupeKey,
|
|
463
|
+
action: action.action,
|
|
464
|
+
command,
|
|
465
|
+
reason: 'inflight_duplicate',
|
|
466
|
+
});
|
|
467
|
+
return { ok: true, skipped: true, reason: 'inflight_duplicate' };
|
|
468
|
+
}
|
|
351
469
|
|
|
352
470
|
// Idempotency guard: short-circuit duplicate milestone plan/submit/verify actions
|
|
353
471
|
// if the order or milestone has already advanced past the required state.
|
|
@@ -384,8 +502,10 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
384
502
|
}
|
|
385
503
|
const milestone = needsMilestoneIndex && Array.isArray(state?.milestones) ? state.milestones.find((m) => m.index === index) : null;
|
|
386
504
|
if (needsMilestoneIndex && milestone) {
|
|
387
|
-
const
|
|
388
|
-
|
|
505
|
+
const expectedStatuses = command[1] === 'milestone-verify'
|
|
506
|
+
? ['submitted']
|
|
507
|
+
: (eventType === 'milestone_rejected' ? ['pending', 'rejected'] : ['pending']);
|
|
508
|
+
if (!expectedStatuses.includes(milestone.status)) {
|
|
389
509
|
log({
|
|
390
510
|
event: 'recommended_action_direct_skip',
|
|
391
511
|
eventType,
|
|
@@ -414,12 +534,68 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
414
534
|
const { execFile } = await import('child_process');
|
|
415
535
|
const childCmd = command[0] === 'atel' ? process.execPath : command[0];
|
|
416
536
|
const childArgs = command[0] === 'atel' ? [process.argv[1], ...command.slice(1)] : command.slice(1);
|
|
537
|
+
const childCwd = command[0] === 'atel' ? getAtelWorkspaceRoot() : cwd;
|
|
417
538
|
|
|
418
539
|
log({ event: 'recommended_action_direct_trigger', eventType, dedupeKey, action: action.action, command });
|
|
540
|
+
globalThis.__atelActiveDirectActionKeys ??= new Set();
|
|
541
|
+
globalThis.__atelActiveDirectActionKeys.add(actionKey);
|
|
419
542
|
|
|
420
543
|
return await new Promise((resolve) => {
|
|
421
|
-
execFile(childCmd, childArgs, { timeout: 180000, cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
544
|
+
execFile(childCmd, childArgs, { timeout: 180000, cwd: childCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
545
|
+
globalThis.__atelActiveDirectActionKeys?.delete(actionKey);
|
|
422
546
|
if (err) {
|
|
547
|
+
const combinedErrorText = String(stderr || err.message || '');
|
|
548
|
+
if (
|
|
549
|
+
command[0] === 'atel' &&
|
|
550
|
+
command[1] === 'milestone-feedback' &&
|
|
551
|
+
command.includes('--approve') &&
|
|
552
|
+
combinedErrorText.includes('order not in milestone_review status')
|
|
553
|
+
) {
|
|
554
|
+
log({
|
|
555
|
+
event: 'recommended_action_direct_skip',
|
|
556
|
+
eventType,
|
|
557
|
+
dedupeKey,
|
|
558
|
+
action: action.action,
|
|
559
|
+
command,
|
|
560
|
+
reason: 'order_status_executing',
|
|
561
|
+
});
|
|
562
|
+
resolve({ ok: true, skipped: true, reason: 'order_status_executing' });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (
|
|
566
|
+
command[0] === 'atel' &&
|
|
567
|
+
command[1] === 'milestone-submit' &&
|
|
568
|
+
combinedErrorText.includes('milestone cannot be submitted in status: submitted')
|
|
569
|
+
) {
|
|
570
|
+
log({
|
|
571
|
+
event: 'recommended_action_direct_skip',
|
|
572
|
+
eventType,
|
|
573
|
+
dedupeKey,
|
|
574
|
+
action: action.action,
|
|
575
|
+
command,
|
|
576
|
+
reason: 'milestone_status_submitted',
|
|
577
|
+
});
|
|
578
|
+
resolve({ ok: true, skipped: true, reason: 'milestone_status_submitted' });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
command[0] === 'atel' &&
|
|
583
|
+
command[1] === 'milestone-verify' &&
|
|
584
|
+
(combinedErrorText.includes('milestone cannot be verified in status: verified') ||
|
|
585
|
+
combinedErrorText.includes('milestone not in submitted status') ||
|
|
586
|
+
combinedErrorText.includes('milestone cannot be verified in status: settled'))
|
|
587
|
+
) {
|
|
588
|
+
log({
|
|
589
|
+
event: 'recommended_action_direct_skip',
|
|
590
|
+
eventType,
|
|
591
|
+
dedupeKey,
|
|
592
|
+
action: action.action,
|
|
593
|
+
command,
|
|
594
|
+
reason: 'milestone_already_processed',
|
|
595
|
+
});
|
|
596
|
+
resolve({ ok: true, skipped: true, reason: 'milestone_already_processed' });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
423
599
|
log({
|
|
424
600
|
event: 'recommended_action_direct_error',
|
|
425
601
|
eventType,
|
|
@@ -439,7 +615,7 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
439
615
|
dedupeKey,
|
|
440
616
|
action: action.action,
|
|
441
617
|
command,
|
|
442
|
-
stdout: (stdout
|
|
618
|
+
stdout: summarizeAgentOutput(stdout, 400),
|
|
443
619
|
});
|
|
444
620
|
resolve({ ok: true, stdout });
|
|
445
621
|
});
|
|
@@ -2574,7 +2750,9 @@ async function cmdStart(port) {
|
|
|
2574
2750
|
return;
|
|
2575
2751
|
}
|
|
2576
2752
|
}
|
|
2577
|
-
|
|
2753
|
+
// Treat late/duplicate/expired callbacks as idempotent skips rather than hard errors.
|
|
2754
|
+
// The callback source has already completed, timed out, or been recovered elsewhere.
|
|
2755
|
+
res.json({ status: 'ok', skipped: true, reason: 'unknown_or_expired_dedupeKey' });
|
|
2578
2756
|
return;
|
|
2579
2757
|
}
|
|
2580
2758
|
log({
|
|
@@ -2588,6 +2766,67 @@ async function cmdStart(port) {
|
|
|
2588
2766
|
});
|
|
2589
2767
|
|
|
2590
2768
|
if (body.status === 'failed') {
|
|
2769
|
+
// Some subagents pessimistically send `failed` after already producing a usable
|
|
2770
|
+
// summary/result because they observed a callback transport error on their side.
|
|
2771
|
+
// If the payload is still actionable, recover it here instead of dropping the flow.
|
|
2772
|
+
const failedAction = buildAgentCallbackAction(pending.eventType, pending.payload || {}, body);
|
|
2773
|
+
if (failedAction.ok && !failedAction.skipped) {
|
|
2774
|
+
log({
|
|
2775
|
+
event: 'agent_callback_failed_recovered',
|
|
2776
|
+
eventType: pending.eventType,
|
|
2777
|
+
dedupeKey,
|
|
2778
|
+
childSessionKey: pending.childSessionKey,
|
|
2779
|
+
summary: body.summary,
|
|
2780
|
+
error: body.error,
|
|
2781
|
+
});
|
|
2782
|
+
|
|
2783
|
+
if (failedAction.action?.type === 'local_result') {
|
|
2784
|
+
try {
|
|
2785
|
+
const localResp = await fetch(`http://127.0.0.1:${p}/atel/v1/result`, {
|
|
2786
|
+
method: 'POST',
|
|
2787
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2788
|
+
body: JSON.stringify({
|
|
2789
|
+
taskId: failedAction.action.taskId,
|
|
2790
|
+
result: failedAction.action.result,
|
|
2791
|
+
success: true,
|
|
2792
|
+
}),
|
|
2793
|
+
signal: AbortSignal.timeout(15000),
|
|
2794
|
+
});
|
|
2795
|
+
const localBody = await localResp.json().catch(() => ({}));
|
|
2796
|
+
pendingAgentCallbacks.delete(dedupeKey);
|
|
2797
|
+
if (localResp.ok) {
|
|
2798
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: failedAction.action, localBody, recoveredFromFailed: true });
|
|
2799
|
+
pending.resolve({ ok: true, recovered: true, body, action: failedAction.action, localBody });
|
|
2800
|
+
res.json({ status: 'ok', recovered: true });
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2804
|
+
pending.resolve({ ok: false, body, action: failedAction.action, localBody, error: localBody.error || 'local_result_callback_failed' });
|
|
2805
|
+
res.status(500).json({ error: localBody.error || 'local_result_callback_failed' });
|
|
2806
|
+
return;
|
|
2807
|
+
} catch (e) {
|
|
2808
|
+
pendingAgentCallbacks.delete(dedupeKey);
|
|
2809
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2810
|
+
pending.resolve({ ok: false, body, action: failedAction.action, error: e.message });
|
|
2811
|
+
res.status(500).json({ error: e.message || 'local_result_callback_failed' });
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
const execResult = await executeRecommendedActionDirect(pending.eventType, failedAction.action, pending.cwd || process.cwd(), dedupeKey);
|
|
2817
|
+
pendingAgentCallbacks.delete(dedupeKey);
|
|
2818
|
+
if (execResult.ok) {
|
|
2819
|
+
markPersistedPendingAgentCallbackCompleted(dedupeKey, { eventType: pending.eventType, payload: pending.payload, cwd: pending.cwd, source: dedupeKey?.startsWith('reconcile:') ? 'reconcile' : 'main', action: failedAction.action, recoveredFromFailed: true });
|
|
2820
|
+
pending.resolve({ ok: true, recovered: true, body, action: failedAction.action, execResult });
|
|
2821
|
+
res.json({ status: 'ok', recovered: true });
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2825
|
+
pending.resolve({ ok: false, body, action: failedAction.action, execResult, error: execResult.error || 'callback_action_failed' });
|
|
2826
|
+
res.status(500).json({ error: execResult.error || 'callback_action_failed' });
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2591
2830
|
pendingAgentCallbacks.delete(dedupeKey);
|
|
2592
2831
|
clearPersistedPendingAgentCallback(dedupeKey);
|
|
2593
2832
|
pending.resolve({ ok: false, body, error: body.error || 'agent_reported_failed' });
|
|
@@ -2661,7 +2900,7 @@ async function cmdStart(port) {
|
|
|
2661
2900
|
res.json({ status: 'ok' });
|
|
2662
2901
|
});
|
|
2663
2902
|
|
|
2664
|
-
function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd) {
|
|
2903
|
+
function buildGatewayCallbackPrompt(eventType, promptText, callbackUrl, dedupeKey, cwd, payload = {}) {
|
|
2665
2904
|
const callbackExamples = {
|
|
2666
2905
|
milestone_submitted: [
|
|
2667
2906
|
'通过时执行:',
|
|
@@ -2701,18 +2940,30 @@ async function cmdStart(port) {
|
|
|
2701
2940
|
].join('\n');
|
|
2702
2941
|
|
|
2703
2942
|
const callbackDone = eventType === 'milestone_submitted' ? callbackExamples.milestone_submitted : callbackExamples.default;
|
|
2943
|
+
const contextFile = join(cwd, 'ORDER_CONTEXT.md');
|
|
2944
|
+
const allowRepoAccess = shouldAllowRepoAccess(payload);
|
|
2945
|
+
const fileAccessRule = allowRepoAccess
|
|
2946
|
+
? `3. 仅允许使用目录 ${cwd} 下与当前订单直接相关的内容;如果订单明确要求读仓库,也只能读取该订单工作区中明确提供的路径。`
|
|
2947
|
+
: `3. 本单禁止读取任何本地文件、共享草稿、仓库或其他项目。不要使用文件搜索、目录浏览、读文件等方式扩展上下文;只允许依据本条消息中的订单描述、里程碑目标、提交内容来工作。`;
|
|
2948
|
+
const contextRule = allowRepoAccess
|
|
2949
|
+
? `4. 优先读取 ${contextFile},严格以其中的订单描述、里程碑目标、提交内容为准。`
|
|
2950
|
+
: `4. 不要扫描本机其他目录,不要读取 /root/atel-workspace 下的共享文件,不要根据历史项目或 stray files 推断任务。`;
|
|
2951
|
+
const repoRule = allowRepoAccess
|
|
2952
|
+
? `5. 只有当订单明确要求分析仓库/代码时,才允许读取订单工作区里显式提供的代码路径;禁止顺带读取其他目录。`
|
|
2953
|
+
: `5. 本单不是 repo/code 任务。禁止把任务扩展成代码分析、工程改造或共享草稿筛选。`;
|
|
2704
2954
|
if (eventType === 'p2p_task') {
|
|
2705
2955
|
return `${promptText}
|
|
2706
2956
|
|
|
2707
2957
|
重要要求:
|
|
2708
2958
|
1. 这是一个 P2P 任务。不要调用 atel result;本地 SDK 会在你回调后自动提交结果。
|
|
2709
2959
|
2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
|
|
2710
|
-
|
|
2711
|
-
|
|
2960
|
+
${fileAccessRule}
|
|
2961
|
+
${contextRule}
|
|
2962
|
+
5. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
|
|
2712
2963
|
|
|
2713
2964
|
${callbackDone}
|
|
2714
2965
|
|
|
2715
|
-
|
|
2966
|
+
6. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
|
|
2716
2967
|
|
|
2717
2968
|
${callbackFailed}
|
|
2718
2969
|
`;
|
|
@@ -2723,17 +2974,191 @@ ${callbackFailed}
|
|
|
2723
2974
|
重要要求:
|
|
2724
2975
|
1. 不要执行 atel milestone-submit / milestone-verify / milestone-feedback 命令;这些命令会由本地 SDK 在你回调后代为执行。
|
|
2725
2976
|
2. 你的任务是认真完成 AI 工作,并把最终结论通过回调发回本地 SDK。
|
|
2726
|
-
|
|
2727
|
-
|
|
2977
|
+
${fileAccessRule}
|
|
2978
|
+
${contextRule}
|
|
2979
|
+
${repoRule}
|
|
2980
|
+
6. 完成后,必须立刻执行下面这个成功回调命令模板,并把其中内容替换成你的真实结果:
|
|
2728
2981
|
|
|
2729
2982
|
${callbackDone}
|
|
2730
2983
|
|
|
2731
|
-
|
|
2984
|
+
7. 如果重试后仍然失败,也必须执行下面这个失败回调命令:
|
|
2732
2985
|
|
|
2733
2986
|
${callbackFailed}
|
|
2734
2987
|
`;
|
|
2735
2988
|
}
|
|
2736
2989
|
|
|
2990
|
+
function buildLocalAgentPrompt(eventType, promptText) {
|
|
2991
|
+
if (eventType === 'milestone_submitted') {
|
|
2992
|
+
return `${promptText}
|
|
2993
|
+
|
|
2994
|
+
重要:你不是在和用户聊天。
|
|
2995
|
+
不要输出 markdown、代码块、解释、分析过程。
|
|
2996
|
+
你必须只输出一行 JSON。
|
|
2997
|
+
|
|
2998
|
+
通过时:
|
|
2999
|
+
{"decision":"pass","summary":"简短通过原因"}
|
|
3000
|
+
|
|
3001
|
+
拒绝时:
|
|
3002
|
+
{"decision":"reject","reason":"具体拒绝原因","summary":"简短审核结论"}`;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
|
|
3006
|
+
return `${promptText}
|
|
3007
|
+
|
|
3008
|
+
重要:你不是在和用户聊天。
|
|
3009
|
+
不要输出 markdown、标题、项目符号、解释、分析过程。
|
|
3010
|
+
你必须只输出一行 JSON。
|
|
3011
|
+
|
|
3012
|
+
格式:
|
|
3013
|
+
{"result":"当前里程碑的真实交付内容"}`;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
return promptText;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function normalizeLocalAgentStdout(stdout) {
|
|
3020
|
+
const text = String(stdout || '').trim();
|
|
3021
|
+
if (!text) return '';
|
|
3022
|
+
try {
|
|
3023
|
+
const parsed = JSON.parse(text);
|
|
3024
|
+
if (Array.isArray(parsed?.payloads)) {
|
|
3025
|
+
for (const item of parsed.payloads) {
|
|
3026
|
+
const candidate = String(item?.text || '').trim();
|
|
3027
|
+
if (candidate) return candidate;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (typeof parsed?.text === 'string' && parsed.text.trim()) return parsed.text.trim();
|
|
3031
|
+
if (typeof parsed?.result === 'string' && parsed.result.trim()) return JSON.stringify({ result: parsed.result.trim() });
|
|
3032
|
+
if (typeof parsed?.decision === 'string') return JSON.stringify(parsed);
|
|
3033
|
+
} catch {}
|
|
3034
|
+
const fencedJson = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
3035
|
+
if (fencedJson?.[1]) return fencedJson[1].trim();
|
|
3036
|
+
const jsonObjects = text.match(/\{[\s\S]*\}/g);
|
|
3037
|
+
if (jsonObjects?.length) {
|
|
3038
|
+
for (let i = jsonObjects.length - 1; i >= 0; i -= 1) {
|
|
3039
|
+
const candidate = jsonObjects[i].trim();
|
|
3040
|
+
try {
|
|
3041
|
+
const parsed = JSON.parse(candidate);
|
|
3042
|
+
if (Array.isArray(parsed?.payloads)) {
|
|
3043
|
+
for (const item of parsed.payloads) {
|
|
3044
|
+
const nested = String(item?.text || '').trim();
|
|
3045
|
+
if (nested) return nested;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (typeof parsed?.text === 'string' && parsed.text.trim()) return parsed.text.trim();
|
|
3049
|
+
if (typeof parsed?.result === 'string' && parsed.result.trim()) return JSON.stringify({ result: parsed.result.trim() });
|
|
3050
|
+
if (typeof parsed?.decision === 'string') return JSON.stringify(parsed);
|
|
3051
|
+
return candidate;
|
|
3052
|
+
} catch {}
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
const jsonLines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
3056
|
+
for (let i = jsonLines.length - 1; i >= 0; i -= 1) {
|
|
3057
|
+
const candidate = jsonLines[i];
|
|
3058
|
+
if (!(candidate.startsWith('{') && candidate.endsWith('}'))) continue;
|
|
3059
|
+
try {
|
|
3060
|
+
JSON.parse(candidate);
|
|
3061
|
+
return candidate;
|
|
3062
|
+
} catch {}
|
|
3063
|
+
}
|
|
3064
|
+
return text;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
function normalizeResult(value) {
|
|
3068
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function sanitizeHookSessionId(value) {
|
|
3072
|
+
const raw = String(value || '').trim();
|
|
3073
|
+
if (!raw) return `atel-hook-${Date.now()}`;
|
|
3074
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
3075
|
+
return (cleaned || `atel-hook-${Date.now()}`).slice(0, 120);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function isOpenClawAgentInvocation(cmd, args = []) {
|
|
3079
|
+
const argv = [String(cmd || ''), ...args.map((v) => String(v || ''))];
|
|
3080
|
+
if (argv[0] === 'openclaw') return argv[1] === 'agent';
|
|
3081
|
+
if (argv[0] === 'npx') return argv[1] === 'openclaw' && argv[2] === 'agent';
|
|
3082
|
+
if (argv[0] === 'node') return argv.includes('agent') && argv.some((v) => /openclaw/i.test(v));
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function prepareHookInvocation(cmd, args = [], hookKey, timeoutSeconds) {
|
|
3087
|
+
const nextArgs = [...args];
|
|
3088
|
+
if (!isOpenClawAgentInvocation(cmd, nextArgs)) return { cmd, args: nextArgs };
|
|
3089
|
+
|
|
3090
|
+
if (!nextArgs.includes('--json')) {
|
|
3091
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3092
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3093
|
+
nextArgs.splice(insertAt, 0, '--json');
|
|
3094
|
+
}
|
|
3095
|
+
if (!nextArgs.includes('--session-id')) {
|
|
3096
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3097
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3098
|
+
nextArgs.splice(insertAt, 0, '--session-id', sanitizeHookSessionId(hookKey));
|
|
3099
|
+
}
|
|
3100
|
+
if (!nextArgs.includes('--timeout')) {
|
|
3101
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3102
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3103
|
+
nextArgs.splice(insertAt, 0, '--timeout', String(timeoutSeconds));
|
|
3104
|
+
}
|
|
3105
|
+
if (!nextArgs.includes('--thinking')) {
|
|
3106
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3107
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3108
|
+
nextArgs.splice(insertAt, 0, '--thinking', 'minimal');
|
|
3109
|
+
}
|
|
3110
|
+
return { cmd, args: nextArgs };
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
function buildLocalAgentActionFromStdout(eventType, payload, stdout) {
|
|
3114
|
+
const cleaned = normalizeLocalAgentStdout(stdout);
|
|
3115
|
+
if (!cleaned) return { ok: false, error: 'empty_local_agent_stdout' };
|
|
3116
|
+
|
|
3117
|
+
if (eventType === 'milestone_submitted') {
|
|
3118
|
+
try {
|
|
3119
|
+
const parsed = JSON.parse(cleaned);
|
|
3120
|
+
return buildAgentCallbackAction(eventType, payload, parsed);
|
|
3121
|
+
} catch {
|
|
3122
|
+
const lowered = cleaned.toLowerCase();
|
|
3123
|
+
if (lowered.startsWith('pass') || cleaned.includes('通过')) {
|
|
3124
|
+
return buildAgentCallbackAction(eventType, payload, { decision: 'pass', summary: cleaned });
|
|
3125
|
+
}
|
|
3126
|
+
if (lowered.startsWith('reject') || cleaned.includes('拒绝')) {
|
|
3127
|
+
return buildAgentCallbackAction(eventType, payload, { decision: 'reject', reason: cleaned, summary: cleaned });
|
|
3128
|
+
}
|
|
3129
|
+
return { ok: false, error: 'invalid_local_review_stdout' };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
|
|
3134
|
+
try {
|
|
3135
|
+
const parsed = JSON.parse(cleaned);
|
|
3136
|
+
return buildAgentCallbackAction(eventType, payload, parsed);
|
|
3137
|
+
} catch {
|
|
3138
|
+
return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function buildMilestoneHookRecoveryKey(eventType, payload = {}) {
|
|
3146
|
+
const orderId = String(payload?.orderId || '').trim();
|
|
3147
|
+
if (!orderId) return '';
|
|
3148
|
+
if (eventType === 'milestone_submitted') {
|
|
3149
|
+
const stage = Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0;
|
|
3150
|
+
const submitCount = Number.isFinite(Number(payload?.submitCount)) ? Number(payload.submitCount) : 0;
|
|
3151
|
+
return `stage:${orderId}:requester:${stage}:${submitCount}`;
|
|
3152
|
+
}
|
|
3153
|
+
if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
|
|
3154
|
+
const stage = eventType === 'milestone_plan_confirmed'
|
|
3155
|
+
? (Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0)
|
|
3156
|
+
: (Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0);
|
|
3157
|
+
return `stage:${orderId}:executor:${stage}`;
|
|
3158
|
+
}
|
|
3159
|
+
return '';
|
|
3160
|
+
}
|
|
3161
|
+
|
|
2737
3162
|
async function runGatewayAgentTask(eventType, dedupeKey, promptText, cwd, payload) {
|
|
2738
3163
|
const gw = discoverGateway();
|
|
2739
3164
|
const cfg = loadOpenClawConfig();
|
|
@@ -2744,7 +3169,9 @@ ${callbackFailed}
|
|
|
2744
3169
|
}
|
|
2745
3170
|
|
|
2746
3171
|
const callbackUrl = `http://127.0.0.1:${p}/atel/v1/agent-callback`;
|
|
2747
|
-
const
|
|
3172
|
+
const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
|
|
3173
|
+
if (!safePrompt) return { ok: false, error: 'empty_agent_prompt' };
|
|
3174
|
+
const taskPrompt = buildGatewayCallbackPrompt(eventType, safePrompt, callbackUrl, dedupeKey, cwd, payload);
|
|
2748
3175
|
const timeoutMs = 10 * 60 * 1000;
|
|
2749
3176
|
|
|
2750
3177
|
return await new Promise(async (resolve) => {
|
|
@@ -2810,9 +3237,11 @@ ${callbackFailed}
|
|
|
2810
3237
|
}
|
|
2811
3238
|
|
|
2812
3239
|
function queueAgentHook(eventType, dedupeKey, promptText, cwd, payload = {}, options = {}) {
|
|
2813
|
-
if (!detectedAgentCmd
|
|
3240
|
+
if (!detectedAgentCmd) return false;
|
|
3241
|
+
const safePrompt = sanitizeAgentPrompt(promptText, { eventType, dedupeKey });
|
|
3242
|
+
if (!safePrompt) return false;
|
|
2814
3243
|
const parsedCmd = detectedAgentCmd.trim().split(/\s+/);
|
|
2815
|
-
parsedCmd.push(
|
|
3244
|
+
parsedCmd.push(safePrompt);
|
|
2816
3245
|
const recoveryKey = options.recoveryKey || '';
|
|
2817
3246
|
if (recoveryKey) {
|
|
2818
3247
|
if (activeRecoveryKeys.has(recoveryKey)) return false;
|
|
@@ -2842,6 +3271,8 @@ ${callbackFailed}
|
|
|
2842
3271
|
const requesterDid = order?.requesterDid || order?.RequesterDID || '';
|
|
2843
3272
|
const executorDid = order?.executorDid || order?.ExecutorDID || '';
|
|
2844
3273
|
const orderStatus = order?.status || order?.Status || '';
|
|
3274
|
+
const orderDescription = order?.description || order?.Description || order?.taskRequest?.description || order?.TaskRequest?.description || '';
|
|
3275
|
+
const chain = order?.chain || order?.Chain || '';
|
|
2845
3276
|
|
|
2846
3277
|
if (['cancelled', 'settled', 'rejected', 'expired'].includes(orderStatus)) {
|
|
2847
3278
|
untrackOrder(orderId);
|
|
@@ -2850,7 +3281,7 @@ ${callbackFailed}
|
|
|
2850
3281
|
|
|
2851
3282
|
if (orderStatus === 'milestone_review') {
|
|
2852
3283
|
const approveAction = { type: 'cli', action: 'approve_plan', command: ['atel', 'milestone-feedback', orderId, '--approve'] };
|
|
2853
|
-
const result = await executeRecommendedActionDirect('order_accepted', approveAction,
|
|
3284
|
+
const result = await executeRecommendedActionDirect('order_accepted', approveAction, getAtelWorkspaceRoot(), `reconcile:${orderId}:approve_plan`);
|
|
2854
3285
|
log({ event: 'trade_reconcile_plan', orderId, ok: result.ok, role: requesterDid === id.did ? 'requester' : 'executor' });
|
|
2855
3286
|
return;
|
|
2856
3287
|
}
|
|
@@ -2863,6 +3294,18 @@ ${callbackFailed}
|
|
|
2863
3294
|
if (executorDid === id.did && ms.phase === 'waiting_executor_submission') {
|
|
2864
3295
|
const currentIndex = Number.isFinite(ms.currentMilestone) ? ms.currentMilestone : 0;
|
|
2865
3296
|
const currentMilestone = (ms.milestones || []).find(m => m.index === currentIndex) || {};
|
|
3297
|
+
const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], currentIndex);
|
|
3298
|
+
const workspace = getOrderWorkspace(orderId, {
|
|
3299
|
+
chain,
|
|
3300
|
+
role: 'executor',
|
|
3301
|
+
status: orderStatus,
|
|
3302
|
+
phase: ms.phase,
|
|
3303
|
+
currentMilestone: currentIndex,
|
|
3304
|
+
milestoneTitle: currentMilestone.title || '',
|
|
3305
|
+
orderDescription,
|
|
3306
|
+
milestoneObjective: currentMilestone.title || '',
|
|
3307
|
+
previousApprovedOutputs,
|
|
3308
|
+
});
|
|
2866
3309
|
const eventType = currentIndex === 0 ? 'milestone_plan_confirmed' : 'milestone_verified';
|
|
2867
3310
|
const payload = currentIndex === 0
|
|
2868
3311
|
? {
|
|
@@ -2870,7 +3313,8 @@ ${callbackFailed}
|
|
|
2870
3313
|
milestoneIndex: 0,
|
|
2871
3314
|
totalMilestones: ms.totalMilestones || 5,
|
|
2872
3315
|
milestoneDescription: currentMilestone.title || '',
|
|
2873
|
-
orderDescription
|
|
3316
|
+
orderDescription,
|
|
3317
|
+
previousApprovedOutputs,
|
|
2874
3318
|
}
|
|
2875
3319
|
: {
|
|
2876
3320
|
orderId,
|
|
@@ -2879,30 +3323,47 @@ ${callbackFailed}
|
|
|
2879
3323
|
totalMilestones: ms.totalMilestones || 5,
|
|
2880
3324
|
allComplete: false,
|
|
2881
3325
|
nextMilestoneDescription: currentMilestone.title || '',
|
|
2882
|
-
orderDescription
|
|
3326
|
+
orderDescription,
|
|
3327
|
+
previousApprovedOutputs,
|
|
2883
3328
|
};
|
|
2884
3329
|
const promptText = currentIndex === 0
|
|
2885
|
-
? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n当前里程碑 M0:${currentMilestone.title || ''}\n
|
|
2886
|
-
: `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n
|
|
2887
|
-
const recoveryKey =
|
|
2888
|
-
|
|
2889
|
-
|
|
3330
|
+
? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n订单原始要求:${orderDescription || '未提供'}\n当前里程碑 M0:${currentMilestone.title || ''}\n请只围绕这个订单要求完成当前里程碑,并通过回调返回最终交付内容。`
|
|
3331
|
+
: `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n订单原始要求:${orderDescription || '未提供'}\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n\n请严格基于这些已通过结果推进当前里程碑,不要自行假设缺失材料,也不要读取本地共享文件来补上下文。完成后通过回调返回最终交付内容。`;
|
|
3332
|
+
const recoveryKey = buildMilestoneHookRecoveryKey(eventType, payload);
|
|
3333
|
+
log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
3334
|
+
const queued = queueAgentHook(eventType, recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
3335
|
+
if (queued) log({ event: 'trade_reconcile_executor_queued', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
2890
3336
|
return;
|
|
2891
3337
|
}
|
|
2892
3338
|
|
|
2893
3339
|
if (requesterDid === id.did && ms.phase === 'waiting_requester_verification') {
|
|
2894
3340
|
const submittedMilestone = (ms.milestones || []).find(m => m.status === 'submitted');
|
|
2895
3341
|
if (!submittedMilestone) return;
|
|
3342
|
+
const previousApprovedOutputs = summarizeApprovedMilestones(ms.milestones || [], submittedMilestone.index);
|
|
3343
|
+
const workspace = getOrderWorkspace(orderId, {
|
|
3344
|
+
chain,
|
|
3345
|
+
role: 'requester',
|
|
3346
|
+
status: orderStatus,
|
|
3347
|
+
phase: ms.phase,
|
|
3348
|
+
currentMilestone: submittedMilestone.index,
|
|
3349
|
+
milestoneTitle: submittedMilestone.title || '',
|
|
3350
|
+
orderDescription,
|
|
3351
|
+
milestoneObjective: submittedMilestone.title || '',
|
|
3352
|
+
resultSummary: submittedMilestone.resultSummary || '',
|
|
3353
|
+
previousApprovedOutputs,
|
|
3354
|
+
});
|
|
2896
3355
|
const payload = {
|
|
2897
3356
|
orderId,
|
|
2898
3357
|
milestoneIndex: submittedMilestone.index,
|
|
2899
3358
|
milestoneDescription: submittedMilestone.title || '',
|
|
2900
3359
|
resultSummary: submittedMilestone.resultSummary || '',
|
|
2901
3360
|
submitCount: submittedMilestone.submitCount || 0,
|
|
3361
|
+
orderDescription,
|
|
3362
|
+
previousApprovedOutputs,
|
|
2902
3363
|
};
|
|
2903
|
-
const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n里程碑目标:${submittedMilestone.title || ''}\n提交内容:${submittedMilestone.resultSummary || ''}\n
|
|
2904
|
-
const recoveryKey =
|
|
2905
|
-
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText,
|
|
3364
|
+
const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n订单原始要求:${orderDescription || '未提供'}\n里程碑目标:${submittedMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n提交内容:${submittedMilestone.resultSummary || ''}\n请只按该订单要求和前序已通过结果审慎决定通过还是拒绝,并通过回调返回 decision=pass 或 decision=reject。`;
|
|
3365
|
+
const recoveryKey = buildMilestoneHookRecoveryKey('milestone_submitted', payload);
|
|
3366
|
+
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
2906
3367
|
if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
|
|
2907
3368
|
}
|
|
2908
3369
|
}
|
|
@@ -3005,7 +3466,20 @@ ${callbackFailed}
|
|
|
3005
3466
|
});
|
|
3006
3467
|
|
|
3007
3468
|
const dedupeKey = body.dedupeKey || `${event}:${body.orderId || payload.orderId || ''}`;
|
|
3008
|
-
const
|
|
3469
|
+
const orderIdForCwd = body.orderId || payload.orderId || '';
|
|
3470
|
+
const workspace = getOrderWorkspace(orderIdForCwd, {
|
|
3471
|
+
chain: payload.chain || body.chain || '',
|
|
3472
|
+
role: payload.executorDid === id.did ? 'executor' : (payload.requesterDid === id.did ? 'requester' : ''),
|
|
3473
|
+
status: payload.orderStatus || body.orderStatus || '',
|
|
3474
|
+
phase: payload.phase || body.phase || '',
|
|
3475
|
+
currentMilestone: payload.currentMilestone ?? payload.milestoneIndex ?? '',
|
|
3476
|
+
milestoneTitle: payload.milestoneDescription || payload.nextMilestoneDescription || '',
|
|
3477
|
+
orderDescription: payload.orderDescription || payload.description || '',
|
|
3478
|
+
milestoneObjective: payload.milestoneDescription || payload.nextMilestoneDescription || '',
|
|
3479
|
+
resultSummary: payload.resultSummary || '',
|
|
3480
|
+
});
|
|
3481
|
+
const hookCwd = workspace.dir;
|
|
3482
|
+
const atelCwd = getAtelWorkspaceRoot();
|
|
3009
3483
|
|
|
3010
3484
|
// 3. Policy mode: auto-execute deterministic operations (not thinking/work)
|
|
3011
3485
|
const currentPolicy = loadPolicy();
|
|
@@ -3070,7 +3544,7 @@ ${callbackFailed}
|
|
|
3070
3544
|
let directExecutionSucceeded = false;
|
|
3071
3545
|
const directActions = getDirectExecutableActions(event, recommendedActions);
|
|
3072
3546
|
for (const action of directActions) {
|
|
3073
|
-
const result = await executeRecommendedActionDirect(event, action,
|
|
3547
|
+
const result = await executeRecommendedActionDirect(event, action, atelCwd, dedupeKey);
|
|
3074
3548
|
if (result.ok) directExecutionSucceeded = true;
|
|
3075
3549
|
}
|
|
3076
3550
|
|
|
@@ -3081,14 +3555,24 @@ ${callbackFailed}
|
|
|
3081
3555
|
const autoTriggerEvents = ['order_accepted', 'milestone_plan_confirmed', 'milestone_submitted', 'milestone_verified', 'milestone_rejected'];
|
|
3082
3556
|
const hasGatewayAction = Array.isArray(recommendedActions) && recommendedActions.some((action) => Array.isArray(action?.command) && action.command[0] === 'atel');
|
|
3083
3557
|
if (agentCmd && prompt && autoTriggerEvents.includes(event) && !shouldSkipAgentHook(event, directExecutionSucceeded)) {
|
|
3558
|
+
const needsActionablePayload = event !== 'milestone_submitted';
|
|
3559
|
+
if (needsActionablePayload && !hasGatewayAction) {
|
|
3560
|
+
log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
|
|
3561
|
+
res.json({ status: 'received', eventId, eventType: event });
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3084
3564
|
if (shouldUseGatewaySession(event) && !hasGatewayAction) {
|
|
3085
3565
|
log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
|
|
3086
3566
|
res.json({ status: 'received', eventId, eventType: event });
|
|
3087
3567
|
return;
|
|
3088
3568
|
}
|
|
3089
3569
|
// Add working directory context so agent runs atel commands in the right place
|
|
3090
|
-
const cwdNote = `\n\n
|
|
3091
|
-
const enrichedPrompt = prompt + cwdNote;
|
|
3570
|
+
const cwdNote = `\n\n重要:OpenClaw 的分析工作目录是 ${hookCwd}。所有 atel 命令必须在目录 ${atelCwd} 下执行(cd ${atelCwd} && atel ...)。`;
|
|
3571
|
+
const enrichedPrompt = sanitizeAgentPrompt(prompt + cwdNote, { eventType: event, dedupeKey });
|
|
3572
|
+
if (!enrichedPrompt) {
|
|
3573
|
+
res.json({ status: 'received', eventId, eventType: event, skipped: true });
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3092
3576
|
|
|
3093
3577
|
// Skip if already triggered for this dedupeKey
|
|
3094
3578
|
if (processedEvents.has('hook:' + dedupeKey)) {
|
|
@@ -3096,13 +3580,17 @@ ${callbackFailed}
|
|
|
3096
3580
|
} else {
|
|
3097
3581
|
processedEvents.add('hook:' + dedupeKey);
|
|
3098
3582
|
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3583
|
+
const queued = queueAgentHook(
|
|
3584
|
+
event,
|
|
3585
|
+
dedupeKey,
|
|
3586
|
+
enrichedPrompt,
|
|
3587
|
+
hookCwd,
|
|
3588
|
+
payload,
|
|
3589
|
+
{ recoveryKey: buildMilestoneHookRecoveryKey(event, payload) },
|
|
3590
|
+
);
|
|
3591
|
+
if (!queued) {
|
|
3592
|
+
log({ event: 'agent_cmd_dedup_recovery_key', eventType: event, dedupeKey });
|
|
3593
|
+
}
|
|
3106
3594
|
}
|
|
3107
3595
|
}
|
|
3108
3596
|
|
|
@@ -3131,11 +3619,18 @@ ${callbackFailed}
|
|
|
3131
3619
|
return;
|
|
3132
3620
|
}
|
|
3133
3621
|
log({ event: 'agent_session_spawn_error', eventType: hookEvent, dedupeKey: hookKey, error: gatewayResult.error, fallback: 'cli' });
|
|
3622
|
+
spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
|
|
3623
|
+
} else if (spawnArgs.length > 0) {
|
|
3624
|
+
const promptArg = spawnArgs[spawnArgs.length - 1] || '';
|
|
3625
|
+
spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
|
|
3134
3626
|
}
|
|
3135
3627
|
|
|
3136
3628
|
const MAX_ATTEMPTS = 5;
|
|
3137
|
-
const
|
|
3138
|
-
|
|
3629
|
+
const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
|
|
3630
|
+
const localHookTimeoutMs = isMilestoneHook ? 180000 : 600000;
|
|
3631
|
+
const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
|
|
3632
|
+
const runHook = (attempt, invocation = preparedInvocation) => {
|
|
3633
|
+
execFile(invocation.cmd, invocation.args, { timeout: localHookTimeoutMs, cwd: hookCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3139
3634
|
const errMsg = (err?.message || '') + (stderr || '');
|
|
3140
3635
|
const isSessionLock = errMsg.includes('session file locked') || errMsg.includes('session locked');
|
|
3141
3636
|
const isNetworkError = err && (err.killed || err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET');
|
|
@@ -3151,8 +3646,40 @@ ${callbackFailed}
|
|
|
3151
3646
|
} else if (err) {
|
|
3152
3647
|
log({ event: 'agent_cmd_error', eventType: hookEvent, error: err.message, stderr: (stderr || '').substring(0, 200) });
|
|
3153
3648
|
finishHook();
|
|
3649
|
+
} else if (isKnownUpstreamModelInputError(stdout)) {
|
|
3650
|
+
log({
|
|
3651
|
+
event: 'agent_cmd_upstream_input_error',
|
|
3652
|
+
eventType: hookEvent,
|
|
3653
|
+
dedupeKey: hookKey,
|
|
3654
|
+
note: 'suppressed known upstream model input-length error',
|
|
3655
|
+
});
|
|
3656
|
+
finishHook();
|
|
3154
3657
|
} else {
|
|
3155
|
-
|
|
3658
|
+
const localAction = buildLocalAgentActionFromStdout(hookEvent, hookPayload || {}, stdout);
|
|
3659
|
+
if (!localAction.ok && localAction.error === 'empty_local_agent_stdout' && invocation.args.includes('--json')) {
|
|
3660
|
+
const retryArgs = invocation.args.filter((arg) => arg !== '--json');
|
|
3661
|
+
log({ event: 'agent_cmd_retry_without_json', eventType: hookEvent, dedupeKey: hookKey });
|
|
3662
|
+
setTimeout(() => runHook(attempt + 1, { ...invocation, args: retryArgs }), 1000);
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
if (localAction.ok && !localAction.skipped) {
|
|
3666
|
+
executeRecommendedActionDirect(hookEvent, localAction.action, hookCwd || process.cwd(), hookKey)
|
|
3667
|
+
.then((execResult) => {
|
|
3668
|
+
if (execResult.ok) {
|
|
3669
|
+
log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'local_stdout_action', dedupeKey: hookKey, stdout: summarizeAgentOutput(stdout, 200) });
|
|
3670
|
+
} else {
|
|
3671
|
+
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: execResult.error || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
|
|
3672
|
+
}
|
|
3673
|
+
finishHook();
|
|
3674
|
+
})
|
|
3675
|
+
.catch((e) => {
|
|
3676
|
+
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: e.message || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
|
|
3677
|
+
finishHook();
|
|
3678
|
+
});
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
log({ event: 'agent_cmd_done', eventType: hookEvent, stdout: summarizeAgentOutput(stdout, 300) });
|
|
3156
3683
|
finishHook();
|
|
3157
3684
|
}
|
|
3158
3685
|
});
|
|
@@ -18,13 +18,10 @@ export function shouldSkipAgentHook(eventType, directExecutionSucceeded) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function shouldUseGatewaySession(eventType) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'milestone_verified',
|
|
26
|
-
'milestone_rejected',
|
|
27
|
-
].includes(eventType);
|
|
21
|
+
// Keep gateway sub-sessions only for explicit P2P task execution.
|
|
22
|
+
// Milestone automation must not depend on gateway callback health; use the
|
|
23
|
+
// local structured fallback so order progression cannot stall on subagent I/O.
|
|
24
|
+
return eventType === 'p2p_task';
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export function normalizeGatewayBind(bind) {
|
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
|
## 六、信任与安全
|