@xiaohhhh1/canvas-agent 0.4.69 → 0.4.71
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.
|
@@ -7,6 +7,7 @@ export declare const FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS = 2;
|
|
|
7
7
|
export declare const FLOW_C_CANDIDATE_REVISION_MAX_ATTEMPTS = 2;
|
|
8
8
|
export declare const FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION = "flow-c-template-direct-v2";
|
|
9
9
|
export declare const FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION = "flow-c-product-profile-direct-v1";
|
|
10
|
+
export declare const FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION = "flow-c-creative-source-direct-v1";
|
|
10
11
|
type ScriptStatus = "queued" | "running" | "complete" | "error" | "expired";
|
|
11
12
|
type DownloadStatus = "waiting" | "running" | "complete" | "error" | "expired";
|
|
12
13
|
type ScriptRecord = {
|
|
@@ -61,12 +62,23 @@ type ProductInput = {
|
|
|
61
62
|
quantity: number;
|
|
62
63
|
sellingForm?: string;
|
|
63
64
|
creativeBrief?: string;
|
|
65
|
+
scriptSourceOverride?: "inherit" | "learned-viral" | "selling-form-library";
|
|
66
|
+
sellingFormSelection?: {
|
|
67
|
+
mode?: "smart" | "controlled-random" | "explicit";
|
|
68
|
+
cardId?: string | null;
|
|
69
|
+
};
|
|
64
70
|
productImageUrlsInExactOrder?: string[];
|
|
65
71
|
};
|
|
66
72
|
type SelectedCandidate = {
|
|
67
73
|
ordinal: number;
|
|
68
74
|
productIndex: number;
|
|
69
75
|
candidateRevision?: string;
|
|
76
|
+
scriptSource?: string;
|
|
77
|
+
creativeSource?: string;
|
|
78
|
+
sellingFormCardId?: string | null;
|
|
79
|
+
sellingFormName?: string | null;
|
|
80
|
+
sellingFormSelectionReason?: string | null;
|
|
81
|
+
sellingFormSelection?: Record<string, unknown>;
|
|
70
82
|
learnedTemplateId?: string | null;
|
|
71
83
|
learnedTemplateSource?: Record<string, unknown> | null;
|
|
72
84
|
productIdentityProfile?: Record<string, unknown>;
|
|
@@ -104,6 +116,8 @@ type ScriptTask = {
|
|
|
104
116
|
workflow: "flow-c";
|
|
105
117
|
market: string;
|
|
106
118
|
target_language?: string | null;
|
|
119
|
+
localization?: Record<string, unknown>;
|
|
120
|
+
script_source_default?: string;
|
|
107
121
|
duration_seconds?: 10 | 20 | 30;
|
|
108
122
|
script_output_contract_version?: string;
|
|
109
123
|
storyboard_layout_version?: StoryboardLayoutVersion;
|
|
@@ -214,7 +228,7 @@ export declare class WorkflowManager {
|
|
|
214
228
|
resetOrdinals?: unknown;
|
|
215
229
|
rewriteOrdinals?: unknown;
|
|
216
230
|
}>;
|
|
217
|
-
submitProductProfileChunk(idValue: unknown, profilesValue: unknown[]): Promise<{
|
|
231
|
+
submitProductProfileChunk(idValue: unknown, profilesValue: unknown[], contractVersion?: string): Promise<{
|
|
218
232
|
accepted: number;
|
|
219
233
|
profiled: number;
|
|
220
234
|
requestedProducts: number;
|
|
@@ -328,7 +342,7 @@ export declare class WorkflowManager {
|
|
|
328
342
|
private pumpScriptQueue;
|
|
329
343
|
private finishDownloadDirectorySelection;
|
|
330
344
|
private runScript;
|
|
331
|
-
/** Analyze each
|
|
345
|
+
/** Analyze each product once; image 1 owns identity and later images may only corroborate visible evidence. */
|
|
332
346
|
private ensureProductExecutionProfiles;
|
|
333
347
|
private runProductExecutionProfile;
|
|
334
348
|
private runCandidateChunk;
|
|
@@ -366,7 +380,7 @@ export declare function recordCreativeReplanAttempts(attempts: Map<number, numbe
|
|
|
366
380
|
export declare function isFlowCPromptPayloadTooLarge(error: unknown): error is Error & {
|
|
367
381
|
code: "FLOW_C_PROMPT_PAYLOAD_TOO_LARGE";
|
|
368
382
|
};
|
|
369
|
-
export declare function productExecutionProfilePrompt(product: ProductInput): string;
|
|
383
|
+
export declare function productExecutionProfilePrompt(product: ProductInput, contractVersion?: string): string;
|
|
370
384
|
export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[], rewriteAttempt?: number): string;
|
|
371
385
|
export declare function creativeCandidatePrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
372
386
|
export declare function selectedBlueprintPromptPayload(values: SelectedCandidate[], fallbackTargetDurationSeconds?: number): {
|
package/dist/workflow/manager.js
CHANGED
|
@@ -11,7 +11,7 @@ import { logger } from "../utils/logger.js";
|
|
|
11
11
|
import { windowsPowerShellExecutable } from "../utils/windows.js";
|
|
12
12
|
import { FLOW_C_SCRIPT_CHUNK_MAX, flowCScriptChunks, flowCScriptChunkSizes } from "./constants.js";
|
|
13
13
|
import { FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION, flowCCreativeCandidateOutputSchema, parseFlowCCreativeCandidateOutput } from "./creative-candidates.js";
|
|
14
|
-
import { FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, flowCProductExecutionProfileOutputSchema, parseFlowCProductExecutionProfileOutput } from "./product-profile.js";
|
|
14
|
+
import { FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, flowCProductExecutionProfileOutputSchema, parseFlowCProductExecutionProfileOutput } from "./product-profile.js";
|
|
15
15
|
import { flowCScriptOutputSchema, parseFlowCScriptOutput } from "./script-output.js";
|
|
16
16
|
const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
|
|
17
17
|
export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
|
|
@@ -20,6 +20,15 @@ export const FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS = 2;
|
|
|
20
20
|
export const FLOW_C_CANDIDATE_REVISION_MAX_ATTEMPTS = 2;
|
|
21
21
|
export const FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION = "flow-c-template-direct-v2";
|
|
22
22
|
export const FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION = "flow-c-product-profile-direct-v1";
|
|
23
|
+
export const FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION = "flow-c-creative-source-direct-v1";
|
|
24
|
+
function isProductProfileDirectContract(value) {
|
|
25
|
+
return value === FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION || value === FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION;
|
|
26
|
+
}
|
|
27
|
+
function productProfileContractForTask(task) {
|
|
28
|
+
return task.script_output_contract_version === FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION
|
|
29
|
+
? FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION
|
|
30
|
+
: FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION;
|
|
31
|
+
}
|
|
23
32
|
/** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
|
|
24
33
|
export class WorkflowManager {
|
|
25
34
|
config;
|
|
@@ -128,11 +137,11 @@ export class WorkflowManager {
|
|
|
128
137
|
throw new Error("每个候选子批必须包含 1–10 个 ordinal 组");
|
|
129
138
|
return commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/candidate-chunks`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ contractVersion: FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION, groups: groupsValue }) });
|
|
130
139
|
}
|
|
131
|
-
async submitProductProfileChunk(idValue, profilesValue) {
|
|
140
|
+
async submitProductProfileChunk(idValue, profilesValue, contractVersion = FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
|
|
132
141
|
const record = this.scriptRecord(workflowId(idValue, "脚本交接 ID"));
|
|
133
142
|
if (!Array.isArray(profilesValue) || profilesValue.length > 10)
|
|
134
143
|
throw new Error("每个商品执行档案子批最多 10 个产品");
|
|
135
|
-
return commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/product-profiles`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ contractVersion
|
|
144
|
+
return commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/product-profiles`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ contractVersion, profiles: profilesValue }) });
|
|
136
145
|
}
|
|
137
146
|
downloadState() {
|
|
138
147
|
return {
|
|
@@ -258,7 +267,7 @@ export class WorkflowManager {
|
|
|
258
267
|
let task = await this.scriptTask(id);
|
|
259
268
|
if (Date.parse(task.expires_at) <= Date.now())
|
|
260
269
|
throw new ExpiredCapabilityError("脚本交接已过期,请在网页重新点击交给本机 Codex");
|
|
261
|
-
if (![FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION, FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION].includes(String(task.script_output_contract_version || "")))
|
|
270
|
+
if (![FLOW_C_LEGACY_DIRECT_CONTRACT_VERSION, FLOW_C_PRODUCT_PROFILE_DIRECT_CONTRACT_VERSION, FLOW_C_CREATIVE_SOURCE_DIRECT_CONTRACT_VERSION].includes(String(task.script_output_contract_version || "")))
|
|
262
271
|
throw new Error("这是旧版候选/执行绑定脚本任务,已停止继续写入;请在网页放弃该任务并用新版直接蓝图重新创建");
|
|
263
272
|
const workspace = ensureSiteWorkspace(this.config);
|
|
264
273
|
const durationSeconds = Number(task.duration_seconds || 10);
|
|
@@ -267,9 +276,10 @@ export class WorkflowManager {
|
|
|
267
276
|
// violates strict response-format invariants.
|
|
268
277
|
for (const chunkSize of new Set(chunkSizes))
|
|
269
278
|
flowCScriptOutputSchema(durationSeconds, chunkSize);
|
|
270
|
-
|
|
279
|
+
if (isProductProfileDirectContract(task.script_output_contract_version))
|
|
280
|
+
flowCProductExecutionProfileOutputSchema(1, productProfileContractForTask(task));
|
|
271
281
|
record.activeChunks = 0;
|
|
272
|
-
if (task.script_output_contract_version
|
|
282
|
+
if (isProductProfileDirectContract(task.script_output_contract_version)) {
|
|
273
283
|
task = await this.ensureProductExecutionProfiles(id, task, workspace.workspacePath);
|
|
274
284
|
}
|
|
275
285
|
let chunkSizeIndex = 0;
|
|
@@ -395,9 +405,10 @@ export class WorkflowManager {
|
|
|
395
405
|
this.runningScripts.delete(id);
|
|
396
406
|
}
|
|
397
407
|
}
|
|
398
|
-
/** Analyze each
|
|
408
|
+
/** Analyze each product once; image 1 owns identity and later images may only corroborate visible evidence. */
|
|
399
409
|
async ensureProductExecutionProfiles(id, task, cwd) {
|
|
400
410
|
const record = this.scriptRecord(id);
|
|
411
|
+
const profileContractVersion = productProfileContractForTask(task);
|
|
401
412
|
const products = [...(task.product_inputs || [])].sort((left, right) => Number(left.productIndex) - Number(right.productIndex));
|
|
402
413
|
if (!products.length)
|
|
403
414
|
throw new Error("中心没有提供产品清单,无法建立商品执行档案");
|
|
@@ -408,24 +419,27 @@ export class WorkflowManager {
|
|
|
408
419
|
record.productProfiles = [...cached.values()].sort((left, right) => left.productIndex - right.productIndex);
|
|
409
420
|
const missing = products.filter((product) => !persisted.has(Number(product.productIndex)));
|
|
410
421
|
if (missing.length) {
|
|
411
|
-
record.message = `正在逐个查看 ${missing.length}
|
|
422
|
+
record.message = `正在逐个查看 ${missing.length} 个商品的有序参考图并保存一次性执行档案;图 1 锁身份,后续图只补证,整批直接复用`;
|
|
412
423
|
record.updatedAt = now();
|
|
413
424
|
this.save();
|
|
414
425
|
let nextProduct = 0;
|
|
415
|
-
const
|
|
426
|
+
const includesSupportingImages = profileContractVersion === FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION
|
|
427
|
+
&& missing.some((product) => (product.productImageUrlsInExactOrder || []).length > 1);
|
|
428
|
+
const profileConcurrency = includesSupportingImages ? 1 : FLOW_C_CODEX_WORKER_CONCURRENCY;
|
|
429
|
+
const workerCount = Math.min(missing.length, Math.max(1, profileConcurrency));
|
|
416
430
|
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
417
431
|
while (nextProduct < missing.length) {
|
|
418
432
|
const product = missing[nextProduct++];
|
|
419
433
|
const productIndex = Number(product.productIndex);
|
|
420
434
|
let profile = cached.get(productIndex);
|
|
421
435
|
if (!profile) {
|
|
422
|
-
profile = await this.runProductExecutionProfile(id, product, cwd);
|
|
436
|
+
profile = await this.runProductExecutionProfile(id, product, cwd, profileContractVersion);
|
|
423
437
|
cached.set(productIndex, profile);
|
|
424
438
|
record.productProfiles = [...cached.values()].sort((left, right) => left.productIndex - right.productIndex);
|
|
425
439
|
record.updatedAt = now();
|
|
426
440
|
this.save();
|
|
427
441
|
}
|
|
428
|
-
await this.submitProductProfileChunk(id, [profile]);
|
|
442
|
+
await this.submitProductProfileChunk(id, [profile], profileContractVersion);
|
|
429
443
|
}
|
|
430
444
|
}));
|
|
431
445
|
task = await this.scriptTask(id);
|
|
@@ -436,26 +450,26 @@ export class WorkflowManager {
|
|
|
436
450
|
throw new Error(`商品执行档案尚未完整保存(${profiled}/${products.length}),请点击重试`);
|
|
437
451
|
if (selected !== Number(task.requested_count)) {
|
|
438
452
|
// Empty idempotent submission retries only server-side matching;
|
|
439
|
-
// the already persisted
|
|
440
|
-
await this.submitProductProfileChunk(id, []);
|
|
453
|
+
// the already persisted one-pass product profiles are never re-run.
|
|
454
|
+
await this.submitProductProfileChunk(id, [], profileContractVersion);
|
|
441
455
|
task = await this.scriptTask(id);
|
|
442
456
|
}
|
|
443
457
|
if (Number(task.candidate_stage?.selected || 0) !== Number(task.requested_count))
|
|
444
|
-
throw new Error("
|
|
445
|
-
record.message = `已保存 ${products.length} 个商品执行档案并完成 ${task.requested_count}
|
|
458
|
+
throw new Error("中心尚未按商品执行档案完成全部创意蓝图绑定,请点击重试");
|
|
459
|
+
record.message = `已保存 ${products.length} 个商品执行档案并完成 ${task.requested_count} 条创意蓝图绑定,正在写脚本`;
|
|
446
460
|
record.updatedAt = now();
|
|
447
461
|
this.save();
|
|
448
462
|
return task;
|
|
449
463
|
}
|
|
450
|
-
async runProductExecutionProfile(id, product, cwd) {
|
|
464
|
+
async runProductExecutionProfile(id, product, cwd, contractVersion) {
|
|
451
465
|
const productIndex = Number(product.productIndex);
|
|
452
|
-
const
|
|
453
|
-
const result = await runCodexWorkflowTurn(productExecutionProfilePrompt(product), this.emit, {
|
|
466
|
+
const attachments = await productImageEvidenceAttachments(product, contractVersion === FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION);
|
|
467
|
+
const result = await runCodexWorkflowTurn(productExecutionProfilePrompt(product, contractVersion), this.emit, {
|
|
454
468
|
cwd,
|
|
455
469
|
permissionMode: "full",
|
|
456
470
|
timeoutMs: FLOW_C_CODEX_TURN_TIMEOUT_MS,
|
|
457
|
-
attachments
|
|
458
|
-
outputSchema: flowCProductExecutionProfileOutputSchema(1),
|
|
471
|
+
attachments,
|
|
472
|
+
outputSchema: flowCProductExecutionProfileOutputSchema(1, contractVersion),
|
|
459
473
|
onThread: (threadId) => { this.scriptRecord(id).threadId = threadId; this.save(); },
|
|
460
474
|
onWorkerStart: () => { const record = this.scriptRecord(id); record.activeChunks = Number(record.activeChunks || 0) + 1; record.updatedAt = now(); this.save(); },
|
|
461
475
|
onWorkerFinish: () => { const record = this.scriptRecord(id); record.activeChunks = Math.max(0, Number(record.activeChunks || 0) - 1); record.updatedAt = now(); this.save(); },
|
|
@@ -464,7 +478,7 @@ export class WorkflowManager {
|
|
|
464
478
|
if (!result.ok || !result.text)
|
|
465
479
|
throw new Error(result.ok ? `商品 ${productIndex + 1} 没有返回执行档案` : result.error);
|
|
466
480
|
try {
|
|
467
|
-
return parseFlowCProductExecutionProfileOutput(result.text, [productIndex])[0];
|
|
481
|
+
return parseFlowCProductExecutionProfileOutput(result.text, [productIndex], contractVersion)[0];
|
|
468
482
|
}
|
|
469
483
|
catch (error) {
|
|
470
484
|
throw new Error(`商品 ${productIndex + 1} 执行档案未通过严格结构校验:${error instanceof Error ? error.message : "未知错误"}`);
|
|
@@ -534,6 +548,7 @@ export class WorkflowManager {
|
|
|
534
548
|
const selected = selectedCandidatesForOrdinals(task, ordinals);
|
|
535
549
|
const jobs = parseFlowCScriptOutput(result.text, ordinals).map((job) => ({
|
|
536
550
|
...job,
|
|
551
|
+
sellingFormId: selected.get(job.ordinal)?.sellingFormCardId || job.sellingFormId,
|
|
537
552
|
expectedCandidateRevision: selected.get(job.ordinal)?.candidateRevision,
|
|
538
553
|
creativePlan: { ...(job.creativePlan || {}), ...selectedCandidatePlan(selected.get(job.ordinal)) },
|
|
539
554
|
}));
|
|
@@ -575,13 +590,13 @@ export class WorkflowManager {
|
|
|
575
590
|
if (rewriteOrdinals.length) {
|
|
576
591
|
if (rewriteAttempt >= FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS) {
|
|
577
592
|
return preserveScriptRecoveryReplans({
|
|
578
|
-
error: `ordinal ${rewriteOrdinals.join(", ")}
|
|
593
|
+
error: `ordinal ${rewriteOrdinals.join(", ")} 已按原选中创意蓝图重写 ${FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS} 次仍与同批完整脚本完全重复;已停止避免继续消耗 Token,已保存脚本保持不变`,
|
|
579
594
|
terminal: true,
|
|
580
595
|
}, error);
|
|
581
596
|
}
|
|
582
597
|
const record = this.scriptRecord(id);
|
|
583
598
|
record.attempts += 1;
|
|
584
|
-
record.message = `ordinal ${rewriteOrdinals.join(", ")}
|
|
599
|
+
record.message = `ordinal ${rewriteOrdinals.join(", ")} 的完整脚本与同批已有结果完全相同,正在保留原选中创意蓝图、商品和镜头质量做第 ${rewriteAttempt + 1}/${FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS} 次定向重写`;
|
|
585
600
|
record.updatedAt = now();
|
|
586
601
|
this.save();
|
|
587
602
|
const refreshedTask = await this.scriptTask(id);
|
|
@@ -855,40 +870,67 @@ class FlowCPromptPayloadTooLargeError extends Error {
|
|
|
855
870
|
export function isFlowCPromptPayloadTooLarge(error) {
|
|
856
871
|
return Boolean(error && typeof error === "object" && error.code === "FLOW_C_PROMPT_PAYLOAD_TOO_LARGE");
|
|
857
872
|
}
|
|
858
|
-
export function productExecutionProfilePrompt(product) {
|
|
859
|
-
|
|
873
|
+
export function productExecutionProfilePrompt(product, contractVersion = FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
|
|
874
|
+
const referenceCount = contractVersion === FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION
|
|
875
|
+
? Math.max(1, Math.min(5, product.productImageUrlsInExactOrder?.length || 0))
|
|
876
|
+
: 1;
|
|
877
|
+
const supportingEvidenceRule = referenceCount > 1
|
|
878
|
+
? `images 2-${referenceCount} may prove only those corroborating facts and never override image 1 identity`
|
|
879
|
+
: "there is no supporting image, so every evidence flag must be supported by image 1";
|
|
880
|
+
const attachmentBoundary = referenceCount > 1
|
|
881
|
+
? `Images 2-${referenceCount} are evidence-only views of the same SKU: they may corroborate another angle, back, interior, included part or multiple visible units, but they may never change image 1's SKU, color, shape, geometry, material, package or markings. Ignore any later-image conflict instead of blending products.`
|
|
882
|
+
: "There is no later supporting image; do not infer a hidden side, interior, extra unit or included part.";
|
|
883
|
+
const currentRequirements = contractVersion === FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION ? `
|
|
884
|
+
- infer one concise ordinary product name and one practical category from the user title plus visible image evidence; never turn a brand into the category;
|
|
885
|
+
- choose one exact categoryId and only resultIds that describe directly observable result types;
|
|
886
|
+
- choose only exact capabilityIds supported by an ordinary, visibly plausible use of this product;
|
|
887
|
+
- record visibleEvidence booleans conservatively: back/interior/multiple angles/multiple units are false unless they are literally visible in the ordered attached images; ${supportingEvidenceRule};
|
|
888
|
+
- availableUnitCount is one, multiple, or unclear from visible evidence only;
|
|
889
|
+
- safetyConstraintIds identify only concrete precautions that later form selection must obey.` : "";
|
|
890
|
+
return `You are creating one reusable Flow C product execution profile from exactly ${referenceCount} ordered attached product image${referenceCount === 1 ? "" : "s"}.
|
|
860
891
|
Product index: ${Number(product.productIndex)}
|
|
861
892
|
User title: ${String(product.title || "").trim().slice(0, 600)}
|
|
862
|
-
User approximate category: ${String(product.category || "").trim().slice(0, 300)}
|
|
893
|
+
User approximate category (optional and may be blank): ${String(product.category || "").trim().slice(0, 300)}
|
|
863
894
|
|
|
864
|
-
The
|
|
895
|
+
The first attachment is product image 1 and is the sole visual identity authority. Inspect every attachment directly in order. ${attachmentBoundary} Use the title/category only to name the likely ordinary function; they never prove performance, quantities, hidden accessories, materials or claims that are not visible.
|
|
865
896
|
Return one profile for the exact productIndex. Record only:
|
|
866
897
|
- visible colors, structures, included parts, and package/quantity that can actually be seen;
|
|
867
898
|
- exact brand or model text only when it is clearly present at the start of the user title or legible on product image 1 (including ordinary Title Case names such as Nike or Apple); return an empty visibleBrandOrModelText array when uncertain, and never copy the full product title or category noun into that field;
|
|
868
899
|
- physically plausible supported actions and observable results that can be filmed without inventing capabilities;
|
|
869
900
|
- demoability and suitable TikTok selling formats;
|
|
870
901
|
- unsupported claims, forbidden actions and evidence limits that later matching/writing must respect.
|
|
902
|
+
${currentRequirements}
|
|
871
903
|
Use concise production English. Empty arrays are valid when evidence is absent. Do not infer price, discount, sales, stock, efficacy, waterproofing, load limits, battery life, materials, certifications or accessories without visible evidence. Do not call tools and do not write a script. Return only the strict response schema.`;
|
|
872
904
|
}
|
|
873
|
-
async function
|
|
905
|
+
async function productImageEvidenceAttachments(product, includeSupportingImages) {
|
|
874
906
|
const productIndex = Number(product.productIndex);
|
|
875
|
-
const
|
|
907
|
+
const urls = (product.productImageUrlsInExactOrder || []).slice(0, includeSupportingImages ? 5 : 1);
|
|
908
|
+
if (!urls.length)
|
|
909
|
+
throw new Error(`商品 ${productIndex + 1} 缺少第 1 张主图`);
|
|
910
|
+
const attachments = [];
|
|
911
|
+
for (const [imageIndex, value] of urls.entries()) {
|
|
912
|
+
const url = safeDownloadUrl(value);
|
|
913
|
+
attachments.push(await productImageAttachment(url, productIndex, imageIndex));
|
|
914
|
+
}
|
|
915
|
+
return attachments;
|
|
916
|
+
}
|
|
917
|
+
async function productImageAttachment(url, productIndex, imageIndex) {
|
|
876
918
|
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(60_000) });
|
|
877
919
|
if (!response.ok)
|
|
878
|
-
throw new Error(`商品 ${productIndex + 1} 第 1
|
|
920
|
+
throw new Error(`商品 ${productIndex + 1} 第 ${imageIndex + 1} 张参考图下载失败(HTTP ${response.status})`);
|
|
879
921
|
const contentType = String(response.headers.get("content-type") || "").split(";")[0].trim().toLowerCase();
|
|
880
922
|
if (!["image/png", "image/jpeg", "image/webp"].includes(contentType))
|
|
881
|
-
throw new Error(`商品 ${productIndex + 1} 第 1
|
|
923
|
+
throw new Error(`商品 ${productIndex + 1} 第 ${imageIndex + 1} 张参考图格式不受支持`);
|
|
882
924
|
const declaredBytes = Number(response.headers.get("content-length") || 0);
|
|
883
925
|
const maxBytes = 12 * 1024 * 1024;
|
|
884
926
|
if (declaredBytes > maxBytes)
|
|
885
|
-
throw new Error(`商品 ${productIndex + 1} 第 1
|
|
927
|
+
throw new Error(`商品 ${productIndex + 1} 第 ${imageIndex + 1} 张参考图超过 12MB`);
|
|
886
928
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
887
929
|
if (!buffer.length || buffer.length > maxBytes)
|
|
888
|
-
throw new Error(`商品 ${productIndex + 1} 第 1
|
|
930
|
+
throw new Error(`商品 ${productIndex + 1} 第 ${imageIndex + 1} 张参考图为空或超过 12MB`);
|
|
889
931
|
return {
|
|
890
932
|
id: randomUUID(),
|
|
891
|
-
name: `flow-c-product-${productIndex + 1}.${contentType === "image/png" ? "png" : contentType === "image/webp" ? "webp" : "jpg"}`,
|
|
933
|
+
name: `flow-c-product-${productIndex + 1}-image-${imageIndex + 1}.${contentType === "image/png" ? "png" : contentType === "image/webp" ? "webp" : "jpg"}`,
|
|
892
934
|
type: contentType,
|
|
893
935
|
size: buffer.length,
|
|
894
936
|
dataUrl: `data:${contentType};base64,${buffer.toString("base64")}`,
|
|
@@ -906,19 +948,20 @@ export function scriptChunkPrompt(id, task, ordinals, rewriteAttempt = 0) {
|
|
|
906
948
|
? "每条只输出 openingState 和一个完整 0–10 秒 segment,不生成 masterScript。"
|
|
907
949
|
: `每条只输出 openingState 和 ${duration / 10} 个各自 0–10 秒的 segments;Agent 会确定性合成 masterScript,不得重复输出总稿,也不得写 10–20 或 20–30 全局时轴。`;
|
|
908
950
|
const rewriteInstruction = rewriteAttempt > 0
|
|
909
|
-
? `\n精确重复定向重写(第 ${rewriteAttempt}/${FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS} 次):中心只因这些 ordinal 的完整脚本文本与同批已有结果完全相同而拒绝。必须继续使用上方同一 executionBlueprint、productAdaptation
|
|
951
|
+
? `\n精确重复定向重写(第 ${rewriteAttempt}/${FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS} 次):中心只因这些 ordinal 的完整脚本文本与同批已有结果完全相同而拒绝。必须继续使用上方同一 executionBlueprint、productAdaptation、商品身份、所选因果结构、节拍比例、动作强度、运镜和画质;禁止重新匹配、重新选题、降低镜头质量或改商品。把 ordinalBindings 中的 variationSeed 与本轮 rewriteSeed(${ordinals.map((ordinal) => `${ordinal}=rewrite-${rewriteAttempt}-ordinal-${ordinal}`).join(";")})同时视为强制差异指令,实质改写该商品的开场措辞、可见执行细节、口播表达、屏幕字及收束反应,使完整脚本明显不同但结构与质量不变;不得只改空格、标点或同义词。\n`
|
|
910
952
|
: "";
|
|
911
|
-
const
|
|
953
|
+
const localization = compactTaskLocalization(task);
|
|
954
|
+
const targetVoiceLanguage = String(localization.targetLanguage || task.target_language || localization.targetLocale || "").trim() || `目标市场 ${task.market} 的自然当地语言`;
|
|
912
955
|
return `你正在后台完成 Flow C 脚本交接 ${id},只写 ordinal:${ordinals.join(", ")};映射:${scriptProductAssignments(task.product_quantities, ordinals)}。
|
|
913
|
-
目标市场:${task.market}。目标口播语言:${targetVoiceLanguage}。商品事实:${compactJson(productFacts, 12_000, "当前脚本子批的商品事实")}
|
|
914
|
-
中心已完成当前 ordinal 的创意选择。executionBlueprints
|
|
956
|
+
目标市场:${task.market}。目标口播语言:${targetVoiceLanguage}。结构化当地化事实(国家、语言/locale、出镜者、受众、生活场景、创作者口吻和CTA彼此独立;不得从国家推断族群):${compactJson(localization, 3_000, "当前脚本子批的当地化事实")}。商品事实:${compactJson(productFacts, 12_000, "当前脚本子批的商品事实")}
|
|
957
|
+
中心已完成当前 ordinal 的创意选择。executionBlueprints 只保存一次共享的模型无关创意蓝图(可能来自已学习爆款,也可能来自已筛选带货形式卡);productAdaptations 只保存一次每个不同的当前商品执行卡;ordinalBindings 只用 blueprintRef/adaptationRef 和 variationSeed 映射到具体脚本。存在 blueprintRef 时,对应 executionBlueprint 是唯一结构权威;没有 blueprintRef 时,对应 productAdaptation 是用户框架的唯一执行权威。相同蓝图可供同一商品整批复用,直接替换成当前商品执行,不要生成候选、重新选题或重新评分:
|
|
915
958
|
${compactJson(selectedBlueprintPromptPayload([...selected.values()], duration), FLOW_C_SELECTED_BLUEPRINT_PROMPT_MAX_CHARS, "当前脚本子批的共享蓝图与商品适配")}
|
|
916
959
|
${rewriteInstruction}
|
|
917
960
|
${durationRules}
|
|
918
961
|
写作要求:
|
|
919
|
-
1. visualHook、conflict、escalation/turn、productIntervention、visibleProof、purchaseReason、callbackMotivation 与所有 shots 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction、purchaseReason、truthBoundary
|
|
962
|
+
1. visualHook、conflict、escalation/turn、productIntervention、visibleProof、purchaseReason、callbackMotivation 与所有 shots 必须执行选中卡的 visualPremise、firstFrame、spectacleEscalation、productProofAction、purchaseReason、truthBoundary 以及七项视觉执行字段;保留所选蓝图的因果时间线、节拍比例、镜头/剪辑节奏、动作压力、证明方式和购买逻辑,只替换当前商品、人物、场景与当地口播槽位,不得稀释构图或换成普通模板。爆款来源的旧商品身份/动作必须替换;形式卡只提供抽象创意结构,绝不能反向改变现有首帧、分段、Omni或参考图制作方式。若 productAdaptation.templateMatch.actionCompatibility 的 openingAction、proofAction 或 spectacleAction 为 false,对应 productAdaptation 的 firstFrame、productProofAction 或 spectacleEscalation 对商品动作内容具有绝对优先级。productProofAction 必须同时写清一个真实动作及其镜头内可见结果;purchaseReason 只解释该结果为何值得目标用户购买,不能变成泛泛 CTA。
|
|
920
963
|
2. 严格执行 executionBlueprints 中的 sourceDurationSeconds → targetDurationSeconds 和 retimingMode:expand 只增加有用的证明停留、使用语境与购买理由,compress 只压缩重复、冗余铺垫和转场,same 保持原节拍比例;三种模式都必须保留 Hook、证明和购买因果,禁止整体拉伸、截断或机械加速。
|
|
921
|
-
3. 每条生成一份稳定 voiceProfile;每段只写简短 voiceCue。口播必须使用 ${targetVoiceLanguage}
|
|
964
|
+
3. 每条生成一份稳定 voiceProfile;每段只写简短 voiceCue。口播必须使用 ${targetVoiceLanguage},并执行 creatorVoiceStyle/ctaStyle;不是逐字翻译,整条内容按 Hook → Body/visible proof → Close 组织,短句口语化且与当前镜头可见动作同步,结尾只给一个简短自然 CTA。只使用少量可信的 localSceneProfile/audienceContext 日常细节;presenterContext 没填写时使用普通创作者,不得把国家代码、语言或地区自动等同于人物族群。商品事实没有提供时不得编造价格、折扣、销量、库存或稀缺性。商品标题只用于理解商品类型与事实,所有输出不得主动写出或念出品牌名、品牌广告语、包装文案,也不得要求模型把品牌字样变得更清晰;商品外观与已有标识的位置、比例、颜色和版式只服从第1张产品参考图。TikTok/TikTok Shop 只用于定义这条写作规则,不得把平台名写入 voiceover、onScreenText、visual、soundBgm、emotionalNote、openingState、endingState 或任何会进入故事板/视频的导演字段。导演说明统一用简洁制作英文;这里只做写作指令,不做语言检测字段。
|
|
922
965
|
4. 每段永远是独立 0–10 秒,含 1–8 个按剧情需要决定的 shots、准确 voiceover、onScreenText、evidence、soundBgm、emotionalNote 与 endingState;模型不要输出 continuity 或 continuityMode。
|
|
923
966
|
5. shot.visual 要直接写出主体位置、景别、光线、材质、动作峰值和当地自然环境,并保留真实皮肤、细发、布料/商品微纹理和实景混合光;禁止塑料蜡感、静态举产品、品牌广告片、电视购物或伪造工厂/销量/来源。
|
|
924
967
|
6. 20/30 秒的后一段剧情承接前段 endingState,但每段仍只使用自己的局部 0–10 秒执行内容。
|
|
@@ -976,21 +1019,44 @@ function relevantProductInputs(task, ordinals) {
|
|
|
976
1019
|
}
|
|
977
1020
|
function scriptPromptProductFacts(products, profiles = []) {
|
|
978
1021
|
const byProduct = new Map(profiles.map((profile) => [Number(profile.productIndex), profile]));
|
|
979
|
-
return products.map((product) =>
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1022
|
+
return products.map((product) => {
|
|
1023
|
+
const profile = byProduct.get(Number(product.productIndex));
|
|
1024
|
+
return {
|
|
1025
|
+
productIndex: product.productIndex,
|
|
1026
|
+
title: product.title || profile?.inferredProductName || "",
|
|
1027
|
+
category: product.category || profile?.inferredCategory || "",
|
|
1028
|
+
quantity: product.quantity,
|
|
1029
|
+
sellingForm: product.sellingForm || "",
|
|
1030
|
+
scriptSourceOverride: product.scriptSourceOverride || "inherit",
|
|
1031
|
+
sellingFormSelection: product.sellingFormSelection || null,
|
|
1032
|
+
productImageReferenceCount: Array.isArray(product.productImageUrlsInExactOrder) ? product.productImageUrlsInExactOrder.length : 0,
|
|
1033
|
+
productExecutionProfileRef: `flow-c-product-${product.productIndex}`,
|
|
1034
|
+
productExecutionProfile: compactProductExecutionProfileForPrompt(profile),
|
|
1035
|
+
};
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
function compactTaskLocalization(task) {
|
|
1039
|
+
const value = task.localization && typeof task.localization === "object" ? task.localization : {};
|
|
1040
|
+
const compact = (key, limit) => String(value[key] || "").trim().replace(/\s+/g, " ").slice(0, limit) || null;
|
|
1041
|
+
return {
|
|
1042
|
+
targetCountryCode: compact("targetCountryCode", 12),
|
|
1043
|
+
targetCountryLabel: compact("targetCountryLabel", 120) || String(task.market || "").slice(0, 120),
|
|
1044
|
+
targetLocale: compact("targetLocale", 40),
|
|
1045
|
+
targetLanguage: compact("targetLanguage", 120) || String(task.target_language || "").slice(0, 120) || null,
|
|
1046
|
+
presenterContext: compact("presenterContext", 300),
|
|
1047
|
+
audienceContext: compact("audienceContext", 300),
|
|
1048
|
+
localSceneProfile: compact("localSceneProfile", 500),
|
|
1049
|
+
creatorVoiceStyle: compact("creatorVoiceStyle", 300),
|
|
1050
|
+
ctaStyle: compact("ctaStyle", 300),
|
|
1051
|
+
};
|
|
989
1052
|
}
|
|
990
1053
|
function compactProductExecutionProfileForPrompt(profile) {
|
|
991
1054
|
if (!profile)
|
|
992
1055
|
return null;
|
|
993
1056
|
return {
|
|
1057
|
+
inferredProductName: String(profile.inferredProductName || "").slice(0, 200),
|
|
1058
|
+
inferredCategory: String(profile.inferredCategory || "").slice(0, 200),
|
|
1059
|
+
categoryId: String(profile.categoryId || "general").slice(0, 80),
|
|
994
1060
|
visibleIdentity: {
|
|
995
1061
|
colors: promptProfileList(profile.visibleIdentity?.colors, 4, 60),
|
|
996
1062
|
structures: promptProfileList(profile.visibleIdentity?.structures, 6, 120),
|
|
@@ -999,6 +1065,11 @@ function compactProductExecutionProfileForPrompt(profile) {
|
|
|
999
1065
|
},
|
|
1000
1066
|
supportedActions: promptProfileList(profile.supportedActions, 6, 160),
|
|
1001
1067
|
observableResults: promptProfileList(profile.observableResults, 6, 160),
|
|
1068
|
+
resultIds: promptProfileList(profile.resultIds, 10, 80),
|
|
1069
|
+
capabilityIds: promptProfileList(profile.capabilityIds, 12, 80),
|
|
1070
|
+
visibleEvidence: profile.visibleEvidence || null,
|
|
1071
|
+
availableUnitCount: String(profile.availableUnitCount || "unclear").slice(0, 20),
|
|
1072
|
+
safetyConstraintIds: promptProfileList(profile.safetyConstraintIds, 10, 80),
|
|
1002
1073
|
demoability: String(profile.demoability || "unclear").slice(0, 20),
|
|
1003
1074
|
suitableSellingFormats: promptProfileList(profile.suitableSellingFormats, 6, 100),
|
|
1004
1075
|
unsupportedClaims: promptProfileList(profile.unsupportedClaims, 6, 160),
|
|
@@ -1032,7 +1103,7 @@ export function selectedBlueprintPromptPayload(values, fallbackTargetDurationSec
|
|
|
1032
1103
|
const blueprintSignature = JSON.stringify({ executionBlueprint: blueprint, sourceDurationSeconds, targetDurationSeconds, retimingMode });
|
|
1033
1104
|
blueprintRef = blueprintRefBySignature.get(blueprintSignature) || null;
|
|
1034
1105
|
if (!blueprintRef) {
|
|
1035
|
-
const requestedRef = String(candidate.learnedTemplateId || `blueprint-${createHash("sha256").update(blueprint).digest("hex").slice(0, 16)}`);
|
|
1106
|
+
const requestedRef = String(candidate.learnedTemplateId || candidate.sellingFormCardId || `blueprint-${createHash("sha256").update(blueprint).digest("hex").slice(0, 16)}`);
|
|
1036
1107
|
const existing = blueprints.get(requestedRef);
|
|
1037
1108
|
const existingSignature = existing ? JSON.stringify(existing) : null;
|
|
1038
1109
|
const requestedRecord = { blueprintRef: requestedRef, executionBlueprint: blueprint, sourceDurationSeconds, targetDurationSeconds, retimingMode };
|
|
@@ -1047,6 +1118,10 @@ export function selectedBlueprintPromptPayload(values, fallbackTargetDurationSec
|
|
|
1047
1118
|
productIndex: candidate.productIndex,
|
|
1048
1119
|
blueprintRef,
|
|
1049
1120
|
productExecutionProfileRef: candidate.productExecutionProfileRef || `flow-c-product-${candidate.productIndex}`,
|
|
1121
|
+
scriptSource: candidate.scriptSource || candidate.creativeSource || null,
|
|
1122
|
+
sellingFormCardId: candidate.sellingFormCardId || null,
|
|
1123
|
+
sellingFormName: candidate.sellingFormName || null,
|
|
1124
|
+
sellingFormSelectionReason: candidate.sellingFormSelectionReason || null,
|
|
1050
1125
|
selectionMode: candidate.selectionMode || null,
|
|
1051
1126
|
templateMatch: candidate.templateMatch || null,
|
|
1052
1127
|
retimingInstruction: candidate.retimingInstruction || null,
|
|
@@ -1097,6 +1172,12 @@ function selectedCandidatePlan(value) {
|
|
|
1097
1172
|
visualPremise: value.visualPremise,
|
|
1098
1173
|
learnedTemplateId: value.learnedTemplateId || null,
|
|
1099
1174
|
learnedTemplateSource: value.learnedTemplateSource || null,
|
|
1175
|
+
scriptSource: value.scriptSource || value.creativeSource || null,
|
|
1176
|
+
sellingFormCardId: value.sellingFormCardId || null,
|
|
1177
|
+
sellingFormName: value.sellingFormName || null,
|
|
1178
|
+
sellingFormSelectionReason: value.sellingFormSelectionReason || null,
|
|
1179
|
+
sellingFormLibraryVersion: String(value.sellingFormSelection?.libraryVersion || "").slice(0, 120) || null,
|
|
1180
|
+
sellingFormSelectionVersion: String(value.sellingFormSelection?.contractVersion || "").slice(0, 120) || null,
|
|
1100
1181
|
productIdentityProfile: value.productIdentityProfile || {},
|
|
1101
1182
|
productExecutionProfileRef: value.productExecutionProfileRef || `flow-c-product-${value.productIndex}`,
|
|
1102
1183
|
mutationAxes: value.mutationAxes,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
type JsonSchema = Record<string, unknown>;
|
|
2
|
-
export declare const FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION = "flow-c-product-execution-profile-
|
|
2
|
+
export declare const FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION = "flow-c-product-execution-profile-v2";
|
|
3
|
+
export declare const FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION = "flow-c-product-execution-profile-v1";
|
|
3
4
|
export type FlowCProductExecutionProfile = {
|
|
4
5
|
productIndex: number;
|
|
6
|
+
inferredProductName?: string;
|
|
7
|
+
inferredCategory?: string;
|
|
8
|
+
categoryId?: "apparel" | "beauty-personal-care" | "home-storage" | "cleaning" | "kitchen-food" | "tools-hardware" | "consumer-electronics" | "accessories" | "baby-family" | "general";
|
|
5
9
|
visibleIdentity: {
|
|
6
10
|
colors: string[];
|
|
7
11
|
structures: string[];
|
|
@@ -11,12 +15,23 @@ export type FlowCProductExecutionProfile = {
|
|
|
11
15
|
visibleBrandOrModelText: string[];
|
|
12
16
|
supportedActions: string[];
|
|
13
17
|
observableResults: string[];
|
|
18
|
+
resultIds?: string[];
|
|
19
|
+
capabilityIds?: string[];
|
|
20
|
+
visibleEvidence?: {
|
|
21
|
+
frontView: boolean;
|
|
22
|
+
backView: boolean;
|
|
23
|
+
interiorView: boolean;
|
|
24
|
+
multipleAngles: boolean;
|
|
25
|
+
multipleUnits: boolean;
|
|
26
|
+
};
|
|
27
|
+
availableUnitCount?: "one" | "multiple" | "unclear";
|
|
28
|
+
safetyConstraintIds?: string[];
|
|
14
29
|
demoability: "high" | "medium" | "low" | "unclear";
|
|
15
30
|
suitableSellingFormats: string[];
|
|
16
31
|
unsupportedClaims: string[];
|
|
17
32
|
forbiddenActions: string[];
|
|
18
33
|
evidenceLimits: string[];
|
|
19
34
|
};
|
|
20
|
-
export declare function flowCProductExecutionProfileOutputSchema(count: number): JsonSchema;
|
|
21
|
-
export declare function parseFlowCProductExecutionProfileOutput(value: string, expectedProductIndexes: number[]): FlowCProductExecutionProfile[];
|
|
35
|
+
export declare function flowCProductExecutionProfileOutputSchema(count: number, contractVersion?: string): JsonSchema;
|
|
36
|
+
export declare function parseFlowCProductExecutionProfileOutput(value: string, expectedProductIndexes: number[], contractVersion?: string): FlowCProductExecutionProfile[];
|
|
22
37
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertStrictResponseSchema } from "./script-output.js";
|
|
2
|
-
export const FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION = "flow-c-product-execution-profile-
|
|
2
|
+
export const FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION = "flow-c-product-execution-profile-v2";
|
|
3
|
+
export const FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION = "flow-c-product-execution-profile-v1";
|
|
3
4
|
const PROFILE_KEYS = [
|
|
4
5
|
"productIndex",
|
|
5
6
|
"visibleIdentity",
|
|
@@ -12,8 +13,44 @@ const PROFILE_KEYS = [
|
|
|
12
13
|
"forbiddenActions",
|
|
13
14
|
"evidenceLimits",
|
|
14
15
|
];
|
|
16
|
+
const PROFILE_V2_KEYS = [
|
|
17
|
+
"productIndex",
|
|
18
|
+
"inferredProductName",
|
|
19
|
+
"inferredCategory",
|
|
20
|
+
"categoryId",
|
|
21
|
+
"visibleIdentity",
|
|
22
|
+
"visibleBrandOrModelText",
|
|
23
|
+
"supportedActions",
|
|
24
|
+
"observableResults",
|
|
25
|
+
"resultIds",
|
|
26
|
+
"capabilityIds",
|
|
27
|
+
"visibleEvidence",
|
|
28
|
+
"availableUnitCount",
|
|
29
|
+
"safetyConstraintIds",
|
|
30
|
+
"demoability",
|
|
31
|
+
"suitableSellingFormats",
|
|
32
|
+
"unsupportedClaims",
|
|
33
|
+
"forbiddenActions",
|
|
34
|
+
"evidenceLimits",
|
|
35
|
+
];
|
|
15
36
|
const VISIBLE_IDENTITY_KEYS = ["colors", "structures", "includedParts", "packageOrQuantity"];
|
|
37
|
+
const VISIBLE_EVIDENCE_KEYS = ["frontView", "backView", "interiorView", "multipleAngles", "multipleUnits"];
|
|
16
38
|
const DEMOABILITY_VALUES = ["high", "medium", "low", "unclear"];
|
|
39
|
+
const AVAILABLE_UNIT_COUNT_VALUES = ["one", "multiple", "unclear"];
|
|
40
|
+
const CATEGORY_IDS = ["apparel", "beauty-personal-care", "home-storage", "cleaning", "kitchen-food", "tools-hardware", "consumer-electronics", "accessories", "baby-family", "general"];
|
|
41
|
+
const RESULT_IDS = ["visible-state-change", "fit-and-drape", "organization-change", "surface-change", "sensory-feedback", "operational-feedback", "capacity-proof", "ease-of-use", "construction-detail", "usage-context", "included-items", "package-contents"];
|
|
42
|
+
const CAPABILITY_IDS = [
|
|
43
|
+
"wear-or-fit", "open-or-close", "assemble-or-install", "store-or-organize", "wipe-or-clean",
|
|
44
|
+
"apply-or-spread", "pour", "dispense-or-squeeze", "spray", "illuminate", "connect-or-charge",
|
|
45
|
+
"screen-or-display", "audio-output", "cook-or-heat", "serve-food", "carry-or-pack", "cut",
|
|
46
|
+
"drill", "fasten", "absorb-liquid", "repel-water", "compress-or-rebound", "style-hair",
|
|
47
|
+
"show-texture", "show-size-or-capacity", "show-visible-before-after", "unbox", "ordinary-use-demo",
|
|
48
|
+
];
|
|
49
|
+
const SAFETY_CONSTRAINT_IDS = [
|
|
50
|
+
"avoid-water", "avoid-heat", "avoid-open-flame", "avoid-impact", "avoid-heavy-load", "avoid-cutting",
|
|
51
|
+
"avoid-disassembly", "avoid-ingestion", "avoid-body-or-efficacy-claim", "avoid-child-unsupervised-use",
|
|
52
|
+
"avoid-skin-contact", "avoid-eye-contact", "avoid-extreme-test",
|
|
53
|
+
];
|
|
17
54
|
const LIST_LIMITS = Object.freeze({
|
|
18
55
|
colors: { maxItems: 12, maxLength: 120 },
|
|
19
56
|
structures: { maxItems: 16, maxLength: 240 },
|
|
@@ -26,6 +63,9 @@ const LIST_LIMITS = Object.freeze({
|
|
|
26
63
|
unsupportedClaims: { maxItems: 20, maxLength: 300 },
|
|
27
64
|
forbiddenActions: { maxItems: 20, maxLength: 300 },
|
|
28
65
|
evidenceLimits: { maxItems: 20, maxLength: 300 },
|
|
66
|
+
capabilityIds: { maxItems: 16, maxLength: 80 },
|
|
67
|
+
resultIds: { maxItems: 12, maxLength: 80 },
|
|
68
|
+
safetyConstraintIds: { maxItems: 12, maxLength: 80 },
|
|
29
69
|
});
|
|
30
70
|
function object(properties) {
|
|
31
71
|
return { type: "object", properties, required: Object.keys(properties), additionalProperties: false };
|
|
@@ -36,8 +76,11 @@ function boundedText(maxLength) {
|
|
|
36
76
|
function boundedList(maxItems, maxLength) {
|
|
37
77
|
return { type: "array", minItems: 0, maxItems, items: boundedText(maxLength) };
|
|
38
78
|
}
|
|
39
|
-
function
|
|
40
|
-
return
|
|
79
|
+
function boundedEnumList(values, maxItems) {
|
|
80
|
+
return { type: "array", minItems: 0, maxItems, items: { type: "string", enum: [...values] } };
|
|
81
|
+
}
|
|
82
|
+
function productProfileSchema(contractVersion) {
|
|
83
|
+
const legacy = {
|
|
41
84
|
productIndex: { type: "integer", minimum: 0 },
|
|
42
85
|
visibleIdentity: object({
|
|
43
86
|
colors: boundedList(LIST_LIMITS.colors.maxItems, LIST_LIMITS.colors.maxLength),
|
|
@@ -53,26 +96,50 @@ function productProfileSchema() {
|
|
|
53
96
|
unsupportedClaims: boundedList(LIST_LIMITS.unsupportedClaims.maxItems, LIST_LIMITS.unsupportedClaims.maxLength),
|
|
54
97
|
forbiddenActions: boundedList(LIST_LIMITS.forbiddenActions.maxItems, LIST_LIMITS.forbiddenActions.maxLength),
|
|
55
98
|
evidenceLimits: boundedList(LIST_LIMITS.evidenceLimits.maxItems, LIST_LIMITS.evidenceLimits.maxLength),
|
|
99
|
+
};
|
|
100
|
+
if (contractVersion === FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION)
|
|
101
|
+
return object(legacy);
|
|
102
|
+
return object({
|
|
103
|
+
productIndex: legacy.productIndex,
|
|
104
|
+
inferredProductName: boundedText(200),
|
|
105
|
+
inferredCategory: boundedText(200),
|
|
106
|
+
categoryId: { type: "string", enum: [...CATEGORY_IDS] },
|
|
107
|
+
visibleIdentity: legacy.visibleIdentity,
|
|
108
|
+
visibleBrandOrModelText: legacy.visibleBrandOrModelText,
|
|
109
|
+
supportedActions: legacy.supportedActions,
|
|
110
|
+
observableResults: legacy.observableResults,
|
|
111
|
+
resultIds: boundedEnumList(RESULT_IDS, LIST_LIMITS.resultIds.maxItems),
|
|
112
|
+
capabilityIds: boundedEnumList(CAPABILITY_IDS, LIST_LIMITS.capabilityIds.maxItems),
|
|
113
|
+
visibleEvidence: object(Object.fromEntries(VISIBLE_EVIDENCE_KEYS.map((key) => [key, { type: "boolean" }]))),
|
|
114
|
+
availableUnitCount: { type: "string", enum: [...AVAILABLE_UNIT_COUNT_VALUES] },
|
|
115
|
+
safetyConstraintIds: boundedEnumList(SAFETY_CONSTRAINT_IDS, LIST_LIMITS.safetyConstraintIds.maxItems),
|
|
116
|
+
demoability: legacy.demoability,
|
|
117
|
+
suitableSellingFormats: legacy.suitableSellingFormats,
|
|
118
|
+
unsupportedClaims: legacy.unsupportedClaims,
|
|
119
|
+
forbiddenActions: legacy.forbiddenActions,
|
|
120
|
+
evidenceLimits: legacy.evidenceLimits,
|
|
56
121
|
});
|
|
57
122
|
}
|
|
58
|
-
export function flowCProductExecutionProfileOutputSchema(count) {
|
|
123
|
+
export function flowCProductExecutionProfileOutputSchema(count, contractVersion = FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
|
|
59
124
|
if (!Number.isInteger(count) || count < 1 || count > 100)
|
|
60
125
|
throw new Error("Product execution profile count must be an integer from 1 to 100");
|
|
126
|
+
if (![FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, FLOW_C_LEGACY_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION].includes(contractVersion))
|
|
127
|
+
throw new Error("Unsupported product execution profile contract version");
|
|
61
128
|
const schema = object({
|
|
62
|
-
contractVersion: { type: "string", enum: [
|
|
63
|
-
profiles: { type: "array", minItems: count, maxItems: count, items: productProfileSchema() },
|
|
129
|
+
contractVersion: { type: "string", enum: [contractVersion] },
|
|
130
|
+
profiles: { type: "array", minItems: count, maxItems: count, items: productProfileSchema(contractVersion) },
|
|
64
131
|
});
|
|
65
132
|
assertStrictResponseSchema(schema);
|
|
66
133
|
return schema;
|
|
67
134
|
}
|
|
68
|
-
export function parseFlowCProductExecutionProfileOutput(value, expectedProductIndexes) {
|
|
135
|
+
export function parseFlowCProductExecutionProfileOutput(value, expectedProductIndexes, contractVersion = FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
|
|
69
136
|
const expected = normalizeExpectedProductIndexes(expectedProductIndexes);
|
|
70
137
|
const source = String(value || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
71
138
|
const parsed = recordOf(JSON.parse(source));
|
|
72
139
|
if (!parsed)
|
|
73
140
|
throw new Error("Codex did not return a product execution profile object");
|
|
74
141
|
assertExactKeys(parsed, ["contractVersion", "profiles"], "Product execution profile response");
|
|
75
|
-
if (parsed.contractVersion !==
|
|
142
|
+
if (parsed.contractVersion !== contractVersion) {
|
|
76
143
|
throw new Error("Product execution profile contract version does not match the current Agent");
|
|
77
144
|
}
|
|
78
145
|
if (!Array.isArray(parsed.profiles) || parsed.profiles.length !== expected.length) {
|
|
@@ -81,7 +148,7 @@ export function parseFlowCProductExecutionProfileOutput(value, expectedProductIn
|
|
|
81
148
|
const expectedSet = new Set(expected);
|
|
82
149
|
const accepted = new Map();
|
|
83
150
|
for (const [position, value] of parsed.profiles.entries()) {
|
|
84
|
-
const profile = normalizeProfile(value, `profiles[${position}]
|
|
151
|
+
const profile = normalizeProfile(value, `profiles[${position}]`, contractVersion);
|
|
85
152
|
if (!expectedSet.has(profile.productIndex))
|
|
86
153
|
throw new Error(`Unexpected product execution profile productIndex ${profile.productIndex}`);
|
|
87
154
|
if (accepted.has(profile.productIndex))
|
|
@@ -102,11 +169,12 @@ function normalizeExpectedProductIndexes(value) {
|
|
|
102
169
|
throw new Error("Expected product indexes must be unique");
|
|
103
170
|
return indexes;
|
|
104
171
|
}
|
|
105
|
-
function normalizeProfile(value, label) {
|
|
172
|
+
function normalizeProfile(value, label, contractVersion) {
|
|
106
173
|
const profile = recordOf(value);
|
|
107
174
|
if (!profile)
|
|
108
175
|
throw new Error(`${label} must be an object`);
|
|
109
|
-
|
|
176
|
+
const current = contractVersion === FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION;
|
|
177
|
+
assertExactKeys(profile, current ? PROFILE_V2_KEYS : PROFILE_KEYS, label);
|
|
110
178
|
const productIndex = profile.productIndex;
|
|
111
179
|
if (typeof productIndex !== "number" || !Number.isInteger(productIndex) || productIndex < 0)
|
|
112
180
|
throw new Error(`${label}.productIndex must be a non-negative integer`);
|
|
@@ -114,10 +182,15 @@ function normalizeProfile(value, label) {
|
|
|
114
182
|
if (!visibleIdentity)
|
|
115
183
|
throw new Error(`${label}.visibleIdentity must be an object`);
|
|
116
184
|
assertExactKeys(visibleIdentity, VISIBLE_IDENTITY_KEYS, `${label}.visibleIdentity`);
|
|
185
|
+
const visibleEvidence = current ? recordOf(profile.visibleEvidence) : null;
|
|
186
|
+
if (current && !visibleEvidence)
|
|
187
|
+
throw new Error(`${label}.visibleEvidence must be an object`);
|
|
188
|
+
if (visibleEvidence)
|
|
189
|
+
assertExactKeys(visibleEvidence, VISIBLE_EVIDENCE_KEYS, `${label}.visibleEvidence`);
|
|
117
190
|
const demoability = profile.demoability;
|
|
118
191
|
if (typeof demoability !== "string" || !DEMOABILITY_VALUES.includes(demoability))
|
|
119
192
|
throw new Error(`${label}.demoability is invalid`);
|
|
120
|
-
|
|
193
|
+
const result = {
|
|
121
194
|
productIndex,
|
|
122
195
|
visibleIdentity: {
|
|
123
196
|
colors: normalizeStringList(visibleIdentity.colors, LIST_LIMITS.colors, `${label}.visibleIdentity.colors`),
|
|
@@ -134,6 +207,48 @@ function normalizeProfile(value, label) {
|
|
|
134
207
|
forbiddenActions: normalizeStringList(profile.forbiddenActions, LIST_LIMITS.forbiddenActions, `${label}.forbiddenActions`),
|
|
135
208
|
evidenceLimits: normalizeStringList(profile.evidenceLimits, LIST_LIMITS.evidenceLimits, `${label}.evidenceLimits`),
|
|
136
209
|
};
|
|
210
|
+
if (!current)
|
|
211
|
+
return result;
|
|
212
|
+
const availableUnitCount = profile.availableUnitCount;
|
|
213
|
+
if (typeof availableUnitCount !== "string" || !AVAILABLE_UNIT_COUNT_VALUES.includes(availableUnitCount))
|
|
214
|
+
throw new Error(`${label}.availableUnitCount is invalid`);
|
|
215
|
+
result.inferredProductName = normalizeRequiredText(profile.inferredProductName, 200, `${label}.inferredProductName`);
|
|
216
|
+
result.inferredCategory = normalizeRequiredText(profile.inferredCategory, 200, `${label}.inferredCategory`);
|
|
217
|
+
if (typeof profile.categoryId !== "string" || !CATEGORY_IDS.includes(profile.categoryId))
|
|
218
|
+
throw new Error(`${label}.categoryId is invalid`);
|
|
219
|
+
result.categoryId = profile.categoryId;
|
|
220
|
+
result.resultIds = normalizeEnumList(profile.resultIds, RESULT_IDS, LIST_LIMITS.resultIds.maxItems, `${label}.resultIds`);
|
|
221
|
+
result.capabilityIds = normalizeEnumList(profile.capabilityIds, CAPABILITY_IDS, LIST_LIMITS.capabilityIds.maxItems, `${label}.capabilityIds`);
|
|
222
|
+
result.visibleEvidence = Object.fromEntries(VISIBLE_EVIDENCE_KEYS.map((key) => {
|
|
223
|
+
const item = visibleEvidence[key];
|
|
224
|
+
if (typeof item !== "boolean")
|
|
225
|
+
throw new Error(`${label}.visibleEvidence.${key} must be a boolean`);
|
|
226
|
+
return [key, item];
|
|
227
|
+
}));
|
|
228
|
+
result.availableUnitCount = availableUnitCount;
|
|
229
|
+
result.safetyConstraintIds = normalizeEnumList(profile.safetyConstraintIds, SAFETY_CONSTRAINT_IDS, LIST_LIMITS.safetyConstraintIds.maxItems, `${label}.safetyConstraintIds`);
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
function normalizeRequiredText(value, maxLength, label) {
|
|
233
|
+
if (typeof value !== "string")
|
|
234
|
+
throw new Error(`${label} must be a string`);
|
|
235
|
+
const normalized = value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
236
|
+
if (!normalized || normalized.length > maxLength)
|
|
237
|
+
throw new Error(`${label} must contain 1-${maxLength} characters`);
|
|
238
|
+
return normalized;
|
|
239
|
+
}
|
|
240
|
+
function normalizeEnumList(value, allowed, maxItems, label) {
|
|
241
|
+
if (!Array.isArray(value) || value.length > maxItems)
|
|
242
|
+
throw new Error(`${label} must be an array with at most ${maxItems} items`);
|
|
243
|
+
const accepted = new Set(allowed);
|
|
244
|
+
const result = [];
|
|
245
|
+
for (const [index, item] of value.entries()) {
|
|
246
|
+
if (typeof item !== "string" || !accepted.has(item))
|
|
247
|
+
throw new Error(`${label}[${index}] is invalid`);
|
|
248
|
+
if (!result.includes(item))
|
|
249
|
+
result.push(item);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
137
252
|
}
|
|
138
253
|
function normalizeStringList(value, limits, label) {
|
|
139
254
|
if (!Array.isArray(value))
|
|
@@ -69,6 +69,8 @@ export function assertStrictResponseSchema(schemaValue, path = "$") {
|
|
|
69
69
|
const schema = recordOf(schemaValue);
|
|
70
70
|
if (!schema)
|
|
71
71
|
throw new Error(`Strict response schema at ${path} must be an object`);
|
|
72
|
+
if ("uniqueItems" in schema)
|
|
73
|
+
throw new Error(`Strict response schema at ${path} must not use unsupported uniqueItems`);
|
|
72
74
|
if (schema.type === "object") {
|
|
73
75
|
const properties = recordOf(schema.properties);
|
|
74
76
|
if (!properties)
|