@swarmclawai/swarmclaw 0.2.0 → 0.3.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 +1 -1
- package/package.json +1 -1
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/src/app/api/setup/check-provider/route.ts +5 -94
- package/src/app/api/setup/openclaw-device/route.ts +11 -0
- package/src/components/agents/agent-sheet.tsx +183 -135
- package/src/lib/providers/index.ts +2 -1
- package/src/lib/providers/openclaw.ts +60 -21
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ To connect an agent to an OpenClaw gateway:
|
|
|
206
206
|
2. Toggle **OpenClaw Gateway** ON
|
|
207
207
|
3. Enter the gateway URL (e.g. `http://192.168.1.50:18789` or `https://my-vps:18789`)
|
|
208
208
|
4. Add a gateway token if authentication is enabled on the remote gateway
|
|
209
|
-
5. Click **
|
|
209
|
+
5. Click **Connect** — approve the device in your gateway's dashboard if prompted, then **Retry Connection**
|
|
210
210
|
|
|
211
211
|
Each agent can point to a **different** OpenClaw gateway — one local, several remote. This is how you manage a **swarm of OpenClaws** from a single dashboard.
|
|
212
212
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { WebSocket } from 'ws'
|
|
3
|
-
import { randomUUID } from 'crypto'
|
|
4
2
|
import { loadCredentials, decryptKey } from '@/lib/server/storage'
|
|
5
|
-
import {
|
|
3
|
+
import { getDeviceId, wsConnect } from '@/lib/providers/openclaw'
|
|
6
4
|
|
|
7
5
|
type SetupProvider =
|
|
8
6
|
| 'openai'
|
|
@@ -159,103 +157,16 @@ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok:
|
|
|
159
157
|
const token = apiKey || undefined
|
|
160
158
|
const deviceId = getDeviceId()
|
|
161
159
|
|
|
162
|
-
|
|
163
|
-
// the
|
|
164
|
-
|
|
160
|
+
const result = await wsConnect(wsUrl, token, true, 10_000)
|
|
161
|
+
// Close the WebSocket immediately — we only care about the handshake result
|
|
162
|
+
if (result.ws) try { result.ws.close() } catch {}
|
|
163
|
+
|
|
165
164
|
if (result.ok) {
|
|
166
165
|
return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId }
|
|
167
166
|
}
|
|
168
|
-
if (result.errorCode === 'PAIRING_REQUIRED') {
|
|
169
|
-
return {
|
|
170
|
-
ok: false,
|
|
171
|
-
errorCode: 'PAIRING_REQUIRED',
|
|
172
|
-
message: 'Device needs approval on your gateway.',
|
|
173
|
-
normalizedEndpoint,
|
|
174
|
-
deviceId,
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (result.errorCode === 'DEVICE_AUTH_INVALID') {
|
|
178
|
-
return {
|
|
179
|
-
ok: false,
|
|
180
|
-
errorCode: 'DEVICE_AUTH_INVALID',
|
|
181
|
-
message: 'Device signature rejected. Your gateway may be running a different protocol version.',
|
|
182
|
-
normalizedEndpoint,
|
|
183
|
-
deviceId,
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
167
|
return { ok: false, message: result.message, normalizedEndpoint, deviceId, errorCode: result.errorCode }
|
|
187
168
|
}
|
|
188
169
|
|
|
189
|
-
function attemptOpenClawConnect(
|
|
190
|
-
wsUrl: string,
|
|
191
|
-
token: string | undefined,
|
|
192
|
-
useDeviceAuth: boolean,
|
|
193
|
-
): Promise<{ ok: boolean; message: string; errorCode?: string }> {
|
|
194
|
-
return new Promise((resolve) => {
|
|
195
|
-
let settled = false
|
|
196
|
-
const done = (result: { ok: boolean; message: string; errorCode?: string }) => {
|
|
197
|
-
if (settled) return
|
|
198
|
-
settled = true
|
|
199
|
-
clearTimeout(timer)
|
|
200
|
-
try { ws.close() } catch {}
|
|
201
|
-
resolve(result)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const timer = setTimeout(() => {
|
|
205
|
-
done({ ok: false, message: 'Connection timed out. Verify the gateway URL and network access.' })
|
|
206
|
-
}, 10_000)
|
|
207
|
-
|
|
208
|
-
const ws = new WebSocket(wsUrl)
|
|
209
|
-
let connectId: string | null = null
|
|
210
|
-
|
|
211
|
-
ws.on('message', (data) => {
|
|
212
|
-
try {
|
|
213
|
-
const msg = JSON.parse(data.toString())
|
|
214
|
-
if (msg.event === 'connect.challenge') {
|
|
215
|
-
connectId = randomUUID()
|
|
216
|
-
ws.send(JSON.stringify({
|
|
217
|
-
type: 'req',
|
|
218
|
-
id: connectId,
|
|
219
|
-
method: 'connect',
|
|
220
|
-
params: buildOpenClawConnectParams(token, msg.payload?.nonce, { useDeviceAuth }),
|
|
221
|
-
}))
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
if (msg.type === 'res' && msg.id === connectId) {
|
|
225
|
-
if (msg.ok) {
|
|
226
|
-
done({ ok: true, message: 'Connected.' })
|
|
227
|
-
} else {
|
|
228
|
-
const message = msg.error?.message || 'Gateway rejected the connection.'
|
|
229
|
-
// Extract error code from structured field or infer from message text
|
|
230
|
-
let errorCode = (msg.error?.details?.code ?? msg.error?.code) as string | undefined
|
|
231
|
-
if (!errorCode) {
|
|
232
|
-
const m = message.toLowerCase()
|
|
233
|
-
if (m.includes('pairing') || m.includes('not paired') || m.includes('pending approval')) errorCode = 'PAIRING_REQUIRED'
|
|
234
|
-
else if (m.includes('signature') || m.includes('device') || m.includes('identity')) errorCode = 'DEVICE_AUTH_INVALID'
|
|
235
|
-
}
|
|
236
|
-
done({ ok: false, message, errorCode })
|
|
237
|
-
}
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
} catch {
|
|
241
|
-
done({ ok: false, message: 'Unexpected response from gateway.' })
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
ws.on('error', (err) => {
|
|
246
|
-
done({ ok: false, message: `Connection failed: ${err.message}` })
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
ws.on('close', (code, reason) => {
|
|
250
|
-
if (code === 1008) {
|
|
251
|
-
done({ ok: false, message: `Unauthorized: ${reason?.toString() || 'invalid token'}` })
|
|
252
|
-
} else {
|
|
253
|
-
done({ ok: false, message: `Connection closed (${code})` })
|
|
254
|
-
}
|
|
255
|
-
})
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
|
|
259
170
|
export async function POST(req: Request) {
|
|
260
171
|
const body = parseBody(await req.json().catch(() => ({})))
|
|
261
172
|
const provider = clean(body.provider) as SetupProvider
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getDeviceId } from '@/lib/providers/openclaw'
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const deviceId = getDeviceId()
|
|
7
|
+
return NextResponse.json({ deviceId })
|
|
8
|
+
} catch (err: any) {
|
|
9
|
+
return NextResponse.json({ deviceId: null, error: err?.message }, { status: 500 })
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -101,6 +101,8 @@ export function AgentSheet() {
|
|
|
101
101
|
const [testMessage, setTestMessage] = useState('')
|
|
102
102
|
const [testErrorCode, setTestErrorCode] = useState<string | null>(null)
|
|
103
103
|
const [testDeviceId, setTestDeviceId] = useState<string | null>(null)
|
|
104
|
+
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
105
|
+
const [configCopied, setConfigCopied] = useState(false)
|
|
104
106
|
|
|
105
107
|
const soulFileRef = useRef<HTMLInputElement>(null)
|
|
106
108
|
const promptFileRef = useRef<HTMLInputElement>(null)
|
|
@@ -234,6 +236,16 @@ export function AgentSheet() {
|
|
|
234
236
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
237
|
}, [mcpServerIds.join(',')])
|
|
236
238
|
|
|
239
|
+
// Fetch OpenClaw device ID when toggle is enabled
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!openclawEnabled) return
|
|
242
|
+
let cancelled = false
|
|
243
|
+
api<{ deviceId: string }>('GET', '/setup/openclaw-device').then((res) => {
|
|
244
|
+
if (!cancelled && res.deviceId) setOpenclawDeviceId(res.deviceId)
|
|
245
|
+
}).catch(() => {})
|
|
246
|
+
return () => { cancelled = true }
|
|
247
|
+
}, [openclawEnabled])
|
|
248
|
+
|
|
237
249
|
const handleGenerate = async () => {
|
|
238
250
|
if (!aiPrompt.trim()) return
|
|
239
251
|
setGenerating(true)
|
|
@@ -380,8 +392,10 @@ export function AgentSheet() {
|
|
|
380
392
|
if (needsTest) {
|
|
381
393
|
const passed = await handleTestConnection()
|
|
382
394
|
if (!passed) return
|
|
383
|
-
|
|
384
|
-
|
|
395
|
+
if (!openclawEnabled) {
|
|
396
|
+
// Brief pause so the user can see the success state on the button
|
|
397
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
398
|
+
}
|
|
385
399
|
}
|
|
386
400
|
setSaving(true)
|
|
387
401
|
await handleSave()
|
|
@@ -400,15 +414,44 @@ export function AgentSheet() {
|
|
|
400
414
|
|
|
401
415
|
return (
|
|
402
416
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
403
|
-
<div className="mb-10">
|
|
404
|
-
<
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
417
|
+
<div className="mb-10 flex items-start justify-between">
|
|
418
|
+
<div>
|
|
419
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
420
|
+
{editing ? 'Edit Agent' : 'New Agent'}
|
|
421
|
+
</h2>
|
|
422
|
+
<p className="text-[14px] text-text-3">Define an AI agent or orchestrator</p>
|
|
423
|
+
</div>
|
|
424
|
+
<div className="flex items-center gap-3 mt-1.5">
|
|
425
|
+
<label className="text-[11px] font-600 text-text-3 uppercase tracking-[0.08em]">OpenClaw</label>
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={() => {
|
|
429
|
+
if (!openclawEnabled) {
|
|
430
|
+
setOpenclawEnabled(true)
|
|
431
|
+
setProvider('openclaw')
|
|
432
|
+
setModel('default')
|
|
433
|
+
if (!apiEndpoint) setApiEndpoint('http://localhost:18789')
|
|
434
|
+
} else {
|
|
435
|
+
setOpenclawEnabled(false)
|
|
436
|
+
const first = providers[0]?.id || 'claude-cli'
|
|
437
|
+
setProvider(first)
|
|
438
|
+
setModel('')
|
|
439
|
+
setApiEndpoint(null)
|
|
440
|
+
setCredentialId(null)
|
|
441
|
+
setTestStatus('idle')
|
|
442
|
+
setTestMessage('')
|
|
443
|
+
setTestErrorCode(null)
|
|
444
|
+
}
|
|
445
|
+
}}
|
|
446
|
+
className={`relative w-11 h-6 rounded-full transition-colors duration-200 cursor-pointer border-none ${openclawEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
447
|
+
>
|
|
448
|
+
<span className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform duration-200 ${openclawEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
408
451
|
</div>
|
|
409
452
|
|
|
410
453
|
{/* AI Generation */}
|
|
411
|
-
{!editing && <AiGenBlock
|
|
454
|
+
{!editing && !openclawEnabled && <AiGenBlock
|
|
412
455
|
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
413
456
|
generating={generating} generated={generated} genError={genError}
|
|
414
457
|
onGenerate={handleGenerate} appSettings={appSettings}
|
|
@@ -425,8 +468,8 @@ export function AgentSheet() {
|
|
|
425
468
|
<input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
426
469
|
</div>
|
|
427
470
|
|
|
428
|
-
{/* Capabilities */}
|
|
429
|
-
<div className="mb-8">
|
|
471
|
+
{/* Capabilities — hidden for OpenClaw (gateway manages its own capabilities) */}
|
|
472
|
+
{!openclawEnabled && <div className="mb-8">
|
|
430
473
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
431
474
|
Capabilities <span className="normal-case tracking-normal font-normal text-text-3">(for agent delegation)</span>
|
|
432
475
|
</label>
|
|
@@ -467,7 +510,7 @@ export function AgentSheet() {
|
|
|
467
510
|
/>
|
|
468
511
|
</div>
|
|
469
512
|
<p className="text-[11px] text-text-3/70 mt-1.5">Press Enter or comma to add. Other agents see these when deciding delegation.</p>
|
|
470
|
-
</div>
|
|
513
|
+
</div>}
|
|
471
514
|
|
|
472
515
|
{provider !== 'openclaw' && (
|
|
473
516
|
<div className="mb-8">
|
|
@@ -508,35 +551,13 @@ export function AgentSheet() {
|
|
|
508
551
|
</div>
|
|
509
552
|
)}
|
|
510
553
|
|
|
511
|
-
{/* OpenClaw Gateway
|
|
512
|
-
|
|
513
|
-
<div className="
|
|
514
|
-
|
|
515
|
-
<
|
|
516
|
-
type="button"
|
|
517
|
-
onClick={() => {
|
|
518
|
-
if (!openclawEnabled) {
|
|
519
|
-
setOpenclawEnabled(true)
|
|
520
|
-
setProvider('openclaw')
|
|
521
|
-
setModel('default')
|
|
522
|
-
if (!apiEndpoint) setApiEndpoint('http://localhost:18789')
|
|
523
|
-
} else {
|
|
524
|
-
setOpenclawEnabled(false)
|
|
525
|
-
setProvider('claude-cli')
|
|
526
|
-
setModel('')
|
|
527
|
-
setApiEndpoint(null)
|
|
528
|
-
setCredentialId(null)
|
|
529
|
-
}
|
|
530
|
-
}}
|
|
531
|
-
className={`relative w-11 h-6 rounded-full transition-colors duration-200 cursor-pointer border-none ${openclawEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
532
|
-
>
|
|
533
|
-
<span className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform duration-200 ${openclawEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
534
|
-
</button>
|
|
535
|
-
</div>
|
|
536
|
-
{openclawEnabled && (
|
|
537
|
-
<div className="mt-4 space-y-4">
|
|
554
|
+
{/* OpenClaw Gateway Fields */}
|
|
555
|
+
{openclawEnabled && (
|
|
556
|
+
<div className="mb-8 space-y-5">
|
|
557
|
+
{/* Connection fields */}
|
|
558
|
+
<div className="space-y-4">
|
|
538
559
|
<div>
|
|
539
|
-
<label className="block text-[12px] text-text-
|
|
560
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Gateway URL</label>
|
|
540
561
|
<input
|
|
541
562
|
type="text"
|
|
542
563
|
value={apiEndpoint || ''}
|
|
@@ -547,7 +568,7 @@ export function AgentSheet() {
|
|
|
547
568
|
/>
|
|
548
569
|
</div>
|
|
549
570
|
<div>
|
|
550
|
-
<label className="block text-[12px] text-text-
|
|
571
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Gateway Token</label>
|
|
551
572
|
{openclawCredentials.length > 0 && !addingKey ? (
|
|
552
573
|
<div className="flex gap-2">
|
|
553
574
|
<select value={credentialId || ''} onChange={(e) => {
|
|
@@ -559,7 +580,7 @@ export function AgentSheet() {
|
|
|
559
580
|
setCredentialId(e.target.value || null)
|
|
560
581
|
}
|
|
561
582
|
}} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
|
|
562
|
-
<option value="">
|
|
583
|
+
<option value="">No token (auth disabled)</option>
|
|
563
584
|
{openclawCredentials.map((c) => (
|
|
564
585
|
<option key={c.id} value={c.id}>{c.name}</option>
|
|
565
586
|
))}
|
|
@@ -574,12 +595,12 @@ export function AgentSheet() {
|
|
|
574
595
|
</button>
|
|
575
596
|
</div>
|
|
576
597
|
) : (
|
|
577
|
-
<div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/
|
|
598
|
+
<div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/10">
|
|
578
599
|
<input
|
|
579
600
|
type="text"
|
|
580
601
|
value={newKeyName}
|
|
581
602
|
onChange={(e) => setNewKeyName(e.target.value)}
|
|
582
|
-
placeholder="
|
|
603
|
+
placeholder="Label (e.g. Local gateway)"
|
|
583
604
|
className={inputClass}
|
|
584
605
|
style={{ fontFamily: 'inherit' }}
|
|
585
606
|
/>
|
|
@@ -619,85 +640,108 @@ export function AgentSheet() {
|
|
|
619
640
|
</div>
|
|
620
641
|
)}
|
|
621
642
|
</div>
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
</p>
|
|
634
|
-
</div>
|
|
635
|
-
)
|
|
636
|
-
return null
|
|
637
|
-
})()}
|
|
638
|
-
{/* OpenClaw troubleshooting — shown on any test failure */}
|
|
639
|
-
{testStatus === 'fail' && openclawEnabled && (
|
|
640
|
-
<div className="mt-3 p-4 rounded-[12px] bg-accent-soft/30 border border-accent-bright/20">
|
|
641
|
-
{testErrorCode === 'PAIRING_REQUIRED' ? (
|
|
642
|
-
<div className="space-y-2">
|
|
643
|
-
<p className="text-[12px] text-accent-bright font-600 mb-1">Device Pairing Required</p>
|
|
644
|
-
<p className="text-[11px] text-text-3 leading-[1.6]">
|
|
645
|
-
Your gateway needs to approve this SwarmClaw instance before it can connect. Follow these steps:
|
|
646
|
-
</p>
|
|
647
|
-
<ol className="text-[11px] text-text-3 leading-[1.8] list-decimal list-inside space-y-1">
|
|
648
|
-
<li>Open your OpenClaw control UI at <span className="text-accent-bright font-500">{apiEndpoint || 'http://localhost:18789'}</span></li>
|
|
649
|
-
<li>Go to <span className="text-text-2 font-500">Devices</span></li>
|
|
650
|
-
<li>Find and approve the pending device {testDeviceId ? <span className="text-text-2 font-mono text-[10px]">({testDeviceId.slice(0, 12)}...)</span> : null}</li>
|
|
651
|
-
<li>Come back here and click <span className="text-text-2 font-500">Test & Save</span> again</li>
|
|
652
|
-
</ol>
|
|
653
|
-
</div>
|
|
654
|
-
) : testErrorCode === 'DEVICE_AUTH_INVALID' ? (
|
|
655
|
-
<div className="space-y-2">
|
|
656
|
-
<p className="text-[12px] text-accent-bright font-600 mb-1">Device Authentication Failed</p>
|
|
657
|
-
<p className="text-[11px] text-text-3 leading-[1.6]">
|
|
658
|
-
The gateway rejected this device's signature. This usually means it needs to be paired first, or there's a protocol mismatch.
|
|
659
|
-
</p>
|
|
660
|
-
<p className="text-[11px] text-text-3 font-500 mt-2 mb-1">Try these steps:</p>
|
|
661
|
-
<ol className="text-[11px] text-text-3 leading-[1.8] list-decimal list-inside space-y-1">
|
|
662
|
-
<li>Open your OpenClaw control UI at <span className="text-accent-bright font-500">{apiEndpoint || 'http://localhost:18789'}</span></li>
|
|
663
|
-
<li>Go to <span className="text-text-2 font-500">Devices</span> and look for a pending device request</li>
|
|
664
|
-
<li>If the device is listed, approve it and click <span className="text-text-2 font-500">Test & Save</span> again</li>
|
|
665
|
-
<li>If not listed, update your gateway to the latest version and restart it</li>
|
|
666
|
-
</ol>
|
|
667
|
-
</div>
|
|
668
|
-
) : (
|
|
669
|
-
<div className="space-y-2">
|
|
670
|
-
<p className="text-[12px] text-accent-bright font-600 mb-1">Connection Failed</p>
|
|
671
|
-
<p className="text-[11px] text-text-3 leading-[1.6]">
|
|
672
|
-
Could not connect to the OpenClaw gateway. Check the following:
|
|
673
|
-
</p>
|
|
674
|
-
<ul className="text-[11px] text-text-3 leading-[1.8] list-disc list-inside space-y-1">
|
|
675
|
-
<li>The gateway is running and reachable at the URL above</li>
|
|
676
|
-
<li>The gateway token matches exactly (if required)</li>
|
|
677
|
-
<li>No firewall is blocking the connection</li>
|
|
678
|
-
</ul>
|
|
679
|
-
</div>
|
|
680
|
-
)}
|
|
681
|
-
{testDeviceId && (
|
|
682
|
-
<div className="mt-3 pt-3 border-t border-white/[0.06]">
|
|
683
|
-
<p className="text-[10px] text-text-3/60">
|
|
684
|
-
SwarmClaw Device ID: <span className="font-mono text-text-3/80 select-all">{testDeviceId}</span>
|
|
685
|
-
</p>
|
|
686
|
-
</div>
|
|
687
|
-
)}
|
|
688
|
-
</div>
|
|
689
|
-
)}
|
|
690
|
-
{/* Device ID info — shown after any successful OpenClaw test */}
|
|
691
|
-
{testStatus === 'pass' && openclawEnabled && testDeviceId && (
|
|
692
|
-
<div className="mt-3 px-3 py-2 rounded-[10px] bg-emerald-500/[0.06] border border-emerald-500/15">
|
|
693
|
-
<p className="text-[10px] text-text-3/60">
|
|
694
|
-
Device ID: <span className="font-mono text-emerald-400/70 select-all">{testDeviceId.slice(0, 16)}...</span>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
{/* Insecure connection warning */}
|
|
646
|
+
{(() => {
|
|
647
|
+
const url = (apiEndpoint || '').trim().toLowerCase()
|
|
648
|
+
const isRemote = url && !/localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(url)
|
|
649
|
+
const isSecure = /^(https|wss):\/\//i.test(url)
|
|
650
|
+
if (isRemote && !isSecure) return (
|
|
651
|
+
<div className="px-3 py-2.5 rounded-[10px] bg-[#fbbf24]/[0.06] border border-[#fbbf24]/20">
|
|
652
|
+
<p className="text-[13px] text-[#fbbf24] leading-[1.5]">
|
|
653
|
+
Unencrypted connection. Use HTTPS or an SSH tunnel for production.
|
|
695
654
|
</p>
|
|
696
655
|
</div>
|
|
697
|
-
)
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
656
|
+
)
|
|
657
|
+
return null
|
|
658
|
+
})()}
|
|
659
|
+
|
|
660
|
+
{/* Status feedback — single unified block */}
|
|
661
|
+
{testStatus === 'pass' && (
|
|
662
|
+
<div className="p-4 rounded-[12px] bg-emerald-500/[0.06] border border-emerald-500/15 space-y-2">
|
|
663
|
+
<div className="flex items-center gap-2">
|
|
664
|
+
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
|
665
|
+
<p className="text-[14px] text-emerald-400 font-600">Connected</p>
|
|
666
|
+
</div>
|
|
667
|
+
<p className="text-[13px] text-text-2/80 leading-[1.6]">Gateway is reachable and this device is paired. Tools and models are managed by the OpenClaw instance.</p>
|
|
668
|
+
</div>
|
|
669
|
+
)}
|
|
670
|
+
{testStatus === 'fail' && (
|
|
671
|
+
<div className="p-4 rounded-[12px] border space-y-3"
|
|
672
|
+
style={{
|
|
673
|
+
background: testErrorCode === 'PAIRING_REQUIRED' ? 'rgba(34,197,94,0.04)' : 'rgba(var(--accent-bright-rgb,120,100,255),0.06)',
|
|
674
|
+
borderColor: testErrorCode === 'PAIRING_REQUIRED' ? 'rgba(34,197,94,0.2)' : 'rgba(var(--accent-bright-rgb,120,100,255),0.15)',
|
|
675
|
+
}}
|
|
676
|
+
>
|
|
677
|
+
{testErrorCode === 'PAIRING_REQUIRED' ? (<>
|
|
678
|
+
<div className="flex items-center gap-2">
|
|
679
|
+
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
|
680
|
+
<p className="text-[14px] text-[#22c55e] font-600">Awaiting Approval</p>
|
|
681
|
+
</div>
|
|
682
|
+
<p className="text-[13px] text-text-2/80 leading-[1.6]">
|
|
683
|
+
This device is pending approval on your gateway. Go to <span className="text-text-2 font-500">Nodes</span>, approve the device{(testDeviceId || openclawDeviceId) ? <> (<code className="text-[12px] font-mono text-text-2/70">{(testDeviceId || openclawDeviceId)!.slice(0, 12)}...</code>)</> : null}, then click <span className="text-text-2 font-500">Retry Connection</span>.
|
|
684
|
+
</p>
|
|
685
|
+
<a
|
|
686
|
+
href={(() => { const ep = (apiEndpoint || 'http://localhost:18789').replace(/\/+$/, ''); return /^https?:\/\//i.test(ep) ? ep : `http://${ep}` })()}
|
|
687
|
+
target="_blank"
|
|
688
|
+
rel="noopener noreferrer"
|
|
689
|
+
className="inline-flex items-center gap-1.5 mt-2 px-4 py-2 rounded-[10px] bg-white/[0.06] border border-white/[0.1] text-[13px] text-text-2 font-500 hover:bg-white/[0.1] transition-colors"
|
|
690
|
+
>
|
|
691
|
+
Approve in Dashboard →
|
|
692
|
+
</a>
|
|
693
|
+
</>) : testErrorCode === 'DEVICE_AUTH_INVALID' ? (<>
|
|
694
|
+
<p className="text-[14px] text-accent-bright font-600">Device Not Paired</p>
|
|
695
|
+
<p className="text-[13px] text-text-2/80 leading-[1.6]">
|
|
696
|
+
The gateway doesn't recognize this device. Go to <span className="text-text-2 font-500">Nodes</span>, and add or approve this device{(testDeviceId || openclawDeviceId) ? <> (<code className="text-[12px] font-mono text-text-2/70">{(testDeviceId || openclawDeviceId)!.slice(0, 12)}...</code>)</> : null}.
|
|
697
|
+
</p>
|
|
698
|
+
<a
|
|
699
|
+
href={(() => { const ep = (apiEndpoint || 'http://localhost:18789').replace(/\/+$/, ''); return /^https?:\/\//i.test(ep) ? ep : `http://${ep}` })()}
|
|
700
|
+
target="_blank"
|
|
701
|
+
rel="noopener noreferrer"
|
|
702
|
+
className="inline-flex items-center gap-1.5 mt-2 px-4 py-2 rounded-[10px] bg-white/[0.06] border border-white/[0.1] text-[13px] text-text-2 font-500 hover:bg-white/[0.1] transition-colors"
|
|
703
|
+
>
|
|
704
|
+
Approve in Dashboard →
|
|
705
|
+
</a>
|
|
706
|
+
</>) : testErrorCode === 'AUTH_TOKEN_MISSING' ? (<>
|
|
707
|
+
<p className="text-[14px] text-accent-bright font-600">Token Required</p>
|
|
708
|
+
<p className="text-[13px] text-text-2/80 leading-[1.6]">
|
|
709
|
+
This gateway requires an auth token. Add one above and try again.
|
|
710
|
+
</p>
|
|
711
|
+
</>) : testErrorCode === 'AUTH_TOKEN_INVALID' ? (<>
|
|
712
|
+
<p className="text-[14px] text-accent-bright font-600">Invalid Token</p>
|
|
713
|
+
<p className="text-[13px] text-text-2/80 leading-[1.6]">
|
|
714
|
+
The gateway rejected this token. Check that it matches the one configured on your OpenClaw instance.
|
|
715
|
+
</p>
|
|
716
|
+
</>) : (<>
|
|
717
|
+
<p className="text-[14px] text-accent-bright font-600">Connection Failed</p>
|
|
718
|
+
<p className="text-[13px] text-text-2/80 leading-[1.6]">
|
|
719
|
+
{testMessage || 'Could not reach the gateway. Check the URL, token, and that the gateway is running.'}
|
|
720
|
+
</p>
|
|
721
|
+
</>)}
|
|
722
|
+
{/* Device ID footer — always shown on failure for debugging */}
|
|
723
|
+
{(testDeviceId || openclawDeviceId) && testErrorCode !== 'AUTH_TOKEN_MISSING' && testErrorCode !== 'AUTH_TOKEN_INVALID' && (
|
|
724
|
+
<div className="pt-2 border-t border-white/[0.04]">
|
|
725
|
+
<p className="text-[12px] text-text-3/70 flex items-center gap-1.5">
|
|
726
|
+
Device <code className="font-mono text-text-2/70 select-all">{(testDeviceId || openclawDeviceId)}</code>
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
onClick={() => {
|
|
730
|
+
navigator.clipboard.writeText((testDeviceId || openclawDeviceId)!)
|
|
731
|
+
setConfigCopied(true)
|
|
732
|
+
setTimeout(() => setConfigCopied(false), 2000)
|
|
733
|
+
}}
|
|
734
|
+
className="text-[12px] text-text-3/60 hover:text-text-3/80 transition-colors cursor-pointer bg-transparent border-none"
|
|
735
|
+
>
|
|
736
|
+
{configCopied ? 'copied' : 'copy'}
|
|
737
|
+
</button>
|
|
738
|
+
</p>
|
|
739
|
+
</div>
|
|
740
|
+
)}
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
744
|
+
)}
|
|
701
745
|
|
|
702
746
|
{!openclawEnabled && <div className="mb-8">
|
|
703
747
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Provider</label>
|
|
@@ -883,7 +927,7 @@ export function AgentSheet() {
|
|
|
883
927
|
</label>
|
|
884
928
|
<input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
|
|
885
929
|
{provider === 'openclaw' && (
|
|
886
|
-
<p className="text-[
|
|
930
|
+
<p className="text-[13px] text-text-3/70 mt-2">The URL of your OpenClaw gateway</p>
|
|
887
931
|
)}
|
|
888
932
|
</div>
|
|
889
933
|
)}
|
|
@@ -952,17 +996,15 @@ export function AgentSheet() {
|
|
|
952
996
|
</div>
|
|
953
997
|
)}
|
|
954
998
|
|
|
955
|
-
{/* Native capability provider note */}
|
|
956
|
-
{hasNativeCapabilities && (
|
|
999
|
+
{/* Native capability provider note — not shown for OpenClaw (covered in connection status) */}
|
|
1000
|
+
{hasNativeCapabilities && !openclawEnabled && (
|
|
957
1001
|
<div className="mb-8 p-4 rounded-[14px] bg-white/[0.02] border border-white/[0.06]">
|
|
958
1002
|
<p className="text-[13px] text-text-3">
|
|
959
|
-
{provider === '
|
|
960
|
-
? '
|
|
961
|
-
: provider === '
|
|
962
|
-
? '
|
|
963
|
-
:
|
|
964
|
-
? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
|
|
965
|
-
: 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
|
|
1003
|
+
{provider === 'claude-cli'
|
|
1004
|
+
? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
|
|
1005
|
+
: provider === 'codex-cli'
|
|
1006
|
+
? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
|
|
1007
|
+
: 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
|
|
966
1008
|
</p>
|
|
967
1009
|
</div>
|
|
968
1010
|
)}
|
|
@@ -1171,13 +1213,13 @@ export function AgentSheet() {
|
|
|
1171
1213
|
</div>
|
|
1172
1214
|
)}
|
|
1173
1215
|
|
|
1174
|
-
{/* Test connection result */}
|
|
1175
|
-
{testStatus === 'fail' && (
|
|
1216
|
+
{/* Test connection result (hidden for OpenClaw — inline status block handles it) */}
|
|
1217
|
+
{!openclawEnabled && testStatus === 'fail' && (
|
|
1176
1218
|
<div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
|
|
1177
1219
|
<p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
|
|
1178
1220
|
</div>
|
|
1179
1221
|
)}
|
|
1180
|
-
{testStatus === 'pass' && (
|
|
1222
|
+
{!openclawEnabled && testStatus === 'pass' && (
|
|
1181
1223
|
<div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
|
|
1182
1224
|
<p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
|
|
1183
1225
|
</div>
|
|
@@ -1207,12 +1249,18 @@ export function AgentSheet() {
|
|
|
1207
1249
|
</button>
|
|
1208
1250
|
<button
|
|
1209
1251
|
onClick={handleTestAndSave}
|
|
1210
|
-
disabled={!name.trim() || providerNeedsKey || testStatus === 'testing' || testStatus === 'pass'
|
|
1252
|
+
disabled={!name.trim() || providerNeedsKey || testStatus === 'testing' || saving || (!openclawEnabled && testStatus === 'pass')}
|
|
1211
1253
|
className={`flex-1 py-3.5 rounded-[14px] border-none text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-60 transition-all hover:brightness-110
|
|
1212
1254
|
${testStatus === 'pass' ? 'bg-emerald-600 shadow-[0_4px_20px_rgba(16,185,129,0.25)]' : 'bg-[#6366F1] shadow-[0_4px_20px_rgba(99,102,241,0.25)]'}`}
|
|
1213
1255
|
style={{ fontFamily: 'inherit' }}
|
|
1214
1256
|
>
|
|
1215
|
-
{
|
|
1257
|
+
{openclawEnabled
|
|
1258
|
+
? (testStatus === 'testing' ? 'Connecting...'
|
|
1259
|
+
: testStatus === 'pass' ? (saving ? 'Saving...' : 'Save')
|
|
1260
|
+
: testStatus === 'fail' && testErrorCode === 'PAIRING_REQUIRED' ? 'Retry Connection'
|
|
1261
|
+
: testStatus === 'fail' ? 'Retry'
|
|
1262
|
+
: 'Connect')
|
|
1263
|
+
: (testStatus === 'testing' ? 'Testing...' : testStatus === 'pass' ? (saving ? 'Saving...' : 'Connected!') : needsTest ? 'Test & Save' : editing ? 'Save' : 'Create')}
|
|
1216
1264
|
</button>
|
|
1217
1265
|
</div>
|
|
1218
1266
|
</BottomSheet>
|
|
@@ -65,7 +65,8 @@ const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
65
65
|
id: 'openclaw',
|
|
66
66
|
name: 'OpenClaw',
|
|
67
67
|
models: ['default'],
|
|
68
|
-
requiresApiKey:
|
|
68
|
+
requiresApiKey: false,
|
|
69
|
+
optionalApiKey: true,
|
|
69
70
|
requiresEndpoint: true,
|
|
70
71
|
defaultEndpoint: 'http://localhost:18789',
|
|
71
72
|
handler: { streamChat: streamOpenClawChat },
|
|
@@ -30,23 +30,50 @@ interface DeviceIdentity {
|
|
|
30
30
|
privateKeyPem: string
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/** Resolve the openclaw CLI's state directory (~/.openclaw by default). */
|
|
34
|
+
function resolveCliStateDir(): string {
|
|
35
|
+
const override = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
|
|
36
|
+
if (override) return path.resolve(override.replace(/^~/, process.env.HOME || ''))
|
|
37
|
+
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
38
|
+
// Check new path first, then legacy
|
|
39
|
+
const newDir = path.join(home, '.openclaw')
|
|
40
|
+
if (fs.existsSync(newDir)) return newDir
|
|
41
|
+
const legacyDir = path.join(home, '.clawdbot')
|
|
42
|
+
if (fs.existsSync(legacyDir)) return legacyDir
|
|
43
|
+
return newDir
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getSwarmClawIdentityPath(): string {
|
|
34
47
|
const dataDir = path.join(process.cwd(), 'data')
|
|
35
48
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
|
36
49
|
return path.join(dataDir, 'openclaw-device.json')
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
function
|
|
40
|
-
const filePath = getIdentityPath()
|
|
52
|
+
function tryLoadIdentityFile(filePath: string): DeviceIdentity | null {
|
|
41
53
|
try {
|
|
42
|
-
if (fs.existsSync(filePath))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
if (!fs.existsSync(filePath)) return null
|
|
55
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
56
|
+
if (parsed?.publicKeyPem && parsed?.privateKeyPem) {
|
|
57
|
+
// Re-derive deviceId from public key (matches CLI behavior)
|
|
58
|
+
const derivedId = fingerprintPublicKey(parsed.publicKeyPem)
|
|
59
|
+
return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem }
|
|
47
60
|
}
|
|
48
61
|
} catch {}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
49
64
|
|
|
65
|
+
function loadOrCreateDeviceIdentity(): DeviceIdentity {
|
|
66
|
+
// 1. Prefer the openclaw CLI's identity — it's likely already paired with the gateway
|
|
67
|
+
const cliIdentityPath = path.join(resolveCliStateDir(), 'identity', 'device.json')
|
|
68
|
+
const cliIdentity = tryLoadIdentityFile(cliIdentityPath)
|
|
69
|
+
if (cliIdentity) return cliIdentity
|
|
70
|
+
|
|
71
|
+
// 2. Fall back to SwarmClaw's own identity
|
|
72
|
+
const swarmClawPath = getSwarmClawIdentityPath()
|
|
73
|
+
const existing = tryLoadIdentityFile(swarmClawPath)
|
|
74
|
+
if (existing) return existing
|
|
75
|
+
|
|
76
|
+
// 3. Generate a new identity
|
|
50
77
|
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
|
|
51
78
|
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }) as string
|
|
52
79
|
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
|
|
@@ -55,7 +82,7 @@ function loadOrCreateDeviceIdentity(): DeviceIdentity {
|
|
|
55
82
|
publicKeyPem,
|
|
56
83
|
privateKeyPem,
|
|
57
84
|
}
|
|
58
|
-
fs.writeFileSync(
|
|
85
|
+
fs.writeFileSync(swarmClawPath, JSON.stringify({ version: 1, ...identity }, null, 2) + '\n', { mode: 0o600 })
|
|
59
86
|
return identity
|
|
60
87
|
}
|
|
61
88
|
|
|
@@ -67,7 +94,7 @@ export function getDeviceId(): string {
|
|
|
67
94
|
// --- Protocol helpers ---
|
|
68
95
|
|
|
69
96
|
function normalizeWsUrl(raw: string): string {
|
|
70
|
-
let url = raw.replace(/\/+$/, '')
|
|
97
|
+
let url = raw.replace(/\/+$/, '')
|
|
71
98
|
if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
|
|
72
99
|
url = url.replace(/^ws:/i, 'http:').replace(/^wss:/i, 'https:')
|
|
73
100
|
return url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
|
|
@@ -92,7 +119,7 @@ export function buildOpenClawConnectParams(
|
|
|
92
119
|
const scopes = ['operator.admin']
|
|
93
120
|
|
|
94
121
|
const params: Record<string, unknown> = {
|
|
95
|
-
minProtocol:
|
|
122
|
+
minProtocol: 3,
|
|
96
123
|
maxProtocol: 3,
|
|
97
124
|
auth: token ? { token } : undefined,
|
|
98
125
|
client: {
|
|
@@ -134,7 +161,7 @@ export function buildOpenClawConnectParams(
|
|
|
134
161
|
|
|
135
162
|
// --- Gateway connection ---
|
|
136
163
|
|
|
137
|
-
interface ConnectResult {
|
|
164
|
+
export interface ConnectResult {
|
|
138
165
|
ok: boolean
|
|
139
166
|
message: string
|
|
140
167
|
errorCode?: string
|
|
@@ -145,7 +172,7 @@ interface ConnectResult {
|
|
|
145
172
|
* Open a WebSocket and complete the connect handshake.
|
|
146
173
|
* Resolves with { ok, ws } on success or { ok: false, message, errorCode } on failure.
|
|
147
174
|
*/
|
|
148
|
-
function wsConnect(
|
|
175
|
+
export function wsConnect(
|
|
149
176
|
wsUrl: string,
|
|
150
177
|
token: string | undefined,
|
|
151
178
|
useDeviceAuth: boolean,
|
|
@@ -185,11 +212,16 @@ function wsConnect(
|
|
|
185
212
|
if (msg.ok) {
|
|
186
213
|
done({ ok: true, message: 'Connected.', ws })
|
|
187
214
|
} else {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
const message = msg.error?.message || 'Gateway connect failed.'
|
|
216
|
+
let errorCode = (msg.error?.details?.code ?? msg.error?.code) as string | undefined
|
|
217
|
+
if (!errorCode) {
|
|
218
|
+
const m = message.toLowerCase()
|
|
219
|
+
if (m.includes('pairing') || m.includes('not paired') || m.includes('pending approval')) errorCode = 'PAIRING_REQUIRED'
|
|
220
|
+
else if (m.includes('signature') || m.includes('device auth')) errorCode = 'DEVICE_AUTH_INVALID'
|
|
221
|
+
else if (m.includes('token missing') || m.includes('token required')) errorCode = 'AUTH_TOKEN_MISSING'
|
|
222
|
+
else if (m.includes('unauthorized') || m.includes('invalid token')) errorCode = 'AUTH_TOKEN_INVALID'
|
|
223
|
+
}
|
|
224
|
+
done({ ok: false, message, errorCode })
|
|
193
225
|
}
|
|
194
226
|
}
|
|
195
227
|
} catch {
|
|
@@ -202,8 +234,15 @@ function wsConnect(
|
|
|
202
234
|
})
|
|
203
235
|
|
|
204
236
|
ws.on('close', (code, reason) => {
|
|
237
|
+
const reasonStr = reason?.toString() || ''
|
|
205
238
|
if (code === 1008) {
|
|
206
|
-
|
|
239
|
+
const m = reasonStr.toLowerCase()
|
|
240
|
+
let errorCode: string | undefined
|
|
241
|
+
if (m.includes('pairing') || m.includes('not paired') || m.includes('pending approval')) errorCode = 'PAIRING_REQUIRED'
|
|
242
|
+
else if (m.includes('signature') || m.includes('device auth') || m.includes('device identity') || m.includes('device nonce')) errorCode = 'DEVICE_AUTH_INVALID'
|
|
243
|
+
else if (m.includes('token missing') || m.includes('token required')) errorCode = 'AUTH_TOKEN_MISSING'
|
|
244
|
+
else if (m.includes('unauthorized') || m.includes('invalid token')) errorCode = 'AUTH_TOKEN_INVALID'
|
|
245
|
+
done({ ok: false, message: reasonStr || 'Unauthorized', errorCode })
|
|
207
246
|
} else {
|
|
208
247
|
done({ ok: false, message: `Connection closed unexpectedly (${code})` })
|
|
209
248
|
}
|
|
@@ -228,14 +267,14 @@ async function connectToGateway(
|
|
|
228
267
|
|
|
229
268
|
// --- Provider ---
|
|
230
269
|
|
|
231
|
-
export function streamOpenClawChat({ session, message, imagePath, write, active }: StreamChatOptions): Promise<string> {
|
|
270
|
+
export function streamOpenClawChat({ session, message, imagePath, apiKey, write, active }: StreamChatOptions): Promise<string> {
|
|
232
271
|
let prompt = message
|
|
233
272
|
if (imagePath) {
|
|
234
273
|
prompt = `[The user has shared an image at: ${imagePath}]\n\n${message}`
|
|
235
274
|
}
|
|
236
275
|
|
|
237
276
|
const wsUrl = session.apiEndpoint ? normalizeWsUrl(session.apiEndpoint) : 'ws://127.0.0.1:18789'
|
|
238
|
-
const token = session.apiKey || undefined
|
|
277
|
+
const token = apiKey || session.apiKey || undefined
|
|
239
278
|
|
|
240
279
|
return new Promise((resolve) => {
|
|
241
280
|
let fullResponse = ''
|