@swarmclawai/swarmclaw 0.4.0 → 0.5.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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundMedia } from './types'
|
|
3
|
+
import { isNoMessage } from './manager'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 10_000
|
|
6
|
+
const DEFAULT_WEBHOOK_PATH = '/api/connectors/{id}/webhook'
|
|
7
|
+
|
|
8
|
+
function getErrorMessage(err: unknown): string {
|
|
9
|
+
if (err instanceof Error && err.message) return err.message
|
|
10
|
+
return String(err)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
+
? value as Record<string, unknown>
|
|
16
|
+
: null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readString(record: Record<string, unknown> | null, key: string): string | undefined {
|
|
20
|
+
if (!record) return undefined
|
|
21
|
+
const value = record[key]
|
|
22
|
+
return typeof value === 'string' ? value : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readNumberLike(record: Record<string, unknown> | null, key: string): number | undefined {
|
|
26
|
+
if (!record) return undefined
|
|
27
|
+
const value = record[key]
|
|
28
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
29
|
+
if (typeof value === 'string' && value.trim()) {
|
|
30
|
+
const parsed = Number.parseFloat(value)
|
|
31
|
+
if (Number.isFinite(parsed)) return parsed
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readBoolean(record: Record<string, unknown> | null, key: string): boolean | undefined {
|
|
37
|
+
if (!record) return undefined
|
|
38
|
+
const value = record[key]
|
|
39
|
+
return typeof value === 'boolean' ? value : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractPayloadMessage(payload: Record<string, unknown>): Record<string, unknown> | null {
|
|
43
|
+
const parseRecord = (value: unknown): Record<string, unknown> | null => {
|
|
44
|
+
const record = asRecord(value)
|
|
45
|
+
if (record) return record
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
for (const item of value) {
|
|
48
|
+
const nested = parseRecord(item)
|
|
49
|
+
if (nested) return nested
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
if (typeof value !== 'string') return null
|
|
54
|
+
const trimmed = value.trim()
|
|
55
|
+
if (!trimmed) return null
|
|
56
|
+
try {
|
|
57
|
+
return parseRecord(JSON.parse(trimmed))
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const dataRaw = payload.data ?? payload.payload ?? payload.event
|
|
64
|
+
const data = parseRecord(dataRaw)
|
|
65
|
+
const messageRaw = payload.message ?? data?.message ?? data
|
|
66
|
+
return parseRecord(messageRaw)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeHandle(value: string): string {
|
|
70
|
+
return value.trim().toLowerCase()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractHandleFromChatGuid(chatGuid: string): string | null {
|
|
74
|
+
const parts = chatGuid.split(';')
|
|
75
|
+
if (parts.length < 3) return null
|
|
76
|
+
const handle = parts[2]?.trim()
|
|
77
|
+
return handle || null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveGroupFlagFromChatGuid(chatGuid?: string): boolean | undefined {
|
|
81
|
+
const guid = chatGuid?.trim()
|
|
82
|
+
if (!guid) return undefined
|
|
83
|
+
const parts = guid.split(';')
|
|
84
|
+
if (parts.length >= 3) {
|
|
85
|
+
if (parts[1] === '+') return true
|
|
86
|
+
if (parts[1] === '-') return false
|
|
87
|
+
}
|
|
88
|
+
if (guid.includes(';+;')) return true
|
|
89
|
+
if (guid.includes(';-;')) return false
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeAttachmentType(mimeType?: string): InboundMedia['type'] {
|
|
94
|
+
const mime = (mimeType || '').trim().toLowerCase()
|
|
95
|
+
if (mime.startsWith('image/')) return 'image'
|
|
96
|
+
if (mime.startsWith('video/')) return 'video'
|
|
97
|
+
if (mime.startsWith('audio/')) return 'audio'
|
|
98
|
+
if (mime.startsWith('application/')) return 'document'
|
|
99
|
+
return 'file'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeAttachments(message: Record<string, unknown>): InboundMedia[] {
|
|
103
|
+
const raw = message.attachments
|
|
104
|
+
if (!Array.isArray(raw)) return []
|
|
105
|
+
|
|
106
|
+
const output: InboundMedia[] = []
|
|
107
|
+
for (const item of raw) {
|
|
108
|
+
const record = asRecord(item)
|
|
109
|
+
if (!record) continue
|
|
110
|
+
|
|
111
|
+
const mimeType = readString(record, 'mimeType') || readString(record, 'mime_type')
|
|
112
|
+
const fileName = readString(record, 'transferName') || readString(record, 'transfer_name')
|
|
113
|
+
const sizeBytes = readNumberLike(record, 'totalBytes') || readNumberLike(record, 'total_bytes')
|
|
114
|
+
|
|
115
|
+
output.push({
|
|
116
|
+
type: normalizeAttachmentType(mimeType),
|
|
117
|
+
mimeType,
|
|
118
|
+
fileName,
|
|
119
|
+
sizeBytes,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return output
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseInboundMessage(payload: Record<string, unknown>): InboundMessage | null {
|
|
127
|
+
const eventType = readString(payload, 'type')?.trim().toLowerCase() || ''
|
|
128
|
+
if (eventType && !['new-message', 'created-message', 'message'].includes(eventType)) {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const message = extractPayloadMessage(payload)
|
|
133
|
+
if (!message) return null
|
|
134
|
+
|
|
135
|
+
const fromMe = readBoolean(message, 'isFromMe') ?? readBoolean(message, 'is_from_me') ?? false
|
|
136
|
+
if (fromMe) return null
|
|
137
|
+
|
|
138
|
+
const text = (
|
|
139
|
+
readString(message, 'text')
|
|
140
|
+
|| readString(message, 'body')
|
|
141
|
+
|| readString(message, 'subject')
|
|
142
|
+
|| ''
|
|
143
|
+
).trim()
|
|
144
|
+
|
|
145
|
+
const handle = asRecord(message.handle) || asRecord(message.sender)
|
|
146
|
+
const rawSenderId = (
|
|
147
|
+
readString(handle, 'address')
|
|
148
|
+
|| readString(handle, 'handle')
|
|
149
|
+
|| readString(handle, 'id')
|
|
150
|
+
|| readString(message, 'senderId')
|
|
151
|
+
|| readString(message, 'sender')
|
|
152
|
+
|| readString(message, 'from')
|
|
153
|
+
|| ''
|
|
154
|
+
).trim()
|
|
155
|
+
|
|
156
|
+
const chatGuid = (
|
|
157
|
+
readString(message, 'chatGuid')
|
|
158
|
+
|| readString(message, 'chat_guid')
|
|
159
|
+
|| ''
|
|
160
|
+
).trim()
|
|
161
|
+
|
|
162
|
+
const inferredSender = !rawSenderId && chatGuid ? (extractHandleFromChatGuid(chatGuid) || '') : ''
|
|
163
|
+
const senderId = normalizeHandle(rawSenderId || inferredSender)
|
|
164
|
+
if (!senderId) return null
|
|
165
|
+
|
|
166
|
+
const chatIdentifier = (
|
|
167
|
+
readString(message, 'chatIdentifier')
|
|
168
|
+
|| readString(message, 'chat_identifier')
|
|
169
|
+
|| ''
|
|
170
|
+
).trim()
|
|
171
|
+
const chatIdNum = readNumberLike(message, 'chatId') || readNumberLike(message, 'chat_id')
|
|
172
|
+
const chatId = chatGuid || chatIdentifier || (Number.isFinite(chatIdNum) ? String(chatIdNum) : senderId)
|
|
173
|
+
const channelName = (
|
|
174
|
+
readString(message, 'chatName')
|
|
175
|
+
|| readString(message, 'displayName')
|
|
176
|
+
|| chatId
|
|
177
|
+
).trim()
|
|
178
|
+
|
|
179
|
+
const senderName = (
|
|
180
|
+
readString(handle, 'displayName')
|
|
181
|
+
|| readString(handle, 'name')
|
|
182
|
+
|| readString(message, 'senderName')
|
|
183
|
+
|| senderId
|
|
184
|
+
).trim()
|
|
185
|
+
|
|
186
|
+
const media = normalizeAttachments(message)
|
|
187
|
+
const fallbackText = media.length > 0 ? '<media:attachment>' : ''
|
|
188
|
+
|
|
189
|
+
const groupFlag = (
|
|
190
|
+
readBoolean(message, 'isGroup')
|
|
191
|
+
?? readBoolean(message, 'is_group')
|
|
192
|
+
?? resolveGroupFlagFromChatGuid(chatGuid)
|
|
193
|
+
?? false
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
platform: 'bluebubbles',
|
|
198
|
+
channelId: chatId,
|
|
199
|
+
channelName,
|
|
200
|
+
senderId,
|
|
201
|
+
senderName,
|
|
202
|
+
text: text || fallbackText,
|
|
203
|
+
media,
|
|
204
|
+
isGroup: groupFlag,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveRequestUrl(baseUrl: string, path: string, password: string): string {
|
|
209
|
+
const base = new URL(baseUrl)
|
|
210
|
+
const url = new URL(path, base)
|
|
211
|
+
url.searchParams.set('password', password)
|
|
212
|
+
return url.toString()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function fetchJsonWithTimeout(
|
|
216
|
+
input: string,
|
|
217
|
+
init: RequestInit,
|
|
218
|
+
timeoutMs: number,
|
|
219
|
+
): Promise<Response> {
|
|
220
|
+
const controller = new AbortController()
|
|
221
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
222
|
+
try {
|
|
223
|
+
return await fetch(input, { ...init, signal: controller.signal })
|
|
224
|
+
} finally {
|
|
225
|
+
clearTimeout(timer)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseCsvList(raw: string | undefined): string[] {
|
|
230
|
+
if (!raw) return []
|
|
231
|
+
return raw.split(',').map((value) => value.trim()).filter(Boolean)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const bluebubbles: PlatformConnector = {
|
|
235
|
+
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
236
|
+
const serverUrl = connector.config.serverUrl?.trim()
|
|
237
|
+
const password = (botToken || connector.config.password || '').trim()
|
|
238
|
+
|
|
239
|
+
if (!serverUrl) throw new Error('Missing serverUrl in connector config')
|
|
240
|
+
if (!password) throw new Error('Missing BlueBubbles password (credential/token)')
|
|
241
|
+
|
|
242
|
+
const timeoutMsRaw = Number.parseInt(connector.config.timeoutMs || '', 10)
|
|
243
|
+
const timeoutMs = Number.isFinite(timeoutMsRaw)
|
|
244
|
+
? Math.max(1_000, Math.min(60_000, timeoutMsRaw))
|
|
245
|
+
: DEFAULT_TIMEOUT_MS
|
|
246
|
+
|
|
247
|
+
const allowedChats = new Set(parseCsvList(connector.config.chatIds))
|
|
248
|
+
|
|
249
|
+
let stopped = false
|
|
250
|
+
|
|
251
|
+
const processWebhookEvent = async (payload: Record<string, unknown>) => {
|
|
252
|
+
if (stopped) throw new Error('Connector is stopped')
|
|
253
|
+
const inbound = parseInboundMessage(payload)
|
|
254
|
+
if (!inbound) return {}
|
|
255
|
+
|
|
256
|
+
if (allowedChats.size > 0) {
|
|
257
|
+
const id = inbound.channelId
|
|
258
|
+
const name = inbound.channelName || ''
|
|
259
|
+
const allowed = Array.from(allowedChats).some((needle) => id.includes(needle) || name.includes(needle))
|
|
260
|
+
if (!allowed) return {}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const response = await onMessage(inbound)
|
|
264
|
+
if (!response || isNoMessage(response)) return {}
|
|
265
|
+
|
|
266
|
+
await sendBlueBubblesText({
|
|
267
|
+
serverUrl,
|
|
268
|
+
password,
|
|
269
|
+
channelId: inbound.channelId,
|
|
270
|
+
text: response,
|
|
271
|
+
timeoutMs,
|
|
272
|
+
})
|
|
273
|
+
return {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const handlerKey = `__swarmclaw_bluebubbles_handler_${connector.id}__`
|
|
277
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
278
|
+
;(globalThis as any)[handlerKey] = processWebhookEvent
|
|
279
|
+
|
|
280
|
+
const pingUrl = resolveRequestUrl(serverUrl, '/api/v1/ping', password)
|
|
281
|
+
const pingRes = await fetchJsonWithTimeout(pingUrl, { method: 'GET' }, timeoutMs)
|
|
282
|
+
if (!pingRes.ok) {
|
|
283
|
+
throw new Error(`BlueBubbles ping failed (${pingRes.status})`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(`[bluebubbles] Connected to ${serverUrl}`)
|
|
287
|
+
console.log(`[bluebubbles] Inbound webhook endpoint: ${DEFAULT_WEBHOOK_PATH.replace('{id}', connector.id)}`)
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
connector,
|
|
291
|
+
async sendMessage(channelId, text) {
|
|
292
|
+
if (stopped) throw new Error('Connector is stopped')
|
|
293
|
+
return await sendBlueBubblesText({
|
|
294
|
+
serverUrl,
|
|
295
|
+
password,
|
|
296
|
+
channelId,
|
|
297
|
+
text,
|
|
298
|
+
timeoutMs,
|
|
299
|
+
})
|
|
300
|
+
},
|
|
301
|
+
async stop() {
|
|
302
|
+
stopped = true
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
|
+
delete (globalThis as any)[handlerKey]
|
|
305
|
+
console.log(`[bluebubbles] Connector stopped`)
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function sendBlueBubblesText(params: {
|
|
312
|
+
serverUrl: string
|
|
313
|
+
password: string
|
|
314
|
+
channelId: string
|
|
315
|
+
text: string
|
|
316
|
+
timeoutMs: number
|
|
317
|
+
}): Promise<{ messageId?: string }> {
|
|
318
|
+
const message = params.text.trim()
|
|
319
|
+
if (!message) return {}
|
|
320
|
+
|
|
321
|
+
const channel = params.channelId.trim()
|
|
322
|
+
if (!channel) throw new Error('BlueBubbles send requires channelId')
|
|
323
|
+
|
|
324
|
+
const payload: Record<string, unknown> = {
|
|
325
|
+
message,
|
|
326
|
+
tempGuid: crypto.randomUUID(),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// For inbound-driven replies we store chat GUID in channelId. If callers pass a phone/email,
|
|
330
|
+
// BlueBubbles can still attempt routing via chatGuid field when it already matches.
|
|
331
|
+
payload.chatGuid = channel
|
|
332
|
+
|
|
333
|
+
const url = resolveRequestUrl(params.serverUrl, '/api/v1/message/text', params.password)
|
|
334
|
+
const res = await fetchJsonWithTimeout(url, {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
337
|
+
body: JSON.stringify(payload),
|
|
338
|
+
}, params.timeoutMs)
|
|
339
|
+
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
const errBody = await res.text().catch(() => '')
|
|
342
|
+
throw new Error(`BlueBubbles send failed (${res.status}): ${errBody || 'unknown'}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const body = await res.json() as Record<string, unknown>
|
|
347
|
+
const data = body?.data && typeof body.data === 'object' ? body.data as Record<string, unknown> : null
|
|
348
|
+
const id = data?.guid || body?.guid || data?.id || body?.id
|
|
349
|
+
return { messageId: typeof id === 'string' ? id : undefined }
|
|
350
|
+
} catch (err) {
|
|
351
|
+
// BlueBubbles may return empty body on success in some setups.
|
|
352
|
+
const message = getErrorMessage(err)
|
|
353
|
+
if (!message.toLowerCase().includes('json')) {
|
|
354
|
+
console.warn(`[bluebubbles] Unable to parse send response body: ${message}`)
|
|
355
|
+
}
|
|
356
|
+
return {}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default bluebubbles
|
|
@@ -9,7 +9,7 @@ import type { InboundMessage, InboundMedia } from './types.ts'
|
|
|
9
9
|
// 1. Connector module resolution (getPlatform)
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
describe('getPlatform — connector module resolution', () => {
|
|
12
|
-
const newPlatforms = ['matrix', 'googlechat', 'teams', 'signal'] as const
|
|
12
|
+
const newPlatforms = ['matrix', 'googlechat', 'teams', 'signal', 'bluebubbles'] as const
|
|
13
13
|
|
|
14
14
|
for (const name of newPlatforms) {
|
|
15
15
|
it(`returns a valid module for "${name}"`, async () => {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { PlatformConnector, ConnectorInstance } from './types'
|
|
1
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
2
|
+
import { isNoMessage } from './manager'
|
|
2
3
|
|
|
3
4
|
const googlechat: PlatformConnector = {
|
|
4
|
-
async start(connector, botToken,
|
|
5
|
+
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
5
6
|
const pkg = 'googleapis'
|
|
6
7
|
const { google } = await import(/* webpackIgnore: true */ pkg)
|
|
7
8
|
|
|
8
9
|
// Parse service account credentials from botToken
|
|
9
|
-
let credentials:
|
|
10
|
+
let credentials: Record<string, unknown>
|
|
10
11
|
try {
|
|
11
12
|
credentials = JSON.parse(botToken)
|
|
12
13
|
} catch {
|
|
@@ -25,17 +26,57 @@ const googlechat: PlatformConnector = {
|
|
|
25
26
|
? connector.config.spaceIds.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
26
27
|
: null
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
// This connector supports outbound messaging. For inbound messages, configure
|
|
30
|
-
// a webhook endpoint at /api/connectors/[id]/webhook that POSTs events here.
|
|
31
|
-
// Polling is not supported by the Google Chat API for bot messages.
|
|
29
|
+
const handlerKey = `__swarmclaw_googlechat_handler_${connector.id}__`
|
|
32
30
|
let stopped = false
|
|
33
31
|
|
|
34
32
|
console.log(`[googlechat] Bot authenticated via service account`)
|
|
35
33
|
if (allowedSpaces) {
|
|
36
34
|
console.log(`[googlechat] Filtering to spaces: ${allowedSpaces.join(', ')}`)
|
|
37
35
|
}
|
|
38
|
-
console.log(`[googlechat]
|
|
36
|
+
console.log(`[googlechat] Inbound webhook endpoint: /api/connectors/${connector.id}/webhook`)
|
|
37
|
+
|
|
38
|
+
function cleanInboundText(raw: unknown): string {
|
|
39
|
+
const txt = typeof raw === 'string' ? raw : ''
|
|
40
|
+
// Google Chat mentions often look like <users/123456789>
|
|
41
|
+
return txt.replace(/<users\/[^>]+>/g, '').trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function processWebhookEvent(event: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
45
|
+
if (stopped) throw new Error('Connector is stopped')
|
|
46
|
+
|
|
47
|
+
const msg = event?.message as Record<string, unknown> | undefined
|
|
48
|
+
if (!msg) return {}
|
|
49
|
+
|
|
50
|
+
const msgSpace = msg?.space as Record<string, unknown> | undefined
|
|
51
|
+
const eventSpace = event?.space as Record<string, unknown> | undefined
|
|
52
|
+
const spaceName: string = (msgSpace?.name as string) || (eventSpace?.name as string) || ''
|
|
53
|
+
if (allowedSpaces && !allowedSpaces.some((s) => spaceName.includes(s))) {
|
|
54
|
+
return {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rawText = (msg?.argumentText as string) || (msg?.text as string) || ''
|
|
58
|
+
const text = cleanInboundText(rawText)
|
|
59
|
+
if (!text) return {}
|
|
60
|
+
|
|
61
|
+
const sender = (msg?.sender || event?.user || {}) as Record<string, unknown>
|
|
62
|
+
const senderName = (sender?.displayName as string) || (sender?.name as string) || 'Google Chat User'
|
|
63
|
+
const senderId = (sender?.name as string) || ''
|
|
64
|
+
const inbound: InboundMessage = {
|
|
65
|
+
platform: 'googlechat',
|
|
66
|
+
channelId: spaceName || ((msg?.thread as Record<string, unknown>)?.name as string) || 'space:unknown',
|
|
67
|
+
channelName: (msgSpace?.displayName as string) || spaceName || 'Google Chat',
|
|
68
|
+
senderId,
|
|
69
|
+
senderName,
|
|
70
|
+
text,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const response = await onMessage(inbound)
|
|
74
|
+
if (!response || isNoMessage(response)) return {}
|
|
75
|
+
return { text: response }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
;(globalThis as any)[handlerKey] = processWebhookEvent
|
|
39
80
|
|
|
40
81
|
return {
|
|
41
82
|
connector,
|
|
@@ -57,6 +98,8 @@ const googlechat: PlatformConnector = {
|
|
|
57
98
|
},
|
|
58
99
|
async stop() {
|
|
59
100
|
stopped = true
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
delete (globalThis as any)[handlerKey]
|
|
60
103
|
console.log(`[googlechat] Bot disconnected`)
|
|
61
104
|
},
|
|
62
105
|
}
|