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 +1 -0
- package/lib/index.js +75 -119
- package/package.json +1 -1
- package/src/index.ts +73 -123
- package/src/locales/zh-CN.yml +0 -9
package/lib/index.d.ts
CHANGED
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"));
|
|
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
|
|
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('
|
|
63
|
-
// 缓存设置
|
|
60
|
+
.description('【容错设置】图片发送超时(毫秒)'),
|
|
64
61
|
autoClearCacheInterval: koishi_1.Schema.number()
|
|
65
62
|
.default(60)
|
|
66
63
|
.min(0)
|
|
67
|
-
.description('
|
|
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(
|
|
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(
|
|
90
|
-
]).catch(
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
130
|
-
return { success: true,
|
|
128
|
+
const filepath = await saveImageToTemp(Buffer.from(res.data), config);
|
|
129
|
+
return { success: true, type: 'file', data: filepath };
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
|
-
catch
|
|
134
|
-
if (retry === config.retryTimes)
|
|
135
|
-
return { success: false,
|
|
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,
|
|
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
|
|
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
|
|
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 (
|
|
195
|
-
await
|
|
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
|
-
|
|
206
|
-
|
|
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
|
|
226
|
-
tipId =
|
|
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) {
|
|
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']
|
|
271
|
+
optional: ['i18n']
|
|
316
272
|
};
|
|
317
|
-
exports.using = [];
|
|
273
|
+
exports.using = [];
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
import { Context, Schema, h, Session } from 'koishi'
|
|
2
|
-
import axios, { AxiosInstance } from '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[]
|
|
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
|
|
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('
|
|
84
|
-
// 缓存设置
|
|
74
|
+
.description('【容错设置】图片发送超时(毫秒)'),
|
|
85
75
|
autoClearCacheInterval: Schema.number()
|
|
86
76
|
.default(60)
|
|
87
77
|
.min(0)
|
|
88
|
-
.description('
|
|
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(
|
|
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(
|
|
113
|
-
]).catch(
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
155
|
-
return { success: true,
|
|
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,
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
219
|
+
createTempDir(config)
|
|
220
|
+
ctx.logger.info('[custom-image] 插件已加载')
|
|
241
221
|
|
|
242
|
-
|
|
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
|
|
263
|
-
tipId = (
|
|
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) {
|
|
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']
|
|
311
|
+
optional: ['i18n']
|
|
362
312
|
}
|
|
363
313
|
|
|
364
|
-
export const using = [] as const
|
|
314
|
+
export const using = [] as const
|
package/src/locales/zh-CN.yml
DELETED
|
@@ -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}}
|