@swarmclawai/swarmclaw 0.7.6 → 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 +18 -10
- package/package.json +1 -1
- 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 +139 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +97 -19
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/providers/provider-list.tsx +221 -17
- 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/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- 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
package/src/cli/index.js
CHANGED
|
@@ -319,10 +319,50 @@ const COMMAND_GROUPS = [
|
|
|
319
319
|
expectsJsonBody: true,
|
|
320
320
|
defaultBody: { action: 'stop-local' },
|
|
321
321
|
}),
|
|
322
|
+
cmd('deploy-local-restart', 'POST', '/openclaw/deploy', 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)', {
|
|
323
|
+
expectsJsonBody: true,
|
|
324
|
+
defaultBody: { action: 'restart-local' },
|
|
325
|
+
}),
|
|
322
326
|
cmd('deploy-bundle', 'POST', '/openclaw/deploy', 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)', {
|
|
323
327
|
expectsJsonBody: true,
|
|
324
328
|
defaultBody: { action: 'bundle' },
|
|
325
329
|
}),
|
|
330
|
+
cmd('deploy-ssh', 'POST', '/openclaw/deploy', 'Push the official-image OpenClaw bundle to a remote host over SSH (use --data JSON for target/ssh/provider)', {
|
|
331
|
+
expectsJsonBody: true,
|
|
332
|
+
defaultBody: { action: 'ssh-deploy' },
|
|
333
|
+
}),
|
|
334
|
+
cmd('deploy-verify', 'POST', '/openclaw/deploy', 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)', {
|
|
335
|
+
expectsJsonBody: true,
|
|
336
|
+
defaultBody: { action: 'verify' },
|
|
337
|
+
}),
|
|
338
|
+
cmd('remote-start', 'POST', '/openclaw/deploy', 'Start a remote SSH-managed OpenClaw stack', {
|
|
339
|
+
expectsJsonBody: true,
|
|
340
|
+
defaultBody: { action: 'remote-start' },
|
|
341
|
+
}),
|
|
342
|
+
cmd('remote-stop', 'POST', '/openclaw/deploy', 'Stop a remote SSH-managed OpenClaw stack', {
|
|
343
|
+
expectsJsonBody: true,
|
|
344
|
+
defaultBody: { action: 'remote-stop' },
|
|
345
|
+
}),
|
|
346
|
+
cmd('remote-restart', 'POST', '/openclaw/deploy', 'Restart a remote SSH-managed OpenClaw stack', {
|
|
347
|
+
expectsJsonBody: true,
|
|
348
|
+
defaultBody: { action: 'remote-restart' },
|
|
349
|
+
}),
|
|
350
|
+
cmd('remote-upgrade', 'POST', '/openclaw/deploy', 'Upgrade a remote SSH-managed OpenClaw stack', {
|
|
351
|
+
expectsJsonBody: true,
|
|
352
|
+
defaultBody: { action: 'remote-upgrade' },
|
|
353
|
+
}),
|
|
354
|
+
cmd('remote-backup', 'POST', '/openclaw/deploy', 'Create a remote backup on an SSH-managed OpenClaw host', {
|
|
355
|
+
expectsJsonBody: true,
|
|
356
|
+
defaultBody: { action: 'remote-backup' },
|
|
357
|
+
}),
|
|
358
|
+
cmd('remote-restore', 'POST', '/openclaw/deploy', 'Restore a remote backup on an SSH-managed OpenClaw host', {
|
|
359
|
+
expectsJsonBody: true,
|
|
360
|
+
defaultBody: { action: 'remote-restore' },
|
|
361
|
+
}),
|
|
362
|
+
cmd('remote-rotate-token', 'POST', '/openclaw/deploy', 'Rotate the gateway token on an SSH-managed OpenClaw host', {
|
|
363
|
+
expectsJsonBody: true,
|
|
364
|
+
defaultBody: { action: 'remote-rotate-token' },
|
|
365
|
+
}),
|
|
326
366
|
cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
|
|
327
367
|
cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'),
|
|
328
368
|
cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }),
|
package/src/cli/index.test.js
CHANGED
|
@@ -197,6 +197,74 @@ test('openclaw deploy bundle command merges action with provided JSON body', asy
|
|
|
197
197
|
assert.equal(stderr.toString(), '')
|
|
198
198
|
})
|
|
199
199
|
|
|
200
|
+
test('openclaw deploy ssh command merges action with provided JSON body', async () => {
|
|
201
|
+
const stdout = makeWritable()
|
|
202
|
+
const stderr = makeWritable()
|
|
203
|
+
const calls = []
|
|
204
|
+
|
|
205
|
+
const fetchImpl = async (url, init) => {
|
|
206
|
+
calls.push({ url: String(url), init })
|
|
207
|
+
return jsonResponse({ ok: true, processId: 'remote-1' })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const exitCode = await runCli(
|
|
211
|
+
['openclaw', 'deploy-ssh', '--data', '{"target":"openclaw.example.com","ssh":{"host":"1.2.3.4"}}', '--json'],
|
|
212
|
+
{
|
|
213
|
+
fetchImpl,
|
|
214
|
+
stdout,
|
|
215
|
+
stderr,
|
|
216
|
+
env: {},
|
|
217
|
+
cwd: process.cwd(),
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
assert.equal(exitCode, 0)
|
|
222
|
+
assert.equal(calls.length, 1)
|
|
223
|
+
assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
|
|
224
|
+
assert.equal(calls[0].init.method, 'POST')
|
|
225
|
+
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
226
|
+
action: 'ssh-deploy',
|
|
227
|
+
target: 'openclaw.example.com',
|
|
228
|
+
ssh: { host: '1.2.3.4' },
|
|
229
|
+
})
|
|
230
|
+
assert.equal(stdout.toString().trim(), '{"ok":true,"processId":"remote-1"}')
|
|
231
|
+
assert.equal(stderr.toString(), '')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('openclaw remote restore command merges action with provided JSON body', async () => {
|
|
235
|
+
const stdout = makeWritable()
|
|
236
|
+
const stderr = makeWritable()
|
|
237
|
+
const calls = []
|
|
238
|
+
|
|
239
|
+
const fetchImpl = async (url, init) => {
|
|
240
|
+
calls.push({ url: String(url), init })
|
|
241
|
+
return jsonResponse({ ok: true, remote: { status: 'running' } })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const exitCode = await runCli(
|
|
245
|
+
['openclaw', 'remote-restore', '--data', '{"backupPath":"/opt/openclaw/backups/latest.tgz","ssh":{"host":"1.2.3.4"}}', '--json'],
|
|
246
|
+
{
|
|
247
|
+
fetchImpl,
|
|
248
|
+
stdout,
|
|
249
|
+
stderr,
|
|
250
|
+
env: {},
|
|
251
|
+
cwd: process.cwd(),
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
assert.equal(exitCode, 0)
|
|
256
|
+
assert.equal(calls.length, 1)
|
|
257
|
+
assert.match(calls[0].url, /\/api\/openclaw\/deploy$/)
|
|
258
|
+
assert.equal(calls[0].init.method, 'POST')
|
|
259
|
+
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
260
|
+
action: 'remote-restore',
|
|
261
|
+
backupPath: '/opt/openclaw/backups/latest.tgz',
|
|
262
|
+
ssh: { host: '1.2.3.4' },
|
|
263
|
+
})
|
|
264
|
+
assert.equal(stdout.toString().trim(), '{"ok":true,"remote":{"status":"running"}}')
|
|
265
|
+
assert.equal(stderr.toString(), '')
|
|
266
|
+
})
|
|
267
|
+
|
|
200
268
|
test('runCli falls back to platform-api-key.txt when env key is missing', async () => {
|
|
201
269
|
const stdout = makeWritable()
|
|
202
270
|
const stderr = makeWritable()
|
package/src/cli/spec.js
CHANGED
|
@@ -224,12 +224,72 @@ const COMMAND_GROUPS = {
|
|
|
224
224
|
path: '/openclaw/deploy',
|
|
225
225
|
staticBody: { action: 'stop-local' },
|
|
226
226
|
},
|
|
227
|
+
'deploy-local-restart': {
|
|
228
|
+
description: 'Restart the managed local OpenClaw deployment (use --data JSON for port/token overrides)',
|
|
229
|
+
method: 'POST',
|
|
230
|
+
path: '/openclaw/deploy',
|
|
231
|
+
staticBody: { action: 'restart-local' },
|
|
232
|
+
},
|
|
227
233
|
'deploy-bundle': {
|
|
228
234
|
description: 'Generate an OpenClaw remote deployment bundle (use --data JSON for template/target/token)',
|
|
229
235
|
method: 'POST',
|
|
230
236
|
path: '/openclaw/deploy',
|
|
231
237
|
staticBody: { action: 'bundle' },
|
|
232
238
|
},
|
|
239
|
+
'deploy-ssh': {
|
|
240
|
+
description: 'Push the official-image OpenClaw bundle to a remote host over SSH (use --data JSON for target/ssh/provider)',
|
|
241
|
+
method: 'POST',
|
|
242
|
+
path: '/openclaw/deploy',
|
|
243
|
+
staticBody: { action: 'ssh-deploy' },
|
|
244
|
+
},
|
|
245
|
+
'deploy-verify': {
|
|
246
|
+
description: 'Verify an OpenClaw endpoint/token pair (use --data JSON for endpoint/token)',
|
|
247
|
+
method: 'POST',
|
|
248
|
+
path: '/openclaw/deploy',
|
|
249
|
+
staticBody: { action: 'verify' },
|
|
250
|
+
},
|
|
251
|
+
'remote-start': {
|
|
252
|
+
description: 'Start a remote SSH-managed OpenClaw stack',
|
|
253
|
+
method: 'POST',
|
|
254
|
+
path: '/openclaw/deploy',
|
|
255
|
+
staticBody: { action: 'remote-start' },
|
|
256
|
+
},
|
|
257
|
+
'remote-stop': {
|
|
258
|
+
description: 'Stop a remote SSH-managed OpenClaw stack',
|
|
259
|
+
method: 'POST',
|
|
260
|
+
path: '/openclaw/deploy',
|
|
261
|
+
staticBody: { action: 'remote-stop' },
|
|
262
|
+
},
|
|
263
|
+
'remote-restart': {
|
|
264
|
+
description: 'Restart a remote SSH-managed OpenClaw stack',
|
|
265
|
+
method: 'POST',
|
|
266
|
+
path: '/openclaw/deploy',
|
|
267
|
+
staticBody: { action: 'remote-restart' },
|
|
268
|
+
},
|
|
269
|
+
'remote-upgrade': {
|
|
270
|
+
description: 'Upgrade a remote SSH-managed OpenClaw stack',
|
|
271
|
+
method: 'POST',
|
|
272
|
+
path: '/openclaw/deploy',
|
|
273
|
+
staticBody: { action: 'remote-upgrade' },
|
|
274
|
+
},
|
|
275
|
+
'remote-backup': {
|
|
276
|
+
description: 'Create a remote backup on an SSH-managed OpenClaw host',
|
|
277
|
+
method: 'POST',
|
|
278
|
+
path: '/openclaw/deploy',
|
|
279
|
+
staticBody: { action: 'remote-backup' },
|
|
280
|
+
},
|
|
281
|
+
'remote-restore': {
|
|
282
|
+
description: 'Restore a remote backup on an SSH-managed OpenClaw host',
|
|
283
|
+
method: 'POST',
|
|
284
|
+
path: '/openclaw/deploy',
|
|
285
|
+
staticBody: { action: 'remote-restore' },
|
|
286
|
+
},
|
|
287
|
+
'remote-rotate-token': {
|
|
288
|
+
description: 'Rotate the gateway token on an SSH-managed OpenClaw host',
|
|
289
|
+
method: 'POST',
|
|
290
|
+
path: '/openclaw/deploy',
|
|
291
|
+
staticBody: { action: 'remote-rotate-token' },
|
|
292
|
+
},
|
|
233
293
|
directory: { description: 'List directory entries from running OpenClaw connectors', method: 'GET', path: '/openclaw/directory' },
|
|
234
294
|
'gateway-status': { description: 'Check OpenClaw gateway connection status', method: 'GET', path: '/openclaw/gateway' },
|
|
235
295
|
gateway: { description: 'Call OpenClaw gateway RPC/control action', method: 'POST', path: '/openclaw/gateway' },
|
|
@@ -72,6 +72,24 @@ function parseIdentityList(value: string): string[] {
|
|
|
72
72
|
})
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function formatGatewayTagList(value: string[] | null | undefined): string {
|
|
76
|
+
return Array.isArray(value) ? value.join(', ') : ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseGatewayTagList(value: string): string[] {
|
|
80
|
+
const seen = new Set<string>()
|
|
81
|
+
return value
|
|
82
|
+
.split(/[,\n]/)
|
|
83
|
+
.map((entry) => entry.trim())
|
|
84
|
+
.filter((entry) => {
|
|
85
|
+
if (!entry) return false
|
|
86
|
+
const key = entry.toLowerCase()
|
|
87
|
+
if (seen.has(key)) return false
|
|
88
|
+
seen.add(key)
|
|
89
|
+
return true
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
export function AgentSheet() {
|
|
76
94
|
const open = useAppStore((s) => s.agentSheetOpen)
|
|
77
95
|
const setOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
@@ -113,6 +131,8 @@ export function AgentSheet() {
|
|
|
113
131
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
114
132
|
const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
|
|
115
133
|
const [gatewayProfileId, setGatewayProfileId] = useState<string | null>(null)
|
|
134
|
+
const [preferredGatewayTagsText, setPreferredGatewayTagsText] = useState('')
|
|
135
|
+
const [preferredGatewayUseCase, setPreferredGatewayUseCase] = useState('')
|
|
116
136
|
const [routingStrategy, setRoutingStrategy] = useState<AgentRoutingStrategy>('single')
|
|
117
137
|
const [routingTargets, setRoutingTargets] = useState<AgentRoutingTarget[]>([])
|
|
118
138
|
const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
|
|
@@ -220,6 +240,8 @@ export function AgentSheet() {
|
|
|
220
240
|
setCredentialId(editing.credentialId || null)
|
|
221
241
|
setApiEndpoint(editing.apiEndpoint || null)
|
|
222
242
|
setGatewayProfileId(editing.gatewayProfileId || null)
|
|
243
|
+
setPreferredGatewayTagsText(formatGatewayTagList(editing.preferredGatewayTags))
|
|
244
|
+
setPreferredGatewayUseCase(editing.preferredGatewayUseCase || '')
|
|
223
245
|
setRoutingStrategy(editing.routingStrategy || 'single')
|
|
224
246
|
setRoutingTargets(editing.routingTargets || [])
|
|
225
247
|
setPlatformAssignScope(editing.platformAssignScope || 'self')
|
|
@@ -287,6 +309,8 @@ export function AgentSheet() {
|
|
|
287
309
|
setCredentialId(null)
|
|
288
310
|
setApiEndpoint(null)
|
|
289
311
|
setGatewayProfileId(null)
|
|
312
|
+
setPreferredGatewayTagsText('')
|
|
313
|
+
setPreferredGatewayUseCase('')
|
|
290
314
|
setRoutingStrategy('single')
|
|
291
315
|
setRoutingTargets([])
|
|
292
316
|
setPlatformAssignScope('self')
|
|
@@ -422,6 +446,8 @@ export function AgentSheet() {
|
|
|
422
446
|
fallbackCredentialIds,
|
|
423
447
|
apiEndpoint,
|
|
424
448
|
gatewayProfileId,
|
|
449
|
+
preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
|
|
450
|
+
preferredGatewayUseCase: preferredGatewayUseCase || null,
|
|
425
451
|
priority: routingTargets.length + 1,
|
|
426
452
|
}
|
|
427
453
|
setRoutingTargets((current) => [...current, nextTarget])
|
|
@@ -463,9 +489,13 @@ export function AgentSheet() {
|
|
|
463
489
|
credentialId,
|
|
464
490
|
apiEndpoint: normalizedEndpoint,
|
|
465
491
|
gatewayProfileId,
|
|
492
|
+
preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
|
|
493
|
+
preferredGatewayUseCase: preferredGatewayUseCase || null,
|
|
466
494
|
routingStrategy,
|
|
467
495
|
routingTargets: routingTargets.map((target, index) => ({
|
|
468
496
|
...target,
|
|
497
|
+
preferredGatewayTags: parseGatewayTagList(formatGatewayTagList(target.preferredGatewayTags)),
|
|
498
|
+
preferredGatewayUseCase: target.preferredGatewayUseCase || null,
|
|
469
499
|
priority: typeof target.priority === 'number' ? target.priority : index + 1,
|
|
470
500
|
})),
|
|
471
501
|
subAgentIds: canDelegateToAgents ? subAgentIds : [],
|
|
@@ -1698,6 +1728,34 @@ export function AgentSheet() {
|
|
|
1698
1728
|
</div>
|
|
1699
1729
|
)}
|
|
1700
1730
|
|
|
1731
|
+
{(provider === 'openclaw' || routingTargets.some((target) => target.provider === 'openclaw') || openclawGatewayProfiles.length > 0) && (
|
|
1732
|
+
<div className="mb-8">
|
|
1733
|
+
<label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
1734
|
+
Gateway Preferences <HintTip text="When multiple OpenClaw gateways are available, prefer matching tags or deployment templates before falling back to the default route." />
|
|
1735
|
+
</label>
|
|
1736
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
1737
|
+
<input
|
|
1738
|
+
type="text"
|
|
1739
|
+
value={preferredGatewayTagsText}
|
|
1740
|
+
onChange={(e) => setPreferredGatewayTagsText(e.target.value)}
|
|
1741
|
+
placeholder="gpu, local, research"
|
|
1742
|
+
className={inputClass}
|
|
1743
|
+
/>
|
|
1744
|
+
<select value={preferredGatewayUseCase} onChange={(e) => setPreferredGatewayUseCase(e.target.value)} className={inputClass}>
|
|
1745
|
+
<option value="">Any OpenClaw template</option>
|
|
1746
|
+
<option value="local-dev">Local Dev</option>
|
|
1747
|
+
<option value="single-vps">Single VPS</option>
|
|
1748
|
+
<option value="private-tailnet">Private Tailnet</option>
|
|
1749
|
+
<option value="browser-heavy">Browser Heavy</option>
|
|
1750
|
+
<option value="team-control">Team Control</option>
|
|
1751
|
+
</select>
|
|
1752
|
+
</div>
|
|
1753
|
+
<p className="text-[11px] text-text-3/70 mt-2">
|
|
1754
|
+
These preferences bias scheduling toward matching OpenClaw control planes without hard-locking the agent to one gateway.
|
|
1755
|
+
</p>
|
|
1756
|
+
</div>
|
|
1757
|
+
)}
|
|
1758
|
+
|
|
1701
1759
|
<div className="mb-8">
|
|
1702
1760
|
<label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
1703
1761
|
Model Routing <HintTip text="Route this agent through a provider/model pool instead of a single fixed model. The base provider remains the default when no route matches." />
|
|
@@ -1752,25 +1810,45 @@ export function AgentSheet() {
|
|
|
1752
1810
|
/>
|
|
1753
1811
|
</div>
|
|
1754
1812
|
{target.provider === 'openclaw' && openclawGatewayProfiles.length > 0 && (
|
|
1755
|
-
<
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1813
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
1814
|
+
<select
|
|
1815
|
+
value={target.gatewayProfileId || ''}
|
|
1816
|
+
onChange={(e) => {
|
|
1817
|
+
const nextId = e.target.value || null
|
|
1818
|
+
const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
|
|
1819
|
+
updateRoutingTarget(target.id, {
|
|
1820
|
+
gatewayProfileId: nextId,
|
|
1821
|
+
apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
|
|
1822
|
+
credentialId: gateway?.credentialId || target.credentialId || null,
|
|
1823
|
+
model: target.model || 'default',
|
|
1824
|
+
})
|
|
1825
|
+
}}
|
|
1826
|
+
className={inputClass}
|
|
1827
|
+
>
|
|
1828
|
+
<option value="">Custom OpenClaw endpoint</option>
|
|
1829
|
+
{openclawGatewayProfiles.map((gateway) => (
|
|
1830
|
+
<option key={gateway.id} value={gateway.id}>{gateway.name}</option>
|
|
1831
|
+
))}
|
|
1832
|
+
</select>
|
|
1833
|
+
<input
|
|
1834
|
+
value={formatGatewayTagList(target.preferredGatewayTags)}
|
|
1835
|
+
onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayTags: parseGatewayTagList(e.target.value) })}
|
|
1836
|
+
placeholder="Prefer tags"
|
|
1837
|
+
className={inputClass}
|
|
1838
|
+
/>
|
|
1839
|
+
<select
|
|
1840
|
+
value={target.preferredGatewayUseCase || ''}
|
|
1841
|
+
onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayUseCase: e.target.value || null })}
|
|
1842
|
+
className={inputClass}
|
|
1843
|
+
>
|
|
1844
|
+
<option value="">Any OpenClaw template</option>
|
|
1845
|
+
<option value="local-dev">Local Dev</option>
|
|
1846
|
+
<option value="single-vps">Single VPS</option>
|
|
1847
|
+
<option value="private-tailnet">Private Tailnet</option>
|
|
1848
|
+
<option value="browser-heavy">Browser Heavy</option>
|
|
1849
|
+
<option value="team-control">Team Control</option>
|
|
1850
|
+
</select>
|
|
1851
|
+
</div>
|
|
1774
1852
|
)}
|
|
1775
1853
|
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
|
|
1776
1854
|
<input
|
|
@@ -54,6 +54,9 @@ interface ConfiguredProvider {
|
|
|
54
54
|
endpoint: string | null
|
|
55
55
|
defaultModel: string
|
|
56
56
|
gatewayProfileId: string | null
|
|
57
|
+
notes?: string | null
|
|
58
|
+
tags?: string[]
|
|
59
|
+
deployment?: GatewayProfile['deployment'] | null
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
interface StarterDraftAgent {
|
|
@@ -88,6 +91,20 @@ const CONNECTOR_ICONS = [
|
|
|
88
91
|
{ name: 'Telegram', icon: 'T' },
|
|
89
92
|
{ name: 'WhatsApp', icon: 'W' },
|
|
90
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
|
+
}
|
|
91
108
|
|
|
92
109
|
function stepIndex(step: SetupStep): number {
|
|
93
110
|
if (step === 'connect') return STEP_ORDER.indexOf('providers')
|
|
@@ -224,6 +241,12 @@ function ConfiguredProviderChips({ providers }: { providers: ConfiguredProvider[
|
|
|
224
241
|
{cp.provider === 'openclaw' && formatEndpointHost(cp.endpoint)
|
|
225
242
|
? `· ${formatEndpointHost(cp.endpoint)}`
|
|
226
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
|
+
: ''}
|
|
227
250
|
{cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
|
|
228
251
|
</span>
|
|
229
252
|
</span>
|
|
@@ -332,6 +355,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
332
355
|
const [endpoint, setEndpoint] = useState('')
|
|
333
356
|
const [apiKey, setApiKey] = useState('')
|
|
334
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)
|
|
335
361
|
const [checkState, setCheckState] = useState<CheckState>('idle')
|
|
336
362
|
const [checkMessage, setCheckMessage] = useState('')
|
|
337
363
|
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
@@ -382,6 +408,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
382
408
|
setEndpoint('')
|
|
383
409
|
setApiKey('')
|
|
384
410
|
setCredentialId(null)
|
|
411
|
+
setProviderNotes('')
|
|
412
|
+
setProviderTags([])
|
|
413
|
+
setProviderDeployment(null)
|
|
385
414
|
setCheckState('idle')
|
|
386
415
|
setCheckMessage('')
|
|
387
416
|
setCheckErrorCode(null)
|
|
@@ -432,6 +461,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
432
461
|
setEndpoint(meta?.defaultEndpoint || '')
|
|
433
462
|
setApiKey('')
|
|
434
463
|
setCredentialId(null)
|
|
464
|
+
setProviderNotes('')
|
|
465
|
+
setProviderTags([])
|
|
466
|
+
setProviderDeployment(null)
|
|
435
467
|
setCheckState('idle')
|
|
436
468
|
setCheckMessage('')
|
|
437
469
|
setCheckErrorCode(null)
|
|
@@ -445,6 +477,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
445
477
|
endpoint?: string
|
|
446
478
|
token?: string
|
|
447
479
|
name?: string
|
|
480
|
+
notes?: string
|
|
481
|
+
deployment?: GatewayProfile['deployment'] | Record<string, unknown> | null
|
|
448
482
|
}) => {
|
|
449
483
|
if (patch.endpoint) {
|
|
450
484
|
setEndpoint(patch.endpoint)
|
|
@@ -456,6 +490,22 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
456
490
|
if (patch.name && (!providerLabel.trim() || providerLabel.trim() === (selectedProvider?.name || ''))) {
|
|
457
491
|
setProviderLabel(patch.name)
|
|
458
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
|
+
}
|
|
459
509
|
setCheckState('idle')
|
|
460
510
|
setCheckMessage('')
|
|
461
511
|
setCheckErrorCode(null)
|
|
@@ -552,6 +602,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
552
602
|
endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
|
|
553
603
|
defaultModel: providerSuggestedModel || getDefaultModelForProvider(provider),
|
|
554
604
|
gatewayProfileId: null,
|
|
605
|
+
notes: providerNotes.trim() || null,
|
|
606
|
+
tags: providerTags,
|
|
607
|
+
deployment: providerDeployment,
|
|
555
608
|
}
|
|
556
609
|
|
|
557
610
|
const nextConfigured = [...configuredProviders, configuredProvider]
|
|
@@ -645,8 +698,13 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
645
698
|
name: configuredProvider.name,
|
|
646
699
|
endpoint: normalizedEndpoint,
|
|
647
700
|
credentialId: configuredProvider.credentialId || null,
|
|
648
|
-
tags: [
|
|
649
|
-
|
|
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',
|
|
650
708
|
isDefault: shouldCreateDefault,
|
|
651
709
|
})
|
|
652
710
|
gatewayProfileIdsByProviderConfig.set(configuredProvider.id, createdGateway.id)
|
|
@@ -1084,6 +1142,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1084
1142
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1085
1143
|
If you only have a WebSocket gateway URL, you can still paste it here. SwarmClaw will normalize it for agent chat.
|
|
1086
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>
|
|
1087
1148
|
</div>
|
|
1088
1149
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1089
1150
|
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Safe defaults</div>
|
|
@@ -1093,6 +1154,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1093
1154
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1094
1155
|
Local quickstart uses the bundled official OpenClaw CLI. Remote quickstart uses the official OpenClaw Docker image or the official repo for managed hosts.
|
|
1095
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.
|
|
1159
|
+
</p>
|
|
1096
1160
|
</div>
|
|
1097
1161
|
</div>
|
|
1098
1162
|
|
|
@@ -1104,6 +1168,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1104
1168
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1105
1169
|
Current target: <span className="text-text-2">{openClawEndpointHost || 'localhost:18789'}</span>{openClawLocal ? ' · local route' : ' · remote route'}
|
|
1106
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>
|
|
1107
1174
|
</div>
|
|
1108
1175
|
</div>
|
|
1109
1176
|
)}
|
|
@@ -1314,6 +1381,12 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1314
1381
|
{configuredProvider.provider === 'openclaw' && formatEndpointHost(configuredProvider.endpoint)
|
|
1315
1382
|
? ` · ${formatEndpointHost(configuredProvider.endpoint)}`
|
|
1316
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
|
+
: ''}
|
|
1317
1390
|
{configuredProvider.defaultModel ? ` · ${configuredProvider.defaultModel}` : ''}
|
|
1318
1391
|
</option>
|
|
1319
1392
|
))}
|