@xiaohhhh1/canvas-agent 0.4.46 → 0.4.47
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.
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
/** Flow C 脚本分段的唯一上限和自动降级顺序,MCP 与调度器必须共用。 */
|
|
2
2
|
export declare const FLOW_C_SCRIPT_CHUNK_SIZES: readonly [30, 15, 10];
|
|
3
3
|
export declare const FLOW_C_SCRIPT_CHUNK_MAX: 30;
|
|
4
|
+
/**
|
|
5
|
+
* 长视频每条都包含总脚本和 2/3 套完整分镜,不能沿用短脚本的 30 条输出量。
|
|
6
|
+
* 小段回传既能更早显示进度,也避免单个 Codex turn 因输出过大而被中断。
|
|
7
|
+
*/
|
|
8
|
+
export declare function flowCScriptChunkSizes(durationSeconds: 10 | 20 | 30): readonly [30, 15, 10] | readonly [2, 1] | readonly [1];
|
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
/** Flow C 脚本分段的唯一上限和自动降级顺序,MCP 与调度器必须共用。 */
|
|
2
2
|
export const FLOW_C_SCRIPT_CHUNK_SIZES = [30, 15, 10];
|
|
3
3
|
export const FLOW_C_SCRIPT_CHUNK_MAX = FLOW_C_SCRIPT_CHUNK_SIZES[0];
|
|
4
|
+
/**
|
|
5
|
+
* 长视频每条都包含总脚本和 2/3 套完整分镜,不能沿用短脚本的 30 条输出量。
|
|
6
|
+
* 小段回传既能更早显示进度,也避免单个 Codex turn 因输出过大而被中断。
|
|
7
|
+
*/
|
|
8
|
+
export function flowCScriptChunkSizes(durationSeconds) {
|
|
9
|
+
if (durationSeconds === 20)
|
|
10
|
+
return [2, 1];
|
|
11
|
+
if (durationSeconds === 30)
|
|
12
|
+
return [1];
|
|
13
|
+
return FLOW_C_SCRIPT_CHUNK_SIZES;
|
|
14
|
+
}
|
package/dist/workflow/manager.js
CHANGED
|
@@ -9,7 +9,7 @@ import { restartCodexApp, runCodexTurn, startCodexThread } from "../agent/codex.
|
|
|
9
9
|
import { CONFIG_DIR, ensureSiteWorkspace } from "../config.js";
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
11
|
import { windowsPowerShellExecutable } from "../utils/windows.js";
|
|
12
|
-
import { FLOW_C_SCRIPT_CHUNK_MAX,
|
|
12
|
+
import { FLOW_C_SCRIPT_CHUNK_MAX, flowCScriptChunkSizes } from "./constants.js";
|
|
13
13
|
import { flowCScriptOutputSchema, parseFlowCScriptOutput } from "./script-output.js";
|
|
14
14
|
const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
|
|
15
15
|
export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
|
|
@@ -59,6 +59,9 @@ export class WorkflowManager {
|
|
|
59
59
|
const id = workflowId(idValue, "脚本交接 ID");
|
|
60
60
|
const record = this.scriptRecord(id);
|
|
61
61
|
record.status = "queued";
|
|
62
|
+
// A failed manual attempt may leave a very large or interrupted thread.
|
|
63
|
+
// Retrying in a fresh thread avoids carrying that damaged context forward.
|
|
64
|
+
delete record.threadId;
|
|
62
65
|
record.priorityAt = now();
|
|
63
66
|
record.message = "正在重新连接本机 Codex";
|
|
64
67
|
record.updatedAt = now();
|
|
@@ -232,11 +235,13 @@ export class WorkflowManager {
|
|
|
232
235
|
record.threadId = String(thread.id || "");
|
|
233
236
|
this.save();
|
|
234
237
|
}
|
|
238
|
+
const durationSeconds = Number(task.duration_seconds || 10);
|
|
239
|
+
const chunkSizes = flowCScriptChunkSizes(durationSeconds);
|
|
235
240
|
while (record.receivedOrdinals.length < task.requested_count) {
|
|
236
241
|
let progressed = false;
|
|
237
242
|
let lastRange = "";
|
|
238
243
|
let lastError = "";
|
|
239
|
-
for (const [attemptIndex, chunkSize] of
|
|
244
|
+
for (const [attemptIndex, chunkSize] of chunkSizes.entries()) {
|
|
240
245
|
const missing = missingOrdinals(task.requested_count, record.receivedOrdinals).slice(0, chunkSize);
|
|
241
246
|
if (!missing.length) {
|
|
242
247
|
progressed = true;
|
|
@@ -248,7 +253,7 @@ export class WorkflowManager {
|
|
|
248
253
|
record.message = `本机 Codex 正在写第 ${missing[0]}–${missing.at(-1)} 条(总计 ${task.requested_count} 条)`;
|
|
249
254
|
record.updatedAt = now();
|
|
250
255
|
this.save();
|
|
251
|
-
const result = await runCodexScriptWithTimeout(scriptChunkPrompt(id, task, missing), this.emit, { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", outputSchema: flowCScriptOutputSchema(
|
|
256
|
+
const result = await runCodexScriptWithTimeout(scriptChunkPrompt(id, task, missing), this.emit, { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", outputSchema: flowCScriptOutputSchema(durationSeconds, missing.length), onThread: (threadId) => { record.threadId = threadId; this.save(); } });
|
|
252
257
|
await this.scriptTask(id);
|
|
253
258
|
progressed = record.receivedOrdinals.length > before;
|
|
254
259
|
if (!progressed && result.ok && result.text) {
|
|
@@ -266,8 +271,8 @@ export class WorkflowManager {
|
|
|
266
271
|
// Keep provider/client details out of the browser-facing state. The
|
|
267
272
|
// underlying Codex runner already records the technical error locally.
|
|
268
273
|
lastError ||= result.ok ? "Codex 未返回可用的结构化脚本" : "Codex 本轮执行失败";
|
|
269
|
-
if (attemptIndex <
|
|
270
|
-
const nextSize =
|
|
274
|
+
if (attemptIndex < chunkSizes.length - 1) {
|
|
275
|
+
const nextSize = chunkSizes[attemptIndex + 1];
|
|
271
276
|
record.message = `第 ${lastRange} 条未成功回传,正在换新会话并缩小为每段 ${nextSize} 条重试`;
|
|
272
277
|
const thread = await startCodexThread(this.emit, workspace.workspacePath, "full");
|
|
273
278
|
record.threadId = String(thread.id || "");
|
|
@@ -275,7 +280,7 @@ export class WorkflowManager {
|
|
|
275
280
|
}
|
|
276
281
|
}
|
|
277
282
|
if (!progressed)
|
|
278
|
-
throw new Error(`本机 Codex 已自动换会话并降级到每段
|
|
283
|
+
throw new Error(`本机 Codex 已自动换会话并降级到每段 ${chunkSizes.at(-1)} 条,仍未回传第 ${lastRange} 条${lastError ? `(${lastError})` : ""},请点击重试`);
|
|
279
284
|
}
|
|
280
285
|
const finalTask = await this.scriptTask(id);
|
|
281
286
|
if (finalTask.status === "ready" && record.receivedOrdinals.length === task.requested_count) {
|
|
@@ -8,10 +8,6 @@ function continuitySchema(frameField) {
|
|
|
8
8
|
}
|
|
9
9
|
function segmentSchema() {
|
|
10
10
|
return object({
|
|
11
|
-
// The central Flow C API receives each long-video segment as an
|
|
12
|
-
// independently executable script. Do not omit this just because the
|
|
13
|
-
// richer storyboard fields below are also present.
|
|
14
|
-
script: { type: "string", minLength: 40 },
|
|
15
11
|
continuityMode: { type: "string", enum: ["reset", "continue"] },
|
|
16
12
|
continuity: continuitySchema("previousEndingFrame"),
|
|
17
13
|
endingState: continuitySchema("endingFrame"),
|
|
@@ -52,5 +48,33 @@ export function parseFlowCScriptOutput(value, expectedOrdinals) {
|
|
|
52
48
|
const actual = parsed.jobs.map((job) => Number(job?.ordinal));
|
|
53
49
|
if (actual.some((ordinal, index) => ordinal !== expectedOrdinals[index]))
|
|
54
50
|
throw new Error("Codex 返回的 ordinal 与当前分段不一致");
|
|
55
|
-
return parsed.jobs;
|
|
51
|
+
return parsed.jobs.map(canonicalizeSegmentContinuity);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 将后一段的起点锁定为前一段的终点。模型常会用语义相同但措辞不同的
|
|
55
|
+
* 文字复述角色/场景,中心严格校验时会误判不连续;这里复制模型自己写的
|
|
56
|
+
* endingState,不发明新内容,同时保证生成阶段真正无缝承接。
|
|
57
|
+
*/
|
|
58
|
+
function canonicalizeSegmentContinuity(jobValue) {
|
|
59
|
+
const job = recordOf(jobValue);
|
|
60
|
+
if (!job || !Array.isArray(job.segments))
|
|
61
|
+
return jobValue;
|
|
62
|
+
const segments = job.segments.map((segmentValue, index, source) => {
|
|
63
|
+
const segment = recordOf(segmentValue);
|
|
64
|
+
if (!segment)
|
|
65
|
+
return segmentValue;
|
|
66
|
+
if (index === 0)
|
|
67
|
+
return { ...segment, continuityMode: "reset" };
|
|
68
|
+
const previous = recordOf(source[index - 1]);
|
|
69
|
+
const endingState = recordOf(previous?.endingState);
|
|
70
|
+
if (!endingState)
|
|
71
|
+
return { ...segment, continuityMode: "continue" };
|
|
72
|
+
const continuity = Object.fromEntries(continuityFields.map((field) => [field, endingState[field]]));
|
|
73
|
+
continuity.previousEndingFrame = endingState.endingFrame;
|
|
74
|
+
return { ...segment, continuityMode: "continue", continuity };
|
|
75
|
+
});
|
|
76
|
+
return { ...job, segments };
|
|
77
|
+
}
|
|
78
|
+
function recordOf(value) {
|
|
79
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
56
80
|
}
|