robustness: shared HTTP client, HTTP status checks, abort controller, streaming perf

- web.go: reuse shared binanceClient with connection pooling instead of per-request client
- sentinel.go: check HTTP status code before parsing Binance ticker response
- brain.go: check HTTP status codes in news scan and market brief APIs
- brain.go: add cleanStaleSignals() to prevent unbounded sync.Map growth
- scheduler.go: periodically clean expired pending trades
- AgentChatPage.tsx: add AbortController to cancel in-flight requests
- AgentChatPage.tsx: check response.ok before parsing JSON
- AgentChatPage.tsx: batch word streaming (~40 frames max) to reduce re-renders
- AgentChatPage.tsx: handle AbortError gracefully (remove orphan bot message)
This commit is contained in:
shinchan-zhai
2026-03-23 11:10:19 +08:00
parent 7ccac89e2b
commit b9c9f05603
5 changed files with 79 additions and 22 deletions

View File

@@ -39,6 +39,7 @@ export function AgentChatPage() {
const [loading, setLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const chatInputRef = useRef<ChatInputHandle>(null)
const abortRef = useRef<AbortController | null>(null)
// Sidebar section collapse state
const [sections, setSections] = useState({
@@ -98,6 +99,11 @@ export function AgentChatPage() {
setLoading(true)
try {
// Abort any in-flight request
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
const res = await fetch('/api/agent/chat', {
method: 'POST',
headers: {
@@ -105,15 +111,22 @@ export function AgentChatPage() {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ message: text, lang: language }),
signal: controller.signal,
})
if (!res.ok) {
const errData = await res.json().catch(() => ({}))
throw new Error(errData.error || `Server error (${res.status})`)
}
const data = await res.json()
const responseText = data.response || data.error || 'No response'
// Simulate streaming by revealing text progressively
// Simulate streaming by revealing text in chunks (batched to reduce renders)
const words = responseText.split(/(\s+)/)
const chunkSize = Math.max(3, Math.ceil(words.length / 40)) // ~40 frames max
let displayed = ''
for (let i = 0; i < words.length; i++) {
displayed += words[i]
for (let i = 0; i < words.length; i += chunkSize) {
const chunk = words.slice(i, i + chunkSize).join('')
displayed += chunk
const current = displayed
setMessages((prev) =>
prev.map((m) =>
@@ -129,8 +142,8 @@ export function AgentChatPage() {
: m
)
)
if (i < words.length - 1) {
await new Promise((r) => setTimeout(r, 10))
if (i + chunkSize < words.length) {
await new Promise((r) => setTimeout(r, 20))
}
}
// Mark streaming done
@@ -138,21 +151,26 @@ export function AgentChatPage() {
prev.map((m) => (m.id === botId ? { ...m, streaming: false } : m))
)
} catch (e: any) {
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? {
...m,
text: '⚠️ Error: ' + e.message,
time: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
streaming: false,
}
: m
if (e.name === 'AbortError') {
// Request was cancelled (e.g. user sent a new message), clean up silently
setMessages((prev) => prev.filter((m) => m.id !== botId))
} else {
setMessages((prev) =>
prev.map((m) =>
m.id === botId
? {
...m,
text: '⚠️ Error: ' + e.message,
time: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
streaming: false,
}
: m
)
)
)
}
}
setLoading(false)
chatInputRef.current?.focus()