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
@@ -64,9 +64,11 @@ import { completedOnboardingTranscript, isProfileComplete, selectedBirthPlace }
import { pendingConsultationStorageKey, timestamp } from "@/lib/home-types";
import {
consultSendBlockedByRectificationSession,
fallbackSessionId,
missingActiveSessionSendAction,
SESSION_NOT_CONSULTATION_CODE,
} from "@/lib/rectification-session-composer-guard";
import { isUnsavedEmptyConsultation } from "@/lib/session-list-filter";
import type {
Account,
AccountDialog,
@@ -606,6 +608,22 @@ export function useConsultationRun(params: ConsultationRunParams) {
return false;
}
if (isUnsavedEmptyConsultation(currentSession)) {
try {
await persistSession(currentSession, "create");
} catch (caught) {
const remaining = sessions.filter((session) => session.id !== currentSession.id);
setSessions(remaining);
const fallbackId = fallbackSessionId(remaining);
setActiveSessionId(fallbackId);
setRequestError({
sessionId: fallbackId,
message: caught instanceof Error ? caught.message : "新对话未能保存到云端。",
});
return false;
}
}
const [year, month, day] = profile.date.split("-").map(Number);
const [hour, minute] = consultationRoute.time?.split(":").map(Number) ?? [];
+40 -48
View File
@@ -29,7 +29,11 @@ import {
patchSessionModel,
readSessions,
} from "@/lib/home-cloud-sync";
import { findReusableEmptyConsultation } from "@/lib/session-list-filter";
import {
isListedSidebarSession,
isUnsavedEmptyConsultation,
replaceUnsavedEmptyConsultations,
} from "@/lib/session-list-filter";
import { chartSnapshotForSession } from "@/lib/home-profile";
import { beginSessionPageLoad, mergeSessionPage } from "@/lib/session-groups";
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
@@ -110,7 +114,6 @@ export function useSessionManagement(params: SessionManagementParams) {
setActiveChartId,
setActiveSessionId,
setBirthTimeConsultationConsent,
setCreatingSession,
setDraft,
setDraftEntrypoint,
setDraftTheme,
@@ -131,12 +134,21 @@ export function useSessionManagement(params: SessionManagementParams) {
const [sessionFullPrompt, setSessionFullPrompt] = useState<{ question: string; theme: Theme } | null>(null);
sessionsRef.current = sessions;
const loadMoreInFlight = useRef(false);
const pendingCreateById = useRef(new Map<string, { continuedFromSessionId?: string }>());
const cloudCreatedIds = useRef(new Set<string>());
for (const session of sessions) {
if (!isUnsavedEmptyConsultation(session)) cloudCreatedIds.current.add(session.id);
}
const visibleSessions = sortSessions(sessions.filter((session) => (showArchivedSessions ? session.archivedAt : !session.archivedAt)
&& (session.sessionType === "birth_time_rectification"
|| session.messages.length > 0
|| !session.messagesHydrated
|| session.id === activeSessionId)));
const visibleSessions = sortSessions(sessions.filter((session) => {
if (showArchivedSessions) {
return Boolean(session.archivedAt)
&& (session.sessionType === "birth_time_rectification"
|| session.messages.length > 0
|| !session.messagesHydrated);
}
return isListedSidebarSession(session);
}));
function updateSession(sessionId: string, change: (session: ChatSession) => ChatSession) {
setSessions((current) => current.map((session) => (session.id === sessionId ? change(session) : session)));
@@ -149,6 +161,9 @@ export function useSessionManagement(params: SessionManagementParams) {
) {
if (!account) throw new Error("账户尚未加载完成");
if (process.env.NODE_ENV === "development" && uiPreview.current) return;
if (mode === "create" && cloudCreatedIds.current.has(session.id)) return;
const continuedFromSessionId = options?.continuedFromSessionId
?? pendingCreateById.current.get(session.id)?.continuedFromSessionId;
const values = mode === "create"
? {
title: session.title,
@@ -160,8 +175,8 @@ export function useSessionManagement(params: SessionManagementParams) {
chart_profile_id: session.chartProfileId,
chart_profile_name: session.chartProfileName,
chart_profile_role: session.chartProfileRole,
...(options?.continuedFromSessionId
? { continued_from_session_id: options.continuedFromSessionId }
...(continuedFromSessionId
? { continued_from_session_id: continuedFromSessionId }
: {}),
}
: {
@@ -173,6 +188,11 @@ export function useSessionManagement(params: SessionManagementParams) {
chart_profile_role: session.chartProfileRole,
};
await writeChatSession(session.id, values, mode);
if (mode === "create") {
pendingCreateById.current.delete(session.id);
cloudCreatedIds.current.add(session.id);
if (!uiPreview.current) writeSessionUrl(session.id, "push");
}
}
async function ensureSessionMessages(sessionId: string) {
@@ -305,55 +325,24 @@ export function useSessionManagement(params: SessionManagementParams) {
async function startNewChat(options?: { continuedFromSessionId?: string }): Promise<ChatSession | null> {
if (!account || !modelCatalog || creatingSession) return null;
if (!options?.continuedFromSessionId) {
const reusable = findReusableEmptyConsultation(sessions);
if (reusable) {
setActiveSessionId(reusable.id);
if (!uiPreview.current) writeSessionUrl(reusable.id, "push");
setDraft("");
setDraftTheme(null);
setDraftEntrypoint(null);
setComposerNotice("");
setRequestError(null);
return reusable;
}
}
const nextSession = {
...createSession(modelCatalog.defaultModelId),
...chartSnapshotForSession(activeChartId, chartLibrary, profile),
};
const previousSessionId = activeSession?.id ?? "";
const previousHref = `${window.location.pathname}${window.location.search}`;
setCreatingSession(true);
setSessions((current) => [nextSession, ...current]);
for (const session of sessions) {
if (isUnsavedEmptyConsultation(session)) pendingCreateById.current.delete(session.id);
}
pendingCreateById.current.set(nextSession.id, {
continuedFromSessionId: options?.continuedFromSessionId,
});
setSessions((current) => replaceUnsavedEmptyConsultations(current, nextSession));
setActiveSessionId(nextSession.id);
if (!uiPreview.current) writeSessionUrl(nextSession.id, "push");
setDraft("");
setDraftTheme(null);
setDraftEntrypoint(null);
setComposerNotice("");
setRequestError(null);
try {
await persistSession(
nextSession,
"create",
options?.continuedFromSessionId
? { continuedFromSessionId: options.continuedFromSessionId }
: undefined,
);
return nextSession;
} catch (caught) {
setSessions((current) => current.filter((session) => session.id !== nextSession.id));
setActiveSessionId(previousSessionId);
if (!uiPreview.current) window.history.replaceState(null, "", previousHref);
setRequestError({
sessionId: previousSessionId,
message: caught instanceof Error ? caught.message : "新对话未能保存到云端。",
});
return null;
} finally {
setCreatingSession(false);
}
return nextSession;
}
function selectSession(sessionId: string) {
@@ -418,6 +407,7 @@ export function useSessionManagement(params: SessionManagementParams) {
archived: showArchivedSessions,
});
const incoming = readSessions(page.sessions, modelCatalog).sessions;
for (const session of incoming) cloudCreatedIds.current.add(session.id);
setSessions((current) => mergeSessionPage(current, incoming));
setSessionsCursor(page.nextCursor);
} catch (caught) {
@@ -434,6 +424,7 @@ export function useSessionManagement(params: SessionManagementParams) {
try {
const page = await fetchSessions(undefined, { archived: nextArchived });
const parsed = readSessions(page.sessions, modelCatalog).sessions;
for (const session of parsed) cloudCreatedIds.current.add(session.id);
const active = sessionsRef.current.find((session) => session.id === activeSessionId);
setSessions(active && !parsed.some((session) => session.id === active.id)
? mergeHydratedSession(parsed, active)
@@ -462,6 +453,7 @@ export function useSessionManagement(params: SessionManagementParams) {
if (query.present && requestedId && !listed.some((session) => session.id === requestedId)) {
void lookupSessionById(requestedId, modelCatalog).then((looked) => {
if (looked.status === "found") {
cloudCreatedIds.current.add(looked.session.id);
setSessions((current) => mergeHydratedSession(current, looked.session));
sessionSelectionSource.current = "history";
selectSession(looked.session.id);