@xiaohhhh1/canvas-agent 0.4.47 → 0.4.49
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/dist/agent/codex-client.d.ts +7 -3
- package/dist/agent/codex-client.js +10 -10
- package/dist/agent/codex-protocol.d.ts +3 -0
- package/dist/agent/codex.d.ts +23 -0
- package/dist/agent/codex.js +63 -0
- package/dist/agent/worker-pool.d.ts +13 -0
- package/dist/agent/worker-pool.js +45 -0
- package/dist/canvas/schemas.d.ts +2 -2
- package/dist/canvas/tools.d.ts +1 -1
- package/dist/workflow/constants.d.ts +6 -4
- package/dist/workflow/constants.js +10 -3
- package/dist/workflow/manager.d.ts +36 -0
- package/dist/workflow/manager.js +124 -76
- package/dist/workflow/script-output.js +106 -12
- package/package.json +1 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type JsonRecord } from "../utils/value.js";
|
|
2
2
|
import type { CodexPlanUpdate, CodexRequestParams, CodexTurn } from "./codex-protocol.js";
|
|
3
3
|
import type { AgentEmit, AgentPermissionMode } from "./types.js";
|
|
4
|
+
export type CodexModelSettings = {
|
|
5
|
+
model?: string;
|
|
6
|
+
reasoningEffort?: string;
|
|
7
|
+
};
|
|
4
8
|
/** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
|
|
5
9
|
export declare class CodexAppClient {
|
|
6
10
|
private child;
|
|
@@ -23,9 +27,9 @@ export declare class CodexAppClient {
|
|
|
23
27
|
/** 启动并初始化 Codex app-server。 */
|
|
24
28
|
static start(emit: AgentEmit, onExit: () => void): Promise<CodexAppClient>;
|
|
25
29
|
/** 创建新的 Codex 线程。 */
|
|
26
|
-
startThread(cwd?: string, permissionMode?: AgentPermissionMode): Promise<import("./codex-protocol.js").CodexThread>;
|
|
30
|
+
startThread(cwd?: string, permissionMode?: AgentPermissionMode, modelSettings?: CodexModelSettings): Promise<import("./codex-protocol.js").CodexThread>;
|
|
27
31
|
/** 恢复已有 Codex 线程。 */
|
|
28
|
-
resumeThread(threadId: string, cwd?: string, permissionMode?: AgentPermissionMode): Promise<import("./codex-protocol.js").CodexThread>;
|
|
32
|
+
resumeThread(threadId: string, cwd?: string, permissionMode?: AgentPermissionMode, modelSettings?: CodexModelSettings): Promise<import("./codex-protocol.js").CodexThread>;
|
|
29
33
|
/** 查询 Codex 线程列表。 */
|
|
30
34
|
listThreads(params: CodexRequestParams<"thread/list">): Promise<{
|
|
31
35
|
data: import("./codex-protocol.js").CodexThread[];
|
|
@@ -43,7 +47,7 @@ export declare class CodexAppClient {
|
|
|
43
47
|
/** 清理已归档线程的任务计划缓存。 */
|
|
44
48
|
clearPlanUpdates(threadId: string): void;
|
|
45
49
|
/** 启动一个 Codex turn 并等待完成通知。 */
|
|
46
|
-
startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void, outputSchema?: JsonRecord): Promise<string>;
|
|
50
|
+
startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void, outputSchema?: JsonRecord, modelSettings?: CodexModelSettings): Promise<string>;
|
|
47
51
|
/** 中断当前正在运行的 Codex turn。 */
|
|
48
52
|
interruptCurrentTurn(): Promise<boolean>;
|
|
49
53
|
/** 终止失去响应的 app-server,让后续队列可以在新进程继续。 */
|
|
@@ -57,15 +57,15 @@ export class CodexAppClient {
|
|
|
57
57
|
return client;
|
|
58
58
|
}
|
|
59
59
|
/** 创建新的 Codex 线程。 */
|
|
60
|
-
async startThread(cwd, permissionMode = "request") {
|
|
61
|
-
const { thread } = await this.request("thread/start", { ...threadSettings(permissionMode), ...(cwd ? { cwd } : {}), threadSource: "user" });
|
|
60
|
+
async startThread(cwd, permissionMode = "request", modelSettings = {}) {
|
|
61
|
+
const { thread } = await this.request("thread/start", { ...threadSettings(permissionMode, modelSettings), ...(cwd ? { cwd } : {}), ...(modelSettings.model ? { model: modelSettings.model } : {}), threadSource: "user" });
|
|
62
62
|
if (!thread.id)
|
|
63
63
|
throw new Error("Codex app-server 没有返回 thread id");
|
|
64
64
|
return thread;
|
|
65
65
|
}
|
|
66
66
|
/** 恢复已有 Codex 线程。 */
|
|
67
|
-
async resumeThread(threadId, cwd, permissionMode = "request") {
|
|
68
|
-
const { thread } = await this.request("thread/resume", { threadId, ...threadSettings(permissionMode), ...(cwd ? { cwd } : {}) });
|
|
67
|
+
async resumeThread(threadId, cwd, permissionMode = "request", modelSettings = {}) {
|
|
68
|
+
const { thread } = await this.request("thread/resume", { threadId, ...threadSettings(permissionMode, modelSettings), ...(cwd ? { cwd } : {}), ...(modelSettings.model ? { model: modelSettings.model } : {}) });
|
|
69
69
|
if (!thread.id)
|
|
70
70
|
throw new Error("Codex app-server 没有返回 thread id");
|
|
71
71
|
return thread;
|
|
@@ -94,9 +94,9 @@ export class CodexAppClient {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
/** 启动一个 Codex turn 并等待完成通知。 */
|
|
97
|
-
async startTurn(threadId, prompt, images, permissionMode, onTurn, outputSchema) {
|
|
97
|
+
async startTurn(threadId, prompt, images, permissionMode, onTurn, outputSchema, modelSettings = {}) {
|
|
98
98
|
this.currentThreadId = threadId;
|
|
99
|
-
const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode), ...(outputSchema ? { outputSchema } : {}) });
|
|
99
|
+
const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode), ...(modelSettings.model ? { model: modelSettings.model } : {}), ...(modelSettings.reasoningEffort ? { effort: modelSettings.reasoningEffort } : {}), ...(outputSchema ? { outputSchema } : {}) });
|
|
100
100
|
const turnId = turn.id;
|
|
101
101
|
if (!turnId)
|
|
102
102
|
throw new Error("Codex app-server 没有返回 turn id");
|
|
@@ -445,11 +445,11 @@ function canvasAgentMcpCommand() {
|
|
|
445
445
|
return entry.endsWith(".ts") ? { command: process.execPath, args: [tsx, entry, "mcp"] } : { command: process.execPath, args: [entry, "mcp"] };
|
|
446
446
|
}
|
|
447
447
|
/** 生成 Codex app-server 使用的 MCP 配置。 */
|
|
448
|
-
function codexConfig(permissionMode) {
|
|
449
|
-
return { model_reasoning_summary: "auto", ...(permissionMode === "automatic" ? { approvals_reviewer: "auto_review" } : {}), mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } } };
|
|
448
|
+
function codexConfig(permissionMode, modelSettings = {}) {
|
|
449
|
+
return { model_reasoning_summary: "auto", ...(modelSettings.reasoningEffort ? { model_reasoning_effort: modelSettings.reasoningEffort } : {}), ...(permissionMode === "automatic" ? { approvals_reviewer: "auto_review" } : {}), mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } } };
|
|
450
450
|
}
|
|
451
|
-
function threadSettings(permissionMode) {
|
|
452
|
-
return { approvalPolicy: permissionMode === "full" ? "never" : "on-request", sandbox: permissionMode === "full" ? "danger-full-access" : "workspace-write", config: codexConfig(permissionMode) };
|
|
451
|
+
function threadSettings(permissionMode, modelSettings = {}) {
|
|
452
|
+
return { approvalPolicy: permissionMode === "full" ? "never" : "on-request", sandbox: permissionMode === "full" ? "danger-full-access" : "workspace-write", config: codexConfig(permissionMode, modelSettings) };
|
|
453
453
|
}
|
|
454
454
|
function turnSettings(permissionMode) {
|
|
455
455
|
return {
|
|
@@ -42,6 +42,7 @@ type ThreadOptions = {
|
|
|
42
42
|
sandbox: "workspace-write" | "danger-full-access";
|
|
43
43
|
config: JsonRecord;
|
|
44
44
|
cwd?: string;
|
|
45
|
+
model?: string;
|
|
45
46
|
};
|
|
46
47
|
type CodexRequestSpec = {
|
|
47
48
|
initialize: {
|
|
@@ -115,6 +116,8 @@ type CodexRequestSpec = {
|
|
|
115
116
|
} | {
|
|
116
117
|
type: "dangerFullAccess";
|
|
117
118
|
};
|
|
119
|
+
model?: string;
|
|
120
|
+
effort?: string;
|
|
118
121
|
outputSchema?: JsonRecord;
|
|
119
122
|
};
|
|
120
123
|
result: {
|
package/dist/agent/codex.d.ts
CHANGED
|
@@ -17,11 +17,34 @@ export type CodexRunResult = {
|
|
|
17
17
|
ok: false;
|
|
18
18
|
error: string;
|
|
19
19
|
};
|
|
20
|
+
export type CodexWorkflowRunResult = CodexRunResult & {
|
|
21
|
+
timings: {
|
|
22
|
+
queueWaitMs: number;
|
|
23
|
+
threadStartMs: number;
|
|
24
|
+
modelMs: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export declare const FLOW_C_CODEX_MODEL = "gpt-5.6-terra";
|
|
28
|
+
export declare const FLOW_C_CODEX_REASONING_EFFORT = "high";
|
|
29
|
+
export declare const FLOW_C_CODEX_WORKER_CONCURRENCY: number;
|
|
30
|
+
export declare function flowCCodexWorkerStatus(): {
|
|
31
|
+
active: number;
|
|
32
|
+
limit: number;
|
|
33
|
+
};
|
|
20
34
|
export { summarizeCodexThread } from "./codex-history.js";
|
|
21
35
|
/** 将 Codex turn 加入串行队列并等待执行完成。 */
|
|
22
36
|
export declare function runCodexTurn(prompt: string, emit: AgentEmit, attachments?: AgentAttachment[], options?: CodexRunOptions): Promise<CodexRunResult>;
|
|
23
37
|
/** 中断当前线程正在执行的 Codex turn。 */
|
|
24
38
|
export declare function interruptCodexTurn(threadId?: string): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Flow C 专用受控并行回合。每个 lane 独占一个 app-server,chunk 使用独立线程,
|
|
41
|
+
* 从而不会被交互式 Codex 全局队列或另一个长脚本回合阻塞。
|
|
42
|
+
*/
|
|
43
|
+
export declare function runCodexWorkflowTurn(prompt: string, emit: AgentEmit, options: CodexRunOptions & {
|
|
44
|
+
timeoutMs: number;
|
|
45
|
+
onWorkerStart?: () => void;
|
|
46
|
+
onWorkerFinish?: () => void;
|
|
47
|
+
}): Promise<CodexWorkflowRunResult>;
|
|
25
48
|
/** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
|
|
26
49
|
export declare function restartCodexApp(message?: string): Promise<void>;
|
|
27
50
|
/** 回复当前 app-server 的待处理权限请求。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -5,11 +5,18 @@ import { logger } from "../utils/logger.js";
|
|
|
5
5
|
import { errorMessage, field } from "../utils/value.js";
|
|
6
6
|
import { CodexAppClient } from "./codex-client.js";
|
|
7
7
|
import { summarizeCodexThread, threadMessages } from "./codex-history.js";
|
|
8
|
+
import { boundedWorkerConcurrency, WorkerPool } from "./worker-pool.js";
|
|
9
|
+
export const FLOW_C_CODEX_MODEL = "gpt-5.6-terra";
|
|
10
|
+
export const FLOW_C_CODEX_REASONING_EFFORT = "high";
|
|
11
|
+
export const FLOW_C_CODEX_WORKER_CONCURRENCY = boundedWorkerConcurrency(process.env.FLOW_C_SCRIPT_CONCURRENCY);
|
|
12
|
+
export function flowCCodexWorkerStatus() { return { active: workflowCodexPool.activeCount, limit: FLOW_C_CODEX_WORKER_CONCURRENCY }; }
|
|
8
13
|
let codexQueue = Promise.resolve();
|
|
9
14
|
let codexApp = null;
|
|
10
15
|
let codexAppStart = null;
|
|
11
16
|
let codexThreadId = "";
|
|
12
17
|
const unmaterializedThreadIds = new Set();
|
|
18
|
+
const workflowCodexPool = new WorkerPool(FLOW_C_CODEX_WORKER_CONCURRENCY);
|
|
19
|
+
const workflowCodexApps = new Map();
|
|
13
20
|
export { summarizeCodexThread } from "./codex-history.js";
|
|
14
21
|
/** 将 Codex turn 加入串行队列并等待执行完成。 */
|
|
15
22
|
export async function runCodexTurn(prompt, emit, attachments = [], options = {}) {
|
|
@@ -24,6 +31,62 @@ export async function interruptCodexTurn(threadId) {
|
|
|
24
31
|
return false;
|
|
25
32
|
return await codexApp.interruptCurrentTurn();
|
|
26
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Flow C 专用受控并行回合。每个 lane 独占一个 app-server,chunk 使用独立线程,
|
|
36
|
+
* 从而不会被交互式 Codex 全局队列或另一个长脚本回合阻塞。
|
|
37
|
+
*/
|
|
38
|
+
export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
39
|
+
if (!prompt.trim())
|
|
40
|
+
return { ok: false, error: "Codex prompt is empty", timings: { queueWaitMs: 0, threadStartMs: 0, modelMs: 0 } };
|
|
41
|
+
return await workflowCodexPool.run(async (workerIndex, queueWaitMs) => {
|
|
42
|
+
options.onWorkerStart?.();
|
|
43
|
+
const modelSettings = { model: FLOW_C_CODEX_MODEL, reasoningEffort: FLOW_C_CODEX_REASONING_EFFORT };
|
|
44
|
+
const threadStartedAt = Date.now();
|
|
45
|
+
let threadStartMs = 0;
|
|
46
|
+
let modelStartedAt = 0;
|
|
47
|
+
let timer;
|
|
48
|
+
let app = workflowCodexApps.get(workerIndex);
|
|
49
|
+
try {
|
|
50
|
+
if (!app)
|
|
51
|
+
app = await startWorkflowCodexApp(workerIndex, options.appEmit || emit);
|
|
52
|
+
const thread = await app.startThread(options.cwd, options.permissionMode || "request", modelSettings);
|
|
53
|
+
const threadId = String(field(thread, "id") || "");
|
|
54
|
+
options.onThread?.(threadId);
|
|
55
|
+
threadStartMs = Date.now() - threadStartedAt;
|
|
56
|
+
modelStartedAt = Date.now();
|
|
57
|
+
const turn = app.startTurn(threadId, prompt, [], options.permissionMode || "request", options.onTurn, options.outputSchema, modelSettings);
|
|
58
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), options.timeoutMs); });
|
|
59
|
+
const result = await Promise.race([turn, timeout]);
|
|
60
|
+
if (result === "timeout") {
|
|
61
|
+
workflowCodexApps.delete(workerIndex);
|
|
62
|
+
await app.terminate("Flow C 脚本回合超时,已仅回收当前 worker");
|
|
63
|
+
await turn.catch(() => undefined);
|
|
64
|
+
return { ok: false, error: "Flow C 脚本回合超过 8 分钟,已自动终止当前 worker", timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, text: result, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
logger.error("Flow C Codex worker failed", { workerIndex, error });
|
|
70
|
+
const message = errorMessage(error);
|
|
71
|
+
emit("agent_error", { message });
|
|
72
|
+
return { ok: false, error: message, timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (timer)
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
options.onWorkerFinish?.();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function startWorkflowCodexApp(workerIndex, emit) {
|
|
82
|
+
let started;
|
|
83
|
+
started = await CodexAppClient.start(emit, () => {
|
|
84
|
+
if (!started || workflowCodexApps.get(workerIndex) === started)
|
|
85
|
+
workflowCodexApps.delete(workerIndex);
|
|
86
|
+
});
|
|
87
|
+
workflowCodexApps.set(workerIndex, started);
|
|
88
|
+
return started;
|
|
89
|
+
}
|
|
27
90
|
/** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
|
|
28
91
|
export async function restartCodexApp(message = "Codex 执行超时,正在重启本机脚本引擎") {
|
|
29
92
|
const app = codexApp;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 小型 FIFO worker pool;每个 worker 同时只执行一个任务。 */
|
|
2
|
+
export declare class WorkerPool {
|
|
3
|
+
readonly concurrency: number;
|
|
4
|
+
private available;
|
|
5
|
+
private waiters;
|
|
6
|
+
private active;
|
|
7
|
+
constructor(concurrency: number);
|
|
8
|
+
run<T>(task: (workerIndex: number, queueWaitMs: number) => Promise<T>): Promise<T>;
|
|
9
|
+
get activeCount(): number;
|
|
10
|
+
private acquire;
|
|
11
|
+
private release;
|
|
12
|
+
}
|
|
13
|
+
export declare function boundedWorkerConcurrency(value: unknown, fallback?: number, maximum?: number): number;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 小型 FIFO worker pool;每个 worker 同时只执行一个任务。 */
|
|
2
|
+
export class WorkerPool {
|
|
3
|
+
concurrency;
|
|
4
|
+
available;
|
|
5
|
+
waiters = [];
|
|
6
|
+
active = new Set();
|
|
7
|
+
constructor(concurrency) {
|
|
8
|
+
this.concurrency = concurrency;
|
|
9
|
+
if (!Number.isInteger(concurrency) || concurrency < 1)
|
|
10
|
+
throw new Error("worker concurrency must be a positive integer");
|
|
11
|
+
this.available = Array.from({ length: concurrency }, (_, index) => index);
|
|
12
|
+
}
|
|
13
|
+
async run(task) {
|
|
14
|
+
const queuedAt = Date.now();
|
|
15
|
+
const workerIndex = await this.acquire();
|
|
16
|
+
this.active.add(workerIndex);
|
|
17
|
+
try {
|
|
18
|
+
return await task(workerIndex, Date.now() - queuedAt);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
this.release(workerIndex);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
get activeCount() { return this.active.size; }
|
|
25
|
+
acquire() {
|
|
26
|
+
const workerIndex = this.available.shift();
|
|
27
|
+
if (workerIndex !== undefined)
|
|
28
|
+
return Promise.resolve(workerIndex);
|
|
29
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
30
|
+
}
|
|
31
|
+
release(workerIndex) {
|
|
32
|
+
this.active.delete(workerIndex);
|
|
33
|
+
const waiter = this.waiters.shift();
|
|
34
|
+
if (waiter)
|
|
35
|
+
waiter(workerIndex);
|
|
36
|
+
else
|
|
37
|
+
this.available.push(workerIndex);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function boundedWorkerConcurrency(value, fallback = 2, maximum = 4) {
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
43
|
+
return fallback;
|
|
44
|
+
return Math.min(parsed, maximum);
|
|
45
|
+
}
|
package/dist/canvas/schemas.d.ts
CHANGED
|
@@ -887,15 +887,15 @@ export declare const toolInputSchemas: {
|
|
|
887
887
|
run: z.ZodOptional<z.ZodBoolean>;
|
|
888
888
|
}, "strip", z.ZodTypeAny, {
|
|
889
889
|
prompt: string;
|
|
890
|
-
count?: number | undefined;
|
|
891
890
|
model?: string | undefined;
|
|
891
|
+
count?: number | undefined;
|
|
892
892
|
size?: string | undefined;
|
|
893
893
|
quality?: string | undefined;
|
|
894
894
|
run?: boolean | undefined;
|
|
895
895
|
}, {
|
|
896
896
|
prompt: string;
|
|
897
|
-
count?: number | undefined;
|
|
898
897
|
model?: string | undefined;
|
|
898
|
+
count?: number | undefined;
|
|
899
899
|
size?: string | undefined;
|
|
900
900
|
quality?: string | undefined;
|
|
901
901
|
run?: boolean | undefined;
|
package/dist/canvas/tools.d.ts
CHANGED
|
@@ -156,8 +156,8 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
|
156
156
|
limit?: number | undefined;
|
|
157
157
|
} | {
|
|
158
158
|
prompt: string;
|
|
159
|
-
count?: number | undefined;
|
|
160
159
|
model?: string | undefined;
|
|
160
|
+
count?: number | undefined;
|
|
161
161
|
size?: string | undefined;
|
|
162
162
|
quality?: string | undefined;
|
|
163
163
|
run?: boolean | undefined;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
1
|
+
/** 中心/MCP 兼容上限仍为 30;结构化 10 秒脚本按更小子批受控并行。 */
|
|
2
|
+
export declare const FLOW_C_SCRIPT_CHUNK_MAX = 30;
|
|
3
|
+
export declare const FLOW_C_SCRIPT_CHUNK_SIZES: readonly [10, 5, 1];
|
|
4
4
|
/**
|
|
5
5
|
* 长视频每条都包含总脚本和 2/3 套完整分镜,不能沿用短脚本的 30 条输出量。
|
|
6
6
|
* 小段回传既能更早显示进度,也避免单个 Codex turn 因输出过大而被中断。
|
|
7
7
|
*/
|
|
8
|
-
export declare function flowCScriptChunkSizes(durationSeconds: 10 | 20 | 30): readonly [
|
|
8
|
+
export declare function flowCScriptChunkSizes(durationSeconds: 10 | 20 | 30): readonly [10, 5, 1] | readonly [2, 1] | readonly [1];
|
|
9
|
+
/** 将当前缺失 ordinal 切成可独立持久化的子批;长视频绝不恢复 30 条大回合。 */
|
|
10
|
+
export declare function flowCScriptChunks(durationSeconds: 10 | 20 | 30, ordinals: number[], size?: number): number[][];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
1
|
+
/** 中心/MCP 兼容上限仍为 30;结构化 10 秒脚本按更小子批受控并行。 */
|
|
2
|
+
export const FLOW_C_SCRIPT_CHUNK_MAX = 30;
|
|
3
|
+
export const FLOW_C_SCRIPT_CHUNK_SIZES = [10, 5, 1];
|
|
4
4
|
/**
|
|
5
5
|
* 长视频每条都包含总脚本和 2/3 套完整分镜,不能沿用短脚本的 30 条输出量。
|
|
6
6
|
* 小段回传既能更早显示进度,也避免单个 Codex turn 因输出过大而被中断。
|
|
@@ -12,3 +12,10 @@ export function flowCScriptChunkSizes(durationSeconds) {
|
|
|
12
12
|
return [1];
|
|
13
13
|
return FLOW_C_SCRIPT_CHUNK_SIZES;
|
|
14
14
|
}
|
|
15
|
+
/** 将当前缺失 ordinal 切成可独立持久化的子批;长视频绝不恢复 30 条大回合。 */
|
|
16
|
+
export function flowCScriptChunks(durationSeconds, ordinals, size = flowCScriptChunkSizes(durationSeconds)[0]) {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
for (let index = 0; index < ordinals.length; index += size)
|
|
19
|
+
chunks.push(ordinals.slice(index, index + size));
|
|
20
|
+
return chunks;
|
|
21
|
+
}
|
|
@@ -14,14 +14,31 @@ type ScriptRecord = {
|
|
|
14
14
|
message?: string;
|
|
15
15
|
expiresAt?: string;
|
|
16
16
|
attempts: number;
|
|
17
|
+
chunkSize?: number;
|
|
18
|
+
activeChunks?: number;
|
|
17
19
|
priorityAt?: string;
|
|
18
20
|
updatedAt: string;
|
|
19
21
|
};
|
|
22
|
+
type ReferenceStyleCard = {
|
|
23
|
+
openingComposition?: unknown;
|
|
24
|
+
visualDensity?: unknown;
|
|
25
|
+
conflictContrast?: unknown;
|
|
26
|
+
characterProductEntrance?: unknown;
|
|
27
|
+
rhythm?: unknown;
|
|
28
|
+
turn?: unknown;
|
|
29
|
+
proofMethod?: unknown;
|
|
30
|
+
voiceTone?: unknown;
|
|
31
|
+
categoryFit?: unknown;
|
|
32
|
+
};
|
|
20
33
|
type ScriptTask = {
|
|
21
34
|
id: string;
|
|
22
35
|
workflow: "flow-c";
|
|
23
36
|
market: string;
|
|
24
37
|
duration_seconds?: 10 | 20 | 30;
|
|
38
|
+
storyboard_layout_version?: "hybrid-three-anchor-v1" | "legacy-five-row-v1";
|
|
39
|
+
storyboardLayoutVersion?: "hybrid-three-anchor-v1" | "legacy-five-row-v1";
|
|
40
|
+
reference_style_card?: ReferenceStyleCard;
|
|
41
|
+
referenceStyleCard?: ReferenceStyleCard;
|
|
25
42
|
requested_count: number;
|
|
26
43
|
product_quantities: number[];
|
|
27
44
|
instructions: string;
|
|
@@ -52,6 +69,8 @@ export declare class WorkflowManager {
|
|
|
52
69
|
requestedCount: number;
|
|
53
70
|
received: number;
|
|
54
71
|
threadId: string | undefined;
|
|
72
|
+
chunkSize: number | null;
|
|
73
|
+
activeChunks: number;
|
|
55
74
|
message: string | undefined;
|
|
56
75
|
expiresAt: string | undefined;
|
|
57
76
|
updatedAt: string;
|
|
@@ -62,6 +81,8 @@ export declare class WorkflowManager {
|
|
|
62
81
|
requestedCount: number;
|
|
63
82
|
received: number;
|
|
64
83
|
threadId: string | undefined;
|
|
84
|
+
chunkSize: number | null;
|
|
85
|
+
activeChunks: number;
|
|
65
86
|
message: string | undefined;
|
|
66
87
|
expiresAt: string | undefined;
|
|
67
88
|
updatedAt: string;
|
|
@@ -72,6 +93,8 @@ export declare class WorkflowManager {
|
|
|
72
93
|
requestedCount: number;
|
|
73
94
|
received: number;
|
|
74
95
|
threadId: string | undefined;
|
|
96
|
+
chunkSize: number | null;
|
|
97
|
+
activeChunks: number;
|
|
75
98
|
message: string | undefined;
|
|
76
99
|
expiresAt: string | undefined;
|
|
77
100
|
updatedAt: string;
|
|
@@ -103,6 +126,8 @@ export declare class WorkflowManager {
|
|
|
103
126
|
};
|
|
104
127
|
health(): {
|
|
105
128
|
activeScriptHandoffIds: string[];
|
|
129
|
+
activeScripts: number;
|
|
130
|
+
scriptConcurrencyLimit: number;
|
|
106
131
|
queuedScripts: number;
|
|
107
132
|
blockedScripts: number;
|
|
108
133
|
};
|
|
@@ -187,6 +212,15 @@ export declare class WorkflowManager {
|
|
|
187
212
|
private pumpScriptQueue;
|
|
188
213
|
private finishDownloadDirectorySelection;
|
|
189
214
|
private runScript;
|
|
215
|
+
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
216
|
+
private runScriptChunk;
|
|
217
|
+
/** 只记录阶段、ordinal 和毫秒数;禁止记录 prompt、令牌或中心能力。 */
|
|
218
|
+
private emitScriptStage;
|
|
219
|
+
/**
|
|
220
|
+
* 中心批量校验失败时逐条补交,避免一条格式异常拖累同轮其他有效脚本。
|
|
221
|
+
* 已接纳 ordinal 由中心持久化,下一轮只会继续缺失项。
|
|
222
|
+
*/
|
|
223
|
+
private submitGeneratedScriptJobs;
|
|
190
224
|
private syncDownloads;
|
|
191
225
|
private syncDownloadRecord;
|
|
192
226
|
private saveDelivery;
|
|
@@ -195,4 +229,6 @@ export declare class WorkflowManager {
|
|
|
195
229
|
private save;
|
|
196
230
|
}
|
|
197
231
|
export declare function compareScriptQueueRecords(left: Pick<ScriptRecord, "priorityAt" | "updatedAt">, right: Pick<ScriptRecord, "priorityAt" | "updatedAt">): number;
|
|
232
|
+
export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
233
|
+
export declare function missingOrdinals(total: number, received: number[]): number[];
|
|
198
234
|
export {};
|
package/dist/workflow/manager.js
CHANGED
|
@@ -5,11 +5,11 @@ 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 {
|
|
8
|
+
import { FLOW_C_CODEX_MODEL, FLOW_C_CODEX_REASONING_EFFORT, FLOW_C_CODEX_WORKER_CONCURRENCY, flowCCodexWorkerStatus, runCodexWorkflowTurn } 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
|
-
import { FLOW_C_SCRIPT_CHUNK_MAX, flowCScriptChunkSizes } from "./constants.js";
|
|
12
|
+
import { FLOW_C_SCRIPT_CHUNK_MAX, flowCScriptChunks, 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;
|
|
@@ -27,6 +27,7 @@ export class WorkflowManager {
|
|
|
27
27
|
this.config = config;
|
|
28
28
|
this.emit = emit;
|
|
29
29
|
for (const record of Object.values(this.state.scripts)) {
|
|
30
|
+
record.activeChunks = 0;
|
|
30
31
|
if (record.status === "queued" || record.status === "running")
|
|
31
32
|
this.scheduleScript(record.id);
|
|
32
33
|
}
|
|
@@ -47,6 +48,8 @@ export class WorkflowManager {
|
|
|
47
48
|
threadId: previous?.threadId,
|
|
48
49
|
expiresAt: String(input.expiresAt || previous?.expiresAt || "") || undefined,
|
|
49
50
|
attempts: previous?.attempts || 0,
|
|
51
|
+
chunkSize: previous?.chunkSize,
|
|
52
|
+
activeChunks: 0,
|
|
50
53
|
priorityAt: now(),
|
|
51
54
|
message: previous?.status === "complete" ? previous.message : "已进入本机 Codex 队列",
|
|
52
55
|
updatedAt: now(),
|
|
@@ -59,6 +62,7 @@ export class WorkflowManager {
|
|
|
59
62
|
const id = workflowId(idValue, "脚本交接 ID");
|
|
60
63
|
const record = this.scriptRecord(id);
|
|
61
64
|
record.status = "queued";
|
|
65
|
+
record.activeChunks = 0;
|
|
62
66
|
// A failed manual attempt may leave a very large or interrupted thread.
|
|
63
67
|
// Retrying in a fresh thread avoids carrying that damaged context forward.
|
|
64
68
|
delete record.threadId;
|
|
@@ -93,7 +97,7 @@ export class WorkflowManager {
|
|
|
93
97
|
const jobs = jobsValue;
|
|
94
98
|
const data = await commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/draft-chunks`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ jobs }) });
|
|
95
99
|
if (Array.isArray(data.receivedOrdinals))
|
|
96
|
-
record.receivedOrdinals = data.receivedOrdinals.map(Number).filter(Number.isInteger).sort((left, right) => left - right);
|
|
100
|
+
record.receivedOrdinals = [...new Set([...record.receivedOrdinals, ...data.receivedOrdinals.map(Number).filter(Number.isInteger)])].sort((left, right) => left - right);
|
|
97
101
|
else {
|
|
98
102
|
const accepted = new Set(record.receivedOrdinals);
|
|
99
103
|
for (const job of jobs)
|
|
@@ -117,8 +121,11 @@ export class WorkflowManager {
|
|
|
117
121
|
}
|
|
118
122
|
health() {
|
|
119
123
|
const records = Object.values(this.state.scripts);
|
|
124
|
+
const workers = flowCCodexWorkerStatus();
|
|
120
125
|
return {
|
|
121
126
|
activeScriptHandoffIds: [...this.runningScripts],
|
|
127
|
+
activeScripts: workers.active,
|
|
128
|
+
scriptConcurrencyLimit: workers.limit,
|
|
122
129
|
queuedScripts: records.filter((record) => record.status === "queued" || record.status === "running").length,
|
|
123
130
|
blockedScripts: records.filter((record) => record.status === "error").length,
|
|
124
131
|
};
|
|
@@ -230,57 +237,36 @@ export class WorkflowManager {
|
|
|
230
237
|
if (Date.parse(task.expires_at) <= Date.now())
|
|
231
238
|
throw new ExpiredCapabilityError("脚本交接已过期,请在网页重新点击交给本机 Codex");
|
|
232
239
|
const workspace = ensureSiteWorkspace(this.config);
|
|
233
|
-
if (!record.threadId) {
|
|
234
|
-
const thread = await startCodexThread(this.emit, workspace.workspacePath, "full");
|
|
235
|
-
record.threadId = String(thread.id || "");
|
|
236
|
-
this.save();
|
|
237
|
-
}
|
|
238
240
|
const durationSeconds = Number(task.duration_seconds || 10);
|
|
239
241
|
const chunkSizes = flowCScriptChunkSizes(durationSeconds);
|
|
242
|
+
record.activeChunks = 0;
|
|
243
|
+
let chunkSizeIndex = 0;
|
|
240
244
|
while (record.receivedOrdinals.length < task.requested_count) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
245
|
+
const before = record.receivedOrdinals.length;
|
|
246
|
+
const missing = missingOrdinals(task.requested_count, record.receivedOrdinals);
|
|
247
|
+
const chunkSize = chunkSizes[chunkSizeIndex];
|
|
248
|
+
const chunks = flowCScriptChunks(durationSeconds, missing, chunkSize);
|
|
249
|
+
record.chunkSize = chunkSize;
|
|
250
|
+
record.attempts += chunks.length;
|
|
251
|
+
record.message = `本机 Codex 正在用 ${FLOW_C_CODEX_WORKER_CONCURRENCY} 个受控 worker 写 ${chunks.length} 个独立子批(${before}/${task.requested_count})`;
|
|
252
|
+
record.updatedAt = now();
|
|
253
|
+
this.save();
|
|
254
|
+
const results = await Promise.all(chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
|
|
255
|
+
await this.scriptTask(id);
|
|
256
|
+
const progressed = record.receivedOrdinals.length > before;
|
|
257
|
+
if (progressed) {
|
|
258
|
+
chunkSizeIndex = 0;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const lastError = results.map((result) => result.error).filter(Boolean).at(-1) || "Codex 未返回可用的结构化脚本";
|
|
262
|
+
if (chunkSizeIndex < chunkSizes.length - 1) {
|
|
263
|
+
chunkSizeIndex += 1;
|
|
264
|
+
record.message = `未成功回传,正在把缺失 ordinal 缩小为每段 ${chunkSizes[chunkSizeIndex]} 条并行重试`;
|
|
254
265
|
record.updatedAt = now();
|
|
255
266
|
this.save();
|
|
256
|
-
|
|
257
|
-
await this.scriptTask(id);
|
|
258
|
-
progressed = record.receivedOrdinals.length > before;
|
|
259
|
-
if (!progressed && result.ok && result.text) {
|
|
260
|
-
try {
|
|
261
|
-
await this.submitScriptChunk(id, parseFlowCScriptOutput(result.text, missing));
|
|
262
|
-
await this.scriptTask(id);
|
|
263
|
-
progressed = record.receivedOrdinals.length > before;
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
lastError = error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验";
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (progressed)
|
|
270
|
-
break;
|
|
271
|
-
// Keep provider/client details out of the browser-facing state. The
|
|
272
|
-
// underlying Codex runner already records the technical error locally.
|
|
273
|
-
lastError ||= result.ok ? "Codex 未返回可用的结构化脚本" : "Codex 本轮执行失败";
|
|
274
|
-
if (attemptIndex < chunkSizes.length - 1) {
|
|
275
|
-
const nextSize = chunkSizes[attemptIndex + 1];
|
|
276
|
-
record.message = `第 ${lastRange} 条未成功回传,正在换新会话并缩小为每段 ${nextSize} 条重试`;
|
|
277
|
-
const thread = await startCodexThread(this.emit, workspace.workspacePath, "full");
|
|
278
|
-
record.threadId = String(thread.id || "");
|
|
279
|
-
this.save();
|
|
280
|
-
}
|
|
267
|
+
continue;
|
|
281
268
|
}
|
|
282
|
-
|
|
283
|
-
throw new Error(`本机 Codex 已自动换会话并降级到每段 ${chunkSizes.at(-1)} 条,仍未回传第 ${lastRange} 条${lastError ? `(${lastError})` : ""},请点击重试`);
|
|
269
|
+
throw new Error(`本机 Codex 已自动隔离失败 ordinal 并降级到每段 ${chunkSizes.at(-1)} 条,仍未回传缺失脚本(${lastError}),请点击重试`);
|
|
284
270
|
}
|
|
285
271
|
const finalTask = await this.scriptTask(id);
|
|
286
272
|
if (finalTask.status === "ready" && record.receivedOrdinals.length === task.requested_count) {
|
|
@@ -297,10 +283,72 @@ export class WorkflowManager {
|
|
|
297
283
|
}
|
|
298
284
|
finally {
|
|
299
285
|
record.updatedAt = now();
|
|
286
|
+
record.activeChunks = 0;
|
|
300
287
|
this.save();
|
|
301
288
|
this.runningScripts.delete(id);
|
|
302
289
|
}
|
|
303
290
|
}
|
|
291
|
+
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
292
|
+
async runScriptChunk(id, task, ordinals, cwd) {
|
|
293
|
+
const durationSeconds = Number(task.duration_seconds || 10);
|
|
294
|
+
const result = await runCodexWorkflowTurn(scriptChunkPrompt(id, task, ordinals), this.emit, {
|
|
295
|
+
cwd,
|
|
296
|
+
permissionMode: "full",
|
|
297
|
+
timeoutMs: FLOW_C_CODEX_TURN_TIMEOUT_MS,
|
|
298
|
+
outputSchema: flowCScriptOutputSchema(durationSeconds, ordinals.length),
|
|
299
|
+
onThread: (threadId) => { this.scriptRecord(id).threadId = threadId; this.save(); },
|
|
300
|
+
onWorkerStart: () => { const record = this.scriptRecord(id); record.activeChunks = Number(record.activeChunks || 0) + 1; record.updatedAt = now(); this.save(); },
|
|
301
|
+
onWorkerFinish: () => { const record = this.scriptRecord(id); record.activeChunks = Math.max(0, Number(record.activeChunks || 0) - 1); record.updatedAt = now(); this.save(); },
|
|
302
|
+
});
|
|
303
|
+
this.emitScriptStage(id, ordinals, "queue_wait", result.timings.queueWaitMs);
|
|
304
|
+
this.emitScriptStage(id, ordinals, "thread_start", result.timings.threadStartMs);
|
|
305
|
+
this.emitScriptStage(id, ordinals, "model", result.timings.modelMs);
|
|
306
|
+
if (!result.ok || !result.text)
|
|
307
|
+
return { error: result.ok ? "Codex 未返回可用的结构化脚本" : result.error };
|
|
308
|
+
try {
|
|
309
|
+
const parseStartedAt = Date.now();
|
|
310
|
+
const jobs = parseFlowCScriptOutput(result.text, ordinals);
|
|
311
|
+
this.emitScriptStage(id, ordinals, "parse", Date.now() - parseStartedAt);
|
|
312
|
+
const persistStartedAt = Date.now();
|
|
313
|
+
await this.submitGeneratedScriptJobs(id, jobs);
|
|
314
|
+
this.emitScriptStage(id, ordinals, "persist", Date.now() - persistStartedAt);
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return { error: error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验" };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** 只记录阶段、ordinal 和毫秒数;禁止记录 prompt、令牌或中心能力。 */
|
|
322
|
+
emitScriptStage(handoffId, ordinals, stage, durationMs) {
|
|
323
|
+
this.emit("agent_event", { agent: "codex", type: "workflow.script.stage", handoffId, ordinals, stage, duration_ms: Math.max(0, Math.round(durationMs)), model: FLOW_C_CODEX_MODEL, reasoning: FLOW_C_CODEX_REASONING_EFFORT });
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 中心批量校验失败时逐条补交,避免一条格式异常拖累同轮其他有效脚本。
|
|
327
|
+
* 已接纳 ordinal 由中心持久化,下一轮只会继续缺失项。
|
|
328
|
+
*/
|
|
329
|
+
async submitGeneratedScriptJobs(id, jobs) {
|
|
330
|
+
try {
|
|
331
|
+
await this.submitScriptChunk(id, jobs);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (jobs.length === 1)
|
|
336
|
+
throw error;
|
|
337
|
+
let accepted = 0;
|
|
338
|
+
let lastError = error;
|
|
339
|
+
for (const job of jobs) {
|
|
340
|
+
try {
|
|
341
|
+
await this.submitScriptChunk(id, [job]);
|
|
342
|
+
accepted += 1;
|
|
343
|
+
}
|
|
344
|
+
catch (jobError) {
|
|
345
|
+
lastError = jobError;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!accepted)
|
|
349
|
+
throw lastError;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
304
352
|
async syncDownloads(onlyBatchId) {
|
|
305
353
|
if (!this.state.downloadDirectory)
|
|
306
354
|
return;
|
|
@@ -439,44 +487,44 @@ export function compareScriptQueueRecords(left, right) {
|
|
|
439
487
|
return String(right.priorityAt || "").localeCompare(String(left.priorityAt || ""));
|
|
440
488
|
return left.updatedAt.localeCompare(right.updatedAt);
|
|
441
489
|
}
|
|
442
|
-
async function runCodexScriptWithTimeout(prompt, emit, options) {
|
|
443
|
-
let timer;
|
|
444
|
-
const turn = runCodexTurn(prompt, emit, [], options);
|
|
445
|
-
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), FLOW_C_CODEX_TURN_TIMEOUT_MS); });
|
|
446
|
-
try {
|
|
447
|
-
const result = await Promise.race([turn, timeout]);
|
|
448
|
-
if (result !== "timeout")
|
|
449
|
-
return result;
|
|
450
|
-
await restartCodexApp("Flow C 脚本回合超过 8 分钟,已自动终止并换新会话");
|
|
451
|
-
await turn;
|
|
452
|
-
return { ok: false, error: "Flow C 脚本回合超过 8 分钟,已自动终止并换新会话" };
|
|
453
|
-
}
|
|
454
|
-
finally {
|
|
455
|
-
if (timer)
|
|
456
|
-
clearTimeout(timer);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
490
|
class ExpiredCapabilityError extends Error {
|
|
460
491
|
}
|
|
461
|
-
function scriptChunkPrompt(id, task, ordinals) {
|
|
492
|
+
export function scriptChunkPrompt(id, task, ordinals) {
|
|
462
493
|
const duration = Number(task.duration_seconds || 10);
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
494
|
+
const layoutVersion = task.storyboard_layout_version || task.storyboardLayoutVersion || "hybrid-three-anchor-v1";
|
|
495
|
+
const styleCard = compactReferenceStyleCard(task.reference_style_card || task.referenceStyleCard);
|
|
496
|
+
const durationRules = duration === 10 ? `
|
|
497
|
+
本任务每条成片为 10 秒,不生成 masterScript。每条只输出一份 openingState 和一个完整 0–10 秒 segment;Agent 会从 structured shots 确定性渲染兼容顶层 script,并按 shots 顺序派生 segmentVoiceovers,模型不要生成这两个派生字段。` : `
|
|
498
|
+
本任务每条成片为 ${duration} 秒。先创作一份完整连贯的 masterScript,再严格展开成 ${duration / 10} 个独立的 0–10 秒 segments;绝不能写 10–20、20–30 秒,也不能让局部段携带总片时轴。
|
|
499
|
+
每条只需回传一份 masterScript、openingState 和完整 segments;不要输出重复的顶层 script 或 segmentVoiceovers。Agent 会从 masterScript 补顶层兼容 script、按 shots 派生 segmentVoiceovers,并把 openingState 变成首段起点、用前一段 endingState 确定性补齐后一段 continuity。`;
|
|
467
500
|
return `你正在后台处理“抖音小辉跨境工具”Flow C 脚本交接 ${id}。
|
|
468
501
|
完整中心任务已经附在本提示词末尾。只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
|
|
469
502
|
本段 productIndex 必须严格按此映射填写:${scriptProductAssignments(task.product_quantities, ordinals)}。不得凭产品名称猜测或把相邻产品编号混用。
|
|
470
|
-
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地语言与偏快但清晰的短视频节奏。${
|
|
503
|
+
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地语言与偏快但清晰的短视频节奏。${durationRules}
|
|
504
|
+
每条先按“画面先行”写 creativePlan,再写本段/总脚本。creativePlan 必须给出 visualHook、conflict、productIntervention、visibleProof、callbackMotivation、truthBoundary、differentiationKey,并在 escalation 或 turn 中至少给出一项真实成立的升级/转折;format 必须依据当前产品、市场、事实证据和真实场景适配选择。中国短剧式冲突/喜剧只能是适合时的实验路线,绝不是默认;工厂、仓库、超市等条件场景也只有在商品动作自然且不伪造来源、库存或销量证据时才可选,不能固定轮换。
|
|
505
|
+
styleCardSummary 只使用已缓存的压缩风格卡,没有就明确写“无”;不要分析、读取或转录原始参考视频。每条只生成一份 voiceProfile(性别、年龄感、音高、音质、语速、口音、停顿习惯、情绪基调),所有局部段共同引用它;每段仅用简短 voiceCue 表达情绪变化,不复写整套音色。
|
|
506
|
+
每个 segment 必须有完整 0–10 秒 shots、voiceCue 和 endingState;不要让模型生成 continuityMode 或 continuity。每个 shot 都包含画面、准确本地口播、可控屏幕字、商品证据、soundBgm 与 emotionalNote。qualityGate 必须逐项用具体证据核对强钩子、冲突、升级/转折、商品介入、可见证明、开头回扣购买动机、事实边界与批内差异,禁止用“已满足”空话。
|
|
507
|
+
每条 differentiationKey 必须在带货形式、第一眼画面、剧情骨架、痛点/反差、商品证明和口播气质上与本批其他条目显著不同,不能只换措辞。
|
|
508
|
+
本脚本回合固定使用 GPT-5.6 Terra 高推理;不要建议换低档模型或降低推理。故事版布局由中心顶层 storyboardLayoutVersion=${layoutVersion} 管理,不要让模型选择,也不要把它重复进每条 job。
|
|
509
|
+
【已缓存压缩的参考 style card】${styleCard || "无参考风格卡"}【style card 结束】
|
|
471
510
|
每一个 ordinal 都必须从头到尾创作一份完整、独立的原生带货脚本,画面和口播必须在同一份脚本中一起独立构思。先让它成为一个当地真人在具体生活情境里自然发现、吐槽、试用或验证商品的短内容,再完成带货;禁止写成品牌广告片、棚拍宣传片、电视购物、逐条念卖点或全程完美对镜讲解。独立脚本天然包含独立口播:不得把任何一份口播当作整批公共模板,不得复用完整台词,也不得只换人物、场景或少数词后保留近似口播。音色身份可以为同一人物保持一致,但每条的 Hook、人物行动、证明表达和 CTA 都必须重新写。
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
只返回符合当前结构化输出契约的 jobs;每条必须包含 ordinal、productIndex、sellingFormId 和 script,长视频还必须包含 masterScript 与完整 segments。无需调用 MCP,Agent 会在本机校验后自动回传。不要创建付费批次,不要调用供应商模型。
|
|
511
|
+
用户没有指定带货方向时,按当前商品可见卖点、自然使用环境、目标市场表达、事实边界和批内差异动态选择;有条件证据门的形式只有中心任务已提供相应真实证据时才能采用。
|
|
512
|
+
只返回符合当前结构化输出契约的 jobs;每条都包含 ordinal、productIndex、sellingFormId、creativePlan、voiceProfile、qualityGate、openingState 和结构化局部段。10 秒只返回一个 segment;20/30 秒另含 masterScript 与完整 segments。兼容 script、continuity 和 segmentVoiceovers 全由 Agent 确定性补齐。无需调用 MCP,Agent 会在本机校验后自动回传。不要创建付费批次,不要调用供应商模型。
|
|
475
513
|
|
|
476
514
|
【中心任务完整 instructions】
|
|
477
515
|
${task.instructions}
|
|
478
516
|
【中心任务 instructions 结束】`;
|
|
479
517
|
}
|
|
518
|
+
function compactReferenceStyleCard(value) {
|
|
519
|
+
if (!value || typeof value !== "object")
|
|
520
|
+
return "";
|
|
521
|
+
const fields = ["openingComposition", "visualDensity", "conflictContrast", "characterProductEntrance", "rhythm", "turn", "proofMethod", "voiceTone", "categoryFit"];
|
|
522
|
+
const compact = Object.fromEntries(fields.flatMap((field) => {
|
|
523
|
+
const text = String(value[field] || "").trim().replace(/\s+/g, " ").slice(0, 400);
|
|
524
|
+
return text ? [[field, text]] : [];
|
|
525
|
+
}));
|
|
526
|
+
return Object.keys(compact).length ? JSON.stringify(compact) : "";
|
|
527
|
+
}
|
|
480
528
|
function scriptProductAssignments(productQuantities, ordinals) {
|
|
481
529
|
const assignments = [];
|
|
482
530
|
let first = ordinals[0];
|
|
@@ -504,12 +552,12 @@ function productIndexForOrdinal(productQuantities, ordinal) {
|
|
|
504
552
|
return -1;
|
|
505
553
|
}
|
|
506
554
|
function publicScript(record) {
|
|
507
|
-
return { id: record.id, status: record.status, requestedCount: record.requestedCount, received: record.receivedOrdinals.length, threadId: record.threadId, message: record.message, expiresAt: record.expiresAt, updatedAt: record.updatedAt };
|
|
555
|
+
return { id: record.id, status: record.status, requestedCount: record.requestedCount, received: record.receivedOrdinals.length, threadId: record.threadId, chunkSize: record.chunkSize || null, activeChunks: Number(record.activeChunks || 0), message: record.message, expiresAt: record.expiresAt, updatedAt: record.updatedAt };
|
|
508
556
|
}
|
|
509
557
|
function publicDownload(record) {
|
|
510
558
|
return { batchId: record.batchId, status: record.status, downloaded: record.downloadedOrdinals.length, market: record.market, message: record.message, expiresAt: record.expiresAt, updatedAt: record.updatedAt };
|
|
511
559
|
}
|
|
512
|
-
function missingOrdinals(total, received) { const set = new Set(received); return Array.from({ length: total }, (_, index) => index + 1).filter((ordinal) => !set.has(ordinal)); }
|
|
560
|
+
export function missingOrdinals(total, received) { const set = new Set(received); return Array.from({ length: total }, (_, index) => index + 1).filter((ordinal) => !set.has(ordinal)); }
|
|
513
561
|
function workflowId(value, label) { const id = String(value || "").trim(); if (!/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(id))
|
|
514
562
|
throw new Error(`${label}无效`); return id; }
|
|
515
563
|
function secret(value, label) { const token = String(value || "").trim(); if (token.length < 32 || token.length > 512)
|
|
@@ -6,10 +6,52 @@ function object(properties, required = Object.keys(properties)) {
|
|
|
6
6
|
function continuitySchema(frameField) {
|
|
7
7
|
return object(Object.fromEntries([...continuityFields, frameField].map((field) => [field, text])));
|
|
8
8
|
}
|
|
9
|
+
function openingStateSchema() {
|
|
10
|
+
return object(Object.fromEntries([...continuityFields, "openingFrame"].map((field) => [field, text])));
|
|
11
|
+
}
|
|
12
|
+
function voiceProfileSchema() {
|
|
13
|
+
return object({
|
|
14
|
+
gender: text,
|
|
15
|
+
ageImpression: text,
|
|
16
|
+
pitch: text,
|
|
17
|
+
timbre: text,
|
|
18
|
+
speakingRate: text,
|
|
19
|
+
accent: text,
|
|
20
|
+
pauseHabit: text,
|
|
21
|
+
emotionalBaseline: text,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function creativePlanSchema() {
|
|
25
|
+
const properties = {
|
|
26
|
+
format: text,
|
|
27
|
+
visualHook: text,
|
|
28
|
+
conflict: text,
|
|
29
|
+
escalation: text,
|
|
30
|
+
turn: text,
|
|
31
|
+
productIntervention: text,
|
|
32
|
+
visibleProof: text,
|
|
33
|
+
callbackMotivation: text,
|
|
34
|
+
truthBoundary: text,
|
|
35
|
+
differentiationKey: text,
|
|
36
|
+
styleCardSummary: text,
|
|
37
|
+
};
|
|
38
|
+
return object(properties, Object.keys(properties).filter((field) => !["escalation", "turn"].includes(field)));
|
|
39
|
+
}
|
|
40
|
+
function qualityGateSchema() {
|
|
41
|
+
return object({
|
|
42
|
+
hook: text,
|
|
43
|
+
conflict: text,
|
|
44
|
+
escalationOrTurn: text,
|
|
45
|
+
productIntervention: text,
|
|
46
|
+
visibleProof: text,
|
|
47
|
+
callbackMotivation: text,
|
|
48
|
+
truthBoundary: text,
|
|
49
|
+
batchDifferentiation: text,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
9
52
|
function segmentSchema() {
|
|
10
53
|
return object({
|
|
11
|
-
|
|
12
|
-
continuity: continuitySchema("previousEndingFrame"),
|
|
54
|
+
voiceCue: text,
|
|
13
55
|
endingState: continuitySchema("endingFrame"),
|
|
14
56
|
shots: {
|
|
15
57
|
type: "array",
|
|
@@ -22,6 +64,8 @@ function segmentSchema() {
|
|
|
22
64
|
voiceover: text,
|
|
23
65
|
onScreenText: text,
|
|
24
66
|
evidence: text,
|
|
67
|
+
soundBgm: text,
|
|
68
|
+
emotionalNote: text,
|
|
25
69
|
}),
|
|
26
70
|
},
|
|
27
71
|
});
|
|
@@ -32,9 +76,14 @@ export function flowCScriptOutputSchema(durationSeconds, count) {
|
|
|
32
76
|
ordinal: { type: "integer", minimum: 1 },
|
|
33
77
|
productIndex: { type: "integer", minimum: 0 },
|
|
34
78
|
sellingFormId: text,
|
|
35
|
-
|
|
79
|
+
creativePlan: creativePlanSchema(),
|
|
80
|
+
voiceProfile: voiceProfileSchema(),
|
|
81
|
+
qualityGate: qualityGateSchema(),
|
|
82
|
+
openingState: openingStateSchema(),
|
|
36
83
|
};
|
|
37
|
-
if (durationSeconds
|
|
84
|
+
if (durationSeconds === 10)
|
|
85
|
+
properties.segment = segmentSchema();
|
|
86
|
+
else {
|
|
38
87
|
properties.masterScript = { type: "string", minLength: 40 };
|
|
39
88
|
properties.segments = { type: "array", minItems: durationSeconds / 10, maxItems: durationSeconds / 10, items: segmentSchema() };
|
|
40
89
|
}
|
|
@@ -43,12 +92,18 @@ export function flowCScriptOutputSchema(durationSeconds, count) {
|
|
|
43
92
|
export function parseFlowCScriptOutput(value, expectedOrdinals) {
|
|
44
93
|
const source = String(value || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
45
94
|
const parsed = JSON.parse(source);
|
|
46
|
-
if (!Array.isArray(parsed.jobs) || parsed.jobs.length
|
|
47
|
-
throw new Error("Codex
|
|
48
|
-
const
|
|
49
|
-
|
|
95
|
+
if (!Array.isArray(parsed.jobs) || !parsed.jobs.length)
|
|
96
|
+
throw new Error("Codex 没有返回脚本");
|
|
97
|
+
const expected = new Set(expectedOrdinals);
|
|
98
|
+
const accepted = new Map();
|
|
99
|
+
for (const job of parsed.jobs) {
|
|
100
|
+
const ordinal = Number(job?.ordinal);
|
|
101
|
+
if (Number.isInteger(ordinal) && expected.has(ordinal) && !accepted.has(ordinal))
|
|
102
|
+
accepted.set(ordinal, job);
|
|
103
|
+
}
|
|
104
|
+
if (!accepted.size)
|
|
50
105
|
throw new Error("Codex 返回的 ordinal 与当前分段不一致");
|
|
51
|
-
return
|
|
106
|
+
return expectedOrdinals.flatMap((ordinal) => accepted.has(ordinal) ? [canonicalizeSegmentContinuity(accepted.get(ordinal))] : []);
|
|
52
107
|
}
|
|
53
108
|
/**
|
|
54
109
|
* 将后一段的起点锁定为前一段的终点。模型常会用语义相同但措辞不同的
|
|
@@ -57,14 +112,24 @@ export function parseFlowCScriptOutput(value, expectedOrdinals) {
|
|
|
57
112
|
*/
|
|
58
113
|
function canonicalizeSegmentContinuity(jobValue) {
|
|
59
114
|
const job = recordOf(jobValue);
|
|
60
|
-
if (!job
|
|
115
|
+
if (!job)
|
|
61
116
|
return jobValue;
|
|
117
|
+
const openingState = recordOf(job.openingState);
|
|
118
|
+
if (!Array.isArray(job.segments)) {
|
|
119
|
+
const segment = recordOf(job.segment);
|
|
120
|
+
if (!segment)
|
|
121
|
+
return jobValue;
|
|
122
|
+
const canonical = { ...segment, continuityMode: "reset", continuity: continuityFromOpeningState(openingState) };
|
|
123
|
+
const rendered = withLegacySegmentScript(canonical, 0, 1, job.voiceProfile);
|
|
124
|
+
const { openingState: _openingState, segment: _segment, ...persistedJob } = job;
|
|
125
|
+
return { ...persistedJob, script: recordOf(rendered)?.script, segmentVoiceovers: [segmentVoiceoverLines(canonical)], segment: rendered };
|
|
126
|
+
}
|
|
62
127
|
const segments = job.segments.map((segmentValue, index, source) => {
|
|
63
128
|
const segment = recordOf(segmentValue);
|
|
64
129
|
if (!segment)
|
|
65
130
|
return segmentValue;
|
|
66
131
|
if (index === 0)
|
|
67
|
-
return { ...segment, continuityMode: "reset" };
|
|
132
|
+
return { ...segment, continuityMode: "reset", continuity: continuityFromOpeningState(openingState) };
|
|
68
133
|
const previous = recordOf(source[index - 1]);
|
|
69
134
|
const endingState = recordOf(previous?.endingState);
|
|
70
135
|
if (!endingState)
|
|
@@ -73,7 +138,36 @@ function canonicalizeSegmentContinuity(jobValue) {
|
|
|
73
138
|
continuity.previousEndingFrame = endingState.endingFrame;
|
|
74
139
|
return { ...segment, continuityMode: "continue", continuity };
|
|
75
140
|
});
|
|
76
|
-
|
|
141
|
+
const { openingState: _openingState, ...persistedJob } = job;
|
|
142
|
+
return { ...persistedJob, script: job.masterScript || job.script, segmentVoiceovers: segments.map(segmentVoiceoverLines), segments: segments.map((segment, index) => withLegacySegmentScript(segment, index, segments.length, job.voiceProfile)) };
|
|
143
|
+
}
|
|
144
|
+
function segmentVoiceoverLines(segmentValue) {
|
|
145
|
+
const segment = recordOf(segmentValue);
|
|
146
|
+
const shots = Array.isArray(segment?.shots) ? segment.shots.map(recordOf).filter((shot) => Boolean(shot)) : [];
|
|
147
|
+
return shots.map((shot) => String(shot.voiceover || "").trim()).filter((line) => line && !/^(none|无|sin voz|sin diálogo)$/i.test(line));
|
|
148
|
+
}
|
|
149
|
+
function continuityFromOpeningState(openingState) {
|
|
150
|
+
const continuity = Object.fromEntries(continuityFields.map((field) => [field, openingState?.[field] || "由本段首镜建立"]));
|
|
151
|
+
continuity.previousEndingFrame = openingState?.openingFrame || "本段首镜";
|
|
152
|
+
return continuity;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 正式站可能仍运行只接收 segment.script 的旧协议。脚本正文直接由同一份
|
|
156
|
+
* structured shots/continuity 渲染,既不要求模型重复输出,也不改变新协议内容。
|
|
157
|
+
*/
|
|
158
|
+
function withLegacySegmentScript(segmentValue, segmentIndex, segmentCount, voiceProfileValue) {
|
|
159
|
+
const segment = recordOf(segmentValue);
|
|
160
|
+
const continuity = recordOf(segment?.continuity);
|
|
161
|
+
const endingState = recordOf(segment?.endingState);
|
|
162
|
+
const voiceProfile = recordOf(voiceProfileValue);
|
|
163
|
+
const shots = Array.isArray(segment?.shots) ? segment.shots.map(recordOf).filter((shot) => Boolean(shot)) : [];
|
|
164
|
+
if (!segment || !continuity || !endingState || !shots.length)
|
|
165
|
+
return segmentValue;
|
|
166
|
+
const context = `人物:${continuity.character};服装:${continuity.wardrobe};地点:${continuity.location};光线:${continuity.lighting};商品状态:${continuity.productState};承接动作:${continuity.unfinishedAction};本段目标:${continuity.nextGoal};上一段结尾画面:${continuity.previousEndingFrame}`;
|
|
167
|
+
const ending = `人物:${endingState.character};服装:${endingState.wardrobe};地点:${endingState.location};光线:${endingState.lighting};商品状态:${endingState.productState};未完成动作:${endingState.unfinishedAction};下一段目标:${endingState.nextGoal};本段结尾画面:${endingState.endingFrame}`;
|
|
168
|
+
const voice = voiceProfile ? `性别:${voiceProfile.gender};年龄感:${voiceProfile.ageImpression};音高:${voiceProfile.pitch};音质:${voiceProfile.timbre};语速:${voiceProfile.speakingRate};口音:${voiceProfile.accent};停顿习惯:${voiceProfile.pauseHabit};情绪基调:${voiceProfile.emotionalBaseline}` : "沿用本条共同音色档案";
|
|
169
|
+
const timeline = shots.map((shot, index) => `镜头 ${index + 1}|${shot.startSeconds}–${shot.endSeconds} 秒|画面:${shot.visual}|口播:${shot.voiceover}|屏幕字:${shot.onScreenText}|证据:${shot.evidence}|声音/BGM:${shot.soundBgm}|情绪:${shot.emotionalNote}`).join("\n");
|
|
170
|
+
return { ...segment, script: `Flow C 独立分段 ${segmentIndex + 1}/${segmentCount}\n局部时轴:0–10 秒;不得引用或绘制总片时间轴。\n共同音色档案:${voice}\n本段音色提示:${segment.voiceCue}\n本段起始连续性:${context}\n${timeline}\n本段结束状态:${ending}` };
|
|
77
171
|
}
|
|
78
172
|
function recordOf(value) {
|
|
79
173
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|