@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
|
@@ -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,14 @@
|
|
|
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
|
+
resolveAgentApiEndpoint,
|
|
10
|
+
resolveReplyTargetAgentId,
|
|
11
|
+
} from './chatroom-helpers'
|
|
5
12
|
|
|
6
13
|
function makeAgents(): Record<string, Agent> {
|
|
7
14
|
const now = Date.now()
|
|
@@ -37,6 +44,48 @@ describe('chatroom-helpers', () => {
|
|
|
37
44
|
assert.deepEqual(mentions, ['default', 'agent_analyst'])
|
|
38
45
|
})
|
|
39
46
|
|
|
47
|
+
it('routes reply-only messages back to the replied-to agent', () => {
|
|
48
|
+
const agents = makeAgents()
|
|
49
|
+
const memberIds = ['default', 'agent_analyst']
|
|
50
|
+
const replyTargetAgentId = resolveReplyTargetAgentId('agent-msg', [
|
|
51
|
+
{
|
|
52
|
+
id: 'agent-msg',
|
|
53
|
+
senderId: 'default',
|
|
54
|
+
senderName: 'Assistant',
|
|
55
|
+
role: 'assistant',
|
|
56
|
+
text: 'Here is the previous answer.',
|
|
57
|
+
mentions: [],
|
|
58
|
+
reactions: [],
|
|
59
|
+
time: Date.now(),
|
|
60
|
+
},
|
|
61
|
+
], memberIds)
|
|
62
|
+
const mentions = parseMentions('Can you expand on that?', agents, memberIds, { replyTargetAgentId })
|
|
63
|
+
assert.deepEqual(mentions, ['default'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('keeps explicit mentions ahead of reply-based implicit targeting', () => {
|
|
67
|
+
const agents = makeAgents()
|
|
68
|
+
const memberIds = ['default', 'agent_analyst']
|
|
69
|
+
const mentions = parseMentions('Actually @Analyst should take this one.', agents, memberIds, { replyTargetAgentId: 'default' })
|
|
70
|
+
assert.deepEqual(mentions, ['agent_analyst'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('ignores replies to non-agent messages', () => {
|
|
74
|
+
const replyTargetAgentId = resolveReplyTargetAgentId('user-msg', [
|
|
75
|
+
{
|
|
76
|
+
id: 'user-msg',
|
|
77
|
+
senderId: 'user',
|
|
78
|
+
senderName: 'You',
|
|
79
|
+
role: 'user',
|
|
80
|
+
text: 'Question',
|
|
81
|
+
mentions: [],
|
|
82
|
+
reactions: [],
|
|
83
|
+
time: Date.now(),
|
|
84
|
+
},
|
|
85
|
+
], ['default', 'agent_analyst'])
|
|
86
|
+
assert.equal(replyTargetAgentId, null)
|
|
87
|
+
})
|
|
88
|
+
|
|
40
89
|
it('compacts long chatrooms with a persisted summary message', () => {
|
|
41
90
|
const now = Date.now()
|
|
42
91
|
const chatroom: Chatroom = {
|
|
@@ -90,5 +139,21 @@ describe('chatroom-helpers', () => {
|
|
|
90
139
|
const attachmentMarkers = history.filter((msg) => msg.text.includes('[Attached:')).length
|
|
91
140
|
assert.ok(attachmentMarkers <= 6)
|
|
92
141
|
})
|
|
93
|
-
})
|
|
94
142
|
|
|
143
|
+
it('resolves default provider endpoints for chatroom sessions', () => {
|
|
144
|
+
const now = Date.now()
|
|
145
|
+
const agent: Agent = {
|
|
146
|
+
id: 'agent_writer',
|
|
147
|
+
name: 'Writer',
|
|
148
|
+
description: '',
|
|
149
|
+
systemPrompt: '',
|
|
150
|
+
provider: 'ollama',
|
|
151
|
+
model: 'glm-5:cloud',
|
|
152
|
+
createdAt: now,
|
|
153
|
+
updatedAt: now,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
assert.equal(resolveAgentApiEndpoint(agent), 'http://localhost:11434')
|
|
157
|
+
assert.equal(buildSyntheticSession(agent, 'room-1').apiEndpoint, 'http://localhost:11434')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import os from 'os'
|
|
2
2
|
import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
|
|
3
3
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
4
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
4
5
|
import { genId } from '@/lib/id'
|
|
5
|
-
import
|
|
6
|
+
import { getProvider } from '@/lib/providers'
|
|
7
|
+
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
8
|
+
import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
|
|
6
9
|
|
|
7
10
|
/** Resolve API key from an agent's credentialId */
|
|
8
11
|
export function resolveApiKey(credentialId: string | null | undefined): string | null {
|
|
@@ -34,6 +37,14 @@ export function isMuted(chatroom: Chatroom, agentId: string): boolean {
|
|
|
34
37
|
return new Date(member.mutedUntil).getTime() > Date.now()
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
export function resolveAgentApiEndpoint(agent: Agent): string | null {
|
|
41
|
+
const explicit = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
42
|
+
if (explicit) return explicit
|
|
43
|
+
const provider = getProvider(agent.provider)
|
|
44
|
+
if (!provider?.defaultEndpoint) return null
|
|
45
|
+
return normalizeProviderEndpoint(agent.provider, provider.defaultEndpoint) || provider.defaultEndpoint.replace(/\/+$/, '')
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
const COMPACTION_PREFIX = '[Conversation summary]'
|
|
38
49
|
|
|
39
50
|
function normalizeMentionToken(raw: string): string {
|
|
@@ -53,7 +64,12 @@ function truncateText(text: string, max: number): string {
|
|
|
53
64
|
import { isImplicitlyMentioned } from './chatroom-orchestration'
|
|
54
65
|
|
|
55
66
|
/** Parse @mentions from message text, returns matching agentIds */
|
|
56
|
-
export function parseMentions(
|
|
67
|
+
export function parseMentions(
|
|
68
|
+
text: string,
|
|
69
|
+
agents: Record<string, Agent>,
|
|
70
|
+
memberIds: string[],
|
|
71
|
+
opts?: { replyTargetAgentId?: string | null },
|
|
72
|
+
): string[] {
|
|
57
73
|
if (/@all\b/i.test(text)) return [...memberIds]
|
|
58
74
|
const mentionPattern = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
|
|
59
75
|
const mentioned: string[] = []
|
|
@@ -73,8 +89,17 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
|
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
// 2.
|
|
77
|
-
// Only if no explicit mentions found
|
|
92
|
+
// 2. Reply-based implicit mention
|
|
93
|
+
// Only if no explicit mentions were found.
|
|
94
|
+
if (mentioned.length === 0) {
|
|
95
|
+
const replyTargetAgentId = opts?.replyTargetAgentId
|
|
96
|
+
if (replyTargetAgentId && memberIds.includes(replyTargetAgentId)) {
|
|
97
|
+
mentioned.push(replyTargetAgentId)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Implicit mentions (OpenClaw Style - Reading the room)
|
|
102
|
+
// Only if no explicit mentions were found.
|
|
78
103
|
if (mentioned.length === 0) {
|
|
79
104
|
for (const id of memberIds) {
|
|
80
105
|
const agent = agents[id]
|
|
@@ -87,6 +112,19 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
|
|
|
87
112
|
return mentioned
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
export function resolveReplyTargetAgentId(
|
|
116
|
+
replyToId: string | undefined,
|
|
117
|
+
messages: ChatroomMessage[],
|
|
118
|
+
memberIds: string[],
|
|
119
|
+
): string | null {
|
|
120
|
+
if (!replyToId) return null
|
|
121
|
+
const replyMsg = messages.find((m) => m.id === replyToId)
|
|
122
|
+
if (!replyMsg) return null
|
|
123
|
+
if (replyMsg.role !== 'assistant') return null
|
|
124
|
+
if (!memberIds.includes(replyMsg.senderId)) return null
|
|
125
|
+
return replyMsg.senderId
|
|
126
|
+
}
|
|
127
|
+
|
|
90
128
|
/**
|
|
91
129
|
* Persisted chatroom compaction so long-lived rooms stay inside context budgets.
|
|
92
130
|
* Returns true when the message list was compacted.
|
|
@@ -126,9 +164,9 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
|
|
|
126
164
|
.map((id) => {
|
|
127
165
|
const a = agents[id]
|
|
128
166
|
if (!a) return null
|
|
129
|
-
const
|
|
167
|
+
const plugins = (a.plugins || a.tools)?.length ? `Plugins: ${(a.plugins || a.tools)!.join(', ')}` : 'No specialized plugins'
|
|
130
168
|
const desc = a.description || a.soul || 'No description'
|
|
131
|
-
return `- **${a.name}**: ${desc}\n ${
|
|
169
|
+
return `- **${a.name}**: ${desc}\n ${plugins}`
|
|
132
170
|
})
|
|
133
171
|
.filter(Boolean)
|
|
134
172
|
.join('\n')
|
|
@@ -182,12 +220,12 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
|
|
|
182
220
|
model: agent.model,
|
|
183
221
|
credentialId: agent.credentialId ?? null,
|
|
184
222
|
fallbackCredentialIds: agent.fallbackCredentialIds,
|
|
185
|
-
apiEndpoint: agent
|
|
223
|
+
apiEndpoint: resolveAgentApiEndpoint(agent),
|
|
186
224
|
claudeSessionId: null,
|
|
187
225
|
messages: [],
|
|
188
226
|
createdAt: Date.now(),
|
|
189
227
|
lastActiveAt: Date.now(),
|
|
190
|
-
|
|
228
|
+
plugins: agent.plugins || agent.tools || [],
|
|
191
229
|
agentId: agent.id,
|
|
192
230
|
}
|
|
193
231
|
}
|
|
@@ -207,6 +245,8 @@ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
|
207
245
|
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
208
246
|
identityLines.push('I should always embody this identity in the group chat. I am a persistent companion with my own soul and presence.')
|
|
209
247
|
parts.push(identityLines.join('\n'))
|
|
248
|
+
const continuityBlock = buildIdentityContinuityContext(null, agent)
|
|
249
|
+
if (continuityBlock) parts.push(continuityBlock)
|
|
210
250
|
|
|
211
251
|
// 2. Runtime & Capabilities (OpenClaw Style)
|
|
212
252
|
const runtimeLines = [
|
|
@@ -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`)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import type { Connector } from '@/types'
|
|
4
|
+
import { buildConnectorDoctorPreview, buildConnectorDoctorReport } from './doctor'
|
|
5
|
+
|
|
6
|
+
test('buildConnectorDoctorPreview merges overrides onto an existing connector', () => {
|
|
7
|
+
const base: Connector = {
|
|
8
|
+
id: 'connector-1',
|
|
9
|
+
name: 'Existing Connector',
|
|
10
|
+
platform: 'slack',
|
|
11
|
+
agentId: 'agent-1',
|
|
12
|
+
chatroomId: null,
|
|
13
|
+
credentialId: 'cred-1',
|
|
14
|
+
config: { replyMode: 'first', threadBinding: 'prefer' },
|
|
15
|
+
isEnabled: true,
|
|
16
|
+
status: 'running',
|
|
17
|
+
createdAt: 1,
|
|
18
|
+
updatedAt: 1,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const preview = buildConnectorDoctorPreview({
|
|
22
|
+
baseConnector: base,
|
|
23
|
+
input: {
|
|
24
|
+
name: 'Preview Connector',
|
|
25
|
+
agentId: null,
|
|
26
|
+
chatroomId: 'chatroom-9',
|
|
27
|
+
config: { replyMode: 'off', sessionScope: 'main' },
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
assert.equal(preview.name, 'Preview Connector')
|
|
32
|
+
assert.equal(preview.agentId, null)
|
|
33
|
+
assert.equal(preview.chatroomId, 'chatroom-9')
|
|
34
|
+
assert.deepEqual(preview.config, { replyMode: 'off', sessionScope: 'main' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('buildConnectorDoctorReport returns effective warnings and policy for preview connectors', () => {
|
|
38
|
+
const connector = buildConnectorDoctorPreview({
|
|
39
|
+
input: {
|
|
40
|
+
platform: 'telegram',
|
|
41
|
+
config: {
|
|
42
|
+
sessionScope: 'main',
|
|
43
|
+
replyMode: 'off',
|
|
44
|
+
threadBinding: 'off',
|
|
45
|
+
idleTimeoutSec: '0',
|
|
46
|
+
maxAgeSec: '0',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const report = buildConnectorDoctorReport(connector)
|
|
52
|
+
|
|
53
|
+
assert.equal(report.policy.scope, 'main')
|
|
54
|
+
assert.equal(report.policy.replyMode, 'off')
|
|
55
|
+
assert.ok(report.warnings.some((item) => item.includes('blend unrelated connector conversations')))
|
|
56
|
+
assert.ok(report.warnings.some((item) => item.includes('freshness reset is disabled')))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('buildConnectorDoctorReport includes runtime warning for stopped existing connectors', () => {
|
|
60
|
+
const connector = buildConnectorDoctorPreview({
|
|
61
|
+
baseConnector: {
|
|
62
|
+
id: 'connector-2',
|
|
63
|
+
name: 'Existing Connector',
|
|
64
|
+
platform: 'slack',
|
|
65
|
+
agentId: 'agent-1',
|
|
66
|
+
chatroomId: null,
|
|
67
|
+
credentialId: 'cred-1',
|
|
68
|
+
config: {},
|
|
69
|
+
isEnabled: true,
|
|
70
|
+
status: 'stopped',
|
|
71
|
+
createdAt: 1,
|
|
72
|
+
updatedAt: 1,
|
|
73
|
+
},
|
|
74
|
+
input: {},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const report = buildConnectorDoctorReport(connector, null, { baseConnector: connector })
|
|
78
|
+
|
|
79
|
+
assert.ok(report.warnings.some((item) => item.includes('not currently connected')))
|
|
80
|
+
})
|