koishi-plugin-imgdraw-selfuse 0.0.8 → 0.1.0

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
@@ -31,6 +31,21 @@ export declare const Config: Schema<Schemastery.ObjectS<{
31
31
  imgMaxHeight: Schema<number, number>;
32
32
  imgQuality: Schema<number, number>;
33
33
  imgMaxFileSize: Schema<number, number>;
34
+ enableImg2ImgBase64: Schema<boolean, boolean>;
35
+ enablePresets: Schema<boolean, boolean>;
36
+ presets: Schema<Schemastery.ObjectS<{
37
+ enable: Schema<boolean, boolean>;
38
+ text: Schema<string, string>;
39
+ command: Schema<string, string>;
40
+ keyword: Schema<string, string>;
41
+ enableKeywordMatch: Schema<boolean, boolean>;
42
+ }>[], Schemastery.ObjectT<{
43
+ enable: Schema<boolean, boolean>;
44
+ text: Schema<string, string>;
45
+ command: Schema<string, string>;
46
+ keyword: Schema<string, string>;
47
+ enableKeywordMatch: Schema<boolean, boolean>;
48
+ }>[]>;
34
49
  apiList: Schema<Schemastery.ObjectS<{
35
50
  enable: Schema<boolean, boolean>;
36
51
  apiKey: Schema<string, string>;
@@ -115,6 +130,21 @@ export declare const Config: Schema<Schemastery.ObjectS<{
115
130
  imgMaxHeight: Schema<number, number>;
116
131
  imgQuality: Schema<number, number>;
117
132
  imgMaxFileSize: Schema<number, number>;
133
+ enableImg2ImgBase64: Schema<boolean, boolean>;
134
+ enablePresets: Schema<boolean, boolean>;
135
+ presets: Schema<Schemastery.ObjectS<{
136
+ enable: Schema<boolean, boolean>;
137
+ text: Schema<string, string>;
138
+ command: Schema<string, string>;
139
+ keyword: Schema<string, string>;
140
+ enableKeywordMatch: Schema<boolean, boolean>;
141
+ }>[], Schemastery.ObjectT<{
142
+ enable: Schema<boolean, boolean>;
143
+ text: Schema<string, string>;
144
+ command: Schema<string, string>;
145
+ keyword: Schema<string, string>;
146
+ enableKeywordMatch: Schema<boolean, boolean>;
147
+ }>[]>;
118
148
  apiList: Schema<Schemastery.ObjectS<{
119
149
  enable: Schema<boolean, boolean>;
120
150
  apiKey: Schema<string, string>;
package/lib/index.js CHANGED
@@ -25,6 +25,14 @@ exports.inject = {
25
25
  };
26
26
  const logger = new koishi_1.Logger('ai-image');
27
27
  // ==================== 配置 Schema ====================
28
+ // 预置提示词单项配置
29
+ const PresetItem = koishi_1.Schema.object({
30
+ enable: koishi_1.Schema.boolean().default(true).description('启用此预置提示词'),
31
+ text: koishi_1.Schema.string().default('').description('预置提示词文本(将自动添加到 prompt 前)'),
32
+ command: koishi_1.Schema.string().default('').description('触发指令(如 draw0,留空则自动生成 drawN)'),
33
+ keyword: koishi_1.Schema.string().default('').description('匹配关键词(如 猫娘,留空则不启用关键词匹配)'),
34
+ enableKeywordMatch: koishi_1.Schema.boolean().default(false).description('启用关键词匹配(用户 prompt 包含关键词时自动添加)'),
35
+ }).description('预置提示词配置项');
28
36
  exports.Config = koishi_1.Schema.object({
29
37
  debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,输出完整请求日志'),
30
38
  apiStrategy: koishi_1.Schema.union([
@@ -38,13 +46,20 @@ exports.Config = koishi_1.Schema.object({
38
46
  txt2imgModel: koishi_1.Schema.string().default('').description('文生图专用模型,留空则使用通用模型'),
39
47
  img2imgModel: koishi_1.Schema.string().default('').description('图生图专用模型,留空则使用通用模型'),
40
48
  maxImages: koishi_1.Schema.number().default(5).description('图生图最大支持图片数量'),
41
- // ==================== 新增:图片压缩配置 ====================
49
+ // ==================== 图片压缩配置 ====================
42
50
  enableImgCompress: koishi_1.Schema.boolean().default(true).description('启用图生图图片压缩(推荐开启,可防止大图超时)'),
43
51
  imgMaxWidth: koishi_1.Schema.number().default(1536).description('图片压缩最大宽度(像素)'),
44
52
  imgMaxHeight: koishi_1.Schema.number().default(1536).description('图片压缩最大高度(像素)'),
45
53
  imgQuality: koishi_1.Schema.number().default(85).description('JPEG 压缩质量 1-100(越高越清晰,建议 80-90)'),
46
54
  imgMaxFileSize: koishi_1.Schema.number().default(3).description('图片最大体积(MB),超过会进一步压缩'),
47
55
  // ==========================================================
56
+ // ==================== 新增:图生图 base64 转换开关 ====================
57
+ enableImg2ImgBase64: koishi_1.Schema.boolean().default(true).description('图生图将图片转换为 base64(关闭则直接传 URL,部分 API 不需要 base64)'),
58
+ // ==========================================================
59
+ // ==================== 预置提示词配置 ====================
60
+ enablePresets: koishi_1.Schema.boolean().default(false).description('启用预置提示词功能(仅用于文生图)'),
61
+ presets: koishi_1.Schema.array(PresetItem).default([]).description('预置提示词列表(可添加多个,支持指令触发和关键词匹配)'),
62
+ // ==========================================================
48
63
  apiList: koishi_1.Schema.array(koishi_1.Schema.object({
49
64
  enable: koishi_1.Schema.boolean().default(true).description('启用此 API'),
50
65
  apiKey: koishi_1.Schema.string().description('API Key'),
@@ -52,7 +67,7 @@ exports.Config = koishi_1.Schema.object({
52
67
  })).default([]).description('API 配置列表(支持多账号负载)'),
53
68
  enableTxt2Img: koishi_1.Schema.boolean().default(true).description('启用文生图'),
54
69
  enableImg2Img: koishi_1.Schema.boolean().default(true).description('启用图生图'),
55
- command: koishi_1.Schema.string().default('draw').description('文生图指令'),
70
+ command: koishi_1.Schema.string().default('draw').description('文生图主指令'),
56
71
  aliases: koishi_1.Schema.array(String).default([]).description('文生图指令别名'),
57
72
  img2imgCommand: koishi_1.Schema.string().default('imgdraw').description('图生图指令'),
58
73
  img2imgAliases: koishi_1.Schema.array(String).default([]).description('图生图指令别名'),
@@ -173,7 +188,7 @@ async function apply(ctx, cfg) {
173
188
  return markdownMatch[1];
174
189
  return null;
175
190
  }
176
- // ==================== 新增:图片压缩函数 ====================
191
+ // ==================== 图片压缩函数 ====================
177
192
  async function compressImage(buffer) {
178
193
  if (!sharp || !cfg.enableImgCompress) {
179
194
  return buffer;
@@ -232,23 +247,36 @@ async function apply(ctx, cfg) {
232
247
  }
233
248
  }
234
249
  // ==========================================================
235
- // ==================== 修改:URL 转 base64(带压缩)====================
236
- async function urlToBase64(url) {
250
+ // ==================== 修改:URL 转 base64(支持开关)====================
251
+ /**
252
+ * 处理图片 URL,根据配置决定返回 base64 还是原始 URL
253
+ * @param url 图片 URL
254
+ * @param forceBase64 强制使用 base64(覆盖全局配置)
255
+ */
256
+ async function processImageUrl(url, forceBase64) {
237
257
  if (!url)
238
258
  return null;
259
+ // 如果已经是 base64,检查是否需要压缩
239
260
  if (url.startsWith('data:image/')) {
240
- // 如果已经是 base64,检查是否需要压缩
241
261
  if (cfg.enableImgCompress && sharp) {
242
262
  return await compressBase64Image(url);
243
263
  }
244
264
  return url;
245
265
  }
266
+ // 判断是否转换为 base64
267
+ const needBase64 = forceBase64 !== undefined ? forceBase64 : cfg.enableImg2ImgBase64;
268
+ if (!needBase64) {
269
+ // 直接返回 URL(不转换 base64)
270
+ if (debug)
271
+ logger.info('图生图使用原始 URL:', url.slice(0, 100));
272
+ return url;
273
+ }
274
+ // 转换为 base64
246
275
  try {
247
276
  const res = await axios_1.default.get(url, {
248
277
  responseType: 'arraybuffer',
249
278
  timeout: 30000,
250
279
  });
251
- // 压缩图片
252
280
  const compressed = await compressImage(Buffer.from(res.data));
253
281
  return `data:image/jpeg;base64,${compressed.toString('base64')}`;
254
282
  }
@@ -257,6 +285,7 @@ async function apply(ctx, cfg) {
257
285
  throw new Error('图片下载失败,请检查 selfUrl 是否可访问');
258
286
  }
259
287
  }
288
+ // ==========================================================
260
289
  // 统一发送图片函数
261
290
  async function sendImage(session, imgUrl) {
262
291
  const trimmed = imgUrl.trim();
@@ -401,6 +430,68 @@ async function apply(ctx, cfg) {
401
430
  }
402
431
  return { success, fail };
403
432
  }
433
+ // ==================== 预置提示词处理函数 ====================
434
+ /**
435
+ * 获取所有启用的预置提示词配置
436
+ */
437
+ function getEnabledPresets() {
438
+ if (!cfg.enablePresets || !cfg.presets)
439
+ return [];
440
+ return cfg.presets.filter((p) => p.enable && p.text);
441
+ }
442
+ /**
443
+ * 根据指令名查找匹配的预置提示词
444
+ * @param cmd 指令名,如 "draw0"
445
+ */
446
+ function findPresetByCommand(cmd) {
447
+ const presets = getEnabledPresets();
448
+ for (const preset of presets) {
449
+ const presetCmd = preset.command?.trim();
450
+ if (presetCmd && presetCmd === cmd) {
451
+ return preset;
452
+ }
453
+ }
454
+ return null;
455
+ }
456
+ /**
457
+ * 根据用户 prompt 关键词匹配预置提示词
458
+ * @param prompt 用户输入的 prompt
459
+ */
460
+ function findPresetsByKeyword(prompt) {
461
+ const presets = getEnabledPresets();
462
+ const matched = [];
463
+ if (!prompt)
464
+ return matched;
465
+ const lowerPrompt = prompt.toLowerCase();
466
+ for (const preset of presets) {
467
+ if (preset.enableKeywordMatch && preset.keyword) {
468
+ const keywords = preset.keyword.split(/[,,|\/\s]+/).map((k) => k.trim().toLowerCase()).filter(Boolean);
469
+ for (const kw of keywords) {
470
+ if (lowerPrompt.includes(kw)) {
471
+ matched.push(preset);
472
+ break;
473
+ }
474
+ }
475
+ }
476
+ }
477
+ return matched;
478
+ }
479
+ /**
480
+ * 拼接预置提示词和用户 prompt
481
+ * @param prompt 用户输入的原始 prompt
482
+ * @param presets 要应用的预置提示词列表
483
+ */
484
+ function buildPromptWithPresets(prompt, presets) {
485
+ if (!presets || presets.length === 0)
486
+ return prompt;
487
+ const presetTexts = presets.map((p) => p.text.trim()).filter(Boolean);
488
+ if (presetTexts.length === 0)
489
+ return prompt;
490
+ const combinedPreset = presetTexts.join(',');
491
+ // 预置提示词放在前面,用户 prompt 在后面
492
+ return `${combinedPreset},${prompt}`;
493
+ }
494
+ // ==========================================================
404
495
  // ==================== 核心生成函数 ====================
405
496
  async function generate(session, prompt, imageUrl, modelOverride) {
406
497
  if (!checkRateLimit()) {
@@ -417,14 +508,14 @@ async function apply(ctx, cfg) {
417
508
  const model = modelOverride || cfg.model;
418
509
  let content;
419
510
  if (imageUrl) {
420
- const base64Url = await urlToBase64(imageUrl);
421
- if (!base64Url) {
422
- await safeSend(session, cfg.messages.fail + '(图片转换失败)');
511
+ const processedUrl = await processImageUrl(imageUrl);
512
+ if (!processedUrl) {
513
+ await safeSend(session, cfg.messages.fail + '(图片处理失败)');
423
514
  return;
424
515
  }
425
516
  content = [
426
517
  { type: 'text', text: prompt },
427
- { type: 'image_url', image_url: { url: base64Url } },
518
+ { type: 'image_url', image_url: { url: processedUrl } },
428
519
  ];
429
520
  }
430
521
  else {
@@ -483,14 +574,14 @@ async function apply(ctx, cfg) {
483
574
  }
484
575
  const model = modelOverride || cfg.model;
485
576
  const finalPrompt = prompt.replace('{url}', imageUrls.join(', '));
486
- const base64Urls = (await Promise.all(imageUrls.map(url => urlToBase64(url)))).filter((url) => url !== null);
487
- if (base64Urls.length === 0) {
488
- await safeSend(session, cfg.messages.fail + '(图片转换失败)');
577
+ const processedUrls = (await Promise.all(imageUrls.map(url => processImageUrl(url)))).filter((url) => url !== null);
578
+ if (processedUrls.length === 0) {
579
+ await safeSend(session, cfg.messages.fail + '(图片处理失败)');
489
580
  return;
490
581
  }
491
582
  const content = [
492
583
  { type: 'text', text: finalPrompt },
493
- ...base64Urls.map(url => ({ type: 'image_url', image_url: { url } })),
584
+ ...processedUrls.map(url => ({ type: 'image_url', image_url: { url } })),
494
585
  ];
495
586
  const body = {
496
587
  model,
@@ -577,30 +668,90 @@ async function apply(ctx, cfg) {
577
668
  }
578
669
  return cleanHtmlTags(textParts.join(' '));
579
670
  }
671
+ // ==================== 新增:文生图核心逻辑(支持预置提示词)====================
672
+ async function doTxt2Img(session, rawPrompt, explicitPresets) {
673
+ if (!session)
674
+ return;
675
+ if (await isBlacklisted(session.userId)) {
676
+ await safeSend(session, cfg.messages.blacklisted);
677
+ return;
678
+ }
679
+ if (!cfg.enableTxt2Img) {
680
+ await safeSend(session, cfg.messages.txt2imgDisabled);
681
+ return;
682
+ }
683
+ let prompt = cleanHtmlTags(rawPrompt || '');
684
+ if (!prompt) {
685
+ await safeSend(session, cfg.messages.empty);
686
+ return;
687
+ }
688
+ // 处理预置提示词
689
+ const appliedPresets = [];
690
+ // 1. 显式指定的预置提示词(通过指令触发)
691
+ if (explicitPresets && explicitPresets.length > 0) {
692
+ appliedPresets.push(...explicitPresets);
693
+ }
694
+ // 2. 关键词匹配(仅在未通过指令触发时,或允许叠加时)
695
+ if (cfg.enablePresets) {
696
+ const keywordPresets = findPresetsByKeyword(prompt);
697
+ for (const kp of keywordPresets) {
698
+ // 避免重复添加
699
+ if (!appliedPresets.some((p) => p.text === kp.text)) {
700
+ appliedPresets.push(kp);
701
+ }
702
+ }
703
+ }
704
+ // 拼接预置提示词
705
+ if (appliedPresets.length > 0) {
706
+ prompt = buildPromptWithPresets(prompt, appliedPresets);
707
+ if (debug) {
708
+ logger.info(`应用预置提示词: ${appliedPresets.map((p) => p.command || 'keyword').join(', ')}`);
709
+ logger.info(`最终 prompt: ${prompt.slice(0, 200)}...`);
710
+ }
711
+ }
712
+ await safeSend(session, cfg.messages.generating);
713
+ const finalPrompt = cfg.txt2imgPrompt.replace('{prompt}', prompt);
714
+ const model = cfg.txt2imgModel || cfg.model;
715
+ await generate(session, finalPrompt, undefined, model);
716
+ }
717
+ // ==========================================================
580
718
  // ==================== 命令注册 ====================
719
+ // 主文生图指令
581
720
  const cmd = ctx.command(`${cfg.command} <raw:text>`, 'draw');
582
721
  cfg.aliases.forEach((alias) => cmd.alias(alias));
583
722
  cmd.action(async ({ session }, raw) => {
584
723
  try {
585
- if (!session)
586
- return;
587
- if (await isBlacklisted(session.userId))
588
- return safeSend(session, cfg.messages.blacklisted);
589
- if (!cfg.enableTxt2Img)
590
- return safeSend(session, cfg.messages.txt2imgDisabled);
591
- const prompt = cleanHtmlTags(raw || '');
592
- if (!prompt)
593
- return safeSend(session, cfg.messages.empty);
594
- await safeSend(session, cfg.messages.generating);
595
- const finalPrompt = cfg.txt2imgPrompt.replace('{prompt}', prompt);
596
- const model = cfg.txt2imgModel || cfg.model;
597
- await generate(session, finalPrompt, undefined, model);
724
+ await doTxt2Img(session, raw);
598
725
  }
599
726
  catch (e) {
600
727
  logger.error('文生图命令异常', e);
601
728
  await safeSend(session, cfg.messages.fail);
602
729
  }
603
730
  });
731
+ // ==================== 新增:预置提示词指令注册 ====================
732
+ if (cfg.enablePresets && cfg.presets) {
733
+ const presets = getEnabledPresets();
734
+ for (let i = 0; i < presets.length; i++) {
735
+ const preset = presets[i];
736
+ const presetCmd = preset.command?.trim() || `draw${i}`;
737
+ // 注册指令
738
+ const pCmd = ctx.command(`${presetCmd} <raw:text>`, `使用预置提示词: ${preset.text.slice(0, 20)}...`);
739
+ pCmd.action(async ({ session }, raw) => {
740
+ try {
741
+ if (debug)
742
+ logger.info(`预置指令触发: ${presetCmd}, 预置文本: ${preset.text.slice(0, 50)}`);
743
+ await doTxt2Img(session, raw, [preset]);
744
+ }
745
+ catch (e) {
746
+ logger.error(`预置指令 ${presetCmd} 异常`, e);
747
+ await safeSend(session, cfg.messages.fail);
748
+ }
749
+ });
750
+ if (debug)
751
+ logger.info(`注册预置提示词指令: ${presetCmd}`);
752
+ }
753
+ }
754
+ // ==========================================================
604
755
  // ==================== 修改:图生图命令支持合并消息 ====================
605
756
  const imgCmd = ctx.command(`${cfg.img2imgCommand} <raw:text>`, 'imgdraw');
606
757
  cfg.img2imgAliases.forEach((alias) => imgCmd.alias(alias));
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.8",
3
+ "description": "修改自ai-image的画图插件,支持openai兼容api,增加base64转换自由开启,增加图文合并消息图生图,增加预置提示词",
4
+ "version": "0.1.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [