@swarmclawai/swarmclaw 0.6.4 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- package/tsconfig.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
2
|
+
import { loadChatrooms, saveChatrooms, loadAgents, loadConnectors, saveConnectors } from '@/lib/server/storage'
|
|
3
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
5
|
import { genId } from '@/lib/id'
|
|
@@ -21,16 +21,33 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
21
21
|
|
|
22
22
|
if (body.name !== undefined) chatroom.name = body.name
|
|
23
23
|
if (body.description !== undefined) chatroom.description = body.description
|
|
24
|
+
if (body.chatMode !== undefined) {
|
|
25
|
+
chatroom.chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
|
|
26
|
+
}
|
|
27
|
+
if (body.autoAddress !== undefined) {
|
|
28
|
+
chatroom.autoAddress = Boolean(body.autoAddress)
|
|
29
|
+
}
|
|
30
|
+
if (body.routingRules !== undefined) {
|
|
31
|
+
chatroom.routingRules = Array.isArray(body.routingRules) ? body.routingRules : undefined
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
// Diff agentIds and inject join/leave system messages
|
|
26
35
|
if (Array.isArray(body.agentIds)) {
|
|
36
|
+
const agents = loadAgents()
|
|
37
|
+
const invalidAgentIds = (body.agentIds as string[]).filter((agentId) => !agents[agentId])
|
|
38
|
+
if (invalidAgentIds.length > 0) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
|
|
41
|
+
{ status: 400 },
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
const oldIds = new Set(chatroom.agentIds)
|
|
28
46
|
const newIds = new Set(body.agentIds as string[])
|
|
29
47
|
const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
|
|
30
48
|
const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
|
|
31
49
|
|
|
32
50
|
if (added.length > 0 || removed.length > 0) {
|
|
33
|
-
const agents = loadAgents()
|
|
34
51
|
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
35
52
|
const now = Date.now()
|
|
36
53
|
let offset = 0
|
|
@@ -77,6 +94,21 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
77
94
|
const chatrooms = loadChatrooms()
|
|
78
95
|
if (!chatrooms[id]) return notFound()
|
|
79
96
|
|
|
97
|
+
// Cascade: null out chatroomId on any connectors that reference this chatroom
|
|
98
|
+
const connectors = loadConnectors()
|
|
99
|
+
let connectorsDirty = false
|
|
100
|
+
for (const connector of Object.values(connectors)) {
|
|
101
|
+
if (connector.chatroomId === id) {
|
|
102
|
+
connector.chatroomId = null
|
|
103
|
+
connector.updatedAt = Date.now()
|
|
104
|
+
connectorsDirty = true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (connectorsDirty) {
|
|
108
|
+
saveConnectors(connectors)
|
|
109
|
+
notify('connectors')
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
delete chatrooms[id]
|
|
81
113
|
saveChatrooms(chatrooms)
|
|
82
114
|
notify('chatrooms')
|
|
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { ChatroomCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
6
|
+
import { z } from 'zod'
|
|
5
7
|
import type { Chatroom, ChatroomMessage } from '@/types'
|
|
6
8
|
|
|
7
9
|
export const dynamic = 'force-dynamic'
|
|
@@ -12,15 +14,31 @@ export async function GET() {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export async function POST(req: Request) {
|
|
15
|
-
const
|
|
17
|
+
const raw = await req.json()
|
|
18
|
+
const parsed = ChatroomCreateSchema.safeParse(raw)
|
|
19
|
+
if (!parsed.success) {
|
|
20
|
+
return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
|
|
21
|
+
}
|
|
22
|
+
const body = parsed.data
|
|
16
23
|
const chatrooms = loadChatrooms()
|
|
17
24
|
const id = genId()
|
|
18
25
|
|
|
19
|
-
const
|
|
26
|
+
const requestedAgentIds: string[] = body.agentIds
|
|
27
|
+
const knownAgents = loadAgents()
|
|
28
|
+
const invalidAgentIds = requestedAgentIds.filter((agentId) => !knownAgents[agentId])
|
|
29
|
+
if (invalidAgentIds.length > 0) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
|
|
32
|
+
{ status: 400 },
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
const agentIds: string[] = requestedAgentIds
|
|
36
|
+
const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
|
|
37
|
+
const autoAddress = Boolean(body.autoAddress)
|
|
20
38
|
const now = Date.now()
|
|
21
39
|
|
|
22
40
|
// Generate join messages for initial agents
|
|
23
|
-
const agents = agentIds.length > 0 ?
|
|
41
|
+
const agents = agentIds.length > 0 ? knownAgents : {}
|
|
24
42
|
const joinMessages: ChatroomMessage[] = agentIds.map((agentId: string, i: number) => ({
|
|
25
43
|
id: genId(),
|
|
26
44
|
senderId: 'system',
|
|
@@ -38,6 +56,11 @@ export async function POST(req: Request) {
|
|
|
38
56
|
description: body.description || '',
|
|
39
57
|
agentIds,
|
|
40
58
|
messages: joinMessages,
|
|
59
|
+
chatMode,
|
|
60
|
+
autoAddress,
|
|
61
|
+
...(Array.isArray(body.routingRules) && body.routingRules.length > 0
|
|
62
|
+
? { routingRules: body.routingRules }
|
|
63
|
+
: {}),
|
|
41
64
|
createdAt: now,
|
|
42
65
|
updatedAt: now,
|
|
43
66
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadConnectors, loadConnectorHealth } from '@/lib/server/storage'
|
|
3
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
|
+
import type { ConnectorHealthEvent } from '@/types'
|
|
5
|
+
|
|
6
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await params
|
|
8
|
+
const connectors = loadConnectors()
|
|
9
|
+
if (!connectors[id]) return notFound()
|
|
10
|
+
|
|
11
|
+
const url = new URL(req.url)
|
|
12
|
+
const since = url.searchParams.get('since')
|
|
13
|
+
|
|
14
|
+
const allHealth = loadConnectorHealth()
|
|
15
|
+
const events: ConnectorHealthEvent[] = []
|
|
16
|
+
|
|
17
|
+
for (const raw of Object.values(allHealth)) {
|
|
18
|
+
const entry = raw as ConnectorHealthEvent
|
|
19
|
+
if (entry.connectorId !== id) continue
|
|
20
|
+
if (since && entry.timestamp < since) continue
|
|
21
|
+
events.push(entry)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Sort by timestamp ascending
|
|
25
|
+
events.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
26
|
+
|
|
27
|
+
// Compute uptime percentage
|
|
28
|
+
const uptimePercent = computeUptime(events)
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ events, uptimePercent })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function computeUptime(events: ConnectorHealthEvent[]): number {
|
|
34
|
+
if (events.length === 0) return 0
|
|
35
|
+
|
|
36
|
+
const firstTime = new Date(events[0].timestamp).getTime()
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const totalMs = now - firstTime
|
|
39
|
+
if (totalMs <= 0) return 100
|
|
40
|
+
|
|
41
|
+
let uptimeMs = 0
|
|
42
|
+
let lastUpAt: number | null = null
|
|
43
|
+
|
|
44
|
+
for (const ev of events) {
|
|
45
|
+
const t = new Date(ev.timestamp).getTime()
|
|
46
|
+
if (ev.event === 'started' || ev.event === 'reconnected') {
|
|
47
|
+
if (lastUpAt === null) {
|
|
48
|
+
lastUpAt = t
|
|
49
|
+
}
|
|
50
|
+
} else if (ev.event === 'stopped' || ev.event === 'error' || ev.event === 'disconnected') {
|
|
51
|
+
if (lastUpAt !== null) {
|
|
52
|
+
uptimeMs += t - lastUpAt
|
|
53
|
+
lastUpAt = null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If still up, count time until now
|
|
59
|
+
if (lastUpAt !== null) {
|
|
60
|
+
uptimeMs += now - lastUpAt
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Math.round((uptimeMs / totalMs) * 10000) / 100
|
|
64
|
+
}
|
|
@@ -2,6 +2,8 @@ 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 { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
6
|
+
import { z } from 'zod'
|
|
5
7
|
import type { Connector } from '@/types'
|
|
6
8
|
export const dynamic = 'force-dynamic'
|
|
7
9
|
|
|
@@ -10,7 +12,7 @@ export async function GET(_req: Request) {
|
|
|
10
12
|
const connectors = loadConnectors()
|
|
11
13
|
// Merge runtime status from manager
|
|
12
14
|
try {
|
|
13
|
-
const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR } = await import('@/lib/server/connectors/manager')
|
|
15
|
+
const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
14
16
|
for (const c of Object.values(connectors) as Connector[]) {
|
|
15
17
|
c.status = getConnectorStatus(c.id)
|
|
16
18
|
if (c.platform === 'whatsapp') {
|
|
@@ -19,13 +21,26 @@ export async function GET(_req: Request) {
|
|
|
19
21
|
const qr = getConnectorQR(c.id)
|
|
20
22
|
if (qr) c.qrDataUrl = qr
|
|
21
23
|
}
|
|
24
|
+
// Surface reconnect state if connector is in a recovery cycle
|
|
25
|
+
const rState = getReconnectState(c.id)
|
|
26
|
+
if (rState) {
|
|
27
|
+
const ext = c as unknown as Record<string, unknown>
|
|
28
|
+
ext.reconnectAttempts = rState.attempts
|
|
29
|
+
ext.nextRetryAt = rState.nextRetryAt
|
|
30
|
+
ext.reconnectError = rState.error
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
} catch { /* manager not loaded yet */ }
|
|
24
34
|
return NextResponse.json(connectors)
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export async function POST(req: Request) {
|
|
28
|
-
const
|
|
38
|
+
const raw = await req.json()
|
|
39
|
+
const parsed = ConnectorCreateSchema.safeParse(raw)
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
|
|
42
|
+
}
|
|
43
|
+
const body = parsed.data
|
|
29
44
|
const connectors = loadConnectors()
|
|
30
45
|
const id = genId()
|
|
31
46
|
|
|
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
|
|
|
25
25
|
return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
|
|
28
|
+
const { title, content, tags, scope, agentIds, source, sourceUrl } = body as Record<string, unknown>
|
|
29
29
|
|
|
30
30
|
if (typeof title !== 'string' || !title.trim()) {
|
|
31
31
|
return NextResponse.json({ error: 'title is required.' }, { status: 400 })
|
|
@@ -43,12 +43,17 @@ export async function POST(req: Request) {
|
|
|
43
43
|
? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
|
|
44
44
|
: []
|
|
45
45
|
|
|
46
|
+
const normalizedSource = typeof source === 'string' && source.trim() ? source.trim() : undefined
|
|
47
|
+
const normalizedSourceUrl = typeof sourceUrl === 'string' && sourceUrl.trim() ? sourceUrl.trim() : undefined
|
|
48
|
+
|
|
46
49
|
const entry = addKnowledge({
|
|
47
50
|
title: title.trim(),
|
|
48
51
|
content,
|
|
49
52
|
tags: normalizedTags,
|
|
50
53
|
scope: normalizedScope,
|
|
51
54
|
agentIds: normalizedAgentIds,
|
|
55
|
+
source: normalizedSource,
|
|
56
|
+
sourceUrl: normalizedSourceUrl,
|
|
52
57
|
})
|
|
53
58
|
|
|
54
59
|
return NextResponse.json(entry)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { runOpenClawDoctor } from '@/lib/server/openclaw-doctor'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const result = await runOpenClawDoctor()
|
|
8
|
+
return NextResponse.json(result)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
const body = await req.json().catch(() => ({}))
|
|
13
|
+
const fix = typeof body.fix === 'boolean' ? body.fix : false
|
|
14
|
+
const workspace = typeof body.workspace === 'string' ? body.workspace : undefined
|
|
15
|
+
const result = await runOpenClawDoctor({ fix, workspace })
|
|
16
|
+
return NextResponse.json(result)
|
|
17
|
+
}
|
|
@@ -53,7 +53,10 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
|
|
|
53
53
|
existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
|
|
54
54
|
existingTask.result = null
|
|
55
55
|
existingTask.error = null
|
|
56
|
+
existingTask.outputFiles = []
|
|
57
|
+
existingTask.artifacts = []
|
|
56
58
|
existingTask.sessionId = null
|
|
59
|
+
existingTask.completionReportPath = null
|
|
57
60
|
existingTask.updatedAt = now
|
|
58
61
|
existingTask.queuedAt = null
|
|
59
62
|
existingTask.startedAt = null
|
|
@@ -25,6 +25,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const encoder = new TextEncoder()
|
|
28
|
+
let abortRun: (() => void) | null = null
|
|
28
29
|
const stream = new ReadableStream({
|
|
29
30
|
start(controller) {
|
|
30
31
|
let closed = false
|
|
@@ -48,7 +49,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
48
49
|
mode: queueMode,
|
|
49
50
|
onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
|
|
50
51
|
replyToId,
|
|
52
|
+
callerSignal: req.signal,
|
|
51
53
|
})
|
|
54
|
+
abortRun = run.abort
|
|
52
55
|
|
|
53
56
|
log.info('chat', `Enqueued session run ${run.runId}`, {
|
|
54
57
|
sessionId: id,
|
|
@@ -86,7 +89,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
86
89
|
})
|
|
87
90
|
},
|
|
88
91
|
cancel() {
|
|
89
|
-
// Client disconnected
|
|
92
|
+
// Client disconnected — abort the run so the LLM stream is cancelled.
|
|
93
|
+
abortRun?.()
|
|
90
94
|
},
|
|
91
95
|
})
|
|
92
96
|
|
|
@@ -11,7 +11,7 @@ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main
|
|
|
11
11
|
export const dynamic = 'force-dynamic'
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
export async function GET(
|
|
14
|
+
export async function GET(req: Request) {
|
|
15
15
|
const sessions = loadSessions()
|
|
16
16
|
for (const id of Object.keys(sessions)) {
|
|
17
17
|
const run = getSessionRunState(id)
|
|
@@ -19,7 +19,16 @@ export async function GET(_req: Request) {
|
|
|
19
19
|
sessions[id].queuedCount = run.queueLength
|
|
20
20
|
sessions[id].currentRunId = run.runningRunId || null
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
const { searchParams } = new URL(req.url)
|
|
24
|
+
const limitParam = searchParams.get('limit')
|
|
25
|
+
if (!limitParam) return NextResponse.json(sessions)
|
|
26
|
+
|
|
27
|
+
const limit = Math.max(1, Number(limitParam) || 50)
|
|
28
|
+
const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
|
|
29
|
+
const all = Object.values(sessions).sort((a, b) => (b.lastActiveAt ?? b.createdAt) - (a.lastActiveAt ?? a.createdAt))
|
|
30
|
+
const items = all.slice(offset, offset + limit)
|
|
31
|
+
return NextResponse.json({ items, total: all.length, hasMore: offset + limit < all.length })
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export async function DELETE(req: Request) {
|
|
@@ -10,6 +10,7 @@ import { notify } from '@/lib/server/ws-hub'
|
|
|
10
10
|
import { createNotification } from '@/lib/server/create-notification'
|
|
11
11
|
import { enqueueSystemEvent } from '@/lib/server/system-events'
|
|
12
12
|
import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
|
|
13
|
+
import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
|
|
13
14
|
|
|
14
15
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
15
16
|
// Keep completed queue integrity even if daemon is not running.
|
|
@@ -29,6 +30,17 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
29
30
|
|
|
30
31
|
const prevStatus = tasks[id].status
|
|
31
32
|
|
|
33
|
+
// DAG validation: reject if proposed blockedBy would create a cycle
|
|
34
|
+
if (Array.isArray(body.blockedBy)) {
|
|
35
|
+
const dagResult = validateDag(tasks, id, body.blockedBy)
|
|
36
|
+
if (!dagResult.valid) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: 'Dependency cycle detected', cycle: dagResult.cycle },
|
|
39
|
+
{ status: 400 },
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
// Support atomic comment append to avoid race conditions
|
|
33
45
|
if (body.appendComment) {
|
|
34
46
|
if (!tasks[id].comments) tasks[id].comments = []
|
|
@@ -114,20 +126,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
// When a task is completed,
|
|
129
|
+
// When a task is completed, cascade unblock dependent tasks
|
|
118
130
|
if (tasks[id].status === 'completed') {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const allDone = deps.every((depId: string) => tasks[depId]?.status === 'completed')
|
|
125
|
-
if (allDone && (blocked.status === 'backlog' || blocked.status === 'todo')) {
|
|
126
|
-
blocked.status = 'queued'
|
|
127
|
-
blocked.queuedAt = Date.now()
|
|
128
|
-
blocked.updatedAt = Date.now()
|
|
129
|
-
saveTasks(tasks)
|
|
130
|
-
enqueueTask(blockedId)
|
|
131
|
+
const unblockedIds = cascadeUnblock(tasks, id)
|
|
132
|
+
if (unblockedIds.length > 0) {
|
|
133
|
+
saveTasks(tasks)
|
|
134
|
+
for (const uid of unblockedIds) {
|
|
135
|
+
enqueueTask(uid)
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
|
|
4
|
+
import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
5
|
+
import { z } from 'zod'
|
|
4
6
|
import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
5
7
|
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
6
8
|
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
@@ -8,6 +10,7 @@ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
|
8
10
|
import { notify } from '@/lib/server/ws-hub'
|
|
9
11
|
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
10
12
|
import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
|
|
13
|
+
import { validateDag } from '@/lib/server/dag-validation'
|
|
11
14
|
|
|
12
15
|
export async function GET(req: Request) {
|
|
13
16
|
// Keep completed queue integrity even if daemon is not running.
|
|
@@ -55,7 +58,12 @@ export async function DELETE(req: Request) {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export async function POST(req: Request) {
|
|
58
|
-
const
|
|
61
|
+
const raw = await req.json()
|
|
62
|
+
const parsed = TaskCreateSchema.safeParse(raw)
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
|
|
65
|
+
}
|
|
66
|
+
const body = { ...raw, ...parsed.data }
|
|
59
67
|
const id = genId()
|
|
60
68
|
const now = Date.now()
|
|
61
69
|
const tasks = loadTasks()
|
|
@@ -66,6 +74,17 @@ export async function POST(req: Request) {
|
|
|
66
74
|
const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
|
|
67
75
|
? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
|
|
68
76
|
: Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
|
|
77
|
+
// DAG validation: reject if proposed blockedBy would create a cycle
|
|
78
|
+
if (Array.isArray(body.blockedBy) && body.blockedBy.length > 0) {
|
|
79
|
+
const dagResult = validateDag(tasks, id, body.blockedBy)
|
|
80
|
+
if (!dagResult.valid) {
|
|
81
|
+
return NextResponse.json(
|
|
82
|
+
{ error: 'Dependency cycle detected', cycle: dagResult.cycle },
|
|
83
|
+
{ status: 400 },
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
// Resolve @mentions in description to auto-assign agent
|
|
70
89
|
const resolvedAgentId = body.description
|
|
71
90
|
? resolveTaskAgentFromDescription(body.description, body.agentId || '', loadAgents())
|
|
@@ -84,6 +103,30 @@ export async function POST(req: Request) {
|
|
|
84
103
|
sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
|
|
85
104
|
result: typeof body.result === 'string' ? body.result : null,
|
|
86
105
|
error: typeof body.error === 'string' ? body.error : null,
|
|
106
|
+
outputFiles: Array.isArray(body.outputFiles)
|
|
107
|
+
? body.outputFiles.filter((entry: unknown) => typeof entry === 'string').slice(0, 24)
|
|
108
|
+
: [],
|
|
109
|
+
artifacts: Array.isArray(body.artifacts)
|
|
110
|
+
? body.artifacts
|
|
111
|
+
.filter((artifact: unknown) => artifact && typeof artifact === 'object')
|
|
112
|
+
.map((artifact: unknown) => {
|
|
113
|
+
const row = artifact as {
|
|
114
|
+
url?: unknown
|
|
115
|
+
type?: unknown
|
|
116
|
+
filename?: unknown
|
|
117
|
+
}
|
|
118
|
+
const normalizedType = String(row.type || '')
|
|
119
|
+
return {
|
|
120
|
+
url: String(row.url || ''),
|
|
121
|
+
type: ['image', 'video', 'pdf', 'file'].includes(normalizedType)
|
|
122
|
+
? (normalizedType as 'image' | 'video' | 'pdf' | 'file')
|
|
123
|
+
: 'file',
|
|
124
|
+
filename: String(row.filename || ''),
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.filter((artifact: { url: string; filename: string }) => artifact.url && artifact.filename)
|
|
128
|
+
.slice(0, 24)
|
|
129
|
+
: [],
|
|
87
130
|
createdAt: now,
|
|
88
131
|
updatedAt: now,
|
|
89
132
|
queuedAt: null,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { loadUsage } from '@/lib/server/storage'
|
|
2
|
+
import { loadUsage, loadSessions, loadAgents } from '@/lib/server/storage'
|
|
3
3
|
import type { UsageRecord } from '@/types'
|
|
4
4
|
export const dynamic = 'force-dynamic'
|
|
5
5
|
|
|
@@ -41,10 +41,14 @@ export async function GET(req: Request) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Build session→agent lookup
|
|
45
|
+
const sessions = loadSessions() as Record<string, { agentId?: string }>
|
|
46
|
+
const agents = loadAgents() as Record<string, { name?: string }>
|
|
47
|
+
|
|
44
48
|
// Compute summaries
|
|
45
49
|
let totalTokens = 0
|
|
46
50
|
let totalCost = 0
|
|
47
|
-
const byAgent: Record<string, { tokens: number;
|
|
51
|
+
const byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }> = {}
|
|
48
52
|
const byProvider: Record<string, { tokens: number; cost: number }> = {}
|
|
49
53
|
const bucketMap: Record<string, { tokens: number; cost: number }> = {}
|
|
50
54
|
|
|
@@ -60,11 +64,16 @@ export async function GET(req: Request) {
|
|
|
60
64
|
byProvider[prov].tokens += tokens
|
|
61
65
|
byProvider[prov].cost += cost
|
|
62
66
|
|
|
63
|
-
// by agent
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// by agent — resolve sessionId → agentId → agent name
|
|
68
|
+
const session = r.sessionId ? sessions[r.sessionId] : undefined
|
|
69
|
+
const agentId = session?.agentId || 'unknown'
|
|
70
|
+
const agentName = agentId !== 'unknown' && agents[agentId]?.name
|
|
71
|
+
? agents[agentId].name
|
|
72
|
+
: agentId
|
|
73
|
+
if (!byAgent[agentId]) byAgent[agentId] = { name: agentName, cost: 0, tokens: 0, count: 0 }
|
|
74
|
+
byAgent[agentId].cost += cost
|
|
75
|
+
byAgent[agentId].tokens += tokens
|
|
76
|
+
byAgent[agentId].count += 1
|
|
68
77
|
|
|
69
78
|
// time series bucketing
|
|
70
79
|
const bk = bucketKey(r.timestamp || now, range)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
|
|
3
|
+
import { sendSol } from '@/lib/server/solana'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import type { AgentWallet, WalletTransaction } from '@/types'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
const { id } = await params
|
|
10
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
11
|
+
const wallet = wallets[id]
|
|
12
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
13
|
+
|
|
14
|
+
const body = await req.json()
|
|
15
|
+
const transactionId = typeof body.transactionId === 'string' ? body.transactionId.trim() : ''
|
|
16
|
+
const decision = body.decision as 'approve' | 'deny'
|
|
17
|
+
|
|
18
|
+
if (!transactionId) {
|
|
19
|
+
return NextResponse.json({ error: 'transactionId is required' }, { status: 400 })
|
|
20
|
+
}
|
|
21
|
+
if (decision !== 'approve' && decision !== 'deny') {
|
|
22
|
+
return NextResponse.json({ error: 'decision must be "approve" or "deny"' }, { status: 400 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
|
|
26
|
+
const tx = allTxs[transactionId]
|
|
27
|
+
if (!tx || tx.walletId !== id) {
|
|
28
|
+
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
|
29
|
+
}
|
|
30
|
+
if (tx.status !== 'pending_approval') {
|
|
31
|
+
return NextResponse.json({ error: `Transaction is already ${tx.status}` }, { status: 409 })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (decision === 'deny') {
|
|
35
|
+
tx.status = 'denied'
|
|
36
|
+
tx.approvedBy = 'user'
|
|
37
|
+
upsertWalletTransaction(transactionId, tx)
|
|
38
|
+
notify('wallets')
|
|
39
|
+
return NextResponse.json({ status: 'denied', transactionId })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Approve — sign and submit
|
|
43
|
+
try {
|
|
44
|
+
const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, tx.toAddress, tx.amountLamports)
|
|
45
|
+
tx.status = 'confirmed'
|
|
46
|
+
tx.signature = signature
|
|
47
|
+
tx.feeLamports = fee
|
|
48
|
+
tx.approvedBy = 'user'
|
|
49
|
+
upsertWalletTransaction(transactionId, tx)
|
|
50
|
+
notify('wallets')
|
|
51
|
+
return NextResponse.json({ status: 'confirmed', transactionId, signature })
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
tx.status = 'failed'
|
|
54
|
+
upsertWalletTransaction(transactionId, tx)
|
|
55
|
+
notify('wallets')
|
|
56
|
+
return NextResponse.json({
|
|
57
|
+
error: err instanceof Error ? err.message : String(err),
|
|
58
|
+
transactionId,
|
|
59
|
+
status: 'failed',
|
|
60
|
+
}, { status: 500 })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadWallets, loadWalletBalanceHistory } from '@/lib/server/storage'
|
|
3
|
+
import type { AgentWallet, WalletBalanceSnapshot } from '@/types'
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
const { id } = await params
|
|
8
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
9
|
+
const wallet = wallets[id]
|
|
10
|
+
if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
|
|
11
|
+
|
|
12
|
+
const allSnapshots = loadWalletBalanceHistory() as Record<string, WalletBalanceSnapshot>
|
|
13
|
+
const walletSnapshots = Object.values(allSnapshots)
|
|
14
|
+
.filter((s) => s.walletId === id)
|
|
15
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
16
|
+
|
|
17
|
+
return NextResponse.json(walletSnapshots)
|
|
18
|
+
}
|