koishi-plugin-imgdraw-selfuse 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/lib/index.d.ts +176 -5
  2. package/lib/index.js +618 -36
  3. package/package.json +5 -1
package/lib/index.d.ts CHANGED
@@ -1,6 +1,177 @@
1
- import { Context, Schema } from 'koishi';
2
- export declare const name = "imgdraw-selfuse";
3
- export interface Config {
1
+ import { Schema } from 'koishi';
2
+ export declare const name = "ai-image";
3
+ export declare const inject: {
4
+ required: string[];
5
+ optional: string[];
6
+ };
7
+ type Infer<T> = T extends Schema<infer U> ? U : never;
8
+ export declare const Config: Schema<Schemastery.ObjectS<{
9
+ debug: Schema<boolean, boolean>;
10
+ apiStrategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
11
+ timeout: Schema<number, number>;
12
+ rateLimit: Schema<number, number>;
13
+ imgWaitTime: Schema<number, number>;
14
+ model: Schema<string, string>;
15
+ txt2imgModel: Schema<string, string>;
16
+ img2imgModel: Schema<string, string>;
17
+ maxImages: Schema<number, number>;
18
+ apiList: Schema<Schemastery.ObjectS<{
19
+ enable: Schema<boolean, boolean>;
20
+ apiKey: Schema<string, string>;
21
+ baseUrl: Schema<string, string>;
22
+ }>[], Schemastery.ObjectT<{
23
+ enable: Schema<boolean, boolean>;
24
+ apiKey: Schema<string, string>;
25
+ baseUrl: Schema<string, string>;
26
+ }>[]>;
27
+ enableTxt2Img: Schema<boolean, boolean>;
28
+ enableImg2Img: Schema<boolean, boolean>;
29
+ command: Schema<string, string>;
30
+ aliases: Schema<string[], string[]>;
31
+ img2imgCommand: Schema<string, string>;
32
+ img2imgAliases: Schema<string[], string[]>;
33
+ txt2imgPrompt: Schema<string, string>;
34
+ img2imgPrompt: Schema<string, string>;
35
+ blacklistAdmins: Schema<string[], string[]>;
36
+ messages: Schema<Schemastery.ObjectS<{
37
+ generating: Schema<string, string>;
38
+ waitImage: Schema<string, string>;
39
+ timeout: Schema<string, string>;
40
+ empty: Schema<string, string>;
41
+ noApi: Schema<string, string>;
42
+ fail: Schema<string, string>;
43
+ modelTextOnly: Schema<string, string>;
44
+ needAssets: Schema<string, string>;
45
+ txt2imgDisabled: Schema<string, string>;
46
+ img2imgDisabled: Schema<string, string>;
47
+ rateLimit: Schema<string, string>;
48
+ alreadyWaiting: Schema<string, string>;
49
+ multiImageReceived: Schema<string, string>;
50
+ multiImageLimit: Schema<string, string>;
51
+ noImageReceived: Schema<string, string>;
52
+ blacklisted: Schema<string, string>;
53
+ noPermission: Schema<string, string>;
54
+ blacklistAddSuccess: Schema<string, string>;
55
+ blacklistRemoveSuccess: Schema<string, string>;
56
+ blacklistAddFail: Schema<string, string>;
57
+ blacklistRemoveFail: Schema<string, string>;
58
+ invalidUserId: Schema<string, string>;
59
+ blacklistListEmpty: Schema<string, string>;
60
+ blacklistListTitle: Schema<string, string>;
61
+ }>, Schemastery.ObjectT<{
62
+ generating: Schema<string, string>;
63
+ waitImage: Schema<string, string>;
64
+ timeout: Schema<string, string>;
65
+ empty: Schema<string, string>;
66
+ noApi: Schema<string, string>;
67
+ fail: Schema<string, string>;
68
+ modelTextOnly: Schema<string, string>;
69
+ needAssets: Schema<string, string>;
70
+ txt2imgDisabled: Schema<string, string>;
71
+ img2imgDisabled: Schema<string, string>;
72
+ rateLimit: Schema<string, string>;
73
+ alreadyWaiting: Schema<string, string>;
74
+ multiImageReceived: Schema<string, string>;
75
+ multiImageLimit: Schema<string, string>;
76
+ noImageReceived: Schema<string, string>;
77
+ blacklisted: Schema<string, string>;
78
+ noPermission: Schema<string, string>;
79
+ blacklistAddSuccess: Schema<string, string>;
80
+ blacklistRemoveSuccess: Schema<string, string>;
81
+ blacklistAddFail: Schema<string, string>;
82
+ blacklistRemoveFail: Schema<string, string>;
83
+ invalidUserId: Schema<string, string>;
84
+ blacklistListEmpty: Schema<string, string>;
85
+ blacklistListTitle: Schema<string, string>;
86
+ }>>;
87
+ }>, Schemastery.ObjectT<{
88
+ debug: Schema<boolean, boolean>;
89
+ apiStrategy: Schema<"sequence" | "roundrobin", "sequence" | "roundrobin">;
90
+ timeout: Schema<number, number>;
91
+ rateLimit: Schema<number, number>;
92
+ imgWaitTime: Schema<number, number>;
93
+ model: Schema<string, string>;
94
+ txt2imgModel: Schema<string, string>;
95
+ img2imgModel: Schema<string, string>;
96
+ maxImages: Schema<number, number>;
97
+ apiList: Schema<Schemastery.ObjectS<{
98
+ enable: Schema<boolean, boolean>;
99
+ apiKey: Schema<string, string>;
100
+ baseUrl: Schema<string, string>;
101
+ }>[], Schemastery.ObjectT<{
102
+ enable: Schema<boolean, boolean>;
103
+ apiKey: Schema<string, string>;
104
+ baseUrl: Schema<string, string>;
105
+ }>[]>;
106
+ enableTxt2Img: Schema<boolean, boolean>;
107
+ enableImg2Img: Schema<boolean, boolean>;
108
+ command: Schema<string, string>;
109
+ aliases: Schema<string[], string[]>;
110
+ img2imgCommand: Schema<string, string>;
111
+ img2imgAliases: Schema<string[], string[]>;
112
+ txt2imgPrompt: Schema<string, string>;
113
+ img2imgPrompt: Schema<string, string>;
114
+ blacklistAdmins: Schema<string[], string[]>;
115
+ messages: Schema<Schemastery.ObjectS<{
116
+ generating: Schema<string, string>;
117
+ waitImage: Schema<string, string>;
118
+ timeout: Schema<string, string>;
119
+ empty: Schema<string, string>;
120
+ noApi: Schema<string, string>;
121
+ fail: Schema<string, string>;
122
+ modelTextOnly: Schema<string, string>;
123
+ needAssets: Schema<string, string>;
124
+ txt2imgDisabled: Schema<string, string>;
125
+ img2imgDisabled: Schema<string, string>;
126
+ rateLimit: Schema<string, string>;
127
+ alreadyWaiting: Schema<string, string>;
128
+ multiImageReceived: Schema<string, string>;
129
+ multiImageLimit: Schema<string, string>;
130
+ noImageReceived: Schema<string, string>;
131
+ blacklisted: Schema<string, string>;
132
+ noPermission: Schema<string, string>;
133
+ blacklistAddSuccess: Schema<string, string>;
134
+ blacklistRemoveSuccess: Schema<string, string>;
135
+ blacklistAddFail: Schema<string, string>;
136
+ blacklistRemoveFail: Schema<string, string>;
137
+ invalidUserId: Schema<string, string>;
138
+ blacklistListEmpty: Schema<string, string>;
139
+ blacklistListTitle: Schema<string, string>;
140
+ }>, Schemastery.ObjectT<{
141
+ generating: Schema<string, string>;
142
+ waitImage: Schema<string, string>;
143
+ timeout: Schema<string, string>;
144
+ empty: Schema<string, string>;
145
+ noApi: Schema<string, string>;
146
+ fail: Schema<string, string>;
147
+ modelTextOnly: Schema<string, string>;
148
+ needAssets: Schema<string, string>;
149
+ txt2imgDisabled: Schema<string, string>;
150
+ img2imgDisabled: Schema<string, string>;
151
+ rateLimit: Schema<string, string>;
152
+ alreadyWaiting: Schema<string, string>;
153
+ multiImageReceived: Schema<string, string>;
154
+ multiImageLimit: Schema<string, string>;
155
+ noImageReceived: Schema<string, string>;
156
+ blacklisted: Schema<string, string>;
157
+ noPermission: Schema<string, string>;
158
+ blacklistAddSuccess: Schema<string, string>;
159
+ blacklistRemoveSuccess: Schema<string, string>;
160
+ blacklistAddFail: Schema<string, string>;
161
+ blacklistRemoveFail: Schema<string, string>;
162
+ invalidUserId: Schema<string, string>;
163
+ blacklistListEmpty: Schema<string, string>;
164
+ blacklistListTitle: Schema<string, string>;
165
+ }>>;
166
+ }>>;
167
+ declare module 'koishi' {
168
+ interface Tables {
169
+ ai_image_blacklist: AIImageBlacklist;
170
+ }
4
171
  }
5
- export declare const Config: Schema<Config>;
6
- export declare function apply(ctx: Context): void;
172
+ interface AIImageBlacklist {
173
+ id: string;
174
+ createdAt: Date;
175
+ }
176
+ export declare function apply(ctx: any, cfg: Infer<typeof Config>): Promise<void>;
177
+ export {};
package/lib/index.js CHANGED
@@ -1,39 +1,621 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
- var __export = (target, all) => {
7
- for (var name2 in all)
8
- __defProp(target, name2, { get: all[name2], enumerable: true });
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
4
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Config = exports.inject = exports.name = void 0;
7
+ exports.apply = apply;
8
+ const koishi_1 = require("koishi");
9
+ const axios_1 = __importDefault(require("axios"));
10
+ const yaml_1 = __importDefault(require("yaml"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ exports.name = 'ai-image';
14
+ exports.inject = {
15
+ required: ['console', 'i18n', 'database'],
16
+ optional: ['assets'],
17
17
  };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var src_exports = {};
22
- __export(src_exports, {
23
- Config: () => Config,
24
- apply: () => apply,
25
- name: () => name
26
- });
27
- module.exports = __toCommonJS(src_exports);
28
- var import_koishi = require("koishi");
29
- var name = "imgdraw-selfuse";
30
- var Config = import_koishi.Schema.object({});
31
- function apply(ctx) {
18
+ const logger = new koishi_1.Logger('ai-image');
19
+ exports.Config = koishi_1.Schema.object({
20
+ debug: koishi_1.Schema.boolean().default(false).description('开启调试模式,输出完整请求日志'),
21
+ apiStrategy: koishi_1.Schema.union([
22
+ koishi_1.Schema.const('sequence').description('顺序模式'),
23
+ koishi_1.Schema.const('roundrobin').description('负载均衡模式'),
24
+ ]).default('roundrobin').description('API 调度策略'),
25
+ timeout: koishi_1.Schema.number().default(300000).description('接口请求超时时间(毫秒)'),
26
+ rateLimit: koishi_1.Schema.number().default(200).description('每小时调用次数限制'),
27
+ imgWaitTime: koishi_1.Schema.number().default(60).description('图生图等待图片超时时间(秒)'),
28
+ model: koishi_1.Schema.string().default('gpt-4o-mini').description('通用模型名称'),
29
+ txt2imgModel: koishi_1.Schema.string().default('').description('文生图专用模型,留空则使用通用模型'),
30
+ img2imgModel: koishi_1.Schema.string().default('').description('图生图专用模型,留空则使用通用模型'),
31
+ maxImages: koishi_1.Schema.number().default(5).description('图生图最大支持图片数量'),
32
+ apiList: koishi_1.Schema.array(koishi_1.Schema.object({
33
+ enable: koishi_1.Schema.boolean().default(true).description('启用此 API'),
34
+ apiKey: koishi_1.Schema.string().description('API Key'),
35
+ baseUrl: koishi_1.Schema.string().description('接口地址,需符合 OpenAI 标准'),
36
+ })).default([]).description('API 配置列表(支持多账号负载)'),
37
+ enableTxt2Img: koishi_1.Schema.boolean().default(true).description('启用文生图'),
38
+ enableImg2Img: koishi_1.Schema.boolean().default(true).description('启用图生图'),
39
+ command: koishi_1.Schema.string().default('draw').description('文生图指令'),
40
+ aliases: koishi_1.Schema.array(String).default([]).description('文生图指令别名'),
41
+ img2imgCommand: koishi_1.Schema.string().default('imgdraw').description('图生图指令'),
42
+ img2imgAliases: koishi_1.Schema.array(String).default([]).description('图生图指令别名'),
43
+ txt2imgPrompt: koishi_1.Schema.string().default('请严格遵循我的要求生成一张图片,不要询问或添加额外说明,直接输出图片。你可以使用联网功能获取最新的数据或信息。要求:{prompt}').description('文生图提示词模板'),
44
+ img2imgPrompt: koishi_1.Schema.string().default('图片链接:{url} 请严格根据以下指令对提供的图片进行编辑或重绘,不要询问,直接输出结果。你可以使用联网功能获取最新的数据或信息。\n指令:{prompt}').description('图生图提示词模板'),
45
+ blacklistAdmins: koishi_1.Schema.array(String).default([]).description('允许管理黑名单的 QQ 号列表'),
46
+ messages: koishi_1.Schema.object({
47
+ generating: koishi_1.Schema.string().default('⏳ 生成中...'),
48
+ waitImage: koishi_1.Schema.string().default('请在60秒内发送需要编辑的图片'),
49
+ timeout: koishi_1.Schema.string().default('等待图片超时,已取消'),
50
+ empty: koishi_1.Schema.string().default('❌ 请输入提示词'),
51
+ noApi: koishi_1.Schema.string().default('❌ 未配置可用API'),
52
+ fail: koishi_1.Schema.string().default('❌ 生成失败'),
53
+ modelTextOnly: koishi_1.Schema.string().default('❌ 模型未生成图片,返回文字:{text}'),
54
+ needAssets: koishi_1.Schema.string().default('❌ 图生图需要正确配置 assets 服务(selfUrl 未正确设置或服务未启动)'),
55
+ txt2imgDisabled: koishi_1.Schema.string().default('❌ 文生图功能未启用'),
56
+ img2imgDisabled: koishi_1.Schema.string().default('❌ 图生图功能未启用'),
57
+ rateLimit: koishi_1.Schema.string().default('❌ 调用次数已达上限,请稍后再试'),
58
+ alreadyWaiting: koishi_1.Schema.string().default('你已在等待发送图片,请直接发送图片或等待超时'),
59
+ multiImageReceived: koishi_1.Schema.string().default('已收到 {count} 张图片,可继续发送或输入"完成"开始生成'),
60
+ multiImageLimit: koishi_1.Schema.string().default('已达到最大图片数量,自动开始生成'),
61
+ noImageReceived: koishi_1.Schema.string().default('未发送任何图片'),
62
+ blacklisted: koishi_1.Schema.string().default('❌ 你已被加入黑名单,无法使用绘图功能'),
63
+ noPermission: koishi_1.Schema.string().default('❌ 你没有权限管理黑名单'),
64
+ blacklistAddSuccess: koishi_1.Schema.string().default('✅ 已将 {targets} 加入黑名单'),
65
+ blacklistRemoveSuccess: koishi_1.Schema.string().default('✅ 已将 {targets} 移出黑名单'),
66
+ blacklistAddFail: koishi_1.Schema.string().default('⚠️ {targets} 已在黑名单中或无效'),
67
+ blacklistRemoveFail: koishi_1.Schema.string().default('⚠️ {targets} 不在黑名单中'),
68
+ invalidUserId: koishi_1.Schema.string().default('⚠️ 无效的QQ号:{targets}'),
69
+ blacklistListEmpty: koishi_1.Schema.string().default('✅ 当前黑名单为空'),
70
+ blacklistListTitle: koishi_1.Schema.string().default('📋 当前黑名单:'),
71
+ }).description('提示文案配置'),
72
+ }).description('AI 绘图插件配置');
73
+ async function apply(ctx, cfg) {
74
+ const debug = cfg.debug;
75
+ try {
76
+ const loc = path_1.default.join(__dirname, 'locales', 'zh-CN.yml');
77
+ if (fs_1.default.existsSync(loc)) {
78
+ ctx.i18n.define('zh-CN', yaml_1.default.parse(fs_1.default.readFileSync(loc, 'utf8')));
79
+ }
80
+ }
81
+ catch { }
82
+ const waitingMap = new Map();
83
+ const apiIdx = { val: 0 };
84
+ const apiCallTimestamps = [];
85
+ ctx.model.extend('ai_image_blacklist', {
86
+ id: 'string',
87
+ createdAt: 'date',
88
+ }, {
89
+ primary: 'id',
90
+ });
91
+ ctx.on('dispose', () => {
92
+ for (const [, task] of waitingMap) {
93
+ clearTimeout(task.timer);
94
+ }
95
+ waitingMap.clear();
96
+ });
97
+ function checkRateLimit() {
98
+ const now = Date.now();
99
+ const oneHourAgo = now - 3600000;
100
+ while (apiCallTimestamps.length > 0 && apiCallTimestamps[0] < oneHourAgo) {
101
+ apiCallTimestamps.shift();
102
+ }
103
+ return apiCallTimestamps.length < cfg.rateLimit;
104
+ }
105
+ function recordApiCall() {
106
+ apiCallTimestamps.push(Date.now());
107
+ }
108
+ function getApi() {
109
+ const list = cfg.apiList.filter(v => v.enable && v.apiKey && v.baseUrl);
110
+ if (!list.length)
111
+ return null;
112
+ if (cfg.apiStrategy === 'sequence')
113
+ return list[0];
114
+ const api = list[apiIdx.val % list.length];
115
+ apiIdx.val++;
116
+ return api;
117
+ }
118
+ function cleanHtmlTags(str) {
119
+ return str.replace(/<[^>]+>/g, '').trim();
120
+ }
121
+ // ==================== 修复:增强图片提取函数 ====================
122
+ function getImageUrlFromContent(text) {
123
+ if (!text)
124
+ return null;
125
+ // 1. 匹配标准 http/https URL
126
+ const httpReg = /https?:\/\/[^<> \n\r()\[\]]+\.(png|jpg|jpeg|gif|webp)/i;
127
+ const httpMatch = text.match(httpReg);
128
+ if (httpMatch)
129
+ return httpMatch[0];
130
+ // 2. 匹配 base64 data URI(gpt-image-2 等模型返回的格式)
131
+ const base64Reg = /data:image\/(png|jpg|jpeg|gif|webp);base64,[A-Za-z0-9+/=]+/;
132
+ const base64Match = text.match(base64Reg);
133
+ if (base64Match)
134
+ return base64Match[0];
135
+ // 3. 匹配 markdown 图片语法 ![alt](url) 中的任意 URL
136
+ const markdownReg = /!\[.*?\]\((.*?)\)/;
137
+ const markdownMatch = text.match(markdownReg);
138
+ if (markdownMatch)
139
+ return markdownMatch[1];
140
+ return null;
141
+ }
142
+ // ==================== 新增:统一发送图片函数 ====================
143
+ async function sendImage(session, imgUrl) {
144
+ const trimmed = imgUrl.trim();
145
+ if (trimmed.startsWith('data:image/')) {
146
+ // base64 图片:使用 h 元素直接发送
147
+ if (debug)
148
+ logger.info('发送 base64 图片,长度:', trimmed.length);
149
+ await safeSend(session, (0, koishi_1.h)('img', { src: trimmed }));
150
+ }
151
+ else if (/^https?:\/\//.test(trimmed)) {
152
+ // http/https URL:使用 segment.image
153
+ if (debug)
154
+ logger.info('发送 URL 图片:', trimmed.slice(0, 100));
155
+ await safeSend(session, koishi_1.segment.image(trimmed));
156
+ }
157
+ else {
158
+ // 未知格式,当作文本发送并记录日志
159
+ logger.warn('未知的图片格式:', trimmed.slice(0, 100));
160
+ await safeSend(session, cfg.messages.fail + '(图片格式异常)');
161
+ }
162
+ }
163
+ async function safeSend(session, message) {
164
+ try {
165
+ await session.send(message);
166
+ }
167
+ catch (e) {
168
+ logger.error('发送消息失败', e);
169
+ }
170
+ }
171
+ function getErrorMessage(err) {
172
+ if (axios_1.default.isAxiosError(err)) {
173
+ if (err.code === 'ECONNABORTED')
174
+ return '请求超时';
175
+ if (err.code === 'ERR_NETWORK' || err.code?.startsWith('ERR_'))
176
+ return '网络连接失败';
177
+ if (err.response) {
178
+ const status = err.response.status;
179
+ if (status >= 500)
180
+ return `服务器错误 (${status})`;
181
+ if (status >= 400)
182
+ return `请求错误 (${status}),请检查 API Key 或参数`;
183
+ }
184
+ return err.message?.slice(0, 100) || '未知网络错误';
185
+ }
186
+ return '未知错误';
187
+ }
188
+ function extractFilenameFromAssetUrl(assetUrl) {
189
+ if (!assetUrl)
190
+ return null;
191
+ try {
192
+ if (assetUrl.startsWith('file://')) {
193
+ return path_1.default.basename(assetUrl.replace('file://', ''));
194
+ }
195
+ const urlObj = new URL(assetUrl);
196
+ const parts = urlObj.pathname.split('/');
197
+ const rawName = parts[parts.length - 1] || '';
198
+ return rawName ? path_1.default.basename(rawName) : null;
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ function deleteCachedFile(assetUrl) {
205
+ const filename = extractFilenameFromAssetUrl(assetUrl);
206
+ if (!filename)
207
+ return;
208
+ const defaultRoot = path_1.default.join(ctx.baseDir, 'data', 'assets');
209
+ const filePath = path_1.default.join(defaultRoot, filename);
210
+ try {
211
+ if (fs_1.default.existsSync(filePath)) {
212
+ fs_1.default.unlinkSync(filePath);
213
+ if (debug)
214
+ logger.info('已删除缓存文件:', filePath);
215
+ }
216
+ }
217
+ catch (e) {
218
+ logger.error('删除缓存文件失败', e);
219
+ }
220
+ }
221
+ function deleteAllCachedFiles(urls) {
222
+ for (const url of urls) {
223
+ deleteCachedFile(url);
224
+ }
225
+ }
226
+ function isValidQQ(id) {
227
+ return /^\d{5,11}$/.test(id);
228
+ }
229
+ async function isBlacklisted(userId) {
230
+ try {
231
+ const rows = await ctx.database.get('ai_image_blacklist', { id: userId });
232
+ return rows.length > 0;
233
+ }
234
+ catch (e) {
235
+ logger.error('查询黑名单失败', e);
236
+ return false;
237
+ }
238
+ }
239
+ async function addToBlacklist(ids) {
240
+ const success = [];
241
+ const fail = [];
242
+ for (const id of ids) {
243
+ if (!isValidQQ(id)) {
244
+ fail.push(id);
245
+ continue;
246
+ }
247
+ try {
248
+ const exists = await ctx.database.get('ai_image_blacklist', { id });
249
+ if (exists.length === 0) {
250
+ await ctx.database.create('ai_image_blacklist', { id, createdAt: new Date() });
251
+ success.push(id);
252
+ }
253
+ else {
254
+ fail.push(id);
255
+ }
256
+ }
257
+ catch (e) {
258
+ logger.error('添加黑名单失败', e);
259
+ fail.push(id);
260
+ }
261
+ }
262
+ return { success, fail };
263
+ }
264
+ async function removeFromBlacklist(ids) {
265
+ const success = [];
266
+ const fail = [];
267
+ for (const id of ids) {
268
+ if (!isValidQQ(id)) {
269
+ fail.push(id);
270
+ continue;
271
+ }
272
+ try {
273
+ const exists = await ctx.database.get('ai_image_blacklist', { id });
274
+ if (exists.length > 0) {
275
+ await ctx.database.remove('ai_image_blacklist', { id });
276
+ success.push(id);
277
+ }
278
+ else {
279
+ fail.push(id);
280
+ }
281
+ }
282
+ catch (e) {
283
+ logger.error('移除黑名单失败', e);
284
+ fail.push(id);
285
+ }
286
+ }
287
+ return { success, fail };
288
+ }
289
+ async function generate(session, prompt, imageUrl, modelOverride) {
290
+ if (!checkRateLimit()) {
291
+ await safeSend(session, cfg.messages.rateLimit);
292
+ return;
293
+ }
294
+ const api = getApi();
295
+ if (!api) {
296
+ if (debug)
297
+ logger.info('无可用API');
298
+ await safeSend(session, cfg.messages.noApi);
299
+ return;
300
+ }
301
+ const model = modelOverride || cfg.model;
302
+ let content;
303
+ if (imageUrl) {
304
+ content = [
305
+ { type: 'text', text: prompt },
306
+ { type: 'image_url', image_url: { url: imageUrl } },
307
+ ];
308
+ }
309
+ else {
310
+ content = prompt;
311
+ }
312
+ const body = {
313
+ model,
314
+ messages: [{ role: 'user', content }],
315
+ };
316
+ if (debug)
317
+ logger.info('请求体:', JSON.stringify(body, null, 2));
318
+ try {
319
+ recordApiCall();
320
+ const res = await axios_1.default.post(api.baseUrl, body, {
321
+ headers: { Authorization: `Bearer ${api.apiKey}` },
322
+ timeout: cfg.timeout,
323
+ });
324
+ if (debug)
325
+ logger.info('API返回:', JSON.stringify(res.data, null, 2));
326
+ // ==================== 修复:增强图片提取逻辑 ====================
327
+ let imgUrl = res.data?.data?.[0]?.url || null;
328
+ if (!imgUrl) {
329
+ const contentText = res.data?.choices?.[0]?.message?.content || '';
330
+ imgUrl = getImageUrlFromContent(contentText);
331
+ }
332
+ if (imgUrl) {
333
+ // 使用统一的发送函数处理 URL 和 base64
334
+ await sendImage(session, imgUrl);
335
+ }
336
+ else {
337
+ const textContent = res.data?.choices?.[0]?.message?.content;
338
+ if (textContent && typeof textContent === 'string' && textContent.trim().length > 0) {
339
+ const msg = cfg.messages.modelTextOnly.replace('{text}', textContent.trim().slice(0, 500));
340
+ await safeSend(session, msg);
341
+ }
342
+ else {
343
+ await safeSend(session, cfg.messages.fail + '(未返回任何内容)');
344
+ }
345
+ }
346
+ }
347
+ catch (err) {
348
+ const reason = getErrorMessage(err);
349
+ logger.error(`API请求失败 [${reason}]`, err);
350
+ await safeSend(session, `${cfg.messages.fail} [${reason}]`);
351
+ }
352
+ }
353
+ async function generateWithMultipleImages(session, prompt, imageUrls, modelOverride) {
354
+ if (!checkRateLimit()) {
355
+ await safeSend(session, cfg.messages.rateLimit);
356
+ return;
357
+ }
358
+ const api = getApi();
359
+ if (!api) {
360
+ if (debug)
361
+ logger.info('无可用API');
362
+ await safeSend(session, cfg.messages.noApi);
363
+ return;
364
+ }
365
+ const model = modelOverride || cfg.model;
366
+ const finalPrompt = prompt.replace('{url}', imageUrls.join(', '));
367
+ const content = [
368
+ { type: 'text', text: finalPrompt },
369
+ ...imageUrls.map(url => ({ type: 'image_url', image_url: { url } })),
370
+ ];
371
+ const body = {
372
+ model,
373
+ messages: [{ role: 'user', content }],
374
+ };
375
+ if (debug)
376
+ logger.info('多图请求体:', JSON.stringify(body, null, 2));
377
+ try {
378
+ recordApiCall();
379
+ const res = await axios_1.default.post(api.baseUrl, body, {
380
+ headers: { Authorization: `Bearer ${api.apiKey}` },
381
+ timeout: cfg.timeout,
382
+ });
383
+ if (debug)
384
+ logger.info('API返回:', JSON.stringify(res.data, null, 2));
385
+ // ==================== 修复:增强图片提取逻辑 ====================
386
+ let imgUrl = res.data?.data?.[0]?.url || null;
387
+ if (!imgUrl) {
388
+ const contentText = res.data?.choices?.[0]?.message?.content || '';
389
+ imgUrl = getImageUrlFromContent(contentText);
390
+ }
391
+ if (imgUrl) {
392
+ // 使用统一的发送函数处理 URL 和 base64
393
+ await sendImage(session, imgUrl);
394
+ }
395
+ else {
396
+ const textContent = res.data?.choices?.[0]?.message?.content;
397
+ if (textContent && typeof textContent === 'string' && textContent.trim().length > 0) {
398
+ const msg = cfg.messages.modelTextOnly.replace('{text}', textContent.trim().slice(0, 500));
399
+ await safeSend(session, msg);
400
+ }
401
+ else {
402
+ await safeSend(session, cfg.messages.fail + '(未返回任何内容)');
403
+ }
404
+ }
405
+ }
406
+ catch (err) {
407
+ const reason = getErrorMessage(err);
408
+ logger.error(`API请求失败 [${reason}]`, err);
409
+ await safeSend(session, `${cfg.messages.fail} [${reason}]`);
410
+ }
411
+ finally {
412
+ deleteAllCachedFiles(imageUrls);
413
+ }
414
+ }
415
+ const cmd = ctx.command(`${cfg.command} <raw:text>`, 'draw');
416
+ cfg.aliases.forEach(alias => cmd.alias(alias));
417
+ cmd.action(async ({ session }, raw) => {
418
+ try {
419
+ if (!session)
420
+ return;
421
+ if (await isBlacklisted(session.userId))
422
+ return safeSend(session, cfg.messages.blacklisted);
423
+ if (!cfg.enableTxt2Img)
424
+ return safeSend(session, cfg.messages.txt2imgDisabled);
425
+ const prompt = cleanHtmlTags(raw || '');
426
+ if (!prompt)
427
+ return safeSend(session, cfg.messages.empty);
428
+ await safeSend(session, cfg.messages.generating);
429
+ const finalPrompt = cfg.txt2imgPrompt.replace('{prompt}', prompt);
430
+ const model = cfg.txt2imgModel || cfg.model;
431
+ await generate(session, finalPrompt, undefined, model);
432
+ }
433
+ catch (e) {
434
+ logger.error('文生图命令异常', e);
435
+ await safeSend(session, cfg.messages.fail);
436
+ }
437
+ });
438
+ const imgCmd = ctx.command(`${cfg.img2imgCommand} <raw:text>`, 'imgdraw');
439
+ cfg.img2imgAliases.forEach(alias => imgCmd.alias(alias));
440
+ imgCmd.action(async ({ session }, raw) => {
441
+ try {
442
+ if (!session)
443
+ return;
444
+ if (await isBlacklisted(session.userId))
445
+ return safeSend(session, cfg.messages.blacklisted);
446
+ if (!cfg.enableImg2Img)
447
+ return safeSend(session, cfg.messages.img2imgDisabled);
448
+ const assets = ctx.assets;
449
+ if (!assets)
450
+ return safeSend(session, cfg.messages.needAssets);
451
+ const prompt = cleanHtmlTags(raw || '');
452
+ if (!prompt)
453
+ return safeSend(session, cfg.messages.empty);
454
+ const key = `${session.guildId || 'private'}-${session.userId}`;
455
+ if (waitingMap.has(key)) {
456
+ return safeSend(session, cfg.messages.alreadyWaiting);
457
+ }
458
+ await safeSend(session, cfg.messages.waitImage.replace('60', String(cfg.imgWaitTime)));
459
+ const timer = setTimeout(() => {
460
+ const task = waitingMap.get(key);
461
+ if (!task)
462
+ return;
463
+ waitingMap.delete(key);
464
+ if (task.imageUrls.length > 0) {
465
+ safeSend(session, cfg.messages.generating).catch(() => { });
466
+ generateWithMultipleImages(session, task.prompt, task.imageUrls, cfg.img2imgModel || cfg.model);
467
+ }
468
+ else {
469
+ safeSend(session, cfg.messages.timeout).catch(() => { });
470
+ }
471
+ }, cfg.imgWaitTime * 1000);
472
+ waitingMap.set(key, { prompt, timer, imageUrls: [] });
473
+ }
474
+ catch (e) {
475
+ logger.error('图生图命令异常', e);
476
+ await safeSend(session, cfg.messages.fail);
477
+ }
478
+ });
479
+ ctx.on('message', async (session) => {
480
+ try {
481
+ if (!session.elements)
482
+ return;
483
+ if (session.bot?.selfId && session.userId === session.bot.selfId)
484
+ return;
485
+ const key = `${session.guildId || 'private'}-${session.userId}`;
486
+ const task = waitingMap.get(key);
487
+ if (!task)
488
+ return;
489
+ const imgs = koishi_1.h.select(session.elements, 'img');
490
+ if (imgs.length > 0) {
491
+ const assets = ctx.assets;
492
+ if (!assets) {
493
+ await safeSend(session, cfg.messages.needAssets);
494
+ return;
495
+ }
496
+ const uploadResults = await Promise.allSettled(imgs.map(img => assets.upload(img.attrs.src, 'ref_image.jpg')));
497
+ const newUrls = [];
498
+ for (const res of uploadResults) {
499
+ if (res.status === 'fulfilled' && /^https?:\/\//.test(res.value)) {
500
+ newUrls.push(res.value);
501
+ }
502
+ }
503
+ if (newUrls.length === 0) {
504
+ await safeSend(session, cfg.messages.needAssets);
505
+ return;
506
+ }
507
+ task.imageUrls.push(...newUrls);
508
+ if (task.imageUrls.length >= cfg.maxImages) {
509
+ clearTimeout(task.timer);
510
+ waitingMap.delete(key);
511
+ await safeSend(session, cfg.messages.generating);
512
+ await generateWithMultipleImages(session, task.prompt, task.imageUrls, cfg.img2imgModel || cfg.model);
513
+ return;
514
+ }
515
+ clearTimeout(task.timer);
516
+ task.timer = setTimeout(() => {
517
+ waitingMap.delete(key);
518
+ if (task.imageUrls.length > 0) {
519
+ safeSend(session, cfg.messages.generating).catch(() => { });
520
+ generateWithMultipleImages(session, task.prompt, task.imageUrls, cfg.img2imgModel || cfg.model);
521
+ }
522
+ else {
523
+ safeSend(session, cfg.messages.timeout).catch(() => { });
524
+ }
525
+ }, cfg.imgWaitTime * 1000);
526
+ await safeSend(session, cfg.messages.multiImageReceived.replace('{count}', String(task.imageUrls.length)));
527
+ return;
528
+ }
529
+ const text = session.content?.trim();
530
+ if (text === '完成' || text === 'done' || text === '生成') {
531
+ clearTimeout(task.timer);
532
+ waitingMap.delete(key);
533
+ if (task.imageUrls.length > 0) {
534
+ await safeSend(session, cfg.messages.generating);
535
+ await generateWithMultipleImages(session, task.prompt, task.imageUrls, cfg.img2imgModel || cfg.model);
536
+ }
537
+ else {
538
+ await safeSend(session, cfg.messages.noImageReceived);
539
+ }
540
+ }
541
+ }
542
+ catch (e) {
543
+ logger.error('消息监听异常', e);
544
+ await safeSend(session, cfg.messages.fail);
545
+ }
546
+ });
547
+ const blacklistCmd = ctx.command('blacklist', 'blacklist');
548
+ blacklistCmd.subcommand('.list', 'blacklist.list').action(async ({ session }) => {
549
+ if (!session)
550
+ return;
551
+ if (!cfg.blacklistAdmins.includes(session.userId)) {
552
+ return safeSend(session, cfg.messages.noPermission);
553
+ }
554
+ try {
555
+ const entries = await ctx.database.get('ai_image_blacklist', {});
556
+ if (entries.length === 0) {
557
+ return safeSend(session, cfg.messages.blacklistListEmpty);
558
+ }
559
+ const list = entries.map(e => e.id).join('\n');
560
+ return safeSend(session, `${cfg.messages.blacklistListTitle}\n${list}`);
561
+ }
562
+ catch (e) {
563
+ logger.error('获取黑名单失败', e);
564
+ return safeSend(session, cfg.messages.fail);
565
+ }
566
+ });
567
+ blacklistCmd.subcommand('.add <...targets:string>', 'blacklist.add').action(async ({ session }, ...targets) => {
568
+ if (!session)
569
+ return;
570
+ if (!cfg.blacklistAdmins.includes(session.userId)) {
571
+ return safeSend(session, cfg.messages.noPermission);
572
+ }
573
+ const ids = [];
574
+ targets.forEach(t => {
575
+ const num = t.replace(/\D/g, '');
576
+ if (num)
577
+ ids.push(num);
578
+ });
579
+ if (ids.length === 0) {
580
+ return safeSend(session, '请提供有效的QQ号');
581
+ }
582
+ const invalid = ids.filter(id => !isValidQQ(id));
583
+ if (invalid.length > 0) {
584
+ return safeSend(session, cfg.messages.invalidUserId.replace('{targets}', invalid.join(', ')));
585
+ }
586
+ const { success, fail } = await addToBlacklist(ids);
587
+ if (success.length) {
588
+ await safeSend(session, cfg.messages.blacklistAddSuccess.replace('{targets}', success.join(', ')));
589
+ }
590
+ if (fail.length) {
591
+ await safeSend(session, cfg.messages.blacklistAddFail.replace('{targets}', fail.join(', ')));
592
+ }
593
+ });
594
+ blacklistCmd.subcommand('.remove <...targets:string>', 'blacklist.remove').action(async ({ session }, ...targets) => {
595
+ if (!session)
596
+ return;
597
+ if (!cfg.blacklistAdmins.includes(session.userId)) {
598
+ return safeSend(session, cfg.messages.noPermission);
599
+ }
600
+ const ids = [];
601
+ targets.forEach(t => {
602
+ const num = t.replace(/\D/g, '');
603
+ if (num)
604
+ ids.push(num);
605
+ });
606
+ if (ids.length === 0) {
607
+ return safeSend(session, '请提供有效的QQ号');
608
+ }
609
+ const invalid = ids.filter(id => !isValidQQ(id));
610
+ if (invalid.length > 0) {
611
+ return safeSend(session, cfg.messages.invalidUserId.replace('{targets}', invalid.join(', ')));
612
+ }
613
+ const { success, fail } = await removeFromBlacklist(ids);
614
+ if (success.length) {
615
+ await safeSend(session, cfg.messages.blacklistRemoveSuccess.replace('{targets}', success.join(', ')));
616
+ }
617
+ if (fail.length) {
618
+ await safeSend(session, cfg.messages.blacklistRemoveFail.replace('{targets}', fail.join(', ')));
619
+ }
620
+ });
32
621
  }
33
- __name(apply, "apply");
34
- // Annotate the CommonJS export names for ESM import in node:
35
- 0 && (module.exports = {
36
- Config,
37
- apply,
38
- name
39
- });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-imgdraw-selfuse",
3
3
  "description": "画图",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -16,5 +16,9 @@
16
16
  ],
17
17
  "peerDependencies": {
18
18
  "koishi": "^4.18.7"
19
+ },
20
+ "dependencies": {
21
+ "axios": "^1.16.1",
22
+ "yaml": "^2.9.0"
19
23
  }
20
24
  }