koishi-plugin-ai-video 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/lib/index.d.ts +148 -38
  2. package/lib/index.js +274 -119
  3. package/package.json +1 -1
  4. package/readme.md +131 -214
package/lib/index.d.ts CHANGED
@@ -21,28 +21,32 @@ export declare const Config: Schema<Schemastery.ObjectS<{
21
21
  pollTimeout: Schema<number, number>;
22
22
  collectTimeout: Schema<number, number>;
23
23
  }> | Schemastery.ObjectS<{
24
- proxyEnabled: Schema<boolean, boolean>;
25
- proxyProtocol: Schema<"http" | "https", "http" | "https">;
26
- proxyHost: Schema<string, string>;
27
- proxyPort: Schema<number, number>;
28
- proxyAuth: Schema<boolean, boolean>;
29
- proxyUsername: Schema<string, string>;
30
- proxyPassword: Schema<string, string>;
31
- }> | Schemastery.ObjectS<{
32
- useCustomApi: Schema<boolean, boolean>;
33
- apiEndpoint: Schema<string, string>;
34
- apiKey: Schema<string, string>;
35
- model: Schema<string, string>;
36
- img2videoModel: Schema<string, string>;
37
- videoDuration: Schema<number, number>;
38
- videoResolution: Schema<string, string>;
39
- txt2videoPrompt: Schema<string, string>;
40
- img2videoPrompt: Schema<string, string>;
41
- customHeaders: Schema<string, string>;
24
+ imageCompression: Schema<Schemastery.ObjectS<{
25
+ enable: Schema<boolean, boolean>;
26
+ maxWidth: Schema<number, number>;
27
+ maxHeight: Schema<number, number>;
28
+ quality: Schema<number, number>;
29
+ }>, Schemastery.ObjectT<{
30
+ enable: Schema<boolean, boolean>;
31
+ maxWidth: Schema<number, number>;
32
+ maxHeight: Schema<number, number>;
33
+ quality: Schema<number, number>;
34
+ }>>;
35
+ imageInputAsBase64: Schema<boolean, boolean>;
36
+ presetPrompts: Schema<Schemastery.ObjectS<{
37
+ name: Schema<string, string>;
38
+ keywords: Schema<string[], string[]>;
39
+ template: Schema<string, string>;
40
+ }>[], Schemastery.ObjectT<{
41
+ name: Schema<string, string>;
42
+ keywords: Schema<string[], string[]>;
43
+ template: Schema<string, string>;
44
+ }>[]>;
42
45
  }> | Schemastery.ObjectS<{
43
46
  apiStrategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
44
- customApiList: Schema<Schemastery.ObjectS<{
47
+ primaryApiList: Schema<Schemastery.ObjectS<{
45
48
  enable: Schema<boolean, boolean>;
49
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
46
50
  endpoint: Schema<string, string>;
47
51
  apiKey: Schema<string, string>;
48
52
  model: Schema<string, string>;
@@ -55,6 +59,7 @@ export declare const Config: Schema<Schemastery.ObjectS<{
55
59
  bodyTemplate: Schema<string, string>;
56
60
  }>[], Schemastery.ObjectT<{
57
61
  enable: Schema<boolean, boolean>;
62
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
58
63
  endpoint: Schema<string, string>;
59
64
  apiKey: Schema<string, string>;
60
65
  model: Schema<string, string>;
@@ -66,6 +71,76 @@ export declare const Config: Schema<Schemastery.ObjectS<{
66
71
  customHeaders: Schema<string, string>;
67
72
  bodyTemplate: Schema<string, string>;
68
73
  }>[]>;
74
+ }> | Schemastery.ObjectS<{
75
+ secondaryApi: Schema<Schemastery.ObjectS<{
76
+ enable: Schema<boolean, boolean>;
77
+ strategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
78
+ list: Schema<Schemastery.ObjectS<{
79
+ enable: Schema<boolean, boolean>;
80
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
81
+ endpoint: Schema<string, string>;
82
+ apiKey: Schema<string, string>;
83
+ model: Schema<string, string>;
84
+ img2videoModel: Schema<string, string>;
85
+ videoDuration: Schema<number, number>;
86
+ videoResolution: Schema<string, string>;
87
+ txt2videoPrompt: Schema<string, string>;
88
+ img2videoPrompt: Schema<string, string>;
89
+ customHeaders: Schema<string, string>;
90
+ bodyTemplate: Schema<string, string>;
91
+ }>[], Schemastery.ObjectT<{
92
+ enable: Schema<boolean, boolean>;
93
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
94
+ endpoint: Schema<string, string>;
95
+ apiKey: Schema<string, string>;
96
+ model: Schema<string, string>;
97
+ img2videoModel: Schema<string, string>;
98
+ videoDuration: Schema<number, number>;
99
+ videoResolution: Schema<string, string>;
100
+ txt2videoPrompt: Schema<string, string>;
101
+ img2videoPrompt: Schema<string, string>;
102
+ customHeaders: Schema<string, string>;
103
+ bodyTemplate: Schema<string, string>;
104
+ }>[]>;
105
+ }>, Schemastery.ObjectT<{
106
+ enable: Schema<boolean, boolean>;
107
+ strategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
108
+ list: Schema<Schemastery.ObjectS<{
109
+ enable: Schema<boolean, boolean>;
110
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
111
+ endpoint: Schema<string, string>;
112
+ apiKey: Schema<string, string>;
113
+ model: Schema<string, string>;
114
+ img2videoModel: Schema<string, string>;
115
+ videoDuration: Schema<number, number>;
116
+ videoResolution: Schema<string, string>;
117
+ txt2videoPrompt: Schema<string, string>;
118
+ img2videoPrompt: Schema<string, string>;
119
+ customHeaders: Schema<string, string>;
120
+ bodyTemplate: Schema<string, string>;
121
+ }>[], Schemastery.ObjectT<{
122
+ enable: Schema<boolean, boolean>;
123
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
124
+ endpoint: Schema<string, string>;
125
+ apiKey: Schema<string, string>;
126
+ model: Schema<string, string>;
127
+ img2videoModel: Schema<string, string>;
128
+ videoDuration: Schema<number, number>;
129
+ videoResolution: Schema<string, string>;
130
+ txt2videoPrompt: Schema<string, string>;
131
+ img2videoPrompt: Schema<string, string>;
132
+ customHeaders: Schema<string, string>;
133
+ bodyTemplate: Schema<string, string>;
134
+ }>[]>;
135
+ }>>;
136
+ }> | Schemastery.ObjectS<{
137
+ proxyEnabled: Schema<boolean, boolean>;
138
+ proxyProtocol: Schema<"http" | "https", "http" | "https">;
139
+ proxyHost: Schema<string, string>;
140
+ proxyPort: Schema<number, number>;
141
+ proxyAuth: Schema<boolean, boolean>;
142
+ proxyUsername: Schema<string, string>;
143
+ proxyPassword: Schema<string, string>;
69
144
  }> | Schemastery.ObjectS<{
70
145
  blacklistAdmins: Schema<string[], string[]>;
71
146
  }> | Schemastery.ObjectS<{
@@ -142,28 +217,23 @@ export declare const Config: Schema<Schemastery.ObjectS<{
142
217
  pollTimeout: number;
143
218
  collectTimeout: number;
144
219
  } & import("cosmokit").Dict & {
145
- proxyEnabled: boolean;
146
- proxyProtocol: "http" | "https";
147
- proxyHost: string;
148
- proxyPort: number;
149
- proxyAuth: boolean;
150
- proxyUsername: string;
151
- proxyPassword: string;
152
- } & {
153
- useCustomApi: boolean;
154
- apiEndpoint: string;
155
- apiKey: string;
156
- model: string;
157
- img2videoModel: string;
158
- videoDuration: number;
159
- videoResolution: string;
160
- txt2videoPrompt: string;
161
- img2videoPrompt: string;
162
- customHeaders: string;
220
+ imageCompression: Schemastery.ObjectT<{
221
+ enable: Schema<boolean, boolean>;
222
+ maxWidth: Schema<number, number>;
223
+ maxHeight: Schema<number, number>;
224
+ quality: Schema<number, number>;
225
+ }>;
226
+ imageInputAsBase64: boolean;
227
+ presetPrompts: Schemastery.ObjectT<{
228
+ name: Schema<string, string>;
229
+ keywords: Schema<string[], string[]>;
230
+ template: Schema<string, string>;
231
+ }>[];
163
232
  } & {
164
233
  apiStrategy: "sequence" | "roundrobin";
165
- customApiList: Schemastery.ObjectT<{
234
+ primaryApiList: Schemastery.ObjectT<{
166
235
  enable: Schema<boolean, boolean>;
236
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
167
237
  endpoint: Schema<string, string>;
168
238
  apiKey: Schema<string, string>;
169
239
  model: Schema<string, string>;
@@ -175,6 +245,46 @@ export declare const Config: Schema<Schemastery.ObjectS<{
175
245
  customHeaders: Schema<string, string>;
176
246
  bodyTemplate: Schema<string, string>;
177
247
  }>[];
248
+ } & {
249
+ secondaryApi: Schemastery.ObjectT<{
250
+ enable: Schema<boolean, boolean>;
251
+ strategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
252
+ list: Schema<Schemastery.ObjectS<{
253
+ enable: Schema<boolean, boolean>;
254
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
255
+ endpoint: Schema<string, string>;
256
+ apiKey: Schema<string, string>;
257
+ model: Schema<string, string>;
258
+ img2videoModel: Schema<string, string>;
259
+ videoDuration: Schema<number, number>;
260
+ videoResolution: Schema<string, string>;
261
+ txt2videoPrompt: Schema<string, string>;
262
+ img2videoPrompt: Schema<string, string>;
263
+ customHeaders: Schema<string, string>;
264
+ bodyTemplate: Schema<string, string>;
265
+ }>[], Schemastery.ObjectT<{
266
+ enable: Schema<boolean, boolean>;
267
+ adapterType: Schema<"" | "chat" | "flat", "" | "chat" | "flat">;
268
+ endpoint: Schema<string, string>;
269
+ apiKey: Schema<string, string>;
270
+ model: Schema<string, string>;
271
+ img2videoModel: Schema<string, string>;
272
+ videoDuration: Schema<number, number>;
273
+ videoResolution: Schema<string, string>;
274
+ txt2videoPrompt: Schema<string, string>;
275
+ img2videoPrompt: Schema<string, string>;
276
+ customHeaders: Schema<string, string>;
277
+ bodyTemplate: Schema<string, string>;
278
+ }>[]>;
279
+ }>;
280
+ } & {
281
+ proxyEnabled: boolean;
282
+ proxyProtocol: "http" | "https";
283
+ proxyHost: string;
284
+ proxyPort: number;
285
+ proxyAuth: boolean;
286
+ proxyUsername: string;
287
+ proxyPassword: string;
178
288
  } & {
179
289
  blacklistAdmins: string[];
180
290
  } & {
package/lib/index.js CHANGED
@@ -16,6 +16,91 @@ exports.inject = {
16
16
  optional: ['assets'],
17
17
  };
18
18
  const logger = new koishi_1.Logger('ai-video');
19
+ let sharp;
20
+ try {
21
+ sharp = require('sharp');
22
+ }
23
+ catch { }
24
+ function guessAdapterType(endpoint) {
25
+ try {
26
+ const url = new URL(endpoint);
27
+ const port = url.port;
28
+ if (port === '7860' || port === '5000' || port === '8888')
29
+ return 'flat';
30
+ }
31
+ catch { }
32
+ return 'chat';
33
+ }
34
+ function compressImage(buffer, cfg) {
35
+ if (!sharp || !cfg.imageCompression?.enable)
36
+ return Promise.resolve(buffer);
37
+ const { maxWidth, maxHeight, quality } = cfg.imageCompression;
38
+ return sharp(buffer)
39
+ .resize({
40
+ width: maxWidth,
41
+ height: maxHeight,
42
+ fit: 'inside',
43
+ withoutEnlargement: true,
44
+ })
45
+ .jpeg({ quality: quality || 80 })
46
+ .toBuffer()
47
+ .catch(() => buffer);
48
+ }
49
+ async function downloadAndCompress(url, cfg) {
50
+ try {
51
+ const res = await axios_1.default.get(url, { responseType: 'arraybuffer', timeout: 30000 });
52
+ let buffer = Buffer.from(res.data);
53
+ const contentType = typeof res.headers['content-type'] === 'string' ? res.headers['content-type'] : '';
54
+ let mime = 'image/png';
55
+ if (/^image\/[a-zA-Z0-9.+-]+/.test(contentType)) {
56
+ mime = contentType.split(';')[0].trim();
57
+ }
58
+ else {
59
+ const ext = url.replace(/[?#].*$/, '').split('.').pop()?.toLowerCase() || 'png';
60
+ const mimeMap = {
61
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
62
+ gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp',
63
+ svg: 'image/svg+xml', ico: 'image/x-icon', tiff: 'image/tiff',
64
+ tif: 'image/tiff', avif: 'image/avif', heic: 'image/heic', heif: 'image/heif',
65
+ };
66
+ mime = mimeMap[ext] || 'image/png';
67
+ }
68
+ buffer = await compressImage(buffer, cfg);
69
+ return `data:${mime};base64,${buffer.toString('base64')}`;
70
+ }
71
+ catch (e) {
72
+ logger.warn('下载或压缩图片失败', url, e);
73
+ return null;
74
+ }
75
+ }
76
+ const apiEntrySchema = koishi_1.Schema.object({
77
+ enable: koishi_1.Schema.boolean().default(true).description('启用'),
78
+ adapterType: koishi_1.Schema.union([
79
+ koishi_1.Schema.const('').description('自动'),
80
+ koishi_1.Schema.const('chat').description('Chat格式'),
81
+ koishi_1.Schema.const('flat').description('平铺格式'),
82
+ ]).default('').description('接口类型(留空自动根据端口判断)'),
83
+ endpoint: koishi_1.Schema.string().default('https://apihub.agnes-ai.com/v1/videos').description('端点'),
84
+ apiKey: koishi_1.Schema.string().role('secret').default('').description('密钥'),
85
+ model: koishi_1.Schema.string().default('agnes-video-v2.0').description('模型'),
86
+ img2videoModel: koishi_1.Schema.string().default('').description('图生视频模型'),
87
+ videoDuration: koishi_1.Schema.number().default(0).description('默认视频时长(秒)'),
88
+ videoResolution: koishi_1.Schema.string().default('').description('默认视频分辨率(宽x高)'),
89
+ txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频模板'),
90
+ img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频模板'),
91
+ customHeaders: koishi_1.Schema.string().role('textarea')
92
+ .default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}')
93
+ .description('自定义请求头(JSON)'),
94
+ bodyTemplate: koishi_1.Schema.string().role('textarea')
95
+ .default(JSON.stringify({
96
+ txt2videoBody: { model: '{model}', prompt: '{prompt}', height: 768, width: 1152, num_frames: 121, frame_rate: 24 },
97
+ img2videoBody: { model: '{model}', prompt: '{prompt}', image: '{url}', height: 768, width: 1152, num_frames: 121, frame_rate: 24 },
98
+ responseVideoPath: 'remixed_from_video_id',
99
+ pollUrlTemplate: 'https://apihub.agnes-ai.com/agnesapi?video_id={task_id}',
100
+ taskIdPath: 'video_id',
101
+ }, null, 2))
102
+ .description('自定义请求体(JSON)'),
103
+ });
19
104
  exports.Config = koishi_1.Schema.intersect([
20
105
  koishi_1.Schema.object({
21
106
  debug: koishi_1.Schema.boolean().default(false).description('开启调试模式'),
@@ -37,6 +122,37 @@ exports.Config = koishi_1.Schema.intersect([
37
122
  pollTimeout: koishi_1.Schema.number().default(600000).description('轮询超时(毫秒)'),
38
123
  collectTimeout: koishi_1.Schema.number().default(120).description('收集模式超时(秒)'),
39
124
  }).description('基本设置'),
125
+ koishi_1.Schema.object({
126
+ imageCompression: koishi_1.Schema.object({
127
+ enable: koishi_1.Schema.boolean().default(false).description('启用图片压缩'),
128
+ maxWidth: koishi_1.Schema.number().default(1024).description('最大宽度'),
129
+ maxHeight: koishi_1.Schema.number().default(1024).description('最大高度'),
130
+ quality: koishi_1.Schema.number().default(80).description('压缩质量(0-100)'),
131
+ }).description('图片压缩设置'),
132
+ imageInputAsBase64: koishi_1.Schema.boolean().default(false).description('图生视频输入转为Base64'),
133
+ presetPrompts: koishi_1.Schema.array(koishi_1.Schema.object({
134
+ name: koishi_1.Schema.string().required().description('预设名称'),
135
+ keywords: koishi_1.Schema.array(String).default([]).description('触发关键词'),
136
+ template: koishi_1.Schema.string().default('{prompt}').description('提示词模板,支持 {prompt} {keyword}'),
137
+ })).default([]).description('预置提示词列表'),
138
+ }).description('扩展功能'),
139
+ koishi_1.Schema.object({
140
+ apiStrategy: koishi_1.Schema.union([
141
+ koishi_1.Schema.const('sequence').description('顺序'),
142
+ koishi_1.Schema.const('roundrobin').description('轮询'),
143
+ ]).default('roundrobin').description('调度策略'),
144
+ primaryApiList: koishi_1.Schema.array(apiEntrySchema).default([]).description('主模型API列表'),
145
+ }).description('主模型设置'),
146
+ koishi_1.Schema.object({
147
+ secondaryApi: koishi_1.Schema.object({
148
+ enable: koishi_1.Schema.boolean().default(false).description('启用副模型'),
149
+ strategy: koishi_1.Schema.union([
150
+ koishi_1.Schema.const('sequence').description('顺序'),
151
+ koishi_1.Schema.const('roundrobin').description('轮询'),
152
+ ]).default('roundrobin').description('调度策略'),
153
+ list: koishi_1.Schema.array(apiEntrySchema).default([]).description('副模型API列表(留空则使用主模型列表)'),
154
+ }).description('副模型配置(启用后 video2 使用此配置)'),
155
+ }).description('副模型设置'),
40
156
  koishi_1.Schema.object({
41
157
  proxyEnabled: koishi_1.Schema.boolean().default(false).description('启用代理'),
42
158
  proxyProtocol: koishi_1.Schema.union([koishi_1.Schema.const('http'), koishi_1.Schema.const('https')]).default('http').description('代理协议'),
@@ -46,79 +162,38 @@ exports.Config = koishi_1.Schema.intersect([
46
162
  proxyUsername: koishi_1.Schema.string().default('').description('代理用户名'),
47
163
  proxyPassword: koishi_1.Schema.string().role('secret').default('').description('代理密码'),
48
164
  }).description('代理设置'),
49
- koishi_1.Schema.object({
50
- useCustomApi: koishi_1.Schema.boolean().default(false).description('使用自定义API'),
51
- apiEndpoint: koishi_1.Schema.string().default('https://apihub.agnes-ai.com/v1/videos').description('API端点'),
52
- apiKey: koishi_1.Schema.string().role('secret').default('').description('API密钥'),
53
- model: koishi_1.Schema.string().default('agnes-video-v2.0').description('模型'),
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('默认视频分辨率(宽x高)'),
57
- txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示模板'),
58
- img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示模板'),
59
- customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头(JSON)'),
60
- }).description('内置API'),
61
- koishi_1.Schema.object({
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调用策略'),
66
- customApiList: koishi_1.Schema.array(koishi_1.Schema.object({
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('图生视频提示模板'),
76
- customHeaders: koishi_1.Schema.string().role('textarea')
77
- .default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}')
78
- .description('自定义请求头(JSON)'),
79
- bodyTemplate: koishi_1.Schema.string().role('textarea')
80
- .default(JSON.stringify({
81
- txt2videoBody: { model: '{model}', prompt: '{prompt}', height: 768, width: 1152, num_frames: 121, frame_rate: 24 },
82
- img2videoBody: { model: '{model}', prompt: '{prompt}', image: '{url}', height: 768, width: 1152, num_frames: 121, frame_rate: 24 },
83
- responseVideoPath: 'remixed_from_video_id',
84
- pollUrlTemplate: 'https://apihub.agnes-ai.com/agnesapi?video_id={task_id}',
85
- taskIdPath: 'video_id',
86
- }, null, 2))
87
- .description('自定义请求体(JSON模板)'),
88
- })).default([]).description('自定义API列表'),
89
- }).description('自定义API配置'),
90
165
  koishi_1.Schema.object({
91
166
  blacklistAdmins: koishi_1.Schema.array(String).default([]).description('管理员QQ号'),
92
167
  }).description('权限管理'),
93
168
  koishi_1.Schema.object({
94
169
  messages: koishi_1.Schema.object({
95
- generating: koishi_1.Schema.string().default('视频生成中,请耐心等待...'),
96
- enterCollect: koishi_1.Schema.string().default('已进入收集模式,请继续发送图片/文字。发送「开始」触发生成,发送「取消」退出。当前已收集: 0 张图片, 0 段文字'),
97
- collectUpdate: koishi_1.Schema.string().default('当前已收集: {images} 张图片, 文字已更新'),
98
- collectTimeout: koishi_1.Schema.string().default('收集超时,已自动退出'),
99
- empty: koishi_1.Schema.string().default('[提示] 请输入提示词或上传图片'),
100
- noApi: koishi_1.Schema.string().default('[提示] 未配置可用API'),
101
- fail: koishi_1.Schema.string().default('[提示] 视频生成失败'),
102
- noContent: koishi_1.Schema.string().default('(未返回任何视频内容)'),
103
- templateError: koishi_1.Schema.string().default('(模板配置错误)'),
104
- txt2videoDisabled: koishi_1.Schema.string().default('[提示] 文生视频功能未启用'),
105
- img2videoDisabled: koishi_1.Schema.string().default('[提示] 图生视频功能未启用'),
106
- rateLimit: koishi_1.Schema.string().default('[提示] 调用次数已达上限'),
107
- needAssets: koishi_1.Schema.string().default('[提示] 图生视频需要正确配置 assets 服务'),
108
- blacklisted: koishi_1.Schema.string().default('[提示] 你已被加入黑名单'),
109
- noPermission: koishi_1.Schema.string().default('[提示] 无权限管理黑名单'),
110
- blacklistAddSuccess: koishi_1.Schema.string().default('已将 {targets} 加入黑名单'),
111
- blacklistRemoveSuccess: koishi_1.Schema.string().default('已将 {targets} 移出黑名单'),
112
- blacklistAddFail: koishi_1.Schema.string().default('{targets} 已在黑名单或无效'),
113
- blacklistRemoveFail: koishi_1.Schema.string().default('{targets} 不在黑名单'),
114
- invalidUserId: koishi_1.Schema.string().default('无效QQ号:{targets}'),
115
- blacklistListEmpty: koishi_1.Schema.string().default('黑名单为空'),
116
- blacklistListTitle: koishi_1.Schema.string().default('当前黑名单:'),
117
- noLastTask: koishi_1.Schema.string().default('没有上一次记录,无法重绘'),
118
- redrawing: koishi_1.Schema.string().default('正在重绘上一次文生视频...'),
119
- redrawImg2Video: koishi_1.Schema.string().default('[提示] 重绘仅支持文生视频'),
120
- cancelCollect: koishi_1.Schema.string().default('已取消收集模式'),
121
- pollWaiting: koishi_1.Schema.string().default('视频生成中(异步轮询),请稍后...'),
170
+ generating: koishi_1.Schema.string().default('视频生成中,请耐心等待...').description('生成中提示'),
171
+ enterCollect: koishi_1.Schema.string().default('已进入收集模式,请继续发送图片/文字。发送「开始」触发生成,发送「取消」退出。当前已收集: 0 张图片, 0 段文字').description('进入收集模式提示'),
172
+ collectUpdate: koishi_1.Schema.string().default('当前已收集: {images} 张图片, 文字已更新').description('收集更新提示'),
173
+ collectTimeout: koishi_1.Schema.string().default('收集超时,已自动退出').description('收集超时提示'),
174
+ empty: koishi_1.Schema.string().default('[提示] 请输入提示词或上传图片').description('空输入提示'),
175
+ noApi: koishi_1.Schema.string().default('[提示] 未配置可用API').description('无API提示'),
176
+ fail: koishi_1.Schema.string().default('[提示] 视频生成失败').description('生成失败提示'),
177
+ noContent: koishi_1.Schema.string().default('(未返回任何视频内容)').description('无内容提示'),
178
+ templateError: koishi_1.Schema.string().default('(模板配置错误)').description('模板错误提示'),
179
+ txt2videoDisabled: koishi_1.Schema.string().default('[提示] 文生视频功能未启用').description('文生视频禁用提示'),
180
+ img2videoDisabled: koishi_1.Schema.string().default('[提示] 图生视频功能未启用').description('图生视频禁用提示'),
181
+ rateLimit: koishi_1.Schema.string().default('[提示] 调用次数已达上限').description('频率限制提示'),
182
+ needAssets: koishi_1.Schema.string().default('[提示] 图生视频需要正确配置 assets 服务').description('需要Assets提示'),
183
+ blacklisted: koishi_1.Schema.string().default('[提示] 你已被加入黑名单').description('黑名单提示'),
184
+ noPermission: koishi_1.Schema.string().default('[提示] 无权限管理黑名单').description('无权限提示'),
185
+ blacklistAddSuccess: koishi_1.Schema.string().default('已将 {targets} 加入黑名单').description('添加黑名单成功提示'),
186
+ blacklistRemoveSuccess: koishi_1.Schema.string().default('已将 {targets} 移出黑名单').description('移除黑名单成功提示'),
187
+ blacklistAddFail: koishi_1.Schema.string().default('{targets} 已在黑名单或无效').description('添加黑名单失败提示'),
188
+ blacklistRemoveFail: koishi_1.Schema.string().default('{targets} 不在黑名单').description('移除黑名单失败提示'),
189
+ invalidUserId: koishi_1.Schema.string().default('无效QQ号:{targets}').description('无效用户ID提示'),
190
+ blacklistListEmpty: koishi_1.Schema.string().default('黑名单为空').description('黑名单为空提示'),
191
+ blacklistListTitle: koishi_1.Schema.string().default('当前黑名单:').description('黑名单标题提示'),
192
+ noLastTask: koishi_1.Schema.string().default('没有上一次记录,无法重绘').description('无上一次任务提示'),
193
+ redrawing: koishi_1.Schema.string().default('正在重绘上一次文生视频...').description('重绘中文生视频提示'),
194
+ redrawImg2Video: koishi_1.Schema.string().default('[提示] 重绘仅支持文生视频').description('重绘图生视频提示'),
195
+ cancelCollect: koishi_1.Schema.string().default('已取消收集模式').description('取消收集提示'),
196
+ pollWaiting: koishi_1.Schema.string().default('视频生成中(异步轮询),请稍后...').description('轮询等待提示'),
122
197
  }).description('消息文本'),
123
198
  }).description('消息文本'),
124
199
  ]);
@@ -132,7 +207,8 @@ async function apply(ctx, cfg) {
132
207
  catch { }
133
208
  const collectSessions = new Map();
134
209
  const lastTaskMap = new Map();
135
- let apiRoundRobinIdx = 0;
210
+ let primaryRoundRobinIdx = 0;
211
+ let secondaryRoundRobinIdx = 0;
136
212
  const apiCallTimestamps = [];
137
213
  ctx.model.extend('ai_video_blacklist', { id: 'string', createdAt: 'date' }, { primary: 'id' });
138
214
  ctx.on('dispose', () => {
@@ -165,6 +241,13 @@ async function apply(ctx, cfg) {
165
241
  }
166
242
  catch { }
167
243
  }
244
+ let adapterType;
245
+ if (entry.adapterType && entry.adapterType !== '') {
246
+ adapterType = entry.adapterType;
247
+ }
248
+ else {
249
+ adapterType = guessAdapterType(entry.endpoint);
250
+ }
168
251
  let txt2videoBody, img2videoBody, responseVideoPath, pollUrlTemplate, taskIdPath;
169
252
  if (entry.bodyTemplate) {
170
253
  try {
@@ -200,56 +283,38 @@ async function apply(ctx, cfg) {
200
283
  img2videoPrompt: entry.img2videoPrompt || '',
201
284
  model: entry.model,
202
285
  img2videoModel: entry.img2videoModel || '',
286
+ adapterType,
203
287
  };
204
288
  }
205
- function buildBuiltinApi() {
206
- const headers = { 'Content-Type': 'application/json' };
207
- if (cfg.apiKey)
208
- headers['Authorization'] = `Bearer ${cfg.apiKey}`;
209
- if (cfg.customHeaders) {
210
- try {
211
- const custom = JSON.parse(cfg.customHeaders);
212
- for (const [k, v] of Object.entries(custom))
213
- headers[k] = typeof v === 'string' ? v.replace(/\{apiKey\}/g, cfg.apiKey || '') : String(v);
214
- }
215
- catch { }
216
- }
217
- return {
218
- endpoint: cfg.apiEndpoint || 'https://apihub.agnes-ai.com/v1/videos',
219
- headers,
220
- txt2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' },
221
- img2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' },
222
- responseVideoPath: 'video_url',
223
- pollUrlTemplate: '',
224
- taskIdPath: '',
225
- method: 'POST',
226
- videoDuration: cfg.videoDuration || 0,
227
- videoResolution: cfg.videoResolution || '',
228
- txt2videoPrompt: cfg.txt2videoPrompt || '',
229
- img2videoPrompt: cfg.img2videoPrompt || '',
230
- model: cfg.model || 'agnes-video-v2.0',
231
- img2videoModel: cfg.img2videoModel || '',
232
- };
233
- }
234
- function getApi() {
235
- if (cfg.useCustomApi) {
236
- const entries = cfg.customApiList.filter(e => e.enable);
289
+ function getApi(secondary = false) {
290
+ if (secondary && cfg.secondaryApi?.enable) {
291
+ const list = cfg.secondaryApi.list?.length ? cfg.secondaryApi.list : cfg.primaryApiList;
292
+ if (!list?.length)
293
+ return null;
294
+ const entries = list.filter(e => e.enable);
237
295
  if (!entries.length)
238
296
  return null;
239
297
  const apis = entries.map(parseApiEntry).filter(Boolean);
240
298
  if (!apis.length)
241
299
  return null;
242
- if (cfg.apiStrategy === 'sequence')
300
+ const strategy = cfg.secondaryApi.strategy || 'roundrobin';
301
+ if (strategy === 'sequence')
243
302
  return apis[0];
244
- const api = apis[apiRoundRobinIdx % apis.length];
245
- apiRoundRobinIdx++;
303
+ const api = apis[secondaryRoundRobinIdx % apis.length];
304
+ secondaryRoundRobinIdx++;
246
305
  return api;
247
306
  }
248
- else {
249
- if (!cfg.apiEndpoint && !cfg.apiKey)
250
- return null;
251
- return buildBuiltinApi();
252
- }
307
+ const entries = cfg.primaryApiList.filter(e => e.enable);
308
+ if (!entries.length)
309
+ return null;
310
+ const apis = entries.map(parseApiEntry).filter(Boolean);
311
+ if (!apis.length)
312
+ return null;
313
+ if (cfg.apiStrategy === 'sequence')
314
+ return apis[0];
315
+ const api = apis[primaryRoundRobinIdx % apis.length];
316
+ primaryRoundRobinIdx++;
317
+ return api;
253
318
  }
254
319
  function resolveTemplate(templateObj, vars) {
255
320
  if (templateObj === null || templateObj === undefined)
@@ -370,15 +435,38 @@ async function apply(ctx, cfg) {
370
435
  return obj;
371
436
  }
372
437
  }
373
- async function customGenerateVideo(session, api, prompt, imageUrl = '') {
438
+ function applyPresetPrompts(prompt, presetName) {
439
+ if (!prompt)
440
+ return prompt;
441
+ if (presetName && cfg.presetPrompts) {
442
+ const preset = cfg.presetPrompts.find(p => p.name === presetName);
443
+ if (preset) {
444
+ return preset.template.replace(/\{prompt\}/g, prompt).replace(/\{keyword\}/g, preset.keywords?.[0] || '');
445
+ }
446
+ }
447
+ if (cfg.presetPrompts && cfg.presetPrompts.length) {
448
+ for (const preset of cfg.presetPrompts) {
449
+ if (preset.keywords?.length) {
450
+ for (const kw of preset.keywords) {
451
+ if (prompt.toLowerCase().includes(kw.toLowerCase())) {
452
+ return preset.template.replace(/\{prompt\}/g, prompt).replace(/\{keyword\}/g, kw);
453
+ }
454
+ }
455
+ }
456
+ }
457
+ }
458
+ return prompt;
459
+ }
460
+ async function customGenerateVideo(session, api, prompt, imageUrl = '', presetName) {
374
461
  const isImg2Video = !!imageUrl;
375
462
  const model = isImg2Video ? (api.img2videoModel || api.model) : api.model;
376
463
  const duration = api.videoDuration || cfg.videoDuration || 5;
377
464
  const resolution = api.videoResolution || cfg.videoResolution || '1024x576';
378
465
  const promptTemplate = isImg2Video ? api.img2videoPrompt : api.txt2videoPrompt;
379
- let finalPrompt = prompt;
466
+ let processedPrompt = applyPresetPrompts(prompt, presetName);
467
+ let finalPrompt = processedPrompt;
380
468
  if (promptTemplate)
381
- finalPrompt = promptTemplate.replace('{prompt}', prompt).replace('{url}', imageUrl);
469
+ finalPrompt = promptTemplate.replace('{prompt}', processedPrompt).replace('{url}', imageUrl);
382
470
  const bodyTemplate = isImg2Video ? api.img2videoBody : api.txt2videoBody;
383
471
  const vars = {
384
472
  model,
@@ -386,8 +474,15 @@ async function apply(ctx, cfg) {
386
474
  duration,
387
475
  size: resolution,
388
476
  };
389
- if (isImg2Video)
390
- vars.url = imageUrl;
477
+ if (isImg2Video) {
478
+ if (cfg.imageInputAsBase64 && imageUrl) {
479
+ const b64 = await downloadAndCompress(imageUrl, cfg);
480
+ vars.url = b64 || imageUrl;
481
+ }
482
+ else {
483
+ vars.url = imageUrl;
484
+ }
485
+ }
391
486
  let body;
392
487
  try {
393
488
  body = resolveTemplate(bodyTemplate, vars);
@@ -429,20 +524,20 @@ async function apply(ctx, cfg) {
429
524
  return null;
430
525
  }
431
526
  }
432
- async function generateVideos(session, prompt, imageUrl, count) {
527
+ async function generateVideos(session, prompt, imageUrl, count, presetName, secondary = false) {
433
528
  const videoUrls = [];
434
529
  for (let i = 0; i < count; i++) {
435
530
  if (!checkRateLimit()) {
436
531
  await safeSend(session, cfg.messages.rateLimit);
437
532
  break;
438
533
  }
439
- const api = getApi();
534
+ const api = getApi(secondary);
440
535
  if (!api) {
441
536
  await safeSend(session, cfg.messages.noApi);
442
537
  break;
443
538
  }
444
539
  recordApiCall();
445
- const url = await customGenerateVideo(session, api, prompt, imageUrl);
540
+ const url = await customGenerateVideo(session, api, prompt, imageUrl, presetName);
446
541
  if (url)
447
542
  videoUrls.push(url);
448
543
  if (i < count - 1)
@@ -478,7 +573,62 @@ async function apply(ctx, cfg) {
478
573
  safeSend(session, cfg.messages.collectTimeout);
479
574
  }, cfg.collectTimeout * 1000);
480
575
  }
481
- ctx.command('video [text]', 'AI视频生成')
576
+ ctx.command('video [text]', 'AI视频生成(主模型)')
577
+ .option('preset', '-p <preset:string>')
578
+ .action(async ({ session, options }, text) => {
579
+ if (!session)
580
+ return;
581
+ if (await isBlacklisted(session.userId))
582
+ return safeSend(session, cfg.messages.blacklisted);
583
+ const preset = options?.preset || undefined;
584
+ const hasImage = koishi_1.h.select(session.elements, 'img').length > 0;
585
+ if (preset && text) {
586
+ if (!cfg.enableTxt2Video && !hasImage)
587
+ return safeSend(session, cfg.messages.txt2videoDisabled);
588
+ if (hasImage && !cfg.enableImg2Video)
589
+ return safeSend(session, cfg.messages.img2videoDisabled);
590
+ let imageUrl = '';
591
+ if (hasImage) {
592
+ const assets = ctx.assets;
593
+ if (!assets)
594
+ return safeSend(session, cfg.messages.needAssets);
595
+ const img = koishi_1.h.select(session.elements, 'img')[0];
596
+ try {
597
+ const up = await assets.upload(img.attrs.src, 'ref_image.jpg');
598
+ if (/^https?:\/\//.test(up))
599
+ imageUrl = up;
600
+ }
601
+ catch { }
602
+ }
603
+ await safeSend(session, cfg.messages.generating);
604
+ return generateVideos(session, text, imageUrl, cfg.maxVideos, preset, false);
605
+ }
606
+ const key = `${session.guildId || 'private'}-${session.userId}`;
607
+ if (collectSessions.has(key))
608
+ return safeSend(session, '你已在收集模式中');
609
+ if (!hasImage && !cfg.enableTxt2Video)
610
+ return safeSend(session, cfg.messages.txt2videoDisabled);
611
+ if (hasImage && !cfg.enableImg2Video)
612
+ return safeSend(session, cfg.messages.img2videoDisabled);
613
+ let imageUrl = '';
614
+ if (hasImage) {
615
+ const assets = ctx.assets;
616
+ if (!assets)
617
+ return safeSend(session, cfg.messages.needAssets);
618
+ const img = koishi_1.h.select(session.elements, 'img')[0];
619
+ try {
620
+ const up = await assets.upload(img.attrs.src, 'ref_image.jpg');
621
+ if (/^https?:\/\//.test(up))
622
+ imageUrl = up;
623
+ }
624
+ catch { }
625
+ }
626
+ const collect = { prompt: text || '', imageUrl, timer: null, presetName: preset };
627
+ collect.timer = startTimer(session, key, collect);
628
+ collectSessions.set(key, collect);
629
+ await safeSend(session, cfg.messages.enterCollect);
630
+ });
631
+ ctx.command('video2 [text]', '使用副模型生成视频')
482
632
  .action(async ({ session }, text) => {
483
633
  if (!session)
484
634
  return;
@@ -531,8 +681,13 @@ async function apply(ctx, cfg) {
531
681
  collectSessions.delete(key);
532
682
  if (!collect.prompt && !collect.imageUrl)
533
683
  return safeSend(session, cfg.messages.empty);
684
+ if (!cfg.enableTxt2Video && !collect.imageUrl)
685
+ return safeSend(session, cfg.messages.txt2videoDisabled);
686
+ if (!cfg.enableImg2Video && collect.imageUrl)
687
+ return safeSend(session, cfg.messages.img2videoDisabled);
688
+ const isVideo2 = session.content?.startsWith('video2') ?? false;
534
689
  await safeSend(session, cfg.messages.generating);
535
- return generateVideos(session, collect.prompt || '默认', collect.imageUrl, cfg.maxVideos);
690
+ return generateVideos(session, collect.prompt || '默认', collect.imageUrl, cfg.maxVideos, collect.presetName, isVideo2);
536
691
  }
537
692
  if (imgs.length > 0 && !collect.imageUrl) {
538
693
  const assets = ctx.assets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ai-video",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Koishi AI 视频生成插件,支持文生视频、图生视频",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
package/readme.md CHANGED
@@ -4,210 +4,140 @@
4
4
 
5
5
  ### 中文
6
6
  一款为 Koishi 聊天机器人框架开发的 AI 视频生成插件,支持**文生视频 + 图生视频**,**兼容 OpenAI 标准接口的同时支持自定义 API 端点**,可接入任意视频生成服务。
7
- 内置多 API 负载均衡、调试日志、超时机制、异步轮询、全配置化提示文案(含自定义提示模板),配置灵活、开箱即用、稳定可靠。
8
- 图生视频支持附带参考图片,自动切换模式。
9
- 提供**黑名单管理**功能,数据持久化到数据库,管理员可通过指令添加、移除用户及查看黑名单。
7
+ 内置多 API 负载均衡、图片压缩、Base64 自动转换、预置提示词、主/副模型分离、端口自动适配、异步轮询、调试日志、超时等待机制,配置灵活、开箱即用、稳定可靠。
8
+ 图生视频支持附带参考图片,并可选择将图片压缩后转为 Base64 发送以兼容更多后端。
9
+ 提供**黑名单管理**功能,数据持久化到数据库,管理员可通过指令添加、移除用户及查看黑名单。
10
10
 
11
11
  ### English
12
12
  An AI video generation plugin for the Koishi chatbot framework, supporting **text-to-video & image-to-video**.
13
13
  Compatible with OpenAI‑standard APIs, and also supports **custom API endpoints** for any video generation service.
14
- Built‑in multi‑API load balancing, debug logging, timeout mechanism, async polling, fully configurable messages and prompt templates.
15
- Image‑to‑video works by attaching a reference image, automatic mode switching.
16
- **Blacklist management** with database persistence.
14
+ Built‑in multi‑API load balancing, image compression, automatic Base64 conversion, preset prompts, primary/secondary model separation, port auto-detection, async polling, debug logging, and timeout mechanism.
15
+ Image‑to‑video supports attaching a reference image, with optional compression and Base64 conversion for compatibility with more backends.
16
+ **Blacklist management** with database persistence.
17
+
18
+ ## 核心指令 (Core Commands)
19
+
20
+ | 指令 (Command) | 说明 (Description) | 示例 (Example) |
21
+ |----------------|--------------------|----------------|
22
+ | `video <提示词>` | 进入收集模式:可发送文字/图片,输入「开始」生成,「取消」退出。支持 `-p <预设>` 直接应用预置提示词模板。 (Enter collection mode: send text/images, then "start" to generate. Use `-p <preset>` to apply a preset prompt template.) | `video 海上日出 -p 电影风格` |
23
+ | `video2 <提示词>` | 使用副模型进入收集模式,操作同 `video`。 (Same as `video` but using the secondary model.) | `video2 一只猫` |
24
+ | `redraw` / `rd` / `重绘` | 重绘上一次的文生视频结果。 (Redraw the last text-to-video result.) | `redraw` |
25
+ | `blacklist list` | 查看当前黑名单(仅管理员)。 (Show current blacklist, admin only.) | `blacklist list` |
26
+ | `blacklist add <QQ号...>` | 将指定 QQ 号加入黑名单(仅管理员)。 (Add QQ numbers to blacklist, admin only.) | `blacklist add 123456` |
27
+ | `blacklist remove <QQ号...>` | 将指定 QQ 号移出黑名单(仅管理员)。 (Remove QQ numbers from blacklist, admin only.) | `blacklist remove 123456` |
28
+
29
+ ## 配置项说明 (Configuration)
30
+
31
+ ### 基本设置 (Basic Settings)
32
+
33
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
34
+ |----------------|-------------|-------------------|---------------------|
35
+ | `debug` | boolean | false | 调试模式,输出完整请求/响应日志 (Debug mode, logs full request/response) |
36
+ | `timeout` | number | 600000 | API 请求超时时间(毫秒) (Request timeout in ms) |
37
+ | `rateLimit` | number | 50 | 每小时调用次数上限 (Hourly call limit) |
38
+ | `maxVideos` | number | 1 | 单次生成最多视频数量 (Max videos per generation) |
39
+ | `videoDuration` | number | 5 | 默认视频时长(秒) (Default video duration in seconds) |
40
+ | `videoResolution` | string | 1024x576 | 默认视频分辨率(宽x高) (Default video resolution WxH) |
41
+ | `enableForward` | boolean | true | 多视频结果是否使用合并转发 (Use forward message for multiple videos) |
42
+ | `enableTxt2Video` | boolean | true | 启用文生视频功能 (Enable txt2video) |
43
+ | `enableImg2Video` | boolean | true | 启用图生视频功能 (Enable img2video) |
44
+ | `videoSendMode` | string | video | 视频发送方式:`video`(仅视频) / `url`(仅链接) / `both`(视频+链接) (Send mode: `video`, `url`, `both`) |
45
+ | `pollEnabled` | boolean | false | 启用异步轮询 (Enable async polling) |
46
+ | `pollInterval` | number | 3000 | 轮询间隔(毫秒) (Polling interval in ms) |
47
+ | `pollTimeout` | number | 600000 | 轮询超时(毫秒) (Polling timeout in ms) |
48
+ | `collectTimeout` | number | 120 | 收集模式等待用户输入的超时秒数 (Collection mode wait timeout in seconds) |
49
+
50
+ ### 扩展功能 (Extended Features)
51
+
52
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
53
+ |----------------|-------------|-------------------|---------------------|
54
+ | `imageCompression.enable` | boolean | false | 启用图片压缩(需安装 `sharp` 模块) (Enable image compression, requires `sharp`) |
55
+ | `imageCompression.maxWidth` | number | 1024 | 压缩后最大宽度 (Max width after compression) |
56
+ | `imageCompression.maxHeight` | number | 1024 | 压缩后最大高度 (Max height after compression) |
57
+ | `imageCompression.quality` | number | 80 | JPEG 压缩质量 0-100 (JPEG quality 0-100) |
58
+ | `imageInputAsBase64` | boolean | false | 图生视频时将参考图片转为 Base64 发送 (Convert reference image to Base64 for img2video) |
59
+ | `presetPrompts` | array | [] | 预置提示词列表,每项包含 `name`(名称)、`keywords`(触发关键词数组)、`template`(提示词模板,支持 `{prompt}` `{keyword}` 变量) (List of preset prompts: `name`, `keywords` array, `template` with `{prompt}` and `{keyword}` variables) |
60
+
61
+ ### 主模型设置 (Primary Model)
62
+
63
+ 主模型始终使用自定义 API 列表,支持多个端点与负载均衡。
64
+ The primary model always uses a custom API list with multiple endpoints and load balancing.
65
+
66
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
67
+ |----------------|-------------|-------------------|---------------------|
68
+ | `apiStrategy` | string | roundrobin | 主模型调度策略:`sequence`(顺序) / `roundrobin`(轮询) (Load balancing strategy for primary model) |
69
+ | `primaryApiList` | array | [] | 主模型 API 条目列表,每个条目包含以下配置 (List of primary API entries, each with the following fields) |
70
+
71
+ **API 条目公共配置 (API Entry Common Fields)**(主模型与副模型通用)
72
+
73
+ | 字段 (Field) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
74
+ |--------------|-------------|-------------------|---------------------|
75
+ | `enable` | boolean | true | 是否启用此 API (Enable this API) |
76
+ | `adapterType` | string | (空) | 接口类型:`chat` / `flat`;留空则根据端口自动判断(7860/5000/8888 → flat,其余 → chat) (Adapter type; auto-detect by port if empty) |
77
+ | `endpoint` | string | https://apihub.agnes-ai.com/v1/videos | API 端点 (Endpoint) |
78
+ | `apiKey` | string | (空) | API 密钥 (API key) |
79
+ | `model` | string | agnes-video-v2.0 | 模型名称 (Model name) |
80
+ | `img2videoModel` | string | (空) | 图生视频专用模型 (Img2video model) |
81
+ | `videoDuration` | number | 0 | 视频时长(秒),0 则使用全局默认 (Video duration in seconds, 0 = use global) |
82
+ | `videoResolution` | string | (空) | 视频分辨率,留空使用全局默认 (Video resolution, falls back to global) |
83
+ | `txt2videoPrompt` | string | (空) | 文生视频提示词模板 (Prompt template for txt2video) |
84
+ | `img2videoPrompt` | string | (空) | 图生视频提示词模板 (Prompt template for img2video) |
85
+ | `customHeaders` | string | 预设 (Preset) | 自定义请求头 JSON,支持 `{apiKey}` 变量 (Custom headers JSON) |
86
+ | `bodyTemplate` | string | 预设 (Preset) | 自定义请求体 JSON,包含 `txt2videoBody`、`img2videoBody`、`responseVideoPath`、`pollUrlTemplate`、`taskIdPath` 等字段 (Custom request body template JSON) |
87
+
88
+ ### 副模型设置 (Secondary Model)
89
+
90
+ 副模型通过 `video2` 指令调用,其 API 列表完全独立于主模型;若副模型列表为空,则自动回退到主模型的 `primaryApiList`。
91
+ The secondary model is invoked via the `video2` command. Its API list is completely independent; if empty, it falls back to the primary `primaryApiList`.
92
+
93
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
94
+ |----------------|-------------|-------------------|---------------------|
95
+ | `secondaryApi.enable` | boolean | false | 启用副模型功能 (Enable secondary model) |
96
+ | `secondaryApi.strategy` | string | roundrobin | 副模型调度策略:`sequence` / `roundrobin` (Load balancing strategy for secondary model) |
97
+ | `secondaryApi.list` | array | [] | 副模型 API 条目列表,结构与主模型条目**完全相同**(留空则使用主模型列表) (Secondary API entry list, same structure as primary entries; if empty, uses primary list) |
98
+
99
+ > 副模型列表的每个条目配置项与主模型 API 条目完全一致,详见上方 **API 条目公共配置** 表格。
100
+ > Each entry in the secondary list has the same fields as a primary API entry; see the **API Entry Common Fields** table above.
101
+
102
+ ### 代理设置 (Proxy Settings)
103
+
104
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
105
+ |----------------|-------------|-------------------|---------------------|
106
+ | `proxyEnabled` | boolean | false | 启用 HTTP/HTTPS 代理 (Enable proxy) |
107
+ | `proxyProtocol` | string | http | 代理协议:`http` / `https` (Proxy protocol) |
108
+ | `proxyHost` | string | (空) | 代理地址 (Proxy host) |
109
+ | `proxyPort` | number | 8080 | 代理端口 (Proxy port) |
110
+ | `proxyAuth` | boolean | false | 代理是否需要认证 (Proxy requires auth) |
111
+ | `proxyUsername` | string | (空) | 代理用户名 (Proxy username) |
112
+ | `proxyPassword` | string | (空) | 代理密码 (Proxy password) |
113
+
114
+ ### 权限管理 (Permissions)
115
+
116
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
117
+ |----------------|-------------|-------------------|---------------------|
118
+ | `blacklistAdmins` | array | [] | 黑名单管理员 QQ 号列表 (Admin QQ number list) |
119
+
120
+ ### 消息文本 (Messages)
121
+
122
+ | 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
123
+ |----------------|-------------|-------------------|---------------------|
124
+ | `messages.*` | object | 见配置页 (See schema) | 所有提示文案均可自定义,支持模板变量 (All messages customizable, supports template variables) |
17
125
 
18
- ## 使用说明 (Usage)
19
-
20
- ### 中文
21
-
22
- | 命令 (Command) | 功能说明 (Description) |
23
- |------------------------------------|------------------------|
24
- | `video <提示词>` | **AI 视频生成**:根据提示词生成视频;若消息中同时附带了图片,则自动切换为图生视频模式。 |
25
- | `redraw` / `rd` / `重绘` | 重绘:重新生成上一次的文生视频结果 |
26
- | `blacklist list` | 查看当前黑名单(仅管理员) |
27
- | `blacklist add <QQ号> [QQ号 ...]` | 将指定 QQ 号加入黑名单(仅管理员) |
28
- | `blacklist remove <QQ号> [QQ号 ...]` | 将指定 QQ 号移出黑名单(仅管理员) |
29
-
30
- ### English
31
-
32
- | Command | Description |
33
- |-------------------------------------|-------------|
34
- | `video <prompt>` | **AI Video Generation**: Generate video from prompt. If an image is attached, automatically switches to image-to-video mode. |
35
- | `redraw` / `rd` / `重绘` | Redraw: Re-generate the last text-to-video result |
36
- | `blacklist list` | Show current blacklist (admin only) |
37
- | `blacklist add <QQ_number> [QQ_number ...]` | Add QQ number(s) to blacklist (admin only) |
38
- | `blacklist remove <QQ_number> [QQ_number ...]` | Remove QQ number(s) from blacklist (admin only) |
39
-
40
- ## 配置说明 (Configuration)
41
-
42
- ### 中文
43
-
44
- | 配置项 | 说明 | 默认值 |
45
- |--------|------|--------|
46
- | **基本设置** | | |
47
- | `debug` | 调试模式,输出完整请求/响应日志 | `false` |
48
- | `timeout` | API 请求超时时间(毫秒),视频生成较慢建议加大 | `600000` |
49
- | `rateLimit` | 每小时调用次数上限 | `50` |
50
- | `maxVideos` | 单次生成最多视频数量 | `1` |
51
- | `videoDuration` | 默认视频时长(秒) | `5` |
52
- | `videoResolution` | 默认视频分辨率(宽x高) | `1024x576` |
53
- | `enableForward` | 多视频结果是否使用合并转发 | `true` |
54
- | `enableTxt2Video` | 启用文生视频功能 | `true` |
55
- | `enableImg2Video` | 启用图生视频功能 | `true` |
56
- | `videoSendMode` | 视频发送方式:`video`(仅视频文件)、`url`(仅链接)、`both`(视频文件+链接) | `video` |
57
- | `pollEnabled` | 是否启用异步轮询(若 API 返回任务 ID 则自动轮询至完成) | `false` |
58
- | `pollInterval` | 轮询间隔(毫秒) | `3000` |
59
- | `pollTimeout` | 轮询总超时时间(毫秒) | `600000` |
60
- | **代理设置** | | |
61
- | `proxyEnabled` | 是否启用 HTTP/HTTPS 代理 | `false` |
62
- | `proxyProtocol` | 代理协议:`http` / `https` | `http` |
63
- | `proxyHost` | 代理地址 | (空) |
64
- | `proxyPort` | 代理端口 | `8080` |
65
- | `proxyAuth` | 代理是否需要认证 | `false` |
66
- | `proxyUsername` | 代理用户名 | (空) |
67
- | `proxyPassword` | 代理密码 | (空) |
68
- | **内置 API 设置** | 简单模式,使用 OpenAI 格式 | |
69
- | `useCustomApi` | 是否启用自定义 API(开启后下方自定义列表生效) | `false` |
70
- | `apiEndpoint` | API 端点地址 | `https://api.openai.com/v1/video/generations` |
71
- | `apiKey` | API 密钥 | (空) |
72
- | `model` | 模型名称 | `video-generation-model` |
73
- | `img2videoModel` | 图生视频专用模型(留空使用上方模型) | (空) |
74
- | `videoDuration` | 视频时长(秒,留空使用全局默认) | `0` |
75
- | `videoResolution` | 视频分辨率(留空使用全局默认) | (空) |
76
- | `txt2videoPrompt` | 文生视频提示词模板(留空直接使用用户输入) | (空) |
77
- | `img2videoPrompt` | 图生视频提示词模板(留空直接使用用户输入) | (空) |
78
- | `customHeaders` | 自定义请求头 JSON(合并到默认请求头) | `{}` |
79
- | **自定义 API 配置** | 高级模式,每个条目独立配置 | `[]` |
80
- | `apiStrategy` | API 调度策略:`sequence` / `roundrobin` | `roundrobin` |
81
- | `customApiList[].enable` | 是否启用此 API | `true` |
82
- | `customApiList[].endpoint` | API 端点地址 | `https://api.openai.com/v1/video/generations` |
83
- | `customApiList[].apiKey` | API 密钥 | (空) |
84
- | `customApiList[].model` | 模型名称 | `video-generation-model` |
85
- | `customApiList[].img2videoModel` | 图生视频专用模型(留空使用上方模型) | (空) |
86
- | `customApiList[].videoDuration` | 视频时长(秒,留空使用全局默认) | `0` |
87
- | `customApiList[].videoResolution` | 视频分辨率(留空使用全局默认) | (空) |
88
- | `customApiList[].txt2videoPrompt` | 文生视频提示词模板 | (空) |
89
- | `customApiList[].img2videoPrompt` | 图生视频提示词模板 | (空) |
90
- | `customApiList[].customHeaders` | 自定义请求头 JSON,支持 `{apiKey}` 变量 | 见预设 |
91
- | `customApiList[].bodyTemplate` | 自定义请求体 JSON(高级,留空使用内置) | 见预设 |
92
- | **权限管理** | | |
93
- | `blacklistAdmins` | 黑名单管理员 QQ 号列表 | `[]` |
94
- | **消息文本** | 所有提示文案均可自定义,支持模板变量 | 见配置页 |
95
-
96
- > 指令名称(`video`、`redraw`、`blacklist`)为固定注册名,可在 Koishi 的指令管理页面中单独配置别名。
97
-
98
- #### 请求范式 JSON (`bodyTemplate` 字段)
99
-
100
- `bodyTemplate` 是高级选项,用于完全自定义请求体。留空时插件使用内置模板。
101
-
102
- | 字段 | 说明 | 默认值 |
103
- |------|------|--------|
104
- | `txt2videoBody` | 文生视频请求体 JSON 模板,支持变量 `{model}` `{prompt}` `{duration}` `{size}` | 内置 |
105
- | `img2videoBody` | 图生视频请求体 JSON 模板,支持变量 `{model}` `{prompt}` `{duration}` `{size}` `{url}` | 内置 |
106
- | `responseVideoPath` | 响应 JSON 中视频 URL 的字段路径,如 `video_url` | `video_url` |
107
- | `pollUrlTemplate` | 轮询地址模板,如 `{endpoint}/{task_id}` | 无 |
108
- | `taskIdPath` | 返回的任务 ID 字段路径,如 `task_id` | 无 |
109
- | `method` | HTTP 方法 | `POST` |
110
-
111
- **变量说明**
112
- - `{model}`:模型名称(取自配置中的全局模型或专用模型)
113
- - `{prompt}`:用户输入的提示词(经过提示模板处理后的最终文本)
114
- - `{duration}`:视频时长(秒)
115
- - `{size}`:视频分辨率(宽x高)
116
- - `{url}`:图生视频时参考图片的链接(字符串)
117
- - `{apiKey}`:API 密钥,仅用于 `headers` 字段
118
-
119
- **异步轮询说明**
120
- 当 `pollEnabled` 开启且 API 返回任务 ID 时,插件会根据 `pollUrlTemplate` 和 `taskIdPath` 自动轮询任务状态,直到完成或超时。
121
-
122
- **视频 URL 自动扫描**
123
- 当 `responseVideoPath` 未命中时,插件会自动扫描响应 JSON 中**第一个包含视频扩展名(.mp4/.mov/.avi/.webm/.mkv)的 HTTP/HTTPS URL** 作为视频地址。
124
-
125
- ---
126
-
127
- ### English
126
+ ## 功能特性 (Features)
127
+ - 文生视频 / 图生视频,支持附带参考图片 (Txt2video & img2video, with reference image)
128
+ - 多视频合并转发 / 单视频发送灵活切换 (Flexible multi-video sending: forward or single)
129
+ - 主/副模型独立,均可配置多端点负载均衡(顺序 / 轮询)(Independent primary & secondary models, each with multi-endpoint load balancing)
130
+ - 预置提示词模板,关键词自动匹配 (Preset prompts with keyword auto-matching)
131
+ - 图片压缩与 Base64 输入转换,兼容更多后端 (Image compression & Base64 conversion)
132
+ - 端口自动适配(7860/5000/8888 flat 格式)(Port auto-detect for adapter type)
133
+ - 支持同步返回与异步轮询两种视频生成模式 (Supports both synchronous and async polling modes)
134
+ - 黑名单管理(持久化到数据库)(Blacklist with database persistence)
135
+ - 全配置化提示文案,调试日志,超时控制,频率限制 (Fully configurable messages, debug log, timeout, rate limit)
128
136
 
129
- | Config Item | Description | Default |
130
- |-------------|-------------|---------|
131
- | **Basic** | | |
132
- | `debug` | Debug mode, logs full request/response | `false` |
133
- | `timeout` | Request timeout (ms), video generation usually slower | `600000` |
134
- | `rateLimit` | Hourly call limit | `50` |
135
- | `maxVideos` | Max number of videos per generation | `1` |
136
- | `videoDuration` | Default video duration (seconds) | `5` |
137
- | `videoResolution` | Default video resolution (WxH) | `1024x576` |
138
- | `enableForward` | Use forward message for multiple videos | `true` |
139
- | `enableTxt2Video` | Enable text-to-video | `true` |
140
- | `enableImg2Video` | Enable image-to-video | `true` |
141
- | `videoSendMode` | Video send mode: `video`, `url`, `both` | `video` |
142
- | `pollEnabled` | Enable async polling (polls until completion if task ID returned) | `false` |
143
- | `pollInterval` | Polling interval (ms) | `3000` |
144
- | `pollTimeout` | Polling total timeout (ms) | `600000` |
145
- | **Proxy** | | |
146
- | `proxyEnabled` | Enable HTTP/HTTPS proxy | `false` |
147
- | `proxyProtocol` | Proxy protocol: `http` / `https` | `http` |
148
- | `proxyHost` | Proxy host | (empty) |
149
- | `proxyPort` | Proxy port | `8080` |
150
- | `proxyAuth` | Proxy requires authentication | `false` |
151
- | `proxyUsername` | Proxy username | (empty) |
152
- | `proxyPassword` | Proxy password | (empty) |
153
- | **Built-in API** | Simple mode, uses OpenAI format | |
154
- | `useCustomApi` | Enable custom API (below list takes effect) | `false` |
155
- | `apiEndpoint` | API endpoint URL | `https://api.openai.com/v1/video/generations` |
156
- | `apiKey` | API key | (empty) |
157
- | `model` | Model name | `video-generation-model` |
158
- | `img2videoModel` | Image-to-video model (falls back to model) | (empty) |
159
- | `videoDuration` | Video duration (seconds, 0 = use global) | `0` |
160
- | `videoResolution` | Video resolution (empty = use global) | (empty) |
161
- | `txt2videoPrompt` | Prompt template (empty = raw user input) | (empty) |
162
- | `img2videoPrompt` | Prompt template (empty = raw user input) | (empty) |
163
- | `customHeaders` | Custom headers JSON (merged into default) | `{}` |
164
- | **Custom API Config** | Advanced mode, per-entry configuration | `[]` |
165
- | `apiStrategy` | API strategy: `sequence` / `roundrobin` | `roundrobin` |
166
- | `customApiList[].enable` | Enable this API | `true` |
167
- | `customApiList[].endpoint` | API endpoint URL | `https://api.openai.com/v1/video/generations` |
168
- | `customApiList[].apiKey` | API key | (empty) |
169
- | `customApiList[].model` | Model name | `video-generation-model` |
170
- | `customApiList[].img2videoModel` | Image-to-video model (falls back to model) | (empty) |
171
- | `customApiList[].videoDuration` | Video duration (seconds, 0 = use global) | `0` |
172
- | `customApiList[].videoResolution` | Video resolution (empty = use global) | (empty) |
173
- | `customApiList[].txt2videoPrompt` | Prompt template | (empty) |
174
- | `customApiList[].img2videoPrompt` | Prompt template | (empty) |
175
- | `customApiList[].customHeaders` | Custom headers JSON, supports `{apiKey}` | preset |
176
- | `customApiList[].bodyTemplate` | Custom request body JSON (advanced) | preset |
177
- | **Permissions** | | |
178
- | `blacklistAdmins` | Admin QQ number list | `[]` |
179
- | **Messages** | All messages customizable with template vars | see schema |
180
-
181
- > Command names (`video`, `redraw`, `blacklist`) are fixed; aliases can be configured via Koishi's command management page.
182
-
183
- #### Request Template JSON (`bodyTemplate` field)
184
-
185
- `bodyTemplate` is the advanced option for full custom request bodies. Leave empty to use built-in templates.
186
-
187
- | Field | Description | Default |
188
- |-------|-------------|---------|
189
- | `txt2videoBody` | Txt2video request body template, vars `{model}` `{prompt}` `{duration}` `{size}` | built-in |
190
- | `img2videoBody` | Img2video request body template, vars `{model}` `{prompt}` `{duration}` `{size}` `{url}` | built-in |
191
- | `responseVideoPath` | JSON path to video URL, e.g. `video_url` | `video_url` |
192
- | `pollUrlTemplate` | Polling URL template, e.g. `{endpoint}/{task_id}` | none |
193
- | `taskIdPath` | JSON path to task ID, e.g. `task_id` | none |
194
- | `method` | HTTP method | `POST` |
195
-
196
- **Variable placeholders**
197
- - `{model}` — Model name
198
- - `{prompt}` — Processed user prompt after template expansion
199
- - `{duration}` — Video duration in seconds
200
- - `{size}` — Video resolution (WxH)
201
- - `{url}` — Reference image URL for image-to-video
202
- - `{apiKey}` — API key, used only in `headers`
203
-
204
- **Async Polling**
205
- When `pollEnabled` is on and the API returns a task ID, the plugin will automatically poll using `pollUrlTemplate` and `taskIdPath` until completion or timeout.
206
-
207
- **Auto Video URL Scanning**
208
- When `responseVideoPath` fails, the plugin scans the response JSON for the **first HTTP/HTTPS URL with a video extension (.mp4/.mov/.avi/.webm/.mkv)** as the video address.
209
-
210
- ---
137
+ ## 依赖 (Dependencies)
138
+ - **数据库 (database)**:必须启用,用于黑名单持久化。 (Required for blacklist persistence)
139
+ - **图生视频 (img2video)**:需要 `assets` 服务及正确的 `selfUrl` 配置。 (Requires `assets` service and correct `selfUrl`)
140
+ - **图片压缩 (optional)**:需要安装 `sharp` 模块。 (Optional: `sharp` module for compression)
211
141
 
212
142
  ## 自定义 API 端点示例 (Custom Endpoint Examples)
213
143
 
@@ -221,7 +151,6 @@ When `responseVideoPath` fails, the plugin scans the response JSON for the **fir
221
151
  "img2videoBody": "{\"model\":\"{model}\",\"prompt\":\"{prompt}\",\"duration\":{duration},\"size\":\"{size}\",\"image_url\":\"{url}\"}",
222
152
  "responseVideoPath": "video_url"
223
153
  }
224
- ```
225
154
 
226
155
  ### 异步轮询 API(返回任务 ID)
227
156
  ```json
@@ -237,25 +166,13 @@ When `responseVideoPath` fails, the plugin scans the response JSON for the **fir
237
166
  }
238
167
  ```
239
168
 
240
- ## 依赖 (Dependencies)
241
- - **数据库 (database)**:必须启用,用于黑名单持久化。
242
- - **图生视频 (img2video)**:需要 `assets` 服务及正确的 `selfUrl` 配置。
243
-
244
- ## 功能特性 (Features)
245
- - 文生视频 / 图生视频(自动识别消息中是否附带图片)
246
- - 多 API 轮询与负载均衡(sequence / roundrobin)
247
- - 支持同步返回与异步轮询两种模式,适配不同视频生成服务
248
- - 灵活的请求体模板系统,变量自动替换,一套配置兼容任意平台
249
- - 视频结果支持文件发送和链接发送
250
- - 黑名单管理(持久化到数据库)
251
- - 全配置化提示文案,支持模板变量
252
- - 调试日志、超时控制、频率限制
253
-
254
169
  ## 项目贡献者 (Contributors)
255
170
  | 贡献者 (Contributor) | 贡献内容 (Contribution) |
256
171
  | --- | --- |
257
- | Minecraft-1314 | 插件完整开发(从绘图插件改造) |
172
+ | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
173
+ | xiaochuqiangda | 参考了 koishi-plugin-imagedraw-selfuse 插件的功能 (This feature references the koishi-plugin-imagedraw-selfuse plugin) |
258
174
  | (欢迎提交 PR / Issues 加入贡献者列表) |
175
+ | (Welcome to join the contributor list via PR or Issues) |
259
176
 
260
177
  ## 许可协议 (License)
261
178
  本项目采用 MIT 许可证,详情参见 LICENSE 文件。