koishi-plugin-emojiluna-plus 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/adapters/autoCommands.js +7 -9
- package/lib/adapters/chatlunaIntegration.js +2 -2
- package/lib/adapters/commands.js +2 -2
- package/lib/adapters/consoleListeners.js +15 -5
- package/lib/adapters/render.d.ts +5 -0
- package/lib/adapters/render.js +11 -2
- package/lib/adapters/restApi.js +5 -3
- package/lib/collectors/autoCollector.d.ts +4 -0
- package/lib/collectors/autoCollector.js +42 -9
- package/lib/services/emojiluna.d.ts +6 -0
- package/lib/services/emojiluna.js +24 -3
- package/lib/types.d.ts +5 -0
- package/lib/types.js +6 -1
- package/lib/utils.js +6 -2
- package/package.json +1 -1
- package/readme.md +10 -19
|
@@ -4,6 +4,8 @@ exports.registerAutoCollectCommands = registerAutoCollectCommands;
|
|
|
4
4
|
const koishi_1 = require("koishi");
|
|
5
5
|
const render_1 = require("./render");
|
|
6
6
|
const types_1 = require("../types");
|
|
7
|
+
const DEFAULT_HOUR_LIMIT = types_1.DEFAULT_COLLECT_LIMITS.hour;
|
|
8
|
+
const DEFAULT_DAY_LIMIT = types_1.DEFAULT_COLLECT_LIMITS.day;
|
|
7
9
|
const RECENT_RECORDS = 6;
|
|
8
10
|
const DEBUG_LINES = 15;
|
|
9
11
|
function registerAutoCollectCommands(ctx, config, autoCollector) {
|
|
@@ -83,7 +85,7 @@ function formatStatus(config, autoCollector) {
|
|
|
83
85
|
`条件: ${config.minEmojiSize}KB-${config.maxEmojiSize}MB${config.minEdgePixels ? ` | 短边≥${config.minEdgePixels}px` : ''} | 相似度阈值 ${config.similarityThreshold}`,
|
|
84
86
|
`取源: HTTP ${config.fetchTimeout}ms 超时 | 本地路径 ${config.allowLocalPaths ? '允许' : '禁止'}${config.localPathRoots.length ? `(限定 ${config.localPathRoots.length} 个目录)` : ''}`,
|
|
85
87
|
`AI: 类型过滤 ${config.enableImageTypeFilter ? `开启(超时 ${config.aiFilterTimeout}ms,兜底 ${config.aiFilterFallback})` : '关闭'} | 入库后分析 ${config.autoAnalyze ? '开启' : '关闭'}`,
|
|
86
|
-
`限流:
|
|
88
|
+
`限流: 单会话 ${quotaSummary(config)} | 全局 ${config.globalDayLimit || '不限'}/日 | 库存上限 ${config.maxEmojiCount}`,
|
|
87
89
|
`统计: 收到 ${stats.seen} 张 | 成功 ${stats.collected} 张 | 队列 ${stats.queue}/${config.autoCollectQueueSize} | 处理中 ${stats.active} | 特征 ${stats.signatures}${stats.preloaded ? '' : '(预热中)'}`,
|
|
88
90
|
];
|
|
89
91
|
const rejected = Object.entries(stats.rejected).sort((a, b) => b[1] - a[1]);
|
|
@@ -101,12 +103,8 @@ function formatStatus(config, autoCollector) {
|
|
|
101
103
|
}
|
|
102
104
|
return lines.join('\n');
|
|
103
105
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return firstQuota(config)?.dayLimit ?? 100;
|
|
109
|
-
}
|
|
110
|
-
function firstQuota(config) {
|
|
111
|
-
return Object.values(config.groupAutoCollectLimit ?? {})[0];
|
|
106
|
+
function quotaSummary(config) {
|
|
107
|
+
const overrides = Object.keys(config.groupAutoCollectLimit ?? {}).length;
|
|
108
|
+
const scope = overrides ? `,按群覆盖 ${overrides} 个` : '';
|
|
109
|
+
return `默认 ${DEFAULT_HOUR_LIMIT}/小时、${DEFAULT_DAY_LIMIT}/日${scope}`;
|
|
112
110
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.applyChatlunaIntegration = applyChatlunaIntegration;
|
|
4
|
+
const render_1 = require("./render");
|
|
4
5
|
function applyChatlunaIntegration(ctx, config) {
|
|
5
6
|
if (!config.injectVariables)
|
|
6
7
|
return;
|
|
@@ -9,8 +10,7 @@ function applyChatlunaIntegration(ctx, config) {
|
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
12
|
ctx.inject(['server', 'chatluna', 'emojiluna'], async (ctx) => {
|
|
12
|
-
const
|
|
13
|
-
const baseUrl = selfUrl + config.backendPath;
|
|
13
|
+
const baseUrl = (0, render_1.resolveSelfUrl)(ctx, config) + config.backendPath;
|
|
14
14
|
await ctx.emojiluna.ready;
|
|
15
15
|
const escapeMarkdown = (text) => text.replace(/([\[\]\(\)])/g, '\\$1');
|
|
16
16
|
const refreshPromptVariable = async () => {
|
package/lib/adapters/commands.js
CHANGED
|
@@ -25,7 +25,7 @@ function registerTriggerMiddleware(ctx, config) {
|
|
|
25
25
|
const content = session.content.trim();
|
|
26
26
|
if (content.length < TRIGGER_MATCH_MIN_LENGTH)
|
|
27
27
|
return next();
|
|
28
|
-
const emoji = await ctx.emojiluna.
|
|
28
|
+
const emoji = await ctx.emojiluna.getTriggerEmoji(content);
|
|
29
29
|
if (!emoji)
|
|
30
30
|
return next();
|
|
31
31
|
return next(async () => (0, render_1.emojiMessage)(ctx, config, emoji));
|
|
@@ -157,7 +157,7 @@ function registerLibraryCommands(ctx, config, emojiluna) {
|
|
|
157
157
|
`自动分析: ${flag(config.autoAnalyze)}`,
|
|
158
158
|
`图片类型过滤: ${flag(config.enableImageTypeFilter)}`,
|
|
159
159
|
`使用模型: ${config.model || '未配置'}`,
|
|
160
|
-
`AI分析统计: ${JSON.stringify(emojiluna.getAiTaskStats())}`,
|
|
160
|
+
`AI分析统计: ${JSON.stringify(await emojiluna.getAiTaskStats())}`,
|
|
161
161
|
'',
|
|
162
162
|
'自动获取:',
|
|
163
163
|
`状态: ${flag(config.autoCollect)}`,
|
|
@@ -4,8 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.applyConsoleListeners = applyConsoleListeners;
|
|
7
|
+
const types_1 = require("../types");
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const render_1 = require("./render");
|
|
9
11
|
const DIRECT_EVENTS = {
|
|
10
12
|
'emojiluna/getEmojiList': 'getEmojiList',
|
|
11
13
|
'emojiluna/getEmojiPage': 'getEmojiPage',
|
|
@@ -31,7 +33,7 @@ const DIRECT_EVENTS = {
|
|
|
31
33
|
'emojiluna/deleteAiTask': 'deleteAiTask',
|
|
32
34
|
'emojiluna/retryAiTask': 'retryAiTask',
|
|
33
35
|
};
|
|
34
|
-
const MAX_REQUESTED_EMOJI_NAME =
|
|
36
|
+
const MAX_REQUESTED_EMOJI_NAME = types_1.MAX_EMOJI_NAME_LENGTH;
|
|
35
37
|
function resolveClientRoot(fromDir) {
|
|
36
38
|
let current = fromDir;
|
|
37
39
|
for (let depth = 0; depth < 5; depth++) {
|
|
@@ -44,7 +46,7 @@ function resolveClientRoot(fromDir) {
|
|
|
44
46
|
break;
|
|
45
47
|
current = parent;
|
|
46
48
|
}
|
|
47
|
-
return path_1.default.resolve(fromDir, '..', '..'
|
|
49
|
+
return path_1.default.resolve(fromDir, '..', '..');
|
|
48
50
|
}
|
|
49
51
|
function applyConsoleListeners(ctx, config) {
|
|
50
52
|
ctx.inject(['console', 'emojiluna'], (ctx) => {
|
|
@@ -61,11 +63,19 @@ async function registerListeners(ctx, config) {
|
|
|
61
63
|
prod: path_1.default.join(clientRoot, 'dist'),
|
|
62
64
|
});
|
|
63
65
|
for (const [event, method] of Object.entries(DIRECT_EVENTS)) {
|
|
64
|
-
|
|
66
|
+
const handler = invoke[method];
|
|
67
|
+
if (typeof handler !== 'function') {
|
|
68
|
+
emojiluna.debug.log('service', `控制台事件目标方法缺失,已跳过注册: ${event} -> ${method}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
ctx.console.addListener(event,
|
|
72
|
+
// Koishi invokes listeners with its own `this`, so the service
|
|
73
|
+
// receiver has to be bound explicitly before handing the method over.
|
|
74
|
+
handler.bind(emojiluna));
|
|
65
75
|
}
|
|
66
76
|
ctx.console.addListener('emojiluna/getBaseUrl', async () => {
|
|
67
|
-
const selfUrl =
|
|
68
|
-
return selfUrl ? `${selfUrl
|
|
77
|
+
const selfUrl = (0, render_1.resolveSelfUrl)(ctx, config);
|
|
78
|
+
return selfUrl ? `${selfUrl}${config.backendPath}` : '';
|
|
69
79
|
});
|
|
70
80
|
ctx.console.addListener('emojiluna/getUploadToken', async () => {
|
|
71
81
|
return emojiluna.uploadToken;
|
package/lib/adapters/render.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Context, h } from 'koishi';
|
|
2
2
|
import { Config } from '../config';
|
|
3
3
|
import { EmojiItem } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* `ctx.get` is the optional lookup: reading `ctx.server` from a scope that did not
|
|
6
|
+
* declare the `server` inject makes Koishi log a service warning on every access.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveSelfUrl(ctx: Context, config: Config): string;
|
|
4
9
|
export declare function truncate(text: string, max?: number): string;
|
|
5
10
|
export declare function emojiUrl(ctx: Context, config: Config, emoji: EmojiItem): string | null;
|
|
6
11
|
export declare function emojiText(emoji: EmojiItem): string;
|
package/lib/adapters/render.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveSelfUrl = resolveSelfUrl;
|
|
3
4
|
exports.truncate = truncate;
|
|
4
5
|
exports.emojiUrl = emojiUrl;
|
|
5
6
|
exports.emojiText = emojiText;
|
|
6
7
|
exports.emojiMessage = emojiMessage;
|
|
7
8
|
const koishi_1 = require("koishi");
|
|
8
9
|
const DEFAULT_TRUNCATE = 200;
|
|
10
|
+
/**
|
|
11
|
+
* `ctx.get` is the optional lookup: reading `ctx.server` from a scope that did not
|
|
12
|
+
* declare the `server` inject makes Koishi log a service warning on every access.
|
|
13
|
+
*/
|
|
14
|
+
function resolveSelfUrl(ctx, config) {
|
|
15
|
+
const server = ctx.get('server');
|
|
16
|
+
return (config.selfUrl || server?.selfUrl || '').replace(/\/+$/, '');
|
|
17
|
+
}
|
|
9
18
|
function truncate(text, max = DEFAULT_TRUNCATE) {
|
|
10
19
|
if (!text)
|
|
11
20
|
return '';
|
|
@@ -14,10 +23,10 @@ function truncate(text, max = DEFAULT_TRUNCATE) {
|
|
|
14
23
|
function emojiUrl(ctx, config, emoji) {
|
|
15
24
|
if (!config.backendServer)
|
|
16
25
|
return null;
|
|
17
|
-
const selfUrl =
|
|
26
|
+
const selfUrl = resolveSelfUrl(ctx, config);
|
|
18
27
|
if (!selfUrl)
|
|
19
28
|
return null;
|
|
20
|
-
return `${selfUrl
|
|
29
|
+
return `${selfUrl}${config.backendPath}/get/${encodeURIComponent(emoji.id)}`;
|
|
21
30
|
}
|
|
22
31
|
function emojiText(emoji) {
|
|
23
32
|
const tags = emoji.tags.length ? ` #${emoji.tags.join(' #')}` : '';
|
package/lib/adapters/restApi.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.applyRestApi = applyRestApi;
|
|
7
|
+
const types_1 = require("../types");
|
|
7
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const utils_1 = require("../utils");
|
|
@@ -176,10 +177,11 @@ function applyRestApi(ctx, config) {
|
|
|
176
177
|
if (!file?.filepath) {
|
|
177
178
|
return json(koa, { success: false, message: 'No file uploaded' }, 400);
|
|
178
179
|
}
|
|
180
|
+
const requestedName = firstField(fields, 'name') ||
|
|
181
|
+
file.originalFilename?.replace(/\.[^/.]+$/, '') ||
|
|
182
|
+
'uploaded';
|
|
179
183
|
const emoji = await ctx.emojiluna.addEmoji({
|
|
180
|
-
name:
|
|
181
|
-
file.originalFilename?.replace(/\.[^/.]+$/, '') ||
|
|
182
|
-
'uploaded',
|
|
184
|
+
name: requestedName.slice(0, types_1.MAX_EMOJI_NAME_LENGTH),
|
|
183
185
|
category: firstField(fields, 'category'),
|
|
184
186
|
tags: parseTagsField(firstField(fields, 'tags')),
|
|
185
187
|
}, { path: file.filepath }, firstField(fields, 'aiAnalysis') === 'true');
|
|
@@ -33,6 +33,7 @@ export declare class AutoCollector {
|
|
|
33
33
|
private rejectedHashes;
|
|
34
34
|
private aiVerdicts;
|
|
35
35
|
private queue;
|
|
36
|
+
private commitTail;
|
|
36
37
|
private active;
|
|
37
38
|
private nameSeq;
|
|
38
39
|
private manualEnabled;
|
|
@@ -52,12 +53,15 @@ export declare class AutoCollector {
|
|
|
52
53
|
dispose(): void;
|
|
53
54
|
private pump;
|
|
54
55
|
private processJob;
|
|
56
|
+
private commit;
|
|
55
57
|
private save;
|
|
56
58
|
private evaluateScope;
|
|
57
59
|
private evaluateSize;
|
|
58
60
|
private evaluateResolution;
|
|
59
61
|
private evaluateKnownHash;
|
|
62
|
+
private findSimilar;
|
|
60
63
|
private evaluateSimilarity;
|
|
64
|
+
private serialize;
|
|
61
65
|
private evaluateFrequency;
|
|
62
66
|
private evaluateLimit;
|
|
63
67
|
private evaluateLibrary;
|
|
@@ -7,6 +7,8 @@ const image_1 = require("../image");
|
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
8
|
const HOUR_MS = 3_600_000;
|
|
9
9
|
const DAY_MS = 86_400_000;
|
|
10
|
+
const DEFAULT_HOUR_LIMIT = types_1.DEFAULT_COLLECT_LIMITS.hour;
|
|
11
|
+
const DEFAULT_DAY_LIMIT = types_1.DEFAULT_COLLECT_LIMITS.day;
|
|
10
12
|
const PENDING_REF_TTL_MS = 60_000;
|
|
11
13
|
const REJECTED_HASH_TTL_MS = 6 * HOUR_MS;
|
|
12
14
|
const AI_VERDICT_TTL_MS = 24 * HOUR_MS;
|
|
@@ -39,6 +41,7 @@ class AutoCollector {
|
|
|
39
41
|
rejectedHashes = new Map();
|
|
40
42
|
aiVerdicts = new Map();
|
|
41
43
|
queue = [];
|
|
44
|
+
commitTail = Promise.resolve();
|
|
42
45
|
active = 0;
|
|
43
46
|
nameSeq = 0;
|
|
44
47
|
manualEnabled = null;
|
|
@@ -64,12 +67,10 @@ class AutoCollector {
|
|
|
64
67
|
return !this.stopped && (this.manualEnabled ?? this.config.autoCollect);
|
|
65
68
|
}
|
|
66
69
|
start() {
|
|
67
|
-
if (!this.config.autoCollect) {
|
|
68
|
-
this.debug.log('collect', '自动获取未启用,跳过监听注册');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
70
|
this.ctx.on('message', (session) => this.observe(session));
|
|
72
|
-
this.ctx.logger.info(
|
|
71
|
+
this.ctx.logger.info(this.config.autoCollect
|
|
72
|
+
? 'Auto collector started'
|
|
73
|
+
: 'Auto collector idle: 自动获取未启用,可用 emojiluna.auto.toggle on 临时开启');
|
|
73
74
|
}
|
|
74
75
|
observe(session) {
|
|
75
76
|
if (!this.isEnabled)
|
|
@@ -237,7 +238,7 @@ class AutoCollector {
|
|
|
237
238
|
detail: `试运行:将保存为 "${this.buildName(session, isDirect)}",未实际写入`,
|
|
238
239
|
});
|
|
239
240
|
}
|
|
240
|
-
const saved = await this.
|
|
241
|
+
const saved = await this.serialize(() => this.commit(image.buffer, signature, hash, session, isDirect));
|
|
241
242
|
if (!pass(saved))
|
|
242
243
|
return;
|
|
243
244
|
this.consumeQuota(chatKey);
|
|
@@ -252,6 +253,22 @@ class AutoCollector {
|
|
|
252
253
|
this.pendingRefs.delete(ref);
|
|
253
254
|
}
|
|
254
255
|
}
|
|
256
|
+
async commit(buffer, signature, hash, session, isDirect) {
|
|
257
|
+
if (this.luna.uploadManager.hasHash(hash)) {
|
|
258
|
+
return { gate: '去重', ok: false, detail: '入库前已出现相同内容' };
|
|
259
|
+
}
|
|
260
|
+
if (signature) {
|
|
261
|
+
const similar = this.findSimilar(signature);
|
|
262
|
+
if (similar) {
|
|
263
|
+
return {
|
|
264
|
+
gate: '相似',
|
|
265
|
+
ok: false,
|
|
266
|
+
detail: `入库前与 ${similar.hash.slice(0, 8)} 相似度 ${similar.score.toFixed(3)}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return this.save(buffer, signature, hash, session, isDirect);
|
|
271
|
+
}
|
|
255
272
|
async save(buffer, signature, hash, session, isDirect) {
|
|
256
273
|
const name = this.buildName(session, isDirect);
|
|
257
274
|
const tags = this.buildTags(session, isDirect);
|
|
@@ -366,7 +383,7 @@ class AutoCollector {
|
|
|
366
383
|
}
|
|
367
384
|
return { gate: '去重', ok: true, detail: `内容哈希 ${hash.slice(0, 8)}` };
|
|
368
385
|
}
|
|
369
|
-
|
|
386
|
+
findSimilar(signature) {
|
|
370
387
|
let best = null;
|
|
371
388
|
for (const [hash, cached] of this.signatures) {
|
|
372
389
|
const score = similarityScore(signature, cached);
|
|
@@ -374,6 +391,10 @@ class AutoCollector {
|
|
|
374
391
|
best = { hash, score };
|
|
375
392
|
}
|
|
376
393
|
}
|
|
394
|
+
return best;
|
|
395
|
+
}
|
|
396
|
+
evaluateSimilarity(signature) {
|
|
397
|
+
const best = this.findSimilar(signature);
|
|
377
398
|
if (!best) {
|
|
378
399
|
return {
|
|
379
400
|
gate: '相似',
|
|
@@ -387,6 +408,11 @@ class AutoCollector {
|
|
|
387
408
|
detail: `与表情 ${best.hash.slice(0, 8)} 相似度 ${best.score.toFixed(3)},阈值 ${this.config.similarityThreshold}`,
|
|
388
409
|
};
|
|
389
410
|
}
|
|
411
|
+
serialize(task) {
|
|
412
|
+
const run = this.commitTail.then(task, task);
|
|
413
|
+
this.commitTail = run.then(() => undefined, () => undefined);
|
|
414
|
+
return run;
|
|
415
|
+
}
|
|
390
416
|
evaluateFrequency(hash, chatKey) {
|
|
391
417
|
const windowMs = this.config.frequencyWindow * 60_000;
|
|
392
418
|
const key = `${hash}|${chatKey}`;
|
|
@@ -411,8 +437,8 @@ class AutoCollector {
|
|
|
411
437
|
evaluateLimit(chatKey) {
|
|
412
438
|
const counter = this.windowCounter(chatKey);
|
|
413
439
|
const quota = this.config.groupAutoCollectLimit?.[chatKey.split(':')[1]];
|
|
414
|
-
const hourLimit = quota?.hourLimit ??
|
|
415
|
-
const dayLimit = quota?.dayLimit ??
|
|
440
|
+
const hourLimit = quota?.hourLimit ?? DEFAULT_HOUR_LIMIT;
|
|
441
|
+
const dayLimit = quota?.dayLimit ?? DEFAULT_DAY_LIMIT;
|
|
416
442
|
if (hourLimit > 0 && counter.hourCount >= hourLimit) {
|
|
417
443
|
return {
|
|
418
444
|
gate: '限流',
|
|
@@ -594,6 +620,13 @@ class AutoCollector {
|
|
|
594
620
|
else
|
|
595
621
|
this.frequency.delete(key);
|
|
596
622
|
}
|
|
623
|
+
for (const [key, counter] of this.counters) {
|
|
624
|
+
if (!counter.hourCount &&
|
|
625
|
+
!counter.dayCount &&
|
|
626
|
+
now - counter.dayStart >= DAY_MS) {
|
|
627
|
+
this.counters.delete(key);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
597
630
|
}
|
|
598
631
|
chatKey(session, isDirect) {
|
|
599
632
|
return isDirect
|
|
@@ -48,6 +48,12 @@ export declare class EmojiLunaService extends Service {
|
|
|
48
48
|
buffer: Buffer;
|
|
49
49
|
}[], aiAnalysis: boolean): Promise<EmojiItem[]>;
|
|
50
50
|
getEmojiByName(name: string): Promise<EmojiItem | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Trigger-word replies should only fire on an exact emoji name or tag; the
|
|
53
|
+
* four-level lookup used by `emojiluna.get` also matches categories and ids,
|
|
54
|
+
* which would make ordinary messages steal a sticker.
|
|
55
|
+
*/
|
|
56
|
+
getTriggerEmoji(text: string): Promise<EmojiItem | null>;
|
|
51
57
|
getEmojisByName(name: string): Promise<EmojiItem[]>;
|
|
52
58
|
getEmojiById(id: string): Promise<EmojiItem | null>;
|
|
53
59
|
private filterEmojis;
|
|
@@ -13,6 +13,7 @@ const debug_1 = require("../debug");
|
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
15
15
|
const crypto_1 = require("crypto");
|
|
16
|
+
const MAX_IMPORT_ERRORS = 20;
|
|
16
17
|
class EmojiLunaService extends koishi_1.Service {
|
|
17
18
|
config;
|
|
18
19
|
_emojiStorage = {};
|
|
@@ -304,6 +305,18 @@ class EmojiLunaService extends koishi_1.Service {
|
|
|
304
305
|
const byCategory = this._byCategory.get(name)?.values().next().value;
|
|
305
306
|
return byCategory ? this._emojiStorage[byCategory] ?? null : null;
|
|
306
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Trigger-word replies should only fire on an exact emoji name or tag; the
|
|
310
|
+
* four-level lookup used by `emojiluna.get` also matches categories and ids,
|
|
311
|
+
* which would make ordinary messages steal a sticker.
|
|
312
|
+
*/
|
|
313
|
+
async getTriggerEmoji(text) {
|
|
314
|
+
const byName = this._byName.get(text);
|
|
315
|
+
if (byName)
|
|
316
|
+
return this._emojiStorage[byName] ?? null;
|
|
317
|
+
const byTag = this._byTag.get(text)?.values().next().value;
|
|
318
|
+
return byTag ? this._emojiStorage[byTag] ?? null : null;
|
|
319
|
+
}
|
|
307
320
|
async getEmojisByName(name) {
|
|
308
321
|
const ids = new Set([
|
|
309
322
|
...(this._byName.get(name) ? [this._byName.get(name)] : []),
|
|
@@ -384,9 +397,15 @@ class EmojiLunaService extends koishi_1.Service {
|
|
|
384
397
|
try {
|
|
385
398
|
const emojis = Object.values(this._emojiStorage);
|
|
386
399
|
const concurrency = 4;
|
|
400
|
+
let failures = 0;
|
|
387
401
|
for (let i = 0; i < emojis.length; i += concurrency) {
|
|
388
402
|
const batch = emojis.slice(i, i + concurrency);
|
|
389
|
-
await Promise.all(batch.map((emoji) => this.deleteEmoji(emoji.id)));
|
|
403
|
+
const results = await Promise.all(batch.map((emoji) => this.deleteEmoji(emoji.id)));
|
|
404
|
+
failures += results.filter((deleted) => !deleted).length;
|
|
405
|
+
}
|
|
406
|
+
if (failures) {
|
|
407
|
+
this.ctx.logger.warn(`清空表情包时有 ${failures} 个未能删除`);
|
|
408
|
+
return false;
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
411
|
catch (error) {
|
|
@@ -617,7 +636,7 @@ class EmojiLunaService extends koishi_1.Service {
|
|
|
617
636
|
return removedCount;
|
|
618
637
|
}
|
|
619
638
|
async categorizeExistingEmojis() {
|
|
620
|
-
if (!this._aiAnalyzer.
|
|
639
|
+
if (!this._aiAnalyzer.hasModel || !this.config.autoCategorize) {
|
|
621
640
|
return { success: 0, failed: 0 };
|
|
622
641
|
}
|
|
623
642
|
let success = 0, failed = 0;
|
|
@@ -776,7 +795,9 @@ class EmojiLunaService extends koishi_1.Service {
|
|
|
776
795
|
}
|
|
777
796
|
catch (error) {
|
|
778
797
|
result.failed++;
|
|
779
|
-
result.errors.
|
|
798
|
+
if (result.errors.length < MAX_IMPORT_ERRORS) {
|
|
799
|
+
result.errors.push(`导入 ${file.name} 失败: ${error.message}`);
|
|
800
|
+
}
|
|
780
801
|
this.ctx.logger.error(`导入失败 ${file.path}:`, error);
|
|
781
802
|
}
|
|
782
803
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -111,6 +111,11 @@ export declare const GROUP_SCOPES: {
|
|
|
111
111
|
readonly whitelist: "仅白名单群";
|
|
112
112
|
readonly blacklist: "排除黑名单群";
|
|
113
113
|
};
|
|
114
|
+
export declare const DEFAULT_COLLECT_LIMITS: {
|
|
115
|
+
readonly hour: 20;
|
|
116
|
+
readonly day: 100;
|
|
117
|
+
};
|
|
118
|
+
export declare const MAX_EMOJI_NAME_LENGTH = 64;
|
|
114
119
|
export declare const COLLECT_MODES: {
|
|
115
120
|
readonly instant: "立即收集";
|
|
116
121
|
readonly frequency: "频次达标后收集";
|
package/lib/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.IMAGE_TYPE_LABELS = exports.DEBUG_SCOPES = exports.AI_FILTER_FALLBACKS = exports.COLLECT_MODES = exports.GROUP_SCOPES = exports.DEFAULT_ACCEPTED_IMAGE_TYPES = exports.IMAGE_CONTENT_TYPES = void 0;
|
|
3
|
+
exports.IMAGE_TYPE_LABELS = exports.DEBUG_SCOPES = exports.AI_FILTER_FALLBACKS = exports.COLLECT_MODES = exports.MAX_EMOJI_NAME_LENGTH = exports.DEFAULT_COLLECT_LIMITS = exports.GROUP_SCOPES = exports.DEFAULT_ACCEPTED_IMAGE_TYPES = exports.IMAGE_CONTENT_TYPES = void 0;
|
|
4
4
|
exports.IMAGE_CONTENT_TYPES = [
|
|
5
5
|
{
|
|
6
6
|
type: 'emoji',
|
|
@@ -72,6 +72,11 @@ exports.GROUP_SCOPES = {
|
|
|
72
72
|
whitelist: '仅白名单群',
|
|
73
73
|
blacklist: '排除黑名单群',
|
|
74
74
|
};
|
|
75
|
+
exports.DEFAULT_COLLECT_LIMITS = {
|
|
76
|
+
hour: 20,
|
|
77
|
+
day: 100,
|
|
78
|
+
};
|
|
79
|
+
exports.MAX_EMOJI_NAME_LENGTH = 64;
|
|
75
80
|
exports.COLLECT_MODES = {
|
|
76
81
|
instant: '立即收集',
|
|
77
82
|
frequency: '频次达标后收集',
|
package/lib/utils.js
CHANGED
|
@@ -119,10 +119,14 @@ function describeImageRef(ref) {
|
|
|
119
119
|
return ref.slice(0, 64);
|
|
120
120
|
}
|
|
121
121
|
async function fetchHttpBuffer(ctx, url, timeout) {
|
|
122
|
-
const
|
|
122
|
+
const http = ctx.get('http');
|
|
123
|
+
if (!http?.get) {
|
|
124
|
+
throw new Error('未加载 http 服务,无法拉取远程图片');
|
|
125
|
+
}
|
|
126
|
+
const data = await http.get(url, {
|
|
123
127
|
responseType: 'arraybuffer',
|
|
124
128
|
timeout,
|
|
125
|
-
headers: { 'User-Agent': USER_AGENT }
|
|
129
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
126
130
|
});
|
|
127
131
|
return Buffer.from(data);
|
|
128
132
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
## 📌 重要公告
|
|
4
4
|
请务必阅读:
|
|
5
5
|
- 本仓库是 [ChatLunaLab/emojiluna](https://github.com/ChatLunaLab/emojiluna) 的 fork,fork 基线为 `1.3.3`(commit `0a1f33d`),包名改为 `koishi-plugin-emojiluna-plus`,版本号从 `0.0.1` 重新计。
|
|
6
|
-
- `koishi-plugin-emojiluna` 与 `koishi-plugin-emojiluna-plus` **只能启用其中一个**:两者提供同名 Koishi 服务 `emojiluna` 与控制台事件 `emojiluna
|
|
6
|
+
- `koishi-plugin-emojiluna` 与 `koishi-plugin-emojiluna-plus` **只能启用其中一个**:两者提供同名 Koishi 服务 `emojiluna` 与控制台事件 `emojiluna/*`,同时启用会互相覆盖。包名本身不冲突。
|
|
7
7
|
|
|
8
8
|
## 项目介绍 (Project Introduction)
|
|
9
9
|
|
|
10
10
|
### 中文
|
|
11
|
-
这是一个为 Koishi
|
|
11
|
+
这是一个为 Koishi 机器人框架开发的**智能表情包管理插件**:自动从群聊与私聊收集表情包,交给 ChatLuna 视觉模型完成分类、命名与打标,并提供分类标签管理、检索、文件夹批量导入、WebUI 管理界面与 HTTP 接口。相对上游,本 fork 重写了自动获取链路。
|
|
12
12
|
|
|
13
13
|
### English
|
|
14
|
-
This is an **intelligent meme management plugin** developed for the Koishi bot framework. It automatically collects memes from group chats and private messages, utilizes ChatLuna
|
|
14
|
+
This is an **intelligent meme management plugin** developed for the Koishi bot framework. It automatically collects memes from group chats and private messages, utilizes the ChatLuna vision model for classification, naming, and tagging, and offers features such as tag management, search capabilities, batch folder imports, a WebUI management interface, and an HTTP API. Compared to the upstream version, this fork features a rewritten automatic acquisition pipeline.
|
|
15
15
|
|
|
16
16
|
## 项目仓库 (Repository)
|
|
17
17
|
- GitHub: `https://github.com/Minecraft-1314/koishi-plugin-emojiluna-plus.git`
|
|
@@ -43,7 +43,7 @@ This is an **intelligent meme management plugin** developed for the Koishi bot f
|
|
|
43
43
|
|
|
44
44
|
1. **生效范围**:群聊看 `collectGroup` 与 `groupScope`,私聊看 `collectDirect`。
|
|
45
45
|
2. **取图**:依次尝试元素的 `url`、`src`、`file`、`path` 属性。
|
|
46
|
-
3.
|
|
46
|
+
3. **格式**:按文件魔数校验,非图片直接拒绝。
|
|
47
47
|
4. **大小与分辨率**:`minEmojiSize`、`maxEmojiSize`、`minEdgePixels`。
|
|
48
48
|
5. **内容去重**:sha256 命中内存缓存或数据库即拦截。
|
|
49
49
|
6. **频次**:`autoCollectMode` 为“频次达标后收集”时要求窗口内达到 `emojiFrequencyThreshold` 次。
|
|
@@ -52,6 +52,8 @@ This is an **intelligent meme management plugin** developed for the Koishi bot f
|
|
|
52
52
|
9. **库存与限流**:`maxEmojiCount`、按会话的 `groupAutoCollectLimit`、全局 `globalDayLimit`。
|
|
53
53
|
10. **入库**:写入存储目录,打上 `自动获取` 与 `群:<群号>` 或 `私聊` 标签,并按 `autoAnalyze` 排队 AI 命名打标。
|
|
54
54
|
|
|
55
|
+
支持的图片来源:`http(s)://`、本地绝对路径(`C:\…\x.jpg`、`/data/x.png`)、`file://`、`data:` URI、`base64://` 内联;元素缺少这四个属性时按取图失败拦截。
|
|
56
|
+
|
|
55
57
|
## 配置项说明 (Configuration)
|
|
56
58
|
|
|
57
59
|
### 基础设置 (Basic Settings)
|
|
@@ -119,17 +121,17 @@ This is an **intelligent meme management plugin** developed for the Koishi bot f
|
|
|
119
121
|
| `globalDayLimit` | number | 1000 | 全局每天自动获取上限,0 表示不限制 |
|
|
120
122
|
| `groupAutoCollectLimit` | dict | `{}` | 按群覆盖每小时与每日上限,默认 20 与 100 |
|
|
121
123
|
| `autoCollectCategory` | string | 其他 | 自动获取入库时的初始分类 |
|
|
122
|
-
| `autoCollectNameTemplate` | string | `自动获取_{date}_{index}` |
|
|
124
|
+
| `autoCollectNameTemplate` | string | `自动获取_{date}_{index}` | 自动获取入库名称模板,变量见下 |
|
|
123
125
|
| `enableImageTypeFilter` | boolean | true | 启用 AI 图片类型过滤 |
|
|
124
126
|
| `acceptedImageTypes` | string[] | 表情包、贴纸、梗图、漫画、动漫图片、宠物图片、艺术作品 | 允许自动获取入库的图片类型 |
|
|
125
127
|
| `aiFilterTimeout` | number | 20000 | AI 图片类型过滤超时,单位毫秒 |
|
|
126
|
-
| `aiFilterFallback` | string | 接受 |
|
|
128
|
+
| `aiFilterFallback` | string | 接受 | 兜底策略,取值:接受、拒绝 |
|
|
127
129
|
|
|
128
130
|
### 调试配置 (Debug Settings)
|
|
129
131
|
| 配置项 (Config) | 类型 (Type) | 默认值 (Default) | 说明 (Description) |
|
|
130
132
|
|----------------|-------------|-------------------|---------------------|
|
|
131
133
|
| `debug` | boolean | false | 输出调试日志 |
|
|
132
|
-
| `debugOnly` | string | 全部 |
|
|
134
|
+
| `debugOnly` | string | 全部 | 取值:全部、自动获取、AI调用、HTTP接口、服务内部 |
|
|
133
135
|
| `logLimit` | number | 200 | 调试日志环形缓冲条数 |
|
|
134
136
|
|
|
135
137
|
## 名称模板变量 (Name Template Variables)
|
|
@@ -144,23 +146,12 @@ This is an **intelligent meme management plugin** developed for the Koishi bot f
|
|
|
144
146
|
| `{chat}` | 群号,私聊时为 direct |
|
|
145
147
|
| `{index}` | 递增序号 |
|
|
146
148
|
|
|
147
|
-
## 支持的图片来源 (Supported Image Sources)
|
|
148
|
-
|
|
149
|
-
| 来源 (Source) | 示例 (Example) | 处理方式 (Handling) |
|
|
150
|
-
|---------------|----------------|---------------------|
|
|
151
|
-
| HTTP 与 HTTPS | `https://gchat.qpic.cn/…` | 走 `ctx.http` 拉取,带 UA 与超时 |
|
|
152
|
-
| 本地绝对路径 | `C:\Users\…\Pic\xx.jpg`、`/data/x.png` | 直接读盘,受 `allowLocalPaths` 与 `localPathRoots` 约束 |
|
|
153
|
-
| file URI | `file:///C:/…/x.png` | 解析为本地路径后读取 |
|
|
154
|
-
| data URI | `data:image/png;base64,…` | 就地解码 |
|
|
155
|
-
| Koishi 内联 | `base64://…` | 就地解码 |
|
|
156
|
-
| 缺失属性 | 元素没有 `url`、`src`、`file`、`path` | 拦截并记为取图失败 |
|
|
157
|
-
|
|
158
149
|
## 项目贡献者 (Contributors)
|
|
159
150
|
|
|
160
151
|
| 贡献者 (Contributor) | 贡献内容 (Contribution) |
|
|
161
152
|
|----------------------|-------------------------|
|
|
162
153
|
| ChatLunaLab | 上游 emojiluna 全部原始功能 (Original upstream work) |
|
|
163
|
-
| Minecraft-1314 | fork 改造 |
|
|
154
|
+
| Minecraft-1314 | fork 改造 (Fork) |
|
|
164
155
|
|
|
165
156
|
(欢迎通过 Issues 或 PR 加入贡献者列表)
|
|
166
157
|
|