@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -2,17 +2,19 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { getSessionRunState } from '@/lib/server/session-run-manager'
|
|
5
|
-
import {
|
|
5
|
+
import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
|
|
6
|
+
import { appendSessionNote } from '@/lib/server/session-note'
|
|
7
|
+
import type { Message, Session } from '@/types'
|
|
6
8
|
|
|
7
9
|
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
10
|
const { id } = await params
|
|
9
|
-
const session = loadStoredItem('sessions', id)
|
|
11
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
10
12
|
if (!session) return notFound()
|
|
11
13
|
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
12
14
|
|
|
13
15
|
const run = getSessionRunState(id)
|
|
14
16
|
const hasLiveRun = active.has(id) || !!run.runningRunId
|
|
15
|
-
if (!hasLiveRun &&
|
|
17
|
+
if (!hasLiveRun && materializeStreamingAssistantArtifacts(session.messages)) {
|
|
16
18
|
upsertStoredItem('sessions', id, session)
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -46,27 +48,49 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
46
48
|
|
|
47
49
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
48
50
|
const { id } = await params
|
|
49
|
-
const body = await req.json() as {
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
const body = await req.json() as {
|
|
52
|
+
kind?: string
|
|
53
|
+
role?: Message['role']
|
|
54
|
+
text?: string
|
|
55
|
+
messageKind?: Message['kind']
|
|
52
56
|
}
|
|
53
|
-
const session = loadStoredItem('sessions', id)
|
|
54
|
-
if (!session) return notFound()
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
if (body.kind === 'context-clear') {
|
|
59
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
60
|
+
if (!session) return notFound()
|
|
61
|
+
|
|
62
|
+
session.messages.push({
|
|
63
|
+
role: 'user',
|
|
64
|
+
text: '',
|
|
65
|
+
kind: 'context-clear',
|
|
66
|
+
time: Date.now(),
|
|
67
|
+
})
|
|
68
|
+
upsertStoredItem('sessions', id, session)
|
|
69
|
+
return NextResponse.json({ ok: true })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (body.kind === 'note') {
|
|
73
|
+
const inserted = appendSessionNote({
|
|
74
|
+
sessionId: id,
|
|
75
|
+
text: body.text || '',
|
|
76
|
+
role: body.role || 'assistant',
|
|
77
|
+
kind: body.messageKind || 'system',
|
|
78
|
+
})
|
|
79
|
+
if (!inserted) {
|
|
80
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
81
|
+
if (!session) return notFound()
|
|
82
|
+
return NextResponse.json({ error: 'Note text is required' }, { status: 400 })
|
|
83
|
+
}
|
|
84
|
+
return NextResponse.json(inserted)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return NextResponse.json({ error: 'Only context-clear and note kinds are supported' }, { status: 400 })
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
67
91
|
const { id } = await params
|
|
68
92
|
const body = await req.json() as { messageIndex: number; bookmarked: boolean }
|
|
69
|
-
const session = loadStoredItem('sessions', id)
|
|
93
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
70
94
|
if (!session) return notFound()
|
|
71
95
|
|
|
72
96
|
const { messageIndex, bookmarked } = body
|
|
@@ -82,7 +106,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
82
106
|
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
83
107
|
const { id } = await params
|
|
84
108
|
const body = await req.json() as { messageIndex: number }
|
|
85
|
-
const session = loadStoredItem('sessions', id)
|
|
109
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
86
110
|
if (!session) return notFound()
|
|
87
111
|
|
|
88
112
|
const { messageIndex } = body
|
|
@@ -111,7 +111,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
111
111
|
const sessions = loadSessions()
|
|
112
112
|
if (!sessions[id]) return notFound()
|
|
113
113
|
if (active.has(id)) {
|
|
114
|
-
try { active.get(id)
|
|
114
|
+
try { active.get(id)?.kill() } catch {}
|
|
115
115
|
active.delete(id)
|
|
116
116
|
}
|
|
117
117
|
deleteSession(id)
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
|
|
3
3
|
import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
|
|
4
4
|
import { cancelSessionRuns } from '@/lib/server/session-run-manager'
|
|
5
|
+
import type { Session } from '@/types'
|
|
5
6
|
|
|
6
7
|
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
8
|
const { id } = await params
|
|
8
9
|
const cancel = cancelSessionRuns(id, 'Stopped by user')
|
|
9
|
-
const session = loadStoredItem('sessions', id)
|
|
10
|
-
if (session && Array.isArray(session.messages) &&
|
|
10
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
11
|
+
if (session && Array.isArray(session.messages) && materializeStreamingAssistantArtifacts(session.messages)) {
|
|
11
12
|
upsertStoredItem('sessions', id, session)
|
|
12
13
|
}
|
|
13
14
|
if (active.has(id)) {
|
|
14
|
-
try { active.get(id)
|
|
15
|
+
try { active.get(id)?.kill() } catch {}
|
|
15
16
|
active.delete(id)
|
|
16
17
|
}
|
|
17
18
|
return NextResponse.json({ ok: true, ...cancel })
|
|
@@ -2,22 +2,37 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
|
|
5
|
+
import { loadSessions, saveSessions, deleteSession, active, loadAgents, upsertStoredItem } from '@/lib/server/storage'
|
|
6
6
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
7
7
|
import { notify } from '@/lib/server/ws-hub'
|
|
8
8
|
import { getSessionRunState } from '@/lib/server/session-run-manager'
|
|
9
9
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
10
10
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
|
|
11
|
+
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
|
|
12
|
+
import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
|
|
13
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
11
14
|
export const dynamic = 'force-dynamic'
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
export async function GET(req: Request) {
|
|
18
|
+
ensureDaemonStarted('api/chats:get')
|
|
15
19
|
const sessions = loadSessions()
|
|
20
|
+
const changedSessionIds: string[] = []
|
|
16
21
|
for (const id of Object.keys(sessions)) {
|
|
17
22
|
const run = getSessionRunState(id)
|
|
18
23
|
sessions[id].active = active.has(id) || !!run.runningRunId
|
|
19
24
|
sessions[id].queuedCount = run.queueLength
|
|
20
25
|
sessions[id].currentRunId = run.runningRunId || null
|
|
26
|
+
if (!sessions[id].active && Array.isArray(sessions[id].messages)) {
|
|
27
|
+
if (materializeStreamingAssistantArtifacts(sessions[id].messages)) changedSessionIds.push(id)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const id of changedSessionIds) {
|
|
31
|
+
const persisted = { ...sessions[id] } as Record<string, unknown>
|
|
32
|
+
delete persisted.active
|
|
33
|
+
delete persisted.queuedCount
|
|
34
|
+
delete persisted.currentRunId
|
|
35
|
+
upsertStoredItem('sessions', id, persisted)
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
const { searchParams } = new URL(req.url)
|
|
@@ -32,6 +47,7 @@ export async function GET(req: Request) {
|
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
export async function DELETE(req: Request) {
|
|
50
|
+
ensureDaemonStarted('api/chats:delete')
|
|
35
51
|
const { ids } = await req.json().catch(() => ({ ids: [] })) as { ids: string[] }
|
|
36
52
|
if (!Array.isArray(ids) || !ids.length) {
|
|
37
53
|
return new NextResponse('Missing ids', { status: 400 })
|
|
@@ -41,7 +57,7 @@ export async function DELETE(req: Request) {
|
|
|
41
57
|
for (const id of ids) {
|
|
42
58
|
if (!sessions[id]) continue
|
|
43
59
|
if (active.has(id)) {
|
|
44
|
-
try { active.get(id)
|
|
60
|
+
try { active.get(id)?.kill() } catch {}
|
|
45
61
|
active.delete(id)
|
|
46
62
|
}
|
|
47
63
|
deleteSession(id)
|
|
@@ -52,6 +68,7 @@ export async function DELETE(req: Request) {
|
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
export async function POST(req: Request) {
|
|
71
|
+
ensureDaemonStarted('api/chats:post')
|
|
55
72
|
const body = await req.json().catch(() => ({}))
|
|
56
73
|
let cwd = (body.cwd || '').trim()
|
|
57
74
|
if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
|
|
@@ -61,6 +78,9 @@ export async function POST(req: Request) {
|
|
|
61
78
|
const id = body.id || genId()
|
|
62
79
|
const sessions = loadSessions()
|
|
63
80
|
const agent = body.agentId ? loadAgents()[body.agentId] : null
|
|
81
|
+
if (isAgentDisabled(agent)) {
|
|
82
|
+
return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'start chats') }, { status: 409 })
|
|
83
|
+
}
|
|
64
84
|
const routePreferredGatewayTags = Array.isArray(body.routePreferredGatewayTags)
|
|
65
85
|
? body.routePreferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
66
86
|
: []
|
|
@@ -101,6 +121,7 @@ export async function POST(req: Request) {
|
|
|
101
121
|
claudeCode: null,
|
|
102
122
|
codex: null,
|
|
103
123
|
opencode: null,
|
|
124
|
+
gemini: null,
|
|
104
125
|
},
|
|
105
126
|
messages: Array.isArray(body.messages) ? body.messages : [],
|
|
106
127
|
createdAt: Date.now(), lastActiveAt: Date.now(),
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadSkills, saveSkills } from '@/lib/server/storage'
|
|
4
4
|
import { fetchSkillContent } from '@/lib/server/clawhub-client'
|
|
5
|
+
import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
|
|
5
6
|
|
|
6
7
|
export async function POST(req: Request) {
|
|
7
8
|
const body = await req.json()
|
|
@@ -19,18 +20,37 @@ export async function POST(req: Request) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
const normalized = normalizeSkillPayload({
|
|
24
|
+
name,
|
|
25
|
+
description,
|
|
26
|
+
content,
|
|
27
|
+
sourceUrl: url,
|
|
28
|
+
author,
|
|
29
|
+
tags,
|
|
30
|
+
})
|
|
31
|
+
|
|
22
32
|
const skills = loadSkills()
|
|
23
33
|
const id = genId()
|
|
24
34
|
skills[id] = {
|
|
25
35
|
id,
|
|
26
|
-
name,
|
|
27
|
-
filename: `skill-${id}.md`,
|
|
28
|
-
content,
|
|
29
|
-
description: description || '',
|
|
30
|
-
sourceFormat:
|
|
31
|
-
sourceUrl:
|
|
32
|
-
author: author || '',
|
|
33
|
-
tags: tags || [],
|
|
36
|
+
name: normalized.name,
|
|
37
|
+
filename: normalized.filename || `skill-${id}.md`,
|
|
38
|
+
content: normalized.content,
|
|
39
|
+
description: normalized.description || '',
|
|
40
|
+
sourceFormat: normalized.sourceFormat,
|
|
41
|
+
sourceUrl: normalized.sourceUrl,
|
|
42
|
+
author: normalized.author || '',
|
|
43
|
+
tags: normalized.tags || [],
|
|
44
|
+
version: normalized.version,
|
|
45
|
+
homepage: normalized.homepage,
|
|
46
|
+
primaryEnv: normalized.primaryEnv,
|
|
47
|
+
skillKey: normalized.skillKey,
|
|
48
|
+
always: normalized.always,
|
|
49
|
+
installOptions: normalized.installOptions,
|
|
50
|
+
skillRequirements: normalized.skillRequirements,
|
|
51
|
+
detectedEnvVars: normalized.detectedEnvVars,
|
|
52
|
+
security: normalized.security,
|
|
53
|
+
frontmatter: normalized.frontmatter,
|
|
34
54
|
createdAt: Date.now(),
|
|
35
55
|
updatedAt: Date.now(),
|
|
36
56
|
}
|
|
@@ -2,8 +2,10 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadConnectors, saveConnectors, logActivity } from '@/lib/server/storage'
|
|
3
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
5
6
|
|
|
6
7
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
ensureDaemonStarted('api/connectors/[id]:get')
|
|
7
9
|
const { id } = await params
|
|
8
10
|
const connectors = loadConnectors()
|
|
9
11
|
const connector = connectors[id]
|
|
@@ -11,8 +13,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
11
13
|
|
|
12
14
|
// Merge runtime status, QR code, and presence
|
|
13
15
|
try {
|
|
14
|
-
const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence } = await import('@/lib/server/connectors/manager')
|
|
15
|
-
|
|
16
|
+
const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
17
|
+
const runtimeStatus = getConnectorStatus(id)
|
|
18
|
+
connector.status = runtimeStatus === 'running'
|
|
19
|
+
? 'running'
|
|
20
|
+
: connector.lastError
|
|
21
|
+
? 'error'
|
|
22
|
+
: 'stopped'
|
|
23
|
+
const rState = getReconnectState(id)
|
|
24
|
+
if (rState) {
|
|
25
|
+
const ext = connector as unknown as Record<string, unknown>
|
|
26
|
+
ext.reconnectAttempts = rState.attempts
|
|
27
|
+
ext.nextRetryAt = rState.nextRetryAt
|
|
28
|
+
ext.reconnectError = rState.error
|
|
29
|
+
ext.reconnectExhausted = rState.exhausted
|
|
30
|
+
}
|
|
16
31
|
const qr = getConnectorQR(id)
|
|
17
32
|
if (qr) connector.qrDataUrl = qr
|
|
18
33
|
connector.authenticated = isConnectorAuthenticated(id)
|
|
@@ -26,6 +41,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
44
|
+
ensureDaemonStarted('api/connectors/[id]:put')
|
|
29
45
|
const { id } = await params
|
|
30
46
|
const body = await req.json()
|
|
31
47
|
const connectors = loadConnectors()
|
|
@@ -38,12 +54,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
38
54
|
try {
|
|
39
55
|
const manager = await import('@/lib/server/connectors/manager')
|
|
40
56
|
if (body.action === 'start') {
|
|
57
|
+
manager.clearReconnectState(id)
|
|
41
58
|
await manager.startConnector(id)
|
|
42
59
|
logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector started: "${connector.name}"` })
|
|
43
60
|
} else if (body.action === 'stop') {
|
|
44
61
|
await manager.stopConnector(id)
|
|
45
62
|
logActivity({ entityType: 'connector', entityId: id, action: 'stopped', actor: 'user', summary: `Connector stopped: "${connector.name}"` })
|
|
46
63
|
} else {
|
|
64
|
+
manager.clearReconnectState(id)
|
|
47
65
|
await manager.repairConnector(id)
|
|
48
66
|
logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector repaired: "${connector.name}"` })
|
|
49
67
|
}
|
|
@@ -69,8 +87,33 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
69
87
|
|
|
70
88
|
connectors[id] = connector
|
|
71
89
|
saveConnectors(connectors)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const manager = await import('@/lib/server/connectors/manager')
|
|
93
|
+
const wasRunning = manager.getConnectorStatus(id) === 'running'
|
|
94
|
+
const shouldStop = body.isEnabled === false
|
|
95
|
+
const shouldReload = wasRunning && (
|
|
96
|
+
body.name !== undefined
|
|
97
|
+
|| body.agentId !== undefined
|
|
98
|
+
|| body.chatroomId !== undefined
|
|
99
|
+
|| body.credentialId !== undefined
|
|
100
|
+
|| body.config !== undefined
|
|
101
|
+
|| body.isEnabled !== undefined
|
|
102
|
+
)
|
|
103
|
+
const shouldStart = body.isEnabled === true && !wasRunning
|
|
104
|
+
|
|
105
|
+
if (shouldStop) {
|
|
106
|
+
await manager.stopConnector(id)
|
|
107
|
+
} else if (shouldReload || shouldStart) {
|
|
108
|
+
manager.clearReconnectState(id)
|
|
109
|
+
await manager.startConnector(id)
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Keep the saved connector update even if the runtime reload fails.
|
|
113
|
+
}
|
|
114
|
+
|
|
72
115
|
notify('connectors')
|
|
73
|
-
return NextResponse.json(connector)
|
|
116
|
+
return NextResponse.json(loadConnectors()[id] || connector)
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -2,19 +2,26 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadConnectors, saveConnectors } from '@/lib/server/storage'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
5
6
|
import { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
6
7
|
import { z } from 'zod'
|
|
7
8
|
import type { Connector } from '@/types'
|
|
8
9
|
export const dynamic = 'force-dynamic'
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
export async function GET(
|
|
12
|
+
export async function GET() {
|
|
13
|
+
ensureDaemonStarted('api/connectors:get')
|
|
12
14
|
const connectors = loadConnectors()
|
|
13
15
|
// Merge runtime status from manager
|
|
14
16
|
try {
|
|
15
17
|
const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
16
18
|
for (const c of Object.values(connectors) as Connector[]) {
|
|
17
|
-
|
|
19
|
+
const runtimeStatus = getConnectorStatus(c.id)
|
|
20
|
+
c.status = runtimeStatus === 'running'
|
|
21
|
+
? 'running'
|
|
22
|
+
: c.lastError
|
|
23
|
+
? 'error'
|
|
24
|
+
: 'stopped'
|
|
18
25
|
if (c.platform === 'whatsapp') {
|
|
19
26
|
c.authenticated = isConnectorAuthenticated(c.id)
|
|
20
27
|
c.hasCredentials = hasConnectorCredentials(c.id)
|
|
@@ -28,6 +35,7 @@ export async function GET(_req: Request) {
|
|
|
28
35
|
ext.reconnectAttempts = rState.attempts
|
|
29
36
|
ext.nextRetryAt = rState.nextRetryAt
|
|
30
37
|
ext.reconnectError = rState.error
|
|
38
|
+
ext.reconnectExhausted = rState.exhausted
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
} catch { /* manager not loaded yet */ }
|
|
@@ -35,6 +43,7 @@ export async function GET(_req: Request) {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export async function POST(req: Request) {
|
|
46
|
+
ensureDaemonStarted('api/connectors:post')
|
|
38
47
|
const raw = await req.json()
|
|
39
48
|
const parsed = ConnectorCreateSchema.safeParse(raw)
|
|
40
49
|
if (!parsed.success) {
|
|
@@ -72,13 +81,8 @@ export async function POST(req: Request) {
|
|
|
72
81
|
try {
|
|
73
82
|
const { startConnector } = await import('@/lib/server/connectors/manager')
|
|
74
83
|
await startConnector(id)
|
|
75
|
-
connector.isEnabled = true
|
|
76
|
-
connector.status = 'running'
|
|
77
|
-
connectors[id] = connector
|
|
78
|
-
saveConnectors(connectors)
|
|
79
|
-
notify('connectors')
|
|
80
84
|
} catch { /* auto-start is best-effort */ }
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
return NextResponse.json(connector)
|
|
87
|
+
return NextResponse.json(loadConnectors()[id] || connector)
|
|
84
88
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { afterEach } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { GET as listExternalAgents, POST as registerExternalAgent } from './route'
|
|
5
|
+
import { POST as heartbeatExternalAgent } from './[id]/heartbeat/route'
|
|
6
|
+
import { PUT as mutateExternalAgent, DELETE as deleteExternalAgent } from './[id]/route'
|
|
7
|
+
import {
|
|
8
|
+
loadExternalAgents,
|
|
9
|
+
loadGatewayProfiles,
|
|
10
|
+
saveExternalAgents,
|
|
11
|
+
saveGatewayProfiles,
|
|
12
|
+
} from '@/lib/server/storage'
|
|
13
|
+
|
|
14
|
+
const originalExternalAgents = loadExternalAgents()
|
|
15
|
+
const originalGatewayProfiles = loadGatewayProfiles()
|
|
16
|
+
|
|
17
|
+
function routeParams(id: string) {
|
|
18
|
+
return { params: Promise.resolve({ id }) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
saveExternalAgents(originalExternalAgents)
|
|
23
|
+
saveGatewayProfiles(originalGatewayProfiles)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('external agent register + heartbeat derives gateway metadata in listing', async () => {
|
|
27
|
+
const gateways = loadGatewayProfiles()
|
|
28
|
+
gateways['gateway-ext-test'] = {
|
|
29
|
+
id: 'gateway-ext-test',
|
|
30
|
+
name: 'Gateway Test',
|
|
31
|
+
provider: 'openclaw',
|
|
32
|
+
endpoint: 'http://127.0.0.1:19999/v1',
|
|
33
|
+
wsUrl: 'ws://127.0.0.1:19999',
|
|
34
|
+
credentialId: null,
|
|
35
|
+
status: 'healthy',
|
|
36
|
+
notes: null,
|
|
37
|
+
tags: ['lan-remote', 'smoke'],
|
|
38
|
+
lastError: null,
|
|
39
|
+
lastCheckedAt: null,
|
|
40
|
+
lastModelCount: 1,
|
|
41
|
+
discoveredHost: '127.0.0.1',
|
|
42
|
+
discoveredPort: 19999,
|
|
43
|
+
deployment: {
|
|
44
|
+
method: 'imported',
|
|
45
|
+
managedBy: 'external',
|
|
46
|
+
useCase: 'single-vps',
|
|
47
|
+
exposure: 'private-lan',
|
|
48
|
+
targetHost: '127.0.0.1',
|
|
49
|
+
},
|
|
50
|
+
stats: null,
|
|
51
|
+
isDefault: false,
|
|
52
|
+
createdAt: Date.now(),
|
|
53
|
+
updatedAt: Date.now(),
|
|
54
|
+
}
|
|
55
|
+
saveGatewayProfiles(gateways)
|
|
56
|
+
|
|
57
|
+
const registerResponse = await registerExternalAgent(new Request('http://local/api/external-agents/register', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'content-type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
id: 'runtime-ext-test',
|
|
62
|
+
name: 'External Runtime',
|
|
63
|
+
sourceType: 'openclaw',
|
|
64
|
+
transport: 'gateway',
|
|
65
|
+
endpoint: 'http://127.0.0.1:19999/v1',
|
|
66
|
+
agentId: 'agent-ext-test',
|
|
67
|
+
gatewayProfileId: 'gateway-ext-test',
|
|
68
|
+
}),
|
|
69
|
+
}))
|
|
70
|
+
assert.equal(registerResponse.status, 200)
|
|
71
|
+
|
|
72
|
+
const heartbeatResponse = await heartbeatExternalAgent(new Request('http://local/api/external-agents/runtime-ext-test/heartbeat', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'content-type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
status: 'online',
|
|
77
|
+
lastHealthNote: 'Heartbeat OK',
|
|
78
|
+
version: '1.2.3',
|
|
79
|
+
tokenStats: { inputTokens: 5, outputTokens: 7, totalTokens: 12 },
|
|
80
|
+
}),
|
|
81
|
+
}), routeParams('runtime-ext-test'))
|
|
82
|
+
assert.equal(heartbeatResponse.status, 200)
|
|
83
|
+
|
|
84
|
+
const listResponse = await listExternalAgents()
|
|
85
|
+
assert.equal(listResponse.status, 200)
|
|
86
|
+
const listPayload = await listResponse.json() as Array<Record<string, unknown>>
|
|
87
|
+
const runtime = listPayload.find((item) => item.id === 'runtime-ext-test')
|
|
88
|
+
|
|
89
|
+
assert.ok(runtime)
|
|
90
|
+
assert.equal(runtime?.status, 'online')
|
|
91
|
+
assert.equal(runtime?.gatewayUseCase, 'single-vps')
|
|
92
|
+
assert.deepEqual(runtime?.gatewayTags, ['lan-remote', 'smoke'])
|
|
93
|
+
assert.equal(runtime?.lastHealthNote, 'Heartbeat OK')
|
|
94
|
+
assert.equal((runtime?.tokenStats as { totalTokens?: number } | undefined)?.totalTokens, 12)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('external agent lifecycle actions update state and delete removes the runtime', async () => {
|
|
98
|
+
const items = loadExternalAgents()
|
|
99
|
+
items['runtime-lifecycle-test'] = {
|
|
100
|
+
id: 'runtime-lifecycle-test',
|
|
101
|
+
name: 'Lifecycle Runtime',
|
|
102
|
+
sourceType: 'openclaw',
|
|
103
|
+
status: 'online',
|
|
104
|
+
provider: 'openclaw',
|
|
105
|
+
model: 'default',
|
|
106
|
+
workspace: null,
|
|
107
|
+
transport: 'gateway',
|
|
108
|
+
endpoint: 'http://127.0.0.1:18888/v1',
|
|
109
|
+
agentId: 'agent-lifecycle-test',
|
|
110
|
+
gatewayProfileId: null,
|
|
111
|
+
capabilities: [],
|
|
112
|
+
labels: [],
|
|
113
|
+
lifecycleState: 'active',
|
|
114
|
+
gatewayTags: [],
|
|
115
|
+
gatewayUseCase: null,
|
|
116
|
+
version: null,
|
|
117
|
+
lastHealthNote: null,
|
|
118
|
+
metadata: null,
|
|
119
|
+
tokenStats: null,
|
|
120
|
+
lastHeartbeatAt: Date.now(),
|
|
121
|
+
lastSeenAt: Date.now(),
|
|
122
|
+
createdAt: Date.now(),
|
|
123
|
+
updatedAt: Date.now(),
|
|
124
|
+
}
|
|
125
|
+
saveExternalAgents(items)
|
|
126
|
+
|
|
127
|
+
const drainResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
|
|
128
|
+
method: 'PUT',
|
|
129
|
+
headers: { 'content-type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ action: 'drain' }),
|
|
131
|
+
}), routeParams('runtime-lifecycle-test'))
|
|
132
|
+
const drainPayload = await drainResponse.json() as Record<string, unknown>
|
|
133
|
+
assert.equal(drainPayload.lifecycleState, 'draining')
|
|
134
|
+
|
|
135
|
+
const cordonResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
|
|
136
|
+
method: 'PUT',
|
|
137
|
+
headers: { 'content-type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({ action: 'cordon' }),
|
|
139
|
+
}), routeParams('runtime-lifecycle-test'))
|
|
140
|
+
const cordonPayload = await cordonResponse.json() as Record<string, unknown>
|
|
141
|
+
assert.equal(cordonPayload.lifecycleState, 'cordoned')
|
|
142
|
+
|
|
143
|
+
const restartResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
|
|
144
|
+
method: 'PUT',
|
|
145
|
+
headers: { 'content-type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({ action: 'restart' }),
|
|
147
|
+
}), routeParams('runtime-lifecycle-test'))
|
|
148
|
+
const restartPayload = await restartResponse.json() as Record<string, unknown>
|
|
149
|
+
assert.equal((restartPayload.metadata as { controlRequest?: { action?: string } } | undefined)?.controlRequest?.action, 'restart')
|
|
150
|
+
|
|
151
|
+
const activateResponse = await mutateExternalAgent(new Request('http://local/api/external-agents/runtime-lifecycle-test', {
|
|
152
|
+
method: 'PUT',
|
|
153
|
+
headers: { 'content-type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ action: 'activate' }),
|
|
155
|
+
}), routeParams('runtime-lifecycle-test'))
|
|
156
|
+
const activatePayload = await activateResponse.json() as Record<string, unknown>
|
|
157
|
+
assert.equal(activatePayload.lifecycleState, 'active')
|
|
158
|
+
|
|
159
|
+
const deleteResponse = await deleteExternalAgent(
|
|
160
|
+
new Request('http://local/api/external-agents/runtime-lifecycle-test', { method: 'DELETE' }),
|
|
161
|
+
routeParams('runtime-lifecycle-test'),
|
|
162
|
+
)
|
|
163
|
+
assert.equal(deleteResponse.status, 200)
|
|
164
|
+
assert.equal(loadExternalAgents()['runtime-lifecycle-test'], undefined)
|
|
165
|
+
})
|
|
@@ -3,34 +3,49 @@ import { probeOpenClawHealth } from '@/lib/server/openclaw-health'
|
|
|
3
3
|
import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import type { GatewayProfile } from '@/types'
|
|
7
|
+
import type { OpenClawHealthResult } from '@/lib/server/openclaw-health'
|
|
6
8
|
export const dynamic = 'force-dynamic'
|
|
7
9
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
+
export function persistGatewayHealthResult(
|
|
11
|
+
id: string,
|
|
12
|
+
result: OpenClawHealthResult,
|
|
13
|
+
now = Date.now(),
|
|
14
|
+
): GatewayProfile | null {
|
|
10
15
|
const gateways = loadGatewayProfiles()
|
|
11
16
|
const gateway = gateways[id]
|
|
12
|
-
if (!gateway) return
|
|
13
|
-
|
|
14
|
-
const result = await probeOpenClawHealth({
|
|
15
|
-
endpoint: gateway.endpoint,
|
|
16
|
-
credentialId: gateway.credentialId || null,
|
|
17
|
-
})
|
|
17
|
+
if (!gateway) return null
|
|
18
18
|
|
|
19
19
|
gateway.status = result.ok ? 'healthy' : (result.authProvided ? 'degraded' : 'offline')
|
|
20
|
-
gateway.lastCheckedAt =
|
|
20
|
+
gateway.lastCheckedAt = now
|
|
21
21
|
gateway.lastError = result.ok ? null : (result.error || result.hint || 'Gateway health check failed.')
|
|
22
22
|
gateway.lastModelCount = Array.isArray(result.models) ? result.models.length : 0
|
|
23
23
|
gateway.deployment = {
|
|
24
24
|
...(gateway.deployment || {}),
|
|
25
|
-
lastVerifiedAt:
|
|
25
|
+
lastVerifiedAt: now,
|
|
26
26
|
lastVerifiedOk: result.ok,
|
|
27
27
|
lastVerifiedMessage: result.ok
|
|
28
|
-
?
|
|
28
|
+
? result.message
|
|
29
29
|
: (result.error || result.hint || 'Gateway health check failed.'),
|
|
30
30
|
}
|
|
31
|
-
gateway.updatedAt =
|
|
31
|
+
gateway.updatedAt = now
|
|
32
32
|
saveGatewayProfiles(gateways)
|
|
33
33
|
notify('gateways')
|
|
34
|
+
return gateway
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
38
|
+
const { id } = await params
|
|
39
|
+
const gateways = loadGatewayProfiles()
|
|
40
|
+
const gateway = gateways[id]
|
|
41
|
+
if (!gateway) return notFound()
|
|
42
|
+
|
|
43
|
+
const result = await probeOpenClawHealth({
|
|
44
|
+
endpoint: gateway.endpoint,
|
|
45
|
+
credentialId: gateway.credentialId || null,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
persistGatewayHealthResult(id, result)
|
|
34
49
|
|
|
35
50
|
return NextResponse.json(result)
|
|
36
51
|
}
|
|
@@ -35,6 +35,8 @@ function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
|
|
|
35
35
|
useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
|
|
36
36
|
exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
|
|
37
37
|
managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
|
|
38
|
+
localInstanceId: normalizeText(deployment.localInstanceId),
|
|
39
|
+
localPort: normalizeNullableNumber(deployment.localPort),
|
|
38
40
|
targetHost: normalizeText(deployment.targetHost),
|
|
39
41
|
sshHost: normalizeText(deployment.sshHost),
|
|
40
42
|
sshUser: normalizeText(deployment.sshUser),
|