@swarmclawai/swarmclaw 0.2.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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
interface SpeechRecognitionErrorEvent {
|
|
6
|
+
error?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SpeechRecognitionEvent {
|
|
10
|
+
results: { [index: number]: { [index: number]: { transcript: string } } }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface UseSpeechRecognitionOptions {
|
|
14
|
+
lang?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useSpeechRecognition(onResult: (text: string) => void, options?: UseSpeechRecognitionOptions) {
|
|
18
|
+
const [recording, setRecording] = useState(false)
|
|
19
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
|
+
const recogRef = useRef<{
|
|
21
|
+
stop: () => void
|
|
22
|
+
start: () => void
|
|
23
|
+
continuous: boolean
|
|
24
|
+
interimResults: boolean
|
|
25
|
+
lang: string
|
|
26
|
+
maxAlternatives?: number
|
|
27
|
+
onresult?: (e: SpeechRecognitionEvent) => void
|
|
28
|
+
onerror?: (e: SpeechRecognitionErrorEvent) => void
|
|
29
|
+
onend?: () => void
|
|
30
|
+
} | null>(null)
|
|
31
|
+
|
|
32
|
+
const toggle = useCallback(() => {
|
|
33
|
+
setError(null)
|
|
34
|
+
if (recording) {
|
|
35
|
+
recogRef.current?.stop()
|
|
36
|
+
setRecording(false)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
|
|
41
|
+
if (!SR) {
|
|
42
|
+
setError('Speech recognition is not supported in this browser.')
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const recog = new SR()
|
|
47
|
+
recog.continuous = false
|
|
48
|
+
recog.interimResults = false
|
|
49
|
+
recog.maxAlternatives = 1
|
|
50
|
+
recog.lang = options?.lang || (typeof navigator !== 'undefined' ? navigator.language : 'en-US')
|
|
51
|
+
|
|
52
|
+
recog.onresult = (e: SpeechRecognitionEvent) => {
|
|
53
|
+
setRecording(false)
|
|
54
|
+
const transcript = e.results?.[0]?.[0]?.transcript?.trim() || ''
|
|
55
|
+
if (transcript) onResult(transcript)
|
|
56
|
+
}
|
|
57
|
+
recog.onerror = (e: SpeechRecognitionErrorEvent) => {
|
|
58
|
+
setRecording(false)
|
|
59
|
+
const code = e?.error || 'unknown'
|
|
60
|
+
const message = code === 'not-allowed'
|
|
61
|
+
? 'Microphone access denied. Allow mic permission and try again.'
|
|
62
|
+
: code === 'no-speech'
|
|
63
|
+
? 'No speech detected. Try again.'
|
|
64
|
+
: `Speech recognition error: ${code}`
|
|
65
|
+
setError(message)
|
|
66
|
+
}
|
|
67
|
+
recog.onend = () => setRecording(false)
|
|
68
|
+
|
|
69
|
+
recogRef.current = recog
|
|
70
|
+
setRecording(true)
|
|
71
|
+
try {
|
|
72
|
+
recog.start()
|
|
73
|
+
} catch {
|
|
74
|
+
setRecording(false)
|
|
75
|
+
setError('Could not start speech recognition.')
|
|
76
|
+
}
|
|
77
|
+
}, [recording, onResult, options?.lang])
|
|
78
|
+
|
|
79
|
+
const supported = typeof window !== 'undefined' &&
|
|
80
|
+
!!((window as any).SpeechRecognition || (window as any).webkitSpeechRecognition)
|
|
81
|
+
|
|
82
|
+
return { recording, toggle, supported, error }
|
|
83
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { api } from './api-client'
|
|
2
|
+
import type { Agent } from '../types'
|
|
3
|
+
|
|
4
|
+
export const fetchAgents = () => api<Record<string, Agent>>('GET', '/agents')
|
|
5
|
+
|
|
6
|
+
export const createAgent = (data: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>) =>
|
|
7
|
+
api<Agent>('POST', '/agents', data)
|
|
8
|
+
|
|
9
|
+
export const updateAgent = (id: string, data: Partial<Agent>) =>
|
|
10
|
+
api<Agent>('PUT', `/agents/${id}`, data)
|
|
11
|
+
|
|
12
|
+
export const deleteAgent = (id: string) =>
|
|
13
|
+
api<string>('DELETE', `/agents/${id}`)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const ACCESS_KEY_STORAGE = 'sc_access_key'
|
|
2
|
+
const DEFAULT_API_TIMEOUT_MS = 12_000
|
|
3
|
+
const DEFAULT_GET_RETRIES = 2
|
|
4
|
+
const RETRY_DELAY_BASE_MS = 300
|
|
5
|
+
|
|
6
|
+
export function getStoredAccessKey(): string {
|
|
7
|
+
if (typeof window === 'undefined') return ''
|
|
8
|
+
return localStorage.getItem(ACCESS_KEY_STORAGE) || ''
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setStoredAccessKey(key: string) {
|
|
12
|
+
localStorage.setItem(ACCESS_KEY_STORAGE, key)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clearStoredAccessKey() {
|
|
16
|
+
localStorage.removeItem(ACCESS_KEY_STORAGE)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sleep(ms: number): Promise<void> {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function fetchWithTimeout(
|
|
24
|
+
input: RequestInfo | URL,
|
|
25
|
+
init: RequestInit,
|
|
26
|
+
timeoutMs: number,
|
|
27
|
+
): Promise<Response> {
|
|
28
|
+
const controller = new AbortController()
|
|
29
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
30
|
+
try {
|
|
31
|
+
return await fetch(input, { ...init, signal: controller.signal })
|
|
32
|
+
} finally {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isAbortError(err: unknown): boolean {
|
|
38
|
+
if (!err || typeof err !== 'object') return false
|
|
39
|
+
return (err as { name?: string }).name === 'AbortError'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function api<T = unknown>(
|
|
43
|
+
method: string,
|
|
44
|
+
path: string,
|
|
45
|
+
body?: unknown,
|
|
46
|
+
options?: { timeoutMs?: number; retries?: number },
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
const key = getStoredAccessKey()
|
|
49
|
+
const timeoutMs = Math.max(1_000, Math.trunc(options?.timeoutMs ?? DEFAULT_API_TIMEOUT_MS))
|
|
50
|
+
const upperMethod = method.toUpperCase()
|
|
51
|
+
const retries = Math.max(0, Math.trunc(options?.retries ?? (upperMethod === 'GET' ? DEFAULT_GET_RETRIES : 0)))
|
|
52
|
+
|
|
53
|
+
const requestInit: RequestInit = {
|
|
54
|
+
method: upperMethod,
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
...(key ? { 'X-Access-Key': key } : {}),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
if (body) requestInit.body = JSON.stringify(body)
|
|
61
|
+
|
|
62
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
const r = await fetchWithTimeout('/api' + path, requestInit, timeoutMs)
|
|
65
|
+
|
|
66
|
+
if (r.status === 401) {
|
|
67
|
+
// Clear stored key on auth failure, redirect to login
|
|
68
|
+
clearStoredAccessKey()
|
|
69
|
+
if (typeof window !== 'undefined') {
|
|
70
|
+
window.dispatchEvent(new Event('sc_auth_required'))
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Unauthorized — invalid access key')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ct = r.headers.get('content-type') || ''
|
|
76
|
+
|
|
77
|
+
if (!r.ok) {
|
|
78
|
+
if (ct.includes('json')) {
|
|
79
|
+
const payload = await r.json().catch(() => null) as { error?: unknown; message?: unknown } | null
|
|
80
|
+
const msg =
|
|
81
|
+
(typeof payload?.error === 'string' && payload.error.trim())
|
|
82
|
+
|| (typeof payload?.message === 'string' && payload.message.trim())
|
|
83
|
+
|| `Request failed (${r.status})`
|
|
84
|
+
throw new Error(msg)
|
|
85
|
+
}
|
|
86
|
+
const text = (await r.text().catch(() => '')).trim()
|
|
87
|
+
throw new Error(text || `Request failed (${r.status})`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (ct.includes('json')) return r.json() as Promise<T>
|
|
91
|
+
return r.text() as unknown as T
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const isLastAttempt = attempt >= retries
|
|
94
|
+
const retryable = isAbortError(err) || (err instanceof TypeError && !String(err.message || '').includes('Unauthorized'))
|
|
95
|
+
if (isLastAttempt || !retryable) throw err
|
|
96
|
+
await sleep(RETRY_DELAY_BASE_MS * (attempt + 1))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw new Error('Request failed')
|
|
100
|
+
}
|
package/src/lib/chat.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { SSEEvent } from '../types'
|
|
2
|
+
import { getStoredAccessKey } from './api-client'
|
|
3
|
+
|
|
4
|
+
interface StreamChatOptions {
|
|
5
|
+
internal?: boolean
|
|
6
|
+
queueMode?: 'followup' | 'steer' | 'collect'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function streamChat(
|
|
10
|
+
sessionId: string,
|
|
11
|
+
message: string,
|
|
12
|
+
imagePath?: string,
|
|
13
|
+
imageUrl?: string,
|
|
14
|
+
onEvent?: (event: SSEEvent) => void,
|
|
15
|
+
options?: StreamChatOptions,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const key = getStoredAccessKey()
|
|
18
|
+
const res = await fetch(`/api/sessions/${sessionId}/chat`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
...(key ? { 'X-Access-Key': key } : {}),
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
message,
|
|
26
|
+
imagePath,
|
|
27
|
+
imageUrl,
|
|
28
|
+
internal: !!options?.internal,
|
|
29
|
+
queueMode: options?.queueMode,
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (!res.ok || !res.body) {
|
|
34
|
+
onEvent?.({ t: 'err', text: `Request failed (${res.status})` })
|
|
35
|
+
onEvent?.({ t: 'done' })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reader = res.body.getReader()
|
|
40
|
+
const decoder = new TextDecoder()
|
|
41
|
+
let buf = ''
|
|
42
|
+
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read()
|
|
45
|
+
if (done) break
|
|
46
|
+
buf += decoder.decode(value, { stream: true })
|
|
47
|
+
const lines = buf.split('\n')
|
|
48
|
+
buf = lines.pop() || ''
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.startsWith('data: ')) continue
|
|
51
|
+
try {
|
|
52
|
+
const event = JSON.parse(line.slice(6)) as SSEEvent
|
|
53
|
+
// Forward all event types including tool_call and tool_result
|
|
54
|
+
onEvent?.(event)
|
|
55
|
+
} catch {
|
|
56
|
+
// skip malformed
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { api } from './api-client'
|
|
2
|
+
import type { MemoryEntry } from '../types'
|
|
3
|
+
|
|
4
|
+
interface MemoryQueryOptions {
|
|
5
|
+
q?: string
|
|
6
|
+
agentId?: string
|
|
7
|
+
depth?: number
|
|
8
|
+
limit?: number
|
|
9
|
+
linkedLimit?: number
|
|
10
|
+
envelope?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const searchMemory = (opts: MemoryQueryOptions = {}) => {
|
|
14
|
+
const params = new URLSearchParams()
|
|
15
|
+
if (opts.q) params.set('q', opts.q)
|
|
16
|
+
if (opts.agentId) params.set('agentId', opts.agentId)
|
|
17
|
+
if (typeof opts.depth === 'number') params.set('depth', String(opts.depth))
|
|
18
|
+
if (typeof opts.limit === 'number') params.set('limit', String(opts.limit))
|
|
19
|
+
if (typeof opts.linkedLimit === 'number') params.set('linkedLimit', String(opts.linkedLimit))
|
|
20
|
+
if (opts.envelope) params.set('envelope', 'true')
|
|
21
|
+
const qs = params.toString()
|
|
22
|
+
return api<MemoryEntry[]>('GET', `/memory${qs ? '?' + qs : ''}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getMemory = (id: string, opts: Omit<MemoryQueryOptions, 'q' | 'agentId'> = {}) => {
|
|
26
|
+
const params = new URLSearchParams()
|
|
27
|
+
if (typeof opts.depth === 'number') params.set('depth', String(opts.depth))
|
|
28
|
+
if (typeof opts.limit === 'number') params.set('limit', String(opts.limit))
|
|
29
|
+
if (typeof opts.linkedLimit === 'number') params.set('linkedLimit', String(opts.linkedLimit))
|
|
30
|
+
if (opts.envelope) params.set('envelope', 'true')
|
|
31
|
+
const qs = params.toString()
|
|
32
|
+
return api<MemoryEntry | MemoryEntry[]>('GET', `/memory/${id}${qs ? '?' + qs : ''}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const createMemory = (data: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>) =>
|
|
36
|
+
api<MemoryEntry>('POST', '/memory', data)
|
|
37
|
+
|
|
38
|
+
export const updateMemory = (id: string, data: Partial<MemoryEntry>) =>
|
|
39
|
+
api<MemoryEntry>('PUT', `/memory/${id}`, data)
|
|
40
|
+
|
|
41
|
+
export const deleteMemory = (id: string) =>
|
|
42
|
+
api<string>('DELETE', `/memory/${id}`)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import {
|
|
4
|
+
deriveOpenClawWsUrl,
|
|
5
|
+
normalizeOpenClawEndpoint,
|
|
6
|
+
normalizeProviderEndpoint,
|
|
7
|
+
} from './openclaw-endpoint.ts'
|
|
8
|
+
|
|
9
|
+
test('normalizeOpenClawEndpoint handles ws/http/path variants', () => {
|
|
10
|
+
assert.equal(
|
|
11
|
+
normalizeOpenClawEndpoint('ws://127.0.0.1:18789'),
|
|
12
|
+
'http://127.0.0.1:18789/v1',
|
|
13
|
+
)
|
|
14
|
+
assert.equal(
|
|
15
|
+
normalizeOpenClawEndpoint('http://localhost:18789'),
|
|
16
|
+
'http://localhost:18789/v1',
|
|
17
|
+
)
|
|
18
|
+
assert.equal(
|
|
19
|
+
normalizeOpenClawEndpoint('http://localhost:18789/v1/chat/completions'),
|
|
20
|
+
'http://localhost:18789/v1',
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('deriveOpenClawWsUrl strips v1 suffix and preserves host', () => {
|
|
25
|
+
assert.equal(
|
|
26
|
+
deriveOpenClawWsUrl('http://localhost:18789/v1'),
|
|
27
|
+
'ws://localhost:18789',
|
|
28
|
+
)
|
|
29
|
+
assert.equal(
|
|
30
|
+
deriveOpenClawWsUrl('https://openclaw.example.com/v1'),
|
|
31
|
+
'wss://openclaw.example.com',
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('normalizeProviderEndpoint only rewrites openclaw provider', () => {
|
|
36
|
+
assert.equal(
|
|
37
|
+
normalizeProviderEndpoint('openclaw', 'ws://localhost:18789'),
|
|
38
|
+
'http://localhost:18789/v1',
|
|
39
|
+
)
|
|
40
|
+
assert.equal(
|
|
41
|
+
normalizeProviderEndpoint('openai', 'https://api.openai.com/v1/'),
|
|
42
|
+
'https://api.openai.com/v1',
|
|
43
|
+
)
|
|
44
|
+
assert.equal(
|
|
45
|
+
normalizeProviderEndpoint('openai', null),
|
|
46
|
+
null,
|
|
47
|
+
)
|
|
48
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const DEFAULT_OPENCLAW_ENDPOINT = 'http://localhost:18789/v1'
|
|
2
|
+
|
|
3
|
+
function hasScheme(value: string): boolean {
|
|
4
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseUrl(raw: string): URL | null {
|
|
8
|
+
const value = raw.trim()
|
|
9
|
+
if (!value) return null
|
|
10
|
+
try {
|
|
11
|
+
return new URL(hasScheme(value) ? value : `http://${value}`)
|
|
12
|
+
} catch {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toHttpProtocol(protocol: string): string {
|
|
18
|
+
if (protocol === 'ws:') return 'http:'
|
|
19
|
+
if (protocol === 'wss:') return 'https:'
|
|
20
|
+
return protocol
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanPath(pathname: string): string {
|
|
24
|
+
let path = pathname.replace(/\/+$/, '')
|
|
25
|
+
const lower = path.toLowerCase()
|
|
26
|
+
|
|
27
|
+
if (lower.endsWith('/chat/completions')) {
|
|
28
|
+
path = path.slice(0, -'/chat/completions'.length)
|
|
29
|
+
}
|
|
30
|
+
if (path.toLowerCase().endsWith('/models')) {
|
|
31
|
+
path = path.slice(0, -'/models'.length)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
path = path.replace(/\/+$/, '')
|
|
35
|
+
if (!path || path === '/') return '/v1'
|
|
36
|
+
if (!/\/v1$/i.test(path)) return `${path}/v1`
|
|
37
|
+
return path
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeOpenClawEndpoint(input?: string | null): string {
|
|
41
|
+
const parsed = parseUrl(input || '') || parseUrl(DEFAULT_OPENCLAW_ENDPOINT)!
|
|
42
|
+
parsed.protocol = toHttpProtocol(parsed.protocol)
|
|
43
|
+
parsed.pathname = cleanPath(parsed.pathname || '/')
|
|
44
|
+
parsed.search = ''
|
|
45
|
+
parsed.hash = ''
|
|
46
|
+
return parsed.toString().replace(/\/+$/, '')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function deriveOpenClawWsUrl(input?: string | null): string {
|
|
50
|
+
const api = normalizeOpenClawEndpoint(input)
|
|
51
|
+
const parsed = parseUrl(api) || parseUrl(DEFAULT_OPENCLAW_ENDPOINT)!
|
|
52
|
+
parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
53
|
+
parsed.pathname = parsed.pathname.replace(/\/v1$/i, '') || '/'
|
|
54
|
+
parsed.search = ''
|
|
55
|
+
parsed.hash = ''
|
|
56
|
+
const value = parsed.toString()
|
|
57
|
+
return value.endsWith('/') ? value.slice(0, -1) : value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeProviderEndpoint(provider: string | null | undefined, endpoint: string | null | undefined): string | null {
|
|
61
|
+
if (typeof endpoint !== 'string') return null
|
|
62
|
+
const trimmed = endpoint.trim()
|
|
63
|
+
if (!trimmed) return null
|
|
64
|
+
if (provider === 'openclaw') return normalizeOpenClawEndpoint(trimmed)
|
|
65
|
+
return trimmed.replace(/\/+$/, '')
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { api } from './api-client'
|
|
2
|
+
import type { ProviderConfig } from '../types'
|
|
3
|
+
|
|
4
|
+
export const fetchProviderConfigs = () => api<ProviderConfig[]>('GET', '/providers/configs')
|
|
5
|
+
|
|
6
|
+
export const createProviderConfig = (data: Partial<ProviderConfig>) =>
|
|
7
|
+
api<ProviderConfig>('POST', '/providers', data)
|
|
8
|
+
|
|
9
|
+
export const updateProviderConfig = (id: string, data: Partial<ProviderConfig>) =>
|
|
10
|
+
api<ProviderConfig>('PUT', `/providers/${id}`, data)
|
|
11
|
+
|
|
12
|
+
export const deleteProviderConfig = (id: string) =>
|
|
13
|
+
api<{ ok: boolean }>('DELETE', `/providers/${id}`)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import https from 'https'
|
|
3
|
+
import type { StreamChatOptions } from './index'
|
|
4
|
+
|
|
5
|
+
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
6
|
+
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
7
|
+
|
|
8
|
+
function fileToContentBlocks(filePath: string): any[] {
|
|
9
|
+
if (!filePath || !fs.existsSync(filePath)) return []
|
|
10
|
+
if (IMAGE_EXTS.test(filePath)) {
|
|
11
|
+
const data = fs.readFileSync(filePath).toString('base64')
|
|
12
|
+
const ext = filePath.split('.').pop()?.toLowerCase() || 'png'
|
|
13
|
+
const mediaType = ext === 'jpg' ? 'image/jpeg' : `image/${ext}`
|
|
14
|
+
return [{ type: 'image', source: { type: 'base64', media_type: mediaType, data } }]
|
|
15
|
+
}
|
|
16
|
+
if (TEXT_EXTS.test(filePath) || filePath.endsWith('.pdf')) {
|
|
17
|
+
try {
|
|
18
|
+
const text = fs.readFileSync(filePath, 'utf-8')
|
|
19
|
+
const name = filePath.split('/').pop() || 'file'
|
|
20
|
+
return [{ type: 'text', text: `[Attached file: ${name}]\n\n${text}` }]
|
|
21
|
+
} catch { return [] }
|
|
22
|
+
}
|
|
23
|
+
return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory }: StreamChatOptions): Promise<string> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
29
|
+
const model = session.model || 'claude-sonnet-4-6'
|
|
30
|
+
|
|
31
|
+
const body: Record<string, unknown> = {
|
|
32
|
+
model,
|
|
33
|
+
max_tokens: 8192,
|
|
34
|
+
messages,
|
|
35
|
+
stream: true,
|
|
36
|
+
}
|
|
37
|
+
if (systemPrompt) {
|
|
38
|
+
body.system = systemPrompt
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const payload = JSON.stringify(body)
|
|
42
|
+
const abortController = { aborted: false }
|
|
43
|
+
let fullResponse = ''
|
|
44
|
+
|
|
45
|
+
const apiReq = https.request({
|
|
46
|
+
hostname: 'api.anthropic.com',
|
|
47
|
+
path: '/v1/messages',
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'x-api-key': apiKey || '',
|
|
51
|
+
'anthropic-version': '2023-06-01',
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
}, (apiRes) => {
|
|
55
|
+
if (apiRes.statusCode !== 200) {
|
|
56
|
+
let errBody = ''
|
|
57
|
+
apiRes.on('data', (c: Buffer) => errBody += c)
|
|
58
|
+
apiRes.on('end', () => {
|
|
59
|
+
console.error(`[${session.id}] anthropic error ${apiRes.statusCode}:`, errBody.slice(0, 200))
|
|
60
|
+
let errMsg = `Anthropic API error (${apiRes.statusCode})`
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(errBody)
|
|
63
|
+
if (parsed.error?.message) errMsg = parsed.error.message
|
|
64
|
+
} catch {}
|
|
65
|
+
write(`data: ${JSON.stringify({ t: 'err', text: errMsg })}\n\n`)
|
|
66
|
+
active.delete(session.id)
|
|
67
|
+
resolve(fullResponse)
|
|
68
|
+
})
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let buf = ''
|
|
73
|
+
apiRes.on('data', (chunk: Buffer) => {
|
|
74
|
+
if (abortController.aborted) return
|
|
75
|
+
buf += chunk.toString()
|
|
76
|
+
const lines = buf.split('\n')
|
|
77
|
+
buf = lines.pop()!
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (!line.startsWith('data: ')) continue
|
|
81
|
+
const data = line.slice(6).trim()
|
|
82
|
+
if (!data) continue
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(data)
|
|
85
|
+
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
|
86
|
+
fullResponse += parsed.delta.text
|
|
87
|
+
write(`data: ${JSON.stringify({ t: 'd', text: parsed.delta.text })}\n\n`)
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
apiRes.on('end', () => {
|
|
94
|
+
active.delete(session.id)
|
|
95
|
+
resolve(fullResponse)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
100
|
+
|
|
101
|
+
apiReq.on('error', (e) => {
|
|
102
|
+
console.error(`[${session.id}] anthropic request error:`, e.message)
|
|
103
|
+
write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
|
|
104
|
+
active.delete(session.id)
|
|
105
|
+
resolve(fullResponse)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
apiReq.end(payload)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildMessages(session: any, message: string, imagePath: string | undefined, loadHistory: (id: string) => any[]) {
|
|
113
|
+
const msgs: Array<{ role: string; content: any }> = []
|
|
114
|
+
|
|
115
|
+
if (loadHistory) {
|
|
116
|
+
const history = loadHistory(session.id)
|
|
117
|
+
for (const m of history) {
|
|
118
|
+
if (m.role === 'user' && m.imagePath) {
|
|
119
|
+
const blocks = fileToContentBlocks(m.imagePath)
|
|
120
|
+
msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: m.text }] })
|
|
121
|
+
} else {
|
|
122
|
+
msgs.push({ role: m.role, content: m.text })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Current message with optional attachment
|
|
128
|
+
if (imagePath) {
|
|
129
|
+
const blocks = fileToContentBlocks(imagePath)
|
|
130
|
+
msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: message }] })
|
|
131
|
+
} else {
|
|
132
|
+
msgs.push({ role: 'user', content: message })
|
|
133
|
+
}
|
|
134
|
+
return msgs
|
|
135
|
+
}
|