mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-16 20:46:08 +00:00
✨ feat: support klavis mcp connector (#10584)
* feat: support klavis mcp connector * feat: update klavis item & klavis call tools * feat: update the noraml klavis mcp (no need oauth) * fix: rollback test * fix: fixed test ci * feat: update the klavis select model & locals settings * fix: change the klavis id to klavis types * fix: delete the klavis into getGlobalConfig * fix: delete useless migrations * fix: improve the code * feat: update test & update the klavis const var * fix: change it to const * feat: use swr to replace useEffect
This commit is contained in:
@@ -403,3 +403,13 @@ OPENAI_API_KEY=sk-xxxxxxxxx
|
||||
|
||||
# MCP tool call timeout (milliseconds)
|
||||
# MCP_TOOL_TIMEOUT=60000
|
||||
|
||||
# #######################################
|
||||
# ######### Klavis Service ##############
|
||||
# #######################################
|
||||
|
||||
# Klavis API Key for accessing Strata hosted MCP servers
|
||||
# Get your API key from: https://klavis.io
|
||||
# IMPORTANT: This key is stored server-side only and NEVER exposed to the client
|
||||
# When this key is set, Klavis integration will be automatically enabled
|
||||
# KLAVIS_API_KEY=your_klavis_api_key_here
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "الامتدادات المدمجة"
|
||||
},
|
||||
"disabled": "النموذج الحالي لا يدعم استدعاء الوظائف، ولا يمكن استخدام الإضافة",
|
||||
"klavis": {
|
||||
"addServer": "إضافة خادم",
|
||||
"authCompleted": "اكتملت المصادقة",
|
||||
"authFailed": "فشلت المصادقة",
|
||||
"authRequired": "المصادقة مطلوبة",
|
||||
"connected": "متصل",
|
||||
"error": "خطأ",
|
||||
"groupName": "أدوات Klavis",
|
||||
"manage": "إدارة Klavis",
|
||||
"manageTitle": "إدارة تكامل Klavis",
|
||||
"noServers": "لا توجد خوادم متصلة حاليًا",
|
||||
"notEnabled": "خدمة Klavis غير مفعّلة",
|
||||
"oauthRequired": "يرجى إكمال مصادقة OAuth في نافذة جديدة",
|
||||
"pendingAuth": "في انتظار المصادقة",
|
||||
"serverCreated": "تم إنشاء الخادم بنجاح",
|
||||
"serverCreatedFailed": "فشل في إنشاء الخادم",
|
||||
"serverRemoved": "تم حذف الخادم",
|
||||
"servers": "خوادم",
|
||||
"tools": "أدوات",
|
||||
"verifyAuth": "لقد أكملت المصادقة"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "ممكّنة {{num}}",
|
||||
"groupName": "الإضافات",
|
||||
"noEnabled": "لا توجد إضافات ممكّنة حاليًا",
|
||||
"store": "متجر الإضافات"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "الكل",
|
||||
"installed": "مفعّلة"
|
||||
},
|
||||
"title": "أدوات الامتداد"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Вградени"
|
||||
},
|
||||
"disabled": "Текущият модел не поддържа извиквания на функции и не може да използва плъгина",
|
||||
"klavis": {
|
||||
"addServer": "Добавяне на сървър",
|
||||
"authCompleted": "Удостоверяването е завършено",
|
||||
"authFailed": "Удостоверяването не бе успешно",
|
||||
"authRequired": "Изисква се удостоверяване",
|
||||
"connected": "Свързан",
|
||||
"error": "Грешка",
|
||||
"groupName": "Инструменти на Klavis",
|
||||
"manage": "Управление на Klavis",
|
||||
"manageTitle": "Управление на интеграцията с Klavis",
|
||||
"noServers": "Няма свързани сървъри",
|
||||
"notEnabled": "Услугата Klavis не е активирана",
|
||||
"oauthRequired": "Моля, завършете OAuth удостоверяването в нов прозорец",
|
||||
"pendingAuth": "Очаква удостоверяване",
|
||||
"serverCreated": "Сървърът е създаден успешно",
|
||||
"serverCreatedFailed": "Създаването на сървъра не бе успешно",
|
||||
"serverRemoved": "Сървърът е изтрит",
|
||||
"servers": "сървъра",
|
||||
"tools": "инструмента",
|
||||
"verifyAuth": "Удостоверяването е завършено"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Активирани: {{num}}",
|
||||
"groupName": "Плъгини",
|
||||
"noEnabled": "Няма активирани плъгини",
|
||||
"store": "Магазин за плъгини"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Всички",
|
||||
"installed": "Активирани"
|
||||
},
|
||||
"title": "Инструменти за разширение"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Integriert"
|
||||
},
|
||||
"disabled": "Das aktuelle Modell unterstützt keine Funktionsaufrufe und kann keine Plugins verwenden",
|
||||
"klavis": {
|
||||
"addServer": "Server hinzufügen",
|
||||
"authCompleted": "Authentifizierung abgeschlossen",
|
||||
"authFailed": "Authentifizierung fehlgeschlagen",
|
||||
"authRequired": "Authentifizierung erforderlich",
|
||||
"connected": "Verbunden",
|
||||
"error": "Fehler",
|
||||
"groupName": "Klavis-Tools",
|
||||
"manage": "Klavis verwalten",
|
||||
"manageTitle": "Klavis-Integration verwalten",
|
||||
"noServers": "Keine verbundenen Server",
|
||||
"notEnabled": "Klavis-Dienst ist nicht aktiviert",
|
||||
"oauthRequired": "Bitte schließen Sie die OAuth-Authentifizierung in einem neuen Fenster ab",
|
||||
"pendingAuth": "Authentifizierung ausstehend",
|
||||
"serverCreated": "Server erfolgreich erstellt",
|
||||
"serverCreatedFailed": "Servererstellung fehlgeschlagen",
|
||||
"serverRemoved": "Server wurde entfernt",
|
||||
"servers": "Server",
|
||||
"tools": "Tools",
|
||||
"verifyAuth": "Ich habe die Authentifizierung abgeschlossen"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Aktiviert: {{num}}",
|
||||
"groupName": "Plugins",
|
||||
"noEnabled": "Keine Plugins aktiviert",
|
||||
"store": "Plugin-Store"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Alle",
|
||||
"installed": "Aktiviert"
|
||||
},
|
||||
"title": "Erweiterungswerkzeuge"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Built-ins"
|
||||
},
|
||||
"disabled": "The current model does not support function calls and cannot use the plugin",
|
||||
"klavis": {
|
||||
"addServer": "Add Server",
|
||||
"authCompleted": "Authentication Completed",
|
||||
"authFailed": "Authentication Failed",
|
||||
"authRequired": "Authentication Required",
|
||||
"connected": "Connected",
|
||||
"error": "Error",
|
||||
"groupName": "Klavis Tools",
|
||||
"manage": "Manage Klavis",
|
||||
"manageTitle": "Manage Klavis Integration",
|
||||
"noServers": "No connected servers",
|
||||
"notEnabled": "Klavis service not enabled",
|
||||
"oauthRequired": "Please complete OAuth authentication in the new window",
|
||||
"pendingAuth": "Pending Authentication",
|
||||
"serverCreated": "Server created successfully",
|
||||
"serverCreatedFailed": "Failed to create server",
|
||||
"serverRemoved": "Server removed",
|
||||
"servers": "servers",
|
||||
"tools": "tools",
|
||||
"verifyAuth": "I have completed authentication"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Enabled: {{num}}",
|
||||
"groupName": "Plugins",
|
||||
"noEnabled": "No plugins enabled",
|
||||
"store": "Plugin Store"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "All",
|
||||
"installed": "Enabled"
|
||||
},
|
||||
"title": "Extension Tools"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Incorporados"
|
||||
},
|
||||
"disabled": "El modelo actual no admite llamadas de función y no se puede utilizar el complemento",
|
||||
"klavis": {
|
||||
"addServer": "Agregar servidor",
|
||||
"authCompleted": "Autenticación completada",
|
||||
"authFailed": "Autenticación fallida",
|
||||
"authRequired": "Autenticación requerida",
|
||||
"connected": "Conectado",
|
||||
"error": "Error",
|
||||
"groupName": "Herramientas de Klavis",
|
||||
"manage": "Gestionar Klavis",
|
||||
"manageTitle": "Gestionar la integración de Klavis",
|
||||
"noServers": "No hay servidores conectados",
|
||||
"notEnabled": "El servicio de Klavis no está habilitado",
|
||||
"oauthRequired": "Por favor, completa la autenticación OAuth en una nueva ventana",
|
||||
"pendingAuth": "Autenticación pendiente",
|
||||
"serverCreated": "Servidor creado con éxito",
|
||||
"serverCreatedFailed": "Error al crear el servidor",
|
||||
"serverRemoved": "Servidor eliminado",
|
||||
"servers": "servidores",
|
||||
"tools": "herramientas",
|
||||
"verifyAuth": "He completado la autenticación"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Habilitados {{num}}",
|
||||
"groupName": "Complementos",
|
||||
"noEnabled": "No hay complementos habilitados por el momento",
|
||||
"store": "Tienda de complementos"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Todo",
|
||||
"installed": "Habilitado"
|
||||
},
|
||||
"title": "Herramientas de extensión"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "افزونههای داخلی"
|
||||
},
|
||||
"disabled": "مدل فعلی از فراخوانی توابع پشتیبانی نمیکند و نمیتوان از افزونهها استفاده کرد",
|
||||
"klavis": {
|
||||
"addServer": "افزودن سرور",
|
||||
"authCompleted": "احراز هویت با موفقیت انجام شد",
|
||||
"authFailed": "احراز هویت ناموفق بود",
|
||||
"authRequired": "احراز هویت لازم است",
|
||||
"connected": "متصل شد",
|
||||
"error": "خطا",
|
||||
"groupName": "ابزارهای Klavis",
|
||||
"manage": "مدیریت Klavis",
|
||||
"manageTitle": "مدیریت یکپارچهسازی Klavis",
|
||||
"noServers": "هیچ سرور متصلی وجود ندارد",
|
||||
"notEnabled": "سرویس Klavis فعال نیست",
|
||||
"oauthRequired": "لطفاً احراز هویت OAuth را در پنجره جدید کامل کنید",
|
||||
"pendingAuth": "در انتظار احراز هویت",
|
||||
"serverCreated": "سرور با موفقیت ایجاد شد",
|
||||
"serverCreatedFailed": "ایجاد سرور ناموفق بود",
|
||||
"serverRemoved": "سرور حذف شد",
|
||||
"servers": "سرور",
|
||||
"tools": "ابزار",
|
||||
"verifyAuth": "احراز هویت را کامل کردهام"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "{{num}} فعال شده است",
|
||||
"groupName": "افزونههای شخص ثالث",
|
||||
"noEnabled": "هیچ افزونه فعالی وجود ندارد",
|
||||
"store": "فروشگاه افزونهها"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "همه",
|
||||
"installed": "فعال شده"
|
||||
},
|
||||
"title": "افزونههای گسترش"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Intégré"
|
||||
},
|
||||
"disabled": "Ce modèle ne prend pas en charge les appels de fonction et ne peut pas utiliser de plugins",
|
||||
"klavis": {
|
||||
"addServer": "Ajouter un serveur",
|
||||
"authCompleted": "Authentification terminée",
|
||||
"authFailed": "Échec de l'authentification",
|
||||
"authRequired": "Authentification requise",
|
||||
"connected": "Connecté",
|
||||
"error": "Erreur",
|
||||
"groupName": "Outils Klavis",
|
||||
"manage": "Gérer Klavis",
|
||||
"manageTitle": "Gérer l'intégration Klavis",
|
||||
"noServers": "Aucun serveur connecté pour le moment",
|
||||
"notEnabled": "Service Klavis non activé",
|
||||
"oauthRequired": "Veuillez compléter l'authentification OAuth dans la nouvelle fenêtre",
|
||||
"pendingAuth": "Authentification en attente",
|
||||
"serverCreated": "Serveur créé avec succès",
|
||||
"serverCreatedFailed": "Échec de la création du serveur",
|
||||
"serverRemoved": "Serveur supprimé",
|
||||
"servers": "serveurs",
|
||||
"tools": "outils",
|
||||
"verifyAuth": "J'ai terminé l'authentification"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Activé {{num}}",
|
||||
"groupName": "Plugins",
|
||||
"noEnabled": "Aucun plugin activé pour le moment",
|
||||
"store": "Boutique de plugins"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Tous",
|
||||
"installed": "Activé"
|
||||
},
|
||||
"title": "Outils supplémentaires"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Predefiniti"
|
||||
},
|
||||
"disabled": "Il modello attuale non supporta le chiamate di funzione e non è possibile utilizzare il plugin",
|
||||
"klavis": {
|
||||
"addServer": "Aggiungi server",
|
||||
"authCompleted": "Autenticazione completata",
|
||||
"authFailed": "Autenticazione fallita",
|
||||
"authRequired": "Autenticazione richiesta",
|
||||
"connected": "Connesso",
|
||||
"error": "Errore",
|
||||
"groupName": "Strumenti Klavis",
|
||||
"manage": "Gestisci Klavis",
|
||||
"manageTitle": "Gestisci integrazione Klavis",
|
||||
"noServers": "Nessun server connesso",
|
||||
"notEnabled": "Servizio Klavis non abilitato",
|
||||
"oauthRequired": "Completa l'autenticazione OAuth in una nuova finestra",
|
||||
"pendingAuth": "In attesa di autenticazione",
|
||||
"serverCreated": "Server creato con successo",
|
||||
"serverCreatedFailed": "Creazione del server non riuscita",
|
||||
"serverRemoved": "Server eliminato",
|
||||
"servers": "server",
|
||||
"tools": "strumenti",
|
||||
"verifyAuth": "Ho completato l'autenticazione"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Abilitato {{num}}",
|
||||
"groupName": "Plugin",
|
||||
"noEnabled": "Nessun plugin abilitato al momento",
|
||||
"store": "Negozio dei plugin"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Tutti",
|
||||
"installed": "Abilitati"
|
||||
},
|
||||
"title": "Strumenti aggiuntivi"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "組み込み"
|
||||
},
|
||||
"disabled": "現在のモデルは関数呼び出しをサポートしていません。プラグインを使用できません",
|
||||
"klavis": {
|
||||
"addServer": "サーバーを追加",
|
||||
"authCompleted": "認証完了",
|
||||
"authFailed": "認証に失敗しました",
|
||||
"authRequired": "認証が必要です",
|
||||
"connected": "接続済み",
|
||||
"error": "エラー",
|
||||
"groupName": "Klavis ツール",
|
||||
"manage": "Klavis を管理",
|
||||
"manageTitle": "Klavis 統合を管理",
|
||||
"noServers": "接続されているサーバーはありません",
|
||||
"notEnabled": "Klavis サービスは有効化されていません",
|
||||
"oauthRequired": "新しいウィンドウで OAuth 認証を完了してください",
|
||||
"pendingAuth": "認証待ち",
|
||||
"serverCreated": "サーバーが正常に作成されました",
|
||||
"serverCreatedFailed": "サーバーの作成に失敗しました",
|
||||
"serverRemoved": "サーバーが削除されました",
|
||||
"servers": "台のサーバー",
|
||||
"tools": "個のツール",
|
||||
"verifyAuth": "認証を完了しました"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "{{num}} が有効",
|
||||
"groupName": "プラグイン",
|
||||
"noEnabled": "有効なプラグインはありません",
|
||||
"store": "プラグインストア"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "すべて",
|
||||
"installed": "有効化済み"
|
||||
},
|
||||
"title": "拡張ツール"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "내장"
|
||||
},
|
||||
"disabled": "현재 모델은 함수 호출을 지원하지 않으며 플러그인을 사용할 수 없습니다",
|
||||
"klavis": {
|
||||
"addServer": "서버 추가",
|
||||
"authCompleted": "인증 완료",
|
||||
"authFailed": "인증 실패",
|
||||
"authRequired": "인증 필요",
|
||||
"connected": "연결됨",
|
||||
"error": "오류",
|
||||
"groupName": "Klavis 도구",
|
||||
"manage": "Klavis 관리",
|
||||
"manageTitle": "Klavis 통합 관리",
|
||||
"noServers": "연결된 서버 없음",
|
||||
"notEnabled": "Klavis 서비스가 활성화되지 않음",
|
||||
"oauthRequired": "새 창에서 OAuth 인증을 완료해 주세요",
|
||||
"pendingAuth": "인증 대기 중",
|
||||
"serverCreated": "서버 생성 성공",
|
||||
"serverCreatedFailed": "서버 생성 실패",
|
||||
"serverRemoved": "서버 삭제됨",
|
||||
"servers": "개의 서버",
|
||||
"tools": "개의 도구",
|
||||
"verifyAuth": "인증을 완료했습니다"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "활성화됨 {{num}}",
|
||||
"groupName": "플러그인",
|
||||
"noEnabled": "활성화된 플러그인이 없음",
|
||||
"store": "플러그인 스토어"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "전체",
|
||||
"installed": "활성화됨"
|
||||
},
|
||||
"title": "확장 도구"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Ingebouwd"
|
||||
},
|
||||
"disabled": "Dit model ondersteunt momenteel geen functieaanroepen en kan geen plug-ins gebruiken",
|
||||
"klavis": {
|
||||
"addServer": "Server toevoegen",
|
||||
"authCompleted": "Authenticatie voltooid",
|
||||
"authFailed": "Authenticatie mislukt",
|
||||
"authRequired": "Authenticatie vereist",
|
||||
"connected": "Verbonden",
|
||||
"error": "Fout",
|
||||
"groupName": "Klavis Tools",
|
||||
"manage": "Beheer Klavis",
|
||||
"manageTitle": "Beheer Klavis-integratie",
|
||||
"noServers": "Geen verbonden servers",
|
||||
"notEnabled": "Klavis-service is niet ingeschakeld",
|
||||
"oauthRequired": "Voltooi de OAuth-authenticatie in een nieuw venster",
|
||||
"pendingAuth": "Authenticatie in behandeling",
|
||||
"serverCreated": "Server succesvol aangemaakt",
|
||||
"serverCreatedFailed": "Server aanmaken mislukt",
|
||||
"serverRemoved": "Server verwijderd",
|
||||
"servers": "servers",
|
||||
"tools": "tools",
|
||||
"verifyAuth": "Ik heb de authenticatie voltooid"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Ingeschakeld {{num}}",
|
||||
"groupName": "Plug-ins",
|
||||
"noEnabled": "Geen plug-ins ingeschakeld",
|
||||
"store": "Plug-in store"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Alles",
|
||||
"installed": "Ingeschakeld"
|
||||
},
|
||||
"title": "Uitbreidingsgereedschap"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Wbudowane"
|
||||
},
|
||||
"disabled": "Aktualny model nie obsługuje wywołań funkcji i nie można użyć wtyczki",
|
||||
"klavis": {
|
||||
"addServer": "Dodaj serwer",
|
||||
"authCompleted": "Uwierzytelnianie zakończone",
|
||||
"authFailed": "Uwierzytelnianie nie powiodło się",
|
||||
"authRequired": "Wymagane uwierzytelnienie",
|
||||
"connected": "Połączono",
|
||||
"error": "Błąd",
|
||||
"groupName": "Narzędzia Klavis",
|
||||
"manage": "Zarządzaj Klavis",
|
||||
"manageTitle": "Zarządzaj integracją Klavis",
|
||||
"noServers": "Brak połączonych serwerów",
|
||||
"notEnabled": "Usługa Klavis nie jest włączona",
|
||||
"oauthRequired": "Proszę zakończyć uwierzytelnianie OAuth w nowym oknie",
|
||||
"pendingAuth": "Oczekujące uwierzytelnienie",
|
||||
"serverCreated": "Serwer został utworzony",
|
||||
"serverCreatedFailed": "Nie udało się utworzyć serwera",
|
||||
"serverRemoved": "Serwer został usunięty",
|
||||
"servers": "serwery",
|
||||
"tools": "narzędzia",
|
||||
"verifyAuth": "Ukończyłem uwierzytelnianie"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Włączone {{num}}",
|
||||
"groupName": "Wtyczki",
|
||||
"noEnabled": "Brak włączonych wtyczek",
|
||||
"store": "Sklep z wtyczkami"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Wszystko",
|
||||
"installed": "Włączone"
|
||||
},
|
||||
"title": "Narzędzia rozszerzeń"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Integrados"
|
||||
},
|
||||
"disabled": "O modelo atual não suporta chamadas de função e não pode usar plugins",
|
||||
"klavis": {
|
||||
"addServer": "Adicionar servidor",
|
||||
"authCompleted": "Autenticação concluída",
|
||||
"authFailed": "Falha na autenticação",
|
||||
"authRequired": "Autenticação necessária",
|
||||
"connected": "Conectado",
|
||||
"error": "Erro",
|
||||
"groupName": "Ferramentas Klavis",
|
||||
"manage": "Gerenciar Klavis",
|
||||
"manageTitle": "Gerenciar integração com Klavis",
|
||||
"noServers": "Nenhum servidor conectado",
|
||||
"notEnabled": "Serviço Klavis não ativado",
|
||||
"oauthRequired": "Conclua a autenticação OAuth em uma nova janela",
|
||||
"pendingAuth": "Autenticação pendente",
|
||||
"serverCreated": "Servidor criado com sucesso",
|
||||
"serverCreatedFailed": "Falha ao criar servidor",
|
||||
"serverRemoved": "Servidor removido",
|
||||
"servers": "servidores",
|
||||
"tools": "ferramentas",
|
||||
"verifyAuth": "Concluí a autenticação"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Ativado {{num}}",
|
||||
"groupName": "Plugins",
|
||||
"noEnabled": "Nenhum plugin ativado no momento",
|
||||
"store": "Loja de Plugins"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Todos",
|
||||
"installed": "Ativado"
|
||||
},
|
||||
"title": "Ferramentas de Extensão"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Встроенные инструменты"
|
||||
},
|
||||
"disabled": "Текущая модель не поддерживает вызов функций и не может использовать плагины",
|
||||
"klavis": {
|
||||
"addServer": "Добавить сервер",
|
||||
"authCompleted": "Аутентификация завершена",
|
||||
"authFailed": "Ошибка аутентификации",
|
||||
"authRequired": "Требуется аутентификация",
|
||||
"connected": "Подключено",
|
||||
"error": "Ошибка",
|
||||
"groupName": "Инструменты Klavis",
|
||||
"manage": "Управление Klavis",
|
||||
"manageTitle": "Управление интеграцией Klavis",
|
||||
"noServers": "Нет подключённых серверов",
|
||||
"notEnabled": "Сервис Klavis не активирован",
|
||||
"oauthRequired": "Пожалуйста, завершите OAuth-аутентификацию в новом окне",
|
||||
"pendingAuth": "Ожидает аутентификации",
|
||||
"serverCreated": "Сервер успешно создан",
|
||||
"serverCreatedFailed": "Не удалось создать сервер",
|
||||
"serverRemoved": "Сервер удалён",
|
||||
"servers": "серверов",
|
||||
"tools": "инструментов",
|
||||
"verifyAuth": "Я завершил аутентификацию"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Активировано {{num}}",
|
||||
"groupName": "Плагины",
|
||||
"noEnabled": "Активированные плагины отсутствуют",
|
||||
"store": "Магазин плагинов"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Все",
|
||||
"installed": "Активировано"
|
||||
},
|
||||
"title": "Дополнительные инструменты"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Dahili Araçlar"
|
||||
},
|
||||
"disabled": "Mevcut model fonksiyon çağrılarını desteklemez, eklenti kullanılamaz",
|
||||
"klavis": {
|
||||
"addServer": "Sunucu Ekle",
|
||||
"authCompleted": "Kimlik Doğrulama Tamamlandı",
|
||||
"authFailed": "Kimlik Doğrulama Başarısız",
|
||||
"authRequired": "Kimlik Doğrulama Gerekli",
|
||||
"connected": "Bağlandı",
|
||||
"error": "Hata",
|
||||
"groupName": "Klavis Araçları",
|
||||
"manage": "Klavis'i Yönet",
|
||||
"manageTitle": "Klavis Entegrasyonunu Yönet",
|
||||
"noServers": "Bağlı sunucu yok",
|
||||
"notEnabled": "Klavis hizmeti etkin değil",
|
||||
"oauthRequired": "Lütfen yeni pencerede OAuth kimlik doğrulamasını tamamlayın",
|
||||
"pendingAuth": "Kimlik Doğrulama Bekleniyor",
|
||||
"serverCreated": "Sunucu başarıyla oluşturuldu",
|
||||
"serverCreatedFailed": "Sunucu oluşturulamadı",
|
||||
"serverRemoved": "Sunucu silindi",
|
||||
"servers": "sunucu",
|
||||
"tools": "araç",
|
||||
"verifyAuth": "Kimlik doğrulamayı tamamladım"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Etkin: {{num}}",
|
||||
"groupName": "Eklentiler",
|
||||
"noEnabled": "Etkin eklenti yok",
|
||||
"store": "Eklenti Mağazası"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Tümü",
|
||||
"installed": "Etkinleştirildi"
|
||||
},
|
||||
"title": "Uzantı Araçları"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "Mở rộng tích hợp sẵn"
|
||||
},
|
||||
"disabled": "Mô hình hiện tại không hỗ trợ gọi hàm, không thể sử dụng plugin",
|
||||
"klavis": {
|
||||
"addServer": "Thêm máy chủ",
|
||||
"authCompleted": "Xác thực hoàn tất",
|
||||
"authFailed": "Xác thực thất bại",
|
||||
"authRequired": "Yêu cầu xác thực",
|
||||
"connected": "Đã kết nối",
|
||||
"error": "Lỗi",
|
||||
"groupName": "Công cụ Klavis",
|
||||
"manage": "Quản lý Klavis",
|
||||
"manageTitle": "Quản lý tích hợp Klavis",
|
||||
"noServers": "Chưa có máy chủ nào được kết nối",
|
||||
"notEnabled": "Dịch vụ Klavis chưa được kích hoạt",
|
||||
"oauthRequired": "Vui lòng hoàn tất xác thực OAuth trong cửa sổ mới",
|
||||
"pendingAuth": "Đang chờ xác thực",
|
||||
"serverCreated": "Tạo máy chủ thành công",
|
||||
"serverCreatedFailed": "Tạo máy chủ thất bại",
|
||||
"serverRemoved": "Máy chủ đã bị xóa",
|
||||
"servers": "máy chủ",
|
||||
"tools": "công cụ",
|
||||
"verifyAuth": "Tôi đã hoàn tất xác thực"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Đã kích hoạt {{num}}",
|
||||
"groupName": "Tiện ích",
|
||||
"noEnabled": "Chưa có tiện ích nào được kích hoạt",
|
||||
"store": "Cửa hàng tiện ích"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "Tất cả",
|
||||
"installed": "Đã bật"
|
||||
},
|
||||
"title": "Công cụ mở rộng"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "内置插件"
|
||||
},
|
||||
"disabled": "当前模型不支持函数调用,无法使用插件",
|
||||
"klavis": {
|
||||
"addServer": "添加服务器",
|
||||
"authCompleted": "认证完成",
|
||||
"authFailed": "认证失败",
|
||||
"authRequired": "需要认证",
|
||||
"connected": "已连接",
|
||||
"error": "错误",
|
||||
"groupName": "Klavis 工具",
|
||||
"manage": "管理 Klavis",
|
||||
"manageTitle": "管理 Klavis 集成",
|
||||
"noServers": "暂无连接的服务器",
|
||||
"notEnabled": "Klavis 服务未启用",
|
||||
"oauthRequired": "请在新窗口中完成 OAuth 认证",
|
||||
"pendingAuth": "待认证",
|
||||
"serverCreated": "服务器创建成功",
|
||||
"serverCreatedFailed": "服务器创建失败",
|
||||
"serverRemoved": "服务器已删除",
|
||||
"servers": "个服务器",
|
||||
"tools": "个工具",
|
||||
"verifyAuth": "我已完成认证"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "已启用 {{num}}",
|
||||
"groupName": "三方插件",
|
||||
"noEnabled": "暂无启用插件",
|
||||
"store": "插件商店"
|
||||
},
|
||||
"tabs": {
|
||||
"installed": "已启用",
|
||||
"all": "全部"
|
||||
},
|
||||
"title": "扩展插件"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,12 +775,37 @@
|
||||
"groupName": "內置"
|
||||
},
|
||||
"disabled": "當前模型不支持函數調用,無法使用插件",
|
||||
"klavis": {
|
||||
"addServer": "新增伺服器",
|
||||
"authCompleted": "驗證完成",
|
||||
"authFailed": "驗證失敗",
|
||||
"authRequired": "需要驗證",
|
||||
"connected": "已連線",
|
||||
"error": "錯誤",
|
||||
"groupName": "Klavis 工具",
|
||||
"manage": "管理 Klavis",
|
||||
"manageTitle": "管理 Klavis 整合",
|
||||
"noServers": "尚未連線任何伺服器",
|
||||
"notEnabled": "Klavis 服務未啟用",
|
||||
"oauthRequired": "請在新視窗中完成 OAuth 驗證",
|
||||
"pendingAuth": "待驗證",
|
||||
"serverCreated": "伺服器建立成功",
|
||||
"serverCreatedFailed": "伺服器建立失敗",
|
||||
"serverRemoved": "伺服器已刪除",
|
||||
"servers": "個伺服器",
|
||||
"tools": "個工具",
|
||||
"verifyAuth": "我已完成驗證"
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "已啟用 {{num}}",
|
||||
"groupName": "插件",
|
||||
"noEnabled": "暫無啟用插件",
|
||||
"store": "插件商店"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "全部",
|
||||
"installed": "已啟用"
|
||||
},
|
||||
"title": "擴展工具"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"jose": "^5.10.0",
|
||||
"js-sha256": "^0.11.1",
|
||||
"jsonl-parse-stringify": "^1.0.3",
|
||||
"klavis": "^2.13.2",
|
||||
"langchain": "^0.3.36",
|
||||
"langfuse": "^3.38.6",
|
||||
"langfuse-core": "^3.38.6",
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from './branding';
|
||||
export * from './currency';
|
||||
export * from './desktop';
|
||||
export * from './discover';
|
||||
export * from './klavis';
|
||||
export * from './layoutTokens';
|
||||
export * from './message';
|
||||
export * from './meta';
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { IconType, SiCaldotcom, SiGithub, SiLinear } from '@icons-pack/react-simple-icons';
|
||||
import { Klavis } from 'klavis';
|
||||
|
||||
export interface KlavisServerType {
|
||||
icon: string | IconType;
|
||||
/**
|
||||
* Identifier used for storage in database (e.g., 'google-calendar')
|
||||
* Format: lowercase, spaces replaced with hyphens
|
||||
*/
|
||||
identifier: string;
|
||||
label: string;
|
||||
/**
|
||||
* Server name used to call Klavis API (e.g., 'Google Calendar')
|
||||
*/
|
||||
serverName: Klavis.McpServerName;
|
||||
}
|
||||
|
||||
export const KLAVIS_SERVER_TYPES: KlavisServerType[] = [
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/gmail.svg',
|
||||
identifier: 'gmail',
|
||||
label: 'Gmail',
|
||||
serverName: Klavis.McpServerName.Gmail,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googlecalendar.svg',
|
||||
identifier: 'google-calendar',
|
||||
label: 'Google Calendar',
|
||||
serverName: Klavis.McpServerName.GoogleCalendar,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/notion.svg',
|
||||
identifier: 'notion',
|
||||
label: 'Notion',
|
||||
serverName: Klavis.McpServerName.Notion,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/airtable.svg',
|
||||
identifier: 'airtable',
|
||||
label: 'Airtable',
|
||||
serverName: Klavis.McpServerName.Airtable,
|
||||
},
|
||||
{
|
||||
icon: SiLinear,
|
||||
identifier: 'linear',
|
||||
label: 'Linear',
|
||||
serverName: Klavis.McpServerName.Linear,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googlesheets.svg',
|
||||
identifier: 'google-sheets',
|
||||
label: 'Google Sheets',
|
||||
serverName: Klavis.McpServerName.GoogleSheets,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googledocs.svg',
|
||||
identifier: 'google-docs',
|
||||
label: 'Google Docs',
|
||||
serverName: Klavis.McpServerName.GoogleDocs,
|
||||
},
|
||||
{
|
||||
icon: SiGithub,
|
||||
identifier: 'github',
|
||||
label: 'GitHub',
|
||||
serverName: Klavis.McpServerName.Github,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/supabase.svg',
|
||||
identifier: 'supabase',
|
||||
label: 'Supabase',
|
||||
serverName: Klavis.McpServerName.Supabase,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googledrive.svg',
|
||||
identifier: 'google-drive',
|
||||
label: 'Google Drive',
|
||||
serverName: Klavis.McpServerName.GoogleDrive,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/slack.svg',
|
||||
identifier: 'slack',
|
||||
label: 'Slack',
|
||||
serverName: Klavis.McpServerName.Slack,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/confluence.svg',
|
||||
identifier: 'confluence',
|
||||
label: 'Confluence',
|
||||
serverName: Klavis.McpServerName.Confluence,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/jira.svg',
|
||||
identifier: 'jira',
|
||||
label: 'Jira',
|
||||
serverName: Klavis.McpServerName.Jira,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/clickup.svg',
|
||||
identifier: 'clickup',
|
||||
label: 'ClickUp',
|
||||
serverName: Klavis.McpServerName.Clickup,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/dropbox.svg',
|
||||
identifier: 'dropbox',
|
||||
label: 'Dropbox',
|
||||
serverName: Klavis.McpServerName.Dropbox,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/figma.svg',
|
||||
identifier: 'figma',
|
||||
label: 'Figma',
|
||||
serverName: Klavis.McpServerName.Figma,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/hubspot.svg',
|
||||
identifier: 'hubspot',
|
||||
label: 'HubSpot',
|
||||
serverName: Klavis.McpServerName.Hubspot,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/onedrive.svg',
|
||||
identifier: 'onedrive',
|
||||
label: 'OneDrive',
|
||||
serverName: Klavis.McpServerName.Onedrive,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/outlook.svg',
|
||||
identifier: 'outlook-mail',
|
||||
label: 'Outlook Mail',
|
||||
serverName: Klavis.McpServerName.OutlookMail,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/salesforce.svg',
|
||||
identifier: 'salesforce',
|
||||
label: 'Salesforce',
|
||||
serverName: Klavis.McpServerName.Salesforce,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/whatsapp.svg',
|
||||
identifier: 'whatsapp',
|
||||
label: 'WhatsApp',
|
||||
serverName: Klavis.McpServerName.Whatsapp,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/youtube.svg',
|
||||
identifier: 'youtube',
|
||||
label: 'YouTube',
|
||||
serverName: Klavis.McpServerName.Youtube,
|
||||
},
|
||||
{
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/zendesk.svg',
|
||||
identifier: 'zendesk',
|
||||
label: 'Zendesk',
|
||||
serverName: Klavis.McpServerName.Zendesk,
|
||||
},
|
||||
{
|
||||
icon: SiCaldotcom,
|
||||
identifier: 'cal-com',
|
||||
label: 'Cal.com',
|
||||
serverName: Klavis.McpServerName.CalCom,
|
||||
},
|
||||
];
|
||||
@@ -423,4 +423,4 @@
|
||||
}
|
||||
],
|
||||
"version": "6"
|
||||
}
|
||||
}
|
||||
@@ -939,4 +939,4 @@
|
||||
"folderMillis": 1764858574403,
|
||||
"hash": "7838f9938b370867470e5e11807855253d23b11c2ac6aa9e90687844a356c949"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -16,7 +16,7 @@ export class PluginModel {
|
||||
create = async (
|
||||
params: Pick<
|
||||
NewInstalledPlugin,
|
||||
'type' | 'identifier' | 'manifest' | 'customParams' | 'settings'
|
||||
'type' | 'identifier' | 'manifest' | 'customParams' | 'settings' | 'source'
|
||||
>,
|
||||
) => {
|
||||
const [result] = await this.db
|
||||
|
||||
@@ -23,6 +23,11 @@ export interface ChatPluginPayload {
|
||||
type: LobeToolRenderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool source indicates where the tool comes from
|
||||
*/
|
||||
export type ToolSource = 'builtin' | 'plugin' | 'mcp' | 'klavis';
|
||||
|
||||
export interface ChatToolPayload {
|
||||
apiName: string;
|
||||
arguments: string;
|
||||
@@ -30,6 +35,10 @@ export interface ChatToolPayload {
|
||||
identifier: string;
|
||||
intervention?: ToolIntervention;
|
||||
result_msg_id?: string;
|
||||
/**
|
||||
* Tool source indicates where the tool comes from
|
||||
*/
|
||||
source?: ToolSource;
|
||||
thoughtSignature?: string;
|
||||
type: LobeToolRenderType;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export type ServerLanguageModel = Partial<Record<GlobalLLMProviderKey, ServerMod
|
||||
export interface GlobalServerConfig {
|
||||
aiProvider: ServerLanguageModel;
|
||||
defaultAgent?: PartialDeep<UserDefaultAgent>;
|
||||
enableKlavis?: boolean;
|
||||
enableUploadFileToServer?: boolean;
|
||||
enabledAccessCode?: boolean;
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,16 @@ export interface CustomPluginParams {
|
||||
// Added headers configuration support
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
/**
|
||||
* Klavis integration parameters
|
||||
*/
|
||||
klavis?: {
|
||||
instanceId: string;
|
||||
isAuthenticated: boolean;
|
||||
oauthUrl?: string;
|
||||
serverName: string;
|
||||
serverUrl: string;
|
||||
};
|
||||
avatar?: string;
|
||||
description?: string;
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Klavis Service Configuration
|
||||
*
|
||||
* Architecture:
|
||||
* - Server-side: KLAVIS_API_KEY is stored and used only on the server
|
||||
* - Client-side: Klavis enabled status is provided via serverConfig store (enableKlavis)
|
||||
* - Client calls server APIs which use the API key
|
||||
*
|
||||
* Security:
|
||||
* - API key is NEVER exposed to the client
|
||||
* - Client gets enabled status from server config
|
||||
*/
|
||||
export const getKlavisConfig = () => {
|
||||
return createEnv({
|
||||
client: {},
|
||||
runtimeEnv: {
|
||||
// Server-side API key (never exposed to client)
|
||||
KLAVIS_API_KEY: process.env.KLAVIS_API_KEY,
|
||||
},
|
||||
server: {
|
||||
KLAVIS_API_KEY: z.string().optional(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const klavisEnv = getKlavisConfig();
|
||||
|
||||
/**
|
||||
* Get Klavis API Key (server-side only)
|
||||
* IMPORTANT: This should only be called from server-side code
|
||||
*/
|
||||
export const getServerKlavisApiKey = (): string | undefined => {
|
||||
if (typeof window !== 'undefined') {
|
||||
console.error('[Klavis] Attempted to access API key from client-side!');
|
||||
return undefined;
|
||||
}
|
||||
return klavisEnv.KLAVIS_API_KEY;
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { Checkbox } from 'antd';
|
||||
import { Loader2, SquareArrowOutUpRight, Unplug } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { KlavisServer, KlavisServerStatus } from '@/store/tool/slices/klavisStore';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userProfileSelectors } from '@/store/user/selectors';
|
||||
|
||||
// 轮询配置
|
||||
const POLL_INTERVAL_MS = 1000; // 每秒轮询一次
|
||||
const POLL_TIMEOUT_MS = 15_000; // 15 秒超时
|
||||
|
||||
interface KlavisServerItemProps {
|
||||
/**
|
||||
* Identifier used for storage (e.g., 'google-calendar')
|
||||
*/
|
||||
identifier: string;
|
||||
label: string;
|
||||
server?: KlavisServer;
|
||||
/**
|
||||
* Server name used to call Klavis API (e.g., 'Google Calendar')
|
||||
*/
|
||||
serverName: string;
|
||||
}
|
||||
|
||||
const KlavisServerItem = memo<KlavisServerItemProps>(
|
||||
({ identifier, label, server, serverName }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isToggling, setIsToggling] = useState(false);
|
||||
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
|
||||
|
||||
const oauthWindowRef = useRef<Window | null>(null);
|
||||
const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const userId = useUserStore(userProfileSelectors.userId);
|
||||
const createKlavisServer = useToolStore((s) => s.createKlavisServer);
|
||||
const refreshKlavisServerTools = useToolStore((s) => s.refreshKlavisServerTools);
|
||||
const removeKlavisServer = useToolStore((s) => s.removeKlavisServer);
|
||||
|
||||
// 清理所有定时器
|
||||
const cleanup = useCallback(() => {
|
||||
if (windowCheckIntervalRef.current) {
|
||||
clearInterval(windowCheckIntervalRef.current);
|
||||
windowCheckIntervalRef.current = null;
|
||||
}
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
if (pollTimeoutRef.current) {
|
||||
clearTimeout(pollTimeoutRef.current);
|
||||
pollTimeoutRef.current = null;
|
||||
}
|
||||
oauthWindowRef.current = null;
|
||||
setIsWaitingAuth(false);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup]);
|
||||
|
||||
// 当服务器状态变为 CONNECTED 时停止所有监听
|
||||
useEffect(() => {
|
||||
if (server?.status === KlavisServerStatus.CONNECTED && isWaitingAuth) {
|
||||
cleanup();
|
||||
}
|
||||
}, [server?.status, isWaitingAuth, cleanup, t]);
|
||||
|
||||
/**
|
||||
* 启动降级轮询(当 window.closed 不可访问时)
|
||||
*/
|
||||
const startFallbackPolling = useCallback(
|
||||
(serverName: string) => {
|
||||
// 已经在轮询了,不重复启动
|
||||
if (pollIntervalRef.current) return;
|
||||
|
||||
// 每秒轮询一次
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
try {
|
||||
await refreshKlavisServerTools(serverName);
|
||||
} catch (error) {
|
||||
console.error('[Klavis] Failed to check auth status:', error);
|
||||
}
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
// 15 秒后超时停止
|
||||
pollTimeoutRef.current = setTimeout(() => {
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
setIsWaitingAuth(false);
|
||||
}, POLL_TIMEOUT_MS);
|
||||
},
|
||||
[refreshKlavisServerTools, t],
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听 OAuth 窗口关闭
|
||||
*/
|
||||
const startWindowMonitor = useCallback(
|
||||
(oauthWindow: Window, serverName: string) => {
|
||||
// 每 500ms 检查窗口状态
|
||||
windowCheckIntervalRef.current = setInterval(() => {
|
||||
try {
|
||||
// 尝试访问 window.closed(可能被 COOP 阻止)
|
||||
if (oauthWindow.closed) {
|
||||
// 窗口已关闭,清理监听并检查认证状态
|
||||
if (windowCheckIntervalRef.current) {
|
||||
clearInterval(windowCheckIntervalRef.current);
|
||||
windowCheckIntervalRef.current = null;
|
||||
}
|
||||
oauthWindowRef.current = null;
|
||||
|
||||
// 窗口关闭后立即检查一次认证状态
|
||||
refreshKlavisServerTools(serverName);
|
||||
}
|
||||
} catch {
|
||||
// COOP 阻止了访问,降级到轮询方案
|
||||
console.log('[Klavis] COOP blocked window.closed access, falling back to polling');
|
||||
if (windowCheckIntervalRef.current) {
|
||||
clearInterval(windowCheckIntervalRef.current);
|
||||
windowCheckIntervalRef.current = null;
|
||||
}
|
||||
startFallbackPolling(serverName);
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
[refreshKlavisServerTools, startFallbackPolling],
|
||||
);
|
||||
|
||||
/**
|
||||
* 打开 OAuth 窗口
|
||||
*/
|
||||
const openOAuthWindow = useCallback(
|
||||
(oauthUrl: string, serverName: string) => {
|
||||
// 清理之前的状态
|
||||
cleanup();
|
||||
setIsWaitingAuth(true);
|
||||
|
||||
// 打开 OAuth 窗口
|
||||
const oauthWindow = window.open(oauthUrl, '_blank', 'width=600,height=700');
|
||||
if (oauthWindow) {
|
||||
oauthWindowRef.current = oauthWindow;
|
||||
startWindowMonitor(oauthWindow, serverName);
|
||||
} else {
|
||||
// 窗口被阻止,直接用轮询
|
||||
startFallbackPolling(serverName);
|
||||
}
|
||||
},
|
||||
[cleanup, startWindowMonitor, startFallbackPolling, t],
|
||||
);
|
||||
|
||||
// Get plugin ID for this server (使用 identifier 作为 pluginId)
|
||||
const pluginId = server ? server.identifier : '';
|
||||
const [checked, togglePlugin] = useAgentStore((s) => [
|
||||
agentSelectors.currentAgentPlugins(s).includes(pluginId),
|
||||
s.togglePlugin,
|
||||
]);
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (server) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
const newServer = await createKlavisServer({
|
||||
identifier,
|
||||
serverName,
|
||||
userId,
|
||||
});
|
||||
|
||||
if (newServer) {
|
||||
// 安装完成后自动启用插件(使用 identifier)
|
||||
const newPluginId = newServer.identifier;
|
||||
await togglePlugin(newPluginId);
|
||||
|
||||
// 如果已认证,直接刷新工具列表,跳过 OAuth
|
||||
if (newServer.isAuthenticated) {
|
||||
await refreshKlavisServerTools(newServer.identifier);
|
||||
} else if (newServer.oauthUrl) {
|
||||
// 需要 OAuth,打开 OAuth 窗口并监听关闭
|
||||
openOAuthWindow(newServer.oauthUrl, newServer.identifier);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Klavis] Failed to connect server:', error);
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (!server) return;
|
||||
setIsToggling(true);
|
||||
await togglePlugin(pluginId);
|
||||
setIsToggling(false);
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
if (!server) return;
|
||||
setIsToggling(true);
|
||||
// 如果当前已启用,先禁用
|
||||
if (checked) {
|
||||
await togglePlugin(pluginId);
|
||||
}
|
||||
// 删除服务器(使用 identifier)
|
||||
await removeKlavisServer(server.identifier);
|
||||
setIsToggling(false);
|
||||
};
|
||||
|
||||
// 渲染右侧控件
|
||||
const renderRightControl = () => {
|
||||
// 正在连接中
|
||||
if (isConnecting) {
|
||||
return (
|
||||
<Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
|
||||
<Icon icon={Loader2} spin />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
// 未连接,显示 Connect 按钮
|
||||
if (!server) {
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleConnect();
|
||||
}}
|
||||
style={{ cursor: 'pointer', opacity: 0.65 }}
|
||||
>
|
||||
{t('tools.klavis.connect', { defaultValue: 'Connect' })}
|
||||
<Icon icon={SquareArrowOutUpRight} size="small" />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
// 根据状态显示不同控件
|
||||
switch (server.status) {
|
||||
case KlavisServerStatus.CONNECTED: {
|
||||
// 正在切换状态
|
||||
if (isToggling) {
|
||||
return <Icon icon={Loader2} spin />;
|
||||
}
|
||||
return (
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Icon
|
||||
icon={Unplug}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDisconnect();
|
||||
}}
|
||||
size="small"
|
||||
style={{ cursor: 'pointer', opacity: 0.5 }}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
case KlavisServerStatus.PENDING_AUTH: {
|
||||
// 正在等待认证
|
||||
if (isWaitingAuth) {
|
||||
return (
|
||||
<Flexbox align="center" gap={4} horizontal onClick={(e) => e.stopPropagation()}>
|
||||
<Icon icon={Loader2} spin />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
gap={4}
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 点击重新打开 OAuth 窗口
|
||||
if (server.oauthUrl) {
|
||||
openOAuthWindow(server.oauthUrl, server.identifier);
|
||||
}
|
||||
}}
|
||||
style={{ cursor: 'pointer', opacity: 0.65 }}
|
||||
>
|
||||
{t('tools.klavis.pendingAuth', { defaultValue: 'Authorize' })}
|
||||
<Icon icon={SquareArrowOutUpRight} size="small" />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
case KlavisServerStatus.ERROR: {
|
||||
return (
|
||||
<span style={{ color: 'red', fontSize: 12 }}>
|
||||
{t('tools.klavis.error', { defaultValue: 'Error' })}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
gap={24}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 如果已连接,点击整行切换状态
|
||||
if (server?.status === KlavisServerStatus.CONNECTED) {
|
||||
handleToggle();
|
||||
}
|
||||
}}
|
||||
style={{ paddingLeft: 8 }}
|
||||
>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{label}
|
||||
</Flexbox>
|
||||
{renderRightControl()}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default KlavisServerItem;
|
||||
@@ -1,22 +1,43 @@
|
||||
import { Segmented } from '@lobehub/ui';
|
||||
import { Blocks } from 'lucide-react';
|
||||
import { Suspense, memo, useState } from 'react';
|
||||
import { Suspense, memo, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PluginStore from '@/features/PluginStore';
|
||||
import { useModelSupportToolUse } from '@/hooks/useModelSupportToolUse';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import {
|
||||
featureFlagsSelectors,
|
||||
serverConfigSelectors,
|
||||
useServerConfigStore,
|
||||
} from '@/store/serverConfig';
|
||||
|
||||
import Action from '../components/Action';
|
||||
import { useControls } from './useControls';
|
||||
|
||||
type TabType = 'all' | 'installed';
|
||||
|
||||
const Tools = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const items = useControls({ setModalOpen, setUpdating });
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
const { marketItems, installedPluginItems } = useControls({
|
||||
setModalOpen,
|
||||
setUpdating,
|
||||
});
|
||||
const { enablePlugins } = useServerConfigStore(featureFlagsSelectors);
|
||||
const enableKlavis = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
// Set default tab based on installed plugins (only on first load)
|
||||
useEffect(() => {
|
||||
if (!isInitializedRef.current && installedPluginItems.length >= 0) {
|
||||
isInitializedRef.current = true;
|
||||
setActiveTab(installedPluginItems.length > 0 ? 'installed' : 'all');
|
||||
}
|
||||
}, [installedPluginItems.length]);
|
||||
|
||||
const model = useAgentStore(agentSelectors.currentAgentModel);
|
||||
const provider = useAgentStore(agentSelectors.currentAgentModelProvider);
|
||||
@@ -27,13 +48,44 @@ const Tools = memo(() => {
|
||||
if (!enableFC)
|
||||
return <Action disabled icon={Blocks} showTooltip={true} title={t('tools.disabled')} />;
|
||||
|
||||
// Use effective tab for display (default to market while initializing)
|
||||
const effectiveTab = activeTab ?? 'all';
|
||||
const currentItems = effectiveTab === 'all' ? marketItems : installedPluginItems;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Action disabled icon={Blocks} title={t('tools.title')} />}>
|
||||
<Action
|
||||
dropdown={{
|
||||
maxHeight: 500,
|
||||
maxWidth: 480,
|
||||
menu: { items },
|
||||
menu: {
|
||||
items: [
|
||||
{
|
||||
key: 'tabs',
|
||||
label: (
|
||||
<Segmented
|
||||
block
|
||||
onChange={(v) => setActiveTab(v as TabType)}
|
||||
options={[
|
||||
{
|
||||
label: t('tools.tabs.all', { defaultValue: 'all' }),
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
|
||||
value: 'installed',
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
value={effectiveTab}
|
||||
/>
|
||||
),
|
||||
type: 'group',
|
||||
},
|
||||
...currentItems,
|
||||
],
|
||||
},
|
||||
minHeight: enableKlavis ? 500 : undefined,
|
||||
minWidth: 320,
|
||||
}}
|
||||
icon={Blocks}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { KLAVIS_SERVER_TYPES, KlavisServerType } from '@lobechat/const';
|
||||
import { Avatar, Icon, ItemType } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ArrowRight, Store, ToyBrick } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
@@ -9,11 +13,37 @@ import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
|
||||
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { builtinToolSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import {
|
||||
builtinToolSelectors,
|
||||
klavisStoreSelectors,
|
||||
pluginSelectors,
|
||||
} from '@/store/tool/selectors';
|
||||
|
||||
import KlavisServerItem from './KlavisServerItem';
|
||||
import ToolItem from './ToolItem';
|
||||
|
||||
/**
|
||||
* Klavis 服务器图标组件
|
||||
* 对于 string 类型的 icon,使用 Image 组件渲染
|
||||
* 对于 IconType 类型的 icon,使用 Icon 组件渲染,并根据主题设置填充色
|
||||
*/
|
||||
const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (typeof icon === 'string') {
|
||||
return (
|
||||
<Image alt={label} height={18} src={icon} style={{ flex: 'none' }} unoptimized width={18} />
|
||||
);
|
||||
}
|
||||
|
||||
// 使用主题色填充,在深色模式下自动适应
|
||||
return <Icon fill={theme.colorText} icon={icon} size={18} />;
|
||||
});
|
||||
|
||||
KlavisIcon.displayName = 'KlavisIcon';
|
||||
|
||||
export const useControls = ({
|
||||
setModalOpen,
|
||||
setUpdating,
|
||||
@@ -36,15 +66,67 @@ export const useControls = ({
|
||||
);
|
||||
const plugins = useAgentStore((s) => agentSelectors.currentAgentPlugins(s));
|
||||
|
||||
const [useFetchPluginStore] = useToolStore((s) => [s.useFetchPluginStore]);
|
||||
// Klavis 相关状态
|
||||
const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
|
||||
const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
||||
|
||||
const [useFetchPluginStore, useFetchUserKlavisServers] = useToolStore((s) => [
|
||||
s.useFetchPluginStore,
|
||||
s.useFetchUserKlavisServers,
|
||||
]);
|
||||
|
||||
useFetchPluginStore();
|
||||
useFetchInstalledPlugins();
|
||||
useCheckPluginsIsInstalled(plugins);
|
||||
|
||||
const items: ItemType[] = [
|
||||
{
|
||||
children: builtinList.map((item) => ({
|
||||
// 使用 SWR 加载用户的 Klavis 集成(从数据库)
|
||||
useFetchUserKlavisServers(isKlavisEnabledInEnv);
|
||||
|
||||
// 根据 identifier 获取已连接的服务器
|
||||
const getServerByName = (identifier: string) => {
|
||||
return allKlavisServers.find((server) => server.identifier === identifier);
|
||||
};
|
||||
|
||||
// 获取所有 Klavis 服务器类型的 identifier 集合(用于过滤 builtinList)
|
||||
// 这里使用 KLAVIS_SERVER_TYPES 而不是已连接的服务器,因为我们要过滤掉所有可能的 Klavis 类型
|
||||
const allKlavisTypeIdentifiers = useMemo(
|
||||
() => new Set(KLAVIS_SERVER_TYPES.map((type) => type.identifier)),
|
||||
[],
|
||||
);
|
||||
// 过滤掉 builtinList 中的 klavis 工具(它们会单独显示在 Klavis 区域)
|
||||
const filteredBuiltinList = useMemo(
|
||||
() =>
|
||||
isKlavisEnabledInEnv
|
||||
? builtinList.filter((item) => !allKlavisTypeIdentifiers.has(item.identifier))
|
||||
: builtinList,
|
||||
[builtinList, allKlavisTypeIdentifiers, isKlavisEnabledInEnv],
|
||||
);
|
||||
|
||||
// Klavis 服务器列表项
|
||||
const klavisServerItems = useMemo(
|
||||
() =>
|
||||
isKlavisEnabledInEnv
|
||||
? KLAVIS_SERVER_TYPES.map((type) => ({
|
||||
icon: <KlavisIcon icon={type.icon} label={type.label} />,
|
||||
key: type.identifier,
|
||||
label: (
|
||||
<KlavisServerItem
|
||||
identifier={type.identifier}
|
||||
label={type.label}
|
||||
server={getServerByName(type.identifier)}
|
||||
serverName={type.serverName}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
: [],
|
||||
[isKlavisEnabledInEnv, allKlavisServers],
|
||||
);
|
||||
|
||||
// 合并 builtin 工具和 Klavis 服务器
|
||||
const builtinItems = useMemo(
|
||||
() => [
|
||||
// 原有的 builtin 工具
|
||||
...filteredBuiltinList.map((item) => ({
|
||||
icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
|
||||
key: item.identifier,
|
||||
label: (
|
||||
@@ -60,7 +142,16 @@ export const useControls = ({
|
||||
/>
|
||||
),
|
||||
})),
|
||||
// Klavis 服务器
|
||||
...klavisServerItems,
|
||||
],
|
||||
[filteredBuiltinList, klavisServerItems, checked, togglePlugin, setUpdating],
|
||||
);
|
||||
|
||||
// 市场 tab 的 items
|
||||
const marketItems: ItemType[] = [
|
||||
{
|
||||
children: builtinItems,
|
||||
key: 'builtins',
|
||||
label: t('tools.builtins.groupName'),
|
||||
type: 'group',
|
||||
@@ -113,5 +204,82 @@ export const useControls = ({
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
// 已安装 tab 的 items - 只显示已安装的插件
|
||||
const installedPluginItems: ItemType[] = useMemo(() => {
|
||||
const installedItems: ItemType[] = [];
|
||||
|
||||
// 已安装的 builtin 工具
|
||||
const enabledBuiltinItems = filteredBuiltinList
|
||||
.filter((item) => checked.includes(item.identifier))
|
||||
.map((item) => ({
|
||||
icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
|
||||
key: item.identifier,
|
||||
label: (
|
||||
<ToolItem
|
||||
checked={true}
|
||||
id={item.identifier}
|
||||
label={item.meta?.title}
|
||||
onUpdate={async () => {
|
||||
setUpdating(true);
|
||||
await togglePlugin(item.identifier);
|
||||
setUpdating(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// 已连接的 Klavis 服务器(放在 builtin 里面)
|
||||
const connectedKlavisItems = klavisServerItems.filter((item) =>
|
||||
checked.includes(item.key as string),
|
||||
);
|
||||
|
||||
// 合并 builtin 和 Klavis
|
||||
const allBuiltinItems = [...enabledBuiltinItems, ...connectedKlavisItems];
|
||||
|
||||
if (allBuiltinItems.length > 0) {
|
||||
installedItems.push({
|
||||
children: allBuiltinItems,
|
||||
key: 'installed-builtins',
|
||||
label: t('tools.builtins.groupName'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
// 已安装的插件
|
||||
const installedPlugins = list
|
||||
.filter((item) => checked.includes(item.identifier))
|
||||
.map((item) => ({
|
||||
icon: item?.avatar ? (
|
||||
<PluginAvatar avatar={item.avatar} size={20} />
|
||||
) : (
|
||||
<Icon icon={ToyBrick} size={20} />
|
||||
),
|
||||
key: item.identifier,
|
||||
label: (
|
||||
<ToolItem
|
||||
checked={true}
|
||||
id={item.identifier}
|
||||
label={item.title}
|
||||
onUpdate={async () => {
|
||||
setUpdating(true);
|
||||
await togglePlugin(item.identifier);
|
||||
setUpdating(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
if (installedPlugins.length > 0) {
|
||||
installedItems.push({
|
||||
children: installedPlugins,
|
||||
key: 'installed-plugins',
|
||||
label: t('tools.plugins.groupName'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
return installedItems;
|
||||
}, [filteredBuiltinList, list, klavisServerItems, checked, togglePlugin, setUpdating, t]);
|
||||
|
||||
return { installedPluginItems, marketItems };
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ const useStyles = createStyles(({ css, prefixCls }) => ({
|
||||
export interface ActionDropdownProps extends DropdownProps {
|
||||
maxHeight?: number | string;
|
||||
maxWidth?: number | string;
|
||||
minHeight?: number | string;
|
||||
minWidth?: number | string;
|
||||
/**
|
||||
* 是否在挂载时预渲染弹层,避免首次触发展开时的渲染卡顿
|
||||
@@ -30,7 +31,7 @@ export interface ActionDropdownProps extends DropdownProps {
|
||||
}
|
||||
|
||||
const ActionDropdown = memo<ActionDropdownProps>(
|
||||
({ menu, maxHeight, minWidth, maxWidth, children, placement = 'top', ...rest }) => {
|
||||
({ menu, maxHeight, minWidth, maxWidth, children, placement = 'top', minHeight, ...rest }) => {
|
||||
const { cx, styles } = useStyles();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -48,6 +49,7 @@ const ActionDropdown = memo<ActionDropdownProps>(
|
||||
style: {
|
||||
maxHeight,
|
||||
maxWidth: isMobile ? undefined : maxWidth,
|
||||
minHeight,
|
||||
minWidth: isMobile ? undefined : minWidth,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
|
||||
@@ -67,6 +67,9 @@ vi.mock('@/store/tool/selectors', () => ({
|
||||
pluginSelectors: {
|
||||
installedPluginManifestList: () => [],
|
||||
},
|
||||
klavisStoreSelectors: {
|
||||
klavisAsLobeTools: () => [],
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../isCanUseFC', () => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { getToolStoreState } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
import { klavisStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import { KnowledgeBaseManifest } from '@/tools/knowledge-base';
|
||||
import { WebBrowsingManifest } from '@/tools/web-browsing';
|
||||
|
||||
@@ -45,8 +45,19 @@ export const createToolsEngine = (config: ToolsEngineConfig = {}): ToolsEngine =
|
||||
(tool) => tool.manifest as LobeChatPluginManifest,
|
||||
);
|
||||
|
||||
// Get Klavis tool manifests
|
||||
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
||||
const klavisManifests = klavisTools
|
||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
||||
.filter(Boolean);
|
||||
|
||||
// Combine all manifests
|
||||
const allManifests = [...pluginManifests, ...builtinManifests, ...additionalManifests];
|
||||
const allManifests = [
|
||||
...pluginManifests,
|
||||
...builtinManifests,
|
||||
...klavisManifests,
|
||||
...additionalManifests,
|
||||
];
|
||||
|
||||
return new ToolsEngine({
|
||||
defaultToolIds,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { KlavisClient } from 'klavis';
|
||||
|
||||
import { getServerKlavisApiKey } from '@/config/klavis';
|
||||
|
||||
/**
|
||||
* Global Klavis Client instance cache (server-side only)
|
||||
*/
|
||||
let klavisClientInstance: { apiKey: string; client: KlavisClient } | undefined;
|
||||
|
||||
/**
|
||||
* Get or create Klavis Client instance (server-side only)
|
||||
* The instance is cached and reused if the API key hasn't changed
|
||||
*/
|
||||
export const getKlavisClient = (): KlavisClient => {
|
||||
const apiKey = getServerKlavisApiKey();
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('Klavis API key is not configured on server');
|
||||
}
|
||||
|
||||
if (!klavisClientInstance || klavisClientInstance.apiKey !== apiKey) {
|
||||
klavisClientInstance = {
|
||||
apiKey,
|
||||
client: new KlavisClient({ apiKey }),
|
||||
};
|
||||
}
|
||||
|
||||
return klavisClientInstance.client;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if Klavis client is available (has API key configured)
|
||||
*/
|
||||
export const isKlavisClientAvailable = (): boolean => {
|
||||
return !!getServerKlavisApiKey();
|
||||
};
|
||||
@@ -779,12 +779,37 @@ export default {
|
||||
groupName: '内置插件',
|
||||
},
|
||||
disabled: '当前模型不支持函数调用,无法使用插件',
|
||||
klavis: {
|
||||
addServer: '添加服务器',
|
||||
authCompleted: '认证完成',
|
||||
authFailed: '认证失败',
|
||||
authRequired: '需要认证',
|
||||
connected: '已连接',
|
||||
error: '错误',
|
||||
groupName: 'Klavis 工具',
|
||||
manage: '管理 Klavis',
|
||||
manageTitle: '管理 Klavis 集成',
|
||||
noServers: '暂无连接的服务器',
|
||||
notEnabled: 'Klavis 服务未启用',
|
||||
oauthRequired: '请在新窗口中完成 OAuth 认证',
|
||||
pendingAuth: '待认证',
|
||||
serverCreated: '服务器创建成功',
|
||||
serverCreatedFailed: '服务器创建失败',
|
||||
serverRemoved: '服务器已删除',
|
||||
servers: '个服务器',
|
||||
tools: '个工具',
|
||||
verifyAuth: '我已完成认证',
|
||||
},
|
||||
plugins: {
|
||||
enabled: '已启用 {{num}}',
|
||||
groupName: '三方插件',
|
||||
noEnabled: '暂无启用插件',
|
||||
store: '插件商店',
|
||||
},
|
||||
tabs: {
|
||||
all: '全部',
|
||||
installed: '已启用',
|
||||
},
|
||||
title: '扩展插件',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { klavisEnv } from '@/config/klavis';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { appEnv, getAppConfig } from '@/envs/app';
|
||||
import { authEnv } from '@/envs/auth';
|
||||
@@ -66,6 +67,7 @@ export const getServerGlobalConfig = async () => {
|
||||
defaultAgent: {
|
||||
config: parseAgentConfig(DEFAULT_AGENT_CONFIG),
|
||||
},
|
||||
enableKlavis: !!klavisEnv.KLAVIS_API_KEY,
|
||||
enableUploadFileToServer: !!fileEnv.S3_SECRET_ACCESS_KEY,
|
||||
enabledAccessCode: ACCESS_CODES?.length > 0,
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { generationTopicRouter } from './generationTopic';
|
||||
import { groupRouter } from './group';
|
||||
import { imageRouter } from './image';
|
||||
import { importerRouter } from './importer';
|
||||
import { klavisRouter } from './klavis';
|
||||
import { knowledgeBaseRouter } from './knowledgeBase';
|
||||
import { marketRouter } from './market';
|
||||
import { messageRouter } from './message';
|
||||
@@ -52,6 +53,7 @@ export const lambdaRouter = router({
|
||||
healthcheck: publicProcedure.query(() => "i'm live!"),
|
||||
image: imageRouter,
|
||||
importer: importerRouter,
|
||||
klavis: klavisRouter,
|
||||
knowledgeBase: knowledgeBaseRouter,
|
||||
market: marketRouter,
|
||||
message: messageRouter,
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PluginModel } from '@/database/models/plugin';
|
||||
import { getKlavisClient } from '@/libs/klavis';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
|
||||
/**
|
||||
* Klavis procedure with API key validation and database access
|
||||
*/
|
||||
const klavisProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
|
||||
const client = getKlavisClient();
|
||||
const pluginModel = new PluginModel(opts.ctx.serverDB, opts.ctx.userId);
|
||||
|
||||
return opts.next({
|
||||
ctx: { ...opts.ctx, klavisClient: client, pluginModel },
|
||||
});
|
||||
});
|
||||
|
||||
export const klavisRouter = router({
|
||||
/**
|
||||
* Create a single MCP server instance and save to database
|
||||
* Returns: { serverUrl, instanceId, oauthUrl?, identifier, serverName }
|
||||
*/
|
||||
createServerInstance: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
/** Identifier for storage (e.g., 'google-calendar') */
|
||||
identifier: z.string(),
|
||||
/** Server name for Klavis API (e.g., 'Google Calendar') */
|
||||
serverName: z.string(),
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { serverName, userId, identifier } = input;
|
||||
|
||||
// 创建单个服务器实例
|
||||
const response = await ctx.klavisClient.mcpServer.createServerInstance({
|
||||
serverName: serverName as any,
|
||||
userId,
|
||||
});
|
||||
|
||||
const { serverUrl, instanceId, oauthUrl } = response;
|
||||
|
||||
// 获取该服务器的工具列表
|
||||
const toolsResponse = await ctx.klavisClient.mcpServer.getTools(serverName as any);
|
||||
const tools = toolsResponse.tools || [];
|
||||
|
||||
// 保存到数据库,使用传入的 identifier(格式:小写,空格替换为连字符)
|
||||
const manifest: LobeChatPluginManifest = {
|
||||
api: tools.map((tool: any) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || { properties: {}, type: 'object' },
|
||||
})),
|
||||
identifier,
|
||||
meta: {
|
||||
avatar: '🔌',
|
||||
description: `Klavis MCP Server: ${serverName}`,
|
||||
title: serverName,
|
||||
},
|
||||
type: 'default',
|
||||
};
|
||||
|
||||
// 保存到数据库,包含 oauthUrl 和 isAuthenticated 状态
|
||||
const isAuthenticated = !oauthUrl; // 如果没有 oauthUrl,说明不需要认证或已认证
|
||||
await ctx.pluginModel.create({
|
||||
customParams: {
|
||||
klavis: {
|
||||
instanceId,
|
||||
isAuthenticated,
|
||||
oauthUrl,
|
||||
serverName,
|
||||
serverUrl,
|
||||
},
|
||||
},
|
||||
identifier,
|
||||
manifest,
|
||||
source: 'klavis',
|
||||
type: 'plugin',
|
||||
});
|
||||
|
||||
return {
|
||||
identifier,
|
||||
instanceId,
|
||||
isAuthenticated,
|
||||
oauthUrl,
|
||||
serverName,
|
||||
serverUrl,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* Delete a server instance
|
||||
*/
|
||||
deleteServerInstance: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
/** Identifier for storage (e.g., 'google-calendar') */
|
||||
identifier: z.string(),
|
||||
instanceId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// 调用 Klavis API 删除服务器实例
|
||||
await ctx.klavisClient.mcpServer.deleteServerInstance(input.instanceId);
|
||||
|
||||
// 从数据库删除(使用 identifier)
|
||||
await ctx.pluginModel.delete(input.identifier);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get Klavis plugins from database
|
||||
*/
|
||||
getKlavisPlugins: klavisProcedure.query(async ({ ctx }) => {
|
||||
const allPlugins = await ctx.pluginModel.query();
|
||||
// Filter plugins that have klavis customParams
|
||||
return allPlugins.filter((plugin) => plugin.customParams?.klavis);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get server instance status from Klavis API
|
||||
*/
|
||||
getServerInstance: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
instanceId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const response = await ctx.klavisClient.mcpServer.getServerInstance(input.instanceId);
|
||||
return {
|
||||
authNeeded: response.authNeeded,
|
||||
externalUserId: response.externalUserId,
|
||||
instanceId: response.instanceId,
|
||||
isAuthenticated: response.isAuthenticated,
|
||||
oauthUrl: response.oauthUrl,
|
||||
platform: response.platform,
|
||||
serverName: response.serverName,
|
||||
};
|
||||
}),
|
||||
|
||||
getUserIntergrations: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const response = await ctx.klavisClient.user.getUserIntegrations(input.userId);
|
||||
|
||||
return {
|
||||
integrations: response.integrations,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* Remove Klavis plugin from database by identifier
|
||||
*/
|
||||
removeKlavisPlugin: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
/** Identifier for storage (e.g., 'google-calendar') */
|
||||
identifier: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await ctx.pluginModel.delete(input.identifier);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update Klavis plugin with tools and auth status in database
|
||||
*/
|
||||
updateKlavisPlugin: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
/** Identifier for storage (e.g., 'google-calendar') */
|
||||
identifier: z.string(),
|
||||
instanceId: z.string(),
|
||||
isAuthenticated: z.boolean(),
|
||||
oauthUrl: z.string().optional(),
|
||||
/** Server name for Klavis API (e.g., 'Google Calendar') */
|
||||
serverName: z.string(),
|
||||
serverUrl: z.string(),
|
||||
tools: z.array(
|
||||
z.object({
|
||||
description: z.string().optional(),
|
||||
inputSchema: z.any().optional(),
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { identifier, serverName, serverUrl, instanceId, tools, isAuthenticated, oauthUrl } =
|
||||
input;
|
||||
|
||||
// 获取现有插件(使用 identifier)
|
||||
const existingPlugin = await ctx.pluginModel.findById(identifier);
|
||||
|
||||
// 构建包含所有工具的 manifest
|
||||
const manifest: LobeChatPluginManifest = {
|
||||
api: tools.map((tool) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || { properties: {}, type: 'object' },
|
||||
})),
|
||||
identifier,
|
||||
meta: existingPlugin?.manifest?.meta || {
|
||||
avatar: '🔌',
|
||||
description: `Klavis MCP Server: ${serverName}`,
|
||||
title: serverName,
|
||||
},
|
||||
type: 'default',
|
||||
};
|
||||
|
||||
const customParams = {
|
||||
klavis: {
|
||||
instanceId,
|
||||
isAuthenticated,
|
||||
oauthUrl,
|
||||
serverName,
|
||||
serverUrl,
|
||||
},
|
||||
};
|
||||
|
||||
// 更新或创建插件
|
||||
if (existingPlugin) {
|
||||
await ctx.pluginModel.update(identifier, { customParams, manifest });
|
||||
} else {
|
||||
await ctx.pluginModel.create({
|
||||
customParams,
|
||||
identifier,
|
||||
manifest,
|
||||
source: 'klavis',
|
||||
type: 'plugin',
|
||||
});
|
||||
}
|
||||
|
||||
return { savedCount: tools.length };
|
||||
}),
|
||||
});
|
||||
|
||||
export type KlavisRouter = typeof klavisRouter;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
||||
|
||||
import { klavisRouter } from './klavis';
|
||||
import { mcpRouter } from './mcp';
|
||||
import { searchRouter } from './search';
|
||||
|
||||
export const toolsRouter = router({
|
||||
healthcheck: publicProcedure.query(() => "i'm live!"),
|
||||
klavis: klavisRouter,
|
||||
mcp: mcpRouter,
|
||||
search: searchRouter,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getKlavisClient } from '@/libs/klavis';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { MCPService } from '@/server/services/mcp';
|
||||
|
||||
/**
|
||||
* Klavis procedure with client initialized in context
|
||||
*/
|
||||
const klavisProcedure = authedProcedure.use(async (opts) => {
|
||||
const klavisClient = getKlavisClient();
|
||||
|
||||
return opts.next({
|
||||
ctx: { ...opts.ctx, klavisClient },
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Klavis router for tools
|
||||
* Contains callTool and listTools which call external Klavis API
|
||||
*/
|
||||
export const klavisRouter = router({
|
||||
/**
|
||||
* Call a tool on a Klavis Strata server
|
||||
*/
|
||||
callTool: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
serverUrl: z.string(),
|
||||
toolArgs: z.record(z.unknown()).optional(),
|
||||
toolName: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const response = await ctx.klavisClient.mcpServer.callTools({
|
||||
serverUrl: input.serverUrl,
|
||||
toolArgs: input.toolArgs,
|
||||
toolName: input.toolName,
|
||||
});
|
||||
|
||||
// Handle error case
|
||||
if (!response.success || !response.result) {
|
||||
return {
|
||||
content: response.error || 'Unknown error',
|
||||
state: {
|
||||
content: [{ text: response.error || 'Unknown error', type: 'text' }],
|
||||
isError: true,
|
||||
},
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Process the response using the common MCP tool call result processor
|
||||
const processedResult = await MCPService.processToolCallResult({
|
||||
content: (response.result.content || []) as any[],
|
||||
isError: response.result.isError,
|
||||
});
|
||||
|
||||
return processedResult;
|
||||
}),
|
||||
|
||||
/**
|
||||
* List tools available on a Klavis Strata server
|
||||
*/
|
||||
listTools: klavisProcedure
|
||||
.input(
|
||||
z.object({
|
||||
serverUrl: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const response = await ctx.klavisClient.mcpServer.listTools({
|
||||
serverUrl: input.serverUrl,
|
||||
});
|
||||
|
||||
return {
|
||||
tools: response.tools,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -21,12 +21,60 @@ import { mcpSystemDepsCheckService } from './deps';
|
||||
|
||||
const log = debug('lobe-mcp:service');
|
||||
|
||||
/**
|
||||
* MCP Tool call raw result type
|
||||
*/
|
||||
export interface MCPToolCallRawResult {
|
||||
content: any[];
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP Tool call processed result type
|
||||
*/
|
||||
export interface MCPToolCallProcessedResult {
|
||||
content: string;
|
||||
error?: Error;
|
||||
state: {
|
||||
content: any[];
|
||||
isError?: boolean;
|
||||
};
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// Removed MCPConnection interface as it's no longer needed
|
||||
|
||||
export class MCPService {
|
||||
// Store instances of the custom MCPClient, keyed by serialized MCPClientParams
|
||||
private clients: Map<string, MCPClient> = new Map();
|
||||
|
||||
/**
|
||||
* Process MCP tool call result with content blocks processing
|
||||
* This is a common utility method that can be used by both internal MCP calls and external services (e.g., Klavis)
|
||||
*/
|
||||
static async processToolCallResult(
|
||||
result: MCPToolCallRawResult,
|
||||
processContentBlocksFn?: ProcessContentBlocksFn,
|
||||
): Promise<MCPToolCallProcessedResult> {
|
||||
// Process content blocks (upload images, etc.)
|
||||
|
||||
const newContent =
|
||||
result.isError || !processContentBlocksFn
|
||||
? result.content
|
||||
: await processContentBlocksFn(result.content);
|
||||
|
||||
// Convert content blocks to string
|
||||
const content = contentBlocksToString(newContent);
|
||||
|
||||
const state = { ...result, content: newContent };
|
||||
|
||||
if (result.isError) {
|
||||
return { content, state, success: true };
|
||||
}
|
||||
|
||||
return { content, state, success: true };
|
||||
}
|
||||
|
||||
private sanitizeForLogging = <T extends Record<string, any>>(obj: T): Omit<T, 'env'> => {
|
||||
if (!obj) return obj;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -162,7 +210,12 @@ export class MCPService {
|
||||
processContentBlocks?: ProcessContentBlocksFn;
|
||||
toolName: string;
|
||||
}): Promise<any> {
|
||||
const { clientParams, toolName, argsStr, processContentBlocks } = options;
|
||||
const {
|
||||
clientParams,
|
||||
toolName,
|
||||
argsStr,
|
||||
processContentBlocks: processContentBlocksFn,
|
||||
} = options;
|
||||
|
||||
const client = await this.getClient(clientParams); // Get client using params
|
||||
|
||||
@@ -179,26 +232,19 @@ export class MCPService {
|
||||
// Delegate the call to the MCPClient instance
|
||||
const result = await client.callTool(toolName, args); // Pass args directly
|
||||
|
||||
// Process content blocks (upload images, etc.)
|
||||
const newContent =
|
||||
result.isError || !processContentBlocks
|
||||
? result.content
|
||||
: await processContentBlocks(result.content);
|
||||
|
||||
// Convert content blocks to string
|
||||
const content = contentBlocksToString(newContent);
|
||||
|
||||
const state = { ...result, content: newContent };
|
||||
// Use the common processing method
|
||||
const processedResult = await MCPService.processToolCallResult(
|
||||
result,
|
||||
processContentBlocksFn,
|
||||
);
|
||||
|
||||
log(
|
||||
`Tool "${toolName}" called successfully for params: %O, result: %O`,
|
||||
loggableParams,
|
||||
state,
|
||||
processedResult.state,
|
||||
);
|
||||
|
||||
if (result.isError) return { content, state, success: true };
|
||||
|
||||
return { content, state, success: true };
|
||||
return processedResult;
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
const mcpError = error as McpError;
|
||||
|
||||
@@ -780,4 +780,4 @@ describe('MCPService', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1041,9 +1041,8 @@ export const streamingExecutor: StateCreator<
|
||||
|
||||
// Only show notification if there's content and no tools
|
||||
if (lastAssistant?.content && !lastAssistant?.tools) {
|
||||
const { desktopNotificationService } = await import(
|
||||
'@/services/electron/desktopNotification'
|
||||
);
|
||||
const { desktopNotificationService } =
|
||||
await import('@/services/electron/desktopNotification');
|
||||
|
||||
await desktopNotificationService.showNotification({
|
||||
body: lastAssistant.content,
|
||||
|
||||
@@ -775,7 +775,6 @@ describe('ChatPluginAction', () => {
|
||||
});
|
||||
|
||||
it('should handle MD5 hashed API names', () => {
|
||||
const apiName = 'testApi';
|
||||
const resolver = new ToolNameResolver();
|
||||
// Generate a very long name to force MD5 hashing
|
||||
const longApiName =
|
||||
|
||||
@@ -6,7 +6,7 @@ import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { ChatStore } from '@/store/chat/store';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
import { klavisStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import { builtinTools } from '@/tools';
|
||||
|
||||
import { displayMessageSelectors } from '../../message/selectors';
|
||||
@@ -40,11 +40,16 @@ export const pluginInternals: StateCreator<
|
||||
const toolStoreState = useToolStore.getState();
|
||||
const manifests: Record<string, LobeChatPluginManifest> = {};
|
||||
|
||||
// Track source for each identifier
|
||||
const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis'> = {};
|
||||
|
||||
// Get all installed plugins
|
||||
const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
|
||||
for (const plugin of installedPlugins) {
|
||||
if (plugin.manifest) {
|
||||
manifests[plugin.identifier] = plugin.manifest as LobeChatPluginManifest;
|
||||
// Check if this plugin has MCP params
|
||||
sourceMap[plugin.identifier] = plugin.customParams?.mcp ? 'mcp' : 'plugin';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +57,25 @@ export const pluginInternals: StateCreator<
|
||||
for (const tool of builtinTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
sourceMap[tool.identifier] = 'builtin';
|
||||
}
|
||||
}
|
||||
|
||||
return toolNameResolver.resolve(toolCalls, manifests);
|
||||
// Get all Klavis tools
|
||||
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
||||
for (const tool of klavisTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
sourceMap[tool.identifier] = 'klavis';
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve tool calls and add source field
|
||||
const resolved = toolNameResolver.resolve(toolCalls, manifests);
|
||||
return resolved.map((payload) => ({
|
||||
...payload,
|
||||
source: sourceMap[payload.identifier],
|
||||
}));
|
||||
},
|
||||
|
||||
internal_constructToolsCallingContext: (id: string) => {
|
||||
|
||||
@@ -32,6 +32,11 @@ export interface PluginTypesAction {
|
||||
*/
|
||||
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Invoke Klavis type plugin
|
||||
*/
|
||||
invokeKlavisTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Invoke markdown type plugin
|
||||
*/
|
||||
@@ -60,6 +65,11 @@ export const pluginTypes: StateCreator<
|
||||
PluginTypesAction
|
||||
> = (set, get) => ({
|
||||
invokeBuiltinTool: async (id, payload) => {
|
||||
// Check if this is a Klavis tool by source field
|
||||
if (payload.source === 'klavis') {
|
||||
return await get().invokeKlavisTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
// run tool api call
|
||||
// @ts-ignore
|
||||
const { [payload.apiName]: action } = get();
|
||||
@@ -82,6 +92,104 @@ export const pluginTypes: StateCreator<
|
||||
return data;
|
||||
},
|
||||
|
||||
invokeKlavisTypePlugin: async (id, payload) => {
|
||||
const {
|
||||
optimisticUpdateMessageContent,
|
||||
optimisticUpdatePluginState,
|
||||
optimisticUpdateMessagePluginError,
|
||||
} = get();
|
||||
|
||||
let data: MCPToolCallResult | undefined;
|
||||
|
||||
// Get message to extract sessionId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
||||
|
||||
// Get abort controller from operation
|
||||
const operationId = get().messageOperationMap[id];
|
||||
const operation = operationId ? get().operations[operationId] : undefined;
|
||||
const abortController = operation?.abortController;
|
||||
|
||||
log(
|
||||
'[invokeKlavisTypePlugin] messageId=%s, tool=%s, operationId=%s, aborted=%s',
|
||||
id,
|
||||
payload.apiName,
|
||||
operationId,
|
||||
abortController?.signal.aborted,
|
||||
);
|
||||
|
||||
try {
|
||||
// payload.identifier 现在是存储用的 identifier(如 'google-calendar')
|
||||
const identifier = payload.identifier;
|
||||
const klavisServers = useToolStore.getState().servers || [];
|
||||
const server = klavisServers.find((s) => s.identifier === identifier);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Klavis server not found: ${identifier}`);
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
const args = safeParseJSON(payload.arguments) || {};
|
||||
|
||||
// Call Klavis tool via store action
|
||||
const result = await useToolStore.getState().callKlavisTool({
|
||||
serverUrl: server.serverUrl,
|
||||
toolArgs: args,
|
||||
toolName: payload.apiName,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Klavis tool execution failed');
|
||||
}
|
||||
|
||||
// result.data is MCPToolCallProcessedResult from server
|
||||
// Convert to MCPToolCallResult format
|
||||
const toolResult = result.data;
|
||||
if (toolResult) {
|
||||
data = {
|
||||
content: toolResult.content,
|
||||
error: toolResult.state?.isError ? toolResult.state : undefined,
|
||||
state: toolResult.state,
|
||||
success: toolResult.success,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[invokeKlavisTypePlugin] Error:', error);
|
||||
|
||||
// ignore the aborted request error
|
||||
const err = error as Error;
|
||||
if (err.message.includes('aborted')) {
|
||||
log('[invokeKlavisTypePlugin] Request aborted: messageId=%s, tool=%s', id, payload.apiName);
|
||||
} else {
|
||||
const result = await messageService.updateMessageError(id, error as any, {
|
||||
sessionId: message?.sessionId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
if (result?.success && result.messages) {
|
||||
get().replaceMessages(result.messages, {
|
||||
sessionId: message?.sessionId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果报错则结束了
|
||||
if (!data) return;
|
||||
|
||||
// operationId already declared above, reuse it
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
await Promise.all([
|
||||
optimisticUpdateMessageContent(id, data.content, undefined, context),
|
||||
(async () => {
|
||||
if (data.success) await optimisticUpdatePluginState(id, data.state, context);
|
||||
else await optimisticUpdateMessagePluginError(id, data.error, context);
|
||||
})(),
|
||||
]);
|
||||
|
||||
return data.content;
|
||||
},
|
||||
|
||||
invokeMarkdownTypePlugin: async (id, payload) => {
|
||||
const { internal_callPluginApi } = get();
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { featureFlagsSelectors } from './selectors';
|
||||
export { featureFlagsSelectors, serverConfigSelectors } from './selectors';
|
||||
export { useServerConfigStore } from './store';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ServerConfigStore } from './store';
|
||||
export const featureFlagsSelectors = (s: ServerConfigStore) => s.featureFlags;
|
||||
|
||||
export const serverConfigSelectors = {
|
||||
enableKlavis: (s: ServerConfigStore) => s.serverConfig.enableKlavis || false,
|
||||
enableUploadFileToServer: (s: ServerConfigStore) => s.serverConfig.enableUploadFileToServer,
|
||||
enabledAccessCode: (s: ServerConfigStore) => !!s.serverConfig?.enabledAccessCode,
|
||||
enabledOAuthSSO: (s: ServerConfigStore) => s.serverConfig.enabledOAuthSSO,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BuiltinToolState, initialBuiltinToolState } from './slices/builtin';
|
||||
import { CustomPluginState, initialCustomPluginState } from './slices/customPlugin';
|
||||
import { KlavisStoreState, initialKlavisStoreState } from './slices/klavisStore';
|
||||
import { MCPStoreState, initialMCPStoreState } from './slices/mcpStore';
|
||||
import { PluginState, initialPluginState } from './slices/plugin';
|
||||
import { PluginStoreState, initialPluginStoreState } from './slices/oldStore';
|
||||
@@ -8,7 +9,8 @@ export type ToolStoreState = PluginState &
|
||||
CustomPluginState &
|
||||
PluginStoreState &
|
||||
BuiltinToolState &
|
||||
MCPStoreState;
|
||||
MCPStoreState &
|
||||
KlavisStoreState;
|
||||
|
||||
export const initialState: ToolStoreState = {
|
||||
...initialPluginState,
|
||||
@@ -16,4 +18,5 @@ export const initialState: ToolStoreState = {
|
||||
...initialPluginStoreState,
|
||||
...initialBuiltinToolState,
|
||||
...initialMCPStoreState,
|
||||
...initialKlavisStoreState,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { builtinToolSelectors } from '../slices/builtin/selectors';
|
||||
export { customPluginSelectors } from '../slices/customPlugin/selectors';
|
||||
export { klavisStoreSelectors } from '../slices/klavisStore/selectors';
|
||||
export { mcpStoreSelectors } from '../slices/mcpStore/selectors';
|
||||
export { pluginStoreSelectors } from '../slices/oldStore/selectors';
|
||||
export { pluginSelectors } from '../slices/plugin/selectors';
|
||||
|
||||
@@ -3,9 +3,11 @@ import { LobeToolMeta } from '@lobechat/types';
|
||||
import { shouldEnableTool } from '@/helpers/toolFilters';
|
||||
|
||||
import type { ToolStoreState } from '../../initialState';
|
||||
import { KlavisServerStatus } from '../klavisStore';
|
||||
|
||||
const metaList = (s: ToolStoreState): LobeToolMeta[] =>
|
||||
s.builtinTools
|
||||
const metaList = (s: ToolStoreState): LobeToolMeta[] => {
|
||||
// Get builtin tools meta list
|
||||
const builtinMetas = s.builtinTools
|
||||
.filter((item) => {
|
||||
// Filter hidden tools
|
||||
if (item.hidden) return false;
|
||||
@@ -19,9 +21,29 @@ const metaList = (s: ToolStoreState): LobeToolMeta[] =>
|
||||
author: 'LobeHub',
|
||||
identifier: t.identifier,
|
||||
meta: t.manifest.meta,
|
||||
type: 'builtin',
|
||||
type: 'builtin' as const,
|
||||
}));
|
||||
|
||||
// Get Klavis servers as builtin tools meta
|
||||
const klavisMetas = (s.servers || [])
|
||||
.filter((server) => server.status === KlavisServerStatus.CONNECTED && server.tools?.length)
|
||||
.map((server) => ({
|
||||
author: 'Klavis',
|
||||
// 使用 identifier 作为存储标识符(如 'google-calendar')
|
||||
identifier: server.identifier,
|
||||
meta: {
|
||||
avatar: '☁️',
|
||||
description: `Klavis MCP Server: ${server.serverName}`,
|
||||
tags: ['klavis', 'mcp'],
|
||||
// title 仍然使用 serverName 显示友好名称
|
||||
title: server.serverName,
|
||||
},
|
||||
type: 'builtin' as const,
|
||||
}));
|
||||
|
||||
return [...builtinMetas, ...klavisMetas];
|
||||
};
|
||||
|
||||
export const builtinToolSelectors = {
|
||||
metaList,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { lambdaClient, toolsClient } from '@/libs/trpc/client';
|
||||
|
||||
import { useToolStore } from '../../store';
|
||||
import { KlavisServerStatus } from './types';
|
||||
|
||||
vi.mock('zustand/traditional');
|
||||
|
||||
vi.mock('@/libs/trpc/client', () => ({
|
||||
lambdaClient: {
|
||||
klavis: {
|
||||
createServerInstance: { mutate: vi.fn() },
|
||||
deleteServerInstance: { mutate: vi.fn() },
|
||||
getKlavisPlugins: { query: vi.fn() },
|
||||
getServerInstance: { query: vi.fn() },
|
||||
updateKlavisPlugin: { mutate: vi.fn() },
|
||||
},
|
||||
},
|
||||
toolsClient: {
|
||||
klavis: {
|
||||
callTool: { mutate: vi.fn() },
|
||||
listTools: { query: vi.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('klavisStore actions', () => {
|
||||
describe('callKlavisTool', () => {
|
||||
it('should call tool successfully and return result', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
content: 'Tool result',
|
||||
success: true,
|
||||
state: { content: [], isError: false },
|
||||
};
|
||||
vi.mocked(toolsClient.klavis.callTool.mutate).mockResolvedValue(mockResponse as any);
|
||||
|
||||
let callResult;
|
||||
await act(async () => {
|
||||
callResult = await result.current.callKlavisTool({
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
toolName: 'sendEmail',
|
||||
toolArgs: { to: 'test@example.com' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(callResult).toEqual({ data: mockResponse, success: true });
|
||||
expect(toolsClient.klavis.callTool.mutate).toHaveBeenCalledWith({
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
toolName: 'sendEmail',
|
||||
toolArgs: { to: 'test@example.com' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle error and return error result', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(toolsClient.klavis.callTool.mutate).mockRejectedValue(
|
||||
new Error('Tool call failed'),
|
||||
);
|
||||
|
||||
let callResult;
|
||||
await act(async () => {
|
||||
callResult = await result.current.callKlavisTool({
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
toolName: 'sendEmail',
|
||||
});
|
||||
});
|
||||
|
||||
expect(callResult).toEqual({ error: 'Tool call failed', success: false });
|
||||
expect(result.current.executingToolIds.has('https://klavis.ai/gmail:sendEmail')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createKlavisServer', () => {
|
||||
it('should create server successfully', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
identifier: 'gmail',
|
||||
instanceId: 'inst-123',
|
||||
isAuthenticated: true,
|
||||
oauthUrl: undefined,
|
||||
serverName: 'Gmail',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
};
|
||||
vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockResolvedValue(mockResponse);
|
||||
|
||||
let server;
|
||||
await act(async () => {
|
||||
server = await result.current.createKlavisServer({
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
userId: 'user-123',
|
||||
});
|
||||
});
|
||||
|
||||
expect(server).toMatchObject({
|
||||
identifier: 'gmail',
|
||||
instanceId: 'inst-123',
|
||||
isAuthenticated: true,
|
||||
serverName: 'Gmail',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
});
|
||||
expect(result.current.servers).toHaveLength(1);
|
||||
expect(result.current.loadingServerIds.has('gmail')).toBe(false);
|
||||
});
|
||||
|
||||
it('should create server with pending auth when oauth needed', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
identifier: 'github',
|
||||
instanceId: 'inst-123',
|
||||
isAuthenticated: false,
|
||||
oauthUrl: 'https://oauth.klavis.ai/github',
|
||||
serverName: 'GitHub',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
};
|
||||
vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockResolvedValue(mockResponse);
|
||||
|
||||
let server;
|
||||
await act(async () => {
|
||||
server = await result.current.createKlavisServer({
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
userId: 'user-123',
|
||||
});
|
||||
});
|
||||
|
||||
expect(server).toMatchObject({
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
oauthUrl: 'https://oauth.klavis.ai/github',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update existing server if already exists', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'old-inst',
|
||||
serverUrl: 'https://old.klavis.ai/gmail',
|
||||
status: KlavisServerStatus.ERROR,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
identifier: 'gmail',
|
||||
instanceId: 'new-inst',
|
||||
isAuthenticated: true,
|
||||
oauthUrl: undefined,
|
||||
serverName: 'Gmail',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
};
|
||||
vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockResolvedValue(mockResponse);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createKlavisServer({
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
userId: 'user-123',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.servers).toHaveLength(1);
|
||||
expect(result.current.servers[0].instanceId).toBe('new-inst');
|
||||
});
|
||||
|
||||
it('should handle creation error', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.createServerInstance.mutate).mockRejectedValue(
|
||||
new Error('Creation failed'),
|
||||
);
|
||||
|
||||
let server;
|
||||
await act(async () => {
|
||||
server = await result.current.createKlavisServer({
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
userId: 'user-123',
|
||||
});
|
||||
});
|
||||
|
||||
expect(server).toBeUndefined();
|
||||
expect(result.current.servers).toHaveLength(0);
|
||||
expect(result.current.loadingServerIds.has('gmail')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Note: useFetchUserKlavisServers uses SWR hook and requires different testing approach
|
||||
// The SWR hook tests should be done in integration tests or with SWR testing utilities
|
||||
|
||||
describe('refreshKlavisServerTools', () => {
|
||||
it('should refresh tools for authenticated server', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
|
||||
isAuthenticated: true,
|
||||
authNeeded: false,
|
||||
} as any);
|
||||
|
||||
vi.mocked(toolsClient.klavis.listTools.query).mockResolvedValue({
|
||||
tools: [{ name: 'sendEmail', description: 'Send email', inputSchema: { type: 'object' } }],
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.updateKlavisPlugin.mutate).mockResolvedValue({} as any);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshKlavisServerTools('gmail');
|
||||
});
|
||||
|
||||
expect(result.current.servers[0].status).toBe(KlavisServerStatus.CONNECTED);
|
||||
expect(result.current.servers[0].isAuthenticated).toBe(true);
|
||||
expect(result.current.servers[0].tools).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should remove server when auth failed and auth needed', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
|
||||
isAuthenticated: false,
|
||||
authNeeded: true,
|
||||
} as any);
|
||||
|
||||
vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockResolvedValue({} as any);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshKlavisServerTools('gmail');
|
||||
});
|
||||
|
||||
expect(result.current.servers).toHaveLength(0);
|
||||
expect(lambdaClient.klavis.deleteServerInstance.mutate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do nothing when server not found', async () => {
|
||||
vi.mocked(lambdaClient.klavis.getServerInstance.query).mockClear();
|
||||
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshKlavisServerTools('non-existent');
|
||||
});
|
||||
|
||||
expect(lambdaClient.klavis.getServerInstance.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle refresh error', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
|
||||
isAuthenticated: true,
|
||||
authNeeded: false,
|
||||
} as any);
|
||||
|
||||
vi.mocked(toolsClient.klavis.listTools.query).mockRejectedValue(new Error('Refresh failed'));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshKlavisServerTools('gmail');
|
||||
});
|
||||
|
||||
expect(result.current.servers[0].status).toBe(KlavisServerStatus.ERROR);
|
||||
expect(result.current.servers[0].errorMessage).toBe('Refresh failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeKlavisServer', () => {
|
||||
it('should remove server from state and call API', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockResolvedValue({} as any);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.removeKlavisServer('gmail');
|
||||
});
|
||||
|
||||
expect(result.current.servers).toHaveLength(0);
|
||||
expect(lambdaClient.klavis.deleteServerInstance.mutate).toHaveBeenCalledWith({
|
||||
identifier: 'gmail',
|
||||
instanceId: 'inst-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle remove when server not found', async () => {
|
||||
vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockClear();
|
||||
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.removeKlavisServer('non-existent');
|
||||
});
|
||||
|
||||
expect(lambdaClient.klavis.deleteServerInstance.mutate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle API error gracefully', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.deleteServerInstance.mutate).mockRejectedValue(
|
||||
new Error('Delete failed'),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.removeKlavisServer('gmail');
|
||||
});
|
||||
|
||||
expect(result.current.servers).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeKlavisServerAuth', () => {
|
||||
it('should call refreshKlavisServerTools', async () => {
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
servers: [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
loadingServerIds: new Set(),
|
||||
executingToolIds: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.getServerInstance.query).mockResolvedValue({
|
||||
isAuthenticated: true,
|
||||
authNeeded: false,
|
||||
} as any);
|
||||
|
||||
vi.mocked(toolsClient.klavis.listTools.query).mockResolvedValue({
|
||||
tools: [],
|
||||
});
|
||||
|
||||
vi.mocked(lambdaClient.klavis.updateKlavisPlugin.mutate).mockResolvedValue({} as any);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.completeKlavisServerAuth('gmail');
|
||||
});
|
||||
|
||||
expect(lambdaClient.klavis.getServerInstance.query).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
import { enableMapSet, produce } from 'immer';
|
||||
import useSWR, { SWRResponse } from 'swr';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { lambdaClient, toolsClient } from '@/libs/trpc/client';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { ToolStore } from '../../store';
|
||||
import { KlavisStoreState } from './initialState';
|
||||
import {
|
||||
CallKlavisToolParams,
|
||||
CallKlavisToolResult,
|
||||
CreateKlavisServerParams,
|
||||
KlavisServer,
|
||||
KlavisServerStatus,
|
||||
KlavisTool,
|
||||
} from './types';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
const n = setNamespace('klavisStore');
|
||||
|
||||
/**
|
||||
* Klavis Store Actions
|
||||
*/
|
||||
export interface KlavisStoreAction {
|
||||
/**
|
||||
* 调用 Klavis 工具
|
||||
*/
|
||||
callKlavisTool: (params: CallKlavisToolParams) => Promise<CallKlavisToolResult>;
|
||||
|
||||
/**
|
||||
* 完成 OAuth 认证后,更新服务器状态
|
||||
* @param identifier - 服务器标识符 (e.g., 'google-calendar')
|
||||
*/
|
||||
completeKlavisServerAuth: (identifier: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 创建单个 Klavis MCP Server 实例
|
||||
* @returns 创建的服务器实例,如果需要 OAuth 则返回带 oauthUrl 的对象
|
||||
*/
|
||||
createKlavisServer: (params: CreateKlavisServerParams) => Promise<KlavisServer | undefined>;
|
||||
|
||||
/**
|
||||
* 刷新 Klavis Server 的工具列表
|
||||
* @param identifier - 服务器标识符 (e.g., 'google-calendar')
|
||||
*/
|
||||
refreshKlavisServerTools: (identifier: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 删除 Klavis Server
|
||||
* @param identifier - 服务器标识符 (e.g., 'google-calendar')
|
||||
*/
|
||||
removeKlavisServer: (identifier: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 使用 SWR 获取用户的 Klavis 服务器列表
|
||||
* @param enabled - 是否启用获取
|
||||
*/
|
||||
useFetchUserKlavisServers: (enabled: boolean) => SWRResponse<KlavisServer[]>;
|
||||
}
|
||||
|
||||
export const createKlavisStoreSlice: StateCreator<
|
||||
ToolStore,
|
||||
[['zustand/devtools', never]],
|
||||
[],
|
||||
KlavisStoreAction
|
||||
> = (set, get) => ({
|
||||
callKlavisTool: async (params) => {
|
||||
const { serverUrl, toolName, toolArgs } = params;
|
||||
|
||||
const toolId = `${serverUrl}:${toolName}`;
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.executingToolIds.add(toolId);
|
||||
}),
|
||||
false,
|
||||
n('callKlavisTool/start'),
|
||||
);
|
||||
|
||||
try {
|
||||
// 调用 tRPC 服务端接口执行工具(使用 toolsClient 以获得更长的超时时间)
|
||||
const response = await toolsClient.klavis.callTool.mutate({
|
||||
serverUrl,
|
||||
toolArgs,
|
||||
toolName,
|
||||
});
|
||||
|
||||
console.log('toolsClient.klavis.callTool-response', response);
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.executingToolIds.delete(toolId);
|
||||
}),
|
||||
false,
|
||||
n('callKlavisTool/success'),
|
||||
);
|
||||
|
||||
return { data: response, success: true };
|
||||
} catch (error) {
|
||||
console.error('[Klavis] Failed to call tool:', error);
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.executingToolIds.delete(toolId);
|
||||
}),
|
||||
false,
|
||||
n('callKlavisTool/error'),
|
||||
);
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
completeKlavisServerAuth: async (identifier) => {
|
||||
// OAuth 完成后,刷新工具列表
|
||||
await get().refreshKlavisServerTools(identifier);
|
||||
},
|
||||
|
||||
createKlavisServer: async (params) => {
|
||||
const { userId, serverName, identifier } = params;
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.loadingServerIds.add(identifier);
|
||||
}),
|
||||
false,
|
||||
n('createKlavisServer/start'),
|
||||
);
|
||||
|
||||
try {
|
||||
// 调用 tRPC 服务端接口创建单个服务器实例
|
||||
const response = await lambdaClient.klavis.createServerInstance.mutate({
|
||||
identifier,
|
||||
serverName,
|
||||
userId,
|
||||
});
|
||||
|
||||
// 构建服务器对象
|
||||
const server: KlavisServer = {
|
||||
createdAt: Date.now(),
|
||||
identifier: response.identifier,
|
||||
instanceId: response.instanceId,
|
||||
isAuthenticated: response.isAuthenticated,
|
||||
oauthUrl: response.oauthUrl,
|
||||
serverName: response.serverName,
|
||||
serverUrl: response.serverUrl,
|
||||
status: response.isAuthenticated
|
||||
? KlavisServerStatus.CONNECTED
|
||||
: KlavisServerStatus.PENDING_AUTH,
|
||||
};
|
||||
|
||||
// 添加到 servers 列表
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
// 检查是否已存在(使用 identifier),如果存在则更新
|
||||
const existingIndex = draft.servers.findIndex((s) => s.identifier === identifier);
|
||||
if (existingIndex >= 0) {
|
||||
draft.servers[existingIndex] = server;
|
||||
} else {
|
||||
draft.servers.push(server);
|
||||
}
|
||||
draft.loadingServerIds.delete(identifier);
|
||||
}),
|
||||
false,
|
||||
n('createKlavisServer/success'),
|
||||
);
|
||||
|
||||
return server;
|
||||
} catch (error) {
|
||||
console.error('[Klavis] Failed to create server:', error);
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.loadingServerIds.delete(identifier);
|
||||
}),
|
||||
false,
|
||||
n('createKlavisServer/error'),
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
refreshKlavisServerTools: async (identifier) => {
|
||||
const { servers } = get();
|
||||
|
||||
// 使用 identifier 查找服务器
|
||||
const server = servers.find((s) => s.identifier === identifier);
|
||||
if (!server) {
|
||||
console.error('[Klavis] Server not found:', identifier);
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.loadingServerIds.add(identifier);
|
||||
}),
|
||||
false,
|
||||
n('refreshKlavisServerTools/start'),
|
||||
);
|
||||
|
||||
try {
|
||||
// 首先检查服务器的认证状态
|
||||
const instanceStatus = await lambdaClient.klavis.getServerInstance.query({
|
||||
instanceId: server.instanceId,
|
||||
});
|
||||
|
||||
// 如果认证失败,删除服务器并重置状态
|
||||
if (!instanceStatus.isAuthenticated) {
|
||||
if (!instanceStatus.authNeeded) {
|
||||
// 如果不需要认证,说明没问题
|
||||
return;
|
||||
}
|
||||
|
||||
// 从本地状态移除(使用 identifier)
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
draft.servers = draft.servers.filter((s) => s.identifier !== identifier);
|
||||
draft.loadingServerIds.delete(identifier);
|
||||
}),
|
||||
false,
|
||||
n('refreshKlavisServerTools/authFailed'),
|
||||
);
|
||||
|
||||
// 从数据库删除
|
||||
await lambdaClient.klavis.deleteServerInstance.mutate({
|
||||
identifier,
|
||||
instanceId: server.instanceId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 认证成功,获取工具列表(使用 toolsClient 以获得更长的超时时间)
|
||||
const response = await toolsClient.klavis.listTools.query({
|
||||
serverUrl: server.serverUrl,
|
||||
});
|
||||
|
||||
const tools = response.tools as KlavisTool[];
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
// 使用 identifier 查找服务器
|
||||
const serverIndex = draft.servers.findIndex((s) => s.identifier === identifier);
|
||||
if (serverIndex >= 0) {
|
||||
draft.servers[serverIndex].tools = tools;
|
||||
draft.servers[serverIndex].status = KlavisServerStatus.CONNECTED;
|
||||
draft.servers[serverIndex].isAuthenticated = true;
|
||||
draft.servers[serverIndex].errorMessage = undefined;
|
||||
}
|
||||
draft.loadingServerIds.delete(identifier);
|
||||
}),
|
||||
false,
|
||||
n('refreshKlavisServerTools/success'),
|
||||
);
|
||||
|
||||
// 更新数据库中的工具列表和认证状态
|
||||
await lambdaClient.klavis.updateKlavisPlugin.mutate({
|
||||
identifier,
|
||||
instanceId: server.instanceId,
|
||||
isAuthenticated: true,
|
||||
serverName: server.serverName,
|
||||
serverUrl: server.serverUrl,
|
||||
tools: tools.map((t) => ({
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
name: t.name,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Klavis] Failed to refresh tools:', error);
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
// 使用 identifier 查找服务器
|
||||
const serverIndex = draft.servers.findIndex((s) => s.identifier === identifier);
|
||||
if (serverIndex >= 0) {
|
||||
draft.servers[serverIndex].status = KlavisServerStatus.ERROR;
|
||||
draft.servers[serverIndex].errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
draft.loadingServerIds.delete(identifier);
|
||||
}),
|
||||
false,
|
||||
n('refreshKlavisServerTools/error'),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
removeKlavisServer: async (identifier) => {
|
||||
const { servers } = get();
|
||||
// 使用 identifier 查找服务器
|
||||
const server = servers.find((s) => s.identifier === identifier);
|
||||
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
// 使用 identifier 过滤
|
||||
draft.servers = draft.servers.filter((s) => s.identifier !== identifier);
|
||||
}),
|
||||
false,
|
||||
n('removeKlavisServer'),
|
||||
);
|
||||
|
||||
// 从 Klavis API 和数据库删除
|
||||
if (server) {
|
||||
try {
|
||||
await lambdaClient.klavis.deleteServerInstance.mutate({
|
||||
identifier,
|
||||
instanceId: server.instanceId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Klavis] Failed to delete server instance:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
useFetchUserKlavisServers: (enabled) =>
|
||||
useSWR<KlavisServer[]>(
|
||||
enabled ? 'fetchUserKlavisServers' : null,
|
||||
async () => {
|
||||
const klavisPlugins = await lambdaClient.klavis.getKlavisPlugins.query();
|
||||
|
||||
if (klavisPlugins.length === 0) return [];
|
||||
|
||||
// 转换为 KlavisServer 对象
|
||||
return klavisPlugins
|
||||
.filter((plugin) => plugin.customParams?.klavis)
|
||||
.map((plugin) => {
|
||||
const klavisParams = plugin.customParams!.klavis!;
|
||||
const tools: KlavisTool[] = (plugin.manifest?.api || []).map((api) => ({
|
||||
description: api.description,
|
||||
inputSchema: api.parameters as KlavisTool['inputSchema'],
|
||||
name: api.name,
|
||||
}));
|
||||
|
||||
return {
|
||||
createdAt: Date.now(),
|
||||
identifier: plugin.identifier,
|
||||
instanceId: klavisParams.instanceId,
|
||||
isAuthenticated: klavisParams.isAuthenticated,
|
||||
oauthUrl: klavisParams.oauthUrl,
|
||||
serverName: klavisParams.serverName,
|
||||
serverUrl: klavisParams.serverUrl,
|
||||
status: klavisParams.isAuthenticated
|
||||
? KlavisServerStatus.CONNECTED
|
||||
: KlavisServerStatus.PENDING_AUTH,
|
||||
tools,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
fallbackData: [],
|
||||
onSuccess: (data) => {
|
||||
if (data.length > 0) {
|
||||
set(
|
||||
produce((draft: KlavisStoreState) => {
|
||||
// 使用 identifier 检查是否已存在
|
||||
const existingIdentifiers = new Set(draft.servers.map((s) => s.identifier));
|
||||
const newServers = data.filter((s) => !existingIdentifiers.has(s.identifier));
|
||||
draft.servers = [...draft.servers, ...newServers];
|
||||
}),
|
||||
false,
|
||||
n('useFetchUserKlavisServers'),
|
||||
);
|
||||
}
|
||||
},
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './action';
|
||||
export * from './initialState';
|
||||
export * from './selectors';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { KlavisServer } from './types';
|
||||
|
||||
/**
|
||||
* Klavis Store 状态接口
|
||||
*
|
||||
* NOTE: API Key is NOT stored in client-side state for security reasons.
|
||||
* It's only available on the server-side.
|
||||
*/
|
||||
export interface KlavisStoreState {
|
||||
/** 已创建的 Klavis Server 列表 */
|
||||
servers: KlavisServer[];
|
||||
/** 正在加载的服务器 ID 集合 */
|
||||
loadingServerIds: Set<string>;
|
||||
/** 正在执行的工具调用 ID 集合 */
|
||||
executingToolIds: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Klavis Store 初始状态
|
||||
*/
|
||||
export const initialKlavisStoreState: KlavisStoreState = {
|
||||
executingToolIds: new Set(),
|
||||
loadingServerIds: new Set(),
|
||||
servers: [],
|
||||
};
|
||||
@@ -0,0 +1,371 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { initialState } from '../../initialState';
|
||||
import { ToolStore } from '../../store';
|
||||
import { klavisStoreSelectors } from './selectors';
|
||||
import { KlavisServerStatus } from './types';
|
||||
|
||||
describe('klavisStoreSelectors', () => {
|
||||
describe('getServers', () => {
|
||||
it('should return empty array when no servers exist', () => {
|
||||
const state = { ...initialState } as ToolStore;
|
||||
const result = klavisStoreSelectors.getServers(state);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all servers', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
instanceId: 'inst-2',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getServers(state);
|
||||
expect(result).toEqual(servers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConnectedServers', () => {
|
||||
it('should return only connected servers', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
instanceId: 'inst-2',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
identifier: 'slack',
|
||||
serverName: 'Slack',
|
||||
instanceId: 'inst-3',
|
||||
serverUrl: 'https://klavis.ai/slack',
|
||||
status: KlavisServerStatus.ERROR,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getConnectedServers(state);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].serverName).toBe('Gmail');
|
||||
});
|
||||
|
||||
it('should return empty array when no servers are connected', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
instanceId: 'inst-2',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getConnectedServers(state);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPendingAuthServers', () => {
|
||||
it('should return only pending auth servers', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
instanceId: 'inst-2',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
oauthUrl: 'https://oauth.klavis.ai/github',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getPendingAuthServers(state);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].serverName).toBe('GitHub');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServerByIdentifier', () => {
|
||||
it('should return server by identifier', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getServerByIdentifier('gmail')(state);
|
||||
expect(result?.identifier).toBe('gmail');
|
||||
expect(result?.serverName).toBe('Gmail');
|
||||
});
|
||||
|
||||
it('should return undefined when server not found', () => {
|
||||
const state = { ...initialState } as ToolStore;
|
||||
const result = klavisStoreSelectors.getServerByIdentifier('non-existent')(state);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllServerIdentifiers', () => {
|
||||
it('should return set of all server identifiers', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
instanceId: 'inst-2',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getAllServerIdentifiers(state);
|
||||
expect(result).toEqual(new Set(['gmail', 'github']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isKlavisServer', () => {
|
||||
it('should return true for existing server by identifier', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.isKlavisServer('gmail')(state);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existing server', () => {
|
||||
const state = { ...initialState } as ToolStore;
|
||||
const result = klavisStoreSelectors.isKlavisServer('non-existent')(state);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isServerLoading', () => {
|
||||
it('should return true when server is loading', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
loadingServerIds: new Set(['gmail']),
|
||||
} as ToolStore;
|
||||
const result = klavisStoreSelectors.isServerLoading('gmail')(state);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when server is not loading', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
loadingServerIds: new Set(),
|
||||
} as ToolStore;
|
||||
const result = klavisStoreSelectors.isServerLoading('gmail')(state);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToolExecuting', () => {
|
||||
it('should return true when tool is executing', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
executingToolIds: new Set(['https://klavis.ai/gmail:sendEmail']),
|
||||
} as ToolStore;
|
||||
const result = klavisStoreSelectors.isToolExecuting(
|
||||
'https://klavis.ai/gmail',
|
||||
'sendEmail',
|
||||
)(state);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when tool is not executing', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
executingToolIds: new Set(),
|
||||
} as ToolStore;
|
||||
const result = klavisStoreSelectors.isToolExecuting(
|
||||
'https://klavis.ai/gmail',
|
||||
'sendEmail',
|
||||
)(state);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllTools', () => {
|
||||
it('should return all tools from connected servers', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
tools: [
|
||||
{ name: 'sendEmail', description: 'Send email', inputSchema: { type: 'object' } },
|
||||
{ name: 'readEmails', description: 'Read emails', inputSchema: { type: 'object' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
identifier: 'github',
|
||||
serverName: 'GitHub',
|
||||
instanceId: 'inst-2',
|
||||
serverUrl: 'https://klavis.ai/github',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
tools: [{ name: 'createPR', description: 'Create PR', inputSchema: { type: 'object' } }],
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getAllTools(state);
|
||||
// Only tools from connected server (Gmail) should be returned
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].name).toBe('sendEmail');
|
||||
expect(result[0].serverName).toBe('Gmail');
|
||||
expect(result[1].name).toBe('readEmails');
|
||||
});
|
||||
|
||||
it('should return empty array when no connected servers have tools', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.getAllTools(state);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('klavisAsLobeTools', () => {
|
||||
it('should convert Klavis servers to LobeTool format', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
tools: [
|
||||
{ name: 'sendEmail', description: 'Send email', inputSchema: { type: 'object' } },
|
||||
],
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.klavisAsLobeTools(state);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].identifier).toBe('gmail');
|
||||
expect(result[0].type).toBe('plugin');
|
||||
expect(result[0].manifest.api).toHaveLength(1);
|
||||
expect(result[0].manifest.api[0].name).toBe('sendEmail');
|
||||
expect(result[0].manifest.meta.title).toBe('Gmail');
|
||||
});
|
||||
|
||||
it('should not include disconnected servers', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.PENDING_AUTH,
|
||||
isAuthenticated: false,
|
||||
createdAt: Date.now(),
|
||||
tools: [
|
||||
{ name: 'sendEmail', description: 'Send email', inputSchema: { type: 'object' } },
|
||||
],
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.klavisAsLobeTools(state);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not include servers without tools', () => {
|
||||
const servers = [
|
||||
{
|
||||
identifier: 'gmail',
|
||||
serverName: 'Gmail',
|
||||
instanceId: 'inst-1',
|
||||
serverUrl: 'https://klavis.ai/gmail',
|
||||
status: KlavisServerStatus.CONNECTED,
|
||||
isAuthenticated: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
const state = { ...initialState, servers } as ToolStore;
|
||||
const result = klavisStoreSelectors.klavisAsLobeTools(state);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ToolStore } from '../../store';
|
||||
import { KlavisServer, KlavisServerStatus } from './types';
|
||||
|
||||
/**
|
||||
* Klavis Store Selectors
|
||||
*/
|
||||
export const klavisStoreSelectors = {
|
||||
/**
|
||||
* 获取所有 Klavis 服务器的 identifier 集合
|
||||
*/
|
||||
getAllServerIdentifiers: (s: ToolStore): Set<string> => {
|
||||
const servers = s.servers || [];
|
||||
return new Set(servers.map((server) => server.identifier));
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有可用的工具(来自所有已连接的服务器)
|
||||
*/
|
||||
getAllTools: (s: ToolStore) => {
|
||||
const connectedServers = klavisStoreSelectors.getConnectedServers(s);
|
||||
return connectedServers.flatMap((server) =>
|
||||
(server.tools || []).map((tool) => ({
|
||||
...tool,
|
||||
// 工具仍然需要 serverName 用于 API 调用
|
||||
serverName: server.serverName,
|
||||
})),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有已连接的服务器
|
||||
*/
|
||||
getConnectedServers: (s: ToolStore): KlavisServer[] =>
|
||||
(s.servers || []).filter((server) => server.status === KlavisServerStatus.CONNECTED),
|
||||
|
||||
/**
|
||||
* 获取所有待认证的服务器
|
||||
*/
|
||||
getPendingAuthServers: (s: ToolStore): KlavisServer[] =>
|
||||
(s.servers || []).filter((server) => server.status === KlavisServerStatus.PENDING_AUTH),
|
||||
|
||||
/**
|
||||
* 根据 identifier 获取服务器
|
||||
* @param identifier - 服务器标识符 (e.g., 'google-calendar')
|
||||
*/
|
||||
getServerByIdentifier: (identifier: string) => (s: ToolStore) =>
|
||||
s.servers?.find((server) => server.identifier === identifier),
|
||||
|
||||
/**
|
||||
* 获取所有 Klavis 服务器
|
||||
*/
|
||||
getServers: (s: ToolStore): KlavisServer[] => s.servers || [],
|
||||
|
||||
/**
|
||||
* 检查给定的 identifier 是否是 Klavis 服务器
|
||||
* @param identifier - 服务器标识符 (e.g., 'google-calendar')
|
||||
*/
|
||||
isKlavisServer:
|
||||
(identifier: string) =>
|
||||
(s: ToolStore): boolean => {
|
||||
const servers = s.servers || [];
|
||||
return servers.some((server) => server.identifier === identifier);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查服务器是否正在加载
|
||||
* @param identifier - 服务器标识符 (e.g., 'google-calendar')
|
||||
*/
|
||||
isServerLoading: (identifier: string) => (s: ToolStore) =>
|
||||
s.loadingServerIds?.has(identifier) || false,
|
||||
|
||||
/**
|
||||
* 检查工具是否正在执行
|
||||
*/
|
||||
isToolExecuting: (serverUrl: string, toolName: string) => (s: ToolStore) => {
|
||||
const toolId = `${serverUrl}:${toolName}`;
|
||||
return s.executingToolIds?.has(toolId) || false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all Klavis tools as LobeTool format for agent use
|
||||
* Converts Klavis tools into the format expected by ToolNameResolver
|
||||
*/
|
||||
klavisAsLobeTools: (s: ToolStore) => {
|
||||
const servers = s.servers || [];
|
||||
const tools: any[] = [];
|
||||
|
||||
servers.forEach((server) => {
|
||||
if (!server.tools || server.status !== KlavisServerStatus.CONNECTED) return;
|
||||
|
||||
// Create a manifest for this server that contains all its tools
|
||||
const apis = server.tools.map((tool) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
parameters: tool.inputSchema || {},
|
||||
}));
|
||||
|
||||
if (apis.length > 0) {
|
||||
tools.push({
|
||||
// 使用 identifier 作为存储和引用的标识符
|
||||
identifier: server.identifier,
|
||||
manifest: {
|
||||
api: apis,
|
||||
author: 'Klavis',
|
||||
homepage: 'https://klavis.ai',
|
||||
identifier: server.identifier,
|
||||
meta: {
|
||||
avatar: '☁️',
|
||||
description: `Klavis MCP Server: ${server.serverName}`,
|
||||
tags: ['klavis', 'mcp'],
|
||||
title: server.serverName,
|
||||
},
|
||||
type: 'builtin',
|
||||
version: '1.0.0',
|
||||
},
|
||||
type: 'plugin',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tools;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Klavis Server 连接状态
|
||||
*/
|
||||
export enum KlavisServerStatus {
|
||||
/** 已连接,可以使用 */
|
||||
CONNECTED = 'connected',
|
||||
/** 连接失败 */
|
||||
ERROR = 'error',
|
||||
/** 未认证,需要完成 OAuth 流程 */
|
||||
PENDING_AUTH = 'pending_auth',
|
||||
}
|
||||
|
||||
/**
|
||||
* Klavis 工具定义(MCP 格式)
|
||||
*/
|
||||
export interface KlavisTool {
|
||||
/** 工具描述 */
|
||||
description?: string;
|
||||
/** 工具输入的 JSON Schema */
|
||||
inputSchema: {
|
||||
properties?: Record<string, any>;
|
||||
required?: string[];
|
||||
type: string;
|
||||
};
|
||||
/** 工具名称 */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Klavis Server 实例
|
||||
*/
|
||||
export interface KlavisServer {
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
/** 错误信息(如果有) */
|
||||
errorMessage?: string;
|
||||
/** 服务器图标 URL */
|
||||
icon?: string;
|
||||
/**
|
||||
* 标识符,用于存储到数据库(如 'google-calendar')
|
||||
* 格式:小写,空格替换为连字符
|
||||
*/
|
||||
identifier: string;
|
||||
/** Klavis 实例 ID */
|
||||
instanceId: string;
|
||||
/** 是否已认证 */
|
||||
isAuthenticated: boolean;
|
||||
/** OAuth 认证 URL */
|
||||
oauthUrl?: string;
|
||||
/**
|
||||
* 服务器名称,用于调用 Klavis API(如 'Google Calendar')
|
||||
*/
|
||||
serverName: string;
|
||||
/** 服务器 URL (用于连接和调用工具) */
|
||||
serverUrl: string;
|
||||
/** 连接状态 */
|
||||
status: KlavisServerStatus;
|
||||
/** 服务器提供的工具列表 */
|
||||
tools?: KlavisTool[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Klavis Server 的参数
|
||||
*/
|
||||
export interface CreateKlavisServerParams {
|
||||
/**
|
||||
* 标识符,用于存储到数据库(如 'google-calendar')
|
||||
*/
|
||||
identifier: string;
|
||||
/**
|
||||
* 服务器名称,用于调用 Klavis API(如 'Google Calendar')
|
||||
*/
|
||||
serverName: string;
|
||||
/** 用户 ID */
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Klavis 工具的参数
|
||||
*/
|
||||
export interface CallKlavisToolParams {
|
||||
/** Strata Server URL */
|
||||
serverUrl: string;
|
||||
/** 工具参数 */
|
||||
toolArgs?: Record<string, unknown>;
|
||||
/** 工具名称 */
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Klavis 工具的结果
|
||||
*/
|
||||
export interface CallKlavisToolResult {
|
||||
/** 返回数据 */
|
||||
data?: any;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
}
|
||||
@@ -53,19 +53,22 @@ const installedPluginManifestList = (s: ToolStoreState) =>
|
||||
.filter((i) => !!i);
|
||||
|
||||
const installedPluginMetaList = (s: ToolStoreState) =>
|
||||
installedPlugins(s).map<InstallPluginMeta>((p) => ({
|
||||
author: p.manifest?.author,
|
||||
createdAt: p.manifest?.createdAt || (p.manifest as any)?.createAt,
|
||||
homepage: p.manifest?.homepage,
|
||||
identifier: p.identifier,
|
||||
/*
|
||||
* should remove meta
|
||||
*/
|
||||
meta: getPluginMetaById(p.identifier)(s),
|
||||
runtimeType: p.runtimeType,
|
||||
type: p.source || p.type,
|
||||
...getPluginMetaById(p.identifier)(s),
|
||||
}));
|
||||
installedPlugins(s)
|
||||
// 过滤掉 Klavis 插件(它们有自己的显示位置)
|
||||
.filter((p) => !p.customParams?.klavis)
|
||||
.map<InstallPluginMeta>((p) => ({
|
||||
author: p.manifest?.author,
|
||||
createdAt: p.manifest?.createdAt || (p.manifest as any)?.createAt,
|
||||
homepage: p.manifest?.homepage,
|
||||
identifier: p.identifier,
|
||||
/*
|
||||
* should remove meta
|
||||
*/
|
||||
meta: getPluginMetaById(p.identifier)(s),
|
||||
runtimeType: p.runtimeType,
|
||||
type: p.source || p.type,
|
||||
...getPluginMetaById(p.identifier)(s),
|
||||
}));
|
||||
const installedCustomPluginMetaList = (s: ToolStoreState) =>
|
||||
installedPluginMetaList(s).filter((p) => p.type === 'customPlugin');
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createDevtools } from '../middleware/createDevtools';
|
||||
import { ToolStoreState, initialState } from './initialState';
|
||||
import { BuiltinToolAction, createBuiltinToolSlice } from './slices/builtin';
|
||||
import { CustomPluginAction, createCustomPluginSlice } from './slices/customPlugin';
|
||||
import { KlavisStoreAction, createKlavisStoreSlice } from './slices/klavisStore';
|
||||
import { PluginMCPStoreAction, createMCPPluginStoreSlice } from './slices/mcpStore';
|
||||
import { PluginAction, createPluginSlice } from './slices/plugin';
|
||||
import { PluginStoreAction, createPluginStoreSlice } from './slices/oldStore';
|
||||
@@ -17,7 +18,8 @@ export type ToolStore = ToolStoreState &
|
||||
PluginAction &
|
||||
PluginStoreAction &
|
||||
BuiltinToolAction &
|
||||
PluginMCPStoreAction;
|
||||
PluginMCPStoreAction &
|
||||
KlavisStoreAction;
|
||||
|
||||
const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...parameters) => ({
|
||||
...initialState,
|
||||
@@ -26,6 +28,7 @@ const createStore: StateCreator<ToolStore, [['zustand/devtools', never]]> = (...
|
||||
...createPluginStoreSlice(...parameters),
|
||||
...createBuiltinToolSlice(...parameters),
|
||||
...createMCPPluginStoreSlice(...parameters),
|
||||
...createKlavisStoreSlice(...parameters),
|
||||
});
|
||||
|
||||
// =============== Implement useStore ============ //
|
||||
|
||||
Reference in New Issue
Block a user