@xiaohhhh1/canvas-agent 0.4.47 → 0.4.48
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.
|
@@ -187,6 +187,11 @@ export declare class WorkflowManager {
|
|
|
187
187
|
private pumpScriptQueue;
|
|
188
188
|
private finishDownloadDirectorySelection;
|
|
189
189
|
private runScript;
|
|
190
|
+
/**
|
|
191
|
+
* 中心批量校验失败时逐条补交,避免一条格式异常拖累同轮其他有效脚本。
|
|
192
|
+
* 已接纳 ordinal 由中心持久化,下一轮只会继续缺失项。
|
|
193
|
+
*/
|
|
194
|
+
private submitGeneratedScriptJobs;
|
|
190
195
|
private syncDownloads;
|
|
191
196
|
private syncDownloadRecord;
|
|
192
197
|
private saveDelivery;
|
package/dist/workflow/manager.js
CHANGED
|
@@ -258,7 +258,7 @@ export class WorkflowManager {
|
|
|
258
258
|
progressed = record.receivedOrdinals.length > before;
|
|
259
259
|
if (!progressed && result.ok && result.text) {
|
|
260
260
|
try {
|
|
261
|
-
await this.
|
|
261
|
+
await this.submitGeneratedScriptJobs(id, parseFlowCScriptOutput(result.text, missing));
|
|
262
262
|
await this.scriptTask(id);
|
|
263
263
|
progressed = record.receivedOrdinals.length > before;
|
|
264
264
|
}
|
|
@@ -266,8 +266,17 @@ export class WorkflowManager {
|
|
|
266
266
|
lastError = error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验";
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
-
if (progressed)
|
|
269
|
+
if (progressed) {
|
|
270
|
+
// Each long structured result is large. A fresh thread keeps the
|
|
271
|
+
// next chunk from re-reading prior outputs while ordinals preserve
|
|
272
|
+
// creative rotation and the center keeps accepted work durable.
|
|
273
|
+
if (record.receivedOrdinals.length < task.requested_count) {
|
|
274
|
+
const thread = await startCodexThread(this.emit, workspace.workspacePath, "full");
|
|
275
|
+
record.threadId = String(thread.id || "");
|
|
276
|
+
this.save();
|
|
277
|
+
}
|
|
270
278
|
break;
|
|
279
|
+
}
|
|
271
280
|
// Keep provider/client details out of the browser-facing state. The
|
|
272
281
|
// underlying Codex runner already records the technical error locally.
|
|
273
282
|
lastError ||= result.ok ? "Codex 未返回可用的结构化脚本" : "Codex 本轮执行失败";
|
|
@@ -301,6 +310,33 @@ export class WorkflowManager {
|
|
|
301
310
|
this.runningScripts.delete(id);
|
|
302
311
|
}
|
|
303
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* 中心批量校验失败时逐条补交,避免一条格式异常拖累同轮其他有效脚本。
|
|
315
|
+
* 已接纳 ordinal 由中心持久化,下一轮只会继续缺失项。
|
|
316
|
+
*/
|
|
317
|
+
async submitGeneratedScriptJobs(id, jobs) {
|
|
318
|
+
try {
|
|
319
|
+
await this.submitScriptChunk(id, jobs);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (jobs.length === 1)
|
|
324
|
+
throw error;
|
|
325
|
+
let accepted = 0;
|
|
326
|
+
let lastError = error;
|
|
327
|
+
for (const job of jobs) {
|
|
328
|
+
try {
|
|
329
|
+
await this.submitScriptChunk(id, [job]);
|
|
330
|
+
accepted += 1;
|
|
331
|
+
}
|
|
332
|
+
catch (jobError) {
|
|
333
|
+
lastError = jobError;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!accepted)
|
|
337
|
+
throw lastError;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
304
340
|
async syncDownloads(onlyBatchId) {
|
|
305
341
|
if (!this.state.downloadDirectory)
|
|
306
342
|
return;
|
|
@@ -462,7 +498,7 @@ function scriptChunkPrompt(id, task, ordinals) {
|
|
|
462
498
|
const duration = Number(task.duration_seconds || 10);
|
|
463
499
|
const longVideoRules = duration === 10 ? "" : `
|
|
464
500
|
本任务每条成片为 ${duration} 秒。先创作一份完整连贯的 masterScript,再严格拆成 ${duration / 10} 个可以独立交给视频模型的 10 秒 segments;每一段内部时间都从 0–10 秒重新写,绝不能引用“上一条视频”或写 10–20、20–30 秒这种模型无法理解的时间。
|
|
465
|
-
|
|
501
|
+
每条只需回传一份 masterScript 和完整 segments;不要再重复输出内容相同的顶层 script,Agent 会从 masterScript 自动补齐兼容字段。segments[0].continuityMode 必须是 reset;同人物同场景延续时后续段用 continue,明确换人物或换场景才用 reset。continue 段必须复述并固定人物年龄、外貌、服装、场景陈设、光线、机位基调和商品当前状态。
|
|
466
502
|
每份 masterScript 和每个 segment 都要写同一套详细音色身份:性别、年龄段、音高、音质、语速、口音、停连和说话习惯;情绪随剧情逐段变化,但音色身份不变。仍按短视频快节奏口播,不因时长增加而拖慢。`;
|
|
467
503
|
return `你正在后台处理“抖音小辉跨境工具”Flow C 脚本交接 ${id}。
|
|
468
504
|
完整中心任务已经附在本提示词末尾。只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
|
|
@@ -471,7 +507,7 @@ function scriptChunkPrompt(id, task, ordinals) {
|
|
|
471
507
|
每一个 ordinal 都必须从头到尾创作一份完整、独立的原生带货脚本,画面和口播必须在同一份脚本中一起独立构思。先让它成为一个当地真人在具体生活情境里自然发现、吐槽、试用或验证商品的短内容,再完成带货;禁止写成品牌广告片、棚拍宣传片、电视购物、逐条念卖点或全程完美对镜讲解。独立脚本天然包含独立口播:不得把任何一份口播当作整批公共模板,不得复用完整台词,也不得只换人物、场景或少数词后保留近似口播。音色身份可以为同一人物保持一致,但每条的 Hook、人物行动、证明表达和 CTA 都必须重新写。
|
|
472
508
|
用户没有指定带货方向时,不得随意只用一种泛化形式;必须按任务 instructions 中“已批准的创意方向”逐条做产品适配轮换。轮换必须按每个产品自己的序号连续计算,不能因 30/15/10 条分段、换会话或跨产品边界而从第一个方向重新开始;让同一产品在重复某一方向前优先覆盖其他适配方向。
|
|
473
509
|
工厂风格 A/B 是默认轮换中的演绎带货布景,不是商品来源声明;不得因用户未提供真实工厂资料而跳过,也绝不能写成我们的真实工厂、真实生产流程、真实产地、工厂直销、厂家出货或仓库现货。
|
|
474
|
-
只返回符合当前结构化输出契约的 jobs
|
|
510
|
+
只返回符合当前结构化输出契约的 jobs;10 秒任务每条包含 ordinal、productIndex、sellingFormId 和 script;20/30 秒任务每条包含 ordinal、productIndex、sellingFormId、masterScript 与完整 segments,中心任务示例里的重复顶层 script 由 Agent 自动补齐。无需调用 MCP,Agent 会在本机校验后自动回传。不要创建付费批次,不要调用供应商模型。
|
|
475
511
|
|
|
476
512
|
【中心任务完整 instructions】
|
|
477
513
|
${task.instructions}
|
|
@@ -32,9 +32,10 @@ export function flowCScriptOutputSchema(durationSeconds, count) {
|
|
|
32
32
|
ordinal: { type: "integer", minimum: 1 },
|
|
33
33
|
productIndex: { type: "integer", minimum: 0 },
|
|
34
34
|
sellingFormId: text,
|
|
35
|
-
script: { type: "string", minLength: 40 },
|
|
36
35
|
};
|
|
37
|
-
if (durationSeconds
|
|
36
|
+
if (durationSeconds === 10)
|
|
37
|
+
properties.script = { type: "string", minLength: 40 };
|
|
38
|
+
else {
|
|
38
39
|
properties.masterScript = { type: "string", minLength: 40 };
|
|
39
40
|
properties.segments = { type: "array", minItems: durationSeconds / 10, maxItems: durationSeconds / 10, items: segmentSchema() };
|
|
40
41
|
}
|
|
@@ -43,12 +44,18 @@ export function flowCScriptOutputSchema(durationSeconds, count) {
|
|
|
43
44
|
export function parseFlowCScriptOutput(value, expectedOrdinals) {
|
|
44
45
|
const source = String(value || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
45
46
|
const parsed = JSON.parse(source);
|
|
46
|
-
if (!Array.isArray(parsed.jobs) || parsed.jobs.length
|
|
47
|
-
throw new Error("Codex
|
|
48
|
-
const
|
|
49
|
-
|
|
47
|
+
if (!Array.isArray(parsed.jobs) || !parsed.jobs.length)
|
|
48
|
+
throw new Error("Codex 没有返回脚本");
|
|
49
|
+
const expected = new Set(expectedOrdinals);
|
|
50
|
+
const accepted = new Map();
|
|
51
|
+
for (const job of parsed.jobs) {
|
|
52
|
+
const ordinal = Number(job?.ordinal);
|
|
53
|
+
if (Number.isInteger(ordinal) && expected.has(ordinal) && !accepted.has(ordinal))
|
|
54
|
+
accepted.set(ordinal, job);
|
|
55
|
+
}
|
|
56
|
+
if (!accepted.size)
|
|
50
57
|
throw new Error("Codex 返回的 ordinal 与当前分段不一致");
|
|
51
|
-
return
|
|
58
|
+
return expectedOrdinals.flatMap((ordinal) => accepted.has(ordinal) ? [canonicalizeSegmentContinuity(accepted.get(ordinal))] : []);
|
|
52
59
|
}
|
|
53
60
|
/**
|
|
54
61
|
* 将后一段的起点锁定为前一段的终点。模型常会用语义相同但措辞不同的
|
|
@@ -73,7 +80,23 @@ function canonicalizeSegmentContinuity(jobValue) {
|
|
|
73
80
|
continuity.previousEndingFrame = endingState.endingFrame;
|
|
74
81
|
return { ...segment, continuityMode: "continue", continuity };
|
|
75
82
|
});
|
|
76
|
-
return { ...job, segments };
|
|
83
|
+
return { ...job, script: job.masterScript || job.script, segments: segments.map((segment, index) => withLegacySegmentScript(segment, index, segments.length)) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 正式站可能仍运行只接收 segment.script 的旧协议。脚本正文直接由同一份
|
|
87
|
+
* structured shots/continuity 渲染,既不要求模型重复输出,也不改变新协议内容。
|
|
88
|
+
*/
|
|
89
|
+
function withLegacySegmentScript(segmentValue, segmentIndex, segmentCount) {
|
|
90
|
+
const segment = recordOf(segmentValue);
|
|
91
|
+
const continuity = recordOf(segment?.continuity);
|
|
92
|
+
const endingState = recordOf(segment?.endingState);
|
|
93
|
+
const shots = Array.isArray(segment?.shots) ? segment.shots.map(recordOf).filter((shot) => Boolean(shot)) : [];
|
|
94
|
+
if (!segment || !continuity || !endingState || !shots.length)
|
|
95
|
+
return segmentValue;
|
|
96
|
+
const context = `人物:${continuity.character};服装:${continuity.wardrobe};地点:${continuity.location};光线:${continuity.lighting};商品状态:${continuity.productState};承接动作:${continuity.unfinishedAction};本段目标:${continuity.nextGoal};上一段结尾画面:${continuity.previousEndingFrame}`;
|
|
97
|
+
const ending = `人物:${endingState.character};服装:${endingState.wardrobe};地点:${endingState.location};光线:${endingState.lighting};商品状态:${endingState.productState};未完成动作:${endingState.unfinishedAction};下一段目标:${endingState.nextGoal};本段结尾画面:${endingState.endingFrame}`;
|
|
98
|
+
const timeline = shots.map((shot, index) => `镜头 ${index + 1}|${shot.startSeconds}–${shot.endSeconds} 秒|画面:${shot.visual}|口播:${shot.voiceover}|屏幕字:${shot.onScreenText}|证据:${shot.evidence}`).join("\n");
|
|
99
|
+
return { ...segment, script: `Flow C 独立分段 ${segmentIndex + 1}/${segmentCount}\n局部时轴:0–10 秒;不得引用或绘制总片时间轴。\n本段起始连续性:${context}\n${timeline}\n本段结束状态:${ending}` };
|
|
77
100
|
}
|
|
78
101
|
function recordOf(value) {
|
|
79
102
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|