@swarmclawai/swarmclaw 0.4.0 → 0.5.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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
import { mergeHistoryMessages, isValidSessionKey } from '@/lib/server/openclaw-history-merge'
|
|
4
|
+
import { loadSessions, saveSessions } from '@/lib/server/storage'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import type { GatewaySessionPreview } from '@/types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract a single session preview from the gateway response.
|
|
10
|
+
* The gateway may return:
|
|
11
|
+
* - A map: { [sessionKey]: preview }
|
|
12
|
+
* - An array: [preview, ...]
|
|
13
|
+
* - A single object with sessionKey field
|
|
14
|
+
*/
|
|
15
|
+
function extractPreview(
|
|
16
|
+
raw: unknown,
|
|
17
|
+
sessionKey: string,
|
|
18
|
+
): GatewaySessionPreview | undefined {
|
|
19
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
20
|
+
|
|
21
|
+
// Direct object with messages array
|
|
22
|
+
if ('messages' in (raw as Record<string, unknown>)) {
|
|
23
|
+
return raw as GatewaySessionPreview
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Map keyed by session key
|
|
27
|
+
const asMap = raw as Record<string, unknown>
|
|
28
|
+
if (asMap[sessionKey] && typeof asMap[sessionKey] === 'object') {
|
|
29
|
+
return asMap[sessionKey] as GatewaySessionPreview
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Array — find matching session
|
|
33
|
+
if (Array.isArray(raw)) {
|
|
34
|
+
return raw.find(
|
|
35
|
+
(p: unknown) =>
|
|
36
|
+
p && typeof p === 'object' && (p as GatewaySessionPreview).sessionKey === sessionKey,
|
|
37
|
+
) as GatewaySessionPreview | undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** GET ?sessionKey=X — preview gateway session history */
|
|
44
|
+
export async function GET(req: Request) {
|
|
45
|
+
const { searchParams } = new URL(req.url)
|
|
46
|
+
const sessionKey = searchParams.get('sessionKey')
|
|
47
|
+
if (!sessionKey || !isValidSessionKey(sessionKey)) {
|
|
48
|
+
return NextResponse.json({ error: 'Missing or invalid sessionKey' }, { status: 400 })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const gw = await ensureGatewayConnected()
|
|
52
|
+
if (!gw) {
|
|
53
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const raw = await gw.rpc('sessions.preview', { keys: [sessionKey], limit: 100 })
|
|
58
|
+
const preview = extractPreview(raw, sessionKey)
|
|
59
|
+
return NextResponse.json(preview ?? { sessionKey, epoch: 0, messages: [] })
|
|
60
|
+
} catch (err: unknown) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
62
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** POST { sessionKey, epoch, localSessionId } — merge gateway history into local session */
|
|
67
|
+
export async function POST(req: Request) {
|
|
68
|
+
const body = await req.json()
|
|
69
|
+
const { sessionKey, localSessionId } = body as {
|
|
70
|
+
sessionKey?: string
|
|
71
|
+
epoch?: number
|
|
72
|
+
localSessionId?: string
|
|
73
|
+
}
|
|
74
|
+
if (!sessionKey || !localSessionId) {
|
|
75
|
+
return NextResponse.json({ error: 'Missing sessionKey or localSessionId' }, { status: 400 })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const gw = await ensureGatewayConnected()
|
|
79
|
+
if (!gw) {
|
|
80
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const raw = await gw.rpc('sessions.preview', { keys: [sessionKey], limit: 100 })
|
|
85
|
+
const preview = extractPreview(raw, sessionKey)
|
|
86
|
+
if (!preview?.messages?.length) {
|
|
87
|
+
return NextResponse.json({ ok: true, merged: 0 })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sessions = loadSessions()
|
|
91
|
+
const session = sessions[localSessionId]
|
|
92
|
+
if (!session) {
|
|
93
|
+
return NextResponse.json({ error: 'Local session not found' }, { status: 404 })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const merged = mergeHistoryMessages(session.messages, preview)
|
|
97
|
+
const newCount = merged.length - session.messages.length
|
|
98
|
+
session.messages = merged
|
|
99
|
+
session.lastActiveAt = Date.now()
|
|
100
|
+
sessions[localSessionId] = session
|
|
101
|
+
saveSessions(sessions)
|
|
102
|
+
notify('sessions')
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({ ok: true, merged: newCount })
|
|
105
|
+
} catch (err: unknown) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
107
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
import { lookup } from 'mime-types'
|
|
4
|
+
|
|
5
|
+
const MAX_SIZE = 25 * 1024 * 1024 // 25MB
|
|
6
|
+
|
|
7
|
+
/** GET ?path=... — proxy agent files (images etc.) from gateway */
|
|
8
|
+
export async function GET(req: Request) {
|
|
9
|
+
const { searchParams } = new URL(req.url)
|
|
10
|
+
const filePath = searchParams.get('path')
|
|
11
|
+
if (!filePath) {
|
|
12
|
+
return NextResponse.json({ error: 'Missing path' }, { status: 400 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Security: path must be under ~/.openclaw/
|
|
16
|
+
if (!filePath.includes('.openclaw') && !filePath.includes('.clawdbot')) {
|
|
17
|
+
return NextResponse.json({ error: 'Path not allowed' }, { status: 403 })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const gw = await ensureGatewayConnected()
|
|
21
|
+
if (!gw) {
|
|
22
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result = await gw.rpc('files.read', { path: filePath }) as { content?: string; encoding?: string } | undefined
|
|
27
|
+
if (!result?.content) {
|
|
28
|
+
return NextResponse.json({ error: 'File not found' }, { status: 404 })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isBase64 = result.encoding === 'base64'
|
|
32
|
+
const buf = isBase64
|
|
33
|
+
? Buffer.from(result.content, 'base64')
|
|
34
|
+
: Buffer.from(result.content, 'utf-8')
|
|
35
|
+
|
|
36
|
+
if (buf.length > MAX_SIZE) {
|
|
37
|
+
return NextResponse.json({ error: 'File too large' }, { status: 413 })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mimeType = lookup(filePath) || 'application/octet-stream'
|
|
41
|
+
|
|
42
|
+
return new NextResponse(buf, {
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': mimeType,
|
|
45
|
+
'Content-Length': String(buf.length),
|
|
46
|
+
'Cache-Control': 'public, max-age=300',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
} catch (err: unknown) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
51
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { fetchGatewayModelPolicy, buildAllowedModelKeys } from '@/lib/server/openclaw-models'
|
|
3
|
+
|
|
4
|
+
/** GET — fetch allowed models for OpenClaw agents from gateway policy */
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const policy = await fetchGatewayModelPolicy()
|
|
7
|
+
const models = buildAllowedModelKeys(policy)
|
|
8
|
+
return NextResponse.json({
|
|
9
|
+
models: models ?? ['default'],
|
|
10
|
+
defaultModel: policy?.defaultModel ?? 'default',
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { PermissionPreset } from '@/types'
|
|
3
|
+
import { getExecConfig } from '@/lib/server/openclaw-exec-config'
|
|
4
|
+
import { resolvePresetFromConfig, applyPreset } from '@/lib/server/openclaw-permission-presets'
|
|
5
|
+
|
|
6
|
+
/** GET ?agentId=X — resolve current permission preset */
|
|
7
|
+
export async function GET(req: Request) {
|
|
8
|
+
const { searchParams } = new URL(req.url)
|
|
9
|
+
const agentId = searchParams.get('agentId')
|
|
10
|
+
if (!agentId) {
|
|
11
|
+
return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const snap = await getExecConfig(agentId)
|
|
16
|
+
const preset = resolvePresetFromConfig(snap.file)
|
|
17
|
+
return NextResponse.json({ preset, config: snap.file })
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
20
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** PUT { agentId, preset } — apply a permission preset */
|
|
25
|
+
export async function PUT(req: Request) {
|
|
26
|
+
const body = await req.json()
|
|
27
|
+
const { agentId, preset } = body as { agentId?: string; preset?: PermissionPreset }
|
|
28
|
+
if (!agentId || !preset) {
|
|
29
|
+
return NextResponse.json({ error: 'Missing agentId or preset' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await applyPreset(agentId, preset)
|
|
34
|
+
return NextResponse.json({ ok: true })
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
37
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
|
|
4
|
+
/** GET — list available and allowed env keys for sandbox */
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const gw = await ensureGatewayConnected()
|
|
7
|
+
if (!gw) {
|
|
8
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Get available keys from dotenv
|
|
13
|
+
const available = await gw.rpc('env.keys') as string[] | undefined
|
|
14
|
+
|
|
15
|
+
// Get current config to find allowed keys
|
|
16
|
+
const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
|
|
17
|
+
const agents = (config as Record<string, unknown>)?.agents as Record<string, unknown> | undefined
|
|
18
|
+
const defaults = agents?.defaults as Record<string, unknown> | undefined
|
|
19
|
+
const sandbox = defaults?.sandbox as Record<string, unknown> | undefined
|
|
20
|
+
const docker = sandbox?.docker as Record<string, unknown> | undefined
|
|
21
|
+
const envList = docker?.env as string[] | undefined
|
|
22
|
+
|
|
23
|
+
// Parse allowed keys from ${KEY} format
|
|
24
|
+
const allowed = (envList ?? [])
|
|
25
|
+
.map((entry) => {
|
|
26
|
+
const m = entry.match(/^\$\{(.+)\}$/)
|
|
27
|
+
return m ? m[1] : entry
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ available: available ?? [], allowed })
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
33
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** PUT { allowed: string[] } — update sandbox env allowlist */
|
|
38
|
+
export async function PUT(req: Request) {
|
|
39
|
+
const body = await req.json()
|
|
40
|
+
const { allowed } = body as { allowed?: string[] }
|
|
41
|
+
if (!allowed || !Array.isArray(allowed)) {
|
|
42
|
+
return NextResponse.json({ error: 'Missing allowed array' }, { status: 400 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const gw = await ensureGatewayConnected()
|
|
46
|
+
if (!gw) {
|
|
47
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Format as ${KEY} for gateway config
|
|
52
|
+
const envEntries = allowed.map((key) => `\${${key}}`)
|
|
53
|
+
|
|
54
|
+
// Fetch current config hash for conflict detection
|
|
55
|
+
const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
|
|
56
|
+
const configHash = (config as Record<string, unknown>)?._hash as string | undefined
|
|
57
|
+
|
|
58
|
+
await gw.rpc('config.set', {
|
|
59
|
+
key: 'agents.defaults.sandbox.docker.env',
|
|
60
|
+
value: envEntries,
|
|
61
|
+
baseHash: configHash,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return NextResponse.json({ ok: true })
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
67
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
|
|
4
|
+
/** POST { name, installId, timeoutMs? } — install a skill via gateway */
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
const body = await req.json()
|
|
7
|
+
const { name, installId, timeoutMs } = body as {
|
|
8
|
+
name?: string
|
|
9
|
+
installId?: string
|
|
10
|
+
timeoutMs?: number
|
|
11
|
+
}
|
|
12
|
+
if (!name) {
|
|
13
|
+
return NextResponse.json({ error: 'Missing skill name' }, { status: 400 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const gw = await ensureGatewayConnected()
|
|
17
|
+
if (!gw) {
|
|
18
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const result = await gw.rpc('skills.install', {
|
|
23
|
+
name,
|
|
24
|
+
installId,
|
|
25
|
+
timeoutMs: timeoutMs ?? 120_000,
|
|
26
|
+
}, (timeoutMs ?? 120_000) + 5_000)
|
|
27
|
+
return NextResponse.json({ ok: true, result })
|
|
28
|
+
} catch (err: unknown) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
30
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
|
|
4
|
+
/** POST { skillKey, source } — remove a skill via gateway */
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
const body = await req.json()
|
|
7
|
+
const { skillKey, source } = body as { skillKey?: string; source?: string }
|
|
8
|
+
if (!skillKey) {
|
|
9
|
+
return NextResponse.json({ error: 'Missing skillKey' }, { status: 400 })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const gw = await ensureGatewayConnected()
|
|
13
|
+
if (!gw) {
|
|
14
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await gw.rpc('skills.remove', { skillKey, source })
|
|
19
|
+
return NextResponse.json({ ok: true })
|
|
20
|
+
} catch (err: unknown) {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
22
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
import { loadAgents, saveAgents } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
|
|
6
|
+
|
|
7
|
+
/** GET ?agentId=X — fetch skills from gateway with eligibility */
|
|
8
|
+
export async function GET(req: Request) {
|
|
9
|
+
const { searchParams } = new URL(req.url)
|
|
10
|
+
const agentId = searchParams.get('agentId')
|
|
11
|
+
if (!agentId) {
|
|
12
|
+
return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const gw = await ensureGatewayConnected()
|
|
16
|
+
if (!gw) {
|
|
17
|
+
return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await gw.rpc('skills.status', { agentId }) as OpenClawSkillEntry[] | undefined
|
|
22
|
+
return NextResponse.json(result ?? [])
|
|
23
|
+
} catch (err: unknown) {
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
25
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** PATCH { skillKey, enabled?, apiKey? } — update a skill's config on gateway */
|
|
30
|
+
export async function PATCH(req: Request) {
|
|
31
|
+
const body = await req.json()
|
|
32
|
+
const { skillKey, enabled, apiKey } = body as {
|
|
33
|
+
skillKey?: string
|
|
34
|
+
enabled?: boolean
|
|
35
|
+
apiKey?: string
|
|
36
|
+
}
|
|
37
|
+
if (!skillKey) {
|
|
38
|
+
return NextResponse.json({ error: 'Missing skillKey' }, { status: 400 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gw = await ensureGatewayConnected()
|
|
42
|
+
if (!gw) {
|
|
43
|
+
return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await gw.rpc('skills.update', { skillKey, enabled, apiKey })
|
|
48
|
+
return NextResponse.json({ ok: true })
|
|
49
|
+
} catch (err: unknown) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
51
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** PUT { agentId, mode, allowedSkills } — save skill allowlist config to agent */
|
|
56
|
+
export async function PUT(req: Request) {
|
|
57
|
+
const body = await req.json()
|
|
58
|
+
const { agentId, mode, allowedSkills } = body as {
|
|
59
|
+
agentId?: string
|
|
60
|
+
mode?: SkillAllowlistMode
|
|
61
|
+
allowedSkills?: string[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!agentId || !mode) {
|
|
65
|
+
return NextResponse.json({ error: 'Missing agentId or mode' }, { status: 400 })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const agents = loadAgents({ includeTrashed: true })
|
|
69
|
+
const agent = agents[agentId]
|
|
70
|
+
if (!agent) {
|
|
71
|
+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
agent.openclawSkillMode = mode
|
|
75
|
+
agent.openclawAllowedSkills = mode === 'selected' ? (allowedSkills ?? []) : undefined
|
|
76
|
+
agent.updatedAt = Date.now()
|
|
77
|
+
agents[agentId] = agent
|
|
78
|
+
saveAgents(agents)
|
|
79
|
+
notify('agents')
|
|
80
|
+
|
|
81
|
+
return NextResponse.json({ ok: true })
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { runSync, type SyncType } from '@/lib/server/openclaw-sync'
|
|
3
|
+
export const dynamic = 'force-dynamic'
|
|
4
|
+
|
|
5
|
+
const VALID_ACTIONS = new Set(['push', 'pull', 'both'])
|
|
6
|
+
const VALID_TYPES: SyncType[] = ['memory', 'workspace', 'schedules', 'credentials', 'plugins']
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request) {
|
|
9
|
+
try {
|
|
10
|
+
const body = await req.json()
|
|
11
|
+
const action = body.action
|
|
12
|
+
const types = body.types
|
|
13
|
+
|
|
14
|
+
if (!action || !VALID_ACTIONS.has(action)) {
|
|
15
|
+
return NextResponse.json({ error: 'Invalid action. Use push, pull, or both.' }, { status: 400 })
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(types) || types.length === 0) {
|
|
18
|
+
return NextResponse.json({ error: 'types must be a non-empty array.' }, { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
const validTypes = types.filter((t: string) => VALID_TYPES.includes(t as SyncType)) as SyncType[]
|
|
21
|
+
if (validTypes.length === 0) {
|
|
22
|
+
return NextResponse.json({ error: `No valid types. Use: ${VALID_TYPES.join(', ')}` }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const results = await runSync({ action, types: validTypes })
|
|
26
|
+
return NextResponse.json({ ok: true, results })
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
const message = err instanceof Error ? err.message : 'Sync failed'
|
|
29
|
+
return NextResponse.json({ error: message }, { status: 500 })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
|
|
4
4
|
import { enqueueTask } from '@/lib/server/queue'
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@ export async function POST(req: Request) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// Create a board task and enqueue it
|
|
19
|
-
const taskId =
|
|
19
|
+
const taskId = genId()
|
|
20
20
|
const now = Date.now()
|
|
21
21
|
const tasks = loadTasks()
|
|
22
22
|
tasks[taskId] = {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills } from '@/lib/server/storage'
|
|
3
|
+
import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
const ops: CollectionOps<any> = { load: loadProjects, save: saveProjects, deleteFn: deleteProject, topic: 'projects' }
|
|
8
|
+
|
|
9
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
const projects = loadProjects()
|
|
12
|
+
if (!projects[id]) return notFound()
|
|
13
|
+
return NextResponse.json(projects[id])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
17
|
+
const { id } = await params
|
|
18
|
+
const body = await req.json()
|
|
19
|
+
const result = mutateItem(ops, id, (project) => {
|
|
20
|
+
Object.assign(project, body, { updatedAt: Date.now() })
|
|
21
|
+
delete (project as Record<string, unknown>).id
|
|
22
|
+
project.id = id
|
|
23
|
+
return project
|
|
24
|
+
})
|
|
25
|
+
if (!result) return notFound()
|
|
26
|
+
return NextResponse.json(result)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
30
|
+
const { id } = await params
|
|
31
|
+
if (!deleteItem(ops, id)) return notFound()
|
|
32
|
+
|
|
33
|
+
// Clear projectId from referencing entities
|
|
34
|
+
const clearProjectId = (load: () => Record<string, Record<string, unknown>>, save: (d: Record<string, Record<string, unknown>>) => void, topic: string) => {
|
|
35
|
+
const items = load()
|
|
36
|
+
let changed = false
|
|
37
|
+
for (const item of Object.values(items)) {
|
|
38
|
+
if (item.projectId === id) {
|
|
39
|
+
item.projectId = undefined
|
|
40
|
+
changed = true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (changed) {
|
|
44
|
+
save(items)
|
|
45
|
+
notify(topic)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clearProjectId(loadAgents, saveAgents, 'agents')
|
|
50
|
+
clearProjectId(loadTasks, saveTasks, 'tasks')
|
|
51
|
+
clearProjectId(loadSchedules, saveSchedules, 'schedules')
|
|
52
|
+
clearProjectId(loadSkills, saveSkills, 'skills')
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({ ok: true })
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadProjects, saveProjects } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
return NextResponse.json(loadProjects())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
const body = await req.json()
|
|
13
|
+
const id = genId()
|
|
14
|
+
const now = Date.now()
|
|
15
|
+
const projects = loadProjects()
|
|
16
|
+
projects[id] = {
|
|
17
|
+
id,
|
|
18
|
+
name: body.name || 'Unnamed Project',
|
|
19
|
+
description: body.description || '',
|
|
20
|
+
color: body.color || undefined,
|
|
21
|
+
createdAt: now,
|
|
22
|
+
updatedAt: now,
|
|
23
|
+
}
|
|
24
|
+
saveProjects(projects)
|
|
25
|
+
notify('projects')
|
|
26
|
+
return NextResponse.json(projects[id])
|
|
27
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadModelOverrides, saveModelOverrides } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
4
|
import { getProviderList } from '@/lib/providers'
|
|
4
5
|
|
|
5
6
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -7,7 +8,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
7
8
|
const overrides = loadModelOverrides()
|
|
8
9
|
const providers = getProviderList()
|
|
9
10
|
const provider = providers.find((p) => p.id === id)
|
|
10
|
-
if (!provider) return
|
|
11
|
+
if (!provider) return notFound()
|
|
11
12
|
return NextResponse.json({ models: provider.models, hasOverride: !!overrides[id] })
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -1,37 +1,35 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
|
|
3
|
-
import {
|
|
3
|
+
import { mutateItem, deleteItem, notFound, badRequest, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
const ops: CollectionOps<any> = { load: loadProviderConfigs, save: saveProviderConfigs, topic: 'providers' }
|
|
4
7
|
|
|
5
8
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
9
|
const { id } = await params
|
|
7
10
|
const configs = loadProviderConfigs()
|
|
8
11
|
const config = configs[id]
|
|
9
|
-
if (!config) return
|
|
12
|
+
if (!config) return notFound()
|
|
10
13
|
return NextResponse.json(config)
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
17
|
const { id } = await params
|
|
15
18
|
const body = await req.json()
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
notify('providers')
|
|
22
|
-
return NextResponse.json(configs[id])
|
|
19
|
+
const result = mutateItem(ops, id, (existing) => ({
|
|
20
|
+
...existing, ...body, id, updatedAt: Date.now(),
|
|
21
|
+
}))
|
|
22
|
+
if (!result) return notFound()
|
|
23
|
+
return NextResponse.json(result)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
26
27
|
const { id } = await params
|
|
27
28
|
const configs = loadProviderConfigs()
|
|
28
|
-
if (!configs[id]) return
|
|
29
|
-
// Only allow deleting custom providers
|
|
29
|
+
if (!configs[id]) return notFound()
|
|
30
30
|
if (configs[id].type === 'builtin') {
|
|
31
|
-
return
|
|
31
|
+
return badRequest('Cannot delete built-in providers')
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
saveProviderConfigs(configs)
|
|
35
|
-
notify('providers')
|
|
33
|
+
if (!deleteItem(ops, id)) return notFound()
|
|
36
34
|
return NextResponse.json({ ok: true })
|
|
37
35
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
3
|
import { getProviderList } from '@/lib/providers'
|
|
4
4
|
import { loadProviderConfigs, saveProviderConfigs } from '@/lib/server/storage'
|
|
5
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
@@ -13,7 +13,7 @@ export async function GET(_req: Request) {
|
|
|
13
13
|
export async function POST(req: Request) {
|
|
14
14
|
const body = await req.json()
|
|
15
15
|
const configs = loadProviderConfigs()
|
|
16
|
-
const id = body.id || `custom-${
|
|
16
|
+
const id = body.id || `custom-${genId()}`
|
|
17
17
|
configs[id] = {
|
|
18
18
|
id,
|
|
19
19
|
name: body.name || 'Custom Provider',
|