@swarmclawai/swarmclaw 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -5,6 +5,7 @@ import fs from 'fs'
|
|
|
5
5
|
import { loadConnectors, loadSettings, UPLOAD_DIR } from '../storage'
|
|
6
6
|
import { genId } from '@/lib/id'
|
|
7
7
|
import { synthesizeElevenLabsMp3 } from '../elevenlabs'
|
|
8
|
+
import { isAudioMime, mimeFromPath } from '../connectors/media'
|
|
8
9
|
import type { ToolBuildContext } from './context'
|
|
9
10
|
import type { Plugin, PluginHooks } from '@/types'
|
|
10
11
|
import { getPluginManager } from '../plugins'
|
|
@@ -17,6 +18,78 @@ const recentConnectorActionCache = new Map<string, { at: number; result: string
|
|
|
17
18
|
const connectorTurnSendBudget = new Map<string, { count: number; at: number; lastResult?: string }>()
|
|
18
19
|
const autonomousOutreachBudget = new Map<string, { at: number; result?: string }>()
|
|
19
20
|
|
|
21
|
+
export const CONNECTOR_MESSAGE_TOOL_ACTIONS = [
|
|
22
|
+
'list_running',
|
|
23
|
+
'list_targets',
|
|
24
|
+
'start',
|
|
25
|
+
'stop',
|
|
26
|
+
'send',
|
|
27
|
+
'send_voice_note',
|
|
28
|
+
'schedule_followup',
|
|
29
|
+
'react',
|
|
30
|
+
'edit',
|
|
31
|
+
'delete',
|
|
32
|
+
'pin',
|
|
33
|
+
'message_react',
|
|
34
|
+
'message_edit',
|
|
35
|
+
'message_delete',
|
|
36
|
+
'message_pin',
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
export const CONNECTOR_MESSAGE_TOOL_PARAMETERS = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
action: { type: 'string', enum: [...CONNECTOR_MESSAGE_TOOL_ACTIONS] },
|
|
43
|
+
connectorId: { type: 'string' },
|
|
44
|
+
connector: { type: 'string' },
|
|
45
|
+
connector_id: { type: 'string' },
|
|
46
|
+
runningConnectorId: { type: 'string' },
|
|
47
|
+
id: { type: 'string' },
|
|
48
|
+
platform: { type: 'string' },
|
|
49
|
+
to: { type: 'string' },
|
|
50
|
+
channel: { type: 'string' },
|
|
51
|
+
channelId: { type: 'string' },
|
|
52
|
+
recipientId: { type: 'string' },
|
|
53
|
+
phoneNumber: { type: 'string' },
|
|
54
|
+
configuredTarget: { type: 'string' },
|
|
55
|
+
target: { type: 'string' },
|
|
56
|
+
recipient: { type: 'string' },
|
|
57
|
+
path: { type: 'string' },
|
|
58
|
+
targets: { type: 'string' },
|
|
59
|
+
message: { type: 'string' },
|
|
60
|
+
text: { type: 'string' },
|
|
61
|
+
content: { type: 'string' },
|
|
62
|
+
body: { type: 'string' },
|
|
63
|
+
messageId: { type: 'string' },
|
|
64
|
+
targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
|
|
65
|
+
emoji: { type: 'string' },
|
|
66
|
+
voiceText: { type: 'string' },
|
|
67
|
+
voiceId: { type: 'string' },
|
|
68
|
+
imageUrl: { type: 'string' },
|
|
69
|
+
fileUrl: { type: 'string' },
|
|
70
|
+
mediaPath: { type: 'string' },
|
|
71
|
+
mimeType: { type: 'string' },
|
|
72
|
+
fileName: { type: 'string' },
|
|
73
|
+
caption: { type: 'string' },
|
|
74
|
+
replyToMessageId: { type: 'string' },
|
|
75
|
+
threadId: { type: 'string' },
|
|
76
|
+
delaySec: { type: 'number' },
|
|
77
|
+
followUpMessage: { type: 'string' },
|
|
78
|
+
followupMessage: { type: 'string' },
|
|
79
|
+
followUpDelaySec: { type: 'number' },
|
|
80
|
+
dedupeKey: { type: 'string' },
|
|
81
|
+
approved: { type: 'boolean' },
|
|
82
|
+
ptt: { type: 'boolean' },
|
|
83
|
+
},
|
|
84
|
+
} as const
|
|
85
|
+
|
|
86
|
+
const LEGACY_CONNECTOR_ACTION_ALIASES: Record<string, string> = {
|
|
87
|
+
message_react: 'react',
|
|
88
|
+
message_edit: 'edit',
|
|
89
|
+
message_delete: 'delete',
|
|
90
|
+
message_pin: 'pin',
|
|
91
|
+
}
|
|
92
|
+
|
|
20
93
|
function pruneOldConnectorToolState(now: number): void {
|
|
21
94
|
for (const [key, entry] of recentConnectorActionCache.entries()) {
|
|
22
95
|
if (now - entry.at > CONNECTOR_ACTION_DEDUPE_TTL_MS) recentConnectorActionCache.delete(key)
|
|
@@ -109,6 +182,130 @@ function normalizeDedupedReplayResult(raw: string, fallback: { connectorId: stri
|
|
|
109
182
|
}
|
|
110
183
|
}
|
|
111
184
|
|
|
185
|
+
export function normalizeConnectorActionName(action: string): string {
|
|
186
|
+
const normalized = String(action || '').trim()
|
|
187
|
+
return LEGACY_CONNECTOR_ACTION_ALIASES[normalized] || normalized
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function inferConnectorActionName(input: Record<string, unknown>): string | null {
|
|
191
|
+
const explicit = typeof input.action === 'string' ? input.action.trim() : ''
|
|
192
|
+
if (explicit) return explicit
|
|
193
|
+
if (typeof input.voiceText === 'string' && input.voiceText.trim()) return 'send_voice_note'
|
|
194
|
+
if (
|
|
195
|
+
typeof input.followUpMessage === 'string'
|
|
196
|
+
|| typeof input.followupMessage === 'string'
|
|
197
|
+
|| typeof input.followUpDelaySec === 'number'
|
|
198
|
+
|| typeof input.delaySec === 'number'
|
|
199
|
+
) return 'schedule_followup'
|
|
200
|
+
if (
|
|
201
|
+
typeof input.message === 'string'
|
|
202
|
+
|| typeof input.text === 'string'
|
|
203
|
+
|| typeof input.content === 'string'
|
|
204
|
+
|| typeof input.body === 'string'
|
|
205
|
+
|| typeof input.mediaPath === 'string'
|
|
206
|
+
|| typeof input.imageUrl === 'string'
|
|
207
|
+
|| typeof input.fileUrl === 'string'
|
|
208
|
+
) return 'send'
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pickConnectorString(value: unknown): string | null {
|
|
213
|
+
if (typeof value === 'string') {
|
|
214
|
+
const trimmed = value.trim()
|
|
215
|
+
return trimmed || null
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
for (const entry of value) {
|
|
219
|
+
const picked = pickConnectorString(entry)
|
|
220
|
+
if (picked) return picked
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveRunningConnectorId(
|
|
227
|
+
running: Array<{ id?: string; name?: string }>,
|
|
228
|
+
value: unknown,
|
|
229
|
+
): string | null {
|
|
230
|
+
const candidate = pickConnectorString(value)
|
|
231
|
+
if (!candidate) return null
|
|
232
|
+
const matched = running.find((connector) => (
|
|
233
|
+
String(connector.id || '').trim() === candidate
|
|
234
|
+
|| String(connector.name || '').trim() === candidate
|
|
235
|
+
))
|
|
236
|
+
return matched ? String(matched.id || '').trim() || null : null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function normalizeConnectorActionInputAliases(
|
|
240
|
+
input: Record<string, unknown>,
|
|
241
|
+
running: Array<{ id?: string; name?: string }> = [],
|
|
242
|
+
): Record<string, unknown> {
|
|
243
|
+
const normalized = { ...input }
|
|
244
|
+
const actionName = normalizeConnectorActionName(inferConnectorActionName(normalized) || String(normalized.action || ''))
|
|
245
|
+
const messageActionUsesRawId = actionName === 'react'
|
|
246
|
+
|| actionName === 'edit'
|
|
247
|
+
|| actionName === 'delete'
|
|
248
|
+
|| actionName === 'pin'
|
|
249
|
+
const messageAlias = pickConnectorString(
|
|
250
|
+
normalized.message
|
|
251
|
+
?? normalized.text
|
|
252
|
+
?? normalized.content
|
|
253
|
+
?? normalized.body,
|
|
254
|
+
)
|
|
255
|
+
if (!pickConnectorString(normalized.message) && messageAlias) {
|
|
256
|
+
normalized.message = messageAlias
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const followUpAlias = pickConnectorString(
|
|
260
|
+
normalized.followUpMessage
|
|
261
|
+
?? normalized.followupMessage,
|
|
262
|
+
)
|
|
263
|
+
if (!pickConnectorString(normalized.followUpMessage) && followUpAlias) {
|
|
264
|
+
normalized.followUpMessage = followUpAlias
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rawId = pickConnectorString(normalized.id)
|
|
268
|
+
const explicitConnectorId = pickConnectorString(
|
|
269
|
+
normalized.connectorId
|
|
270
|
+
?? normalized.runningConnectorId
|
|
271
|
+
?? normalized.connector
|
|
272
|
+
?? normalized.connector_id,
|
|
273
|
+
)
|
|
274
|
+
const aliasConnectorId = explicitConnectorId
|
|
275
|
+
? resolveRunningConnectorId(running, explicitConnectorId) || explicitConnectorId
|
|
276
|
+
: resolveRunningConnectorId(running, normalized.channel) || resolveRunningConnectorId(running, rawId)
|
|
277
|
+
|
|
278
|
+
if (!pickConnectorString(normalized.connectorId) && aliasConnectorId) {
|
|
279
|
+
normalized.connectorId = aliasConnectorId
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const rawIdIsConnector = !!(rawId && resolveRunningConnectorId(running, rawId))
|
|
283
|
+
if (!pickConnectorString(normalized.messageId) && rawId && !rawIdIsConnector && messageActionUsesRawId) {
|
|
284
|
+
normalized.messageId = rawId
|
|
285
|
+
}
|
|
286
|
+
const targetAlias = pickConnectorString(
|
|
287
|
+
normalized.to
|
|
288
|
+
?? normalized.channelId
|
|
289
|
+
?? normalized.recipientId
|
|
290
|
+
?? normalized.phoneNumber
|
|
291
|
+
?? normalized.configuredTarget
|
|
292
|
+
?? normalized.target
|
|
293
|
+
?? normalized.recipient
|
|
294
|
+
?? normalized.path
|
|
295
|
+
?? normalized.targets,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if (!pickConnectorString(normalized.to)) {
|
|
299
|
+
if (targetAlias) {
|
|
300
|
+
normalized.to = targetAlias
|
|
301
|
+
} else if (rawId && !rawIdIsConnector && !messageActionUsesRawId) {
|
|
302
|
+
normalized.to = rawId
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return normalized
|
|
307
|
+
}
|
|
308
|
+
|
|
112
309
|
/** Resolve /api/uploads/filename URLs to actual disk paths */
|
|
113
310
|
function resolveUploadUrl(url: string | undefined): { mediaPath: string; mimeType?: string } | null {
|
|
114
311
|
if (!url) return null
|
|
@@ -140,13 +337,83 @@ function parseCsv(raw: string | undefined): string[] {
|
|
|
140
337
|
return raw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
141
338
|
}
|
|
142
339
|
|
|
340
|
+
function trimToString(value: unknown): string {
|
|
341
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveSessionConnectorTargets(
|
|
345
|
+
session: {
|
|
346
|
+
connectorContext?: Record<string, unknown>
|
|
347
|
+
messages?: Array<Record<string, unknown>>
|
|
348
|
+
} | null | undefined,
|
|
349
|
+
connectorId: string,
|
|
350
|
+
): Array<{ channelId: string; senderId?: string; senderName?: string }> {
|
|
351
|
+
const targets: Array<{ channelId: string; senderId?: string; senderName?: string }> = []
|
|
352
|
+
const seen = new Set<string>()
|
|
353
|
+
const pushTarget = (target: { channelId: string; senderId?: string; senderName?: string } | null) => {
|
|
354
|
+
if (!target?.channelId || seen.has(target.channelId)) return
|
|
355
|
+
seen.add(target.channelId)
|
|
356
|
+
targets.push(target)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const context = session?.connectorContext
|
|
360
|
+
if (trimToString(context?.connectorId) === connectorId) {
|
|
361
|
+
const channelId = trimToString(context?.channelId)
|
|
362
|
+
pushTarget(channelId
|
|
363
|
+
? {
|
|
364
|
+
channelId,
|
|
365
|
+
senderId: trimToString(context?.senderId) || undefined,
|
|
366
|
+
senderName: trimToString(context?.senderName) || undefined,
|
|
367
|
+
}
|
|
368
|
+
: null)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const messages = Array.isArray(session?.messages) ? session.messages : []
|
|
372
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
373
|
+
const source = messages[i]?.source as Record<string, unknown> | undefined
|
|
374
|
+
if (!source || trimToString(source.connectorId) !== connectorId) continue
|
|
375
|
+
const channelId = trimToString(source.channelId)
|
|
376
|
+
if (!channelId) continue
|
|
377
|
+
pushTarget({
|
|
378
|
+
channelId,
|
|
379
|
+
senderId: trimToString(source.senderId) || undefined,
|
|
380
|
+
senderName: trimToString(source.senderName) || undefined,
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return targets
|
|
385
|
+
}
|
|
386
|
+
|
|
143
387
|
function pickChannelTarget(params: {
|
|
144
388
|
connector: { config?: Record<string, string> }
|
|
389
|
+
connectorId: string
|
|
145
390
|
to?: string
|
|
146
391
|
recentChannelId: string | null
|
|
392
|
+
currentSession?: {
|
|
393
|
+
connectorContext?: Record<string, unknown>
|
|
394
|
+
messages?: Array<Record<string, unknown>>
|
|
395
|
+
} | null
|
|
147
396
|
}): { channelId: string; error?: string } {
|
|
148
397
|
let channelId = params.to?.trim() || ''
|
|
149
398
|
const connector = params.connector
|
|
399
|
+
const sessionTargets = resolveSessionConnectorTargets(params.currentSession, params.connectorId)
|
|
400
|
+
|
|
401
|
+
if (!channelId && sessionTargets.length === 1) {
|
|
402
|
+
channelId = sessionTargets[0].channelId
|
|
403
|
+
}
|
|
404
|
+
if (!channelId && sessionTargets.length > 1) {
|
|
405
|
+
const choices = sessionTargets.map((target) => (
|
|
406
|
+
target.senderName
|
|
407
|
+
? `${target.senderName} (${target.channelId})`
|
|
408
|
+
: target.senderId
|
|
409
|
+
? `${target.senderId} (${target.channelId})`
|
|
410
|
+
: target.channelId
|
|
411
|
+
))
|
|
412
|
+
return {
|
|
413
|
+
channelId: '',
|
|
414
|
+
error: `Error: this chat currently references multiple connector recipients for this connector: ${JSON.stringify(choices)}. Re-call with the "to" parameter so the message goes to the right person.`,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
150
417
|
|
|
151
418
|
if (!channelId) {
|
|
152
419
|
const outbound = connector.config?.outboundJid?.trim()
|
|
@@ -290,33 +557,10 @@ interface ConnectorActionContext {
|
|
|
290
557
|
}
|
|
291
558
|
|
|
292
559
|
async function executeConnectorAction(input: ConnectorActionInput, bctx: ConnectorActionContext) {
|
|
293
|
-
const
|
|
294
|
-
const {
|
|
295
|
-
action,
|
|
296
|
-
connectorId,
|
|
297
|
-
platform,
|
|
298
|
-
to,
|
|
299
|
-
message,
|
|
300
|
-
voiceText,
|
|
301
|
-
voiceId,
|
|
302
|
-
imageUrl,
|
|
303
|
-
fileUrl,
|
|
304
|
-
mediaPath,
|
|
305
|
-
mimeType,
|
|
306
|
-
fileName,
|
|
307
|
-
caption,
|
|
308
|
-
messageId,
|
|
309
|
-
targetMessage,
|
|
310
|
-
emoji,
|
|
311
|
-
replyToMessageId,
|
|
312
|
-
threadId,
|
|
313
|
-
dedupeKey,
|
|
314
|
-
approved,
|
|
315
|
-
ptt,
|
|
316
|
-
} = normalized as ConnectorActionInput
|
|
560
|
+
const baseNormalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
|
|
317
561
|
|
|
318
562
|
try {
|
|
319
|
-
const
|
|
563
|
+
const tentativePlatform = pickConnectorString(baseNormalized.platform)
|
|
320
564
|
const {
|
|
321
565
|
listRunningConnectors,
|
|
322
566
|
sendConnectorMessage,
|
|
@@ -324,7 +568,34 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
324
568
|
scheduleConnectorFollowUp,
|
|
325
569
|
performConnectorMessageAction,
|
|
326
570
|
} = await import('../connectors/manager')
|
|
327
|
-
const running = listRunningConnectors(
|
|
571
|
+
const running = listRunningConnectors(tentativePlatform || undefined)
|
|
572
|
+
const normalized = normalizeConnectorActionInputAliases(baseNormalized, running)
|
|
573
|
+
const inferredAction = inferConnectorActionName(normalized)
|
|
574
|
+
const {
|
|
575
|
+
action,
|
|
576
|
+
connectorId,
|
|
577
|
+
platform,
|
|
578
|
+
to,
|
|
579
|
+
message,
|
|
580
|
+
voiceText,
|
|
581
|
+
voiceId,
|
|
582
|
+
imageUrl,
|
|
583
|
+
fileUrl,
|
|
584
|
+
mediaPath,
|
|
585
|
+
mimeType,
|
|
586
|
+
fileName,
|
|
587
|
+
caption,
|
|
588
|
+
messageId,
|
|
589
|
+
targetMessage,
|
|
590
|
+
emoji,
|
|
591
|
+
replyToMessageId,
|
|
592
|
+
threadId,
|
|
593
|
+
dedupeKey,
|
|
594
|
+
approved,
|
|
595
|
+
ptt,
|
|
596
|
+
} = normalized as ConnectorActionInput
|
|
597
|
+
const actionName = normalizeConnectorActionName(String(inferredAction || action || ''))
|
|
598
|
+
if (!actionName) return 'Error: action is required.'
|
|
328
599
|
|
|
329
600
|
if (actionName === 'list_running' || actionName === 'list_targets') {
|
|
330
601
|
return JSON.stringify(running)
|
|
@@ -391,8 +662,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
391
662
|
|
|
392
663
|
const target = pickChannelTarget({
|
|
393
664
|
connector,
|
|
665
|
+
connectorId: selected.id,
|
|
394
666
|
to,
|
|
395
667
|
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
668
|
+
currentSession,
|
|
396
669
|
})
|
|
397
670
|
if (target.error) return target.error
|
|
398
671
|
|
|
@@ -416,22 +689,61 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
416
689
|
}
|
|
417
690
|
|
|
418
691
|
if (actionName === 'send_voice_note') {
|
|
692
|
+
const media = resolveConnectorMediaInput({ cwd: bctx.cwd, mediaPath, imageUrl, fileUrl })
|
|
693
|
+
if (media.error) return media.error
|
|
694
|
+
if (media.imageUrl || media.fileUrl) {
|
|
695
|
+
return 'Error: send_voice_note requires an audio mediaPath or voiceText. Remote image/file URLs are not valid voice-note inputs.'
|
|
696
|
+
}
|
|
419
697
|
const ttsText = (voiceText || message || '').trim()
|
|
420
|
-
if (!ttsText) return 'Error: voiceText or
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
698
|
+
if (!media.mediaPath && !ttsText) return 'Error: voiceText, message, or an audio mediaPath is required.'
|
|
699
|
+
const voiceActionKey = buildConnectorActionKey([
|
|
700
|
+
sessionId,
|
|
701
|
+
actionName,
|
|
702
|
+
selected.id,
|
|
703
|
+
channelId,
|
|
704
|
+
media.mediaPath || '',
|
|
705
|
+
ttsText,
|
|
706
|
+
voiceId?.trim() || '',
|
|
707
|
+
fileName?.trim() || '',
|
|
708
|
+
caption?.trim() || '',
|
|
709
|
+
ptt ?? true,
|
|
710
|
+
])
|
|
711
|
+
const cachedVoice = recentConnectorActionCache.get(voiceActionKey)
|
|
712
|
+
if (cachedVoice && now - cachedVoice.at <= CONNECTOR_ACTION_DEDUPE_TTL_MS) {
|
|
713
|
+
return cachedVoice.result
|
|
714
|
+
}
|
|
715
|
+
let voicePath = media.mediaPath
|
|
716
|
+
let outboundMimeType = mimeType?.trim() || undefined
|
|
717
|
+
if (voicePath) {
|
|
718
|
+
outboundMimeType = outboundMimeType || mimeFromPath(voicePath)
|
|
719
|
+
if (!isAudioMime(outboundMimeType)) {
|
|
720
|
+
return `Error: send_voice_note mediaPath must point to an audio file. Resolved MIME type was "${outboundMimeType}".`
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
const audioBuffer = await synthesizeElevenLabsMp3({ text: ttsText, voiceId: voiceId?.trim() || undefined })
|
|
724
|
+
const voiceFileName = `${Date.now()}-${genId()}-voicenote.mp3`
|
|
725
|
+
voicePath = path.join(UPLOAD_DIR, voiceFileName)
|
|
726
|
+
fs.writeFileSync(voicePath, audioBuffer)
|
|
727
|
+
outboundMimeType = 'audio/mpeg'
|
|
728
|
+
}
|
|
425
729
|
|
|
426
730
|
const sent = await sendConnectorMessage({
|
|
427
|
-
connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType:
|
|
731
|
+
connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: outboundMimeType,
|
|
428
732
|
fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
|
|
429
733
|
sessionId,
|
|
430
734
|
replyToMessageId: replyToMessageId?.trim() || undefined,
|
|
431
735
|
threadId: threadId?.trim() || undefined,
|
|
432
736
|
})
|
|
433
|
-
const result = JSON.stringify({
|
|
737
|
+
const result = JSON.stringify({
|
|
738
|
+
status: 'voice_sent',
|
|
739
|
+
connectorId: sent.connectorId,
|
|
740
|
+
platform: sent.platform,
|
|
741
|
+
to: sent.channelId,
|
|
742
|
+
messageId: sent.messageId || null,
|
|
743
|
+
voiceFile: voicePath,
|
|
744
|
+
})
|
|
434
745
|
connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
|
|
746
|
+
recentConnectorActionCache.set(voiceActionKey, { at: now, result })
|
|
435
747
|
return result
|
|
436
748
|
}
|
|
437
749
|
|
|
@@ -503,8 +815,10 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
|
|
|
503
815
|
const { selected } = resolved
|
|
504
816
|
const target = pickChannelTarget({
|
|
505
817
|
connector: resolved.connector,
|
|
818
|
+
connectorId: selected.id,
|
|
506
819
|
to,
|
|
507
820
|
recentChannelId: getConnectorRecentChannelId(selected.id),
|
|
821
|
+
currentSession,
|
|
508
822
|
})
|
|
509
823
|
if (target.error) return target.error
|
|
510
824
|
const result = await performConnectorMessageAction({
|
|
@@ -546,34 +860,7 @@ const ConnectorPlugin: Plugin = {
|
|
|
546
860
|
{
|
|
547
861
|
name: 'connector_message_tool',
|
|
548
862
|
description: 'Send and manage outbound messages across chat platforms.',
|
|
549
|
-
parameters:
|
|
550
|
-
type: 'object',
|
|
551
|
-
properties: {
|
|
552
|
-
action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note', 'schedule_followup', 'react', 'edit', 'delete', 'pin'] },
|
|
553
|
-
connectorId: { type: 'string' },
|
|
554
|
-
platform: { type: 'string' },
|
|
555
|
-
to: { type: 'string' },
|
|
556
|
-
message: { type: 'string' },
|
|
557
|
-
messageId: { type: 'string' },
|
|
558
|
-
targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
|
|
559
|
-
emoji: { type: 'string' },
|
|
560
|
-
voiceText: { type: 'string' },
|
|
561
|
-
voiceId: { type: 'string' },
|
|
562
|
-
imageUrl: { type: 'string' },
|
|
563
|
-
fileUrl: { type: 'string' },
|
|
564
|
-
mediaPath: { type: 'string' },
|
|
565
|
-
mimeType: { type: 'string' },
|
|
566
|
-
fileName: { type: 'string' },
|
|
567
|
-
caption: { type: 'string' },
|
|
568
|
-
replyToMessageId: { type: 'string' },
|
|
569
|
-
threadId: { type: 'string' },
|
|
570
|
-
delaySec: { type: 'number' },
|
|
571
|
-
followUpMessage: { type: 'string' },
|
|
572
|
-
followUpDelaySec: { type: 'number' },
|
|
573
|
-
dedupeKey: { type: 'string' },
|
|
574
|
-
},
|
|
575
|
-
required: ['action']
|
|
576
|
-
},
|
|
863
|
+
parameters: CONNECTOR_MESSAGE_TOOL_PARAMETERS,
|
|
577
864
|
execute: async (args, context) => executeConnectorAction(args as ConnectorActionInput, { ...context.session, cwd: context.session.cwd || process.cwd() })
|
|
578
865
|
}
|
|
579
866
|
]
|
|
@@ -48,15 +48,24 @@ function normalizeWorkspaceAlias(cwd: string, filePath: string): string {
|
|
|
48
48
|
return trimmed
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Safe absolute paths that agents are allowed to write to outside the workspace.
|
|
53
|
+
* Kept minimal to prevent accidental writes to sensitive system locations.
|
|
54
|
+
*/
|
|
55
|
+
const ALLOWED_ABSOLUTE_PREFIXES = ['/tmp/', '/var/tmp/']
|
|
56
|
+
|
|
51
57
|
export function safePath(cwd: string, filePath: string): string {
|
|
52
58
|
const path = require('path')
|
|
53
59
|
const normalized = normalizeWorkspaceAlias(cwd, filePath)
|
|
54
60
|
const resolvedRoot = path.resolve(cwd)
|
|
55
61
|
const resolved = path.resolve(resolvedRoot, normalized)
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
// Allow workspace-relative paths
|
|
63
|
+
if (resolved.startsWith(resolvedRoot)) return resolved
|
|
64
|
+
// Allow explicitly safe absolute paths (e.g., /tmp/)
|
|
65
|
+
if (path.isAbsolute(normalized) && ALLOWED_ABSOLUTE_PREFIXES.some((p: string) => resolved.startsWith(p))) {
|
|
66
|
+
return resolved
|
|
58
67
|
}
|
|
59
|
-
|
|
68
|
+
throw new Error('Path traversal not allowed')
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
export function truncate(text: string, max: number): string {
|