@swarmclawai/swarmclaw 0.8.4 → 0.8.7
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 +9 -9
- package/bin/swarmclaw.js +5 -1
- package/bin/worker-cmd.js +73 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +17 -7
- package/src/app/api/agents/route.ts +21 -8
- package/src/app/api/approvals/route.test.ts +6 -6
- package/src/app/api/approvals/route.ts +2 -1
- package/src/app/api/auth/route.ts +2 -3
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
- package/src/app/api/chatrooms/[id]/route.ts +7 -6
- package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/route.ts +9 -9
- package/src/app/api/chats/[id]/devserver/route.ts +2 -1
- package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
- package/src/app/api/chats/[id]/fork/route.ts +3 -5
- package/src/app/api/chats/[id]/restore/route.ts +6 -7
- package/src/app/api/chats/[id]/retry/route.ts +3 -4
- package/src/app/api/chats/[id]/route.ts +61 -62
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/connectors/[id]/route.ts +7 -8
- package/src/app/api/connectors/route.ts +5 -4
- package/src/app/api/eval/run/route.ts +2 -1
- package/src/app/api/eval/suite/route.ts +2 -1
- package/src/app/api/external-agents/route.test.ts +1 -1
- package/src/app/api/external-agents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +1 -1
- package/src/app/api/gateways/[id]/route.ts +7 -5
- package/src/app/api/gateways/route.ts +1 -1
- package/src/app/api/knowledge/upload/route.ts +1 -1
- package/src/app/api/logs/route.ts +5 -7
- package/src/app/api/memory-images/[filename]/route.ts +2 -3
- package/src/app/api/openclaw/agent-files/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -4
- package/src/app/api/openclaw/config-sync/route.ts +3 -2
- package/src/app/api/openclaw/cron/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
- package/src/app/api/openclaw/exec-config/route.ts +3 -2
- package/src/app/api/openclaw/gateway/route.ts +5 -4
- package/src/app/api/openclaw/history/route.ts +3 -2
- package/src/app/api/openclaw/media/route.ts +2 -1
- package/src/app/api/openclaw/permissions/route.ts +3 -2
- package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
- package/src/app/api/openclaw/skills/install/route.ts +2 -1
- package/src/app/api/openclaw/skills/remove/route.ts +2 -1
- package/src/app/api/openclaw/skills/route.ts +3 -2
- package/src/app/api/orchestrator/run/route.ts +5 -14
- package/src/app/api/perf/route.ts +43 -0
- package/src/app/api/plugins/dependencies/route.ts +2 -1
- package/src/app/api/plugins/install/route.ts +2 -1
- package/src/app/api/plugins/marketplace/route.ts +3 -2
- package/src/app/api/plugins/settings/route.ts +2 -1
- package/src/app/api/preview-server/route.ts +11 -10
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/schedules/[id]/route.test.ts +128 -0
- package/src/app/api/schedules/[id]/route.ts +43 -43
- package/src/app/api/schedules/[id]/run/route.ts +11 -62
- package/src/app/api/schedules/route.ts +21 -87
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/doctor/route.ts +9 -8
- package/src/app/api/tasks/[id]/approve/route.ts +33 -30
- package/src/app/api/tasks/[id]/route.ts +12 -35
- package/src/app/api/tasks/import/github/route.ts +2 -1
- package/src/app/api/tasks/route.ts +79 -91
- package/src/app/api/wallets/[id]/approve/route.ts +2 -1
- package/src/app/api/wallets/[id]/route.ts +13 -19
- package/src/app/api/wallets/[id]/send/route.ts +2 -1
- package/src/app/api/wallets/route.ts +2 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.test.ts +3 -1
- package/src/app/page.tsx +23 -331
- package/src/cli/index.js +19 -0
- package/src/cli/index.ts +38 -7
- package/src/cli/spec.js +9 -0
- package/src/components/activity/activity-feed.tsx +7 -4
- package/src/components/agents/agent-card.tsx +32 -6
- package/src/components/agents/agent-chat-list.tsx +55 -22
- package/src/components/agents/agent-files-editor.tsx +3 -2
- package/src/components/agents/agent-sheet.tsx +123 -22
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/openclaw-skills-panel.tsx +2 -1
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +8 -2
- package/src/components/auth/setup-wizard.tsx +10 -9
- package/src/components/auth/user-picker.tsx +3 -2
- package/src/components/chat/chat-area.tsx +20 -1
- package/src/components/chat/chat-card.tsx +18 -3
- package/src/components/chat/chat-header.tsx +24 -4
- package/src/components/chat/chat-list.tsx +2 -11
- package/src/components/chat/heartbeat-history-panel.tsx +2 -1
- package/src/components/chat/message-bubble.tsx +45 -6
- package/src/components/chat/message-list.tsx +280 -145
- package/src/components/chat/streaming-bubble.tsx +217 -60
- package/src/components/chat/swarm-panel.test.ts +274 -0
- package/src/components/chat/swarm-panel.tsx +410 -0
- package/src/components/chat/swarm-status-card.tsx +346 -0
- package/src/components/chat/tool-call-bubble.tsx +48 -23
- package/src/components/chatrooms/chatroom-list.tsx +8 -5
- package/src/components/chatrooms/chatroom-message.tsx +10 -7
- package/src/components/chatrooms/chatroom-view.tsx +12 -9
- package/src/components/connectors/connector-health.tsx +6 -4
- package/src/components/connectors/connector-list.tsx +16 -11
- package/src/components/connectors/connector-sheet.tsx +12 -6
- package/src/components/home/home-view.tsx +38 -24
- package/src/components/input/chat-input.tsx +10 -1
- package/src/components/layout/app-layout.tsx +2 -38
- package/src/components/layout/sheet-layer.tsx +50 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
- package/src/components/plugins/plugin-list.tsx +8 -4
- package/src/components/plugins/plugin-sheet.tsx +2 -1
- package/src/components/providers/provider-list.tsx +3 -2
- package/src/components/providers/provider-sheet.tsx +2 -1
- package/src/components/runs/run-list.tsx +11 -7
- package/src/components/schedules/schedule-card.tsx +5 -3
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/attachment-chip.tsx +19 -3
- package/src/components/shared/notification-center.tsx +6 -3
- package/src/components/shared/settings/plugin-manager.tsx +3 -2
- package/src/components/shared/settings/section-embedding.tsx +2 -1
- package/src/components/shared/settings/section-orchestrator.tsx +2 -1
- package/src/components/shared/settings/section-user-preferences.tsx +107 -0
- package/src/components/shared/settings/settings-page.tsx +13 -9
- package/src/components/skills/clawhub-browser.tsx +15 -4
- package/src/components/skills/skill-list.tsx +15 -4
- package/src/components/tasks/approvals-panel.tsx +2 -1
- package/src/components/tasks/task-board.tsx +35 -37
- package/src/components/tasks/task-sheet.tsx +4 -3
- package/src/components/ui/full-screen-loader.tsx +164 -0
- package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
- package/src/components/wallets/wallet-panel.tsx +6 -5
- package/src/components/wallets/wallet-section.tsx +3 -2
- package/src/components/webhooks/webhook-list.tsx +4 -5
- package/src/components/webhooks/webhook-sheet.tsx +6 -6
- package/src/hooks/use-app-bootstrap.ts +202 -0
- package/src/hooks/use-mounted-ref.ts +14 -0
- package/src/hooks/use-now.ts +31 -0
- package/src/hooks/use-openclaw-gateway.ts +2 -1
- package/src/instrumentation.ts +20 -8
- package/src/lib/agent-default-tools.test.ts +52 -0
- package/src/lib/agent-default-tools.ts +40 -0
- package/src/lib/api-client.test.ts +21 -0
- package/src/lib/api-client.ts +6 -11
- package/src/lib/canvas-content.test.ts +360 -0
- package/src/lib/chat-streaming-state.test.ts +49 -2
- package/src/lib/chat-streaming-state.ts +26 -10
- package/src/lib/fetch-timeout.test.ts +54 -0
- package/src/lib/fetch-timeout.ts +60 -3
- package/src/lib/live-tool-events.test.ts +77 -0
- package/src/lib/live-tool-events.ts +73 -0
- package/src/lib/local-observability.test.ts +2 -2
- package/src/lib/openclaw-endpoint.test.ts +1 -1
- package/src/lib/providers/anthropic.ts +12 -16
- package/src/lib/providers/index.ts +4 -2
- package/src/lib/providers/ollama.ts +9 -6
- package/src/lib/providers/openai.ts +11 -14
- package/src/lib/runtime-env.test.ts +8 -8
- package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
- package/src/lib/schedule-dedupe.test.ts +1 -1
- package/src/lib/schedule-dedupe.ts +3 -2
- package/src/lib/server/agent-thread-session.test.ts +6 -6
- package/src/lib/server/agent-thread-session.ts +6 -9
- package/src/lib/server/alert-dispatch.ts +2 -1
- package/src/lib/server/api-routes.test.ts +6 -6
- package/src/lib/server/approval-connector-notify.test.ts +4 -4
- package/src/lib/server/approvals-auto-approve.test.ts +29 -29
- package/src/lib/server/approvals.test.ts +317 -0
- package/src/lib/server/approvals.ts +5 -4
- package/src/lib/server/autonomy-runtime.test.ts +11 -11
- package/src/lib/server/browser-state.ts +2 -2
- package/src/lib/server/capability-router.test.ts +1 -1
- package/src/lib/server/capability-router.ts +3 -2
- package/src/lib/server/chat-execution-advanced.test.ts +15 -2
- package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
- package/src/lib/server/chat-execution-disabled.test.ts +3 -3
- package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
- package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
- package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
- package/src/lib/server/chat-execution-tool-events.ts +116 -0
- package/src/lib/server/chat-execution-utils.test.ts +479 -0
- package/src/lib/server/chat-execution-utils.ts +533 -0
- package/src/lib/server/chat-execution.ts +153 -748
- package/src/lib/server/chat-streaming-utils.ts +174 -0
- package/src/lib/server/chat-turn-tool-routing.ts +310 -0
- package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
- package/src/lib/server/clawhub-client.ts +2 -1
- package/src/lib/server/collection-helpers.test.ts +92 -0
- package/src/lib/server/collection-helpers.ts +25 -3
- package/src/lib/server/connectors/access.ts +146 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
- package/src/lib/server/connectors/bluebubbles.ts +4 -4
- package/src/lib/server/connectors/commands.ts +367 -0
- package/src/lib/server/connectors/connector-routing.test.ts +4 -4
- package/src/lib/server/connectors/delivery.ts +142 -0
- package/src/lib/server/connectors/discord.ts +37 -40
- package/src/lib/server/connectors/email.ts +11 -10
- package/src/lib/server/connectors/googlechat.ts +4 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
- package/src/lib/server/connectors/ingress-delivery.ts +23 -0
- package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
- package/src/lib/server/connectors/manager.test.ts +352 -77
- package/src/lib/server/connectors/manager.ts +134 -673
- package/src/lib/server/connectors/matrix.ts +4 -4
- package/src/lib/server/connectors/message-sentinel.ts +7 -0
- package/src/lib/server/connectors/openclaw.test.ts +1 -1
- package/src/lib/server/connectors/openclaw.ts +8 -10
- package/src/lib/server/connectors/outbox.test.ts +192 -0
- package/src/lib/server/connectors/outbox.ts +369 -0
- package/src/lib/server/connectors/pairing.test.ts +18 -1
- package/src/lib/server/connectors/pairing.ts +49 -4
- package/src/lib/server/connectors/policy.ts +9 -3
- package/src/lib/server/connectors/reconnect-state.ts +71 -0
- package/src/lib/server/connectors/response-media.ts +256 -0
- package/src/lib/server/connectors/runtime-state.ts +67 -0
- package/src/lib/server/connectors/session.test.ts +357 -0
- package/src/lib/server/connectors/session.ts +422 -0
- package/src/lib/server/connectors/signal.ts +7 -7
- package/src/lib/server/connectors/slack.ts +43 -43
- package/src/lib/server/connectors/teams.ts +4 -4
- package/src/lib/server/connectors/telegram.ts +37 -43
- package/src/lib/server/connectors/types.ts +31 -1
- package/src/lib/server/connectors/whatsapp.test.ts +108 -0
- package/src/lib/server/connectors/whatsapp.ts +106 -34
- package/src/lib/server/context-manager.test.ts +409 -0
- package/src/lib/server/cost.test.ts +1 -1
- package/src/lib/server/daemon-policy.ts +78 -0
- package/src/lib/server/daemon-state-connectors.test.ts +167 -0
- package/src/lib/server/daemon-state.test.ts +283 -55
- package/src/lib/server/daemon-state.ts +106 -109
- package/src/lib/server/data-dir.test.ts +5 -5
- package/src/lib/server/data-dir.ts +4 -0
- package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
- package/src/lib/server/delegation-jobs.test.ts +87 -0
- package/src/lib/server/delegation-jobs.ts +42 -48
- package/src/lib/server/devserver-launch.ts +1 -1
- package/src/lib/server/document-utils.ts +7 -9
- package/src/lib/server/elevenlabs.ts +2 -1
- package/src/lib/server/embeddings.test.ts +105 -0
- package/src/lib/server/ethereum.ts +3 -2
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -1
- package/src/lib/server/eval/scorer.ts +2 -1
- package/src/lib/server/evm-swap.ts +2 -1
- package/src/lib/server/gateway/protocol.test.ts +1 -1
- package/src/lib/server/guardian.ts +2 -1
- package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
- package/src/lib/server/heartbeat-service.test.ts +406 -0
- package/src/lib/server/heartbeat-service.ts +54 -7
- package/src/lib/server/heartbeat-wake.test.ts +19 -0
- package/src/lib/server/heartbeat-wake.ts +17 -16
- package/src/lib/server/integrity-monitor.test.ts +149 -0
- package/src/lib/server/json-utils.ts +22 -0
- package/src/lib/server/knowledge-db.test.ts +13 -13
- package/src/lib/server/link-understanding.ts +2 -1
- package/src/lib/server/llm-response-cache.test.ts +1 -1
- package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
- package/src/lib/server/main-agent-loop.test.ts +6 -6
- package/src/lib/server/main-agent-loop.ts +21 -7
- package/src/lib/server/mcp-client.test.ts +1 -1
- package/src/lib/server/mcp-conformance.test.ts +1 -1
- package/src/lib/server/mcp-conformance.ts +3 -2
- package/src/lib/server/memory-consolidation.ts +2 -1
- package/src/lib/server/memory-db.test.ts +485 -0
- package/src/lib/server/memory-db.ts +39 -26
- package/src/lib/server/memory-graph.test.ts +2 -2
- package/src/lib/server/memory-policy.test.ts +7 -7
- package/src/lib/server/memory-retrieval.test.ts +1 -1
- package/src/lib/server/openclaw-config-sync.ts +2 -1
- package/src/lib/server/openclaw-deploy.test.ts +1 -1
- package/src/lib/server/openclaw-deploy.ts +8 -12
- package/src/lib/server/openclaw-exec-config.ts +2 -1
- package/src/lib/server/openclaw-gateway.ts +6 -7
- package/src/lib/server/openclaw-skills-normalize.ts +2 -1
- package/src/lib/server/openclaw-sync.ts +7 -5
- package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
- package/src/lib/server/orchestrator-lg.ts +199 -327
- package/src/lib/server/path-utils.ts +31 -0
- package/src/lib/server/perf.ts +161 -0
- package/src/lib/server/plugins-approval-guidance.ts +115 -0
- package/src/lib/server/plugins.test.ts +1 -1
- package/src/lib/server/plugins.ts +22 -132
- package/src/lib/server/process-manager.ts +5 -8
- package/src/lib/server/provider-health.test.ts +137 -0
- package/src/lib/server/provider-health.ts +3 -3
- package/src/lib/server/provider-model-discovery.ts +3 -12
- package/src/lib/server/queue-followups.test.ts +9 -9
- package/src/lib/server/queue-reconcile.test.ts +2 -2
- package/src/lib/server/queue-recovery.test.ts +269 -0
- package/src/lib/server/queue.test.ts +570 -0
- package/src/lib/server/queue.ts +62 -455
- package/src/lib/server/resolve-image.ts +30 -0
- package/src/lib/server/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
- package/src/lib/server/schedule-normalization.test.ts +279 -0
- package/src/lib/server/schedule-service.ts +263 -0
- package/src/lib/server/scheduler.ts +17 -74
- package/src/lib/server/session-mailbox.test.ts +191 -0
- package/src/lib/server/session-run-manager.test.ts +640 -0
- package/src/lib/server/session-run-manager.ts +59 -15
- package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
- package/src/lib/server/session-tools/calendar.ts +2 -1
- package/src/lib/server/session-tools/canvas.ts +2 -1
- package/src/lib/server/session-tools/chatroom.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +26 -28
- package/src/lib/server/session-tools/context-mgmt.ts +3 -2
- package/src/lib/server/session-tools/crawl.ts +4 -3
- package/src/lib/server/session-tools/crud.ts +105 -324
- package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
- package/src/lib/server/session-tools/delegate.ts +6 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
- package/src/lib/server/session-tools/discovery.ts +4 -3
- package/src/lib/server/session-tools/document.ts +2 -1
- package/src/lib/server/session-tools/email.ts +2 -1
- package/src/lib/server/session-tools/extract.ts +2 -1
- package/src/lib/server/session-tools/file.ts +4 -3
- package/src/lib/server/session-tools/http.ts +2 -1
- package/src/lib/server/session-tools/human-loop.ts +2 -1
- package/src/lib/server/session-tools/image-gen.ts +4 -3
- package/src/lib/server/session-tools/index.ts +26 -30
- package/src/lib/server/session-tools/mailbox.ts +2 -1
- package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
- package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
- package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
- package/src/lib/server/session-tools/monitor.ts +2 -1
- package/src/lib/server/session-tools/platform.ts +2 -1
- package/src/lib/server/session-tools/plugin-creator.ts +2 -1
- package/src/lib/server/session-tools/replicate.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
- package/src/lib/server/session-tools/shell.ts +4 -9
- package/src/lib/server/session-tools/subagent.ts +322 -170
- package/src/lib/server/session-tools/table.ts +6 -5
- package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
- package/src/lib/server/session-tools/wallet.ts +7 -6
- package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
- package/src/lib/server/session-tools/web-utils.ts +317 -0
- package/src/lib/server/session-tools/web.ts +62 -328
- package/src/lib/server/skill-prompt-budget.test.ts +1 -1
- package/src/lib/server/skills-normalize.ts +2 -1
- package/src/lib/server/storage-item-access.test.ts +302 -0
- package/src/lib/server/storage.ts +366 -314
- package/src/lib/server/stream-agent-chat.test.ts +82 -3
- package/src/lib/server/stream-agent-chat.ts +146 -510
- package/src/lib/server/stream-continuation.ts +412 -0
- package/src/lib/server/subagent-lineage.test.ts +647 -0
- package/src/lib/server/subagent-lineage.ts +435 -0
- package/src/lib/server/subagent-runtime.test.ts +484 -0
- package/src/lib/server/subagent-runtime.ts +419 -0
- package/src/lib/server/subagent-swarm.test.ts +391 -0
- package/src/lib/server/subagent-swarm.ts +564 -0
- package/src/lib/server/system-events.ts +3 -3
- package/src/lib/server/task-followups.test.ts +491 -0
- package/src/lib/server/task-followups.ts +391 -0
- package/src/lib/server/task-lifecycle.test.ts +205 -0
- package/src/lib/server/task-lifecycle.ts +200 -0
- package/src/lib/server/task-quality-gate.test.ts +1 -1
- package/src/lib/server/task-resume.ts +208 -0
- package/src/lib/server/task-service.test.ts +108 -0
- package/src/lib/server/task-service.ts +264 -0
- package/src/lib/server/task-validation.test.ts +1 -1
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
- package/src/lib/server/tool-capability-policy.test.ts +2 -2
- package/src/lib/server/tool-capability-policy.ts +3 -2
- package/src/lib/server/tool-planning.ts +2 -1
- package/src/lib/server/tool-retry.ts +2 -3
- package/src/lib/server/wake-dispatcher.test.ts +303 -0
- package/src/lib/server/wake-dispatcher.ts +318 -0
- package/src/lib/server/wake-mode.test.ts +161 -0
- package/src/lib/server/wake-mode.ts +174 -0
- package/src/lib/server/wallet-service.ts +8 -9
- package/src/lib/server/watch-jobs.ts +2 -1
- package/src/lib/server/workspace-context.ts +2 -2
- package/src/lib/shared-utils.test.ts +142 -0
- package/src/lib/shared-utils.ts +62 -0
- package/src/lib/tool-event-summary.ts +2 -1
- package/src/lib/view-routes.test.ts +100 -0
- package/src/lib/wallet.test.ts +322 -6
- package/src/proxy.test.ts +4 -4
- package/src/proxy.ts +2 -3
- package/src/stores/set-if-changed.ts +40 -0
- package/src/stores/slices/agent-slice.ts +111 -0
- package/src/stores/slices/auth-slice.ts +25 -0
- package/src/stores/slices/data-slice.ts +301 -0
- package/src/stores/slices/index.ts +7 -0
- package/src/stores/slices/session-slice.ts +112 -0
- package/src/stores/slices/task-slice.ts +63 -0
- package/src/stores/slices/ui-slice.ts +192 -0
- package/src/stores/use-app-store.ts +17 -822
- package/src/stores/use-approval-store.ts +2 -1
- package/src/stores/use-chat-store.ts +8 -1
- package/src/types/index.ts +10 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import type { WhatsAppApprovedContact } from '@/types'
|
|
5
|
+
import { CONNECTORS_DATA_DIR } from '../data-dir'
|
|
6
|
+
import { safeJsonParseObject } from '../json-utils'
|
|
4
7
|
|
|
5
|
-
const DEFAULT_DATA_DIR = path.join(process.cwd(), 'data')
|
|
6
8
|
const STORE_VERSION = 1
|
|
7
9
|
const PENDING_TTL_MS = 24 * 60 * 60 * 1000
|
|
8
10
|
const MAX_PENDING_PER_CONNECTOR = 100
|
|
@@ -10,8 +12,7 @@ const PAIR_CODE_LENGTH = 8
|
|
|
10
12
|
const PAIR_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
|
11
13
|
|
|
12
14
|
function resolveStorePath(): string {
|
|
13
|
-
|
|
14
|
-
return path.join(dataDir, 'connectors', 'pairing-store.json')
|
|
15
|
+
return path.join(CONNECTORS_DATA_DIR, 'pairing-store.json')
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type PairingPolicy = 'open' | 'allowlist' | 'pairing' | 'disabled'
|
|
@@ -68,6 +69,24 @@ function dedupe(items: string[]): string[] {
|
|
|
68
69
|
return out
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function normalizeApprovedContactPhone(value: unknown): string {
|
|
73
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeApprovedContactLabel(value: unknown, fallback: string): string {
|
|
77
|
+
const trimmed = typeof value === 'string' ? value.trim() : ''
|
|
78
|
+
return trimmed || fallback
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function approvedContactKey(phone: string): string {
|
|
82
|
+
const normalized = normalizeSenderId(phone)
|
|
83
|
+
if (!normalized) return ''
|
|
84
|
+
const digits = normalized.replace(/[^\d]/g, '')
|
|
85
|
+
if (digits) return digits
|
|
86
|
+
const jidUser = normalized.split('@')[0]?.split(':')[0]?.trim()
|
|
87
|
+
return jidUser || normalized
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
function prunePending(entries: PairingRequest[]): PairingRequest[] {
|
|
72
91
|
const now = Date.now()
|
|
73
92
|
return entries.filter((entry) => {
|
|
@@ -86,7 +105,7 @@ function loadStore(): PairingStore {
|
|
|
86
105
|
try {
|
|
87
106
|
if (!fs.existsSync(storePath)) return emptyStore()
|
|
88
107
|
const raw = fs.readFileSync(storePath, 'utf8')
|
|
89
|
-
const parsed =
|
|
108
|
+
const parsed = safeJsonParseObject<PairingStore>(raw)
|
|
90
109
|
if (!parsed || typeof parsed !== 'object' || typeof parsed.connectors !== 'object') {
|
|
91
110
|
return emptyStore()
|
|
92
111
|
}
|
|
@@ -151,6 +170,32 @@ export function parseAllowFromCsv(value: unknown): string[] {
|
|
|
151
170
|
return dedupe(value.split(',').map((item) => item.trim()).filter(Boolean))
|
|
152
171
|
}
|
|
153
172
|
|
|
173
|
+
export function normalizeWhatsAppApprovedContacts(value: unknown): WhatsAppApprovedContact[] {
|
|
174
|
+
if (!Array.isArray(value)) return []
|
|
175
|
+
|
|
176
|
+
const seen = new Set<string>()
|
|
177
|
+
const out: WhatsAppApprovedContact[] = []
|
|
178
|
+
for (const entry of value) {
|
|
179
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue
|
|
180
|
+
const record = entry as Record<string, unknown>
|
|
181
|
+
const phone = normalizeApprovedContactPhone(record.phone)
|
|
182
|
+
if (!phone) continue
|
|
183
|
+
const dedupeKey = approvedContactKey(phone)
|
|
184
|
+
if (!dedupeKey || seen.has(dedupeKey)) continue
|
|
185
|
+
seen.add(dedupeKey)
|
|
186
|
+
out.push({
|
|
187
|
+
id: typeof record.id === 'string' && record.id.trim() ? record.id.trim() : `wa-contact-${out.length + 1}`,
|
|
188
|
+
label: normalizeApprovedContactLabel(record.label, phone),
|
|
189
|
+
phone,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getWhatsAppApprovedSenderIds(value: unknown): string[] {
|
|
196
|
+
return normalizeWhatsAppApprovedContacts(value).map((entry) => entry.phone)
|
|
197
|
+
}
|
|
198
|
+
|
|
154
199
|
export function listStoredAllowedSenders(connectorId: string): string[] {
|
|
155
200
|
const store = loadStore()
|
|
156
201
|
const state = ensureConnectorState(store, connectorId)
|
|
@@ -2,8 +2,8 @@ import type { Connector, Session, SessionResetMode, SessionResetType } from '@/t
|
|
|
2
2
|
import { getProvider } from '@/lib/providers'
|
|
3
3
|
import type { InboundMessage } from './types'
|
|
4
4
|
import { evaluateSessionFreshness, inferSessionResetType, resetSessionRuntime, resolveSessionResetPolicy } from '../session-reset-policy'
|
|
5
|
-
import { listStoredAllowedSenders, parseAllowFromCsv, parsePairingPolicy } from './pairing'
|
|
6
|
-
import { loadAgents, loadChatrooms, loadCredentials } from '../storage'
|
|
5
|
+
import { getWhatsAppApprovedSenderIds, listStoredAllowedSenders, parseAllowFromCsv, parsePairingPolicy } from './pairing'
|
|
6
|
+
import { loadAgents, loadChatrooms, loadCredentials, loadSettings } from '../storage'
|
|
7
7
|
|
|
8
8
|
export type ConnectorSessionScope = 'main' | 'channel' | 'peer' | 'channel-peer' | 'thread'
|
|
9
9
|
export type ConnectorReplyMode = 'off' | 'first' | 'all'
|
|
@@ -377,7 +377,13 @@ export function buildConnectorDoctorWarnings(params: {
|
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
const dmPolicy = parsePairingPolicy(connector.config?.dmPolicy, 'open')
|
|
380
|
-
const
|
|
380
|
+
const globalWhatsAppAllowFrom = connector.platform === 'whatsapp'
|
|
381
|
+
? getWhatsAppApprovedSenderIds(loadSettings().whatsappApprovedContacts)
|
|
382
|
+
: []
|
|
383
|
+
const configuredAllowFrom = parseAllowFromCsv([
|
|
384
|
+
connector.config?.allowFrom,
|
|
385
|
+
...globalWhatsAppAllowFrom,
|
|
386
|
+
].filter(Boolean).join(','))
|
|
381
387
|
const storedAllowFrom = listStoredAllowedSenders(connector.id)
|
|
382
388
|
if (parseBool(connector.config?.statusReactions, true) && connector.platform === 'telegram') {
|
|
383
389
|
warnings.push('Status reactions are enabled, but Telegram support is partial and may no-op depending on bot permissions.')
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { connectorRuntimeState, type ConnectorReconnectState } from './runtime-state'
|
|
2
|
+
|
|
3
|
+
export type { ConnectorReconnectState }
|
|
4
|
+
|
|
5
|
+
interface ConnectorReconnectPolicy {
|
|
6
|
+
initialBackoffMs?: number
|
|
7
|
+
maxBackoffMs?: number
|
|
8
|
+
maxAttempts?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const connectorReconnectStateStore: Map<string, ConnectorReconnectState> =
|
|
12
|
+
connectorRuntimeState.reconnectStates
|
|
13
|
+
|
|
14
|
+
const RECONNECT_INITIAL_BACKOFF_MS = 1_000
|
|
15
|
+
const RECONNECT_MAX_BACKOFF_MS = 5 * 60 * 1_000
|
|
16
|
+
const RECONNECT_MAX_ATTEMPTS = 10
|
|
17
|
+
|
|
18
|
+
export function createConnectorReconnectState(
|
|
19
|
+
init: Partial<ConnectorReconnectState> = {},
|
|
20
|
+
policy: ConnectorReconnectPolicy = {},
|
|
21
|
+
): ConnectorReconnectState {
|
|
22
|
+
return {
|
|
23
|
+
attempts: init.attempts ?? 0,
|
|
24
|
+
lastAttemptAt: init.lastAttemptAt ?? 0,
|
|
25
|
+
nextRetryAt: init.nextRetryAt ?? 0,
|
|
26
|
+
backoffMs: init.backoffMs ?? policy.initialBackoffMs ?? RECONNECT_INITIAL_BACKOFF_MS,
|
|
27
|
+
error: init.error ?? '',
|
|
28
|
+
exhausted: init.exhausted ?? false,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function advanceConnectorReconnectState(
|
|
33
|
+
previous: ConnectorReconnectState,
|
|
34
|
+
error: string,
|
|
35
|
+
now = Date.now(),
|
|
36
|
+
policy: ConnectorReconnectPolicy = {},
|
|
37
|
+
): ConnectorReconnectState {
|
|
38
|
+
const initialBackoffMs = policy.initialBackoffMs ?? RECONNECT_INITIAL_BACKOFF_MS
|
|
39
|
+
const maxBackoffMs = policy.maxBackoffMs ?? RECONNECT_MAX_BACKOFF_MS
|
|
40
|
+
const maxAttempts = policy.maxAttempts ?? RECONNECT_MAX_ATTEMPTS
|
|
41
|
+
const attempts = previous.attempts + 1
|
|
42
|
+
const backoffMs = Math.min(maxBackoffMs, initialBackoffMs * (2 ** Math.max(0, attempts - 1)))
|
|
43
|
+
return {
|
|
44
|
+
attempts,
|
|
45
|
+
lastAttemptAt: now,
|
|
46
|
+
nextRetryAt: now + backoffMs,
|
|
47
|
+
backoffMs,
|
|
48
|
+
error,
|
|
49
|
+
exhausted: attempts >= maxAttempts,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearReconnectState(connectorId: string): void {
|
|
54
|
+
connectorReconnectStateStore.delete(connectorId)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setReconnectState(connectorId: string, state: ConnectorReconnectState): void {
|
|
58
|
+
connectorReconnectStateStore.set(connectorId, state)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getReconnectState(connectorId: string): ConnectorReconnectState | null {
|
|
62
|
+
return connectorReconnectStateStore.get(connectorId) ?? null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getAllReconnectStates(): Record<string, ConnectorReconnectState> {
|
|
66
|
+
const out: Record<string, ConnectorReconnectState> = {}
|
|
67
|
+
for (const [id, state] of connectorReconnectStateStore.entries()) {
|
|
68
|
+
out[id] = { ...state }
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { UPLOAD_DIR } from '../storage'
|
|
4
|
+
import { safeJsonParseObject } from '../json-utils'
|
|
5
|
+
import type { InboundMessage, InboundMedia } from './types'
|
|
6
|
+
|
|
7
|
+
function resolveUploadPathFromUrl(rawUrl: string): string | null {
|
|
8
|
+
if (!rawUrl) return null
|
|
9
|
+
const normalized = rawUrl.trim()
|
|
10
|
+
const match = normalized.match(/\/api\/uploads\/([^?#)\s]+)/)
|
|
11
|
+
if (!match) return null
|
|
12
|
+
let decoded: string
|
|
13
|
+
try { decoded = decodeURIComponent(match[1]) } catch { decoded = match[1] }
|
|
14
|
+
const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
15
|
+
if (!safeName) return null
|
|
16
|
+
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
17
|
+
return fs.existsSync(filePath) ? filePath : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function uploadApiUrlFromPath(filePath: string): string | null {
|
|
21
|
+
const rel = path.relative(UPLOAD_DIR, filePath)
|
|
22
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null
|
|
23
|
+
const fileName = path.basename(rel)
|
|
24
|
+
return `/api/uploads/${encodeURIComponent(fileName)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseSseDataEvents(raw: string): Array<Record<string, unknown>> {
|
|
28
|
+
if (!raw) return []
|
|
29
|
+
const events: Array<Record<string, unknown>> = []
|
|
30
|
+
const lines = raw.split('\n')
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
if (!line.startsWith('data: ')) continue
|
|
33
|
+
const parsed = safeJsonParseObject(line.slice(6))
|
|
34
|
+
if (parsed) events.push(parsed)
|
|
35
|
+
}
|
|
36
|
+
return events
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseConnectorToolResult(toolOutput: string): { status?: string; to?: string; followUpId?: string; messageId?: string } | null {
|
|
40
|
+
const record = safeJsonParseObject(toolOutput)
|
|
41
|
+
if (!record) return null
|
|
42
|
+
const status = typeof record.status === 'string' ? String(record.status) : undefined
|
|
43
|
+
const to = typeof record.to === 'string' ? String(record.to) : undefined
|
|
44
|
+
const followUpId = typeof record.followUpId === 'string' ? String(record.followUpId) : undefined
|
|
45
|
+
const messageId = typeof record.messageId === 'string' ? String(record.messageId) : undefined
|
|
46
|
+
return { status, to, followUpId, messageId }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseConnectorToolInput(toolInput: string): Record<string, unknown> | null {
|
|
50
|
+
return safeJsonParseObject(toolInput)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function visibleConnectorToolText(input: Record<string, unknown> | null): string {
|
|
54
|
+
if (!input) return ''
|
|
55
|
+
const voiceText = typeof input.voiceText === 'string' ? input.voiceText.trim() : ''
|
|
56
|
+
if (voiceText) return voiceText
|
|
57
|
+
const message = typeof input.message === 'string' ? input.message.trim() : ''
|
|
58
|
+
if (message) return message
|
|
59
|
+
const caption = typeof input.caption === 'string' ? input.caption.trim() : ''
|
|
60
|
+
if (caption) return caption
|
|
61
|
+
const text = typeof input.text === 'string' ? input.text.trim() : ''
|
|
62
|
+
if (text) return text
|
|
63
|
+
return ''
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonicalUploadMediaKey(filePath: string): string {
|
|
67
|
+
const base = path.basename(filePath)
|
|
68
|
+
const ext = path.extname(base).toLowerCase()
|
|
69
|
+
const normalized = base
|
|
70
|
+
.replace(/^\d{10,16}-/, '')
|
|
71
|
+
.replace(/^(?:browser|screenshot)-\d{10,16}(?:-\d+)?\./, 'playwright-capture.')
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
return normalized || `unknown${ext}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shouldAllowMultipleMediaSends(userText: string): boolean {
|
|
77
|
+
const text = (userText || '').toLowerCase()
|
|
78
|
+
return /\b(all|both|multiple|several|many|every|each|two|three|4|four|screenshots|images|photos|files|documents)\b/.test(text)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function preferSingleBestMediaFile(files: Array<{ path: string; alt: string }>): Array<{ path: string; alt: string }> {
|
|
82
|
+
if (files.length <= 1) return files
|
|
83
|
+
const ranked = [...files].sort((a, b) => {
|
|
84
|
+
const score = (entry: { path: string }) => {
|
|
85
|
+
const base = path.basename(entry.path).toLowerCase()
|
|
86
|
+
let value = 0
|
|
87
|
+
if (/^\d{10,16}-/.test(base)) value += 20
|
|
88
|
+
if (!base.startsWith('browser-') && !base.startsWith('screenshot-')) value += 10
|
|
89
|
+
if (base.endsWith('.pdf')) value += 8
|
|
90
|
+
if (base.endsWith('.png') || base.endsWith('.jpg') || base.endsWith('.jpeg') || base.endsWith('.webp')) value += 6
|
|
91
|
+
try {
|
|
92
|
+
const stat = fs.statSync(entry.path)
|
|
93
|
+
value += Math.min(5, Math.round((stat.mtimeMs % 10_000) / 2_000))
|
|
94
|
+
} catch { /* ignore stat errors */ }
|
|
95
|
+
return value
|
|
96
|
+
}
|
|
97
|
+
return score(b) - score(a)
|
|
98
|
+
})
|
|
99
|
+
return [ranked[0]]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function selectOutboundMediaFiles(
|
|
103
|
+
files: Array<{ path: string; alt: string }>,
|
|
104
|
+
userText: string,
|
|
105
|
+
): Array<{ path: string; alt: string }> {
|
|
106
|
+
if (files.length === 0) return []
|
|
107
|
+
const mergedFiles: Array<{ path: string; alt: string }> = []
|
|
108
|
+
const seenMediaKeys = new Set<string>()
|
|
109
|
+
for (const candidate of files) {
|
|
110
|
+
const mediaKey = canonicalUploadMediaKey(candidate.path)
|
|
111
|
+
if (seenMediaKeys.has(mediaKey)) continue
|
|
112
|
+
seenMediaKeys.add(mediaKey)
|
|
113
|
+
mergedFiles.push(candidate)
|
|
114
|
+
}
|
|
115
|
+
return shouldAllowMultipleMediaSends(userText || '')
|
|
116
|
+
? mergedFiles
|
|
117
|
+
: preferSingleBestMediaFile(mergedFiles)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function extractEmbeddedMedia(text: string): { cleanText: string; files: Array<{ path: string; alt: string }> } {
|
|
121
|
+
const files: Array<{ path: string; alt: string }> = []
|
|
122
|
+
const seen = new Set<string>()
|
|
123
|
+
let cleanText = text
|
|
124
|
+
|
|
125
|
+
const pushFile = (filePath: string, alt: string) => {
|
|
126
|
+
if (!filePath || seen.has(filePath)) return
|
|
127
|
+
seen.add(filePath)
|
|
128
|
+
files.push({ path: filePath, alt: alt.trim() })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
|
|
132
|
+
cleanText = cleanText.replace(imageRegex, (full, altRaw, urlRaw) => {
|
|
133
|
+
const filePath = resolveUploadPathFromUrl(String(urlRaw || ''))
|
|
134
|
+
if (!filePath) return full
|
|
135
|
+
pushFile(filePath, String(altRaw || ''))
|
|
136
|
+
return ''
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const linkRegex = /(?<!!)\[([^\]]*)\]\(([^)]+)\)/g
|
|
140
|
+
cleanText = cleanText.replace(linkRegex, (full, altRaw, urlRaw) => {
|
|
141
|
+
const filePath = resolveUploadPathFromUrl(String(urlRaw || ''))
|
|
142
|
+
if (!filePath) return full
|
|
143
|
+
pushFile(filePath, String(altRaw || ''))
|
|
144
|
+
return ''
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const bareUploadUrlRegex = /(?:https?:\/\/[^\s)]+)?\/api\/uploads\/[^\s)\]]+/g
|
|
148
|
+
cleanText = cleanText.replace(bareUploadUrlRegex, (full) => {
|
|
149
|
+
const filePath = resolveUploadPathFromUrl(full)
|
|
150
|
+
if (!filePath) return full
|
|
151
|
+
pushFile(filePath, '')
|
|
152
|
+
return ''
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (files.length === 0) return { cleanText: text, files }
|
|
156
|
+
cleanText = cleanText.replace(/\n{3,}/g, '\n\n').trim()
|
|
157
|
+
return { cleanText, files }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildInboundAttachmentPaths(msg: InboundMessage): string[] {
|
|
161
|
+
if (!Array.isArray(msg.media) || msg.media.length === 0) return []
|
|
162
|
+
const paths: string[] = []
|
|
163
|
+
const seen = new Set<string>()
|
|
164
|
+
for (const media of msg.media) {
|
|
165
|
+
const localPath = typeof media.localPath === 'string' ? media.localPath.trim() : ''
|
|
166
|
+
if (!localPath || seen.has(localPath)) continue
|
|
167
|
+
if (!fs.existsSync(localPath)) continue
|
|
168
|
+
seen.add(localPath)
|
|
169
|
+
paths.push(localPath)
|
|
170
|
+
}
|
|
171
|
+
return paths
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Normalize a phone number string to E.164 format (+<digits>).
|
|
176
|
+
* Strips formatting characters, `whatsapp:` prefixes, and ensures a leading `+`.
|
|
177
|
+
* Works for all country codes — no country-specific heuristics.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeE164(number: string): string {
|
|
180
|
+
const withoutPrefix = number.replace(/^whatsapp:/i, '').trim()
|
|
181
|
+
const digits = withoutPrefix.replace(/[^\d+]/g, '')
|
|
182
|
+
if (digits.startsWith('+')) return `+${digits.slice(1)}`
|
|
183
|
+
return `+${digits}`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i
|
|
187
|
+
const WHATSAPP_LID_RE = /^(\d+)(?::\d+)?@lid$/i
|
|
188
|
+
const WHATSAPP_GROUP_JID_RE = /^[\d]+(-[\d]+)*@g\.us$/i
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Normalize a WhatsApp target (phone number, user JID, group JID) into
|
|
192
|
+
* a canonical JID suitable for sending messages.
|
|
193
|
+
*
|
|
194
|
+
* - Group JIDs (`…@g.us`) are preserved as-is.
|
|
195
|
+
* - User JIDs (`…@s.whatsapp.net`, `…@lid`) extract the phone number.
|
|
196
|
+
* - Plain phone numbers are cleaned to digits and suffixed with `@s.whatsapp.net`.
|
|
197
|
+
*
|
|
198
|
+
* Works for all country codes — ported from OpenClaw's normalizeWhatsAppTarget.
|
|
199
|
+
*/
|
|
200
|
+
export function normalizeWhatsappTarget(raw: string): string {
|
|
201
|
+
const trimmed = raw.replace(/^whatsapp:/i, '').trim()
|
|
202
|
+
if (!trimmed) return trimmed
|
|
203
|
+
|
|
204
|
+
// Group JIDs — preserve as-is
|
|
205
|
+
if (WHATSAPP_GROUP_JID_RE.test(trimmed)) return trimmed
|
|
206
|
+
|
|
207
|
+
// User JIDs — extract the phone number digits
|
|
208
|
+
const userMatch = trimmed.match(WHATSAPP_USER_JID_RE)
|
|
209
|
+
if (userMatch) return `${userMatch[1]}@s.whatsapp.net`
|
|
210
|
+
|
|
211
|
+
const lidMatch = trimmed.match(WHATSAPP_LID_RE)
|
|
212
|
+
if (lidMatch) return trimmed // LID JIDs can't be converted to phone-based JIDs
|
|
213
|
+
|
|
214
|
+
// Unknown JID format — return as-is to avoid mangling
|
|
215
|
+
if (trimmed.includes('@')) return trimmed
|
|
216
|
+
|
|
217
|
+
// Plain phone number — strip to digits and build JID
|
|
218
|
+
const digits = trimmed.replace(/[^\d+]/g, '')
|
|
219
|
+
const cleaned = digits.startsWith('+') ? digits.slice(1) : digits
|
|
220
|
+
return cleaned ? `${cleaned}@s.whatsapp.net` : trimmed
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function connectorSupportsBinaryMedia(platform: string): boolean {
|
|
224
|
+
return platform === 'whatsapp'
|
|
225
|
+
|| platform === 'telegram'
|
|
226
|
+
|| platform === 'slack'
|
|
227
|
+
|| platform === 'discord'
|
|
228
|
+
|| platform === 'openclaw'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function formatMediaLine(media: InboundMedia): string {
|
|
232
|
+
const typeLabel = media.type.toUpperCase()
|
|
233
|
+
const name = media.fileName || media.mimeType || 'attachment'
|
|
234
|
+
const size = media.sizeBytes ? ` (${Math.max(1, Math.round(media.sizeBytes / 1024))} KB)` : ''
|
|
235
|
+
if (media.url) return `- ${typeLabel}: ${name}${size} -> ${media.url}`
|
|
236
|
+
return `- ${typeLabel}: ${name}${size}`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function formatInboundUserText(msg: InboundMessage): string {
|
|
240
|
+
const baseText = (msg.text || '').trim()
|
|
241
|
+
const lines: string[] = []
|
|
242
|
+
if (baseText) lines.push(`[${msg.senderName}] ${baseText}`)
|
|
243
|
+
else lines.push(`[${msg.senderName}]`)
|
|
244
|
+
|
|
245
|
+
if (Array.isArray(msg.media) && msg.media.length > 0) {
|
|
246
|
+
lines.push('')
|
|
247
|
+
lines.push('Media received:')
|
|
248
|
+
const preview = msg.media.slice(0, 6)
|
|
249
|
+
for (const media of preview) lines.push(formatMediaLine(media))
|
|
250
|
+
if (msg.media.length > preview.length) {
|
|
251
|
+
lines.push(`- ...and ${msg.media.length - preview.length} more attachment(s)`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return lines.join('\n').trim()
|
|
256
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Connector, Session } from '@/types'
|
|
2
|
+
import type { ConnectorInstance, InboundMessage } from './types'
|
|
3
|
+
import { hmrSingleton } from '@/lib/shared-utils'
|
|
4
|
+
|
|
5
|
+
export interface ConnectorReconnectState {
|
|
6
|
+
attempts: number
|
|
7
|
+
lastAttemptAt: number
|
|
8
|
+
nextRetryAt: number
|
|
9
|
+
backoffMs: number
|
|
10
|
+
error: string
|
|
11
|
+
exhausted: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ScheduledConnectorFollowup {
|
|
15
|
+
id: string
|
|
16
|
+
connectorId?: string
|
|
17
|
+
platform?: string
|
|
18
|
+
channelId: string
|
|
19
|
+
sendAt: number
|
|
20
|
+
timer: ReturnType<typeof setTimeout>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DebouncedInboundEntry {
|
|
24
|
+
connector: Connector
|
|
25
|
+
messages: InboundMessage[]
|
|
26
|
+
timer: ReturnType<typeof setTimeout>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RouteMessageHandler = (connector: Connector, msg: InboundMessage) => Promise<string>
|
|
30
|
+
|
|
31
|
+
export interface ConnectorRuntimeState {
|
|
32
|
+
running: Map<string, ConnectorInstance>
|
|
33
|
+
lastInboundChannelByConnector: Map<string, string>
|
|
34
|
+
lastInboundTimeByConnector: Map<string, number>
|
|
35
|
+
locks: Map<string, Promise<void>>
|
|
36
|
+
generationCounter: Map<string, number>
|
|
37
|
+
scheduledFollowups: Map<string, ScheduledConnectorFollowup>
|
|
38
|
+
recentInboundByKey: Map<string, number>
|
|
39
|
+
pendingInboundDebounce: Map<string, DebouncedInboundEntry>
|
|
40
|
+
scheduledFollowupByDedupe: Map<string, { id: string; sendAt: number }>
|
|
41
|
+
reconnectStates: Map<string, ConnectorReconnectState>
|
|
42
|
+
routeMessageHandlerRef: { current: RouteMessageHandler }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getConnectorRuntimeState(): ConnectorRuntimeState {
|
|
46
|
+
return hmrSingleton('__swarmclaw_connector_runtime_state__', () => ({
|
|
47
|
+
running: hmrSingleton('__swarmclaw_running_connectors__', () => new Map<string, ConnectorInstance>()),
|
|
48
|
+
lastInboundChannelByConnector: hmrSingleton('__swarmclaw_connector_last_inbound__', () => new Map<string, string>()),
|
|
49
|
+
lastInboundTimeByConnector: hmrSingleton('__swarmclaw_connector_last_inbound_time__', () => new Map<string, number>()),
|
|
50
|
+
locks: hmrSingleton('__swarmclaw_connector_locks__', () => new Map<string, Promise<void>>()),
|
|
51
|
+
generationCounter: hmrSingleton('__swarmclaw_connector_gen__', () => new Map<string, number>()),
|
|
52
|
+
scheduledFollowups: hmrSingleton('__swarmclaw_connector_followups__', () => new Map<string, ScheduledConnectorFollowup>()),
|
|
53
|
+
recentInboundByKey: hmrSingleton('__swarmclaw_connector_inbound_dedupe__', () => new Map<string, number>()),
|
|
54
|
+
pendingInboundDebounce: hmrSingleton('__swarmclaw_connector_inbound_debounce__', () => new Map<string, DebouncedInboundEntry>()),
|
|
55
|
+
scheduledFollowupByDedupe: hmrSingleton('__swarmclaw_connector_followup_dedupe__', () => new Map<string, { id: string; sendAt: number }>()),
|
|
56
|
+
reconnectStates: hmrSingleton('__swarmclaw_connector_reconnect_state__', () => new Map<string, ConnectorReconnectState>()),
|
|
57
|
+
routeMessageHandlerRef: hmrSingleton('__swarmclaw_connector_route_handler__', () => ({
|
|
58
|
+
current: async () => '[Error] Connector router unavailable.',
|
|
59
|
+
})),
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const connectorRuntimeState = getConnectorRuntimeState()
|
|
64
|
+
|
|
65
|
+
export const runningConnectors = connectorRuntimeState.running
|
|
66
|
+
|
|
67
|
+
export type ConnectorThreadSession = Session
|