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:
parent
e02faa8984
commit
192ca411f2
@ -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",
|
||||
|
||||
@ -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" />}
|
||||
|
||||
@ -17,7 +17,7 @@ export default function useScrollActiveItemIntoView({
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
ref.current.scrollIntoView({
|
||||
ref.current?.scrollIntoView({
|
||||
behavior,
|
||||
block,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: "ステップ1:Telegramボットを作成する",
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'ı açı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ı açı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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: "*",
|
||||
|
||||
176
frontend/src/models/telegram.js
Normal file
176
frontend/src/models/telegram.js
Normal 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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
*/
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -170,6 +170,9 @@ export default {
|
||||
mobileConnections: () => {
|
||||
return `/settings/mobile-connections`;
|
||||
},
|
||||
telegram: () => {
|
||||
return `/settings/external-connections/telegram`;
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
builder: () => {
|
||||
|
||||
307
server/endpoints/telegram.js
Normal file
307
server/endpoints/telegram.js
Normal 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 };
|
||||
@ -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);
|
||||
|
||||
|
||||
64
server/jobs/handle-telegram-chat.js
Normal file
64
server/jobs/handle-telegram-chat.js
Normal 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();
|
||||
}
|
||||
});
|
||||
110
server/models/externalCommunicationConnector.js
Normal file
110
server/models/externalCommunicationConnector.js
Normal 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 };
|
||||
@ -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",
|
||||
|
||||
12
server/prisma/migrations/20260319202916_init/migration.sql
Normal file
12
server/prisma/migrations/20260319202916_init/migration.sql
Normal 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");
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.");
|
||||
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
377
server/utils/telegramBot/chat/agent.js
Normal file
377
server/utils/telegramBot/chat/agent.js
Normal 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 };
|
||||
469
server/utils/telegramBot/chat/stream.js
Normal file
469
server/utils/telegramBot/chat/stream.js
Normal 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 };
|
||||
22
server/utils/telegramBot/constants.js
Normal file
22
server/utils/telegramBot/constants.js
Normal 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,
|
||||
};
|
||||
632
server/utils/telegramBot/index.js
Normal file
632
server/utils/telegramBot/index.js
Normal 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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
202
server/utils/telegramBot/utils/commands/handlers/handleProof.js
Normal file
202
server/utils/telegramBot/utils/commands/handlers/handleProof.js
Normal 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,
|
||||
};
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
125
server/utils/telegramBot/utils/commands/index.js
Normal file
125
server/utils/telegramBot/utils/commands/index.js
Normal 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,
|
||||
};
|
||||
180
server/utils/telegramBot/utils/format.js
Normal file
180
server/utils/telegramBot/utils/format.js
Normal 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(/^>\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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
203
server/utils/telegramBot/utils/index.js
Normal file
203
server/utils/telegramBot/utils/index.js
Normal 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,
|
||||
};
|
||||
163
server/utils/telegramBot/utils/media.js
Normal file
163
server/utils/telegramBot/utils/media.js
Normal 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,
|
||||
};
|
||||
53
server/utils/telegramBot/utils/messageQueue.js
Normal file
53
server/utils/telegramBot/utils/messageQueue.js
Normal 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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
57
server/utils/telegramBot/utils/navigation/callbacks/index.js
Normal file
57
server/utils/telegramBot/utils/navigation/callbacks/index.js
Normal 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,
|
||||
};
|
||||
35
server/utils/telegramBot/utils/navigation/index.js
Normal file
35
server/utils/telegramBot/utils/navigation/index.js
Normal 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,
|
||||
};
|
||||
171
server/utils/telegramBot/utils/verification.js
Normal file
171
server/utils/telegramBot/utils/verification.js
Normal 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,
|
||||
};
|
||||
895
server/yarn.lock
895
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user