koishi-plugin-imgdraw-selfuse 0.0.2 → 0.0.4
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 +15 -12
- package/lib/index.js +51 -20
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { Schema } from 'koishi';
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
2
|
export declare const name = "ai-image";
|
|
3
3
|
export declare const inject: {
|
|
4
4
|
required: string[];
|
|
5
5
|
optional: string[];
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
declare module 'koishi' {
|
|
8
|
+
interface Tables {
|
|
9
|
+
ai_image_blacklist: BlacklistEntry;
|
|
10
|
+
}
|
|
11
|
+
interface Context {
|
|
12
|
+
assets?: any;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
interface BlacklistEntry {
|
|
16
|
+
id: string;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
}
|
|
8
19
|
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
9
20
|
debug: Schema<boolean, boolean>;
|
|
10
21
|
apiStrategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
|
|
@@ -164,14 +175,6 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
164
175
|
blacklistListTitle: Schema<string, string>;
|
|
165
176
|
}>>;
|
|
166
177
|
}>>;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
ai_image_blacklist: AIImageBlacklist;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
interface AIImageBlacklist {
|
|
173
|
-
id: string;
|
|
174
|
-
createdAt: Date;
|
|
175
|
-
}
|
|
176
|
-
export declare function apply(ctx: any, cfg: Infer<typeof Config>): Promise<void>;
|
|
178
|
+
export type Config = any;
|
|
179
|
+
export declare function apply(ctx: Context, cfg: any): Promise<void>;
|
|
177
180
|
export {};
|
package/lib/index.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.inject = {
|
|
|
16
16
|
optional: ['assets'],
|
|
17
17
|
};
|
|
18
18
|
const logger = new koishi_1.Logger('ai-image');
|
|
19
|
+
// ==================== 配置 Schema ====================
|
|
19
20
|
exports.Config = koishi_1.Schema.object({
|
|
20
21
|
debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,输出完整请求日志'),
|
|
21
22
|
apiStrategy: koishi_1.Schema.union([
|
|
@@ -70,8 +71,10 @@ exports.Config = koishi_1.Schema.object({
|
|
|
70
71
|
blacklistListTitle: koishi_1.Schema.string().default('📋 当前黑名单:'),
|
|
71
72
|
}).description('提示文案配置'),
|
|
72
73
|
}).description('AI 绘图插件配置');
|
|
74
|
+
// ==================== 主函数 ====================
|
|
73
75
|
async function apply(ctx, cfg) {
|
|
74
76
|
const debug = cfg.debug;
|
|
77
|
+
// 加载本地化文件
|
|
75
78
|
try {
|
|
76
79
|
const loc = path_1.default.join(__dirname, 'locales', 'zh-CN.yml');
|
|
77
80
|
if (fs_1.default.existsSync(loc)) {
|
|
@@ -82,18 +85,21 @@ async function apply(ctx, cfg) {
|
|
|
82
85
|
const waitingMap = new Map();
|
|
83
86
|
const apiIdx = { val: 0 };
|
|
84
87
|
const apiCallTimestamps = [];
|
|
88
|
+
// 扩展数据库表
|
|
85
89
|
ctx.model.extend('ai_image_blacklist', {
|
|
86
90
|
id: 'string',
|
|
87
91
|
createdAt: 'date',
|
|
88
92
|
}, {
|
|
89
93
|
primary: 'id',
|
|
90
94
|
});
|
|
95
|
+
// 清理定时器
|
|
91
96
|
ctx.on('dispose', () => {
|
|
92
97
|
for (const [, task] of waitingMap) {
|
|
93
98
|
clearTimeout(task.timer);
|
|
94
99
|
}
|
|
95
100
|
waitingMap.clear();
|
|
96
101
|
});
|
|
102
|
+
// ==================== 工具函数 ====================
|
|
97
103
|
function checkRateLimit() {
|
|
98
104
|
const now = Date.now();
|
|
99
105
|
const oneHourAgo = now - 3600000;
|
|
@@ -106,7 +112,7 @@ async function apply(ctx, cfg) {
|
|
|
106
112
|
apiCallTimestamps.push(Date.now());
|
|
107
113
|
}
|
|
108
114
|
function getApi() {
|
|
109
|
-
const list = cfg.apiList.filter(v => v.enable && v.apiKey && v.baseUrl);
|
|
115
|
+
const list = cfg.apiList.filter((v) => v.enable && v.apiKey && v.baseUrl);
|
|
110
116
|
if (!list.length)
|
|
111
117
|
return null;
|
|
112
118
|
if (cfg.apiStrategy === 'sequence')
|
|
@@ -116,46 +122,61 @@ async function apply(ctx, cfg) {
|
|
|
116
122
|
return api;
|
|
117
123
|
}
|
|
118
124
|
function cleanHtmlTags(str) {
|
|
119
|
-
return str.replace(
|
|
125
|
+
return str.replace(/<<[^>]+>/g, '').trim();
|
|
120
126
|
}
|
|
121
|
-
//
|
|
127
|
+
// 增强图片提取函数
|
|
122
128
|
function getImageUrlFromContent(text) {
|
|
123
129
|
if (!text)
|
|
124
130
|
return null;
|
|
125
|
-
// 1. 匹配标准 http/https URL
|
|
126
131
|
const httpReg = /https?:\/\/[^<> \n\r()\[\]]+\.(png|jpg|jpeg|gif|webp)/i;
|
|
127
132
|
const httpMatch = text.match(httpReg);
|
|
128
133
|
if (httpMatch)
|
|
129
134
|
return httpMatch[0];
|
|
130
|
-
// 2. 匹配 base64 data URI(gpt-image-2 等模型返回的格式)
|
|
131
135
|
const base64Reg = /data:image\/(png|jpg|jpeg|gif|webp);base64,[A-Za-z0-9+/=]+/;
|
|
132
136
|
const base64Match = text.match(base64Reg);
|
|
133
137
|
if (base64Match)
|
|
134
138
|
return base64Match[0];
|
|
135
|
-
// 3. 匹配 markdown 图片语法  中的任意 URL
|
|
136
139
|
const markdownReg = /!\[.*?\]\((.*?)\)/;
|
|
137
140
|
const markdownMatch = text.match(markdownReg);
|
|
138
141
|
if (markdownMatch)
|
|
139
142
|
return markdownMatch[1];
|
|
140
143
|
return null;
|
|
141
144
|
}
|
|
142
|
-
// ====================
|
|
145
|
+
// ==================== 新增:URL 转 base64 ====================
|
|
146
|
+
async function urlToBase64(url) {
|
|
147
|
+
if (!url)
|
|
148
|
+
return null;
|
|
149
|
+
if (url.startsWith('data:image/')) {
|
|
150
|
+
return url;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const res = await axios_1.default.get(url, {
|
|
154
|
+
responseType: 'arraybuffer',
|
|
155
|
+
timeout: 30000,
|
|
156
|
+
});
|
|
157
|
+
const base64 = Buffer.from(res.data).toString('base64');
|
|
158
|
+
const mime = res.headers['content-type'] || 'image/jpeg';
|
|
159
|
+
return `data:${mime};base64,${base64}`;
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
logger.error('图片转 base64 失败', e);
|
|
163
|
+
throw new Error('图片下载失败,请检查 selfUrl 是否可访问');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 统一发送图片函数
|
|
143
167
|
async function sendImage(session, imgUrl) {
|
|
144
168
|
const trimmed = imgUrl.trim();
|
|
145
169
|
if (trimmed.startsWith('data:image/')) {
|
|
146
|
-
// base64 图片:使用 h 元素直接发送
|
|
147
170
|
if (debug)
|
|
148
171
|
logger.info('发送 base64 图片,长度:', trimmed.length);
|
|
149
172
|
await safeSend(session, (0, koishi_1.h)('img', { src: trimmed }));
|
|
150
173
|
}
|
|
151
174
|
else if (/^https?:\/\//.test(trimmed)) {
|
|
152
|
-
// http/https URL:使用 segment.image
|
|
153
175
|
if (debug)
|
|
154
176
|
logger.info('发送 URL 图片:', trimmed.slice(0, 100));
|
|
155
177
|
await safeSend(session, koishi_1.segment.image(trimmed));
|
|
156
178
|
}
|
|
157
179
|
else {
|
|
158
|
-
// 未知格式,当作文本发送并记录日志
|
|
159
180
|
logger.warn('未知的图片格式:', trimmed.slice(0, 100));
|
|
160
181
|
await safeSend(session, cfg.messages.fail + '(图片格式异常)');
|
|
161
182
|
}
|
|
@@ -286,6 +307,7 @@ async function apply(ctx, cfg) {
|
|
|
286
307
|
}
|
|
287
308
|
return { success, fail };
|
|
288
309
|
}
|
|
310
|
+
// ==================== 核心生成函数 ====================
|
|
289
311
|
async function generate(session, prompt, imageUrl, modelOverride) {
|
|
290
312
|
if (!checkRateLimit()) {
|
|
291
313
|
await safeSend(session, cfg.messages.rateLimit);
|
|
@@ -301,9 +323,14 @@ async function apply(ctx, cfg) {
|
|
|
301
323
|
const model = modelOverride || cfg.model;
|
|
302
324
|
let content;
|
|
303
325
|
if (imageUrl) {
|
|
326
|
+
const base64Url = await urlToBase64(imageUrl);
|
|
327
|
+
if (!base64Url) {
|
|
328
|
+
await safeSend(session, cfg.messages.fail + '(图片转换失败)');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
304
331
|
content = [
|
|
305
332
|
{ type: 'text', text: prompt },
|
|
306
|
-
{ type: 'image_url', image_url: { url:
|
|
333
|
+
{ type: 'image_url', image_url: { url: base64Url } },
|
|
307
334
|
];
|
|
308
335
|
}
|
|
309
336
|
else {
|
|
@@ -323,14 +350,12 @@ async function apply(ctx, cfg) {
|
|
|
323
350
|
});
|
|
324
351
|
if (debug)
|
|
325
352
|
logger.info('API返回:', JSON.stringify(res.data, null, 2));
|
|
326
|
-
// ==================== 修复:增强图片提取逻辑 ====================
|
|
327
353
|
let imgUrl = res.data?.data?.[0]?.url || null;
|
|
328
354
|
if (!imgUrl) {
|
|
329
355
|
const contentText = res.data?.choices?.[0]?.message?.content || '';
|
|
330
356
|
imgUrl = getImageUrlFromContent(contentText);
|
|
331
357
|
}
|
|
332
358
|
if (imgUrl) {
|
|
333
|
-
// 使用统一的发送函数处理 URL 和 base64
|
|
334
359
|
await sendImage(session, imgUrl);
|
|
335
360
|
}
|
|
336
361
|
else {
|
|
@@ -364,9 +389,14 @@ async function apply(ctx, cfg) {
|
|
|
364
389
|
}
|
|
365
390
|
const model = modelOverride || cfg.model;
|
|
366
391
|
const finalPrompt = prompt.replace('{url}', imageUrls.join(', '));
|
|
392
|
+
const base64Urls = (await Promise.all(imageUrls.map(url => urlToBase64(url)))).filter((url) => url !== null);
|
|
393
|
+
if (base64Urls.length === 0) {
|
|
394
|
+
await safeSend(session, cfg.messages.fail + '(图片转换失败)');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
367
397
|
const content = [
|
|
368
398
|
{ type: 'text', text: finalPrompt },
|
|
369
|
-
...
|
|
399
|
+
...base64Urls.map(url => ({ type: 'image_url', image_url: { url } })),
|
|
370
400
|
];
|
|
371
401
|
const body = {
|
|
372
402
|
model,
|
|
@@ -382,14 +412,12 @@ async function apply(ctx, cfg) {
|
|
|
382
412
|
});
|
|
383
413
|
if (debug)
|
|
384
414
|
logger.info('API返回:', JSON.stringify(res.data, null, 2));
|
|
385
|
-
// ==================== 修复:增强图片提取逻辑 ====================
|
|
386
415
|
let imgUrl = res.data?.data?.[0]?.url || null;
|
|
387
416
|
if (!imgUrl) {
|
|
388
417
|
const contentText = res.data?.choices?.[0]?.message?.content || '';
|
|
389
418
|
imgUrl = getImageUrlFromContent(contentText);
|
|
390
419
|
}
|
|
391
420
|
if (imgUrl) {
|
|
392
|
-
// 使用统一的发送函数处理 URL 和 base64
|
|
393
421
|
await sendImage(session, imgUrl);
|
|
394
422
|
}
|
|
395
423
|
else {
|
|
@@ -412,8 +440,9 @@ async function apply(ctx, cfg) {
|
|
|
412
440
|
deleteAllCachedFiles(imageUrls);
|
|
413
441
|
}
|
|
414
442
|
}
|
|
443
|
+
// ==================== 命令注册 ====================
|
|
415
444
|
const cmd = ctx.command(`${cfg.command} <raw:text>`, 'draw');
|
|
416
|
-
cfg.aliases.forEach(alias => cmd.alias(alias));
|
|
445
|
+
cfg.aliases.forEach((alias) => cmd.alias(alias));
|
|
417
446
|
cmd.action(async ({ session }, raw) => {
|
|
418
447
|
try {
|
|
419
448
|
if (!session)
|
|
@@ -436,7 +465,7 @@ async function apply(ctx, cfg) {
|
|
|
436
465
|
}
|
|
437
466
|
});
|
|
438
467
|
const imgCmd = ctx.command(`${cfg.img2imgCommand} <raw:text>`, 'imgdraw');
|
|
439
|
-
cfg.img2imgAliases.forEach(alias => imgCmd.alias(alias));
|
|
468
|
+
cfg.img2imgAliases.forEach((alias) => imgCmd.alias(alias));
|
|
440
469
|
imgCmd.action(async ({ session }, raw) => {
|
|
441
470
|
try {
|
|
442
471
|
if (!session)
|
|
@@ -476,6 +505,7 @@ async function apply(ctx, cfg) {
|
|
|
476
505
|
await safeSend(session, cfg.messages.fail);
|
|
477
506
|
}
|
|
478
507
|
});
|
|
508
|
+
// 消息监听
|
|
479
509
|
ctx.on('message', async (session) => {
|
|
480
510
|
try {
|
|
481
511
|
if (!session.elements)
|
|
@@ -493,7 +523,7 @@ async function apply(ctx, cfg) {
|
|
|
493
523
|
await safeSend(session, cfg.messages.needAssets);
|
|
494
524
|
return;
|
|
495
525
|
}
|
|
496
|
-
const uploadResults = await Promise.allSettled(imgs.map(img => assets.upload(img.attrs.src, 'ref_image.jpg')));
|
|
526
|
+
const uploadResults = await Promise.allSettled(imgs.map((img) => assets.upload(img.attrs.src, 'ref_image.jpg')));
|
|
497
527
|
const newUrls = [];
|
|
498
528
|
for (const res of uploadResults) {
|
|
499
529
|
if (res.status === 'fulfilled' && /^https?:\/\//.test(res.value)) {
|
|
@@ -544,6 +574,7 @@ async function apply(ctx, cfg) {
|
|
|
544
574
|
await safeSend(session, cfg.messages.fail);
|
|
545
575
|
}
|
|
546
576
|
});
|
|
577
|
+
// ==================== 黑名单命令 ====================
|
|
547
578
|
const blacklistCmd = ctx.command('blacklist', 'blacklist');
|
|
548
579
|
blacklistCmd.subcommand('.list', 'blacklist.list').action(async ({ session }) => {
|
|
549
580
|
if (!session)
|
|
@@ -556,7 +587,7 @@ async function apply(ctx, cfg) {
|
|
|
556
587
|
if (entries.length === 0) {
|
|
557
588
|
return safeSend(session, cfg.messages.blacklistListEmpty);
|
|
558
589
|
}
|
|
559
|
-
const list = entries.map(e => e.id).join('\n');
|
|
590
|
+
const list = entries.map((e) => e.id).join('\n');
|
|
560
591
|
return safeSend(session, `${cfg.messages.blacklistListTitle}\n${list}`);
|
|
561
592
|
}
|
|
562
593
|
catch (e) {
|