refactor(chat): extract profile onboarding and rectification surface hooks

Close the home-page split by moving account/onboarding and rectification glue out of page.tsx without relocating React state, keeping the route static and hook order intact.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 05:56:47 +08:00
parent fbb80fa376
commit bf6989eccf
15 changed files with 920 additions and 527 deletions
@@ -0,0 +1,424 @@
"use client";
import type { Dispatch, FormEvent, MutableRefObject, SetStateAction } from "react";
import {
beamAvatarSchema,
type BeamAvatarPatch,
} from "@/lib/beam-avatar";
import {
applyPersistedBirthTime,
birthTimePersistenceValues,
isBirthTimeDraftReady,
} from "@/lib/birth-time-intake-model";
import {
birthTimeConsultationOptionsCopy,
createBirthTimeConsultationConsentState,
type BirthTimeConsultationConsentState,
type LatestAccountRequestGuard,
} from "@/lib/birth-time-consultation-consent";
import { composerDraftSnapshot } from "@/lib/composer-draft";
import {
activeChartStorageKey,
fetchAccount,
friendlyError,
LoginRedirectError,
saveCloudChartProfile,
} from "@/lib/home-cloud-sync";
import {
birthProfileDeclarationChanged,
buildSelfChartRecord,
missingProfileStep,
readProfile,
selectedBirthPlace,
} from "@/lib/home-profile";
import { createStartGreeting } from "@/lib/onboarding-client";
import {
timestamp,
type Account,
type AccountDialog,
type ChartLibraryRecord,
type OnboardingStep,
type Profile,
} from "@/lib/home-types";
import { preserveShallowEqual } from "@/lib/preserve-shallow-equal";
import {
requestBirthTimeAssessment,
type JourneyClientResponse,
} from "@/lib/birth-time-journey-client";
import { previewRectificationJourney } from "@/lib/birth-time-guided-preview";
import { selfHostedOtpActions } from "@/modules/identity/client";
import type { BirthTimeAssessmentPhase } from "@/components/birth-time-assessment-overlay";
export type ProfileOnboardingParams = {
account: Account | null;
accountRefreshGuard: MutableRefObject<LatestAccountRequestGuard>;
accountTrigger: MutableRefObject<HTMLButtonElement | null>;
activeChartId: string;
avatarSaving: boolean;
birthTimeRevisionPending: MutableRefObject<boolean>;
chartLibrary: ChartLibraryRecord[];
dialogReturnTarget: MutableRefObject<HTMLButtonElement | null>;
profile: Profile;
profileDraft: Profile;
profileSaving: boolean;
setAccount: Dispatch<SetStateAction<Account | null>>;
setAccountError: Dispatch<SetStateAction<string>>;
setAccountMenuOpen: Dispatch<SetStateAction<boolean>>;
setActiveAccountDialog: Dispatch<SetStateAction<AccountDialog | null>>;
setActiveChartId: Dispatch<SetStateAction<string>>;
setAvatarNotice: Dispatch<SetStateAction<string>>;
setAvatarSaving: Dispatch<SetStateAction<boolean>>;
setBirthTimeAssessmentPhase: Dispatch<SetStateAction<BirthTimeAssessmentPhase | null>>;
setBirthTimeConsultationConsent: Dispatch<SetStateAction<BirthTimeConsultationConsentState>>;
setBirthTimeError: Dispatch<SetStateAction<string>>;
setBirthTimeJourney: Dispatch<SetStateAction<JourneyClientResponse | null>>;
setDraft: (value: string) => void;
setEditingSelfChart: Dispatch<SetStateAction<boolean>>;
setOnboardingJustCompleted: Dispatch<SetStateAction<boolean>>;
setOnboardingStep: Dispatch<SetStateAction<OnboardingStep>>;
setPresetMessageLength: Dispatch<SetStateAction<number>>;
setProfile: Dispatch<SetStateAction<Profile>>;
setProfileDraft: Dispatch<SetStateAction<Profile>>;
setProfileNotice: Dispatch<SetStateAction<string>>;
setProfileSaving: Dispatch<SetStateAction<boolean>>;
setRectificationError: Dispatch<SetStateAction<string>>;
setSigningOut: Dispatch<SetStateAction<boolean>>;
setStartGreeting: Dispatch<SetStateAction<string>>;
signingOut: boolean;
uiPreview: MutableRefObject<boolean>;
};
export function useProfileOnboarding(params: ProfileOnboardingParams) {
const {
account,
accountRefreshGuard,
accountTrigger,
activeChartId,
avatarSaving,
birthTimeRevisionPending,
chartLibrary,
dialogReturnTarget,
profile,
profileDraft,
profileSaving,
setAccount,
setAccountError,
setAccountMenuOpen,
setActiveAccountDialog,
setActiveChartId,
setAvatarNotice,
setAvatarSaving,
setBirthTimeAssessmentPhase,
setBirthTimeConsultationConsent,
setBirthTimeError,
setBirthTimeJourney,
setDraft,
setEditingSelfChart,
setOnboardingJustCompleted,
setOnboardingStep,
setPresetMessageLength,
setProfile,
setProfileDraft,
setProfileNotice,
setProfileSaving,
setRectificationError,
setSigningOut,
setStartGreeting,
signingOut,
uiPreview,
} = params;
async function refreshAccount() {
const requestIdentity = accountRefreshGuard.current.begin();
try {
const latest = await fetchAccount();
if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return;
const nextProfile = readProfile(latest.profile);
const activeOther = activeChartId !== "self"
&& chartLibrary.find((record) => record.id === activeChartId && record.role === "other");
if (!activeOther) {
setProfile((current) => preserveShallowEqual(current, nextProfile));
}
setAccount(latest);
setAccountError("");
} catch (caught) {
if (caught instanceof LoginRedirectError) return;
if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return;
setAccountError(caught instanceof Error ? caught.message : "暂时无法读取账户信息");
}
}
function openAccountDialog(dialog: AccountDialog, returnTarget: HTMLButtonElement | null = accountTrigger.current) {
dialogReturnTarget.current = returnTarget ?? accountTrigger.current;
setAccountMenuOpen(false);
setAccountError("");
if (dialog === "profile") {
setProfileNotice("");
setAvatarNotice("");
}
if (dialog === "chart-library") {
setProfileDraft(profile);
setProfileNotice("");
setEditingSelfChart(false);
}
setActiveAccountDialog(dialog);
}
function closeAccountDialog() {
if (signingOut) return;
setActiveAccountDialog(null);
const returnTarget = dialogReturnTarget.current;
window.requestAnimationFrame(() => returnTarget?.focus());
}
async function persistAvatar(patch: BeamAvatarPatch) {
if (!account?.avatar || avatarSaving) return;
setAvatarSaving(true);
setAvatarNotice("");
setAccountError("");
try {
const response = await fetch("/api/account/avatar", {
method: "PATCH",
credentials: "same-origin",
headers: { "content-type": "application/json" },
body: JSON.stringify(patch),
});
const payload = await response.json().catch(() => null) as { avatar?: unknown; error?: string } | null;
if (!response.ok) throw new Error(payload?.error || "头像暂时无法保存。");
const avatar = beamAvatarSchema.parse(payload?.avatar);
setAccount((current) => current ? { ...current, avatar } : current);
setAvatarNotice(patch.action === "randomize" ? "已生成并保存新头像。" : "头像配色已保存。");
} catch (caught) {
setAccountError(friendlyError(caught instanceof Error ? caught.message : "头像保存失败"));
} finally {
setAvatarSaving(false);
}
}
async function persistProfile(nextProfile: Profile): Promise<Profile> {
if (!account) throw new Error("账户尚未加载完成");
if (process.env.NODE_ENV === "development" && uiPreview.current) return nextProfile;
const birthPlace = selectedBirthPlace(nextProfile);
const response = await fetch("/api/account", {
method: "PATCH",
credentials: "same-origin",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: nextProfile.name.trim() || null,
birth_date: nextProfile.date || null,
...birthTimePersistenceValues(nextProfile),
country_code: nextProfile.countryCode,
province_code: nextProfile.provinceCode || null,
city_code: nextProfile.cityCode || null,
district_code: nextProfile.districtCode || null,
birth_place_label: nextProfile.birthPlaceLabel || null,
birth_place_type: nextProfile.birthPlaceType || null,
birth_place_provider: nextProfile.birthPlaceProvider || null,
birth_place_provider_id: nextProfile.birthPlaceProviderId || null,
latitude: birthPlace?.lat ?? null,
longitude: birthPlace?.lon ?? null,
timezone_id: nextProfile.timezoneId || null,
timezone_offset: birthPlace?.tz ?? null,
timezone_source: nextProfile.timezoneSource || null,
}),
});
const payload = await response.json().catch(() => null) as {
error?: string;
birthTime?: unknown;
} | null;
if (!response.ok) {
throw new Error(payload?.error || "账户资料暂时无法保存。");
}
const savedProfile = applyPersistedBirthTime(nextProfile, payload?.birthTime);
setAccount((current) => current ? { ...current, profile: savedProfile } : current);
setActiveChartId("self");
localStorage.setItem(activeChartStorageKey(account.user.id), "self");
await saveCloudChartProfile({ ...buildSelfChartRecord(savedProfile), updatedAt: timestamp() }).catch(() => null);
return savedProfile;
}
async function assessSavedBirthTime(nextProfile: Profile) {
const result = process.env.NODE_ENV === "development" && uiPreview.current
? previewRectificationJourney
: await requestBirthTimeAssessment();
const nextStatus = result.snapshot.state === "ready"
? "confirmed"
: result.snapshot.state === "candidate"
? "candidate"
: "rectifying";
const assessedProfile: Profile = {
...nextProfile,
time: result.snapshot.activeTime ?? "",
birthTimeStatus: nextStatus,
rectificationCaseId: result.caseId,
};
setBirthTimeJourney(result);
setBirthTimeError("");
setProfile(assessedProfile);
setProfileDraft(assessedProfile);
return assessedProfile;
}
async function saveProfile(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!profileDraft.name.trim() || !isBirthTimeDraftReady(profileDraft) || !selectedBirthPlace(profileDraft) || !account || profileSaving) return;
setProfileSaving(true);
setProfileNotice("");
setAccountError("");
try {
const declarationChanged = birthProfileDeclarationChanged(profile, profileDraft);
const savedProfile = await persistProfile(profileDraft);
setProfile(savedProfile);
setProfileDraft(savedProfile);
setRectificationError("");
if (declarationChanged) {
setBirthTimeConsultationConsent(createBirthTimeConsultationConsentState());
void refreshAccount();
}
setProfileNotice(savedProfile.birthTimeStatus === "confirmed"
? "出生资料已保存到云端,可在同一账号的其他设备使用。"
: `出生资料已保存。${birthTimeConsultationOptionsCopy(savedProfile)}`);
} catch (caught) {
setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生资料保存失败"));
} finally {
setProfileSaving(false);
}
}
async function saveOnboardingName() {
const name = composerDraftSnapshot().replace(/\s+/g, " ").trim().slice(0, 80);
if (!name || !account || profileSaving) return;
const nextProfile = { ...profileDraft, name };
setProfileSaving(true);
setAccountError("");
try {
const savedProfile = await persistProfile(nextProfile);
setProfile(savedProfile);
setProfileDraft(savedProfile);
setStartGreeting(createStartGreeting(savedProfile.name));
setDraft("");
setPresetMessageLength(0);
const nextStep = missingProfileStep(savedProfile);
if (nextStep) setOnboardingStep(nextStep);
else setOnboardingJustCompleted(false);
} catch (caught) {
setAccountError(friendlyError(caught instanceof Error ? caught.message : "称呼保存失败"));
} finally {
setProfileSaving(false);
}
}
async function saveOnboardingBirth(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!isBirthTimeDraftReady(profileDraft) || !account || profileSaving) return;
setProfileSaving(true);
setBirthTimeAssessmentPhase("saving_profile");
setAccountError("");
try {
const savedProfile = await persistProfile(profileDraft);
birthTimeRevisionPending.current = false;
setProfile(savedProfile);
setProfileDraft(savedProfile);
setPresetMessageLength(0);
const nextStep = missingProfileStep(savedProfile);
if (nextStep) setOnboardingStep(nextStep);
else setOnboardingJustCompleted(false);
} catch (caught) {
setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生时间保存失败"));
} finally {
setBirthTimeAssessmentPhase(null);
setProfileSaving(false);
}
}
function editDeclaredBirthTimeDetails() {
birthTimeRevisionPending.current = true;
setBirthTimeError("");
setPresetMessageLength(0);
setOnboardingStep("birth");
}
async function saveOnboardingPlace(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!selectedBirthPlace(profileDraft) || !account || profileSaving) return;
setProfileSaving(true);
setBirthTimeAssessmentPhase("entering_home");
setAccountError("");
try {
const savedProfile = await persistProfile(profileDraft);
setProfile(savedProfile);
setProfileDraft(savedProfile);
setStartGreeting(createStartGreeting(savedProfile.name));
setPresetMessageLength(0);
setOnboardingJustCompleted(false);
} catch (caught) {
setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生地点保存失败"));
} finally {
setBirthTimeAssessmentPhase(null);
setProfileSaving(false);
}
}
function completeGuidedBirthTime(result: JourneyClientResponse) {
if (result.nextAction.kind !== "ready") return;
const confirmedProfile: Profile = {
...profileDraft,
time: result.nextAction.activeTime,
birthTimeStatus: "confirmed",
rectificationCaseId: result.caseId,
};
setProfile(confirmedProfile);
setProfileDraft(confirmedProfile);
setPresetMessageLength(0);
setOnboardingJustCompleted(false);
}
async function retryBirthTimeAssessment() {
if (!account || profileSaving) return;
setProfileSaving(true);
setBirthTimeError("");
try {
const assessedProfile = await assessSavedBirthTime(profileDraft);
setPresetMessageLength(0);
if (assessedProfile.birthTimeStatus === "confirmed") setOnboardingJustCompleted(false);
} catch (caught) {
setBirthTimeError(caught instanceof Error ? caught.message : "生时评估暂时不可用,请稍后重试。");
} finally {
setProfileSaving(false);
}
}
async function signOut() {
if (signingOut) return;
setSigningOut(true);
setAccountError("");
try {
await selfHostedOtpActions.signOut();
window.location.assign("/login");
} catch (caught) {
const message = caught instanceof Error ? caught.message : "退出失败";
setAccountError(friendlyError(message));
setSigningOut(false);
}
}
return {
refreshAccount,
openAccountDialog,
closeAccountDialog,
persistAvatar,
persistProfile,
assessSavedBirthTime,
saveProfile,
saveOnboardingName,
saveOnboardingBirth,
editDeclaredBirthTimeDetails,
saveOnboardingPlace,
completeGuidedBirthTime,
retryBirthTimeAssessment,
signOut,
};
}
@@ -0,0 +1,306 @@
"use client";
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
import { resolveSessionTitle } from "@/lib/agent-reply";
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
import { writeSessionUrl } from "@/lib/chat-session-url";
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
import { chartSnapshotForSession, missingProfileStep } from "@/lib/home-profile";
import {
timestamp,
type Account,
type AccountDialog,
type ChartLibraryRecord,
type ChatSession,
type Message,
type OnboardingStep,
type Profile,
type Theme,
} from "@/lib/home-types";
import {
entrySummaryFromResponse,
isTerminalRectificationStatus,
openRectificationRequestBody,
openResponseFromPayload,
type RectificationEntrySummary,
} from "@/lib/rectification-entry";
import type { PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification";
import type { PublicLanguageModelCatalog } from "@/lib/public-models";
export type RectificationSurfaceParams = {
account: Account | null;
activeChartId: string;
activeSessionIdRef: MutableRefObject<string>;
chartLibrary: ChartLibraryRecord[];
creatingSession: boolean;
modelCatalog: PublicLanguageModelCatalog | null;
persistSession: (session: ChatSession, mode?: "create" | "update") => Promise<void>;
profile: Profile;
rectificationLoading: boolean;
rectificationMutationPending: boolean;
rectificationOpenInFlight: MutableRefObject<boolean>;
rectificationSessionId: string | null;
resumeRectificationSession: MutableRefObject<(session: ChatSession) => void>;
sessionSelectionSource: MutableRefObject<"user" | "history">;
sessions: ChatSession[];
setActiveSessionId: Dispatch<SetStateAction<string>>;
setDraft: (value: string) => void;
setDraftEntrypoint: (entrypoint: ConsultationEntrypoint | null) => void;
setDraftTheme: (theme: Theme | null) => void;
setOnboardingStep: Dispatch<SetStateAction<OnboardingStep>>;
setProfileNotice: Dispatch<SetStateAction<string>>;
setRectificationCaseId: Dispatch<SetStateAction<string | null>>;
setRectificationEntrySummary: Dispatch<SetStateAction<RectificationEntrySummary | null>>;
setRectificationError: Dispatch<SetStateAction<string>>;
setRectificationLoading: Dispatch<SetStateAction<boolean>>;
setRectificationPendingQuestion: Dispatch<SetStateAction<string | null>>;
setRectificationReadonly: Dispatch<SetStateAction<boolean>>;
setRectificationSessionId: Dispatch<SetStateAction<string | null>>;
setRectificationShouldStartOpening: Dispatch<SetStateAction<boolean>>;
setRectificationTurns: Dispatch<SetStateAction<PersistedRectificationTurn[]>>;
setSessions: Dispatch<SetStateAction<ChatSession[]>>;
uiPreview: MutableRefObject<boolean>;
updateSession: (sessionId: string, change: (session: ChatSession) => ChatSession) => void;
openAccountDialog: (dialog: AccountDialog, returnTarget?: HTMLButtonElement | null) => void;
refreshAccount: () => Promise<void>;
};
export function useRectificationSurface(params: RectificationSurfaceParams) {
const {
account,
activeChartId,
activeSessionIdRef,
chartLibrary,
creatingSession,
modelCatalog,
persistSession,
profile,
rectificationLoading,
rectificationMutationPending,
rectificationOpenInFlight,
rectificationSessionId,
resumeRectificationSession,
sessionSelectionSource,
sessions,
setActiveSessionId,
setDraft,
setDraftEntrypoint,
setDraftTheme,
setOnboardingStep,
setProfileNotice,
setRectificationCaseId,
setRectificationEntrySummary,
setRectificationError,
setRectificationLoading,
setRectificationPendingQuestion,
setRectificationReadonly,
setRectificationSessionId,
setRectificationShouldStartOpening,
setRectificationTurns,
setSessions,
uiPreview,
updateSession,
openAccountDialog,
refreshAccount,
} = params;
async function refreshRectificationEntrySummary() {
if (!account) return;
try {
const response = await fetch("/api/rectification/cases/entry-summary", { cache: "no-store" });
if (!response.ok) return;
const payload = await response.json().catch(() => null);
setRectificationEntrySummary(entrySummaryFromResponse(payload));
} catch {
// The CTA falls back to the server-agnostic default labels.
}
}
async function refreshRectificationCase(caseId: string, sessionId: string) {
try {
const response = await fetch(
`/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
{ cache: "no-store" },
);
if (!response.ok) return;
const payload = await response.json().catch(() => null);
const turns = Array.isArray(payload?.turns) ? payload.turns : [];
setRectificationTurns(turns.map((turn: { id?: unknown; role?: unknown; text?: unknown; status?: unknown; receipt?: unknown }) => ({
id: String(turn?.id ?? ""),
role: turn?.role === "user" ? "user" as const : "assistant" as const,
text: typeof turn?.text === "string" ? turn.text : null,
status: String(turn?.status ?? "completed"),
receipt: turn?.receipt && typeof turn.receipt === "object" ? {
status: String((turn.receipt as { status?: unknown }).status ?? ""),
phases: Array.isArray((turn.receipt as { phases?: unknown }).phases) ? (turn.receipt as { phases: unknown[] }).phases.map(String) : [],
tools: Array.isArray((turn.receipt as { tools?: unknown }).tools) ? (turn.receipt as { tools: unknown[] }).tools.map(String) : [],
methods: Array.isArray((turn.receipt as { methods?: unknown }).methods) ? (turn.receipt as { methods: unknown[] }).methods.map(String) : [],
skill_name: typeof (turn.receipt as { skill_name?: unknown }).skill_name === "string" ? (turn.receipt as { skill_name: string }).skill_name : undefined,
skill_version: typeof (turn.receipt as { skill_version?: unknown }).skill_version === "string" ? (turn.receipt as { skill_version: string }).skill_version : undefined,
} : null,
})));
} catch {
// History refresh is best-effort; the stream restores live turns.
}
}
async function openRectificationCase(
intent: "homepage" | "session" | "new",
exactSessionId: string | null,
pendingConsultationQuestion: string | null,
) {
if (!account || !modelCatalog || creatingSession || rectificationLoading || rectificationOpenInFlight.current
|| rectificationMutationPending) {
sessionSelectionSource.current = "user";
return null;
}
const missingStep = missingProfileStep(profile);
if (missingStep) {
sessionSelectionSource.current = "user";
setRectificationSessionId(null);
setRectificationCaseId(null);
setRectificationPendingQuestion(null);
setOnboardingStep(missingStep);
setComposerNotice("请先完成出生资料,再开始生时校正。");
return null;
}
rectificationOpenInFlight.current = true;
setRectificationLoading(true);
setRectificationError("");
try {
const response = await fetch("/api/rectification/cases/open", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(openRectificationRequestBody(intent, exactSessionId)),
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
const code = payload?.code;
if (code === "profile_incomplete") {
handleRectificationProfileIncomplete();
} else if (code === "invalid_open_request") {
setRectificationError(payload?.error || "生时校正打开请求不正确,请刷新后重试。");
} else if (code === "active_case_conflict") {
setRectificationError(payload?.error || "仍有未完成的校正,请先回到当前校正。");
} else if (code === "case_session_not_found") {
setRectificationError("该校正会话不存在或已结束。");
} else {
setRectificationError(payload?.error || "暂时无法打开生时校正。");
}
return null;
}
const opened = openResponseFromPayload(payload);
if (!opened) {
setRectificationError("生时校正服务响应不正确,请稍后重试。");
return null;
}
// Merge the server-created session into the local list. The browser
// never generates a Case id; it only mirrors the returned binding.
const existing = sessions.find((session) => session.id === opened.sessionId);
const merged: ChatSession = {
id: opened.sessionId,
title: resolveSessionTitle("生时校正", undefined, {
entrypoint: "birth_time_rectification",
existingTitles: sessions.map((session) => session.title),
}),
theme: "general",
modelId: modelCatalog.defaultModelId ?? "",
messages: [],
updatedAt: timestamp(),
sessionType: "birth_time_rectification",
rectificationCaseId: opened.caseId,
pinned: existing?.pinned ?? false,
archivedAt: existing?.archivedAt ?? null,
messagesHydrated: true,
...chartSnapshotForSession(activeChartId, chartLibrary, profile),
};
setSessions((current) => [merged, ...current.filter((session) => session.id !== merged.id)]);
void persistSession(merged).catch(() => {});
setRectificationPendingQuestion(pendingConsultationQuestion);
setDraft("");
setDraftTheme(null);
setDraftEntrypoint(null);
setRectificationSessionId(opened.sessionId);
setRectificationCaseId(opened.caseId);
setRectificationShouldStartOpening(opened.shouldStartOpening);
setRectificationReadonly(
opened.disposition === "readonly" || isTerminalRectificationStatus(opened.status),
);
setRectificationTurns([]);
activeSessionIdRef.current = opened.sessionId;
setActiveSessionId(opened.sessionId);
if (!uiPreview.current && sessionSelectionSource.current === "user") {
writeSessionUrl(opened.sessionId, "push");
}
void refreshRectificationCase(opened.caseId, opened.sessionId);
void refreshRectificationEntrySummary();
return opened;
} catch {
setRectificationError("生时校正会话暂时无法打开,请稍后重试。");
return null;
} finally {
sessionSelectionSource.current = "user";
rectificationOpenInFlight.current = false;
setRectificationLoading(false);
}
}
async function openRectificationFromHomepage(pendingConsultationQuestion: string | null = null) {
await openRectificationCase("homepage", null, pendingConsultationQuestion);
}
async function openRectificationSession(exactSessionId: string) {
await openRectificationCase("session", exactSessionId, null);
}
async function startNewRectification() {
await openRectificationCase("new", null, null);
}
resumeRectificationSession.current = (session) => {
sessionSelectionSource.current = "history";
void openRectificationSession(session.id);
};
function handleRectificationProfileIncomplete() {
setRectificationError("profile_incomplete");
setRectificationSessionId(null);
setRectificationCaseId(null);
setRectificationPendingQuestion(null);
const missingStep = missingProfileStep(profile);
if (missingStep) {
setOnboardingStep(missingStep);
setComposerNotice("请先完成出生资料,再开始生时校正。");
return;
}
openAccountDialog("profile");
setProfileNotice("服务端未能读取完整出生资料,请重新确认并保存。");
void refreshAccount();
}
function handleRectificationMessagesChange(messages: Message[]) {
if (!rectificationSessionId) return;
updateSession(rectificationSessionId, (session) => ({
...session,
messages,
updatedAt: timestamp(),
}));
}
return {
refreshRectificationEntrySummary,
refreshRectificationCase,
openRectificationCase,
openRectificationFromHomepage,
openRectificationSession,
startNewRectification,
handleRectificationProfileIncomplete,
handleRectificationMessagesChange,
};
}