@swarmclawai/swarmclaw 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -2,9 +2,104 @@ import { App, LogLevel } from '@slack/bolt'
|
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import type { Connector } from '@/types'
|
|
5
|
-
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
5
|
+
import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundThreadHistoryEntry } from './types'
|
|
6
6
|
import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
|
|
7
|
-
import { isNoMessage } from './manager'
|
|
7
|
+
import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
|
|
8
|
+
|
|
9
|
+
function normalizeSlackEmoji(input: string): string {
|
|
10
|
+
const raw = input.trim().replace(/^:|:$/g, '')
|
|
11
|
+
if (!raw) return 'eyes'
|
|
12
|
+
if (raw === '👀') return 'eyes'
|
|
13
|
+
if (raw === '✅') return 'white_check_mark'
|
|
14
|
+
if (raw === '🤐') return 'zipper_mouth_face'
|
|
15
|
+
return raw
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseSlackTimestamp(raw: unknown): number {
|
|
19
|
+
const value = typeof raw === 'string' ? Number.parseFloat(raw) : typeof raw === 'number' ? raw : Number.NaN
|
|
20
|
+
return Number.isFinite(value) ? value : 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function resolveSlackUserDisplayName(client: any, userId?: string): Promise<string | undefined> {
|
|
24
|
+
if (!userId) return undefined
|
|
25
|
+
try {
|
|
26
|
+
const userInfo = await client.users.info({ user: userId })
|
|
27
|
+
return userInfo.user?.real_name || userInfo.user?.name || userId
|
|
28
|
+
} catch {
|
|
29
|
+
return userId
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildSlackThreadTitle(channelName: string, starterText: string, fallbackTs: string): string {
|
|
34
|
+
const snippet = starterText.replace(/\s+/g, ' ').trim().slice(0, 56)
|
|
35
|
+
if (snippet) return `${channelName} · ${snippet}`
|
|
36
|
+
return `${channelName} thread ${fallbackTs}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function hydrateSlackThreadContext(params: {
|
|
40
|
+
client: any
|
|
41
|
+
inbound: InboundMessage
|
|
42
|
+
currentTs?: string
|
|
43
|
+
botUserId?: string
|
|
44
|
+
}): Promise<void> {
|
|
45
|
+
const threadTs = params.inbound.threadId
|
|
46
|
+
if (!threadTs) return
|
|
47
|
+
try {
|
|
48
|
+
const result = await params.client.conversations.replies({
|
|
49
|
+
channel: params.inbound.channelId,
|
|
50
|
+
ts: threadTs,
|
|
51
|
+
limit: 12,
|
|
52
|
+
inclusive: true,
|
|
53
|
+
})
|
|
54
|
+
const messages = Array.isArray((result as any)?.messages) ? (result as any).messages as any[] : []
|
|
55
|
+
if (!messages.length) return
|
|
56
|
+
|
|
57
|
+
const userIds = [...new Set(messages.map((message) => typeof message?.user === 'string' ? message.user : '').filter(Boolean))]
|
|
58
|
+
const nameMap = new Map<string, string>()
|
|
59
|
+
await Promise.all(userIds.map(async (userId) => {
|
|
60
|
+
const name = await resolveSlackUserDisplayName(params.client, userId)
|
|
61
|
+
if (name) nameMap.set(userId, name)
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
const starter = messages[0]
|
|
65
|
+
const starterText = typeof starter?.text === 'string' ? starter.text.trim() : ''
|
|
66
|
+
const starterSenderName = nameMap.get(starter?.user)
|
|
67
|
+
|| starter?.username
|
|
68
|
+
|| starter?.user
|
|
69
|
+
|| (starter?.bot_id ? 'Slack Bot' : '')
|
|
70
|
+
const currentTsValue = parseSlackTimestamp(params.currentTs)
|
|
71
|
+
const history: InboundThreadHistoryEntry[] = messages
|
|
72
|
+
.filter((message) => {
|
|
73
|
+
const tsValue = parseSlackTimestamp(message?.ts)
|
|
74
|
+
if (!tsValue) return false
|
|
75
|
+
if (String(message?.ts) === String(threadTs)) return false
|
|
76
|
+
if (currentTsValue && tsValue >= currentTsValue) return false
|
|
77
|
+
return true
|
|
78
|
+
})
|
|
79
|
+
.slice(-6)
|
|
80
|
+
.map((message) => ({
|
|
81
|
+
role: (message?.bot_id || (params.botUserId && message?.user === params.botUserId) ? 'assistant' : 'user') as 'assistant' | 'user',
|
|
82
|
+
senderName: nameMap.get(message?.user) || message?.username || message?.user || (message?.bot_id ? 'Slack Bot' : 'Unknown'),
|
|
83
|
+
text: typeof message?.text === 'string' ? message.text : '',
|
|
84
|
+
messageId: typeof message?.ts === 'string' ? message.ts : undefined,
|
|
85
|
+
}))
|
|
86
|
+
.filter((entry) => entry.text.trim().length > 0)
|
|
87
|
+
|
|
88
|
+
params.inbound.threadParentChannelId = params.inbound.channelId
|
|
89
|
+
params.inbound.threadParentChannelName = params.inbound.channelName || params.inbound.channelId
|
|
90
|
+
params.inbound.threadStarterText = starterText || undefined
|
|
91
|
+
params.inbound.threadStarterSenderName = starterSenderName || undefined
|
|
92
|
+
params.inbound.threadTitle = buildSlackThreadTitle(
|
|
93
|
+
params.inbound.channelName || params.inbound.channelId,
|
|
94
|
+
starterText,
|
|
95
|
+
threadTs,
|
|
96
|
+
)
|
|
97
|
+
params.inbound.threadPersonaLabel = params.inbound.threadTitle
|
|
98
|
+
params.inbound.threadHistory = history.length ? history : undefined
|
|
99
|
+
} catch (err: unknown) {
|
|
100
|
+
console.warn(`[slack] Thread context bootstrap failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
8
103
|
|
|
9
104
|
const slack: PlatformConnector = {
|
|
10
105
|
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
@@ -136,24 +231,50 @@ const slack: PlatformConnector = {
|
|
|
136
231
|
senderId: msg.user,
|
|
137
232
|
senderName,
|
|
138
233
|
text: msg.text || (media.length > 0 ? '(media message)' : ''),
|
|
234
|
+
isGroup: !String(channelId).startsWith('D'),
|
|
235
|
+
messageId: msg.ts || undefined,
|
|
236
|
+
replyToMessageId: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
|
|
237
|
+
threadId: msg.thread_ts || undefined,
|
|
238
|
+
mentionsBot: !!(botUserId && typeof msg.text === 'string' && msg.text.includes(`<@${botUserId}>`)),
|
|
139
239
|
imageUrl: media.find((m) => m.type === 'image')?.url,
|
|
140
240
|
media,
|
|
141
241
|
}
|
|
242
|
+
await hydrateSlackThreadContext({ client, inbound, currentTs: msg.ts || undefined, botUserId })
|
|
142
243
|
|
|
143
244
|
try {
|
|
144
245
|
const response = await onMessage(inbound)
|
|
145
246
|
|
|
146
247
|
if (isNoMessage(response)) return
|
|
147
248
|
|
|
249
|
+
const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
|
|
250
|
+
const threadTs = replyOptions.threadId || replyOptions.replyToMessageId
|
|
251
|
+
let lastMessageId: string | undefined
|
|
252
|
+
|
|
148
253
|
// Slack has a 4000 char limit for messages
|
|
149
254
|
if (response.length <= 4000) {
|
|
150
|
-
await
|
|
255
|
+
const sent = await client.chat.postMessage({
|
|
256
|
+
channel: channelId,
|
|
257
|
+
text: response,
|
|
258
|
+
thread_ts: threadTs,
|
|
259
|
+
})
|
|
260
|
+
lastMessageId = sent.ts || undefined
|
|
151
261
|
} else {
|
|
152
262
|
const chunks = response.match(/[\s\S]{1,3990}/g) || [response]
|
|
153
263
|
for (const chunk of chunks) {
|
|
154
|
-
await
|
|
264
|
+
const sent = await client.chat.postMessage({
|
|
265
|
+
channel: channelId,
|
|
266
|
+
text: chunk,
|
|
267
|
+
thread_ts: threadTs,
|
|
268
|
+
})
|
|
269
|
+
lastMessageId = sent.ts || undefined
|
|
155
270
|
}
|
|
156
271
|
}
|
|
272
|
+
await recordConnectorOutboundDelivery({
|
|
273
|
+
connectorId: connector.id,
|
|
274
|
+
inbound,
|
|
275
|
+
messageId: lastMessageId,
|
|
276
|
+
state: 'sent',
|
|
277
|
+
})
|
|
157
278
|
} catch (err: any) {
|
|
158
279
|
console.error(`[slack] Error handling message:`, err.message)
|
|
159
280
|
try {
|
|
@@ -179,12 +300,36 @@ const slack: PlatformConnector = {
|
|
|
179
300
|
senderId: event.user || 'unknown',
|
|
180
301
|
senderName,
|
|
181
302
|
text: event.text.replace(/<@[^>]+>/g, '').trim(), // Strip @mentions
|
|
303
|
+
isGroup: !String(event.channel).startsWith('D'),
|
|
304
|
+
messageId: (event as any).ts || undefined,
|
|
305
|
+
replyToMessageId: (event as any).thread_ts && (event as any).thread_ts !== (event as any).ts
|
|
306
|
+
? (event as any).thread_ts
|
|
307
|
+
: undefined,
|
|
308
|
+
threadId: (event as any).thread_ts || undefined,
|
|
309
|
+
mentionsBot: true,
|
|
182
310
|
}
|
|
311
|
+
await hydrateSlackThreadContext({
|
|
312
|
+
client,
|
|
313
|
+
inbound,
|
|
314
|
+
currentTs: (event as any).ts || undefined,
|
|
315
|
+
botUserId,
|
|
316
|
+
})
|
|
183
317
|
|
|
184
318
|
try {
|
|
185
319
|
const response = await onMessage(inbound)
|
|
186
320
|
if (isNoMessage(response)) return
|
|
187
|
-
|
|
321
|
+
const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
|
|
322
|
+
const sent = await client.chat.postMessage({
|
|
323
|
+
channel: event.channel,
|
|
324
|
+
text: response,
|
|
325
|
+
thread_ts: replyOptions.threadId || replyOptions.replyToMessageId,
|
|
326
|
+
})
|
|
327
|
+
await recordConnectorOutboundDelivery({
|
|
328
|
+
connectorId: connector.id,
|
|
329
|
+
inbound,
|
|
330
|
+
messageId: sent.ts || undefined,
|
|
331
|
+
state: 'sent',
|
|
332
|
+
})
|
|
188
333
|
} catch (err: any) {
|
|
189
334
|
console.error(`[slack] Error handling mention:`, err.message)
|
|
190
335
|
}
|
|
@@ -202,6 +347,7 @@ const slack: PlatformConnector = {
|
|
|
202
347
|
},
|
|
203
348
|
async sendMessage(channelId, text, options) {
|
|
204
349
|
const webClient = app.client
|
|
350
|
+
const threadTs = options?.threadId?.trim() || options?.replyToMessageId?.trim() || undefined
|
|
205
351
|
|
|
206
352
|
// File upload (local path or URL)
|
|
207
353
|
const hasMedia = options?.mediaPath || options?.imageUrl || options?.fileUrl
|
|
@@ -219,18 +365,25 @@ const slack: PlatformConnector = {
|
|
|
219
365
|
}
|
|
220
366
|
|
|
221
367
|
if (fileContent) {
|
|
222
|
-
const
|
|
368
|
+
const uploadArgsBase = {
|
|
223
369
|
channel_id: channelId,
|
|
224
370
|
file: fileContent,
|
|
225
371
|
filename: fileName,
|
|
226
372
|
initial_comment: options?.caption || text || undefined,
|
|
227
|
-
}
|
|
373
|
+
}
|
|
374
|
+
const result = threadTs
|
|
375
|
+
? await webClient.filesUploadV2({
|
|
376
|
+
...uploadArgsBase,
|
|
377
|
+
thread_ts: threadTs,
|
|
378
|
+
})
|
|
379
|
+
: await webClient.filesUploadV2(uploadArgsBase)
|
|
228
380
|
return { messageId: (result as any)?.files?.[0]?.id }
|
|
229
381
|
} else if (fileUrl) {
|
|
230
382
|
// Send URL as message with unfurl
|
|
231
383
|
const msg = await webClient.chat.postMessage({
|
|
232
384
|
channel: channelId,
|
|
233
385
|
text: `${options?.caption || text || ''}\n${fileUrl}`.trim(),
|
|
386
|
+
thread_ts: threadTs,
|
|
234
387
|
unfurl_links: true,
|
|
235
388
|
unfurl_media: true,
|
|
236
389
|
})
|
|
@@ -241,17 +394,43 @@ const slack: PlatformConnector = {
|
|
|
241
394
|
// Text only
|
|
242
395
|
const payload = text || options?.caption || ''
|
|
243
396
|
if (payload.length <= 4000) {
|
|
244
|
-
const msg = await webClient.chat.postMessage({ channel: channelId, text: payload })
|
|
397
|
+
const msg = await webClient.chat.postMessage({ channel: channelId, text: payload, thread_ts: threadTs })
|
|
245
398
|
return { messageId: msg.ts || undefined }
|
|
246
399
|
}
|
|
247
400
|
const chunks = payload.match(/[\s\S]{1,3990}/g) || [payload]
|
|
248
401
|
let lastTs: string | undefined
|
|
249
402
|
for (const chunk of chunks) {
|
|
250
|
-
const msg = await webClient.chat.postMessage({ channel: channelId, text: chunk })
|
|
403
|
+
const msg = await webClient.chat.postMessage({ channel: channelId, text: chunk, thread_ts: threadTs })
|
|
251
404
|
lastTs = msg.ts || undefined
|
|
252
405
|
}
|
|
253
406
|
return { messageId: lastTs }
|
|
254
407
|
},
|
|
408
|
+
async sendReaction(channelId, messageId, emoji) {
|
|
409
|
+
await app.client.reactions.add({
|
|
410
|
+
channel: channelId,
|
|
411
|
+
timestamp: messageId,
|
|
412
|
+
name: normalizeSlackEmoji(emoji),
|
|
413
|
+
})
|
|
414
|
+
},
|
|
415
|
+
async editMessage(channelId, messageId, newText) {
|
|
416
|
+
await app.client.chat.update({
|
|
417
|
+
channel: channelId,
|
|
418
|
+
ts: messageId,
|
|
419
|
+
text: newText,
|
|
420
|
+
})
|
|
421
|
+
},
|
|
422
|
+
async deleteMessage(channelId, messageId) {
|
|
423
|
+
await app.client.chat.delete({
|
|
424
|
+
channel: channelId,
|
|
425
|
+
ts: messageId,
|
|
426
|
+
})
|
|
427
|
+
},
|
|
428
|
+
async pinMessage(channelId, messageId) {
|
|
429
|
+
await app.client.pins.add({
|
|
430
|
+
channel: channelId,
|
|
431
|
+
timestamp: messageId,
|
|
432
|
+
})
|
|
433
|
+
},
|
|
255
434
|
async stop() {
|
|
256
435
|
appStopped = true
|
|
257
436
|
await app.stop()
|
|
@@ -4,11 +4,12 @@ import path from 'path'
|
|
|
4
4
|
import type { Connector } from '@/types'
|
|
5
5
|
import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundMediaType } from './types'
|
|
6
6
|
import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime, isAudioMime } from './media'
|
|
7
|
-
import { isNoMessage } from './manager'
|
|
7
|
+
import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
|
|
8
8
|
|
|
9
9
|
const telegram: PlatformConnector = {
|
|
10
10
|
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
11
11
|
const bot = new Bot(botToken)
|
|
12
|
+
let botUsername = ''
|
|
12
13
|
|
|
13
14
|
// Optional: restrict to specific chat IDs
|
|
14
15
|
const allowedChats = connector.config.chatIds
|
|
@@ -140,6 +141,11 @@ const telegram: PlatformConnector = {
|
|
|
140
141
|
senderId: String(ctx.from.id),
|
|
141
142
|
senderName: ctx.from.first_name + (ctx.from.last_name ? ` ${ctx.from.last_name}` : ''),
|
|
142
143
|
text: text || (media.length > 0 ? '(media message)' : ''),
|
|
144
|
+
isGroup: ctx.chat.type !== 'private',
|
|
145
|
+
messageId: String(raw.message_id),
|
|
146
|
+
replyToMessageId: raw.reply_to_message?.message_id ? String(raw.reply_to_message.message_id) : undefined,
|
|
147
|
+
threadId: raw.message_thread_id ? String(raw.message_thread_id) : undefined,
|
|
148
|
+
mentionsBot: !!(botUsername && text && new RegExp(`@${botUsername}\\b`, 'i').test(String(text))),
|
|
143
149
|
imageUrl: media.find((m) => m.type === 'image')?.url,
|
|
144
150
|
media,
|
|
145
151
|
}
|
|
@@ -150,15 +156,34 @@ const telegram: PlatformConnector = {
|
|
|
150
156
|
|
|
151
157
|
if (isNoMessage(response)) return
|
|
152
158
|
|
|
159
|
+
const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
|
|
160
|
+
const baseOptions: Record<string, unknown> = {}
|
|
161
|
+
if (replyOptions.replyToMessageId) {
|
|
162
|
+
baseOptions.reply_parameters = { message_id: Number(replyOptions.replyToMessageId) }
|
|
163
|
+
}
|
|
164
|
+
if (replyOptions.threadId) {
|
|
165
|
+
baseOptions.message_thread_id = Number(replyOptions.threadId)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let lastMessageId: string | undefined
|
|
169
|
+
|
|
153
170
|
// Telegram has a 4096 char limit
|
|
154
171
|
if (response.length <= 4096) {
|
|
155
|
-
await ctx.
|
|
172
|
+
const sent = await ctx.api.sendMessage(ctx.chat.id, response, baseOptions as any)
|
|
173
|
+
lastMessageId = String(sent.message_id)
|
|
156
174
|
} else {
|
|
157
175
|
const chunks = response.match(/[\s\S]{1,4090}/g) || [response]
|
|
158
|
-
for (
|
|
159
|
-
await ctx.api.sendMessage(ctx.chat.id,
|
|
176
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
177
|
+
const sent = await ctx.api.sendMessage(ctx.chat.id, chunks[i], (i === 0 ? baseOptions : {}) as any)
|
|
178
|
+
lastMessageId = String(sent.message_id)
|
|
160
179
|
}
|
|
161
180
|
}
|
|
181
|
+
await recordConnectorOutboundDelivery({
|
|
182
|
+
connectorId: connector.id,
|
|
183
|
+
inbound,
|
|
184
|
+
messageId: lastMessageId,
|
|
185
|
+
state: 'sent',
|
|
186
|
+
})
|
|
162
187
|
} catch (err: any) {
|
|
163
188
|
console.error(`[telegram] Error handling message:`, err.message)
|
|
164
189
|
try {
|
|
@@ -174,6 +199,7 @@ const telegram: PlatformConnector = {
|
|
|
174
199
|
bot.start({
|
|
175
200
|
allowed_updates: ['message', 'edited_message'],
|
|
176
201
|
onStart: (botInfo) => {
|
|
202
|
+
botUsername = botInfo.username || ''
|
|
177
203
|
console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
|
|
178
204
|
},
|
|
179
205
|
}).catch((err) => {
|
|
@@ -189,6 +215,13 @@ const telegram: PlatformConnector = {
|
|
|
189
215
|
async sendMessage(channelId, text, options) {
|
|
190
216
|
const chatId = channelId
|
|
191
217
|
const caption = options?.caption || text || undefined
|
|
218
|
+
const extra: Record<string, unknown> = {}
|
|
219
|
+
if (options?.replyToMessageId) {
|
|
220
|
+
extra.reply_parameters = { message_id: Number(options.replyToMessageId) }
|
|
221
|
+
}
|
|
222
|
+
if (options?.threadId) {
|
|
223
|
+
extra.message_thread_id = Number(options.threadId)
|
|
224
|
+
}
|
|
192
225
|
|
|
193
226
|
// Local file
|
|
194
227
|
if (options?.mediaPath) {
|
|
@@ -196,21 +229,21 @@ const telegram: PlatformConnector = {
|
|
|
196
229
|
const mime = options.mimeType || mimeFromPath(options.mediaPath)
|
|
197
230
|
const inputFile = new InputFile(options.mediaPath, options.fileName || path.basename(options.mediaPath))
|
|
198
231
|
if (isImageMime(mime)) {
|
|
199
|
-
const msg = await bot.api.sendPhoto(chatId, inputFile, { caption })
|
|
232
|
+
const msg = await bot.api.sendPhoto(chatId, inputFile, { caption, ...(extra as any) })
|
|
200
233
|
return { messageId: String(msg.message_id) }
|
|
201
234
|
} else if (isAudioMime(mime)) {
|
|
202
235
|
const msg = options?.ptt
|
|
203
|
-
? await bot.api.sendVoice(chatId, inputFile, { caption })
|
|
204
|
-
: await bot.api.sendAudio(chatId, inputFile, { caption })
|
|
236
|
+
? await bot.api.sendVoice(chatId, inputFile, { caption, ...(extra as any) })
|
|
237
|
+
: await bot.api.sendAudio(chatId, inputFile, { caption, ...(extra as any) })
|
|
205
238
|
return { messageId: String(msg.message_id) }
|
|
206
239
|
} else {
|
|
207
|
-
const msg = await bot.api.sendDocument(chatId, inputFile, { caption })
|
|
240
|
+
const msg = await bot.api.sendDocument(chatId, inputFile, { caption, ...(extra as any) })
|
|
208
241
|
return { messageId: String(msg.message_id) }
|
|
209
242
|
}
|
|
210
243
|
}
|
|
211
244
|
// URL-based image
|
|
212
245
|
if (options?.imageUrl) {
|
|
213
|
-
const msg = await bot.api.sendPhoto(chatId, options.imageUrl, { caption })
|
|
246
|
+
const msg = await bot.api.sendPhoto(chatId, options.imageUrl, { caption, ...(extra as any) })
|
|
214
247
|
return { messageId: String(msg.message_id) }
|
|
215
248
|
}
|
|
216
249
|
// URL-based file
|
|
@@ -218,25 +251,42 @@ const telegram: PlatformConnector = {
|
|
|
218
251
|
const mime = options.mimeType || ''
|
|
219
252
|
const msg = isAudioMime(mime)
|
|
220
253
|
? options?.ptt
|
|
221
|
-
? await bot.api.sendVoice(chatId, options.fileUrl, { caption })
|
|
222
|
-
: await bot.api.sendAudio(chatId, options.fileUrl, { caption })
|
|
223
|
-
: await bot.api.sendDocument(chatId, options.fileUrl, { caption })
|
|
254
|
+
? await bot.api.sendVoice(chatId, options.fileUrl, { caption, ...(extra as any) })
|
|
255
|
+
: await bot.api.sendAudio(chatId, options.fileUrl, { caption, ...(extra as any) })
|
|
256
|
+
: await bot.api.sendDocument(chatId, options.fileUrl, { caption, ...(extra as any) })
|
|
224
257
|
return { messageId: String(msg.message_id) }
|
|
225
258
|
}
|
|
226
259
|
// Text only
|
|
227
260
|
const payload = text || caption || ''
|
|
228
261
|
if (payload.length <= 4096) {
|
|
229
|
-
const msg = await bot.api.sendMessage(chatId, payload)
|
|
262
|
+
const msg = await bot.api.sendMessage(chatId, payload, extra as any)
|
|
230
263
|
return { messageId: String(msg.message_id) }
|
|
231
264
|
}
|
|
232
265
|
const chunks = payload.match(/[\s\S]{1,4090}/g) || [payload]
|
|
233
266
|
let lastId: string | undefined
|
|
234
|
-
for (
|
|
235
|
-
const msg = await bot.api.sendMessage(chatId,
|
|
267
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
268
|
+
const msg = await bot.api.sendMessage(chatId, chunks[i], (i === 0 ? extra : {}) as any)
|
|
236
269
|
lastId = String(msg.message_id)
|
|
237
270
|
}
|
|
238
271
|
return { messageId: lastId }
|
|
239
272
|
},
|
|
273
|
+
async sendReaction(channelId, messageId, emoji) {
|
|
274
|
+
const fn = (bot.api as any).setMessageReaction
|
|
275
|
+
if (typeof fn !== 'function') return
|
|
276
|
+
await fn.call(bot.api, channelId, Number(messageId), [{ type: 'emoji', emoji }])
|
|
277
|
+
},
|
|
278
|
+
async editMessage(channelId, messageId, newText) {
|
|
279
|
+
await bot.api.editMessageText(channelId, Number(messageId), newText)
|
|
280
|
+
},
|
|
281
|
+
async deleteMessage(channelId, messageId) {
|
|
282
|
+
await bot.api.deleteMessage(channelId, Number(messageId))
|
|
283
|
+
},
|
|
284
|
+
async pinMessage(channelId, messageId) {
|
|
285
|
+
await bot.api.pinChatMessage(channelId, Number(messageId))
|
|
286
|
+
},
|
|
287
|
+
async sendTyping(channelId) {
|
|
288
|
+
await bot.api.sendChatAction(channelId, 'typing')
|
|
289
|
+
},
|
|
240
290
|
async stop() {
|
|
241
291
|
botRunning = false
|
|
242
292
|
await bot.stop()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { buildConnectorThreadContextBlock, resolveThreadPersonaLabel } from './thread-context'
|
|
4
|
+
|
|
5
|
+
test('resolveThreadPersonaLabel prefers explicit and title-based labels', () => {
|
|
6
|
+
assert.equal(resolveThreadPersonaLabel({
|
|
7
|
+
platform: 'slack',
|
|
8
|
+
threadPersonaLabel: 'Incident Bridge',
|
|
9
|
+
threadTitle: 'ignored',
|
|
10
|
+
threadStarterText: 'ignored',
|
|
11
|
+
threadId: 't1',
|
|
12
|
+
channelName: 'ops',
|
|
13
|
+
}), 'Incident Bridge')
|
|
14
|
+
|
|
15
|
+
assert.equal(resolveThreadPersonaLabel({
|
|
16
|
+
platform: 'discord',
|
|
17
|
+
threadPersonaLabel: undefined,
|
|
18
|
+
threadTitle: 'Release Coordination',
|
|
19
|
+
threadStarterText: 'root message',
|
|
20
|
+
threadId: 't1',
|
|
21
|
+
channelName: 'deploys',
|
|
22
|
+
}), 'Release Coordination')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('buildConnectorThreadContextBlock includes starter, history, and first-turn note', () => {
|
|
26
|
+
const block = buildConnectorThreadContextBlock({
|
|
27
|
+
platform: 'slack',
|
|
28
|
+
threadId: 'thread-1',
|
|
29
|
+
threadTitle: 'Checkout Incident',
|
|
30
|
+
threadStarterText: 'Prod checkout is returning 500s.',
|
|
31
|
+
threadStarterSenderName: 'Alice',
|
|
32
|
+
threadParentChannelName: 'incidents',
|
|
33
|
+
threadHistory: [
|
|
34
|
+
{ role: 'assistant', senderName: 'Swarmy', text: 'I am tracing the failing service now.' },
|
|
35
|
+
{ role: 'user', senderName: 'Bob', text: 'Looks isolated to EU traffic.' },
|
|
36
|
+
],
|
|
37
|
+
}, { isFirstThreadTurn: true })
|
|
38
|
+
|
|
39
|
+
assert.match(block, /Native Thread Context/)
|
|
40
|
+
assert.match(block, /Thread persona: Checkout Incident/)
|
|
41
|
+
assert.match(block, /Thread starter: Alice: Prod checkout is returning 500s\./)
|
|
42
|
+
assert.match(block, /first turn in a thread-bound session/i)
|
|
43
|
+
assert.match(block, /\[assistant\] Swarmy: I am tracing the failing service now\./)
|
|
44
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { InboundMessage, InboundThreadHistoryEntry } from './types'
|
|
2
|
+
|
|
3
|
+
function normalizeText(value: unknown, maxChars: number): string {
|
|
4
|
+
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxChars)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveThreadPersonaLabel(msg: Pick<InboundMessage, 'threadPersonaLabel' | 'threadTitle' | 'threadStarterText' | 'threadId' | 'channelName' | 'platform'>): string | null {
|
|
8
|
+
const explicit = normalizeText(msg.threadPersonaLabel, 120)
|
|
9
|
+
if (explicit) return explicit
|
|
10
|
+
const title = normalizeText(msg.threadTitle, 120)
|
|
11
|
+
if (title) return title
|
|
12
|
+
const starter = normalizeText(msg.threadStarterText, 72)
|
|
13
|
+
if (starter) {
|
|
14
|
+
return `${msg.platform} thread: ${starter}`.slice(0, 120)
|
|
15
|
+
}
|
|
16
|
+
const channel = normalizeText(msg.channelName, 64)
|
|
17
|
+
if (msg.threadId && channel) return `${channel} thread`
|
|
18
|
+
if (msg.threadId) return `${msg.platform} thread`
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatHistoryEntry(entry: InboundThreadHistoryEntry): string {
|
|
23
|
+
const speaker = normalizeText(entry.senderName, 60) || (entry.role === 'assistant' ? 'assistant' : 'user')
|
|
24
|
+
const text = normalizeText(entry.text, 220)
|
|
25
|
+
return `- [${entry.role}] ${speaker}: ${text}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildConnectorThreadContextBlock(
|
|
29
|
+
msg: Pick<
|
|
30
|
+
InboundMessage,
|
|
31
|
+
'platform'
|
|
32
|
+
| 'threadId'
|
|
33
|
+
| 'replyToMessageId'
|
|
34
|
+
| 'threadTitle'
|
|
35
|
+
| 'threadStarterText'
|
|
36
|
+
| 'threadStarterSenderName'
|
|
37
|
+
| 'threadParentChannelName'
|
|
38
|
+
| 'threadHistory'
|
|
39
|
+
| 'threadPersonaLabel'
|
|
40
|
+
>,
|
|
41
|
+
opts?: { isFirstThreadTurn?: boolean },
|
|
42
|
+
): string {
|
|
43
|
+
const hasThreadContext = !!(
|
|
44
|
+
msg.threadId
|
|
45
|
+
|| msg.replyToMessageId
|
|
46
|
+
|| msg.threadTitle
|
|
47
|
+
|| msg.threadStarterText
|
|
48
|
+
|| (Array.isArray(msg.threadHistory) && msg.threadHistory.length > 0)
|
|
49
|
+
)
|
|
50
|
+
if (!hasThreadContext) return ''
|
|
51
|
+
|
|
52
|
+
const persona = resolveThreadPersonaLabel(msg)
|
|
53
|
+
const lines = ['## Native Thread Context']
|
|
54
|
+
if (persona) lines.push(`Thread persona: ${persona}`)
|
|
55
|
+
if (msg.threadTitle) lines.push(`Thread title: ${normalizeText(msg.threadTitle, 140)}`)
|
|
56
|
+
if (msg.threadParentChannelName) lines.push(`Parent channel: ${normalizeText(msg.threadParentChannelName, 100)}`)
|
|
57
|
+
if (opts?.isFirstThreadTurn) {
|
|
58
|
+
lines.push('This is the first turn in a thread-bound session. Treat the starter and history below as earlier context from the same conversation.')
|
|
59
|
+
}
|
|
60
|
+
if (msg.threadStarterText) {
|
|
61
|
+
const speaker = normalizeText(msg.threadStarterSenderName, 60) || 'unknown'
|
|
62
|
+
lines.push(`Thread starter: ${speaker}: ${normalizeText(msg.threadStarterText, 260)}`)
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(msg.threadHistory) && msg.threadHistory.length > 0) {
|
|
65
|
+
lines.push('Recent thread history before this turn:')
|
|
66
|
+
for (const entry of msg.threadHistory.slice(-6)) {
|
|
67
|
+
lines.push(formatHistoryEntry(entry))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
lines.push('Respond as part of this ongoing thread, not as if the message started a brand new conversation.')
|
|
71
|
+
return lines.join('\n')
|
|
72
|
+
}
|
|
@@ -2,6 +2,13 @@ import type { Connector } from '@/types'
|
|
|
2
2
|
|
|
3
3
|
export type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'file'
|
|
4
4
|
|
|
5
|
+
export interface InboundThreadHistoryEntry {
|
|
6
|
+
role: 'user' | 'assistant'
|
|
7
|
+
senderName: string
|
|
8
|
+
text: string
|
|
9
|
+
messageId?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export interface InboundMedia {
|
|
6
13
|
type: InboundMediaType
|
|
7
14
|
fileName?: string
|
|
@@ -22,12 +29,43 @@ export interface InboundMessage {
|
|
|
22
29
|
senderName: string // display name
|
|
23
30
|
text: string
|
|
24
31
|
isGroup?: boolean
|
|
32
|
+
messageId?: string
|
|
25
33
|
imageUrl?: string
|
|
26
34
|
media?: InboundMedia[]
|
|
27
35
|
replyToMessageId?: string
|
|
36
|
+
threadId?: string
|
|
37
|
+
threadTitle?: string
|
|
38
|
+
threadStarterText?: string
|
|
39
|
+
threadStarterSenderName?: string
|
|
40
|
+
threadPersonaLabel?: string
|
|
41
|
+
threadParentChannelId?: string
|
|
42
|
+
threadParentChannelName?: string
|
|
43
|
+
threadHistory?: InboundThreadHistoryEntry[]
|
|
44
|
+
mentionsBot?: boolean
|
|
28
45
|
agentIdOverride?: string
|
|
29
46
|
}
|
|
30
47
|
|
|
48
|
+
export interface OutboundSendOptions {
|
|
49
|
+
imageUrl?: string
|
|
50
|
+
fileUrl?: string
|
|
51
|
+
/** Absolute local file path (e.g. screenshot saved to disk) */
|
|
52
|
+
mediaPath?: string
|
|
53
|
+
mimeType?: string
|
|
54
|
+
fileName?: string
|
|
55
|
+
caption?: string
|
|
56
|
+
/** Send audio as a WhatsApp voice note (push-to-talk) */
|
|
57
|
+
ptt?: boolean
|
|
58
|
+
/** Platform-native reply target when supported */
|
|
59
|
+
replyToMessageId?: string
|
|
60
|
+
/** Platform-native thread or topic identifier when supported */
|
|
61
|
+
threadId?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface OutboundTypingOptions {
|
|
65
|
+
/** Platform-native thread or topic identifier when supported */
|
|
66
|
+
threadId?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
/** A running connector instance */
|
|
32
70
|
export interface ConnectorInstance {
|
|
33
71
|
connector: Connector
|
|
@@ -36,17 +74,7 @@ export interface ConnectorInstance {
|
|
|
36
74
|
sendMessage?: (
|
|
37
75
|
channelId: string,
|
|
38
76
|
text: string,
|
|
39
|
-
options?:
|
|
40
|
-
imageUrl?: string
|
|
41
|
-
fileUrl?: string
|
|
42
|
-
/** Absolute local file path (e.g. screenshot saved to disk) */
|
|
43
|
-
mediaPath?: string
|
|
44
|
-
mimeType?: string
|
|
45
|
-
fileName?: string
|
|
46
|
-
caption?: string
|
|
47
|
-
/** Send audio as a WhatsApp voice note (push-to-talk) */
|
|
48
|
-
ptt?: boolean
|
|
49
|
-
},
|
|
77
|
+
options?: OutboundSendOptions,
|
|
50
78
|
) => Promise<{ messageId?: string } | void>
|
|
51
79
|
/** Current QR code data URL (WhatsApp only, null when paired) */
|
|
52
80
|
qrDataUrl?: string | null
|
|
@@ -62,6 +90,8 @@ export interface ConnectorInstance {
|
|
|
62
90
|
deleteMessage?: (channelId: string, messageId: string) => Promise<void>
|
|
63
91
|
/** Rich messaging: pin a message */
|
|
64
92
|
pinMessage?: (channelId: string, messageId: string) => Promise<void>
|
|
93
|
+
/** Best-effort typing or "working" indicator for the target conversation */
|
|
94
|
+
sendTyping?: (channelId: string, options?: OutboundTypingOptions) => Promise<void>
|
|
65
95
|
/** Health check: returns true if the underlying connection is alive */
|
|
66
96
|
isAlive?: () => boolean
|
|
67
97
|
}
|