koishi-plugin-custom-image 0.1.6 → 0.1.7

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
@@ -16,6 +16,7 @@ export interface Config {
16
16
  imageSendTimeout: number;
17
17
  autoClearCacheInterval: number;
18
18
  imageParseFormat: string;
19
+ tempDir: string;
19
20
  }
20
21
  export declare const Config: Schema<Config>;
21
22
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -6,13 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.using = exports.inject = exports.Config = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
- const axios_1 = __importDefault(require("axios")); // 核心:直接导入axios包
9
+ const axios_1 = __importDefault(require("axios"));
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
- // 插件名称
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const os_1 = __importDefault(require("os"));
12
14
  exports.name = 'custom-image';
13
- // 移除冗余断言,符合Koishi配置规范
14
15
  exports.Config = koishi_1.Schema.object({
15
- // 基础设置
16
16
  enable: koishi_1.Schema.boolean()
17
17
  .default(true)
18
18
  .description('【基础设置】启用插件'),
@@ -25,7 +25,7 @@ exports.Config = koishi_1.Schema.object({
25
25
  sameImageApiInterval: koishi_1.Schema.number()
26
26
  .default(60)
27
27
  .min(0)
28
- .description('【基础设置】重复API调用间隔:相同API的最小调用间隔(秒)'),
28
+ .description('【基础设置】重复API调用间隔(秒)'),
29
29
  timeout: koishi_1.Schema.number()
30
30
  .default(10000)
31
31
  .min(0)
@@ -34,7 +34,6 @@ exports.Config = koishi_1.Schema.object({
34
34
  .default([])
35
35
  .role('textarea')
36
36
  .description('【基础设置】自定义图片API列表(每行一个地址)'),
37
- // 功能开关
38
37
  customImageEnabled: koishi_1.Schema.boolean()
39
38
  .default(true)
40
39
  .description('【功能开关】启用自定义图片API功能'),
@@ -44,14 +43,13 @@ exports.Config = koishi_1.Schema.object({
44
43
  dmjpEnabled: koishi_1.Schema.boolean()
45
44
  .default(true)
46
45
  .description('【功能开关】启用动漫举牌功能'),
47
- // 容错设置
48
46
  ignoreSendError: koishi_1.Schema.boolean()
49
47
  .default(true)
50
- .description('【容错设置】忽略发送错误:避免插件因消息发送失败崩溃'),
48
+ .description('【容错设置】忽略发送错误'),
51
49
  retryTimes: koishi_1.Schema.number()
52
50
  .default(1)
53
51
  .min(0)
54
- .description('【容错设置】API重试次数:请求失败时的重试次数'),
52
+ .description('【容错设置】API重试次数'),
55
53
  retryInterval: koishi_1.Schema.number()
56
54
  .default(500)
57
55
  .min(0)
@@ -59,89 +57,86 @@ exports.Config = koishi_1.Schema.object({
59
57
  imageSendTimeout: koishi_1.Schema.number()
60
58
  .default(15000)
61
59
  .min(0)
62
- .description('【容错设置】图片发送超时(毫秒),0表示不限制'),
63
- // 缓存设置
60
+ .description('【容错设置】图片发送超时(毫秒)'),
64
61
  autoClearCacheInterval: koishi_1.Schema.number()
65
62
  .default(60)
66
63
  .min(0)
67
- .description('【缓存设置】自动清理缓存间隔(分钟),0表示不自动清理'),
68
- // 格式设置
64
+ .description('【缓存设置】自动清理缓存间隔(分钟)'),
69
65
  imageParseFormat: koishi_1.Schema.string()
70
66
  .default('✅ 图片获取成功')
71
- .description('【格式设置】图片发送前的提示文本')
67
+ .description('【格式设置】图片发送前的提示文本'),
68
+ tempDir: koishi_1.Schema.string()
69
+ .default(path_1.default.join(os_1.default.tmpdir(), 'koishi-custom-image'))
70
+ .description('【文件设置】临时图片保存目录')
72
71
  });
73
- // ==================== 全局缓存与工具函数 ====================
74
72
  const processedApi = new Map();
75
73
  const imageBuffer = new Map();
76
- // 延迟函数
74
+ const tempFiles = new Set();
77
75
  const delay = (ms) => new Promise(r => setTimeout(r, ms));
78
- // 带超时的消息发送
79
76
  async function sendTimeout(session, content, config) {
80
77
  if (config.imageSendTimeout <= 0) {
81
- return session.send(content).catch(err => {
82
- if (config.ignoreSendError)
83
- return null;
84
- throw err;
85
- });
78
+ return session.send(content).catch(() => null);
86
79
  }
87
80
  return Promise.race([
88
81
  session.send(content),
89
- new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.imageSendTimeout))
90
- ]).catch(err => {
91
- if (config.ignoreSendError)
92
- return null;
93
- throw err;
94
- });
82
+ new Promise((_, reject) => setTimeout(() => reject(new Error()), config.imageSendTimeout))
83
+ ]).catch(() => null);
95
84
  }
96
- // 清空缓存
97
85
  function clearAllCache() {
98
86
  processedApi.clear();
99
87
  imageBuffer.forEach(buf => clearTimeout(buf.timer));
100
88
  imageBuffer.clear();
89
+ tempFiles.forEach(file => {
90
+ try {
91
+ fs_1.default.unlinkSync(file);
92
+ }
93
+ catch { }
94
+ });
95
+ tempFiles.clear();
101
96
  return true;
102
97
  }
103
- // ==================== 核心业务函数(关键:不再依赖ctx.axios) ====================
104
- /**
105
- * 随机选择自定义图片API
106
- */
107
- function randomSelectApi(customImageApis) {
108
- const validApis = customImageApis.filter(api => api.trim());
109
- if (validApis.length === 0)
110
- return null;
111
- return validApis[Math.floor(Math.random() * validApis.length)];
98
+ function createTempDir(config) {
99
+ if (!fs_1.default.existsSync(config.tempDir)) {
100
+ fs_1.default.mkdirSync(config.tempDir, { recursive: true });
101
+ }
102
+ }
103
+ async function saveImageToTemp(buffer, config) {
104
+ createTempDir(config);
105
+ const filename = `${crypto_1.default.randomUUID()}.jpg`;
106
+ const filepath = path_1.default.join(config.tempDir, filename);
107
+ await fs_1.default.promises.writeFile(filepath, buffer);
108
+ tempFiles.add(filepath);
109
+ return filepath;
110
+ }
111
+ function isImageUrl(url) {
112
+ return /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url);
112
113
  }
113
- /**
114
- * 调用图片API(核心修改:直接使用axios包,无需ctx)
115
- */
116
114
  async function fetchImage(url, config) {
117
- // 直接创建axios实例,不再依赖ctx.axios
118
115
  const http = axios_1.default.create({
119
116
  timeout: config.timeout,
120
117
  headers: {
121
118
  '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'
122
119
  }
123
120
  });
124
- // 重试逻辑
125
121
  for (let retry = 0; retry <= config.retryTimes; retry++) {
126
122
  try {
123
+ if (isImageUrl(url)) {
124
+ return { success: true, type: 'url', data: url };
125
+ }
127
126
  const res = await http.get(url, { responseType: 'arraybuffer' });
128
127
  if (res.status === 200 && res.data) {
129
- const imgBase64 = Buffer.from(res.data).toString('base64');
130
- return { success: true, data: imgBase64, msg: '图片获取成功' };
128
+ const filepath = await saveImageToTemp(Buffer.from(res.data), config);
129
+ return { success: true, type: 'file', data: filepath };
131
130
  }
132
131
  }
133
- catch (err) {
134
- if (retry === config.retryTimes) {
135
- return { success: false, data: null, msg: `请求失败:${err.message}` };
136
- }
132
+ catch {
133
+ if (retry === config.retryTimes)
134
+ return { success: false, type: '', data: '' };
137
135
  await delay(config.retryInterval);
138
136
  }
139
137
  }
140
- return { success: false, data: null, msg: '重试次数用尽,图片获取失败' };
138
+ return { success: false, type: '', data: '' };
141
139
  }
142
- /**
143
- * 处理黑丝举牌API请求
144
- */
145
140
  async function fetchHsjpImage(msg, msg1, msg2, config) {
146
141
  const encodedMsg = encodeURIComponent(msg);
147
142
  const encodedMsg1 = encodeURIComponent(msg1);
@@ -149,38 +144,26 @@ async function fetchHsjpImage(msg, msg1, msg2, config) {
149
144
  const url = `https://api.suyanw.cn/api/hsjp/?msg=${encodedMsg}&msg1=${encodedMsg1}&msg2=${encodedMsg2}`;
150
145
  return fetchImage(url, config);
151
146
  }
152
- /**
153
- * 处理动漫举牌API请求
154
- */
155
147
  async function fetchDmjpImage(text, config) {
156
148
  const encodedText = encodeURIComponent(text);
157
149
  const url = `https://api.suyanw.cn/api/dmjp.php?text=${encodedText}`;
158
150
  return fetchImage(url, config);
159
151
  }
160
- /**
161
- * 处理自定义图片API请求(带防重复逻辑)
162
- */
163
152
  async function processCustomImage(session, apiUrl, config) {
164
153
  const hash = crypto_1.default.createHash('md5').update(apiUrl).digest('hex');
165
154
  const now = Date.now();
166
155
  if (processedApi.get(hash) && now - processedApi.get(hash) < config.sameImageApiInterval * 1000) {
167
- return { success: false, msg: '请勿重复调用该API' };
156
+ return { success: false };
168
157
  }
169
158
  processedApi.set(hash, now);
170
159
  const result = await fetchImage(apiUrl, config);
171
160
  if (result.success && result.data) {
172
161
  await sendTimeout(session, config.imageParseFormat, config);
173
162
  await sendTimeout(session, koishi_1.h.image(result.data), config);
174
- return { success: true, msg: result.msg };
175
- }
176
- else {
177
- await sendTimeout(session, `❌ ${result.msg}`, config);
178
- return { success: false, msg: result.msg };
163
+ return { success: true };
179
164
  }
165
+ return { success: false };
180
166
  }
181
- /**
182
- * 刷新消息缓冲
183
- */
184
167
  async function flush(session, config, manualApi) {
185
168
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
186
169
  const buf = imageBuffer.get(key);
@@ -191,27 +174,24 @@ async function flush(session, config, manualApi) {
191
174
  }
192
175
  if (config.customImageEnabled && apis.length) {
193
176
  const apiUrl = randomSelectApi(apis);
194
- if (!apiUrl) {
195
- await sendTimeout(session, '❌ 自定义图片API列表为空', config);
196
- return;
197
- }
198
- await processCustomImage(session, apiUrl, config);
177
+ if (apiUrl)
178
+ await processCustomImage(session, apiUrl, config);
199
179
  }
200
180
  }
201
- // ==================== 插件主逻辑 ====================
181
+ function randomSelectApi(customImageApis) {
182
+ const validApis = customImageApis.filter(api => api.trim());
183
+ if (validApis.length === 0)
184
+ return null;
185
+ return validApis[Math.floor(Math.random() * validApis.length)];
186
+ }
202
187
  function apply(ctx, config) {
203
- // 初始化:清空缓存
204
188
  clearAllCache();
205
- ctx.logger.info('[custom-image] 自定义图片插件已加载');
206
- // 🔥 关键:删除所有ctx.axios相关的代码,无需加载axios服务
207
- // 随机图片指令
208
- ctx.command('random-image [apis...]', '随机获取自定义图片API的图片')
189
+ createTempDir(config);
190
+ ctx.logger.info('[custom-image] 插件已加载');
191
+ ctx.command('random-image [apis...]', '随机获取自定义图片')
209
192
  .option('api', '-a <api> 指定单个API地址')
210
193
  .action(async ({ session, options = {} }, ...apis) => {
211
- if (!config.enable || !config.customImageEnabled) {
212
- return '❌ 自定义图片功能已禁用';
213
- }
214
- if (!session)
194
+ if (!config.enable || !config.customImageEnabled || !session)
215
195
  return;
216
196
  const targetApi = options.api || (apis.length ? apis.join(' ') : null);
217
197
  if (targetApi) {
@@ -219,13 +199,11 @@ function apply(ctx, config) {
219
199
  }
220
200
  else {
221
201
  const key = `${session.platform}:${session.userId}:${session.channelId}`;
222
- // 显示等待提示
223
202
  let tipId;
224
203
  if (config.showWaitingTip) {
225
- const m = await sendTimeout(session, config.waitingTipText, config);
226
- tipId = m?.messageId || m?.id || m;
204
+ const tip = await sendTimeout(session, config.waitingTipText, config);
205
+ tipId = tip?.messageId || tip?.id || tip;
227
206
  }
228
- // 消息缓冲
229
207
  if (imageBuffer.has(key)) {
230
208
  const b = imageBuffer.get(key);
231
209
  clearTimeout(b.timer);
@@ -240,15 +218,10 @@ function apply(ctx, config) {
240
218
  }
241
219
  }
242
220
  });
243
- // 黑丝举牌指令
244
221
  ctx.command('hsjp <msg> [msg1] [msg2]', '生成黑丝举牌图片')
245
222
  .action(async ({ session }, msg, msg1 = '', msg2 = '') => {
246
- if (!config.enable || !config.hsjpEnabled) {
247
- return '❌ 黑丝举牌功能已禁用';
248
- }
249
- if (!session || !msg) {
250
- return '❌ 请输入举牌文字!格式:hsjp <第一行> [第二行] [第三行]';
251
- }
223
+ if (!config.enable || !config.hsjpEnabled || !session || !msg)
224
+ return;
252
225
  if (config.showWaitingTip) {
253
226
  await sendTimeout(session, config.waitingTipText, config);
254
227
  }
@@ -257,19 +230,11 @@ function apply(ctx, config) {
257
230
  await sendTimeout(session, config.imageParseFormat, config);
258
231
  await sendTimeout(session, koishi_1.h.image(result.data), config);
259
232
  }
260
- else {
261
- await sendTimeout(session, `❌ ${result.msg}`, config);
262
- }
263
233
  });
264
- // 动漫举牌指令
265
234
  ctx.command('dmjp <text>', '生成动漫举牌图片')
266
235
  .action(async ({ session }, text) => {
267
- if (!config.enable || !config.dmjpEnabled) {
268
- return '❌ 动漫举牌功能已禁用';
269
- }
270
- if (!session || !text) {
271
- return '❌ 请输入举牌文字!格式:dmjp <文本>';
272
- }
236
+ if (!config.enable || !config.dmjpEnabled || !session || !text)
237
+ return;
273
238
  if (config.showWaitingTip) {
274
239
  await sendTimeout(session, config.waitingTipText, config);
275
240
  }
@@ -278,40 +243,31 @@ function apply(ctx, config) {
278
243
  await sendTimeout(session, config.imageParseFormat, config);
279
244
  await sendTimeout(session, koishi_1.h.image(result.data), config);
280
245
  }
281
- else {
282
- await sendTimeout(session, `❌ ${result.msg}`, config);
283
- }
284
246
  });
285
- // 清空缓存指令
286
- ctx.command('clear-image-cache', '清空图片插件缓存')
247
+ ctx.command('clear-image-cache', '清空图片缓存')
287
248
  .action(() => {
288
249
  clearAllCache();
289
- return '✅ 图片插件缓存已清空';
290
250
  });
291
- // 定时清理过期缓存(每小时)
292
251
  setInterval(() => {
293
252
  const now = Date.now();
294
253
  processedApi.forEach((time, hash) => {
295
- if (now - time > 86400000) { // 24小时过期
254
+ if (now - time > 86400000) {
296
255
  processedApi.delete(hash);
297
256
  }
298
257
  });
299
258
  }, 3600000);
300
- // 自动清理缓存(按配置)
301
259
  if (config.autoClearCacheInterval > 0) {
302
260
  setInterval(() => {
303
261
  clearAllCache();
304
- ctx.logger.info('[custom-image] 自动清理缓存完成');
262
+ ctx.logger.info('[custom-image] 缓存已自动清理');
305
263
  }, config.autoClearCacheInterval * 60000);
306
264
  }
307
- // 进程退出清理
308
265
  process.on('exit', () => {
309
266
  clearAllCache();
310
- ctx.logger.info('[custom-image] 插件缓存已清理,进程退出');
267
+ ctx.logger.info('[custom-image] 插件缓存已清理');
311
268
  });
312
269
  }
313
- // ==================== 插件元信息(移除axios依赖声明) ====================
314
270
  exports.inject = {
315
- optional: ['i18n'] // 仅保留可选的i18n,移除axios
271
+ optional: ['i18n']
316
272
  };
317
- exports.using = []; // 清空using,无需依赖任何服务
273
+ exports.using = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-custom-image",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Koishi自定义图片API插件(支持黑丝/动漫举牌)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
package/src/index.ts CHANGED
@@ -1,39 +1,32 @@
1
1
  import { Context, Schema, h, Session } from 'koishi'
2
- import axios, { AxiosInstance } from 'axios' // 核心:直接导入axios包
2
+ import axios, { AxiosInstance } from 'axios'
3
3
  import crypto from 'crypto'
4
4
  import fs from 'fs'
5
5
  import path from 'path'
6
+ import os from 'os'
6
7
 
7
- // 插件名称
8
8
  export const name = 'custom-image'
9
9
 
10
- // ==================== 配置接口与 Schema ====================
11
10
  export interface Config {
12
- // 基础设置
13
11
  enable: boolean
14
12
  showWaitingTip: boolean
15
13
  waitingTipText: string
16
14
  sameImageApiInterval: number
17
15
  timeout: number
18
- customImageApis: string[] // 自定义图片API列表
19
- // 功能开关
16
+ customImageApis: string[]
20
17
  customImageEnabled: boolean
21
18
  hsjpEnabled: boolean
22
19
  dmjpEnabled: boolean
23
- // 容错设置
24
20
  ignoreSendError: boolean
25
21
  retryTimes: number
26
22
  retryInterval: number
27
23
  imageSendTimeout: number
28
- // 缓存设置
29
24
  autoClearCacheInterval: number
30
- // 格式设置
31
25
  imageParseFormat: string
26
+ tempDir: string
32
27
  }
33
28
 
34
- // 移除冗余断言,符合Koishi配置规范
35
29
  export const Config: Schema<Config> = Schema.object({
36
- // 基础设置
37
30
  enable: Schema.boolean()
38
31
  .default(true)
39
32
  .description('【基础设置】启用插件'),
@@ -46,7 +39,7 @@ export const Config: Schema<Config> = Schema.object({
46
39
  sameImageApiInterval: Schema.number()
47
40
  .default(60)
48
41
  .min(0)
49
- .description('【基础设置】重复API调用间隔:相同API的最小调用间隔(秒)'),
42
+ .description('【基础设置】重复API调用间隔(秒)'),
50
43
  timeout: Schema.number()
51
44
  .default(10000)
52
45
  .min(0)
@@ -55,7 +48,6 @@ export const Config: Schema<Config> = Schema.object({
55
48
  .default([])
56
49
  .role('textarea')
57
50
  .description('【基础设置】自定义图片API列表(每行一个地址)'),
58
- // 功能开关
59
51
  customImageEnabled: Schema.boolean()
60
52
  .default(true)
61
53
  .description('【功能开关】启用自定义图片API功能'),
@@ -65,14 +57,13 @@ export const Config: Schema<Config> = Schema.object({
65
57
  dmjpEnabled: Schema.boolean()
66
58
  .default(true)
67
59
  .description('【功能开关】启用动漫举牌功能'),
68
- // 容错设置
69
60
  ignoreSendError: Schema.boolean()
70
61
  .default(true)
71
- .description('【容错设置】忽略发送错误:避免插件因消息发送失败崩溃'),
62
+ .description('【容错设置】忽略发送错误'),
72
63
  retryTimes: Schema.number()
73
64
  .default(1)
74
65
  .min(0)
75
- .description('【容错设置】API重试次数:请求失败时的重试次数'),
66
+ .description('【容错设置】API重试次数'),
76
67
  retryInterval: Schema.number()
77
68
  .default(500)
78
69
  .min(0)
@@ -80,65 +71,68 @@ export const Config: Schema<Config> = Schema.object({
80
71
  imageSendTimeout: Schema.number()
81
72
  .default(15000)
82
73
  .min(0)
83
- .description('【容错设置】图片发送超时(毫秒),0表示不限制'),
84
- // 缓存设置
74
+ .description('【容错设置】图片发送超时(毫秒)'),
85
75
  autoClearCacheInterval: Schema.number()
86
76
  .default(60)
87
77
  .min(0)
88
- .description('【缓存设置】自动清理缓存间隔(分钟),0表示不自动清理'),
89
- // 格式设置
78
+ .description('【缓存设置】自动清理缓存间隔(分钟)'),
90
79
  imageParseFormat: Schema.string()
91
80
  .default('✅ 图片获取成功')
92
- .description('【格式设置】图片发送前的提示文本')
81
+ .description('【格式设置】图片发送前的提示文本'),
82
+ tempDir: Schema.string()
83
+ .default(path.join(os.tmpdir(), 'koishi-custom-image'))
84
+ .description('【文件设置】临时图片保存目录')
93
85
  })
94
86
 
95
- // ==================== 全局缓存与工具函数 ====================
96
87
  const processedApi = new Map<string, number>()
97
88
  const imageBuffer = new Map<string, { apis: string[], timer: NodeJS.Timeout, tipMsgId?: string | number }>()
89
+ const tempFiles = new Set<string>()
98
90
 
99
- // 延迟函数
100
91
  const delay = (ms: number) => new Promise(r => setTimeout(r, ms))
101
92
 
102
- // 带超时的消息发送
103
93
  async function sendTimeout(session: Session, content: any, config: Config) {
104
94
  if (config.imageSendTimeout <= 0) {
105
- return session.send(content).catch(err => {
106
- if (config.ignoreSendError) return null
107
- throw err
108
- })
95
+ return session.send(content).catch(() => null)
109
96
  }
110
97
  return Promise.race([
111
98
  session.send(content),
112
- new Promise((_, reject) => setTimeout(() => reject(new Error('发送超时')), config.imageSendTimeout))
113
- ]).catch(err => {
114
- if (config.ignoreSendError) return null
115
- throw err
116
- })
99
+ new Promise((_, reject) => setTimeout(() => reject(new Error()), config.imageSendTimeout))
100
+ ]).catch(() => null)
117
101
  }
118
102
 
119
- // 清空缓存
120
103
  function clearAllCache() {
121
104
  processedApi.clear()
122
105
  imageBuffer.forEach(buf => clearTimeout(buf.timer))
123
106
  imageBuffer.clear()
107
+ tempFiles.forEach(file => {
108
+ try {
109
+ fs.unlinkSync(file)
110
+ } catch {}
111
+ })
112
+ tempFiles.clear()
124
113
  return true
125
114
  }
126
115
 
127
- // ==================== 核心业务函数(关键:不再依赖ctx.axios) ====================
128
- /**
129
- * 随机选择自定义图片API
130
- */
131
- function randomSelectApi(customImageApis: string[]): string | null {
132
- const validApis = customImageApis.filter(api => api.trim())
133
- if (validApis.length === 0) return null
134
- return validApis[Math.floor(Math.random() * validApis.length)]
116
+ function createTempDir(config: Config) {
117
+ if (!fs.existsSync(config.tempDir)) {
118
+ fs.mkdirSync(config.tempDir, { recursive: true })
119
+ }
120
+ }
121
+
122
+ async function saveImageToTemp(buffer: Buffer, config: Config) {
123
+ createTempDir(config)
124
+ const filename = `${crypto.randomUUID()}.jpg`
125
+ const filepath = path.join(config.tempDir, filename)
126
+ await fs.promises.writeFile(filepath, buffer)
127
+ tempFiles.add(filepath)
128
+ return filepath
129
+ }
130
+
131
+ function isImageUrl(url: string) {
132
+ return /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)/i.test(url)
135
133
  }
136
134
 
137
- /**
138
- * 调用图片API(核心修改:直接使用axios包,无需ctx)
139
- */
140
135
  async function fetchImage(url: string, config: Config) {
141
- // 直接创建axios实例,不再依赖ctx.axios
142
136
  const http: AxiosInstance = axios.create({
143
137
  timeout: config.timeout,
144
138
  headers: {
@@ -146,56 +140,46 @@ async function fetchImage(url: string, config: Config) {
146
140
  }
147
141
  })
148
142
 
149
- // 重试逻辑
150
143
  for (let retry = 0; retry <= config.retryTimes; retry++) {
151
144
  try {
145
+ if (isImageUrl(url)) {
146
+ return { success: true, type: 'url', data: url }
147
+ }
148
+
152
149
  const res = await http.get(url, { responseType: 'arraybuffer' })
153
150
  if (res.status === 200 && res.data) {
154
- const imgBase64 = Buffer.from(res.data).toString('base64')
155
- return { success: true, data: imgBase64, msg: '图片获取成功' }
156
- }
157
- } catch (err) {
158
- if (retry === config.retryTimes) {
159
- return { success: false, data: null, msg: `请求失败:${(err as Error).message}` }
151
+ const filepath = await saveImageToTemp(Buffer.from(res.data), config)
152
+ return { success: true, type: 'file', data: filepath }
160
153
  }
154
+ } catch {
155
+ if (retry === config.retryTimes) return { success: false, type: '', data: '' }
161
156
  await delay(config.retryInterval)
162
157
  }
163
158
  }
164
159
 
165
- return { success: false, data: null, msg: '重试次数用尽,图片获取失败' }
160
+ return { success: false, type: '', data: '' }
166
161
  }
167
162
 
168
- /**
169
- * 处理黑丝举牌API请求
170
- */
171
163
  async function fetchHsjpImage(msg: string, msg1: string, msg2: string, config: Config) {
172
164
  const encodedMsg = encodeURIComponent(msg)
173
165
  const encodedMsg1 = encodeURIComponent(msg1)
174
166
  const encodedMsg2 = encodeURIComponent(msg2)
175
167
  const url = `https://api.suyanw.cn/api/hsjp/?msg=${encodedMsg}&msg1=${encodedMsg1}&msg2=${encodedMsg2}`
176
-
177
168
  return fetchImage(url, config)
178
169
  }
179
170
 
180
- /**
181
- * 处理动漫举牌API请求
182
- */
183
171
  async function fetchDmjpImage(text: string, config: Config) {
184
172
  const encodedText = encodeURIComponent(text)
185
173
  const url = `https://api.suyanw.cn/api/dmjp.php?text=${encodedText}`
186
-
187
174
  return fetchImage(url, config)
188
175
  }
189
176
 
190
- /**
191
- * 处理自定义图片API请求(带防重复逻辑)
192
- */
193
177
  async function processCustomImage(session: Session, apiUrl: string, config: Config) {
194
178
  const hash = crypto.createHash('md5').update(apiUrl).digest('hex')
195
179
  const now = Date.now()
196
180
 
197
181
  if (processedApi.get(hash) && now - processedApi.get(hash)! < config.sameImageApiInterval * 1000) {
198
- return { success: false, msg: '请勿重复调用该API' }
182
+ return { success: false }
199
183
  }
200
184
  processedApi.set(hash, now)
201
185
 
@@ -203,16 +187,11 @@ async function processCustomImage(session: Session, apiUrl: string, config: Conf
203
187
  if (result.success && result.data) {
204
188
  await sendTimeout(session, config.imageParseFormat, config)
205
189
  await sendTimeout(session, h.image(result.data), config)
206
- return { success: true, msg: result.msg }
207
- } else {
208
- await sendTimeout(session, `❌ ${result.msg}`, config)
209
- return { success: false, msg: result.msg }
190
+ return { success: true }
210
191
  }
192
+ return { success: false }
211
193
  }
212
194
 
213
- /**
214
- * 刷新消息缓冲
215
- */
216
195
  async function flush(session: Session, config: Config, manualApi?: string) {
217
196
  const key = `${session.platform}:${session.userId}:${session.channelId}`
218
197
  const buf = imageBuffer.get(key)
@@ -225,30 +204,25 @@ async function flush(session: Session, config: Config, manualApi?: string) {
225
204
 
226
205
  if (config.customImageEnabled && apis.length) {
227
206
  const apiUrl = randomSelectApi(apis)
228
- if (!apiUrl) {
229
- await sendTimeout(session, '❌ 自定义图片API列表为空', config)
230
- return
231
- }
232
- await processCustomImage(session, apiUrl, config)
207
+ if (apiUrl) await processCustomImage(session, apiUrl, config)
233
208
  }
234
209
  }
235
210
 
236
- // ==================== 插件主逻辑 ====================
211
+ function randomSelectApi(customImageApis: string[]): string | null {
212
+ const validApis = customImageApis.filter(api => api.trim())
213
+ if (validApis.length === 0) return null
214
+ return validApis[Math.floor(Math.random() * validApis.length)]
215
+ }
216
+
237
217
  export function apply(ctx: Context, config: Config) {
238
- // 初始化:清空缓存
239
218
  clearAllCache()
240
- ctx.logger.info('[custom-image] 自定义图片插件已加载')
219
+ createTempDir(config)
220
+ ctx.logger.info('[custom-image] 插件已加载')
241
221
 
242
- // 🔥 关键:删除所有ctx.axios相关的代码,无需加载axios服务
243
-
244
- // 随机图片指令
245
- ctx.command('random-image [apis...]', '随机获取自定义图片API的图片')
222
+ ctx.command('random-image [apis...]', '随机获取自定义图片')
246
223
  .option('api', '-a <api> 指定单个API地址')
247
224
  .action(async ({ session, options = {} }, ...apis) => {
248
- if (!config.enable || !config.customImageEnabled) {
249
- return '❌ 自定义图片功能已禁用'
250
- }
251
- if (!session) return
225
+ if (!config.enable || !config.customImageEnabled || !session) return
252
226
 
253
227
  const targetApi = options.api || (apis.length ? apis.join(' ') : null)
254
228
  if (targetApi) {
@@ -256,14 +230,12 @@ export function apply(ctx: Context, config: Config) {
256
230
  } else {
257
231
  const key = `${session.platform}:${session.userId}:${session.channelId}`
258
232
 
259
- // 显示等待提示
260
233
  let tipId
261
234
  if (config.showWaitingTip) {
262
- const m = await sendTimeout(session, config.waitingTipText, config)
263
- tipId = (m as any)?.messageId || (m as any)?.id || m
235
+ const tip = await sendTimeout(session, config.waitingTipText, config)
236
+ tipId = (tip as any)?.messageId || (tip as any)?.id || tip
264
237
  }
265
238
 
266
- // 消息缓冲
267
239
  if (imageBuffer.has(key)) {
268
240
  const b = imageBuffer.get(key)!
269
241
  clearTimeout(b.timer)
@@ -278,15 +250,9 @@ export function apply(ctx: Context, config: Config) {
278
250
  }
279
251
  })
280
252
 
281
- // 黑丝举牌指令
282
253
  ctx.command('hsjp <msg> [msg1] [msg2]', '生成黑丝举牌图片')
283
254
  .action(async ({ session }, msg, msg1 = '', msg2 = '') => {
284
- if (!config.enable || !config.hsjpEnabled) {
285
- return '❌ 黑丝举牌功能已禁用'
286
- }
287
- if (!session || !msg) {
288
- return '❌ 请输入举牌文字!格式:hsjp <第一行> [第二行] [第三行]'
289
- }
255
+ if (!config.enable || !config.hsjpEnabled || !session || !msg) return
290
256
 
291
257
  if (config.showWaitingTip) {
292
258
  await sendTimeout(session, config.waitingTipText, config)
@@ -296,20 +262,12 @@ export function apply(ctx: Context, config: Config) {
296
262
  if (result.success && result.data) {
297
263
  await sendTimeout(session, config.imageParseFormat, config)
298
264
  await sendTimeout(session, h.image(result.data), config)
299
- } else {
300
- await sendTimeout(session, `❌ ${result.msg}`, config)
301
265
  }
302
266
  })
303
267
 
304
- // 动漫举牌指令
305
268
  ctx.command('dmjp <text>', '生成动漫举牌图片')
306
269
  .action(async ({ session }, text) => {
307
- if (!config.enable || !config.dmjpEnabled) {
308
- return '❌ 动漫举牌功能已禁用'
309
- }
310
- if (!session || !text) {
311
- return '❌ 请输入举牌文字!格式:dmjp <文本>'
312
- }
270
+ if (!config.enable || !config.dmjpEnabled || !session || !text) return
313
271
 
314
272
  if (config.showWaitingTip) {
315
273
  await sendTimeout(session, config.waitingTipText, config)
@@ -319,46 +277,38 @@ export function apply(ctx: Context, config: Config) {
319
277
  if (result.success && result.data) {
320
278
  await sendTimeout(session, config.imageParseFormat, config)
321
279
  await sendTimeout(session, h.image(result.data), config)
322
- } else {
323
- await sendTimeout(session, `❌ ${result.msg}`, config)
324
280
  }
325
281
  })
326
282
 
327
- // 清空缓存指令
328
- ctx.command('clear-image-cache', '清空图片插件缓存')
283
+ ctx.command('clear-image-cache', '清空图片缓存')
329
284
  .action(() => {
330
285
  clearAllCache()
331
- return '✅ 图片插件缓存已清空'
332
286
  })
333
287
 
334
- // 定时清理过期缓存(每小时)
335
288
  setInterval(() => {
336
289
  const now = Date.now()
337
290
  processedApi.forEach((time, hash) => {
338
- if (now - time > 86400000) { // 24小时过期
291
+ if (now - time > 86400000) {
339
292
  processedApi.delete(hash)
340
293
  }
341
294
  })
342
295
  }, 3600000)
343
296
 
344
- // 自动清理缓存(按配置)
345
297
  if (config.autoClearCacheInterval > 0) {
346
298
  setInterval(() => {
347
299
  clearAllCache()
348
- ctx.logger.info('[custom-image] 自动清理缓存完成')
300
+ ctx.logger.info('[custom-image] 缓存已自动清理')
349
301
  }, config.autoClearCacheInterval * 60000)
350
302
  }
351
303
 
352
- // 进程退出清理
353
304
  process.on('exit', () => {
354
305
  clearAllCache()
355
- ctx.logger.info('[custom-image] 插件缓存已清理,进程退出')
306
+ ctx.logger.info('[custom-image] 插件缓存已清理')
356
307
  })
357
308
  }
358
309
 
359
- // ==================== 插件元信息(移除axios依赖声明) ====================
360
310
  export const inject = {
361
- optional: ['i18n'] // 仅保留可选的i18n,移除axios
311
+ optional: ['i18n']
362
312
  }
363
313
 
364
- export const using = [] as const // 清空using,无需依赖任何服务
314
+ export const using = [] as const
@@ -1,9 +0,0 @@
1
- customImage:
2
- noApi: 自定义图片API列表为空,请先在插件配置中添加!
3
- error: 图片获取失败:{{msg}}
4
- hsjpDisabled: 黑丝举牌功能已禁用,请联系管理员开启!
5
- hsjpNoText: 请输入举牌文字!格式:hsjp <第一行> [第二行] [第三行]
6
- hsjpError: 黑丝举牌生成失败:{{msg}}
7
- dmjpDisabled: 动漫举牌功能已禁用,请联系管理员开启!
8
- dmjpNoText: 请输入举牌文字!格式:dmjp <文本>
9
- dmjpError: 动漫举牌生成失败:{{msg}}