@swarmclawai/swarmclaw 0.8.2 → 0.8.3
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 +8 -8
- package/package.json +2 -2
- package/src/app/api/agents/route.ts +6 -3
- package/src/app/api/auth/route.ts +20 -10
- package/src/app/api/chats/[id]/devserver/route.ts +74 -48
- package/src/app/api/chats/[id]/route.ts +16 -1
- package/src/app/api/chats/route.ts +14 -6
- package/src/app/api/daemon/route.ts +4 -3
- package/src/app/api/openclaw/approvals/route.ts +3 -3
- package/src/app/api/wallets/[id]/route.ts +18 -4
- package/src/app/page.tsx +19 -23
- package/src/cli/index.js +1 -1
- package/src/cli/spec.js +1 -1
- package/src/components/auth/access-key-gate.tsx +5 -3
- package/src/components/chat/chat-area.tsx +50 -29
- package/src/components/chat/chat-card.tsx +4 -7
- package/src/components/chat/chat-header.tsx +19 -13
- package/src/components/chat/chat-list.tsx +11 -9
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/home/home-view.tsx +6 -2
- package/src/components/layout/app-layout.tsx +2 -3
- package/src/hooks/use-ws.ts +33 -7
- package/src/instrumentation.ts +21 -11
- package/src/lib/api-client.test.ts +49 -0
- package/src/lib/api-client.ts +53 -30
- package/src/lib/chats.ts +3 -0
- package/src/lib/runtime-env.test.ts +28 -0
- package/src/lib/runtime-env.ts +13 -0
- package/src/lib/server/chat-execution.ts +1 -1
- package/src/lib/server/connectors/manager.ts +4 -2
- package/src/lib/server/daemon-state.test.ts +23 -0
- package/src/lib/server/daemon-state.ts +34 -16
- package/src/lib/server/heartbeat-service.ts +61 -8
- package/src/lib/server/plugins.ts +12 -9
- package/src/lib/server/queue.ts +6 -1
- package/src/lib/server/storage.ts +100 -8
- package/src/lib/server/wallet-portfolio.ts +6 -0
- package/src/lib/session-summary.test.ts +49 -0
- package/src/lib/session-summary.ts +59 -0
- package/src/lib/ws-client.ts +1 -2
- package/src/proxy.test.ts +40 -0
- package/src/proxy.ts +23 -17
- package/src/stores/use-app-store.ts +66 -22
- package/src/stores/use-chat-store.ts +2 -2
- package/src/types/index.ts +4 -0
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
151
|
-
To pin a version: `SWARMCLAW_VERSION=v0.8.
|
|
151
|
+
To pin a version: `SWARMCLAW_VERSION=v0.8.3 curl ... | bash`
|
|
152
152
|
|
|
153
153
|
Or run locally from the repo (friendly for non-technical users):
|
|
154
154
|
|
|
@@ -701,7 +701,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
701
701
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
702
702
|
|
|
703
703
|
```bash
|
|
704
|
-
# example minor release (v0.8.
|
|
704
|
+
# example minor release (v0.8.3 style)
|
|
705
705
|
npm version minor
|
|
706
706
|
git push origin main --follow-tags
|
|
707
707
|
```
|
|
@@ -711,14 +711,14 @@ On `v*` tags, GitHub Actions will:
|
|
|
711
711
|
2. Create a GitHub Release
|
|
712
712
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
713
713
|
|
|
714
|
-
#### v0.8.
|
|
714
|
+
#### v0.8.3 Release Readiness Notes
|
|
715
715
|
|
|
716
|
-
Before shipping `v0.8.
|
|
716
|
+
Before shipping `v0.8.3`, confirm the following user-facing changes are reflected in docs:
|
|
717
717
|
|
|
718
|
-
1.
|
|
719
|
-
2.
|
|
720
|
-
3.
|
|
721
|
-
4. Site and README install/version strings are updated to `v0.8.
|
|
718
|
+
1. Chat/session docs note that the chat index now serves lightweight session summaries instead of full transcript payloads, and full messages are loaded from per-chat endpoints.
|
|
719
|
+
2. Operator/runtime docs note that the daemon once again owns scheduler/queue startup; background services should be described from the daemon controls rather than as unconditional boot behavior.
|
|
720
|
+
3. Local auth/troubleshooting docs mention that development-like runtimes are detected even when `NODE_ENV` is unset, so local rate limits and bootstrap timeouts now behave like dev instead of production.
|
|
721
|
+
4. Site and README install/version strings are updated to `v0.8.3`, including install snippets, release notes index text, and sidebar/footer labels.
|
|
722
722
|
|
|
723
723
|
## CLI
|
|
724
724
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"quickstart": "node ./scripts/easy-setup.mjs --start",
|
|
43
43
|
"quickstart:prod": "node ./scripts/easy-setup.mjs --prod",
|
|
44
44
|
"update:easy": "node ./scripts/easy-update.mjs",
|
|
45
|
-
"dev": "next dev --hostname 0.0.0.0 -p 3456",
|
|
45
|
+
"dev": "next dev --webpack --hostname 0.0.0.0 -p 3456",
|
|
46
46
|
"dev:webpack": "next dev --webpack --hostname 0.0.0.0 -p 3456",
|
|
47
47
|
"dev:clean": "rm -rf .next && next dev --hostname 0.0.0.0 -p 3456",
|
|
48
48
|
"build": "next build",
|
|
@@ -3,15 +3,18 @@ import { genId } from '@/lib/id'
|
|
|
3
3
|
import { loadAgents, loadSessions, loadUsage, saveAgents, logActivity } from '@/lib/server/storage'
|
|
4
4
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
5
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
-
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
7
6
|
import { getAgentSpendWindows } from '@/lib/server/cost'
|
|
8
7
|
import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
9
8
|
import { z } from 'zod'
|
|
10
9
|
export const dynamic = 'force-dynamic'
|
|
11
10
|
|
|
11
|
+
async function ensureDaemonIfNeeded(source: string) {
|
|
12
|
+
const { ensureDaemonStarted } = await import('@/lib/server/daemon-state')
|
|
13
|
+
ensureDaemonStarted(source)
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
export async function GET(req: Request) {
|
|
14
|
-
ensureDaemonStarted('api/agents:get')
|
|
15
18
|
const agents = loadAgents()
|
|
16
19
|
const sessions = loadSessions()
|
|
17
20
|
const usage = loadUsage()
|
|
@@ -44,7 +47,7 @@ export async function GET(req: Request) {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export async function POST(req: Request) {
|
|
47
|
-
|
|
50
|
+
await ensureDaemonIfNeeded('api/agents:post')
|
|
48
51
|
const raw = await req.json()
|
|
49
52
|
const parsed = AgentCreateSchema.safeParse(raw)
|
|
50
53
|
if (!parsed.success) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { validateAccessKey, isFirstTimeSetup, markSetupComplete } from '@/lib/server/storage'
|
|
3
|
-
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
4
3
|
import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
|
|
4
|
+
import { isProductionRuntime } from '@/lib/runtime-env'
|
|
5
5
|
export const dynamic = 'force-dynamic'
|
|
6
6
|
|
|
7
7
|
interface AuthAttemptEntry {
|
|
@@ -16,6 +16,10 @@ const authRateLimitMap = (
|
|
|
16
16
|
const MAX_ATTEMPTS = 5
|
|
17
17
|
const LOCKOUT_MS = 15 * 60 * 1000
|
|
18
18
|
|
|
19
|
+
function isRateLimitEnabled(): boolean {
|
|
20
|
+
return isProductionRuntime()
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
function getClientIp(req: Request): string {
|
|
20
24
|
const forwarded = req.headers.get('x-forwarded-for')
|
|
21
25
|
if (forwarded) {
|
|
@@ -59,9 +63,10 @@ export async function GET(req: Request) {
|
|
|
59
63
|
|
|
60
64
|
/** POST /api/auth — validate an access key */
|
|
61
65
|
export async function POST(req: Request) {
|
|
66
|
+
const rateLimitEnabled = isRateLimitEnabled()
|
|
62
67
|
const clientIp = getClientIp(req)
|
|
63
|
-
const entry = authRateLimitMap.get(clientIp)
|
|
64
|
-
if (entry && entry.lockedUntil > Date.now()) {
|
|
68
|
+
const entry = rateLimitEnabled ? authRateLimitMap.get(clientIp) : undefined
|
|
69
|
+
if (rateLimitEnabled && entry && entry.lockedUntil > Date.now()) {
|
|
65
70
|
const retryAfter = Math.ceil((entry.lockedUntil - Date.now()) / 1000)
|
|
66
71
|
return clearAuthCookie(NextResponse.json(
|
|
67
72
|
{ error: 'Too many failed attempts. Try again later.', retryAfter },
|
|
@@ -71,26 +76,31 @@ export async function POST(req: Request) {
|
|
|
71
76
|
|
|
72
77
|
const { key } = await req.json()
|
|
73
78
|
if (!key || !validateAccessKey(key)) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
current.
|
|
79
|
+
let remaining = MAX_ATTEMPTS
|
|
80
|
+
if (rateLimitEnabled) {
|
|
81
|
+
const current = authRateLimitMap.get(clientIp) ?? { count: 0, lockedUntil: 0 }
|
|
82
|
+
current.count += 1
|
|
83
|
+
if (current.count >= MAX_ATTEMPTS) {
|
|
84
|
+
current.lockedUntil = Date.now() + LOCKOUT_MS
|
|
85
|
+
}
|
|
86
|
+
authRateLimitMap.set(clientIp, current)
|
|
87
|
+
remaining = Math.max(0, MAX_ATTEMPTS - current.count)
|
|
78
88
|
}
|
|
79
|
-
authRateLimitMap.set(clientIp, current)
|
|
80
89
|
return clearAuthCookie(NextResponse.json(
|
|
81
90
|
{ error: 'Invalid access key' },
|
|
82
91
|
{
|
|
83
92
|
status: 401,
|
|
84
|
-
headers: { 'X-RateLimit-Remaining': String(
|
|
93
|
+
headers: { 'X-RateLimit-Remaining': String(remaining) },
|
|
85
94
|
},
|
|
86
95
|
))
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
authRateLimitMap.delete(clientIp)
|
|
98
|
+
if (rateLimitEnabled) authRateLimitMap.delete(clientIp)
|
|
90
99
|
// If this was first-time setup, mark it as claimed
|
|
91
100
|
if (isFirstTimeSetup()) {
|
|
92
101
|
markSetupComplete()
|
|
93
102
|
}
|
|
103
|
+
const { ensureDaemonStarted } = await import('@/lib/server/daemon-state')
|
|
94
104
|
ensureDaemonStarted('api/auth:post')
|
|
95
105
|
return setAuthCookie(NextResponse.json({ ok: true }), req, key)
|
|
96
106
|
}
|
|
@@ -5,6 +5,13 @@ import { notFound } from '@/lib/server/collection-helpers'
|
|
|
5
5
|
import { resolveDevServerLaunchDir } from '@/lib/server/devserver-launch'
|
|
6
6
|
import net from 'net'
|
|
7
7
|
|
|
8
|
+
interface DevServerStartResult {
|
|
9
|
+
status?: number
|
|
10
|
+
body: Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const inflightDevServerStarts = new Map<string, Promise<DevServerStartResult>>()
|
|
14
|
+
|
|
8
15
|
function findFreePort(): Promise<number> {
|
|
9
16
|
return new Promise((resolve, reject) => {
|
|
10
17
|
const server = net.createServer()
|
|
@@ -24,73 +31,92 @@ function buildDevArgs(framework: string, port: number): string[] {
|
|
|
24
31
|
return ['--', '--host', '0.0.0.0', '--port', String(port)]
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (action === 'start') {
|
|
36
|
-
if (devServers.has(id)) {
|
|
37
|
-
const ds = devServers.get(id)!
|
|
38
|
-
return NextResponse.json({ running: true, url: ds.url })
|
|
39
|
-
}
|
|
34
|
+
async function startDevServer(id: string, session: { cwd: string }): Promise<DevServerStartResult> {
|
|
35
|
+
const launch = resolveDevServerLaunchDir(session.cwd)
|
|
36
|
+
const port = await findFreePort()
|
|
37
|
+
const proc = spawn('npm', ['run', 'dev', ...buildDevArgs(launch.framework, port)], {
|
|
38
|
+
cwd: launch.launchDir,
|
|
39
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
40
|
+
env: { ...process.env, FORCE_COLOR: '0', PORT: String(port) },
|
|
41
|
+
})
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
cwd: launch.launchDir,
|
|
45
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
-
env: { ...process.env, FORCE_COLOR: '0', PORT: String(port) },
|
|
47
|
-
})
|
|
43
|
+
let output = ''
|
|
44
|
+
let detectedUrl: string | null = null
|
|
45
|
+
const urlRe = /https?:\/\/(?:localhost|0\.0\.0\.0|[\d.]+):(\d+)/
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
const port = match[1]
|
|
59
|
-
detectedUrl = `http://${localIP()}:${port}`
|
|
60
|
-
const ds = devServers.get(id)
|
|
61
|
-
if (ds) ds.url = detectedUrl
|
|
62
|
-
}
|
|
47
|
+
function onData(chunk: Buffer) {
|
|
48
|
+
output += chunk.toString()
|
|
49
|
+
if (!detectedUrl) {
|
|
50
|
+
const match = output.match(urlRe)
|
|
51
|
+
if (match) {
|
|
52
|
+
const detectedPort = match[1]
|
|
53
|
+
detectedUrl = `http://${localIP()}:${detectedPort}`
|
|
54
|
+
const ds = devServers.get(id)
|
|
55
|
+
if (ds) ds.url = detectedUrl
|
|
63
56
|
}
|
|
64
57
|
}
|
|
58
|
+
}
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
proc.stdout!.on('data', onData)
|
|
61
|
+
proc.stderr!.on('data', onData)
|
|
62
|
+
proc.on('close', () => { devServers.delete(id); console.log(`[${id}] dev server stopped`) })
|
|
63
|
+
proc.on('error', () => devServers.delete(id))
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
devServers.set(id, { proc, url: `http://${localIP()}:${port}` })
|
|
66
|
+
console.log(`[${id}] starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 4000))
|
|
69
|
+
const ds = devServers.get(id)
|
|
70
|
+
if (!ds) {
|
|
71
|
+
return {
|
|
72
|
+
status: 502,
|
|
73
|
+
body: {
|
|
79
74
|
running: false,
|
|
80
75
|
error: 'Dev server exited during startup',
|
|
81
76
|
cwd: launch.launchDir,
|
|
82
77
|
sessionCwd: session.cwd,
|
|
83
78
|
framework: launch.framework,
|
|
84
79
|
output: output.slice(-4000),
|
|
85
|
-
},
|
|
80
|
+
},
|
|
86
81
|
}
|
|
87
|
-
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
body: {
|
|
88
86
|
running: true,
|
|
89
87
|
url: ds.url,
|
|
90
88
|
cwd: launch.launchDir,
|
|
91
89
|
sessionCwd: session.cwd,
|
|
92
90
|
framework: launch.framework,
|
|
93
|
-
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
96
|
+
const { id } = await params
|
|
97
|
+
const sessions = loadSessions()
|
|
98
|
+
const session = sessions[id]
|
|
99
|
+
if (!session) return notFound()
|
|
100
|
+
|
|
101
|
+
const { action } = await req.json()
|
|
102
|
+
|
|
103
|
+
if (action === 'start') {
|
|
104
|
+
if (devServers.has(id)) {
|
|
105
|
+
const ds = devServers.get(id)!
|
|
106
|
+
return NextResponse.json({ running: true, url: ds.url })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let startPromise = inflightDevServerStarts.get(id)
|
|
110
|
+
if (!startPromise) {
|
|
111
|
+
startPromise = startDevServer(id, session).finally(() => {
|
|
112
|
+
if (inflightDevServerStarts.get(id) === startPromise) {
|
|
113
|
+
inflightDevServerStarts.delete(id)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
inflightDevServerStarts.set(id, startPromise)
|
|
117
|
+
}
|
|
118
|
+
const result = await startPromise
|
|
119
|
+
return NextResponse.json(result.body, result.status ? { status: result.status } : undefined)
|
|
94
120
|
|
|
95
121
|
} else if (action === 'stop') {
|
|
96
122
|
if (devServers.has(id)) {
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
|
|
2
|
+
import { loadSessions, saveSessions, deleteSession, active, loadAgents, loadStoredItem } from '@/lib/server/storage'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
5
5
|
import { resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
|
|
6
|
+
import { getSessionRunState } from '@/lib/server/session-run-manager'
|
|
7
|
+
import type { Session } from '@/types'
|
|
8
|
+
|
|
9
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
const session = loadStoredItem('sessions', id) as Session | null
|
|
12
|
+
if (!session) return notFound()
|
|
13
|
+
|
|
14
|
+
const run = getSessionRunState(id)
|
|
15
|
+
session.active = active.has(id) || !!run.runningRunId
|
|
16
|
+
session.queuedCount = run.queueLength
|
|
17
|
+
session.currentRunId = run.runningRunId || null
|
|
18
|
+
|
|
19
|
+
return NextResponse.json(session)
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
23
|
const { id } = await params
|
|
@@ -10,12 +10,16 @@ import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
|
10
10
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
|
|
11
11
|
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
|
|
12
12
|
import { materializeStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
|
|
13
|
-
import {
|
|
13
|
+
import { buildSessionListSummary } from '@/lib/session-summary'
|
|
14
14
|
export const dynamic = 'force-dynamic'
|
|
15
15
|
|
|
16
|
+
async function ensureDaemonIfNeeded(source: string) {
|
|
17
|
+
const { ensureDaemonStarted } = await import('@/lib/server/daemon-state')
|
|
18
|
+
ensureDaemonStarted(source)
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
|
|
17
22
|
export async function GET(req: Request) {
|
|
18
|
-
ensureDaemonStarted('api/chats:get')
|
|
19
23
|
const sessions = loadSessions()
|
|
20
24
|
const changedSessionIds: string[] = []
|
|
21
25
|
for (const id of Object.keys(sessions)) {
|
|
@@ -35,19 +39,23 @@ export async function GET(req: Request) {
|
|
|
35
39
|
upsertStoredItem('sessions', id, persisted)
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
const summarized = Object.fromEntries(
|
|
43
|
+
Object.entries(sessions).map(([id, session]) => [id, buildSessionListSummary(session)]),
|
|
44
|
+
)
|
|
45
|
+
|
|
38
46
|
const { searchParams } = new URL(req.url)
|
|
39
47
|
const limitParam = searchParams.get('limit')
|
|
40
|
-
if (!limitParam) return NextResponse.json(
|
|
48
|
+
if (!limitParam) return NextResponse.json(summarized)
|
|
41
49
|
|
|
42
50
|
const limit = Math.max(1, Number(limitParam) || 50)
|
|
43
51
|
const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
|
|
44
|
-
const all = Object.values(
|
|
52
|
+
const all = Object.values(summarized).sort((a, b) => (b.lastActiveAt ?? b.createdAt) - (a.lastActiveAt ?? a.createdAt))
|
|
45
53
|
const items = all.slice(offset, offset + limit)
|
|
46
54
|
return NextResponse.json({ items, total: all.length, hasMore: offset + limit < all.length })
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
export async function DELETE(req: Request) {
|
|
50
|
-
|
|
58
|
+
await ensureDaemonIfNeeded('api/chats:delete')
|
|
51
59
|
const { ids } = await req.json().catch(() => ({ ids: [] })) as { ids: string[] }
|
|
52
60
|
if (!Array.isArray(ids) || !ids.length) {
|
|
53
61
|
return new NextResponse('Missing ids', { status: 400 })
|
|
@@ -68,7 +76,7 @@ export async function DELETE(req: Request) {
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
export async function POST(req: Request) {
|
|
71
|
-
|
|
79
|
+
await ensureDaemonIfNeeded('api/chats:post')
|
|
72
80
|
const body = await req.json().catch(() => ({}))
|
|
73
81
|
let cwd = (body.cwd || '').trim()
|
|
74
82
|
if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { ensureDaemonStarted, getDaemonStatus, startDaemon, stopDaemon } from '@/lib/server/daemon-state'
|
|
3
2
|
import { notify } from '@/lib/server/ws-hub'
|
|
4
3
|
export const dynamic = 'force-dynamic'
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
export async function GET(
|
|
8
|
-
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const { getDaemonStatus } = await import('@/lib/server/daemon-state')
|
|
9
8
|
return NextResponse.json(getDaemonStatus())
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -14,10 +13,12 @@ export async function POST(req: Request) {
|
|
|
14
13
|
const action = body.action
|
|
15
14
|
|
|
16
15
|
if (action === 'start') {
|
|
16
|
+
const { startDaemon } = await import('@/lib/server/daemon-state')
|
|
17
17
|
startDaemon({ source: 'api/daemon:post:start', manualStart: true })
|
|
18
18
|
notify('daemon')
|
|
19
19
|
return NextResponse.json({ ok: true, status: 'running' })
|
|
20
20
|
} else if (action === 'stop') {
|
|
21
|
+
const { stopDaemon } = await import('@/lib/server/daemon-state')
|
|
21
22
|
stopDaemon({ source: 'api/daemon:post:stop', manualStop: true })
|
|
22
23
|
notify('daemon')
|
|
23
24
|
return NextResponse.json({ ok: true, status: 'stopped' })
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
2
|
+
import { ensureGatewayConnected, getGateway } from '@/lib/server/openclaw-gateway'
|
|
3
3
|
import type { PendingExecApproval, ExecApprovalDecision } from '@/types'
|
|
4
4
|
|
|
5
5
|
/** GET — fetch pending execution approvals from gateway */
|
|
6
6
|
export async function GET() {
|
|
7
|
-
const gw =
|
|
8
|
-
if (!gw) {
|
|
7
|
+
const gw = getGateway()
|
|
8
|
+
if (!gw?.connected) {
|
|
9
9
|
return NextResponse.json([], { status: 200 })
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -3,7 +3,7 @@ import { loadWallets, upsertWallet, deleteWallet as deleteWalletFromStore, loadA
|
|
|
3
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
4
4
|
import { getWalletLimitAtomic, normalizeAtomicString } from '@/lib/wallet'
|
|
5
5
|
import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary } from '@/types'
|
|
6
|
-
import { buildEmptyWalletPortfolio } from '@/lib/server/wallet-portfolio'
|
|
6
|
+
import { buildEmptyWalletPortfolio, getCachedWalletPortfolio } from '@/lib/server/wallet-portfolio'
|
|
7
7
|
import {
|
|
8
8
|
getAgentActiveWalletId,
|
|
9
9
|
getWalletPortfolioSnapshot,
|
|
@@ -43,12 +43,28 @@ function withPortfolio(
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export async function GET(
|
|
46
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
47
47
|
const { id } = await params
|
|
48
48
|
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
49
49
|
const wallet = wallets[id]
|
|
50
50
|
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
51
51
|
|
|
52
|
+
const url = new URL(req.url)
|
|
53
|
+
const cachedOnly = url.searchParams.get('cached') === '1'
|
|
54
|
+
const agents = loadAgents()
|
|
55
|
+
const isActive = getAgentActiveWalletId(agents[wallet.agentId]) === wallet.id
|
|
56
|
+
|
|
57
|
+
if (cachedOnly) {
|
|
58
|
+
const cached = getCachedWalletPortfolio(wallet)
|
|
59
|
+
if (!cached) {
|
|
60
|
+
return NextResponse.json({
|
|
61
|
+
...stripWalletPrivateKey(wallet as unknown as Record<string, unknown>),
|
|
62
|
+
isActive,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return NextResponse.json(withPortfolio(wallet, cached, isActive))
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
let portfolio = buildEmptyWalletPortfolio(wallet)
|
|
53
69
|
try {
|
|
54
70
|
portfolio = await getWalletPortfolioSnapshot(wallet, {
|
|
@@ -59,8 +75,6 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
59
75
|
// RPC failure — return 0
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
const agents = loadAgents()
|
|
63
|
-
const isActive = getAgentActiveWalletId(agents[wallet.agentId]) === wallet.id
|
|
64
78
|
return NextResponse.json(withPortfolio(wallet, portfolio, isActive))
|
|
65
79
|
}
|
|
66
80
|
|
package/src/app/page.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { getStoredAccessKey, clearStoredAccessKey, api } from '@/lib/api-client'
|
|
|
7
7
|
import { safeStorageGet, safeStorageRemove, safeStorageSet } from '@/lib/safe-storage'
|
|
8
8
|
import { connectWs, disconnectWs } from '@/lib/ws-client'
|
|
9
9
|
import { fetchWithTimeout } from '@/lib/fetch-timeout'
|
|
10
|
-
import {
|
|
10
|
+
import { isDevelopmentLikeRuntime } from '@/lib/runtime-env'
|
|
11
11
|
import { useWs } from '@/hooks/use-ws'
|
|
12
12
|
import { AccessKeyGate } from '@/components/auth/access-key-gate'
|
|
13
13
|
import { UserPicker } from '@/components/auth/user-picker'
|
|
@@ -16,8 +16,8 @@ import { AppLayout } from '@/components/layout/app-layout'
|
|
|
16
16
|
import { useViewRouter } from '@/hooks/use-view-router'
|
|
17
17
|
import type { Agent } from '@/types'
|
|
18
18
|
|
|
19
|
-
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
20
|
-
const POST_AUTH_BOOTSTRAP_TIMEOUT_MS = 8_000
|
|
19
|
+
const AUTH_CHECK_TIMEOUT_MS = isDevelopmentLikeRuntime() ? 20_000 : 8_000
|
|
20
|
+
const POST_AUTH_BOOTSTRAP_TIMEOUT_MS = isDevelopmentLikeRuntime() ? 20_000 : 8_000
|
|
21
21
|
|
|
22
22
|
function FullScreenLoader(props: {
|
|
23
23
|
stage?: string | null
|
|
@@ -202,17 +202,22 @@ export default function Home() {
|
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
const checkAuth = useCallback(async () => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
setAuthenticated(data?.authenticated === true)
|
|
211
|
-
} catch {
|
|
212
|
-
setAuthenticated(false)
|
|
213
|
-
} finally {
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetchWithTimeout('/api/auth', {}, AUTH_CHECK_TIMEOUT_MS)
|
|
207
|
+
const data = await res.json().catch(() => ({}))
|
|
208
|
+
if (data?.authenticated === true) {
|
|
209
|
+
setAuthenticated(true)
|
|
214
210
|
setAuthChecked(true)
|
|
211
|
+
return
|
|
215
212
|
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Fall back to stored-key bootstrap below if the auth probe is unavailable.
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const key = getStoredAccessKey()
|
|
218
|
+
if (!key) {
|
|
219
|
+
setAuthenticated(false)
|
|
220
|
+
setAuthChecked(true)
|
|
216
221
|
return
|
|
217
222
|
}
|
|
218
223
|
|
|
@@ -260,8 +265,7 @@ export default function Home() {
|
|
|
260
265
|
|
|
261
266
|
useEffect(() => {
|
|
262
267
|
if (!authenticated) return
|
|
263
|
-
|
|
264
|
-
if (key) connectWs(key)
|
|
268
|
+
connectWs()
|
|
265
269
|
syncUserFromServer()
|
|
266
270
|
loadNetworkInfo()
|
|
267
271
|
loadSettings()
|
|
@@ -269,15 +273,7 @@ export default function Home() {
|
|
|
269
273
|
return () => { disconnectWs() }
|
|
270
274
|
}, [authenticated])
|
|
271
275
|
|
|
272
|
-
useWs('sessions', loadSessions,
|
|
273
|
-
|
|
274
|
-
useEffect(() => {
|
|
275
|
-
if (!authenticated || !isLocalhostBrowser()) return
|
|
276
|
-
const pollId = setInterval(() => {
|
|
277
|
-
void loadSessions()
|
|
278
|
-
}, 5000)
|
|
279
|
-
return () => clearInterval(pollId)
|
|
280
|
-
}, [authenticated, loadSessions])
|
|
276
|
+
useWs('sessions', loadSessions, 15000)
|
|
281
277
|
|
|
282
278
|
// Auto-select agent's thread on load — resolves a persisted agentId into a session,
|
|
283
279
|
// or falls back to defaultAgentId from settings, then first agent.
|
package/src/cli/index.js
CHANGED
|
@@ -490,7 +490,7 @@ const COMMAND_GROUPS = [
|
|
|
490
490
|
description: 'Manage agent chats and runtime controls',
|
|
491
491
|
commands: [
|
|
492
492
|
cmd('list', 'GET', '/chats', 'List chats'),
|
|
493
|
-
cmd('get', 'GET', '/chats/:id', 'Get chat by id'
|
|
493
|
+
cmd('get', 'GET', '/chats/:id', 'Get chat by id'),
|
|
494
494
|
cmd('create', 'POST', '/chats', 'Create chat', { expectsJsonBody: true }),
|
|
495
495
|
cmd('update', 'PUT', '/chats/:id', 'Update chat', { expectsJsonBody: true }),
|
|
496
496
|
cmd('delete', 'DELETE', '/chats/:id', 'Delete chat'),
|
package/src/cli/spec.js
CHANGED
|
@@ -379,7 +379,7 @@ const COMMAND_GROUPS = {
|
|
|
379
379
|
commands: {
|
|
380
380
|
list: { description: 'List chats', method: 'GET', path: '/chats' },
|
|
381
381
|
create: { description: 'Create chat', method: 'POST', path: '/chats' },
|
|
382
|
-
get: { description: 'Get chat by id
|
|
382
|
+
get: { description: 'Get chat by id', method: 'GET', path: '/chats/:id', params: ['id'] },
|
|
383
383
|
update: { description: 'Update chat fields', method: 'PUT', path: '/chats/:id', params: ['id'] },
|
|
384
384
|
delete: { description: 'Delete one chat', method: 'DELETE', path: '/chats/:id', params: ['id'] },
|
|
385
385
|
'delete-many': { description: 'Delete multiple chats (body: {"ids":[...]})', method: 'DELETE', path: '/chats' },
|
|
@@ -55,11 +55,13 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
55
55
|
setStoredAccessKey(trimmed)
|
|
56
56
|
onAuthenticated()
|
|
57
57
|
} else {
|
|
58
|
-
|
|
58
|
+
const payload = await res.json().catch(() => null) as { error?: unknown } | null
|
|
59
|
+
setError(typeof payload?.error === 'string' && payload.error.trim() ? payload.error : 'Invalid access key')
|
|
59
60
|
setKey('')
|
|
60
61
|
}
|
|
61
|
-
} catch {
|
|
62
|
-
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const message = err instanceof Error && err.message.trim() ? err.message : 'Connection failed'
|
|
64
|
+
setError(message)
|
|
63
65
|
} finally {
|
|
64
66
|
setLoading(false)
|
|
65
67
|
}
|