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:
Shinji-Li
2025-12-05 14:43:22 +08:00
committed by GitHub
parent bde9bde17c
commit e3ec79e28d
62 changed files with 3441 additions and 57 deletions
+10
View File
@@ -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
+25
View File
@@ -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": "أدوات الامتداد"
}
}
+25
View File
@@ -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": "Инструменти за разширение"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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": "افزونه‌های گسترش"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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": "拡張ツール"
}
}
+25
View File
@@ -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": "확장 도구"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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ń"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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": "Дополнительные инструменты"
}
}
+25
View File
@@ -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ı"
}
}
+25
View File
@@ -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"
}
}
+25
View File
@@ -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": "扩展插件"
}
}
+25
View File
@@ -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": "擴展工具"
}
}
+1
View File
@@ -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",
+1
View File
@@ -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';
+163
View File
@@ -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"
}
}
+1 -1
View File
@@ -939,4 +939,4 @@
"folderMillis": 1764858574403,
"hash": "7838f9938b370867470e5e11807855253d23b11c2ac6aa9e90687844a356c949"
}
]
]
+1 -1
View File
@@ -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;
}
+1
View File
@@ -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;
/**
+10
View File
@@ -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 */
+41
View File
@@ -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', () => ({
+13 -2
View File
@@ -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,
+36
View File
@@ -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();
};
+25
View File
@@ -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: '扩展插件',
},
};
+2
View File
@@ -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,
+2
View File
@@ -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,
+249
View File
@@ -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;
+2
View File
@@ -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,
});
+80
View File
@@ -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,
};
}),
});
+61 -15
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
export { featureFlagsSelectors } from './selectors';
export { featureFlagsSelectors, serverConfigSelectors } from './selectors';
export { useServerConfigStore } from './store';
+1
View File
@@ -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,
+4 -1
View File
@@ -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
View File
@@ -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';
+25 -3
View File
@@ -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();
});
});
});
+375
View File
@@ -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;
},
};
+100
View File
@@ -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;
}
+16 -13
View File
@@ -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');
+4 -1
View File
@@ -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 ============ //