fix: flushHeaders + heartbeat + clearInterval

This commit is contained in:
aedes
2026-04-20 17:37:20 +03:00
parent aae7ede6f0
commit 3c3812bc18

View File

@@ -37,9 +37,13 @@ app.post('/api/chat', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
res.flushHeaders(); // establish SSE connection immediately
const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir }; const env = { ...process.env, CLAUDE_CONFIG_DIR: configDir };
// heartbeat to keep connection alive during long generation
const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000);
const proc = spawn(CLAUDE_BIN, ['-p', text, '--model', modelId, '--output-format', 'stream-json', '--verbose'], { const proc = spawn(CLAUDE_BIN, ['-p', text, '--model', modelId, '--output-format', 'stream-json', '--verbose'], {
env, env,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@@ -49,24 +53,17 @@ app.post('/api/chat', (req, res) => {
proc.stdout.on('data', (chunk) => { proc.stdout.on('data', (chunk) => {
buf += chunk.toString(); buf += chunk.toString();
const lines = buf.split('\n'); const lines = buf.split('\n');
buf = lines.pop(); // keep incomplete line buf = lines.pop();
for (const line of lines) { for (const line of lines) {
if (!line.trim()) continue; if (!line.trim()) continue;
try { try {
const obj = JSON.parse(line); const obj = JSON.parse(line);
// streaming delta if (obj.type === 'assistant' && obj.message?.content) {
if (obj.type === 'content_block_delta' && obj.delta?.type === 'text_delta') {
res.write(`data: ${JSON.stringify({ text: obj.delta.text })}\n\n`);
}
// fallback: full assistant message
else if (obj.type === 'assistant' && obj.message?.content) {
for (const block of obj.message.content) { for (const block of obj.message.content) {
if (block.type === 'text') res.write(`data: ${JSON.stringify({ text: block.text })}\n\n`); if (block.type === 'text') res.write(`data: ${JSON.stringify({ text: block.text })}\n\n`);
} }
} } else if (obj.type === 'result' && obj.is_error) {
// error in result res.write(`data: ${JSON.stringify({ error: obj.result || 'error' })}\n\n`);
else if (obj.type === 'result' && obj.is_error) {
res.write(`data: ${JSON.stringify({ error: obj.result || 'unknown error' })}\n\n`);
} }
} catch (e) {} } catch (e) {}
} }
@@ -85,6 +82,7 @@ app.post('/api/chat', (req, res) => {
}); });
proc.on('close', (code, signal) => { proc.on('close', (code, signal) => {
clearInterval(heartbeat);
console.log(`[claude close] code=${code} signal=${signal} stderr=${stderrBuf.substring(0,300)}`); console.log(`[claude close] code=${code} signal=${signal} stderr=${stderrBuf.substring(0,300)}`);
if (code !== 0) { if (code !== 0) {
const msg = stderrBuf.trim() || `code=${code} signal=${signal}`; const msg = stderrBuf.trim() || `code=${code} signal=${signal}`;
@@ -96,7 +94,7 @@ app.post('/api/chat', (req, res) => {
let finished = false; let finished = false;
proc.on('close', () => { finished = true; }); proc.on('close', () => { finished = true; });
res.on('close', () => { if (!finished) try { proc.kill(); } catch (e) {} }); res.on('close', () => { clearInterval(heartbeat); if (!finished) try { proc.kill(); } catch (e) {} });
}); });
app.listen(8108, () => console.log('sechenov :8108')); app.listen(8108, () => console.log('sechenov :8108'));