gaoding-cli 1.0.0-alpha.5

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.
Files changed (177) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/SECURITY.md +34 -0
  5. package/dist/bin/gd-cli.d.ts +2 -0
  6. package/dist/bin/gd-cli.js +10 -0
  7. package/dist/src/api/app-runner.d.ts +33 -0
  8. package/dist/src/api/app-runner.js +1177 -0
  9. package/dist/src/api/dam-client.d.ts +111 -0
  10. package/dist/src/api/dam-client.js +897 -0
  11. package/dist/src/api/direct-auth.d.ts +3 -0
  12. package/dist/src/api/direct-auth.js +11 -0
  13. package/dist/src/api/direct-client.d.ts +27 -0
  14. package/dist/src/api/direct-client.js +151 -0
  15. package/dist/src/api/direct-tools.d.ts +43 -0
  16. package/dist/src/api/direct-tools.js +1155 -0
  17. package/dist/src/api/hub-client.d.ts +20 -0
  18. package/dist/src/api/hub-client.js +99 -0
  19. package/dist/src/api/sso-client.d.ts +60 -0
  20. package/dist/src/api/sso-client.js +140 -0
  21. package/dist/src/api/task-poller.d.ts +38 -0
  22. package/dist/src/api/task-poller.js +72 -0
  23. package/dist/src/api/tool-invoker.d.ts +12 -0
  24. package/dist/src/api/tool-invoker.js +11 -0
  25. package/dist/src/api/usage-client.d.ts +39 -0
  26. package/dist/src/api/usage-client.js +179 -0
  27. package/dist/src/auth/credential-store-v2.d.ts +12 -0
  28. package/dist/src/auth/credential-store-v2.js +59 -0
  29. package/dist/src/commands/auth.d.ts +2 -0
  30. package/dist/src/commands/auth.js +191 -0
  31. package/dist/src/commands/dam.d.ts +2 -0
  32. package/dist/src/commands/dam.js +293 -0
  33. package/dist/src/commands/models.d.ts +2 -0
  34. package/dist/src/commands/models.js +78 -0
  35. package/dist/src/commands/org.d.ts +2 -0
  36. package/dist/src/commands/org.js +137 -0
  37. package/dist/src/commands/skills.d.ts +2 -0
  38. package/dist/src/commands/skills.js +213 -0
  39. package/dist/src/commands/tools.d.ts +4 -0
  40. package/dist/src/commands/tools.js +383 -0
  41. package/dist/src/commands/update.d.ts +2 -0
  42. package/dist/src/commands/update.js +167 -0
  43. package/dist/src/commands/usage.d.ts +7 -0
  44. package/dist/src/commands/usage.js +43 -0
  45. package/dist/src/commands/workflows.d.ts +3 -0
  46. package/dist/src/commands/workflows.js +398 -0
  47. package/dist/src/core/auth/auth-provider.d.ts +8 -0
  48. package/dist/src/core/auth/auth-provider.js +12 -0
  49. package/dist/src/core/auth/device-oauth-provider.d.ts +12 -0
  50. package/dist/src/core/auth/device-oauth-provider.js +124 -0
  51. package/dist/src/core/auth/org-manager.d.ts +26 -0
  52. package/dist/src/core/auth/org-manager.js +224 -0
  53. package/dist/src/core/auth/signature.d.ts +19 -0
  54. package/dist/src/core/auth/signature.js +33 -0
  55. package/dist/src/core/auth/types.d.ts +52 -0
  56. package/dist/src/core/auth/types.js +3 -0
  57. package/dist/src/core/output/cli-error.d.ts +5 -0
  58. package/dist/src/core/output/cli-error.js +8 -0
  59. package/dist/src/core/output/detector.d.ts +5 -0
  60. package/dist/src/core/output/detector.js +14 -0
  61. package/dist/src/core/output/engine.d.ts +32 -0
  62. package/dist/src/core/output/engine.js +77 -0
  63. package/dist/src/core/output/envelope.d.ts +40 -0
  64. package/dist/src/core/output/envelope.js +119 -0
  65. package/dist/src/core/output/index.d.ts +4 -0
  66. package/dist/src/core/output/index.js +4 -0
  67. package/dist/src/core/output/live-display.d.ts +15 -0
  68. package/dist/src/core/output/live-display.js +111 -0
  69. package/dist/src/core/output/result-card.d.ts +18 -0
  70. package/dist/src/core/output/result-card.js +39 -0
  71. package/dist/src/core/prompt/index.d.ts +31 -0
  72. package/dist/src/core/prompt/index.js +36 -0
  73. package/dist/src/io.d.ts +14 -0
  74. package/dist/src/io.js +31 -0
  75. package/dist/src/middleware/auth.d.ts +3 -0
  76. package/dist/src/middleware/auth.js +74 -0
  77. package/dist/src/middleware/error-handler.d.ts +3 -0
  78. package/dist/src/middleware/error-handler.js +431 -0
  79. package/dist/src/middleware/interactive-params.d.ts +14 -0
  80. package/dist/src/middleware/interactive-params.js +53 -0
  81. package/dist/src/middleware/output.d.ts +14 -0
  82. package/dist/src/middleware/output.js +546 -0
  83. package/dist/src/models/model-catalog.d.ts +26 -0
  84. package/dist/src/models/model-catalog.js +216 -0
  85. package/dist/src/program.d.ts +2 -0
  86. package/dist/src/program.js +152 -0
  87. package/dist/src/registry/agent-contracts.d.ts +43 -0
  88. package/dist/src/registry/agent-contracts.js +483 -0
  89. package/dist/src/registry/app-loader.d.ts +2 -0
  90. package/dist/src/registry/app-loader.js +70 -0
  91. package/dist/src/registry/direct-registry.d.ts +11 -0
  92. package/dist/src/registry/direct-registry.js +234 -0
  93. package/dist/src/registry/registry-cache.d.ts +10 -0
  94. package/dist/src/registry/registry-cache.js +52 -0
  95. package/dist/src/registry/registry-client.d.ts +28 -0
  96. package/dist/src/registry/registry-client.js +50 -0
  97. package/dist/src/registry/types.d.ts +69 -0
  98. package/dist/src/registry/types.js +1 -0
  99. package/dist/src/tty/auth-presenter.d.ts +3 -0
  100. package/dist/src/tty/auth-presenter.js +21 -0
  101. package/dist/src/update/install-source.d.ts +19 -0
  102. package/dist/src/update/install-source.js +139 -0
  103. package/dist/src/update/notifier.d.ts +2 -0
  104. package/dist/src/update/notifier.js +59 -0
  105. package/dist/src/update/update-checker.d.ts +22 -0
  106. package/dist/src/update/update-checker.js +110 -0
  107. package/dist/src/update/version.d.ts +9 -0
  108. package/dist/src/update/version.js +52 -0
  109. package/dist/src/utils/agent-skill-installer.d.ts +57 -0
  110. package/dist/src/utils/agent-skill-installer.js +389 -0
  111. package/dist/src/utils/capability-metadata.d.ts +18 -0
  112. package/dist/src/utils/capability-metadata.js +88 -0
  113. package/dist/src/utils/capability-presenter.d.ts +4 -0
  114. package/dist/src/utils/capability-presenter.js +190 -0
  115. package/dist/src/utils/config.d.ts +16 -0
  116. package/dist/src/utils/config.js +54 -0
  117. package/dist/src/utils/credential-permissions.d.ts +2 -0
  118. package/dist/src/utils/credential-permissions.js +22 -0
  119. package/dist/src/utils/ecommerce-input-guidance.d.ts +15 -0
  120. package/dist/src/utils/ecommerce-input-guidance.js +85 -0
  121. package/dist/src/utils/endpoints.d.ts +52 -0
  122. package/dist/src/utils/endpoints.js +258 -0
  123. package/dist/src/utils/enrich-upload-params.d.ts +6 -0
  124. package/dist/src/utils/enrich-upload-params.js +11 -0
  125. package/dist/src/utils/entitlement.d.ts +17 -0
  126. package/dist/src/utils/entitlement.js +146 -0
  127. package/dist/src/utils/gd-home.d.ts +7 -0
  128. package/dist/src/utils/gd-home.js +26 -0
  129. package/dist/src/utils/help-verify.d.ts +9 -0
  130. package/dist/src/utils/help-verify.js +41 -0
  131. package/dist/src/utils/input-json.d.ts +3 -0
  132. package/dist/src/utils/input-json.js +61 -0
  133. package/dist/src/utils/list-output-format.d.ts +10 -0
  134. package/dist/src/utils/list-output-format.js +34 -0
  135. package/dist/src/utils/logger.d.ts +6 -0
  136. package/dist/src/utils/logger.js +19 -0
  137. package/dist/src/utils/matting-compose.d.ts +10 -0
  138. package/dist/src/utils/matting-compose.js +45 -0
  139. package/dist/src/utils/open-browser.d.ts +1 -0
  140. package/dist/src/utils/open-browser.js +4 -0
  141. package/dist/src/utils/output-path.d.ts +5 -0
  142. package/dist/src/utils/output-path.js +70 -0
  143. package/dist/src/utils/redact.d.ts +3 -0
  144. package/dist/src/utils/redact.js +96 -0
  145. package/dist/src/utils/reference-image.d.ts +8 -0
  146. package/dist/src/utils/reference-image.js +43 -0
  147. package/dist/src/utils/resolve-app-local-images.d.ts +8 -0
  148. package/dist/src/utils/resolve-app-local-images.js +84 -0
  149. package/dist/src/utils/sensitive-path.d.ts +6 -0
  150. package/dist/src/utils/sensitive-path.js +69 -0
  151. package/dist/src/utils/settings-store.d.ts +42 -0
  152. package/dist/src/utils/settings-store.js +50 -0
  153. package/dist/src/utils/sleep.d.ts +1 -0
  154. package/dist/src/utils/sleep.js +3 -0
  155. package/dist/src/utils/spinner.d.ts +2 -0
  156. package/dist/src/utils/spinner.js +4 -0
  157. package/dist/src/utils/static-help.d.ts +2 -0
  158. package/dist/src/utils/static-help.js +3 -0
  159. package/dist/src/utils/tool-display.d.ts +10 -0
  160. package/dist/src/utils/tool-display.js +55 -0
  161. package/dist/src/utils/transient-network-error.d.ts +4 -0
  162. package/dist/src/utils/transient-network-error.js +40 -0
  163. package/dist/src/utils/url-safety.d.ts +6 -0
  164. package/dist/src/utils/url-safety.js +67 -0
  165. package/dist/src/utils/write-app-deliverables.d.ts +18 -0
  166. package/dist/src/utils/write-app-deliverables.js +386 -0
  167. package/package.json +91 -0
  168. package/skills/gd-cli/SKILL.md +57 -0
  169. package/skills/gd-cli/references/auth.md +31 -0
  170. package/skills/gd-cli/references/deliverables.md +22 -0
  171. package/skills/gd-cli/references/entitlement.md +21 -0
  172. package/skills/gd-cli/references/errors.md +19 -0
  173. package/skills/gd-cli/references/org.md +18 -0
  174. package/skills/gd-cli/references/sop.md +25 -0
  175. package/skills/gd-cli/references/tools.md +29 -0
  176. package/skills/gd-cli/references/update.md +16 -0
  177. package/skills/gd-cli/references/workflows.md +22 -0
@@ -0,0 +1,1177 @@
1
+ import { DirectToolInvoker } from "./direct-tools.js";
2
+ import { DirectApiError } from "./direct-client.js";
3
+ /** 多步 App 含多次生图/对话,默认至少等待 10 分钟 */
4
+ const APP_RUN_MIN_WAIT_SEC = 600;
5
+ function resolveAppWaitTimeout(explicit, estimatedSeconds) {
6
+ if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) {
7
+ return Math.floor(explicit);
8
+ }
9
+ const est = Number(estimatedSeconds);
10
+ if (Number.isFinite(est) && est > APP_RUN_MIN_WAIT_SEC) {
11
+ return Math.ceil(est);
12
+ }
13
+ return APP_RUN_MIN_WAIT_SEC;
14
+ }
15
+ function resolveConcurrency(value) {
16
+ const numeric = Number(value);
17
+ if (!Number.isFinite(numeric) || numeric <= 0)
18
+ return 2;
19
+ return Math.max(1, Math.min(4, Math.floor(numeric)));
20
+ }
21
+ async function mapWithConcurrency(items, concurrency, worker) {
22
+ let next = 0;
23
+ const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
24
+ while (next < items.length) {
25
+ const index = next;
26
+ next += 1;
27
+ await worker(items[index], index);
28
+ }
29
+ });
30
+ await Promise.all(runners);
31
+ }
32
+ class AppWorkflowFailure extends Error {
33
+ result;
34
+ constructor(result) {
35
+ const error = result.error;
36
+ super(typeof error?.message === "string" ? error.message : "App workflow 执行失败");
37
+ this.result = result;
38
+ this.name = "AppWorkflowFailure";
39
+ }
40
+ }
41
+ class AppWorkflowContext {
42
+ appId;
43
+ options;
44
+ outputs = {};
45
+ steps = [];
46
+ order = 0;
47
+ constructor(appId, options = {}) {
48
+ this.appId = appId;
49
+ this.options = options;
50
+ }
51
+ async invoke(stepId, stepName, tool, fn) {
52
+ const base = this.newStep(stepId, stepName, tool);
53
+ this.emitStepStart(base);
54
+ try {
55
+ const invocation = await fn();
56
+ if (invocation.status === "failed") {
57
+ throw taskResultFailure(invocation);
58
+ }
59
+ const completedStep = { ...base, status: "completed", invocation };
60
+ this.steps.push(completedStep);
61
+ this.emitStepComplete(completedStep, invocation);
62
+ return invocation;
63
+ }
64
+ catch (error) {
65
+ const normalized = normalizeAppStepError(error, base);
66
+ const failed = { ...base, status: "failed", error: normalized };
67
+ this.steps.push(failed);
68
+ this.emitStepComplete(failed);
69
+ throw new AppWorkflowFailure(this.failed(normalized, failed));
70
+ }
71
+ }
72
+ async invokeOptional(stepId, stepName, tool, fn, fallback) {
73
+ const base = this.newStep(stepId, stepName, tool);
74
+ this.emitStepStart(base);
75
+ try {
76
+ const invocation = await fn();
77
+ if (invocation.status === "failed") {
78
+ throw taskResultFailure(invocation);
79
+ }
80
+ const completedStep = { ...base, status: "completed", invocation, optional: true };
81
+ this.steps.push(completedStep);
82
+ this.emitStepComplete(completedStep, invocation);
83
+ return invocation;
84
+ }
85
+ catch (error) {
86
+ const normalized = normalizeAppStepError(error, base);
87
+ const failed = { ...base, status: "failed", error: normalized, optional: true };
88
+ this.steps.push(failed);
89
+ this.emitStepComplete(failed);
90
+ return fallback();
91
+ }
92
+ }
93
+ addAsset(label, role, result) {
94
+ const url = outputString(result, "url", false);
95
+ if (!url)
96
+ return;
97
+ const assets = Array.isArray(this.outputs.assets)
98
+ ? this.outputs.assets
99
+ : [];
100
+ this.outputs.assets = [
101
+ ...assets.filter((item) => item.url !== url),
102
+ { label, role, type: "image", url },
103
+ ];
104
+ }
105
+ completed(extraOutputs = {}) {
106
+ return completedApp(this.appId, {
107
+ ...this.outputs,
108
+ ...extraOutputs,
109
+ step_outputs: this.orderedSteps(),
110
+ completed_steps: this.completedSteps(),
111
+ });
112
+ }
113
+ failed(error, failedStep) {
114
+ return {
115
+ task_id: `app-${this.appId}-${Date.now()}`,
116
+ status: "failed",
117
+ error: {
118
+ ...error,
119
+ app_id: this.appId,
120
+ step: publicStep(failedStep),
121
+ },
122
+ outputs: {
123
+ ...this.outputs,
124
+ partial: true,
125
+ step_outputs: this.orderedSteps(),
126
+ completed_steps: this.completedSteps(),
127
+ failed_step: publicStep(failedStep),
128
+ },
129
+ };
130
+ }
131
+ newStep(stepId, stepName, tool) {
132
+ this.order += 1;
133
+ return {
134
+ step_order: this.order,
135
+ step_id: stepId,
136
+ step_name: stepName,
137
+ tool,
138
+ };
139
+ }
140
+ completedSteps() {
141
+ return this.orderedSteps()
142
+ .filter((step) => step.status === "completed")
143
+ .map(publicStep);
144
+ }
145
+ orderedSteps() {
146
+ return [...this.steps].sort((a, b) => a.step_order - b.step_order);
147
+ }
148
+ emitStepStart(step) {
149
+ this.options.onStepStart?.({
150
+ step_order: step.step_order,
151
+ step_id: step.step_id,
152
+ step_name: step.step_name,
153
+ tool: step.tool,
154
+ status: "running",
155
+ });
156
+ }
157
+ emitStepComplete(step, result) {
158
+ this.options.onStepComplete?.({
159
+ step_order: step.step_order,
160
+ step_id: step.step_id,
161
+ step_name: step.step_name,
162
+ tool: step.tool,
163
+ status: step.status,
164
+ }, result);
165
+ }
166
+ }
167
+ export class AppRunner {
168
+ invoker = new DirectToolInvoker();
169
+ constructor(client) {
170
+ void client;
171
+ }
172
+ async run(appId, params, options = {}) {
173
+ if (options.async) {
174
+ return {
175
+ task_id: `app-${appId}-${Date.now()}`,
176
+ status: "failed",
177
+ message: "本地 App workflow 暂不支持 --async,请去掉 --async 直接运行。",
178
+ };
179
+ }
180
+ void resolveAppWaitTimeout(options.timeout, undefined);
181
+ void options.outputDir;
182
+ try {
183
+ if (appId === "listing-gallery")
184
+ return await this.runListingGallery(params, options);
185
+ if (appId === "pdp-a-plus")
186
+ return await this.runPdpAPlus(params, options);
187
+ if (appId === "social-seeding-pack")
188
+ return await this.runSocialSeedingPack(params, options);
189
+ if (appId === "campaign-kv-pack")
190
+ return await this.runCampaignKvPack(params, options);
191
+ }
192
+ catch (error) {
193
+ if (error instanceof AppWorkflowFailure)
194
+ return error.result;
195
+ throw error;
196
+ }
197
+ throw new Error("暂不支持的本地 App workflow: " + appId);
198
+ }
199
+ async runListingGallery(params, options) {
200
+ const ctx = new AppWorkflowContext("listing-gallery", options);
201
+ const image = requireString(params.image ?? params.images, "listing-gallery 需要 --image");
202
+ const brief = requireString(params.brief ?? params.prompt, "listing-gallery 需要 --brief");
203
+ const profile = inferProductCategoryProfile(brief);
204
+ const copyPlan = resolveCopyPlan(params, brief);
205
+ const ratios = resolveRoleRatios(params.ratio, {
206
+ main_image: "1:1",
207
+ selling_point_image: "4:5",
208
+ detail_image: "4:5",
209
+ lifestyle_image: "4:5",
210
+ });
211
+ const prepared = await this.prepareProduct(ctx, image, allowSensitivePath(params));
212
+ const title = await this.generateTitle(ctx, brief);
213
+ const titleText = outputString(title, "text", false) || brief;
214
+ const promptPlan = await this.optimizePrompt(ctx, "prompt_plan", "生成图库素材规划", `为电商 Listing 图库生成素材规划:${brief}。品类判断:${profile.label}。需要覆盖白底主图、单卖点证明图、材质/结构细节图、生活方式图;${copyPlanningGuidance(copyPlan)}每张图只回答一个购买问题,不生成假认证和不可验证参数。`, prepared.cutoutUrl, `Listing 图库素材规划:主图、卖点图、细节图、生活方式图。品类:${profile.label}。商品卖点:${brief}`);
215
+ const galleryCopy = buildListingGalleryCopy(titleText, brief);
216
+ const overlayCopy = {
217
+ selling_point_image: buildOverlayCopy("卖点图", titleText, brief),
218
+ detail_image: buildOverlayCopy("细节图", titleText, brief),
219
+ lifestyle_image: buildOverlayCopy("生活方式图", titleText, brief),
220
+ };
221
+ Object.assign(ctx.outputs, {
222
+ text: buildSummary([
223
+ ["标题", titleText],
224
+ ["图库交付说明", galleryCopy],
225
+ ["素材规划", promptPlan.text],
226
+ ]),
227
+ title_text: titleText,
228
+ delivery_copy: galleryCopy,
229
+ prompt_plan: promptPlan.text,
230
+ copy_plan: copyPlanOutput(copyPlan),
231
+ overlay_copy: overlayCopy,
232
+ category_profile: publicCategoryProfile(profile),
233
+ ratios,
234
+ title: title.outputs,
235
+ prompt: promptPlan.result.outputs,
236
+ });
237
+ await this.generateImageJobs(ctx, [
238
+ {
239
+ key: "main_image",
240
+ label: "白底主图",
241
+ stepName: "生成白底主图",
242
+ image: prepared.cutoutUrl,
243
+ ratio: ratios.main_image,
244
+ model: copyPlan.imageModel,
245
+ prompt: buildEcommerceImagePrompt({
246
+ role: "白底商品主图",
247
+ brief,
248
+ profile,
249
+ commercialRole: "Listing 第一张主图,用于平台审核和缩略图识别,让用户看清商品主体、颜色、形态、包装和配件边界。",
250
+ visibleEvidence: "商品完整居中,轮廓清晰,标签或关键识别面可读,主体占画面 75%-80%。",
251
+ composition: "暖白或纯白背景,真实柔和投影,干净棚拍光,画面没有道具、文字、促销元素和装饰。",
252
+ platformFit: "适合 Amazon / 天猫 / 京东基础上架主图;优先保证过审和主体保真。",
253
+ copyMode: "no-text",
254
+ }),
255
+ },
256
+ {
257
+ key: "selling_point_image",
258
+ label: "卖点图",
259
+ stepName: "生成卖点图",
260
+ image: prepared.cutoutUrl,
261
+ ratio: ratios.selling_point_image,
262
+ model: copyPlan.imageModel,
263
+ prompt: buildEcommerceImagePrompt({
264
+ role: "核心卖点证明图",
265
+ brief,
266
+ profile,
267
+ commercialRole: "Listing 第二张图,只解释一个核心购买利益点,不把多个卖点塞进同一张图。",
268
+ visibleEvidence: `围绕“${primaryBriefPoint(brief)}”设计可见证据,效果只出现在相关产品部位附近,不编造数字、不生成假认证。`,
269
+ composition: "商品在前景占主导,背景用克制的场景或视觉隐喻辅助说明,左上或右侧保留后期短标题空间。",
270
+ platformFit: "适合电商图库卖点图,默认交付可排版成品所需视觉证据和短文案,不交付空白占位图。",
271
+ copyMode: copyPlan.mode,
272
+ overlayCopy: overlayCopy.selling_point_image,
273
+ }),
274
+ },
275
+ {
276
+ key: "detail_image",
277
+ label: "细节图",
278
+ stepName: "生成细节图",
279
+ image: prepared.cutoutUrl,
280
+ ratio: ratios.detail_image,
281
+ model: copyPlan.imageModel,
282
+ prompt: buildEcommerceImagePrompt({
283
+ role: "材质结构细节图",
284
+ brief,
285
+ profile,
286
+ commercialRole: "Listing 第三张图,用近景或微距证明材质、结构、接口、按钮、织物纹理、包装或关键部件真实存在。",
287
+ visibleEvidence: profile.detailEvidence,
288
+ composition: "微距商业摄影,侧光突出触感和层次,浅景深但保留足够产品上下文,局部细节和整体识别不冲突。",
289
+ platformFit: "适合详情页和图库细节说明;不生成伪文字、错误标签、额外配件或不存在的内部结构。",
290
+ copyMode: copyPlan.mode,
291
+ overlayCopy: overlayCopy.detail_image,
292
+ }),
293
+ },
294
+ {
295
+ key: "lifestyle_image",
296
+ label: "生活方式图",
297
+ stepName: "生成生活方式图",
298
+ image: prepared.cutoutUrl,
299
+ ratio: ratios.lifestyle_image,
300
+ model: copyPlan.imageModel,
301
+ prompt: buildEcommerceImagePrompt({
302
+ role: "生活方式使用图",
303
+ brief,
304
+ profile,
305
+ commercialRole: "Listing 第四张图,让用户知道商品适合谁、在什么场景使用、使用时的状态是什么。",
306
+ visibleEvidence: profile.usageEvidence,
307
+ composition: "真实生活环境,商品自然融入但仍是视觉主角;可出现手部或背影动作,不出现名人脸和复杂人群。",
308
+ platformFit: "适合小红书、电商图库和独立站补充场景;手机端缩略图仍能一眼看清商品。",
309
+ copyMode: copyPlan.mode,
310
+ overlayCopy: overlayCopy.lifestyle_image,
311
+ }),
312
+ },
313
+ ], options);
314
+ return ctx.completed();
315
+ }
316
+ async runPdpAPlus(params, options) {
317
+ const ctx = new AppWorkflowContext("pdp-a-plus", options);
318
+ const image = requireString(params.image ?? params.images, "pdp-a-plus 需要 --image");
319
+ const brief = requireString(params.brief ?? params.prompt, "pdp-a-plus 需要 --brief");
320
+ const profile = inferProductCategoryProfile(brief);
321
+ const copyPlan = resolveCopyPlan(params, brief);
322
+ const ratios = resolveRoleRatios(params.ratio, {
323
+ hero_module: "16:9",
324
+ pain_point_module: "4:5",
325
+ structure_module: "4:5",
326
+ usage_module: "4:5",
327
+ comparison_module: "1:1",
328
+ });
329
+ const prepared = await this.prepareProduct(ctx, image, allowSensitivePath(params));
330
+ const title = await this.generateTitle(ctx, brief);
331
+ const titleText = outputString(title, "text", false) || brief;
332
+ const promptPlan = await this.optimizePrompt(ctx, "prompt_plan", "生成详情页模块规划", `为电商详情页/A+模块生成购买说服顺序:${brief}。品类判断:${profile.label}。模块包含首屏、痛点解决、结构/细节证明、真实使用场景、对比规格;${copyPlanningGuidance(copyPlan)}不编造参数和认证。`, prepared.cutoutUrl, `详情页模块规划:首屏、痛点、结构细节、使用场景、对比规格。品类:${profile.label}。商品信息:${brief}`);
333
+ const moduleCopy = buildPdpModuleCopy(titleText, brief);
334
+ const overlayCopy = {
335
+ hero_module: buildOverlayCopy("首屏模块", titleText, brief),
336
+ pain_point_module: buildOverlayCopy("痛点模块", titleText, brief),
337
+ structure_module: buildOverlayCopy("结构细节模块", titleText, brief),
338
+ usage_module: buildOverlayCopy("使用场景模块", titleText, brief),
339
+ comparison_module: buildOverlayCopy("对比规格模块", titleText, brief),
340
+ };
341
+ Object.assign(ctx.outputs, {
342
+ text: buildSummary([
343
+ ["标题", titleText],
344
+ ["详情页模块文案", moduleCopy],
345
+ ["模块规划", promptPlan.text],
346
+ ]),
347
+ title_text: titleText,
348
+ delivery_copy: moduleCopy,
349
+ prompt_plan: promptPlan.text,
350
+ copy_plan: copyPlanOutput(copyPlan),
351
+ overlay_copy: overlayCopy,
352
+ selling_points: splitBriefPoints(brief),
353
+ category_profile: publicCategoryProfile(profile),
354
+ ratios,
355
+ title: title.outputs,
356
+ prompt: promptPlan.result.outputs,
357
+ });
358
+ await this.generateImageJobs(ctx, [
359
+ {
360
+ key: "hero_module",
361
+ label: "首屏模块",
362
+ stepName: "生成首屏模块",
363
+ image: prepared.cutoutUrl,
364
+ ratio: ratios.hero_module,
365
+ model: copyPlan.imageModel,
366
+ prompt: buildEcommerceImagePrompt({
367
+ role: "详情页首屏 Hero 模块",
368
+ brief,
369
+ profile,
370
+ commercialRole: "PDP/A+ 首屏模块,用商品全貌和一个核心利益点建立第一购买理由。",
371
+ visibleEvidence: `围绕“${primaryBriefPoint(brief)}”建立视觉锚点,商品清晰、比例准确、品牌识别面可读。`,
372
+ composition: "横版详情页头图构图,商品占据视觉中心或黄金分割位,背景高级克制,留出后期标题和利益点排版区。",
373
+ platformFit: "适合详情页首屏、独立站首屏和 A+ 头图;不交付空白背景,画面必须承载第一购买理由。",
374
+ copyMode: copyPlan.mode,
375
+ overlayCopy: overlayCopy.hero_module,
376
+ }),
377
+ },
378
+ {
379
+ key: "pain_point_module",
380
+ label: "痛点模块",
381
+ stepName: "生成痛点模块",
382
+ image: prepared.cutoutUrl,
383
+ ratio: ratios.pain_point_module,
384
+ model: copyPlan.imageModel,
385
+ prompt: buildEcommerceImagePrompt({
386
+ role: "痛点解决模块",
387
+ brief,
388
+ profile,
389
+ commercialRole: "解释用户为什么需要这件商品,用克制的真实场景表达问题和解决前后关系。",
390
+ visibleEvidence: "一侧呈现用户痛点或使用前困扰,另一侧通过商品出现表达解决方向;不做夸大疗效或不可验证对比。",
391
+ composition: "可使用分区式或前后景关系,画面干净,商品和问题场景关联清晰,保留后期短文案空间。",
392
+ platformFit: "适合详情页说服模块;不生成虚假数字、医疗化表述、认证徽章和过度戏剧化效果。",
393
+ copyMode: copyPlan.mode,
394
+ overlayCopy: overlayCopy.pain_point_module,
395
+ }),
396
+ },
397
+ {
398
+ key: "structure_module",
399
+ label: "结构细节模块",
400
+ stepName: "生成结构细节模块",
401
+ image: prepared.cutoutUrl,
402
+ ratio: ratios.structure_module,
403
+ model: copyPlan.imageModel,
404
+ prompt: buildEcommerceImagePrompt({
405
+ role: "结构细节证明模块",
406
+ brief,
407
+ profile,
408
+ commercialRole: "用材质、结构、接口、配件、织物或关键部件证明卖点,不靠抽象口号。",
409
+ visibleEvidence: profile.detailEvidence,
410
+ composition: "局部放大、轻拆解或微距构图,技术感克制,callout 区可以留白但必须有真实可见细节,不发明内部结构。",
411
+ platformFit: "适合详情页结构/材质说明;参数和箭头由后期设计添加。",
412
+ copyMode: copyPlan.mode,
413
+ overlayCopy: overlayCopy.structure_module,
414
+ }),
415
+ },
416
+ {
417
+ key: "usage_module",
418
+ label: "使用场景模块",
419
+ stepName: "生成使用场景模块",
420
+ image: prepared.cutoutUrl,
421
+ ratio: ratios.usage_module,
422
+ model: copyPlan.imageModel,
423
+ prompt: buildEcommerceImagePrompt({
424
+ role: "真实使用场景模块",
425
+ brief,
426
+ profile,
427
+ commercialRole: "展示商品在目标用户真实环境中的使用动作、空间关系和体验结果。",
428
+ visibleEvidence: profile.usageEvidence,
429
+ composition: "自然光或真实室内光,手部/桌面/空间动作可信,商品始终清晰可见,背景不杂乱。",
430
+ platformFit: "适合 PDP 使用方式说明;不出现名人脸、复杂人物、无关道具和隐藏商品的生活照。",
431
+ copyMode: copyPlan.mode,
432
+ overlayCopy: overlayCopy.usage_module,
433
+ }),
434
+ },
435
+ {
436
+ key: "comparison_module",
437
+ label: "对比规格模块",
438
+ stepName: "生成对比规格模块",
439
+ image: prepared.cutoutUrl,
440
+ ratio: ratios.comparison_module,
441
+ model: copyPlan.imageModel,
442
+ prompt: buildEcommerceImagePrompt({
443
+ role: "对比规格模块",
444
+ brief,
445
+ profile,
446
+ commercialRole: "为尺寸、规格、适配、SKU 或参数对比预留视觉框架,只表达确定的信息。",
447
+ visibleEvidence: "商品以准确透视和比例陈列,可放入真实尺度参照物;没有明确参数时只展示规格对比场景和信息框架,不生成具体数字。",
448
+ composition: "理性网格或干净桌面陈列,留出后期规格表和短标签空间,商品不变形。",
449
+ platformFit: "适合详情页参数模块;不生成假规格、假认证、假价格和错误 SKU。",
450
+ copyMode: copyPlan.mode,
451
+ overlayCopy: overlayCopy.comparison_module,
452
+ }),
453
+ },
454
+ ], options);
455
+ return ctx.completed();
456
+ }
457
+ async runSocialSeedingPack(params, options) {
458
+ const ctx = new AppWorkflowContext("social-seeding-pack", options);
459
+ const image = requireString(params.image ?? params.images, "social-seeding-pack 需要 --image");
460
+ const brief = requireString(params.brief ?? params.prompt, "social-seeding-pack 需要 --brief");
461
+ const profile = inferProductCategoryProfile(brief);
462
+ const copyPlan = resolveCopyPlan(params, brief);
463
+ const ratios = resolveRoleRatios(params.ratio, {
464
+ cover_image: "3:4",
465
+ scene_image: "9:16",
466
+ });
467
+ const prepared = await this.prepareProduct(ctx, image, allowSensitivePath(params));
468
+ const title = await this.generateTitle(ctx, brief);
469
+ const titleText = outputString(title, "text", false) || brief;
470
+ const xhsCaption = buildXhsCaption(titleText, brief);
471
+ const [coverPrompt, scenePrompt] = await Promise.all([
472
+ this.optimizePrompt(ctx, "cover_prompt", "生成小红书封面提示词", `请生成适合小红书封面图的提示词:${brief}。品类判断:${profile.label}。要求真实创作者视角、移动端缩略图第一眼可读、商品清楚;${copyPlanningGuidance(copyPlan)}无假功效。`, prepared.cutoutUrl, `小红书封面图,真实创作者视角,移动端第一眼可读,保留标题空间。品类:${profile.label}。需求:${brief}`),
473
+ this.optimizePrompt(ctx, "scene_prompt", "生成小红书场景图提示词", `请生成适合小红书种草内容图的提示词:${brief}。品类判断:${profile.label}。要求真实生活台面、通勤、开箱或使用动作,有分享感,商品主体一致且不被场景吞掉。`, prepared.cutoutUrl, `小红书种草场景图,真实生活方式,自然动作,分享感,商品主体一致。品类:${profile.label}。需求:${brief}`),
474
+ ]);
475
+ ctx.outputs.prompt = coverPrompt.result.outputs;
476
+ const overlayCopy = {
477
+ cover_image: buildOverlayCopy("小红书封面图", titleText, brief),
478
+ scene_image: buildOverlayCopy("小红书场景图", titleText, brief),
479
+ };
480
+ Object.assign(ctx.outputs, {
481
+ text: buildSummary([
482
+ ["标题", titleText],
483
+ ["小红书种草文案", xhsCaption],
484
+ ["封面图提示词", coverPrompt.text],
485
+ ["场景图提示词", scenePrompt.text],
486
+ ]),
487
+ title_text: titleText,
488
+ xhs_caption: xhsCaption,
489
+ cover_prompt: coverPrompt.text,
490
+ scene_prompt: scenePrompt.text,
491
+ copy_plan: copyPlanOutput(copyPlan),
492
+ overlay_copy: overlayCopy,
493
+ category_profile: publicCategoryProfile(profile),
494
+ ratios,
495
+ title: title.outputs,
496
+ scene_prompt_outputs: scenePrompt.result.outputs,
497
+ });
498
+ await this.generateImageJobs(ctx, [
499
+ {
500
+ key: "cover_image",
501
+ label: "小红书封面图",
502
+ stepName: "生成小红书封面图",
503
+ image: prepared.cutoutUrl,
504
+ ratio: ratios.cover_image,
505
+ model: copyPlan.imageModel,
506
+ prompt: buildEcommerceImagePrompt({
507
+ role: "小红书种草封面图",
508
+ brief,
509
+ profile,
510
+ commercialRole: "小红书笔记封面,负责在移动端缩略图中让用户看清商品和内容角度。",
511
+ visibleEvidence: `围绕“${primaryBriefPoint(brief)}”建立一个真实分享切入点,商品占据明确视觉焦点。`,
512
+ composition: `真实创作者视角,构图有标题区但画面必须有商品证据。优化后的封面方向:${coverPrompt.text}`,
513
+ platformFit: "适合小红书封面和信息流首屏;不出现夸张营销海报感、假标签、假口播字幕和不可验证功效。",
514
+ copyMode: copyPlan.mode,
515
+ overlayCopy: overlayCopy.cover_image,
516
+ }),
517
+ },
518
+ {
519
+ key: "scene_image",
520
+ label: "小红书场景图",
521
+ stepName: "生成小红书场景图",
522
+ image: prepared.cutoutUrl,
523
+ ratio: ratios.scene_image,
524
+ model: copyPlan.imageModel,
525
+ prompt: buildEcommerceImagePrompt({
526
+ role: "小红书真实场景图",
527
+ brief,
528
+ profile,
529
+ commercialRole: "小红书内容图,负责补充真实使用、开箱、通勤、桌面或生活方式证据。",
530
+ visibleEvidence: profile.usageEvidence,
531
+ composition: `竖版手机构图,自然光,手部或桌面动作可信,商品主体不被遮挡。优化后的场景方向:${scenePrompt.text}`,
532
+ platformFit: "适合小红书种草正文配图;不出现名人脸、过度精修硬广感、复杂字幕和隐藏商品。",
533
+ copyMode: copyPlan.mode,
534
+ overlayCopy: overlayCopy.scene_image,
535
+ }),
536
+ },
537
+ ], options);
538
+ return ctx.completed();
539
+ }
540
+ async runCampaignKvPack(params, options) {
541
+ const ctx = new AppWorkflowContext("campaign-kv-pack", options);
542
+ const image = requireString(params.image ?? params.images, "campaign-kv-pack 需要 --image");
543
+ const brief = requireString(params.brief ?? params.prompt, "campaign-kv-pack 需要 --brief");
544
+ const ratios = parseRatioList(params.ratios ?? params.ratio, ["16:9", "1:1", "4:5"]);
545
+ const profile = inferProductCategoryProfile(brief);
546
+ const copyPlan = resolveCopyPlan(params, brief);
547
+ const prepared = await this.prepareProduct(ctx, image, allowSensitivePath(params));
548
+ const title = await this.generateTitle(ctx, brief);
549
+ const titleText = outputString(title, "text", false) || brief;
550
+ const promptPlan = await this.optimizePrompt(ctx, "prompt_plan", "生成 Campaign KV 主视觉提示词", `为新品 Campaign KV 生成${copyPlan.mode === "no-text" ? "无字主视觉" : "可直接投放主视觉"}提示词:${brief}。品类判断:${profile.label}。要求一个清晰记忆符号,可裁切到官网、社媒、电商活动和投放;${copyPlanningGuidance(copyPlan)}不改 logo、不生成假促销标。`, prepared.cutoutUrl, copyPlan.mode === "no-text"
551
+ ? `Campaign KV 无字主视觉,一个清晰记忆符号,多渠道可裁切,商品是视觉锚点。品类:${profile.label}。需求:${brief}`
552
+ : `Campaign KV 可直接投放主视觉,包含指定 Campaign 标题和核心利益点,一个清晰记忆符号,多渠道可裁切,商品是视觉锚点。品类:${profile.label}。需求:${brief}`);
553
+ const campaignCopy = buildCampaignCopy(titleText, brief, ratios);
554
+ const overlayCopy = Object.fromEntries(ratios.map((ratio) => [`kv_${slugKey(ratio)}`, buildOverlayCopy(`Campaign KV ${ratio}`, titleText, brief)]));
555
+ Object.assign(ctx.outputs, {
556
+ text: buildSummary([
557
+ ["Campaign 标题", titleText],
558
+ ["渠道文案与裁切建议", campaignCopy],
559
+ ["主视觉提示词", promptPlan.text],
560
+ ]),
561
+ title_text: titleText,
562
+ delivery_copy: campaignCopy,
563
+ prompt_plan: promptPlan.text,
564
+ copy_plan: copyPlanOutput(copyPlan),
565
+ overlay_copy: overlayCopy,
566
+ ratios,
567
+ category_profile: publicCategoryProfile(profile),
568
+ title: title.outputs,
569
+ prompt: promptPlan.result.outputs,
570
+ });
571
+ await this.generateImageJobs(ctx, ratios.map((ratio) => {
572
+ const key = `kv_${slugKey(ratio)}`;
573
+ return {
574
+ key,
575
+ label: `Campaign KV ${ratio}`,
576
+ stepName: `生成 Campaign KV ${ratio}`,
577
+ image: prepared.cutoutUrl,
578
+ ratio,
579
+ model: copyPlan.imageModel,
580
+ prompt: buildEcommerceImagePrompt({
581
+ role: `Campaign KV 主视觉 ${ratio}`,
582
+ brief,
583
+ profile,
584
+ commercialRole: "活动主视觉,负责建立一个可跨官网、活动页、社媒和电商投放延展的品牌记忆符号。",
585
+ visibleEvidence: copyPlan.mode === "render-text"
586
+ ? `商品是视觉锚点,围绕“${primaryBriefPoint(brief)}”建立可感知的场景、光影或材质符号,画内短文案只点明核心利益点,不能替代商品证据。`
587
+ : `商品是视觉锚点,围绕“${primaryBriefPoint(brief)}”建立可感知的场景、光影或材质符号,不依赖画内文字表达卖点。`,
588
+ composition: copyPlan.mode === "render-text"
589
+ ? `画面比例 ${ratio},高端商业广告光影,主体和背景层次明确,给指定 Campaign 标题和利益点留出清晰版式位置。优化后的 KV 方向:${promptPlan.text}`
590
+ : `画面比例 ${ratio},高端商业广告光影,主体和背景层次明确,保留后期标题、价格或利益点排版空间。优化后的 KV 方向:${promptPlan.text}`,
591
+ platformFit: copyPlan.mode === "render-text"
592
+ ? "适合多渠道裁切,横版、方图、竖图都能看清商品和指定短文案;不生成额外活动标、假价格或假认证。"
593
+ : "适合多渠道裁切,横版、方图、竖图都能看清商品;可作为后期排版底图。",
594
+ copyMode: copyPlan.mode,
595
+ overlayCopy: String(overlayCopy[key] ?? ""),
596
+ }),
597
+ };
598
+ }), options);
599
+ return ctx.completed();
600
+ }
601
+ async generateImageJobs(ctx, jobs, options) {
602
+ await mapWithConcurrency(jobs, resolveConcurrency(options.concurrency), async (job) => {
603
+ const result = await this.generateImage(ctx, job.key, job.stepName, job.prompt, job.image, job.ratio, job.model);
604
+ ctx.outputs[job.key] = result.outputs;
605
+ ctx.addAsset(job.label, job.key, result);
606
+ });
607
+ }
608
+ async prepareProduct(ctx, image, allowSensitive) {
609
+ const product = await ctx.invoke("product_upload", "临时上传商品图", "asset.upload", () => this.invoker.invoke("upload", { file: image, allow_sensitive_path: allowSensitive }));
610
+ const productUrl = outputString(product, "url");
611
+ ctx.outputs.product = product.outputs;
612
+ const cutout = await ctx.invoke("product_cutout", "商品图抠图", "image.cutout", () => this.invoker.invoke("cutout", { image: productUrl, mode: "product" }));
613
+ const cutoutUrl = outputString(cutout, "url");
614
+ ctx.outputs.cutout = cutout.outputs;
615
+ return { product, cutout, productUrl, cutoutUrl };
616
+ }
617
+ async optimizePrompt(ctx, stepId, stepName, prompt, image, fallback) {
618
+ const result = await ctx.invoke(stepId, stepName, "text.prompt", () => this.invoker.invoke("prompt", { prompt, image }));
619
+ return {
620
+ result,
621
+ text: outputString(result, "text", false) || fallback,
622
+ };
623
+ }
624
+ async generateImage(ctx, stepId, stepName, prompt, image, ratio, model) {
625
+ return ctx.invoke(stepId, stepName, "image.generate", async () => {
626
+ try {
627
+ return await this.invokeGenerateImage(prompt, image, ratio, model);
628
+ }
629
+ catch (error) {
630
+ const message = extractErrorMessage(error);
631
+ if (!isToolIntentRoutingError(message) &&
632
+ !isThirdPartyUnsupportedError(message) &&
633
+ !isContentSafetyError(message)) {
634
+ throw error;
635
+ }
636
+ return this.invokeGenerateImage(isContentSafetyError(message)
637
+ ? safeGeneratePrompt(stepName, prompt)
638
+ : fallbackGeneratePrompt(stepName, prompt), image, ratio, model);
639
+ }
640
+ });
641
+ }
642
+ async invokeGenerateImage(prompt, image, ratio, model) {
643
+ return this.invoker.invoke("generate", {
644
+ prompt,
645
+ image,
646
+ ratio,
647
+ ...(model ? { model } : {}),
648
+ }, {
649
+ timeout: 600,
650
+ });
651
+ }
652
+ async generateTitle(ctx, brief) {
653
+ return ctx.invokeOptional("title", "生成标题", "text.title", () => this.invoker.invoke("title", { prompt: brief }), () => ({
654
+ task_id: `title-fallback-${Date.now()}`,
655
+ status: "completed",
656
+ outputs: {
657
+ text: brief,
658
+ warning: "标题生成失败,已使用 brief 作为标题 fallback。",
659
+ },
660
+ }));
661
+ }
662
+ }
663
+ function completedApp(appId, outputs) {
664
+ return {
665
+ task_id: `app-${appId}-${Date.now()}`,
666
+ status: "completed",
667
+ outputs,
668
+ };
669
+ }
670
+ function taskResultFailure(result) {
671
+ const message = typeof result.error === "string"
672
+ ? result.error
673
+ : result.error && typeof result.error === "object" && "message" in result.error
674
+ ? String(result.error.message)
675
+ : result.message ?? "工具调用失败";
676
+ return new Error(message);
677
+ }
678
+ function normalizeAppStepError(error, step) {
679
+ const rawMessage = extractErrorMessage(error);
680
+ const code = extractErrorCode(error);
681
+ if (isContentSafetyError(rawMessage)) {
682
+ return {
683
+ code: "CONTENT_SAFETY_REJECTED",
684
+ type: "business",
685
+ reason: "reference-image-sensitive",
686
+ message: `${step.step_name} 未通过平台内容安全审核。`,
687
+ raw_message: rawMessage,
688
+ hint: "平台判断参考图或当前生成上下文可能包含敏感内容。请换一张主体更清晰、少文字少 logo 的商品图,或先单独运行 image.cutout / image.generate 验证该图片。",
689
+ next_steps: [
690
+ "检查商品图中是否有敏感文字、人物、商标水印、医疗/政治/暴露等容易误判的元素。",
691
+ "更换或裁剪商品图后重试原 workflows run 命令。",
692
+ "如确认是误判,请把失败步骤和原图提供给平台排查。",
693
+ ],
694
+ };
695
+ }
696
+ if (isThirdPartyUnsupportedError(rawMessage)) {
697
+ return {
698
+ code: "THIRD_PARTY_UNSUPPORTED",
699
+ type: "business",
700
+ reason: "provider-unsupported",
701
+ message: `${step.step_name} 当前暂不支持这次生成请求。`,
702
+ raw_message: rawMessage,
703
+ hint: "平台服务当前调用到暂未开通的生成通道。如果持续复现,请联系平台确认能力配置。",
704
+ next_steps: [
705
+ "停止重试,不要切换模型、环境、团队或调用内部工具。",
706
+ "请提供任务编号、失败步骤和输入图片给平台排查。",
707
+ ],
708
+ };
709
+ }
710
+ if (isRateLimitErrorText(rawMessage)) {
711
+ return {
712
+ code: "RATE_LIMITED",
713
+ type: "business",
714
+ reason: "rate-limited",
715
+ message: `${step.step_name} 请求过于频繁,已被平台限流。`,
716
+ raw_message: rawMessage,
717
+ hint: "请稍后重试,或减少并发和连续生成次数。多图 App 建议等上一轮完成后再启动下一轮。",
718
+ next_steps: [
719
+ "等待 1-3 分钟后重试原 workflows run 命令。",
720
+ "如正在批量测试,请降低并发或拆分执行。",
721
+ ],
722
+ };
723
+ }
724
+ if (isMediaResourceDownloadErrorText(rawMessage)) {
725
+ return {
726
+ code: "MEDIA_RESOURCE_UNREACHABLE",
727
+ type: "business",
728
+ reason: "reference-image-unreachable",
729
+ message: `${step.step_name} 的参考图无法被生成服务下载。`,
730
+ raw_message: rawMessage,
731
+ hint: "请换用公网可访问的 HTTPS 图片 URL,例如上一轮生成结果 URL;如果是视频任务,也可以先去掉参考图改用文生视频。",
732
+ next_steps: [
733
+ "确认参考图 URL 在无登录、无内网环境下可直接下载。",
734
+ "避免把本地上传后的 DAM 签名链接直接作为视频首帧。",
735
+ ],
736
+ };
737
+ }
738
+ if (isTransientNetworkErrorText(rawMessage)) {
739
+ return {
740
+ code: "NETWORK_TRANSIENT",
741
+ type: "network",
742
+ message: `${step.step_name} 请求暂时失败。`,
743
+ raw_message: rawMessage,
744
+ hint: "网络或服务短暂不可用,可稍后重试原命令。",
745
+ next_steps: ["稍后重试原 workflows run 命令。"],
746
+ };
747
+ }
748
+ return {
749
+ code,
750
+ type: code.startsWith("HTTP_401") || /未登录|鉴权|认证|authorization/i.test(rawMessage)
751
+ ? "auth"
752
+ : "business",
753
+ message: `${step.step_name} 执行失败:${friendlyErrorMessage(rawMessage)}`,
754
+ raw_message: rawMessage,
755
+ hint: "可根据失败步骤单独调用对应 Tool 验证输入,也可以调整图片或 brief 后重试 App。",
756
+ next_steps: [
757
+ `单独验证该步骤:gd-cli tools call ${step.tool}`,
758
+ "调整输入后重试原 workflows run 命令。",
759
+ ],
760
+ };
761
+ }
762
+ function publicStep(step) {
763
+ return {
764
+ step_order: step.step_order,
765
+ step_id: step.step_id,
766
+ step_name: step.step_name,
767
+ tool: step.tool,
768
+ status: step.status,
769
+ };
770
+ }
771
+ function extractErrorMessage(error) {
772
+ if (error instanceof DirectApiError) {
773
+ return extractDirectApiMessage(error.body) ?? error.message;
774
+ }
775
+ if (error instanceof Error)
776
+ return error.message;
777
+ return String(error);
778
+ }
779
+ function extractErrorCode(error) {
780
+ if (error instanceof DirectApiError) {
781
+ const parsed = extractDirectApiCode(error.body);
782
+ return parsed ?? `HTTP_${error.statusCode}`;
783
+ }
784
+ return "APP_STEP_FAILED";
785
+ }
786
+ function extractDirectApiMessage(body) {
787
+ const value = objectRecord(body);
788
+ if (!value)
789
+ return typeof body === "string" && body ? body : undefined;
790
+ const nested = objectRecord(value.error);
791
+ return stringField(nested, "message") ??
792
+ stringField(value, "message") ??
793
+ stringField(value, "msg") ??
794
+ stringField(value, "error");
795
+ }
796
+ function extractDirectApiCode(body) {
797
+ const value = objectRecord(body);
798
+ if (!value)
799
+ return undefined;
800
+ const nested = objectRecord(value.error);
801
+ return stringField(nested, "error_code") ??
802
+ stringField(nested, "code") ??
803
+ stringField(value, "error_code") ??
804
+ stringField(value, "code");
805
+ }
806
+ function objectRecord(value) {
807
+ return value && typeof value === "object" && !Array.isArray(value)
808
+ ? value
809
+ : undefined;
810
+ }
811
+ function stringField(value, key) {
812
+ const field = value?.[key];
813
+ return typeof field === "string" && field.trim() ? field.trim() : undefined;
814
+ }
815
+ function isContentSafetyError(message) {
816
+ return /参考图.*敏感|内容敏感|敏感内容|安全审核|审核未通过|sensitive|safety/i.test(message);
817
+ }
818
+ function isThirdPartyUnsupportedError(message) {
819
+ return /该第三方暂未支持|第三方.*暂未支持|180000011|third[- ]?party.*unsupported|provider.*unsupported/i
820
+ .test(message);
821
+ }
822
+ function isRateLimitErrorText(message) {
823
+ return /429|Too Many Requests|rate.?limit|请求过于频繁|限流|频率/i.test(message);
824
+ }
825
+ function isMediaResourceDownloadErrorText(message) {
826
+ return /resource download failed|image_url.*not valid|content\[\d+\]\.image_url|参考图.*下载|媒体.*下载失败|图片.*下载失败/i
827
+ .test(message);
828
+ }
829
+ function isTransientNetworkErrorText(message) {
830
+ return /ECONNRESET|ETIMEDOUT|ENOTFOUND|network|fetch failed|timeout|超时|HTTP status code 503|grpc-status=14|UNAVAILABLE|Service Unavailable/i
831
+ .test(message);
832
+ }
833
+ function isToolIntentRoutingError(message) {
834
+ return /tool-intent 没有返回 scene_code|tool-intent.*scene_code/i.test(message);
835
+ }
836
+ function friendlyErrorMessage(message) {
837
+ return extractEmbeddedBackendMessage(message) ?? message
838
+ .replace(/^业务 API 请求失败\s*\[[^\]]+\]\s+\S+:\s*/i, "")
839
+ .replace(/, /g, ",")
840
+ .trim();
841
+ }
842
+ const CATEGORY_PROFILES = [
843
+ {
844
+ id: "gift-set",
845
+ label: "礼盒/套装",
846
+ guidance: "重点表达套装层级、开箱秩序、包装质感和送礼价值。",
847
+ detailEvidence: "展示礼盒结构、包装材质、配件层级、内托细节和主商品识别面,所有套装物件数量准确。",
848
+ usageEvidence: "使用开箱、送礼、桌面陈列或节日场景证明套装价值,主商品和包装都要清晰可见。",
849
+ },
850
+ {
851
+ id: "digital",
852
+ label: "3C 数码",
853
+ guidance: "重点表达科技感、结构精密度、材质高光和功能可信度。",
854
+ detailEvidence: "展示接口、按键、镜头、声学结构、金属/塑胶材质、接缝或核心部件,避免发明不存在的内部结构。",
855
+ usageEvidence: "使用通勤桌面、影音娱乐、移动办公或科技舞台场景证明使用状态,产品不能变成背景小物。",
856
+ },
857
+ {
858
+ id: "apparel-outdoor",
859
+ label: "服饰/户外",
860
+ guidance: "重点表达面料触感、版型适配、通勤/户外使用感和穿搭氛围。",
861
+ detailEvidence: "展示面料纹理、缝线、拉链、扣具、透气结构、鞋底或背负系统等可见细节。",
862
+ usageEvidence: "使用通勤、户外、运动、旅行或衣橱穿搭场景;可用手部、背影或局部穿着动作,不使用名人正脸。",
863
+ },
864
+ {
865
+ id: "beauty-personal-care",
866
+ label: "美妆个护",
867
+ guidance: "重点表达质地、清爽度、仪式感和瓶身包装质感,避免医疗化和绝对功效。",
868
+ detailEvidence: "展示瓶身、泵头、盖子、膏体/液体质地、材质反光和包装细节,不生成假成分表。",
869
+ usageEvidence: "使用梳妆台、随身包、手部试用或浴室台面场景表达日常感,不生成夸张肤效和医疗承诺。",
870
+ },
871
+ {
872
+ id: "food-beverage",
873
+ label: "食品/饮品",
874
+ guidance: "重点表达食欲感、原料真实感、口味氛围和包装可信度。",
875
+ detailEvidence: "展示包装密封、原料纹理、液体质感、杯壁水珠或食材切面,避免漂浮堆满的随机原料。",
876
+ usageEvidence: "使用早餐、下午茶、办公桌、露营或聚会场景表达饮用/食用时刻,不暗示未经证明的健康疗效。",
877
+ },
878
+ {
879
+ id: "home-living",
880
+ label: "家居/家电",
881
+ guidance: "重点表达空间适配、材质、尺寸关系、真实居家光线和使用便利性。",
882
+ detailEvidence: "展示开关、连接件、收纳结构、灯光效果、滤芯/配件、材质表面或空间尺度。",
883
+ usageEvidence: "使用客厅、卧室、厨房、办公桌或玄关场景证明尺寸与用途,环境干净但不要像空素材库。",
884
+ },
885
+ {
886
+ id: "general",
887
+ label: "通用商品",
888
+ guidance: "优先保证产品保真、主体清楚、卖点可见和平台友好。",
889
+ detailEvidence: "展示材质纹理、结构边界、包装、配件或关键功能部位,证据必须来自参考商品可推断的真实信息。",
890
+ usageEvidence: "使用真实桌面、通勤、家居或轻生活方式场景证明商品用途,商品仍是画面主角。",
891
+ },
892
+ ];
893
+ function inferProductCategoryProfile(brief) {
894
+ const text = brief.toLowerCase();
895
+ if (/礼盒|套装|组合|礼包|礼物|送礼|节日|开箱/.test(text))
896
+ return CATEGORY_PROFILES[0];
897
+ if (/耳机|手机|相机|键盘|鼠标|充电|电脑|数码|3c|智能|音箱|手表|路由|摄像|镜头/.test(text))
898
+ return CATEGORY_PROFILES[1];
899
+ if (/防晒衣|衣|服|鞋|背包|包袋|帽|户外|通勤|跑步|穿搭|面料|夹克|裤|裙/.test(text))
900
+ return CATEGORY_PROFILES[2];
901
+ if (/精华|面霜|护肤|美妆|口红|香水|洗护|个护|防晒霜|乳液|洁面|面膜/.test(text))
902
+ return CATEGORY_PROFILES[3];
903
+ if (/茶|咖啡|饮|食品|零食|酒|果汁|牛奶|奶|味|香|糕|饼|糖/.test(text))
904
+ return CATEGORY_PROFILES[4];
905
+ if (/家居|收纳|灯|空气|净化|桌面|厨房|床|椅|沙发|柜|电器|卧室|客厅/.test(text))
906
+ return CATEGORY_PROFILES[5];
907
+ return CATEGORY_PROFILES[6];
908
+ }
909
+ function publicCategoryProfile(profile) {
910
+ return {
911
+ id: profile.id,
912
+ label: profile.label,
913
+ guidance: profile.guidance,
914
+ };
915
+ }
916
+ function resolveCopyPlan(params, brief = "") {
917
+ const raw = optionalString(params.copy_mode ?? params.copyMode ?? params["copy-mode"]) ?? "auto";
918
+ const normalized = raw.toLowerCase().replace(/_/g, "-");
919
+ if (normalized === "ask") {
920
+ throw new Error("COPY_REQUIREMENTS_REQUIRED: 当前选择 copy-mode=ask,需要先明确每张图是否生成画内文字、主卖点和目标平台后再执行。");
921
+ }
922
+ const mode = normalized === "render-text"
923
+ ? "render-text"
924
+ : normalized === "no-text"
925
+ ? "no-text"
926
+ : normalized === "layout-copy"
927
+ ? "layout-copy"
928
+ : resolveAutoCopyMode(brief);
929
+ const explicitModel = optionalString(params.image_model ?? params.imageModel ?? params["image-model"] ?? params.model);
930
+ return {
931
+ mode,
932
+ renderText: mode === "render-text",
933
+ imageModel: explicitModel,
934
+ };
935
+ }
936
+ function resolveAutoCopyMode(brief) {
937
+ return hasNoTextIntent(brief) ? "no-text" : "render-text";
938
+ }
939
+ function hasNoTextIntent(brief) {
940
+ return /无字|无文字|不带字|不要文字|不生成文字|无字幕|不要字幕|无文案|纯视觉|后期排版|no[-\s]?text|without\s+text/i
941
+ .test(brief);
942
+ }
943
+ function copyPlanOutput(plan) {
944
+ return {
945
+ mode: plan.mode,
946
+ render_text: plan.renderText,
947
+ image_model: plan.imageModel,
948
+ };
949
+ }
950
+ function copyPlanningGuidance(plan) {
951
+ if (plan.mode === "render-text") {
952
+ return "生成可直接交付的画内短文案版本,短文案必须来自商品信息、卖点和场景,不编造价格、参数、认证或促销;";
953
+ }
954
+ if (plan.mode === "layout-copy") {
955
+ return "图片不直接生成文字,但必须提供后期排版所需的视觉证据、商品场景和标题/卖点空间;";
956
+ }
957
+ return "图片不生成文字,但必须用商品主体、材质、结构、动作或场景表达卖点,不能是空白底图;";
958
+ }
959
+ function buildOverlayCopy(role, title, brief) {
960
+ const point = primaryBriefPoint(brief);
961
+ const baseTitle = title.trim() || point;
962
+ if (/白底|主图/.test(role))
963
+ return "";
964
+ if (/卖点/.test(role))
965
+ return `${point}|一眼看懂核心卖点`;
966
+ if (/细节|结构/.test(role))
967
+ return `细节实拍|${point}`;
968
+ if (/生活|使用|场景/.test(role))
969
+ return `${baseTitle}|真实使用场景`;
970
+ if (/痛点/.test(role))
971
+ return `解决痛点|${point}`;
972
+ if (/对比|规格/.test(role))
973
+ return `规格对比|${point}`;
974
+ if (/封面/.test(role))
975
+ return baseTitle;
976
+ if (/Campaign|KV/i.test(role))
977
+ return `${baseTitle}|${point}`;
978
+ return `${baseTitle}|${point}`;
979
+ }
980
+ function buildEcommerceImagePrompt(input) {
981
+ const copyMode = input.copyMode ?? "layout-copy";
982
+ const copyInstruction = buildCopyInstruction(copyMode, input.overlayCopy);
983
+ return [
984
+ `电商图片生成任务:${input.role}。`,
985
+ "严格参考上传图片中的商品主体,把参考图作为唯一商品真值;完整保留主体轮廓、比例、颜色、材质、Logo/标签位置、包装和配件关系,不修改、不重绘主体,只围绕主体渲染场景与氛围。",
986
+ `商品需求:${input.brief}`,
987
+ `品类策略:${input.profile.label}。${input.profile.guidance}`,
988
+ `商业角色:${input.commercialRole}`,
989
+ `可见证据:${input.visibleEvidence}`,
990
+ copyInstruction,
991
+ `构图光影:${input.composition}`,
992
+ `平台边界:${input.platformFit}`,
993
+ copyMode === "render-text"
994
+ ? "负面约束:除指定短文案外,不生成额外文字、水印、价格、促销徽章、认证标识或乱码包装字;指定短文案必须清晰可读、避免错字和伪字;不添加不存在的附件、颜色、SKU 或夸张功效;不出现畸形手指、错误透视、变形 Logo、无关道具和杂乱背景。"
995
+ : "负面约束:不生成画内文字、水印、价格、促销徽章、认证标识或乱码包装字;不添加不存在的附件、颜色、SKU 或夸张功效;不出现畸形手指、错误透视、变形 Logo、无关道具和杂乱背景。",
996
+ "输出要求:真实高质量商业摄影,主体清晰,缩略图可读,适合后期排版。",
997
+ ].join("\n");
998
+ }
999
+ function buildCopyInstruction(mode, copy) {
1000
+ if (mode === "no-text") {
1001
+ return "文案策略:生成纯视觉底图,但画面必须有清晰商品证据和用途表达,不能只是空背景或无意义留白。";
1002
+ }
1003
+ if (mode === "render-text" && copy) {
1004
+ return `文案策略:在画面中准确渲染这句短文案:「${copy}」。文字必须清晰、少字、位置克制,不能新增其他文字。`;
1005
+ }
1006
+ if (copy) {
1007
+ return `文案策略:不要在图里生成文字,但画面要围绕后期将要排版的短文案「${copy}」提供明确视觉证据,并预留合理标题/卖点区。`;
1008
+ }
1009
+ return "文案策略:不要在图里生成文字,但画面必须提供明确视觉证据和后期排版空间,不能是空白占位图。";
1010
+ }
1011
+ function resolveRoleRatios(value, defaults) {
1012
+ const ratio = typeof value === "string" && /^\d+:\d+$/.test(value.trim())
1013
+ ? value.trim()
1014
+ : "";
1015
+ if (!ratio)
1016
+ return defaults;
1017
+ return Object.fromEntries(Object.keys(defaults).map((key) => [key, ratio]));
1018
+ }
1019
+ function primaryBriefPoint(brief) {
1020
+ return splitBriefPoints(brief)[0] ?? brief;
1021
+ }
1022
+ function renderTextCopyFromPrompt(prompt) {
1023
+ const match = prompt.match(/准确渲染这句短文案:「([^」]+)」/);
1024
+ return match?.[1]?.trim();
1025
+ }
1026
+ function extractEmbeddedBackendMessage(message) {
1027
+ const match = message.match(/\\?"message\\?"\s*:\s*\\?"([^"\\]+)\\?"/);
1028
+ return match?.[1]?.trim();
1029
+ }
1030
+ function fallbackGeneratePrompt(stepName, prompt) {
1031
+ const copy = renderTextCopyFromPrompt(prompt);
1032
+ return [
1033
+ `电商图片生成任务:${stepName}。`,
1034
+ "请基于参考商品图生成一张真实商业摄影风格的电商图片,商品主体清晰、形态一致、无水印、无假 logo、无乱码。",
1035
+ copy
1036
+ ? `画面中需要清晰渲染指定短文案「${copy}」,不得生成额外文字、假价格或假认证。`
1037
+ : "如果原始需求没有指定画内短文案,则不生成画内文字;但画面必须有明确商品证据和场景价值,不能是空白底图。",
1038
+ `原始需求:${prompt}`,
1039
+ ].join("\n");
1040
+ }
1041
+ function safeGeneratePrompt(stepName, prompt) {
1042
+ const copy = renderTextCopyFromPrompt(prompt);
1043
+ return [
1044
+ `电商图片生成任务:${stepName}。`,
1045
+ "请生成安全保守的商品商业摄影图:无人物、无儿童、无敏感文字、无商标水印、无医疗政治元素、无暴露内容。",
1046
+ copy
1047
+ ? `商品主体清晰,使用干净桌面、自然光、纯色背景或简单生活道具,只渲染指定短文案「${copy}」,不生成额外文字。`
1048
+ : "商品主体清晰,使用干净桌面、自然光、纯色背景或简单生活道具;不生成画内文字,但不能是空白占位图。",
1049
+ `原始需求:${prompt}`,
1050
+ ].join("\n");
1051
+ }
1052
+ function splitBriefPoints(brief) {
1053
+ const points = brief
1054
+ .split(/[,,。;;、\n]+/g)
1055
+ .map(normalizeBriefPoint)
1056
+ .filter((item) => item && !isWorkflowOnlyBriefPoint(item));
1057
+ return [...new Set(points)].slice(0, 6);
1058
+ }
1059
+ function normalizeBriefPoint(value) {
1060
+ return value
1061
+ .trim()
1062
+ .replace(/^(?:天猫\/京东上架素材包|淘宝上架素材|商品图库素材包|详情页\/A\+模块|详情页模块|小红书[^::]*种草|Campaign\s*KV|[^::]{0,16}Campaign)[::]\s*/i, "")
1063
+ .trim();
1064
+ }
1065
+ function isWorkflowOnlyBriefPoint(value) {
1066
+ return /^(?:需要)?(?:白底主图|核心卖点证明图|结构细节图|桌面使用场景图|官网横版|社媒方图|电商竖图|多渠道投放|素材包|详情页模块)$/i
1067
+ .test(value);
1068
+ }
1069
+ function parseRatioList(value, fallback) {
1070
+ const raw = typeof value === "string" ? value : "";
1071
+ const ratios = raw
1072
+ .split(/[,\s,、]+/g)
1073
+ .map((item) => item.trim())
1074
+ .filter((item) => /^\d+:\d+$/.test(item));
1075
+ return ratios.length > 0 ? [...new Set(ratios)].slice(0, 4) : fallback;
1076
+ }
1077
+ function slugKey(value) {
1078
+ return value.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "asset";
1079
+ }
1080
+ function buildSummary(sections) {
1081
+ return sections
1082
+ .filter(([, body]) => body.trim())
1083
+ .map(([title, body]) => `## ${title}\n${body.trim()}`)
1084
+ .join("\n\n");
1085
+ }
1086
+ function buildListingGalleryCopy(title, brief) {
1087
+ const points = splitBriefPoints(brief);
1088
+ return [
1089
+ `标题:${title}`,
1090
+ "",
1091
+ "图库结构:",
1092
+ "- 白底主图:看清商品主体、颜色、形态和核心配件。",
1093
+ "- 卖点图:只回答一个购买问题,后期可加一句卖点文案。",
1094
+ "- 细节图:证明材质、结构、工艺或关键功能。",
1095
+ "- 生活方式图:让用户知道商品怎么用、适合谁、在什么场景使用。",
1096
+ "",
1097
+ "卖点拆解:",
1098
+ ...(points.length ? points.map((point) => `- ${point}`) : [`- ${brief}`]),
1099
+ "",
1100
+ "上线检查:主体保真、无假字假认证、缩略图仍能看清商品。",
1101
+ ].join("\n");
1102
+ }
1103
+ function buildPdpModuleCopy(title, brief) {
1104
+ const points = splitBriefPoints(brief);
1105
+ return [
1106
+ `标题:${title}`,
1107
+ "",
1108
+ "详情页模块顺序:",
1109
+ "1. 首屏模块:先给出核心利益点和商品全貌。",
1110
+ "2. 痛点模块:解释用户为什么需要它。",
1111
+ "3. 结构细节模块:用可见结构证明功能,不编造参数。",
1112
+ "4. 使用场景模块:展示真实操作和生活环境。",
1113
+ "5. 对比规格模块:留给后期添加参数、尺寸、适配信息。",
1114
+ "",
1115
+ "可展开卖点:",
1116
+ ...(points.length ? points.map((point) => `- ${point}`) : [`- ${brief}`]),
1117
+ "",
1118
+ "上线检查:参数后期排版,AI 图不生成假认证、假数字和医疗化表述。",
1119
+ ].join("\n");
1120
+ }
1121
+ function buildXhsCaption(title, brief) {
1122
+ const points = splitBriefPoints(brief);
1123
+ const highlights = points.length > 0
1124
+ ? points.map((point) => `- ${point}`).join("\n")
1125
+ : `- ${brief}`;
1126
+ return [
1127
+ title,
1128
+ "",
1129
+ "最近试到一个很适合日常分享的好物,第一眼不是硬广感,而是能看出真实使用场景。",
1130
+ "它的重点不是堆参数,而是把用户真正关心的细节讲清楚:好不好用、适不适合日常、是不是值得入手。",
1131
+ "",
1132
+ "种草点:",
1133
+ highlights,
1134
+ "",
1135
+ "#好物分享 #小红书种草 #日常好物 #真实使用感",
1136
+ ].join("\n");
1137
+ }
1138
+ function buildCampaignCopy(title, brief, ratios) {
1139
+ return [
1140
+ `Campaign 标题:${title}`,
1141
+ "",
1142
+ "核心利益点:",
1143
+ ...splitBriefPoints(brief).map((point) => `- ${point}`),
1144
+ "",
1145
+ "渠道裁切建议:",
1146
+ ...ratios.map((ratio) => `- ${ratio}:保留商品为视觉锚点,文字、价格和活动利益点后期排版。`),
1147
+ "",
1148
+ "上线检查:KV 保持无字版本,确认横版、方图、竖图裁切后商品仍清楚。",
1149
+ ].join("\n");
1150
+ }
1151
+ function requireString(value, message) {
1152
+ const normalized = typeof value === "string" ? value.trim() : "";
1153
+ if (!normalized)
1154
+ throw new Error(message);
1155
+ return normalized;
1156
+ }
1157
+ function optionalString(value) {
1158
+ if (typeof value !== "string")
1159
+ return undefined;
1160
+ const normalized = value.trim();
1161
+ return normalized ? normalized : undefined;
1162
+ }
1163
+ function allowSensitivePath(params) {
1164
+ return (params.allow_sensitive_path === true ||
1165
+ params.allowSensitivePath === true ||
1166
+ params["allow-sensitive-path"] === true);
1167
+ }
1168
+ function outputString(result, key, required = true) {
1169
+ const outputs = result.outputs;
1170
+ const value = outputs?.[key];
1171
+ if (typeof value === "string" && value.trim())
1172
+ return value;
1173
+ if (required) {
1174
+ throw new Error(`App workflow 缺少上游输出字段: ${key}`);
1175
+ }
1176
+ return "";
1177
+ }