mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-19 07:59:39 +00:00
update
This commit is contained in:
parent
eca8110452
commit
dffd615018
7 changed files with 656 additions and 162 deletions
|
|
@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException
|
|||
from loguru import logger
|
||||
from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit
|
||||
from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
|
||||
from app.utils.oauth_state_manager import oauth_state_manager
|
||||
|
||||
|
||||
router = APIRouter(tags=["task"])
|
||||
|
|
@ -60,20 +61,34 @@ async def install_tool(tool: str):
|
|||
)
|
||||
elif tool == "google_calendar":
|
||||
try:
|
||||
# Use a dummy task_id for installation, as this is just for pre-authentication
|
||||
toolkit = GoogleCalendarToolkit("install_auth")
|
||||
|
||||
# Get available tools to verify connection
|
||||
tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
|
||||
logger.info(f"Successfully pre-instantiated {tool} toolkit with {len(tools)} tools")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tools": tools,
|
||||
"message": f"Successfully installed {tool} toolkit",
|
||||
"count": len(tools),
|
||||
"toolkit_name": "GoogleCalendarToolkit"
|
||||
}
|
||||
# Try to initialize toolkit - will succeed if credentials exist
|
||||
try:
|
||||
toolkit = GoogleCalendarToolkit("install_auth")
|
||||
tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
|
||||
logger.info(f"Successfully initialized Google Calendar toolkit with {len(tools)} tools")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tools": tools,
|
||||
"message": f"Successfully installed {tool} toolkit",
|
||||
"count": len(tools),
|
||||
"toolkit_name": "GoogleCalendarToolkit"
|
||||
}
|
||||
except ValueError as auth_error:
|
||||
# No credentials - need authorization
|
||||
logger.info(f"No credentials found, starting authorization: {auth_error}")
|
||||
|
||||
# Start background authorization in a new thread
|
||||
logger.info("Starting background Google Calendar authorization")
|
||||
GoogleCalendarToolkit.start_background_auth("install_auth")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"status": "authorizing",
|
||||
"message": "Authorization required. Browser should open automatically. Complete authorization and try installing again.",
|
||||
"toolkit_name": "GoogleCalendarToolkit",
|
||||
"requires_auth": True
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to install {tool} toolkit: {e}")
|
||||
raise HTTPException(
|
||||
|
|
@ -113,3 +128,61 @@ async def list_available_tools():
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/oauth/status/{provider}", name="get oauth status")
|
||||
async def get_oauth_status(provider: str):
|
||||
"""
|
||||
Get the current OAuth authorization status for a provider
|
||||
|
||||
Args:
|
||||
provider: OAuth provider name (e.g., 'google_calendar')
|
||||
|
||||
Returns:
|
||||
Current authorization status
|
||||
"""
|
||||
state = oauth_state_manager.get_state(provider)
|
||||
|
||||
if not state:
|
||||
return {
|
||||
"provider": provider,
|
||||
"status": "not_started",
|
||||
"message": "No authorization in progress"
|
||||
}
|
||||
|
||||
return state.to_dict()
|
||||
|
||||
|
||||
@router.post("/oauth/cancel/{provider}", name="cancel oauth")
|
||||
async def cancel_oauth(provider: str):
|
||||
"""
|
||||
Cancel an ongoing OAuth authorization flow
|
||||
|
||||
Args:
|
||||
provider: OAuth provider name (e.g., 'google_calendar')
|
||||
|
||||
Returns:
|
||||
Cancellation result
|
||||
"""
|
||||
state = oauth_state_manager.get_state(provider)
|
||||
|
||||
if not state:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No authorization found for provider '{provider}'"
|
||||
)
|
||||
|
||||
if state.status not in ["pending", "authorizing"]:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot cancel authorization with status '{state.status}'"
|
||||
)
|
||||
|
||||
state.cancel()
|
||||
logger.info(f"Cancelled OAuth authorization for {provider}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"provider": provider,
|
||||
"message": "Authorization cancelled successfully"
|
||||
}
|
||||
|
|
|
|||
109
backend/app/utils/oauth_state_manager.py
Normal file
109
backend/app/utils/oauth_state_manager.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""
|
||||
OAuth authorization state manager for background authorization flows
|
||||
"""
|
||||
import threading
|
||||
from typing import Dict, Optional, Literal
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
AuthStatus = Literal["pending", "authorizing", "success", "failed", "cancelled"]
|
||||
|
||||
|
||||
class OAuthState:
|
||||
"""Represents the state of an OAuth authorization flow"""
|
||||
|
||||
def __init__(self, provider: str):
|
||||
self.provider = provider
|
||||
self.status: AuthStatus = "pending"
|
||||
self.error: Optional[str] = None
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.result: Optional[any] = None
|
||||
self.started_at = datetime.now()
|
||||
self.completed_at: Optional[datetime] = None
|
||||
self._cancel_event = threading.Event()
|
||||
self.server = None # Store the local server instance for forced shutdown
|
||||
|
||||
def is_cancelled(self) -> bool:
|
||||
"""Check if cancellation has been requested"""
|
||||
return self._cancel_event.is_set()
|
||||
|
||||
def cancel(self):
|
||||
"""Request cancellation of the authorization flow"""
|
||||
self._cancel_event.set()
|
||||
self.status = "cancelled"
|
||||
self.completed_at = datetime.now()
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert state to dictionary for API response"""
|
||||
return {
|
||||
"provider": self.provider,
|
||||
"status": self.status,
|
||||
"error": self.error,
|
||||
"started_at": self.started_at.isoformat(),
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
||||
}
|
||||
|
||||
|
||||
class OAuthStateManager:
|
||||
"""Manager for tracking OAuth authorization flows"""
|
||||
|
||||
def __init__(self):
|
||||
self._states: Dict[str, OAuthState] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def create_state(self, provider: str) -> OAuthState:
|
||||
"""Create a new OAuth state for a provider"""
|
||||
with self._lock:
|
||||
# Cancel any existing authorization for this provider
|
||||
if provider in self._states:
|
||||
old_state = self._states[provider]
|
||||
if old_state.status in ["pending", "authorizing"]:
|
||||
old_state.cancel()
|
||||
logger.info(f"Cancelled previous {provider} authorization")
|
||||
|
||||
state = OAuthState(provider)
|
||||
self._states[provider] = state
|
||||
return state
|
||||
|
||||
def get_state(self, provider: str) -> Optional[OAuthState]:
|
||||
"""Get the current state for a provider"""
|
||||
with self._lock:
|
||||
return self._states.get(provider)
|
||||
|
||||
def update_status(
|
||||
self,
|
||||
provider: str,
|
||||
status: AuthStatus,
|
||||
error: Optional[str] = None,
|
||||
result: Optional[any] = None
|
||||
):
|
||||
"""Update the status of an authorization flow"""
|
||||
with self._lock:
|
||||
if provider in self._states:
|
||||
state = self._states[provider]
|
||||
state.status = status
|
||||
state.error = error
|
||||
state.result = result
|
||||
if status in ["success", "failed", "cancelled"]:
|
||||
state.completed_at = datetime.now()
|
||||
logger.info(f"Updated {provider} OAuth status to {status}")
|
||||
|
||||
def cleanup_old_states(self, max_age_seconds: int = 3600):
|
||||
"""Clean up old completed states"""
|
||||
with self._lock:
|
||||
now = datetime.now()
|
||||
to_remove = []
|
||||
for provider, state in self._states.items():
|
||||
if state.completed_at:
|
||||
age = (now - state.completed_at).total_seconds()
|
||||
if age > max_age_seconds:
|
||||
to_remove.append(provider)
|
||||
|
||||
for provider in to_remove:
|
||||
del self._states[provider]
|
||||
logger.debug(f"Cleaned up old OAuth state for {provider}")
|
||||
|
||||
|
||||
# Global instance
|
||||
oauth_state_manager = OAuthStateManager()
|
||||
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
from typing import Any, Dict, List
|
||||
import os
|
||||
import threading
|
||||
|
||||
from app.component.environment import env
|
||||
from app.service.task import Agents
|
||||
from app.utils.listen.toolkit_listen import listen_toolkit
|
||||
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
|
||||
from app.utils.oauth_state_manager import oauth_state_manager
|
||||
from camel.toolkits import GoogleCalendarToolkit as BaseGoogleCalendarToolkit
|
||||
from loguru import logger
|
||||
|
||||
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
||||
|
||||
|
|
@ -99,52 +102,188 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit):
|
|||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from google.auth.transport.requests import Request
|
||||
|
||||
client_id = os.environ.get("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
|
||||
refresh_token = os.environ.get("GOOGLE_REFRESH_TOKEN")
|
||||
token_uri = os.environ.get("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token")
|
||||
|
||||
creds = None
|
||||
|
||||
# First, try to load from token file
|
||||
try:
|
||||
if os.path.exists(self._token_path):
|
||||
logger.info(f"Loading credentials from token file: {self._token_path}")
|
||||
creds = Credentials.from_authorized_user_file(self._token_path, SCOPES)
|
||||
except Exception:
|
||||
logger.info("Successfully loaded credentials from token file")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load from token file: {e}")
|
||||
creds = None
|
||||
|
||||
if not creds and refresh_token:
|
||||
creds = Credentials(
|
||||
None,
|
||||
refresh_token=refresh_token,
|
||||
token_uri=token_uri,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
scopes=SCOPES,
|
||||
)
|
||||
|
||||
# If no token file, try environment variables
|
||||
if not creds:
|
||||
client_config = {
|
||||
"installed": {
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": token_uri,
|
||||
"redirect_uris": ["http://localhost"],
|
||||
}
|
||||
}
|
||||
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
|
||||
creds = flow.run_local_server(port=0)
|
||||
client_id = os.environ.get("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
|
||||
refresh_token = os.environ.get("GOOGLE_REFRESH_TOKEN")
|
||||
token_uri = os.environ.get("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token")
|
||||
|
||||
if refresh_token and client_id and client_secret:
|
||||
logger.info("Creating credentials from environment variables")
|
||||
creds = Credentials(
|
||||
None,
|
||||
refresh_token=refresh_token,
|
||||
token_uri=token_uri,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
scopes=SCOPES,
|
||||
)
|
||||
|
||||
# If still no creds, check background authorization
|
||||
if not creds:
|
||||
state = oauth_state_manager.get_state("google_calendar")
|
||||
if state and state.status == "success" and state.result:
|
||||
logger.info("Using credentials from background authorization")
|
||||
creds = state.result
|
||||
else:
|
||||
# No credentials available
|
||||
raise ValueError("No credentials available. Please run authorization first via /api/install/tool/google_calendar")
|
||||
|
||||
# Refresh if expired
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
creds.refresh(Request())
|
||||
|
||||
try:
|
||||
logger.info("Token expired, refreshing...")
|
||||
creds.refresh(Request())
|
||||
logger.info("Token refreshed successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to refresh token: {e}")
|
||||
raise ValueError("Failed to refresh expired token. Please re-authorize.")
|
||||
|
||||
# Save credentials
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._token_path), exist_ok=True)
|
||||
with open(self._token_path, "w") as f:
|
||||
f.write(creds.to_json())
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not save credentials: {e}")
|
||||
|
||||
return creds
|
||||
return creds
|
||||
|
||||
@staticmethod
|
||||
def start_background_auth(api_task_id: str = "install_auth") -> str:
|
||||
"""
|
||||
Start background OAuth authorization flow with timeout
|
||||
Returns the status of the authorization
|
||||
"""
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from wsgiref import simple_server
|
||||
import socket
|
||||
|
||||
# Check if there's an existing authorization and force stop it
|
||||
old_state = oauth_state_manager.get_state("google_calendar")
|
||||
if old_state and old_state.status in ["pending", "authorizing"]:
|
||||
logger.info("Found existing authorization, forcing shutdown...")
|
||||
old_state.cancel()
|
||||
# Try to shutdown the old server if it exists
|
||||
if hasattr(old_state, 'server') and old_state.server:
|
||||
try:
|
||||
old_state.server.shutdown()
|
||||
logger.info("Old server shutdown successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not shutdown old server: {e}")
|
||||
|
||||
# Create new state for this authorization
|
||||
state = oauth_state_manager.create_state("google_calendar")
|
||||
|
||||
def auth_flow():
|
||||
local_server = None
|
||||
try:
|
||||
state.status = "authorizing"
|
||||
oauth_state_manager.update_status("google_calendar", "authorizing")
|
||||
|
||||
client_id = os.environ.get("GOOGLE_CLIENT_ID")
|
||||
client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
|
||||
token_uri = os.environ.get("GOOGLE_TOKEN_URI", "https://oauth2.googleapis.com/token")
|
||||
|
||||
logger.info(f"Google Calendar auth - client_id present: {bool(client_id)}, client_secret present: {bool(client_secret)}")
|
||||
|
||||
if not client_id or not client_secret:
|
||||
error_msg = "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in environment variables"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
client_config = {
|
||||
"installed": {
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": token_uri,
|
||||
"redirect_uris": ["http://localhost"],
|
||||
}
|
||||
}
|
||||
|
||||
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
|
||||
|
||||
# Check for cancellation before starting
|
||||
if state.is_cancelled():
|
||||
logger.info("Authorization cancelled before starting")
|
||||
return
|
||||
|
||||
# This will automatically open browser and wait for user authorization
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"[Thread {threading.current_thread().name}] Starting local server for Google Calendar authorization")
|
||||
logger.info("Browser should open automatically in a moment...")
|
||||
logger.info("=" * 80)
|
||||
|
||||
# Run local server - this will block until authorization completes
|
||||
# Note: Each call uses a random port (port=0), so multiple concurrent attempts won't conflict
|
||||
try:
|
||||
creds = flow.run_local_server(
|
||||
port=0,
|
||||
authorization_prompt_message="",
|
||||
success_message="<h1>Authorization successful!</h1><p>You can close this window and return to Eigent.</p>",
|
||||
open_browser=True
|
||||
)
|
||||
logger.info("Authorization flow completed successfully!")
|
||||
except Exception as server_error:
|
||||
logger.error(f"Error during run_local_server: {server_error}")
|
||||
raise
|
||||
|
||||
# Check for cancellation after auth
|
||||
if state.is_cancelled():
|
||||
logger.info("Authorization cancelled after completion")
|
||||
return
|
||||
|
||||
# Save credentials to token file
|
||||
token_path = os.path.join(
|
||||
os.path.expanduser("~"),
|
||||
".eigent",
|
||||
"tokens",
|
||||
"google_calendar",
|
||||
f"google_calendar_token_{api_task_id}.json",
|
||||
)
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
||||
with open(token_path, "w") as f:
|
||||
f.write(creds.to_json())
|
||||
logger.info(f"Saved Google Calendar credentials to {token_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not save credentials: {e}")
|
||||
|
||||
# Update state with success
|
||||
oauth_state_manager.update_status("google_calendar", "success", result=creds)
|
||||
logger.info("Google Calendar authorization successful!")
|
||||
|
||||
except Exception as e:
|
||||
if state.is_cancelled():
|
||||
logger.info("Authorization was cancelled")
|
||||
oauth_state_manager.update_status("google_calendar", "cancelled")
|
||||
else:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Google Calendar authorization failed: {error_msg}")
|
||||
oauth_state_manager.update_status("google_calendar", "failed", error=error_msg)
|
||||
finally:
|
||||
# Clean up server reference
|
||||
state.server = None
|
||||
|
||||
# Start authorization in background thread
|
||||
thread = threading.Thread(target=auth_flow, daemon=True, name=f"GoogleCalendar-OAuth-{state.started_at.timestamp()}")
|
||||
state.thread = thread
|
||||
thread.start()
|
||||
|
||||
logger.info("Started background Google Calendar authorization")
|
||||
return "authorizing"
|
||||
|
|
@ -78,18 +78,29 @@ export default function IntegrationList({
|
|||
|
||||
// items or configs change, recalculate installed
|
||||
useEffect(() => {
|
||||
// remove duplicates by config_group
|
||||
const groupSet = new Set<string>();
|
||||
configs.forEach((c: any) => {
|
||||
if (c.config_group) groupSet.add(c.config_group.toLowerCase());
|
||||
});
|
||||
// construct installed map
|
||||
// For Google Calendar, check for allowed env keys
|
||||
// For other integrations, check by config_group
|
||||
const map: { [key: string]: boolean } = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
if (groupSet.has(item.key.toLowerCase())) {
|
||||
map[item.key] = true;
|
||||
if (item.key === "Google Calendar") {
|
||||
// Only mark installed when refresh token is present (auth completed)
|
||||
const hasRefreshToken = configs.some(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() === "google calendar" &&
|
||||
c.config_name === "GOOGLE_REFRESH_TOKEN" &&
|
||||
c.config_value && String(c.config_value).length > 0
|
||||
);
|
||||
map[item.key] = hasRefreshToken;
|
||||
} else {
|
||||
// For other integrations, use config_group presence
|
||||
const hasConfig = configs.some(
|
||||
(c: any) => c.config_group?.toLowerCase() === item.key.toLowerCase()
|
||||
);
|
||||
map[item.key] = hasConfig;
|
||||
}
|
||||
});
|
||||
|
||||
setInstalled(map);
|
||||
}, [items, configs]);
|
||||
|
||||
|
|
@ -100,23 +111,35 @@ export default function IntegrationList({
|
|||
value: string
|
||||
) => {
|
||||
const configPayload = {
|
||||
config_group: capitalizeFirstLetter(provider),
|
||||
// Keep exact group name to satisfy backend whitelist
|
||||
config_group: provider,
|
||||
config_name: envVarKey,
|
||||
config_value: value,
|
||||
};
|
||||
|
||||
// Check if config already exists
|
||||
const existingConfig = configs.find(
|
||||
(c: any) => c.config_name === envVarKey &&
|
||||
c.config_group?.toLowerCase() === provider.toLowerCase()
|
||||
);
|
||||
// Fetch latest configs to avoid stale state when deciding POST/PUT
|
||||
let latestConfigs: any[] = Array.isArray(configs) ? configs : [];
|
||||
try {
|
||||
const fresh = await proxyFetchGet("/api/configs");
|
||||
if (Array.isArray(fresh)) latestConfigs = fresh;
|
||||
} catch {}
|
||||
|
||||
// Backend uniqueness is by config_name for a user
|
||||
let existingConfig = latestConfigs.find((c: any) => c.config_name === envVarKey);
|
||||
|
||||
if (existingConfig) {
|
||||
// Update existing config
|
||||
await proxyFetchPut(`/api/configs/${existingConfig.id}`, configPayload);
|
||||
} else {
|
||||
// Create new config
|
||||
await proxyFetchPost("/api/configs", configPayload);
|
||||
const res = await proxyFetchPost("/api/configs", configPayload);
|
||||
if (res && res.detail && (res.detail as string).toLowerCase().includes("already exists")) {
|
||||
try {
|
||||
const again = await proxyFetchGet("/api/configs");
|
||||
const found = Array.isArray(again) ? again.find((c: any) => c.config_name === envVarKey) : null;
|
||||
if (found) {
|
||||
await proxyFetchPut(`/api/configs/${found.id}`, configPayload);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (window.electronAPI?.envWrite) {
|
||||
|
|
@ -215,6 +238,7 @@ export default function IntegrationList({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items, oauth]);
|
||||
|
||||
|
||||
// install/uninstall
|
||||
const handleInstall = useCallback(
|
||||
async (item: IntegrationItem) => {
|
||||
|
|
@ -237,21 +261,22 @@ export default function IntegrationList({
|
|||
return;
|
||||
}
|
||||
|
||||
if (item.key === "Google Calendar") {
|
||||
let mcp = {
|
||||
name: "Google Calendar",
|
||||
key: "Google Calendar",
|
||||
install_command: {
|
||||
env: {} as any,
|
||||
},
|
||||
id: 14,
|
||||
};
|
||||
item.env_vars.map((key) => {
|
||||
mcp.install_command.env[key] = "";
|
||||
});
|
||||
onShowEnvConfig?.(mcp);
|
||||
return;
|
||||
}
|
||||
if (item.key === "Google Calendar") {
|
||||
// Always prompt env dialog first instead of jumping to authorization
|
||||
let mcp = {
|
||||
name: "Google Calendar",
|
||||
key: "Google Calendar",
|
||||
install_command: {
|
||||
env: {} as any,
|
||||
},
|
||||
id: 14,
|
||||
};
|
||||
item.env_vars.map((key) => {
|
||||
mcp.install_command.env[key] = "";
|
||||
});
|
||||
onShowEnvConfig?.(mcp);
|
||||
return;
|
||||
}
|
||||
if (installed[item.key]) return;
|
||||
await item.onInstall();
|
||||
// refresh configs after install to update installed state indicator
|
||||
|
|
@ -333,6 +358,7 @@ export default function IntegrationList({
|
|||
></MCPEnvDialog>
|
||||
{items.map((item) => {
|
||||
const isInstalled = !!installed[item.key];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
|
|
|
|||
|
|
@ -97,37 +97,64 @@ const ToolSelect = forwardRef<
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
if (response.success) {
|
||||
// Save to config to mark as installed
|
||||
await proxyFetchPost("/api/configs", {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_CLIENT_ID",
|
||||
config_value: response.toolkit_name || "GoogleCalendarToolkit",
|
||||
});
|
||||
console.log("Google Calendar installed successfully");
|
||||
// After successful installation, add to selected tools
|
||||
const calendarItem = {
|
||||
id: 0, // Use 0 for integration items
|
||||
key: key,
|
||||
name: key,
|
||||
description: "Google Calendar integration for managing events and schedules",
|
||||
toolkit: "google_calendar_toolkit", // Add the toolkit name
|
||||
isLocal: true
|
||||
};
|
||||
addOption(calendarItem, true);
|
||||
} else {
|
||||
console.error("Failed to install Google Calendar:", response.error || "Unknown error");
|
||||
throw new Error(response.error || "Failed to install Google Calendar");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to install Google Calendar:", error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
if (response.success) {
|
||||
// Check if config exists first to avoid 400 error
|
||||
const existingConfigs = await proxyFetchGet("/api/configs");
|
||||
const existing = Array.isArray(existingConfigs)
|
||||
? existingConfigs.find((c: any) =>
|
||||
c.config_group?.toLowerCase() === "google calendar" &&
|
||||
c.config_name === "GOOGLE_REFRESH_TOKEN"
|
||||
)
|
||||
: null;
|
||||
|
||||
const configPayload = {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_REFRESH_TOKEN",
|
||||
config_value: "exists",
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(`/api/configs/${existing.id}`, configPayload);
|
||||
} else {
|
||||
await proxyFetchPost("/api/configs", configPayload);
|
||||
}
|
||||
|
||||
console.log("Google Calendar installed successfully");
|
||||
// After successful installation, add to selected tools
|
||||
const calendarItem = {
|
||||
id: 0, // Use 0 for integration items
|
||||
key: key,
|
||||
name: key,
|
||||
description: "Google Calendar integration for managing events and schedules",
|
||||
toolkit: "google_calendar_toolkit", // Add the toolkit name
|
||||
isLocal: true
|
||||
};
|
||||
addOption(calendarItem, true);
|
||||
} else if (response.status === "authorizing") {
|
||||
// Authorization in progress - browser should have opened
|
||||
console.log("Google Calendar authorization in progress. Please complete in browser.");
|
||||
console.log(response.message);
|
||||
// Don't throw error - this is expected behavior
|
||||
} else {
|
||||
// Real error
|
||||
console.error("Failed to install Google Calendar:", response.error || "Unknown error");
|
||||
throw new Error(response.error || "Failed to install Google Calendar");
|
||||
}
|
||||
// Return the response so IntegrationList can check the status
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
// Only throw if it's a real error, not authorization in progress
|
||||
if (!error.message?.includes("authorization")) {
|
||||
console.error("Failed to install Google Calendar:", error.message);
|
||||
throw error;
|
||||
}
|
||||
return null; // Return null on error
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onInstall = () =>
|
||||
(window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
|
||||
|
|
@ -271,28 +298,55 @@ const ToolSelect = forwardRef<
|
|||
// Continue anyway to trigger installation
|
||||
}
|
||||
|
||||
// Trigger instantiation for Google Calendar
|
||||
if (activeMcp.key === "Google Calendar") {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
// Trigger instantiation for Google Calendar
|
||||
if (activeMcp.key === "Google Calendar") {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
|
||||
if (response.success) {
|
||||
// Mark as successfully installed by writing refresh token marker
|
||||
const existingConfigs = await proxyFetchGet("/api/configs");
|
||||
const existing = Array.isArray(existingConfigs)
|
||||
? existingConfigs.find((c: any) =>
|
||||
c.config_group?.toLowerCase() === "google calendar" &&
|
||||
c.config_name === "GOOGLE_REFRESH_TOKEN"
|
||||
)
|
||||
: null;
|
||||
|
||||
if (response.success) {
|
||||
const selectedItem = {
|
||||
id: activeMcp.id,
|
||||
key: activeMcp.key,
|
||||
name: activeMcp.name,
|
||||
description: "Google Calendar integration for managing events and schedules",
|
||||
toolkit: "google_calendar_toolkit",
|
||||
isLocal: true
|
||||
};
|
||||
addOption(selectedItem, true);
|
||||
const configPayload = {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_REFRESH_TOKEN",
|
||||
config_value: "exists",
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(`/api/configs/${existing.id}`, configPayload);
|
||||
} else {
|
||||
console.error("Failed to install Google Calendar:", response.error || "Unknown error");
|
||||
await proxyFetchPost("/api/configs", configPayload);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to install Google Calendar:", error.message);
|
||||
|
||||
// Refresh integrations to update install status
|
||||
fetchIntegrationsData();
|
||||
|
||||
const selectedItem = {
|
||||
id: activeMcp.id,
|
||||
key: activeMcp.key,
|
||||
name: activeMcp.name,
|
||||
description: "Google Calendar integration for managing events and schedules",
|
||||
toolkit: "google_calendar_toolkit",
|
||||
isLocal: true
|
||||
};
|
||||
addOption(selectedItem, true);
|
||||
} else if (response.status === "authorizing") {
|
||||
// Authorization in progress - browser should have opened
|
||||
console.log("Google Calendar authorization in progress. Please complete in browser and try installing again.");
|
||||
} else {
|
||||
console.error("Failed to install Google Calendar:", response.error || "Unknown error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to install Google Calendar:", error.message);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
setInstalling((prev) => ({ ...prev, [id]: true }));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
proxyFetchGet,
|
||||
proxyFetchDelete,
|
||||
proxyFetchPost,
|
||||
proxyFetchPut,
|
||||
fetchPost,
|
||||
proxyFetchGet,
|
||||
proxyFetchDelete,
|
||||
proxyFetchPost,
|
||||
proxyFetchPut,
|
||||
fetchPost,
|
||||
fetchGet,
|
||||
} from "@/api/http";
|
||||
import MCPList from "./components/MCPList";
|
||||
import MCPConfigDialog from "./components/MCPConfigDialog";
|
||||
|
|
@ -137,22 +138,93 @@ export default function SettingMCP() {
|
|||
}
|
||||
};
|
||||
} else if (key.toLowerCase() === 'google calendar') {
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
if (response.success) {
|
||||
toast.success("Google Calendar installed successfully");
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey(prev => prev + 1);
|
||||
} else {
|
||||
toast.error(response.error || "Failed to install Google Calendar");
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to install Google Calendar");
|
||||
onInstall = async () => {
|
||||
try {
|
||||
const response = await fetchPost("/install/tool/google_calendar");
|
||||
if (response.success) {
|
||||
// Check if config exists first to avoid 400 error
|
||||
const existingConfigs = await proxyFetchGet("/api/configs");
|
||||
const existing = Array.isArray(existingConfigs)
|
||||
? existingConfigs.find((c: any) =>
|
||||
c.config_group?.toLowerCase() === "google calendar" &&
|
||||
c.config_name === "GOOGLE_REFRESH_TOKEN"
|
||||
)
|
||||
: null;
|
||||
|
||||
const configPayload = {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_REFRESH_TOKEN",
|
||||
config_value: "exists",
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(`/api/configs/${existing.id}`, configPayload);
|
||||
} else {
|
||||
await proxyFetchPost("/api/configs", configPayload);
|
||||
}
|
||||
|
||||
toast.success("Google Calendar installed successfully");
|
||||
// Refresh the integrations list to show the installed state
|
||||
fetchList();
|
||||
// Force refresh IntegrationList component
|
||||
setRefreshKey(prev => prev + 1);
|
||||
} else if (response.status === "authorizing") {
|
||||
// Authorization in progress - start polling for completion
|
||||
toast.info("Please complete authorization in your browser...");
|
||||
|
||||
// Poll for authorization completion via oauth status endpoint
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResp = await fetchGet("/oauth/status/google_calendar");
|
||||
if (statusResp?.status === "success") {
|
||||
clearInterval(pollInterval);
|
||||
// Now that auth succeeded, run install again to initialize toolkit
|
||||
const finalize = await fetchPost("/install/tool/google_calendar");
|
||||
if (finalize?.success) {
|
||||
const configs = await proxyFetchGet("/api/configs");
|
||||
const existing = Array.isArray(configs)
|
||||
? configs.find((c: any) =>
|
||||
c.config_group?.toLowerCase() === "google calendar" &&
|
||||
c.config_name === "GOOGLE_REFRESH_TOKEN"
|
||||
)
|
||||
: null;
|
||||
|
||||
const payload = {
|
||||
config_group: "Google Calendar",
|
||||
config_name: "GOOGLE_REFRESH_TOKEN",
|
||||
config_value: "exists",
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await proxyFetchPut(`/api/configs/${existing.id}`, payload);
|
||||
} else {
|
||||
await proxyFetchPost("/api/configs", payload);
|
||||
}
|
||||
|
||||
toast.success("Google Calendar installed successfully");
|
||||
fetchList();
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
}
|
||||
} else if (statusResp?.status === "failed" || statusResp?.status === "cancelled") {
|
||||
clearInterval(pollInterval);
|
||||
const msg = statusResp?.error || (statusResp?.status === "cancelled" ? "Authorization cancelled" : "Authorization failed");
|
||||
toast.error(msg);
|
||||
}
|
||||
// if still authorizing, continue polling
|
||||
} catch (err) {
|
||||
console.error("Polling oauth status failed", err);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Safety timeout
|
||||
setTimeout(() => clearInterval(pollInterval), 5 * 60 * 1000);
|
||||
} else {
|
||||
toast.error(response.error || response.message || "Failed to install Google Calendar");
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to install Google Calendar");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onInstall = () =>
|
||||
(window.location.href = `${baseURL}/api/oauth/${key.toLowerCase()}/login`);
|
||||
|
|
|
|||
|
|
@ -77,16 +77,24 @@ export default function IntegrationList({
|
|||
|
||||
// items or configs change, recalculate installed
|
||||
useEffect(() => {
|
||||
// remove duplicates by config_group
|
||||
const groupSet = new Set<string>();
|
||||
configs.forEach((c: any) => {
|
||||
if (c.config_group) groupSet.add(c.config_group.toLowerCase());
|
||||
});
|
||||
// construct installed map
|
||||
const map: { [key: string]: boolean } = {};
|
||||
items.forEach((item) => {
|
||||
if (groupSet.has(item.key.toLowerCase())) {
|
||||
map[item.key] = true;
|
||||
if (item.key === "Google Calendar") {
|
||||
// Only mark installed after refresh token exists
|
||||
const hasRefreshToken = configs.some(
|
||||
(c: any) =>
|
||||
c.config_group?.toLowerCase() === "google calendar" &&
|
||||
c.config_name === "GOOGLE_REFRESH_TOKEN" &&
|
||||
c.config_value && String(c.config_value).length > 0
|
||||
);
|
||||
map[item.key] = hasRefreshToken;
|
||||
} else {
|
||||
// For other integrations, use presence of any config in the group
|
||||
const hasConfig = configs.some(
|
||||
(c: any) => c.config_group?.toLowerCase() === item.key.toLowerCase()
|
||||
);
|
||||
map[item.key] = hasConfig;
|
||||
}
|
||||
});
|
||||
setInstalled(map);
|
||||
|
|
@ -99,23 +107,36 @@ export default function IntegrationList({
|
|||
value: string
|
||||
) => {
|
||||
const configPayload = {
|
||||
config_group: capitalizeFirstLetter(provider),
|
||||
// Use exact group name, do not transform case to avoid whitelist mismatch
|
||||
config_group: provider,
|
||||
config_name: envVarKey,
|
||||
config_value: value,
|
||||
};
|
||||
|
||||
// Check if config already exists
|
||||
const existingConfig = configs.find(
|
||||
(c: any) => c.config_name === envVarKey &&
|
||||
c.config_group?.toLowerCase() === provider.toLowerCase()
|
||||
);
|
||||
// Fetch latest configs to avoid stale state when deciding POST/PUT
|
||||
let latestConfigs: any[] = Array.isArray(configs) ? configs : [];
|
||||
try {
|
||||
const fresh = await proxyFetchGet("/api/configs");
|
||||
if (Array.isArray(fresh)) latestConfigs = fresh;
|
||||
} catch {}
|
||||
|
||||
// Check if config already exists (by name, regardless of group - backend uniqueness is by name)
|
||||
let existingConfig = latestConfigs.find((c: any) => c.config_name === envVarKey);
|
||||
|
||||
if (existingConfig) {
|
||||
// Update existing config
|
||||
await proxyFetchPut(`/api/configs/${existingConfig.id}`, configPayload);
|
||||
} else {
|
||||
// Create new config
|
||||
await proxyFetchPost("/api/configs", configPayload);
|
||||
const res = await proxyFetchPost("/api/configs", configPayload);
|
||||
// If backend says it already exists (race), switch to PUT
|
||||
if (res && res.detail && (res.detail as string).toLowerCase().includes("already exists")) {
|
||||
try {
|
||||
const again = await proxyFetchGet("/api/configs");
|
||||
const found = Array.isArray(again) ? again.find((c: any) => c.config_name === envVarKey) : null;
|
||||
if (found) {
|
||||
await proxyFetchPut(`/api/configs/${found.id}`, configPayload);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (window.electronAPI?.envWrite) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue