koishi-plugin-imgdraw-selfuse 0.0.6 → 0.0.8
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 +10 -0
- package/lib/index.js +124 -7
- package/package.json +2 -2
package/lib/index.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
26
26
|
txt2imgModel: Schema<string, string>;
|
|
27
27
|
img2imgModel: Schema<string, string>;
|
|
28
28
|
maxImages: Schema<number, number>;
|
|
29
|
+
enableImgCompress: Schema<boolean, boolean>;
|
|
30
|
+
imgMaxWidth: Schema<number, number>;
|
|
31
|
+
imgMaxHeight: Schema<number, number>;
|
|
32
|
+
imgQuality: Schema<number, number>;
|
|
33
|
+
imgMaxFileSize: Schema<number, number>;
|
|
29
34
|
apiList: Schema<Schemastery.ObjectS<{
|
|
30
35
|
enable: Schema<boolean, boolean>;
|
|
31
36
|
apiKey: Schema<string, string>;
|
|
@@ -105,6 +110,11 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
105
110
|
txt2imgModel: Schema<string, string>;
|
|
106
111
|
img2imgModel: Schema<string, string>;
|
|
107
112
|
maxImages: Schema<number, number>;
|
|
113
|
+
enableImgCompress: Schema<boolean, boolean>;
|
|
114
|
+
imgMaxWidth: Schema<number, number>;
|
|
115
|
+
imgMaxHeight: Schema<number, number>;
|
|
116
|
+
imgQuality: Schema<number, number>;
|
|
117
|
+
imgMaxFileSize: Schema<number, number>;
|
|
108
118
|
apiList: Schema<Schemastery.ObjectS<{
|
|
109
119
|
enable: Schema<boolean, boolean>;
|
|
110
120
|
apiKey: Schema<string, string>;
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,14 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
10
10
|
const yaml_1 = __importDefault(require("yaml"));
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
// 尝试导入 sharp,如果未安装则给出提示
|
|
14
|
+
let sharp;
|
|
15
|
+
try {
|
|
16
|
+
sharp = require('sharp');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
sharp = null;
|
|
20
|
+
}
|
|
13
21
|
exports.name = 'ai-image';
|
|
14
22
|
exports.inject = {
|
|
15
23
|
required: ['console', 'i18n', 'database'],
|
|
@@ -30,6 +38,13 @@ exports.Config = koishi_1.Schema.object({
|
|
|
30
38
|
txt2imgModel: koishi_1.Schema.string().default('').description('文生图专用模型,留空则使用通用模型'),
|
|
31
39
|
img2imgModel: koishi_1.Schema.string().default('').description('图生图专用模型,留空则使用通用模型'),
|
|
32
40
|
maxImages: koishi_1.Schema.number().default(5).description('图生图最大支持图片数量'),
|
|
41
|
+
// ==================== 新增:图片压缩配置 ====================
|
|
42
|
+
enableImgCompress: koishi_1.Schema.boolean().default(true).description('启用图生图图片压缩(推荐开启,可防止大图超时)'),
|
|
43
|
+
imgMaxWidth: koishi_1.Schema.number().default(1536).description('图片压缩最大宽度(像素)'),
|
|
44
|
+
imgMaxHeight: koishi_1.Schema.number().default(1536).description('图片压缩最大高度(像素)'),
|
|
45
|
+
imgQuality: koishi_1.Schema.number().default(85).description('JPEG 压缩质量 1-100(越高越清晰,建议 80-90)'),
|
|
46
|
+
imgMaxFileSize: koishi_1.Schema.number().default(3).description('图片最大体积(MB),超过会进一步压缩'),
|
|
47
|
+
// ==========================================================
|
|
33
48
|
apiList: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
34
49
|
enable: koishi_1.Schema.boolean().default(true).description('启用此 API'),
|
|
35
50
|
apiKey: koishi_1.Schema.string().description('API Key'),
|
|
@@ -74,6 +89,11 @@ exports.Config = koishi_1.Schema.object({
|
|
|
74
89
|
// ==================== 主函数 ====================
|
|
75
90
|
async function apply(ctx, cfg) {
|
|
76
91
|
const debug = cfg.debug;
|
|
92
|
+
// 检查 sharp 是否安装
|
|
93
|
+
if (cfg.enableImgCompress && !sharp) {
|
|
94
|
+
logger.warn('图片压缩已启用,但未检测到 sharp 库。请运行:npm install sharp');
|
|
95
|
+
logger.warn('在未安装 sharp 的情况下,将跳过压缩,可能导致大图超时');
|
|
96
|
+
}
|
|
77
97
|
// 加载本地化文件
|
|
78
98
|
try {
|
|
79
99
|
const loc = path_1.default.join(__dirname, 'locales', 'zh-CN.yml');
|
|
@@ -121,8 +141,19 @@ async function apply(ctx, cfg) {
|
|
|
121
141
|
apiIdx.val++;
|
|
122
142
|
return api;
|
|
123
143
|
}
|
|
144
|
+
// ==================== 修复:增强 HTML/XML 清理 ====================
|
|
124
145
|
function cleanHtmlTags(str) {
|
|
125
|
-
|
|
146
|
+
if (!str)
|
|
147
|
+
return '';
|
|
148
|
+
// 1. 清理标准 HTML 标签
|
|
149
|
+
let cleaned = str.replace(/<[^>]+>/g, ' ');
|
|
150
|
+
// 2. 清理 QQ XML 图片标签(如 <img src="..." file="..."/>)
|
|
151
|
+
cleaned = cleaned.replace(/<img\s+[^>]+\/>/gi, ' ');
|
|
152
|
+
// 3. 清理其他 XML 标签
|
|
153
|
+
cleaned = cleaned.replace(/<[^>]+>/g, ' ');
|
|
154
|
+
// 4. 清理多余空格和换行
|
|
155
|
+
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
156
|
+
return cleaned;
|
|
126
157
|
}
|
|
127
158
|
// 增强图片提取函数
|
|
128
159
|
function getImageUrlFromContent(text) {
|
|
@@ -142,11 +173,74 @@ async function apply(ctx, cfg) {
|
|
|
142
173
|
return markdownMatch[1];
|
|
143
174
|
return null;
|
|
144
175
|
}
|
|
145
|
-
// ====================
|
|
176
|
+
// ==================== 新增:图片压缩函数 ====================
|
|
177
|
+
async function compressImage(buffer) {
|
|
178
|
+
if (!sharp || !cfg.enableImgCompress) {
|
|
179
|
+
return buffer;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
let image = sharp(buffer);
|
|
183
|
+
const metadata = await image.metadata();
|
|
184
|
+
// 计算缩放比例
|
|
185
|
+
let width = metadata.width || cfg.imgMaxWidth;
|
|
186
|
+
let height = metadata.height || cfg.imgMaxHeight;
|
|
187
|
+
const ratio = Math.min(cfg.imgMaxWidth / width, cfg.imgMaxHeight / height, 1 // 不放大
|
|
188
|
+
);
|
|
189
|
+
if (ratio < 1) {
|
|
190
|
+
width = Math.round(width * ratio);
|
|
191
|
+
height = Math.round(height * ratio);
|
|
192
|
+
image = image.resize(width, height, { fit: 'inside' });
|
|
193
|
+
}
|
|
194
|
+
// 压缩并转换格式
|
|
195
|
+
let quality = Math.max(1, Math.min(100, cfg.imgQuality));
|
|
196
|
+
let compressed = await image
|
|
197
|
+
.jpeg({ quality, progressive: true, mozjpeg: true })
|
|
198
|
+
.toBuffer();
|
|
199
|
+
// 如果还超过限制,进一步降低质量
|
|
200
|
+
const maxBytes = cfg.imgMaxFileSize * 1024 * 1024;
|
|
201
|
+
while (compressed.length > maxBytes && quality > 40) {
|
|
202
|
+
quality -= 5;
|
|
203
|
+
compressed = await image
|
|
204
|
+
.jpeg({ quality, progressive: true, mozjpeg: true })
|
|
205
|
+
.toBuffer();
|
|
206
|
+
}
|
|
207
|
+
if (debug) {
|
|
208
|
+
logger.info(`图片压缩: ${buffer.length} -> ${compressed.length} bytes ` +
|
|
209
|
+
`(${Math.round(compressed.length / buffer.length * 100)}%), ` +
|
|
210
|
+
`尺寸: ${Math.round(width)}x${Math.round(height)}, 质量: ${quality}`);
|
|
211
|
+
}
|
|
212
|
+
return compressed;
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
logger.error('图片压缩失败,使用原图', e);
|
|
216
|
+
return buffer;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function compressBase64Image(base64Url) {
|
|
220
|
+
if (!sharp || !cfg.enableImgCompress) {
|
|
221
|
+
return base64Url;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const base64Data = base64Url.split(',')[1];
|
|
225
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
226
|
+
const compressed = await compressImage(buffer);
|
|
227
|
+
return `data:image/jpeg;base64,${compressed.toString('base64')}`;
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
logger.error('base64 图片压缩失败,使用原图', e);
|
|
231
|
+
return base64Url;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ==========================================================
|
|
235
|
+
// ==================== 修改:URL 转 base64(带压缩)====================
|
|
146
236
|
async function urlToBase64(url) {
|
|
147
237
|
if (!url)
|
|
148
238
|
return null;
|
|
149
239
|
if (url.startsWith('data:image/')) {
|
|
240
|
+
// 如果已经是 base64,检查是否需要压缩
|
|
241
|
+
if (cfg.enableImgCompress && sharp) {
|
|
242
|
+
return await compressBase64Image(url);
|
|
243
|
+
}
|
|
150
244
|
return url;
|
|
151
245
|
}
|
|
152
246
|
try {
|
|
@@ -154,9 +248,9 @@ async function apply(ctx, cfg) {
|
|
|
154
248
|
responseType: 'arraybuffer',
|
|
155
249
|
timeout: 30000,
|
|
156
250
|
});
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
return `data
|
|
251
|
+
// 压缩图片
|
|
252
|
+
const compressed = await compressImage(Buffer.from(res.data));
|
|
253
|
+
return `data:image/jpeg;base64,${compressed.toString('base64')}`;
|
|
160
254
|
}
|
|
161
255
|
catch (e) {
|
|
162
256
|
logger.error('图片转 base64 失败', e);
|
|
@@ -471,6 +565,18 @@ async function apply(ctx, cfg) {
|
|
|
471
565
|
await safeSend(session, cfg.messages.generating);
|
|
472
566
|
await generateWithMultipleImages(session, finalPrompt, finalUrls, cfg.img2imgModel || cfg.model);
|
|
473
567
|
}
|
|
568
|
+
// ==================== 新增:从 session.elements 提取纯文本 ====================
|
|
569
|
+
function extractTextFromElements(elements) {
|
|
570
|
+
if (!elements || !Array.isArray(elements))
|
|
571
|
+
return '';
|
|
572
|
+
const textParts = [];
|
|
573
|
+
for (const el of elements) {
|
|
574
|
+
if (el.type === 'text') {
|
|
575
|
+
textParts.push(el.attrs?.content || el.attrs?.text || '');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return cleanHtmlTags(textParts.join(' '));
|
|
579
|
+
}
|
|
474
580
|
// ==================== 命令注册 ====================
|
|
475
581
|
const cmd = ctx.command(`${cfg.command} <raw:text>`, 'draw');
|
|
476
582
|
cfg.aliases.forEach((alias) => cmd.alias(alias));
|
|
@@ -507,13 +613,24 @@ async function apply(ctx, cfg) {
|
|
|
507
613
|
return safeSend(session, cfg.messages.blacklisted);
|
|
508
614
|
if (!cfg.enableImg2Img)
|
|
509
615
|
return safeSend(session, cfg.messages.img2imgDisabled);
|
|
510
|
-
|
|
616
|
+
// ==================== 修复:从 elements 提取纯文本,避免 XML 污染 ====================
|
|
617
|
+
let prompt = '';
|
|
618
|
+
if (session.elements && session.elements.length > 0) {
|
|
619
|
+
// 有 elements,从中提取纯文本(排除图片等)
|
|
620
|
+
prompt = extractTextFromElements(session.elements);
|
|
621
|
+
if (debug)
|
|
622
|
+
logger.info('从 elements 提取的 prompt:', prompt);
|
|
623
|
+
}
|
|
624
|
+
// 如果 elements 没有文本,回退到 raw
|
|
625
|
+
if (!prompt && raw) {
|
|
626
|
+
prompt = cleanHtmlTags(raw);
|
|
627
|
+
}
|
|
511
628
|
// 检查消息中是否包含图片(QQ 合并消息)
|
|
512
629
|
const messageImages = session.elements ? koishi_1.h.select(session.elements, 'img') : [];
|
|
513
630
|
if (messageImages.length > 0) {
|
|
514
631
|
// 合并消息:同时包含文字和图片,直接处理
|
|
515
632
|
if (debug)
|
|
516
|
-
logger.info(`检测到合并消息,包含 ${messageImages.length}
|
|
633
|
+
logger.info(`检测到合并消息,包含 ${messageImages.length} 张图片,prompt: "${prompt}"`);
|
|
517
634
|
if (!prompt) {
|
|
518
635
|
// 如果没有文字 prompt,使用默认提示
|
|
519
636
|
await processImg2ImgWithImages(session, '请根据图片内容进行编辑', messageImages);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-imgdraw-selfuse",
|
|
3
|
-
"description": "修改自ai-image的画图插件,支持openai兼容api,增加base64转换以适配GPTimg2
|
|
4
|
-
"version": "0.0.
|
|
3
|
+
"description": "修改自ai-image的画图插件,支持openai兼容api,增加base64转换以适配GPTimg2,支持发送合并消息图生图",
|
|
4
|
+
"version": "0.0.8",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|