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.
- package/LICENSE +24 -0
- package/dist/index.js +10 -0
- package/dist/style.css +1 -0
- package/lib/adapters/autoCommands.d.ts +4 -0
- package/lib/adapters/autoCommands.js +111 -0
- package/lib/adapters/backend.d.ts +3 -0
- package/lib/adapters/backend.js +11 -0
- package/lib/adapters/chatlunaIntegration.d.ts +3 -0
- package/lib/adapters/chatlunaIntegration.js +48 -0
- package/lib/adapters/commands.d.ts +4 -0
- package/lib/adapters/commands.js +330 -0
- package/lib/adapters/consoleListeners.d.ts +3 -0
- package/lib/adapters/consoleListeners.js +168 -0
- package/lib/adapters/index.d.ts +2 -0
- package/lib/adapters/index.js +7 -0
- package/lib/adapters/render.d.ts +7 -0
- package/lib/adapters/render.js +35 -0
- package/lib/adapters/restApi.d.ts +3 -0
- package/lib/adapters/restApi.js +225 -0
- package/lib/collectors/autoCollector.d.ts +75 -0
- package/lib/collectors/autoCollector.js +728 -0
- package/lib/collectors/index.d.ts +1 -0
- package/lib/collectors/index.js +17 -0
- package/lib/config.d.ts +194 -0
- package/lib/config.js +369 -0
- package/lib/debug.d.ts +13 -0
- package/lib/debug.js +44 -0
- package/lib/image/imageProcessor.d.ts +20 -0
- package/lib/image/imageProcessor.js +243 -0
- package/lib/image/index.d.ts +1 -0
- package/lib/image/index.js +17 -0
- package/lib/index.d.ts +57 -0
- package/lib/index.js +40 -0
- package/lib/services/aiAnalyzer.d.ts +24 -0
- package/lib/services/aiAnalyzer.js +162 -0
- package/lib/services/emojiluna.d.ts +102 -0
- package/lib/services/emojiluna.js +910 -0
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.js +18 -0
- package/lib/storage/index.d.ts +1 -0
- package/lib/storage/index.js +17 -0
- package/lib/storage/uploadManager.d.ts +102 -0
- package/lib/storage/uploadManager.js +424 -0
- package/lib/types.d.ts +159 -0
- package/lib/types.js +69 -0
- package/lib/utils.d.ts +29 -0
- package/lib/utils.js +256 -0
- package/package.json +99 -0
- package/readme.md +174 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.applyConsoleListeners = applyConsoleListeners;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const DIRECT_EVENTS = {
|
|
9
|
+
'emojiluna/getEmojiList': 'getEmojiList',
|
|
10
|
+
'emojiluna/getEmojiPage': 'getEmojiPage',
|
|
11
|
+
'emojiluna/searchEmoji': 'searchEmoji',
|
|
12
|
+
'emojiluna/getCategories': 'getCategories',
|
|
13
|
+
'emojiluna/getCategoriesPage': 'getCategoriesPage',
|
|
14
|
+
'emojiluna/getAllTags': 'getAllTags',
|
|
15
|
+
'emojiluna/getTagsPage': 'getTagsPage',
|
|
16
|
+
'emojiluna/updateEmojiName': 'updateEmojiName',
|
|
17
|
+
'emojiluna/updateEmojiTags': 'updateEmojiTags',
|
|
18
|
+
'emojiluna/updateEmojiCategory': 'updateEmojiCategory',
|
|
19
|
+
'emojiluna/deleteEmoji': 'deleteEmoji',
|
|
20
|
+
'emojiluna/addCategory': 'addCategory',
|
|
21
|
+
'emojiluna/deleteCategory': 'deleteCategory',
|
|
22
|
+
'emojiluna/cleanupEmptyCategories': 'cleanupEmptyCategories',
|
|
23
|
+
'emojiluna/cleanupEmptyTags': 'cleanupEmptyTags',
|
|
24
|
+
'emojiluna/getAiTaskStats': 'getAiTaskStats',
|
|
25
|
+
'emojiluna/getFailedAiEmojiIds': 'getFailedAiEmojiIds',
|
|
26
|
+
'emojiluna/retryFailedTasks': 'retryFailedTasks',
|
|
27
|
+
'emojiluna/getAiTasksAll': 'getAiTasksAll',
|
|
28
|
+
'emojiluna/setAiPaused': 'setAiPaused',
|
|
29
|
+
'emojiluna/reanalyzeBatch': 'reanalyzeBatch',
|
|
30
|
+
'emojiluna/deleteAiTask': 'deleteAiTask',
|
|
31
|
+
'emojiluna/retryAiTask': 'retryAiTask',
|
|
32
|
+
};
|
|
33
|
+
const MAX_REQUESTED_EMOJI_NAME = 64;
|
|
34
|
+
function applyConsoleListeners(ctx, config) {
|
|
35
|
+
ctx.inject(['console', 'emojiluna'], (ctx) => {
|
|
36
|
+
void registerListeners(ctx, config);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function registerListeners(ctx, config) {
|
|
40
|
+
const emojiluna = ctx.emojiluna;
|
|
41
|
+
const invoke = emojiluna;
|
|
42
|
+
await emojiluna.ready;
|
|
43
|
+
ctx.console.addEntry({
|
|
44
|
+
dev: path_1.default.resolve(__dirname, '../../client/index.ts'),
|
|
45
|
+
prod: path_1.default.resolve(__dirname, '../dist'),
|
|
46
|
+
});
|
|
47
|
+
for (const [event, method] of Object.entries(DIRECT_EVENTS)) {
|
|
48
|
+
ctx.console.addListener(event, invoke[method]);
|
|
49
|
+
}
|
|
50
|
+
ctx.console.addListener('emojiluna/getBaseUrl', async () => {
|
|
51
|
+
const selfUrl = config.selfUrl || ctx.server?.selfUrl || '';
|
|
52
|
+
return selfUrl ? `${selfUrl.replace(/\/+$/, '')}${config.backendPath}` : '';
|
|
53
|
+
});
|
|
54
|
+
ctx.console.addListener('emojiluna/getUploadToken', async () => {
|
|
55
|
+
return emojiluna.uploadToken;
|
|
56
|
+
});
|
|
57
|
+
ctx.console.addListener('emojiluna/getEmojiCount', async (options) => {
|
|
58
|
+
const list = await emojiluna.getEmojiList(options ?? {});
|
|
59
|
+
return list.length;
|
|
60
|
+
});
|
|
61
|
+
ctx.console.addListener('emojiluna/addEmoji', async (emojiData) => {
|
|
62
|
+
const buffer = decodeImagePayload(emojiData);
|
|
63
|
+
return await emojiluna.addEmoji({
|
|
64
|
+
name: normalizeName(emojiData.name),
|
|
65
|
+
category: emojiData.category || config.autoCollectCategory,
|
|
66
|
+
tags: emojiData.tags ?? [],
|
|
67
|
+
}, buffer);
|
|
68
|
+
});
|
|
69
|
+
ctx.console.addListener('emojiluna/addEmojis', async (emojis, aiAnalysis) => {
|
|
70
|
+
if (!Array.isArray(emojis) || !emojis.length) {
|
|
71
|
+
throw new Error('表情包数据数组为必填项');
|
|
72
|
+
}
|
|
73
|
+
const payloads = emojis.map((item) => ({
|
|
74
|
+
options: {
|
|
75
|
+
name: normalizeName(item.name),
|
|
76
|
+
category: item.category || config.autoCollectCategory,
|
|
77
|
+
tags: item.tags ?? [],
|
|
78
|
+
},
|
|
79
|
+
buffer: decodeImagePayload(item),
|
|
80
|
+
}));
|
|
81
|
+
return await emojiluna.addEmojis(payloads, aiAnalysis);
|
|
82
|
+
});
|
|
83
|
+
ctx.console.addListener('emojiluna/analyzeEmoji', async (id) => {
|
|
84
|
+
const emoji = await emojiluna.getEmojiById(id);
|
|
85
|
+
if (!emoji)
|
|
86
|
+
throw new Error('表情包不存在');
|
|
87
|
+
const imageBuffer = await emojiluna.readEmojiBuffer(emoji);
|
|
88
|
+
if (!imageBuffer)
|
|
89
|
+
throw new Error('表情包文件读取失败');
|
|
90
|
+
const result = await emojiluna.analyzeEmoji(imageBuffer.toString('base64'));
|
|
91
|
+
if (!result)
|
|
92
|
+
return { success: false, message: 'AI分析未返回结果,请检查模型或开启 debug' };
|
|
93
|
+
const oldData = {
|
|
94
|
+
name: emoji.name,
|
|
95
|
+
category: emoji.category,
|
|
96
|
+
tags: [...emoji.tags],
|
|
97
|
+
};
|
|
98
|
+
const updates = [];
|
|
99
|
+
const nextTags = normalizeTags(result.tags, emoji.tags);
|
|
100
|
+
if (result.category && result.category !== emoji.category) {
|
|
101
|
+
await emojiluna.updateEmojiCategory(id, result.category);
|
|
102
|
+
updates.push(`分类: ${emoji.category} → ${result.category}`);
|
|
103
|
+
}
|
|
104
|
+
if (nextTags.join(',') !== oldData.tags.join(',')) {
|
|
105
|
+
await emojiluna.updateEmojiTags(id, nextTags);
|
|
106
|
+
updates.push(`标签: [${oldData.tags.join(', ')}] → [${nextTags.join(', ')}]`);
|
|
107
|
+
}
|
|
108
|
+
emojiluna.debug.log('ai', `手动分析 ${id} 完成`, { updates, result });
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
updates,
|
|
112
|
+
result,
|
|
113
|
+
oldData,
|
|
114
|
+
newData: {
|
|
115
|
+
name: result.name,
|
|
116
|
+
category: result.category,
|
|
117
|
+
tags: nextTags,
|
|
118
|
+
description: result.description,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
ctx.console.addListener('emojiluna/scanFolder', async (folderPath) => {
|
|
123
|
+
assertFolderAllowed(ctx, config, folderPath);
|
|
124
|
+
return await emojiluna.scanFolder(folderPath);
|
|
125
|
+
});
|
|
126
|
+
ctx.console.addListener('emojiluna/importFromFolder', async (options) => {
|
|
127
|
+
assertFolderAllowed(ctx, config, options?.folderPath);
|
|
128
|
+
emojiluna.debug.log('service', `控制台导入目录 ${options?.folderPath}`, options);
|
|
129
|
+
return await emojiluna.importFromFolder(options);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function normalizeName(name) {
|
|
133
|
+
const value = (name ?? '').trim();
|
|
134
|
+
if (!value)
|
|
135
|
+
throw new Error('表情包名称为必填项');
|
|
136
|
+
return value.slice(0, MAX_REQUESTED_EMOJI_NAME);
|
|
137
|
+
}
|
|
138
|
+
function normalizeTags(next, current) {
|
|
139
|
+
const merged = [...new Set([...(current ?? []), ...(next ?? [])])];
|
|
140
|
+
return merged.map((tag) => tag.trim()).filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
function decodeImagePayload(emojiData) {
|
|
143
|
+
if (!emojiData?.imageData) {
|
|
144
|
+
throw new Error('图片数据为必填项');
|
|
145
|
+
}
|
|
146
|
+
const buffer = Buffer.from(emojiData.imageData, 'base64');
|
|
147
|
+
if (!buffer.length)
|
|
148
|
+
throw new Error('图片数据为空或不是合法的 base64');
|
|
149
|
+
return buffer;
|
|
150
|
+
}
|
|
151
|
+
function assertFolderAllowed(ctx, config, folderPath) {
|
|
152
|
+
if (!folderPath?.trim())
|
|
153
|
+
throw new Error('文件夹路径不能为空');
|
|
154
|
+
const roots = config.folderImportRoots ?? [];
|
|
155
|
+
if (!roots.length)
|
|
156
|
+
return;
|
|
157
|
+
const resolved = path_1.default.resolve(folderPath).toLowerCase();
|
|
158
|
+
const allowed = roots.some((root) => {
|
|
159
|
+
if (!root?.trim())
|
|
160
|
+
return false;
|
|
161
|
+
const prefix = path_1.default.resolve(root).toLowerCase().replace(/[\\/]+$/, '');
|
|
162
|
+
return resolved === prefix || resolved.startsWith(prefix + path_1.default.sep);
|
|
163
|
+
});
|
|
164
|
+
if (!allowed) {
|
|
165
|
+
ctx.emojiluna.debug.log('service', `拒绝访问目录 ${folderPath}(不在允许列表内)`);
|
|
166
|
+
throw new Error('该目录不在 folderImportRoots 允许范围内');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyCommands = exports.applyBackend = void 0;
|
|
4
|
+
var backend_1 = require("./backend");
|
|
5
|
+
Object.defineProperty(exports, "applyBackend", { enumerable: true, get: function () { return backend_1.applyBackend; } });
|
|
6
|
+
var commands_1 = require("./commands");
|
|
7
|
+
Object.defineProperty(exports, "applyCommands", { enumerable: true, get: function () { return commands_1.applyCommands; } });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Context, h } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
import { EmojiItem } from '../types';
|
|
4
|
+
export declare function truncate(text: string, max?: number): string;
|
|
5
|
+
export declare function emojiUrl(ctx: Context, config: Config, emoji: EmojiItem): string | null;
|
|
6
|
+
export declare function emojiText(emoji: EmojiItem): string;
|
|
7
|
+
export declare function emojiMessage(ctx: Context, config: Config, emoji: EmojiItem): Promise<h>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.truncate = truncate;
|
|
4
|
+
exports.emojiUrl = emojiUrl;
|
|
5
|
+
exports.emojiText = emojiText;
|
|
6
|
+
exports.emojiMessage = emojiMessage;
|
|
7
|
+
const koishi_1 = require("koishi");
|
|
8
|
+
const DEFAULT_TRUNCATE = 200;
|
|
9
|
+
function truncate(text, max = DEFAULT_TRUNCATE) {
|
|
10
|
+
if (!text)
|
|
11
|
+
return '';
|
|
12
|
+
return text.length <= max ? text : text.slice(0, max - 1) + '…';
|
|
13
|
+
}
|
|
14
|
+
function emojiUrl(ctx, config, emoji) {
|
|
15
|
+
if (!config.backendServer)
|
|
16
|
+
return null;
|
|
17
|
+
const selfUrl = config.selfUrl || ctx.server?.selfUrl || '';
|
|
18
|
+
if (!selfUrl)
|
|
19
|
+
return null;
|
|
20
|
+
return `${selfUrl.replace(/\/+$/, '')}${config.backendPath}/get/${encodeURIComponent(emoji.id)}`;
|
|
21
|
+
}
|
|
22
|
+
function emojiText(emoji) {
|
|
23
|
+
const tags = emoji.tags.length ? ` #${emoji.tags.join(' #')}` : '';
|
|
24
|
+
return truncate(`${emoji.name} [${emoji.category}]${tags}`, 120);
|
|
25
|
+
}
|
|
26
|
+
async function emojiMessage(ctx, config, emoji) {
|
|
27
|
+
const url = emojiUrl(ctx, config, emoji);
|
|
28
|
+
if (url)
|
|
29
|
+
return koishi_1.h.image(url);
|
|
30
|
+
const buffer = await ctx.emojiluna.readEmojiBuffer(emoji);
|
|
31
|
+
if (!buffer) {
|
|
32
|
+
return koishi_1.h.text(`表情包文件不可读: ${emoji.name} (${emoji.id})`);
|
|
33
|
+
}
|
|
34
|
+
return koishi_1.h.image(`data:${emoji.mimeType};base64,${buffer.toString('base64')}`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.applyRestApi = applyRestApi;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const formidable_1 = __importDefault(require("formidable"));
|
|
11
|
+
const UPLOAD_DIR_NAME = 'uploads';
|
|
12
|
+
const MAX_QUERY_LIMIT = 200;
|
|
13
|
+
const DEFAULT_SEARCH_LIMIT = 50;
|
|
14
|
+
function getSingleQuery(value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value[0];
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return value;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function toFiniteNumber(value) {
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
24
|
+
}
|
|
25
|
+
function getPaginationParams(query) {
|
|
26
|
+
const limit = toFiniteNumber(getSingleQuery(query.limit));
|
|
27
|
+
const offset = toFiniteNumber(getSingleQuery(query.offset));
|
|
28
|
+
return {
|
|
29
|
+
limit: limit === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: Math.max(1, Math.min(MAX_QUERY_LIMIT, limit)),
|
|
32
|
+
offset: offset === undefined ? undefined : Math.max(0, offset),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function firstField(fields, key) {
|
|
36
|
+
return getSingleQuery(fields[key]);
|
|
37
|
+
}
|
|
38
|
+
function parseTagsField(raw) {
|
|
39
|
+
if (!raw)
|
|
40
|
+
return [];
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (Array.isArray(parsed)) {
|
|
44
|
+
return parsed.filter((tag) => typeof tag === 'string');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return raw
|
|
49
|
+
.split(',')
|
|
50
|
+
.map((tag) => tag.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
function readUploadToken(koa) {
|
|
56
|
+
const header = (koa.get('x-upload-token') || koa.get('authorization') || '').toString();
|
|
57
|
+
return header.startsWith('Bearer ') ? header.slice(7).trim() : header.trim();
|
|
58
|
+
}
|
|
59
|
+
function json(koa, body, status = 200) {
|
|
60
|
+
koa.status = status;
|
|
61
|
+
koa.set('Content-Type', 'application/json');
|
|
62
|
+
koa.body = JSON.stringify(body);
|
|
63
|
+
}
|
|
64
|
+
async function serveImage(ctx, koa, emoji) {
|
|
65
|
+
if (!emoji) {
|
|
66
|
+
koa.status = 404;
|
|
67
|
+
koa.body = 'Emoji not found';
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const buffer = await ctx.emojiluna.readEmojiBuffer(emoji);
|
|
71
|
+
if (!buffer) {
|
|
72
|
+
koa.status = 404;
|
|
73
|
+
koa.body = 'Emoji file unreadable';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
koa.status = 200;
|
|
77
|
+
koa.set('Content-Type', emoji.mimeType || (0, utils_1.getImageType)(buffer));
|
|
78
|
+
koa.set('Content-Length', buffer.length.toString());
|
|
79
|
+
koa.body = buffer;
|
|
80
|
+
}
|
|
81
|
+
async function serveRandom(ctx, koa, emojis) {
|
|
82
|
+
if (!emojis.length) {
|
|
83
|
+
koa.status = 404;
|
|
84
|
+
koa.body = 'No emojis available';
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const picked = emojis[Math.floor(Math.random() * emojis.length)];
|
|
88
|
+
await serveImage(ctx, koa, picked);
|
|
89
|
+
}
|
|
90
|
+
function uploadFailureStatus(message) {
|
|
91
|
+
if (/已达上限/.test(message))
|
|
92
|
+
return 409;
|
|
93
|
+
if (/already exists|已存在/.test(message))
|
|
94
|
+
return 409;
|
|
95
|
+
if (/图片过大|too large|maxFileSize|File size/.test(message))
|
|
96
|
+
return 413;
|
|
97
|
+
if (/无法识别的图片格式|Invalid image/.test(message))
|
|
98
|
+
return 415;
|
|
99
|
+
return 500;
|
|
100
|
+
}
|
|
101
|
+
function applyRestApi(ctx, config) {
|
|
102
|
+
if (!config.backendServer)
|
|
103
|
+
return;
|
|
104
|
+
const debug = ctx.emojiluna.debug;
|
|
105
|
+
ctx.inject(['server', 'emojiluna'], async (ctx) => {
|
|
106
|
+
await ctx.emojiluna.ready;
|
|
107
|
+
const base = config.backendPath;
|
|
108
|
+
ctx.server.get(`${base}/list`, async (koa) => {
|
|
109
|
+
const { limit, offset } = getPaginationParams(koa.request.query);
|
|
110
|
+
const tag = getSingleQuery(koa.request.query.tag);
|
|
111
|
+
const emojis = await ctx.emojiluna.getEmojiList({
|
|
112
|
+
keyword: getSingleQuery(koa.request.query.keyword),
|
|
113
|
+
category: getSingleQuery(koa.request.query.category),
|
|
114
|
+
tags: tag ? [tag] : undefined,
|
|
115
|
+
limit,
|
|
116
|
+
offset,
|
|
117
|
+
});
|
|
118
|
+
debug.log('http', `GET ${base}/list -> ${emojis.length}`);
|
|
119
|
+
json(koa, emojis);
|
|
120
|
+
});
|
|
121
|
+
ctx.server.get(`${base}/search`, async (koa) => {
|
|
122
|
+
const keyword = getSingleQuery(koa.request.query.keyword) ?? '';
|
|
123
|
+
const { limit, offset } = getPaginationParams(koa.request.query);
|
|
124
|
+
const matched = await ctx.emojiluna.searchEmoji(keyword);
|
|
125
|
+
const from = offset ?? 0;
|
|
126
|
+
const emojis = matched.slice(from, limit ?? DEFAULT_SEARCH_LIMIT);
|
|
127
|
+
debug.log('http', `GET ${base}/search ${keyword} -> ${emojis.length}/${matched.length}`);
|
|
128
|
+
json(koa, emojis);
|
|
129
|
+
});
|
|
130
|
+
ctx.server.get(`${base}/categories`, async (koa) => {
|
|
131
|
+
const { limit, offset } = getPaginationParams(koa.request.query);
|
|
132
|
+
const page = await ctx.emojiluna.getCategoriesPage({
|
|
133
|
+
keyword: getSingleQuery(koa.request.query.keyword),
|
|
134
|
+
limit,
|
|
135
|
+
offset,
|
|
136
|
+
});
|
|
137
|
+
debug.log('http', `GET ${base}/categories -> ${page.items.length}/${page.total}`);
|
|
138
|
+
json(koa, page.items);
|
|
139
|
+
});
|
|
140
|
+
ctx.server.get(`${base}/categories/:category`, async (koa) => {
|
|
141
|
+
const { category } = koa.params;
|
|
142
|
+
const emojis = await ctx.emojiluna.getEmojiList({ category });
|
|
143
|
+
debug.log('http', `GET ${base}/categories/${category} -> ${emojis.length}`);
|
|
144
|
+
await serveRandom(ctx, koa, emojis);
|
|
145
|
+
});
|
|
146
|
+
ctx.server.get(`${base}/tags`, async (koa) => {
|
|
147
|
+
const tags = await ctx.emojiluna.getAllTags();
|
|
148
|
+
debug.log('http', `GET ${base}/tags -> ${tags.length}`);
|
|
149
|
+
json(koa, tags);
|
|
150
|
+
});
|
|
151
|
+
ctx.server.get(`${base}/tags/:tag`, async (koa) => {
|
|
152
|
+
const { tag } = koa.params;
|
|
153
|
+
const emojis = await ctx.emojiluna.getEmojiList({ tags: [tag] });
|
|
154
|
+
debug.log('http', `GET ${base}/tags/${tag} -> ${emojis.length}`);
|
|
155
|
+
await serveRandom(ctx, koa, emojis);
|
|
156
|
+
});
|
|
157
|
+
ctx.server.get(`${base}/random`, async (koa) => {
|
|
158
|
+
const emojis = await ctx.emojiluna.getEmojiList();
|
|
159
|
+
debug.log('http', `GET ${base}/random from ${emojis.length} emojis`);
|
|
160
|
+
await serveRandom(ctx, koa, emojis);
|
|
161
|
+
});
|
|
162
|
+
ctx.server.get(`${base}/get/:id`, async (koa) => {
|
|
163
|
+
const { id } = koa.params;
|
|
164
|
+
const emoji = (await ctx.emojiluna.getEmojiById(id)) ??
|
|
165
|
+
(await ctx.emojiluna.getEmojiByName(id));
|
|
166
|
+
debug.log('http', `GET ${base}/get/${id} -> ${emoji ? emoji.id : 'miss'}`);
|
|
167
|
+
await serveImage(ctx, koa, emoji);
|
|
168
|
+
});
|
|
169
|
+
ctx.server.post(`${base}/upload`, async (koa) => {
|
|
170
|
+
if (readUploadToken(koa) !== ctx.emojiluna.uploadToken) {
|
|
171
|
+
debug.log('http', 'POST upload 被拒绝:token 不匹配');
|
|
172
|
+
return json(koa, { success: false, message: 'Unauthorized' }, 401);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const { fields, file } = await readUploadPayload(ctx, koa);
|
|
176
|
+
if (!file?.filepath) {
|
|
177
|
+
return json(koa, { success: false, message: 'No file uploaded' }, 400);
|
|
178
|
+
}
|
|
179
|
+
const emoji = await ctx.emojiluna.addEmoji({
|
|
180
|
+
name: firstField(fields, 'name') ||
|
|
181
|
+
file.originalFilename?.replace(/\.[^/.]+$/, '') ||
|
|
182
|
+
'uploaded',
|
|
183
|
+
category: firstField(fields, 'category'),
|
|
184
|
+
tags: parseTagsField(firstField(fields, 'tags')),
|
|
185
|
+
}, { path: file.filepath }, firstField(fields, 'aiAnalysis') === 'true');
|
|
186
|
+
debug.log('http', `POST upload -> ${emoji.id}`);
|
|
187
|
+
return json(koa, { success: true, emoji });
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const message = error.message ?? String(error);
|
|
191
|
+
ctx.logger.error(`Upload endpoint error: ${message}`);
|
|
192
|
+
debug.log('http', `POST upload 失败: ${message}`);
|
|
193
|
+
return json(koa, { success: false, message }, uploadFailureStatus(message));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function readUploadPayload(ctx, koa) {
|
|
199
|
+
const request = koa.request;
|
|
200
|
+
if (request.files) {
|
|
201
|
+
const file = request.files.file;
|
|
202
|
+
return {
|
|
203
|
+
fields: request.body ?? {},
|
|
204
|
+
file: Array.isArray(file) ? file[0] : file,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const uploadDir = path_1.default.join(ctx.emojiluna.storageDir, UPLOAD_DIR_NAME);
|
|
208
|
+
await promises_1.default.mkdir(uploadDir, { recursive: true });
|
|
209
|
+
const form = (0, formidable_1.default)({
|
|
210
|
+
uploadDir,
|
|
211
|
+
keepExtensions: true,
|
|
212
|
+
maxFileSize: (ctx.emojiluna.config.maxFileSize || ctx.emojiluna.config.maxEmojiSize) *
|
|
213
|
+
1024 * 1024,
|
|
214
|
+
multiples: false,
|
|
215
|
+
});
|
|
216
|
+
const [fields, files] = await new Promise((settle, fail) => {
|
|
217
|
+
form.parse(koa.req, (error, parsedFields, parsedFiles) => {
|
|
218
|
+
if (error)
|
|
219
|
+
return fail(error);
|
|
220
|
+
settle([parsedFields, parsedFiles]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
const file = files.file;
|
|
224
|
+
return { fields, file: (Array.isArray(file) ? file[0] : file) };
|
|
225
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Context, h, Session } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
import { DebugLogger } from '../debug';
|
|
4
|
+
export interface CollectTrace {
|
|
5
|
+
gate: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CollectStats {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
configured: boolean;
|
|
12
|
+
mode: 'instant' | 'frequency';
|
|
13
|
+
seen: number;
|
|
14
|
+
collected: number;
|
|
15
|
+
rejected: Record<string, number>;
|
|
16
|
+
queue: number;
|
|
17
|
+
active: number;
|
|
18
|
+
signatures: number;
|
|
19
|
+
preloaded: boolean;
|
|
20
|
+
recent: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare class AutoCollector {
|
|
23
|
+
private ctx;
|
|
24
|
+
private config;
|
|
25
|
+
private debug;
|
|
26
|
+
private signatures;
|
|
27
|
+
private frequency;
|
|
28
|
+
private counters;
|
|
29
|
+
private globalDay;
|
|
30
|
+
private pendingRefs;
|
|
31
|
+
private inFlight;
|
|
32
|
+
private rejectedHashes;
|
|
33
|
+
private aiVerdicts;
|
|
34
|
+
private queue;
|
|
35
|
+
private active;
|
|
36
|
+
private nameSeq;
|
|
37
|
+
private manualEnabled;
|
|
38
|
+
private signaturesReady;
|
|
39
|
+
private stopped;
|
|
40
|
+
private metrics;
|
|
41
|
+
private recent;
|
|
42
|
+
constructor(ctx: Context, config: Config, debug: DebugLogger);
|
|
43
|
+
private get luna();
|
|
44
|
+
get isEnabled(): boolean;
|
|
45
|
+
start(): void;
|
|
46
|
+
observe(session: Session): number;
|
|
47
|
+
probe(session: Session, dryRun?: boolean, elements?: h[]): Promise<CollectTrace[]>;
|
|
48
|
+
stats(): CollectStats;
|
|
49
|
+
setEnabled(enabled: boolean | null): void;
|
|
50
|
+
resetCounters(): void;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
private pump;
|
|
53
|
+
private processJob;
|
|
54
|
+
private save;
|
|
55
|
+
private evaluateScope;
|
|
56
|
+
private evaluateSize;
|
|
57
|
+
private evaluateResolution;
|
|
58
|
+
private evaluateKnownHash;
|
|
59
|
+
private evaluateSimilarity;
|
|
60
|
+
private evaluateFrequency;
|
|
61
|
+
private evaluateLimit;
|
|
62
|
+
private evaluateLibrary;
|
|
63
|
+
private evaluateAiFilter;
|
|
64
|
+
private buildSignature;
|
|
65
|
+
private preloadSignatures;
|
|
66
|
+
private rememberSignature;
|
|
67
|
+
private windowCounter;
|
|
68
|
+
private consumeQuota;
|
|
69
|
+
private pruneCaches;
|
|
70
|
+
private chatKey;
|
|
71
|
+
private buildName;
|
|
72
|
+
private buildTags;
|
|
73
|
+
private reject;
|
|
74
|
+
private record;
|
|
75
|
+
}
|