koishi-plugin-custom-image 0.2.3 → 0.2.4

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 CHANGED
@@ -3,21 +3,16 @@ export declare const name = "custom-image";
3
3
  export interface Config {
4
4
  enable: boolean;
5
5
  showWaitingTip: boolean;
6
- sameImageApiInterval: number;
7
6
  timeout: number;
8
7
  customImageApis: string[];
9
8
  customImageEnabled: boolean;
10
9
  hsjpEnabled: boolean;
11
10
  dmjpEnabled: boolean;
12
- ignoreSendError: boolean;
13
- retryTimes: number;
14
- retryInterval: number;
15
11
  imageSendTimeout: number;
16
12
  autoClearCacheInterval: number;
17
- downloadImageBeforeSend: boolean;
18
- messageBufferDelay: number;
19
13
  tempDir: string;
20
- useImageUrlInsteadOfFile: boolean;
14
+ showSuccessTip: boolean;
15
+ showTimeoutTip: boolean;
21
16
  }
22
17
  export declare const Config: Schema<Config>;
23
18
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -7,7 +7,6 @@ exports.using = exports.inject = exports.Config = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
10
- const crypto_1 = __importDefault(require("crypto"));
11
10
  const fs_1 = __importDefault(require("fs"));
12
11
  const path_1 = __importDefault(require("path"));
13
12
  const promises_1 = require("stream/promises");
@@ -40,10 +39,6 @@ exports.Config = koishi_1.Schema.object({
40
39
  showWaitingTip: koishi_1.Schema.boolean()
41
40
  .default(true)
42
41
  .description('【基础设置】请求图片时显示等待提示'),
43
- sameImageApiInterval: koishi_1.Schema.number()
44
- .default(60)
45
- .min(0)
46
- .description('【基础设置】重复API调用间隔(秒)'),
47
42
  timeout: koishi_1.Schema.number()
48
43
  .default(10000)
49
44
  .min(0)
@@ -61,38 +56,23 @@ exports.Config = koishi_1.Schema.object({
61
56
  dmjpEnabled: koishi_1.Schema.boolean()
62
57
  .default(true)
63
58
  .description('【功能开关】启用动漫举牌功能'),
64
- ignoreSendError: koishi_1.Schema.boolean()
65
- .default(true)
66
- .description('【容错设置】忽略发送错误'),
67
- retryTimes: koishi_1.Schema.number()
68
- .default(1)
69
- .min(0)
70
- .description('【容错设置】API重试次数'),
71
- retryInterval: koishi_1.Schema.number()
72
- .default(500)
73
- .min(0)
74
- .description('【容错设置】重试间隔(毫秒)'),
75
59
  imageSendTimeout: koishi_1.Schema.number()
76
60
  .default(15000)
77
61
  .min(0)
78
- .description('【容错设置】图片发送超时(毫秒)'),
62
+ .description('【基础设置】图片发送超时(毫秒)'),
79
63
  autoClearCacheInterval: koishi_1.Schema.number()
80
64
  .default(60)
81
65
  .min(0)
82
- .description('【缓存设置】自动清理缓存间隔(分钟)'),
83
- downloadImageBeforeSend: koishi_1.Schema.boolean()
84
- .default(false)
85
- .description('【展示设置】发送前下载图片(关闭可解决OneBot文件上传问题)'),
86
- messageBufferDelay: koishi_1.Schema.number()
87
- .default(0)
88
- .min(0)
89
- .description('【性能设置】消息缓冲延迟:合并短时间内的多个图片请求(秒)'),
66
+ .description('【基础设置】自动清理缓存间隔(分钟)'),
90
67
  tempDir: koishi_1.Schema.string()
91
68
  .default(path_1.default.join(process.cwd(), 'temp_images'))
92
- .description('【文件设置】临时图片保存目录'),
93
- useImageUrlInsteadOfFile: koishi_1.Schema.boolean()
69
+ .description('【基础设置】临时图片保存目录'),
70
+ showSuccessTip: koishi_1.Schema.boolean()
71
+ .default(true)
72
+ .description('【提示设置】请求图片成功时显示提示'),
73
+ showTimeoutTip: koishi_1.Schema.boolean()
94
74
  .default(true)
95
- .description('【兼容设置】强制使用图片URL发送(避免OneBot文件上传错误)')
75
+ .description('【提示设置】API请求超时后给用户提示')
96
76
  });
97
77
  if (!worker_threads_1.isMainThread) {
98
78
  const { url, filePath } = worker_threads_1.workerData;
@@ -137,28 +117,6 @@ async function sendTimeout(session, content, config) {
137
117
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.imageSendTimeout))
138
118
  ]).catch(() => null);
139
119
  }
140
- async function downloadImageWithThreads(url, filename, config) {
141
- return new Promise((resolve, reject) => {
142
- if (!fs_1.default.existsSync(config.tempDir)) {
143
- fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
144
- }
145
- const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
146
- const worker = new worker_threads_1.Worker(currentFilePath, { workerData: { url, filePath } });
147
- worker.on('message', (result) => {
148
- if (result.success && result.filePath) {
149
- resolve(result.filePath);
150
- }
151
- else {
152
- reject(new Error(result.error || '图片获取失败'));
153
- }
154
- });
155
- worker.on('error', reject);
156
- worker.on('exit', (code) => {
157
- if (code !== 0)
158
- reject(new Error('图片下载线程异常'));
159
- });
160
- });
161
- }
162
120
  function clearAllCache(config) {
163
121
  processedApi.clear();
164
122
  imageBuffer.forEach(buf => clearTimeout(buf.timer));
@@ -190,23 +148,20 @@ async function fetchImage(url, config) {
190
148
  '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'
191
149
  }
192
150
  });
193
- for (let retry = 0; retry <= config.retryTimes; retry++) {
194
- try {
195
- if (/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url)) {
196
- return { success: true, type: 'url', data: url };
197
- }
198
- const res = await http.get(url, { responseType: 'arraybuffer' });
199
- if (res.status === 200 && res.data) {
200
- return { success: true, type: 'buffer', data: res.data };
201
- }
151
+ try {
152
+ if (/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url)) {
153
+ return { success: true, type: 'url', data: url, timeout: false };
202
154
  }
203
- catch (error) {
204
- if (retry === config.retryTimes)
205
- return { success: false, type: '', data: '' };
206
- await delay(config.retryInterval);
155
+ const res = await http.get(url, { responseType: 'arraybuffer' });
156
+ if (res.status === 200 && res.data) {
157
+ return { success: true, type: 'buffer', data: res.data, timeout: false };
207
158
  }
208
159
  }
209
- return { success: false, type: '', data: '' };
160
+ catch (error) {
161
+ const isTimeout = error.message.includes('timeout');
162
+ return { success: false, type: '', data: '', timeout: isTimeout };
163
+ }
164
+ return { success: false, type: '', data: '', timeout: false };
210
165
  }
211
166
  async function fetchHsjpImage(msg, msg1, msg2, config) {
212
167
  const encodedMsg = encodeURIComponent(msg);
@@ -221,46 +176,16 @@ async function fetchDmjpImage(text, config) {
221
176
  return fetchImage(url, config);
222
177
  }
223
178
  async function processCustomImage(session, apiUrl, config) {
224
- const hash = crypto_1.default.createHash('md5').update(apiUrl).digest('hex');
225
- const now = Date.now();
226
- if (processedApi.get(hash) && now - processedApi.get(hash) < config.sameImageApiInterval * 1000) {
227
- return { success: false, msg: getI18nText(session, 'messages.repeatRequest') };
228
- }
229
- processedApi.set(hash, now);
230
179
  const result = await fetchImage(apiUrl, config);
180
+ if (result.timeout && config.showTimeoutTip) {
181
+ return { success: false, msg: '图片请求超时,请稍后再试' };
182
+ }
231
183
  if (!result.success) {
232
184
  return { success: false, msg: getI18nText(session, 'messages.fetchFailed') };
233
185
  }
234
- let imageElem;
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) {
239
- try {
240
- const filename = crypto_1.default.createHash('md5').update(result.data).digest('hex');
241
- const filePath = await downloadImageWithThreads(result.data, filename, config);
242
- const absPath = path_1.default.resolve(filePath);
243
- fs_1.default.chmodSync(absPath, 0o777);
244
- imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
245
- }
246
- catch (error) {
247
- imageElem = koishi_1.h.image(result.data);
248
- }
249
- }
250
- else if (result.type === 'buffer') {
251
- if (!fs_1.default.existsSync(config.tempDir)) {
252
- fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
253
- }
254
- const filename = crypto_1.default.randomUUID();
255
- const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
256
- const absPath = path_1.default.resolve(filePath);
257
- fs_1.default.writeFileSync(absPath, result.data, { mode: 0o777 });
258
- imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
259
- }
260
- else {
261
- imageElem = koishi_1.h.image(result.data);
262
- }
263
- return { success: true, msg: 'ok', data: { text: getI18nText(session, 'messages.fetchSuccess'), image: imageElem } };
186
+ const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
187
+ const successText = config.showSuccessTip ? getI18nText(session, 'messages.fetchSuccess') : '';
188
+ return { success: true, msg: 'ok', data: { text: successText, image: imageElem } };
264
189
  }
265
190
  async function flush(session, config, manualApi) {
266
191
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
@@ -283,21 +208,21 @@ async function flush(session, config, manualApi) {
283
208
  errs.push(`【${apiUrl.slice(0, 22)}...】:${result.msg}`);
284
209
  }
285
210
  }
286
- if (errs.length && !config.ignoreSendError) {
211
+ if (errs.length) {
287
212
  const errorMsg = `${getI18nText(session, 'messages.partialFailed')}\n${errs.join('\n')}`;
288
213
  await sendTimeout(session, errorMsg, config);
289
214
  await delay(600);
290
215
  }
291
216
  if (items.length === 0) {
292
217
  const failMsg = `${getI18nText(session, 'messages.allFailed')}\n${errs.join('\n')}`;
293
- if (!config.ignoreSendError) {
294
- await sendTimeout(session, failMsg, config);
295
- }
218
+ await sendTimeout(session, failMsg, config);
296
219
  return;
297
220
  }
298
221
  for (const item of items) {
299
- await sendTimeout(session, item.text, config);
300
- await delay(300);
222
+ if (item.text) {
223
+ await sendTimeout(session, item.text, config);
224
+ await delay(300);
225
+ }
301
226
  await sendTimeout(session, item.image, config);
302
227
  await delay(1000);
303
228
  }
@@ -329,12 +254,12 @@ function apply(ctx, config) {
329
254
  if (imageBuffer.has(key)) {
330
255
  const b = imageBuffer.get(key);
331
256
  clearTimeout(b.timer);
332
- b.timer = setTimeout(() => flush(session, config), config.messageBufferDelay * 1000);
257
+ b.timer = setTimeout(() => flush(session, config), 0);
333
258
  }
334
259
  else {
335
260
  imageBuffer.set(key, {
336
261
  apis: config.customImageApis,
337
- timer: setTimeout(() => flush(session, config), config.messageBufferDelay * 1000),
262
+ timer: setTimeout(() => flush(session, config), 0),
338
263
  tipMsgId: tipId
339
264
  });
340
265
  }
@@ -352,39 +277,21 @@ function apply(ctx, config) {
352
277
  await sendTimeout(session, 'messages.fetchWaiting', config);
353
278
  }
354
279
  const result = await fetchHsjpImage(msg, msg1, msg2, config);
280
+ if (result.timeout && config.showTimeoutTip) {
281
+ await sendTimeout(session, '图片请求超时,请稍后再试', config);
282
+ return;
283
+ }
355
284
  if (result.success) {
356
- let imageElem;
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) {
361
- try {
362
- const filename = crypto_1.default.createHash('md5').update(result.data).digest('hex');
363
- const filePath = await downloadImageWithThreads(result.data, filename, config);
364
- const absPath = path_1.default.resolve(filePath);
365
- fs_1.default.chmodSync(absPath, 0o777);
366
- imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
367
- }
368
- catch (error) {
369
- imageElem = koishi_1.h.image(result.data);
370
- }
371
- }
372
- else if (result.type === 'buffer') {
373
- if (!fs_1.default.existsSync(config.tempDir)) {
374
- fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
375
- }
376
- const filename = crypto_1.default.randomUUID();
377
- const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
378
- const absPath = path_1.default.resolve(filePath);
379
- fs_1.default.writeFileSync(absPath, result.data, { mode: 0o777 });
380
- imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
381
- }
382
- else {
383
- imageElem = koishi_1.h.image(result.data);
285
+ const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
286
+ if (config.showSuccessTip) {
287
+ await sendTimeout(session, 'messages.fetchSuccess', config);
288
+ await delay(300);
384
289
  }
385
- await sendTimeout(session, 'messages.fetchSuccess', config);
386
290
  await sendTimeout(session, imageElem, config);
387
291
  }
292
+ else {
293
+ await sendTimeout(session, 'messages.fetchFailed', config);
294
+ }
388
295
  });
389
296
  ctx.command('dmjp <text>', locales['zh-CN']['commands.dmjp'])
390
297
  .action(async ({ session }, text) => {
@@ -398,39 +305,21 @@ function apply(ctx, config) {
398
305
  await sendTimeout(session, 'messages.fetchWaiting', config);
399
306
  }
400
307
  const result = await fetchDmjpImage(text, config);
308
+ if (result.timeout && config.showTimeoutTip) {
309
+ await sendTimeout(session, '图片请求超时,请稍后再试', config);
310
+ return;
311
+ }
401
312
  if (result.success) {
402
- let imageElem;
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) {
407
- try {
408
- const filename = crypto_1.default.createHash('md5').update(result.data).digest('hex');
409
- const filePath = await downloadImageWithThreads(result.data, filename, config);
410
- const absPath = path_1.default.resolve(filePath);
411
- fs_1.default.chmodSync(absPath, 0o777);
412
- imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
413
- }
414
- catch (error) {
415
- imageElem = koishi_1.h.image(result.data);
416
- }
313
+ const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
314
+ if (config.showSuccessTip) {
315
+ await sendTimeout(session, 'messages.fetchSuccess', config);
316
+ await delay(300);
417
317
  }
418
- else if (result.type === 'buffer') {
419
- if (!fs_1.default.existsSync(config.tempDir)) {
420
- fs_1.default.mkdirSync(config.tempDir, { recursive: true, mode: 0o777 });
421
- }
422
- const filename = crypto_1.default.randomUUID();
423
- const filePath = path_1.default.join(config.tempDir, `${filename}.jpg`);
424
- const absPath = path_1.default.resolve(filePath);
425
- fs_1.default.writeFileSync(absPath, result.data, { mode: 0o777 });
426
- imageElem = koishi_1.h.file(`file://${absPath.replace(/\\/g, '/')}`);
427
- }
428
- else {
429
- imageElem = koishi_1.h.image(result.data);
430
- }
431
- await sendTimeout(session, 'messages.fetchSuccess', config);
432
318
  await sendTimeout(session, imageElem, config);
433
319
  }
320
+ else {
321
+ await sendTimeout(session, 'messages.fetchFailed', config);
322
+ }
434
323
  });
435
324
  ctx.command('clear-image-cache', locales['zh-CN']['commands.clear-image-cache'])
436
325
  .action(({ session }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-custom-image",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Koishi自定义图片API插件(內置支持黑丝/动漫举牌)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",