@lightcone-ai/daemon 0.13.0 → 0.14.1
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/mysql/core.js +270 -0
- package/mcp-servers/mysql/index.js +79 -151
- package/mcp-servers/mysql/manifest.json +8 -6
- 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/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 +10 -1
- package/src/chat-bridge.js +440 -15
- package/src/drivers/claude.js +10 -3
- package/src/mcp-config.js +3 -1
- 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
|
@@ -462,7 +462,12 @@ export class AgentManager {
|
|
|
462
462
|
chatBridgePath,
|
|
463
463
|
config,
|
|
464
464
|
triggerType = 'resume',
|
|
465
|
+
triggerContext = null,
|
|
465
466
|
}) {
|
|
467
|
+
const mergedTriggerContext = {
|
|
468
|
+
source: 'daemon-agent-manager',
|
|
469
|
+
...(triggerContext && typeof triggerContext === 'object' ? triggerContext : {}),
|
|
470
|
+
};
|
|
466
471
|
const res = await fetch(`${this.serverUrl}/governance/spawn-directive`, {
|
|
467
472
|
method: 'POST',
|
|
468
473
|
headers: {
|
|
@@ -474,7 +479,7 @@ export class AgentManager {
|
|
|
474
479
|
workspace_id: workspaceId ?? null,
|
|
475
480
|
trigger: {
|
|
476
481
|
type: triggerType,
|
|
477
|
-
context:
|
|
482
|
+
context: mergedTriggerContext,
|
|
478
483
|
},
|
|
479
484
|
runtime_context: {
|
|
480
485
|
cli_type: runtime,
|
|
@@ -535,6 +540,10 @@ export class AgentManager {
|
|
|
535
540
|
const nextDirective = await this._fetchSpawnDirective({
|
|
536
541
|
...context,
|
|
537
542
|
triggerType: 'lease_refresh',
|
|
543
|
+
triggerContext: {
|
|
544
|
+
spawn_bundle_id: directive?.spawn_bundle_id ?? null,
|
|
545
|
+
policy_version: directive?.policy_version ?? null,
|
|
546
|
+
},
|
|
538
547
|
});
|
|
539
548
|
const agent = this.agents.get(key);
|
|
540
549
|
if (!agent) return;
|