@lobehub/lobehub 2.0.0-next.230 → 2.0.0-next.232

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/desktop-onboarding.json +1 -1
  4. package/locales/bg-BG/desktop-onboarding.json +1 -1
  5. package/locales/de-DE/desktop-onboarding.json +1 -1
  6. package/locales/en-US/desktop-onboarding.json +1 -1
  7. package/locales/es-ES/desktop-onboarding.json +1 -1
  8. package/locales/fa-IR/desktop-onboarding.json +1 -1
  9. package/locales/fr-FR/desktop-onboarding.json +1 -1
  10. package/locales/it-IT/desktop-onboarding.json +1 -1
  11. package/locales/ja-JP/desktop-onboarding.json +1 -1
  12. package/locales/ko-KR/desktop-onboarding.json +1 -1
  13. package/locales/nl-NL/desktop-onboarding.json +1 -1
  14. package/locales/pl-PL/desktop-onboarding.json +1 -1
  15. package/locales/pt-BR/desktop-onboarding.json +1 -1
  16. package/locales/ru-RU/desktop-onboarding.json +1 -1
  17. package/locales/tr-TR/desktop-onboarding.json +1 -1
  18. package/locales/vi-VN/desktop-onboarding.json +1 -1
  19. package/locales/zh-CN/desktop-onboarding.json +1 -1
  20. package/locales/zh-TW/desktop-onboarding.json +1 -1
  21. package/package.json +1 -1
  22. package/src/components/Loading/BrandTextLoading/index.module.css +81 -0
  23. package/src/components/Loading/BrandTextLoading/index.tsx +24 -17
  24. package/src/libs/redis/index.ts +1 -0
  25. package/src/libs/redis/keys.ts +59 -0
  26. package/src/libs/redis/manager.ts +63 -0
  27. package/src/locales/default/desktop-onboarding.ts +1 -1
  28. package/src/server/services/agent/index.test.ts +150 -0
  29. package/src/server/services/agent/index.ts +51 -2
  30. package/src/utils/identifier.test.ts +103 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.232](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.231...v2.0.0-next.232)
6
+
7
+ <sup>Released on **2026-01-07**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Correct BrandTextLoading position after removing SSG CSS-in-JS injection.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Correct BrandTextLoading position after removing SSG CSS-in-JS injection, closes [#11312](https://github.com/lobehub/lobe-chat/issues/11312) ([0de4eb8](https://github.com/lobehub/lobe-chat/commit/0de4eb8))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 2.0.0-next.231](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.230...v2.0.0-next.231)
31
+
32
+ <sup>Released on **2026-01-07**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Update desktop onboarding privacy description.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Update desktop onboarding privacy description, closes [#11307](https://github.com/lobehub/lobe-chat/issues/11307) [#11308](https://github.com/lobehub/lobe-chat/issues/11308) ([58b10a2](https://github.com/lobehub/lobe-chat/commit/58b10a2))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 2.0.0-next.230](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.229...v2.0.0-next.230)
6
56
 
7
57
  <sup>Released on **2026-01-07**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Correct BrandTextLoading position after removing SSG CSS-in-JS injection."
6
+ ]
7
+ },
8
+ "date": "2026-01-07",
9
+ "version": "2.0.0-next.232"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Update desktop onboarding privacy description."
15
+ ]
16
+ },
17
+ "date": "2026-01-07",
18
+ "version": "2.0.0-next.231"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "اختر كيف تريد مشاركة البيانات. يساعدنا اختيارك على التحسين، ويمكنك تغيير ذلك في أي وقت من الإعدادات.",
46
46
  "screen4.footerNote": "يمكنك تغيير ذلك في أي وقت من الإعدادات",
47
47
  "screen4.navigation.next": "متابعة",
48
- "screen4.privacy.description": "احتفظ بكل شيء محليًا. لا يتم جمع أو مشاركة أي بيانات خصوصية كاملة لمحادثاتك وسير عملك.",
48
+ "screen4.privacy.description": "عطّل تحليلات الاستخدام المجهّلة. لا تتم مشاركة بيانات الأداء أو استخدام النماذج أو تفاعلات الميزات.",
49
49
  "screen4.privacy.items.1": "لا جمع للبيانات",
50
50
  "screen4.privacy.items.2": "لا تحليلات استخدام",
51
51
  "screen4.privacy.items.3": "جميع المعالجة تتم محليًا",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Избери как искаш да споделяш данни. Твоят избор ни помага да се подобрим, а можеш да го промениш по всяко време от настройките.",
46
46
  "screen4.footerNote": "Можеш да го промениш по всяко време от настройките",
47
47
  "screen4.navigation.next": "Продължи",
48
- "screen4.privacy.description": "Всичко остава локално. Никакви данни не се събират или споделят пълна поверителност за твоите разговори и работни процеси.",
48
+ "screen4.privacy.description": "Изключете анонимната аналитика за използване. Не споделяме данни за производителност, използване на модели или взаимодействия с функции.",
49
49
  "screen4.privacy.items.1": "Без събиране на данни",
50
50
  "screen4.privacy.items.2": "Без анализ на използването",
51
51
  "screen4.privacy.items.3": "Цялата обработка е локална",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Wählen Sie, wie Sie Daten teilen möchten. Ihre Entscheidung hilft uns bei der Verbesserung, und Sie können dies jederzeit in den Einstellungen ändern.",
46
46
  "screen4.footerNote": "Sie können dies jederzeit in den Einstellungen ändern",
47
47
  "screen4.navigation.next": "Weiter",
48
- "screen4.privacy.description": "Alles bleibt lokal. Es werden keine Daten gesammelt oder geteilt – vollständige Privatsphäre für Ihre Gespräche und Arbeitsabläufe.",
48
+ "screen4.privacy.description": "Deaktivieren Sie anonymisierte Nutzungsanalysen. Es werden keine Leistungsdaten, Modellnutzung oder Funktionsinteraktionen geteilt.",
49
49
  "screen4.privacy.items.1": "Keine Datenerfassung",
50
50
  "screen4.privacy.items.2": "Keine Nutzungsanalysen",
51
51
  "screen4.privacy.items.3": "Alle Verarbeitungen bleiben lokal",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Choose how you want to share data. Your choice helps us improve, and you can change this anytime in settings.",
46
46
  "screen4.footerNote": "You can change this anytime in settings",
47
47
  "screen4.navigation.next": "Continue",
48
- "screen4.privacy.description": "Keep everything local. No data is collected or shared—complete privacy for your conversations and workflows.",
48
+ "screen4.privacy.description": "Disable anonymized usage analytics. No performance, model usage, or feature interaction data is shared.",
49
49
  "screen4.privacy.items.1": "No data collection",
50
50
  "screen4.privacy.items.2": "No usage analytics",
51
51
  "screen4.privacy.items.3": "All processing stays local",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Elige cómo deseas compartir tus datos. Tu elección nos ayuda a mejorar, y puedes cambiarla en cualquier momento desde la configuración.",
46
46
  "screen4.footerNote": "Puedes cambiar esto en cualquier momento desde la configuración",
47
47
  "screen4.navigation.next": "Continuar",
48
- "screen4.privacy.description": "Mantén todo local. No se recopilan ni comparten datos: privacidad total para tus conversaciones y flujos de trabajo.",
48
+ "screen4.privacy.description": "Desactiva la analítica de uso anónima. No se comparten métricas de rendimiento, uso del modelo ni interacciones con funciones.",
49
49
  "screen4.privacy.items.1": "Sin recopilación de datos",
50
50
  "screen4.privacy.items.2": "Sin análisis de uso",
51
51
  "screen4.privacy.items.3": "Todo el procesamiento permanece local",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "نحوه اشتراک‌گذاری داده‌ها را انتخاب کنید. انتخاب شما به ما در بهبود کمک می‌کند و هر زمان می‌توانید آن را در تنظیمات تغییر دهید.",
46
46
  "screen4.footerNote": "شما می‌توانید هر زمان از طریق تنظیمات آن را تغییر دهید",
47
47
  "screen4.navigation.next": "ادامه",
48
- "screen4.privacy.description": "همه چیز به صورت محلی باقی می‌ماند. هیچ داده‌ای جمع‌آوری یا به اشتراک گذاشته نمی‌شود حریم خصوصی کامل برای گفتگوها و جریان‌های کاری شما.",
48
+ "screen4.privacy.description": "تحلیل استفادهٔ ناشناس را غیرفعال کنید. هیچ داده‌ای از عملکرد، میزان استفاده از مدل یا تعاملات ویژگی‌ها به اشتراک گذاشته نمی‌شود.",
49
49
  "screen4.privacy.items.1": "بدون جمع‌آوری داده",
50
50
  "screen4.privacy.items.2": "بدون تحلیل استفاده",
51
51
  "screen4.privacy.items.3": "تمام پردازش‌ها به صورت محلی انجام می‌شود",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Choisissez comment vous souhaitez partager vos données. Votre choix nous aide à nous améliorer, et vous pouvez le modifier à tout moment dans les paramètres.",
46
46
  "screen4.footerNote": "Vous pouvez modifier cela à tout moment dans les paramètres",
47
47
  "screen4.navigation.next": "Continuer",
48
- "screen4.privacy.description": "Gardez tout en local. Aucune donnée n'est collectée ni partagée confidentialité totale pour vos conversations et flux de travail.",
48
+ "screen4.privacy.description": "Désactivez l’analyse d’usage anonyme. Aucune donnée de performance, d’utilisation des modèles ou d’interactions avec les fonctionnalités n’est partagée.",
49
49
  "screen4.privacy.items.1": "Aucune collecte de données",
50
50
  "screen4.privacy.items.2": "Aucune analyse d'utilisation",
51
51
  "screen4.privacy.items.3": "Tout le traitement reste local",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Scegli come desideri condividere i dati. La tua scelta ci aiuta a migliorare e puoi modificarla in qualsiasi momento nelle impostazioni.",
46
46
  "screen4.footerNote": "Puoi modificarla in qualsiasi momento nelle impostazioni",
47
47
  "screen4.navigation.next": "Continua",
48
- "screen4.privacy.description": "Mantieni tutto in locale. Nessun dato viene raccolto o condiviso—privacy totale per le tue conversazioni e flussi di lavoro.",
48
+ "screen4.privacy.description": "Disattiva l’analisi d’uso anonima. Non vengono condivisi dati su prestazioni, utilizzo dei modelli o interazioni con le funzionalità.",
49
49
  "screen4.privacy.items.1": "Nessuna raccolta dati",
50
50
  "screen4.privacy.items.2": "Nessuna analisi d'uso",
51
51
  "screen4.privacy.items.3": "Tutto l'elaborazione resta in locale",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "データの共有方法を選択してください。ご選択は改善に役立ち、設定からいつでも変更できます。",
46
46
  "screen4.footerNote": "設定からいつでも変更できます",
47
47
  "screen4.navigation.next": "続行",
48
- "screen4.privacy.description": "すべてをローカルに保持。データは収集・共有されず、会話やワークフローは完全にプライベートです。",
48
+ "screen4.privacy.description": "匿名の利用分析を無効にします。パフォーマンス、モデル利用状況、機能の操作データは共有されません。",
49
49
  "screen4.privacy.items.1": "データ収集なし",
50
50
  "screen4.privacy.items.2": "使用状況分析なし",
51
51
  "screen4.privacy.items.3": "すべての処理はローカルで実行",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "데이터 공유 방식을 선택하세요. 선택은 개선에 도움이 되며, 설정에서 언제든지 변경할 수 있습니다.",
46
46
  "screen4.footerNote": "설정에서 언제든지 변경할 수 있습니다",
47
47
  "screen4.navigation.next": "계속",
48
- "screen4.privacy.description": "모든 데이터를 로컬에 유지합니다. 데이터 수집이나 공유 없이 대화와 워크플로우의 완전한 프라이버시를 보장합니다.",
48
+ "screen4.privacy.description": "익명 사용 분석을 끕니다. 성능, 모델 사용량, 기능 상호작용 데이터는 공유되지 않습니다.",
49
49
  "screen4.privacy.items.1": "데이터 수집 없음",
50
50
  "screen4.privacy.items.2": "사용 분석 없음",
51
51
  "screen4.privacy.items.3": "모든 처리는 로컬에서 수행",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Kies hoe je gegevens wilt delen. Jouw keuze helpt ons verbeteren, en je kunt dit op elk moment wijzigen in de instellingen.",
46
46
  "screen4.footerNote": "Je kunt dit op elk moment wijzigen in de instellingen",
47
47
  "screen4.navigation.next": "Doorgaan",
48
- "screen4.privacy.description": "Houd alles lokaal. Er worden geen gegevens verzameld of gedeeld—volledige privacy voor je gesprekken en workflows.",
48
+ "screen4.privacy.description": "Schakel geanonimiseerde gebruiksanalyses uit. Er worden geen prestatiegegevens, modelgebruik of functie-interacties gedeeld.",
49
49
  "screen4.privacy.items.1": "Geen gegevensverzameling",
50
50
  "screen4.privacy.items.2": "Geen gebruiksanalyses",
51
51
  "screen4.privacy.items.3": "Alle verwerking blijft lokaal",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Wybierz, jak chcesz udostępniać dane. Twój wybór pomaga nam się rozwijać i możesz go zmienić w każdej chwili w ustawieniach.",
46
46
  "screen4.footerNote": "Możesz to zmienić w każdej chwili w ustawieniach",
47
47
  "screen4.navigation.next": "Kontynuuj",
48
- "screen4.privacy.description": "Zachowaj wszystko lokalnie. Żadne dane nie zbierane ani udostępniane pełna prywatność Twoich rozmów i procesów.",
48
+ "screen4.privacy.description": "Wyłącz anonimową analitykę użycia. Nie udostępniamy danych o wydajności, użyciu modeli ani interakcjach z funkcjami.",
49
49
  "screen4.privacy.items.1": "Brak zbierania danych",
50
50
  "screen4.privacy.items.2": "Brak analityki użytkowania",
51
51
  "screen4.privacy.items.3": "Całe przetwarzanie lokalne",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Escolha como deseja compartilhar dados. Sua escolha nos ajuda a melhorar, e você pode alterá-la a qualquer momento nas configurações.",
46
46
  "screen4.footerNote": "Você pode alterar isso a qualquer momento nas configurações",
47
47
  "screen4.navigation.next": "Continuar",
48
- "screen4.privacy.description": "Mantenha tudo local. Nenhum dado é coletado ou compartilhado privacidade total para suas conversas e fluxos de trabalho.",
48
+ "screen4.privacy.description": "Desative a análise de uso anônima. Nenhum dado de desempenho, uso do modelo ou interações com recursos é compartilhado.",
49
49
  "screen4.privacy.items.1": "Sem coleta de dados",
50
50
  "screen4.privacy.items.2": "Sem análise de uso",
51
51
  "screen4.privacy.items.3": "Todo o processamento é local",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Выберите, как вы хотите делиться данными. Ваш выбор помогает нам становиться лучше. Вы можете изменить его в любое время в настройках.",
46
46
  "screen4.footerNote": "Вы можете изменить это в любое время в настройках",
47
47
  "screen4.navigation.next": "Продолжить",
48
- "screen4.privacy.description": "Всё остаётся локально. Данные не собираются и не передаются полная конфиденциальность ваших разговоров и рабочих процессов.",
48
+ "screen4.privacy.description": "Отключите анонимную аналитику использования. Мы не передаём данные о производительности, использовании моделей и взаимодействиях с функциями.",
49
49
  "screen4.privacy.items.1": "Без сбора данных",
50
50
  "screen4.privacy.items.2": "Без аналитики использования",
51
51
  "screen4.privacy.items.3": "Вся обработка — локально",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Verilerinizi nasıl paylaşmak istediğinizi seçin. Seçiminiz bize gelişmemizde yardımcı olur ve bunu istediğiniz zaman ayarlardan değiştirebilirsiniz.",
46
46
  "screen4.footerNote": "Bunu istediğiniz zaman ayarlardan değiştirebilirsiniz",
47
47
  "screen4.navigation.next": "Devam Et",
48
- "screen4.privacy.description": "Her şey yerel kalır. Hiçbir veri toplanmaz veya paylaşılmaz—konuşmalarınız ve akışlarınız tamamen gizli kalır.",
48
+ "screen4.privacy.description": "Anonim kullanım analizini kapatın. Performans, model kullanımı veya özellik etkileşim verileri paylaşılmaz.",
49
49
  "screen4.privacy.items.1": "Veri toplanmaz",
50
50
  "screen4.privacy.items.2": "Kullanım analitiği yok",
51
51
  "screen4.privacy.items.3": "Tüm işlemler yerel olarak gerçekleştirilir",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "Chọn cách bạn muốn chia sẻ dữ liệu. Quyết định của bạn giúp chúng tôi cải thiện, và bạn có thể thay đổi bất cứ lúc nào trong phần cài đặt.",
46
46
  "screen4.footerNote": "Bạn có thể thay đổi điều này bất cứ lúc nào trong phần cài đặt",
47
47
  "screen4.navigation.next": "Tiếp tục",
48
- "screen4.privacy.description": "Giữ mọi thứ cục bộ. Không thu thập hay chia sẻ dữ liệu—bảo mật hoàn toàn cho cuộc trò chuyện quy trình làm việc của bạn.",
48
+ "screen4.privacy.description": "Tắt phân tích sử dụng ẩn danh. Không chia sẻ dữ liệu hiệu năng, mức sử dụng hình hoặc tương tác tính năng.",
49
49
  "screen4.privacy.items.1": "Không thu thập dữ liệu",
50
50
  "screen4.privacy.items.2": "Không phân tích hành vi sử dụng",
51
51
  "screen4.privacy.items.3": "Mọi xử lý đều diễn ra cục bộ",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "选择您希望如何共享数据。您的选择将帮助我们改进,您也可以随时在设置中更改。",
46
46
  "screen4.footerNote": "您可以随时在设置中更改",
47
47
  "screen4.navigation.next": "继续",
48
- "screen4.privacy.description": "所有数据仅保留在本地。不收集或共享任何信息——为您的对话和工作流提供完整隐私保护。",
48
+ "screen4.privacy.description": "不共享匿名使用数据,也不会上传性能、模型使用或功能交互等统计信息。",
49
49
  "screen4.privacy.items.1": "不收集数据",
50
50
  "screen4.privacy.items.2": "不做使用分析",
51
51
  "screen4.privacy.items.3": "处理尽量在本地完成",
@@ -45,7 +45,7 @@
45
45
  "screen4.description": "選擇你希望如何分享資料。你的選擇將幫助我們改進,你可隨時在設定中變更。",
46
46
  "screen4.footerNote": "你可隨時在設定中變更",
47
47
  "screen4.navigation.next": "繼續",
48
- "screen4.privacy.description": "所有資料皆保留於本地。不會收集或分享任何資料,確保對話與工作流程的完整隱私。",
48
+ "screen4.privacy.description": "不分享匿名使用資料,也不會上傳效能、模型使用或功能互動等統計資訊。",
49
49
  "screen4.privacy.items.1": "不收集資料",
50
50
  "screen4.privacy.items.2": "不進行使用分析",
51
51
  "screen4.privacy.items.3": "所有處理皆在本地完成",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.230",
3
+ "version": "2.0.0-next.232",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,81 @@
1
+ .container {
2
+ --brand-text-color: var(--colorText, #1f1f1f);
3
+ --brand-muted-color: var(--colorTextSecondary, #8c8c8c);
4
+ --brand-border-color: var(--colorBorder, #d9d9d9);
5
+ --brand-tag-bg: var(--colorFillTertiary, rgba(0, 0, 0, 0.04));
6
+
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+
12
+ width: 100%;
13
+ height: 100vh;
14
+ height: 100dvh;
15
+ gap: 12px;
16
+ }
17
+
18
+ [data-theme='dark'] .container {
19
+ --brand-text-color: var(--colorText, #f0f0f0);
20
+ --brand-muted-color: var(--colorTextSecondary, #a6a6a6);
21
+ --brand-border-color: var(--colorBorder, #424242);
22
+ --brand-tag-bg: var(--colorFillTertiary, rgba(255, 255, 255, 0.08));
23
+ }
24
+
25
+ .brand {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 12px;
29
+
30
+ opacity: 0.6;
31
+ color: var(--brand-text-color);
32
+ }
33
+
34
+ .brand :global(.lobe-brand-loading) {
35
+ display: block;
36
+ color: inherit;
37
+ }
38
+
39
+ .debug {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ gap: 4px;
44
+
45
+ padding: 16px;
46
+ }
47
+
48
+ .debugRow {
49
+ display: flex;
50
+ gap: 8px;
51
+ align-items: center;
52
+
53
+ font-size: 12px;
54
+ color: var(--brand-muted-color);
55
+ }
56
+
57
+ .debugRow code {
58
+ font-family:
59
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
60
+ monospace;
61
+ }
62
+
63
+ .debugTag {
64
+ display: inline-flex;
65
+ align-items: center;
66
+
67
+ padding: 2px 8px;
68
+ border: 1px solid var(--brand-border-color);
69
+ border-radius: 6px;
70
+
71
+ background: var(--brand-tag-bg);
72
+ }
73
+
74
+ .debugTag code {
75
+ color: var(--brand-text-color);
76
+ }
77
+
78
+ .debugHint {
79
+ font-size: 12px;
80
+ color: var(--brand-muted-color);
81
+ }
@@ -1,34 +1,41 @@
1
- import { Center, Tag, Text } from '@lobehub/ui';
2
1
  import { BrandLoading, LobeHubText } from '@lobehub/ui/brand';
3
2
 
4
3
  import { isCustomBranding } from '@/const/version';
5
4
 
6
5
  import CircleLoading from '../CircleLoading';
6
+ import styles from './index.module.css';
7
7
 
8
8
  interface BrandTextLoadingProps {
9
9
  debugId: string;
10
10
  }
11
11
 
12
12
  const BrandTextLoading = ({ debugId }: BrandTextLoadingProps) => {
13
- if (isCustomBranding) return <CircleLoading />;
13
+ if (isCustomBranding)
14
+ return (
15
+ <div className={styles.container}>
16
+ <CircleLoading />
17
+ </div>
18
+ );
19
+
20
+ const showDebug = process.env.NODE_ENV === 'development' && debugId;
14
21
 
15
22
  return (
16
- <Center height={'100%'} width={'100%'}>
17
- <BrandLoading size={40} style={{ opacity: 0.6 }} text={LobeHubText} />
18
- {process.env.NODE_ENV === 'development' && debugId && (
19
- <Center gap={4} padding={16}>
20
- <Text code style={{ alignItems: 'center', display: 'flex' }}>
21
- Debug ID:{' '}
22
- <Tag size={'large'}>
23
- <Text code>{debugId}</Text>
24
- </Tag>
25
- </Text>
26
- <Text fontSize={12} type={'secondary'}>
27
- only visible in development
28
- </Text>
29
- </Center>
23
+ <div className={styles.container}>
24
+ <div aria-label="Loading" className={styles.brand} role="status">
25
+ <BrandLoading size={40} text={LobeHubText} />
26
+ </div>
27
+ {showDebug && (
28
+ <div className={styles.debug}>
29
+ <div className={styles.debugRow}>
30
+ <code>Debug ID:</code>
31
+ <span className={styles.debugTag}>
32
+ <code>{debugId}</code>
33
+ </span>
34
+ </div>
35
+ <div className={styles.debugHint}>only visible in development</div>
36
+ </div>
30
37
  )}
31
- </Center>
38
+ </div>
32
39
  );
33
40
  };
34
41
 
@@ -1,3 +1,4 @@
1
+ export * from './keys';
1
2
  export * from './manager';
2
3
  export * from './redis';
3
4
  export * from './types';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Centralized Redis key definitions
3
+ *
4
+ * All Redis keys should be defined here for easy management and consistency.
5
+ *
6
+ * Structure:
7
+ * - RedisKeyNamespace: Contains all available prefixes
8
+ * - RedisKeys: Contains key builders organized by namespace/scope
9
+ */
10
+
11
+ /**
12
+ * Redis key namespace prefixes
13
+ *
14
+ * Each prefix creates an isolated keyspace in Redis.
15
+ * When using `createRedisWithPrefix`, pass one of these as the prefix parameter.
16
+ */
17
+ export const RedisKeyNamespace = {
18
+ /**
19
+ * AI generation related keys (agent welcome, placeholders, etc.)
20
+ */
21
+ AI_GENERATION: 'aiGeneration',
22
+ /**
23
+ * Core LOBEHUB application keys (sessions, cache, etc.)
24
+ */
25
+ LOBEHUB: 'lobechat',
26
+ } as const;
27
+
28
+ /**
29
+ * Redis key builders organized by namespace/scope
30
+ *
31
+ * Usage:
32
+ * ```ts
33
+ * // Get full key: agent_welcome:{agentId}
34
+ * const key = RedisKeys.aiGeneration.agentWelcome(agentId);
35
+ *
36
+ * // Use with Redis client (prefix is added by createRedisWithPrefix)
37
+ * const redis = await createRedisWithPrefix(config, RedisKeyNamespace.AI_GENERATION);
38
+ * await redis.get(key);
39
+ * // Actual Redis key: aiGeneration:agent_welcome:{agentId}
40
+ * ```
41
+ */
42
+ export const RedisKeys = {
43
+ /**
44
+ * AI generation scope - for AI-generated content like welcome messages
45
+ */
46
+ aiGeneration: {
47
+ /**
48
+ * Agent welcome message and open questions
49
+ * Full key: aiGeneration:agent_welcome:{agentId}
50
+ */
51
+ agentWelcome: (agentId: string): string => `agent_welcome:${agentId}`,
52
+ },
53
+ /**
54
+ * Lobechat core scope - for application-level caching
55
+ */
56
+ lobechat: {
57
+ // Add lobechat scope keys here as needed
58
+ },
59
+ } as const;
@@ -94,3 +94,66 @@ export const createRedisWithPrefix = async (
94
94
  await provider.initialize();
95
95
  return provider;
96
96
  };
97
+
98
+ /**
99
+ * Manages singleton Redis clients per prefix
100
+ */
101
+ class PrefixedRedisManager {
102
+ private static instances = new Map<string, BaseRedisProvider>();
103
+ private static initPromises = new Map<string, Promise<BaseRedisProvider | null>>();
104
+
105
+ static async initialize(config: RedisConfig, prefix: string): Promise<BaseRedisProvider | null> {
106
+ const existing = this.instances.get(prefix);
107
+ if (existing) return existing;
108
+
109
+ const pendingPromise = this.initPromises.get(prefix);
110
+ if (pendingPromise) return pendingPromise;
111
+
112
+ const initPromise = (async () => {
113
+ const provider = createProvider(config, prefix);
114
+ if (!provider) return null;
115
+
116
+ await provider.initialize();
117
+ this.instances.set(prefix, provider);
118
+ return provider;
119
+ })().catch((error) => {
120
+ this.initPromises.delete(prefix);
121
+ throw error;
122
+ });
123
+
124
+ this.initPromises.set(prefix, initPromise);
125
+ return initPromise;
126
+ }
127
+
128
+ static async reset(prefix?: string) {
129
+ if (prefix) {
130
+ const instance = this.instances.get(prefix);
131
+ if (instance) {
132
+ await instance.disconnect();
133
+ this.instances.delete(prefix);
134
+ this.initPromises.delete(prefix);
135
+ }
136
+ } else {
137
+ for (const instance of this.instances.values()) {
138
+ await instance.disconnect();
139
+ }
140
+ this.instances.clear();
141
+ this.initPromises.clear();
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Initialize a singleton Redis client with custom prefix
148
+ *
149
+ * Unlike createRedisWithPrefix, this reuses the same client for each prefix,
150
+ * avoiding connection leaks when called frequently.
151
+ *
152
+ * @param config - Redis config
153
+ * @param prefix - Custom prefix for all keys (e.g., 'aiGeneration')
154
+ * @returns Redis client or null if Redis is disabled
155
+ */
156
+ export const initializeRedisWithPrefix = (config: RedisConfig, prefix: string) =>
157
+ PrefixedRedisManager.initialize(config, prefix);
158
+
159
+ export const resetPrefixedRedisClient = (prefix?: string) => PrefixedRedisManager.reset(prefix);
@@ -58,7 +58,7 @@ export default {
58
58
  'screen4.footerNote': 'You can change this anytime in settings',
59
59
  'screen4.navigation.next': 'Continue',
60
60
  'screen4.privacy.description':
61
- 'Keep everything local. No data is collected or shared—complete privacy for your conversations and workflows.',
61
+ 'Disable anonymized usage analytics. No performance, model usage, or feature interaction data is shared.',
62
62
  'screen4.privacy.items.1': 'No data collection',
63
63
  'screen4.privacy.items.2': 'No usage analytics',
64
64
  'screen4.privacy.items.3': 'All processing stays local',
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
5
  import { AgentModel } from '@/database/models/agent';
6
6
  import { SessionModel } from '@/database/models/session';
7
7
  import { UserModel } from '@/database/models/user';
8
+ import { RedisKeys, initializeRedisWithPrefix, isRedisEnabled } from '@/libs/redis';
8
9
  import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
9
10
 
10
11
  import { AgentService } from './index';
@@ -34,6 +35,19 @@ vi.mock('@/database/models/user', () => ({
34
35
  UserModel: vi.fn(),
35
36
  }));
36
37
 
38
+ vi.mock('@/envs/redis', () => ({
39
+ getRedisConfig: vi.fn().mockReturnValue({ enabled: true }),
40
+ }));
41
+
42
+ vi.mock('@/libs/redis', async (importOriginal) => {
43
+ const original = await importOriginal<typeof import('@/libs/redis')>();
44
+ return {
45
+ ...original,
46
+ initializeRedisWithPrefix: vi.fn(),
47
+ isRedisEnabled: vi.fn(),
48
+ };
49
+ });
50
+
37
51
  describe('AgentService', () => {
38
52
  let service: AgentService;
39
53
  const mockDb = {} as any;
@@ -271,5 +285,141 @@ describe('AgentService', () => {
271
285
  expect(result?.model).toBe('claude-3');
272
286
  expect(result?.provider).toBe('anthropic');
273
287
  });
288
+
289
+ describe('Redis welcome data integration', () => {
290
+ const mockRedisGet = vi.fn();
291
+ const mockRedisClient = { get: mockRedisGet };
292
+
293
+ beforeEach(() => {
294
+ vi.mocked(initializeRedisWithPrefix).mockReset();
295
+ vi.mocked(isRedisEnabled).mockReset();
296
+ mockRedisGet.mockReset();
297
+ });
298
+
299
+ it('should merge Redis welcome data when available', async () => {
300
+ const mockAgent = {
301
+ id: 'agent-1',
302
+ model: 'gpt-4',
303
+ };
304
+ const welcomeData = {
305
+ openQuestions: ['Question 1?', 'Question 2?'],
306
+ welcomeMessage: 'Hello from Redis!',
307
+ };
308
+
309
+ const mockAgentModel = {
310
+ getAgentConfigById: vi.fn().mockResolvedValue(mockAgent),
311
+ };
312
+
313
+ (AgentModel as any).mockImplementation(() => mockAgentModel);
314
+ (parseAgentConfig as any).mockReturnValue({});
315
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
316
+ vi.mocked(initializeRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
317
+ mockRedisGet.mockResolvedValue(JSON.stringify(welcomeData));
318
+
319
+ const newService = new AgentService(mockDb, mockUserId);
320
+ const result = await newService.getAgentConfigById('agent-1');
321
+
322
+ expect(result?.openingMessage).toBe('Hello from Redis!');
323
+ expect(result?.openingQuestions).toEqual(['Question 1?', 'Question 2?']);
324
+ expect(mockRedisGet).toHaveBeenCalledWith(RedisKeys.aiGeneration.agentWelcome('agent-1'));
325
+ });
326
+
327
+ it('should return normal config when Redis is disabled', async () => {
328
+ const mockAgent = {
329
+ id: 'agent-1',
330
+ model: 'gpt-4',
331
+ openingMessage: 'Default message',
332
+ };
333
+
334
+ const mockAgentModel = {
335
+ getAgentConfigById: vi.fn().mockResolvedValue(mockAgent),
336
+ };
337
+
338
+ (AgentModel as any).mockImplementation(() => mockAgentModel);
339
+ (parseAgentConfig as any).mockReturnValue({});
340
+ vi.mocked(isRedisEnabled).mockReturnValue(false);
341
+
342
+ const newService = new AgentService(mockDb, mockUserId);
343
+ const result = await newService.getAgentConfigById('agent-1');
344
+
345
+ // Should keep original config, not override with Redis data
346
+ expect(result?.openingMessage).toBe('Default message');
347
+ // openingQuestions comes from DEFAULT_AGENT_CONFIG (empty array)
348
+ expect(result?.openingQuestions).toEqual([]);
349
+ expect(initializeRedisWithPrefix).not.toHaveBeenCalled();
350
+ });
351
+
352
+ it('should return normal config when Redis key does not exist', async () => {
353
+ const mockAgent = {
354
+ id: 'agent-1',
355
+ model: 'gpt-4',
356
+ };
357
+
358
+ const mockAgentModel = {
359
+ getAgentConfigById: vi.fn().mockResolvedValue(mockAgent),
360
+ };
361
+
362
+ (AgentModel as any).mockImplementation(() => mockAgentModel);
363
+ (parseAgentConfig as any).mockReturnValue({});
364
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
365
+ vi.mocked(initializeRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
366
+ mockRedisGet.mockResolvedValue(null);
367
+
368
+ const newService = new AgentService(mockDb, mockUserId);
369
+ const result = await newService.getAgentConfigById('agent-1');
370
+
371
+ // No Redis welcome data, so openingMessage remains from DEFAULT_AGENT_CONFIG
372
+ expect(result?.openingMessage).toBeUndefined();
373
+ // openingQuestions comes from DEFAULT_AGENT_CONFIG (empty array)
374
+ expect(result?.openingQuestions).toEqual([]);
375
+ });
376
+
377
+ it('should gracefully fallback when Redis throws error', async () => {
378
+ const mockAgent = {
379
+ id: 'agent-1',
380
+ model: 'gpt-4',
381
+ };
382
+
383
+ const mockAgentModel = {
384
+ getAgentConfigById: vi.fn().mockResolvedValue(mockAgent),
385
+ };
386
+
387
+ (AgentModel as any).mockImplementation(() => mockAgentModel);
388
+ (parseAgentConfig as any).mockReturnValue({});
389
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
390
+ vi.mocked(initializeRedisWithPrefix).mockRejectedValue(new Error('Redis connection failed'));
391
+
392
+ const newService = new AgentService(mockDb, mockUserId);
393
+ const result = await newService.getAgentConfigById('agent-1');
394
+
395
+ // Should return normal config without error
396
+ expect(result?.id).toBe('agent-1');
397
+ expect(result?.model).toBe('gpt-4');
398
+ });
399
+
400
+ it('should gracefully handle invalid JSON in Redis', async () => {
401
+ const mockAgent = {
402
+ id: 'agent-1',
403
+ model: 'gpt-4',
404
+ };
405
+
406
+ const mockAgentModel = {
407
+ getAgentConfigById: vi.fn().mockResolvedValue(mockAgent),
408
+ };
409
+
410
+ (AgentModel as any).mockImplementation(() => mockAgentModel);
411
+ (parseAgentConfig as any).mockReturnValue({});
412
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
413
+ vi.mocked(initializeRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
414
+ mockRedisGet.mockResolvedValue('invalid json {');
415
+
416
+ const newService = new AgentService(mockDb, mockUserId);
417
+ const result = await newService.getAgentConfigById('agent-1');
418
+
419
+ // Should return normal config without error
420
+ expect(result?.id).toBe('agent-1');
421
+ expect(result?.openingMessage).toBeUndefined();
422
+ });
423
+ });
274
424
  });
275
425
  });
@@ -3,15 +3,25 @@ import { DEFAULT_AGENT_CONFIG } from '@lobechat/const';
3
3
  import { type LobeChatDatabase } from '@lobechat/database';
4
4
  import { type AgentItem, type LobeAgentConfig } from '@lobechat/types';
5
5
  import { cleanObject, merge } from '@lobechat/utils';
6
+ import debug from 'debug';
6
7
  import type { PartialDeep } from 'type-fest';
7
8
 
8
9
  import { AgentModel } from '@/database/models/agent';
9
10
  import { SessionModel } from '@/database/models/session';
10
11
  import { UserModel } from '@/database/models/user';
12
+ import { getRedisConfig } from '@/envs/redis';
13
+ import { RedisKeyNamespace, RedisKeys, initializeRedisWithPrefix, isRedisEnabled } from '@/libs/redis';
11
14
  import { getServerDefaultAgentConfig } from '@/server/globalConfig';
12
15
 
13
16
  import { type UpdateAgentResult } from './type';
14
17
 
18
+ const log = debug('lobe-agent:service');
19
+
20
+ interface AgentWelcomeData {
21
+ openQuestions: string[];
22
+ welcomeMessage: string;
23
+ }
24
+
15
25
  /**
16
26
  * Agent Service
17
27
  *
@@ -76,13 +86,52 @@ export class AgentService {
76
86
  * 2. Server's globalDefaultAgentConfig (from environment variable DEFAULT_AGENT_CONFIG)
77
87
  * 3. User's defaultAgentConfig (from user settings)
78
88
  * 4. The actual agent config from database
89
+ * 5. AI-generated welcome data from Redis (if available)
79
90
  */
80
91
  async getAgentConfigById(agentId: string) {
81
- const [agent, defaultAgentConfig] = await Promise.all([
92
+ const [agent, defaultAgentConfig, welcomeData] = await Promise.all([
82
93
  this.agentModel.getAgentConfigById(agentId),
83
94
  this.userModel.getUserSettingsDefaultAgentConfig(),
95
+ this.getAgentWelcomeFromRedis(agentId),
84
96
  ]);
85
- return this.mergeDefaultConfig(agent, defaultAgentConfig);
97
+
98
+ const config = this.mergeDefaultConfig(agent, defaultAgentConfig);
99
+ if (!config) return null;
100
+
101
+ // Merge AI-generated welcome data if available
102
+ if (welcomeData) {
103
+ return {
104
+ ...config,
105
+ openingMessage: welcomeData.welcomeMessage,
106
+ openingQuestions: welcomeData.openQuestions,
107
+ };
108
+ }
109
+
110
+ return config;
111
+ }
112
+
113
+ /**
114
+ * Get AI-generated welcome data from Redis
115
+ * Returns null if Redis is disabled or data doesn't exist
116
+ */
117
+ private async getAgentWelcomeFromRedis(agentId: string): Promise<AgentWelcomeData | null> {
118
+ try {
119
+ const redisConfig = getRedisConfig();
120
+ if (!isRedisEnabled(redisConfig)) return null;
121
+
122
+ const redis = await initializeRedisWithPrefix(redisConfig, RedisKeyNamespace.AI_GENERATION);
123
+ if (!redis) return null;
124
+
125
+ const key = RedisKeys.aiGeneration.agentWelcome(agentId);
126
+ const value = await redis.get(key);
127
+ if (!value) return null;
128
+
129
+ return JSON.parse(value) as AgentWelcomeData;
130
+ } catch (error) {
131
+ // Log error for observability but don't break agent retrieval
132
+ log('Failed to get agent welcome from Redis for agent %s: %O', agentId, error);
133
+ return null;
134
+ }
86
135
  }
87
136
 
88
137
  /**
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { standardizeIdentifier } from './identifier';
4
+
5
+ describe('standardizeIdentifier', () => {
6
+ describe('extracting ID from prefixed identifiers', () => {
7
+ it('should extract ID from docs_ prefix', () => {
8
+ expect(standardizeIdentifier('docs_123')).toBe('123');
9
+ });
10
+
11
+ it('should extract ID from agt_ prefix', () => {
12
+ expect(standardizeIdentifier('agt_456')).toBe('456');
13
+ });
14
+
15
+ it('should extract ID from any custom prefix', () => {
16
+ expect(standardizeIdentifier('custom_789')).toBe('789');
17
+ });
18
+
19
+ it('should extract ID from identifier with multiple underscores', () => {
20
+ // split('_')[1] only takes the second part, so 'docs_abc_def_123' becomes 'abc'
21
+ expect(standardizeIdentifier('docs_abc_def_123')).toBe('abc');
22
+ });
23
+
24
+ it('should handle numeric IDs', () => {
25
+ expect(standardizeIdentifier('docs_12345')).toBe('12345');
26
+ });
27
+
28
+ it('should handle alphanumeric IDs', () => {
29
+ expect(standardizeIdentifier('agt_abc123xyz')).toBe('abc123xyz');
30
+ });
31
+ });
32
+
33
+ describe('adding prefix to plain identifiers', () => {
34
+ it('should add docs prefix when specified', () => {
35
+ expect(standardizeIdentifier('123', 'docs')).toBe('docs_123');
36
+ });
37
+
38
+ it('should add agt prefix when specified', () => {
39
+ expect(standardizeIdentifier('456', 'agt')).toBe('agt_456');
40
+ });
41
+
42
+ it('should add prefix to alphanumeric identifier', () => {
43
+ expect(standardizeIdentifier('abc123', 'docs')).toBe('docs_abc123');
44
+ });
45
+
46
+ it('should add prefix to string identifier', () => {
47
+ expect(standardizeIdentifier('my-identifier', 'agt')).toBe('agt_my-identifier');
48
+ });
49
+ });
50
+
51
+ describe('returning identifier unchanged', () => {
52
+ it('should return plain identifier unchanged when no prefix specified', () => {
53
+ expect(standardizeIdentifier('123')).toBe('123');
54
+ });
55
+
56
+ it('should return plain alphanumeric identifier unchanged when no prefix specified', () => {
57
+ expect(standardizeIdentifier('abc123')).toBe('abc123');
58
+ });
59
+
60
+ it('should return plain string identifier unchanged when no prefix specified', () => {
61
+ expect(standardizeIdentifier('my-identifier')).toBe('my-identifier');
62
+ });
63
+ });
64
+
65
+ describe('edge cases', () => {
66
+ it('should handle empty string', () => {
67
+ expect(standardizeIdentifier('')).toBe('');
68
+ });
69
+
70
+ it('should handle empty string with prefix', () => {
71
+ expect(standardizeIdentifier('', 'docs')).toBe('docs_');
72
+ });
73
+
74
+ it('should handle identifier with only underscore', () => {
75
+ expect(standardizeIdentifier('_')).toBe('');
76
+ });
77
+
78
+ it('should handle identifier starting with underscore', () => {
79
+ // '_123' splits to ['', '123'], so [1] returns '123'
80
+ expect(standardizeIdentifier('_123')).toBe('123');
81
+ });
82
+
83
+ it('should handle identifier ending with underscore', () => {
84
+ expect(standardizeIdentifier('docs_')).toBe('');
85
+ });
86
+
87
+ it('should prioritize extraction over prefix addition when underscore present', () => {
88
+ // Even with prefix parameter, if underscore exists, it extracts
89
+ expect(standardizeIdentifier('docs_123', 'agt')).toBe('123');
90
+ });
91
+
92
+ it('should handle UUID-like identifiers', () => {
93
+ const uuid = '550e8400-e29b-41d4-a716-446655440000';
94
+ expect(standardizeIdentifier(uuid)).toBe(uuid);
95
+ expect(standardizeIdentifier(uuid, 'docs')).toBe(`docs_${uuid}`);
96
+ });
97
+
98
+ it('should handle very long identifiers', () => {
99
+ const longId = 'a'.repeat(1000);
100
+ expect(standardizeIdentifier(longId, 'docs')).toBe(`docs_${longId}`);
101
+ });
102
+ });
103
+ });