@xiaohhhh1/canvas-agent 0.4.45 → 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
+ }
@@ -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, FLOW_C_SCRIPT_CHUNK_SIZES } from "./constants.js";
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 FLOW_C_SCRIPT_CHUNK_SIZES.entries()) {
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(Number(task.duration_seconds || 10), missing.length), onThread: (threadId) => { record.threadId = threadId; this.save(); } });
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 < FLOW_C_SCRIPT_CHUNK_SIZES.length - 1) {
270
- const nextSize = FLOW_C_SCRIPT_CHUNK_SIZES[attemptIndex + 1];
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 已自动换会话并降级到每段 10 条,仍未回传第 ${lastRange} 条${lastError ? `(${lastError})` : ""},请点击重试`);
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) {
@@ -48,5 +48,33 @@ export function parseFlowCScriptOutput(value, expectedOrdinals) {
48
48
  const actual = parsed.jobs.map((job) => Number(job?.ordinal));
49
49
  if (actual.some((ordinal, index) => ordinal !== expectedOrdinals[index]))
50
50
  throw new Error("Codex 返回的 ordinal 与当前分段不一致");
51
- 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;
52
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.45",
3
+ "version": "0.4.47",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",