@zaofan/dsh-qqbot 0.6.0
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 +21 -0
- package/README.md +308 -0
- package/README_EN.md +273 -0
- package/client/qqbot-settings.js +1463 -0
- package/cordis.patch.yml +36 -0
- package/dist/api/group-admin.d.ts +112 -0
- package/dist/api/group-admin.d.ts.map +1 -0
- package/dist/api/group-admin.js +179 -0
- package/dist/api/group-admin.js.map +1 -0
- package/dist/channel-tools.d.ts +14 -0
- package/dist/channel-tools.d.ts.map +1 -0
- package/dist/channel-tools.js +880 -0
- package/dist/channel-tools.js.map +1 -0
- package/dist/commands/help.d.ts +11 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +21 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/index.d.ts +13 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +29 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/misc.d.ts +14 -0
- package/dist/commands/misc.d.ts.map +1 -0
- package/dist/commands/misc.js +39 -0
- package/dist/commands/misc.js.map +1 -0
- package/dist/commands/model.d.ts +7 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +58 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/session.d.ts +10 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +26 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +24 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/config.d.ts +205 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +203 -0
- package/dist/config.js.map +1 -0
- package/dist/features/chat-ledger.d.ts +53 -0
- package/dist/features/chat-ledger.d.ts.map +1 -0
- package/dist/features/chat-ledger.js +185 -0
- package/dist/features/chat-ledger.js.map +1 -0
- package/dist/features/face-tags.d.ts +17 -0
- package/dist/features/face-tags.d.ts.map +1 -0
- package/dist/features/face-tags.js +79 -0
- package/dist/features/face-tags.js.map +1 -0
- package/dist/features/group-join-request.d.ts +65 -0
- package/dist/features/group-join-request.d.ts.map +1 -0
- package/dist/features/group-join-request.js +190 -0
- package/dist/features/group-join-request.js.map +1 -0
- package/dist/features/history-store.d.ts +8 -0
- package/dist/features/history-store.d.ts.map +1 -0
- package/dist/features/history-store.js +24 -0
- package/dist/features/history-store.js.map +1 -0
- package/dist/features/qq-approval.d.ts +78 -0
- package/dist/features/qq-approval.d.ts.map +1 -0
- package/dist/features/qq-approval.js +165 -0
- package/dist/features/qq-approval.js.map +1 -0
- package/dist/features/schedule-store.d.ts +62 -0
- package/dist/features/schedule-store.d.ts.map +1 -0
- package/dist/features/schedule-store.js +253 -0
- package/dist/features/schedule-store.js.map +1 -0
- package/dist/features/scheduler.d.ts +9 -0
- package/dist/features/scheduler.d.ts.map +1 -0
- package/dist/features/scheduler.js +146 -0
- package/dist/features/scheduler.js.map +1 -0
- package/dist/features/sticker-gate.d.ts +90 -0
- package/dist/features/sticker-gate.d.ts.map +1 -0
- package/dist/features/sticker-gate.js +391 -0
- package/dist/features/sticker-gate.js.map +1 -0
- package/dist/features/sticker-store.d.ts +158 -0
- package/dist/features/sticker-store.d.ts.map +1 -0
- package/dist/features/sticker-store.js +676 -0
- package/dist/features/sticker-store.js.map +1 -0
- package/dist/features/sticker-tagger.d.ts +10 -0
- package/dist/features/sticker-tagger.d.ts.map +1 -0
- package/dist/features/sticker-tagger.js +139 -0
- package/dist/features/sticker-tagger.js.map +1 -0
- package/dist/gateway/bootstrap.d.ts +12 -0
- package/dist/gateway/bootstrap.d.ts.map +1 -0
- package/dist/gateway/bootstrap.js +346 -0
- package/dist/gateway/bootstrap.js.map +1 -0
- package/dist/gateway/debounce.d.ts +40 -0
- package/dist/gateway/debounce.d.ts.map +1 -0
- package/dist/gateway/debounce.js +262 -0
- package/dist/gateway/debounce.js.map +1 -0
- package/dist/gateway/index.d.ts +3 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/middleware-setup.d.ts +12 -0
- package/dist/gateway/middleware-setup.d.ts.map +1 -0
- package/dist/gateway/middleware-setup.js +156 -0
- package/dist/gateway/middleware-setup.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/attachment.d.ts +11 -0
- package/dist/middleware/attachment.d.ts.map +1 -0
- package/dist/middleware/attachment.js +17 -0
- package/dist/middleware/attachment.js.map +1 -0
- package/dist/middleware/media-history.d.ts +30 -0
- package/dist/middleware/media-history.d.ts.map +1 -0
- package/dist/middleware/media-history.js +54 -0
- package/dist/middleware/media-history.js.map +1 -0
- package/dist/middleware/sticker-capture.d.ts +17 -0
- package/dist/middleware/sticker-capture.d.ts.map +1 -0
- package/dist/middleware/sticker-capture.js +97 -0
- package/dist/middleware/sticker-capture.js.map +1 -0
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +9 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/model-resolver.d.ts +88 -0
- package/dist/model/model-resolver.d.ts.map +1 -0
- package/dist/model/model-resolver.js +151 -0
- package/dist/model/model-resolver.js.map +1 -0
- package/dist/model/prefs-store.d.ts +24 -0
- package/dist/model/prefs-store.d.ts.map +1 -0
- package/dist/model/prefs-store.js +126 -0
- package/dist/model/prefs-store.js.map +1 -0
- package/dist/model/settings-reader.d.ts +19 -0
- package/dist/model/settings-reader.d.ts.map +1 -0
- package/dist/model/settings-reader.js +76 -0
- package/dist/model/settings-reader.js.map +1 -0
- package/dist/model/types.d.ts +15 -0
- package/dist/model/types.d.ts.map +1 -0
- package/dist/model/types.js +5 -0
- package/dist/model/types.js.map +1 -0
- package/dist/session/idle-evictor.d.ts +18 -0
- package/dist/session/idle-evictor.d.ts.map +1 -0
- package/dist/session/idle-evictor.js +31 -0
- package/dist/session/idle-evictor.js.map +1 -0
- package/dist/session/index.d.ts +9 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/session-manager.d.ts +97 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +624 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/session/types.d.ts +130 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/session/workspace-attach.d.ts +22 -0
- package/dist/session/workspace-attach.d.ts.map +1 -0
- package/dist/session/workspace-attach.js +21 -0
- package/dist/session/workspace-attach.js.map +1 -0
- package/dist/setup.d.ts +25 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +198 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared/index.d.ts +7 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/scope.d.ts +14 -0
- package/dist/shared/scope.d.ts.map +1 -0
- package/dist/shared/scope.js +12 -0
- package/dist/shared/scope.js.map +1 -0
- package/dist/shared/send-helper.d.ts +11 -0
- package/dist/shared/send-helper.d.ts.map +1 -0
- package/dist/shared/send-helper.js +13 -0
- package/dist/shared/send-helper.js.map +1 -0
- package/dist/shared/utils.d.ts +28 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +143 -0
- package/dist/shared/utils.js.map +1 -0
- package/dist/transport/attachment.d.ts +18 -0
- package/dist/transport/attachment.d.ts.map +1 -0
- package/dist/transport/attachment.js +106 -0
- package/dist/transport/attachment.js.map +1 -0
- package/dist/transport/chunker.d.ts +14 -0
- package/dist/transport/chunker.d.ts.map +1 -0
- package/dist/transport/chunker.js +78 -0
- package/dist/transport/chunker.js.map +1 -0
- package/dist/transport/events.d.ts +80 -0
- package/dist/transport/events.d.ts.map +1 -0
- package/dist/transport/events.js +62 -0
- package/dist/transport/events.js.map +1 -0
- package/dist/transport/inbound.d.ts +8 -0
- package/dist/transport/inbound.d.ts.map +1 -0
- package/dist/transport/inbound.js +292 -0
- package/dist/transport/inbound.js.map +1 -0
- package/dist/transport/index.d.ts +10 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +10 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/inject-rules.d.ts +24 -0
- package/dist/transport/inject-rules.d.ts.map +1 -0
- package/dist/transport/inject-rules.js +72 -0
- package/dist/transport/inject-rules.js.map +1 -0
- package/dist/transport/outbound-buffer.d.ts +56 -0
- package/dist/transport/outbound-buffer.d.ts.map +1 -0
- package/dist/transport/outbound-buffer.js +131 -0
- package/dist/transport/outbound-buffer.js.map +1 -0
- package/dist/transport/outbound.d.ts +30 -0
- package/dist/transport/outbound.d.ts.map +1 -0
- package/dist/transport/outbound.js +139 -0
- package/dist/transport/outbound.js.map +1 -0
- package/dist/transport/rich-media.d.ts +41 -0
- package/dist/transport/rich-media.d.ts.map +1 -0
- package/dist/transport/rich-media.js +104 -0
- package/dist/transport/rich-media.js.map +1 -0
- package/dist/transport/streaming-writer.d.ts +47 -0
- package/dist/transport/streaming-writer.d.ts.map +1 -0
- package/dist/transport/streaming-writer.js +110 -0
- package/dist/transport/streaming-writer.js.map +1 -0
- package/dist/transport/tool-presenter.d.ts +38 -0
- package/dist/transport/tool-presenter.d.ts.map +1 -0
- package/dist/transport/tool-presenter.js +121 -0
- package/dist/transport/tool-presenter.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +106 -0
- package/settings-host.js +927 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sticker-store.ts — 表情包图库存储服务(P0 底座)
|
|
3
|
+
*
|
|
4
|
+
* 职责:把 QQ 群里的图片消息自动收藏为本地表情包库。
|
|
5
|
+
* - 安全下载(仅 HTTPS + SSRF 防护 + 大小上限 + 超时)
|
|
6
|
+
* - SHA-1 指纹去重(同图只存一份)
|
|
7
|
+
* - 分层目录 lib/candidate | library | negative + index.json 元数据
|
|
8
|
+
* - search/list 供 list_stickers 工具查询
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ 本地手改功能(fork 新增,SDK 无此能力):维护清单见工作区根《插件改动维护注意事项.md》。
|
|
11
|
+
* 运行时数据根默认 = {agent cwd}/表情包(config.sticker.dataDir 可覆盖)。
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, rmSync, statSync } from 'node:fs';
|
|
15
|
+
import { join, extname, resolve } from 'node:path';
|
|
16
|
+
import * as dns from 'node:dns';
|
|
17
|
+
/** 搜索词拆分: 空格/中英文逗号/顿号/分号分隔, 去空去重 */
|
|
18
|
+
function splitTerms(raw) {
|
|
19
|
+
return Array.from(new Set((raw ?? '').trim().toLowerCase().split(/[\s,,、;;]+/).filter(Boolean)));
|
|
20
|
+
}
|
|
21
|
+
/** 一条记录可搜索的拼接 hay(标签+描述+来源+发言者+原消息) */
|
|
22
|
+
function stickerHay(it) {
|
|
23
|
+
return [it.tags.join(' '), it.desc ?? '', it.sourceGroup ?? '', it.senderName ?? '', it.msgText ?? ''].join(' ').toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
/** 命中词数(0=无命中); 多词任一命中即可(OR), 命中越多越靠前 */
|
|
26
|
+
function hitCount(terms, hay) {
|
|
27
|
+
let n = 0;
|
|
28
|
+
for (const t of terms)
|
|
29
|
+
if (hay.includes(t))
|
|
30
|
+
n += 1;
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
/** 图片大小上限(超过不收) */
|
|
34
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
35
|
+
/** 下载超时 ms */
|
|
36
|
+
const DOWNLOAD_TIMEOUT_MS = 15_000;
|
|
37
|
+
/** 目录层 */
|
|
38
|
+
const LAYER_DIRS = {
|
|
39
|
+
candidate: 'lib/candidate',
|
|
40
|
+
library: 'lib/library',
|
|
41
|
+
negative: 'lib/negative',
|
|
42
|
+
trash: 'lib/trash',
|
|
43
|
+
};
|
|
44
|
+
/** content_type → 扩展名 */
|
|
45
|
+
const MIME_EXT = {
|
|
46
|
+
'image/jpeg': 'jpg',
|
|
47
|
+
'image/jpg': 'jpg',
|
|
48
|
+
'image/png': 'png',
|
|
49
|
+
'image/gif': 'gif',
|
|
50
|
+
'image/webp': 'webp',
|
|
51
|
+
'image/bmp': 'bmp',
|
|
52
|
+
};
|
|
53
|
+
// ── SSRF 防护(阻断指向私有/保留地址的下载) ──
|
|
54
|
+
const PRIVATE_RANGES = [
|
|
55
|
+
[0x0a000000n, 8], // 10.0.0.0/8
|
|
56
|
+
[0xac100000n, 12], // 172.16.0.0/12
|
|
57
|
+
[0xc0a80000n, 16], // 192.168.0.0/16
|
|
58
|
+
[0x7f000000n, 8], // 127.0.0.0/8
|
|
59
|
+
[0xa9fe0000n, 16], // 169.254.0.0/16
|
|
60
|
+
[0xe0000000n, 4], // 224.0.0.0/4
|
|
61
|
+
];
|
|
62
|
+
function ipToBigInt(ip) {
|
|
63
|
+
return ip.split('.').reduce((acc, octet) => (acc << 8n) | BigInt(Number(octet)), 0n);
|
|
64
|
+
}
|
|
65
|
+
function isPrivateIP(ip) {
|
|
66
|
+
// IPv4 映射 (::ffff:a.b.c.d) 剥前缀按 IPv4 判
|
|
67
|
+
if (ip.startsWith('::ffff:')) {
|
|
68
|
+
const v4 = ip.slice(7);
|
|
69
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(v4)) {
|
|
70
|
+
const val = ipToBigInt(v4);
|
|
71
|
+
return PRIVATE_RANGES.some(([mask, prefix]) => (val >> (32n - BigInt(prefix))) === (mask >> (32n - BigInt(prefix))));
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// IPv6 私有/本地/文档段:::1、fe8/fe9/fea/feb(link-local)、fc/fd(ULA)、2001:db8(文档)
|
|
76
|
+
if (/^::1$/.test(ip))
|
|
77
|
+
return true;
|
|
78
|
+
if (/^fe[89ab]c/i.test(ip))
|
|
79
|
+
return true;
|
|
80
|
+
if (/^f[cd]/i.test(ip))
|
|
81
|
+
return true;
|
|
82
|
+
if (/^2001:db8/i.test(ip))
|
|
83
|
+
return true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
async function assertSafeHostname(hostname) {
|
|
87
|
+
// v4+v6 全查(QQ 多媒体 CDN 域名常只有 AAAA 记录)
|
|
88
|
+
const addrs = await dns.promises.lookup(hostname, { all: true }).catch(() => []);
|
|
89
|
+
if (addrs.length === 0)
|
|
90
|
+
throw new Error(`DNS resolution failed: ${hostname}`);
|
|
91
|
+
for (const { address } of addrs) {
|
|
92
|
+
if (isPrivateIP(address))
|
|
93
|
+
throw new Error(`SSRF blocked: ${hostname} resolves to private IP ${address}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** 魔数校验:仅接受 JPEG/PNG/GIF/WEBP/BMP(content_type 不可靠时的兜底,非图不入库) */
|
|
97
|
+
function isImageBuffer(buf) {
|
|
98
|
+
if (buf.length < 12)
|
|
99
|
+
return false;
|
|
100
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
101
|
+
return true; // JPEG
|
|
102
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
103
|
+
return true; // PNG
|
|
104
|
+
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x38)
|
|
105
|
+
return true; // GIF
|
|
106
|
+
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
|
|
107
|
+
&& buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50)
|
|
108
|
+
return true; // WEBP
|
|
109
|
+
if (buf[0] === 0x42 && buf[1] === 0x4d)
|
|
110
|
+
return true; // BMP
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
export class StickerStore {
|
|
114
|
+
logger;
|
|
115
|
+
items = new Map();
|
|
116
|
+
saveTimer = null;
|
|
117
|
+
dirty = false;
|
|
118
|
+
dataDir;
|
|
119
|
+
constructor(dataDir, logger) {
|
|
120
|
+
this.logger = logger;
|
|
121
|
+
this.dataDir = dataDir;
|
|
122
|
+
for (const dir of Object.values(LAYER_DIRS)) {
|
|
123
|
+
mkdirSync(join(dataDir, dir), { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
this.loadIndex();
|
|
126
|
+
}
|
|
127
|
+
indexPath() {
|
|
128
|
+
return join(this.dataDir, 'index.json');
|
|
129
|
+
}
|
|
130
|
+
loadIndex() {
|
|
131
|
+
try {
|
|
132
|
+
const raw = readFileSync(this.indexPath(), 'utf8');
|
|
133
|
+
const data = JSON.parse(raw);
|
|
134
|
+
for (const it of data.items ?? []) {
|
|
135
|
+
this.items.set(it.id, it);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
this.items = new Map(); // 无 index:全新开始
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 防抖落盘 index.json(写操作先改内存) */
|
|
143
|
+
save() {
|
|
144
|
+
this.dirty = true;
|
|
145
|
+
if (this.saveTimer)
|
|
146
|
+
return;
|
|
147
|
+
this.saveTimer = setTimeout(() => {
|
|
148
|
+
this.saveTimer = null;
|
|
149
|
+
if (!this.dirty)
|
|
150
|
+
return;
|
|
151
|
+
this.dirty = false;
|
|
152
|
+
try {
|
|
153
|
+
const payload = { version: 1, items: [...this.items.values()] };
|
|
154
|
+
writeFileSync(this.indexPath(), JSON.stringify(payload, null, 2), 'utf8');
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.logger?.warn?.(`[sticker-store] index save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
158
|
+
}
|
|
159
|
+
}, 500);
|
|
160
|
+
}
|
|
161
|
+
/** 关闭时立即落盘(进程退出兜底) */
|
|
162
|
+
flush() {
|
|
163
|
+
if (this.saveTimer) {
|
|
164
|
+
clearTimeout(this.saveTimer);
|
|
165
|
+
this.saveTimer = null;
|
|
166
|
+
}
|
|
167
|
+
if (!this.dirty)
|
|
168
|
+
return;
|
|
169
|
+
this.dirty = false;
|
|
170
|
+
try {
|
|
171
|
+
writeFileSync(this.indexPath(), JSON.stringify({ version: 1, items: [...this.items.values()] }, null, 2), 'utf8');
|
|
172
|
+
}
|
|
173
|
+
catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
filePathOf(meta) {
|
|
176
|
+
return join(this.dataDir, LAYER_DIRS[meta.layer], `${meta.id}.${meta.ext}`);
|
|
177
|
+
}
|
|
178
|
+
/** 单条路径(给工具/发送用) */
|
|
179
|
+
pathOf(id) {
|
|
180
|
+
const it = this.items.get(id);
|
|
181
|
+
return it ? this.filePathOf(it) : undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 收藏一张图:安全下载 → SHA-1 去重 → 存候选区 → 记 index。
|
|
185
|
+
* 返回 'new' | 'dup' | 'error'。
|
|
186
|
+
*/
|
|
187
|
+
async capture(url, input, contentType) {
|
|
188
|
+
let buf;
|
|
189
|
+
try {
|
|
190
|
+
buf = await this.download(url);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
194
|
+
}
|
|
195
|
+
// 魔数校验:非图片(误收的语音/视频/未知)直接拒,不入库
|
|
196
|
+
if (!isImageBuffer(buf)) {
|
|
197
|
+
return { status: 'error', error: 'not an image (magic bytes mismatch)' };
|
|
198
|
+
}
|
|
199
|
+
const hash = createHash('sha1').update(buf).digest('hex');
|
|
200
|
+
const id = hash.slice(0, 12);
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
const existing = this.items.get(id);
|
|
203
|
+
if (existing) {
|
|
204
|
+
existing.lastSeenAt = now;
|
|
205
|
+
this.save();
|
|
206
|
+
return { status: 'dup', id };
|
|
207
|
+
}
|
|
208
|
+
const fromUrl = extname(new URL(url).pathname).replace(/^\./, '').slice(0, 5) || 'jpg';
|
|
209
|
+
const ext = (contentType && MIME_EXT[contentType.toLowerCase()]) || fromUrl;
|
|
210
|
+
const meta = {
|
|
211
|
+
id,
|
|
212
|
+
layer: 'candidate',
|
|
213
|
+
hash,
|
|
214
|
+
ext,
|
|
215
|
+
size: buf.length,
|
|
216
|
+
sourceGroup: input.sourceGroup,
|
|
217
|
+
senderId: input.senderId,
|
|
218
|
+
senderName: input.senderName,
|
|
219
|
+
msgText: input.msgText,
|
|
220
|
+
capturedAt: now,
|
|
221
|
+
firstSeenAt: now,
|
|
222
|
+
lastSeenAt: now,
|
|
223
|
+
useCount: 0,
|
|
224
|
+
tags: input.tags ?? [],
|
|
225
|
+
desc: input.desc,
|
|
226
|
+
needsDescribe: !input.desc,
|
|
227
|
+
sourceUrl: input.sourceUrl,
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
const dest = this.filePathOf(meta);
|
|
231
|
+
const tmp = `${dest}.tmp-${now}`;
|
|
232
|
+
writeFileSync(tmp, buf);
|
|
233
|
+
renameSync(tmp, dest);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
return { status: 'error', error: `write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
237
|
+
}
|
|
238
|
+
this.items.set(id, meta);
|
|
239
|
+
this.save();
|
|
240
|
+
return { status: 'new', id };
|
|
241
|
+
}
|
|
242
|
+
/** 安全下载:仅 HTTPS + SSRF + 上限 + 超时 */
|
|
243
|
+
async download(url) {
|
|
244
|
+
const parsed = new URL(url);
|
|
245
|
+
if (parsed.protocol !== 'https:')
|
|
246
|
+
throw new Error(`Only HTTPS allowed: ${parsed.protocol}`);
|
|
247
|
+
await assertSafeHostname(parsed.hostname);
|
|
248
|
+
const resp = await fetch(url, {
|
|
249
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
250
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' },
|
|
251
|
+
});
|
|
252
|
+
if (!resp.ok)
|
|
253
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
254
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
255
|
+
if (buf.length > MAX_IMAGE_BYTES)
|
|
256
|
+
throw new Error(`Image exceeds 10MB (${buf.length})`);
|
|
257
|
+
return buf;
|
|
258
|
+
}
|
|
259
|
+
/** 从本地已有文件入库(后续升级/负样本用) */
|
|
260
|
+
addLocalFile(srcPath, meta) {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const full = {
|
|
263
|
+
id: meta.id,
|
|
264
|
+
layer: meta.layer,
|
|
265
|
+
hash: meta.hash ?? meta.id,
|
|
266
|
+
ext: meta.ext,
|
|
267
|
+
size: meta.size ?? 0,
|
|
268
|
+
sourceGroup: meta.sourceGroup,
|
|
269
|
+
senderId: meta.senderId,
|
|
270
|
+
senderName: meta.senderName,
|
|
271
|
+
msgText: meta.msgText,
|
|
272
|
+
capturedAt: meta.capturedAt ?? now,
|
|
273
|
+
firstSeenAt: meta.firstSeenAt ?? now,
|
|
274
|
+
lastSeenAt: meta.lastSeenAt ?? now,
|
|
275
|
+
useCount: meta.useCount ?? 0,
|
|
276
|
+
tags: meta.tags ?? [],
|
|
277
|
+
desc: meta.desc,
|
|
278
|
+
needsDescribe: meta.needsDescribe,
|
|
279
|
+
trashedAt: meta.trashedAt,
|
|
280
|
+
prevLayer: meta.prevLayer,
|
|
281
|
+
};
|
|
282
|
+
if (existsSync(srcPath) && !existsSync(this.filePathOf(full))) {
|
|
283
|
+
mkdirSync(join(this.dataDir, LAYER_DIRS[full.layer]), { recursive: true });
|
|
284
|
+
renameSync(srcPath, this.filePathOf(full));
|
|
285
|
+
}
|
|
286
|
+
this.items.set(full.id, full);
|
|
287
|
+
this.save();
|
|
288
|
+
}
|
|
289
|
+
/** 标记使用(候选→正式库并搬文件; 发送过=用过, 计入 useCount) */
|
|
290
|
+
markUsed(id) {
|
|
291
|
+
const it = this.items.get(id);
|
|
292
|
+
if (!it)
|
|
293
|
+
return;
|
|
294
|
+
it.useCount += 1;
|
|
295
|
+
it.lastSeenAt = Date.now();
|
|
296
|
+
if (it.layer === 'candidate') {
|
|
297
|
+
const from = it.layer;
|
|
298
|
+
it.layer = 'library';
|
|
299
|
+
this.moveFile(it, from);
|
|
300
|
+
}
|
|
301
|
+
this.save();
|
|
302
|
+
}
|
|
303
|
+
/** 主动收藏/打标认可: 升级到正式区(不计数; 正式区不参与自动滚动清理) */
|
|
304
|
+
promote(id) {
|
|
305
|
+
const it = this.items.get(id);
|
|
306
|
+
if (!it || it.layer === 'library')
|
|
307
|
+
return;
|
|
308
|
+
if (it.layer === 'trash') {
|
|
309
|
+
it.trashedAt = undefined;
|
|
310
|
+
it.prevLayer = undefined;
|
|
311
|
+
}
|
|
312
|
+
const from = it.layer;
|
|
313
|
+
it.layer = 'library';
|
|
314
|
+
it.lastSeenAt = Date.now();
|
|
315
|
+
this.moveFile(it, from);
|
|
316
|
+
this.save();
|
|
317
|
+
}
|
|
318
|
+
markReaction(id, kind) {
|
|
319
|
+
const it = this.items.get(id);
|
|
320
|
+
if (!it)
|
|
321
|
+
return;
|
|
322
|
+
if (kind === 'dislike' && it.layer !== 'negative') {
|
|
323
|
+
const from = it.layer;
|
|
324
|
+
it.layer = 'negative';
|
|
325
|
+
this.moveFile(it, from);
|
|
326
|
+
}
|
|
327
|
+
this.save();
|
|
328
|
+
}
|
|
329
|
+
/** 查询:q/tag/描述命中 或 最近入库(默认不含回收站/负样本) */
|
|
330
|
+
search(opts) {
|
|
331
|
+
const tag = (opts.tag ?? '').trim().toLowerCase();
|
|
332
|
+
const limit = Math.max(1, Math.min(50, opts.limit || 10));
|
|
333
|
+
let pool = [...this.items.values()].filter(it => it.layer !== 'negative'
|
|
334
|
+
&& (opts.includeTrash === true || it.layer !== 'trash')
|
|
335
|
+
&& !it.msgText?.startsWith('[系统'));
|
|
336
|
+
if (tag) {
|
|
337
|
+
pool = pool.filter(it => it.tags.some(t => t.toLowerCase().includes(tag)));
|
|
338
|
+
}
|
|
339
|
+
// 多词搜索: 拆词后"任一命中即可"(OR), 命中词越多排越前, 同分按最近入库
|
|
340
|
+
const terms = splitTerms(opts.q);
|
|
341
|
+
if (terms.length) {
|
|
342
|
+
const scores = new Map();
|
|
343
|
+
pool = pool.filter(it => {
|
|
344
|
+
const n = hitCount(terms, stickerHay(it));
|
|
345
|
+
if (n > 0) {
|
|
346
|
+
scores.set(it, n);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
});
|
|
351
|
+
pool.sort((a, b) => (scores.get(b) ?? 0) - (scores.get(a) ?? 0) || (b.firstSeenAt - a.firstSeenAt));
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
pool.sort((a, b) => b.firstSeenAt - a.firstSeenAt);
|
|
355
|
+
}
|
|
356
|
+
const top = pool.slice(0, limit);
|
|
357
|
+
return top.map(it => ({
|
|
358
|
+
id: it.id,
|
|
359
|
+
layer: it.layer,
|
|
360
|
+
path: this.filePathOf(it),
|
|
361
|
+
tags: it.tags,
|
|
362
|
+
desc: it.desc,
|
|
363
|
+
sourceGroup: it.sourceGroup,
|
|
364
|
+
msgText: it.msgText,
|
|
365
|
+
capturedAt: it.capturedAt,
|
|
366
|
+
useCount: it.useCount,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
/** 更新一张图的标签与详细描述(tags/desc 传 undefined 表示不动该项; 自动去重) */
|
|
370
|
+
setDescTags(id, tags, desc) {
|
|
371
|
+
const it = this.items.get(id);
|
|
372
|
+
if (!it)
|
|
373
|
+
return false;
|
|
374
|
+
if (tags !== undefined) {
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
const cleaned = [];
|
|
377
|
+
for (const t of tags) {
|
|
378
|
+
const c = (t ?? '').trim();
|
|
379
|
+
if (c && !seen.has(c)) {
|
|
380
|
+
seen.add(c);
|
|
381
|
+
cleaned.push(c.slice(0, 20));
|
|
382
|
+
}
|
|
383
|
+
if (cleaned.length >= 20)
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
it.tags = cleaned;
|
|
387
|
+
}
|
|
388
|
+
if (desc !== undefined) {
|
|
389
|
+
const d = (desc ?? '').trim();
|
|
390
|
+
if (d) {
|
|
391
|
+
it.desc = d.slice(0, 300);
|
|
392
|
+
it.needsDescribe = false;
|
|
393
|
+
}
|
|
394
|
+
else if (it.desc) {
|
|
395
|
+
it.desc = undefined;
|
|
396
|
+
it.needsDescribe = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.save();
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
/** 把本地已有图片文件导入收藏(算hash去重, 复制进候选区); 返回 {status:'new'|'dup'|'error', id?} */
|
|
403
|
+
importLocalFile(srcPath, input) {
|
|
404
|
+
try {
|
|
405
|
+
if (!existsSync(srcPath))
|
|
406
|
+
return { status: 'error', error: `文件不存在: ${srcPath}` };
|
|
407
|
+
const buf = readFileSync(srcPath);
|
|
408
|
+
if (!isImageBuffer(buf))
|
|
409
|
+
return { status: 'error', error: 'not an image (magic bytes mismatch)' };
|
|
410
|
+
const hash = createHash('sha1').update(buf).digest('hex');
|
|
411
|
+
const id = hash.slice(0, 12);
|
|
412
|
+
const existing = this.items.get(id);
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
if (existing) {
|
|
415
|
+
existing.lastSeenAt = now;
|
|
416
|
+
this.save();
|
|
417
|
+
return { status: 'dup', id };
|
|
418
|
+
}
|
|
419
|
+
const ext = (input?.sourceUrl ? extname(new URL(input.sourceUrl).pathname).replace(/^\./, '').slice(0, 5) : extname(srcPath).replace(/^\./, '').toLowerCase().slice(0, 5)) || 'jpg';
|
|
420
|
+
const meta = {
|
|
421
|
+
id,
|
|
422
|
+
layer: 'candidate',
|
|
423
|
+
hash,
|
|
424
|
+
ext,
|
|
425
|
+
size: buf.length,
|
|
426
|
+
sourceGroup: input?.sourceGroup,
|
|
427
|
+
senderId: input?.senderId,
|
|
428
|
+
senderName: input?.senderName,
|
|
429
|
+
msgText: input?.msgText,
|
|
430
|
+
capturedAt: now,
|
|
431
|
+
firstSeenAt: now,
|
|
432
|
+
lastSeenAt: now,
|
|
433
|
+
useCount: 0,
|
|
434
|
+
tags: input?.tags ?? [],
|
|
435
|
+
desc: input?.desc,
|
|
436
|
+
needsDescribe: !input?.desc,
|
|
437
|
+
sourceUrl: input?.sourceUrl,
|
|
438
|
+
};
|
|
439
|
+
const dest = this.filePathOf(meta);
|
|
440
|
+
mkdirSync(join(this.dataDir, LAYER_DIRS.candidate), { recursive: true });
|
|
441
|
+
if (!existsSync(dest)) {
|
|
442
|
+
const tmp = `${dest}.tmp-${now}`;
|
|
443
|
+
writeFileSync(tmp, buf);
|
|
444
|
+
renameSync(tmp, dest);
|
|
445
|
+
}
|
|
446
|
+
this.items.set(id, meta);
|
|
447
|
+
this.save();
|
|
448
|
+
return { status: 'new', id };
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
return { status: 'error', error: e instanceof Error ? e.message : String(e) };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** 送回收站(可逆; prevLayer 记原层用于还原) */
|
|
455
|
+
markTrash(id) {
|
|
456
|
+
const it = this.items.get(id);
|
|
457
|
+
if (!it || it.layer === 'trash')
|
|
458
|
+
return false;
|
|
459
|
+
const from = it.layer;
|
|
460
|
+
it.prevLayer = it.layer;
|
|
461
|
+
it.layer = 'trash';
|
|
462
|
+
it.trashedAt = Date.now();
|
|
463
|
+
this.moveFile(it, from);
|
|
464
|
+
this.save();
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
/** 从回收站还原(回到原层或候选区) */
|
|
468
|
+
restore(id) {
|
|
469
|
+
const it = this.items.get(id);
|
|
470
|
+
if (!it || it.layer !== 'trash')
|
|
471
|
+
return false;
|
|
472
|
+
const from = it.layer;
|
|
473
|
+
it.layer = it.prevLayer ?? 'candidate';
|
|
474
|
+
it.trashedAt = undefined;
|
|
475
|
+
it.prevLayer = undefined;
|
|
476
|
+
this.moveFile(it, from);
|
|
477
|
+
this.save();
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
/** 物理删除回收站中超过 ttlMs 的条目(含文件+index) */
|
|
481
|
+
purgeTrashed(ttlMs = 30 * 24 * 3600 * 1000) {
|
|
482
|
+
const cutoff = Date.now() - ttlMs;
|
|
483
|
+
let n = 0;
|
|
484
|
+
for (const it of [...this.items.values()]) {
|
|
485
|
+
if (it.layer === 'trash' && (it.trashedAt ?? it.lastSeenAt) < cutoff) {
|
|
486
|
+
this.deleteFile(it);
|
|
487
|
+
this.items.delete(it.id);
|
|
488
|
+
n += 1;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (n > 0)
|
|
492
|
+
this.save();
|
|
493
|
+
return n;
|
|
494
|
+
}
|
|
495
|
+
/** 候选区超限清理: 超过 maxCandidates 时, 最久未见的滚进回收站(不物理删) */
|
|
496
|
+
cleanupCandidates(maxCandidates) {
|
|
497
|
+
const cands = [...this.items.values()].filter(it => it.layer === 'candidate');
|
|
498
|
+
if (cands.length <= maxCandidates)
|
|
499
|
+
return 0;
|
|
500
|
+
const sorted = cands.sort((a, b) => a.lastSeenAt - b.lastSeenAt); // 最旧在前
|
|
501
|
+
const excess = sorted.slice(0, cands.length - maxCandidates);
|
|
502
|
+
for (const it of excess)
|
|
503
|
+
this.markTrash(it.id);
|
|
504
|
+
return excess.length;
|
|
505
|
+
}
|
|
506
|
+
/** 清理损坏/空文件条目(文件缺失或 0 字节) */
|
|
507
|
+
cleanupBroken() {
|
|
508
|
+
let n = 0;
|
|
509
|
+
for (const it of [...this.items.values()]) {
|
|
510
|
+
try {
|
|
511
|
+
const p = this.filePathOf(it);
|
|
512
|
+
if (!existsSync(p)) {
|
|
513
|
+
this.items.delete(it.id);
|
|
514
|
+
n += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const st = statSync(p);
|
|
518
|
+
if (st.size === 0) {
|
|
519
|
+
this.deleteFile(it);
|
|
520
|
+
this.items.delete(it.id);
|
|
521
|
+
n += 1;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
this.items.delete(it.id);
|
|
526
|
+
n += 1;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (n > 0)
|
|
530
|
+
this.save();
|
|
531
|
+
return n;
|
|
532
|
+
}
|
|
533
|
+
/** 按层统计(整理/仪表盘用) */
|
|
534
|
+
stats() {
|
|
535
|
+
const s = { total: 0, candidate: 0, library: 0, trash: 0, negative: 0, untagged: 0, totalBytes: 0 };
|
|
536
|
+
for (const it of this.items.values()) {
|
|
537
|
+
s.total += 1;
|
|
538
|
+
if (it.layer === 'candidate')
|
|
539
|
+
s.candidate += 1;
|
|
540
|
+
else if (it.layer === 'library')
|
|
541
|
+
s.library += 1;
|
|
542
|
+
else if (it.layer === 'trash')
|
|
543
|
+
s.trash += 1;
|
|
544
|
+
else
|
|
545
|
+
s.negative += 1;
|
|
546
|
+
if (!it.tags?.length && !it.desc)
|
|
547
|
+
s.untagged += 1;
|
|
548
|
+
s.totalBytes += it.size;
|
|
549
|
+
}
|
|
550
|
+
return s;
|
|
551
|
+
}
|
|
552
|
+
/** 按来源 URL 反查条目(agent 拿群消息里的 URL 定位本地图) */
|
|
553
|
+
findBySourceUrl(url) {
|
|
554
|
+
const u = (url ?? '').trim();
|
|
555
|
+
if (!u)
|
|
556
|
+
return undefined;
|
|
557
|
+
for (const it of this.items.values()) {
|
|
558
|
+
if (it.sourceUrl === u)
|
|
559
|
+
return it;
|
|
560
|
+
}
|
|
561
|
+
// 容错: URL 归一(去 spec/query 尾参)或前缀匹配
|
|
562
|
+
const norm = u.split('&spec=')[0] ?? u;
|
|
563
|
+
for (const it of this.items.values()) {
|
|
564
|
+
if (it.sourceUrl && it.sourceUrl.startsWith(norm))
|
|
565
|
+
return it;
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
/** 全量查询(管理面板用): 可按层/关键词/未打标过滤, 默认不含回收站 */
|
|
570
|
+
queryAll(opts = {}) {
|
|
571
|
+
// 多词搜索(同 search): 任一命中即可, 命中多排前
|
|
572
|
+
const terms = splitTerms(opts.q);
|
|
573
|
+
let pool = [...this.items.values()].filter(it => {
|
|
574
|
+
if (!opts.includeTrash && it.layer === 'trash')
|
|
575
|
+
return false;
|
|
576
|
+
if (opts.layer && opts.layer !== 'all' && it.layer !== opts.layer)
|
|
577
|
+
return false;
|
|
578
|
+
if (opts.untagged && (it.tags?.length || it.desc))
|
|
579
|
+
return false;
|
|
580
|
+
if (terms.length && hitCount(terms, stickerHay(it)) === 0)
|
|
581
|
+
return false;
|
|
582
|
+
return true;
|
|
583
|
+
});
|
|
584
|
+
if (terms.length) {
|
|
585
|
+
const scores = new Map();
|
|
586
|
+
for (const it of pool)
|
|
587
|
+
scores.set(it, hitCount(terms, stickerHay(it)));
|
|
588
|
+
pool.sort((a, b) => (scores.get(b) ?? 0) - (scores.get(a) ?? 0) || (b.firstSeenAt - a.firstSeenAt));
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
pool.sort((a, b) => b.firstSeenAt - a.firstSeenAt);
|
|
592
|
+
}
|
|
593
|
+
return pool.map(it => ({
|
|
594
|
+
id: it.id,
|
|
595
|
+
path: this.filePathOf(it),
|
|
596
|
+
layer: it.layer,
|
|
597
|
+
tags: it.tags,
|
|
598
|
+
desc: it.desc,
|
|
599
|
+
useCount: it.useCount,
|
|
600
|
+
size: it.size,
|
|
601
|
+
firstSeenAt: it.firstSeenAt,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
get(id) {
|
|
605
|
+
return this.items.get(id);
|
|
606
|
+
}
|
|
607
|
+
size() {
|
|
608
|
+
return this.items.size;
|
|
609
|
+
}
|
|
610
|
+
deleteFile(it) {
|
|
611
|
+
try {
|
|
612
|
+
const p = this.filePathOf(it);
|
|
613
|
+
if (existsSync(p))
|
|
614
|
+
rmSync(p, { force: true });
|
|
615
|
+
}
|
|
616
|
+
catch { /* ignore */ }
|
|
617
|
+
}
|
|
618
|
+
/** 层间移动文件到新路径(目录自动建) */
|
|
619
|
+
moveFile(it, fromLayer) {
|
|
620
|
+
try {
|
|
621
|
+
const from = join(this.dataDir, LAYER_DIRS[fromLayer], `${it.id}.${it.ext}`);
|
|
622
|
+
const dir = join(this.dataDir, LAYER_DIRS[it.layer]);
|
|
623
|
+
mkdirSync(dir, { recursive: true });
|
|
624
|
+
const to = join(dir, `${it.id}.${it.ext}`);
|
|
625
|
+
if (existsSync(from) && from !== to)
|
|
626
|
+
renameSync(from, to);
|
|
627
|
+
}
|
|
628
|
+
catch { /* ignore */ }
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// ── 多例注册表(多账号支持, 2026-09-03): 每账号实例一个 dataDir → 各自独立的图库。
|
|
632
|
+
// Map<realpath(dataDir), StickerStore>; primary = 最早 configure 的实例(兼容原单账号无参调用)。
|
|
633
|
+
const _stores = new Map();
|
|
634
|
+
let _primaryDir;
|
|
635
|
+
/**
|
|
636
|
+
* 配置图库实例(启动期每账号按自己 dataDir 预初始化; 同目录幂等)。
|
|
637
|
+
* ⚠️ 时序坑(线上踩坑):必须启动早期按 config.cwd 解析的 dataDir 初始化,
|
|
638
|
+
* 避免无参 getStickerStore 读到 process.cwd 的错目录(C盘幽灵库)。
|
|
639
|
+
*/
|
|
640
|
+
export function configureStickerStore(dataDir, logger) {
|
|
641
|
+
const key = resolve(dataDir);
|
|
642
|
+
let s = _stores.get(key);
|
|
643
|
+
if (!s) {
|
|
644
|
+
s = new StickerStore(dataDir, logger);
|
|
645
|
+
_stores.set(key, s);
|
|
646
|
+
}
|
|
647
|
+
if (!_primaryDir)
|
|
648
|
+
_primaryDir = key;
|
|
649
|
+
return s;
|
|
650
|
+
}
|
|
651
|
+
/** 获取图库实例。带 dataDir → 按目录取(不在则容错新建); 无参 → primary(主账号库, 兼容旧调用)。 */
|
|
652
|
+
export function getStickerStore(dataDir, logger) {
|
|
653
|
+
if (dataDir) {
|
|
654
|
+
const key = resolve(dataDir);
|
|
655
|
+
let s = _stores.get(key);
|
|
656
|
+
if (!s) {
|
|
657
|
+
s = new StickerStore(dataDir, logger);
|
|
658
|
+
_stores.set(key, s);
|
|
659
|
+
}
|
|
660
|
+
return s;
|
|
661
|
+
}
|
|
662
|
+
if (_primaryDir) {
|
|
663
|
+
const s = _stores.get(_primaryDir);
|
|
664
|
+
if (s)
|
|
665
|
+
return s;
|
|
666
|
+
}
|
|
667
|
+
if (_stores.size === 1)
|
|
668
|
+
return _stores.values().next().value;
|
|
669
|
+
const dir = join(process.cwd(), '表情包');
|
|
670
|
+
const s = new StickerStore(dir, logger);
|
|
671
|
+
_stores.set(resolve(dir), s);
|
|
672
|
+
if (!_primaryDir)
|
|
673
|
+
_primaryDir = resolve(dir);
|
|
674
|
+
return s;
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=sticker-store.js.map
|