@xiaohhhh1/canvas-agent 0.4.42 → 0.4.43
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 +3 -1
- package/dist/agent/codex-client.js +15 -8
- package/dist/agent/codex-protocol.d.ts +1 -0
- package/dist/agent/codex.d.ts +2 -0
- package/dist/agent/codex.js +4 -3
- package/dist/workflow/manager.js +19 -4
- 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,7 +43,7 @@ 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>;
|
|
47
49
|
/** 回复网页端已经确认的 Codex 权限请求。 */
|
|
@@ -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() {
|
|
@@ -235,6 +236,9 @@ export class CodexAppClient {
|
|
|
235
236
|
const streamedText = this.textByItem.get(id);
|
|
236
237
|
if (item?.type === "agent_message" && streamedText && !item.text)
|
|
237
238
|
item.text = streamedText;
|
|
239
|
+
const turnId = String(field(params, "turnId") || "");
|
|
240
|
+
if (turnId && item?.type === "agent_message" && item.text)
|
|
241
|
+
this.finalTextByTurn.set(turnId, String(item.text));
|
|
238
242
|
if (id)
|
|
239
243
|
this.textByItem.delete(id);
|
|
240
244
|
}
|
|
@@ -253,12 +257,14 @@ export class CodexAppClient {
|
|
|
253
257
|
const turnId = turn.id;
|
|
254
258
|
const pending = this.activeTurns.get(turnId);
|
|
255
259
|
const error = turn.error;
|
|
260
|
+
const text = this.finalTextByTurn.get(turnId) || "";
|
|
261
|
+
this.finalTextByTurn.delete(turnId);
|
|
256
262
|
if (pending) {
|
|
257
263
|
this.activeTurns.delete(turnId);
|
|
258
|
-
error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(
|
|
264
|
+
error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(text);
|
|
259
265
|
}
|
|
260
266
|
else if (turnId) {
|
|
261
|
-
this.completedTurns.set(turnId, error ? new Error(error.message || "Codex turn failed") : null);
|
|
267
|
+
this.completedTurns.set(turnId, { error: error ? new Error(error.message || "Codex turn failed") : null, text });
|
|
262
268
|
}
|
|
263
269
|
if (turnId === this.currentTurnId) {
|
|
264
270
|
this.currentThreadId = "";
|
|
@@ -328,6 +334,7 @@ export class CodexAppClient {
|
|
|
328
334
|
this.activeTurns.clear();
|
|
329
335
|
this.pendingDeltas.clear();
|
|
330
336
|
this.textByItem.clear();
|
|
337
|
+
this.finalTextByTurn.clear();
|
|
331
338
|
this.approvalRequests.clear();
|
|
332
339
|
this.currentThreadId = "";
|
|
333
340
|
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;
|
package/dist/agent/codex.js
CHANGED
|
@@ -103,7 +103,8 @@ async function runCodexTurnNow(prompt, emit, attachments, options) {
|
|
|
103
103
|
options.onThread?.(threadId);
|
|
104
104
|
unmaterializedThreadIds.delete(threadId);
|
|
105
105
|
try {
|
|
106
|
-
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
|
|
106
|
+
const text = await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn, options.outputSchema);
|
|
107
|
+
return { ok: true, text };
|
|
107
108
|
}
|
|
108
109
|
catch (error) {
|
|
109
110
|
if (!isRecoverableThreadError(error))
|
|
@@ -113,9 +114,9 @@ async function runCodexTurnNow(prompt, emit, attachments, options) {
|
|
|
113
114
|
threadId = await ensureCodexThread(app, { cwd: options.cwd }, emit);
|
|
114
115
|
options.onThread?.(threadId);
|
|
115
116
|
unmaterializedThreadIds.delete(threadId);
|
|
116
|
-
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
|
|
117
|
+
const text = await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn, options.outputSchema);
|
|
118
|
+
return { ok: true, text };
|
|
117
119
|
}
|
|
118
|
-
return { ok: true };
|
|
119
120
|
}
|
|
120
121
|
catch (error) {
|
|
121
122
|
logger.error("Codex turn failed", error);
|
package/dist/workflow/manager.js
CHANGED
|
@@ -10,6 +10,7 @@ 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");
|
|
14
15
|
/** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
|
|
15
16
|
export class WorkflowManager {
|
|
@@ -235,14 +236,24 @@ export class WorkflowManager {
|
|
|
235
236
|
record.message = `本机 Codex 正在写第 ${missing[0]}–${missing.at(-1)} 条(总计 ${task.requested_count} 条)`;
|
|
236
237
|
record.updatedAt = now();
|
|
237
238
|
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(); } });
|
|
239
|
+
const result = await runCodexTurn(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
240
|
await this.scriptTask(id);
|
|
240
241
|
progressed = record.receivedOrdinals.length > before;
|
|
242
|
+
if (!progressed && result.ok && result.text) {
|
|
243
|
+
try {
|
|
244
|
+
await this.submitScriptChunk(id, parseFlowCScriptOutput(result.text, missing));
|
|
245
|
+
await this.scriptTask(id);
|
|
246
|
+
progressed = record.receivedOrdinals.length > before;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
lastError = error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
241
252
|
if (progressed)
|
|
242
253
|
break;
|
|
243
254
|
// Keep provider/client details out of the browser-facing state. The
|
|
244
255
|
// underlying Codex runner already records the technical error locally.
|
|
245
|
-
lastError
|
|
256
|
+
lastError ||= result.ok ? "Codex 未返回可用的结构化脚本" : "Codex 本轮执行失败";
|
|
246
257
|
if (attemptIndex < FLOW_C_SCRIPT_CHUNK_SIZES.length - 1) {
|
|
247
258
|
const nextSize = FLOW_C_SCRIPT_CHUNK_SIZES[attemptIndex + 1];
|
|
248
259
|
record.message = `第 ${lastRange} 条未成功回传,正在换新会话并缩小为每段 ${nextSize} 条重试`;
|
|
@@ -415,13 +426,17 @@ function scriptChunkPrompt(id, task, ordinals) {
|
|
|
415
426
|
每条回传同时填写 script=masterScript、masterScript 和 segments。segments[0].continuityMode 必须是 reset;同人物同场景延续时后续段用 continue,明确换人物或换场景才用 reset。continue 段必须复述并固定人物年龄、外貌、服装、场景陈设、光线、机位基调和商品当前状态。
|
|
416
427
|
每份 masterScript 和每个 segment 都要写同一套详细音色身份:性别、年龄段、音高、音质、语速、口音、停连和说话习惯;情绪随剧情逐段变化,但音色身份不变。仍按短视频快节奏口播,不因时长增加而拖慢。`;
|
|
417
428
|
return `你正在后台处理“抖音小辉跨境工具”Flow C 脚本交接 ${id}。
|
|
418
|
-
|
|
429
|
+
完整中心任务已经附在本提示词末尾。只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
|
|
419
430
|
本段 productIndex 必须严格按此映射填写:${scriptProductAssignments(task.product_quantities, ordinals)}。不得凭产品名称猜测或把相邻产品编号混用。
|
|
420
431
|
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地语言与偏快但清晰的短视频节奏。${longVideoRules}
|
|
421
432
|
每一个 ordinal 都必须从头到尾创作一份完整、独立的原生带货脚本,画面和口播必须在同一份脚本中一起独立构思。先让它成为一个当地真人在具体生活情境里自然发现、吐槽、试用或验证商品的短内容,再完成带货;禁止写成品牌广告片、棚拍宣传片、电视购物、逐条念卖点或全程完美对镜讲解。独立脚本天然包含独立口播:不得把任何一份口播当作整批公共模板,不得复用完整台词,也不得只换人物、场景或少数词后保留近似口播。音色身份可以为同一人物保持一致,但每条的 Hook、人物行动、证明表达和 CTA 都必须重新写。
|
|
422
433
|
用户没有指定带货方向时,不得随意只用一种泛化形式;必须按任务 instructions 中“已批准的创意方向”逐条做产品适配轮换。轮换必须按每个产品自己的序号连续计算,不能因 30/15/10 条分段、换会话或跨产品边界而从第一个方向重新开始;让同一产品在重复某一方向前优先覆盖其他适配方向。
|
|
423
434
|
工厂风格 A/B 是默认轮换中的演绎带货布景,不是商品来源声明;不得因用户未提供真实工厂资料而跳过,也绝不能写成我们的真实工厂、真实生产流程、真实产地、工厂直销、厂家出货或仓库现货。
|
|
424
|
-
|
|
435
|
+
只返回符合当前结构化输出契约的 jobs;每条必须包含 ordinal、productIndex、sellingFormId 和 script,长视频还必须包含 masterScript 与完整 segments。无需调用 MCP,Agent 会在本机校验后自动回传。不要创建付费批次,不要调用供应商模型。
|
|
436
|
+
|
|
437
|
+
【中心任务完整 instructions】
|
|
438
|
+
${task.instructions}
|
|
439
|
+
【中心任务 instructions 结束】`;
|
|
425
440
|
}
|
|
426
441
|
function scriptProductAssignments(productQuantities, ordinals) {
|
|
427
442
|
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
|
+
}
|