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 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 zh_CN_yml_1 = __importDefault(require("./locales/zh-CN.yml"));
16
- const currentFilePath = path_1.default.join(process.cwd(), 'src', 'index.ts');
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(true)
67
- .description('【展示设置】发送前下载图片:发送前先下载图片到本地,再发送文件(仅OneBot'),
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' ? session.text(content) : content;
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.text('messages.repeatRequest') };
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.text('messages.fetchFailed') };
232
+ return { success: false, msg: getI18nText(session, 'messages.fetchFailed') };
203
233
  }
204
234
  let imageElem;
205
- if (result.type === 'url' && config.downloadImageBeforeSend) {
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
- imageElem = koishi_1.h.file(`file:///${absPath.replace(/\\/g, '/')}`);
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:///${absPath.replace(/\\/g, '/')}`);
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.text('messages.fetchSuccess'), image: imageElem } };
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.text('messages.partialFailed')}\n${errs.join('\n')}`;
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.text('messages.allFailed')}\n${errs.join('\n')}`;
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
- ctx.i18n.define('zh-CN', zh_CN_yml_1.default);
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...]', '#commands.random-image')
278
- .option('api', '-a <api> #commands.random-image.options.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]', '#commands.hsjp')
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 (result.type === 'url' && config.downloadImageBeforeSend) {
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
- imageElem = koishi_1.h.file(`file:///${absPath.replace(/\\/g, '/')}`);
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:///${absPath.replace(/\\/g, '/')}`);
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>', '#commands.dmjp')
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 (result.type === 'url' && config.downloadImageBeforeSend) {
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
- imageElem = koishi_1.h.file(`file:///${absPath.replace(/\\/g, '/')}`);
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:///${absPath.replace(/\\/g, '/')}`);
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', '#commands.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.text('messages.cacheCleared') : '✅ 图片缓存已清空';
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.1",
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
@@ -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: ❌ 所有图片获取失败
package/src/shims.d.ts DELETED
@@ -1,9 +0,0 @@
1
- declare module '*.yml' {
2
- const content: Record<string, any>;
3
- export default content;
4
- }
5
-
6
- declare module '*.yaml' {
7
- const content: Record<string, any>;
8
- export default content;
9
- }