fix(web): keep rectification sessions listed and persist chats on first send
Independent Staging Quality Gate / validate (push) Failing after 16m52s
Independent Staging Quality Gate / publish (push) Skipped

Empty-consultation filtering is now session_type scoped so birth-time rows stay in the sidebar. Subtitles use updatedAt with sort/group/cursor. New chats stay local until the first send.
This commit is contained in:
jesse-ux
2026-09-21 16:39:32 +08:00
parent f8d65e484d
commit e71e4f9200
21 changed files with 461 additions and 186 deletions
+16 -51
View File
@@ -127,7 +127,6 @@ import {
CONSULTATION_EVIDENCE_VALIDATION_LABEL,
CONSULTATION_LOADING_METHOD_LABEL,
} from "@/lib/consultation-activity-labels";
import { writeChatSession } from "@/lib/chat-session-write-contract";
import {
SESSION_MISSING_NOTICE,
clearLoginSessionReturn,
@@ -210,7 +209,6 @@ import {
resolveLookupBootstrap,
payloadCode,
payloadMessage,
readDraftConsultation,
readSessions,
readStoredDailyStarlanguage,
readStoredPendingConsultation,
@@ -228,7 +226,7 @@ import {
starterHomeLandingNeedsConsultation,
type BootstrapPhase,
} from "@/lib/home-bootstrap";
import { findReusableEmptyConsultation } from "@/lib/session-list-filter";
import { isUnsavedEmptyConsultation } from "@/lib/session-list-filter";
const BirthTimeRectification = dynamic(
() => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification),
@@ -931,31 +929,12 @@ export default function Home() {
const parsedSessions = readSessions(listBoot.rawRows, nextModelCatalog);
setSessionsCursor(listBoot.cursor);
let nextSessions = parsedSessions.sessions.length > 0 ? parsedSessions.sessions : listBoot.sessions;
const draftSession = readDraftConsultation(listBoot.draftRow, nextModelCatalog);
if (draftSession && !nextSessions.some((session) => session.id === draftSession.id)) {
nextSessions = [...nextSessions, draftSession];
}
if (nextSessions.length === 0) {
if (controller.signal.aborted) return;
const initialSession = createSession(
nextSessions = [createSession(
nextModelCatalog?.defaultModelId ?? "",
"consultation",
chartSnapshotForSession("self", [], nextProfile),
);
if (nextModelCatalog) {
await writeChatSession(initialSession.id, {
title: initialSession.title,
theme: initialSession.theme,
model_id: initialSession.modelId,
messages: [],
session_type: initialSession.sessionType,
rectification_case_id: initialSession.rectificationCaseId,
chart_profile_id: initialSession.chartProfileId,
chart_profile_name: initialSession.chartProfileName,
chart_profile_role: initialSession.chartProfileRole,
}, "create");
}
nextSessions = [initialSession];
)];
}
nextSessions = applyLegacySessionControls(nextAccount.user.id, nextSessions);
@@ -1010,32 +989,13 @@ export default function Home() {
bootstrapSelection.urlAction,
);
if (starterHomeLandingNeedsConsultation(nextSessions, landingSessionId, bootstrapSelection.urlAction)) {
const reusable = findReusableEmptyConsultation(nextSessions);
if (reusable) {
landingSessionId = reusable.id;
} else {
if (controller.signal.aborted) return;
const homeSession = createSession(
nextModelCatalog?.defaultModelId ?? "",
"consultation",
chartSnapshotForSession("self", [], nextProfile),
);
if (nextModelCatalog) {
await writeChatSession(homeSession.id, {
title: homeSession.title,
theme: homeSession.theme,
model_id: homeSession.modelId,
messages: [],
session_type: homeSession.sessionType,
rectification_case_id: homeSession.rectificationCaseId,
chart_profile_id: homeSession.chartProfileId,
chart_profile_name: homeSession.chartProfileName,
chart_profile_role: homeSession.chartProfileRole,
}, "create");
}
nextSessions = [homeSession, ...nextSessions];
landingSessionId = homeSession.id;
}
const homeSession = createSession(
nextModelCatalog?.defaultModelId ?? "",
"consultation",
chartSnapshotForSession("self", [], nextProfile),
);
nextSessions = [homeSession, ...nextSessions];
landingSessionId = homeSession.id;
}
const activeListed = nextSessions.find((session) => session.id === landingSessionId)
?? nextSessions[0];
@@ -1062,7 +1022,12 @@ export default function Home() {
setActiveSessionId(landingSessionId);
if (bootstrapSelection.clearStoredReturn) clearLoginSessionReturn();
if (bootstrapSelection.urlAction === "replace-clear") writeSessionUrl(null, "replace");
if (bootstrapSelection.urlAction === "replace-selected") writeSessionUrl(bootstrapSelection.sessionId, "replace");
if (bootstrapSelection.urlAction === "replace-selected") {
const selected = nextSessions.find((session) => session.id === bootstrapSelection.sessionId);
if (!selected || !isUnsavedEmptyConsultation(selected)) {
writeSessionUrl(bootstrapSelection.sessionId, "replace");
}
}
if (reservedConsultation?.status === "reserved") {
const recoverySession = nextSessions.find((session) => session.id === reservedConsultation.sessionId);
if (recoverySession) restoreConsultationRecovery(recoverySession, reservedConsultation.requestId, storedPending);
+3 -31
View File
@@ -16,22 +16,9 @@ import {
parseSessionCursor,
sessionCursorFilter,
} from "@/lib/session-cursor";
const SESSION_LIST_COLUMNS = "id,title,theme,model_id,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,created_at,updated_at,pinned,archived_at";
import { applyArchiveFilter, excludeEmptyConsultations } from "@/lib/session-list-filter";
function excludeEmptyConsultations<Query extends {
not: (column: string, operator: string, value: unknown) => Query;
}>(query: Query): Query {
// Empty consultations are `messages = []`. Rectification rows keep an opening
// turn, so excluding empty arrays leaves them in the list (BUG-928).
return query.not("messages", "eq", []);
}
function applyArchiveFilter<Query extends {
is: (column: string, value: null) => Query;
not: (column: string, operator: string, value: null) => Query;
}>(query: Query, archived: boolean): Query {
return archived ? query.not("archived_at", "is", null) : query.is("archived_at", null);
}
const SESSION_LIST_COLUMNS = "id,title,theme,model_id,session_type,rectification_case_id,chart_profile_id,chart_profile_name,chart_profile_role,updated_at,pinned,archived_at";
export async function GET(request: Request) {
try {
@@ -67,7 +54,6 @@ export async function GET(request: Request) {
if (pageError) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
let pinnedRows: typeof pageRows = [];
let draft: NonNullable<typeof pageRows>[number] | null = null;
if (!cursor) {
const { data: pinnedData, error: pinnedError } = await excludeEmptyConsultations(
applyArchiveFilter(
@@ -83,26 +69,12 @@ export async function GET(request: Request) {
.order("id", { ascending: false });
if (pinnedError) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
pinnedRows = pinnedData ?? [];
if (!archived) {
const { data: draftData, error: draftError } = await supabase
.from("chat_sessions")
.select(SESSION_LIST_COLUMNS)
.eq("user_id", user.id)
.eq("session_type", "consultation")
.eq("messages", [])
.is("archived_at", null)
.order("updated_at", { ascending: false })
.order("id", { ascending: false })
.limit(1);
if (draftError) return NextResponse.json({ error: "聊天记录暂时无法读取" }, { status: 500 });
draft = draftData?.[0] ?? null;
}
}
const nextCursor = nextSessionCursor(pageRows ?? [], limit);
const page = (pageRows ?? []).slice(0, limit);
const sessions = cursor ? page : [...(pinnedRows ?? []), ...page];
return NextResponse.json({ sessions, nextCursor, draft });
return NextResponse.json({ sessions, nextCursor });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });