opencode-zombie-monitor 1.0.2 → 1.1.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/README.md +58 -97
- package/index.mjs +55 -25
- package/package.json +2 -2
- package/download-emoji.mjs +0 -115
package/README.md
CHANGED
|
@@ -1,153 +1,114 @@
|
|
|
1
|
-
# 🧟
|
|
1
|
+
# 🧟 opencode-zombie-monitor
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/opencode-zombie-monitor)
|
|
4
|
+
[](https://opencode.ai)
|
|
5
|
+
[](./LICENSE)
|
|
6
6
|
|
|
7
7
|
> *"The only good zombie is a dead zombie"* 💀
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
OpenCode plugin that hunts down and eliminates orphaned processes devouring your RAM.
|
|
10
10
|
|
|
11
11
|
## 😱 The Horror Story
|
|
12
12
|
|
|
13
13
|
You're coding happily with OpenCode. You close a terminal tab. Life goes on...
|
|
14
14
|
|
|
15
|
-
**BUT WAIT!** The process didn't die. It's still there. Lurking.
|
|
15
|
+
**BUT WAIT!** The process didn't die. It's still there. Lurking. Eating your RAM. And every time you close a tab without pressing `q`... another zombie rises.
|
|
16
16
|
|
|
17
|
-
Before you know it:
|
|
18
17
|
```
|
|
19
18
|
$ ps aux | grep opencode
|
|
20
|
-
opencode ??
|
|
21
|
-
opencode ??
|
|
22
|
-
opencode ??
|
|
23
|
-
|
|
24
|
-
... 💀 YOUR RAM IS GONE 💀
|
|
19
|
+
opencode ?? S 156MB
|
|
20
|
+
opencode ?? S 143MB
|
|
21
|
+
opencode ?? S 98MB
|
|
22
|
+
... 💀 your RAM is gone
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
Those `??` = no TTY attached = zombie 🧟
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- 🔍 **Detects** zombie processes (no TTY = undead)
|
|
32
|
-
- 🧟 **Kills** them on sight (every message you send)
|
|
33
|
-
- 📢 **Reports** the kills (without wasting LLM tokens)
|
|
34
|
-
- 🧹 **Keeps** your system clean automatically
|
|
35
|
-
|
|
36
|
-
## 📦 Installation
|
|
37
|
-
|
|
38
|
-
Add to your `opencode.json`:
|
|
27
|
+
## 📦 Install
|
|
39
28
|
|
|
40
29
|
```json
|
|
41
30
|
{
|
|
42
|
-
"plugin": ["
|
|
31
|
+
"plugin": ["opencode-zombie-monitor"]
|
|
43
32
|
}
|
|
44
33
|
```
|
|
45
34
|
|
|
46
|
-
Restart OpenCode. The hunt begins
|
|
35
|
+
Restart OpenCode. The hunt begins 🎯
|
|
47
36
|
|
|
48
|
-
##
|
|
37
|
+
## 🔫 How it works
|
|
49
38
|
|
|
50
|
-
|
|
39
|
+
Every message you send → plugin scans for zombies → calculates their RAM → kills them → reports exact numbers:
|
|
51
40
|
|
|
52
|
-
Just chat normally. The plugin silently patrols your system and eliminates zombies on every message.
|
|
53
|
-
|
|
54
|
-
When zombies are neutralized, you'll see:
|
|
55
41
|
```
|
|
56
|
-
🧟 Killed 3 zombie
|
|
42
|
+
🧟 Killed 3 zombie processes | Freed 397MB RAM | Headshot! 💥
|
|
57
43
|
```
|
|
58
44
|
|
|
59
|
-
|
|
45
|
+
No tokens wasted - notification goes via `ignored` message.
|
|
46
|
+
|
|
47
|
+
## 🎮 Commands
|
|
60
48
|
|
|
61
|
-
|
|
49
|
+
| Command | What |
|
|
50
|
+
|---------|------|
|
|
51
|
+
| `/zombies` | check status |
|
|
52
|
+
| `/kill-zombies` | manual headshot 💥 |
|
|
62
53
|
|
|
63
54
|
```
|
|
64
55
|
/zombies
|
|
56
|
+
✅ 2 processes, no zombies
|
|
65
57
|
```
|
|
66
58
|
|
|
67
|
-
Response when all clear:
|
|
68
59
|
```
|
|
69
|
-
|
|
60
|
+
/zombies
|
|
61
|
+
🧟 3 zombies of 5 processes | 284MB RAM | /kill-zombies
|
|
70
62
|
```
|
|
71
63
|
|
|
72
|
-
Response when trouble brewing:
|
|
73
64
|
```
|
|
74
|
-
|
|
65
|
+
/kill-zombies
|
|
66
|
+
💥 Headshot! Killed 3 zombies | Freed 284MB RAM
|
|
75
67
|
```
|
|
76
68
|
|
|
77
|
-
## ⚙️
|
|
78
|
-
|
|
79
|
-
Edit `index.mjs` to adjust aggression level:
|
|
69
|
+
## ⚙️ Config
|
|
80
70
|
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// PACIFIST MODE: Only notify, never kill
|
|
89
|
-
const AUTO_KILL_THRESHOLD = 999
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"plugin": [
|
|
74
|
+
["opencode-zombie-monitor", { "autoKill": false }]
|
|
75
|
+
]
|
|
76
|
+
}
|
|
90
77
|
```
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
| macOS | ✅ Ready to hunt | `??` |
|
|
97
|
-
| Linux | ✅ Ready to hunt | `?` |
|
|
98
|
-
| Windows | ❌ Zombies win | N/A |
|
|
99
|
-
|
|
100
|
-
## 🌍 Language Support
|
|
101
|
-
|
|
102
|
-
Auto-detects your language. Because zombies are international.
|
|
103
|
-
|
|
104
|
-
| Language | Detection | Example |
|
|
105
|
-
|----------|-----------|---------|
|
|
106
|
-
| 🇬🇧 English | default | "Killed 3 zombie processes" |
|
|
107
|
-
| 🇷🇺 Russian | `LANG=ru*` | "Убито 3 зомби-процессов" |
|
|
108
|
-
| 🇨🇳 Chinese | `LANG=zh*` | "已击杀 3 个僵尸进程" |
|
|
109
|
-
|
|
110
|
-
## 🔬 How It Works
|
|
79
|
+
| Option | Default | What |
|
|
80
|
+
|--------|---------|------|
|
|
81
|
+
| `autoKill` | `true` | auto-kill or just notify |
|
|
82
|
+
| `threshold` | `1` | min zombies to trigger |
|
|
111
83
|
|
|
84
|
+
**RAMBO MODE** (default) - kill on sight:
|
|
85
|
+
```json
|
|
86
|
+
{ "autoKill": true, "threshold": 1 }
|
|
112
87
|
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
┌─────────────────────────────────────────┐
|
|
118
|
-
│ Plugin runs: ps aux | grep opencode │
|
|
119
|
-
└─────────────────┬───────────────────────┘
|
|
120
|
-
▼
|
|
121
|
-
┌─────────────────────────────────────────┐
|
|
122
|
-
│ Filter: TTY == "??" (no terminal) │
|
|
123
|
-
│ These are the zombies 🧟 │
|
|
124
|
-
└─────────────────┬───────────────────────┘
|
|
125
|
-
▼
|
|
126
|
-
┌─────────────────────────────────────────┐
|
|
127
|
-
│ Execute: kill -9 <zombie_pids> │
|
|
128
|
-
│ Headshot! 💥 │
|
|
129
|
-
└─────────────────┬───────────────────────┘
|
|
130
|
-
▼
|
|
131
|
-
┌─────────────────────────────────────────┐
|
|
132
|
-
│ Notify via "ignored" message │
|
|
133
|
-
│ (Free! No LLM tokens used) │
|
|
134
|
-
└─────────────────────────────────────────┘
|
|
88
|
+
|
|
89
|
+
**MANUAL MODE** - you pull the trigger:
|
|
90
|
+
```json
|
|
91
|
+
{ "autoKill": false }
|
|
135
92
|
```
|
|
136
93
|
|
|
137
|
-
##
|
|
94
|
+
## 🖥️ Platforms
|
|
95
|
+
|
|
96
|
+
| Platform | Status |
|
|
97
|
+
|----------|--------|
|
|
98
|
+
| macOS | ✅ hunting |
|
|
99
|
+
| Linux | ✅ hunting |
|
|
100
|
+
| Windows | ❌ zombies win |
|
|
138
101
|
|
|
139
|
-
|
|
102
|
+
## 🌍 Languages
|
|
140
103
|
|
|
141
|
-
|
|
104
|
+
Auto-detects from `LANG`: EN, RU, ZH.
|
|
142
105
|
|
|
143
106
|
## 📜 License
|
|
144
107
|
|
|
145
|
-
MIT
|
|
108
|
+
MIT - use it, fork it, kill zombies with it.
|
|
146
109
|
|
|
147
110
|
---
|
|
148
111
|
|
|
149
112
|
<p align="center">
|
|
150
|
-
|
|
151
|
-
<br><br>
|
|
152
|
-
<b>From Russia with <img src="./assets/russia-heart.png" width="20" height="20" alt="love"></b>
|
|
113
|
+
From Russia with <img src="./assets/russia-heart.png" width="16" height="16" alt="love">
|
|
153
114
|
</p>
|
package/index.mjs
CHANGED
|
@@ -24,28 +24,34 @@ const formatMB = (mb) => mb >= 1024 ? `${(mb / 1024).toFixed(1)}GB` : `${Math.ro
|
|
|
24
24
|
// Localized messages
|
|
25
25
|
const i18n = {
|
|
26
26
|
en: {
|
|
27
|
-
killed: (n, mb) => `🧟 Killed ${n} zombie
|
|
28
|
-
found: (n, mb) => `🧟 ${n} zombie
|
|
27
|
+
killed: (n, mb) => `🧟 Killed ${n} zombie process${n > 1 ? "es" : ""} | Freed ${formatMB(mb)} RAM | Headshot! 💥`,
|
|
28
|
+
found: (n, mb) => `🧟 ${n} zombie process${n > 1 ? "es" : ""} | ${formatMB(mb)} RAM | /kill-zombies`,
|
|
29
29
|
status: (zombies, total, mb) => zombies > 0
|
|
30
|
-
? `🧟 ${zombies} zombie${zombies > 1 ? "s" : ""} of ${total} process${total > 1 ? "es" : ""} | ${formatMB(mb)} RAM |
|
|
30
|
+
? `🧟 ${zombies} zombie${zombies > 1 ? "s" : ""} of ${total} process${total > 1 ? "es" : ""} | ${formatMB(mb)} RAM | /kill-zombies`
|
|
31
31
|
: `✅ ${total} process${total > 1 ? "es" : ""}, no zombies`,
|
|
32
|
-
commandDesc: "Check zombie opencode processes"
|
|
32
|
+
commandDesc: "Check zombie opencode processes",
|
|
33
|
+
killDesc: "Kill zombie opencode processes",
|
|
34
|
+
manualKilled: (n, mb) => `💥 Headshot! Killed ${n} zombie${n > 1 ? "s" : ""} | Freed ${formatMB(mb)} RAM`
|
|
33
35
|
},
|
|
34
36
|
ru: {
|
|
35
|
-
killed: (n, mb) => `🧟 Убито ${n}
|
|
36
|
-
found: (n, mb) => `🧟 ${n}
|
|
37
|
+
killed: (n, mb) => `🧟 Убито ${n} зомби-процесс${n > 1 ? "ов" : ""} | Освобождено ${formatMB(mb)} RAM | Headshot! 💥`,
|
|
38
|
+
found: (n, mb) => `🧟 ${n} зомби-процесс${n > 1 ? "ов" : ""} | ${formatMB(mb)} RAM | /kill-zombies`,
|
|
37
39
|
status: (zombies, total, mb) => zombies > 0
|
|
38
|
-
? `🧟 ${zombies} зомби из ${total} процессов | ${formatMB(mb)} RAM |
|
|
40
|
+
? `🧟 ${zombies} зомби из ${total} процессов | ${formatMB(mb)} RAM | /kill-zombies`
|
|
39
41
|
: `✅ ${total} процессов, зомби нет`,
|
|
40
|
-
commandDesc: "Проверить зомби-процессы opencode"
|
|
42
|
+
commandDesc: "Проверить зомби-процессы opencode",
|
|
43
|
+
killDesc: "Убить зомби-процессы opencode",
|
|
44
|
+
manualKilled: (n, mb) => `💥 Headshot! Убито ${n} зомби | Освобождено ${formatMB(mb)} RAM`
|
|
41
45
|
},
|
|
42
46
|
zh: {
|
|
43
|
-
killed: (n, mb) => `🧟 已击杀 ${n}
|
|
44
|
-
found: (n, mb) => `🧟 发现 ${n}
|
|
47
|
+
killed: (n, mb) => `🧟 已击杀 ${n} 个僵尸进程 | 释放 ${formatMB(mb)} 内存 | Headshot! 💥`,
|
|
48
|
+
found: (n, mb) => `🧟 发现 ${n} 个僵尸进程 | ${formatMB(mb)} 内存 | /kill-zombies`,
|
|
45
49
|
status: (zombies, total, mb) => zombies > 0
|
|
46
|
-
? `🧟 ${total} 个进程中有 ${zombies} 个僵尸 | ${formatMB(mb)} 内存 |
|
|
50
|
+
? `🧟 ${total} 个进程中有 ${zombies} 个僵尸 | ${formatMB(mb)} 内存 | /kill-zombies`
|
|
47
51
|
: `✅ ${total} 个进程,没有僵尸`,
|
|
48
|
-
commandDesc: "
|
|
52
|
+
commandDesc: "检查僵尸进程",
|
|
53
|
+
killDesc: "击杀僵尸进程",
|
|
54
|
+
manualKilled: (n, mb) => `💥 Headshot! 已击杀 ${n} 个僵尸 | 释放 ${formatMB(mb)} 内存`
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -85,8 +91,13 @@ const killZombies = async () => {
|
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
//
|
|
89
|
-
|
|
94
|
+
// Configuration defaults
|
|
95
|
+
// autoKill: true = kill automatically, false = notify only (manual mode)
|
|
96
|
+
// threshold: minimum zombies to trigger action (1 = immediate)
|
|
97
|
+
const DEFAULT_CONFIG = {
|
|
98
|
+
autoKill: true,
|
|
99
|
+
threshold: 1
|
|
100
|
+
}
|
|
90
101
|
|
|
91
102
|
const sendNotification = async (client, sessionId, text) => {
|
|
92
103
|
await client.session.prompt({
|
|
@@ -107,8 +118,11 @@ const getSessionIdFromMessages = (messages) => {
|
|
|
107
118
|
return null
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
export const ZombieMonitor = async ({ client }) => {
|
|
121
|
+
export const ZombieMonitor = async ({ client, config: pluginConfig }) => {
|
|
111
122
|
let lastNotifiedCount = 0
|
|
123
|
+
|
|
124
|
+
// Merge user config with defaults
|
|
125
|
+
const cfg = { ...DEFAULT_CONFIG, ...pluginConfig }
|
|
112
126
|
|
|
113
127
|
return {
|
|
114
128
|
config: async (opencodeConfig) => {
|
|
@@ -117,6 +131,10 @@ export const ZombieMonitor = async ({ client }) => {
|
|
|
117
131
|
template: "",
|
|
118
132
|
description: t.commandDesc
|
|
119
133
|
}
|
|
134
|
+
opencodeConfig.command["kill-zombies"] = {
|
|
135
|
+
template: "",
|
|
136
|
+
description: t.killDesc
|
|
137
|
+
}
|
|
120
138
|
},
|
|
121
139
|
|
|
122
140
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
@@ -125,15 +143,15 @@ export const ZombieMonitor = async ({ client }) => {
|
|
|
125
143
|
|
|
126
144
|
const { count: zombies, mb } = await getZombieStats()
|
|
127
145
|
|
|
128
|
-
// Auto-kill if zombies >= threshold
|
|
129
|
-
if (zombies >=
|
|
146
|
+
// Auto-kill if enabled and zombies >= threshold
|
|
147
|
+
if (cfg.autoKill && zombies >= cfg.threshold) {
|
|
130
148
|
await killZombies()
|
|
131
149
|
try {
|
|
132
150
|
await sendNotification(client, sessionId, t.killed(zombies, mb))
|
|
133
151
|
} catch (e) {}
|
|
134
152
|
lastNotifiedCount = 0
|
|
135
153
|
}
|
|
136
|
-
// Notify if zombies appeared (
|
|
154
|
+
// Notify if zombies appeared (manual mode or below threshold)
|
|
137
155
|
else if (zombies > 0 && zombies > lastNotifiedCount) {
|
|
138
156
|
lastNotifiedCount = zombies
|
|
139
157
|
try {
|
|
@@ -145,13 +163,25 @@ export const ZombieMonitor = async ({ client }) => {
|
|
|
145
163
|
},
|
|
146
164
|
|
|
147
165
|
"command.execute.before": async (input) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
// /zombies - check status
|
|
167
|
+
if (input.command === "zombies") {
|
|
168
|
+
const { count: zombies, mb } = await getZombieStats()
|
|
169
|
+
const total = await getTotalCount()
|
|
170
|
+
await sendNotification(client, input.sessionID, t.status(zombies, total, mb))
|
|
171
|
+
throw new Error("__ZOMBIES_HANDLED__")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// /kill-zombies - manual kill
|
|
175
|
+
if (input.command === "kill-zombies") {
|
|
176
|
+
const { count: zombies, mb } = await getZombieStats()
|
|
177
|
+
if (zombies > 0) {
|
|
178
|
+
await killZombies()
|
|
179
|
+
await sendNotification(client, input.sessionID, t.manualKilled(zombies, mb))
|
|
180
|
+
} else {
|
|
181
|
+
await sendNotification(client, input.sessionID, t.status(0, await getTotalCount(), 0))
|
|
182
|
+
}
|
|
183
|
+
throw new Error("__ZOMBIES_HANDLED__")
|
|
184
|
+
}
|
|
155
185
|
}
|
|
156
186
|
}
|
|
157
187
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-zombie-monitor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Auto-kill zombie opencode processes. Supports macOS and Linux with auto language detection (EN/RU).",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/
|
|
12
|
+
"url": "https://github.com/Matroskin86/opencode-zombie-monitor"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@opencode-ai/plugin": ">=1.0.0"
|
package/download-emoji.mjs
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Скрипт для скачивания Telegram custom emoji
|
|
3
|
-
*
|
|
4
|
-
* 1. Отправь эмодзи боту @your_bot
|
|
5
|
-
* 2. Запусти: node download-emoji.mjs
|
|
6
|
-
* 3. Эмодзи сохранится в ./assets/emoji.webp
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import https from 'https'
|
|
10
|
-
import fs from 'fs'
|
|
11
|
-
import path from 'path'
|
|
12
|
-
|
|
13
|
-
const BOT_TOKEN = '8061734285:AAHeLB0YHXomdjPPyTg18GM0_RrdvTbp4aA'
|
|
14
|
-
const API_URL = `https://api.telegram.org/bot${BOT_TOKEN}`
|
|
15
|
-
|
|
16
|
-
async function fetchJson(url) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
https.get(url, (res) => {
|
|
19
|
-
let data = ''
|
|
20
|
-
res.on('data', chunk => data += chunk)
|
|
21
|
-
res.on('end', () => resolve(JSON.parse(data)))
|
|
22
|
-
}).on('error', reject)
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function downloadFile(filePath, destPath) {
|
|
27
|
-
const fileUrl = `https://api.telegram.org/file/bot${BOT_TOKEN}/${filePath}`
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
const file = fs.createWriteStream(destPath)
|
|
30
|
-
https.get(fileUrl, (res) => {
|
|
31
|
-
res.pipe(file)
|
|
32
|
-
file.on('finish', () => {
|
|
33
|
-
file.close()
|
|
34
|
-
resolve(destPath)
|
|
35
|
-
})
|
|
36
|
-
}).on('error', reject)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function main() {
|
|
41
|
-
console.log('🔍 Получаю последние сообщения...')
|
|
42
|
-
|
|
43
|
-
// Получить updates
|
|
44
|
-
const updates = await fetchJson(`${API_URL}/getUpdates?limit=10`)
|
|
45
|
-
|
|
46
|
-
if (!updates.ok || !updates.result.length) {
|
|
47
|
-
console.log('❌ Нет сообщений. Отправь эмодзи боту и попробуй снова.')
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Найти сообщение с custom emoji
|
|
52
|
-
let customEmojiId = null
|
|
53
|
-
let messageText = null
|
|
54
|
-
|
|
55
|
-
for (const update of updates.result.reverse()) {
|
|
56
|
-
const msg = update.message
|
|
57
|
-
if (msg?.entities) {
|
|
58
|
-
for (const entity of msg.entities) {
|
|
59
|
-
if (entity.type === 'custom_emoji') {
|
|
60
|
-
customEmojiId = entity.custom_emoji_id
|
|
61
|
-
messageText = msg.text
|
|
62
|
-
break
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (customEmojiId) break
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!customEmojiId) {
|
|
70
|
-
console.log('❌ Custom emoji не найден. Отправь эмодзи из платного/премиум пака.')
|
|
71
|
-
console.log('📋 Последние сообщения:', updates.result.map(u => u.message?.text).filter(Boolean))
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
console.log(`✅ Найден emoji: "${messageText}" (ID: ${customEmojiId})`)
|
|
76
|
-
|
|
77
|
-
// Получить стикер по emoji ID
|
|
78
|
-
console.log('📥 Получаю файл стикера...')
|
|
79
|
-
const stickers = await fetchJson(`${API_URL}/getCustomEmojiStickers?custom_emoji_ids=["${customEmojiId}"]`)
|
|
80
|
-
|
|
81
|
-
if (!stickers.ok || !stickers.result.length) {
|
|
82
|
-
console.log('❌ Не удалось получить стикер:', stickers)
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const sticker = stickers.result[0]
|
|
87
|
-
console.log(`📦 Стикер: ${sticker.file_id}`)
|
|
88
|
-
|
|
89
|
-
// Получить путь к файлу
|
|
90
|
-
const fileInfo = await fetchJson(`${API_URL}/getFile?file_id=${sticker.file_id}`)
|
|
91
|
-
|
|
92
|
-
if (!fileInfo.ok) {
|
|
93
|
-
console.log('❌ Не удалось получить файл:', fileInfo)
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Создать папку assets
|
|
98
|
-
const assetsDir = './assets'
|
|
99
|
-
if (!fs.existsSync(assetsDir)) {
|
|
100
|
-
fs.mkdirSync(assetsDir)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Скачать файл
|
|
104
|
-
const ext = path.extname(fileInfo.result.file_path) || '.webp'
|
|
105
|
-
const destPath = `${assetsDir}/emoji${ext}`
|
|
106
|
-
|
|
107
|
-
console.log(`💾 Скачиваю в ${destPath}...`)
|
|
108
|
-
await downloadFile(fileInfo.result.file_path, destPath)
|
|
109
|
-
|
|
110
|
-
console.log(`✅ Готово! Файл сохранён: ${destPath}`)
|
|
111
|
-
console.log(`\n📝 Для README используй:`)
|
|
112
|
-
console.log(`<img src="${destPath}" width="20" height="20">`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
main().catch(console.error)
|