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
208 lines
12 KiB
TypeScript
208 lines
12 KiB
TypeScript
/**
|
||
* 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);
|
||
}
|