@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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/desktop-onboarding.json +1 -1
- package/locales/bg-BG/desktop-onboarding.json +1 -1
- package/locales/de-DE/desktop-onboarding.json +1 -1
- package/locales/en-US/desktop-onboarding.json +1 -1
- package/locales/es-ES/desktop-onboarding.json +1 -1
- package/locales/fa-IR/desktop-onboarding.json +1 -1
- package/locales/fr-FR/desktop-onboarding.json +1 -1
- package/locales/it-IT/desktop-onboarding.json +1 -1
- package/locales/ja-JP/desktop-onboarding.json +1 -1
- package/locales/ko-KR/desktop-onboarding.json +1 -1
- package/locales/nl-NL/desktop-onboarding.json +1 -1
- package/locales/pl-PL/desktop-onboarding.json +1 -1
- package/locales/pt-BR/desktop-onboarding.json +1 -1
- package/locales/ru-RU/desktop-onboarding.json +1 -1
- package/locales/tr-TR/desktop-onboarding.json +1 -1
- package/locales/vi-VN/desktop-onboarding.json +1 -1
- package/locales/zh-CN/desktop-onboarding.json +1 -1
- package/locales/zh-TW/desktop-onboarding.json +1 -1
- package/package.json +1 -1
- package/src/components/Loading/BrandTextLoading/index.module.css +81 -0
- package/src/components/Loading/BrandTextLoading/index.tsx +24 -17
- package/src/libs/redis/index.ts +1 -0
- package/src/libs/redis/keys.ts +59 -0
- package/src/libs/redis/manager.ts +63 -0
- package/src/locales/default/desktop-onboarding.ts +1 -1
- package/src/server/services/agent/index.test.ts +150 -0
- package/src/server/services/agent/index.ts +51 -2
- 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
|
+
[](#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
|
+
[](#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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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 mô 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.
|
|
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)
|
|
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
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</
|
|
29
|
-
</
|
|
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
|
-
</
|
|
38
|
+
</div>
|
|
32
39
|
);
|
|
33
40
|
};
|
|
34
41
|
|
package/src/libs/redis/index.ts
CHANGED
|
@@ -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
|
-
'
|
|
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
|
-
|
|
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
|
+
});
|