koishi-plugin-ai-video 0.0.1 → 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 (3) hide show
  1. package/lib/index.d.ts +17 -0
  2. package/lib/index.js +390 -427
  3. package/package.json +1 -1
package/lib/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const Config: Schema<Schemastery.ObjectS<{
19
19
  pollEnabled: Schema<boolean, boolean>;
20
20
  pollInterval: Schema<number, number>;
21
21
  pollTimeout: Schema<number, number>;
22
+ collectTimeout: Schema<number, number>;
22
23
  }> | Schemastery.ObjectS<{
23
24
  proxyEnabled: Schema<boolean, boolean>;
24
25
  proxyProtocol: Schema<"http" | "https", "http" | "https">;
@@ -70,6 +71,9 @@ export declare const Config: Schema<Schemastery.ObjectS<{
70
71
  }> | Schemastery.ObjectS<{
71
72
  messages: Schema<Schemastery.ObjectS<{
72
73
  generating: Schema<string, string>;
74
+ enterCollect: Schema<string, string>;
75
+ collectUpdate: Schema<string, string>;
76
+ collectTimeout: Schema<string, string>;
73
77
  empty: Schema<string, string>;
74
78
  noApi: Schema<string, string>;
75
79
  fail: Schema<string, string>;
@@ -91,8 +95,13 @@ export declare const Config: Schema<Schemastery.ObjectS<{
91
95
  noLastTask: Schema<string, string>;
92
96
  redrawing: Schema<string, string>;
93
97
  redrawImg2Video: Schema<string, string>;
98
+ cancelCollect: Schema<string, string>;
99
+ pollWaiting: Schema<string, string>;
94
100
  }>, Schemastery.ObjectT<{
95
101
  generating: Schema<string, string>;
102
+ enterCollect: Schema<string, string>;
103
+ collectUpdate: Schema<string, string>;
104
+ collectTimeout: Schema<string, string>;
96
105
  empty: Schema<string, string>;
97
106
  noApi: Schema<string, string>;
98
107
  fail: Schema<string, string>;
@@ -114,6 +123,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
114
123
  noLastTask: Schema<string, string>;
115
124
  redrawing: Schema<string, string>;
116
125
  redrawImg2Video: Schema<string, string>;
126
+ cancelCollect: Schema<string, string>;
127
+ pollWaiting: Schema<string, string>;
117
128
  }>>;
118
129
  }>, {
119
130
  debug: boolean;
@@ -129,6 +140,7 @@ export declare const Config: Schema<Schemastery.ObjectS<{
129
140
  pollEnabled: boolean;
130
141
  pollInterval: number;
131
142
  pollTimeout: number;
143
+ collectTimeout: number;
132
144
  } & import("cosmokit").Dict & {
133
145
  proxyEnabled: boolean;
134
146
  proxyProtocol: "http" | "https";
@@ -168,6 +180,9 @@ export declare const Config: Schema<Schemastery.ObjectS<{
168
180
  } & {
169
181
  messages: Schemastery.ObjectT<{
170
182
  generating: Schema<string, string>;
183
+ enterCollect: Schema<string, string>;
184
+ collectUpdate: Schema<string, string>;
185
+ collectTimeout: Schema<string, string>;
171
186
  empty: Schema<string, string>;
172
187
  noApi: Schema<string, string>;
173
188
  fail: Schema<string, string>;
@@ -189,6 +204,8 @@ export declare const Config: Schema<Schemastery.ObjectS<{
189
204
  noLastTask: Schema<string, string>;
190
205
  redrawing: Schema<string, string>;
191
206
  redrawImg2Video: Schema<string, string>;
207
+ cancelCollect: Schema<string, string>;
208
+ pollWaiting: Schema<string, string>;
192
209
  }>;
193
210
  }>;
194
211
  declare module 'koishi' {
package/lib/index.js CHANGED
@@ -18,181 +18,159 @@ exports.inject = {
18
18
  const logger = new koishi_1.Logger('ai-video');
19
19
  exports.Config = koishi_1.Schema.intersect([
20
20
  koishi_1.Schema.object({
21
- debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,输出完整请求日志'),
22
- timeout: koishi_1.Schema.number().default(600000).description('接口请求超时时间(毫秒),视频生成较慢,建议加大'),
23
- rateLimit: koishi_1.Schema.number().default(50).description('每小时调用次数限制'),
21
+ debug: koishi_1.Schema.boolean().default(false).description('开启调试模式'),
22
+ timeout: koishi_1.Schema.number().default(600000).description('超时时间(毫秒)'),
23
+ rateLimit: koishi_1.Schema.number().default(50).description('每小时调用次数上限'),
24
24
  maxVideos: koishi_1.Schema.number().default(1).description('单次生成最多视频数量'),
25
25
  videoDuration: koishi_1.Schema.number().default(5).description('默认视频时长(秒)'),
26
26
  videoResolution: koishi_1.Schema.string().default('1024x576').description('默认视频分辨率(宽x高)'),
27
- enableForward: koishi_1.Schema.boolean().default(true).description('多视频结果是否使用合并转发'),
28
- enableTxt2Video: koishi_1.Schema.boolean().default(true).description('启用文生视频功能'),
29
- enableImg2Video: koishi_1.Schema.boolean().default(true).description('启用图生视频功能'),
27
+ enableForward: koishi_1.Schema.boolean().default(true).description('多视频合并转发'),
28
+ enableTxt2Video: koishi_1.Schema.boolean().default(true).description('启用文生视频'),
29
+ enableImg2Video: koishi_1.Schema.boolean().default(true).description('启用图生视频'),
30
30
  videoSendMode: koishi_1.Schema.union([
31
- koishi_1.Schema.const('video').description('仅发送视频文件'),
32
- koishi_1.Schema.const('url').description('仅发送视频链接'),
33
- koishi_1.Schema.const('both').description('发送视频文件和链接'),
34
- ]).default('video').description('生成结果发送方式'),
35
- pollEnabled: koishi_1.Schema.boolean().default(false).description('是否启用异步轮询(若 API 返回任务 ID 则自动轮询至完成)'),
31
+ koishi_1.Schema.const('video').description('仅视频'),
32
+ koishi_1.Schema.const('url').description('仅链接'),
33
+ koishi_1.Schema.const('both').description('视频+链接'),
34
+ ]).default('video').description('发送方式'),
35
+ pollEnabled: koishi_1.Schema.boolean().default(false).description('启用异步轮询'),
36
36
  pollInterval: koishi_1.Schema.number().default(3000).description('轮询间隔(毫秒)'),
37
- pollTimeout: koishi_1.Schema.number().default(600000).description('轮询总超时时间(毫秒)'),
37
+ pollTimeout: koishi_1.Schema.number().default(600000).description('轮询超时(毫秒)'),
38
+ collectTimeout: koishi_1.Schema.number().default(120).description('收集模式超时(秒)'),
38
39
  }).description('基本设置'),
39
40
  koishi_1.Schema.object({
40
- proxyEnabled: koishi_1.Schema.boolean().default(false).description('是否启用 HTTP/HTTPS 代理'),
41
- proxyProtocol: koishi_1.Schema.union([
42
- koishi_1.Schema.const('http').description('HTTP'),
43
- koishi_1.Schema.const('https').description('HTTPS'),
44
- ]).default('http').description('代理协议'),
45
- proxyHost: koishi_1.Schema.string().default('').description('代理地址'),
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').description('代理协议'),
43
+ proxyHost: koishi_1.Schema.string().default('').description('代理主机地址'),
46
44
  proxyPort: koishi_1.Schema.number().default(8080).description('代理端口'),
47
- proxyAuth: koishi_1.Schema.boolean().default(false).description('代理是否需要认证'),
45
+ proxyAuth: koishi_1.Schema.boolean().default(false).description('启用代理认证'),
48
46
  proxyUsername: koishi_1.Schema.string().default('').description('代理用户名'),
49
47
  proxyPassword: koishi_1.Schema.string().role('secret').default('').description('代理密码'),
50
48
  }).description('代理设置'),
51
49
  koishi_1.Schema.object({
52
- useCustomApi: koishi_1.Schema.boolean().default(false).description('是否使用自定义 API 配置(开启后下方自定义列表生效)'),
53
- apiEndpoint: koishi_1.Schema.string().default('https://api.openai.com/v1/video/generations').description('API 端点地址'),
54
- apiKey: koishi_1.Schema.string().role('secret').default('').description('API 密钥'),
55
- model: koishi_1.Schema.string().default('video-generation-model').description('模型名称'),
56
- img2videoModel: koishi_1.Schema.string().default('').description('图生视频专用模型名称(留空则使用上方模型)'),
57
- videoDuration: koishi_1.Schema.number().default(0).description('视频时长(秒,留空则使用全局默认)'),
58
- videoResolution: koishi_1.Schema.string().default('').description('视频分辨率(留空则使用全局默认)'),
59
- txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示词模板。变量:{prompt}(留空则直接使用用户输入)'),
60
- img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示词模板。变量:{url} {prompt}(留空则直接使用用户输入)'),
61
- customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头 JSON 对象(合并到默认请求头)'),
62
- }).description('内置 API 设置'),
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'),
63
61
  koishi_1.Schema.object({
64
62
  apiStrategy: koishi_1.Schema.union([
65
- koishi_1.Schema.const('sequence').description('顺序模式'),
66
- koishi_1.Schema.const('roundrobin').description('负载均衡模式'),
67
- ]).default('roundrobin').description('API 调度策略'),
63
+ koishi_1.Schema.const('sequence').description('顺序使用'),
64
+ koishi_1.Schema.const('roundrobin').description('轮询使用')
65
+ ]).default('roundrobin').description('API调用策略'),
68
66
  customApiList: koishi_1.Schema.array(koishi_1.Schema.object({
69
- enable: koishi_1.Schema.boolean().default(true).description('是否启用此 API'),
70
- endpoint: koishi_1.Schema.string().default('https://api.openai.com/v1/video/generations').description('API 端点地址'),
71
- apiKey: koishi_1.Schema.string().role('secret').default('').description('API 密钥'),
72
- model: koishi_1.Schema.string().default('video-generation-model').description('模型名称'),
73
- img2videoModel: koishi_1.Schema.string().default('').description('图生视频专用模型名称(留空则使用上方模型)'),
74
- videoDuration: koishi_1.Schema.number().default(0).description('视频时长(秒,留空使用全局默认)'),
75
- videoResolution: koishi_1.Schema.string().default('').description('视频分辨率(留空使用全局默认)'),
76
- txt2videoPrompt: koishi_1.Schema.string().default('').description('文生视频提示词模板。变量:{prompt}(留空则直接使用用户输入)'),
77
- img2videoPrompt: koishi_1.Schema.string().default('').description('图生视频提示词模板。变量:{url} {prompt}(留空则直接使用用户输入)'),
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('图生视频提示模板'),
78
76
  customHeaders: koishi_1.Schema.string().role('textarea')
79
77
  .default('{"Authorization":"Bearer {apiKey}","Content-Type":"application/json"}')
80
- .description('自定义请求头 JSON,支持 {apiKey} 变量'),
78
+ .description('自定义请求头(JSON)'),
81
79
  bodyTemplate: koishi_1.Schema.string().role('textarea')
82
80
  .default(JSON.stringify({
83
- txt2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}' },
84
- img2videoBody: { model: '{model}', prompt: '{prompt}', duration: '{duration}', size: '{size}', image_url: '{url}' },
85
- responseVideoPath: 'video_url',
86
- pollUrlTemplate: '{endpoint}/{task_id}',
87
- taskIdPath: 'task_id',
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',
88
86
  }, null, 2))
89
- .description('自定义请求体 JSON 模板(高级,留空使用内置格式)。\n支持变量:{model}、{prompt}、{duration}、{size}、{url}。\n轮询配置可选:pollUrlTemplate、taskIdPath'),
90
- })).default([]).description('自定义 API 配置列表(仅当"使用自定义 API 配置"开启时生效)'),
91
- }).description('自定义 API 配置'),
87
+ .description('自定义请求体(JSON模板)'),
88
+ })).default([]).description('自定义API列表'),
89
+ }).description('自定义API配置'),
92
90
  koishi_1.Schema.object({
93
- blacklistAdmins: koishi_1.Schema.array(String).default([]).description('黑名单管理员的 QQ 号列表'),
91
+ blacklistAdmins: koishi_1.Schema.array(String).default([]).description('管理员QQ'),
94
92
  }).description('权限管理'),
95
93
  koishi_1.Schema.object({
96
94
  messages: koishi_1.Schema.object({
97
- generating: koishi_1.Schema.string().default('视频生成中,请耐心等待...').description('开始生成视频时的提示'),
98
- empty: koishi_1.Schema.string().default('[提示] 请输入提示词').description('未输入提示词时的提示'),
99
- noApi: koishi_1.Schema.string().default('[提示] 未配置可用API').description('无可用 API 时的提示'),
100
- fail: koishi_1.Schema.string().default('[提示] 视频生成失败').description('视频生成失败时的提示'),
101
- noContent: koishi_1.Schema.string().default('(未返回任何视频内容)').description('API 返回空结果时的追加提示'),
102
- templateError: koishi_1.Schema.string().default('(模板配置错误)').description('请求体模板解析失败时的追加提示'),
103
- txt2videoDisabled: koishi_1.Schema.string().default('[提示] 文生视频功能未启用').description('文生视频被禁用时的提示'),
104
- img2videoDisabled: koishi_1.Schema.string().default('[提示] 图生视频功能未启用').description('图生视频被禁用时的提示'),
105
- rateLimit: koishi_1.Schema.string().default('[提示] 调用次数已达上限,请稍后再试').description('触发频率限制时的提示'),
106
- needAssets: koishi_1.Schema.string().default('[提示] 图生视频需要正确配置 assets 服务(selfUrl 未正确设置或服务未启动)').description('缺少 assets 服务时的提示'),
107
- blacklisted: koishi_1.Schema.string().default('[提示] 你已被加入黑名单,无法使用视频生成功能').description('黑名单用户被拦截时的提示'),
108
- noPermission: koishi_1.Schema.string().default('[提示] 你没有权限管理黑名单').description('无黑名单管理权限时的提示'),
109
- blacklistAddSuccess: koishi_1.Schema.string().default('已将 {targets} 加入黑名单').description('黑名单添加成功的提示'),
110
- blacklistRemoveSuccess: koishi_1.Schema.string().default('已将 {targets} 移出黑名单').description('黑名单移除成功的提示'),
111
- blacklistAddFail: koishi_1.Schema.string().default('{targets} 已在黑名单中或无效').description('黑名单添加失败的提示'),
112
- blacklistRemoveFail: koishi_1.Schema.string().default('{targets} 不在黑名单中').description('黑名单移除失败的提示'),
113
- invalidUserId: koishi_1.Schema.string().default('无效的QQ号:{targets}').description('无效 QQ 号的提示'),
114
- blacklistListEmpty: koishi_1.Schema.string().default('当前黑名单为空').description('黑名单为空时的提示'),
115
- blacklistListTitle: koishi_1.Schema.string().default('当前黑名单:').description('黑名单列表标题'),
116
- noLastTask: koishi_1.Schema.string().default('没有上一次生成记录,无法重绘').description('无重绘历史时的提示'),
117
- redrawing: koishi_1.Schema.string().default('正在重绘上一次文生视频...').description('重绘开始时的提示'),
118
- redrawImg2Video: koishi_1.Schema.string().default('[提示] 重绘仅支持文生视频任务,图生视频任务请直接发起新的图生视频指令').description('图生视频任务无法重绘时的提示'),
119
- }).description('所有提示文案的自定义配置,支持模板变量'),
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('视频生成中(异步轮询),请稍后...'),
122
+ }).description('消息文本'),
120
123
  }).description('消息文本'),
121
124
  ]);
122
125
  async function apply(ctx, cfg) {
123
126
  const debug = cfg.debug;
124
127
  try {
125
128
  const loc = path_1.default.join(__dirname, 'locales', 'zh-CN.yml');
126
- if (fs_1.default.existsSync(loc)) {
129
+ if (fs_1.default.existsSync(loc))
127
130
  ctx.i18n.define('zh-CN', yaml_1.default.parse(fs_1.default.readFileSync(loc, 'utf8')));
128
- }
129
131
  }
130
132
  catch { }
133
+ const collectSessions = new Map();
131
134
  const lastTaskMap = new Map();
132
135
  let apiRoundRobinIdx = 0;
133
136
  const apiCallTimestamps = [];
134
- ctx.model.extend('ai_video_blacklist', {
135
- id: 'string',
136
- createdAt: 'date',
137
- }, {
138
- primary: 'id',
137
+ ctx.model.extend('ai_video_blacklist', { id: 'string', createdAt: 'date' }, { primary: 'id' });
138
+ ctx.on('dispose', () => {
139
+ for (const [, s] of collectSessions)
140
+ clearTimeout(s.timer);
141
+ collectSessions.clear();
139
142
  });
140
143
  function checkRateLimit() {
141
144
  const now = Date.now();
142
145
  const oneHourAgo = now - 3600000;
143
- let trimIdx = 0;
144
- while (trimIdx < apiCallTimestamps.length && apiCallTimestamps[trimIdx] < oneHourAgo) {
145
- trimIdx++;
146
- }
147
- if (trimIdx > 0) {
148
- apiCallTimestamps.splice(0, trimIdx);
149
- }
146
+ let i = 0;
147
+ while (i < apiCallTimestamps.length && apiCallTimestamps[i] < oneHourAgo)
148
+ i++;
149
+ if (i > 0)
150
+ apiCallTimestamps.splice(0, i);
150
151
  return apiCallTimestamps.length + 1 <= cfg.rateLimit;
151
152
  }
152
- function recordApiCall() {
153
- apiCallTimestamps.push(Date.now());
154
- }
155
- const BUILTIN_TXT2VIDEO = {
156
- model: '{model}',
157
- prompt: '{prompt}',
158
- duration: '{duration}',
159
- size: '{size}',
160
- };
161
- const BUILTIN_IMG2VIDEO = {
162
- model: '{model}',
163
- prompt: '{prompt}',
164
- duration: '{duration}',
165
- size: '{size}',
166
- image_url: '{url}',
167
- };
153
+ function recordApiCall() { apiCallTimestamps.push(Date.now()); }
168
154
  function parseApiEntry(entry) {
169
155
  if (!entry.endpoint)
170
156
  return null;
171
- const headers = {
172
- 'Content-Type': 'application/json'
173
- };
174
- if (entry.apiKey) {
157
+ const headers = { 'Content-Type': 'application/json' };
158
+ if (entry.apiKey)
175
159
  headers['Authorization'] = `Bearer ${entry.apiKey}`;
176
- }
177
160
  if (entry.customHeaders) {
178
161
  try {
179
162
  const custom = JSON.parse(entry.customHeaders);
180
- for (const [k, v] of Object.entries(custom)) {
163
+ for (const [k, v] of Object.entries(custom))
181
164
  headers[k] = typeof v === 'string' ? v.replace(/\{apiKey\}/g, entry.apiKey || '') : String(v);
182
- }
183
165
  }
184
166
  catch { }
185
167
  }
186
- let txt2videoBody;
187
- let img2videoBody;
188
- let responseVideoPath;
189
- let pollUrlTemplate;
190
- let taskIdPath;
168
+ let txt2videoBody, img2videoBody, responseVideoPath, pollUrlTemplate, taskIdPath;
191
169
  if (entry.bodyTemplate) {
192
170
  try {
193
171
  const tmpl = JSON.parse(entry.bodyTemplate);
194
- txt2videoBody = tmpl.txt2videoBody || BUILTIN_TXT2VIDEO;
195
- 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}' };
196
174
  responseVideoPath = tmpl.responseVideoPath || 'video_url';
197
175
  pollUrlTemplate = tmpl.pollUrlTemplate || '';
198
176
  taskIdPath = tmpl.taskIdPath || '';
@@ -202,8 +180,8 @@ async function apply(ctx, cfg) {
202
180
  }
203
181
  }
204
182
  else {
205
- txt2videoBody = BUILTIN_TXT2VIDEO;
206
- 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}' };
207
185
  responseVideoPath = 'video_url';
208
186
  pollUrlTemplate = '';
209
187
  taskIdPath = '';
@@ -211,8 +189,7 @@ async function apply(ctx, cfg) {
211
189
  return {
212
190
  endpoint: entry.endpoint,
213
191
  headers,
214
- txt2videoBody,
215
- img2videoBody,
192
+ txt2videoBody, img2videoBody,
216
193
  responseVideoPath,
217
194
  pollUrlTemplate,
218
195
  taskIdPath,
@@ -226,26 +203,22 @@ async function apply(ctx, cfg) {
226
203
  };
227
204
  }
228
205
  function buildBuiltinApi() {
229
- const headers = {
230
- 'Content-Type': 'application/json'
231
- };
232
- if (cfg.apiKey) {
206
+ const headers = { 'Content-Type': 'application/json' };
207
+ if (cfg.apiKey)
233
208
  headers['Authorization'] = `Bearer ${cfg.apiKey}`;
234
- }
235
209
  if (cfg.customHeaders) {
236
210
  try {
237
211
  const custom = JSON.parse(cfg.customHeaders);
238
- for (const [k, v] of Object.entries(custom)) {
212
+ for (const [k, v] of Object.entries(custom))
239
213
  headers[k] = typeof v === 'string' ? v.replace(/\{apiKey\}/g, cfg.apiKey || '') : String(v);
240
- }
241
214
  }
242
215
  catch { }
243
216
  }
244
217
  return {
245
- endpoint: cfg.apiEndpoint || 'https://api.openai.com/v1/video/generations',
218
+ endpoint: cfg.apiEndpoint || 'https://apihub.agnes-ai.com/v1/videos',
246
219
  headers,
247
- txt2videoBody: BUILTIN_TXT2VIDEO,
248
- 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}' },
249
222
  responseVideoPath: 'video_url',
250
223
  pollUrlTemplate: '',
251
224
  taskIdPath: '',
@@ -254,19 +227,17 @@ async function apply(ctx, cfg) {
254
227
  videoResolution: cfg.videoResolution || '',
255
228
  txt2videoPrompt: cfg.txt2videoPrompt || '',
256
229
  img2videoPrompt: cfg.img2videoPrompt || '',
257
- model: cfg.model || 'video-generation-model',
230
+ model: cfg.model || 'agnes-video-v2.0',
258
231
  img2videoModel: cfg.img2videoModel || '',
259
232
  };
260
233
  }
261
234
  function getApi() {
262
235
  if (cfg.useCustomApi) {
263
- const entries = cfg.customApiList.filter((e) => e.enable);
264
- if (entries.length === 0)
236
+ const entries = cfg.customApiList.filter(e => e.enable);
237
+ if (!entries.length)
265
238
  return null;
266
- const apis = entries
267
- .map((e) => parseApiEntry(e))
268
- .filter((a) => a !== null);
269
- if (apis.length === 0)
239
+ const apis = entries.map(parseApiEntry).filter(Boolean);
240
+ if (!apis.length)
270
241
  return null;
271
242
  if (cfg.apiStrategy === 'sequence')
272
243
  return apis[0];
@@ -280,401 +251,393 @@ async function apply(ctx, cfg) {
280
251
  return buildBuiltinApi();
281
252
  }
282
253
  }
283
- function resolveTemplate(template, vars) {
284
- const jsonStr = JSON.stringify(template);
285
- let processed = jsonStr;
286
- for (const [key, value] of Object.entries(vars)) {
287
- if (value === undefined || value === null)
288
- continue;
289
- const regex = new RegExp(`\\{${key}\\}`, 'g');
290
- processed = processed.replace(regex, JSON.stringify(String(value)).slice(1, -1));
291
- }
292
- return JSON.parse(processed);
293
- }
294
- function getValueByPath(obj, pathStr) {
295
- if (!obj || !pathStr)
296
- return undefined;
297
- const normalized = pathStr.replace(/\[(\d+)\]/g, '.$1');
298
- const keys = normalized.split('.').filter(k => k !== '');
299
- let current = obj;
300
- for (const key of keys) {
301
- if (current === undefined || current === null)
302
- return undefined;
303
- const numKey = /^\d+$/.test(key) ? parseInt(key) : key;
304
- current = current[numKey];
305
- }
306
- return current;
307
- }
308
- async function pollForResult(pollUrl, headers, path, interval, timeout) {
309
- const start = Date.now();
310
- while (Date.now() - start < timeout) {
311
- try {
312
- const res = await axios_1.default.get(pollUrl, { headers, timeout: 10000 });
313
- const status = getValueByPath(res.data, 'status');
314
- if (status === 'completed' || status === 'succeeded') {
315
- return res.data;
316
- }
317
- if (status === 'failed' || status === 'error') {
318
- throw new Error('Task failed');
319
- }
320
- await new Promise(r => setTimeout(r, interval));
321
- }
322
- catch (e) {
323
- if (e.message === 'Task failed')
324
- throw e;
325
- await new Promise(r => setTimeout(r, interval));
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]];
326
261
  }
262
+ return templateObj;
327
263
  }
328
- throw new Error('Polling timeout');
329
- }
330
- async function sendVideo(session, url) {
331
- const mode = cfg.videoSendMode;
332
- if (mode === 'url') {
333
- await safeSend(session, url);
264
+ if (Array.isArray(templateObj)) {
265
+ return templateObj.map(item => resolveTemplate(item, vars));
334
266
  }
335
- else {
336
- try {
337
- await safeSend(session, koishi_1.segment.video(url));
338
- }
339
- catch {
340
- if (mode === 'both')
341
- await safeSend(session, url);
267
+ if (typeof templateObj === 'object') {
268
+ const result = {};
269
+ for (const key of Object.keys(templateObj)) {
270
+ result[key] = resolveTemplate(templateObj[key], vars);
342
271
  }
343
- if (mode === 'both')
344
- await safeSend(session, url);
272
+ return result;
345
273
  }
274
+ return templateObj;
346
275
  }
347
- async function handleVideoResponse(session, responseData, api) {
348
- let videoUrl = getValueByPath(responseData, api.responseVideoPath);
349
- if (!videoUrl) {
350
- const found = findFirstVideoUrl(responseData);
351
- if (found)
352
- videoUrl = found;
353
- }
354
- if (videoUrl && typeof videoUrl === 'string') {
355
- await sendVideo(session, videoUrl.trim());
356
- }
357
- else {
358
- await safeSend(session, cfg.messages.fail + cfg.messages.noContent);
276
+ function getValueByPath(obj, path) {
277
+ if (!obj || !path)
278
+ return undefined;
279
+ const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(k => k);
280
+ let cur = obj;
281
+ for (const k of keys) {
282
+ if (cur == null)
283
+ return undefined;
284
+ cur = cur[/^\d+$/.test(k) ? parseInt(k) : k];
359
285
  }
286
+ return cur;
360
287
  }
361
- function findFirstVideoUrl(obj) {
288
+ function findVideoUrl(obj) {
362
289
  if (!obj)
363
290
  return null;
364
291
  if (typeof obj === 'string') {
365
- const trimmed = obj.trim();
366
- if (/^https?:\/\/.+\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i.test(trimmed))
367
- return trimmed;
292
+ const t = obj.trim();
293
+ if (/^https?:\/\/.+\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i.test(t))
294
+ return t;
368
295
  return null;
369
296
  }
370
297
  if (Array.isArray(obj)) {
371
298
  for (const item of obj) {
372
- const found = findFirstVideoUrl(item);
373
- if (found)
374
- return found;
299
+ const f = findVideoUrl(item);
300
+ if (f)
301
+ return f;
375
302
  }
376
303
  }
377
304
  else if (typeof obj === 'object') {
378
- for (const key of Object.keys(obj)) {
379
- const found = findFirstVideoUrl(obj[key]);
380
- if (found)
381
- return found;
305
+ for (const k of Object.keys(obj)) {
306
+ const f = findVideoUrl(obj[k]);
307
+ if (f)
308
+ return f;
382
309
  }
383
310
  }
384
311
  return null;
385
312
  }
386
- function validateEndpointUrl(url) {
313
+ function validateEndpoint(url) {
387
314
  try {
388
- const parsed = new URL(url);
389
- return parsed.protocol === 'http:' || parsed.protocol === 'https:';
315
+ const p = new URL(url);
316
+ return p.protocol === 'http:' || p.protocol === 'https:';
390
317
  }
391
318
  catch {
392
319
  return false;
393
320
  }
394
321
  }
395
- async function safeSend(session, message) {
322
+ async function pollForResult(pollUrl, headers, interval, timeout) {
323
+ const start = Date.now();
324
+ while (Date.now() - start < timeout) {
325
+ try {
326
+ const res = await axios_1.default.get(pollUrl, { headers, timeout: 10000 });
327
+ const status = getValueByPath(res.data, 'status');
328
+ if (status === 'completed' || status === 'succeeded')
329
+ return res.data;
330
+ if (status === 'failed' || status === 'error')
331
+ throw new Error('Task failed');
332
+ await new Promise(r => setTimeout(r, interval));
333
+ }
334
+ catch (e) {
335
+ if (e.message === 'Task failed')
336
+ throw e;
337
+ await new Promise(r => setTimeout(r, interval));
338
+ }
339
+ }
340
+ throw new Error('Polling timeout');
341
+ }
342
+ async function safeSend(session, msg) {
396
343
  try {
397
- await session.send(message);
344
+ await session.send(msg);
398
345
  }
399
346
  catch (e) {
400
- logger.error('发送消息失败', e);
347
+ logger.error('发送失败', e);
401
348
  }
402
349
  }
403
- function getErrorMessage(err) {
404
- if (axios_1.default.isAxiosError(err)) {
405
- if (err.code === 'ECONNABORTED')
406
- return '请求超时';
407
- if (err.response) {
408
- const status = err.response.status;
409
- if (status === 401)
410
- return 'API Key 无效';
411
- if (status === 429)
412
- return '请求过于频繁';
413
- if (status >= 500)
414
- return '服务器错误';
415
- return `HTTP ${status}`;
350
+ async function sendSingleVideo(session, url) {
351
+ const mode = cfg.videoSendMode;
352
+ if (mode === 'url')
353
+ await safeSend(session, url);
354
+ else {
355
+ try {
356
+ await safeSend(session, koishi_1.segment.video(url));
416
357
  }
417
- return '网络错误';
358
+ catch { }
359
+ if (mode === 'both')
360
+ await safeSend(session, url);
418
361
  }
419
- return err.message?.slice(0, 100) || '未知错误';
420
362
  }
421
363
  function sanitizeForLog(obj, sensitive) {
422
364
  if (!sensitive)
423
365
  return obj;
424
366
  try {
425
- const str = JSON.stringify(obj);
426
- return JSON.parse(str.split(sensitive).join('***'));
367
+ return JSON.parse(JSON.stringify(obj).split(sensitive).join('***'));
427
368
  }
428
369
  catch {
429
370
  return obj;
430
371
  }
431
372
  }
432
- function isValidQQ(id) {
433
- return /^\d{5,11}$/.test(id);
434
- }
435
- async function isBlacklisted(userId) {
436
- try {
437
- const rows = await ctx.database.get('ai_video_blacklist', { id: userId });
438
- return rows.length > 0;
439
- }
440
- catch (e) {
441
- return false;
442
- }
443
- }
444
- async function addToBlacklist(ids) {
445
- const success = [];
446
- const fail = [];
447
- const validIds = ids.filter(id => { if (isValidQQ(id))
448
- return true; fail.push(id); return false; });
449
- if (validIds.length === 0)
450
- return { success, fail };
451
- const existing = await ctx.database.get('ai_video_blacklist', { id: validIds });
452
- const existingSet = new Set(existing.map((e) => e.id));
453
- const toCreate = validIds.filter(id => !existingSet.has(id));
454
- for (const id of toCreate) {
455
- try {
456
- await ctx.database.create('ai_video_blacklist', { id, createdAt: new Date() });
457
- success.push(id);
458
- }
459
- catch {
460
- fail.push(id);
461
- }
462
- }
463
- for (const entry of existing)
464
- fail.push(entry.id);
465
- return { success, fail };
466
- }
467
- async function removeFromBlacklist(ids) {
468
- const success = [];
469
- const fail = [];
470
- const validIds = ids.filter(id => { if (isValidQQ(id))
471
- return true; fail.push(id); return false; });
472
- if (validIds.length === 0)
473
- return { success, fail };
474
- const existing = await ctx.database.get('ai_video_blacklist', { id: validIds });
475
- const existingSet = new Set(existing.map((e) => e.id));
476
- const toRemove = validIds.filter(id => existingSet.has(id));
477
- for (const id of toRemove) {
478
- try {
479
- await ctx.database.remove('ai_video_blacklist', { id });
480
- success.push(id);
481
- }
482
- catch {
483
- fail.push(id);
484
- }
485
- }
486
- for (const id of validIds.filter(id => !existingSet.has(id)))
487
- fail.push(id);
488
- return { success, fail };
489
- }
490
- async function customGenerateVideo(session, api, prompt, imageUrl = '', modelOverride) {
373
+ async function customGenerateVideo(session, api, prompt, imageUrl = '') {
491
374
  const isImg2Video = !!imageUrl;
492
- const model = modelOverride || (isImg2Video ? (api.img2videoModel || api.model) : api.model);
375
+ const model = isImg2Video ? (api.img2videoModel || api.model) : api.model;
493
376
  const duration = api.videoDuration || cfg.videoDuration || 5;
494
377
  const resolution = api.videoResolution || cfg.videoResolution || '1024x576';
495
378
  const promptTemplate = isImg2Video ? api.img2videoPrompt : api.txt2videoPrompt;
496
379
  let finalPrompt = prompt;
497
- if (promptTemplate) {
498
- finalPrompt = promptTemplate.replace('{prompt}', prompt).replace('{url}', imageUrl || '');
499
- }
380
+ if (promptTemplate)
381
+ finalPrompt = promptTemplate.replace('{prompt}', prompt).replace('{url}', imageUrl);
500
382
  const bodyTemplate = isImg2Video ? api.img2videoBody : api.txt2videoBody;
501
- const bodyVars = { model, prompt: finalPrompt, duration: String(duration), size: resolution };
502
- if (isImg2Video) {
503
- bodyVars['url'] = imageUrl;
504
- }
383
+ const vars = {
384
+ model,
385
+ prompt: finalPrompt,
386
+ duration,
387
+ size: resolution,
388
+ };
389
+ if (isImg2Video)
390
+ vars.url = imageUrl;
505
391
  let body;
506
392
  try {
507
- body = resolveTemplate(bodyTemplate, bodyVars);
393
+ body = resolveTemplate(bodyTemplate, vars);
508
394
  }
509
395
  catch {
510
396
  await safeSend(session, cfg.messages.fail + cfg.messages.templateError);
511
- return;
397
+ return null;
512
398
  }
513
- if (!validateEndpointUrl(api.endpoint)) {
514
- await safeSend(session, cfg.messages.fail + '(API端点配置无效)');
515
- return;
399
+ if (!validateEndpoint(api.endpoint)) {
400
+ await safeSend(session, cfg.messages.fail + '(端点无效)');
401
+ return null;
516
402
  }
517
403
  const sensitive = api.headers?.Authorization?.split(' ')[1] || '';
518
- if (debug) {
404
+ if (debug)
519
405
  logger.info('API请求', JSON.stringify(sanitizeForLog(body, sensitive)));
520
- }
521
406
  try {
522
- const config = {
523
- url: api.endpoint,
524
- method: api.method,
525
- headers: api.headers,
526
- data: body,
527
- timeout: cfg.timeout,
528
- };
407
+ const config = { url: api.endpoint, method: api.method, headers: api.headers, data: body, timeout: cfg.timeout };
529
408
  if (cfg.proxyEnabled && cfg.proxyHost) {
530
409
  config.proxy = {
531
- protocol: cfg.proxyProtocol,
532
- host: cfg.proxyHost,
533
- port: cfg.proxyPort,
534
- auth: cfg.proxyAuth && cfg.proxyUsername ? { username: cfg.proxyUsername, password: cfg.proxyPassword } : undefined,
410
+ protocol: cfg.proxyProtocol, host: cfg.proxyHost, port: cfg.proxyPort,
411
+ auth: cfg.proxyAuth && cfg.proxyUsername ? { username: cfg.proxyUsername, password: cfg.proxyPassword } : undefined
535
412
  };
536
413
  }
537
- let responseData = (await (0, axios_1.default)(config)).data;
414
+ let res = (await (0, axios_1.default)(config)).data;
538
415
  if (cfg.pollEnabled && api.taskIdPath) {
539
- const taskId = getValueByPath(responseData, api.taskIdPath);
416
+ const taskId = getValueByPath(res, api.taskIdPath);
540
417
  if (taskId) {
541
- const pollUrl = api.pollUrlTemplate.replace('{endpoint}', api.endpoint).replace('{task_id}', taskId);
542
- responseData = await pollForResult(pollUrl, api.headers, api.responseVideoPath, cfg.pollInterval, cfg.pollTimeout);
418
+ const pollUrl = api.pollUrlTemplate.replace('{task_id}', taskId);
419
+ await safeSend(session, cfg.messages.pollWaiting);
420
+ res = await pollForResult(pollUrl, api.headers, cfg.pollInterval, cfg.pollTimeout);
543
421
  }
544
422
  }
545
- await handleVideoResponse(session, responseData, api);
546
- const userId = `${session.guildId || 'private'}-${session.userId}`;
547
- lastTaskMap.set(userId, {
548
- prompt,
549
- imageUrl,
550
- isImg2Video,
551
- model,
552
- duration,
553
- resolution,
554
- });
423
+ const videoUrl = getValueByPath(res, api.responseVideoPath) || findVideoUrl(res);
424
+ return videoUrl || null;
555
425
  }
556
426
  catch (err) {
557
427
  logger.error('视频生成失败', err);
558
- await safeSend(session, cfg.messages.fail + ` [${getErrorMessage(err)}]`);
428
+ await safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
429
+ return null;
559
430
  }
560
431
  }
561
- async function generateVideo(session, prompt, imageUrl = '', modelOverride) {
562
- if (!checkRateLimit()) {
563
- await safeSend(session, cfg.messages.rateLimit);
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);
564
453
  return;
565
454
  }
566
- const api = getApi();
567
- if (!api) {
568
- await safeSend(session, cfg.messages.noApi);
569
- return;
455
+ if (videoUrls.length === 1 || !cfg.enableForward) {
456
+ for (const url of videoUrls) {
457
+ await sendSingleVideo(session, url);
458
+ }
570
459
  }
571
- recordApiCall();
572
- return customGenerateVideo(session, api, prompt, imageUrl, modelOverride);
573
- }
574
- ctx.command('video <raw:text>', 'AI视频生成(文生视频/图生视频自动识别)')
575
- .action(async ({ session }, raw) => {
576
- try {
577
- if (!session)
578
- return;
579
- if (await isBlacklisted(session.userId))
580
- return safeSend(session, cfg.messages.blacklisted);
581
- const prompt = (raw || '').trim();
582
- const imgs = koishi_1.h.select(session.elements, 'img');
583
- const hasImage = imgs.length > 0;
584
- if (hasImage && !cfg.enableImg2Video)
585
- return safeSend(session, cfg.messages.img2videoDisabled);
586
- if (!hasImage && !cfg.enableTxt2Video)
587
- return safeSend(session, cfg.messages.txt2videoDisabled);
588
- if (!prompt) {
589
- if (hasImage)
590
- return safeSend(session, '图生视频请提供提示词');
591
- return safeSend(session, cfg.messages.empty);
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));
592
464
  }
593
- if (prompt.length > 6000)
594
- return safeSend(session, '提示词过长,请限制在6000字符以内');
595
- if (!hasImage) {
596
- await safeSend(session, cfg.messages.generating);
597
- await generateVideo(session, prompt);
598
- return;
465
+ catch {
466
+ for (const url of videoUrls)
467
+ await sendSingleVideo(session, url);
599
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
+ }
474
+ }
475
+ function startTimer(session, key, collect) {
476
+ return setTimeout(() => {
477
+ collectSessions.delete(key);
478
+ safeSend(session, cfg.messages.collectTimeout);
479
+ }, cfg.collectTimeout * 1000);
480
+ }
481
+ ctx.command('video [text]', 'AI视频生成')
482
+ .action(async ({ session }, text) => {
483
+ if (!session)
484
+ return;
485
+ if (await isBlacklisted(session.userId))
486
+ return safeSend(session, cfg.messages.blacklisted);
487
+ const key = `${session.guildId || 'private'}-${session.userId}`;
488
+ if (collectSessions.has(key))
489
+ return safeSend(session, '你已在收集模式中');
490
+ const hasImage = koishi_1.h.select(session.elements, 'img').length > 0;
491
+ if (!hasImage && !cfg.enableTxt2Video)
492
+ return safeSend(session, cfg.messages.txt2videoDisabled);
493
+ if (hasImage && !cfg.enableImg2Video)
494
+ return safeSend(session, cfg.messages.img2videoDisabled);
495
+ let imageUrl = '';
496
+ if (hasImage) {
600
497
  const assets = ctx.assets;
601
498
  if (!assets)
602
499
  return safeSend(session, cfg.messages.needAssets);
603
- const uploadResult = await assets.upload(imgs[0].attrs.src, 'ref_image.jpg');
604
- if (!uploadResult || !/^https?:\/\//.test(uploadResult)) {
605
- return safeSend(session, cfg.messages.needAssets);
500
+ const img = koishi_1.h.select(session.elements, 'img')[0];
501
+ try {
502
+ const up = await assets.upload(img.attrs.src, 'ref_image.jpg');
503
+ if (/^https?:\/\//.test(up))
504
+ imageUrl = up;
606
505
  }
506
+ catch { }
507
+ }
508
+ const collect = { prompt: text || '', imageUrl, timer: null };
509
+ collect.timer = startTimer(session, key, collect);
510
+ collectSessions.set(key, collect);
511
+ await safeSend(session, cfg.messages.enterCollect);
512
+ });
513
+ ctx.on('message', async (session) => {
514
+ if (session.command)
515
+ return;
516
+ if (!session || !session.elements)
517
+ return;
518
+ const key = `${session.guildId || 'private'}-${session.userId}`;
519
+ const collect = collectSessions.get(key);
520
+ if (!collect)
521
+ return;
522
+ const text = (session.content || '').trim();
523
+ const imgs = koishi_1.h.select(session.elements, 'img');
524
+ if (text === '取消' || text === 'cancel') {
525
+ clearTimeout(collect.timer);
526
+ collectSessions.delete(key);
527
+ return safeSend(session, cfg.messages.cancelCollect);
528
+ }
529
+ if (text === '开始' || text === 'start' || text === '生成') {
530
+ clearTimeout(collect.timer);
531
+ collectSessions.delete(key);
532
+ if (!collect.prompt && !collect.imageUrl)
533
+ return safeSend(session, cfg.messages.empty);
607
534
  await safeSend(session, cfg.messages.generating);
608
- await generateVideo(session, prompt, uploadResult);
535
+ return generateVideos(session, collect.prompt || '默认', collect.imageUrl, cfg.maxVideos);
609
536
  }
610
- catch (e) {
611
- logger.error('video命令异常', e);
612
- await safeSend(session, cfg.messages.fail);
537
+ if (imgs.length > 0 && !collect.imageUrl) {
538
+ const assets = ctx.assets;
539
+ if (!assets)
540
+ return safeSend(session, cfg.messages.needAssets);
541
+ try {
542
+ const up = await assets.upload(imgs[0].attrs.src, 'ref_image.jpg');
543
+ if (/^https?:\/\//.test(up)) {
544
+ collect.imageUrl = up;
545
+ clearTimeout(collect.timer);
546
+ collect.timer = startTimer(session, key, collect);
547
+ await safeSend(session, cfg.messages.collectUpdate.replace('{images}', '1'));
548
+ }
549
+ }
550
+ catch { }
551
+ return;
552
+ }
553
+ if (text) {
554
+ collect.prompt = collect.prompt ? collect.prompt + ' ' + text : text;
555
+ clearTimeout(collect.timer);
556
+ collect.timer = startTimer(session, key, collect);
557
+ await safeSend(session, cfg.messages.collectUpdate.replace('{images}', collect.imageUrl ? '1' : '0'));
613
558
  }
614
559
  });
615
560
  ctx.command('redraw', '重绘上一次文生视频')
616
561
  .action(async ({ session }) => {
617
- try {
618
- if (!session)
619
- return;
620
- if (await isBlacklisted(session.userId))
621
- return safeSend(session, cfg.messages.blacklisted);
622
- const userId = `${session.guildId || 'private'}-${session.userId}`;
623
- const last = lastTaskMap.get(userId);
624
- if (!last)
625
- return safeSend(session, cfg.messages.noLastTask);
626
- if (last.isImg2Video)
627
- return safeSend(session, cfg.messages.redrawImg2Video);
628
- await safeSend(session, cfg.messages.redrawing);
629
- await generateVideo(session, last.prompt, '', last.model);
630
- }
631
- catch (e) {
632
- logger.error('重绘命令异常', e);
633
- await safeSend(session, cfg.messages.fail);
634
- }
635
- });
636
- const blacklistCmd = ctx.command('blacklist', '黑名单管理');
637
- blacklistCmd.subcommand('.list', '查看黑名单').action(async ({ session }) => {
638
562
  if (!session)
639
563
  return;
640
- if (!cfg.blacklistAdmins.includes(session.userId))
641
- return safeSend(session, cfg.messages.noPermission);
564
+ if (await isBlacklisted(session.userId))
565
+ return safeSend(session, cfg.messages.blacklisted);
566
+ const last = lastTaskMap.get(`${session.guildId || 'private'}-${session.userId}`);
567
+ if (!last)
568
+ return safeSend(session, cfg.messages.noLastTask);
569
+ if (last.isImg2Video)
570
+ return safeSend(session, cfg.messages.redrawImg2Video);
571
+ await safeSend(session, cfg.messages.redrawing);
572
+ return generateVideos(session, last.prompt, '', cfg.maxVideos);
573
+ });
574
+ function isValidQQ(id) { return /^\d{5,11}$/.test(id); }
575
+ async function isBlacklisted(userId) {
642
576
  try {
643
- const entries = await ctx.database.get('ai_video_blacklist', {});
644
- if (entries.length === 0)
645
- return safeSend(session, cfg.messages.blacklistListEmpty);
646
- const list = entries.map(e => e.id).join('\n');
647
- return safeSend(session, cfg.messages.blacklistListTitle + '\n' + list);
577
+ return (await ctx.database.get('ai_video_blacklist', { id: userId })).length > 0;
648
578
  }
649
- catch (e) {
650
- return safeSend(session, cfg.messages.fail);
579
+ catch {
580
+ return false;
651
581
  }
582
+ }
583
+ const blacklistCmd = ctx.command('blacklist', '黑名单管理');
584
+ blacklistCmd.subcommand('.list').action(async ({ session }) => {
585
+ if (!session || !cfg.blacklistAdmins.includes(session.userId))
586
+ return safeSend(session, cfg.messages.noPermission);
587
+ const entries = await ctx.database.get('ai_video_blacklist', {});
588
+ if (!entries.length)
589
+ return safeSend(session, cfg.messages.blacklistListEmpty);
590
+ return safeSend(session, cfg.messages.blacklistListTitle + '\n' + entries.map(e => e.id).join('\n'));
652
591
  });
653
- blacklistCmd.subcommand('.add <...targets:string>', '添加黑名单').action(async ({ session }, ...targets) => {
654
- if (!session)
655
- return;
656
- if (!cfg.blacklistAdmins.includes(session.userId))
592
+ blacklistCmd.subcommand('.add <...targets:string>').action(async ({ session }, ...targets) => {
593
+ if (!session || !cfg.blacklistAdmins.includes(session.userId))
657
594
  return safeSend(session, cfg.messages.noPermission);
658
- const ids = targets.map(t => t.trim()).filter(id => id.length > 0);
595
+ const ids = targets.map(t => t.trim()).filter(Boolean);
659
596
  const invalid = ids.filter(id => !isValidQQ(id));
660
597
  if (invalid.length)
661
598
  return safeSend(session, cfg.messages.invalidUserId.replace('{targets}', invalid.join(', ')));
662
- const { success, fail } = await addToBlacklist(ids);
599
+ const success = [], fail = [];
600
+ for (const id of ids) {
601
+ try {
602
+ const exist = await ctx.database.get('ai_video_blacklist', { id });
603
+ if (exist.length)
604
+ fail.push(id);
605
+ else {
606
+ await ctx.database.create('ai_video_blacklist', { id, createdAt: new Date() });
607
+ success.push(id);
608
+ }
609
+ }
610
+ catch {
611
+ fail.push(id);
612
+ }
613
+ }
663
614
  if (success.length)
664
615
  await safeSend(session, cfg.messages.blacklistAddSuccess.replace('{targets}', success.join(', ')));
665
616
  if (fail.length)
666
617
  await safeSend(session, cfg.messages.blacklistAddFail.replace('{targets}', fail.join(', ')));
667
618
  });
668
- blacklistCmd.subcommand('.remove <...targets:string>', '移除黑名单').action(async ({ session }, ...targets) => {
669
- if (!session)
670
- return;
671
- if (!cfg.blacklistAdmins.includes(session.userId))
619
+ blacklistCmd.subcommand('.remove <...targets:string>').action(async ({ session }, ...targets) => {
620
+ if (!session || !cfg.blacklistAdmins.includes(session.userId))
672
621
  return safeSend(session, cfg.messages.noPermission);
673
- const ids = targets.map(t => t.trim()).filter(id => id.length > 0);
622
+ const ids = targets.map(t => t.trim()).filter(Boolean);
674
623
  const invalid = ids.filter(id => !isValidQQ(id));
675
624
  if (invalid.length)
676
625
  return safeSend(session, cfg.messages.invalidUserId.replace('{targets}', invalid.join(', ')));
677
- const { success, fail } = await removeFromBlacklist(ids);
626
+ const success = [], fail = [];
627
+ for (const id of ids) {
628
+ try {
629
+ const exist = await ctx.database.get('ai_video_blacklist', { id });
630
+ if (exist.length) {
631
+ await ctx.database.remove('ai_video_blacklist', { id });
632
+ success.push(id);
633
+ }
634
+ else
635
+ fail.push(id);
636
+ }
637
+ catch {
638
+ fail.push(id);
639
+ }
640
+ }
678
641
  if (success.length)
679
642
  await safeSend(session, cfg.messages.blacklistRemoveSuccess.replace('{targets}', success.join(', ')));
680
643
  if (fail.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ai-video",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Koishi AI 视频生成插件,支持文生视频、图生视频",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",