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.
- package/lib/index.d.ts +17 -0
- package/lib/index.js +390 -427
- 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('
|
|
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('
|
|
41
|
-
proxyProtocol: koishi_1.Schema.union([
|
|
42
|
-
|
|
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('
|
|
53
|
-
apiEndpoint: koishi_1.Schema.string().default('https://
|
|
54
|
-
apiKey: koishi_1.Schema.string().role('secret').default('').description('API
|
|
55
|
-
model: koishi_1.Schema.string().default('video-
|
|
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('
|
|
60
|
-
img2videoPrompt: koishi_1.Schema.string().default('').description('
|
|
61
|
-
customHeaders: koishi_1.Schema.string().role('textarea').default('{}').description('自定义请求头
|
|
62
|
-
}).description('内置
|
|
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('
|
|
70
|
-
endpoint: koishi_1.Schema.string().default('https://
|
|
71
|
-
apiKey: koishi_1.Schema.string().role('secret').default('').description('API
|
|
72
|
-
model: koishi_1.Schema.string().default('video-
|
|
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('
|
|
77
|
-
img2videoPrompt: koishi_1.Schema.string().default('').description('
|
|
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('自定义请求头
|
|
78
|
+
.description('自定义请求头(JSON)'),
|
|
81
79
|
bodyTemplate: koishi_1.Schema.string().role('textarea')
|
|
82
80
|
.default(JSON.stringify({
|
|
83
|
-
txt2videoBody: { model: '{model}', prompt: '{prompt}',
|
|
84
|
-
img2videoBody: { model: '{model}', prompt: '{prompt}',
|
|
85
|
-
responseVideoPath: '
|
|
86
|
-
pollUrlTemplate: '
|
|
87
|
-
taskIdPath: '
|
|
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('自定义请求体
|
|
90
|
-
})).default([]).description('自定义
|
|
91
|
-
}).description('自定义
|
|
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('
|
|
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('视频生成中,请耐心等待...')
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
144
|
-
while (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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 ||
|
|
195
|
-
img2videoBody = tmpl.img2videoBody ||
|
|
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 =
|
|
206
|
-
img2videoBody =
|
|
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
|
-
|
|
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://
|
|
218
|
+
endpoint: cfg.apiEndpoint || 'https://apihub.agnes-ai.com/v1/videos',
|
|
246
219
|
headers,
|
|
247
|
-
txt2videoBody:
|
|
248
|
-
img2videoBody:
|
|
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-
|
|
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(
|
|
264
|
-
if (entries.length
|
|
236
|
+
const entries = cfg.customApiList.filter(e => e.enable);
|
|
237
|
+
if (!entries.length)
|
|
265
238
|
return null;
|
|
266
|
-
const apis = entries
|
|
267
|
-
|
|
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(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
344
|
-
await safeSend(session, url);
|
|
272
|
+
return result;
|
|
345
273
|
}
|
|
274
|
+
return templateObj;
|
|
346
275
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
288
|
+
function findVideoUrl(obj) {
|
|
362
289
|
if (!obj)
|
|
363
290
|
return null;
|
|
364
291
|
if (typeof obj === 'string') {
|
|
365
|
-
const
|
|
366
|
-
if (/^https?:\/\/.+\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i.test(
|
|
367
|
-
return
|
|
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
|
|
373
|
-
if (
|
|
374
|
-
return
|
|
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
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
return
|
|
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
|
|
313
|
+
function validateEndpoint(url) {
|
|
387
314
|
try {
|
|
388
|
-
const
|
|
389
|
-
return
|
|
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
|
|
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(
|
|
344
|
+
await session.send(msg);
|
|
398
345
|
}
|
|
399
346
|
catch (e) {
|
|
400
|
-
logger.error('
|
|
347
|
+
logger.error('发送失败', e);
|
|
401
348
|
}
|
|
402
349
|
}
|
|
403
|
-
function
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
502
|
-
|
|
503
|
-
|
|
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,
|
|
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 (!
|
|
514
|
-
await safeSend(session, cfg.messages.fail + '
|
|
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
|
-
|
|
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
|
|
414
|
+
let res = (await (0, axios_1.default)(config)).data;
|
|
538
415
|
if (cfg.pollEnabled && api.taskIdPath) {
|
|
539
|
-
const taskId = getValueByPath(
|
|
416
|
+
const taskId = getValueByPath(res, api.taskIdPath);
|
|
540
417
|
if (taskId) {
|
|
541
|
-
const pollUrl = api.pollUrlTemplate.replace('{
|
|
542
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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 +
|
|
428
|
+
await safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
|
|
429
|
+
return null;
|
|
559
430
|
}
|
|
560
431
|
}
|
|
561
|
-
async function
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
455
|
+
if (videoUrls.length === 1 || !cfg.enableForward) {
|
|
456
|
+
for (const url of videoUrls) {
|
|
457
|
+
await sendSingleVideo(session, url);
|
|
458
|
+
}
|
|
570
459
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
535
|
+
return generateVideos(session, collect.prompt || '默认', collect.imageUrl, cfg.maxVideos);
|
|
609
536
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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 (
|
|
641
|
-
return safeSend(session, cfg.messages.
|
|
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
|
-
|
|
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
|
|
650
|
-
return
|
|
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>'
|
|
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(
|
|
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
|
|
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>'
|
|
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(
|
|
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
|
|
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)
|