@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,559 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import {
|
|
3
|
+
loadConnectors, saveConnectors, loadSessions, saveSessions,
|
|
4
|
+
loadAgents, loadCredentials, decryptKey, loadSettings, loadSkills,
|
|
5
|
+
} from '../storage'
|
|
6
|
+
import { streamAgentChat } from '../stream-agent-chat'
|
|
7
|
+
import { logExecution } from '../execution-log'
|
|
8
|
+
import type { Connector } from '@/types'
|
|
9
|
+
import type { ConnectorInstance, InboundMessage, InboundMedia } from './types'
|
|
10
|
+
|
|
11
|
+
/** Sentinel value agents return when no outbound reply should be sent */
|
|
12
|
+
export const NO_MESSAGE_SENTINEL = 'NO_MESSAGE'
|
|
13
|
+
|
|
14
|
+
/** Check if an agent response is the NO_MESSAGE sentinel (case-insensitive, trimmed) */
|
|
15
|
+
export function isNoMessage(text: string): boolean {
|
|
16
|
+
return text.trim().toUpperCase() === NO_MESSAGE_SENTINEL
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Map of running connector instances by connector ID.
|
|
20
|
+
* Stored on globalThis to survive HMR reloads in dev mode —
|
|
21
|
+
* prevents duplicate sockets fighting for the same WhatsApp session. */
|
|
22
|
+
const globalKey = '__swarmclaw_running_connectors__' as const
|
|
23
|
+
const running: Map<string, ConnectorInstance> =
|
|
24
|
+
(globalThis as any)[globalKey] ?? ((globalThis as any)[globalKey] = new Map<string, ConnectorInstance>())
|
|
25
|
+
|
|
26
|
+
/** Most recent inbound channel per connector (used for proactive replies/default outbound target) */
|
|
27
|
+
const lastInboundKey = '__swarmclaw_connector_last_inbound__' as const
|
|
28
|
+
const lastInboundChannelByConnector: Map<string, string> =
|
|
29
|
+
(globalThis as any)[lastInboundKey] ?? ((globalThis as any)[lastInboundKey] = new Map<string, string>())
|
|
30
|
+
|
|
31
|
+
/** Per-connector lock to prevent concurrent start/stop operations */
|
|
32
|
+
const lockKey = '__swarmclaw_connector_locks__' as const
|
|
33
|
+
const locks: Map<string, Promise<void>> =
|
|
34
|
+
(globalThis as any)[lockKey] ?? ((globalThis as any)[lockKey] = new Map<string, Promise<void>>())
|
|
35
|
+
|
|
36
|
+
/** Get platform implementation lazily */
|
|
37
|
+
export async function getPlatform(platform: string) {
|
|
38
|
+
switch (platform) {
|
|
39
|
+
case 'discord': return (await import('./discord')).default
|
|
40
|
+
case 'telegram': return (await import('./telegram')).default
|
|
41
|
+
case 'slack': return (await import('./slack')).default
|
|
42
|
+
case 'whatsapp': return (await import('./whatsapp')).default
|
|
43
|
+
case 'openclaw': return (await import('./openclaw')).default
|
|
44
|
+
case 'signal': return (await import('./signal')).default
|
|
45
|
+
case 'teams': return (await import('./teams')).default
|
|
46
|
+
case 'googlechat': return (await import('./googlechat')).default
|
|
47
|
+
case 'matrix': return (await import('./matrix')).default
|
|
48
|
+
default: throw new Error(`Unknown platform: ${platform}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatMediaLine(media: InboundMedia): string {
|
|
53
|
+
const typeLabel = media.type.toUpperCase()
|
|
54
|
+
const name = media.fileName || media.mimeType || 'attachment'
|
|
55
|
+
const size = media.sizeBytes ? ` (${Math.max(1, Math.round(media.sizeBytes / 1024))} KB)` : ''
|
|
56
|
+
if (media.url) return `- ${typeLabel}: ${name}${size} -> ${media.url}`
|
|
57
|
+
return `- ${typeLabel}: ${name}${size}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatInboundUserText(msg: InboundMessage): string {
|
|
61
|
+
const baseText = (msg.text || '').trim()
|
|
62
|
+
const lines: string[] = []
|
|
63
|
+
if (baseText) lines.push(`[${msg.senderName}] ${baseText}`)
|
|
64
|
+
else lines.push(`[${msg.senderName}]`)
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(msg.media) && msg.media.length > 0) {
|
|
67
|
+
lines.push('')
|
|
68
|
+
lines.push('Media received:')
|
|
69
|
+
const preview = msg.media.slice(0, 6)
|
|
70
|
+
for (const media of preview) lines.push(formatMediaLine(media))
|
|
71
|
+
if (msg.media.length > preview.length) {
|
|
72
|
+
lines.push(`- ...and ${msg.media.length - preview.length} more attachment(s)`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.join('\n').trim()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Route an inbound message through the assigned agent and return the response */
|
|
80
|
+
async function routeMessage(connector: Connector, msg: InboundMessage): Promise<string> {
|
|
81
|
+
if (msg?.channelId) {
|
|
82
|
+
lastInboundChannelByConnector.set(connector.id, msg.channelId)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const agents = loadAgents()
|
|
86
|
+
const agent = agents[connector.agentId]
|
|
87
|
+
if (!agent) return '[Error] Connector agent not found.'
|
|
88
|
+
|
|
89
|
+
// Log connector trigger
|
|
90
|
+
const triggerSessionKey = `connector:${connector.id}:${msg.channelId}`
|
|
91
|
+
const allSessions = loadSessions()
|
|
92
|
+
const existingSession = Object.values(allSessions).find((s: any) => s.name === triggerSessionKey)
|
|
93
|
+
if (existingSession) {
|
|
94
|
+
logExecution(existingSession.id, 'trigger', `${msg.platform} message from ${msg.senderName}`, {
|
|
95
|
+
agentId: agent.id,
|
|
96
|
+
detail: {
|
|
97
|
+
source: 'connector',
|
|
98
|
+
platform: msg.platform,
|
|
99
|
+
connectorId: connector.id,
|
|
100
|
+
channelId: msg.channelId,
|
|
101
|
+
senderName: msg.senderName,
|
|
102
|
+
messagePreview: (msg.text || '').slice(0, 200),
|
|
103
|
+
hasMedia: !!(msg.media?.length || msg.imageUrl),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Resolve API key for the agent's provider
|
|
109
|
+
let apiKey: string | null = null
|
|
110
|
+
if (agent.credentialId) {
|
|
111
|
+
const creds = loadCredentials()
|
|
112
|
+
const cred = creds[agent.credentialId]
|
|
113
|
+
if (cred?.encryptedKey) {
|
|
114
|
+
try { apiKey = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Find or create a session keyed by platform + channel
|
|
119
|
+
const sessionKey = `connector:${connector.id}:${msg.channelId}`
|
|
120
|
+
const sessions = loadSessions()
|
|
121
|
+
let session = Object.values(sessions).find((s: any) => s.name === sessionKey)
|
|
122
|
+
if (!session) {
|
|
123
|
+
const id = crypto.randomBytes(4).toString('hex')
|
|
124
|
+
session = {
|
|
125
|
+
id,
|
|
126
|
+
name: sessionKey,
|
|
127
|
+
cwd: process.cwd(),
|
|
128
|
+
user: 'connector',
|
|
129
|
+
provider: agent.provider === 'claude-cli' ? 'anthropic' : agent.provider,
|
|
130
|
+
model: agent.model,
|
|
131
|
+
credentialId: agent.credentialId || null,
|
|
132
|
+
apiEndpoint: agent.apiEndpoint || null,
|
|
133
|
+
claudeSessionId: null,
|
|
134
|
+
codexThreadId: null,
|
|
135
|
+
opencodeSessionId: null,
|
|
136
|
+
delegateResumeIds: {
|
|
137
|
+
claudeCode: null,
|
|
138
|
+
codex: null,
|
|
139
|
+
opencode: null,
|
|
140
|
+
},
|
|
141
|
+
messages: [],
|
|
142
|
+
createdAt: Date.now(),
|
|
143
|
+
lastActiveAt: Date.now(),
|
|
144
|
+
sessionType: 'human' as const,
|
|
145
|
+
agentId: agent.id,
|
|
146
|
+
tools: agent.tools || [],
|
|
147
|
+
}
|
|
148
|
+
sessions[id] = session
|
|
149
|
+
saveSessions(sessions)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build system prompt: [userPrompt] \n\n [soul] \n\n [systemPrompt]
|
|
153
|
+
const settings = loadSettings()
|
|
154
|
+
const promptParts: string[] = []
|
|
155
|
+
if (settings.userPrompt) promptParts.push(settings.userPrompt)
|
|
156
|
+
if (agent.soul) promptParts.push(agent.soul)
|
|
157
|
+
if (agent.systemPrompt) promptParts.push(agent.systemPrompt)
|
|
158
|
+
if (agent.skillIds?.length) {
|
|
159
|
+
const allSkills = loadSkills()
|
|
160
|
+
for (const skillId of agent.skillIds) {
|
|
161
|
+
const skill = allSkills[skillId]
|
|
162
|
+
if (skill?.content) promptParts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Add connector context
|
|
166
|
+
promptParts.push(`\nYou are receiving messages via ${msg.platform}. The user "${msg.senderName}" is messaging from channel "${msg.channelName || msg.channelId}". Respond naturally and conversationally.
|
|
167
|
+
|
|
168
|
+
## Knowing When Not to Reply
|
|
169
|
+
Real conversations have natural pauses — not every message needs a response. Reply with exactly "NO_MESSAGE" (nothing else) to stay silent when replying would feel unnatural or forced.
|
|
170
|
+
Stay silent for simple acknowledgments ("okay", "alright", "cool", "got it", "sounds good"), conversation closers ("thanks", "bye", "night", "ttyl"), reactions (emoji, "haha", "lol"), and forwarded content with no question attached.
|
|
171
|
+
Always reply when there's a question, task, instruction, emotional sharing, or something genuinely useful to add.
|
|
172
|
+
The test: would a thoughtful friend feel compelled to type something back? If not, NO_MESSAGE.`)
|
|
173
|
+
const systemPrompt = promptParts.join('\n\n')
|
|
174
|
+
|
|
175
|
+
// Add message to session
|
|
176
|
+
const firstImage = msg.media?.find((m) => m.type === 'image')
|
|
177
|
+
const firstImageUrl = msg.imageUrl || (firstImage?.url) || undefined
|
|
178
|
+
const firstImagePath = firstImage?.localPath || undefined
|
|
179
|
+
const inboundText = formatInboundUserText(msg)
|
|
180
|
+
session.messages.push({
|
|
181
|
+
role: 'user',
|
|
182
|
+
text: inboundText,
|
|
183
|
+
time: Date.now(),
|
|
184
|
+
imageUrl: firstImageUrl,
|
|
185
|
+
imagePath: firstImagePath,
|
|
186
|
+
})
|
|
187
|
+
session.lastActiveAt = Date.now()
|
|
188
|
+
const s1 = loadSessions()
|
|
189
|
+
s1[session.id] = session
|
|
190
|
+
saveSessions(s1)
|
|
191
|
+
|
|
192
|
+
// Stream the response
|
|
193
|
+
let fullText = ''
|
|
194
|
+
const hasTools = session.tools?.length && session.provider !== 'claude-cli'
|
|
195
|
+
console.log(`[connector] Routing message to agent "${agent.name}" (${agent.provider}/${agent.model}), hasTools=${!!hasTools}`)
|
|
196
|
+
|
|
197
|
+
if (hasTools) {
|
|
198
|
+
try {
|
|
199
|
+
const result = await streamAgentChat({
|
|
200
|
+
session,
|
|
201
|
+
message: msg.text,
|
|
202
|
+
imagePath: firstImagePath,
|
|
203
|
+
apiKey,
|
|
204
|
+
systemPrompt,
|
|
205
|
+
write: () => {}, // no SSE needed for connectors
|
|
206
|
+
history: session.messages,
|
|
207
|
+
})
|
|
208
|
+
// Use finalResponse for connectors — strips intermediate planning/tool-use text
|
|
209
|
+
fullText = result.finalResponse
|
|
210
|
+
console.log(`[connector] streamAgentChat returned ${result.fullText.length} chars total, ${fullText.length} chars final`)
|
|
211
|
+
} catch (err: any) {
|
|
212
|
+
console.error(`[connector] streamAgentChat error:`, err.message || err)
|
|
213
|
+
return `[Error] ${err.message}`
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// Use the provider directly
|
|
217
|
+
const { getProvider } = await import('../../providers')
|
|
218
|
+
const provider = getProvider(session.provider)
|
|
219
|
+
if (!provider) return '[Error] Provider not found.'
|
|
220
|
+
|
|
221
|
+
await provider.handler.streamChat({
|
|
222
|
+
session,
|
|
223
|
+
message: msg.text,
|
|
224
|
+
imagePath: firstImagePath,
|
|
225
|
+
apiKey,
|
|
226
|
+
systemPrompt,
|
|
227
|
+
write: (data: string) => {
|
|
228
|
+
if (data.startsWith('data: ')) {
|
|
229
|
+
try {
|
|
230
|
+
const event = JSON.parse(data.slice(6))
|
|
231
|
+
if (event.t === 'd') fullText += event.text || ''
|
|
232
|
+
else if (event.t === 'r') fullText = event.text || ''
|
|
233
|
+
} catch { /* ignore */ }
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
active: new Map(),
|
|
237
|
+
loadHistory: () => session.messages,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If the agent chose NO_MESSAGE, skip saving it to history — the user's message
|
|
242
|
+
// is already recorded, and saving the sentinel would pollute the LLM's context
|
|
243
|
+
if (isNoMessage(fullText)) {
|
|
244
|
+
console.log(`[connector] Agent returned NO_MESSAGE — suppressing outbound reply`)
|
|
245
|
+
logExecution(session.id, 'decision', 'Agent suppressed outbound (NO_MESSAGE)', {
|
|
246
|
+
agentId: agent.id,
|
|
247
|
+
detail: { platform: msg.platform, channelId: msg.channelId },
|
|
248
|
+
})
|
|
249
|
+
return NO_MESSAGE_SENTINEL
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Log outbound message
|
|
253
|
+
logExecution(session.id, 'outbound', `Reply sent via ${msg.platform}`, {
|
|
254
|
+
agentId: agent.id,
|
|
255
|
+
detail: {
|
|
256
|
+
platform: msg.platform,
|
|
257
|
+
channelId: msg.channelId,
|
|
258
|
+
recipientName: msg.senderName,
|
|
259
|
+
responsePreview: fullText.slice(0, 500),
|
|
260
|
+
responseLength: fullText.length,
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// Save assistant response to session
|
|
265
|
+
if (fullText.trim()) {
|
|
266
|
+
session.messages.push({ role: 'assistant', text: fullText.trim(), time: Date.now() })
|
|
267
|
+
session.lastActiveAt = Date.now()
|
|
268
|
+
const s2 = loadSessions()
|
|
269
|
+
s2[session.id] = session
|
|
270
|
+
saveSessions(s2)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return fullText || '(no response)'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Start a connector (serialized per ID to prevent concurrent start/stop races) */
|
|
277
|
+
export async function startConnector(connectorId: string): Promise<void> {
|
|
278
|
+
// Wait for any pending operation on this connector to finish (with timeout)
|
|
279
|
+
const pending = locks.get(connectorId)
|
|
280
|
+
if (pending) {
|
|
281
|
+
await Promise.race([pending, new Promise(r => setTimeout(r, 15_000))]).catch(() => {})
|
|
282
|
+
locks.delete(connectorId)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const op = withTimeout(_startConnectorImpl(connectorId), 30_000, 'Connector start timed out')
|
|
286
|
+
locks.set(connectorId, op)
|
|
287
|
+
try { await op } finally {
|
|
288
|
+
if (locks.get(connectorId) === op) locks.delete(connectorId)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, msg: string): Promise<T> {
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const timer = setTimeout(() => reject(new Error(msg)), ms)
|
|
295
|
+
promise.then(resolve, reject).finally(() => clearTimeout(timer))
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function _startConnectorImpl(connectorId: string): Promise<void> {
|
|
300
|
+
// If already running, stop it first (handles stale entries)
|
|
301
|
+
if (running.has(connectorId)) {
|
|
302
|
+
try {
|
|
303
|
+
const existing = running.get(connectorId)
|
|
304
|
+
await existing?.stop()
|
|
305
|
+
} catch { /* ignore cleanup errors */ }
|
|
306
|
+
running.delete(connectorId)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const connectors = loadConnectors()
|
|
310
|
+
const connector = connectors[connectorId] as Connector | undefined
|
|
311
|
+
if (!connector) throw new Error('Connector not found')
|
|
312
|
+
|
|
313
|
+
// Resolve bot token from credential
|
|
314
|
+
let botToken = ''
|
|
315
|
+
if (connector.credentialId) {
|
|
316
|
+
const creds = loadCredentials()
|
|
317
|
+
const cred = creds[connector.credentialId]
|
|
318
|
+
if (cred?.encryptedKey) {
|
|
319
|
+
try { botToken = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Also check config for inline token (some platforms)
|
|
323
|
+
if (!botToken && connector.config.botToken) {
|
|
324
|
+
botToken = connector.config.botToken
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!botToken && connector.platform !== 'whatsapp' && connector.platform !== 'openclaw' && connector.platform !== 'signal') {
|
|
328
|
+
throw new Error('No bot token configured')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const platform = await getPlatform(connector.platform)
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const instance = await platform.start(connector, botToken, (msg) => routeMessage(connector, msg))
|
|
335
|
+
running.set(connectorId, instance)
|
|
336
|
+
|
|
337
|
+
// Update status in storage
|
|
338
|
+
connector.status = 'running'
|
|
339
|
+
connector.isEnabled = true
|
|
340
|
+
connector.lastError = null
|
|
341
|
+
connector.updatedAt = Date.now()
|
|
342
|
+
connectors[connectorId] = connector
|
|
343
|
+
saveConnectors(connectors)
|
|
344
|
+
|
|
345
|
+
console.log(`[connector] Started ${connector.platform} connector: ${connector.name}`)
|
|
346
|
+
} catch (err: any) {
|
|
347
|
+
connector.status = 'error'
|
|
348
|
+
connector.isEnabled = false
|
|
349
|
+
connector.lastError = err.message
|
|
350
|
+
connector.updatedAt = Date.now()
|
|
351
|
+
connectors[connectorId] = connector
|
|
352
|
+
saveConnectors(connectors)
|
|
353
|
+
throw err
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Stop a connector */
|
|
358
|
+
export async function stopConnector(connectorId: string): Promise<void> {
|
|
359
|
+
const instance = running.get(connectorId)
|
|
360
|
+
if (instance) {
|
|
361
|
+
await instance.stop()
|
|
362
|
+
running.delete(connectorId)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const connectors = loadConnectors()
|
|
366
|
+
const connector = connectors[connectorId]
|
|
367
|
+
if (connector) {
|
|
368
|
+
connector.status = 'stopped'
|
|
369
|
+
connector.isEnabled = false
|
|
370
|
+
connector.lastError = null
|
|
371
|
+
connector.updatedAt = Date.now()
|
|
372
|
+
connectors[connectorId] = connector
|
|
373
|
+
saveConnectors(connectors)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log(`[connector] Stopped connector: ${connectorId}`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Get the runtime status of a connector */
|
|
380
|
+
export function getConnectorStatus(connectorId: string): 'running' | 'stopped' {
|
|
381
|
+
return running.has(connectorId) ? 'running' : 'stopped'
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Get the QR code data URL for a WhatsApp connector (null if not available) */
|
|
385
|
+
export function getConnectorQR(connectorId: string): string | null {
|
|
386
|
+
const instance = running.get(connectorId)
|
|
387
|
+
return instance?.qrDataUrl ?? null
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Check if a WhatsApp connector has authenticated (paired) */
|
|
391
|
+
export function isConnectorAuthenticated(connectorId: string): boolean {
|
|
392
|
+
const instance = running.get(connectorId)
|
|
393
|
+
if (!instance) return false
|
|
394
|
+
return instance.authenticated === true
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Check if a WhatsApp connector has stored credentials */
|
|
398
|
+
export function hasConnectorCredentials(connectorId: string): boolean {
|
|
399
|
+
const instance = running.get(connectorId)
|
|
400
|
+
if (!instance) return false
|
|
401
|
+
return instance.hasCredentials === true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Clear WhatsApp auth state and restart connector for fresh QR pairing */
|
|
405
|
+
export async function repairConnector(connectorId: string): Promise<void> {
|
|
406
|
+
// Stop existing instance
|
|
407
|
+
const instance = running.get(connectorId)
|
|
408
|
+
if (instance) {
|
|
409
|
+
await instance.stop()
|
|
410
|
+
running.delete(connectorId)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Clear auth directory
|
|
414
|
+
const { clearAuthDir } = await import('./whatsapp')
|
|
415
|
+
clearAuthDir(connectorId)
|
|
416
|
+
|
|
417
|
+
// Restart the connector — will get fresh QR
|
|
418
|
+
await startConnector(connectorId)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Stop all running connectors (for cleanup) */
|
|
422
|
+
export async function stopAllConnectors(): Promise<void> {
|
|
423
|
+
for (const [id] of running) {
|
|
424
|
+
await stopConnector(id)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Auto-start connectors that are marked as enabled (skips already-running ones) */
|
|
429
|
+
export async function autoStartConnectors(): Promise<void> {
|
|
430
|
+
const connectors = loadConnectors()
|
|
431
|
+
for (const connector of Object.values(connectors) as Connector[]) {
|
|
432
|
+
if (connector.isEnabled && !running.has(connector.id)) {
|
|
433
|
+
try {
|
|
434
|
+
console.log(`[connector] Auto-starting ${connector.platform} connector: ${connector.name}`)
|
|
435
|
+
await startConnector(connector.id)
|
|
436
|
+
} catch (err: any) {
|
|
437
|
+
console.error(`[connector] Failed to auto-start ${connector.name}:`, err.message)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** List connector IDs that are currently running (optionally by platform) */
|
|
444
|
+
export function listRunningConnectors(platform?: string): Array<{
|
|
445
|
+
id: string
|
|
446
|
+
name: string
|
|
447
|
+
platform: string
|
|
448
|
+
supportsSend: boolean
|
|
449
|
+
configuredTargets: string[]
|
|
450
|
+
recentChannelId: string | null
|
|
451
|
+
}> {
|
|
452
|
+
const connectors = loadConnectors()
|
|
453
|
+
const out: Array<{
|
|
454
|
+
id: string
|
|
455
|
+
name: string
|
|
456
|
+
platform: string
|
|
457
|
+
supportsSend: boolean
|
|
458
|
+
configuredTargets: string[]
|
|
459
|
+
recentChannelId: string | null
|
|
460
|
+
}> = []
|
|
461
|
+
|
|
462
|
+
for (const [id, instance] of running.entries()) {
|
|
463
|
+
const connector = connectors[id] as Connector | undefined
|
|
464
|
+
if (!connector) continue
|
|
465
|
+
if (platform && connector.platform !== platform) continue
|
|
466
|
+
const configuredTargets: string[] = []
|
|
467
|
+
if (connector.platform === 'whatsapp') {
|
|
468
|
+
const outboundJid = connector.config?.outboundJid?.trim()
|
|
469
|
+
if (outboundJid) configuredTargets.push(outboundJid)
|
|
470
|
+
const allowed = connector.config?.allowedJids?.split(',').map((s) => s.trim()).filter(Boolean) || []
|
|
471
|
+
configuredTargets.push(...allowed)
|
|
472
|
+
}
|
|
473
|
+
out.push({
|
|
474
|
+
id,
|
|
475
|
+
name: connector.name,
|
|
476
|
+
platform: connector.platform,
|
|
477
|
+
supportsSend: typeof instance.sendMessage === 'function',
|
|
478
|
+
configuredTargets: Array.from(new Set(configuredTargets)),
|
|
479
|
+
recentChannelId: lastInboundChannelByConnector.get(id) || null,
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return out
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Get the most recent inbound channel id seen for a connector */
|
|
487
|
+
export function getConnectorRecentChannelId(connectorId: string): string | null {
|
|
488
|
+
return lastInboundChannelByConnector.get(connectorId) || null
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Send an outbound message through a running connector.
|
|
493
|
+
* Intended for proactive agent notifications (e.g. WhatsApp updates).
|
|
494
|
+
*/
|
|
495
|
+
export async function sendConnectorMessage(params: {
|
|
496
|
+
connectorId?: string
|
|
497
|
+
platform?: string
|
|
498
|
+
channelId: string
|
|
499
|
+
text: string
|
|
500
|
+
imageUrl?: string
|
|
501
|
+
fileUrl?: string
|
|
502
|
+
mediaPath?: string
|
|
503
|
+
mimeType?: string
|
|
504
|
+
fileName?: string
|
|
505
|
+
caption?: string
|
|
506
|
+
}): Promise<{ connectorId: string; platform: string; channelId: string; messageId?: string }> {
|
|
507
|
+
const connectors = loadConnectors()
|
|
508
|
+
const requestedId = params.connectorId?.trim()
|
|
509
|
+
let connector: Connector | undefined
|
|
510
|
+
let connectorId: string | undefined
|
|
511
|
+
|
|
512
|
+
if (requestedId) {
|
|
513
|
+
connector = connectors[requestedId] as Connector | undefined
|
|
514
|
+
connectorId = requestedId
|
|
515
|
+
if (!connector) throw new Error(`Connector not found: ${requestedId}`)
|
|
516
|
+
} else {
|
|
517
|
+
const candidates = Object.values(connectors) as Connector[]
|
|
518
|
+
const filtered = candidates.filter((c) => {
|
|
519
|
+
if (params.platform && c.platform !== params.platform) return false
|
|
520
|
+
return running.has(c.id)
|
|
521
|
+
})
|
|
522
|
+
if (!filtered.length) {
|
|
523
|
+
throw new Error(`No running connector found${params.platform ? ` for platform "${params.platform}"` : ''}.`)
|
|
524
|
+
}
|
|
525
|
+
connector = filtered[0]
|
|
526
|
+
connectorId = connector.id
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!connector || !connectorId) throw new Error('Connector resolution failed.')
|
|
530
|
+
|
|
531
|
+
const instance = running.get(connectorId)
|
|
532
|
+
if (!instance) {
|
|
533
|
+
throw new Error(`Connector "${connectorId}" is not running.`)
|
|
534
|
+
}
|
|
535
|
+
if (typeof instance.sendMessage !== 'function') {
|
|
536
|
+
throw new Error(`Connector "${connector.name}" (${connector.platform}) does not support outbound sends.`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Apply NO_MESSAGE filter at the delivery layer so all outbound paths respect it
|
|
540
|
+
if (isNoMessage(params.text) && !params.imageUrl && !params.fileUrl && !params.mediaPath) {
|
|
541
|
+
console.log(`[connector] sendConnectorMessage: NO_MESSAGE — suppressing outbound send`)
|
|
542
|
+
return { connectorId, platform: connector.platform, channelId: params.channelId }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const result = await instance.sendMessage(params.channelId, params.text, {
|
|
546
|
+
imageUrl: params.imageUrl,
|
|
547
|
+
fileUrl: params.fileUrl,
|
|
548
|
+
mediaPath: params.mediaPath,
|
|
549
|
+
mimeType: params.mimeType,
|
|
550
|
+
fileName: params.fileName,
|
|
551
|
+
caption: params.caption,
|
|
552
|
+
})
|
|
553
|
+
return {
|
|
554
|
+
connectorId,
|
|
555
|
+
platform: connector.platform,
|
|
556
|
+
channelId: params.channelId,
|
|
557
|
+
messageId: result?.messageId,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { DATA_DIR } from '../data-dir'
|
|
4
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
5
|
+
import { isNoMessage } from './manager'
|
|
6
|
+
|
|
7
|
+
const matrix: PlatformConnector = {
|
|
8
|
+
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
9
|
+
const pkg = 'matrix-bot-sdk'
|
|
10
|
+
const { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } = await import(/* webpackIgnore: true */ pkg)
|
|
11
|
+
|
|
12
|
+
const homeserverUrl = connector.config.homeserverUrl
|
|
13
|
+
if (!homeserverUrl) throw new Error('Missing homeserverUrl in connector config')
|
|
14
|
+
|
|
15
|
+
// Ensure storage directory exists
|
|
16
|
+
const storageDir = path.join(DATA_DIR, 'matrix-storage', connector.id)
|
|
17
|
+
fs.mkdirSync(storageDir, { recursive: true })
|
|
18
|
+
|
|
19
|
+
const storage = new SimpleFsStorageProvider(path.join(storageDir, 'bot.json'))
|
|
20
|
+
const client = new MatrixClient(homeserverUrl, botToken, storage)
|
|
21
|
+
|
|
22
|
+
AutojoinRoomsMixin.setupOnClient(client)
|
|
23
|
+
|
|
24
|
+
// Optional: restrict to specific rooms
|
|
25
|
+
const allowedRooms = connector.config.roomIds
|
|
26
|
+
? connector.config.roomIds.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
27
|
+
: null
|
|
28
|
+
|
|
29
|
+
client.on('room.message', async (roomId: string, event: any) => {
|
|
30
|
+
// Ignore own messages
|
|
31
|
+
const userId = await client.getUserId()
|
|
32
|
+
if (event.sender === userId) return
|
|
33
|
+
|
|
34
|
+
// Ignore non-text messages and edits
|
|
35
|
+
if (!event.content?.body) return
|
|
36
|
+
if (event.content['m.relates_to']?.rel_type === 'm.replace') return
|
|
37
|
+
|
|
38
|
+
// Filter by allowed rooms if configured
|
|
39
|
+
if (allowedRooms && !allowedRooms.includes(roomId)) return
|
|
40
|
+
|
|
41
|
+
const inbound: InboundMessage = {
|
|
42
|
+
platform: 'matrix',
|
|
43
|
+
channelId: roomId,
|
|
44
|
+
channelName: roomId,
|
|
45
|
+
senderId: event.sender,
|
|
46
|
+
senderName: event.sender.split(':')[0].replace('@', '') || event.sender,
|
|
47
|
+
text: event.content.body || '',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await onMessage(inbound)
|
|
52
|
+
if (isNoMessage(response)) return
|
|
53
|
+
await client.sendText(roomId, response)
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
console.error(`[matrix] Error handling message:`, err.message)
|
|
56
|
+
try {
|
|
57
|
+
await client.sendText(roomId, 'Sorry, I encountered an error processing your message.')
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await client.start()
|
|
63
|
+
console.log(`[matrix] Bot connected to ${homeserverUrl}`)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
connector,
|
|
67
|
+
async sendMessage(channelId, text) {
|
|
68
|
+
await client.sendText(channelId, text)
|
|
69
|
+
},
|
|
70
|
+
async stop() {
|
|
71
|
+
client.stop()
|
|
72
|
+
console.log(`[matrix] Bot disconnected`)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default matrix
|