mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-21 06:29:59 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa6d64df2 | |||
| 403aebd52e | |||
| 356cf0c392 | |||
| be98d56ef4 | |||
| 3fae1b2638 |
@@ -1,4 +1,11 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Support more Text2Image from Qwen."]
|
||||
},
|
||||
"date": "2025-07-29",
|
||||
"version": "1.105.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Implement API Key management functionality."]
|
||||
|
||||
@@ -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": "الإحصائيات"
|
||||
|
||||
@@ -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": "Статистика"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -535,6 +535,7 @@
|
||||
"experiment": "Experiment",
|
||||
"hotkey": "Hotkeys",
|
||||
"llm": "Language Model",
|
||||
"plugin": "Plugin Management",
|
||||
"provider": "AI Service Provider",
|
||||
"proxy": "Network Proxy",
|
||||
"storage": "Data Storage",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Generado automáticamente",
|
||||
"copy": "Copiar",
|
||||
"copyError": "Error al copiar",
|
||||
"copySuccess": "Clave API copiada al portapapeles",
|
||||
"enterPlaceholder": "Por favor ingrese",
|
||||
"hide": "Ocultar",
|
||||
"neverExpires": "Nunca expira",
|
||||
"neverUsed": "Nunca usado",
|
||||
"show": "Mostrar"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Fecha de expiración",
|
||||
"placeholder": "Nunca expira"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nombre",
|
||||
"placeholder": "Por favor ingrese el nombre de la clave API"
|
||||
}
|
||||
},
|
||||
"submit": "Crear",
|
||||
"title": "Crear Clave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Crear Clave API",
|
||||
"delete": "Eliminar",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Confirmar"
|
||||
},
|
||||
"content": "¿Está seguro de eliminar esta clave API?",
|
||||
"title": "Confirmar acción"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Acciones",
|
||||
"expiresAt": "Fecha de expiración",
|
||||
"key": "Clave",
|
||||
"lastUsedAt": "Último uso",
|
||||
"name": "Nombre",
|
||||
"status": "Estado"
|
||||
},
|
||||
"title": "Lista de Claves API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "El contenido no puede estar vacío"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Último mes",
|
||||
"recent30Days": "Últimos 30 días"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Palabras"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestión de Claves API",
|
||||
"profile": "Perfil",
|
||||
"security": "Seguridad",
|
||||
"stats": "Estadísticas"
|
||||
|
||||
@@ -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": "آمار"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "統計"
|
||||
|
||||
@@ -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": "통계"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "Статистика"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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ê"
|
||||
|
||||
@@ -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": "数据统计"
|
||||
|
||||
@@ -535,6 +535,7 @@
|
||||
"experiment": "实验",
|
||||
"hotkey": "快捷键",
|
||||
"llm": "语言模型",
|
||||
"plugin": "插件管理",
|
||||
"provider": "AI 服务商",
|
||||
"proxy": "网络代理",
|
||||
"storage": "数据存储",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自動生成",
|
||||
"copy": "複製",
|
||||
"copyError": "複製失敗",
|
||||
"copySuccess": "API Key 已複製到剪貼簿",
|
||||
"enterPlaceholder": "請輸入",
|
||||
"hide": "隱藏",
|
||||
"neverExpires": "永不過期",
|
||||
"neverUsed": "從未使用",
|
||||
"show": "顯示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "過期時間",
|
||||
"placeholder": "永不過期"
|
||||
},
|
||||
"name": {
|
||||
"label": "名稱",
|
||||
"placeholder": "請輸入 API Key 名稱"
|
||||
}
|
||||
},
|
||||
"submit": "建立",
|
||||
"title": "建立 API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "建立 API Key",
|
||||
"delete": "刪除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"ok": "確認"
|
||||
},
|
||||
"content": "確認刪除該 API Key 嗎?",
|
||||
"title": "確認操作"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "過期時間",
|
||||
"key": "密鑰",
|
||||
"lastUsedAt": "最後使用時間",
|
||||
"name": "名稱",
|
||||
"status": "啟用狀態"
|
||||
},
|
||||
"title": "API Key 列表"
|
||||
},
|
||||
"validation": {
|
||||
"required": "內容不得為空"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "上個月",
|
||||
"recent30Days": "最近30天"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "總字數"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key 管理",
|
||||
"profile": "個人資料",
|
||||
"security": "安全",
|
||||
"stats": "數據統計"
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AgentRuntimeError } from '@/libs/model-runtime';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { RequestHandler, checkAuth } from './index';
|
||||
import { checkAuthMethod } from './utils';
|
||||
@@ -20,8 +20,8 @@ vi.mock('./utils', () => ({
|
||||
checkAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn(),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('checkAuth', () => {
|
||||
@@ -50,7 +50,7 @@ describe('checkAuth', () => {
|
||||
it('should return error response on getJWTPayload error', async () => {
|
||||
const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
mockRequest.headers.set('Authorization', 'invalid');
|
||||
vi.mocked(getJWTPayload).mockRejectedValueOnce(mockError);
|
||||
vi.mocked(getXorPayload).mockRejectedValueOnce(mockError);
|
||||
|
||||
await checkAuth(mockHandler)(mockRequest, mockOptions);
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('checkAuth', () => {
|
||||
it('should return error response on checkAuthMethod error', async () => {
|
||||
const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
mockRequest.headers.set('Authorization', 'valid');
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({});
|
||||
vi.mocked(getXorPayload).mockResolvedValueOnce({});
|
||||
vi.mocked(checkAuthMethod).mockImplementationOnce(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AuthObject } from '@clerk/backend';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
JWTPayload,
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
LOBE_CHAT_OIDC_AUTH_HEADER,
|
||||
OAUTH_AUTHORIZED,
|
||||
@@ -13,18 +13,18 @@ import { AgentRuntime, AgentRuntimeError, ChatCompletionErrorPayload } from '@/l
|
||||
import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { checkAuthMethod } from './utils';
|
||||
|
||||
type CreateRuntime = (jwtPayload: JWTPayload) => AgentRuntime;
|
||||
type CreateRuntime = (jwtPayload: ClientSecretPayload) => AgentRuntime;
|
||||
type RequestOptions = { createRuntime?: CreateRuntime; params: Promise<{ provider: string }> };
|
||||
|
||||
export type RequestHandler = (
|
||||
req: Request,
|
||||
options: RequestOptions & {
|
||||
createRuntime?: CreateRuntime;
|
||||
jwtPayload: JWTPayload;
|
||||
jwtPayload: ClientSecretPayload;
|
||||
},
|
||||
) => Promise<Response>;
|
||||
|
||||
@@ -36,7 +36,7 @@ export const checkAuth =
|
||||
return handler(req, { ...options, jwtPayload: { userId: 'DEV_USER' } });
|
||||
}
|
||||
|
||||
let jwtPayload: JWTPayload;
|
||||
let jwtPayload: ClientSecretPayload;
|
||||
|
||||
try {
|
||||
// get Authorization from header
|
||||
@@ -55,7 +55,7 @@ export const checkAuth =
|
||||
clerkAuth = data.clerkAuth;
|
||||
}
|
||||
|
||||
jwtPayload = await getJWTPayload(authorization);
|
||||
jwtPayload = getXorPayload(authorization);
|
||||
|
||||
const oidcAuthorization = req.headers.get(LOBE_CHAT_OIDC_AUTH_HEADER);
|
||||
let isUseOidcAuth = false;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { checkAuthMethod } from '@/app/(backend)/middleware/auth/utils';
|
||||
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
|
||||
import { AgentRuntime, LobeRuntimeAI } from '@/libs/model-runtime';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { POST } from './route';
|
||||
|
||||
@@ -18,8 +18,8 @@ vi.mock('@/app/(backend)/middleware/auth/utils', () => ({
|
||||
checkAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn(),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn(),
|
||||
}));
|
||||
|
||||
// 定义一个变量来存储 enableAuth 的值
|
||||
@@ -61,7 +61,7 @@ describe('POST handler', () => {
|
||||
const mockParams = Promise.resolve({ provider: 'test-provider' });
|
||||
|
||||
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -78,7 +78,7 @@ describe('POST handler', () => {
|
||||
await POST(request as unknown as Request, { params: mockParams });
|
||||
|
||||
// 验证是否正确调用了模拟函数
|
||||
expect(getJWTPayload).toHaveBeenCalledWith('Bearer some-valid-token');
|
||||
expect(getXorPayload).toHaveBeenCalledWith('Bearer some-valid-token');
|
||||
expect(spy).toHaveBeenCalledWith('test-provider', expect.anything());
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('POST handler', () => {
|
||||
it('should have pass clerk Auth when enable clerk', async () => {
|
||||
enableClerk = true;
|
||||
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -142,7 +142,9 @@ describe('POST handler', () => {
|
||||
|
||||
it('should return InternalServerError error when throw a unknown error', async () => {
|
||||
const mockParams = Promise.resolve({ provider: 'test-provider' });
|
||||
vi.mocked(getJWTPayload).mockRejectedValueOnce(new Error('unknown error'));
|
||||
vi.mocked(getXorPayload).mockImplementationOnce(() => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
|
||||
const response = await POST(request, { params: mockParams });
|
||||
|
||||
@@ -159,7 +161,7 @@ describe('POST handler', () => {
|
||||
|
||||
describe('chat', () => {
|
||||
it('should correctly handle chat completion with valid payload', async () => {
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -189,7 +191,7 @@ describe('POST handler', () => {
|
||||
|
||||
it('should return an error response when chat completion fails', async () => {
|
||||
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AgentRuntimeError } from '@/libs/model-runtime';
|
||||
import { TraceClient } from '@/libs/traces';
|
||||
import { ChatErrorType, ErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
import { getTracePayload } from '@/utils/trace';
|
||||
|
||||
import { parserPluginSettings } from './settings';
|
||||
@@ -44,7 +44,7 @@ export const POST = async (req: Request) => {
|
||||
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
|
||||
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
||||
const payload = await getJWTPayload(authorization);
|
||||
const payload = getXorPayload(authorization);
|
||||
|
||||
const result = checkAuth(payload.accessCode!, oauthAuthorized);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
const giteeaiChatModels: AIChatModelCard[] = [
|
||||
{
|
||||
@@ -222,6 +222,273 @@ const giteeaiChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...giteeaiChatModels];
|
||||
const giteeaiImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'FLUX.1-dev 是由 Black Forest Labs 开发的一款开源 多模态语言模型(Multimodal Language Model, MLLM),专为图文任务优化,融合了图像和文本的理解与生成能力。它建立在先进的大语言模型(如 Mistral-7B)基础上,通过精心设计的视觉编码器与多阶段指令微调,实现了图文协同处理与复杂任务推理的能力。',
|
||||
displayName: 'FLUX.1-dev',
|
||||
enabled: true,
|
||||
id: 'FLUX.1-dev',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '1536x1536'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'由 Black Forest Labs 开发的 120 亿参数文生图模型,采用潜在对抗扩散蒸馏技术,能够在 1 到 4 步内生成高质量图像。该模型性能媲美闭源替代品,并在 Apache-2.0 许可证下发布,适用于个人、科研和商业用途。',
|
||||
displayName: 'flux-1-schnell',
|
||||
enabled: true,
|
||||
id: 'flux-1-schnell',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '1536x1536', '2048x2048'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'FLUX.1-Kontext-dev 是由 Black Forest Labs 开发的一款基于 Rectified Flow Transformer 架构 的多模态图像生成与编辑模型,拥有 12B(120 亿)参数规模,专注于在给定上下文条件下生成、重构、增强或编辑图像。该模型结合了扩散模型的可控生成优势与 Transformer 的上下文建模能力,支持高质量图像输出,广泛适用于图像修复、图像补全、视觉场景重构等任务。',
|
||||
displayName: 'FLUX.1-Kontext-dev',
|
||||
enabled: true,
|
||||
id: 'FLUX.1-Kontext-dev',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '1536x1536', '2048x2048'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Stable Diffusion 3.5 Large Turbo 专注于高质量图像生成,具备强大的细节表现力和场景还原能力。',
|
||||
displayName: 'stable-diffusion-3.5-large-turbo',
|
||||
enabled: true,
|
||||
id: 'stable-diffusion-3.5-large-turbo',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'由 Stability AI 推出的最新文生图大模型。这一版本在继承了前代的优点上,对图像质量、文本理解和风格多样性等方面进行了显著改进,能够更准确地解读复杂的自然语言提示,并生成更为精确和多样化的图像。',
|
||||
displayName: 'stable-diffusion-3-medium',
|
||||
enabled: true,
|
||||
id: 'stable-diffusion-3-medium',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'由 Stability AI 开发并开源的文生图大模型,其创意图像生成能力位居行业前列。具备出色的指令理解能力,能够支持反向 Prompt 定义来精确生成内容。',
|
||||
displayName: 'stable-diffusion-xl-base-1.0',
|
||||
enabled: true,
|
||||
id: 'stable-diffusion-xl-base-1.0',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Kolors 是由快手 Kolors 团队开发的文生图模型。由数十亿的参数训练,在视觉质量、中文语义理解和文本渲染方面有显著优势。',
|
||||
displayName: 'Kolors',
|
||||
enabled: true,
|
||||
id: 'Kolors',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'hunyuandit-v1.2-distilled 是一款轻量级的文生图模型,经过蒸馏优化,能够快速生成高质量的图像,特别适用于低资源环境和实时生成任务。',
|
||||
displayName: 'HunyuanDiT-v1.2-Diffusers-Distilled',
|
||||
enabled: true,
|
||||
id: 'HunyuanDiT-v1.2-Diffusers-Distilled',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HiDream-I1 是一个全新的开源图像生成基础模型,是由国内企业智象未来开源的。拥有 170 亿参数(Flux是12B参数),能够在几秒内实现行业领先的图像生成质量。',
|
||||
displayName: 'HiDream-I1-Full',
|
||||
enabled: true,
|
||||
id: 'HiDream-I1-Full',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HiDream-E1-Full 是由智象未来(HiDream.ai)推出的一款 开源多模态图像编辑大模型,基于先进的 Diffusion Transformer 架构,并结合强大的语言理解能力(内嵌 LLaMA 3.1-8B-Instruct),支持通过自然语言指令进行图像生成、风格迁移、局部编辑和内容重绘,具备出色的图文理解与执行能力。',
|
||||
displayName: 'HiDream-E1-Full',
|
||||
enabled: true,
|
||||
id: 'HiDream-I1-Full',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'HelloMeme 是一个可以根据你提供的图片或动作,自动生成表情包、动图或短视频的 AI 工具。它不需要你有任何绘画或编程基础,只需要准备好参考图片,它就能帮你做出好看、有趣、风格一致的内容。',
|
||||
displayName: 'HelloMeme',
|
||||
enabled: true,
|
||||
id: 'HelloMeme',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'OmniConsistency 通过引入大规模 Diffusion Transformers(DiTs)和配对风格化数据,提升图像到图像(Image-to-Image)任务中的风格一致性和泛化能力,避免风格退化。',
|
||||
displayName: 'OmniConsistency',
|
||||
enabled: true,
|
||||
id: 'OmniConsistency',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'InstantCharacter 是由腾讯 AI 团队在 2025 年发布的一款 无需微调(tuning-free) 的个性化角色生成模型,旨在实现高保真、跨场景的一致角色生成。该模型支持仅基于 一张参考图像 对角色进行建模,并能够将该角色灵活迁移到各种风格、动作和背景中。',
|
||||
displayName: 'InstantCharacter',
|
||||
enabled: true,
|
||||
id: 'InstantCharacter',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'DreamO 是由字节跳动与北京大学联合研发的开源图像定制生成模型,旨在通过统一架构支持多任务图像生成。它采用高效的组合建模方法,可根据用户指定的身份、主体、风格、背景等多个条件生成高度一致且定制化的图像。',
|
||||
displayName: 'DreamO',
|
||||
enabled: true,
|
||||
id: 'DreamO',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'AnimeSharp(又名 “4x‑AnimeSharp”) 是 Kim2091 基于 ESRGAN 架构开发的开源超分辨率模型,专注于动漫风格图像的放大与锐化。它于 2022 年 2 月重命名自 “4x-TextSharpV1”,原本也适用于文字图像但性能针对动漫内容进行了大幅优化',
|
||||
displayName: 'AnimeSharp',
|
||||
enabled: true,
|
||||
id: 'AnimeSharp',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024'],
|
||||
},
|
||||
},
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...giteeaiChatModels, ...giteeaiImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// https://siliconflow.cn/zh-cn/models
|
||||
const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
@@ -830,6 +830,28 @@ const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...siliconcloudChatModels];
|
||||
const siliconcloudImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'Kolors 是由快手 Kolors 团队开发的基于潜在扩散的大规模文本到图像生成模型。该模型通过数十亿文本-图像对的训练,在视觉质量、复杂语义准确性以及中英文字符渲染方面展现出显著优势。它不仅支持中英文输入,在理解和生成中文特定内容方面也表现出色',
|
||||
displayName: 'Kolors',
|
||||
enabled: true,
|
||||
id: 'Kwai-Kolors/Kolors',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '960x1280', '768x1024', '720x1440', '720x1280'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2024-07-06',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...siliconcloudChatModels, ...siliconcloudImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// https://platform.stepfun.com/docs/pricing/details
|
||||
|
||||
@@ -275,6 +275,71 @@ const stepfunChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...stepfunChatModels];
|
||||
const stepfunImageModels: AIImageModelCard[] = [
|
||||
// https://platform.stepfun.com/docs/llm/image
|
||||
{
|
||||
description:
|
||||
'阶跃星辰新一代生图模型,该模型专注于图像生成任务,能够根据用户提供的文本描述,生成高质量的图像。新模型生成图片质感更真实,中英文文字生成能力更强。',
|
||||
displayName: 'Step 2X Large',
|
||||
enabled: true,
|
||||
id: 'step-2x-large',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['256x256', '512x512', '768x768', '1024x1024', '1280x800', '800x1280'],
|
||||
},
|
||||
steps: { default: 50, max: 100, min: 1 },
|
||||
},
|
||||
releasedAt: '2024-08-07',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'该模型拥有强大的图像生成能力,支持文本描述作为输入方式。具备原生的中文支持,能够更好的理解和处理中文文本描述,并且能够更准确地捕捉文本描述中的语义信息,并将其转化为图像特征,从而实现更精准的图像生成。模型能够根据输入生成高分辨率、高质量的图像,并具备一定的风格迁移能力。',
|
||||
displayName: 'Step 1X Medium',
|
||||
enabled: true,
|
||||
id: 'step-1x-medium',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['256x256', '512x512', '768x768', '1024x1024', '1280x800', '800x1280'],
|
||||
},
|
||||
steps: { default: 50, max: 100, min: 1 },
|
||||
},
|
||||
releasedAt: '2025-07-15',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'该模型专注于图像编辑任务,能够根据用户提供的图片和文本描述,对图片进行修改和增强。支持多种输入格式,包括文本描述和示例图像。模型能够理解用户的意图,并生成符合要求的图像编辑结果。',
|
||||
displayName: 'Step 1X Edit',
|
||||
enabled: true,
|
||||
id: 'step-1x-edit',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x512', '768x768', '1024x1024'],
|
||||
},
|
||||
steps: { default: 28, max: 100, min: 1 },
|
||||
},
|
||||
releasedAt: '2025-03-04',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...stepfunChatModels, ...stepfunImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// modelInfo https://www.volcengine.com/docs/82379/1330310
|
||||
// pricing https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
|
||||
@@ -509,6 +509,60 @@ const doubaoChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...doubaoChatModels];
|
||||
const volcengineImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
/*
|
||||
// TODO: AIImageModelCard 不支持 config.deploymentName
|
||||
config: {
|
||||
deploymentName: 'doubao-seedream-3-0-t2i-250415',
|
||||
},
|
||||
*/
|
||||
description:
|
||||
'Doubao图片生成模型由字节跳动 Seed 团队研发,支持文字与图片输入,提供高可控、高质量的图片生成体验。基于文本提示词生成图片。',
|
||||
displayName: 'Doubao Seedream 3.0 t2i',
|
||||
enabled: true,
|
||||
id: 'doubao-seedream-3-0-t2i-250415',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '864x1152', '1152x864', '1280x720', '720x1280', '832x1248', '1248x832', '1512x648'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-04-15',
|
||||
type: 'image',
|
||||
},
|
||||
/*
|
||||
// Note: Doubao 图生图模型与文生图模型公用一个 Endpoint,当前如果存在 imageUrl 会切换至 edit endpoint 下
|
||||
{
|
||||
config: {
|
||||
deploymentName: 'doubao-seededit-3-0-i2i-250628',
|
||||
},
|
||||
description:
|
||||
'Doubao图片生成模型由字节跳动 Seed 团队研发,支持文字与图片输入,提供高可控、高质量的图片生成体验。支持通过文本指令编辑图像,生成图像的边长在512~1536之间。',
|
||||
displayName: 'Doubao SeedEdit 3.0 i2i',
|
||||
enabled: true,
|
||||
id: 'doubao-seededit-3-0-i2i-250628',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '864x1152', '1152x864', '1280x720', '720x1280', '832x1248', '1248x832', '1512x648'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-06-28',
|
||||
type: 'image',
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
export const allModels = [...doubaoChatModels, ...volcengineImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
const wenxinChatModels: AIChatModelCard[] = [
|
||||
{
|
||||
@@ -564,6 +564,66 @@ const wenxinChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...wenxinChatModels];
|
||||
const wenxinImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'百度自研的iRAG(image based RAG),检索增强的文生图技术,将百度搜索的亿级图片资源跟强大的基础模型能力相结合,就可以生成各种超真实的图片,整体效果远远超过文生图原生系统,去掉了AI味儿,而且成本很低。iRAG具备无幻觉、超真实、立等可取等特点。',
|
||||
displayName: 'ERNIE iRAG',
|
||||
enabled: true,
|
||||
id: 'irag-1.0',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['768x768', '1024x1024', '1536x1536', '2048x2048', '1024x768', '2048x1536', '768x1024', '1536x2048', '1024x576', '2048x1152', '576x1024', '1152x2048'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-02-05',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'百度自研的ERNIE iRAG Edit图像编辑模型支持基于图片进行erase(消除对象)、repaint(重绘对象)、variation(生成变体)等操作。',
|
||||
displayName: 'ERNIE iRAG Edit',
|
||||
enabled: true,
|
||||
id: 'ernie-irag-edit',
|
||||
parameters: {
|
||||
imageUrl: { default: null },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['768x768', '1024x1024', '1536x1536', '2048x2048', '1024x768', '2048x1536', '768x1024', '1536x2048', '1024x576', '2048x1152', '576x1024', '1152x2048'],
|
||||
},
|
||||
},
|
||||
releasedAt: '2025-04-17',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'具有120亿参数的修正流变换器,能够根据文本描述生成图像。',
|
||||
displayName: 'FLUX.1-schnell',
|
||||
enabled: true,
|
||||
id: 'flux.1-schnell',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['768x768', '1024x1024', '1536x1536', '2048x2048', '1024x768', '2048x1536', '768x1024', '1536x2048', '1024x576', '2048x1152', '576x1024', '1152x2048'],
|
||||
},
|
||||
steps: { default: 25, max: 50, min: 1 },
|
||||
},
|
||||
releasedAt: '2025-03-27',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...wenxinChatModels, ...wenxinImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatModelCard } from '@/types/aiModel';
|
||||
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
||||
|
||||
// https://docs.x.ai/docs/models
|
||||
const xaiChatModels: AIChatModelCard[] = [
|
||||
@@ -158,6 +158,23 @@ const xaiChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...xaiChatModels];
|
||||
const xaiImageModels: AIImageModelCard[] = [
|
||||
{
|
||||
description:
|
||||
'我们最新的图像生成模型可以根据文本提示生成生动逼真的图像。它在营销、社交媒体和娱乐等领域的图像生成方面表现出色。',
|
||||
displayName: 'Grok 2 Image 1212',
|
||||
enabled: true,
|
||||
id: 'grok-2-image-1212',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
releasedAt: '2024-12-12',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
export const allModels = [...xaiChatModels, ...xaiImageModels];
|
||||
|
||||
export default allModels;
|
||||
|
||||
+2
-3
@@ -9,11 +9,10 @@ export const LOBE_CHAT_OIDC_AUTH_HEADER = 'Oidc-Auth';
|
||||
|
||||
export const OAUTH_AUTHORIZED = 'X-oauth-authorized';
|
||||
|
||||
export const JWT_SECRET_KEY = 'LobeHub · LobeChat';
|
||||
export const NON_HTTP_PREFIX = 'http_nosafe';
|
||||
export const SECRET_XOR_KEY = 'LobeHub · LobeHub';
|
||||
|
||||
/* eslint-disable typescript-sort-keys/interface */
|
||||
export interface JWTPayload {
|
||||
export interface ClientSecretPayload {
|
||||
/**
|
||||
* password
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import debug from 'debug';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { LobeChatDatabase } from '@/database/type';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
|
||||
const log = debug('lobe-async:context');
|
||||
|
||||
export interface AsyncAuthContext {
|
||||
jwtPayload: JWTPayload;
|
||||
jwtPayload: ClientSecretPayload;
|
||||
secret: string;
|
||||
serverDB?: LobeChatDatabase;
|
||||
userId?: string | null;
|
||||
@@ -19,7 +19,7 @@ export interface AsyncAuthContext {
|
||||
* This is useful for testing when we don't want to mock Next.js' request/response
|
||||
*/
|
||||
export const createAsyncContextInner = async (params?: {
|
||||
jwtPayload?: JWTPayload;
|
||||
jwtPayload?: ClientSecretPayload;
|
||||
secret?: string;
|
||||
userId?: string | null;
|
||||
}): Promise<AsyncAuthContext> => ({
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { User } from 'next-auth';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
|
||||
import {
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
enableClerk,
|
||||
enableNextAuth,
|
||||
} from '@/const/auth';
|
||||
import { ClerkAuth, IClerkAuth } from '@/libs/clerk-auth';
|
||||
|
||||
export interface AuthContext {
|
||||
authorizationHeader?: string | null;
|
||||
clerkAuth?: IClerkAuth;
|
||||
jwtPayload?: JWTPayload | null;
|
||||
jwtPayload?: ClientSecretPayload | null;
|
||||
nextAuth?: User;
|
||||
userId?: string | null;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createCallerFactory } from '@/libs/trpc/edge';
|
||||
import { AuthContext, createContextInner } from '@/libs/trpc/edge/context';
|
||||
import { edgeTrpc as trpc } from '@/libs/trpc/edge/init';
|
||||
import * as utils from '@/utils/server/jwt';
|
||||
import * as utils from '@/utils/server/xor';
|
||||
|
||||
import { jwtPayloadChecker } from './jwtPayload';
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('passwordChecker middleware', () => {
|
||||
it('should call next with jwtPayload in context if access code is correct', async () => {
|
||||
const jwtPayload = { accessCode: '123' };
|
||||
|
||||
vi.spyOn(utils, 'getJWTPayload').mockResolvedValue(jwtPayload);
|
||||
vi.spyOn(utils, 'getXorPayload').mockResolvedValue(jwtPayload);
|
||||
|
||||
ctx = await createContextInner({ authorizationHeader: 'Bearer token' });
|
||||
router = createCaller(ctx);
|
||||
@@ -52,7 +52,7 @@ describe('passwordChecker middleware', () => {
|
||||
|
||||
it('should call next with jwtPayload in context if no access codes are set', async () => {
|
||||
const jwtPayload = {};
|
||||
vi.spyOn(utils, 'getJWTPayload').mockResolvedValue(jwtPayload);
|
||||
vi.spyOn(utils, 'getXorPayload').mockResolvedValue(jwtPayload);
|
||||
|
||||
ctx = await createContextInner({ authorizationHeader: 'Bearer token' });
|
||||
router = createCaller(ctx);
|
||||
@@ -63,7 +63,7 @@ describe('passwordChecker middleware', () => {
|
||||
});
|
||||
it('should call next with jwtPayload in context if access codes is undefined', async () => {
|
||||
const jwtPayload = {};
|
||||
vi.spyOn(utils, 'getJWTPayload').mockResolvedValue(jwtPayload);
|
||||
vi.spyOn(utils, 'getXorPayload').mockResolvedValue(jwtPayload);
|
||||
|
||||
ctx = await createContextInner({ authorizationHeader: 'Bearer token' });
|
||||
router = createCaller(ctx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { edgeTrpc } from '../init';
|
||||
|
||||
@@ -9,7 +9,7 @@ export const jwtPayloadChecker = edgeTrpc.middleware(async (opts) => {
|
||||
|
||||
if (!ctx.authorizationHeader) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
|
||||
const jwtPayload = await getJWTPayload(ctx.authorizationHeader);
|
||||
const jwtPayload = getXorPayload(ctx.authorizationHeader);
|
||||
|
||||
return opts.next({ ctx: { jwtPayload } });
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { User } from 'next-auth';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
JWTPayload,
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
LOBE_CHAT_OIDC_AUTH_HEADER,
|
||||
enableClerk,
|
||||
@@ -29,7 +29,7 @@ export interface OIDCAuth {
|
||||
export interface AuthContext {
|
||||
authorizationHeader?: string | null;
|
||||
clerkAuth?: IClerkAuth;
|
||||
jwtPayload?: JWTPayload | null;
|
||||
jwtPayload?: ClientSecretPayload | null;
|
||||
marketAccessToken?: string;
|
||||
nextAuth?: User;
|
||||
// Add OIDC authentication information
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { trpc } from '../init';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const keyVaults = trpc.middleware(async (opts) => {
|
||||
if (!ctx.authorizationHeader) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
|
||||
try {
|
||||
const jwtPayload = await getJWTPayload(ctx.authorizationHeader);
|
||||
const jwtPayload = getXorPayload(ctx.authorizationHeader);
|
||||
|
||||
return opts.next({ ctx: { jwtPayload } });
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import {
|
||||
LobeAnthropicAI,
|
||||
LobeAzureOpenAI,
|
||||
@@ -67,7 +67,10 @@ vi.mock('@/config/llm', () => ({
|
||||
describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
describe('should initialize with options correctly', () => {
|
||||
it('OpenAI provider: with apikey and endpoint', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-openai-key', baseURL: 'user-endpoint' };
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-openai-key',
|
||||
baseURL: 'user-endpoint',
|
||||
};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenAI, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
||||
@@ -75,7 +78,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Azure AI provider: with apikey, endpoint and apiversion', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-azure-key',
|
||||
baseURL: 'user-azure-endpoint',
|
||||
azureApiVersion: '2024-06-01',
|
||||
@@ -87,35 +90,35 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('ZhiPu AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'zhipu.user-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'zhipu.user-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.ZhiPu, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeZhipuAI);
|
||||
});
|
||||
|
||||
it('Google provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-google-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-google-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Google, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeGoogleAI);
|
||||
});
|
||||
|
||||
it('Moonshot AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-moonshot-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-moonshot-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Moonshot, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeMoonshotAI);
|
||||
});
|
||||
|
||||
it('Qwen AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-qwen-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-qwen-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeQwenAI);
|
||||
});
|
||||
|
||||
it('Bedrock AI provider: with apikey, awsAccessKeyId, awsSecretAccessKey, awsRegion', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-bedrock-key',
|
||||
awsAccessKeyId: 'user-aws-id',
|
||||
awsSecretAccessKey: 'user-aws-secret',
|
||||
@@ -127,7 +130,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Ollama provider: with endpoint', async () => {
|
||||
const jwtPayload: JWTPayload = { baseURL: 'http://user-ollama-url' };
|
||||
const jwtPayload: ClientSecretPayload = { baseURL: 'http://user-ollama-url' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Ollama, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOllamaAI);
|
||||
@@ -135,49 +138,49 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Perplexity AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-perplexity-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-perplexity-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Perplexity, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobePerplexityAI);
|
||||
});
|
||||
|
||||
it('Anthropic AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-anthropic-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-anthropic-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Anthropic, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeAnthropicAI);
|
||||
});
|
||||
|
||||
it('Minimax AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-minimax-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-minimax-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Minimax, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeMinimaxAI);
|
||||
});
|
||||
|
||||
it('Mistral AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-mistral-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-mistral-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Mistral, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeMistralAI);
|
||||
});
|
||||
|
||||
it('OpenRouter AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-openrouter-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-openrouter-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenRouter, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenRouterAI);
|
||||
});
|
||||
|
||||
it('DeepSeek AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-deepseek-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-deepseek-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.DeepSeek, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeDeepSeekAI);
|
||||
});
|
||||
|
||||
it('Together AI provider: with apikey', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-togetherai-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-togetherai-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.TogetherAI, jwtPayload);
|
||||
expect(runtime).toBeInstanceOf(AgentRuntime);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeTogetherAI);
|
||||
@@ -205,7 +208,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Unknown Provider: with apikey and endpoint, should initialize to OpenAi', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
apiKey: 'user-unknown-key',
|
||||
baseURL: 'user-unknown-endpoint',
|
||||
};
|
||||
@@ -218,13 +221,13 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
|
||||
describe('should initialize without some options', () => {
|
||||
it('OpenAI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenAI, jwtPayload);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
||||
});
|
||||
|
||||
it('Azure AI Provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {
|
||||
const jwtPayload: ClientSecretPayload = {
|
||||
azureApiVersion: 'test-azure-api-version',
|
||||
};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Azure, jwtPayload);
|
||||
@@ -233,7 +236,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('ZhiPu AI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.ZhiPu, jwtPayload);
|
||||
|
||||
// 假设 LobeZhipuAI 是 ZhiPu 提供者的实现类
|
||||
@@ -248,7 +251,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Moonshot AI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Moonshot, jwtPayload);
|
||||
|
||||
// 假设 LobeMoonshotAI 是 Moonshot 提供者的实现类
|
||||
@@ -256,7 +259,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Qwen AI provider: without apikey', async () => {
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
|
||||
// 假设 LobeQwenAI 是 Qwen 提供者的实现类
|
||||
@@ -264,7 +267,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
});
|
||||
|
||||
it('Qwen AI provider: without endpoint', async () => {
|
||||
const jwtPayload: JWTPayload = { apiKey: 'user-qwen-key' };
|
||||
const jwtPayload: ClientSecretPayload = { apiKey: 'user-qwen-key' };
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
|
||||
// 假设 LobeQwenAI 是 Qwen 提供者的实现类
|
||||
@@ -356,7 +359,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
it('OpenAI provider: without apikey with OPENAI_PROXY_URL', async () => {
|
||||
process.env.OPENAI_PROXY_URL = 'https://proxy.example.com/v1';
|
||||
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.OpenAI, jwtPayload);
|
||||
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
||||
// 应返回 OPENAI_PROXY_URL
|
||||
@@ -366,7 +369,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
|
||||
it('Qwen AI provider: without apiKey and endpoint with OPENAI_PROXY_URL', async () => {
|
||||
process.env.OPENAI_PROXY_URL = 'https://proxy.example.com/v1';
|
||||
|
||||
const jwtPayload: JWTPayload = {};
|
||||
const jwtPayload: ClientSecretPayload = {};
|
||||
const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Qwen, jwtPayload);
|
||||
|
||||
// 假设 LobeQwenAI 是 Qwen 提供者的实现类
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getLLMConfig } from '@/config/llm';
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import { AgentRuntime, ModelProvider } from '@/libs/model-runtime';
|
||||
|
||||
import apiKeyManager from './apiKeyManager';
|
||||
@@ -14,7 +14,7 @@ export * from './trace';
|
||||
* @param payload - The JWT payload.
|
||||
* @returns The options object.
|
||||
*/
|
||||
const getParamsFromPayload = (provider: string, payload: JWTPayload) => {
|
||||
const getParamsFromPayload = (provider: string, payload: ClientSecretPayload) => {
|
||||
const llmConfig = getLLMConfig() as Record<string, any>;
|
||||
|
||||
switch (provider) {
|
||||
@@ -115,7 +115,7 @@ const getParamsFromPayload = (provider: string, payload: JWTPayload) => {
|
||||
*/
|
||||
export const initAgentRuntimeWithUserPayload = (
|
||||
provider: string,
|
||||
payload: JWTPayload,
|
||||
payload: ClientSecretPayload,
|
||||
params: any = {},
|
||||
) => {
|
||||
return AgentRuntime.initializeWithProvider(provider, {
|
||||
|
||||
@@ -3,7 +3,7 @@ import superjson from 'superjson';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { serverDBEnv } from '@/config/db';
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { appEnv } from '@/envs/app';
|
||||
import { createAsyncCallerFactory } from '@/libs/trpc/async';
|
||||
@@ -13,7 +13,7 @@ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { asyncRouter } from './index';
|
||||
import type { AsyncRouter } from './index';
|
||||
|
||||
export const createAsyncServerClient = async (userId: string, payload: JWTPayload) => {
|
||||
export const createAsyncServerClient = async (userId: string, payload: ClientSecretPayload) => {
|
||||
const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${serverDBEnv.KEY_VAULTS_SECRET}`,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
|
||||
import { searchRouter } from './search';
|
||||
|
||||
// Mock JWT verification
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn().mockResolvedValue({ userId: '1' }),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn().mockReturnValue({ userId: '1' }),
|
||||
}));
|
||||
|
||||
vi.mock('@lobechat/web-crawler', () => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JWTPayload } from '@/const/auth';
|
||||
import { ClientSecretPayload } from '@/const/auth';
|
||||
import { AsyncTaskModel } from '@/database/models/asyncTask';
|
||||
import { FileModel } from '@/database/models/file';
|
||||
import { serverDB } from '@/database/server';
|
||||
@@ -30,7 +30,7 @@ export class ChunkService {
|
||||
return this.chunkClient.chunkContent(params);
|
||||
}
|
||||
|
||||
async asyncEmbeddingFileChunks(fileId: string, payload: JWTPayload) {
|
||||
async asyncEmbeddingFileChunks(fileId: string, payload: ClientSecretPayload) {
|
||||
const result = await this.fileModel.findById(fileId);
|
||||
|
||||
if (!result) return;
|
||||
@@ -66,7 +66,7 @@ export class ChunkService {
|
||||
/**
|
||||
* parse file to chunks with async task
|
||||
*/
|
||||
async asyncParseFileToChunks(fileId: string, payload: JWTPayload, skipExist?: boolean) {
|
||||
async asyncParseFileToChunks(fileId: string, payload: ClientSecretPayload, skipExist?: boolean) {
|
||||
const result = await this.fileModel.findById(fileId);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
||||
import { isDeprecatedEdition } from '@/const/version';
|
||||
import { ModelProvider } from '@/libs/model-runtime';
|
||||
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
CloudflareKeyVault,
|
||||
OpenAICompatibleKeyVault,
|
||||
} from '@/types/user/settings';
|
||||
import { createJWT } from '@/utils/jwt';
|
||||
import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
|
||||
|
||||
export const getProviderAuthPayload = (
|
||||
provider: string,
|
||||
@@ -80,7 +80,7 @@ const createAuthTokenWithPayload = async (payload = {}) => {
|
||||
const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState());
|
||||
const userId = userProfileSelectors.userId(useUserStore.getState());
|
||||
|
||||
return createJWT<JWTPayload>({ accessCode, userId, ...payload });
|
||||
return obfuscatePayloadWithXOR<ClientSecretPayload>({ accessCode, userId, ...payload });
|
||||
};
|
||||
|
||||
interface AuthParams {
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum SettingsTabs {
|
||||
Common = 'common',
|
||||
Hotkey = 'hotkey',
|
||||
LLM = 'llm',
|
||||
Plugin = 'plugin',
|
||||
Provider = 'provider',
|
||||
Proxy = 'proxy',
|
||||
Storage = 'storage',
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { SECRET_XOR_KEY } from '@/const/auth';
|
||||
|
||||
import { obfuscatePayloadWithXOR } from './xor-obfuscation';
|
||||
|
||||
describe('xor-obfuscation', () => {
|
||||
describe('obfuscatePayloadWithXOR', () => {
|
||||
it('应该对简单字符串进行混淆并返回Base64字符串', () => {
|
||||
const payload = 'hello world';
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
|
||||
// 验证结果长度大于0
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该对JSON对象进行混淆', () => {
|
||||
const payload = { name: 'test', value: 123, active: true };
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该对数组进行混淆', () => {
|
||||
const payload = [1, 2, 3, 'test', { nested: true }];
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该对复杂嵌套对象进行混淆', () => {
|
||||
const payload = {
|
||||
user: {
|
||||
id: 123,
|
||||
profile: {
|
||||
name: 'John Doe',
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
preferences: ['email', 'sms'],
|
||||
},
|
||||
},
|
||||
},
|
||||
tokens: ['abc123', 'def456'],
|
||||
metadata: null,
|
||||
};
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('相同的输入应该产生相同的输出', () => {
|
||||
const payload = { test: 'consistent' };
|
||||
const result1 = obfuscatePayloadWithXOR(payload);
|
||||
const result2 = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it('不同的输入应该产生不同的输出', () => {
|
||||
const payload1 = { test: 'value1' };
|
||||
const payload2 = { test: 'value2' };
|
||||
|
||||
const result1 = obfuscatePayloadWithXOR(payload1);
|
||||
const result2 = obfuscatePayloadWithXOR(payload2);
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
it('应该处理包含特殊字符的字符串', () => {
|
||||
const payload = 'Hello! @#$%^&*()_+-=[]{}|;:,.<>?/~`"\'\\';
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理包含Unicode字符的字符串', () => {
|
||||
const payload = '你好世界 🌍 émojis 日本語 한국어';
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
const payload = '';
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理空对象', () => {
|
||||
const payload = {};
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理空数组', () => {
|
||||
const result = obfuscatePayloadWithXOR([]);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理null值', () => {
|
||||
const payload = null;
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理数字', () => {
|
||||
const payload = 42;
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理布尔值', () => {
|
||||
const payloadTrue = true;
|
||||
const payloadFalse = false;
|
||||
|
||||
const resultTrue = obfuscatePayloadWithXOR(payloadTrue);
|
||||
const resultFalse = obfuscatePayloadWithXOR(payloadFalse);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof resultTrue).toBe('string');
|
||||
expect(typeof resultFalse).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(resultTrue)).not.toThrow();
|
||||
expect(() => atob(resultFalse)).not.toThrow();
|
||||
|
||||
// 验证不同布尔值产生不同结果
|
||||
expect(resultTrue).not.toBe(resultFalse);
|
||||
});
|
||||
|
||||
it('应该处理包含特殊JSON字符的对象', () => {
|
||||
const payload = {
|
||||
quotes: '"double quotes"',
|
||||
singleQuotes: "'single quotes'",
|
||||
backslash: 'back\\slash',
|
||||
newline: 'line1\nline2',
|
||||
tab: 'col1\tcol2',
|
||||
unicode: '\u0041\u0042\u0043',
|
||||
};
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理很长的字符串', () => {
|
||||
const payload = 'a'.repeat(10000);
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
|
||||
// 验证结果长度合理(Base64编码后长度应该大约是原始长度的4/3)
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该产生不同长度输入的不同输出长度', () => {
|
||||
const shortPayload = 'short';
|
||||
const longPayload = 'this is a much longer string that should produce different output';
|
||||
|
||||
const shortResult = obfuscatePayloadWithXOR(shortPayload);
|
||||
const longResult = obfuscatePayloadWithXOR(longPayload);
|
||||
|
||||
// 较长的输入应该产生较长的输出
|
||||
expect(longResult.length).toBeGreaterThan(shortResult.length);
|
||||
});
|
||||
|
||||
it('应该验证输出是有效的Base64格式', () => {
|
||||
const payload = { test: 'base64 validation' };
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证Base64格式的正则表达式
|
||||
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
||||
expect(base64Regex.test(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理包含循环引用的对象(通过JSON.stringify处理)', () => {
|
||||
// JSON.stringify 会抛出错误处理循环引用,但我们测试正常情况
|
||||
const payload = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
nested: {
|
||||
back: 'reference',
|
||||
},
|
||||
};
|
||||
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该对undefined值进行处理', () => {
|
||||
const payload = undefined;
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
|
||||
// 验证返回值是字符串
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// 验证返回值是有效的Base64字符串
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该对包含函数的对象进行处理(函数会被JSON.stringify忽略)', () => {
|
||||
const payload = {
|
||||
name: 'test',
|
||||
fn: function () {
|
||||
return 'test';
|
||||
},
|
||||
arrow: () => 'arrow',
|
||||
value: 123,
|
||||
};
|
||||
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该确保XOR操作的确定性', () => {
|
||||
const payload = 'deterministic test';
|
||||
const results: any[] = [];
|
||||
|
||||
// 多次运行相同输入
|
||||
for (let i = 0; i < 10; i++) {
|
||||
results.push(obfuscatePayloadWithXOR(payload));
|
||||
}
|
||||
|
||||
// 所有结果应该相同
|
||||
expect(results.every((result) => result === results[0])).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理包含日期对象的数据', () => {
|
||||
const payload = {
|
||||
timestamp: new Date('2024-01-01T00:00:00Z'),
|
||||
created: new Date(),
|
||||
name: 'date test',
|
||||
};
|
||||
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理包含Symbol的对象(Symbol会被JSON.stringify忽略)', () => {
|
||||
const sym = Symbol('test');
|
||||
const payload = {
|
||||
name: 'symbol test',
|
||||
[sym]: 'symbol value',
|
||||
normalKey: 'normal value',
|
||||
};
|
||||
|
||||
const result = obfuscatePayloadWithXOR(payload);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(() => atob(result)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应该验证混淆后的数据长度合理性', () => {
|
||||
const originalPayload = { test: 'length check' };
|
||||
const originalJSON = JSON.stringify(originalPayload);
|
||||
const result = obfuscatePayloadWithXOR(originalPayload);
|
||||
|
||||
// Base64 编码后的长度通常是原始长度的 4/3 倍(向上取整到4的倍数)
|
||||
const expectedMinLength = Math.ceil((originalJSON.length * 4) / 3 / 4) * 4;
|
||||
expect(result.length).toBeGreaterThanOrEqual(expectedMinLength - 4); // 允许一些误差
|
||||
});
|
||||
|
||||
it('应该验证XOR操作的正确性(通过逆向操作)', () => {
|
||||
const originalPayload = { message: 'XOR test', value: 42 };
|
||||
const obfuscatedResult = obfuscatePayloadWithXOR(originalPayload);
|
||||
|
||||
// 手动实现逆向操作来验证 XOR 操作的正确性
|
||||
const base64Decoded = atob(obfuscatedResult);
|
||||
const xoredBytes = new Uint8Array(base64Decoded.length);
|
||||
for (let i = 0; i < base64Decoded.length; i++) {
|
||||
xoredBytes[i] = base64Decoded.charCodeAt(i);
|
||||
}
|
||||
|
||||
// 使用相同的密钥进行逆向 XOR 操作
|
||||
const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY);
|
||||
const decodedBytes = new Uint8Array(xoredBytes.length);
|
||||
for (let i = 0; i < xoredBytes.length; i++) {
|
||||
decodedBytes[i] = xoredBytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
// 将结果转换回字符串
|
||||
const decodedString = new TextDecoder().decode(decodedBytes);
|
||||
const decodedPayload = JSON.parse(decodedString);
|
||||
|
||||
// 验证解码后的数据与原始数据相同
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('应该验证不同输入产生不同的Base64输出', () => {
|
||||
const payloads = [
|
||||
'test1',
|
||||
'test2',
|
||||
{ key: 'value1' },
|
||||
{ key: 'value2' },
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
];
|
||||
|
||||
const results = payloads.map((payload) => obfuscatePayloadWithXOR(payload));
|
||||
|
||||
// 验证所有结果都不相同
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
for (let j = i + 1; j < results.length; j++) {
|
||||
expect(results[i]).not.toBe(results[j]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SECRET_XOR_KEY } from '@/const/auth';
|
||||
|
||||
/**
|
||||
* 将字符串转换为 Uint8Array (UTF-8 编码)
|
||||
*/
|
||||
const stringToUint8Array = (str: string): Uint8Array => {
|
||||
return new TextEncoder().encode(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 对 Uint8Array 进行 XOR 运算
|
||||
* @param data 要处理的 Uint8Array
|
||||
* @param key 用于 XOR 的密钥 (Uint8Array)
|
||||
* @returns 经过 XOR 运算的 Uint8Array
|
||||
*/
|
||||
const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
|
||||
const result = new Uint8Array(data.length);
|
||||
for (const [i, datum] of data.entries()) {
|
||||
result[i] = datum ^ key[i % key.length]; // 密钥循环使用
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 对 payload 进行 XOR 混淆并 Base64 编码
|
||||
* @param payload 要混淆的 JSON 对象
|
||||
* @returns Base64 编码后的混淆字符串
|
||||
*/
|
||||
export const obfuscatePayloadWithXOR = <T>(payload: T): string => {
|
||||
const jsonString = JSON.stringify(payload);
|
||||
const dataBytes = stringToUint8Array(jsonString);
|
||||
const keyBytes = stringToUint8Array(SECRET_XOR_KEY);
|
||||
|
||||
const xoredBytes = xorProcess(dataBytes, keyBytes);
|
||||
|
||||
// 将 Uint8Array 转换为 Base64 字符串
|
||||
// 浏览器环境 btoa 只能处理 Latin-1 字符,所以需要先转换为适合 btoa 的字符串
|
||||
return btoa(String.fromCharCode(...xoredBytes));
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { NON_HTTP_PREFIX } from '@/const/auth';
|
||||
|
||||
import { createJWT } from './jwt';
|
||||
|
||||
describe('createJWT', () => {
|
||||
it('should create a JWT token', async () => {
|
||||
const payload = { test: 'test' };
|
||||
const token = await createJWT(payload);
|
||||
expect(token).toBeTruthy();
|
||||
});
|
||||
it('should return a token with NON_HTTP_PREFIX when crypto.subtle does not exist', async () => {
|
||||
const originalCryptoSubtle = crypto.subtle;
|
||||
// @ts-ignore
|
||||
crypto['subtle'] = undefined;
|
||||
|
||||
const payload = { test: 'test' };
|
||||
const token = await createJWT(payload);
|
||||
|
||||
expect(token.startsWith(NON_HTTP_PREFIX)).toBeTruthy();
|
||||
|
||||
// @ts-ignore
|
||||
crypto['subtle'] = originalCryptoSubtle;
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import { SignJWT, importJWK } from 'jose';
|
||||
|
||||
import { JWT_SECRET_KEY, NON_HTTP_PREFIX } from '@/const/auth';
|
||||
|
||||
export const createJWT = async <T>(payload: T) => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const duration = 100; // 100s
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// fix the issue that crypto.subtle is not available in non-HTTPS environment
|
||||
// refs: https://github.com/lobehub/lobe-chat/pull/1238
|
||||
if (!crypto.subtle) {
|
||||
const buffer = encoder.encode(JSON.stringify(payload));
|
||||
|
||||
return `${NON_HTTP_PREFIX}.${Buffer.from(buffer).toString('base64')}`;
|
||||
}
|
||||
|
||||
// create a secret key
|
||||
const secretKey = await crypto.subtle.digest('SHA-256', encoder.encode(JWT_SECRET_KEY));
|
||||
|
||||
// get the JWK from the secret key
|
||||
const jwkSecretKey = await importJWK(
|
||||
{
|
||||
k: Buffer.from(secretKey).toString('base64'),
|
||||
kty: 'oct',
|
||||
},
|
||||
'HS256',
|
||||
);
|
||||
|
||||
// 创建JWT
|
||||
return new SignJWT(payload as Record<string, any>)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt(now) // 设置JWT的iat(签发时间)声明
|
||||
.setExpirationTime(now + duration) // 设置 JWT 的 exp(过期时间)为 100 s
|
||||
.sign(jwkSecretKey);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { NON_HTTP_PREFIX } from '@/const/auth';
|
||||
|
||||
import { getJWTPayload } from './jwt';
|
||||
|
||||
let enableClerkMock = false;
|
||||
let enableNextAuthMock = false;
|
||||
|
||||
vi.mock('@/const/auth', async (importOriginal) => {
|
||||
const data = await importOriginal();
|
||||
|
||||
return {
|
||||
...(data as any),
|
||||
get enableClerk() {
|
||||
return enableClerkMock;
|
||||
},
|
||||
get enableNextAuth() {
|
||||
return enableNextAuthMock;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/envs/app', () => ({
|
||||
getAppConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getJWTPayload', () => {
|
||||
it('should parse JWT payload for non-HTTPS token', async () => {
|
||||
const token = `${NON_HTTP_PREFIX}.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ`;
|
||||
const payload = await getJWTPayload(token);
|
||||
expect(payload).toEqual({
|
||||
sub: '1234567890',
|
||||
name: 'John Doe',
|
||||
iat: 1516239022,
|
||||
});
|
||||
});
|
||||
|
||||
it('should verify and parse JWT payload for HTTPS token', async () => {
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3NDb2RlIjoiIiwidXNlcklkIjoiMDAxMzYyYzMtNDhjNS00NjM1LWJkM2ItODM3YmZmZjU4ZmMwIiwiYXBpS2V5IjoiYWJjIiwiZW5kcG9pbnQiOiJhYmMiLCJpYXQiOjE3MTY4MDIyMjUsImV4cCI6MTAwMDAwMDAwMDE3MTY4MDIwMDB9.FF0FxsE8Cajs-_hv5GD0TNUDwvekAkI9l_LL_IOPdGQ';
|
||||
const payload = await getJWTPayload(token);
|
||||
expect(payload).toEqual({
|
||||
accessCode: '',
|
||||
apiKey: 'abc',
|
||||
endpoint: 'abc',
|
||||
exp: 10000000001716802000,
|
||||
iat: 1716802225,
|
||||
userId: '001362c3-48c5-4635-bd3b-837bfff58fc0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not verify success and parse JWT payload for dated token', async () => {
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3NDb2RlIjoiIiwidXNlcklkIjoiYWY3M2JhODktZjFhMy00YjliLWEwM2QtZGViZmZlMzE4NmQxIiwiYXBpS2V5IjoiYWJjIiwiZW5kcG9pbnQiOiJhYmMiLCJpYXQiOjE3MTY3OTk5ODAsImV4cCI6MTcxNjgwMDA4MH0.8AGFsLcwyrQG82kVUYOGFXHIwihm2n16ctyArKW9100';
|
||||
try {
|
||||
await getJWTPayload(token);
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toEqual('"exp" claim timestamp check failed');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { importJWK, jwtVerify } from 'jose';
|
||||
|
||||
import { JWTPayload, JWT_SECRET_KEY, NON_HTTP_PREFIX } from '@/const/auth';
|
||||
|
||||
export const getJWTPayload = async (token: string): Promise<JWTPayload> => {
|
||||
//如果是 HTTP 协议发起的请求,直接解析 token
|
||||
// 这是一个非常 hack 的解决方案,未来要找更好的解决方案来处理这个问题
|
||||
// refs: https://github.com/lobehub/lobe-chat/pull/1238
|
||||
if (token.startsWith(NON_HTTP_PREFIX)) {
|
||||
const jwtParts = token.split('.');
|
||||
|
||||
const payload = jwtParts[1];
|
||||
|
||||
return JSON.parse(atob(payload));
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const secretKey = await crypto.subtle.digest('SHA-256', encoder.encode(JWT_SECRET_KEY));
|
||||
|
||||
const jwkSecretKey = await importJWK(
|
||||
{ k: Buffer.from(secretKey).toString('base64'), kty: 'oct' },
|
||||
'HS256',
|
||||
);
|
||||
|
||||
const { payload } = await jwtVerify(token, jwkSecretKey);
|
||||
|
||||
return payload as JWTPayload;
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
|
||||
|
||||
import { getXorPayload } from './xor';
|
||||
|
||||
describe('getXorPayload', () => {
|
||||
it('should correctly decode XOR obfuscated payload with user data', () => {
|
||||
const originalPayload = {
|
||||
userId: '001362c3-48c5-4635-bd3b-837bfff58fc0',
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: 'https://api.example.com',
|
||||
};
|
||||
|
||||
// 使用客户端的混淆函数生成token
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
|
||||
// 使用服务端的解码函数解码
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should correctly decode XOR obfuscated payload with minimal data', () => {
|
||||
const originalPayload = {
|
||||
userId: '12345',
|
||||
};
|
||||
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should correctly decode XOR obfuscated payload with AWS credentials', () => {
|
||||
const originalPayload = {
|
||||
userId: 'aws-user-123',
|
||||
awsAccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
awsSecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
awsRegion: 'us-east-1',
|
||||
awsSessionToken: 'session-token-example',
|
||||
};
|
||||
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should correctly decode XOR obfuscated payload with Azure data', () => {
|
||||
const originalPayload = {
|
||||
userId: 'azure-user-456',
|
||||
apiKey: 'azure-api-key',
|
||||
baseURL: 'https://your-resource.openai.azure.com',
|
||||
azureApiVersion: '2024-02-15-preview',
|
||||
};
|
||||
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should correctly decode XOR obfuscated payload with Cloudflare data', () => {
|
||||
const originalPayload = {
|
||||
userId: 'cf-user-789',
|
||||
apiKey: 'cloudflare-api-key',
|
||||
cloudflareBaseURLOrAccountID: 'account-id-example',
|
||||
};
|
||||
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should handle empty payload correctly', () => {
|
||||
const originalPayload = {};
|
||||
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should handle payload with undefined values', () => {
|
||||
const originalPayload = {
|
||||
userId: 'test-user',
|
||||
accessCode: undefined,
|
||||
apiKey: 'test-key',
|
||||
};
|
||||
|
||||
const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
|
||||
const decodedPayload = getXorPayload(obfuscatedToken);
|
||||
|
||||
expect(decodedPayload).toEqual(originalPayload);
|
||||
});
|
||||
|
||||
it('should throw error for invalid base64 token', () => {
|
||||
const invalidToken = 'invalid-base64-token!@#';
|
||||
|
||||
expect(() => getXorPayload(invalidToken)).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it('should throw error for token that cannot be parsed as JSON', () => {
|
||||
// 创建一个能正确base64解码但不是有效JSON的token
|
||||
const invalidJsonString = 'this is not json';
|
||||
const invalidJsonBytes = new TextEncoder().encode(invalidJsonString);
|
||||
const keyBytes = new TextEncoder().encode('LobeHub · LobeHub');
|
||||
|
||||
// 进行XOR处理
|
||||
const result = new Uint8Array(invalidJsonBytes.length);
|
||||
for (const [i, datum] of invalidJsonBytes.entries()) {
|
||||
result[i] = datum ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
// 转换为base64
|
||||
const invalidToken = Buffer.from(result).toString('base64');
|
||||
|
||||
expect(() => getXorPayload(invalidToken)).toThrow(SyntaxError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ClientSecretPayload, SECRET_XOR_KEY } from '@/const/auth';
|
||||
|
||||
/**
|
||||
* 将 Base64 字符串转换为 Uint8Array
|
||||
*/
|
||||
const base64ToUint8Array = (base64: string): Uint8Array => {
|
||||
// Node.js 环境下直接使用 Buffer
|
||||
return Buffer.from(base64, 'base64');
|
||||
};
|
||||
|
||||
/**
|
||||
* 对 Uint8Array 进行 XOR 运算 (与客户端的 xorProcess 函数相同)
|
||||
*/
|
||||
const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
|
||||
const result = new Uint8Array(data.length);
|
||||
for (const [i, datum] of data.entries()) {
|
||||
result[i] = datum ^ key[i % key.length];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 Uint8Array 转换为字符串 (UTF-8 解码)
|
||||
*/
|
||||
const uint8ArrayToString = (arr: Uint8Array): string => {
|
||||
return new TextDecoder().decode(arr);
|
||||
};
|
||||
|
||||
export const getXorPayload = (token: string): ClientSecretPayload => {
|
||||
const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY);
|
||||
|
||||
// 1. Base64 解码
|
||||
const base64DecodedBytes = base64ToUint8Array(token);
|
||||
|
||||
// 2. XOR 解混淆
|
||||
const xorDecryptedBytes = xorProcess(base64DecodedBytes, keyBytes);
|
||||
|
||||
// 3. 转换为字符串并解析 JSON
|
||||
const decodedJsonString = uint8ArrayToString(xorDecryptedBytes);
|
||||
|
||||
return JSON.parse(decodedJsonString) as ClientSecretPayload;
|
||||
};
|
||||
Reference in New Issue
Block a user