@xiaohhhh1/canvas-agent 0.4.74 → 0.4.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/codex-client.d.ts +53 -2
- package/dist/agent/codex-client.js +299 -60
- package/dist/agent/codex.d.ts +25 -0
- package/dist/agent/codex.js +179 -29
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +18 -0
- package/dist/workflow/commerce-http.d.ts +34 -0
- package/dist/workflow/commerce-http.js +114 -0
- package/dist/workflow/manager.d.ts +34 -15
- package/dist/workflow/manager.js +178 -81
- package/package.json +1 -1
package/dist/workflow/manager.js
CHANGED
|
@@ -13,6 +13,7 @@ import { FLOW_C_SCRIPT_CHUNK_MAX, flowCScriptChunks, flowCScriptChunkSizes } fro
|
|
|
13
13
|
import { FLOW_C_CREATIVE_CANDIDATE_CONTRACT_VERSION, flowCCreativeCandidateOutputSchema, parseFlowCCreativeCandidateOutput } from "./creative-candidates.js";
|
|
14
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
|
+
import { commerceJson, CommerceRequestError } from "./commerce-http.js";
|
|
16
17
|
const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
|
|
17
18
|
export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
|
|
18
19
|
export const FLOW_C_SELECTED_BLUEPRINT_PROMPT_MAX_CHARS = 36_000;
|
|
@@ -67,6 +68,8 @@ export class WorkflowManager {
|
|
|
67
68
|
chunkSize: previous?.chunkSize,
|
|
68
69
|
activeChunks: 0,
|
|
69
70
|
productProfiles: previous?.productProfiles || [],
|
|
71
|
+
pendingScriptJobs: previous?.pendingScriptJobs || [],
|
|
72
|
+
lastFailure: previous?.lastFailure,
|
|
70
73
|
priorityAt: now(),
|
|
71
74
|
message: previous?.status === "complete" ? previous.message : "已进入本机 Codex 队列",
|
|
72
75
|
updatedAt: now(),
|
|
@@ -78,6 +81,15 @@ export class WorkflowManager {
|
|
|
78
81
|
retryScript(idValue) {
|
|
79
82
|
const id = workflowId(idValue, "脚本交接 ID");
|
|
80
83
|
const record = this.scriptRecord(id);
|
|
84
|
+
if (this.runningScripts.has(id)) {
|
|
85
|
+
record.retryRequested = true;
|
|
86
|
+
record.priorityAt = now();
|
|
87
|
+
record.message = "当前任务仍在处理,结束后优先重试";
|
|
88
|
+
record.updatedAt = now();
|
|
89
|
+
this.save();
|
|
90
|
+
return this.scriptStatus(id);
|
|
91
|
+
}
|
|
92
|
+
delete record.retryRequested;
|
|
81
93
|
record.status = "queued";
|
|
82
94
|
record.activeChunks = 0;
|
|
83
95
|
// A failed manual attempt may leave a very large or interrupted thread.
|
|
@@ -97,12 +109,13 @@ export class WorkflowManager {
|
|
|
97
109
|
/** MCP 读取服务端持久化的完整任务,不向模型暴露令牌。 */
|
|
98
110
|
async scriptTask(idValue) {
|
|
99
111
|
const record = this.scriptRecord(workflowId(idValue, "脚本交接 ID"));
|
|
100
|
-
const data = await commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/task`, record.accessToken, "x-workflow-handoff-token");
|
|
112
|
+
const data = await commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/task`, record.accessToken, "x-workflow-handoff-token", {}, this.scriptRequestOptions(record));
|
|
101
113
|
const task = data.handoff;
|
|
102
114
|
record.requestedCount = Number(task.requested_count || 0);
|
|
103
|
-
record.receivedOrdinals =
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
record.receivedOrdinals = [...new Set([...record.receivedOrdinals, ...task.received_ordinals.map(Number).filter(Number.isInteger)])].sort((left, right) => left - right);
|
|
116
|
+
task.received_ordinals = record.receivedOrdinals;
|
|
117
|
+
record.productProfiles = mergeProductProfiles(record.productProfiles, task.product_execution_profiles);
|
|
118
|
+
record.pendingScriptJobs = (record.pendingScriptJobs || []).filter((job) => !record.receivedOrdinals.includes(job.ordinal));
|
|
106
119
|
record.expiresAt = task.expires_at;
|
|
107
120
|
record.updatedAt = now();
|
|
108
121
|
this.save();
|
|
@@ -114,7 +127,13 @@ export class WorkflowManager {
|
|
|
114
127
|
if (!Array.isArray(jobsValue) || !jobsValue.length || jobsValue.length > FLOW_C_SCRIPT_CHUNK_MAX)
|
|
115
128
|
throw new Error(`每段必须包含 1–${FLOW_C_SCRIPT_CHUNK_MAX} 条脚本`);
|
|
116
129
|
const jobs = jobsValue;
|
|
117
|
-
const
|
|
130
|
+
const pending = new Map((record.pendingScriptJobs || []).map((job) => [job.ordinal, job]));
|
|
131
|
+
for (const job of jobs)
|
|
132
|
+
if (!record.receivedOrdinals.includes(job.ordinal))
|
|
133
|
+
pending.set(job.ordinal, structuredClone(job));
|
|
134
|
+
record.pendingScriptJobs = [...pending.values()].sort((left, right) => left.ordinal - right.ordinal);
|
|
135
|
+
this.save();
|
|
136
|
+
const data = await commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/draft-chunks`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ jobs }) }, this.scriptRequestOptions(record));
|
|
118
137
|
if (Array.isArray(data.receivedOrdinals))
|
|
119
138
|
record.receivedOrdinals = [...new Set([...record.receivedOrdinals, ...data.receivedOrdinals.map(Number).filter(Number.isInteger)])].sort((left, right) => left - right);
|
|
120
139
|
else {
|
|
@@ -125,6 +144,7 @@ export class WorkflowManager {
|
|
|
125
144
|
record.receivedOrdinals = [...accepted].sort((a, b) => a - b);
|
|
126
145
|
}
|
|
127
146
|
record.requestedCount = Number(data.requestedCount || record.requestedCount);
|
|
147
|
+
record.pendingScriptJobs = (record.pendingScriptJobs || []).filter((job) => !record.receivedOrdinals.includes(job.ordinal));
|
|
128
148
|
record.status = data.status === "ready" ? "complete" : "running";
|
|
129
149
|
record.message = data.status === "ready" ? `全部 ${record.requestedCount} 条高质量脚本已回传` : `已回传 ${data.received}/${data.requestedCount} 条脚本`;
|
|
130
150
|
record.updatedAt = now();
|
|
@@ -135,13 +155,21 @@ export class WorkflowManager {
|
|
|
135
155
|
const record = this.scriptRecord(workflowId(idValue, "脚本交接 ID"));
|
|
136
156
|
if (!Array.isArray(groupsValue) || !groupsValue.length || groupsValue.length > 10)
|
|
137
157
|
throw new Error("每个候选子批必须包含 1–10 个 ordinal 组");
|
|
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 }) });
|
|
158
|
+
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 }) }, this.scriptRequestOptions(record));
|
|
139
159
|
}
|
|
140
160
|
async submitProductProfileChunk(idValue, profilesValue, contractVersion = FLOW_C_PRODUCT_EXECUTION_PROFILE_CONTRACT_VERSION) {
|
|
141
161
|
const record = this.scriptRecord(workflowId(idValue, "脚本交接 ID"));
|
|
142
162
|
if (!Array.isArray(profilesValue) || profilesValue.length > 10)
|
|
143
163
|
throw new Error("每个商品执行档案子批最多 10 个产品");
|
|
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 }) });
|
|
164
|
+
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 }) }, this.scriptRequestOptions(record));
|
|
165
|
+
}
|
|
166
|
+
scriptRequestOptions(record) {
|
|
167
|
+
return { expiresAt: record.expiresAt, onFailure: (diagnostic) => {
|
|
168
|
+
record.lastFailure = diagnostic;
|
|
169
|
+
record.updatedAt = now();
|
|
170
|
+
this.save();
|
|
171
|
+
logger.failure("Flow C center request failed", { handoffId: record.id, ...diagnostic });
|
|
172
|
+
} };
|
|
145
173
|
}
|
|
146
174
|
downloadState() {
|
|
147
175
|
return {
|
|
@@ -260,6 +288,7 @@ export class WorkflowManager {
|
|
|
260
288
|
const record = this.scriptRecord(id);
|
|
261
289
|
try {
|
|
262
290
|
delete record.priorityAt;
|
|
291
|
+
delete record.retryRequested;
|
|
263
292
|
record.status = "running";
|
|
264
293
|
record.message = "正在读取完整产品清单";
|
|
265
294
|
record.updatedAt = now();
|
|
@@ -309,9 +338,14 @@ export class WorkflowManager {
|
|
|
309
338
|
}));
|
|
310
339
|
const results = pipelineResults.flat();
|
|
311
340
|
task = await this.scriptTask(id);
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
314
|
-
|
|
341
|
+
const terminalFailure = terminalScriptChunkFailure(results);
|
|
342
|
+
if (terminalFailure) {
|
|
343
|
+
if (terminalFailure.terminalKind === "delivery")
|
|
344
|
+
throw new Error(terminalFailure.error);
|
|
345
|
+
if (terminalFailure.terminalKind === "transport")
|
|
346
|
+
throw new Error(`创意或脚本阶段的本机 Codex 进程连续异常且自动恢复未成功,已停止当前任务且未提交缺失脚本(${terminalFailure.error})`);
|
|
347
|
+
throw new Error(`创意或脚本结构化契约被 Codex 拒绝,已停止自动重试(${terminalFailure.error})`);
|
|
348
|
+
}
|
|
315
349
|
const replanOrdinals = scriptCreativeReplanOrdinals(results);
|
|
316
350
|
if (replanOrdinals.length) {
|
|
317
351
|
recordCreativeReplanAttempts(creativeReplanAttempts, replanOrdinals);
|
|
@@ -319,9 +353,14 @@ export class WorkflowManager {
|
|
|
319
353
|
record.updatedAt = now();
|
|
320
354
|
this.save();
|
|
321
355
|
const replanResults = await Promise.all(replanOrdinals.map((ordinal) => this.runCandidateChunk(id, task, [ordinal], workspace.workspacePath)));
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
|
|
356
|
+
const replanTerminalFailure = terminalScriptChunkFailure(replanResults);
|
|
357
|
+
if (replanTerminalFailure) {
|
|
358
|
+
if (replanTerminalFailure.terminalKind === "delivery")
|
|
359
|
+
throw new Error(replanTerminalFailure.error);
|
|
360
|
+
if (replanTerminalFailure.terminalKind === "transport")
|
|
361
|
+
throw new Error(`创意重新选题时本机 Codex 进程异常且自动恢复未成功,已停止当前任务(${replanTerminalFailure.error})`);
|
|
362
|
+
throw new Error(`创意重新选题被 Codex 拒绝,已停止自动重试(${replanTerminalFailure.error})`);
|
|
363
|
+
}
|
|
325
364
|
const replanError = replanResults.find((result) => result.error)?.error;
|
|
326
365
|
if (replanError)
|
|
327
366
|
throw new Error(`创意重新选题失败(${replanError}),请点击重试`);
|
|
@@ -350,9 +389,13 @@ export class WorkflowManager {
|
|
|
350
389
|
this.save();
|
|
351
390
|
const results = await Promise.all(wave.chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
|
|
352
391
|
task = await this.scriptTask(id);
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
355
|
-
|
|
392
|
+
const terminalFailure = terminalScriptChunkFailure(results);
|
|
393
|
+
if (terminalFailure) {
|
|
394
|
+
if (terminalFailure.terminalKind === "delivery")
|
|
395
|
+
throw new Error(terminalFailure.error);
|
|
396
|
+
if (terminalFailure.terminalKind === "transport")
|
|
397
|
+
throw new Error(`本机 Codex 脚本引擎连续异常且自动恢复未成功,已停止当前任务且未提交缺失脚本。诊断已保存在本机 Agent 日志中(${terminalFailure.error})`);
|
|
398
|
+
throw new Error(`本机脚本结构化契约被 Codex 拒绝,已停止自动重试且未提交缺失脚本。请先升级或修复 Canvas Agent,再手动重试(${terminalFailure.error})`);
|
|
356
399
|
}
|
|
357
400
|
const replanOrdinals = scriptCreativeReplanOrdinals(results);
|
|
358
401
|
if (replanOrdinals.length) {
|
|
@@ -361,9 +404,14 @@ export class WorkflowManager {
|
|
|
361
404
|
record.updatedAt = now();
|
|
362
405
|
this.save();
|
|
363
406
|
const replanResults = await Promise.all(replanOrdinals.map((ordinal) => this.runCandidateChunk(id, task, [ordinal], workspace.workspacePath)));
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
|
|
407
|
+
const replanTerminalFailure = terminalScriptChunkFailure(replanResults);
|
|
408
|
+
if (replanTerminalFailure) {
|
|
409
|
+
if (replanTerminalFailure.terminalKind === "delivery")
|
|
410
|
+
throw new Error(replanTerminalFailure.error);
|
|
411
|
+
if (replanTerminalFailure.terminalKind === "transport")
|
|
412
|
+
throw new Error(`创意重新选题时本机 Codex 进程异常且自动恢复未成功,已停止当前任务(${replanTerminalFailure.error})`);
|
|
413
|
+
throw new Error(`创意重新选题被 Codex 拒绝,已停止自动重试(${replanTerminalFailure.error})`);
|
|
414
|
+
}
|
|
367
415
|
const replanError = replanResults.find((result) => result.error)?.error;
|
|
368
416
|
if (replanError)
|
|
369
417
|
throw new Error(`创意重新选题失败(${replanError}),请点击重试`);
|
|
@@ -394,15 +442,26 @@ export class WorkflowManager {
|
|
|
394
442
|
throw new Error(`中心校验尚未通过(${record.receivedOrdinals.length}/${task.requested_count} 条),请点击重试`);
|
|
395
443
|
}
|
|
396
444
|
catch (error) {
|
|
397
|
-
|
|
445
|
+
const expired = error instanceof ExpiredCapabilityError || error instanceof CommerceRequestError && error.code === "WORKFLOW_CAPABILITY_EXPIRED"
|
|
446
|
+
|| record.lastFailure?.name === "CapabilityExpired" && Date.parse(record.expiresAt || "") <= Date.now();
|
|
447
|
+
record.status = expired ? "expired" : "error";
|
|
398
448
|
record.message = error instanceof Error ? error.message : "本机 Codex 处理失败";
|
|
399
449
|
logger.warn("Local Flow C script handoff paused", { handoffId: id, error: record.message });
|
|
400
450
|
}
|
|
401
451
|
finally {
|
|
452
|
+
const retry = record.retryRequested && record.status !== "complete";
|
|
453
|
+
delete record.retryRequested;
|
|
454
|
+
if (retry) {
|
|
455
|
+
record.status = "queued";
|
|
456
|
+
delete record.threadId;
|
|
457
|
+
record.message = "已保存本机结果,正在优先重试";
|
|
458
|
+
}
|
|
402
459
|
record.updatedAt = now();
|
|
403
460
|
record.activeChunks = 0;
|
|
404
461
|
this.save();
|
|
405
462
|
this.runningScripts.delete(id);
|
|
463
|
+
if (retry)
|
|
464
|
+
this.scheduleScript(id);
|
|
406
465
|
}
|
|
407
466
|
}
|
|
408
467
|
/** Analyze each product once; image 1 owns identity and later images may only corroborate visible evidence. */
|
|
@@ -427,21 +486,37 @@ export class WorkflowManager {
|
|
|
427
486
|
&& missing.some((product) => (product.productImageUrlsInExactOrder || []).length > 1);
|
|
428
487
|
const profileConcurrency = includesSupportingImages ? 1 : FLOW_C_CODEX_WORKER_CONCURRENCY;
|
|
429
488
|
const workerCount = Math.min(missing.length, Math.max(1, profileConcurrency));
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
489
|
+
let stopped = false;
|
|
490
|
+
let firstFailure;
|
|
491
|
+
// Do not release the handoff lock while a sibling model turn can
|
|
492
|
+
// still produce a result. A quick retry must see its durable cache.
|
|
493
|
+
await Promise.allSettled(Array.from({ length: workerCount }, async () => {
|
|
494
|
+
try {
|
|
495
|
+
while (!stopped && nextProduct < missing.length) {
|
|
496
|
+
const product = missing[nextProduct++];
|
|
497
|
+
const productIndex = Number(product.productIndex);
|
|
498
|
+
let profile = cached.get(productIndex);
|
|
499
|
+
if (!profile) {
|
|
500
|
+
profile = await this.runProductExecutionProfile(id, product, cwd, profileContractVersion);
|
|
501
|
+
cached.set(productIndex, profile);
|
|
502
|
+
record.productProfiles = [...cached.values()].sort((left, right) => left.productIndex - right.productIndex);
|
|
503
|
+
record.updatedAt = now();
|
|
504
|
+
this.save();
|
|
505
|
+
}
|
|
506
|
+
if (stopped)
|
|
507
|
+
return;
|
|
508
|
+
await this.submitProductProfileChunk(id, [profile], profileContractVersion);
|
|
441
509
|
}
|
|
442
|
-
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (!stopped)
|
|
513
|
+
firstFailure = error;
|
|
514
|
+
stopped = true;
|
|
515
|
+
throw error;
|
|
443
516
|
}
|
|
444
517
|
}));
|
|
518
|
+
if (stopped)
|
|
519
|
+
throw firstFailure;
|
|
445
520
|
task = await this.scriptTask(id);
|
|
446
521
|
}
|
|
447
522
|
const profiled = Number(task.product_profile_stage?.completed || task.product_execution_profiles?.length || 0);
|
|
@@ -504,8 +579,10 @@ export class WorkflowManager {
|
|
|
504
579
|
onWorkerFinish: () => { const record = this.scriptRecord(id); record.activeChunks = Math.max(0, Number(record.activeChunks || 0) - 1); record.updatedAt = now(); this.save(); },
|
|
505
580
|
});
|
|
506
581
|
this.emitScriptStage(id, ordinals, "candidate_model", result.timings.queueWaitMs + result.timings.threadStartMs + result.timings.modelMs);
|
|
507
|
-
if (!result.ok
|
|
508
|
-
return { error: result.
|
|
582
|
+
if (!result.ok)
|
|
583
|
+
return { error: result.error, terminal: !result.retryable, ...(!result.retryable ? { terminalKind: result.failureKind } : {}) };
|
|
584
|
+
if (!result.text)
|
|
585
|
+
return { error: "Codex 未返回创意候选", terminal: false };
|
|
509
586
|
try {
|
|
510
587
|
const groups = parseFlowCCreativeCandidateOutput(result.text, ordinals);
|
|
511
588
|
const startedAt = Date.now();
|
|
@@ -519,31 +596,44 @@ export class WorkflowManager {
|
|
|
519
596
|
}
|
|
520
597
|
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
521
598
|
async runScriptChunk(id, task, ordinals, cwd, rewriteAttempt = 0, revisionAttempt = 0) {
|
|
522
|
-
const durationSeconds = Number(task.duration_seconds || 10);
|
|
523
|
-
let prompt;
|
|
524
|
-
try {
|
|
525
|
-
prompt = scriptChunkPrompt(id, task, ordinals, rewriteAttempt);
|
|
526
|
-
}
|
|
527
|
-
catch (error) {
|
|
528
|
-
if (isFlowCPromptPayloadTooLarge(error))
|
|
529
|
-
return { error: error.message, terminal: false };
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
532
|
-
const result = await runCodexWorkflowTurn(prompt, this.emit, {
|
|
533
|
-
cwd,
|
|
534
|
-
permissionMode: "full",
|
|
535
|
-
timeoutMs: FLOW_C_CODEX_TURN_TIMEOUT_MS,
|
|
536
|
-
outputSchema: flowCScriptOutputSchema(durationSeconds, ordinals.length),
|
|
537
|
-
onThread: (threadId) => { this.scriptRecord(id).threadId = threadId; this.save(); },
|
|
538
|
-
onWorkerStart: () => { const record = this.scriptRecord(id); record.activeChunks = Number(record.activeChunks || 0) + 1; record.updatedAt = now(); this.save(); },
|
|
539
|
-
onWorkerFinish: () => { const record = this.scriptRecord(id); record.activeChunks = Math.max(0, Number(record.activeChunks || 0) - 1); record.updatedAt = now(); this.save(); },
|
|
540
|
-
});
|
|
541
|
-
this.emitScriptStage(id, ordinals, "queue_wait", result.timings.queueWaitMs);
|
|
542
|
-
this.emitScriptStage(id, ordinals, "thread_start", result.timings.threadStartMs);
|
|
543
|
-
this.emitScriptStage(id, ordinals, "model", result.timings.modelMs);
|
|
544
|
-
if (!result.ok || !result.text)
|
|
545
|
-
return { error: result.ok ? "Codex 未返回可用的结构化脚本" : result.error, terminal: !result.ok && !result.retryable };
|
|
546
599
|
try {
|
|
600
|
+
const record = this.scriptRecord(id);
|
|
601
|
+
ordinals = ordinals.filter((ordinal) => !record.receivedOrdinals.includes(ordinal));
|
|
602
|
+
if (!ordinals.length)
|
|
603
|
+
return { terminal: false };
|
|
604
|
+
const cached = (record.pendingScriptJobs || []).filter((job) => ordinals.includes(job.ordinal));
|
|
605
|
+
if (cached.length) {
|
|
606
|
+
await this.submitGeneratedScriptJobs(id, cached);
|
|
607
|
+
ordinals = ordinals.filter((ordinal) => !record.receivedOrdinals.includes(ordinal));
|
|
608
|
+
if (!ordinals.length)
|
|
609
|
+
return { terminal: false };
|
|
610
|
+
}
|
|
611
|
+
const durationSeconds = Number(task.duration_seconds || 10);
|
|
612
|
+
let prompt;
|
|
613
|
+
try {
|
|
614
|
+
prompt = scriptChunkPrompt(id, task, ordinals, rewriteAttempt);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (isFlowCPromptPayloadTooLarge(error))
|
|
618
|
+
return { error: error.message, terminal: false };
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
const result = await runCodexWorkflowTurn(prompt, this.emit, {
|
|
622
|
+
cwd,
|
|
623
|
+
permissionMode: "full",
|
|
624
|
+
timeoutMs: FLOW_C_CODEX_TURN_TIMEOUT_MS,
|
|
625
|
+
outputSchema: flowCScriptOutputSchema(durationSeconds, ordinals.length),
|
|
626
|
+
onThread: (threadId) => { this.scriptRecord(id).threadId = threadId; this.save(); },
|
|
627
|
+
onWorkerStart: () => { const record = this.scriptRecord(id); record.activeChunks = Number(record.activeChunks || 0) + 1; record.updatedAt = now(); this.save(); },
|
|
628
|
+
onWorkerFinish: () => { const record = this.scriptRecord(id); record.activeChunks = Math.max(0, Number(record.activeChunks || 0) - 1); record.updatedAt = now(); this.save(); },
|
|
629
|
+
});
|
|
630
|
+
this.emitScriptStage(id, ordinals, "queue_wait", result.timings.queueWaitMs);
|
|
631
|
+
this.emitScriptStage(id, ordinals, "thread_start", result.timings.threadStartMs);
|
|
632
|
+
this.emitScriptStage(id, ordinals, "model", result.timings.modelMs);
|
|
633
|
+
if (!result.ok)
|
|
634
|
+
return { error: result.error, terminal: !result.retryable, ...(!result.retryable ? { terminalKind: result.failureKind } : {}) };
|
|
635
|
+
if (!result.text)
|
|
636
|
+
return { error: "Codex 未返回可用的结构化脚本", terminal: false };
|
|
547
637
|
const parseStartedAt = Date.now();
|
|
548
638
|
const selected = selectedCandidatesForOrdinals(task, ordinals);
|
|
549
639
|
const jobs = parseFlowCScriptOutput(result.text, ordinals).map((job) => ({
|
|
@@ -559,6 +649,14 @@ export class WorkflowManager {
|
|
|
559
649
|
return { terminal: false };
|
|
560
650
|
}
|
|
561
651
|
catch (error) {
|
|
652
|
+
// Only explicit semantic rejection may discard the affected saved output.
|
|
653
|
+
// Network/response loss, auth failures and validation stops retain it.
|
|
654
|
+
const invalidated = [...candidateRevisionChangedOrdinals(error), ...scriptRewriteOrdinals(error), ...creativeReplanOrdinals(error)];
|
|
655
|
+
if (invalidated.length) {
|
|
656
|
+
const record = this.scriptRecord(id);
|
|
657
|
+
record.pendingScriptJobs = (record.pendingScriptJobs || []).filter((job) => !invalidated.includes(job.ordinal));
|
|
658
|
+
this.save();
|
|
659
|
+
}
|
|
562
660
|
const revisionOrdinals = candidateRevisionChangedOrdinals(error);
|
|
563
661
|
if (revisionOrdinals.length && !creativeReplanOrdinals(error).length) {
|
|
564
662
|
if (revisionAttempt >= FLOW_C_CANDIDATE_REVISION_MAX_ATTEMPTS) {
|
|
@@ -579,9 +677,11 @@ export class WorkflowManager {
|
|
|
579
677
|
if (!pendingRevisionOrdinals.length)
|
|
580
678
|
return { terminal: false };
|
|
581
679
|
const revisionResults = await Promise.all(pendingRevisionOrdinals.map((ordinal) => this.runScriptChunk(id, refreshedTask, [ordinal], cwd, 0, revisionAttempt + 1)));
|
|
582
|
-
const
|
|
680
|
+
const revisionTerminalFailure = terminalScriptChunkFailure(revisionResults);
|
|
681
|
+
const revisionError = revisionTerminalFailure?.error || revisionResults.map((result) => result.error).filter(Boolean).join(";");
|
|
583
682
|
return {
|
|
584
|
-
terminal: Boolean(
|
|
683
|
+
terminal: Boolean(revisionTerminalFailure),
|
|
684
|
+
...(revisionTerminalFailure?.terminalKind ? { terminalKind: revisionTerminalFailure.terminalKind } : {}),
|
|
585
685
|
...(revisionError ? { error: revisionError } : {}),
|
|
586
686
|
...(scriptCreativeReplanOrdinals(revisionResults).length ? { replanOrdinals: scriptCreativeReplanOrdinals(revisionResults) } : {}),
|
|
587
687
|
};
|
|
@@ -605,14 +705,18 @@ export class WorkflowManager {
|
|
|
605
705
|
if (!pendingRewriteOrdinals.length)
|
|
606
706
|
return preserveScriptRecoveryReplans({ terminal: false }, error);
|
|
607
707
|
const rewriteResults = await Promise.all(pendingRewriteOrdinals.map((ordinal) => this.runScriptChunk(id, refreshedTask, [ordinal], cwd, rewriteAttempt + 1)));
|
|
608
|
-
const
|
|
708
|
+
const rewriteTerminalFailure = terminalScriptChunkFailure(rewriteResults);
|
|
709
|
+
const rewriteError = rewriteTerminalFailure?.error || rewriteResults.map((result) => result.error).filter(Boolean).join(";");
|
|
609
710
|
const rewriteResult = {
|
|
610
|
-
terminal: Boolean(
|
|
711
|
+
terminal: Boolean(rewriteTerminalFailure),
|
|
712
|
+
...(rewriteTerminalFailure?.terminalKind ? { terminalKind: rewriteTerminalFailure.terminalKind } : {}),
|
|
611
713
|
...(rewriteError ? { error: rewriteError } : {}),
|
|
612
714
|
...(scriptCreativeReplanOrdinals(rewriteResults).length ? { replanOrdinals: scriptCreativeReplanOrdinals(rewriteResults) } : {}),
|
|
613
715
|
};
|
|
614
716
|
return preserveScriptRecoveryReplans(rewriteResult, error);
|
|
615
717
|
}
|
|
718
|
+
if (error instanceof CommerceRequestError && !creativeReplanOrdinals(error).length)
|
|
719
|
+
return { error: error.message, terminal: true, terminalKind: "delivery" };
|
|
616
720
|
return {
|
|
617
721
|
error: error instanceof Error ? `结构化脚本未通过本机校验:${error.message}` : "结构化脚本未通过本机校验",
|
|
618
722
|
terminal: terminalScriptValidationError(error),
|
|
@@ -634,7 +738,7 @@ export class WorkflowManager {
|
|
|
634
738
|
return;
|
|
635
739
|
}
|
|
636
740
|
catch (error) {
|
|
637
|
-
if (jobs.length === 1)
|
|
741
|
+
if (jobs.length === 1 || error instanceof CommerceRequestError && ![400, 409, 422].includes(error.status || 0))
|
|
638
742
|
throw error;
|
|
639
743
|
let accepted = 0;
|
|
640
744
|
const failures = [];
|
|
@@ -644,6 +748,8 @@ export class WorkflowManager {
|
|
|
644
748
|
accepted += 1;
|
|
645
749
|
}
|
|
646
750
|
catch (jobError) {
|
|
751
|
+
if (jobError instanceof CommerceRequestError && ![400, 409, 422].includes(jobError.status || 0))
|
|
752
|
+
throw jobError;
|
|
647
753
|
failures.push(jobError);
|
|
648
754
|
}
|
|
649
755
|
}
|
|
@@ -793,7 +899,10 @@ export function compareScriptQueueRecords(left, right) {
|
|
|
793
899
|
}
|
|
794
900
|
/** A deterministic response-format failure stops fallback isolation immediately. */
|
|
795
901
|
export function terminalScriptChunkError(results) {
|
|
796
|
-
return results
|
|
902
|
+
return terminalScriptChunkFailure(results)?.error || "";
|
|
903
|
+
}
|
|
904
|
+
export function terminalScriptChunkFailure(results) {
|
|
905
|
+
return results.find((result) => result.terminal);
|
|
797
906
|
}
|
|
798
907
|
export function scriptCreativeReplanOrdinals(results) {
|
|
799
908
|
return [...new Set(results.flatMap((result) => (result && typeof result === "object" ? result.replanOrdinals || [] : [])).filter((ordinal) => Number.isInteger(ordinal) && ordinal > 0))].sort((left, right) => left - right);
|
|
@@ -1304,23 +1413,11 @@ function safeDownloadUrl(value) { const url = new URL(String(value || "")); if (
|
|
|
1304
1413
|
throw new Error("下载地址必须使用 HTTPS"); return url.toString(); }
|
|
1305
1414
|
function safeName(value) { return String(value || "市场").replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[. ]+$/g, "").slice(0, 60) || "市场"; }
|
|
1306
1415
|
function now() { return new Date().toISOString(); }
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
headers.set("content-type", "application/json");
|
|
1313
|
-
const response = await fetch(url, { ...init, headers, signal: AbortSignal.timeout(120_000) });
|
|
1314
|
-
const body = await response.json().catch(() => ({}));
|
|
1315
|
-
if (!response.ok) {
|
|
1316
|
-
const error = new Error(body.error || `中心接口返回 ${response.status}`);
|
|
1317
|
-
error.status = response.status;
|
|
1318
|
-
error.code = body.code;
|
|
1319
|
-
error.resetOrdinals = body.resetOrdinals;
|
|
1320
|
-
error.rewriteOrdinals = body.rewriteOrdinals;
|
|
1321
|
-
throw error;
|
|
1322
|
-
}
|
|
1323
|
-
return body;
|
|
1416
|
+
export function mergeProductProfiles(local = [], confirmed = []) {
|
|
1417
|
+
const merged = new Map(local.map((profile) => [Number(profile.productIndex), profile]));
|
|
1418
|
+
for (const profile of confirmed || [])
|
|
1419
|
+
merged.set(Number(profile.productIndex), profile);
|
|
1420
|
+
return [...merged.values()].sort((left, right) => left.productIndex - right.productIndex);
|
|
1324
1421
|
}
|
|
1325
1422
|
function loadState() {
|
|
1326
1423
|
try {
|