| """ |
| Connector Manager — Manus-style connector ecosystem |
| Manages OAuth tokens, connection state, API access |
| """ |
| import json |
| import os |
| import time |
| from typing import Dict, List, Optional |
| import structlog |
|
|
| log = structlog.get_logger() |
|
|
| CONNECTORS_CONFIG = [ |
| { |
| "id": "github", |
| "name": "GitHub", |
| "icon": "github", |
| "color": "#24292e", |
| "env_key": "GITHUB_TOKEN", |
| "description": "Repos, Issues, PRs, Commits", |
| "scopes": ["repo", "issues", "pull_requests"], |
| "category": "code", |
| }, |
| { |
| "id": "huggingface", |
| "name": "HuggingFace", |
| "icon": "huggingface", |
| "color": "#ff9d00", |
| "env_key": "HF_TOKEN", |
| "description": "Spaces, Models, Datasets", |
| "scopes": ["spaces", "models"], |
| "category": "ai", |
| }, |
| { |
| "id": "vercel", |
| "name": "Vercel", |
| "icon": "vercel", |
| "color": "#000000", |
| "env_key": "VERCEL_TOKEN", |
| "description": "Deployments, Domains, Functions", |
| "scopes": ["deployments", "projects"], |
| "category": "deploy", |
| }, |
| { |
| "id": "openai", |
| "name": "OpenAI", |
| "icon": "openai", |
| "color": "#10a37f", |
| "env_key": "OPENAI_API_KEY", |
| "description": "GPT-4o, Embeddings, DALL-E", |
| "scopes": ["chat", "embeddings"], |
| "category": "ai", |
| }, |
| { |
| "id": "groq", |
| "name": "Groq", |
| "icon": "groq", |
| "color": "#f55036", |
| "env_key": "GROQ_API_KEY", |
| "description": "Llama 3.3 70B — Ultra Fast", |
| "scopes": ["chat"], |
| "category": "ai", |
| }, |
| { |
| "id": "cerebras", |
| "name": "Cerebras", |
| "icon": "cerebras", |
| "color": "#7c3aed", |
| "env_key": "CEREBRAS_API_KEY", |
| "description": "Llama 3.1 70B — Long Context", |
| "scopes": ["chat"], |
| "category": "ai", |
| }, |
| { |
| "id": "openrouter", |
| "name": "OpenRouter", |
| "icon": "openrouter", |
| "color": "#6366f1", |
| "env_key": "OPENROUTER_API_KEY", |
| "description": "Multi-model router, free tier", |
| "scopes": ["chat"], |
| "category": "ai", |
| }, |
| { |
| "id": "anthropic", |
| "name": "Anthropic", |
| "icon": "anthropic", |
| "color": "#d4a27f", |
| "env_key": "ANTHROPIC_API_KEY", |
| "description": "Claude 3.5 Sonnet", |
| "scopes": ["chat"], |
| "category": "ai", |
| }, |
| { |
| "id": "n8n", |
| "name": "n8n", |
| "icon": "n8n", |
| "color": "#ea4b71", |
| "env_key": "N8N_URL", |
| "description": "Workflow automation engine", |
| "scopes": ["workflows", "executions"], |
| "category": "workflow", |
| }, |
| { |
| "id": "telegram", |
| "name": "Telegram", |
| "icon": "telegram", |
| "color": "#0088cc", |
| "env_key": "TELEGRAM_BOT_TOKEN", |
| "description": "Bot API, messages, webhooks", |
| "scopes": ["messages", "bots"], |
| "category": "messaging", |
| }, |
| { |
| "id": "discord", |
| "name": "Discord", |
| "icon": "discord", |
| "color": "#5865f2", |
| "env_key": "DISCORD_BOT_TOKEN", |
| "description": "Bot, channels, webhooks", |
| "scopes": ["messages", "bots"], |
| "category": "messaging", |
| }, |
| { |
| "id": "slack", |
| "name": "Slack", |
| "icon": "slack", |
| "color": "#4a154b", |
| "env_key": "SLACK_BOT_TOKEN", |
| "description": "Messages, channels, workflows", |
| "scopes": ["messages", "channels"], |
| "category": "messaging", |
| }, |
| { |
| "id": "cloudflare", |
| "name": "Cloudflare", |
| "icon": "cloudflare", |
| "color": "#f38020", |
| "env_key": "CLOUDFLARE_API_TOKEN", |
| "description": "Workers, KV, Pages", |
| "scopes": ["workers", "kv", "pages"], |
| "category": "infra", |
| }, |
| ] |
|
|
|
|
| class ConnectorManager: |
| """Manages all platform connectors — connection state, tokens, status.""" |
|
|
| def __init__(self): |
| self._configs = {c["id"]: c for c in CONNECTORS_CONFIG} |
|
|
| def get_all(self) -> List[Dict]: |
| """Get all connectors with connection status.""" |
| result = [] |
| for cfg in CONNECTORS_CONFIG: |
| token = os.environ.get(cfg["env_key"], "") |
| result.append({ |
| **cfg, |
| "connected": bool(token), |
| "token_preview": f"{token[:8]}..." if token else None, |
| }) |
| return result |
|
|
| def get_connected(self) -> List[Dict]: |
| """Get only connected connectors.""" |
| return [c for c in self.get_all() if c["connected"]] |
|
|
| def get_by_category(self, category: str) -> List[Dict]: |
| """Get connectors by category.""" |
| return [c for c in self.get_all() if c["category"] == category] |
|
|
| def is_connected(self, connector_id: str) -> bool: |
| cfg = self._configs.get(connector_id) |
| if not cfg: |
| return False |
| return bool(os.environ.get(cfg["env_key"], "")) |
|
|
| def get_token(self, connector_id: str) -> Optional[str]: |
| cfg = self._configs.get(connector_id) |
| if not cfg: |
| return None |
| return os.environ.get(cfg["env_key"]) or None |
|
|
| def set_token(self, connector_id: str, token: str): |
| """Set connector token at runtime (does not persist across restarts).""" |
| cfg = self._configs.get(connector_id) |
| if cfg: |
| os.environ[cfg["env_key"]] = token |
| log.info("Connector token set", connector=connector_id) |
|
|
| def get_summary(self) -> Dict: |
| all_c = self.get_all() |
| connected = [c for c in all_c if c["connected"]] |
| by_cat = {} |
| for c in all_c: |
| cat = c["category"] |
| if cat not in by_cat: |
| by_cat[cat] = {"total": 0, "connected": 0} |
| by_cat[cat]["total"] += 1 |
| if c["connected"]: |
| by_cat[cat]["connected"] += 1 |
| return { |
| "total": len(all_c), |
| "connected": len(connected), |
| "by_category": by_cat, |
| "ai_ready": self.is_connected("openai") or self.is_connected("groq") |
| or self.is_connected("openrouter") or self.is_connected("anthropic") |
| or self.is_connected("cerebras"), |
| } |
|
|