@infimind/video-mcp-cli 1.0.12 → 1.0.13

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/server.js CHANGED
@@ -8,8 +8,17 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { readFileSync, existsSync, statSync } from "fs";
10
10
  import { extname, basename } from "path";
11
- import { MCP_SERVER_VERSION, getMcpDefaultTaskMode, getMcpEngineName, getMcpVideoStatusHint, isMcpRoute, isValidMcpTaskMode, getMcpModelForRoute, isMcpModel, MCP_VIDEO_MODELS, } from "./videoRules.js";
11
+ import { MCP_PUBLIC_MODELS, MCP_SERVER_VERSION, getMcpDefaultTaskMode, getMcpEngineName, getMcpVideoStatusHint, isMcpRoute, isRetiredSaModel, isRetiredSaRoute, isValidMcpTaskMode, resolveMcpEngine, isMcpModel, } from "./videoRules.js";
12
12
  import { MCP_CREATIVE_TOOLS } from "./creativeTools.js";
13
+ const WAN3_OUTPUT_SECONDS = [5, 10, 15, 20, 25, 30];
14
+ const WAN3_OUTPUT_SECONDS_LABEL = WAN3_OUTPUT_SECONDS.join("、");
15
+ function isWan3OutputSeconds(value, mode) {
16
+ if (mode === "edit")
17
+ return value === -1;
18
+ return (typeof value === "number" &&
19
+ Number.isInteger(value) &&
20
+ WAN3_OUTPUT_SECONDS.includes(value));
21
+ }
13
22
  class McpToolError extends Error {
14
23
  code;
15
24
  route;
@@ -64,7 +73,7 @@ export function createServer(token, apiBaseUrl, options = {}) {
64
73
  {
65
74
  name: "create_video_task",
66
75
  title: "生成 AI 视频",
67
- description: "根据提示词和可选的图片、视频或音频素材创建 AI 视频生成任务,可选择 SA、SD 或 HA 专业版;素材可使用本地文件路径或公网 URL。" +
76
+ description: "根据提示词和可选的图片、视频或音频素材创建 AI 视频生成任务,可选择 WA、SD、SD25、HA 或 H3 专业版;素材可使用本地文件路径或公网 URL。" +
68
77
  "本工具返回 taskId,不直接返回最终视频;创建后请调用 get_video_status 并原样传入 taskId,轮询生成进度和结果。",
69
78
  inputSchema: {
70
79
  type: "object",
@@ -79,7 +88,7 @@ export function createServer(token, apiBaseUrl, options = {}) {
79
88
  sourceImage: {
80
89
  type: "string",
81
90
  title: "单张参考图片",
82
- description: "可选。本地图片路径或公网 URL;SA 专业版图生视频或 HA 专业版标准模式单图生成时使用。",
91
+ description: "可选。本地图片路径或公网 URL;HA 专业版标准模式单图生成时使用。",
83
92
  },
84
93
  sourceImages: {
85
94
  type: "array",
@@ -135,19 +144,45 @@ export function createServer(token, apiBaseUrl, options = {}) {
135
144
  type: "string",
136
145
  title: "输出尺寸",
137
146
  description: "可选。视频的输出尺寸,默认 1280x720;可选择横屏、竖屏、正方形、4:3 或 3:4 尺寸。",
138
- enum: ["1280x720", "720x1280", "1024x1024", "1024x768", "768x1024"],
147
+ enum: [
148
+ "1280x720",
149
+ "720x1280",
150
+ "1024x1024",
151
+ "1024x768",
152
+ "768x1024",
153
+ ],
139
154
  },
140
155
  seconds: {
141
156
  type: "number",
142
157
  title: "视频时长",
143
- description: "可选。输出视频时长,单位为秒;SA 专业版支持 10 或 15 秒,SD 专业版和 HA 专业版标准/图片参考模式支持 5、10 或 15 秒,SD2.5 专业版支持 5、10、15、20、25 或 30 秒,H3 专业版支持 5、10 或 15 秒,HA 专业版视频编辑模式由输入视频决定。",
144
- enum: [5, 10, 15, 20, 25, 30],
158
+ description: "可选。WA 普通模式仅支持 5、10、15、20、25、30 秒,默认 5 秒;WA edit 视频编辑仅支持 -1(智能时长),可省略;其他模型按各自规则。",
159
+ // JSON Schema 无法在此共享字段上可靠表达按 model 分支的 enum。
160
+ // 精确离散时长规则由 validateCreateVideoArgs 按模型统一校验。
161
+ minimum: -1,
162
+ maximum: 30,
163
+ multipleOf: 1,
145
164
  },
146
165
  language: {
147
166
  type: "string",
148
167
  title: "提示词优化语言",
149
168
  description: "可选。用于理解和优化视频提示词的语言区域代码,默认 zh-CN。",
150
- enum: ["zh-CN", "en-US", "ja-JP", "ko-KR", "es-ES", "fr-FR", "th-TH", "id-ID", "de-DE", "pt-PT", "bn-BD", "ru-RU", "ar-SA", "zh-HK", "hi-IN"],
169
+ enum: [
170
+ "zh-CN",
171
+ "en-US",
172
+ "ja-JP",
173
+ "ko-KR",
174
+ "es-ES",
175
+ "fr-FR",
176
+ "th-TH",
177
+ "id-ID",
178
+ "de-DE",
179
+ "pt-PT",
180
+ "bn-BD",
181
+ "ru-RU",
182
+ "ar-SA",
183
+ "zh-HK",
184
+ "hi-IN",
185
+ ],
151
186
  },
152
187
  count: {
153
188
  type: "number",
@@ -159,14 +194,15 @@ export function createServer(token, apiBaseUrl, options = {}) {
159
194
  model: {
160
195
  type: "string",
161
196
  title: "生成模型",
162
- description: "可选。SA=极睿 Sa 专业版(默认),SD=极睿 Sd 专业版,SD25=极睿 SD2.5 专业版,HA=极睿 Ha 专业版,H3=极睿 H3 专业版;SD、SD25、HA、H3 仅限 VIP 用户使用。",
163
- enum: ["SA", "SD", "SD25", "HA", "H3"],
197
+ description: "可选。WA=极睿 WA 专业版(默认,非 VIP 可用),SD=极睿 SD 专业版,SD25=极睿 SD2.5 专业版,HA=极睿 HA 专业版,H3=极睿 H3 专业版;除 WA 外仅限 VIP 用户使用。",
198
+ enum: [...MCP_PUBLIC_MODELS],
199
+ default: "WA",
164
200
  },
165
201
  route: {
166
202
  type: "string",
167
203
  title: "兼容模型参数",
168
- description: "已废弃的兼容参数,仅供旧客户端使用;新调用请使用 model 选择 SA、SD、SD25、HA 或 H3 专业版。",
169
- enum: ["S2", "S5", "S6", "S7", "S8"],
204
+ description: "已废弃的兼容参数,仅供旧客户端使用;新调用请使用 model。",
205
+ enum: ["S5", "S6", "S7", "S8", "S9"],
170
206
  deprecated: true,
171
207
  },
172
208
  taskMode: {
@@ -178,24 +214,30 @@ export function createServer(token, apiBaseUrl, options = {}) {
178
214
  resolution: {
179
215
  type: "string",
180
216
  title: "输出清晰度",
181
- description: "可选。SD 或 HA 专业版可选 720p 或 1080p;SD2.5 专业版仅支持 480p 或 720p;默认 720p。",
182
- enum: ["720p", "1080p"],
217
+ description: "可选。SD/HA 支持 720p 或 1080p;SD2.5 与 WA 支持 480p、720p 或 1080p(WA 480p 仅管理员);默认 720p。",
218
+ enum: ["480p", "720p", "1080p"],
219
+ },
220
+ outputFormat: {
221
+ type: "string",
222
+ title: "输出格式",
223
+ description: "可选。默认 mp4;mov 仅适用于 SD2.5 专业版。",
224
+ enum: ["mp4", "mov"],
183
225
  },
184
226
  ratio: {
185
227
  type: "string",
186
228
  title: "画面比例",
187
- description: "可选。HA 专业版图片参考或标准文生视频模式的画面比例,可选 16:9、9:16、1:1、4:3 或 3:4。",
188
- enum: ["16:9", "9:16", "1:1", "4:3", "3:4"],
229
+ description: "可选。WA 支持 adaptive、16:9、4:3、1:1、3:4、9:16,默认 adaptive。",
230
+ enum: ["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4"],
189
231
  },
190
232
  generateAudio: {
191
233
  type: "boolean",
192
234
  title: "生成音频",
193
- description: "可选。SD / SD2.5 专业版是否同时生成音频,默认 true。",
235
+ description: "可选。SD / SD2.5 / WA 专业版是否同时生成音频,默认 true。",
194
236
  },
195
237
  webSearchEnabled: {
196
238
  type: "boolean",
197
239
  title: "启用内容增强",
198
- description: "可选。SD 专业版是否启用联网内容增强,默认 false;SD2.5 专业版不支持。",
240
+ description: "可选。SD / SD2.5 专业版是否启用联网内容增强,默认 false。",
199
241
  },
200
242
  dryRun: {
201
243
  type: "boolean",
@@ -211,27 +253,107 @@ export function createServer(token, apiBaseUrl, options = {}) {
211
253
  title: "AI 视频任务创建结果",
212
254
  description: "返回任务标识、所用配置、积分信息和后续查询提示;预检或失败时部分字段可能不返回。",
213
255
  properties: {
214
- success: { type: "boolean", title: "是否成功", description: "表示任务创建或预检请求是否成功。" },
215
- taskId: { type: "number", title: "任务 ID", description: "创建成功后返回的视频任务 ID;dryRun 或失败时可能不返回,请用于调用 get_video_status。" },
216
- model: { type: "string", title: "生成模型", description: "实际使用的模型短码:SA、SD、SD25、HA 或 H3。" },
217
- route: { type: "string", title: "兼容模型标识", description: "为旧客户端返回的兼容模型标识。" },
218
- engineName: { type: "string", title: "模型名称", description: "实际使用的极睿专业版模型名称。" },
219
- usedRouteAlias: { type: "boolean", title: "是否使用兼容参数", description: "表示本次调用是否通过已废弃的 route 参数选择模型。" },
220
- taskMode: { type: "string", title: "生成模式", description: "本次任务实际使用的生成模式。" },
221
- videoCount: { type: "number", title: "视频数量", description: "本次任务计划生成的视频数量。" },
222
- dryRun: { type: "boolean", title: "是否仅预检", description: "表示本次请求是否只进行了预检而未创建任务。" },
223
- estimatedCredits: { type: "number", title: "预估积分", description: "完成本次任务预计需要的积分;无法估算时可能不返回。" },
224
- estimateSource: { type: "string", title: "估算依据", description: "预估积分所依据的素材检测结果或时长提示。" },
225
- remainingCredits: { type: "number", title: "剩余积分", description: "请求处理后的账户剩余积分。" },
226
- statusHint: { type: "string", title: "后续操作提示", description: "提示是否应使用 taskId 调用 get_video_status 查询任务进度。" },
256
+ success: {
257
+ type: "boolean",
258
+ title: "是否成功",
259
+ description: "表示任务创建或预检请求是否成功。",
260
+ },
261
+ taskId: {
262
+ type: "number",
263
+ title: "任务 ID",
264
+ description: "创建成功后返回的视频任务 ID;dryRun 或失败时可能不返回,请用于调用 get_video_status。",
265
+ },
266
+ model: {
267
+ type: "string",
268
+ title: "生成模型",
269
+ description: "实际使用的模型短码:WA、SD、SD25、HA 或 H3。",
270
+ },
271
+ route: {
272
+ type: "string",
273
+ title: "兼容模型标识",
274
+ description: "为旧客户端返回的兼容模型标识。",
275
+ },
276
+ engineName: {
277
+ type: "string",
278
+ title: "模型名称",
279
+ description: "实际使用的极睿专业版模型名称。",
280
+ },
281
+ usedRouteAlias: {
282
+ type: "boolean",
283
+ title: "是否使用兼容参数",
284
+ description: "表示本次调用是否通过已废弃的 route 参数选择模型。",
285
+ },
286
+ taskMode: {
287
+ type: "string",
288
+ title: "生成模式",
289
+ description: "本次任务实际使用的生成模式。",
290
+ },
291
+ videoCount: {
292
+ type: "number",
293
+ title: "视频数量",
294
+ description: "本次任务计划生成的视频数量。",
295
+ },
296
+ dryRun: {
297
+ type: "boolean",
298
+ title: "是否仅预检",
299
+ description: "表示本次请求是否只进行了预检而未创建任务。",
300
+ },
301
+ creditEstimateKind: {
302
+ type: "string",
303
+ title: "积分估算类型",
304
+ enum: ["reservation"],
305
+ description: "WA 视频编辑返回 reservation,表示积分预留上限。",
306
+ },
307
+ reservationCeilingCredits: {
308
+ type: "number",
309
+ title: "积分预留上限",
310
+ description: "积分预留上限;完成后按可信输入和实际输出时长结算。",
311
+ },
312
+ settlementDescription: {
313
+ type: "string",
314
+ title: "结算说明",
315
+ description: "实际积分结算说明。",
316
+ },
317
+ estimatedCredits: {
318
+ type: "number",
319
+ title: "预估积分",
320
+ description: "完成本次任务预计需要的积分;无法估算时可能不返回。",
321
+ },
322
+ estimateSource: {
323
+ type: "string",
324
+ title: "估算依据",
325
+ description: "预估积分所依据的素材检测结果或时长提示。",
326
+ },
327
+ remainingCredits: {
328
+ type: "number",
329
+ title: "剩余积分",
330
+ description: "请求处理后的账户剩余积分。",
331
+ },
332
+ statusHint: {
333
+ type: "string",
334
+ title: "后续操作提示",
335
+ description: "提示是否应使用 taskId 调用 get_video_status 查询任务进度。",
336
+ },
227
337
  validationWarnings: {
228
338
  type: "array",
229
339
  title: "预检警告",
230
340
  description: "参数或素材预检产生的非阻断性提示列表。",
231
- items: { type: "string", title: "预检警告项", description: "一条需要关注但不阻断请求的预检提示。" },
341
+ items: {
342
+ type: "string",
343
+ title: "预检警告项",
344
+ description: "一条需要关注但不阻断请求的预检提示。",
345
+ },
346
+ },
347
+ code: {
348
+ type: "string",
349
+ title: "错误代码",
350
+ description: "请求失败时返回的机器可读错误代码,成功时可能不返回。",
351
+ },
352
+ message: {
353
+ type: "string",
354
+ title: "结果消息",
355
+ description: "请求结果或失败原因的可读说明。",
232
356
  },
233
- code: { type: "string", title: "错误代码", description: "请求失败时返回的机器可读错误代码,成功时可能不返回。" },
234
- message: { type: "string", title: "结果消息", description: "请求结果或失败原因的可读说明。" },
235
357
  },
236
358
  required: ["success"],
237
359
  },
@@ -258,26 +380,106 @@ export function createServer(token, apiBaseUrl, options = {}) {
258
380
  title: "AI 视频生成状态与结果",
259
381
  description: "返回任务进度、生成结果和错误信息;任务未完成时结果链接和完成时间可能为空。",
260
382
  properties: {
261
- success: { type: "boolean", title: "是否成功", description: "表示查询请求是否成功。" },
262
- taskId: { type: "number", title: "任务 ID", description: "当前查询的视频任务 ID。" },
263
- status: { type: "string", title: "任务状态", description: "视频任务当前的处理状态;未完成时应根据 statusHint 稍后重试。" },
264
- progress: { type: "number", title: "总体进度", description: "任务总体处理进度,通常为 0–100 的数值。" },
265
- totalVideos: { type: "number", title: "视频总数", description: "任务计划生成的视频总数。" },
266
- completedVideos: { type: "number", title: "已完成数量", description: "当前已经完成生成的视频数量。" },
267
- estimatedCredits: { type: "number", title: "预估积分", description: "任务预计消耗的积分;无法估算时可能不返回。" },
268
- taskMode: { type: ["string", "null"], title: "生成模式", description: "任务使用的生成模式;旧任务或尚未确定时可能为空。" },
269
- statusHint: { type: "string", title: "状态提示", description: "面向用户的当前状态和下一步轮询提示。" },
270
- createdAt: { type: ["string", "null"], title: "创建时间", description: "任务创建时间;不可用时为空。" },
271
- updatedAt: { type: ["string", "null"], title: "更新时间", description: "任务最近更新时间;不可用时为空。" },
272
- completedAt: { type: ["string", "null"], title: "完成时间", description: "任务完成时间;未完成时为空。" },
273
- errorMessage: { type: "string", title: "错误信息", description: "任务失败时的可读原因,未失败时可能不返回。" },
274
- errorCode: { type: "string", title: "错误代码", description: "任务失败时的机器可读错误代码,未失败时可能不返回。" },
275
- errorCategory: { type: "string", title: "错误分类", description: "任务失败时的错误类别,未失败时可能不返回。" },
276
- retryable: { type: "boolean", title: "是否可重试", description: "表示失败后是否建议重新发起任务,未失败时可能不返回。" },
277
- code: { type: "string", title: "查询错误代码", description: "查询请求失败时的机器可读错误代码,成功时可能不返回。" },
278
- message: { type: "string", title: "查询消息", description: "查询结果或失败原因的可读说明。" },
279
- route: { type: "string", title: "兼容模型标识", description: "任务对应的兼容模型标识。" },
280
- engineName: { type: "string", title: "模型名称", description: "任务实际使用的极睿专业版模型名称。" },
383
+ success: {
384
+ type: "boolean",
385
+ title: "是否成功",
386
+ description: "表示查询请求是否成功。",
387
+ },
388
+ taskId: {
389
+ type: "number",
390
+ title: "任务 ID",
391
+ description: "当前查询的视频任务 ID。",
392
+ },
393
+ status: {
394
+ type: "string",
395
+ title: "任务状态",
396
+ description: "视频任务当前的处理状态;未完成时应根据 statusHint 稍后重试。",
397
+ },
398
+ progress: {
399
+ type: "number",
400
+ title: "总体进度",
401
+ description: "任务总体处理进度,通常为 0–100 的数值。",
402
+ },
403
+ totalVideos: {
404
+ type: "number",
405
+ title: "视频总数",
406
+ description: "任务计划生成的视频总数。",
407
+ },
408
+ completedVideos: {
409
+ type: "number",
410
+ title: "已完成数量",
411
+ description: "当前已经完成生成的视频数量。",
412
+ },
413
+ estimatedCredits: {
414
+ type: "number",
415
+ title: "预估积分",
416
+ description: "任务预计消耗的积分;无法估算时可能不返回。",
417
+ },
418
+ taskMode: {
419
+ type: ["string", "null"],
420
+ title: "生成模式",
421
+ description: "任务使用的生成模式;旧任务或尚未确定时可能为空。",
422
+ },
423
+ statusHint: {
424
+ type: "string",
425
+ title: "状态提示",
426
+ description: "面向用户的当前状态和下一步轮询提示。",
427
+ },
428
+ createdAt: {
429
+ type: ["string", "null"],
430
+ title: "创建时间",
431
+ description: "任务创建时间;不可用时为空。",
432
+ },
433
+ updatedAt: {
434
+ type: ["string", "null"],
435
+ title: "更新时间",
436
+ description: "任务最近更新时间;不可用时为空。",
437
+ },
438
+ completedAt: {
439
+ type: ["string", "null"],
440
+ title: "完成时间",
441
+ description: "任务完成时间;未完成时为空。",
442
+ },
443
+ errorMessage: {
444
+ type: "string",
445
+ title: "错误信息",
446
+ description: "任务失败时的可读原因,未失败时可能不返回。",
447
+ },
448
+ errorCode: {
449
+ type: "string",
450
+ title: "错误代码",
451
+ description: "任务失败时的机器可读错误代码,未失败时可能不返回。",
452
+ },
453
+ errorCategory: {
454
+ type: "string",
455
+ title: "错误分类",
456
+ description: "任务失败时的错误类别,未失败时可能不返回。",
457
+ },
458
+ retryable: {
459
+ type: "boolean",
460
+ title: "是否可重试",
461
+ description: "表示失败后是否建议重新发起任务,未失败时可能不返回。",
462
+ },
463
+ code: {
464
+ type: "string",
465
+ title: "查询错误代码",
466
+ description: "查询请求失败时的机器可读错误代码,成功时可能不返回。",
467
+ },
468
+ message: {
469
+ type: "string",
470
+ title: "查询消息",
471
+ description: "查询结果或失败原因的可读说明。",
472
+ },
473
+ route: {
474
+ type: "string",
475
+ title: "兼容模型标识",
476
+ description: "任务对应的兼容模型标识。",
477
+ },
478
+ engineName: {
479
+ type: "string",
480
+ title: "模型名称",
481
+ description: "任务实际使用的极睿专业版模型名称。",
482
+ },
281
483
  videos: {
282
484
  type: "array",
283
485
  title: "视频结果列表",
@@ -287,18 +489,64 @@ export function createServer(token, apiBaseUrl, options = {}) {
287
489
  title: "视频结果",
288
490
  description: "一个视频生成结果及其状态、链接、时间和错误信息。",
289
491
  properties: {
290
- id: { type: "number", title: "视频结果 ID", description: "该视频结果在任务中的唯一标识。" },
291
- status: { type: "string", title: "视频状态", description: "该视频当前的生成状态。" },
292
- videoUrl: { type: ["string", "null"], title: "视频 URL", description: "生成完成后的视频访问 URL;未完成或失败时为空。" },
293
- thumbnailUrl: { type: ["string", "null"], title: "缩略图 URL", description: "视频缩略图访问 URL;尚未生成时为空。" },
294
- errorMessage: { type: ["string", "null"], title: "错误信息", description: "该视频生成失败时的可读原因;未失败时为空。" },
295
- errorCode: { type: ["string", "null"], title: "错误代码", description: "该视频生成失败时的机器可读错误代码;未失败时为空。" },
296
- errorCategory: { type: ["string", "null"], title: "错误分类", description: "该视频生成失败时的错误类别;未失败时为空。" },
297
- retryable: { type: ["boolean", "null"], title: "是否可重试", description: "表示该视频失败后是否建议重新创建任务;未失败时为空。" },
298
- createdAt: { type: ["string", "null"], title: "创建时间", description: "该视频结果的创建时间;不可用时为空。" },
299
- completedAt: { type: ["string", "null"], title: "完成时间", description: "该视频的完成时间;未完成时为空。" },
492
+ id: {
493
+ type: "number",
494
+ title: "视频结果 ID",
495
+ description: "该视频结果在任务中的唯一标识。",
496
+ },
497
+ status: {
498
+ type: "string",
499
+ title: "视频状态",
500
+ description: "该视频当前的生成状态。",
501
+ },
502
+ videoUrl: {
503
+ type: ["string", "null"],
504
+ title: "视频 URL",
505
+ description: "生成完成后的视频访问 URL;未完成或失败时为空。",
506
+ },
507
+ thumbnailUrl: {
508
+ type: ["string", "null"],
509
+ title: "缩略图 URL",
510
+ description: "视频缩略图访问 URL;尚未生成时为空。",
511
+ },
512
+ errorMessage: {
513
+ type: ["string", "null"],
514
+ title: "错误信息",
515
+ description: "该视频生成失败时的可读原因;未失败时为空。",
516
+ },
517
+ errorCode: {
518
+ type: ["string", "null"],
519
+ title: "错误代码",
520
+ description: "该视频生成失败时的机器可读错误代码;未失败时为空。",
521
+ },
522
+ errorCategory: {
523
+ type: ["string", "null"],
524
+ title: "错误分类",
525
+ description: "该视频生成失败时的错误类别;未失败时为空。",
526
+ },
527
+ retryable: {
528
+ type: ["boolean", "null"],
529
+ title: "是否可重试",
530
+ description: "表示该视频失败后是否建议重新创建任务;未失败时为空。",
531
+ },
532
+ createdAt: {
533
+ type: ["string", "null"],
534
+ title: "创建时间",
535
+ description: "该视频结果的创建时间;不可用时为空。",
536
+ },
537
+ completedAt: {
538
+ type: ["string", "null"],
539
+ title: "完成时间",
540
+ description: "该视频的完成时间;未完成时为空。",
541
+ },
300
542
  },
301
- required: ["id", "status", "videoUrl", "thumbnailUrl", "errorMessage"],
543
+ required: [
544
+ "id",
545
+ "status",
546
+ "videoUrl",
547
+ "thumbnailUrl",
548
+ "errorMessage",
549
+ ],
302
550
  },
303
551
  },
304
552
  },
@@ -328,13 +576,41 @@ export function createServer(token, apiBaseUrl, options = {}) {
328
576
  title: "视频分析任务创建结果",
329
577
  description: "返回分析任务标识、积分信息和后续查询提示;失败时部分字段可能不返回。",
330
578
  properties: {
331
- success: { type: "boolean", title: "是否成功", description: "表示视频分析任务是否创建成功。" },
332
- analysisId: { type: "number", title: "分析任务 ID", description: "创建成功后返回的分析任务 ID,请用于调用 get_video_analysis_status。" },
333
- creditsUsed: { type: "number", title: "消耗积分", description: "创建本次视频分析任务实际扣除的积分。" },
334
- remainingCredits: { type: "number", title: "剩余积分", description: "任务创建后的账户剩余积分。" },
335
- statusHint: { type: "string", title: "后续操作提示", description: "提示使用 analysisId 调用 get_video_analysis_status 查询分析进度。" },
336
- code: { type: "string", title: "错误代码", description: "创建失败时返回的机器可读错误代码,成功时可能不返回。" },
337
- message: { type: "string", title: "结果消息", description: "创建结果或失败原因的可读说明。" },
579
+ success: {
580
+ type: "boolean",
581
+ title: "是否成功",
582
+ description: "表示视频分析任务是否创建成功。",
583
+ },
584
+ analysisId: {
585
+ type: "number",
586
+ title: "分析任务 ID",
587
+ description: "创建成功后返回的分析任务 ID,请用于调用 get_video_analysis_status。",
588
+ },
589
+ creditsUsed: {
590
+ type: "number",
591
+ title: "消耗积分",
592
+ description: "创建本次视频分析任务实际扣除的积分。",
593
+ },
594
+ remainingCredits: {
595
+ type: "number",
596
+ title: "剩余积分",
597
+ description: "任务创建后的账户剩余积分。",
598
+ },
599
+ statusHint: {
600
+ type: "string",
601
+ title: "后续操作提示",
602
+ description: "提示使用 analysisId 调用 get_video_analysis_status 查询分析进度。",
603
+ },
604
+ code: {
605
+ type: "string",
606
+ title: "错误代码",
607
+ description: "创建失败时返回的机器可读错误代码,成功时可能不返回。",
608
+ },
609
+ message: {
610
+ type: "string",
611
+ title: "结果消息",
612
+ description: "创建结果或失败原因的可读说明。",
613
+ },
338
614
  },
339
615
  required: ["success"],
340
616
  },
@@ -361,27 +637,85 @@ export function createServer(token, apiBaseUrl, options = {}) {
361
637
  title: "视频分析状态与结果",
362
638
  description: "返回分析进度和结构化内容;任务未完成或失败时分析内容可能为空。",
363
639
  properties: {
364
- success: { type: "boolean", title: "是否成功", description: "表示查询请求是否成功。" },
365
- analysisId: { type: "number", title: "分析任务 ID", description: "当前查询的视频分析任务 ID。" },
366
- status: { type: "string", title: "分析状态", description: "视频分析任务当前的处理状态;未完成时应稍后继续查询。" },
367
- conversionProgress: { type: ["number", "null"], title: "处理进度", description: "视频转换和分析进度,通常为 0–100;尚未开始时可能为空。" },
368
- script: { type: ["string", "null"], title: "视频脚本", description: "从参考视频中提取的脚本内容;分析未完成时为空。" },
369
- style: { type: ["string", "null"], title: "视频风格", description: "识别出的视觉、叙事和表达风格;分析未完成时为空。" },
370
- sellingPoints: { type: ["string", "null"], title: "卖点分析", description: "从视频中识别出的商品或内容卖点;分析未完成时为空。" },
371
- viralFormula: { type: ["string", "null"], title: "创作公式", description: "总结的视频结构和内容创作公式;分析未完成时为空。" },
372
- generatedPrompt: { type: ["string", "null"], title: "生成提示词", description: "基于分析结果生成的后续视频创作提示词;分析未完成时为空。" },
373
- errorMessage: { type: ["string", "null"], title: "错误信息", description: "分析失败时的可读原因;未失败时为空。" },
374
- originalVideoUrl: { type: ["string", "null"], title: "原视频 URL", description: "本次分析所使用的视频访问 URL;不可用时为空。" },
375
- videoFileName: { type: ["string", "null"], title: "视频文件名", description: "参考视频的文件名;不可用时为空。" },
376
- code: { type: "string", title: "查询错误代码", description: "查询请求失败时返回的机器可读错误代码,成功时可能不返回。" },
377
- message: { type: "string", title: "查询消息", description: "查询结果或失败原因的可读说明。" },
640
+ success: {
641
+ type: "boolean",
642
+ title: "是否成功",
643
+ description: "表示查询请求是否成功。",
644
+ },
645
+ analysisId: {
646
+ type: "number",
647
+ title: "分析任务 ID",
648
+ description: "当前查询的视频分析任务 ID。",
649
+ },
650
+ status: {
651
+ type: "string",
652
+ title: "分析状态",
653
+ description: "视频分析任务当前的处理状态;未完成时应稍后继续查询。",
654
+ },
655
+ conversionProgress: {
656
+ type: ["number", "null"],
657
+ title: "处理进度",
658
+ description: "视频转换和分析进度,通常为 0–100;尚未开始时可能为空。",
659
+ },
660
+ script: {
661
+ type: ["string", "null"],
662
+ title: "视频脚本",
663
+ description: "从参考视频中提取的脚本内容;分析未完成时为空。",
664
+ },
665
+ style: {
666
+ type: ["string", "null"],
667
+ title: "视频风格",
668
+ description: "识别出的视觉、叙事和表达风格;分析未完成时为空。",
669
+ },
670
+ sellingPoints: {
671
+ type: ["string", "null"],
672
+ title: "卖点分析",
673
+ description: "从视频中识别出的商品或内容卖点;分析未完成时为空。",
674
+ },
675
+ viralFormula: {
676
+ type: ["string", "null"],
677
+ title: "创作公式",
678
+ description: "总结的视频结构和内容创作公式;分析未完成时为空。",
679
+ },
680
+ generatedPrompt: {
681
+ type: ["string", "null"],
682
+ title: "生成提示词",
683
+ description: "基于分析结果生成的后续视频创作提示词;分析未完成时为空。",
684
+ },
685
+ errorMessage: {
686
+ type: ["string", "null"],
687
+ title: "错误信息",
688
+ description: "分析失败时的可读原因;未失败时为空。",
689
+ },
690
+ originalVideoUrl: {
691
+ type: ["string", "null"],
692
+ title: "原视频 URL",
693
+ description: "本次分析所使用的视频访问 URL;不可用时为空。",
694
+ },
695
+ videoFileName: {
696
+ type: ["string", "null"],
697
+ title: "视频文件名",
698
+ description: "参考视频的文件名;不可用时为空。",
699
+ },
700
+ code: {
701
+ type: "string",
702
+ title: "查询错误代码",
703
+ description: "查询请求失败时返回的机器可读错误代码,成功时可能不返回。",
704
+ },
705
+ message: {
706
+ type: "string",
707
+ title: "查询消息",
708
+ description: "查询结果或失败原因的可读说明。",
709
+ },
378
710
  },
379
711
  required: ["success"],
380
712
  },
381
713
  },
382
714
  ...MCP_CREATIVE_TOOLS,
383
715
  ];
384
- const filtered = options.filterTools ? options.filterTools(allTools) : allTools;
716
+ const filtered = options.filterTools
717
+ ? options.filterTools(allTools)
718
+ : allTools;
385
719
  return { tools: filtered };
386
720
  });
387
721
  /**
@@ -512,7 +846,12 @@ async function processFileInput(fileInput, expectedType, maxBytes) {
512
846
  throw new McpToolError(`文件 ${basename(filePath)} 类型不匹配:当前是 ${mimeType},但该字段需要 ${expectedType} 文件。`, "INVALID_FILE_TYPE");
513
847
  }
514
848
  const size = statSync(filePath).size;
515
- const effectiveMaxBytes = maxBytes ?? (expectedType === "video" ? 50 * 1024 * 1024 : expectedType === "audio" ? 15 * 1024 * 1024 : undefined);
849
+ const effectiveMaxBytes = maxBytes ??
850
+ (expectedType === "video"
851
+ ? 50 * 1024 * 1024
852
+ : expectedType === "audio"
853
+ ? 15 * 1024 * 1024
854
+ : undefined);
516
855
  if (effectiveMaxBytes !== undefined && size > effectiveMaxBytes) {
517
856
  throw new McpToolError(`文件 ${basename(filePath)} 超过 ${Math.floor(effectiveMaxBytes / 1024 / 1024)}MB 限制。`, "MEDIA_FILE_TOO_LARGE");
518
857
  }
@@ -537,43 +876,50 @@ async function processFileInputs(fileInputs, expectedType) {
537
876
  return processed.length > 0 ? processed : undefined;
538
877
  }
539
878
  function hasItems(value) {
540
- return Array.isArray(value) && value.some((item) => typeof item === "string" && item.length > 0);
879
+ return (Array.isArray(value) &&
880
+ value.some(item => typeof item === "string" && item.length > 0));
541
881
  }
542
882
  function itemCount(value) {
543
- return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0).length : 0;
883
+ return Array.isArray(value)
884
+ ? value.filter(item => typeof item === "string" && item.length > 0).length
885
+ : 0;
544
886
  }
545
887
  function normalizeRoute(args) {
546
- // 优先 model;若 model 未传或不是 SA/SD/HA,再看 route(route 现在是 deprecated alias)
547
- if (args && typeof args === "object" && isMcpModel(args.model)) {
548
- const m = args.model;
549
- return {
550
- route: MCP_VIDEO_MODELS[m].route,
551
- model: m,
552
- usedRouteAlias: false,
553
- };
888
+ // CLI 只做本地预检;最终模型与线路由 HTTP 服务端裁定并在响应里回传,
889
+ // 避免双配置源发生漂移。
890
+ const model = args && typeof args === "object" ? args.model : undefined;
891
+ if (isRetiredSaModel(model)) {
892
+ throw new McpToolError("SA 模型(sora-2)已下线,请改用 WA。", "MODEL_RETIRED");
893
+ }
894
+ if (isMcpModel(model)) {
895
+ return resolveMcpEngine({ model });
896
+ }
897
+ if (model !== undefined && model !== null) {
898
+ throw new McpToolError("model 必须是 WA、SD、SD25、HA 或 H3。", "INVALID_ENGINE");
554
899
  }
555
900
  const raw = args && typeof args === "object" ? args.route : undefined;
556
- const route = typeof raw === "string" ? raw : "S2";
557
- if (!isMcpRoute(route)) {
558
- throw new McpToolError("model 必须是 SA、SD、SD25、HA 或 H3;route 只接受 S2、S5、S6、S7、S8(已废弃),请改用 model。", "INVALID_ENGINE");
901
+ if (isRetiredSaRoute(raw)) {
902
+ throw new McpToolError("SA 模型(sora-2)已下线,请改用 WA。", "MODEL_RETIRED");
559
903
  }
560
- const m = getMcpModelForRoute(route);
561
- return {
562
- route,
563
- model: m,
564
- usedRouteAlias: true,
565
- };
904
+ if (raw === undefined || raw === null) {
905
+ // 省略 model/route 时统一回退到 WA,与 resolveMcpEngine 的默认值一致。
906
+ return resolveMcpEngine({});
907
+ }
908
+ if (typeof raw !== "string" || !isMcpRoute(raw)) {
909
+ throw new McpToolError("route 只接受 S5、S6、S7、S8、S9(已废弃),请改用 model。", "INVALID_ENGINE");
910
+ }
911
+ return resolveMcpEngine({ route: raw });
566
912
  }
567
913
  function normalizeTaskMode(route, value) {
568
914
  const mode = typeof value === "string" ? value : getMcpDefaultTaskMode(route);
569
- if (route === "S2") {
570
- if (value !== undefined) {
571
- throw new McpToolError("taskMode 仅适用于极睿SD专业版或极睿HA专业版;S2 极睿SA专业版无需传 taskMode。", "UNSUPPORTED_FIELD", route);
572
- }
573
- return getMcpDefaultTaskMode(route);
574
- }
575
915
  if (!isValidMcpTaskMode(route, mode)) {
576
- const allowedModes = route === "S5" || route === "S8" ? "allinone 或 firstlast" : route === "S7" ? "allinone、firstlast 或 reference" : "standard、reference 或 edit";
916
+ const allowedModes = route === "S5" || route === "S8"
917
+ ? "allinone、firstlast 或 edit"
918
+ : route === "S9"
919
+ ? "allinone、firstlast 或 edit"
920
+ : route === "S7"
921
+ ? "allinone、firstlast 或 reference"
922
+ : "standard、reference 或 edit";
577
923
  throw new McpToolError(`${getMcpEngineName(route)} 的 taskMode 只能是 ${allowedModes}。`, "INVALID_TASK_MODE", route);
578
924
  }
579
925
  return mode;
@@ -585,49 +931,69 @@ export function validateCreateVideoArgs(args) {
585
931
  if (!args.prompt || typeof args.prompt !== "string" || !args.prompt.trim()) {
586
932
  throw new McpToolError("prompt 不能为空。");
587
933
  }
588
- if (args.count !== undefined && (!Number.isInteger(args.count) || args.count < 1 || args.count > 12)) {
934
+ if (args.count !== undefined &&
935
+ (!Number.isInteger(args.count) || args.count < 1 || args.count > 12)) {
589
936
  throw new McpToolError("count 必须是 1-12 的整数。", "INVALID_COUNT");
590
937
  }
591
938
  const norm = normalizeRoute(args);
592
939
  const route = norm.route;
593
940
  const taskMode = normalizeTaskMode(route, args.taskMode);
594
941
  const resolution = typeof args.resolution === "string" ? args.resolution : "720p";
942
+ const outputFormat = typeof args.outputFormat === "string" ? args.outputFormat : "mp4";
595
943
  const dryRun = args.dryRun === true;
596
- if (route === "S8" && resolution !== "480p" && resolution !== "720p") {
597
- throw new McpToolError(`${getMcpEngineName(route)} 仅支持 480p 或 720p。`, "INVALID_RESOLUTION", route);
944
+ if (route === "S8" &&
945
+ resolution !== "480p" &&
946
+ resolution !== "720p" &&
947
+ resolution !== "1080p") {
948
+ throw new McpToolError(`${getMcpEngineName(route)} 仅支持 480p、720p 或 1080p。`, "INVALID_RESOLUTION", route);
949
+ }
950
+ if (outputFormat !== "mp4" && outputFormat !== "mov") {
951
+ throw new McpToolError("outputFormat 只能是 mp4 或 mov。", "UNSUPPORTED_FIELD", route);
952
+ }
953
+ if (outputFormat === "mov" && route !== "S8") {
954
+ throw new McpToolError(`MOV 仅适用于${getMcpEngineName("S8")}。`, "UNSUPPORTED_FIELD", route);
955
+ }
956
+ if (route === "S9" &&
957
+ resolution !== "480p" &&
958
+ resolution !== "720p" &&
959
+ resolution !== "1080p") {
960
+ throw new McpToolError(`${getMcpEngineName(route)} 仅支持 480p、720p 或 1080p。`, "INVALID_RESOLUTION", route);
598
961
  }
599
- if ((route === "S5" || route === "S6") && resolution !== "720p" && resolution !== "1080p") {
962
+ if ((route === "S5" || route === "S6") &&
963
+ resolution !== "720p" &&
964
+ resolution !== "1080p") {
600
965
  throw new McpToolError(`${getMcpEngineName(route)} 仅支持 720p 或 1080p。`, "INVALID_RESOLUTION", route);
601
966
  }
602
- if (route === "S2" && args.seconds === 5) {
603
- throw new McpToolError("极睿SA专业版不支持 5 秒视频,请使用 10 或 15 秒。", "INVALID_SECONDS", route);
967
+ if (route === "S9" &&
968
+ args.seconds !== undefined &&
969
+ !isWan3OutputSeconds(args.seconds, taskMode)) {
970
+ throw new McpToolError(taskMode === "edit"
971
+ ? "WA 视频编辑仅支持智能时长(-1)。"
972
+ : `${getMcpEngineName(route)} 视频时长仅支持 ${WAN3_OUTPUT_SECONDS_LABEL} 秒。`, "INVALID_SECONDS", route);
604
973
  }
605
974
  // S5/S6/S7/S8 的 seconds 白名单
606
975
  if (route === "S5" || route === "S6" || route === "S7" || route === "S8") {
607
- const allowedSeconds = route === "S8" ? [5, 10, 15, 20, 25, 30] : [5, 10, 15];
608
- if (args.seconds !== undefined && !allowedSeconds.includes(args.seconds)) {
609
- throw new McpToolError(route === "S8"
610
- ? `${getMcpEngineName(route)} 视频时长仅支持 5、10、15、20、25、30 秒。`
611
- : `${getMcpEngineName(route)} 视频时长仅支持 5、10、15 秒。`, "INVALID_SECONDS", route);
976
+ if ((route === "S5" || route === "S8") && taskMode === "edit") {
977
+ const maxEditSeconds = route === "S8" ? 30 : 15;
978
+ if (args.seconds !== undefined &&
979
+ (!Number.isInteger(args.seconds) ||
980
+ args.seconds < 4 ||
981
+ args.seconds > maxEditSeconds)) {
982
+ throw new McpToolError(`${getMcpEngineName(route)} Edit 参考视频时长必须为 4–${maxEditSeconds} 秒。`, "INVALID_SECONDS", route);
983
+ }
612
984
  }
613
- }
614
- if (route === "S2") {
615
- const unsupported = [
616
- hasItems(args.sourceImages) ? "sourceImages" : undefined,
617
- hasItems(args.sourceVideos) ? "sourceVideos" : undefined,
618
- hasItems(args.sourceAudios) ? "sourceAudios" : undefined,
619
- args.resolution !== undefined ? "resolution" : undefined,
620
- args.ratio !== undefined ? "ratio" : undefined,
621
- args.generateAudio !== undefined ? "generateAudio" : undefined,
622
- args.webSearchEnabled !== undefined ? "webSearchEnabled" : undefined,
623
- ].filter(Boolean);
624
- if (unsupported.length > 0) {
625
- throw new McpToolError(`这些字段不适用于极睿SA专业版:${unsupported.join("、")}。S2 单图请使用 sourceImage。`, "UNSUPPORTED_FIELD", route);
985
+ else {
986
+ const allowedSeconds = route === "S8" ? [5, 10, 15, 20, 25, 30] : [5, 10, 15];
987
+ if (args.seconds !== undefined &&
988
+ !allowedSeconds.includes(args.seconds)) {
989
+ throw new McpToolError(route === "S8"
990
+ ? `${getMcpEngineName(route)} 视频时长仅支持 5、10、15、20、25、30 秒。`
991
+ : `${getMcpEngineName(route)} 视频时长仅支持 5、10、15 秒。`, "INVALID_SECONDS", route);
992
+ }
626
993
  }
627
994
  }
628
995
  if (route === "S5" || route === "S8") {
629
- // S8 = SD2.5 独立通道:素材上限放宽(30 图 / 10 视频 / 10 音频),支持纯音频参考,
630
- // 不支持联网搜索;文案与上限按通道区分。
996
+ // S8 = SD2.5 独立通道:素材上限放宽(30 图 / 10 视频 / 10 音频)。
631
997
  const isSd25 = route === "S8";
632
998
  const sdEngine = getMcpEngineName(route);
633
999
  if (args.sourceImage !== undefined) {
@@ -636,13 +1002,11 @@ export function validateCreateVideoArgs(args) {
636
1002
  if (args.ratio !== undefined) {
637
1003
  throw new McpToolError(`ratio 仅适用于极睿HA专业版;${sdEngine}请使用 size 控制画面比例。`, "UNSUPPORTED_FIELD", route);
638
1004
  }
639
- if (isSd25 && args.webSearchEnabled !== undefined) {
640
- throw new McpToolError(`webSearchEnabled 不适用于${sdEngine}(SD2.5 不支持联网搜索)。`, "UNSUPPORTED_FIELD", route);
641
- }
642
1005
  if (taskMode === "firstlast" && !hasItems(args.sourceImages)) {
643
1006
  throw new McpToolError(`${sdEngine}首尾帧模式需要在 sourceImages 中提供至少 1 张首帧图片。`, "MISSING_SOURCE_IMAGE", route);
644
1007
  }
645
- if (taskMode === "firstlast" && (hasItems(args.sourceVideos) || hasItems(args.sourceAudios))) {
1008
+ if (taskMode === "firstlast" &&
1009
+ (hasItems(args.sourceVideos) || hasItems(args.sourceAudios))) {
646
1010
  throw new McpToolError(`sourceVideos/sourceAudios 仅适用于${sdEngine}全能参考模式;首尾帧模式只使用 sourceImages。`, "UNSUPPORTED_FIELD", route);
647
1011
  }
648
1012
  if (taskMode === "firstlast" && itemCount(args.sourceImages) > 2) {
@@ -651,19 +1015,59 @@ export function validateCreateVideoArgs(args) {
651
1015
  const maxImages = isSd25 ? 30 : 9;
652
1016
  const maxVideos = isSd25 ? 10 : 3;
653
1017
  const maxAudios = isSd25 ? 10 : 3;
654
- if (taskMode === "allinone" && itemCount(args.sourceImages) > maxImages) {
1018
+ if ((taskMode === "allinone" || taskMode === "edit") &&
1019
+ itemCount(args.sourceImages) > maxImages) {
655
1020
  throw new McpToolError(`${sdEngine}全能参考模式最多支持 ${maxImages} 张参考图片。`, "MEDIA_COUNT_EXCEEDED", route);
656
1021
  }
657
- if (taskMode === "allinone" && itemCount(args.sourceVideos) > maxVideos) {
1022
+ if ((taskMode === "allinone" || taskMode === "edit") &&
1023
+ itemCount(args.sourceVideos) > maxVideos) {
658
1024
  throw new McpToolError(`${sdEngine}全能参考模式最多支持 ${maxVideos} 个参考视频。`, "MEDIA_COUNT_EXCEEDED", route);
659
1025
  }
660
- if (taskMode === "allinone" && itemCount(args.sourceAudios) > maxAudios) {
1026
+ if ((taskMode === "allinone" || taskMode === "edit") &&
1027
+ itemCount(args.sourceAudios) > maxAudios) {
661
1028
  throw new McpToolError(`${sdEngine}全能参考模式最多支持 ${maxAudios} 个参考音频。`, "MEDIA_COUNT_EXCEEDED", route);
662
1029
  }
663
1030
  // 纯音频参考:S5 2.0 要求音频搭配图片/视频;S8(SD2.5)支持纯音频参考,豁免
664
- if (!isSd25 && taskMode === "allinone" && hasItems(args.sourceAudios) && !hasItems(args.sourceImages) && !hasItems(args.sourceVideos)) {
1031
+ if (!isSd25 &&
1032
+ taskMode === "allinone" &&
1033
+ hasItems(args.sourceAudios) &&
1034
+ !hasItems(args.sourceImages) &&
1035
+ !hasItems(args.sourceVideos)) {
665
1036
  throw new McpToolError(`${sdEngine}全能参考模式中,音频素材必须搭配至少一张参考图片或一个参考视频。`, "MISSING_VISUAL_REFERENCE", route);
666
1037
  }
1038
+ if (taskMode === "edit" && !hasItems(args.sourceVideos)) {
1039
+ throw new McpToolError(`${sdEngine} Edit 至少 1 个参考视频。`, "MISSING_SOURCE_VIDEO", route);
1040
+ }
1041
+ }
1042
+ if (route === "S9") {
1043
+ if (args.sourceImage !== undefined) {
1044
+ throw new McpToolError("sourceImage 不适用于极睿WA专业版;请使用 sourceImages。", "UNSUPPORTED_FIELD", route);
1045
+ }
1046
+ if (args.webSearchEnabled !== undefined) {
1047
+ throw new McpToolError("webSearchEnabled 不适用于极睿WA专业版。", "UNSUPPORTED_FIELD", route);
1048
+ }
1049
+ if (args.ratio !== undefined &&
1050
+ !["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16"].includes(args.ratio)) {
1051
+ throw new McpToolError("WA 不支持该画面比例。", "INVALID_RATIO", route);
1052
+ }
1053
+ if (taskMode === "firstlast") {
1054
+ const images = itemCount(args.sourceImages);
1055
+ if (images < 1 || images > 2) {
1056
+ throw new McpToolError("WA 首尾帧模式必须提供 1 张首帧,最多再提供 1 张尾帧。", "MISSING_SOURCE_IMAGE", route);
1057
+ }
1058
+ if (hasItems(args.sourceVideos) || hasItems(args.sourceAudios)) {
1059
+ throw new McpToolError("WA 参考素材与首尾帧模式严格互斥。", "UNSUPPORTED_FIELD", route);
1060
+ }
1061
+ }
1062
+ if (taskMode === "edit" && !hasItems(args.sourceVideos)) {
1063
+ throw new McpToolError("WA 视频编辑至少需要一个参考视频。", "MISSING_SOURCE_VIDEO", route);
1064
+ }
1065
+ if (taskMode !== "firstlast" && itemCount(args.sourceImages) > 10) {
1066
+ throw new McpToolError("WA 参考图片最多支持 10 张。", "MEDIA_COUNT_EXCEEDED", route);
1067
+ }
1068
+ if (itemCount(args.sourceVideos) > 5 || itemCount(args.sourceAudios) > 5) {
1069
+ throw new McpToolError("WA 参考视频和参考音频各最多支持 5 个。", "MEDIA_COUNT_EXCEEDED", route);
1070
+ }
667
1071
  }
668
1072
  if (route === "S6") {
669
1073
  const unsupported = [
@@ -674,10 +1078,12 @@ export function validateCreateVideoArgs(args) {
674
1078
  if (unsupported.length > 0) {
675
1079
  throw new McpToolError(`这些字段仅适用于极睿SD专业版,不能用于极睿HA专业版:${unsupported.join("、")}。`, "UNSUPPORTED_FIELD", route);
676
1080
  }
677
- if (taskMode === "standard" && (hasItems(args.sourceImages) || hasItems(args.sourceVideos))) {
1081
+ if (taskMode === "standard" &&
1082
+ (hasItems(args.sourceImages) || hasItems(args.sourceVideos))) {
678
1083
  throw new McpToolError("极睿HA专业版标准模式只支持 sourceImage 作为单图输入;多图请使用 reference 模式,视频请使用 edit 模式。", "UNSUPPORTED_FIELD", route);
679
1084
  }
680
- if ((taskMode === "reference" || taskMode === "edit") && args.sourceImage !== undefined) {
1085
+ if ((taskMode === "reference" || taskMode === "edit") &&
1086
+ args.sourceImage !== undefined) {
681
1087
  throw new McpToolError("sourceImage 仅适用于极睿HA专业版标准模式;reference/edit 模式请使用 sourceImages。", "UNSUPPORTED_FIELD", route);
682
1088
  }
683
1089
  if (taskMode === "reference" && !hasItems(args.sourceImages)) {
@@ -704,7 +1110,8 @@ export function validateCreateVideoArgs(args) {
704
1110
  if (args.sourceImage !== undefined) {
705
1111
  throw new McpToolError(`sourceImage 不适用于${getMcpEngineName(route)};请使用 sourceImages 传参考图或首尾帧。`, "UNSUPPORTED_FIELD", route);
706
1112
  }
707
- if (args.generateAudio !== undefined || args.webSearchEnabled !== undefined) {
1113
+ if (args.generateAudio !== undefined ||
1114
+ args.webSearchEnabled !== undefined) {
708
1115
  throw new McpToolError(`generateAudio/webSearchEnabled 不适用于${getMcpEngineName(route)}。`, "UNSUPPORTED_FIELD", route);
709
1116
  }
710
1117
  if (taskMode === "firstlast") {
@@ -734,6 +1141,7 @@ export function validateCreateVideoArgs(args) {
734
1141
  usedRouteAlias: norm.usedRouteAlias,
735
1142
  taskMode,
736
1143
  resolution,
1144
+ outputFormat,
737
1145
  dryRun,
738
1146
  statusHint: getMcpVideoStatusHint(dryRun),
739
1147
  };
@@ -746,7 +1154,8 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
746
1154
  const isProfessionalRoute = normalized.route === "S5" ||
747
1155
  normalized.route === "S6" ||
748
1156
  normalized.route === "S7" ||
749
- normalized.route === "S8";
1157
+ normalized.route === "S8" ||
1158
+ normalized.route === "S9";
750
1159
  const requestBody = {
751
1160
  token,
752
1161
  prompt: args.prompt,
@@ -754,7 +1163,12 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
754
1163
  ? (await processFileInput(args.sourceImage, "image")).data
755
1164
  : undefined,
756
1165
  size: args.size || "1280x720",
757
- seconds: args.seconds || 10,
1166
+ seconds: args.seconds ??
1167
+ (normalized.route === "S9"
1168
+ ? normalized.taskMode === "edit"
1169
+ ? -1
1170
+ : 5
1171
+ : 10),
758
1172
  language: args.language || "zh-CN",
759
1173
  count: args.count || 1,
760
1174
  model: normalized.model,
@@ -768,7 +1182,9 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
768
1182
  requestBody.taskMode = normalized.taskMode;
769
1183
  requestBody.isFirstLastMode = normalized.taskMode === "firstlast";
770
1184
  requestBody.resolution = normalized.resolution;
771
- requestBody.ratio = args.ratio || undefined;
1185
+ requestBody.outputFormat = normalized.outputFormat;
1186
+ requestBody.ratio =
1187
+ args.ratio || (normalized.route === "S9" ? "adaptive" : undefined);
772
1188
  requestBody.generateAudio = args.generateAudio;
773
1189
  requestBody.webSearchEnabled = args.webSearchEnabled;
774
1190
  if (normalized.dryRun) {
@@ -793,7 +1209,9 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
793
1209
  `- **当前积分**: ${response.remainingCredits}`,
794
1210
  ];
795
1211
  if (typeof estimatedCredits === "number") {
796
- result.splice(6, 0, `- **预估积分**: ${estimatedCredits}(视频完成后按成功扣费)`);
1212
+ result.splice(6, 0, response.creditEstimateKind === "reservation"
1213
+ ? `- **积分预留上限**: ${response.reservationCeilingCredits ?? estimatedCredits}(完成后按输入视频及实际时长结算)`
1214
+ : `- **预估积分**: ${estimatedCredits}(视频完成后按成功扣费)`);
797
1215
  }
798
1216
  if (!dryRun && response.taskId !== undefined) {
799
1217
  result.splice(4, 0, `- **任务 ID**: ${response.taskId}`);
@@ -820,6 +1238,13 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
820
1238
  videoCount: response.videoCount,
821
1239
  dryRun,
822
1240
  estimatedCredits,
1241
+ ...(response.creditEstimateKind === "reservation"
1242
+ ? {
1243
+ creditEstimateKind: response.creditEstimateKind,
1244
+ reservationCeilingCredits: response.reservationCeilingCredits ?? estimatedCredits,
1245
+ settlementDescription: response.settlementDescription,
1246
+ }
1247
+ : {}),
823
1248
  estimateSource: response.estimateSource,
824
1249
  remainingCredits: response.remainingCredits,
825
1250
  statusHint,
@@ -833,9 +1258,13 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
833
1258
  */
834
1259
  async function handleGetVideoStatus(apiBaseUrl, token, args) {
835
1260
  const response = await callRestApi(apiBaseUrl, `/mcp/video/status/${args.taskId}`, { token }, "GET");
836
- const statusText = response.status === "completed" ? "✅ 已完成" :
837
- response.status === "processing" ? "🔄 处理中" :
838
- response.status === "failed" ? "❌ 失败" : `⏳ ${response.status}`;
1261
+ const statusText = response.status === "completed"
1262
+ ? "✅ 已完成"
1263
+ : response.status === "processing"
1264
+ ? "🔄 处理中"
1265
+ : response.status === "failed"
1266
+ ? "❌ 失败"
1267
+ : `⏳ ${response.status}`;
839
1268
  let result = [
840
1269
  `# 视频生成任务状态`,
841
1270
  ``,
@@ -844,7 +1273,9 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
844
1273
  `- **进度**: ${response.progress}% (${response.completedVideos}/${response.totalVideos} 个视频)`,
845
1274
  response.engineName ? `- **生成引擎**: ${response.engineName}` : undefined,
846
1275
  response.taskMode ? `- **模式**: ${response.taskMode}` : undefined,
847
- typeof response.estimatedCredits === "number" ? `- **预估积分**: ${response.estimatedCredits}` : undefined,
1276
+ typeof response.estimatedCredits === "number"
1277
+ ? `- **预估积分**: ${response.estimatedCredits}`
1278
+ : undefined,
848
1279
  ``,
849
1280
  ].filter((line) => line !== undefined);
850
1281
  if (response.errorMessage) {
@@ -857,9 +1288,13 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
857
1288
  if (response.videos && response.videos.length > 0) {
858
1289
  result.push(`## 视频列表:`);
859
1290
  response.videos.forEach((video, index) => {
860
- const videoStatus = video.status === "completed" ? "✅" :
861
- video.status === "processing" ? "🔄" :
862
- video.status === "failed" ? "❌" : "⏳";
1291
+ const videoStatus = video.status === "completed"
1292
+ ? "✅"
1293
+ : video.status === "processing"
1294
+ ? "🔄"
1295
+ : video.status === "failed"
1296
+ ? "❌"
1297
+ : "⏳";
863
1298
  result.push(``);
864
1299
  result.push(`${index + 1}. ${videoStatus} 视频 ID: ${video.id}`);
865
1300
  if (video.videoUrl) {
@@ -949,10 +1384,15 @@ async function handleCreateVideoAnalysisTask(apiBaseUrl, token, args) {
949
1384
  */
950
1385
  async function handleGetVideoAnalysisStatus(apiBaseUrl, token, args) {
951
1386
  const response = await callRestApi(apiBaseUrl, `/mcp/video-analysis/status/${args.analysisId}`, { token }, "GET");
952
- const statusText = response.status === "completed" ? "✅ 已完成" :
953
- response.status === "processing" ? "🔄 处理中" :
954
- response.status === "pending" ? "⏳ 等待中" :
955
- response.status === "failed" ? "❌ 失败" : `⏳ ${response.status}`;
1387
+ const statusText = response.status === "completed"
1388
+ ? "✅ 已完成"
1389
+ : response.status === "processing"
1390
+ ? "🔄 处理中"
1391
+ : response.status === "pending"
1392
+ ? "⏳ 等待中"
1393
+ : response.status === "failed"
1394
+ ? "❌ 失败"
1395
+ : `⏳ ${response.status}`;
956
1396
  let result = [
957
1397
  `# 视频拆解任务状态`,
958
1398
  ``,
@@ -1032,7 +1472,9 @@ function creativeToolResult(output, lines) {
1032
1472
  };
1033
1473
  }
1034
1474
  async function handleCreateMotionVideoRemakeTask(apiBaseUrl, token, args) {
1035
- if (!args || typeof args.sourceImage !== "string" || typeof args.referenceVideo !== "string") {
1475
+ if (!args ||
1476
+ typeof args.sourceImage !== "string" ||
1477
+ typeof args.referenceVideo !== "string") {
1036
1478
  throw new McpToolError("sourceImage 和 referenceVideo 不能为空。", "MISSING_PARAMETERS");
1037
1479
  }
1038
1480
  const sourceImage = await processFileInput(args.sourceImage, "image", 5 * 1024 * 1024);
@@ -1053,7 +1495,8 @@ async function handleCreateMotionVideoRemakeTask(apiBaseUrl, token, args) {
1053
1495
  response.estimateSource === "duration_hint"
1054
1496
  ? `- 时长提示: ${response.durationHintSeconds} 秒(正式创建将使用 OSS 真实检测值)`
1055
1497
  : "- 时长提示: 未提供,正式创建后由 OSS 检测",
1056
- response.estimatedCredits === null || response.estimatedCredits === undefined
1498
+ response.estimatedCredits === null ||
1499
+ response.estimatedCredits === undefined
1057
1500
  ? "- 预计积分: 暂不可用"
1058
1501
  : `- 预计积分: ${response.estimatedCredits}`,
1059
1502
  `- 当前余额: ${response.remainingCredits}`,
@@ -1084,11 +1527,15 @@ async function handleGetMotionVideoRemakeStatus(apiBaseUrl, token, args) {
1084
1527
  `- 状态: ${response.status}`,
1085
1528
  `- 进度: ${response.progress}%`,
1086
1529
  `- 已完成: ${completed}/${response.count}`,
1087
- ...(response.status === "completed" ? ["- 视频链接已返回在 results 中"] : []),
1530
+ ...(response.status === "completed"
1531
+ ? ["- 视频链接已返回在 results 中"]
1532
+ : []),
1088
1533
  ]);
1089
1534
  }
1090
1535
  async function handleCreateEcommerceStoryboardTask(apiBaseUrl, token, args) {
1091
- if (!args || typeof args.productName !== "string" || typeof args.productDescription !== "string") {
1536
+ if (!args ||
1537
+ typeof args.productName !== "string" ||
1538
+ typeof args.productDescription !== "string") {
1092
1539
  throw new McpToolError("productName 和 productDescription 不能为空。", "MISSING_PARAMETERS");
1093
1540
  }
1094
1541
  const productImage = args.productImage
@@ -1148,7 +1595,9 @@ async function handleGetEcommerceStoryboardStatus(apiBaseUrl, token, args) {
1148
1595
  */
1149
1596
  async function callRestApi(apiBaseUrl, path, params, method = "GET") {
1150
1597
  // Build URL
1151
- const baseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl.slice(0, -1) : apiBaseUrl;
1598
+ const baseUrl = apiBaseUrl.endsWith("/")
1599
+ ? apiBaseUrl.slice(0, -1)
1600
+ : apiBaseUrl;
1152
1601
  const fullPath = path.startsWith("/") ? path : "/" + path;
1153
1602
  let fullUrl = `${baseUrl}${fullPath}`;
1154
1603
  // For GET requests, add params as query string