Telegram bot connector (#5190)

* wip telegram bot connector

* encrypt bot token, reorg telegram bot modules, secure pairing codes

* offload telegram chat to background worker, add @agent support with chart png rendering, reconnect ui

* refactor telegram bot settings page into subcomponents

* response.locals for mum, telemetry for connecting to telegram

* simplify telegram command registration

* improve telegram bot ux: rework switch/history/resume commands

* add voice, photo, and TTS support to telegram bot with long message handling

* lint

* rename external_connectors to external_communication_connectors, add voice response mode, persist chat workspace/thread selection

* lint

* fix telegram bot connect/disconnect bugs, kill telegram bot on multiuser mode enable

* add english translations

* fix qr code in light mode

* repatch migration

* WIP checkpoint

* pipeline overhaul for using response obj

* format functions

* fix comment block

* remove conditional dumpENV + lint

* remove .end() from sendStatus calls

* patch broken streaming where streaming only first chunk

* refactor

* use Ephemeral handler now

* show metrics and citations in real GUI

* bugfixes

* prevent MuM persistence, UI cleanup, styling for status

* add new workspace flow in UI
Add thread chat count
fix 69 byte payload callback limit bug

* handle pagination for workspaces, threads, and models

* modularize commands and navigation

* add /proof support for citation recall

* handle backlog message spam

* support abort of response streams

* code cleanup

* spam prevention

* fix translations, update voice typing indicator, fix token bug

* frontend refactor, update tips on /status and voice response improvements

* collapse agent though blocks

* support images

* Fix mime issues with audio from other devices

* fix config issue post server stop

* persist image on agentic chats

* 5189 i18n (#5245)

* i18n translations
connect #5189

* prune translations

* fix errors

* fix translation gaps

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2026-03-23 15:10:21 -07:00 committed by GitHub
parent e02faa8984
commit 192ca411f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 8209 additions and 243 deletions

View File

@ -27,6 +27,11 @@ const ACCEPTED_MIMES = {
"audio/wav": [".wav"],
"audio/mpeg": [".mp3"],
"audio/ogg": [".ogg", ".oga"],
"audio/opus": [".opus"],
"audio/mp4": [".m4a"],
"audio/x-m4a": [".m4a"],
"audio/webm": [".webm"],
"video/mp4": [".mp4"],
"video/mpeg": [".mpeg"],
@ -68,6 +73,11 @@ const SUPPORTED_FILETYPE_CONVERTERS = {
".wav": "./convert/asAudio.js",
".mp4": "./convert/asAudio.js",
".mpeg": "./convert/asAudio.js",
".ogg": "./convert/asAudio.js",
".oga": "./convert/asAudio.js",
".opus": "./convert/asAudio.js",
".m4a": "./convert/asAudio.js",
".webm": "./convert/asAudio.js",
".png": "./convert/asImage.js",
".jpg": "./convert/asImage.js",

View File

@ -10,6 +10,7 @@ import {
PencilSimpleLine,
Nut,
Toolbox,
Plugs,
} from "@phosphor-icons/react";
import AgentIcon from "@/media/animations/agent-static.png";
import CommunityHubIcon from "@/media/illustrations/community-hub.png";
@ -363,6 +364,19 @@ const SidebarOptions = ({ user = null, t }) => (
},
]}
/>
<Option
btnText={t("settings.channels")}
icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
user={user}
childOptions={[
{
btnText: t("settings.available-channels.telegram"),
href: paths.settings.telegram(),
flex: true,
hidden: !!user,
},
]}
/>
<Option
btnText={t("settings.tools")}
icon={<Toolbox className="h-5 w-5 flex-shrink-0" />}

View File

@ -17,7 +17,7 @@ export default function useScrollActiveItemIntoView({
useEffect(() => {
if (isActive) {
ref.current.scrollIntoView({
ref.current?.scrollIntoView({
behavior,
block,
});

View File

@ -50,7 +50,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "اسم مساحة العمل",
user: "مستعمِل",
selection: "اختيار النموذج",
saving: "حفظ...",
save: "حفظ التغييرات",
@ -104,6 +103,10 @@ const TRANSLATIONS = {
"your-account": "حسابك",
"import-item": "استيراد العنصر",
},
channels: "القنوات",
"available-channels": {
telegram: "تليجرام",
},
},
login: {
"multi-user": {
@ -180,18 +183,12 @@ const TRANSLATIONS = {
title: "وضع المحادثة",
chat: {
title: "المحادثة",
description:
'سيوفر إجابات بناءً على المعرفة العامة للنموذج اللغوي الكبير، بالإضافة إلى سياق المستندات.<br />ستحتاج إلى استخدام الأمر "@agent" لاستخدام الأدوات.',
},
query: {
title: "استعلام",
description:
'سيوفر الإجابات <b>فقط</b> إذا تم العثور على سياق الوثيقة.<br />ستحتاج إلى استخدام الأمر "@agent" لاستخدام الأدوات.',
},
automatic: {
title: "سيارة",
description:
'سيتم استخدام الأدوات تلقائيًا إذا كان النموذج ومزود الخدمة يدعمان استدعاء الأدوات الأصلية. إذا لم يتم دعم الأدوات الأصلية، فستحتاج إلى استخدام الأمر "@agent" لاستخدام الأدوات.',
},
},
history: {
@ -734,7 +731,6 @@ const TRANSLATIONS = {
see_less: "اقرأ المزيد",
see_more: "عرض المزيد",
tools: "الأدوات",
browse: "تصفح",
text_size_label: "حجم النص",
select_model: "اختر الطراز",
sources: "مصادر",
@ -747,7 +743,6 @@ const TRANSLATIONS = {
edit: "تحرير",
publish: "نشر",
stop_generating: "توقف عن إنشاء رد",
pause_tts_speech_message: "توقف عن قراءة النص بصوت مسجل.",
slash_commands: "أوامر مختصرة",
agent_skills: "مهارات الوكيل",
manage_agent_skills: "إدارة مهارات الوكلاء",
@ -998,6 +993,82 @@ const TRANSLATIONS = {
"لا تم التخصيص لأي مساحة عمل.\nيرجى الاتصال بمدير المثيل لطلب الوصول إلى مساحة عمل.",
goToWorkspace: 'الذهاب إلى "{{workspace}}"',
},
telegram: {
title: "روبوت تيليجرام",
description:
"قم بتوصيل مثيل AnyLLM الخاص بك بـ Telegram، حتى تتمكن من الدردشة مع مساحات العمل الخاصة بك من أي جهاز.",
setup: {
step1: {
title: "الخطوة الأولى: إنشاء روبوت Telegram الخاص بك.",
description:
"افتح حساب @BotFather على تطبيق تيليجرام، وأرسل الرسالة `/newbot` إلى حساب @BotFather، واتبع التعليمات، وانسخ رمز API.",
"open-botfather": "ابدأ محادثة مع BotFather",
"instruction-1": "1. افتح الرابط أو قم بمسح رمز QR.",
"instruction-2":
"2. أرسل <code>/newbot</code> إلى <code>@BotFather</code>",
"instruction-3": "3. اختر اسمًا واسم مستخدم لروبوتك.",
"instruction-4": "4. انسخ رمز واجهة برمجة التطبيقات الذي تتلقاه.",
},
step2: {
title: "الخطوة الثانية: قم بتوصيل روبوتك.",
description:
"الصق رمز API الذي تلقيته من @BotFather، ثم اختر مساحة عمل افتراضية لجهازك التابع للروبوت للدردشة فيها.",
"bot-token": "رمز الرمز (Token)",
"default-workspace": "مساحة العمل الافتراضية",
"no-workspace": "لا توجد مساحات عمل متاحة. سيتم إنشاء مساحة عمل جديدة.",
connecting: "التحميل...",
"connect-bot": "روبوت الاتصال",
},
security: {
title: "إعدادات الأمان الموصى بها",
description:
'لزيادة الأمان، قم بتكوين هذه الإعدادات في حساب "@BotFather".',
"disable-groups": " منع إضافة الروبوتات إلى المجموعات",
"disable-inline": " منع استخدام الروبوت في عمليات البحث المباشرة.",
"obscure-username":
"استخدم اسم مستخدم روبوت غير تقليدي لتقليل فرص اكتشافه.",
},
"toast-enter-token": "الرجاء إدخال رمز البوت.",
"toast-connect-failed": "فشل الاتصال بالروبوت.",
},
connected: {
status: "متصل",
"status-disconnected":
"غير متصل — قد يكون الرمز منتهي الصلاحية أو غير صالح",
"placeholder-token": "ألصق رمز البوت الجديد...",
reconnect: "إعادة الاتصال",
workspace: "مساحة العمل",
"bot-link": "رابط الروبوت",
"voice-response": "رد صوتي",
disconnecting: "قطع الاتصال...",
disconnect: "افصل",
"voice-text-only": "النص فقط",
"voice-mirror": "مرآة (يرد الصوت عند إرسال المستخدم له)",
"voice-always": "يرجى تضمين تسجيل صوتي (إرسال تسجيل صوتي مع كل رد).",
"toast-disconnect-failed": "فشل فصل الوحدة الآلية.",
"toast-reconnect-failed": "فشل إعادة الاتصال بالروبوت.",
"toast-voice-failed": "فشلت عملية تحديث وضع الصوت.",
"toast-approve-failed": "فشل في الموافقة على المستخدم.",
"toast-deny-failed": "فشل في رفض طلب المستخدم.",
"toast-revoke-failed": "فشل إلغاء صلاحية المستخدم.",
},
users: {
"pending-title": "معلق على الموافقة",
"pending-description":
"المستخدمون في انتظار التحقق. قارن رمز المطابقة المعروض هنا بالرمز المعروض في محادثتهم على تطبيق Telegram.",
"approved-title": "المستخدمون المعتمدون",
"approved-description":
"المستخدمون الذين تم منحهم الإذن للتواصل مع روبوتك.",
user: "المستخدم",
"pairing-code": "رمز التوفيق",
"no-pending": "لا توجد طلبات معلقة.",
"no-approved": "لا يوجد مستخدمون معتمدون.",
unknown: "غير معروف",
approve: "الموافقة",
deny: "رفض",
revoke: "إلغاء",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Název pracovního prostoru",
user: "Uživatel",
selection: "Výběr modelu",
saving: "Ukládání...",
save: "Uložit změny",
@ -113,6 +112,10 @@ const TRANSLATIONS = {
"your-account": "Váš účet",
"import-item": "Importovat položku",
},
channels: "Kanály",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -196,18 +199,12 @@ const TRANSLATIONS = {
title: "Režim chatu",
chat: {
title: "Chat",
description:
"poskytne odpovědi založené na obecných znalostech LLM a kontextu dokumentu, který je k dispozici.<br />Pro použití nástrojů budete muset použít příkaz @agent.",
},
query: {
title: "Dotaz",
description:
"budou poskytovat odpovědi <b>pouze</b>, pokud je nalezen kontext dokumentu.<br />Pro použití nástrojů budete muset použít příkaz @agent.",
},
automatic: {
title: "Auto",
description:
"automaticky použije nástroje, pokud to podporují jak model, tak poskytovatel. Pokud není podporováno nativní volání nástrojů, budete muset použít příkaz `@agent` pro použití nástrojů.",
},
},
history: {
@ -863,7 +860,6 @@ const TRANSLATIONS = {
see_less: "Zobrazit méně",
see_more: "Více",
tools: "Nářadí",
browse: "Prohlédněte si",
text_size_label: "Velikost písma",
select_model: "Vyberte model",
sources: "Zdroje",
@ -876,8 +872,6 @@ const TRANSLATIONS = {
edit: "Upravit",
publish: "Publikovat",
stop_generating: "Zastavte generování odpovědi",
pause_tts_speech_message:
"Zastavte čtení textu pomocí syntetické řeči z tohoto zprávy.",
slash_commands: "Příkazy v řádku",
agent_skills: "Dovednosti agenta",
manage_agent_skills: "Řízení dovedností agentů",
@ -1013,6 +1007,86 @@ const TRANSLATIONS = {
},
},
},
telegram: {
title: "Bot pro Telegram",
description:
"Propojte svůj instance AnythingLLM s aplikací Telegram, abyste mohli komunikovat se svými pracovními prostory odkudkoli.",
setup: {
step1: {
title: "Krok 1: Vytvořte svého Telegramového robota",
description:
"Otevřete aplikaci @BotFather na Telegramu, odešlete příkaz `/newbot` na adresu <code>@BotFather</code>, postupujte podle pokynů a zkopírujte API token.",
"open-botfather": "Spusťte BotFather",
"instruction-1": "1. Otevřete odkaz nebo naskenujte QR kód",
"instruction-2":
"2. Pošlete <code>/newbot</code> na adresu <code>@BotFather</code>",
"instruction-3":
"3. Vyberte jméno a uživatelské jméno pro svého robota.",
"instruction-4": "4. Zkopírujte API token, který obdržíte.",
},
step2: {
title: "Krok 2: Připojte svého robota",
description:
"Vložte API token, který jste obdrželi od účtu @BotFather, a vyberte výchozí pracovní prostor, se kterým bude váš bot komunikovat.",
"bot-token": "Token Bot",
"default-workspace": "Výchozí pracovní prostor",
"no-workspace":
"Nejsou k dispozici žádné pracovní prostory. Bude vytvořeno nové.",
connecting: "Připojování...",
"connect-bot": "Bot pro připojení",
},
security: {
title: "Doporučené bezpečnostní nastavení",
description:
"Pro zvýšení bezpečnosti, nakonfigurujte tyto nastavení v účtu @BotFather.",
"disable-groups": "— Zabránit přidávání bot do skupin",
"disable-inline":
"— Zabraňte použití robota při vyhledávání v reálném čase.",
"obscure-username":
"Použijte neobvyklé uživatelské jméno pro robota, abyste snížili jeho snadnou identifikovatelnost.",
},
"toast-enter-token": "Prosím, zadejte token pro robota.",
"toast-connect-failed": "Nedaří se připojit k botovi.",
},
connected: {
status: "Spojené",
"status-disconnected": "Neaktivní token může být prošlý nebo neplatný",
"placeholder-token": "Vložte nový token pro robota...",
reconnect: "Znovu se spojit",
workspace: "Pracovní prostor",
"bot-link": "Odkaz na robota",
"voice-response": "Reakce na hlasový vstup",
disconnecting: "Odpojování...",
disconnect: "Odpojit",
"voice-text-only": "Pouze text",
"voice-mirror":
"Zrcadlo (odpovězte hlasem, když uživatel pošle hlasovou zprávu)",
"voice-always":
"Vždy uveďte zvukový záznam (odesílejte zvukový záznam ke každé odpovědi)",
"toast-disconnect-failed": "Nepodařilo se odpojit automat.",
"toast-reconnect-failed": "Nedaří se znovu navázat spojení s botem.",
"toast-voice-failed": "Nepodařilo se aktualizovat hlasový režim.",
"toast-approve-failed": "Neúspěšné schválení uživatele.",
"toast-deny-failed": "Nezucceededo v odmítnutí uživatele.",
"toast-revoke-failed": "Nezdařilo se zrušit uživatelskou účet.",
},
users: {
"pending-title": "Čeká na schválení",
"pending-description":
"Uživatelé, kteří čekají na ověření. Porovnejte kód pro spárování, který je zde uveden, s tím, který je zobrazen v jejich chatu na Telegramu.",
"approved-title": "Schválení uživatelů",
"approved-description":
"Uživatelé, kteří byli schváleni pro komunikaci s vaším botem.",
user: "Uživatel",
"pairing-code": "Kód pro párování",
"no-pending": "Žádné čekající požadavky",
"no-approved": "Žádní registrovaní uživatelé",
unknown: "Neznámé",
approve: "Schválit",
deny: "Odmítnout",
revoke: "Zrušit",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Navn på arbejdsområder",
user: "Bruger",
selection: "Modelvalg",
saving: "Gemmer...",
save: "Gem ændringer",
@ -107,6 +106,10 @@ const TRANSLATIONS = {
"your-account": "Dit konti",
"import-item": "Importeret vare",
},
channels: "Kanaler",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -182,18 +185,12 @@ const TRANSLATIONS = {
title: "Chat-tilstand",
chat: {
title: "Chat",
description:
'vil give svar baseret på LLM\'s generelle viden og den relevante kontekst fra dokumentet. Du skal bruge kommandoen "@agent" for at bruge værktøjerne.',
},
query: {
title: "Forespørgsel",
description:
"vil give svar <b>kun</b>, hvis dokumentets kontekst er fundet.<br />Du skal bruge kommandoen @agent for at bruge værktøjerne.",
},
automatic: {
title: "Bil",
description:
'vil automatisk bruge værktøjer, hvis modellen og leverandøren understøtter native værktøjskald.<br />Hvis native værktøjskald ikke understøttes, skal du bruge kommandoen "@agent" for at bruge værktøjer.',
},
},
history: {
@ -748,7 +745,6 @@ const TRANSLATIONS = {
see_less: "Se mindre",
see_more: "Se flere",
tools: "Værktøj",
browse: "Gennemse",
text_size_label: "Tekststørrelse",
select_model: "Vælg model",
sources: "Kilder",
@ -761,7 +757,6 @@ const TRANSLATIONS = {
edit: "Rediger",
publish: "Udgive",
stop_generating: "Stop med at generere svar",
pause_tts_speech_message: "Pause TTS-læsningen af beskeden",
slash_commands: "Kommandoer",
agent_skills: "Agenters kompetencer",
manage_agent_skills: "Administrer agenters kompetencer",
@ -1020,6 +1015,86 @@ const TRANSLATIONS = {
"Du er ikke tildelt til nogen arbejdsområder.\nKontakt din administrator for at anmode om adgang til et arbejdsområde.",
goToWorkspace: 'Gå til "{{workspace}}"',
},
telegram: {
title: "Telegram-bot",
description:
"Forbind dit AnythingLLM-instans med Telegram, så du kan kommunikere med dine arbejdsområder fra enhver enhed.",
setup: {
step1: {
title: "Trin 1: Opret din Telegram-bot",
description:
"Åbn @BotFather i Telegram, send `/newbot` til <code>@BotFather</code>, følg instruktionerne, og kopier API-tokenet.",
"open-botfather": "Åbn BotFather",
"instruction-1": "1. Åbn linket eller scann QR-koden",
"instruction-2":
"2. Send <code>/newbot</code> til <code>@BotFather</code>",
"instruction-3": "3. Vælg et navn og et brugernavn til din bot",
"instruction-4": "4. Kopier API-tokenet, du modtager.",
},
step2: {
title: "Trin 2: Forbind din bot",
description:
"Indsæt API-tokenet, du modtog fra @BotFather, og vælg et standard-arbejdsområde, hvor din bot kan kommunikere.",
"bot-token": "Bot-token",
"default-workspace": "Standardarbejdsområde",
"no-workspace":
"Ingen ledige arbejdsområder. Et nyt vil blive oprettet.",
connecting: "Forbindes...",
"connect-bot": "Connect Bot",
},
security: {
title: "Anbefalede sikkerhedsindstillinger",
description:
"For yderligere sikkerhed, kan du konfigurere disse indstillinger via @BotFather.",
"disable-groups": "— Forhindre tilføjelse af bots til grupper",
"disable-inline":
"— Forhindr brugen af bot i søgninger direkte i søgefeltet",
"obscure-username":
"Brug et brugernavn til en bot, der ikke er åbenlyst, for at reducere synligheden.",
},
"toast-enter-token": "Vær venligst opført et bot-token.",
"toast-connect-failed": "Kunne ikke etablere forbindelse med botten.",
},
connected: {
status: "Forbundet",
"status-disconnected":
"Afbrudt tokenet kan være udløbet eller ugyldigt",
"placeholder-token": "Indsæt nyt bot-token...",
reconnect: "Genopslå",
workspace: "Arbejdsområde",
"bot-link": "Bot-link",
"voice-response": "Stemmebesvarelse",
disconnecting: "Afbryde...",
disconnect: "Afbryde",
"voice-text-only": "Kun tekst",
"voice-mirror": "Spejl (svar med stemme, når brugeren sender en stemme)",
"voice-always":
"Sørg altid for at inkludere en lydbesked (send lyd sammen med hvert svar).",
"toast-disconnect-failed": "Kunne ikke afbryde robotten.",
"toast-reconnect-failed":
"Kunne ikke genoprette forbindelsen med botten.",
"toast-voice-failed": "Kunne ikke opdatere stemmemodus.",
"toast-approve-failed": "Mislykkedes med at godkende bruger.",
"toast-deny-failed": "Kunne ikke afvise brugeren.",
"toast-revoke-failed": "Kunne ikke annullere brugerens adgang.",
},
users: {
"pending-title": "Afventer godkendelse",
"pending-description":
"Brugere, der venter på at blive verificeret. Sammenlign den kode, der vises her, med den, der vises i deres Telegram-chat.",
"approved-title": "Godkendte brugere",
"approved-description":
"Brugere, der er blevet godkendt til at kommunikere med din bot.",
user: "Bruger",
"pairing-code": "Kombinationskode",
"no-pending": "Ingen igangværende anmodninger",
"no-approved": "Ingen godkendte brugere",
unknown: "Ukendt",
approve: "Godkend",
deny: "Afvise",
revoke: "Annullere",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Namen der Workspaces",
user: "Benutzer",
selection: "Modellauswahl",
saving: "Speichern...",
save: "Änderungen speichern",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Ihr Konto",
"import-item": "Artikel importieren",
},
channels: "Kanäle",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -189,18 +192,12 @@ const TRANSLATIONS = {
title: "Chat-Modus",
chat: {
title: "Chat",
description:
"wird Antworten basierend auf dem allgemeinen Wissen des LLM und dem relevanten Kontext aus den Dokumenten <b> und </b> liefern. <br /> Sie benötigen den Befehl `@agent`, um die Tools zu nutzen.",
},
query: {
title: "Abfrage",
description:
'wird nur Antworten <b> und </b> bereitstellen, falls der Kontext des Dokuments gefunden wurde.<br />Sie müssen den Befehl "@agent" verwenden, um die Tools zu nutzen.',
},
automatic: {
title: "Auto",
description:
'wird automatisch Werkzeuge verwenden, wenn das Modell und der Anbieter native Werkzeugaufrufe unterstützen. <br />Wenn native Werkzeugaufrufe nicht unterstützt werden, müssen Sie den Befehl "@agent" verwenden, um Werkzeuge zu nutzen.',
},
},
history: {
@ -842,7 +839,6 @@ const TRANSLATIONS = {
see_less: "Weniger anzeigen",
see_more: "Mehr anzeigen",
tools: "Werkzeuge",
browse: "Durchsuchen",
text_size_label: "Schriftgröße",
select_model: "Modell auswählen",
sources: "Quellen",
@ -855,7 +851,6 @@ const TRANSLATIONS = {
edit: "Bearbeiten",
publish: "Veröffentlichen",
stop_generating: "Stoppen Sie die Generierung von Antworten",
pause_tts_speech_message: "Pause die Text-to-Speech-Funktion der Nachricht",
slash_commands: "Befehlszeilen",
agent_skills: "Fähigkeiten von Agenten",
manage_agent_skills: "Verwalten Sie die Fähigkeiten von Agenten",
@ -1024,6 +1019,93 @@ const TRANSLATIONS = {
"Sie sind nicht zugewiesen zu einem Arbeitsbereich.\nBitte kontaktieren Sie Ihren Administrator, um Zugriff auf einen Arbeitsbereich zu erhalten.",
goToWorkspace: 'Zurück zum Arbeitsbereich "{{workspace}}"',
},
telegram: {
title: "Telegram-Bot",
description:
"Verbinden Sie Ihre AnyLLM-Instanz mit Telegram, damit Sie von jedem Gerät mit Ihren Arbeitsbereichen chatten können.",
setup: {
step1: {
title: "Schritt 1: Erstellen Sie Ihren Telegram-Bot",
description:
"Öffnen Sie @BotFather in Telegram, senden Sie <code>/newbot</code> an <code>@BotFather</code>, befolgen Sie die Anweisungen und kopieren Sie den API-Token.",
"open-botfather": "Öffnen Sie BotFather",
"instruction-1": "1. Öffnen Sie den Link oder scannen Sie den QR-Code",
"instruction-2":
"2. Senden Sie <code>/newbot</code> an <code>@BotFather</code>",
"instruction-3":
"3. Wählen Sie einen Namen und einen Benutzernamen für Ihren Bot aus.",
"instruction-4": "4. Kopieren Sie den API-Token, den Sie erhalten.",
},
step2: {
title: "Schritt 2: Verbinden Sie Ihren Bot",
description:
"Fügen Sie den API-Token ein, den Sie von @BotFather erhalten haben, und wählen Sie einen Standard-Arbeitsbereich für Ihren Bot aus, mit dem er kommunizieren soll.",
"bot-token": "Bot-Token",
"default-workspace": "Standardarbeitsbereich",
"no-workspace":
"Keine verfügbaren Arbeitsbereiche. Ein neuer Bereich wird erstellt.",
connecting: "Verbinde...",
"connect-bot": "Connect-Bot",
},
security: {
title: "Empfohlene Sicherheitseinstellungen",
description:
"Für zusätzliche Sicherheit, konfigurieren Sie diese Einstellungen über @BotFather.",
"disable-groups":
" Verhinderung der automatisierten Anmeldung von Bots in Gruppen",
"disable-inline":
" Verhindern Sie die Verwendung von Bots in der Inline-Suche",
"obscure-username":
"Verwenden Sie einen Benutzernamen für den Bot, der nicht offensichtlich ist, um die Auffindbarkeit zu reduzieren.",
},
"toast-enter-token": "Bitte geben Sie einen Bot-Token ein.",
"toast-connect-failed":
"Verbindung zum Bot konnte nicht hergestellt werden.",
},
connected: {
status: "Verbunden",
"status-disconnected":
"Abgekoppelt Token möglicherweise abgelaufen oder ungültig",
"placeholder-token": "Neuen Bot-Token einfügen...",
reconnect: "Wiederherstellen",
workspace: "Arbeitsbereich",
"bot-link": "Link",
"voice-response": "Sprachantwort",
disconnecting: "Abmelden...",
disconnect: "Abkoppeln",
"voice-text-only": "Nur Text",
"voice-mirror":
"Echo (Antworten mit Sprache, wenn der Benutzer Sprache sendet)",
"voice-always":
"Bitte immer Sprachnachrichten senden (Audio mit jeder Antwort hinzufügen)",
"toast-disconnect-failed":
"Es konnte nicht erfolgreich die Verbindung zum Bot trennen.",
"toast-reconnect-failed":
"Verbindung zum Bot konnte nicht hergestellt werden.",
"toast-voice-failed":
"Fehlgeschlagen bei der Aktualisierung des Sprachmodus.",
"toast-approve-failed": "Benutzer konnte nicht autorisiert werden.",
"toast-deny-failed": "Nicht in der Lage, den Benutzer abzuweisen.",
"toast-revoke-failed":
"Fehlgeschlagener Versuch, das Benutzerkonto zu deaktivieren.",
},
users: {
"pending-title": "Warte auf Genehmigung",
"pending-description":
"Benutzer, die noch verifiziert werden müssen. Vergleichen Sie den hier angezeigten Pairing-Code mit dem, der in ihrem Telegram-Chat angezeigt wird.",
"approved-title": "Benutzer mit Genehmigung",
"approved-description":
"Nutzer, denen die Erlaubnis erteilt wurde, mit Ihrem Bot zu kommunizieren.",
user: "Benutzer",
"pairing-code": "Paarcode",
"no-pending": "Keine ausstehenden Anfragen",
"no-approved": "Keine autorisierten Benutzer",
unknown: "Unbekannt",
approve: "Genehmigen",
deny: "Leugnen",
revoke: "Aufheben",
},
},
};
export default TRANSLATIONS;

View File

@ -50,7 +50,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Workspace Name",
user: "User",
selection: "Model Selection",
saving: "Saving...",
save: "Save changes",
@ -111,6 +110,10 @@ const TRANSLATIONS = {
contact: "Contact Support",
"browser-extension": "Browser Extension",
"mobile-app": "AnythingLLM Mobile",
channels: "Channels",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -195,18 +198,12 @@ const TRANSLATIONS = {
title: "Chat mode",
automatic: {
title: "Auto",
description:
"will automatically use tools if the model and provider support native tool calling.<br />If native tooling is not supported, you will need to use the @agent command to use tools.",
},
chat: {
title: "Chat",
description:
"will provide answers with the LLM's general knowledge <b>and</b> document context that is found.<br />You will need to use the @agent command to use tools.",
},
query: {
title: "Query",
description:
"will provide answers <b>only</b> if document context is found.<br />You will need to use the @agent command to use tools.",
},
},
history: {
@ -583,6 +580,81 @@ const TRANSLATIONS = {
at: "Sent At",
},
},
telegram: {
title: "Telegram Bot",
description:
"Connect your AnythingLLM instance to Telegram so you can chat with your workspaces from any device.",
setup: {
step1: {
title: "Step 1: Create your Telegram bot",
description:
"Open @BotFather in Telegram, send <code>/newbot</code> to <code>@BotFather</code>, follow the prompts, and copy the API token.",
"open-botfather": "Open BotFather",
"instruction-1": "1. Open the link or scan the QR code",
"instruction-2":
"2. Send <code>/newbot</code> to <code>@BotFather</code>",
"instruction-3": "3. Choose a name and username for your bot",
"instruction-4": "4. Copy the API token you receive",
},
step2: {
title: "Step 2: Connect your bot",
description:
"Paste the API token you received from @BotFather and select a default workspace for your bot to chat with.",
"bot-token": "Bot Token",
"default-workspace": "Default Workspace",
"no-workspace": "No available workspaces. A new one will be created.",
connecting: "Connecting...",
"connect-bot": "Connect Bot",
},
security: {
title: "Recommended Security Settings",
description:
"For additional security, configure these settings in @BotFather.",
"disable-groups": "— Prevent adding bot to groups",
"disable-inline": "— Prevent bot from being used in inline search",
"obscure-username":
"Use a non-obvious bot handle username to reduce discoverability",
},
"toast-enter-token": "Please enter a bot token.",
"toast-connect-failed": "Failed to connect bot.",
},
connected: {
status: "Connected",
"status-disconnected": "Disconnected — token may be expired or invalid",
"placeholder-token": "Paste new bot token...",
reconnect: "Reconnect",
workspace: "Workspace",
"bot-link": "Bot Link",
"voice-response": "Voice Response",
disconnecting: "Disconnecting...",
disconnect: "Disconnect",
"voice-text-only": "Text only",
"voice-mirror": "Mirror (reply with voice when user sends voice)",
"voice-always": "Always voice (send audio with every reply)",
"toast-disconnect-failed": "Failed to disconnect bot.",
"toast-reconnect-failed": "Failed to reconnect bot.",
"toast-voice-failed": "Failed to update voice mode.",
"toast-approve-failed": "Failed to approve user.",
"toast-deny-failed": "Failed to deny user.",
"toast-revoke-failed": "Failed to revoke user.",
},
users: {
"pending-title": "Pending Approval",
"pending-description":
"Users waiting to be verified. Match the pairing code shown here with the one displayed in their Telegram chat.",
"approved-title": "Approved Users",
"approved-description":
"Users who have been approved to chat with your bot.",
user: "User",
"pairing-code": "Pairing Code",
"no-pending": "No pending requests",
"no-approved": "No approved users",
unknown: "Unknown",
approve: "Approve",
deny: "Deny",
revoke: "Revoke",
},
},
security: {
title: "Security",
multiuser: {
@ -834,7 +906,6 @@ const TRANSLATIONS = {
source_count_other: "{{count}} references",
document: "Document",
similarity_match: "match",
pause_tts_speech_message: "Pause TTS speech of message",
fork: "Fork",
delete: "Delete",
cancel: "Cancel",
@ -865,7 +936,6 @@ const TRANSLATIONS = {
normal: "Normal",
large: "Large",
tools: "Tools",
browse: "Browse",
text_size_label: "Text Size",
select_model: "Select Model",
slash_commands: "Slash Commands",

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Nombre de los espacios de trabajo",
user: "Usuario",
selection: "Selección de modelo",
saving: "Guardando...",
save: "Guardar cambios",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Su cuenta",
"import-item": "Importar artículo",
},
channels: "Canales",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -190,18 +193,12 @@ const TRANSLATIONS = {
title: "Modo de chat",
chat: {
title: "Chat",
description:
'proporcionará respuestas basándose en el conocimiento general del LLM y en el contexto del documento que se encuentre disponible. Para utilizar las herramientas, deberá utilizar el comando "@agent".',
},
query: {
title: "Consulta",
description:
'proporcionará respuestas <b>solo</b> si se encuentra el contexto del documento.<br />Deberá utilizar el comando "@agent" para utilizar las herramientas.',
},
automatic: {
title: "Coche",
description:
'Utilizará automáticamente las herramientas si el modelo y el proveedor admiten la llamada a herramientas nativas. Si no se admiten las herramientas nativas, deberá utilizar el comando "@agent" para utilizar las herramientas.',
},
},
history: {
@ -856,7 +853,6 @@ const TRANSLATIONS = {
see_less: "Ver menos",
see_more: "Ver más",
tools: "Herramientas",
browse: "Explorar",
text_size_label: "Tamaño del texto",
select_model: "Seleccionar modelo",
sources: "Fuentes",
@ -869,7 +865,6 @@ const TRANSLATIONS = {
edit: "Editar",
publish: "Publicar",
stop_generating: "Dejar de generar respuestas",
pause_tts_speech_message: "Pausa la lectura de voz del mensaje.",
slash_commands: "Comandos abreviados",
agent_skills: "Habilidades del agente",
manage_agent_skills: "Gestionar las habilidades del agente.",
@ -1038,6 +1033,88 @@ const TRANSLATIONS = {
"Actualmente no estás asignado a ningún espacio de trabajo.\nPor favor, contacta a tu administrador para solicitar acceso a un espacio de trabajo.",
goToWorkspace: 'Ir a "{{workspace}}"',
},
telegram: {
title: "Bot de Telegram",
description:
"Conecte su instancia de AnythingLLM a Telegram para poder conversar con sus espacios de trabajo desde cualquier dispositivo.",
setup: {
step1: {
title: "Paso 1: Crea tu bot de Telegram.",
description:
"Abra el bot @BotFather en Telegram, envíe /newbot al chat con <code>@BotFather, siga las instrucciones y copie el token de la API.",
"open-botfather": "Iniciar BotFather",
"instruction-1": "1. Abra el enlace o escanee el código QR.",
"instruction-2":
"2. Enviar <code>/newbot</code> a <code>@BotFather</code>",
"instruction-3":
"3. Elija un nombre y un nombre de usuario para su bot.",
"instruction-4": "4. Copie el token de la API que reciba.",
},
step2: {
title: "Paso 2: Conecte su bot.",
description:
"Copia el token de API que recibiste de @BotFather y selecciona un espacio de trabajo predeterminado para que tu bot pueda comunicarse.",
"bot-token": "Token de Bot",
"default-workspace": "Espacio de trabajo predeterminado",
"no-workspace":
"No hay espacios de trabajo disponibles. Se creará uno nuevo.",
connecting: "Conectando...",
"connect-bot": "Bot de conexión",
},
security: {
title: "Configuraciones de seguridad recomendadas",
description:
"Para una mayor seguridad, configure estas opciones a través de @BotFather.",
"disable-groups": "— Evitar que se añadan bots a los grupos",
"disable-inline":
"— Evitar que los bots se utilicen en búsquedas dentro de la página.",
"obscure-username":
"Utiliza un nombre de usuario para el bot que no sea obvio para reducir su visibilidad.",
},
"toast-enter-token": "Por favor, introduzca un token de bot.",
"toast-connect-failed": "No se pudo establecer la conexión con el bot.",
},
connected: {
status: "Conectado",
"status-disconnected":
"Desconectado — el token puede estar caducado o ser inválido.",
"placeholder-token": "Pegar nuevo token de bot...",
reconnect: "Restablecer la conexión",
workspace: "Espacio de trabajo",
"bot-link": "Enlace a bot",
"voice-response": "Respuesta por voz",
disconnecting: "Desconectando...",
disconnect: "Desconectar",
"voice-text-only": "Solo texto",
"voice-mirror":
"Espejo (responder con voz cuando el usuario envía una grabación de voz)",
"voice-always":
"Siempre incluir una grabación de voz (enviar audio con cada respuesta).",
"toast-disconnect-failed": "No se pudo desconectar el robot.",
"toast-reconnect-failed":
"No se pudo restablecer la conexión con el bot.",
"toast-voice-failed": "No se pudo actualizar el modo de voz.",
"toast-approve-failed": "No se pudo aprobar el usuario.",
"toast-deny-failed": "No se pudo negar la solicitud del usuario.",
"toast-revoke-failed": "No se pudo revocar el acceso del usuario.",
},
users: {
"pending-title": "Sujeto a aprobación",
"pending-description":
"Usuarios que están esperando la verificación. Compara el código de emparejamiento que se muestra aquí con el que aparece en su conversación de Telegram.",
"approved-title": "Usuarios autorizados",
"approved-description":
"Usuarios que han sido aprobados para comunicarse con tu bot.",
user: "Usuario",
"pairing-code": "Código de combinación",
"no-pending": "No hay solicitudes pendientes.",
"no-approved": "Usuarios no autorizados",
unknown: "Desconocido",
approve: "Aprobar",
deny: "Negar",
revoke: "Revocar",
},
},
};
export default TRANSLATIONS;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Tööruumide nimi",
user: "Kasutaja",
selection: "Mudeli valik",
saving: "Salvestan…",
save: "Salvesta muudatused",
@ -105,6 +104,10 @@ const TRANSLATIONS = {
"your-account": "Teie konto",
"import-item": "Importeeritud toode",
},
channels: "Kaasavad",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -186,18 +189,12 @@ const TRANSLATIONS = {
title: "Vestlusrežiim",
chat: {
title: "Vestlus",
description:
'teenab vastuseid, kasutades LLM-i üldist teadmist ja dokumentide konteksti, mida on leitav.<br /> Selleks peate kasutama käsku "@agent".',
},
query: {
title: "Päring",
description:
'teenib vastuseid <b>ainult__, kui dokumendi kontekst on leitud.</b> Vajate kasutama käesu "agent", et kasutada tööriime.',
},
automatic: {
title: "Automaailm",
description:
'kasutab automaatselt tööriistu, kui mudel ja pakkuja toetavad native tööriistade kasutamist. <br />Kui native tööriistade kasutamine pole toetatud, peate kasutama käsku "@agent", et tööriiste kasutada.',
},
},
history: {
@ -807,7 +804,6 @@ const TRANSLATIONS = {
see_less: "Näita vähem",
see_more: "Vaata rohkem",
tools: "Vahendid",
browse: "Sirva",
text_size_label: "Teksti suurus",
select_model: "Valige mudel",
sources: "Allikasid",
@ -820,7 +816,6 @@ const TRANSLATIONS = {
edit: "Redigeerimine",
publish: "Avaldada",
stop_generating: "Lõpeta vastuste genereerimine",
pause_tts_speech_message: "Peata sõna-sünteesi (TTS) rääkimine sõnumis",
slash_commands: "Lihtsasti kasutatavad käsud",
agent_skills: "Agentide oskused",
manage_agent_skills: "Halda agentide oskusi",
@ -976,6 +971,84 @@ const TRANSLATIONS = {
"Sa ei ole täidetud ühtegi tööruumi.\nPäringu tööruumiks, palun pööra teie administraatorile.",
goToWorkspace: 'Mine tööruumiks "{{workspace}}"',
},
telegram: {
title: "Telegrami bot",
description:
"Ühendage oma AnythingLLM instants Telegramiga, et saaksite vestleda oma tööruumidega igast seadmist.",
setup: {
step1: {
title: "1. samm: Looge oma Telegrami bot",
description:
"Ava Telegramis konto @BotFather, saat <code>/newbot</code> aadressile <code>@BotFather</code>, järgige juhiseid ja kopeerige API-token.",
"open-botfather": "Ava BotFather",
"instruction-1": "1. Avage link või skannige QR-kood",
"instruction-2":
"2. Saada <code>/newbot</code> aadressile <code>@BotFather</code>",
"instruction-3": "3. Valige oma botile nimi ja kasutajanimi.",
"instruction-4": "4. Kopeerige API-token, mida teile antakse.",
},
step2: {
title: "2. Samuti ühendage oma bot",
description:
"Kleepige API-token, mis teil on saanud kasutaja @BotFatherilt, ning valige oma botile vaikimõistmine.",
"bot-token": "Bot token",
"default-workspace": "Vaikimisi kasutatav tööruum",
"no-workspace":
"Praegu pole saadaval vaba töökohti. Ühe uue töökohtade loomine on plaanis.",
connecting: "Ühendamine...",
"connect-bot": "Ühendusrobott",
},
security: {
title: "Soovitavad turvameetmed",
description:
"Lisaks turvalisusele, konfigureerige need seaded @BotFatheris.",
"disable-groups": "— Ennetada, et botid ei lisataks gruppi",
"disable-inline": "— Vältida, et bot kasutaks otsingut reaalajas.",
"obscure-username":
"Kasutage mitteolivaid kasutajanime, et vähendada avastamise võimalust.",
},
"toast-enter-token": "Palun sisestage bot'i token.",
"toast-connect-failed": "Bot ei suutnud ühendust tehes.",
},
connected: {
status: "Ühendatud",
"status-disconnected":
"Vabandus, toet toet võib olla kehtimatuna või kehtima lõppenud",
"placeholder-token": "Sisestage uus bot'i token...",
reconnect: "Taastada ühendus",
workspace: "Tööruum",
"bot-link": "Bot link",
"voice-response": "Häälreaktsioon",
disconnecting: "Ühendus katkestatud...",
disconnect: "Ühenduse katkestamine",
"voice-text-only": "Tekst ainult",
"voice-mirror":
"Helisüsteem (vastake häältega, kui kasutaja kasutab helifunktsiooni)",
"voice-always": "Alati lisage hääl (saada helifail koos iga vastusega)",
"toast-disconnect-failed": "Impeer ei õnnestunud seadistada.",
"toast-reconnect-failed": "Bot ei suutnud ühendust taastada.",
"toast-voice-failed": "Ärkimõõtmeid ei õnnestunud uuendada.",
"toast-approve-failed": "Kasutaja kinnitamise ebaõnnestumine.",
"toast-deny-failed": "Ei suutnud kasutaja kohta infot väita.",
"toast-revoke-failed": "Ebaõnnestuti kasutaja konto kustutamises.",
},
users: {
"pending-title": "Ootea faasis, ootamas heakskiitu",
"pending-description":
"Kasutajad, kes ootavad kinnitamist. Võrdige siin näidatud vastuvõtusümboli koos nende Telegrami vestluses näidatud sümboliga.",
"approved-title": "Heakskiidud kasutajad",
"approved-description":
"Kasutajad, kellele on antud lubadus teie botiga vestelda.",
user: "Kasutaja",
"pairing-code": "Koosamis kood",
"no-pending": "Hetkel pole ootamisel ühtegi taotlust",
"no-approved": "Pole heakskiidud kasutajaid",
unknown: "Tuntud pole",
approve: "Heakskiid",
deny: "Nõgata",
revoke: "Tingimata",
},
},
};
export default TRANSLATIONS;

View File

@ -53,7 +53,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "نام فضای کار",
user: "کاربر",
selection: "انتخاب مدل",
saving: "در حال ذخیره...",
save: "ذخیره تغییرات",
@ -107,6 +106,10 @@ const TRANSLATIONS = {
"your-account": "حساب شما",
"import-item": "وارد کردن کالا",
},
channels: "کانال‌ها",
"available-channels": {
telegram: "تلگرام",
},
},
login: {
"multi-user": {
@ -181,18 +184,12 @@ const TRANSLATIONS = {
title: "حالت گفتگو",
chat: {
title: "گفتگو",
description:
"با استفاده از دانش عمومی مدل زبانی و اطلاعات موجود در سند، پاسخ‌ها را ارائه خواهد داد. برای استفاده از ابزارها، باید از دستور @agent استفاده کنید.",
},
query: {
title: "پرس‌وجو",
description:
"پاسخ‌ها را تنها در صورت یافتن زمینه سند ارائه می‌دهد. برای استفاده از ابزارها، باید از دستور @agent استفاده کنید.",
},
automatic: {
title: "خودرو",
description:
"اگر مدل و ارائه‌دهنده از فراخوانی ابزار به صورت پیش‌فرض پشتیبانی کنند، ابزارها به‌طور خودکار استفاده خواهند شد. <br />در صورتی که فراخوانی ابزار به صورت پیش‌فرض پشتیبانی نشود، شما باید از دستور @agent برای استفاده از ابزارها استفاده کنید.",
},
},
history: {
@ -743,7 +740,6 @@ const TRANSLATIONS = {
see_less: "کمی بیشتر",
see_more: "بیشتر",
tools: "ابزارها",
browse: "جستجو",
text_size_label: "اندازه متن",
select_model: "انتخاب مدل",
sources: "منابع",
@ -756,7 +752,6 @@ const TRANSLATIONS = {
edit: "ویرایش",
publish: "انتشار",
stop_generating: "متوقف کردن تولید پاسخ",
pause_tts_speech_message: "مکث در پخش صدای متن",
slash_commands: "دستورات کوتاه‌شده",
agent_skills: "مهارت‌های کارگزار",
manage_agent_skills: "مدیریت مهارت‌های نمایندگان",
@ -1012,6 +1007,84 @@ const TRANSLATIONS = {
"شما در حال حاضر به هیچ فضای کاری اختصاص نیافته‌اید.\nلطفاً با مدیر خود تماس بگیرید تا دسترسی به یک فضای کار را درخواست کنید.",
goToWorkspace: 'به فضای کار "{{workspace}}" بروید',
},
telegram: {
title: "ربات تلگرام",
description:
"با اتصال نمونه AnythingLLM خود به تلگرام، می‌توانید از هر دستگاهی با فضاهای کاری خود گفتگو کنید.",
setup: {
step1: {
title: "مرحله ۱: ایجاد ربات Telegram خود",
description:
"اپلیکیشن @BotFather را در تلگرام باز کنید، دستور <code>/newbot</code> را برای <code>@BotFather</code> ارسال کنید، دستورالعمل‌ها را دنبال کنید و توکن API را کپی کنید.",
"open-botfather": "شروع با ربات BotFather",
"instruction-1": "1. لینک را باز کنید یا کد QR را اسکن کنید",
"instruction-2":
"2. پیام <code>/newbot را برای <code>@BotFather ارسال کنید.",
"instruction-3": "3. یک نام و نام کاربری برای ربات خود انتخاب کنید.",
"instruction-4": "4. توکنی که دریافت می‌کنید را کپی کنید.",
},
step2: {
title: "مرحله دوم: اتصال ربات خود",
description:
"توکن API را که از @BotFather دریافت کرده‌اید، کپی کنید و یک فضای کاری پیش‌فرض را برای ربات خود انتخاب کنید تا بتواند با کاربران ارتباط برقرار کند.",
"bot-token": "توکن ربات",
"default-workspace": "فضای کاری پیش‌فرض",
"no-workspace":
"فضاهای کاری موجود نیست. یک فضای کاری جدید ایجاد خواهد شد.",
connecting: "در حال اتصال...",
"connect-bot": "اتصال ربات",
},
security: {
title: "تنظیمات امنیتی پیشنهادی",
description:
"برای افزایش امنیت، این تنظیمات را در حساب @BotFather پیکربندی کنید.",
"disable-groups": "— جلوگیری از اضافه کردن ربات به گروه‌ها",
"disable-inline":
"— از استفاده ربات‌ها در جستجوی درون‌صفحه‌ای جلوگیری کنید.",
"obscure-username":
"از یک نام کاربری برای ربات که به راحتی قابل تشخیص نباشد، استفاده کنید تا میزان شناسایی آن را کاهش دهید.",
},
"toast-enter-token": "لطفاً یک توکن برای ربات وارد کنید.",
"toast-connect-failed": "عدم امکان اتصال ربات.",
},
connected: {
status: "اتصال یافته",
"status-disconnected":
"قطع شده احتمال دارد توکن منقضی شده یا نامعتبر باشد",
"placeholder-token": "وارد کردن توکن جدید برای ربات...",
reconnect: "بازسازی ارتباط",
workspace: "فضای کاری",
"bot-link": "لینک ربات",
"voice-response": "پاسخ صوتی",
disconnecting: "قطع ارتباط...",
disconnect: "قطع ارتباط",
"voice-text-only": "فقط متن",
"voice-mirror": "بازتاب (پاسخگویی با صدا هنگام ارسال صدا توسط کاربر)",
"voice-always":
"همیشه، حتماً، یک صدای صوتی (ارسال فایل صوتی همراه با هر پاسخ)",
"toast-disconnect-failed": "عدم توانایی در قطع ارتباط با ربات.",
"toast-reconnect-failed": "عدم امکان برقراری ارتباط مجدد با ربات.",
"toast-voice-failed": "عدم امکان به‌روزرسانی حالت صدا.",
"toast-approve-failed": "عدم تایید کاربر.",
"toast-deny-failed": "امکان رد درخواست کاربر وجود نداشت.",
"toast-revoke-failed": "امکان لغو کردن حساب کاربری وجود نداشت.",
},
users: {
"pending-title": "منتظر تایید",
"pending-description":
"کاربرانی که منتظر تایید هستند. کد تطبیقی که در اینجا نشان داده شده را با کد موجود در چت تلگرام خود مطابقت دهید.",
"approved-title": "کاربران تایید شده",
"approved-description": "کاربرانی که مجوز دارند با ربات شما گفتگو کنند.",
user: "کاربر",
"pairing-code": "کد جفت‌سازی",
"no-pending": "هیچ درخواست در حال انجام وجود ندارد.",
"no-approved": "کاربران تایید شده وجود ندارد",
unknown: "نامشخص",
approve: "تایید",
deny: "رد",
revoke: "اعلام لغو",
},
},
};
export default TRANSLATIONS;

View File

@ -54,12 +54,14 @@ function collectFiles(dir, results = []) {
const sourceFiles = collectFiles(FRONTEND_SRC);
// ---------------------------------------------------------------------------
// 3. Scan source files for t() references (literal and dynamic)
// 3. Scan source files for t() and i18nKey references (literal and dynamic)
// ---------------------------------------------------------------------------
const referencedKeys = new Set();
const tCallRegex = /\bt\(\s*["'`]([^"'`]+)["'`]/g;
const dynamicTCallRegex = /\bt\(\s*([a-zA-Z_$][a-zA-Z0-9_$.]*)\s*[,)]/g;
const templateTCallRegex = /\bt\(\s*`([^`]*\$\{[^`]*)`\s*[,)]/g;
const i18nKeyRegex = /i18nKey=["'`]([^"'`]+)["'`]/g;
const i18nKeyJsxRegex = /i18nKey=\{["'`]([^"'`]+)["'`]\}/g;
const dynamicUsages = [];
for (const file of sourceFiles) {
@ -70,6 +72,14 @@ for (const file of sourceFiles) {
referencedKeys.add(match[1]);
}
while ((match = i18nKeyRegex.exec(content)) !== null) {
referencedKeys.add(match[1]);
}
while ((match = i18nKeyJsxRegex.exec(content)) !== null) {
referencedKeys.add(match[1]);
}
while ((match = dynamicTCallRegex.exec(content)) !== null) {
const arg = match[1];
if (/^["'`]/.test(arg)) continue;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Nom des espaces de travail",
user: "Utilisateur",
selection: "Sélection du modèle",
saving: "Enregistrement...",
save: "Enregistrer les modifications",
@ -105,6 +104,10 @@ const TRANSLATIONS = {
"your-account": "Votre compte",
"import-item": "Importer",
},
channels: "Canaux",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -182,18 +185,12 @@ const TRANSLATIONS = {
title: "Mode de chat",
chat: {
title: "Chat",
description:
'fournira des réponses en utilisant les connaissances générales du LLM et le contexte du document correspondant. <br />Vous devrez utiliser la commande "@agent" pour utiliser les outils.',
},
query: {
title: "Requête",
description:
"fournira des réponses <b>uniquement</b> si le contexte du document est trouvé.<br />Vous devrez utiliser la commande @agent pour utiliser les outils.",
},
automatic: {
title: "Voiture",
description:
"utilisera automatiquement les outils si le modèle et le fournisseur prennent en charge l'appel de outils natifs. <br />Si l'utilisation d'outils natifs n'est pas prise en charge, vous devrez utiliser la commande \"@agent\" pour utiliser les outils.",
},
},
history: {
@ -747,7 +744,6 @@ const TRANSLATIONS = {
see_less: "Voir moins",
see_more: "Voir plus",
tools: "Outils",
browse: "Parcourir",
text_size_label: "Taille du texte",
select_model: "Sélectionner le modèle",
sources: "Sources",
@ -760,8 +756,6 @@ const TRANSLATIONS = {
edit: "Modifier",
publish: "Publier",
stop_generating: "Arrêtez de générer des réponses",
pause_tts_speech_message:
"Mettre en pause la lecture de la voix synthétique du message",
slash_commands: "Commandes abrégées",
agent_skills: "Compétences des agents",
manage_agent_skills: "Gérer les compétences des agents",
@ -1016,6 +1010,87 @@ const TRANSLATIONS = {
"Vous n'êtes actuellement pas affecté à aucun espace de travail.\nPour accéder à un espace de travail, veuillez contacter votre administrateur.",
goToWorkspace: 'Aller à "{{workspace}}"',
},
telegram: {
title: "Bot Telegram",
description:
"Connectez votre instance de AnythingLLM à Telegram afin de pouvoir communiquer avec vos espaces de travail depuis n'importe quel appareil.",
setup: {
step1: {
title: "Étape 1 : Créez votre bot Telegram",
description:
"Ouvrez @BotFather sur Telegram, envoyez `/newbot` à <code>@BotFather</code>, suivez les instructions, et copiez le jeton API.",
"open-botfather": "Ouvrir BotFather",
"instruction-1": "1. Ouvrez le lien ou numérisez le code QR.",
"instruction-2":
"2. Envoyer <code>/newbot</code> à <code>@BotFather</code>",
"instruction-3":
"3. Choisissez un nom et un nom d'utilisateur pour votre bot.",
"instruction-4": "4. Copiez le jeton API que vous recevez.",
},
step2: {
title: "Étape 2 : Connectez votre bot",
description:
"Collez le jeton API que vous avez reçu de @BotFather et sélectionnez un espace de travail par défaut pour que votre bot puisse communiquer.",
"bot-token": "Token Bot",
"default-workspace": "Espace de travail par défaut",
"no-workspace":
"Il n'y a pas d'espaces de travail disponibles. Un nouvel espace sera créé.",
connecting: "Connexion...",
"connect-bot": "Bot de connexion",
},
security: {
title: "Paramètres de sécurité recommandés",
description:
"Pour une sécurité supplémentaire, configurez ces paramètres via @BotFather.",
"disable-groups": "— Empêcher l'ajout de bots aux groupes",
"disable-inline":
"— Empêcher l'utilisation de bots dans les recherches en ligne.",
"obscure-username":
"Utilisez un nom d'utilisateur de bot non évident pour réduire sa visibilité.",
},
"toast-enter-token": "Veuillez saisir un jeton de bot.",
"toast-connect-failed": "Échec de la connexion du bot.",
},
connected: {
status: "Connecté",
"status-disconnected":
"Non connecté le jeton pourrait être expiré ou invalide.",
"placeholder-token": "Coller le nouveau jeton de bot...",
reconnect: "Reconnexion",
workspace: "Espace de travail",
"bot-link": "Lien vers le bot",
"voice-response": "Réponse vocale",
disconnecting: "Déconnexion...",
disconnect: "Déconnecter",
"voice-text-only": "Texte uniquement",
"voice-mirror":
"Écho (répondre par la voix lorsque l'utilisateur envoie une voix)",
"voice-always":
"Toujours inclure une voix (envoyer un enregistrement audio avec chaque réponse)",
"toast-disconnect-failed": "Échec de la déconnexion du robot.",
"toast-reconnect-failed": "Échec de la reconnexion du bot.",
"toast-voice-failed": "Impossible de mettre à jour le mode vocal.",
"toast-approve-failed": "Échec de la validation de l'utilisateur.",
"toast-deny-failed": "Impossible de refuser l'accès à l'utilisateur.",
"toast-revoke-failed": "Impossible de supprimer l'utilisateur.",
},
users: {
"pending-title": "En attente d'approbation",
"pending-description":
"Utilisateurs en attente de vérification. Correspondez le code de correspondance affiché ici avec celui qui apparaît dans leur conversation Telegram.",
"approved-title": "Utilisateurs autorisés",
"approved-description":
"Utilisateurs qui ont été autorisés à communiquer avec votre bot.",
user: "Utilisateur",
"pairing-code": "Code de correspondance",
"no-pending": "Aucune demande en cours",
"no-approved": "Aucun utilisateur autorisé",
unknown: "Inconnu",
approve: "Approuver",
deny: "Rejeter",
revoke: "Annuler",
},
},
};
export default TRANSLATIONS;

View File

@ -49,7 +49,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "שם סביבת העבודה",
user: "משתמש",
selection: "בחירת מודל",
saving: "שומר...",
save: "שמור שינויים",
@ -103,6 +102,10 @@ const TRANSLATIONS = {
"your-account": "החשבון שלך",
"import-item": "ייבוא פריט",
},
channels: "ערוצים",
"available-channels": {
telegram: "טלגרם",
},
},
login: {
"multi-user": {
@ -184,18 +187,12 @@ const TRANSLATIONS = {
title: "מצב צ'אט",
chat: {
title: "צ'אט",
description:
'יוכל לספק תשובות בהתבסס על הידע הכללי של ה-LLM ועל ההקשר הרלוונטי מתוך המסמך. <b> ו-</b>\nתצטרכו להשתמש בפקודה "@agent" כדי להשתמש בכלי.',
},
query: {
title: "שאילתה",
description:
"יספק תשובות <b>רק</b>במידה ויהיה ניתן למצוא הקשר של המסמך.<br />תצטרכו להשתמש בפקודה @agent כדי להשתמש בכלי.",
},
automatic: {
title: "רכב",
description:
'הכלי ישתמש באופן אוטומטי בכלים אם המודל והספק תומכים בהם. <br />אם אין תמיכה בכלים מקומיים, תצטרכו להשתמש בפקודה "@agent" כדי להשתמש בכלים.',
},
},
history: {
@ -811,7 +808,6 @@ const TRANSLATIONS = {
see_less: "ראה פחות",
see_more: "לראות עוד",
tools: "כלים",
browse: "גלו",
text_size_label: "גודל הטקסט",
select_model: "בחר מודל",
sources: "מקורות",
@ -824,8 +820,6 @@ const TRANSLATIONS = {
edit: "עריכה",
publish: "להוציא לאור",
stop_generating: "הפסיקו ליצור תגובה",
pause_tts_speech_message:
"השהייה של קריאת טקסט באמצעות תוכנת TTS (Text-to-Speech)",
slash_commands: "פקודות קיצור",
agent_skills: "כישורים של סוכן",
manage_agent_skills: "ניהול מיומנויות של סוכנים",
@ -982,6 +976,80 @@ const TRANSLATIONS = {
"אינך מוקצה לכל סביבת עבודה.\nיש ליצור קשר עם המנהל שלך כדי לבקש גישה לסביבת עבודה.",
goToWorkspace: 'עבור לסביבת עבודה "{{workspace}}"',
},
telegram: {
title: "בוט של טלגרם",
description:
"חברו את ההתקנה של AnythingLLM ל-Telegram, כך שתוכלו לתקשר עם סביבות העבודה שלכם ממכשיר כלשהו.",
setup: {
step1: {
title: "שלב 1: צרו את הבוט שלכם ב-Telegram",
description:
"פתח את <code> ב-Telegram, שלח </code> לכתובת <code>@BotFather, עקוב אחר ההוראות, והעתק את מזהה ה-API.",
"open-botfather": "פתוח את BotFather",
"instruction-1": "1. פתחו את הקישור או סרקו את קוד ה-QR",
"instruction-2":
"2. שלחו את <code>/newbot</code> לכתובת <code>@BotFather</code>",
"instruction-3": "3. בחרו שם וכינוי משתמש עבור הבוט שלכם",
"instruction-4": "4. העתק את מזהה ה-API שקיבלת.",
},
step2: {
title: "שלב 2: חברו את הבוט שלכם",
description:
"הדבק את טוקן ה-API שקיבלת מחשבון @BotFather ובחר את חלל העבודה הראשי עבור הבוט שלך, כדי שיוכל לתקשר.",
"bot-token": "טוקן בוט",
"default-workspace": "סביבת עבודה ברירת מחדל",
"no-workspace": "אין מקומות עבודה זמינים. ייקבע מקום עבודה חדש.",
connecting: "חיבור...",
"connect-bot": "צ'אטבוט",
},
security: {
title: "הגדרות אבטחה מומלצות",
description:
"לנוחיות נוספת, יש לבצע את ההגדרות הללו דרך חשבון ה-@BotFather.",
"disable-groups": "— למנוע הוספת רובוטים לקבוצות",
"disable-inline": " למנוע שימוש בבוט בחיפוש ישיר",
"obscure-username":
"השתמש בשם משתמש של בוט שאינו בולט, כדי להקטין את הסיכוי שהוא יימצא.",
},
"toast-enter-token": "אנא הזן את טוקן הבוט.",
"toast-connect-failed": "לא הצליח להתחבר עם הבוט.",
},
connected: {
status: "מחובר",
"status-disconnected": "נתקע הטוקן עשוי להיות פג או לא תקין",
"placeholder-token": "הדבק את מפתח הבוט החדש...",
reconnect: "שוב קשר",
workspace: "חלל עבודה",
"bot-link": "קישור לבוט",
"voice-response": "תגובה קולית",
disconnecting: "ניתוק...",
disconnect: "ניתוק",
"voice-text-only": "טקסט בלבד",
"voice-mirror": "משקף (להגיב בקול כאשר המשתמש שולח קול)",
"voice-always": "יש לציין תמיד (לשלוח קבצי אודיו עם כל תגובה)",
"toast-disconnect-failed": "לא הצלחתי לבטל את פעולת הבוט.",
"toast-reconnect-failed": "לא הצליח לשחזר את הבוט.",
"toast-voice-failed": "לא הצליח לעדכן את מצב השמע.",
"toast-approve-failed": "לא ניתן לאשר את המשתמש.",
"toast-deny-failed": "לא הצליח לסרב לבקשה של המשתמש.",
"toast-revoke-failed": "לא הצלחתי לבטל את החשבון של המשתמש.",
},
users: {
"pending-title": "נמצא בהמתנה לאישור",
"pending-description":
"משתמשים הממתינים לאישור. יש להתאים את הקוד שמוצג כאן עם הקוד המוצג בשיחה שלהם ב-Telegram.",
"approved-title": "משתמשים מורשים",
"approved-description": "משתמשים שאושרו לנהל שיחה עם הבוט שלכם.",
user: "משתמש",
"pairing-code": "קוד התאמה",
"no-pending": "אין בקשות בתהליך",
"no-approved": "אין משתמשים מורשים",
unknown: "לא ידוע",
approve: "אישור",
deny: "לדחות",
revoke: "בטל",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Nome delle aree di lavoro",
user: "Utente",
selection: "Selezione del modello",
saving: "Salvo...",
save: "Salva modifiche",
@ -107,6 +106,10 @@ const TRANSLATIONS = {
"your-account": "Il tuo account",
"import-item": "Importa articolo",
},
channels: "Canali",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -183,18 +186,12 @@ const TRANSLATIONS = {
title: "Modalità chat",
chat: {
title: "Chat",
description:
"fornirà risposte basate sulla conoscenza generale del modello LLM e sul contesto del documento <b>e</b> che è disponibile.<br />Per utilizzare gli strumenti, sarà necessario utilizzare il comando @agent.",
},
query: {
title: "Query",
description:
'fornirà risposte solo se il contesto del documento viene trovato. Per utilizzare gli strumenti, sarà necessario utilizzare il comando "@agent".',
},
automatic: {
title: "Auto",
description:
'utilizzerà automaticamente gli strumenti se il modello e il fornitore supportano la chiamata nativa agli strumenti. <br /> Se la chiamata nativa agli strumenti non è supportata, sarà necessario utilizzare il comando "@agent" per utilizzare gli strumenti.',
},
},
history: {
@ -756,7 +753,6 @@ const TRANSLATIONS = {
see_less: "Visualizza meno",
see_more: "Visualizza altro",
tools: "Strumenti",
browse: "Naviga",
text_size_label: "Dimensione del testo",
select_model: "Seleziona il modello",
sources: "Fonti",
@ -769,8 +765,6 @@ const TRANSLATIONS = {
edit: "Modifica",
publish: "Pubblicare",
stop_generating: "Interrompi la generazione della risposta",
pause_tts_speech_message:
"Mettere in pausa la lettura vocale del messaggio",
slash_commands: "Comandi abbreviati",
agent_skills: "Competenze dell'agente",
manage_agent_skills: "Gestire le competenze degli agenti",
@ -1041,6 +1035,87 @@ const TRANSLATIONS = {
"Non sei assegnato a nessuno spazio di lavoro.\nContatta il tuo amministratore per richiedere l'accesso a uno spazio di lavoro.",
goToWorkspace: 'Vai allo spazio di lavoro "{{workspace}}"',
},
telegram: {
title: "Bot per Telegram",
description:
"Collega la tua istanza di AnythingLLM a Telegram in modo da poter chattare con i tuoi spazi di lavoro da qualsiasi dispositivo.",
setup: {
step1: {
title: "Passo 1: Crea il tuo bot Telegram",
description:
"Apri il bot @BotFather su Telegram, invia `/newbot` a @BotFather, segui le istruzioni e copia il token API.",
"open-botfather": "Avvia BotFather",
"instruction-1": "1. Apri il link o scansiona il codice QR",
"instruction-2":
"2. Invia <code>/newbot</code> a <code>@BotFather</code>",
"instruction-3": "3. Scegli un nome e un nome utente per il tuo bot.",
"instruction-4": "4. Copiare il token API che riceverete",
},
step2: {
title: "Passo 2: Collegare il tuo bot",
description:
"Incolla il token API che hai ricevuto da @BotFather e seleziona uno spazio di lavoro predefinito per il tuo bot, in modo che possa comunicare.",
"bot-token": "Token Bot",
"default-workspace": "Spazio di lavoro predefinito",
"no-workspace":
"Non sono disponibili spazi di lavoro. Ne verrà creato uno nuovo.",
connecting: "Connessione...",
"connect-bot": "Bot di connessione",
},
security: {
title: "Impostazioni di sicurezza consigliate",
description:
"Per una maggiore sicurezza, configurare queste impostazioni tramite @BotFather.",
"disable-groups": "— Impedire l'aggiunta di bot ai gruppi",
"disable-inline": "— Impedire l'uso di bot nelle ricerche inline",
"obscure-username":
"Utilizza un nome utente per il bot che non sia ovvio, per ridurre la sua visibilità.",
},
"toast-enter-token": "Si prega di inserire un token per il bot.",
"toast-connect-failed":
"Impossibile stabilire la connessione con il bot.",
},
connected: {
status: "Collegato",
"status-disconnected":
"Non connesso il token potrebbe essere scaduto o non valido",
"placeholder-token": "Incolla il nuovo token del bot...",
reconnect: "Riconnettersi",
workspace: "Spazio di lavoro",
"bot-link": "Link al bot",
"voice-response": "Risposta vocale",
disconnecting: "Disconnessione...",
disconnect: "Disconnetti",
"voice-text-only": "Testo solo",
"voice-mirror":
"Specchio (risposta vocale quando l'utente invia un messaggio vocale)",
"voice-always":
"Invia sempre un messaggio vocale (registra un audio con ogni risposta).",
"toast-disconnect-failed": "Impossibile disconnettere il bot.",
"toast-reconnect-failed":
"Impossibile ristabilire la connessione con il bot.",
"toast-voice-failed": "Impossibile aggiornare la modalità vocale.",
"toast-approve-failed": "Impossibile approvare l'utente.",
"toast-deny-failed": "Impossibile negare l'accesso all'utente.",
"toast-revoke-failed": "Impossibile revocare l'accesso dell'utente.",
},
users: {
"pending-title": "In attesa di approvazione",
"pending-description":
"Utenti in attesa di verifica. Confrontare il codice di abbinamento visualizzato qui con quello visualizzato nella loro chat di Telegram.",
"approved-title": "Utenti approvati",
"approved-description":
"Utenti che sono stati approvati per chattare con il vostro bot.",
user: "Utente",
"pairing-code": "Codice di abbinamento",
"no-pending": "Non ci sono richieste in sospeso.",
"no-approved": "Nessun utente autorizzato",
unknown: "Sconosciuto",
approve: "Approvare",
deny: "Negare",
revoke: "Annullare",
},
},
};
export default TRANSLATIONS;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "ワークスペース名",
user: "ユーザー",
selection: "モデル選択",
saving: "保存中...",
save: "変更を保存",
@ -105,6 +104,10 @@ const TRANSLATIONS = {
"your-account": "あなたのアカウント",
"import-item": "輸入品",
},
channels: "チャンネル",
"available-channels": {
telegram: "テレグラム",
},
},
login: {
"multi-user": {
@ -180,18 +183,12 @@ const TRANSLATIONS = {
title: "チャットモード",
chat: {
title: "チャット",
description:
"LLMの一般的な知識と、関連するドキュメントの文脈に基づいて、回答を提供します。ツールを使用するには、`@agent`コマンドを使用する必要があります。",
},
query: {
title: "クエリ",
description:
"該当する情報が見つかった場合、回答を<b>のみ</b>提供します。ツールを使用するには、@agentコマンドを使用する必要があります。",
},
automatic: {
title: "自動車",
description:
"ネイティブなツール呼び出しをサポートしている場合、モデルとプロバイダーが自動的にツールを使用します。<br />ネイティブなツール呼び出しがサポートされていない場合は、@agentコマンドを使用してツールを使用する必要があります。",
},
},
history: {
@ -736,7 +733,6 @@ const TRANSLATIONS = {
see_less: "詳細を見る",
see_more: "詳細を見る",
tools: "道具",
browse: "閲覧",
text_size_label: "文字サイズ",
select_model: "モデルを選択",
sources: "出典",
@ -749,7 +745,6 @@ const TRANSLATIONS = {
edit: "編集",
publish: "出版",
stop_generating: "応答の生成を停止する",
pause_tts_speech_message: "メッセージのテキスト読み上げ機能を一時停止する",
slash_commands: "スラッシュコマンド",
agent_skills: "エージェントのスキル",
manage_agent_skills: "エージェントのスキル管理",
@ -1017,6 +1012,83 @@ const TRANSLATIONS = {
"現在、あなたはどのワークスペースにも割り当てられていません。\nワークスペースへのアクセスを要求するには、管理者にお問い合わせください。",
goToWorkspace: 'ワークスペースに移動 "{{workspace}}"',
},
telegram: {
title: "テレグラムボット",
description:
"AnyLLM のインスタンスを Telegram に接続することで、あらゆるデバイスからワークスペースとのチャットが可能になります。",
setup: {
step1: {
title: "ステップ1Telegramボットを作成する",
description:
"Telegramの@BotFatherを開き、「/newbot」と入力して<code>@BotFather</code>に送信します。指示に従い、APIトークンをコピーしてください。",
"open-botfather": "BotFather を起動する",
"instruction-1": "1. リンクを開くか、QRコードをスキャンする",
"instruction-2":
"2. 「<code>」/「newbot」を「</code>」で、「<code>」@「BotFather」に送信してください。",
"instruction-3": "3. 独自の名前とユーザー名をボットに設定してください",
"instruction-4": "4. 受け取ったAPIトークンをコピーしてください",
},
step2: {
title: "ステップ2ボットとの接続",
description:
"@BotFatherから受け取ったAPIトークンを貼り付け、ボットとのチャットに使用するデフォルトのワークスペースを選択してください。",
"bot-token": "ボット トークン",
"default-workspace": "デフォルトのワークスペース",
"no-workspace":
"利用可能な作業スペースがありません。新しい作業スペースが作成されます。",
connecting: "接続中...",
"connect-bot": "コネクトボット",
},
security: {
title: "推奨されるセキュリティ設定",
description:
"追加のセキュリティのため、@BotFatherでこれらの設定を設定してください。",
"disable-groups": "— グループへのボットの追加を防止",
"disable-inline": "— インライン検索でのボットの使用を防止",
"obscure-username":
"目立たないユーザー名をbotに使用することで、発見されにくくする。",
},
"toast-enter-token": "ボットのトークンを入力してください。",
"toast-connect-failed": "ボットとの接続に失敗しました。",
},
connected: {
status: "接続されている",
"status-disconnected":
"通信エラー - トークンが無効または期限切れになっている可能性があります",
"placeholder-token": "新しいボットのトークンを貼り付け...",
reconnect: "再接続",
workspace: "作業スペース",
"bot-link": "ボットへのリンク",
"voice-response": "音声応答",
disconnecting: "接続を解除...",
disconnect: "接続を解除する",
"voice-text-only": "テキストのみ",
"voice-mirror": "(ユーザーが音声で送信した場合、音声で返信)",
"voice-always": "常に音声メッセージ(返信ごとに音声データを送信)",
"toast-disconnect-failed": "ボットとの接続を解除できませんでした。",
"toast-reconnect-failed": "ボットとの再接続に失敗しました。",
"toast-voice-failed": "音声モードの更新に失敗しました。",
"toast-approve-failed": "ユーザーの承認に失敗しました。",
"toast-deny-failed": "ユーザーからの拒否を拒否できませんでした。",
"toast-revoke-failed": "ユーザーの権限停止に失敗。",
},
users: {
"pending-title": "承認待ち",
"pending-description":
"本人情報の確認待ちのユーザー。ここに表示されているペアリングコードを、彼らがTelegramで表示しているコードと照合してください。",
"approved-title": "承認されたユーザー",
"approved-description":
"あなたのボットとのチャットを許可されたユーザー。",
user: "利用者",
"pairing-code": "組み合わせコード",
"no-pending": "処理中のリクエストはありません",
"no-approved": "承認されたユーザーはいません",
unknown: "不明",
approve: "承認",
deny: "否定",
revoke: "無効化する",
},
},
};
export default TRANSLATIONS;

View File

@ -50,7 +50,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "워크스페이스 이름",
user: "사용자",
selection: "모델 선택",
saving: "저장 중...",
save: "저장",
@ -104,6 +103,10 @@ const TRANSLATIONS = {
"your-account": "당신의 계정",
"import-item": "수입 품목",
},
channels: "채널",
"available-channels": {
telegram: "텔레그램",
},
},
login: {
"multi-user": {
@ -185,18 +188,12 @@ const TRANSLATIONS = {
title: "채팅 모드",
chat: {
title: "채팅",
description:
"LLM의 일반적인 지식과 관련 문맥 정보를 활용하여 답변을 제공합니다. 도구를 사용하려면 @agent 명령어를 사용해야 합니다.",
},
query: {
title: "쿼리",
description:
"문서 맥락이 발견되면 <b>에만</b> 답변을 제공합니다.<br /> 도구를 사용하려면 @agent 명령을 사용해야 합니다.",
},
automatic: {
title: "자동",
description:
"모델과 제공업체가 네이티브 도구 호출을 지원하는 경우, 자동으로 도구를 사용합니다. <br /> 네이티브 도구 호출이 지원되지 않는 경우, 도구를 사용하려면 @agent 명령을 사용해야 합니다.",
},
},
history: {
@ -821,7 +818,6 @@ const TRANSLATIONS = {
see_less: "더 보기",
see_more: "더 보기",
tools: "도구",
browse: "검색",
text_size_label: "글자 크기",
select_model: "모델 선택",
sources: "출처",
@ -834,7 +830,6 @@ const TRANSLATIONS = {
edit: "수정",
publish: "출판",
stop_generating: "응답 생성 중단",
pause_tts_speech_message: "메시지의 텍스트 음성 변환(TTS) 기능을 일시 중지",
slash_commands: "슬래시 명령어",
agent_skills: "에이전트의 역량",
manage_agent_skills: "에이전트 역량 관리",
@ -995,6 +990,82 @@ const TRANSLATIONS = {
"현재 워크스페이스에 할당되지 않았습니다.\n워크스페이스에 대한 접근을 요청하려면 관리자에게 문의하세요.",
goToWorkspace: '워크스페이스로 이동 "{{workspace}}"',
},
telegram: {
title: "텔레그램 봇",
description:
"AnyLLM 인스턴스를 Telegram과 연결하여, 어떤 기기에서든 워크스페이스와 채팅할 수 있도록 합니다.",
setup: {
step1: {
title: "1단계: 텔레그램 봇을 만드세요",
description:
"텔레그램에서 @BotFather를 열고, <code>/newbot</code>를 <code>@BotFather</code>에게 보내고, 안내에 따라 진행하여 API 토큰을 복사합니다.",
"open-botfather": "BotFather 시작",
"instruction-1": "1. 링크를 열거나 QR 코드를 스캔",
"instruction-2":
"2. <code>/newbot</code>를 <code>@BotFather</code>에게 전송",
"instruction-3": "3. 봇의 이름과 사용자 이름을 선택하세요.",
"instruction-4": "4. 받은 API 토큰을 복사합니다.",
},
step2: {
title: "2단계: 봇을 연결합니다.",
description:
"@BotFather로부터 받은 API 토큰을 복사하여, 봇이 채팅할 기본 워크스페이스를 선택하세요.",
"bot-token": "봇 토큰",
"default-workspace": "기본 워크스페이스",
"no-workspace":
"사용 가능한 작업 공간이 없습니다. 새로운 작업 공간이 생성될 것입니다.",
connecting: "연결 중...",
"connect-bot": "연결 봇",
},
security: {
title: "권장 보안 설정",
description:
"추가적인 보안을 위해, @BotFather에서 다음 설정을 구성해 주세요.",
"disable-groups": "— 그룹에 봇 추가 방지",
"disable-inline": "— 인라인 검색에서 봇 사용을 방지",
"obscure-username":
"자명한 봇 사용자 이름을 피하고, 발견 가능성을 줄이기 위해",
},
"toast-enter-token": "봇 토큰을 입력해 주세요.",
"toast-connect-failed": "봇 연결에 실패했습니다.",
},
connected: {
status: "연결된",
"status-disconnected":
"연결되지 않음 토큰이 만료되었거나 유효하지 않을 수 있습니다",
"placeholder-token": "새로운 봇 토큰을 붙여넣으세요...",
reconnect: "재 연결",
workspace: "업무 공간",
"bot-link": "봇 링크",
"voice-response": "음성 응답",
disconnecting: "연결 해제 중...",
disconnect: "연결 해제",
"voice-text-only": "텍스트만",
"voice-mirror": "(사용자가 음성으로 응답하면, 음성으로 답변)",
"voice-always": "항상 음성 메시지 (답변과 함께 오디오 전송)",
"toast-disconnect-failed": "봇과의 연결을 해제하는 데 실패했습니다.",
"toast-reconnect-failed": "봇과의 연결에 실패했습니다.",
"toast-voice-failed": "음성 모드 업데이트에 실패했습니다.",
"toast-approve-failed": "사용자 승인에 실패했습니다.",
"toast-deny-failed": "사용자에게 거부 권한을 부여하지 못함.",
"toast-revoke-failed": "사용자 계정 삭제에 실패했습니다.",
},
users: {
"pending-title": "승인 대기 중",
"pending-description":
"승인 대기 중인 사용자. 여기 표시된 매칭 코드를 자신의 Telegram 채팅에서 표시된 코드로 일치시켜 주세요.",
"approved-title": "승인된 사용자",
"approved-description": "당신의 봇과 대화할 수 있도록 승인된 사용자.",
user: "사용자",
"pairing-code": "코드 매칭",
"no-pending": "처리 중인 요청이 없습니다.",
"no-approved": "승인된 사용자가 없습니다",
unknown: "알 수 없음",
approve: "승인",
deny: "부인",
revoke: "취소",
},
},
};
export default TRANSLATIONS;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Darbo srities pavadinimas",
user: "Vartotojas",
selection: "Modelio pasirinkimas",
saving: "Saugoma...",
save: "Išsaugoti pakeitimus",
@ -112,6 +111,10 @@ const TRANSLATIONS = {
contact: "Susisiekti su pagalba",
"browser-extension": "Naršyklės plėtinys",
"mobile-app": "AnythingLLM mobiliesiems",
channels: "Kanalai",
"available-channels": {
telegram: "„Telegram“",
},
},
login: {
"multi-user": {
@ -195,18 +198,12 @@ const TRANSLATIONS = {
title: "Pokalbio režimas",
automatic: {
title: "Auto",
description:
"automatiškai naudos įrankius, jei modelis ir tiekėjas palaiko natūralų įrankių iškvietimą.<br />Jei natūralus įrankių iškvietimas nepalaikomas, norėdami naudoti įrankius turėsite naudoti @agent komandą.",
},
chat: {
title: "Pokalbis",
description:
"pateiks atsakymus remdamasis LLM bendrosiomis žiniomis <b>ir</b> rastu dokumentų kontekstu.<br />Norėdami naudoti įrankius, turėsite naudoti @agent komandą.",
},
query: {
title: "Užklausa",
description:
"pateiks atsakymus <b>tik</b> jei bus rastas dokumentų kontekstas.<br />Norėdami naudoti įrankius, turėsite naudoti @agent komandą.",
},
},
history: {
@ -834,7 +831,6 @@ const TRANSLATIONS = {
source_count_other: "{{count}} nuorodos",
document: "Dokumentas",
similarity_match: "atitikimas",
pause_tts_speech_message: "Sustabdyti garsinį žinutės skaitymą",
fork: "Atskirti (Fork)",
delete: "Ištrinti",
cancel: "Atšaukti",
@ -866,7 +862,6 @@ const TRANSLATIONS = {
normal: "Normalus",
large: "Didelis",
tools: "Įrankiai",
browse: "Naršyti",
text_size_label: "Teksto dydis",
select_model: "Pasirinkti modelį",
slash_commands: "Komandos su „/“",
@ -1016,6 +1011,86 @@ const TRANSLATIONS = {
},
},
},
telegram: {
title: "Telegram robotas",
description:
"Prisijunkite savo „AnythingLLM“ instanciją prie „Telegram“, kad galėtumėte kalbėti su savo darbo vietomis iš bet kurio įrenginio.",
setup: {
step1: {
title: "1 žingsnis: Sukurkite savo Telegram botą",
description:
"Atidarykite @BotFather kanalą Telegram, atsiųskite `/newbot` į <code>@BotFather</code>, sekite instrukcijas ir kopijuokite API raktą.",
"open-botfather": "Atidarykite „BotFather“",
"instruction-1": "1. Atidarykite nuorodą arba nuskaitykite QR kodą",
"instruction-2":
"2. Siųkite <code>/newbot</code> adresą <code>@BotFather</code>",
"instruction-3":
"3. Pasirinkite pavadinimą ir vartotojo vardą savo botui.",
"instruction-4": "4. Paškopuokite gautą API žymiklį.",
},
step2: {
title: "2 etapas: Prisijunkite prie savo „bot“",
description:
"Įveskite API žymiklį, kurį gavote iš @BotFather, ir pasirinkite numatytą darbo vietą, kur jūsų bot galės kalbėti.",
"bot-token": "„Bot Token“",
"default-workspace": "Numatytasis darbo erdvė",
"no-workspace": "Nėra laisvų darbo vietų. Bus sukurta nauja.",
connecting: "Prisijungiam...",
"connect-bot": "„Connect Bot“",
},
security: {
title: "Išvardytos saugos nustatymai",
description:
"Papildomos saugumo užtikrinimui, konfigūruokite šias nustatymus @BotFather.",
"disable-groups": " Prieš pridėdamas botą į grupes",
"disable-inline":
" Užtikrinkite, kad „bot“ negali būti naudojamas paieškoje.",
"obscure-username":
"Naudokite neatsinaužytą „bot“ vardą, kad sumažintumėte jo aptikimo galimybes.",
},
"toast-enter-token": "Prašome įvesti boto žymę.",
"toast-connect-failed": "Nepavyko susieti robotą.",
},
connected: {
status: "Susijęs",
"status-disconnected":
"Nusijungtas žetonas gali būti neregistruotas arba netinkamas",
"placeholder-token": "Įdiekite naują „bot“ žetoną…",
reconnect: "Vykdyti ryšį",
workspace: "Darbo zona",
"bot-link": "„Bot Link“",
"voice-response": "Garsas kaip atsakymas",
disconnecting: "Atsijungimas...",
disconnect: "Nutraukti ryšį",
"voice-text-only": "Tik tekstas",
"voice-mirror":
"Atspindžio funkcija (atsakymas balsu, kai vartotojas siunčia balsą)",
"voice-always":
"Visada naudokite balsą (siųsdami garsą su kiekvienu atsakymu)",
"toast-disconnect-failed": "Nepavyko atjungti robotą.",
"toast-reconnect-failed": "Nepavyko atkurti ryšį su botu.",
"toast-voice-failed": "Nepavyko atnaujinti balsinio režimo.",
"toast-approve-failed": "Nepavyko patvirtinti vartotojo.",
"toast-deny-failed": "Nepavyko užtikrinti vartotojo saugumo.",
"toast-revoke-failed": "Nepavyko atšalinti vartotojo.",
},
users: {
"pending-title": "Laikant patvirtinimo",
"pending-description":
"Naudotojai, laukiantys patvirtinimo. Palyginkite čia pateiktą kodą su tuo, kuris rodomas jų „Telegram“ pokalbyje.",
"approved-title": "Įsijungę vartotojai",
"approved-description":
"Naudotojai, kuriems suteikiama galimybė kalbėti su jūsų botu.",
user: "Naudotojas",
"pairing-code": "Kombinacijos kodas",
"no-pending": "Nėra atidėtų užklausų",
"no-approved": "Nėra patvirtintų vartotojų",
unknown: "Nenurodytas",
approve: "Aptinka",
deny: "Atsisakyti",
revoke: "Anuliuoti",
},
},
};
export default TRANSLATIONS;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Darba telpas nosaukums",
user: "Lietotājs",
selection: "Modeļa izvēle",
saving: "Saglabā...",
save: "Saglabāt izmaiņas",
@ -105,6 +104,10 @@ const TRANSLATIONS = {
"your-account": "Jūsu konts",
"import-item": "Importētā prece",
},
channels: "Kanāli",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -188,18 +191,12 @@ const TRANSLATIONS = {
title: "Sarunas režīms",
chat: {
title: "Saruna",
description:
'sniedz atbildes, izmantojot LLM vispārīgo zināšanu un dokumenta kontekstu, kas ir pieejams.<br />Lai izmantotu rīkus, jums jāizmantojat komandu "@agent".',
},
query: {
title: "Vaicājums",
description:
'sniedz atbildes <b> tikai </b>, ja dokumenta konteksts ir atrasts. <br />Lai izmantotu rīkus, jums būs jāizmanto komanda "@agent".',
},
automatic: {
title: "Automobiļs",
description:
'automātiski izmantos rīkus, ja modelis un sniedzējs atbalsta vietējo rīku izmantošanu. <br />Ja vietējā rīku izmantošana netiek atbalstīta, jums būs jāizmantojas "@agent" komanda, lai izmantotu rīkus.',
},
},
history: {
@ -840,7 +837,6 @@ const TRANSLATIONS = {
see_less: "Skatīt mazāk",
see_more: "Skatīt vairāk",
tools: "Rīki",
browse: "Izpētiet",
text_size_label: "Teksta izmērs",
select_model: "Izvēlieties modeli",
sources: "Avotus",
@ -853,8 +849,6 @@ const TRANSLATIONS = {
edit: "Rediģēt",
publish: "Publicēt",
stop_generating: "Atsauciet atbildes ģenerēšanu",
pause_tts_speech_message:
"Pārtrauciet TTS (teksta-izrunas) žēstā vēstījuma izrunu.",
slash_commands: "Īs termini komandās",
agent_skills: "Aģenta prasmes",
manage_agent_skills: "Iesaista aģenta prasmes",
@ -1020,6 +1014,86 @@ const TRANSLATIONS = {
"Jūs nav piešķirts nevienai darba vietai.\nLūdzu, sazinieties ar savu administratoru, lai pieprasītu piekļuvi darba vietai.",
goToWorkspace: 'Pāriet uz darba vietu "{{workspace}}"',
},
telegram: {
title: "Telegram bot",
description:
"Iespējiet savu AnythingLLM instanci, lai varētu tikt savienots ar Telegram, un tāpēc varēsat runāt ar saviem darba grupām no jebkura ierīces.",
setup: {
step1: {
title: "1. darbība: Izveidot savu Telegram botu",
description:
"Atveriet `@BotFather` Telegramā, nosūtiet `/newbot` un ievietojiet to adresē <code>@BotFather</code>, sekojiet norādījumiem un kopējiet API atslēgu.",
"open-botfather": "Atvērt BotFather",
"instruction-1": "1. Atveriet saiti vai skenējiet QR kodu",
"instruction-2":
"2. Nosūtiet <code>/newbot</code> uz <code>@BotFather</code>",
"instruction-3":
"3. Izvēlieties nosaukumu un lietotājvārdu savam botam",
"instruction-4": "4. Kopējiet API atslēgu, ko saņemat",
},
step2: {
title: "2. darbība: Pievienojiet savu botu",
description:
"Ievietojiet API atslēgu, ko saņēsit no @BotFather, un izvēlieties nokārtotā darba telpu, kuras jūsu bots varēs veikt sazi.",
"bot-token": "Bots tokens",
"default-workspace": "Pamatojas darba videne",
"no-workspace": "Nav pieejamas darba vietas. Tiks izveidota jauna.",
connecting: "Savienojums...",
"connect-bot": "Saistītais bot",
},
security: {
title: "Ieteicamās drošības iestatījumi",
description:
"Lai nodrošinātu papildu drošību, konfigurējiet šos iestatījumus, izmantojot @BotFather.",
"disable-groups": "— Novērst, lai boti tiktu pievienoti grupām",
"disable-inline":
"— Novērst, lai bots tiktu izmantoti tiešajā meklēšanā.",
"obscure-username":
"Izmantojiet neparādu botu lietotāju vārdu, lai samazinātu atklājamo iespēju.",
},
"toast-enter-token": "Lūdzu, ievadiet bot tokenu.",
"toast-connect-failed": "Neizdevās pievienot botu.",
},
connected: {
status: "Saistīts",
"status-disconnected":
"Atvienots — tokens var būt nolaidēts vai nederīgs",
"placeholder-token": "Ievietojiet jaunu bot tokenu...",
reconnect: "Atjaunot sazi",
workspace: "Darba telpa",
"bot-link": "Bots saite",
"voice-response": "Balss atbildes",
disconnecting: "Atvienojot...",
disconnect: "Izslēgt",
"voice-text-only": "Tikai teksts",
"voice-mirror":
"Atspoguļošana (atbildēt ar balsi, kad lietotājs nosauc balsi)",
"voice-always":
"Vienmēr pievienojiet audio (sūtiet audio ar katru atbildi).",
"toast-disconnect-failed": "Neizdevās izslēgt botu.",
"toast-reconnect-failed": "Neizdevās atjaunot saikni ar botu.",
"toast-voice-failed": "Neizdevās atjaunināt balsī noteiktās režimas.",
"toast-approve-failed": "Nespēja apstiprināt lietotāju.",
"toast-deny-failed": "Nespēja atspējot lietotāju.",
"toast-revoke-failed": "Neizdevās atcelt lietotāja tiesības.",
},
users: {
"pending-title": "Atkarībā no apstākļiem",
"pending-description":
"Izmantotāji, kas gaida apstiprinājumu. Salīdziniet šeit norādīto koda numuru ar to, kas redzams viņu Telegram sarunā.",
"approved-title": "Atļautie lietotāji",
"approved-description":
"Izmantotāji, kuriem ir atļauts veikt saziņai ar jūsu botu.",
user: "Izmantotājs",
"pairing-code": "Kopējā koda numura kombinācija",
"no-pending": "Neizpildīti pieprasījumi",
"no-approved": "No apstiprinātiem lietotājiem",
unknown: "Nezināms",
approve: "Aptver",
deny: "Atbrīsties; atgrūst",
revoke: "Atcel",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Werkruimten Naam",
user: "Gebruiker",
selection: "Model Selectie",
saving: "Opslaan...",
save: "Wijzigingen opslaan",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Uw account",
"import-item": "Importeren",
},
channels: "Kanaal",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -181,18 +184,12 @@ const TRANSLATIONS = {
title: "Chatmodus",
chat: {
title: "Chat",
description:
"zal antwoorden geven met de algemene kennis van het LLM en de relevante context uit het document. U moet het `@agent`-commando gebruiken om tools te gebruiken.",
},
query: {
title: "Query",
description:
"zal antwoorden <b>alleen</b> geven, indien de context van het document wordt gevonden.<br />U moet het commando @agent gebruiken om tools te gebruiken.",
},
automatic: {
title: "Auto",
description:
"zal automatisch tools gebruiken als het model en de provider native tool-aanroepen ondersteunen.<br />Als native tooling niet wordt ondersteund, moet u het `@agent`-commando gebruiken om tools te gebruiken.",
},
},
history: {
@ -744,7 +741,6 @@ const TRANSLATIONS = {
see_less: "Minder zien",
see_more: "Meer zien",
tools: "Gereedschap",
browse: "Bladeren",
text_size_label: "Lettergrootte",
select_model: "Kies het model",
sources: "Bronnen",
@ -757,7 +753,6 @@ const TRANSLATIONS = {
edit: "Bewerk",
publish: "Publiceren",
stop_generating: "Stoppen met het genereren van antwoorden",
pause_tts_speech_message: "Pauzeer de spraak van de tekstberichten.",
slash_commands: "Korte commando's",
agent_skills: "Vaardigheden van agenten",
manage_agent_skills: "Beheer van de vaardigheden van de agent",
@ -1020,6 +1015,88 @@ const TRANSLATIONS = {
"Je bent nog niet toegewezen aan een werkruimte.\nNeem contact op met je beheerder om toegang te vragen tot een werkruimte.",
goToWorkspace: 'Ga naar de werkruimte "{{workspace}}"',
},
telegram: {
title: "Telegram Bot",
description:
"Verbind uw AnythingLLM-instantie met Telegram, zodat u vanuit elk apparaat kunt communiceren met uw werkruimtes.",
setup: {
step1: {
title: "Stap 1: Maak je Telegram-bot",
description:
"Open het @BotFather-kanaal in Telegram, stuur `/newbot` naar @BotFather, volg de instructies en kopieer het API-token.",
"open-botfather": "Open BotFather",
"instruction-1": "1. Open het link of scan de QR-code",
"instruction-2":
"2. Stuur <code>/newbot</code> naar <code>@BotFather</code>",
"instruction-3": "3. Kies een naam en gebruikersnaam voor je bot",
"instruction-4": "4. Kopieer de API-token die je ontvangt",
},
step2: {
title: "Stap 2: Verbind uw bot",
description:
"Plak de API-token die je van @BotFather hebt ontvangen en selecteer een standaard werkruimte voor je bot om mee te communiceren.",
"bot-token": "Bot-token",
"default-workspace": "Standaard werkruimte",
"no-workspace":
"Er zijn geen beschikbare werkplekken. Een nieuwe zal worden aangemaakt.",
connecting: "Verbinding wordt gemaakt...",
"connect-bot": "Connect Bot",
},
security: {
title: "Aanbevolen beveiligingsinstellingen",
description:
"Voor extra beveiliging, configureer deze instellingen via @BotFather.",
"disable-groups": "— Voorkom het toevoegen van bots aan groepen",
"disable-inline":
"— Voorkom dat de bot wordt gebruikt in inline zoekopdrachten",
"obscure-username":
"Gebruik een bot-username dat niet direct herkenbaar is, om de vindbaarheid te verminderen.",
},
"toast-enter-token": "Voer alstublieft een bot-token in.",
"toast-connect-failed": "Verbinding met de bot is mislukt.",
},
connected: {
status: "Verbonden",
"status-disconnected":
"Niet verbonden het token kan verlopen zijn of ongeldig",
"placeholder-token": "Plak het nieuwe bot-token...",
reconnect: "Herstellen van de verbinding",
workspace: "Werkplek",
"bot-link": "Bot-link",
"voice-response": "Spraakherkenning",
disconnecting: "Verbinding verbreken...",
disconnect: "Aansluiting verbreiden",
"voice-text-only": "Alleen tekst",
"voice-mirror":
"Spiegel (antwoord met spraak wanneer de gebruiker spraak verzendt)",
"voice-always":
"Zorg ervoor dat er altijd een audio-opname (een geluidsfragment) bij de reactie wordt toegevoegd.",
"toast-disconnect-failed":
"Het was niet mogelijk om de robot los te koppelen.",
"toast-reconnect-failed": "Fout bij het opnieuw verbinden van de bot.",
"toast-voice-failed": "Niet mogelijk om de spraakmodus bij te werken.",
"toast-approve-failed": "Fout bij goedkeuren van gebruiker.",
"toast-deny-failed": "Niet in staat om gebruiker te weigeren.",
"toast-revoke-failed":
"Fout bij het intrekken van het gebruikersaccount.",
},
users: {
"pending-title": "Afhankelijk van goedkeuring",
"pending-description":
"Gebruikers die nog geverifieerd moeten worden. Vergelijk de code die hier wordt getoond met de code die in hun Telegram-chat wordt weergegeven.",
"approved-title": "Goedgekeurde gebruikers",
"approved-description":
"Gebruikers die zijn goedgekeurd om met uw bot te communiceren.",
user: "Gebruiker",
"pairing-code": "Code voor het koppelen",
"no-pending": "Er zijn geen lopende verzoeken.",
"no-approved": "Geen goedgekeurde gebruikers",
unknown: "Onbekend",
approve: "Goedkeuren",
deny: "Afgewijzen",
revoke: "Intrekken",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Nazwa obszaru roboczego",
user: "Użytkownik",
selection: "Wybór modelu",
saving: "Zapisywanie...",
save: "Zapisz zmiany",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Twój profil",
"import-item": "Importuj element",
},
channels: "Kanały",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -188,18 +191,12 @@ const TRANSLATIONS = {
title: "Tryb czatu",
chat: {
title: "Czat",
description:
"zapewni odpowiedzi, wykorzystując ogólną wiedzę LLM oraz kontekst dokumentu, w którym ta wiedza znajduje się.<br />Będziesz musiał użyć komendy `@agent` w celu korzystania z narzędzi.",
},
query: {
title: "Zapytanie (wyszukiwanie)",
description:
"będzie dostarczać odpowiedzi <b>tylko</b>, jeśli zostanie zidentyfikowany kontekst dokumentu.<br />Będziesz musiał użyć komendy `@agent` w celu korzystania z narzędzi.",
},
automatic: {
title: "Samochód",
description:
"automatycznie będzie wykorzystywał narzędzia, jeśli model i dostawca obsługują natywne wywoływanie narzędzi. Jeśli natywne narzędzia nie są obsługiwane, konieczne będzie użycie polecenia `@agent` w celu wykorzystania narzędzi.",
},
},
history: {
@ -837,7 +834,6 @@ const TRANSLATIONS = {
see_less: "Zobacz mniej",
see_more: "Zobacz więcej",
tools: "Narzędzia",
browse: "Przeglądaj",
text_size_label: "Rozmiar czcionki",
select_model: "Wybierz model",
sources: "Źródła",
@ -850,7 +846,6 @@ const TRANSLATIONS = {
edit: "Edytuj",
publish: "Opublikować",
stop_generating: "Przestań generować odpowiedź",
pause_tts_speech_message: "Wstrzymać odtwarzanie mowy z wiadomości",
slash_commands: "Polecenia skrótowe",
agent_skills: "Umiejętności agenta",
manage_agent_skills: "Zarządzanie umiejętnościami agentów",
@ -1016,6 +1011,87 @@ const TRANSLATIONS = {
"Nie jesteś przypisany do żadnego obszaru roboczego.\nSkontaktuj się z administratorem, aby poprosić o dostęp do obszaru roboczego.",
goToWorkspace: 'Przejdź do obszaru roboczego "{{workspace}}"',
},
telegram: {
title: "Bot na Telegramie",
description:
"Połącz swoją instancję AnythingLLM z Telegramem, aby móc rozmawiać z przestrzeniami roboczymi z dowolnego urządzenia.",
setup: {
step1: {
title: "Krok 1: Utwórz swojego bota w Telegramie",
description:
"Otwórz aplikację @BotFather w Telegramie, wyślij wiadomość <code>/newbot</code> do <code>@BotFather</code>, postępuj zgodnie z instrukcjami i skopiuj token API.",
"open-botfather": "Otwórz BotFather",
"instruction-1": "1. Otwórz link lub zeskanuj kod QR",
"instruction-2":
"2. Wyślij <code>/newbot</code> na adres <code>@BotFather</code>",
"instruction-3":
"3. Wybierz nazwę i nazwę użytkownika dla swojego robota.",
"instruction-4": "4. Skopiuj token API, który otrzymasz.",
},
step2: {
title: "Krok 2: Połącz swojego robota",
description:
"Wklej token API, który otrzymałeś od @BotFather, i wybierz domyślny przestrzeń roboczą, z której Twój bot będzie mógł komunikować się.",
"bot-token": "Token Bot",
"default-workspace": "Domyślne miejsce pracy",
"no-workspace":
"Brak dostępnych miejsc pracy. Nowe zostanie utworzone.",
connecting: "Połączenie...",
"connect-bot": "Bot łączący",
},
security: {
title: "Zalecane ustawienia bezpieczeństwa",
description:
"W celu zwiększenia bezpieczeństwa, skonfiguruj te ustawienia w kanale @BotFather.",
"disable-groups": "— Zapobiegaj dodawaniu botów do grup",
"disable-inline":
"— Zapobieg użyciu robota w wyszukiwaniach w czasie rzeczywistym.",
"obscure-username":
"Użyj nietypowej nazwy użytkownika dla bota, aby zmniejszyć jego widoczność.",
},
"toast-enter-token": "Prosimy o wprowadzenie tokena dla bota.",
"toast-connect-failed": "Nie udało się nawiązać połączenia z botem.",
},
connected: {
status: "Połączony",
"status-disconnected":
"Brak połączenia token może być nieprawidłowy lub wygasł",
"placeholder-token": "Wklej nowy token dla bota...",
reconnect: "Ponowne połączenie",
workspace: "Przestrzeń robocza",
"bot-link": "Link do bota",
"voice-response": "Reakcja na głos",
disconnecting: "Odłączanie...",
disconnect: "Odłączyć",
"voice-text-only": "Tylko tekst",
"voice-mirror":
"Odbiór (odpowiedź za pomocą głosu, gdy użytkownik wysyła głos)",
"voice-always":
"Zawsze dołączaj nagranie (wysyłaj dźwięk wraz z każdą odpowiedzią)",
"toast-disconnect-failed": "Nie udało się odłączyć bota.",
"toast-reconnect-failed": "Nie udało się nawiązać połączenia z botem.",
"toast-voice-failed": "Nie udało się zaktualizować trybu głosu.",
"toast-approve-failed": "Nie udało się zatwierdzić użytkownika.",
"toast-deny-failed": "Nie udało się odrzucić żądania użytkownika.",
"toast-revoke-failed": "Nie udało się odwołać konta użytkownika.",
},
users: {
"pending-title": "Czekając na zatwierdzenie",
"pending-description":
"Użytkownicy, którzy czekają na weryfikację. Dopasuj kod parowania, który znajduje się tutaj, z tym, który widnieje w ich rozmowie na Telegramie.",
"approved-title": "Użytkownicy, którym przyznano uprawnienia",
"approved-description":
"Użytkownicy, którzy zostali zatwierdzeni do rozmowy z Twoim botem.",
user: "Użytkownik",
"pairing-code": "Kod dopasowania",
"no-pending": "Brak oczekujących żądań",
"no-approved": "Brak zatwierdzonych użytkowników",
unknown: "Nieznany",
approve: "Zaakceptować",
deny: "Odrzucać",
revoke: "Odstrzegać",
},
},
};
export default TRANSLATIONS;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Nome do Workspace",
user: "Usuário",
selection: "Seleção de Modelo",
saving: "Salvando...",
save: "Salvar alterações",
@ -105,6 +104,10 @@ const TRANSLATIONS = {
"your-account": "Sua Conta",
"import-item": "Importar Item",
},
channels: "Canais",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -188,18 +191,12 @@ const TRANSLATIONS = {
title: "Modo de Chat",
chat: {
title: "Chat",
description:
'fornecerá respostas com base no conhecimento geral do LLM e no contexto do documento encontrado.<br />Você precisará usar o comando "@agent" para utilizar as ferramentas.',
},
query: {
title: "Consulta",
description:
'fornecerá respostas <b>apenas</b> caso o contexto do documento seja encontrado.<br />Você precisará usar o comando "@agent" para utilizar as ferramentas.',
},
automatic: {
title: "Automóvel",
description:
'utilizará automaticamente as ferramentas, se o modelo e o provedor suportarem a chamada nativa de ferramentas. Se a chamada nativa de ferramentas não for suportada, você precisará usar o comando "@agent" para utilizar as ferramentas.',
},
},
history: {
@ -821,7 +818,6 @@ const TRANSLATIONS = {
see_less: "Ver menos",
see_more: "Ver mais",
tools: "Ferramentas",
browse: "Navegar",
text_size_label: "Tamanho do texto",
select_model: "Selecione o modelo",
sources: "Fontes",
@ -834,7 +830,6 @@ const TRANSLATIONS = {
edit: "Editar",
publish: "Publicar",
stop_generating: "Pare de gerar respostas",
pause_tts_speech_message: "Pausar a leitura de voz da mensagem",
slash_commands: "Comandos Rápidos",
agent_skills: "Habilidades do Agente",
manage_agent_skills: "Gerenciar as habilidades dos agentes",
@ -992,6 +987,86 @@ const TRANSLATIONS = {
"Você ainda não está atribuído a nenhum espaço de trabalho.\nEntre em contato com seu administrador para solicitar acesso a um espaço de trabalho.",
goToWorkspace: 'Ir para o espaço de trabalho "{{workspace}}"',
},
telegram: {
title: "Bot do Telegram",
description:
"Conecte sua instância do AnythingLLM ao Telegram para que possa conversar com seus espaços de trabalho a partir de qualquer dispositivo.",
setup: {
step1: {
title: "Passo 1: Crie seu bot do Telegram",
description:
"Abra o @BotFather no Telegram, envie /newbot</code> para <code>@BotFather</code>, siga as instruções e copie o token da API.",
"open-botfather": "Iniciar o BotFather",
"instruction-1": "1. Abra o link ou escaneie o código QR.",
"instruction-2":
"2. Envie <code>/newbot</code> para <code>@BotFather</code>",
"instruction-3":
"3. Escolha um nome e um nome de usuário para o seu bot.",
"instruction-4": "4. Copie o token da API que você receber.",
},
step2: {
title: "Passo 2: Conecte seu bot",
description:
"Cole o token da API que recebeu do @BotFather e selecione um espaço de trabalho padrão para que seu bot possa conversar.",
"bot-token": "Token do Bot",
"default-workspace": "Espaço de Trabalho Padrão",
"no-workspace":
"Não há espaços de trabalho disponíveis. Um novo será criado.",
connecting: "Conectando...",
"connect-bot": "Bot de Conexão",
},
security: {
title: "Configurações de segurança recomendadas",
description:
"Para maior segurança, configure estas opções no @BotFather.",
"disable-groups": "— Impedir a adição de bots a grupos",
"disable-inline": "— Impedir que o bot seja usado na pesquisa inline.",
"obscure-username":
"Utilize um nome de usuário de bot menos óbvio para reduzir a sua visibilidade.",
},
"toast-enter-token": "Por favor, insira um token de bot.",
"toast-connect-failed": "Falhou a conexão com o bot.",
},
connected: {
status: "Conectado",
"status-disconnected":
"Desconectado — o token pode ter expirado ou ser inválido",
"placeholder-token": "Cole o novo token do bot...",
reconnect: "Reconectar",
workspace: "Espaço de trabalho",
"bot-link": "Link do bot",
"voice-response": "Resposta por voz",
disconnecting: "Desconectando...",
disconnect: "Desconectar",
"voice-text-only": "Apenas texto",
"voice-mirror":
"Espelho (responder com voz quando o usuário enviar uma mensagem de voz)",
"voice-always":
"Sempre inclua uma gravação de áudio (envie um áudio com cada resposta).",
"toast-disconnect-failed": "Falhou ao desconectar o bot.",
"toast-reconnect-failed": "Falha ao tentar reconectar o bot.",
"toast-voice-failed": "Falhou ao atualizar o modo de voz.",
"toast-approve-failed": "Falhou ao aprovar o usuário.",
"toast-deny-failed": "Não foi possível negar o acesso ao usuário.",
"toast-revoke-failed": "Falhou ao revogar o acesso do usuário.",
},
users: {
"pending-title": "Aguardando Aprovação",
"pending-description":
"Usuários aguardando a verificação. Compare o código de pareamento exibido aqui com o que aparece em seu chat do Telegram.",
"approved-title": "Usuários Aprovados",
"approved-description":
"Usuários que foram aprovados para conversar com o seu bot.",
user: "Usuário",
"pairing-code": "Código de emparelhamento",
"no-pending": "Não há solicitações pendentes.",
"no-approved": "Sem usuários autorizados",
unknown: "Desconhecido",
approve: "Aprovar",
deny: "Negar",
revoke: "Anular",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Numele spațiilor de lucru",
user: "Utilizator",
selection: "Selecția modelului",
saving: "Se salvează...",
save: "Salvează modificările",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Contul dumneavoastră",
"import-item": "Importați articolul",
},
channels: "Canale",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -190,18 +193,12 @@ const TRANSLATIONS = {
title: "Mod chat",
chat: {
title: "Chat",
description:
'va oferi răspunsuri folosind cunoștințele generale ale modelului LLM și contextul documentului respectiv.<br />Va trebui să utilizați comanda "@agent" pentru a utiliza instrumentele.',
},
query: {
title: "Interogare",
description:
'vor oferi răspunsuri **doar** dacă contextul documentului este identificat. Veți avea nevoie să utilizați comanda "@agent" pentru a utiliza instrumentele.',
},
automatic: {
title: "Mașină",
description:
'va utiliza automat instrumentele, dacă modelul și furnizorul suportă apelarea nativă a instrumentelor.<br />Dacă apelarea nativă a instrumentelor nu este suportată, veți avea nevoie să utilizați comanda "@agent" pentru a utiliza instrumentele.',
},
},
history: {
@ -546,7 +543,6 @@ const TRANSLATIONS = {
see_less: "Vezi mai puțin",
see_more: "Vezi mai multe",
tools: "Unelte",
browse: "Navigați",
text_size_label: "Dimensiunea textului",
select_model: "Selectați modelul",
sources: "Surse",
@ -559,8 +555,6 @@ const TRANSLATIONS = {
edit: "Editează",
publish: "Publica",
stop_generating: "Opriți generarea răspunsului",
pause_tts_speech_message:
"Pauză în redarea vocii prin Text-to-Speech (TTS) a mesajului.",
slash_commands: "Comenzi scurte",
agent_skills: "Abilități ale agentului",
manage_agent_skills: "Gestionarea competențelor agenților",
@ -1021,6 +1015,88 @@ const TRANSLATIONS = {
"Momentan nu te-ai atribuit la niciun spațiu de lucru.\nContactează-ți administratorul pentru a solicita acces la un spațiu de lucru.",
goToWorkspace: 'Mai departe la spațiul de lucru "{{workspace}}"',
},
telegram: {
title: "Bot pentru Telegram",
description:
"Conectați instanța dumneavoastră AnythingLLM cu Telegram, astfel încât să puteți interacționa cu spațiile de lucru de pe orice dispozitiv.",
setup: {
step1: {
title: "Pasul 1: Creați botul dumneavoastră Telegram",
description:
"Deschide chatul cu @BotFather pe Telegram, trimite mesajul `/newbot` către <code>@BotFather</code>, urmează instrucțiunile și copiază token-ul API.",
"open-botfather": "Deschide aplicația BotFather",
"instruction-1": "1. Deschideți link-ul sau scanați codul QR",
"instruction-2":
"2. Trimite <code>/newbot</code> către <code>@BotFather</code>",
"instruction-3":
"3. Alege un nume și un nume de utilizator pentru botul tău.",
"instruction-4": "4. Copiați token-ul API pe care îl primiți.",
},
step2: {
title: "Pasul 2: Conectați-vă bot-ul",
description:
"Lipește token-ul API pe care l-ați primit de la @BotFather și selectați un spațiu de lucru implicit pentru ca botul dumneavoastră să poată interacționa.",
"bot-token": "Token Bot",
"default-workspace": "Spațiu de lucru implicit",
"no-workspace":
"Nu există spații de lucru disponibile. Va fi creat unul nou.",
connecting: "Conectare...",
"connect-bot": "Conectare automată",
},
security: {
title: "Recomandări privind setările de securitate",
description:
"Pentru o securitate suplimentară, configurați aceste setări în contul @BotFather.",
"disable-groups": "— Preveniți adăugarea de bot-uri în grupuri",
"disable-inline":
"— Previne utilizarea bot-urilor în căutările directe",
"obscure-username":
"Utilizați un nume de utilizator pentru bot, care nu este evident, pentru a reduce vizibilitatea acestuia.",
},
"toast-enter-token": "Vă rugăm să introduceți un token pentru bot.",
"toast-connect-failed": "Nu a reușit să se conecteze bot-ul.",
},
connected: {
status: "Conectat",
"status-disconnected":
"Deconectat token-ul poate fi expirat sau invalid",
"placeholder-token": "Creați un nou token pentru bot...",
reconnect: "Restabilește conexiunea",
workspace: "Spațiu de lucru",
"bot-link": "Link către bot",
"voice-response": "Răspuns vocal",
disconnecting: "Deconectare...",
disconnect: "Dezactivează",
"voice-text-only": "Doar text",
"voice-mirror":
"Reflectare (răspunde cu voce atunci când utilizatorul trimite înregistrare audio)",
"voice-always":
"Asigurați-vă întotdeauna că includeți un mesaj audio (trimiteți înregistrarea audio împreună cu fiecare răspuns).",
"toast-disconnect-failed": "Nu s-a reușit deconectarea bot-ului.",
"toast-reconnect-failed": "Nu a reușit să se reconecteze.",
"toast-voice-failed": "Nu a reușit să actualizeze modul de voce.",
"toast-approve-failed": "Nu a fost posibilă aprobarea utilizatorului.",
"toast-deny-failed": "Nu a reușit să respingă cererea utilizatorului.",
"toast-revoke-failed":
"Nu a fost posibil să se anuleze contul utilizatorului.",
},
users: {
"pending-title": "Așteptare aprobare",
"pending-description":
"Utilizatorii care așteaptă să fie verificați. Potrivirea codului de asociere afișat aici cu cel afișat în chat-ul lor de pe Telegram.",
"approved-title": "Utilizatori autorizați",
"approved-description":
"Utilizatorii care au fost autorizați să interacționeze cu botul dumneavoastră.",
user: "Utilizator",
"pairing-code": "Cod de asociere",
"no-pending": "Nu există cereri în așteptare.",
"no-approved": "Nu există utilizatori autorizați.",
unknown: "Necunoscut",
approve: "Aprobă",
deny: "Negarea",
revoke: "Anula",
},
},
};
export default TRANSLATIONS;

View File

@ -51,7 +51,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Имя рабочих пространств",
user: "Пользователь",
selection: "Выбор модели",
saving: "Сохранение...",
save: "Сохранить изменения",
@ -105,6 +104,10 @@ const TRANSLATIONS = {
"your-account": "Ваш аккаунт",
"import-item": "Импорт товара",
},
channels: "Каналы",
"available-channels": {
telegram: "Телеграм",
},
},
login: {
"multi-user": {
@ -181,18 +184,12 @@ const TRANSLATIONS = {
title: "Режим чата",
chat: {
title: "Чат",
description:
"предоставит ответы, используя общие знания, содержащиеся в LLM, и контекст документа, который был предоставлен.<br />Для использования инструментов необходимо использовать команду @agent.",
},
query: {
title: "Запрос",
description:
"предоставит ответы <b>только в том случае, если будет найден контекст документа.</b>Для использования инструментов необходимо использовать команду @agent.",
},
automatic: {
title: "Авто",
description:
"автоматически будет использовать инструменты, если модель и поставщик поддерживают вызов инструментов. <br />Если вызов инструментов не поддерживается, вам потребуется использовать команду `@agent` для использования инструментов.",
},
},
history: {
@ -747,7 +744,6 @@ const TRANSLATIONS = {
see_less: "Показать меньше",
see_more: "Узнать больше",
tools: "Инструменты",
browse: "Просматривать",
text_size_label: "Размер текста",
select_model: "Выберите модель",
sources: "Источники",
@ -760,8 +756,6 @@ const TRANSLATIONS = {
edit: "Редактировать",
publish: "Опубликовать",
stop_generating: "Прекратите генерацию ответа",
pause_tts_speech_message:
"Приостановить чтение текста с помощью синтезатора речи.",
slash_commands: "Команды, введенные сокращенной формой",
agent_skills: "Навыки агента",
manage_agent_skills: "Управление навыками агентов",
@ -1027,6 +1021,86 @@ const TRANSLATIONS = {
"Вы не назначены ни к одной рабочей области.\nСвяжитесь с администратором, чтобы запросить доступ к рабочей области.",
goToWorkspace: 'Перейти к рабочей области "{{workspace}}"',
},
telegram: {
title: "Бот для Telegram",
description:
"Подключите свою инстанцию AnythingLLM к Telegram, чтобы вы могли общаться со своими рабочими пространствами с любого устройства.",
setup: {
step1: {
title: "Шаг 1: Создайте своего Telegram-бота.",
description:
"Откройте чат с @BotFather в Telegram, отправьте команду `/newbot` в чат с <code>@BotFather</code>, следуйте инструкциям и скопируйте API-токен.",
"open-botfather": "Запустить BotFather",
"instruction-1": "1. Откройте ссылку или отсканируйте QR-код",
"instruction-2":
"2. Отправьте <code>/newbot</code> на адрес <code>@BotFather</code>",
"instruction-3": "3. Выберите имя и имя пользователя для вашего бота.",
"instruction-4": "4. Скопируйте API-токен, который вы получили.",
},
step2: {
title: "Шаг 2: Подключите своего бота",
description:
"Вставьте API-токен, который вы получили от @BotFather, и выберите основной рабочий стол для вашего бота, чтобы он мог общаться.",
"bot-token": "Токен бота",
"default-workspace": "Основной рабочий стол",
"no-workspace": "Недоступны рабочие места. Будет создано новое.",
connecting: "Устанавливается соединение...",
"connect-bot": "Bot Connect",
},
security: {
title: "Рекомендуемые настройки безопасности",
description:
"Для дополнительной безопасности, настройте эти параметры в @BotFather.",
"disable-groups": "— Предотвратить добавление ботов в группы",
"disable-inline":
"— Предотвратить использование бота в поиске по запросу",
"obscure-username":
"Используйте не очевидное имя пользователя для бота, чтобы уменьшить его видимость.",
},
"toast-enter-token": "Пожалуйста, введите токен для бота.",
"toast-connect-failed": "Не удалось установить соединение с ботом.",
},
connected: {
status: "Соединенный",
"status-disconnected":
"Отключено токен может быть просроченным или недействительным",
"placeholder-token": "Вставьте новый токен для бота...",
reconnect: "Возобновить",
workspace: "Рабочее пространство",
"bot-link": "Ссылка на бота",
"voice-response": "Ответ голосом",
disconnecting: "Отключение...",
disconnect: "Отключить",
"voice-text-only": "Текст только",
"voice-mirror":
"Зеркало (отвечать голосом, когда пользователь отправляет голосовое сообщение)",
"voice-always":
"Всегда добавляйте голосовое сообщение (отправляйте аудио вместе с каждым ответом).",
"toast-disconnect-failed": "Не удалось отключить бота.",
"toast-reconnect-failed": "Не удалось восстановить соединение с ботом.",
"toast-voice-failed": "Не удалось обновить режим голосового управления.",
"toast-approve-failed":
"Не удалось подтвердить учетную запись пользователя.",
"toast-deny-failed": "Не удалось отклонить запрос пользователя.",
"toast-revoke-failed": "Не удалось отменить действия пользователя.",
},
users: {
"pending-title": "Ожидается утверждение",
"pending-description":
"Пользователи, ожидающие подтверждения. Сравните код, указанный здесь, с кодом, отображаемым в их чате в Telegram.",
"approved-title": "Утвержденные пользователи",
"approved-description":
"Пользователи, которым разрешено общаться с вашим ботом.",
user: "Пользователь",
"pairing-code": "Код сопоставления",
"no-pending": "Отсутствуют незавершенные запросы.",
"no-approved": "Нет зарегистрированных пользователей",
unknown: "Неизвестно",
approve: "Одобрить",
deny: "Отрицать",
revoke: "Отменить",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Çalışma Alanları Adı",
user: "Kullanıcı",
selection: "Model Seçimi",
saving: "Kaydediliyor...",
save: "Değişiklikleri Kaydet",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Hesabınız",
"import-item": "İthal Edilen Ürün",
},
channels: "Kanalalar",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -181,18 +184,12 @@ const TRANSLATIONS = {
title: "Sohbet Modu",
chat: {
title: "Sohbet",
description:
"LLM'nin genel bilgisi ve bulunan doküman bağlamıyla cevaplar sunacaktır. Araçları kullanmak için @agent komutunu kullanmanız gerekecektir.",
},
query: {
title: "Sorgu",
description:
"yalnızca doküman bağlamı bulunursa yanıtlar sağlayacaktır.<b>İhtiyaç duyacağınız araçları kullanmak için @agent komutunu kullanmanız gerekecektir.</b>",
},
automatic: {
title: "Oto",
description:
"<br />Varsa, model ve sağlayıcı tarafından desteklenen yerel araçları otomatik olarak kullanacaktır. Yerel araç kullanımı desteklenmiyorsa, araçları kullanmak için @agent komutunu kullanmanız gerekecektir.",
},
},
history: {
@ -742,7 +739,6 @@ const TRANSLATIONS = {
see_less: "Daha az",
see_more: "Daha Fazla",
tools: "Araçlar",
browse: "Gezin",
text_size_label: "Metin Boyutu",
select_model: "Model Seçimi",
sources: "Kaynaklar",
@ -755,7 +751,6 @@ const TRANSLATIONS = {
edit: "Düzenle",
publish: "Yayınla",
stop_generating: "Yanıt üretmeyi durdurun",
pause_tts_speech_message: "Mesajın metin okuma (TTS) özelliğini durdur",
slash_commands: "Komut Satırı Komutları",
agent_skills: "Ajansın Yetenekleri",
manage_agent_skills: "Temsilcinin becerilerini yönetin",
@ -1013,6 +1008,85 @@ const TRANSLATIONS = {
"Şu anda hiçbir çalışma alanına atanmamışsınız.\nBir çalışma alanına erişmek için yöneticinize başvurun.",
goToWorkspace: 'Çalışma alanına git "{{workspace}}"',
},
telegram: {
title: "Telegram Bot'u",
description:
"AnythingLLM örneğinizi Telegram ile bağlantılandırarak, herhangi bir cihazdan çalışma alanlarınızla sohbet edebilmelisiniz.",
setup: {
step1: {
title: "1. Adım: Telegram botunuzu oluşturun",
description:
"Telegram uygulamasında @BotFather'ıın, \"<code>/newbot</code>\" komutunu <code>@BotFather</code>'e gönderin, talimatları izleyin ve API anahtarını kopyalayın.",
"open-botfather": "BotFather'ı aç",
"instruction-1": "1. Bağlantıyıın veya QR kodunu tarayın",
"instruction-2":
"2. <code>/newbot</code> adresine <code>@BotFather</code>'e gönderin.",
"instruction-3": "3. Botunuz için bir isim ve kullanıcı adı seçin",
"instruction-4": "4. Alınan API token'ı kopyalayın",
},
step2: {
title: "Adım 2: Botunuzu bağlayın",
description:
"Aldığınız API token'ı (@BotFather) kopyalayın ve botunuzun iletişim kuracağı varsayılan çalışma alanını seçin.",
"bot-token": "Bot Token",
"default-workspace": "Varsayılan Çalışma Alanı",
"no-workspace":
"Mevcut çalışma alanları bulunmamaktadır. Yeni bir çalışma alanı oluşturulacaktır.",
connecting: "Bağlantı kuruluyor...",
"connect-bot": "Bağlantı Botu",
},
security: {
title: "Önerilen Güvenlik Ayarları",
description:
"Ek güvenlik için, bu ayarları @BotFather üzerinden yapılandırın.",
"disable-groups": "— Gruplara bot eklenmesini engelleme",
"disable-inline":
"— Bot'un, arama çubuklarında kullanılmasını engellemek",
"obscure-username":
"Daha az bilinen bir bot kullanıcı adı kullanarak görünürlüğünü azaltın.",
},
"toast-enter-token": "Lütfen bir bot belirteci girin.",
"toast-connect-failed": "Bot ile bağlantı kurulamadı.",
},
connected: {
status: "Bağlı",
"status-disconnected":
"Bağlantı kesildi — belirteç geçersiz veya süresi dolmuş olabilir",
"placeholder-token": "Yeni bot token'ı yapıştırın...",
reconnect: "Yeniden bağlantı kur",
workspace: "Çalışma alanı",
"bot-link": "Bot bağlantısı",
"voice-response": "Sesle etkileşim",
disconnecting: "Bağlantıyı kesiyorum...",
disconnect: "Bağlantıyı kes",
"voice-text-only": "Sadece metin",
"voice-mirror":
"Sesli yanıt (kullanıcı ses gönderdiğinde, sesli yanıtla cevaplayın)",
"voice-always": "Her yanıtla birlikte sesli (sesli yanıt gönderme)",
"toast-disconnect-failed": "Bot'u ayırmada başarısız.",
"toast-reconnect-failed": "Bot yeniden bağlantı kuramadı.",
"toast-voice-failed": "Ses modunu güncelleme başarısız oldu.",
"toast-approve-failed": "Kullanıcıın onaylanması başarısız oldu.",
"toast-deny-failed": "Kullanıcıyı reddetmeyi başaramadı.",
"toast-revoke-failed": "Kullanıcıyı silme işlemi başarısız oldu.",
},
users: {
"pending-title": "Onay Bekliyor",
"pending-description":
"Doğrulama işlemi bekleyen kullanıcılar. Burada gösterilen eşleştirme kodunu, Telegram sohbetlerinde görüntülenen kodla karşılaştırın.",
"approved-title": "Onaylanmış Kullanıcılar",
"approved-description":
"Botunuzla sohbet etmeye yetkili olan kullanıcılar.",
user: "Kullanıcı",
"pairing-code": "Eşleştirme Kodu",
"no-pending": "Henüz tamamlanmamış herhangi bir istek bulunmamaktadır.",
"no-approved": "Onaylanmış kullanıcı bulunmamaktadır",
unknown: "Bilinmiyor",
approve: "Onayla",
deny: "İnkar",
revoke: "İptal et",
},
},
};
export default TRANSLATIONS;

View File

@ -52,7 +52,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "Tên không gian làm việc",
user: "Người dùng",
selection: "Lựa chọn mô hình",
saving: "Đang lưu...",
save: "Lưu thay đổi",
@ -106,6 +105,10 @@ const TRANSLATIONS = {
"your-account": "Tài khoản của bạn",
"import-item": "Nhập hàng",
},
channels: "Kênh",
"available-channels": {
telegram: "Telegram",
},
},
login: {
"multi-user": {
@ -181,18 +184,12 @@ const TRANSLATIONS = {
title: "Chế độ trò chuyện",
chat: {
title: "Trò chuyện",
description:
"sẽ cung cấp câu trả lời dựa trên kiến thức chung của LLM và ngữ cảnh tài liệu được cung cấp.<br />Bạn cần sử dụng lệnh @agent để sử dụng các công cụ.",
},
query: {
title: "Truy vấn",
description:
"sẽ cung cấp câu trả lời <b>chỉ</b> khi ngữ cảnh của tài liệu được tìm thấy.<br />Bạn cần sử dụng lệnh @agent để sử dụng các công cụ.",
},
automatic: {
title: "Tự động",
description:
"sẽ tự động sử dụng các công cụ nếu mô hình và nhà cung cấp hỗ trợ gọi công cụ gốc.<br />Nếu không hỗ trợ gọi công cụ gốc, bạn sẽ cần sử dụng lệnh `@agent` để sử dụng các công cụ.",
},
},
history: {
@ -739,7 +736,6 @@ const TRANSLATIONS = {
see_less: "Xem ít hơn",
see_more: "Xem thêm",
tools: "Dụng cụ",
browse: "Duyệt",
text_size_label: "Kích thước văn bản",
select_model: "Chọn mẫu",
sources: "Nguồn",
@ -752,7 +748,6 @@ const TRANSLATIONS = {
edit: "Chỉnh sửa",
publish: "Đăng tải",
stop_generating: "Dừng tạo ra phản hồi",
pause_tts_speech_message: "Tạm dừng phát giọng đọc của tin nhắn",
slash_commands: "Lệnh tắt/bật",
agent_skills: "Kỹ năng của đại lý",
manage_agent_skills: "Quản lý kỹ năng của đại lý",
@ -1012,6 +1007,85 @@ const TRANSLATIONS = {
"Bạn hiện không được giao việc nào.\nLiên hệ với quản trị viên của bạn để yêu cầu truy cập vào khu vực làm việc.",
goToWorkspace: 'Chuyển đến khu vực làm việc "{{workspace}}"',
},
telegram: {
title: "Bot Telegram",
description:
"Kết nối phiên bản AnythingLLM của bạn với Telegram để bạn có thể trò chuyện với các không gian làm việc của mình từ bất kỳ thiết bị nào.",
setup: {
step1: {
title: "Bước 1: Tạo bot Telegram của bạn",
description:
"Mở ứng dụng @BotFather trên Telegram, gửi lệnh <code>/newbot</code> đến tài khoản <code>@BotFather</code>, làm theo hướng dẫn và sao chép mã API.",
"open-botfather": "Mở BotFather",
"instruction-1": "1. Mở liên kết hoặc quét mã QR",
"instruction-2":
"2. Gửi <code>/newbot</code> đến <code>@BotFather</code>",
"instruction-3": "3. Chọn tên và tên người dùng cho bot của bạn",
"instruction-4": "4. Sao chép mã API mà bạn nhận được",
},
step2: {
title: "Bước 2: Kết nối bot của bạn",
description:
"Dán mã API mà bạn nhận được từ @BotFather và chọn không gian làm việc mặc định để bot của bạn có thể trò chuyện.",
"bot-token": "Token Bot",
"default-workspace": "Không gian làm việc mặc định",
"no-workspace":
"Không có không gian làm việc nào khả dụng. Một không gian mới sẽ được tạo ra.",
connecting: "Kết nối...",
"connect-bot": "Bot kết nối",
},
security: {
title: "Các cài đặt bảo mật được khuyến nghị",
description:
"Để tăng cường bảo mật, hãy cấu hình các cài đặt này trên tài khoản @BotFather.",
"disable-groups": "— Ngăn chặn việc thêm bot vào các nhóm",
"disable-inline":
"— Ngăn chặn việc sử dụng bot trong tìm kiếm trực tiếp.",
"obscure-username":
"Sử dụng tên người dùng bot không phổ biến để giảm khả năng được tìm thấy.",
},
"toast-enter-token": "Vui lòng nhập mã token cho bot.",
"toast-connect-failed": "Không thể kết nối với trợ lý.",
},
connected: {
status: "Kết nối",
"status-disconnected":
"Không kết nối — mã token có thể đã hết hạn hoặc không hợp lệ",
"placeholder-token": "Dán mã token mới cho bot...",
reconnect: "Khôi phục kết nối",
workspace: "Không gian làm việc",
"bot-link": "Liên kết Bot",
"voice-response": "Phản hồi bằng giọng nói",
disconnecting: "Ngắt kết nối...",
disconnect: "Ngắt kết nối",
"voice-text-only": "Chỉ nội dung",
"voice-mirror": "Trả lời bằng giọng nói (khi người dùng gửi giọng nói)",
"voice-always":
"Luôn luôn có thể gửi phản hồi bằng giọng nói (gửi kèm âm thanh trong mỗi phản hồi).",
"toast-disconnect-failed": "Không thể ngắt kết nối bot.",
"toast-reconnect-failed": "Không thể kết nối lại với trình bot.",
"toast-voice-failed": "Không thể cập nhật chế độ giọng nói.",
"toast-approve-failed": "Không thể xác nhận tài khoản người dùng.",
"toast-deny-failed": "Không thể từ chối yêu cầu của người dùng.",
"toast-revoke-failed": "Không thể thu hồi quyền truy cập cho người dùng.",
},
users: {
"pending-title": "Chờ phê duyệt",
"pending-description":
"Người dùng đang chờ xác nhận. So sánh mã ghép đôi được hiển thị ở đây với mã hiển thị trong cuộc trò chuyện Telegram của họ.",
"approved-title": "Người dùng đã được phê duyệt",
"approved-description":
"Người dùng đã được chấp thuận để trò chuyện với bot của bạn.",
user: "Người dùng",
"pairing-code": "Mã ghép",
"no-pending": "Không có yêu cầu nào đang chờ xử lý.",
"no-approved": "Không có người dùng được xác nhận",
unknown: "Không xác định",
approve: "Chấp thuận",
deny: "Từ chối",
revoke: "Thu hồi",
},
},
};
export default TRANSLATIONS;

View File

@ -48,7 +48,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "工作区名称",
user: "用户",
selection: "模型选择",
save: "保存更改",
saving: "保存中...",
@ -102,6 +101,10 @@ const TRANSLATIONS = {
"your-account": "您的账户",
"import-item": "进口商品",
},
channels: "频道",
"available-channels": {
telegram: "电报",
},
},
login: {
"multi-user": {
@ -182,18 +185,12 @@ const TRANSLATIONS = {
title: "聊天模式",
chat: {
title: "聊天",
description:
"将提供答案利用LLM的通用知识和相关文档的上下文信息。您需要使用 `@agent` 命令来使用工具。",
},
query: {
title: "查询",
description:
"将在找到文档上下文时提供答案 <b>仅限</b>。您需要使用 @agent 命令来使用工具。",
},
automatic: {
title: "自动",
description:
"如果模型和提供商支持原生工具调用,则会自动使用这些工具。<br />如果不支持原生工具调用,则需要使用 `@agent` 命令来使用工具。",
},
},
history: {
@ -782,7 +779,6 @@ const TRANSLATIONS = {
see_less: "查看更多",
see_more: "查看更多",
tools: "工具",
browse: "浏览",
text_size_label: "字体大小",
select_model: "选择型号",
sources: "来源",
@ -795,7 +791,6 @@ const TRANSLATIONS = {
edit: "编辑",
publish: "出版",
stop_generating: "停止生成回复",
pause_tts_speech_message: "暂停消息的语音合成TTS功能",
slash_commands: "快捷命令",
agent_skills: "代理人技能",
manage_agent_skills: "管理代理人技能",
@ -951,6 +946,79 @@ const TRANSLATIONS = {
"你目前还没有分配到任何工作区。\n请联系你的管理员请求访问一个工作区。",
goToWorkspace: '前往 "{{workspace}}"',
},
telegram: {
title: "Telegram 机器人",
description:
"将您的 AnythingLLM 实例与 Telegram 连接起来,这样您就可以从任何设备与您的工作空间进行聊天。",
setup: {
step1: {
title: "第一步:创建您的 Telegram 机器人",
description:
"打开 Telegram 上的 @BotFather发送 `/newbot` 到 <code>@BotFather</code>,按照提示操作,并复制 API 令牌。",
"open-botfather": "启动 BotFather",
"instruction-1": "1. 打开链接或扫描二维码",
"instruction-2":
"2. 将 <code>/newbot</code> 发送给 <code>@BotFather</code>",
"instruction-3": "3. 为您的机器人选择一个名称和用户名",
"instruction-4": "4. 复制您收到的 API 令牌",
},
step2: {
title: "步骤 2连接您的机器人",
description:
"将您从 @BotFather 获得的 API 令牌粘贴到指定位置,并选择一个默认的工作区,以便您的机器人可以进行对话。",
"bot-token": "机器人代币",
"default-workspace": "默认工作区",
"no-workspace": "目前没有可用的工作空间。将会创建一个新的工作空间。",
connecting: "正在连接...",
"connect-bot": "连接机器人",
},
security: {
title: "推荐的安全设置",
description: "为了进一步增强安全性,请在 @BotFather 中配置这些设置。",
"disable-groups": "— 阻止机器人加入群组",
"disable-inline": "— 阻止机器人被用于内联搜索",
"obscure-username":
"使用一个不显眼的机器人用户名,以降低其被发现的可能性。",
},
"toast-enter-token": "请您输入一个机器人令牌。",
"toast-connect-failed": "未能连接机器人。",
},
connected: {
status: "连接",
"status-disconnected": "未连接—— 令牌可能已过期或无效",
"placeholder-token": "粘贴新的机器人令牌...",
reconnect: "重新连接",
workspace: "工作空间",
"bot-link": "机器人链接",
"voice-response": "语音响应",
disconnecting: "断开连接...",
disconnect: "断开",
"voice-text-only": "仅提供文字",
"voice-mirror": "回声(当用户发送语音时,会以语音形式回复)",
"voice-always": "请务必在回复中添加语音(发送音频)。",
"toast-disconnect-failed": "未能成功断开机器人。",
"toast-reconnect-failed": "机器人连接失败。",
"toast-voice-failed": "无法更新语音模式。",
"toast-approve-failed": "未能批准用户。",
"toast-deny-failed": "未能拒绝用户请求。",
"toast-revoke-failed": "未能撤销用户权限。",
},
users: {
"pending-title": "待审批",
"pending-description":
"等待验证的用户。请将此处显示的配对代码与他们在 Telegram 聊天中显示的配对代码进行匹配。",
"approved-title": "已批准的用户",
"approved-description": "已获得批准,可以与您的机器人进行对话的用户。",
user: "用户",
"pairing-code": "配对代码",
"no-pending": "目前没有待处理的请求",
"no-approved": "未批准的用户",
unknown: "未知",
approve: "批准",
deny: "否认",
revoke: "撤销",
},
},
};
export default TRANSLATIONS;

View File

@ -48,7 +48,6 @@ const TRANSLATIONS = {
},
common: {
"workspaces-name": "工作區名稱",
user: "使用者",
selection: "模型選擇",
saving: "儲存中...",
save: "儲存變更",
@ -102,6 +101,10 @@ const TRANSLATIONS = {
"your-account": "您的帳戶",
"import-item": "匯入項目",
},
channels: "頻道",
"available-channels": {
telegram: "電訊",
},
},
login: {
"multi-user": {
@ -174,18 +177,12 @@ const TRANSLATIONS = {
title: "對話模式",
chat: {
title: "對話",
description:
"將提供答案,利用 LLM 的一般知識和相關文件內容。您需要使用 `@agent` 命令來使用工具。",
},
query: {
title: "查詢",
description:
"將提供答案,僅在找到文件上下文時 <b>。您需要使用 @agent 指令來使用工具。",
},
automatic: {
title: "自動",
description:
"如果模型和供應商支援原生工具調用,則系統會自動使用這些工具。<br />如果原生工具調用不受支援,您需要使用 `@agent` 命令來使用工具。",
},
},
history: {
@ -692,7 +689,6 @@ const TRANSLATIONS = {
see_less: "顯示較少",
see_more: "查看更多",
tools: "工具",
browse: "瀏覽",
text_size_label: "文字大小",
select_model: "選擇模型",
sources: "來源",
@ -705,7 +701,6 @@ const TRANSLATIONS = {
edit: "編輯",
publish: "發佈",
stop_generating: "停止產生回應",
pause_tts_speech_message: "暫停語音合成的訊息",
slash_commands: "斜線指令",
agent_skills: "智慧代理人技能",
manage_agent_skills: "管理智慧代理人技能",
@ -944,6 +939,79 @@ const TRANSLATIONS = {
"您目前尚未被分配到任何工作區。\n請聯絡您的管理員以申請工作區的存取權限。",
goToWorkspace: '前往 "{{workspace}}"',
},
telegram: {
title: "Telegram 機器人",
description:
"將您的 AnythingLLM 實例連接到 Telegram以便您可以在任何裝置上與您的工作空間進行對話。",
setup: {
step1: {
title: "第一步:建立您的 Telegram 機器人",
description:
'在 Telegram 中開啟 @BotFather將 "<code>/newbot" 訊息發送至 <code>@BotFather</code>,按照指示操作,並複製 API 令牌。',
"open-botfather": "開啟 BotFather",
"instruction-1": "1. 點擊連結或掃描 QR 碼",
"instruction-2":
"2. 將 <code>/newbot</code> 傳送至 <code>@BotFather</code>",
"instruction-3": "3. 為您的機器人選擇一個名稱和使用者名稱。",
"instruction-4": "4. 複製您收到的 API 令牌",
},
step2: {
title: "步驟 2連接您的機器人",
description:
"請將您從 @BotFather 處獲得的 API 令牌複製並貼上,然後選擇一個預設的工作空間,讓您的機器人與其對話。",
"bot-token": "機器人代幣",
"default-workspace": "預設工作空間",
"no-workspace": "目前沒有可用的工作空間。將會創建一個新的工作空間。",
connecting: "正在連接...",
"connect-bot": "連線機器人",
},
security: {
title: "建議的安全設定",
description: "為了額外保障,請在 @BotFather 中設定這些選項。",
"disable-groups": "— 阻止自動程式加入群組",
"disable-inline": "— 阻止機器人被用於內嵌式搜尋",
"obscure-username":
"使用一個不顯眼的機器人帳號名稱,以降低被發現的機會。",
},
"toast-enter-token": "請輸入機器人憑證。",
"toast-connect-failed": "無法連接機器人。",
},
connected: {
status: "連接",
"status-disconnected": "無法連接 — 可能是 token 已經過期或無效",
"placeholder-token": "黏貼新的機器人代碼...",
reconnect: "重新建立聯繫",
workspace: "工作空間",
"bot-link": "機器人連結",
"voice-response": "語音回應",
disconnecting: "斷線...",
disconnect: "斷開連接",
"voice-text-only": "僅提供文字",
"voice-mirror": "語音回覆 (使用者發送語音時,系統會回覆語音)",
"voice-always": "請務必在回覆中加入語音 (發送音訊)。",
"toast-disconnect-failed": "未能成功斷開機器人。",
"toast-reconnect-failed": "無法重新連線機器人。",
"toast-voice-failed": "無法更新語音模式。",
"toast-approve-failed": "無法驗證使用者。",
"toast-deny-failed": "未能阻止使用者。",
"toast-revoke-failed": "未能取消使用者權限。",
},
users: {
"pending-title": "待審核",
"pending-description":
"等待驗證的使用者。請將這裡顯示的配對碼與他們在 Telegram 聊天中顯示的配對碼對齊。",
"approved-title": "已授權的使用者",
"approved-description": "已獲得批准,可以與您的機器人進行對話的使用者。",
user: "使用者",
"pairing-code": "編碼組合",
"no-pending": "目前沒有待處理的請求",
"no-approved": "目前沒有已授權的使用者",
unknown: "未知的",
approve: "批准",
deny: "拒絕",
revoke: "撤銷",
},
},
};
export default TRANSLATIONS;

View File

@ -372,6 +372,15 @@ const router = createBrowserRouter([
return { element: <ManagerRoute Component={MobileConnections} /> };
},
},
{
path: "/settings/external-connections/telegram",
lazy: async () => {
const { default: TelegramBotSettings } = await import(
"@/pages/GeneralSettings/Connections/TelegramBot"
);
return { element: <AdminRoute Component={TelegramBotSettings} /> };
},
},
// Catch-all route for 404s
{
path: "*",

View File

@ -0,0 +1,176 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const Telegram = {
/**
* Get the current Telegram bot configuration.
* @returns {Promise<{config: object|null, error: string|null}>}
*/
getConfig: async function () {
return await fetch(`${API_BASE}/telegram/config`, {
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { config: null, error: e.message };
});
},
/**
* Connect and start the Telegram bot with given token and workspace.
* @param {string} botToken - The bot API token from BotFather.
* @param {string} workspaceSlug - The default workspace slug.
* @returns {Promise<{success: boolean, bot_username: string|null, error: string|null}>}
*/
connect: async function (botToken, workspaceSlug) {
return await fetch(`${API_BASE}/telegram/connect`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({
bot_token: botToken,
default_workspace: workspaceSlug,
}),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
/**
* Disconnect and stop the Telegram bot.
* @returns {Promise<{success: boolean, error: string|null}>}
*/
disconnect: async function () {
return await fetch(`${API_BASE}/telegram/disconnect`, {
method: "POST",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
/**
* Get the current bot connection status.
* @returns {Promise<{active: boolean, bot_username: string|null}>}
*/
status: async function () {
return await fetch(`${API_BASE}/telegram/status`, {
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { active: false, bot_username: null };
});
},
/**
* Get pending pairing requests.
* @returns {Promise<{users: Array}>}
*/
getPendingUsers: async function () {
return await fetch(`${API_BASE}/telegram/pending-users`, {
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { users: [] };
});
},
/**
* Get approved users list.
* @returns {Promise<{users: Array}>}
*/
getApprovedUsers: async function () {
return await fetch(`${API_BASE}/telegram/approved-users`, {
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { users: [] };
});
},
/**
* Approve a pending user.
* @param {string} chatId
* @returns {Promise<{success: boolean, error: string|null}>}
*/
approveUser: async function (chatId) {
return await fetch(`${API_BASE}/telegram/approve-user`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ chatId }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
/**
* Deny a pending user.
* @param {string} chatId
* @returns {Promise<{success: boolean, error: string|null}>}
*/
denyUser: async function (chatId) {
return await fetch(`${API_BASE}/telegram/deny-user`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ chatId }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
/**
* Update the Telegram bot configuration.
* @param {object} updates - Config fields to update (e.g. voice_response_mode).
* @returns {Promise<{success: boolean, error: string|null}>}
*/
updateConfig: async function (updates) {
return await fetch(`${API_BASE}/telegram/update-config`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify(updates),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
/**
* Revoke an approved user.
* @param {string} chatId
* @returns {Promise<{success: boolean, error: string|null}>}
*/
revokeUser: async function (chatId) {
return await fetch(`${API_BASE}/telegram/revoke-user`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ chatId }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return { success: false, error: e.message };
});
},
};
export default Telegram;

View File

@ -0,0 +1,153 @@
import { useTranslation } from "react-i18next";
import Telegram from "@/models/telegram";
import showToast from "@/utils/toast";
export default function UsersTable({
title,
description,
users = [],
isPending = false,
fetchUsers = () => {},
}) {
const { t } = useTranslation();
if (users.length === 0) return null;
const colCount = isPending ? 4 : 3;
async function handleApprove(chatId) {
const res = await Telegram.approveUser(chatId);
if (!res.success) {
showToast(
res.error || t("telegram.connected.toast-approve-failed"),
"error"
);
return;
}
fetchUsers();
}
async function handleDeny(chatId) {
const res = await Telegram.denyUser(chatId);
if (!res.success) {
showToast(
res.error || t("telegram.connected.toast-deny-failed"),
"error"
);
return;
}
fetchUsers();
}
async function handleRevoke(chatId) {
const res = await Telegram.revokeUser(chatId);
if (!res.success) {
showToast(
res.error || t("telegram.connected.toast-revoke-failed"),
"error"
);
return;
}
fetchUsers();
}
return (
<div className="flex flex-col gap-y-2">
<p className="text-sm font-semibold text-theme-text-primary">{title}</p>
<p className="text-xs text-theme-text-secondary">{description}</p>
<div className="overflow-x-auto mt-2">
<table className="w-1/2 text-xs text-left rounded-lg min-w-[480px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3">
{t("telegram.users.user")}
</th>
{isPending && (
<th scope="col" className="px-6 py-3">
{t("telegram.users.pairing-code")}
</th>
)}
<th scope="col" className="px-6 py-3">
{" "}
</th>
</tr>
</thead>
<tbody>
{users.length === 0 ? (
<tr className="bg-transparent text-theme-text-secondary text-sm font-medium">
<td colSpan={colCount} className="px-6 py-4 text-center">
{isPending
? t("telegram.users.no-pending")
: t("telegram.users.no-approved")}
</td>
</tr>
) : (
users.map((user) => {
const chatId = typeof user === "string" ? user : user.chatId;
const username = user.telegramUsername || user.username || null;
const firstName = user.firstName || null;
const displayName = username
? `@${username}`
: firstName || t("telegram.users.unknown");
const code = user.code;
return (
<tr
key={chatId}
className="bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10"
>
<td className="px-6 whitespace-nowrap">
<span className="text-sm text-theme-text-primary">
{displayName}
</span>
</td>
{isPending && (
<td className="px-6 whitespace-nowrap">
<code className="bg-theme-bg-primary px-2 py-1 rounded text-theme-text-primary font-mono text-sm">
{code}
</code>
</td>
)}
<td className="px-6 flex items-center gap-x-6 h-full mt-1">
{isPending ? (
<>
<ActionButton
onClick={() => handleApprove(chatId)}
className="hover:light:bg-green-50 hover:light:text-green-500 hover:text-green-300"
>
{t("telegram.users.approve")}
</ActionButton>
<ActionButton
onClick={() => handleDeny(chatId)}
className="hover:light:bg-red-50 hover:light:text-red-500 hover:text-red-300"
>
{t("telegram.users.deny")}
</ActionButton>
</>
) : (
<ActionButton
onClick={() => handleRevoke(chatId)}
className="hover:light:bg-red-50"
>
{t("telegram.users.revoke")}
</ActionButton>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
);
}
function ActionButton({ onClick, className = "", children }) {
return (
<button
onClick={onClick}
className={`border-none flex items-center justify-center text-xs font-medium text-white/80 light:text-black/80 rounded-lg p-1 hover:bg-white hover:bg-opacity-10 ${className}`}
>
{children}
</button>
);
}

View File

@ -0,0 +1,313 @@
import { useEffect, useState, useCallback } from "react";
import {
ArrowSquareOut,
CircleNotch,
Eye,
EyeSlash,
TelegramLogo,
} from "@phosphor-icons/react";
import Telegram from "@/models/telegram";
import showToast from "@/utils/toast";
import UsersTable from "./UsersTable";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export default function ConnectedView({
config,
workspaces,
onDisconnected,
onReconnected,
}) {
const { t } = useTranslation();
const connected = config.connected;
const [newToken, setNewToken] = useState("");
const [pendingUsers, setPendingUsers] = useState([]);
const [approvedUsers, setApprovedUsers] = useState([]);
const workspaceName =
workspaces.find((ws) => ws.slug === config.default_workspace)?.name ||
config.default_workspace;
const fetchUsers = useCallback(async () => {
const [pending, approved] = await Promise.all([
Telegram.getPendingUsers(),
Telegram.getApprovedUsers(),
]);
setPendingUsers(pending?.users || []);
setApprovedUsers(approved?.users || []);
}, []);
useEffect(() => {
fetchUsers();
const interval = setInterval(fetchUsers, 5_000);
return () => clearInterval(interval);
}, [fetchUsers]);
if (!connected) {
return (
<DisconnectedView
config={config}
onReconnected={onReconnected}
newToken={newToken}
setNewToken={setNewToken}
/>
);
}
return (
<div className="mt-6 flex flex-col gap-y-6">
<div className="flex flex-col gap-y-4 max-w-[480px]">
<div className="flex items-center gap-x-3 rounded-lg border border-green-500/20 bg-green-500/5 light:border-green-700/20 light:bg-green-500/10 p-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-500/10">
<TelegramLogo
className="h-5 w-5 text-green-400 light:text-green-700"
weight="fill"
/>
</div>
<div className="flex flex-col">
<p className="text-sm font-semibold text-theme-text-primary">
@{config.bot_username}
</p>
<p className="text-xs text-green-400 light:text-green-700">
{t("telegram.connected.status")}
</p>
</div>
</div>
<div className="flex flex-col gap-y-2 rounded-lg bg-black light:bg-black/5 light:border light:border-black/10 p-4">
<WorkspaceName name={workspaceName} />
<BotLink username={config.bot_username} />
{/*
Disabled for now - works fine, but I am not sure we want to enabled this feature.
How many people really need a REPLY with voice mode? Even then, we should support on device TTS
and more it out the frontend so people can do voice gen without having to pay for it.
*/}
{/* <VoiceModeSelector config={config} /> */}
<DisconnectButton onDisconnected={onDisconnected} />
</div>
</div>
<UsersTable
title={t("telegram.users.pending-title")}
description={t("telegram.users.pending-description")}
users={pendingUsers}
isPending
fetchUsers={fetchUsers}
/>
<UsersTable
title={t("telegram.users.approved-title")}
description={t("telegram.users.approved-description")}
users={approvedUsers}
fetchUsers={fetchUsers}
/>
</div>
);
}
function BotLink({ username }) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-between">
<span className="text-xs text-white">
{t("telegram.connected.bot-link")}
</span>
<Link
to={`https://t.me/${username}`}
target="_blank"
rel="noreferrer"
className="text-xs text-sky-500 light:text-sky-600 hover:underline flex items-center gap-x-1"
>
t.me/{username}
<ArrowSquareOut className="h-3 w-3" />
</Link>
</div>
);
}
function DisconnectButton({ onDisconnected }) {
const { t } = useTranslation();
const [disconnecting, setDisconnecting] = useState(false);
async function handleDisconnect() {
setDisconnecting(true);
const res = await Telegram.disconnect();
setDisconnecting(false);
if (!res.success) {
showToast(
res.error || t("telegram.connected.toast-disconnect-failed"),
"error"
);
return;
}
onDisconnected();
}
return (
<button
onClick={handleDisconnect}
disabled={disconnecting}
className="flex items-center justify-center gap-x-2 text-sm font-medium text-white bg-red-800 hover:bg-red-700 light:bg-red-600 light:hover:bg-red-500 light:text-white rounded-lg px-4 py-0.5 w-fit transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{disconnecting ? (
<>
<CircleNotch className="h-4 w-4 animate-spin" />
{t("telegram.connected.disconnecting")}
</>
) : (
t("telegram.connected.disconnect")
)}
</button>
);
}
function WorkspaceName({ name }) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-between">
<span className="text-xs text-white">
{t("telegram.connected.workspace")}
</span>
<span className="text-xs text-white font-medium">{name}</span>
</div>
);
}
function DisconnectedView({ config, onReconnected, newToken, setNewToken }) {
const { t } = useTranslation();
const [reconnecting, setReconnecting] = useState(false);
const [showToken, setShowToken] = useState(false);
const Icon = showToken ? Eye : EyeSlash;
async function handleReconnect(e) {
e.preventDefault();
if (!newToken.trim()) return;
setReconnecting(true);
const res = await Telegram.connect(
newToken.trim(),
config.default_workspace
);
setReconnecting(false);
if (!res.success)
return showToast(
res.error || t("telegram.connected.toast-reconnect-failed"),
"error"
);
setNewToken("");
onReconnected({
active: true,
connected: true,
bot_username: res.bot_username,
default_workspace: config.default_workspace,
});
}
return (
<div className="mt-6 flex flex-col gap-y-6">
<div className="flex flex-col gap-y-4 max-w-[480px]">
<div className="flex flex-col gap-y-3">
<div className="flex items-center gap-x-3 rounded-lg border border-red-500/20 bg-red-500/5 p-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-500/10">
<TelegramLogo className="h-5 w-5 text-red-400" weight="fill" />
</div>
<div className="flex flex-col">
<p className="text-sm font-semibold text-white">
@{config.bot_username}
</p>
<p className="text-xs text-red-400">
{t("telegram.connected.status-disconnected")}
</p>
</div>
</div>
<form onSubmit={handleReconnect} className="flex items-end gap-x-2">
<div className="relative w-full">
<input
type={showToken ? "text" : "password"}
value={newToken}
onChange={(e) => setNewToken(e.target.value)}
placeholder={t("telegram.connected.placeholder-token")}
className="w-[99%] bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 pr-10"
autoComplete="off"
/>
{newToken.length > 0 && (
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
>
<Icon className="h-5 w-5" />
</button>
)}
</div>
<button
type="submit"
disabled={reconnecting}
className="flex items-center gap-x-2 text-sm font-medium !text-white bg-sky-500 hover:bg-sky-600 rounded-lg px-4 py-2.5 whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{reconnecting ? (
<CircleNotch className="h-4 w-4 animate-spin" />
) : (
t("telegram.connected.reconnect")
)}
</button>
</form>
</div>
</div>
</div>
);
}
/**
This code is disabled for now - works fine, but I am not sure we want to enabled this feature.
How many people really need a REPLY with voice mode? Even then, we should support on device TTS
and more it out the frontend so people can do voice gen without having to pay for it.
When we do enabled this, we should uncomment this code and remove the disabled comment.
const getVoiceModeOptions = (t) => {
return [
{ value: "text_only", label: t("telegram.connected.voice-text-only") },
{ value: "mirror", label: t("telegram.connected.voice-mirror") },
{ value: "always_voice", label: t("telegram.connected.voice-always") },
];
};
function VoiceModeSelector({ config }) {
const { t } = useTranslation();
const [voiceMode, setVoiceMode] = useState(
config.voice_response_mode || "text_only"
);
async function handleVoiceModeChange(e) {
const mode = e.target.value;
setVoiceMode(mode);
const res = await Telegram.updateConfig({ voice_response_mode: mode });
if (!res.success) {
showToast(
res.error || t("telegram.connected.toast-voice-failed"),
"error"
);
setVoiceMode(config.voice_response_mode || "text_only");
}
}
return (
<div className="flex items-center justify-between">
<span className="text-xs text-white">
{t("telegram.connected.voice-response")}
</span>
<select
value={voiceMode}
onChange={handleVoiceModeChange}
className="text-xs text-right bg-transparent text-white rounded-md px-2 py-1 outline-none max-w-[260px]"
>
{getVoiceModeOptions(t).map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
);
}
*/

View File

@ -0,0 +1,118 @@
import { QRCodeSVG } from "qrcode.react";
import { ShieldCheck, TelegramLogo } from "@phosphor-icons/react";
import Logo from "@/media/logo/anything-llm-infinity.png";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
const BOTFATHER_URL = "https://t.me/BotFather";
export default function CreateBotSection() {
const { t } = useTranslation();
const qrSize = 180;
const logoSize = { width: 35 * 1.2, height: 22 * 1.2 };
return (
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1">
<p className="text-sm font-semibold text-theme-text-primary">
{t("telegram.setup.step1.title")}
</p>
<p className="text-xs text-theme-text-secondary mb-3">
<Trans
i18nKey="telegram.setup.step1.description"
components={{
code: (
<code className="bg-theme-bg-primary px-1 py-0.5 rounded text-theme-text-primary" />
),
}}
/>
</p>
<div className="flex items-start gap-x-6 flex-wrap gap-y-4">
<div className="flex flex-col items-center gap-y-2">
<div className="bg-white/10 light:bg-black/5 rounded-lg p-4 flex items-center justify-center">
<QRCodeSVG
value={BOTFATHER_URL}
size={qrSize}
bgColor="transparent"
fgColor="currentColor"
className="text-white light:text-black light:[&_image]:invert"
level="L"
imageSettings={{
src: Logo,
x: qrSize / 2 - logoSize.width / 2,
y: qrSize / 2 - logoSize.height / 2,
height: logoSize.height,
width: logoSize.width,
excavate: true,
}}
/>
</div>
</div>
<div className="flex flex-col gap-y-3 pt-2">
<Link
to={BOTFATHER_URL}
target="_blank"
rel="noreferrer"
className="flex items-center gap-x-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-lg px-4 py-2.5 w-fit transition-colors duration-200"
>
<TelegramLogo className="h-4 w-4" weight="fill" />
{t("telegram.setup.step1.open-botfather")}
</Link>
<div className="flex flex-col gap-y-1.5 text-xs text-theme-text-secondary">
<p>{t("telegram.setup.step1.instruction-1")}</p>
<p>
<Trans
i18nKey="telegram.setup.step1.instruction-2"
components={{
code: (
<code className="bg-theme-bg-primary px-1 py-0.5 rounded text-theme-text-primary" />
),
}}
/>
</p>
<p>{t("telegram.setup.step1.instruction-3")}</p>
<p>{t("telegram.setup.step1.instruction-4")}</p>
</div>
</div>
</div>
</div>
<SecurityTips />
</div>
);
}
function SecurityTips() {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-y-2 p-3 rounded-lg bg-theme-bg-primary border border-theme-sidebar-border">
<div className="flex items-center gap-x-2">
<ShieldCheck
className="h-4 w-4 text-theme-text-secondary"
weight="bold"
/>
<p className="text-xs font-semibold text-theme-text-primary">
{t("telegram.setup.security.title")}
</p>
</div>
<p className="text-xs text-theme-text-secondary">
{t("telegram.setup.security.description")}
</p>
<ul className="text-xs text-theme-text-secondary list-disc list-inside space-y-1 ml-1">
<li>
<code className="bg-theme-bg-secondary px-1 py-0.5 rounded text-theme-text-primary">
Disable Groups
</code>{" "}
{t("telegram.setup.security.disable-groups")}
</li>
<li>
<code className="bg-theme-bg-secondary px-1 py-0.5 rounded text-theme-text-primary">
Disable Inline
</code>{" "}
{t("telegram.setup.security.disable-inline")}
</li>
<li>{t("telegram.setup.security.obscure-username")}</li>
</ul>
</div>
);
}

View File

@ -0,0 +1,159 @@
import { useState } from "react";
import {
CircleNotch,
Eye,
EyeSlash,
TelegramLogo,
} from "@phosphor-icons/react";
import Telegram from "@/models/telegram";
import showToast from "@/utils/toast";
import CreateBotSection from "./CreateBotSection";
import { useTranslation } from "react-i18next";
export default function SetupView({ workspaces, onConnected }) {
const { t } = useTranslation();
const [botToken, setBotToken] = useState("");
const [selectedWorkspace, setSelectedWorkspace] = useState(
workspaces[0]?.slug || ""
);
const [connecting, setConnecting] = useState(false);
async function handleConnect(e) {
e.preventDefault();
if (!botToken.trim())
return showToast(t("telegram.setup.toast-enter-token"), "error");
setConnecting(true);
const res = await Telegram.connect(botToken.trim(), selectedWorkspace);
setConnecting(false);
if (!res.success) {
showToast(res.error || t("telegram.setup.toast-connect-failed"), "error");
return;
}
onConnected({
active: true,
connected: true,
bot_username: res.bot_username,
default_workspace: selectedWorkspace || null,
});
}
return (
<div className="flex flex-col gap-y-6 mt-6">
<CreateBotSection />
<form onSubmit={handleConnect} className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1">
<p className="text-sm font-semibold text-theme-text-primary">
{t("telegram.setup.step2.title")}
</p>
<p className="text-xs text-theme-text-secondary">
{t("telegram.setup.step2.description")}
</p>
</div>
<div className="flex flex-col gap-y-4 max-w-[480px]">
<BotTokenInput botToken={botToken} setBotToken={setBotToken} />
<WorkspaceSelect
workspaces={workspaces}
selectedWorkspace={selectedWorkspace}
setSelectedWorkspace={setSelectedWorkspace}
/>
<button
type="submit"
disabled={connecting}
className="flex items-center justify-center gap-x-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-lg px-4 py-2.5 w-fit transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{connecting ? (
<>
<CircleNotch className="h-4 w-4 animate-spin" />
{t("telegram.setup.step2.connecting")}
</>
) : (
<>
<TelegramLogo className="h-4 w-4" weight="bold" />
{t("telegram.setup.step2.connect-bot")}
</>
)}
</button>
</div>
</form>
</div>
);
}
function BotTokenInput({ botToken, setBotToken }) {
const { t } = useTranslation();
const [showToken, setShowToken] = useState(false);
const Icon = showToken ? Eye : EyeSlash;
return (
<div className="flex flex-col gap-y-1">
<label className="text-xs font-medium text-theme-text-secondary">
{t("telegram.setup.step2.bot-token")}
</label>
<div className="relative">
<input
type={showToken ? "text" : "password"}
value={botToken}
onChange={(e) => setBotToken(e.target.value)}
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v..."
className="bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 pr-10"
autoComplete="off"
/>
{botToken.length > 0 && (
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-theme-text-secondary hover:text-theme-text-primary transition-colors"
>
<Icon className="h-5 w-5" />
</button>
)}
</div>
</div>
);
}
function WorkspaceSelect({
workspaces,
selectedWorkspace,
setSelectedWorkspace,
}) {
const { t } = useTranslation();
if (!workspaces.length) {
return (
<div className="flex flex-col gap-y-1">
<label className="text-xs font-medium text-theme-text-secondary">
{t("telegram.setup.step2.default-workspace")}{" "}
<span className="italic font-normal">({t("common.optional")})</span>
</label>
<input
disabled
placeholder={t("telegram.setup.step2.no-workspace")}
className="bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
/>
</div>
);
}
return (
<div className="flex flex-col gap-y-1">
<label className="text-xs font-medium text-theme-text-secondary">
{t("telegram.setup.step2.default-workspace")}{" "}
<span className="italic font-normal">({t("common.optional")})</span>
</label>
<select
value={selectedWorkspace}
onChange={(e) => setSelectedWorkspace(e.target.value)}
className="bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
>
{workspaces.map((ws) => (
<option key={ws.slug} value={ws.slug}>
{ws.name}
</option>
))}
</select>
</div>
);
}

View File

@ -0,0 +1,97 @@
import { useEffect, useState } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import { CircleNotch } from "@phosphor-icons/react";
import Workspace from "@/models/workspace";
import Telegram from "@/models/telegram";
import ConnectedView from "./ConnectedView";
import SetupView from "./SetupView";
import { useTranslation } from "react-i18next";
import System from "@/models/system";
import paths from "@/utils/paths";
export default function TelegramBotSettings() {
const [loading, setLoading] = useState(true);
const [config, setConfig] = useState(null);
const [workspaces, setWorkspaces] = useState([]);
useEffect(() => {
async function fetchData() {
const [isMultiUserMode, configRes, allWorkspaces] = await Promise.all([
System.isMultiUserMode(),
Telegram.getConfig(),
Workspace.all(),
]);
if (isMultiUserMode) window.location = paths.home();
setConfig(configRes?.config || null);
setWorkspaces(allWorkspaces || []);
setLoading(false);
}
fetchData();
}, []);
const handleConnected = (newConfig) => setConfig(newConfig);
const handleDisconnected = () => setConfig(null);
if (loading) {
return (
<ConnectionsLayout>
<div className="flex items-center justify-center h-full">
<CircleNotch className="h-8 w-8 text-theme-text-secondary animate-spin" />
</div>
</ConnectionsLayout>
);
}
const hasConfig = config?.active && config?.bot_username;
if (!hasConfig) {
return (
<ConnectionsLayout fullPage={true}>
<SetupView workspaces={workspaces} onConnected={handleConnected} />
</ConnectionsLayout>
);
}
return (
<ConnectionsLayout fullPage={true}>
<ConnectedView
config={config}
workspaces={workspaces}
onDisconnected={handleDisconnected}
onReconnected={handleConnected}
/>
</ConnectionsLayout>
);
}
function ConnectionsLayout({ children, fullPage = false }) {
const { t } = useTranslation();
return (
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
>
{fullPage ? (
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-4 border-white/10 border-b-2">
<div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-theme-text-primary">
{t("telegram.title")}
</p>
</div>
<p className="text-xs leading-[18px] font-base text-theme-text-secondary mt-2">
{t("telegram.description")}
</p>
</div>
{children}
</div>
) : (
children
)}
</div>
</div>
);
}

View File

@ -170,6 +170,9 @@ export default {
mobileConnections: () => {
return `/settings/mobile-connections`;
},
telegram: () => {
return `/settings/external-connections/telegram`;
},
},
agents: {
builder: () => {

View File

@ -0,0 +1,307 @@
const {
ExternalCommunicationConnector,
} = require("../models/externalCommunicationConnector");
const { Telemetry } = require("../models/telemetry");
const { TelegramBotService } = require("../utils/telegramBot");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { isSingleUserMode } = require("../utils/middleware/multiUserProtected");
const { reqBody } = require("../utils/http");
const { EventLogs } = require("../models/eventLogs");
const { Workspace } = require("../models/workspace");
const { encryptToken } = require("../utils/telegramBot/utils");
function telegramEndpoints(app) {
if (!app) return;
app.get(
"/telegram/config",
[validatedRequest, isSingleUserMode],
async (_request, response) => {
try {
const connector = await ExternalCommunicationConnector.get("telegram");
if (!connector) {
return response.status(200).json({ config: null });
}
const service = new TelegramBotService();
return response.status(200).json({
config: {
active: connector.active,
connected: service.isRunning,
bot_username: connector.config.bot_username || null,
default_workspace: connector.config.default_workspace || null,
voice_response_mode:
connector.config.voice_response_mode || "text_only",
},
});
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
/**
* Verify token, save config, and start the Telegram bot.
*/
app.post(
"/telegram/connect",
[validatedRequest, isSingleUserMode],
async (request, response) => {
try {
const { bot_token, default_workspace = null } = reqBody(request);
if (!bot_token) {
return response.status(400).json({
success: false,
error: "Bot token is required.",
});
}
// Verify the token with Telegram API
const verification = await TelegramBotService.verifyToken(
String(bot_token)
);
if (!verification.valid) {
return response.status(400).json({
success: false,
error: `Invalid bot token: ${verification.error}`,
});
}
let workspaceSlug = null;
if (default_workspace) workspaceSlug = String(default_workspace);
else {
const workspaces = await Workspace.where({}, 1);
if (workspaces.length) workspaceSlug = workspaces[0].slug;
else {
const { workspace } = await Workspace.new(
`${verification.username} Workspace`,
null,
{ chatMode: "automatic" }
);
if (workspace) workspaceSlug = workspace.slug;
}
}
if (!workspaceSlug) {
return response.status(400).json({
success: false,
error: "No workspace found or could be created.",
});
}
// Preserve approved users when reconnecting with a new token
const existing = await ExternalCommunicationConnector.get("telegram");
const storedConfig = {
bot_username: verification.username,
default_workspace: workspaceSlug,
approved_users: existing?.config?.approved_users || [],
voice_response_mode:
existing?.config?.voice_response_mode || "text_only",
};
// Save config with encrypted token
const { error } = await ExternalCommunicationConnector.upsert(
"telegram",
{
...storedConfig,
bot_token: encryptToken(String(bot_token)),
active: true,
}
);
if (error) return response.status(500).json({ success: false, error });
// Start the bot with the plaintext token
const service = new TelegramBotService();
await service.start({ ...storedConfig, bot_token: String(bot_token) });
await EventLogs.logEvent("telegram_bot_connected", {
bot_username: verification.username,
});
await Telemetry.sendTelemetry("telegram_bot_connected");
return response.status(200).json({
success: true,
bot_username: verification.username,
});
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.post(
"/telegram/disconnect",
[validatedRequest, isSingleUserMode],
async (_request, response) => {
try {
const service = new TelegramBotService();
service.stop();
await ExternalCommunicationConnector.delete("telegram");
await EventLogs.logEvent("telegram_bot_disconnected");
return response.status(200).json({ success: true });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.get(
"/telegram/status",
[validatedRequest, isSingleUserMode],
async (_request, response) => {
try {
const connector = await ExternalCommunicationConnector.get("telegram");
const service = new TelegramBotService();
return response.status(200).json({
active: connector?.active && service.isRunning,
bot_username: connector?.config?.bot_username || null,
});
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.get(
"/telegram/pending-users",
[validatedRequest, isSingleUserMode],
async (_request, response) => {
try {
const service = new TelegramBotService();
return response
.status(200)
.json({ users: service.pendingPairings || [] });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.get(
"/telegram/approved-users",
[validatedRequest, isSingleUserMode],
async (_request, response) => {
try {
const connector = await ExternalCommunicationConnector.get("telegram");
const approved = connector?.config?.approved_users || [];
return response.status(200).json({ users: approved });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.post(
"/telegram/approve-user",
[validatedRequest, isSingleUserMode],
async (request, response) => {
try {
const { chatId } = reqBody(request);
if (!chatId)
return response
.status(400)
.json({ success: false, error: "chatId is required." });
const service = new TelegramBotService();
await service.approvePendingUser(chatId);
await EventLogs.logEvent("telegram_user_approved", { chatId });
return response.status(200).json({ success: true });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.post(
"/telegram/deny-user",
[validatedRequest, isSingleUserMode],
async (request, response) => {
try {
const { chatId } = reqBody(request);
if (!chatId)
return response
.status(400)
.json({ success: false, error: "chatId is required." });
const service = new TelegramBotService();
await service.denyPendingUser(chatId);
await EventLogs.logEvent("telegram_user_denied", { chatId });
return response.status(200).json({ success: true });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.post(
"/telegram/revoke-user",
[validatedRequest, isSingleUserMode],
async (request, response) => {
try {
const { chatId } = reqBody(request);
if (!chatId)
return response
.status(400)
.json({ success: false, error: "chatId is required." });
const service = new TelegramBotService();
await service.revokeExistingUser(chatId);
await EventLogs.logEvent("telegram_user_revoked", { chatId });
return response.status(200).json({ success: true });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
app.post(
"/telegram/update-config",
[validatedRequest, isSingleUserMode],
async (request, response) => {
try {
const { voice_response_mode } = reqBody(request);
const updates = {};
if (
voice_response_mode &&
["text_only", "mirror", "always_voice"].includes(voice_response_mode)
) {
updates.voice_response_mode = voice_response_mode;
}
if (Object.keys(updates).length === 0) {
return response
.status(400)
.json({ success: false, error: "No valid updates provided." });
}
const { error } = await ExternalCommunicationConnector.updateConfig(
"telegram",
updates
);
if (error) {
return response.status(500).json({ success: false, error });
}
// Update the running bot's config so changes take effect immediately
const service = new TelegramBotService();
if (service.isRunning) service.updateConfig(updates);
return response.status(200).json({ success: true });
} catch (e) {
console.error(e.message, e);
response.sendStatus(500);
}
}
);
}
module.exports = { telegramEndpoints };

View File

@ -30,6 +30,7 @@ const { agentFlowEndpoints } = require("./endpoints/agentFlows");
const { mcpServersEndpoints } = require("./endpoints/mcpServers");
const { mobileEndpoints } = require("./endpoints/mobile");
const { webPushEndpoints } = require("./endpoints/webPush");
const { telegramEndpoints } = require("./endpoints/telegram");
const { httpLogger } = require("./middleware/httpLogger");
const app = express();
const apiRouter = express.Router();
@ -81,6 +82,7 @@ agentFlowEndpoints(apiRouter);
mcpServersEndpoints(apiRouter);
mobileEndpoints(apiRouter);
webPushEndpoints(apiRouter);
telegramEndpoints(apiRouter);
// Externally facing embedder endpoints
embeddedEndpoints(apiRouter);

View File

@ -0,0 +1,64 @@
// Suppress deprecated content-type warning when sending files via the Telegram bot API.
// https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#file-options-metadata
process.env.NTBA_FIX_350 = 1;
const TelegramBot = require("node-telegram-bot-api");
const { log, conclude } = require("./helpers/index.js");
const { Workspace } = require("../models/workspace");
const { WorkspaceThread } = require("../models/workspaceThread");
const { streamResponse } = require("../utils/telegramBot/chat/stream");
process.on("message", async (payload) => {
const {
botToken,
chatId,
workspaceSlug,
threadSlug,
message,
attachments = [],
voiceResponse = false,
} = payload;
try {
const bot = new TelegramBot(botToken, { polling: false });
const ctx = {
bot,
log: (text, ...args) =>
log(args.length ? `${text} ${args.join(" ")}` : text),
};
const workspace = await Workspace.get({ slug: workspaceSlug });
if (!workspace) {
await bot.sendMessage(
chatId,
"No workspace configured. Use /switch to select one."
);
conclude();
return;
}
const thread = threadSlug
? await WorkspaceThread.get({ slug: threadSlug })
: null;
await streamResponse({
ctx,
chatId,
workspace,
thread,
message,
attachments,
voiceResponse,
});
} catch (error) {
log(`Telegram chat error: ${error.message}`);
try {
const bot = new TelegramBot(botToken, { polling: false });
await bot.sendMessage(
chatId,
"Sorry, something went wrong. Please try again."
);
} catch {}
} finally {
conclude();
}
});

View File

@ -0,0 +1,110 @@
const prisma = require("../utils/prisma");
const { safeJsonParse } = require("../utils/http");
const ExternalCommunicationConnector = {
supportedTypes: ["telegram"],
/**
* Get a connector by type.
* @param {'telegram'} type
* @returns {Promise<{id: number, type: string, config: object, active: boolean}|null>}
*/
get: async function (type) {
try {
const connector =
await prisma.external_communication_connectors.findUnique({
where: { type },
});
if (!connector) return null;
return {
...connector,
config: safeJsonParse(connector.config, {}),
};
} catch (error) {
console.error("ExternalCommunicationConnector.get", error.message);
return null;
}
},
/**
* Create or update a connector's config and active state.
* @param {'telegram'} type
* @param {object} config
* @param {boolean} active
* @returns {Promise<{connector: object|null, error: string|null}>}
*/
upsert: async function (type, config = {}) {
if (!this.supportedTypes.includes(type))
return { connector: null, error: `Unsupported connector type: ${type}` };
try {
let update = {},
create = {};
if (config.hasOwnProperty("active")) {
update.active = Boolean(config.active);
create.active = Boolean(config.active);
delete config.active;
}
update = Object.assign(update, {
config: JSON.stringify(config),
lastUpdatedAt: new Date(),
});
create = Object.assign(create, {
config: JSON.stringify(config),
type: String(type),
});
const connector = await prisma.external_communication_connectors.upsert({
where: { type: String(type) },
update,
create,
});
return {
connector: {
...connector,
config: safeJsonParse(connector.config, {}),
},
error: null,
};
} catch (error) {
console.error("ExternalCommunicationConnector.upsert", error.message);
return { connector: null, error: error.message };
}
},
/**
* Merge partial config updates into an existing connector.
* @param {'telegram'} type
* @param {object} configUpdates - Partial config to merge.
* @returns {Promise<{connector: object|null, error: string|null}>}
*/
updateConfig: async function (type, configUpdates = {}) {
const existing = await this.get(type);
if (!existing)
return { connector: null, error: `No ${type} connector found` };
const mergedConfig = { ...existing.config, ...configUpdates };
return this.upsert(type, mergedConfig, existing.active);
},
/**
* Delete a connector entirely.
* @param {'telegram'} type
* @returns {Promise<boolean>}
*/
delete: async function (type) {
try {
await prisma.external_communication_connectors.delete({
where: { type },
});
return true;
} catch (error) {
console.error("ExternalCommunicationConnector.delete", error.message);
return false;
}
},
};
module.exports = { ExternalCommunicationConnector };

View File

@ -45,6 +45,8 @@
"bcryptjs": "^3.0.3",
"body-parser": "^1.20.3",
"chalk": "^4",
"chart.js": "^4.5.1",
"chartjs-node-canvas": "^5.0.0",
"check-disk-space": "^3.4.0",
"cheerio": "^1.0.0",
"chromadb": "^2.0.1",
@ -69,6 +71,7 @@
"mssql": "^10.0.2",
"multer": "2.0.0",
"mysql2": "^3.9.8",
"node-telegram-bot-api": "^0.67.0",
"ollama": "^0.6.3",
"openai": "4.95.1",
"pg": "^8.11.5",

View File

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "external_communication_connectors" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL,
"config" TEXT NOT NULL DEFAULT '{}',
"active" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex
CREATE UNIQUE INDEX "external_communication_connectors_type_key" ON "external_communication_connectors"("type");

View File

@ -385,3 +385,12 @@ model workspace_parsed_files {
@@index([workspaceId])
@@index([userId])
}
model external_communication_connectors {
id Int @id @default(autoincrement())
type String @unique
config String @default("{}")
active Boolean @default(false)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
}

View File

@ -91,6 +91,50 @@ class BackgroundService {
origin: message.name,
});
}
/**
* Run a one-off job via Bree with a data payload sent over IPC.
* The job file receives the payload via process.on('message').
* @param {string} name - Job filename (without .js) in the jobs directory
* @param {object} payload - Data to send to the job via IPC
* @param {object} [opts]
* @param {function} [opts.onMessage] - Callback for IPC messages from the child process
* @returns {Promise<void>} Resolves when the job exits with code 0
*/
async runJob(name, payload = {}, { onMessage } = {}) {
const jobId = `${name}-${Date.now()}`;
await this.bree.add({
name: jobId,
path: path.resolve(this.#root, `${name}.js`),
});
await this.bree.run(jobId);
const worker = this.bree.workers.get(jobId);
if (worker && typeof worker.send === "function") {
worker.send(payload);
}
if (worker && onMessage) {
worker.on("message", onMessage);
}
return new Promise((resolve, reject) => {
worker.on("exit", async (code) => {
try {
await this.bree.remove(jobId);
} catch {}
if (code === 0) resolve();
else reject(new Error(`Job ${jobId} exited with code ${code}`));
});
worker.on("error", async (err) => {
try {
await this.bree.remove(jobId);
} catch {}
reject(err);
});
});
}
}
module.exports.BackgroundService = BackgroundService;

View File

@ -39,7 +39,7 @@ class EncryptionManager {
this.log("Self-assigning key & salt for encrypting arbitrary data.");
process.env[this.#keyENV] = crypto.randomBytes(32).toString("hex");
process.env[this.#saltENV] = crypto.randomBytes(32).toString("hex");
if (process.env.NODE_ENV === "production") dumpENV();
dumpENV();
} else
this.log("Loaded existing key & salt for encrypting arbitrary data.");

View File

@ -5,6 +5,7 @@ const {
} = require("../../models/workspaceAgentInvocation");
const { WorkspaceParsedFiles } = require("../../models/workspaceParsedFiles");
const { User } = require("../../models/user");
const { Workspace } = require("../../models/workspace");
const { WorkspaceChats } = require("../../models/workspaceChats");
const { safeJsonParse } = require("../http");
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
@ -36,6 +37,41 @@ class AgentHandler {
this.log(`End ${this.#invocationUUID}::${this.provider}:${this.model}`);
}
/**
* Determine if the message should invoke the agent handler.
* This is true when the user explicitly invokes an agent (via @agent prefix)
* or when the workspace is in automatic mode **and** the provider supports native tool calling.
* @param {object} parameters
* @param {string} parameters.message - The message to check for agent invocation.
* @param { import("@prisma/client").workspaces} parameters.workspace - The workspace to check for agent invocation.
* @param {string} parameters.chatMode - The chat mode to check for agent invocation.
* @returns {Promise<boolean>}
*/
static async isAgentInvocation({
message,
workspace = null,
chatMode = null,
}) {
if (this.#isAgentCommandInvocation({ message })) return true;
if (chatMode === "automatic") {
if (!workspace) return false;
if (await Workspace.supportsNativeToolCalling(workspace)) return true;
return false;
}
return false;
}
/**
* Determine if the message provided is an agent invocation.
* @param {{message:string}} parameters
* @returns {boolean}
*/
static #isAgentCommandInvocation({ message }) {
const agentHandles = WorkspaceAgentInvocation.parseAgents(message);
if (agentHandles.length > 0) return true;
return false;
}
async #chatHistory(limit = 10) {
try {
const rawHistory = (

View File

@ -6,6 +6,7 @@ const setupTelemetry = require("../telemetry");
const eagerLoadContextWindows = require("./eagerLoadContextWindows");
const markOnboarded = require("./markOnboarded");
const { PushNotifications } = require("../PushNotifications");
const { TelegramBotService } = require("../telegramBot");
// Testing SSL? You can make a self signed certificate and point the ENVs to that location
// make a directory in server called 'sslcert' - cd into it
@ -37,6 +38,7 @@ function bootSSL(app, port = 3001) {
new BackgroundService().boot();
await eagerLoadContextWindows();
await PushNotifications.setupPushNotificationService();
await TelegramBotService.bootIfActive();
console.log(`Primary server in HTTPS mode listening on port ${port}`);
})
.on("error", catchSigTerms);
@ -69,6 +71,7 @@ function bootHTTP(app, port = 3001) {
new BackgroundService().boot();
await eagerLoadContextWindows();
await PushNotifications.setupPushNotificationService();
await TelegramBotService.bootIfActive();
console.log(`Primary server in HTTP mode listening on port ${port}`);
})
.on("error", catchSigTerms);

View File

@ -8,6 +8,18 @@ const ROLES = {
};
const DEFAULT_ROLES = [ROLES.admin, ROLES.admin];
/**
* Explicitly check that single user mode is enabled as well as that the
* requesting user has the appropriate role to modify or call the URL.
* @returns {function}
*/
async function isSingleUserMode(_request, response, next) {
const multiUserMode = await SystemSettings.isMultiUserMode();
if (multiUserMode) return response.sendStatus(401).end();
next();
return;
}
/**
* Explicitly check that multi user mode is enabled as well as that the
* requesting user has the appropriate role to modify or call the URL.
@ -88,6 +100,7 @@ async function isMultiUserSetup(_request, response, next) {
module.exports = {
ROLES,
isSingleUserMode,
strictMultiUserRoleValid,
flexUserRoleValid,
isMultiUserSetup,

View File

@ -0,0 +1,377 @@
const { v4: uuidv4 } = require("uuid");
const { EphemeralAgentHandler } = require("../../agents/ephemeral");
const { WorkspaceChats } = require("../../../models/workspaceChats");
const { safeJsonParse } = require("../../http");
const {
editMessage,
sendFormattedMessage,
upsertMessage,
} = require("../utils");
const { escapeHTML } = require("../utils/format");
const { sendVoiceResponse } = require("../utils/media");
const {
STREAM_EDIT_INTERVAL,
MAX_MSG_LEN,
CURSOR_CHAR,
} = require("../constants");
const THOUGHT_FLUSH_INTERVAL_MS = 1500;
/**
* Run the agent pipeline for @agent messages and send the result to Telegram.
* Uses EphemeralAgentHandler to avoid creating a DB invocation record per call.
* @param {import("../commands").BotContext} ctx
* @param {number} chatId
* @param {import('@prisma/client').workspaces} workspace
* @param {object|null} thread
* @param {string} message
* @param {boolean} voiceResponse - Whether to send the response as voice audio
* @param {Array<{name: string, mime: string, contentString: string}>} attachments - Image/file attachments for multimodal support
*/
async function handleAgentResponse(
ctx,
chatId,
workspace,
thread,
message,
voiceResponse = false,
attachments = []
) {
let finalResponse = "";
let metrics = {};
const sources = [];
const thoughts = [];
const charts = [];
const files = [];
let thoughtMsgId = null;
let lastThoughtText = "";
// Streaming response state (similar to stream.js createStreamHandler)
let streamingText = "";
let responseMsgId = null;
let responsePending = null;
let lastEditTime = 0;
let editTimer = null;
let msgOffset = 0;
const currentResponseText = () => streamingText.slice(msgOffset);
const handleStreamChunk = (chunk) => {
streamingText += chunk;
// Handle message splitting when content exceeds Telegram's limit
if (responseMsgId !== null && currentResponseText().length > MAX_MSG_LEN) {
clearTimeout(editTimer);
editTimer = null;
editMessage(
ctx.bot,
chatId,
responseMsgId,
streamingText.slice(msgOffset, msgOffset + MAX_MSG_LEN),
ctx.log,
{ format: true }
).catch(() => {});
msgOffset += MAX_MSG_LEN;
responseMsgId = null;
responsePending = null;
}
// Send initial message if none exists yet
if (responseMsgId === null && !responsePending) {
responsePending = ctx.bot
.sendMessage(chatId, currentResponseText() + CURSOR_CHAR)
.then((sent) => {
responseMsgId = sent.message_id;
lastEditTime = Date.now();
})
.catch(() => {
responsePending = null;
});
return;
}
if (!responseMsgId) return;
// Throttled message updates
const now = Date.now();
if (now - lastEditTime >= STREAM_EDIT_INTERVAL) {
clearTimeout(editTimer);
lastEditTime = now;
editMessage(
ctx.bot,
chatId,
responseMsgId,
currentResponseText() + CURSOR_CHAR,
ctx.log
).catch(() => {});
} else if (!editTimer) {
editTimer = setTimeout(() => {
lastEditTime = Date.now();
editMessage(
ctx.bot,
chatId,
responseMsgId,
currentResponseText() + CURSOR_CHAR,
ctx.log
).catch(() => {});
editTimer = null;
}, STREAM_EDIT_INTERVAL);
}
};
const handler = {
send(data) {
const parsed = safeJsonParse(data, null);
if (!parsed) return;
switch (parsed.type) {
case "statusResponse":
if (parsed.content) thoughts.push(parsed.content);
return;
case "rechartVisualize":
if (parsed.content) charts.push(parsed.content);
return;
case "fileDownload":
if (parsed.content) files.push(parsed.content);
return;
case "reportStreamEvent":
const inner = parsed.content;
if (!inner) return;
if (inner.type === "textResponseChunk" && inner.content) {
handleStreamChunk(inner.content);
return;
}
if (inner.type === "fullTextResponse" && inner.content) {
finalResponse = inner.content;
return;
}
if (inner.type === "usageMetrics" && inner.metrics) {
metrics = inner.metrics;
return;
}
if (inner.type === "citations" && inner.citations) {
sources.push(...inner.citations);
}
return;
default:
if (!parsed.from || parsed.from === "USER" || !parsed.content) return;
finalResponse = parsed.content;
break;
}
},
close() {},
};
// Periodically flush thoughts as a single live-updating message
let flushing = false;
let thoughtFlushTimeout = null;
const formatThoughtsAsBlockquote = (thoughtList, done = false) => {
const header = done
? "✓ <b>Agent completed:</b>"
: "🤔 <b>Agent is thinking:</b>";
const icon = done ? "✓" : "⏳";
const content = thoughtList
.map((t) => `${icon} ${escapeHTML(t)}`)
.join("\n");
const fullContent = `${header}\n${content}`;
const tag =
fullContent.length > 200 ? "blockquote expandable" : "blockquote";
return `<${tag}>${fullContent}</${tag.split(" ")[0]}>`;
};
const flushThoughts = async () => {
if (flushing || thoughts.length === 0) {
thoughtFlushTimeout = setTimeout(
flushThoughts,
THOUGHT_FLUSH_INTERVAL_MS
);
return;
}
const text = formatThoughtsAsBlockquote(thoughts, false);
if (text === lastThoughtText) {
thoughtFlushTimeout = setTimeout(
flushThoughts,
THOUGHT_FLUSH_INTERVAL_MS
);
return;
}
lastThoughtText = text;
flushing = true;
try {
thoughtMsgId = await upsertMessage(
ctx.bot,
chatId,
thoughtMsgId,
text,
ctx.log,
{ html: true, disableLinkPreview: true }
);
} catch (err) {
ctx.log?.error?.("Failed to update thought message:", err);
} finally {
flushing = false;
thoughtFlushTimeout = setTimeout(
flushThoughts,
THOUGHT_FLUSH_INTERVAL_MS
);
}
};
thoughtFlushTimeout = setTimeout(flushThoughts, THOUGHT_FLUSH_INTERVAL_MS);
const typingInterval = setInterval(() => {
ctx.bot.sendChatAction(chatId, "typing").catch(() => {});
}, 4000);
try {
const agentHandler = await new EphemeralAgentHandler({
uuid: uuidv4(),
workspace,
prompt: message,
userId: null,
threadId: thread?.id || null,
attachments,
}).init();
await agentHandler.createAIbitat({ handler });
// httpSocket terminates after the first agent message, but cap rounds
// as a safety net so the agent can't loop indefinitely.
agentHandler.aibitat.maxRounds = 2;
await agentHandler.startAgentCluster();
} finally {
clearInterval(typingInterval);
clearTimeout(thoughtFlushTimeout);
clearTimeout(editTimer);
}
// Final thought update, mark as completed
if (thoughtMsgId && thoughts.length > 0) {
const doneText = formatThoughtsAsBlockquote(thoughts, true);
await editMessage(ctx.bot, chatId, thoughtMsgId, doneText, ctx.log, {
html: true,
disableLinkPreview: true,
});
}
// Send charts as locally rendered images
for (const chart of charts) {
try {
const buffer = await renderChartToBuffer(chart);
await ctx.bot.sendPhoto(
chatId,
buffer,
{ caption: chart.title },
{
filename: "chart.png",
contentType: "image/png",
knownLength: buffer.length,
}
);
} catch {
await ctx.bot.sendMessage(
chatId,
`${chart.title}: failed to render chart.`
);
}
}
// Send files as Telegram documents
for (const file of files) {
try {
const base64Data = file.b64Content.split(",")[1];
const buffer = Buffer.from(base64Data, "base64");
await ctx.bot.sendDocument(
chatId,
buffer,
{},
{
filename: file.filename,
contentType: "application/octet-stream",
}
);
} catch {}
}
// Ensure the initial sendMessage has resolved before deciding how to deliver
if (responsePending) await responsePending;
// Fall back to the accumulated streamed text when no explicit
// fullTextResponse event was received (e.g. audio/voice messages).
const responseText = finalResponse || streamingText;
if (responseText) {
await WorkspaceChats.new({
workspaceId: workspace.id,
prompt: message,
response: {
text: responseText,
sources,
type: "chat",
metrics,
attachments,
},
threadId: thread?.id || null,
});
// Always deliver text response first
if (responseMsgId) {
await editMessage(
ctx.bot,
chatId,
responseMsgId,
finalResponse || currentResponseText(),
ctx.log,
{
format: true,
}
).catch(() => {});
} else {
await sendFormattedMessage(ctx.bot, chatId, responseText);
}
// Send voice as an additional attachment if requested
if (voiceResponse) {
ctx.log?.info?.(`Generating voice response for ${chatId}`);
await sendVoiceResponse(ctx.bot, chatId, responseText);
}
}
}
/**
* Render a chart to a PNG buffer using chartjs-node-canvas.
* @param {object} chart - { type, title, dataset }
* @returns {Promise<Buffer>}
*/
async function renderChartToBuffer(chart) {
const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
const canvas = new ChartJSNodeCanvas({ width: 600, height: 400 });
const data = JSON.parse(chart.dataset);
const labels = data.map((d) => d.name);
const valueKey = Object.keys(data[0]).find((k) => k !== "name");
const values = data.map((d) => d[valueKey]);
const config = {
type: chart.type === "area" ? "line" : chart.type,
data: {
labels,
datasets: [
{
label: chart.title,
data: values,
fill: chart.type === "area",
borderColor: "rgb(59, 130, 246)",
backgroundColor: "rgba(59, 130, 246, 0.2)",
},
],
},
options: {
plugins: { title: { display: true, text: chart.title } },
},
};
return await canvas.renderToBuffer(config);
}
module.exports = { handleAgentResponse };

View File

@ -0,0 +1,469 @@
const { WorkspaceChats } = require("../../../models/workspaceChats");
const { getLLMProvider, getVectorDbClass } = require("../../helpers");
const { DocumentManager } = require("../../DocumentManager");
const {
sourceIdentifier,
recentChatHistory,
chatPrompt,
} = require("../../chats");
const { fillSourceWindow } = require("../../helpers/chat");
const { AgentHandler } = require("../../agents");
const {
STREAM_EDIT_INTERVAL,
MAX_MSG_LEN,
CURSOR_CHAR,
} = require("../constants");
const { editMessage, sendFormattedMessage } = require("../utils");
const { sendVoiceResponse } = require("../utils/media");
const { safeJsonParse } = require("../../http");
const { handleAgentResponse } = require("./agent");
/**
* Check if the history is agentic by checking if any user messages start with "@agent"
* so that "chat" mode workspaces can still carry on with agentic conversations
* otherwise this is handled with "automatic" mode.
* @param {'chat' | 'automatic' | 'query'} chatMode - The chat mode.
* @param {{role: 'user' | 'assistant', content: string}[]} chatHistory - The chat history.
* @returns {boolean} - True if the history is agentic, false otherwise.
*/
function historyIsAgentic(chatMode, chatHistory) {
if (chatMode !== "chat") return false;
return chatHistory.some(
(message) => message.role === "user" && message.content.startsWith("@agent")
);
}
/**
* Stream a response to Telegram by running the full RAG pipeline.
* Uses the same pipeline as the web UI (RAG, parsed docs, pinned docs, etc.)
* and stores chats with thread_id so they appear in the AnythingLLM UI.
*
* However, we are able to consistently handle agentic conversations in "chat" mode by checking the chat history
* without needing to open/close an agent invocation every chat which is wasteful on the DB.
*
* Query mode is also not supported in this flow - as it would be pretty useless.
*
* @param {object} context - The context object.
* @param {import("../commands").BotContext} context.ctx - The bot object.
* @param {number} context.chatId - The chat ID.
* @param {import('@prisma/client').workspaces} context.workspace - The workspace object.
* @param {object|null} context.thread - The thread object.
* @param {string} context.message - The message to send.
* @param {array} context.attachments - The attachments to send.
* @param {boolean} context.voiceResponse - Whether to send the response as voice.
*/
async function streamResponse({
ctx = null,
chatId = null,
workspace = null,
thread = null,
message = "",
attachments = [],
voiceResponse = false,
}) {
if (!ctx?.bot || !chatId || !workspace || !message)
throw new Error("Invalid context or missing required parameters!");
await ctx.bot.sendChatAction(chatId, "typing");
const chatMode = workspace.chatMode || "chat";
const messageLimit = workspace?.openAiHistory || 20;
const { rawHistory, chatHistory } = await recentChatHistory({
workspace,
thread,
messageLimit,
});
if (
historyIsAgentic(chatMode, chatHistory) ||
(await AgentHandler.isAgentInvocation({
message,
workspace,
chatMode: workspace.chatMode ?? "chat",
}))
) {
return await handleAgentResponse(
ctx,
chatId,
workspace,
thread,
message,
voiceResponse,
attachments
);
}
const typingInterval = setInterval(() => {
ctx.bot.sendChatAction(chatId, "typing").catch(() => {});
}, 4000);
const LLMConnector = getLLMProvider({
provider: workspace?.chatProvider,
model: workspace?.chatModel,
});
const VectorDb = getVectorDbClass();
const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);
const {
contextTexts: pinnedContextTexts,
sources: pinnedSources,
pinnedDocIdentifiers,
} = await collectPinnedDocs(workspace, LLMConnector);
const {
contextTexts: searchContextTexts,
sources: searchSources,
error: searchError,
} = await buildSearchContext({
workspace,
message,
VectorDb,
LLMConnector,
embeddingsCount,
rawHistory,
pinnedDocIdentifiers,
});
if (searchError) {
clearInterval(typingInterval);
return await ctx.bot.sendMessage(chatId, searchError);
}
const contextTexts = [...pinnedContextTexts, ...searchContextTexts];
const sources = [...pinnedSources, ...searchSources];
const messages = await LLMConnector.compressMessages(
{
systemPrompt: await chatPrompt(workspace),
userPrompt: message,
contextTexts,
chatHistory,
attachments,
},
rawHistory
);
try {
const { completeText, metrics } = await generateResponse({
LLMConnector,
messages,
workspace,
ctx,
chatId,
});
await persistAndDeliver({
workspace,
thread,
message,
completeText,
sources,
chatMode,
metrics,
attachments,
voiceResponse,
ctx,
chatId,
});
} catch (error) {
console.error("Error streaming response:", error);
await ctx.bot.sendMessage(
chatId,
"An error occurred while streaming the response."
);
} finally {
clearInterval(typingInterval);
}
}
/**
* Gather context texts, sources, and identifiers from pinned documents.
* @returns {Promise<{ contextTexts: string[], sources: object[], pinnedDocIdentifiers: string[] }>}
*/
async function collectPinnedDocs(workspace, LLMConnector) {
const contextTexts = [];
const sources = [];
const pinnedDocIdentifiers = [];
const pinnedDocs = await new DocumentManager({
workspace,
maxTokens: LLMConnector.promptWindowLimit(),
}).pinnedDocs();
for (const doc of pinnedDocs) {
const { pageContent, ...metadata } = doc;
pinnedDocIdentifiers.push(sourceIdentifier(doc));
contextTexts.push(pageContent);
sources.push({
text:
pageContent.slice(0, 1_000) + "...continued on in source document...",
...metadata,
});
}
return { contextTexts, sources, pinnedDocIdentifiers };
}
/**
* Run vector similarity search and fill the source window.
* @returns {Promise<{ contextTexts: string[], sources: object[], error: string|null }>}
*/
async function buildSearchContext({
workspace,
message,
VectorDb,
LLMConnector,
embeddingsCount,
rawHistory,
pinnedDocIdentifiers,
}) {
const vectorSearchResults =
embeddingsCount !== 0
? await VectorDb.performSimilaritySearch({
namespace: workspace.slug,
input: message,
LLMConnector,
similarityThreshold: workspace?.similarityThreshold,
topN: workspace?.topN,
filterIdentifiers: pinnedDocIdentifiers,
rerank: workspace?.vectorSearchMode === "rerank",
})
: { contextTexts: [], sources: [], message: null };
if (vectorSearchResults.message) {
return {
contextTexts: [],
sources: [],
error: "Vector search failed. Please try again.",
};
}
const filledSources = fillSourceWindow({
nDocs: workspace?.topN || 4,
searchResults: vectorSearchResults.sources,
history: rawHistory,
filterIdentifiers: pinnedDocIdentifiers,
});
return {
contextTexts: filledSources.contextTexts,
sources: vectorSearchResults.sources,
error: null,
};
}
/**
* Run the LLM completion (streaming or non-streaming) and deliver the in-progress response.
* Clears the typing indicator when done.
* @returns {Promise<{ completeText: string, metrics: object }>}
*/
async function generateResponse({
LLMConnector,
messages,
workspace,
ctx,
chatId,
}) {
let completeText = "";
let metrics = {};
if (LLMConnector.streamingEnabled() === true) {
const stream = await LLMConnector.streamGetChatCompletion(messages, {
temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,
});
const { responseHandler, flushEdit } = createStreamHandler({
ctx,
chatId,
});
completeText = await LLMConnector.handleStream(responseHandler, stream, {
uuid: chatId.toString(),
});
await flushEdit(true);
metrics = stream.metrics || {};
} else {
const { textResponse, metrics: performanceMetrics } =
await LLMConnector.getChatCompletion(messages, {
temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,
user: null,
});
completeText = textResponse;
metrics = performanceMetrics || {};
if (completeText?.length > 0)
await sendFormattedMessage(ctx.bot, chatId, completeText);
}
return { completeText, metrics };
}
/**
* Save the completed chat to the database and optionally deliver a voice response.
*/
async function persistAndDeliver({
workspace,
thread,
message,
completeText,
sources,
chatMode,
metrics,
attachments,
voiceResponse,
ctx,
chatId,
}) {
if (!completeText?.length) {
await ctx.bot.sendMessage(chatId, "No response generated.");
return;
}
await WorkspaceChats.new({
workspaceId: workspace.id,
prompt: message,
response: {
text: completeText,
sources,
type: chatMode,
metrics,
attachments,
},
threadId: thread?.id || null,
});
// Send voice as an additional attachment if requested
if (voiceResponse) {
ctx.log?.info?.(`Generating voice response for ${chatId}`);
await sendVoiceResponse(ctx.bot, chatId, completeText);
}
}
/**
* Parse an SSE data chunk and return the text token, or null if not a text token.
*/
function parseSSEChunk(data) {
const match = data.match(/^data: (.+)\n\n$/s);
if (!match) return null;
const parsed = safeJsonParse(match[1], null);
if (!parsed || !parsed.textResponse || parsed.close) return null;
return parsed.textResponse;
}
/**
* Create a stream response handler for editing Telegram messages as tokens arrive.
* Manages message splitting when content exceeds Telegram's length limit.
* @param {object} options
* @param {import("./commands").BotContext} options.ctx - Bot context
* @param {number} options.chatId - Telegram chat ID
* @returns {{ responseHandler: object, flushEdit: function }}
*/
function createStreamHandler({ ctx, chatId }) {
let completeText = "";
let messageId = null;
let messagePending = null;
let lastEditTime = 0;
let editTimer = null;
let msgOffset = 0;
const currentText = () => completeText.slice(msgOffset);
/**
* Finalize the current message and reset state when accumulated text
* exceeds Telegram's max message length.
*/
function splitMessageIfOverflow() {
if (messageId === null || currentText().length <= MAX_MSG_LEN) return;
clearTimeout(editTimer);
editTimer = null;
editMessage(
ctx.bot,
chatId,
messageId,
completeText.slice(msgOffset, msgOffset + MAX_MSG_LEN),
ctx.log,
{ format: true }
).catch(() => {});
msgOffset += MAX_MSG_LEN;
messageId = null;
messagePending = null;
}
/**
* Send a new Telegram message when none exists yet.
* @returns {boolean} true if a new message was initiated (caller should skip edit).
*/
function startNewMessageIfNeeded() {
if (messageId !== null || messagePending) return false;
messagePending = ctx.bot
.sendMessage(chatId, currentText() + CURSOR_CHAR)
.then((sent) => {
messageId = sent.message_id;
lastEditTime = Date.now();
})
.catch(() => {
messagePending = null;
});
return true;
}
/**
* Throttle edits to the current message so we don't exceed Telegram rate limits.
*/
function scheduleThrottledEdit() {
if (!messageId) return;
const now = Date.now();
if (now - lastEditTime >= STREAM_EDIT_INTERVAL) {
clearTimeout(editTimer);
lastEditTime = now;
editMessage(
ctx.bot,
chatId,
messageId,
currentText() + CURSOR_CHAR,
ctx.log
).catch(() => {});
} else if (!editTimer) {
editTimer = setTimeout(() => {
lastEditTime = Date.now();
editMessage(
ctx.bot,
chatId,
messageId,
currentText() + CURSOR_CHAR,
ctx.log
).catch(() => {});
editTimer = null;
}, STREAM_EDIT_INTERVAL);
}
}
const flushEdit = async (final = false) => {
if (messagePending) await messagePending;
if (!messageId) return;
clearTimeout(editTimer);
editTimer = null;
const text = currentText();
const display = final ? text : text + CURSOR_CHAR;
await editMessage(ctx.bot, chatId, messageId, display, ctx.log, {
format: final,
}).catch(() => {});
};
const responseHandler = {
on: () => {},
removeListener: () => {},
write: (data) => {
const token = parseSSEChunk(data);
if (!token) return;
completeText += token;
splitMessageIfOverflow();
if (!startNewMessageIfNeeded()) scheduleThrottledEdit();
},
};
return { responseHandler, flushEdit };
}
module.exports = { streamResponse };

View File

@ -0,0 +1,22 @@
/**
* Minimum interval between Telegram message edits (ms) to avoid rate limiting
*/
const STREAM_EDIT_INTERVAL = 600;
/**
* Telegram messages cap at 4096 chars. We use 4000 to leave headroom
* so we can finalize the current message and continue in a new one.
*/
const MAX_MSG_LEN = 4000;
/**
* The cursor character to use for streaming responses.
* Looks like a blinking block, but doesnt actually blink.
*/
const CURSOR_CHAR = " \u258d";
module.exports = {
STREAM_EDIT_INTERVAL,
MAX_MSG_LEN,
CURSOR_CHAR,
};

View File

@ -0,0 +1,632 @@
// Suppress deprecated content-type warning when sending files via the Telegram bot API.
// https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
process.env.NTBA_FIX_350 = 1;
const TelegramBot = require("node-telegram-bot-api");
const {
ExternalCommunicationConnector,
} = require("../../models/externalCommunicationConnector");
const { BackgroundService } = require("../BackgroundWorkers");
const { MessageQueue } = require("./utils/messageQueue");
const { decryptToken } = require("./utils");
const {
WorkspaceAgentInvocation,
} = require("../../models/workspaceAgentInvocation");
const {
isVerified,
sendPairingRequest,
approveUser,
denyUser,
revokeUser,
} = require("./utils/verification");
const { BOT_COMMANDS } = require("./utils/commands");
const { handleKeyboardQueryCallback } = require("./utils/navigation");
const {
downloadTelegramFile,
transcribeAudio,
documentToText,
photoToAttachment,
} = require("./utils/media");
class TelegramBotService {
static _instance = null;
#bot = null;
#config = null;
#queue = new MessageQueue();
// Per-chat state: { workspaceSlug, threadSlug }
#chatState = new Map();
// Pending pairing requests: chatId -> { code, telegramUsername, firstName }
#pendingPairings = new Map();
// Active workers per chat: chatId -> { worker, jobId }
#activeWorkers = new Map();
constructor() {
if (TelegramBotService._instance) return TelegramBotService._instance;
TelegramBotService._instance = this;
}
get isRunning() {
return this.#bot !== null;
}
get pendingPairings() {
const pairings = [];
for (const [chatId, data] of this.#pendingPairings) {
pairings.push({ chatId: String(chatId), ...data });
}
return pairings;
}
#log(text, ...args) {
console.log(`\x1b[35m[TelegramBot]\x1b[0m ${text}`, ...args);
}
async start(config) {
if (this.#bot) await this.stop();
this.#config = config;
// Clear pending updates on startup, keeping only the last message per chat
// This prevents processing a backlog of messages when the bot restarts.
this.#bot = new TelegramBot(config.bot_token, { polling: false });
const lastMessages = await this.#clearPendingUpdates();
this.#bot.startPolling();
// Restore per-user workspace/thread state from saved config
for (const user of config.approved_users || []) {
if (user.active_workspace) {
this.#chatState.set(Number(user.chatId), {
workspaceSlug: user.active_workspace,
threadSlug: user.active_thread || null,
});
}
}
this.#setupHandlers();
await this.#registerCommands();
this.#log(`Started polling as @${config.bot_username || "unknown"}`);
// Process only the last message from each chat that was pending
if (lastMessages.size > 0) {
this.#log(
`Processing ${lastMessages.size} pending message(s) from startup`
);
const ctx = this.#createContext();
for (const [chatId, msg] of lastMessages) {
if (!isVerified(this.#config.approved_users, chatId)) continue;
this.#processPendingMessage(ctx, msg);
}
}
}
/**
* Process a single pending message from startup.
* Handles both commands and regular messages.
*/
#processPendingMessage(ctx, msg) {
const text = msg.text || "";
// Handle commands
if (text.startsWith("/")) {
const commandMatch = text.match(/^\/(\w+)/);
if (!commandMatch) return;
const commandName = commandMatch[1];
const command = BOT_COMMANDS.find((c) => c.command === commandName);
if (command) {
const handler = command.initHandler();
handler(ctx, msg.chat.id, text);
return;
}
}
// Handle regular messages
this.#handleMessage(ctx, msg);
}
/**
* Check if the instance is running in multi-user mode
* @returns {Promise<boolean>}
*/
async checkMultiUserMode() {
const { SystemSettings } = require("../../models/systemSettings");
return await SystemSettings.isMultiUserMode();
}
updateConfig(updates) {
if (!this.#config) return;
Object.assign(this.#config, updates);
}
async stop() {
if (!this.#bot) return;
try {
await this.#bot.stopPolling();
} catch {
// Polling may already be stopped
}
// Kill any active workers before clearing state
for (const chatId of this.#activeWorkers.keys()) {
this.abortChat(chatId);
}
this.#bot = null;
this.#config = null;
this.#queue.clear();
this.#chatState.clear();
this.#pendingPairings.clear();
this.#activeWorkers.clear();
this.#log("Stopped");
}
/**
* Self-cleanup when the bot token becomes invalid (e.g., bot deleted).
* Stops polling and removes the connector from the database.
*/
async #selfCleanup(reason) {
this.#log(`Self-cleanup triggered: ${reason}`);
await this.stop();
await ExternalCommunicationConnector.delete("telegram");
this.#log("Connector deleted due to invalid token");
}
/**
* Handle polling errors with special handling for 401 Unauthorized.
* - 401 errors: Self-cleanup and delete connector
* - Other HTTP error codes: Stop polling immediately
*/
async #handlePollingError(error) {
this.#log("Polling error:", error.message);
if (error.message?.includes("401")) {
this.#log(
"Got 401 - bot token may be invalid. Stopping polling and deleting connector."
);
return this.#selfCleanup("401 Unauthorized");
}
this.#log(
`Got HTTP error ${error.message}. Stopping polling to prevent further errors.`
);
return this.stop();
}
/**
* Clear pending updates on startup, keeping only the last user message per chat.
* This prevents processing a backlog of messages when the bot restarts.
* @returns {Promise<Map<number, object>>} Map of chatId -> last message to process
*/
async #clearPendingUpdates() {
const lastMessages = new Map();
try {
// Fetch all pending updates (up to 100)
const updates = await this.#bot.getUpdates({ limit: 100, timeout: 0 });
if (!updates || updates.length === 0) return lastMessages;
this.#log(`Found ${updates.length} pending update(s) on startup`);
// Find the last message per chat (including commands)
for (const update of updates) {
const msg = update.message;
if (!msg) continue;
const chatId = msg.chat.id;
// Keep overwriting to get the last message per chat
lastMessages.set(chatId, msg);
}
// Mark all updates as processed by requesting with offset past the last one
const lastUpdateId = updates[updates.length - 1].update_id;
await this.#bot.getUpdates({
offset: lastUpdateId + 1,
limit: 1,
timeout: 0,
});
this.#log(
`Cleared pending updates, will process ${lastMessages.size} last message(s)`
);
} catch (error) {
this.#log("Failed to clear pending updates:", error.message);
}
return lastMessages;
}
async #registerCommands() {
try {
const commands = BOT_COMMANDS.map((c) => ({
command: c.command,
description: c.description,
}));
await this.#bot.setMyCommands(commands);
} catch (error) {
this.#log("Failed to register commands:", error.message);
}
}
#getState(chatId) {
if (!this.#chatState.has(chatId)) {
this.#chatState.set(chatId, {
workspaceSlug: this.#config.default_workspace,
threadSlug: null,
});
}
return this.#chatState.get(chatId);
}
#setState(chatId, updates) {
const state = this.#getState(chatId);
Object.assign(state, updates);
this.#persistChatState(chatId, state);
}
async #persistChatState(chatId, state) {
const approved = (this.#config.approved_users || []).map((u) => {
if (String(u.chatId) === String(chatId)) {
return {
...u,
active_workspace: state.workspaceSlug,
active_thread: state.threadSlug,
};
}
return u;
});
this.#config.approved_users = approved;
await ExternalCommunicationConnector.updateConfig("telegram", {
approved_users: approved,
});
}
/**
* Build a context object that handler modules use to access
* the bot instance, config, and state helpers.
*/
#createContext() {
return {
bot: this.#bot,
config: this.#config,
getState: (chatId) => this.#getState(chatId),
setState: (chatId, updates) => this.#setState(chatId, updates),
log: (text, ...args) => this.#log(text, ...args),
};
}
async approvePendingUser(chatId) {
await approveUser(this.#bot, chatId, this.#config, this.#pendingPairings);
}
async denyPendingUser(chatId) {
await denyUser(this.#bot, chatId, this.#pendingPairings);
}
async revokeExistingUser(chatId) {
await revokeUser(chatId, this.#config);
}
/**
* Assert that the bot is running in single-user mode.
* If the instance is running in multi-user mode, it will stop the bot and delete the connector.
* - Returns true if the bot is running in single-user mode.
* - Returns false if the bot is running in multi-user mode.
* @returns {Promise<boolean>}
*/
async #assertSingleUserMode() {
const isMultiUserMode = await this.checkMultiUserMode();
if (!isMultiUserMode) return true;
this.#log(
"Invalid state: Multi-user mode detected. Cleaning up and deleting connector."
);
await this.stop();
await ExternalCommunicationConnector.delete("telegram");
return false;
}
#setupHandlers() {
const ctx = this.#createContext();
const guard = async (msg, handler) => {
if (!isVerified(this.#config.approved_users, msg.chat.id)) {
sendPairingRequest(this.#bot, msg, this.#pendingPairings);
return;
}
const isSingleUserMode = await this.#assertSingleUserMode();
if (!isSingleUserMode) return;
handler();
};
// Register all commands (history is registered separately below)
for (const command of BOT_COMMANDS) {
if (command.skipAutoSetup) continue;
const handler = command.initHandler();
this.#bot.onText(new RegExp(`\\/${command.command}`), (msg) =>
guard(msg, () => handler(ctx, msg.chat.id, msg.text))
);
}
// Register /history separately so we can pass the message text for argument parsing
// Ex: /history 25 shows last 25 messages
this.#bot.onText(/\/history(.*)/, (msg) => {
const handler = BOT_COMMANDS.find(
(c) => c.command === "history"
).initHandler();
guard(msg, () => handler(ctx, msg.chat.id, msg.text));
});
// Register callback queries, used for workspace/thread selection interactive menus
this.#bot.on("callback_query", (query) =>
handleKeyboardQueryCallback(ctx, query)
);
this.#bot.on("message", (msg) => {
if (msg.text?.startsWith("/")) return;
guard(msg, () => this.#handleMessage(ctx, msg));
});
this.#bot.on("polling_error", (error) => {
this.#handlePollingError(error);
});
}
async #runChatJob(ctx, chatId, payload) {
const state = this.#getState(chatId);
try {
const bgService = new BackgroundService();
const jobId = `handle-telegram-chat-${Date.now()}`;
let invocationUuid = null;
let wasAborted = false;
await bgService.bree.add({
name: jobId,
path: require("path").resolve(
__dirname,
"../../jobs/handle-telegram-chat.js"
),
});
await bgService.bree.run(jobId);
const worker = bgService.bree.workers.get(jobId);
if (worker && typeof worker.send === "function") {
worker.send({
botToken: this.#config.bot_token,
chatId,
workspaceSlug: state.workspaceSlug,
threadSlug: state.threadSlug,
...payload,
});
}
if (worker) {
worker.on("message", (msg) => {
if (msg?.type === "closeInvocation") invocationUuid = msg.uuid;
});
this.#activeWorkers.set(chatId, { worker, jobId, bgService });
}
await new Promise((resolve, reject) => {
worker.on("exit", async (code) => {
this.#activeWorkers.delete(chatId);
try {
await bgService.bree.remove(jobId);
} catch {}
if (code === 0 || wasAborted) resolve();
else reject(new Error(`Job ${jobId} exited with code ${code}`));
});
worker.on("error", async (err) => {
this.#activeWorkers.delete(chatId);
try {
await bgService.bree.remove(jobId);
} catch {}
reject(err);
});
const active = this.#activeWorkers.get(chatId);
if (active) {
active.markAborted = () => {
wasAborted = true;
};
}
});
if (invocationUuid) await WorkspaceAgentInvocation.close(invocationUuid);
} catch (error) {
this.#activeWorkers.delete(chatId);
if (error.message?.includes("aborted")) return;
this.#log("Chat worker error:", error.message);
await ctx.bot.sendMessage(
chatId,
"Sorry, something went wrong. Please try again."
);
}
}
/**
* Abort any active LLM worker for a given chat.
* @param {number} chatId
* @returns {boolean} True if a worker was aborted, false otherwise.
*/
abortChat(chatId) {
const active = this.#activeWorkers.get(chatId);
if (!active) return false;
const { worker, jobId, bgService, markAborted } = active;
this.#log(`Aborting worker for chat ${chatId} (job: ${jobId})`);
if (markAborted) markAborted();
try {
worker.kill("SIGTERM");
} catch (err) {
this.#log(`Failed to kill worker: ${err.message}`);
}
this.#activeWorkers.delete(chatId);
try {
bgService.bree.remove(jobId).catch(() => {});
} catch {}
return true;
}
#shouldVoiceRespond(isVoiceMessage) {
if (!this.#config) return false;
const mode = this.#config.voice_response_mode || "text_only";
if (mode === "always_voice") return true;
if (mode === "mirror" && isVoiceMessage) return true;
return false;
}
#handleMessage(ctx, msg) {
const chatId = msg.chat.id;
// Voice messages: transcribe then send to LLM
if (msg.voice || msg.audio) {
this.#queue.enqueue(chatId, async () => {
try {
const audioInfo = msg.voice || msg.audio;
const fileId = audioInfo.file_id;
const mimeType = audioInfo.mime_type || "audio/ogg";
await ctx.bot.sendChatAction(chatId, "typing");
const audioBuffer = await downloadTelegramFile(ctx.bot, fileId);
const transcription = await transcribeAudio(audioBuffer, mimeType);
if (!transcription?.trim()) {
await ctx.bot.sendMessage(
chatId,
"Could not transcribe the voice message."
);
return;
}
await this.#runChatJob(ctx, chatId, {
message: transcription,
voiceResponse: this.#shouldVoiceRespond(true),
});
} catch (error) {
this.#log("Voice handling error:", error.message);
const isConfigError =
error.message.includes("transcription") ||
error.message.includes("Whisper") ||
error.message.includes("OpenAI");
await ctx.bot.sendMessage(
chatId,
isConfigError
? error.message
: "Failed to process voice message. Please try again."
);
}
});
return;
}
// Photo messages: extract image and send to LLM with vision
if (msg.photo) {
this.#queue.enqueue(chatId, async () => {
try {
await ctx.bot.sendChatAction(chatId, "typing");
const attachment = await photoToAttachment(ctx.bot, msg.photo);
await this.#runChatJob(ctx, chatId, {
message: msg.caption || "Describe this image.",
attachments: [attachment],
voiceResponse: this.#shouldVoiceRespond(false),
});
} catch (error) {
this.#log("Photo handling error:", error.message);
await ctx.bot.sendMessage(
chatId,
"Failed to process the image. Please try again."
);
}
});
return;
}
// Document messages: parse and send extracted text to LLM
if (msg.document) {
this.#queue.enqueue(chatId, async () => {
try {
await ctx.bot.sendChatAction(chatId, "typing");
const filename = msg.document.file_name || "document";
const docBuffer = await downloadTelegramFile(
ctx.bot,
msg.document.file_id
);
const { text, filename: docName } = await documentToText(
docBuffer,
filename
);
const userPrompt = msg.caption?.trim()
? msg.caption.trim()
: "Summarize this document.";
const fullMessage = `The user has shared a document named "${docName}". Here is the extracted content:\n\n---\n${text}\n---\n\nUser's request: ${userPrompt}`;
await this.#runChatJob(ctx, chatId, {
message: fullMessage,
voiceResponse: this.#shouldVoiceRespond(false),
});
} catch (error) {
this.#log("Document handling error:", error.message);
await ctx.bot.sendMessage(
chatId,
error.message.includes("collector")
? error.message
: "Failed to process the document. Please try again."
);
}
});
return;
}
if (!msg.text) return;
this.#queue.enqueue(chatId, async () => {
await this.#runChatJob(ctx, chatId, {
message: msg.text,
voiceResponse: this.#shouldVoiceRespond(false),
});
});
}
/**
* Verify a bot token with the Telegram API without starting polling.
*/
static async verifyToken(token) {
try {
const bot = new TelegramBot(token, { polling: false });
const me = await bot.getMe();
return { valid: true, username: me.username, error: null };
} catch (error) {
return { valid: false, username: null, error: error.message };
}
}
/**
* Boot the bot from database config on server startup.
* Decrypts the stored bot token before starting.
* If the instance is running in multi-user mode, it will skip boot and delete the connector if it exists.
* @returns {Promise<void>}
*/
static async bootIfActive() {
const service = new TelegramBotService();
try {
const connector = await ExternalCommunicationConnector.get("telegram");
if (!connector || !connector.active || !connector.config?.bot_token)
return;
// If there is a valid config, but the instance is running in multi-user mode - skip boot
// but also cleanup the config and approved users
const isSingleUserMode = await service.#assertSingleUserMode();
if (!isSingleUserMode) return;
const config = { ...connector.config };
config.bot_token = decryptToken(config.bot_token);
if (!config.bot_token) {
service.#log("Failed to decrypt bot token. Re-connect to fix.");
return;
}
await service.start(config);
} catch (error) {
service.#log("Failed to boot:", error.message);
}
}
}
module.exports = { TelegramBotService };

View File

@ -0,0 +1,15 @@
/**
* /abort - Kill any ongoing LLM worker for this chat.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleAbort(ctx, chatId) {
const { TelegramBotService } = require("../../../index");
const service = new TelegramBotService();
const aborted = service.abortChat(chatId);
if (aborted) await ctx.bot.sendMessage(chatId, "Response aborted by user.");
else await ctx.bot.sendMessage(chatId, "No active response to abort.");
}
module.exports = { handleAbort };

View File

@ -0,0 +1,15 @@
/**
* /help - Show all available commands.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleHelp(ctx, chatId) {
const { BOT_COMMANDS } = require("../index");
const lines = BOT_COMMANDS.map((c) => `/${c.command} - ${c.description}`);
await ctx.bot.sendMessage(
chatId,
`Available commands:\n\n${lines.join("\n")}`
);
}
module.exports = { handleHelp };

View File

@ -0,0 +1,80 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
const { WorkspaceChats } = require("../../../../../models/workspaceChats");
const { convertToChatHistory } = require("../../../../helpers/chat/responses");
const { sendBatchedMessages } = require("../../../utils");
const { escapeHTML } = require("../../format");
const DEFAULT_HISTORY_COUNT = 10;
const MAX_HISTORY_COUNT = 50;
/**
* /history [count] - Show recent chat history.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
* @param {string} [messageText] - Full message text to parse count from
*/
async function handleHistory(ctx, chatId, messageText = "") {
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
if (!workspace) {
await ctx.bot.sendMessage(chatId, "No workspace configured.");
return;
}
const match = messageText.match(/\/history\s+(\d+)/);
const count = match
? Math.min(parseInt(match[1], 10), MAX_HISTORY_COUNT)
: DEFAULT_HISTORY_COUNT;
const thread = state.threadSlug
? await WorkspaceThread.get({ slug: state.threadSlug })
: null;
const rawChats = await WorkspaceChats.where(
{
workspaceId: workspace.id,
user_id: null,
thread_id: thread?.id || null,
api_session_id: null,
include: true,
},
count,
{ id: "desc" }
);
if (!rawChats.length) {
await ctx.bot.sendMessage(chatId, "No messages yet in this thread.");
return;
}
const history = convertToChatHistory(rawChats.reverse());
const exchanges = [];
for (let i = 0; i < history.length; i++) {
const entry = history[i];
if (entry.role === "user") {
let block = `<b>You:</b> ${escapeHTML(entry.content || "")}`;
if (i + 1 < history.length && history[i + 1].role === "assistant") {
block += `\n\n<b>AI:</b> ${escapeHTML(history[i + 1].content || "")}`;
i++;
}
exchanges.push(block);
} else if (entry.role === "assistant") {
exchanges.push(`<b>AI:</b> ${escapeHTML(entry.content || "")}`);
}
}
if (!exchanges.length) return;
const threadName = thread?.name || "Default";
const header = `<b>${workspace.name}${threadName}</b>\nLast ${exchanges.length} message(s)\n\n`;
await sendBatchedMessages(ctx.bot, chatId, exchanges, {
header,
separator: "\n\n———\n\n",
sendOptions: { parse_mode: "HTML" },
});
}
module.exports = { handleHistory };

View File

@ -0,0 +1,35 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
/**
* /new - Creates a new thread in the current workspace.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleNewThread(ctx, chatId) {
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
if (!workspace) {
await ctx.bot.sendMessage(chatId, "No workspace configured.");
return;
}
const { thread, message: error } = await WorkspaceThread.new(
workspace,
null,
{ name: "Telegram Thread" }
);
if (error || !thread) {
await ctx.bot.sendMessage(chatId, "Failed to create thread.");
return;
}
ctx.setState(chatId, { threadSlug: thread.slug });
await ctx.bot.sendMessage(
chatId,
`New thread created in "${workspace.name}". Your messages will now go here.`
);
}
module.exports = { handleNewThread };

View File

@ -0,0 +1,202 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
const { WorkspaceChats } = require("../../../../../models/workspaceChats");
const SOURCES_PER_PAGE = 6;
/**
* Check if a source is a web source (identified by link:// prefix in chunkSource).
* @param {object} source
* @returns {boolean}
*/
function isWebSource(source) {
return source?.chunkSource?.startsWith("link://");
}
/**
* Get a display title for a source.
* @param {object} source
* @param {number} index
* @returns {string}
*/
function getSourceTitle(source, index) {
if (source.title) return source.title;
if (source.id) return source.id;
return `Source ${index + 1}`;
}
/**
* Truncate text to a maximum length.
* @param {string} text
* @param {number} maxLength
* @returns {string}
*/
function truncateText(text, maxLength = 30) {
if (!text) return "";
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
}
/**
* Get the last assistant message for the current workspace/thread.
* @param {number} workspaceId
* @param {number|null} threadId
* @returns {Promise<{sources: object[], text: string}|null>}
*/
async function getLastAssistantMessage(workspaceId, threadId) {
const chat = await WorkspaceChats.get(
{
workspaceId,
user_id: null,
thread_id: threadId || null,
api_session_id: null,
include: true,
},
1,
{ id: "desc" }
);
if (!chat) return null;
try {
const response = JSON.parse(chat.response);
return {
sources: response.sources || [],
text: response.text || "",
};
} catch {
return null;
}
}
/**
* Build the sources menu with pagination.
* @param {object[]} sources
* @param {number} page
* @returns {{text: string, buttons: object[][]}}
*/
function buildSourcesMenu(sources, page = 0) {
const totalPages = Math.ceil(sources.length / SOURCES_PER_PAGE);
const safePage = Math.max(0, Math.min(page, totalPages - 1));
const startIdx = safePage * SOURCES_PER_PAGE;
const pageSources = sources.slice(startIdx, startIdx + SOURCES_PER_PAGE);
const buttons = pageSources.map((source, idx) => {
const globalIdx = startIdx + idx;
const isWeb = isWebSource(source);
const emoji = isWeb ? "🌐" : "📄";
const title = truncateText(getSourceTitle(source, globalIdx), 28);
return [
{
text: `${emoji} ${title}`,
callback_data: `src:${globalIdx}`,
},
];
});
const navRow = [];
if (safePage > 0) {
navRow.push({ text: "← Prev", callback_data: `srcpg:${safePage - 1}` });
}
if (safePage < totalPages - 1) {
navRow.push({ text: "Next →", callback_data: `srcpg:${safePage + 1}` });
}
if (navRow.length) buttons.push(navRow);
buttons.push([{ text: "Close", callback_data: "src:close" }]);
const text =
totalPages > 1
? `📚 <b>Citations</b> (${safePage + 1}/${totalPages}, ${sources.length} total)\n\nSelect a source to view:`
: `📚 <b>Citations</b> (${sources.length} source${sources.length > 1 ? "s" : ""})\n\nSelect a source to view:`;
return { text, buttons };
}
/**
* Show the sources menu for the /proof command.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
* @param {number} page
* @param {number|null} messageId - If provided, edits existing message
*/
async function showSourcesMenu(ctx, chatId, page = 0, messageId = null) {
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
if (!workspace) {
await ctx.bot.sendMessage(chatId, "No workspace configured.");
return;
}
const thread = state.threadSlug
? await WorkspaceThread.get({ slug: state.threadSlug })
: null;
const lastMessage = await getLastAssistantMessage(
workspace.id,
thread?.id || null
);
if (!lastMessage) {
const text = "There are no citations for the previous reply.";
if (messageId) {
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
});
} else {
await ctx.bot.sendMessage(chatId, text);
}
return;
}
const { sources } = lastMessage;
if (!sources || sources.length === 0) {
const text = "The previous reply has no citations available.";
if (messageId) {
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
});
} else {
await ctx.bot.sendMessage(chatId, text);
}
return;
}
// Store sources in state for callback handlers to access
ctx.setState(chatId, { _proofSources: sources });
const { text, buttons } = buildSourcesMenu(sources, page);
const opts = {
parse_mode: "HTML",
reply_markup: { inline_keyboard: buttons },
};
if (messageId) {
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
...opts,
});
} else {
await ctx.bot.sendMessage(chatId, text, opts);
}
}
/**
* /proof - Show citations from the previous assistant message.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleProof(ctx, chatId) {
await showSourcesMenu(ctx, chatId, 0, null);
}
module.exports = {
handleProof,
showSourcesMenu,
isWebSource,
getSourceTitle,
SOURCES_PER_PAGE,
};

View File

@ -0,0 +1,32 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
const { WorkspaceChats } = require("../../../../../models/workspaceChats");
/**
* /reset - Clears LLM chat history context.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleReset(ctx, chatId) {
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
if (!workspace) return;
const thread = state.threadSlug
? await WorkspaceThread.get({ slug: state.threadSlug })
: null;
await WorkspaceChats.markThreadHistoryInvalidV2({
workspaceId: workspace.id,
user_id: null,
thread_id: thread?.id || null,
api_session_id: null,
});
await ctx.bot.sendMessage(
chatId,
"Chat history has been cleared for the LLM. Previous messages still appear above but won't be used as context."
);
}
module.exports = { handleReset };

View File

@ -0,0 +1,19 @@
const { Workspace } = require("../../../../../models/workspace");
/**
* /start - Welcome message with current workspace info.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleStart(ctx, chatId) {
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
const name = workspace?.name || state.workspaceSlug;
await ctx.bot.sendMessage(
chatId,
`Welcome to AnythingLLM!\n\nYour messages go to the "${name}" workspace. Use /switch to change workspaces or threads, and /help to see all commands.`
);
}
module.exports = { handleStart };

View File

@ -0,0 +1,80 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
const {
resolveWorkspaceProvider,
sendFormattedMessage,
} = require("../../../utils");
/**
* /status - Show current workspace, thread, and model info.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
*/
async function handleStatus(ctx, chatId) {
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
if (!workspace) {
await ctx.bot.sendMessage(chatId, "No workspace configured.");
return;
}
let threadName = "Default";
if (state.threadSlug) {
const thread = await WorkspaceThread.get({ slug: state.threadSlug });
if (thread) threadName = thread.name;
}
const markdown = [];
markdown.push(`# Workspace:
${workspace.name}
# Thread:
_${threadName}_
--------------------------------`);
const AIbitat = require("../../../../agents/aibitat");
const { provider, model } = resolveWorkspaceProvider(workspace);
const agentConfig = { provider, model };
const agentProvider = new AIbitat(agentConfig).getProviderForConfig(
agentConfig
);
const nativeToolCalling = await agentProvider.supportsNativeToolCalling?.();
markdown.push(`# LLM Provider:
${provider}
# LLM Model:
${model}
# Native Tool Calling:
${nativeToolCalling ? "Enabled" : "Disabled"}
# Chat Mode:
${workspace.chatMode ?? "chat"}`);
if (workspace.chatMode === "automatic" && !nativeToolCalling) {
markdown.unshift(
`<blockquote>**⚠️ Note**\nNative tool calling is unavailable for this provider/model. You can only use tools with the @agent command.</blockquote>`
);
}
if (workspace.chatMode === "chat") {
if (nativeToolCalling) {
markdown.unshift(
`<blockquote>**💡 Tip**\nChange this workspace's chat mode to "automatic" to use tools without the @agent command.</blockquote>`
);
} else {
markdown.unshift(
`<blockquote>**⚠️ Note**\nNative tool calling is unavailable for this provider/model. You can only use tools with the @agent command.</blockquote>`
);
}
}
await sendFormattedMessage(ctx.bot, chatId, markdown.join("\n"), {
format: true,
escapeHtml: false,
});
}
module.exports = { handleStatus };

View File

@ -0,0 +1,98 @@
const { Workspace } = require("../../../../../models/workspace");
const { resolveWorkspaceProvider } = require("../../index");
const {
getCustomModels,
SUPPORT_CUSTOM_MODELS,
} = require("../../../../helpers/customModels");
const MODELS_PER_PAGE = 8;
/**
* Show the model selection inline keyboard with pagination.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
* @param {number} page - Current page (0-indexed)
* @param {number|null} messageId - If provided, edits existing message instead of sending new one
*/
async function showModelMenu(ctx, chatId, page = 0, messageId = null) {
const pageNum = typeof page === "number" && !isNaN(page) ? page : 0;
const state = ctx.getState(chatId);
const workspace = await Workspace.get({ slug: state.workspaceSlug });
if (!workspace) {
await ctx.bot.sendMessage(
chatId,
"No workspace configured. Use /switch to select a workspace."
);
return;
}
const { provider, model: currentModel } = resolveWorkspaceProvider(workspace);
if (!SUPPORT_CUSTOM_MODELS.includes(provider)) {
await ctx.bot.sendMessage(
chatId,
`The "${provider}" provider does not support model selection via API.`
);
return;
}
const { models, error } = await getCustomModels(provider);
if (error || !models?.length) {
await ctx.bot.sendMessage(
chatId,
error || `No models available for "${provider}".`
);
return;
}
const sortedModels = [...models].sort((a, b) => {
const aId = a.id || a.name;
const bId = b.id || b.name;
const aIsActive = aId === currentModel;
const bIsActive = bId === currentModel;
if (aIsActive && !bIsActive) return -1;
if (!aIsActive && bIsActive) return 1;
return 0;
});
const totalPages = Math.ceil(sortedModels.length / MODELS_PER_PAGE);
const safePage = Math.max(0, Math.min(pageNum, totalPages - 1));
const startIdx = safePage * MODELS_PER_PAGE;
const pageModels = sortedModels.slice(startIdx, startIdx + MODELS_PER_PAGE);
const buttons = pageModels.map((m) => {
const modelId = m.id || m.name;
const displayName = m.name || m.id;
const isActive = modelId === currentModel;
return [
{
text: isActive ? `🟢 ${displayName} (active)` : displayName,
callback_data: `mdl:${workspace.id}:${modelId.slice(0, 40)}`,
},
];
});
const navRow = [];
if (safePage > 0) {
navRow.push({ text: "← Prev", callback_data: `mdlpg:${safePage - 1}` });
}
if (safePage < totalPages - 1) {
navRow.push({ text: "Next →", callback_data: `mdlpg:${safePage + 1}` });
}
if (navRow.length) buttons.push(navRow);
buttons.push([{ text: "✕ Cancel", callback_data: "mdl:cancel" }]);
const text = `"${workspace.name}" — Select a model (${safePage + 1}/${totalPages}, ${sortedModels.length} total):`;
const opts = { reply_markup: { inline_keyboard: buttons } };
if (messageId) {
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
...opts,
});
} else {
await ctx.bot.sendMessage(chatId, text, opts);
}
}
module.exports = { showModelMenu };

View File

@ -0,0 +1,120 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceChats } = require("../../../../../models/workspaceChats");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
const THREADS_PER_PAGE = 8;
/**
* Show the thread selection inline keyboard for a workspace with pagination.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
* @param {number} workspaceId - must be ID, not slug due to 64-byte limit on callback data
* @param {number} page - Current page (0-indexed)
* @param {number|null} messageId
*/
async function showThreadMenu(
ctx,
chatId,
workspaceId,
page = 0,
messageId = null
) {
const pageNum = typeof page === "number" && !isNaN(page) ? page : 0;
const workspace = await Workspace.get({ id: workspaceId });
if (!workspace) return;
const threads = await WorkspaceThread.where({
workspace_id: workspace.id,
});
const state = ctx.getState(chatId);
const allItems = [];
const defaultThreadChatCount = await WorkspaceChats.count({
workspaceId: workspace.id,
thread_id: null,
});
const isDefaultActive = !state.threadSlug;
let defaultThreadText = isDefaultActive ? "🟢 Default (active)" : "Default";
if (defaultThreadChatCount > 0)
defaultThreadText += ` - ${defaultThreadChatCount} chats`;
allItems.push({
text: defaultThreadText,
callback_data: `th:${workspace.id}:0`,
isActive: isDefaultActive,
});
for (const thread of threads) {
const threadChatCount = await WorkspaceChats.count({
workspaceId: workspace.id,
thread_id: thread.id,
});
const isCurrent = thread.slug === state.threadSlug;
let threadText = isCurrent ? `🟢 ${thread.name} (active)` : thread.name;
if (threadChatCount > 0) threadText += ` - ${threadChatCount} chats`;
allItems.push({
text: threadText,
callback_data: `th:${workspace.id}:${thread.id}`,
isActive: isCurrent,
});
}
allItems.sort((a, b) => {
if (a.isActive && !b.isActive) return -1;
if (!a.isActive && b.isActive) return 1;
return 0;
});
const totalPages = Math.ceil(allItems.length / THREADS_PER_PAGE);
const safePage = Math.max(0, Math.min(pageNum, totalPages - 1));
const startIdx = safePage * THREADS_PER_PAGE;
const pageItems = allItems.slice(startIdx, startIdx + THREADS_PER_PAGE);
const buttons = pageItems.map((item) => [
{ text: item.text, callback_data: item.callback_data },
]);
const navRow = [];
if (safePage > 0) {
navRow.push({
text: "← Prev",
callback_data: `thpg:${workspaceId}:${safePage - 1}`,
});
}
if (safePage < totalPages - 1) {
navRow.push({
text: "Next →",
callback_data: `thpg:${workspaceId}:${safePage + 1}`,
});
}
if (navRow.length) buttons.push(navRow);
buttons.push([
{
text: "← Back to workspaces",
callback_data: "back:workspaces",
},
]);
const text =
totalPages > 1
? `"${workspace.name}" — Select a thread (${safePage + 1}/${totalPages}, ${allItems.length} total):`
: `"${workspace.name}" — Select a thread:`;
const opts = {
reply_markup: { inline_keyboard: buttons },
};
if (messageId) {
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
...opts,
});
} else {
await ctx.bot.sendMessage(chatId, text, opts);
}
}
module.exports = { showThreadMenu };

View File

@ -0,0 +1,84 @@
const { Workspace } = require("../../../../../models/workspace");
const WORKSPACES_PER_PAGE = 8;
/**
* Show the workspace selection inline keyboard with pagination.
* @param {import("../index").BotContext} ctx
* @param {number} chatId
* @param {number} page - Current page (0-indexed)
* @param {number|null} messageId - If provided, edits existing message instead of sending new one
*/
async function showWorkspaceMenu(ctx, chatId, page = 0, messageId = null) {
const pageNum = typeof page === "number" && !isNaN(page) ? page : 0;
const workspaces = await Workspace.where({});
if (!workspaces.length) {
await ctx.bot.sendMessage(
chatId,
"No workspaces found. Create one to get started!",
{
reply_markup: {
inline_keyboard: [
[{ text: " Create Workspace", callback_data: "ws-create" }],
],
},
}
);
return;
}
const state = ctx.getState(chatId);
const sortedWorkspaces = [...workspaces].sort((a, b) => {
const aIsActive = a.slug === state.workspaceSlug;
const bIsActive = b.slug === state.workspaceSlug;
if (aIsActive && !bIsActive) return -1;
if (!aIsActive && bIsActive) return 1;
return 0;
});
const totalPages = Math.ceil(sortedWorkspaces.length / WORKSPACES_PER_PAGE);
const safePage = Math.max(0, Math.min(pageNum, totalPages - 1));
const startIdx = safePage * WORKSPACES_PER_PAGE;
const pageWorkspaces = sortedWorkspaces.slice(
startIdx,
startIdx + WORKSPACES_PER_PAGE
);
const buttons = pageWorkspaces.map((ws) => {
const isCurrent = ws.slug === state.workspaceSlug;
return [
{
text: isCurrent ? `🟢 ${ws.name} (active)` : ws.name,
callback_data: `ws:${ws.id}`,
},
];
});
const navRow = [];
if (safePage > 0) {
navRow.push({ text: "← Prev", callback_data: `wspg:${safePage - 1}` });
}
if (safePage < totalPages - 1) {
navRow.push({ text: "Next →", callback_data: `wspg:${safePage + 1}` });
}
if (navRow.length) buttons.push(navRow);
const text =
totalPages > 1
? `Select a workspace (${safePage + 1}/${totalPages}, ${sortedWorkspaces.length} total):`
: "Select a workspace:";
const opts = {
reply_markup: { inline_keyboard: buttons },
};
if (messageId) {
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
...opts,
});
} else {
await ctx.bot.sendMessage(chatId, text, opts);
}
}
module.exports = { showWorkspaceMenu };

View File

@ -0,0 +1,125 @@
/**
* All command handler functions receive a `ctx` object:
* @typedef {object} BotContext
* @property {import('node-telegram-bot-api')} bot - The bot object.
* @property {object} config - The bot configuration.
* @property {(chatId: number) => { workspaceSlug: string, threadSlug: string | null }} getState - Get state for a chat.
* @property {(chatId: number, updates: object) => void} setState - Update state for a chat.
* @property {(text: string, ...args: any[]) => void} log - Log a message.
*/
/**
* @typedef {object} BotCommandConfig
* @property {string} command - The command name.
* @property {string} description - The command description.
* @property {boolean} skipAutoSetup - Whether to skip automatic setup.
* @property {() => (ctx: BotContext, chatId: number, messageText?: string) => Promise<void>} initHandler - The handler function to initialize the command.
*/
const BASE_COMMAND = {
skipAutoSetup: false,
initHandler: () => {
throw new Error("Not implemented");
},
};
/**
* @type {BotCommandConfig[]}
*/
const BOT_COMMANDS = [
{
...BASE_COMMAND,
command: "start",
description: "Start the bot",
initHandler: () => {
const { handleStart } = require("./handlers/handleStart");
return handleStart;
},
},
{
...BASE_COMMAND,
command: "switch",
description: "Switch workspace or thread",
initHandler: () => {
const { showWorkspaceMenu } = require("./handlers/showWorkspaceMenu");
return showWorkspaceMenu;
},
},
{
...BASE_COMMAND,
command: "model",
description: "Change the LLM model",
initHandler: () => {
const { showModelMenu } = require("./handlers/showModelMenu");
return showModelMenu;
},
},
{
...BASE_COMMAND,
command: "new",
description: "Start a new thread",
initHandler: () => {
const { handleNewThread } = require("./handlers/handleNewThread");
return handleNewThread;
},
},
{
...BASE_COMMAND,
command: "history",
description: "Show recent messages (e.g. /history 25)",
skipAutoSetup: true,
initHandler: () => {
const { handleHistory } = require("./handlers/handleHistory");
return handleHistory;
},
},
{
...BASE_COMMAND,
command: "status",
description: "Show current workspace and model",
initHandler: () => {
const { handleStatus } = require("./handlers/handleStatus");
return handleStatus;
},
},
{
...BASE_COMMAND,
command: "reset",
description: "Clear chat history in current thread",
initHandler: () => {
const { handleReset } = require("./handlers/handleReset");
return handleReset;
},
},
{
...BASE_COMMAND,
command: "help",
description: "Show available commands",
initHandler: () => {
const { handleHelp } = require("./handlers/handleHelp");
return handleHelp;
},
},
{
...BASE_COMMAND,
command: "proof",
description: "Show citations for the last reply",
initHandler: () => {
const { handleProof } = require("./handlers/handleProof");
return handleProof;
},
},
{
...BASE_COMMAND,
command: "abort",
description: "Stop the current response",
initHandler: () => {
const { handleAbort } = require("./handlers/handleAbort");
return handleAbort;
},
},
];
module.exports = {
BOT_COMMANDS,
};

View File

@ -0,0 +1,180 @@
/**
* Convert standard markdown to Telegram HTML format.
* Telegram HTML supports: <b>, <i>, <u>, <s>, <code>, <pre>, <a href="">, <tg-spoiler>
*
* @param {string} text - The markdown text to convert
* @param {object} [opts]
* @param {boolean} [opts.escapeHtml=true] - Whether to escape HTML in non-code text
* @returns {string} - HTML formatted text for Telegram
*/
function markdownToTelegram(text, { escapeHtml = true } = {}) {
if (!text) return "";
let result = text;
// Use null char placeholders that won't be affected by other transformations
const codeBlocks = [];
const inlineCode = [];
const thinkBlocks = [];
// Handle <think> blocks - including partial tags from split messages
// Process complete blocks first, then handle partials
// First: complete <think>...</think> blocks
result = result.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
const placeholder = `\x00THINKBLOCK${thinkBlocks.length}\x00`;
const trimmed = content.trim();
const tag = trimmed.length > 200 ? "blockquote expandable" : "blockquote";
thinkBlocks.push(
`<${tag}>💭 <b>Thinking:</b>\n${escapeHTML(trimmed)}</blockquote>`
);
return placeholder;
});
// Second: unclosed <think> tag (split message part 1)
if (result.includes("<think>")) {
result = result.replace(/<think>([\s\S]*)$/, (_, content) => {
const placeholder = `\x00THINKBLOCK${thinkBlocks.length}\x00`;
const trimmed = content.trim();
const tag = trimmed.length > 200 ? "blockquote expandable" : "blockquote";
thinkBlocks.push(
`<${tag}>💭 <b>Thinking:</b>\n${escapeHTML(trimmed)}</blockquote>`
);
return placeholder;
});
}
// Third: closing </think> without open (split message part 2)
if (result.includes("</think>")) {
result = result.replace(/([\s\S]*?)<\/think>/g, (_, content) => {
const placeholder = `\x00THINKBLOCK${thinkBlocks.length}\x00`;
const trimmed = content.trim();
const tag = trimmed.length > 200 ? "blockquote expandable" : "blockquote";
thinkBlocks.push(
`<${tag}>💭 <b>Thinking continued:</b>\n${escapeHTML(trimmed)}</blockquote>`
);
return placeholder;
});
}
// Extract fenced code blocks (```...```)
result = result.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
const placeholder = `\x00CODEBLOCK${codeBlocks.length}\x00`;
codeBlocks.push(`<pre>${escapeHTML(code.trimEnd())}</pre>`);
return placeholder;
});
// Extract and convert markdown tables to preformatted text
result = result.replace(
/(?:^|\n)((?:\|[^\n]+\|(?:\n|$))+)/g,
(match, tableContent) => {
const converted = convertTableToPreformatted(tableContent);
const placeholder = `\x00CODEBLOCK${codeBlocks.length}\x00`;
codeBlocks.push(`<pre>${escapeHTML(converted)}</pre>`);
return `\n${placeholder}`;
}
);
// Extract inline code (`...`)
result = result.replace(/`([^`\n]+)`/g, (_, code) => {
const placeholder = `\x00INLINECODE${inlineCode.length}\x00`;
inlineCode.push(`<code>${escapeHTML(code)}</code>`);
return placeholder;
});
// Escape HTML in remaining text
if (escapeHtml) result = escapeHTML(result);
// Convert markdown to HTML (order matters - do bold before italic)
result = result.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
result = result.replace(/__(.+?)__/g, "<b>$1</b>");
result = result.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
result = result.replace(
/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g,
"<i>$1</i>"
);
result = result.replace(/~~(.+?)~~/g, "<s>$1</s>");
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
result = result.replace(/^&gt;\s*(.+)$/gm, "<i>$1</i>");
result = result.replace(/^[-*_]{3,}$/gm, "————————————");
result = result.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
// Convert list items: - item or * item → • item
result = result.replace(/^[-*]\s+/gm, "• ");
// Restore preserved blocks
thinkBlocks.forEach((block, i) => {
result = result.replace(`\x00THINKBLOCK${i}\x00`, block);
});
codeBlocks.forEach((block, i) => {
result = result.replace(`\x00CODEBLOCK${i}\x00`, block);
});
inlineCode.forEach((code, i) => {
result = result.replace(`\x00INLINECODE${i}\x00`, code);
});
return result;
}
/**
* Escape HTML special characters.
* @param {string} text
* @returns {string}
*/
function escapeHTML(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
/**
* Convert a markdown table to aligned preformatted text.
* @param {string} tableMarkdown
* @returns {string}
*/
function convertTableToPreformatted(tableMarkdown) {
const lines = tableMarkdown.trim().split("\n");
const rows = [];
for (const line of lines) {
const trimmed = line.trim();
const withoutPipes = trimmed.replace(/\|/g, "").trim();
if (/^[\s\-:]+$/.test(withoutPipes) && withoutPipes.includes("-")) continue;
const cells = line
.split("|")
.slice(1, -1)
.map((cell) => cell.trim());
if (cells.length > 0) rows.push(cells);
}
if (rows.length === 0) return tableMarkdown;
const colCount = Math.max(...rows.map((r) => r.length));
const colWidths = Array(colCount).fill(0);
for (const row of rows) {
for (let i = 0; i < row.length; i++) {
colWidths[i] = Math.max(colWidths[i], row[i].length);
}
}
const output = [];
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
const row = rows[rowIdx];
const paddedCells = row.map((cell, i) => cell.padEnd(colWidths[i]));
output.push(paddedCells.join(" │ "));
if (rowIdx === 0) {
output.push(colWidths.map((w) => "─".repeat(w)).join("─┼─"));
}
}
return output.join("\n");
}
module.exports = {
escapeHTML,
markdownToTelegram,
};

View File

@ -0,0 +1,203 @@
const { MAX_MSG_LEN } = require("../constants");
const { markdownToTelegram } = require("../utils/format");
const { EncryptionManager } = require("../../EncryptionManager");
const ENCRYPTED_PREFIX = "enc:";
/**
* Edit a Telegram message with truncation to stay under the 4096 char limit.
* @param {import("../commands").BotContext} ctx
* @param {number} chatId
* @param {number} messageId
* @param {string} text
* @param {function} log
* @param {object} [opts]
* @param {boolean} [opts.format=false] - Whether to format markdown as HTML
*/
async function editMessage(bot, chatId, messageId, text, log, opts = {}) {
if (!text || !bot) return;
const { format = false, html = false, disableLinkPreview = false } = opts;
let finalText = text;
let parseMode = undefined;
if (html) {
parseMode = "HTML";
} else if (format) {
try {
finalText = markdownToTelegram(text);
parseMode = "HTML";
} catch {
finalText = text;
}
}
const truncated =
finalText.length > 4096 ? finalText.slice(0, 4090) + "\n..." : finalText;
try {
await bot.editMessageText(truncated, {
chat_id: chatId,
message_id: messageId,
parse_mode: parseMode,
disable_web_page_preview: disableLinkPreview || undefined,
});
} catch (error) {
if (!error.message?.includes("message is not modified")) {
log("Edit error:", error.message);
}
// If HTML parsing failed, retry without formatting
if (parseMode && error.message?.includes("parse")) {
try {
const plainTruncated =
text.length > 4096 ? text.slice(0, 4090) + "\n..." : text;
await bot.editMessageText(plainTruncated, {
chat_id: chatId,
message_id: messageId,
disable_web_page_preview: disableLinkPreview || undefined,
});
} catch {}
}
}
}
/**
* Send a formatted message to Telegram with markdown converted to HTML.
* Falls back to plain text if HTML parsing fails.
* @param {TelegramBot} bot
* @param {number} chatId
* @param {string} text
* @param {object} [opts]
* @param {boolean} [opts.format=true] - Whether to format markdown as HTML
* @param {boolean} [opts.escapeHtml=true] - Whether to escape HTML tags in non-code text (unsafe - use only with fixed input)
* @returns {Promise<object>} The sent message object
*/
async function sendFormattedMessage(bot, chatId, text, opts = {}) {
const { format = true, escapeHtml = true } = opts;
if (!format) {
return bot.sendMessage(chatId, text);
}
try {
const formatted = markdownToTelegram(text, { escapeHtml });
return await bot.sendMessage(chatId, formatted, { parse_mode: "HTML" });
} catch (error) {
// If HTML parsing failed, retry without formatting
if (error.message?.includes("parse") || error.message?.includes("can't")) {
return bot.sendMessage(chatId, text);
}
throw error;
}
}
/**
* Send a list of text blocks as batched Telegram messages that
* stay under the 4096 char limit. Blocks are joined with the
* given separator and split into new messages when they'd overflow.
* @param {TelegramBot} bot
* @param {number} chatId
* @param {string[]} blocks - individual text blocks to send
* @param {object} [opts]
* @param {string} [opts.header] - text prepended to the first message
* @param {string} [opts.separator] - string between blocks (default "\n\n")
* @param {object} [opts.sendOptions] - extra options passed to sendMessage (e.g. parse_mode)
*/
async function sendBatchedMessages(bot, chatId, blocks, opts = {}) {
const { header = "", separator = "\n\n", sendOptions = {} } = opts;
if (!blocks.length) return;
let currentMsg = header;
for (let i = 0; i < blocks.length; i++) {
const addition = (i === 0 ? "" : separator) + blocks[i];
if (currentMsg.length + addition.length > MAX_MSG_LEN) {
await bot.sendMessage(chatId, currentMsg.trim(), sendOptions);
currentMsg = blocks[i];
} else {
currentMsg += addition;
}
}
if (currentMsg.trim()) {
await bot.sendMessage(chatId, currentMsg.trim(), sendOptions);
}
}
/**
* Encrypt a bot token for safe storage in the database.
* @param {string} token
* @returns {string|null}
*/
function encryptToken(token) {
if (!token) return null;
const manager = new EncryptionManager();
const encrypted = manager.encrypt(token);
return encrypted ? ENCRYPTED_PREFIX + encrypted : null;
}
/**
* Decrypt an encrypted bot token from the database.
* Returns plaintext tokens as-is for backward compatibility.
* @param {string} encryptedToken
* @returns {string|null}
*/
function decryptToken(encryptedToken) {
if (!encryptedToken) return null;
if (!encryptedToken.startsWith(ENCRYPTED_PREFIX)) return encryptedToken;
const manager = new EncryptionManager();
return manager.decrypt(encryptedToken.slice(ENCRYPTED_PREFIX.length));
}
/**
* Resolve the LLM provider for a workspace.
* @param {object} workspace
* @returns {{ provider: string, model: string }}
*/
function resolveWorkspaceProvider(workspace) {
const { getBaseLLMProviderModel } = require("../../helpers");
const provider =
workspace?.agentProvider ??
workspace?.chatProvider ??
process.env.LLM_PROVIDER;
const model =
workspace?.agentModel ??
workspace?.chatModel ??
getBaseLLMProviderModel({ provider });
return { provider, model };
}
/**
* Send a new message or edit an existing one (upsert pattern).
* @param {import('../commands').BotContext} ctx
* @param {number} chatId
* @param {number|null} msgId - Existing message ID, or null to send new
* @param {string} text
* @param {object} [log]
* @param {object} [opts]
* @param {boolean} [opts.html=false] - Whether text is pre-formatted HTML
* @returns {Promise<number>} The message ID (new or existing)
*/
async function upsertMessage(bot, chatId, msgId, text, log, opts = {}) {
const { html = false, disableLinkPreview = false } = opts;
if (!msgId) {
const sent = await bot.sendMessage(chatId, text, {
parse_mode: html ? "HTML" : undefined,
disable_web_page_preview: disableLinkPreview || undefined,
});
return sent.message_id;
}
await editMessage(bot, chatId, msgId, text, log, opts);
return msgId;
}
module.exports = {
editMessage,
upsertMessage,
sendBatchedMessages,
sendFormattedMessage,
encryptToken,
decryptToken,
resolveWorkspaceProvider,
};

View File

@ -0,0 +1,163 @@
/**
* Download a file from Telegram by file ID.
* @param {TelegramBot} bot
* @param {string} fileId
* @returns {Promise<Buffer>}
*/
async function downloadTelegramFile(bot, fileId) {
const fileLink = await bot.getFileLink(fileId);
const response = await fetch(fileLink);
if (!response.ok) throw new Error("Failed to download file from Telegram");
return Buffer.from(await response.arrayBuffer());
}
/**
* Get appropriate file extension from MIME type.
* @param {string} mimeType
* @returns {string}
*/
function getExtensionFromMime(mimeType) {
const mimeToExt = {
"audio/ogg": ".ogg",
"audio/oga": ".ogg",
"audio/opus": ".opus",
"audio/mp3": ".mp3",
"audio/mpeg": ".mp3",
"audio/wav": ".wav",
"audio/x-wav": ".wav",
"audio/mp4": ".m4a",
"audio/m4a": ".m4a",
"audio/webm": ".webm",
"audio/flac": ".flac",
};
return mimeToExt[mimeType] || ".ogg"; // Default to .ogg for unknown (common for Telegram mobile)
}
/**
* Transcribe an audio buffer using the configured whisper provider.
* Writes the audio to the collector hotdir and runs it through the
* same parse pipeline used for document processing.
* @param {Buffer} audioBuffer
* @param {string} [mimeType] - The MIME type of the audio (e.g., "audio/ogg")
* @returns {Promise<string>}
*/
async function transcribeAudio(audioBuffer, mimeType = "audio/ogg") {
const fs = require("fs");
const path = require("path");
const { CollectorApi } = require("../../collectorApi");
const { hotdirPath } = require("../../files");
if (!fs.existsSync(hotdirPath)) fs.mkdirSync(hotdirPath, { recursive: true });
const ext = getExtensionFromMime(mimeType);
const filename = `telegram-voice-${Date.now()}${ext}`;
fs.writeFileSync(path.join(hotdirPath, filename), audioBuffer);
const collector = new CollectorApi();
const result = await collector.parseDocument(filename);
if (!result?.success || !result.documents?.length) {
throw new Error(result?.reason || "Failed to transcribe audio.");
}
return result.documents[0].pageContent;
}
/**
* Parse a document buffer and extract its text content.
* Writes the document to the collector hotdir and runs it through
* the collector's parse pipeline.
* @param {Buffer} documentBuffer
* @param {string} originalFilename - The original filename with extension
* @returns {Promise<{text: string, filename: string}>}
*/
async function documentToText(documentBuffer, originalFilename) {
const fs = require("fs");
const path = require("path");
const { CollectorApi } = require("../../collectorApi");
const { hotdirPath } = require("../../files");
if (!fs.existsSync(hotdirPath)) fs.mkdirSync(hotdirPath, { recursive: true });
const sanitizedName = originalFilename.replace(/[^a-zA-Z0-9._-]/g, "_");
const filename = `telegram-doc-${Date.now()}-${sanitizedName}`;
fs.writeFileSync(path.join(hotdirPath, filename), documentBuffer);
const collector = new CollectorApi();
if (!(await collector.online())) {
throw new Error(
"Document processing is unavailable. The collector service is offline."
);
}
const result = await collector.parseDocument(filename);
if (!result?.success || !result.documents?.length) {
throw new Error(
result?.reason || `Failed to parse document: ${originalFilename}`
);
}
const text = result.documents.map((doc) => doc.pageContent).join("\n\n");
return { text, filename: originalFilename };
}
/**
* Download the largest photo from a Telegram photo array and return
* it as an attachment object compatible with the LLM chat pipeline.
* @param {TelegramBot} bot
* @param {Array} photos - Telegram PhotoSize array (ascending size)
* @returns {Promise<{name: string, mime: string, contentString: string}>}
*/
async function photoToAttachment(bot, photos) {
const largest = photos[photos.length - 1];
const buffer = await downloadTelegramFile(bot, largest.file_id);
const base64 = buffer.toString("base64");
return {
name: "telegram-photo.jpg",
mime: "image/jpeg",
contentString: `data:image/jpeg;base64,${base64}`,
};
}
/**
* Convert text to speech and send as an audio message in Telegram.
* Silently does nothing if TTS is not configured.
* @param {TelegramBot} bot
* @param {number} chatId
* @param {string} text
*/
/**
* @returns {Promise<boolean>} true if voice was sent, false if TTS failed
*/
async function sendVoiceResponse(bot, chatId, text) {
try {
const { getTTSProvider } = require("../../TextToSpeech");
const provider = getTTSProvider();
const buffer = await provider.ttsBuffer(text);
if (!buffer) return false;
await bot.sendAudio(
chatId,
buffer,
{},
{
filename: `${chatId}-response.mp3`,
contentType: "audio/mpeg",
}
);
return true;
} catch {
await bot
.sendMessage(
chatId,
"Voice responses require a text-to-speech provider. Set one up in Settings > Voice & Speech > Text-to-Speech Preference."
)
.catch(() => {});
return false;
}
}
module.exports = {
downloadTelegramFile,
transcribeAudio,
documentToText,
photoToAttachment,
sendVoiceResponse,
};

View File

@ -0,0 +1,53 @@
/**
* A simple per-key async message queue that ensures sequential processing.
* Reusable across any connector (Telegram, Discord, Slack, etc.)
* where concurrent messages from the same source must be processed in order.
*
* Usage:
* const queue = new MessageQueue();
* queue.enqueue(userId, async () => { ... });
*/
class MessageQueue {
#chains = new Map();
/**
* Enqueue an async handler to run after all prior handlers for this key complete.
* Different keys run in parallel; same key runs sequentially.
* @param {string|number} key - Unique identifier (e.g. chat ID, user ID)
* @param {() => Promise<void>} handler - Async function to execute
* @returns {Promise<void>}
*/
enqueue(key, handler) {
const prev = this.#chains.get(key) || Promise.resolve();
const chain = prev
.then(handler)
.catch((err) =>
console.error(`[MessageQueue] Error processing key ${key}:`, err)
)
.finally(() => {
// Clean up if this is the last item in the chain
if (this.#chains.get(key) === chain) {
this.#chains.delete(key);
}
});
this.#chains.set(key, chain);
return chain;
}
/**
* Clear all pending chains. Does not cancel in-flight work.
*/
clear() {
this.#chains.clear();
}
/**
* Number of active keys being processed.
* @returns {number}
*/
get size() {
return this.#chains.size;
}
}
module.exports = { MessageQueue };

View File

@ -0,0 +1,16 @@
const { showSourcesMenu } = require("../../commands/handlers/handleProof");
/**
* Handle back to sources - returns to the sources menu.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
*/
async function handleBackSources({ ctx, chatId, query, messageId } = {}) {
await showSourcesMenu(ctx, chatId, 0, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleBackSources };

View File

@ -0,0 +1,18 @@
const {
showWorkspaceMenu,
} = require("../../commands/handlers/showWorkspaceMenu");
/**
* Handle back navigation to workspace menu.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
*/
async function handleBackWorkspaces({ ctx, chatId, query, messageId } = {}) {
await showWorkspaceMenu(ctx, chatId, 0, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleBackWorkspaces };

View File

@ -0,0 +1,14 @@
/**
* Handle model selection cancellation.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
*/
async function handleModelCancel({ ctx, chatId, query, messageId } = {}) {
await ctx.bot.deleteMessage(chatId, messageId);
await ctx.bot.answerCallbackQuery(query.id, { text: "Cancelled" });
}
module.exports = { handleModelCancel };

View File

@ -0,0 +1,24 @@
const { showModelMenu } = require("../../commands/handlers/showModelMenu");
/**
* Handle model menu pagination.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleModelPagination({
ctx,
chatId,
query,
messageId,
data,
} = {}) {
const page = parseInt(data.slice(6), 10);
await showModelMenu(ctx, chatId, page, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleModelPagination };

View File

@ -0,0 +1,52 @@
const { Workspace } = require("../../../../../models/workspace");
const { getCustomModels } = require("../../../../helpers/customModels");
const { resolveWorkspaceProvider } = require("../../index");
/**
* Handle model selection for a workspace.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleModelSelect({ ctx, chatId, query, messageId, data } = {}) {
const parts = data.slice(4).split(":");
const workspaceId = parseInt(parts[0], 10);
const modelIdPrefix = parts.slice(1).join(":");
const workspace = await Workspace.get({ id: workspaceId });
if (!workspace) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "Workspace not found.",
});
return;
}
const { provider } = resolveWorkspaceProvider(workspace);
const { models } = await getCustomModels(provider);
const selectedModel = models?.find((m) => {
const id = m.id || m.name;
return id === modelIdPrefix || id.startsWith(modelIdPrefix);
});
if (!selectedModel) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "Model not found.",
});
return;
}
const modelId = selectedModel.id || selectedModel.name;
await Workspace.update(workspace.id, { chatModel: modelId });
await ctx.bot.answerCallbackQuery(query.id, { text: "Model updated!" });
await ctx.bot.deleteMessage(chatId, messageId);
await ctx.bot.sendMessage(
chatId,
`Model changed to "${selectedModel.name || modelId}" in "${workspace.name}".`
);
}
module.exports = { handleModelSelect };

View File

@ -0,0 +1,24 @@
const { showSourcesMenu } = require("../../commands/handlers/handleProof");
/**
* Handle source pagination - navigates between source pages.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleSourcePagination({
ctx,
chatId,
query,
messageId,
data,
} = {}) {
const page = parseInt(data.slice(6), 10);
await showSourcesMenu(ctx, chatId, page, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleSourcePagination };

View File

@ -0,0 +1,106 @@
const { escapeHTML } = require("../../format");
const {
isWebSource,
getSourceTitle,
} = require("../../commands/handlers/handleProof");
/**
* Truncate source text for display.
* @param {string} text
* @param {number} maxLength
* @returns {string}
*/
function truncateSourceText(text, maxLength = 3500) {
if (!text) return "(No content available)";
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 20) + "\n\n[...truncated]";
}
/**
* Extract URL from a web source.
* @param {object} source
* @returns {string|null}
*/
function extractWebUrl(source) {
if (!source?.chunkSource?.startsWith("link://")) return null;
return source.chunkSource.replace("link://", "");
}
/**
* Handle source selection - shows source content or prompts to open web link.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleSourceSelect({
ctx,
chatId,
query,
messageId,
data,
} = {}) {
// Handle close action
if (data === "src:close") {
await ctx.bot.deleteMessage(chatId, messageId);
await ctx.bot.answerCallbackQuery(query.id);
return;
}
const sourceIdx = parseInt(data.slice(4), 10);
const state = ctx.getState(chatId);
const sources = state._proofSources;
if (!sources || sourceIdx < 0 || sourceIdx >= sources.length) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "Source not found. Please try /proof again.",
});
return;
}
const source = sources[sourceIdx];
const title = getSourceTitle(source, sourceIdx);
if (isWebSource(source)) {
const url = extractWebUrl(source);
if (!url) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "Invalid web source URL.",
});
return;
}
const text = `🌐 <b>${escapeHTML(title)}</b>\n\nOpen this website:`;
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [
[{ text: "🔗 Open Website", url }],
[{ text: "← Back to Sources", callback_data: "src:back" }],
],
},
});
} else {
const content = truncateSourceText(source.text);
const text = `📄 <b>${escapeHTML(title)}</b>\n\n<blockquote expandable>${escapeHTML(content)}</blockquote>`;
await ctx.bot.editMessageText(text, {
chat_id: chatId,
message_id: messageId,
parse_mode: "HTML",
reply_markup: {
inline_keyboard: [
[{ text: "← Back to Sources", callback_data: "src:back" }],
],
},
});
}
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleSourceSelect };

View File

@ -0,0 +1,26 @@
const { showThreadMenu } = require("../../commands/handlers/showThreadMenu");
/**
* Handle thread menu pagination.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleThreadPagination({
ctx,
chatId,
query,
messageId,
data,
} = {}) {
const parts = data.slice(5).split(":");
const workspaceId = parseInt(parts[0], 10);
const page = parseInt(parts[1], 10);
await showThreadMenu(ctx, chatId, workspaceId, page, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleThreadPagination };

View File

@ -0,0 +1,43 @@
const { Workspace } = require("../../../../../models/workspace");
const { WorkspaceThread } = require("../../../../../models/workspaceThread");
/**
* Handle thread selection - sets active workspace and thread.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {string} params.data
*/
async function handleThreadSelect({ ctx, chatId, query, data } = {}) {
const parts = data.slice(3).split(":");
const workspaceId = parseInt(parts[0], 10);
const threadId = parseInt(parts[1], 10);
const workspace = await Workspace.get({ id: workspaceId });
if (!workspace) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "Workspace not found.",
});
return;
}
let threadSlug = null;
let threadName = "Default";
if (threadId !== 0) {
const thread = await WorkspaceThread.get({ id: threadId });
if (thread) {
threadSlug = thread.slug;
threadName = thread.name;
}
}
ctx.setState(chatId, { workspaceSlug: workspace.slug, threadSlug });
await ctx.bot.answerCallbackQuery(query.id, { text: "Switched!" });
await ctx.bot.sendMessage(
chatId,
`Switched to "${workspace.name}" → ${threadName}`
);
}
module.exports = { handleThreadSelect };

View File

@ -0,0 +1,33 @@
const { Workspace } = require("../../../../../models/workspace");
/**
* Handle the creation of a new workspace.
* @param {object} params - The parameters for the function
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query - Telegram callback query object
*/
async function handleWorkspaceCreate({ ctx, chatId, query } = {}) {
const botName = ctx.config.bot_username || "Bot";
const wsName = `${botName} Workspace`;
const { workspace, message: error } = await Workspace.new(wsName, null, {
chatMode: "automatic",
});
if (error || !workspace) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "Failed to create workspace.",
});
return;
}
ctx.setState(chatId, { workspaceSlug: workspace.slug, threadSlug: null });
await ctx.bot.answerCallbackQuery(query.id, {
text: "Workspace created!",
});
await ctx.bot.sendMessage(
chatId,
`Created and switched to "${workspace.name}". You can start chatting now!`
);
}
module.exports = { handleWorkspaceCreate };

View File

@ -0,0 +1,26 @@
const {
showWorkspaceMenu,
} = require("../../commands/handlers/showWorkspaceMenu");
/**
* Handle workspace menu pagination.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleWorkspacePagination({
ctx,
chatId,
query,
messageId,
data,
} = {}) {
const page = parseInt(data.slice(5), 10);
await showWorkspaceMenu(ctx, chatId, page, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleWorkspacePagination };

View File

@ -0,0 +1,24 @@
const { showThreadMenu } = require("../../commands/handlers/showThreadMenu");
/**
* Handle workspace selection - shows thread menu for selected workspace.
* @param {object} params
* @param {import("../../commands/index").BotContext} params.ctx
* @param {number} params.chatId
* @param {{id: string}} params.query
* @param {number} params.messageId
* @param {string} params.data
*/
async function handleWorkspaceSelect({
ctx,
chatId,
query,
messageId,
data,
} = {}) {
const workspaceId = parseInt(data.slice(3), 10);
await showThreadMenu(ctx, chatId, workspaceId, 0, messageId);
await ctx.bot.answerCallbackQuery(query.id);
}
module.exports = { handleWorkspaceSelect };

View File

@ -0,0 +1,57 @@
const { handleWorkspaceCreate } = require("./handleWorkspaceCreate");
const { handleWorkspacePagination } = require("./handleWorkspacePagination");
const { handleWorkspaceSelect } = require("./handleWorkspaceSelect");
const { handleThreadPagination } = require("./handleThreadPagination");
const { handleThreadSelect } = require("./handleThreadSelect");
const { handleBackWorkspaces } = require("./handleBackWorkspaces");
const { handleModelPagination } = require("./handleModelPagination");
const { handleModelCancel } = require("./handleModelCancel");
const { handleModelSelect } = require("./handleModelSelect");
const { handleSourceSelect } = require("./handleSourceSelect");
const { handleSourcePagination } = require("./handleSourcePagination");
const { handleBackSources } = require("./handleBackSources");
const ExactCallbackHandlers = {
"ws-create": handleWorkspaceCreate,
"back:workspaces": handleBackWorkspaces,
"mdl:cancel": handleModelCancel,
"src:back": handleBackSources,
"src:close": handleSourceSelect,
};
const PrefixCallbackHandlers = [
{ prefix: "wspg:", handler: handleWorkspacePagination },
{ prefix: "ws:", handler: handleWorkspaceSelect },
{ prefix: "thpg:", handler: handleThreadPagination },
{ prefix: "th:", handler: handleThreadSelect },
{ prefix: "mdlpg:", handler: handleModelPagination },
{ prefix: "mdl:", handler: handleModelSelect },
{ prefix: "srcpg:", handler: handleSourcePagination },
{ prefix: "src:", handler: handleSourceSelect },
];
/**
* Resolves the appropriate callback handler for the given data string.
* First checks exact matches, then prefix matches (in order).
* @param {string} data - The callback data string
* @returns {Function|null} - The handler function or null if not found
*/
function resolveCallbackHandler(data) {
if (ExactCallbackHandlers[data]) {
return ExactCallbackHandlers[data];
}
for (const { prefix, handler } of PrefixCallbackHandlers) {
if (data.startsWith(prefix)) {
return handler;
}
}
return null;
}
module.exports = {
ExactCallbackHandlers,
PrefixCallbackHandlers,
resolveCallbackHandler,
};

View File

@ -0,0 +1,35 @@
const { isVerified } = require("../verification");
const { resolveCallbackHandler } = require("./callbacks");
/**
* Handle inline keyboard callback queries (workspace/thread selection).
* @param {BotContext} ctx
* @param {object} query - Telegram callback query object
*/
async function handleKeyboardQueryCallback(ctx, query) {
const chatId = query.message.chat.id;
const messageId = query.message.message_id;
const data = query.data;
if (!isVerified(ctx.config.approved_users, chatId)) {
await ctx.bot.answerCallbackQuery(query.id, {
text: "You are not approved.",
});
return;
}
try {
const handler = resolveCallbackHandler(data);
if (!handler) throw new Error(`Callback handler not found: ${data}`);
await handler({ ctx, chatId, query, messageId, data });
} catch (error) {
ctx.log("Callback error:", error.message);
await ctx.bot.answerCallbackQuery(query.id, {
text: "Something went wrong.",
});
}
}
module.exports = {
handleKeyboardQueryCallback,
};

View File

@ -0,0 +1,171 @@
const crypto = require("crypto");
const {
ExternalCommunicationConnector,
} = require("../../../models/externalCommunicationConnector");
const { markdownToTelegram } = require("./format");
/**
* The maximum number of pending pairings to allow.
* This is to prevent abuse and ensure the bot is not responding to too many requests.
* @type {number}
*/
const MAX_PENDING_PAIRINGS = 10;
/**
* Generate a random 6-digit pairing code.
* @returns {string}
*/
function generatePairingCode() {
return String(crypto.randomInt(0, 1000000)).padStart(6, "0");
}
/**
* Enforce the max pending pairings cap using LIFO (evict oldest first).
* @param {Map} pendingPairings
*/
function enforcePendingCap(pendingPairings) {
if (pendingPairings.size < MAX_PENDING_PAIRINGS) return;
// Sort by requestedAt ascending (oldest first) and remove excess
const entries = [...pendingPairings.entries()].sort(
(a, b) => new Date(a[1].requestedAt) - new Date(b[1].requestedAt)
);
const toRemove = entries.length - MAX_PENDING_PAIRINGS + 1;
for (let i = 0; i < toRemove; i++) {
pendingPairings.delete(entries[i][0]);
}
}
/**
* Check if a chat ID is in the approved users list.
* Handles both legacy string format and new object format.
* @param {Array} approvedUsers
* @param {number|string} chatId
* @returns {boolean}
*/
function isVerified(approvedUsers, chatId) {
return (approvedUsers || []).some(
(u) => (typeof u === "string" ? u : u.chatId) === String(chatId)
);
}
/**
* Send a pairing request message to an unverified user.
* @param {TelegramBot} bot
* @param {object} msg - Telegram message object
* @param {Map} pendingPairings
*/
async function sendPairingRequest(bot, msg, pendingPairings) {
const chatId = msg.chat.id;
const firstName = msg.from?.first_name || "Unknown";
const username = msg.from?.username || null;
// Reuse existing code if the user already has a pending request
const existing = pendingPairings.get(chatId);
const code = existing?.code || generatePairingCode();
// Enforce cap before adding new entries (not for existing users)
if (!existing) enforcePendingCap(pendingPairings);
pendingPairings.set(chatId, {
code,
telegramUsername: username,
firstName,
requestedAt: existing?.requestedAt || new Date().toISOString(),
});
const formattedMessage = markdownToTelegram(
`You need to be **approved** before using this bot.
Your pairing code is: <code>${code}</code>
In AnythingLLM, go to Settings Connections Telegram and approve your request.
Make sure the pairing code shown here matches what is displayed in the settings page.
This ensures no one else is trying to connect on your behalf.`,
{ escapeHtml: false }
);
await bot.sendMessage(chatId, formattedMessage, { parse_mode: "HTML" });
}
/**
* Approve a pending user by their chat ID.
* @param {TelegramBot|null} bot
* @param {string|number} chatId
* @param {object} config - Current connector config (mutated in place)
* @param {Map} pendingPairings
*/
async function approveUser(bot, chatId, config, pendingPairings) {
const approved = config.approved_users || [];
const alreadyApproved = isVerified(approved, chatId);
if (!alreadyApproved) {
const pending = pendingPairings.get(Number(chatId));
approved.push({
chatId: String(chatId),
username: pending?.telegramUsername || null,
firstName: pending?.firstName || null,
});
config.approved_users = approved;
await ExternalCommunicationConnector.updateConfig("telegram", {
approved_users: approved,
});
}
pendingPairings.delete(Number(chatId));
if (bot) {
try {
await bot.sendMessage(
chatId,
"You've been approved! Send a message to start chatting."
);
} catch {
// User may have blocked bot
}
}
}
/**
* Deny a pending user by their chat ID.
* @param {TelegramBot|null} bot
* @param {string|number} chatId
* @param {Map} pendingPairings
*/
async function denyUser(bot, chatId, pendingPairings) {
pendingPairings.delete(Number(chatId));
if (bot) {
try {
await bot.sendMessage(chatId, "Your access request was denied.");
} catch {
// User may have blocked bot
}
}
}
/**
* Revoke an already-approved user.
* @param {string|number} chatId
* @param {object} config - Current connector config (mutated in place)
*/
async function revokeUser(chatId, config) {
const approved = (config.approved_users || []).filter(
(u) => (typeof u === "string" ? u : u.chatId) !== String(chatId)
);
config.approved_users = approved;
await ExternalCommunicationConnector.updateConfig("telegram", {
approved_users: approved,
});
}
module.exports = {
MAX_PENDING_PAIRINGS,
isVerified,
sendPairingRequest,
approveUser,
denyUser,
revokeUser,
};

File diff suppressed because it is too large Load Diff