@lightcone-ai/daemon 0.14.0 → 0.14.2
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/mcp-servers/official/page-understanding/index.js +93 -0
- package/mcp-servers/official/page-understanding/manifest.json +20 -0
- package/mcp-servers/official/video-narration-planner/core.js +1436 -0
- package/mcp-servers/official/video-narration-planner/index.js +98 -0
- package/mcp-servers/official/video-narration-planner/manifest.json +30 -0
- package/mcp-servers/publisher/index.js +9 -0
- package/mcp-servers/sophon-data/index.js +449 -0
- package/mcp-servers/sophon-data/manifest.json +19 -0
- package/package.json +1 -1
- package/src/_vendor/video/composer/index.js +377 -0
- package/src/agent-manager.js +102 -1
- package/src/chat-bridge.js +407 -15
- package/src/drivers/claude.js +10 -3
- package/src/mcp-config.js +1 -0
- package/src/publish-job-runner.js +438 -0
- package/src/workspace-file-upload.js +71 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { access, mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { constants as fsConstants } from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
|
|
7
|
+
const MAX_STDERR_LENGTH = 4000;
|
|
8
|
+
|
|
9
|
+
const TRANSCODE_TARGETS = Object.freeze({
|
|
10
|
+
short_video_cn: {
|
|
11
|
+
width: 1080,
|
|
12
|
+
height: 1920,
|
|
13
|
+
fps: 30,
|
|
14
|
+
videoCodec: 'libx264',
|
|
15
|
+
profile: 'baseline',
|
|
16
|
+
pixelFormat: 'yuv420p',
|
|
17
|
+
crf: 23,
|
|
18
|
+
preset: 'veryfast',
|
|
19
|
+
level: '4.0',
|
|
20
|
+
audioCodec: 'aac',
|
|
21
|
+
audioBitrate: '128k',
|
|
22
|
+
audioSampleRate: 48000,
|
|
23
|
+
audioChannels: 2,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function normalizeText(value) {
|
|
28
|
+
if (typeof value !== 'string') return '';
|
|
29
|
+
return value.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePath(value, label) {
|
|
33
|
+
const raw = normalizeText(value);
|
|
34
|
+
if (!raw) throw new Error(`${label} required`);
|
|
35
|
+
return path.resolve(raw);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ensureReadableFile(filePath, label) {
|
|
39
|
+
try {
|
|
40
|
+
await access(filePath, fsConstants.R_OK);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${label} not found or unreadable: ${filePath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const st = await stat(filePath);
|
|
46
|
+
if (!st.isFile()) throw new Error(`${label} is not a file: ${filePath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function ensureParentDir(filePath) {
|
|
50
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sanitizeStderr(stderr) {
|
|
54
|
+
const text = String(stderr ?? '').trim();
|
|
55
|
+
if (!text) return '';
|
|
56
|
+
if (text.length <= MAX_STDERR_LENGTH) return text;
|
|
57
|
+
return text.slice(text.length - MAX_STDERR_LENGTH);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toolError(prefix, error, stderr = '') {
|
|
61
|
+
const details = [];
|
|
62
|
+
const message = normalizeText(error?.message);
|
|
63
|
+
if (message) details.push(message);
|
|
64
|
+
const cleanedStderr = sanitizeStderr(stderr);
|
|
65
|
+
if (cleanedStderr) details.push(cleanedStderr);
|
|
66
|
+
const suffix = details.length > 0 ? `: ${details.join(' | ')}` : '';
|
|
67
|
+
return new Error(`${prefix}${suffix}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runProcess(binary, args, { name = binary } = {}) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
73
|
+
let stdout = '';
|
|
74
|
+
let stderr = '';
|
|
75
|
+
|
|
76
|
+
child.stdout.on('data', (chunk) => {
|
|
77
|
+
stdout += chunk.toString();
|
|
78
|
+
});
|
|
79
|
+
child.stderr.on('data', (chunk) => {
|
|
80
|
+
stderr += chunk.toString();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
child.on('error', (error) => {
|
|
84
|
+
reject(toolError(`${name} failed`, error, stderr));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
child.on('close', (code) => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
resolve({ stdout, stderr });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
reject(toolError(`${name} exited with code ${code}`, null, stderr));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeStartMs(value) {
|
|
98
|
+
const parsed = Number(value);
|
|
99
|
+
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
100
|
+
return Math.floor(parsed);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveStartMsFromEvents(segment, eventsLog = []) {
|
|
104
|
+
const phase = normalizeText(segment?.phase ?? segment?.phase_id ?? segment?.phaseId);
|
|
105
|
+
if (!phase) return null;
|
|
106
|
+
|
|
107
|
+
let candidate = null;
|
|
108
|
+
for (const event of eventsLog) {
|
|
109
|
+
const eventPhase = normalizeText(event?.phase ?? event?.phase_id ?? event?.phaseId);
|
|
110
|
+
if (!eventPhase || eventPhase !== phase) continue;
|
|
111
|
+
|
|
112
|
+
const eventStart = normalizeStartMs(
|
|
113
|
+
event?.t_ms_start
|
|
114
|
+
?? event?.tMsStart
|
|
115
|
+
?? event?.t_ms
|
|
116
|
+
?? event?.tMs
|
|
117
|
+
);
|
|
118
|
+
if (eventStart == null) continue;
|
|
119
|
+
if (candidate == null || eventStart < candidate) candidate = eventStart;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeAudioSegments(audioSegments = [], eventsLog = []) {
|
|
126
|
+
if (!Array.isArray(audioSegments)) {
|
|
127
|
+
throw new Error('audio_segments must be an array');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return audioSegments.map((segment, index) => {
|
|
131
|
+
if (!segment || typeof segment !== 'object' || Array.isArray(segment)) {
|
|
132
|
+
throw new Error(`audio_segments[${index}] must be an object`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const audioPath = normalizePath(segment.audio_path ?? segment.audioPath, `audio_segments[${index}].audio_path`);
|
|
136
|
+
const startMs = normalizeStartMs(segment.start_ms ?? segment.startMs)
|
|
137
|
+
?? resolveStartMsFromEvents(segment, eventsLog);
|
|
138
|
+
if (startMs == null) {
|
|
139
|
+
throw new Error(`audio_segments[${index}].start_ms missing (and no matching events_log entry found)`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
audioPath,
|
|
144
|
+
startMs,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function defaultOutputPath(inputPath, suffix) {
|
|
150
|
+
const ext = path.extname(inputPath) || '.mp4';
|
|
151
|
+
const base = path.basename(inputPath, ext);
|
|
152
|
+
return path.join(path.dirname(inputPath), `${base}.${suffix}${ext}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function escapeConcatPath(filePath) {
|
|
156
|
+
return filePath.replace(/'/g, `'\\''`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function muxAudioToVideo({
|
|
160
|
+
video_path,
|
|
161
|
+
audio_segments = [],
|
|
162
|
+
events_log = [],
|
|
163
|
+
output = null,
|
|
164
|
+
} = {}) {
|
|
165
|
+
const videoPath = normalizePath(video_path, 'video_path');
|
|
166
|
+
await ensureReadableFile(videoPath, 'video_path');
|
|
167
|
+
|
|
168
|
+
const segments = normalizeAudioSegments(audio_segments, events_log).sort((a, b) => a.startMs - b.startMs);
|
|
169
|
+
for (const segment of segments) {
|
|
170
|
+
await ensureReadableFile(segment.audioPath, `audio segment (${segment.audioPath})`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const outputPath = output
|
|
174
|
+
? normalizePath(output, 'output')
|
|
175
|
+
: defaultOutputPath(videoPath, 'muxed');
|
|
176
|
+
if (outputPath === videoPath) throw new Error('output must not equal video_path');
|
|
177
|
+
await ensureParentDir(outputPath);
|
|
178
|
+
|
|
179
|
+
if (segments.length === 0) {
|
|
180
|
+
await runProcess('ffmpeg', [
|
|
181
|
+
'-y',
|
|
182
|
+
'-i', videoPath,
|
|
183
|
+
'-map', '0:v:0',
|
|
184
|
+
'-c:v', 'copy',
|
|
185
|
+
'-an',
|
|
186
|
+
outputPath,
|
|
187
|
+
], { name: 'ffmpeg mux(no-audio)' });
|
|
188
|
+
return outputPath;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const filterChunks = [];
|
|
192
|
+
const mixInputs = [];
|
|
193
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
194
|
+
const delay = segments[i].startMs;
|
|
195
|
+
const label = `a${i}`;
|
|
196
|
+
filterChunks.push(`[${i + 1}:a]adelay=${delay}|${delay},aresample=async=1:first_pts=0[${label}]`);
|
|
197
|
+
mixInputs.push(`[${label}]`);
|
|
198
|
+
}
|
|
199
|
+
filterChunks.push(`${mixInputs.join('')}amix=inputs=${segments.length}:duration=longest:dropout_transition=0,apad[a]`);
|
|
200
|
+
|
|
201
|
+
const args = [
|
|
202
|
+
'-y',
|
|
203
|
+
'-i', videoPath,
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (const segment of segments) {
|
|
207
|
+
args.push('-i', segment.audioPath);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
args.push(
|
|
211
|
+
'-filter_complex', filterChunks.join(';'),
|
|
212
|
+
'-map', '0:v:0',
|
|
213
|
+
'-map', '[a]',
|
|
214
|
+
'-c:v', 'copy',
|
|
215
|
+
'-c:a', 'aac',
|
|
216
|
+
'-shortest',
|
|
217
|
+
'-movflags', '+faststart',
|
|
218
|
+
outputPath
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
await runProcess('ffmpeg', args, { name: 'ffmpeg mux' });
|
|
222
|
+
return outputPath;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function concatVideos({
|
|
226
|
+
inputs = [],
|
|
227
|
+
output,
|
|
228
|
+
} = {}) {
|
|
229
|
+
if (!Array.isArray(inputs) || inputs.length === 0) {
|
|
230
|
+
throw new Error('inputs must be a non-empty array');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const normalizedInputs = inputs.map((input, index) => normalizePath(input, `inputs[${index}]`));
|
|
234
|
+
for (const inputPath of normalizedInputs) {
|
|
235
|
+
await ensureReadableFile(inputPath, `concat input (${inputPath})`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const outputPath = output
|
|
239
|
+
? normalizePath(output, 'output')
|
|
240
|
+
: defaultOutputPath(normalizedInputs[0], 'concat');
|
|
241
|
+
await ensureParentDir(outputPath);
|
|
242
|
+
|
|
243
|
+
if (normalizedInputs.length === 1) {
|
|
244
|
+
await runProcess('ffmpeg', [
|
|
245
|
+
'-y',
|
|
246
|
+
'-i', normalizedInputs[0],
|
|
247
|
+
'-c', 'copy',
|
|
248
|
+
outputPath,
|
|
249
|
+
], { name: 'ffmpeg concat(single-input)' });
|
|
250
|
+
return outputPath;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'lightcone-concat-'));
|
|
254
|
+
const listPath = path.join(tempDir, 'inputs.txt');
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const content = normalizedInputs
|
|
258
|
+
.map(inputPath => `file '${escapeConcatPath(inputPath)}'`)
|
|
259
|
+
.join('\n');
|
|
260
|
+
await writeFile(listPath, `${content}\n`, 'utf8');
|
|
261
|
+
|
|
262
|
+
await runProcess('ffmpeg', [
|
|
263
|
+
'-y',
|
|
264
|
+
'-f', 'concat',
|
|
265
|
+
'-safe', '0',
|
|
266
|
+
'-i', listPath,
|
|
267
|
+
'-c', 'copy',
|
|
268
|
+
'-movflags', '+faststart',
|
|
269
|
+
outputPath,
|
|
270
|
+
], { name: 'ffmpeg concat' });
|
|
271
|
+
} finally {
|
|
272
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return outputPath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveTranscodeTarget(target) {
|
|
279
|
+
const normalized = normalizeText(target).toLowerCase();
|
|
280
|
+
if (!normalized) return TRANSCODE_TARGETS.short_video_cn;
|
|
281
|
+
|
|
282
|
+
if (normalized === 'short_video_cn' || normalized === 'douyin' || normalized === 'xhs') {
|
|
283
|
+
return TRANSCODE_TARGETS.short_video_cn;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw new Error(`unsupported transcode target: ${target}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function transcodeForPlatform({
|
|
290
|
+
input,
|
|
291
|
+
output,
|
|
292
|
+
target = 'short_video_cn',
|
|
293
|
+
} = {}) {
|
|
294
|
+
const inputPath = normalizePath(input, 'input');
|
|
295
|
+
await ensureReadableFile(inputPath, 'input');
|
|
296
|
+
|
|
297
|
+
const outputPath = output
|
|
298
|
+
? normalizePath(output, 'output')
|
|
299
|
+
: defaultOutputPath(inputPath, 'platform');
|
|
300
|
+
if (outputPath === inputPath) throw new Error('output must not equal input');
|
|
301
|
+
await ensureParentDir(outputPath);
|
|
302
|
+
|
|
303
|
+
const preset = resolveTranscodeTarget(target);
|
|
304
|
+
const vf = [
|
|
305
|
+
`scale=${preset.width}:${preset.height}:force_original_aspect_ratio=decrease`,
|
|
306
|
+
`pad=${preset.width}:${preset.height}:(ow-iw)/2:(oh-ih)/2:black`,
|
|
307
|
+
'setsar=1',
|
|
308
|
+
].join(',');
|
|
309
|
+
|
|
310
|
+
await runProcess('ffmpeg', [
|
|
311
|
+
'-y',
|
|
312
|
+
'-i', inputPath,
|
|
313
|
+
'-vf', vf,
|
|
314
|
+
'-r', String(preset.fps),
|
|
315
|
+
'-c:v', preset.videoCodec,
|
|
316
|
+
'-profile:v', preset.profile,
|
|
317
|
+
'-level', preset.level,
|
|
318
|
+
'-pix_fmt', preset.pixelFormat,
|
|
319
|
+
'-preset', preset.preset,
|
|
320
|
+
'-crf', String(preset.crf),
|
|
321
|
+
'-c:a', preset.audioCodec,
|
|
322
|
+
'-b:a', preset.audioBitrate,
|
|
323
|
+
'-ar', String(preset.audioSampleRate),
|
|
324
|
+
'-ac', String(preset.audioChannels),
|
|
325
|
+
'-movflags', '+faststart',
|
|
326
|
+
outputPath,
|
|
327
|
+
], { name: 'ffmpeg transcode' });
|
|
328
|
+
|
|
329
|
+
return outputPath;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function probeDurationMs(inputPath) {
|
|
333
|
+
const resolved = normalizePath(inputPath, 'input');
|
|
334
|
+
await ensureReadableFile(resolved, 'input');
|
|
335
|
+
|
|
336
|
+
const { stdout } = await runProcess('ffprobe', [
|
|
337
|
+
'-v', 'error',
|
|
338
|
+
'-show_entries', 'format=duration',
|
|
339
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
340
|
+
resolved,
|
|
341
|
+
], { name: 'ffprobe duration' });
|
|
342
|
+
|
|
343
|
+
const seconds = Number.parseFloat(String(stdout ?? '').trim());
|
|
344
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
345
|
+
throw new Error(`ffprobe returned invalid duration for ${resolved}`);
|
|
346
|
+
}
|
|
347
|
+
return Math.floor(seconds * 1000);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function readMediaSpec(inputPath) {
|
|
351
|
+
const resolved = normalizePath(inputPath, 'input');
|
|
352
|
+
await ensureReadableFile(resolved, 'input');
|
|
353
|
+
|
|
354
|
+
const { stdout } = await runProcess('ffprobe', [
|
|
355
|
+
'-v', 'error',
|
|
356
|
+
'-select_streams', 'v:0',
|
|
357
|
+
'-show_entries', 'stream=width,height,r_frame_rate,pix_fmt,codec_name',
|
|
358
|
+
'-of', 'json',
|
|
359
|
+
resolved,
|
|
360
|
+
], { name: 'ffprobe spec' });
|
|
361
|
+
|
|
362
|
+
let parsed;
|
|
363
|
+
try {
|
|
364
|
+
parsed = JSON.parse(stdout);
|
|
365
|
+
} catch {
|
|
366
|
+
throw new Error(`Failed to parse ffprobe spec output for ${resolved}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const stream = parsed?.streams?.[0] ?? {};
|
|
370
|
+
return {
|
|
371
|
+
width: Number(stream.width) || null,
|
|
372
|
+
height: Number(stream.height) || null,
|
|
373
|
+
frame_rate: String(stream.r_frame_rate ?? ''),
|
|
374
|
+
pixel_format: String(stream.pix_fmt ?? ''),
|
|
375
|
+
video_codec: String(stream.codec_name ?? ''),
|
|
376
|
+
};
|
|
377
|
+
}
|
package/src/agent-manager.js
CHANGED
|
@@ -16,6 +16,7 @@ import { injectWorkspaceContext } from './drivers/claude.js';
|
|
|
16
16
|
import { parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
|
|
17
17
|
import { startSession, stopSession, stopAllSessions } from './browser-login.js';
|
|
18
18
|
import { markInvalidatedLeases } from './governance-state.js';
|
|
19
|
+
import { runPublishJob } from './publish-job-runner.js';
|
|
19
20
|
|
|
20
21
|
const KIMI_SYSTEM_PROMPT_FILE = '.lightcone-kimi-system.md';
|
|
21
22
|
const KIMI_AGENT_FILE = '.lightcone-kimi-agent.yaml';
|
|
@@ -156,6 +157,7 @@ export class AgentManager {
|
|
|
156
157
|
case 'agent:start': return this._startAgent(msg, connection);
|
|
157
158
|
case 'agent:stop': return this._stopAgent(msg.agentId, msg.workspaceId, connection);
|
|
158
159
|
case 'agent:deliver': return this._deliverMessage(msg, connection);
|
|
160
|
+
case 'publish:job': return this._handlePublishJob(msg, connection);
|
|
159
161
|
case 'browser:start_login': return this._startBrowserLogin(msg, connection);
|
|
160
162
|
case 'browser:stop_login': return this._stopBrowserLogin(msg);
|
|
161
163
|
case 'policy_invalidate': return this._handlePolicyInvalidate(msg, connection);
|
|
@@ -462,7 +464,12 @@ export class AgentManager {
|
|
|
462
464
|
chatBridgePath,
|
|
463
465
|
config,
|
|
464
466
|
triggerType = 'resume',
|
|
467
|
+
triggerContext = null,
|
|
465
468
|
}) {
|
|
469
|
+
const mergedTriggerContext = {
|
|
470
|
+
source: 'daemon-agent-manager',
|
|
471
|
+
...(triggerContext && typeof triggerContext === 'object' ? triggerContext : {}),
|
|
472
|
+
};
|
|
466
473
|
const res = await fetch(`${this.serverUrl}/governance/spawn-directive`, {
|
|
467
474
|
method: 'POST',
|
|
468
475
|
headers: {
|
|
@@ -474,7 +481,7 @@ export class AgentManager {
|
|
|
474
481
|
workspace_id: workspaceId ?? null,
|
|
475
482
|
trigger: {
|
|
476
483
|
type: triggerType,
|
|
477
|
-
context:
|
|
484
|
+
context: mergedTriggerContext,
|
|
478
485
|
},
|
|
479
486
|
runtime_context: {
|
|
480
487
|
cli_type: runtime,
|
|
@@ -535,6 +542,10 @@ export class AgentManager {
|
|
|
535
542
|
const nextDirective = await this._fetchSpawnDirective({
|
|
536
543
|
...context,
|
|
537
544
|
triggerType: 'lease_refresh',
|
|
545
|
+
triggerContext: {
|
|
546
|
+
spawn_bundle_id: directive?.spawn_bundle_id ?? null,
|
|
547
|
+
policy_version: directive?.policy_version ?? null,
|
|
548
|
+
},
|
|
538
549
|
});
|
|
539
550
|
const agent = this.agents.get(key);
|
|
540
551
|
if (!agent) return;
|
|
@@ -996,6 +1007,96 @@ export class AgentManager {
|
|
|
996
1007
|
}
|
|
997
1008
|
}
|
|
998
1009
|
|
|
1010
|
+
async _postInternalActionComplete({ agentId, actionId, ok, result, error }) {
|
|
1011
|
+
const url = `${this.serverUrl.replace(/\/$/, '')}/internal/agent/${encodeURIComponent(agentId)}/actions/${encodeURIComponent(actionId)}/complete`;
|
|
1012
|
+
const res = await fetch(url, {
|
|
1013
|
+
method: 'POST',
|
|
1014
|
+
headers: {
|
|
1015
|
+
'Content-Type': 'application/json',
|
|
1016
|
+
Authorization: `Bearer ${this.machineApiKey}`,
|
|
1017
|
+
},
|
|
1018
|
+
body: JSON.stringify({ ok, result, error }),
|
|
1019
|
+
});
|
|
1020
|
+
if (!res.ok) {
|
|
1021
|
+
const text = await res.text();
|
|
1022
|
+
throw new Error(`action complete failed (${res.status}): ${text}`);
|
|
1023
|
+
}
|
|
1024
|
+
return res.json();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async _handlePublishJob(msg, connection) {
|
|
1028
|
+
const job = normalizeObject(msg.job);
|
|
1029
|
+
const jobId = String(job.id ?? '').trim();
|
|
1030
|
+
const actionId = String(job.approval_action_id ?? '').trim();
|
|
1031
|
+
const agentId = String(job.agent_id ?? '').trim();
|
|
1032
|
+
const workspaceId = String(job.workspace_id ?? '').trim() || null;
|
|
1033
|
+
|
|
1034
|
+
if (!jobId || !actionId || !agentId) {
|
|
1035
|
+
console.warn('[AgentManager] publish:job missing required fields (id, approval_action_id, agent_id)');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
connection.send({
|
|
1040
|
+
type: 'publish:job_status',
|
|
1041
|
+
job_id: jobId,
|
|
1042
|
+
action_id: actionId,
|
|
1043
|
+
status: 'running',
|
|
1044
|
+
started_at: new Date().toISOString(),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
const workspaceDir = this._workspaceDir(agentId, workspaceId);
|
|
1049
|
+
const publishResult = await runPublishJob({
|
|
1050
|
+
serverUrl: this.serverUrl,
|
|
1051
|
+
machineApiKey: this.machineApiKey,
|
|
1052
|
+
agentId,
|
|
1053
|
+
workspaceDir,
|
|
1054
|
+
job,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
await this._postInternalActionComplete({
|
|
1058
|
+
agentId,
|
|
1059
|
+
actionId,
|
|
1060
|
+
ok: true,
|
|
1061
|
+
result: publishResult.completionResult,
|
|
1062
|
+
error: null,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
connection.send({
|
|
1066
|
+
type: 'publish:job_status',
|
|
1067
|
+
job_id: jobId,
|
|
1068
|
+
action_id: actionId,
|
|
1069
|
+
status: 'succeeded',
|
|
1070
|
+
completed_at: new Date().toISOString(),
|
|
1071
|
+
result: publishResult.completionResult,
|
|
1072
|
+
});
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
const errorMessage = err?.message ?? String(err);
|
|
1075
|
+
try {
|
|
1076
|
+
await this._postInternalActionComplete({
|
|
1077
|
+
agentId,
|
|
1078
|
+
actionId,
|
|
1079
|
+
ok: false,
|
|
1080
|
+
result: null,
|
|
1081
|
+
error: errorMessage,
|
|
1082
|
+
});
|
|
1083
|
+
} catch (completeErr) {
|
|
1084
|
+
console.error(
|
|
1085
|
+
`[AgentManager] Failed to report publish job completion for action=${actionId}: ${completeErr.message}`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
connection.send({
|
|
1090
|
+
type: 'publish:job_status',
|
|
1091
|
+
job_id: jobId,
|
|
1092
|
+
action_id: actionId,
|
|
1093
|
+
status: 'failed',
|
|
1094
|
+
completed_at: new Date().toISOString(),
|
|
1095
|
+
error: errorMessage,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
999
1100
|
_stopAgent(agentId, workspaceId, connection) {
|
|
1000
1101
|
const key = this._key(agentId, workspaceId);
|
|
1001
1102
|
const agent = this.agents.get(key);
|