import { z } from "zod"; export type DailyStarlanguageCard = { readonly trend: string; readonly action: string; readonly caution: string; }; export type DailyStarlanguageEvidence = { readonly today: string; readonly birthTimeVerified: boolean; readonly ascendantSign: string | null; readonly moonSign: string | null; readonly transitMoonSign: string | null; readonly transitMoonHouse: number | null; readonly vimshottari: { readonly mahadasha: string | null; readonly antardasha: string | null; readonly remainingDays: number | null; readonly nakshatra: string | null; } | null; readonly narayanaSign: string | null; readonly divisional: readonly { readonly chart: string; readonly ascendantSign: string | null; readonly moonSign: string | null; }[]; readonly transit: { readonly totalTriggers: number; readonly top: readonly string[]; } | null; readonly functionalBenefics: readonly string[]; readonly functionalMalefics: readonly string[]; readonly missingLayers: readonly string[]; }; export type DailyStarlanguageEngineLayers = { readonly chart: unknown; readonly vimshottari: unknown; readonly narayana: unknown; readonly varga: unknown; readonly transit: unknown; }; const cardSchema = z.object({ trend: z.string().transform(normalizeLine).pipe(z.string().min(8).max(90)), action: z.string().transform(normalizeLine).pipe(z.string().min(4).max(90)), caution: z.string().transform(normalizeLine).pipe(z.string().min(4).max(90)), }); function normalizeLine(value: string): string { return value.replace(/\s+/g, " ").trim().slice(0, 90); } function asRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function asArray(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : []; } function asText(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim().slice(0, 60) : null; } function asCount(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function at(value: unknown, ...path: readonly string[]): unknown { return path.reduce((current, key) => asRecord(current)?.[key], value); } function chartModule(chart: unknown): unknown { return at(chart, "modules", "chart") ?? chart; } function signOf(value: unknown): string | null { return asText(asRecord(value)?.sign); } function planetSign(container: unknown, planet: string): string | null { return signOf(at(container, "planets", planet)); } function vimshottariLayer(response: unknown): DailyStarlanguageEvidence["vimshottari"] { const current = at(response, "vimshottari_analysis", "current"); if (!asRecord(current)) return null; return { mahadasha: asText(at(current, "mahadasha", "lord")), antardasha: asText(at(current, "antardasha", "lord")), remainingDays: asCount(asRecord(current)?.remaining_days), nakshatra: asText(at(response, "vimshottari_analysis", "nakshatra", "name")), }; } function narayanaSign(response: unknown, today: string): string | null { const periods = asArray(asRecord(response)?.periods); const running = periods.find((period) => { const start = asText(asRecord(period)?.start); const end = asText(asRecord(period)?.end); return Boolean(start && end && start <= today && today <= end); }); return asText(asRecord(running ?? periods[0])?.lord); } function divisionalLayer(response: unknown): DailyStarlanguageEvidence["divisional"] { const result = asRecord(asRecord(response)?.result); if (!result) return []; return Object.entries(result) .slice(0, 4) .map(([chart, value]) => ({ chart, ascendantSign: signOf(asRecord(value)?.ascendant), moonSign: planetSign(value, "Moon"), })) .filter((entry) => entry.ascendantSign || entry.moonSign); } function transitLayer(response: unknown): DailyStarlanguageEvidence["transit"] { const summary = asRecord(asRecord(response)?.summary); if (!summary) return null; const top = asArray(summary.top_triggers) .slice(0, 3) .map((trigger) => { const planet = asText(asRecord(trigger)?.planet); const target = asText(asRecord(trigger)?.target); const event = asText(asRecord(trigger)?.event); return [planet, target, event].filter(Boolean).join(" → "); }) .filter((line) => line.length > 0); return { totalTriggers: asCount(summary.total_triggers) ?? 0, top }; } function functionalPlanets(chart: unknown, key: "functional_benefics" | "functional_malefics"): readonly string[] { const snapshot = asRecord(at(chart, "ai_prompt_pack", "evidence_snapshot", "functional_benefic_malefic")); if (!snapshot || snapshot.status !== "used") return []; return asArray(snapshot[key]) .map((planet) => asText(planet)) .filter((planet): planet is string => Boolean(planet)) .slice(0, 5); } export function dailyStarlanguageEvidence( layers: DailyStarlanguageEngineLayers, today: string, birthTimeVerified: boolean, ): DailyStarlanguageEvidence { const chart = chartModule(layers.chart); const vimshottari = vimshottariLayer(layers.vimshottari); const divisional = divisionalLayer(layers.varga); const transit = transitLayer(layers.transit); const narayana = narayanaSign(layers.narayana, today); const missingLayers = [ vimshottari ? "" : "Vimshottari", narayana ? "" : "Narayana", divisional.length ? "" : "分盘", transit ? "" : "过境", ].filter((layer) => layer.length > 0); return { today, birthTimeVerified, ascendantSign: signOf(asRecord(chart)?.ascendant), moonSign: planetSign(chart, "Moon"), transitMoonSign: null, transitMoonHouse: null, vimshottari, narayanaSign: narayana, divisional, transit, functionalBenefics: functionalPlanets(layers.chart, "functional_benefics"), functionalMalefics: functionalPlanets(layers.chart, "functional_malefics"), missingLayers, }; } export function evidenceFromDailyGuidance( packet: Record, today: string, birthTimeVerified: boolean, ): DailyStarlanguageEvidence | null { if (packet.success !== true) return null; const moonHouse = asCount(packet.moon_house); const transitMoonHouse = moonHouse !== null && moonHouse >= 1 && moonHouse <= 12 ? moonHouse : null; const mahadasha = asText(packet.mahadasha_lord); const transitMoonSign = asText(packet.moon_sign); if (!transitMoonHouse && !mahadasha && !transitMoonSign) return null; return { today: asText(packet.date) ?? today, birthTimeVerified, ascendantSign: asText(packet.ascendant_sign), moonSign: asText(packet.natal_moon_sign), transitMoonSign, transitMoonHouse, vimshottari: mahadasha ? { mahadasha, antardasha: null, remainingDays: null, nakshatra: null } : null, narayanaSign: null, divisional: [], transit: transitMoonSign ? { totalTriggers: transitMoonHouse ? 1 : 0, top: [`Moon → ${transitMoonSign}`] } : null, functionalBenefics: [], functionalMalefics: [], missingLayers: [ mahadasha ? "" : "Vimshottari", "Narayana", "分盘", ].filter((layer) => layer.length > 0), }; } export function dailyStarlanguagePrompt(evidence: DailyStarlanguageEvidence): string { return [ `今天的日期:${evidence.today}`, evidence.birthTimeVerified ? "出生时间状态:已确认。" : "出生时间状态:未经校正确认,禁止任何依赖分钟精度的判断。", JSON.stringify(evidence), "只依据上面这份证据写今天的星语。缺失的层不要假装存在,也不要提到证据缺失本身。", ].join("\n"); } export function parseDailyStarlanguageText(text: string): DailyStarlanguageCard | null { const normalized = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""); const start = normalized.indexOf("{"); const end = normalized.lastIndexOf("}"); if (start < 0 || end <= start) return null; let payload: unknown; try { payload = JSON.parse(normalized.slice(start, end + 1)); } catch { return null; } const parsed = cardSchema.safeParse(payload); return parsed.success ? parsed.data : null; } export function dailyStarlanguageCacheKey(userId: string, profileFingerprint: string, today: string): string { return JSON.stringify([userId, profileFingerprint, today]); } export function dailyStarlanguageProfileKey(profile: { readonly date?: string; readonly time?: string; readonly timezoneId?: string; readonly latitude?: number | null; readonly longitude?: number | null; readonly timezoneOffset?: number | null; readonly birthTimeStatus?: string; }): string { return JSON.stringify([ profile.date ?? "", profile.time ?? "", profile.timezoneId ?? "", profile.latitude ?? null, profile.longitude ?? null, profile.timezoneOffset ?? null, profile.birthTimeStatus ?? "", ]); } export function calendarDateInTimeZone(now: Date, timeZone?: string | null): string { const utc = now.toISOString().slice(0, 10); const zone = timeZone?.trim(); if (!zone) return utc; try { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(now); const year = parts.find((part) => part.type === "year")?.value; const month = parts.find((part) => part.type === "month")?.value; const day = parts.find((part) => part.type === "day")?.value; return year && month && day ? `${year}-${month}-${day}` : utc; } catch { return utc; } } const SIGNS = [ "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", ] as const; const PLANET_ZH: Record = { Sun: "太阳", Moon: "月亮", Mars: "火星", Mercury: "水星", Jupiter: "木星", Venus: "金星", Saturn: "土星", Rahu: "罗睺", Ketu: "计都", }; const HOUSE_THEMES: Record = { 1: { theme: "自我", action: "整理状态,把节奏重新起起来。" }, 2: { theme: "财务", action: "核对一笔开支或定价,给资源设上限。" }, 3: { theme: "沟通", action: "把一条卡住的消息写成可以直接发出去的版本。" }, 4: { theme: "家庭", action: "处理一件家宅事务,或把一个角落收拾完。" }, 5: { theme: "表达", action: "完成一小段创作或一次轻松的联系。" }, 6: { theme: "执行", action: "从清单里勾掉一件能今天做完的事。" }, 7: { theme: "合作", action: "主动联系一个人,把合作里未说清的一句补上。" }, 8: { theme: "复盘", action: "清理一个旧问题,只推进到可复查的一步。" }, 9: { theme: "学习", action: "记下一个观点,或把远程联络发出去。" }, 10: { theme: "事业", action: "把一件成果推进一步,让别人看得见。" }, 11: { theme: "人脉", action: "在一个群或一次交流里,完成一次具体的交换。" }, 12: { theme: "休整", action: "先收尾一件未完成的事,再给自己留一段安静。" }, }; function planetZh(name: string | null): string | null { return name ? PLANET_ZH[name] ?? null : null; } function moonHouse(evidence: DailyStarlanguageEvidence): number | null { if (!evidence.birthTimeVerified) return null; if (evidence.transitMoonHouse) return evidence.transitMoonHouse; const asc = evidence.ascendantSign; const moon = evidence.transitMoonSign ?? evidence.moonSign; if (!asc || !moon) return null; const ascIndex = SIGNS.indexOf(asc as typeof SIGNS[number]); const moonIndex = SIGNS.indexOf(moon as typeof SIGNS[number]); if (ascIndex < 0 || moonIndex < 0) return null; return (moonIndex - ascIndex + 12) % 12 + 1; } export function composeDailyStarlanguageCard(evidence: DailyStarlanguageEvidence): DailyStarlanguageCard { const house = moonHouse(evidence); const theme = house ? HOUSE_THEMES[house] : undefined; const mahadasha = planetZh(evidence.vimshottari?.mahadasha ?? null); const hasTransitWindow = Boolean(evidence.transitMoonSign || (evidence.transit?.totalTriggers ?? 0) > 0); const trend = theme && mahadasha ? `今天${theme.theme}的节奏更明显,${mahadasha}大运里适合先把一件小事做完。` : theme ? `今天${theme.theme}的节奏更明显,适合把计划压到一件能完成的事。` : mahadasha ? `今天按${mahadasha}大运的节奏推进,先把一件主事做小、做完。` : hasTransitWindow ? "今天月亮换了观察窗口,先按你当前节奏推进一件主事。" : "今天按你本命的节奏稳步推进,把计划压到一件主事。"; const action = theme?.action ?? "把今天要做的事收成一件,做完再看下一件。"; const caution = evidence.birthTimeVerified ? (hasTransitWindow ? "这是今天的观察窗口,先别把它当成今天就会发生的结果。" : "大运描述的是阶段质地,不是今天的结果。") : "出生时间尚未确认,这里只看大方向,不依赖具体分钟。"; return { trend: normalizeLine(trend), action: normalizeLine(action), caution: normalizeLine(caution), }; } export const dailyStarlanguageCardSchema = z.object({ trend: z.string().min(8).max(90), action: z.string().min(4).max(90), caution: z.string().min(4).max(90), });