@swarmclawai/swarmclaw 0.6.0 → 0.6.3
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 +56 -42
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +16 -35
- package/src/app/api/tts/stream/route.ts +14 -42
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +76 -24
- package/src/components/chat/chat-header.tsx +522 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +113 -8
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +84 -17
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +125 -14
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/connector-routing.test.ts +118 -1
- package/src/lib/server/connectors/discord.ts +31 -8
- package/src/lib/server/connectors/manager.ts +594 -16
- package/src/lib/server/connectors/media.ts +5 -0
- package/src/lib/server/connectors/telegram.ts +12 -2
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +28 -2
- package/src/lib/server/elevenlabs.test.ts +60 -0
- package/src/lib/server/elevenlabs.ts +103 -0
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +182 -8
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +583 -63
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/file.ts +26 -7
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +118 -8
- package/src/lib/server/stream-agent-chat.ts +39 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -1,107 +1,619 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
|
|
6
|
+
import { genId } from '@/lib/id'
|
|
7
|
+
import { synthesizeElevenLabsMp3 } from '../elevenlabs'
|
|
4
8
|
import type { ToolBuildContext } from './context'
|
|
5
9
|
|
|
10
|
+
const CONNECTOR_ACTION_DEDUPE_TTL_MS = 30_000
|
|
11
|
+
const CONNECTOR_TURN_SEND_TTL_MS = 180_000
|
|
12
|
+
const AUTONOMOUS_OUTREACH_COOLDOWN_MS = 2 * 60 * 60 * 1000
|
|
13
|
+
const recentConnectorActionCache = new Map<string, { at: number; result: string }>()
|
|
14
|
+
const connectorTurnSendBudget = new Map<string, { count: number; at: number; lastResult?: string }>()
|
|
15
|
+
const autonomousOutreachBudget = new Map<string, { at: number; result?: string }>()
|
|
16
|
+
|
|
17
|
+
function pruneOldConnectorToolState(now: number): void {
|
|
18
|
+
for (const [key, entry] of recentConnectorActionCache.entries()) {
|
|
19
|
+
if (now - entry.at > CONNECTOR_ACTION_DEDUPE_TTL_MS) recentConnectorActionCache.delete(key)
|
|
20
|
+
}
|
|
21
|
+
for (const [key, entry] of connectorTurnSendBudget.entries()) {
|
|
22
|
+
if (now - entry.at > CONNECTOR_TURN_SEND_TTL_MS) connectorTurnSendBudget.delete(key)
|
|
23
|
+
}
|
|
24
|
+
for (const [key, entry] of autonomousOutreachBudget.entries()) {
|
|
25
|
+
if (now - entry.at > AUTONOMOUS_OUTREACH_COOLDOWN_MS) autonomousOutreachBudget.delete(key)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseLatestUserTurn(
|
|
30
|
+
session: { messages?: Array<Record<string, unknown>> } | null | undefined,
|
|
31
|
+
): { text: string; time: number } {
|
|
32
|
+
const msgs = Array.isArray(session?.messages) ? session.messages : []
|
|
33
|
+
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
|
34
|
+
const msg = msgs[i]
|
|
35
|
+
if (String(msg?.role || '') !== 'user') continue
|
|
36
|
+
const text = typeof msg.text === 'string' ? msg.text.trim() : ''
|
|
37
|
+
const time = typeof msg.time === 'number' ? msg.time : 0
|
|
38
|
+
return { text, time }
|
|
39
|
+
}
|
|
40
|
+
return { text: '', time: 0 }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function userExplicitlyWantsMultipleOutbound(userText: string): boolean {
|
|
44
|
+
if (!userText) return false
|
|
45
|
+
const text = userText.toLowerCase()
|
|
46
|
+
return /\b(both|multiple|all of them|all numbers|two messages|three messages|each number|every number|and also|plus also|send again|resend)\b/.test(text)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function userExplicitlyRequestedFollowup(userText: string): boolean {
|
|
50
|
+
if (!userText) return false
|
|
51
|
+
const text = userText.toLowerCase()
|
|
52
|
+
if (/connector_message_tool/.test(text) && /(schedule_followup|followupmessage|followup|delaysec|follow.?up)/.test(text)) return true
|
|
53
|
+
return /\b(follow[ -]?up|check[ -]?in|remind(?: me)?|later|tomorrow|in \d+\s*(sec|secs|second|seconds|min|mins|minute|minutes|hour|hours|day|days))\b/.test(text)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isAutonomousSystemTurn(userText: string): boolean {
|
|
57
|
+
if (!userText) return false
|
|
58
|
+
const text = userText.toUpperCase()
|
|
59
|
+
return text.includes('AGENT_HEARTBEAT_WAKE')
|
|
60
|
+
|| text.includes('SWARM_MAIN_MISSION_TICK')
|
|
61
|
+
|| text.includes('SWARM_MAIN_AUTO_FOLLOWUP')
|
|
62
|
+
|| text.includes('SWARM_HEARTBEAT_CHECK')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isSignificantOutreachText(raw: string): boolean {
|
|
66
|
+
const text = (raw || '').trim().toLowerCase()
|
|
67
|
+
if (text.length < 12) return false
|
|
68
|
+
if (/\b(just checking in|checking in|touching base|quick check-in|hope you'?re well|any updates\??)\b/.test(text)) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
return /\b(completed|complete|done|finished|failed|failure|error|blocked|urgent|important|deadline|overdue|incident|warning|reminder|birthday|anniversary|milestone|congrats|congratulations|celebrate|payment|invoice|appointment|meeting)\b/.test(text)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isUrgentOutreachText(raw: string): boolean {
|
|
75
|
+
const text = (raw || '').toLowerCase()
|
|
76
|
+
return /\b(urgent|immediately|asap|critical|incident|outage|failed|failure|blocked|overdue|deadline)\b/.test(text)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildConnectorActionKey(parts: Array<string | number | boolean | null | undefined>): string {
|
|
80
|
+
return parts.map((part) => String(part ?? '')).join('|')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeDedupedReplayResult(raw: string, fallback: { connectorId: string; platform: string; to: string }): string {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw)
|
|
86
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid')
|
|
87
|
+
const record = parsed as Record<string, unknown>
|
|
88
|
+
if (String(record.status || '') === 'deduped') {
|
|
89
|
+
return JSON.stringify({
|
|
90
|
+
status: 'sent',
|
|
91
|
+
connectorId: String(record.connectorId || fallback.connectorId),
|
|
92
|
+
platform: String(record.platform || fallback.platform),
|
|
93
|
+
to: String(record.to || fallback.to),
|
|
94
|
+
deduped: true,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
return raw
|
|
98
|
+
} catch {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
status: 'sent',
|
|
101
|
+
connectorId: fallback.connectorId,
|
|
102
|
+
platform: fallback.platform,
|
|
103
|
+
to: fallback.to,
|
|
104
|
+
deduped: true,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Resolve /api/uploads/filename URLs to actual disk paths */
|
|
110
|
+
function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeType?: string } | null {
|
|
111
|
+
if (!url) return null
|
|
112
|
+
const match = url.match(/^\/api\/uploads\/([^?#]+)/)
|
|
113
|
+
if (!match) return null
|
|
114
|
+
// Decode URL-encoded filenames (e.g. from encodeURIComponent) before sanitizing
|
|
115
|
+
let decoded: string
|
|
116
|
+
try { decoded = decodeURIComponent(match[1]) } catch { decoded = match[1] }
|
|
117
|
+
const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
118
|
+
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
119
|
+
if (!fs.existsSync(filePath)) return null
|
|
120
|
+
return { mediaPath: filePath }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeWhatsAppTarget(input: string): string {
|
|
124
|
+
const raw = input.trim()
|
|
125
|
+
if (!raw) return raw
|
|
126
|
+
if (raw.includes('@')) return raw
|
|
127
|
+
let cleaned = raw.replace(/[^\d+]/g, '')
|
|
128
|
+
if (cleaned.startsWith('+')) cleaned = cleaned.slice(1)
|
|
129
|
+
if (cleaned.startsWith('0') && cleaned.length >= 10) {
|
|
130
|
+
cleaned = `44${cleaned.slice(1)}`
|
|
131
|
+
}
|
|
132
|
+
cleaned = cleaned.replace(/[^\d]/g, '')
|
|
133
|
+
return cleaned ? `${cleaned}@s.whatsapp.net` : raw
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseCsv(raw: string | undefined): string[] {
|
|
137
|
+
if (!raw) return []
|
|
138
|
+
return raw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pickChannelTarget(params: {
|
|
142
|
+
connector: { config?: Record<string, string> }
|
|
143
|
+
to?: string
|
|
144
|
+
recentChannelId: string | null
|
|
145
|
+
}): { channelId: string; error?: string } {
|
|
146
|
+
let channelId = params.to?.trim() || ''
|
|
147
|
+
const connector = params.connector
|
|
148
|
+
|
|
149
|
+
if (!channelId) {
|
|
150
|
+
const outbound = connector.config?.outboundJid?.trim()
|
|
151
|
+
if (outbound) channelId = outbound
|
|
152
|
+
}
|
|
153
|
+
if (!channelId) {
|
|
154
|
+
const outbound = connector.config?.outboundTarget?.trim()
|
|
155
|
+
if (outbound) channelId = outbound
|
|
156
|
+
}
|
|
157
|
+
if (!channelId && params.recentChannelId) {
|
|
158
|
+
channelId = params.recentChannelId
|
|
159
|
+
}
|
|
160
|
+
if (!channelId) {
|
|
161
|
+
const allowed = parseCsv(connector.config?.allowedJids)
|
|
162
|
+
if (allowed.length) channelId = allowed[0]
|
|
163
|
+
}
|
|
164
|
+
if (!channelId) {
|
|
165
|
+
const allowed = parseCsv(connector.config?.allowFrom)
|
|
166
|
+
if (allowed.length) channelId = allowed[0]
|
|
167
|
+
}
|
|
168
|
+
if (!channelId) {
|
|
169
|
+
const knownTargets = [
|
|
170
|
+
connector.config?.outboundJid?.trim(),
|
|
171
|
+
connector.config?.outboundTarget?.trim(),
|
|
172
|
+
...parseCsv(connector.config?.allowedJids),
|
|
173
|
+
...parseCsv(connector.config?.allowFrom),
|
|
174
|
+
].filter(Boolean) as string[]
|
|
175
|
+
const unique = [...new Set(knownTargets)]
|
|
176
|
+
if (unique.length) {
|
|
177
|
+
return {
|
|
178
|
+
channelId: '',
|
|
179
|
+
error: `Error: no default outbound target is set, but the connector has ${unique.length} configured number(s)/target(s): ${JSON.stringify(unique)}. Ask the user which one to send to, then re-call with the "to" parameter set to their choice.`,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
channelId: '',
|
|
184
|
+
error: 'Error: no target recipient configured and no known contacts on this connector. Ask the user for the recipient number/ID, then re-call with the "to" parameter. They can also configure "allowedJids" or "outboundJid" in the connector settings.',
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { channelId }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveConnectorMediaInput(params: {
|
|
191
|
+
cwd: string
|
|
192
|
+
mediaPath?: string
|
|
193
|
+
imageUrl?: string
|
|
194
|
+
fileUrl?: string
|
|
195
|
+
}): { mediaPath?: string; imageUrl?: string; fileUrl?: string; error?: string } {
|
|
196
|
+
let resolvedMediaPath = params.mediaPath?.trim() || undefined
|
|
197
|
+
let resolvedImageUrl = params.imageUrl?.trim() || undefined
|
|
198
|
+
let resolvedFileUrl = params.fileUrl?.trim() || undefined
|
|
199
|
+
|
|
200
|
+
if (resolvedMediaPath && !path.isAbsolute(resolvedMediaPath) && !resolvedMediaPath.startsWith('/api/uploads/')) {
|
|
201
|
+
const candidatePaths = [
|
|
202
|
+
path.resolve(params.cwd, resolvedMediaPath),
|
|
203
|
+
path.resolve(params.cwd, 'uploads', resolvedMediaPath),
|
|
204
|
+
path.join(UPLOAD_DIR, resolvedMediaPath),
|
|
205
|
+
path.join(UPLOAD_DIR, path.basename(resolvedMediaPath)),
|
|
206
|
+
]
|
|
207
|
+
const found = candidatePaths.find((p) => fs.existsSync(p))
|
|
208
|
+
if (found) {
|
|
209
|
+
resolvedMediaPath = found
|
|
210
|
+
} else {
|
|
211
|
+
return {
|
|
212
|
+
error: `Error: File not found. Tried: ${candidatePaths.join(', ')}. Use an absolute path or ensure the file exists in the session workspace.`,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!resolvedMediaPath) {
|
|
218
|
+
const fromImage = resolveUploadUrl(resolvedImageUrl)
|
|
219
|
+
if (fromImage) {
|
|
220
|
+
resolvedMediaPath = fromImage.mediaPath
|
|
221
|
+
resolvedImageUrl = undefined
|
|
222
|
+
}
|
|
223
|
+
const fromFile = resolveUploadUrl(resolvedFileUrl)
|
|
224
|
+
if (fromFile) {
|
|
225
|
+
resolvedMediaPath = fromFile.mediaPath
|
|
226
|
+
resolvedFileUrl = undefined
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
mediaPath: resolvedMediaPath,
|
|
232
|
+
imageUrl: resolvedImageUrl,
|
|
233
|
+
fileUrl: resolvedFileUrl,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
6
237
|
export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
7
238
|
const tools: StructuredToolInterface[] = []
|
|
8
|
-
const {
|
|
239
|
+
const { hasTool } = bctx
|
|
9
240
|
|
|
10
241
|
if (hasTool('manage_connectors')) {
|
|
242
|
+
const settings = loadSettings()
|
|
243
|
+
const hasElevenLabsKey = !!String(settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY || '').trim()
|
|
244
|
+
const voiceNoteToolEnabled = settings.elevenLabsEnabled === true && hasElevenLabsKey
|
|
245
|
+
const actionSchema = voiceNoteToolEnabled
|
|
246
|
+
? z.enum([
|
|
247
|
+
'list_running',
|
|
248
|
+
'list_targets',
|
|
249
|
+
'start',
|
|
250
|
+
'stop',
|
|
251
|
+
'send',
|
|
252
|
+
'send_voice_note',
|
|
253
|
+
'schedule_followup',
|
|
254
|
+
'message_react',
|
|
255
|
+
'message_edit',
|
|
256
|
+
'message_delete',
|
|
257
|
+
'message_pin',
|
|
258
|
+
] as const)
|
|
259
|
+
: z.enum([
|
|
260
|
+
'list_running',
|
|
261
|
+
'list_targets',
|
|
262
|
+
'start',
|
|
263
|
+
'stop',
|
|
264
|
+
'send',
|
|
265
|
+
'schedule_followup',
|
|
266
|
+
'message_react',
|
|
267
|
+
'message_edit',
|
|
268
|
+
'message_delete',
|
|
269
|
+
'message_pin',
|
|
270
|
+
] as const)
|
|
11
271
|
tools.push(
|
|
12
272
|
tool(
|
|
13
|
-
async ({
|
|
273
|
+
async ({
|
|
274
|
+
action,
|
|
275
|
+
connectorId,
|
|
276
|
+
platform,
|
|
277
|
+
to,
|
|
278
|
+
message,
|
|
279
|
+
voiceText,
|
|
280
|
+
voiceId,
|
|
281
|
+
imageUrl,
|
|
282
|
+
fileUrl,
|
|
283
|
+
mediaPath,
|
|
284
|
+
mimeType,
|
|
285
|
+
fileName,
|
|
286
|
+
caption,
|
|
287
|
+
delaySec,
|
|
288
|
+
followUpMessage,
|
|
289
|
+
followUpDelaySec,
|
|
290
|
+
approved,
|
|
291
|
+
ptt,
|
|
292
|
+
}) => {
|
|
14
293
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (!raw) return raw
|
|
18
|
-
if (raw.includes('@')) return raw
|
|
19
|
-
let cleaned = raw.replace(/[^\d+]/g, '')
|
|
20
|
-
if (cleaned.startsWith('+')) cleaned = cleaned.slice(1)
|
|
21
|
-
if (cleaned.startsWith('0') && cleaned.length >= 10) {
|
|
22
|
-
cleaned = '44' + cleaned.slice(1)
|
|
23
|
-
}
|
|
24
|
-
cleaned = cleaned.replace(/[^\d]/g, '')
|
|
25
|
-
return cleaned ? `${cleaned}@s.whatsapp.net` : raw
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId } = await import('../connectors/manager')
|
|
294
|
+
const actionName = String(action)
|
|
295
|
+
const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId, scheduleConnectorFollowUp } = await import('../connectors/manager')
|
|
29
296
|
const running = listRunningConnectors(platform || undefined)
|
|
30
297
|
|
|
31
|
-
if (
|
|
298
|
+
if (actionName === 'list_running' || actionName === 'list_targets') {
|
|
32
299
|
return JSON.stringify(running)
|
|
33
300
|
}
|
|
34
301
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
302
|
+
if (actionName === 'start') {
|
|
303
|
+
if (!connectorId) {
|
|
304
|
+
// If no ID given, list available connectors to start
|
|
305
|
+
const allConnectors = loadConnectors()
|
|
306
|
+
const stopped = Object.values(allConnectors)
|
|
307
|
+
.filter((c) => !platform || c.platform === platform)
|
|
308
|
+
.filter((c) => !running.find((r) => r.id === c.id))
|
|
309
|
+
.map((c) => ({ id: c.id, name: c.name, platform: c.platform }))
|
|
310
|
+
if (!stopped.length) return 'All connectors are already running.'
|
|
311
|
+
return `Error: connectorId is required. Stopped connectors available to start: ${JSON.stringify(stopped)}`
|
|
39
312
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
313
|
+
const { startConnector: doStart } = await import('../connectors/manager')
|
|
314
|
+
await doStart(connectorId)
|
|
315
|
+
return JSON.stringify({ status: 'started', connectorId })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (actionName === 'stop') {
|
|
319
|
+
if (!connectorId) return 'Error: connectorId is required for stop action.'
|
|
320
|
+
const { stopConnector: doStop } = await import('../connectors/manager')
|
|
321
|
+
await doStop(connectorId)
|
|
322
|
+
return JSON.stringify({ status: 'stopped', connectorId })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const resolveSelectedConnector = () => {
|
|
43
326
|
if (!running.length) {
|
|
44
|
-
|
|
327
|
+
const allConnectors = loadConnectors()
|
|
328
|
+
const configured = Object.values(allConnectors)
|
|
329
|
+
.filter((c) => !platform || c.platform === platform)
|
|
330
|
+
.map((c) => ({ id: c.id, name: c.name, platform: c.platform, agentId: c.agentId || null }))
|
|
331
|
+
if (configured.length) {
|
|
332
|
+
return {
|
|
333
|
+
error: `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}, but ${configured.length} configured connector(s) found: ${JSON.stringify(configured)}. These connectors exist but are not currently started. Ask the user if they'd like you to start one (use action "start" with the connectorId), then retry the send.`,
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
error: `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}. No connectors are configured for this platform either — the user needs to set one up in the Connectors panel first.`,
|
|
338
|
+
}
|
|
45
339
|
}
|
|
46
|
-
|
|
47
340
|
const selected = connectorId
|
|
48
341
|
? running.find((c) => c.id === connectorId)
|
|
49
342
|
: running[0]
|
|
50
|
-
if (!selected) return `Error: running connector not found: ${connectorId}`
|
|
51
|
-
|
|
343
|
+
if (!selected) return { error: `Error: running connector not found: ${connectorId}` }
|
|
52
344
|
const connectors = loadConnectors()
|
|
53
345
|
const connector = connectors[selected.id]
|
|
54
|
-
if (!connector) return `Error: connector not found: ${selected.id}`
|
|
346
|
+
if (!connector) return { error: `Error: connector not found: ${selected.id}` }
|
|
347
|
+
return { selected, connector }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (actionName === 'send' || actionName === 'send_voice_note' || actionName === 'schedule_followup') {
|
|
351
|
+
const settings = loadSettings()
|
|
352
|
+
if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
|
|
353
|
+
return 'Error: outbound connector sends require explicit approval. Re-run with approved=true after user confirmation.'
|
|
354
|
+
}
|
|
355
|
+
const now = Date.now()
|
|
356
|
+
pruneOldConnectorToolState(now)
|
|
357
|
+
const resolved = resolveSelectedConnector()
|
|
358
|
+
if ('error' in resolved) return resolved.error
|
|
359
|
+
const { selected, connector } = resolved
|
|
360
|
+
|
|
361
|
+
const target = pickChannelTarget({
|
|
362
|
+
connector,
|
|
363
|
+
to,
|
|
364
|
+
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
365
|
+
})
|
|
366
|
+
if (target.error) return target.error
|
|
367
|
+
|
|
368
|
+
let channelId = target.channelId
|
|
369
|
+
if (connector.platform === 'whatsapp') channelId = normalizeWhatsAppTarget(channelId)
|
|
55
370
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
371
|
+
const currentSession = bctx.resolveCurrentSession()
|
|
372
|
+
const latestUserTurn = parseLatestUserTurn(currentSession)
|
|
373
|
+
const sessionId = bctx.ctx?.sessionId || currentSession?.id || 'unknown-session'
|
|
374
|
+
const turnKey = buildConnectorActionKey([sessionId, latestUserTurn.time || 'no-user-turn'])
|
|
375
|
+
const multiOutboundAllowed = userExplicitlyWantsMultipleOutbound(latestUserTurn.text)
|
|
376
|
+
const followupExplicitlyRequested = userExplicitlyRequestedFollowup(latestUserTurn.text)
|
|
377
|
+
const autonomousTurn = isAutonomousSystemTurn(latestUserTurn.text)
|
|
378
|
+
const existingBudget = connectorTurnSendBudget.get(turnKey)
|
|
379
|
+
if (
|
|
380
|
+
!multiOutboundAllowed
|
|
381
|
+
&& existingBudget
|
|
382
|
+
&& now - existingBudget.at <= CONNECTOR_TURN_SEND_TTL_MS
|
|
383
|
+
&& existingBudget.count >= 1
|
|
384
|
+
) {
|
|
385
|
+
if (existingBudget.lastResult) {
|
|
386
|
+
return normalizeDedupedReplayResult(existingBudget.lastResult, {
|
|
387
|
+
connectorId: selected.id,
|
|
388
|
+
platform: selected.platform,
|
|
389
|
+
to: channelId,
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
return JSON.stringify({
|
|
393
|
+
status: 'sent',
|
|
394
|
+
connectorId: selected.id,
|
|
395
|
+
platform: selected.platform,
|
|
396
|
+
to: channelId,
|
|
397
|
+
deduped: true,
|
|
398
|
+
})
|
|
60
399
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
400
|
+
|
|
401
|
+
if (actionName === 'send_voice_note') {
|
|
402
|
+
if (!voiceNoteToolEnabled) {
|
|
403
|
+
return 'Error: send_voice_note is unavailable. Enable ElevenLabs in Settings > Voice and set a valid API key.'
|
|
404
|
+
}
|
|
405
|
+
const ttsText = (voiceText || message || '').trim()
|
|
406
|
+
if (!ttsText) return 'Error: voiceText or message is required for send_voice_note action.'
|
|
407
|
+
const voiceActionKey = buildConnectorActionKey([
|
|
408
|
+
sessionId,
|
|
409
|
+
actionName,
|
|
410
|
+
selected.id,
|
|
411
|
+
channelId,
|
|
412
|
+
ttsText,
|
|
413
|
+
voiceId?.trim() || '',
|
|
414
|
+
fileName?.trim() || '',
|
|
415
|
+
caption?.trim() || '',
|
|
416
|
+
ptt ?? true,
|
|
417
|
+
])
|
|
418
|
+
const cachedVoice = recentConnectorActionCache.get(voiceActionKey)
|
|
419
|
+
if (cachedVoice && now - cachedVoice.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
|
|
420
|
+
return cachedVoice.result
|
|
421
|
+
}
|
|
422
|
+
const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
|
|
423
|
+
const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
|
|
424
|
+
const voicePath = path.join(UPLOAD_DIR, voiceFileName)
|
|
425
|
+
fs.writeFileSync(voicePath, audioBuffer)
|
|
426
|
+
|
|
427
|
+
const sent = await sendConnectorMessage({
|
|
428
|
+
connectorId: selected.id,
|
|
429
|
+
channelId,
|
|
430
|
+
text: '',
|
|
431
|
+
mediaPath: voicePath,
|
|
432
|
+
mimeType: 'audio/mpeg',
|
|
433
|
+
fileName: fileName?.trim() || 'voicenote.mp3',
|
|
434
|
+
caption: caption?.trim() || undefined,
|
|
435
|
+
ptt: ptt ?? true,
|
|
436
|
+
})
|
|
437
|
+
const result = JSON.stringify({
|
|
438
|
+
status: 'voice_sent',
|
|
439
|
+
connectorId: sent.connectorId,
|
|
440
|
+
platform: sent.platform,
|
|
441
|
+
to: sent.channelId,
|
|
442
|
+
messageId: sent.messageId || null,
|
|
443
|
+
voiceFile: voicePath,
|
|
444
|
+
})
|
|
445
|
+
connectorTurnSendBudget.set(turnKey, {
|
|
446
|
+
count: (existingBudget?.count || 0) + 1,
|
|
447
|
+
at: now,
|
|
448
|
+
lastResult: result,
|
|
449
|
+
})
|
|
450
|
+
recentConnectorActionCache.set(voiceActionKey, { at: now, result })
|
|
451
|
+
return result
|
|
64
452
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
453
|
+
|
|
454
|
+
const media = resolveConnectorMediaInput({
|
|
455
|
+
cwd: bctx.cwd,
|
|
456
|
+
mediaPath,
|
|
457
|
+
imageUrl,
|
|
458
|
+
fileUrl,
|
|
459
|
+
})
|
|
460
|
+
if (media.error) return media.error
|
|
461
|
+
|
|
462
|
+
const hasText = !!message?.trim()
|
|
463
|
+
const hasMedia = !!media.mediaPath || !!media.imageUrl || !!media.fileUrl
|
|
464
|
+
if (actionName === 'send' && !hasText && !hasMedia) {
|
|
465
|
+
return 'Error: message, media URL, or mediaPath is required for send action.'
|
|
68
466
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
467
|
+
|
|
468
|
+
let followUpText = followUpMessage?.trim() || ''
|
|
469
|
+
const followDelaySec = Number.isFinite(followUpDelaySec) ? Number(followUpDelaySec) : 300
|
|
470
|
+
|
|
471
|
+
const proactivePayload = followUpText || message?.trim() || ''
|
|
472
|
+
const significantAutonomousOutreach = autonomousTurn && isSignificantOutreachText(proactivePayload)
|
|
473
|
+
const urgentAutonomousOutreach = autonomousTurn && isUrgentOutreachText(proactivePayload)
|
|
474
|
+
const outreachBudgetKey = buildConnectorActionKey([selected.id, channelId])
|
|
475
|
+
const priorAutonomousOutreach = autonomousOutreachBudget.get(outreachBudgetKey)
|
|
476
|
+
if (
|
|
477
|
+
autonomousTurn
|
|
478
|
+
&& significantAutonomousOutreach
|
|
479
|
+
&& priorAutonomousOutreach
|
|
480
|
+
&& !urgentAutonomousOutreach
|
|
481
|
+
&& now - priorAutonomousOutreach.at <= AUTONOMOUS_OUTREACH_COOLDOWN_MS
|
|
482
|
+
) {
|
|
483
|
+
if (priorAutonomousOutreach.result) {
|
|
484
|
+
return normalizeDedupedReplayResult(priorAutonomousOutreach.result, {
|
|
485
|
+
connectorId: selected.id,
|
|
486
|
+
platform: selected.platform,
|
|
487
|
+
to: channelId,
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
return JSON.stringify({
|
|
491
|
+
status: 'sent',
|
|
492
|
+
connectorId: selected.id,
|
|
493
|
+
platform: selected.platform,
|
|
494
|
+
to: channelId,
|
|
495
|
+
deduped: true,
|
|
496
|
+
})
|
|
72
497
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
498
|
+
|
|
499
|
+
if (followUpText && !followupExplicitlyRequested && !significantAutonomousOutreach) {
|
|
500
|
+
followUpText = ''
|
|
76
501
|
}
|
|
77
|
-
|
|
78
|
-
|
|
502
|
+
|
|
503
|
+
if (actionName === 'schedule_followup') {
|
|
504
|
+
if (!followupExplicitlyRequested && !significantAutonomousOutreach) {
|
|
505
|
+
return 'Error: schedule_followup requires either an explicit user request or a significant autonomous event.'
|
|
506
|
+
}
|
|
507
|
+
const payload = followUpText || message?.trim() || ''
|
|
508
|
+
if (!payload) return 'Error: followUpMessage or message is required for schedule_followup action.'
|
|
509
|
+
const scheduleActionKey = buildConnectorActionKey([
|
|
510
|
+
sessionId,
|
|
511
|
+
actionName,
|
|
512
|
+
selected.id,
|
|
513
|
+
channelId,
|
|
514
|
+
payload,
|
|
515
|
+
Number.isFinite(delaySec) ? Number(delaySec) : followDelaySec,
|
|
516
|
+
])
|
|
517
|
+
const cachedSchedule = recentConnectorActionCache.get(scheduleActionKey)
|
|
518
|
+
if (cachedSchedule && now - cachedSchedule.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
|
|
519
|
+
return cachedSchedule.result
|
|
520
|
+
}
|
|
521
|
+
const scheduled = scheduleConnectorFollowUp({
|
|
522
|
+
connectorId: selected.id,
|
|
523
|
+
channelId,
|
|
524
|
+
text: payload,
|
|
525
|
+
delaySec: Number.isFinite(delaySec) ? Number(delaySec) : followDelaySec,
|
|
526
|
+
})
|
|
527
|
+
const result = JSON.stringify({
|
|
528
|
+
status: 'followup_scheduled',
|
|
529
|
+
connectorId: selected.id,
|
|
530
|
+
platform: selected.platform,
|
|
531
|
+
to: channelId,
|
|
532
|
+
followUpId: scheduled.followUpId,
|
|
533
|
+
sendAt: scheduled.sendAt,
|
|
534
|
+
})
|
|
535
|
+
connectorTurnSendBudget.set(turnKey, {
|
|
536
|
+
count: (existingBudget?.count || 0) + 1,
|
|
537
|
+
at: now,
|
|
538
|
+
lastResult: result,
|
|
539
|
+
})
|
|
540
|
+
if (autonomousTurn && significantAutonomousOutreach) {
|
|
541
|
+
autonomousOutreachBudget.set(outreachBudgetKey, { at: now, result })
|
|
542
|
+
}
|
|
543
|
+
recentConnectorActionCache.set(scheduleActionKey, { at: now, result })
|
|
544
|
+
return result
|
|
79
545
|
}
|
|
80
|
-
|
|
81
|
-
|
|
546
|
+
|
|
547
|
+
const sendActionKey = buildConnectorActionKey([
|
|
548
|
+
sessionId,
|
|
549
|
+
actionName,
|
|
550
|
+
selected.id,
|
|
551
|
+
channelId,
|
|
552
|
+
message?.trim() || '',
|
|
553
|
+
media.mediaPath || '',
|
|
554
|
+
media.imageUrl || '',
|
|
555
|
+
media.fileUrl || '',
|
|
556
|
+
mimeType?.trim() || '',
|
|
557
|
+
fileName?.trim() || '',
|
|
558
|
+
caption?.trim() || '',
|
|
559
|
+
ptt ?? '',
|
|
560
|
+
followUpText,
|
|
561
|
+
followDelaySec,
|
|
562
|
+
])
|
|
563
|
+
const cachedSend = recentConnectorActionCache.get(sendActionKey)
|
|
564
|
+
if (cachedSend && now - cachedSend.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
|
|
565
|
+
return cachedSend.result
|
|
82
566
|
}
|
|
83
567
|
|
|
84
568
|
const sent = await sendConnectorMessage({
|
|
85
569
|
connectorId: selected.id,
|
|
86
570
|
channelId,
|
|
87
571
|
text: message?.trim() || '',
|
|
88
|
-
imageUrl: imageUrl
|
|
89
|
-
fileUrl: fileUrl
|
|
90
|
-
mediaPath: mediaPath
|
|
572
|
+
imageUrl: media.imageUrl,
|
|
573
|
+
fileUrl: media.fileUrl,
|
|
574
|
+
mediaPath: media.mediaPath,
|
|
91
575
|
mimeType: mimeType?.trim() || undefined,
|
|
92
576
|
fileName: fileName?.trim() || undefined,
|
|
93
577
|
caption: caption?.trim() || undefined,
|
|
578
|
+
ptt: ptt ?? undefined,
|
|
94
579
|
})
|
|
95
|
-
|
|
580
|
+
|
|
581
|
+
let followup: { followUpId: string; sendAt: number } | null = null
|
|
582
|
+
if (followUpText) {
|
|
583
|
+
followup = scheduleConnectorFollowUp({
|
|
584
|
+
connectorId: selected.id,
|
|
585
|
+
channelId,
|
|
586
|
+
text: followUpText,
|
|
587
|
+
delaySec: followDelaySec,
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const result = JSON.stringify({
|
|
96
592
|
status: 'sent',
|
|
97
593
|
connectorId: sent.connectorId,
|
|
98
594
|
platform: sent.platform,
|
|
99
595
|
to: sent.channelId,
|
|
100
596
|
messageId: sent.messageId || null,
|
|
597
|
+
...(followup
|
|
598
|
+
? {
|
|
599
|
+
followUpId: followup.followUpId,
|
|
600
|
+
followUpSendAt: followup.sendAt,
|
|
601
|
+
}
|
|
602
|
+
: {}),
|
|
603
|
+
})
|
|
604
|
+
connectorTurnSendBudget.set(turnKey, {
|
|
605
|
+
count: (existingBudget?.count || 0) + 1,
|
|
606
|
+
at: now,
|
|
607
|
+
lastResult: result,
|
|
101
608
|
})
|
|
609
|
+
if (autonomousTurn && significantAutonomousOutreach) {
|
|
610
|
+
autonomousOutreachBudget.set(outreachBudgetKey, { at: now, result })
|
|
611
|
+
}
|
|
612
|
+
recentConnectorActionCache.set(sendActionKey, { at: now, result })
|
|
613
|
+
return result
|
|
102
614
|
}
|
|
103
615
|
|
|
104
|
-
if (
|
|
616
|
+
if (actionName === 'message_react' || actionName === 'message_edit' || actionName === 'message_pin' || actionName === 'message_delete') {
|
|
105
617
|
if (!connectorId) return 'Error: connectorId is required for rich messaging actions.'
|
|
106
618
|
const { getRunningInstance } = await import('../connectors/manager')
|
|
107
619
|
const inst = getRunningInstance(connectorId)
|
|
@@ -112,25 +624,25 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
112
624
|
if (!targetMessageId) return 'Error: message parameter (used as messageId) is required for rich messaging actions.'
|
|
113
625
|
|
|
114
626
|
try {
|
|
115
|
-
if (
|
|
627
|
+
if (actionName === 'message_react') {
|
|
116
628
|
if (!inst.sendReaction) return 'Error: this connector does not support reactions.'
|
|
117
629
|
const emoji = caption?.trim() || '👍'
|
|
118
630
|
await inst.sendReaction(targetChannel, targetMessageId, emoji)
|
|
119
631
|
return JSON.stringify({ status: 'reacted', connectorId, messageId: targetMessageId, emoji })
|
|
120
632
|
}
|
|
121
|
-
if (
|
|
633
|
+
if (actionName === 'message_edit') {
|
|
122
634
|
if (!inst.editMessage) return 'Error: this connector does not support message editing.'
|
|
123
635
|
const newText = caption?.trim() || ''
|
|
124
636
|
if (!newText) return 'Error: caption (new text) is required for message_edit.'
|
|
125
637
|
await inst.editMessage(targetChannel, targetMessageId, newText)
|
|
126
638
|
return JSON.stringify({ status: 'edited', connectorId, messageId: targetMessageId })
|
|
127
639
|
}
|
|
128
|
-
if (
|
|
640
|
+
if (actionName === 'message_delete') {
|
|
129
641
|
if (!inst.deleteMessage) return 'Error: this connector does not support message deletion.'
|
|
130
642
|
await inst.deleteMessage(targetChannel, targetMessageId)
|
|
131
643
|
return JSON.stringify({ status: 'deleted', connectorId, messageId: targetMessageId })
|
|
132
644
|
}
|
|
133
|
-
if (
|
|
645
|
+
if (actionName === 'message_pin') {
|
|
134
646
|
if (!inst.pinMessage) return 'Error: this connector does not support message pinning.'
|
|
135
647
|
await inst.pinMessage(targetChannel, targetMessageId)
|
|
136
648
|
return JSON.stringify({ status: 'pinned', connectorId, messageId: targetMessageId })
|
|
@@ -140,26 +652,34 @@ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInter
|
|
|
140
652
|
}
|
|
141
653
|
}
|
|
142
654
|
|
|
143
|
-
return 'Unknown action. Use list_running, list_targets, or
|
|
655
|
+
return 'Unknown action. Use list_running, list_targets, start, stop, send, send_voice_note, schedule_followup, or message_* actions.'
|
|
144
656
|
} catch (err: unknown) {
|
|
145
657
|
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
146
658
|
}
|
|
147
659
|
},
|
|
148
660
|
{
|
|
149
661
|
name: 'connector_message_tool',
|
|
150
|
-
description:
|
|
662
|
+
description: voiceNoteToolEnabled
|
|
663
|
+
? 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, Discord, etc.). Use "start"/"stop" to manage connector lifecycle, "list_running"/"list_targets" to discover available connectors and recipients, "send" to deliver text/media, "send_voice_note" to synthesize and send audio via ElevenLabs, "schedule_followup" for delayed check-ins, and rich actions (react, edit, delete, pin) for message management. When a send fails because no connector is running, check if one is configured and offer to start it. When no target is set, list available configured numbers and ask the user which to send to.'
|
|
664
|
+
: 'Manage and send messages through chat platform connectors (WhatsApp, Telegram, Slack, Discord, etc.). Use "start"/"stop" to manage connector lifecycle, "list_running"/"list_targets" to discover available connectors and recipients, "send" to deliver text/media, "schedule_followup" for delayed check-ins, and rich actions (react, edit, delete, pin) for message management. Voice-note sending appears only when ElevenLabs is enabled with an API key in Settings > Voice. When a send fails because no connector is running, check if one is configured and offer to start it. When no target is set, list available configured numbers and ask the user which to send to.',
|
|
151
665
|
schema: z.object({
|
|
152
|
-
action:
|
|
666
|
+
action: actionSchema.describe('connector messaging action'),
|
|
153
667
|
connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
|
|
154
668
|
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord, bluebubbles, etc.).'),
|
|
155
669
|
to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
|
|
156
|
-
message: z.string().optional().describe('Message text to send
|
|
670
|
+
message: z.string().optional().describe('Message text to send. Required for send unless media is provided. Used as fallback for send_voice_note/schedule_followup when voiceText/followUpMessage are omitted.'),
|
|
671
|
+
voiceText: z.string().optional().describe('Text to synthesize for send_voice_note. Uses message when omitted.'),
|
|
672
|
+
voiceId: z.string().optional().describe('Optional ElevenLabs voice override for send_voice_note.'),
|
|
157
673
|
imageUrl: z.string().optional().describe('Optional public image URL to attach/send where platform supports media.'),
|
|
158
674
|
fileUrl: z.string().optional().describe('Optional public file URL to attach/send where platform supports documents.'),
|
|
159
675
|
mediaPath: z.string().optional().describe('Absolute local file path to send (e.g. a screenshot). Auto-detects mime type from extension. Takes priority over imageUrl/fileUrl.'),
|
|
160
676
|
mimeType: z.string().optional().describe('Optional MIME type for mediaPath or fileUrl.'),
|
|
161
677
|
fileName: z.string().optional().describe('Optional display file name for mediaPath or fileUrl.'),
|
|
162
678
|
caption: z.string().optional().describe('Optional caption used with image/file sends.'),
|
|
679
|
+
delaySec: z.number().optional().describe('Delay in seconds for schedule_followup.'),
|
|
680
|
+
followUpMessage: z.string().optional().describe('Optional delayed follow-up text (for send) or primary text for schedule_followup.'),
|
|
681
|
+
followUpDelaySec: z.number().optional().describe('Delay in seconds for followUpMessage when action=send. Default 300 seconds.'),
|
|
682
|
+
ptt: z.boolean().optional().describe('Send audio as a WhatsApp voice note (push-to-talk). Defaults to true for audio files.'),
|
|
163
683
|
approved: z.boolean().optional().describe('Set true to explicitly confirm outbound send when safetyRequireApprovalForOutbound is enabled.'),
|
|
164
684
|
}),
|
|
165
685
|
},
|