fix(chat): keep stored rectification titles on open
Independent Staging Quality Gate / validate (push) Successful in 11m43s
Independent Staging Quality Gate / publish (push) Successful in 2m3s

Opening a saved birth-time session no longer restamps the title or updatedAt from the wall clock. Occupies BUG-699, recurrence of BUG-553.
This commit is contained in:
jesse-ux
2026-09-15 14:48:03 +08:00
parent 224244c014
commit 2d7698ead3
13 changed files with 397 additions and 10 deletions
@@ -3,6 +3,7 @@
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
import { resolveSessionTitle } from "@/lib/agent-reply";
import { rectificationOpenIdentity } from "@/lib/rectification-session-open";
import { showChatNotice as setComposerNotice } from "@/lib/chat-notice";
import { writeSessionUrl } from "@/lib/chat-session-url";
import type { ConsultationEntrypoint } from "@/lib/consultation-entrypoint";
@@ -213,16 +214,20 @@ export function useRectificationSurface(params: RectificationSurfaceParams) {
// 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,
const openIdentity = rectificationOpenIdentity(existing, {
title: resolveSessionTitle("生时校正", undefined, {
entrypoint: "birth_time_rectification",
existingTitles: sessions.map((session) => session.title),
}),
updatedAt: timestamp(),
});
const merged: ChatSession = {
id: opened.sessionId,
title: openIdentity.title,
theme: "general",
modelId: modelCatalog.defaultModelId ?? "",
messages: [],
updatedAt: timestamp(),
updatedAt: openIdentity.updatedAt,
sessionType: "birth_time_rectification",
rectificationCaseId: opened.caseId,
pinned: existing?.pinned ?? false,
+4 -1
View File
@@ -57,6 +57,9 @@ export function parseAgentReply(value: string, metadata?: ConsultationReplyMetad
export type SessionTitleOptions = {
readonly entrypoint?: ConsultationEntrypoint | null;
readonly theme?: ConsultationDomain;
/** Wall clock. Only for minting a brand-new session. Re-titling an existing
* session must pass that session's created time; the default `new Date()`
* would stamp today's date onto old chats (BUG-699). */
readonly at?: Date;
readonly existingTitles?: readonly string[];
};
@@ -89,7 +92,7 @@ export function resolveSessionTitle(
modelTitle?: string,
options: SessionTitleOptions = {},
): string {
const at = options.at ?? new Date();
const at = options.at ?? new Date(); // new sessions only; see SessionTitleOptions.at
const existingTitles = options.existingTitles ?? [];
if (modelTitle && !isGenericSessionTitle(modelTitle)) {
return uniquifySessionTitle(clipTitle(modelTitle), existingTitles, at);
@@ -0,0 +1,9 @@
export function rectificationOpenIdentity(
existing: { title: string; updatedAt: number } | undefined,
minted: { title: string; updatedAt: number },
): { title: string; updatedAt: number } {
if (existing) {
return { title: existing.title, updatedAt: existing.updatedAt };
}
return minted;
}
@@ -0,0 +1,43 @@
/** Product UI dates are wall-clock in China. Repair uses this zone, never UTC. */
export const RECTIFICATION_TITLE_TIMEZONE = "Asia/Shanghai";
export const RECTIFICATION_DATED_TITLE =
/^(\d{1,2})月(\d{1,2})日\s*·\s*生时校正(?:\s+\d{2}:\d{2})?$/;
export function datedRectificationTitle(
createdAt: Date,
timeZone = RECTIFICATION_TITLE_TIMEZONE,
): string {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
month: "numeric",
day: "numeric",
}).formatToParts(createdAt);
const month = Number(parts.find((part) => part.type === "month")?.value);
const day = Number(parts.find((part) => part.type === "day")?.value);
if (!Number.isInteger(month) || !Number.isInteger(day)) {
return "生时校正";
}
return `${month}${day}日 · 生时校正`;
}
export function repairedRectificationTitle(row: {
title: string;
createdAt: Date | null;
}): string | null {
const matched = row.title.match(RECTIFICATION_DATED_TITLE);
if (!matched) return null;
if (!row.createdAt || Number.isNaN(row.createdAt.getTime())) {
return "生时校正";
}
const next = datedRectificationTitle(row.createdAt);
const nextMatch = next.match(RECTIFICATION_DATED_TITLE);
if (
nextMatch
&& Number(matched[1]) === Number(nextMatch[1])
&& Number(matched[2]) === Number(nextMatch[2])
) {
return null;
}
return next;
}