The homepage was waiting on a full chart plus four extra engines, so the card timed out and showed the failure copy on every visit. Co-authored-by: Cursor <cursoragent@cursor.com>
364 lines
13 KiB
TypeScript
364 lines
13 KiB
TypeScript
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<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : 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<unknown>((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<string, unknown>,
|
|
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<string, string> = {
|
|
Sun: "太阳",
|
|
Moon: "月亮",
|
|
Mars: "火星",
|
|
Mercury: "水星",
|
|
Jupiter: "木星",
|
|
Venus: "金星",
|
|
Saturn: "土星",
|
|
Rahu: "罗睺",
|
|
Ketu: "计都",
|
|
};
|
|
|
|
const HOUSE_THEMES: Record<number, { readonly theme: string; readonly action: string }> = {
|
|
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),
|
|
});
|