export const SESSION_URL_QUERY_KEY = "c"; export const NEW_CHAT_QUERY_KEY = "new"; /** * Legacy key only. Until BUG-1052 a 401 or a secondary-page link stashed the * current `?c=` here and a bare `/` reopened it. Nothing writes it any more; * bootstrap removes a leftover value so an old tab cannot keep a stale id. */ export const SESSION_URL_RETURN_STORAGE_KEY = "jyotisha.session-url-return"; export const SESSION_MISSING_NOTICE = "该对话不存在或已被删除"; export const SESSION_LOOKUP_FAILED_NOTICE = "这条对话暂时读不到,请稍后重试。"; export const SESSION_URL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; export type SessionUrlQuery = { readonly present: boolean; readonly sessionId: string | null; }; export type BootstrapSessionSelection = { readonly sessionId: string; readonly urlAction: "keep" | "replace-clear" | "none" | "lookup" | "new-chat"; readonly missing: boolean; }; /** * The one new-chat intent. The read-only sidebar and /people「和 TA 对话」 both * link here; the person rides on the current-subject store, not the URL. * Bootstrap turns it into a local empty consultation, so no older session is * reopened (BUG-1015/1038). */ export function newChatHref(): string { return "/?new=1"; } export function parseNewChatIntent(search: string): boolean { const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search); return params.has(NEW_CHAT_QUERY_KEY); } export function parseSessionUrlQuery(search: string): SessionUrlQuery { const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search); if (!params.has(SESSION_URL_QUERY_KEY)) { return { present: false, sessionId: null }; } const raw = params.get(SESSION_URL_QUERY_KEY) ?? ""; return { present: true, sessionId: SESSION_URL_ID_PATTERN.test(raw) ? raw : null, }; } export function sessionHref(search: string, sessionId: string | null, pathname = "/"): string { const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search); params.delete(NEW_CHAT_QUERY_KEY); if (sessionId) params.set(SESSION_URL_QUERY_KEY, sessionId); else params.delete(SESSION_URL_QUERY_KEY); const query = params.toString(); const path = pathname || "/"; return query ? `${path}?${query}` : path; } export function writeSessionUrl(sessionId: string | null, mode: "push" | "replace") { const { location, history } = globalThis.window; const next = sessionHref(location.search, sessionId, location.pathname); const current = `${location.pathname}${location.search}`; if (current === next) return; if (mode === "push") history.pushState(null, "", next); else history.replaceState(null, "", next); } /** Removes a stash an older build left behind (BUG-1052). Never read back. */ export function clearLoginSessionReturn() { try { globalThis.sessionStorage.removeItem(SESSION_URL_RETURN_STORAGE_KEY); } catch { // Ignore private-mode quota errors. } } /** * Where a full load of `/` lands (BUG-1052): `?new=1` → a new chat; `?c=` → * that conversation (shared links, sidebar links, refresh inside a * conversation); anything else — login, a typed address, a bookmark, a * refresh of bare `/` — → the blank starter home. There is no stored * "return to the last conversation" any more. */ export function resolveBootstrapSessionSelection(input: { readonly listedIds: readonly string[]; readonly defaultSessionId: string; readonly search: string; }): BootstrapSessionSelection { if (parseNewChatIntent(input.search)) { return { sessionId: input.defaultSessionId, urlAction: "new-chat", missing: false }; } const query = parseSessionUrlQuery(input.search); if (query.present) { if (query.sessionId && input.listedIds.includes(query.sessionId)) { return { sessionId: query.sessionId, urlAction: "keep", missing: false }; } if (query.sessionId) { return { sessionId: query.sessionId, urlAction: "lookup", missing: false }; } return { sessionId: input.defaultSessionId, urlAction: "replace-clear", missing: true }; } return { sessionId: input.defaultSessionId, urlAction: "none", missing: false }; } /** Outcome of looking up a `?c=` id that is not in the loaded page (BUG-705). */ export function bootstrapSelectionFromLookup( status: "found" | "missing" | "unavailable", requestedId: string, defaultSessionId: string, ): BootstrapSessionSelection { if (status === "found") return { sessionId: requestedId, urlAction: "keep", missing: false }; if (status === "missing") return { sessionId: defaultSessionId, urlAction: "replace-clear", missing: true }; return { sessionId: defaultSessionId, urlAction: "none", missing: false }; }