@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
|
@@ -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'
|
|
@@ -22,18 +33,25 @@ export function isNoMessage(text: string): boolean {
|
|
|
22
33
|
* Stored on globalThis to survive HMR reloads in dev mode —
|
|
23
34
|
* prevents duplicate sockets fighting for the same WhatsApp session. */
|
|
24
35
|
const globalKey = '__swarmclaw_running_connectors__' as const
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const g = globalThis as any
|
|
25
38
|
const running: Map<string, ConnectorInstance> =
|
|
26
|
-
|
|
39
|
+
g[globalKey] ?? (g[globalKey] = new Map<string, ConnectorInstance>())
|
|
27
40
|
|
|
28
41
|
/** Most recent inbound channel per connector (used for proactive replies/default outbound target) */
|
|
29
42
|
const lastInboundKey = '__swarmclaw_connector_last_inbound__' as const
|
|
30
43
|
const lastInboundChannelByConnector: Map<string, string> =
|
|
31
|
-
|
|
44
|
+
g[lastInboundKey] ?? (g[lastInboundKey] = new Map<string, string>())
|
|
45
|
+
|
|
46
|
+
/** Last inbound message timestamp per connector (for presence indicators) */
|
|
47
|
+
const lastInboundTimeKey = '__swarmclaw_connector_last_inbound_time__' as const
|
|
48
|
+
const lastInboundTimeByConnector: Map<string, number> =
|
|
49
|
+
g[lastInboundTimeKey] ?? (g[lastInboundTimeKey] = new Map<string, number>())
|
|
32
50
|
|
|
33
51
|
/** Per-connector lock to prevent concurrent start/stop operations */
|
|
34
52
|
const lockKey = '__swarmclaw_connector_locks__' as const
|
|
35
53
|
const locks: Map<string, Promise<void>> =
|
|
36
|
-
|
|
54
|
+
g[lockKey] ?? (g[lockKey] = new Map<string, Promise<void>>())
|
|
37
55
|
|
|
38
56
|
/** Get platform implementation lazily */
|
|
39
57
|
export async function getPlatform(platform: string) {
|
|
@@ -43,6 +61,7 @@ export async function getPlatform(platform: string) {
|
|
|
43
61
|
case 'slack': return (await import('./slack')).default
|
|
44
62
|
case 'whatsapp': return (await import('./whatsapp')).default
|
|
45
63
|
case 'openclaw': return (await import('./openclaw')).default
|
|
64
|
+
case 'bluebubbles': return (await import('./bluebubbles')).default
|
|
46
65
|
case 'signal': return (await import('./signal')).default
|
|
47
66
|
case 'teams': return (await import('./teams')).default
|
|
48
67
|
case 'googlechat': return (await import('./googlechat')).default
|
|
@@ -78,19 +97,329 @@ export function formatInboundUserText(msg: InboundMessage): string {
|
|
|
78
97
|
return lines.join('\n').trim()
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
type ConnectorCommandName = 'help' | 'status' | 'new' | 'reset' | 'compact' | 'think' | 'pair'
|
|
101
|
+
|
|
102
|
+
interface ParsedConnectorCommand {
|
|
103
|
+
name: ConnectorCommandName
|
|
104
|
+
args: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
|
|
108
|
+
const trimmed = text.trim()
|
|
109
|
+
if (!trimmed.startsWith('/')) return null
|
|
110
|
+
const [head, ...rest] = trimmed.split(/\s+/)
|
|
111
|
+
const name = head.slice(1).toLowerCase()
|
|
112
|
+
const args = rest.join(' ').trim()
|
|
113
|
+
switch (name) {
|
|
114
|
+
case 'help':
|
|
115
|
+
case 'status':
|
|
116
|
+
case 'new':
|
|
117
|
+
case 'reset':
|
|
118
|
+
case 'compact':
|
|
119
|
+
case 'think':
|
|
120
|
+
case 'pair':
|
|
121
|
+
return { name, args } as ParsedConnectorCommand
|
|
122
|
+
default:
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
function pushSessionMessage(session: Record<string, any>, role: 'user' | 'assistant', text: string): void {
|
|
129
|
+
if (!text.trim()) return
|
|
130
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
131
|
+
session.messages.push({ role, text: text.trim(), time: Date.now() })
|
|
132
|
+
session.lastActiveAt = Date.now()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
function persistSession(session: Record<string, any>): void {
|
|
137
|
+
const sessions = loadSessions()
|
|
138
|
+
sessions[session.id] = session
|
|
139
|
+
saveSessions(sessions)
|
|
140
|
+
notify(`messages:${session.id}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function summarizeForCompaction(messages: Array<{ role?: string; text?: string }>): string {
|
|
144
|
+
const preview = messages
|
|
145
|
+
.slice(-8)
|
|
146
|
+
.map((m, i) => {
|
|
147
|
+
const role = (m.role || 'unknown').toUpperCase()
|
|
148
|
+
const body = (m.text || '').replace(/\s+/g, ' ').trim()
|
|
149
|
+
const clipped = body.length > 180 ? `${body.slice(0, 177)}...` : body
|
|
150
|
+
return `${i + 1}. [${role}] ${clipped || '(no text)'}`
|
|
151
|
+
})
|
|
152
|
+
if (!preview.length) return 'No earlier messages to summarize.'
|
|
153
|
+
return preview.join('\n')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolvePairingAccess(connector: Connector, msg: InboundMessage): {
|
|
157
|
+
policy: PairingPolicy
|
|
158
|
+
configAllowFrom: string[]
|
|
159
|
+
isAllowed: boolean
|
|
160
|
+
hasAnyApprover: boolean
|
|
161
|
+
} {
|
|
162
|
+
const policy = parsePairingPolicy(connector.config?.dmPolicy, 'open')
|
|
163
|
+
const configAllowFrom = parseAllowFromCsv(connector.config?.allowFrom)
|
|
164
|
+
const stored = listStoredAllowedSenders(connector.id)
|
|
165
|
+
const isAllowed = isSenderAllowed({
|
|
166
|
+
connectorId: connector.id,
|
|
167
|
+
senderId: msg.senderId,
|
|
168
|
+
configAllowFrom,
|
|
169
|
+
})
|
|
170
|
+
return {
|
|
171
|
+
policy,
|
|
172
|
+
configAllowFrom,
|
|
173
|
+
isAllowed,
|
|
174
|
+
hasAnyApprover: (configAllowFrom.length + stored.length) > 0,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function handlePairCommand(params: {
|
|
179
|
+
connector: Connector
|
|
180
|
+
msg: InboundMessage
|
|
181
|
+
args: string
|
|
182
|
+
}): Promise<string> {
|
|
183
|
+
const { connector, msg, args } = params
|
|
184
|
+
const access = resolvePairingAccess(connector, msg)
|
|
185
|
+
const parts = args.split(/\s+/).map((item) => item.trim()).filter(Boolean)
|
|
186
|
+
const subcommand = (parts[0] || 'status').toLowerCase()
|
|
187
|
+
|
|
188
|
+
if (subcommand === 'request') {
|
|
189
|
+
const request = createOrTouchPairingRequest({
|
|
190
|
+
connectorId: connector.id,
|
|
191
|
+
senderId: msg.senderId,
|
|
192
|
+
senderName: msg.senderName,
|
|
193
|
+
channelId: msg.channelId,
|
|
194
|
+
})
|
|
195
|
+
return request.created
|
|
196
|
+
? `Pairing request created. Share this code with an approved user: ${request.code}`
|
|
197
|
+
: `Pairing request is already pending. Your code is: ${request.code}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (subcommand === 'list') {
|
|
201
|
+
if (access.hasAnyApprover && !access.isAllowed) {
|
|
202
|
+
return 'Pairing list is restricted to approved senders.'
|
|
203
|
+
}
|
|
204
|
+
const pending = listPendingPairingRequests(connector.id)
|
|
205
|
+
if (!pending.length) return 'No pending pairing requests.'
|
|
206
|
+
const lines = pending.slice(0, 20).map((entry) => {
|
|
207
|
+
const ageMin = Math.max(1, Math.round((Date.now() - entry.updatedAt) / 60_000))
|
|
208
|
+
const sender = entry.senderName ? `${entry.senderName} (${entry.senderId})` : entry.senderId
|
|
209
|
+
return `- ${entry.code} -> ${sender} (${ageMin}m ago)`
|
|
210
|
+
})
|
|
211
|
+
return `Pending pairing requests (${pending.length}):\n${lines.join('\n')}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (subcommand === 'approve') {
|
|
215
|
+
const code = (parts[1] || '').trim()
|
|
216
|
+
if (!code) return 'Usage: /pair approve <code>'
|
|
217
|
+
if (access.hasAnyApprover && !access.isAllowed) {
|
|
218
|
+
return 'Pairing approvals are restricted to approved senders.'
|
|
219
|
+
}
|
|
220
|
+
const approved = approvePairingCode(connector.id, code)
|
|
221
|
+
if (!approved.ok) return approved.reason || 'Pairing approval failed.'
|
|
222
|
+
const sender = approved.senderName ? `${approved.senderName} (${approved.senderId})` : approved.senderId
|
|
223
|
+
return `Pairing approved: ${sender}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (subcommand === 'allow') {
|
|
227
|
+
const senderId = (parts[1] || '').trim()
|
|
228
|
+
if (!senderId) return 'Usage: /pair allow <senderId>'
|
|
229
|
+
if (access.hasAnyApprover && !access.isAllowed) {
|
|
230
|
+
return 'Allowlist updates are restricted to approved senders.'
|
|
231
|
+
}
|
|
232
|
+
const result = addAllowedSender(connector.id, senderId)
|
|
233
|
+
if (!result.normalized) return 'Could not parse senderId.'
|
|
234
|
+
return result.added
|
|
235
|
+
? `Allowed sender: ${result.normalized}`
|
|
236
|
+
: `Sender is already allowed: ${result.normalized}`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pending = listPendingPairingRequests(connector.id)
|
|
240
|
+
const stored = listStoredAllowedSenders(connector.id)
|
|
241
|
+
const policyLine = `Policy: ${access.policy}`
|
|
242
|
+
const approvedLine = `You are ${access.isAllowed ? 'approved' : 'not approved'} as ${msg.senderId}`
|
|
243
|
+
return [
|
|
244
|
+
'Pairing controls:',
|
|
245
|
+
policyLine,
|
|
246
|
+
approvedLine,
|
|
247
|
+
`- Stored approvals: ${stored.length}`,
|
|
248
|
+
`- Pending requests: ${pending.length}`,
|
|
249
|
+
'- Commands: /pair request, /pair list, /pair approve <code>, /pair allow <senderId>',
|
|
250
|
+
].join('\n')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function enforceInboundAccessPolicy(connector: Connector, msg: InboundMessage): string | null {
|
|
254
|
+
if (msg.isGroup) return null
|
|
255
|
+
const { policy, configAllowFrom, isAllowed } = resolvePairingAccess(connector, msg)
|
|
256
|
+
const storedAllowFrom = listStoredAllowedSenders(connector.id)
|
|
257
|
+
if (policy === 'open') return null
|
|
258
|
+
|
|
259
|
+
if (policy === 'disabled') return NO_MESSAGE_SENTINEL
|
|
260
|
+
if (isAllowed) return null
|
|
261
|
+
|
|
262
|
+
if (policy === 'allowlist') {
|
|
263
|
+
if (!configAllowFrom.length && !storedAllowFrom.length) {
|
|
264
|
+
return 'This connector is set to allowlist mode, but no allowFrom entries are configured.'
|
|
265
|
+
}
|
|
266
|
+
return 'You are not authorized for this connector. Ask an approved user to add your sender ID via /pair allow <senderId>.'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (policy === 'pairing') {
|
|
270
|
+
const request = createOrTouchPairingRequest({
|
|
271
|
+
connectorId: connector.id,
|
|
272
|
+
senderId: msg.senderId,
|
|
273
|
+
senderName: msg.senderName,
|
|
274
|
+
channelId: msg.channelId,
|
|
275
|
+
})
|
|
276
|
+
return [
|
|
277
|
+
'Pairing is required before this connector will respond.',
|
|
278
|
+
`Your pairing code: ${request.code}`,
|
|
279
|
+
'Ask an approved sender to run /pair approve <code>.',
|
|
280
|
+
'Tip: if this is first-time setup with no approvals yet, run /pair approve <code> from this chat to bootstrap.',
|
|
281
|
+
].join('\n')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function handleConnectorCommand(params: {
|
|
288
|
+
command: ParsedConnectorCommand
|
|
289
|
+
connector: Connector
|
|
290
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
291
|
+
session: Record<string, any>
|
|
292
|
+
msg: InboundMessage
|
|
293
|
+
agentName: string
|
|
294
|
+
}): Promise<string> {
|
|
295
|
+
const { command, connector, session, msg, agentName } = params
|
|
296
|
+
const inboundText = formatInboundUserText(msg)
|
|
297
|
+
|
|
298
|
+
if (command.name === 'help') {
|
|
299
|
+
const text = [
|
|
300
|
+
'Connector commands:',
|
|
301
|
+
'/status — Show active session status',
|
|
302
|
+
'/new or /reset — Clear this connector conversation thread',
|
|
303
|
+
'/compact [keepLastN] — Summarize older history and keep recent messages (default 10)',
|
|
304
|
+
'/think <minimal|low|medium|high> — Set connector thread reasoning guidance',
|
|
305
|
+
'/pair — Pairing/access controls (status, request, list, approve, allow)',
|
|
306
|
+
'/help — Show this list',
|
|
307
|
+
].join('\n')
|
|
308
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
309
|
+
pushSessionMessage(session, 'assistant', text)
|
|
310
|
+
persistSession(session)
|
|
311
|
+
return text
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (command.name === 'status') {
|
|
315
|
+
const all = Array.isArray(session.messages) ? session.messages : []
|
|
316
|
+
const userCount = all.filter((m: { role?: string }) => m?.role === 'user').length
|
|
317
|
+
const assistantCount = all.filter((m: { role?: string }) => m?.role === 'assistant').length
|
|
318
|
+
const toolsCount = Array.isArray(session.tools) ? session.tools.length : 0
|
|
319
|
+
const statusText = [
|
|
320
|
+
`Status for ${connector.platform} / ${connector.name}:`,
|
|
321
|
+
`- Agent: ${agentName}`,
|
|
322
|
+
`- Session: ${session.id}`,
|
|
323
|
+
`- Model: ${session.provider}/${session.model}`,
|
|
324
|
+
`- Messages: ${all.length} (${userCount} user, ${assistantCount} assistant)`,
|
|
325
|
+
`- Tools enabled: ${toolsCount}`,
|
|
326
|
+
`- Channel: ${msg.channelName || msg.channelId}`,
|
|
327
|
+
`- Last active: ${new Date(session.lastActiveAt || session.createdAt || Date.now()).toLocaleString()}`,
|
|
328
|
+
].join('\n')
|
|
329
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
330
|
+
pushSessionMessage(session, 'assistant', statusText)
|
|
331
|
+
persistSession(session)
|
|
332
|
+
return statusText
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (command.name === 'new' || command.name === 'reset') {
|
|
336
|
+
const cleared = Array.isArray(session.messages) ? session.messages.length : 0
|
|
337
|
+
session.messages = []
|
|
338
|
+
session.claudeSessionId = null
|
|
339
|
+
session.codexThreadId = null
|
|
340
|
+
session.opencodeSessionId = null
|
|
341
|
+
session.delegateResumeIds = { claudeCode: null, codex: null, opencode: null }
|
|
342
|
+
session.lastActiveAt = Date.now()
|
|
343
|
+
persistSession(session)
|
|
344
|
+
return `Reset complete for ${connector.platform} channel thread. Cleared ${cleared} message(s).`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (command.name === 'compact') {
|
|
348
|
+
const keepParsed = Number.parseInt(command.args, 10)
|
|
349
|
+
const keepLastN = Number.isFinite(keepParsed) ? Math.max(4, Math.min(50, keepParsed)) : 10
|
|
350
|
+
const history = Array.isArray(session.messages) ? session.messages : []
|
|
351
|
+
if (history.length <= keepLastN) {
|
|
352
|
+
const text = `Nothing to compact. Current history has ${history.length} message(s), keepLastN=${keepLastN}.`
|
|
353
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
354
|
+
pushSessionMessage(session, 'assistant', text)
|
|
355
|
+
persistSession(session)
|
|
356
|
+
return text
|
|
357
|
+
}
|
|
358
|
+
const oldMessages = history.slice(0, -keepLastN)
|
|
359
|
+
const recentMessages = history.slice(-keepLastN)
|
|
360
|
+
const summary = summarizeForCompaction(oldMessages)
|
|
361
|
+
const summaryMessage = {
|
|
362
|
+
role: 'assistant' as const,
|
|
363
|
+
text: `[Context summary: compacted ${oldMessages.length} message(s)]\n${summary}`,
|
|
364
|
+
time: Date.now(),
|
|
365
|
+
kind: 'system' as const,
|
|
366
|
+
}
|
|
367
|
+
session.messages = [summaryMessage, ...recentMessages]
|
|
368
|
+
session.lastActiveAt = Date.now()
|
|
369
|
+
const text = `Compacted ${oldMessages.length} message(s). Kept ${recentMessages.length} recent message(s) plus a summary.`
|
|
370
|
+
pushSessionMessage(session, 'assistant', text)
|
|
371
|
+
persistSession(session)
|
|
372
|
+
return text
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (command.name === 'think') {
|
|
376
|
+
const requested = command.args.trim().toLowerCase()
|
|
377
|
+
const allowed = new Set(['minimal', 'low', 'medium', 'high'])
|
|
378
|
+
if (!requested) {
|
|
379
|
+
const current = typeof session.connectorThinkLevel === 'string' && allowed.has(session.connectorThinkLevel)
|
|
380
|
+
? session.connectorThinkLevel
|
|
381
|
+
: 'medium'
|
|
382
|
+
const text = `Current /think level: ${current}. Usage: /think <minimal|low|medium|high>.`
|
|
383
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
384
|
+
pushSessionMessage(session, 'assistant', text)
|
|
385
|
+
persistSession(session)
|
|
386
|
+
return text
|
|
387
|
+
}
|
|
388
|
+
if (!allowed.has(requested)) {
|
|
389
|
+
const text = 'Invalid /think level. Use one of: minimal, low, medium, high.'
|
|
390
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
391
|
+
pushSessionMessage(session, 'assistant', text)
|
|
392
|
+
persistSession(session)
|
|
393
|
+
return text
|
|
394
|
+
}
|
|
395
|
+
session.connectorThinkLevel = requested
|
|
396
|
+
session.lastActiveAt = Date.now()
|
|
397
|
+
const text = `Set /think level to ${requested} for this connector thread.`
|
|
398
|
+
pushSessionMessage(session, 'user', inboundText)
|
|
399
|
+
pushSessionMessage(session, 'assistant', text)
|
|
400
|
+
persistSession(session)
|
|
401
|
+
return text
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return 'Unknown command.'
|
|
405
|
+
}
|
|
406
|
+
|
|
81
407
|
/** Route an inbound message through the assigned agent and return the response */
|
|
82
408
|
async function routeMessage(connector: Connector, msg: InboundMessage): Promise<string> {
|
|
83
409
|
if (msg?.channelId) {
|
|
84
410
|
lastInboundChannelByConnector.set(connector.id, msg.channelId)
|
|
85
411
|
}
|
|
412
|
+
lastInboundTimeByConnector.set(connector.id, Date.now())
|
|
86
413
|
|
|
87
414
|
const agents = loadAgents()
|
|
88
|
-
const
|
|
415
|
+
const effectiveAgentId = msg.agentIdOverride || connector.agentId
|
|
416
|
+
const agent = agents[effectiveAgentId]
|
|
89
417
|
if (!agent) return '[Error] Connector agent not found.'
|
|
90
418
|
|
|
91
419
|
// Log connector trigger
|
|
92
420
|
const triggerSessionKey = `connector:${connector.id}:${msg.channelId}`
|
|
93
421
|
const allSessions = loadSessions()
|
|
422
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
423
|
const existingSession = Object.values(allSessions).find((s: any) => s.name === triggerSessionKey)
|
|
95
424
|
if (existingSession) {
|
|
96
425
|
logExecution(existingSession.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
|
|
@@ -120,9 +449,10 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
120
449
|
// Find or create a session keyed by platform + channel
|
|
121
450
|
const sessionKey = `connector:${connector.id}:${msg.channelId}`
|
|
122
451
|
const sessions = loadSessions()
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
453
|
let session = Object.values(sessions).find((s: any) => s.name === sessionKey)
|
|
124
454
|
if (!session) {
|
|
125
|
-
const id =
|
|
455
|
+
const id = genId()
|
|
126
456
|
session = {
|
|
127
457
|
id,
|
|
128
458
|
name: sessionKey,
|
|
@@ -151,6 +481,59 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
151
481
|
saveSessions(sessions)
|
|
152
482
|
}
|
|
153
483
|
|
|
484
|
+
const parsedCommand = parseConnectorCommand(msg.text || '')
|
|
485
|
+
if (parsedCommand?.name === 'pair') {
|
|
486
|
+
const commandResult = await handlePairCommand({
|
|
487
|
+
connector,
|
|
488
|
+
msg,
|
|
489
|
+
args: parsedCommand.args,
|
|
490
|
+
})
|
|
491
|
+
logExecution(session.id, 'decision', 'Connector pair command handled', {
|
|
492
|
+
agentId: agent.id,
|
|
493
|
+
detail: {
|
|
494
|
+
platform: msg.platform,
|
|
495
|
+
channelId: msg.channelId,
|
|
496
|
+
command: 'pair',
|
|
497
|
+
args: parsedCommand.args || null,
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
return commandResult
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const accessPolicyResult = enforceInboundAccessPolicy(connector, msg)
|
|
504
|
+
if (accessPolicyResult) {
|
|
505
|
+
logExecution(session.id, 'decision', 'Connector inbound blocked by access policy', {
|
|
506
|
+
agentId: agent.id,
|
|
507
|
+
detail: {
|
|
508
|
+
platform: msg.platform,
|
|
509
|
+
channelId: msg.channelId,
|
|
510
|
+
senderId: msg.senderId,
|
|
511
|
+
policy: parsePairingPolicy(connector.config?.dmPolicy, 'open'),
|
|
512
|
+
},
|
|
513
|
+
})
|
|
514
|
+
return accessPolicyResult
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (parsedCommand) {
|
|
518
|
+
const commandResult = await handleConnectorCommand({
|
|
519
|
+
command: parsedCommand,
|
|
520
|
+
connector,
|
|
521
|
+
session,
|
|
522
|
+
msg,
|
|
523
|
+
agentName: agent.name,
|
|
524
|
+
})
|
|
525
|
+
logExecution(session.id, 'decision', `Connector command handled: /${parsedCommand.name}`, {
|
|
526
|
+
agentId: agent.id,
|
|
527
|
+
detail: {
|
|
528
|
+
platform: msg.platform,
|
|
529
|
+
channelId: msg.channelId,
|
|
530
|
+
command: parsedCommand.name,
|
|
531
|
+
args: parsedCommand.args || null,
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
return commandResult
|
|
535
|
+
}
|
|
536
|
+
|
|
154
537
|
// Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt]
|
|
155
538
|
const settings = loadSettings()
|
|
156
539
|
const promptParts: string[] = []
|
|
@@ -164,6 +547,12 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
164
547
|
if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
165
548
|
}
|
|
166
549
|
}
|
|
550
|
+
const thinkLevel = typeof session.connectorThinkLevel === 'string'
|
|
551
|
+
? session.connectorThinkLevel.trim().toLowerCase()
|
|
552
|
+
: ''
|
|
553
|
+
if (thinkLevel) {
|
|
554
|
+
promptParts.push(`Connector thinking guidance: ${thinkLevel}. Keep responses concise and useful for chat.`)
|
|
555
|
+
}
|
|
167
556
|
// Add connector context
|
|
168
557
|
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
558
|
|
|
@@ -210,9 +599,10 @@ The test: would a thoughtful friend feel compelled to type something back? If no
|
|
|
210
599
|
// Use finalResponse for connectors — strips intermediate planning/tool-use text
|
|
211
600
|
fullText = result.finalResponse
|
|
212
601
|
console.log(`[connector] streamAgentChat returned ${result.fullText.length} chars total, ${fullText.length} chars final`)
|
|
213
|
-
} catch (err:
|
|
214
|
-
|
|
215
|
-
|
|
602
|
+
} catch (err: unknown) {
|
|
603
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
604
|
+
console.error(`[connector] streamAgentChat error:`, message)
|
|
605
|
+
return `[Error] ${message}`
|
|
216
606
|
}
|
|
217
607
|
} else {
|
|
218
608
|
// Use the provider directly
|
|
@@ -326,6 +716,9 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
326
716
|
if (!botToken && connector.config.botToken) {
|
|
327
717
|
botToken = connector.config.botToken
|
|
328
718
|
}
|
|
719
|
+
if (!botToken && connector.platform === 'bluebubbles' && connector.config.password) {
|
|
720
|
+
botToken = connector.config.password
|
|
721
|
+
}
|
|
329
722
|
|
|
330
723
|
if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal') {
|
|
331
724
|
throw new Error('No bot token configured')
|
|
@@ -347,10 +740,10 @@ async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
|
347
740
|
notify('connectors')
|
|
348
741
|
|
|
349
742
|
console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
|
|
350
|
-
} catch (err:
|
|
743
|
+
} catch (err: unknown) {
|
|
351
744
|
connector.status = 'error'
|
|
352
745
|
connector.isEnabled = false
|
|
353
|
-
connector.lastError = err.message
|
|
746
|
+
connector.lastError = err instanceof Error ? err.message : String(err)
|
|
354
747
|
connector.updatedAt = Date.now()
|
|
355
748
|
connectors[connectorId] = connector
|
|
356
749
|
saveConnectors(connectors)
|
|
@@ -439,8 +832,8 @@ export async function autoStartConnectors(): Promise<void> {
|
|
|
439
832
|
try {
|
|
440
833
|
console.log(`[connector] Auto-starting ${connector.platform} connector: ${connector.name}`)
|
|
441
834
|
await startConnector(connector.id)
|
|
442
|
-
} catch (err:
|
|
443
|
-
console.error(`[connector] Failed to auto-start ${connector.name}:`, err.message)
|
|
835
|
+
} catch (err: unknown) {
|
|
836
|
+
console.error(`[connector] Failed to auto-start ${connector.name}:`, err instanceof Error ? err.message : err)
|
|
444
837
|
}
|
|
445
838
|
}
|
|
446
839
|
}
|
|
@@ -475,6 +868,11 @@ export function listRunningConnectors(platform?: string): Array<{
|
|
|
475
868
|
if (outboundJid) configuredTargets.push(outboundJid)
|
|
476
869
|
const allowed = connector.config?.allowedJids?.split(',').map((s) => s.trim()).filter(Boolean) || []
|
|
477
870
|
configuredTargets.push(...allowed)
|
|
871
|
+
} else if (connector.platform === 'bluebubbles') {
|
|
872
|
+
const outbound = connector.config?.outboundTarget?.trim()
|
|
873
|
+
if (outbound) configuredTargets.push(outbound)
|
|
874
|
+
const allowed = connector.config?.allowFrom?.split(',').map((s) => s.trim()).filter(Boolean) || []
|
|
875
|
+
configuredTargets.push(...allowed)
|
|
478
876
|
}
|
|
479
877
|
out.push({
|
|
480
878
|
id,
|
|
@@ -494,6 +892,19 @@ export function getConnectorRecentChannelId(connectorId: string): string | null
|
|
|
494
892
|
return lastInboundChannelByConnector.get(connectorId) || null
|
|
495
893
|
}
|
|
496
894
|
|
|
895
|
+
/** Get presence info for a connector */
|
|
896
|
+
export function getConnectorPresence(connectorId: string): { lastMessageAt: number | null; channelId: string | null } {
|
|
897
|
+
return {
|
|
898
|
+
lastMessageAt: lastInboundTimeByConnector.get(connectorId) ?? null,
|
|
899
|
+
channelId: lastInboundChannelByConnector.get(connectorId) ?? null,
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Get a running connector instance (internal use for rich messaging). */
|
|
904
|
+
export function getRunningInstance(connectorId: string): ConnectorInstance | undefined {
|
|
905
|
+
return running.get(connectorId)
|
|
906
|
+
}
|
|
907
|
+
|
|
497
908
|
/**
|
|
498
909
|
* Send an outbound message through a running connector.
|
|
499
910
|
* 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,14 @@ 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
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
834
|
+
const { setSharedDeviceToken } = require('../openclaw-sync')
|
|
835
|
+
setSharedDeviceToken(normalized)
|
|
836
|
+
} catch { /* openclaw-sync not available */ }
|
|
837
|
+
}
|
|
830
838
|
}
|
|
831
839
|
|
|
832
840
|
function clearStaleTokenIfNeeded(reason?: string) {
|
|
@@ -928,6 +936,23 @@ const openclaw: PlatformConnector = {
|
|
|
928
936
|
source: 'event' | 'history',
|
|
929
937
|
) {
|
|
930
938
|
if (!matchesSessionKey(configuredSessionFilter, inbound.channelId)) return
|
|
939
|
+
|
|
940
|
+
// Multi-agent routing: match sessionKey against agentRouting config
|
|
941
|
+
const agentRouting = connector.config.agentRouting
|
|
942
|
+
if (agentRouting) {
|
|
943
|
+
try {
|
|
944
|
+
const routingMap: Record<string, string> = typeof agentRouting === 'string'
|
|
945
|
+
? JSON.parse(agentRouting)
|
|
946
|
+
: agentRouting as unknown as Record<string, string>
|
|
947
|
+
for (const [pattern, agentId] of Object.entries(routingMap)) {
|
|
948
|
+
if (matchesSessionKey(pattern, inbound.channelId)) {
|
|
949
|
+
inbound.agentIdOverride = agentId
|
|
950
|
+
break
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
} catch { /* ignore malformed routing config */ }
|
|
954
|
+
}
|
|
955
|
+
|
|
931
956
|
if (!rememberSeenEntry(seenInbound, dedupeKey, MAX_SEEN_CHAT_EVENTS)) return
|
|
932
957
|
|
|
933
958
|
const now = Date.now()
|
|
@@ -1120,6 +1145,46 @@ const openclaw: PlatformConnector = {
|
|
|
1120
1145
|
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1121
1146
|
await sendChat(channelId || defaultSessionKey, text, options)
|
|
1122
1147
|
},
|
|
1148
|
+
async sendReaction(channelId, messageId, emoji) {
|
|
1149
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1150
|
+
try {
|
|
1151
|
+
await rpcRequest('chat.react', { sessionKey: channelId || defaultSessionKey, messageId, emoji })
|
|
1152
|
+
} catch (err: unknown) {
|
|
1153
|
+
const msg = getErrorMessage(err)
|
|
1154
|
+
if (msg.toLowerCase().includes('unknown method')) return // graceful degrade
|
|
1155
|
+
throw err
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
async editMessage(channelId, messageId, newText) {
|
|
1159
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1160
|
+
try {
|
|
1161
|
+
await rpcRequest('chat.edit', { sessionKey: channelId || defaultSessionKey, messageId, text: newText })
|
|
1162
|
+
} catch (err: unknown) {
|
|
1163
|
+
const msg = getErrorMessage(err)
|
|
1164
|
+
if (msg.toLowerCase().includes('unknown method')) return
|
|
1165
|
+
throw err
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
async deleteMessage(channelId, messageId) {
|
|
1169
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1170
|
+
try {
|
|
1171
|
+
await rpcRequest('chat.delete', { sessionKey: channelId || defaultSessionKey, messageId })
|
|
1172
|
+
} catch (err: unknown) {
|
|
1173
|
+
const msg = getErrorMessage(err)
|
|
1174
|
+
if (msg.toLowerCase().includes('unknown method')) return
|
|
1175
|
+
throw err
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
async pinMessage(channelId, messageId) {
|
|
1179
|
+
if (!connected) throw new Error('openclaw connector is not connected')
|
|
1180
|
+
try {
|
|
1181
|
+
await rpcRequest('chat.pin', { sessionKey: channelId || defaultSessionKey, messageId })
|
|
1182
|
+
} catch (err: unknown) {
|
|
1183
|
+
const msg = getErrorMessage(err)
|
|
1184
|
+
if (msg.toLowerCase().includes('unknown method')) return
|
|
1185
|
+
throw err
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1123
1188
|
async stop() {
|
|
1124
1189
|
stopped = true
|
|
1125
1190
|
cleanupSocket()
|