feat: release chat and mindspace UI updates

This commit is contained in:
john
2026-06-27 21:57:21 +08:00
parent c924b2bb77
commit 8264e71f9e
24 changed files with 2042 additions and 209 deletions
+55 -5
View File
@@ -266,7 +266,7 @@ export async function buildVisionPayload({
};
}
export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, llmProviderService, localFetchAsset, subscriptionService }) {
export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, llmProviderService, localFetchAsset, subscriptionService, sessionSnapshotService }) {
const targets = apiTargets?.length ? apiTargets : apiTarget ? [apiTarget] : [];
const primaryTarget = targets[0] ?? apiTarget ?? '';
let rrIdx = 0;
@@ -296,6 +296,45 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth,
}
}
function isGeneratedSessionName(name) {
const normalized = typeof name === 'string' ? name.trim() : '';
return /^20\d{6}(?:[_-]\d+)?$/.test(normalized);
}
// A session "name" that carries no real topic: empty, or Goose's default
// placeholder before its describe step has run.
function hasDefaultSessionName(session) {
if (session?.user_set_name) return false;
const name = typeof session?.name === 'string' ? session.name.trim() : '';
return name === '' || name === 'New Chat' || name === '新对话' || isGeneratedSessionName(name);
}
// For sessions still carrying a default name, fill in a title derived from the
// first user message (kept in the snapshot cache) so the history list shows a
// meaningful label instead of "会话 <id>". Best-effort: never blocks the list.
async function enrichSessionHistory(sessions) {
if (!sessionSnapshotService?.isEnabled?.()) return;
const needsSummary = sessions.filter(
(session) => hasDefaultSessionName(session) || Number(session?.message_count ?? 0) === 0,
);
if (needsSummary.length === 0) return;
try {
const summaries = await sessionSnapshotService.getHistorySummaries(needsSummary.map((s) => s.id));
for (const session of needsSummary) {
const summary = summaries.get(session.id);
if (!summary) continue;
if (summary.title && hasDefaultSessionName(session)) {
session.name = summary.title;
}
if (Number(session?.message_count ?? 0) === 0 && Number(summary.messageCount ?? 0) > 0) {
session.message_count = Number(summary.messageCount);
}
}
} catch {
// Non-fatal: fall back to the client-side label.
}
}
async function pickTarget() {
if (targets.length <= 1) return primaryTarget;
for (let i = 0; i < targets.length; i += 1) {
@@ -632,7 +671,10 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth,
if (healthyTargets < targets.length) {
res.setHeader('X-TKMind-Degraded', '1');
}
res.json({ sessions: [...sessionsById.values()] });
const sessions = [...sessionsById.values()];
await enrichSessionHistory(sessions);
res.json({ sessions });
} catch (err) {
res.status(500).json({
message: sanitizeUserFacingProxyMessage(
@@ -725,6 +767,13 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth,
});
const source = Readable.fromWeb(upstream.body);
// 每 20s 发一个注释行保持连接,防止 nginx/代理因静默超时切断 SSE
const keepalive = setInterval(() => {
if (!res.writableEnded) res.write(': keepalive\n\n');
}, 20000);
const stopKeepalive = () => clearInterval(keepalive);
billingTransform.on('data', (chunk) => {
res.write(chunk);
if (pendingBalance != null) {
@@ -732,9 +781,10 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth,
pendingBalance = null;
}
});
billingTransform.on('end', () => res.end());
billingTransform.on('error', () => res.end());
source.on('error', () => res.end());
billingTransform.on('end', () => { stopKeepalive(); res.end(); });
billingTransform.on('error', () => { stopKeepalive(); res.end(); });
source.on('error', () => { stopKeepalive(); res.end(); });
req.on('close', stopKeepalive);
source.pipe(billingTransform);
} catch (err) {
res.status(502).json({