@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
|
@@ -3,8 +3,10 @@ import fs from 'fs'
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import type { Connector } from '@/types'
|
|
5
5
|
import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundMediaType } from './types'
|
|
6
|
+
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
7
|
+
import { deliverChunkedConnectorText } from './delivery'
|
|
6
8
|
import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime, isAudioMime } from './media'
|
|
7
|
-
import {
|
|
9
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
8
10
|
|
|
9
11
|
const telegram: PlatformConnector = {
|
|
10
12
|
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
@@ -152,37 +154,25 @@ const telegram: PlatformConnector = {
|
|
|
152
154
|
|
|
153
155
|
try {
|
|
154
156
|
await ctx.api.sendChatAction(ctx.chat.id, 'typing')
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
|
|
160
|
-
const baseOptions: Record<string, unknown> = {}
|
|
161
|
-
if (replyOptions.replyToMessageId) {
|
|
162
|
-
baseOptions.reply_parameters = { message_id: Number(replyOptions.replyToMessageId) }
|
|
163
|
-
}
|
|
164
|
-
if (replyOptions.threadId) {
|
|
165
|
-
baseOptions.message_thread_id = Number(replyOptions.threadId)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let lastMessageId: string | undefined
|
|
169
|
-
|
|
170
|
-
// Telegram has a 4096 char limit
|
|
171
|
-
if (response.length <= 4096) {
|
|
172
|
-
const sent = await ctx.api.sendMessage(ctx.chat.id, response, baseOptions as any)
|
|
173
|
-
lastMessageId = String(sent.message_id)
|
|
174
|
-
} else {
|
|
175
|
-
const chunks = response.match(/[\s\S]{1,4090}/g) || [response]
|
|
176
|
-
for (let i = 0; i < chunks.length; i += 1) {
|
|
177
|
-
const sent = await ctx.api.sendMessage(ctx.chat.id, chunks[i], (i === 0 ? baseOptions : {}) as any)
|
|
178
|
-
lastMessageId = String(sent.message_id)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
await recordConnectorOutboundDelivery({
|
|
157
|
+
const reply = await resolveConnectorIngressReply(onMessage, inbound)
|
|
158
|
+
if (!reply) return
|
|
159
|
+
await deliverChunkedConnectorText({
|
|
182
160
|
connectorId: connector.id,
|
|
183
161
|
inbound,
|
|
184
|
-
|
|
185
|
-
|
|
162
|
+
text: reply.visibleText,
|
|
163
|
+
maxSingleMessageLength: 4096,
|
|
164
|
+
chunkLength: 4090,
|
|
165
|
+
sendChunk: async (chunk, meta) => {
|
|
166
|
+
const options: Record<string, unknown> = {}
|
|
167
|
+
if (meta.isFirstChunk && meta.replyToMessageId) {
|
|
168
|
+
options.reply_parameters = { message_id: Number(meta.replyToMessageId) }
|
|
169
|
+
}
|
|
170
|
+
if (meta.threadId) {
|
|
171
|
+
options.message_thread_id = Number(meta.threadId)
|
|
172
|
+
}
|
|
173
|
+
const sent = await ctx.api.sendMessage(ctx.chat.id, chunk, options as any)
|
|
174
|
+
return String(sent.message_id)
|
|
175
|
+
},
|
|
186
176
|
})
|
|
187
177
|
} catch (err: any) {
|
|
188
178
|
console.error(`[telegram] Error handling message:`, err.message)
|
|
@@ -195,19 +185,7 @@ const telegram: PlatformConnector = {
|
|
|
195
185
|
// Track whether the bot is actively polling
|
|
196
186
|
let botRunning = true
|
|
197
187
|
|
|
198
|
-
|
|
199
|
-
bot.start({
|
|
200
|
-
allowed_updates: ['message', 'edited_message'],
|
|
201
|
-
onStart: (botInfo) => {
|
|
202
|
-
botUsername = botInfo.username || ''
|
|
203
|
-
console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
|
|
204
|
-
},
|
|
205
|
-
}).catch((err) => {
|
|
206
|
-
botRunning = false
|
|
207
|
-
console.error(`[telegram] Polling stopped with error:`, err.message || err)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
return {
|
|
188
|
+
const instance: ConnectorInstance = {
|
|
211
189
|
connector,
|
|
212
190
|
isAlive() {
|
|
213
191
|
return botRunning
|
|
@@ -293,6 +271,22 @@ const telegram: PlatformConnector = {
|
|
|
293
271
|
console.log(`[telegram] Bot stopped`)
|
|
294
272
|
},
|
|
295
273
|
}
|
|
274
|
+
|
|
275
|
+
// Start polling — not awaited (runs in background)
|
|
276
|
+
bot.start({
|
|
277
|
+
allowed_updates: ['message', 'edited_message'],
|
|
278
|
+
onStart: (botInfo) => {
|
|
279
|
+
botUsername = botInfo.username || ''
|
|
280
|
+
console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
|
|
281
|
+
},
|
|
282
|
+
}).catch((err: unknown) => {
|
|
283
|
+
botRunning = false
|
|
284
|
+
const errMsg = errorMessage(err)
|
|
285
|
+
console.error(`[telegram] Polling stopped with error:`, errMsg)
|
|
286
|
+
instance.onCrash?.(`Polling stopped: ${errMsg}`)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
return instance
|
|
296
290
|
},
|
|
297
291
|
}
|
|
298
292
|
|
|
@@ -68,6 +68,34 @@ export interface OutboundTypingOptions {
|
|
|
68
68
|
threadId?: string
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface ConnectorRouteResult {
|
|
72
|
+
managerHandled: boolean
|
|
73
|
+
visibleText: string
|
|
74
|
+
delivery: 'sent' | 'silent'
|
|
75
|
+
messageId?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ConnectorIngressResult = string | ConnectorRouteResult
|
|
79
|
+
|
|
80
|
+
export function isConnectorRouteResult(value: unknown): value is ConnectorRouteResult {
|
|
81
|
+
return !!value
|
|
82
|
+
&& typeof value === 'object'
|
|
83
|
+
&& !Array.isArray(value)
|
|
84
|
+
&& typeof (value as ConnectorRouteResult).managerHandled === 'boolean'
|
|
85
|
+
&& typeof (value as ConnectorRouteResult).visibleText === 'string'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function normalizeConnectorIngressResult(value: ConnectorIngressResult): ConnectorRouteResult {
|
|
89
|
+
if (isConnectorRouteResult(value)) return value
|
|
90
|
+
const visibleText = typeof value === 'string' ? value : ''
|
|
91
|
+
const normalized = visibleText.trim().toUpperCase()
|
|
92
|
+
return {
|
|
93
|
+
managerHandled: false,
|
|
94
|
+
visibleText,
|
|
95
|
+
delivery: normalized && normalized !== 'NO_MESSAGE' ? 'sent' : 'silent',
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
/** A running connector instance */
|
|
72
100
|
export interface ConnectorInstance {
|
|
73
101
|
connector: Connector
|
|
@@ -96,6 +124,8 @@ export interface ConnectorInstance {
|
|
|
96
124
|
sendTyping?: (channelId: string, options?: OutboundTypingOptions) => Promise<void>
|
|
97
125
|
/** Health check: returns true if the underlying connection is alive */
|
|
98
126
|
isAlive?: () => boolean
|
|
127
|
+
/** Called by the platform when the connection dies terminally (no internal retry) */
|
|
128
|
+
onCrash?: (error: string) => void
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
/** Platform-specific connector implementation */
|
|
@@ -103,6 +133,6 @@ export interface PlatformConnector {
|
|
|
103
133
|
start(
|
|
104
134
|
connector: Connector,
|
|
105
135
|
botToken: string,
|
|
106
|
-
onMessage: (msg: InboundMessage) => Promise<
|
|
136
|
+
onMessage: (msg: InboundMessage) => Promise<ConnectorIngressResult>,
|
|
107
137
|
): Promise<ConnectorInstance>
|
|
108
138
|
}
|
|
@@ -3,10 +3,13 @@ import test from 'node:test'
|
|
|
3
3
|
import {
|
|
4
4
|
buildWhatsAppTextPayloads,
|
|
5
5
|
buildWhatsAppInboundMessage,
|
|
6
|
+
isWhatsAppSocketAlive,
|
|
6
7
|
isWhatsAppInboundAllowed,
|
|
7
8
|
normalizeWhatsAppAudioForSend,
|
|
8
9
|
normalizeWhatsAppIdentifier,
|
|
10
|
+
resolveWhatsAppAllowedIdentifiers,
|
|
9
11
|
} from './whatsapp'
|
|
12
|
+
import { normalizeE164, normalizeWhatsappTarget } from './response-media'
|
|
10
13
|
|
|
11
14
|
test('buildWhatsAppTextPayloads disables link previews for text sends', () => {
|
|
12
15
|
const payloads = buildWhatsAppTextPayloads('See https://example.com for details')
|
|
@@ -32,6 +35,19 @@ test('normalizeWhatsAppIdentifier strips jid wrappers and device suffixes', () =
|
|
|
32
35
|
assert.equal(normalizeWhatsAppIdentifier('199900000001@lid'), '199900000001')
|
|
33
36
|
})
|
|
34
37
|
|
|
38
|
+
test('normalizeWhatsAppIdentifier handles international numbers', () => {
|
|
39
|
+
// Swiss
|
|
40
|
+
assert.equal(normalizeWhatsAppIdentifier('+41 79 666 68 64@s.whatsapp.net'), '41796666864')
|
|
41
|
+
// German
|
|
42
|
+
assert.equal(normalizeWhatsAppIdentifier('491711234567@s.whatsapp.net'), '491711234567')
|
|
43
|
+
// Brazilian
|
|
44
|
+
assert.equal(normalizeWhatsAppIdentifier('+55 11 99999-8888'), '5511999998888')
|
|
45
|
+
// Indian
|
|
46
|
+
assert.equal(normalizeWhatsAppIdentifier('919876543210:0@s.whatsapp.net'), '919876543210')
|
|
47
|
+
// Plain number with whatsapp: prefix
|
|
48
|
+
assert.equal(normalizeWhatsAppIdentifier('whatsapp:+447911123456'), '447911123456')
|
|
49
|
+
})
|
|
50
|
+
|
|
35
51
|
test('isWhatsAppInboundAllowed matches allow-list entries against alt phone JIDs', () => {
|
|
36
52
|
const allowed = ['15550001111']
|
|
37
53
|
const msg = {
|
|
@@ -45,6 +61,28 @@ test('isWhatsAppInboundAllowed matches allow-list entries against alt phone JIDs
|
|
|
45
61
|
assert.equal(isWhatsAppInboundAllowed({ allowedJids: ['15559990000'], msg }), false)
|
|
46
62
|
})
|
|
47
63
|
|
|
64
|
+
test('resolveWhatsAppAllowedIdentifiers merges connector and settings approvals', () => {
|
|
65
|
+
const allowed = resolveWhatsAppAllowedIdentifiers({
|
|
66
|
+
configuredAllowedJids: '15550001111',
|
|
67
|
+
settingsContacts: [
|
|
68
|
+
{ id: 'family', label: 'Family', phone: '+1 (666) 000-2222' },
|
|
69
|
+
{ id: 'dup', label: 'Family JID', phone: '16660002222@s.whatsapp.net' },
|
|
70
|
+
],
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(allowed, ['15550001111', '16660002222'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('resolveWhatsAppAllowedIdentifiers keeps the connector open when no allowedJids are configured', () => {
|
|
77
|
+
const allowed = resolveWhatsAppAllowedIdentifiers({
|
|
78
|
+
settingsContacts: [
|
|
79
|
+
{ id: 'family', label: 'Family', phone: '+1 (666) 000-2222' },
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
assert.equal(allowed, null)
|
|
84
|
+
})
|
|
85
|
+
|
|
48
86
|
test('buildWhatsAppInboundMessage includes modern WhatsApp metadata', () => {
|
|
49
87
|
const inbound = buildWhatsAppInboundMessage({
|
|
50
88
|
msg: {
|
|
@@ -79,6 +117,34 @@ test('buildWhatsAppInboundMessage includes modern WhatsApp metadata', () => {
|
|
|
79
117
|
assert.equal(inbound?.text, 'Hey there')
|
|
80
118
|
})
|
|
81
119
|
|
|
120
|
+
test('isWhatsAppSocketAlive reports disconnected sockets as dead so daemon restarts can run', () => {
|
|
121
|
+
assert.equal(isWhatsAppSocketAlive({
|
|
122
|
+
stopped: false,
|
|
123
|
+
socket: { ws: { isClosed: true } },
|
|
124
|
+
connectionState: 'close',
|
|
125
|
+
}), false)
|
|
126
|
+
|
|
127
|
+
assert.equal(isWhatsAppSocketAlive({
|
|
128
|
+
stopped: false,
|
|
129
|
+
socket: { ws: { isClosing: true } },
|
|
130
|
+
connectionState: 'connecting',
|
|
131
|
+
}), false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('isWhatsAppSocketAlive keeps QR and active sessions marked live', () => {
|
|
135
|
+
assert.equal(isWhatsAppSocketAlive({
|
|
136
|
+
stopped: false,
|
|
137
|
+
socket: { ws: { isConnecting: true } },
|
|
138
|
+
connectionState: 'connecting',
|
|
139
|
+
}), true)
|
|
140
|
+
|
|
141
|
+
assert.equal(isWhatsAppSocketAlive({
|
|
142
|
+
stopped: false,
|
|
143
|
+
socket: { ws: { isOpen: true } },
|
|
144
|
+
connectionState: 'open',
|
|
145
|
+
}), true)
|
|
146
|
+
})
|
|
147
|
+
|
|
82
148
|
test('normalizeWhatsAppAudioForSend transcodes mp3 voice notes to Android-safe opus/ogg', () => {
|
|
83
149
|
let transcodeCalls = 0
|
|
84
150
|
const converted = normalizeWhatsAppAudioForSend({
|
|
@@ -132,3 +198,45 @@ test('normalizeWhatsAppAudioForSend leaves normal audio attachments alone when p
|
|
|
132
198
|
assert.equal(converted.buffer.toString(), 'music')
|
|
133
199
|
assert.equal(converted.mimeType, 'audio/mpeg')
|
|
134
200
|
})
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// normalizeE164 — E.164 formatting for all country codes
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
test('normalizeE164 strips formatting and ensures + prefix', () => {
|
|
207
|
+
assert.equal(normalizeE164('+41 79 666 68 64'), '+41796666864')
|
|
208
|
+
assert.equal(normalizeE164('(555) 123-4567'), '+5551234567')
|
|
209
|
+
assert.equal(normalizeE164('whatsapp:+447911123456'), '+447911123456')
|
|
210
|
+
assert.equal(normalizeE164('491711234567'), '+491711234567')
|
|
211
|
+
assert.equal(normalizeE164('+55 11 99999-8888'), '+5511999998888')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// normalizeWhatsappTarget — JID normalization for outbound messages
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
test('normalizeWhatsappTarget builds user JIDs from plain numbers', () => {
|
|
219
|
+
assert.equal(normalizeWhatsappTarget('+41796666864'), '41796666864@s.whatsapp.net')
|
|
220
|
+
assert.equal(normalizeWhatsappTarget('447911123456'), '447911123456@s.whatsapp.net')
|
|
221
|
+
assert.equal(normalizeWhatsappTarget('+55 11 99999-8888'), '5511999998888@s.whatsapp.net')
|
|
222
|
+
assert.equal(normalizeWhatsappTarget('(555) 123-4567'), '5551234567@s.whatsapp.net')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('normalizeWhatsappTarget preserves group JIDs', () => {
|
|
226
|
+
assert.equal(normalizeWhatsappTarget('120363401234567890@g.us'), '120363401234567890@g.us')
|
|
227
|
+
assert.equal(normalizeWhatsappTarget('120363-401234567890@g.us'), '120363-401234567890@g.us')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('normalizeWhatsappTarget extracts phone from user JIDs', () => {
|
|
231
|
+
assert.equal(normalizeWhatsappTarget('41796666864:0@s.whatsapp.net'), '41796666864@s.whatsapp.net')
|
|
232
|
+
assert.equal(normalizeWhatsappTarget('15550001111:7@s.whatsapp.net'), '15550001111@s.whatsapp.net')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('normalizeWhatsappTarget passes through LID JIDs unchanged', () => {
|
|
236
|
+
assert.equal(normalizeWhatsappTarget('199900000001@lid'), '199900000001@lid')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('normalizeWhatsappTarget strips whatsapp: prefix', () => {
|
|
240
|
+
assert.equal(normalizeWhatsappTarget('whatsapp:+447911123456'), '447911123456@s.whatsapp.net')
|
|
241
|
+
assert.equal(normalizeWhatsappTarget('whatsapp:120363401234567890@g.us'), '120363401234567890@g.us')
|
|
242
|
+
})
|
|
@@ -12,13 +12,16 @@ import fs from 'fs'
|
|
|
12
12
|
import os from 'os'
|
|
13
13
|
import { spawnSync } from 'child_process'
|
|
14
14
|
import type { Connector } from '@/types'
|
|
15
|
+
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
15
16
|
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
17
|
+
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
16
18
|
import { saveInboundMediaBuffer, mimeFromPath, isImageMime, isAudioMime } from './media'
|
|
17
|
-
import {
|
|
19
|
+
import { recordConnectorOutboundDelivery } from './delivery'
|
|
18
20
|
import { formatTextForWhatsApp } from './whatsapp-text'
|
|
21
|
+
import { getWhatsAppApprovedSenderIds } from './pairing'
|
|
19
22
|
|
|
20
23
|
import { DATA_DIR } from '../data-dir'
|
|
21
|
-
import { loadConnectors } from '../storage'
|
|
24
|
+
import { loadConnectors, loadSettings } from '../storage'
|
|
22
25
|
|
|
23
26
|
const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth')
|
|
24
27
|
const INBOUND_DEDUPE_TTL_MS = 2 * 60 * 1000
|
|
@@ -29,6 +32,15 @@ const WHATSAPP_VOICE_NOTE_EXTS = new Set(['.ogg', '.opus'])
|
|
|
29
32
|
|
|
30
33
|
let cachedFfmpegBinary: string | null | undefined
|
|
31
34
|
|
|
35
|
+
type WhatsAppSocketState = {
|
|
36
|
+
ws?: {
|
|
37
|
+
isOpen?: boolean
|
|
38
|
+
isClosed?: boolean
|
|
39
|
+
isClosing?: boolean
|
|
40
|
+
isConnecting?: boolean
|
|
41
|
+
} | null
|
|
42
|
+
} | null
|
|
43
|
+
|
|
32
44
|
export function buildWhatsAppTextPayloads(text: string): Array<{ text: string; linkPreview: null }> {
|
|
33
45
|
const chunks = text.length <= WHATSAPP_SINGLE_MESSAGE_MAX
|
|
34
46
|
? [text]
|
|
@@ -36,6 +48,26 @@ export function buildWhatsAppTextPayloads(text: string): Array<{ text: string; l
|
|
|
36
48
|
return chunks.map((chunk) => ({ text: chunk, linkPreview: null }))
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
export function isWhatsAppSocketAlive(params: {
|
|
52
|
+
stopped: boolean
|
|
53
|
+
socket: WhatsAppSocketState
|
|
54
|
+
connectionState?: string | null
|
|
55
|
+
}): boolean {
|
|
56
|
+
if (params.stopped) return false
|
|
57
|
+
if (!params.socket) return false
|
|
58
|
+
|
|
59
|
+
const ws = params.socket.ws
|
|
60
|
+
if (!ws) return false
|
|
61
|
+
if (params.connectionState === 'close') return false
|
|
62
|
+
if (ws.isClosed || ws.isClosing) return false
|
|
63
|
+
if (ws.isOpen || ws.isConnecting) return true
|
|
64
|
+
if (params.connectionState === 'open' || params.connectionState === 'connecting') return true
|
|
65
|
+
|
|
66
|
+
// Treat an existing socket with no explicit close signal as live while QR/auth
|
|
67
|
+
// negotiation is still in progress.
|
|
68
|
+
return params.connectionState == null
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
function normalizeMimeType(mimeType?: string): string {
|
|
40
72
|
return String(mimeType || '').toLowerCase().split(';')[0].trim()
|
|
41
73
|
}
|
|
@@ -141,6 +173,7 @@ export function normalizeWhatsAppAudioForSend(params: {
|
|
|
141
173
|
return converted || { buffer: params.buffer, mimeType }
|
|
142
174
|
}
|
|
143
175
|
|
|
176
|
+
/** Extract the user part from a JID, stripping the server and device suffix */
|
|
144
177
|
function jidUserPart(raw: string): string {
|
|
145
178
|
const trimmed = String(raw || '').trim().toLowerCase()
|
|
146
179
|
if (!trimmed) return ''
|
|
@@ -148,16 +181,15 @@ function jidUserPart(raw: string): string {
|
|
|
148
181
|
return withoutServer.split(':')[0]
|
|
149
182
|
}
|
|
150
183
|
|
|
151
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Normalize a phone number or JID to a bare-digit identifier for matching.
|
|
186
|
+
* Works for all country codes — strips formatting, `whatsapp:` prefixes,
|
|
187
|
+
* JID suffixes (`@s.whatsapp.net`, `@lid`), and device suffixes (`:0`).
|
|
188
|
+
* Returns bare digits (no `+` prefix) for comparison.
|
|
189
|
+
*/
|
|
152
190
|
export function normalizeWhatsAppIdentifier(raw: string): string {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (n.startsWith('0') && n.length >= 10) {
|
|
156
|
-
n = '44' + n.slice(1)
|
|
157
|
-
}
|
|
158
|
-
// Strip leading +
|
|
159
|
-
if (n.startsWith('+')) n = n.slice(1)
|
|
160
|
-
return n.replace(/[^a-z0-9]/g, '')
|
|
191
|
+
const withoutPrefix = String(raw || '').replace(/^whatsapp:/i, '').trim()
|
|
192
|
+
return jidUserPart(withoutPrefix).replace(/[^\da-z]/g, '')
|
|
161
193
|
}
|
|
162
194
|
|
|
163
195
|
function parseAllowedIdentifiers(raw: unknown): string[] | null {
|
|
@@ -169,6 +201,19 @@ function parseAllowedIdentifiers(raw: unknown): string[] | null {
|
|
|
169
201
|
return out.length ? out : null
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
export function resolveWhatsAppAllowedIdentifiers(params: {
|
|
205
|
+
configuredAllowedJids?: unknown
|
|
206
|
+
settingsContacts?: unknown
|
|
207
|
+
}): string[] | null {
|
|
208
|
+
const configured = parseAllowedIdentifiers(params.configuredAllowedJids)
|
|
209
|
+
if (!configured?.length) return null
|
|
210
|
+
const settings = getWhatsAppApprovedSenderIds(params.settingsContacts)
|
|
211
|
+
.map((entry) => normalizeWhatsAppIdentifier(entry))
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
const merged = dedup([...configured, ...settings])
|
|
214
|
+
return merged.length ? merged : null
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
function messageContextInfo(content: any): any {
|
|
173
218
|
return content?.extendedTextMessage?.contextInfo
|
|
174
219
|
|| content?.imageMessage?.contextInfo
|
|
@@ -190,7 +235,7 @@ export function collectWhatsAppAddressCandidates(msg: Pick<WAMessage, 'key'>): s
|
|
|
190
235
|
const normalized = raw
|
|
191
236
|
.map((entry) => normalizeWhatsAppIdentifier(String(entry || '')))
|
|
192
237
|
.filter(Boolean)
|
|
193
|
-
return
|
|
238
|
+
return dedup(normalized)
|
|
194
239
|
}
|
|
195
240
|
|
|
196
241
|
export function isWhatsAppInboundAllowed(params: {
|
|
@@ -292,21 +337,38 @@ const whatsapp: PlatformConnector = {
|
|
|
292
337
|
let sock: ReturnType<typeof makeWASocket> | null = null
|
|
293
338
|
let stopped = false
|
|
294
339
|
let socketGen = 0 // Track socket generation to ignore stale events
|
|
340
|
+
let connectionState: string | null = 'connecting'
|
|
341
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
295
342
|
const seenInboundMessageIds = new Map<string, number>()
|
|
296
343
|
|
|
344
|
+
const clearReconnectTimer = () => {
|
|
345
|
+
if (!reconnectTimer) return
|
|
346
|
+
clearTimeout(reconnectTimer)
|
|
347
|
+
reconnectTimer = null
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const scheduleReconnect = (delayMs: number) => {
|
|
351
|
+
if (stopped) return
|
|
352
|
+
clearReconnectTimer()
|
|
353
|
+
reconnectTimer = setTimeout(() => {
|
|
354
|
+
reconnectTimer = null
|
|
355
|
+
if (stopped) return
|
|
356
|
+
startSocket()
|
|
357
|
+
}, delayMs)
|
|
358
|
+
reconnectTimer.unref?.()
|
|
359
|
+
}
|
|
360
|
+
|
|
297
361
|
const instance: ConnectorInstance = {
|
|
298
362
|
connector,
|
|
299
363
|
qrDataUrl: null,
|
|
300
364
|
authenticated: false,
|
|
301
365
|
hasCredentials: hasStoredCreds(authDir),
|
|
302
366
|
isAlive() {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
// If we have a socket but not yet authenticated (QR phase), still considered alive
|
|
309
|
-
return !stopped
|
|
367
|
+
return isWhatsAppSocketAlive({
|
|
368
|
+
stopped,
|
|
369
|
+
socket: sock,
|
|
370
|
+
connectionState,
|
|
371
|
+
})
|
|
310
372
|
},
|
|
311
373
|
async sendMessage(channelId, text, options) {
|
|
312
374
|
if (!sock) throw new Error('WhatsApp connector is not connected')
|
|
@@ -324,7 +386,7 @@ const whatsapp: PlatformConnector = {
|
|
|
324
386
|
try {
|
|
325
387
|
sent = await sock.sendMessage(channelId, { image: buf, caption, mimetype: mime })
|
|
326
388
|
} catch (err: unknown) {
|
|
327
|
-
const errMsg =
|
|
389
|
+
const errMsg = errorMessage(err)
|
|
328
390
|
console.warn(`[whatsapp] Image send failed (${errMsg}); retrying as document: ${fName}`)
|
|
329
391
|
sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
|
|
330
392
|
}
|
|
@@ -378,6 +440,8 @@ const whatsapp: PlatformConnector = {
|
|
|
378
440
|
},
|
|
379
441
|
async stop() {
|
|
380
442
|
stopped = true
|
|
443
|
+
connectionState = 'close'
|
|
444
|
+
clearReconnectTimer()
|
|
381
445
|
try { sock?.end(undefined) } catch { /* ignore */ }
|
|
382
446
|
sock = null
|
|
383
447
|
console.log(`[whatsapp] Stopped connector: ${connector.name}`)
|
|
@@ -388,6 +452,9 @@ const whatsapp: PlatformConnector = {
|
|
|
388
452
|
const sentMessageIds = new Set<string>()
|
|
389
453
|
|
|
390
454
|
const startSocket = () => {
|
|
455
|
+
if (stopped) return
|
|
456
|
+
clearReconnectTimer()
|
|
457
|
+
|
|
391
458
|
// Close previous socket to prevent stale event handlers
|
|
392
459
|
if (sock) {
|
|
393
460
|
try { sock.ev.removeAllListeners('connection.update') } catch { /* ignore */ }
|
|
@@ -398,6 +465,7 @@ const whatsapp: PlatformConnector = {
|
|
|
398
465
|
}
|
|
399
466
|
|
|
400
467
|
const gen = ++socketGen // Capture generation for stale detection
|
|
468
|
+
connectionState = 'connecting'
|
|
401
469
|
console.log(`[whatsapp] Starting socket gen=${gen} for ${connector.name} (hasCreds=${instance.hasCredentials})`)
|
|
402
470
|
|
|
403
471
|
sock = makeWASocket({
|
|
@@ -416,6 +484,7 @@ const whatsapp: PlatformConnector = {
|
|
|
416
484
|
if (gen !== socketGen) return // Ignore events from stale sockets
|
|
417
485
|
|
|
418
486
|
const { connection, lastDisconnect, qr } = update
|
|
487
|
+
if (typeof connection === 'string' && connection) connectionState = connection
|
|
419
488
|
console.log(`[whatsapp] Connection update gen=${gen}: connection=${connection}, hasQR=${!!qr}`)
|
|
420
489
|
|
|
421
490
|
if (qr) {
|
|
@@ -444,16 +513,17 @@ const whatsapp: PlatformConnector = {
|
|
|
444
513
|
if (!stopped) {
|
|
445
514
|
// Recreate auth dir and state for fresh start
|
|
446
515
|
fs.mkdirSync(authDir, { recursive: true })
|
|
447
|
-
|
|
516
|
+
scheduleReconnect(1000)
|
|
448
517
|
}
|
|
449
518
|
} else if (reason === 440) {
|
|
450
519
|
// Conflict — another session replaced this one. Do NOT reconnect
|
|
451
520
|
// (reconnecting would create a ping-pong loop with the other session)
|
|
452
521
|
console.log(`[whatsapp] Session conflict (replaced by another connection) — stopping`)
|
|
453
522
|
instance.authenticated = false
|
|
523
|
+
instance.onCrash?.('Session conflict — replaced by another connection')
|
|
454
524
|
} else if (!stopped) {
|
|
455
525
|
console.log(`[whatsapp] Reconnecting in 3s...`)
|
|
456
|
-
|
|
526
|
+
scheduleReconnect(3000)
|
|
457
527
|
} else {
|
|
458
528
|
console.log(`[whatsapp] Disconnected permanently`)
|
|
459
529
|
}
|
|
@@ -518,7 +588,10 @@ const whatsapp: PlatformConnector = {
|
|
|
518
588
|
|
|
519
589
|
const jid = msg.key.remoteJid || ''
|
|
520
590
|
const latestConnector = (loadConnectors()[connector.id] as Connector | undefined) || connector
|
|
521
|
-
const allowedJids =
|
|
591
|
+
const allowedJids = resolveWhatsAppAllowedIdentifiers({
|
|
592
|
+
configuredAllowedJids: latestConnector.config?.allowedJids,
|
|
593
|
+
settingsContacts: loadSettings().whatsappApprovedContacts,
|
|
594
|
+
})
|
|
522
595
|
|
|
523
596
|
// Match allowed JIDs using normalized numbers
|
|
524
597
|
// Self-chat always passes the filter (it's the bot's own account)
|
|
@@ -580,18 +653,17 @@ const whatsapp: PlatformConnector = {
|
|
|
580
653
|
|
|
581
654
|
try {
|
|
582
655
|
await sock!.sendPresenceUpdate('composing', jid)
|
|
583
|
-
const
|
|
656
|
+
const reply = await resolveConnectorIngressReply(onMessage, inbound)
|
|
584
657
|
await sock!.sendPresenceUpdate('paused', jid)
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
658
|
+
if (!reply) continue
|
|
659
|
+
|
|
660
|
+
const sent = await instance.sendMessage?.(jid, reply.visibleText)
|
|
661
|
+
await recordConnectorOutboundDelivery({
|
|
662
|
+
connectorId: connector.id,
|
|
663
|
+
inbound,
|
|
664
|
+
messageId: sent?.messageId,
|
|
665
|
+
state: 'sent',
|
|
666
|
+
})
|
|
595
667
|
} catch (err: any) {
|
|
596
668
|
console.error(`[whatsapp] Error handling message:`, err.message)
|
|
597
669
|
try {
|