@xiaohhhh1/canvas-agent 0.4.42 → 0.4.44

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,4 @@
1
+ import { type JsonRecord } from "../utils/value.js";
1
2
  import type { CodexPlanUpdate, CodexRequestParams } from "./codex-protocol.js";
2
3
  import type { AgentEmit, AgentPermissionMode } from "./types.js";
3
4
  /** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
@@ -13,6 +14,7 @@ export declare class CodexAppClient {
13
14
  private pending;
14
15
  private activeTurns;
15
16
  private completedTurns;
17
+ private finalTextByTurn;
16
18
  private pendingDeltas;
17
19
  private plansByTurn;
18
20
  private approvalRequests;
@@ -41,9 +43,11 @@ export declare class CodexAppClient {
41
43
  /** 清理已归档线程的任务计划缓存。 */
42
44
  clearPlanUpdates(threadId: string): void;
43
45
  /** 启动一个 Codex turn 并等待完成通知。 */
44
- startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void): Promise<void>;
46
+ startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void, outputSchema?: JsonRecord): Promise<string>;
45
47
  /** 中断当前正在运行的 Codex turn。 */
46
48
  interruptCurrentTurn(): Promise<boolean>;
49
+ /** 终止失去响应的 app-server,让后续队列可以在新进程继续。 */
50
+ terminate(message?: string): Promise<void>;
47
51
  /** 回复网页端已经确认的 Codex 权限请求。 */
48
52
  resolveApproval(requestId: string, decision: string): boolean;
49
53
  /** 发送 JSON-RPC 请求并保存待处理 Promise。 */
@@ -21,6 +21,7 @@ export class CodexAppClient {
21
21
  pending = new Map();
22
22
  activeTurns = new Map();
23
23
  completedTurns = new Map();
24
+ finalTextByTurn = new Map();
24
25
  pendingDeltas = new Map();
25
26
  plansByTurn = new Map();
26
27
  approvalRequests = new Map();
@@ -92,9 +93,9 @@ export class CodexAppClient {
92
93
  });
93
94
  }
94
95
  /** 启动一个 Codex turn 并等待完成通知。 */
95
- async startTurn(threadId, prompt, images, permissionMode, onTurn) {
96
+ async startTurn(threadId, prompt, images, permissionMode, onTurn, outputSchema) {
96
97
  this.currentThreadId = threadId;
97
- const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode) });
98
+ const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode), ...(outputSchema ? { outputSchema } : {}) });
98
99
  const turnId = turn.id;
99
100
  if (!turnId)
100
101
  throw new Error("Codex app-server 没有返回 turn id");
@@ -105,11 +106,11 @@ export class CodexAppClient {
105
106
  this.completedTurns.delete(turnId);
106
107
  this.currentThreadId = "";
107
108
  this.currentTurnId = "";
108
- if (completed)
109
- throw completed;
110
- return;
109
+ if (completed?.error)
110
+ throw completed.error;
111
+ return completed?.text || "";
111
112
  }
112
- await new Promise((resolve, reject) => this.activeTurns.set(turnId, { resolve, reject }));
113
+ return await new Promise((resolve, reject) => this.activeTurns.set(turnId, { resolve, reject }));
113
114
  }
114
115
  /** 中断当前正在运行的 Codex turn。 */
115
116
  async interruptCurrentTurn() {
@@ -127,6 +128,17 @@ export class CodexAppClient {
127
128
  return false;
128
129
  }
129
130
  }
131
+ /** 终止失去响应的 app-server,让后续队列可以在新进程继续。 */
132
+ terminate(message = "Codex app-server was restarted") {
133
+ this.failAll(message);
134
+ if (this.child.exitCode !== null)
135
+ return Promise.resolve();
136
+ return new Promise((resolve) => {
137
+ const timer = setTimeout(resolve, 2000);
138
+ this.child.once("exit", () => { clearTimeout(timer); resolve(); });
139
+ this.child.kill();
140
+ });
141
+ }
130
142
  /** 回复网页端已经确认的 Codex 权限请求。 */
131
143
  resolveApproval(requestId, decision) {
132
144
  const request = this.approvalRequests.get(requestId);
@@ -235,6 +247,9 @@ export class CodexAppClient {
235
247
  const streamedText = this.textByItem.get(id);
236
248
  if (item?.type === "agent_message" && streamedText && !item.text)
237
249
  item.text = streamedText;
250
+ const turnId = String(field(params, "turnId") || "");
251
+ if (turnId && item?.type === "agent_message" && item.text)
252
+ this.finalTextByTurn.set(turnId, String(item.text));
238
253
  if (id)
239
254
  this.textByItem.delete(id);
240
255
  }
@@ -253,12 +268,14 @@ export class CodexAppClient {
253
268
  const turnId = turn.id;
254
269
  const pending = this.activeTurns.get(turnId);
255
270
  const error = turn.error;
271
+ const text = this.finalTextByTurn.get(turnId) || "";
272
+ this.finalTextByTurn.delete(turnId);
256
273
  if (pending) {
257
274
  this.activeTurns.delete(turnId);
258
- error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(event);
275
+ error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(text);
259
276
  }
260
277
  else if (turnId) {
261
- this.completedTurns.set(turnId, error ? new Error(error.message || "Codex turn failed") : null);
278
+ this.completedTurns.set(turnId, { error: error ? new Error(error.message || "Codex turn failed") : null, text });
262
279
  }
263
280
  if (turnId === this.currentTurnId) {
264
281
  this.currentThreadId = "";
@@ -328,6 +345,7 @@ export class CodexAppClient {
328
345
  this.activeTurns.clear();
329
346
  this.pendingDeltas.clear();
330
347
  this.textByItem.clear();
348
+ this.finalTextByTurn.clear();
331
349
  this.approvalRequests.clear();
332
350
  this.currentThreadId = "";
333
351
  this.currentTurnId = "";
@@ -114,6 +114,7 @@ type CodexRequestSpec = {
114
114
  } | {
115
115
  type: "dangerFullAccess";
116
116
  };
117
+ outputSchema?: JsonRecord;
117
118
  };
118
119
  result: {
119
120
  turn: CodexTurn;
@@ -4,6 +4,7 @@ type CodexRunOptions = {
4
4
  cwd?: string;
5
5
  permissionMode?: AgentPermissionMode;
6
6
  appEmit?: AgentEmit;
7
+ outputSchema?: Record<string, unknown>;
7
8
  onStart?: () => void;
8
9
  onThread?: (threadId: string) => void;
9
10
  onTurn?: (turnId: string) => void;
@@ -11,6 +12,7 @@ type CodexRunOptions = {
11
12
  };
12
13
  export type CodexRunResult = {
13
14
  ok: true;
15
+ text: string;
14
16
  } | {
15
17
  ok: false;
16
18
  error: string;
@@ -20,6 +22,8 @@ export { summarizeCodexThread } from "./codex-history.js";
20
22
  export declare function runCodexTurn(prompt: string, emit: AgentEmit, attachments?: AgentAttachment[], options?: CodexRunOptions): Promise<CodexRunResult>;
21
23
  /** 中断当前线程正在执行的 Codex turn。 */
22
24
  export declare function interruptCodexTurn(threadId?: string): Promise<boolean>;
25
+ /** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
26
+ export declare function restartCodexApp(message?: string): Promise<void>;
23
27
  /** 回复当前 app-server 的待处理权限请求。 */
24
28
  export declare function resolveCodexApproval(requestId: string, decision: string): Promise<boolean>;
25
29
  /** 创建新的 Codex 线程并记录当前线程 ID。 */
@@ -24,6 +24,14 @@ export async function interruptCodexTurn(threadId) {
24
24
  return false;
25
25
  return await codexApp.interruptCurrentTurn();
26
26
  }
27
+ /** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
28
+ export async function restartCodexApp(message = "Codex 执行超时,正在重启本机脚本引擎") {
29
+ const app = codexApp;
30
+ codexApp = null;
31
+ codexAppStart = null;
32
+ codexThreadId = "";
33
+ await app?.terminate(message);
34
+ }
27
35
  /** 回复当前 app-server 的待处理权限请求。 */
28
36
  export async function resolveCodexApproval(requestId, decision) {
29
37
  return Boolean(codexApp?.resolveApproval(requestId, decision));
@@ -103,7 +111,8 @@ async function runCodexTurnNow(prompt, emit, attachments, options) {
103
111
  options.onThread?.(threadId);
104
112
  unmaterializedThreadIds.delete(threadId);
105
113
  try {
106
- await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
114
+ const text = await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn, options.outputSchema);
115
+ return { ok: true, text };
107
116
  }
108
117
  catch (error) {
109
118
  if (!isRecoverableThreadError(error))
@@ -113,9 +122,9 @@ async function runCodexTurnNow(prompt, emit, attachments, options) {
113
122
  threadId = await ensureCodexThread(app, { cwd: options.cwd }, emit);
114
123
  options.onThread?.(threadId);
115
124
  unmaterializedThreadIds.delete(threadId);
116
- await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
125
+ const text = await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn, options.outputSchema);
126
+ return { ok: true, text };
117
127
  }
118
- return { ok: true };
119
128
  }
120
129
  catch (error) {
121
130
  logger.error("Codex turn failed", error);
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ if (process.argv[2] === "mcp") {
7
7
  await ensureHttpServer();
8
8
  await startMcpServer();
9
9
  }
10
+ else if (process.argv[2] === "upgrade") {
11
+ await ensureHttpServer();
12
+ console.log("Canvas Agent upgraded and running in the background.");
13
+ }
10
14
  else if (process.argv[2] === "watch")
11
15
  startHttpSupervisor();
12
16
  else
@@ -59,7 +59,7 @@ export function startHttpServer() {
59
59
  return void res.json({});
60
60
  next();
61
61
  });
62
- app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION, relayReady: relayStatus.ready, relayLastReadyAt: relayStatus.lastReadyAt, relayLastDisconnectAt: relayStatus.lastDisconnectAt }));
62
+ app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION, workflow: workflows.health(), relayReady: relayStatus.ready, relayLastReadyAt: relayStatus.lastReadyAt, relayLastDisconnectAt: relayStatus.lastDisconnectAt }));
63
63
  app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true }));
64
64
  app.use((req, res, next) => {
65
65
  if (validToken(req, requestUrl(req, config), config.token))
@@ -1,7 +1,22 @@
1
1
  import type { AgentEmit } from "../agent/types.js";
2
2
  import { type CanvasAgentConfig } from "../config.js";
3
+ export declare const FLOW_C_CODEX_TURN_TIMEOUT_MS: number;
3
4
  type ScriptStatus = "queued" | "running" | "complete" | "error" | "expired";
4
5
  type DownloadStatus = "waiting" | "running" | "complete" | "error" | "expired";
6
+ type ScriptRecord = {
7
+ id: string;
8
+ apiBase: string;
9
+ accessToken: string;
10
+ status: ScriptStatus;
11
+ requestedCount: number;
12
+ receivedOrdinals: number[];
13
+ threadId?: string;
14
+ message?: string;
15
+ expiresAt?: string;
16
+ attempts: number;
17
+ priorityAt?: string;
18
+ updatedAt: string;
19
+ };
5
20
  type ScriptTask = {
6
21
  id: string;
7
22
  workflow: "flow-c";
@@ -86,6 +101,11 @@ export declare class WorkflowManager {
86
101
  updatedAt: string;
87
102
  }[];
88
103
  };
104
+ health(): {
105
+ activeScriptHandoffIds: string[];
106
+ queuedScripts: number;
107
+ blockedScripts: number;
108
+ };
89
109
  startDownloadDirectorySelection(): {
90
110
  id: string;
91
111
  status: "selecting" | "selected" | "cancelled" | "error";
@@ -174,4 +194,5 @@ export declare class WorkflowManager {
174
194
  private downloadRecord;
175
195
  private save;
176
196
  }
197
+ export declare function compareScriptQueueRecords(left: Pick<ScriptRecord, "priorityAt" | "updatedAt">, right: Pick<ScriptRecord, "priorityAt" | "updatedAt">): number;
177
198
  export {};
@@ -5,12 +5,14 @@ import { mkdir, open, rename, stat, unlink } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { Readable, Transform } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
- import { runCodexTurn, startCodexThread } from "../agent/codex.js";
8
+ import { restartCodexApp, runCodexTurn, startCodexThread } from "../agent/codex.js";
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
12
  import { FLOW_C_SCRIPT_CHUNK_MAX, FLOW_C_SCRIPT_CHUNK_SIZES } from "./constants.js";
13
+ import { flowCScriptOutputSchema, parseFlowCScriptOutput } from "./script-output.js";
13
14
  const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
15
+ export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
14
16
  /** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
15
17
  export class WorkflowManager {
16
18
  config;
@@ -45,6 +47,7 @@ export class WorkflowManager {
45
47
  threadId: previous?.threadId,
46
48
  expiresAt: String(input.expiresAt || previous?.expiresAt || "") || undefined,
47
49
  attempts: previous?.attempts || 0,
50
+ priorityAt: now(),
48
51
  message: previous?.status === "complete" ? previous.message : "已进入本机 Codex 队列",
49
52
  updatedAt: now(),
50
53
  };
@@ -56,6 +59,7 @@ export class WorkflowManager {
56
59
  const id = workflowId(idValue, "脚本交接 ID");
57
60
  const record = this.scriptRecord(id);
58
61
  record.status = "queued";
62
+ record.priorityAt = now();
59
63
  record.message = "正在重新连接本机 Codex";
60
64
  record.updatedAt = now();
61
65
  this.save();
@@ -108,6 +112,14 @@ export class WorkflowManager {
108
112
  subscriptions: Object.values(this.state.downloads).map(publicDownload),
109
113
  };
110
114
  }
115
+ health() {
116
+ const records = Object.values(this.state.scripts);
117
+ return {
118
+ activeScriptHandoffIds: [...this.runningScripts],
119
+ queuedScripts: records.filter((record) => record.status === "queued" || record.status === "running").length,
120
+ blockedScripts: records.filter((record) => record.status === "error").length,
121
+ };
122
+ }
111
123
  startDownloadDirectorySelection() {
112
124
  if (this.directorySelection?.status === "selecting")
113
125
  return this.directorySelection;
@@ -176,7 +188,7 @@ export class WorkflowManager {
176
188
  while (true) {
177
189
  const next = Object.values(this.state.scripts)
178
190
  .filter((record) => (record.status === "queued" || record.status === "running") && !this.runningScripts.has(record.id))
179
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))[0];
191
+ .sort(compareScriptQueueRecords)[0];
180
192
  if (!next)
181
193
  break;
182
194
  await this.runScript(next.id);
@@ -206,6 +218,7 @@ export class WorkflowManager {
206
218
  this.runningScripts.add(id);
207
219
  const record = this.scriptRecord(id);
208
220
  try {
221
+ delete record.priorityAt;
209
222
  record.status = "running";
210
223
  record.message = "正在读取完整产品清单";
211
224
  record.updatedAt = now();
@@ -235,14 +248,24 @@ export class WorkflowManager {
235
248
  record.message = `本机 Codex 正在写第 ${missing[0]}–${missing.at(-1)} 条(总计 ${task.requested_count} 条)`;
236
249
  record.updatedAt = now();
237
250
  this.save();
238
- const result = await runCodexTurn(scriptChunkPrompt(id, task, missing), this.emit, [], { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", onThread: (threadId) => { record.threadId = threadId; 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(); } });
239
252
  await this.scriptTask(id);
240
253
  progressed = record.receivedOrdinals.length > before;
254
+ if (!progressed && result.ok && result.text) {
255
+ try {
256
+ await this.submitScriptChunk(id, parseFlowCScriptOutput(result.text, missing));
257
+ await this.scriptTask(id);
258
+ progressed = record.receivedOrdinals.length > before;
259
+ }
260
+ catch (error) {
261
+ lastError = error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验";
262
+ }
263
+ }
241
264
  if (progressed)
242
265
  break;
243
266
  // Keep provider/client details out of the browser-facing state. The
244
267
  // underlying Codex runner already records the technical error locally.
245
- lastError = result.ok ? "Codex 未调用回传工具" : "Codex 本轮执行失败";
268
+ lastError ||= result.ok ? "Codex 未返回可用的结构化脚本" : "Codex 本轮执行失败";
246
269
  if (attemptIndex < FLOW_C_SCRIPT_CHUNK_SIZES.length - 1) {
247
270
  const nextSize = FLOW_C_SCRIPT_CHUNK_SIZES[attemptIndex + 1];
248
271
  record.message = `第 ${lastRange} 条未成功回传,正在换新会话并缩小为每段 ${nextSize} 条重试`;
@@ -406,6 +429,28 @@ export class WorkflowManager {
406
429
  }
407
430
  save() { saveState(this.state); }
408
431
  }
432
+ export function compareScriptQueueRecords(left, right) {
433
+ if (left.priorityAt || right.priorityAt)
434
+ return String(right.priorityAt || "").localeCompare(String(left.priorityAt || ""));
435
+ return left.updatedAt.localeCompare(right.updatedAt);
436
+ }
437
+ async function runCodexScriptWithTimeout(prompt, emit, options) {
438
+ let timer;
439
+ const turn = runCodexTurn(prompt, emit, [], options);
440
+ const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), FLOW_C_CODEX_TURN_TIMEOUT_MS); });
441
+ try {
442
+ const result = await Promise.race([turn, timeout]);
443
+ if (result !== "timeout")
444
+ return result;
445
+ await restartCodexApp("Flow C 脚本回合超过 8 分钟,已自动终止并换新会话");
446
+ await turn;
447
+ return { ok: false, error: "Flow C 脚本回合超过 8 分钟,已自动终止并换新会话" };
448
+ }
449
+ finally {
450
+ if (timer)
451
+ clearTimeout(timer);
452
+ }
453
+ }
409
454
  class ExpiredCapabilityError extends Error {
410
455
  }
411
456
  function scriptChunkPrompt(id, task, ordinals) {
@@ -415,13 +460,17 @@ function scriptChunkPrompt(id, task, ordinals) {
415
460
  每条回传同时填写 script=masterScript、masterScript 和 segments。segments[0].continuityMode 必须是 reset;同人物同场景延续时后续段用 continue,明确换人物或换场景才用 reset。continue 段必须复述并固定人物年龄、外貌、服装、场景陈设、光线、机位基调和商品当前状态。
416
461
  每份 masterScript 和每个 segment 都要写同一套详细音色身份:性别、年龄段、音高、音质、语速、口音、停连和说话习惯;情绪随剧情逐段变化,但音色身份不变。仍按短视频快节奏口播,不因时长增加而拖慢。`;
417
462
  return `你正在后台处理“抖音小辉跨境工具”Flow C 脚本交接 ${id}。
418
- 必须使用 MCP 工具 flow_c_get_script_task 读取完整任务,再只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
463
+ 完整中心任务已经附在本提示词末尾。只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
419
464
  本段 productIndex 必须严格按此映射填写:${scriptProductAssignments(task.product_quantities, ordinals)}。不得凭产品名称猜测或把相邻产品编号混用。
420
465
  脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地语言与偏快但清晰的短视频节奏。${longVideoRules}
421
466
  每一个 ordinal 都必须从头到尾创作一份完整、独立的原生带货脚本,画面和口播必须在同一份脚本中一起独立构思。先让它成为一个当地真人在具体生活情境里自然发现、吐槽、试用或验证商品的短内容,再完成带货;禁止写成品牌广告片、棚拍宣传片、电视购物、逐条念卖点或全程完美对镜讲解。独立脚本天然包含独立口播:不得把任何一份口播当作整批公共模板,不得复用完整台词,也不得只换人物、场景或少数词后保留近似口播。音色身份可以为同一人物保持一致,但每条的 Hook、人物行动、证明表达和 CTA 都必须重新写。
422
467
  用户没有指定带货方向时,不得随意只用一种泛化形式;必须按任务 instructions 中“已批准的创意方向”逐条做产品适配轮换。轮换必须按每个产品自己的序号连续计算,不能因 30/15/10 条分段、换会话或跨产品边界而从第一个方向重新开始;让同一产品在重复某一方向前优先覆盖其他适配方向。
423
468
  工厂风格 A/B 是默认轮换中的演绎带货布景,不是商品来源声明;不得因用户未提供真实工厂资料而跳过,也绝不能写成我们的真实工厂、真实生产流程、真实产地、工厂直销、厂家出货或仓库现货。
424
- 写完后必须调用 flow_c_submit_script_chunk 一次回传这 ${ordinals.length} 条,handoffId=${id}。不要创建付费批次,不要调用供应商模型,不要在聊天输出大段 JSON。工具返回成功后仅简短结束。`;
469
+ 只返回符合当前结构化输出契约的 jobs;每条必须包含 ordinal、productIndex、sellingFormId script,长视频还必须包含 masterScript 与完整 segments。无需调用 MCP,Agent 会在本机校验后自动回传。不要创建付费批次,不要调用供应商模型。
470
+
471
+ 【中心任务完整 instructions】
472
+ ${task.instructions}
473
+ 【中心任务 instructions 结束】`;
425
474
  }
426
475
  function scriptProductAssignments(productQuantities, ordinals) {
427
476
  const assignments = [];
@@ -0,0 +1,5 @@
1
+ type JsonSchema = Record<string, unknown>;
2
+ /** Codex app-server 的结构化脚本返回契约;回传由 Agent 完成,不依赖模型调用 MCP。 */
3
+ export declare function flowCScriptOutputSchema(durationSeconds: 10 | 20 | 30, count: number): JsonSchema;
4
+ export declare function parseFlowCScriptOutput(value: string, expectedOrdinals: number[]): unknown[];
5
+ export {};
@@ -0,0 +1,52 @@
1
+ const text = { type: "string", minLength: 1 };
2
+ const continuityFields = ["character", "wardrobe", "location", "lighting", "productState", "unfinishedAction", "nextGoal"];
3
+ function object(properties, required = Object.keys(properties)) {
4
+ return { type: "object", properties, required, additionalProperties: false };
5
+ }
6
+ function continuitySchema(frameField) {
7
+ return object(Object.fromEntries([...continuityFields, frameField].map((field) => [field, text])));
8
+ }
9
+ function segmentSchema() {
10
+ return object({
11
+ continuityMode: { type: "string", enum: ["reset", "continue"] },
12
+ continuity: continuitySchema("previousEndingFrame"),
13
+ endingState: continuitySchema("endingFrame"),
14
+ shots: {
15
+ type: "array",
16
+ minItems: 1,
17
+ maxItems: 8,
18
+ items: object({
19
+ startSeconds: { type: "number", minimum: 0, maximum: 10 },
20
+ endSeconds: { type: "number", minimum: 0, maximum: 10 },
21
+ visual: text,
22
+ voiceover: text,
23
+ onScreenText: text,
24
+ evidence: text,
25
+ }),
26
+ },
27
+ });
28
+ }
29
+ /** Codex app-server 的结构化脚本返回契约;回传由 Agent 完成,不依赖模型调用 MCP。 */
30
+ export function flowCScriptOutputSchema(durationSeconds, count) {
31
+ const properties = {
32
+ ordinal: { type: "integer", minimum: 1 },
33
+ productIndex: { type: "integer", minimum: 0 },
34
+ sellingFormId: text,
35
+ script: { type: "string", minLength: 40 },
36
+ };
37
+ if (durationSeconds !== 10) {
38
+ properties.masterScript = { type: "string", minLength: 40 };
39
+ properties.segments = { type: "array", minItems: durationSeconds / 10, maxItems: durationSeconds / 10, items: segmentSchema() };
40
+ }
41
+ return object({ jobs: { type: "array", minItems: count, maxItems: count, items: object(properties) } });
42
+ }
43
+ export function parseFlowCScriptOutput(value, expectedOrdinals) {
44
+ const source = String(value || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
45
+ const parsed = JSON.parse(source);
46
+ if (!Array.isArray(parsed.jobs) || parsed.jobs.length !== expectedOrdinals.length)
47
+ throw new Error("Codex 返回的脚本数量不正确");
48
+ const actual = parsed.jobs.map((job) => Number(job?.ordinal));
49
+ if (actual.some((ordinal, index) => ordinal !== expectedOrdinals[index]))
50
+ throw new Error("Codex 返回的 ordinal 与当前分段不一致");
51
+ return parsed.jobs;
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.42",
3
+ "version": "0.4.44",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",