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 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
- return str.replace(/<<[^>]+>/g, '').trim();
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
- // ==================== 新增:URL 转 base64 ====================
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
- const base64 = Buffer.from(res.data).toString('base64');
158
- const mime = res.headers['content-type'] || 'image/jpeg';
159
- return `data:${mime};base64,${base64}`;
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
- const prompt = cleanHtmlTags(raw || '');
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.6",
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": [