koishi-plugin-emojiluna-plus 0.0.1

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 (49) hide show
  1. package/LICENSE +24 -0
  2. package/dist/index.js +10 -0
  3. package/dist/style.css +1 -0
  4. package/lib/adapters/autoCommands.d.ts +4 -0
  5. package/lib/adapters/autoCommands.js +111 -0
  6. package/lib/adapters/backend.d.ts +3 -0
  7. package/lib/adapters/backend.js +11 -0
  8. package/lib/adapters/chatlunaIntegration.d.ts +3 -0
  9. package/lib/adapters/chatlunaIntegration.js +48 -0
  10. package/lib/adapters/commands.d.ts +4 -0
  11. package/lib/adapters/commands.js +330 -0
  12. package/lib/adapters/consoleListeners.d.ts +3 -0
  13. package/lib/adapters/consoleListeners.js +168 -0
  14. package/lib/adapters/index.d.ts +2 -0
  15. package/lib/adapters/index.js +7 -0
  16. package/lib/adapters/render.d.ts +7 -0
  17. package/lib/adapters/render.js +35 -0
  18. package/lib/adapters/restApi.d.ts +3 -0
  19. package/lib/adapters/restApi.js +225 -0
  20. package/lib/collectors/autoCollector.d.ts +75 -0
  21. package/lib/collectors/autoCollector.js +728 -0
  22. package/lib/collectors/index.d.ts +1 -0
  23. package/lib/collectors/index.js +17 -0
  24. package/lib/config.d.ts +194 -0
  25. package/lib/config.js +369 -0
  26. package/lib/debug.d.ts +13 -0
  27. package/lib/debug.js +44 -0
  28. package/lib/image/imageProcessor.d.ts +20 -0
  29. package/lib/image/imageProcessor.js +243 -0
  30. package/lib/image/index.d.ts +1 -0
  31. package/lib/image/index.js +17 -0
  32. package/lib/index.d.ts +57 -0
  33. package/lib/index.js +40 -0
  34. package/lib/services/aiAnalyzer.d.ts +24 -0
  35. package/lib/services/aiAnalyzer.js +162 -0
  36. package/lib/services/emojiluna.d.ts +102 -0
  37. package/lib/services/emojiluna.js +910 -0
  38. package/lib/services/index.d.ts +2 -0
  39. package/lib/services/index.js +18 -0
  40. package/lib/storage/index.d.ts +1 -0
  41. package/lib/storage/index.js +17 -0
  42. package/lib/storage/uploadManager.d.ts +102 -0
  43. package/lib/storage/uploadManager.js +424 -0
  44. package/lib/types.d.ts +159 -0
  45. package/lib/types.js +69 -0
  46. package/lib/utils.d.ts +29 -0
  47. package/lib/utils.js +256 -0
  48. package/package.json +99 -0
  49. package/readme.md +174 -0
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectImageFormat = detectImageFormat;
4
+ exports.getImageMetadata = getImageMetadata;
5
+ exports.sampleFrameIndices = sampleFrameIndices;
6
+ exports.extractSampledFrames = extractSampledFrames;
7
+ exports.extractFrameRgba = extractFrameRgba;
8
+ exports.resizeFrameToGrayscale = resizeFrameToGrayscale;
9
+ const core_1 = require("@jimp/core");
10
+ const jimp_1 = require("jimp");
11
+ const gifuct_js_1 = require("gifuct-js");
12
+ const Jimp = (0, core_1.createJimp)({
13
+ formats: [...jimp_1.defaultFormats],
14
+ plugins: jimp_1.defaultPlugins
15
+ });
16
+ const clampFrameIndex = (index, frameCount) => {
17
+ if (frameCount <= 1)
18
+ return 0;
19
+ return Math.max(0, Math.min(frameCount - 1, index));
20
+ };
21
+ const toArrayBuffer = (buffer) => {
22
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
23
+ };
24
+ function detectImageFormat(buffer) {
25
+ const header = buffer.subarray(0, 12);
26
+ if (header[0] === 0xff && header[1] === 0xd8)
27
+ return 'jpeg';
28
+ if (header[0] === 0x89 &&
29
+ header[1] === 0x50 &&
30
+ header[2] === 0x4e &&
31
+ header[3] === 0x47)
32
+ return 'png';
33
+ if (header[0] === 0x47 && header[1] === 0x49 && header[2] === 0x46)
34
+ return 'gif';
35
+ if (header[0] === 0x52 &&
36
+ header[1] === 0x49 &&
37
+ header[2] === 0x46 &&
38
+ header[8] === 0x57 &&
39
+ header[9] === 0x45 &&
40
+ header[10] === 0x42 &&
41
+ header[11] === 0x50)
42
+ return 'webp';
43
+ if (header[0] === 0x42 && header[1] === 0x4d)
44
+ return 'bmp';
45
+ return 'unknown';
46
+ }
47
+ async function getImageMetadata(buffer) {
48
+ const format = detectImageFormat(buffer);
49
+ if (format === 'gif') {
50
+ const gif = (0, gifuct_js_1.parseGIF)(toArrayBuffer(buffer));
51
+ const frames = (0, gifuct_js_1.decompressFrames)(gif, false);
52
+ return {
53
+ format,
54
+ width: gif.lsd?.width ?? 0,
55
+ height: gif.lsd?.height ?? 0,
56
+ frameCount: frames.length || 1
57
+ };
58
+ }
59
+ if (format === 'webp') {
60
+ const frame = await decodeWebpFrame(buffer);
61
+ return {
62
+ format,
63
+ width: frame.width,
64
+ height: frame.height,
65
+ frameCount: 1
66
+ };
67
+ }
68
+ const image = await Jimp.read(buffer);
69
+ return {
70
+ format,
71
+ width: image.bitmap.width,
72
+ height: image.bitmap.height,
73
+ frameCount: 1
74
+ };
75
+ }
76
+ function sampleFrameIndices(frameCount, sampleCount) {
77
+ const safeFrameCount = Math.max(1, frameCount);
78
+ const safeSampleCount = Math.max(1, Math.min(sampleCount, safeFrameCount));
79
+ if (safeFrameCount === 1)
80
+ return [0];
81
+ if (safeSampleCount === 1)
82
+ return [Math.floor(safeFrameCount / 2)];
83
+ const indices = new Set();
84
+ for (let i = 0; i < safeSampleCount; i++) {
85
+ const ratio = i / (safeSampleCount - 1);
86
+ const index = Math.round((safeFrameCount - 1) * ratio);
87
+ indices.add(index);
88
+ }
89
+ return Array.from(indices).sort((a, b) => a - b);
90
+ }
91
+ async function extractSampledFrames(buffer, sampleCount, outputFormat = 'png') {
92
+ const sourceMetadata = await getImageMetadata(buffer);
93
+ const indices = sampleFrameIndices(sourceMetadata.frameCount, sampleCount);
94
+ const decodedFrames = await decodeFrames(buffer, sourceMetadata, indices);
95
+ const normalizedOutputFormat = sourceMetadata.format === 'gif' ? 'png' : outputFormat;
96
+ const frames = await Promise.all(decodedFrames.map((frame) => encodeFrame(frame, normalizedOutputFormat)));
97
+ const firstFrame = decodedFrames[0];
98
+ const metadata = firstFrame
99
+ ? {
100
+ format: normalizedOutputFormat,
101
+ width: firstFrame.width,
102
+ height: firstFrame.height,
103
+ frameCount: 1
104
+ }
105
+ : {
106
+ format: normalizedOutputFormat,
107
+ width: sourceMetadata.width,
108
+ height: sourceMetadata.height,
109
+ frameCount: 1
110
+ };
111
+ return { frames, metadata };
112
+ }
113
+ async function extractFrameRgba(buffer, metadata, frameIndex) {
114
+ const index = clampFrameIndex(frameIndex, metadata.frameCount);
115
+ const [frame] = await decodeFrames(buffer, metadata, [index]);
116
+ if (!frame) {
117
+ throw new Error('Unable to decode frame');
118
+ }
119
+ return frame;
120
+ }
121
+ async function resizeFrameToGrayscale(frame, targetWidth, targetHeight) {
122
+ return resizeToGrayscale(frame.data, frame.width, frame.height, targetWidth, targetHeight);
123
+ }
124
+ async function decodeFrames(buffer, metadata, frameIndices) {
125
+ if (metadata.frameCount <= 1 || frameIndices.length === 0) {
126
+ const frame = await decodeStaticFrame(buffer);
127
+ return [frame];
128
+ }
129
+ if (metadata.format === 'gif') {
130
+ return decodeGifFrames(buffer, frameIndices);
131
+ }
132
+ if (metadata.format === 'webp') {
133
+ const frame = await decodeWebpFrame(buffer);
134
+ return [frame];
135
+ }
136
+ const frame = await decodeStaticFrame(buffer);
137
+ return [frame];
138
+ }
139
+ async function decodeStaticFrame(buffer) {
140
+ const image = await Jimp.read(buffer);
141
+ return {
142
+ width: image.bitmap.width,
143
+ height: image.bitmap.height,
144
+ data: new Uint8Array(image.bitmap.data)
145
+ };
146
+ }
147
+ function decodeGifFrames(buffer, frameIndices) {
148
+ const gif = (0, gifuct_js_1.parseGIF)(toArrayBuffer(buffer));
149
+ const frames = (0, gifuct_js_1.decompressFrames)(gif, true);
150
+ const width = gif.lsd?.width ?? frames[0]?.dims?.width ?? 0;
151
+ const height = gif.lsd?.height ?? frames[0]?.dims?.height ?? 0;
152
+ const target = new Set(frameIndices);
153
+ const output = [];
154
+ let canvas = new Uint8Array(width * height * 4);
155
+ let restoreCanvas = null;
156
+ let previousDisposal = 0;
157
+ let previousDims = null;
158
+ frames.forEach((frame, index) => {
159
+ if (index > 0) {
160
+ if (previousDisposal === 2 && previousDims) {
161
+ clearRect(canvas, width, previousDims);
162
+ }
163
+ else if (previousDisposal === 3 && restoreCanvas) {
164
+ canvas = new Uint8Array(restoreCanvas);
165
+ }
166
+ }
167
+ restoreCanvas = frame.disposalType === 3 ? new Uint8Array(canvas) : null;
168
+ drawPatch(canvas, width, new Uint8Array(frame.patch), frame.dims);
169
+ if (target.has(index)) {
170
+ output.push({
171
+ width,
172
+ height,
173
+ data: new Uint8Array(canvas)
174
+ });
175
+ }
176
+ previousDisposal = frame.disposalType ?? 0;
177
+ previousDims = frame.dims ?? null;
178
+ });
179
+ return output;
180
+ }
181
+ function clearRect(canvas, width, dims) {
182
+ const { left, top, width: rectWidth, height: rectHeight } = dims;
183
+ for (let y = 0; y < rectHeight; y++) {
184
+ const rowStart = (top + y) * width + left;
185
+ const rowEnd = rowStart + rectWidth;
186
+ for (let x = rowStart; x < rowEnd; x++) {
187
+ const offset = x * 4;
188
+ canvas[offset] = 0;
189
+ canvas[offset + 1] = 0;
190
+ canvas[offset + 2] = 0;
191
+ canvas[offset + 3] = 0;
192
+ }
193
+ }
194
+ }
195
+ function drawPatch(canvas, width, patch, dims) {
196
+ const { left, top, width: patchWidth, height: patchHeight } = dims;
197
+ for (let y = 0; y < patchHeight; y++) {
198
+ for (let x = 0; x < patchWidth; x++) {
199
+ const patchOffset = (y * patchWidth + x) * 4;
200
+ const alpha = patch[patchOffset + 3];
201
+ if (alpha === 0)
202
+ continue;
203
+ const destOffset = ((top + y) * width + (left + x)) * 4;
204
+ canvas[destOffset] = patch[patchOffset];
205
+ canvas[destOffset + 1] = patch[patchOffset + 1];
206
+ canvas[destOffset + 2] = patch[patchOffset + 2];
207
+ canvas[destOffset + 3] = alpha;
208
+ }
209
+ }
210
+ }
211
+ async function decodeWebpFrame(buffer) {
212
+ const image = await Jimp.read(buffer);
213
+ return {
214
+ width: image.bitmap.width,
215
+ height: image.bitmap.height,
216
+ data: new Uint8Array(image.bitmap.data)
217
+ };
218
+ }
219
+ async function encodeFrame(frame, format) {
220
+ const image = new Jimp({
221
+ data: Buffer.from(frame.data),
222
+ width: frame.width,
223
+ height: frame.height
224
+ });
225
+ if (format === 'jpeg') {
226
+ return await image.getBuffer('image/jpeg', { quality: 70 });
227
+ }
228
+ return await image.getBuffer('image/png');
229
+ }
230
+ async function resizeToGrayscale(pixels, width, height, targetWidth, targetHeight) {
231
+ const base = new Jimp({
232
+ data: Buffer.from(pixels),
233
+ width,
234
+ height
235
+ });
236
+ const image = base.resize({ w: targetWidth, h: targetHeight }).greyscale();
237
+ const resized = image.bitmap.data;
238
+ const grayscale = new Uint8Array(targetWidth * targetHeight);
239
+ for (let i = 0, j = 0; i < resized.length; i += 4, j++) {
240
+ grayscale[j] = resized[i];
241
+ }
242
+ return grayscale;
243
+ }
@@ -0,0 +1 @@
1
+ export * from './imageProcessor';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./imageProcessor"), exports);
package/lib/index.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from './config';
3
+ import { EmojiLunaService } from './services';
4
+ import { Category, EmojiItem, EmojiSearchOptions } from './types';
5
+ export declare function apply(ctx: Context, config: Config): void;
6
+ export * from './config';
7
+ export * from './types';
8
+ export declare const inject: string[];
9
+ declare module 'koishi' {
10
+ interface Context {
11
+ emojiluna: EmojiLunaService;
12
+ }
13
+ interface Events {
14
+ 'emojiluna/emoji-added': (emoji: EmojiItem) => void;
15
+ 'emojiluna/emoji-deleted': (id: string) => void;
16
+ 'emojiluna/emoji-updated': (emoji: EmojiItem) => void;
17
+ 'emojiluna/category-added': (category: Category) => void;
18
+ 'emojiluna/category-deleted': (id: string) => void;
19
+ }
20
+ interface Tables {
21
+ emojiluna_emojis: {
22
+ id: string;
23
+ name: string;
24
+ category: string;
25
+ path: string;
26
+ size: number;
27
+ mime_type: string;
28
+ created_at: Date;
29
+ tags: string;
30
+ image_hash: string;
31
+ description: string;
32
+ };
33
+ emojiluna_categories: {
34
+ id: string;
35
+ name: string;
36
+ description: string;
37
+ emoji_count: number;
38
+ created_at: Date;
39
+ };
40
+ }
41
+ }
42
+ declare module '@koishijs/console' {
43
+ interface Events {
44
+ 'emojiluna/getAiTaskStats': () => Promise<{
45
+ pending: number;
46
+ processing: number;
47
+ succeeded: number;
48
+ failed: number;
49
+ paused: boolean;
50
+ }>;
51
+ 'emojiluna/getEmojiCount': (options?: EmojiSearchOptions) => Promise<number>;
52
+ 'emojiluna/getFailedAiEmojiIds': () => Promise<string[]>;
53
+ 'emojiluna/reanalyzeBatch': (ids: string[]) => Promise<number>;
54
+ 'emojiluna/setAiPaused': (paused: boolean) => void;
55
+ 'emojiluna/retryFailedTasks': () => Promise<number>;
56
+ }
57
+ }
package/lib/index.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.inject = void 0;
18
+ exports.apply = apply;
19
+ const services_1 = require("./services");
20
+ const adapters_1 = require("./adapters");
21
+ const collectors_1 = require("./collectors");
22
+ const schema_1 = require("koishi-plugin-chatluna/utils/schema");
23
+ function apply(ctx, config) {
24
+ ctx.plugin(services_1.EmojiLunaService, config);
25
+ ctx.on('ready', () => {
26
+ ctx.inject(['emojiluna'], (ctx) => {
27
+ const emojiluna = ctx.emojiluna;
28
+ const autoCollector = new collectors_1.AutoCollector(ctx, config, emojiluna.debug);
29
+ emojiluna.autoCollector = autoCollector;
30
+ (0, adapters_1.applyCommands)(ctx, config, autoCollector);
31
+ (0, adapters_1.applyBackend)(ctx, config);
32
+ ctx.effect(() => () => autoCollector.dispose());
33
+ autoCollector.start();
34
+ });
35
+ (0, schema_1.modelSchema)(ctx);
36
+ });
37
+ }
38
+ __exportStar(require("./config"), exports);
39
+ __exportStar(require("./types"), exports);
40
+ exports.inject = ['chatluna', 'database'];
@@ -0,0 +1,24 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from '../config';
3
+ import { DebugLogger } from '../debug';
4
+ import { AIAnalyzeResult, AICategorizeResult, AIImageFilterResult } from '../types';
5
+ import { ChatLunaChatModel } from 'koishi-plugin-chatluna/llm-core/platform/model';
6
+ import { ComputedRef } from 'koishi-plugin-chatluna';
7
+ export declare class AIAnalyzer {
8
+ private ctx;
9
+ private config;
10
+ private debug;
11
+ private static readonly AI_FRAME_SAMPLES;
12
+ private _model;
13
+ constructor(ctx: Context, config: Config, debug: DebugLogger);
14
+ get model(): ComputedRef<ChatLunaChatModel>;
15
+ get hasModel(): boolean;
16
+ initialize(): Promise<void>;
17
+ private parseAIResult;
18
+ private callModel;
19
+ private invokeAI;
20
+ private buildAIImages;
21
+ categorize(imageBase64: string): Promise<AICategorizeResult | null>;
22
+ analyze(imageBase64: string): Promise<AIAnalyzeResult | null>;
23
+ filterImageType(imageBase64: string): Promise<AIImageFilterResult | null>;
24
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AIAnalyzer = void 0;
4
+ const utils_1 = require("../utils");
5
+ const image_1 = require("../image");
6
+ const messages_1 = require("@langchain/core/messages");
7
+ const string_1 = require("koishi-plugin-chatluna/utils/string");
8
+ const count_tokens_1 = require("koishi-plugin-chatluna/llm-core/utils/count_tokens");
9
+ class AIAnalyzer {
10
+ ctx;
11
+ config;
12
+ debug;
13
+ static AI_FRAME_SAMPLES = 3;
14
+ _model = null;
15
+ constructor(ctx, config, debug) {
16
+ this.ctx = ctx;
17
+ this.config = config;
18
+ this.debug = debug;
19
+ }
20
+ get model() {
21
+ return this._model;
22
+ }
23
+ get hasModel() {
24
+ return Boolean(this._model?.value);
25
+ }
26
+ async initialize() {
27
+ const needsModel = this.config.autoCategorize ||
28
+ this.config.autoAnalyze ||
29
+ this.config.enableImageTypeFilter;
30
+ if (!needsModel) {
31
+ this.debug.log('ai', '未启用任何 AI 功能,跳过模型加载');
32
+ return;
33
+ }
34
+ if (!this.config.model) {
35
+ this.ctx.logger.warn('AI 功能已启用但未选择模型,请在配置项「使用的AI模型」中设置 model');
36
+ return;
37
+ }
38
+ try {
39
+ const [platform] = (0, count_tokens_1.parseRawModelName)(this.config.model);
40
+ await this.ctx.chatluna.awaitLoadPlatform(platform);
41
+ this._model = await this.ctx.chatluna.createChatModel(this.config.model);
42
+ this.ctx.logger.success('AI模型加载成功');
43
+ this.debug.log('ai', `模型加载成功: ${this.config.model}`);
44
+ }
45
+ catch (error) {
46
+ this._model = null;
47
+ this.ctx.logger.error(`AI模型加载失败: ${error.message}`);
48
+ }
49
+ }
50
+ parseAIResult(result) {
51
+ for (const extractor of utils_1.extractors) {
52
+ const extracted = extractor(result);
53
+ const parsed = (0, utils_1.tryParse)(extracted);
54
+ if (parsed) {
55
+ this.debug.log('ai', `解析成功 (${result.length} 字符)`, parsed);
56
+ return parsed;
57
+ }
58
+ }
59
+ this.ctx.logger.error(`AI结果解析失败: ${result}`);
60
+ return null;
61
+ }
62
+ async callModel(prompt, imageBase64) {
63
+ const images = await this.buildAIImages(imageBase64);
64
+ this.debug.log('ai', `发起请求,图片帧 ${images.length} 张`, {
65
+ model: this.config.model,
66
+ promptLength: prompt.length,
67
+ });
68
+ const result = await (0, utils_1.withTimeout)(this._model.value.invoke([
69
+ new messages_1.SystemMessage(prompt),
70
+ new messages_1.HumanMessage({
71
+ content: [
72
+ { type: 'text', text: 'Please analyze this emoji' },
73
+ ...images.map((image) => ({
74
+ type: 'image_url',
75
+ image_url: {
76
+ url: `data:${image.mimeType};base64,${image.data}`,
77
+ detail: 'low',
78
+ },
79
+ })),
80
+ ],
81
+ }),
82
+ ]), this.config.AIRequestTimeout, 'AI 请求');
83
+ return (0, string_1.getMessageContent)(result.content);
84
+ }
85
+ async invokeAI(prompt, imageBase64) {
86
+ if (!this._model?.value)
87
+ return null;
88
+ return this.parseAIResult(await this.callModel(prompt, imageBase64));
89
+ }
90
+ async buildAIImages(imageBase64) {
91
+ try {
92
+ const buffer = Buffer.from(imageBase64, 'base64');
93
+ const metadata = await (0, image_1.getImageMetadata)(buffer);
94
+ if (metadata.frameCount <= 1) {
95
+ return [
96
+ { data: imageBase64, mimeType: `image/${metadata.format}` }
97
+ ];
98
+ }
99
+ const { frames, metadata: framesMetadata } = await (0, image_1.extractSampledFrames)(buffer, AIAnalyzer.AI_FRAME_SAMPLES, metadata.format);
100
+ if (frames.length === 0) {
101
+ return [
102
+ { data: imageBase64, mimeType: `image/${metadata.format}` }
103
+ ];
104
+ }
105
+ return frames.map((frame) => ({
106
+ data: frame.toString('base64'),
107
+ mimeType: `image/${framesMetadata.format}`
108
+ }));
109
+ }
110
+ catch (error) {
111
+ this.ctx.logger.warn(`AI image preparation failed: ${error.message}`);
112
+ return [{ data: imageBase64, mimeType: 'image/png' }];
113
+ }
114
+ }
115
+ async categorize(imageBase64) {
116
+ if (!this._model?.value || !this.config.autoCategorize)
117
+ return null;
118
+ try {
119
+ const prompt = this.config.categorizePrompt.replaceAll('{categories}', this.config.categories.join(', '));
120
+ return await this.invokeAI(prompt, imageBase64);
121
+ }
122
+ catch (error) {
123
+ this.ctx.logger.error('AI categorization failed:', error);
124
+ return null;
125
+ }
126
+ }
127
+ async analyze(imageBase64) {
128
+ if (!this._model?.value || !this.config.autoAnalyze)
129
+ return null;
130
+ try {
131
+ const prompt = this.config.analyzePrompt.replaceAll('{categories}', this.config.categories.join(', '));
132
+ return await this.invokeAI(prompt, imageBase64);
133
+ }
134
+ catch (error) {
135
+ this.ctx.logger.error('AI analysis failed:', error);
136
+ return null;
137
+ }
138
+ }
139
+ async filterImageType(imageBase64) {
140
+ if (!this._model?.value || !this.config.enableImageTypeFilter) {
141
+ return null;
142
+ }
143
+ try {
144
+ const parsedResult = await this.invokeAI(this.config.imageFilterPrompt, imageBase64);
145
+ if (!parsedResult)
146
+ return null;
147
+ const isAcceptable = parsedResult.isUseful &&
148
+ this.config.acceptedImageTypes.includes(parsedResult.imageType);
149
+ return {
150
+ imageType: parsedResult.imageType,
151
+ isAcceptable,
152
+ confidence: parsedResult.confidence,
153
+ reason: parsedResult.reason
154
+ };
155
+ }
156
+ catch (error) {
157
+ this.ctx.logger.error('AI图片类型过滤失败:', error);
158
+ return null;
159
+ }
160
+ }
161
+ }
162
+ exports.AIAnalyzer = AIAnalyzer;
@@ -0,0 +1,102 @@
1
+ import { Context, Service } from 'koishi';
2
+ import { Config } from '../config';
3
+ import { UploadManager } from '../storage';
4
+ import { AIAnalyzer } from './aiAnalyzer';
5
+ import { AIAnalyzeResult, AICategorizeResult, AIImageFilterResult, AITaskDetail, Category, CategorySearchOptions, EmojiAddOptions, EmojiItem, EmojiSearchOptions, FolderImportOptions, FolderImportResult, FolderScanResult, PaginatedResult, TagInfo, TagSearchOptions } from '../types';
6
+ import { DebugLogger } from '../debug';
7
+ import type { AutoCollector } from '../collectors/autoCollector';
8
+ export declare class EmojiLunaService extends Service {
9
+ config: Config;
10
+ private _emojiStorage;
11
+ private _categories;
12
+ private _isInitialized;
13
+ private _readyPromise;
14
+ private _readyResolve;
15
+ private _isDisposed;
16
+ private _uploadManager;
17
+ private _aiAnalyzer;
18
+ private _byName;
19
+ private _byTag;
20
+ private _byCategory;
21
+ private _storageDir;
22
+ private _uploadToken;
23
+ readonly debug: DebugLogger;
24
+ autoCollector: AutoCollector | null;
25
+ constructor(ctx: Context, config: Config);
26
+ get ready(): Promise<void>;
27
+ get uploadManager(): UploadManager;
28
+ get storageDir(): string;
29
+ get aiAnalyzer(): AIAnalyzer;
30
+ get uploadToken(): string;
31
+ private initializeStorage;
32
+ get isInitialized(): boolean;
33
+ readEmojiBuffer(emoji: EmojiItem): Promise<Buffer | null>;
34
+ private indexEmoji;
35
+ private unindexEmoji;
36
+ private reindexEmoji;
37
+ categorizeEmoji(imageBase64: string): Promise<AICategorizeResult | null>;
38
+ analyzeEmoji(imageBase64: string): Promise<AIAnalyzeResult | null>;
39
+ filterImageByType(imageBase64: string): Promise<AIImageFilterResult | null>;
40
+ private assertActive;
41
+ addEmoji(options: EmojiAddOptions, source: Buffer | {
42
+ path: string;
43
+ }, aiAnalysis?: boolean): Promise<EmojiItem>;
44
+ private buildEmojiRecord;
45
+ private persistEmoji;
46
+ addEmojis(emojis: {
47
+ options: EmojiAddOptions;
48
+ buffer: Buffer;
49
+ }[], aiAnalysis: boolean): Promise<EmojiItem[]>;
50
+ getEmojiByName(name: string): Promise<EmojiItem | null>;
51
+ getEmojisByName(name: string): Promise<EmojiItem[]>;
52
+ getEmojiById(id: string): Promise<EmojiItem | null>;
53
+ private filterEmojis;
54
+ getEmojiList(options?: EmojiSearchOptions): Promise<EmojiItem[]>;
55
+ getEmojiPage(options?: EmojiSearchOptions): Promise<PaginatedResult<EmojiItem>>;
56
+ searchEmoji(keyword: string): Promise<EmojiItem[]>;
57
+ deleteEmoji(id: string): Promise<boolean>;
58
+ deleteAllEmojis(): Promise<boolean>;
59
+ updateEmojiInfo(id: string, updates: Partial<Pick<EmojiItem, 'name' | 'category' | 'tags' | 'description'>>): Promise<boolean>;
60
+ updateEmojiName(id: string, name: string): Promise<boolean>;
61
+ updateEmojiTags(id: string, tags: string[]): Promise<boolean>;
62
+ updateEmojiCategory(id: string, category: string): Promise<boolean>;
63
+ addCategory(name: string, description?: string): Promise<Category>;
64
+ getCategories(): Promise<Category[]>;
65
+ getCategoriesPage(options?: CategorySearchOptions): Promise<PaginatedResult<Category>>;
66
+ getCategoryByName(name: string): Promise<Category | null>;
67
+ deleteCategory(id: string, deleteEmojis?: boolean): Promise<boolean>;
68
+ cleanupEmptyCategories(): Promise<number>;
69
+ private buildTagUsageMap;
70
+ getAllTags(): Promise<string[]>;
71
+ getTagsPage(options?: TagSearchOptions): Promise<PaginatedResult<TagInfo>>;
72
+ cleanupEmptyTags(): Promise<number>;
73
+ categorizeExistingEmojis(): Promise<{
74
+ success: number;
75
+ failed: number;
76
+ }>;
77
+ private static readonly SUPPORTED_EXTENSIONS;
78
+ scanFolder(folderPath: string): Promise<FolderScanResult>;
79
+ private scanFolderRecursive;
80
+ private emojiNameExists;
81
+ importFromFolder(options: FolderImportOptions): Promise<FolderImportResult>;
82
+ private updateCategoryEmojiCount;
83
+ private loadEmojis;
84
+ private loadCategories;
85
+ getEmojiCount(): number;
86
+ getCategoryCount(): number;
87
+ getAiTaskStats(): Promise<{
88
+ pending: number;
89
+ processing: number;
90
+ succeeded: number;
91
+ failed: number;
92
+ paused: boolean;
93
+ }>;
94
+ getFailedAiEmojiIds(): Promise<string[]>;
95
+ setAiPaused(paused: boolean): void;
96
+ retryFailedTasks(): Promise<number>;
97
+ reanalyzeBatch(ids: string[]): Promise<number>;
98
+ getAiTasksAll(): Promise<AITaskDetail[]>;
99
+ deleteAiTask(emojiId: string): Promise<void>;
100
+ retryAiTask(emojiId: string): Promise<void>;
101
+ static inject: string[];
102
+ }