@swarmclawai/swarmclaw 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadChatrooms, saveChatrooms, loadAgents, loadSettings, loadSkills, loadCredentials, decryptKey } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
6
|
+
import { streamAgentChat } from '@/lib/server/stream-agent-chat'
|
|
7
|
+
import { getProvider } from '@/lib/providers'
|
|
8
|
+
import type { Chatroom, ChatroomMessage, Agent, Session, Message } from '@/types'
|
|
9
|
+
|
|
10
|
+
export const dynamic = 'force-dynamic'
|
|
11
|
+
export const maxDuration = 300
|
|
12
|
+
|
|
13
|
+
const MAX_CHAIN_DEPTH = 5
|
|
14
|
+
|
|
15
|
+
/** Resolve API key from an agent's credentialId */
|
|
16
|
+
function resolveApiKey(credentialId: string | null | undefined): string | null {
|
|
17
|
+
if (!credentialId) return null
|
|
18
|
+
const creds = loadCredentials()
|
|
19
|
+
const cred = creds[credentialId]
|
|
20
|
+
if (!cred?.encryptedKey) return null
|
|
21
|
+
try { return decryptKey(cred.encryptedKey) } catch { return null }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parse @mentions from message text, returns matching agentIds */
|
|
25
|
+
function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
|
|
26
|
+
if (/@all\b/i.test(text)) return [...memberIds]
|
|
27
|
+
const mentionPattern = /@(\S+)/g
|
|
28
|
+
const mentioned: string[] = []
|
|
29
|
+
let match: RegExpExecArray | null
|
|
30
|
+
while ((match = mentionPattern.exec(text)) !== null) {
|
|
31
|
+
const name = match[1].toLowerCase()
|
|
32
|
+
for (const id of memberIds) {
|
|
33
|
+
const agent = agents[id]
|
|
34
|
+
if (agent && agent.name.toLowerCase().replace(/\s+/g, '') === name) {
|
|
35
|
+
if (!mentioned.includes(id)) mentioned.push(id)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return mentioned
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build chatroom context as a system prompt addendum with agent profiles and collaboration guidelines */
|
|
43
|
+
function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<string, Agent>, agentId: string): string {
|
|
44
|
+
const selfAgent = agents[agentId]
|
|
45
|
+
const selfName = selfAgent?.name || agentId
|
|
46
|
+
|
|
47
|
+
// Build team profiles with capabilities
|
|
48
|
+
const teamProfiles = chatroom.agentIds
|
|
49
|
+
.filter((id) => id !== agentId)
|
|
50
|
+
.map((id) => {
|
|
51
|
+
const a = agents[id]
|
|
52
|
+
if (!a) return null
|
|
53
|
+
const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
|
|
54
|
+
const desc = a.description || a.soul || 'No description'
|
|
55
|
+
return `- **${a.name}**: ${desc}\n ${tools}`
|
|
56
|
+
})
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join('\n')
|
|
59
|
+
|
|
60
|
+
const recentMessages = chatroom.messages.slice(-30).map((m) => {
|
|
61
|
+
return `[${m.senderName}]: ${m.text}`
|
|
62
|
+
}).join('\n')
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
`## Chatroom Context`,
|
|
66
|
+
`You are **${selfName}** in chatroom "${chatroom.name}".`,
|
|
67
|
+
selfAgent?.description ? `Your role: ${selfAgent.description}` : '',
|
|
68
|
+
selfAgent?.tools?.length ? `Your tools: ${selfAgent.tools.join(', ')}` : '',
|
|
69
|
+
'',
|
|
70
|
+
'## Team Members',
|
|
71
|
+
teamProfiles || '(no other agents)',
|
|
72
|
+
'',
|
|
73
|
+
'## Collaboration Guidelines',
|
|
74
|
+
'- Before executing complex tasks, briefly discuss your approach with the team.',
|
|
75
|
+
'- When delegating to another agent, explain what you need, why they are best suited, and what output you expect. Example: "@DataBot I need a summary of recent API errors from the logs — you have the shell tool to grep through them."',
|
|
76
|
+
'- If someone mentions a task you are well-suited for, proactively offer to help.',
|
|
77
|
+
'- Do not just @mention mechanically — explain your reasoning when involving others.',
|
|
78
|
+
'- If you can handle a request entirely yourself, just do it. Only delegate what you cannot do.',
|
|
79
|
+
'',
|
|
80
|
+
'## Recent Messages',
|
|
81
|
+
recentMessages || '(no messages yet)',
|
|
82
|
+
].filter((line) => line !== undefined).join('\n')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Build a synthetic session object for an agent in a chatroom */
|
|
86
|
+
function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
87
|
+
return {
|
|
88
|
+
id: `chatroom-${chatroomId}-${agent.id}`,
|
|
89
|
+
name: `Chatroom session for ${agent.name}`,
|
|
90
|
+
cwd: process.cwd(),
|
|
91
|
+
user: 'chatroom',
|
|
92
|
+
provider: agent.provider,
|
|
93
|
+
model: agent.model,
|
|
94
|
+
credentialId: agent.credentialId ?? null,
|
|
95
|
+
fallbackCredentialIds: agent.fallbackCredentialIds,
|
|
96
|
+
apiEndpoint: agent.apiEndpoint ?? null,
|
|
97
|
+
claudeSessionId: null,
|
|
98
|
+
messages: [],
|
|
99
|
+
createdAt: Date.now(),
|
|
100
|
+
lastActiveAt: Date.now(),
|
|
101
|
+
tools: agent.tools || [],
|
|
102
|
+
agentId: agent.id,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Build agent's system prompt including skills */
|
|
107
|
+
function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
108
|
+
const settings = loadSettings()
|
|
109
|
+
const parts: string[] = []
|
|
110
|
+
if (settings.userPrompt) parts.push(settings.userPrompt)
|
|
111
|
+
if (agent.soul) parts.push(agent.soul)
|
|
112
|
+
if (agent.systemPrompt) parts.push(agent.systemPrompt)
|
|
113
|
+
if (agent.skillIds?.length) {
|
|
114
|
+
const allSkills = loadSkills()
|
|
115
|
+
for (const skillId of agent.skillIds) {
|
|
116
|
+
const skill = allSkills[skillId]
|
|
117
|
+
if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return parts.join('\n\n')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Convert chatroom messages to Message history format for LLM */
|
|
124
|
+
function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imagePath?: string, attachedFiles?: string[]): Message[] {
|
|
125
|
+
const history = chatroom.messages.slice(-50).map((m) => {
|
|
126
|
+
let msgText = `[${m.senderName}]: ${m.text}`
|
|
127
|
+
// Include attachment info in history
|
|
128
|
+
if (m.attachedFiles?.length) {
|
|
129
|
+
const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
|
|
130
|
+
msgText += `\n[Attached: ${names}]`
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
role: m.senderId === agentId ? 'assistant' as const : 'user' as const,
|
|
134
|
+
text: msgText,
|
|
135
|
+
time: m.time,
|
|
136
|
+
...(m.imagePath ? { imagePath: m.imagePath } : {}),
|
|
137
|
+
...(m.attachedFiles ? { attachedFiles: m.attachedFiles } : {}),
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
// Pass through imagePath/attachedFiles from the current message to the last history entry
|
|
141
|
+
if (history.length > 0 && (imagePath || attachedFiles)) {
|
|
142
|
+
const last = history[history.length - 1]
|
|
143
|
+
if (imagePath && !last.imagePath) last.imagePath = imagePath
|
|
144
|
+
if (attachedFiles && !last.attachedFiles) last.attachedFiles = attachedFiles
|
|
145
|
+
}
|
|
146
|
+
return history
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
150
|
+
const { id } = await params
|
|
151
|
+
const body = await req.json()
|
|
152
|
+
|
|
153
|
+
const chatrooms = loadChatrooms()
|
|
154
|
+
const chatroom = chatrooms[id] as Chatroom | undefined
|
|
155
|
+
if (!chatroom) return notFound()
|
|
156
|
+
|
|
157
|
+
const text = typeof body.text === 'string' ? body.text : ''
|
|
158
|
+
const senderId = typeof body.senderId === 'string' ? body.senderId : 'user'
|
|
159
|
+
const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
|
|
160
|
+
const attachedFiles = Array.isArray(body.attachedFiles)
|
|
161
|
+
? (body.attachedFiles as unknown[]).filter((f): f is string => typeof f === 'string')
|
|
162
|
+
: undefined
|
|
163
|
+
const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
|
|
164
|
+
|
|
165
|
+
if (!text.trim() && !imagePath && !attachedFiles?.length) {
|
|
166
|
+
return NextResponse.json({ error: 'text or attachment is required' }, { status: 400 })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const agents = loadAgents() as Record<string, Agent>
|
|
170
|
+
|
|
171
|
+
// Persist incoming message
|
|
172
|
+
const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
|
|
173
|
+
let mentions = parseMentions(text, agents, chatroom.agentIds)
|
|
174
|
+
// Auto-address: if enabled and no explicit mentions, address all agents
|
|
175
|
+
if (chatroom.autoAddress && mentions.length === 0) {
|
|
176
|
+
mentions = [...chatroom.agentIds]
|
|
177
|
+
}
|
|
178
|
+
const userMessage: ChatroomMessage = {
|
|
179
|
+
id: genId(),
|
|
180
|
+
senderId,
|
|
181
|
+
senderName,
|
|
182
|
+
role: senderId === 'user' ? 'user' : 'assistant',
|
|
183
|
+
text,
|
|
184
|
+
mentions,
|
|
185
|
+
reactions: [],
|
|
186
|
+
time: Date.now(),
|
|
187
|
+
...(imagePath ? { imagePath } : {}),
|
|
188
|
+
...(attachedFiles ? { attachedFiles } : {}),
|
|
189
|
+
...(replyToId ? { replyToId } : {}),
|
|
190
|
+
}
|
|
191
|
+
chatroom.messages.push(userMessage)
|
|
192
|
+
chatroom.updatedAt = Date.now()
|
|
193
|
+
chatrooms[id] = chatroom
|
|
194
|
+
saveChatrooms(chatrooms)
|
|
195
|
+
notify('chatrooms')
|
|
196
|
+
notify(`chatroom:${id}`)
|
|
197
|
+
|
|
198
|
+
// Build reply context if replying to a message
|
|
199
|
+
let replyContext = ''
|
|
200
|
+
if (replyToId) {
|
|
201
|
+
const replyMsg = chatroom.messages.find((m) => m.id === replyToId)
|
|
202
|
+
if (replyMsg) {
|
|
203
|
+
const truncated = replyMsg.text.length > 200 ? replyMsg.text.slice(0, 200) + '...' : replyMsg.text
|
|
204
|
+
replyContext = `> [${replyMsg.senderName}]: ${truncated}\n\n`
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// SSE stream
|
|
209
|
+
const encoder = new TextEncoder()
|
|
210
|
+
const stream = new ReadableStream({
|
|
211
|
+
start(controller) {
|
|
212
|
+
let closed = false
|
|
213
|
+
const writeEvent = (event: Record<string, unknown>) => {
|
|
214
|
+
if (closed) return
|
|
215
|
+
try {
|
|
216
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
|
|
217
|
+
} catch {
|
|
218
|
+
closed = true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const processAgents = async () => {
|
|
223
|
+
// Build agent queue: start with mentioned agents, then chain
|
|
224
|
+
const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
|
|
225
|
+
const processed = new Set<string>()
|
|
226
|
+
const agentQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = []
|
|
227
|
+
|
|
228
|
+
/** Process a single agent: stream response, persist message, return chained mentions */
|
|
229
|
+
const processOneAgent = async (item: { agentId: string; depth: number; contextMessage?: string }): Promise<string[]> => {
|
|
230
|
+
if (processed.has(item.agentId) || item.depth >= MAX_CHAIN_DEPTH) return []
|
|
231
|
+
processed.add(item.agentId)
|
|
232
|
+
|
|
233
|
+
const agent = agents[item.agentId]
|
|
234
|
+
if (!agent) return []
|
|
235
|
+
|
|
236
|
+
// Pre-flight: check if the agent's provider is usable before attempting to stream
|
|
237
|
+
const providerInfo = getProvider(agent.provider)
|
|
238
|
+
const apiKey = resolveApiKey(agent.credentialId)
|
|
239
|
+
if (providerInfo?.requiresApiKey && !apiKey) {
|
|
240
|
+
writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
|
|
241
|
+
writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
|
|
242
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
243
|
+
return []
|
|
244
|
+
}
|
|
245
|
+
if (providerInfo?.requiresEndpoint && !agent.apiEndpoint) {
|
|
246
|
+
writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
|
|
247
|
+
writeEvent({ t: 'err', text: `${agent.name} has no endpoint configured`, agentId: agent.id, agentName: agent.name })
|
|
248
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const freshChatrooms = loadChatrooms()
|
|
256
|
+
const freshChatroom = freshChatrooms[id] as Chatroom
|
|
257
|
+
|
|
258
|
+
const syntheticSession = buildSyntheticSession(agent, id)
|
|
259
|
+
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
260
|
+
const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
|
|
261
|
+
const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
|
|
262
|
+
const history = buildHistoryForAgent(freshChatroom, agent.id, imagePath, attachedFiles)
|
|
263
|
+
|
|
264
|
+
// Use enriched context message for chained agents, or reply context + original text
|
|
265
|
+
const messageForAgent = item.contextMessage || (replyContext + text)
|
|
266
|
+
|
|
267
|
+
let fullText = ''
|
|
268
|
+
let agentError = ''
|
|
269
|
+
const result = await streamAgentChat({
|
|
270
|
+
session: syntheticSession,
|
|
271
|
+
message: messageForAgent,
|
|
272
|
+
imagePath,
|
|
273
|
+
attachedFiles,
|
|
274
|
+
apiKey,
|
|
275
|
+
systemPrompt: fullSystemPrompt,
|
|
276
|
+
write: (raw: string) => {
|
|
277
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
if (!line.startsWith('data: ')) continue
|
|
280
|
+
try {
|
|
281
|
+
const parsed = JSON.parse(line.slice(6).trim())
|
|
282
|
+
if (parsed.t === 'd' && parsed.text) {
|
|
283
|
+
fullText += parsed.text
|
|
284
|
+
writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
285
|
+
} else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
|
|
286
|
+
writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
|
|
287
|
+
} else if (parsed.t === 'err' && parsed.text) {
|
|
288
|
+
agentError = parsed.text
|
|
289
|
+
writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// skip malformed lines
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
history,
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const responseText = result.fullText || fullText
|
|
300
|
+
|
|
301
|
+
// Don't persist empty or error-only messages — they pollute chat history
|
|
302
|
+
if (!responseText.trim() && agentError) {
|
|
303
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
304
|
+
return []
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (responseText.trim()) {
|
|
308
|
+
const newMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
|
|
309
|
+
const agentMessage: ChatroomMessage = {
|
|
310
|
+
id: genId(),
|
|
311
|
+
senderId: agent.id,
|
|
312
|
+
senderName: agent.name,
|
|
313
|
+
role: 'assistant',
|
|
314
|
+
text: responseText,
|
|
315
|
+
mentions: newMentions,
|
|
316
|
+
reactions: [],
|
|
317
|
+
time: Date.now(),
|
|
318
|
+
}
|
|
319
|
+
const latestChatrooms = loadChatrooms()
|
|
320
|
+
const latestChatroom = latestChatrooms[id] as Chatroom
|
|
321
|
+
latestChatroom.messages.push(agentMessage)
|
|
322
|
+
latestChatroom.updatedAt = Date.now()
|
|
323
|
+
latestChatrooms[id] = latestChatroom
|
|
324
|
+
saveChatrooms(latestChatrooms)
|
|
325
|
+
notify(`chatroom:${id}`)
|
|
326
|
+
|
|
327
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
328
|
+
|
|
329
|
+
// Return chained agent IDs — enriched context is built below when queuing
|
|
330
|
+
return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
334
|
+
return []
|
|
335
|
+
} catch (err: unknown) {
|
|
336
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
337
|
+
writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
|
|
338
|
+
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
339
|
+
return []
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (chatroom.chatMode === 'parallel') {
|
|
344
|
+
// Process initial batch in parallel
|
|
345
|
+
const results = await Promise.all(initialQueue.map(processOneAgent))
|
|
346
|
+
// Chained agents from parallel responses queue sequentially
|
|
347
|
+
for (const chainedIds of results) {
|
|
348
|
+
for (const cid of chainedIds) {
|
|
349
|
+
agentQueue.push({ agentId: cid, depth: 1 })
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
// Sequential: push initial queue items
|
|
354
|
+
agentQueue.push(...initialQueue)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Process remaining chained agents sequentially with enriched context
|
|
358
|
+
while (agentQueue.length > 0) {
|
|
359
|
+
const item = agentQueue.shift()!
|
|
360
|
+
|
|
361
|
+
// Build enriched context for chained agents by looking at the most recent message
|
|
362
|
+
if (item.depth > 0 && !item.contextMessage) {
|
|
363
|
+
const latestChatrooms = loadChatrooms()
|
|
364
|
+
const latestChatroom = latestChatrooms[id] as Chatroom
|
|
365
|
+
const lastAgentMsg = [...latestChatroom.messages].reverse().find(
|
|
366
|
+
(m) => m.role === 'assistant' && m.senderId !== item.agentId
|
|
367
|
+
)
|
|
368
|
+
if (lastAgentMsg) {
|
|
369
|
+
const truncated = lastAgentMsg.text.length > 500 ? lastAgentMsg.text.slice(0, 500) + '...' : lastAgentMsg.text
|
|
370
|
+
item.contextMessage = `${lastAgentMsg.senderName} said: "${truncated}" — They're requesting your help. Review the conversation and respond.`
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const chainedIds = await processOneAgent(item)
|
|
375
|
+
for (const cid of chainedIds) {
|
|
376
|
+
agentQueue.push({ agentId: cid, depth: item.depth + 1 })
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
writeEvent({ t: 'done' })
|
|
381
|
+
if (!closed) {
|
|
382
|
+
try { controller.close() } catch { /* already closed */ }
|
|
383
|
+
closed = true
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
processAgents().catch((err) => {
|
|
388
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
389
|
+
writeEvent({ t: 'err', text: msg })
|
|
390
|
+
writeEvent({ t: 'done' })
|
|
391
|
+
if (!closed) {
|
|
392
|
+
try { controller.close() } catch { /* already closed */ }
|
|
393
|
+
closed = true
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
},
|
|
397
|
+
cancel() {
|
|
398
|
+
// Client disconnected
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
return new NextResponse(stream, {
|
|
403
|
+
headers: {
|
|
404
|
+
'Content-Type': 'text/event-stream',
|
|
405
|
+
'Cache-Control': 'no-cache',
|
|
406
|
+
'Connection': 'keep-alive',
|
|
407
|
+
'X-Accel-Buffering': 'no',
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { genId } from '@/lib/id'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
const chatroom = chatrooms[id]
|
|
12
|
+
if (!chatroom) return notFound()
|
|
13
|
+
|
|
14
|
+
const agentId = body.agentId as string
|
|
15
|
+
if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
16
|
+
|
|
17
|
+
if (!chatroom.agentIds.includes(agentId)) {
|
|
18
|
+
chatroom.agentIds.push(agentId)
|
|
19
|
+
|
|
20
|
+
// Inject a system event message
|
|
21
|
+
const agents = loadAgents()
|
|
22
|
+
const agentName = agents[agentId]?.name || 'Unknown agent'
|
|
23
|
+
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
24
|
+
chatroom.messages.push({
|
|
25
|
+
id: genId(),
|
|
26
|
+
senderId: 'system',
|
|
27
|
+
senderName: 'System',
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
text: `${agentName} has joined the chat`,
|
|
30
|
+
mentions: [],
|
|
31
|
+
reactions: [],
|
|
32
|
+
time: Date.now(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
chatroom.updatedAt = Date.now()
|
|
36
|
+
chatrooms[id] = chatroom
|
|
37
|
+
saveChatrooms(chatrooms)
|
|
38
|
+
notify('chatrooms')
|
|
39
|
+
notify(`chatroom:${id}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return NextResponse.json(chatroom)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
46
|
+
const { id } = await params
|
|
47
|
+
const body = await req.json()
|
|
48
|
+
const chatrooms = loadChatrooms()
|
|
49
|
+
const chatroom = chatrooms[id]
|
|
50
|
+
if (!chatroom) return notFound()
|
|
51
|
+
|
|
52
|
+
const agentId = body.agentId as string
|
|
53
|
+
if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
54
|
+
|
|
55
|
+
const wasPresent = chatroom.agentIds.includes(agentId)
|
|
56
|
+
chatroom.agentIds = chatroom.agentIds.filter((aid: string) => aid !== agentId)
|
|
57
|
+
|
|
58
|
+
// Inject a system event message
|
|
59
|
+
if (wasPresent) {
|
|
60
|
+
const agents = loadAgents()
|
|
61
|
+
const agentName = agents[agentId]?.name || 'Unknown agent'
|
|
62
|
+
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
63
|
+
chatroom.messages.push({
|
|
64
|
+
id: genId(),
|
|
65
|
+
senderId: 'system',
|
|
66
|
+
senderName: 'System',
|
|
67
|
+
role: 'assistant',
|
|
68
|
+
text: `${agentName} has left the chat`,
|
|
69
|
+
mentions: [],
|
|
70
|
+
reactions: [],
|
|
71
|
+
time: Date.now(),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
chatroom.updatedAt = Date.now()
|
|
76
|
+
chatrooms[id] = chatroom
|
|
77
|
+
saveChatrooms(chatrooms)
|
|
78
|
+
notify('chatrooms')
|
|
79
|
+
notify(`chatroom:${id}`)
|
|
80
|
+
|
|
81
|
+
return NextResponse.json(chatroom)
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import type { Chatroom } from '@/types'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
const chatroom = chatrooms[id] as Chatroom | undefined
|
|
12
|
+
if (!chatroom) return notFound()
|
|
13
|
+
|
|
14
|
+
const messageId = body.messageId as string
|
|
15
|
+
if (!messageId) {
|
|
16
|
+
return NextResponse.json({ error: 'messageId is required' }, { status: 400 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const message = chatroom.messages.find((m) => m.id === messageId)
|
|
20
|
+
if (!message) {
|
|
21
|
+
return NextResponse.json({ error: 'Message not found' }, { status: 404 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Toggle: remove if pinned, add if not
|
|
25
|
+
if (!chatroom.pinnedMessageIds) chatroom.pinnedMessageIds = []
|
|
26
|
+
const idx = chatroom.pinnedMessageIds.indexOf(messageId)
|
|
27
|
+
if (idx >= 0) {
|
|
28
|
+
chatroom.pinnedMessageIds.splice(idx, 1)
|
|
29
|
+
} else {
|
|
30
|
+
chatroom.pinnedMessageIds.push(messageId)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
chatroom.updatedAt = Date.now()
|
|
34
|
+
chatrooms[id] = chatroom
|
|
35
|
+
saveChatrooms(chatrooms)
|
|
36
|
+
notify(`chatroom:${id}`)
|
|
37
|
+
|
|
38
|
+
return NextResponse.json({ ok: true, pinnedMessageIds: chatroom.pinnedMessageIds })
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import type { Chatroom, ChatroomMessage, ChatroomReaction } from '@/types'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const chatrooms = loadChatrooms()
|
|
11
|
+
const chatroom = chatrooms[id] as Chatroom | undefined
|
|
12
|
+
if (!chatroom) return notFound()
|
|
13
|
+
|
|
14
|
+
const messageId = body.messageId as string
|
|
15
|
+
const emoji = body.emoji as string
|
|
16
|
+
const reactorId = (body.reactorId as string) || 'user'
|
|
17
|
+
if (!messageId || !emoji) {
|
|
18
|
+
return NextResponse.json({ error: 'messageId and emoji are required' }, { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const message = chatroom.messages.find((m: ChatroomMessage) => m.id === messageId)
|
|
22
|
+
if (!message) {
|
|
23
|
+
return NextResponse.json({ error: 'Message not found' }, { status: 404 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Toggle: remove if already exists, add if not
|
|
27
|
+
const existingIdx = message.reactions.findIndex(
|
|
28
|
+
(r: ChatroomReaction) => r.emoji === emoji && r.reactorId === reactorId
|
|
29
|
+
)
|
|
30
|
+
if (existingIdx >= 0) {
|
|
31
|
+
message.reactions.splice(existingIdx, 1)
|
|
32
|
+
} else {
|
|
33
|
+
message.reactions.push({ emoji, reactorId, time: Date.now() })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
chatroom.updatedAt = Date.now()
|
|
37
|
+
chatrooms[id] = chatroom
|
|
38
|
+
saveChatrooms(chatrooms)
|
|
39
|
+
notify(`chatroom:${id}`)
|
|
40
|
+
|
|
41
|
+
return NextResponse.json({ ok: true, reactions: message.reactions })
|
|
42
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { genId } from '@/lib/id'
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const chatrooms = loadChatrooms()
|
|
10
|
+
const chatroom = chatrooms[id]
|
|
11
|
+
if (!chatroom) return notFound()
|
|
12
|
+
return NextResponse.json(chatroom)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
16
|
+
const { id } = await params
|
|
17
|
+
const body = await req.json()
|
|
18
|
+
const chatrooms = loadChatrooms()
|
|
19
|
+
const chatroom = chatrooms[id]
|
|
20
|
+
if (!chatroom) return notFound()
|
|
21
|
+
|
|
22
|
+
if (body.name !== undefined) chatroom.name = body.name
|
|
23
|
+
if (body.description !== undefined) chatroom.description = body.description
|
|
24
|
+
|
|
25
|
+
// Diff agentIds and inject join/leave system messages
|
|
26
|
+
if (Array.isArray(body.agentIds)) {
|
|
27
|
+
const oldIds = new Set(chatroom.agentIds)
|
|
28
|
+
const newIds = new Set(body.agentIds as string[])
|
|
29
|
+
const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
|
|
30
|
+
const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
|
|
31
|
+
|
|
32
|
+
if (added.length > 0 || removed.length > 0) {
|
|
33
|
+
const agents = loadAgents()
|
|
34
|
+
if (!Array.isArray(chatroom.messages)) chatroom.messages = []
|
|
35
|
+
const now = Date.now()
|
|
36
|
+
let offset = 0
|
|
37
|
+
for (const aid of added) {
|
|
38
|
+
chatroom.messages.push({
|
|
39
|
+
id: genId(),
|
|
40
|
+
senderId: 'system',
|
|
41
|
+
senderName: 'System',
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
text: `${agents[aid]?.name || 'Unknown agent'} has joined the chat`,
|
|
44
|
+
mentions: [],
|
|
45
|
+
reactions: [],
|
|
46
|
+
time: now + offset++,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
for (const aid of removed) {
|
|
50
|
+
chatroom.messages.push({
|
|
51
|
+
id: genId(),
|
|
52
|
+
senderId: 'system',
|
|
53
|
+
senderName: 'System',
|
|
54
|
+
role: 'assistant',
|
|
55
|
+
text: `${agents[aid]?.name || 'Unknown agent'} has left the chat`,
|
|
56
|
+
mentions: [],
|
|
57
|
+
reactions: [],
|
|
58
|
+
time: now + offset++,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
chatroom.agentIds = body.agentIds
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
chatroom.updatedAt = Date.now()
|
|
67
|
+
|
|
68
|
+
chatrooms[id] = chatroom
|
|
69
|
+
saveChatrooms(chatrooms)
|
|
70
|
+
notify('chatrooms')
|
|
71
|
+
notify(`chatroom:${id}`)
|
|
72
|
+
return NextResponse.json(chatroom)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
76
|
+
const { id } = await params
|
|
77
|
+
const chatrooms = loadChatrooms()
|
|
78
|
+
if (!chatrooms[id]) return notFound()
|
|
79
|
+
|
|
80
|
+
delete chatrooms[id]
|
|
81
|
+
saveChatrooms(chatrooms)
|
|
82
|
+
notify('chatrooms')
|
|
83
|
+
return NextResponse.json({ ok: true })
|
|
84
|
+
}
|