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.
- package/lib/index.d.ts +17 -0
- package/lib/index.js +332 -423
- 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('
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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('
|
|
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('分辨率'),
|
|
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)
|
|
70
|
-
endpoint: koishi_1.Schema.string().default('https://
|
|
71
|
-
apiKey: koishi_1.Schema.string().role('secret').default('')
|
|
72
|
-
model: koishi_1.Schema.string().default('video-
|
|
73
|
-
img2videoModel: koishi_1.Schema.string().default('')
|
|
74
|
-
videoDuration: koishi_1.Schema.number().default(0)
|
|
75
|
-
videoResolution: koishi_1.Schema.string().default('')
|
|
76
|
-
txt2videoPrompt: koishi_1.Schema.string().default('')
|
|
77
|
-
img2videoPrompt: koishi_1.Schema.string().default('')
|
|
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}',
|
|
84
|
-
img2videoBody: { model: '{model}', prompt: '{prompt}',
|
|
85
|
-
responseVideoPath: '
|
|
86
|
-
pollUrlTemplate: '
|
|
87
|
-
taskIdPath: '
|
|
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('自定义请求体
|
|
90
|
-
})).default([]).description('自定义
|
|
91
|
-
}).description('自定义
|
|
83
|
+
.description('自定义请求体'),
|
|
84
|
+
})).default([]).description('自定义API列表'),
|
|
85
|
+
}).description('自定义API配置'),
|
|
92
86
|
koishi_1.Schema.object({
|
|
93
|
-
blacklistAdmins: koishi_1.Schema.array(String).default([]).description('
|
|
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('视频生成中,请耐心等待...')
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
144
|
-
while (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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://
|
|
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-
|
|
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(
|
|
264
|
-
if (entries.length
|
|
234
|
+
const entries = cfg.customApiList.filter(e => e.enable);
|
|
235
|
+
if (!entries.length)
|
|
265
236
|
return null;
|
|
266
|
-
const apis = entries
|
|
267
|
-
|
|
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
|
-
|
|
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,
|
|
295
|
-
if (!obj || !
|
|
262
|
+
function getValueByPath(obj, path) {
|
|
263
|
+
if (!obj || !path)
|
|
296
264
|
return undefined;
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
+
return cur;
|
|
329
273
|
}
|
|
330
|
-
|
|
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
|
|
366
|
-
if (/^https?:\/\/.+\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i.test(
|
|
367
|
-
return
|
|
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
|
|
373
|
-
if (
|
|
374
|
-
return
|
|
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
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
return
|
|
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
|
|
299
|
+
function validateEndpoint(url) {
|
|
387
300
|
try {
|
|
388
|
-
const
|
|
389
|
-
return
|
|
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
|
|
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(
|
|
330
|
+
await session.send(msg);
|
|
398
331
|
}
|
|
399
332
|
catch (e) {
|
|
400
|
-
logger.error('
|
|
333
|
+
logger.error('发送失败', e);
|
|
401
334
|
}
|
|
402
335
|
}
|
|
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}`;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
502
|
-
if (isImg2Video)
|
|
503
|
-
|
|
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,
|
|
374
|
+
body = resolveTemplate(bodyTemplate, vars);
|
|
508
375
|
}
|
|
509
376
|
catch {
|
|
510
|
-
|
|
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
|
-
|
|
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
|
|
392
|
+
let res = (await (0, axios_1.default)(config)).data;
|
|
538
393
|
if (cfg.pollEnabled && api.taskIdPath) {
|
|
539
|
-
const taskId = getValueByPath(
|
|
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
|
-
|
|
397
|
+
safeSend(session, cfg.messages.pollWaiting);
|
|
398
|
+
res = await pollForResult(pollUrl, api.headers, cfg.pollInterval, cfg.pollTimeout);
|
|
543
399
|
}
|
|
544
400
|
}
|
|
545
|
-
|
|
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
|
-
|
|
411
|
+
safeSend(session, cfg.messages.fail + ' [' + (err.message?.slice(0, 100) || '未知错误') + ']');
|
|
559
412
|
}
|
|
560
413
|
}
|
|
561
|
-
async function generateVideo(session, prompt, imageUrl = ''
|
|
562
|
-
if (!checkRateLimit())
|
|
563
|
-
|
|
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
|
-
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
418
|
+
if (!api)
|
|
419
|
+
return safeSend(session, cfg.messages.noApi);
|
|
571
420
|
recordApiCall();
|
|
572
|
-
return customGenerateVideo(session, api, prompt, imageUrl
|
|
421
|
+
return customGenerateVideo(session, api, prompt, imageUrl);
|
|
573
422
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
481
|
+
return generateVideo(session, collect.prompt || '默认', collect.imageUrl);
|
|
609
482
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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 (
|
|
641
|
-
return safeSend(session, cfg.messages.
|
|
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
|
-
|
|
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
|
|
650
|
-
return
|
|
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>'
|
|
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(
|
|
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
|
|
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>'
|
|
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(
|
|
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
|
|
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)
|