Files
Jyotisha/frontend/src/lib/daily-starlanguage-library.ts
T
Jesse_ChenandClaude Opus 5.5 dc45fde613
Independent Staging Quality Gate / validate (push) Successful in 12m47s
Independent Staging Quality Gate / publish (push) Successful in 3m47s
feat(home): warm older-sister greeting voice; daily line says why, plainly
Late night no longer remarks on being awake (产品 2026-10-01); every daypart,
weekday and returning line reworded in the same caring voice. Daily line:
each house sentence names where the Moon is in plain words, then one concrete
sentence; no stacked hedges. Sign-fallback moods rewritten the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-01 19:34:16 +08:00

208 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Home 「今日星语」 from a fixed sentence library (BUG-1109, product 2026-09-30:
* 「这句好像都是固定的,可以不走大模型,咱们设计一套」). No model call.
*
* Keys: the transit Moon's house counted from the natal ascendant (1–12, only
* when the birth time is confirmed — the house depends on the ascendant) × the
* lunar phase of the day (4) × a variant chosen from the date, so two days in a
* row in the same house read differently. Without a usable house the line falls
* back to the Moon's sign; without that, to the older evidence composer.
*
* Copy rules live in docs/VOICE.md (今日星语): plain, concrete, no fate, no
* promises, no medical / legal / money advice, no 宫主 / 相位 in the trend line.
*/
import { composeDailyStarlanguageCard, type DailyStarlanguageCard, type DailyStarlanguageEvidence } from "./daily-starlanguage";
type Line2 = readonly [string, string];
type Line3 = readonly [string, string, string];
export type LunarPhase = "new" | "waxing" | "full" | "waning";
export const HOUSE_LINES: Readonly<Record<number, Line3>> = {
1: [
"今天月亮走到你盘上管自己的位置,身体和心情都更敏感,先把自己照顾好。",
"月亮今天回到管你自己的那一块,想做的事今天开头最顺手。",
"今天月亮落在管你自己的位置,情绪会直接写在脸上,累了就说累。",
],
2: [
"今天月亮走到你盘上管钱的位置,买东西、谈价钱前多算一遍,今天容易花多。",
"月亮今天在管钱和说话的那一块,想说的话先在心里过一遍,再开口。",
"今天月亮落在管收入的位置,适合把这个月的账理一理,心里会踏实很多。",
],
3: [
"今天月亮走到你盘上管沟通和跑腿的位置,拖着的消息和小事,今天最好办。",
"月亮今天在管联络的那一块,想找谁就主动找,别等对方先开口。",
"今天月亮落在管出门办事的位置,短途走一趟、见个人,都比待着有收获。",
],
4: [
"今天月亮走到你盘上管家的位置,心思会往家里跑,给家人打个电话会很安心。",
"月亮今天在管家和情绪的那一块,把住的地方收拾一下,人也会跟着静下来。",
"今天月亮落在管安全感的位置,别把日程排太满,给自己留个角落。",
],
5: [
"今天月亮走到你盘上管开心和恋爱的位置,做点纯粹喜欢的事,不用有用。",
"月亮今天在管表达的那一块,有个想法就写下来、画出来,今天手感好。",
"今天月亮落在管心动和孩子的位置,和喜欢的人待一会儿,心情会亮起来。",
],
6: [
"今天月亮走到你盘上管日常琐事的位置,按清单一件件来,反而最省力。",
"月亮今天在管身体小信号的那一块,哪里不舒服别硬扛,早点休息。",
"今天月亮落在管工作杂事的位置,事会多一点,先做完最烦的那件。",
],
7: [
"今天月亮走到你盘上管伴侣和合作的位置,有话想跟对方说,今天开口比憋着好。",
"月亮今天在管身边人的那一块,别人的态度会影响你,先听完再回应。",
"今天月亮落在管合作的位置,两个人商量着来,比一个人硬扛轻松。",
],
8: [
"今天月亮走到你盘上管心里压力的位置,想得会多一点,写下来比憋着好受。",
"月亮今天在管共同钱物的那一块,和别人有关的账,今天适合慢慢对清楚。",
"今天月亮落在管深处情绪的位置,适合面对一件一直不想碰的小事。",
],
9: [
"今天月亮走到你盘上管学习和远方的位置,读点东西、听一堂课,都会有收获。",
"月亮今天在管长远打算的那一块,适合想想半年后的自己,别只盯着眼前。",
"今天月亮落在管老师和长辈的位置,拿不准的事,去问问比你有经验的人。",
],
10: [
"今天月亮走到你盘上管工作的位置,你做的事更容易被看见,把成果拿出来。",
"月亮今天在管事业的那一块,先做最要紧的那件,别被杂事拉走。",
"今天月亮落在管对外形象的位置,说话做事稍微正式一点,会给你加分。",
],
11: [
"今天月亮走到你盘上管朋友的位置,和朋友聊一聊,会听到有用的消息。",
"月亮今天在管圈子和心愿的那一块,想要什么就说出来,会有人接住。",
"今天月亮落在管人脉的位置,联系一个很久没聊的人,正是时候。",
],
12: [
"今天月亮走到你盘上管休息的位置,精力偏低,重要的事放明天,今晚早点收工。",
"月亮今天在管独处的那一块,想一个人待着很正常,不用解释。",
"今天月亮落在管花销的位置,钱和精力都容易悄悄流走,留意一下去向。",
],
};
export const PHASE_TAILS: Readonly<Record<LunarPhase, Line2>> = {
new: ["这两天是新月,适合开个小头。", "这两天是新月,先想清楚再动手。"],
waxing: ["月亮正在变圆,往前推一步会顺。", "月亮正在变圆,做加法比做减法顺。"],
full: ["这两天是满月,情绪容易满,先缓一缓再回话。", "这两天是满月,适合把结果摆出来看看。"],
waning: ["月亮正在变缺,适合收尾和清理。", "月亮正在变缺,做减法比开新头顺。"],
};
export const HOUSE_ACTIONS: Readonly<Record<number, Line2>> = {
1: ["早点睡或出门走二十分钟,把状态先养好。", "给自己定一件今天一定做完的小事。"],
2: ["记下今天的每笔开销,或把一个价格重新核一遍。", "开口前先想好一句最想表达的话。"],
3: ["把拖了最久的那条消息今天发出去。", "列三件小事,逐一办掉。"],
4: ["收拾一个角落,或给家里人打个电话。", "晚上留半小时给自己,什么都不排。"],
5: ["花一小时做一件纯粹因为喜欢而做的事。", "把一个点子写下来或画出来。"],
6: ["从待办里挑一件今天能做完的,先做它。", "调一个作息:早睡半小时,或少喝一杯咖啡。"],
7: ["主动约对方,把一件没说清的事聊清楚。", "先问一句「你怎么看」,再说自己的想法。"],
8: ["把最担心的一件事写下来,再写下一步怎么做。", "整理一笔共同账目或一份旧文件。"],
9: ["读完一篇收藏很久的文章。", "给远方的人发一条问候。"],
10: ["把这周的一项进展整理成一段话,发给相关的人。", "先完成最要紧的一件,再回消息。"],
11: ["联系一个很久没聊的朋友。", "在群里分享一条对别人有用的信息。"],
12: ["给自己留一段不看手机的时间。", "清一清订阅和自动扣费。"],
};
export const HOUSE_CAUTIONS: Readonly<Record<number, Line2>> = {
1: ["别因为一时情绪就做决定。", "别把自己排得太满。"],
2: ["冲动消费和气话都容易在今天冒出来。", "别为了面子答应超出预算的事。"],
3: ["消息多,容易看漏细节,重要的事再确认一遍。", "别同时开太多头。"],
4: ["别把外面的烦心事带回家发泄。", "情绪低的时候别做大决定。"],
5: ["开心归开心,别在兴头上许下太大的承诺。", "别拿自己和别人比。"],
6: ["别让琐事占满整天,留出做正事的时间。", "身体有小不适别硬撑,该休息就休息。"],
7: ["别把对方的一句话想得太重。", "意见不合时,先别急着争对错。"],
8: ["别在焦虑的时候翻旧账。", "涉及钱物的约定,写清楚再定。"],
9: ["别急着说服别人接受你的看法。", "远途安排多留一点余量。"],
10: ["别为了表现把自己逼得太紧。", "对外说话前,再看一遍措辞。"],
11: ["人多的场合,别轻易答应帮忙。", "别被别人的节奏带着跑。"],
12: ["别用熬夜来换独处的时间。", "留意那些看不见的开销。"],
};
const SIGN_ZH: Readonly<Record<string, string>> = {
Aries: "白羊座", Taurus: "金牛座", Gemini: "双子座", Cancer: "巨蟹座", Leo: "狮子座", Virgo: "处女座",
Libra: "天秤座", Scorpio: "天蝎座", Sagittarius: "射手座", Capricorn: "摩羯座", Aquarius: "水瓶座", Pisces: "双鱼座",
};
/** Used when the house cannot be counted (birth time not confirmed, or no ascendant). */
export const SIGN_MOODS: Readonly<Record<string, string>> = {
Aries: "做事会急一点,想到就先做一小步,别一口气全揽下。",
Taurus: "人会想要安稳,把手上的事做扎实,比开新事舒服。",
Gemini: "消息和想法都多,适合聊天、学东西,重要的事记下来。",
Cancer: "心会软一点,照顾一下家人,也照顾一下自己。",
Leo: "想被看见很正常,把你做好的东西拿出来给人看。",
Virgo: "适合整理细节,列个清单一件件勾掉,心里会清爽。",
Libra: "会更在意别人怎么想,有分歧时先商量,不急着定。",
Scorpio: "感受会深一点,专心做一件要紧事,别分心太多处。",
Sagittarius: "心情会开阔,出门走走或学点新东西,都很合适。",
Capricorn: "人会务实一点,适合推进需要耐心的事,一步一步来就好。",
Aquarius: "想跳出常规,换个角度看老问题,会有新办法。",
Pisces: "更感性也更容易累,今天对自己宽松一点。",
};
const SIGN_ACTIONS: Line2 = ["把今天要做的事收成一件,做完再看下一件。", "给自己留一段安静的时间,想清楚再动。"];
const UNVERIFIED_CAUTION = "出生时间尚未确认,这里只看大方向,不依赖具体分钟。";
const SIGN_CAUTIONS: Line2 = ["这是今天的大致气氛,不是一定会发生的事。", "别让一时的情绪替你做决定。"];
const DAY_MS = 86_400_000;
/** A known new moon: 2000-01-06 18:14 UTC. */
const NEW_MOON_EPOCH_MS = Date.UTC(2000, 0, 6, 18, 14);
/** Mean synodic month in days. Mean phase, so accurate to about ±1 day — enough for four broad phases. */
const SYNODIC_DAYS = 29.530588853;
/** Days since 1970-01-01 for a `YYYY-MM-DD` date, or 0 if it does not parse. */
function dayNumber(today: string): number {
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(today);
if (!match) return 0;
return Math.floor(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])) / DAY_MS);
}
/** Moon age in days at local noon of `today`, from the mean synodic month. */
export function moonAgeDays(today: string): number {
const noon = dayNumber(today) * DAY_MS + DAY_MS / 2;
const age = ((noon - NEW_MOON_EPOCH_MS) / DAY_MS) % SYNODIC_DAYS;
return age < 0 ? age + SYNODIC_DAYS : age;
}
export function lunarPhase(today: string): LunarPhase {
const age = moonAgeDays(today);
if (age < 3.7 || age >= 25.8) return "new";
if (age < 11.1) return "waxing";
if (age < 18.5) return "full";
return "waning";
}
function houseOf(evidence: DailyStarlanguageEvidence): number | null {
if (!evidence.birthTimeVerified) return null;
const house = evidence.transitMoonHouse;
return house && house >= 1 && house <= 12 ? house : null;
}
/** Deterministic pick from the date; consecutive days never repeat a three-variant line. */
function pick<T>(lines: readonly T[], day: number, stride = 1): T {
return lines[Math.floor(day / stride) % lines.length]!;
}
export function composeDailyCard(evidence: DailyStarlanguageEvidence, today: string = evidence.today): DailyStarlanguageCard {
const day = dayNumber(today);
const tail = pick(PHASE_TAILS[lunarPhase(today)], day, 3);
const house = houseOf(evidence);
if (house) {
return {
trend: `${pick(HOUSE_LINES[house]!, day)}${tail}`,
action: pick(HOUSE_ACTIONS[house]!, day),
caution: pick(HOUSE_CAUTIONS[house]!, day),
};
}
const sign = evidence.transitMoonSign;
const mood = sign ? SIGN_MOODS[sign] : undefined;
if (sign && mood) {
return {
trend: `今天月亮在${SIGN_ZH[sign]},${mood}${tail}`,
action: pick(SIGN_ACTIONS, day),
caution: evidence.birthTimeVerified ? pick(SIGN_CAUTIONS, day) : UNVERIFIED_CAUTION,
};
}
return composeDailyStarlanguageCard(evidence);
}