machinaos 0.0.76 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -61,7 +61,10 @@ MessageHandler = Callable[[Dict[str, Any], WebSocket], Awaitable[Dict[str, Any]]
|
|
|
61
61
|
|
|
62
62
|
def ws_handler(*required_fields: str):
|
|
63
63
|
"""Simple decorator for WebSocket handlers. Validates required fields and wraps errors."""
|
|
64
|
+
import functools
|
|
65
|
+
|
|
64
66
|
def decorator(func: MessageHandler) -> MessageHandler:
|
|
67
|
+
@functools.wraps(func)
|
|
65
68
|
async def wrapper(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
66
69
|
for field in required_fields:
|
|
67
70
|
if not data.get(field):
|
|
@@ -297,7 +300,7 @@ async def handle_load_options(
|
|
|
297
300
|
Body: ``{"method": "...", "params": {...}}``
|
|
298
301
|
Response: ``{"options": [{"value": ..., "label": ...}]}``
|
|
299
302
|
"""
|
|
300
|
-
from services.
|
|
303
|
+
from services.ws_handler_registry import dispatch_load_options
|
|
301
304
|
|
|
302
305
|
options = await dispatch_load_options(data["method"], data.get("params", {}))
|
|
303
306
|
return {"method": data["method"], "options": options}
|
|
@@ -309,7 +312,7 @@ async def handle_list_load_options_methods(
|
|
|
309
312
|
) -> Dict[str, Any]:
|
|
310
313
|
"""Return registered loadOptionsMethod names. Editor uses this to
|
|
311
314
|
know which dynamic-option loaders are wired."""
|
|
312
|
-
from services.
|
|
315
|
+
from services.ws_handler_registry import list_load_options_methods
|
|
313
316
|
|
|
314
317
|
return {"methods": list_load_options_methods()}
|
|
315
318
|
|
|
@@ -358,9 +361,24 @@ async def handle_get_credential_catalogue(data: Dict[str, Any], websocket: WebSo
|
|
|
358
361
|
kind = provider.get("kind", "")
|
|
359
362
|
status_hook = provider.get("status_hook")
|
|
360
363
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
+
tokens = None
|
|
365
|
+
# Declarative per-provider override for the "stored" check.
|
|
366
|
+
# Lets Telegram (kind=oauth + status_hook but actual storage
|
|
367
|
+
# is api_key for the bot token) signal that "stored" should
|
|
368
|
+
# be ``has_valid_key("telegram_bot_token")`` rather than the
|
|
369
|
+
# default ``get_oauth_tokens(status_hook)`` lookup. Other
|
|
370
|
+
# providers don't declare ``stored_check`` and keep the
|
|
371
|
+
# original kind/status_hook-based logic untouched -- so
|
|
372
|
+
# Google's saved client_secret (password field) does NOT
|
|
373
|
+
# flip the connected dot before the user actually completes
|
|
374
|
+
# the OAuth flow.
|
|
375
|
+
stored_check = provider.get("stored_check")
|
|
376
|
+
if stored_check and stored_check.get("type") == "api_key":
|
|
377
|
+
provider["stored"] = await auth_service.has_valid_key(stored_check.get("key", pid))
|
|
378
|
+
elif status_hook:
|
|
379
|
+
# Status-hook providers (whatsapp, android, twitter, google,
|
|
380
|
+
# claude_code, codex_cli) use OAuth tokens for the runtime
|
|
381
|
+
# connection state.
|
|
364
382
|
tokens = await auth_service.get_oauth_tokens(status_hook)
|
|
365
383
|
provider["stored"] = tokens is not None
|
|
366
384
|
elif kind == "apiKey":
|
|
@@ -373,6 +391,15 @@ async def handle_get_credential_catalogue(data: Dict[str, Any], websocket: WebSo
|
|
|
373
391
|
else:
|
|
374
392
|
provider["stored"] = False
|
|
375
393
|
|
|
394
|
+
# Surface the connected account identifier (email > display name)
|
|
395
|
+
# so the modal can render "Connected as foo@bar.com" without a
|
|
396
|
+
# per-provider status hook. Twitter / Google / Stripe / Claude
|
|
397
|
+
# all populate `email` / `name` via `auth_service.store_oauth_tokens`.
|
|
398
|
+
if tokens:
|
|
399
|
+
provider["account_label"] = tokens.get("email") or tokens.get("name")
|
|
400
|
+
else:
|
|
401
|
+
provider["account_label"] = None
|
|
402
|
+
|
|
376
403
|
return catalogue
|
|
377
404
|
|
|
378
405
|
|
|
@@ -1218,35 +1245,61 @@ async def handle_get_ai_models(data: Dict[str, Any], websocket: WebSocket) -> Di
|
|
|
1218
1245
|
async def handle_validate_api_key(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1219
1246
|
"""Validate and store an API key.
|
|
1220
1247
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1248
|
+
Pure dispatch — looks up the plugin's ``Credential`` subclass in
|
|
1249
|
+
``CREDENTIAL_REGISTRY`` and calls its ``validate`` classmethod. The
|
|
1250
|
+
base ``Credential.validate`` (defined in
|
|
1251
|
+
``services/plugin/credential.py``) wires the shared scaffold
|
|
1252
|
+
(storage + status broadcast + error classification + response
|
|
1253
|
+
envelope) and dispatches the per-provider probe via the
|
|
1254
|
+
subclass-supplied ``_probe`` hook. Cloud LLM providers inherit
|
|
1255
|
+
``_LLMApiKey._probe`` (``ai_service.fetch_models``); Maps + Apify +
|
|
1256
|
+
local-LLM credentials override ``_probe`` (or the whole ``validate``
|
|
1257
|
+
method, in the local-LLM case) with their own bespoke probes.
|
|
1258
|
+
|
|
1259
|
+
The router doesn't know about specific providers — adding a new
|
|
1260
|
+
provider with a special validator is a single new ``_probe``
|
|
1261
|
+
override on the plugin's ``Credential`` subclass.
|
|
1227
1262
|
"""
|
|
1263
|
+
from services.plugin.credential import CREDENTIAL_REGISTRY
|
|
1264
|
+
|
|
1228
1265
|
provider = data["provider"].lower()
|
|
1229
1266
|
normalized = dict(data, provider=provider)
|
|
1230
1267
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1268
|
+
cred_cls = CREDENTIAL_REGISTRY.get(provider)
|
|
1269
|
+
if cred_cls is None:
|
|
1270
|
+
return {
|
|
1271
|
+
"success": False,
|
|
1272
|
+
"valid": False,
|
|
1273
|
+
"error": f"Unknown provider '{provider}' — no Credential class registered.",
|
|
1274
|
+
}
|
|
1275
|
+
return await cred_cls.validate(normalized)
|
|
1233
1276
|
|
|
1234
|
-
ai_service = container.ai_service()
|
|
1235
|
-
auth_service = container.auth_service()
|
|
1236
|
-
broadcaster = get_status_broadcaster()
|
|
1237
|
-
api_key = data["api_key"].strip()
|
|
1238
1277
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
)
|
|
1249
|
-
|
|
1278
|
+
def _lookup_credential_default(storage_key: str) -> Optional[str]:
|
|
1279
|
+
"""Look up a field's catalogue ``default`` for the given storage key.
|
|
1280
|
+
|
|
1281
|
+
Storage keys are either the provider id (``"openai"`` for cloud
|
|
1282
|
+
providers whose field key is ``apiKey``) or the field key itself
|
|
1283
|
+
(``"lmstudio_proxy"`` etc. for local-LLM providers). Both shapes
|
|
1284
|
+
map back to a credential_providers.json field; this helper finds
|
|
1285
|
+
the one and returns its ``default`` value if declared. Used by
|
|
1286
|
+
``handle_get_stored_api_key`` to surface canonical defaults
|
|
1287
|
+
(e.g. local-LLM Base URL ``http://localhost:1234/v1``) to the
|
|
1288
|
+
frontend without requiring per-panel pre-fill logic.
|
|
1289
|
+
"""
|
|
1290
|
+
from services.credential_registry import get_credential_registry
|
|
1291
|
+
registry = get_credential_registry()
|
|
1292
|
+
for provider in registry.get_all_providers():
|
|
1293
|
+
provider_id = provider.get("id") or provider.get("name", "").lower()
|
|
1294
|
+
for field in (provider.get("fields") or []):
|
|
1295
|
+
field_key = field.get("key")
|
|
1296
|
+
if not field_key:
|
|
1297
|
+
continue
|
|
1298
|
+
field_storage_key = provider_id if field_key == "apiKey" else field_key
|
|
1299
|
+
if field_storage_key == storage_key:
|
|
1300
|
+
default = field.get("default")
|
|
1301
|
+
return default if default else None
|
|
1302
|
+
return None
|
|
1250
1303
|
|
|
1251
1304
|
|
|
1252
1305
|
@ws_handler("provider")
|
|
@@ -1257,11 +1310,21 @@ async def handle_get_stored_api_key(data: Dict[str, Any], websocket: WebSocket)
|
|
|
1257
1310
|
``update_api_key_status`` broadcast shape — every WS payload the
|
|
1258
1311
|
frontend receives for API key state uses the same convention, so no
|
|
1259
1312
|
per-field adapter is needed on the TypeScript side.
|
|
1313
|
+
|
|
1314
|
+
When nothing is stored AND the catalogue declares a ``default``
|
|
1315
|
+
for this field (e.g. local-LLM canonical Base URL), the default
|
|
1316
|
+
value is returned in ``apiKey`` with ``hasKey: false``. The
|
|
1317
|
+
frontend renders the value but tracks ``stored`` separately via
|
|
1318
|
+
``hasKey`` so the validated/connected badge stays honest. Lets
|
|
1319
|
+
users click Fetch on a fresh install without retyping the URL.
|
|
1260
1320
|
"""
|
|
1261
1321
|
auth_service = container.auth_service()
|
|
1262
1322
|
provider = data["provider"].lower()
|
|
1263
1323
|
api_key = await auth_service.get_api_key(provider, data.get("session_id", "default"))
|
|
1264
1324
|
if not api_key:
|
|
1325
|
+
default = _lookup_credential_default(provider)
|
|
1326
|
+
if default is not None:
|
|
1327
|
+
return {"provider": provider, "hasKey": False, "apiKey": default}
|
|
1265
1328
|
return {"provider": provider, "hasKey": False}
|
|
1266
1329
|
models = await auth_service.get_stored_models(provider, data.get("session_id", "default"))
|
|
1267
1330
|
return {"provider": provider, "hasKey": True, "apiKey": api_key, "models": models, "timestamp": time.time()}
|
|
@@ -1283,12 +1346,20 @@ async def handle_save_api_key(data: Dict[str, Any], websocket: WebSocket) -> Dic
|
|
|
1283
1346
|
|
|
1284
1347
|
async def _do_save() -> Dict[str, Any]:
|
|
1285
1348
|
auth_service = container.auth_service()
|
|
1349
|
+
broadcaster = get_status_broadcaster()
|
|
1286
1350
|
await auth_service.store_api_key(
|
|
1287
1351
|
provider=provider,
|
|
1288
1352
|
api_key=data["api_key"].strip(),
|
|
1289
1353
|
models=data.get("models", []),
|
|
1290
1354
|
session_id=data.get("session_id", "default"),
|
|
1291
1355
|
)
|
|
1356
|
+
# Symmetric broadcast: tells every connected client to refetch
|
|
1357
|
+
# the catalogue so the `stored` flag flips on this provider.
|
|
1358
|
+
# Don't claim validity here — save_api_key doesn't validate.
|
|
1359
|
+
await broadcaster.broadcast_credential_event(
|
|
1360
|
+
"credential.api_key.saved",
|
|
1361
|
+
provider=provider,
|
|
1362
|
+
)
|
|
1292
1363
|
return {"provider": data["provider"]}
|
|
1293
1364
|
|
|
1294
1365
|
return await store.run(data.get("request_id"), _do_save)
|
|
@@ -1307,7 +1378,20 @@ async def handle_delete_api_key(data: Dict[str, Any], websocket: WebSocket) -> D
|
|
|
1307
1378
|
|
|
1308
1379
|
async def _do_delete() -> Dict[str, Any]:
|
|
1309
1380
|
auth_service = container.auth_service()
|
|
1381
|
+
broadcaster = get_status_broadcaster()
|
|
1310
1382
|
await auth_service.remove_api_key(provider, data.get("session_id", "default"))
|
|
1383
|
+
# Two broadcasts: api_key_status clears `apiKeyStatuses[provider]`
|
|
1384
|
+
# on every connected client (in-memory validation cache); the
|
|
1385
|
+
# CloudEvents-typed credential.api_key.deleted invalidates the
|
|
1386
|
+
# catalogue so the `stored` flag flips. Both go through the
|
|
1387
|
+
# 300 ms invalidateCatalogue debounce — one refetch.
|
|
1388
|
+
await broadcaster.update_api_key_status(
|
|
1389
|
+
provider, valid=False, has_key=False, message="deleted", models=[],
|
|
1390
|
+
)
|
|
1391
|
+
await broadcaster.broadcast_credential_event(
|
|
1392
|
+
"credential.api_key.deleted",
|
|
1393
|
+
provider=provider,
|
|
1394
|
+
)
|
|
1311
1395
|
return {"provider": data["provider"]}
|
|
1312
1396
|
|
|
1313
1397
|
return await store.run(data.get("request_id"), _do_delete)
|
|
@@ -1326,306 +1410,17 @@ async def handle_claude_oauth_login(data: Dict[str, Any], websocket: WebSocket)
|
|
|
1326
1410
|
|
|
1327
1411
|
@ws_handler()
|
|
1328
1412
|
async def handle_claude_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1329
|
-
"""Check Claude OAuth credentials status
|
|
1330
|
-
from services.claude_oauth import
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
# ============================================================================
|
|
1335
|
-
# Twitter OAuth Handlers
|
|
1336
|
-
# ============================================================================
|
|
1337
|
-
|
|
1338
|
-
@ws_handler()
|
|
1339
|
-
async def handle_twitter_oauth_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1340
|
-
"""
|
|
1341
|
-
Initiate Twitter OAuth 2.0 with PKCE flow.
|
|
1342
|
-
|
|
1343
|
-
Opens browser to Twitter authorization page. After user authorizes,
|
|
1344
|
-
Twitter redirects to /api/twitter/callback which stores tokens.
|
|
1345
|
-
"""
|
|
1346
|
-
from services.twitter_oauth import TwitterOAuth
|
|
1347
|
-
|
|
1348
|
-
auth_service = container.auth_service()
|
|
1349
|
-
|
|
1350
|
-
# Get stored client credentials (configured via Credentials Modal)
|
|
1351
|
-
client_id = await auth_service.get_api_key("twitter_client_id")
|
|
1352
|
-
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
1353
|
-
|
|
1354
|
-
if not client_id:
|
|
1355
|
-
return {
|
|
1356
|
-
"success": False,
|
|
1357
|
-
"error": "Twitter Client ID not configured. Add your Twitter API credentials first."
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
# Create OAuth instance and generate authorization URL
|
|
1361
|
-
from services.oauth_utils import get_redirect_uri
|
|
1362
|
-
redirect_uri = get_redirect_uri(websocket, "twitter")
|
|
1363
|
-
|
|
1364
|
-
oauth = TwitterOAuth(
|
|
1365
|
-
client_id=client_id,
|
|
1366
|
-
client_secret=client_secret,
|
|
1367
|
-
redirect_uri=redirect_uri,
|
|
1368
|
-
)
|
|
1369
|
-
|
|
1370
|
-
auth_data = oauth.generate_authorization_url()
|
|
1371
|
-
|
|
1372
|
-
return {
|
|
1373
|
-
"success": True,
|
|
1374
|
-
"message": "Opening Twitter authorization in browser...",
|
|
1375
|
-
"url": auth_data["url"],
|
|
1376
|
-
"state": auth_data["state"],
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
@ws_handler()
|
|
1381
|
-
async def handle_twitter_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1382
|
-
"""
|
|
1383
|
-
Check Twitter OAuth connection status.
|
|
1384
|
-
|
|
1385
|
-
Returns connection status and user info if connected.
|
|
1386
|
-
Uses OAuth token system (matches REST endpoint in routers/twitter.py).
|
|
1387
|
-
"""
|
|
1388
|
-
from services.twitter_oauth import TwitterOAuth
|
|
1389
|
-
|
|
1390
|
-
auth_service = container.auth_service()
|
|
1391
|
-
|
|
1392
|
-
# Get tokens from OAuth system (NOT api_key system)
|
|
1393
|
-
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
1394
|
-
|
|
1395
|
-
if not tokens or not tokens.get("access_token"):
|
|
1396
|
-
return {
|
|
1397
|
-
"connected": False,
|
|
1398
|
-
"username": None,
|
|
1399
|
-
"user_id": None,
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
access_token = tokens["access_token"]
|
|
1403
|
-
refresh_token = tokens.get("refresh_token", "")
|
|
1404
|
-
|
|
1405
|
-
# Get client credentials for API calls (these are correctly stored as API keys)
|
|
1406
|
-
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
1407
|
-
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
1408
|
-
|
|
1409
|
-
from services.oauth_utils import get_redirect_uri
|
|
1410
|
-
redirect_uri = get_redirect_uri(websocket, "twitter")
|
|
1411
|
-
|
|
1412
|
-
oauth = TwitterOAuth(
|
|
1413
|
-
client_id=client_id,
|
|
1414
|
-
client_secret=client_secret,
|
|
1415
|
-
redirect_uri=redirect_uri,
|
|
1416
|
-
)
|
|
1417
|
-
|
|
1418
|
-
# Verify token by getting user info
|
|
1419
|
-
user_info = await oauth.get_user_info(access_token)
|
|
1420
|
-
|
|
1421
|
-
if not user_info.get("success") and refresh_token:
|
|
1422
|
-
refresh_result = await oauth.refresh_access_token(refresh_token)
|
|
1423
|
-
if refresh_result.get("success"):
|
|
1424
|
-
# Store refreshed tokens in OAuth system
|
|
1425
|
-
await auth_service.store_oauth_tokens(
|
|
1426
|
-
provider="twitter",
|
|
1427
|
-
access_token=refresh_result["access_token"],
|
|
1428
|
-
refresh_token=refresh_result.get("refresh_token") or refresh_token,
|
|
1429
|
-
email=tokens.get("email", ""),
|
|
1430
|
-
name=tokens.get("name", ""),
|
|
1431
|
-
scopes=tokens.get("scopes", ""),
|
|
1432
|
-
customer_id="owner",
|
|
1433
|
-
)
|
|
1434
|
-
# Retry user info
|
|
1435
|
-
user_info = await oauth.get_user_info(refresh_result["access_token"])
|
|
1436
|
-
|
|
1437
|
-
if not user_info.get("success"):
|
|
1438
|
-
return {
|
|
1439
|
-
"connected": False,
|
|
1440
|
-
"username": None,
|
|
1441
|
-
"user_id": None,
|
|
1442
|
-
"error": user_info.get("error"),
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
return {
|
|
1446
|
-
"connected": True,
|
|
1447
|
-
"username": user_info.get("username"),
|
|
1448
|
-
"user_id": user_info.get("id"),
|
|
1449
|
-
"name": user_info.get("name"),
|
|
1450
|
-
"profile_image_url": user_info.get("profile_image_url"),
|
|
1451
|
-
"verified": user_info.get("verified"),
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
@ws_handler()
|
|
1456
|
-
async def handle_twitter_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1457
|
-
"""
|
|
1458
|
-
Disconnect Twitter by revoking tokens and clearing stored credentials.
|
|
1459
|
-
Uses OAuth token system (matches REST endpoint in routers/twitter.py).
|
|
1460
|
-
"""
|
|
1461
|
-
from services.twitter_oauth import TwitterOAuth
|
|
1462
|
-
|
|
1463
|
-
auth_service = container.auth_service()
|
|
1464
|
-
|
|
1465
|
-
# Get tokens from OAuth system
|
|
1466
|
-
tokens = await auth_service.get_oauth_tokens("twitter", customer_id="owner")
|
|
1467
|
-
access_token = tokens.get("access_token") if tokens else None
|
|
1468
|
-
refresh_token = tokens.get("refresh_token") if tokens else None
|
|
1469
|
-
|
|
1470
|
-
# Get client credentials (correctly stored as API keys)
|
|
1471
|
-
client_id = await auth_service.get_api_key("twitter_client_id") or ""
|
|
1472
|
-
client_secret = await auth_service.get_api_key("twitter_client_secret")
|
|
1473
|
-
|
|
1474
|
-
# Revoke tokens if we have them
|
|
1475
|
-
if access_token or refresh_token:
|
|
1476
|
-
from services.oauth_utils import get_redirect_uri
|
|
1477
|
-
redirect_uri = get_redirect_uri(websocket, "twitter")
|
|
1478
|
-
|
|
1479
|
-
oauth = TwitterOAuth(
|
|
1480
|
-
client_id=client_id,
|
|
1481
|
-
client_secret=client_secret,
|
|
1482
|
-
redirect_uri=redirect_uri,
|
|
1483
|
-
)
|
|
1484
|
-
|
|
1485
|
-
if access_token:
|
|
1486
|
-
await oauth.revoke_token(access_token, "access_token")
|
|
1487
|
-
if refresh_token:
|
|
1488
|
-
await oauth.revoke_token(refresh_token, "refresh_token")
|
|
1489
|
-
|
|
1490
|
-
# Clear from OAuth system
|
|
1491
|
-
await auth_service.remove_oauth_tokens("twitter", customer_id="owner")
|
|
1492
|
-
|
|
1493
|
-
# Clean up any stale API key entries (from old broken code)
|
|
1494
|
-
for key in ["twitter_access_token", "twitter_refresh_token", "twitter_user_info"]:
|
|
1495
|
-
try:
|
|
1496
|
-
await auth_service.remove_api_key(key)
|
|
1497
|
-
except Exception:
|
|
1498
|
-
pass
|
|
1499
|
-
|
|
1500
|
-
return {"success": True, "message": "Twitter disconnected"}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
# ============================================================================
|
|
1504
|
-
# Google Workspace OAuth Handlers
|
|
1505
|
-
# ============================================================================
|
|
1506
|
-
|
|
1507
|
-
@ws_handler()
|
|
1508
|
-
async def handle_google_oauth_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1509
|
-
"""
|
|
1510
|
-
Initiate Google Workspace OAuth 2.0 flow.
|
|
1511
|
-
|
|
1512
|
-
Opens browser to Google authorization page. After user authorizes,
|
|
1513
|
-
Google redirects to /api/google/callback which stores tokens.
|
|
1514
|
-
Grants access to all Google Workspace services (Gmail, Calendar, Drive, etc).
|
|
1515
|
-
"""
|
|
1516
|
-
from services.google_oauth import GoogleOAuth
|
|
1517
|
-
|
|
1518
|
-
auth_service = container.auth_service()
|
|
1519
|
-
|
|
1520
|
-
client_id = await auth_service.get_api_key("google_client_id")
|
|
1521
|
-
client_secret = await auth_service.get_api_key("google_client_secret")
|
|
1522
|
-
|
|
1523
|
-
if not client_id or not client_secret:
|
|
1524
|
-
return {
|
|
1525
|
-
"success": False,
|
|
1526
|
-
"error": "Google Workspace Client ID and Secret not configured. Add your Google API credentials first."
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
from services.oauth_utils import get_redirect_uri
|
|
1530
|
-
redirect_uri = get_redirect_uri(websocket, "google")
|
|
1413
|
+
"""Check Claude OAuth credentials status via ``claude auth status``."""
|
|
1414
|
+
from services.claude_oauth import claude_auth_status
|
|
1415
|
+
has_token = await claude_auth_status()
|
|
1416
|
+
return {"success": True, "has_token": has_token}
|
|
1531
1417
|
|
|
1532
|
-
oauth = GoogleOAuth(
|
|
1533
|
-
client_id=client_id,
|
|
1534
|
-
client_secret=client_secret,
|
|
1535
|
-
redirect_uri=redirect_uri,
|
|
1536
|
-
)
|
|
1537
1418
|
|
|
1538
|
-
|
|
1419
|
+
# NOTE: `cli_login` / `cli_auth_status` handlers are owned by
|
|
1420
|
+
# `services/cli_agent/_handlers.py` and self-registered into
|
|
1421
|
+
# `services.ws_handler_registry` on package import — no entries needed
|
|
1422
|
+
# here. See `services/cli_agent/__init__.py`.
|
|
1539
1423
|
|
|
1540
|
-
return {
|
|
1541
|
-
"success": True,
|
|
1542
|
-
"message": "Opening Google authorization in browser...",
|
|
1543
|
-
"url": auth_data["url"],
|
|
1544
|
-
"state": auth_data["state"],
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
@ws_handler()
|
|
1549
|
-
async def handle_google_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1550
|
-
"""
|
|
1551
|
-
Check Google Workspace OAuth connection status.
|
|
1552
|
-
|
|
1553
|
-
Returns connection status and user info if connected.
|
|
1554
|
-
Also updates broadcaster status so all clients stay in sync.
|
|
1555
|
-
"""
|
|
1556
|
-
from services.google_oauth import GoogleOAuth
|
|
1557
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
1558
|
-
|
|
1559
|
-
auth_service = container.auth_service()
|
|
1560
|
-
broadcaster = get_status_broadcaster()
|
|
1561
|
-
|
|
1562
|
-
tokens = await auth_service.get_oauth_tokens("google", customer_id="owner")
|
|
1563
|
-
|
|
1564
|
-
if not tokens or not tokens.get("access_token"):
|
|
1565
|
-
status = {"connected": False, "email": None, "name": None}
|
|
1566
|
-
broadcaster._status["google"] = status
|
|
1567
|
-
return status
|
|
1568
|
-
|
|
1569
|
-
email = tokens.get("email")
|
|
1570
|
-
name = tokens.get("name")
|
|
1571
|
-
refresh_token = tokens.get("refresh_token")
|
|
1572
|
-
|
|
1573
|
-
# Try to refresh token proactively
|
|
1574
|
-
try:
|
|
1575
|
-
client_id = await auth_service.get_api_key("google_client_id") or ""
|
|
1576
|
-
client_secret = await auth_service.get_api_key("google_client_secret") or ""
|
|
1577
|
-
|
|
1578
|
-
if refresh_token and client_id and client_secret:
|
|
1579
|
-
refreshed = GoogleOAuth.refresh_credentials(
|
|
1580
|
-
refresh_token=refresh_token,
|
|
1581
|
-
client_id=client_id,
|
|
1582
|
-
client_secret=client_secret,
|
|
1583
|
-
)
|
|
1584
|
-
if refreshed.get("success") and refreshed.get("access_token"):
|
|
1585
|
-
await auth_service.store_oauth_tokens(
|
|
1586
|
-
provider="google",
|
|
1587
|
-
access_token=refreshed["access_token"],
|
|
1588
|
-
refresh_token=refresh_token,
|
|
1589
|
-
email=email,
|
|
1590
|
-
name=name,
|
|
1591
|
-
customer_id="owner",
|
|
1592
|
-
)
|
|
1593
|
-
|
|
1594
|
-
status = {"connected": True, "email": email, "name": name}
|
|
1595
|
-
broadcaster._status["google"] = status
|
|
1596
|
-
# Broadcast so all connected clients update
|
|
1597
|
-
await broadcaster.broadcast({
|
|
1598
|
-
"type": "google_status",
|
|
1599
|
-
"data": status,
|
|
1600
|
-
})
|
|
1601
|
-
return status
|
|
1602
|
-
except Exception as e:
|
|
1603
|
-
logger.warning(f"Google token validation failed: {e}")
|
|
1604
|
-
status = {"connected": False, "email": None, "name": None, "error": str(e)}
|
|
1605
|
-
broadcaster._status["google"] = {"connected": False, "email": None, "name": None}
|
|
1606
|
-
return status
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
@ws_handler()
|
|
1610
|
-
async def handle_google_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1611
|
-
"""
|
|
1612
|
-
Disconnect Google Workspace by clearing stored credentials.
|
|
1613
|
-
"""
|
|
1614
|
-
from services.status_broadcaster import get_status_broadcaster
|
|
1615
|
-
|
|
1616
|
-
auth_service = container.auth_service()
|
|
1617
|
-
broadcaster = get_status_broadcaster()
|
|
1618
|
-
|
|
1619
|
-
await auth_service.remove_oauth_tokens("google", customer_id="owner")
|
|
1620
|
-
|
|
1621
|
-
# Clear broadcaster status and notify all clients
|
|
1622
|
-
broadcaster._status["google"] = {"connected": False, "email": None, "name": None}
|
|
1623
|
-
await broadcaster.broadcast({
|
|
1624
|
-
"type": "google_status",
|
|
1625
|
-
"data": {"connected": False, "email": None, "name": None},
|
|
1626
|
-
})
|
|
1627
|
-
|
|
1628
|
-
return {"success": True, "message": "Google Workspace disconnected"}
|
|
1629
1424
|
|
|
1630
1425
|
|
|
1631
1426
|
@ws_handler("url")
|
|
@@ -1666,458 +1461,22 @@ async def handle_test_ai_proxy(data: Dict[str, Any], websocket: WebSocket) -> Di
|
|
|
1666
1461
|
|
|
1667
1462
|
|
|
1668
1463
|
# ============================================================================
|
|
1669
|
-
# Android
|
|
1670
|
-
#
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
"""Get list of connected Android devices."""
|
|
1675
|
-
android_service = container.android_service()
|
|
1676
|
-
devices = await android_service.list_devices()
|
|
1677
|
-
return {"devices": devices, "timestamp": time.time()}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
@ws_handler("service_id", "action")
|
|
1681
|
-
async def handle_execute_android_action(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1682
|
-
"""Execute an Android service action."""
|
|
1683
|
-
android_service = container.android_service()
|
|
1684
|
-
broadcaster = get_status_broadcaster()
|
|
1685
|
-
service_id, action = data["service_id"], data["action"]
|
|
1686
|
-
node_id = data.get("node_id", f"android_{service_id}_{action}")
|
|
1687
|
-
|
|
1688
|
-
await broadcaster.update_node_status(node_id, "executing")
|
|
1689
|
-
result = await android_service.execute_service(
|
|
1690
|
-
node_id=node_id, service_id=service_id, action=action,
|
|
1691
|
-
parameters=data.get("parameters", {}),
|
|
1692
|
-
android_host=data.get("android_host", "localhost"),
|
|
1693
|
-
android_port=data.get("android_port", 8888)
|
|
1694
|
-
)
|
|
1695
|
-
|
|
1696
|
-
status = "success" if result.get("success") else "error"
|
|
1697
|
-
await broadcaster.update_node_status(node_id, status, result.get("result") or {"error": result.get("error")})
|
|
1698
|
-
return result
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
@ws_handler()
|
|
1702
|
-
async def handle_android_relay_connect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1703
|
-
"""Connect to Android relay server.
|
|
1704
|
-
|
|
1705
|
-
Establishes WebSocket connection to relay server and broadcasts QR code for pairing.
|
|
1706
|
-
Status updates are automatically broadcast via the relay client's broadcaster integration.
|
|
1707
|
-
"""
|
|
1708
|
-
from services.android import get_relay_client
|
|
1709
|
-
|
|
1710
|
-
url = data.get("url", "")
|
|
1711
|
-
api_key = data.get("api_key")
|
|
1712
|
-
|
|
1713
|
-
if not url:
|
|
1714
|
-
return {
|
|
1715
|
-
"success": False,
|
|
1716
|
-
"connected": False,
|
|
1717
|
-
"error": "Relay URL is required"
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
if not api_key:
|
|
1721
|
-
return {
|
|
1722
|
-
"success": False,
|
|
1723
|
-
"connected": False,
|
|
1724
|
-
"error": "API key is required"
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
logger.info(f"[WebSocket] Android relay connect: {url}")
|
|
1728
|
-
|
|
1729
|
-
try:
|
|
1730
|
-
client, error = await get_relay_client(url, api_key)
|
|
1731
|
-
if client:
|
|
1732
|
-
logger.info(f"[WebSocket] Android relay connect success, qr_data present: {bool(client.qr_data)}, session_token: {client.session_token}")
|
|
1733
|
-
return {
|
|
1734
|
-
"success": True,
|
|
1735
|
-
"connected": True,
|
|
1736
|
-
"session_token": client.session_token,
|
|
1737
|
-
"qr_data": client.qr_data,
|
|
1738
|
-
"message": "Connected to relay server"
|
|
1739
|
-
}
|
|
1740
|
-
else:
|
|
1741
|
-
return {
|
|
1742
|
-
"success": False,
|
|
1743
|
-
"connected": False,
|
|
1744
|
-
"error": error or "Failed to connect to relay server"
|
|
1745
|
-
}
|
|
1746
|
-
except Exception as e:
|
|
1747
|
-
logger.error(f"[WebSocket] Android relay connect error: {e}")
|
|
1748
|
-
return {"success": False, "error": str(e)}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
@ws_handler()
|
|
1752
|
-
async def handle_android_relay_disconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1753
|
-
"""Disconnect from Android relay server.
|
|
1754
|
-
|
|
1755
|
-
Closes the relay WebSocket connection and broadcasts disconnected status.
|
|
1756
|
-
"""
|
|
1757
|
-
from services.android import close_relay_client
|
|
1758
|
-
|
|
1759
|
-
logger.info("[WebSocket] Android relay disconnect requested")
|
|
1760
|
-
|
|
1761
|
-
try:
|
|
1762
|
-
await close_relay_client()
|
|
1763
|
-
return {
|
|
1764
|
-
"success": True,
|
|
1765
|
-
"connected": False,
|
|
1766
|
-
"message": "Disconnected from relay server"
|
|
1767
|
-
}
|
|
1768
|
-
except Exception as e:
|
|
1769
|
-
logger.error(f"[WebSocket] Android relay disconnect error: {e}")
|
|
1770
|
-
return {"success": False, "error": str(e)}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
@ws_handler()
|
|
1774
|
-
async def handle_android_relay_reconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1775
|
-
"""Reconnect to Android relay server with a new session token.
|
|
1776
|
-
|
|
1777
|
-
Forces disconnect and reconnect to get fresh session_token and QR code.
|
|
1778
|
-
Useful when pairing fails or Android device needs to re-pair.
|
|
1779
|
-
"""
|
|
1780
|
-
from services.android import close_relay_client, get_relay_client
|
|
1781
|
-
|
|
1782
|
-
url = data.get("url", "")
|
|
1783
|
-
api_key = data.get("api_key")
|
|
1784
|
-
|
|
1785
|
-
if not url:
|
|
1786
|
-
return {
|
|
1787
|
-
"success": False,
|
|
1788
|
-
"connected": False,
|
|
1789
|
-
"error": "Relay URL is required"
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
if not api_key:
|
|
1793
|
-
return {
|
|
1794
|
-
"success": False,
|
|
1795
|
-
"connected": False,
|
|
1796
|
-
"error": "API key is required"
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
logger.info("[WebSocket] Android relay reconnect: forcing new session")
|
|
1800
|
-
|
|
1801
|
-
try:
|
|
1802
|
-
# Force disconnect existing connection
|
|
1803
|
-
await close_relay_client()
|
|
1804
|
-
|
|
1805
|
-
# Small delay to ensure clean disconnect
|
|
1806
|
-
await asyncio.sleep(0.5)
|
|
1807
|
-
|
|
1808
|
-
# Reconnect with fresh session
|
|
1809
|
-
client, error = await get_relay_client(url, api_key)
|
|
1810
|
-
if client:
|
|
1811
|
-
return {
|
|
1812
|
-
"success": True,
|
|
1813
|
-
"connected": True,
|
|
1814
|
-
"session_token": client.session_token,
|
|
1815
|
-
"qr_data": client.qr_data,
|
|
1816
|
-
"message": "Reconnected with new session token"
|
|
1817
|
-
}
|
|
1818
|
-
else:
|
|
1819
|
-
return {
|
|
1820
|
-
"success": False,
|
|
1821
|
-
"connected": False,
|
|
1822
|
-
"error": error or "Failed to reconnect to relay server"
|
|
1823
|
-
}
|
|
1824
|
-
except Exception as e:
|
|
1825
|
-
logger.error(f"[WebSocket] Android relay reconnect error: {e}")
|
|
1826
|
-
return {"success": False, "error": str(e)}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
# ============================================================================
|
|
1830
|
-
# Maps Handlers
|
|
1831
|
-
# ============================================================================
|
|
1832
|
-
|
|
1833
|
-
async def handle_validate_maps_key(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1834
|
-
"""Validate Google Maps API key and save to database if valid."""
|
|
1835
|
-
import httpx
|
|
1836
|
-
broadcaster = get_status_broadcaster()
|
|
1837
|
-
auth_service = container.auth_service()
|
|
1838
|
-
|
|
1839
|
-
api_key = data.get("api_key", "").strip()
|
|
1840
|
-
session_id = data.get("session_id", "default")
|
|
1841
|
-
|
|
1842
|
-
if not api_key:
|
|
1843
|
-
return {"success": False, "valid": False, "error": "api_key required"}
|
|
1844
|
-
|
|
1845
|
-
try:
|
|
1846
|
-
# Test the API key with a simple geocoding request
|
|
1847
|
-
async with httpx.AsyncClient() as client:
|
|
1848
|
-
response = await client.get(
|
|
1849
|
-
"https://maps.googleapis.com/maps/api/geocode/json",
|
|
1850
|
-
params={
|
|
1851
|
-
"address": "1600 Amphitheatre Parkway, Mountain View, CA",
|
|
1852
|
-
"key": api_key
|
|
1853
|
-
},
|
|
1854
|
-
timeout=10.0
|
|
1855
|
-
)
|
|
1856
|
-
|
|
1857
|
-
response_data = response.json()
|
|
1858
|
-
|
|
1859
|
-
if response_data.get("status") == "OK":
|
|
1860
|
-
# Save the validated key to database
|
|
1861
|
-
await auth_service.store_api_key(
|
|
1862
|
-
provider="google_maps",
|
|
1863
|
-
api_key=api_key,
|
|
1864
|
-
models=[],
|
|
1865
|
-
session_id=session_id
|
|
1866
|
-
)
|
|
1867
|
-
await broadcaster.update_api_key_status(
|
|
1868
|
-
provider="google_maps",
|
|
1869
|
-
valid=True,
|
|
1870
|
-
message="API key validated successfully"
|
|
1871
|
-
)
|
|
1872
|
-
return {"success": True, "valid": True, "message": "Google Maps API key is valid"}
|
|
1873
|
-
|
|
1874
|
-
elif response_data.get("status") == "REQUEST_DENIED":
|
|
1875
|
-
error_msg = response_data.get("error_message", "Invalid API key")
|
|
1876
|
-
await broadcaster.update_api_key_status(
|
|
1877
|
-
provider="google_maps",
|
|
1878
|
-
valid=False,
|
|
1879
|
-
message=error_msg
|
|
1880
|
-
)
|
|
1881
|
-
return {"success": True, "valid": False, "message": error_msg}
|
|
1882
|
-
|
|
1883
|
-
else:
|
|
1884
|
-
# Other statuses like ZERO_RESULTS still mean the key works
|
|
1885
|
-
# Save the validated key to database
|
|
1886
|
-
await auth_service.store_api_key(
|
|
1887
|
-
provider="google_maps",
|
|
1888
|
-
api_key=api_key,
|
|
1889
|
-
models=[],
|
|
1890
|
-
session_id=session_id
|
|
1891
|
-
)
|
|
1892
|
-
await broadcaster.update_api_key_status(
|
|
1893
|
-
provider="google_maps",
|
|
1894
|
-
valid=True,
|
|
1895
|
-
message="API key validated"
|
|
1896
|
-
)
|
|
1897
|
-
return {"success": True, "valid": True, "message": f"API key is valid (status: {response_data.get('status')})"}
|
|
1898
|
-
|
|
1899
|
-
except httpx.TimeoutException:
|
|
1900
|
-
await broadcaster.update_api_key_status(
|
|
1901
|
-
provider="google_maps",
|
|
1902
|
-
valid=False,
|
|
1903
|
-
message="Validation request timed out"
|
|
1904
|
-
)
|
|
1905
|
-
return {"success": False, "valid": False, "error": "Validation request timed out"}
|
|
1906
|
-
|
|
1907
|
-
except Exception as e:
|
|
1908
|
-
logger.error("Maps key validation failed", error=str(e))
|
|
1909
|
-
await broadcaster.update_api_key_status(
|
|
1910
|
-
provider="google_maps",
|
|
1911
|
-
valid=False,
|
|
1912
|
-
message=str(e)
|
|
1913
|
-
)
|
|
1914
|
-
return {"success": False, "valid": False, "error": str(e)}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
# ============================================================================
|
|
1918
|
-
# Apify Handlers
|
|
1464
|
+
# Android handlers (5 of them: get_android_devices, execute_android_action,
|
|
1465
|
+
# android_relay_{connect,disconnect,reconnect}) live in
|
|
1466
|
+
# ``nodes/android/_handlers.py`` and self-register via
|
|
1467
|
+
# ``register_ws_handlers``. The plugin's HTTP router lives in
|
|
1468
|
+
# ``nodes/android/_router.py`` and mounts via the plugin-router loop.
|
|
1919
1469
|
# ============================================================================
|
|
1920
1470
|
|
|
1921
|
-
async def handle_validate_apify_key(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
1922
|
-
"""Validate Apify API token and save to database if valid."""
|
|
1923
|
-
from nodes.scraper.apify_actor import validate_apify_token
|
|
1924
|
-
|
|
1925
|
-
broadcaster = get_status_broadcaster()
|
|
1926
|
-
auth_service = container.auth_service()
|
|
1927
|
-
|
|
1928
|
-
api_key = data.get("api_key", "").strip()
|
|
1929
|
-
session_id = data.get("session_id", "default")
|
|
1930
|
-
|
|
1931
|
-
if not api_key:
|
|
1932
|
-
return {"success": False, "valid": False, "error": "api_key required"}
|
|
1933
|
-
|
|
1934
|
-
try:
|
|
1935
|
-
result = await validate_apify_token(api_key)
|
|
1936
|
-
|
|
1937
|
-
if result.get("valid"):
|
|
1938
|
-
# Save the validated key to database
|
|
1939
|
-
await auth_service.store_api_key(
|
|
1940
|
-
provider="apify",
|
|
1941
|
-
api_key=api_key,
|
|
1942
|
-
models=[],
|
|
1943
|
-
session_id=session_id
|
|
1944
|
-
)
|
|
1945
|
-
await broadcaster.update_api_key_status(
|
|
1946
|
-
provider="apify",
|
|
1947
|
-
valid=True,
|
|
1948
|
-
message=f"Apify token validated - user: {result.get('username', 'unknown')}"
|
|
1949
|
-
)
|
|
1950
|
-
return {
|
|
1951
|
-
"success": True,
|
|
1952
|
-
"valid": True,
|
|
1953
|
-
"message": "Apify API token is valid",
|
|
1954
|
-
"username": result.get("username"),
|
|
1955
|
-
"email": result.get("email"),
|
|
1956
|
-
"plan": result.get("plan")
|
|
1957
|
-
}
|
|
1958
|
-
else:
|
|
1959
|
-
error_msg = result.get("error", "Invalid API token")
|
|
1960
|
-
await broadcaster.update_api_key_status(
|
|
1961
|
-
provider="apify",
|
|
1962
|
-
valid=False,
|
|
1963
|
-
message=error_msg
|
|
1964
|
-
)
|
|
1965
|
-
return {"success": True, "valid": False, "message": error_msg}
|
|
1966
|
-
|
|
1967
|
-
except Exception as e:
|
|
1968
|
-
logger.error("Apify key validation failed", error=str(e))
|
|
1969
|
-
await broadcaster.update_api_key_status(
|
|
1970
|
-
provider="apify",
|
|
1971
|
-
valid=False,
|
|
1972
|
-
message=str(e)
|
|
1973
|
-
)
|
|
1974
|
-
return {"success": False, "valid": False, "error": str(e)}
|
|
1975
1471
|
|
|
1976
|
-
|
|
1977
|
-
# Per-provider
|
|
1978
|
-
#
|
|
1979
|
-
#
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
# ============================================================================
|
|
1987
|
-
# WhatsApp Handlers - Wrappers for services.whatsapp_service functions
|
|
1988
|
-
# ============================================================================
|
|
1989
|
-
|
|
1990
|
-
from services.whatsapp_service import (
|
|
1991
|
-
handle_whatsapp_status as _wa_status,
|
|
1992
|
-
handle_whatsapp_connected_phone as _wa_connected_phone,
|
|
1993
|
-
handle_whatsapp_qr as _wa_qr,
|
|
1994
|
-
handle_whatsapp_send as _wa_send,
|
|
1995
|
-
handle_whatsapp_start as _wa_start,
|
|
1996
|
-
handle_whatsapp_restart as _wa_restart,
|
|
1997
|
-
handle_whatsapp_groups as _wa_groups,
|
|
1998
|
-
handle_whatsapp_group_info as _wa_group_info,
|
|
1999
|
-
handle_whatsapp_chat_history as _wa_chat_history,
|
|
2000
|
-
handle_whatsapp_newsletters as _wa_newsletters,
|
|
2001
|
-
whatsapp_rpc_call as _wa_rpc_call,
|
|
2002
|
-
)
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
async def handle_whatsapp_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2006
|
-
return await _wa_status()
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
async def handle_whatsapp_connected_phone(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2010
|
-
"""Get the connected WhatsApp phone number."""
|
|
2011
|
-
return await _wa_connected_phone()
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
async def handle_whatsapp_qr(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2015
|
-
return await _wa_qr()
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
async def handle_whatsapp_send(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2019
|
-
"""Forward all send params to WhatsApp handler - supports all message types."""
|
|
2020
|
-
return await _wa_send(data)
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
async def handle_whatsapp_start(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2024
|
-
return await _wa_start()
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
async def handle_whatsapp_restart(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2028
|
-
return await _wa_restart()
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
async def handle_whatsapp_groups(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2032
|
-
return await _wa_groups()
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
async def handle_whatsapp_newsletters(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2036
|
-
"""Get list of subscribed newsletter channels."""
|
|
2037
|
-
return await _wa_newsletters()
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
async def handle_whatsapp_group_info(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2041
|
-
"""Get group participants with resolved phone numbers."""
|
|
2042
|
-
group_id = data.get("group_id", "")
|
|
2043
|
-
return await _wa_group_info(group_id)
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
async def handle_whatsapp_chat_history(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2047
|
-
"""Get chat history from WhatsApp history store."""
|
|
2048
|
-
return await _wa_chat_history(data)
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
async def handle_whatsapp_rate_limit_get(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2052
|
-
"""Get rate limit config and current stats."""
|
|
2053
|
-
result = await _wa_rpc_call("rate_limit_get", {})
|
|
2054
|
-
return result
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
async def handle_whatsapp_rate_limit_set(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2058
|
-
"""Update rate limit configuration."""
|
|
2059
|
-
config = data.get("config", {})
|
|
2060
|
-
result = await _wa_rpc_call("rate_limit_set", config)
|
|
2061
|
-
return result
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
async def handle_whatsapp_rate_limit_stats(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2065
|
-
"""Get current rate limit statistics."""
|
|
2066
|
-
result = await _wa_rpc_call("rate_limit_stats", {})
|
|
2067
|
-
return result
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
async def handle_whatsapp_rate_limit_unpause(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2071
|
-
"""Resume rate limiting after automatic pause."""
|
|
2072
|
-
result = await _wa_rpc_call("rate_limit_unpause", {})
|
|
2073
|
-
return result
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
async def handle_whatsapp_mark_read(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2077
|
-
"""Mark messages as read. Schema: mark_read({message_ids, chat_jid, sender_jid?})"""
|
|
2078
|
-
message_ids = data.get("message_ids", [])
|
|
2079
|
-
chat_jid = data.get("chat_jid", "")
|
|
2080
|
-
if not message_ids or not chat_jid:
|
|
2081
|
-
return {"success": False, "error": "message_ids (array) and chat_jid are required"}
|
|
2082
|
-
params: Dict[str, Any] = {"message_ids": message_ids, "chat_jid": chat_jid}
|
|
2083
|
-
sender_jid = data.get("sender_jid")
|
|
2084
|
-
if sender_jid:
|
|
2085
|
-
params["sender_jid"] = sender_jid
|
|
2086
|
-
result = await _wa_rpc_call("mark_read", params)
|
|
2087
|
-
return result
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
async def handle_whatsapp_typing(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2091
|
-
"""Send typing indicator. Schema: typing({jid, state: 'composing'|'paused', media?})"""
|
|
2092
|
-
jid = data.get("jid", "")
|
|
2093
|
-
state = data.get("state", "composing")
|
|
2094
|
-
if not jid:
|
|
2095
|
-
return {"success": False, "error": "jid is required"}
|
|
2096
|
-
params: Dict[str, Any] = {"jid": jid, "state": state}
|
|
2097
|
-
media = data.get("media")
|
|
2098
|
-
if media:
|
|
2099
|
-
params["media"] = media
|
|
2100
|
-
result = await _wa_rpc_call("typing", params)
|
|
2101
|
-
return result
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
async def handle_whatsapp_presence(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2105
|
-
"""Set online/offline presence. Schema: presence({status: 'available'|'unavailable'})"""
|
|
2106
|
-
status = data.get("status", "available")
|
|
2107
|
-
result = await _wa_rpc_call("presence", {"status": status})
|
|
2108
|
-
return result
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
async def handle_whatsapp_stop(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2112
|
-
"""Graceful WhatsApp shutdown."""
|
|
2113
|
-
result = await _wa_rpc_call("stop", {})
|
|
2114
|
-
return result
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
async def handle_whatsapp_diagnostics(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
2118
|
-
"""Get WhatsApp diagnostics/debug info."""
|
|
2119
|
-
result = await _wa_rpc_call("diagnostics", {})
|
|
2120
|
-
return result
|
|
1472
|
+
# ----------------------------------------------------------------------------
|
|
1473
|
+
# Per-provider credential validators (Maps geocode probe, Apify /users/me,
|
|
1474
|
+
# Ollama / LM Studio SDK probes) live on their plugins Credential
|
|
1475
|
+
# subclass via the shared Credential._probe / Credential.validate scaffold
|
|
1476
|
+
# in services/plugin/credential.py. handle_validate_api_key (above) is a
|
|
1477
|
+
# pure dispatch through CREDENTIAL_REGISTRY no per-provider branches in
|
|
1478
|
+
# this router.
|
|
1479
|
+
# ----------------------------------------------------------------------------
|
|
2121
1480
|
|
|
2122
1481
|
|
|
2123
1482
|
# ============================================================================
|
|
@@ -2736,17 +2095,25 @@ async def handle_clear_memory(data: Dict[str, Any], websocket: WebSocket) -> Dic
|
|
|
2736
2095
|
|
|
2737
2096
|
Business logic lives in :func:`services.memory.clear_agent_session_state`
|
|
2738
2097
|
— this handler only decodes the request and shapes the response.
|
|
2098
|
+
|
|
2099
|
+
When ``memory_node_id`` is provided (claude_code_agent JSONL bridge
|
|
2100
|
+
surface), the simpleMemory node's ``memory_content`` is reset and
|
|
2101
|
+
``memory_jsonl`` + ``last_session_id`` are wiped server-side. Legacy
|
|
2102
|
+
callers that omit it still get ``default_content`` for the
|
|
2103
|
+
frontend's existing markdown reset path.
|
|
2739
2104
|
"""
|
|
2740
2105
|
from services.memory import clear_agent_session_state
|
|
2741
2106
|
|
|
2742
2107
|
session_id = data.get("session_id", "default")
|
|
2743
2108
|
workflow_id = data.get("workflow_id")
|
|
2744
2109
|
clear_long_term = data.get("clear_long_term", False)
|
|
2110
|
+
memory_node_id = data.get("memory_node_id")
|
|
2745
2111
|
|
|
2746
|
-
cleared = clear_agent_session_state(
|
|
2112
|
+
cleared = await clear_agent_session_state(
|
|
2747
2113
|
session_id=session_id,
|
|
2748
2114
|
workflow_id=workflow_id,
|
|
2749
2115
|
clear_long_term=clear_long_term,
|
|
2116
|
+
memory_node_id=memory_node_id,
|
|
2750
2117
|
)
|
|
2751
2118
|
|
|
2752
2119
|
return {
|
|
@@ -2754,6 +2121,7 @@ async def handle_clear_memory(data: Dict[str, Any], websocket: WebSocket) -> Dic
|
|
|
2754
2121
|
"default_content": "# Conversation History\n\n*No messages yet.*\n",
|
|
2755
2122
|
"cleared_vector_store": cleared["cleared_vector_store"],
|
|
2756
2123
|
"cleared_todo_keys": cleared["cleared_todo_keys"],
|
|
2124
|
+
"cleared_memory_node": cleared["cleared_memory_node"],
|
|
2757
2125
|
"session_id": session_id,
|
|
2758
2126
|
}
|
|
2759
2127
|
|
|
@@ -3326,48 +2694,19 @@ MESSAGE_HANDLERS: Dict[str, MessageHandler] = {
|
|
|
3326
2694
|
"claude_oauth_status": handle_claude_oauth_status,
|
|
3327
2695
|
|
|
3328
2696
|
# Twitter OAuth operations
|
|
3329
|
-
"twitter_oauth_login": handle_twitter_oauth_login,
|
|
3330
|
-
"twitter_oauth_status": handle_twitter_oauth_status,
|
|
3331
|
-
"twitter_logout": handle_twitter_logout,
|
|
3332
2697
|
|
|
3333
2698
|
# Google Workspace OAuth operations
|
|
3334
|
-
"google_oauth_login": handle_google_oauth_login,
|
|
3335
|
-
"google_oauth_status": handle_google_oauth_status,
|
|
3336
|
-
"google_logout": handle_google_logout,
|
|
3337
2699
|
|
|
3338
2700
|
# Android operations
|
|
3339
|
-
"get_android_devices": handle_get_android_devices,
|
|
3340
|
-
"execute_android_action": handle_execute_android_action,
|
|
3341
|
-
"android_relay_connect": handle_android_relay_connect,
|
|
3342
|
-
"android_relay_disconnect": handle_android_relay_disconnect,
|
|
3343
|
-
"android_relay_reconnect": handle_android_relay_reconnect,
|
|
3344
|
-
|
|
3345
|
-
# Maps operations
|
|
3346
|
-
"validate_maps_key": handle_validate_maps_key,
|
|
3347
2701
|
|
|
3348
|
-
# Apify
|
|
3349
|
-
|
|
2702
|
+
# Maps + Apify validation now flow through ``handle_validate_api_key``
|
|
2703
|
+
# (which dispatches via ``CREDENTIAL_REGISTRY`` to
|
|
2704
|
+
# ``GoogleMapsCredential._probe`` / ``ApifyCredential._probe``).
|
|
2705
|
+
# The legacy ``validate_maps_key`` / ``validate_apify_key`` WS message
|
|
2706
|
+
# types are no longer needed — the frontend already uses
|
|
2707
|
+
# ``validate_api_key`` for all providers.
|
|
3350
2708
|
|
|
3351
2709
|
# WhatsApp operations
|
|
3352
|
-
"whatsapp_status": handle_whatsapp_status,
|
|
3353
|
-
"whatsapp_connected_phone": handle_whatsapp_connected_phone,
|
|
3354
|
-
"whatsapp_qr": handle_whatsapp_qr,
|
|
3355
|
-
"whatsapp_send": handle_whatsapp_send,
|
|
3356
|
-
"whatsapp_start": handle_whatsapp_start,
|
|
3357
|
-
"whatsapp_restart": handle_whatsapp_restart,
|
|
3358
|
-
"whatsapp_groups": handle_whatsapp_groups,
|
|
3359
|
-
"whatsapp_newsletters": handle_whatsapp_newsletters,
|
|
3360
|
-
"whatsapp_group_info": handle_whatsapp_group_info,
|
|
3361
|
-
"whatsapp_chat_history": handle_whatsapp_chat_history,
|
|
3362
|
-
"whatsapp_rate_limit_get": handle_whatsapp_rate_limit_get,
|
|
3363
|
-
"whatsapp_rate_limit_set": handle_whatsapp_rate_limit_set,
|
|
3364
|
-
"whatsapp_rate_limit_stats": handle_whatsapp_rate_limit_stats,
|
|
3365
|
-
"whatsapp_rate_limit_unpause": handle_whatsapp_rate_limit_unpause,
|
|
3366
|
-
"whatsapp_mark_read": handle_whatsapp_mark_read,
|
|
3367
|
-
"whatsapp_typing": handle_whatsapp_typing,
|
|
3368
|
-
"whatsapp_presence": handle_whatsapp_presence,
|
|
3369
|
-
"whatsapp_stop": handle_whatsapp_stop,
|
|
3370
|
-
"whatsapp_diagnostics": handle_whatsapp_diagnostics,
|
|
3371
2710
|
|
|
3372
2711
|
# Telegram operations live in nodes/telegram/_handlers.py and
|
|
3373
2712
|
# self-register via services.ws_handler_registry. Dispatch hits them
|