diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index c981727..1ca6cfd 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -2187,14 +2187,6 @@ export function createTkmindProxy({ } } - if (healthyTargets === 0) { - res.status(502).json({ message: lastFailure ?? '后端连接失败,请稍后重试' }); - return; - } - if (healthyTargets < targets.length) { - res.setHeader('X-TKMind-Degraded', '1'); - } - const directSessionIds = [...owned].filter(isDirectChatSessionId); const ownedMissingFromGoose = [...owned].filter((sessionId) => !sessionsById.has(sessionId)); const snapshotFallbackIds = [...new Set([...directSessionIds, ...ownedMissingFromGoose])]; @@ -2231,6 +2223,14 @@ export function createTkmindProxy({ } } + if (healthyTargets === 0 && owned.size > 0 && sessionsById.size === 0) { + res.status(502).json({ message: lastFailure ?? '后端连接失败,请稍后重试' }); + return; + } + if (healthyTargets < targets.length || healthyTargets === 0) { + res.setHeader('X-TKMind-Degraded', '1'); + } + const sessions = [...sessionsById.values()].sort(sortSessionsByRecent); await enrichSessionHistory(sessions, req.currentUser.id); if (typeof userAuth.getSessionOrigins === 'function' && sessions.length > 0) { diff --git a/tkmind-proxy.test.mjs b/tkmind-proxy.test.mjs index 6cf4ddc..bec40c8 100644 --- a/tkmind-proxy.test.mjs +++ b/tkmind-proxy.test.mjs @@ -452,6 +452,134 @@ test('buildVisionPayload injects public standard image urls for page generation' assert.match(text, /不得改写图片里人物的年龄、性别、人数或主体关系/); }); +test('GET /sessions serves snapshot summaries when every upstream is unavailable', async () => { + let upstream; + try { + upstream = createServer((_req, res) => { + res.writeHead(503, { 'Content-Type': 'text/plain' }); + res.end('temporarily unavailable'); + }); + const upstreamPort = await listen(upstream); + const proxy = createTkmindProxy({ + apiTarget: `http://127.0.0.1:${upstreamPort}`, + apiSecret: 'test-secret', + userAuth: { + async listOwnedSessionIds() { + return new Set(['session-1']); + }, + async getSessionOrigins() { + return new Map([['session-1', 'h5']]); + }, + }, + sessionSnapshotService: { + isEnabled() { + return true; + }, + async get(sessionId) { + assert.equal(sessionId, 'session-1'); + return { + session: { + id: sessionId, + name: 'Recovered session', + updated_at: '2026-07-27T00:00:00.000Z', + }, + messages: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }], + meta: { + synced_msg_count: 1, + source_updated_at: '2026-07-27T00:00:00.000Z', + }, + }; + }, + }, + }); + + const req = { + currentUser: { id: 'user-1', username: 'john' }, + query: {}, + }; + let payload; + const headers = new Map(); + const res = { + statusCode: 200, + status(code) { + this.statusCode = code; + return this; + }, + setHeader(name, value) { + headers.set(name, value); + }, + json(value) { + payload = value; + return this; + }, + }; + + await proxy.handlers['GET /sessions'][1](req, res); + + assert.equal(res.statusCode, 200); + assert.equal(headers.get('X-TKMind-Degraded'), '1'); + assert.equal(payload.sessions.length, 1); + assert.equal(payload.sessions[0].id, 'session-1'); + assert.equal(payload.sessions[0].name, 'Recovered session'); + assert.equal(payload.sessions[0].message_count, 1); + } finally { + await closeServer(upstream); + } +}); + +test('GET /sessions keeps the upstream error when no fallback session can be recovered', async () => { + let upstream; + try { + upstream = createServer((_req, res) => { + res.writeHead(503, { 'Content-Type': 'text/plain' }); + res.end('temporarily unavailable'); + }); + const upstreamPort = await listen(upstream); + const proxy = createTkmindProxy({ + apiTarget: `http://127.0.0.1:${upstreamPort}`, + apiSecret: 'test-secret', + userAuth: { + async listOwnedSessionIds() { + return new Set(['session-1']); + }, + }, + sessionSnapshotService: { + isEnabled() { + return true; + }, + async get() { + return null; + }, + }, + }); + + const req = { + currentUser: { id: 'user-1', username: 'john' }, + query: {}, + }; + let payload; + const res = { + statusCode: 200, + status(code) { + this.statusCode = code; + return this; + }, + setHeader() {}, + json(value) { + payload = value; + return this; + }, + }; + + await proxy.handlers['GET /sessions'][1](req, res); + + assert.equal(res.statusCode, 502); + assert.equal(payload.message, 'temporarily unavailable'); + } finally { + await closeServer(upstream); + } +}); + test('proxySessionEvents does not send JSON after SSE headers were sent', async () => { let upstream;