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 +30 -0
- package/lib/index.js +179 -28
- package/package.json +2 -2
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
|
-
|
|
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
|
|
421
|
-
if (!
|
|
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:
|
|
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
|
|
487
|
-
if (
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
4
|
-
"version": "0.0
|
|
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": [
|