@lightcone-ai/daemon 0.14.13 → 0.14.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/package.json +1 -1
- package/src/chat-bridge.js +24 -0
- package/src/publish-job-runner.js +97 -10
- package/src/submit-to-library-tool.js +39 -0
package/package.json
CHANGED
package/src/chat-bridge.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
VIDEO_MIME,
|
|
18
18
|
writeLocalFileToWorkspace,
|
|
19
19
|
} from './workspace-file-upload.js';
|
|
20
|
+
import { runSubmitToLibraryTool } from './submit-to-library-tool.js';
|
|
20
21
|
import { isLeaseInvalidated, clearInvalidatedLease } from './governance-state.js';
|
|
21
22
|
import { classifyLeaseWindow } from './lease-window.js';
|
|
22
23
|
|
|
@@ -197,6 +198,7 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
197
198
|
request_credential_auth: 'mandatory',
|
|
198
199
|
generate_voiceover: 'mandatory',
|
|
199
200
|
compose_video: 'mandatory',
|
|
201
|
+
submit_to_library: 'mandatory',
|
|
200
202
|
register_data_source: 'mandatory',
|
|
201
203
|
bind_workspace_scenario: 'mandatory',
|
|
202
204
|
create_workspace: 'mandatory',
|
|
@@ -254,6 +256,7 @@ const CACHE_INVALIDATION_TOOLS = new Set([
|
|
|
254
256
|
'supersede_goal_field',
|
|
255
257
|
'request_credential_auth',
|
|
256
258
|
'register_data_source',
|
|
259
|
+
'submit_to_library',
|
|
257
260
|
'bind_workspace_scenario',
|
|
258
261
|
'create_workspace',
|
|
259
262
|
'rename_workspace',
|
|
@@ -331,6 +334,7 @@ function inferToolForApi(method, apiPath, body) {
|
|
|
331
334
|
if (method === 'POST' && cleanPath === '/goal-fields/supersede') return 'supersede_goal_field';
|
|
332
335
|
if (method === 'POST' && cleanPath === '/credential-auth/request') return 'request_credential_auth';
|
|
333
336
|
if (method === 'POST' && cleanPath === '/tts/voiceover') return 'generate_voiceover';
|
|
337
|
+
if (method === 'POST' && cleanPath === '/content-library/submit') return 'submit_to_library';
|
|
334
338
|
if (method === 'POST' && cleanPath === '/api/data-sources') return 'register_data_source';
|
|
335
339
|
if (method === 'POST' && cleanPath === '/orchestrate/decision') return 'write_governance_decision';
|
|
336
340
|
if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
|
|
@@ -1503,6 +1507,26 @@ server.tool('compose_video',
|
|
|
1503
1507
|
}
|
|
1504
1508
|
);
|
|
1505
1509
|
|
|
1510
|
+
// ── submit_to_library ──────────────────────────────────────────────────────────
|
|
1511
|
+
server.tool('submit_to_library',
|
|
1512
|
+
'把已生成的视频成片归档进内容库(content_video_draft entry)。调用前 mp4 必须已经通过 write_workspace_file 落到 workspace 的 artifacts/ 路径。归档后内容库会出现一张新卡片,含视频预览 + 元数据 + 后续支持发布/回采链路。',
|
|
1513
|
+
{
|
|
1514
|
+
video_path: z.string().describe('Workspace-relative video path, e.g. "artifacts/foo.mp4"'),
|
|
1515
|
+
title: z.string().optional().describe('Library card 标题;若省略由系统从 understanding 推导'),
|
|
1516
|
+
summary: z.string().optional().describe('一句话摘要'),
|
|
1517
|
+
source_url: z.string().optional().describe('原始 URL(如有)'),
|
|
1518
|
+
target_platform: z.string().optional().describe('目标发布平台,如 xhs / douyin'),
|
|
1519
|
+
metadata: z.record(z.any()).optional().describe('其它 metadata(brand_voice / persona / account / goal_state 等)'),
|
|
1520
|
+
understanding: z.record(z.any()).optional().describe('analyze_page 输出'),
|
|
1521
|
+
plan: z.record(z.any()).optional().describe('plan_video / detail_sections 输出'),
|
|
1522
|
+
},
|
|
1523
|
+
async (args) => runSubmitToLibraryTool({
|
|
1524
|
+
args,
|
|
1525
|
+
currentWorkspaceId,
|
|
1526
|
+
apiFn: api,
|
|
1527
|
+
})
|
|
1528
|
+
);
|
|
1529
|
+
|
|
1506
1530
|
// ── register_data_source ───────────────────────────────────────────────────────
|
|
1507
1531
|
server.tool('register_data_source',
|
|
1508
1532
|
'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.',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync } from 'fs';
|
|
1
|
+
import { existsSync, statSync, realpathSync, mkdirSync, writeFileSync, renameSync, rmSync, readdirSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getSession, closeSession } from '../mcp-servers/publisher/chrome-pool.js';
|
|
4
4
|
import { XhsAdapter } from '../mcp-servers/publisher/adapters/xhs.js';
|
|
@@ -26,6 +26,7 @@ const DEFAULT_MEDIA_LIMITS = {
|
|
|
26
26
|
douyin: { maxImages: 35, imageExts: ['.jpg', '.jpeg', '.png', '.webp'], videoExts: ['.mp4', '.mov', '.webm'] },
|
|
27
27
|
kuaishou: { maxImages: 12, imageExts: ['.jpg', '.jpeg', '.png'], videoExts: ['.mp4', '.mov'] },
|
|
28
28
|
};
|
|
29
|
+
const PARTIAL_FILE_RE = /\.partial-\d+-[a-z0-9]+$/;
|
|
29
30
|
|
|
30
31
|
function normalizeText(value) {
|
|
31
32
|
const normalized = String(value ?? '').trim();
|
|
@@ -130,6 +131,82 @@ function decodeWorkspaceContent(content) {
|
|
|
130
131
|
return Buffer.from(decodeURIComponent(body), 'utf8');
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function buildPartialFilePath(localPath) {
|
|
135
|
+
return `${localPath}.partial-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeExpectedSizeBytes(value) {
|
|
139
|
+
if (value == null) return null;
|
|
140
|
+
const size = Number(value);
|
|
141
|
+
if (!Number.isInteger(size) || size < 0) return null;
|
|
142
|
+
return size;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isWorkspaceCacheValid(localPath, expected = {}) {
|
|
146
|
+
if (!existsSync(localPath)) return false;
|
|
147
|
+
let stat = null;
|
|
148
|
+
try {
|
|
149
|
+
stat = statSync(localPath);
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (!stat.isFile()) return false;
|
|
154
|
+
const expectedSize = normalizeExpectedSizeBytes(expected?.size_bytes);
|
|
155
|
+
if (expectedSize != null && stat.size !== expectedSize) return false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function writeFileAtomically(localPath, data, {
|
|
160
|
+
mkdirRecursive = mkdirSync,
|
|
161
|
+
writeFile = writeFileSync,
|
|
162
|
+
renameFile = renameSync,
|
|
163
|
+
removeFile = rmSync,
|
|
164
|
+
} = {}) {
|
|
165
|
+
mkdirRecursive(path.dirname(localPath), { recursive: true });
|
|
166
|
+
const tmpPath = buildPartialFilePath(localPath);
|
|
167
|
+
try {
|
|
168
|
+
writeFile(tmpPath, data);
|
|
169
|
+
renameFile(tmpPath, localPath);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
try { removeFile(tmpPath, { force: true }); } catch {}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return localPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function removePartialFilesRecursively(rootDir) {
|
|
178
|
+
const stack = [rootDir];
|
|
179
|
+
while (stack.length > 0) {
|
|
180
|
+
const current = stack.pop();
|
|
181
|
+
let entries = null;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const entryPath = path.join(current, entry.name);
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
stack.push(entryPath);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!entry.isFile()) continue;
|
|
194
|
+
if (!PARTIAL_FILE_RE.test(entry.name)) continue;
|
|
195
|
+
try { rmSync(entryPath, { force: true }); } catch {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function cleanupWorkspacePartialFiles(workspaceRootDir) {
|
|
201
|
+
const normalizedRoot = String(workspaceRootDir ?? '').trim();
|
|
202
|
+
if (!normalizedRoot) return;
|
|
203
|
+
const scanRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(normalizedRoot, dir));
|
|
204
|
+
for (const root of scanRoots) {
|
|
205
|
+
if (!existsSync(root)) continue;
|
|
206
|
+
removePartialFilesRecursively(root);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
133
210
|
export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, jobId, relPath }) {
|
|
134
211
|
const normalizedJobId = String(jobId ?? '').trim();
|
|
135
212
|
if (!normalizedJobId) {
|
|
@@ -148,7 +225,14 @@ export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, j
|
|
|
148
225
|
return res.json();
|
|
149
226
|
}
|
|
150
227
|
|
|
151
|
-
export async function streamPublishJobVideo({
|
|
228
|
+
export async function streamPublishJobVideo({
|
|
229
|
+
serverUrl,
|
|
230
|
+
machineApiKey,
|
|
231
|
+
jobId,
|
|
232
|
+
videoId,
|
|
233
|
+
localPath,
|
|
234
|
+
writeBinaryFile = writeFileAtomically,
|
|
235
|
+
}) {
|
|
152
236
|
const normalizedJobId = String(jobId ?? '').trim();
|
|
153
237
|
if (!normalizedJobId) {
|
|
154
238
|
throw new Error('publish job id is required to fetch video files');
|
|
@@ -168,8 +252,7 @@ export async function streamPublishJobVideo({ serverUrl, machineApiKey, jobId, v
|
|
|
168
252
|
throw new Error(`publish-jobs video-files GET failed (${res.status}): ${text}`);
|
|
169
253
|
}
|
|
170
254
|
const data = Buffer.from(await res.arrayBuffer());
|
|
171
|
-
|
|
172
|
-
writeFileSync(localPath, data);
|
|
255
|
+
writeBinaryFile(localPath, data);
|
|
173
256
|
return localPath;
|
|
174
257
|
}
|
|
175
258
|
|
|
@@ -208,11 +291,15 @@ export async function materializeWorkspaceMedia({
|
|
|
208
291
|
jobId,
|
|
209
292
|
fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
|
|
210
293
|
streamVideoFile = streamPublishJobVideo,
|
|
294
|
+
writeBinaryFile = writeFileAtomically,
|
|
211
295
|
}) {
|
|
212
|
-
if (!filePath
|
|
296
|
+
if (!filePath) return filePath;
|
|
213
297
|
|
|
214
298
|
const workspacePath = workspacePathFromMediaPath(filePath, workspaceId);
|
|
215
|
-
if (!workspacePath?.workspaceId || !workspacePath.relPath)
|
|
299
|
+
if (!workspacePath?.workspaceId || !workspacePath.relPath) {
|
|
300
|
+
if (existsSync(filePath)) return filePath;
|
|
301
|
+
return filePath;
|
|
302
|
+
}
|
|
216
303
|
|
|
217
304
|
const localPath = path.resolve(workspaceRootDir, workspacePath.relPath);
|
|
218
305
|
const allowedRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(workspaceRootDir, dir));
|
|
@@ -220,14 +307,14 @@ export async function materializeWorkspaceMedia({
|
|
|
220
307
|
throw new Error(`workspace media path is outside allowed workspace directories: ${filePath}`);
|
|
221
308
|
}
|
|
222
309
|
|
|
223
|
-
if (existsSync(localPath)) return localPath;
|
|
224
|
-
|
|
225
310
|
const data = await fetchWorkspaceFile({
|
|
226
311
|
serverUrl,
|
|
227
312
|
machineApiKey,
|
|
228
313
|
jobId,
|
|
229
314
|
relPath: workspacePath.relPath,
|
|
230
315
|
});
|
|
316
|
+
if (isWorkspaceCacheValid(localPath, data)) return localPath;
|
|
317
|
+
|
|
231
318
|
if (data?.videoId) {
|
|
232
319
|
await streamVideoFile({
|
|
233
320
|
serverUrl,
|
|
@@ -237,8 +324,7 @@ export async function materializeWorkspaceMedia({
|
|
|
237
324
|
localPath,
|
|
238
325
|
});
|
|
239
326
|
} else {
|
|
240
|
-
|
|
241
|
-
writeFileSync(localPath, decodeWorkspaceContent(data.content));
|
|
327
|
+
writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
|
|
242
328
|
}
|
|
243
329
|
return localPath;
|
|
244
330
|
}
|
|
@@ -253,6 +339,7 @@ async function materializeMedia({
|
|
|
253
339
|
machineApiKey,
|
|
254
340
|
jobId,
|
|
255
341
|
}) {
|
|
342
|
+
cleanupWorkspacePartialFiles(workspaceRootDir);
|
|
256
343
|
return {
|
|
257
344
|
images: await Promise.all(images.map(filePath => materializeWorkspaceMedia({
|
|
258
345
|
filePath,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function toolText(text) {
|
|
2
|
+
return { content: [{ type: 'text', text }] };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function toolError(text) {
|
|
6
|
+
return { isError: true, content: [{ type: 'text', text }] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildSubmitToLibraryBody(args = {}, workspaceId = '') {
|
|
10
|
+
return {
|
|
11
|
+
...(args ?? {}),
|
|
12
|
+
workspace_id: workspaceId,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runSubmitToLibraryTool({
|
|
17
|
+
args = {},
|
|
18
|
+
currentWorkspaceId = '',
|
|
19
|
+
apiFn,
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (!currentWorkspaceId) {
|
|
22
|
+
return toolError('No workspace context.');
|
|
23
|
+
}
|
|
24
|
+
if (typeof apiFn !== 'function') {
|
|
25
|
+
return toolError('Error: submit_to_library apiFn is required.');
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const data = await apiFn(
|
|
29
|
+
'POST',
|
|
30
|
+
'/content-library/submit',
|
|
31
|
+
buildSubmitToLibraryBody(args, currentWorkspaceId)
|
|
32
|
+
);
|
|
33
|
+
return toolText(
|
|
34
|
+
`Submitted to content library: itemId=${data.itemId}, version=${data.version}, kind=${data.kind ?? 'content_video_draft'}`
|
|
35
|
+
);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return toolError(`Error: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|