@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
package/src/chat-bridge.js
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
5
|
+
import { createReadStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
6
6
|
import { createHash, randomUUID } from 'crypto';
|
|
7
7
|
import path, { extname } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import {
|
|
10
|
+
concatVideos,
|
|
11
|
+
muxAudioToVideo,
|
|
12
|
+
probeDurationMs,
|
|
13
|
+
transcodeForPlatform,
|
|
14
|
+
} from './_vendor/video/composer/index.js';
|
|
15
|
+
import {
|
|
16
|
+
VIDEO_EXT,
|
|
17
|
+
VIDEO_MIME,
|
|
18
|
+
writeLocalFileToWorkspace,
|
|
19
|
+
} from './workspace-file-upload.js';
|
|
8
20
|
import { isLeaseInvalidated, clearInvalidatedLease } from './governance-state.js';
|
|
9
21
|
import { classifyLeaseWindow } from './lease-window.js';
|
|
10
22
|
|
|
@@ -41,14 +53,9 @@ const BUNDLE_EVENT_FLUSH_MS = Number(process.env.GOVERNANCE_BUNDLE_FLUSH_MS ?? 2
|
|
|
41
53
|
// Current active workspaceId for memory isolation (defaults to spawn-time WORKSPACE_ID)
|
|
42
54
|
let currentWorkspaceId = WORKSPACE_ID;
|
|
43
55
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'.jpeg': 'image/jpeg',
|
|
48
|
-
'.webp': 'image/webp',
|
|
49
|
-
'.gif': 'image/gif',
|
|
50
|
-
'.pdf': 'application/pdf',
|
|
51
|
-
};
|
|
56
|
+
const VOICEOVER_LOCAL_DIR = path.join(WORKSPACE_DIR, 'artifacts', 'audio');
|
|
57
|
+
const VIDEO_COMPOSE_LOCAL_DIR = path.join(WORKSPACE_DIR, 'artifacts', 'video');
|
|
58
|
+
const DEFAULT_OUTRO_PATH = path.join(homedir(), '.lightcone', 'assets', 'outros', 'default.mp4');
|
|
52
59
|
|
|
53
60
|
function dataUrlSummary(content) {
|
|
54
61
|
if (typeof content !== 'string' || !content.startsWith('data:')) return null;
|
|
@@ -73,6 +80,27 @@ function formatBytes(bytes) {
|
|
|
73
80
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
function normalizeVoiceFormat(value) {
|
|
84
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
85
|
+
if (!normalized) return 'mp3';
|
|
86
|
+
if (['mp3', 'wav', 'flac'].includes(normalized)) return normalized;
|
|
87
|
+
return 'mp3';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inferAudioExtension(url, format = 'mp3') {
|
|
91
|
+
const normalizedFormat = normalizeVoiceFormat(format);
|
|
92
|
+
if (typeof url === 'string' && url.trim()) {
|
|
93
|
+
try {
|
|
94
|
+
const pathname = new URL(url).pathname;
|
|
95
|
+
const ext = extname(pathname).toLowerCase();
|
|
96
|
+
if (ext && ['.mp3', '.wav', '.flac'].includes(ext)) return ext;
|
|
97
|
+
} catch {
|
|
98
|
+
// noop
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return `.${normalizedFormat}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
76
104
|
function isInsideDir(filePath, dir) {
|
|
77
105
|
const rel = path.relative(dir, filePath);
|
|
78
106
|
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
@@ -88,6 +116,58 @@ function resolveLocalWorkspaceFile(filePath) {
|
|
|
88
116
|
throw new Error(`Local file must be inside the agent workspace or workspace shared artifacts/notes/tmp directories. Got: ${filePath}`);
|
|
89
117
|
}
|
|
90
118
|
|
|
119
|
+
function normalizeComposePath(filePath, label) {
|
|
120
|
+
const normalized = String(filePath ?? '').trim();
|
|
121
|
+
if (!normalized) throw new Error(`${label} is required.`);
|
|
122
|
+
return path.resolve(WORKSPACE_DIR, normalized);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeComposeTarget(value) {
|
|
126
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
127
|
+
if (!normalized) return 'short_video_cn';
|
|
128
|
+
if (['short_video_cn', 'douyin', 'xhs'].includes(normalized)) return normalized;
|
|
129
|
+
throw new Error(`Unsupported compose target: ${value}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeComposeAudioSegments(audioSegments) {
|
|
133
|
+
if (!Array.isArray(audioSegments) || audioSegments.length === 0) {
|
|
134
|
+
throw new Error('audio_segments must be a non-empty array.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return audioSegments.map((segment, index) => {
|
|
138
|
+
if (!segment || typeof segment !== 'object' || Array.isArray(segment)) {
|
|
139
|
+
throw new Error(`audio_segments[${index}] must be an object.`);
|
|
140
|
+
}
|
|
141
|
+
const audioPath = normalizeComposePath(
|
|
142
|
+
segment.audio_path ?? segment.audioPath,
|
|
143
|
+
`audio_segments[${index}].audio_path`
|
|
144
|
+
);
|
|
145
|
+
const startMsRaw = segment.start_ms ?? segment.startMs;
|
|
146
|
+
const startMs = startMsRaw == null ? null : Number(startMsRaw);
|
|
147
|
+
if (startMsRaw != null && (!Number.isFinite(startMs) || startMs < 0)) {
|
|
148
|
+
throw new Error(`audio_segments[${index}].start_ms must be a non-negative number.`);
|
|
149
|
+
}
|
|
150
|
+
const phase = String(segment.phase ?? segment.phase_id ?? segment.phaseId ?? '').trim();
|
|
151
|
+
if (startMs == null && !phase) {
|
|
152
|
+
throw new Error(`audio_segments[${index}] requires start_ms, or provide phase with events_log.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const normalized = { audio_path: audioPath };
|
|
156
|
+
if (startMs != null) normalized.start_ms = Math.floor(startMs);
|
|
157
|
+
if (phase) normalized.phase = phase;
|
|
158
|
+
return {
|
|
159
|
+
...normalized,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function cleanupLocalFiles(paths = []) {
|
|
165
|
+
for (const filePath of paths) {
|
|
166
|
+
if (!filePath) continue;
|
|
167
|
+
try { rmSync(filePath, { force: true }); } catch { /* noop */ }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
91
171
|
const DEFAULT_TOOL_CLASSIFICATION = {
|
|
92
172
|
check_messages: 'local',
|
|
93
173
|
list_memory: 'local',
|
|
@@ -115,6 +195,9 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
115
195
|
update_goal_field: 'mandatory',
|
|
116
196
|
supersede_goal_field: 'mandatory',
|
|
117
197
|
request_credential_auth: 'mandatory',
|
|
198
|
+
generate_voiceover: 'mandatory',
|
|
199
|
+
compose_video: 'mandatory',
|
|
200
|
+
register_data_source: 'mandatory',
|
|
118
201
|
bind_workspace_scenario: 'mandatory',
|
|
119
202
|
create_workspace: 'mandatory',
|
|
120
203
|
rename_workspace: 'mandatory',
|
|
@@ -170,6 +253,7 @@ const CACHE_INVALIDATION_TOOLS = new Set([
|
|
|
170
253
|
'update_goal_field',
|
|
171
254
|
'supersede_goal_field',
|
|
172
255
|
'request_credential_auth',
|
|
256
|
+
'register_data_source',
|
|
173
257
|
'bind_workspace_scenario',
|
|
174
258
|
'create_workspace',
|
|
175
259
|
'rename_workspace',
|
|
@@ -246,6 +330,8 @@ function inferToolForApi(method, apiPath, body) {
|
|
|
246
330
|
if (method === 'POST' && cleanPath === '/goal-fields/update') return 'update_goal_field';
|
|
247
331
|
if (method === 'POST' && cleanPath === '/goal-fields/supersede') return 'supersede_goal_field';
|
|
248
332
|
if (method === 'POST' && cleanPath === '/credential-auth/request') return 'request_credential_auth';
|
|
333
|
+
if (method === 'POST' && cleanPath === '/tts/voiceover') return 'generate_voiceover';
|
|
334
|
+
if (method === 'POST' && cleanPath === '/api/data-sources') return 'register_data_source';
|
|
249
335
|
if (method === 'POST' && cleanPath === '/orchestrate/decision') return 'write_governance_decision';
|
|
250
336
|
if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
|
|
251
337
|
if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
|
|
@@ -417,6 +503,32 @@ async function directApi(method, apiPath, body) {
|
|
|
417
503
|
return res.json();
|
|
418
504
|
}
|
|
419
505
|
|
|
506
|
+
async function directApiVideoUpload(apiPath, {
|
|
507
|
+
localPath,
|
|
508
|
+
filename,
|
|
509
|
+
contentType = VIDEO_MIME,
|
|
510
|
+
}) {
|
|
511
|
+
const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${apiPath}`;
|
|
512
|
+
const headers = {
|
|
513
|
+
'Authorization': `Bearer ${MACHINE_API_KEY}`,
|
|
514
|
+
'Content-Type': contentType,
|
|
515
|
+
};
|
|
516
|
+
if (filename) headers['X-File-Name'] = filename;
|
|
517
|
+
|
|
518
|
+
const res = await fetch(url, {
|
|
519
|
+
method: 'POST',
|
|
520
|
+
headers,
|
|
521
|
+
body: createReadStream(localPath),
|
|
522
|
+
duplex: 'half',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
if (!res.ok) {
|
|
526
|
+
const text = await res.text();
|
|
527
|
+
throw new Error(`API POST ${apiPath} → ${res.status}: ${text}`);
|
|
528
|
+
}
|
|
529
|
+
return res.json();
|
|
530
|
+
}
|
|
531
|
+
|
|
420
532
|
async function callGovernance(payload, { retry = true } = {}) {
|
|
421
533
|
const attempts = retry ? 2 : 1;
|
|
422
534
|
let lastError = null;
|
|
@@ -504,6 +616,61 @@ async function governanceRoundTrip({ method, apiPath, body, toolName, classifica
|
|
|
504
616
|
return directApi(method, apiPath, nextBody);
|
|
505
617
|
}
|
|
506
618
|
|
|
619
|
+
async function runMandatoryLocalTool({ toolName, toolInput = {}, executor }) {
|
|
620
|
+
const classification = TOOL_CLASSIFICATION[toolName] ?? 'mandatory';
|
|
621
|
+
const traceId = randomUUID();
|
|
622
|
+
enqueueBundleEvent('tool_call_started', {
|
|
623
|
+
trace_id: traceId,
|
|
624
|
+
tool_name: toolName,
|
|
625
|
+
tool_classification: classification,
|
|
626
|
+
method: 'LOCAL',
|
|
627
|
+
api_path: '/local-tool',
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
await ensureGovernanceContext();
|
|
632
|
+
const governancePayload = {
|
|
633
|
+
spawn_bundle_id: governanceContext.spawnBundleId,
|
|
634
|
+
policy_version: governanceContext.policyVersion,
|
|
635
|
+
tool_name: toolName,
|
|
636
|
+
tool_input: toolInput,
|
|
637
|
+
tool_classification: classification,
|
|
638
|
+
agent_id: AGENT_ID,
|
|
639
|
+
idempotency_key: randomUUID(),
|
|
640
|
+
lease_id: governanceContext.lease?.lease_id ?? null,
|
|
641
|
+
};
|
|
642
|
+
const governance = await callGovernance(governancePayload, { retry: true });
|
|
643
|
+
if (governance.policy_lease) applyPolicyLease(governance.policy_lease);
|
|
644
|
+
if (governance.verdict === 'reject' || governance.verdict === 'defer_human') {
|
|
645
|
+
throw governanceError(governanceReasonCode(governance.reason));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const checkedInput = (governance.verdict === 'modify' && governance.modified_input && typeof governance.modified_input === 'object')
|
|
649
|
+
? { ...toolInput, ...governance.modified_input }
|
|
650
|
+
: toolInput;
|
|
651
|
+
const result = await executor(checkedInput);
|
|
652
|
+
if (CACHE_INVALIDATION_TOOLS.has(toolName)) {
|
|
653
|
+
governanceContext.cache.clear();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
enqueueBundleEvent('tool_call_succeeded', {
|
|
657
|
+
trace_id: traceId,
|
|
658
|
+
tool_name: toolName,
|
|
659
|
+
tool_classification: classification,
|
|
660
|
+
source: 'governance_roundtrip',
|
|
661
|
+
});
|
|
662
|
+
return result;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
enqueueBundleEvent('tool_call_failed', {
|
|
665
|
+
trace_id: traceId,
|
|
666
|
+
tool_name: toolName,
|
|
667
|
+
tool_classification: classification,
|
|
668
|
+
reason: error?.code ?? error?.message ?? 'unknown_error',
|
|
669
|
+
});
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
507
674
|
function renewCacheInBackground({ method, apiPath, body, toolName, cacheKey }) {
|
|
508
675
|
if (governanceContext.renewalInFlight.has(cacheKey)) return;
|
|
509
676
|
governanceContext.renewalInFlight.add(cacheKey);
|
|
@@ -921,12 +1088,54 @@ server.tool('write_workspace_file', 'Write a local file directly to the shared w
|
|
|
921
1088
|
}, async ({ file_path, path }) => {
|
|
922
1089
|
if (!currentWorkspaceId) return { content: [{ type: 'text', text: 'No workspace context.' }] };
|
|
923
1090
|
const localPath = resolveLocalWorkspaceFile(file_path);
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1091
|
+
const result = await writeLocalFileToWorkspace({
|
|
1092
|
+
localPath,
|
|
1093
|
+
workspacePath: path,
|
|
1094
|
+
workspaceId: currentWorkspaceId,
|
|
1095
|
+
readFileSyncFn: readFileSync,
|
|
1096
|
+
uploadWorkspaceMemory: async ({ workspacePath, workspaceId, content }) => {
|
|
1097
|
+
await api('PUT', `/workspace-memory?path=${encodeURIComponent(workspacePath)}&workspaceId=${encodeURIComponent(workspaceId)}`, { content });
|
|
1098
|
+
},
|
|
1099
|
+
uploadVideo: async ({
|
|
1100
|
+
localPath: videoLocalPath,
|
|
1101
|
+
workspacePath,
|
|
1102
|
+
workspaceId,
|
|
1103
|
+
filename,
|
|
1104
|
+
mime,
|
|
1105
|
+
}) => {
|
|
1106
|
+
const uploadResult = await runMandatoryLocalTool({
|
|
1107
|
+
toolName: 'write_workspace_file',
|
|
1108
|
+
toolInput: {
|
|
1109
|
+
file_path,
|
|
1110
|
+
path: workspacePath,
|
|
1111
|
+
upload_mode: 'upload_video',
|
|
1112
|
+
},
|
|
1113
|
+
executor: async () => directApiVideoUpload(
|
|
1114
|
+
`/upload-video?workspaceId=${encodeURIComponent(workspaceId)}&filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(workspacePath)}`,
|
|
1115
|
+
{
|
|
1116
|
+
localPath: videoLocalPath,
|
|
1117
|
+
filename,
|
|
1118
|
+
contentType: mime,
|
|
1119
|
+
}
|
|
1120
|
+
),
|
|
1121
|
+
});
|
|
1122
|
+
return {
|
|
1123
|
+
mode: 'upload-video',
|
|
1124
|
+
mime: VIDEO_MIME,
|
|
1125
|
+
bytes: Number(uploadResult?.sizeBytes) || 0,
|
|
1126
|
+
};
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
const finalMime = result?.mime ?? (extname(path || localPath).toLowerCase() === VIDEO_EXT ? VIDEO_MIME : 'application/octet-stream');
|
|
1131
|
+
const finalBytes = Number.isFinite(result?.bytes) ? result.bytes : 0;
|
|
1132
|
+
const sizeText = finalBytes > 0 ? formatBytes(finalBytes) : 'unknown size';
|
|
1133
|
+
return {
|
|
1134
|
+
content: [{
|
|
1135
|
+
type: 'text',
|
|
1136
|
+
text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})`,
|
|
1137
|
+
}],
|
|
1138
|
+
};
|
|
930
1139
|
});
|
|
931
1140
|
|
|
932
1141
|
// ── skill_list ───────────────────────────────────────────────────────────────
|
|
@@ -1117,6 +1326,222 @@ server.tool('request_credential_auth',
|
|
|
1117
1326
|
}
|
|
1118
1327
|
);
|
|
1119
1328
|
|
|
1329
|
+
// ── generate_voiceover ─────────────────────────────────────────────────────────
|
|
1330
|
+
server.tool('generate_voiceover',
|
|
1331
|
+
'Generate a TTS voiceover using an authorized tts_provider credential and return a local audio file path.',
|
|
1332
|
+
{
|
|
1333
|
+
workspace_id: z.string().optional().describe('Target workspace id. Defaults to current workspace context.'),
|
|
1334
|
+
text: z.string().describe('Text content to synthesize.'),
|
|
1335
|
+
voice_preset: z.string().optional().describe('Platform-neutral voice preset id, e.g. "warm_female_zh_01".'),
|
|
1336
|
+
speed: z.number().optional().describe('Speech speed. Typical range is 0.5 to 2.0.'),
|
|
1337
|
+
format: z.enum(['mp3', 'wav', 'flac']).optional().describe('Audio format. Defaults to mp3.'),
|
|
1338
|
+
credential_id: z.string().optional().describe('Optional explicit credential id. If omitted, uses latest granted tts_provider credential.'),
|
|
1339
|
+
},
|
|
1340
|
+
async ({ workspace_id, text, voice_preset, speed, format, credential_id }) => {
|
|
1341
|
+
const targetWorkspaceId = (workspace_id ?? currentWorkspaceId ?? WORKSPACE_ID ?? '').trim();
|
|
1342
|
+
if (!targetWorkspaceId) {
|
|
1343
|
+
return { isError: true, content: [{ type: 'text', text: 'workspace_id is required (no current workspace context).' }] };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const normalizedText = String(text ?? '').trim();
|
|
1347
|
+
if (!normalizedText) {
|
|
1348
|
+
return { isError: true, content: [{ type: 'text', text: 'text is required for generate_voiceover.' }] };
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const normalizedSpeed = speed == null ? 1 : Number(speed);
|
|
1352
|
+
if (!Number.isFinite(normalizedSpeed)) {
|
|
1353
|
+
return { isError: true, content: [{ type: 'text', text: 'speed must be numeric.' }] };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const normalizedFormat = normalizeVoiceFormat(format);
|
|
1357
|
+
const payload = {
|
|
1358
|
+
workspace_id: targetWorkspaceId,
|
|
1359
|
+
text: normalizedText,
|
|
1360
|
+
speed: normalizedSpeed,
|
|
1361
|
+
format: normalizedFormat,
|
|
1362
|
+
};
|
|
1363
|
+
if (voice_preset) payload.voice_preset = String(voice_preset).trim();
|
|
1364
|
+
if (credential_id) payload.credential_id = String(credential_id).trim();
|
|
1365
|
+
|
|
1366
|
+
const data = await api('POST', '/tts/voiceover', payload);
|
|
1367
|
+
const remoteAudioUrl = String(data.audio_url ?? '').trim();
|
|
1368
|
+
if (!remoteAudioUrl) {
|
|
1369
|
+
return { isError: true, content: [{ type: 'text', text: 'Voiceover API did not return audio_url.' }] };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const downloadRes = await fetch(remoteAudioUrl, {
|
|
1373
|
+
method: 'GET',
|
|
1374
|
+
headers: { 'Authorization': `Bearer ${MACHINE_API_KEY}` },
|
|
1375
|
+
});
|
|
1376
|
+
if (!downloadRes.ok) {
|
|
1377
|
+
return {
|
|
1378
|
+
isError: true,
|
|
1379
|
+
content: [{ type: 'text', text: `Failed to download synthesized audio (${downloadRes.status})` }],
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const fileBuffer = Buffer.from(await downloadRes.arrayBuffer());
|
|
1384
|
+
mkdirSync(VOICEOVER_LOCAL_DIR, { recursive: true });
|
|
1385
|
+
const audioExt = inferAudioExtension(remoteAudioUrl, data.format ?? normalizedFormat);
|
|
1386
|
+
const localFileName = `voiceover-${Date.now()}-${randomUUID().slice(0, 8)}${audioExt}`;
|
|
1387
|
+
const localAudioPath = path.join(VOICEOVER_LOCAL_DIR, localFileName);
|
|
1388
|
+
writeFileSync(localAudioPath, fileBuffer);
|
|
1389
|
+
|
|
1390
|
+
return {
|
|
1391
|
+
content: [{
|
|
1392
|
+
type: 'text',
|
|
1393
|
+
text:
|
|
1394
|
+
`Voiceover generated.\n` +
|
|
1395
|
+
`workspace_id=${data.workspace_id ?? targetWorkspaceId}\n` +
|
|
1396
|
+
`local_audio_path=${localAudioPath}\n` +
|
|
1397
|
+
`duration_ms=${data.duration_ms ?? 'unknown'}\n` +
|
|
1398
|
+
`sample_rate=${data.sample_rate ?? 'unknown'}\n` +
|
|
1399
|
+
`format=${data.format ?? normalizedFormat}\n` +
|
|
1400
|
+
`size=${formatBytes(fileBuffer.length)}`,
|
|
1401
|
+
}],
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
);
|
|
1405
|
+
|
|
1406
|
+
// ── compose_video ───────────────────────────────────────────────────────────────
|
|
1407
|
+
server.tool('compose_video',
|
|
1408
|
+
'Compose a final short video by muxing audio onto a base video, optionally concatenating an outro, and transcoding to platform spec.',
|
|
1409
|
+
{
|
|
1410
|
+
video_path: z.string().describe('Base silent video path. Relative paths resolve from the current workspace.'),
|
|
1411
|
+
audio_segments: z.array(z.object({
|
|
1412
|
+
audio_path: z.string().describe('Audio file path for one narration segment.'),
|
|
1413
|
+
start_ms: z.union([z.number(), z.string()]).optional().describe('Segment start offset in milliseconds.'),
|
|
1414
|
+
phase: z.string().optional().describe('Optional phase id. Used with events_log to derive start time.'),
|
|
1415
|
+
})).describe('Ordered or unordered narration audio segments.'),
|
|
1416
|
+
events_log: z.array(z.any()).optional().describe('Optional recorder event log. Used to resolve segment start time by phase.'),
|
|
1417
|
+
outro_path: z.string().optional().describe('Optional outro mp4 path. If omitted, uses ~/.lightcone/assets/outros/default.mp4 when present.'),
|
|
1418
|
+
target: z.enum(['short_video_cn', 'douyin', 'xhs']).optional().describe('Transcode target profile. Defaults to short_video_cn.'),
|
|
1419
|
+
},
|
|
1420
|
+
async ({ video_path, audio_segments, events_log, outro_path, target }) => {
|
|
1421
|
+
const composeInput = { video_path, audio_segments, events_log, outro_path, target };
|
|
1422
|
+
try {
|
|
1423
|
+
const result = await runMandatoryLocalTool({
|
|
1424
|
+
toolName: 'compose_video',
|
|
1425
|
+
toolInput: composeInput,
|
|
1426
|
+
executor: async (checkedInput) => {
|
|
1427
|
+
const videoPath = normalizeComposePath(checkedInput.video_path, 'video_path');
|
|
1428
|
+
const audioSegments = normalizeComposeAudioSegments(checkedInput.audio_segments);
|
|
1429
|
+
const eventsLog = Array.isArray(checkedInput.events_log) ? checkedInput.events_log : [];
|
|
1430
|
+
const targetProfile = normalizeComposeTarget(checkedInput.target);
|
|
1431
|
+
const requestedOutroPath = String(checkedInput.outro_path ?? '').trim();
|
|
1432
|
+
|
|
1433
|
+
let resolvedOutroPath = null;
|
|
1434
|
+
if (requestedOutroPath) {
|
|
1435
|
+
resolvedOutroPath = path.resolve(WORKSPACE_DIR, requestedOutroPath);
|
|
1436
|
+
if (!existsSync(resolvedOutroPath)) {
|
|
1437
|
+
throw new Error(`outro_path not found: ${resolvedOutroPath}`);
|
|
1438
|
+
}
|
|
1439
|
+
} else if (existsSync(DEFAULT_OUTRO_PATH)) {
|
|
1440
|
+
resolvedOutroPath = DEFAULT_OUTRO_PATH;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
mkdirSync(VIDEO_COMPOSE_LOCAL_DIR, { recursive: true });
|
|
1444
|
+
const runId = `${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
1445
|
+
const muxedPath = path.join(VIDEO_COMPOSE_LOCAL_DIR, `compose-${runId}.muxed.mp4`);
|
|
1446
|
+
const concatPath = path.join(VIDEO_COMPOSE_LOCAL_DIR, `compose-${runId}.concat.mp4`);
|
|
1447
|
+
const finalPath = path.join(VIDEO_COMPOSE_LOCAL_DIR, `compose-${runId}.final.mp4`);
|
|
1448
|
+
const intermediates = [muxedPath, concatPath];
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
await muxAudioToVideo({
|
|
1452
|
+
video_path: videoPath,
|
|
1453
|
+
audio_segments: audioSegments,
|
|
1454
|
+
events_log: eventsLog,
|
|
1455
|
+
output: muxedPath,
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
let composedPath = muxedPath;
|
|
1459
|
+
if (resolvedOutroPath) {
|
|
1460
|
+
await concatVideos({
|
|
1461
|
+
inputs: [muxedPath, resolvedOutroPath],
|
|
1462
|
+
output: concatPath,
|
|
1463
|
+
});
|
|
1464
|
+
composedPath = concatPath;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
await transcodeForPlatform({
|
|
1468
|
+
input: composedPath,
|
|
1469
|
+
output: finalPath,
|
|
1470
|
+
target: targetProfile,
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
const durationMs = await probeDurationMs(finalPath);
|
|
1474
|
+
cleanupLocalFiles(intermediates);
|
|
1475
|
+
return {
|
|
1476
|
+
finalVideoPath: finalPath,
|
|
1477
|
+
durationMs,
|
|
1478
|
+
outroPath: resolvedOutroPath,
|
|
1479
|
+
target: targetProfile,
|
|
1480
|
+
};
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
cleanupLocalFiles([...intermediates, finalPath]);
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
const outroText = result.outroPath ? result.outroPath : 'skipped';
|
|
1489
|
+
return {
|
|
1490
|
+
content: [{
|
|
1491
|
+
type: 'text',
|
|
1492
|
+
text:
|
|
1493
|
+
`Video composed.\n` +
|
|
1494
|
+
`final_video_path=${result.finalVideoPath}\n` +
|
|
1495
|
+
`duration_ms=${result.durationMs}\n` +
|
|
1496
|
+
`target=${result.target}\n` +
|
|
1497
|
+
`outro=${outroText}`,
|
|
1498
|
+
}],
|
|
1499
|
+
};
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
);
|
|
1505
|
+
|
|
1506
|
+
// ── register_data_source ───────────────────────────────────────────────────────
|
|
1507
|
+
server.tool('register_data_source',
|
|
1508
|
+
'Register a workspace data source without binding credential yet. Returns a one-time secure auth URL (10-minute expiry) that should be sent to the user via IM.',
|
|
1509
|
+
{
|
|
1510
|
+
workspace_id: z.string().optional().describe('Target workspace id. Defaults to current workspace context if omitted.'),
|
|
1511
|
+
display_name: z.string().describe('Human-readable data source name.'),
|
|
1512
|
+
source_type: z.string().describe('Data source type, e.g. "mysql", "postgresql", "api", "csv", "rss", "google_sheets".'),
|
|
1513
|
+
schema_hint: z.any().optional().describe('Optional schema hint JSON for downstream query planning.'),
|
|
1514
|
+
},
|
|
1515
|
+
async ({ workspace_id, display_name, source_type, schema_hint }) => {
|
|
1516
|
+
const targetWorkspaceId = (workspace_id ?? currentWorkspaceId ?? WORKSPACE_ID ?? '').trim();
|
|
1517
|
+
if (!targetWorkspaceId) {
|
|
1518
|
+
return { isError: true, content: [{ type: 'text', text: 'workspace_id is required (no current workspace context).' }] };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const body = {
|
|
1522
|
+
workspace_id: targetWorkspaceId,
|
|
1523
|
+
display_name,
|
|
1524
|
+
source_type,
|
|
1525
|
+
};
|
|
1526
|
+
if (schema_hint !== undefined) body.schema_hint = schema_hint;
|
|
1527
|
+
|
|
1528
|
+
const data = await api('POST', '/api/data-sources', body);
|
|
1529
|
+
return {
|
|
1530
|
+
content: [{
|
|
1531
|
+
type: 'text',
|
|
1532
|
+
text:
|
|
1533
|
+
`Data source registered.\n` +
|
|
1534
|
+
`workspace_id=${data.workspace_id ?? targetWorkspaceId}\n` +
|
|
1535
|
+
`data_source_id=${data.data_source_id}\n` +
|
|
1536
|
+
`source_type=${data.source_type ?? source_type}\n` +
|
|
1537
|
+
`expires_at=${data.expires_at ?? 'unknown'}\n` +
|
|
1538
|
+
`secure_auth_url=${data.secure_auth_url}\n\n` +
|
|
1539
|
+
`Send secure_auth_url to the user in IM so they can complete credential binding securely.`,
|
|
1540
|
+
}],
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
);
|
|
1544
|
+
|
|
1120
1545
|
// ── request_approval ──────────────────────────────────────────────────────────
|
|
1121
1546
|
server.tool('request_approval',
|
|
1122
1547
|
'Request human approval before executing a sensitive platform action (posting, sending, publishing). Returns an action_id. After the human approves, call execute_approved_action with that ID.',
|
package/src/drivers/claude.js
CHANGED
|
@@ -5,7 +5,7 @@ const t = (name) => `mcp__chat__${name}`;
|
|
|
5
5
|
// stable so server-built and daemon-built system prompts can both target it.
|
|
6
6
|
export const WORKSPACE_CONTEXT_PLACEHOLDER = '__WORKSPACE_CONTEXT_BLOCK__';
|
|
7
7
|
|
|
8
|
-
const BASE_PROMPT = (displayName, name, description, agentId, feishuBotName) => `\
|
|
8
|
+
const BASE_PROMPT = (displayName, name, description, agentId, feishuBotName, workspaceDir = '', workspaceSharedDir = '') => `\
|
|
9
9
|
You are "${displayName || name}", an AI agent in lightcone — a collaborative platform for human-AI collaboration.
|
|
10
10
|
${feishuBotName ? `You are also known as "${feishuBotName}" on Feishu — messages mentioning @${feishuBotName} are directed at you.\n` : ''}\
|
|
11
11
|
|
|
@@ -13,6 +13,13 @@ ${feishuBotName ? `You are also known as "${feishuBotName}" on Feishu — messag
|
|
|
13
13
|
|
|
14
14
|
Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.
|
|
15
15
|
|
|
16
|
+
## Runtime paths
|
|
17
|
+
|
|
18
|
+
- Personal workspace absolute path: ${workspaceDir || '(not provided)'}
|
|
19
|
+
- Shared workspace absolute path: ${workspaceSharedDir || '(not provided)'}
|
|
20
|
+
|
|
21
|
+
Treat these as authoritative. Do not invent substitute roots such as /home/agent/workspace unless they exactly match the paths above.
|
|
22
|
+
|
|
16
23
|
${WORKSPACE_CONTEXT_PLACEHOLDER}
|
|
17
24
|
|
|
18
25
|
## Communication — MCP tools ONLY
|
|
@@ -350,9 +357,9 @@ export function buildSystemPrompt(config, agentId, skills) {
|
|
|
350
357
|
if (typeof config?.systemPrompt === 'string' && config.systemPrompt.trim()) {
|
|
351
358
|
return config.systemPrompt;
|
|
352
359
|
}
|
|
353
|
-
const { name, displayName, description, feishuBotName, rolePrompt } = config;
|
|
360
|
+
const { name, displayName, description, feishuBotName, rolePrompt, workspaceDir, workspaceSharedDir } = config;
|
|
354
361
|
|
|
355
|
-
const base = BASE_PROMPT(displayName, name, description, agentId, feishuBotName);
|
|
362
|
+
const base = BASE_PROMPT(displayName, name, description, agentId, feishuBotName, workspaceDir, workspaceSharedDir);
|
|
356
363
|
|
|
357
364
|
const roleSection = rolePrompt
|
|
358
365
|
? `\n\n## Your role in this workspace\n\n${rolePrompt}`
|
package/src/mcp-config.js
CHANGED
|
@@ -58,7 +58,8 @@ function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceI
|
|
|
58
58
|
return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
|
|
59
59
|
}
|
|
60
60
|
if (
|
|
61
|
-
serverKey === '
|
|
61
|
+
serverKey === 'mysql'
|
|
62
|
+
|| serverKey === 'publisher'
|
|
62
63
|
|| serverKey === 'platform'
|
|
63
64
|
|| serverKey === 'research-fetch'
|
|
64
65
|
|| serverKey === 'market-data-query'
|
|
@@ -68,6 +69,7 @@ function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceI
|
|
|
68
69
|
|| serverKey === 'compliance-check'
|
|
69
70
|
|| serverKey === 'portfolio-read'
|
|
70
71
|
|| serverKey === 'portfolio-analysis'
|
|
72
|
+
|| serverKey === 'video-narration-planner'
|
|
71
73
|
) {
|
|
72
74
|
return {
|
|
73
75
|
SERVER_URL: serverUrl,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path, { extname } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
export const WORKSPACE_BINARY_MIME = {
|
|
5
|
+
'.png': 'image/png',
|
|
6
|
+
'.jpg': 'image/jpeg',
|
|
7
|
+
'.jpeg': 'image/jpeg',
|
|
8
|
+
'.webp': 'image/webp',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.pdf': 'application/pdf',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const VIDEO_EXT = '.mp4';
|
|
14
|
+
export const VIDEO_MIME = 'video/mp4';
|
|
15
|
+
|
|
16
|
+
export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
|
|
17
|
+
const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
|
|
18
|
+
const ext = extname(target).toLowerCase();
|
|
19
|
+
const filename = path.basename(target || localPath);
|
|
20
|
+
if (ext === VIDEO_EXT) {
|
|
21
|
+
return {
|
|
22
|
+
mode: 'upload-video',
|
|
23
|
+
ext,
|
|
24
|
+
filename,
|
|
25
|
+
mime: VIDEO_MIME,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
mode: 'workspace-memory',
|
|
30
|
+
ext,
|
|
31
|
+
filename,
|
|
32
|
+
mime: WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function writeLocalFileToWorkspace({
|
|
37
|
+
localPath,
|
|
38
|
+
workspacePath,
|
|
39
|
+
workspaceId,
|
|
40
|
+
readFileSyncFn = readFileSync,
|
|
41
|
+
uploadWorkspaceMemory,
|
|
42
|
+
uploadVideo,
|
|
43
|
+
}) {
|
|
44
|
+
const plan = resolveWorkspaceFileUploadPlan({ localPath, workspacePath });
|
|
45
|
+
if (plan.mode === 'upload-video') {
|
|
46
|
+
return uploadVideo({
|
|
47
|
+
localPath,
|
|
48
|
+
workspacePath,
|
|
49
|
+
workspaceId,
|
|
50
|
+
filename: plan.filename,
|
|
51
|
+
mime: plan.mime,
|
|
52
|
+
mode: plan.mode,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const buf = readFileSyncFn(localPath);
|
|
57
|
+
const content = `data:${plan.mime};base64,${buf.toString('base64')}`;
|
|
58
|
+
await uploadWorkspaceMemory({
|
|
59
|
+
workspacePath,
|
|
60
|
+
workspaceId,
|
|
61
|
+
content,
|
|
62
|
+
mime: plan.mime,
|
|
63
|
+
bytes: buf.length,
|
|
64
|
+
mode: plan.mode,
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
mode: plan.mode,
|
|
68
|
+
mime: plan.mime,
|
|
69
|
+
bytes: buf.length,
|
|
70
|
+
};
|
|
71
|
+
}
|