@swarmclawai/swarmclaw 0.4.0 → 0.4.5
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 +13 -2
- package/next.config.ts +8 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +20 -21
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +10 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -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/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -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]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +2 -2
- 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/page.tsx +3 -24
- package/src/cli/index.js +28 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +2 -0
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +116 -14
- package/src/components/chat/chat-area.tsx +27 -4
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/tool-call-bubble.tsx +9 -3
- 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 +122 -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/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 +73 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/hooks/use-continuous-speech.ts +144 -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/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 +11 -0
- 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 +38 -4
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +392 -3
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -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/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 +6 -6
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -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 +22 -10
- 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 +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- 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 +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +29 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.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 +28 -1
- package/src/stores/use-chat-store.ts +9 -1
- package/src/types/index.ts +27 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import {
|
|
3
3
|
loadConnectors, saveConnectors, loadSessions, saveSessions,
|
|
4
4
|
loadAgents, loadCredentials, decryptKey, loadSettings, loadSkills,
|
|
@@ -9,6 +9,17 @@ import { notify } from '../ws-hub'
|
|
|
9
9
|
import { logExecution } from '../execution-log'
|
|
10
10
|
import type { Connector } from '@/types'
|
|
11
11
|
import type { ConnectorInstance, InboundMessage, InboundMedia } from './types'
|
|
12
|
+
import {
|
|
13
|
+
addAllowedSender,
|
|
14
|
+
approvePairingCode,
|
|
15
|
+
createOrTouchPairingRequest,
|
|
16
|
+
isSenderAllowed,
|
|
17
|
+
listPendingPairingRequests,
|
|
18
|
+
listStoredAllowedSenders,
|
|
19
|
+
parseAllowFromCsv,
|
|
20
|
+
parsePairingPolicy,
|
|
21
|
+
type PairingPolicy,
|
|
22
|
+
} from './pairing'
|
|
12
23
|
|
|
13
24
|
/** Sentinel value agents return when no outbound reply should be sent */
|
|
14
25
|
export const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
|
|
@@ -43,6 +54,7 @@ export async function getPlatform(platform: string) {
|
|
|
43
54
|
case 'slack': return (await import('./slack')).default
|
|
44
55
|
case 'whatsapp': return (await import('./whatsapp')).default
|
|
45
56
|
case 'openclaw': return (await import('./openclaw')).default
|
|
57
|
+
case 'bluebubbles': return (await import('./bluebubbles')).default
|
|
46
58
|
case 'signal': return (await import('./signal')).default
|
|
47
59
|
case 'teams': return (await import('./teams')).default
|
|
48
60
|
case 'googlechat': return (await import('./googlechat')).default
|
|
@@ -78,6 +90,310 @@ export function formatInboundUserText(msg: InboundMessage): string {
|
|
|
78
90
|
return lines.join('\n').trim()
|
|
79
91
|
}
|
|
80
92
|
|
|
93
|
+
type ConnectorCommandName = 'help' | 'status' | 'new' | 'reset' | 'compact' | 'think' | 'pair'
|
|
94
|
+
|
|
95
|
+
interface ParsedConnectorCommand {
|
|
96
|
+
name: ConnectorCommandName
|
|
97
|
+
args: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
|
|
101
|
+
const trimmed = text.trim()
|
|
102
|
+
if (!trimmed.startsWith('/')) return null
|
|
103
|
+
const [head, ...rest] = trimmed.split(/\s+/)
|
|
104
|
+
const name = head.slice(1).toLowerCase()
|
|
105
|
+
const args = rest.join(' ').trim()
|
|
106
|
+
switch (name) {
|
|
107
|
+
case 'help':
|
|
108
|
+
case 'status':
|
|
109
|
+
case 'new':
|
|
110
|
+
case 'reset':
|
|
111
|
+
case 'compact':
|
|
112
|
+
case 'think':
|
|
113
|
+
case 'pair':
|
|
114
|
+
return { name, args } as ParsedConnectorCommand
|
|
115
|
+
default:
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pushSessionMessage(session: any, role: 'user' | 'assistant', text: string): void {
|
|
121
|
+
if (!text.trim()) return
|
|
122
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
123
|
+
session.messages.push({ role, text: text.trim(), time: Date.now() })
|
|
124
|
+
session.lastActiveAt = Date.now()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function persistSession(session: any): void {
|
|
128
|
+
const sessions = loadSessions()
|
|
129
|
+
sessions[session.id] = session
|
|
130
|
+
saveSessions(sessions)
|
|
131
|
+
notify(`messages:${session.id}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function summarizeForCompaction(messages: Array<{ role?: string; text?: string }>): string {
|
|
135
|
+
const preview = messages
|
|
136
|
+
.slice(-8)
|
|
137
|
+
.map((m, i) => {
|
|
138
|
+
const role = (m.role || 'unknown').toUpperCase()
|
|
139
|
+
const body = (m.text || '').replace(/\s+/g, ' ').trim()
|
|
140
|
+
const clipped = body.length > 180 ? `${body.slice(0, 177)}...` : body
|
|
141
|
+
return `${i + 1}. [${role}] ${clipped || '(no text)'}`
|
|
142
|
+
})
|
|
143
|
+
if (!preview.length) return 'No earlier messages to summarize.'
|
|
144
|
+
return preview.join('\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolvePairingAccess(connector: Connector, msg: InboundMessage): {
|
|
148
|
+
policy: PairingPolicy
|
|
149
|
+
configAllowFrom: string[]
|
|
150
|
+
isAllowed: boolean
|
|
151
|
+
hasAnyApprover: boolean
|
|
152
|
+
} {
|
|
153
|
+
const policy = parsePairingPolicy(connector.config?.dmPolicy, 'open')
|
|
154
|
+
const configAllowFrom = parseAllowFromCsv(connector.config?.allowFrom)
|
|
155
|
+
const stored = listStoredAllowedSenders(connector.id)
|
|
156
|
+
const isAllowed = isSenderAllowed({
|
|
157
|
+
connectorId: connector.id,
|
|
158
|
+
senderId: msg.senderId,
|
|
159
|
+
configAllowFrom,
|
|
160
|
+
})
|
|
161
|
+
return {
|
|
162
|
+
policy,
|
|
163
|
+
configAllowFrom,
|
|
164
|
+
isAllowed,
|
|
165
|
+
hasAnyApprover: (configAllowFrom.length + stored.length) > 0,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handlePairCommand(params: {
|
|
170
|
+
connector: Connector
|
|
171
|
+
msg: InboundMessage
|
|
172
|
+
args: string
|
|
173
|
+
}): Promise<string> {
|
|
174
|
+
const { connector, msg, args } = params
|
|
175
|
+
const access = resolvePairingAccess(connector, msg)
|
|
176
|
+
const parts = args.split(/\s+/).map((item) => item.trim()).filter(Boolean)
|
|
177
|
+
const subcommand = (parts[0] || 'status').toLowerCase()
|
|
178
|
+
|
|
179
|
+
if (subcommand === 'request') {
|
|
180
|
+
const request = createOrTouchPairingRequest({
|
|
181
|
+
connectorId: connector.id,
|
|
182
|
+
senderId: msg.senderId,
|
|
183
|
+
senderName: msg.senderName,
|
|
184
|
+
channelId: msg.channelId,
|
|
185
|
+
})
|
|
186
|
+
return request.created
|
|
187
|
+
? `Pairing request created. Share this code with an approved user: ${request.code}`
|
|
188
|
+
: `Pairing request is already pending. Your code is: ${request.code}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (subcommand === 'list') {
|
|
192
|
+
if (access.hasAnyApprover && !access.isAllowed) {
|
|
193
|
+
return 'Pairing list is restricted to approved senders.'
|
|
194
|
+
}
|
|
195
|
+
const pending = listPendingPairingRequests(connector.id)
|
|
196
|
+
if (!pending.length) return 'No pending pairing requests.'
|
|
197
|
+
const lines = pending.slice(0, 20).map((entry) => {
|
|
198
|
+
const ageMin = Math.max(1, Math.round((Date.now() - entry.updatedAt) / 60_000))
|
|
199
|
+
const sender = entry.senderName ? `${entry.senderName} (${entry.senderId})` : entry.senderId
|
|
200
|
+
return `- ${entry.code} -> ${sender} (${ageMin}m ago)`
|
|
201
|
+
})
|
|
202
|
+
return `Pending pairing requests (${pending.length}):\n${lines.join('\n')}`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (subcommand === 'approve') {
|
|
206
|
+
const code = (parts[1] || '').trim()
|
|
207
|
+
if (!code) return 'Usage: /pair approve <code>'
|
|
208
|
+
if (access.hasAnyApprover && !access.isAllowed) {
|
|
209
|
+
return 'Pairing approvals are restricted to approved senders.'
|
|
210
|
+
}
|
|
211
|
+
const approved = approvePairingCode(connector.id, code)
|
|
212
|
+
if (!approved.ok) return approved.reason || 'Pairing approval failed.'
|
|
213
|
+
const sender = approved.senderName ? `${approved.senderName} (${approved.senderId})` : approved.senderId
|
|
214
|
+
return `Pairing approved: ${sender}`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (subcommand === 'allow') {
|
|
218
|
+
const senderId = (parts[1] || '').trim()
|
|
219
|
+
if (!senderId) return 'Usage: /pair allow <senderId>'
|
|
220
|
+
if (access.hasAnyApprover && !access.isAllowed) {
|
|
221
|
+
return 'Allowlist updates are restricted to approved senders.'
|
|
222
|
+
}
|
|
223
|
+
const result = addAllowedSender(connector.id, senderId)
|
|
224
|
+
if (!result.normalized) return 'Could not parse senderId.'
|
|
225
|
+
return result.added
|
|
226
|
+
? `Allowed sender: ${result.normalized}`
|
|
227
|
+
: `Sender is already allowed: ${result.normalized}`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const pending = listPendingPairingRequests(connector.id)
|
|
231
|
+
const stored = listStoredAllowedSenders(connector.id)
|
|
232
|
+
const policyLine = `Policy: ${access.policy}`
|
|
233
|
+
const approvedLine = `You are ${access.isAllowed ? 'approved' : 'not approved'} as ${msg.senderId}`
|
|
234
|
+
return [
|
|
235
|
+
'Pairing controls:',
|
|
236
|
+
policyLine,
|
|
237
|
+
approvedLine,
|
|
238
|
+
`- Stored approvals: ${stored.length}`,
|
|
239
|
+
`- Pending requests: ${pending.length}`,
|
|
240
|
+
'- Commands: /pair request, /pair list, /pair approve <code>, /pair allow <senderId>',
|
|
241
|
+
].join('\n')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function enforceInboundAccessPolicy(connector: Connector, msg: InboundMessage): string | null {
|
|
245
|
+
if (msg.isGroup) return null
|
|
246
|
+
const { policy, configAllowFrom, isAllowed } = resolvePairingAccess(connector, msg)
|
|
247
|
+
const storedAllowFrom = listStoredAllowedSenders(connector.id)
|
|
248
|
+
if (policy === 'open') return null
|
|
249
|
+
|
|
250
|
+
if (policy === 'disabled') return NO_MESSAGE_SENTINEL
|
|
251
|
+
if (isAllowed) return null
|
|
252
|
+
|
|
253
|
+
if (policy === 'allowlist') {
|
|
254
|
+
if (!configAllowFrom.length && !storedAllowFrom.length) {
|
|
255
|
+
return 'This connector is set to allowlist mode, but no allowFrom entries are configured.'
|
|
256
|
+
}
|
|
257
|
+
return 'You are not authorized for this connector. Ask an approved user to add your sender ID via /pair allow <senderId>.'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (policy === 'pairing') {
|
|
261
|
+
const request = createOrTouchPairingRequest({
|
|
262
|
+
connectorId: connector.id,
|
|
263
|
+
senderId: msg.senderId,
|
|
264
|
+
senderName: msg.senderName,
|
|
265
|
+
channelId: msg.channelId,
|
|
266
|
+
})
|
|
267
|
+
return [
|
|
268
|
+
'Pairing is required before this connector will respond.',
|
|
269
|
+
`Your pairing code: ${request.code}`,
|
|
270
|
+
'Ask an approved sender to run /pair approve <code>.',
|
|
271
|
+
'Tip: if this is first-time setup with no approvals yet, run /pair approve <code> from this chat to bootstrap.',
|
|
272
|
+
].join('\n')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function handleConnectorCommand(params: {
|
|
279
|
+
command: ParsedConnectorCommand
|
|
280
|
+
connector: Connector
|
|
281
|
+
session: any
|
|
282
|
+
msg: InboundMessage
|
|
283
|
+
agentName: string
|
|
284
|
+
}): Promise<string> {
|
|
285
|
+
const { command, connector, session, msg, agentName } = params
|
|
286
|
+
const inboundText = formatInboundUserText(msg)
|
|
287
|
+
|
|
288
|
+
if (command.name === 'help') {
|
|
289
|
+
const text = [
|
|
290
|
+
'Connector commands:',
|
|
291
|
+
'/status — Show active session status',
|
|
292
|
+
'/new or /reset — Clear this connector conversation thread',
|
|
293
|
+
'/compact [keepLastN] — Summarize older history and keep recent messages (default 10)',
|
|
294
|
+
'/think <minimal|low|medium|high> — Set connector thread reasoning guidance',
|
|
295
|
+
'/pair — Pairing/access controls (status, request, list, approve, allow)',
|
|
296
|
+
'/help — Show this list',
|
|
297
|
+
].join('\n')
|
|
298
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
299
|
+
pushSessionMessage(session, 'assistant', text)
|
|
300
|
+
persistSession(session)
|
|
301
|
+
return text
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (command.name === 'status') {
|
|
305
|
+
const all = Array.isArray(session.messages) ? session.messages : []
|
|
306
|
+
const userCount = all.filter((m: any) => m?.role === 'user').length
|
|
307
|
+
const assistantCount = all.filter((m: any) => m?.role === 'assistant').length
|
|
308
|
+
const toolsCount = Array.isArray(session.tools) ? session.tools.length : 0
|
|
309
|
+
const statusText = [
|
|
310
|
+
`Status for ${connector.platform} / ${connector.name}:`,
|
|
311
|
+
`- Agent: ${agentName}`,
|
|
312
|
+
`- Session: ${session.id}`,
|
|
313
|
+
`- Model: ${session.provider}/${session.model}`,
|
|
314
|
+
`- Messages: ${all.length} (${userCount} user, ${assistantCount} assistant)`,
|
|
315
|
+
`- Tools enabled: ${toolsCount}`,
|
|
316
|
+
`- Channel: ${msg.channelName || msg.channelId}`,
|
|
317
|
+
`- Last active: ${new Date(session.lastActiveAt || session.createdAt || Date.now()).toLocaleString()}`,
|
|
318
|
+
].join('\n')
|
|
319
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
320
|
+
pushSessionMessage(session, 'assistant', statusText)
|
|
321
|
+
persistSession(session)
|
|
322
|
+
return statusText
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (command.name === 'new' || command.name === 'reset') {
|
|
326
|
+
const cleared = Array.isArray(session.messages) ? session.messages.length : 0
|
|
327
|
+
session.messages = []
|
|
328
|
+
session.claudeSessionId = null
|
|
329
|
+
session.codexThreadId = null
|
|
330
|
+
session.opencodeSessionId = null
|
|
331
|
+
session.delegateResumeIds = { claudeCode: null, codex: null, opencode: null }
|
|
332
|
+
session.lastActiveAt = Date.now()
|
|
333
|
+
persistSession(session)
|
|
334
|
+
return `Reset complete for ${connector.platform} channel thread. Cleared ${cleared} message(s).`
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (command.name === 'compact') {
|
|
338
|
+
const keepParsed = Number.parseInt(command.args, 10)
|
|
339
|
+
const keepLastN = Number.isFinite(keepParsed) ? Math.max(4, Math.min(50, keepParsed)) : 10
|
|
340
|
+
const history = Array.isArray(session.messages) ? session.messages : []
|
|
341
|
+
if (history.length <= keepLastN) {
|
|
342
|
+
const text = `Nothing to compact. Current history has ${history.length} message(s), keepLastN=${keepLastN}.`
|
|
343
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
344
|
+
pushSessionMessage(session, 'assistant', text)
|
|
345
|
+
persistSession(session)
|
|
346
|
+
return text
|
|
347
|
+
}
|
|
348
|
+
const oldMessages = history.slice(0, -keepLastN)
|
|
349
|
+
const recentMessages = history.slice(-keepLastN)
|
|
350
|
+
const summary = summarizeForCompaction(oldMessages)
|
|
351
|
+
const summaryMessage = {
|
|
352
|
+
role: 'assistant' as const,
|
|
353
|
+
text: `[Context summary: compacted ${oldMessages.length} message(s)]\n${summary}`,
|
|
354
|
+
time: Date.now(),
|
|
355
|
+
kind: 'system' as const,
|
|
356
|
+
}
|
|
357
|
+
session.messages = [summaryMessage, ...recentMessages]
|
|
358
|
+
session.lastActiveAt = Date.now()
|
|
359
|
+
const text = `Compacted ${oldMessages.length} message(s). Kept ${recentMessages.length} recent message(s) plus a summary.`
|
|
360
|
+
pushSessionMessage(session, 'assistant', text)
|
|
361
|
+
persistSession(session)
|
|
362
|
+
return text
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (command.name === 'think') {
|
|
366
|
+
const requested = command.args.trim().toLowerCase()
|
|
367
|
+
const allowed = new Set(['minimal', 'low', 'medium', 'high'])
|
|
368
|
+
if (!requested) {
|
|
369
|
+
const current = typeof session.connectorThinkLevel === 'string' && allowed.has(session.connectorThinkLevel)
|
|
370
|
+
? session.connectorThinkLevel
|
|
371
|
+
: 'medium'
|
|
372
|
+
const text = `Current /think level: ${current}. Usage: /think <minimal|low|medium|high>.`
|
|
373
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
374
|
+
pushSessionMessage(session, 'assistant', text)
|
|
375
|
+
persistSession(session)
|
|
376
|
+
return text
|
|
377
|
+
}
|
|
378
|
+
if (!allowed.has(requested)) {
|
|
379
|
+
const text = 'Invalid /think level. Use one of: minimal, low, medium, high.'
|
|
380
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
381
|
+
pushSessionMessage(session, 'assistant', text)
|
|
382
|
+
persistSession(session)
|
|
383
|
+
return text
|
|
384
|
+
}
|
|
385
|
+
session.connectorThinkLevel = requested
|
|
386
|
+
session.lastActiveAt = Date.now()
|
|
387
|
+
const text = `Set /think level to ${requested} for this connector thread.`
|
|
388
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
389
|
+
pushSessionMessage(session, 'assistant', text)
|
|
390
|
+
persistSession(session)
|
|
391
|
+
return text
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return 'Unknown command.'
|
|
395
|
+
}
|
|
396
|
+
|
|
81
397
|
/** Route an inbound message through the assigned agent and return the response */
|
|
82
398
|
async function routeMessage(connector: Connector, msg: InboundMessage): Promise<string> {
|
|
83
399
|
if (msg?.channelId) {
|
|
@@ -85,7 +401,8 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
85
401
|
}
|
|
86
402
|
|
|
87
403
|
const agents = loadAgents()
|
|
88
|
-
const
|
|
404
|
+
const effectiveAgentId = msg.agentIdOverride || connector.agentId
|
|
405
|
+
const agent = agents[effectiveAgentId]
|
|
89
406
|
if (!agent) return '[Error] Connector agent not found.'
|
|
90
407
|
|
|
91
408
|
// Log connector trigger
|
|
@@ -122,7 +439,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
122
439
|
const sessions = loadSessions()
|
|
123
440
|
let session = Object.values(sessions).find((s: any) => s.name === sessionKey)
|
|
124
441
|
if (!session) {
|
|
125
|
-
const id =
|
|
442
|
+
const id = genId()
|
|
126
443
|
session = {
|
|
127
444
|
id,
|
|
128
445
|
name: sessionKey,
|
|
@@ -151,6 +468,59 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
151
468
|
saveSessions(sessions)
|
|
152
469
|
}
|
|
153
470
|
|
|
471
|
+
const parsedCommand = parseConnectorCommand(msg.text || '')
|
|
472
|
+
if (parsedCommand?.name === 'pair') {
|
|
473
|
+
const commandResult = await handlePairCommand({
|
|
474
|
+
connector,
|
|
475
|
+
msg,
|
|
476
|
+
args: parsedCommand.args,
|
|
477
|
+
})
|
|
478
|
+
logExecution(session.id, 'decision', 'Connector pair command handled', {
|
|
479
|
+
agentId: agent.id,
|
|
480
|
+
detail: {
|
|
481
|
+
platform: msg.platform,
|
|
482
|
+
channelId: msg.channelId,
|
|
483
|
+
command: 'pair',
|
|
484
|
+
args: parsedCommand.args || null,
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
return commandResult
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const accessPolicyResult = enforceInboundAccessPolicy(connector, msg)
|
|
491
|
+
if (accessPolicyResult) {
|
|
492
|
+
logExecution(session.id, 'decision', 'Connector inbound blocked by access policy', {
|
|
493
|
+
agentId: agent.id,
|
|
494
|
+
detail: {
|
|
495
|
+
platform: msg.platform,
|
|
496
|
+
channelId: msg.channelId,
|
|
497
|
+
senderId: msg.senderId,
|
|
498
|
+
policy: parsePairingPolicy(connector.config?.dmPolicy, 'open'),
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
return accessPolicyResult
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (parsedCommand) {
|
|
505
|
+
const commandResult = await handleConnectorCommand({
|
|
506
|
+
command: parsedCommand,
|
|
507
|
+
connector,
|
|
508
|
+
session,
|
|
509
|
+
msg,
|
|
510
|
+
agentName: agent.name,
|
|
511
|
+
})
|
|
512
|
+
logExecution(session.id, 'decision', `Connector command handled: /${parsedCommand.name}`, {
|
|
513
|
+
agentId: agent.id,
|
|
514
|
+
detail: {
|
|
515
|
+
platform: msg.platform,
|
|
516
|
+
channelId: msg.channelId,
|
|
517
|
+
command: parsedCommand.name,
|
|
518
|
+
args: parsedCommand.args || null,
|
|
519
|
+
},
|
|
520
|
+
})
|
|
521
|
+
return commandResult
|
|
522
|
+
}
|
|
523
|
+
|
|
154
524
|
// Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt]
|
|
155
525
|
const settings = loadSettings()
|
|
156
526
|
const promptParts: string[] = []
|
|
@@ -164,6 +534,12 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
164
534
|
if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
165
535
|
}
|
|
166
536
|
}
|
|
537
|
+
const thinkLevel = typeof session.connectorThinkLevel === 'string'
|
|
538
|
+
? session.connectorThinkLevel.trim().toLowerCase()
|
|
539
|
+
: ''
|
|
540
|
+
if (thinkLevel) {
|
|
541
|
+
promptParts.push(`Connector thinking guidance: ${thinkLevel}. Keep responses concise and useful for chat.`)
|
|
542
|
+
}
|
|
167
543
|
// Add connector context
|
|
168
544
|
promptParts.push(`\nYou are receiving messages via ${msg.platform}. The user "${msg.senderName}" is messaging from channel "${msg.channelName || msg.channelId}". Respond naturally and conversationally.
|
|
169
545
|
|
|
@@ -326,6 +702,9 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
326
702
|
if (!botToken && connector.config.botToken) {
|
|
327
703
|
botToken = connector.config.botToken
|
|
328
704
|
}
|
|
705
|
+
if (!botToken && connector.platform === 'bluebubbles' && connector.config.password) {
|
|
706
|
+
botToken = connector.config.password
|
|
707
|
+
}
|
|
329
708
|
|
|
330
709
|
if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal') {
|
|
331
710
|
throw new Error('No bot token configured')
|
|
@@ -475,6 +854,11 @@ export function listRunningConnectors(platform?: string): Array<{
|
|
|
475
854
|
if (outboundJid) configuredTargets.push(outboundJid)
|
|
476
855
|
const allowed = connector.config?.allowedJids?.split(',').map((s) => s.trim()).filter(Boolean) || []
|
|
477
856
|
configuredTargets.push(...allowed)
|
|
857
|
+
} else if (connector.platform === 'bluebubbles') {
|
|
858
|
+
const outbound = connector.config?.outboundTarget?.trim()
|
|
859
|
+
if (outbound) configuredTargets.push(outbound)
|
|
860
|
+
const allowed = connector.config?.allowFrom?.split(',').map((s) => s.trim()).filter(Boolean) || []
|
|
861
|
+
configuredTargets.push(...allowed)
|
|
478
862
|
}
|
|
479
863
|
out.push({
|
|
480
864
|
id,
|
|
@@ -494,6 +878,11 @@ export function getConnectorRecentChannelId(connectorId: string): string | null
|
|
|
494
878
|
return lastInboundChannelByConnector.get(connectorId) || null
|
|
495
879
|
}
|
|
496
880
|
|
|
881
|
+
/** Get a running connector instance (internal use for rich messaging). */
|
|
882
|
+
export function getRunningInstance(connectorId: string): ConnectorInstance | undefined {
|
|
883
|
+
return running.get(connectorId)
|
|
884
|
+
}
|
|
885
|
+
|
|
497
886
|
/**
|
|
498
887
|
* Send an outbound message through a running connector.
|
|
499
888
|
* Intended for proactive agent notifications (e.g. WhatsApp updates).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import crypto from 'crypto'
|
|
2
1
|
import fs from 'fs'
|
|
3
2
|
import path from 'path'
|
|
3
|
+
import { genId } from '@/lib/id'
|
|
4
4
|
import { UPLOAD_DIR } from '../storage'
|
|
5
5
|
import type { InboundMedia, InboundMediaType } from './types'
|
|
6
6
|
|
|
@@ -94,7 +94,7 @@ export function saveInboundMediaBuffer(params: {
|
|
|
94
94
|
|
|
95
95
|
const ext = extFromName(params.fileName) || extFromMime(params.mimeType) || '.bin'
|
|
96
96
|
const base = safeBaseName(params.fileName)
|
|
97
|
-
const unique =
|
|
97
|
+
const unique = genId()
|
|
98
98
|
const filename = `${params.connectorId}-${Date.now()}-${base}-${unique}${ext}`
|
|
99
99
|
const localPath = path.join(UPLOAD_DIR, filename)
|
|
100
100
|
fs.writeFileSync(localPath, params.buffer)
|
|
@@ -827,6 +827,13 @@ const openclaw: PlatformConnector = {
|
|
|
827
827
|
if (identity.deviceToken === normalized) return
|
|
828
828
|
identity = { ...identity, deviceToken: normalized }
|
|
829
829
|
persistIdentity(identityPath, identity)
|
|
830
|
+
// Cross-sync device token for provider identity resolution
|
|
831
|
+
if (normalized) {
|
|
832
|
+
try {
|
|
833
|
+
const { setSharedDeviceToken } = require('../openclaw-sync')
|
|
834
|
+
setSharedDeviceToken(normalized)
|
|
835
|
+
} catch { /* openclaw-sync not available */ }
|
|
836
|
+
}
|
|
830
837
|
}
|
|
831
838
|
|
|
832
839
|
function clearStaleTokenIfNeeded(reason?: string) {
|
|
@@ -928,6 +935,23 @@ const openclaw: PlatformConnector = {
|
|
|
928
935
|
source: 'event' | 'history',
|
|
929
936
|
) {
|
|
930
937
|
if (!matchesSessionKey(configuredSessionFilter, inbound.channelId)) return
|
|
938
|
+
|
|
939
|
+
// Multi-agent routing: match sessionKey against agentRouting config
|
|
940
|
+
const agentRouting = connector.config.agentRouting
|
|
941
|
+
if (agentRouting) {
|
|
942
|
+
try {
|
|
943
|
+
const routingMap: Record<string, string> = typeof agentRouting === 'string'
|
|
944
|
+
? JSON.parse(agentRouting)
|
|
945
|
+
: agentRouting as unknown as Record<string, string>
|
|
946
|
+
for (const [pattern, agentId] of Object.entries(routingMap)) {
|
|
947
|
+
if (matchesSessionKey(pattern, inbound.channelId)) {
|
|
948
|
+
inbound.agentIdOverride = agentId
|
|
949
|
+
break
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch { /* ignore malformed routing config */ }
|
|
953
|
+
}
|
|
954
|
+
|
|
931
955
|
if (!rememberSeenEntry(seenInbound, dedupeKey, MAX_SEEN_CHAT_EVENTS)) return
|
|
932
956
|
|
|
933
957
|
const now = Date.now()
|
|
@@ -1120,6 +1144,46 @@ const openclaw: PlatformConnector = {
|
|
|
1120
1144
|
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1121
1145
|
await sendChat(channelId || defaultSessionKey, text, options)
|
|
1122
1146
|
},
|
|
1147
|
+
async sendReaction(channelId, messageId, emoji) {
|
|
1148
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1149
|
+
try {
|
|
1150
|
+
await rpcRequest('chat.react', { sessionKey: channelId || defaultSessionKey, messageId, emoji })
|
|
1151
|
+
} catch (err: unknown) {
|
|
1152
|
+
const msg = getErrorMessage(err)
|
|
1153
|
+
if (msg.toLowerCase().includes('unknown method')) return // graceful degrade
|
|
1154
|
+
throw err
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
async editMessage(channelId, messageId, newText) {
|
|
1158
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1159
|
+
try {
|
|
1160
|
+
await rpcRequest('chat.edit', { sessionKey: channelId || defaultSessionKey, messageId, text: newText })
|
|
1161
|
+
} catch (err: unknown) {
|
|
1162
|
+
const msg = getErrorMessage(err)
|
|
1163
|
+
if (msg.toLowerCase().includes('unknown method')) return
|
|
1164
|
+
throw err
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
async deleteMessage(channelId, messageId) {
|
|
1168
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1169
|
+
try {
|
|
1170
|
+
await rpcRequest('chat.delete', { sessionKey: channelId || defaultSessionKey, messageId })
|
|
1171
|
+
} catch (err: unknown) {
|
|
1172
|
+
const msg = getErrorMessage(err)
|
|
1173
|
+
if (msg.toLowerCase().includes('unknown method')) return
|
|
1174
|
+
throw err
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
async pinMessage(channelId, messageId) {
|
|
1178
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1179
|
+
try {
|
|
1180
|
+
await rpcRequest('chat.pin', { sessionKey: channelId || defaultSessionKey, messageId })
|
|
1181
|
+
} catch (err: unknown) {
|
|
1182
|
+
const msg = getErrorMessage(err)
|
|
1183
|
+
if (msg.toLowerCase().includes('unknown method')) return
|
|
1184
|
+
throw err
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1123
1187
|
async stop() {
|
|
1124
1188
|
stopped = true
|
|
1125
1189
|
cleanupSocket()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import {
|
|
7
|
+
addAllowedSender,
|
|
8
|
+
approvePairingCode,
|
|
9
|
+
clearConnectorPairingState,
|
|
10
|
+
createOrTouchPairingRequest,
|
|
11
|
+
isSenderAllowed,
|
|
12
|
+
listPendingPairingRequests,
|
|
13
|
+
listStoredAllowedSenders,
|
|
14
|
+
parseAllowFromCsv,
|
|
15
|
+
parsePairingPolicy,
|
|
16
|
+
} from './pairing.ts'
|
|
17
|
+
|
|
18
|
+
function withTempDataDir<T>(fn: (dir: string) => T): T {
|
|
19
|
+
const original = process.env.DATA_DIR
|
|
20
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-pairing-test-'))
|
|
21
|
+
process.env.DATA_DIR = tempDir
|
|
22
|
+
try {
|
|
23
|
+
return fn(tempDir)
|
|
24
|
+
} finally {
|
|
25
|
+
if (typeof original === 'string') process.env.DATA_DIR = original
|
|
26
|
+
else delete process.env.DATA_DIR
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('pairing store creates request, approves code, and persists allowlist', () => {
|
|
32
|
+
withTempDataDir(() => {
|
|
33
|
+
const connectorId = 'pair-test-1'
|
|
34
|
+
|
|
35
|
+
const first = createOrTouchPairingRequest({
|
|
36
|
+
connectorId,
|
|
37
|
+
senderId: '+15551234567',
|
|
38
|
+
senderName: 'Alice',
|
|
39
|
+
channelId: 'chat:1',
|
|
40
|
+
})
|
|
41
|
+
assert.equal(first.created, true)
|
|
42
|
+
assert.equal(first.code.length, 8)
|
|
43
|
+
|
|
44
|
+
const second = createOrTouchPairingRequest({
|
|
45
|
+
connectorId,
|
|
46
|
+
senderId: '+15551234567',
|
|
47
|
+
senderName: 'Alice',
|
|
48
|
+
channelId: 'chat:1',
|
|
49
|
+
})
|
|
50
|
+
assert.equal(second.created, false)
|
|
51
|
+
assert.equal(second.code, first.code)
|
|
52
|
+
|
|
53
|
+
const pendingBefore = listPendingPairingRequests(connectorId)
|
|
54
|
+
assert.equal(pendingBefore.length, 1)
|
|
55
|
+
assert.equal(pendingBefore[0].senderId, '+15551234567')
|
|
56
|
+
|
|
57
|
+
const bad = approvePairingCode(connectorId, 'INVALID')
|
|
58
|
+
assert.equal(bad.ok, false)
|
|
59
|
+
|
|
60
|
+
const approved = approvePairingCode(connectorId, first.code)
|
|
61
|
+
assert.equal(approved.ok, true)
|
|
62
|
+
assert.equal(approved.senderId, '+15551234567')
|
|
63
|
+
|
|
64
|
+
const pendingAfter = listPendingPairingRequests(connectorId)
|
|
65
|
+
assert.equal(pendingAfter.length, 0)
|
|
66
|
+
|
|
67
|
+
const stored = listStoredAllowedSenders(connectorId)
|
|
68
|
+
assert.deepEqual(stored, ['+15551234567'])
|
|
69
|
+
|
|
70
|
+
assert.equal(isSenderAllowed({ connectorId, senderId: '+15551234567' }), true)
|
|
71
|
+
assert.equal(isSenderAllowed({ connectorId, senderId: '+16667778888' }), false)
|
|
72
|
+
|
|
73
|
+
clearConnectorPairingState(connectorId)
|
|
74
|
+
assert.deepEqual(listStoredAllowedSenders(connectorId), [])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('pairing helpers normalize policy and allowFrom csv entries', () => {
|
|
79
|
+
assert.equal(parsePairingPolicy('PAIRING'), 'pairing')
|
|
80
|
+
assert.equal(parsePairingPolicy('allowlist'), 'allowlist')
|
|
81
|
+
assert.equal(parsePairingPolicy('unknown', 'open'), 'open')
|
|
82
|
+
|
|
83
|
+
const list = parseAllowFromCsv(' +1555,TEST@example.com,+1555 , ')
|
|
84
|
+
assert.deepEqual(list, ['+1555', 'test@example.com'])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('addAllowedSender deduplicates and normalizes sender ids', () => {
|
|
88
|
+
withTempDataDir(() => {
|
|
89
|
+
const connectorId = 'pair-test-2'
|
|
90
|
+
const first = addAllowedSender(connectorId, ' TEST@Example.com ')
|
|
91
|
+
assert.equal(first.added, true)
|
|
92
|
+
assert.equal(first.normalized, 'test@example.com')
|
|
93
|
+
|
|
94
|
+
const second = addAllowedSender(connectorId, 'test@example.com')
|
|
95
|
+
assert.equal(second.added, false)
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(listStoredAllowedSenders(connectorId), ['test@example.com'])
|
|
98
|
+
})
|
|
99
|
+
})
|