@swarmclawai/swarmclaw 0.2.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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import type { SSEEvent } from '@/types'
|
|
3
|
+
import { active, loadSessions } from './storage'
|
|
4
|
+
import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
|
|
5
|
+
import { loadRuntimeSettings } from './runtime-settings'
|
|
6
|
+
import { log } from './logger'
|
|
7
|
+
import { handleMainLoopRunResult, type MainLoopFollowupRequest } from './main-agent-loop'
|
|
8
|
+
|
|
9
|
+
export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
10
|
+
export type SessionQueueMode = 'followup' | 'steer' | 'collect'
|
|
11
|
+
|
|
12
|
+
export interface SessionRunRecord {
|
|
13
|
+
id: string
|
|
14
|
+
sessionId: string
|
|
15
|
+
source: string
|
|
16
|
+
internal: boolean
|
|
17
|
+
mode: SessionQueueMode
|
|
18
|
+
status: SessionRunStatus
|
|
19
|
+
messagePreview: string
|
|
20
|
+
dedupeKey?: string
|
|
21
|
+
queuedAt: number
|
|
22
|
+
startedAt?: number
|
|
23
|
+
endedAt?: number
|
|
24
|
+
error?: string
|
|
25
|
+
resultPreview?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface QueueEntry {
|
|
29
|
+
executionKey: string
|
|
30
|
+
run: SessionRunRecord
|
|
31
|
+
message: string
|
|
32
|
+
imagePath?: string
|
|
33
|
+
imageUrl?: string
|
|
34
|
+
onEvents: Array<(event: SSEEvent) => void>
|
|
35
|
+
signalController: AbortController
|
|
36
|
+
maxRuntimeMs?: number
|
|
37
|
+
modelOverride?: string
|
|
38
|
+
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
39
|
+
resolve: (value: ExecuteChatTurnResult) => void
|
|
40
|
+
reject: (error: Error) => void
|
|
41
|
+
promise: Promise<ExecuteChatTurnResult>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RuntimeState {
|
|
45
|
+
runningByExecution: Map<string, QueueEntry>
|
|
46
|
+
queueByExecution: Map<string, QueueEntry[]>
|
|
47
|
+
runs: Map<string, SessionRunRecord>
|
|
48
|
+
recentRunIds: string[]
|
|
49
|
+
promises: Map<string, Promise<ExecuteChatTurnResult>>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const MAX_RECENT_RUNS = 500
|
|
53
|
+
const COLLECT_COALESCE_WINDOW_MS = 1500
|
|
54
|
+
const globalKey = '__swarmclaw_session_run_manager__' as const
|
|
55
|
+
const state: RuntimeState = (globalThis as any)[globalKey] ?? ((globalThis as any)[globalKey] = {
|
|
56
|
+
runningByExecution: new Map<string, QueueEntry>(),
|
|
57
|
+
queueByExecution: new Map<string, QueueEntry[]>(),
|
|
58
|
+
runs: new Map<string, SessionRunRecord>(),
|
|
59
|
+
recentRunIds: [],
|
|
60
|
+
promises: new Map<string, Promise<ExecuteChatTurnResult>>(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function now() {
|
|
64
|
+
return Date.now()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function messagePreview(text: string): string {
|
|
68
|
+
return (text || '').replace(/\s+/g, ' ').trim().slice(0, 140)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trimRecentRuns() {
|
|
72
|
+
while (state.recentRunIds.length > MAX_RECENT_RUNS) {
|
|
73
|
+
const id = state.recentRunIds.shift()
|
|
74
|
+
if (!id) continue
|
|
75
|
+
state.runs.delete(id)
|
|
76
|
+
state.promises.delete(id)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function registerRun(run: SessionRunRecord) {
|
|
81
|
+
state.runs.set(run.id, run)
|
|
82
|
+
state.recentRunIds.push(run.id)
|
|
83
|
+
trimRecentRuns()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function emitToSubscribers(entry: QueueEntry, event: SSEEvent) {
|
|
87
|
+
for (const send of entry.onEvents) {
|
|
88
|
+
try {
|
|
89
|
+
send(event)
|
|
90
|
+
} catch {
|
|
91
|
+
// Subscriber stream can be closed by the client.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function emitRunMeta(entry: QueueEntry, status: SessionRunStatus, extra?: Record<string, unknown>) {
|
|
97
|
+
emitToSubscribers(entry, {
|
|
98
|
+
t: 'md',
|
|
99
|
+
text: JSON.stringify({
|
|
100
|
+
run: {
|
|
101
|
+
id: entry.run.id,
|
|
102
|
+
sessionId: entry.run.sessionId,
|
|
103
|
+
status,
|
|
104
|
+
source: entry.run.source,
|
|
105
|
+
internal: entry.run.internal,
|
|
106
|
+
...extra,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function executionKeyForSession(sessionId: string): string {
|
|
113
|
+
return `session:${sessionId}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function queueForExecution(executionKey: string): QueueEntry[] {
|
|
117
|
+
const existing = state.queueByExecution.get(executionKey)
|
|
118
|
+
if (existing) return existing
|
|
119
|
+
const created: QueueEntry[] = []
|
|
120
|
+
state.queueByExecution.set(executionKey, created)
|
|
121
|
+
return created
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeMode(mode: string | undefined, internal: boolean): SessionQueueMode {
|
|
125
|
+
if (mode === 'steer' || mode === 'collect' || mode === 'followup') return mode
|
|
126
|
+
return internal ? 'collect' : 'followup'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cancelPendingForSession(sessionId: string, reason: string): number {
|
|
130
|
+
let cancelled = 0
|
|
131
|
+
for (const [key, queue] of state.queueByExecution.entries()) {
|
|
132
|
+
if (!queue.length) continue
|
|
133
|
+
const keep: QueueEntry[] = []
|
|
134
|
+
for (const entry of queue) {
|
|
135
|
+
if (entry.run.sessionId !== sessionId) {
|
|
136
|
+
keep.push(entry)
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
entry.run.status = 'cancelled'
|
|
140
|
+
entry.run.endedAt = now()
|
|
141
|
+
entry.run.error = reason
|
|
142
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
143
|
+
entry.reject(new Error(reason))
|
|
144
|
+
cancelled++
|
|
145
|
+
}
|
|
146
|
+
if (keep.length > 0) state.queueByExecution.set(key, keep)
|
|
147
|
+
else state.queueByExecution.delete(key)
|
|
148
|
+
}
|
|
149
|
+
return cancelled
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'): { cancelledQueued: number; abortedRunning: number } {
|
|
153
|
+
let cancelledQueued = 0
|
|
154
|
+
let abortedRunning = 0
|
|
155
|
+
|
|
156
|
+
for (const [key, queue] of state.queueByExecution.entries()) {
|
|
157
|
+
if (!queue.length) continue
|
|
158
|
+
const keep: QueueEntry[] = []
|
|
159
|
+
for (const entry of queue) {
|
|
160
|
+
const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
|
|
161
|
+
if (!isHeartbeat) {
|
|
162
|
+
keep.push(entry)
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
entry.run.status = 'cancelled'
|
|
166
|
+
entry.run.endedAt = now()
|
|
167
|
+
entry.run.error = reason
|
|
168
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
169
|
+
entry.reject(new Error(reason))
|
|
170
|
+
cancelledQueued += 1
|
|
171
|
+
}
|
|
172
|
+
if (keep.length > 0) state.queueByExecution.set(key, keep)
|
|
173
|
+
else state.queueByExecution.delete(key)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const entry of state.runningByExecution.values()) {
|
|
177
|
+
const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
|
|
178
|
+
if (!isHeartbeat) continue
|
|
179
|
+
abortedRunning += 1
|
|
180
|
+
entry.signalController.abort()
|
|
181
|
+
try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { cancelledQueued, abortedRunning }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupRequest) {
|
|
188
|
+
const delayMs = Math.max(0, Math.trunc(followup.delayMs || 0))
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
try {
|
|
191
|
+
const sessions = loadSessions()
|
|
192
|
+
const session = sessions[sessionId]
|
|
193
|
+
if (!session || session.name !== '__main__') return
|
|
194
|
+
enqueueSessionRun({
|
|
195
|
+
sessionId,
|
|
196
|
+
message: followup.message,
|
|
197
|
+
internal: true,
|
|
198
|
+
source: 'main-loop-followup',
|
|
199
|
+
mode: 'collect',
|
|
200
|
+
dedupeKey: followup.dedupeKey,
|
|
201
|
+
})
|
|
202
|
+
} catch (err: any) {
|
|
203
|
+
log.warn('session-run', `Failed to enqueue main-loop followup for ${sessionId}`, err?.message || String(err))
|
|
204
|
+
}
|
|
205
|
+
}, delayMs)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function drainExecution(executionKey: string): Promise<void> {
|
|
209
|
+
if (state.runningByExecution.has(executionKey)) return
|
|
210
|
+
const q = queueForExecution(executionKey)
|
|
211
|
+
const next = q.shift()
|
|
212
|
+
if (!next) return
|
|
213
|
+
|
|
214
|
+
state.runningByExecution.set(executionKey, next)
|
|
215
|
+
next.run.status = 'running'
|
|
216
|
+
next.run.startedAt = now()
|
|
217
|
+
emitRunMeta(next, 'running')
|
|
218
|
+
log.info('session-run', `Run started ${next.run.id}`, {
|
|
219
|
+
sessionId: next.run.sessionId,
|
|
220
|
+
source: next.run.source,
|
|
221
|
+
internal: next.run.internal,
|
|
222
|
+
mode: next.run.mode,
|
|
223
|
+
timeoutMs: next.maxRuntimeMs || null,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
let runtimeTimer: ReturnType<typeof setTimeout> | null = null
|
|
227
|
+
if (next.maxRuntimeMs && next.maxRuntimeMs > 0) {
|
|
228
|
+
runtimeTimer = setTimeout(() => {
|
|
229
|
+
next.signalController.abort()
|
|
230
|
+
}, next.maxRuntimeMs)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = await executeSessionChatTurn({
|
|
235
|
+
sessionId: next.run.sessionId,
|
|
236
|
+
message: next.message,
|
|
237
|
+
imagePath: next.imagePath,
|
|
238
|
+
imageUrl: next.imageUrl,
|
|
239
|
+
internal: next.run.internal,
|
|
240
|
+
source: next.run.source,
|
|
241
|
+
runId: next.run.id,
|
|
242
|
+
signal: next.signalController.signal,
|
|
243
|
+
onEvent: (event) => emitToSubscribers(next, event),
|
|
244
|
+
modelOverride: next.modelOverride,
|
|
245
|
+
heartbeatConfig: next.heartbeatConfig,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const failed = !!result.error
|
|
249
|
+
let followup: MainLoopFollowupRequest | null = null
|
|
250
|
+
try {
|
|
251
|
+
followup = handleMainLoopRunResult({
|
|
252
|
+
sessionId: next.run.sessionId,
|
|
253
|
+
message: next.message,
|
|
254
|
+
internal: next.run.internal,
|
|
255
|
+
source: next.run.source,
|
|
256
|
+
resultText: result.text,
|
|
257
|
+
error: result.error,
|
|
258
|
+
toolEvents: result.toolEvents,
|
|
259
|
+
})
|
|
260
|
+
} catch (mainLoopErr: any) {
|
|
261
|
+
log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
next.run.status = failed ? 'failed' : 'completed'
|
|
265
|
+
next.run.endedAt = now()
|
|
266
|
+
next.run.error = result.error
|
|
267
|
+
next.run.resultPreview = result.text?.slice(0, 280)
|
|
268
|
+
emitRunMeta(next, next.run.status, {
|
|
269
|
+
persisted: result.persisted,
|
|
270
|
+
hasText: !!result.text,
|
|
271
|
+
error: result.error || null,
|
|
272
|
+
})
|
|
273
|
+
log.info('session-run', `Run finished ${next.run.id}`, {
|
|
274
|
+
sessionId: next.run.sessionId,
|
|
275
|
+
status: next.run.status,
|
|
276
|
+
persisted: result.persisted,
|
|
277
|
+
hasText: !!result.text,
|
|
278
|
+
error: result.error || null,
|
|
279
|
+
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
280
|
+
})
|
|
281
|
+
next.resolve(result)
|
|
282
|
+
if (!failed && followup) {
|
|
283
|
+
scheduleMainLoopFollowup(next.run.sessionId, followup)
|
|
284
|
+
log.info('session-run', `Queued main-loop followup after ${next.run.id}`, {
|
|
285
|
+
sessionId: next.run.sessionId,
|
|
286
|
+
delayMs: followup.delayMs,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
} catch (err: any) {
|
|
290
|
+
const aborted = next.signalController.signal.aborted
|
|
291
|
+
next.run.status = aborted ? 'cancelled' : 'failed'
|
|
292
|
+
next.run.endedAt = now()
|
|
293
|
+
next.run.error = err?.message || String(err)
|
|
294
|
+
emitRunMeta(next, next.run.status, { error: next.run.error })
|
|
295
|
+
log.error('session-run', `Run failed ${next.run.id}`, {
|
|
296
|
+
sessionId: next.run.sessionId,
|
|
297
|
+
status: next.run.status,
|
|
298
|
+
error: next.run.error,
|
|
299
|
+
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
300
|
+
})
|
|
301
|
+
try {
|
|
302
|
+
handleMainLoopRunResult({
|
|
303
|
+
sessionId: next.run.sessionId,
|
|
304
|
+
message: next.message,
|
|
305
|
+
internal: next.run.internal,
|
|
306
|
+
source: next.run.source,
|
|
307
|
+
resultText: '',
|
|
308
|
+
error: next.run.error,
|
|
309
|
+
toolEvents: [],
|
|
310
|
+
})
|
|
311
|
+
} catch {
|
|
312
|
+
// Main-loop bookkeeping failures should not affect queue execution.
|
|
313
|
+
}
|
|
314
|
+
next.reject(err instanceof Error ? err : new Error(next.run.error))
|
|
315
|
+
} finally {
|
|
316
|
+
if (runtimeTimer) clearTimeout(runtimeTimer)
|
|
317
|
+
state.runningByExecution.delete(executionKey)
|
|
318
|
+
void drainExecution(executionKey)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function findDedupeMatch(sessionId: string, dedupeKey?: string): QueueEntry | null {
|
|
323
|
+
if (!dedupeKey) return null
|
|
324
|
+
const executionKey = executionKeyForSession(sessionId)
|
|
325
|
+
const running = state.runningByExecution.get(executionKey)
|
|
326
|
+
if (running?.run.sessionId === sessionId && running?.run.dedupeKey === dedupeKey) return running
|
|
327
|
+
const q = queueForExecution(executionKey)
|
|
328
|
+
return q.find((e) => e.run.sessionId === sessionId && e.run.dedupeKey === dedupeKey) || null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface EnqueueSessionRunInput {
|
|
332
|
+
sessionId: string
|
|
333
|
+
message: string
|
|
334
|
+
imagePath?: string
|
|
335
|
+
imageUrl?: string
|
|
336
|
+
internal?: boolean
|
|
337
|
+
source?: string
|
|
338
|
+
mode?: SessionQueueMode
|
|
339
|
+
onEvent?: (event: SSEEvent) => void
|
|
340
|
+
dedupeKey?: string
|
|
341
|
+
maxRuntimeMs?: number
|
|
342
|
+
modelOverride?: string
|
|
343
|
+
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface EnqueueSessionRunResult {
|
|
347
|
+
runId: string
|
|
348
|
+
position: number
|
|
349
|
+
deduped?: boolean
|
|
350
|
+
coalesced?: boolean
|
|
351
|
+
promise: Promise<ExecuteChatTurnResult>
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSessionRunResult {
|
|
355
|
+
const internal = input.internal === true
|
|
356
|
+
const mode = normalizeMode(input.mode, internal)
|
|
357
|
+
const source = input.source || 'chat'
|
|
358
|
+
const executionKey = executionKeyForSession(input.sessionId)
|
|
359
|
+
const runtime = loadRuntimeSettings()
|
|
360
|
+
const defaultMaxRuntimeMs = runtime.ongoingLoopMaxRuntimeMs ?? (10 * 60_000)
|
|
361
|
+
const effectiveMaxRuntimeMs = typeof input.maxRuntimeMs === 'number'
|
|
362
|
+
? input.maxRuntimeMs
|
|
363
|
+
: defaultMaxRuntimeMs
|
|
364
|
+
|
|
365
|
+
const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
|
|
366
|
+
if (dedupe) {
|
|
367
|
+
if (input.onEvent) dedupe.onEvents.push(input.onEvent)
|
|
368
|
+
return {
|
|
369
|
+
runId: dedupe.run.id,
|
|
370
|
+
position: 0,
|
|
371
|
+
deduped: true,
|
|
372
|
+
promise: dedupe.promise,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (mode === 'steer') {
|
|
377
|
+
const running = state.runningByExecution.get(executionKey)
|
|
378
|
+
if (running && running.run.sessionId === input.sessionId) {
|
|
379
|
+
running.signalController.abort()
|
|
380
|
+
try { active.get(input.sessionId)?.kill?.() } catch { /* noop */ }
|
|
381
|
+
}
|
|
382
|
+
cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const running = state.runningByExecution.get(executionKey)
|
|
386
|
+
const q = queueForExecution(executionKey)
|
|
387
|
+
if (mode === 'collect' && !input.imagePath && !input.imageUrl) {
|
|
388
|
+
const nowMs = now()
|
|
389
|
+
const candidate = q.at(-1)
|
|
390
|
+
const canCoalesce = !!candidate
|
|
391
|
+
&& candidate.run.mode === 'collect'
|
|
392
|
+
&& candidate.run.internal === internal
|
|
393
|
+
&& candidate.run.source === source
|
|
394
|
+
&& !candidate.imagePath
|
|
395
|
+
&& !candidate.imageUrl
|
|
396
|
+
&& (nowMs - candidate.run.queuedAt) <= COLLECT_COALESCE_WINDOW_MS
|
|
397
|
+
|
|
398
|
+
if (candidate && canCoalesce) {
|
|
399
|
+
const nextChunk = input.message.trim()
|
|
400
|
+
if (nextChunk) {
|
|
401
|
+
const current = candidate.message.trim()
|
|
402
|
+
candidate.message = current
|
|
403
|
+
? `${current}\n\n[Collected follow-up]\n${nextChunk}`
|
|
404
|
+
: nextChunk
|
|
405
|
+
candidate.run.messagePreview = messagePreview(candidate.message)
|
|
406
|
+
candidate.run.queuedAt = nowMs
|
|
407
|
+
}
|
|
408
|
+
if (input.onEvent) candidate.onEvents.push(input.onEvent)
|
|
409
|
+
emitRunMeta(candidate, 'queued', { position: 0, coalesced: true, mergedIntoRunId: candidate.run.id })
|
|
410
|
+
return {
|
|
411
|
+
runId: candidate.run.id,
|
|
412
|
+
position: 0,
|
|
413
|
+
coalesced: true,
|
|
414
|
+
promise: candidate.promise,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const runId = crypto.randomBytes(8).toString('hex')
|
|
420
|
+
const run: SessionRunRecord = {
|
|
421
|
+
id: runId,
|
|
422
|
+
sessionId: input.sessionId,
|
|
423
|
+
source,
|
|
424
|
+
internal,
|
|
425
|
+
mode,
|
|
426
|
+
status: 'queued',
|
|
427
|
+
messagePreview: messagePreview(input.message),
|
|
428
|
+
dedupeKey: input.dedupeKey,
|
|
429
|
+
queuedAt: now(),
|
|
430
|
+
}
|
|
431
|
+
registerRun(run)
|
|
432
|
+
|
|
433
|
+
let resolve!: (value: ExecuteChatTurnResult) => void
|
|
434
|
+
let reject!: (error: Error) => void
|
|
435
|
+
const promise = new Promise<ExecuteChatTurnResult>((res, rej) => {
|
|
436
|
+
resolve = res
|
|
437
|
+
reject = rej
|
|
438
|
+
})
|
|
439
|
+
state.promises.set(runId, promise)
|
|
440
|
+
|
|
441
|
+
const entry: QueueEntry = {
|
|
442
|
+
executionKey,
|
|
443
|
+
run,
|
|
444
|
+
message: input.message,
|
|
445
|
+
imagePath: input.imagePath,
|
|
446
|
+
imageUrl: input.imageUrl,
|
|
447
|
+
onEvents: input.onEvent ? [input.onEvent] : [],
|
|
448
|
+
signalController: new AbortController(),
|
|
449
|
+
maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|
|
450
|
+
modelOverride: input.modelOverride,
|
|
451
|
+
heartbeatConfig: input.heartbeatConfig,
|
|
452
|
+
resolve,
|
|
453
|
+
reject,
|
|
454
|
+
promise,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
q.push(entry)
|
|
458
|
+
const position = (running ? 1 : 0) + q.length - 1
|
|
459
|
+
emitRunMeta(entry, 'queued', { position })
|
|
460
|
+
void drainExecution(executionKey)
|
|
461
|
+
|
|
462
|
+
return { runId, position, promise }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function getSessionRunState(sessionId: string): {
|
|
466
|
+
runningRunId?: string
|
|
467
|
+
queueLength: number
|
|
468
|
+
} {
|
|
469
|
+
const executionKey = executionKeyForSession(sessionId)
|
|
470
|
+
const running = state.runningByExecution.get(executionKey)
|
|
471
|
+
const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
|
|
472
|
+
return {
|
|
473
|
+
runningRunId: running?.run.sessionId === sessionId ? running.run.id : undefined,
|
|
474
|
+
queueLength: queued,
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function getRunById(runId: string): SessionRunRecord | null {
|
|
479
|
+
return state.runs.get(runId) || null
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function listRuns(params?: {
|
|
483
|
+
sessionId?: string
|
|
484
|
+
status?: SessionRunStatus
|
|
485
|
+
limit?: number
|
|
486
|
+
}): SessionRunRecord[] {
|
|
487
|
+
const limit = Math.max(1, Math.min(1000, params?.limit ?? 200))
|
|
488
|
+
const ordered = [...state.recentRunIds].reverse()
|
|
489
|
+
const out: SessionRunRecord[] = []
|
|
490
|
+
for (const id of ordered) {
|
|
491
|
+
const run = state.runs.get(id)
|
|
492
|
+
if (!run) continue
|
|
493
|
+
if (params?.sessionId && run.sessionId !== params.sessionId) continue
|
|
494
|
+
if (params?.status && run.status !== params.status) continue
|
|
495
|
+
out.push(run)
|
|
496
|
+
if (out.length >= limit) break
|
|
497
|
+
}
|
|
498
|
+
return out
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function cancelSessionRuns(sessionId: string, reason = 'Cancelled'): { cancelledQueued: number; cancelledRunning: boolean } {
|
|
502
|
+
const executionKey = executionKeyForSession(sessionId)
|
|
503
|
+
const running = state.runningByExecution.get(executionKey)
|
|
504
|
+
let cancelledRunning = false
|
|
505
|
+
if (running && running.run.sessionId === sessionId) {
|
|
506
|
+
cancelledRunning = true
|
|
507
|
+
running.signalController.abort()
|
|
508
|
+
try { active.get(sessionId)?.kill?.() } catch { /* noop */ }
|
|
509
|
+
}
|
|
510
|
+
const cancelledQueued = cancelPendingForSession(sessionId, reason)
|
|
511
|
+
return { cancelledQueued, cancelledRunning }
|
|
512
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { loadConnectors, loadSettings } from '../storage'
|
|
4
|
+
import type { ToolBuildContext } from './context'
|
|
5
|
+
|
|
6
|
+
export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
7
|
+
const tools: StructuredToolInterface[] = []
|
|
8
|
+
const { ctx, hasTool } = bctx
|
|
9
|
+
|
|
10
|
+
if (hasTool('manage_connectors')) {
|
|
11
|
+
tools.push(
|
|
12
|
+
tool(
|
|
13
|
+
async ({ action, connectorId, platform, to, message, imageUrl, fileUrl, mediaPath, mimeType, fileName, caption, approved }) => {
|
|
14
|
+
try {
|
|
15
|
+
const normalizeWhatsAppTarget = (input: string): string => {
|
|
16
|
+
const raw = input.trim()
|
|
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')
|
|
29
|
+
const running = listRunningConnectors(platform || undefined)
|
|
30
|
+
|
|
31
|
+
if (action === 'list_running' || action === 'list_targets') {
|
|
32
|
+
return JSON.stringify(running)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (action === 'send') {
|
|
36
|
+
const settings = loadSettings()
|
|
37
|
+
if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
|
|
38
|
+
return 'Error: outbound connector sends require explicit approval. Re-run with approved=true after user confirmation.'
|
|
39
|
+
}
|
|
40
|
+
const hasText = !!message?.trim()
|
|
41
|
+
const hasMedia = !!imageUrl?.trim() || !!fileUrl?.trim()
|
|
42
|
+
if (!hasText && !hasMedia) return 'Error: message or media URL is required for send action.'
|
|
43
|
+
if (!running.length) {
|
|
44
|
+
return `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}.`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const selected = connectorId
|
|
48
|
+
? running.find((c) => c.id === connectorId)
|
|
49
|
+
: running[0]
|
|
50
|
+
if (!selected) return `Error: running connector not found: ${connectorId}`
|
|
51
|
+
|
|
52
|
+
const connectors = loadConnectors()
|
|
53
|
+
const connector = connectors[selected.id]
|
|
54
|
+
if (!connector) return `Error: connector not found: ${selected.id}`
|
|
55
|
+
|
|
56
|
+
let channelId = to?.trim() || ''
|
|
57
|
+
if (!channelId) {
|
|
58
|
+
const outbound = connector.config?.outboundJid?.trim()
|
|
59
|
+
if (outbound) channelId = outbound
|
|
60
|
+
}
|
|
61
|
+
if (!channelId) {
|
|
62
|
+
const recentChannelId = getConnectorRecentChannelId(selected.id)
|
|
63
|
+
if (recentChannelId) channelId = recentChannelId
|
|
64
|
+
}
|
|
65
|
+
if (!channelId) {
|
|
66
|
+
const allowed = connector.config?.allowedJids?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
|
|
67
|
+
if (allowed.length) channelId = allowed[0]
|
|
68
|
+
}
|
|
69
|
+
if (!channelId) {
|
|
70
|
+
return `Error: no target recipient configured. Provide "to", or set connector config "outboundJid"/"allowedJids".`
|
|
71
|
+
}
|
|
72
|
+
if (connector.platform === 'whatsapp') {
|
|
73
|
+
channelId = normalizeWhatsAppTarget(channelId)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sent = await sendConnectorMessage({
|
|
77
|
+
connectorId: selected.id,
|
|
78
|
+
channelId,
|
|
79
|
+
text: message?.trim() || '',
|
|
80
|
+
imageUrl: imageUrl?.trim() || undefined,
|
|
81
|
+
fileUrl: fileUrl?.trim() || undefined,
|
|
82
|
+
mediaPath: mediaPath?.trim() || undefined,
|
|
83
|
+
mimeType: mimeType?.trim() || undefined,
|
|
84
|
+
fileName: fileName?.trim() || undefined,
|
|
85
|
+
caption: caption?.trim() || undefined,
|
|
86
|
+
})
|
|
87
|
+
return JSON.stringify({
|
|
88
|
+
status: 'sent',
|
|
89
|
+
connectorId: sent.connectorId,
|
|
90
|
+
platform: sent.platform,
|
|
91
|
+
to: sent.channelId,
|
|
92
|
+
messageId: sent.messageId || null,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return 'Unknown action. Use list_running, list_targets, or send.'
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
return `Error: ${err.message || String(err)}`
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'connector_message_tool',
|
|
103
|
+
description: 'Send proactive outbound messages through running connectors (for example WhatsApp status updates). Supports listing running connectors/targets and sending text plus optional media (URLs or local file paths).',
|
|
104
|
+
schema: z.object({
|
|
105
|
+
action: z.enum(['list_running', 'list_targets', 'send']).describe('connector messaging action'),
|
|
106
|
+
connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
|
|
107
|
+
platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord).'),
|
|
108
|
+
to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
|
|
109
|
+
message: z.string().optional().describe('Message text to send (required for send action).'),
|
|
110
|
+
imageUrl: z.string().optional().describe('Optional public image URL to attach/send where platform supports media.'),
|
|
111
|
+
fileUrl: z.string().optional().describe('Optional public file URL to attach/send where platform supports documents.'),
|
|
112
|
+
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.'),
|
|
113
|
+
mimeType: z.string().optional().describe('Optional MIME type for mediaPath or fileUrl.'),
|
|
114
|
+
fileName: z.string().optional().describe('Optional display file name for mediaPath or fileUrl.'),
|
|
115
|
+
caption: z.string().optional().describe('Optional caption used with image/file sends.'),
|
|
116
|
+
approved: z.boolean().optional().describe('Set true to explicitly confirm outbound send when safetyRequireApprovalForOutbound is enabled.'),
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tools
|
|
124
|
+
}
|