koishi-plugin-ai-video 0.0.1 → 0.0.2

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