@johpaz/hive-agents 0.0.35 → 0.0.37
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 +64 -39
- package/dist/hive.js +3231 -3189
- package/dist/tool-worker.js +218406 -0
- package/dist/ui/assets/{AgentCreateForm-B4eK7efF.js → AgentCreateForm-tJZv9FZC.js} +1 -1
- package/dist/ui/assets/{AgentDetailPage-BD2uoJWk.js → AgentDetailPage-Du-mRcAX.js} +1 -1
- package/dist/ui/assets/AgentNewPage-DIFYd_Ys.js +1 -0
- package/dist/ui/assets/{AgentsPage-4JUZXvkA.js → AgentsPage-YvSgWRiw.js} +6 -6
- package/dist/ui/assets/CanvasPage-DtMwGvxf.js +33 -0
- package/dist/ui/assets/{ChannelsPage-BUn7-nhV.js → ChannelsPage-BdBXWHjj.js} +1 -1
- package/dist/ui/assets/DashboardPage-ghl1ZguH.js +6 -0
- package/dist/ui/assets/{LoginPage-C8j_urUD.js → LoginPage-CAmSI9Vy.js} +1 -1
- package/dist/ui/assets/LogsPage-DAPBHkwK.js +1 -0
- package/dist/ui/assets/MeetingPage-WjjGOqqU.js +1 -0
- package/dist/ui/assets/{NotFound-Drh-sJPN.js → NotFound-BMeQSGcG.js} +1 -1
- package/dist/ui/assets/ProvidersPage-Ct6HsAi1.js +1 -0
- package/dist/ui/assets/{RecoverPage-DNb1Pr8h.js → RecoverPage-DpW3l-yv.js} +1 -1
- package/dist/ui/assets/SettingsPage-DBJ7_E6C.js +9 -0
- package/dist/ui/assets/SetupPage-DKmLVUaj.js +1 -0
- package/dist/ui/assets/{WebChatPage-R-YOwA4F.js → WebChatPage-CVRcKept.js} +2 -2
- package/dist/ui/assets/accordion-C5d5Rm5z.js +1 -0
- package/dist/ui/assets/{alert-U8FsgWi7.js → alert-C-NE-P3s.js} +1 -1
- package/dist/ui/assets/{alert-dialog-CRdMkkmk.js → alert-dialog-C5mzbHdP.js} +1 -1
- package/dist/ui/assets/{badge-Cli1jnH5.js → badge-ChpACfWO.js} +1 -1
- package/dist/ui/assets/chevron-up-BYhk0K2J.js +1 -0
- package/dist/ui/assets/{dialog-DQ3s-LuO.js → dialog-QnZ0ad8O.js} +1 -1
- package/dist/ui/assets/dropdown-menu-BK-CO3Od.js +1 -0
- package/dist/ui/assets/{es-DcMjrpbA.js → es-NQNoaWDx.js} +1 -1
- package/dist/ui/assets/index-B2fCYtTS.css +2 -0
- package/dist/ui/assets/index-DMCjjdqf.js +116 -0
- package/dist/ui/assets/{label-0BvGVXvZ.js → label-D2H1IR_J.js} +1 -1
- package/dist/ui/assets/progress-BherYzY6.js +1 -0
- package/dist/ui/assets/scroll-area-DkeyX32e.js +1 -0
- package/dist/ui/assets/{slider-D47dOrRa.js → slider-CsiUDxc3.js} +1 -1
- package/dist/ui/assets/switch-BDwN8RYV.js +1 -0
- package/dist/ui/assets/{table-DhowbNxQ.js → table-CSc8ubon.js} +1 -1
- package/dist/ui/assets/terminal-DN38Q456.js +1 -0
- package/dist/ui/assets/useProviders-C6_QHsEi.js +1 -0
- package/dist/ui/assets/{vendor-radix-JY4ncZrD.js → vendor-radix-cw1bQaVC.js} +4 -4
- package/dist/ui/assets/{vendor-react-CscwQerf.js → vendor-react-D4s9E-zj.js} +1 -1
- package/dist/ui/dist/assets/AgentCreateForm-tJZv9FZC.js +1 -0
- package/dist/ui/dist/assets/AgentDetailPage-Du-mRcAX.js +1 -0
- package/dist/ui/dist/assets/AgentNewPage-DIFYd_Ys.js +1 -0
- package/dist/ui/dist/assets/AgentsPage-YvSgWRiw.js +10 -0
- package/dist/ui/dist/assets/CanvasPage-DtMwGvxf.js +33 -0
- package/dist/ui/dist/assets/ChannelsPage-BdBXWHjj.js +8 -0
- package/dist/ui/dist/assets/DashboardPage-ghl1ZguH.js +6 -0
- package/dist/ui/dist/assets/LoginPage-CAmSI9Vy.js +1 -0
- package/dist/ui/dist/assets/LogsPage-DAPBHkwK.js +1 -0
- package/dist/ui/dist/assets/MeetingPage-WjjGOqqU.js +1 -0
- package/dist/ui/dist/assets/NotFound-BMeQSGcG.js +1 -0
- package/dist/ui/dist/assets/ProvidersPage-Ct6HsAi1.js +1 -0
- package/dist/ui/dist/assets/RecoverPage-DpW3l-yv.js +1 -0
- package/dist/ui/dist/assets/SettingsPage-DBJ7_E6C.js +9 -0
- package/dist/ui/dist/assets/SetupPage-DKmLVUaj.js +1 -0
- package/dist/ui/dist/assets/WebChatPage-CVRcKept.js +16 -0
- package/dist/ui/dist/assets/accordion-C5d5Rm5z.js +1 -0
- package/dist/ui/dist/assets/activity-c3pNngT_.js +1 -0
- package/dist/ui/dist/assets/alert-C-NE-P3s.js +1 -0
- package/dist/ui/dist/assets/alert-dialog-C5mzbHdP.js +1 -0
- package/dist/ui/dist/assets/arrow-left-CBcbX5EZ.js +1 -0
- package/dist/ui/dist/assets/badge-ChpACfWO.js +1 -0
- package/dist/ui/dist/assets/calendar-B-KZ9RQO.js +1 -0
- package/dist/ui/dist/assets/card-CNf6BS2e.js +1 -0
- package/dist/ui/dist/assets/chevron-left-D4U-5A27.js +1 -0
- package/dist/ui/dist/assets/chevron-right-CR4Skrf3.js +1 -0
- package/dist/ui/dist/assets/chevron-up-BYhk0K2J.js +1 -0
- package/dist/ui/dist/assets/circle-alert-CyHDwUj8.js +1 -0
- package/dist/ui/dist/assets/circle-check-Bb54Ebmu.js +1 -0
- package/dist/ui/dist/assets/cpu-Cdgc_B1K.js +1 -0
- package/dist/ui/dist/assets/dialog-QnZ0ad8O.js +1 -0
- package/dist/ui/dist/assets/download-C3ifGMjJ.js +1 -0
- package/dist/ui/dist/assets/dropdown-menu-BK-CO3Od.js +1 -0
- package/dist/ui/dist/assets/es-NQNoaWDx.js +1 -0
- package/dist/ui/dist/assets/external-link-BvxYeTP1.js +1 -0
- package/dist/ui/dist/assets/eye-DqNTU_GD.js +1 -0
- package/dist/ui/dist/assets/file-text-BT_9S9SM.js +1 -0
- package/dist/ui/dist/assets/folder-open-BhH8y9ac.js +1 -0
- package/dist/ui/dist/assets/format-GVHeOyWI.js +1 -0
- package/dist/ui/dist/assets/gateway-url-COCbW0IR.js +1 -0
- package/dist/ui/dist/assets/gauge-D_TMa4i9.js +1 -0
- package/dist/ui/dist/assets/globe-DeCQTCDJ.js +1 -0
- package/dist/ui/dist/assets/hexagon-DsGOUl-H.js +1 -0
- package/dist/ui/dist/assets/history-BSG-Ypqf.js +1 -0
- package/dist/ui/dist/assets/index-B2fCYtTS.css +2 -0
- package/dist/ui/dist/assets/index-DMCjjdqf.js +116 -0
- package/dist/ui/dist/assets/info-NwLoa2Mj.js +1 -0
- package/dist/ui/dist/assets/key-3EP0dhkT.js +1 -0
- package/dist/ui/dist/assets/label-D2H1IR_J.js +1 -0
- package/dist/ui/dist/assets/loader-circle-CZNax6kS.js +1 -0
- package/dist/ui/dist/assets/lock-Ei1_J-Nq.js +1 -0
- package/dist/ui/dist/assets/pause-BUqah9Bi.js +1 -0
- package/dist/ui/dist/assets/play-NcZ4swwL.js +1 -0
- package/dist/ui/dist/assets/plus-CX1xyhp5.js +1 -0
- package/dist/ui/dist/assets/progress-BherYzY6.js +1 -0
- package/dist/ui/dist/assets/refresh-cw-DaYdjQFk.js +1 -0
- package/dist/ui/dist/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
- package/dist/ui/dist/assets/save-CUdYyHNy.js +1 -0
- package/dist/ui/dist/assets/scroll-area-DkeyX32e.js +1 -0
- package/dist/ui/dist/assets/send-B0H5SEIE.js +1 -0
- package/dist/ui/dist/assets/settings-Ds4SqD8s.js +1 -0
- package/dist/ui/dist/assets/slider-CsiUDxc3.js +14 -0
- package/dist/ui/dist/assets/sparkles-yUEb-7oH.js +1 -0
- package/dist/ui/dist/assets/square-BD81nFtN.js +1 -0
- package/dist/ui/dist/assets/switch-BDwN8RYV.js +1 -0
- package/dist/ui/dist/assets/table-CSc8ubon.js +1 -0
- package/dist/ui/dist/assets/terminal-DN38Q456.js +1 -0
- package/dist/ui/dist/assets/textarea-CXgXWKrT.js +1 -0
- package/dist/ui/dist/assets/trash-2-CNjMkoq6.js +1 -0
- package/dist/ui/dist/assets/triangle-alert-C9Y8Ub4X.js +1 -0
- package/dist/ui/dist/assets/useProviders-C6_QHsEi.js +1 -0
- package/dist/ui/dist/assets/utils-3pnRFmFe.js +1 -0
- package/dist/ui/dist/assets/vendor-charts-Bu2lyBKP.js +65 -0
- package/dist/ui/dist/assets/vendor-query-DsWPbQdG.js +1 -0
- package/dist/ui/dist/assets/vendor-radix-cw1bQaVC.js +63 -0
- package/dist/ui/dist/assets/vendor-react-D4s9E-zj.js +1 -0
- package/dist/ui/dist/assets/vendor-router-C9pIYwbJ.js +3 -0
- package/dist/ui/dist/assets/volume-2-CeSXNDv4.js +1 -0
- package/dist/ui/dist/assets/zap-hlXjpSeA.js +1 -0
- package/dist/ui/dist/favicon.ico +0 -0
- package/dist/ui/dist/index.html +40 -0
- package/dist/ui/dist/placeholder.svg +1 -0
- package/dist/ui/index.html +6 -6
- package/package.json +138 -13
- package/packages/cli/src/adapters/binary.ts +461 -0
- package/packages/cli/src/adapters/bun-global.ts +378 -0
- package/packages/cli/src/adapters/config.ts +314 -0
- package/packages/cli/src/adapters/docker.ts +308 -0
- package/packages/cli/src/adapters/factory.ts +168 -0
- package/packages/cli/src/adapters/index.ts +80 -0
- package/packages/cli/src/adapters/types.ts +218 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +49 -0
- package/packages/cli/src/commands/cron.ts +487 -0
- package/packages/cli/src/commands/dev.ts +58 -0
- package/packages/cli/src/commands/doctor.ts +320 -0
- package/packages/cli/src/commands/gateway.ts +719 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/migrate.ts +90 -0
- package/packages/cli/src/commands/onboard.ts +1656 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +215 -0
- package/packages/cli/src/commands/update.ts +203 -0
- package/packages/cli/src/index.ts +210 -0
- package/packages/cli/src/ui-bundle.generated.ts +3 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/core/src/agent/agent-loop.ts +691 -0
- package/packages/core/src/agent/compaction.ts +240 -0
- package/packages/core/src/agent/context-compiler.ts +467 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/conversation-store.ts +244 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/llm-client.ts +167 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +212 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/llm-providers/gemini.ts +215 -0
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/interface.ts +195 -0
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/local-llama.ts +37 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/llm-providers/ollama.ts +175 -0
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +379 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/native-tools.ts +31 -0
- package/packages/core/src/agent/playbook-selector.ts +147 -0
- package/packages/core/src/agent/prompt-builder.ts +169 -0
- package/packages/core/src/agent/providers/index.ts +204 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +200 -0
- package/packages/core/src/agent/service.ts +267 -0
- package/packages/core/src/agent/skill-selector.ts +479 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/tool-selector.ts +569 -0
- package/packages/core/src/agent/tracer.ts +100 -0
- package/packages/core/src/auth/auth.ts +108 -0
- package/packages/core/src/auth/index.ts +1 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-manager.ts +390 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +3 -0
- package/packages/core/src/channels/base.ts +154 -0
- package/packages/core/src/channels/discord.ts +273 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +450 -0
- package/packages/core/src/channels/slack.ts +323 -0
- package/packages/core/src/channels/telegram.ts +612 -0
- package/packages/core/src/channels/webchat.ts +139 -0
- package/packages/core/src/channels/whatsapp.ts +548 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +569 -0
- package/packages/core/src/events/agent-bus.ts +460 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/channel-notify.ts +64 -0
- package/packages/core/src/gateway/helpers/cors.ts +32 -0
- package/packages/core/src/gateway/helpers/index.ts +4 -0
- package/packages/core/src/gateway/helpers/narration.ts +57 -0
- package/packages/core/src/gateway/helpers/path.ts +13 -0
- package/packages/core/src/gateway/helpers/redact.ts +61 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +363 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/llm-local/client.ts +94 -0
- package/packages/core/src/gateway/llm-local/detector.ts +321 -0
- package/packages/core/src/gateway/llm-local/downloader.ts +216 -0
- package/packages/core/src/gateway/llm-local/index.ts +34 -0
- package/packages/core/src/gateway/llm-local/manager.ts +186 -0
- package/packages/core/src/gateway/llm-local/models.ts +149 -0
- package/packages/core/src/gateway/llm-local/server.ts +179 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/routes/agents.ts +210 -0
- package/packages/core/src/gateway/routes/auth.ts +244 -0
- package/packages/core/src/gateway/routes/channels.ts +484 -0
- package/packages/core/src/gateway/routes/chat.ts +241 -0
- package/packages/core/src/gateway/routes/config.ts +12 -0
- package/packages/core/src/gateway/routes/cron-api.ts +544 -0
- package/packages/core/src/gateway/routes/ethics.ts +46 -0
- package/packages/core/src/gateway/routes/llm-local.ts +271 -0
- package/packages/core/src/gateway/routes/mcp.ts +319 -0
- package/packages/core/src/gateway/routes/meeting.ts +232 -0
- package/packages/core/src/gateway/routes/models.ts +163 -0
- package/packages/core/src/gateway/routes/multimodal.ts +93 -0
- package/packages/core/src/gateway/routes/providers.ts +220 -0
- package/packages/core/src/gateway/routes/setup.ts +441 -0
- package/packages/core/src/gateway/routes/skills.ts +115 -0
- package/packages/core/src/gateway/routes/system.ts +469 -0
- package/packages/core/src/gateway/routes/tasks.ts +44 -0
- package/packages/core/src/gateway/routes/tools.ts +59 -0
- package/packages/core/src/gateway/routes/tts-local.ts +388 -0
- package/packages/core/src/gateway/routes/users.ts +122 -0
- package/packages/core/src/gateway/routes/voice.ts +189 -0
- package/packages/core/src/gateway/routes/workspace.ts +281 -0
- package/packages/core/src/gateway/server.ts +2744 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +207 -0
- package/packages/core/src/gateway/tts/README.md +94 -0
- package/packages/core/src/gateway/tts/package.json +25 -0
- package/packages/core/src/gateway/tts/src/client.ts +59 -0
- package/packages/core/src/gateway/tts/src/detect.ts +42 -0
- package/packages/core/src/gateway/tts/src/index.ts +15 -0
- package/packages/core/src/gateway/tts/src/install.ts +129 -0
- package/packages/core/src/gateway/tts/src/models.ts +50 -0
- package/packages/core/src/gateway/tts/src/server.ts +252 -0
- package/packages/core/src/gateway/tts/voices/.gitkeep +0 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +56 -0
- package/packages/core/src/mcp/hot-reload.ts +148 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/tool-sync.ts +176 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/multimodal/vision-service.ts +283 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/scheduler/CronScheduler.ts +699 -0
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
- package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
- package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
- package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
- package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
- package/packages/core/src/scheduler/dag/errors.ts +37 -0
- package/packages/core/src/scheduler/dag/index.ts +26 -0
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/scheduler/index.ts +22 -0
- package/packages/core/src/scheduler/integration.ts +237 -0
- package/packages/core/src/scheduler/types.ts +164 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/crypto.ts +197 -0
- package/packages/core/src/storage/migrate.ts +147 -0
- package/packages/core/src/storage/onboarding.ts +1506 -0
- package/packages/core/src/storage/schema.ts +666 -0
- package/packages/core/src/storage/seed.ts +628 -0
- package/packages/core/src/storage/sqlite.ts +407 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/tool-runtime/index.ts +502 -0
- package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +610 -0
- package/packages/core/src/tools/canvas/index.ts +420 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/core/index.ts +478 -0
- package/packages/core/src/tools/cron/index.ts +635 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +197 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +5 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +643 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +47 -0
- package/packages/mcp/src/manager.ts +439 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +238 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/src/bundled/agents/agent_spawner/SKILL.md +167 -0
- package/packages/skills/src/bundled/agents/code_delegator/SKILL.md +156 -0
- package/packages/skills/src/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/skills/src/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/skills/src/bundled/agents/task_orchestrator/SKILL.md +198 -0
- package/packages/skills/src/bundled/canvas/a2ui_dashboard/SKILL.md +176 -0
- package/packages/skills/src/bundled/canvas/a2ui_form/SKILL.md +202 -0
- package/packages/skills/src/bundled/canvas/a2ui_interactive/SKILL.md +206 -0
- package/packages/skills/src/bundled/canvas/canvas_dashboard/SKILL.md +146 -0
- package/packages/skills/src/bundled/canvas/canvas_interact/SKILL.md +148 -0
- package/packages/skills/src/bundled/canvas/canvas_report/SKILL.md +146 -0
- package/packages/skills/src/bundled/cli/cli_pipeline/SKILL.md +136 -0
- package/packages/skills/src/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +188 -0
- package/packages/skills/src/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/skills/src/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/skills/src/bundled/filesystem/file_read_and_summarize/SKILL.md +108 -0
- package/packages/skills/src/bundled/filesystem/file_writer/SKILL.md +135 -0
- package/packages/skills/src/bundled/meeting/meeting_transcription/SKILL.md +213 -0
- package/packages/skills/src/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/skills/src/bundled/search_knowledge/busqueda_fts5/SKILL.md +74 -0
- package/packages/skills/src/bundled/voice/voice_assistant/SKILL.md +174 -0
- package/packages/skills/src/bundled/voice/voice_input/SKILL.md +146 -0
- package/packages/skills/src/bundled/voice/voice_output/SKILL.md +151 -0
- package/packages/skills/src/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/skills/src/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/skills/src/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/skills/src/bundled/web/web_research/SKILL.md +119 -0
- package/packages/skills/src/bundled-data.generated.ts +1964 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +388 -0
- package/dist/ui/assets/AgentNewPage-GB-tVN50.js +0 -1
- package/dist/ui/assets/BridgePage-DDcDILKu.js +0 -1
- package/dist/ui/assets/CanvasPage-oOk2sGOD.js +0 -33
- package/dist/ui/assets/DashboardPage-DV_2qWYJ.js +0 -6
- package/dist/ui/assets/LogsPage-DayYjh01.js +0 -1
- package/dist/ui/assets/MeetingPage-C01uPuqj.js +0 -1
- package/dist/ui/assets/ProjectsPage-B8_am_Ib.js +0 -1
- package/dist/ui/assets/ProvidersPage-DBzi66e4.js +0 -1
- package/dist/ui/assets/SettingsPage-CFA_Tknl.js +0 -9
- package/dist/ui/assets/SetupPage-BrUWbhvT.js +0 -1
- package/dist/ui/assets/accordion-DdAEfIXR.js +0 -1
- package/dist/ui/assets/chevron-down-DIosfU_U.js +0 -1
- package/dist/ui/assets/chevron-up-CI-W21Fy.js +0 -1
- package/dist/ui/assets/circle-S0-ouLz-.js +0 -1
- package/dist/ui/assets/circle-minus-CE0iJrl8.js +0 -1
- package/dist/ui/assets/circle-x-jUJ5zZvQ.js +0 -1
- package/dist/ui/assets/dropdown-menu-C2CXM1VE.js +0 -1
- package/dist/ui/assets/index-BN0875JH.css +0 -2
- package/dist/ui/assets/index-CH6sBa3Q.js +0 -116
- package/dist/ui/assets/pencil-5VdSj-h5.js +0 -1
- package/dist/ui/assets/progress-JN30I5fF.js +0 -1
- package/dist/ui/assets/scroll-area-BQQPitM8.js +0 -1
- package/dist/ui/assets/search-ChPgnVKj.js +0 -1
- package/dist/ui/assets/switch-C7W2-KEx.js +0 -1
- package/dist/ui/assets/terminal-C-R5Fckz.js +0 -1
- package/dist/ui/assets/useProviders-TBnWn-Hq.js +0 -1
- /package/dist/ui/assets/{card-DFKnZ6ky.js → card-CNf6BS2e.js} +0 -0
- /package/dist/ui/assets/{circle-alert-KuAm2FWh.js → circle-alert-CyHDwUj8.js} +0 -0
- /package/dist/ui/assets/{circle-check-6Ard1-2z.js → circle-check-Bb54Ebmu.js} +0 -0
- /package/dist/ui/assets/{cpu-KDy6-FAI.js → cpu-Cdgc_B1K.js} +0 -0
- /package/dist/ui/assets/{download-Cjbk4Rek.js → download-C3ifGMjJ.js} +0 -0
- /package/dist/ui/assets/{external-link-HtrFM63g.js → external-link-BvxYeTP1.js} +0 -0
- /package/dist/ui/assets/{eye-D1dB40_o.js → eye-DqNTU_GD.js} +0 -0
- /package/dist/ui/assets/{file-text-CE58EfH0.js → file-text-BT_9S9SM.js} +0 -0
- /package/dist/ui/assets/{folder-open-DIPKeiI_.js → folder-open-BhH8y9ac.js} +0 -0
- /package/dist/ui/assets/{format-BwdV8bB5.js → format-GVHeOyWI.js} +0 -0
- /package/dist/ui/assets/{gateway-url-D5uj6Nxg.js → gateway-url-COCbW0IR.js} +0 -0
- /package/dist/ui/assets/{gauge-DmQmJHEg.js → gauge-D_TMa4i9.js} +0 -0
- /package/dist/ui/assets/{globe-_hUGxQF4.js → globe-DeCQTCDJ.js} +0 -0
- /package/dist/ui/assets/{hexagon-BaNGQlQj.js → hexagon-DsGOUl-H.js} +0 -0
- /package/dist/ui/assets/{history-BfZVGlZa.js → history-BSG-Ypqf.js} +0 -0
- /package/dist/ui/assets/{info-CBZ5-AlC.js → info-NwLoa2Mj.js} +0 -0
- /package/dist/ui/assets/{key-Bv5DdTPh.js → key-3EP0dhkT.js} +0 -0
- /package/dist/ui/assets/{loader-circle-C4hhXLgp.js → loader-circle-CZNax6kS.js} +0 -0
- /package/dist/ui/assets/{lock-CkZYexqw.js → lock-Ei1_J-Nq.js} +0 -0
- /package/dist/ui/assets/{pause-Bpy1_s7y.js → pause-BUqah9Bi.js} +0 -0
- /package/dist/ui/assets/{play-Cj4osqJZ.js → play-NcZ4swwL.js} +0 -0
- /package/dist/ui/assets/{plus-BQhgZN3A.js → plus-CX1xyhp5.js} +0 -0
- /package/dist/ui/assets/{refresh-cw-BfREHVQM.js → refresh-cw-DaYdjQFk.js} +0 -0
- /package/dist/ui/assets/{save-FFTD4dMp.js → save-CUdYyHNy.js} +0 -0
- /package/dist/ui/assets/{settings-BdHKUL92.js → settings-Ds4SqD8s.js} +0 -0
- /package/dist/ui/assets/{sparkles-r4uJbJAl.js → sparkles-yUEb-7oH.js} +0 -0
- /package/dist/ui/assets/{square-G7Hyufqm.js → square-BD81nFtN.js} +0 -0
- /package/dist/ui/assets/{textarea-5kyuD04X.js → textarea-CXgXWKrT.js} +0 -0
- /package/dist/ui/assets/{trash-2-DXVBRWfh.js → trash-2-CNjMkoq6.js} +0 -0
- /package/dist/ui/assets/{triangle-alert-Bu5seg9O.js → triangle-alert-C9Y8Ub4X.js} +0 -0
- /package/dist/ui/assets/{vendor-router-CCECILJ0.js → vendor-router-C9pIYwbJ.js} +0 -0
- /package/dist/ui/assets/{volume-2-s9DuS696.js → volume-2-CeSXNDv4.js} +0 -0
- /package/dist/ui/assets/{zap-BPHZzXKV.js → zap-hlXjpSeA.js} +0 -0
|
@@ -0,0 +1,2744 @@
|
|
|
1
|
+
import type { Config } from "../config/loader";
|
|
2
|
+
import { loadConfig, getHiveDir } from "../config/loader";
|
|
3
|
+
import { logger, onLogEntry } from "../utils/logger";
|
|
4
|
+
import { sessionManager, parseSessionId } from "./session";
|
|
5
|
+
import { laneQueue } from "./lane-queue";
|
|
6
|
+
import {
|
|
7
|
+
type InboundMessage,
|
|
8
|
+
type OutboundMessage,
|
|
9
|
+
isSlashCommand,
|
|
10
|
+
executeSlashCommand,
|
|
11
|
+
} from "./slash-commands";
|
|
12
|
+
import { ChannelManager } from "../channels/manager";
|
|
13
|
+
import { AgentService } from "../agent/service";
|
|
14
|
+
import { AgentRunner } from "../agent/providers/index";
|
|
15
|
+
import type { IncomingMessage } from "../channels/base";
|
|
16
|
+
import { mkdirSync, rmSync, unlinkSync, watch, existsSync, writeFileSync, readFileSync } from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
|
|
19
|
+
// Read version from package.json at module load time
|
|
20
|
+
const _pkgVersion = (() => {
|
|
21
|
+
try {
|
|
22
|
+
const pkgPath = path.join(import.meta.dir, "../../../package.json");
|
|
23
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version as string;
|
|
24
|
+
} catch {
|
|
25
|
+
return "0.0.27";
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
28
|
+
import { cpus as osCpus } from "node:os";
|
|
29
|
+
import { getDb, getDbPathLazy, initializeDatabase } from "../storage/sqlite";
|
|
30
|
+
import { seedAllData } from "../storage/seed";
|
|
31
|
+
import { canvasManager } from "../canvas/canvas-manager.ts";
|
|
32
|
+
import { subscribeCanvas, unsubscribeCanvas, emitCanvas, getCanvasSnapshot, removeCanvasComponent } from "../canvas/emitter";
|
|
33
|
+
import { resolveCanvasInteraction } from "../tools/canvas/index";
|
|
34
|
+
import { randomUUID } from "crypto";
|
|
35
|
+
import { legacyDecryptAES, loadMcpHeaders } from "../storage/crypto.ts";
|
|
36
|
+
import { resolveContext } from "./resolver";
|
|
37
|
+
import { voiceService } from "../voice/index";
|
|
38
|
+
import { multimodalService } from "../multimodal/index";
|
|
39
|
+
import { initializeGateway, type GatewayInitializationResult } from "./initializer";
|
|
40
|
+
import { handleSetupStatus, handleVerifyProvider, handleCompleteSetup, handleSetupProviders, handleSetupEthics, handleSetupOllamaModels } from "./routes/setup";
|
|
41
|
+
import { handleAuthStatus, handleLogin, handleSetupCredentials, handleChangePassword, handleRecover, handleDisableAuth, handleRecoveryKey } from "./routes/auth";
|
|
42
|
+
import { resolveUserId } from "../storage/onboarding";
|
|
43
|
+
import { handleGetAgents, handleCreateAgent, handleUpdateAgent, handleDeleteAgent } from "./routes/agents";
|
|
44
|
+
import { handleGetProviders, handleCreateProvider, handleToggleProvider, handleUpdateProvider, handleSyncProviderModels } from "./routes/providers";
|
|
45
|
+
import { handleGetUsers, handleCreateUser, handleUpdateUserSettings, handleGetUserChannels, handleLinkUserChannel } from "./routes/users";
|
|
46
|
+
import { handleGetSkills, handleActivateSkill, handleUpdateSkill, handleDeleteSkill, handleCreateSkill } from "./routes/skills";
|
|
47
|
+
import { handleGetEthics, handleActivateEthics, handleDeleteEthics } from "./routes/ethics";
|
|
48
|
+
import { handleGetTools, handleActivateTool, handleUpdateTool } from "./routes/tools";
|
|
49
|
+
import { handleGetTasks, handleUpdateTask } from "./routes/tasks";
|
|
50
|
+
import { setChannelSendFn } from "./channel-notify";
|
|
51
|
+
import { CronScheduler } from "../scheduler/CronScheduler";
|
|
52
|
+
import { DAGScheduler, ParallelStrategy } from "../scheduler/dag/index";
|
|
53
|
+
import { createTaskHandler, setSchedulerForCleanup } from "../scheduler/integration";
|
|
54
|
+
|
|
55
|
+
import { setSchedulerInstance as setScheduleToolsInstance } from "../tools/cron/index.ts";
|
|
56
|
+
import { setSchedulerInstance as setCronApiInstance } from "./routes/cron-api";
|
|
57
|
+
import {
|
|
58
|
+
handleGetCronJobs,
|
|
59
|
+
handleGetCronJob,
|
|
60
|
+
handleCreateCronJob,
|
|
61
|
+
handleUpdateCronJob,
|
|
62
|
+
handleDeleteCronJob,
|
|
63
|
+
handlePauseCronJob,
|
|
64
|
+
handleResumeCronJob,
|
|
65
|
+
handleTriggerCronJob,
|
|
66
|
+
handleGetCronJobHistory,
|
|
67
|
+
handleGetCronStatus,
|
|
68
|
+
handleGetCronChannels,
|
|
69
|
+
} from "./routes/cron-api";
|
|
70
|
+
import { handleGetChannels, handleGetChannelConfig, handleActivateChannel, handleDeactivateChannel, handleCreateChannel, handleGetChannelAccount, handleUpdateChannelAccount, handleDeleteChannelAccount, handleChannelAction, handleUpdateChannelSettings, handleToggleChannel, handleGetChannelStatus, handleReconnectChannel, handleGetWhatsAppDetails, handleDisconnectWhatsApp, handleUpdateWhatsAppConfig } from "./routes/channels";
|
|
71
|
+
import { handleGetMcpServers, handleGetMcpServerDetail, handleCreateMcpServer, handleUpdateMcpServer, handleDeleteMcpServer, handleToggleMcpServer, handleGetMCPServerTools } from "./routes/mcp";
|
|
72
|
+
import { handleGetModels, handleCreateModel, handleToggleModel, handleGetModelsConfig, handleUpdateModelsConfig, handleDeleteModel, handleUpdateModel } from "./routes/models";
|
|
73
|
+
import { handleGetVoiceProviders, handleGetConfiguredVoiceProviders, handleSaveVoiceProviderKey, handleTestVoice, handleGetChannelVoice, handleUpdateChannelVoice, handleGetVoiceProviderVoices } from "./routes/voice";
|
|
74
|
+
import { handleGetVisionProviders, handleGetChannelVision, handleUpdateChannelVision, handleOcrImage } from "./routes/multimodal";
|
|
75
|
+
import { handleGetLocalTTSStatus, handleGetLocalTTSLogs, handleInstallLocalTTS, handleStartLocalTTS, handleStopLocalTTS, handleSpeakLocalTTS, handleGetAvailableModels, handleGetInstalledVoices, handleDownloadModel, handleGetDownloadLogs, initializeLocalTTS } from "./routes/tts-local";
|
|
76
|
+
import { handleGetLocalLLMStatus, handleGetLocalLLMLogs, handleInstallLocalLLM, handleStartLocalLLM, handleStopLocalLLM, handleDownloadLLMModel, initializeLocalLLM } from "./routes/llm-local";
|
|
77
|
+
import { handleCreateMeeting, handleListMeetings, handleGetMeeting, handleAddMeetingSegment, handleStopMeeting } from "./routes/meeting";
|
|
78
|
+
import { handleGetActivityStats, handleGetSystemStats, handleGetUsageStats, handleSystemReload, handleApiReload, handleGetVersion, handleTriggerUpdate } from "./routes/system";
|
|
79
|
+
import { handleGetChatHistory, handleGetCanvas, handleGetNotes, handleUpdateNote } from "./routes/chat";
|
|
80
|
+
import { handleChat as handlePostChat } from "./routes/chat";
|
|
81
|
+
import { handleGetConfig } from "./routes/config";
|
|
82
|
+
import { handleGetWorkspace, handleUpdateWorkspace, handleValidateWorkspace, handleCreateWorkspace, handleOpenWorkspace } from "./routes/workspace";
|
|
83
|
+
import { getNarration, expandPath, addCorsHeaders, CORS_ORIGINS } from "./helpers";
|
|
84
|
+
import { redactConfig } from "./helpers";
|
|
85
|
+
|
|
86
|
+
const logSubscribers = new Set<string>();
|
|
87
|
+
|
|
88
|
+
// Helpers imported from ./helpers/index.ts
|
|
89
|
+
// - getNarration, TOOL_NARRATIONS
|
|
90
|
+
// - expandPath
|
|
91
|
+
// - addCorsHeaders, CORS_ORIGINS
|
|
92
|
+
// - redactConfig, redactValue
|
|
93
|
+
|
|
94
|
+
interface WebSocketData {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
authenticatedAt: number;
|
|
97
|
+
providerId?: string;
|
|
98
|
+
modelId?: string;
|
|
99
|
+
meetingSessionId?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function startGateway(config: Config): Promise<void> {
|
|
103
|
+
const host = config.gateway?.host ?? "127.0.0.1";
|
|
104
|
+
const port = config.gateway?.port ?? 18790;
|
|
105
|
+
const pidFile = expandPath(config.gateway?.pidFile ?? "~/.hive/gateway.pid");
|
|
106
|
+
|
|
107
|
+
// FIX 2 — startTime para calcular uptime en /status y /api/agents
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
|
|
110
|
+
// CPU delta sampling — process.cpuUsage() is cumulative; we diff between calls
|
|
111
|
+
const numCores = osCpus().length || 1;
|
|
112
|
+
let lastCpuSample = process.cpuUsage();
|
|
113
|
+
let lastCpuSampleTime = Date.now();
|
|
114
|
+
const log = logger.child("gateway");
|
|
115
|
+
|
|
116
|
+
log.info(`Starting gateway on ${host}:${port}`);
|
|
117
|
+
|
|
118
|
+
// ── Auto-generate auth token if not provided ─────────────────────────────
|
|
119
|
+
// Priority: HIVE_AUTH_TOKEN env var > persisted token file > generate new
|
|
120
|
+
const tokenFile = path.join(getHiveDir(), ".auth_token");
|
|
121
|
+
if (!process.env.HIVE_AUTH_TOKEN) {
|
|
122
|
+
if (existsSync(tokenFile)) {
|
|
123
|
+
process.env.HIVE_AUTH_TOKEN = readFileSync(tokenFile, "utf-8").trim();
|
|
124
|
+
log.info("🔑 Auth token loaded from persistent storage");
|
|
125
|
+
} else {
|
|
126
|
+
const generated = randomUUID().replace(/-/g, "");
|
|
127
|
+
process.env.HIVE_AUTH_TOKEN = generated;
|
|
128
|
+
mkdirSync(path.dirname(tokenFile), { recursive: true });
|
|
129
|
+
writeFileSync(tokenFile, generated, { mode: 0o600 });
|
|
130
|
+
log.info("🔑 Auth token auto-generated and persisted");
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// User provided token via env — persist it so it's visible in the file too
|
|
134
|
+
writeFileSync(tokenFile, process.env.HIVE_AUTH_TOKEN, { mode: 0o600 });
|
|
135
|
+
log.info("🔑 Auth token loaded from environment variable");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Inicialización modular con manejo de errores ──────────────────────────
|
|
139
|
+
let agent: AgentService;
|
|
140
|
+
let runner: AgentRunner;
|
|
141
|
+
let channelManager: ChannelManager;
|
|
142
|
+
let dbProvider: string;
|
|
143
|
+
let dbModel: string;
|
|
144
|
+
// ── Bind port immediately so parent health-check doesn't timeout ──────────
|
|
145
|
+
// The full handler is loaded via server.reload() once initialization finishes
|
|
146
|
+
let server = Bun.serve<WebSocketData>({
|
|
147
|
+
port,
|
|
148
|
+
hostname: host,
|
|
149
|
+
idleTimeout: 0, // Disable 10s idle timeout — SSE streams can run for minutes
|
|
150
|
+
fetch: (req) => {
|
|
151
|
+
const origin = req.headers.get("Origin") ?? ""
|
|
152
|
+
const isLocalhost = origin.includes("localhost") || origin.includes("127.0.0.1") || origin.includes("0.0.0.0")
|
|
153
|
+
const corsHeaders = isLocalhost ? {
|
|
154
|
+
"Access-Control-Allow-Origin": origin,
|
|
155
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
156
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, X-Requested-With",
|
|
157
|
+
"Access-Control-Allow-Credentials": "true",
|
|
158
|
+
} : {}
|
|
159
|
+
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders })
|
|
160
|
+
const pathname = new URL(req.url).pathname
|
|
161
|
+
if (pathname === "/health" || pathname === "/health/") {
|
|
162
|
+
return Response.json({ status: "starting" }, { headers: corsHeaders })
|
|
163
|
+
}
|
|
164
|
+
return Response.json({ status: "starting" }, { status: 503, headers: corsHeaders })
|
|
165
|
+
},
|
|
166
|
+
websocket: { open() { }, message() { }, close() { } },
|
|
167
|
+
});
|
|
168
|
+
log.info(`Port ${port} bound (initializing gateway...)`);
|
|
169
|
+
|
|
170
|
+
// Inicializar DB siempre (en setup mode crea la DB vacía, los endpoints retornan [] en vez de 500)
|
|
171
|
+
try {
|
|
172
|
+
const db = initializeDatabase();
|
|
173
|
+
// Seed providers/models/hive_capabilities so setup wizard has data before onboarding completes
|
|
174
|
+
seedAllData();
|
|
175
|
+
} catch { /* si falla, los endpoints manejarán el error */ }
|
|
176
|
+
|
|
177
|
+
// Setup mode: no DB file OR DB existe pero tiene 0 usuarios (primera ejecución interrumpida)
|
|
178
|
+
let gatewaySetupMode = false;
|
|
179
|
+
try {
|
|
180
|
+
const count = (getDb().query("SELECT COUNT(*) as count FROM users").get() as { count: number }).count;
|
|
181
|
+
gatewaySetupMode = count === 0;
|
|
182
|
+
} catch {
|
|
183
|
+
gatewaySetupMode = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Usar el inicializador modular para todos los componentes críticos
|
|
188
|
+
const init = await initializeGateway(config, pidFile);
|
|
189
|
+
|
|
190
|
+
agent = init.agent;
|
|
191
|
+
runner = init.runner;
|
|
192
|
+
channelManager = init.channelManager;
|
|
193
|
+
dbProvider = init.provider;
|
|
194
|
+
dbModel = init.model;
|
|
195
|
+
|
|
196
|
+
// Auto-iniciar TTS y LLM local si están instalados
|
|
197
|
+
await initializeLocalTTS();
|
|
198
|
+
await initializeLocalLLM();
|
|
199
|
+
|
|
200
|
+
// Conectar channel-notify singleton para que las tools (notify, report_progress) puedan enviar mensajes
|
|
201
|
+
setChannelSendFn(async (channel, sessionId, content) => {
|
|
202
|
+
await channelManager.send(channel, sessionId, { content, type: "progress" });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (gatewaySetupMode) {
|
|
206
|
+
log.info("🎉 Setup mode: gateway running — open http://localhost:" + port + "/setup to configure");
|
|
207
|
+
} else {
|
|
208
|
+
log.info("✅ Gateway initialization completed successfully");
|
|
209
|
+
|
|
210
|
+
// ── Initialize New Cron Scheduler (Croner-based) ───────────────────────
|
|
211
|
+
try {
|
|
212
|
+
const db = getDb();
|
|
213
|
+
|
|
214
|
+
// Repair orphaned task_runs (status='running' from previous crash)
|
|
215
|
+
db.query(`
|
|
216
|
+
UPDATE task_runs
|
|
217
|
+
SET status = 'timeout', finished_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
|
218
|
+
WHERE status = 'running'
|
|
219
|
+
`).run();
|
|
220
|
+
|
|
221
|
+
// Create and boot scheduler
|
|
222
|
+
const handler = createTaskHandler();
|
|
223
|
+
const scheduler = new CronScheduler(db, handler);
|
|
224
|
+
scheduler.boot();
|
|
225
|
+
|
|
226
|
+
// Register scheduler globally for tools, routes, and internal cleanup
|
|
227
|
+
setScheduleToolsInstance(scheduler);
|
|
228
|
+
setCronApiInstance(scheduler);
|
|
229
|
+
setSchedulerForCleanup(scheduler);
|
|
230
|
+
|
|
231
|
+
log.info(`📅 CronScheduler initialized with ${scheduler.getStatus().length} task(s)`);
|
|
232
|
+
|
|
233
|
+
// Register DAGScheduler as a global service (opt-in by swarms)
|
|
234
|
+
const dagScheduler = new DAGScheduler({ strategy: new ParallelStrategy(), maxConcurrentWorkers: 2 });
|
|
235
|
+
(globalThis as any).__dagScheduler = dagScheduler;
|
|
236
|
+
log.info("🔀 DAGScheduler ready");
|
|
237
|
+
} catch (err) {
|
|
238
|
+
log.error(`❌ CronScheduler initialization failed: ${(err as Error).message}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
log.error(`❌ Gateway initialization failed: ${(error as Error).message}`);
|
|
244
|
+
log.error("Stack trace:", (error as Error).stack);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for insecure binding
|
|
249
|
+
if (host === "0.0.0.0" && config.security?.warnOnInsecureConfig !== false) {
|
|
250
|
+
log.warn("Gateway binding to 0.0.0.0 exposes server to all network interfaces!");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── CRON Handler setup ─────────────────────────────────────────────────────
|
|
254
|
+
function prepareTools(agentInstance: AgentService, sessionId: string) {
|
|
255
|
+
// Tools are now handled by the native agent-loop internally
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Set up hot reload watchers
|
|
260
|
+
const watchers: Array<() => void> = [];
|
|
261
|
+
|
|
262
|
+
// Note: Context store, Ethics, Agent Loop, LLM runner, and Channel Manager
|
|
263
|
+
// are now initialized by initializeGateway() above
|
|
264
|
+
|
|
265
|
+
// Handle messages from channels (Telegram, Discord, WhatsApp, Slack)
|
|
266
|
+
if (!gatewaySetupMode) channelManager.onMessage(async (message: IncomingMessage) => {
|
|
267
|
+
log.info(`📥 Message from ${message.channel}:${message.accountId}`);
|
|
268
|
+
log.info(` Session: ${message.sessionId}`);
|
|
269
|
+
|
|
270
|
+
const voiceConfig = voiceService.getChannelVoiceConfig(message.channel);
|
|
271
|
+
const visionConfig = multimodalService.getChannelVisionConfig(message.channel);
|
|
272
|
+
let messageContent = message.content;
|
|
273
|
+
|
|
274
|
+
let preferAudioResponse = false;
|
|
275
|
+
let inputType: "text" | "audio_transcribed" | "image" | "document" = "text";
|
|
276
|
+
let sttProviderUsed: string | null = null;
|
|
277
|
+
let contentParts: import("../multimodal/types").ContentPart[] | undefined;
|
|
278
|
+
|
|
279
|
+
if (voiceConfig.voiceEnabled && message.audio) {
|
|
280
|
+
log.info(`🎙️ Voice enabled, processing audio...`);
|
|
281
|
+
|
|
282
|
+
if (!voiceConfig.sttProvider) {
|
|
283
|
+
log.warn(`⚠️ STT provider not configured for channel ${message.channel}`);
|
|
284
|
+
await channelManager.send(message.channel, message.sessionId, {
|
|
285
|
+
content: `🎙️ Para usar notas de voz, necesitas configurar el proveedor STT en la configuración del canal. Ve a Configuración > Canales > [Tu canal] y configura "Prov. STT" (ej: groq-whisper o openai)`,
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const audioInput = voiceService.normalizeAudioFromChannel(message.channel, message.audio);
|
|
292
|
+
sttProviderUsed = voiceConfig.sttProvider || "groq-whisper";
|
|
293
|
+
messageContent = await voiceService.transcribe(audioInput, sttProviderUsed);
|
|
294
|
+
log.info(`📝 Transcribed: ${messageContent.substring(0, 100)}...`);
|
|
295
|
+
|
|
296
|
+
inputType = "audio_transcribed";
|
|
297
|
+
// If user sent audio and TTS is available, always respond in audio
|
|
298
|
+
preferAudioResponse = !!voiceConfig.ttsProvider;
|
|
299
|
+
|
|
300
|
+
await channelManager.send(message.channel, message.sessionId, {
|
|
301
|
+
content: `🎙️ Transcripción: ${messageContent}`,
|
|
302
|
+
type: "message"
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
log.error(`❌ Transcription failed: ${(error as Error).message}`);
|
|
306
|
+
await channelManager.send(message.channel, message.sessionId, {
|
|
307
|
+
content: `Error al transcribir audio: ${(error as Error).message}`,
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Multimodal: image/document processing ──
|
|
314
|
+
if (message.image || message.document) {
|
|
315
|
+
log.info(`🖼️ Multimodal content detected on channel ${message.channel}`);
|
|
316
|
+
|
|
317
|
+
if (message.image) {
|
|
318
|
+
try {
|
|
319
|
+
const imageInput = multimodalService.normalizeImageFromChannel(message.channel, message.image);
|
|
320
|
+
const activeModelId = dbModel;
|
|
321
|
+
const activeProviderId = dbProvider;
|
|
322
|
+
const modelHasVision = activeModelId && activeProviderId
|
|
323
|
+
? multimodalService.modelSupportsVision(activeProviderId, activeModelId)
|
|
324
|
+
: false;
|
|
325
|
+
|
|
326
|
+
if (visionConfig.visionEnabled && modelHasVision) {
|
|
327
|
+
contentParts = await multimodalService.processImage(imageInput, visionConfig.visionModelId || undefined);
|
|
328
|
+
inputType = "image";
|
|
329
|
+
log.info(`🖼️ Image sent as vision ContentParts (model supports vision)`);
|
|
330
|
+
} else {
|
|
331
|
+
const ocrProvider = visionConfig.ocrProvider || "openai";
|
|
332
|
+
log.info(`🖼️ Model lacks vision, using OCR via ${ocrProvider}...`);
|
|
333
|
+
const ocrText = await multimodalService.ocrImage(imageInput, ocrProvider);
|
|
334
|
+
messageContent = ocrText
|
|
335
|
+
? `[Imagen adjunta — contenido extraído por OCR]\n${ocrText}\n\n${messageContent || ""}`
|
|
336
|
+
: messageContent || "";
|
|
337
|
+
inputType = "image";
|
|
338
|
+
log.info(`🖼️ OCR result: ${ocrText.substring(0, 100)}...`);
|
|
339
|
+
}
|
|
340
|
+
} catch (imgError) {
|
|
341
|
+
log.error(`❌ Image processing failed: ${(imgError as Error).message}`);
|
|
342
|
+
await channelManager.send(message.channel, message.sessionId, {
|
|
343
|
+
content: `⚠️ Error al procesar la imagen: ${(imgError as Error).message}`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (message.document) {
|
|
349
|
+
try {
|
|
350
|
+
const docInput = multimodalService.normalizeDocumentFromChannel(message.channel, message.document);
|
|
351
|
+
const ocrProvider = visionConfig.ocrProvider || "openai";
|
|
352
|
+
log.info(`📄 Document detected, extracting text via OCR (${ocrProvider})...`);
|
|
353
|
+
const docImage: import("../multimodal/types").ImageInput = {
|
|
354
|
+
type: docInput.type,
|
|
355
|
+
data: docInput.data,
|
|
356
|
+
mimeType: docInput.mimeType,
|
|
357
|
+
caption: docInput.fileName,
|
|
358
|
+
};
|
|
359
|
+
const ocrText = await multimodalService.ocrImage(docImage, ocrProvider);
|
|
360
|
+
messageContent = ocrText
|
|
361
|
+
? `[Documento adjunto: ${docInput.fileName || "unknown"}]\n${ocrText}\n\n${messageContent || ""}`
|
|
362
|
+
: messageContent || "";
|
|
363
|
+
inputType = "document";
|
|
364
|
+
log.info(`📄 Document OCR result: ${ocrText.substring(0, 100)}...`);
|
|
365
|
+
} catch (docError) {
|
|
366
|
+
log.error(`❌ Document processing failed: ${(docError as Error).message}`);
|
|
367
|
+
await channelManager.send(message.channel, message.sessionId, {
|
|
368
|
+
content: `⚠️ Error al procesar el documento: ${(docError as Error).message}`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
log.info(` Content: ${messageContent.substring(0, 150)}${messageContent.length > 150 ? "..." : ""}`);
|
|
375
|
+
|
|
376
|
+
const { userId } = resolveContext({
|
|
377
|
+
channel: message.channel,
|
|
378
|
+
channelUserId: message.sessionId,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const telegramMeta = message.metadata?.telegram as { messageId?: number } | undefined;
|
|
382
|
+
const messageId = telegramMeta?.messageId?.toString();
|
|
383
|
+
await Promise.all([
|
|
384
|
+
channelManager.markAsRead(message.channel, message.sessionId, messageId),
|
|
385
|
+
channelManager.startTyping(message.channel, message.sessionId),
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
// unifiedSessionId = userId del onboarding → historial y thread LangGraph unificados
|
|
389
|
+
const unifiedSessionId = userId;
|
|
390
|
+
// routingSessionId = peerId del canal → para enviar respuestas de vuelta al canal correcto
|
|
391
|
+
const routingSessionId = message.sessionId;
|
|
392
|
+
|
|
393
|
+
const userMetadata = inputType === "audio_transcribed"
|
|
394
|
+
? { input_type: "audio_transcribed", stt_provider: sttProviderUsed, channel: message.channel }
|
|
395
|
+
: inputType === "image" || inputType === "document"
|
|
396
|
+
? { input_type: inputType, ocr_provider: visionConfig.ocrProvider, channel: message.channel }
|
|
397
|
+
: { input_type: "text", channel: message.channel };
|
|
398
|
+
|
|
399
|
+
// Obtener la zona horaria del usuario para el timestamp exacto
|
|
400
|
+
const userRow = getDb()
|
|
401
|
+
.query<any, [string]>("SELECT * FROM users WHERE id = ?")
|
|
402
|
+
.get(userId);
|
|
403
|
+
const userTimezone = userRow?.timezone || "UTC";
|
|
404
|
+
const now = new Date();
|
|
405
|
+
let exactTime = "";
|
|
406
|
+
try {
|
|
407
|
+
exactTime = now.toLocaleString("en-US", {
|
|
408
|
+
timeZone: userTimezone,
|
|
409
|
+
dateStyle: "full",
|
|
410
|
+
timeStyle: "long",
|
|
411
|
+
});
|
|
412
|
+
} catch (e) {
|
|
413
|
+
exactTime = now.toISOString();
|
|
414
|
+
}
|
|
415
|
+
const messageContentWithTime = `[Timestamp: ${exactTime} (${userTimezone})]\n${messageContent}`;
|
|
416
|
+
|
|
417
|
+
const messages = contentParts
|
|
418
|
+
? [{ role: "user" as const, content: [{ type: "text" as const, text: messageContentWithTime }, ...contentParts] as import("../multimodal/types").ContentPart[] }]
|
|
419
|
+
: [{ role: "user" as const, content: messageContentWithTime }];
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
log.info(`🤖 Routing to agent loop...`);
|
|
423
|
+
|
|
424
|
+
const response = await runner.generate({
|
|
425
|
+
provider: dbProvider as any,
|
|
426
|
+
messages,
|
|
427
|
+
rawUserMessage: messageContent,
|
|
428
|
+
maxTokens: 4096,
|
|
429
|
+
tools: prepareTools(agent, unifiedSessionId),
|
|
430
|
+
maxSteps: 15,
|
|
431
|
+
threadId: unifiedSessionId,
|
|
432
|
+
userId,
|
|
433
|
+
channel: message.channel,
|
|
434
|
+
onStep: async (step) => {
|
|
435
|
+
// "text" = el agente narra lo que está pensando/haciendo antes de un tool_call
|
|
436
|
+
if (step.type === "text" && step.message) {
|
|
437
|
+
const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
|
|
438
|
+
if (trimmedMessage) {
|
|
439
|
+
log.debug(`[NARRATION] ${trimmedMessage.substring(0, 100)}`);
|
|
440
|
+
try {
|
|
441
|
+
await channelManager.send(message.channel, routingSessionId, {
|
|
442
|
+
content: trimmedMessage,
|
|
443
|
+
type: "progress",
|
|
444
|
+
});
|
|
445
|
+
} catch (err) {
|
|
446
|
+
log.warn(`[onStep] Narration send failed: ${(err as Error).message}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
|
|
453
|
+
if (step.type === "tool_call" && step.toolName) {
|
|
454
|
+
const narration = getNarration(step.toolName);
|
|
455
|
+
log.debug(`[TOOL] ${step.toolName} → "${narration}"`);
|
|
456
|
+
try {
|
|
457
|
+
await channelManager.send(message.channel, routingSessionId, {
|
|
458
|
+
content: narration,
|
|
459
|
+
type: "progress",
|
|
460
|
+
});
|
|
461
|
+
} catch (err) {
|
|
462
|
+
log.warn(`[onStep] Tool narration send failed: ${(err as Error).message}`);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// "tool_result" = resultado de la herramienta
|
|
468
|
+
// Solo enviamos al usuario si el resultado lo pide explícitamente
|
|
469
|
+
if (step.type === "tool_result" && step.message) {
|
|
470
|
+
try {
|
|
471
|
+
const result = JSON.parse(step.message);
|
|
472
|
+
if (result._sendToUser) {
|
|
473
|
+
const userMessage = result.message || result.status || step.message;
|
|
474
|
+
try {
|
|
475
|
+
await channelManager.send(message.channel, routingSessionId, {
|
|
476
|
+
content: userMessage,
|
|
477
|
+
type: "progress",
|
|
478
|
+
});
|
|
479
|
+
} catch (err) {
|
|
480
|
+
log.warn(`[onStep] Tool result send failed: ${(err as Error).message}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
// No es JSON estructurado — no enviamos resultados crudos al usuario
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const responseContent = response.content?.trim() || "";
|
|
492
|
+
if (!responseContent) {
|
|
493
|
+
log.warn(`📤 LLM response: empty — skipping send`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
log.info(`📤 LLM response: ${responseContent.substring(0, 100)}${responseContent.length > 100 ? "..." : ""}`);
|
|
497
|
+
|
|
498
|
+
const shouldSpeak = preferAudioResponse;
|
|
499
|
+
let responseType: "text" | "audio" = "text";
|
|
500
|
+
let ttsProviderUsed: string | null = null;
|
|
501
|
+
let ttsMimeType: string | null = null;
|
|
502
|
+
|
|
503
|
+
if (responseContent) {
|
|
504
|
+
if (shouldSpeak) {
|
|
505
|
+
if (!voiceConfig.ttsProvider) {
|
|
506
|
+
log.warn(`⚠️ TTS provider not configured, user requested audio`);
|
|
507
|
+
await channelManager.send(message.channel, routingSessionId, {
|
|
508
|
+
content: `${responseContent}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > [Tu canal] (ej: elevenlabs, openai-tts)`
|
|
509
|
+
});
|
|
510
|
+
} else {
|
|
511
|
+
try {
|
|
512
|
+
log.info(`🔊 TTS enabled, synthesizing audio...`);
|
|
513
|
+
const audioOutput = await voiceService.speak(responseContent, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
|
|
514
|
+
ttsProviderUsed = voiceConfig.ttsProvider;
|
|
515
|
+
ttsMimeType = audioOutput.mimeType;
|
|
516
|
+
responseType = "audio";
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const channel = channelManager.getChannel(message.channel);
|
|
520
|
+
if (channel?.sendAudio) {
|
|
521
|
+
await channel.sendAudio(routingSessionId, audioOutput.data as Buffer, audioOutput.mimeType);
|
|
522
|
+
log.info(`✅ Audio sent to ${routingSessionId}`);
|
|
523
|
+
} else {
|
|
524
|
+
log.warn(`Channel ${message.channel} does not support audio, sending text`);
|
|
525
|
+
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
526
|
+
}
|
|
527
|
+
} catch (audioError) {
|
|
528
|
+
log.error(`❌ Audio send failed: ${(audioError as Error).message}, sending text instead`);
|
|
529
|
+
// Fallback to text
|
|
530
|
+
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
531
|
+
}
|
|
532
|
+
} catch (ttsError) {
|
|
533
|
+
log.error(`❌ TTS failed: ${(ttsError as Error).message}, sending text instead`);
|
|
534
|
+
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const assistantMetadata = {
|
|
543
|
+
response_type: responseType,
|
|
544
|
+
tts_provider: ttsProviderUsed,
|
|
545
|
+
mime_type: ttsMimeType,
|
|
546
|
+
channel: message.channel
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
await channelManager.stopTyping(message.channel, routingSessionId);
|
|
550
|
+
log.info(`✅ Response sent to ${routingSessionId} via ${message.channel}`);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
await channelManager.stopTyping(message.channel, routingSessionId);
|
|
553
|
+
log.error(`❌ Error: ${(error as Error).message} `);
|
|
554
|
+
await channelManager.send(message.channel, routingSessionId, {
|
|
555
|
+
content: `Error: ${(error as Error).message} `,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// ── Auth helper ──────────────────────────────────────────────────────────
|
|
561
|
+
// Dev mode when HIVE_DEV is set to "true" or "1".
|
|
562
|
+
// Set HIVE_DEV=true in your development environment.
|
|
563
|
+
const isDev = process.env.HIVE_DEV === "true" || process.env.HIVE_DEV === "1";
|
|
564
|
+
|
|
565
|
+
function checkAuth(req: Request, url: URL): boolean {
|
|
566
|
+
// En modo desarrollo, permitir todo
|
|
567
|
+
if (isDev) return true;
|
|
568
|
+
|
|
569
|
+
// En setup mode (sin usuarios), bypass total — el wizard no tiene token aún
|
|
570
|
+
if (gatewaySetupMode) return true;
|
|
571
|
+
|
|
572
|
+
// Setup endpoints are always public — needed before the client has a token
|
|
573
|
+
if (url.pathname.startsWith("/api/setup/")) return true;
|
|
574
|
+
|
|
575
|
+
// Auth endpoints: status, login, recover are public; others require token
|
|
576
|
+
if (url.pathname === "/api/auth/status") return true;
|
|
577
|
+
if (url.pathname === "/api/auth/login") return true;
|
|
578
|
+
if (url.pathname === "/api/auth/recover") return true;
|
|
579
|
+
|
|
580
|
+
// Users endpoint is public when no credentials configured (matches /api/auth/status behavior)
|
|
581
|
+
// This allows the UI to load user data when login is not configured yet
|
|
582
|
+
if (url.pathname === "/api/users" && req.method === "GET") {
|
|
583
|
+
try {
|
|
584
|
+
const user = getDb().query(
|
|
585
|
+
`SELECT email, password_hash FROM users LIMIT 1`
|
|
586
|
+
).get() as { email: string | null; password_hash: string | null } | null;
|
|
587
|
+
const hasCredentials = !!(user?.email && user?.password_hash);
|
|
588
|
+
// Allow access if no credentials configured
|
|
589
|
+
if (!hasCredentials) return true;
|
|
590
|
+
} catch {
|
|
591
|
+
// If DB query fails, fall through to token check
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Si no hay credenciales configuradas (modo open), bypass total — el UI
|
|
596
|
+
// no tiene token en localStorage porque nunca pasó por login.
|
|
597
|
+
// Coincide con el comportamiento de AuthGuard: status.hasCredentials === false → open.
|
|
598
|
+
try {
|
|
599
|
+
const user = getDb().query(
|
|
600
|
+
`SELECT email, password_hash FROM users LIMIT 1`
|
|
601
|
+
).get() as { email: string | null; password_hash: string | null } | null;
|
|
602
|
+
const hasCredentials = !!(user?.email && user?.password_hash);
|
|
603
|
+
if (!hasCredentials) return true;
|
|
604
|
+
} catch {
|
|
605
|
+
// Si falla la consulta, caemos al chequeo de token
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const activeToken = process.env.HIVE_AUTH_TOKEN;
|
|
609
|
+
if (!activeToken) return true;
|
|
610
|
+
const authHeader = req.headers.get("authorization");
|
|
611
|
+
const provided = authHeader?.replace(/^Bearer\s+/i, "") ?? url.searchParams.get("token");
|
|
612
|
+
return provided === activeToken;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Reload with full handler now that initialization is complete
|
|
616
|
+
server.reload({
|
|
617
|
+
async fetch(req, server) {
|
|
618
|
+
const start = Date.now();
|
|
619
|
+
const url = new URL(req.url);
|
|
620
|
+
const method = req.method;
|
|
621
|
+
|
|
622
|
+
const logRequest = (status: number, duration: number) => {
|
|
623
|
+
// Skip health checks from spamming logs unless debug
|
|
624
|
+
if (url.pathname === "/health" || url.pathname === "/health/") {
|
|
625
|
+
log.debug(`${method} ${url.pathname} - ${status} (${duration}ms)`);
|
|
626
|
+
} else {
|
|
627
|
+
log.info(`${method} ${url.pathname} - ${status} (${duration}ms)`);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const handleRequest = async (): Promise<Response | undefined> => {
|
|
632
|
+
|
|
633
|
+
// ── CORS preflight ────────────────────────────────────────────────────
|
|
634
|
+
if (req.method === "OPTIONS") {
|
|
635
|
+
const origin = req.headers.get("Origin");
|
|
636
|
+
if (origin && (origin.includes("localhost") || origin.includes("127.0.0.1") || origin.includes("0.0.0.0") || CORS_ORIGINS.some(o => origin.includes(o.replace("http://", ""))))) {
|
|
637
|
+
return new Response(null, {
|
|
638
|
+
status: 204,
|
|
639
|
+
headers: {
|
|
640
|
+
"Access-Control-Allow-Origin": origin,
|
|
641
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
642
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, X-Requested-With",
|
|
643
|
+
"Access-Control-Allow-Credentials": "true",
|
|
644
|
+
"Access-Control-Max-Age": "86400",
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return new Response(null, { status: 204 });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ── WebSocket upgrade ────────────────────────────────────────────────
|
|
652
|
+
if (url.pathname === "/ws" || url.pathname === "/ws/") {
|
|
653
|
+
let sessionId = url.searchParams.get("session") || resolveUserId({}) || "default";
|
|
654
|
+
// Auth: accept ?token=<authToken> (same as REST Bearer) as alternative to ?session=<userId>
|
|
655
|
+
if (!isDev && !gatewaySetupMode) {
|
|
656
|
+
const tokenParam = url.searchParams.get("token");
|
|
657
|
+
const activeToken = process.env.HIVE_AUTH_TOKEN;
|
|
658
|
+
if (tokenParam && activeToken && tokenParam === activeToken) {
|
|
659
|
+
// Token auth — resolve the real userId from DB
|
|
660
|
+
const user = getDb().query("SELECT id FROM users LIMIT 1").get() as { id: string } | undefined;
|
|
661
|
+
if (user) sessionId = user.id;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
const userExists = getDb().query("SELECT 1 FROM users WHERE id = ? LIMIT 1").get(sessionId);
|
|
665
|
+
if (!userExists) {
|
|
666
|
+
return new Response("Unauthorized", { status: 401 });
|
|
667
|
+
}
|
|
668
|
+
} catch {
|
|
669
|
+
return new Response("Unauthorized", { status: 401 });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!sessionId) {
|
|
673
|
+
return new Response("Missing session or user ID", { status: 400 });
|
|
674
|
+
}
|
|
675
|
+
const success = server.upgrade(req, {
|
|
676
|
+
data: { sessionId, authenticatedAt: Date.now() },
|
|
677
|
+
});
|
|
678
|
+
if (success) return undefined;
|
|
679
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
// ── Meeting Stream WebSocket upgrade ───────────────────────────────────
|
|
686
|
+
if (url.pathname === "/meeting-stream" || url.pathname === "/meeting-stream/") {
|
|
687
|
+
const meetingSessionId = url.searchParams.get("meetingSessionId") ?? "";
|
|
688
|
+
const sessionId = `meeting:${meetingSessionId || crypto.randomUUID()}`;
|
|
689
|
+
const success = server.upgrade(req, {
|
|
690
|
+
data: { sessionId, meetingSessionId, authenticatedAt: Date.now() },
|
|
691
|
+
});
|
|
692
|
+
if (success) return undefined;
|
|
693
|
+
return new Response("Meeting stream WebSocket upgrade failed", { status: 400 });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ── Health (must be before UI routing so it works in dev mode too) ───
|
|
697
|
+
if (url.pathname === "/health" || url.pathname === "/health/") {
|
|
698
|
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
|
699
|
+
return addCorsHeaders(Response.json({ status: "ok", version: _pkgVersion, uptime }), req);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── Dashboard / UI ────────────────────────────────────────────────────
|
|
703
|
+
// In development: UI is served by Vite on port 5173, Gateway only handles /api and /ws
|
|
704
|
+
// In production: serve static files from packages/hive-ui/dist
|
|
705
|
+
|
|
706
|
+
// Check if this is an API or WebSocket request
|
|
707
|
+
const isApiRequest = url.pathname.startsWith("/api");
|
|
708
|
+
const isWsRequest = url.pathname.startsWith("/ws");
|
|
709
|
+
const isUiRequest = url.pathname === "/ui" || url.pathname === "/ui/" || url.pathname.startsWith("/ui/") || url.pathname.startsWith("/ui?");
|
|
710
|
+
const isSetupRequest = url.pathname === "/setup" || url.pathname === "/setup/" || url.pathname.startsWith("/setup/") || url.pathname.startsWith("/setup?");
|
|
711
|
+
|
|
712
|
+
// In development mode, serve static files with HMR support
|
|
713
|
+
// In production, serve static files from dist folder
|
|
714
|
+
if (!isApiRequest && !isWsRequest) {
|
|
715
|
+
// In development: serve from packages/hive-ui/dist with HMR injection
|
|
716
|
+
if (isDev) {
|
|
717
|
+
const uiDir = path.join(process.cwd(), "packages/hive-ui/dist");
|
|
718
|
+
|
|
719
|
+
// Verificar si existe el build de la UI
|
|
720
|
+
const indexPath = path.join(uiDir, "index.html");
|
|
721
|
+
if (!existsSync(indexPath)) {
|
|
722
|
+
return new Response(
|
|
723
|
+
"UI build not found. Please run: cd packages/hive-ui && bun run build\n\n" +
|
|
724
|
+
"Or use: bun run dev (from root) which builds automatically.",
|
|
725
|
+
{ status: 503, headers: { "Content-Type": "text/plain" } }
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let subPath = url.pathname;
|
|
730
|
+
if (subPath === "/" || subPath === "/setup" || subPath === "/ui" || subPath === "/ui/") {
|
|
731
|
+
subPath = "/index.html";
|
|
732
|
+
} else if (subPath.startsWith("/ui/")) {
|
|
733
|
+
subPath = subPath.replace(/^\/ui/, "");
|
|
734
|
+
} else if (subPath.startsWith("/setup/")) {
|
|
735
|
+
subPath = subPath.replace(/^\/setup/, "");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const filePath = path.join(uiDir, subPath);
|
|
739
|
+
|
|
740
|
+
// Para index.html, inyectar script de HMR de Vite
|
|
741
|
+
if (subPath === "/index.html") {
|
|
742
|
+
const indexFile = Bun.file(filePath);
|
|
743
|
+
if (await indexFile.exists()) {
|
|
744
|
+
let html = await indexFile.text();
|
|
745
|
+
// Inyectar script de HMR de Vite antes de </head>
|
|
746
|
+
const hmrScript = `<script type="module" src="http://localhost:5173/@vite/client"></script>`;
|
|
747
|
+
html = html.replace("</head>", `${hmrScript}</head>`);
|
|
748
|
+
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const uiFile = Bun.file(filePath);
|
|
753
|
+
if (await uiFile.exists()) {
|
|
754
|
+
return new Response(uiFile);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// SPA fallback: servir index.html para rutas de React Router
|
|
758
|
+
const fallbackFile = Bun.file(path.join(uiDir, "index.html"));
|
|
759
|
+
if (await fallbackFile.exists()) {
|
|
760
|
+
let html = await fallbackFile.text();
|
|
761
|
+
// Inyectar script de HMR de Vite
|
|
762
|
+
const hmrScript = `<script type="module" src="http://localhost:5173/@vite/client"></script>`;
|
|
763
|
+
html = html.replace("</head>", `${hmrScript}</head>`);
|
|
764
|
+
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return new Response("Not found", { status: 404 });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// In production: serve from dist folder
|
|
771
|
+
// Priority: HIVE_UI_DIR (Docker) > ~/.hive/ui > HIVE_DIST_DIR/ui (global npm) > cwd/packages/hive-ui/dist (monorepo)
|
|
772
|
+
const uiDirFromEnv = process.env.HIVE_UI_DIR;
|
|
773
|
+
const uiDirFromHive = path.join(getHiveDir(), "ui");
|
|
774
|
+
const uiDirFromDist = process.env.HIVE_DIST_DIR ? path.join(process.env.HIVE_DIST_DIR, "ui") : null;
|
|
775
|
+
const uiDirFromCwd = path.join(process.cwd(), "packages/hive-ui/dist");
|
|
776
|
+
const uiDir = uiDirFromEnv
|
|
777
|
+
|| (existsSync(path.join(uiDirFromHive, "index.html")) ? uiDirFromHive
|
|
778
|
+
: uiDirFromDist && existsSync(path.join(uiDirFromDist, "index.html")) ? uiDirFromDist
|
|
779
|
+
: uiDirFromCwd);
|
|
780
|
+
let subPath = url.pathname;
|
|
781
|
+
|
|
782
|
+
// En setup mode: / y /ui redirigen a /setup
|
|
783
|
+
if (gatewaySetupMode && (subPath === "/" || subPath === "/ui" || subPath === "/ui/")) {
|
|
784
|
+
const _publicBase = process.env.HIVE_PUBLIC_URL?.replace(/\/$/, "")
|
|
785
|
+
?? `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
|
|
786
|
+
return Response.redirect(`${_publicBase}/setup`, 302);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Normalize path for /ui routes
|
|
790
|
+
if (subPath === "/ui" || subPath === "/ui/") {
|
|
791
|
+
subPath = "/index.html";
|
|
792
|
+
} else if (subPath.startsWith("/ui/")) {
|
|
793
|
+
subPath = subPath.replace(/^\/ui/, "");
|
|
794
|
+
if (!subPath) subPath = "/index.html";
|
|
795
|
+
} else if (subPath === "/") {
|
|
796
|
+
subPath = "/index.html";
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Normalize path for /setup routes
|
|
800
|
+
if (subPath === "/setup" || subPath === "/setup/") {
|
|
801
|
+
subPath = "/index.html";
|
|
802
|
+
} else if (subPath.startsWith("/setup/")) {
|
|
803
|
+
subPath = subPath.replace(/^\/setup/, "");
|
|
804
|
+
if (!subPath) subPath = "/index.html";
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const filePath = path.join(uiDir, subPath);
|
|
808
|
+
const uiFile = Bun.file(filePath);
|
|
809
|
+
if (await uiFile.exists()) {
|
|
810
|
+
return new Response(uiFile);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// SPA fallback: paths without a file extension are React Router routes — serve index.html
|
|
814
|
+
if (!path.extname(subPath)) {
|
|
815
|
+
const indexFile = Bun.file(path.join(uiDir, "index.html"));
|
|
816
|
+
if (await indexFile.exists()) {
|
|
817
|
+
return new Response(indexFile);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// If UI is not available, show helpful message for any non-API route
|
|
822
|
+
return new Response(
|
|
823
|
+
"UI not found.\n\n" +
|
|
824
|
+
"Options:\n" +
|
|
825
|
+
" 1. Place the UI in ~/.hive/ui/ (copy hive-ui/dist contents there)\n" +
|
|
826
|
+
" 2. Set HIVE_UI_DIR=/path/to/ui\n" +
|
|
827
|
+
" 3. Build from source: cd packages/hive-ui && bun run build\n",
|
|
828
|
+
{ status: 404, headers: { "Content-Type": "text/plain" } }
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Handle /dashboard redirect for backwards compatibility
|
|
833
|
+
if (url.pathname.startsWith("/dashboard")) {
|
|
834
|
+
const tokenParam = url.searchParams.get("token") ? `? token = ${url.searchParams.get("token")} ` : "";
|
|
835
|
+
return Response.redirect(`/ ui${tokenParam} `, 301);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ── Rutas que requieren autenticación ────────────────────────────────
|
|
839
|
+
if (!checkAuth(req, url)) {
|
|
840
|
+
log.warn(`[AUTH] Unauthorized request to ${url.pathname} from ${req.headers.get("origin")} `);
|
|
841
|
+
return addCorsHeaders(new Response("Unauthorized", { status: 401 }), req);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ── Setup API ────────────────────────────────────────────────────────
|
|
845
|
+
// GET /api/setup/status
|
|
846
|
+
if (url.pathname === "/api/setup/status" || url.pathname === "/api/setup/status/") {
|
|
847
|
+
return addCorsHeaders(await handleSetupStatus(), req)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// GET /api/setup/providers
|
|
851
|
+
if (url.pathname === "/api/setup/providers" && req.method === "GET") {
|
|
852
|
+
return handleSetupProviders(addCorsHeaders, req)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// GET /api/setup/ollama-models
|
|
856
|
+
if (url.pathname === "/api/setup/ollama-models" && req.method === "GET") {
|
|
857
|
+
return handleSetupOllamaModels(addCorsHeaders, req)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// GET /api/setup/ethics
|
|
861
|
+
if (url.pathname === "/api/setup/ethics" && req.method === "GET") {
|
|
862
|
+
return handleSetupEthics(addCorsHeaders, req)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// POST /api/setup/verify-provider
|
|
866
|
+
if (url.pathname === "/api/setup/verify-provider" && req.method === "POST") {
|
|
867
|
+
return addCorsHeaders(await handleVerifyProvider(req), req)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// POST /api/setup/complete
|
|
871
|
+
if (url.pathname === "/api/setup/complete" && req.method === "POST") {
|
|
872
|
+
return await handleCompleteSetup(req, config, addCorsHeaders)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// ── Auth API ─────────────────────────────────────────────────────────
|
|
876
|
+
// GET /api/auth/status
|
|
877
|
+
if (url.pathname === "/api/auth/status" && req.method === "GET") {
|
|
878
|
+
return handleAuthStatus(req, addCorsHeaders);
|
|
879
|
+
}
|
|
880
|
+
// POST /api/auth/login
|
|
881
|
+
if (url.pathname === "/api/auth/login" && req.method === "POST") {
|
|
882
|
+
return handleLogin(req, addCorsHeaders);
|
|
883
|
+
}
|
|
884
|
+
// POST /api/auth/setup-credentials
|
|
885
|
+
if (url.pathname === "/api/auth/setup-credentials" && req.method === "POST") {
|
|
886
|
+
return handleSetupCredentials(req, addCorsHeaders);
|
|
887
|
+
}
|
|
888
|
+
// POST /api/auth/change-password
|
|
889
|
+
if (url.pathname === "/api/auth/change-password" && req.method === "POST") {
|
|
890
|
+
return handleChangePassword(req, addCorsHeaders);
|
|
891
|
+
}
|
|
892
|
+
// POST /api/auth/recover
|
|
893
|
+
if (url.pathname === "/api/auth/recover" && req.method === "POST") {
|
|
894
|
+
return handleRecover(req, addCorsHeaders);
|
|
895
|
+
}
|
|
896
|
+
// POST /api/auth/disable
|
|
897
|
+
if (url.pathname === "/api/auth/disable" && req.method === "POST") {
|
|
898
|
+
return handleDisableAuth(req, addCorsHeaders);
|
|
899
|
+
}
|
|
900
|
+
// GET /api/auth/recovery-key
|
|
901
|
+
if (url.pathname === "/api/auth/recovery-key" && req.method === "GET") {
|
|
902
|
+
return handleRecoveryKey(req, addCorsHeaders);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ── Status ───────────────────────────────────────────────────────────
|
|
906
|
+
if (url.pathname === "/status" || url.pathname === "/status/") {
|
|
907
|
+
return addCorsHeaders(new Response(
|
|
908
|
+
JSON.stringify({
|
|
909
|
+
status: "ok",
|
|
910
|
+
version: "0.1.7",
|
|
911
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
912
|
+
gateway: { host, port },
|
|
913
|
+
sessions: sessionManager.list().map((s) => ({
|
|
914
|
+
id: s.id,
|
|
915
|
+
createdAt: s.createdAt,
|
|
916
|
+
messageCount: s.messageCount,
|
|
917
|
+
})),
|
|
918
|
+
channels: channelManager?.listChannels() ?? [],
|
|
919
|
+
queue: { activeSessions: 0 },
|
|
920
|
+
}),
|
|
921
|
+
{ headers: { "Content-Type": "application/json", "Cache-Control": "max-age=5" } }
|
|
922
|
+
), req);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ── Activity Stats ─────────────────────────────────────────────────
|
|
926
|
+
if (url.pathname === "/api/activity-stats" || url.pathname === "/api/activity-stats/") {
|
|
927
|
+
return await handleGetActivityStats(req, addCorsHeaders)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// ── System Stats ───────────────────────────────────────────────────
|
|
931
|
+
if (url.pathname === "/api/system-stats" || url.pathname === "/api/system-stats/") {
|
|
932
|
+
return await handleGetSystemStats(req, addCorsHeaders, startTime)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// ── Version Check ──────────────────────────────────────────────────
|
|
936
|
+
if (url.pathname === "/api/version" || url.pathname === "/api/version/") {
|
|
937
|
+
return await handleGetVersion(req, addCorsHeaders)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ── Trigger Update ─────────────────────────────────────────────────
|
|
941
|
+
if (url.pathname === "/api/update" || url.pathname === "/api/update/") {
|
|
942
|
+
if (req.method === "POST") {
|
|
943
|
+
return await handleTriggerUpdate(req, addCorsHeaders)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ── Usage Stats ─────────────────────────────────────────────────────
|
|
948
|
+
if (url.pathname === "/api/usage-stats" || url.pathname === "/api/usage-stats/") {
|
|
949
|
+
return await handleGetUsageStats(req, addCorsHeaders)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// ── System Reload ─────────────────────────────────────────────────
|
|
953
|
+
if (url.pathname === "/api/system/reload" || url.pathname === "/api/system/reload/") {
|
|
954
|
+
return await handleSystemReload(req, addCorsHeaders)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// ── Config ─────────────────────────────────────────────────────────
|
|
958
|
+
if (url.pathname === "/api/config") {
|
|
959
|
+
if (req.method === "GET") {
|
|
960
|
+
return await handleGetConfig(req, addCorsHeaders, config);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ── Tasks API ─────────────────────────────────────────────────────
|
|
965
|
+
if ((url.pathname === "/api/tasks" || url.pathname === "/api/tasks/") && req.method === "GET") {
|
|
966
|
+
return await handleGetTasks(req, addCorsHeaders)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const taskDetailMatch = url.pathname.match(/^\/api\/tasks\/(\d+)$/)
|
|
970
|
+
if (taskDetailMatch && req.method === "PATCH") {
|
|
971
|
+
return await handleUpdateTask(req, addCorsHeaders)
|
|
972
|
+
}
|
|
973
|
+
const channelDetailMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/([^/]+)$/);
|
|
974
|
+
if (channelDetailMatch) {
|
|
975
|
+
const name = channelDetailMatch[1];
|
|
976
|
+
const accountId = channelDetailMatch[2];
|
|
977
|
+
|
|
978
|
+
if (req.method === "GET") {
|
|
979
|
+
return await handleGetChannelAccount(req, addCorsHeaders, name, accountId);
|
|
980
|
+
}
|
|
981
|
+
if (req.method === "PUT") {
|
|
982
|
+
const body = await req.json().catch(() => ({}));
|
|
983
|
+
if (!body.config) return new Response("Missing config", { status: 400 });
|
|
984
|
+
|
|
985
|
+
config.channels = config.channels || {};
|
|
986
|
+
config.channels[name] = config.channels[name] || { enabled: true, accounts: {} };
|
|
987
|
+
const channelEntry = config.channels[name] as any;
|
|
988
|
+
channelEntry.accounts = channelEntry.accounts || {};
|
|
989
|
+
channelEntry.accounts[accountId] = body.config;
|
|
990
|
+
return await handleUpdateChannelAccount(req, addCorsHeaders, name, accountId, channelManager);
|
|
991
|
+
}
|
|
992
|
+
if (req.method === "DELETE") {
|
|
993
|
+
// Config update handled by caller
|
|
994
|
+
if (config.channels?.[name]) {
|
|
995
|
+
const channelEntry = config.channels[name] as any;
|
|
996
|
+
if (channelEntry.accounts) {
|
|
997
|
+
delete channelEntry.accounts[accountId];
|
|
998
|
+
if (Object.keys(channelEntry.accounts).length === 0) {
|
|
999
|
+
delete config.channels[name];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return await handleDeleteChannelAccount(req, addCorsHeaders, name, accountId, config, channelManager);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const channelActionMatch = url.pathname.match(
|
|
1008
|
+
/^\/api\/channels\/([^/]+)\/([^/]+)\/(start|stop)$/
|
|
1009
|
+
);
|
|
1010
|
+
if (channelActionMatch) {
|
|
1011
|
+
const [, name, accountId, action] = channelActionMatch;
|
|
1012
|
+
if (req.method === "POST") {
|
|
1013
|
+
return await handleChannelAction(req, addCorsHeaders, name, accountId, action as "start" | "stop", channelManager);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ── Skills API ───────────────────────────────────────────────────────
|
|
1018
|
+
if ((url.pathname === "/api/skills" || url.pathname === "/api/skills/") && req.method === "POST") {
|
|
1019
|
+
return await handleCreateSkill(req, addCorsHeaders);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ── Model Config API ─────────────────────────────────────────────────
|
|
1023
|
+
if (url.pathname === "/api/config/models") {
|
|
1024
|
+
if (req.method === "GET") {
|
|
1025
|
+
return await handleGetModelsConfig(req, addCorsHeaders, config);
|
|
1026
|
+
}
|
|
1027
|
+
if (req.method === "POST") {
|
|
1028
|
+
return await handleUpdateModelsConfig(req, addCorsHeaders, config, agent);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ── MCP API ──────────────────────────────────────────────────────────
|
|
1033
|
+
// Note: Full MCP route handlers are in routes/mcp.ts
|
|
1034
|
+
if (url.pathname === "/api/mcp/servers" && req.method === "GET") {
|
|
1035
|
+
const mcpManager = agent?.getMCPManager() ?? null;
|
|
1036
|
+
return await handleGetMcpServers(req, addCorsHeaders, mcpManager)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// GET /api/mcp/servers/:id — detail with unredacted headers (for editing)
|
|
1040
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "GET") {
|
|
1041
|
+
const serverId = url.pathname.split("/")[4];
|
|
1042
|
+
return await handleGetMcpServerDetail(req, addCorsHeaders, serverId)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (url.pathname === "/api/mcp/servers" && req.method === "POST") {
|
|
1046
|
+
const response = await handleCreateMcpServer(req, addCorsHeaders)
|
|
1047
|
+
|
|
1048
|
+
// Hot reload will auto-connect the server within 2 seconds
|
|
1049
|
+
// No manual connection needed
|
|
1050
|
+
|
|
1051
|
+
return response
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// PUT /api/mcp/servers/:id — update server config
|
|
1055
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "PUT") {
|
|
1056
|
+
return await handleUpdateMcpServer(req, addCorsHeaders)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// DELETE /api/mcp/servers/:id — remove server
|
|
1060
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "DELETE") {
|
|
1061
|
+
return await handleDeleteMcpServer(req, addCorsHeaders)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/[^/]+\/toggle$/)) {
|
|
1065
|
+
const mcpId = url.pathname.split("/")[4];
|
|
1066
|
+
if (req.method === "POST") {
|
|
1067
|
+
return await handleToggleMcpServer(req, addCorsHeaders, mcpId)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ── Workspace API ────────────────────────────────────────────────────
|
|
1072
|
+
// Validate workspace path
|
|
1073
|
+
if (url.pathname === "/api/workspace/validate" && req.method === "POST") {
|
|
1074
|
+
return await handleValidateWorkspace(req, addCorsHeaders);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Create workspace directory
|
|
1078
|
+
if (url.pathname === "/api/workspace/create" && req.method === "POST") {
|
|
1079
|
+
return await handleCreateWorkspace(req, addCorsHeaders);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Open workspace in file explorer
|
|
1083
|
+
if (url.pathname === "/api/workspace/open" && req.method === "GET") {
|
|
1084
|
+
return await handleOpenWorkspace(req, addCorsHeaders);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Get/Update workspace files (soul, user, ethics)
|
|
1088
|
+
for (const wsType of ["soul", "user", "ethics"] as const) {
|
|
1089
|
+
if (url.pathname === `/api/workspace/${wsType}`) {
|
|
1090
|
+
const coordinatorRow = getDb().query<{ workspace: string | null }, []>(
|
|
1091
|
+
"SELECT workspace FROM agents WHERE role = 'coordinator' LIMIT 1"
|
|
1092
|
+
).get();
|
|
1093
|
+
const liveWorkspacePath = coordinatorRow?.workspace
|
|
1094
|
+
? expandPath(coordinatorRow.workspace)
|
|
1095
|
+
: expandPath("~/.hive/workspace");
|
|
1096
|
+
if (req.method === "GET") {
|
|
1097
|
+
return await handleGetWorkspace(req, addCorsHeaders, liveWorkspacePath, wsType);
|
|
1098
|
+
}
|
|
1099
|
+
if (req.method === "POST") {
|
|
1100
|
+
const reloadFn = async (type: string) => {
|
|
1101
|
+
if (type === "soul") agent.reloadSoul();
|
|
1102
|
+
if (type === "user") agent.reloadUser();
|
|
1103
|
+
if (type === "ethics") await agent.reloadEthics();
|
|
1104
|
+
};
|
|
1105
|
+
return await handleUpdateWorkspace(req, addCorsHeaders, liveWorkspacePath, wsType, reloadFn);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ── Reload API ───────────────────────────────────────────────────────
|
|
1111
|
+
if (url.pathname === "/api/reload" && req.method === "POST") {
|
|
1112
|
+
return await handleApiReload(req, addCorsHeaders, agent);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ── User Channel Linking API ────────────────────────────────────────────
|
|
1116
|
+
if (url.pathname === "/api/user/channels" && req.method === "POST") {
|
|
1117
|
+
return await handleLinkUserChannel(req, addCorsHeaders, config, log);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (url.pathname === "/api/user/channels" && req.method === "GET") {
|
|
1121
|
+
return await handleGetUserChannels(req, addCorsHeaders, config);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ── Agents API ─────────────────────────────────────────────────────
|
|
1125
|
+
if (url.pathname === "/api/agents" && req.method === "GET") {
|
|
1126
|
+
return await handleGetAgents(req, addCorsHeaders)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (url.pathname === "/api/agents" && req.method === "POST") {
|
|
1130
|
+
return await handleCreateAgent(req, addCorsHeaders)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (url.pathname.startsWith("/api/agents/") && (req.method === "PATCH" || req.method === "PUT")) {
|
|
1134
|
+
return await handleUpdateAgent(req, addCorsHeaders)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (url.pathname.match(/^\/api\/agents\/[^/]+$/) && req.method === "DELETE") {
|
|
1138
|
+
return await handleDeleteAgent(req, addCorsHeaders)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// ── Providers API ───────────────────────────────────────────────────
|
|
1142
|
+
if (url.pathname === "/api/providers" && req.method === "GET") {
|
|
1143
|
+
return await handleGetProviders(req, addCorsHeaders)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (url.pathname === "/api/providers" && req.method === "POST") {
|
|
1147
|
+
return await handleCreateProvider(req, addCorsHeaders)
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (url.pathname.match(/^\/api\/providers\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1151
|
+
return await handleToggleProvider(req, addCorsHeaders)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const providerIdMatch = url.pathname.match(/^\/api\/providers\/([^/]+)$/)
|
|
1155
|
+
if (providerIdMatch && (req.method === "PUT" || req.method === "PATCH")) {
|
|
1156
|
+
return await handleUpdateProvider(req, addCorsHeaders)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// ── Models API ───────────────────────────────────────────────────
|
|
1160
|
+
// GET /api/models?provider_id=xxx - Get models filtered by provider
|
|
1161
|
+
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
1162
|
+
return await handleGetModels(req, addCorsHeaders)
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// POST /api/providers/:id/sync-models — sincroniza modelos desde la API local del provider
|
|
1166
|
+
const syncModelsMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/sync-models$/)
|
|
1167
|
+
if (syncModelsMatch && req.method === "POST") {
|
|
1168
|
+
const providerId = syncModelsMatch[1]
|
|
1169
|
+
return await handleSyncProviderModels(req, addCorsHeaders, providerId)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// POST /api/models - Create a new model
|
|
1173
|
+
if (url.pathname === "/api/models" && req.method === "POST") {
|
|
1174
|
+
return await handleCreateModel(req, addCorsHeaders)
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (url.pathname.match(/^\/api\/models\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1178
|
+
return await handleToggleModel(req, addCorsHeaders)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// DELETE /api/models/:id
|
|
1182
|
+
if (url.pathname.match(/^\/api\/models\/[^/]+$/) && req.method === "DELETE") {
|
|
1183
|
+
return await handleDeleteModel(req, addCorsHeaders)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// PUT /api/models/:id
|
|
1187
|
+
if (url.pathname.match(/^\/api\/models\/[^/]+$/) && req.method === "PUT") {
|
|
1188
|
+
return await handleUpdateModel(req, addCorsHeaders)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// ── Skills API ─────────────────────────────────────────────────────
|
|
1192
|
+
if (url.pathname === "/api/skills" && req.method === "GET") {
|
|
1193
|
+
return await handleGetSkills(req, addCorsHeaders)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (url.pathname === "/api/skills" && req.method === "POST") {
|
|
1197
|
+
const body = await req.json().catch(() => ({}))
|
|
1198
|
+
const { name, description, category, tools, triggers, preferred_agents, body: bodyContent } = body
|
|
1199
|
+
if (!name) return addCorsHeaders(new Response("Missing name", { status: 400 }), req)
|
|
1200
|
+
const id = randomUUID()
|
|
1201
|
+
getDb().query(`INSERT INTO skills(id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active) VALUES(?, ?, ?, ?, ?, ?, ?, ?, '0.0.1', 1, 1)`).run(id, name, description || "", category || "", tools || "", triggers || "", typeof preferred_agents === 'object' ? JSON.stringify(preferred_agents || []) : (preferred_agents || "[]"), bodyContent || "")
|
|
1202
|
+
return addCorsHeaders(Response.json({ success: true, id }), req)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (url.pathname.match(/^\/api\/skills\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1206
|
+
return await handleActivateSkill(req, addCorsHeaders)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (url.pathname.match(/^\/api\/skills\/[^/]+$/) && req.method === "PUT") {
|
|
1210
|
+
return await handleUpdateSkill(req, addCorsHeaders)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (url.pathname.match(/^\/api\/skills\/[^/]+$/) && req.method === "DELETE") {
|
|
1214
|
+
return await handleDeleteSkill(req, addCorsHeaders)
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// ── Tools API ────────────────────────────────────────────────────────
|
|
1218
|
+
if (url.pathname === "/api/tools" && req.method === "GET") {
|
|
1219
|
+
return await handleGetTools(req, addCorsHeaders)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (url.pathname.match(/^\/api\/tools\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1223
|
+
return await handleActivateTool(req, addCorsHeaders)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (url.pathname.match(/^\/api\/tools\/[^/]+$/) && req.method === "PUT") {
|
|
1227
|
+
return await handleUpdateTool(req, addCorsHeaders)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// ── Ethics API ──────────────────────────────────────────────────────
|
|
1231
|
+
if (url.pathname === "/api/ethics" && req.method === "GET") {
|
|
1232
|
+
return await handleGetEthics(req, addCorsHeaders)
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (url.pathname === "/api/ethics" && req.method === "POST") {
|
|
1236
|
+
const body = await req.json().catch(() => ({}))
|
|
1237
|
+
const { name, description, content, is_default } = body
|
|
1238
|
+
if (!name || !content) return addCorsHeaders(Response.json({ success: false, error: "Missing name or content" }, { status: 400 }), req)
|
|
1239
|
+
const id = randomUUID()
|
|
1240
|
+
getDb().query(`INSERT INTO ethics(id, name, description, content, is_default, enabled, active) VALUES(?, ?, ?, ?, ?, 1, 1)`).run(id, name, description || "", content, is_default ? 1 : 0)
|
|
1241
|
+
return addCorsHeaders(Response.json({ success: true, id }), req)
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (url.pathname.match(/^\/api\/ethics\/[^/]+$/) && req.method === "PUT") {
|
|
1245
|
+
return await handleActivateEthics(req, addCorsHeaders)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (url.pathname.match(/^\/api\/ethics\/[^/]+$/) && req.method === "DELETE") {
|
|
1249
|
+
return await handleDeleteEthics(req, addCorsHeaders)
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// ── Users API ───────────────────────────────────────────────────────
|
|
1253
|
+
if (url.pathname === "/api/users" && req.method === "GET") {
|
|
1254
|
+
return await handleGetUsers(req, addCorsHeaders)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (url.pathname === "/api/users" && req.method === "POST") {
|
|
1258
|
+
return await handleCreateUser(req, addCorsHeaders)
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (url.pathname === "/api/user/settings" && req.method === "PATCH") {
|
|
1262
|
+
return await handleUpdateUserSettings(req, addCorsHeaders)
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// ── MCP Servers API ──────────────────────────────────────────────────
|
|
1266
|
+
if (url.pathname === "/api/mcp/servers" && req.method === "GET") {
|
|
1267
|
+
return await handleGetMcpServers(req, addCorsHeaders, agent?.getMCPManager() ?? null)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// GET /api/mcp/servers/:id — detail with unredacted headers (for editing)
|
|
1271
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "GET") {
|
|
1272
|
+
const serverId = url.pathname.split("/")[4];
|
|
1273
|
+
return await handleGetMcpServerDetail(req, addCorsHeaders, serverId)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// GET /api/mcp/servers/:id/tools - Get tools for a specific MCP server
|
|
1277
|
+
// Note: Tools are loaded from MCP Manager at runtime, not from DB
|
|
1278
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)\/tools$/) && req.method === "GET") {
|
|
1279
|
+
const serverId = url.pathname.split("/")[4];
|
|
1280
|
+
const mcpManager = agent?.getMCPManager() ?? null;
|
|
1281
|
+
return await handleGetMCPServerTools(req, addCorsHeaders, serverId, mcpManager)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Note: /api/mcp/tools/:id/toggle and /api/mcp/tools/:id DELETE removed
|
|
1285
|
+
// MCP tools are not stored in DB - they are loaded at runtime from servers
|
|
1286
|
+
|
|
1287
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)\/toggle$/)) {
|
|
1288
|
+
const mcpName = url.pathname.split("/")[4];
|
|
1289
|
+
if (req.method === "POST") {
|
|
1290
|
+
const body = await req.json().catch(() => ({}))
|
|
1291
|
+
// Support both { active: boolean } and { action: "connect"|"disconnect" }
|
|
1292
|
+
let active = body.active
|
|
1293
|
+
if (active === undefined && body.action !== undefined) {
|
|
1294
|
+
active = body.action === "connect"
|
|
1295
|
+
}
|
|
1296
|
+
if (active === undefined) {
|
|
1297
|
+
return addCorsHeaders(Response.json({ success: false, error: "Missing active field" }, { status: 400 }), req)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
log.info(`[MCP] Toggle connection for ${mcpName}, active=${active}`)
|
|
1301
|
+
|
|
1302
|
+
// Update DB
|
|
1303
|
+
getDb().query(`UPDATE mcp_servers SET active = ?, enabled = ? WHERE id = ? OR name = ?`).run(active ? 1 : 0, active ? 1 : 0, mcpName, mcpName)
|
|
1304
|
+
|
|
1305
|
+
// Connect/Disconnect MCP server in real-time (no restart needed)
|
|
1306
|
+
try {
|
|
1307
|
+
const mcp = agent?.getMCPManager() ?? null;
|
|
1308
|
+
if (mcp) {
|
|
1309
|
+
log.info(`[MCP] Manager found, connecting ${mcpName}...`)
|
|
1310
|
+
if (active) {
|
|
1311
|
+
const server = getDb().query(`SELECT * FROM mcp_servers WHERE id = ? OR name = ?`).get(mcpName, mcpName) as Record<string, any> | undefined;
|
|
1312
|
+
if (server) {
|
|
1313
|
+
log.info(`[MCP] Server config: transport=${server.transport}, url=${server.url}`)
|
|
1314
|
+
|
|
1315
|
+
// Build MCP server config
|
|
1316
|
+
const mcpServerConfig: any = {
|
|
1317
|
+
transport: server.transport as string,
|
|
1318
|
+
command: server.command as string | null,
|
|
1319
|
+
args: server.args ? JSON.parse(server.args as string) : [],
|
|
1320
|
+
url: server.url as string | null,
|
|
1321
|
+
enabled: true,
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Load headers from keychain (modern approach), fall back to legacy AES
|
|
1325
|
+
const keychainHeaders = await loadMcpHeaders(server.id as string);
|
|
1326
|
+
if (Object.keys(keychainHeaders).length > 0) {
|
|
1327
|
+
mcpServerConfig.headers = keychainHeaders;
|
|
1328
|
+
} else if (server.headers_encrypted && server.headers_iv) {
|
|
1329
|
+
try {
|
|
1330
|
+
mcpServerConfig.headers = legacyDecryptAES(server.headers_encrypted, server.headers_iv);
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
log.warn(`Failed to decrypt legacy headers for ${mcpName}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Get current MCP config and add/update this server
|
|
1337
|
+
const currentConfig = (mcp as any).config || { servers: {} }
|
|
1338
|
+
const newServersConfig = { ...currentConfig.servers }
|
|
1339
|
+
newServersConfig[mcpName] = mcpServerConfig
|
|
1340
|
+
|
|
1341
|
+
// Update MCP Manager config (this will register and auto-connect the server)
|
|
1342
|
+
await mcp.updateConfig({
|
|
1343
|
+
...currentConfig,
|
|
1344
|
+
servers: newServersConfig,
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
log.info(`[MCP] Server registered in MCP Manager`)
|
|
1348
|
+
|
|
1349
|
+
// Get tools after connection
|
|
1350
|
+
const tools = mcp.getServerTools(mcpName) || [];
|
|
1351
|
+
const serverDetails = mcp.getServerDetails?.(mcpName);
|
|
1352
|
+
const serverStatus = serverDetails?.status ?? mcp.getServerStatus(mcpName);
|
|
1353
|
+
if (serverStatus === "error" && serverDetails?.error) {
|
|
1354
|
+
log.error(`[MCP] Connection error for ${mcpName}: ${serverDetails.error}`);
|
|
1355
|
+
}
|
|
1356
|
+
log.info(`[MCP] Connected! Tools: ${tools.length}, status: ${serverStatus}`);
|
|
1357
|
+
getDb().query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ? OR name = ?`).run(serverStatus === "connected" ? "connected" : "error", tools.length, mcpName, mcpName);
|
|
1358
|
+
} else {
|
|
1359
|
+
log.error(`[MCP] Server not found in DB: ${mcpName}`)
|
|
1360
|
+
}
|
|
1361
|
+
} else {
|
|
1362
|
+
await mcp.disconnectServer(mcpName);
|
|
1363
|
+
getDb().query(`UPDATE mcp_servers SET status = ? WHERE id = ? OR name = ?`).run("disconnected", mcpName, mcpName);
|
|
1364
|
+
}
|
|
1365
|
+
} else {
|
|
1366
|
+
log.error(`[MCP] No MCP Manager found`)
|
|
1367
|
+
}
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
log.error(`[MCP] Failed to connect ${mcpName}: ${(error as Error).message}`);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return addCorsHeaders(Response.json({ success: true, active, message: active ? "Servidor MCP conectado" : "Servidor MCP desconectado" }), req)
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// GET /api/mcp/servers/:id — detail with unredacted headers (for editing)
|
|
1377
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/) && req.method === "GET") {
|
|
1378
|
+
const serverId = url.pathname.split("/")[4];
|
|
1379
|
+
return await handleGetMcpServerDetail(req, addCorsHeaders, serverId)
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Support /api/mcp/servers/{name} with POST for connect (frontend uses this)
|
|
1383
|
+
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/)) {
|
|
1384
|
+
const mcpName = url.pathname.split("/")[4];
|
|
1385
|
+
if (req.method === "POST") {
|
|
1386
|
+
const body = await req.json().catch(() => ({}))
|
|
1387
|
+
// Support both { active: boolean } and { action: "connect"|"disconnect" }
|
|
1388
|
+
let active = body.active
|
|
1389
|
+
if (active === undefined && body.action !== undefined) {
|
|
1390
|
+
active = body.action === "connect"
|
|
1391
|
+
}
|
|
1392
|
+
if (active === undefined) {
|
|
1393
|
+
return addCorsHeaders(Response.json({ success: false, error: "Missing active field" }, { status: 400 }), req)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Update DB
|
|
1397
|
+
getDb().query(`UPDATE mcp_servers SET active = ?, enabled = ? WHERE id = ? OR name = ?`).run(active ? 1 : 0, active ? 1 : 0, mcpName, mcpName)
|
|
1398
|
+
|
|
1399
|
+
// Connect/Disconnect MCP server in real-time (no restart needed)
|
|
1400
|
+
try {
|
|
1401
|
+
const mcp = agent?.getMCPManager() ?? null;
|
|
1402
|
+
if (mcp) {
|
|
1403
|
+
if (active) {
|
|
1404
|
+
const server = getDb().query(`SELECT * FROM mcp_servers WHERE id = ? OR name = ?`).get(mcpName, mcpName) as Record<string, any> | undefined;
|
|
1405
|
+
if (server) {
|
|
1406
|
+
log.info(`[MCP] Server config: transport=${server.transport}, url=${server.url}`)
|
|
1407
|
+
|
|
1408
|
+
// Build MCP server config
|
|
1409
|
+
const mcpServerConfig: any = {
|
|
1410
|
+
transport: server.transport as string,
|
|
1411
|
+
command: server.command as string | null,
|
|
1412
|
+
args: server.args ? JSON.parse(server.args as string) : [],
|
|
1413
|
+
url: server.url as string | null,
|
|
1414
|
+
enabled: true,
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Load headers from keychain (modern approach), fall back to legacy AES
|
|
1418
|
+
const keychainHeaders2 = await loadMcpHeaders(server.id as string);
|
|
1419
|
+
if (Object.keys(keychainHeaders2).length > 0) {
|
|
1420
|
+
mcpServerConfig.headers = keychainHeaders2;
|
|
1421
|
+
} else if (server.headers_encrypted && server.headers_iv) {
|
|
1422
|
+
try {
|
|
1423
|
+
mcpServerConfig.headers = legacyDecryptAES(server.headers_encrypted, server.headers_iv);
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
log.warn(`Failed to decrypt legacy headers for ${mcpName}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Get current MCP config and add/update this server
|
|
1430
|
+
const currentConfig = (mcp as any).config || { servers: {} }
|
|
1431
|
+
const newServersConfig = { ...currentConfig.servers }
|
|
1432
|
+
newServersConfig[mcpName] = mcpServerConfig
|
|
1433
|
+
|
|
1434
|
+
// Update MCP Manager config (this will register and auto-connect the server)
|
|
1435
|
+
await mcp.updateConfig({
|
|
1436
|
+
...currentConfig,
|
|
1437
|
+
servers: newServersConfig,
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
log.info(`[MCP] Server registered in MCP Manager`)
|
|
1441
|
+
|
|
1442
|
+
// Get tools after connection
|
|
1443
|
+
const tools = mcp.getServerTools(mcpName) || [];
|
|
1444
|
+
const serverDetails2 = mcp.getServerDetails?.(mcpName);
|
|
1445
|
+
const serverStatus2 = serverDetails2?.status ?? mcp.getServerStatus(mcpName);
|
|
1446
|
+
if (serverStatus2 === "error" && serverDetails2?.error) {
|
|
1447
|
+
log.error(`[MCP] Connection error for ${mcpName}: ${serverDetails2.error}`);
|
|
1448
|
+
}
|
|
1449
|
+
log.info(`[MCP] Connected! Tools: ${tools.length}, status: ${serverStatus2}`);
|
|
1450
|
+
|
|
1451
|
+
// Update DB with status and tools
|
|
1452
|
+
getDb().query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ? OR name = ?`).run(serverStatus2 === "connected" ? "connected" : "error", tools.length, mcpName, mcpName);
|
|
1453
|
+
} else {
|
|
1454
|
+
log.error(`[MCP] Server not found in DB: ${mcpName}`)
|
|
1455
|
+
}
|
|
1456
|
+
} else {
|
|
1457
|
+
await mcp.disconnectServer(mcpName);
|
|
1458
|
+
getDb().query(`UPDATE mcp_servers SET status = ? WHERE id = ? OR name = ?`).run("disconnected", mcpName, mcpName);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
log.error(`[MCP] Failed to connect ${mcpName}: ${(error as Error).message}`);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return addCorsHeaders(Response.json({ success: true, active, message: active ? "Servidor MCP conectado" : "Servidor MCP desconectado" }), req)
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// ── Channels API ───────────────────────────────────────────────────
|
|
1470
|
+
if (url.pathname === "/api/channels" && req.method === "GET") {
|
|
1471
|
+
return await handleGetChannels(req, addCorsHeaders, channelManager);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// PUT /api/channels/:id - Update channel settings
|
|
1475
|
+
const channelIdMatch = url.pathname.match(/^\/api\/channels\/([^/]+)$/);
|
|
1476
|
+
if (channelIdMatch && req.method === "PUT") {
|
|
1477
|
+
const channelId = channelIdMatch[1];
|
|
1478
|
+
return await handleUpdateChannelSettings(req, addCorsHeaders, channelId);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (url.pathname.match(/^\/api\/channels\/[^/]+\/toggle$/)) {
|
|
1482
|
+
const channelId = url.pathname.split("/")[3];
|
|
1483
|
+
if (req.method === "POST") {
|
|
1484
|
+
return await handleToggleChannel(req, addCorsHeaders, channelId);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// GET /api/channels/:type/:id/status — connection state + QR for WhatsApp
|
|
1489
|
+
if (url.pathname.match(/^\/api\/channels\/[^/]+\/[^/]+\/status$/) && req.method === "GET") {
|
|
1490
|
+
return await handleGetChannelStatus(req, addCorsHeaders, channelManager);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// WhatsApp-specific endpoints
|
|
1494
|
+
// GET /api/channels/whatsapp/:id/details
|
|
1495
|
+
if (url.pathname.match(/^\/api\/channels\/whatsapp\/([^/]+)\/details$/) && req.method === "GET") {
|
|
1496
|
+
const accountId = url.pathname.split("/")[3];
|
|
1497
|
+
return await handleGetWhatsAppDetails(req, addCorsHeaders, accountId, channelManager);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// POST /api/channels/whatsapp/:id/disconnect
|
|
1501
|
+
if (url.pathname.match(/^\/api\/channels\/whatsapp\/([^/]+)\/disconnect$/) && req.method === "POST") {
|
|
1502
|
+
const accountId = url.pathname.split("/")[3];
|
|
1503
|
+
return await handleDisconnectWhatsApp(req, addCorsHeaders, accountId, channelManager);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// PUT /api/channels/whatsapp/:id/config
|
|
1507
|
+
if (url.pathname.match(/^\/api\/channels\/whatsapp\/([^/]+)\/config$/) && req.method === "PUT") {
|
|
1508
|
+
const accountId = url.pathname.split("/")[3];
|
|
1509
|
+
return await handleUpdateWhatsAppConfig(req, addCorsHeaders, accountId, channelManager);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// POST /api/channels/:id/reconnect — restart channel (with optional new credentials)
|
|
1513
|
+
if (url.pathname.match(/^\/api\/channels\/[^/]+\/reconnect$/) && req.method === "POST") {
|
|
1514
|
+
const channelId = url.pathname.split("/")[3];
|
|
1515
|
+
return await handleReconnectChannel(req, addCorsHeaders, channelId, channelManager);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// ── Voice API ───────────────────────────────────────────────────────
|
|
1519
|
+
if (url.pathname === "/api/voice/providers" && req.method === "GET") {
|
|
1520
|
+
return await handleGetVoiceProviders(req, addCorsHeaders)
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if (url.pathname === "/api/voice/configured-providers" && req.method === "GET") {
|
|
1524
|
+
return await handleGetConfiguredVoiceProviders(req, addCorsHeaders)
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// POST /api/voice/providers/:providerId/key - Save API key for a voice provider
|
|
1528
|
+
const voiceProviderKeyMatch = url.pathname.match(/^\/api\/voice\/providers\/([^/]+)\/key$/)
|
|
1529
|
+
if (voiceProviderKeyMatch && req.method === "POST") {
|
|
1530
|
+
return await handleSaveVoiceProviderKey(req, addCorsHeaders)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
if (url.pathname === "/api/voice/test" && req.method === "POST") {
|
|
1534
|
+
return await handleTestVoice(req, addCorsHeaders)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// GET /api/voice/:provider/voices - Get available voices for a provider
|
|
1538
|
+
const voiceProviderVoicesMatch = url.pathname.match(/^\/api\/voice\/([^/]+)\/voices$/)
|
|
1539
|
+
if (voiceProviderVoicesMatch && req.method === "GET") {
|
|
1540
|
+
const providerId = voiceProviderVoicesMatch[1]
|
|
1541
|
+
return await handleGetVoiceProviderVoices(req, addCorsHeaders, providerId)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// GET /api/channels/:id/voice - Get voice config for a channel
|
|
1545
|
+
const channelVoiceMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/voice$/)
|
|
1546
|
+
if (channelVoiceMatch && req.method === "GET") {
|
|
1547
|
+
const channelId = channelVoiceMatch[1]
|
|
1548
|
+
return await handleGetChannelVoice(req, addCorsHeaders, channelId)
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// PATCH /api/channels/:id/voice - Update voice config for a channel
|
|
1552
|
+
if (channelVoiceMatch && req.method === "PATCH") {
|
|
1553
|
+
const channelId = channelVoiceMatch[1]
|
|
1554
|
+
return await handleUpdateChannelVoice(req, addCorsHeaders, channelId)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ── Multimodal / Vision API ─────────────────────────────────────────
|
|
1558
|
+
if (url.pathname === "/api/multimodal/vision-providers" && req.method === "GET") {
|
|
1559
|
+
return await handleGetVisionProviders(req, addCorsHeaders)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (url.pathname === "/api/multimodal/ocr" && req.method === "POST") {
|
|
1563
|
+
return await handleOcrImage(req, addCorsHeaders)
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const channelVisionMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/vision$/)
|
|
1567
|
+
if (channelVisionMatch && req.method === "GET") {
|
|
1568
|
+
const channelId = channelVisionMatch[1]
|
|
1569
|
+
return await handleGetChannelVision(req, addCorsHeaders, channelId)
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (channelVisionMatch && req.method === "PATCH") {
|
|
1573
|
+
const channelId = channelVisionMatch[1]
|
|
1574
|
+
return await handleUpdateChannelVision(req, addCorsHeaders, channelId)
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// ── Piper TTS Local ──────────────────────────────────────────────────
|
|
1578
|
+
if (url.pathname === "/api/tts-local/status" && req.method === "GET") {
|
|
1579
|
+
return await handleGetLocalTTSStatus(req, addCorsHeaders)
|
|
1580
|
+
}
|
|
1581
|
+
if (url.pathname === "/api/tts-local/logs" && req.method === "GET") {
|
|
1582
|
+
return await handleGetLocalTTSLogs(req, addCorsHeaders)
|
|
1583
|
+
}
|
|
1584
|
+
if (url.pathname === "/api/tts-local/install" && req.method === "POST") {
|
|
1585
|
+
return await handleInstallLocalTTS(req, addCorsHeaders)
|
|
1586
|
+
}
|
|
1587
|
+
if (url.pathname === "/api/tts-local/start" && req.method === "POST") {
|
|
1588
|
+
return await handleStartLocalTTS(req, addCorsHeaders)
|
|
1589
|
+
}
|
|
1590
|
+
if (url.pathname === "/api/tts-local/stop" && req.method === "POST") {
|
|
1591
|
+
return await handleStopLocalTTS(req, addCorsHeaders)
|
|
1592
|
+
}
|
|
1593
|
+
if (url.pathname === "/api/tts-local/speak" && req.method === "POST") {
|
|
1594
|
+
return await handleSpeakLocalTTS(req, addCorsHeaders)
|
|
1595
|
+
}
|
|
1596
|
+
// Modelos
|
|
1597
|
+
if (url.pathname === "/api/tts-local/models" && req.method === "GET") {
|
|
1598
|
+
return await handleGetAvailableModels(req, addCorsHeaders)
|
|
1599
|
+
}
|
|
1600
|
+
if (url.pathname === "/api/tts-local/models/download" && req.method === "POST") {
|
|
1601
|
+
return await handleDownloadModel(req, addCorsHeaders)
|
|
1602
|
+
}
|
|
1603
|
+
if (url.pathname === "/api/tts-local/models/logs" && req.method === "GET") {
|
|
1604
|
+
return await handleGetDownloadLogs(req, addCorsHeaders)
|
|
1605
|
+
}
|
|
1606
|
+
if (url.pathname === "/api/tts-local/voices" && req.method === "GET") {
|
|
1607
|
+
return await handleGetInstalledVoices(req, addCorsHeaders)
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// ── LLM Local ────────────────────────────────────────────────────────
|
|
1611
|
+
if (url.pathname === "/api/llm-local/status" && req.method === "GET") {
|
|
1612
|
+
return await handleGetLocalLLMStatus(req, addCorsHeaders)
|
|
1613
|
+
}
|
|
1614
|
+
if (url.pathname === "/api/llm-local/logs" && req.method === "GET") {
|
|
1615
|
+
return await handleGetLocalLLMLogs(req, addCorsHeaders)
|
|
1616
|
+
}
|
|
1617
|
+
if (url.pathname === "/api/llm-local/install" && req.method === "POST") {
|
|
1618
|
+
return await handleInstallLocalLLM(req, addCorsHeaders)
|
|
1619
|
+
}
|
|
1620
|
+
if (url.pathname === "/api/llm-local/start" && req.method === "POST") {
|
|
1621
|
+
return await handleStartLocalLLM(req, addCorsHeaders)
|
|
1622
|
+
}
|
|
1623
|
+
if (url.pathname === "/api/llm-local/stop" && req.method === "POST") {
|
|
1624
|
+
return await handleStopLocalLLM(req, addCorsHeaders)
|
|
1625
|
+
}
|
|
1626
|
+
if (url.pathname === "/api/llm-local/download-model" && req.method === "POST") {
|
|
1627
|
+
return await handleDownloadLLMModel(req, addCorsHeaders)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// ── Meeting Transcription API ────────────────────────────────────────
|
|
1631
|
+
if (url.pathname === "/api/meetings" && req.method === "POST") {
|
|
1632
|
+
return await handleCreateMeeting(req, addCorsHeaders);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (url.pathname === "/api/meetings" && req.method === "GET") {
|
|
1636
|
+
return await handleListMeetings(req, addCorsHeaders);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const meetingIdMatch = url.pathname.match(/^\/api\/meetings\/([^/]+)$/);
|
|
1640
|
+
if (meetingIdMatch && req.method === "GET") {
|
|
1641
|
+
return await handleGetMeeting(req, addCorsHeaders, meetingIdMatch[1]);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const meetingSegmentMatch = url.pathname.match(/^\/api\/meetings\/([^/]+)\/segments$/);
|
|
1645
|
+
if (meetingSegmentMatch && req.method === "POST") {
|
|
1646
|
+
return await handleAddMeetingSegment(req, addCorsHeaders, meetingSegmentMatch[1]);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const meetingStopMatch = url.pathname.match(/^\/api\/meetings\/([^/]+)\/stop$/);
|
|
1650
|
+
if (meetingStopMatch && req.method === "POST") {
|
|
1651
|
+
return await handleStopMeeting(req, addCorsHeaders, meetingStopMatch[1]);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// ── Chat / Canvas / Notes API ───────────────────────────────────────
|
|
1655
|
+
if (url.pathname === "/api/chat/history" && req.method === "GET") {
|
|
1656
|
+
return await handleGetChatHistory(req, addCorsHeaders)
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (url.pathname === "/api/chat" && req.method === "POST") {
|
|
1660
|
+
return await handlePostChat(req, addCorsHeaders)
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (url.pathname === "/api/canvas" && req.method === "GET") {
|
|
1664
|
+
return await handleGetCanvas(req, addCorsHeaders)
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (url.pathname === "/api/notes" && req.method === "GET") {
|
|
1668
|
+
return await handleGetNotes(req, addCorsHeaders)
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// ── Cron Jobs API ──────────────────────────────────────────────────
|
|
1672
|
+
const cronMatch = url.pathname.match(/^\/api\/cron(\/[^/]+)?(\/[^/]+)?$/);
|
|
1673
|
+
if (cronMatch && req.method === "GET" && !cronMatch[2]) {
|
|
1674
|
+
if (cronMatch[1] === "/status") {
|
|
1675
|
+
return await handleGetCronStatus(req, addCorsHeaders);
|
|
1676
|
+
}
|
|
1677
|
+
if (cronMatch[1] === "/channels") {
|
|
1678
|
+
return await handleGetCronChannels(req, addCorsHeaders);
|
|
1679
|
+
}
|
|
1680
|
+
if (cronMatch[1]) {
|
|
1681
|
+
const taskId = cronMatch[1].slice(1);
|
|
1682
|
+
return await handleGetCronJob(req, addCorsHeaders, taskId);
|
|
1683
|
+
}
|
|
1684
|
+
return await handleGetCronJobs(req, addCorsHeaders);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
if (cronMatch && req.method === "POST" && !cronMatch[2]) {
|
|
1688
|
+
return await handleCreateCronJob(req, addCorsHeaders);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (cronMatch && req.method === "GET" && cronMatch[2] === "/history") {
|
|
1692
|
+
const taskId = cronMatch[1]?.slice(1);
|
|
1693
|
+
return await handleGetCronJobHistory(req, addCorsHeaders, taskId || "");
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
if (cronMatch && req.method === "POST" && cronMatch[2] === "/pause") {
|
|
1697
|
+
const taskId = cronMatch[1]?.slice(1);
|
|
1698
|
+
return await handlePauseCronJob(req, addCorsHeaders, taskId || "");
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (cronMatch && req.method === "POST" && cronMatch[2] === "/resume") {
|
|
1702
|
+
const taskId = cronMatch[1]?.slice(1);
|
|
1703
|
+
return await handleResumeCronJob(req, addCorsHeaders, taskId || "");
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (cronMatch && req.method === "POST" && cronMatch[2] === "/trigger") {
|
|
1707
|
+
const taskId = cronMatch[1]?.slice(1);
|
|
1708
|
+
return await handleTriggerCronJob(req, addCorsHeaders, taskId || "");
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (cronMatch && req.method === "PATCH" && cronMatch[1] && !cronMatch[2]) {
|
|
1712
|
+
const taskId = cronMatch[1].slice(1);
|
|
1713
|
+
return await handleUpdateCronJob(req, addCorsHeaders, taskId);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (cronMatch && req.method === "DELETE" && cronMatch[1] && !cronMatch[2]) {
|
|
1717
|
+
const taskId = cronMatch[1].slice(1);
|
|
1718
|
+
return await handleDeleteCronJob(req, addCorsHeaders, taskId);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
return addCorsHeaders(new Response("Not Found", { status: 404 }), req)
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
try {
|
|
1725
|
+
const response = await handleRequest();
|
|
1726
|
+
const duration = Date.now() - start;
|
|
1727
|
+
if (response) {
|
|
1728
|
+
logRequest(response.status, duration);
|
|
1729
|
+
} else {
|
|
1730
|
+
// Bun upgrade returns undefined on success
|
|
1731
|
+
log.info(`${method} ${url.pathname} - 101 Switching Protocols(${duration}ms)`);
|
|
1732
|
+
}
|
|
1733
|
+
return response;
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
const duration = Date.now() - start;
|
|
1736
|
+
log.error(`${method} ${url.pathname} - Internal Error(${duration}ms): ${(error as Error).message} `);
|
|
1737
|
+
return addCorsHeaders(Response.json({ success: false, error: (error as Error).message, message: "Error interno del servidor" }, { status: 500 }), req);
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
|
|
1741
|
+
websocket: {
|
|
1742
|
+
open(ws) {
|
|
1743
|
+
const data = ws.data;
|
|
1744
|
+
// ── Meeting Stream ─────────────────────────────────────────────────────
|
|
1745
|
+
if (data.sessionId.startsWith("meeting:")) {
|
|
1746
|
+
log.info(`Meeting stream client connected: ${data.sessionId}`);
|
|
1747
|
+
ws.send(JSON.stringify({ type: "meeting:connected", sessionId: data.sessionId, meetingSessionId: (data as any).meetingSessionId }));
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// ── LLM Local ─────────────────────────────────────────────────────────
|
|
1752
|
+
if (data.sessionId.startsWith("llm-local:")) {
|
|
1753
|
+
log.info(`Local LLM client connected: ${data.sessionId}`);
|
|
1754
|
+
ws.send(JSON.stringify({ type: "llm-local:connected", sessionId: data.sessionId }));
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
log.debug(`WebSocket connected: ${data.sessionId} `);
|
|
1759
|
+
|
|
1760
|
+
sessionManager.create(data.sessionId, ws);
|
|
1761
|
+
|
|
1762
|
+
const channel = channelManager?.getChannel("webchat") as any;
|
|
1763
|
+
if (channel?.registerConnection) channel.registerConnection(ws);
|
|
1764
|
+
|
|
1765
|
+
// Send status message
|
|
1766
|
+
ws.send(JSON.stringify({
|
|
1767
|
+
type: "status",
|
|
1768
|
+
sessionId: data.sessionId,
|
|
1769
|
+
status: { state: "connected", model: `${dbProvider}/${dbModel}` },
|
|
1770
|
+
} as OutboundMessage));
|
|
1771
|
+
|
|
1772
|
+
// Send welcome message with real user data
|
|
1773
|
+
try {
|
|
1774
|
+
const db = getDb();
|
|
1775
|
+
const user = db.query("SELECT id, name, language FROM users LIMIT 1").get() as { id: string; name: string; language: string } | undefined;
|
|
1776
|
+
const agent = db.query("SELECT id, name, provider_id, model_id FROM agents WHERE role = 'coordinator' LIMIT 1").get() as { id: string; name: string; provider_id: string; model_id: string } | undefined;
|
|
1777
|
+
|
|
1778
|
+
// Get channels
|
|
1779
|
+
const channels = db.query("SELECT id FROM channels WHERE active = 1").all() as Array<{ id: string }>;
|
|
1780
|
+
|
|
1781
|
+
// Get voice config from webchat channel
|
|
1782
|
+
const voiceConfig = db.query("SELECT voice_enabled, stt_provider, tts_provider FROM channels WHERE id = 'webchat'").get() as { voice_enabled: number; stt_provider: string; tts_provider: string } | undefined;
|
|
1783
|
+
|
|
1784
|
+
ws.send(JSON.stringify({
|
|
1785
|
+
type: "welcome",
|
|
1786
|
+
sessionId: data.sessionId,
|
|
1787
|
+
user: user ? { id: user.id, name: user.name, language: user.language } : null,
|
|
1788
|
+
agent: agent ? { id: agent.id, name: agent.name, provider: agent.provider_id, model: agent.model_id } : null,
|
|
1789
|
+
channels: channels.map(c => c.id),
|
|
1790
|
+
voice: voiceConfig ? {
|
|
1791
|
+
enabled: voiceConfig.voice_enabled === 1,
|
|
1792
|
+
sttProvider: voiceConfig.stt_provider,
|
|
1793
|
+
ttsProvider: voiceConfig.tts_provider
|
|
1794
|
+
} : { enabled: false, sttProvider: null, ttsProvider: null },
|
|
1795
|
+
} as OutboundMessage));
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
log.error("Error sending welcome message:", err);
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
|
|
1801
|
+
async message(ws, message) {
|
|
1802
|
+
const data = ws.data;
|
|
1803
|
+
|
|
1804
|
+
// LLM Local
|
|
1805
|
+
if (data.sessionId.startsWith("llm-local:")) {
|
|
1806
|
+
const { handleLLMWebSocket } = await import("./llm-local/server");
|
|
1807
|
+
await handleLLMWebSocket(ws as any, message.toString());
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Bridge events clients are read-only; only respond to ping keepalive
|
|
1812
|
+
if (data.sessionId.startsWith("bridge:")) {
|
|
1813
|
+
try {
|
|
1814
|
+
const m = JSON.parse(message.toString());
|
|
1815
|
+
if (m?.type === "ping") ws.send(JSON.stringify({ type: "pong" }));
|
|
1816
|
+
} catch { /* ignore */ }
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Meeting stream: handle audio chunks and stop commands
|
|
1821
|
+
if (data.sessionId.startsWith("meeting:")) {
|
|
1822
|
+
try {
|
|
1823
|
+
const m = JSON.parse(message.toString()) as Record<string, unknown>;
|
|
1824
|
+
if (m?.type === "ping") {
|
|
1825
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
if (m?.type === "audio_chunk") {
|
|
1829
|
+
const meetingSessionId = (data as any).meetingSessionId as string;
|
|
1830
|
+
const audioBase64 = m.audio as string;
|
|
1831
|
+
const speaker = (m.speaker as string) || null;
|
|
1832
|
+
const mimeType = (m.mime_type as string) || "audio/webm";
|
|
1833
|
+
(async () => {
|
|
1834
|
+
try {
|
|
1835
|
+
const db = getDb();
|
|
1836
|
+
const session = db.query(
|
|
1837
|
+
`SELECT id, stt_model, status FROM meeting_sessions WHERE id = ?`
|
|
1838
|
+
).get(meetingSessionId) as { id: string; stt_model: string; status: string } | undefined;
|
|
1839
|
+
if (!session || session.status !== "active") {
|
|
1840
|
+
ws.send(JSON.stringify({ type: "error", error: "Sesión no activa o no encontrada" }));
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const transcription = await voiceService.transcribe(
|
|
1844
|
+
{ type: "base64", data: audioBase64, mimeType },
|
|
1845
|
+
session.stt_model
|
|
1846
|
+
);
|
|
1847
|
+
const seqResult = db.query(
|
|
1848
|
+
`SELECT COALESCE(MAX(seq) + 1, 0) as next_seq FROM meeting_segments WHERE session_id = ?`
|
|
1849
|
+
).get(meetingSessionId) as { next_seq: number };
|
|
1850
|
+
const seq = seqResult.next_seq;
|
|
1851
|
+
db.query(
|
|
1852
|
+
`INSERT INTO meeting_segments (session_id, seq, speaker, text) VALUES (?, ?, ?, ?)`
|
|
1853
|
+
).run(meetingSessionId, seq, speaker, transcription);
|
|
1854
|
+
ws.send(JSON.stringify({ type: "transcript_segment", seq, speaker, text: transcription }));
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
ws.send(JSON.stringify({ type: "error", error: (err as Error).message }));
|
|
1857
|
+
}
|
|
1858
|
+
})();
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (m?.type === "meeting_stop") {
|
|
1862
|
+
const meetingSessionId = (data as any).meetingSessionId as string;
|
|
1863
|
+
try {
|
|
1864
|
+
const db = getDb();
|
|
1865
|
+
db.query(
|
|
1866
|
+
`UPDATE meeting_sessions SET status = 'stopped', stopped_at = unixepoch() WHERE id = ? AND status = 'active'`
|
|
1867
|
+
).run(meetingSessionId);
|
|
1868
|
+
const countResult = db.query(
|
|
1869
|
+
`SELECT COUNT(*) as count FROM meeting_segments WHERE session_id = ?`
|
|
1870
|
+
).get(meetingSessionId) as { count: number };
|
|
1871
|
+
ws.send(JSON.stringify({ type: "meeting_stopped", session_id: meetingSessionId, segment_count: countResult.count }));
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
ws.send(JSON.stringify({ type: "error", error: (err as Error).message }));
|
|
1874
|
+
}
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
} catch { /* ignore malformed messages */ }
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
let msg: InboundMessage;
|
|
1886
|
+
try {
|
|
1887
|
+
msg = JSON.parse(message.toString()) as InboundMessage;
|
|
1888
|
+
} catch {
|
|
1889
|
+
ws.send(JSON.stringify({
|
|
1890
|
+
type: "error",
|
|
1891
|
+
sessionId: data.sessionId,
|
|
1892
|
+
error: "Invalid JSON message",
|
|
1893
|
+
} as OutboundMessage));
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
msg.sessionId = msg.sessionId ?? data.sessionId;
|
|
1898
|
+
sessionManager.touch(msg.sessionId);
|
|
1899
|
+
|
|
1900
|
+
if (msg.type === "ping") {
|
|
1901
|
+
ws.send(JSON.stringify({ type: "pong", sessionId: msg.sessionId } as OutboundMessage));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Canvas subscribe
|
|
1906
|
+
if (msg.type === "canvas_subscribe") {
|
|
1907
|
+
subscribeCanvas(ws);
|
|
1908
|
+
canvasManager.registerSession(`canvas:${data.sessionId}`, ws);
|
|
1909
|
+
ws.send(JSON.stringify({
|
|
1910
|
+
type: "canvas:snapshot",
|
|
1911
|
+
data: getCanvasSnapshot(),
|
|
1912
|
+
}));
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Canvas unsubscribe
|
|
1917
|
+
if (msg.type === "canvas_unsubscribe") {
|
|
1918
|
+
unsubscribeCanvas(ws);
|
|
1919
|
+
canvasManager.unregisterSession(`canvas:${data.sessionId}`);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// A2UI actions — user interacted with an A2UI surface component
|
|
1924
|
+
if (msg.type === "a2ui:action") {
|
|
1925
|
+
const actionData = (msg.data ?? msg) as Record<string, unknown>;
|
|
1926
|
+
const actionName = actionData.name as string ?? "action";
|
|
1927
|
+
const surfaceId = actionData.surfaceId as string ?? "unknown";
|
|
1928
|
+
const sourceComponentId = actionData.sourceComponentId as string ?? "unknown";
|
|
1929
|
+
const context = actionData.context as Record<string, unknown> ?? {};
|
|
1930
|
+
|
|
1931
|
+
const interactionMsg = `[a2ui:action] surface=${surfaceId} action=${actionName} component=${sourceComponentId}${Object.keys(context).length > 0 ? ` context=${JSON.stringify(context)}` : ""}`;
|
|
1932
|
+
log.info(`A2UI action forwarded to agent: ${interactionMsg}`);
|
|
1933
|
+
|
|
1934
|
+
const sessionId = data.sessionId;
|
|
1935
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: true, sessionId } as OutboundMessage));
|
|
1936
|
+
|
|
1937
|
+
laneQueue.enqueue(sessionId, async (_task, signal) => {
|
|
1938
|
+
if (signal.aborted) {
|
|
1939
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
try {
|
|
1943
|
+
const { userId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
|
|
1944
|
+
const messages = [{ role: "user" as const, content: interactionMsg }];
|
|
1945
|
+
let streamedContent = "";
|
|
1946
|
+
const messageId = crypto.randomUUID();
|
|
1947
|
+
|
|
1948
|
+
const response = await runner.generate({
|
|
1949
|
+
provider: dbProvider as any,
|
|
1950
|
+
messages,
|
|
1951
|
+
maxTokens: 4096,
|
|
1952
|
+
tools: prepareTools(agent, sessionId),
|
|
1953
|
+
maxSteps: 15,
|
|
1954
|
+
threadId: sessionId,
|
|
1955
|
+
userId,
|
|
1956
|
+
onToken: async (token: string) => {
|
|
1957
|
+
if (signal.aborted) return;
|
|
1958
|
+
streamedContent += token;
|
|
1959
|
+
ws.send(JSON.stringify({ type: "message", id: messageId, sessionId, content: token, isChunk: true, isStep: false } as OutboundMessage));
|
|
1960
|
+
},
|
|
1961
|
+
onStep: async (step) => {
|
|
1962
|
+
if (signal.aborted) return;
|
|
1963
|
+
log.debug(`[a2ui:action TOOL] ${step.type}: ${step.toolName || ""}`);
|
|
1964
|
+
},
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
|
|
1968
|
+
|
|
1969
|
+
const content = streamedContent || response.content?.trim() || "";
|
|
1970
|
+
if (content && streamedContent.length === 0) {
|
|
1971
|
+
ws.send(JSON.stringify({ type: "message", sessionId, content, isStep: false } as OutboundMessage));
|
|
1972
|
+
}
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
|
|
1975
|
+
ws.send(JSON.stringify({ type: "error", sessionId, error: (error as Error).message } as OutboundMessage));
|
|
1976
|
+
log.error(`A2UI action agent error: ${(error as Error).message}`);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Canvas interactions from the main session — route to canvasManager and local resolver
|
|
1984
|
+
if (msg.type === "canvas:interact") {
|
|
1985
|
+
const { componentId } = msg;
|
|
1986
|
+
let resolvedByPending = false;
|
|
1987
|
+
if (componentId) {
|
|
1988
|
+
removeCanvasComponent(componentId);
|
|
1989
|
+
// Resolve local pending interactions (canvas_confirm tool)
|
|
1990
|
+
resolvedByPending = resolveCanvasInteraction(componentId, msg.data);
|
|
1991
|
+
}
|
|
1992
|
+
const canvasSessionId = `canvas:${data.sessionId}`;
|
|
1993
|
+
canvasManager.handleMessage(canvasSessionId, JSON.stringify(msg));
|
|
1994
|
+
|
|
1995
|
+
// If no tool was waiting for this interaction, forward it to the agent as a new turn
|
|
1996
|
+
if (!resolvedByPending) {
|
|
1997
|
+
const interactionData = msg.data as Record<string, unknown> | undefined;
|
|
1998
|
+
const action = (msg as any).action as string | undefined;
|
|
1999
|
+
|
|
2000
|
+
// Build a human-readable message describing what the user clicked
|
|
2001
|
+
let interactionMsg = `[canvas:interact] componentId=${componentId ?? "unknown"}, action=${action ?? "click"}`;
|
|
2002
|
+
if (interactionData && Object.keys(interactionData).length > 0) {
|
|
2003
|
+
interactionMsg += `, data=${JSON.stringify(interactionData)}`;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
log.info(`Canvas interaction forwarded to agent: ${interactionMsg}`);
|
|
2007
|
+
|
|
2008
|
+
const sessionId = data.sessionId;
|
|
2009
|
+
|
|
2010
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: true, sessionId } as OutboundMessage));
|
|
2011
|
+
|
|
2012
|
+
laneQueue.enqueue(sessionId, async (_task, signal) => {
|
|
2013
|
+
if (signal.aborted) {
|
|
2014
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
const { userId } = resolveContext({ channel: "webchat", channelUserId: sessionId });
|
|
2019
|
+
const messages = [{ role: "user" as const, content: interactionMsg }];
|
|
2020
|
+
let streamedContent = "";
|
|
2021
|
+
const messageId = crypto.randomUUID();
|
|
2022
|
+
|
|
2023
|
+
const response = await runner.generate({
|
|
2024
|
+
provider: dbProvider as any,
|
|
2025
|
+
messages,
|
|
2026
|
+
maxTokens: 4096,
|
|
2027
|
+
tools: prepareTools(agent, sessionId),
|
|
2028
|
+
maxSteps: 15,
|
|
2029
|
+
threadId: sessionId,
|
|
2030
|
+
userId,
|
|
2031
|
+
onToken: async (token: string) => {
|
|
2032
|
+
if (signal.aborted) return;
|
|
2033
|
+
streamedContent += token;
|
|
2034
|
+
ws.send(JSON.stringify({ type: "message", id: messageId, sessionId, content: token, isChunk: true, isStep: false } as OutboundMessage));
|
|
2035
|
+
},
|
|
2036
|
+
onStep: async (step) => {
|
|
2037
|
+
if (signal.aborted) return;
|
|
2038
|
+
log.debug(`[canvas:interact TOOL] ${step.type}: ${step.toolName || ""}`);
|
|
2039
|
+
},
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
|
|
2043
|
+
|
|
2044
|
+
const content = streamedContent || response.content?.trim() || "";
|
|
2045
|
+
if (content && streamedContent.length === 0) {
|
|
2046
|
+
ws.send(JSON.stringify({ type: "message", sessionId, content, isStep: false } as OutboundMessage));
|
|
2047
|
+
}
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId } as OutboundMessage));
|
|
2050
|
+
ws.send(JSON.stringify({ type: "error", sessionId, error: (error as Error).message } as OutboundMessage));
|
|
2051
|
+
log.error(`Canvas interact agent error: ${(error as Error).message}`);
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Canvas session - handle interactions
|
|
2060
|
+
if (msg.type === "command" || (msg.content && isSlashCommand(msg.content))) {
|
|
2061
|
+
const result = await executeSlashCommand(msg.sessionId, msg.content ?? `/${msg.command}`, ws);
|
|
2062
|
+
if (result) {
|
|
2063
|
+
ws.send(JSON.stringify(result));
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// Logs subscription
|
|
2069
|
+
if (msg.type === "logs_subscribe") {
|
|
2070
|
+
logSubscribers.add(data.sessionId);
|
|
2071
|
+
log.debug(`Session ${data.sessionId} subscribed to logs`);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (msg.type === "logs_unsubscribe") {
|
|
2076
|
+
logSubscribers.delete(data.sessionId);
|
|
2077
|
+
log.debug(`Session ${data.sessionId} unsubscribed from logs`);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Stop generation (like ChatGPT/Claude stop button)
|
|
2082
|
+
if (msg.type === "stop") {
|
|
2083
|
+
const cancelled = laneQueue.cancel(msg.sessionId);
|
|
2084
|
+
log.info(`[stop] Session ${msg.sessionId} — cancelled: ${cancelled}`);
|
|
2085
|
+
ws.send(JSON.stringify({
|
|
2086
|
+
type: "typing",
|
|
2087
|
+
isTyping: false,
|
|
2088
|
+
sessionId: msg.sessionId,
|
|
2089
|
+
} as OutboundMessage));
|
|
2090
|
+
if (cancelled) {
|
|
2091
|
+
ws.send(JSON.stringify({
|
|
2092
|
+
type: "status",
|
|
2093
|
+
sessionId: msg.sessionId,
|
|
2094
|
+
status: { state: "cancelled" },
|
|
2095
|
+
} as OutboundMessage));
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// Handle audio messages from WebChat
|
|
2101
|
+
let webchatPreferAudio = false;
|
|
2102
|
+
if (msg.type === "audio" && msg.audio) {
|
|
2103
|
+
log.info(`WebChat audio from session ${msg.sessionId}`);
|
|
2104
|
+
|
|
2105
|
+
const voiceConfig = voiceService.getChannelVoiceConfig("webchat");
|
|
2106
|
+
|
|
2107
|
+
if (!voiceConfig.voiceEnabled) {
|
|
2108
|
+
ws.send(JSON.stringify({
|
|
2109
|
+
type: "error",
|
|
2110
|
+
sessionId: msg.sessionId,
|
|
2111
|
+
error: "Voice input not enabled for this channel"
|
|
2112
|
+
} as OutboundMessage));
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (!voiceConfig.sttProvider) {
|
|
2117
|
+
ws.send(JSON.stringify({
|
|
2118
|
+
type: "message",
|
|
2119
|
+
sessionId: msg.sessionId,
|
|
2120
|
+
content: "🎙️ Para usar notas de voz, configura el proveedor STT en Configuración > Canales > WebChat (ej: groq-whisper)"
|
|
2121
|
+
} as OutboundMessage));
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
ws.send(JSON.stringify({
|
|
2126
|
+
type: "typing",
|
|
2127
|
+
isTyping: true,
|
|
2128
|
+
sessionId: msg.sessionId,
|
|
2129
|
+
} as OutboundMessage));
|
|
2130
|
+
|
|
2131
|
+
try {
|
|
2132
|
+
const audioInput = { type: "base64" as const, data: msg.audio, mimeType: "audio/webm" };
|
|
2133
|
+
const sttProvider = voiceConfig.sttProvider || "groq-whisper";
|
|
2134
|
+
const messageContent = await voiceService.transcribe(audioInput, sttProvider);
|
|
2135
|
+
|
|
2136
|
+
log.info(`📝 Transcribed: ${messageContent.substring(0, 100)}...`);
|
|
2137
|
+
|
|
2138
|
+
webchatPreferAudio = true;
|
|
2139
|
+
|
|
2140
|
+
ws.send(JSON.stringify({
|
|
2141
|
+
type: "message",
|
|
2142
|
+
sessionId: msg.sessionId,
|
|
2143
|
+
content: `🎙️ Transcripción: ${messageContent}`
|
|
2144
|
+
} as OutboundMessage));
|
|
2145
|
+
|
|
2146
|
+
ws.send(JSON.stringify({
|
|
2147
|
+
type: "typing",
|
|
2148
|
+
isTyping: false,
|
|
2149
|
+
sessionId: msg.sessionId,
|
|
2150
|
+
} as OutboundMessage));
|
|
2151
|
+
|
|
2152
|
+
laneQueue.enqueue(msg.sessionId, async (_task, signal) => {
|
|
2153
|
+
if (signal.aborted) {
|
|
2154
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
|
|
2155
|
+
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, error: "Task cancelled" } as OutboundMessage));
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
try {
|
|
2160
|
+
const unifiedSessionId = msg.sessionId;
|
|
2161
|
+
const messages = [{ role: "user" as const, content: messageContent }];
|
|
2162
|
+
log.info(`Generating response for session ${unifiedSessionId}...`);
|
|
2163
|
+
|
|
2164
|
+
const { userId } = resolveContext({
|
|
2165
|
+
channel: "webchat",
|
|
2166
|
+
channelUserId: msg.sessionId,
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
// Streaming: send tokens as they arrive
|
|
2170
|
+
let streamedContent = "";
|
|
2171
|
+
let messageId = crypto.randomUUID();
|
|
2172
|
+
|
|
2173
|
+
const response = await runner.generate({
|
|
2174
|
+
provider: dbProvider as any,
|
|
2175
|
+
messages,
|
|
2176
|
+
maxTokens: 4096,
|
|
2177
|
+
tools: prepareTools(agent, unifiedSessionId),
|
|
2178
|
+
maxSteps: 15,
|
|
2179
|
+
threadId: unifiedSessionId,
|
|
2180
|
+
userId,
|
|
2181
|
+
onToken: async (token: string) => {
|
|
2182
|
+
if (signal.aborted) return;
|
|
2183
|
+
streamedContent += token;
|
|
2184
|
+
// Send chunk to client
|
|
2185
|
+
ws.send(JSON.stringify({
|
|
2186
|
+
type: "message",
|
|
2187
|
+
id: messageId,
|
|
2188
|
+
sessionId: unifiedSessionId,
|
|
2189
|
+
content: token,
|
|
2190
|
+
isChunk: true,
|
|
2191
|
+
isStep: false,
|
|
2192
|
+
} as OutboundMessage));
|
|
2193
|
+
},
|
|
2194
|
+
onStep: async (step) => {
|
|
2195
|
+
if (signal.aborted) return;
|
|
2196
|
+
|
|
2197
|
+
// "text" = el agente narra lo que esta pensando/haciendo
|
|
2198
|
+
if (step.type === "text" && step.message) {
|
|
2199
|
+
const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
|
|
2200
|
+
if (trimmedMessage) {
|
|
2201
|
+
ws.send(JSON.stringify({
|
|
2202
|
+
type: "progress",
|
|
2203
|
+
sessionId: unifiedSessionId,
|
|
2204
|
+
content: trimmedMessage,
|
|
2205
|
+
} as OutboundMessage));
|
|
2206
|
+
}
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
|
|
2211
|
+
if (step.type === "tool_call" && step.toolName) {
|
|
2212
|
+
const narration = getNarration(step.toolName);
|
|
2213
|
+
ws.send(JSON.stringify({
|
|
2214
|
+
type: "progress",
|
|
2215
|
+
sessionId: unifiedSessionId,
|
|
2216
|
+
content: narration,
|
|
2217
|
+
} as OutboundMessage));
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// "tool_result" = resultado de herramienta → solo si pide enviarse al usuario
|
|
2222
|
+
if (step.type === "tool_result" && step.message) {
|
|
2223
|
+
try {
|
|
2224
|
+
const result = JSON.parse(step.message);
|
|
2225
|
+
if (result._sendToUser || result.status) {
|
|
2226
|
+
const userMessage = result.message || result.status || "";
|
|
2227
|
+
if (userMessage) {
|
|
2228
|
+
ws.send(JSON.stringify({
|
|
2229
|
+
type: "progress",
|
|
2230
|
+
sessionId: unifiedSessionId,
|
|
2231
|
+
content: userMessage,
|
|
2232
|
+
} as OutboundMessage));
|
|
2233
|
+
}
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
} catch { }
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
// Use streamed content from onToken, fallback to response.content
|
|
2242
|
+
const content = streamedContent || response.content?.trim() || "";
|
|
2243
|
+
log.info(`Response sent to session ${unifiedSessionId} (${content.length} chars)`);
|
|
2244
|
+
|
|
2245
|
+
const voiceCfg = voiceService.getChannelVoiceConfig("webchat");
|
|
2246
|
+
const shouldSpeak = webchatPreferAudio;
|
|
2247
|
+
let responseType: "text" | "audio" = "text";
|
|
2248
|
+
let ttsProviderUsed: string | null = null;
|
|
2249
|
+
let ttsMimeType: string | null = null;
|
|
2250
|
+
|
|
2251
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
|
|
2252
|
+
|
|
2253
|
+
// Don't send text message if already streamed (content came via onToken)
|
|
2254
|
+
const alreadyStreamed = streamedContent.length > 0;
|
|
2255
|
+
|
|
2256
|
+
if (content && !alreadyStreamed) {
|
|
2257
|
+
if (shouldSpeak) {
|
|
2258
|
+
if (!voiceCfg.ttsProvider) {
|
|
2259
|
+
ws.send(JSON.stringify({
|
|
2260
|
+
type: "message",
|
|
2261
|
+
sessionId: unifiedSessionId,
|
|
2262
|
+
content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
|
|
2263
|
+
isStep: false,
|
|
2264
|
+
} as OutboundMessage));
|
|
2265
|
+
} else {
|
|
2266
|
+
try {
|
|
2267
|
+
log.info(`🔊 TTS enabled, synthesizing audio for WebChat...`);
|
|
2268
|
+
const audioOutput = await voiceService.speak(content, voiceCfg.ttsProvider, voiceCfg.ttsVoiceId || undefined);
|
|
2269
|
+
ttsProviderUsed = voiceCfg.ttsProvider;
|
|
2270
|
+
ttsMimeType = audioOutput.mimeType;
|
|
2271
|
+
responseType = "audio";
|
|
2272
|
+
const base64Audio = (audioOutput.data as Buffer).toString("base64");
|
|
2273
|
+
log.info(`Audio generated: ${base64Audio.length} bytes, mimeType: ${audioOutput.mimeType}`);
|
|
2274
|
+
ws.send(JSON.stringify({
|
|
2275
|
+
type: "message",
|
|
2276
|
+
sessionId: unifiedSessionId,
|
|
2277
|
+
content,
|
|
2278
|
+
audio: base64Audio,
|
|
2279
|
+
mimeType: audioOutput.mimeType,
|
|
2280
|
+
isStep: false
|
|
2281
|
+
} as OutboundMessage));
|
|
2282
|
+
} catch (ttsError) {
|
|
2283
|
+
log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
|
|
2284
|
+
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
} else {
|
|
2288
|
+
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
2289
|
+
}
|
|
2290
|
+
} else if (alreadyStreamed && shouldSpeak && voiceCfg.ttsProvider) {
|
|
2291
|
+
try {
|
|
2292
|
+
log.info(`🔊 TTS enabled, synthesizing audio after streaming...`);
|
|
2293
|
+
const audioOutput = await voiceService.speak(content, voiceCfg.ttsProvider, voiceCfg.ttsVoiceId || undefined);
|
|
2294
|
+
const base64Audio = (audioOutput.data as Buffer).toString("base64");
|
|
2295
|
+
log.info(`Audio generated after streaming: ${base64Audio.length} bytes`);
|
|
2296
|
+
ws.send(JSON.stringify({
|
|
2297
|
+
type: "message",
|
|
2298
|
+
sessionId: unifiedSessionId,
|
|
2299
|
+
content,
|
|
2300
|
+
audio: base64Audio,
|
|
2301
|
+
mimeType: audioOutput.mimeType,
|
|
2302
|
+
isStep: false
|
|
2303
|
+
} as OutboundMessage));
|
|
2304
|
+
} catch (ttsError) {
|
|
2305
|
+
log.error(`TTS after streaming failed: ${(ttsError as Error).message}), skipping audio`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
|
|
2310
|
+
ws.send(JSON.stringify({
|
|
2311
|
+
type: "error",
|
|
2312
|
+
sessionId: msg.sessionId,
|
|
2313
|
+
error: (error as Error).message,
|
|
2314
|
+
} as OutboundMessage));
|
|
2315
|
+
log.error(`Error for session ${msg.sessionId}: ${(error as Error).message}`);
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
ws.send(JSON.stringify({
|
|
2320
|
+
type: "typing",
|
|
2321
|
+
isTyping: false,
|
|
2322
|
+
sessionId: msg.sessionId,
|
|
2323
|
+
} as OutboundMessage));
|
|
2324
|
+
ws.send(JSON.stringify({
|
|
2325
|
+
type: "error",
|
|
2326
|
+
sessionId: msg.sessionId,
|
|
2327
|
+
error: `Transcription failed: ${(error as Error).message}`
|
|
2328
|
+
} as OutboundMessage));
|
|
2329
|
+
}
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (msg.type === "message" && msg.content) {
|
|
2334
|
+
log.info(`WebChat message from session ${msg.sessionId}: ${msg.content.substring(0, 100)}`);
|
|
2335
|
+
|
|
2336
|
+
// FIX 6 — typing indicator inmediato ANTES de encolar
|
|
2337
|
+
// El usuario ve "escribiendo..." de inmediato, no después del queue
|
|
2338
|
+
ws.send(JSON.stringify({
|
|
2339
|
+
type: "typing",
|
|
2340
|
+
isTyping: true,
|
|
2341
|
+
sessionId: msg.sessionId,
|
|
2342
|
+
} as OutboundMessage));
|
|
2343
|
+
|
|
2344
|
+
laneQueue.enqueue(msg.sessionId, async (_task, signal) => {
|
|
2345
|
+
if (signal.aborted) {
|
|
2346
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
|
|
2347
|
+
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, error: "Task cancelled" } as OutboundMessage));
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
try {
|
|
2352
|
+
const unifiedSessionId = msg.sessionId;
|
|
2353
|
+
|
|
2354
|
+
// Multimodal: process image/document if present
|
|
2355
|
+
let finalMessageContent = msg.content;
|
|
2356
|
+
let contentParts: any[] | undefined = undefined;
|
|
2357
|
+
const visionConfig = multimodalService.getChannelVisionConfig("webchat");
|
|
2358
|
+
|
|
2359
|
+
if (msg.image || msg.document) {
|
|
2360
|
+
log.info(`🖼️ Multimodal content detected from WebChat session ${unifiedSessionId}`);
|
|
2361
|
+
|
|
2362
|
+
if (msg.image) {
|
|
2363
|
+
try {
|
|
2364
|
+
const imageInput = {
|
|
2365
|
+
type: "base64" as const,
|
|
2366
|
+
data: msg.image.base64,
|
|
2367
|
+
mimeType: msg.image.mimeType || "image/jpeg",
|
|
2368
|
+
caption: msg.image.caption
|
|
2369
|
+
};
|
|
2370
|
+
|
|
2371
|
+
const activeModelId = dbModel;
|
|
2372
|
+
const activeProviderId = dbProvider;
|
|
2373
|
+
const modelHasVision = activeModelId && activeProviderId
|
|
2374
|
+
? multimodalService.modelSupportsVision(activeProviderId, activeModelId)
|
|
2375
|
+
: false;
|
|
2376
|
+
|
|
2377
|
+
if (visionConfig.visionEnabled && modelHasVision) {
|
|
2378
|
+
contentParts = await multimodalService.processImage(imageInput, visionConfig.visionModelId || undefined);
|
|
2379
|
+
log.info(`🖼️ Image sent as vision ContentParts (model supports vision)`);
|
|
2380
|
+
} else {
|
|
2381
|
+
const ocrProvider = visionConfig.ocrProvider || (["openai", "gemini", "anthropic"].includes(dbProvider) ? dbProvider : "openai");
|
|
2382
|
+
log.info(`🖼️ Model lacks vision or vision disabled, using OCR via ${ocrProvider}...`);
|
|
2383
|
+
const ocrText = await multimodalService.ocrImage(imageInput, ocrProvider);
|
|
2384
|
+
finalMessageContent = ocrText
|
|
2385
|
+
? `[Imagen adjunta — contenido extraído por OCR]\n${ocrText}\n\n${finalMessageContent || ""}`
|
|
2386
|
+
: finalMessageContent || "";
|
|
2387
|
+
log.info(`🖼️ OCR result: ${ocrText.substring(0, 100)}...`);
|
|
2388
|
+
}
|
|
2389
|
+
} catch (imgError) {
|
|
2390
|
+
log.error(`❌ Image processing failed: ${(imgError as Error).message}`);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
if (msg.document) {
|
|
2395
|
+
try {
|
|
2396
|
+
const ocrProvider = visionConfig.ocrProvider || (["openai", "gemini", "anthropic"].includes(dbProvider) ? dbProvider : "openai");
|
|
2397
|
+
log.info(`📄 Document detected from WebChat, extracting text via OCR (${ocrProvider})...`);
|
|
2398
|
+
const docImage = {
|
|
2399
|
+
type: "base64" as const,
|
|
2400
|
+
data: msg.document.base64,
|
|
2401
|
+
mimeType: msg.document.mimeType || "application/pdf",
|
|
2402
|
+
caption: (msg.document as any).fileName || (msg.document as any).caption
|
|
2403
|
+
};
|
|
2404
|
+
const ocrText = await multimodalService.ocrImage(docImage, ocrProvider);
|
|
2405
|
+
finalMessageContent = ocrText
|
|
2406
|
+
? `[Documento adjunto]\n${ocrText}\n\n${finalMessageContent || ""}`
|
|
2407
|
+
: finalMessageContent || "";
|
|
2408
|
+
log.info(`📄 Document OCR result: ${ocrText.substring(0, 100)}...`);
|
|
2409
|
+
} catch (docError) {
|
|
2410
|
+
log.error(`❌ Document processing failed: ${(docError as Error).message}`);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
const messages: any[] = contentParts
|
|
2416
|
+
? [{ role: "user" as const, content: contentParts }]
|
|
2417
|
+
: [{ role: "user" as const, content: finalMessageContent }];
|
|
2418
|
+
|
|
2419
|
+
log.info(`Generating response for session ${unifiedSessionId} (multimodal: ${!!(msg.image || msg.document)})...`);
|
|
2420
|
+
|
|
2421
|
+
const { userId } = resolveContext({
|
|
2422
|
+
channel: "webchat",
|
|
2423
|
+
channelUserId: msg.sessionId,
|
|
2424
|
+
});
|
|
2425
|
+
|
|
2426
|
+
// Streaming: send tokens as they arrive
|
|
2427
|
+
let streamedContent = "";
|
|
2428
|
+
let messageId = crypto.randomUUID();
|
|
2429
|
+
|
|
2430
|
+
const response = await runner.generate({
|
|
2431
|
+
provider: dbProvider as any,
|
|
2432
|
+
messages,
|
|
2433
|
+
maxTokens: 4096,
|
|
2434
|
+
tools: prepareTools(agent, unifiedSessionId),
|
|
2435
|
+
maxSteps: 15,
|
|
2436
|
+
threadId: unifiedSessionId,
|
|
2437
|
+
userId,
|
|
2438
|
+
signal,
|
|
2439
|
+
onToken: async (token: string) => {
|
|
2440
|
+
if (signal.aborted) return;
|
|
2441
|
+
streamedContent += token;
|
|
2442
|
+
// Send chunk to client
|
|
2443
|
+
ws.send(JSON.stringify({
|
|
2444
|
+
type: "message",
|
|
2445
|
+
id: messageId,
|
|
2446
|
+
sessionId: unifiedSessionId,
|
|
2447
|
+
content: token,
|
|
2448
|
+
isChunk: true,
|
|
2449
|
+
isStep: false,
|
|
2450
|
+
} as OutboundMessage));
|
|
2451
|
+
},
|
|
2452
|
+
onStep: async (step) => {
|
|
2453
|
+
if (signal.aborted) return;
|
|
2454
|
+
|
|
2455
|
+
// "text" = el agente narra lo que esta pensando/haciendo
|
|
2456
|
+
if (step.type === "text" && step.message) {
|
|
2457
|
+
const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
|
|
2458
|
+
if (trimmedMessage) {
|
|
2459
|
+
ws.send(JSON.stringify({
|
|
2460
|
+
type: "progress",
|
|
2461
|
+
sessionId: unifiedSessionId,
|
|
2462
|
+
content: trimmedMessage,
|
|
2463
|
+
} as OutboundMessage));
|
|
2464
|
+
}
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
|
|
2469
|
+
if (step.type === "tool_call" && step.toolName) {
|
|
2470
|
+
const narration = getNarration(step.toolName);
|
|
2471
|
+
ws.send(JSON.stringify({
|
|
2472
|
+
type: "progress",
|
|
2473
|
+
sessionId: unifiedSessionId,
|
|
2474
|
+
content: narration,
|
|
2475
|
+
} as OutboundMessage));
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// "tool_result" = resultado de herramienta → solo si pide enviarse al usuario
|
|
2480
|
+
if (step.type === "tool_result" && step.message) {
|
|
2481
|
+
try {
|
|
2482
|
+
const result = JSON.parse(step.message);
|
|
2483
|
+
if (result._sendToUser || result.status) {
|
|
2484
|
+
const userMessage = result.message || result.status || "";
|
|
2485
|
+
if (userMessage) {
|
|
2486
|
+
ws.send(JSON.stringify({
|
|
2487
|
+
type: "progress",
|
|
2488
|
+
sessionId: unifiedSessionId,
|
|
2489
|
+
content: userMessage,
|
|
2490
|
+
} as OutboundMessage));
|
|
2491
|
+
}
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
} catch { }
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
// Use streamed content from onToken, fallback to response.content
|
|
2500
|
+
const content = streamedContent || response.content?.trim() || "";
|
|
2501
|
+
log.info(`Response sent to session ${unifiedSessionId} (${content.length} chars)`);
|
|
2502
|
+
|
|
2503
|
+
const voiceConfig = voiceService.getChannelVoiceConfig("webchat");
|
|
2504
|
+
const shouldSpeak = webchatPreferAudio;
|
|
2505
|
+
let responseType: "text" | "audio" = "text";
|
|
2506
|
+
let ttsProviderUsed: string | null = null;
|
|
2507
|
+
let ttsMimeType: string | null = null;
|
|
2508
|
+
|
|
2509
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
|
|
2510
|
+
|
|
2511
|
+
// Don't send text message if already streamed (content came via onToken)
|
|
2512
|
+
const alreadyStreamed = streamedContent.length > 0;
|
|
2513
|
+
|
|
2514
|
+
if (content && !alreadyStreamed) {
|
|
2515
|
+
if (shouldSpeak) {
|
|
2516
|
+
if (!voiceConfig.ttsProvider) {
|
|
2517
|
+
ws.send(JSON.stringify({
|
|
2518
|
+
type: "message",
|
|
2519
|
+
sessionId: unifiedSessionId,
|
|
2520
|
+
content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
|
|
2521
|
+
isStep: false
|
|
2522
|
+
} as OutboundMessage));
|
|
2523
|
+
} else {
|
|
2524
|
+
try {
|
|
2525
|
+
log.info(`🔊 TTS enabled, synthesizing audio for WebChat...`);
|
|
2526
|
+
const audioOutput = await voiceService.speak(content, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
|
|
2527
|
+
ttsProviderUsed = voiceConfig.ttsProvider;
|
|
2528
|
+
ttsMimeType = audioOutput.mimeType;
|
|
2529
|
+
responseType = "audio";
|
|
2530
|
+
const base64Audio = (audioOutput.data as Buffer).toString("base64");
|
|
2531
|
+
ws.send(JSON.stringify({
|
|
2532
|
+
type: "message",
|
|
2533
|
+
sessionId: unifiedSessionId,
|
|
2534
|
+
content,
|
|
2535
|
+
audio: base64Audio,
|
|
2536
|
+
mimeType: audioOutput.mimeType,
|
|
2537
|
+
isStep: false
|
|
2538
|
+
} as OutboundMessage));
|
|
2539
|
+
} catch (ttsError) {
|
|
2540
|
+
log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
|
|
2541
|
+
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
} else {
|
|
2545
|
+
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
2546
|
+
}
|
|
2547
|
+
} else if (alreadyStreamed && shouldSpeak && voiceConfig.ttsProvider) {
|
|
2548
|
+
try {
|
|
2549
|
+
log.info(`🔊 TTS enabled, synthesizing audio after streaming...`);
|
|
2550
|
+
const audioOutput = await voiceService.speak(content, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
|
|
2551
|
+
const base64Audio = (audioOutput.data as Buffer).toString("base64");
|
|
2552
|
+
log.info(`Audio generated after streaming: ${base64Audio.length} bytes`);
|
|
2553
|
+
ws.send(JSON.stringify({
|
|
2554
|
+
type: "message",
|
|
2555
|
+
sessionId: unifiedSessionId,
|
|
2556
|
+
content,
|
|
2557
|
+
audio: base64Audio,
|
|
2558
|
+
mimeType: audioOutput.mimeType,
|
|
2559
|
+
isStep: false
|
|
2560
|
+
} as OutboundMessage));
|
|
2561
|
+
} catch (ttsError) {
|
|
2562
|
+
log.error(`TTS after streaming failed: ${(ttsError as Error).message}), skipping audio`);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
const unifiedSessionId = msg.sessionId;
|
|
2567
|
+
// Detener typing aunque falle — nunca dejar el spinner infinito
|
|
2568
|
+
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
|
|
2569
|
+
ws.send(JSON.stringify({
|
|
2570
|
+
type: "error",
|
|
2571
|
+
sessionId: unifiedSessionId,
|
|
2572
|
+
error: (error as Error).message,
|
|
2573
|
+
} as OutboundMessage));
|
|
2574
|
+
log.error(`Error for session ${unifiedSessionId}: ${(error as Error).message}`);
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
ws.send(JSON.stringify({
|
|
2582
|
+
type: "error",
|
|
2583
|
+
sessionId: msg.sessionId,
|
|
2584
|
+
error: "Unknown message type",
|
|
2585
|
+
} as OutboundMessage));
|
|
2586
|
+
},
|
|
2587
|
+
|
|
2588
|
+
close(ws) {
|
|
2589
|
+
const data = ws.data;
|
|
2590
|
+
if (data.sessionId.startsWith("meeting:")) {
|
|
2591
|
+
log.info(`Meeting stream client disconnected: ${data.sessionId}`);
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
log.debug(`WebSocket disconnected: ${data.sessionId}`);
|
|
2596
|
+
logSubscribers.delete(data.sessionId);
|
|
2597
|
+
sessionManager.delete(data.sessionId);
|
|
2598
|
+
laneQueue.cancel(data.sessionId);
|
|
2599
|
+
unsubscribeCanvas(ws);
|
|
2600
|
+
canvasManager.unregisterSession(`canvas:${data.sessionId}`);
|
|
2601
|
+
|
|
2602
|
+
const channel = channelManager?.getChannel("webchat") as any;
|
|
2603
|
+
if (channel?.unregisterConnection) channel.unregisterConnection(data.sessionId);
|
|
2604
|
+
},
|
|
2605
|
+
},
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
onLogEntry((entry) => {
|
|
2609
|
+
if (logSubscribers.size === 0) return;
|
|
2610
|
+
|
|
2611
|
+
const payload = JSON.stringify({
|
|
2612
|
+
type: "log",
|
|
2613
|
+
sessionId: entry.meta?.sessionId || "system",
|
|
2614
|
+
logEntry: entry,
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
for (const sessionId of logSubscribers) {
|
|
2618
|
+
const session = sessionManager.get(sessionId);
|
|
2619
|
+
if (session?.ws && session.ws.readyState === 1) {
|
|
2620
|
+
try {
|
|
2621
|
+
session.ws.send(payload);
|
|
2622
|
+
} catch {
|
|
2623
|
+
logSubscribers.delete(sessionId);
|
|
2624
|
+
}
|
|
2625
|
+
} else {
|
|
2626
|
+
logSubscribers.delete(sessionId);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
log.info(`Gateway started successfully`);
|
|
2632
|
+
|
|
2633
|
+
// Check if running as child process in dev mode (parent handles browser open)
|
|
2634
|
+
const isGatewayChild = process.env.HIVE_GATEWAY_CHILD === "1";
|
|
2635
|
+
|
|
2636
|
+
// Print URLs based on mode
|
|
2637
|
+
if (isDev) {
|
|
2638
|
+
// In development: Gateway serves UI on port 18790 (same as production), Vite provides HMR on 5173
|
|
2639
|
+
const devUrl = gatewaySetupMode ? `http://localhost:${port}/setup` : `http://localhost:${port}`;
|
|
2640
|
+
log.info(`[gateway] UI: ${devUrl}`);
|
|
2641
|
+
log.info(`[gateway] API: http://${host}:${port}`);
|
|
2642
|
+
log.info(`[gateway] WebSocket: ws://${host}:${port}/ws`);
|
|
2643
|
+
log.info(`[gateway] Canvas: ws://${host}:${port}/canvas`);
|
|
2644
|
+
log.info(`[gateway] Modo: desarrollo`);
|
|
2645
|
+
if (!isGatewayChild) {
|
|
2646
|
+
log.info(gatewaySetupMode ? `🎉 Primer arranque — abriendo setup...` : `🐝 Administra tu Hive aquí: ${devUrl}`);
|
|
2647
|
+
}
|
|
2648
|
+
} else {
|
|
2649
|
+
// In production: Gateway serves UI from dist/
|
|
2650
|
+
const isSetupMode = gatewaySetupMode;
|
|
2651
|
+
const baseUrl = process.env.HIVE_PUBLIC_URL?.replace(/\/$/, "") ?? `http://${host}:${port}`;
|
|
2652
|
+
const uiUrl = isSetupMode ? `${baseUrl}/setup` : `${baseUrl}/ui`;
|
|
2653
|
+
|
|
2654
|
+
log.info(`[gateway] UI: ${uiUrl}`);
|
|
2655
|
+
log.info(`[gateway] API: http://${host}:${port}`);
|
|
2656
|
+
log.info(`[gateway] WebSocket: ws://${host}:${port}/ws`);
|
|
2657
|
+
log.info(`[gateway] Canvas: ws://${host}:${port}/canvas`);
|
|
2658
|
+
|
|
2659
|
+
// Always open browser on startup (setup and normal mode).
|
|
2660
|
+
// Set NO_BROWSER=1 to skip in headless/server environments (e.g. CLI parent manages the browser).
|
|
2661
|
+
if (!process.env.NO_BROWSER) {
|
|
2662
|
+
log.info(isSetupMode ? `🎉 Primer arranque — abriendo wizard de configuración...` : `🐝 Administra tu Hive aquí: ${uiUrl}`);
|
|
2663
|
+
try {
|
|
2664
|
+
const platform = process.platform;
|
|
2665
|
+
let shellCmd: string;
|
|
2666
|
+
if (platform === "win32") {
|
|
2667
|
+
shellCmd = `start "" "${uiUrl}"`;
|
|
2668
|
+
} else if (platform === "darwin") {
|
|
2669
|
+
shellCmd = `open "${uiUrl}"`;
|
|
2670
|
+
} else {
|
|
2671
|
+
// Linux: gio open first (GNOME/Wayland native), then xdg-open fallbacks
|
|
2672
|
+
shellCmd = `gio open "${uiUrl}" 2>/dev/null || xdg-open "${uiUrl}" 2>/dev/null || sensible-browser "${uiUrl}" 2>/dev/null || x-www-browser "${uiUrl}" 2>/dev/null || true`;
|
|
2673
|
+
}
|
|
2674
|
+
const shell = platform === "win32" ? "cmd" : "/bin/sh";
|
|
2675
|
+
const shellArg = platform === "win32" ? "/c" : "-c";
|
|
2676
|
+
// Use Bun.spawn (native Bun API) for reliable detached subprocess
|
|
2677
|
+
const proc = Bun.spawn([shell, shellArg, shellCmd], {
|
|
2678
|
+
stdout: "ignore",
|
|
2679
|
+
stderr: "ignore",
|
|
2680
|
+
stdin: "ignore",
|
|
2681
|
+
});
|
|
2682
|
+
proc.unref();
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
log.warn(`Could not open browser: ${(err as Error).message}`);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
if (!gatewaySetupMode) log.info(`Channels: ${channelManager.listChannels().map((c) => c.name).join(", ") || "none"}`);
|
|
2689
|
+
|
|
2690
|
+
// FIX 7 — SIGTERM: graceful shutdown with full cleanup
|
|
2691
|
+
process.on("SIGTERM", async () => {
|
|
2692
|
+
log.info("Received SIGTERM, shutting down gracefully...");
|
|
2693
|
+
watchers.forEach((close) => close());
|
|
2694
|
+
|
|
2695
|
+
const mcp = agent?.getMCPManager();
|
|
2696
|
+
if (mcp) {
|
|
2697
|
+
log.info("Disconnecting MCP servers...");
|
|
2698
|
+
await mcp.disconnectAll().catch(() => { });
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
if (channelManager) {
|
|
2702
|
+
log.info("Stopping channels...");
|
|
2703
|
+
await channelManager.stopAll();
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// BrowserService — kill any running browser processes
|
|
2707
|
+
try {
|
|
2708
|
+
const mod = await import("../tools/web/browser-service");
|
|
2709
|
+
mod.CDPClient.closeAll();
|
|
2710
|
+
log.info("Browser processes cleaned up");
|
|
2711
|
+
} catch { }
|
|
2712
|
+
|
|
2713
|
+
// CanvasManager — stop heartbeat intervals
|
|
2714
|
+
try {
|
|
2715
|
+
canvasManager.clearAll();
|
|
2716
|
+
log.info("Canvas sessions cleaned up");
|
|
2717
|
+
} catch { }
|
|
2718
|
+
|
|
2719
|
+
// MCP hot-reload — stop polling interval
|
|
2720
|
+
try {
|
|
2721
|
+
const { stopMCPHotReload } = await import("../mcp/hot-reload");
|
|
2722
|
+
stopMCPHotReload();
|
|
2723
|
+
log.info("MCP hot-reload stopped");
|
|
2724
|
+
} catch { }
|
|
2725
|
+
|
|
2726
|
+
server.stop();
|
|
2727
|
+
|
|
2728
|
+
try { unlinkSync(pidFile); } catch { }
|
|
2729
|
+
log.info("Gateway shutdown complete");
|
|
2730
|
+
process.exit(0);
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
process.on("SIGHUP", async () => {
|
|
2734
|
+
log.info("Received SIGHUP, reloading configuration...");
|
|
2735
|
+
try {
|
|
2736
|
+
const newConfig = await loadConfig();
|
|
2737
|
+
await agent.updateConfig(newConfig);
|
|
2738
|
+
await agent.reload();
|
|
2739
|
+
log.info("Configuration reloaded successfully");
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
log.error(`Failed to reload configuration: ${(error as Error).message}`);
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
}
|