koishi-plugin-custom-image 0.2.1 → 0.2.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.js +44 -15
- 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.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()
|
|
@@ -97,8 +115,17 @@ if (!worker_threads_1.isMainThread) {
|
|
|
97
115
|
const processedApi = new Map();
|
|
98
116
|
const imageBuffer = new Map();
|
|
99
117
|
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
118
|
+
function getI18nText(session, key) {
|
|
119
|
+
if (session?.text) {
|
|
120
|
+
return session.text(key);
|
|
121
|
+
}
|
|
122
|
+
const lang = 'zh-CN';
|
|
123
|
+
return locales[lang][key] || key;
|
|
124
|
+
}
|
|
100
125
|
async function sendTimeout(session, content, config) {
|
|
101
|
-
const text = typeof content === 'string'
|
|
126
|
+
const text = typeof content === 'string'
|
|
127
|
+
? getI18nText(session, content)
|
|
128
|
+
: content;
|
|
102
129
|
if (config.imageSendTimeout <= 0) {
|
|
103
130
|
return session.send(text).catch(() => null);
|
|
104
131
|
}
|
|
@@ -194,12 +221,12 @@ async function processCustomImage(session, apiUrl, config) {
|
|
|
194
221
|
const hash = crypto_1.default.createHash('md5').update(apiUrl).digest('hex');
|
|
195
222
|
const now = Date.now();
|
|
196
223
|
if (processedApi.get(hash) && now - processedApi.get(hash) < config.sameImageApiInterval * 1000) {
|
|
197
|
-
return { success: false, msg: session
|
|
224
|
+
return { success: false, msg: getI18nText(session, 'messages.repeatRequest') };
|
|
198
225
|
}
|
|
199
226
|
processedApi.set(hash, now);
|
|
200
227
|
const result = await fetchImage(apiUrl, config);
|
|
201
228
|
if (!result.success) {
|
|
202
|
-
return { success: false, msg: session
|
|
229
|
+
return { success: false, msg: getI18nText(session, 'messages.fetchFailed') };
|
|
203
230
|
}
|
|
204
231
|
let imageElem;
|
|
205
232
|
if (result.type === 'url' && config.downloadImageBeforeSend) {
|
|
@@ -226,7 +253,7 @@ async function processCustomImage(session, apiUrl, config) {
|
|
|
226
253
|
else {
|
|
227
254
|
imageElem = koishi_1.h.image(result.data);
|
|
228
255
|
}
|
|
229
|
-
return { success: true, msg: 'ok', data: { text: session
|
|
256
|
+
return { success: true, msg: 'ok', data: { text: getI18nText(session, 'messages.fetchSuccess'), image: imageElem } };
|
|
230
257
|
}
|
|
231
258
|
async function flush(session, config, manualApi) {
|
|
232
259
|
const key = `${session.platform}:${session.userId}:${session.channelId}`;
|
|
@@ -250,12 +277,12 @@ async function flush(session, config, manualApi) {
|
|
|
250
277
|
}
|
|
251
278
|
}
|
|
252
279
|
if (errs.length && !config.ignoreSendError) {
|
|
253
|
-
const errorMsg = `${session
|
|
280
|
+
const errorMsg = `${getI18nText(session, 'messages.partialFailed')}\n${errs.join('\n')}`;
|
|
254
281
|
await sendTimeout(session, errorMsg, config);
|
|
255
282
|
await delay(600);
|
|
256
283
|
}
|
|
257
284
|
if (items.length === 0) {
|
|
258
|
-
const failMsg = `${session
|
|
285
|
+
const failMsg = `${getI18nText(session, 'messages.allFailed')}\n${errs.join('\n')}`;
|
|
259
286
|
if (!config.ignoreSendError) {
|
|
260
287
|
await sendTimeout(session, failMsg, config);
|
|
261
288
|
}
|
|
@@ -271,11 +298,13 @@ async function flush(session, config, manualApi) {
|
|
|
271
298
|
function apply(ctx, config) {
|
|
272
299
|
if (!worker_threads_1.isMainThread)
|
|
273
300
|
return;
|
|
274
|
-
|
|
301
|
+
Object.keys(locales).forEach(lang => {
|
|
302
|
+
ctx.i18n.define(lang, locales[lang]);
|
|
303
|
+
});
|
|
275
304
|
clearAllCache(config);
|
|
276
305
|
ctx.logger.info('[custom-image] 插件已加载');
|
|
277
|
-
ctx.command('random-image [apis...]', '
|
|
278
|
-
.option('api',
|
|
306
|
+
ctx.command('random-image [apis...]', locales['zh-CN']['commands.random-image'])
|
|
307
|
+
.option('api', `-a <api> ${locales['zh-CN']['commands.random-image.options.api']}`)
|
|
279
308
|
.action(async ({ session, options = {} }, ...apis) => {
|
|
280
309
|
if (!config.enable || !config.customImageEnabled || !session)
|
|
281
310
|
return;
|
|
@@ -304,7 +333,7 @@ function apply(ctx, config) {
|
|
|
304
333
|
}
|
|
305
334
|
}
|
|
306
335
|
});
|
|
307
|
-
ctx.command('hsjp <msg> [msg1] [msg2]', '
|
|
336
|
+
ctx.command('hsjp <msg> [msg1] [msg2]', locales['zh-CN']['commands.hsjp'])
|
|
308
337
|
.action(async ({ session }, msg, msg1 = '', msg2 = '') => {
|
|
309
338
|
if (!config.enable || !config.hsjpEnabled || !session)
|
|
310
339
|
return;
|
|
@@ -346,7 +375,7 @@ function apply(ctx, config) {
|
|
|
346
375
|
await sendTimeout(session, imageElem, config);
|
|
347
376
|
}
|
|
348
377
|
});
|
|
349
|
-
ctx.command('dmjp <text>', '
|
|
378
|
+
ctx.command('dmjp <text>', locales['zh-CN']['commands.dmjp'])
|
|
350
379
|
.action(async ({ session }, text) => {
|
|
351
380
|
if (!config.enable || !config.dmjpEnabled || !session)
|
|
352
381
|
return;
|
|
@@ -388,10 +417,10 @@ function apply(ctx, config) {
|
|
|
388
417
|
await sendTimeout(session, imageElem, config);
|
|
389
418
|
}
|
|
390
419
|
});
|
|
391
|
-
ctx.command('clear-image-cache', '
|
|
420
|
+
ctx.command('clear-image-cache', locales['zh-CN']['commands.clear-image-cache'])
|
|
392
421
|
.action(({ session }) => {
|
|
393
422
|
clearAllCache(config);
|
|
394
|
-
return session ? session
|
|
423
|
+
return session ? getI18nText(session, 'messages.cacheCleared') : '✅ 图片缓存已清空';
|
|
395
424
|
});
|
|
396
425
|
setInterval(() => {
|
|
397
426
|
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.2",
|
|
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: ❌ 所有图片获取失败
|