@xiaohhhh1/canvas-agent 0.4.69 → 0.4.70

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 primary product image once, persist it centrally, then let the server rank the full learned library. */
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): {
@@ -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: FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION, profiles: profilesValue }) });
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
- flowCProductExecutionProfileOutputSchema(1);
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 === FLOW_C_PRODUCT_PROFILE_DIRECT_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 primary product image once, persist it centrally, then let the server rank the full learned library. */
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} 个商品的第 1 张主图并保存一次性执行档案;后续整批直接复用`;
422
+ record.message = `正在逐个查看 ${missing.length} 个商品的有序参考图并保存一次性执行档案;图 1 锁身份,后续图只补证,整批直接复用`;
412
423
  record.updatedAt = now();
413
424
  this.save();
414
425
  let nextProduct = 0;
415
- const workerCount = Math.min(missing.length, Math.max(1, FLOW_C_CODEX_WORKER_CONCURRENCY));
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 primary-image profiles are never re-run.
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 attachment = await primaryProductImageAttachment(product);
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: [attachment],
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(", ")} 已按原爆款蓝图重写 ${FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS} 次仍与同批完整脚本完全重复;已停止避免继续消耗 Token,已保存脚本保持不变`,
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(", ")} 的完整脚本与同批已有结果完全相同,正在保留原爆款蓝图、商品和镜头质量做第 ${rewriteAttempt + 1}/${FLOW_C_SCRIPT_REWRITE_MAX_ATTEMPTS} 次定向重写`;
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
- return `You are creating one reusable Flow C product execution profile from exactly one attached primary product image.
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 attached image is product image 1 and is the sole visual identity authority. Inspect it directly. 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.
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 primaryProductImageAttachment(product) {
905
+ async function productImageEvidenceAttachments(product, includeSupportingImages) {
874
906
  const productIndex = Number(product.productIndex);
875
- const url = safeDownloadUrl(product.productImageUrlsInExactOrder?.[0]);
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 张主图下载失败(HTTP ${response.status})`);
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 张主图超过 12MB`);
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 张主图为空或超过 12MB`);
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、商品身份、爆款因果结构、节拍比例、动作强度、运镜和画质;禁止重新匹配、重新选题、降低镜头质量或改商品。把 ordinalBindings 中的 variationSeed 与本轮 rewriteSeed(${ordinals.map((ordinal) => `${ordinal}=rewrite-${rewriteAttempt}-ordinal-${ordinal}`).join(";")})同时视为强制差异指令,实质改写该商品的开场措辞、可见执行细节、口播表达、屏幕字及收束反应,使完整脚本明显不同但结构与质量不变;不得只改空格、标点或同义词。\n`
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 targetVoiceLanguage = String(task.target_language || "").trim() || `目标市场 ${task.market} 的自然当地语言`;
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 只保存一次共享爆款详细蓝图;productAdaptations 只保存一次每个不同的当前商品执行卡;ordinalBindings 只用 blueprintRef/adaptationRef 和 variationSeed 映射到具体脚本。存在 blueprintRef 时,对应 executionBlueprint 是唯一结构权威;没有 blueprintRef 时,对应 productAdaptation 是现存普通 Flow C 候选或用户框架的唯一执行权威。相同蓝图可供同一商品整批复用,直接替换成当前商品执行,不要生成候选、重新选题或重新评分:
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 以及七项视觉执行字段;保留爆款的因果时间线、节拍比例、镜头/剪辑节奏、动作压力、证明方式和购买逻辑,只替换原商品、人物、来源身份、文案及目标市场口播,不得稀释构图或换成普通模板。若 productAdaptation.templateMatch.actionCompatibility 的 openingAction、proofAction 或 spectacleAction 为 false,对应 productAdaptation 的 firstFrame、productProofAction 或 spectacleEscalation 对商品动作内容具有绝对优先级:只保留 executionBlueprint 的结构角色、时间比例、镜头压力与视觉质量,必须删除其旧商品动作,不得折中混用。productProofAction 必须同时写清一个真实动作及其镜头内可见结果;purchaseReason 只解释该结果为何值得目标用户购买,不能变成泛泛 CTA。
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},并采用目标市场 ${task.market} 的当地 TikTok 带货创作者真实会说的口吻:不是逐字翻译,整条内容按 Hook → Body/visible proof → Close 组织,短句口语化且与当前镜头可见动作同步,结尾只给一个简短自然 CTA;商品事实没有提供时不得编造价格、折扣、销量、库存或稀缺性。商品标题只用于理解商品类型与事实,所有输出不得主动写出或念出品牌名、品牌广告语、包装文案,也不得要求模型把品牌字样变得更清晰;商品外观与已有标识的位置、比例、颜色和版式只服从第1张产品参考图。TikTok/TikTok Shop 只用于定义这条写作规则,不得把平台名写入 voiceover、onScreenText、visual、soundBgm、emotionalNote、openingState、endingState 或任何会进入故事板/视频的导演字段。导演说明统一用简洁制作英文;这里只做写作指令,不做语言检测字段。
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
- productIndex: product.productIndex,
981
- title: product.title,
982
- category: product.category || "",
983
- quantity: product.quantity,
984
- sellingForm: product.sellingForm || "",
985
- productImageReferenceCount: Array.isArray(product.productImageUrlsInExactOrder) ? product.productImageUrlsInExactOrder.length : 0,
986
- productExecutionProfileRef: `flow-c-product-${product.productIndex}`,
987
- productExecutionProfile: compactProductExecutionProfileForPrompt(byProduct.get(Number(product.productIndex))),
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-v1";
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-v1";
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 productProfileSchema() {
40
- return object({
79
+ function boundedEnumList(values, maxItems) {
80
+ return { type: "array", minItems: 0, maxItems, uniqueItems: true, 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: [FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION] },
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 !== FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
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
- assertExactKeys(profile, PROFILE_KEYS, label);
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
- return {
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.69",
3
+ "version": "0.4.70",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",