@lightcone-ai/daemon 0.15.63 → 0.15.65
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/package.json +1 -1
- package/src/mcp-config.js +30 -15
- package/src/tools/compose-video-v2.js +40 -2
package/package.json
CHANGED
package/src/mcp-config.js
CHANGED
|
@@ -62,25 +62,40 @@ function resolveSkillArg(arg, config) {
|
|
|
62
62
|
return arg;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// MCP servers that talk to the lightcone server at runtime — either a full
|
|
66
|
+
// server-backed MCP (mysql / publisher / platform / domain-data services) or a
|
|
67
|
+
// thin-proxy (`daemon/mcp-servers/_thin-proxy/forward.js`) whose tools forward
|
|
68
|
+
// to /internal/agent/:agentId/mcp/:serverId/:toolName. All of these need the
|
|
69
|
+
// SERVER_URL / MACHINE_API_KEY / AGENT_ID triple injected, or the proxy throws
|
|
70
|
+
// "thin-proxy missing SERVER_URL / MACHINE_API_KEY / AGENT_ID env".
|
|
71
|
+
const SERVER_BACKED_MCP_SERVERS = new Set([
|
|
72
|
+
'mysql',
|
|
73
|
+
'publisher',
|
|
74
|
+
'platform',
|
|
75
|
+
'research-fetch',
|
|
76
|
+
'wechat-mp-fetch',
|
|
77
|
+
'market-data-query',
|
|
78
|
+
'company-fundamentals',
|
|
79
|
+
'industry-report',
|
|
80
|
+
'risk-metrics',
|
|
81
|
+
'compliance-check',
|
|
82
|
+
'portfolio-read',
|
|
83
|
+
'portfolio-analysis',
|
|
84
|
+
// thin-proxy MCP services migrated to the server (roadmap §4) — every one of
|
|
85
|
+
// these uses startThinProxy() and requires the SERVER_URL/MACHINE_API_KEY/AGENT_ID triple.
|
|
86
|
+
'video-narration-planner',
|
|
87
|
+
'page-understanding',
|
|
88
|
+
'platform-policy-db',
|
|
89
|
+
'keyword-research',
|
|
90
|
+
'audience-research',
|
|
91
|
+
'hook-pattern-library',
|
|
92
|
+
]);
|
|
93
|
+
|
|
65
94
|
function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceId, workspaceDir }) {
|
|
66
95
|
if (serverKey === 'workspace-migrate') {
|
|
67
96
|
return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
|
|
68
97
|
}
|
|
69
|
-
if (
|
|
70
|
-
serverKey === 'mysql'
|
|
71
|
-
|| serverKey === 'publisher'
|
|
72
|
-
|| serverKey === 'platform'
|
|
73
|
-
|| serverKey === 'research-fetch'
|
|
74
|
-
|| serverKey === 'wechat-mp-fetch'
|
|
75
|
-
|| serverKey === 'market-data-query'
|
|
76
|
-
|| serverKey === 'company-fundamentals'
|
|
77
|
-
|| serverKey === 'industry-report'
|
|
78
|
-
|| serverKey === 'risk-metrics'
|
|
79
|
-
|| serverKey === 'compliance-check'
|
|
80
|
-
|| serverKey === 'portfolio-read'
|
|
81
|
-
|| serverKey === 'portfolio-analysis'
|
|
82
|
-
|| serverKey === 'video-narration-planner'
|
|
83
|
-
) {
|
|
98
|
+
if (SERVER_BACKED_MCP_SERVERS.has(serverKey)) {
|
|
84
99
|
return {
|
|
85
100
|
SERVER_URL: serverUrl,
|
|
86
101
|
MACHINE_API_KEY: authToken,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import os from 'os';
|
|
3
|
+
import fs from 'fs';
|
|
3
4
|
import { randomUUID } from 'crypto';
|
|
4
5
|
import { composeVideoV2 } from '../_vendor/video/composer-v2/index.js';
|
|
5
6
|
|
|
@@ -11,11 +12,22 @@ function toolError(text) {
|
|
|
11
12
|
return { isError: true, content: [{ type: 'text', text }] };
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// A real screenshot / rendered card / video frame is at least tens of KB. A
|
|
16
|
+
// few-KB image is almost certainly a blank or broken capture (e.g. the page
|
|
17
|
+
// hadn't finished loading when screenshotted). Composing from those produces a
|
|
18
|
+
// near-blank video, so reject up front rather than emit garbage.
|
|
19
|
+
const MIN_IMAGE_BYTES = 4 * 1024;
|
|
20
|
+
|
|
21
|
+
function statSizeOrNull(p) {
|
|
22
|
+
try { return fs.statSync(p).size; } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export async function runComposeVideoV2Tool({ segments, outro_paths, format, resolution, output_path, workspaceDir }) {
|
|
15
26
|
if (!Array.isArray(segments) || segments.length === 0) {
|
|
16
27
|
return toolError('segments must be a non-empty array.');
|
|
17
28
|
}
|
|
18
29
|
|
|
30
|
+
const imagePaths = [];
|
|
19
31
|
for (let i = 0; i < segments.length; i++) {
|
|
20
32
|
const seg = segments[i];
|
|
21
33
|
const kind = seg.visual_kind;
|
|
@@ -30,6 +42,30 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
30
42
|
if (kind !== 'carousel' && !seg.visual_path) {
|
|
31
43
|
return toolError(`segments[${i}]: visual_path required for kind=${kind}.`);
|
|
32
44
|
}
|
|
45
|
+
if (kind === 'image' && seg.visual_path) imagePaths.push(seg.visual_path);
|
|
46
|
+
if (kind === 'carousel' && Array.isArray(seg.visual_paths)) imagePaths.push(...seg.visual_paths.filter(Boolean));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Garbage-in guard: refuse to compose from blank/broken source images.
|
|
50
|
+
for (const p of new Set(imagePaths)) {
|
|
51
|
+
const size = statSizeOrNull(p);
|
|
52
|
+
if (size === null) {
|
|
53
|
+
return toolError(`Source image not found: ${p}. Re-capture/render it before composing.`);
|
|
54
|
+
}
|
|
55
|
+
if (size < MIN_IMAGE_BYTES) {
|
|
56
|
+
return toolError(
|
|
57
|
+
`Source image ${p} is only ${size} bytes — almost certainly a blank or broken capture `
|
|
58
|
+
+ `(e.g. the page hadn't finished loading when screenshotted). Re-capture with the page fully loaded, then retry.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Heuristic warning: a multi-segment image video that reuses one single image
|
|
63
|
+
// will look near-static — usually a sign the source page didn't render and the
|
|
64
|
+
// agent fell back to one blank screenshot.
|
|
65
|
+
let warning = null;
|
|
66
|
+
if (imagePaths.length >= 2 && new Set(imagePaths).size === 1) {
|
|
67
|
+
warning = `WARNING: all ${imagePaths.length} image segments reuse the same file (${imagePaths[0]}). `
|
|
68
|
+
+ 'The output will be near-static — verify the source page actually rendered before submitting this video.';
|
|
33
69
|
}
|
|
34
70
|
|
|
35
71
|
const outDir = workspaceDir
|
|
@@ -46,13 +82,15 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
46
82
|
output_path: outPath,
|
|
47
83
|
});
|
|
48
84
|
|
|
49
|
-
|
|
85
|
+
const lines = [
|
|
50
86
|
'compose_video_v2 completed.',
|
|
51
87
|
`path=${result.path}`,
|
|
52
88
|
`duration_ms=${result.duration_ms}`,
|
|
53
89
|
`segments=${segments.length}`,
|
|
54
90
|
`outro_clips=${(outro_paths ?? []).length}`,
|
|
55
|
-
]
|
|
91
|
+
];
|
|
92
|
+
if (warning) lines.push(warning);
|
|
93
|
+
return toolText(lines.join('\n'));
|
|
56
94
|
} catch (error) {
|
|
57
95
|
return toolError(`compose_video_v2 failed: ${error.message}`);
|
|
58
96
|
}
|