gaoding-cli 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (77) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +47 -60
  3. package/dist/bin/gd-cli.js +2 -2
  4. package/dist/src/api/app-runner.d.ts +1 -4
  5. package/dist/src/api/app-runner.js +11 -44
  6. package/dist/src/api/creative-client.d.ts +34 -0
  7. package/dist/src/api/creative-client.js +184 -0
  8. package/dist/src/api/dam-client.d.ts +0 -7
  9. package/dist/src/api/dam-client.js +0 -17
  10. package/dist/src/api/direct-client.d.ts +9 -2
  11. package/dist/src/api/direct-client.js +45 -18
  12. package/dist/src/api/hub-client.d.ts +11 -8
  13. package/dist/src/api/hub-client.js +17 -18
  14. package/dist/src/api/usage-client.d.ts +4 -4
  15. package/dist/src/api/usage-client.js +9 -49
  16. package/dist/src/auth/credential-store-v2.d.ts +0 -1
  17. package/dist/src/auth/credential-store-v2.js +0 -6
  18. package/dist/src/commands/auth.js +17 -53
  19. package/dist/src/commands/creative.d.ts +8 -0
  20. package/dist/src/commands/creative.js +178 -0
  21. package/dist/src/commands/dam.js +270 -127
  22. package/dist/src/commands/org.js +31 -47
  23. package/dist/src/commands/skills.js +33 -118
  24. package/dist/src/commands/update.js +11 -82
  25. package/dist/src/commands/usage.d.ts +1 -1
  26. package/dist/src/commands/usage.js +10 -21
  27. package/dist/src/commands/workflows.d.ts +1 -1
  28. package/dist/src/commands/workflows.js +299 -197
  29. package/dist/src/core/auth/auth-guard.d.ts +21 -0
  30. package/dist/src/core/auth/auth-guard.js +121 -0
  31. package/dist/src/core/auth/org-manager.d.ts +2 -1
  32. package/dist/src/core/auth/org-manager.js +61 -6
  33. package/dist/src/core/output/envelope.d.ts +1 -0
  34. package/dist/src/core/output/envelope.js +76 -6
  35. package/dist/src/creative/contract.d.ts +1060 -0
  36. package/dist/src/creative/contract.js +722 -0
  37. package/dist/src/creative/media.d.ts +19 -0
  38. package/dist/src/creative/media.js +83 -0
  39. package/dist/src/creative/protocol.d.ts +60 -0
  40. package/dist/src/creative/protocol.js +337 -0
  41. package/dist/src/io.d.ts +0 -2
  42. package/dist/src/io.js +0 -8
  43. package/dist/src/middleware/auth.d.ts +6 -2
  44. package/dist/src/middleware/auth.js +25 -63
  45. package/dist/src/middleware/error-handler.d.ts +8 -0
  46. package/dist/src/middleware/error-handler.js +61 -57
  47. package/dist/src/program.d.ts +1 -0
  48. package/dist/src/program.js +137 -122
  49. package/dist/src/registry/agent-contracts.d.ts +14 -34
  50. package/dist/src/registry/agent-contracts.js +209 -297
  51. package/dist/src/registry/direct-registry.d.ts +0 -5
  52. package/dist/src/registry/direct-registry.js +0 -19
  53. package/dist/src/registry/registry-client.d.ts +0 -21
  54. package/dist/src/registry/registry-client.js +1 -40
  55. package/dist/src/utils/capability-presenter.js +6 -29
  56. package/dist/src/utils/config.d.ts +2 -2
  57. package/dist/src/utils/config.js +1 -1
  58. package/dist/src/utils/help-verify.d.ts +1 -3
  59. package/dist/src/utils/help-verify.js +1 -25
  60. package/dist/src/utils/sensitive-path.js +1 -4
  61. package/package.json +4 -3
  62. package/skills/gd-cli/SKILL.md +13 -12
  63. package/skills/gd-cli/references/auth.md +9 -10
  64. package/skills/gd-cli/references/creative.md +47 -0
  65. package/skills/gd-cli/references/entitlement.md +3 -3
  66. package/skills/gd-cli/references/errors.md +10 -3
  67. package/skills/gd-cli/references/org.md +3 -3
  68. package/skills/gd-cli/references/sop.md +8 -8
  69. package/skills/gd-cli/references/update.md +4 -3
  70. package/skills/gd-cli/references/workflows.md +7 -6
  71. package/dist/src/commands/models.d.ts +0 -2
  72. package/dist/src/commands/models.js +0 -78
  73. package/dist/src/commands/tools.d.ts +0 -4
  74. package/dist/src/commands/tools.js +0 -383
  75. package/dist/src/utils/list-output-format.d.ts +0 -10
  76. package/dist/src/utils/list-output-format.js +0 -34
  77. package/skills/gd-cli/references/tools.md +0 -29
@@ -0,0 +1,722 @@
1
+ import { CliUsageError } from "../core/output/cli-error.js";
2
+ export const CREATIVE_SCHEMA_VERSION = "creative/v1";
3
+ export const CREATIVE_INPUT_SCHEMA = {
4
+ $schema: "https://json-schema.org/draft/2020-12/schema",
5
+ $id: "https://www.gaoding.com/cli/schemas/creative-input-v1.json",
6
+ description: "发起或继续一次稿定 Creative 创作所需的完整输入。",
7
+ type: "object",
8
+ additionalProperties: false,
9
+ properties: {
10
+ prompt: {
11
+ type: "string",
12
+ minLength: 1,
13
+ description: "详细描述期望的最终结果,包括主体、用途、内容、风格、场景、构图或镜头,以及必须保留和避免的事项。比例、分辨率、时长等可结构化表达的要求,建议通过 parameters 传入。",
14
+ },
15
+ media: {
16
+ type: "array",
17
+ minItems: 1,
18
+ description: "本次创作使用的输入素材;媒体不能脱离 prompt 或 answers 单独提交。",
19
+ items: {
20
+ type: "object",
21
+ additionalProperties: false,
22
+ required: ["source", "role"],
23
+ properties: {
24
+ source: {
25
+ type: "string",
26
+ minLength: 1,
27
+ description: "本地文件路径或可访问的 HTTP(S) URL。",
28
+ },
29
+ role: {
30
+ type: "string",
31
+ enum: ["reference", "first_frame", "last_frame"],
32
+ description: "素材用途;reference 是参考素材,first_frame 和 last_frame 分别是视频首帧和尾帧。CLI 不根据数组顺序、文件名或 prompt 推断。",
33
+ },
34
+ },
35
+ },
36
+ },
37
+ parameters: {
38
+ type: "object",
39
+ description: "可选的结构化创作约束。GD Agent 会优先遵循本字段中的约束;存在对应参数时,建议使用本字段,而不是仅在 prompt 中通过文本描述。",
40
+ additionalProperties: false,
41
+ properties: {
42
+ ratio: {
43
+ type: "string",
44
+ minLength: 1,
45
+ description: "期望输出媒体的宽高比,例如 1:1、16:9 或 9:16。",
46
+ },
47
+ resolution: {
48
+ type: "string",
49
+ minLength: 1,
50
+ description: "期望输出媒体的分辨率,例如 720p、1080p、2K 或 4K。",
51
+ },
52
+ duration_seconds: {
53
+ type: "integer",
54
+ minimum: 1,
55
+ description: "期望生成视频的时长,单位为秒;仅适用于视频创作。",
56
+ },
57
+ },
58
+ },
59
+ conversation_id: {
60
+ type: "string",
61
+ minLength: 1,
62
+ description: "继续已有创作或回答追问时使用的会话标识;首次请求和独立的新创作不传。",
63
+ },
64
+ question_message_id: {
65
+ type: "string",
66
+ minLength: 1,
67
+ description: "正在回答或跳过的追问消息标识,原样使用上一次 requires_input 输出中的值。",
68
+ },
69
+ answers: {
70
+ type: "array",
71
+ minItems: 1,
72
+ description: "对上一次 requires_input 输出中一个或多个问题的结构化回答。",
73
+ items: {
74
+ type: "object",
75
+ additionalProperties: false,
76
+ required: ["question", "values"],
77
+ properties: {
78
+ question: {
79
+ type: "string",
80
+ minLength: 1,
81
+ description: "必须与上一次输出中的 question 文本一致。",
82
+ },
83
+ values: {
84
+ type: "array",
85
+ minItems: 1,
86
+ description: "选择项或自定义回答;推荐 options 不是枚举限制。",
87
+ items: {
88
+ type: "string",
89
+ minLength: 1,
90
+ },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ anyOf: [
97
+ { required: ["prompt"] },
98
+ { required: ["answers"] },
99
+ ],
100
+ dependentRequired: {
101
+ answers: ["conversation_id", "question_message_id"],
102
+ question_message_id: ["conversation_id"],
103
+ },
104
+ allOf: [
105
+ {
106
+ properties: {
107
+ media: {
108
+ description: "用于检查首帧数量的素材列表。",
109
+ contains: {
110
+ type: "object",
111
+ properties: {
112
+ role: {
113
+ const: "first_frame",
114
+ description: "首帧素材角色。",
115
+ },
116
+ },
117
+ required: ["role"],
118
+ },
119
+ minContains: 0,
120
+ maxContains: 1,
121
+ },
122
+ },
123
+ },
124
+ {
125
+ properties: {
126
+ media: {
127
+ description: "用于检查尾帧数量的素材列表。",
128
+ contains: {
129
+ type: "object",
130
+ properties: {
131
+ role: {
132
+ const: "last_frame",
133
+ description: "尾帧素材角色。",
134
+ },
135
+ },
136
+ required: ["role"],
137
+ },
138
+ minContains: 0,
139
+ maxContains: 1,
140
+ },
141
+ },
142
+ },
143
+ ],
144
+ };
145
+ export const CREATIVE_OUTPUT_SCHEMA = {
146
+ $schema: "https://json-schema.org/draft/2020-12/schema",
147
+ $id: "https://www.gaoding.com/cli/schemas/creative-output-v1.json",
148
+ description: "creative run 的统一最终输出;中间事件和内部调用过程不属于本 Schema。",
149
+ type: "object",
150
+ additionalProperties: false,
151
+ required: ["status", "data", "error", "meta"],
152
+ properties: {
153
+ status: {
154
+ type: "string",
155
+ enum: ["completed", "failed"],
156
+ description: "CLI 调用是否成功;requires_input 仍属于 completed。",
157
+ },
158
+ data: {
159
+ description: "status 为 completed 时存在的 Creative 最终结果;failed 时为 null。",
160
+ },
161
+ error: {
162
+ description: "status 为 failed 时存在的结构化错误;completed 时为 null。",
163
+ },
164
+ meta: {
165
+ type: "object",
166
+ description: "本次输出对应的命令与公共契约版本。",
167
+ additionalProperties: false,
168
+ required: ["command", "schema_version"],
169
+ properties: {
170
+ command: {
171
+ const: "creative run",
172
+ description: "产生本 Envelope 的 GD CLI 命令。",
173
+ },
174
+ schema_version: {
175
+ const: CREATIVE_SCHEMA_VERSION,
176
+ description: "Creative 公共契约版本。",
177
+ },
178
+ },
179
+ },
180
+ },
181
+ oneOf: [
182
+ {
183
+ properties: {
184
+ status: { const: "completed", description: "成功状态。" },
185
+ data: { $ref: "#/$defs/result", description: "Creative 结果。" },
186
+ error: { type: "null", description: "成功时没有错误。" },
187
+ },
188
+ },
189
+ {
190
+ properties: {
191
+ status: { const: "failed", description: "失败状态。" },
192
+ data: { type: "null", description: "失败时没有 Creative 结果。" },
193
+ error: { $ref: "#/$defs/error", description: "结构化失败信息。" },
194
+ },
195
+ },
196
+ ],
197
+ $defs: {
198
+ result: {
199
+ type: "object",
200
+ additionalProperties: false,
201
+ required: ["conversation_id", "finish_reason", "message", "outputs"],
202
+ properties: {
203
+ conversation_id: {
204
+ type: "string",
205
+ minLength: 1,
206
+ description: "新请求由 GD Agent 创建;连续请求沿用输入值。",
207
+ },
208
+ finish_reason: {
209
+ type: "string",
210
+ enum: ["completed", "requires_input"],
211
+ description: "completed 表示本轮完成;requires_input 表示需要继续回答 questions。",
212
+ },
213
+ message: {
214
+ type: "string",
215
+ description: "GD Agent 面向使用者的最终说明。",
216
+ },
217
+ outputs: {
218
+ type: "array",
219
+ description: "本轮最终产出的文本、结构化数据或媒体。",
220
+ items: { $ref: "#/$defs/output" },
221
+ },
222
+ question_message_id: {
223
+ type: "string",
224
+ minLength: 1,
225
+ description: "requires_input 时必填,并在下一次请求中原样回传。",
226
+ },
227
+ questions: {
228
+ $ref: "#/$defs/question_group",
229
+ description: "requires_input 时必填的结构化追问。",
230
+ },
231
+ },
232
+ allOf: [
233
+ {
234
+ if: {
235
+ properties: {
236
+ finish_reason: {
237
+ const: "requires_input",
238
+ description: "需要使用者回答。",
239
+ },
240
+ },
241
+ },
242
+ then: { required: ["question_message_id", "questions"] },
243
+ else: {
244
+ not: {
245
+ anyOf: [
246
+ { required: ["question_message_id"] },
247
+ { required: ["questions"] },
248
+ ],
249
+ },
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ question_group: {
255
+ type: "object",
256
+ additionalProperties: false,
257
+ required: ["title", "questions"],
258
+ properties: {
259
+ title: {
260
+ type: "string",
261
+ description: "本组追问的标题。",
262
+ },
263
+ questions: {
264
+ type: "array",
265
+ minItems: 1,
266
+ description: "需要使用者回答的一个或多个问题。",
267
+ items: {
268
+ type: "object",
269
+ additionalProperties: false,
270
+ required: ["question", "options", "allow_multiple"],
271
+ properties: {
272
+ question: {
273
+ type: "string",
274
+ minLength: 1,
275
+ description: "问题文本;提交 answers 时原样作为 answers[].question。",
276
+ },
277
+ options: {
278
+ type: "array",
279
+ description: "推荐选项,可以为空;label 不是严格枚举。",
280
+ items: {
281
+ type: "object",
282
+ additionalProperties: false,
283
+ required: ["label"],
284
+ properties: {
285
+ label: {
286
+ type: "string",
287
+ minLength: 1,
288
+ description: "推荐回答的展示文本。",
289
+ },
290
+ },
291
+ },
292
+ },
293
+ allow_multiple: {
294
+ type: "boolean",
295
+ description: "是否允许为本问题提交多个回答值。",
296
+ },
297
+ },
298
+ },
299
+ },
300
+ },
301
+ },
302
+ output: {
303
+ oneOf: [
304
+ {
305
+ type: "object",
306
+ additionalProperties: false,
307
+ required: ["type", "text"],
308
+ properties: {
309
+ type: { const: "text", description: "结果类型。" },
310
+ text: { type: "string", description: "文本内容。" },
311
+ },
312
+ },
313
+ {
314
+ type: "object",
315
+ additionalProperties: false,
316
+ required: ["type", "value"],
317
+ properties: {
318
+ type: { const: "json", description: "结果类型。" },
319
+ value: { description: "任意合法 JSON 值。" },
320
+ },
321
+ },
322
+ {
323
+ type: "object",
324
+ additionalProperties: false,
325
+ required: ["type", "url", "mime_type"],
326
+ properties: {
327
+ type: {
328
+ type: "string",
329
+ enum: ["image", "video", "audio", "file"],
330
+ description: "媒体或文件类型。",
331
+ },
332
+ url: {
333
+ type: "string",
334
+ format: "uri",
335
+ pattern: "^https?://",
336
+ description: "可访问或下载结果的 HTTP(S) URL。",
337
+ },
338
+ mime_type: {
339
+ type: "string",
340
+ minLength: 1,
341
+ description: "结果的 MIME type,例如 image/png 或 video/mp4。",
342
+ },
343
+ name: {
344
+ type: "string",
345
+ minLength: 1,
346
+ description: "建议的展示名或文件名。",
347
+ },
348
+ asset_id: {
349
+ type: "string",
350
+ minLength: 1,
351
+ description: "结果已进入 DAM 时的素材标识。",
352
+ },
353
+ width: {
354
+ type: "number",
355
+ minimum: 0,
356
+ description: "图片或视频的像素宽度。",
357
+ },
358
+ height: {
359
+ type: "number",
360
+ minimum: 0,
361
+ description: "图片或视频的像素高度。",
362
+ },
363
+ duration_seconds: {
364
+ type: "number",
365
+ minimum: 0,
366
+ description: "视频或音频的时长,单位为秒。",
367
+ },
368
+ },
369
+ },
370
+ ],
371
+ },
372
+ error: {
373
+ type: "object",
374
+ additionalProperties: false,
375
+ required: ["code", "type", "message"],
376
+ properties: {
377
+ code: {
378
+ type: "string",
379
+ minLength: 1,
380
+ description: "稳定、可机器判断的错误码。",
381
+ },
382
+ type: {
383
+ type: "string",
384
+ enum: ["validation", "auth", "business", "network", "unknown"],
385
+ description: "错误所属类别。",
386
+ },
387
+ message: {
388
+ type: "string",
389
+ minLength: 1,
390
+ description: "面向使用者的错误说明。",
391
+ },
392
+ hint: {
393
+ type: "string",
394
+ minLength: 1,
395
+ description: "可执行的处理建议。",
396
+ },
397
+ retryable: {
398
+ type: "boolean",
399
+ description: "使用相同输入稍后重试是否可能成功。",
400
+ },
401
+ },
402
+ },
403
+ },
404
+ };
405
+ export function getCreativeSchema() {
406
+ return {
407
+ schema_version: CREATIVE_SCHEMA_VERSION,
408
+ input_schema: CREATIVE_INPUT_SCHEMA,
409
+ output_schema: CREATIVE_OUTPUT_SCHEMA,
410
+ };
411
+ }
412
+ export const CREATIVE_EXAMPLES = {
413
+ examples: [
414
+ {
415
+ name: "text_creation",
416
+ description: "纯文本创作",
417
+ command: 'gd-cli creative run --prompt "为一款轻量缓震跑鞋写三条电商主图标题,语言简洁有力。" --json',
418
+ input: {
419
+ prompt: "为一款轻量缓震跑鞋写三条电商主图标题,语言简洁有力。",
420
+ },
421
+ },
422
+ {
423
+ name: "image_with_reference",
424
+ description: "使用参考图创作图片",
425
+ command: "gd-cli creative run --input creative-request.json --json",
426
+ input: {
427
+ prompt: "以产品为主体生成一张户外晨跑场景广告图,保持鞋款外观和配色。",
428
+ media: [
429
+ {
430
+ source: "./product.png",
431
+ role: "reference",
432
+ },
433
+ ],
434
+ parameters: {
435
+ ratio: "1:1",
436
+ resolution: "2K",
437
+ },
438
+ },
439
+ },
440
+ {
441
+ name: "video_with_first_and_last_frames",
442
+ description: "使用首尾帧创作视频",
443
+ command: "gd-cli creative run --input creative-request.json --json",
444
+ input: {
445
+ prompt: "生成从清晨城市街道过渡到终点冲线的运动广告视频,镜头运动自然。",
446
+ media: [
447
+ {
448
+ source: "./first-frame.png",
449
+ role: "first_frame",
450
+ },
451
+ {
452
+ source: "./last-frame.png",
453
+ role: "last_frame",
454
+ },
455
+ ],
456
+ parameters: {
457
+ ratio: "16:9",
458
+ resolution: "1080p",
459
+ duration_seconds: 5,
460
+ },
461
+ },
462
+ },
463
+ {
464
+ name: "continue_creation",
465
+ description: "继续修改已有创作",
466
+ command: "gd-cli creative run --input creative-request.json --json",
467
+ input: {
468
+ conversation_id: "conversation_123",
469
+ prompt: "保留主体和构图,把整体风格调整得更克制,并降低背景饱和度。",
470
+ },
471
+ },
472
+ {
473
+ name: "answer_questions",
474
+ description: "回答 GD Agent 的结构化追问",
475
+ command: "gd-cli creative run --input creative-request.json --json",
476
+ input: {
477
+ conversation_id: "conversation_123",
478
+ question_message_id: "message_456",
479
+ answers: [
480
+ {
481
+ question: "希望采用什么视觉风格?",
482
+ values: ["极简"],
483
+ },
484
+ {
485
+ question: "主要投放到哪些渠道?",
486
+ values: ["电商详情页", "社交媒体"],
487
+ },
488
+ ],
489
+ },
490
+ },
491
+ ],
492
+ };
493
+ export function getCreativeExamples() {
494
+ return CREATIVE_EXAMPLES;
495
+ }
496
+ const INPUT_KEYS = new Set([
497
+ "prompt",
498
+ "media",
499
+ "parameters",
500
+ "conversation_id",
501
+ "question_message_id",
502
+ "answers",
503
+ ]);
504
+ const PARAMETER_KEYS = new Set(["ratio", "resolution", "duration_seconds"]);
505
+ const MEDIA_KEYS = new Set(["source", "role"]);
506
+ const ANSWER_KEYS = new Set(["question", "values"]);
507
+ const MEDIA_ROLES = new Set(["reference", "first_frame", "last_frame"]);
508
+ const IMAGE_EXTENSIONS = new Set([
509
+ "avif",
510
+ "bmp",
511
+ "gif",
512
+ "heic",
513
+ "heif",
514
+ "jpeg",
515
+ "jpg",
516
+ "png",
517
+ "svg",
518
+ "webp",
519
+ ]);
520
+ const VIDEO_EXTENSIONS = new Set(["avi", "m4v", "mkv", "mov", "mp4", "webm"]);
521
+ const AUDIO_EXTENSIONS = new Set([
522
+ "aac",
523
+ "flac",
524
+ "m4a",
525
+ "mp3",
526
+ "oga",
527
+ "ogg",
528
+ "opus",
529
+ "wav",
530
+ ]);
531
+ const KNOWN_NON_MEDIA_EXTENSIONS = new Set([
532
+ "csv",
533
+ "doc",
534
+ "docx",
535
+ "html",
536
+ "json",
537
+ "pdf",
538
+ "ppt",
539
+ "pptx",
540
+ "txt",
541
+ "xls",
542
+ "xlsx",
543
+ "xml",
544
+ "zip",
545
+ ]);
546
+ export function validateCreativeInput(value) {
547
+ const input = requireRecord(value, "$", "Creative 输入必须是 JSON object");
548
+ rejectUnknownKeys(input, INPUT_KEYS, "$");
549
+ const hasPrompt = Object.hasOwn(input, "prompt");
550
+ const hasAnswers = Object.hasOwn(input, "answers");
551
+ const prompt = hasPrompt ? requireNonEmptyString(input.prompt, "prompt") : undefined;
552
+ const conversationId = Object.hasOwn(input, "conversation_id")
553
+ ? requireNonEmptyString(input.conversation_id, "conversation_id")
554
+ : undefined;
555
+ const questionMessageId = Object.hasOwn(input, "question_message_id")
556
+ ? requireNonEmptyString(input.question_message_id, "question_message_id")
557
+ : undefined;
558
+ if (!hasPrompt && !hasAnswers) {
559
+ invalid("prompt", "普通创作必须提供非空 prompt;回答追问时可以改为提供 answers");
560
+ }
561
+ if (questionMessageId && !conversationId) {
562
+ invalid("question_message_id", "question_message_id 必须与 conversation_id 一起使用");
563
+ }
564
+ if (Object.hasOwn(input, "media"))
565
+ validateMedia(input.media);
566
+ if (Object.hasOwn(input, "parameters"))
567
+ validateParameters(input.parameters);
568
+ if (hasAnswers) {
569
+ validateAnswers(input.answers);
570
+ if (!conversationId || !questionMessageId) {
571
+ invalid("answers", "answers 必须同时携带 conversation_id 和 question_message_id");
572
+ }
573
+ }
574
+ if (prompt === undefined && !hasAnswers) {
575
+ invalid("prompt", "prompt 不能为空");
576
+ }
577
+ return input;
578
+ }
579
+ function validateMedia(value) {
580
+ if (!Array.isArray(value) || value.length === 0) {
581
+ invalid("media", "media 必须是至少包含一项素材的数组");
582
+ }
583
+ let firstFrameCount = 0;
584
+ let lastFrameCount = 0;
585
+ value.forEach((item, index) => {
586
+ const path = `media[${index}]`;
587
+ const media = requireRecord(item, path, `${path} 必须是 object`);
588
+ rejectUnknownKeys(media, MEDIA_KEYS, path);
589
+ const source = requireNonEmptyString(media.source, `${path}.source`);
590
+ const role = requireNonEmptyString(media.role, `${path}.role`);
591
+ if (!MEDIA_ROLES.has(role)) {
592
+ invalid(`${path}.role`, "role 只支持 reference、first_frame 或 last_frame");
593
+ }
594
+ validateMediaSource(source, role, `${path}.source`);
595
+ if (role === "first_frame")
596
+ firstFrameCount += 1;
597
+ if (role === "last_frame")
598
+ lastFrameCount += 1;
599
+ });
600
+ if (firstFrameCount > 1) {
601
+ invalid("media", "first_frame 最多只能提供一份");
602
+ }
603
+ if (lastFrameCount > 1) {
604
+ invalid("media", "last_frame 最多只能提供一份");
605
+ }
606
+ }
607
+ function validateMediaSource(source, role, path) {
608
+ const isWindowsPath = /^[A-Za-z]:[\\/]/.test(source);
609
+ const scheme = !isWindowsPath ? source.match(/^([A-Za-z][A-Za-z\d+.-]*):/)?.[1] : undefined;
610
+ if (scheme && scheme !== "http" && scheme !== "https") {
611
+ invalid(path, "source 只支持本地文件路径或 HTTP(S) URL");
612
+ }
613
+ const remote = /^https?:\/\//i.test(source);
614
+ if (remote) {
615
+ try {
616
+ new URL(source);
617
+ }
618
+ catch {
619
+ invalid(path, "source 必须是合法的 HTTP(S) URL");
620
+ }
621
+ }
622
+ const extension = mediaExtension(source, remote);
623
+ const kind = mediaKind(extension);
624
+ if (role === "first_frame" || role === "last_frame") {
625
+ if (kind === "video" || kind === "audio" || kind === "unsupported") {
626
+ invalid(path, `${role} 只支持图片素材`);
627
+ }
628
+ if (!remote && kind !== "image") {
629
+ invalid(path, `${role} 的本地文件必须是支持的图片格式`);
630
+ }
631
+ return;
632
+ }
633
+ if (kind === "unsupported" || (!remote && kind === "unknown")) {
634
+ invalid(path, "reference 只支持图片、视频或音频素材");
635
+ }
636
+ }
637
+ function validateParameters(value) {
638
+ const parameters = requireRecord(value, "parameters", "parameters 必须是 object");
639
+ rejectUnknownKeys(parameters, PARAMETER_KEYS, "parameters");
640
+ if (Object.hasOwn(parameters, "ratio")) {
641
+ requireNonEmptyString(parameters.ratio, "parameters.ratio");
642
+ }
643
+ if (Object.hasOwn(parameters, "resolution")) {
644
+ requireNonEmptyString(parameters.resolution, "parameters.resolution");
645
+ }
646
+ if (Object.hasOwn(parameters, "duration_seconds")) {
647
+ const duration = parameters.duration_seconds;
648
+ if (typeof duration !== "number" || !Number.isInteger(duration) || duration < 1) {
649
+ invalid("parameters.duration_seconds", "duration_seconds 必须是大于等于 1 的整数");
650
+ }
651
+ }
652
+ }
653
+ function validateAnswers(value) {
654
+ if (!Array.isArray(value) || value.length === 0) {
655
+ invalid("answers", "answers 必须是至少包含一项回答的数组");
656
+ }
657
+ value.forEach((item, index) => {
658
+ const path = `answers[${index}]`;
659
+ const answer = requireRecord(item, path, `${path} 必须是 object`);
660
+ rejectUnknownKeys(answer, ANSWER_KEYS, path);
661
+ requireNonEmptyString(answer.question, `${path}.question`);
662
+ if (!Array.isArray(answer.values) || answer.values.length === 0) {
663
+ invalid(`${path}.values`, "values 必须至少包含一项非空回答");
664
+ }
665
+ answer.values.forEach((itemValue, valueIndex) => {
666
+ requireNonEmptyString(itemValue, `${path}.values[${valueIndex}]`);
667
+ });
668
+ });
669
+ }
670
+ function requireRecord(value, path, message) {
671
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
672
+ invalid(path, message);
673
+ }
674
+ return value;
675
+ }
676
+ function requireNonEmptyString(value, path) {
677
+ if (typeof value !== "string" || !value.trim()) {
678
+ invalid(path, `${path} 必须是非空字符串`);
679
+ }
680
+ return value;
681
+ }
682
+ function rejectUnknownKeys(value, allowed, path) {
683
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
684
+ if (unknown.length > 0) {
685
+ invalid(path, `不支持字段: ${unknown.join(", ")}`);
686
+ }
687
+ }
688
+ function mediaExtension(source, remote) {
689
+ let pathname = source;
690
+ if (remote) {
691
+ try {
692
+ pathname = new URL(source).pathname;
693
+ }
694
+ catch {
695
+ return "";
696
+ }
697
+ }
698
+ const filename = pathname.split(/[\\/]/).pop() ?? "";
699
+ const dot = filename.lastIndexOf(".");
700
+ return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : "";
701
+ }
702
+ function mediaKind(extension) {
703
+ if (IMAGE_EXTENSIONS.has(extension))
704
+ return "image";
705
+ if (VIDEO_EXTENSIONS.has(extension))
706
+ return "video";
707
+ if (AUDIO_EXTENSIONS.has(extension))
708
+ return "audio";
709
+ if (KNOWN_NON_MEDIA_EXTENSIONS.has(extension))
710
+ return "unsupported";
711
+ return "unknown";
712
+ }
713
+ function invalid(path, message) {
714
+ throw new CliUsageError({
715
+ code: "PARAM_INVALID",
716
+ type: "validation",
717
+ message,
718
+ hint: "请运行 gd-cli creative schema --json 查看完整输入结构。",
719
+ retryable: false,
720
+ detail: { path },
721
+ });
722
+ }