Compare commits

...

22 Commits

Author SHA1 Message Date
arvinxx 4fa6d64df2 add plugin settings 2025-07-29 14:48:46 +08:00
Zhijie He 403aebd52e 💄 style: add more OpenAI SDK Text2Image providers (#8573) 2025-07-29 14:45:19 +08:00
LobeHub Bot 356cf0c392 💄 style: update i18n (#8593)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-29 14:37:29 +08:00
Arvin Xu be98d56ef4 ♻️ refactor: refactor jose-JWT to xor obfuscation (#8595)
* refactor jose-jwt to xor obfuscation

* rename JWTPayload type to ClientSecretPayload

* fix tests

* revert next version
2025-07-29 14:37:01 +08:00
lobehubbot 3fae1b2638 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-29 04:07:54 +00:00
semantic-release-bot ee4cc6c2e0 🔖 chore(release): v1.105.1 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-29 04:07:02 +00:00
Zhijie He b8c0e2d639 💄 style: support more Text2Image from Qwen (#8574) 2025-07-29 11:51:40 +08:00
lobehubbot 74d20bdbe8 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 09:12:32 +00:00
semantic-release-bot 7bab44e74c 🔖 chore(release): v1.105.0 [skip ci]
## [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 09:11:38 +00:00
Zephyr fdaa72564c feat: Implement API Key management functionality (#8535)
*  feat: Implement API Key management functionality

- Added new components for API Key management including creation, deletion, and display.
- Introduced a new database schema for storing API Keys.
- Implemented server and client services for API Key operations.
- Integrated API Key management into the profile section with appropriate routing and feature flags.
- Enhanced localization support for API Key related UI elements.

This commit lays the groundwork for managing API Keys within the application, allowing users to create, view, and manage their keys effectively.

* fix: server config unit test

*  feat(database): Create api_keys table with conditional existence check

- Added a conditional check to create the "api_keys" table only if it does not already exist.
- Ensured the foreign key constraint for "user_id" references the "users" table remains intact.

This change enhances the migration process by preventing errors during table creation if the table already exists.

* feat: Implement API Key management interface

- Introduced a new Client component for managing API keys, including creation, updating, and deletion functionalities.
- Replaced the previous page component with the new Client component in the API key management page.
- Removed obsolete client and server service files related to API key management, streamlining the service layer.

This update enhances the user experience by providing a dedicated interface for API key operations.
2025-07-28 16:55:57 +08:00
lobehubbot 7d85151cb6 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 08:53:48 +00:00
semantic-release-bot 23ef2eea59 🔖 chore(release): v1.104.5 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 08:52:59 +00:00
Shinji-Li 74ab822140 💄 style: fix setting window layout when in desktop was disappear (#8585) 2025-07-28 16:37:45 +08:00
lobehubbot d726ff108d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 02:37:48 +00:00
semantic-release-bot dd7b661140 🔖 chore(release): v1.104.4 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 02:36:53 +00:00
Shinji-Li 49023419cf 💄 style: fix setting window layout size (#8483) 2025-07-28 10:21:30 +08:00
LobeHub Bot 2eccbc79eb 💄 style: update i18n (#8579)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-28 10:19:46 +08:00
lobehubbot 3527cb65f1 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-26 09:48:50 +00:00
semantic-release-bot 1731c841d8 🔖 chore(release): v1.104.3 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-26 09:47:57 +00:00
afon 404ac21229 💄 style: Add Gemini 2.5 Flash-Lite GA model (#8539) 2025-07-26 17:32:34 +08:00
Arvin Xu 2eaa2dbea0 🔨 chore: add react scan debugger and bump deps (#8576)
* add REACT_SCAN debug

* upgrade lobehub/ui

* clean

* head

* update
2025-07-26 16:32:55 +08:00
lobehubbot 50c0ed168d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-26 07:32:43 +00:00
109 changed files with 10082 additions and 284 deletions
+126
View File
@@ -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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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>
+42
View File
@@ -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": [
+54
View File
@@ -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": "الإحصائيات"
+3
View File
@@ -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، مصمم للاستخدام واسع النطاق."
},
+54
View File
@@ -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": "Статистика"
+3
View File
@@ -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, проектиран за мащабна употреба."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+1
View File
@@ -535,6 +535,7 @@
"experiment": "Experiment",
"hotkey": "Hotkeys",
"llm": "Language Model",
"plugin": "Plugin Management",
"provider": "AI Service Provider",
"proxy": "Network Proxy",
"storage": "Data Storage",
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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": "آمار"
+3
View File
@@ -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 کوچک‌ترین و مقرون‌به‌صرفه‌ترین مدل گوگل است که برای استفاده در مقیاس بزرگ طراحی شده است."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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": "統計"
+3
View File
@@ -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の最小かつコストパフォーマンスに優れたモデルで、大規模利用を目的に設計されています。"
},
+54
View File
@@ -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": "통계"
+3
View File
@@ -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는 구글의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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": "Статистика"
+3
View File
@@ -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, разработанная для масштабного использования."
},
+54
View File
@@ -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"
+3
View File
@@ -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."
},
+54
View File
@@ -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ê"
+3
View File
@@ -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."
},
+54
View File
@@ -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": "数据统计"
+3
View File
@@ -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
View File
@@ -535,6 +535,7 @@
"experiment": "实验",
"hotkey": "快捷键",
"llm": "语言模型",
"plugin": "插件管理",
"provider": "AI 服务商",
"proxy": "网络代理",
"storage": "数据存储",
+54
View File
@@ -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": "數據統計"
+3
View File
@@ -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
View File
@@ -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;
});
+6 -6
View File
@@ -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';
+5
View File
@@ -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
+269 -2
View File
@@ -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 TransformersDiTs)和配对风格化数据,提升图像到图像(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(又名 “4xAnimeSharp”) 是 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;
+27
View File
@@ -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
View File
@@ -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];
+24 -2
View File
@@ -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;
+67 -2
View File
@@ -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;
+27
View File
@@ -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,
+56 -2
View File
@@ -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;
+62 -2
View File
@@ -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:
'百度自研的iRAGimage 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;
+19 -2
View File
@@ -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;
+7
View File
@@ -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
View File
@@ -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"
+116
View File
@@ -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)));
};
}
+25
View File
@@ -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
View File
@@ -1,5 +1,6 @@
export * from './agent';
export * from './aiInfra';
export * from './apiKey';
export * from './asyncTask';
export * from './document';
export * from './file';
+11 -1
View 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,
});
-15
View File
@@ -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);
-2
View File
@@ -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>
+3 -3
View File
@@ -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);
});
+29 -9
View File
@@ -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: {
+3 -3
View File
@@ -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> => ({
+7 -2
View File
@@ -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);
+2 -2
View File
@@ -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 } });
});
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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) {
+54
View File
@@ -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: '数据统计',
+28 -25
View File
@@ -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 提供者的实现类
+3 -3
View File
@@ -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, {
+2 -2
View File
@@ -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}`,
+80
View File
@@ -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);
}),
});
+2
View File
@@ -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,
+2 -2
View File
@@ -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', () => ({
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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 {
+2
View File
@@ -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',
+1
View File
@@ -19,6 +19,7 @@ describe('featureFlagsSelectors', () => {
expect(result).toEqual({
enableWebrtc: false,
isAgentEditable: false,
showApiKeyManage: false,
enablePlugins: true,
showCreateSession: true,
showChangelog: true,
+12
View File
@@ -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