@swarmclawai/swarmclaw 0.7.5 → 0.7.7
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 +41 -10
- package/package.json +2 -2
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +12 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +240 -0
- package/src/cli/index.js +53 -0
- package/src/cli/index.test.js +102 -0
- package/src/cli/spec.js +79 -0
- package/src/components/agents/agent-sheet.tsx +97 -19
- package/src/components/auth/setup-wizard.tsx +111 -54
- package/src/components/gateways/gateway-sheet.tsx +202 -10
- package/src/components/openclaw/openclaw-deploy-panel.tsx +1208 -0
- package/src/components/providers/provider-list.tsx +321 -22
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/chat-execution.ts +8 -2
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/openclaw-deploy.test.ts +75 -0
- package/src/lib/server/openclaw-deploy.ts +1384 -0
- package/src/lib/server/orchestrator.ts +9 -0
- package/src/lib/server/queue.ts +45 -2
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +65 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
|
+
import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import type { ProviderType, Credential, GatewayProfile } from '@/types'
|
|
7
8
|
import {
|
|
@@ -53,6 +54,9 @@ interface ConfiguredProvider {
|
|
|
53
54
|
endpoint: string | null
|
|
54
55
|
defaultModel: string
|
|
55
56
|
gatewayProfileId: string | null
|
|
57
|
+
notes?: string | null
|
|
58
|
+
tags?: string[]
|
|
59
|
+
deployment?: GatewayProfile['deployment'] | null
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
interface StarterDraftAgent {
|
|
@@ -87,6 +91,20 @@ const CONNECTOR_ICONS = [
|
|
|
87
91
|
{ name: 'Telegram', icon: 'T' },
|
|
88
92
|
{ name: 'WhatsApp', icon: 'W' },
|
|
89
93
|
]
|
|
94
|
+
const OPENCLAW_USE_CASE_LABELS: Record<NonNullable<NonNullable<GatewayProfile['deployment']>['useCase']>, string> = {
|
|
95
|
+
'local-dev': 'Local Dev',
|
|
96
|
+
'single-vps': 'Single VPS',
|
|
97
|
+
'private-tailnet': 'Private Tailnet',
|
|
98
|
+
'browser-heavy': 'Browser Heavy',
|
|
99
|
+
'team-control': 'Team Control',
|
|
100
|
+
}
|
|
101
|
+
const OPENCLAW_EXPOSURE_LABELS: Record<NonNullable<NonNullable<GatewayProfile['deployment']>['exposure']>, string> = {
|
|
102
|
+
'private-lan': 'Private LAN',
|
|
103
|
+
tailscale: 'Tailscale',
|
|
104
|
+
caddy: 'Caddy',
|
|
105
|
+
nginx: 'Nginx',
|
|
106
|
+
'ssh-tunnel': 'SSH Tunnel',
|
|
107
|
+
}
|
|
90
108
|
|
|
91
109
|
function stepIndex(step: SetupStep): number {
|
|
92
110
|
if (step === 'connect') return STEP_ORDER.indexOf('providers')
|
|
@@ -142,12 +160,6 @@ function isLocalOpenClawEndpoint(value: string | null | undefined): boolean {
|
|
|
142
160
|
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
|
|
143
161
|
}
|
|
144
162
|
|
|
145
|
-
function resolveOpenClawPort(value: string | null | undefined): number {
|
|
146
|
-
const parsed = parseProviderUrl(value)
|
|
147
|
-
const port = parsed ? Number(parsed.port) : NaN
|
|
148
|
-
return Number.isFinite(port) && port > 0 ? port : 18789
|
|
149
|
-
}
|
|
150
|
-
|
|
151
163
|
function resolveOpenClawDashboardUrl(value: string | null | undefined): string {
|
|
152
164
|
const parsed = parseProviderUrl(value)
|
|
153
165
|
if (!parsed) return 'http://localhost:18789'
|
|
@@ -229,6 +241,12 @@ function ConfiguredProviderChips({ providers }: { providers: ConfiguredProvider[
|
|
|
229
241
|
{cp.provider === 'openclaw' && formatEndpointHost(cp.endpoint)
|
|
230
242
|
? `· ${formatEndpointHost(cp.endpoint)}`
|
|
231
243
|
: ''}
|
|
244
|
+
{cp.provider === 'openclaw' && cp.deployment?.useCase
|
|
245
|
+
? ` · ${OPENCLAW_USE_CASE_LABELS[cp.deployment.useCase]}`
|
|
246
|
+
: ''}
|
|
247
|
+
{cp.provider === 'openclaw' && cp.deployment?.exposure
|
|
248
|
+
? ` · ${OPENCLAW_EXPOSURE_LABELS[cp.deployment.exposure]}`
|
|
249
|
+
: ''}
|
|
232
250
|
{cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
|
|
233
251
|
</span>
|
|
234
252
|
</span>
|
|
@@ -337,12 +355,14 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
337
355
|
const [endpoint, setEndpoint] = useState('')
|
|
338
356
|
const [apiKey, setApiKey] = useState('')
|
|
339
357
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
358
|
+
const [providerNotes, setProviderNotes] = useState('')
|
|
359
|
+
const [providerTags, setProviderTags] = useState<string[]>([])
|
|
360
|
+
const [providerDeployment, setProviderDeployment] = useState<GatewayProfile['deployment'] | null>(null)
|
|
340
361
|
const [checkState, setCheckState] = useState<CheckState>('idle')
|
|
341
362
|
const [checkMessage, setCheckMessage] = useState('')
|
|
342
363
|
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
343
364
|
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
344
365
|
const [providerSuggestedModel, setProviderSuggestedModel] = useState('')
|
|
345
|
-
const [commandCopyState, setCommandCopyState] = useState<'idle' | 'copied' | 'failed'>('idle')
|
|
346
366
|
|
|
347
367
|
const [doctorState, setDoctorState] = useState<'idle' | 'checking' | 'done' | 'error'>('idle')
|
|
348
368
|
const [doctorError, setDoctorError] = useState('')
|
|
@@ -381,9 +401,6 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
381
401
|
? resolveOpenClawDashboardUrl(openClawEndpointValue)
|
|
382
402
|
: null
|
|
383
403
|
const openClawLocal = provider === 'openclaw' ? isLocalOpenClawEndpoint(openClawEndpointValue) : false
|
|
384
|
-
const openClawPort = provider === 'openclaw' ? resolveOpenClawPort(openClawEndpointValue) : 18789
|
|
385
|
-
const openClawLocalCommand = `npx openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
|
|
386
|
-
const openClawLocalCommandPnpm = `pnpm openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
|
|
387
404
|
|
|
388
405
|
const resetProviderForm = () => {
|
|
389
406
|
setProvider(null)
|
|
@@ -391,12 +408,14 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
391
408
|
setEndpoint('')
|
|
392
409
|
setApiKey('')
|
|
393
410
|
setCredentialId(null)
|
|
411
|
+
setProviderNotes('')
|
|
412
|
+
setProviderTags([])
|
|
413
|
+
setProviderDeployment(null)
|
|
394
414
|
setCheckState('idle')
|
|
395
415
|
setCheckMessage('')
|
|
396
416
|
setCheckErrorCode(null)
|
|
397
417
|
setOpenclawDeviceId(null)
|
|
398
418
|
setProviderSuggestedModel('')
|
|
399
|
-
setCommandCopyState('idle')
|
|
400
419
|
setError('')
|
|
401
420
|
}
|
|
402
421
|
|
|
@@ -442,16 +461,57 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
442
461
|
setEndpoint(meta?.defaultEndpoint || '')
|
|
443
462
|
setApiKey('')
|
|
444
463
|
setCredentialId(null)
|
|
464
|
+
setProviderNotes('')
|
|
465
|
+
setProviderTags([])
|
|
466
|
+
setProviderDeployment(null)
|
|
445
467
|
setCheckState('idle')
|
|
446
468
|
setCheckMessage('')
|
|
447
469
|
setCheckErrorCode(null)
|
|
448
470
|
setOpenclawDeviceId(null)
|
|
449
471
|
setProviderSuggestedModel(getDefaultModelForProvider(nextProvider))
|
|
450
|
-
setCommandCopyState('idle')
|
|
451
472
|
setError('')
|
|
452
473
|
setStep('connect')
|
|
453
474
|
}
|
|
454
475
|
|
|
476
|
+
const applyOpenClawDeployPatch = (patch: {
|
|
477
|
+
endpoint?: string
|
|
478
|
+
token?: string
|
|
479
|
+
name?: string
|
|
480
|
+
notes?: string
|
|
481
|
+
deployment?: GatewayProfile['deployment'] | Record<string, unknown> | null
|
|
482
|
+
}) => {
|
|
483
|
+
if (patch.endpoint) {
|
|
484
|
+
setEndpoint(patch.endpoint)
|
|
485
|
+
}
|
|
486
|
+
if (patch.token) {
|
|
487
|
+
setApiKey(patch.token)
|
|
488
|
+
setCredentialId(null)
|
|
489
|
+
}
|
|
490
|
+
if (patch.name && (!providerLabel.trim() || providerLabel.trim() === (selectedProvider?.name || ''))) {
|
|
491
|
+
setProviderLabel(patch.name)
|
|
492
|
+
}
|
|
493
|
+
if (patch.notes) {
|
|
494
|
+
setProviderNotes(patch.notes)
|
|
495
|
+
}
|
|
496
|
+
if (patch.deployment) {
|
|
497
|
+
const nextDeployment = patch.deployment as GatewayProfile['deployment']
|
|
498
|
+
setProviderDeployment((current) => ({
|
|
499
|
+
...(current || {}),
|
|
500
|
+
...(nextDeployment || {}),
|
|
501
|
+
}))
|
|
502
|
+
setProviderTags((current) => Array.from(new Set([
|
|
503
|
+
...current,
|
|
504
|
+
'onboarding',
|
|
505
|
+
...(nextDeployment?.useCase ? [nextDeployment.useCase] : []),
|
|
506
|
+
...(nextDeployment?.exposure ? [nextDeployment.exposure] : []),
|
|
507
|
+
])))
|
|
508
|
+
}
|
|
509
|
+
setCheckState('idle')
|
|
510
|
+
setCheckMessage('')
|
|
511
|
+
setCheckErrorCode(null)
|
|
512
|
+
setError('')
|
|
513
|
+
}
|
|
514
|
+
|
|
455
515
|
const runConnectionCheck = async (): Promise<boolean> => {
|
|
456
516
|
if (!provider || !selectedProvider) return false
|
|
457
517
|
if (requiresKey && !apiKey.trim()) {
|
|
@@ -542,6 +602,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
542
602
|
endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
|
|
543
603
|
defaultModel: providerSuggestedModel || getDefaultModelForProvider(provider),
|
|
544
604
|
gatewayProfileId: null,
|
|
605
|
+
notes: providerNotes.trim() || null,
|
|
606
|
+
tags: providerTags,
|
|
607
|
+
deployment: providerDeployment,
|
|
545
608
|
}
|
|
546
609
|
|
|
547
610
|
const nextConfigured = [...configuredProviders, configuredProvider]
|
|
@@ -603,17 +666,6 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
603
666
|
}))
|
|
604
667
|
}
|
|
605
668
|
|
|
606
|
-
const copyOpenClawLocalCommand = async () => {
|
|
607
|
-
try {
|
|
608
|
-
await navigator.clipboard.writeText(openClawLocalCommand)
|
|
609
|
-
setCommandCopyState('copied')
|
|
610
|
-
window.setTimeout(() => setCommandCopyState('idle'), 1200)
|
|
611
|
-
} catch {
|
|
612
|
-
setCommandCopyState('failed')
|
|
613
|
-
window.setTimeout(() => setCommandCopyState('idle'), 1800)
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
669
|
const createAgentsAndFinish = async () => {
|
|
618
670
|
const enabledDrafts = draftAgents.filter((draft) => draft.enabled)
|
|
619
671
|
if (enabledDrafts.some((draft) => !draft.provider)) {
|
|
@@ -646,8 +698,13 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
646
698
|
name: configuredProvider.name,
|
|
647
699
|
endpoint: normalizedEndpoint,
|
|
648
700
|
credentialId: configuredProvider.credentialId || null,
|
|
649
|
-
tags: [
|
|
650
|
-
|
|
701
|
+
tags: Array.from(new Set([
|
|
702
|
+
'onboarding',
|
|
703
|
+
...(configuredProvider.tags || []),
|
|
704
|
+
])),
|
|
705
|
+
notes: configuredProvider.notes || `Created during setup for ${configuredProvider.name}.`,
|
|
706
|
+
deployment: configuredProvider.deployment || null,
|
|
707
|
+
status: configuredProvider.deployment?.lastVerifiedOk ? 'healthy' : 'pending',
|
|
651
708
|
isDefault: shouldCreateDefault,
|
|
652
709
|
})
|
|
653
710
|
gatewayProfileIdsByProviderConfig.set(configuredProvider.id, createdGateway.id)
|
|
@@ -1063,6 +1120,16 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1063
1120
|
|
|
1064
1121
|
{provider === 'openclaw' && (
|
|
1065
1122
|
<div className="rounded-[14px] border border-white/[0.08] bg-surface p-4 space-y-4">
|
|
1123
|
+
<OpenClawDeployPanel
|
|
1124
|
+
compact
|
|
1125
|
+
endpoint={openClawEndpointValue}
|
|
1126
|
+
token={apiKey}
|
|
1127
|
+
suggestedName={providerLabel || selectedProvider.name}
|
|
1128
|
+
title="Smart Deploy OpenClaw"
|
|
1129
|
+
description="Launch the bundled official OpenClaw gateway locally, or generate an official-image VPS bundle for major providers without relying on third-party deployment services."
|
|
1130
|
+
onApply={applyOpenClawDeployPatch}
|
|
1131
|
+
/>
|
|
1132
|
+
|
|
1066
1133
|
<div className="grid gap-3 md:grid-cols-2">
|
|
1067
1134
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1068
1135
|
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Remote gateway</div>
|
|
@@ -1075,39 +1142,20 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1075
1142
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1076
1143
|
If you only have a WebSocket gateway URL, you can still paste it here. SwarmClaw will normalize it for agent chat.
|
|
1077
1144
|
</p>
|
|
1145
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1146
|
+
Safer remote defaults: use <code className="text-text-2">private-tailnet</code> with <code className="text-text-2">tailscale</code> or <code className="text-text-2">ssh-tunnel</code> unless you intentionally want public HTTPS ingress.
|
|
1147
|
+
</p>
|
|
1078
1148
|
</div>
|
|
1079
1149
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1080
|
-
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">
|
|
1150
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Safe defaults</div>
|
|
1081
1151
|
<p className="text-[13px] text-text-2 leading-relaxed">
|
|
1082
|
-
|
|
1152
|
+
Smart Deploy generates a gateway token for you, defaults to the standard OpenClaw ports, and prefills this setup form automatically.
|
|
1083
1153
|
</p>
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
<div className="mt-2 flex items-center gap-2">
|
|
1090
|
-
<button
|
|
1091
|
-
type="button"
|
|
1092
|
-
onClick={copyOpenClawLocalCommand}
|
|
1093
|
-
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
1094
|
-
>
|
|
1095
|
-
{commandCopyState === 'copied'
|
|
1096
|
-
? 'Copied'
|
|
1097
|
-
: commandCopyState === 'failed'
|
|
1098
|
-
? 'Copy failed'
|
|
1099
|
-
: 'Copy command'}
|
|
1100
|
-
</button>
|
|
1101
|
-
<button
|
|
1102
|
-
type="button"
|
|
1103
|
-
onClick={() => { setEndpoint(selectedProvider.defaultEndpoint || 'http://localhost:18789/v1'); setCheckState('idle'); setCheckMessage(''); setCheckErrorCode(null) }}
|
|
1104
|
-
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
1105
|
-
>
|
|
1106
|
-
Use local default
|
|
1107
|
-
</button>
|
|
1108
|
-
</div>
|
|
1109
|
-
<p className="mt-2 text-[11px] text-text-3">
|
|
1110
|
-
In a source checkout, use <code className="text-text-2">{openClawLocalCommandPnpm}</code>.
|
|
1154
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1155
|
+
Local quickstart uses the bundled official OpenClaw CLI. Remote quickstart uses the official OpenClaw Docker image or the official repo for managed hosts.
|
|
1156
|
+
</p>
|
|
1157
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1158
|
+
Choose <code className="text-text-2">local-dev</code> for one-machine setup, <code className="text-text-2">single-vps</code> for most hosted installs, or <code className="text-text-2">private-tailnet</code> when the gateway should stay private.
|
|
1111
1159
|
</p>
|
|
1112
1160
|
</div>
|
|
1113
1161
|
</div>
|
|
@@ -1120,6 +1168,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1120
1168
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1121
1169
|
Current target: <span className="text-text-2">{openClawEndpointHost || 'localhost:18789'}</span>{openClawLocal ? ' · local route' : ' · remote route'}
|
|
1122
1170
|
</p>
|
|
1171
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1172
|
+
Use <code className="text-text-2">caddy</code> or <code className="text-text-2">nginx</code> only when you intentionally want HTTPS/public ingress managed on the gateway side.
|
|
1173
|
+
</p>
|
|
1123
1174
|
</div>
|
|
1124
1175
|
</div>
|
|
1125
1176
|
)}
|
|
@@ -1330,6 +1381,12 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1330
1381
|
{configuredProvider.provider === 'openclaw' && formatEndpointHost(configuredProvider.endpoint)
|
|
1331
1382
|
? ` · ${formatEndpointHost(configuredProvider.endpoint)}`
|
|
1332
1383
|
: ''}
|
|
1384
|
+
{configuredProvider.provider === 'openclaw' && configuredProvider.deployment?.useCase
|
|
1385
|
+
? ` · ${OPENCLAW_USE_CASE_LABELS[configuredProvider.deployment.useCase]}`
|
|
1386
|
+
: ''}
|
|
1387
|
+
{configuredProvider.provider === 'openclaw' && configuredProvider.deployment?.exposure
|
|
1388
|
+
? ` · ${OPENCLAW_EXPOSURE_LABELS[configuredProvider.deployment.exposure]}`
|
|
1389
|
+
: ''}
|
|
1333
1390
|
{configuredProvider.defaultModel ? ` · ${configuredProvider.defaultModel}` : ''}
|
|
1334
1391
|
</option>
|
|
1335
1392
|
))}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
5
|
+
import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import { api } from '@/lib/api-client'
|
|
7
8
|
import { toast } from 'sonner'
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
Credential,
|
|
11
|
+
OpenClawDevicePairRequest,
|
|
12
|
+
OpenClawNode,
|
|
13
|
+
OpenClawNodePairRequest,
|
|
14
|
+
OpenClawPairedDevice,
|
|
15
|
+
GatewayProfile,
|
|
16
|
+
} from '@/types'
|
|
9
17
|
|
|
10
18
|
interface DiscoveryResult {
|
|
11
19
|
host: string
|
|
@@ -30,6 +38,17 @@ interface PairingListResult<T> {
|
|
|
30
38
|
paired?: OpenClawPairedDevice[]
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
interface GatewayImportShape {
|
|
42
|
+
name?: string
|
|
43
|
+
endpoint?: string
|
|
44
|
+
credentialId?: string | null
|
|
45
|
+
token?: string | null
|
|
46
|
+
notes?: string | null
|
|
47
|
+
tags?: string[]
|
|
48
|
+
isDefault?: boolean
|
|
49
|
+
deployment?: GatewayProfile['deployment']
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
export function GatewaySheet() {
|
|
34
53
|
const open = useAppStore((s) => s.gatewaySheetOpen)
|
|
35
54
|
const setOpen = useAppStore((s) => s.setGatewaySheetOpen)
|
|
@@ -46,6 +65,7 @@ export function GatewaySheet() {
|
|
|
46
65
|
const [name, setName] = useState('')
|
|
47
66
|
const [endpoint, setEndpoint] = useState('http://localhost:18789')
|
|
48
67
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
68
|
+
const [tokenDraft, setTokenDraft] = useState('')
|
|
49
69
|
const [notes, setNotes] = useState('')
|
|
50
70
|
const [tags, setTags] = useState('')
|
|
51
71
|
const [isDefault, setIsDefault] = useState(false)
|
|
@@ -65,6 +85,8 @@ export function GatewaySheet() {
|
|
|
65
85
|
const [invokeParamsText, setInvokeParamsText] = useState('{}')
|
|
66
86
|
const [invokeResult, setInvokeResult] = useState('')
|
|
67
87
|
const [invoking, setInvoking] = useState(false)
|
|
88
|
+
const [deployment, setDeployment] = useState<GatewayProfile['deployment'] | null>(null)
|
|
89
|
+
const importFileRef = useRef<HTMLInputElement>(null)
|
|
68
90
|
|
|
69
91
|
useEffect(() => {
|
|
70
92
|
if (!open) return
|
|
@@ -81,17 +103,21 @@ export function GatewaySheet() {
|
|
|
81
103
|
setName(editing.name)
|
|
82
104
|
setEndpoint(editing.endpoint)
|
|
83
105
|
setCredentialId(editing.credentialId || null)
|
|
106
|
+
setTokenDraft('')
|
|
84
107
|
setNotes(editing.notes || '')
|
|
85
108
|
setTags((editing.tags || []).join(', '))
|
|
86
109
|
setIsDefault(editing.isDefault === true)
|
|
110
|
+
setDeployment(editing.deployment || null)
|
|
87
111
|
return
|
|
88
112
|
}
|
|
89
113
|
setName('')
|
|
90
114
|
setEndpoint('http://localhost:18789')
|
|
91
115
|
setCredentialId(null)
|
|
116
|
+
setTokenDraft('')
|
|
92
117
|
setNotes('')
|
|
93
118
|
setTags('')
|
|
94
119
|
setIsDefault(gatewayProfiles.length === 0)
|
|
120
|
+
setDeployment(null)
|
|
95
121
|
setNodes([])
|
|
96
122
|
setNodePairings([])
|
|
97
123
|
setDevicePairings([])
|
|
@@ -133,10 +159,18 @@ export function GatewaySheet() {
|
|
|
133
159
|
setNodePairings(nextNodePairings)
|
|
134
160
|
setDevicePairings(nextDevicePairings)
|
|
135
161
|
setPairedDevices(nextPairedDevices)
|
|
162
|
+
const nextStats: NonNullable<GatewayProfile['stats']> = {
|
|
163
|
+
nodeCount: nextNodes.length,
|
|
164
|
+
connectedNodeCount: nextNodes.filter((node) => node.connected).length,
|
|
165
|
+
pendingNodePairings: nextNodePairings.length,
|
|
166
|
+
pairedDeviceCount: nextPairedDevices.length,
|
|
167
|
+
pendingDevicePairings: nextDevicePairings.length,
|
|
168
|
+
}
|
|
136
169
|
if (nextNodes[0]) {
|
|
137
170
|
setInvokeNodeId((current) => current || nextNodes[0].nodeId)
|
|
138
171
|
setInvokeCommand((current) => current || nextNodes[0].commands?.[0] || '')
|
|
139
172
|
}
|
|
173
|
+
void api('PUT', `/gateways/${profileId}`, { stats: nextStats }).catch(() => {})
|
|
140
174
|
} catch (err: unknown) {
|
|
141
175
|
setNodesError(err instanceof Error ? err.message : 'Failed to load nodes for this gateway.')
|
|
142
176
|
} finally {
|
|
@@ -157,12 +191,22 @@ export function GatewaySheet() {
|
|
|
157
191
|
const handleSave = async () => {
|
|
158
192
|
setSaving(true)
|
|
159
193
|
try {
|
|
194
|
+
let nextCredentialId = credentialId
|
|
195
|
+
if (tokenDraft.trim()) {
|
|
196
|
+
const created = await api<Credential>('POST', '/credentials', {
|
|
197
|
+
provider: 'openclaw',
|
|
198
|
+
name: `${name.trim() || 'OpenClaw Gateway'} token`,
|
|
199
|
+
apiKey: tokenDraft.trim(),
|
|
200
|
+
})
|
|
201
|
+
nextCredentialId = created.id
|
|
202
|
+
}
|
|
160
203
|
const payload = {
|
|
161
204
|
name: name.trim() || 'OpenClaw Gateway',
|
|
162
205
|
endpoint: endpoint.trim() || 'http://localhost:18789',
|
|
163
|
-
credentialId:
|
|
206
|
+
credentialId: nextCredentialId || null,
|
|
164
207
|
notes: notes.trim() || null,
|
|
165
208
|
tags: tags.split(',').map((item) => item.trim()).filter(Boolean),
|
|
209
|
+
deployment,
|
|
166
210
|
isDefault,
|
|
167
211
|
}
|
|
168
212
|
if (editing) {
|
|
@@ -172,7 +216,7 @@ export function GatewaySheet() {
|
|
|
172
216
|
await api('POST', '/gateways', payload)
|
|
173
217
|
toast.success('Gateway added')
|
|
174
218
|
}
|
|
175
|
-
await loadGatewayProfiles()
|
|
219
|
+
await Promise.all([loadGatewayProfiles(), loadCredentials()])
|
|
176
220
|
onClose()
|
|
177
221
|
} catch (err: unknown) {
|
|
178
222
|
toast.error(err instanceof Error ? err.message : 'Failed to save gateway')
|
|
@@ -188,6 +232,7 @@ export function GatewaySheet() {
|
|
|
188
232
|
const params = new URLSearchParams()
|
|
189
233
|
params.set('endpoint', endpoint.trim() || 'http://localhost:18789')
|
|
190
234
|
if (credentialId) params.set('credentialId', credentialId)
|
|
235
|
+
if (tokenDraft.trim()) params.set('token', tokenDraft.trim())
|
|
191
236
|
const result = await api<{ ok: boolean; models: string[]; error?: string; hint?: string }>('GET', `/providers/openclaw/health?${params.toString()}`)
|
|
192
237
|
if (result.ok) {
|
|
193
238
|
setCheckMessage(`Connected. ${result.models?.length ? `${result.models.length} model${result.models.length === 1 ? '' : 's'} visible.` : 'Gateway responded normally.'}`)
|
|
@@ -278,15 +323,112 @@ export function GatewaySheet() {
|
|
|
278
323
|
|
|
279
324
|
const inputClass = 'w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow'
|
|
280
325
|
|
|
326
|
+
const applyDeployPatch = (patch: { endpoint?: string; token?: string; name?: string; notes?: string; deployment?: GatewayProfile['deployment'] | Record<string, unknown> | null }) => {
|
|
327
|
+
if (patch.endpoint) {
|
|
328
|
+
setEndpoint(patch.endpoint)
|
|
329
|
+
setCheckMessage('')
|
|
330
|
+
}
|
|
331
|
+
if (patch.token) {
|
|
332
|
+
setTokenDraft(patch.token)
|
|
333
|
+
setCredentialId(null)
|
|
334
|
+
}
|
|
335
|
+
if (patch.name && !name.trim()) {
|
|
336
|
+
setName(patch.name)
|
|
337
|
+
}
|
|
338
|
+
if (patch.notes && !notes.trim()) {
|
|
339
|
+
setNotes(patch.notes)
|
|
340
|
+
}
|
|
341
|
+
if (patch.deployment) {
|
|
342
|
+
setDeployment((current) => ({
|
|
343
|
+
...(current || {}),
|
|
344
|
+
...(patch.deployment as GatewayProfile['deployment']),
|
|
345
|
+
}))
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const handleExportGateway = () => {
|
|
350
|
+
const payload: GatewayImportShape = {
|
|
351
|
+
name: name.trim() || 'OpenClaw Gateway',
|
|
352
|
+
endpoint: endpoint.trim() || 'http://localhost:18789',
|
|
353
|
+
credentialId: credentialId || null,
|
|
354
|
+
token: tokenDraft.trim() || null,
|
|
355
|
+
notes: notes.trim() || null,
|
|
356
|
+
tags: tags.split(',').map((item) => item.trim()).filter(Boolean),
|
|
357
|
+
isDefault,
|
|
358
|
+
deployment,
|
|
359
|
+
}
|
|
360
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
361
|
+
const url = URL.createObjectURL(blob)
|
|
362
|
+
const link = document.createElement('a')
|
|
363
|
+
link.href = url
|
|
364
|
+
link.download = `${(payload.name || 'openclaw-gateway').replace(/[^a-zA-Z0-9_-]/g, '_')}.gateway.json`
|
|
365
|
+
link.click()
|
|
366
|
+
URL.revokeObjectURL(url)
|
|
367
|
+
toast.success('Gateway config exported')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const handleImportGateway = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
371
|
+
const file = event.target.files?.[0]
|
|
372
|
+
if (!file) return
|
|
373
|
+
const reader = new FileReader()
|
|
374
|
+
reader.onload = (loadEvent) => {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(String(loadEvent.target?.result || '{}')) as GatewayImportShape
|
|
377
|
+
setName(typeof parsed.name === 'string' ? parsed.name : '')
|
|
378
|
+
setEndpoint(typeof parsed.endpoint === 'string' ? parsed.endpoint : 'http://localhost:18789')
|
|
379
|
+
setCredentialId(typeof parsed.credentialId === 'string' ? parsed.credentialId : null)
|
|
380
|
+
setTokenDraft(typeof parsed.token === 'string' ? parsed.token : '')
|
|
381
|
+
setNotes(typeof parsed.notes === 'string' ? parsed.notes : '')
|
|
382
|
+
setTags(Array.isArray(parsed.tags) ? parsed.tags.join(', ') : '')
|
|
383
|
+
setIsDefault(parsed.isDefault === true)
|
|
384
|
+
setDeployment(parsed.deployment || null)
|
|
385
|
+
setCheckMessage('')
|
|
386
|
+
toast.success('Gateway config imported into this form')
|
|
387
|
+
} catch {
|
|
388
|
+
toast.error('Invalid gateway JSON')
|
|
389
|
+
} finally {
|
|
390
|
+
event.target.value = ''
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
reader.readAsText(file)
|
|
394
|
+
}
|
|
395
|
+
|
|
281
396
|
return (
|
|
282
397
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
398
|
+
<input
|
|
399
|
+
ref={importFileRef}
|
|
400
|
+
type="file"
|
|
401
|
+
accept="application/json,.json"
|
|
402
|
+
onChange={handleImportGateway}
|
|
403
|
+
className="hidden"
|
|
404
|
+
/>
|
|
283
405
|
<div className="mb-10">
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
406
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
407
|
+
<div>
|
|
408
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
409
|
+
{editing ? 'Edit Gateway' : 'New Gateway'}
|
|
410
|
+
</h2>
|
|
411
|
+
<p className="text-[14px] text-text-3">
|
|
412
|
+
First-class OpenClaw gateway profiles for local or remote control planes.
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
<div className="flex flex-wrap gap-2">
|
|
416
|
+
<button
|
|
417
|
+
type="button"
|
|
418
|
+
onClick={() => importFileRef.current?.click()}
|
|
419
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 hover:bg-white/[0.04] transition-all cursor-pointer"
|
|
420
|
+
>
|
|
421
|
+
Import JSON
|
|
422
|
+
</button>
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
onClick={handleExportGateway}
|
|
426
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 hover:bg-white/[0.04] transition-all cursor-pointer"
|
|
427
|
+
>
|
|
428
|
+
Export JSON
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
290
432
|
</div>
|
|
291
433
|
|
|
292
434
|
<div className="mb-6">
|
|
@@ -309,6 +451,44 @@ export function GatewaySheet() {
|
|
|
309
451
|
<p className="text-[11px] text-text-3/60 mt-2">Remote HTTPS URLs and local loopback endpoints are both supported.</p>
|
|
310
452
|
</div>
|
|
311
453
|
|
|
454
|
+
<div className="mb-6">
|
|
455
|
+
<OpenClawDeployPanel
|
|
456
|
+
endpoint={endpoint}
|
|
457
|
+
token={tokenDraft}
|
|
458
|
+
deployment={deployment}
|
|
459
|
+
suggestedName={name || null}
|
|
460
|
+
title="Deploy OpenClaw From SwarmClaw"
|
|
461
|
+
description="Use official OpenClaw sources only. Start it on this host, or generate a pre-configured remote bundle for VPS and hosted deployments."
|
|
462
|
+
onApply={applyDeployPatch}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
{deployment && (
|
|
467
|
+
<div className="mb-6 rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4">
|
|
468
|
+
<div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/70 mb-2">Deploy metadata</div>
|
|
469
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-[12px] text-text-3/75">
|
|
470
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface px-3 py-3">
|
|
471
|
+
<div className="uppercase tracking-[0.08em] text-text-3/55">Method</div>
|
|
472
|
+
<div className="mt-1 text-text-2">{deployment.method || 'manual'}</div>
|
|
473
|
+
</div>
|
|
474
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface px-3 py-3">
|
|
475
|
+
<div className="uppercase tracking-[0.08em] text-text-3/55">Use case</div>
|
|
476
|
+
<div className="mt-1 text-text-2">{deployment.useCase || 'general'}</div>
|
|
477
|
+
</div>
|
|
478
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface px-3 py-3">
|
|
479
|
+
<div className="uppercase tracking-[0.08em] text-text-3/55">Exposure</div>
|
|
480
|
+
<div className="mt-1 text-text-2">{deployment.exposure || 'manual'}</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
{deployment.lastDeploySummary && (
|
|
484
|
+
<p className="mt-3 text-[12px] text-text-3 leading-relaxed">{deployment.lastDeploySummary}</p>
|
|
485
|
+
)}
|
|
486
|
+
{deployment.lastVerifiedMessage && (
|
|
487
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">{deployment.lastVerifiedMessage}</p>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
|
|
312
492
|
{discoveries.length > 0 && (
|
|
313
493
|
<div className="mb-6">
|
|
314
494
|
<div className="text-[12px] text-text-3/70 mb-2">Detected healthy gateways</div>
|
|
@@ -342,6 +522,18 @@ export function GatewaySheet() {
|
|
|
342
522
|
<option key={item.id} value={item.id}>{item.name}</option>
|
|
343
523
|
))}
|
|
344
524
|
</select>
|
|
525
|
+
<input
|
|
526
|
+
value={tokenDraft}
|
|
527
|
+
onChange={(e) => {
|
|
528
|
+
setTokenDraft(e.target.value)
|
|
529
|
+
if (e.target.value) setCredentialId(null)
|
|
530
|
+
}}
|
|
531
|
+
placeholder="Or paste/generate a new gateway token"
|
|
532
|
+
className={`${inputClass} mt-3 font-mono text-[13px]`}
|
|
533
|
+
/>
|
|
534
|
+
<p className="mt-2 text-[11px] text-text-3/60">
|
|
535
|
+
A pasted token is stored as a new encrypted OpenClaw credential when you save this gateway.
|
|
536
|
+
</p>
|
|
345
537
|
</div>
|
|
346
538
|
|
|
347
539
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|