@flame0510/project-aether 1.1.13 → 1.1.15
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/app/agents/PageClient.tsx +56 -12
- package/app/agents/create/PageClient.tsx +11 -4
- package/app/api/agents/[id]/recreate/route.ts +3 -3
- package/app/api/agents/[id]/route.ts +3 -1
- package/app/api/agents/channels-summary/route.ts +48 -17
- package/app/api/agents/create/route.ts +17 -10
- package/app/api/agents/image-status/route.ts +92 -85
- package/app/api/agents/route.ts +25 -29
- package/app/api/gateway/agent/route.ts +8 -6
- package/app/api/gateway/route.ts +102 -136
- package/app/api/gateway/sync.ts +12 -15
- package/app/api/provider/upstream.ts +12 -3
- package/app/api/skills/route.js +88 -96
- package/app/components/Skeleton.tsx +71 -0
- package/app/crons/PageClient.tsx +32 -7
- package/docs/ARCHITECTURE.md +0 -1
- package/docs/dev/API-REFERENCE.md +4 -4
- package/lib/channelManager.ts +23 -16
- package/lib/docker-exec.ts +195 -0
- package/lib/docker-utils.ts +22 -48
- package/lib/openclaw-cron.ts +81 -48
- package/package.json +1 -1
package/lib/openclaw-cron.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
1
|
+
import { execFileSync, execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { dockerExecNoFail, mapWithConcurrency } from '@/lib/docker-exec';
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* How many agents to query at once.
|
|
9
|
+
*
|
|
10
|
+
* Each one costs a `docker exec` plus an OpenClaw CLI boot — ~2.5-3s measured,
|
|
11
|
+
* and the route used to pay them in sequence: 19.2s on a seven-agent host, with
|
|
12
|
+
* the event loop blocked throughout. Running them concurrently makes the total
|
|
13
|
+
* the slowest agent rather than the sum. The cap is here because every one of
|
|
14
|
+
* these is a process, and the Docker daemon serialises past a point regardless.
|
|
15
|
+
*/
|
|
16
|
+
const AGENT_QUERY_CONCURRENCY = 6;
|
|
2
17
|
|
|
3
18
|
// ── Types ─────────────────────────────────────────────────────────────────
|
|
4
19
|
|
|
@@ -75,24 +90,26 @@ function extractScheduleExpr(job: OpenClawCronJob): string {
|
|
|
75
90
|
|
|
76
91
|
// ── Discover agent containers ────────────────────────────────────────────
|
|
77
92
|
|
|
78
|
-
function discoverAgentContainers(): AgentInfo[] {
|
|
93
|
+
async function discoverAgentContainers(): Promise<AgentInfo[]> {
|
|
79
94
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
agentId =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// One `docker ps` for everything. This used to run an extra `docker inspect`
|
|
96
|
+
// per container just to read the AGENT_ID label — but `docker ps` prints
|
|
97
|
+
// labels itself, so that was N+1 process spawns (~260ms each) for data the
|
|
98
|
+
// first call already had.
|
|
99
|
+
const raw = await execFileAsync('docker', [
|
|
100
|
+
'ps', '--filter', 'label=AGENT_ID', '--format', '{{.Label "AGENT_ID"}}\t{{.Names}}',
|
|
101
|
+
], { encoding: 'utf8', timeout: 5000 });
|
|
102
|
+
|
|
103
|
+
return raw.stdout.trim().split('\n')
|
|
104
|
+
.map((line) => {
|
|
105
|
+
const [agentId, containerName] = line.split('\t');
|
|
106
|
+
if (!containerName?.trim()) return null;
|
|
107
|
+
return {
|
|
108
|
+
containerName: containerName.trim(),
|
|
109
|
+
agentId: agentId?.trim() || containerName.trim(),
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
.filter(Boolean) as AgentInfo[];
|
|
96
113
|
} catch {
|
|
97
114
|
return [];
|
|
98
115
|
}
|
|
@@ -113,34 +130,38 @@ function parseCronListOutput(raw: string): OpenClawCronJob[] | null {
|
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
function listJobsOnHost(): OpenClawCronJob[] | null {
|
|
133
|
+
async function listJobsOnHost(): Promise<OpenClawCronJob[] | null> {
|
|
117
134
|
try {
|
|
118
|
-
|
|
135
|
+
// Not a container command — this is the openclaw CLI on the host itself,
|
|
136
|
+
// so it goes through execFile directly rather than the docker-exec helper.
|
|
137
|
+
const raw = await execFileAsync('openclaw', ['cron', 'list', '--json', '--all'], {
|
|
119
138
|
encoding: 'utf8', timeout: 15000,
|
|
120
139
|
});
|
|
121
|
-
return parseCronListOutput(raw);
|
|
140
|
+
return parseCronListOutput(raw.stdout);
|
|
122
141
|
} catch {
|
|
123
142
|
return null;
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
|
|
127
|
-
function listJobsInContainer(containerName: string): OpenClawCronJob[] | null {
|
|
146
|
+
async function listJobsInContainer(containerName: string): Promise<OpenClawCronJob[] | null> {
|
|
128
147
|
// Try both common agent gateway ports. Agents default to 3000 (OpenClaw
|
|
129
148
|
// gateway --bind lan --port 3000) but may also run on 18789 (default).
|
|
149
|
+
// Kept sequential on purpose: the second is a fallback for the first, so
|
|
150
|
+
// firing both would double the load to discard one answer.
|
|
130
151
|
const gatewayUrls = [
|
|
131
152
|
'http://127.0.0.1:3000',
|
|
132
153
|
'http://127.0.0.1:18789',
|
|
133
154
|
];
|
|
134
155
|
|
|
135
156
|
for (const gwUrl of gatewayUrls) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
const raw = await dockerExecNoFail(
|
|
158
|
+
containerName,
|
|
159
|
+
['openclaw', 'cron', 'list', '--json', '--all'],
|
|
160
|
+
{ env: { OPENCLAW_GATEWAY_URL: gwUrl }, timeoutMs: 15000 },
|
|
161
|
+
);
|
|
162
|
+
if (!raw) continue;
|
|
163
|
+
const parsed = parseCronListOutput(raw);
|
|
164
|
+
if (parsed) return parsed;
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
return null;
|
|
@@ -180,12 +201,20 @@ function toggleInContainer(containerName: string, jobId: string, enabled: boolea
|
|
|
180
201
|
|
|
181
202
|
// ── List: host gateway + per-container cron ─────────────────────────────
|
|
182
203
|
|
|
183
|
-
export function listOpenClawCronJobs(): OpenClawCronListResult {
|
|
204
|
+
export async function listOpenClawCronJobs(): Promise<OpenClawCronListResult> {
|
|
184
205
|
const seen = new Set<string>();
|
|
185
206
|
const jobs: OpenClawCronJob[] = [];
|
|
186
207
|
|
|
208
|
+
// The host query and the agent discovery don't depend on each other, so they
|
|
209
|
+
// overlap. Everything below is ordered deterministically afterwards — host
|
|
210
|
+
// jobs first, then agents in discovery order — so the response doesn't shuffle
|
|
211
|
+
// between polls just because a container answered sooner this time.
|
|
212
|
+
const [hostJobs, agents] = await Promise.all([
|
|
213
|
+
listJobsOnHost(),
|
|
214
|
+
discoverAgentContainers(),
|
|
215
|
+
]);
|
|
216
|
+
|
|
187
217
|
// 1. Host gateway cron jobs
|
|
188
|
-
const hostJobs = listJobsOnHost();
|
|
189
218
|
if (hostJobs) {
|
|
190
219
|
for (const job of hostJobs) {
|
|
191
220
|
job.scheduleExpr = extractScheduleExpr(job);
|
|
@@ -195,22 +224,26 @@ export function listOpenClawCronJobs(): OpenClawCronListResult {
|
|
|
195
224
|
}
|
|
196
225
|
}
|
|
197
226
|
|
|
198
|
-
// 2. Agent container cron jobs
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
// 2. Agent container cron jobs — queried concurrently, merged in order.
|
|
228
|
+
const perAgent = await mapWithConcurrency(
|
|
229
|
+
agents,
|
|
230
|
+
AGENT_QUERY_CONCURRENCY,
|
|
231
|
+
(agent) => listJobsInContainer(agent.containerName),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
for (const [i, containerJobs] of perAgent.entries()) {
|
|
235
|
+
if (!containerJobs) continue;
|
|
236
|
+
const agent = agents[i];
|
|
237
|
+
for (const job of containerJobs) {
|
|
238
|
+
job.scheduleExpr = extractScheduleExpr(job);
|
|
239
|
+
job.source = 'container';
|
|
240
|
+
job.containerName = agent.containerName;
|
|
241
|
+
if (!job.agentId) job.agentId = agent.agentId;
|
|
242
|
+
|
|
243
|
+
if (job.id && seen.has(job.id)) continue;
|
|
244
|
+
|
|
245
|
+
jobs.push(job);
|
|
246
|
+
if (job.id) seen.add(job.id);
|
|
214
247
|
}
|
|
215
248
|
}
|
|
216
249
|
|