@link-assistant/hive-mind 1.74.5 → 1.74.7
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/CHANGELOG.md +14 -0
- package/README.hi.md +22 -6
- package/README.md +22 -6
- package/README.ru.md +35 -19
- package/README.zh.md +21 -6
- package/package.json +1 -1
- package/src/telegram-auth-command.lib.mjs +298 -0
- package/src/telegram-bot.mjs +29 -23
- package/src/telegram-safe-reply.lib.mjs +274 -38
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.74.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8ea7110: Document the issue #1858 case study and add an experimental private Telegram
|
|
8
|
+
`/auth` command for allowlisted chat owners to check or start GitHub, Claude,
|
|
9
|
+
and Codex auth flows.
|
|
10
|
+
|
|
11
|
+
## 1.74.6
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- e07b243: Split oversized Telegram text messages in the safe reply/edit helper so localized `/help` output cannot exceed Telegram's 4096-character limit.
|
|
16
|
+
|
|
3
17
|
## 1.74.5
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -29,14 +29,19 @@ Hive Mind एक **सामान्यवादी AI** (मिनी-AGI) ह
|
|
|
29
29
|
| **पूर्व-स्थापित टूलचेन** | 25GB+ तैयार: 10 भाषा रनटाइम, 2 थ्योरम प्रूवर, बिल्ड टूल्स। और इंस्टॉल कर सकते हैं। |
|
|
30
30
|
| **टोकन दक्षता** | नियमित कार्य कोड में स्वचालित, ताकि AI टोकन रचनात्मक समस्या-समाधान पर केंद्रित रहें। |
|
|
31
31
|
| **समय की स्वतंत्रता** | जो काम मनुष्यों को 2-8 घंटे लगता है, AI प्रत्येक कार्य सत्र में 10-25 मिनट में पूरा करता है। रिपॉजिटरी में कार्यों का बड़े पैमाने पर निष्पादन संभव है। "सोते समय कोड लिखा जाता है।" |
|
|
32
|
-
| **ऑर्केस्ट्रेशन के साथ स्केल** | समानांतर वर्कर डेवलपर्स की एक टीम की तरह महसूस होते
|
|
32
|
+
| **ऑर्केस्ट्रेशन के साथ स्केल** | समानांतर वर्कर डेवलपर्स की एक टीम की तरह महसूस होते हैं। Claude MAX और ChatGPT Pro (प्रत्येक $200) को जोड़कर दो स्वतंत्र लगभग असीमित बजट मिलते हैं। |
|
|
33
33
|
| **मानव नियंत्रण** | AI ड्राफ्ट PR बनाता है - आप तय करते हैं क्या मर्ज होगा। जहाँ मायने रखता है वहाँ गुणवत्ता द्वार। |
|
|
34
34
|
| **किसी भी डिवाइस से प्रोग्रामिंग** | Telegram बॉट के माध्यम से `/solve` और `/hive` से किसी भी डिवाइस से AI प्रबंधित करें। कोई PC, IDE या लैपटॉप की आवश्यकता नहीं। |
|
|
35
35
|
| **100% ओपन सोर्स** | Unlicense (पब्लिक डोमेन)। पूर्ण पारदर्शिता, कोई वेंडर लॉक-इन नहीं। |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**लागत**: Hive Mind दो $200/माह सदस्यताओं को पूर्ण-फीचर वाले लगभग "unlimited" विकल्पों के रूप में समर्थन देता है:
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
| सदस्यता | `--tool` के साथ | डिफ़ॉल्ट मॉडल | किसके लिए बेहतर है |
|
|
40
|
+
| ---------------------------------------------------------------- | ------------------- | ------------- | ------------------------------------------------- |
|
|
41
|
+
| **Anthropic Claude MAX** (~$200/माह, अक्सर 50% छूट = $400 मूल्य) | `claude` (डिफ़ॉल्ट) | Sonnet/Haiku | उच्चतम रचनात्मकता, मजबूत सामान्य कोड रीजनिंग |
|
|
42
|
+
| **OpenAI ChatGPT Pro** ($200/माह, Codex शामिल) | `codex` | `gpt-5.5` | भरोसेमंद deterministic refactors और तेज iteration |
|
|
43
|
+
|
|
44
|
+
दोनों टूल एक ही hive में साथ उपयोग किए जा सकते हैं। Worker अलग-अलग टूल समानांतर चला सकते हैं, और `/codex` या `/solve --tool codex` कार्यों को ChatGPT Pro पर भेजता है जबकि डिफ़ॉल्ट Claude MAX पर जाता है। किसी एक को चुनना आवश्यक नहीं है: किसी भी एक सदस्यता से संचालन संभव है, और दोनों का उपयोग per-tool/model concurrency mode (#1474) खोलता है।
|
|
40
45
|
|
|
41
46
|
Hive Mind में औसत प्रोग्रामर से अलग न पहचानी जा सकने वाली उच्च रचनात्मकता है। यदि आवश्यकताएँ अस्पष्ट हों तो यह प्रश्न पूछता है, और आप PR टिप्पणियों के माध्यम से चलते-चलते स्पष्ट कर सकते हैं।
|
|
42
47
|
|
|
@@ -48,7 +53,7 @@ Hive Mind में औसत प्रोग्रामर से अलग
|
|
|
48
53
|
|
|
49
54
|
इंस्टॉलेशन के लिए Docker का उपयोग करना अनुशंसित है (स्थानीय और सर्वर दोनों पर)। नीचे [Docker इंस्टॉलेशन](#using-docker) अनुभाग देखें।
|
|
50
55
|
|
|
51
|
-
यह सॉफ़्टवेयर Claude Code
|
|
56
|
+
यह सॉफ़्टवेयर Claude Code और Codex जैसे समर्थित AI टूल को पूर्ण स्वायत्त मोड में चलाता है, जिसका अर्थ है कि वे जो भी उचित समझें वे कमांड निष्पादित करने के लिए स्वतंत्र हैं।
|
|
52
57
|
|
|
53
58
|
इसका मतलब है कि इससे अप्रत्याशित दुष्प्रभाव हो सकते हैं।
|
|
54
59
|
|
|
@@ -60,7 +65,7 @@ Hive Mind में औसत प्रोग्रामर से अलग
|
|
|
60
65
|
|
|
61
66
|
इंटरनेट से जुड़ी वर्चुअल मशीन से टोकन निकालने के असंख्य तरीके हैं। इसमें शामिल हैं लेकिन इन तक सीमित नहीं:
|
|
62
67
|
|
|
63
|
-
- **Claude MAX टोकन** - AI संचालन के लिए
|
|
68
|
+
- **Claude MAX टोकन** और/या **ChatGPT Pro (Codex) टोकन** - AI संचालन के लिए आवश्यक; आप इनमें से किसी एक या दोनों के साथ चला सकते हैं
|
|
64
69
|
- **GitHub टोकन** - रिपॉजिटरी एक्सेस के लिए आवश्यक
|
|
65
70
|
- **API keys और क्रेडेंशियल** - सिस्टम पर कोई भी संवेदनशील डेटा
|
|
66
71
|
|
|
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
|
|
|
161
166
|
# Inside the container, authenticate with GitHub
|
|
162
167
|
gh-setup-git-identity
|
|
163
168
|
|
|
164
|
-
# Authenticate with Claude
|
|
169
|
+
# Authenticate with Claude (if you have Claude MAX)
|
|
165
170
|
claude
|
|
166
171
|
|
|
167
172
|
# Optionally set configuration like this:
|
|
@@ -174,6 +179,14 @@ claude
|
|
|
174
179
|
# Optionally test Claude connection
|
|
175
180
|
claude -p hi --model haiku
|
|
176
181
|
|
|
182
|
+
# Authenticate with Codex (if you have ChatGPT Pro)
|
|
183
|
+
codex login --device-auth
|
|
184
|
+
|
|
185
|
+
# Optionally test Codex connection. codex exec refuses to run unless
|
|
186
|
+
# either cwd is a git repo it trusts or --skip-git-repo-check is passed.
|
|
187
|
+
# It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
|
|
188
|
+
codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
|
|
189
|
+
|
|
177
190
|
# You might need to update hive-mind and agent to latest versions:
|
|
178
191
|
bun install -g @link-assistant/hive-mind
|
|
179
192
|
bun install -g @link-assistant/agent
|
|
@@ -546,6 +559,7 @@ Shows:
|
|
|
546
559
|
- ✅ **Screen सत्र**: कमांड डिटैच्ड screen सत्रों में चलते हैं
|
|
547
560
|
- ✅ **Live Terminal Watch**: `/terminal_watch` और opt-in auto-start live session logs दिखाते हैं
|
|
548
561
|
- ✅ **चैट प्रतिबंध**: अनुमत चैट ID की वैकल्पिक सफेद सूची
|
|
562
|
+
- ✅ **Private Auth Check**: allowlisted chat owners के लिए experimental `/auth --status <gh|claude|codex>` और `/auth --login <gh|claude|codex>`
|
|
549
563
|
- ✅ **डायग्नोस्टिक टूल**: चैट ID और कॉन्फ़िगरेशन जानकारी प्राप्त करें
|
|
550
564
|
|
|
551
565
|
#### Live Terminal Watch
|
|
@@ -564,6 +578,8 @@ sessions के लिए अपने आप एक अलग live terminal wat
|
|
|
564
578
|
|
|
565
579
|
- केवल उन ग्रुप चैट में काम करता है जहाँ बॉट एडमिन है
|
|
566
580
|
- `TELEGRAM_ALLOWED_CHATS` के माध्यम से वैकल्पिक चैट ID प्रतिबंध
|
|
581
|
+
- private `/auth` तब disabled रहता है जब `TELEGRAM_ALLOWED_CHATS` set नहीं है,
|
|
582
|
+
और इसे केवल listed chats के owners इस्तेमाल कर सकते हैं
|
|
567
583
|
- बॉट चलाने वाले सिस्टम उपयोगकर्ता के रूप में कमांड चलते हैं
|
|
568
584
|
- उचित प्रमाणीकरण सुनिश्चित करें (`gh auth login`, `claude-profiles`)
|
|
569
585
|
|
package/README.md
CHANGED
|
@@ -29,14 +29,19 @@ Hive Mind is a **generalist AI** (mini-AGI) capable of working on a wide range o
|
|
|
29
29
|
| **Pre-installed Toolchain** | 25GB+ ready: 10 language runtimes, 2 theorem provers, build tools. Can install more. |
|
|
30
30
|
| **Token Efficiency** | Routine tasks automated in code, so AI tokens focus on creative problem-solving. |
|
|
31
31
|
| **Time Freedom** | What takes humans 2-8 hours, AI completes each working session in 10-25 minutes. Mass execution of tasks in repository is possible. "The code is written while you sleep." |
|
|
32
|
-
| **Scale with Orchestration** | Parallel workers feel like a team of developers
|
|
32
|
+
| **Scale with Orchestration** | Parallel workers feel like a team of developers. Pair Claude MAX and ChatGPT Pro ($200 each) for two independent unlimited budgets. |
|
|
33
33
|
| **Human Control** | AI creates draft PRs - you decide what merges. Quality gates where they matter. |
|
|
34
34
|
| **Any Device Programming** | Manage AI from any device with `/solve` and `/hive` via Telegram bot. No PC, IDE, or laptop required. |
|
|
35
35
|
| **100% Open Source** | Unlicense (public domain). Full transparency, no vendor lock-in. |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**Cost**: Hive Mind supports two $200/month subscriptions as full-featured "unlimited" options:
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
| Subscription | Pairs with `--tool` | Default model | Best for |
|
|
40
|
+
| ------------------------------------------------------------------ | ------------------- | ------------- | ------------------------------------------------------- |
|
|
41
|
+
| **Anthropic Claude MAX** (~$200/month, often 50% off = $400 value) | `claude` (default) | Sonnet/Haiku | Highest creativity, strongest general code reasoning |
|
|
42
|
+
| **OpenAI ChatGPT Pro** ($200/month, includes Codex) | `codex` | `gpt-5.5` | Strong deterministic refactors and fast iteration loops |
|
|
43
|
+
|
|
44
|
+
Both tools can be combined in the same hive. Workers can run different tools in parallel, and `/codex` or `/solve --tool codex` routes tasks to ChatGPT Pro while the default routes to Claude MAX. There is no requirement to pick one: either single subscription is enough to operate, and using both unlocks per-tool/model concurrency mode (#1474).
|
|
40
45
|
|
|
41
46
|
Hive Mind has high creativity indistinguishable from average programmers. It asks questions if requirements are unclear, and you can clarify on the go via PR comments.
|
|
42
47
|
|
|
@@ -48,7 +53,7 @@ It is UNSAFE to run this software on your developer machine.
|
|
|
48
53
|
|
|
49
54
|
It is recommended to use Docker for installation (both locally and on servers). See the [Docker installation](#using-docker) section below.
|
|
50
55
|
|
|
51
|
-
This software
|
|
56
|
+
This software runs supported AI tools such as Claude Code and Codex in full autonomous mode, which means they are free to execute any commands they see fit.
|
|
52
57
|
|
|
53
58
|
That means it can lead to unexpected side effects.
|
|
54
59
|
|
|
@@ -60,7 +65,7 @@ There is also a known issue of space leakage. So you need to make sure you are a
|
|
|
60
65
|
|
|
61
66
|
There are infinite ways to extract tokens from a virtual machine connected to the internet. This includes but is not limited to:
|
|
62
67
|
|
|
63
|
-
- **Claude MAX tokens** - Required for AI operations
|
|
68
|
+
- **Claude MAX tokens** and/or **ChatGPT Pro (Codex) tokens** - Required for AI operations; you can run with either or both
|
|
64
69
|
- **GitHub tokens** - Required for repository access
|
|
65
70
|
- **API keys and credentials** - Any sensitive data on the system
|
|
66
71
|
|
|
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
|
|
|
161
166
|
# Inside the container, authenticate with GitHub
|
|
162
167
|
gh-setup-git-identity
|
|
163
168
|
|
|
164
|
-
# Authenticate with Claude
|
|
169
|
+
# Authenticate with Claude (if you have Claude MAX)
|
|
165
170
|
claude
|
|
166
171
|
|
|
167
172
|
# Optionally set configuration like this:
|
|
@@ -174,6 +179,14 @@ claude
|
|
|
174
179
|
# Optionally test Claude connection
|
|
175
180
|
claude -p hi --model haiku
|
|
176
181
|
|
|
182
|
+
# Authenticate with Codex (if you have ChatGPT Pro)
|
|
183
|
+
codex login --device-auth
|
|
184
|
+
|
|
185
|
+
# Optionally test Codex connection. codex exec refuses to run unless
|
|
186
|
+
# either cwd is a git repo it trusts or --skip-git-repo-check is passed.
|
|
187
|
+
# It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
|
|
188
|
+
codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
|
|
189
|
+
|
|
177
190
|
# Verify Playwright MCP is registered for both CLIs in this container image
|
|
178
191
|
claude mcp list | grep playwright
|
|
179
192
|
codex mcp list | grep playwright
|
|
@@ -567,6 +580,7 @@ Shows:
|
|
|
567
580
|
- ✅ **Screen Sessions**: Commands run in detached screen sessions
|
|
568
581
|
- ✅ **Live Terminal Watch**: `/terminal_watch` and opt-in auto-start show live session logs
|
|
569
582
|
- ✅ **Chat Restrictions**: Optional whitelist of allowed chat IDs
|
|
583
|
+
- ✅ **Private Auth Check**: Experimental `/auth --status <gh|claude|codex>` and `/auth --login <gh|claude|codex>` for owners of allowlisted chats
|
|
570
584
|
- ✅ **Diagnostic Tools**: Get chat ID and configuration info
|
|
571
585
|
|
|
572
586
|
#### Live Terminal Watch
|
|
@@ -584,6 +598,8 @@ When enabled with `--auto-start-screen-watch-message`, the bot automatically sta
|
|
|
584
598
|
|
|
585
599
|
- Only works in group chats where the bot is admin
|
|
586
600
|
- Optional chat ID restrictions via `TELEGRAM_ALLOWED_CHATS`
|
|
601
|
+
- Private `/auth` is disabled unless `TELEGRAM_ALLOWED_CHATS` is set and only
|
|
602
|
+
owners of listed chats can use it
|
|
587
603
|
- Commands run as the system user running the bot
|
|
588
604
|
- Ensure proper authentication (`gh auth login`, `claude-profiles`)
|
|
589
605
|
|
package/README.ru.md
CHANGED
|
@@ -21,22 +21,27 @@
|
|
|
21
21
|
|
|
22
22
|
Hive Mind — это **универсальный ИИ** (мини-AGI), способный работать над широким спектром задач, а не только над программированием. Практически всё, что можно сделать с файлами в репозитории, поддаётся автоматизации.
|
|
23
23
|
|
|
24
|
-
| Возможность | Что это означает для вас
|
|
25
|
-
| ---------------------------------------- |
|
|
26
|
-
| **Без постоянного контроля** | Полный автономный режим с правами sudo. ИИ обладает творческой свободой, как настоящий программист.
|
|
27
|
-
| **Изоляция в облаке** | Работает на выделенных виртуальных машинах или в Docker. Легко восстановить в случае поломки.
|
|
28
|
-
| **Полный доступ к интернету + Sudo** | ИИ может устанавливать пакеты, получать документацию и настраивать систему по мере необходимости.
|
|
29
|
-
| **Предустановленный инструментарий** | Более 25 ГБ готово к работе: 10 языковых сред выполнения, 2 средства доказательства теорем, инструменты сборки. Можно установить дополнительные.
|
|
30
|
-
| **Эффективность токенов** | Рутинные задачи автоматизированы в коде, поэтому токены ИИ сосредоточены на творческом решении проблем.
|
|
31
|
-
| **Свобода времени** | То, что занимает у людей 2–8 часов, ИИ выполняет за 10–25 минут за рабочую сессию. Возможно массовое выполнение задач в репозитории. «Код пишется, пока вы спите».
|
|
32
|
-
| **Масштабирование с оркестрацией** | Параллельные воркеры ощущаются как команда
|
|
33
|
-
| **Контроль со стороны человека** | ИИ создаёт черновые PR — вы решаете, что вливать. Контроль качества там, где это важно.
|
|
34
|
-
| **Программирование с любого устройства** | Управляйте ИИ с любого устройства через `/solve` и `/hive` посредством Telegram-бота. ПК, IDE или ноутбук не нужны.
|
|
35
|
-
| **100% открытый исходный код** | Лицензия Unlicense (общественное достояние). Полная прозрачность, без привязки к поставщику.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
| Возможность | Что это означает для вас |
|
|
25
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| **Без постоянного контроля** | Полный автономный режим с правами sudo. ИИ обладает творческой свободой, как настоящий программист. |
|
|
27
|
+
| **Изоляция в облаке** | Работает на выделенных виртуальных машинах или в Docker. Легко восстановить в случае поломки. |
|
|
28
|
+
| **Полный доступ к интернету + Sudo** | ИИ может устанавливать пакеты, получать документацию и настраивать систему по мере необходимости. |
|
|
29
|
+
| **Предустановленный инструментарий** | Более 25 ГБ готово к работе: 10 языковых сред выполнения, 2 средства доказательства теорем, инструменты сборки. Можно установить дополнительные. |
|
|
30
|
+
| **Эффективность токенов** | Рутинные задачи автоматизированы в коде, поэтому токены ИИ сосредоточены на творческом решении проблем. |
|
|
31
|
+
| **Свобода времени** | То, что занимает у людей 2–8 часов, ИИ выполняет за 10–25 минут за рабочую сессию. Возможно массовое выполнение задач в репозитории. «Код пишется, пока вы спите». |
|
|
32
|
+
| **Масштабирование с оркестрацией** | Параллельные воркеры ощущаются как команда разработчиков. Свяжите Claude MAX и ChatGPT Pro (по $200 каждый), чтобы получить два независимых почти безлимитных бюджета. |
|
|
33
|
+
| **Контроль со стороны человека** | ИИ создаёт черновые PR — вы решаете, что вливать. Контроль качества там, где это важно. |
|
|
34
|
+
| **Программирование с любого устройства** | Управляйте ИИ с любого устройства через `/solve` и `/hive` посредством Telegram-бота. ПК, IDE или ноутбук не нужны. |
|
|
35
|
+
| **100% открытый исходный код** | Лицензия Unlicense (общественное достояние). Полная прозрачность, без привязки к поставщику. |
|
|
36
|
+
|
|
37
|
+
**Стоимость**: Hive Mind поддерживает две подписки по $200 в месяц как полнофункциональные почти «безлимитные» варианты:
|
|
38
|
+
|
|
39
|
+
| Подписка | Используется с `--tool` | Модель по умолчанию | Лучше всего подходит для |
|
|
40
|
+
| ----------------------------------------------------------------- | ----------------------- | ------------------- | ---------------------------------------------------------- |
|
|
41
|
+
| **Anthropic Claude MAX** (~$200 в месяц, часто скидка 50% = $400) | `claude` (по умолчанию) | Sonnet/Haiku | Максимальная креативность и сильное общее кодовое мышление |
|
|
42
|
+
| **OpenAI ChatGPT Pro** ($200 в месяц, включает Codex) | `codex` | `gpt-5.5` | Надёжные детерминированные рефакторинги и быстрые итерации |
|
|
43
|
+
|
|
44
|
+
Оба инструмента можно сочетать в одном hive. Воркеры могут параллельно запускать разные инструменты, а `/codex` или `/solve --tool codex` направляет задачи в ChatGPT Pro, тогда как маршрут по умолчанию идёт в Claude MAX. Выбирать только один вариант не требуется: любой одной подписки достаточно для работы, а использование обеих открывает режим параллелизма по инструментам/моделям (#1474).
|
|
40
45
|
|
|
41
46
|
Hive Mind обладает высоким уровнем творчества, неотличимым от среднего программиста. Он задаёт вопросы, если требования неясны, и вы можете уточнять их на ходу через комментарии к PR.
|
|
42
47
|
|
|
@@ -48,7 +53,7 @@ Hive Mind обладает высоким уровнем творчества,
|
|
|
48
53
|
|
|
49
54
|
Рекомендуется использовать Docker для установки (как локально, так и на серверах). Смотрите раздел [Установка через Docker](#использование-docker) ниже.
|
|
50
55
|
|
|
51
|
-
Это программное обеспечение
|
|
56
|
+
Это программное обеспечение запускает поддерживаемые ИИ-инструменты, такие как Claude Code и Codex, в полностью автономном режиме, а значит, они могут выполнять любые команды по своему усмотрению.
|
|
52
57
|
|
|
53
58
|
Это может привести к непредвиденным побочным эффектам.
|
|
54
59
|
|
|
@@ -60,7 +65,7 @@ Hive Mind обладает высоким уровнем творчества,
|
|
|
60
65
|
|
|
61
66
|
Существует бесконечное множество способов извлечь токены с виртуальной машины, подключённой к интернету. Это включает, но не ограничивается:
|
|
62
67
|
|
|
63
|
-
- **Токены Claude MAX** — необходимы для работы
|
|
68
|
+
- **Токены Claude MAX** и/или **токены ChatGPT Pro (Codex)** — необходимы для работы ИИ; можно запускать с любым из них или с обоими
|
|
64
69
|
- **Токены GitHub** — необходимы для доступа к репозиториям
|
|
65
70
|
- **API-ключи и учётные данные** — любые конфиденциальные данные в системе
|
|
66
71
|
|
|
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
|
|
|
161
166
|
# Inside the container, authenticate with GitHub
|
|
162
167
|
gh-setup-git-identity
|
|
163
168
|
|
|
164
|
-
# Authenticate with Claude
|
|
169
|
+
# Authenticate with Claude (if you have Claude MAX)
|
|
165
170
|
claude
|
|
166
171
|
|
|
167
172
|
# Optionally set configuration like this:
|
|
@@ -174,6 +179,14 @@ claude
|
|
|
174
179
|
# Optionally test Claude connection
|
|
175
180
|
claude -p hi --model haiku
|
|
176
181
|
|
|
182
|
+
# Authenticate with Codex (if you have ChatGPT Pro)
|
|
183
|
+
codex login --device-auth
|
|
184
|
+
|
|
185
|
+
# Optionally test Codex connection. codex exec refuses to run unless
|
|
186
|
+
# either cwd is a git repo it trusts or --skip-git-repo-check is passed.
|
|
187
|
+
# It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
|
|
188
|
+
codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
|
|
189
|
+
|
|
177
190
|
# You might need to update hive-mind and agent to latest versions:
|
|
178
191
|
bun install -g @link-assistant/hive-mind
|
|
179
192
|
bun install -g @link-assistant/agent
|
|
@@ -548,6 +561,7 @@ Shows:
|
|
|
548
561
|
- ✅ **Screen-сессии**: команды запускаются в отсоединённых screen-сессиях
|
|
549
562
|
- ✅ **Live Terminal Watch**: `/terminal_watch` и opt-in auto-start показывают live session logs
|
|
550
563
|
- ✅ **Ограничения по чатам**: опциональный белый список разрешённых ID чатов
|
|
564
|
+
- ✅ **Приватная проверка auth**: экспериментальные `/auth --status <gh|claude|codex>` и `/auth --login <gh|claude|codex>` для владельцев разрешённых чатов
|
|
551
565
|
- ✅ **Диагностические инструменты**: получение ID чата и информации о конфигурации
|
|
552
566
|
|
|
553
567
|
#### Live Terminal Watch
|
|
@@ -566,6 +580,8 @@ Shows:
|
|
|
566
580
|
|
|
567
581
|
- Работает только в групповых чатах, где бот является администратором
|
|
568
582
|
- Опциональное ограничение по ID чата через `TELEGRAM_ALLOWED_CHATS`
|
|
583
|
+
- Приватная `/auth` отключена, если `TELEGRAM_ALLOWED_CHATS` не задан, и
|
|
584
|
+
доступна только владельцам перечисленных чатов
|
|
569
585
|
- Команды выполняются от имени системного пользователя, запустившего бота
|
|
570
586
|
- Убедитесь в наличии надлежащей аутентификации (`gh auth login`, `claude-profiles`)
|
|
571
587
|
|
package/README.zh.md
CHANGED
|
@@ -29,14 +29,19 @@ Hive Mind 是一款**通用 AI**(迷你 AGI),能够处理广泛的任务
|
|
|
29
29
|
| **预装工具链** | 25GB+ 开箱即用:10 种语言运行时、2 个定理证明器、构建工具,还可继续安装更多。 |
|
|
30
30
|
| **高效利用 Token** | 常规任务通过代码自动化完成,让 AI Token 专注于创造性问题解决。 |
|
|
31
31
|
| **节省时间** | 人类需要 2~8 小时的工作,AI 每个工作会话仅需 10~25 分钟完成。可批量执行仓库中的任务。"代码在你睡觉时已写好。" |
|
|
32
|
-
| **编排式扩展** |
|
|
32
|
+
| **编排式扩展** | 并行工作进程犹如一支开发团队。可将 Claude MAX 和 ChatGPT Pro(各 $200)配对,获得两份独立的近乎无限预算。 |
|
|
33
33
|
| **人工控制** | AI 创建草稿 PR——由您决定是否合并。在关键节点设置质量把关。 |
|
|
34
34
|
| **任意设备编程** | 通过 Telegram 机器人使用 `/solve` 和 `/hive` 命令,在任意设备上管理 AI,无需 PC、IDE 或笔记本电脑。 |
|
|
35
35
|
| **100% 开源** | Unlicense(公共领域)。完全透明,无供应商锁定。 |
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**费用**:Hive Mind 支持两种 $200/月订阅作为功能完整、近乎“无限”的选项:
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
| 订阅 | 搭配 `--tool` | 默认模型 | 最适合场景 |
|
|
40
|
+
| ---------------------------------------------------------------- | ---------------- | ------------ | ---------------------------- |
|
|
41
|
+
| **Anthropic Claude MAX**(约 $200/月,常有五折优惠 = 价值 $400) | `claude`(默认) | Sonnet/Haiku | 最高创造力、最强通用代码推理 |
|
|
42
|
+
| **OpenAI ChatGPT Pro**($200/月,包含 Codex) | `codex` | `gpt-5.5` | 稳定确定性的重构和快速迭代 |
|
|
43
|
+
|
|
44
|
+
两个工具可以在同一个 hive 中组合使用。Worker 可以并行运行不同工具,`/codex` 或 `/solve --tool codex` 会将任务路由到 ChatGPT Pro,而默认路由到 Claude MAX。无需二选一:任意单一订阅都足以运行,同时使用两者可解锁按工具/模型划分的并发模式(#1474)。
|
|
40
45
|
|
|
41
46
|
Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确时,它会主动提问,您也可以随时通过 PR 评论进行说明补充。
|
|
42
47
|
|
|
@@ -48,7 +53,7 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
|
|
|
48
53
|
|
|
49
54
|
建议使用 Docker 进行安装(本地和服务器均适用)。请参阅下方的 [Docker 安装](#使用-docker) 部分。
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
本软件会以完全自主模式运行 Claude Code、Codex 等受支持的 AI 工具,这意味着它们可以自由执行任何它们认为合适的命令。
|
|
52
57
|
|
|
53
58
|
这可能导致意想不到的副作用。
|
|
54
59
|
|
|
@@ -60,7 +65,7 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
|
|
|
60
65
|
|
|
61
66
|
从连接互联网的虚拟机中提取 Token 的方式多种多样,包括但不限于:
|
|
62
67
|
|
|
63
|
-
- **Claude MAX Token** - AI
|
|
68
|
+
- **Claude MAX Token** 和/或 **ChatGPT Pro (Codex) Token** - AI 操作所必需;您可以使用其中任意一种或同时使用两者
|
|
64
69
|
- **GitHub Token** - 仓库访问所必需
|
|
65
70
|
- **API 密钥和凭证** - 系统上的任何敏感数据
|
|
66
71
|
|
|
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
|
|
|
161
166
|
# Inside the container, authenticate with GitHub
|
|
162
167
|
gh-setup-git-identity
|
|
163
168
|
|
|
164
|
-
# Authenticate with Claude
|
|
169
|
+
# Authenticate with Claude (if you have Claude MAX)
|
|
165
170
|
claude
|
|
166
171
|
|
|
167
172
|
# Optionally set configuration like this:
|
|
@@ -174,6 +179,14 @@ claude
|
|
|
174
179
|
# Optionally test Claude connection
|
|
175
180
|
claude -p hi --model haiku
|
|
176
181
|
|
|
182
|
+
# Authenticate with Codex (if you have ChatGPT Pro)
|
|
183
|
+
codex login --device-auth
|
|
184
|
+
|
|
185
|
+
# Optionally test Codex connection. codex exec refuses to run unless
|
|
186
|
+
# either cwd is a git repo it trusts or --skip-git-repo-check is passed.
|
|
187
|
+
# It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
|
|
188
|
+
codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
|
|
189
|
+
|
|
177
190
|
# You might need to update hive-mind and agent to latest versions:
|
|
178
191
|
bun install -g @link-assistant/hive-mind
|
|
179
192
|
bun install -g @link-assistant/agent
|
|
@@ -542,6 +555,7 @@ Shows:
|
|
|
542
555
|
- ✅ **Screen 会话**:命令在后台 Screen 会话中运行
|
|
543
556
|
- ✅ **Live Terminal Watch**:`/terminal_watch` 和 opt-in auto-start 显示 live session logs
|
|
544
557
|
- ✅ **聊天限制**:可选配置允许的聊天 ID 白名单
|
|
558
|
+
- ✅ **私聊 Auth 检查**:为白名单聊天所有者提供实验性的 `/auth --status <gh|claude|codex>` 和 `/auth --login <gh|claude|codex>`
|
|
545
559
|
- ✅ **诊断工具**:获取聊天 ID 和配置信息
|
|
546
560
|
|
|
547
561
|
#### Live Terminal Watch
|
|
@@ -560,6 +574,7 @@ Shows:
|
|
|
560
574
|
|
|
561
575
|
- 仅在机器人为管理员的群聊中有效
|
|
562
576
|
- 可通过 `TELEGRAM_ALLOWED_CHATS` 配置可选的聊天 ID 限制
|
|
577
|
+
- 如果未设置 `TELEGRAM_ALLOWED_CHATS`,私聊 `/auth` 会被禁用,且只有所列聊天的所有者可以使用
|
|
563
578
|
- 命令以运行机器人的系统用户身份执行
|
|
564
579
|
- 请确保已完成正确的身份验证(`gh auth login`、`claude-profiles`)
|
|
565
580
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { parseCommandArgs } from './telegram-solve-command.lib.mjs';
|
|
3
|
+
|
|
4
|
+
export const AUTH_PROVIDERS = Object.freeze(['gh', 'claude', 'codex']);
|
|
5
|
+
|
|
6
|
+
const AUTH_PROVIDER_SET = new Set(AUTH_PROVIDERS);
|
|
7
|
+
const AUTH_USAGE = 'Usage: /auth --status <gh|claude|codex> or /auth --login <gh|claude|codex>';
|
|
8
|
+
// eslint-disable-next-line no-control-regex
|
|
9
|
+
const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
10
|
+
const TOKEN_RE = /\b(?:gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,})\b/g;
|
|
11
|
+
const TOKEN_FIELD_RE = /\b(token|access_token|refresh_token|api[_-]?key|authorization)\s*[:=]\s*["']?[^"'\s,}]+/gi;
|
|
12
|
+
|
|
13
|
+
function trimOutput(text, max = 3500) {
|
|
14
|
+
const value = String(text || '').trim();
|
|
15
|
+
if (value.length <= max) return value;
|
|
16
|
+
return value.slice(0, max) + `\n... truncated ${value.length - max} characters`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function escapeCodeFence(text) {
|
|
20
|
+
return String(text || '').replace(/```/g, '` ` `');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeProvider(provider) {
|
|
24
|
+
return String(provider || '')
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readActionValue(arg) {
|
|
30
|
+
if (arg === '--status') return { action: 'status', provider: null, consumesNext: true };
|
|
31
|
+
if (arg === '--login') return { action: 'login', provider: null, consumesNext: true };
|
|
32
|
+
if (arg.startsWith('--status=')) return { action: 'status', provider: arg.slice('--status='.length), consumesNext: false };
|
|
33
|
+
if (arg.startsWith('--login=')) return { action: 'login', provider: arg.slice('--login='.length), consumesNext: false };
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseAuthRequest(text) {
|
|
38
|
+
const args = parseCommandArgs(text || '');
|
|
39
|
+
let action = null;
|
|
40
|
+
let provider = null;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const parsed = readActionValue(args[i]);
|
|
44
|
+
if (!parsed) {
|
|
45
|
+
return { action: null, provider: null, error: `Unsupported /auth argument: ${args[i]}\n\n${AUTH_USAGE}` };
|
|
46
|
+
}
|
|
47
|
+
if (action) {
|
|
48
|
+
return { action: null, provider: null, error: `Use exactly one of --status or --login.\n\n${AUTH_USAGE}` };
|
|
49
|
+
}
|
|
50
|
+
action = parsed.action;
|
|
51
|
+
provider = normalizeProvider(parsed.provider);
|
|
52
|
+
if (parsed.consumesNext) {
|
|
53
|
+
const next = args[i + 1];
|
|
54
|
+
if (!next || next.startsWith('--')) {
|
|
55
|
+
return { action: null, provider: null, error: AUTH_USAGE };
|
|
56
|
+
}
|
|
57
|
+
provider = normalizeProvider(next);
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!action || !provider) {
|
|
63
|
+
return { action: null, provider: null, error: AUTH_USAGE };
|
|
64
|
+
}
|
|
65
|
+
if (!AUTH_PROVIDER_SET.has(provider)) {
|
|
66
|
+
return { action, provider: null, error: `Unsupported auth provider: ${provider}\n\n${AUTH_USAGE}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { action, provider, error: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildAuthCommand(action, provider) {
|
|
73
|
+
if (action === 'status') {
|
|
74
|
+
if (provider === 'gh') return { command: 'gh', args: ['auth', 'status', '--hostname', 'github.com'] };
|
|
75
|
+
if (provider === 'claude') return { command: 'claude', args: ['auth', 'status'] };
|
|
76
|
+
if (provider === 'codex') return { command: 'codex', args: ['login', 'status'] };
|
|
77
|
+
}
|
|
78
|
+
if (action === 'login') {
|
|
79
|
+
if (provider === 'gh') return { command: 'gh', args: ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'] };
|
|
80
|
+
if (provider === 'claude') return { command: 'claude', args: ['auth', 'login', '--claudeai'] };
|
|
81
|
+
if (provider === 'codex') return { command: 'codex', args: ['login', '--device-auth'] };
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unsupported auth command: ${action} ${provider}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function redactAuthOutput(output) {
|
|
87
|
+
return String(output || '')
|
|
88
|
+
.replace(ANSI_RE, '')
|
|
89
|
+
.replace(TOKEN_RE, '[REDACTED_TOKEN]')
|
|
90
|
+
.replace(TOKEN_FIELD_RE, (_, name) => `${name}: [REDACTED_TOKEN]`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectAuthOutput(result) {
|
|
94
|
+
return redactAuthOutput([result?.stdout, result?.stderr].filter(Boolean).join('\n'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function extractAuthStartDetails(output) {
|
|
98
|
+
const text = redactAuthOutput(output);
|
|
99
|
+
const urls = [...new Set([...text.matchAll(/https?:\/\/[^\s<>)"']+/g)].map(match => match[0].replace(/[.,;:!?]+$/, '')))];
|
|
100
|
+
|
|
101
|
+
const codePatterns = [/\bone-time code\s*[:=]\s*([A-Z0-9][A-Z0-9-]{3,})/i, /\b(?:user code|verification code|code)\s*[:=]\s*([A-Z0-9][A-Z0-9-]{3,})/i, /\b([A-Z0-9]{4,}-[A-Z0-9-]{4,})\b/];
|
|
102
|
+
let code = null;
|
|
103
|
+
for (const pattern of codePatterns) {
|
|
104
|
+
const match = text.match(pattern);
|
|
105
|
+
if (match) {
|
|
106
|
+
code = match[1].toUpperCase();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { urls, code };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatAuthStatusMessage(provider, result) {
|
|
115
|
+
const code = result?.code;
|
|
116
|
+
const ok = code === 0;
|
|
117
|
+
const output = trimOutput(collectAuthOutput(result)) || '(no output)';
|
|
118
|
+
return `${ok ? 'OK' : 'ERROR'} *${provider} auth status*\n\nExit code: ${code ?? 'unknown'}\n\n\`\`\`\n${escapeCodeFence(output)}\n\`\`\``;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatAuthLoginMessage(provider, result) {
|
|
122
|
+
const output = collectAuthOutput(result);
|
|
123
|
+
const details = extractAuthStartDetails(output);
|
|
124
|
+
const lines = [`*${provider} auth login started*`, '', 'The local login command was cancelled locally after capturing the browser step, so this bot command did not replace existing credentials.'];
|
|
125
|
+
|
|
126
|
+
if (details.urls.length > 0) {
|
|
127
|
+
lines.push('', 'Open this URL:');
|
|
128
|
+
for (const url of details.urls) lines.push(url);
|
|
129
|
+
}
|
|
130
|
+
if (details.code) {
|
|
131
|
+
lines.push('', `Code: \`${details.code}\``);
|
|
132
|
+
}
|
|
133
|
+
if (details.urls.length === 0 && !details.code) {
|
|
134
|
+
const shownOutput = trimOutput(output) || '(no output captured)';
|
|
135
|
+
lines.push('', 'Captured output:', '', '```', escapeCodeFence(shownOutput), '```');
|
|
136
|
+
}
|
|
137
|
+
if (result?.cancelled) {
|
|
138
|
+
lines.push('', 'Status: cancelled locally after capture.');
|
|
139
|
+
} else if (typeof result?.code === 'number') {
|
|
140
|
+
lines.push('', `Status: login command exited with code ${result.code}.`);
|
|
141
|
+
}
|
|
142
|
+
lines.push('', 'Continuation by replying with a provider code is not automated yet; this is the first experimental CLI-backed /auth path.');
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const resolveAllowedAuthChatIds = allowedChats => {
|
|
147
|
+
if (!allowedChats) return [];
|
|
148
|
+
const raw = typeof allowedChats === 'function' ? allowedChats() : allowedChats;
|
|
149
|
+
if (!Array.isArray(raw)) return [];
|
|
150
|
+
return raw.map(value => String(value)).filter(Boolean);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export async function isAuthOperator({ telegram, userId, allowedChatIds }) {
|
|
154
|
+
if (!telegram || !userId || !allowedChatIds || allowedChatIds.length === 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
for (const chatId of allowedChatIds) {
|
|
158
|
+
if (String(chatId) === String(userId)) return true;
|
|
159
|
+
try {
|
|
160
|
+
const member = await telegram.getChatMember(chatId, userId);
|
|
161
|
+
if (member?.status === 'creator') return true;
|
|
162
|
+
} catch {
|
|
163
|
+
// Try the next configured chat. The bot may no longer be a member.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function runAuthCommand(command, args, options = {}) {
|
|
170
|
+
const { mode = 'status', loginCaptureMs = 15000, outputLimit = 20000, env = process.env } = options;
|
|
171
|
+
return new Promise(resolve => {
|
|
172
|
+
const child = spawn(command, args, {
|
|
173
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
174
|
+
env,
|
|
175
|
+
});
|
|
176
|
+
let stdout = '';
|
|
177
|
+
let stderr = '';
|
|
178
|
+
let settled = false;
|
|
179
|
+
let captureTimer = null;
|
|
180
|
+
|
|
181
|
+
const settle = result => {
|
|
182
|
+
if (settled) return;
|
|
183
|
+
settled = true;
|
|
184
|
+
if (captureTimer) clearTimeout(captureTimer);
|
|
185
|
+
resolve({
|
|
186
|
+
stdout: stdout.slice(0, outputLimit),
|
|
187
|
+
stderr: stderr.slice(0, outputLimit),
|
|
188
|
+
...result,
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const maybeCancelLogin = () => {
|
|
193
|
+
if (mode !== 'login' || settled) return;
|
|
194
|
+
const details = extractAuthStartDetails(`${stdout}\n${stderr}`);
|
|
195
|
+
if (details.urls.length === 0 && !details.code) return;
|
|
196
|
+
child.kill('SIGTERM');
|
|
197
|
+
settle({ code: null, signal: 'SIGTERM', cancelled: true });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
child.stdout.on('data', data => {
|
|
201
|
+
stdout += data.toString();
|
|
202
|
+
maybeCancelLogin();
|
|
203
|
+
});
|
|
204
|
+
child.stderr.on('data', data => {
|
|
205
|
+
stderr += data.toString();
|
|
206
|
+
maybeCancelLogin();
|
|
207
|
+
});
|
|
208
|
+
child.on('error', error => {
|
|
209
|
+
settle({ code: null, error: error.message });
|
|
210
|
+
});
|
|
211
|
+
child.on('close', (code, signal) => {
|
|
212
|
+
settle({ code, signal, cancelled: false });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (mode === 'login') {
|
|
216
|
+
captureTimer = setTimeout(() => {
|
|
217
|
+
child.kill('SIGTERM');
|
|
218
|
+
settle({ code: null, signal: 'SIGTERM', cancelled: true });
|
|
219
|
+
}, loginCaptureMs);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function registerAuthCommand(bot, options = {}) {
|
|
225
|
+
const { VERBOSE = false, isOldMessage, isForwardedOrReply, allowedChats, authEnabled = true } = options;
|
|
226
|
+
const execute = options.runCommand || runAuthCommand;
|
|
227
|
+
const reply = options.safeReply || ((ctx, text, replyOptions) => ctx.reply(text, replyOptions));
|
|
228
|
+
|
|
229
|
+
async function handleAuthCommand(ctx) {
|
|
230
|
+
VERBOSE && console.log('[VERBOSE] /auth command received');
|
|
231
|
+
|
|
232
|
+
if (isOldMessage && isOldMessage(ctx)) {
|
|
233
|
+
VERBOSE && console.log('[VERBOSE] /auth ignored: old message');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (isForwardedOrReply && isForwardedOrReply(ctx)) {
|
|
237
|
+
VERBOSE && console.log('[VERBOSE] /auth ignored: forwarded or reply');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!authEnabled) {
|
|
241
|
+
await reply(ctx, 'The /auth command is disabled on this bot instance.', { reply_to_message_id: ctx.message?.message_id });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!ctx.chat || !ctx.from || !ctx.message) return;
|
|
245
|
+
if (ctx.chat.type !== 'private') {
|
|
246
|
+
await reply(ctx, 'The /auth command is only available in private messages.', { reply_to_message_id: ctx.message.message_id });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const allowedChatIds = resolveAllowedAuthChatIds(allowedChats);
|
|
251
|
+
if (allowedChatIds.length === 0) {
|
|
252
|
+
await reply(ctx, 'The /auth command is disabled because TELEGRAM_ALLOWED_CHATS is not configured.', { reply_to_message_id: ctx.message.message_id });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const authorized = await isAuthOperator({ telegram: ctx.telegram, userId: ctx.from.id, allowedChatIds });
|
|
257
|
+
if (!authorized) {
|
|
258
|
+
VERBOSE && console.log(`[VERBOSE] /auth denied: user ${ctx.from.id} is not creator of any allowed chat`);
|
|
259
|
+
await reply(ctx, 'The /auth command is only available to owners of allowlisted chats.', { reply_to_message_id: ctx.message.message_id });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const request = parseAuthRequest(ctx.message.text || '');
|
|
264
|
+
if (request.error) {
|
|
265
|
+
await reply(ctx, request.error, { reply_to_message_id: ctx.message.message_id });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { command, args } = buildAuthCommand(request.action, request.provider);
|
|
270
|
+
let result;
|
|
271
|
+
try {
|
|
272
|
+
result = await execute(command, args, { mode: request.action, provider: request.provider });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
await reply(ctx, `Failed to run ${request.provider} auth ${request.action}: ${error.message || String(error)}`, { reply_to_message_id: ctx.message.message_id });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const message = request.action === 'status' ? formatAuthStatusMessage(request.provider, result) : formatAuthLoginMessage(request.provider, result);
|
|
279
|
+
await reply(ctx, message, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
bot.command('auth', handleAuthCommand);
|
|
283
|
+
return { handleAuthCommand };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export default {
|
|
287
|
+
AUTH_PROVIDERS,
|
|
288
|
+
buildAuthCommand,
|
|
289
|
+
extractAuthStartDetails,
|
|
290
|
+
formatAuthLoginMessage,
|
|
291
|
+
formatAuthStatusMessage,
|
|
292
|
+
isAuthOperator,
|
|
293
|
+
parseAuthRequest,
|
|
294
|
+
redactAuthOutput,
|
|
295
|
+
registerAuthCommand,
|
|
296
|
+
resolveAllowedAuthChatIds,
|
|
297
|
+
runAuthCommand,
|
|
298
|
+
};
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFro
|
|
|
40
40
|
const { executeStartScreen: executeStartScreenCommand, buildExecuteAndUpdateMessage } = await import('./telegram-command-execution.lib.mjs');
|
|
41
41
|
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
42
42
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
43
|
-
const { installTelegramFormattingFallback, safeEditMessageText, safeReply } = await import('./telegram-safe-reply.lib.mjs');
|
|
43
|
+
const { installTelegramFormattingFallback, isTelegramFormattingError, isTelegramMessageTooLongError, safeEditMessageText, safeReply, TELEGRAM_TEXT_LIMIT } = await import('./telegram-safe-reply.lib.mjs');
|
|
44
44
|
const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await import('./telegram-terminal-watch-command.lib.mjs');
|
|
45
45
|
const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
|
|
46
46
|
const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
|
|
@@ -100,6 +100,11 @@ const config = yargs(hideBin(process.argv))
|
|
|
100
100
|
description: 'Enable /task and /split commands (use --no-task to disable)',
|
|
101
101
|
default: getenv('TELEGRAM_TASK', 'true') !== 'false',
|
|
102
102
|
})
|
|
103
|
+
.option('auth', {
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
description: 'Enable experimental private /auth command for allowlisted chat owners (use --no-auth to disable)',
|
|
106
|
+
default: getenv('TELEGRAM_AUTH', 'true') !== 'false',
|
|
107
|
+
})
|
|
103
108
|
.option('dryRun', {
|
|
104
109
|
type: 'boolean',
|
|
105
110
|
description: 'Validate configuration and options without starting the bot',
|
|
@@ -163,6 +168,7 @@ const hiveOverrides = resolvedHiveOverrides
|
|
|
163
168
|
const solveEnabled = config.solve;
|
|
164
169
|
const hiveEnabled = config.hive;
|
|
165
170
|
const taskEnabled = config.task;
|
|
171
|
+
const authEnabled = config.auth;
|
|
166
172
|
// Isolation mode (experimental): uses `$` from start-command with specified backend
|
|
167
173
|
const ISOLATION_BACKEND = (config.isolation || getenv('TELEGRAM_ISOLATION', '')).trim().toLowerCase();
|
|
168
174
|
let isolationRunner = null;
|
|
@@ -283,7 +289,7 @@ if (config.dryRun) {
|
|
|
283
289
|
if (allowedTopics && allowedTopics.length > 0) {
|
|
284
290
|
console.log(' Allowed topics:', lino.formatLinks(allowedTopics));
|
|
285
291
|
}
|
|
286
|
-
console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
|
|
292
|
+
console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
|
|
287
293
|
if (solveOverrides.length > 0) {
|
|
288
294
|
console.log(' Solve overrides:', lino.format(solveOverrides));
|
|
289
295
|
}
|
|
@@ -606,6 +612,8 @@ const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.m
|
|
|
606
612
|
registerSubscribeCommands(bot, sharedCommandOpts);
|
|
607
613
|
const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
|
|
608
614
|
const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
|
|
615
|
+
const { registerAuthCommand } = await import('./telegram-auth-command.lib.mjs');
|
|
616
|
+
const { handleAuthCommand } = registerAuthCommand(bot, { ...sharedCommandOpts, allowedChats, authEnabled, safeReply });
|
|
609
617
|
|
|
610
618
|
// Named handler for /solve command - extracted for reuse by text-based fallback (issue #1207)
|
|
611
619
|
async function handleSolveCommand(ctx) {
|
|
@@ -1170,7 +1178,7 @@ bot.on('message', async (ctx, next) => {
|
|
|
1170
1178
|
const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
|
|
1171
1179
|
const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
|
|
1172
1180
|
// /queue is the short alias for /solve_queue (issue #1837)
|
|
1173
|
-
const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
|
|
1181
|
+
const handlers = { ...solveHandlers, ...taskHandlers, auth: handleAuthCommand, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
|
|
1174
1182
|
|
|
1175
1183
|
const handler = handlers[extracted.command];
|
|
1176
1184
|
if (!handler) return next();
|
|
@@ -1214,15 +1222,17 @@ bot.catch((error, ctx) => {
|
|
|
1214
1222
|
|
|
1215
1223
|
// Try to notify the user about the error with more details
|
|
1216
1224
|
if (ctx?.reply) {
|
|
1217
|
-
const isTelegramParsingError =
|
|
1225
|
+
const isTelegramParsingError = isTelegramFormattingError(error);
|
|
1226
|
+
const isTelegramTextLimitError = isTelegramMessageTooLongError(error);
|
|
1218
1227
|
|
|
1219
1228
|
let errorMessage;
|
|
1220
1229
|
|
|
1221
|
-
if (isTelegramParsingError) {
|
|
1230
|
+
if (isTelegramParsingError || isTelegramTextLimitError) {
|
|
1222
1231
|
// Issue #1460: Log detailed context for root cause analysis (always logged, not just in verbose mode)
|
|
1223
1232
|
const userInfo = ctx.from ? { id: ctx.from.id, username: ctx.from.username, first_name: ctx.from.first_name, last_name: ctx.from.last_name } : 'unknown';
|
|
1224
|
-
|
|
1225
|
-
console.error(`[telegram-bot]
|
|
1233
|
+
const errorKind = isTelegramTextLimitError ? 'Message length error' : 'Parsing error';
|
|
1234
|
+
console.error(`[telegram-bot] ${errorKind}: ${error.message}`);
|
|
1235
|
+
console.error(`[telegram-bot] ${errorKind} context - user: ${JSON.stringify(userInfo)}, command: ${ctx.message?.text?.split(' ')[0] || 'unknown'}`);
|
|
1226
1236
|
console.error(`[telegram-bot] User input text: ${ctx.message?.text || 'none'}`);
|
|
1227
1237
|
if (ctx.message?.text) {
|
|
1228
1238
|
const visibleInput = makeSpecialCharsVisible(ctx.message.text, { maxLength: 500 });
|
|
@@ -1233,8 +1243,11 @@ bot.catch((error, ctx) => {
|
|
|
1233
1243
|
}
|
|
1234
1244
|
}
|
|
1235
1245
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1246
|
+
if (isTelegramTextLimitError) {
|
|
1247
|
+
errorMessage = `❌ Telegram rejected an oversized bot message.\n\nThe bot splits messages above ${TELEGRAM_TEXT_LIMIT} characters automatically. Please try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
|
|
1248
|
+
} else {
|
|
1249
|
+
errorMessage = `❌ Telegram rejected a formatted bot message, and the fallback handler could not recover automatically.\n\nPlease try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
|
|
1250
|
+
}
|
|
1238
1251
|
} else {
|
|
1239
1252
|
errorMessage = '❌ An error occurred while processing your request.\n\n';
|
|
1240
1253
|
if (error.message) {
|
|
@@ -1251,20 +1264,13 @@ bot.catch((error, ctx) => {
|
|
|
1251
1264
|
if (VERBOSE) errorMessage += `\n\n🔍 Debug info: Update ID: ${ctx.update.update_id}`;
|
|
1252
1265
|
}
|
|
1253
1266
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1267
|
+
safeReply(ctx, errorMessage, { fallbackLocale: resolveLocaleFromTelegramCtx(ctx), verbose: VERBOSE }).catch(replyError => {
|
|
1268
|
+
console.error('Failed to send error message to user:', replyError);
|
|
1269
|
+
const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
|
|
1270
|
+
ctx.reply(plainMessage).catch(fallbackError => {
|
|
1271
|
+
console.error('Failed to send fallback error message:', fallbackError);
|
|
1258
1272
|
});
|
|
1259
|
-
}
|
|
1260
|
-
ctx.reply(errorMessage, { parse_mode: 'Markdown' }).catch(replyError => {
|
|
1261
|
-
console.error('Failed to send error message to user:', replyError);
|
|
1262
|
-
const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
|
|
1263
|
-
ctx.reply(plainMessage).catch(fallbackError => {
|
|
1264
|
-
console.error('Failed to send fallback error message:', fallbackError);
|
|
1265
|
-
});
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1273
|
+
});
|
|
1268
1274
|
}
|
|
1269
1275
|
});
|
|
1270
1276
|
|
|
@@ -1281,7 +1287,7 @@ if (allowedChats && allowedChats.length > 0) {
|
|
|
1281
1287
|
if (allowedTopics && allowedTopics.length > 0) {
|
|
1282
1288
|
console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
|
|
1283
1289
|
}
|
|
1284
|
-
console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
|
|
1290
|
+
console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
|
|
1285
1291
|
if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
|
|
1286
1292
|
if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
|
|
1287
1293
|
if (VERBOSE) {
|
|
@@ -2,6 +2,7 @@ import { normalizeLocale, t } from './i18n.lib.mjs';
|
|
|
2
2
|
|
|
3
3
|
const FORMATTING_FALLBACK_INSTALLED = Symbol.for('hiveMind.telegramFormattingFallbackInstalled');
|
|
4
4
|
const DEFAULT_FORMATTING_FALLBACK_WARNING = '⚠️ Formatting error detected. Showing plain text fallback.';
|
|
5
|
+
export const TELEGRAM_TEXT_LIMIT = 4096;
|
|
5
6
|
const FORMATTING_FALLBACK_WARNINGS = {
|
|
6
7
|
en: DEFAULT_FORMATTING_FALLBACK_WARNING,
|
|
7
8
|
ru: '⚠️ Обнаружена ошибка форматирования. Показываю обычный текст.',
|
|
@@ -18,9 +19,18 @@ function splitOptions(options = {}) {
|
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
function getTelegramErrorMessage(error) {
|
|
23
|
+
return error?.description || error?.message || String(error || '');
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function isTelegramFormattingError(error) {
|
|
22
|
-
const message =
|
|
23
|
-
return /can't parse entities/i.test(message) || /can't find end of/i.test(message) || /entity.*parse/i.test(message) || (/
|
|
27
|
+
const message = getTelegramErrorMessage(error);
|
|
28
|
+
return /can't parse entities/i.test(message) || /can't find end of/i.test(message) || /entity.*parse/i.test(message) || /parse.*entity/i.test(message) || /character .* is reserved/i.test(message) || /unsupported start tag/i.test(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isTelegramMessageTooLongError(error) {
|
|
32
|
+
const message = getTelegramErrorMessage(error);
|
|
33
|
+
return /message is too long/i.test(message) || /message text is too long/i.test(message) || /text is too long/i.test(message) || /message_too_long/i.test(message) || (/bad request/i.test(message) && /too long/i.test(message) && /(message|text|caption)/i.test(message));
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
export function stripTelegramMarkdown(text) {
|
|
@@ -36,6 +46,45 @@ export function stripTelegramMarkdown(text) {
|
|
|
36
46
|
.replace(/`([^`]+)`/g, '$1');
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
function findTelegramSplitIndex(text, limit) {
|
|
50
|
+
const minUsefulSplit = Math.floor(limit * 0.45);
|
|
51
|
+
const separators = ['\n\n', '\n', '. ', '; ', ', ', ' '];
|
|
52
|
+
|
|
53
|
+
for (const separator of separators) {
|
|
54
|
+
const searchEnd = Math.max(0, limit - separator.length);
|
|
55
|
+
const index = text.lastIndexOf(separator, searchEnd);
|
|
56
|
+
if (index >= minUsefulSplit) {
|
|
57
|
+
return index + separator.length;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return limit;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function splitTelegramMessageText(text, limit = TELEGRAM_TEXT_LIMIT) {
|
|
65
|
+
const source = String(text ?? '');
|
|
66
|
+
if (source.length <= limit) return [source];
|
|
67
|
+
|
|
68
|
+
const chunks = [];
|
|
69
|
+
let remaining = source;
|
|
70
|
+
|
|
71
|
+
while (remaining.length > limit) {
|
|
72
|
+
let splitAt = findTelegramSplitIndex(remaining, limit);
|
|
73
|
+
let chunk = remaining.slice(0, splitAt).trimEnd();
|
|
74
|
+
|
|
75
|
+
if (!chunk) {
|
|
76
|
+
splitAt = limit;
|
|
77
|
+
chunk = remaining.slice(0, splitAt);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
chunks.push(chunk);
|
|
81
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (remaining) chunks.push(remaining);
|
|
85
|
+
return chunks;
|
|
86
|
+
}
|
|
87
|
+
|
|
39
88
|
function getFormattingFallbackWarning(locale) {
|
|
40
89
|
const key = 'telegram.formatting_fallback_warning';
|
|
41
90
|
const normalizedLocale = normalizeLocale(locale);
|
|
@@ -50,7 +99,7 @@ export function buildTelegramFormattingFallbackText(text, options = {}) {
|
|
|
50
99
|
}
|
|
51
100
|
|
|
52
101
|
function logFormattingFailure(scope, error, text, verbose = false, fallbackText = null) {
|
|
53
|
-
const message =
|
|
102
|
+
const message = getTelegramErrorMessage(error);
|
|
54
103
|
console.error(`[telegram-bot] ${scope}: formatted Telegram message failed: ${message}`);
|
|
55
104
|
if (verbose) {
|
|
56
105
|
const originalBytes = Buffer.byteLength(String(text ?? ''), 'utf-8');
|
|
@@ -76,54 +125,241 @@ function logFormattingFailure(scope, error, text, verbose = false, fallbackText
|
|
|
76
125
|
}
|
|
77
126
|
}
|
|
78
127
|
|
|
128
|
+
function logMessageTooLongFailure(scope, error, text, verbose = false, fallbackText = null) {
|
|
129
|
+
const message = getTelegramErrorMessage(error);
|
|
130
|
+
console.error(`[telegram-bot] ${scope}: Telegram message exceeded ${TELEGRAM_TEXT_LIMIT} character limit: ${message}`);
|
|
131
|
+
if (verbose) {
|
|
132
|
+
const original = String(text ?? '');
|
|
133
|
+
console.error(`[telegram-bot] ${scope}: Oversized message (${original.length} chars, ${Buffer.byteLength(original, 'utf-8')} bytes): ${original}`);
|
|
134
|
+
if (fallbackText !== null) {
|
|
135
|
+
const fallback = String(fallbackText ?? '');
|
|
136
|
+
console.error(`[telegram-bot] ${scope}: Plain text fallback (${fallback.length} chars, ${Buffer.byteLength(fallback, 'utf-8')} bytes): ${fallback}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function logChunking(scope, text, chunks, verbose = false) {
|
|
142
|
+
if (chunks.length <= 1) return;
|
|
143
|
+
const source = String(text ?? '');
|
|
144
|
+
console.warn(`[telegram-bot] ${scope}: Telegram text is ${source.length} chars (${Buffer.byteLength(source, 'utf-8')} bytes), splitting into ${chunks.length} messages (limit ${TELEGRAM_TEXT_LIMIT}).`);
|
|
145
|
+
if (verbose) {
|
|
146
|
+
chunks.forEach((chunk, index) => {
|
|
147
|
+
console.error(`[telegram-bot] ${scope}: Chunk ${index + 1}/${chunks.length} (${chunk.length} chars, ${Buffer.byteLength(chunk, 'utf-8')} bytes): ${chunk}`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getPlainTextOptions(telegramOptions) {
|
|
153
|
+
return { ...telegramOptions, parse_mode: undefined, entities: undefined };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function sendPlainTextChunks({ text, telegramOptions, scope, verbose, sendChunk }) {
|
|
157
|
+
const plainOptions = getPlainTextOptions(telegramOptions);
|
|
158
|
+
const chunks = splitTelegramMessageText(text);
|
|
159
|
+
logChunking(`${scope}:plainText`, text, chunks, verbose);
|
|
160
|
+
|
|
161
|
+
let firstResult;
|
|
162
|
+
for (const chunk of chunks) {
|
|
163
|
+
const result = await sendChunk(chunk, plainOptions);
|
|
164
|
+
if (firstResult === undefined) firstResult = result;
|
|
165
|
+
}
|
|
166
|
+
return firstResult;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function sendTelegramTextChunks({ text, telegramOptions, fallbackLocale, verbose, scope, sendChunk }) {
|
|
170
|
+
const chunks = splitTelegramMessageText(text);
|
|
171
|
+
logChunking(scope, text, chunks, verbose);
|
|
172
|
+
|
|
173
|
+
let firstResult;
|
|
174
|
+
for (const chunk of chunks) {
|
|
175
|
+
try {
|
|
176
|
+
const result = await sendChunk(chunk, telegramOptions);
|
|
177
|
+
if (firstResult === undefined) firstResult = result;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
let fallbackText;
|
|
180
|
+
if (isTelegramFormattingError(error)) {
|
|
181
|
+
fallbackText = buildTelegramFormattingFallbackText(chunk, { fallbackLocale });
|
|
182
|
+
logFormattingFailure(scope, error, chunk, verbose, fallbackText);
|
|
183
|
+
} else if (isTelegramMessageTooLongError(error)) {
|
|
184
|
+
fallbackText = stripTelegramMarkdown(chunk);
|
|
185
|
+
logMessageTooLongFailure(scope, error, chunk, verbose, fallbackText);
|
|
186
|
+
} else {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await sendPlainTextChunks({
|
|
191
|
+
text: fallbackText,
|
|
192
|
+
telegramOptions,
|
|
193
|
+
scope,
|
|
194
|
+
verbose,
|
|
195
|
+
sendChunk,
|
|
196
|
+
});
|
|
197
|
+
if (firstResult === undefined) firstResult = result;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return firstResult;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function sendRemainingEditChunks({ chunks, telegramOptions, fallbackLocale, verbose, scope, sendFollowUpChunk }) {
|
|
205
|
+
if (chunks.length === 0) return undefined;
|
|
206
|
+
if (!sendFollowUpChunk) {
|
|
207
|
+
console.error(`[telegram-bot] ${scope}: cannot send ${chunks.length} remaining chunk(s) after edit because chat_id is unavailable.`);
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return await sendTelegramTextChunks({
|
|
212
|
+
text: chunks.join('\n'),
|
|
213
|
+
telegramOptions,
|
|
214
|
+
fallbackLocale,
|
|
215
|
+
verbose,
|
|
216
|
+
scope: `${scope}:followUp`,
|
|
217
|
+
sendChunk: sendFollowUpChunk,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function sendPlainRemainingEditChunks({ chunks, telegramOptions, verbose, scope, sendFollowUpChunk }) {
|
|
222
|
+
if (chunks.length === 0) return undefined;
|
|
223
|
+
if (!sendFollowUpChunk) {
|
|
224
|
+
console.error(`[telegram-bot] ${scope}: cannot send ${chunks.length} plain-text fallback chunk(s) after edit because chat_id is unavailable.`);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return await sendPlainTextChunks({
|
|
229
|
+
text: chunks.join('\n'),
|
|
230
|
+
telegramOptions,
|
|
231
|
+
scope: `${scope}:followUp`,
|
|
232
|
+
verbose,
|
|
233
|
+
sendChunk: sendFollowUpChunk,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function editTelegramTextChunks({ text, telegramOptions, fallbackLocale, verbose, scope, editChunk, sendFollowUpChunk }) {
|
|
238
|
+
const chunks = splitTelegramMessageText(text);
|
|
239
|
+
logChunking(scope, text, chunks, verbose);
|
|
240
|
+
|
|
241
|
+
const [firstChunk, ...remainingChunks] = chunks;
|
|
242
|
+
try {
|
|
243
|
+
const result = await editChunk(firstChunk, telegramOptions);
|
|
244
|
+
await sendRemainingEditChunks({
|
|
245
|
+
chunks: remainingChunks,
|
|
246
|
+
telegramOptions,
|
|
247
|
+
fallbackLocale,
|
|
248
|
+
verbose,
|
|
249
|
+
scope,
|
|
250
|
+
sendFollowUpChunk,
|
|
251
|
+
});
|
|
252
|
+
return result;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
let fallbackText;
|
|
255
|
+
if (isTelegramFormattingError(error)) {
|
|
256
|
+
fallbackText = buildTelegramFormattingFallbackText(firstChunk, { fallbackLocale });
|
|
257
|
+
logFormattingFailure(scope, error, firstChunk, verbose, fallbackText);
|
|
258
|
+
} else if (isTelegramMessageTooLongError(error)) {
|
|
259
|
+
fallbackText = stripTelegramMarkdown(firstChunk);
|
|
260
|
+
logMessageTooLongFailure(scope, error, firstChunk, verbose, fallbackText);
|
|
261
|
+
} else {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const plainOptions = getPlainTextOptions(telegramOptions);
|
|
266
|
+
const fallbackChunks = splitTelegramMessageText(fallbackText);
|
|
267
|
+
logChunking(`${scope}:plainText`, fallbackText, fallbackChunks, verbose);
|
|
268
|
+
const [firstFallbackChunk, ...remainingFallbackChunks] = fallbackChunks;
|
|
269
|
+
const result = await editChunk(firstFallbackChunk, plainOptions);
|
|
270
|
+
await sendPlainRemainingEditChunks({
|
|
271
|
+
chunks: [...remainingFallbackChunks, ...remainingChunks.map(stripTelegramMarkdown)],
|
|
272
|
+
telegramOptions,
|
|
273
|
+
verbose,
|
|
274
|
+
scope,
|
|
275
|
+
sendFollowUpChunk,
|
|
276
|
+
});
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
79
281
|
// Issue #1460/#1497/#1788: try Markdown first, fall back to localized plain text on parsing errors.
|
|
80
282
|
export async function safeReply(ctx, text, options = {}) {
|
|
81
283
|
const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
|
|
82
284
|
const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
285
|
+
return await sendTelegramTextChunks({
|
|
286
|
+
text,
|
|
287
|
+
telegramOptions: firstOptions,
|
|
288
|
+
fallbackLocale,
|
|
289
|
+
verbose,
|
|
290
|
+
scope: 'safeReply',
|
|
291
|
+
sendChunk: (chunk, chunkOptions) => ctx.reply(chunk, chunkOptions),
|
|
292
|
+
});
|
|
91
293
|
}
|
|
92
294
|
|
|
93
295
|
export async function safeEditMessageText(telegram, chatId, messageId, inlineMessageId, text, options = {}) {
|
|
94
296
|
const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
|
|
95
297
|
const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
298
|
+
return await editTelegramTextChunks({
|
|
299
|
+
text,
|
|
300
|
+
telegramOptions: firstOptions,
|
|
301
|
+
fallbackLocale,
|
|
302
|
+
verbose,
|
|
303
|
+
scope: 'safeEditMessageText',
|
|
304
|
+
editChunk: (chunk, chunkOptions) => telegram.editMessageText(chatId, messageId, inlineMessageId, chunk, chunkOptions),
|
|
305
|
+
sendFollowUpChunk: chatId !== undefined && chatId !== null && typeof telegram.sendMessage === 'function' ? (chunk, chunkOptions) => telegram.sendMessage(chatId, chunk, chunkOptions) : null,
|
|
306
|
+
});
|
|
104
307
|
}
|
|
105
308
|
|
|
106
|
-
function
|
|
107
|
-
const original = telegram?.
|
|
309
|
+
function wrapTelegramSendMessage(telegram, defaults = {}) {
|
|
310
|
+
const original = telegram?.sendMessage;
|
|
108
311
|
if (typeof original !== 'function') return;
|
|
109
312
|
|
|
110
|
-
telegram
|
|
111
|
-
const text = args[
|
|
112
|
-
const originalOptions = args[
|
|
313
|
+
telegram.sendMessage = async function wrappedTelegramSendMessage(...args) {
|
|
314
|
+
const text = args[1];
|
|
315
|
+
const originalOptions = args[2] || {};
|
|
113
316
|
const { telegramOptions, fallbackLocale, verbose } = splitOptions(originalOptions);
|
|
114
|
-
args[
|
|
317
|
+
args[2] = telegramOptions;
|
|
115
318
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
319
|
+
if (typeof text !== 'string') return await original.apply(this, args);
|
|
320
|
+
|
|
321
|
+
return await sendTelegramTextChunks({
|
|
322
|
+
text,
|
|
323
|
+
telegramOptions,
|
|
324
|
+
fallbackLocale: fallbackLocale || defaults.fallbackLocale,
|
|
325
|
+
verbose: verbose || defaults.verbose,
|
|
326
|
+
scope: 'sendMessage',
|
|
327
|
+
sendChunk: (chunk, chunkOptions) => {
|
|
328
|
+
const chunkArgs = [...args];
|
|
329
|
+
chunkArgs[1] = chunk;
|
|
330
|
+
chunkArgs[2] = chunkOptions;
|
|
331
|
+
return original.apply(this, chunkArgs);
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function wrapTelegramEditMessageText(telegram, defaults = {}) {
|
|
338
|
+
const original = telegram?.editMessageText;
|
|
339
|
+
if (typeof original !== 'function') return;
|
|
340
|
+
|
|
341
|
+
telegram.editMessageText = async function wrappedTelegramEditMessageText(...args) {
|
|
342
|
+
const text = args[3];
|
|
343
|
+
const originalOptions = args[4] || {};
|
|
344
|
+
const { telegramOptions, fallbackLocale, verbose } = splitOptions(originalOptions);
|
|
345
|
+
args[4] = telegramOptions;
|
|
346
|
+
|
|
347
|
+
if (typeof text !== 'string') return await original.apply(this, args);
|
|
348
|
+
|
|
349
|
+
return await editTelegramTextChunks({
|
|
350
|
+
text,
|
|
351
|
+
telegramOptions,
|
|
352
|
+
fallbackLocale: fallbackLocale || defaults.fallbackLocale,
|
|
353
|
+
verbose: verbose || defaults.verbose,
|
|
354
|
+
scope: 'editMessageText',
|
|
355
|
+
editChunk: (chunk, chunkOptions) => {
|
|
356
|
+
const chunkArgs = [...args];
|
|
357
|
+
chunkArgs[3] = chunk;
|
|
358
|
+
chunkArgs[4] = chunkOptions;
|
|
359
|
+
return original.apply(this, chunkArgs);
|
|
360
|
+
},
|
|
361
|
+
sendFollowUpChunk: args[0] !== undefined && args[0] !== null && typeof this.sendMessage === 'function' ? (chunk, chunkOptions) => this.sendMessage(args[0], chunk, chunkOptions) : null,
|
|
362
|
+
});
|
|
127
363
|
};
|
|
128
364
|
}
|
|
129
365
|
|
|
@@ -135,8 +371,8 @@ export function installTelegramFormattingFallback(telegram, options = {}) {
|
|
|
135
371
|
verbose: Boolean(options.verbose),
|
|
136
372
|
};
|
|
137
373
|
|
|
138
|
-
|
|
139
|
-
|
|
374
|
+
wrapTelegramSendMessage(telegram, defaults);
|
|
375
|
+
wrapTelegramEditMessageText(telegram, defaults);
|
|
140
376
|
telegram[FORMATTING_FALLBACK_INSTALLED] = true;
|
|
141
377
|
return telegram;
|
|
142
378
|
}
|