koishi-plugin-custom-image 0.2.1 → 0.2.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 +1 -0
- package/lib/index.js +78 -34
- package/package.json +5 -13
- package/src/index.ts +0 -471
- package/src/locales/zh-CN.yml +0 -16
- package/src/shims.d.ts +0 -9
package/lib/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Config {
|
|
|
17
17
|
downloadImageBeforeSend: boolean;
|
|
18
18
|
messageBufferDelay: number;
|
|
19
19
|
tempDir: string;
|
|
20
|
+
useImageUrlInsteadOfFile: boolean;
|
|
20
21
|
}
|
|
21
22
|
export declare const Config: Schema<Config>;
|
|
22
23
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -12,8 +12,26 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
const promises_1 = require("stream/promises");
|
|
14
14
|
const worker_threads_1 = require("worker_threads");
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const locales = {
|
|
16
|
+
'zh-CN': {
|
|
17
|
+
'messages.repeatRequest': '请求过于频繁,请稍后再试',
|
|
18
|
+
'messages.fetchFailed': '图片获取失败',
|
|
19
|
+
'messages.partialFailed': '部分图片获取失败:',
|
|
20
|
+
'messages.allFailed': '所有图片获取失败:',
|
|
21
|
+
'messages.fetchSuccess': '图片获取成功!',
|
|
22
|
+
'messages.fetchWaiting': '正在获取图片,请稍候...',
|
|
23
|
+
'messages.inputError': '输入内容不能为空!',
|
|
24
|
+
'messages.cacheCleared': '✅ 图片缓存已清空',
|
|
25
|
+
'commands.random-image': '随机获取图片',
|
|
26
|
+
'commands.random-image.options.api': '指定图片API地址',
|
|
27
|
+
'commands.hsjp': '生成黑丝举牌图片',
|
|
28
|
+
'commands.dmjp': '生成动漫举牌图片',
|
|
29
|
+
'commands.clear-image-cache': '清空图片缓存'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const currentFilePath = worker_threads_1.isMainThread
|
|
33
|
+
? __filename
|
|
34
|
+
: path_1.default.join(process.cwd(), worker_threads_1.isMainThread ? 'src/index.ts' : 'lib/index.js');
|
|
17
35
|
exports.name = 'custom-image';
|
|
18
36
|
exports.Config = koishi_1.Schema.object({
|
|
19
37
|
enable: koishi_1.Schema.boolean()
|
|
@@ -63,15 +81,18 @@ exports.Config = koishi_1.Schema.object({
|
|
|
63
81
|
.min(0)
|
|
64
82
|
.description('【缓存设置】自动清理缓存间隔(分钟)'),
|
|
65
83
|
downloadImageBeforeSend: koishi_1.Schema.boolean()
|
|
66
|
-
.default(
|
|
67
|
-
.description('
|
|
84
|
+
.default(false)
|
|
85
|
+
.description('【展示设置】发送前下载图片(关闭可解决OneBot文件上传问题)'),
|
|
68
86
|
messageBufferDelay: koishi_1.Schema.number()
|
|
69
87
|
.default(0)
|
|
70
88
|
.min(0)
|
|
71
89
|
.description('【性能设置】消息缓冲延迟:合并短时间内的多个图片请求(秒)'),
|
|
72
90
|
tempDir: koishi_1.Schema.string()
|
|
73
91
|
.default(path_1.default.join(process.cwd(), 'temp_images'))
|
|
74
|
-
.description('【文件设置】临时图片保存目录')
|
|
92
|
+
.description('【文件设置】临时图片保存目录'),
|
|
93
|
+
useImageUrlInsteadOfFile: koishi_1.Schema.boolean()
|
|
94
|
+
.default(true)
|
|
95
|
+
.description('【兼容设置】强制使用图片URL发送(避免OneBot文件上传错误)')
|
|
75
96
|
});
|
|
76
97
|
if (!worker_threads_1.isMainThread) {
|
|
77
98
|
const { url, filePath } = worker_threads_1.workerData;
|
|
@@ -97,8 +118,17 @@ if (!worker_threads_1.isMainThread) {
|
|
|
97
118
|
const processedApi = new Map();
|
|
98
119
|
const imageBuffer = new Map();
|
|
99
120
|
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
121
|
+
function getI18nText(session, key) {
|
|
122
|
+
if (session?.text) {
|
|
123
|
+
return session.text(key);
|
|
124
|
+
}
|
|
125
|
+
const lang = 'zh-CN';
|
|
126
|
+
return locales[lang][key] || key;
|
|
127
|
+
}
|
|
100
128
|
async function sendTimeout(session, content, config) {
|
|
101
|
-
const text = typeof content === 'string'
|
|
129
|
+
const text = typeof content === 'string'
|
|
130
|
+
? getI18nText(session, content)
|
|
131
|
+
: content;
|
|
102
132
|
if (config.imageSendTimeout <= 0) {
|
|
103
133
|
return session.send(text).catch(() => null);
|
|
104
134
|
}
|
|
@@ -110,7 +140,7 @@ async function sendTimeout(session, content, config) {
|
|
|
110
140
|
async function downloadImageWithThreads(url, filename, config) {
|
|
111
141
|
return new Promise((resolve, reject) => {
|
|
112
142
|
if (!fs_1.default.existsSync(config.tempDir)) {
|
|
113
|
-
fs_1.default.mkdirSync(config.tempDir, { recursive: true });
|
|
143
|
+
fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
|
|
114
144
|
}
|
|
115
145
|
const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
|
|
116
146
|
const worker = new worker_threads_1.Worker(currentFilePath, { workerData: { url, filePath } });
|
|
@@ -194,20 +224,24 @@ async function processCustomImage(session, apiUrl, config) {
|
|
|
194
224
|
const hash = crypto_1.default.createHash('md5').update(apiUrl).digest('hex');
|
|
195
225
|
const now = Date.now();
|
|
196
226
|
if (processedApi.get(hash) && now - processedApi.get(hash) < config.sameImageApiInterval * 1000) {
|
|
197
|
-
return { success: false, msg: session
|
|
227
|
+
return { success: false, msg: getI18nText(session, 'messages.repeatRequest') };
|
|
198
228
|
}
|
|
199
229
|
processedApi.set(hash, now);
|
|
200
230
|
const result = await fetchImage(apiUrl, config);
|
|
201
231
|
if (!result.success) {
|
|
202
|
-
return { success: false, msg: session
|
|
232
|
+
return { success: false, msg: getI18nText(session, 'messages.fetchFailed') };
|
|
203
233
|
}
|
|
204
234
|
let imageElem;
|
|
205
|
-
if (
|
|
235
|
+
if (config.useImageUrlInsteadOfFile) {
|
|
236
|
+
imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
|
|
237
|
+
}
|
|
238
|
+
else if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
206
239
|
try {
|
|
207
240
|
const filename = crypto_1.default.createHash('md5').update(result.data).digest('hex');
|
|
208
241
|
const filePath = await downloadImageWithThreads(result.data, filename, config);
|
|
209
242
|
const absPath = path_1.default.resolve(filePath);
|
|
210
|
-
|
|
243
|
+
fs_1.default.chmodSync(absPath, 0o777);
|
|
244
|
+
imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
|
|
211
245
|
}
|
|
212
246
|
catch (error) {
|
|
213
247
|
imageElem = koishi_1.h.image(result.data);
|
|
@@ -215,18 +249,18 @@ async function processCustomImage(session, apiUrl, config) {
|
|
|
215
249
|
}
|
|
216
250
|
else if (result.type === 'buffer') {
|
|
217
251
|
if (!fs_1.default.existsSync(config.tempDir)) {
|
|
218
|
-
fs_1.default.mkdirSync(config.tempDir, { recursive: true });
|
|
252
|
+
fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
|
|
219
253
|
}
|
|
220
254
|
const filename = crypto_1.default.randomUUID();
|
|
221
255
|
const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
|
|
222
256
|
const absPath = path_1.default.resolve(filePath);
|
|
223
|
-
fs_1.default.writeFileSync(absPath, result.data);
|
|
224
|
-
imageElem = koishi_1.h.file(`file
|
|
257
|
+
fs_1.default.writeFileSync(absPath, result.data, { mode: 0o777 });
|
|
258
|
+
imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
|
|
225
259
|
}
|
|
226
260
|
else {
|
|
227
261
|
imageElem = koishi_1.h.image(result.data);
|
|
228
262
|
}
|
|
229
|
-
return { success: true, msg: 'ok', data: { text: session
|
|
263
|
+
return { success: true, msg: 'ok', data: { text: getI18nText(session, 'messages.fetchSuccess'), image: imageElem } };
|
|
230
264
|
}
|
|
231
265
|
async function flush(session, config, manualApi) {
|
|
232
266
|
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
@@ -250,12 +284,12 @@ async function flush(session, config, manualApi) {
|
|
|
250
284
|
}
|
|
251
285
|
}
|
|
252
286
|
if (errs.length && !config.ignoreSendError) {
|
|
253
|
-
const errorMsg = `${session
|
|
287
|
+
const errorMsg = `${getI18nText(session, 'messages.partialFailed')}\n${errs.join('\n')}`;
|
|
254
288
|
await sendTimeout(session, errorMsg, config);
|
|
255
289
|
await delay(600);
|
|
256
290
|
}
|
|
257
291
|
if (items.length === 0) {
|
|
258
|
-
const failMsg = `${session
|
|
292
|
+
const failMsg = `${getI18nText(session, 'messages.allFailed')}\n${errs.join('\n')}`;
|
|
259
293
|
if (!config.ignoreSendError) {
|
|
260
294
|
await sendTimeout(session, failMsg, config);
|
|
261
295
|
}
|
|
@@ -271,11 +305,13 @@ async function flush(session, config, manualApi) {
|
|
|
271
305
|
function apply(ctx, config) {
|
|
272
306
|
if (!worker_threads_1.isMainThread)
|
|
273
307
|
return;
|
|
274
|
-
|
|
308
|
+
Object.keys(locales).forEach(lang => {
|
|
309
|
+
ctx.i18n.define(lang, locales[lang]);
|
|
310
|
+
});
|
|
275
311
|
clearAllCache(config);
|
|
276
312
|
ctx.logger.info('[custom-image] 插件已加载');
|
|
277
|
-
ctx.command('random-image [apis...]', '
|
|
278
|
-
.option('api',
|
|
313
|
+
ctx.command('random-image [apis...]', locales['zh-CN']['commands.random-image'])
|
|
314
|
+
.option('api', `-a <api> ${locales['zh-CN']['commands.random-image.options.api']}`)
|
|
279
315
|
.action(async ({ session, options = {} }, ...apis) => {
|
|
280
316
|
if (!config.enable || !config.customImageEnabled || !session)
|
|
281
317
|
return;
|
|
@@ -304,7 +340,7 @@ function apply(ctx, config) {
|
|
|
304
340
|
}
|
|
305
341
|
}
|
|
306
342
|
});
|
|
307
|
-
ctx.command('hsjp <msg> [msg1] [msg2]', '
|
|
343
|
+
ctx.command('hsjp <msg> [msg1] [msg2]', locales['zh-CN']['commands.hsjp'])
|
|
308
344
|
.action(async ({ session }, msg, msg1 = '', msg2 = '') => {
|
|
309
345
|
if (!config.enable || !config.hsjpEnabled || !session)
|
|
310
346
|
return;
|
|
@@ -318,12 +354,16 @@ function apply(ctx, config) {
|
|
|
318
354
|
const result = await fetchHsjpImage(msg, msg1, msg2, config);
|
|
319
355
|
if (result.success) {
|
|
320
356
|
let imageElem;
|
|
321
|
-
if (
|
|
357
|
+
if (config.useImageUrlInsteadOfFile) {
|
|
358
|
+
imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
|
|
359
|
+
}
|
|
360
|
+
else if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
322
361
|
try {
|
|
323
362
|
const filename = crypto_1.default.createHash('md5').update(result.data).digest('hex');
|
|
324
363
|
const filePath = await downloadImageWithThreads(result.data, filename, config);
|
|
325
364
|
const absPath = path_1.default.resolve(filePath);
|
|
326
|
-
|
|
365
|
+
fs_1.default.chmodSync(absPath, 0o777);
|
|
366
|
+
imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
|
|
327
367
|
}
|
|
328
368
|
catch (error) {
|
|
329
369
|
imageElem = koishi_1.h.image(result.data);
|
|
@@ -331,13 +371,13 @@ function apply(ctx, config) {
|
|
|
331
371
|
}
|
|
332
372
|
else if (result.type === 'buffer') {
|
|
333
373
|
if (!fs_1.default.existsSync(config.tempDir)) {
|
|
334
|
-
fs_1.default.mkdirSync(config.tempDir, { recursive: true });
|
|
374
|
+
fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
|
|
335
375
|
}
|
|
336
376
|
const filename = crypto_1.default.randomUUID();
|
|
337
377
|
const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
|
|
338
378
|
const absPath = path_1.default.resolve(filePath);
|
|
339
|
-
fs_1.default.writeFileSync(absPath, result.data);
|
|
340
|
-
imageElem = koishi_1.h.file(`file
|
|
379
|
+
fs_1.default.writeFileSync(absPath, result.data, { mode: 0o777 });
|
|
380
|
+
imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
|
|
341
381
|
}
|
|
342
382
|
else {
|
|
343
383
|
imageElem = koishi_1.h.image(result.data);
|
|
@@ -346,7 +386,7 @@ function apply(ctx, config) {
|
|
|
346
386
|
await sendTimeout(session, imageElem, config);
|
|
347
387
|
}
|
|
348
388
|
});
|
|
349
|
-
ctx.command('dmjp <text>', '
|
|
389
|
+
ctx.command('dmjp <text>', locales['zh-CN']['commands.dmjp'])
|
|
350
390
|
.action(async ({ session }, text) => {
|
|
351
391
|
if (!config.enable || !config.dmjpEnabled || !session)
|
|
352
392
|
return;
|
|
@@ -360,12 +400,16 @@ function apply(ctx, config) {
|
|
|
360
400
|
const result = await fetchDmjpImage(text, config);
|
|
361
401
|
if (result.success) {
|
|
362
402
|
let imageElem;
|
|
363
|
-
if (
|
|
403
|
+
if (config.useImageUrlInsteadOfFile) {
|
|
404
|
+
imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
|
|
405
|
+
}
|
|
406
|
+
else if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
364
407
|
try {
|
|
365
408
|
const filename = crypto_1.default.createHash('md5').update(result.data).digest('hex');
|
|
366
409
|
const filePath = await downloadImageWithThreads(result.data, filename, config);
|
|
367
410
|
const absPath = path_1.default.resolve(filePath);
|
|
368
|
-
|
|
411
|
+
fs_1.default.chmodSync(absPath, 0o777);
|
|
412
|
+
imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
|
|
369
413
|
}
|
|
370
414
|
catch (error) {
|
|
371
415
|
imageElem = koishi_1.h.image(result.data);
|
|
@@ -373,13 +417,13 @@ function apply(ctx, config) {
|
|
|
373
417
|
}
|
|
374
418
|
else if (result.type === 'buffer') {
|
|
375
419
|
if (!fs_1.default.existsSync(config.tempDir)) {
|
|
376
|
-
fs_1.default.mkdirSync(config.tempDir, { recursive: true });
|
|
420
|
+
fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
|
|
377
421
|
}
|
|
378
422
|
const filename = crypto_1.default.randomUUID();
|
|
379
423
|
const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
|
|
380
424
|
const absPath = path_1.default.resolve(filePath);
|
|
381
|
-
fs_1.default.writeFileSync(absPath, result.data);
|
|
382
|
-
imageElem = koishi_1.h.file(`file
|
|
425
|
+
fs_1.default.writeFileSync(absPath, result.data, { mode: 0o777 });
|
|
426
|
+
imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
|
|
383
427
|
}
|
|
384
428
|
else {
|
|
385
429
|
imageElem = koishi_1.h.image(result.data);
|
|
@@ -388,10 +432,10 @@ function apply(ctx, config) {
|
|
|
388
432
|
await sendTimeout(session, imageElem, config);
|
|
389
433
|
}
|
|
390
434
|
});
|
|
391
|
-
ctx.command('clear-image-cache', '
|
|
435
|
+
ctx.command('clear-image-cache', locales['zh-CN']['commands.clear-image-cache'])
|
|
392
436
|
.action(({ session }) => {
|
|
393
437
|
clearAllCache(config);
|
|
394
|
-
return session ? session
|
|
438
|
+
return session ? getI18nText(session, 'messages.cacheCleared') : '✅ 图片缓存已清空';
|
|
395
439
|
});
|
|
396
440
|
setInterval(() => {
|
|
397
441
|
const now = Date.now();
|
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-custom-image",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Koishi自定义图片API插件(內置支持黑丝/动漫举牌)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"lib"
|
|
9
|
-
"src",
|
|
10
|
-
"src/locales"
|
|
8
|
+
"lib"
|
|
11
9
|
],
|
|
12
10
|
"scripts": {
|
|
13
11
|
"build": "tsc",
|
|
@@ -18,22 +16,16 @@
|
|
|
18
16
|
"koishi": "^4.0.0"
|
|
19
17
|
},
|
|
20
18
|
"dependencies": {
|
|
21
|
-
"axios": "^1.6.8"
|
|
22
|
-
"yaml": "^2.4.1"
|
|
19
|
+
"axios": "^1.6.8"
|
|
23
20
|
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"typescript": "^5.3.3",
|
|
26
|
-
"@types/node": "^20.11.17"
|
|
27
|
-
"@koishijs/typings": "^4.0.0",
|
|
28
|
-
"@types/yaml": "^1.9.7"
|
|
23
|
+
"@types/node": "^20.11.17"
|
|
29
24
|
},
|
|
30
25
|
"koishi": {
|
|
31
26
|
"description": {
|
|
32
27
|
"zh-CN": "自定义图片API调用 + 內置黑丝/动漫举牌功能"
|
|
33
|
-
}
|
|
34
|
-
"locales": [
|
|
35
|
-
"zh-CN"
|
|
36
|
-
]
|
|
28
|
+
}
|
|
37
29
|
},
|
|
38
30
|
"keywords": [
|
|
39
31
|
"koishi",
|
package/src/index.ts
DELETED
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
import { Context, Schema, h, Session } from 'koishi'
|
|
2
|
-
import axios from 'axios'
|
|
3
|
-
import crypto from 'crypto'
|
|
4
|
-
import fs from 'fs'
|
|
5
|
-
import path from 'path'
|
|
6
|
-
import { pipeline } from 'stream/promises'
|
|
7
|
-
import { isMainThread, Worker, workerData, parentPort } from 'worker_threads'
|
|
8
|
-
|
|
9
|
-
import zhCN from './locales/zh-CN.yml'
|
|
10
|
-
|
|
11
|
-
const currentFilePath = path.join(process.cwd(), 'src', 'index.ts');
|
|
12
|
-
|
|
13
|
-
export const name = 'custom-image'
|
|
14
|
-
|
|
15
|
-
export interface Config {
|
|
16
|
-
enable: boolean
|
|
17
|
-
showWaitingTip: boolean
|
|
18
|
-
sameImageApiInterval: number
|
|
19
|
-
timeout: number
|
|
20
|
-
customImageApis: string[]
|
|
21
|
-
customImageEnabled: boolean
|
|
22
|
-
hsjpEnabled: boolean
|
|
23
|
-
dmjpEnabled: boolean
|
|
24
|
-
ignoreSendError: boolean
|
|
25
|
-
retryTimes: number
|
|
26
|
-
retryInterval: number
|
|
27
|
-
imageSendTimeout: number
|
|
28
|
-
autoClearCacheInterval: number
|
|
29
|
-
downloadImageBeforeSend: boolean
|
|
30
|
-
messageBufferDelay: number
|
|
31
|
-
tempDir: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const Config: Schema<Config> = Schema.object({
|
|
35
|
-
enable: Schema.boolean()
|
|
36
|
-
.default(true)
|
|
37
|
-
.description('【基础设置】启用插件'),
|
|
38
|
-
showWaitingTip: Schema.boolean()
|
|
39
|
-
.default(true)
|
|
40
|
-
.description('【基础设置】请求图片时显示等待提示'),
|
|
41
|
-
sameImageApiInterval: Schema.number()
|
|
42
|
-
.default(60)
|
|
43
|
-
.min(0)
|
|
44
|
-
.description('【基础设置】重复API调用间隔(秒)'),
|
|
45
|
-
timeout: Schema.number()
|
|
46
|
-
.default(10000)
|
|
47
|
-
.min(0)
|
|
48
|
-
.description('【基础设置】API请求超时时间(毫秒)'),
|
|
49
|
-
customImageApis: Schema.array(Schema.string())
|
|
50
|
-
.default([])
|
|
51
|
-
.role('textarea')
|
|
52
|
-
.description('【基础设置】自定义图片API列表(每行一个地址)'),
|
|
53
|
-
customImageEnabled: Schema.boolean()
|
|
54
|
-
.default(true)
|
|
55
|
-
.description('【功能开关】启用自定义图片API功能'),
|
|
56
|
-
hsjpEnabled: Schema.boolean()
|
|
57
|
-
.default(true)
|
|
58
|
-
.description('【功能开关】启用黑丝举牌功能'),
|
|
59
|
-
dmjpEnabled: Schema.boolean()
|
|
60
|
-
.default(true)
|
|
61
|
-
.description('【功能开关】启用动漫举牌功能'),
|
|
62
|
-
ignoreSendError: Schema.boolean()
|
|
63
|
-
.default(true)
|
|
64
|
-
.description('【容错设置】忽略发送错误'),
|
|
65
|
-
retryTimes: Schema.number()
|
|
66
|
-
.default(1)
|
|
67
|
-
.min(0)
|
|
68
|
-
.description('【容错设置】API重试次数'),
|
|
69
|
-
retryInterval: Schema.number()
|
|
70
|
-
.default(500)
|
|
71
|
-
.min(0)
|
|
72
|
-
.description('【容错设置】重试间隔(毫秒)'),
|
|
73
|
-
imageSendTimeout: Schema.number()
|
|
74
|
-
.default(15000)
|
|
75
|
-
.min(0)
|
|
76
|
-
.description('【容错设置】图片发送超时(毫秒)'),
|
|
77
|
-
autoClearCacheInterval: Schema.number()
|
|
78
|
-
.default(60)
|
|
79
|
-
.min(0)
|
|
80
|
-
.description('【缓存设置】自动清理缓存间隔(分钟)'),
|
|
81
|
-
downloadImageBeforeSend: Schema.boolean()
|
|
82
|
-
.default(true)
|
|
83
|
-
.description('【展示设置】发送前下载图片:发送前先下载图片到本地,再发送文件(仅OneBot)'),
|
|
84
|
-
messageBufferDelay: Schema.number()
|
|
85
|
-
.default(0)
|
|
86
|
-
.min(0)
|
|
87
|
-
.description('【性能设置】消息缓冲延迟:合并短时间内的多个图片请求(秒)'),
|
|
88
|
-
tempDir: Schema.string()
|
|
89
|
-
.default(path.join(process.cwd(), 'temp_images'))
|
|
90
|
-
.description('【文件设置】临时图片保存目录')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
if (!isMainThread) {
|
|
94
|
-
const { url, filePath } = workerData;
|
|
95
|
-
(async () => {
|
|
96
|
-
try {
|
|
97
|
-
const response = await axios({
|
|
98
|
-
url,
|
|
99
|
-
method: 'GET',
|
|
100
|
-
responseType: 'stream',
|
|
101
|
-
timeout: 60000,
|
|
102
|
-
headers: {
|
|
103
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await pipeline(response.data, fs.createWriteStream(filePath));
|
|
108
|
-
parentPort?.postMessage({ success: true, filePath });
|
|
109
|
-
} catch (error) {
|
|
110
|
-
parentPort?.postMessage({ success: false, error: (error as Error).message });
|
|
111
|
-
}
|
|
112
|
-
})();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const processedApi = new Map<string, number>()
|
|
116
|
-
const imageBuffer = new Map<string, { apis: string[], timer: NodeJS.Timeout, tipMsgId?: string | number }>()
|
|
117
|
-
const delay = (ms: number) => new Promise(r => setTimeout(r, ms))
|
|
118
|
-
|
|
119
|
-
async function sendTimeout(session: Session, content: string | any, config: Config) {
|
|
120
|
-
const text = typeof content === 'string' ? session.text(content) : content;
|
|
121
|
-
if (config.imageSendTimeout <= 0) {
|
|
122
|
-
return session.send(text).catch(() => null);
|
|
123
|
-
}
|
|
124
|
-
return Promise.race([
|
|
125
|
-
session.send(text),
|
|
126
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.imageSendTimeout))
|
|
127
|
-
]).catch(() => null);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function downloadImageWithThreads(url: string, filename: string, config: Config): Promise<string> {
|
|
131
|
-
return new Promise((resolve, reject) => {
|
|
132
|
-
if (!fs.existsSync(config.tempDir)) {
|
|
133
|
-
fs.mkdirSync(config.tempDir, { recursive: true });
|
|
134
|
-
}
|
|
135
|
-
const filePath = path.join(config.tempDir, `${filename}.jpg`);
|
|
136
|
-
|
|
137
|
-
const worker = new Worker(currentFilePath, { workerData: { url, filePath } });
|
|
138
|
-
worker.on('message', (result: { success: boolean, filePath?: string, error?: string }) => {
|
|
139
|
-
if (result.success && result.filePath) {
|
|
140
|
-
resolve(result.filePath);
|
|
141
|
-
} else {
|
|
142
|
-
reject(new Error(result.error || '图片获取失败'));
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
worker.on('error', reject);
|
|
146
|
-
worker.on('exit', (code: number) => {
|
|
147
|
-
if (code !== 0) reject(new Error('图片下载线程异常'));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function clearAllCache(config: Config) {
|
|
153
|
-
processedApi.clear();
|
|
154
|
-
imageBuffer.forEach(buf => clearTimeout(buf.timer));
|
|
155
|
-
imageBuffer.clear();
|
|
156
|
-
|
|
157
|
-
if (fs.existsSync(config.tempDir)) {
|
|
158
|
-
fs.readdirSync(config.tempDir).forEach(file => {
|
|
159
|
-
try {
|
|
160
|
-
const filePath = path.join(config.tempDir, file);
|
|
161
|
-
const stat = fs.statSync(filePath);
|
|
162
|
-
if (Date.now() - stat.mtimeMs > 3600000) {
|
|
163
|
-
fs.unlinkSync(filePath);
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function randomSelectApi(customImageApis: string[]): string | null {
|
|
172
|
-
const validApis = customImageApis.filter(api => api.trim());
|
|
173
|
-
if (validApis.length === 0) return null;
|
|
174
|
-
return validApis[Math.floor(Math.random() * validApis.length)];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function fetchImage(url: string, config: Config) {
|
|
178
|
-
const http = axios.create({
|
|
179
|
-
timeout: config.timeout,
|
|
180
|
-
headers: {
|
|
181
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
for (let retry = 0; retry <= config.retryTimes; retry++) {
|
|
186
|
-
try {
|
|
187
|
-
if (/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url)) {
|
|
188
|
-
return { success: true, type: 'url', data: url };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const res = await http.get(url, { responseType: 'arraybuffer' });
|
|
192
|
-
if (res.status === 200 && res.data) {
|
|
193
|
-
return { success: true, type: 'buffer', data: res.data };
|
|
194
|
-
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (retry === config.retryTimes) return { success: false, type: '', data: '' };
|
|
197
|
-
await delay(config.retryInterval);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return { success: false, type: '', data: '' };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function fetchHsjpImage(msg: string, msg1: string, msg2: string, config: Config) {
|
|
205
|
-
const encodedMsg = encodeURIComponent(msg);
|
|
206
|
-
const encodedMsg1 = encodeURIComponent(msg1);
|
|
207
|
-
const encodedMsg2 = encodeURIComponent(msg2);
|
|
208
|
-
const url = `https://api.suyanw.cn/api/hsjp/?msg=${encodedMsg}&msg1=${encodedMsg1}&msg2=${encodedMsg2}`;
|
|
209
|
-
return fetchImage(url, config);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function fetchDmjpImage(text: string, config: Config) {
|
|
213
|
-
const encodedText = encodeURIComponent(text);
|
|
214
|
-
const url = `https://api.suyanw.cn/api/dmjp.php?text=${encodedText}`;
|
|
215
|
-
return fetchImage(url, config);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function processCustomImage(session: Session, apiUrl: string, config: Config) {
|
|
219
|
-
const hash = crypto.createHash('md5').update(apiUrl).digest('hex');
|
|
220
|
-
const now = Date.now();
|
|
221
|
-
|
|
222
|
-
if (processedApi.get(hash) && now - processedApi.get(hash)! < config.sameImageApiInterval * 1000) {
|
|
223
|
-
return { success: false, msg: session.text('messages.repeatRequest') };
|
|
224
|
-
}
|
|
225
|
-
processedApi.set(hash, now);
|
|
226
|
-
|
|
227
|
-
const result = await fetchImage(apiUrl, config);
|
|
228
|
-
if (!result.success) {
|
|
229
|
-
return { success: false, msg: session.text('messages.fetchFailed') };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let imageElem: any;
|
|
233
|
-
if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
234
|
-
try {
|
|
235
|
-
const filename = crypto.createHash('md5').update(result.data as string).digest('hex');
|
|
236
|
-
const filePath = await downloadImageWithThreads(result.data as string, filename, config);
|
|
237
|
-
const absPath = path.resolve(filePath);
|
|
238
|
-
imageElem = h.file(`file:///${absPath.replace(/\\/g, '/')}`);
|
|
239
|
-
} catch (error) {
|
|
240
|
-
imageElem = h.image(result.data as string);
|
|
241
|
-
}
|
|
242
|
-
} else if (result.type === 'buffer') {
|
|
243
|
-
if (!fs.existsSync(config.tempDir)) {
|
|
244
|
-
fs.mkdirSync(config.tempDir, { recursive: true });
|
|
245
|
-
}
|
|
246
|
-
const filename = crypto.randomUUID();
|
|
247
|
-
const filePath = path.join(config.tempDir, `${filename}.jpg`);
|
|
248
|
-
const absPath = path.resolve(filePath);
|
|
249
|
-
fs.writeFileSync(absPath, result.data as Buffer);
|
|
250
|
-
imageElem = h.file(`file:///${absPath.replace(/\\/g, '/')}`);
|
|
251
|
-
} else {
|
|
252
|
-
imageElem = h.image(result.data as string);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return { success: true, msg: 'ok', data: { text: session.text('messages.fetchSuccess'), image: imageElem } };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function flush(session: Session, config: Config, manualApi?: string) {
|
|
259
|
-
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
260
|
-
const buf = imageBuffer.get(key);
|
|
261
|
-
const apis = manualApi ? [manualApi] : buf?.apis || [];
|
|
262
|
-
|
|
263
|
-
if (buf) {
|
|
264
|
-
clearTimeout(buf.timer);
|
|
265
|
-
imageBuffer.delete(key);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!config.customImageEnabled || !apis.length) return;
|
|
269
|
-
|
|
270
|
-
const items: any[] = [];
|
|
271
|
-
const errs: string[] = [];
|
|
272
|
-
|
|
273
|
-
for (const apiUrl of apis) {
|
|
274
|
-
const result = await processCustomImage(session, apiUrl, config);
|
|
275
|
-
if (result.success && result.data) {
|
|
276
|
-
items.push(result.data);
|
|
277
|
-
} else {
|
|
278
|
-
errs.push(`【${apiUrl.slice(0, 22)}...】:${result.msg}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (errs.length && !config.ignoreSendError) {
|
|
283
|
-
const errorMsg = `${session.text('messages.partialFailed')}\n${errs.join('\n')}`;
|
|
284
|
-
await sendTimeout(session, errorMsg, config);
|
|
285
|
-
await delay(600);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (items.length === 0) {
|
|
289
|
-
const failMsg = `${session.text('messages.allFailed')}\n${errs.join('\n')}`;
|
|
290
|
-
if (!config.ignoreSendError) {
|
|
291
|
-
await sendTimeout(session, failMsg, config);
|
|
292
|
-
}
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
for (const item of items) {
|
|
297
|
-
await sendTimeout(session, item.text, config);
|
|
298
|
-
await delay(300);
|
|
299
|
-
await sendTimeout(session, item.image, config);
|
|
300
|
-
await delay(1000);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function apply(ctx: Context, config: Config) {
|
|
305
|
-
if (!isMainThread) return;
|
|
306
|
-
|
|
307
|
-
ctx.i18n.define('zh-CN', zhCN);
|
|
308
|
-
|
|
309
|
-
clearAllCache(config);
|
|
310
|
-
ctx.logger.info('[custom-image] 插件已加载');
|
|
311
|
-
|
|
312
|
-
ctx.command('random-image [apis...]', '#commands.random-image')
|
|
313
|
-
.option('api', '-a <api> #commands.random-image.options.api')
|
|
314
|
-
.action(async ({ session, options = {} }, ...apis) => {
|
|
315
|
-
if (!config.enable || !config.customImageEnabled || !session) return;
|
|
316
|
-
|
|
317
|
-
const targetApi = options.api || (apis.length ? apis.join(' ') : null);
|
|
318
|
-
if (targetApi) {
|
|
319
|
-
await flush(session, config, targetApi);
|
|
320
|
-
} else {
|
|
321
|
-
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
322
|
-
|
|
323
|
-
let tipId;
|
|
324
|
-
if (config.showWaitingTip) {
|
|
325
|
-
const tip = await sendTimeout(session, 'messages.fetchWaiting', config);
|
|
326
|
-
tipId = (tip as any)?.messageId || (tip as any)?.id || tip;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (imageBuffer.has(key)) {
|
|
330
|
-
const b = imageBuffer.get(key)!;
|
|
331
|
-
clearTimeout(b.timer);
|
|
332
|
-
b.timer = setTimeout(() => flush(session, config), config.messageBufferDelay * 1000);
|
|
333
|
-
} else {
|
|
334
|
-
imageBuffer.set(key, {
|
|
335
|
-
apis: config.customImageApis,
|
|
336
|
-
timer: setTimeout(() => flush(session, config), config.messageBufferDelay * 1000),
|
|
337
|
-
tipMsgId: tipId
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
ctx.command('hsjp <msg> [msg1] [msg2]', '#commands.hsjp')
|
|
344
|
-
.action(async ({ session }, msg, msg1 = '', msg2 = '') => {
|
|
345
|
-
if (!config.enable || !config.hsjpEnabled || !session) return;
|
|
346
|
-
|
|
347
|
-
if (!msg || msg.trim().length === 0) {
|
|
348
|
-
await sendTimeout(session, 'messages.inputError', config);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (config.showWaitingTip) {
|
|
353
|
-
await sendTimeout(session, 'messages.fetchWaiting', config);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const result = await fetchHsjpImage(msg, msg1, msg2, config);
|
|
357
|
-
if (result.success) {
|
|
358
|
-
let imageElem: any;
|
|
359
|
-
if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
360
|
-
try {
|
|
361
|
-
const filename = crypto.createHash('md5').update(result.data as string).digest('hex');
|
|
362
|
-
const filePath = await downloadImageWithThreads(result.data as string, filename, config);
|
|
363
|
-
const absPath = path.resolve(filePath);
|
|
364
|
-
imageElem = h.file(`file:///${absPath.replace(/\\/g, '/')}`);
|
|
365
|
-
} catch (error) {
|
|
366
|
-
imageElem = h.image(result.data as string);
|
|
367
|
-
}
|
|
368
|
-
} else if (result.type === 'buffer') {
|
|
369
|
-
if (!fs.existsSync(config.tempDir)) {
|
|
370
|
-
fs.mkdirSync(config.tempDir, { recursive: true });
|
|
371
|
-
}
|
|
372
|
-
const filename = crypto.randomUUID();
|
|
373
|
-
const filePath = path.join(config.tempDir, `${filename}.jpg`);
|
|
374
|
-
const absPath = path.resolve(filePath);
|
|
375
|
-
fs.writeFileSync(absPath, result.data as Buffer);
|
|
376
|
-
imageElem = h.file(`file:///${absPath.replace(/\\/g, '/')}`);
|
|
377
|
-
} else {
|
|
378
|
-
imageElem = h.image(result.data as string);
|
|
379
|
-
}
|
|
380
|
-
await sendTimeout(session, 'messages.fetchSuccess', config);
|
|
381
|
-
await sendTimeout(session, imageElem, config);
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
ctx.command('dmjp <text>', '#commands.dmjp')
|
|
386
|
-
.action(async ({ session }, text) => {
|
|
387
|
-
if (!config.enable || !config.dmjpEnabled || !session) return;
|
|
388
|
-
|
|
389
|
-
if (!text || text.trim().length === 0) {
|
|
390
|
-
await sendTimeout(session, 'messages.inputError', config);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (config.showWaitingTip) {
|
|
395
|
-
await sendTimeout(session, 'messages.fetchWaiting', config);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const result = await fetchDmjpImage(text, config);
|
|
399
|
-
if (result.success) {
|
|
400
|
-
let imageElem: any;
|
|
401
|
-
if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
402
|
-
try {
|
|
403
|
-
const filename = crypto.createHash('md5').update(result.data as string).digest('hex');
|
|
404
|
-
const filePath = await downloadImageWithThreads(result.data as string, filename, config);
|
|
405
|
-
const absPath = path.resolve(filePath);
|
|
406
|
-
imageElem = h.file(`file:///${absPath.replace(/\\/g, '/')}`);
|
|
407
|
-
} catch (error) {
|
|
408
|
-
imageElem = h.image(result.data as string);
|
|
409
|
-
}
|
|
410
|
-
} else if (result.type === 'buffer') {
|
|
411
|
-
if (!fs.existsSync(config.tempDir)) {
|
|
412
|
-
fs.mkdirSync(config.tempDir, { recursive: true });
|
|
413
|
-
}
|
|
414
|
-
const filename = crypto.randomUUID();
|
|
415
|
-
const filePath = path.join(config.tempDir, `${filename}.jpg`);
|
|
416
|
-
const absPath = path.resolve(filePath);
|
|
417
|
-
fs.writeFileSync(absPath, result.data as Buffer);
|
|
418
|
-
imageElem = h.file(`file:///${absPath.replace(/\\/g, '/')}`);
|
|
419
|
-
} else {
|
|
420
|
-
imageElem = h.image(result.data as string);
|
|
421
|
-
}
|
|
422
|
-
await sendTimeout(session, 'messages.fetchSuccess', config);
|
|
423
|
-
await sendTimeout(session, imageElem, config);
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
ctx.command('clear-image-cache', '#commands.clear-image-cache')
|
|
428
|
-
.action(({ session }) => {
|
|
429
|
-
clearAllCache(config);
|
|
430
|
-
return session ? session.text('messages.cacheCleared') : '✅ 图片缓存已清空';
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
setInterval(() => {
|
|
434
|
-
const now = Date.now();
|
|
435
|
-
processedApi.forEach((timestamp, hash) => {
|
|
436
|
-
if (now - timestamp > 86400000) processedApi.delete(hash);
|
|
437
|
-
});
|
|
438
|
-
}, 3600000);
|
|
439
|
-
|
|
440
|
-
setInterval(() => {
|
|
441
|
-
if (!fs.existsSync(config.tempDir)) return;
|
|
442
|
-
|
|
443
|
-
const now = Date.now();
|
|
444
|
-
fs.readdirSync(config.tempDir).forEach(file => {
|
|
445
|
-
try {
|
|
446
|
-
const stat = fs.statSync(path.join(config.tempDir, file));
|
|
447
|
-
if (now - stat.mtimeMs > 3600000) {
|
|
448
|
-
fs.unlinkSync(path.join(config.tempDir, file));
|
|
449
|
-
}
|
|
450
|
-
} catch (error) {}
|
|
451
|
-
});
|
|
452
|
-
}, 1800000);
|
|
453
|
-
|
|
454
|
-
if (config.autoClearCacheInterval > 0) {
|
|
455
|
-
setInterval(() => {
|
|
456
|
-
clearAllCache(config);
|
|
457
|
-
ctx.logger.info('[custom-image] 缓存已自动清理');
|
|
458
|
-
}, config.autoClearCacheInterval * 60000);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
process.on('exit', () => {
|
|
462
|
-
clearAllCache(config);
|
|
463
|
-
ctx.logger.info('[custom-image] 插件缓存已清理');
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export const inject = {
|
|
468
|
-
optional: ['i18n']
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export const using = [] as const
|
package/src/locales/zh-CN.yml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
commands:
|
|
2
|
-
random-image: 随机获取自定义图片
|
|
3
|
-
random-image.options.api: 指定单个API地址
|
|
4
|
-
hsjp: 生成黑丝举牌图片
|
|
5
|
-
dmjp: 生成动漫举牌图片
|
|
6
|
-
clear-image-cache: 清空图片缓存
|
|
7
|
-
|
|
8
|
-
messages:
|
|
9
|
-
inputError: ❌ 指令输入错误:请输入举牌文字
|
|
10
|
-
cacheCleared: ✅ 图片缓存已清空
|
|
11
|
-
fetchSuccess: ✅ 图片获取成功
|
|
12
|
-
fetchWaiting: 正在获取图片,请稍候...
|
|
13
|
-
repeatRequest: 请勿重复请求
|
|
14
|
-
fetchFailed: 图片获取失败
|
|
15
|
-
partialFailed: ⚠️ 部分图片获取失败
|
|
16
|
-
allFailed: ❌ 所有图片获取失败
|