@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
|
@@ -2,7 +2,7 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { DATA_DIR } from '../data-dir'
|
|
4
4
|
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
5
|
-
import {
|
|
5
|
+
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
6
6
|
|
|
7
7
|
const matrix: PlatformConnector = {
|
|
8
8
|
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
@@ -48,9 +48,9 @@ const matrix: PlatformConnector = {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
await client.sendText(roomId,
|
|
51
|
+
const reply = await resolveConnectorIngressReply(onMessage, inbound)
|
|
52
|
+
if (!reply) return
|
|
53
|
+
await client.sendText(roomId, reply.visibleText)
|
|
54
54
|
} catch (err: any) {
|
|
55
55
|
console.error(`[matrix] Error handling message:`, err.message)
|
|
56
56
|
try {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Sentinel value agents return when no outbound reply should be sent */
|
|
2
|
+
export const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
|
|
3
|
+
|
|
4
|
+
/** Check if an agent response is the NO_MESSAGE sentinel (case-insensitive, trimmed) */
|
|
5
|
+
export function isNoMessage(text: string): boolean {
|
|
6
|
+
return text.trim().toUpperCase() === NO_MESSAGE_SENTINEL
|
|
7
|
+
}
|
|
@@ -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 {
|
|
4
|
+
import { OPENCLAW_DATA_DIR } from '../data-dir'
|
|
5
|
+
import { safeJsonParse } from '../json-utils'
|
|
5
6
|
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
7
|
+
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
6
8
|
import {
|
|
7
9
|
createGatewayRequestFrame,
|
|
8
10
|
parseGatewayFrame,
|
|
@@ -38,7 +40,6 @@ const DEFAULT_CHAT_HISTORY_LIMIT = 40
|
|
|
38
40
|
const MIN_CHAT_HISTORY_LIMIT = 5
|
|
39
41
|
const MAX_CHAT_HISTORY_LIMIT = 200
|
|
40
42
|
const MAX_INLINE_ATTACHMENT_BYTES = 5_000_000
|
|
41
|
-
const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
|
|
42
43
|
const MAX_SEEN_HISTORY_MESSAGES = 4_096
|
|
43
44
|
const RECENT_HISTORY_DUPLICATE_WINDOW_MS = 20_000
|
|
44
45
|
const HISTORY_ERROR_LOG_INTERVAL_MS = 30_000
|
|
@@ -126,10 +127,6 @@ function isSecureWsUrl(url: string): boolean {
|
|
|
126
127
|
return false
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
function isNoMessage(text: string): boolean {
|
|
130
|
-
return text.trim().toUpperCase() === NO_MESSAGE_SENTINEL
|
|
131
|
-
}
|
|
132
|
-
|
|
133
130
|
function base64UrlEncode(buf: Buffer): string {
|
|
134
131
|
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '')
|
|
135
132
|
}
|
|
@@ -185,7 +182,7 @@ function signDevicePayload(privateKeyPem: string, payload: string): string {
|
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
function resolveIdentityPath(connectorId: string): string {
|
|
188
|
-
return path.join(
|
|
185
|
+
return path.join(OPENCLAW_DATA_DIR, `${connectorId}-device.json`)
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
function persistIdentity(filePath: string, identity: DeviceIdentity) {
|
|
@@ -205,7 +202,7 @@ function persistIdentity(filePath: string, identity: DeviceIdentity) {
|
|
|
205
202
|
function loadOrCreateIdentity(filePath: string): DeviceIdentity {
|
|
206
203
|
try {
|
|
207
204
|
if (fs.existsSync(filePath)) {
|
|
208
|
-
const parsed =
|
|
205
|
+
const parsed = safeJsonParse<StoredIdentity | null>(fs.readFileSync(filePath, 'utf8'), null)
|
|
209
206
|
if (
|
|
210
207
|
parsed?.version === 1
|
|
211
208
|
&& typeof parsed.deviceId === 'string'
|
|
@@ -960,8 +957,9 @@ const openclaw: PlatformConnector = {
|
|
|
960
957
|
markRecentInbound(inbound, now)
|
|
961
958
|
|
|
962
959
|
try {
|
|
963
|
-
const
|
|
964
|
-
if (!
|
|
960
|
+
const reply = await resolveConnectorIngressReply(onMessage, inbound)
|
|
961
|
+
if (!reply) return
|
|
962
|
+
await sendChat(inbound.channelId, reply.visibleText)
|
|
965
963
|
} catch (err: unknown) {
|
|
966
964
|
const message = getErrorMessage(err)
|
|
967
965
|
console.error('[openclaw] Error routing inbound chat event:', message)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { runWithTempDataDir } from '../test-utils/run-with-temp-data-dir'
|
|
4
|
+
|
|
5
|
+
describe('connector outbox', () => {
|
|
6
|
+
it('delivers scheduled follow-ups through the durable outbox worker', () => {
|
|
7
|
+
const output = runWithTempDataDir(`
|
|
8
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
9
|
+
const managerMod = await import('./src/lib/server/connectors/manager')
|
|
10
|
+
const outboxMod = await import('./src/lib/server/connectors/outbox')
|
|
11
|
+
const pluginsMod = await import('./src/lib/server/plugins')
|
|
12
|
+
const storage = storageMod.default || storageMod
|
|
13
|
+
const manager = managerMod.default || managerMod
|
|
14
|
+
const outbox = outboxMod.default || outboxMod
|
|
15
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
16
|
+
|
|
17
|
+
const attempts = []
|
|
18
|
+
plugins.getPluginManager().registerBuiltin('test-outbox-plugin', {
|
|
19
|
+
name: 'Test Outbox Plugin',
|
|
20
|
+
connectors: [{
|
|
21
|
+
id: 'test-outbox',
|
|
22
|
+
name: 'Test Outbox',
|
|
23
|
+
description: 'Test connector for outbox delivery',
|
|
24
|
+
startListener: async () => async () => {},
|
|
25
|
+
sendMessage: async (channelId, text) => {
|
|
26
|
+
attempts.push({ channelId, text })
|
|
27
|
+
return { messageId: 'outbox-msg-1' }
|
|
28
|
+
},
|
|
29
|
+
}],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const now = Date.now()
|
|
33
|
+
storage.saveSettings({})
|
|
34
|
+
storage.saveConnectors({
|
|
35
|
+
conn_1: {
|
|
36
|
+
id: 'conn_1',
|
|
37
|
+
name: 'Outbox Connector',
|
|
38
|
+
platform: 'test-outbox',
|
|
39
|
+
agentId: 'agent_1',
|
|
40
|
+
credentialId: null,
|
|
41
|
+
config: { botToken: 'test-token' },
|
|
42
|
+
isEnabled: true,
|
|
43
|
+
status: 'stopped',
|
|
44
|
+
createdAt: now,
|
|
45
|
+
updatedAt: now,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await manager.startConnector('conn_1')
|
|
50
|
+
const scheduled = manager.scheduleConnectorFollowUp({
|
|
51
|
+
connectorId: 'conn_1',
|
|
52
|
+
channelId: '15550001111',
|
|
53
|
+
text: 'Follow up later',
|
|
54
|
+
delaySec: 1,
|
|
55
|
+
})
|
|
56
|
+
const before = storage.loadConnectorOutbox()[scheduled.followUpId]
|
|
57
|
+
await outbox.runConnectorOutboxNow({ now: scheduled.sendAt + 5 })
|
|
58
|
+
const after = storage.loadConnectorOutbox()[scheduled.followUpId]
|
|
59
|
+
console.log(JSON.stringify({ scheduled, before, after, attempts }))
|
|
60
|
+
`, { prefix: 'swarmclaw-outbox-test-' })
|
|
61
|
+
|
|
62
|
+
assert.equal(output.before.status, 'pending')
|
|
63
|
+
assert.equal(output.after.status, 'sent')
|
|
64
|
+
assert.equal(output.after.lastMessageId, 'outbox-msg-1')
|
|
65
|
+
assert.deepEqual(output.attempts, [{ channelId: '15550001111', text: 'Follow up later' }])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('retries failed outbox sends with backoff and eventually marks them sent', () => {
|
|
69
|
+
const output = runWithTempDataDir(`
|
|
70
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
71
|
+
const managerMod = await import('./src/lib/server/connectors/manager')
|
|
72
|
+
const outboxMod = await import('./src/lib/server/connectors/outbox')
|
|
73
|
+
const pluginsMod = await import('./src/lib/server/plugins')
|
|
74
|
+
const storage = storageMod.default || storageMod
|
|
75
|
+
const manager = managerMod.default || managerMod
|
|
76
|
+
const outbox = outboxMod.default || outboxMod
|
|
77
|
+
const plugins = pluginsMod.default || pluginsMod
|
|
78
|
+
|
|
79
|
+
let sendCount = 0
|
|
80
|
+
plugins.getPluginManager().registerBuiltin('test-outbox-retry-plugin', {
|
|
81
|
+
name: 'Test Outbox Retry Plugin',
|
|
82
|
+
connectors: [{
|
|
83
|
+
id: 'test-outbox-retry',
|
|
84
|
+
name: 'Test Outbox Retry',
|
|
85
|
+
description: 'Test connector for outbox retries',
|
|
86
|
+
startListener: async () => async () => {},
|
|
87
|
+
sendMessage: async (channelId, text) => {
|
|
88
|
+
sendCount += 1
|
|
89
|
+
if (sendCount === 1) throw new Error('temporary send failure')
|
|
90
|
+
return { messageId: 'retry-msg-2' }
|
|
91
|
+
},
|
|
92
|
+
}],
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const now = Date.now()
|
|
96
|
+
storage.saveSettings({})
|
|
97
|
+
storage.saveConnectors({
|
|
98
|
+
conn_retry: {
|
|
99
|
+
id: 'conn_retry',
|
|
100
|
+
name: 'Retry Connector',
|
|
101
|
+
platform: 'test-outbox-retry',
|
|
102
|
+
agentId: 'agent_1',
|
|
103
|
+
credentialId: null,
|
|
104
|
+
config: { botToken: 'test-token' },
|
|
105
|
+
isEnabled: true,
|
|
106
|
+
status: 'stopped',
|
|
107
|
+
createdAt: now,
|
|
108
|
+
updatedAt: now,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
await manager.startConnector('conn_retry')
|
|
113
|
+
const queued = outbox.enqueueConnectorOutbox({
|
|
114
|
+
connectorId: 'conn_retry',
|
|
115
|
+
channelId: 'retry-channel',
|
|
116
|
+
text: 'Retry me',
|
|
117
|
+
sendAt: now,
|
|
118
|
+
maxAttempts: 3,
|
|
119
|
+
})
|
|
120
|
+
await outbox.runConnectorOutboxNow({ now: queued.sendAt + 1 })
|
|
121
|
+
const afterFirst = storage.loadConnectorOutbox()[queued.outboxId]
|
|
122
|
+
await outbox.runConnectorOutboxNow({ now: afterFirst.sendAt + 1 })
|
|
123
|
+
const afterSecond = storage.loadConnectorOutbox()[queued.outboxId]
|
|
124
|
+
console.log(JSON.stringify({ afterFirst, afterSecond, sendCount }))
|
|
125
|
+
`, { prefix: 'swarmclaw-outbox-test-' })
|
|
126
|
+
|
|
127
|
+
assert.equal(output.afterFirst.status, 'pending')
|
|
128
|
+
assert.equal(output.afterFirst.attemptCount, 1)
|
|
129
|
+
assert.match(output.afterFirst.lastError, /temporary send failure/i)
|
|
130
|
+
assert.equal(output.afterSecond.status, 'sent')
|
|
131
|
+
assert.equal(output.afterSecond.attemptCount, 2)
|
|
132
|
+
assert.equal(output.afterSecond.lastMessageId, 'retry-msg-2')
|
|
133
|
+
assert.equal(output.sendCount, 2)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('dedupes and replaces scheduled follow-ups durably', () => {
|
|
137
|
+
const output = runWithTempDataDir(`
|
|
138
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
139
|
+
const managerMod = await import('./src/lib/server/connectors/manager')
|
|
140
|
+
const storage = storageMod.default || storageMod
|
|
141
|
+
const manager = managerMod.default || managerMod
|
|
142
|
+
|
|
143
|
+
const now = Date.now()
|
|
144
|
+
storage.saveSettings({})
|
|
145
|
+
storage.saveConnectors({
|
|
146
|
+
conn_1: {
|
|
147
|
+
id: 'conn_1',
|
|
148
|
+
name: 'Outbox Connector',
|
|
149
|
+
platform: 'whatsapp',
|
|
150
|
+
agentId: 'agent_1',
|
|
151
|
+
credentialId: null,
|
|
152
|
+
config: {},
|
|
153
|
+
isEnabled: true,
|
|
154
|
+
status: 'stopped',
|
|
155
|
+
createdAt: now,
|
|
156
|
+
updatedAt: now,
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const first = manager.scheduleConnectorFollowUp({
|
|
161
|
+
connectorId: 'conn_1',
|
|
162
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
163
|
+
text: 'First follow-up',
|
|
164
|
+
delaySec: 30,
|
|
165
|
+
dedupeKey: 'dup-1',
|
|
166
|
+
})
|
|
167
|
+
const second = manager.scheduleConnectorFollowUp({
|
|
168
|
+
connectorId: 'conn_1',
|
|
169
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
170
|
+
text: 'Second follow-up same dedupe',
|
|
171
|
+
delaySec: 30,
|
|
172
|
+
dedupeKey: 'dup-1',
|
|
173
|
+
})
|
|
174
|
+
const third = manager.scheduleConnectorFollowUp({
|
|
175
|
+
connectorId: 'conn_1',
|
|
176
|
+
channelId: '15550001111@s.whatsapp.net',
|
|
177
|
+
text: 'Replacement follow-up',
|
|
178
|
+
delaySec: 30,
|
|
179
|
+
dedupeKey: 'dup-1',
|
|
180
|
+
replaceExisting: true,
|
|
181
|
+
})
|
|
182
|
+
const outbox = storage.loadConnectorOutbox()
|
|
183
|
+
console.log(JSON.stringify({ first, second, third, outbox }))
|
|
184
|
+
`, { prefix: 'swarmclaw-outbox-test-' })
|
|
185
|
+
|
|
186
|
+
assert.equal(output.first.followUpId, output.second.followUpId)
|
|
187
|
+
assert.notEqual(output.third.followUpId, output.first.followUpId)
|
|
188
|
+
assert.equal(output.outbox[output.first.followUpId].status, 'cancelled')
|
|
189
|
+
assert.equal(output.outbox[output.third.followUpId].status, 'pending')
|
|
190
|
+
assert.equal(output.outbox[output.third.followUpId].text, 'Replacement follow-up')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import {
|
|
3
|
+
loadConnectorOutbox,
|
|
4
|
+
patchStoredItem,
|
|
5
|
+
upsertConnectorOutboxItem,
|
|
6
|
+
} from '../storage'
|
|
7
|
+
import { notify } from '../ws-hub'
|
|
8
|
+
import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export type ConnectorOutboxStatus =
|
|
12
|
+
| 'pending'
|
|
13
|
+
| 'processing'
|
|
14
|
+
| 'sent'
|
|
15
|
+
| 'suppressed'
|
|
16
|
+
| 'failed'
|
|
17
|
+
| 'cancelled'
|
|
18
|
+
|
|
19
|
+
export interface ConnectorOutboxEntry extends Record<string, unknown> {
|
|
20
|
+
id: string
|
|
21
|
+
status: ConnectorOutboxStatus
|
|
22
|
+
sendAt: number
|
|
23
|
+
createdAt: number
|
|
24
|
+
updatedAt: number
|
|
25
|
+
attemptCount: number
|
|
26
|
+
maxAttempts: number
|
|
27
|
+
/** Destination fields (set by enqueueConnectorOutbox) */
|
|
28
|
+
connectorId?: string
|
|
29
|
+
platform?: string
|
|
30
|
+
channelId?: string
|
|
31
|
+
text?: string
|
|
32
|
+
sessionId?: string | null
|
|
33
|
+
imageUrl?: string
|
|
34
|
+
fileUrl?: string
|
|
35
|
+
mediaPath?: string
|
|
36
|
+
mimeType?: string
|
|
37
|
+
fileName?: string
|
|
38
|
+
caption?: string
|
|
39
|
+
replyToMessageId?: string
|
|
40
|
+
threadId?: string
|
|
41
|
+
ptt?: boolean
|
|
42
|
+
dedupeKey?: string | null
|
|
43
|
+
lastError?: string | null
|
|
44
|
+
deliveredAt?: number | null
|
|
45
|
+
lastMessageId?: string | null
|
|
46
|
+
processingLeaseId?: string | null
|
|
47
|
+
processingStartedAt?: number | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const RETRY_BASE_MS = 5_000
|
|
51
|
+
const RETRY_MAX_MS = 5 * 60_000
|
|
52
|
+
const DEFAULT_MAX_ATTEMPTS = 6
|
|
53
|
+
const CLAIM_STALE_MS = 60_000
|
|
54
|
+
const MAX_BATCH_SIZE = 10
|
|
55
|
+
|
|
56
|
+
type OutboxState = {
|
|
57
|
+
timer: ReturnType<typeof setTimeout> | null
|
|
58
|
+
dueAt: number | null
|
|
59
|
+
running: boolean
|
|
60
|
+
pendingKick: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const outboxState: OutboxState = hmrSingleton<OutboxState>('__swarmclaw_connector_outbox_state__', () => ({
|
|
64
|
+
timer: null,
|
|
65
|
+
dueAt: null,
|
|
66
|
+
running: false,
|
|
67
|
+
pendingKick: false,
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
function normalizeEntry(value: unknown): ConnectorOutboxEntry | null {
|
|
71
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
72
|
+
const row = value as Record<string, unknown>
|
|
73
|
+
const id = typeof row.id === 'string' ? row.id.trim() : ''
|
|
74
|
+
const channelId = typeof row.channelId === 'string' ? row.channelId : ''
|
|
75
|
+
if (!id || !channelId) return null
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
channelId,
|
|
81
|
+
text: typeof row.text === 'string' ? row.text : '',
|
|
82
|
+
sessionId: typeof row.sessionId === 'string' ? row.sessionId : null,
|
|
83
|
+
imageUrl: typeof row.imageUrl === 'string' ? row.imageUrl : undefined,
|
|
84
|
+
fileUrl: typeof row.fileUrl === 'string' ? row.fileUrl : undefined,
|
|
85
|
+
mediaPath: typeof row.mediaPath === 'string' ? row.mediaPath : undefined,
|
|
86
|
+
mimeType: typeof row.mimeType === 'string' ? row.mimeType : undefined,
|
|
87
|
+
fileName: typeof row.fileName === 'string' ? row.fileName : undefined,
|
|
88
|
+
caption: typeof row.caption === 'string' ? row.caption : undefined,
|
|
89
|
+
replyToMessageId: typeof row.replyToMessageId === 'string' ? row.replyToMessageId : undefined,
|
|
90
|
+
threadId: typeof row.threadId === 'string' ? row.threadId : undefined,
|
|
91
|
+
ptt: row.ptt === true,
|
|
92
|
+
status: normalizeStatus(row.status),
|
|
93
|
+
sendAt: typeof row.sendAt === 'number' ? row.sendAt : 0,
|
|
94
|
+
createdAt: typeof row.createdAt === 'number' ? row.createdAt : 0,
|
|
95
|
+
updatedAt: typeof row.updatedAt === 'number' ? row.updatedAt : 0,
|
|
96
|
+
attemptCount: typeof row.attemptCount === 'number' ? row.attemptCount : 0,
|
|
97
|
+
maxAttempts: typeof row.maxAttempts === 'number' ? row.maxAttempts : DEFAULT_MAX_ATTEMPTS,
|
|
98
|
+
dedupeKey: typeof row.dedupeKey === 'string' ? row.dedupeKey : null,
|
|
99
|
+
lastError: typeof row.lastError === 'string' ? row.lastError : null,
|
|
100
|
+
deliveredAt: typeof row.deliveredAt === 'number' ? row.deliveredAt : null,
|
|
101
|
+
lastMessageId: typeof row.lastMessageId === 'string' ? row.lastMessageId : null,
|
|
102
|
+
processingLeaseId: typeof row.processingLeaseId === 'string' ? row.processingLeaseId : null,
|
|
103
|
+
processingStartedAt: typeof row.processingStartedAt === 'number' ? row.processingStartedAt : null,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeStatus(value: unknown): ConnectorOutboxStatus {
|
|
108
|
+
switch (value) {
|
|
109
|
+
case 'processing':
|
|
110
|
+
case 'sent':
|
|
111
|
+
case 'suppressed':
|
|
112
|
+
case 'failed':
|
|
113
|
+
case 'cancelled':
|
|
114
|
+
return value
|
|
115
|
+
default:
|
|
116
|
+
return 'pending'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isTerminalStatus(status: ConnectorOutboxStatus): boolean {
|
|
121
|
+
return status === 'sent' || status === 'suppressed' || status === 'failed' || status === 'cancelled'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isClaimEligible(entry: ConnectorOutboxEntry, now: number): boolean {
|
|
125
|
+
if (entry.status === 'pending') return entry.sendAt <= now
|
|
126
|
+
if (entry.status !== 'processing') return false
|
|
127
|
+
const startedAt = entry.processingStartedAt || entry.updatedAt || entry.sendAt || 0
|
|
128
|
+
return now - startedAt >= CLAIM_STALE_MS
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function listEntries(): ConnectorOutboxEntry[] {
|
|
132
|
+
return Object.values(loadConnectorOutbox())
|
|
133
|
+
.map((value) => normalizeEntry(value))
|
|
134
|
+
.filter((value): value is ConnectorOutboxEntry => !!value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function nextRetryAt(now: number, attemptCount: number): number {
|
|
138
|
+
const backoff = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * (2 ** Math.max(0, attemptCount - 1)))
|
|
139
|
+
return now + backoff
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function scheduleTimer(delayMs: number): void {
|
|
143
|
+
const nextDueAt = Date.now() + Math.max(0, delayMs)
|
|
144
|
+
if (outboxState.timer && outboxState.dueAt !== null && outboxState.dueAt <= nextDueAt) return
|
|
145
|
+
if (outboxState.timer) {
|
|
146
|
+
clearTimeout(outboxState.timer)
|
|
147
|
+
outboxState.timer = null
|
|
148
|
+
}
|
|
149
|
+
outboxState.dueAt = nextDueAt
|
|
150
|
+
outboxState.timer = setTimeout(() => {
|
|
151
|
+
outboxState.timer = null
|
|
152
|
+
outboxState.dueAt = null
|
|
153
|
+
void runConnectorOutboxNow().catch((err: unknown) => {
|
|
154
|
+
console.warn(`[connector-outbox] Worker tick failed: ${errorMessage(err)}`)
|
|
155
|
+
})
|
|
156
|
+
}, Math.max(0, delayMs))
|
|
157
|
+
outboxState.timer.unref?.()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rescheduleFromStorage(now = Date.now()): void {
|
|
161
|
+
const active = listEntries()
|
|
162
|
+
.filter((entry) => !isTerminalStatus(entry.status))
|
|
163
|
+
.sort((a, b) => a.sendAt - b.sendAt || a.createdAt - b.createdAt)
|
|
164
|
+
if (!active.length) {
|
|
165
|
+
if (outboxState.timer) {
|
|
166
|
+
clearTimeout(outboxState.timer)
|
|
167
|
+
outboxState.timer = null
|
|
168
|
+
}
|
|
169
|
+
outboxState.dueAt = null
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const nextDue = active[0].sendAt
|
|
173
|
+
scheduleTimer(Math.max(0, nextDue - now))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function claimEntry(id: string, now: number): ConnectorOutboxEntry | null {
|
|
177
|
+
const leaseId = `${now}:${Math.random().toString(16).slice(2, 10)}`
|
|
178
|
+
const claimed = patchStoredItem<ConnectorOutboxEntry>('connector_outbox', id, (current) => {
|
|
179
|
+
const entry = normalizeEntry(current)
|
|
180
|
+
if (!entry) return current
|
|
181
|
+
if (!isClaimEligible(entry, now)) return entry
|
|
182
|
+
return {
|
|
183
|
+
...entry,
|
|
184
|
+
status: 'processing',
|
|
185
|
+
processingLeaseId: leaseId,
|
|
186
|
+
processingStartedAt: now,
|
|
187
|
+
updatedAt: now,
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
const normalized = normalizeEntry(claimed)
|
|
191
|
+
if (!normalized) return null
|
|
192
|
+
if (normalized.status !== 'processing' || normalized.processingLeaseId !== leaseId) return null
|
|
193
|
+
return normalized
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function processEntry(id: string, now: number): Promise<ConnectorOutboxEntry | null> {
|
|
197
|
+
const claimed = claimEntry(id, now)
|
|
198
|
+
if (!claimed) return null
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const { sendConnectorMessage } = await import('./manager')
|
|
202
|
+
// Outbox entries always have channelId+text from enqueueConnectorOutbox
|
|
203
|
+
const result = await sendConnectorMessage(claimed as ConnectorOutboxEntry & { channelId: string; text: string })
|
|
204
|
+
const deliveredAt = Date.now()
|
|
205
|
+
const next: ConnectorOutboxEntry = {
|
|
206
|
+
...claimed,
|
|
207
|
+
connectorId: result.connectorId,
|
|
208
|
+
platform: result.platform,
|
|
209
|
+
channelId: result.channelId,
|
|
210
|
+
status: result.suppressed ? 'suppressed' : 'sent',
|
|
211
|
+
attemptCount: claimed.attemptCount + 1,
|
|
212
|
+
updatedAt: deliveredAt,
|
|
213
|
+
deliveredAt,
|
|
214
|
+
lastMessageId: result.messageId || null,
|
|
215
|
+
lastError: null,
|
|
216
|
+
processingLeaseId: null,
|
|
217
|
+
processingStartedAt: null,
|
|
218
|
+
}
|
|
219
|
+
upsertConnectorOutboxItem(next.id, next)
|
|
220
|
+
notify('connector_outbox')
|
|
221
|
+
return next
|
|
222
|
+
} catch (err: unknown) {
|
|
223
|
+
const failedAt = Date.now()
|
|
224
|
+
const nextAttemptCount = claimed.attemptCount + 1
|
|
225
|
+
const permanent = nextAttemptCount >= claimed.maxAttempts
|
|
226
|
+
const next: ConnectorOutboxEntry = {
|
|
227
|
+
...claimed,
|
|
228
|
+
status: permanent ? 'failed' : 'pending',
|
|
229
|
+
attemptCount: nextAttemptCount,
|
|
230
|
+
updatedAt: failedAt,
|
|
231
|
+
sendAt: permanent ? claimed.sendAt : nextRetryAt(failedAt, nextAttemptCount),
|
|
232
|
+
lastError: errorMessage(err),
|
|
233
|
+
processingLeaseId: null,
|
|
234
|
+
processingStartedAt: null,
|
|
235
|
+
}
|
|
236
|
+
upsertConnectorOutboxItem(next.id, next)
|
|
237
|
+
notify('connector_outbox')
|
|
238
|
+
return next
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function enqueueConnectorOutbox(
|
|
243
|
+
input: Record<string, unknown> & {
|
|
244
|
+
sendAt?: number
|
|
245
|
+
maxAttempts?: number
|
|
246
|
+
dedupeKey?: string | null
|
|
247
|
+
},
|
|
248
|
+
options?: { replaceExisting?: boolean },
|
|
249
|
+
): { outboxId: string; sendAt: number } {
|
|
250
|
+
const now = Date.now()
|
|
251
|
+
const requestedSendAt = typeof input.sendAt === 'number' ? input.sendAt : now
|
|
252
|
+
const sendAt = Math.max(now, requestedSendAt)
|
|
253
|
+
const dedupeKey = input.dedupeKey?.trim() || null
|
|
254
|
+
|
|
255
|
+
if (dedupeKey) {
|
|
256
|
+
const existing = findPendingConnectorOutboxByDedupe(dedupeKey, now)
|
|
257
|
+
if (existing && existing.sendAt > now && !options?.replaceExisting) {
|
|
258
|
+
return { outboxId: existing.id, sendAt: existing.sendAt }
|
|
259
|
+
}
|
|
260
|
+
if (existing && options?.replaceExisting) {
|
|
261
|
+
patchStoredItem<ConnectorOutboxEntry>('connector_outbox', existing.id, (current) => {
|
|
262
|
+
const entry = normalizeEntry(current)
|
|
263
|
+
if (!entry || isTerminalStatus(entry.status)) return entry
|
|
264
|
+
return {
|
|
265
|
+
...entry,
|
|
266
|
+
status: 'cancelled',
|
|
267
|
+
updatedAt: now,
|
|
268
|
+
processingLeaseId: null,
|
|
269
|
+
processingStartedAt: null,
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const entry: ConnectorOutboxEntry = {
|
|
276
|
+
...input,
|
|
277
|
+
id: genId(),
|
|
278
|
+
status: 'pending',
|
|
279
|
+
sendAt,
|
|
280
|
+
createdAt: now,
|
|
281
|
+
updatedAt: now,
|
|
282
|
+
attemptCount: 0,
|
|
283
|
+
maxAttempts: Math.max(1, Math.trunc(input.maxAttempts || DEFAULT_MAX_ATTEMPTS)),
|
|
284
|
+
dedupeKey,
|
|
285
|
+
lastError: null,
|
|
286
|
+
deliveredAt: null,
|
|
287
|
+
lastMessageId: null,
|
|
288
|
+
processingLeaseId: null,
|
|
289
|
+
processingStartedAt: null,
|
|
290
|
+
}
|
|
291
|
+
upsertConnectorOutboxItem(entry.id, entry)
|
|
292
|
+
notify('connector_outbox')
|
|
293
|
+
rescheduleFromStorage(now)
|
|
294
|
+
return { outboxId: entry.id, sendAt: entry.sendAt }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function findPendingConnectorOutboxByDedupe(dedupeKey: string, now = Date.now()): ConnectorOutboxEntry | null {
|
|
298
|
+
const normalizedKey = dedupeKey.trim()
|
|
299
|
+
if (!normalizedKey) return null
|
|
300
|
+
return listEntries()
|
|
301
|
+
.filter((entry) =>
|
|
302
|
+
entry.dedupeKey === normalizedKey
|
|
303
|
+
&& !isTerminalStatus(entry.status)
|
|
304
|
+
&& (entry.sendAt > now || entry.status === 'processing'),
|
|
305
|
+
)
|
|
306
|
+
.sort((a, b) => a.sendAt - b.sendAt || a.createdAt - b.createdAt)[0] || null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function runConnectorOutboxNow(options?: {
|
|
310
|
+
now?: number
|
|
311
|
+
maxItems?: number
|
|
312
|
+
onlyIds?: string[]
|
|
313
|
+
}): Promise<ConnectorOutboxEntry[]> {
|
|
314
|
+
if (outboxState.running) {
|
|
315
|
+
outboxState.pendingKick = true
|
|
316
|
+
return []
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
outboxState.running = true
|
|
320
|
+
const now = options?.now ?? Date.now()
|
|
321
|
+
try {
|
|
322
|
+
const onlyIds = new Set((options?.onlyIds || []).filter(Boolean))
|
|
323
|
+
const candidates = listEntries()
|
|
324
|
+
.filter((entry) => (onlyIds.size === 0 || onlyIds.has(entry.id)) && isClaimEligible(entry, now))
|
|
325
|
+
.sort((a, b) => a.sendAt - b.sendAt || a.createdAt - b.createdAt)
|
|
326
|
+
.slice(0, Math.max(1, options?.maxItems || MAX_BATCH_SIZE))
|
|
327
|
+
|
|
328
|
+
const processed: ConnectorOutboxEntry[] = []
|
|
329
|
+
for (const entry of candidates) {
|
|
330
|
+
const next = await processEntry(entry.id, now)
|
|
331
|
+
if (next) processed.push(next)
|
|
332
|
+
}
|
|
333
|
+
return processed
|
|
334
|
+
} finally {
|
|
335
|
+
outboxState.running = false
|
|
336
|
+
if (outboxState.pendingKick) {
|
|
337
|
+
outboxState.pendingKick = false
|
|
338
|
+
scheduleTimer(0)
|
|
339
|
+
} else {
|
|
340
|
+
rescheduleFromStorage()
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function startConnectorOutboxWorker(): void {
|
|
346
|
+
rescheduleFromStorage()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function stopConnectorOutboxWorker(): void {
|
|
350
|
+
if (outboxState.timer) {
|
|
351
|
+
clearTimeout(outboxState.timer)
|
|
352
|
+
outboxState.timer = null
|
|
353
|
+
}
|
|
354
|
+
outboxState.dueAt = null
|
|
355
|
+
outboxState.pendingKick = false
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function getConnectorOutboxStatus(): {
|
|
359
|
+
queued: number
|
|
360
|
+
running: boolean
|
|
361
|
+
nextDueAt: number | null
|
|
362
|
+
} {
|
|
363
|
+
const queued = listEntries().filter((entry) => !isTerminalStatus(entry.status)).length
|
|
364
|
+
return {
|
|
365
|
+
queued,
|
|
366
|
+
running: outboxState.running,
|
|
367
|
+
nextDueAt: outboxState.dueAt,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
approvePairingCode,
|
|
9
9
|
clearConnectorPairingState,
|
|
10
10
|
createOrTouchPairingRequest,
|
|
11
|
+
getWhatsAppApprovedSenderIds,
|
|
11
12
|
isSenderAllowed,
|
|
12
13
|
listPendingPairingRequests,
|
|
13
14
|
listStoredAllowedSenders,
|
|
15
|
+
normalizeWhatsAppApprovedContacts,
|
|
14
16
|
parseAllowFromCsv,
|
|
15
17
|
parsePairingPolicy,
|
|
16
|
-
} from './pairing
|
|
18
|
+
} from './pairing'
|
|
17
19
|
|
|
18
20
|
function withTempDataDir<T>(fn: (dir: string) => T): T {
|
|
19
21
|
const original = process.env.DATA_DIR
|
|
@@ -97,3 +99,18 @@ test('addAllowedSender deduplicates and normalizes sender ids', () => {
|
|
|
97
99
|
assert.deepEqual(listStoredAllowedSenders(connectorId), ['test@example.com'])
|
|
98
100
|
})
|
|
99
101
|
})
|
|
102
|
+
|
|
103
|
+
test('normalizeWhatsAppApprovedContacts trims entries and deduplicates equivalent phone identifiers', () => {
|
|
104
|
+
const contacts = normalizeWhatsAppApprovedContacts([
|
|
105
|
+
{ id: 'one', label: ' Alice ', phone: ' +1 (555) 000-1111 ' },
|
|
106
|
+
{ id: 'two', label: 'Alice JID', phone: '15550001111@s.whatsapp.net' },
|
|
107
|
+
{ id: '', label: '', phone: '16660002222@s.whatsapp.net' },
|
|
108
|
+
{ id: 'empty', label: 'Ignored', phone: ' ' },
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(contacts, [
|
|
112
|
+
{ id: 'one', label: 'Alice', phone: '+1 (555) 000-1111' },
|
|
113
|
+
{ id: 'wa-contact-2', label: '16660002222@s.whatsapp.net', phone: '16660002222@s.whatsapp.net' },
|
|
114
|
+
])
|
|
115
|
+
assert.deepEqual(getWhatsAppApprovedSenderIds(contacts), ['+1 (555) 000-1111', '16660002222@s.whatsapp.net'])
|
|
116
|
+
})
|