@roitium/telegram-stickers-brain 2.0.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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # telegram-stickers-brain
2
+
3
+ Semantic Telegram sticker management for OpenClaw. Features VLM-powered captioning, vector search, and automated/manual collection syncing.
4
+
5
+ ## Features
6
+
7
+ - **Semantic Search**: Find stickers by emotion, action, or character description (e.g., "happy smiling girl").
8
+ - **VLM Captioning**: Automatically describes stickers using Google Gemini Vision.
9
+ - **Vector Indexing**: High-performance local search using SQLite-vec and GGUF embeddings.
10
+ - **Flexible Syncing**:
11
+ - **Auto-Collect**: Automatically sync sticker sets from messages received in Telegram.
12
+ - **Manual Sync**: Direct sync via sticker set name or link.
13
+ - **Admin Notifications**: Real-time progress updates for sync tasks.
14
+
15
+ ## Installation
16
+
17
+ 1. Clone this into your OpenClaw extensions directory:
18
+ ```bash
19
+ cd ~/.openclaw/extensions
20
+ git clone <repo-url> telegram-stickers
21
+ ```
22
+ 2. Install dependencies:
23
+ ```bash
24
+ cd telegram-stickers
25
+ npm install
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ Add the following to your `~/.openclaw/openclaw.json`:
31
+
32
+ ```json
33
+ {
34
+ "plugins": {
35
+ "entries": {
36
+ "telegram-stickers": {
37
+ "enabled": true,
38
+ "config": {
39
+ "vlmApiKey": "YOUR_GEMINI_API_KEY",
40
+ "vlmModel": "gemini-3.1-flash-lite-preview",
41
+ "autoCollect": true,
42
+ "notifyChatId": "YOUR_TELEGRAM_ID"
43
+ }
44
+ }
45
+ }
46
+ },
47
+ "channels": {
48
+ "telegram": {
49
+ "actions": {
50
+ "sticker": true
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Tools
58
+
59
+ - `search_sticker_by_emotion`: Returns a `sticker_id` for semantic queries.
60
+ - `sync_sticker_set_by_name`: Manually queue a set by name or `t.me` link.
61
+ - `get_sticker_stats`: Check the total number of indexed stickers.
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,57 @@
1
+ const { GoogleGenerativeAI } = require("@google/generative-ai");
2
+ const https = require("https");
3
+
4
+ let apiKey = process.env.VLM_API_KEY;
5
+
6
+ if (!apiKey) {
7
+ console.error("VLM_API_KEY is not set in plugin config.");
8
+ process.exit(1);
9
+ }
10
+
11
+ const genAI = new GoogleGenerativeAI(apiKey);
12
+
13
+ // If the model string includes a provider prefix (e.g. "google/gemini..."), strip it.
14
+ let rawModel = process.env.VLM_MODEL || "gemini-3.1-flash-lite-preview";
15
+ const modelName = rawModel.includes("/") ? rawModel.split("/")[1] : rawModel;
16
+
17
+ async function describeImage(imageUrl, promptText) {
18
+ const model = genAI.getGenerativeModel({ model: modelName });
19
+
20
+ return new Promise((resolve, reject) => {
21
+ https.get(imageUrl, (res) => {
22
+ const chunks = [];
23
+ res.on('data', chunk => chunks.push(chunk));
24
+ res.on('end', async () => {
25
+ const buffer = Buffer.concat(chunks);
26
+
27
+ try {
28
+ const imageParts = [
29
+ {
30
+ inlineData: {
31
+ data: buffer.toString("base64"),
32
+ mimeType: "image/webp"
33
+ }
34
+ }
35
+ ];
36
+
37
+ const result = await model.generateContent([promptText, ...imageParts]);
38
+ const response = await result.response;
39
+ const text = response.text();
40
+ resolve(text);
41
+ } catch (e) {
42
+ reject(e);
43
+ }
44
+ });
45
+ }).on('error', reject);
46
+ });
47
+ }
48
+
49
+ const [,, imageUrl, prompt] = process.argv;
50
+ if (imageUrl && prompt) {
51
+ describeImage(imageUrl, prompt)
52
+ .then(text => console.log(text))
53
+ .catch(err => {
54
+ console.error(err.message);
55
+ process.exit(1);
56
+ });
57
+ }
package/index.js ADDED
@@ -0,0 +1,400 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const cp = require('child_process');
4
+ const https = require('https');
5
+ const sqlite3 = require('sqlite3');
6
+ const { load } = require('sqlite-vec');
7
+
8
+ module.exports = function(api) {
9
+ // Wrap async init in an IIFE to keep module.exports sync
10
+ (async () => {
11
+
12
+ const STATE_DIR = api.runtime.state.resolveStateDir();
13
+ const CORE_CACHE_FILE = path.join(STATE_DIR, "telegram", "sticker-cache.json");
14
+ const METADATA_DIR = "/root/.openclaw/workspace/stickers_metadata";
15
+
16
+ if (!fs.existsSync(METADATA_DIR)) {
17
+ fs.mkdirSync(METADATA_DIR, { recursive: true });
18
+ }
19
+
20
+ let db = null;
21
+ let embeddingContext = null;
22
+ let isModelLoading = false;
23
+
24
+ function ensureDB() {
25
+ if (!db) {
26
+ try {
27
+ db = new sqlite3.Database('/root/.cache/qmd/index.sqlite');
28
+ load(db);
29
+ } catch (e) {
30
+ api.logger.error("[Stickers] Failed to load vector db: " + e.message);
31
+ }
32
+ }
33
+ return db;
34
+ }
35
+
36
+ async function ensureVectorModel() {
37
+ if (embeddingContext || isModelLoading) return embeddingContext;
38
+ isModelLoading = true;
39
+ try {
40
+ const { getLlama, resolveModelFile } = await import("node-llama-cpp");
41
+ api.logger.info("[Stickers] Ensuring embedding model is available (downloading if missing)...");
42
+ const modelPath = await resolveModelFile(
43
+ "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf",
44
+ { directory: "/root/.cache/qmd/models", cli: false }
45
+ );
46
+ const llama = await getLlama();
47
+ const model = await llama.loadModel({ modelPath });
48
+ embeddingContext = await model.createEmbeddingContext();
49
+ api.logger.info("[Stickers] Lazy-loaded embeddinggemma-300M model into memory.");
50
+ } catch (e) {
51
+ api.logger.error("[Stickers] Failed to lazy-load embedding model: " + e.message);
52
+ }
53
+ isModelLoading = false;
54
+ return embeddingContext;
55
+ }
56
+
57
+ // Helper to run commands
58
+ function runCmd(cmd) {
59
+ try {
60
+ return cp.execSync(cmd).toString().trim();
61
+ } catch (e) {
62
+ api.logger.error(`Command failed: ${cmd} - ${e.message}`);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ // Get Bot Token
68
+ function getBotToken() {
69
+ return runCmd("jq -r .channels.telegram.botToken ~/.openclaw/openclaw.json");
70
+ }
71
+
72
+ // Telegram API helper
73
+ async function tgRequest(method, params = {}) {
74
+ const token = getBotToken();
75
+ if (!token) throw new Error("Bot token not found");
76
+ return new Promise((resolve, reject) => {
77
+ const url = `https://api.telegram.org/bot${token}/${method}`;
78
+ const req = https.request(url, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' }
81
+ }, (res) => {
82
+ let data = '';
83
+ res.on('data', chunk => data += chunk);
84
+ res.on('end', () => {
85
+ const result = JSON.parse(data);
86
+ if (result.ok) resolve(result.result);
87
+ else reject(new Error(result.description));
88
+ });
89
+ });
90
+ req.on('error', reject);
91
+ req.write(JSON.stringify(params));
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ // Helper for admin notifications
97
+ async function notifyAdmin(text) {
98
+ const pCfg = api.config?.plugins?.entries?.['telegram-stickers']?.config || {};
99
+ const target = pCfg.notifyChatId;
100
+ if (!target) return;
101
+ try {
102
+ await tgRequest('sendMessage', { chat_id: target, text });
103
+ } catch (e) {
104
+ api.logger.warn(`Failed to notify admin: ${e.message}`);
105
+ }
106
+ }
107
+
108
+ // Task Queue for Background Service
109
+ const queue = [];
110
+ let isProcessing = false;
111
+
112
+ async function processQueue() {
113
+ if (isProcessing || queue.length === 0) return;
114
+ isProcessing = true;
115
+
116
+ while (queue.length > 0) {
117
+ const setName = queue.shift();
118
+ try {
119
+ api.logger.info(`[Stickers] Syncing set: ${setName}`);
120
+ await notifyAdmin(`🔄 [Stickers] 开始同步表情包合集: ${setName}`);
121
+
122
+ const stickerSet = await tgRequest('getStickerSet', { name: setName });
123
+ let processedCount = 0;
124
+
125
+ for (const sticker of stickerSet.stickers) {
126
+ const qmdPath = path.join(METADATA_DIR, `${sticker.file_unique_id}.qmd`);
127
+ if (fs.existsSync(qmdPath)) continue;
128
+
129
+ api.logger.info(`[Stickers] Captioning sticker ${sticker.file_unique_id} in ${setName}...`);
130
+
131
+ // 1. Get File Path
132
+ const fileInfo = await tgRequest('getFile', { file_id: sticker.file_id });
133
+ const downloadUrl = `https://api.telegram.org/file/bot${getBotToken()}/${fileInfo.file_path}`;
134
+
135
+ // 2. Vision Caption
136
+ const prompt = "请用中文详细描述这个表情包的情绪、动作和角色特征。如果是二次元风格,请注明。简洁但要有表现力。";
137
+
138
+ let caption = "无法生成描述";
139
+ try {
140
+ const pCfg = api.config?.plugins?.entries?.['telegram-stickers']?.config || {};
141
+ const scriptPath = path.join(__dirname, "describe_sticker.js");
142
+ const child = cp.spawnSync('node', [scriptPath, downloadUrl, prompt], {
143
+ env: {
144
+ ...process.env,
145
+ VLM_API_KEY: pCfg.vlmApiKey || "",
146
+ VLM_MODEL: pCfg.vlmModel || "gemini-3.1-flash-lite-preview"
147
+ }
148
+ });
149
+ const result = child.stdout.toString().trim();
150
+ if (result) caption = result;
151
+ if (child.stderr && child.stderr.length > 0) {
152
+ const stderrStr = child.stderr.toString().trim();
153
+ if (stderrStr) api.logger.warn(`[Stickers] VLM script stderr: ${stderrStr}`);
154
+ }
155
+ } catch (e) {
156
+ api.logger.error(`[Stickers] VLM script failed: ${e.message}`);
157
+ }
158
+
159
+ // 3. Write QMD
160
+ const content = `---
161
+ file_id: "${sticker.file_id}"
162
+ file_unique_id: "${sticker.file_unique_id}"
163
+ emoji: "${sticker.emoji}"
164
+ set_name: "${setName}"
165
+ ---
166
+ # Sticker Description
167
+ ${caption}`;
168
+
169
+ fs.writeFileSync(qmdPath, content);
170
+ processedCount++;
171
+
172
+ // 4. Update Core Cache to "poison" it
173
+ try {
174
+ const coreCache = JSON.parse(fs.readFileSync(CORE_CACHE_FILE, 'utf8'));
175
+ coreCache.stickers[sticker.file_unique_id] = {
176
+ fileId: sticker.file_id,
177
+ fileUniqueId: sticker.file_unique_id,
178
+ emoji: sticker.emoji,
179
+ setName: setName,
180
+ description: caption,
181
+ cachedAt: new Date().toISOString(),
182
+ receivedFrom: "plugin:telegram-stickers"
183
+ };
184
+ fs.writeFileSync(CORE_CACHE_FILE, JSON.stringify(coreCache, null, 2));
185
+ } catch (e) {
186
+ api.logger.warn("Failed to update core sticker cache: " + e.message);
187
+ }
188
+ }
189
+
190
+ api.logger.info(`[Stickers] Set ${setName} processed (${processedCount} new). Updating QMD index...`);
191
+ runCmd(`qmd collection add ${METADATA_DIR} --name stickers --mask "**/*.qmd" || true`);
192
+ runCmd(`cd ${METADATA_DIR} && qmd update`);
193
+ runCmd(`qmd embed`);
194
+
195
+ await notifyAdmin(`✅ [Stickers] 表情包合集 ${setName} 同步完成,新增 ${processedCount} 张!`);
196
+ } catch (err) {
197
+ api.logger.error(`[Stickers] Error processing set ${setName}: ${err.message}`);
198
+ await notifyAdmin(`❌ [Stickers] 同步表情包合集 ${setName} 失败: ${err.message}`);
199
+ }
200
+ }
201
+ isProcessing = false;
202
+ }
203
+
204
+ // Use Register Service for background queue processing
205
+ let syncInterval;
206
+ api.registerService({
207
+ id: "telegram-stickers-sync",
208
+ start: () => {
209
+ syncInterval = setInterval(() => {
210
+ processQueue().catch(e => api.logger.error("Sync error: " + e));
211
+ }, 5000);
212
+ },
213
+ stop: () => {
214
+ if (syncInterval) clearInterval(syncInterval);
215
+ }
216
+ });
217
+
218
+ // Message Handler for Auto-Steal via `api.on` (Classic Method)
219
+ if (api.on) {
220
+ api.on('message_received', async (event, ctx) => {
221
+ // Only process telegram messages
222
+ const channel = event.metadata?.channel || event.metadata?.originatingChannel;
223
+ if (channel !== 'telegram') return;
224
+
225
+ if (!event.content || (!event.content.includes('<media:sticker>') && !event.content.includes('sticker'))) return;
226
+
227
+ const pCfg = api.config?.plugins?.entries?.['telegram-stickers']?.config || {};
228
+ const autoCollect = pCfg.autoCollect !== false;
229
+ if (!autoCollect) return;
230
+
231
+ setTimeout(() => {
232
+ try {
233
+ const cache = JSON.parse(fs.readFileSync(CORE_CACHE_FILE, 'utf8'));
234
+ const senderId = event.metadata?.senderId || event.from?.split(':')?.[1];
235
+ if (!senderId) return;
236
+
237
+ const lastSticker = Object.values(cache.stickers)
238
+ .filter(s => s.receivedFrom === `telegram:${senderId}`)
239
+ .sort((a, b) => new Date(b.cachedAt) - new Date(a.cachedAt))[0];
240
+
241
+ if (lastSticker && lastSticker.setName && !queue.includes(lastSticker.setName)) {
242
+ api.logger.info(`[Stickers] Detected new sticker from set: ${lastSticker.setName}. Queuing for sync.`);
243
+ queue.push(lastSticker.setName);
244
+ }
245
+ } catch (e) {
246
+ api.logger.error("Failed to detect sticker set from cache: " + e.message);
247
+ }
248
+ }, 2000);
249
+ });
250
+ }
251
+
252
+ // Explicit sync tool
253
+ api.registerTool({
254
+ name: "sync_sticker_set_by_name",
255
+ emoji: "📥",
256
+ description: "通过表情包合集的名字(或者包含名字的链接)来手动同步一个 Telegram 表情包合集。当用户发给你一个表情包链接,或者告诉你合集名字让你收集时调用。",
257
+ parameters: {
258
+ type: "object",
259
+ properties: {
260
+ setNameOrUrl: {
261
+ type: "string",
262
+ description: "合集的名字(例如:AnimalPack)或者合集的分享链接(例如:https://t.me/addstickers/AnimalPack)"
263
+ }
264
+ },
265
+ required: ["setNameOrUrl"]
266
+ },
267
+ async execute(id, params, context) {
268
+ try {
269
+ let targetSetName = params.setNameOrUrl.trim();
270
+
271
+ // If it's a URL, extract the set name from the end
272
+ if (targetSetName.includes("t.me/addstickers/")) {
273
+ const parts = targetSetName.split("/");
274
+ targetSetName = parts[parts.length - 1].split("?")[0]; // remove any query params
275
+ }
276
+
277
+ if (!targetSetName) {
278
+ return { content: [{ type: "text", text: `无法从你提供的参数中提取合集名称,请检查格式!` }] };
279
+ }
280
+
281
+ if (queue.includes(targetSetName)) {
282
+ return { content: [{ type: "text", text: `表情包合集 ${targetSetName} 已经在同步队列中啦!` }] };
283
+ }
284
+
285
+ queue.push(targetSetName);
286
+ return { content: [{ type: "text", text: `好的!我已经把合集 ${targetSetName} 加入后台同步队列了,同步完成后我会再通知。` }] };
287
+ } catch (e) {
288
+ return { content: [{ type: "text", text: `同步任务提交失败: ${e.message}` }] };
289
+ }
290
+ }
291
+ });
292
+
293
+ api.registerTool({
294
+ name: "get_sticker_stats",
295
+ emoji: "📊",
296
+ description: "查询当前表情包库中已处理和索引的表情包数量。当用户询问表情包库状态、进度时调用。",
297
+ parameters: { type: "object", properties: {} },
298
+ async execute() {
299
+ const count = runCmd(`ls -1 ${METADATA_DIR}/*.qmd 2>/dev/null | wc -l`) || "0";
300
+ return { content: [{ type: "text", text: `当前表情包库中共索引了 ${count.trim()} 张表情包。` }] };
301
+ }
302
+ });
303
+
304
+ api.registerTool({
305
+ name: "search_sticker_by_emotion",
306
+ emoji: "🔎",
307
+ description: "通过语义(情感、动作、特征)搜索表情包库,并返回匹配的 sticker_id,用于随后通过 message(action=sticker) 发送。\n构建 query 时,请使用具体的情绪、动作、视觉特征的中文词汇组合(如 '开心 笑着 跑' 或 '无奈 叹气 摆烂')。",
308
+ parameters: {
309
+ type: "object",
310
+ properties: {
311
+ query: {
312
+ type: "string",
313
+ description: "表情包的语义搜索词(例如:'开心 笑着 跑')"
314
+ }
315
+ },
316
+ required: ["query"]
317
+ },
318
+ async execute(id, params, context) {
319
+ api.logger.info(`[Stickers] Semantic search for: "${params.query}"`);
320
+ const searchStart = Date.now();
321
+ let results = [];
322
+
323
+ try {
324
+ const embCtx = await ensureVectorModel();
325
+ const database = ensureDB();
326
+ if (!database || !embCtx) {
327
+ return { content: [{ type: "text", text: "表情包系统未就绪(数据库或模型加载失败),请稍后再试或检查日志。" }] };
328
+ }
329
+
330
+ // 1. Generate Embedding in-memory
331
+ const embedding = await embCtx.getEmbeddingFor(params.query);
332
+ const vecJson = JSON.stringify(Array.from(embedding.vector));
333
+
334
+ // 2. Query SQLite
335
+ const sql = `
336
+ SELECT d.title, v.distance
337
+ FROM vectors_vec v
338
+ JOIN documents d ON v.hash_seq = (d.hash || '_0')
339
+ WHERE v.embedding MATCH ? AND v.k = 10
340
+ ORDER BY v.distance ASC
341
+ `;
342
+
343
+ results = await new Promise((resolve, reject) => {
344
+ database.all(sql, [vecJson], (err, rows) => {
345
+ if (err) reject(err); else resolve(rows);
346
+ });
347
+ });
348
+
349
+ const queryDuration = Date.now() - searchStart;
350
+ api.logger.info(`[Stickers] Search for "${params.query}" took ${queryDuration}ms`);
351
+
352
+ if (!results || results.length === 0) {
353
+ return { content: [{ type: "text", text: "未找到语义匹配的表情包,请更换关键词重试。" }] };
354
+ }
355
+
356
+ // --- Temperature-based random sampling ---
357
+ const temperature = 0.5; // Higher = more random, Lower = strictly top 1
358
+
359
+ // 1. Calculate unnormalized weights
360
+ const weights = results.map(r => {
361
+ let similarity = Math.max(0, 1 - (r.distance / 2));
362
+ return Math.exp(similarity / temperature);
363
+ });
364
+
365
+ // 2. Calculate total sum of weights
366
+ const totalWeight = weights.reduce((sum, w) => sum + w, 0);
367
+
368
+ // 3. Roll the dice [0, totalWeight)
369
+ let randomVal = Math.random() * totalWeight;
370
+ let pickedIndex = 0;
371
+
372
+ for (let i = 0; i < weights.length; i++) {
373
+ randomVal -= weights[i];
374
+ if (randomVal <= 0) {
375
+ pickedIndex = i;
376
+ break;
377
+ }
378
+ }
379
+
380
+ const picked = results[pickedIndex];
381
+ api.logger.info(`[Stickers] Selected #${pickedIndex+1} (${picked.title}) from ${results.length} candidates`);
382
+ const qmdPath = path.join(METADATA_DIR, `${picked.title}.qmd`);
383
+
384
+ if (!fs.existsSync(qmdPath)) throw new Error(`Metadata file missing: ${qmdPath}`);
385
+ const fileContent = fs.readFileSync(qmdPath, 'utf8');
386
+ const fileIdMatch = fileContent.match(/file_id: "(.*?)"/);
387
+ const fileId = fileIdMatch ? fileIdMatch[1] : null;
388
+
389
+ if (!fileId) return { content: [{ type: "text", text: "无法从索引中解析 file_id。" }] };
390
+
391
+ return { content: [{ type: "text", text: `{"sticker_id": "${fileId}"}` }] };
392
+
393
+ } catch (e) {
394
+ api.logger.error(`[Stickers] Search error: ${e.message}`);
395
+ return { content: [{ type: "text", text: `搜索失败: ${e.message}` }] };
396
+ }
397
+ }
398
+ });
399
+ })();
400
+ };
@@ -0,0 +1,47 @@
1
+ {
2
+ "id": "telegram-stickers",
3
+ "name": "Telegram Stickers Brain",
4
+ "description": "Semantic Telegram sticker management with VLM captioning and QMD vector indexing",
5
+ "version": "3.3.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "vlmModel": {
11
+ "type": "string",
12
+ "default": "gemini-3.1-flash-lite-preview",
13
+ "description": "Model used for background sticker captioning"
14
+ },
15
+ "vlmApiKey": {
16
+ "type": "string",
17
+ "description": "Gemini API Key for VLM captioning"
18
+ },
19
+ "autoCollect": {
20
+ "type": "boolean",
21
+ "default": true,
22
+ "description": "Whether to automatically steal new sticker sets when seen"
23
+ },
24
+ "notifyChatId": {
25
+ "type": "string",
26
+ "description": "If set, sends a message to this Chat ID when sync starts/finishes"
27
+ }
28
+ }
29
+ },
30
+ "uiHints": {
31
+ "vlmApiKey": {
32
+ "label": "Gemini API Key",
33
+ "sensitive": true
34
+ },
35
+ "vlmModel": {
36
+ "label": "VLM Model Name",
37
+ "placeholder": "gemini-3.1-flash-lite-preview"
38
+ },
39
+ "autoCollect": {
40
+ "label": "Auto Collect New Sets"
41
+ },
42
+ "notifyChatId": {
43
+ "label": "Admin Chat ID (for notifications)",
44
+ "placeholder": "e.g. 123456789"
45
+ }
46
+ }
47
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@roitium/telegram-stickers-brain",
3
+ "version": "2.0.0",
4
+ "description": "Telegram Stickers Plugin with auto-collection and load balancing",
5
+ "main": "index.js",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./index.js"
9
+ ]
10
+ },
11
+ "keywords": [
12
+ "telegram",
13
+ "stickers",
14
+ "emoji",
15
+ "load-balancing",
16
+ "openclaw-plugin"
17
+ ],
18
+ "author": "Mashiro",
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@google/generative-ai": "^0.24.1",
22
+ "node-llama-cpp": "^3.17.1",
23
+ "sqlite-vec": "^0.1.7-alpha.2",
24
+ "sqlite3": "^5.1.7"
25
+ },
26
+ "engines": {
27
+ "node": ">=16.0.0"
28
+ },
29
+ "files": [
30
+ "index.js",
31
+ "describe_sticker.js",
32
+ "openclaw.plugin.json",
33
+ "README.md",
34
+ "package.json"
35
+ ]
36
+ }