@infimind/video-mcp-cli 1.0.11 → 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/README.md +83 -28
- package/dist/auth/credentialWriter.js +85 -0
- package/dist/auth/exchange.js +67 -0
- package/dist/auth/installationId.js +36 -0
- package/dist/auth/login.js +77 -0
- package/dist/auth/logout.js +34 -0
- package/dist/auth/loopback.js +155 -0
- package/dist/auth/paths.js +30 -0
- package/dist/auth/pkce.js +11 -0
- package/dist/auth/profile.js +15 -0
- package/dist/auth/redact.js +63 -0
- package/dist/auth/status.js +35 -0
- package/dist/creativeTools.js +585 -0
- package/dist/index.js +139 -39
- package/dist/server.js +900 -211
- package/dist/videoRules.js +102 -20
- package/package.json +35 -8
- package/dist/index.d.ts +0 -2
- package/dist/server.d.ts +0 -11
- package/dist/videoRules.d.ts +0 -49
package/dist/server.js
CHANGED
|
@@ -8,7 +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,
|
|
11
|
+
import { MCP_PUBLIC_MODELS, MCP_SERVER_VERSION, getMcpDefaultTaskMode, getMcpEngineName, getMcpVideoStatusHint, isMcpRoute, isRetiredSaModel, isRetiredSaRoute, isValidMcpTaskMode, resolveMcpEngine, isMcpModel, } from "./videoRules.js";
|
|
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
|
+
}
|
|
12
22
|
class McpToolError extends Error {
|
|
13
23
|
code;
|
|
14
24
|
route;
|
|
@@ -21,10 +31,7 @@ class McpToolError extends Error {
|
|
|
21
31
|
this.engineName = route ? getMcpEngineName(route) : undefined;
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
|
-
|
|
25
|
-
* Create and configure the MCP Server
|
|
26
|
-
*/
|
|
27
|
-
export function createServer(token, apiBaseUrl) {
|
|
34
|
+
export function createServer(token, apiBaseUrl, options = {}) {
|
|
28
35
|
const server = new Server({
|
|
29
36
|
name: "sora-video-generator-mcp",
|
|
30
37
|
version: MCP_SERVER_VERSION,
|
|
@@ -37,7 +44,7 @@ export function createServer(token, apiBaseUrl) {
|
|
|
37
44
|
* List available tools
|
|
38
45
|
*/
|
|
39
46
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
40
|
-
const
|
|
47
|
+
const allTools = [
|
|
41
48
|
{
|
|
42
49
|
name: "get_user_info",
|
|
43
50
|
title: "获取用户信息",
|
|
@@ -65,103 +72,177 @@ export function createServer(token, apiBaseUrl) {
|
|
|
65
72
|
},
|
|
66
73
|
{
|
|
67
74
|
name: "create_video_task",
|
|
68
|
-
title: "
|
|
69
|
-
description: "
|
|
70
|
-
"
|
|
75
|
+
title: "生成 AI 视频",
|
|
76
|
+
description: "根据提示词和可选的图片、视频或音频素材创建 AI 视频生成任务,可选择 WA、SD、SD25、HA 或 H3 专业版;素材可使用本地文件路径或公网 URL。" +
|
|
77
|
+
"本工具返回 taskId,不直接返回最终视频;创建后请调用 get_video_status 并原样传入 taskId,轮询生成进度和结果。",
|
|
71
78
|
inputSchema: {
|
|
72
79
|
type: "object",
|
|
80
|
+
title: "AI 视频生成参数",
|
|
81
|
+
description: "创建 AI 视频生成任务所需的提示词、参考素材和生成配置。",
|
|
73
82
|
properties: {
|
|
74
83
|
prompt: {
|
|
75
84
|
type: "string",
|
|
76
|
-
|
|
85
|
+
title: "视频生成提示词",
|
|
86
|
+
description: "必填。描述希望生成的视频内容、主体、动作、场景、镜头和风格。",
|
|
77
87
|
},
|
|
78
88
|
sourceImage: {
|
|
79
89
|
type: "string",
|
|
80
|
-
|
|
90
|
+
title: "单张参考图片",
|
|
91
|
+
description: "可选。本地图片路径或公网 URL;HA 专业版标准模式单图生成时使用。",
|
|
81
92
|
},
|
|
82
93
|
sourceImages: {
|
|
83
94
|
type: "array",
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
title: "参考图片列表",
|
|
96
|
+
description: "可选。本地图片路径或公网 URL 列表;SD 专业版首尾帧模式按首帧、尾帧顺序传入,HA 专业版图片参考或视频编辑模式按引用顺序传入。",
|
|
97
|
+
items: {
|
|
98
|
+
type: "string",
|
|
99
|
+
title: "参考图片",
|
|
100
|
+
description: "一张参考图片的本地路径或公网 URL。",
|
|
101
|
+
},
|
|
86
102
|
},
|
|
87
103
|
sourceVideos: {
|
|
88
104
|
type: "array",
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
title: "参考视频列表",
|
|
106
|
+
description: "可选。本地视频路径或公网 URL 列表;用于 SD 专业版全能参考模式或 HA 专业版视频编辑模式。",
|
|
107
|
+
items: {
|
|
108
|
+
type: "string",
|
|
109
|
+
title: "参考视频",
|
|
110
|
+
description: "一段参考视频的本地路径或公网 URL。",
|
|
111
|
+
},
|
|
91
112
|
},
|
|
92
113
|
sourceAudios: {
|
|
93
114
|
type: "array",
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
title: "参考音频列表",
|
|
116
|
+
description: "可选。本地音频路径或公网 URL 列表;用于 SD 专业版全能参考模式。",
|
|
117
|
+
items: {
|
|
118
|
+
type: "string",
|
|
119
|
+
title: "参考音频",
|
|
120
|
+
description: "一段参考音频的本地路径或公网 URL。",
|
|
121
|
+
},
|
|
96
122
|
},
|
|
97
123
|
sourceVideoDurationSeconds: {
|
|
98
124
|
type: "array",
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
title: "参考视频时长列表",
|
|
126
|
+
description: "可选。仅在 dryRun 预检时用于估算积分,单位为秒,顺序须与 sourceVideos 一致。",
|
|
127
|
+
items: {
|
|
128
|
+
type: "number",
|
|
129
|
+
title: "参考视频时长",
|
|
130
|
+
description: "对应参考视频的时长,单位为秒。",
|
|
131
|
+
},
|
|
101
132
|
},
|
|
102
133
|
sourceAudioDurationSeconds: {
|
|
103
134
|
type: "array",
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
title: "参考音频时长列表",
|
|
136
|
+
description: "可选。仅在 dryRun 预检时用于估算积分,单位为秒,顺序须与 sourceAudios 一致。",
|
|
137
|
+
items: {
|
|
138
|
+
type: "number",
|
|
139
|
+
title: "参考音频时长",
|
|
140
|
+
description: "对应参考音频的时长,单位为秒。",
|
|
141
|
+
},
|
|
106
142
|
},
|
|
107
143
|
size: {
|
|
108
144
|
type: "string",
|
|
109
|
-
|
|
110
|
-
|
|
145
|
+
title: "输出尺寸",
|
|
146
|
+
description: "可选。视频的输出尺寸,默认 1280x720;可选择横屏、竖屏、正方形、4:3 或 3:4 尺寸。",
|
|
147
|
+
enum: [
|
|
148
|
+
"1280x720",
|
|
149
|
+
"720x1280",
|
|
150
|
+
"1024x1024",
|
|
151
|
+
"1024x768",
|
|
152
|
+
"768x1024",
|
|
153
|
+
],
|
|
111
154
|
},
|
|
112
155
|
seconds: {
|
|
113
156
|
type: "number",
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
title: "视频时长",
|
|
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,
|
|
116
164
|
},
|
|
117
165
|
language: {
|
|
118
166
|
type: "string",
|
|
119
|
-
|
|
120
|
-
|
|
167
|
+
title: "提示词优化语言",
|
|
168
|
+
description: "可选。用于理解和优化视频提示词的语言区域代码,默认 zh-CN。",
|
|
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
|
+
],
|
|
121
186
|
},
|
|
122
187
|
count: {
|
|
123
188
|
type: "number",
|
|
124
|
-
|
|
189
|
+
title: "生成数量",
|
|
190
|
+
description: "可选。需要生成的视频数量,范围 1–12,默认 1。",
|
|
125
191
|
minimum: 1,
|
|
126
192
|
maximum: 12,
|
|
127
193
|
},
|
|
128
194
|
model: {
|
|
129
195
|
type: "string",
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
title: "生成模型",
|
|
197
|
+
description: "可选。WA=极睿 WA 专业版(默认,非 VIP 可用),SD=极睿 SD 专业版,SD25=极睿 SD2.5 专业版,HA=极睿 HA 专业版,H3=极睿 H3 专业版;除 WA 外仅限 VIP 用户使用。",
|
|
198
|
+
enum: [...MCP_PUBLIC_MODELS],
|
|
199
|
+
default: "WA",
|
|
132
200
|
},
|
|
133
201
|
route: {
|
|
134
202
|
type: "string",
|
|
135
|
-
|
|
136
|
-
|
|
203
|
+
title: "兼容模型参数",
|
|
204
|
+
description: "已废弃的兼容参数,仅供旧客户端使用;新调用请使用 model。",
|
|
205
|
+
enum: ["S5", "S6", "S7", "S8", "S9"],
|
|
137
206
|
deprecated: true,
|
|
138
207
|
},
|
|
139
208
|
taskMode: {
|
|
140
209
|
type: "string",
|
|
141
|
-
|
|
210
|
+
title: "生成模式",
|
|
211
|
+
description: "可选。allinone=全能参考,firstlast=首尾帧,standard=标准模式,reference=图片参考,edit=视频编辑;可用模式取决于所选模型。",
|
|
142
212
|
enum: ["allinone", "firstlast", "standard", "reference", "edit"],
|
|
143
213
|
},
|
|
144
214
|
resolution: {
|
|
145
215
|
type: "string",
|
|
146
|
-
|
|
147
|
-
|
|
216
|
+
title: "输出清晰度",
|
|
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"],
|
|
148
225
|
},
|
|
149
226
|
ratio: {
|
|
150
227
|
type: "string",
|
|
151
|
-
|
|
152
|
-
|
|
228
|
+
title: "画面比例",
|
|
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"],
|
|
153
231
|
},
|
|
154
232
|
generateAudio: {
|
|
155
233
|
type: "boolean",
|
|
156
|
-
|
|
234
|
+
title: "生成音频",
|
|
235
|
+
description: "可选。SD / SD2.5 / WA 专业版是否同时生成音频,默认 true。",
|
|
157
236
|
},
|
|
158
237
|
webSearchEnabled: {
|
|
159
238
|
type: "boolean",
|
|
160
|
-
|
|
239
|
+
title: "启用内容增强",
|
|
240
|
+
description: "可选。SD / SD2.5 专业版是否启用联网内容增强,默认 false。",
|
|
161
241
|
},
|
|
162
242
|
dryRun: {
|
|
163
243
|
type: "boolean",
|
|
164
|
-
|
|
244
|
+
title: "仅预检",
|
|
245
|
+
description: "可选。设为 true 时仅校验参数、素材、权限并估算积分,不创建任务且不扣费;默认 false。",
|
|
165
246
|
},
|
|
166
247
|
},
|
|
167
248
|
required: ["prompt"],
|
|
@@ -169,84 +250,303 @@ export function createServer(token, apiBaseUrl) {
|
|
|
169
250
|
},
|
|
170
251
|
outputSchema: {
|
|
171
252
|
type: "object",
|
|
253
|
+
title: "AI 视频任务创建结果",
|
|
254
|
+
description: "返回任务标识、所用配置、积分信息和后续查询提示;预检或失败时部分字段可能不返回。",
|
|
172
255
|
properties: {
|
|
173
|
-
success: {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
},
|
|
186
337
|
validationWarnings: {
|
|
187
338
|
type: "array",
|
|
188
|
-
|
|
339
|
+
title: "预检警告",
|
|
340
|
+
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: "请求结果或失败原因的可读说明。",
|
|
189
356
|
},
|
|
190
|
-
code: { type: "string" },
|
|
191
|
-
message: { type: "string" },
|
|
192
357
|
},
|
|
193
358
|
required: ["success"],
|
|
194
359
|
},
|
|
195
360
|
},
|
|
196
361
|
{
|
|
197
362
|
name: "get_video_status",
|
|
198
|
-
title: "
|
|
199
|
-
description: "
|
|
363
|
+
title: "查询 AI 视频生成结果",
|
|
364
|
+
description: "根据 create_video_task 返回的 taskId 查询 AI 视频生成进度和结果。任务未完成时请间隔一段时间后使用同一 taskId 再次调用;完成后返回视频链接,失败时返回原因。",
|
|
200
365
|
inputSchema: {
|
|
201
366
|
type: "object",
|
|
367
|
+
title: "AI 视频任务查询参数",
|
|
368
|
+
description: "指定需要查询的视频生成任务。",
|
|
202
369
|
properties: {
|
|
203
370
|
taskId: {
|
|
204
371
|
type: "number",
|
|
205
|
-
|
|
372
|
+
title: "任务 ID",
|
|
373
|
+
description: "必填。由 create_video_task 返回的视频任务 ID,请原样传入并用于后续轮询。",
|
|
206
374
|
},
|
|
207
375
|
},
|
|
208
376
|
required: ["taskId"],
|
|
209
377
|
},
|
|
210
378
|
outputSchema: {
|
|
211
379
|
type: "object",
|
|
380
|
+
title: "AI 视频生成状态与结果",
|
|
381
|
+
description: "返回任务进度、生成结果和错误信息;任务未完成时结果链接和完成时间可能为空。",
|
|
212
382
|
properties: {
|
|
213
|
-
success: {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
},
|
|
233
483
|
videos: {
|
|
234
484
|
type: "array",
|
|
485
|
+
title: "视频结果列表",
|
|
486
|
+
description: "每个待生成视频的状态和结果;单项未完成时链接可能为空。",
|
|
235
487
|
items: {
|
|
236
488
|
type: "object",
|
|
489
|
+
title: "视频结果",
|
|
490
|
+
description: "一个视频生成结果及其状态、链接、时间和错误信息。",
|
|
237
491
|
properties: {
|
|
238
|
-
id: {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
},
|
|
248
542
|
},
|
|
249
|
-
required: [
|
|
543
|
+
required: [
|
|
544
|
+
"id",
|
|
545
|
+
"status",
|
|
546
|
+
"videoUrl",
|
|
547
|
+
"thumbnailUrl",
|
|
548
|
+
"errorMessage",
|
|
549
|
+
],
|
|
250
550
|
},
|
|
251
551
|
},
|
|
252
552
|
},
|
|
@@ -255,70 +555,168 @@ export function createServer(token, apiBaseUrl) {
|
|
|
255
555
|
},
|
|
256
556
|
{
|
|
257
557
|
name: "create_video_analysis_task",
|
|
258
|
-
title: "
|
|
259
|
-
description: "
|
|
260
|
-
"
|
|
558
|
+
title: "分析参考视频内容",
|
|
559
|
+
description: "分析本地视频或公网 URL,提取脚本、风格、卖点、创作公式并生成视频创作提示词;本工具不会生成最终视频。" +
|
|
560
|
+
"本工具返回 analysisId,创建后请调用 get_video_analysis_status 并原样传入 analysisId,轮询分析进度和结果。",
|
|
261
561
|
inputSchema: {
|
|
262
562
|
type: "object",
|
|
563
|
+
title: "参考视频分析参数",
|
|
564
|
+
description: "指定需要分析的参考视频。",
|
|
263
565
|
properties: {
|
|
264
566
|
videoFile: {
|
|
265
567
|
type: "string",
|
|
266
|
-
|
|
568
|
+
title: "参考视频",
|
|
569
|
+
description: "必填。需要分析的视频本地文件路径或公网 URL。",
|
|
267
570
|
},
|
|
268
571
|
},
|
|
269
572
|
required: ["videoFile"],
|
|
270
573
|
},
|
|
271
574
|
outputSchema: {
|
|
272
575
|
type: "object",
|
|
576
|
+
title: "视频分析任务创建结果",
|
|
577
|
+
description: "返回分析任务标识、积分信息和后续查询提示;失败时部分字段可能不返回。",
|
|
273
578
|
properties: {
|
|
274
|
-
success: {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
},
|
|
281
614
|
},
|
|
282
615
|
required: ["success"],
|
|
283
616
|
},
|
|
284
617
|
},
|
|
285
618
|
{
|
|
286
619
|
name: "get_video_analysis_status",
|
|
287
|
-
title: "
|
|
288
|
-
description: "
|
|
620
|
+
title: "查询视频分析结果",
|
|
621
|
+
description: "根据 create_video_analysis_task 返回的 analysisId 查询分析进度和结构化结果。任务未完成时请间隔一段时间后使用同一 analysisId 再次调用。",
|
|
289
622
|
inputSchema: {
|
|
290
623
|
type: "object",
|
|
624
|
+
title: "视频分析任务查询参数",
|
|
625
|
+
description: "指定需要查询的视频分析任务。",
|
|
291
626
|
properties: {
|
|
292
627
|
analysisId: {
|
|
293
628
|
type: "number",
|
|
294
|
-
|
|
629
|
+
title: "分析任务 ID",
|
|
630
|
+
description: "必填。由 create_video_analysis_task 返回的分析任务 ID,请原样传入并用于后续轮询。",
|
|
295
631
|
},
|
|
296
632
|
},
|
|
297
633
|
required: ["analysisId"],
|
|
298
634
|
},
|
|
299
635
|
outputSchema: {
|
|
300
636
|
type: "object",
|
|
637
|
+
title: "视频分析状态与结果",
|
|
638
|
+
description: "返回分析进度和结构化内容;任务未完成或失败时分析内容可能为空。",
|
|
301
639
|
properties: {
|
|
302
|
-
success: {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
},
|
|
316
710
|
},
|
|
317
711
|
required: ["success"],
|
|
318
712
|
},
|
|
319
713
|
},
|
|
714
|
+
...MCP_CREATIVE_TOOLS,
|
|
320
715
|
];
|
|
321
|
-
|
|
716
|
+
const filtered = options.filterTools
|
|
717
|
+
? options.filterTools(allTools)
|
|
718
|
+
: allTools;
|
|
719
|
+
return { tools: filtered };
|
|
322
720
|
});
|
|
323
721
|
/**
|
|
324
722
|
* Handle tool calls
|
|
@@ -337,6 +735,14 @@ export function createServer(token, apiBaseUrl) {
|
|
|
337
735
|
return await handleCreateVideoAnalysisTask(apiBaseUrl, token, args);
|
|
338
736
|
case "get_video_analysis_status":
|
|
339
737
|
return await handleGetVideoAnalysisStatus(apiBaseUrl, token, args);
|
|
738
|
+
case "create_motion_video_remake_task":
|
|
739
|
+
return await handleCreateMotionVideoRemakeTask(apiBaseUrl, token, args);
|
|
740
|
+
case "get_motion_video_remake_status":
|
|
741
|
+
return await handleGetMotionVideoRemakeStatus(apiBaseUrl, token, args);
|
|
742
|
+
case "create_ecommerce_storyboard_task":
|
|
743
|
+
return await handleCreateEcommerceStoryboardTask(apiBaseUrl, token, args);
|
|
744
|
+
case "get_ecommerce_storyboard_status":
|
|
745
|
+
return await handleGetEcommerceStoryboardStatus(apiBaseUrl, token, args);
|
|
340
746
|
default:
|
|
341
747
|
throw new Error(`Unknown tool: ${name}`);
|
|
342
748
|
}
|
|
@@ -402,7 +808,7 @@ async function handleGetUserInfo(apiBaseUrl, token) {
|
|
|
402
808
|
* 处理文件输入:支持本地文件路径或公网 URL
|
|
403
809
|
* 对于本地文件,读取并转换为 base64
|
|
404
810
|
*/
|
|
405
|
-
async function processFileInput(fileInput, expectedType) {
|
|
811
|
+
async function processFileInput(fileInput, expectedType, maxBytes) {
|
|
406
812
|
// 检查是否是公网 URL
|
|
407
813
|
if (fileInput.startsWith("http://") || fileInput.startsWith("https://")) {
|
|
408
814
|
return {
|
|
@@ -418,8 +824,6 @@ async function processFileInput(fileInput, expectedType) {
|
|
|
418
824
|
if (!existsSync(filePath)) {
|
|
419
825
|
throw new McpToolError(`文件不存在: ${filePath}`, "FILE_NOT_FOUND");
|
|
420
826
|
}
|
|
421
|
-
// 读取文件
|
|
422
|
-
const buffer = readFileSync(filePath);
|
|
423
827
|
// 获取文件扩展名并确定 MIME type
|
|
424
828
|
const ext = extname(filePath).toLowerCase();
|
|
425
829
|
let mimeType = "application/octet-stream";
|
|
@@ -442,12 +846,17 @@ async function processFileInput(fileInput, expectedType) {
|
|
|
442
846
|
throw new McpToolError(`文件 ${basename(filePath)} 类型不匹配:当前是 ${mimeType},但该字段需要 ${expectedType} 文件。`, "INVALID_FILE_TYPE");
|
|
443
847
|
}
|
|
444
848
|
const size = statSync(filePath).size;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
849
|
+
const effectiveMaxBytes = maxBytes ??
|
|
850
|
+
(expectedType === "video"
|
|
851
|
+
? 50 * 1024 * 1024
|
|
852
|
+
: expectedType === "audio"
|
|
853
|
+
? 15 * 1024 * 1024
|
|
854
|
+
: undefined);
|
|
855
|
+
if (effectiveMaxBytes !== undefined && size > effectiveMaxBytes) {
|
|
856
|
+
throw new McpToolError(`文件 ${basename(filePath)} 超过 ${Math.floor(effectiveMaxBytes / 1024 / 1024)}MB 限制。`, "MEDIA_FILE_TOO_LARGE");
|
|
450
857
|
}
|
|
858
|
+
// 通过大小预检后再读取,避免将超限文件装入内存。
|
|
859
|
+
const buffer = readFileSync(filePath);
|
|
451
860
|
// 转换为 base64
|
|
452
861
|
const base64 = `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
453
862
|
return {
|
|
@@ -467,109 +876,197 @@ async function processFileInputs(fileInputs, expectedType) {
|
|
|
467
876
|
return processed.length > 0 ? processed : undefined;
|
|
468
877
|
}
|
|
469
878
|
function hasItems(value) {
|
|
470
|
-
return Array.isArray(value) &&
|
|
879
|
+
return (Array.isArray(value) &&
|
|
880
|
+
value.some(item => typeof item === "string" && item.length > 0));
|
|
471
881
|
}
|
|
472
882
|
function itemCount(value) {
|
|
473
|
-
return Array.isArray(value)
|
|
883
|
+
return Array.isArray(value)
|
|
884
|
+
? value.filter(item => typeof item === "string" && item.length > 0).length
|
|
885
|
+
: 0;
|
|
474
886
|
}
|
|
475
887
|
function normalizeRoute(args) {
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
};
|
|
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");
|
|
484
899
|
}
|
|
485
900
|
const raw = args && typeof args === "object" ? args.route : undefined;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
throw new McpToolError("model 必须是 SA、SD 或 HA;route 只接受 S2、S5、S6(已废弃),请改用 model。", "INVALID_ENGINE");
|
|
901
|
+
if (isRetiredSaRoute(raw)) {
|
|
902
|
+
throw new McpToolError("SA 模型(sora-2)已下线,请改用 WA。", "MODEL_RETIRED");
|
|
489
903
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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 });
|
|
496
912
|
}
|
|
497
913
|
function normalizeTaskMode(route, value) {
|
|
498
914
|
const mode = typeof value === "string" ? value : getMcpDefaultTaskMode(route);
|
|
499
|
-
if (route === "S2") {
|
|
500
|
-
if (value !== undefined) {
|
|
501
|
-
throw new McpToolError("taskMode 仅适用于极睿Sd专业版或极睿Ha专业版;S2 极睿Sa专业版无需传 taskMode。", "UNSUPPORTED_FIELD", route);
|
|
502
|
-
}
|
|
503
|
-
return getMcpDefaultTaskMode(route);
|
|
504
|
-
}
|
|
505
915
|
if (!isValidMcpTaskMode(route, mode)) {
|
|
506
|
-
const allowedModes = route === "S5"
|
|
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";
|
|
507
923
|
throw new McpToolError(`${getMcpEngineName(route)} 的 taskMode 只能是 ${allowedModes}。`, "INVALID_TASK_MODE", route);
|
|
508
924
|
}
|
|
509
925
|
return mode;
|
|
510
926
|
}
|
|
511
|
-
function validateCreateVideoArgs(args) {
|
|
927
|
+
export function validateCreateVideoArgs(args) {
|
|
512
928
|
if (!args || typeof args !== "object") {
|
|
513
929
|
throw new McpToolError("请提供视频生成参数。");
|
|
514
930
|
}
|
|
515
931
|
if (!args.prompt || typeof args.prompt !== "string" || !args.prompt.trim()) {
|
|
516
932
|
throw new McpToolError("prompt 不能为空。");
|
|
517
933
|
}
|
|
518
|
-
if (args.count !== undefined &&
|
|
934
|
+
if (args.count !== undefined &&
|
|
935
|
+
(!Number.isInteger(args.count) || args.count < 1 || args.count > 12)) {
|
|
519
936
|
throw new McpToolError("count 必须是 1-12 的整数。", "INVALID_COUNT");
|
|
520
937
|
}
|
|
521
938
|
const norm = normalizeRoute(args);
|
|
522
939
|
const route = norm.route;
|
|
523
940
|
const taskMode = normalizeTaskMode(route, args.taskMode);
|
|
524
941
|
const resolution = typeof args.resolution === "string" ? args.resolution : "720p";
|
|
942
|
+
const outputFormat = typeof args.outputFormat === "string" ? args.outputFormat : "mp4";
|
|
525
943
|
const dryRun = args.dryRun === true;
|
|
526
|
-
if (
|
|
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);
|
|
961
|
+
}
|
|
962
|
+
if ((route === "S5" || route === "S6") &&
|
|
963
|
+
resolution !== "720p" &&
|
|
964
|
+
resolution !== "1080p") {
|
|
527
965
|
throw new McpToolError(`${getMcpEngineName(route)} 仅支持 720p 或 1080p。`, "INVALID_RESOLUTION", route);
|
|
528
966
|
}
|
|
529
|
-
if (route === "
|
|
530
|
-
|
|
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);
|
|
531
973
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
974
|
+
// S5/S6/S7/S8 的 seconds 白名单
|
|
975
|
+
if (route === "S5" || route === "S6" || route === "S7" || route === "S8") {
|
|
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
|
+
}
|
|
984
|
+
}
|
|
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
|
+
}
|
|
544
993
|
}
|
|
545
994
|
}
|
|
546
|
-
if (route === "S5") {
|
|
995
|
+
if (route === "S5" || route === "S8") {
|
|
996
|
+
// S8 = SD2.5 独立通道:素材上限放宽(30 图 / 10 视频 / 10 音频)。
|
|
997
|
+
const isSd25 = route === "S8";
|
|
998
|
+
const sdEngine = getMcpEngineName(route);
|
|
547
999
|
if (args.sourceImage !== undefined) {
|
|
548
|
-
throw new McpToolError(
|
|
1000
|
+
throw new McpToolError(`sourceImage 不适用于${sdEngine};请使用 sourceImages 传参考图或首尾帧。`, "UNSUPPORTED_FIELD", route);
|
|
549
1001
|
}
|
|
550
1002
|
if (args.ratio !== undefined) {
|
|
551
|
-
throw new McpToolError(
|
|
1003
|
+
throw new McpToolError(`ratio 仅适用于极睿HA专业版;${sdEngine}请使用 size 控制画面比例。`, "UNSUPPORTED_FIELD", route);
|
|
552
1004
|
}
|
|
553
1005
|
if (taskMode === "firstlast" && !hasItems(args.sourceImages)) {
|
|
554
|
-
throw new McpToolError(
|
|
1006
|
+
throw new McpToolError(`${sdEngine}首尾帧模式需要在 sourceImages 中提供至少 1 张首帧图片。`, "MISSING_SOURCE_IMAGE", route);
|
|
555
1007
|
}
|
|
556
|
-
if (taskMode === "firstlast" &&
|
|
557
|
-
|
|
1008
|
+
if (taskMode === "firstlast" &&
|
|
1009
|
+
(hasItems(args.sourceVideos) || hasItems(args.sourceAudios))) {
|
|
1010
|
+
throw new McpToolError(`sourceVideos/sourceAudios 仅适用于${sdEngine}全能参考模式;首尾帧模式只使用 sourceImages。`, "UNSUPPORTED_FIELD", route);
|
|
558
1011
|
}
|
|
559
1012
|
if (taskMode === "firstlast" && itemCount(args.sourceImages) > 2) {
|
|
560
|
-
throw new McpToolError(
|
|
1013
|
+
throw new McpToolError(`${sdEngine}首尾帧模式最多支持 2 张图片。`, "MEDIA_COUNT_EXCEEDED", route);
|
|
561
1014
|
}
|
|
562
|
-
|
|
563
|
-
|
|
1015
|
+
const maxImages = isSd25 ? 30 : 9;
|
|
1016
|
+
const maxVideos = isSd25 ? 10 : 3;
|
|
1017
|
+
const maxAudios = isSd25 ? 10 : 3;
|
|
1018
|
+
if ((taskMode === "allinone" || taskMode === "edit") &&
|
|
1019
|
+
itemCount(args.sourceImages) > maxImages) {
|
|
1020
|
+
throw new McpToolError(`${sdEngine}全能参考模式最多支持 ${maxImages} 张参考图片。`, "MEDIA_COUNT_EXCEEDED", route);
|
|
564
1021
|
}
|
|
565
|
-
if (taskMode === "allinone"
|
|
566
|
-
|
|
1022
|
+
if ((taskMode === "allinone" || taskMode === "edit") &&
|
|
1023
|
+
itemCount(args.sourceVideos) > maxVideos) {
|
|
1024
|
+
throw new McpToolError(`${sdEngine}全能参考模式最多支持 ${maxVideos} 个参考视频。`, "MEDIA_COUNT_EXCEEDED", route);
|
|
567
1025
|
}
|
|
568
|
-
if (taskMode === "allinone"
|
|
569
|
-
|
|
1026
|
+
if ((taskMode === "allinone" || taskMode === "edit") &&
|
|
1027
|
+
itemCount(args.sourceAudios) > maxAudios) {
|
|
1028
|
+
throw new McpToolError(`${sdEngine}全能参考模式最多支持 ${maxAudios} 个参考音频。`, "MEDIA_COUNT_EXCEEDED", route);
|
|
570
1029
|
}
|
|
571
|
-
|
|
572
|
-
|
|
1030
|
+
// 纯音频参考:S5 2.0 要求音频搭配图片/视频;S8(SD2.5)支持纯音频参考,豁免
|
|
1031
|
+
if (!isSd25 &&
|
|
1032
|
+
taskMode === "allinone" &&
|
|
1033
|
+
hasItems(args.sourceAudios) &&
|
|
1034
|
+
!hasItems(args.sourceImages) &&
|
|
1035
|
+
!hasItems(args.sourceVideos)) {
|
|
1036
|
+
throw new McpToolError(`${sdEngine}全能参考模式中,音频素材必须搭配至少一张参考图片或一个参考视频。`, "MISSING_VISUAL_REFERENCE", route);
|
|
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);
|
|
573
1070
|
}
|
|
574
1071
|
}
|
|
575
1072
|
if (route === "S6") {
|
|
@@ -579,28 +1076,63 @@ function validateCreateVideoArgs(args) {
|
|
|
579
1076
|
args.webSearchEnabled !== undefined ? "webSearchEnabled" : undefined,
|
|
580
1077
|
].filter(Boolean);
|
|
581
1078
|
if (unsupported.length > 0) {
|
|
582
|
-
throw new McpToolError(`这些字段仅适用于极睿
|
|
1079
|
+
throw new McpToolError(`这些字段仅适用于极睿SD专业版,不能用于极睿HA专业版:${unsupported.join("、")}。`, "UNSUPPORTED_FIELD", route);
|
|
583
1080
|
}
|
|
584
|
-
if (taskMode === "standard" &&
|
|
585
|
-
|
|
1081
|
+
if (taskMode === "standard" &&
|
|
1082
|
+
(hasItems(args.sourceImages) || hasItems(args.sourceVideos))) {
|
|
1083
|
+
throw new McpToolError("极睿HA专业版标准模式只支持 sourceImage 作为单图输入;多图请使用 reference 模式,视频请使用 edit 模式。", "UNSUPPORTED_FIELD", route);
|
|
586
1084
|
}
|
|
587
|
-
if ((taskMode === "reference" || taskMode === "edit") &&
|
|
588
|
-
|
|
1085
|
+
if ((taskMode === "reference" || taskMode === "edit") &&
|
|
1086
|
+
args.sourceImage !== undefined) {
|
|
1087
|
+
throw new McpToolError("sourceImage 仅适用于极睿HA专业版标准模式;reference/edit 模式请使用 sourceImages。", "UNSUPPORTED_FIELD", route);
|
|
589
1088
|
}
|
|
590
1089
|
if (taskMode === "reference" && !hasItems(args.sourceImages)) {
|
|
591
|
-
throw new McpToolError("极睿
|
|
1090
|
+
throw new McpToolError("极睿HA专业版图片参考模式需要在 sourceImages 中提供至少 1 张参考图片。", "MISSING_SOURCE_IMAGE", route);
|
|
592
1091
|
}
|
|
593
1092
|
if (taskMode === "edit" && !hasItems(args.sourceVideos)) {
|
|
594
|
-
throw new McpToolError("极睿
|
|
1093
|
+
throw new McpToolError("极睿HA专业版视频编辑模式需要在 sourceVideos 中提供至少 1 个参考视频。", "MISSING_SOURCE_VIDEO", route);
|
|
595
1094
|
}
|
|
596
1095
|
if (taskMode === "reference" && itemCount(args.sourceImages) > 9) {
|
|
597
|
-
throw new McpToolError("极睿
|
|
1096
|
+
throw new McpToolError("极睿HA专业版图片参考模式最多支持 9 张参考图片。", "MEDIA_COUNT_EXCEEDED", route);
|
|
598
1097
|
}
|
|
599
1098
|
if (taskMode === "edit" && itemCount(args.sourceVideos) > 1) {
|
|
600
|
-
throw new McpToolError("极睿
|
|
1099
|
+
throw new McpToolError("极睿HA专业版视频编辑模式最多支持 1 个参考视频。", "MEDIA_COUNT_EXCEEDED", route);
|
|
601
1100
|
}
|
|
602
1101
|
if (taskMode === "edit" && itemCount(args.sourceImages) > 5) {
|
|
603
|
-
throw new McpToolError("极睿
|
|
1102
|
+
throw new McpToolError("极睿HA专业版视频编辑模式最多支持 5 张参考图片。", "MEDIA_COUNT_EXCEEDED", route);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (route === "S7") {
|
|
1106
|
+
// S7 = H3(MiniMax V2):固定 2K 输出;i2va/r2va 互斥;seconds 白名单 [5,10,15](已在上方统一校验)。
|
|
1107
|
+
if (args.resolution !== undefined) {
|
|
1108
|
+
throw new McpToolError(`${getMcpEngineName(route)}固定 2K 输出,无需传 resolution。`, "UNSUPPORTED_FIELD", route);
|
|
1109
|
+
}
|
|
1110
|
+
if (args.sourceImage !== undefined) {
|
|
1111
|
+
throw new McpToolError(`sourceImage 不适用于${getMcpEngineName(route)};请使用 sourceImages 传参考图或首尾帧。`, "UNSUPPORTED_FIELD", route);
|
|
1112
|
+
}
|
|
1113
|
+
if (args.generateAudio !== undefined ||
|
|
1114
|
+
args.webSearchEnabled !== undefined) {
|
|
1115
|
+
throw new McpToolError(`generateAudio/webSearchEnabled 不适用于${getMcpEngineName(route)}。`, "UNSUPPORTED_FIELD", route);
|
|
1116
|
+
}
|
|
1117
|
+
if (taskMode === "firstlast") {
|
|
1118
|
+
if (!hasItems(args.sourceImages)) {
|
|
1119
|
+
throw new McpToolError(`${getMcpEngineName(route)}首尾帧模式需要在 sourceImages 中提供至少 1 张首帧图片。`, "MISSING_SOURCE_IMAGE", route);
|
|
1120
|
+
}
|
|
1121
|
+
if (hasItems(args.sourceVideos) || hasItems(args.sourceAudios)) {
|
|
1122
|
+
throw new McpToolError(`sourceVideos/sourceAudios 仅适用于${getMcpEngineName(route)}全能参考模式;首尾帧模式只使用 sourceImages。`, "UNSUPPORTED_FIELD", route);
|
|
1123
|
+
}
|
|
1124
|
+
if (itemCount(args.sourceImages) > 2) {
|
|
1125
|
+
throw new McpToolError(`${getMcpEngineName(route)}首尾帧模式最多支持 2 张图片(首帧 + 尾帧)。`, "MEDIA_COUNT_EXCEEDED", route);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else if (hasItems(args.sourceAudios) &&
|
|
1129
|
+
!hasItems(args.sourceImages) &&
|
|
1130
|
+
!hasItems(args.sourceVideos)) {
|
|
1131
|
+
// r2va 不允许仅音频(与网页端 / 服务端校验一致)
|
|
1132
|
+
throw new McpToolError(`${getMcpEngineName(route)}全能参考模式中,音频素材必须搭配至少一张参考图片或一个参考视频。`, "MISSING_VISUAL_REFERENCE", route);
|
|
1133
|
+
}
|
|
1134
|
+
if (itemCount(args.sourceImages) > 9) {
|
|
1135
|
+
throw new McpToolError(`${getMcpEngineName(route)}最多支持 9 张参考图片。`, "MEDIA_COUNT_EXCEEDED", route);
|
|
604
1136
|
}
|
|
605
1137
|
}
|
|
606
1138
|
return {
|
|
@@ -609,6 +1141,7 @@ function validateCreateVideoArgs(args) {
|
|
|
609
1141
|
usedRouteAlias: norm.usedRouteAlias,
|
|
610
1142
|
taskMode,
|
|
611
1143
|
resolution,
|
|
1144
|
+
outputFormat,
|
|
612
1145
|
dryRun,
|
|
613
1146
|
statusHint: getMcpVideoStatusHint(dryRun),
|
|
614
1147
|
};
|
|
@@ -618,7 +1151,11 @@ function validateCreateVideoArgs(args) {
|
|
|
618
1151
|
*/
|
|
619
1152
|
async function handleCreateVideoTask(apiBaseUrl, token, args) {
|
|
620
1153
|
const normalized = validateCreateVideoArgs(args);
|
|
621
|
-
const isProfessionalRoute = normalized.route === "S5" ||
|
|
1154
|
+
const isProfessionalRoute = normalized.route === "S5" ||
|
|
1155
|
+
normalized.route === "S6" ||
|
|
1156
|
+
normalized.route === "S7" ||
|
|
1157
|
+
normalized.route === "S8" ||
|
|
1158
|
+
normalized.route === "S9";
|
|
622
1159
|
const requestBody = {
|
|
623
1160
|
token,
|
|
624
1161
|
prompt: args.prompt,
|
|
@@ -626,7 +1163,12 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
|
|
|
626
1163
|
? (await processFileInput(args.sourceImage, "image")).data
|
|
627
1164
|
: undefined,
|
|
628
1165
|
size: args.size || "1280x720",
|
|
629
|
-
seconds: args.seconds
|
|
1166
|
+
seconds: args.seconds ??
|
|
1167
|
+
(normalized.route === "S9"
|
|
1168
|
+
? normalized.taskMode === "edit"
|
|
1169
|
+
? -1
|
|
1170
|
+
: 5
|
|
1171
|
+
: 10),
|
|
630
1172
|
language: args.language || "zh-CN",
|
|
631
1173
|
count: args.count || 1,
|
|
632
1174
|
model: normalized.model,
|
|
@@ -640,7 +1182,9 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
|
|
|
640
1182
|
requestBody.taskMode = normalized.taskMode;
|
|
641
1183
|
requestBody.isFirstLastMode = normalized.taskMode === "firstlast";
|
|
642
1184
|
requestBody.resolution = normalized.resolution;
|
|
643
|
-
requestBody.
|
|
1185
|
+
requestBody.outputFormat = normalized.outputFormat;
|
|
1186
|
+
requestBody.ratio =
|
|
1187
|
+
args.ratio || (normalized.route === "S9" ? "adaptive" : undefined);
|
|
644
1188
|
requestBody.generateAudio = args.generateAudio;
|
|
645
1189
|
requestBody.webSearchEnabled = args.webSearchEnabled;
|
|
646
1190
|
if (normalized.dryRun) {
|
|
@@ -665,7 +1209,9 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
|
|
|
665
1209
|
`- **当前积分**: ${response.remainingCredits}`,
|
|
666
1210
|
];
|
|
667
1211
|
if (typeof estimatedCredits === "number") {
|
|
668
|
-
result.splice(6, 0,
|
|
1212
|
+
result.splice(6, 0, response.creditEstimateKind === "reservation"
|
|
1213
|
+
? `- **积分预留上限**: ${response.reservationCeilingCredits ?? estimatedCredits}(完成后按输入视频及实际时长结算)`
|
|
1214
|
+
: `- **预估积分**: ${estimatedCredits}(视频完成后按成功扣费)`);
|
|
669
1215
|
}
|
|
670
1216
|
if (!dryRun && response.taskId !== undefined) {
|
|
671
1217
|
result.splice(4, 0, `- **任务 ID**: ${response.taskId}`);
|
|
@@ -692,6 +1238,13 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
|
|
|
692
1238
|
videoCount: response.videoCount,
|
|
693
1239
|
dryRun,
|
|
694
1240
|
estimatedCredits,
|
|
1241
|
+
...(response.creditEstimateKind === "reservation"
|
|
1242
|
+
? {
|
|
1243
|
+
creditEstimateKind: response.creditEstimateKind,
|
|
1244
|
+
reservationCeilingCredits: response.reservationCeilingCredits ?? estimatedCredits,
|
|
1245
|
+
settlementDescription: response.settlementDescription,
|
|
1246
|
+
}
|
|
1247
|
+
: {}),
|
|
695
1248
|
estimateSource: response.estimateSource,
|
|
696
1249
|
remainingCredits: response.remainingCredits,
|
|
697
1250
|
statusHint,
|
|
@@ -705,9 +1258,13 @@ async function handleCreateVideoTask(apiBaseUrl, token, args) {
|
|
|
705
1258
|
*/
|
|
706
1259
|
async function handleGetVideoStatus(apiBaseUrl, token, args) {
|
|
707
1260
|
const response = await callRestApi(apiBaseUrl, `/mcp/video/status/${args.taskId}`, { token }, "GET");
|
|
708
|
-
const statusText = response.status === "completed"
|
|
709
|
-
|
|
710
|
-
|
|
1261
|
+
const statusText = response.status === "completed"
|
|
1262
|
+
? "✅ 已完成"
|
|
1263
|
+
: response.status === "processing"
|
|
1264
|
+
? "🔄 处理中"
|
|
1265
|
+
: response.status === "failed"
|
|
1266
|
+
? "❌ 失败"
|
|
1267
|
+
: `⏳ ${response.status}`;
|
|
711
1268
|
let result = [
|
|
712
1269
|
`# 视频生成任务状态`,
|
|
713
1270
|
``,
|
|
@@ -716,7 +1273,9 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
|
|
|
716
1273
|
`- **进度**: ${response.progress}% (${response.completedVideos}/${response.totalVideos} 个视频)`,
|
|
717
1274
|
response.engineName ? `- **生成引擎**: ${response.engineName}` : undefined,
|
|
718
1275
|
response.taskMode ? `- **模式**: ${response.taskMode}` : undefined,
|
|
719
|
-
typeof response.estimatedCredits === "number"
|
|
1276
|
+
typeof response.estimatedCredits === "number"
|
|
1277
|
+
? `- **预估积分**: ${response.estimatedCredits}`
|
|
1278
|
+
: undefined,
|
|
720
1279
|
``,
|
|
721
1280
|
].filter((line) => line !== undefined);
|
|
722
1281
|
if (response.errorMessage) {
|
|
@@ -729,9 +1288,13 @@ async function handleGetVideoStatus(apiBaseUrl, token, args) {
|
|
|
729
1288
|
if (response.videos && response.videos.length > 0) {
|
|
730
1289
|
result.push(`## 视频列表:`);
|
|
731
1290
|
response.videos.forEach((video, index) => {
|
|
732
|
-
const videoStatus = video.status === "completed"
|
|
733
|
-
|
|
734
|
-
|
|
1291
|
+
const videoStatus = video.status === "completed"
|
|
1292
|
+
? "✅"
|
|
1293
|
+
: video.status === "processing"
|
|
1294
|
+
? "🔄"
|
|
1295
|
+
: video.status === "failed"
|
|
1296
|
+
? "❌"
|
|
1297
|
+
: "⏳";
|
|
735
1298
|
result.push(``);
|
|
736
1299
|
result.push(`${index + 1}. ${videoStatus} 视频 ID: ${video.id}`);
|
|
737
1300
|
if (video.videoUrl) {
|
|
@@ -821,10 +1384,15 @@ async function handleCreateVideoAnalysisTask(apiBaseUrl, token, args) {
|
|
|
821
1384
|
*/
|
|
822
1385
|
async function handleGetVideoAnalysisStatus(apiBaseUrl, token, args) {
|
|
823
1386
|
const response = await callRestApi(apiBaseUrl, `/mcp/video-analysis/status/${args.analysisId}`, { token }, "GET");
|
|
824
|
-
const statusText = response.status === "completed"
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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}`;
|
|
828
1396
|
let result = [
|
|
829
1397
|
`# 视频拆解任务状态`,
|
|
830
1398
|
``,
|
|
@@ -897,6 +1465,125 @@ async function handleGetVideoAnalysisStatus(apiBaseUrl, token, args) {
|
|
|
897
1465
|
},
|
|
898
1466
|
};
|
|
899
1467
|
}
|
|
1468
|
+
function creativeToolResult(output, lines) {
|
|
1469
|
+
return {
|
|
1470
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1471
|
+
structuredContent: output,
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
async function handleCreateMotionVideoRemakeTask(apiBaseUrl, token, args) {
|
|
1475
|
+
if (!args ||
|
|
1476
|
+
typeof args.sourceImage !== "string" ||
|
|
1477
|
+
typeof args.referenceVideo !== "string") {
|
|
1478
|
+
throw new McpToolError("sourceImage 和 referenceVideo 不能为空。", "MISSING_PARAMETERS");
|
|
1479
|
+
}
|
|
1480
|
+
const sourceImage = await processFileInput(args.sourceImage, "image", 5 * 1024 * 1024);
|
|
1481
|
+
const referenceVideo = await processFileInput(args.referenceVideo, "video", 100 * 1024 * 1024);
|
|
1482
|
+
const response = await callRestApi(apiBaseUrl, "/mcp/motion-remake/create", {
|
|
1483
|
+
token,
|
|
1484
|
+
sourceImage: sourceImage.data,
|
|
1485
|
+
referenceVideo: referenceVideo.data,
|
|
1486
|
+
mode: args.mode,
|
|
1487
|
+
quality: args.quality,
|
|
1488
|
+
count: args.count,
|
|
1489
|
+
referenceVideoDurationSeconds: args.referenceVideoDurationSeconds,
|
|
1490
|
+
dryRun: args.dryRun === true,
|
|
1491
|
+
}, "POST");
|
|
1492
|
+
const lines = response.dryRun
|
|
1493
|
+
? [
|
|
1494
|
+
"# 爆款视频复刻预检通过",
|
|
1495
|
+
response.estimateSource === "duration_hint"
|
|
1496
|
+
? `- 时长提示: ${response.durationHintSeconds} 秒(正式创建将使用 OSS 真实检测值)`
|
|
1497
|
+
: "- 时长提示: 未提供,正式创建后由 OSS 检测",
|
|
1498
|
+
response.estimatedCredits === null ||
|
|
1499
|
+
response.estimatedCredits === undefined
|
|
1500
|
+
? "- 预计积分: 暂不可用"
|
|
1501
|
+
: `- 预计积分: ${response.estimatedCredits}`,
|
|
1502
|
+
`- 当前余额: ${response.remainingCredits}`,
|
|
1503
|
+
"- 未上传 OSS、未创建任务、未扣费",
|
|
1504
|
+
]
|
|
1505
|
+
: [
|
|
1506
|
+
"# 爆款视频复刻任务已创建",
|
|
1507
|
+
`- 任务 ID: ${response.taskId}`,
|
|
1508
|
+
`- 模式: ${response.mode}`,
|
|
1509
|
+
`- 质量: ${response.quality}`,
|
|
1510
|
+
`- 数量: ${response.count}`,
|
|
1511
|
+
`- 已扣积分: ${response.creditsDeducted}`,
|
|
1512
|
+
`- ${response.statusHint}`,
|
|
1513
|
+
];
|
|
1514
|
+
return creativeToolResult(response, lines);
|
|
1515
|
+
}
|
|
1516
|
+
async function handleGetMotionVideoRemakeStatus(apiBaseUrl, token, args) {
|
|
1517
|
+
if (!Number.isInteger(args?.taskId)) {
|
|
1518
|
+
throw new McpToolError("taskId 必须是整数。", "BAD_REQUEST");
|
|
1519
|
+
}
|
|
1520
|
+
const response = await callRestApi(apiBaseUrl, `/mcp/motion-remake/status/${args.taskId}`, { token }, "GET");
|
|
1521
|
+
const completed = Array.isArray(response.results)
|
|
1522
|
+
? response.results.filter((item) => item.status === "completed").length
|
|
1523
|
+
: 0;
|
|
1524
|
+
return creativeToolResult(response, [
|
|
1525
|
+
"# 爆款视频复刻任务状态",
|
|
1526
|
+
`- 任务 ID: ${response.taskId}`,
|
|
1527
|
+
`- 状态: ${response.status}`,
|
|
1528
|
+
`- 进度: ${response.progress}%`,
|
|
1529
|
+
`- 已完成: ${completed}/${response.count}`,
|
|
1530
|
+
...(response.status === "completed"
|
|
1531
|
+
? ["- 视频链接已返回在 results 中"]
|
|
1532
|
+
: []),
|
|
1533
|
+
]);
|
|
1534
|
+
}
|
|
1535
|
+
async function handleCreateEcommerceStoryboardTask(apiBaseUrl, token, args) {
|
|
1536
|
+
if (!args ||
|
|
1537
|
+
typeof args.productName !== "string" ||
|
|
1538
|
+
typeof args.productDescription !== "string") {
|
|
1539
|
+
throw new McpToolError("productName 和 productDescription 不能为空。", "MISSING_PARAMETERS");
|
|
1540
|
+
}
|
|
1541
|
+
const productImage = args.productImage
|
|
1542
|
+
? (await processFileInput(args.productImage, "image", 5 * 1024 * 1024)).data
|
|
1543
|
+
: undefined;
|
|
1544
|
+
const response = await callRestApi(apiBaseUrl, "/mcp/storyboard/create", {
|
|
1545
|
+
token,
|
|
1546
|
+
productName: args.productName,
|
|
1547
|
+
productDescription: args.productDescription,
|
|
1548
|
+
targetAudience: args.targetAudience,
|
|
1549
|
+
videoStyle: args.videoStyle,
|
|
1550
|
+
productImage,
|
|
1551
|
+
targetDuration: args.targetDuration,
|
|
1552
|
+
language: args.language,
|
|
1553
|
+
orientation: args.orientation,
|
|
1554
|
+
dryRun: args.dryRun === true,
|
|
1555
|
+
}, "POST");
|
|
1556
|
+
const lines = response.dryRun
|
|
1557
|
+
? [
|
|
1558
|
+
"# 电商视频分镜预检通过",
|
|
1559
|
+
`- 预计积分: ${response.estimatedCredits}`,
|
|
1560
|
+
`- 当前余额: ${response.remainingCredits}`,
|
|
1561
|
+
"- 未上传商品图、未创建任务、未扣费",
|
|
1562
|
+
]
|
|
1563
|
+
: [
|
|
1564
|
+
"# 电商视频分镜任务已创建",
|
|
1565
|
+
`- 任务 ID: ${response.taskId}`,
|
|
1566
|
+
`- 已扣积分: ${response.creditsDeducted}`,
|
|
1567
|
+
`- ${response.statusHint}`,
|
|
1568
|
+
];
|
|
1569
|
+
return creativeToolResult(response, lines);
|
|
1570
|
+
}
|
|
1571
|
+
async function handleGetEcommerceStoryboardStatus(apiBaseUrl, token, args) {
|
|
1572
|
+
if (!Number.isInteger(args?.taskId)) {
|
|
1573
|
+
throw new McpToolError("taskId 必须是整数。", "BAD_REQUEST");
|
|
1574
|
+
}
|
|
1575
|
+
const response = await callRestApi(apiBaseUrl, `/mcp/storyboard/status/${args.taskId}`, { token }, "GET");
|
|
1576
|
+
return creativeToolResult(response, [
|
|
1577
|
+
"# 电商视频分镜任务状态",
|
|
1578
|
+
`- 任务 ID: ${response.taskId}`,
|
|
1579
|
+
`- 状态: ${response.status}`,
|
|
1580
|
+
`- 进度: ${response.progress}%`,
|
|
1581
|
+
`- 分镜数量: ${Array.isArray(response.scenes) ? response.scenes.length : 0}`,
|
|
1582
|
+
...(response.status === "completed"
|
|
1583
|
+
? ["- 完整剧情和逐镜 Prompt/插图已返回在 structuredContent 中"]
|
|
1584
|
+
: []),
|
|
1585
|
+
]);
|
|
1586
|
+
}
|
|
900
1587
|
/**
|
|
901
1588
|
* Call REST API
|
|
902
1589
|
*
|
|
@@ -908,7 +1595,9 @@ async function handleGetVideoAnalysisStatus(apiBaseUrl, token, args) {
|
|
|
908
1595
|
*/
|
|
909
1596
|
async function callRestApi(apiBaseUrl, path, params, method = "GET") {
|
|
910
1597
|
// Build URL
|
|
911
|
-
const baseUrl = apiBaseUrl.endsWith("/")
|
|
1598
|
+
const baseUrl = apiBaseUrl.endsWith("/")
|
|
1599
|
+
? apiBaseUrl.slice(0, -1)
|
|
1600
|
+
: apiBaseUrl;
|
|
912
1601
|
const fullPath = path.startsWith("/") ? path : "/" + path;
|
|
913
1602
|
let fullUrl = `${baseUrl}${fullPath}`;
|
|
914
1603
|
// For GET requests, add params as query string
|