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 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
- type Infer<T> = T extends Schema<infer U> ? U : never;
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
- declare module 'koishi' {
168
- interface Tables {
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(/<[^>]+>/g, '').trim();
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 图片语法 ![alt](url) 中的任意 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: imageUrl } },
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
- ...imageUrls.map(url => ({ type: 'image_url', image_url: { url } })),
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-imgdraw-selfuse",
3
3
  "description": "画图",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [