@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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 +19 -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 +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- 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/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/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 +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
package/README.md
CHANGED
|
@@ -54,11 +54,15 @@ The deployment flow stays **in-house and official-only**:
|
|
|
54
54
|
|
|
55
55
|
Supported VPS presets currently include Hetzner, DigitalOcean, Vultr, Linode, Lightsail, Google Cloud, Azure, OCI, and a generic Ubuntu host path. Smart defaults prefill the gateway token, endpoint, storage paths, and copy-paste commands so the resulting gateway can be saved into SwarmClaw with minimal manual editing.
|
|
56
56
|
|
|
57
|
+
For existing hosts, SwarmClaw can also push the same official-image bundle **over SSH** and then keep remote lifecycle controls attached to that saved gateway profile: start, stop, restart, upgrade, backup, restore, and token rotation.
|
|
58
|
+
|
|
57
59
|
The OpenClaw Control Plane in SwarmClaw adds:
|
|
58
60
|
- Reload mode switching (`hot`, `hybrid`, `full`)
|
|
59
61
|
- Config issue detection and guided repair
|
|
60
62
|
- Remote history sync
|
|
61
63
|
- Live execution approval handling
|
|
64
|
+
- Gateway import/export JSON, clone flows, and richer external runtime fleet visibility
|
|
65
|
+
- Agent and route-target preferences for steering work toward OpenClaw gateways by tags or use case (`local-dev`, `single-vps`, `private-tailnet`, `browser-heavy`, `team-control`)
|
|
62
66
|
|
|
63
67
|
The Agent Inspector Panel lets you edit OpenClaw files (`SOUL.md`, `IDENTITY.md`, `USER.md`), tune personality/system behavior, and manage OpenClaw-compatible skills. SwarmClaw also supports importing OpenClaw `SKILL.md` files from URL.
|
|
64
68
|
|
|
@@ -79,8 +83,12 @@ CLI operators can use the same deploy surface without opening the UI:
|
|
|
79
83
|
```bash
|
|
80
84
|
swarmclaw openclaw deploy-status
|
|
81
85
|
swarmclaw openclaw deploy-local-start --data '{"port":18789}'
|
|
86
|
+
swarmclaw openclaw deploy-local-restart --data '{"port":18789}'
|
|
82
87
|
swarmclaw openclaw deploy-bundle --data '{"template":"docker","provider":"hetzner","target":"openclaw.example.com"}'
|
|
83
|
-
swarmclaw openclaw deploy-
|
|
88
|
+
swarmclaw openclaw deploy-ssh --data '{"target":"openclaw.example.com","provider":"hetzner","ssh":{"host":"your-vps-ip"}}'
|
|
89
|
+
swarmclaw openclaw remote-backup --data '{"ssh":{"host":"your-vps-ip"}}'
|
|
90
|
+
swarmclaw openclaw remote-restore --data '{"backupPath":"/opt/openclaw/backups/openclaw-backup-123.tgz","ssh":{"host":"your-vps-ip"}}'
|
|
91
|
+
swarmclaw openclaw deploy-verify --data '{"endpoint":"https://openclaw.example.com/v1"}'
|
|
84
92
|
```
|
|
85
93
|
|
|
86
94
|
## SwarmClaw ClawHub Skill
|
|
@@ -140,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
140
148
|
```
|
|
141
149
|
|
|
142
150
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
143
|
-
To pin a version: `SWARMCLAW_VERSION=v0.7.
|
|
151
|
+
To pin a version: `SWARMCLAW_VERSION=v0.7.8 curl ... | bash`
|
|
144
152
|
|
|
145
153
|
Or run locally from the repo (friendly for non-technical users):
|
|
146
154
|
|
|
@@ -693,7 +701,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
693
701
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
694
702
|
|
|
695
703
|
```bash
|
|
696
|
-
# example patch release (v0.7.
|
|
704
|
+
# example patch release (v0.7.8 style)
|
|
697
705
|
npm version patch
|
|
698
706
|
git push origin main --follow-tags
|
|
699
707
|
```
|
|
@@ -703,15 +711,16 @@ On `v*` tags, GitHub Actions will:
|
|
|
703
711
|
2. Create a GitHub Release
|
|
704
712
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
705
713
|
|
|
706
|
-
#### v0.7.
|
|
714
|
+
#### v0.7.8 Release Readiness Notes
|
|
707
715
|
|
|
708
|
-
Before shipping `v0.7.
|
|
716
|
+
Before shipping `v0.7.8`, confirm the following user-facing changes are reflected in docs:
|
|
709
717
|
|
|
710
|
-
1.
|
|
711
|
-
2.
|
|
712
|
-
3.
|
|
713
|
-
4.
|
|
714
|
-
5.
|
|
718
|
+
1. Project docs explain the new project operating-system fields: objective, audience, pilot priorities, open objectives, credential requirements, success metrics, and heartbeat prompt/interval.
|
|
719
|
+
2. Task and approval docs cover the new approval controls, task/project management toggles, and durable task continuation behavior (`continueFromTaskId`, dependency blocking, and session resume reuse).
|
|
720
|
+
3. Connector/operator docs mention automatic connector recovery on disconnect or dev-server restart, including bounded exponential backoff instead of silent disablement.
|
|
721
|
+
4. Chat/runtime docs note the project-aware agent context, Gemini resume-handle visibility, and improved web/connector input handling where relevant.
|
|
722
|
+
5. Site and README install/version strings are updated to `v0.7.8`, including install snippets, release notes index text, and sidebar/footer labels.
|
|
723
|
+
6. Release notes summarize the user-visible changes from the current worktree, especially project operating context, approval/task controls, connector resilience, and chat/runtime polish.
|
|
715
724
|
|
|
716
725
|
## CLI
|
|
717
726
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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
|
"publishConfig": {
|
|
@@ -23,6 +23,16 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
23
23
|
body.apiEndpoint,
|
|
24
24
|
)
|
|
25
25
|
}
|
|
26
|
+
if (body.preferredGatewayTags !== undefined) {
|
|
27
|
+
agent.preferredGatewayTags = Array.isArray(body.preferredGatewayTags)
|
|
28
|
+
? body.preferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
29
|
+
: []
|
|
30
|
+
}
|
|
31
|
+
if (body.preferredGatewayUseCase !== undefined) {
|
|
32
|
+
agent.preferredGatewayUseCase = typeof body.preferredGatewayUseCase === 'string' && body.preferredGatewayUseCase.trim()
|
|
33
|
+
? body.preferredGatewayUseCase.trim()
|
|
34
|
+
: null
|
|
35
|
+
}
|
|
26
36
|
if (body.routingTargets !== undefined && Array.isArray(body.routingTargets)) {
|
|
27
37
|
agent.routingTargets = body.routingTargets.map((target: Record<string, unknown>, index: number) => ({
|
|
28
38
|
id: typeof target.id === 'string' && target.id.trim() ? target.id.trim() : `route-${index + 1}`,
|
|
@@ -37,6 +47,12 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
37
47
|
typeof target.apiEndpoint === 'string' ? target.apiEndpoint : null,
|
|
38
48
|
),
|
|
39
49
|
gatewayProfileId: target.gatewayProfileId ?? null,
|
|
50
|
+
preferredGatewayTags: Array.isArray(target.preferredGatewayTags)
|
|
51
|
+
? target.preferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
52
|
+
: [],
|
|
53
|
+
preferredGatewayUseCase: typeof target.preferredGatewayUseCase === 'string' && target.preferredGatewayUseCase.trim()
|
|
54
|
+
? target.preferredGatewayUseCase.trim()
|
|
55
|
+
: null,
|
|
40
56
|
priority: typeof target.priority === 'number' ? target.priority : index + 1,
|
|
41
57
|
}))
|
|
42
58
|
}
|
|
@@ -64,6 +64,8 @@ export async function POST(req: Request) {
|
|
|
64
64
|
fallbackCredentialIds: body.fallbackCredentialIds,
|
|
65
65
|
apiEndpoint: normalizeProviderEndpoint(body.provider, body.apiEndpoint || null),
|
|
66
66
|
gatewayProfileId: body.gatewayProfileId,
|
|
67
|
+
preferredGatewayTags: body.preferredGatewayTags,
|
|
68
|
+
preferredGatewayUseCase: body.preferredGatewayUseCase,
|
|
67
69
|
routingStrategy: body.routingStrategy,
|
|
68
70
|
routingTargets: body.routingTargets?.map((target) => ({
|
|
69
71
|
...target,
|
|
@@ -18,7 +18,20 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
|
|
21
|
-
const
|
|
21
|
+
const routePreferredGatewayTags = updates.routePreferredGatewayTags !== undefined
|
|
22
|
+
? (Array.isArray(updates.routePreferredGatewayTags)
|
|
23
|
+
? updates.routePreferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
24
|
+
: [])
|
|
25
|
+
: (sessions[id].routePreferredGatewayTags || [])
|
|
26
|
+
const routePreferredGatewayUseCase = updates.routePreferredGatewayUseCase !== undefined
|
|
27
|
+
? (typeof updates.routePreferredGatewayUseCase === 'string' && updates.routePreferredGatewayUseCase.trim()
|
|
28
|
+
? updates.routePreferredGatewayUseCase.trim()
|
|
29
|
+
: null)
|
|
30
|
+
: (sessions[id].routePreferredGatewayUseCase || null)
|
|
31
|
+
const linkedRoute = linkedAgent ? resolvePrimaryAgentRoute(linkedAgent, undefined, {
|
|
32
|
+
preferredGatewayTags: routePreferredGatewayTags,
|
|
33
|
+
preferredGatewayUseCase: routePreferredGatewayUseCase,
|
|
34
|
+
}) : null
|
|
22
35
|
|
|
23
36
|
if (updates.name !== undefined) sessions[id].name = updates.name
|
|
24
37
|
if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
|
|
@@ -39,6 +52,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
39
52
|
if (updates.gatewayProfileId !== undefined) sessions[id].gatewayProfileId = updates.gatewayProfileId
|
|
40
53
|
else if (agentIdUpdateProvided && linkedRoute) sessions[id].gatewayProfileId = linkedRoute.gatewayProfileId ?? null
|
|
41
54
|
|
|
55
|
+
if (updates.routePreferredGatewayTags !== undefined) {
|
|
56
|
+
sessions[id].routePreferredGatewayTags = routePreferredGatewayTags
|
|
57
|
+
}
|
|
58
|
+
if (updates.routePreferredGatewayUseCase !== undefined) {
|
|
59
|
+
sessions[id].routePreferredGatewayUseCase = routePreferredGatewayUseCase
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
if (updates.plugins !== undefined) sessions[id].plugins = updates.plugins
|
|
43
63
|
else if (agentIdUpdateProvided && linkedAgent) sessions[id].plugins = Array.isArray(linkedAgent.plugins) ? linkedAgent.plugins : []
|
|
44
64
|
|
|
@@ -61,7 +61,16 @@ export async function POST(req: Request) {
|
|
|
61
61
|
const id = body.id || genId()
|
|
62
62
|
const sessions = loadSessions()
|
|
63
63
|
const agent = body.agentId ? loadAgents()[body.agentId] : null
|
|
64
|
-
const
|
|
64
|
+
const routePreferredGatewayTags = Array.isArray(body.routePreferredGatewayTags)
|
|
65
|
+
? body.routePreferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
66
|
+
: []
|
|
67
|
+
const routePreferredGatewayUseCase = typeof body.routePreferredGatewayUseCase === 'string' && body.routePreferredGatewayUseCase.trim()
|
|
68
|
+
? body.routePreferredGatewayUseCase.trim()
|
|
69
|
+
: null
|
|
70
|
+
const resolvedRoute = agent ? resolvePrimaryAgentRoute(agent, undefined, {
|
|
71
|
+
preferredGatewayTags: routePreferredGatewayTags,
|
|
72
|
+
preferredGatewayUseCase: routePreferredGatewayUseCase,
|
|
73
|
+
}) : null
|
|
65
74
|
const requestedPlugins = Array.isArray(body.plugins) ? body.plugins : (Array.isArray(body.tools) ? body.tools : null)
|
|
66
75
|
const resolvedPlugins = requestedPlugins ?? (Array.isArray(agent?.plugins) ? agent.plugins : (Array.isArray(agent?.tools) ? agent.tools : []))
|
|
67
76
|
|
|
@@ -83,6 +92,8 @@ export async function POST(req: Request) {
|
|
|
83
92
|
body.provider || agent?.provider || 'claude-cli',
|
|
84
93
|
body.apiEndpoint || agent?.apiEndpoint || null,
|
|
85
94
|
),
|
|
95
|
+
routePreferredGatewayTags,
|
|
96
|
+
routePreferredGatewayUseCase,
|
|
86
97
|
claudeSessionId: null,
|
|
87
98
|
codexThreadId: null,
|
|
88
99
|
opencodeSessionId: null,
|
|
@@ -90,6 +101,7 @@ export async function POST(req: Request) {
|
|
|
90
101
|
claudeCode: null,
|
|
91
102
|
codex: null,
|
|
92
103
|
opencode: null,
|
|
104
|
+
gemini: null,
|
|
93
105
|
},
|
|
94
106
|
messages: Array.isArray(body.messages) ? body.messages : [],
|
|
95
107
|
createdAt: Date.now(), lastActiveAt: Date.now(),
|
|
@@ -2,8 +2,10 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadConnectors, saveConnectors, logActivity } from '@/lib/server/storage'
|
|
3
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
5
6
|
|
|
6
7
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
ensureDaemonStarted('api/connectors/[id]:get')
|
|
7
9
|
const { id } = await params
|
|
8
10
|
const connectors = loadConnectors()
|
|
9
11
|
const connector = connectors[id]
|
|
@@ -11,8 +13,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
11
13
|
|
|
12
14
|
// Merge runtime status, QR code, and presence
|
|
13
15
|
try {
|
|
14
|
-
const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence } = await import('@/lib/server/connectors/manager')
|
|
15
|
-
|
|
16
|
+
const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
17
|
+
const runtimeStatus = getConnectorStatus(id)
|
|
18
|
+
connector.status = runtimeStatus === 'running'
|
|
19
|
+
? 'running'
|
|
20
|
+
: connector.lastError
|
|
21
|
+
? 'error'
|
|
22
|
+
: 'stopped'
|
|
23
|
+
const rState = getReconnectState(id)
|
|
24
|
+
if (rState) {
|
|
25
|
+
const ext = connector as unknown as Record<string, unknown>
|
|
26
|
+
ext.reconnectAttempts = rState.attempts
|
|
27
|
+
ext.nextRetryAt = rState.nextRetryAt
|
|
28
|
+
ext.reconnectError = rState.error
|
|
29
|
+
ext.reconnectExhausted = rState.exhausted
|
|
30
|
+
}
|
|
16
31
|
const qr = getConnectorQR(id)
|
|
17
32
|
if (qr) connector.qrDataUrl = qr
|
|
18
33
|
connector.authenticated = isConnectorAuthenticated(id)
|
|
@@ -26,6 +41,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
44
|
+
ensureDaemonStarted('api/connectors/[id]:put')
|
|
29
45
|
const { id } = await params
|
|
30
46
|
const body = await req.json()
|
|
31
47
|
const connectors = loadConnectors()
|
|
@@ -38,12 +54,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
38
54
|
try {
|
|
39
55
|
const manager = await import('@/lib/server/connectors/manager')
|
|
40
56
|
if (body.action === 'start') {
|
|
57
|
+
manager.clearReconnectState(id)
|
|
41
58
|
await manager.startConnector(id)
|
|
42
59
|
logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector started: "${connector.name}"` })
|
|
43
60
|
} else if (body.action === 'stop') {
|
|
44
61
|
await manager.stopConnector(id)
|
|
45
62
|
logActivity({ entityType: 'connector', entityId: id, action: 'stopped', actor: 'user', summary: `Connector stopped: "${connector.name}"` })
|
|
46
63
|
} else {
|
|
64
|
+
manager.clearReconnectState(id)
|
|
47
65
|
await manager.repairConnector(id)
|
|
48
66
|
logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector repaired: "${connector.name}"` })
|
|
49
67
|
}
|
|
@@ -2,19 +2,26 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadConnectors, saveConnectors } from '@/lib/server/storage'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
5
6
|
import { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
6
7
|
import { z } from 'zod'
|
|
7
8
|
import type { Connector } from '@/types'
|
|
8
9
|
export const dynamic = 'force-dynamic'
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
export async function GET(
|
|
12
|
+
export async function GET() {
|
|
13
|
+
ensureDaemonStarted('api/connectors:get')
|
|
12
14
|
const connectors = loadConnectors()
|
|
13
15
|
// Merge runtime status from manager
|
|
14
16
|
try {
|
|
15
17
|
const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
16
18
|
for (const c of Object.values(connectors) as Connector[]) {
|
|
17
|
-
|
|
19
|
+
const runtimeStatus = getConnectorStatus(c.id)
|
|
20
|
+
c.status = runtimeStatus === 'running'
|
|
21
|
+
? 'running'
|
|
22
|
+
: c.lastError
|
|
23
|
+
? 'error'
|
|
24
|
+
: 'stopped'
|
|
18
25
|
if (c.platform === 'whatsapp') {
|
|
19
26
|
c.authenticated = isConnectorAuthenticated(c.id)
|
|
20
27
|
c.hasCredentials = hasConnectorCredentials(c.id)
|
|
@@ -28,6 +35,7 @@ export async function GET(_req: Request) {
|
|
|
28
35
|
ext.reconnectAttempts = rState.attempts
|
|
29
36
|
ext.nextRetryAt = rState.nextRetryAt
|
|
30
37
|
ext.reconnectError = rState.error
|
|
38
|
+
ext.reconnectExhausted = rState.exhausted
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
} catch { /* manager not loaded yet */ }
|
|
@@ -35,6 +43,7 @@ export async function GET(_req: Request) {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export async function POST(req: Request) {
|
|
46
|
+
ensureDaemonStarted('api/connectors:post')
|
|
38
47
|
const raw = await req.json()
|
|
39
48
|
const parsed = ConnectorCreateSchema.safeParse(raw)
|
|
40
49
|
if (!parsed.success) {
|
|
@@ -72,13 +81,8 @@ export async function POST(req: Request) {
|
|
|
72
81
|
try {
|
|
73
82
|
const { startConnector } = await import('@/lib/server/connectors/manager')
|
|
74
83
|
await startConnector(id)
|
|
75
|
-
connector.isEnabled = true
|
|
76
|
-
connector.status = 'running'
|
|
77
|
-
connectors[id] = connector
|
|
78
|
-
saveConnectors(connectors)
|
|
79
|
-
notify('connectors')
|
|
80
84
|
} catch { /* auto-start is best-effort */ }
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
return NextResponse.json(connector)
|
|
87
|
+
return NextResponse.json(loadConnectors()[id] || connector)
|
|
84
88
|
}
|
|
@@ -15,6 +15,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
15
15
|
runtime.lastSeenAt = now
|
|
16
16
|
runtime.updatedAt = now
|
|
17
17
|
runtime.status = body.status || 'online'
|
|
18
|
+
if (typeof body.lifecycleState === 'string' && body.lifecycleState) runtime.lifecycleState = body.lifecycleState
|
|
19
|
+
if (typeof body.version === 'string') runtime.version = body.version || null
|
|
20
|
+
if (typeof body.lastHealthNote === 'string') runtime.lastHealthNote = body.lastHealthNote || null
|
|
18
21
|
if (body.tokenStats && typeof body.tokenStats === 'object') {
|
|
19
22
|
runtime.tokenStats = {
|
|
20
23
|
...(runtime.tokenStats || {}),
|
|
@@ -10,12 +10,44 @@ const ops: CollectionOps<any> = { load: loadExternalAgents, save: saveExternalAg
|
|
|
10
10
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
11
|
const { id } = await params
|
|
12
12
|
const body = await req.json().catch(() => ({}))
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const now = Date.now()
|
|
14
|
+
const result = mutateItem(ops, id, (runtime) => {
|
|
15
|
+
const action = typeof body.action === 'string' ? body.action : ''
|
|
16
|
+
const nextMetadata = body.metadata && typeof body.metadata === 'object'
|
|
17
|
+
? { ...(runtime.metadata || {}), ...body.metadata }
|
|
18
|
+
: runtime.metadata
|
|
19
|
+
|
|
20
|
+
const next = {
|
|
21
|
+
...runtime,
|
|
22
|
+
...body,
|
|
23
|
+
id,
|
|
24
|
+
metadata: nextMetadata,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (action === 'activate') {
|
|
29
|
+
next.lifecycleState = 'active'
|
|
30
|
+
next.lastHealthNote = body.lastHealthNote || 'Runtime returned to active service.'
|
|
31
|
+
} else if (action === 'drain') {
|
|
32
|
+
next.lifecycleState = 'draining'
|
|
33
|
+
next.lastHealthNote = body.lastHealthNote || 'Runtime draining after current work.'
|
|
34
|
+
} else if (action === 'cordon') {
|
|
35
|
+
next.lifecycleState = 'cordoned'
|
|
36
|
+
next.lastHealthNote = body.lastHealthNote || 'Runtime cordoned from new work.'
|
|
37
|
+
} else if (action === 'restart') {
|
|
38
|
+
next.metadata = {
|
|
39
|
+
...(next.metadata || {}),
|
|
40
|
+
controlRequest: {
|
|
41
|
+
action: 'restart',
|
|
42
|
+
requestedAt: now,
|
|
43
|
+
source: 'swarmclaw',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
next.lastHealthNote = body.lastHealthNote || 'Restart requested from SwarmClaw.'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return next
|
|
50
|
+
})
|
|
19
51
|
if (!result) return notFound()
|
|
20
52
|
return NextResponse.json(result)
|
|
21
53
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { formatZodError, ExternalAgentRegisterSchema } from '@/lib/validation/schemas'
|
|
4
|
-
import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
|
|
4
|
+
import { loadExternalAgents, loadGatewayProfiles, saveExternalAgents } from '@/lib/server/storage'
|
|
5
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
6
6
|
import type { ExternalAgentRuntime } from '@/types'
|
|
7
7
|
import { z } from 'zod'
|
|
@@ -13,9 +13,20 @@ function withDerivedStatus(record: ExternalAgentRuntime): ExternalAgentRuntime {
|
|
|
13
13
|
const staleMs = 3 * 60_000
|
|
14
14
|
if (!lastSeenAt) return { ...record, status: record.status || 'offline' }
|
|
15
15
|
if (record.status === 'offline') return record
|
|
16
|
+
const gateways = loadGatewayProfiles()
|
|
17
|
+
const gateway = record.gatewayProfileId ? gateways[record.gatewayProfileId] as Record<string, unknown> | undefined : undefined
|
|
18
|
+
const gatewayTags = Array.isArray(gateway?.tags)
|
|
19
|
+
? gateway?.tags.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
20
|
+
: []
|
|
21
|
+
const gatewayUseCase = gateway?.deployment && typeof gateway.deployment === 'object' && typeof (gateway.deployment as Record<string, unknown>).useCase === 'string'
|
|
22
|
+
? (gateway.deployment as Record<string, unknown>).useCase as string
|
|
23
|
+
: null
|
|
16
24
|
return {
|
|
17
25
|
...record,
|
|
18
26
|
status: now - lastSeenAt > staleMs ? 'stale' : (record.status || 'online'),
|
|
27
|
+
lifecycleState: record.lifecycleState || 'active',
|
|
28
|
+
gatewayTags: record.gatewayTags?.length ? record.gatewayTags : gatewayTags,
|
|
29
|
+
gatewayUseCase: record.gatewayUseCase || gatewayUseCase,
|
|
19
30
|
}
|
|
20
31
|
}
|
|
21
32
|
|
|
@@ -53,6 +64,11 @@ export async function POST(req: Request) {
|
|
|
53
64
|
gatewayProfileId: body.gatewayProfileId || null,
|
|
54
65
|
capabilities: body.capabilities,
|
|
55
66
|
labels: body.labels,
|
|
67
|
+
lifecycleState: body.lifecycleState || existing?.lifecycleState || 'active',
|
|
68
|
+
gatewayTags: body.gatewayTags,
|
|
69
|
+
gatewayUseCase: body.gatewayUseCase || null,
|
|
70
|
+
version: body.version || null,
|
|
71
|
+
lastHealthNote: body.lastHealthNote || null,
|
|
56
72
|
metadata: body.metadata,
|
|
57
73
|
tokenStats: body.tokenStats,
|
|
58
74
|
lastHeartbeatAt: existing?.lastHeartbeatAt || now,
|
|
@@ -20,6 +20,14 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
20
20
|
gateway.lastCheckedAt = Date.now()
|
|
21
21
|
gateway.lastError = result.ok ? null : (result.error || result.hint || 'Gateway health check failed.')
|
|
22
22
|
gateway.lastModelCount = Array.isArray(result.models) ? result.models.length : 0
|
|
23
|
+
gateway.deployment = {
|
|
24
|
+
...(gateway.deployment || {}),
|
|
25
|
+
lastVerifiedAt: Date.now(),
|
|
26
|
+
lastVerifiedOk: result.ok,
|
|
27
|
+
lastVerifiedMessage: result.ok
|
|
28
|
+
? `Verified ${Array.isArray(result.models) ? result.models.length : 0} model${result.models?.length === 1 ? '' : 's'}`
|
|
29
|
+
: (result.error || result.hint || 'Gateway health check failed.'),
|
|
30
|
+
}
|
|
23
31
|
gateway.updatedAt = Date.now()
|
|
24
32
|
saveGatewayProfiles(gateways)
|
|
25
33
|
notify('gateways')
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
|
|
3
3
|
import { loadAgents, loadGatewayProfiles, saveAgents, saveGatewayProfiles } from '@/lib/server/storage'
|
|
4
4
|
import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
5
|
-
import type { Agent, AgentRoutingTarget, GatewayProfile } from '@/types'
|
|
5
|
+
import type { Agent, AgentRoutingTarget, GatewayProfile, OpenClawDeploymentConfig, OpenClawGatewayStats } from '@/types'
|
|
6
6
|
|
|
7
7
|
const ops: CollectionOps<GatewayProfile> = {
|
|
8
8
|
load: loadGatewayProfiles,
|
|
@@ -17,6 +17,56 @@ function normalizeTags(value: unknown): string[] {
|
|
|
17
17
|
.filter(Boolean)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function normalizeText(value: unknown): string | null {
|
|
21
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeNullableNumber(value: unknown): number | null {
|
|
25
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
|
|
29
|
+
if (!value || typeof value !== 'object') return null
|
|
30
|
+
const deployment = value as Record<string, unknown>
|
|
31
|
+
return {
|
|
32
|
+
method: normalizeText(deployment.method) as OpenClawDeploymentConfig['method'],
|
|
33
|
+
provider: normalizeText(deployment.provider) as OpenClawDeploymentConfig['provider'],
|
|
34
|
+
remoteTarget: normalizeText(deployment.remoteTarget) as OpenClawDeploymentConfig['remoteTarget'],
|
|
35
|
+
useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
|
|
36
|
+
exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
|
|
37
|
+
managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
|
|
38
|
+
targetHost: normalizeText(deployment.targetHost),
|
|
39
|
+
sshHost: normalizeText(deployment.sshHost),
|
|
40
|
+
sshUser: normalizeText(deployment.sshUser),
|
|
41
|
+
sshPort: normalizeNullableNumber(deployment.sshPort),
|
|
42
|
+
sshKeyPath: normalizeText(deployment.sshKeyPath),
|
|
43
|
+
sshTargetDir: normalizeText(deployment.sshTargetDir),
|
|
44
|
+
image: normalizeText(deployment.image),
|
|
45
|
+
version: normalizeText(deployment.version),
|
|
46
|
+
lastDeployAt: normalizeNullableNumber(deployment.lastDeployAt),
|
|
47
|
+
lastDeployAction: normalizeText(deployment.lastDeployAction),
|
|
48
|
+
lastDeployProcessId: normalizeText(deployment.lastDeployProcessId),
|
|
49
|
+
lastDeploySummary: normalizeText(deployment.lastDeploySummary),
|
|
50
|
+
lastVerifiedAt: normalizeNullableNumber(deployment.lastVerifiedAt),
|
|
51
|
+
lastVerifiedOk: typeof deployment.lastVerifiedOk === 'boolean' ? deployment.lastVerifiedOk : null,
|
|
52
|
+
lastVerifiedMessage: normalizeText(deployment.lastVerifiedMessage),
|
|
53
|
+
lastBackupPath: normalizeText(deployment.lastBackupPath),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeStats(value: unknown): OpenClawGatewayStats | null {
|
|
58
|
+
if (!value || typeof value !== 'object') return null
|
|
59
|
+
const stats = value as Record<string, unknown>
|
|
60
|
+
return {
|
|
61
|
+
nodeCount: normalizeNullableNumber(stats.nodeCount) ?? undefined,
|
|
62
|
+
connectedNodeCount: normalizeNullableNumber(stats.connectedNodeCount) ?? undefined,
|
|
63
|
+
pendingNodePairings: normalizeNullableNumber(stats.pendingNodePairings) ?? undefined,
|
|
64
|
+
pairedDeviceCount: normalizeNullableNumber(stats.pairedDeviceCount) ?? undefined,
|
|
65
|
+
pendingDevicePairings: normalizeNullableNumber(stats.pendingDevicePairings) ?? undefined,
|
|
66
|
+
externalRuntimeCount: normalizeNullableNumber(stats.externalRuntimeCount) ?? undefined,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
20
70
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
21
71
|
const { id } = await params
|
|
22
72
|
const body = await req.json().catch(() => ({}))
|
|
@@ -39,6 +89,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
39
89
|
if (body.lastModelCount !== undefined) gateway.lastModelCount = body.lastModelCount || null
|
|
40
90
|
if (body.discoveredHost !== undefined) gateway.discoveredHost = body.discoveredHost || null
|
|
41
91
|
if (body.discoveredPort !== undefined) gateway.discoveredPort = body.discoveredPort || null
|
|
92
|
+
if (body.deployment !== undefined) gateway.deployment = { ...(gateway.deployment || {}), ...(normalizeDeployment(body.deployment) || {}) }
|
|
93
|
+
if (body.stats !== undefined) gateway.stats = { ...(gateway.stats || {}), ...(normalizeStats(body.stats) || {}) }
|
|
42
94
|
if (body.isDefault !== undefined) gateway.isDefault = body.isDefault === true
|
|
43
95
|
gateway.updatedAt = Date.now()
|
|
44
96
|
return gateway
|
|
@@ -4,6 +4,7 @@ import { normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
|
|
|
4
4
|
import { getGatewayProfiles } from '@/lib/server/agent-runtime-config'
|
|
5
5
|
import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
|
|
6
6
|
import { notify } from '@/lib/server/ws-hub'
|
|
7
|
+
import type { OpenClawDeploymentConfig, OpenClawGatewayStats } from '@/types'
|
|
7
8
|
export const dynamic = 'force-dynamic'
|
|
8
9
|
|
|
9
10
|
function normalizeTags(value: unknown): string[] {
|
|
@@ -13,6 +14,56 @@ function normalizeTags(value: unknown): string[] {
|
|
|
13
14
|
.filter(Boolean)
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function normalizeText(value: unknown): string | null {
|
|
18
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeNullableNumber(value: unknown): number | null {
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
|
|
26
|
+
if (!value || typeof value !== 'object') return null
|
|
27
|
+
const deployment = value as Record<string, unknown>
|
|
28
|
+
return {
|
|
29
|
+
method: normalizeText(deployment.method) as OpenClawDeploymentConfig['method'],
|
|
30
|
+
provider: normalizeText(deployment.provider) as OpenClawDeploymentConfig['provider'],
|
|
31
|
+
remoteTarget: normalizeText(deployment.remoteTarget) as OpenClawDeploymentConfig['remoteTarget'],
|
|
32
|
+
useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
|
|
33
|
+
exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
|
|
34
|
+
managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
|
|
35
|
+
targetHost: normalizeText(deployment.targetHost),
|
|
36
|
+
sshHost: normalizeText(deployment.sshHost),
|
|
37
|
+
sshUser: normalizeText(deployment.sshUser),
|
|
38
|
+
sshPort: normalizeNullableNumber(deployment.sshPort),
|
|
39
|
+
sshKeyPath: normalizeText(deployment.sshKeyPath),
|
|
40
|
+
sshTargetDir: normalizeText(deployment.sshTargetDir),
|
|
41
|
+
image: normalizeText(deployment.image),
|
|
42
|
+
version: normalizeText(deployment.version),
|
|
43
|
+
lastDeployAt: normalizeNullableNumber(deployment.lastDeployAt),
|
|
44
|
+
lastDeployAction: normalizeText(deployment.lastDeployAction),
|
|
45
|
+
lastDeployProcessId: normalizeText(deployment.lastDeployProcessId),
|
|
46
|
+
lastDeploySummary: normalizeText(deployment.lastDeploySummary),
|
|
47
|
+
lastVerifiedAt: normalizeNullableNumber(deployment.lastVerifiedAt),
|
|
48
|
+
lastVerifiedOk: typeof deployment.lastVerifiedOk === 'boolean' ? deployment.lastVerifiedOk : null,
|
|
49
|
+
lastVerifiedMessage: normalizeText(deployment.lastVerifiedMessage),
|
|
50
|
+
lastBackupPath: normalizeText(deployment.lastBackupPath),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeStats(value: unknown): OpenClawGatewayStats | null {
|
|
55
|
+
if (!value || typeof value !== 'object') return null
|
|
56
|
+
const stats = value as Record<string, unknown>
|
|
57
|
+
return {
|
|
58
|
+
nodeCount: normalizeNullableNumber(stats.nodeCount) ?? undefined,
|
|
59
|
+
connectedNodeCount: normalizeNullableNumber(stats.connectedNodeCount) ?? undefined,
|
|
60
|
+
pendingNodePairings: normalizeNullableNumber(stats.pendingNodePairings) ?? undefined,
|
|
61
|
+
pairedDeviceCount: normalizeNullableNumber(stats.pairedDeviceCount) ?? undefined,
|
|
62
|
+
pendingDevicePairings: normalizeNullableNumber(stats.pendingDevicePairings) ?? undefined,
|
|
63
|
+
externalRuntimeCount: normalizeNullableNumber(stats.externalRuntimeCount) ?? undefined,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
16
67
|
export async function GET() {
|
|
17
68
|
return NextResponse.json(getGatewayProfiles('openclaw'))
|
|
18
69
|
}
|
|
@@ -46,6 +97,8 @@ export async function POST(req: Request) {
|
|
|
46
97
|
lastModelCount: null,
|
|
47
98
|
discoveredHost: typeof body.discoveredHost === 'string' ? body.discoveredHost : null,
|
|
48
99
|
discoveredPort: typeof body.discoveredPort === 'number' ? body.discoveredPort : null,
|
|
100
|
+
deployment: normalizeDeployment(body.deployment),
|
|
101
|
+
stats: normalizeStats(body.stats),
|
|
49
102
|
isDefault,
|
|
50
103
|
createdAt: now,
|
|
51
104
|
updatedAt: now,
|