mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-15 12:10:16 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa6d64df2 | |||
| 403aebd52e | |||
| 356cf0c392 | |||
| be98d56ef4 | |||
| 3fae1b2638 | |||
| ee4cc6c2e0 | |||
| b8c0e2d639 | |||
| 74d20bdbe8 | |||
| 7bab44e74c | |||
| fdaa72564c | |||
| 7d85151cb6 | |||
| 23ef2eea59 | |||
| 74ab822140 | |||
| d726ff108d | |||
| dd7b661140 | |||
| 49023419cf | |||
| 2eccbc79eb | |||
| 3527cb65f1 | |||
| 1731c841d8 | |||
| 404ac21229 | |||
| 2eaa2dbea0 | |||
| 50c0ed168d |
+126
@@ -2,6 +2,132 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 1.105.1](https://github.com/lobehub/lobe-chat/compare/v1.105.0...v1.105.1)
|
||||
|
||||
<sup>Released on **2025-07-29**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Support more Text2Image from Qwen.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Support more Text2Image from Qwen, closes [#8574](https://github.com/lobehub/lobe-chat/issues/8574) ([b8c0e2d](https://github.com/lobehub/lobe-chat/commit/b8c0e2d))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.105.0](https://github.com/lobehub/lobe-chat/compare/v1.104.5...v1.105.0)
|
||||
|
||||
<sup>Released on **2025-07-28**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Implement API Key management functionality.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Implement API Key management functionality, closes [#8535](https://github.com/lobehub/lobe-chat/issues/8535) ([fdaa725](https://github.com/lobehub/lobe-chat/commit/fdaa725))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.5](https://github.com/lobehub/lobe-chat/compare/v1.104.4...v1.104.5)
|
||||
|
||||
<sup>Released on **2025-07-28**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Fix setting window layout when in desktop was disappear.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Fix setting window layout when in desktop was disappear, closes [#8585](https://github.com/lobehub/lobe-chat/issues/8585) ([74ab822](https://github.com/lobehub/lobe-chat/commit/74ab822))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.4](https://github.com/lobehub/lobe-chat/compare/v1.104.3...v1.104.4)
|
||||
|
||||
<sup>Released on **2025-07-28**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Fix setting window layout size, update i18n.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Fix setting window layout size, closes [#8483](https://github.com/lobehub/lobe-chat/issues/8483) ([4902341](https://github.com/lobehub/lobe-chat/commit/4902341))
|
||||
- **misc**: Update i18n, closes [#8579](https://github.com/lobehub/lobe-chat/issues/8579) ([2eccbc7](https://github.com/lobehub/lobe-chat/commit/2eccbc7))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.3](https://github.com/lobehub/lobe-chat/compare/v1.104.2...v1.104.3)
|
||||
|
||||
<sup>Released on **2025-07-26**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Add Gemini 2.5 Flash-Lite GA model.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Add Gemini 2.5 Flash-Lite GA model, closes [#8539](https://github.com/lobehub/lobe-chat/issues/8539) ([404ac21](https://github.com/lobehub/lobe-chat/commit/404ac21))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.2](https://github.com/lobehub/lobe-chat/compare/v1.104.1...v1.104.2)
|
||||
|
||||
<sup>Released on **2025-07-26**</sup>
|
||||
|
||||
@@ -1,4 +1,46 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Support more Text2Image from Qwen."]
|
||||
},
|
||||
"date": "2025-07-29",
|
||||
"version": "1.105.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Implement API Key management functionality."]
|
||||
},
|
||||
"date": "2025-07-28",
|
||||
"version": "1.105.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Fix setting window layout when in desktop was disappear."]
|
||||
},
|
||||
"date": "2025-07-28",
|
||||
"version": "1.104.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Fix setting window layout size, update i18n."]
|
||||
},
|
||||
"date": "2025-07-28",
|
||||
"version": "1.104.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add Gemini 2.5 Flash-Lite GA model."]
|
||||
},
|
||||
"date": "2025-07-26",
|
||||
"version": "1.104.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix update hotkey invalid when input mod in desktop."]
|
||||
},
|
||||
"date": "2025-07-26",
|
||||
"version": "1.104.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": [
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "تم الإنشاء تلقائيًا",
|
||||
"copy": "نسخ",
|
||||
"copyError": "فشل النسخ",
|
||||
"copySuccess": "تم نسخ مفتاح API إلى الحافظة",
|
||||
"enterPlaceholder": "الرجاء الإدخال",
|
||||
"hide": "إخفاء",
|
||||
"neverExpires": "لا تنتهي صلاحيتها أبدًا",
|
||||
"neverUsed": "لم يُستخدم أبدًا",
|
||||
"show": "عرض"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "تاريخ الانتهاء",
|
||||
"placeholder": "لا تنتهي صلاحيتها أبدًا"
|
||||
},
|
||||
"name": {
|
||||
"label": "الاسم",
|
||||
"placeholder": "الرجاء إدخال اسم مفتاح API"
|
||||
}
|
||||
},
|
||||
"submit": "إنشاء",
|
||||
"title": "إنشاء مفتاح API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "إنشاء مفتاح API",
|
||||
"delete": "حذف",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "إلغاء",
|
||||
"ok": "تأكيد"
|
||||
},
|
||||
"content": "هل أنت متأكد من حذف هذا المفتاح؟",
|
||||
"title": "تأكيد العملية"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "الإجراءات",
|
||||
"expiresAt": "تاريخ الانتهاء",
|
||||
"key": "المفتاح",
|
||||
"lastUsedAt": "آخر استخدام",
|
||||
"name": "الاسم",
|
||||
"status": "حالة التفعيل"
|
||||
},
|
||||
"title": "قائمة مفاتيح API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "لا يمكن أن يكون المحتوى فارغًا"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "الشهر الماضي",
|
||||
"recent30Days": "آخر 30 يومًا"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "كلمات"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "إدارة مفاتيح API",
|
||||
"profile": "الملف الشخصي",
|
||||
"security": "الأمان",
|
||||
"stats": "الإحصائيات"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash هو نموذج Google الأكثر فعالية من حيث التكلفة، ويوفر وظائف شاملة."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite هو أصغر وأفضل نموذج من حيث التكلفة من Google، مصمم للاستخدام على نطاق واسع."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview هو أصغر وأكفأ نموذج من Google، مصمم للاستخدام واسع النطاق."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Автоматично генериран",
|
||||
"copy": "Копирай",
|
||||
"copyError": "Грешка при копиране",
|
||||
"copySuccess": "API ключът е копиран в клипборда",
|
||||
"enterPlaceholder": "Моля, въведете",
|
||||
"hide": "Скрий",
|
||||
"neverExpires": "Никога не изтича",
|
||||
"neverUsed": "Никога не е използван",
|
||||
"show": "Покажи"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Дата на изтичане",
|
||||
"placeholder": "Никога не изтича"
|
||||
},
|
||||
"name": {
|
||||
"label": "Име",
|
||||
"placeholder": "Моля, въведете име на API ключ"
|
||||
}
|
||||
},
|
||||
"submit": "Създай",
|
||||
"title": "Създаване на API ключ"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Създай API ключ",
|
||||
"delete": "Изтрий",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Отказ",
|
||||
"ok": "Потвърди"
|
||||
},
|
||||
"content": "Сигурни ли сте, че искате да изтриете този API ключ?",
|
||||
"title": "Потвърждение на действие"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Действия",
|
||||
"expiresAt": "Дата на изтичане",
|
||||
"key": "Ключ",
|
||||
"lastUsedAt": "Последна употреба",
|
||||
"name": "Име",
|
||||
"status": "Статус на активиране"
|
||||
},
|
||||
"title": "Списък с API ключове"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Полето не може да бъде празно"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Миналия месец",
|
||||
"recent30Days": "Последните 30 дни"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Думи"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Управление на API ключове",
|
||||
"profile": "Профил",
|
||||
"security": "Сигурност",
|
||||
"stats": "Статистика"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash е най-ефективният модел на Google, предлагащ пълна функционалност."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite е най-малкият и най-ефективен модел на Google, създаден специално за масово използване."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview е най-малкият и най-ефективен модел на Google, проектиран за мащабна употреба."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Automatisch generiert",
|
||||
"copy": "Kopieren",
|
||||
"copyError": "Kopieren fehlgeschlagen",
|
||||
"copySuccess": "API-Schlüssel wurde in die Zwischenablage kopiert",
|
||||
"enterPlaceholder": "Bitte eingeben",
|
||||
"hide": "Verbergen",
|
||||
"neverExpires": "Läuft nie ab",
|
||||
"neverUsed": "Nie verwendet",
|
||||
"show": "Anzeigen"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Ablaufdatum",
|
||||
"placeholder": "Läuft nie ab"
|
||||
},
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Bitte API-Schlüsselname eingeben"
|
||||
}
|
||||
},
|
||||
"submit": "Erstellen",
|
||||
"title": "API-Schlüssel erstellen"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API-Schlüssel erstellen",
|
||||
"delete": "Löschen",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "Bestätigen"
|
||||
},
|
||||
"content": "Möchten Sie diesen API-Schlüssel wirklich löschen?",
|
||||
"title": "Bestätigung"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Aktionen",
|
||||
"expiresAt": "Ablaufdatum",
|
||||
"key": "Schlüssel",
|
||||
"lastUsedAt": "Letzte Verwendung",
|
||||
"name": "Name",
|
||||
"status": "Aktivierungsstatus"
|
||||
},
|
||||
"title": "API-Schlüssel Liste"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Inhalt darf nicht leer sein"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Letzter Monat",
|
||||
"recent30Days": "Letzte 30 Tage"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Wörter"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API-Schlüssel Verwaltung",
|
||||
"profile": "Profil",
|
||||
"security": "Sicherheit",
|
||||
"stats": "Statistiken"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash ist Googles kosteneffizientestes Modell und bietet umfassende Funktionen."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite ist Googles kleinstes und kosteneffizientestes Modell, das speziell für den großflächigen Einsatz entwickelt wurde."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview ist Googles kleinstes und kosteneffizientestes Modell, speziell für den großflächigen Einsatz konzipiert."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Auto-generated",
|
||||
"copy": "Copy",
|
||||
"copyError": "Copy failed",
|
||||
"copySuccess": "API Key copied to clipboard",
|
||||
"enterPlaceholder": "Please enter",
|
||||
"hide": "Hide",
|
||||
"neverExpires": "Never expires",
|
||||
"neverUsed": "Never used",
|
||||
"show": "Show"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Expiration Date",
|
||||
"placeholder": "Never expires"
|
||||
},
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Please enter API Key name"
|
||||
}
|
||||
},
|
||||
"submit": "Create",
|
||||
"title": "Create API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Create API Key",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancel",
|
||||
"ok": "Confirm"
|
||||
},
|
||||
"content": "Are you sure you want to delete this API Key?",
|
||||
"title": "Confirm Action"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Actions",
|
||||
"expiresAt": "Expiration Date",
|
||||
"key": "Key",
|
||||
"lastUsedAt": "Last Used",
|
||||
"name": "Name",
|
||||
"status": "Enabled Status"
|
||||
},
|
||||
"title": "API Key List"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field cannot be empty"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Last Month",
|
||||
"recent30Days": "Last 30 Days"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Total Words"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key Management",
|
||||
"profile": "Profile",
|
||||
"security": "Security",
|
||||
"stats": "Statistics"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash is Google's most cost-effective model, offering comprehensive capabilities."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite is Google's smallest and most cost-effective model, designed for large-scale use."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview is Google's smallest and most cost-efficient model, designed for large-scale usage."
|
||||
},
|
||||
|
||||
@@ -535,6 +535,7 @@
|
||||
"experiment": "Experiment",
|
||||
"hotkey": "Hotkeys",
|
||||
"llm": "Language Model",
|
||||
"plugin": "Plugin Management",
|
||||
"provider": "AI Service Provider",
|
||||
"proxy": "Network Proxy",
|
||||
"storage": "Data Storage",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Generado automáticamente",
|
||||
"copy": "Copiar",
|
||||
"copyError": "Error al copiar",
|
||||
"copySuccess": "Clave API copiada al portapapeles",
|
||||
"enterPlaceholder": "Por favor ingrese",
|
||||
"hide": "Ocultar",
|
||||
"neverExpires": "Nunca expira",
|
||||
"neverUsed": "Nunca usado",
|
||||
"show": "Mostrar"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Fecha de expiración",
|
||||
"placeholder": "Nunca expira"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nombre",
|
||||
"placeholder": "Por favor ingrese el nombre de la clave API"
|
||||
}
|
||||
},
|
||||
"submit": "Crear",
|
||||
"title": "Crear Clave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Crear Clave API",
|
||||
"delete": "Eliminar",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Confirmar"
|
||||
},
|
||||
"content": "¿Está seguro de eliminar esta clave API?",
|
||||
"title": "Confirmar acción"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Acciones",
|
||||
"expiresAt": "Fecha de expiración",
|
||||
"key": "Clave",
|
||||
"lastUsedAt": "Último uso",
|
||||
"name": "Nombre",
|
||||
"status": "Estado"
|
||||
},
|
||||
"title": "Lista de Claves API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "El contenido no puede estar vacío"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Último mes",
|
||||
"recent30Days": "Últimos 30 días"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Palabras"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestión de Claves API",
|
||||
"profile": "Perfil",
|
||||
"security": "Seguridad",
|
||||
"stats": "Estadísticas"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash es el modelo de mejor relación calidad-precio de Google, que ofrece funcionalidades completas."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite es el modelo más pequeño y rentable de Google, diseñado para un uso a gran escala."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview es el modelo más pequeño y con mejor relación calidad-precio de Google, diseñado para un uso a gran escala."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "تولید خودکار",
|
||||
"copy": "کپی",
|
||||
"copyError": "کپی ناموفق بود",
|
||||
"copySuccess": "کلید API به کلیپبورد کپی شد",
|
||||
"enterPlaceholder": "لطفاً وارد کنید",
|
||||
"hide": "مخفی کردن",
|
||||
"neverExpires": "هرگز منقضی نمیشود",
|
||||
"neverUsed": "هرگز استفاده نشده",
|
||||
"show": "نمایش"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "تاریخ انقضا",
|
||||
"placeholder": "هرگز منقضی نمیشود"
|
||||
},
|
||||
"name": {
|
||||
"label": "نام",
|
||||
"placeholder": "لطفاً نام کلید API را وارد کنید"
|
||||
}
|
||||
},
|
||||
"submit": "ایجاد",
|
||||
"title": "ایجاد کلید API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "ایجاد کلید API",
|
||||
"delete": "حذف",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "لغو",
|
||||
"ok": "تأیید"
|
||||
},
|
||||
"content": "آیا از حذف این کلید API مطمئن هستید؟",
|
||||
"title": "تأیید عملیات"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "عملیات",
|
||||
"expiresAt": "تاریخ انقضا",
|
||||
"key": "کلید",
|
||||
"lastUsedAt": "آخرین زمان استفاده",
|
||||
"name": "نام",
|
||||
"status": "وضعیت فعال"
|
||||
},
|
||||
"title": "فهرست کلیدهای API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "محتوا نباید خالی باشد"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "ماه گذشته",
|
||||
"recent30Days": "۳۰ روز گذشته"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "کلمات"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "مدیریت کلید API",
|
||||
"profile": "پروفایل",
|
||||
"security": "امنیت",
|
||||
"stats": "آمار"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash مدل با بهترین نسبت قیمت به کارایی گوگل است که امکانات جامع را ارائه میدهد."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite کوچکترین و مقرونبهصرفهترین مدل گوگل است که برای استفاده در مقیاس وسیع طراحی شده است."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview کوچکترین و مقرونبهصرفهترین مدل گوگل است که برای استفاده در مقیاس بزرگ طراحی شده است."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Généré automatiquement",
|
||||
"copy": "Copier",
|
||||
"copyError": "Échec de la copie",
|
||||
"copySuccess": "Clé API copiée dans le presse-papiers",
|
||||
"enterPlaceholder": "Veuillez saisir",
|
||||
"hide": "Cacher",
|
||||
"neverExpires": "N'expire jamais",
|
||||
"neverUsed": "Jamais utilisé",
|
||||
"show": "Afficher"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Date d'expiration",
|
||||
"placeholder": "N'expire jamais"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nom",
|
||||
"placeholder": "Veuillez saisir le nom de la clé API"
|
||||
}
|
||||
},
|
||||
"submit": "Créer",
|
||||
"title": "Créer une clé API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Créer une clé API",
|
||||
"delete": "Supprimer",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Annuler",
|
||||
"ok": "Confirmer"
|
||||
},
|
||||
"content": "Confirmez-vous la suppression de cette clé API ?",
|
||||
"title": "Confirmation"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Actions",
|
||||
"expiresAt": "Date d'expiration",
|
||||
"key": "Clé",
|
||||
"lastUsedAt": "Dernière utilisation",
|
||||
"name": "Nom",
|
||||
"status": "Statut d'activation"
|
||||
},
|
||||
"title": "Liste des clés API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Ce champ est obligatoire"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Le mois dernier",
|
||||
"recent30Days": "Les 30 derniers jours"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Mots"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestion des clés API",
|
||||
"profile": "Profil",
|
||||
"security": "Sécurité",
|
||||
"stats": "Statistiques"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash est le modèle le plus rentable de Google, offrant des fonctionnalités complètes."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite est le modèle le plus petit et le plus rentable de Google, conçu pour une utilisation à grande échelle."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview est le modèle le plus compact et rentable de Google, conçu pour une utilisation à grande échelle."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Generato automaticamente",
|
||||
"copy": "Copia",
|
||||
"copyError": "Copia non riuscita",
|
||||
"copySuccess": "Chiave API copiata negli appunti",
|
||||
"enterPlaceholder": "Inserisci",
|
||||
"hide": "Nascondi",
|
||||
"neverExpires": "Non scade mai",
|
||||
"neverUsed": "Mai usato",
|
||||
"show": "Mostra"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Data di scadenza",
|
||||
"placeholder": "Non scade mai"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nome",
|
||||
"placeholder": "Inserisci il nome della Chiave API"
|
||||
}
|
||||
},
|
||||
"submit": "Crea",
|
||||
"title": "Crea Chiave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Crea Chiave API",
|
||||
"delete": "Elimina",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Annulla",
|
||||
"ok": "Conferma"
|
||||
},
|
||||
"content": "Sei sicuro di voler eliminare questa Chiave API?",
|
||||
"title": "Conferma azione"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Azioni",
|
||||
"expiresAt": "Data di scadenza",
|
||||
"key": "Chiave",
|
||||
"lastUsedAt": "Ultimo utilizzo",
|
||||
"name": "Nome",
|
||||
"status": "Stato attivo"
|
||||
},
|
||||
"title": "Elenco Chiavi API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Il contenuto non può essere vuoto"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Mese Scorso",
|
||||
"recent30Days": "Ultimi 30 Giorni"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Parole"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestione Chiavi API",
|
||||
"profile": "Profilo",
|
||||
"security": "Sicurezza",
|
||||
"stats": "Statistiche"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash è il modello Google con il miglior rapporto qualità-prezzo, offrendo funzionalità complete."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite è il modello più piccolo e conveniente di Google, progettato per un utilizzo su larga scala."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview è il modello Google più piccolo e con il miglior rapporto qualità-prezzo, progettato per un utilizzo su larga scala."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自動生成",
|
||||
"copy": "コピー",
|
||||
"copyError": "コピーに失敗しました",
|
||||
"copySuccess": "APIキーがクリップボードにコピーされました",
|
||||
"enterPlaceholder": "入力してください",
|
||||
"hide": "非表示",
|
||||
"neverExpires": "期限なし",
|
||||
"neverUsed": "未使用",
|
||||
"show": "表示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "有効期限",
|
||||
"placeholder": "期限なし"
|
||||
},
|
||||
"name": {
|
||||
"label": "名前",
|
||||
"placeholder": "APIキーの名前を入力してください"
|
||||
}
|
||||
},
|
||||
"submit": "作成",
|
||||
"title": "APIキーを作成"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "APIキーを作成",
|
||||
"delete": "削除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "キャンセル",
|
||||
"ok": "確認"
|
||||
},
|
||||
"content": "このAPIキーを削除してもよろしいですか?",
|
||||
"title": "操作の確認"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "有効期限",
|
||||
"key": "キー",
|
||||
"lastUsedAt": "最終使用日時",
|
||||
"name": "名前",
|
||||
"status": "有効状態"
|
||||
},
|
||||
"title": "APIキー一覧"
|
||||
},
|
||||
"validation": {
|
||||
"required": "内容を入力してください"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "先月",
|
||||
"recent30Days": "過去30日間"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "単語"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "APIキー管理",
|
||||
"profile": "プロフィール",
|
||||
"security": "セキュリティ",
|
||||
"stats": "統計"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 FlashはGoogleのコストパフォーマンスに優れたモデルで、包括的な機能を提供します。"
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite は、Google の中で最も小さく、コストパフォーマンスに優れたモデルであり、大規模な利用を目的に設計されています。"
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite PreviewはGoogleの最小かつコストパフォーマンスに優れたモデルで、大規模利用を目的に設計されています。"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "자동 생성",
|
||||
"copy": "복사",
|
||||
"copyError": "복사 실패",
|
||||
"copySuccess": "API 키가 클립보드에 복사되었습니다",
|
||||
"enterPlaceholder": "입력하세요",
|
||||
"hide": "숨기기",
|
||||
"neverExpires": "만료되지 않음",
|
||||
"neverUsed": "한 번도 사용되지 않음",
|
||||
"show": "표시"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "만료 시간",
|
||||
"placeholder": "만료되지 않음"
|
||||
},
|
||||
"name": {
|
||||
"label": "이름",
|
||||
"placeholder": "API 키 이름을 입력하세요"
|
||||
}
|
||||
},
|
||||
"submit": "생성",
|
||||
"title": "API 키 생성"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API 키 생성",
|
||||
"delete": "삭제",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "취소",
|
||||
"ok": "확인"
|
||||
},
|
||||
"content": "이 API 키를 삭제하시겠습니까?",
|
||||
"title": "작업 확인"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "작업",
|
||||
"expiresAt": "만료 시간",
|
||||
"key": "키",
|
||||
"lastUsedAt": "마지막 사용 시간",
|
||||
"name": "이름",
|
||||
"status": "활성 상태"
|
||||
},
|
||||
"title": "API 키 목록"
|
||||
},
|
||||
"validation": {
|
||||
"required": "내용을 비워둘 수 없습니다"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "지난 달",
|
||||
"recent30Days": "최근 30일"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "단어"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API 키 관리",
|
||||
"profile": "프로필",
|
||||
"security": "보안",
|
||||
"stats": "통계"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash는 구글에서 가장 가성비가 뛰어난 모델로, 포괄적인 기능을 제공합니다."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite는 Google의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview는 구글의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Automatisch gegenereerd",
|
||||
"copy": "Kopiëren",
|
||||
"copyError": "Kopiëren mislukt",
|
||||
"copySuccess": "API-sleutel is gekopieerd naar het klembord",
|
||||
"enterPlaceholder": "Voer in",
|
||||
"hide": "Verbergen",
|
||||
"neverExpires": "Verloopt nooit",
|
||||
"neverUsed": "Nooit gebruikt",
|
||||
"show": "Weergeven"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Vervaldatum",
|
||||
"placeholder": "Verloopt nooit"
|
||||
},
|
||||
"name": {
|
||||
"label": "Naam",
|
||||
"placeholder": "Voer de naam van de API-sleutel in"
|
||||
}
|
||||
},
|
||||
"submit": "Aanmaken",
|
||||
"title": "API-sleutel aanmaken"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API-sleutel aanmaken",
|
||||
"delete": "Verwijderen",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Annuleren",
|
||||
"ok": "Bevestigen"
|
||||
},
|
||||
"content": "Weet u zeker dat u deze API-sleutel wilt verwijderen?",
|
||||
"title": "Bevestig actie"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Acties",
|
||||
"expiresAt": "Vervaldatum",
|
||||
"key": "Sleutel",
|
||||
"lastUsedAt": "Laatst gebruikt",
|
||||
"name": "Naam",
|
||||
"status": "Status"
|
||||
},
|
||||
"title": "API-sleutellijst"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Inhoud mag niet leeg zijn"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Vorige maand",
|
||||
"recent30Days": "Laatste 30 dagen"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Woorden"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API-sleutelbeheer",
|
||||
"profile": "Profiel",
|
||||
"security": "Beveiliging",
|
||||
"stats": "Statistieken"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash is het meest kosteneffectieve model van Google en biedt uitgebreide functionaliteiten."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite is het kleinste en meest kosteneffectieve model van Google, speciaal ontworpen voor grootschalig gebruik."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview is het kleinste en meest kosteneffectieve model van Google, speciaal ontworpen voor grootschalig gebruik."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Automatycznie wygenerowany",
|
||||
"copy": "Kopiuj",
|
||||
"copyError": "Kopiowanie nie powiodło się",
|
||||
"copySuccess": "Klucz API został skopiowany do schowka",
|
||||
"enterPlaceholder": "Wpisz",
|
||||
"hide": "Ukryj",
|
||||
"neverExpires": "Nigdy nie wygasa",
|
||||
"neverUsed": "Nigdy nie używany",
|
||||
"show": "Pokaż"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Data wygaśnięcia",
|
||||
"placeholder": "Nigdy nie wygasa"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nazwa",
|
||||
"placeholder": "Wpisz nazwę klucza API"
|
||||
}
|
||||
},
|
||||
"submit": "Utwórz",
|
||||
"title": "Utwórz klucz API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Utwórz klucz API",
|
||||
"delete": "Usuń",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Anuluj",
|
||||
"ok": "Potwierdź"
|
||||
},
|
||||
"content": "Czy na pewno chcesz usunąć ten klucz API?",
|
||||
"title": "Potwierdź operację"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Akcje",
|
||||
"expiresAt": "Data wygaśnięcia",
|
||||
"key": "Klucz",
|
||||
"lastUsedAt": "Ostatnie użycie",
|
||||
"name": "Nazwa",
|
||||
"status": "Status aktywacji"
|
||||
},
|
||||
"title": "Lista kluczy API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Pole nie może być puste"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Poprzedni miesiąc",
|
||||
"recent30Days": "Ostatnie 30 dni"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Słowa"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Zarządzanie kluczami API",
|
||||
"profile": "Profil",
|
||||
"security": "Bezpieczeństwo",
|
||||
"stats": "Statystyki"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash to najbardziej opłacalny model Google, oferujący wszechstronne funkcje."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite to najmniejszy i najbardziej opłacalny model Google, zaprojektowany z myślą o szerokim zastosowaniu."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview to najmniejszy i najbardziej opłacalny model Google, zaprojektowany z myślą o masowym zastosowaniu."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Gerado automaticamente",
|
||||
"copy": "Copiar",
|
||||
"copyError": "Falha ao copiar",
|
||||
"copySuccess": "Chave API copiada para a área de transferência",
|
||||
"enterPlaceholder": "Por favor, insira",
|
||||
"hide": "Ocultar",
|
||||
"neverExpires": "Nunca expira",
|
||||
"neverUsed": "Nunca usado",
|
||||
"show": "Mostrar"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Data de expiração",
|
||||
"placeholder": "Nunca expira"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nome",
|
||||
"placeholder": "Por favor, insira o nome da Chave API"
|
||||
}
|
||||
},
|
||||
"submit": "Criar",
|
||||
"title": "Criar Chave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Criar Chave API",
|
||||
"delete": "Excluir",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Confirmar"
|
||||
},
|
||||
"content": "Tem certeza de que deseja excluir esta Chave API?",
|
||||
"title": "Confirmar ação"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Ações",
|
||||
"expiresAt": "Data de expiração",
|
||||
"key": "Chave",
|
||||
"lastUsedAt": "Último uso",
|
||||
"name": "Nome",
|
||||
"status": "Status de ativação"
|
||||
},
|
||||
"title": "Lista de Chaves API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "O conteúdo não pode estar vazio"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Último Mês",
|
||||
"recent30Days": "Últimos 30 Dias"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Palavras"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gerenciamento de Chave API",
|
||||
"profile": "Perfil",
|
||||
"security": "Segurança",
|
||||
"stats": "Estatísticas"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash é o modelo com melhor custo-benefício do Google, oferecendo funcionalidades abrangentes."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite é o modelo mais compacto e com melhor custo-benefício do Google, projetado para uso em larga escala."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview é o modelo mais compacto e com melhor custo-benefício do Google, projetado para uso em larga escala."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Автоматически сгенерировано",
|
||||
"copy": "Копировать",
|
||||
"copyError": "Ошибка копирования",
|
||||
"copySuccess": "API ключ скопирован в буфер обмена",
|
||||
"enterPlaceholder": "Пожалуйста, введите",
|
||||
"hide": "Скрыть",
|
||||
"neverExpires": "Никогда не истекает",
|
||||
"neverUsed": "Никогда не использовался",
|
||||
"show": "Показать"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Срок действия",
|
||||
"placeholder": "Никогда не истекает"
|
||||
},
|
||||
"name": {
|
||||
"label": "Название",
|
||||
"placeholder": "Пожалуйста, введите название API ключа"
|
||||
}
|
||||
},
|
||||
"submit": "Создать",
|
||||
"title": "Создать API ключ"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Создать API ключ",
|
||||
"delete": "Удалить",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Отмена",
|
||||
"ok": "Подтвердить"
|
||||
},
|
||||
"content": "Вы уверены, что хотите удалить этот API ключ?",
|
||||
"title": "Подтверждение действия"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Действия",
|
||||
"expiresAt": "Срок действия",
|
||||
"key": "Ключ",
|
||||
"lastUsedAt": "Последнее использование",
|
||||
"name": "Название",
|
||||
"status": "Статус активации"
|
||||
},
|
||||
"title": "Список API ключей"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Поле не может быть пустым"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Прошлый месяц",
|
||||
"recent30Days": "Последние 30 дней"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Слова"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Управление API ключами",
|
||||
"profile": "Профиль",
|
||||
"security": "Безопасность",
|
||||
"stats": "Статистика"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash — самая экономичная модель Google, предоставляющая полный набор функций."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite — это самая компактная и экономичная модель от Google, разработанная для масштабного использования."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview — самая компактная и экономичная модель Google, разработанная для масштабного использования."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Otomatik Oluşturuldu",
|
||||
"copy": "Kopyala",
|
||||
"copyError": "Kopyalama Başarısız",
|
||||
"copySuccess": "API Anahtarı panoya kopyalandı",
|
||||
"enterPlaceholder": "Lütfen giriniz",
|
||||
"hide": "Gizle",
|
||||
"neverExpires": "Asla Süresi Dolmaz",
|
||||
"neverUsed": "Hiç Kullanılmadı",
|
||||
"show": "Göster"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Son Kullanma Tarihi",
|
||||
"placeholder": "Asla Süresi Dolmaz"
|
||||
},
|
||||
"name": {
|
||||
"label": "Ad",
|
||||
"placeholder": "Lütfen API Anahtarı adını giriniz"
|
||||
}
|
||||
},
|
||||
"submit": "Oluştur",
|
||||
"title": "API Anahtarı Oluştur"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API Anahtarı Oluştur",
|
||||
"delete": "Sil",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "İptal",
|
||||
"ok": "Onayla"
|
||||
},
|
||||
"content": "Bu API Anahtarını silmek istediğinize emin misiniz?",
|
||||
"title": "İşlemi Onayla"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "İşlemler",
|
||||
"expiresAt": "Son Kullanma Tarihi",
|
||||
"key": "Anahtar",
|
||||
"lastUsedAt": "Son Kullanım Tarihi",
|
||||
"name": "Ad",
|
||||
"status": "Etkinlik Durumu"
|
||||
},
|
||||
"title": "API Anahtarı Listesi"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Bu alan boş bırakılamaz"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Geçen Ay",
|
||||
"recent30Days": "Son 30 Gün"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Toplam kelime sayısı"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Anahtarı Yönetimi",
|
||||
"profile": "Profil",
|
||||
"security": "Güvenlik",
|
||||
"stats": "İstatistikler"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash, Google'ın en yüksek maliyet-performans modelidir ve kapsamlı özellikler sunar."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite, Google'ın en küçük ve en uygun maliyetli modeli olup, geniş çaplı kullanım için tasarlanmıştır."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Önizlemesi, Google'ın en küçük ve en yüksek maliyet-performans modelidir ve büyük ölçekli kullanım için tasarlanmıştır."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Tự động tạo",
|
||||
"copy": "Sao chép",
|
||||
"copyError": "Sao chép thất bại",
|
||||
"copySuccess": "API Key đã được sao chép vào bộ nhớ tạm",
|
||||
"enterPlaceholder": "Vui lòng nhập",
|
||||
"hide": "Ẩn",
|
||||
"neverExpires": "Không bao giờ hết hạn",
|
||||
"neverUsed": "Chưa từng sử dụng",
|
||||
"show": "Hiển thị"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Thời gian hết hạn",
|
||||
"placeholder": "Không bao giờ hết hạn"
|
||||
},
|
||||
"name": {
|
||||
"label": "Tên",
|
||||
"placeholder": "Vui lòng nhập tên API Key"
|
||||
}
|
||||
},
|
||||
"submit": "Tạo",
|
||||
"title": "Tạo API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Tạo API Key",
|
||||
"delete": "Xóa",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Hủy",
|
||||
"ok": "Xác nhận"
|
||||
},
|
||||
"content": "Bạn có chắc chắn muốn xóa API Key này không?",
|
||||
"title": "Xác nhận thao tác"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Thao tác",
|
||||
"expiresAt": "Thời gian hết hạn",
|
||||
"key": "Khóa",
|
||||
"lastUsedAt": "Lần sử dụng cuối",
|
||||
"name": "Tên",
|
||||
"status": "Trạng thái kích hoạt"
|
||||
},
|
||||
"title": "Danh sách API Key"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Nội dung không được để trống"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Tháng trước",
|
||||
"recent30Days": "30 ngày qua"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Từ"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Quản lý API Key",
|
||||
"profile": "Hồ sơ",
|
||||
"security": "Bảo mật",
|
||||
"stats": "Thống kê"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash là mô hình có hiệu suất chi phí tốt nhất của Google, cung cấp đầy đủ các chức năng."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite là mô hình nhỏ nhất và có hiệu suất chi phí tốt nhất của Google, được thiết kế dành cho việc sử dụng quy mô lớn."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview là mô hình nhỏ nhất và có hiệu suất chi phí tốt nhất của Google, được thiết kế dành cho sử dụng quy mô lớn."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自动生成",
|
||||
"copy": "复制",
|
||||
"copyError": "复制失败",
|
||||
"copySuccess": "API Key 已复制到剪贴板",
|
||||
"enterPlaceholder": "请输入",
|
||||
"hide": "隐藏",
|
||||
"neverExpires": "永不过期",
|
||||
"neverUsed": "从未使用",
|
||||
"show": "显示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "过期时间",
|
||||
"placeholder": "永不过期"
|
||||
},
|
||||
"name": {
|
||||
"label": "名称",
|
||||
"placeholder": "请输入 API Key 名称"
|
||||
}
|
||||
},
|
||||
"submit": "创建",
|
||||
"title": "创建 API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "创建 API Key",
|
||||
"delete": "删除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"ok": "确认"
|
||||
},
|
||||
"content": "确认删除该 API Key 吗?",
|
||||
"title": "确认操作"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "过期时间",
|
||||
"key": "密钥",
|
||||
"lastUsedAt": "最后使用时间",
|
||||
"name": "名称",
|
||||
"status": "启用状态"
|
||||
},
|
||||
"title": "API Key 列表"
|
||||
},
|
||||
"validation": {
|
||||
"required": "内容不得为空"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "上个月",
|
||||
"recent30Days": "最近30天"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "累计字数"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key 管理",
|
||||
"profile": "个人资料",
|
||||
"security": "安全",
|
||||
"stats": "数据统计"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash 是 Google 性价比最高的模型,提供全面的功能。"
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。"
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview 是 Google 最小、性价比最高的模型,专为大规模使用而设计。"
|
||||
},
|
||||
|
||||
@@ -535,6 +535,7 @@
|
||||
"experiment": "实验",
|
||||
"hotkey": "快捷键",
|
||||
"llm": "语言模型",
|
||||
"plugin": "插件管理",
|
||||
"provider": "AI 服务商",
|
||||
"proxy": "网络代理",
|
||||
"storage": "数据存储",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自動生成",
|
||||
"copy": "複製",
|
||||
"copyError": "複製失敗",
|
||||
"copySuccess": "API Key 已複製到剪貼簿",
|
||||
"enterPlaceholder": "請輸入",
|
||||
"hide": "隱藏",
|
||||
"neverExpires": "永不過期",
|
||||
"neverUsed": "從未使用",
|
||||
"show": "顯示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "過期時間",
|
||||
"placeholder": "永不過期"
|
||||
},
|
||||
"name": {
|
||||
"label": "名稱",
|
||||
"placeholder": "請輸入 API Key 名稱"
|
||||
}
|
||||
},
|
||||
"submit": "建立",
|
||||
"title": "建立 API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "建立 API Key",
|
||||
"delete": "刪除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"ok": "確認"
|
||||
},
|
||||
"content": "確認刪除該 API Key 嗎?",
|
||||
"title": "確認操作"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "過期時間",
|
||||
"key": "密鑰",
|
||||
"lastUsedAt": "最後使用時間",
|
||||
"name": "名稱",
|
||||
"status": "啟用狀態"
|
||||
},
|
||||
"title": "API Key 列表"
|
||||
},
|
||||
"validation": {
|
||||
"required": "內容不得為空"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "上個月",
|
||||
"recent30Days": "最近30天"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "總字數"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key 管理",
|
||||
"profile": "個人資料",
|
||||
"security": "安全",
|
||||
"stats": "數據統計"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash 是 Google 性價比最高的模型,提供全面的功能。"
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite 是 Google 最小、性價比最高的模型,專為大規模使用而設計。"
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview 是 Google 最小、性價比最高的模型,專為大規模使用而設計。"
|
||||
},
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/chat",
|
||||
"version": "1.104.2",
|
||||
"version": "1.105.1",
|
||||
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
@@ -153,7 +153,7 @@
|
||||
"@lobehub/icons": "^2.17.0",
|
||||
"@lobehub/market-sdk": "^0.22.7",
|
||||
"@lobehub/tts": "^2.0.1",
|
||||
"@lobehub/ui": "^2.7.4",
|
||||
"@lobehub/ui": "^2.7.5",
|
||||
"@modelcontextprotocol/sdk": "^1.16.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@next/third-parties": "^15.4.3",
|
||||
@@ -196,7 +196,7 @@
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"immer": "^10.1.1",
|
||||
"jose": "^5.10.0",
|
||||
"jose": "^6.0.12",
|
||||
"js-sha256": "^0.11.1",
|
||||
"jsonl-parse-stringify": "^1.0.3",
|
||||
"keyv": "^4.5.4",
|
||||
@@ -241,7 +241,7 @@
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hotkeys-hook": "^5.1.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-layout-kit": "^1.9.2",
|
||||
"react-layout-kit": "^2.0.0",
|
||||
"react-lazy-load": "^4.0.1",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-rnd": "^10.5.2",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AgentRuntimeError } from '@/libs/model-runtime';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { RequestHandler, checkAuth } from './index';
|
||||
import { checkAuthMethod } from './utils';
|
||||
@@ -20,8 +20,8 @@ vi.mock('./utils', () => ({
|
||||
checkAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn(),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('checkAuth', () => {
|
||||
@@ -50,7 +50,7 @@ describe('checkAuth', () => {
|
||||
it('should return error response on getJWTPayload error', async () => {
|
||||
const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
mockRequest.headers.set('Authorization', 'invalid');
|
||||
vi.mocked(getJWTPayload).mockRejectedValueOnce(mockError);
|
||||
vi.mocked(getXorPayload).mockRejectedValueOnce(mockError);
|
||||
|
||||
await checkAuth(mockHandler)(mockRequest, mockOptions);
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('checkAuth', () => {
|
||||
it('should return error response on checkAuthMethod error', async () => {
|
||||
const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
mockRequest.headers.set('Authorization', 'valid');
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({});
|
||||
vi.mocked(getXorPayload).mockResolvedValueOnce({});
|
||||
vi.mocked(checkAuthMethod).mockImplementationOnce(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AuthObject } from '@clerk/backend';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
JWTPayload,
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
LOBE_CHAT_OIDC_AUTH_HEADER,
|
||||
OAUTH_AUTHORIZED,
|
||||
@@ -13,18 +13,18 @@ import { AgentRuntime, AgentRuntimeError, ChatCompletionErrorPayload } from '@/l
|
||||
import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { checkAuthMethod } from './utils';
|
||||
|
||||
type CreateRuntime = (jwtPayload: JWTPayload) => AgentRuntime;
|
||||
type CreateRuntime = (jwtPayload: ClientSecretPayload) => AgentRuntime;
|
||||
type RequestOptions = { createRuntime?: CreateRuntime; params: Promise<{ provider: string }> };
|
||||
|
||||
export type RequestHandler = (
|
||||
req: Request,
|
||||
options: RequestOptions & {
|
||||
createRuntime?: CreateRuntime;
|
||||
jwtPayload: JWTPayload;
|
||||
jwtPayload: ClientSecretPayload;
|
||||
},
|
||||
) => Promise<Response>;
|
||||
|
||||
@@ -36,7 +36,7 @@ export const checkAuth =
|
||||
return handler(req, { ...options, jwtPayload: { userId: 'DEV_USER' } });
|
||||
}
|
||||
|
||||
let jwtPayload: JWTPayload;
|
||||
let jwtPayload: ClientSecretPayload;
|
||||
|
||||
try {
|
||||
// get Authorization from header
|
||||
@@ -55,7 +55,7 @@ export const checkAuth =
|
||||
clerkAuth = data.clerkAuth;
|
||||
}
|
||||
|
||||
jwtPayload = await getJWTPayload(authorization);
|
||||
jwtPayload = getXorPayload(authorization);
|
||||
|
||||
const oidcAuthorization = req.headers.get(LOBE_CHAT_OIDC_AUTH_HEADER);
|
||||
let isUseOidcAuth = false;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { checkAuthMethod } from '@/app/(backend)/middleware/auth/utils';
|
||||
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
|
||||
import { AgentRuntime, LobeRuntimeAI } from '@/libs/model-runtime';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { POST } from './route';
|
||||
|
||||
@@ -18,8 +18,8 @@ vi.mock('@/app/(backend)/middleware/auth/utils', () => ({
|
||||
checkAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn(),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn(),
|
||||
}));
|
||||
|
||||
// 定义一个变量来存储 enableAuth 的值
|
||||
@@ -61,7 +61,7 @@ describe('POST handler', () => {
|
||||
const mockParams = Promise.resolve({ provider: 'test-provider' });
|
||||
|
||||
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -78,7 +78,7 @@ describe('POST handler', () => {
|
||||
await POST(request as unknown as Request, { params: mockParams });
|
||||
|
||||
// 验证是否正确调用了模拟函数
|
||||
expect(getJWTPayload).toHaveBeenCalledWith('Bearer some-valid-token');
|
||||
expect(getXorPayload).toHaveBeenCalledWith('Bearer some-valid-token');
|
||||
expect(spy).toHaveBeenCalledWith('test-provider', expect.anything());
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('POST handler', () => {
|
||||
it('should have pass clerk Auth when enable clerk', async () => {
|
||||
enableClerk = true;
|
||||
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -142,7 +142,9 @@ describe('POST handler', () => {
|
||||
|
||||
it('should return InternalServerError error when throw a unknown error', async () => {
|
||||
const mockParams = Promise.resolve({ provider: 'test-provider' });
|
||||
vi.mocked(getJWTPayload).mockRejectedValueOnce(new Error('unknown error'));
|
||||
vi.mocked(getXorPayload).mockImplementationOnce(() => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
|
||||
const response = await POST(request, { params: mockParams });
|
||||
|
||||
@@ -159,7 +161,7 @@ describe('POST handler', () => {
|
||||
|
||||
describe('chat', () => {
|
||||
it('should correctly handle chat completion with valid payload', async () => {
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -189,7 +191,7 @@ describe('POST handler', () => {
|
||||
|
||||
it('should return an error response when chat completion fails', async () => {
|
||||
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AgentRuntimeError } from '@/libs/model-runtime';
|
||||
import { TraceClient } from '@/libs/traces';
|
||||
import { ChatErrorType, ErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
import { getTracePayload } from '@/utils/trace';
|
||||
|
||||
import { parserPluginSettings } from './settings';
|
||||
@@ -44,7 +44,7 @@ export const POST = async (req: Request) => {
|
||||
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
|
||||
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
||||
const payload = await getJWTPayload(authorization);
|
||||
const payload = getXorPayload(authorization);
|
||||
|
||||
const result = checkAuth(payload.accessCode!, oauthAuthorized);
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Popconfirm, Switch } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Trash } from 'lucide-react';
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { ApiKeyItem, CreateApiKeyParams, UpdateApiKeyParams } from '@/types/apiKey';
|
||||
|
||||
import { ApiKeyDisplay, ApiKeyModal, EditableCell } from './features';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
container: css`
|
||||
.ant-pro-card-body {
|
||||
padding-inline: 0;
|
||||
|
||||
.ant-pro-table-list-toolbar-container {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: ${token.margin}px;
|
||||
`,
|
||||
table: css`
|
||||
border-radius: ${token.borderRadius}px;
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Client: FC = () => {
|
||||
const { styles } = useStyles();
|
||||
const { t } = useTranslation('auth');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const actionRef = useRef<ActionType>(null);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (params: CreateApiKeyParams) => lambdaClient.apiKey.createApiKey.mutate(params),
|
||||
onSuccess: () => {
|
||||
actionRef.current?.reload();
|
||||
setModalOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, params }: { id: number; params: UpdateApiKeyParams }) =>
|
||||
lambdaClient.apiKey.updateApiKey.mutate({ id, value: params }),
|
||||
onSuccess: () => {
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => lambdaClient.apiKey.deleteApiKey.mutate({ id }),
|
||||
onSuccess: () => {
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalOk = (values: CreateApiKeyParams) => {
|
||||
createMutation.mutate(values);
|
||||
};
|
||||
|
||||
const columns: ProColumns<ApiKeyItem>[] = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (_, apiKey) => (
|
||||
<EditableCell
|
||||
onSubmit={(name) => {
|
||||
if (!name || name === apiKey.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutate({ id: apiKey.id!, params: { name: name as string } });
|
||||
}}
|
||||
placeholder={t('apikey.display.enterPlaceholder')}
|
||||
type="text"
|
||||
value={apiKey.name}
|
||||
/>
|
||||
),
|
||||
title: t('apikey.list.columns.name'),
|
||||
},
|
||||
{
|
||||
dataIndex: 'key',
|
||||
ellipsis: true,
|
||||
key: 'key',
|
||||
render: (_, apiKey) => <ApiKeyDisplay apiKey={apiKey.key} />,
|
||||
title: t('apikey.list.columns.key'),
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (_, apiKey: ApiKeyItem) => (
|
||||
<Switch
|
||||
checked={!!apiKey.enabled}
|
||||
onChange={(checked) => {
|
||||
updateMutation.mutate({ id: apiKey.id!, params: { enabled: checked } });
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: t('apikey.list.columns.status'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataIndex: 'expiresAt',
|
||||
key: 'expiresAt',
|
||||
render: (_, apiKey) => (
|
||||
<EditableCell
|
||||
onSubmit={(expiresAt) => {
|
||||
if (expiresAt === apiKey.expiresAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
id: apiKey.id!,
|
||||
params: { expiresAt: expiresAt ? new Date(expiresAt as string) : null },
|
||||
});
|
||||
}}
|
||||
placeholder={t('apikey.display.neverExpires')}
|
||||
type="date"
|
||||
value={apiKey.expiresAt?.toLocaleString() || t('apikey.display.neverExpires')}
|
||||
/>
|
||||
),
|
||||
title: t('apikey.list.columns.expiresAt'),
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastUsedAt',
|
||||
key: 'lastUsedAt',
|
||||
renderText: (_, apiKey: ApiKeyItem) =>
|
||||
apiKey.lastUsedAt?.toLocaleString() || t('apikey.display.neverUsed'),
|
||||
title: t('apikey.list.columns.lastUsedAt'),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
render: (_: any, apiKey: ApiKeyItem) => (
|
||||
<Popconfirm
|
||||
cancelText={t('apikey.list.actions.deleteConfirm.actions.cancel')}
|
||||
description={t('apikey.list.actions.deleteConfirm.content')}
|
||||
okText={t('apikey.list.actions.deleteConfirm.actions.ok')}
|
||||
onConfirm={() => deleteMutation.mutate(apiKey.id!)}
|
||||
title={t('apikey.list.actions.deleteConfirm.title')}
|
||||
>
|
||||
<Button
|
||||
icon={Trash}
|
||||
size="small"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
title={t('apikey.list.actions.delete')}
|
||||
type="text"
|
||||
/>
|
||||
</Popconfirm>
|
||||
),
|
||||
title: t('apikey.list.columns.actions'),
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
className={styles.table}
|
||||
columns={columns}
|
||||
headerTitle={t('apikey.list.title')}
|
||||
options={false}
|
||||
pagination={false}
|
||||
request={async () => {
|
||||
const apiKeys = await lambdaClient.apiKey.getApiKeys.query();
|
||||
|
||||
return {
|
||||
data: apiKeys,
|
||||
success: true,
|
||||
};
|
||||
}}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolbar={{
|
||||
actions: [
|
||||
<Button key="create" onClick={handleCreate} type="primary">
|
||||
{t('apikey.list.actions.create')}
|
||||
</Button>,
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ApiKeyModal
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={handleModalOk}
|
||||
open={modalOpen}
|
||||
submitLoading={createMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Client;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DatePicker } from '@lobehub/ui';
|
||||
import { DatePickerProps, Flex } from 'antd';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ApiKeyDatePickerProps extends Omit<DatePickerProps, 'onChange'> {
|
||||
onChange?: (date: Dayjs | null) => void;
|
||||
}
|
||||
|
||||
const ApiKeyDatePicker: FC<ApiKeyDatePickerProps> = ({ value, onChange, ...props }) => {
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
const handleOnChange = (date: Dayjs | null) => {
|
||||
// 如果选择了日期,设置为当天的 23:59:59
|
||||
const submitData = date ? date.hour(23).minute(59).second(59).millisecond(999) : null;
|
||||
|
||||
onChange?.(submitData);
|
||||
};
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
key={value?.valueOf() || 'EMPTY'}
|
||||
value={value}
|
||||
{...props}
|
||||
minDate={dayjs()}
|
||||
onChange={handleOnChange}
|
||||
placeholder={t('apikey.form.fields.expiresAt.placeholder')}
|
||||
renderExtraFooter={() => (
|
||||
<Flex justify="center">
|
||||
<a onClick={() => handleOnChange(null)}>{t('apikey.display.neverExpires')}</a>
|
||||
</Flex>
|
||||
)}
|
||||
showNow={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyDatePicker;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { App, Flex } from 'antd';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ApiKeyDisplayProps {
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
const ApiKeyDisplay: FC<ApiKeyDisplayProps> = ({ apiKey }) => {
|
||||
const { t } = useTranslation('auth');
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { message } = App.useApp();
|
||||
|
||||
const toggleVisibility = () => {
|
||||
setIsVisible(!isVisible);
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!apiKey) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(apiKey);
|
||||
message.success(t('apikey.display.copySuccess'));
|
||||
} catch {
|
||||
message.error(t('apikey.display.copyError'));
|
||||
}
|
||||
};
|
||||
|
||||
const displayValue = apiKey && (isVisible ? apiKey : `lb-${'*'.repeat(apiKey.length - 2)}`);
|
||||
|
||||
if (!apiKey) {
|
||||
return t('apikey.display.autoGenerated');
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex align="center" gap={8}>
|
||||
<span style={{ fontSize: '14px' }}>{displayValue}</span>
|
||||
<Flex>
|
||||
<Button
|
||||
icon={isVisible ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
onClick={toggleVisibility}
|
||||
size="small"
|
||||
title={isVisible ? t('apikey.display.hide') : t('apikey.display.show')}
|
||||
type="text"
|
||||
/>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
size="small"
|
||||
title={t('apikey.display.copy')}
|
||||
type="text"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyDisplay;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FormModal, Input } from '@lobehub/ui';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CreateApiKeyParams } from '@/types/apiKey';
|
||||
|
||||
import ApiKeyDatePicker from '../ApiKeyDatePicker';
|
||||
|
||||
interface ApiKeyModalProps {
|
||||
onCancel: () => void;
|
||||
onOk: (values: CreateApiKeyParams) => void;
|
||||
open: boolean;
|
||||
submitLoading?: boolean;
|
||||
}
|
||||
|
||||
type FormValues = Omit<CreateApiKeyParams, 'expiresAt'> & {
|
||||
expiresAt: Dayjs | null;
|
||||
};
|
||||
|
||||
const ApiKeyModal: FC<ApiKeyModalProps> = ({ open, onCancel, onOk, submitLoading }) => {
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
destroyOnHidden
|
||||
height={'90%'}
|
||||
itemMinWidth={'max(30%,240px)'}
|
||||
items={[
|
||||
{
|
||||
children: <Input placeholder={t('apikey.form.fields.name.placeholder')} />,
|
||||
label: t('apikey.form.fields.name.label'),
|
||||
name: 'name',
|
||||
rules: [{ required: true }],
|
||||
},
|
||||
{
|
||||
children: <ApiKeyDatePicker style={{ width: '100%' }} />,
|
||||
label: t('apikey.form.fields.expiresAt.label'),
|
||||
name: 'expiresAt',
|
||||
},
|
||||
]}
|
||||
itemsType={'flat'}
|
||||
onCancel={onCancel}
|
||||
onFinish={(values: FormValues) => {
|
||||
onOk({
|
||||
...values,
|
||||
expiresAt: values.expiresAt ? values.expiresAt.toDate() : null,
|
||||
} satisfies CreateApiKeyParams);
|
||||
}}
|
||||
open={open}
|
||||
submitLoading={submitLoading}
|
||||
submitText={t('apikey.form.submit')}
|
||||
title={t('apikey.form.title')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyModal;
|
||||
@@ -0,0 +1,223 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, Input } from '@lobehub/ui';
|
||||
import { App, InputRef } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { Check, Edit, X } from 'lucide-react';
|
||||
import React, { memo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ApiKeyDatePicker from '../ApiKeyDatePicker';
|
||||
|
||||
// 内容类型定义
|
||||
export type ContentType = 'text' | 'date';
|
||||
|
||||
// 组件Props接口定义
|
||||
export interface EditableCellProps {
|
||||
/** 是否禁用编辑 */
|
||||
disabled?: boolean;
|
||||
/** 提交回调函数 */
|
||||
onSubmit: (value: string | Date | null) => void;
|
||||
/** 占位符文本 */
|
||||
placeholder?: string;
|
||||
/** 内容类型 */
|
||||
type: ContentType;
|
||||
/** 从数据库中查出的值,不管是什么类型,存进去的都是字符串 */
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
// 样式定义
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
actionButtons: css`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
`,
|
||||
container: css`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
min-height: 32px;
|
||||
|
||||
&:hover .edit-button {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
content: css`
|
||||
min-width: 0;
|
||||
line-height: 1.5;
|
||||
color: ${token.colorText};
|
||||
word-break: break-all;
|
||||
`,
|
||||
editButton: css`
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&.edit-button {
|
||||
opacity: 0;
|
||||
}
|
||||
`,
|
||||
editingContainer: css`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
`,
|
||||
inputWrapper: css`
|
||||
flex: 1;
|
||||
`,
|
||||
textareaWrapper: css`
|
||||
flex: 1;
|
||||
`,
|
||||
}));
|
||||
|
||||
// 主组件实现
|
||||
const EditableCell = memo<EditableCellProps>(
|
||||
({ value, type, onSubmit, placeholder, disabled = false }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const { t } = useTranslation('auth');
|
||||
const { message } = App.useApp();
|
||||
|
||||
// 编辑状态管理
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
// 用于Input的ref
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
// 格式化显示值
|
||||
const formatDisplayValue = (val: string | null) => {
|
||||
if (type === 'date' && val) {
|
||||
const date = dayjs(val);
|
||||
|
||||
return date.isValid() ? date.format('YYYY-MM-DD') : val || placeholder || '';
|
||||
}
|
||||
|
||||
return val || placeholder || '';
|
||||
};
|
||||
|
||||
// 开始编辑
|
||||
const handleEdit = () => {
|
||||
if (disabled) return;
|
||||
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
// 提交编辑
|
||||
const handleSubmit = () => {
|
||||
if (type === 'text') {
|
||||
const inputValue = inputRef.current?.input?.value;
|
||||
|
||||
if (!inputValue) {
|
||||
message.warning(t('apikey.validation.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(inputValue);
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
// 输入框组件的键盘事件处理
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
// 日期选择器提交
|
||||
const handleDatePickerSubmit = (date: Dayjs | null) => {
|
||||
onSubmit(date && dayjs(date).toISOString());
|
||||
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
// 渲染编辑模式
|
||||
const renderEditMode = () => {
|
||||
switch (type) {
|
||||
case 'text': {
|
||||
return (
|
||||
<div className={styles.inputWrapper}>
|
||||
<Input
|
||||
autoFocus
|
||||
defaultValue={value as string}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'date': {
|
||||
const dateValue = value && dayjs(value).isValid() ? dayjs(value) : null;
|
||||
|
||||
return (
|
||||
<ApiKeyDatePicker
|
||||
defaultValue={dateValue}
|
||||
onChange={handleDatePickerSubmit}
|
||||
onOpenChange={() => {
|
||||
if (isEditing) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
open={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 文本类型的编辑模式,展示保存和取消按钮
|
||||
if (type === 'text' && isEditing) {
|
||||
return (
|
||||
<div className={styles.editingContainer}>
|
||||
{renderEditMode()}
|
||||
<div className={styles.actionButtons}>
|
||||
<ActionIcon icon={Check} onClick={handleSubmit} size="small" />
|
||||
<ActionIcon icon={X} onClick={handleCancel} size="small" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 日期类型的编辑模式,展示日期选择器
|
||||
if (type === 'date' && isEditing) {
|
||||
return renderEditMode();
|
||||
}
|
||||
|
||||
// 展示模式
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>{formatDisplayValue(value)}</div>
|
||||
<ActionIcon
|
||||
className={cx(styles.editButton, 'edit-button')}
|
||||
icon={Edit}
|
||||
onClick={handleEdit}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
EditableCell.displayName = 'EditableCell';
|
||||
|
||||
export default EditableCell;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as ApiKeyDisplay } from './ApiKeyDisplay';
|
||||
export { default as ApiKeyModal } from './ApiKeyModal';
|
||||
export { default as EditableCell } from './EditableCell';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { serverFeatureFlags } from '@/config/featureFlags';
|
||||
import { metadataModule } from '@/server/metadata';
|
||||
import { translation } from '@/server/translation';
|
||||
import { DynamicLayoutProps } from '@/types/next';
|
||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
||||
|
||||
import Page from '../../settings/system-agent';
|
||||
import Client from './Client';
|
||||
|
||||
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
||||
const locale = await RouteVariants.getLocale(props);
|
||||
const { t } = await translation('auth', locale);
|
||||
return metadataModule.generate({
|
||||
description: t('header.desc'),
|
||||
title: t('tab.apikey'),
|
||||
url: '/profile/apikey',
|
||||
});
|
||||
};
|
||||
|
||||
const page = () => {
|
||||
const { showApiKeyManage } = serverFeatureFlags();
|
||||
|
||||
if (!showApiKeyManage) return notFound();
|
||||
|
||||
return <Client />;
|
||||
};
|
||||
|
||||
Page.displayName = 'ApiKey';
|
||||
|
||||
export default page;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { ChartColumnBigIcon, ShieldCheck, UserCircle } from 'lucide-react';
|
||||
import { ChartColumnBigIcon, KeyIcon, ShieldCheck, UserCircle } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -7,12 +7,14 @@ import type { MenuProps } from '@/components/Menu';
|
||||
import { enableAuth } from '@/const/auth';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { ProfileTabs } from '@/store/global/initialState';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { authSelectors } from '@/store/user/slices/auth/selectors';
|
||||
|
||||
export const useCategory = () => {
|
||||
const { t } = useTranslation('auth');
|
||||
const [isLoginWithClerk] = useUserStore((s) => [authSelectors.isLoginWithClerk(s)]);
|
||||
const { showApiKeyManage } = useServerConfigStore(featureFlagsSelectors);
|
||||
|
||||
const cateItems: MenuProps['items'] = [
|
||||
{
|
||||
@@ -43,6 +45,15 @@ export const useCategory = () => {
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
!!showApiKeyManage && {
|
||||
icon: <Icon icon={KeyIcon} />,
|
||||
key: ProfileTabs.APIKey,
|
||||
label: (
|
||||
<Link href={'/profile/apikey'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.apikey')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
].filter(Boolean) as MenuProps['items'];
|
||||
|
||||
return cateItems;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { LayoutProps } from '../type';
|
||||
import Header from './Header';
|
||||
import SideBar from './SideBar';
|
||||
|
||||
const SKIP_PATHS = ['/settings/provider', '/settings/agent'];
|
||||
const SKIP_PATHS = ['/settings/provider', '/settings/agent', '/settings/plugin'];
|
||||
|
||||
const Layout = memo<LayoutProps>(({ children, category }) => {
|
||||
const ref = useRef<any>(null);
|
||||
@@ -36,8 +36,7 @@ const Layout = memo<LayoutProps>(({ children, category }) => {
|
||||
height={'100%'}
|
||||
horizontal={md}
|
||||
ref={ref}
|
||||
style={{ background: theme.colorBgContainer, position: 'relative' }}
|
||||
width={'100%'}
|
||||
style={{ background: theme.colorBgContainer, flex: '1', position: 'relative' }}
|
||||
>
|
||||
{md ? (
|
||||
<SideBar>{category}</SideBar>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Info,
|
||||
KeyboardIcon,
|
||||
Mic2,
|
||||
Puzzle,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
@@ -115,6 +116,15 @@ export const useCategory = () => {
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Puzzle} />,
|
||||
key: SettingsTabs.Plugin,
|
||||
label: (
|
||||
<Link href={'/settings/plugin'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.plugin')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import McpDetail from '@/features/PluginStore/McpList/Detail';
|
||||
import PluginDetail from '@/features/PluginStore/PluginList/Detail';
|
||||
import CustomPluginEmptyState from '@/features/PluginStore/InstalledList/Detail/CustomPluginEmptyState';
|
||||
|
||||
interface DetailProps {
|
||||
identifier: string;
|
||||
runtimeType?: 'mcp' | 'default';
|
||||
type?: 'plugin' | 'customPlugin' | 'builtin';
|
||||
}
|
||||
|
||||
const Detail = memo<DetailProps>(({ identifier, type, runtimeType }) => {
|
||||
if (type === 'customPlugin') return <CustomPluginEmptyState identifier={identifier} />;
|
||||
|
||||
if (runtimeType === 'mcp') return <McpDetail identifier={identifier} />;
|
||||
|
||||
if (type === 'plugin') return <PluginDetail identifier={identifier} />;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default Detail;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Empty } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
import { LobeToolType } from '@/types/tool/tool';
|
||||
|
||||
import PluginItem from '@/features/PluginStore/InstalledList/List/Item';
|
||||
|
||||
interface ListProps {
|
||||
identifier?: string;
|
||||
keywords?: string;
|
||||
setIdentifier?: (props: {
|
||||
identifier?: string;
|
||||
runtimeType: 'mcp' | 'default';
|
||||
type?: LobeToolType;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const List = memo<ListProps>(({ keywords, identifier, setIdentifier }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const installedPlugins = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
|
||||
|
||||
const filteredPluginList = useMemo(
|
||||
() =>
|
||||
installedPlugins.filter((item) =>
|
||||
[item?.title, item?.description, item.author, ...(item?.tags || [])]
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
.toLowerCase()
|
||||
.includes((keywords || '')?.toLowerCase()),
|
||||
),
|
||||
[installedPlugins, keywords],
|
||||
);
|
||||
|
||||
const isEmpty = installedPlugins.length === 0;
|
||||
|
||||
if (isEmpty)
|
||||
return (
|
||||
<Center paddingBlock={40}>
|
||||
<Empty description={t('store.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Center>
|
||||
);
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
data={filteredPluginList}
|
||||
itemContent={(_, item) => {
|
||||
return (
|
||||
<Flexbox
|
||||
key={item.identifier}
|
||||
onClick={() => {
|
||||
setIdentifier?.({
|
||||
identifier: item.identifier,
|
||||
runtimeType: item.runtimeType as any,
|
||||
type: item.type,
|
||||
});
|
||||
}}
|
||||
paddingBlock={2}
|
||||
paddingInline={4}
|
||||
>
|
||||
<PluginItem active={identifier === item.identifier} {...(item as any)} />
|
||||
</Flexbox>
|
||||
);
|
||||
}}
|
||||
overscan={400}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
totalCount={filteredPluginList.length}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default List;
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanel } from '@lobehub/ui';
|
||||
import { Empty, Input } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { Search } from 'lucide-react';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
import { LobeToolType } from '@/types/tool/tool';
|
||||
|
||||
import Detail from './components/Detail';
|
||||
import List from './components/List';
|
||||
|
||||
const PluginSettings = memo(() => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const [keywords, setKeywords] = useState<string>('');
|
||||
const [type, setType] = useState<LobeToolType>();
|
||||
const [runtimeType, setRuntimeType] = useState<'mcp' | 'default'>();
|
||||
|
||||
const [identifier] = useToolStore((s) => [s.activePluginIdentifier]);
|
||||
const isEmpty = useToolStore((s) => pluginSelectors.installedPluginMetaList(s).length === 0);
|
||||
useFetchInstalledPlugins();
|
||||
|
||||
if (isEmpty)
|
||||
return (
|
||||
<Center height={'60vh'} paddingBlock={40}>
|
||||
<Empty description={t('store.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Center>
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
height={'100vh'}
|
||||
horizontal
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
width={'100%'}
|
||||
>
|
||||
<DraggablePanel maxWidth={1024} minWidth={420} placement={'left'}>
|
||||
<Flexbox padding={8}>
|
||||
<Input
|
||||
allowClear
|
||||
onChange={(e) => setKeywords(e.target.value)}
|
||||
placeholder={t('store.search')}
|
||||
prefix={<Search size={16} />}
|
||||
style={{ width: '100%' }}
|
||||
value={keywords}
|
||||
/>
|
||||
</Flexbox>
|
||||
|
||||
<List
|
||||
identifier={identifier}
|
||||
keywords={keywords}
|
||||
setIdentifier={({ identifier, type, runtimeType }) => {
|
||||
useToolStore.setState({ activePluginIdentifier: identifier });
|
||||
setType(type);
|
||||
setRuntimeType(runtimeType);
|
||||
ref?.current?.scrollTo({ top: 0 });
|
||||
}}
|
||||
/>
|
||||
</DraggablePanel>
|
||||
{identifier ? (
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
padding={16}
|
||||
ref={ref}
|
||||
style={{
|
||||
background: theme.colorBgContainerSecondary,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
width={'100%'}
|
||||
>
|
||||
<Detail identifier={identifier} runtimeType={runtimeType} type={type} />
|
||||
</Flexbox>
|
||||
) : (
|
||||
<Center
|
||||
height={'100%'}
|
||||
style={{
|
||||
background: theme.colorBgContainerSecondary,
|
||||
}}
|
||||
width={'100%'}
|
||||
>
|
||||
<Empty description={t('store.emptySelectHint')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Center>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginSettings;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { metadataModule } from '@/server/metadata';
|
||||
import { translation } from '@/server/translation';
|
||||
import { DynamicLayoutProps } from '@/types/next';
|
||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
||||
|
||||
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
||||
const locale = await RouteVariants.getLocale(props);
|
||||
const { t } = await translation('setting', locale);
|
||||
|
||||
return metadataModule.generate({
|
||||
description: t('header.desc'),
|
||||
title: t('tab.plugin'),
|
||||
url: '/settings/plugin',
|
||||
});
|
||||
};
|
||||
|
||||
export { default } from './index';
|
||||
@@ -32,6 +32,11 @@ const RootLayout = async ({ children, params, modal }: RootLayoutProps) => {
|
||||
|
||||
return (
|
||||
<html dir={direction} lang={locale} suppressHydrationWarning>
|
||||
<head>
|
||||
{process.env.DEBUG_REACT_SCAN === '1' && (
|
||||
<script crossOrigin="anonymous" src="https://unpkg.com/react-scan/dist/auto.global.js" />
|
||||
)}
|
||||
</head>
|
||||
<body>
|
||||
<NuqsAdapter>
|
||||
<GlobalProvider
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
const giteeaiChatModels: AIChatModelCard[] = [
|
||||
{
|
||||
@@ -222,6 +222,273 @@ const giteeaiChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...giteeaiChatModels];
|
||||
const giteeaiImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'FLUX.1-dev 是由 Black Forest Labs 开发的一款开源 多模态语言模型(Multimodal Language Model, MLLM),专为图文任务优化,融合了图像和文本的理解与生成能力。它建立在先进的大语言模型(如 Mistral-7B)基础上,通过精心设计的视觉编码器与多阶段指令微调,实现了图文协同处理与复杂任务推理的能力。',
|
||||
displayName: 'FLUX.1-dev',
|
||||
enabled: true,
|
||||
id: 'FLUX.1-dev',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '1536x1536'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'由 Black Forest Labs 开发的 120 亿参数文生图模型,采用潜在对抗扩散蒸馏技术,能够在 1 到 4 步内生成高质量图像。该模型性能媲美闭源替代品,并在 Apache-2.0 许可证下发布,适用于个人、科研和商业用途。',
|
||||
displayName: 'flux-1-schnell',
|
||||
enabled: true,
|
||||
id: 'flux-1-schnell',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '1536x1536', '2048x2048'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'FLUX.1-Kontext-dev 是由 Black Forest Labs 开发的一款基于 Rectified Flow Transformer 架构 的多模态图像生成与编辑模型,拥有 12B(120 亿)参数规模,专注于在给定上下文条件下生成、重构、增强或编辑图像。该模型结合了扩散模型的可控生成优势与 Transformer 的上下文建模能力,支持高质量图像输出,广泛适用于图像修复、图像补全、视觉场景重构等任务。',
|
||||
displayName: 'FLUX.1-Kontext-dev',
|
||||
enabled: true,
|
||||
id: 'FLUX.1-Kontext-dev',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '1536x1536', '2048x2048'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Stable Diffusion 3.5 Large Turbo 专注于高质量图像生成,具备强大的细节表现力和场景还原能力。',
|
||||
displayName: 'stable-diffusion-3.5-large-turbo',
|
||||
enabled: true,
|
||||
id: 'stable-diffusion-3.5-large-turbo',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'由 Stability AI 推出的最新文生图大模型。这一版本在继承了前代的优点上,对图像质量、文本理解和风格多样性等方面进行了显著改进,能够更准确地解读复杂的自然语言提示,并生成更为精确和多样化的图像。',
|
||||
displayName: 'stable-diffusion-3-medium',
|
||||
enabled: true,
|
||||
id: 'stable-diffusion-3-medium',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'由 Stability AI 开发并开源的文生图大模型,其创意图像生成能力位居行业前列。具备出色的指令理解能力,能够支持反向 Prompt 定义来精确生成内容。',
|
||||
displayName: 'stable-diffusion-xl-base-1.0',
|
||||
enabled: true,
|
||||
id: 'stable-diffusion-xl-base-1.0',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Kolors 是由快手 Kolors 团队开发的文生图模型。由数十亿的参数训练,在视觉质量、中文语义理解和文本渲染方面有显著优势。',
|
||||
displayName: 'Kolors',
|
||||
enabled: true,
|
||||
id: 'Kolors',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'hunyuandit-v1.2-distilled 是一款轻量级的文生图模型,经过蒸馏优化,能够快速生成高质量的图像,特别适用于低资源环境和实时生成任务。',
|
||||
displayName: 'HunyuanDiT-v1.2-Diffusers-Distilled',
|
||||
enabled: true,
|
||||
id: 'HunyuanDiT-v1.2-Diffusers-Distilled',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HiDream-I1 是一个全新的开源图像生成基础模型,是由国内企业智象未来开源的。拥有 170 亿参数(Flux是12B参数),能够在几秒内实现行业领先的图像生成质量。',
|
||||
displayName: 'HiDream-I1-Full',
|
||||
enabled: true,
|
||||
id: 'HiDream-I1-Full',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HiDream-E1-Full 是由智象未来(HiDream.ai)推出的一款 开源多模态图像编辑大模型,基于先进的 Diffusion Transformer 架构,并结合强大的语言理解能力(内嵌 LLaMA 3.1-8B-Instruct),支持通过自然语言指令进行图像生成、风格迁移、局部编辑和内容重绘,具备出色的图文理解与执行能力。',
|
||||
displayName: 'HiDream-E1-Full',
|
||||
enabled: true,
|
||||
id: 'HiDream-I1-Full',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HelloMeme 是一个可以根据你提供的图片或动作,自动生成表情包、动图或短视频的 AI 工具。它不需要你有任何绘画或编程基础,只需要准备好参考图片,它就能帮你做出好看、有趣、风格一致的内容。',
|
||||
displayName: 'HelloMeme',
|
||||
enabled: true,
|
||||
id: 'HelloMeme',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'OmniConsistency 通过引入大规模 Diffusion Transformers(DiTs)和配对风格化数据,提升图像到图像(Image-to-Image)任务中的风格一致性和泛化能力,避免风格退化。',
|
||||
displayName: 'OmniConsistency',
|
||||
enabled: true,
|
||||
id: 'OmniConsistency',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'InstantCharacter 是由腾讯 AI 团队在 2025 年发布的一款 无需微调(tuning-free) 的个性化角色生成模型,旨在实现高保真、跨场景的一致角色生成。该模型支持仅基于 一张参考图像 对角色进行建模,并能够将该角色灵活迁移到各种风格、动作和背景中。',
|
||||
displayName: 'InstantCharacter',
|
||||
enabled: true,
|
||||
id: 'InstantCharacter',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'DreamO 是由字节跳动与北京大学联合研发的开源图像定制生成模型,旨在通过统一架构支持多任务图像生成。它采用高效的组合建模方法,可根据用户指定的身份、主体、风格、背景等多个条件生成高度一致且定制化的图像。',
|
||||
displayName: 'DreamO',
|
||||
enabled: true,
|
||||
id: 'DreamO',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'AnimeSharp(又名 “4x‑AnimeSharp”) 是 Kim2091 基于 ESRGAN 架构开发的开源超分辨率模型,专注于动漫风格图像的放大与锐化。它于 2022 年 2 月重命名自 “4x-TextSharpV1”,原本也适用于文字图像但性能针对动漫内容进行了大幅优化',
|
||||
displayName: 'AnimeSharp',
|
||||
enabled: true,
|
||||
id: 'AnimeSharp',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...giteeaiChatModels, ...giteeaiImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -17,6 +17,7 @@ const googleChatModels: AIChatModelCard[] = [
|
||||
id: 'gemini-2.5-pro',
|
||||
maxOutput: 65_536,
|
||||
pricing: {
|
||||
cachedInput: 0.31, // prompts <= 200k tokens
|
||||
input: 1.25, // prompts <= 200k tokens
|
||||
output: 10, // prompts <= 200k tokens
|
||||
},
|
||||
@@ -177,6 +178,32 @@ const googleChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
search: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 1_048_576 + 65_536,
|
||||
description: 'Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。',
|
||||
displayName: 'Gemini 2.5 Flash-Lite',
|
||||
enabled: true,
|
||||
id: 'gemini-2.5-flash-lite',
|
||||
maxOutput: 65_536,
|
||||
pricing: {
|
||||
cachedInput: 0.025,
|
||||
input: 0.1,
|
||||
output: 0.4,
|
||||
},
|
||||
releasedAt: '2025-07-22',
|
||||
settings: {
|
||||
extendParams: ['thinkingBudget'],
|
||||
searchImpl: 'params',
|
||||
searchProvider: 'google',
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
|
||||
+207
-2
@@ -958,9 +958,42 @@ const qwenChatModels: AIChatModelCard[] = [
|
||||
|
||||
const qwenImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description: '阿里云通义旗下的文生图模型',
|
||||
displayName: 'Wanxiang T2I Turbo',
|
||||
description: '万相2.2极速版,当前最新模型。在创意性、稳定性、写实质感上全面升级,生成速度快,性价比高。',
|
||||
displayName: 'Wanxiang2.2 T2I Flash',
|
||||
enabled: true,
|
||||
id: 'wan2.2-t2i-flash',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
},
|
||||
releasedAt: '2025-07-28',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: '万相2.2专业版,当前最新模型。在创意性、稳定性、写实质感上全面升级,生成细节丰富。',
|
||||
displayName: 'Wanxiang2.2 T2I Plus',
|
||||
enabled: true,
|
||||
id: 'wan2.2-t2i-plus',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
},
|
||||
releasedAt: '2025-07-28',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: '全面升级版本。生成速度快、效果全面、综合性价比高。对应通义万相官网2.1极速模型。',
|
||||
displayName: 'Wanxiang2.1 T2I Turbo',
|
||||
id: 'wanx2.1-t2i-turbo',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
@@ -974,6 +1007,178 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
releasedAt: '2025-01-08',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: '全面升级版本。生成图像细节更丰富,速度稍慢。对应通义万相官网2.1专业模型。',
|
||||
displayName: 'Wanxiang2.1 T2I Plus',
|
||||
id: 'wanx2.1-t2i-plus',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
},
|
||||
releasedAt: '2025-01-08',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: '擅长质感人像,速度中等、成本较低。对应通义万相官网2.0极速模型。',
|
||||
displayName: 'Wanxiang2.0 T2I Turbo',
|
||||
id: 'wanx2.0-t2i-turbo',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
},
|
||||
releasedAt: '2025-01-17',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: '基础文生图模型。对应通义万相官网1.0通用模型。',
|
||||
displayName: 'Wanxiang v1',
|
||||
id: 'wanx-v1',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
},
|
||||
releasedAt: '2024-05-22',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'FLUX.1 [schnell] 作为目前开源最先进的少步模型,不仅超越了同类竞争者,甚至还优于诸如 Midjourney v6.0 和 DALL·E 3 (HD) 等强大的非精馏模型。该模型经过专门微调,以保留预训练阶段的全部输出多样性,相较于当前市场上的最先进模型,FLUX.1 [schnell] 显著提升了在视觉质量、指令遵从、尺寸/比例变化、字体处理及输出多样性等方面的可能,为用户带来更为丰富多样的创意图像生成体验。',
|
||||
displayName: 'FLUX.1 [schnell]',
|
||||
enabled: true,
|
||||
id: 'flux-schnell',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x1024', '768x512', '768x1024', '1024x576', '576x1024', '1024x1024'],
|
||||
},
|
||||
steps: { default: 4, max: 12, min: 1 },
|
||||
},
|
||||
releasedAt: '2024-08-07',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'FLUX.1 [dev] 是一款面向非商业应用的开源权重、精炼模型。FLUX.1 [dev] 在保持了与FLUX专业版相近的图像质量和指令遵循能力的同时,具备更高的运行效率。相较于同尺寸的标准模型,它在资源利用上更为高效。',
|
||||
displayName: 'FLUX.1 [dev]',
|
||||
enabled: true,
|
||||
id: 'flux-dev',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x1024', '768x512', '768x1024', '1024x576', '576x1024', '1024x1024'],
|
||||
},
|
||||
steps: { default: 50, max: 50, min: 1 },
|
||||
},
|
||||
releasedAt: '2024-08-07',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'FLUX.1-merged 模型结合了 "DEV" 在开发阶段探索的深度特性和 "Schnell" 所代表的高速执行优势。通过这一举措,FLUX.1-merged 不仅提升了模型的性能界限,还拓宽了其应用范围。',
|
||||
displayName: 'FLUX.1-merged',
|
||||
enabled: true,
|
||||
id: 'flux-merged',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x1024', '768x512', '768x1024', '1024x576', '576x1024', '1024x1024'],
|
||||
},
|
||||
steps: { default: 30, max: 30, min: 1 },
|
||||
},
|
||||
releasedAt: '2024-08-22',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'stable-diffusion-3.5-large 是一个具有8亿参数的多模态扩散变压器(MMDiT)文本到图像生成模型,具备卓越的图像质量和提示词匹配度,支持生成 100 万像素的高分辨率图像,且能够在普通消费级硬件上高效运行。',
|
||||
displayName: 'StableDiffusion 3.5 Large',
|
||||
id: 'stable-diffusion-3.5-large',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 40, max: 500, min: 1 },
|
||||
width: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
releasedAt: '2024-10-25',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'stable-diffusion-3.5-large-turbo 是在 stable-diffusion-3.5-large 的基础上采用对抗性扩散蒸馏(ADD)技术的模型,具备更快的速度。',
|
||||
displayName: 'StableDiffusion 3.5 Large Turbo',
|
||||
id: 'stable-diffusion-3.5-large-turbo',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 40, max: 500, min: 1 },
|
||||
width: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
releasedAt: '2024-10-25',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'stable-diffusion-xl 相比于 v1.5 做了重大的改进,并且与当前开源的文生图 SOTA 模型 midjourney 效果相当。具体改进之处包括: 更大的 unet backbone,是之前的 3 倍; 增加了 refinement 模块用于改善生成图片的质量;更高效的训练技巧等。',
|
||||
displayName: 'StableDiffusion xl',
|
||||
id: 'stable-diffusion-xl',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 50, max: 500, min: 1 },
|
||||
width: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
releasedAt: '2024-04-09',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description: 'stable-diffusion-v1.5 是以 stable-diffusion-v1.2 检查点的权重进行初始化,并在 "laion-aesthetics v2 5+" 上以 512x512 的分辨率进行了595k步的微调,减少了 10% 的文本条件化,以提高无分类器的引导采样。',
|
||||
displayName: 'StableDiffusion v1.5',
|
||||
id: 'stable-diffusion-v1.5',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 512, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 50, max: 500, min: 1 },
|
||||
width: { default: 512, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
releasedAt: '2024-04-09',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...qwenChatModels, ...qwenImageModels];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// https://siliconflow.cn/zh-cn/models
|
||||
const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
@@ -830,6 +830,28 @@ const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...siliconcloudChatModels];
|
||||
const siliconcloudImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'Kolors 是由快手 Kolors 团队开发的基于潜在扩散的大规模文本到图像生成模型。该模型通过数十亿文本-图像对的训练,在视觉质量、复杂语义准确性以及中英文字符渲染方面展现出显著优势。它不仅支持中英文输入,在理解和生成中文特定内容方面也表现出色',
|
||||
displayName: 'Kolors',
|
||||
enabled: true,
|
||||
id: 'Kwai-Kolors/Kolors',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '960x1280', '768x1024', '720x1440', '720x1280'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2024-07-06',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...siliconcloudChatModels, ...siliconcloudImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// https://platform.stepfun.com/docs/pricing/details
|
||||
|
||||
@@ -275,6 +275,71 @@ const stepfunChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...stepfunChatModels];
|
||||
const stepfunImageModels: AIImageModelCard[] = [
|
||||
// https://platform.stepfun.com/docs/llm/image
|
||||
{
|
||||
description:
|
||||
'阶跃星辰新一代生图模型,该模型专注于图像生成任务,能够根据用户提供的文本描述,生成高质量的图像。新模型生成图片质感更真实,中英文文字生成能力更强。',
|
||||
displayName: 'Step 2X Large',
|
||||
enabled: true,
|
||||
id: 'step-2x-large',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['256x256', '512x512', '768x768', '1024x1024', '1280x800', '800x1280'],
|
||||
},
|
||||
steps: { default: 50, max: 100, min: 1 },
|
||||
},
|
||||
releasedAt: '2024-08-07',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'该模型拥有强大的图像生成能力,支持文本描述作为输入方式。具备原生的中文支持,能够更好的理解和处理中文文本描述,并且能够更准确地捕捉文本描述中的语义信息,并将其转化为图像特征,从而实现更精准的图像生成。模型能够根据输入生成高分辨率、高质量的图像,并具备一定的风格迁移能力。',
|
||||
displayName: 'Step 1X Medium',
|
||||
enabled: true,
|
||||
id: 'step-1x-medium',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['256x256', '512x512', '768x768', '1024x1024', '1280x800', '800x1280'],
|
||||
},
|
||||
steps: { default: 50, max: 100, min: 1 },
|
||||
},
|
||||
releasedAt: '2025-07-15',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'该模型专注于图像编辑任务,能够根据用户提供的图片和文本描述,对图片进行修改和增强。支持多种输入格式,包括文本描述和示例图像。模型能够理解用户的意图,并生成符合要求的图像编辑结果。',
|
||||
displayName: 'Step 1X Edit',
|
||||
enabled: true,
|
||||
id: 'step-1x-edit',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x512', '768x768', '1024x1024'],
|
||||
},
|
||||
steps: { default: 28, max: 100, min: 1 },
|
||||
},
|
||||
releasedAt: '2025-03-04',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...stepfunChatModels, ...stepfunImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -17,6 +17,7 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
||||
id: 'gemini-2.5-pro',
|
||||
maxOutput: 65_536,
|
||||
pricing: {
|
||||
cachedInput: 0.31, // prompts <= 200k tokens
|
||||
input: 1.25, // prompts <= 200k tokens
|
||||
output: 10, // prompts <= 200k tokens
|
||||
},
|
||||
@@ -80,6 +81,7 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
||||
id: 'gemini-2.5-flash',
|
||||
maxOutput: 65_536,
|
||||
pricing: {
|
||||
cachedInput: 0.075,
|
||||
input: 0.3,
|
||||
output: 2.5,
|
||||
},
|
||||
@@ -109,6 +111,31 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
||||
releasedAt: '2025-04-17',
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
reasoning: true,
|
||||
search: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 1_000_000 + 64_000,
|
||||
description: 'Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。',
|
||||
displayName: 'Gemini 2.5 Flash-Lite',
|
||||
enabled: true,
|
||||
id: 'gemini-2.5-flash-lite',
|
||||
maxOutput: 64_000,
|
||||
pricing: {
|
||||
cachedInput: 0.025,
|
||||
input: 0.1,
|
||||
output: 0.4,
|
||||
},
|
||||
releasedAt: '2025-07-22',
|
||||
settings: {
|
||||
searchImpl: 'params',
|
||||
searchProvider: 'google',
|
||||
},
|
||||
type: 'chat',
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
functionCall: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// modelInfo https://www.volcengine.com/docs/82379/1330310
|
||||
// pricing https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
|
||||
@@ -509,6 +509,60 @@ const doubaoChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...doubaoChatModels];
|
||||
const volcengineImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
/*
|
||||
// TODO: AIImageModelCard 不支持 config.deploymentName
|
||||
config: {
|
||||
deploymentName: 'doubao-seedream-3-0-t2i-250415',
|
||||
},
|
||||
*/
|
||||
description:
|
||||
'Doubao图片生成模型由字节跳动 Seed 团队研发,支持文字与图片输入,提供高可控、高质量的图片生成体验。基于文本提示词生成图片。',
|
||||
displayName: 'Doubao Seedream 3.0 t2i',
|
||||
enabled: true,
|
||||
id: 'doubao-seedream-3-0-t2i-250415',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '864x1152', '1152x864', '1280x720', '720x1280', '832x1248', '1248x832', '1512x648'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-04-15',
|
||||
type: 'image',
|
||||
},
|
||||
/*
|
||||
// Note: Doubao 图生图模型与文生图模型公用一个 Endpoint,当前如果存在 imageUrl 会切换至 edit endpoint 下
|
||||
{
|
||||
config: {
|
||||
deploymentName: 'doubao-seededit-3-0-i2i-250628',
|
||||
},
|
||||
description:
|
||||
'Doubao图片生成模型由字节跳动 Seed 团队研发,支持文字与图片输入,提供高可控、高质量的图片生成体验。支持通过文本指令编辑图像,生成图像的边长在512~1536之间。',
|
||||
displayName: 'Doubao SeedEdit 3.0 i2i',
|
||||
enabled: true,
|
||||
id: 'doubao-seededit-3-0-i2i-250628',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '864x1152', '1152x864', '1280x720', '720x1280', '832x1248', '1248x832', '1512x648'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-06-28',
|
||||
type: 'image',
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
export const allModels = [...doubaoChatModels, ...volcengineImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
const wenxinChatModels: AIChatModelCard[] = [
|
||||
{
|
||||
@@ -564,6 +564,66 @@ const wenxinChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...wenxinChatModels];
|
||||
const wenxinImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'百度自研的iRAG(image based RAG),检索增强的文生图技术,将百度搜索的亿级图片资源跟强大的基础模型能力相结合,就可以生成各种超真实的图片,整体效果远远超过文生图原生系统,去掉了AI味儿,而且成本很低。iRAG具备无幻觉、超真实、立等可取等特点。',
|
||||
displayName: 'ERNIE iRAG',
|
||||
enabled: true,
|
||||
id: 'irag-1.0',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['768x768', '1024x1024', '1536x1536', '2048x2048', '1024x768', '2048x1536', '768x1024', '1536x2048', '1024x576', '2048x1152', '576x1024', '1152x2048'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-02-05',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'百度自研的ERNIE iRAG Edit图像编辑模型支持基于图片进行erase(消除对象)、repaint(重绘对象)、variation(生成变体)等操作。',
|
||||
displayName: 'ERNIE iRAG Edit',
|
||||
enabled: true,
|
||||
id: 'ernie-irag-edit',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['768x768', '1024x1024', '1536x1536', '2048x2048', '1024x768', '2048x1536', '768x1024', '1536x2048', '1024x576', '2048x1152', '576x1024', '1152x2048'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-04-17',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'具有120亿参数的修正流变换器,能够根据文本描述生成图像。',
|
||||
displayName: 'FLUX.1-schnell',
|
||||
enabled: true,
|
||||
id: 'flux.1-schnell',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['768x768', '1024x1024', '1536x1536', '2048x2048', '1024x768', '2048x1536', '768x1024', '1536x2048', '1024x576', '2048x1152', '576x1024', '1152x2048'],
|
||||
},
|
||||
steps: { default: 25, max: 50, min: 1 },
|
||||
},
|
||||
releasedAt: '2025-03-27',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...wenxinChatModels, ...wenxinImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// https://docs.x.ai/docs/models
|
||||
const xaiChatModels: AIChatModelCard[] = [
|
||||
@@ -158,6 +158,23 @@ const xaiChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...xaiChatModels];
|
||||
const xaiImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'我们最新的图像生成模型可以根据文本提示生成生动逼真的图像。它在营销、社交媒体和娱乐等领域的图像生成方面表现出色。',
|
||||
displayName: 'Grok 2 Image 1212',
|
||||
enabled: true,
|
||||
id: 'grok-2-image-1212',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
releasedAt: '2024-12-12',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...xaiChatModels, ...xaiImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -16,6 +16,9 @@ export const FeatureFlagsSchema = z.object({
|
||||
openai_api_key: z.boolean().optional(),
|
||||
openai_proxy_url: z.boolean().optional(),
|
||||
|
||||
// profile
|
||||
api_key_manage: z.boolean().optional(),
|
||||
|
||||
create_session: z.boolean().optional(),
|
||||
edit_agent: z.boolean().optional(),
|
||||
|
||||
@@ -56,6 +59,8 @@ export const DEFAULT_FEATURE_FLAGS: IFeatureFlags = {
|
||||
openai_api_key: true,
|
||||
openai_proxy_url: true,
|
||||
|
||||
api_key_manage: false,
|
||||
|
||||
create_session: true,
|
||||
edit_agent: true,
|
||||
|
||||
@@ -97,6 +102,8 @@ export const mapFeatureFlagsEnvToState = (config: IFeatureFlags) => {
|
||||
showOpenAIApiKey: config.openai_api_key,
|
||||
showOpenAIProxyUrl: config.openai_proxy_url,
|
||||
|
||||
showApiKeyManage: config.api_key_manage,
|
||||
|
||||
enablePlugins: config.plugins,
|
||||
showDalle: config.dalle,
|
||||
showChangelog: config.changelog,
|
||||
|
||||
+2
-3
@@ -9,11 +9,10 @@ export const LOBE_CHAT_OIDC_AUTH_HEADER = 'Oidc-Auth';
|
||||
|
||||
export const OAUTH_AUTHORIZED = 'X-oauth-authorized';
|
||||
|
||||
export const JWT_SECRET_KEY = 'LobeHub · LobeChat';
|
||||
export const NON_HTTP_PREFIX = 'http_nosafe';
|
||||
export const SECRET_XOR_KEY = 'LobeHub · LobeHub';
|
||||
|
||||
/* eslint-disable typescript-sort-keys/interface */
|
||||
export interface JWTPayload {
|
||||
export interface ClientSecretPayload {
|
||||
/**
|
||||
* password
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS "api_keys" (
|
||||
"id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "api_keys_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||
"name" varchar(256) NOT NULL,
|
||||
"key" varchar(256) NOT NULL,
|
||||
"enabled" boolean DEFAULT true,
|
||||
"expires_at" timestamp with time zone,
|
||||
"last_used_at" timestamp with time zone,
|
||||
"user_id" text NOT NULL,
|
||||
"accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "api_keys_key_unique" UNIQUE("key")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "rbac_roles" ADD COLUMN "metadata" jsonb DEFAULT '{}'::jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -203,6 +203,13 @@
|
||||
"when": 1752567402506,
|
||||
"tag": "0028_oauth_handoffs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 29,
|
||||
"version": "7",
|
||||
"when": 1753201379817,
|
||||
"tag": "0029_add_apikey_manage",
|
||||
"breakpoints": true
|
||||
}
|
||||
],
|
||||
"version": "6"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { and, desc, eq } from 'drizzle-orm/expressions';
|
||||
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import { generateApiKey, isApiKeyExpired, validateApiKeyFormat } from '@/utils/apiKey';
|
||||
|
||||
import { ApiKeyItem, NewApiKeyItem, apiKeys } from '../schemas';
|
||||
|
||||
type EncryptAPIKeyVaults = (keyVaults: string) => Promise<string>;
|
||||
type DecryptAPIKeyVaults = (keyVaults: string) => Promise<{ plaintext: string }>;
|
||||
|
||||
const defaultSerialize = (s: string) => s;
|
||||
|
||||
export class ApiKeyModel {
|
||||
private userId: string;
|
||||
private db: LobeChatDatabase;
|
||||
|
||||
constructor(db: LobeChatDatabase, userId: string) {
|
||||
this.userId = userId;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
create = async (
|
||||
params: Omit<NewApiKeyItem, 'userId' | 'id' | 'key'>,
|
||||
encryptor?: EncryptAPIKeyVaults,
|
||||
) => {
|
||||
const key = generateApiKey();
|
||||
|
||||
const encrypt = encryptor || defaultSerialize;
|
||||
|
||||
const encryptedKey = await encrypt(key);
|
||||
|
||||
const [result] = await this.db
|
||||
.insert(apiKeys)
|
||||
.values({ ...params, key: encryptedKey, userId: this.userId })
|
||||
.returning();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
delete = async (id: number) => {
|
||||
return this.db.delete(apiKeys).where(and(eq(apiKeys.id, id), eq(apiKeys.userId, this.userId)));
|
||||
};
|
||||
|
||||
deleteAll = async () => {
|
||||
return this.db.delete(apiKeys).where(eq(apiKeys.userId, this.userId));
|
||||
};
|
||||
|
||||
query = async (decryptor?: DecryptAPIKeyVaults) => {
|
||||
const results = await this.db.query.apiKeys.findMany({
|
||||
orderBy: [desc(apiKeys.updatedAt)],
|
||||
where: eq(apiKeys.userId, this.userId),
|
||||
});
|
||||
|
||||
// 如果没有提供解密器,直接返回原始结果
|
||||
if (!decryptor) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// 对每个 API Key 的 key 字段进行解密
|
||||
const decryptedResults = await Promise.all(
|
||||
results.map(async (apiKey) => {
|
||||
const decryptedKey = await decryptor(apiKey.key);
|
||||
return {
|
||||
...apiKey,
|
||||
key: decryptedKey.plaintext,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return decryptedResults;
|
||||
};
|
||||
|
||||
findByKey = async (key: string, encryptor?: EncryptAPIKeyVaults) => {
|
||||
if (!validateApiKeyFormat(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encrypt = encryptor || defaultSerialize;
|
||||
|
||||
const encryptedKey = await encrypt(key);
|
||||
|
||||
return this.db.query.apiKeys.findFirst({
|
||||
where: eq(apiKeys.key, encryptedKey),
|
||||
});
|
||||
};
|
||||
|
||||
validateKey = async (key: string) => {
|
||||
const apiKey = await this.findByKey(key);
|
||||
|
||||
if (!apiKey) return false;
|
||||
if (!apiKey.enabled) return false;
|
||||
if (isApiKeyExpired(apiKey.expiresAt)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
update = async (id: number, value: Partial<ApiKeyItem>) => {
|
||||
return this.db
|
||||
.update(apiKeys)
|
||||
.set({ ...value, updatedAt: new Date() })
|
||||
.where(and(eq(apiKeys.id, id), eq(apiKeys.userId, this.userId)));
|
||||
};
|
||||
|
||||
findById = async (id: number) => {
|
||||
return this.db.query.apiKeys.findFirst({
|
||||
where: and(eq(apiKeys.id, id), eq(apiKeys.userId, this.userId)),
|
||||
});
|
||||
};
|
||||
|
||||
updateLastUsed = async (id: number) => {
|
||||
return this.db
|
||||
.update(apiKeys)
|
||||
.set({ lastUsedAt: new Date() })
|
||||
.where(and(eq(apiKeys.id, id), eq(apiKeys.userId, this.userId)));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
import { boolean, integer, pgTable, text, varchar } from 'drizzle-orm/pg-core';
|
||||
import { createInsertSchema } from 'drizzle-zod';
|
||||
|
||||
import { timestamps, timestamptz } from './_helpers';
|
||||
import { users } from './user';
|
||||
|
||||
export const apiKeys = pgTable('api_keys', {
|
||||
id: integer('id').primaryKey().generatedByDefaultAsIdentity(), // auto-increment primary key
|
||||
name: varchar('name', { length: 256 }).notNull(), // name of the API key
|
||||
key: varchar('key', { length: 256 }).notNull().unique(), // API key
|
||||
enabled: boolean('enabled').default(true), // whether the API key is enabled
|
||||
expiresAt: timestamptz('expires_at'), // expires time
|
||||
lastUsedAt: timestamptz('last_used_at'), // last used time
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(), // belongs to user, when user is deleted, the API key will be deleted
|
||||
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const insertApiKeySchema = createInsertSchema(apiKeys);
|
||||
|
||||
export type ApiKeyItem = typeof apiKeys.$inferSelect;
|
||||
export type NewApiKeyItem = typeof apiKeys.$inferInsert;
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './agent';
|
||||
export * from './aiInfra';
|
||||
export * from './apiKey';
|
||||
export * from './asyncTask';
|
||||
export * from './document';
|
||||
export * from './file';
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
import { boolean, index, integer, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
import { timestamps } from './_helpers';
|
||||
import { users } from './user';
|
||||
@@ -12,6 +21,7 @@ export const roles = pgTable('rbac_roles', {
|
||||
description: text('description'), // Role description
|
||||
isSystem: boolean('is_system').default(false).notNull(), // Whether it's a system role
|
||||
isActive: boolean('is_active').default(true).notNull(), // Whether it's active
|
||||
metadata: jsonb('metadata').default({}), // Role metadata
|
||||
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
|
||||
const ReactScan = memo(() => {
|
||||
const [debug] = useQueryState('debug', { clearOnDefault: true, defaultValue: '' });
|
||||
|
||||
return !!debug && <Script src="https://unpkg.com/react-scan/dist/auto.global.js" />;
|
||||
});
|
||||
|
||||
export default withSuspense(ReactScan);
|
||||
@@ -13,7 +13,6 @@ import AppTheme from './AppTheme';
|
||||
import ImportSettings from './ImportSettings';
|
||||
import Locale from './Locale';
|
||||
import QueryProvider from './Query';
|
||||
import ReactScan from './ReactScan';
|
||||
import StoreInitialization from './StoreInitialization';
|
||||
import StyleRegistry from './StyleRegistry';
|
||||
|
||||
@@ -61,7 +60,6 @@ const GlobalLayout = async ({
|
||||
<StoreInitialization />
|
||||
<Suspense>
|
||||
<ImportSettings />
|
||||
<ReactScan />
|
||||
{process.env.NODE_ENV === 'development' && <DevPanel />}
|
||||
</Suspense>
|
||||
</ServerConfigStoreProvider>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LangfuseGenerationClient, LangfuseTraceClient } from 'langfuse-core';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as langfuseCfg from '@/config/langfuse';
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import { TraceNameMap } from '@/const/trace';
|
||||
import { AgentRuntime, ChatStreamPayload, LobeOpenAI, ModelProvider } from '@/libs/model-runtime';
|
||||
import { providerRuntimeMap } from '@/libs/model-runtime/runtimeMap';
|
||||
@@ -51,7 +51,7 @@ const specialProviders = [
|
||||
const testRuntime = (providerId: string, payload?: any) => {
|
||||
describe(`${providerId} provider runtime`, () => {
|
||||
it('should initialize correctly', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-key', ...payload };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-key', ...payload };
|
||||
const runtime = await AgentRuntime.initializeWithProvider(providerId, jwtPayload);
|
||||
|
||||
// @ts-ignore
|
||||
@@ -66,7 +66,7 @@ const testRuntime = (providerId: string, payload?: any) => {
|
||||
|
||||
let mockModelRuntime: AgentRuntime;
|
||||
beforeEach(async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-openai-key', baseURL: 'user-endpoint' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-openai-key', baseURL: 'user-endpoint' };
|
||||
mockModelRuntime = await AgentRuntime.initializeWithProvider(ModelProvider.OpenAI, jwtPayload);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,20 +18,38 @@ interface QwenImageTaskResponse {
|
||||
request_id: string;
|
||||
}
|
||||
|
||||
const QwenText2ImageModels = [
|
||||
'wan2.2-t2i',
|
||||
'wanx2.1-t2i',
|
||||
'wanx2.0-t2i',
|
||||
'wanx-v1',
|
||||
'flux',
|
||||
'stable-diffusion'
|
||||
];
|
||||
|
||||
const getModelType = (model: string): string => {
|
||||
// 可以添加其他模型类型的判断
|
||||
// if (QwenImage2ImageModels.some(prefix => model.startsWith(prefix))) {
|
||||
// return 'image2image';
|
||||
// }
|
||||
|
||||
if (QwenText2ImageModels.some(prefix => model.startsWith(prefix))) {
|
||||
return 'text2image';
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported model: ${model}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an image generation task with Qwen API
|
||||
*/
|
||||
async function createImageTask(payload: CreateImagePayload, apiKey: string): Promise<string> {
|
||||
const { model, params } = payload;
|
||||
// I can only say that the design of Alibaba Cloud's API is really bad; each model has a different endpoint path.
|
||||
const modelEndpointMap: Record<string, string> = {
|
||||
'wanx2.1-t2i-turbo':
|
||||
'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis',
|
||||
};
|
||||
|
||||
const endpoint = modelEndpointMap[model];
|
||||
const modelType = getModelType(model);
|
||||
const endpoint = `https://dashscope.aliyuncs.com/api/v1/services/aigc/${modelType}/image-synthesis`
|
||||
if (!endpoint) {
|
||||
throw new Error(`Unsupported model: ${model}`);
|
||||
throw new Error(`No endpoint configured for model type: ${modelType}`);
|
||||
}
|
||||
log('Creating image task with model: %s, endpoint: %s', model, endpoint);
|
||||
|
||||
@@ -45,10 +63,12 @@ async function createImageTask(payload: CreateImagePayload, apiKey: string): Pro
|
||||
model,
|
||||
parameters: {
|
||||
n: 1,
|
||||
...(params.seed !== undefined ? { seed: params.seed } : {}),
|
||||
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
|
||||
...(params.width && params.height
|
||||
? { size: `${params.width}*${params.height}` }
|
||||
: { size: '1024*1024' }),
|
||||
: params.size
|
||||
? { size: params.size.replaceAll('x', '*') }
|
||||
: { size: '1024*1024' }),
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import debug from 'debug';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
|
||||
const log = debug('lobe-async:context');
|
||||
|
||||
export interface AsyncAuthContext {
|
||||
jwtPayload: JWTPayload;
|
||||
jwtPayload: ClientSecretPayload;
|
||||
secret: string;
|
||||
serverDB?: LobeChatDatabase;
|
||||
userId?: string | null;
|
||||
@@ -19,7 +19,7 @@ export interface AsyncAuthContext {
|
||||
* This is useful for testing when we don't want to mock Next.js' request/response
|
||||
*/
|
||||
export const createAsyncContextInner = async (params?: {
|
||||
jwtPayload?: JWTPayload;
|
||||
jwtPayload?: ClientSecretPayload;
|
||||
secret?: string;
|
||||
userId?: string | null;
|
||||
}): Promise<AsyncAuthContext> => ({
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { User } from 'next-auth';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
|
||||
import {
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
enableClerk,
|
||||
enableNextAuth,
|
||||
} from '@/const/auth';
|
||||
import { ClerkAuth, IClerkAuth } from '@/libs/clerk-auth';
|
||||
|
||||
export interface AuthContext {
|
||||
authorizationHeader?: string | null;
|
||||
clerkAuth?: IClerkAuth;
|
||||
jwtPayload?: JWTPayload | null;
|
||||
jwtPayload?: ClientSecretPayload | null;
|
||||
nextAuth?: User;
|
||||
userId?: string | null;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createCallerFactory } from '@/libs/trpc/edge';
|
||||
import { AuthContext, createContextInner } from '@/libs/trpc/edge/context';
|
||||
import { edgeTrpc as trpc } from '@/libs/trpc/edge/init';
|
||||
import * as utils from '@/utils/server/jwt';
|
||||
import * as utils from '@/utils/server/xor';
|
||||
|
||||
import { jwtPayloadChecker } from './jwtPayload';
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('passwordChecker middleware', () => {
|
||||
it('should call next with jwtPayload in context if access code is correct', async () => {
|
||||
const jwtPayload = { accessCode: '123' };
|
||||
|
||||
vi.spyOn(utils, 'getJWTPayload').mockResolvedValue(jwtPayload);
|
||||
vi.spyOn(utils, 'getXorPayload').mockResolvedValue(jwtPayload);
|
||||
|
||||
ctx = await createContextInner({ authorizationHeader: 'Bearer token' });
|
||||
router = createCaller(ctx);
|
||||
@@ -52,7 +52,7 @@ describe('passwordChecker middleware', () => {
|
||||
|
||||
it('should call next with jwtPayload in context if no access codes are set', async () => {
|
||||
const jwtPayload = {};
|
||||
vi.spyOn(utils, 'getJWTPayload').mockResolvedValue(jwtPayload);
|
||||
vi.spyOn(utils, 'getXorPayload').mockResolvedValue(jwtPayload);
|
||||
|
||||
ctx = await createContextInner({ authorizationHeader: 'Bearer token' });
|
||||
router = createCaller(ctx);
|
||||
@@ -63,7 +63,7 @@ describe('passwordChecker middleware', () => {
|
||||
});
|
||||
it('should call next with jwtPayload in context if access codes is undefined', async () => {
|
||||
const jwtPayload = {};
|
||||
vi.spyOn(utils, 'getJWTPayload').mockResolvedValue(jwtPayload);
|
||||
vi.spyOn(utils, 'getXorPayload').mockResolvedValue(jwtPayload);
|
||||
|
||||
ctx = await createContextInner({ authorizationHeader: 'Bearer token' });
|
||||
router = createCaller(ctx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { edgeTrpc } from '../init';
|
||||
|
||||
@@ -9,7 +9,7 @@ export const jwtPayloadChecker = edgeTrpc.middleware(async (opts) => {
|
||||
|
||||
if (!ctx.authorizationHeader) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
|
||||
const jwtPayload = await getJWTPayload(ctx.authorizationHeader);
|
||||
const jwtPayload = getXorPayload(ctx.authorizationHeader);
|
||||
|
||||
return opts.next({ ctx: { jwtPayload } });
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { User } from 'next-auth';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
JWTPayload,
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
LOBE_CHAT_OIDC_AUTH_HEADER,
|
||||
enableClerk,
|
||||
@@ -29,7 +29,7 @@ export interface OIDCAuth {
|
||||
export interface AuthContext {
|
||||
authorizationHeader?: string | null;
|
||||
clerkAuth?: IClerkAuth;
|
||||
jwtPayload?: JWTPayload | null;
|
||||
jwtPayload?: ClientSecretPayload | null;
|
||||
marketAccessToken?: string;
|
||||
nextAuth?: User;
|
||||
// Add OIDC authentication information
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { trpc } from '../init';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const keyVaults = trpc.middleware(async (opts) => {
|
||||
if (!ctx.authorizationHeader) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
|
||||
try {
|
||||
const jwtPayload = await getJWTPayload(ctx.authorizationHeader);
|
||||
const jwtPayload = getXorPayload(ctx.authorizationHeader);
|
||||
|
||||
return opts.next({ ctx: { jwtPayload } });
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
export default {
|
||||
apikey: {
|
||||
display: {
|
||||
autoGenerated: '自动生成',
|
||||
copy: '复制',
|
||||
copyError: '复制失败',
|
||||
copySuccess: 'API Key 已复制到剪贴板',
|
||||
enterPlaceholder: '请输入',
|
||||
hide: '隐藏',
|
||||
neverExpires: '永不过期',
|
||||
neverUsed: '从未使用',
|
||||
show: '显示',
|
||||
},
|
||||
form: {
|
||||
fields: {
|
||||
expiresAt: {
|
||||
label: '过期时间',
|
||||
placeholder: '永不过期',
|
||||
},
|
||||
name: {
|
||||
label: '名称',
|
||||
placeholder: '请输入 API Key 名称',
|
||||
},
|
||||
},
|
||||
submit: '创建',
|
||||
title: '创建 API Key',
|
||||
},
|
||||
list: {
|
||||
actions: {
|
||||
create: '创建 API Key',
|
||||
delete: '删除',
|
||||
deleteConfirm: {
|
||||
actions: {
|
||||
cancel: '取消',
|
||||
ok: '确认',
|
||||
},
|
||||
content: '确认删除该 API Key 吗?',
|
||||
title: '确认操作',
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
actions: '操作',
|
||||
expiresAt: '过期时间',
|
||||
key: '密钥',
|
||||
lastUsedAt: '最后使用时间',
|
||||
name: '名称',
|
||||
status: '启用状态',
|
||||
},
|
||||
title: 'API Key 列表',
|
||||
},
|
||||
validation: {
|
||||
required: '内容不得为空',
|
||||
},
|
||||
},
|
||||
date: {
|
||||
prevMonth: '上个月',
|
||||
recent30Days: '最近30天',
|
||||
@@ -90,6 +143,7 @@ export default {
|
||||
words: '累计字数',
|
||||
},
|
||||
tab: {
|
||||
apikey: 'API Key 管理',
|
||||
profile: '个人资料',
|
||||
security: '安全',
|
||||
stats: '数据统计',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import {
|
||||
LobeAnthropicAI,
|
||||
LobeAzureOpenAI,
|
||||
@@ -67,7 +67,10 @@ vi.mock('@/config/llm', () => ({
|
||||
describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
describe('should initialize with options correctly', () => {
|
||||
it('OpenAI provider: with apikey and endpoint', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-openai-key', baseURL: 'user-endpoint' };
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-openai-key',
|
||||
baseURL: 'user-endpoint',
|
||||
};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenAI, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
||||
@@ -75,7 +78,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Azure AI provider: with apikey, endpoint and apiversion', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-azure-key',
|
||||
baseURL: 'user-azure-endpoint',
|
||||
azureApiVersion: '2024-06-01',
|
||||
@@ -87,35 +90,35 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('ZhiPu AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'zhipu.user-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'zhipu.user-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.ZhiPu, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeZhipuAI);
|
||||
});
|
||||
|
||||
it('Google provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-google-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-google-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Google, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeGoogleAI);
|
||||
});
|
||||
|
||||
it('Moonshot AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-moonshot-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-moonshot-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Moonshot, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeMoonshotAI);
|
||||
});
|
||||
|
||||
it('Qwen AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-qwen-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-qwen-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeQwenAI);
|
||||
});
|
||||
|
||||
it('Bedrock AI provider: with apikey, awsAccessKeyId, awsSecretAccessKey, awsRegion', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-bedrock-key',
|
||||
awsAccessKeyId: 'user-aws-id',
|
||||
awsSecretAccessKey: 'user-aws-secret',
|
||||
@@ -127,7 +130,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Ollama provider: with endpoint', async () => {
|
||||
const jwtPayload: JWTPayload = { baseURL: 'http://user-ollama-url' };
|
||||
const jwtPayload: ClientSecretPayload = { baseURL: 'http://user-ollama-url' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Ollama, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOllamaAI);
|
||||
@@ -135,49 +138,49 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Perplexity AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-perplexity-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-perplexity-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Perplexity, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobePerplexityAI);
|
||||
});
|
||||
|
||||
it('Anthropic AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-anthropic-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-anthropic-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Anthropic, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeAnthropicAI);
|
||||
});
|
||||
|
||||
it('Minimax AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-minimax-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-minimax-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Minimax, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeMinimaxAI);
|
||||
});
|
||||
|
||||
it('Mistral AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-mistral-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-mistral-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Mistral, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeMistralAI);
|
||||
});
|
||||
|
||||
it('OpenRouter AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-openrouter-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-openrouter-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenRouter, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenRouterAI);
|
||||
});
|
||||
|
||||
it('DeepSeek AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-deepseek-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-deepseek-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.DeepSeek, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeDeepSeekAI);
|
||||
});
|
||||
|
||||
it('Together AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-togetherai-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-togetherai-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.TogetherAI, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeTogetherAI);
|
||||
@@ -205,7 +208,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Unknown Provider: with apikey and endpoint, should initialize to OpenAi', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-unknown-key',
|
||||
baseURL: 'user-unknown-endpoint',
|
||||
};
|
||||
@@ -218,13 +221,13 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
|
||||
describe('should initialize without some options', () => {
|
||||
it('OpenAI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenAI, jwtPayload);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
||||
});
|
||||
|
||||
it('Azure AI Provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
azureApiVersion: 'test-azure-api-version',
|
||||
};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Azure, jwtPayload);
|
||||
@@ -233,7 +236,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('ZhiPu AI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.ZhiPu, jwtPayload);
|
||||
|
||||
// 假设 LobeZhipuAI 是 ZhiPu 提供者的实现类
|
||||
@@ -248,7 +251,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Moonshot AI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Moonshot, jwtPayload);
|
||||
|
||||
// 假设 LobeMoonshotAI 是 Moonshot 提供者的实现类
|
||||
@@ -256,7 +259,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Qwen AI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
|
||||
// 假设 LobeQwenAI 是 Qwen 提供者的实现类
|
||||
@@ -264,7 +267,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Qwen AI provider: without endpoint', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-qwen-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-qwen-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
|
||||
// 假设 LobeQwenAI 是 Qwen 提供者的实现类
|
||||
@@ -356,7 +359,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
it('OpenAI provider: without apikey with OPENAI_PROXY_URL', async () => {
|
||||
process.env.OPENAI_PROXY_URL = 'https://proxy.example.com/v1';
|
||||
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenAI, jwtPayload);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
||||
// 应返回 OPENAI_PROXY_URL
|
||||
@@ -366,7 +369,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
it('Qwen AI provider: without apiKey and endpoint with OPENAI_PROXY_URL', async () => {
|
||||
process.env.OPENAI_PROXY_URL = 'https://proxy.example.com/v1';
|
||||
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
|
||||
// 假设 LobeQwenAI 是 Qwen 提供者的实现类
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getLLMConfig } from '@/config/llm';
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import { AgentRuntime, ModelProvider } from '@/libs/model-runtime';
|
||||
|
||||
import apiKeyManager from './apiKeyManager';
|
||||
@@ -14,7 +14,7 @@ export * from './trace';
|
||||
* @param payload - The JWT payload.
|
||||
* @returns The options object.
|
||||
*/
|
||||
const getParamsFromPayload = (provider: string, payload: JWTPayload) => {
|
||||
const getParamsFromPayload = (provider: string, payload: ClientSecretPayload) => {
|
||||
const llmConfig = getLLMConfig() as Record<string, any>;
|
||||
|
||||
switch (provider) {
|
||||
@@ -115,7 +115,7 @@ const getParamsFromPayload = (provider: string, payload: JWTPayload) => {
|
||||
*/
|
||||
export const initAgentRuntimeWithUserPayload = (
|
||||
provider: string,
|
||||
payload: JWTPayload,
|
||||
payload: ClientSecretPayload,
|
||||
params: any = {},
|
||||
) => {
|
||||
return AgentRuntime.initializeWithProvider(provider, {
|
||||
|
||||
@@ -3,7 +3,7 @@ import superjson from 'superjson';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { serverDBEnv } from '@/config/db';
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { appEnv } from '@/envs/app';
|
||||
import { createAsyncCallerFactory } from '@/libs/trpc/async';
|
||||
@@ -13,7 +13,7 @@ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { asyncRouter } from './index';
|
||||
import type { AsyncRouter } from './index';
|
||||
|
||||
export const createAsyncServerClient = async (userId: string, payload: JWTPayload) => {
|
||||
export const createAsyncServerClient = async (userId: string, payload: ClientSecretPayload) => {
|
||||
const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${serverDBEnv.KEY_VAULTS_SECRET}`,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ApiKeyModel } from '@/database/models/apiKey';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
|
||||
const apiKeyProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
|
||||
const { ctx } = opts;
|
||||
|
||||
const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
|
||||
|
||||
return opts.next({
|
||||
ctx: {
|
||||
apiKeyModel: new ApiKeyModel(ctx.serverDB, ctx.userId),
|
||||
gateKeeper,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const apiKeyRouter = router({
|
||||
createApiKey: apiKeyProcedure
|
||||
.input(
|
||||
z.object({
|
||||
expiresAt: z.date().optional().nullable(),
|
||||
name: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return await ctx.apiKeyModel.create(input, ctx.gateKeeper.encrypt);
|
||||
}),
|
||||
|
||||
deleteAllApiKeys: apiKeyProcedure.mutation(async ({ ctx }) => {
|
||||
return ctx.apiKeyModel.deleteAll();
|
||||
}),
|
||||
|
||||
deleteApiKey: apiKeyProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.apiKeyModel.delete(input.id);
|
||||
}),
|
||||
|
||||
getApiKey: apiKeyProcedure
|
||||
.input(z.object({ apiKey: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.apiKeyModel.findByKey(input.apiKey, ctx.gateKeeper.encrypt);
|
||||
}),
|
||||
|
||||
getApiKeyById: apiKeyProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.apiKeyModel.findById(input.id);
|
||||
}),
|
||||
|
||||
getApiKeys: apiKeyProcedure.query(async ({ ctx }) => {
|
||||
return ctx.apiKeyModel.query(ctx.gateKeeper.decrypt);
|
||||
}),
|
||||
|
||||
updateApiKey: apiKeyProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
value: z.object({
|
||||
description: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
expiresAt: z.date().optional().nullable(),
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return ctx.apiKeyModel.update(input.id, input.value);
|
||||
}),
|
||||
|
||||
validateApiKey: apiKeyProcedure
|
||||
.input(z.object({ key: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.apiKeyModel.validateKey(input.key);
|
||||
}),
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { publicProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { agentRouter } from './agent';
|
||||
import { aiModelRouter } from './aiModel';
|
||||
import { aiProviderRouter } from './aiProvider';
|
||||
import { apiKeyRouter } from './apiKey';
|
||||
import { chunkRouter } from './chunk';
|
||||
import { configRouter } from './config';
|
||||
import { documentRouter } from './document';
|
||||
@@ -31,6 +32,7 @@ export const lambdaRouter = router({
|
||||
agent: agentRouter,
|
||||
aiModel: aiModelRouter,
|
||||
aiProvider: aiProviderRouter,
|
||||
apiKey: apiKeyRouter,
|
||||
chunk: chunkRouter,
|
||||
config: configRouter,
|
||||
document: documentRouter,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
|
||||
import { searchRouter } from './search';
|
||||
|
||||
// Mock JWT verification
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn().mockResolvedValue({ userId: '1' }),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn().mockReturnValue({ userId: '1' }),
|
||||
}));
|
||||
|
||||
vi.mock('@lobechat/web-crawler', () => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import { AsyncTaskModel } from '@/database/models/asyncTask';
|
||||
import { FileModel } from '@/database/models/file';
|
||||
import { serverDB } from '@/database/server';
|
||||
@@ -30,7 +30,7 @@ export class ChunkService {
|
||||
return this.chunkClient.chunkContent(params);
|
||||
}
|
||||
|
||||
async asyncEmbeddingFileChunks(fileId: string, payload: JWTPayload) {
|
||||
async asyncEmbeddingFileChunks(fileId: string, payload: ClientSecretPayload) {
|
||||
const result = await this.fileModel.findById(fileId);
|
||||
|
||||
if (!result) return;
|
||||
@@ -66,7 +66,7 @@ export class ChunkService {
|
||||
/**
|
||||
* parse file to chunks with async task
|
||||
*/
|
||||
async asyncParseFileToChunks(fileId: string, payload: JWTPayload, skipExist?: boolean) {
|
||||
async asyncParseFileToChunks(fileId: string, payload: ClientSecretPayload, skipExist?: boolean) {
|
||||
const result = await this.fileModel.findById(fileId);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { ModelProvider } from '@/libs/model-runtime';
|
||||
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
CloudflareKeyVault,
|
||||
OpenAICompatibleKeyVault,
|
||||
} from '@/types/user/settings';
|
||||
import { createJWT } from '@/utils/jwt';
|
||||
import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
|
||||
|
||||
export const getProviderAuthPayload = (
|
||||
provider: string,
|
||||
@@ -80,7 +80,7 @@ const createAuthTokenWithPayload = async (payload = {}) => {
|
||||
const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState());
|
||||
const userId = userProfileSelectors.userId(useUserStore.getState());
|
||||
|
||||
return createJWT<JWTPayload>({ accessCode, userId, ...payload });
|
||||
return obfuscatePayloadWithXOR<ClientSecretPayload>({ accessCode, userId, ...payload });
|
||||
};
|
||||
|
||||
interface AuthParams {
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum SettingsTabs {
|
||||
Common = 'common',
|
||||
Hotkey = 'hotkey',
|
||||
LLM = 'llm',
|
||||
Plugin = 'plugin',
|
||||
Provider = 'provider',
|
||||
Proxy = 'proxy',
|
||||
Storage = 'storage',
|
||||
@@ -40,6 +41,7 @@ export enum SettingsTabs {
|
||||
}
|
||||
|
||||
export enum ProfileTabs {
|
||||
APIKey = 'apikey',
|
||||
Profile = 'profile',
|
||||
Security = 'security',
|
||||
Stats = 'stats',
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('featureFlagsSelectors', () => {
|
||||
expect(result).toEqual({
|
||||
enableWebrtc: false,
|
||||
isAgentEditable: false,
|
||||
showApiKeyManage: false,
|
||||
enablePlugins: true,
|
||||
showCreateSession: true,
|
||||
showChangelog: true,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export { type ApiKeyItem } from '@/database/schemas/apiKey';
|
||||
|
||||
export interface CreateApiKeyParams {
|
||||
expiresAt?: Date | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UpdateApiKeyParams {
|
||||
enabled?: boolean;
|
||||
expiresAt?: Date | null;
|
||||
name?: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user