@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.
- package/dist/agent/codex-client.d.ts +5 -1
- package/dist/agent/codex-client.js +26 -8
- package/dist/agent/codex-protocol.d.ts +1 -0
- package/dist/agent/codex.d.ts +4 -0
- package/dist/agent/codex.js +12 -3
- package/dist/index.js +4 -0
- package/dist/server/http.js +1 -1
- package/dist/workflow/manager.d.ts +21 -0
- package/dist/workflow/manager.js +55 -6
- package/dist/workflow/script-output.d.ts +5 -0
- package/dist/workflow/script-output.js +52 -0
- package/package.json +1 -1
|
@@ -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<
|
|
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(
|
|
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 = "";
|
package/dist/agent/codex.d.ts
CHANGED
|
@@ -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。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -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
|
package/dist/server/http.js
CHANGED
|
@@ -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 {};
|
package/dist/workflow/manager.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|