fix: context propagation in LLM calls, SSE error handling, security hardening

- Propagate request context through MCP client (Request.Ctx field)
  — Client disconnects now cancel in-flight LLM API calls (both streaming and non-streaming)
  — Prevents wasted LLM tokens when user navigates away mid-response
- proxyBinance: use http.NewRequestWithContext for context-aware upstream calls
  — Client disconnects cancel Binance proxy requests
  — Distinguishes client cancellation from upstream failures
- Fix SSE parse bug in AgentChatPage: catch block was swallowing error events
  — throw new Error(data) for 'error' events was caught by the same try/catch
  — Now parse JSON separately, then handle events outside try/catch
- Add io.LimitReader to Hyperliquid coins.go JSON decoder (4MB limit)
- Use safe.ReadAllLimited in batch ticker handler for consistency
- Sanitize URL validation error in handler_ai_model.go (was leaking raw error)
- Cache agent tool definitions (built once, reused per message)
- Sort trade history by exit time for consistent ordering
This commit is contained in:
shinchan-zhai
2026-03-23 15:37:36 +08:00
parent 4c73a7960d
commit 3f1dd71f2b
8 changed files with 189 additions and 77 deletions

View File

@@ -145,45 +145,48 @@ export function AgentChatPage() {
eventType = line.slice(7).trim()
} else if (line.startsWith('data: ') && eventType) {
const rawData = line.slice(6)
let data: string
try {
const data = JSON.parse(rawData)
if (eventType === 'delta') {
// data is the accumulated text so far
finalText = data
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? { ...m, text: data, time: now() }
: m
)
)
} else if (eventType === 'tool') {
// Show tool being called as a status indicator
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? {
...m,
text: m.text || `🔧 _Calling ${data}..._`,
time: now(),
}
: m
)
)
} else if (eventType === 'done') {
finalText = data
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? { ...m, text: data, time: now(), streaming: false }
: m
)
)
} else if (eventType === 'error') {
throw new Error(data)
}
} catch (parseErr) {
data = JSON.parse(rawData)
} catch {
// Ignore malformed SSE data lines
eventType = ''
continue
}
if (eventType === 'delta') {
// data is the accumulated text so far
finalText = data
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? { ...m, text: data, time: now() }
: m
)
)
} else if (eventType === 'tool') {
// Show tool being called as a status indicator
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? {
...m,
text: m.text || `🔧 _Calling ${data}..._`,
time: now(),
}
: m
)
)
} else if (eventType === 'done') {
finalText = data
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? { ...m, text: data, time: now(), streaming: false }
: m
)
)
} else if (eventType === 'error') {
throw new Error(data)
}
eventType = ''
}