@swarmclawai/swarmclaw 0.3.1 → 0.4.5
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 +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadConnectors } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
function readSecret(req: Request): string {
|
|
7
|
+
const url = new URL(req.url)
|
|
8
|
+
return (
|
|
9
|
+
req.headers.get('x-connector-secret')
|
|
10
|
+
|| url.searchParams.get('secret')
|
|
11
|
+
|| ''
|
|
12
|
+
).trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseWebhookBody(rawBody: string): Record<string, unknown> {
|
|
16
|
+
const trimmed = rawBody.trim()
|
|
17
|
+
if (!trimmed) return {}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(trimmed)
|
|
21
|
+
if (Array.isArray(parsed)) return { data: parsed }
|
|
22
|
+
return parsed && typeof parsed === 'object' ? parsed as Record<string, unknown> : {}
|
|
23
|
+
} catch {
|
|
24
|
+
// Fall back to URL-encoded payloads used by some webhook providers.
|
|
25
|
+
const params = new URLSearchParams(rawBody)
|
|
26
|
+
const nested = params.get('payload') || params.get('data') || params.get('message') || ''
|
|
27
|
+
if (nested) {
|
|
28
|
+
try {
|
|
29
|
+
const parsedNested = JSON.parse(nested)
|
|
30
|
+
if (Array.isArray(parsedNested)) return { data: parsedNested }
|
|
31
|
+
return parsedNested && typeof parsedNested === 'object'
|
|
32
|
+
? parsedNested as Record<string, unknown>
|
|
33
|
+
: {}
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore malformed nested JSON and return flat map below.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const out: Record<string, unknown> = {}
|
|
39
|
+
for (const [key, value] of params.entries()) out[key] = value
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
45
|
+
const { id } = await params
|
|
46
|
+
const connectors = loadConnectors()
|
|
47
|
+
const connector = connectors[id]
|
|
48
|
+
if (!connector) return NextResponse.json({ error: 'Connector not found' }, { status: 404 })
|
|
49
|
+
|
|
50
|
+
const requiredSecret = String(connector.config?.webhookSecret || '').trim()
|
|
51
|
+
if (requiredSecret && readSecret(req) !== requiredSecret) {
|
|
52
|
+
return NextResponse.json({ error: 'Invalid connector webhook secret' }, { status: 401 })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rawBody = await req.text().catch(() => '')
|
|
56
|
+
const payload = parseWebhookBody(rawBody)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (connector.platform === 'teams') {
|
|
60
|
+
const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
|
|
61
|
+
const handler = (globalThis as any)[handlerKey]
|
|
62
|
+
if (typeof handler !== 'function') {
|
|
63
|
+
return NextResponse.json({ error: 'Teams connector is not running or not ready' }, { status: 409 })
|
|
64
|
+
}
|
|
65
|
+
await handler(payload)
|
|
66
|
+
return NextResponse.json({ ok: true })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (connector.platform === 'googlechat') {
|
|
70
|
+
const handlerKey = `__swarmclaw_googlechat_handler_${connector.id}__`
|
|
71
|
+
const handler = (globalThis as any)[handlerKey]
|
|
72
|
+
if (typeof handler !== 'function') {
|
|
73
|
+
return NextResponse.json({ error: 'Google Chat connector is not running or not ready' }, { status: 409 })
|
|
74
|
+
}
|
|
75
|
+
const result = await handler(payload)
|
|
76
|
+
if (result && typeof result === 'object' && Object.keys(result).length > 0) {
|
|
77
|
+
return NextResponse.json(result)
|
|
78
|
+
}
|
|
79
|
+
return NextResponse.json({})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (connector.platform === 'bluebubbles') {
|
|
83
|
+
const handlerKey = `__swarmclaw_bluebubbles_handler_${connector.id}__`
|
|
84
|
+
const handler = (globalThis as any)[handlerKey]
|
|
85
|
+
if (typeof handler !== 'function') {
|
|
86
|
+
return NextResponse.json({ error: 'BlueBubbles connector is not running or not ready' }, { status: 409 })
|
|
87
|
+
}
|
|
88
|
+
const result = await handler(payload)
|
|
89
|
+
if (result && typeof result === 'object' && Object.keys(result).length > 0) {
|
|
90
|
+
return NextResponse.json(result)
|
|
91
|
+
}
|
|
92
|
+
return NextResponse.json({})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return NextResponse.json({ error: `Platform "${connector.platform}" does not support connector webhook ingress.` }, { status: 400 })
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
return NextResponse.json({ error: err?.message || 'Webhook processing failed' }, { status: 500 })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadConnectors, saveConnectors } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
5
|
import type { Connector } from '@/types'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
export async function GET(_req: Request) {
|
|
7
10
|
const connectors = loadConnectors()
|
|
8
11
|
// Merge runtime status from manager
|
|
9
12
|
try {
|
|
@@ -24,7 +27,7 @@ export async function GET() {
|
|
|
24
27
|
export async function POST(req: Request) {
|
|
25
28
|
const body = await req.json()
|
|
26
29
|
const connectors = loadConnectors()
|
|
27
|
-
const id =
|
|
30
|
+
const id = genId()
|
|
28
31
|
|
|
29
32
|
const connector: Connector = {
|
|
30
33
|
id,
|
|
@@ -42,9 +45,13 @@ export async function POST(req: Request) {
|
|
|
42
45
|
|
|
43
46
|
connectors[id] = connector
|
|
44
47
|
saveConnectors(connectors)
|
|
48
|
+
notify('connectors')
|
|
45
49
|
|
|
46
50
|
// Auto-start if connector has credentials (or is WhatsApp which uses QR)
|
|
47
|
-
const hasCredentials = connector.platform === 'whatsapp'
|
|
51
|
+
const hasCredentials = connector.platform === 'whatsapp'
|
|
52
|
+
|| connector.platform === 'openclaw'
|
|
53
|
+
|| (connector.platform === 'bluebubbles' && (!!connector.credentialId || !!connector.config.password))
|
|
54
|
+
|| !!connector.credentialId
|
|
48
55
|
if (hasCredentials && body.autoStart !== false) {
|
|
49
56
|
try {
|
|
50
57
|
const { startConnector } = await import('@/lib/server/connectors/manager')
|
|
@@ -53,6 +60,7 @@ export async function POST(req: Request) {
|
|
|
53
60
|
connector.status = 'running'
|
|
54
61
|
connectors[id] = connector
|
|
55
62
|
saveConnectors(connectors)
|
|
63
|
+
notify('connectors')
|
|
56
64
|
} catch { /* auto-start is best-effort */ }
|
|
57
65
|
}
|
|
58
66
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadCredentials, saveCredentials } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
4
|
|
|
4
5
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
6
|
const { id: credId } = await params
|
|
6
7
|
const creds = loadCredentials()
|
|
7
8
|
if (!creds[credId]) {
|
|
8
|
-
return
|
|
9
|
+
return notFound()
|
|
9
10
|
}
|
|
10
11
|
delete creds[credId]
|
|
11
12
|
saveCredentials(creds)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadCredentials, saveCredentials, encryptKey } from '@/lib/server/storage'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: Request) {
|
|
6
8
|
const creds = loadCredentials()
|
|
7
9
|
const safe: Record<string, any> = {}
|
|
8
10
|
for (const [id, c] of Object.entries(creds) as [string, any][]) {
|
|
@@ -16,7 +18,7 @@ export async function POST(req: Request) {
|
|
|
16
18
|
if (!provider || !apiKey) {
|
|
17
19
|
return NextResponse.json({ error: 'provider and apiKey are required' }, { status: 400 })
|
|
18
20
|
}
|
|
19
|
-
const id = 'cred_' +
|
|
21
|
+
const id = 'cred_' + genId(6)
|
|
20
22
|
const creds = loadCredentials()
|
|
21
23
|
creds[id] = {
|
|
22
24
|
id,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { ensureDaemonStarted, getDaemonStatus, startDaemon, stopDaemon } from '@/lib/server/daemon-state'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: Request) {
|
|
5
8
|
ensureDaemonStarted('api/daemon:get')
|
|
6
9
|
return NextResponse.json(getDaemonStatus())
|
|
7
10
|
}
|
|
@@ -12,9 +15,11 @@ export async function POST(req: Request) {
|
|
|
12
15
|
|
|
13
16
|
if (action === 'start') {
|
|
14
17
|
startDaemon({ source: 'api/daemon:post:start', manualStart: true })
|
|
18
|
+
notify('daemon')
|
|
15
19
|
return NextResponse.json({ ok: true, status: 'running' })
|
|
16
20
|
} else if (action === 'stop') {
|
|
17
21
|
stopDaemon({ source: 'api/daemon:post:stop', manualStop: true })
|
|
22
|
+
notify('daemon')
|
|
18
23
|
return NextResponse.json({ ok: true, status: 'stopped' })
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import { NextResponse } from 'next/server'
|
|
3
3
|
import { loadDocuments, saveDocuments } from '@/lib/server/storage'
|
|
4
4
|
|
|
@@ -69,7 +69,7 @@ export async function POST(req: Request) {
|
|
|
69
69
|
const body = await req.json().catch(() => ({}))
|
|
70
70
|
const now = Date.now()
|
|
71
71
|
const docs = loadDocuments()
|
|
72
|
-
const id = body.id ||
|
|
72
|
+
const id = body.id || genId(6)
|
|
73
73
|
const fileName = body.fileName || body.filename || ''
|
|
74
74
|
const title = body.title || fileName || 'Untitled Document'
|
|
75
75
|
const content = typeof body.content === 'string' ? body.content : ''
|
|
@@ -21,6 +21,14 @@ const MIME_MAP: Record<string, string> = {
|
|
|
21
21
|
'.jsx': 'text/plain',
|
|
22
22
|
'.py': 'text/plain',
|
|
23
23
|
'.sh': 'text/plain',
|
|
24
|
+
'.pdf': 'application/pdf',
|
|
25
|
+
'.csv': 'text/csv',
|
|
26
|
+
'.xml': 'application/xml',
|
|
27
|
+
'.zip': 'application/zip',
|
|
28
|
+
'.doc': 'application/msword',
|
|
29
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
30
|
+
'.xls': 'application/vnd.ms-excel',
|
|
31
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
|
package/src/app/api/ip/route.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { localIP } from '@/lib/server/storage'
|
|
3
|
+
export const dynamic = 'force-dynamic'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
export async function GET(_req: Request) {
|
|
5
7
|
return NextResponse.json({ ip: localIP(), port: parseInt(process.env.PORT || '3000') })
|
|
6
8
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
2
3
|
import { getMemoryDb } from '@/lib/server/memory-db'
|
|
3
4
|
|
|
4
5
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -6,7 +7,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
6
7
|
const db = getMemoryDb()
|
|
7
8
|
const entry = db.get(id)
|
|
8
9
|
if (!entry || entry.category !== 'knowledge') {
|
|
9
|
-
return
|
|
10
|
+
return notFound()
|
|
10
11
|
}
|
|
11
12
|
return NextResponse.json(entry)
|
|
12
13
|
}
|
|
@@ -16,7 +17,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
16
17
|
const db = getMemoryDb()
|
|
17
18
|
const existing = db.get(id)
|
|
18
19
|
if (!existing || existing.category !== 'knowledge') {
|
|
19
|
-
return
|
|
20
|
+
return notFound()
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const body = await req.json().catch(() => null)
|
|
@@ -44,7 +45,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
44
45
|
|
|
45
46
|
const updated = db.update(id, updates)
|
|
46
47
|
if (!updated) {
|
|
47
|
-
return
|
|
48
|
+
return notFound()
|
|
48
49
|
}
|
|
49
50
|
return NextResponse.json(updated)
|
|
50
51
|
}
|
|
@@ -54,7 +55,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
54
55
|
const db = getMemoryDb()
|
|
55
56
|
const existing = db.get(id)
|
|
56
57
|
if (!existing || existing.category !== 'knowledge') {
|
|
57
|
-
return
|
|
58
|
+
return notFound()
|
|
58
59
|
}
|
|
59
60
|
db.delete(id)
|
|
60
61
|
return NextResponse.json({ deleted: id })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
6
6
|
|
|
7
7
|
const TEXT_EXTS = new Set([
|
|
@@ -40,7 +40,7 @@ export async function POST(req: Request) {
|
|
|
40
40
|
|
|
41
41
|
// Save file to uploads
|
|
42
42
|
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
43
|
-
const safeName =
|
|
43
|
+
const safeName = genId() + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
44
44
|
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
45
45
|
fs.writeFileSync(filePath, buf)
|
|
46
46
|
|
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadMcpServers, saveMcpServers, deleteMcpServer } from '@/lib/server/storage'
|
|
3
|
+
import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
const ops: CollectionOps<any> = { load: loadMcpServers, save: saveMcpServers, deleteFn: deleteMcpServer }
|
|
3
7
|
|
|
4
8
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
9
|
const { id } = await params
|
|
6
10
|
const servers = loadMcpServers()
|
|
7
|
-
if (!servers[id]) return
|
|
11
|
+
if (!servers[id]) return notFound()
|
|
8
12
|
return NextResponse.json(servers[id])
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
12
16
|
const { id } = await params
|
|
13
17
|
const body = await req.json()
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
id,
|
|
20
|
-
updatedAt: Date.now(),
|
|
21
|
-
}
|
|
22
|
-
saveMcpServers(servers)
|
|
23
|
-
return NextResponse.json(servers[id])
|
|
18
|
+
const result = mutateItem(ops, id, (server) => ({
|
|
19
|
+
...server, ...body, id, updatedAt: Date.now(),
|
|
20
|
+
}))
|
|
21
|
+
if (!result) return notFound()
|
|
22
|
+
return NextResponse.json(result)
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
27
26
|
const { id } = await params
|
|
28
|
-
|
|
29
|
-
if (!servers[id]) return new NextResponse(null, { status: 404 })
|
|
30
|
-
deleteMcpServer(id)
|
|
27
|
+
if (!deleteItem(ops, id)) return notFound()
|
|
31
28
|
return NextResponse.json({ deleted: id })
|
|
32
29
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadMcpServers } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
4
|
import { connectMcpServer, mcpToolsToLangChain, disconnectMcpServer } from '@/lib/server/mcp-client'
|
|
4
5
|
|
|
5
6
|
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
7
|
const { id } = await params
|
|
7
8
|
const servers = loadMcpServers()
|
|
8
9
|
const server = servers[id]
|
|
9
|
-
if (!server) return
|
|
10
|
+
if (!server) return notFound()
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
const { client, transport } = await connectMcpServer(server)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadMcpServers } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
4
|
import { connectMcpServer, disconnectMcpServer } from '@/lib/server/mcp-client'
|
|
4
5
|
|
|
5
6
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
7
|
const { id } = await params
|
|
7
8
|
const servers = loadMcpServers()
|
|
8
9
|
const config = servers[id]
|
|
9
|
-
if (!config) return
|
|
10
|
+
if (!config) return notFound()
|
|
10
11
|
|
|
11
12
|
let client: any
|
|
12
13
|
let transport: any
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadMcpServers, saveMcpServers } from '@/lib/server/storage'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: Request) {
|
|
6
8
|
return NextResponse.json(loadMcpServers())
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export async function POST(req: Request) {
|
|
10
12
|
const body = await req.json()
|
|
11
13
|
const servers = loadMcpServers()
|
|
12
|
-
const id =
|
|
14
|
+
const id = genId()
|
|
13
15
|
servers[id] = {
|
|
14
16
|
id,
|
|
15
17
|
name: body.name,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import { NextResponse } from 'next/server'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
5
|
import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset, storeMemoryImageFromDataUrl } from '@/lib/server/memory-db'
|
|
5
6
|
import { resolveLookupRequest } from '@/lib/server/memory-graph'
|
|
6
7
|
import type { MemoryImage } from '@/types'
|
|
@@ -42,7 +43,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
42
43
|
|
|
43
44
|
if (limits.maxDepth <= 0) {
|
|
44
45
|
const entry = db.get(id)
|
|
45
|
-
if (!entry) return
|
|
46
|
+
if (!entry) return notFound()
|
|
46
47
|
if (envelope) {
|
|
47
48
|
return NextResponse.json({
|
|
48
49
|
entries: [entry],
|
|
@@ -55,7 +56,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const result = db.getWithLinked(id, limits.maxDepth, limits.maxPerLookup, limits.maxLinkedExpansion)
|
|
58
|
-
if (!result) return
|
|
59
|
+
if (!result) return notFound()
|
|
59
60
|
if (envelope) return NextResponse.json(result)
|
|
60
61
|
return NextResponse.json(result.entries)
|
|
61
62
|
}
|
|
@@ -78,7 +79,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
78
79
|
const updated = linkAction === 'link'
|
|
79
80
|
? db.link(id, targetIds, true)
|
|
80
81
|
: db.unlink(id, targetIds, true)
|
|
81
|
-
if (!updated) return
|
|
82
|
+
if (!updated) return notFound()
|
|
82
83
|
return NextResponse.json(updated)
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -90,7 +91,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
90
91
|
image = null
|
|
91
92
|
} else if (inputImageDataUrl) {
|
|
92
93
|
try {
|
|
93
|
-
image = await storeMemoryImageFromDataUrl(inputImageDataUrl, `${id}-${
|
|
94
|
+
image = await storeMemoryImageFromDataUrl(inputImageDataUrl, `${id}-${genId(2)}`)
|
|
94
95
|
} catch (err) {
|
|
95
96
|
return NextResponse.json({ error: err instanceof Error ? err.message : 'Invalid image data URL' }, { status: 400 })
|
|
96
97
|
}
|
|
@@ -99,7 +100,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
99
100
|
return NextResponse.json({ error: `Image file not found: ${inputImagePath}` }, { status: 400 })
|
|
100
101
|
}
|
|
101
102
|
try {
|
|
102
|
-
image = await storeMemoryImageAsset(inputImagePath, `${id}-${
|
|
103
|
+
image = await storeMemoryImageAsset(inputImagePath, `${id}-${genId(2)}`)
|
|
103
104
|
} catch (err) {
|
|
104
105
|
return NextResponse.json({ error: err instanceof Error ? err.message : 'Failed to store memory image' }, { status: 400 })
|
|
105
106
|
}
|
|
@@ -114,7 +115,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
114
115
|
? String((image as { path: string }).path)
|
|
115
116
|
: (typeof body.imagePath === 'string' ? body.imagePath : undefined),
|
|
116
117
|
})
|
|
117
|
-
if (!entry) return
|
|
118
|
+
if (!entry) return notFound()
|
|
118
119
|
return NextResponse.json(entry)
|
|
119
120
|
}
|
|
120
121
|
|
|
@@ -122,5 +123,5 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
122
123
|
const { id } = await params
|
|
123
124
|
const db = getMemoryDb()
|
|
124
125
|
db.delete(id)
|
|
125
|
-
return NextResponse.json(
|
|
126
|
+
return NextResponse.json({ ok: true })
|
|
126
127
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import { NextResponse } from 'next/server'
|
|
4
4
|
import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset, storeMemoryImageFromDataUrl } from '@/lib/server/memory-db'
|
|
@@ -72,7 +72,7 @@ export async function POST(req: Request) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const db = getMemoryDb()
|
|
75
|
-
const draftId =
|
|
75
|
+
const draftId = genId(6)
|
|
76
76
|
|
|
77
77
|
let image = body.image
|
|
78
78
|
const inputImagePath = typeof body.imagePath === 'string' ? body.imagePath.trim() : ''
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
2
3
|
import fs from 'fs'
|
|
3
4
|
import path from 'path'
|
|
4
5
|
|
|
@@ -20,7 +21,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
|
|
|
20
21
|
const filePath = path.join(IMAGES_DIR, safeName)
|
|
21
22
|
|
|
22
23
|
if (!fs.existsSync(filePath)) {
|
|
23
|
-
return
|
|
24
|
+
return notFound()
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const ext = path.extname(safeName).toLowerCase()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
export const dynamic = 'force-dynamic'
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const { listRunningConnectors, getRunningInstance } = await import('@/lib/server/connectors/manager')
|
|
7
|
+
const openclawConnectors = listRunningConnectors('openclaw')
|
|
8
|
+
|
|
9
|
+
if (!openclawConnectors.length) {
|
|
10
|
+
return NextResponse.json({ devices: [], note: 'No running OpenClaw connector.' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The directory.list RPC requires gateway support — degrade gracefully
|
|
14
|
+
return NextResponse.json({
|
|
15
|
+
devices: [],
|
|
16
|
+
connectors: openclawConnectors.map((c) => ({
|
|
17
|
+
id: c.id,
|
|
18
|
+
name: c.name,
|
|
19
|
+
platform: c.platform,
|
|
20
|
+
})),
|
|
21
|
+
note: 'Directory listing requires OpenClaw gateway directory.list RPC support.',
|
|
22
|
+
})
|
|
23
|
+
} catch (err: any) {
|
|
24
|
+
return NextResponse.json({ error: err.message || 'Directory listing failed' }, { status: 500 })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import { probeOpenClawHealth } from '@/lib/server/openclaw-health'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PORTS = [18789, 18790]
|
|
7
|
+
const PROBE_TIMEOUT_MS = 4000
|
|
8
|
+
|
|
9
|
+
function getLanIps(): string[] {
|
|
10
|
+
const interfaces = os.networkInterfaces()
|
|
11
|
+
const ips: string[] = []
|
|
12
|
+
for (const iface of Object.values(interfaces)) {
|
|
13
|
+
if (!iface) continue
|
|
14
|
+
for (const info of iface) {
|
|
15
|
+
if (info.family === 'IPv4' && !info.internal) {
|
|
16
|
+
ips.push(info.address)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return ips
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function GET() {
|
|
24
|
+
try {
|
|
25
|
+
const hosts = ['127.0.0.1', ...getLanIps()]
|
|
26
|
+
const probes: Array<Promise<{
|
|
27
|
+
host: string
|
|
28
|
+
port: number
|
|
29
|
+
healthy: boolean
|
|
30
|
+
models?: string[]
|
|
31
|
+
error?: string
|
|
32
|
+
}>> = []
|
|
33
|
+
|
|
34
|
+
for (const host of hosts) {
|
|
35
|
+
for (const port of DEFAULT_PORTS) {
|
|
36
|
+
probes.push(
|
|
37
|
+
probeOpenClawHealth({
|
|
38
|
+
endpoint: `http://${host}:${port}`,
|
|
39
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
40
|
+
}).then((result) => ({
|
|
41
|
+
host,
|
|
42
|
+
port,
|
|
43
|
+
healthy: result.ok,
|
|
44
|
+
models: result.models.length > 0 ? result.models : undefined,
|
|
45
|
+
error: result.error || undefined,
|
|
46
|
+
})).catch(() => ({
|
|
47
|
+
host,
|
|
48
|
+
port,
|
|
49
|
+
healthy: false,
|
|
50
|
+
error: 'unreachable',
|
|
51
|
+
})),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const results = await Promise.all(probes)
|
|
57
|
+
return NextResponse.json({ gateways: results })
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
return NextResponse.json({ error: err.message || 'Discovery failed' }, { status: 500 })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { runSync, type SyncType } from '@/lib/server/openclaw-sync'
|
|
3
|
+
export const dynamic = 'force-dynamic'
|
|
4
|
+
|
|
5
|
+
const VALID_ACTIONS = new Set(['push', 'pull', 'both'])
|
|
6
|
+
const VALID_TYPES: SyncType[] = ['memory', 'workspace', 'schedules', 'credentials', 'plugins']
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request) {
|
|
9
|
+
try {
|
|
10
|
+
const body = await req.json()
|
|
11
|
+
const action = body.action
|
|
12
|
+
const types = body.types
|
|
13
|
+
|
|
14
|
+
if (!action || !VALID_ACTIONS.has(action)) {
|
|
15
|
+
return NextResponse.json({ error: 'Invalid action. Use push, pull, or both.' }, { status: 400 })
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(types) || types.length === 0) {
|
|
18
|
+
return NextResponse.json({ error: 'types must be a non-empty array.' }, { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
const validTypes = types.filter((t: string) => VALID_TYPES.includes(t as SyncType)) as SyncType[]
|
|
21
|
+
if (validTypes.length === 0) {
|
|
22
|
+
return NextResponse.json({ error: `No valid types. Use: ${VALID_TYPES.join(', ')}` }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const results = await runSync({ action, types: validTypes })
|
|
26
|
+
return NextResponse.json({ ok: true, results })
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
return NextResponse.json({ error: err.message || 'Sync failed' }, { status: 500 })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
export const dynamic = 'force-dynamic'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export async function GET(_req: Request) {
|
|
6
|
+
return NextResponse.json({
|
|
7
|
+
nodes: [
|
|
8
|
+
{ id: 'agent', description: 'LLM reasoning node — calls the model with tools bound' },
|
|
9
|
+
{ id: 'tools', description: 'Tool execution node — runs tool calls from the agent' },
|
|
10
|
+
{ id: 'router', description: 'Routing node — inspects tool results, handles fallback on delegate failures' },
|
|
11
|
+
],
|
|
12
|
+
edges: [
|
|
13
|
+
{ from: '__start__', to: 'agent', type: 'direct' },
|
|
14
|
+
{ from: 'agent', to: 'tools', type: 'conditional', condition: 'has_tool_calls' },
|
|
15
|
+
{ from: 'agent', to: '__end__', type: 'conditional', condition: 'no_tool_calls' },
|
|
16
|
+
{ from: 'tools', to: 'router', type: 'direct' },
|
|
17
|
+
{ from: 'router', to: 'agent', type: 'conditional', condition: 'fallback_or_continue' },
|
|
18
|
+
],
|
|
19
|
+
features: {
|
|
20
|
+
checkpointing: true,
|
|
21
|
+
interruptBefore: 'tools (when capability policy is strict)',
|
|
22
|
+
fallbackRouting: 'Max 2 attempts when delegate_to_agent fails',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|