koishi-plugin-ai-video 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/index.js +117 -63
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -39,12 +39,12 @@ exports.Config = koishi_1.Schema.intersect([
39
39
  }).description('基本设置'),
40
40
  koishi_1.Schema.object({
41
41
  proxyEnabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
42
- proxyProtocol: koishi_1.Schema.union([koishi_1.Schema.const('http'), koishi_1.Schema.const('https')]).default('http'),
43
- proxyHost: koishi_1.Schema.string().default(''),
44
- proxyPort: koishi_1.Schema.number().default(8080),
45
- proxyAuth: koishi_1.Schema.boolean().default(false),
46
- proxyUsername: koishi_1.Schema.string().default(''),
47
- proxyPassword: koishi_1.Schema.string().role('secret').default(''),
42
+ proxyProtocol: koishi_1.Schema.union([koishi_1.Schema.const('http'), koishi_1.Schema.const('https')]).default('http').description('代理协议'),
43
+ proxyHost: koishi_1.Schema.string().default('').description('代理主机地址'),
44
+ proxyPort: koishi_1.Schema.number().default(8080).description('代理端口'),
45
+ proxyAuth: koishi_1.Schema.boolean().default(false).description('启用代理认证'),
46
+ proxyUsername: koishi_1.Schema.string().default('').description('代理用户名'),
47
+ proxyPassword: koishi_1.Schema.string().role('secret').default('').description('代理密码'),
48
48
  }).description('代理设置'),
49
49
  koishi_1.Schema.object({
50
50
  useCustomApi: koishi_1.Schema.boolean().default(false).description('使用自定义API'),
@@ -52,26 +52,30 @@ exports.Config = koishi_1.Schema.intersect([
52
52
  apiKey: koishi_1.Schema.string().role('secret').default('').description('API密钥'),
53
53
  model: koishi_1.Schema.string().default('agnes-video-v2.0').description('模型'),
54
54
  img2videoModel: koishi_1.Schema.string().default('').description('图生视频模型'),
55
- videoDuration: koishi_1.Schema.number().default(0).description('时长'),
56
- videoResolution: koishi_1.Schema.string().default('').description('分辨率'),
55
+ videoDuration: koishi_1.Schema.number().default(0).description('默认视频时长(秒)'),
56
+ videoResolution: koishi_1.Schema.string().default('').description('默认视频分辨率(宽x高)'),
57
57
  txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示模板'),
58
58
  img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示模板'),
59
- customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头'),
59
+ customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头(JSON)'),
60
60
  }).description('内置API'),
61
61
  koishi_1.Schema.object({
62
- apiStrategy: koishi_1.Schema.union([koishi_1.Schema.const('sequence'), koishi_1.Schema.const('roundrobin')]).default('roundrobin'),
62
+ apiStrategy: koishi_1.Schema.union([
63
+ koishi_1.Schema.const('sequence').description('顺序使用'),
64
+ koishi_1.Schema.const('roundrobin').description('轮询使用')
65
+ ]).default('roundrobin').description('API调用策略'),
63
66
  customApiList: koishi_1.Schema.array(koishi_1.Schema.object({
64
- enable: koishi_1.Schema.boolean().default(true),
65
- endpoint: koishi_1.Schema.string().default('https://apihub.agnes-ai.com/v1/videos'),
66
- apiKey: koishi_1.Schema.string().role('secret').default(''),
67
- model: koishi_1.Schema.string().default('agnes-video-v2.0'),
68
- img2videoModel: koishi_1.Schema.string().default(''),
69
- videoDuration: koishi_1.Schema.number().default(0),
70
- videoResolution: koishi_1.Schema.string().default(''),
71
- txt2videoPrompt: koishi_1.Schema.string().default(''),
72
- img2videoPrompt: koishi_1.Schema.string().default(''),
67
+ enable: koishi_1.Schema.boolean().default(true).description('启用'),
68
+ endpoint: koishi_1.Schema.string().default('https://apihub.agnes-ai.com/v1/videos').description('API端点'),
69
+ apiKey: koishi_1.Schema.string().role('secret').default('').description('API密钥'),
70
+ model: koishi_1.Schema.string().default('agnes-video-v2.0').description('模型'),
71
+ img2videoModel: koishi_1.Schema.string().default('').description('图生视频模型'),
72
+ videoDuration: koishi_1.Schema.number().default(0).description('默认视频时长(秒)'),
73
+ videoResolution: koishi_1.Schema.string().default('').description('默认视频分辨率(宽x高)'),
74
+ txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示模板'),
75
+ img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示模板'),
73
76
  customHeaders: koishi_1.Schema.string().role('textarea')
74
- .default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}'),
77
+ .default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}')
78
+ .description('自定义请求头(JSON)'),
75
79
  bodyTemplate: koishi_1.Schema.string().role('textarea')
76
80
  .default(JSON.stringify({
77
81
  txt2videoBody: { model: '{model}', prompt: '{prompt}', height: 768, width: 1152, num_frames: 121, frame_rate: 24 },
@@ -80,11 +84,11 @@ exports.Config = koishi_1.Schema.intersect([
80
84
  pollUrlTemplate: 'https://apihub.agnes-ai.com/agnesapi?video_id={task_id}',
81
85
  taskIdPath: 'video_id',
82
86
  }, null, 2))
83
- .description('自定义请求体'),
87
+ .description('自定义请求体(JSON模板)'),
84
88
  })).default([]).description('自定义API列表'),
85
89
  }).description('自定义API配置'),
86
90
  koishi_1.Schema.object({
87
- blacklistAdmins: koishi_1.Schema.array(String).default([]).description('管理员QQ'),
91
+ blacklistAdmins: koishi_1.Schema.array(String).default([]).description('管理员QQ'),
88
92
  }).description('权限管理'),
89
93
  koishi_1.Schema.object({
90
94
  messages: koishi_1.Schema.object({
@@ -147,8 +151,6 @@ async function apply(ctx, cfg) {
147
151
  return apiCallTimestamps.length + 1 <= cfg.rateLimit;
148
152
  }
149
153
  function recordApiCall() { apiCallTimestamps.push(Date.now()); }
150
- const BUILTIN_TXT2VIDEO = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' };
151
- const BUILTIN_IMG2VIDEO = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' };
152
154
  function parseApiEntry(entry) {
153
155
  if (!entry.endpoint)
154
156
  return null;
@@ -167,8 +169,8 @@ async function apply(ctx, cfg) {
167
169
  if (entry.bodyTemplate) {
168
170
  try {
169
171
  const tmpl = JSON.parse(entry.bodyTemplate);
170
- txt2videoBody = tmpl.txt2videoBody || BUILTIN_TXT2VIDEO;
171
- img2videoBody = tmpl.img2videoBody || BUILTIN_IMG2VIDEO;
172
+ txt2videoBody = tmpl.txt2videoBody || { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' };
173
+ img2videoBody = tmpl.img2videoBody || { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' };
172
174
  responseVideoPath = tmpl.responseVideoPath || 'video_url';
173
175
  pollUrlTemplate = tmpl.pollUrlTemplate || '';
174
176
  taskIdPath = tmpl.taskIdPath || '';
@@ -178,8 +180,8 @@ async function apply(ctx, cfg) {
178
180
  }
179
181
  }
180
182
  else {
181
- txt2videoBody = BUILTIN_TXT2VIDEO;
182
- img2videoBody = BUILTIN_IMG2VIDEO;
183
+ txt2videoBody = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' };
184
+ img2videoBody = { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' };
183
185
  responseVideoPath = 'video_url';
184
186
  pollUrlTemplate = '';
185
187
  taskIdPath = '';
@@ -215,8 +217,8 @@ async function apply(ctx, cfg) {
215
217
  return {
216
218
  endpoint: cfg.apiEndpoint || 'https://apihub.agnes-ai.com/v1/videos',
217
219
  headers,
218
- txt2videoBody: BUILTIN_TXT2VIDEO,
219
- img2videoBody: BUILTIN_IMG2VIDEO,
220
+ txt2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' },
221
+ img2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' },
220
222
  responseVideoPath: 'video_url',
221
223
  pollUrlTemplate: '',
222
224
  taskIdPath: '',
@@ -249,15 +251,27 @@ async function apply(ctx, cfg) {
249
251
  return buildBuiltinApi();
250
252
  }
251
253
  }
252
- function resolveTemplate(template, vars) {
253
- const jsonStr = JSON.stringify(template);
254
- let processed = jsonStr;
255
- for (const [key, value] of Object.entries(vars)) {
256
- if (value === undefined || value === null)
257
- continue;
258
- processed = processed.replace(new RegExp(`\\{${key}\\}`, 'g'), JSON.stringify(String(value)).slice(1, -1));
259
- }
260
- return JSON.parse(processed);
254
+ function resolveTemplate(templateObj, vars) {
255
+ if (templateObj === null || templateObj === undefined)
256
+ return templateObj;
257
+ if (typeof templateObj === 'string') {
258
+ const match = templateObj.match(/^\{(\w+)\}$/);
259
+ if (match && vars.hasOwnProperty(match[1])) {
260
+ return vars[match[1]];
261
+ }
262
+ return templateObj;
263
+ }
264
+ if (Array.isArray(templateObj)) {
265
+ return templateObj.map(item => resolveTemplate(item, vars));
266
+ }
267
+ if (typeof templateObj === 'object') {
268
+ const result = {};
269
+ for (const key of Object.keys(templateObj)) {
270
+ result[key] = resolveTemplate(templateObj[key], vars);
271
+ }
272
+ return result;
273
+ }
274
+ return templateObj;
261
275
  }
262
276
  function getValueByPath(obj, path) {
263
277
  if (!obj || !path)
@@ -333,7 +347,7 @@ async function apply(ctx, cfg) {
333
347
  logger.error('发送失败', e);
334
348
  }
335
349
  }
336
- async function sendVideo(session, url) {
350
+ async function sendSingleVideo(session, url) {
337
351
  const mode = cfg.videoSendMode;
338
352
  if (mode === 'url')
339
353
  await safeSend(session, url);
@@ -366,7 +380,12 @@ async function apply(ctx, cfg) {
366
380
  if (promptTemplate)
367
381
  finalPrompt = promptTemplate.replace('{prompt}', prompt).replace('{url}', imageUrl);
368
382
  const bodyTemplate = isImg2Video ? api.img2videoBody : api.txt2videoBody;
369
- const vars = { model, prompt: finalPrompt, duration: String(duration), size: resolution };
383
+ const vars = {
384
+ model,
385
+ prompt: finalPrompt,
386
+ duration,
387
+ size: resolution,
388
+ };
370
389
  if (isImg2Video)
371
390
  vars.url = imageUrl;
372
391
  let body;
@@ -374,10 +393,13 @@ async function apply(ctx, cfg) {
374
393
  body = resolveTemplate(bodyTemplate, vars);
375
394
  }
376
395
  catch {
377
- return safeSend(session, cfg.messages.fail + cfg.messages.templateError);
396
+ await safeSend(session, cfg.messages.fail + cfg.messages.templateError);
397
+ return null;
398
+ }
399
+ if (!validateEndpoint(api.endpoint)) {
400
+ await safeSend(session, cfg.messages.fail + '(端点无效)');
401
+ return null;
378
402
  }
379
- if (!validateEndpoint(api.endpoint))
380
- return safeSend(session, cfg.messages.fail + '(端点无效)');
381
403
  const sensitive = api.headers?.Authorization?.split(' ')[1] || '';
382
404
  if (debug)
383
405
  logger.info('API请求', JSON.stringify(sanitizeForLog(body, sensitive)));
@@ -393,32 +415,62 @@ async function apply(ctx, cfg) {
393
415
  if (cfg.pollEnabled && api.taskIdPath) {
394
416
  const taskId = getValueByPath(res, api.taskIdPath);
395
417
  if (taskId) {
396
- const pollUrl = api.pollUrlTemplate.replace('{endpoint}', api.endpoint).replace('{task_id}', taskId);
397
- safeSend(session, cfg.messages.pollWaiting);
418
+ const pollUrl = api.pollUrlTemplate.replace('{task_id}', taskId);
419
+ await safeSend(session, cfg.messages.pollWaiting);
398
420
  res = await pollForResult(pollUrl, api.headers, cfg.pollInterval, cfg.pollTimeout);
399
421
  }
400
422
  }
401
423
  const videoUrl = getValueByPath(res, api.responseVideoPath) || findVideoUrl(res);
402
- if (videoUrl)
403
- await sendVideo(session, videoUrl);
404
- else
405
- await safeSend(session, cfg.messages.fail + cfg.messages.noContent);
406
- const userId = `${session.guildId || 'private'}-${session.userId}`;
407
- lastTaskMap.set(userId, { prompt, imageUrl, isImg2Video, model });
424
+ return videoUrl || null;
408
425
  }
409
426
  catch (err) {
410
427
  logger.error('视频生成失败', err);
411
- safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
428
+ await safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
429
+ return null;
412
430
  }
413
431
  }
414
- async function generateVideo(session, prompt, imageUrl = '') {
415
- if (!checkRateLimit())
416
- return safeSend(session, cfg.messages.rateLimit);
417
- const api = getApi();
418
- if (!api)
419
- return safeSend(session, cfg.messages.noApi);
420
- recordApiCall();
421
- return customGenerateVideo(session, api, prompt, imageUrl);
432
+ async function generateVideos(session, prompt, imageUrl, count) {
433
+ const videoUrls = [];
434
+ for (let i = 0; i < count; i++) {
435
+ if (!checkRateLimit()) {
436
+ await safeSend(session, cfg.messages.rateLimit);
437
+ break;
438
+ }
439
+ const api = getApi();
440
+ if (!api) {
441
+ await safeSend(session, cfg.messages.noApi);
442
+ break;
443
+ }
444
+ recordApiCall();
445
+ const url = await customGenerateVideo(session, api, prompt, imageUrl);
446
+ if (url)
447
+ videoUrls.push(url);
448
+ if (i < count - 1)
449
+ await new Promise(r => setTimeout(r, 1000));
450
+ }
451
+ if (videoUrls.length === 0) {
452
+ await safeSend(session, cfg.messages.fail + cfg.messages.noContent);
453
+ return;
454
+ }
455
+ if (videoUrls.length === 1 || !cfg.enableForward) {
456
+ for (const url of videoUrls) {
457
+ await sendSingleVideo(session, url);
458
+ }
459
+ }
460
+ else {
461
+ const children = videoUrls.map(url => (0, koishi_1.h)('message', koishi_1.h.video(url)));
462
+ try {
463
+ await safeSend(session, (0, koishi_1.h)('message', { forward: true }, ...children));
464
+ }
465
+ catch {
466
+ for (const url of videoUrls)
467
+ await sendSingleVideo(session, url);
468
+ }
469
+ }
470
+ const userId = `${session.guildId || 'private'}-${session.userId}`;
471
+ if (videoUrls.length > 0) {
472
+ lastTaskMap.set(userId, { prompt, imageUrl, isImg2Video: !!imageUrl, model: 'multiple' });
473
+ }
422
474
  }
423
475
  function startTimer(session, key, collect) {
424
476
  return setTimeout(() => {
@@ -459,6 +511,8 @@ async function apply(ctx, cfg) {
459
511
  await safeSend(session, cfg.messages.enterCollect);
460
512
  });
461
513
  ctx.on('message', async (session) => {
514
+ if (session.command)
515
+ return;
462
516
  if (!session || !session.elements)
463
517
  return;
464
518
  const key = `${session.guildId || 'private'}-${session.userId}`;
@@ -478,7 +532,7 @@ async function apply(ctx, cfg) {
478
532
  if (!collect.prompt && !collect.imageUrl)
479
533
  return safeSend(session, cfg.messages.empty);
480
534
  await safeSend(session, cfg.messages.generating);
481
- return generateVideo(session, collect.prompt || '默认', collect.imageUrl);
535
+ return generateVideos(session, collect.prompt || '默认', collect.imageUrl, cfg.maxVideos);
482
536
  }
483
537
  if (imgs.length > 0 && !collect.imageUrl) {
484
538
  const assets = ctx.assets;
@@ -515,7 +569,7 @@ async function apply(ctx, cfg) {
515
569
  if (last.isImg2Video)
516
570
  return safeSend(session, cfg.messages.redrawImg2Video);
517
571
  await safeSend(session, cfg.messages.redrawing);
518
- return generateVideo(session, last.prompt);
572
+ return generateVideos(session, last.prompt, '', cfg.maxVideos);
519
573
  });
520
574
  function isValidQQ(id) { return /^\d{5,11}$/.test(id); }
521
575
  async function isBlacklisted(userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ai-video",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Koishi AI 视频生成插件,支持文生视频、图生视频",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",