koishi-plugin-custom-image 0.2.4 → 0.2.5

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.
Files changed (3) hide show
  1. package/lib/index.js +42 -180
  2. package/package.json +1 -1
  3. package/readme.md +83 -0
package/lib/index.js CHANGED
@@ -33,60 +33,24 @@ const currentFilePath = worker_threads_1.isMainThread
33
33
  : path_1.default.join(process.cwd(), worker_threads_1.isMainThread ? 'src/index.ts' : 'lib/index.js');
34
34
  exports.name = 'custom-image';
35
35
  exports.Config = koishi_1.Schema.object({
36
- enable: koishi_1.Schema.boolean()
37
- .default(true)
38
- .description('【基础设置】启用插件'),
39
- showWaitingTip: koishi_1.Schema.boolean()
40
- .default(true)
41
- .description('【基础设置】请求图片时显示等待提示'),
42
- timeout: koishi_1.Schema.number()
43
- .default(10000)
44
- .min(0)
45
- .description('【基础设置】API请求超时时间(毫秒)'),
46
- customImageApis: koishi_1.Schema.array(koishi_1.Schema.string())
47
- .default([])
48
- .role('textarea')
49
- .description('【基础设置】自定义图片API列表(每行一个地址)'),
50
- customImageEnabled: koishi_1.Schema.boolean()
51
- .default(true)
52
- .description('【功能开关】启用自定义图片API功能'),
53
- hsjpEnabled: koishi_1.Schema.boolean()
54
- .default(true)
55
- .description('【功能开关】启用黑丝举牌功能'),
56
- dmjpEnabled: koishi_1.Schema.boolean()
57
- .default(true)
58
- .description('【功能开关】启用动漫举牌功能'),
59
- imageSendTimeout: koishi_1.Schema.number()
60
- .default(15000)
61
- .min(0)
62
- .description('【基础设置】图片发送超时(毫秒)'),
63
- autoClearCacheInterval: koishi_1.Schema.number()
64
- .default(60)
65
- .min(0)
66
- .description('【基础设置】自动清理缓存间隔(分钟)'),
67
- tempDir: koishi_1.Schema.string()
68
- .default(path_1.default.join(process.cwd(), 'temp_images'))
69
- .description('【基础设置】临时图片保存目录'),
70
- showSuccessTip: koishi_1.Schema.boolean()
71
- .default(true)
72
- .description('【提示设置】请求图片成功时显示提示'),
73
- showTimeoutTip: koishi_1.Schema.boolean()
74
- .default(true)
75
- .description('【提示设置】API请求超时后给用户提示')
36
+ enable: koishi_1.Schema.boolean().default(true).description('启用插件'),
37
+ showWaitingTip: koishi_1.Schema.boolean().default(true).description('请求图片时显示等待提示'),
38
+ timeout: koishi_1.Schema.number().default(10000).min(0).description('API请求超时时间(毫秒)'),
39
+ customImageApis: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).role('textarea').description('自定义图片API列表'),
40
+ customImageEnabled: koishi_1.Schema.boolean().default(true).description('启用自定义图片API功能'),
41
+ hsjpEnabled: koishi_1.Schema.boolean().default(true).description('启用黑丝举牌功能'),
42
+ dmjpEnabled: koishi_1.Schema.boolean().default(true).description('启用动漫举牌功能'),
43
+ imageSendTimeout: koishi_1.Schema.number().default(15000).min(0).description('图片发送超时(毫秒)'),
44
+ autoClearCacheInterval: koishi_1.Schema.number().default(60).min(0).description('自动清理缓存间隔(分钟)'),
45
+ tempDir: koishi_1.Schema.string().default(path_1.default.join(process.cwd(), 'temp_images')).description('临时图片保存目录'),
46
+ showSuccessTip: koishi_1.Schema.boolean().default(true).description('请求图片成功时显示提示'),
47
+ showTimeoutTip: koishi_1.Schema.boolean().default(true).description('API请求超时后给用户提示')
76
48
  });
77
49
  if (!worker_threads_1.isMainThread) {
78
50
  const { url, filePath } = worker_threads_1.workerData;
79
51
  (async () => {
80
52
  try {
81
- const response = await (0, axios_1.default)({
82
- url,
83
- method: 'GET',
84
- responseType: 'stream',
85
- timeout: 60000,
86
- headers: {
87
- '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'
88
- }
89
- });
53
+ const response = await (0, axios_1.default)({ url, method: 'GET', responseType: 'stream', timeout: 60000, headers: { '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' } });
90
54
  await (0, promises_1.pipeline)(response.data, fs_1.default.createWriteStream(filePath));
91
55
  worker_threads_1.parentPort?.postMessage({ success: true, filePath });
92
56
  }
@@ -95,40 +59,27 @@ if (!worker_threads_1.isMainThread) {
95
59
  }
96
60
  })();
97
61
  }
98
- const processedApi = new Map();
99
- const imageBuffer = new Map();
100
62
  const delay = (ms) => new Promise(r => setTimeout(r, ms));
101
63
  function getI18nText(session, key) {
102
- if (session?.text) {
64
+ if (session?.text)
103
65
  return session.text(key);
104
- }
105
66
  const lang = 'zh-CN';
106
67
  return locales[lang][key] || key;
107
68
  }
108
69
  async function sendTimeout(session, content, config) {
109
- const text = typeof content === 'string'
110
- ? getI18nText(session, content)
111
- : content;
112
- if (config.imageSendTimeout <= 0) {
70
+ const text = typeof content === 'string' ? getI18nText(session, content) : content;
71
+ if (config.imageSendTimeout <= 0)
113
72
  return session.send(text).catch(() => null);
114
- }
115
- return Promise.race([
116
- session.send(text),
117
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.imageSendTimeout))
118
- ]).catch(() => null);
73
+ return Promise.race([session.send(text), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.imageSendTimeout))]).catch(() => null);
119
74
  }
120
75
  function clearAllCache(config) {
121
- processedApi.clear();
122
- imageBuffer.forEach(buf => clearTimeout(buf.timer));
123
- imageBuffer.clear();
124
76
  if (fs_1.default.existsSync(config.tempDir)) {
125
77
  fs_1.default.readdirSync(config.tempDir).forEach(file => {
126
78
  try {
127
79
  const filePath = path_1.default.join(config.tempDir, file);
128
80
  const stat = fs_1.default.statSync(filePath);
129
- if (Date.now() - stat.mtimeMs > 3600000) {
81
+ if (Date.now() - stat.mtimeMs > 3600000)
130
82
  fs_1.default.unlinkSync(filePath);
131
- }
132
83
  }
133
84
  catch (error) { }
134
85
  });
@@ -142,20 +93,13 @@ function randomSelectApi(customImageApis) {
142
93
  return validApis[Math.floor(Math.random() * validApis.length)];
143
94
  }
144
95
  async function fetchImage(url, config) {
145
- const http = axios_1.default.create({
146
- timeout: config.timeout,
147
- headers: {
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'
149
- }
150
- });
96
+ const http = axios_1.default.create({ timeout: config.timeout, headers: { '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' } });
151
97
  try {
152
- if (/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url)) {
98
+ if (/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url))
153
99
  return { success: true, type: 'url', data: url, timeout: false };
154
- }
155
100
  const res = await http.get(url, { responseType: 'arraybuffer' });
156
- if (res.status === 200 && res.data) {
101
+ if (res.status === 200 && res.data)
157
102
  return { success: true, type: 'buffer', data: res.data, timeout: false };
158
- }
159
103
  }
160
104
  catch (error) {
161
105
  const isTimeout = error.message.includes('timeout');
@@ -177,62 +121,18 @@ async function fetchDmjpImage(text, config) {
177
121
  }
178
122
  async function processCustomImage(session, apiUrl, config) {
179
123
  const result = await fetchImage(apiUrl, config);
180
- if (result.timeout && config.showTimeoutTip) {
124
+ if (result.timeout && config.showTimeoutTip)
181
125
  return { success: false, msg: '图片请求超时,请稍后再试' };
182
- }
183
- if (!result.success) {
126
+ if (!result.success)
184
127
  return { success: false, msg: getI18nText(session, 'messages.fetchFailed') };
185
- }
186
128
  const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
187
129
  const successText = config.showSuccessTip ? getI18nText(session, 'messages.fetchSuccess') : '';
188
130
  return { success: true, msg: 'ok', data: { text: successText, image: imageElem } };
189
131
  }
190
- async function flush(session, config, manualApi) {
191
- const key = `${session.platform}:${session.userId}:${session.channelId}`;
192
- const buf = imageBuffer.get(key);
193
- const apis = manualApi ? [manualApi] : buf?.apis || [];
194
- if (buf) {
195
- clearTimeout(buf.timer);
196
- imageBuffer.delete(key);
197
- }
198
- if (!config.customImageEnabled || !apis.length)
199
- return;
200
- const items = [];
201
- const errs = [];
202
- for (const apiUrl of apis) {
203
- const result = await processCustomImage(session, apiUrl, config);
204
- if (result.success && result.data) {
205
- items.push(result.data);
206
- }
207
- else {
208
- errs.push(`【${apiUrl.slice(0, 22)}...】:${result.msg}`);
209
- }
210
- }
211
- if (errs.length) {
212
- const errorMsg = `${getI18nText(session, 'messages.partialFailed')}\n${errs.join('\n')}`;
213
- await sendTimeout(session, errorMsg, config);
214
- await delay(600);
215
- }
216
- if (items.length === 0) {
217
- const failMsg = `${getI18nText(session, 'messages.allFailed')}\n${errs.join('\n')}`;
218
- await sendTimeout(session, failMsg, config);
219
- return;
220
- }
221
- for (const item of items) {
222
- if (item.text) {
223
- await sendTimeout(session, item.text, config);
224
- await delay(300);
225
- }
226
- await sendTimeout(session, item.image, config);
227
- await delay(1000);
228
- }
229
- }
230
132
  function apply(ctx, config) {
231
133
  if (!worker_threads_1.isMainThread)
232
134
  return;
233
- Object.keys(locales).forEach(lang => {
234
- ctx.i18n.define(lang, locales[lang]);
235
- });
135
+ Object.keys(locales).forEach(lang => { ctx.i18n.define(lang, locales[lang]); });
236
136
  clearAllCache(config);
237
137
  ctx.logger.info('[custom-image] 插件已加载');
238
138
  ctx.command('random-image [apis...]', locales['zh-CN']['commands.random-image'])
@@ -241,28 +141,21 @@ function apply(ctx, config) {
241
141
  if (!config.enable || !config.customImageEnabled || !session)
242
142
  return;
243
143
  const targetApi = options.api || (apis.length ? apis.join(' ') : null);
244
- if (targetApi) {
245
- await flush(session, config, targetApi);
144
+ if (config.showWaitingTip)
145
+ await sendTimeout(session, 'messages.fetchWaiting', config);
146
+ const api = targetApi || randomSelectApi(config.customImageApis);
147
+ if (!api)
148
+ return;
149
+ const result = await processCustomImage(session, api, config);
150
+ if (result.success && result.data) {
151
+ if (result.data.text) {
152
+ await sendTimeout(session, result.data.text, config);
153
+ await delay(300);
154
+ }
155
+ await sendTimeout(session, result.data.image, config);
246
156
  }
247
157
  else {
248
- const key = `${session.platform}:${session.userId}:${session.channelId}`;
249
- let tipId;
250
- if (config.showWaitingTip) {
251
- const tip = await sendTimeout(session, 'messages.fetchWaiting', config);
252
- tipId = tip?.messageId || tip?.id || tip;
253
- }
254
- if (imageBuffer.has(key)) {
255
- const b = imageBuffer.get(key);
256
- clearTimeout(b.timer);
257
- b.timer = setTimeout(() => flush(session, config), 0);
258
- }
259
- else {
260
- imageBuffer.set(key, {
261
- apis: config.customImageApis,
262
- timer: setTimeout(() => flush(session, config), 0),
263
- tipMsgId: tipId
264
- });
265
- }
158
+ await sendTimeout(session, result.msg, config);
266
159
  }
267
160
  });
268
161
  ctx.command('hsjp <msg> [msg1] [msg2]', locales['zh-CN']['commands.hsjp'])
@@ -273,9 +166,8 @@ function apply(ctx, config) {
273
166
  await sendTimeout(session, 'messages.inputError', config);
274
167
  return;
275
168
  }
276
- if (config.showWaitingTip) {
169
+ if (config.showWaitingTip)
277
170
  await sendTimeout(session, 'messages.fetchWaiting', config);
278
- }
279
171
  const result = await fetchHsjpImage(msg, msg1, msg2, config);
280
172
  if (result.timeout && config.showTimeoutTip) {
281
173
  await sendTimeout(session, '图片请求超时,请稍后再试', config);
@@ -301,9 +193,8 @@ function apply(ctx, config) {
301
193
  await sendTimeout(session, 'messages.inputError', config);
302
194
  return;
303
195
  }
304
- if (config.showWaitingTip) {
196
+ if (config.showWaitingTip)
305
197
  await sendTimeout(session, 'messages.fetchWaiting', config);
306
- }
307
198
  const result = await fetchDmjpImage(text, config);
308
199
  if (result.timeout && config.showTimeoutTip) {
309
200
  await sendTimeout(session, '图片请求超时,请稍后再试', config);
@@ -326,39 +217,10 @@ function apply(ctx, config) {
326
217
  clearAllCache(config);
327
218
  return session ? getI18nText(session, 'messages.cacheCleared') : '✅ 图片缓存已清空';
328
219
  });
329
- setInterval(() => {
330
- const now = Date.now();
331
- processedApi.forEach((timestamp, hash) => {
332
- if (now - timestamp > 86400000)
333
- processedApi.delete(hash);
334
- });
335
- }, 3600000);
336
- setInterval(() => {
337
- if (!fs_1.default.existsSync(config.tempDir))
338
- return;
339
- const now = Date.now();
340
- fs_1.default.readdirSync(config.tempDir).forEach(file => {
341
- try {
342
- const stat = fs_1.default.statSync(path_1.default.join(config.tempDir, file));
343
- if (now - stat.mtimeMs > 3600000) {
344
- fs_1.default.unlinkSync(path_1.default.join(config.tempDir, file));
345
- }
346
- }
347
- catch (error) { }
348
- });
349
- }, 1800000);
350
220
  if (config.autoClearCacheInterval > 0) {
351
- setInterval(() => {
352
- clearAllCache(config);
353
- ctx.logger.info('[custom-image] 缓存已自动清理');
354
- }, config.autoClearCacheInterval * 60000);
221
+ setInterval(() => { clearAllCache(config); ctx.logger.info('[custom-image] 缓存已自动清理'); }, config.autoClearCacheInterval * 60000);
355
222
  }
356
- process.on('exit', () => {
357
- clearAllCache(config);
358
- ctx.logger.info('[custom-image] 插件缓存已清理');
359
- });
223
+ process.on('exit', () => { clearAllCache(config); ctx.logger.info('[custom-image] 插件缓存已清理'); });
360
224
  }
361
- exports.inject = {
362
- optional: ['i18n']
363
- };
225
+ exports.inject = { optional: ['i18n'] };
364
226
  exports.using = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-custom-image",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Koishi自定义图片API插件(內置支持黑丝/动漫举牌)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
package/readme.md ADDED
@@ -0,0 +1,83 @@
1
+ # koishi-plugin-custom-image
2
+
3
+ ## 项目介绍 (Project Introduction)
4
+
5
+ ### 中文
6
+ 这是一个为 Koishi 机器人框架开发的**随机图片获取插件**,支持从自定义 API 列表中随机选取接口获取图片,同时内置黑丝举牌、动漫举牌等趣味图片功能。核心特性:
7
+ - 🚀 从自定义 API 列表中**随机单张选取**,不批量、不轮询
8
+ - 🖼️ 强制使用图片 URL 直连发送,不下载本地文件
9
+ - 💡 可配置等待提示、成功提示、超时提示
10
+ - ⚙️ 简单易用的指令与配置项,开箱即用
11
+ - 🧹 支持自动清理缓存与手动清理指令
12
+ - 🎭 内置 `hsjp`、`dmjp` 等趣味图片接口
13
+
14
+ ### English
15
+ A random image plugin for the Koishi bot framework. It randomly selects one API from your custom API list to fetch images, with built-in fun image APIs such as haisi and dongman. Core features:
16
+ - 🚀 Randomly select **one API** from the custom list, no batch / loop requests
17
+ - 🖼️ Force sending images via URL, no local file downloading
18
+ - 💡 Configurable waiting, success, and timeout tips
19
+ - ⚙️ Simple commands and configurations, ready to use
20
+ - 🧹 Auto cache cleanup & manual clear command
21
+ - 🎭 Built-in fun image APIs: `hsjp`, `dmjp`
22
+
23
+ ## 项目仓库 (Repository)
24
+ - GitHub: https://github.com/yourname/koishi-plugin-custom-image
25
+ - Issues: https://github.com/yourname/koishi-plugin-custom-image/issues
26
+
27
+ ## 核心指令 (Core Commands)
28
+
29
+ | 指令 (Command) | 说明 (Description) | 示例 (Example) |
30
+ |----------------|--------------------|----------------|
31
+ | `random-image` | 从自定义 API 列表随机获取一张图片 | `random-image` |
32
+ | `hsjp <msg> [msg1] [msg2]` | 生成黑丝举牌图片 | `hsjp 我喜欢你` |
33
+ | `dmjp <text>` | 生成动漫举牌图片 | `dmjp Koishi 真棒` |
34
+ | `clear-image-cache` | 清空图片缓存 | `clear-image-cache` |
35
+
36
+ ## 配置项说明 (Configuration)
37
+
38
+ | 配置项 (Config Item) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
39
+ |----------------------|-------------|------------------|--------------------|
40
+ | `enable` | boolean | true | 是否启用插件 |
41
+ | `showWaitingTip` | boolean | true | 请求图片时是否显示等待提示 |
42
+ | `timeout` | number | 10000 | API 请求超时时间(毫秒) |
43
+ | `customImageApis` | string[] | [] | 自定义图片 API 列表 |
44
+ | `customImageEnabled` | boolean | true | 是否启用自定义图片功能 |
45
+ | `hsjpEnabled` | boolean | true | 是否启用黑丝举牌功能 |
46
+ | `dmjpEnabled` | boolean | true | 是否启用动漫举牌功能 |
47
+ | `imageSendTimeout` | number | 15000 | 图片发送超时(毫秒) |
48
+ | `autoClearCacheInterval` | number | 60 | 自动清理缓存间隔(分钟) |
49
+ | `tempDir` | string | ./temp_images | 临时图片保存目录 |
50
+ | `showSuccessTip` | boolean | true | 请求图片成功时是否显示提示 |
51
+ | `showTimeoutTip` | boolean | true | API 请求超时后是否给用户提示 |
52
+
53
+ ## 支持功能 (Supported Features)
54
+
55
+ | 功能 (Feature) | 状态 (Status) |
56
+ |----------------|---------------|
57
+ | 自定义 API 随机图片 | ✅ 稳定 |
58
+ | 黑丝举牌图片 | ✅ 稳定 |
59
+ | 动漫举牌图片 | ✅ 稳定 |
60
+ | 手动清理缓存 | ✅ 稳定 |
61
+ | 自动清理缓存 | ✅ 稳定 |
62
+ | 超时提示开关 | ✅ 稳定 |
63
+ | 成功提示开关 | ✅ 稳定 |
64
+
65
+ ## 项目贡献者 (Contributors)
66
+
67
+ | 贡献者 (Contributor) | 贡献内容 (Contribution) |
68
+ |----------------------|-------------------------|
69
+ | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
70
+ | 素颜API | 提供 API 接口支持 |
71
+ | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
72
+
73
+ ## 许可协议 (License)
74
+
75
+ 本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
76
+
77
+ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
78
+
79
+ ## 支持我们 (Support Us)
80
+
81
+ 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
82
+
83
+ If this project is helpful to you, please feel free to star it in the upper right corner ⭐ to support us, which will be the greatest encouragement to all contributors!