@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- 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/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- 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/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- 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 +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- 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 +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -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 +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- 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 +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- 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 +994 -130
- 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 +189 -10
- 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/daemon-state.ts +62 -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/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- 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 +31 -964
- 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 +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- 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 +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- 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 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- 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/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { Agent } from '@/types'
|
|
4
|
+
import { filterHealthyChatroomAgents } from './chatroom-health'
|
|
5
|
+
|
|
6
|
+
describe('filterHealthyChatroomAgents', () => {
|
|
7
|
+
it('treats providers with default endpoints as healthy without explicit agent endpoints', () => {
|
|
8
|
+
const now = Date.now()
|
|
9
|
+
const agents: Record<string, Agent> = {
|
|
10
|
+
agent_writer: {
|
|
11
|
+
id: 'agent_writer',
|
|
12
|
+
name: 'Writer',
|
|
13
|
+
description: '',
|
|
14
|
+
systemPrompt: '',
|
|
15
|
+
provider: 'ollama',
|
|
16
|
+
model: 'glm-5:cloud',
|
|
17
|
+
createdAt: now,
|
|
18
|
+
updatedAt: now,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = filterHealthyChatroomAgents(['agent_writer'], agents)
|
|
23
|
+
assert.deepEqual(result.healthyAgentIds, ['agent_writer'])
|
|
24
|
+
assert.deepEqual(result.skipped, [])
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProvider } from '@/lib/providers'
|
|
2
2
|
import type { Agent } from '@/types'
|
|
3
|
-
import { resolveApiKey } from './chatroom-helpers'
|
|
3
|
+
import { resolveAgentApiEndpoint, resolveApiKey } from './chatroom-helpers'
|
|
4
4
|
import { isProviderCoolingDown } from './provider-health'
|
|
5
5
|
|
|
6
6
|
export interface ChatroomAgentHealthSkip {
|
|
@@ -47,7 +47,7 @@ export function filterHealthyChatroomAgents(
|
|
|
47
47
|
skipped.push({ agentId, reason: 'missing_api_credentials' })
|
|
48
48
|
continue
|
|
49
49
|
}
|
|
50
|
-
if (providerInfo.requiresEndpoint && !agent
|
|
50
|
+
if (providerInfo.requiresEndpoint && !resolveAgentApiEndpoint(agent)) {
|
|
51
51
|
skipped.push({ agentId, reason: 'missing_api_endpoint' })
|
|
52
52
|
continue
|
|
53
53
|
}
|
|
@@ -57,4 +57,3 @@ export function filterHealthyChatroomAgents(
|
|
|
57
57
|
|
|
58
58
|
return { healthyAgentIds, skipped }
|
|
59
59
|
}
|
|
60
|
-
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { describe, it } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import type { Agent, Chatroom } from '@/types'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
parseMentions,
|
|
6
|
+
compactChatroomMessages,
|
|
7
|
+
buildHistoryForAgent,
|
|
8
|
+
buildSyntheticSession,
|
|
9
|
+
resolveChatroomWorkspaceDir,
|
|
10
|
+
resolveAgentApiEndpoint,
|
|
11
|
+
resolveReplyTargetAgentId,
|
|
12
|
+
} from './chatroom-helpers'
|
|
5
13
|
|
|
6
14
|
function makeAgents(): Record<string, Agent> {
|
|
7
15
|
const now = Date.now()
|
|
@@ -37,6 +45,48 @@ describe('chatroom-helpers', () => {
|
|
|
37
45
|
assert.deepEqual(mentions, ['default', 'agent_analyst'])
|
|
38
46
|
})
|
|
39
47
|
|
|
48
|
+
it('routes reply-only messages back to the replied-to agent', () => {
|
|
49
|
+
const agents = makeAgents()
|
|
50
|
+
const memberIds = ['default', 'agent_analyst']
|
|
51
|
+
const replyTargetAgentId = resolveReplyTargetAgentId('agent-msg', [
|
|
52
|
+
{
|
|
53
|
+
id: 'agent-msg',
|
|
54
|
+
senderId: 'default',
|
|
55
|
+
senderName: 'Assistant',
|
|
56
|
+
role: 'assistant',
|
|
57
|
+
text: 'Here is the previous answer.',
|
|
58
|
+
mentions: [],
|
|
59
|
+
reactions: [],
|
|
60
|
+
time: Date.now(),
|
|
61
|
+
},
|
|
62
|
+
], memberIds)
|
|
63
|
+
const mentions = parseMentions('Can you expand on that?', agents, memberIds, { replyTargetAgentId })
|
|
64
|
+
assert.deepEqual(mentions, ['default'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('keeps explicit mentions ahead of reply-based implicit targeting', () => {
|
|
68
|
+
const agents = makeAgents()
|
|
69
|
+
const memberIds = ['default', 'agent_analyst']
|
|
70
|
+
const mentions = parseMentions('Actually @Analyst should take this one.', agents, memberIds, { replyTargetAgentId: 'default' })
|
|
71
|
+
assert.deepEqual(mentions, ['agent_analyst'])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('ignores replies to non-agent messages', () => {
|
|
75
|
+
const replyTargetAgentId = resolveReplyTargetAgentId('user-msg', [
|
|
76
|
+
{
|
|
77
|
+
id: 'user-msg',
|
|
78
|
+
senderId: 'user',
|
|
79
|
+
senderName: 'You',
|
|
80
|
+
role: 'user',
|
|
81
|
+
text: 'Question',
|
|
82
|
+
mentions: [],
|
|
83
|
+
reactions: [],
|
|
84
|
+
time: Date.now(),
|
|
85
|
+
},
|
|
86
|
+
], ['default', 'agent_analyst'])
|
|
87
|
+
assert.equal(replyTargetAgentId, null)
|
|
88
|
+
})
|
|
89
|
+
|
|
40
90
|
it('compacts long chatrooms with a persisted summary message', () => {
|
|
41
91
|
const now = Date.now()
|
|
42
92
|
const chatroom: Chatroom = {
|
|
@@ -90,5 +140,27 @@ describe('chatroom-helpers', () => {
|
|
|
90
140
|
const attachmentMarkers = history.filter((msg) => msg.text.includes('[Attached:')).length
|
|
91
141
|
assert.ok(attachmentMarkers <= 6)
|
|
92
142
|
})
|
|
93
|
-
})
|
|
94
143
|
|
|
144
|
+
it('resolves default provider endpoints for chatroom sessions', () => {
|
|
145
|
+
const now = Date.now()
|
|
146
|
+
const agent: Agent = {
|
|
147
|
+
id: 'agent_writer',
|
|
148
|
+
name: 'Writer',
|
|
149
|
+
description: '',
|
|
150
|
+
systemPrompt: '',
|
|
151
|
+
provider: 'ollama',
|
|
152
|
+
model: 'glm-5:cloud',
|
|
153
|
+
createdAt: now,
|
|
154
|
+
updatedAt: now,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
assert.equal(resolveAgentApiEndpoint(agent), 'http://localhost:11434')
|
|
158
|
+
assert.equal(buildSyntheticSession(agent, 'room-1').apiEndpoint, 'http://localhost:11434')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('keeps chatroom execution inside the workspace instead of the repo root', () => {
|
|
162
|
+
const cwd = buildSyntheticSession(makeAgents().default, 'room-safe').cwd
|
|
163
|
+
assert.equal(cwd, resolveChatroomWorkspaceDir('room-safe'))
|
|
164
|
+
assert.match(cwd, /chatrooms[\/\\]room-safe$/)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
1
2
|
import os from 'os'
|
|
2
|
-
import
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { loadSettings, loadSkills, loadCredentials, decryptKey, loadSessions, saveSessions } from './storage'
|
|
3
5
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
6
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
4
7
|
import { genId } from '@/lib/id'
|
|
5
|
-
import
|
|
8
|
+
import { getProvider } from '@/lib/providers'
|
|
9
|
+
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
10
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
11
|
+
import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
|
|
12
|
+
import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
|
|
6
13
|
|
|
7
14
|
/** Resolve API key from an agent's credentialId */
|
|
8
15
|
export function resolveApiKey(credentialId: string | null | undefined): string | null {
|
|
@@ -34,6 +41,14 @@ export function isMuted(chatroom: Chatroom, agentId: string): boolean {
|
|
|
34
41
|
return new Date(member.mutedUntil).getTime() > Date.now()
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
export function resolveAgentApiEndpoint(agent: Agent): string | null {
|
|
45
|
+
const explicit = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
46
|
+
if (explicit) return explicit
|
|
47
|
+
const provider = getProvider(agent.provider)
|
|
48
|
+
if (!provider?.defaultEndpoint) return null
|
|
49
|
+
return normalizeProviderEndpoint(agent.provider, provider.defaultEndpoint) || provider.defaultEndpoint.replace(/\/+$/, '')
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
const COMPACTION_PREFIX = '[Conversation summary]'
|
|
38
53
|
|
|
39
54
|
function normalizeMentionToken(raw: string): string {
|
|
@@ -53,7 +68,12 @@ function truncateText(text: string, max: number): string {
|
|
|
53
68
|
import { isImplicitlyMentioned } from './chatroom-orchestration'
|
|
54
69
|
|
|
55
70
|
/** Parse @mentions from message text, returns matching agentIds */
|
|
56
|
-
export function parseMentions(
|
|
71
|
+
export function parseMentions(
|
|
72
|
+
text: string,
|
|
73
|
+
agents: Record<string, Agent>,
|
|
74
|
+
memberIds: string[],
|
|
75
|
+
opts?: { replyTargetAgentId?: string | null },
|
|
76
|
+
): string[] {
|
|
57
77
|
if (/@all\b/i.test(text)) return [...memberIds]
|
|
58
78
|
const mentionPattern = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
|
|
59
79
|
const mentioned: string[] = []
|
|
@@ -73,8 +93,17 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
|
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
// 2.
|
|
77
|
-
// Only if no explicit mentions found
|
|
96
|
+
// 2. Reply-based implicit mention
|
|
97
|
+
// Only if no explicit mentions were found.
|
|
98
|
+
if (mentioned.length === 0) {
|
|
99
|
+
const replyTargetAgentId = opts?.replyTargetAgentId
|
|
100
|
+
if (replyTargetAgentId && memberIds.includes(replyTargetAgentId)) {
|
|
101
|
+
mentioned.push(replyTargetAgentId)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. Implicit mentions (OpenClaw Style - Reading the room)
|
|
106
|
+
// Only if no explicit mentions were found.
|
|
78
107
|
if (mentioned.length === 0) {
|
|
79
108
|
for (const id of memberIds) {
|
|
80
109
|
const agent = agents[id]
|
|
@@ -87,6 +116,19 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
|
|
|
87
116
|
return mentioned
|
|
88
117
|
}
|
|
89
118
|
|
|
119
|
+
export function resolveReplyTargetAgentId(
|
|
120
|
+
replyToId: string | undefined,
|
|
121
|
+
messages: ChatroomMessage[],
|
|
122
|
+
memberIds: string[],
|
|
123
|
+
): string | null {
|
|
124
|
+
if (!replyToId) return null
|
|
125
|
+
const replyMsg = messages.find((m) => m.id === replyToId)
|
|
126
|
+
if (!replyMsg) return null
|
|
127
|
+
if (replyMsg.role !== 'assistant') return null
|
|
128
|
+
if (!memberIds.includes(replyMsg.senderId)) return null
|
|
129
|
+
return replyMsg.senderId
|
|
130
|
+
}
|
|
131
|
+
|
|
90
132
|
/**
|
|
91
133
|
* Persisted chatroom compaction so long-lived rooms stay inside context budgets.
|
|
92
134
|
* Returns true when the message list was compacted.
|
|
@@ -172,24 +214,113 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
|
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
/** Build a synthetic session object for an agent in a chatroom */
|
|
175
|
-
export function
|
|
217
|
+
export function resolveChatroomWorkspaceDir(chatroomId: string): string {
|
|
218
|
+
return path.join(WORKSPACE_DIR, 'chatrooms', chatroomId)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveSyntheticSessionId(chatroomId: string, agentId: string): string {
|
|
222
|
+
return `chatroom-${chatroomId}-${agentId}`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
|
|
176
226
|
return {
|
|
177
|
-
|
|
227
|
+
claudeCode: null,
|
|
228
|
+
codex: null,
|
|
229
|
+
opencode: null,
|
|
230
|
+
gemini: null,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
235
|
+
const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
|
|
236
|
+
fs.mkdirSync(roomWorkspace, { recursive: true })
|
|
237
|
+
const now = Date.now()
|
|
238
|
+
return applyResolvedRoute({
|
|
239
|
+
id: resolveSyntheticSessionId(chatroomId, agent.id),
|
|
178
240
|
name: `Chatroom session for ${agent.name}`,
|
|
179
|
-
cwd:
|
|
241
|
+
cwd: roomWorkspace,
|
|
180
242
|
user: 'chatroom',
|
|
181
243
|
provider: agent.provider,
|
|
182
244
|
model: agent.model,
|
|
183
245
|
credentialId: agent.credentialId ?? null,
|
|
184
246
|
fallbackCredentialIds: agent.fallbackCredentialIds,
|
|
185
|
-
apiEndpoint: agent
|
|
247
|
+
apiEndpoint: resolveAgentApiEndpoint(agent),
|
|
186
248
|
claudeSessionId: null,
|
|
249
|
+
codexThreadId: null,
|
|
250
|
+
opencodeSessionId: null,
|
|
251
|
+
delegateResumeIds: buildEmptyDelegateResumeIds(),
|
|
187
252
|
messages: [],
|
|
188
|
-
createdAt:
|
|
189
|
-
lastActiveAt:
|
|
253
|
+
createdAt: now,
|
|
254
|
+
lastActiveAt: now,
|
|
255
|
+
sessionType: 'human',
|
|
190
256
|
plugins: agent.plugins || agent.tools || [],
|
|
191
257
|
agentId: agent.id,
|
|
258
|
+
}, resolvePrimaryAgentRoute(agent))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function ensureSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
262
|
+
const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
|
|
263
|
+
fs.mkdirSync(roomWorkspace, { recursive: true })
|
|
264
|
+
const sessionId = resolveSyntheticSessionId(chatroomId, agent.id)
|
|
265
|
+
const sessions = loadSessions()
|
|
266
|
+
const now = Date.now()
|
|
267
|
+
const existing = sessions[sessionId]
|
|
268
|
+
const session: Session = existing
|
|
269
|
+
? applyResolvedRoute({
|
|
270
|
+
...existing,
|
|
271
|
+
id: sessionId,
|
|
272
|
+
name: `Chatroom session for ${agent.name}`,
|
|
273
|
+
cwd: roomWorkspace,
|
|
274
|
+
user: 'chatroom',
|
|
275
|
+
provider: agent.provider,
|
|
276
|
+
model: agent.model,
|
|
277
|
+
credentialId: agent.credentialId ?? null,
|
|
278
|
+
fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
|
|
279
|
+
apiEndpoint: resolveAgentApiEndpoint(agent),
|
|
280
|
+
sessionType: existing.sessionType || 'human',
|
|
281
|
+
agentId: agent.id,
|
|
282
|
+
plugins: agent.plugins || agent.tools || [],
|
|
283
|
+
tools: agent.plugins || agent.tools || [],
|
|
284
|
+
createdAt: existing.createdAt || now,
|
|
285
|
+
lastActiveAt: now,
|
|
286
|
+
}, resolvePrimaryAgentRoute(agent))
|
|
287
|
+
: applyResolvedRoute({
|
|
288
|
+
...buildSyntheticSession(agent, chatroomId),
|
|
289
|
+
fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
|
|
290
|
+
lastActiveAt: now,
|
|
291
|
+
tools: agent.plugins || agent.tools || [],
|
|
292
|
+
}, resolvePrimaryAgentRoute(agent))
|
|
293
|
+
|
|
294
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
295
|
+
if (!session.delegateResumeIds || typeof session.delegateResumeIds !== 'object') {
|
|
296
|
+
session.delegateResumeIds = buildEmptyDelegateResumeIds()
|
|
192
297
|
}
|
|
298
|
+
if (session.codexThreadId === undefined) session.codexThreadId = null
|
|
299
|
+
if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
|
|
300
|
+
sessions[sessionId] = session
|
|
301
|
+
saveSessions(sessions)
|
|
302
|
+
return session
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function appendSyntheticSessionMessage(
|
|
306
|
+
sessionId: string,
|
|
307
|
+
role: 'user' | 'assistant',
|
|
308
|
+
text: string,
|
|
309
|
+
): void {
|
|
310
|
+
const trimmed = String(text || '').trim()
|
|
311
|
+
if (!trimmed) return
|
|
312
|
+
const sessions = loadSessions()
|
|
313
|
+
const session = sessions[sessionId]
|
|
314
|
+
if (!session) return
|
|
315
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
316
|
+
session.messages.push({
|
|
317
|
+
role,
|
|
318
|
+
text: trimmed,
|
|
319
|
+
time: Date.now(),
|
|
320
|
+
})
|
|
321
|
+
session.lastActiveAt = Date.now()
|
|
322
|
+
sessions[sessionId] = session
|
|
323
|
+
saveSessions(sessions)
|
|
193
324
|
}
|
|
194
325
|
|
|
195
326
|
/** Build agent's system prompt including skills and identity context */
|
|
@@ -207,6 +338,8 @@ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
|
207
338
|
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
208
339
|
identityLines.push('I should always embody this identity in the group chat. I am a persistent companion with my own soul and presence.')
|
|
209
340
|
parts.push(identityLines.join('\n'))
|
|
341
|
+
const continuityBlock = buildIdentityContinuityContext(null, agent)
|
|
342
|
+
if (continuityBlock) parts.push(continuityBlock)
|
|
210
343
|
|
|
211
344
|
// 2. Runtime & Capabilities (OpenClaw Style)
|
|
212
345
|
const runtimeLines = [
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chatroom-session-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('chatroom synthetic session persistence', () => {
|
|
36
|
+
it('reuses stored synthetic sessions and preserves delegate resume state', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const helpersMod = await import('./src/lib/server/chatroom-helpers.ts')
|
|
39
|
+
const helpers = helpersMod.default || helpersMod
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
41
|
+
const storage = storageMod.default || storageMod
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const agent = {
|
|
44
|
+
id: 'default',
|
|
45
|
+
name: 'Molly',
|
|
46
|
+
description: '',
|
|
47
|
+
systemPrompt: '',
|
|
48
|
+
provider: 'openai',
|
|
49
|
+
model: 'gpt-4o',
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
plugins: ['delegate'],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const first = helpers.ensureSyntheticSession(agent, 'room-1')
|
|
56
|
+
helpers.appendSyntheticSessionMessage(first.id, 'user', 'first prompt')
|
|
57
|
+
|
|
58
|
+
const sessions = storage.loadSessions()
|
|
59
|
+
sessions[first.id].delegateResumeIds = {
|
|
60
|
+
claudeCode: null,
|
|
61
|
+
codex: 'resume-123',
|
|
62
|
+
opencode: null,
|
|
63
|
+
gemini: null,
|
|
64
|
+
}
|
|
65
|
+
storage.saveSessions(sessions)
|
|
66
|
+
|
|
67
|
+
const second = helpers.ensureSyntheticSession({ ...agent, model: 'gpt-4.1' }, 'room-1')
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
sessionId: second.id,
|
|
70
|
+
cwd: second.cwd,
|
|
71
|
+
model: second.model,
|
|
72
|
+
messageCount: second.messages.length,
|
|
73
|
+
firstMessage: second.messages[0]?.text || '',
|
|
74
|
+
delegateResumeIds: second.delegateResumeIds,
|
|
75
|
+
plugins: second.plugins || [],
|
|
76
|
+
}))
|
|
77
|
+
`)
|
|
78
|
+
|
|
79
|
+
assert.equal(output.sessionId, 'chatroom-room-1-default')
|
|
80
|
+
assert.match(String(output.cwd), /chatrooms[\/\\]room-1$/)
|
|
81
|
+
assert.equal(output.model, 'gpt-4.1')
|
|
82
|
+
assert.equal(output.messageCount, 1)
|
|
83
|
+
assert.equal(output.firstMessage, 'first prompt')
|
|
84
|
+
assert.equal(output.delegateResumeIds?.codex, 'resume-123')
|
|
85
|
+
assert.deepEqual(output.plugins, ['delegate'])
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -2,9 +2,94 @@ import { Client, GatewayIntentBits, Events, Partials, AttachmentBuilder } from '
|
|
|
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 } from './media'
|
|
7
|
-
import { isNoMessage } from './manager'
|
|
7
|
+
import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
|
|
8
|
+
|
|
9
|
+
function buildDiscordThreadTitle(params: {
|
|
10
|
+
threadName?: string
|
|
11
|
+
channelName?: string
|
|
12
|
+
starterText?: string
|
|
13
|
+
fallbackId: string
|
|
14
|
+
}): string {
|
|
15
|
+
const threadName = String(params.threadName || '').trim()
|
|
16
|
+
if (threadName) return threadName
|
|
17
|
+
const snippet = String(params.starterText || '').replace(/\s+/g, ' ').trim().slice(0, 56)
|
|
18
|
+
if (snippet) return `${params.channelName || 'Discord'} · ${snippet}`
|
|
19
|
+
return `${params.channelName || 'Discord'} thread ${params.fallbackId}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function discordSenderName(message: any): string {
|
|
23
|
+
return message?.member?.displayName
|
|
24
|
+
|| message?.author?.globalName
|
|
25
|
+
|| message?.author?.displayName
|
|
26
|
+
|| message?.author?.username
|
|
27
|
+
|| 'Unknown'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function hydrateDiscordThreadContext(message: any, inbound: InboundMessage): Promise<void> {
|
|
31
|
+
const channel = message.channel as any
|
|
32
|
+
const isThread = typeof channel?.isThread === 'function' && channel.isThread()
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (isThread) {
|
|
36
|
+
const starter = typeof channel.fetchStarterMessage === 'function'
|
|
37
|
+
? await channel.fetchStarterMessage().catch(() => null)
|
|
38
|
+
: null
|
|
39
|
+
const historyCollection = channel?.messages && typeof channel.messages.fetch === 'function'
|
|
40
|
+
? await channel.messages.fetch({ limit: 8, before: message.id }).catch(() => null)
|
|
41
|
+
: null
|
|
42
|
+
const historyMessages = historyCollection
|
|
43
|
+
? Array.from(historyCollection.values()).sort((a: any, b: any) => (a.createdTimestamp || 0) - (b.createdTimestamp || 0))
|
|
44
|
+
: []
|
|
45
|
+
const history: InboundThreadHistoryEntry[] = historyMessages
|
|
46
|
+
.filter((item: any) => item?.content?.trim())
|
|
47
|
+
.map((item: any) => ({
|
|
48
|
+
role: (item.author?.bot ? 'assistant' : 'user') as 'assistant' | 'user',
|
|
49
|
+
senderName: discordSenderName(item),
|
|
50
|
+
text: item.content,
|
|
51
|
+
messageId: item.id,
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
inbound.threadParentChannelId = channel?.parentId || undefined
|
|
55
|
+
inbound.threadParentChannelName = channel?.parent?.name || undefined
|
|
56
|
+
inbound.threadStarterText = starter?.content?.trim() || undefined
|
|
57
|
+
inbound.threadStarterSenderName = starter ? discordSenderName(starter) : undefined
|
|
58
|
+
inbound.threadTitle = buildDiscordThreadTitle({
|
|
59
|
+
threadName: channel?.name,
|
|
60
|
+
channelName: inbound.threadParentChannelName || inbound.channelName,
|
|
61
|
+
starterText: inbound.threadStarterText,
|
|
62
|
+
fallbackId: inbound.threadId || inbound.channelId,
|
|
63
|
+
})
|
|
64
|
+
inbound.threadPersonaLabel = inbound.threadTitle
|
|
65
|
+
inbound.threadHistory = history.length ? history : undefined
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (message.reference?.messageId && typeof message.fetchReference === 'function') {
|
|
70
|
+
const starter = await message.fetchReference().catch(() => null)
|
|
71
|
+
if (!starter) return
|
|
72
|
+
inbound.threadStarterText = starter.content?.trim() || undefined
|
|
73
|
+
inbound.threadStarterSenderName = discordSenderName(starter)
|
|
74
|
+
inbound.threadParentChannelId = inbound.channelId
|
|
75
|
+
inbound.threadParentChannelName = inbound.channelName
|
|
76
|
+
inbound.threadTitle = buildDiscordThreadTitle({
|
|
77
|
+
channelName: inbound.channelName,
|
|
78
|
+
starterText: inbound.threadStarterText,
|
|
79
|
+
fallbackId: starter.id || inbound.replyToMessageId || inbound.channelId,
|
|
80
|
+
})
|
|
81
|
+
inbound.threadPersonaLabel = inbound.threadTitle
|
|
82
|
+
inbound.threadHistory = [{
|
|
83
|
+
role: (starter.author?.bot ? 'assistant' : 'user') as 'assistant' | 'user',
|
|
84
|
+
senderName: discordSenderName(starter),
|
|
85
|
+
text: starter.content || '',
|
|
86
|
+
messageId: starter.id,
|
|
87
|
+
}].filter((entry) => entry.text.trim().length > 0)
|
|
88
|
+
}
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
console.warn(`[discord] Thread context bootstrap failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
8
93
|
|
|
9
94
|
const discord: PlatformConnector = {
|
|
10
95
|
async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
|
|
@@ -23,6 +108,23 @@ const discord: PlatformConnector = {
|
|
|
23
108
|
? connector.config.channelIds.split(',').map((s) => s.trim()).filter(Boolean)
|
|
24
109
|
: null
|
|
25
110
|
|
|
111
|
+
async function resolveTextChannel(targetChannelId: string) {
|
|
112
|
+
const channel = await client.channels.fetch(targetChannelId)
|
|
113
|
+
if (!channel || !('send' in channel) || typeof (channel as any).send !== 'function') {
|
|
114
|
+
throw new Error(`Cannot send to channel ${targetChannelId}`)
|
|
115
|
+
}
|
|
116
|
+
return channel as any
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function resolveChannelMessage(targetChannelId: string, messageId: string) {
|
|
120
|
+
const channel = await resolveTextChannel(targetChannelId)
|
|
121
|
+
const messages = (channel as any).messages
|
|
122
|
+
if (!messages || typeof messages.fetch !== 'function') {
|
|
123
|
+
throw new Error(`Channel ${targetChannelId} does not support message actions`)
|
|
124
|
+
}
|
|
125
|
+
return await messages.fetch(messageId)
|
|
126
|
+
}
|
|
127
|
+
|
|
26
128
|
client.on(Events.MessageCreate, async (message) => {
|
|
27
129
|
console.log(`[discord] Message from ${message.author.username} in ${message.channel.type === 1 ? 'DM' : '#' + ('name' in message.channel ? (message.channel as any).name : message.channelId)}: ${message.content.slice(0, 80)}`)
|
|
28
130
|
// Ignore bot messages
|
|
@@ -71,9 +173,17 @@ const discord: PlatformConnector = {
|
|
|
71
173
|
senderId: message.author.id,
|
|
72
174
|
senderName: message.author.displayName || message.author.username,
|
|
73
175
|
text: message.content || (media.length > 0 ? '(media message)' : ''),
|
|
176
|
+
isGroup: message.channel.type !== 1,
|
|
177
|
+
messageId: message.id,
|
|
178
|
+
mentionsBot: client.user ? message.mentions.users.has(client.user.id) : false,
|
|
74
179
|
imageUrl: firstImage?.url,
|
|
75
180
|
media,
|
|
181
|
+
replyToMessageId: message.reference?.messageId || undefined,
|
|
182
|
+
threadId: typeof (message.channel as any).isThread === 'function' && (message.channel as any).isThread()
|
|
183
|
+
? message.channelId
|
|
184
|
+
: undefined,
|
|
76
185
|
}
|
|
186
|
+
await hydrateDiscordThreadContext(message, inbound)
|
|
77
187
|
|
|
78
188
|
try {
|
|
79
189
|
// Show typing indicator
|
|
@@ -82,16 +192,41 @@ const discord: PlatformConnector = {
|
|
|
82
192
|
|
|
83
193
|
if (isNoMessage(response)) return
|
|
84
194
|
|
|
195
|
+
const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
|
|
196
|
+
const targetChannelId = replyOptions.threadId || inbound.channelId
|
|
197
|
+
const sendChunk = async (chunk: string, isFirstChunk: boolean) => {
|
|
198
|
+
const channel = await resolveTextChannel(targetChannelId)
|
|
199
|
+
const payload: Record<string, unknown> = {
|
|
200
|
+
content: chunk,
|
|
201
|
+
allowedMentions: { repliedUser: false },
|
|
202
|
+
}
|
|
203
|
+
if (isFirstChunk && replyOptions.replyToMessageId) {
|
|
204
|
+
payload.reply = {
|
|
205
|
+
messageReference: replyOptions.replyToMessageId,
|
|
206
|
+
failIfNotExists: false,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const sent = await channel.send(payload)
|
|
210
|
+
return String(sent.id || '')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let lastMessageId: string | undefined
|
|
85
214
|
// Discord has a 2000 char limit per message
|
|
86
215
|
if (response.length <= 2000) {
|
|
87
|
-
await
|
|
216
|
+
lastMessageId = await sendChunk(response, true)
|
|
88
217
|
} else {
|
|
89
218
|
// Split into chunks
|
|
90
219
|
const chunks = response.match(/[\s\S]{1,1990}/g) || [response]
|
|
91
|
-
for (
|
|
92
|
-
await
|
|
220
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
221
|
+
lastMessageId = await sendChunk(chunks[i], i === 0)
|
|
93
222
|
}
|
|
94
223
|
}
|
|
224
|
+
await recordConnectorOutboundDelivery({
|
|
225
|
+
connectorId: connector.id,
|
|
226
|
+
inbound,
|
|
227
|
+
messageId: lastMessageId,
|
|
228
|
+
state: 'sent',
|
|
229
|
+
})
|
|
95
230
|
} catch (err: any) {
|
|
96
231
|
console.error(`[discord] Error handling message:`, err.message)
|
|
97
232
|
try {
|
|
@@ -109,10 +244,8 @@ const discord: PlatformConnector = {
|
|
|
109
244
|
return client.isReady()
|
|
110
245
|
},
|
|
111
246
|
async sendMessage(channelId, text, options) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
throw new Error(`Cannot send to channel ${channelId}`)
|
|
115
|
-
}
|
|
247
|
+
const targetChannelId = options?.threadId?.trim() || channelId
|
|
248
|
+
const channel = await resolveTextChannel(targetChannelId)
|
|
116
249
|
|
|
117
250
|
const files: AttachmentBuilder[] = []
|
|
118
251
|
if (options?.mediaPath) {
|
|
@@ -125,12 +258,43 @@ const discord: PlatformConnector = {
|
|
|
125
258
|
}
|
|
126
259
|
|
|
127
260
|
const content = options?.caption || text || undefined
|
|
128
|
-
const
|
|
261
|
+
const payload: Record<string, unknown> = {
|
|
129
262
|
content: content || (files.length ? undefined : '(empty)'),
|
|
130
263
|
files: files.length ? files : undefined,
|
|
131
|
-
}
|
|
264
|
+
}
|
|
265
|
+
if (options?.replyToMessageId) {
|
|
266
|
+
payload.reply = {
|
|
267
|
+
messageReference: options.replyToMessageId,
|
|
268
|
+
failIfNotExists: false,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const msg = await channel.send(payload)
|
|
132
273
|
return { messageId: msg.id }
|
|
133
274
|
},
|
|
275
|
+
async sendReaction(channelId, messageId, emoji) {
|
|
276
|
+
const message = await resolveChannelMessage(channelId, messageId)
|
|
277
|
+
await message.react(emoji)
|
|
278
|
+
},
|
|
279
|
+
async editMessage(channelId, messageId, newText) {
|
|
280
|
+
const message = await resolveChannelMessage(channelId, messageId)
|
|
281
|
+
await message.edit(newText)
|
|
282
|
+
},
|
|
283
|
+
async deleteMessage(channelId, messageId) {
|
|
284
|
+
const message = await resolveChannelMessage(channelId, messageId)
|
|
285
|
+
await message.delete()
|
|
286
|
+
},
|
|
287
|
+
async pinMessage(channelId, messageId) {
|
|
288
|
+
const message = await resolveChannelMessage(channelId, messageId)
|
|
289
|
+
await message.pin()
|
|
290
|
+
},
|
|
291
|
+
async sendTyping(channelId, options) {
|
|
292
|
+
const targetChannelId = options?.threadId?.trim() || channelId
|
|
293
|
+
const channel = await resolveTextChannel(targetChannelId)
|
|
294
|
+
if (typeof channel.sendTyping === 'function') {
|
|
295
|
+
await channel.sendTyping()
|
|
296
|
+
}
|
|
297
|
+
},
|
|
134
298
|
async stop() {
|
|
135
299
|
client.destroy()
|
|
136
300
|
console.log(`[discord] Bot disconnected`)
|