@swarmclawai/swarmclaw 0.7.7 → 0.8.0
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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -39,6 +39,23 @@ function normalizeSenderId(value: string): string {
|
|
|
39
39
|
return value.trim().toLowerCase()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function senderIdVariants(value: string): string[] {
|
|
43
|
+
const normalized = normalizeSenderId(value)
|
|
44
|
+
if (!normalized) return []
|
|
45
|
+
|
|
46
|
+
const variants = new Set<string>([normalized])
|
|
47
|
+
const jidUser = normalized.split('@')[0]?.split(':')[0]?.trim()
|
|
48
|
+
if (jidUser) variants.add(jidUser)
|
|
49
|
+
|
|
50
|
+
const digits = normalized.replace(/[^\d]/g, '')
|
|
51
|
+
if (digits) {
|
|
52
|
+
variants.add(digits)
|
|
53
|
+
variants.add(`${digits}@s.whatsapp.net`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [...variants]
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
function dedupe(items: string[]): string[] {
|
|
43
60
|
const seen = new Set<string>()
|
|
44
61
|
const out: string[] = []
|
|
@@ -237,15 +254,19 @@ export function isSenderAllowed(params: {
|
|
|
237
254
|
senderId: string
|
|
238
255
|
configAllowFrom?: string[]
|
|
239
256
|
}): boolean {
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
257
|
+
const senderVariants = new Set(senderIdVariants(params.senderId))
|
|
258
|
+
if (senderVariants.size === 0) return false
|
|
242
259
|
|
|
243
|
-
const
|
|
244
|
-
|
|
260
|
+
const configMatches = (params.configAllowFrom || []).some((item) =>
|
|
261
|
+
senderIdVariants(item).some((variant) => senderVariants.has(variant)),
|
|
262
|
+
)
|
|
263
|
+
if (configMatches) return true
|
|
245
264
|
|
|
246
265
|
const store = loadStore()
|
|
247
266
|
const state = ensureConnectorState(store, params.connectorId)
|
|
248
|
-
return state.allowedSenderIds.
|
|
267
|
+
return state.allowedSenderIds.some((item) =>
|
|
268
|
+
senderIdVariants(item).some((variant) => senderVariants.has(variant)),
|
|
269
|
+
)
|
|
249
270
|
}
|
|
250
271
|
|
|
251
272
|
export function clearConnectorPairingState(connectorId: string): void {
|
|
@@ -24,8 +24,10 @@ export interface InboundMedia {
|
|
|
24
24
|
export interface InboundMessage {
|
|
25
25
|
platform: string
|
|
26
26
|
channelId: string // platform-specific channel/chat ID
|
|
27
|
+
channelIdAlt?: string
|
|
27
28
|
channelName?: string // human-readable name
|
|
28
29
|
senderId: string // platform-specific user ID
|
|
30
|
+
senderIdAlt?: string
|
|
29
31
|
senderName: string // display name
|
|
30
32
|
text: string
|
|
31
33
|
isGroup?: boolean
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import {
|
|
4
|
+
buildWhatsAppTextPayloads,
|
|
5
|
+
buildWhatsAppInboundMessage,
|
|
6
|
+
isWhatsAppInboundAllowed,
|
|
7
|
+
normalizeWhatsAppAudioForSend,
|
|
8
|
+
normalizeWhatsAppIdentifier,
|
|
9
|
+
} from './whatsapp'
|
|
10
|
+
|
|
11
|
+
test('buildWhatsAppTextPayloads disables link previews for text sends', () => {
|
|
12
|
+
const payloads = buildWhatsAppTextPayloads('See https://example.com for details')
|
|
13
|
+
|
|
14
|
+
assert.deepEqual(payloads, [
|
|
15
|
+
{ text: 'See https://example.com for details', linkPreview: null },
|
|
16
|
+
])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('buildWhatsAppTextPayloads chunks long messages and disables previews for each chunk', () => {
|
|
20
|
+
const payloads = buildWhatsAppTextPayloads('x'.repeat(4500))
|
|
21
|
+
|
|
22
|
+
assert.equal(payloads.length, 2)
|
|
23
|
+
assert.equal(payloads[0].text.length, 4000)
|
|
24
|
+
assert.equal(payloads[1].text.length, 500)
|
|
25
|
+
assert.equal(payloads[0].linkPreview, null)
|
|
26
|
+
assert.equal(payloads[1].linkPreview, null)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('normalizeWhatsAppIdentifier strips jid wrappers and device suffixes', () => {
|
|
30
|
+
assert.equal(normalizeWhatsAppIdentifier('+1 (555) 000-1111@s.whatsapp.net'), '15550001111')
|
|
31
|
+
assert.equal(normalizeWhatsAppIdentifier('15550001111:7@s.whatsapp.net'), '15550001111')
|
|
32
|
+
assert.equal(normalizeWhatsAppIdentifier('199900000001@lid'), '199900000001')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('isWhatsAppInboundAllowed matches allow-list entries against alt phone JIDs', () => {
|
|
36
|
+
const allowed = ['15550001111']
|
|
37
|
+
const msg = {
|
|
38
|
+
key: {
|
|
39
|
+
remoteJid: '199900000001@lid',
|
|
40
|
+
remoteJidAlt: '15550001111@s.whatsapp.net',
|
|
41
|
+
},
|
|
42
|
+
} as any
|
|
43
|
+
|
|
44
|
+
assert.equal(isWhatsAppInboundAllowed({ allowedJids: allowed, msg }), true)
|
|
45
|
+
assert.equal(isWhatsAppInboundAllowed({ allowedJids: ['15559990000'], msg }), false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('buildWhatsAppInboundMessage includes modern WhatsApp metadata', () => {
|
|
49
|
+
const inbound = buildWhatsAppInboundMessage({
|
|
50
|
+
msg: {
|
|
51
|
+
key: {
|
|
52
|
+
remoteJid: '199900000001@lid',
|
|
53
|
+
remoteJidAlt: '15550001111@s.whatsapp.net',
|
|
54
|
+
id: 'wamid-1',
|
|
55
|
+
},
|
|
56
|
+
pushName: 'Alice',
|
|
57
|
+
message: {
|
|
58
|
+
extendedTextMessage: {
|
|
59
|
+
text: 'Hey there',
|
|
60
|
+
contextInfo: {
|
|
61
|
+
stanzaId: 'quoted-1',
|
|
62
|
+
mentionedJid: ['bot@s.whatsapp.net'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
} as any,
|
|
67
|
+
selfJids: ['bot@s.whatsapp.net'],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
assert.ok(inbound)
|
|
71
|
+
assert.equal(inbound?.channelId, '199900000001@lid')
|
|
72
|
+
assert.equal(inbound?.channelIdAlt, '15550001111@s.whatsapp.net')
|
|
73
|
+
assert.equal(inbound?.senderId, '199900000001@lid')
|
|
74
|
+
assert.equal(inbound?.senderIdAlt, '15550001111@s.whatsapp.net')
|
|
75
|
+
assert.equal(inbound?.messageId, 'wamid-1')
|
|
76
|
+
assert.equal(inbound?.replyToMessageId, 'quoted-1')
|
|
77
|
+
assert.equal(inbound?.mentionsBot, true)
|
|
78
|
+
assert.equal(inbound?.isGroup, false)
|
|
79
|
+
assert.equal(inbound?.text, 'Hey there')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('normalizeWhatsAppAudioForSend transcodes mp3 voice notes to Android-safe opus/ogg', () => {
|
|
83
|
+
let transcodeCalls = 0
|
|
84
|
+
const converted = normalizeWhatsAppAudioForSend({
|
|
85
|
+
buffer: Buffer.from('mp3-audio'),
|
|
86
|
+
mimeType: 'audio/mpeg',
|
|
87
|
+
fileName: 'voice-note.mp3',
|
|
88
|
+
ptt: true,
|
|
89
|
+
transcode: ({ buffer, mimeType, fileName }) => {
|
|
90
|
+
transcodeCalls += 1
|
|
91
|
+
assert.equal(buffer.toString(), 'mp3-audio')
|
|
92
|
+
assert.equal(mimeType, 'audio/mpeg')
|
|
93
|
+
assert.equal(fileName, 'voice-note.mp3')
|
|
94
|
+
return {
|
|
95
|
+
buffer: Buffer.from('ogg-opus-audio'),
|
|
96
|
+
mimeType: 'audio/ogg; codecs=opus',
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
assert.equal(transcodeCalls, 1)
|
|
102
|
+
assert.equal(converted.buffer.toString(), 'ogg-opus-audio')
|
|
103
|
+
assert.equal(converted.mimeType, 'audio/ogg; codecs=opus')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('normalizeWhatsAppAudioForSend keeps existing ogg voice notes unchanged', () => {
|
|
107
|
+
const converted = normalizeWhatsAppAudioForSend({
|
|
108
|
+
buffer: Buffer.from('already-ogg'),
|
|
109
|
+
mimeType: 'audio/ogg',
|
|
110
|
+
fileName: 'voice-note.ogg',
|
|
111
|
+
ptt: true,
|
|
112
|
+
transcode: () => {
|
|
113
|
+
throw new Error('transcode should not be called')
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
assert.equal(converted.buffer.toString(), 'already-ogg')
|
|
118
|
+
assert.equal(converted.mimeType, 'audio/ogg; codecs=opus')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('normalizeWhatsAppAudioForSend leaves normal audio attachments alone when ptt is disabled', () => {
|
|
122
|
+
const converted = normalizeWhatsAppAudioForSend({
|
|
123
|
+
buffer: Buffer.from('music'),
|
|
124
|
+
mimeType: 'audio/mpeg',
|
|
125
|
+
fileName: 'music.mp3',
|
|
126
|
+
ptt: false,
|
|
127
|
+
transcode: () => {
|
|
128
|
+
throw new Error('transcode should not be called')
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
assert.equal(converted.buffer.toString(), 'music')
|
|
133
|
+
assert.equal(converted.mimeType, 'audio/mpeg')
|
|
134
|
+
})
|
|
@@ -4,31 +4,264 @@ import makeWASocket, {
|
|
|
4
4
|
fetchLatestBaileysVersion,
|
|
5
5
|
normalizeMessageContent,
|
|
6
6
|
downloadMediaMessage,
|
|
7
|
+
type WAMessage,
|
|
7
8
|
} from '@whiskeysockets/baileys'
|
|
8
9
|
import QRCode from 'qrcode'
|
|
9
10
|
import path from 'path'
|
|
10
11
|
import fs from 'fs'
|
|
12
|
+
import os from 'os'
|
|
13
|
+
import { spawnSync } from 'child_process'
|
|
11
14
|
import type { Connector } from '@/types'
|
|
12
15
|
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
13
16
|
import { saveInboundMediaBuffer, mimeFromPath, isImageMime, isAudioMime } from './media'
|
|
14
|
-
import { isNoMessage } from './manager'
|
|
17
|
+
import { isNoMessage, recordConnectorOutboundDelivery } from './manager'
|
|
15
18
|
import { formatTextForWhatsApp } from './whatsapp-text'
|
|
16
19
|
|
|
17
20
|
import { DATA_DIR } from '../data-dir'
|
|
21
|
+
import { loadConnectors } from '../storage'
|
|
18
22
|
|
|
19
23
|
const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth')
|
|
20
24
|
const INBOUND_DEDUPE_TTL_MS = 2 * 60 * 1000
|
|
25
|
+
const WHATSAPP_SINGLE_MESSAGE_MAX = 4096
|
|
26
|
+
const WHATSAPP_TEXT_CHUNK_MAX = 4000
|
|
27
|
+
const WHATSAPP_VOICE_NOTE_MIME = 'audio/ogg; codecs=opus'
|
|
28
|
+
const WHATSAPP_VOICE_NOTE_EXTS = new Set(['.ogg', '.opus'])
|
|
29
|
+
|
|
30
|
+
let cachedFfmpegBinary: string | null | undefined
|
|
31
|
+
|
|
32
|
+
export function buildWhatsAppTextPayloads(text: string): Array<{ text: string; linkPreview: null }> {
|
|
33
|
+
const chunks = text.length <= WHATSAPP_SINGLE_MESSAGE_MAX
|
|
34
|
+
? [text]
|
|
35
|
+
: (text.match(new RegExp(`[\\s\\S]{1,${WHATSAPP_TEXT_CHUNK_MAX}}`, 'g')) || [text])
|
|
36
|
+
return chunks.map((chunk) => ({ text: chunk, linkPreview: null }))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeMimeType(mimeType?: string): string {
|
|
40
|
+
return String(mimeType || '').toLowerCase().split(';')[0].trim()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function looksLikeWhatsAppVoiceNote(params: { mimeType?: string; fileName?: string }): boolean {
|
|
44
|
+
const mime = normalizeMimeType(params.mimeType)
|
|
45
|
+
if (mime === 'audio/ogg' || mime === 'audio/opus') return true
|
|
46
|
+
const ext = path.extname(String(params.fileName || '')).toLowerCase()
|
|
47
|
+
return WHATSAPP_VOICE_NOTE_EXTS.has(ext)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveAudioExt(params: { mimeType?: string; fileName?: string }): string {
|
|
51
|
+
const ext = path.extname(String(params.fileName || '')).toLowerCase()
|
|
52
|
+
if (ext) return ext
|
|
53
|
+
const mime = normalizeMimeType(params.mimeType)
|
|
54
|
+
if (mime === 'audio/mpeg' || mime === 'audio/mp3') return '.mp3'
|
|
55
|
+
if (mime === 'audio/wav' || mime === 'audio/x-wav') return '.wav'
|
|
56
|
+
if (mime === 'audio/mp4' || mime === 'audio/m4a' || mime === 'audio/x-m4a') return '.m4a'
|
|
57
|
+
if (mime === 'audio/ogg' || mime === 'audio/opus') return '.ogg'
|
|
58
|
+
return '.bin'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveFfmpegBinary(): string | null {
|
|
62
|
+
if (cachedFfmpegBinary !== undefined) return cachedFfmpegBinary
|
|
63
|
+
const candidates = ['ffmpeg', '/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg']
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
const probe = spawnSync(candidate, ['-version'], { encoding: 'utf-8', timeout: 2_000 })
|
|
66
|
+
if ((probe.status ?? 1) === 0) {
|
|
67
|
+
cachedFfmpegBinary = candidate
|
|
68
|
+
return candidate
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
cachedFfmpegBinary = null
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function transcodeToWhatsAppVoiceNote(params: {
|
|
76
|
+
buffer: Buffer
|
|
77
|
+
mimeType?: string
|
|
78
|
+
fileName?: string
|
|
79
|
+
}): { buffer: Buffer; mimeType: string } | null {
|
|
80
|
+
const ffmpeg = resolveFfmpegBinary()
|
|
81
|
+
if (!ffmpeg) return null
|
|
82
|
+
|
|
83
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-wa-voice-'))
|
|
84
|
+
const inputPath = path.join(tempDir, `input${resolveAudioExt(params)}`)
|
|
85
|
+
const outputPath = path.join(tempDir, 'voice-note.ogg')
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
fs.writeFileSync(inputPath, params.buffer)
|
|
89
|
+
const result = spawnSync(ffmpeg, [
|
|
90
|
+
'-y',
|
|
91
|
+
'-i', inputPath,
|
|
92
|
+
'-vn',
|
|
93
|
+
'-ac', '1',
|
|
94
|
+
'-ar', '48000',
|
|
95
|
+
'-c:a', 'libopus',
|
|
96
|
+
'-b:a', '32k',
|
|
97
|
+
'-vbr', 'on',
|
|
98
|
+
'-compression_level', '10',
|
|
99
|
+
'-application', 'voip',
|
|
100
|
+
'-f', 'ogg',
|
|
101
|
+
outputPath,
|
|
102
|
+
], {
|
|
103
|
+
encoding: 'utf-8',
|
|
104
|
+
timeout: 20_000,
|
|
105
|
+
})
|
|
106
|
+
if ((result.status ?? 1) !== 0 || !fs.existsSync(outputPath)) {
|
|
107
|
+
const stderr = (result.stderr || '').trim()
|
|
108
|
+
console.warn(`[whatsapp] Failed to transcode voice note to opus/ogg${stderr ? `: ${stderr}` : ''}`)
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
buffer: fs.readFileSync(outputPath),
|
|
113
|
+
mimeType: WHATSAPP_VOICE_NOTE_MIME,
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeWhatsAppAudioForSend(params: {
|
|
121
|
+
buffer: Buffer
|
|
122
|
+
mimeType?: string
|
|
123
|
+
fileName?: string
|
|
124
|
+
ptt?: boolean
|
|
125
|
+
transcode?: (params: { buffer: Buffer; mimeType?: string; fileName?: string }) => { buffer: Buffer; mimeType: string } | null
|
|
126
|
+
}): { buffer: Buffer; mimeType: string } {
|
|
127
|
+
const mimeType = params.mimeType || 'application/octet-stream'
|
|
128
|
+
if (params.ptt === false) return { buffer: params.buffer, mimeType }
|
|
129
|
+
if (looksLikeWhatsAppVoiceNote(params)) {
|
|
130
|
+
return {
|
|
131
|
+
buffer: params.buffer,
|
|
132
|
+
mimeType: normalizeMimeType(mimeType) === 'audio/ogg' ? WHATSAPP_VOICE_NOTE_MIME : mimeType,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const transcode = params.transcode || transcodeToWhatsAppVoiceNote
|
|
136
|
+
const converted = transcode({
|
|
137
|
+
buffer: params.buffer,
|
|
138
|
+
mimeType: params.mimeType,
|
|
139
|
+
fileName: params.fileName,
|
|
140
|
+
})
|
|
141
|
+
return converted || { buffer: params.buffer, mimeType }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function jidUserPart(raw: string): string {
|
|
145
|
+
const trimmed = String(raw || '').trim().toLowerCase()
|
|
146
|
+
if (!trimmed) return ''
|
|
147
|
+
const withoutServer = trimmed.includes('@') ? trimmed.split('@')[0] : trimmed
|
|
148
|
+
return withoutServer.split(':')[0]
|
|
149
|
+
}
|
|
21
150
|
|
|
22
|
-
/** Normalize a phone number
|
|
23
|
-
function
|
|
24
|
-
let n =
|
|
151
|
+
/** Normalize a phone number or JID user part for inbound matching */
|
|
152
|
+
export function normalizeWhatsAppIdentifier(raw: string): string {
|
|
153
|
+
let n = jidUserPart(raw).replace(/[\s\-()]/g, '')
|
|
25
154
|
// UK local: 07xxx → 447xxx
|
|
26
155
|
if (n.startsWith('0') && n.length >= 10) {
|
|
27
156
|
n = '44' + n.slice(1)
|
|
28
157
|
}
|
|
29
158
|
// Strip leading +
|
|
30
159
|
if (n.startsWith('+')) n = n.slice(1)
|
|
31
|
-
return n
|
|
160
|
+
return n.replace(/[^a-z0-9]/g, '')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseAllowedIdentifiers(raw: unknown): string[] | null {
|
|
164
|
+
if (typeof raw !== 'string') return null
|
|
165
|
+
const out = raw
|
|
166
|
+
.split(',')
|
|
167
|
+
.map((entry) => normalizeWhatsAppIdentifier(entry))
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
return out.length ? out : null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function messageContextInfo(content: any): any {
|
|
173
|
+
return content?.extendedTextMessage?.contextInfo
|
|
174
|
+
|| content?.imageMessage?.contextInfo
|
|
175
|
+
|| content?.videoMessage?.contextInfo
|
|
176
|
+
|| content?.documentMessage?.contextInfo
|
|
177
|
+
|| content?.audioMessage?.contextInfo
|
|
178
|
+
|| content?.stickerMessage?.contextInfo
|
|
179
|
+
|| null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function collectWhatsAppAddressCandidates(msg: Pick<WAMessage, 'key'>): string[] {
|
|
183
|
+
const key = msg?.key || {}
|
|
184
|
+
const raw = [
|
|
185
|
+
key.remoteJid,
|
|
186
|
+
key.remoteJidAlt,
|
|
187
|
+
key.participant,
|
|
188
|
+
key.participantAlt,
|
|
189
|
+
]
|
|
190
|
+
const normalized = raw
|
|
191
|
+
.map((entry) => normalizeWhatsAppIdentifier(String(entry || '')))
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
return Array.from(new Set(normalized))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isWhatsAppInboundAllowed(params: {
|
|
197
|
+
allowedJids: string[] | null
|
|
198
|
+
msg: Pick<WAMessage, 'key'>
|
|
199
|
+
isSelfChat?: boolean
|
|
200
|
+
}): boolean {
|
|
201
|
+
if (!params.allowedJids?.length || params.isSelfChat) return true
|
|
202
|
+
const candidates = collectWhatsAppAddressCandidates(params.msg)
|
|
203
|
+
return candidates.some((candidate) =>
|
|
204
|
+
params.allowedJids!.some((allowed) => candidate.includes(allowed) || allowed.includes(candidate)),
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function buildWhatsAppInboundMessage(params: {
|
|
209
|
+
msg: WAMessage
|
|
210
|
+
media?: NonNullable<InboundMessage['media']>
|
|
211
|
+
selfJids?: string[]
|
|
212
|
+
}): InboundMessage | null {
|
|
213
|
+
const { msg } = params
|
|
214
|
+
const media = Array.isArray(params.media) ? params.media : []
|
|
215
|
+
const jid = msg.key.remoteJid || ''
|
|
216
|
+
if (!jid) return null
|
|
217
|
+
|
|
218
|
+
const content: any = normalizeMessageContent(msg.message as any) || msg.message || {}
|
|
219
|
+
const text = content?.conversation
|
|
220
|
+
|| content?.extendedTextMessage?.text
|
|
221
|
+
|| content?.imageMessage?.caption
|
|
222
|
+
|| content?.videoMessage?.caption
|
|
223
|
+
|| content?.documentMessage?.caption
|
|
224
|
+
|| ''
|
|
225
|
+
if (!text && media.length === 0) return null
|
|
226
|
+
|
|
227
|
+
const isGroup = jid.endsWith('@g.us')
|
|
228
|
+
const channelIdAlt = typeof msg.key.remoteJidAlt === 'string' && msg.key.remoteJidAlt.trim()
|
|
229
|
+
? msg.key.remoteJidAlt.trim()
|
|
230
|
+
: undefined
|
|
231
|
+
const senderId = isGroup
|
|
232
|
+
? (msg.key.participant || jid)
|
|
233
|
+
: jid
|
|
234
|
+
const senderIdAlt = isGroup
|
|
235
|
+
? (msg.key.participantAlt || undefined)
|
|
236
|
+
: channelIdAlt
|
|
237
|
+
const senderName = msg.pushName || jidUserPart(senderIdAlt || senderId) || jidUserPart(jid)
|
|
238
|
+
const contextInfo = messageContextInfo(content)
|
|
239
|
+
const mentionedJids = Array.isArray(contextInfo?.mentionedJid)
|
|
240
|
+
? contextInfo.mentionedJid.map((entry: unknown) => String(entry || '')).filter(Boolean)
|
|
241
|
+
: []
|
|
242
|
+
const selfIds = Array.isArray(params.selfJids) ? params.selfJids.map((entry: unknown) => normalizeWhatsAppIdentifier(String(entry || ''))).filter(Boolean) : []
|
|
243
|
+
const mentionsBot = selfIds.length > 0
|
|
244
|
+
? mentionedJids.some((entry: string) => selfIds.includes(normalizeWhatsAppIdentifier(entry)))
|
|
245
|
+
: false
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
platform: 'whatsapp',
|
|
249
|
+
channelId: jid,
|
|
250
|
+
channelIdAlt,
|
|
251
|
+
channelName: isGroup ? (channelIdAlt || jid) : `DM:${senderName}`,
|
|
252
|
+
senderId,
|
|
253
|
+
senderIdAlt,
|
|
254
|
+
senderName,
|
|
255
|
+
text: text || '(media message)',
|
|
256
|
+
isGroup,
|
|
257
|
+
messageId: msg.key.id || undefined,
|
|
258
|
+
imageUrl: media.find((item) => item.type === 'image')?.url,
|
|
259
|
+
media,
|
|
260
|
+
replyToMessageId: typeof contextInfo?.stanzaId === 'string' && contextInfo.stanzaId.trim()
|
|
261
|
+
? contextInfo.stanzaId.trim()
|
|
262
|
+
: undefined,
|
|
263
|
+
mentionsBot,
|
|
264
|
+
}
|
|
32
265
|
}
|
|
33
266
|
|
|
34
267
|
/** Check if auth directory has saved credentials */
|
|
@@ -96,7 +329,17 @@ const whatsapp: PlatformConnector = {
|
|
|
96
329
|
sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
|
|
97
330
|
}
|
|
98
331
|
} else if (isAudioMime(mime)) {
|
|
99
|
-
|
|
332
|
+
const normalizedAudio = normalizeWhatsAppAudioForSend({
|
|
333
|
+
buffer: buf,
|
|
334
|
+
mimeType: mime,
|
|
335
|
+
fileName: fName,
|
|
336
|
+
ptt: options.ptt !== false,
|
|
337
|
+
})
|
|
338
|
+
sent = await sock.sendMessage(channelId, {
|
|
339
|
+
audio: normalizedAudio.buffer,
|
|
340
|
+
mimetype: normalizedAudio.mimeType,
|
|
341
|
+
ptt: options.ptt !== false,
|
|
342
|
+
})
|
|
100
343
|
} else {
|
|
101
344
|
sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
|
|
102
345
|
}
|
|
@@ -123,10 +366,9 @@ const whatsapp: PlatformConnector = {
|
|
|
123
366
|
}
|
|
124
367
|
|
|
125
368
|
const payload = normalizedText || normalizedCaption || ''
|
|
126
|
-
const chunks = payload.length <= 4096 ? [payload] : (payload.match(/[\s\S]{1,4000}/g) || [payload])
|
|
127
369
|
let lastMessageId: string | undefined
|
|
128
|
-
for (const chunk of
|
|
129
|
-
const sent = await sock.sendMessage(channelId,
|
|
370
|
+
for (const chunk of buildWhatsAppTextPayloads(payload)) {
|
|
371
|
+
const sent = await sock.sendMessage(channelId, chunk)
|
|
130
372
|
if (sent?.key?.id) {
|
|
131
373
|
lastMessageId = sent.key.id
|
|
132
374
|
sentMessageIds.add(sent.key.id)
|
|
@@ -142,18 +384,9 @@ const whatsapp: PlatformConnector = {
|
|
|
142
384
|
},
|
|
143
385
|
}
|
|
144
386
|
|
|
145
|
-
// Normalize allowed JIDs for matching
|
|
146
|
-
const allowedJids = connector.config.allowedJids
|
|
147
|
-
? connector.config.allowedJids.split(',').map((s) => normalizeNumber(s.trim())).filter(Boolean)
|
|
148
|
-
: null
|
|
149
|
-
|
|
150
387
|
// Track message IDs sent by the bot to avoid infinite loops in self-chat
|
|
151
388
|
const sentMessageIds = new Set<string>()
|
|
152
389
|
|
|
153
|
-
if (allowedJids) {
|
|
154
|
-
console.log(`[whatsapp] Allowed JIDs (normalized): ${allowedJids.join(', ')}`)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
390
|
const startSocket = () => {
|
|
158
391
|
// Close previous socket to prevent stale event handlers
|
|
159
392
|
if (sock) {
|
|
@@ -284,28 +517,22 @@ const whatsapp: PlatformConnector = {
|
|
|
284
517
|
if (msg.key.fromMe && !isSelfChat) continue
|
|
285
518
|
|
|
286
519
|
const jid = msg.key.remoteJid || ''
|
|
520
|
+
const latestConnector = (loadConnectors()[connector.id] as Connector | undefined) || connector
|
|
521
|
+
const allowedJids = parseAllowedIdentifiers(latestConnector.config?.allowedJids)
|
|
287
522
|
|
|
288
523
|
// Match allowed JIDs using normalized numbers
|
|
289
524
|
// Self-chat always passes the filter (it's the bot's own account)
|
|
290
|
-
if (allowedJids && !isSelfChat) {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
console.log(`[whatsapp] JID filter: jidNumber=${jidNumber}, allowedJids=${allowedJids.join(',')}, matched=${matched}`)
|
|
525
|
+
if (allowedJids?.length && !isSelfChat) {
|
|
526
|
+
const matched = isWhatsAppInboundAllowed({ allowedJids, msg, isSelfChat })
|
|
527
|
+
console.log(`[whatsapp] JID filter: candidates=${collectWhatsAppAddressCandidates(msg).join(',')}, allowedJids=${allowedJids.join(',')}, matched=${matched}`)
|
|
294
528
|
if (!matched) {
|
|
295
529
|
console.log(`[whatsapp] Skipping message from non-allowed JID: ${jid}`)
|
|
296
530
|
continue
|
|
297
531
|
}
|
|
298
532
|
}
|
|
299
533
|
|
|
300
|
-
const content: any = normalizeMessageContent(msg.message as any) || msg.message || {}
|
|
301
|
-
const text = content?.conversation
|
|
302
|
-
|| content?.extendedTextMessage?.text
|
|
303
|
-
|| content?.imageMessage?.caption
|
|
304
|
-
|| content?.videoMessage?.caption
|
|
305
|
-
|| content?.documentMessage?.caption
|
|
306
|
-
|| ''
|
|
307
|
-
|
|
308
534
|
const media: NonNullable<InboundMessage['media']> = []
|
|
535
|
+
const content: any = normalizeMessageContent(msg.message as any) || msg.message || {}
|
|
309
536
|
const mediaCandidate:
|
|
310
537
|
| { kind: 'image' | 'video' | 'audio' | 'document' | 'file'; payload: any }
|
|
311
538
|
| null =
|
|
@@ -342,23 +569,14 @@ const whatsapp: PlatformConnector = {
|
|
|
342
569
|
}
|
|
343
570
|
}
|
|
344
571
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
572
|
+
const selfJids = [
|
|
573
|
+
sock?.user?.id || '',
|
|
574
|
+
sock?.user?.lid || '',
|
|
575
|
+
].filter(Boolean)
|
|
576
|
+
const inbound = buildWhatsAppInboundMessage({ msg, media, selfJids })
|
|
577
|
+
if (!inbound) continue
|
|
351
578
|
|
|
352
|
-
|
|
353
|
-
platform: 'whatsapp',
|
|
354
|
-
channelId: jid,
|
|
355
|
-
channelName: isGroup ? jid : `DM:${senderName}`,
|
|
356
|
-
senderId: msg.key.participant || jid,
|
|
357
|
-
senderName,
|
|
358
|
-
text: text || '(media message)',
|
|
359
|
-
imageUrl: media.find((m) => m.type === 'image')?.url,
|
|
360
|
-
media,
|
|
361
|
-
}
|
|
579
|
+
console.log(`[whatsapp] Message from ${inbound.senderName} (${jid}): ${inbound.text.slice(0, 80)}`)
|
|
362
580
|
|
|
363
581
|
try {
|
|
364
582
|
await sock!.sendPresenceUpdate('composing', jid)
|
|
@@ -366,7 +584,13 @@ const whatsapp: PlatformConnector = {
|
|
|
366
584
|
await sock!.sendPresenceUpdate('paused', jid)
|
|
367
585
|
|
|
368
586
|
if (!isNoMessage(response)) {
|
|
369
|
-
await instance.sendMessage?.(jid, response)
|
|
587
|
+
const sent = await instance.sendMessage?.(jid, response)
|
|
588
|
+
await recordConnectorOutboundDelivery({
|
|
589
|
+
connectorId: connector.id,
|
|
590
|
+
inbound,
|
|
591
|
+
messageId: sent?.messageId,
|
|
592
|
+
state: 'sent',
|
|
593
|
+
})
|
|
370
594
|
}
|
|
371
595
|
} catch (err: any) {
|
|
372
596
|
console.error(`[whatsapp] Error handling message:`, err.message)
|
|
@@ -204,7 +204,7 @@ export function consolidateToMemory(
|
|
|
204
204
|
if (hasDecision || hasKeyFact || hasResult) {
|
|
205
205
|
// Create a concise summary (first 500 chars)
|
|
206
206
|
const summary = text.length > 500 ? text.slice(0, 500) + '...' : text
|
|
207
|
-
const category =
|
|
207
|
+
const category = 'working/scratch'
|
|
208
208
|
const title = `[auto-consolidated] ${text.slice(0, 60).replace(/\n/g, ' ')}`
|
|
209
209
|
|
|
210
210
|
db.add({
|
|
@@ -213,6 +213,11 @@ export function consolidateToMemory(
|
|
|
213
213
|
category,
|
|
214
214
|
title,
|
|
215
215
|
content: summary,
|
|
216
|
+
metadata: {
|
|
217
|
+
origin: 'auto-consolidated',
|
|
218
|
+
kind: hasDecision ? 'decision' : hasResult ? 'result' : 'note',
|
|
219
|
+
tier: 'working',
|
|
220
|
+
},
|
|
216
221
|
})
|
|
217
222
|
stored++
|
|
218
223
|
}
|