feat(home): write the daily card with the Agent and vary the starter heading
The daily starlanguage card claimed to be personal but was a four-card rotation picked by hashing the date and birth place, with the same pool duplicated as a client fallback. It now collects chart, Vimshottari and Narayana dasha, D9/D10 and today's transits, hands that evidence to a dedicated Agent, and keeps the result per account per day in process. A failed generation says so instead of printing generic advice. The starter heading is drawn from a pool on each visit, and the rectification card drops its fine print. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
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 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)),
|
||||
action: z.string().transform(normalizeLine).pipe(z.string().min(4)),
|
||||
caution: z.string().transform(normalizeLine).pipe(z.string().min(4)),
|
||||
});
|
||||
|
||||
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"),
|
||||
vimshottari,
|
||||
narayanaSign: narayana,
|
||||
divisional,
|
||||
transit,
|
||||
functionalBenefics: functionalPlanets(layers.chart, "functional_benefics"),
|
||||
functionalMalefics: functionalPlanets(layers.chart, "functional_malefics"),
|
||||
missingLayers,
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const starterPrompts = [
|
||||
"今天想先理清什么?",
|
||||
"现在最想弄明白哪件事?",
|
||||
"今天想从哪里说起?",
|
||||
"此刻最挂心的是什么?",
|
||||
"最近有什么反复出现的问题?",
|
||||
"眼下最想要一个答案的是什么?",
|
||||
"想先把哪件事想清楚?",
|
||||
"有什么事正等着你决定?",
|
||||
] as const;
|
||||
|
||||
export const starterPromptVariants: readonly string[] = starterPrompts;
|
||||
|
||||
export function createStarterPrompt(selection = Math.random()): string {
|
||||
const index = Math.floor(selection * starterPrompts.length);
|
||||
return starterPrompts[Math.min(Math.max(index, 0), starterPrompts.length - 1)];
|
||||
}
|
||||
Reference in New Issue
Block a user