只做启程(一):同一笔预扣覆盖已公布的标准档,更贵的只扣差价;引导状态与人物关系只加列。tsc 与 lint 通过。test:db 91 项,失败 14,其中 13 条为 Windows 已知失败;校正遥测单独重跑通过。 Co-Authored-By: Grok (agent loop)
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
/** Server-owned onboarding flags. The only keys the checklist and hints may store. */
|
|
|
|
export const ONBOARDING_STATE_KEYS = ["checklistDismissedAt", "hintsSeen", "chartVisitedAt"] as const;
|
|
|
|
export const ONBOARDING_HINT_IDS = [
|
|
"follow-up",
|
|
"sidebar-chart",
|
|
"tab-chart",
|
|
"tab-info",
|
|
"tab-dasha",
|
|
"tab-west",
|
|
"syn-ready",
|
|
"ephemeris",
|
|
] as const;
|
|
|
|
export type OnboardingHintId = (typeof ONBOARDING_HINT_IDS)[number];
|
|
|
|
export type OnboardingState = {
|
|
readonly checklistDismissedAt?: string | null;
|
|
readonly hintsSeen?: readonly OnboardingHintId[];
|
|
readonly chartVisitedAt?: string | null;
|
|
};
|
|
|
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
|
|
function isHintId(value: unknown): value is OnboardingHintId {
|
|
return ONBOARDING_HINT_IDS.some((id) => id === value);
|
|
}
|
|
|
|
function timestamp(value: unknown): string | null | undefined {
|
|
if (value == null) return null;
|
|
if (typeof value !== "string" || !TIMESTAMP.test(value) || Number.isNaN(Date.parse(value))) return undefined;
|
|
return value;
|
|
}
|
|
|
|
export function parseOnboardingState(value: unknown): OnboardingState {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
const record = value as Record<string, unknown>;
|
|
const state: {
|
|
checklistDismissedAt?: string | null;
|
|
hintsSeen?: OnboardingHintId[];
|
|
chartVisitedAt?: string | null;
|
|
} = {};
|
|
if ("checklistDismissedAt" in record) {
|
|
const parsed = timestamp(record.checklistDismissedAt);
|
|
if (parsed !== undefined) state.checklistDismissedAt = parsed;
|
|
}
|
|
if ("chartVisitedAt" in record) {
|
|
const parsed = timestamp(record.chartVisitedAt);
|
|
if (parsed !== undefined) state.chartVisitedAt = parsed;
|
|
}
|
|
if (Array.isArray(record.hintsSeen)) {
|
|
const hints = record.hintsSeen.filter(isHintId);
|
|
if (hints.length === record.hintsSeen.length) state.hintsSeen = hints;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
export function parseOnboardingPatch(
|
|
value: unknown,
|
|
): { ok: true; patch: OnboardingState } | { ok: false; error: string } {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return { ok: false, error: "引导状态格式不正确" };
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
const unknownKey = Object.keys(record).find((key) => !ONBOARDING_STATE_KEYS.some((allowed) => allowed === key));
|
|
if (unknownKey) return { ok: false, error: "只能记下清单、提示和是否进过星盘页" };
|
|
if (Object.keys(record).length === 0) return { ok: false, error: "没有要保存的引导状态" };
|
|
|
|
const patch: {
|
|
checklistDismissedAt?: string | null;
|
|
hintsSeen?: OnboardingHintId[];
|
|
chartVisitedAt?: string | null;
|
|
} = {};
|
|
if ("checklistDismissedAt" in record) {
|
|
const parsed = timestamp(record.checklistDismissedAt);
|
|
if (parsed === undefined) return { ok: false, error: "时间格式不正确" };
|
|
patch.checklistDismissedAt = parsed;
|
|
}
|
|
if ("chartVisitedAt" in record) {
|
|
const parsed = timestamp(record.chartVisitedAt);
|
|
if (parsed === undefined) return { ok: false, error: "时间格式不正确" };
|
|
patch.chartVisitedAt = parsed;
|
|
}
|
|
if ("hintsSeen" in record) {
|
|
if (!Array.isArray(record.hintsSeen) || record.hintsSeen.some((item) => !isHintId(item))) {
|
|
return { ok: false, error: "这个提示不在名单里" };
|
|
}
|
|
patch.hintsSeen = record.hintsSeen.filter(isHintId);
|
|
}
|
|
return { ok: true, patch };
|
|
}
|