koishi-plugin-custom-image 0.2.7 → 0.2.9

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 +183 -85
  2. package/package.json +10 -2
  3. package/readme.md +70 -70
package/lib/index.js CHANGED
@@ -20,7 +20,7 @@ const locales = {
20
20
  'messages.fetchSuccess': '图片获取成功!',
21
21
  'messages.fetchWaiting': '正在获取图片,请稍候...',
22
22
  'messages.inputError': '输入内容不能为空!',
23
- 'messages.cacheCleared': '图片缓存已清空',
23
+ 'messages.cacheCleared': '图片缓存已清空',
24
24
  'commands.random-image': '随机获取图片',
25
25
  'commands.random-image.options.api': '指定图片API地址',
26
26
  'commands.hsjp': '生成黑丝举牌图片',
@@ -28,29 +28,21 @@ const locales = {
28
28
  'commands.clear-image-cache': '清空图片缓存'
29
29
  }
30
30
  };
31
- const currentFilePath = worker_threads_1.isMainThread
32
- ? __filename
33
- : path_1.default.join(process.cwd(), worker_threads_1.isMainThread ? 'src/index.ts' : 'lib/index.js');
34
- exports.name = 'custom-image';
35
- exports.Config = koishi_1.Schema.object({
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请求超时后给用户提示')
48
- });
49
31
  if (!worker_threads_1.isMainThread) {
50
32
  const { url, filePath } = worker_threads_1.workerData;
51
33
  (async () => {
52
34
  try {
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' } });
35
+ const response = await (0, axios_1.default)({
36
+ url,
37
+ method: 'GET',
38
+ responseType: 'stream',
39
+ timeout: 60000,
40
+ maxRedirects: 5,
41
+ maxContentLength: 50 * 1024 * 1024,
42
+ headers: {
43
+ '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'
44
+ }
45
+ });
54
46
  await (0, promises_1.pipeline)(response.data, fs_1.default.createWriteStream(filePath));
55
47
  worker_threads_1.parentPort?.postMessage({ success: true, filePath });
56
48
  }
@@ -59,51 +51,117 @@ if (!worker_threads_1.isMainThread) {
59
51
  }
60
52
  })();
61
53
  }
54
+ const httpClient = axios_1.default.create({
55
+ timeout: 30000,
56
+ maxRedirects: 5,
57
+ maxContentLength: 50 * 1024 * 1024,
58
+ headers: {
59
+ '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'
60
+ }
61
+ });
62
+ const rateLimitMap = new Map();
63
+ const RATE_LIMIT_MS = 3000;
64
+ function checkRateLimit(userId) {
65
+ const now = Date.now();
66
+ const last = rateLimitMap.get(userId);
67
+ if (last && now - last < RATE_LIMIT_MS)
68
+ return false;
69
+ rateLimitMap.set(userId, now);
70
+ if (rateLimitMap.size > 10000) {
71
+ for (const [key, time] of rateLimitMap) {
72
+ if (now - time > RATE_LIMIT_MS * 2)
73
+ rateLimitMap.delete(key);
74
+ }
75
+ }
76
+ return true;
77
+ }
78
+ function isValidUrl(url) {
79
+ try {
80
+ const parsed = new URL(url);
81
+ if (!['http:', 'https:'].includes(parsed.protocol))
82
+ return false;
83
+ if (parsed.username || parsed.password)
84
+ return false;
85
+ if (!parsed.hostname)
86
+ return false;
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
62
93
  const delay = (ms) => new Promise(r => setTimeout(r, ms));
63
94
  function getI18nText(session, key) {
64
95
  if (session?.text)
65
96
  return session.text(key);
66
97
  const lang = 'zh-CN';
67
- return locales[lang][key] || key;
98
+ return locales[lang]?.[key] ?? key;
68
99
  }
69
100
  async function sendTimeout(session, content, config) {
70
101
  const text = typeof content === 'string' ? getI18nText(session, content) : content;
71
- if (config.imageSendTimeout <= 0)
72
- return session.send(text).catch(() => null);
73
- return Promise.race([session.send(text), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.imageSendTimeout))]).catch(() => null);
102
+ const timeoutMs = config.imageSendTimeout;
103
+ if (timeoutMs <= 0) {
104
+ await session.send(text).catch(() => { });
105
+ return;
106
+ }
107
+ let timerId;
108
+ const timeout = new Promise(resolve => { timerId = setTimeout(() => resolve('timeout'), timeoutMs); });
109
+ await Promise.race([session.send(text).then(() => 'sent', () => 'error'), timeout]);
110
+ clearTimeout(timerId);
74
111
  }
75
- function clearAllCache(config) {
76
- if (fs_1.default.existsSync(config.tempDir)) {
77
- fs_1.default.readdirSync(config.tempDir).forEach(file => {
112
+ async function clearAllCache(config) {
113
+ let count = 0;
114
+ try {
115
+ const resolvedDir = path_1.default.resolve(config.tempDir);
116
+ const entries = await fs_1.default.promises.readdir(resolvedDir);
117
+ const now = Date.now();
118
+ for (const entry of entries) {
78
119
  try {
79
- const filePath = path_1.default.join(config.tempDir, file);
80
- const stat = fs_1.default.statSync(filePath);
81
- if (Date.now() - stat.mtimeMs > 3600000)
82
- fs_1.default.unlinkSync(filePath);
120
+ const filePath = path_1.default.join(resolvedDir, entry);
121
+ const realPath = await fs_1.default.promises.realpath(filePath);
122
+ if (!realPath.startsWith(resolvedDir + path_1.default.sep))
123
+ continue;
124
+ const stat = await fs_1.default.promises.stat(filePath);
125
+ if (!stat.isFile())
126
+ continue;
127
+ if (now - stat.mtimeMs > 3600000) {
128
+ await fs_1.default.promises.unlink(filePath);
129
+ count++;
130
+ }
83
131
  }
84
- catch (error) { }
85
- });
132
+ catch { }
133
+ }
86
134
  }
87
- return true;
135
+ catch { }
136
+ return count;
88
137
  }
89
138
  function randomSelectApi(customImageApis) {
90
- const validApis = customImageApis.filter(api => api.trim());
139
+ const validApis = customImageApis.filter(api => api && api.trim());
91
140
  if (validApis.length === 0)
92
141
  return null;
93
142
  return validApis[Math.floor(Math.random() * validApis.length)];
94
143
  }
95
144
  async function fetchImage(url, config) {
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' } });
145
+ if (!isValidUrl(url)) {
146
+ return { success: false, type: '', data: '', timeout: false };
147
+ }
148
+ const imageUrlPattern = /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i;
149
+ if (imageUrlPattern.test(url)) {
150
+ return { success: true, type: 'url', data: url, timeout: false };
151
+ }
97
152
  try {
98
- if (/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url))
99
- return { success: true, type: 'url', data: url, timeout: false };
100
- const res = await http.get(url, { responseType: 'arraybuffer' });
101
- if (res.status === 200 && res.data)
102
- return { success: true, type: 'buffer', data: res.data, timeout: false };
153
+ const res = await httpClient.get(url, {
154
+ responseType: 'arraybuffer',
155
+ timeout: config.timeout
156
+ });
157
+ if (res.status >= 200 && res.status < 300 && res.data) {
158
+ const contentType = res.headers['content-type'];
159
+ return { success: true, type: 'buffer', data: res.data, timeout: false, contentType };
160
+ }
103
161
  }
104
162
  catch (error) {
105
- const isTimeout = error.message.includes('timeout');
106
- return { success: false, type: '', data: '', timeout: isTimeout };
163
+ const isTimeout = error.message?.toLowerCase().includes('timeout');
164
+ return { success: false, type: '', data: '', timeout: !!isTimeout };
107
165
  }
108
166
  return { success: false, type: '', data: '', timeout: false };
109
167
  }
@@ -119,27 +177,76 @@ async function fetchDmjpImage(text, config) {
119
177
  const url = `https://api.suyanw.cn/api/dmjp.php?text=${encodedText}`;
120
178
  return fetchImage(url, config);
121
179
  }
180
+ function buildImageElement(result) {
181
+ if (result.type === 'url') {
182
+ return koishi_1.h.image(result.data);
183
+ }
184
+ const mime = result.contentType || 'image/jpeg';
185
+ return koishi_1.h.image(`data:${mime};base64,${Buffer.from(result.data).toString('base64')}`);
186
+ }
122
187
  async function processCustomImage(session, apiUrl, config) {
123
188
  const result = await fetchImage(apiUrl, config);
124
189
  if (result.timeout && config.showTimeoutTip)
125
190
  return { success: false, msg: '图片请求超时,请稍后再试' };
126
191
  if (!result.success)
127
192
  return { success: false, msg: getI18nText(session, 'messages.fetchFailed') };
128
- const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
193
+ const imageElem = buildImageElement(result);
129
194
  const successText = config.showSuccessTip ? getI18nText(session, 'messages.fetchSuccess') : '';
130
195
  return { success: true, msg: 'ok', data: { text: successText, image: imageElem } };
131
196
  }
197
+ async function processFunImage(session, config, fetchFn) {
198
+ const result = await fetchFn();
199
+ if (result.timeout && config.showTimeoutTip) {
200
+ await sendTimeout(session, '图片请求超时,请稍后再试', config);
201
+ return;
202
+ }
203
+ if (result.success) {
204
+ const imageElem = buildImageElement(result);
205
+ if (config.showSuccessTip) {
206
+ await sendTimeout(session, 'messages.fetchSuccess', config);
207
+ await delay(300);
208
+ }
209
+ await sendTimeout(session, imageElem, config);
210
+ }
211
+ else {
212
+ await sendTimeout(session, 'messages.fetchFailed', config);
213
+ }
214
+ }
215
+ exports.name = 'custom-image';
216
+ exports.Config = koishi_1.Schema.object({
217
+ enable: koishi_1.Schema.boolean().default(true).description('启用插件'),
218
+ showWaitingTip: koishi_1.Schema.boolean().default(true).description('请求图片时显示等待提示'),
219
+ timeout: koishi_1.Schema.number().default(10000).min(0).description('API请求超时时间(毫秒)'),
220
+ customImageApis: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).role('textarea').description('自定义图片API列表'),
221
+ customImageEnabled: koishi_1.Schema.boolean().default(true).description('启用自定义图片API功能'),
222
+ hsjpEnabled: koishi_1.Schema.boolean().default(true).description('启用黑丝举牌功能'),
223
+ dmjpEnabled: koishi_1.Schema.boolean().default(true).description('启用动漫举牌功能'),
224
+ imageSendTimeout: koishi_1.Schema.number().default(15000).min(0).description('图片发送超时(毫秒)'),
225
+ autoClearCacheInterval: koishi_1.Schema.number().default(60).min(0).description('自动清理缓存间隔(分钟)'),
226
+ tempDir: koishi_1.Schema.string().default(path_1.default.join(process.cwd(), 'temp_images')).description('临时图片保存目录'),
227
+ showSuccessTip: koishi_1.Schema.boolean().default(true).description('请求图片成功时显示提示'),
228
+ showTimeoutTip: koishi_1.Schema.boolean().default(true).description('API请求超时后给用户提示')
229
+ });
132
230
  function apply(ctx, config) {
133
231
  if (!worker_threads_1.isMainThread)
134
232
  return;
135
- Object.keys(locales).forEach(lang => { ctx.i18n.define(lang, locales[lang]); });
136
- clearAllCache(config);
233
+ if (ctx.i18n) {
234
+ Object.keys(locales).forEach(lang => {
235
+ ctx.i18n.define(lang, locales[lang]);
236
+ });
237
+ }
238
+ clearAllCache(config).catch(() => { });
137
239
  ctx.logger.info('[custom-image] 插件已加载');
138
240
  ctx.command('random-image [apis...]', locales['zh-CN']['commands.random-image'])
139
241
  .option('api', `-a <api> ${locales['zh-CN']['commands.random-image.options.api']}`)
140
242
  .action(async ({ session, options = {} }, ...apis) => {
141
243
  if (!config.enable || !config.customImageEnabled || !session)
142
244
  return;
245
+ const userId = session.userId || session.uid || 'unknown';
246
+ if (!checkRateLimit(userId)) {
247
+ await sendTimeout(session, 'messages.repeatRequest', config);
248
+ return;
249
+ }
143
250
  const targetApi = options.api || (apis.length ? apis.join(' ') : null);
144
251
  if (config.showWaitingTip)
145
252
  await sendTimeout(session, 'messages.fetchWaiting', config);
@@ -162,65 +269,56 @@ function apply(ctx, config) {
162
269
  .action(async ({ session }, msg, msg1 = '', msg2 = '') => {
163
270
  if (!config.enable || !config.hsjpEnabled || !session)
164
271
  return;
165
- if (!msg || msg.trim().length === 0) {
272
+ if (!msg || !msg.trim()) {
166
273
  await sendTimeout(session, 'messages.inputError', config);
167
274
  return;
168
275
  }
169
- if (config.showWaitingTip)
170
- await sendTimeout(session, 'messages.fetchWaiting', config);
171
- const result = await fetchHsjpImage(msg, msg1, msg2, config);
172
- if (result.timeout && config.showTimeoutTip) {
173
- await sendTimeout(session, '图片请求超时,请稍后再试', config);
276
+ if (!checkRateLimit(session.userId || session.uid || 'unknown')) {
277
+ await sendTimeout(session, 'messages.repeatRequest', config);
174
278
  return;
175
279
  }
176
- if (result.success) {
177
- const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
178
- if (config.showSuccessTip) {
179
- await sendTimeout(session, 'messages.fetchSuccess', config);
180
- await delay(300);
181
- }
182
- await sendTimeout(session, imageElem, config);
183
- }
184
- else {
185
- await sendTimeout(session, 'messages.fetchFailed', config);
186
- }
280
+ if (config.showWaitingTip)
281
+ await sendTimeout(session, 'messages.fetchWaiting', config);
282
+ await processFunImage(session, config, () => fetchHsjpImage(msg, msg1, msg2, config));
187
283
  });
188
284
  ctx.command('dmjp <text>', locales['zh-CN']['commands.dmjp'])
189
285
  .action(async ({ session }, text) => {
190
286
  if (!config.enable || !config.dmjpEnabled || !session)
191
287
  return;
192
- if (!text || text.trim().length === 0) {
288
+ if (!text || !text.trim()) {
193
289
  await sendTimeout(session, 'messages.inputError', config);
194
290
  return;
195
291
  }
196
- if (config.showWaitingTip)
197
- await sendTimeout(session, 'messages.fetchWaiting', config);
198
- const result = await fetchDmjpImage(text, config);
199
- if (result.timeout && config.showTimeoutTip) {
200
- await sendTimeout(session, '图片请求超时,请稍后再试', config);
292
+ if (!checkRateLimit(session.userId || session.uid || 'unknown')) {
293
+ await sendTimeout(session, 'messages.repeatRequest', config);
201
294
  return;
202
295
  }
203
- if (result.success) {
204
- const imageElem = koishi_1.h.image(result.type === 'url' ? result.data : `data:image/jpeg;base64,${Buffer.from(result.data).toString('base64')}`);
205
- if (config.showSuccessTip) {
206
- await sendTimeout(session, 'messages.fetchSuccess', config);
207
- await delay(300);
208
- }
209
- await sendTimeout(session, imageElem, config);
210
- }
211
- else {
212
- await sendTimeout(session, 'messages.fetchFailed', config);
213
- }
296
+ if (config.showWaitingTip)
297
+ await sendTimeout(session, 'messages.fetchWaiting', config);
298
+ await processFunImage(session, config, () => fetchDmjpImage(text, config));
214
299
  });
215
300
  ctx.command('clear-image-cache', locales['zh-CN']['commands.clear-image-cache'])
216
- .action(({ session }) => {
217
- clearAllCache(config);
218
- return session ? getI18nText(session, 'messages.cacheCleared') : '图片缓存已清空';
301
+ .action(async ({ session }) => {
302
+ const count = await clearAllCache(config);
303
+ const base = session ? getI18nText(session, 'messages.cacheCleared') : '图片缓存已清空';
304
+ return `${base}(已清理 ${count} 个文件)`;
219
305
  });
220
306
  if (config.autoClearCacheInterval > 0) {
221
- setInterval(() => { clearAllCache(config); ctx.logger.info('[custom-image] 缓存已自动清理'); }, config.autoClearCacheInterval * 60000);
307
+ setInterval(() => {
308
+ clearAllCache(config).then(() => {
309
+ ctx.logger.info('[custom-image] 缓存已自动清理');
310
+ }).catch(() => { });
311
+ }, config.autoClearCacheInterval * 60000);
222
312
  }
223
- process.on('exit', () => { clearAllCache(config); ctx.logger.info('[custom-image] 插件缓存已清理'); });
313
+ process.on('exit', () => {
314
+ fs_1.default.existsSync(config.tempDir) && fs_1.default.readdirSync(config.tempDir).forEach(file => {
315
+ try {
316
+ fs_1.default.unlinkSync(path_1.default.join(config.tempDir, file));
317
+ }
318
+ catch { }
319
+ });
320
+ ctx.logger.info('[custom-image] 插件缓存已清理');
321
+ });
224
322
  }
225
323
  exports.inject = { optional: ['i18n'] };
226
324
  exports.using = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-custom-image",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Koishi自定义图片API插件(內置支持黑丝/动漫举牌)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -36,5 +36,13 @@
36
36
  "dmjp"
37
37
  ],
38
38
  "author": "",
39
- "license": "MIT"
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/Minecraft-1314/koishi-plugin-custom-image.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/Minecraft-1314/koishi-plugin-custom-image/issues"
46
+ },
47
+ "homepage": "https://github.com/Minecraft-1314/koishi-plugin-custom-image#readme"
40
48
  }
package/readme.md CHANGED
@@ -1,71 +1,71 @@
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 the use of image URLs for direct connection sending
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
- ## 项目贡献者 (Contributors)
54
-
55
- | 贡献者 (Contributor) | 贡献内容 (Contribution) |
56
- |----------------------|-------------------------|
57
- | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
58
- | 素颜API | 提供 API 接口支持 |
59
- | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
60
-
61
- ## 许可协议 (License)
62
-
63
- 本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
64
-
65
- This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
66
-
67
- ## 支持我们 (Support Us)
68
-
69
- 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
70
-
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 the use of image URLs for direct connection sending
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/Minecraft-1314/koishi-plugin-custom-image
25
+ - Issues: https://github.com/Minecraft-1314/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
+ ## 项目贡献者 (Contributors)
54
+
55
+ | 贡献者 (Contributor) | 贡献内容 (Contribution) |
56
+ |----------------------|-------------------------|
57
+ | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
58
+ | 素颜API | 提供 API 接口支持 |
59
+ | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
60
+
61
+ ## 许可协议 (License)
62
+
63
+ 本项目采用 MIT 许可证,详情参见 [LICENSE](LICENSE) 文件。
64
+
65
+ This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.
66
+
67
+ ## 支持我们 (Support Us)
68
+
69
+ 如果这个项目对您有帮助,欢迎点亮右上角的 Star ⭐ 支持我们,这将是对所有贡献者最大的鼓励!
70
+
71
71
  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!