Implement three divination modes, learn pages, and PM2 deploy on port 3130.

Add liuyao/bazi/combined flows with shared calc and AI infrastructure, 64-gua learn routes, and update Ubuntu PM2 deployment docs for port 3130.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 20:19:49 +08:00
parent d141623070
commit fff77dac3f
41 changed files with 2590 additions and 385 deletions
+176
View File
@@ -0,0 +1,176 @@
import { Solar } from "lunar-javascript";
import {
adjustToTrueSolarTime,
formatDateTime,
parseDateTime,
} from "@/lib/calc/time";
export interface BaziInput {
date: string;
time: string;
gender: "male" | "female";
longitude: number;
unknownHour?: boolean;
}
export interface PillarInfo {
ganZhi: string;
shiShenGan: string;
shiShenZhi: string[];
hideGan: string[];
naYin: string;
}
export interface DaYunInfo {
ganZhi: string;
startAge: number;
}
export interface BaziChart {
birthTime: string;
trueSolarTime: string;
unknownHour: boolean;
lunarDate: string;
pillars: {
year: PillarInfo;
month: PillarInfo;
day: PillarInfo;
time: PillarInfo;
};
daYun: {
startYear: number;
startMonth: number;
startDay: number;
items: DaYunInfo[];
};
liuNian: { year: number; ganZhi: string }[];
shenSha: {
ji: string[];
xiong: string[];
};
}
function buildPillar(
ganZhi: string,
shiShenGan: string,
shiShenZhi: string[],
hideGan: string[],
naYin: string,
): PillarInfo {
return { ganZhi, shiShenGan, shiShenZhi, hideGan, naYin };
}
export function calculateBazi(input: BaziInput): BaziChart {
const localTime = parseDateTime(input.date, input.time);
const trueSolar = adjustToTrueSolarTime(localTime, input.longitude);
const solar = Solar.fromYmdHms(
trueSolar.getFullYear(),
trueSolar.getMonth() + 1,
trueSolar.getDate(),
input.unknownHour ? 12 : trueSolar.getHours(),
input.unknownHour ? 0 : trueSolar.getMinutes(),
0,
);
const lunar = solar.getLunar();
const ec = lunar.getEightChar();
const genderCode = input.gender === "male" ? 1 : 0;
const yun = ec.getYun(genderCode);
const currentYear = new Date().getFullYear();
const liuNian: { year: number; ganZhi: string }[] = [];
for (let year = currentYear - 2; year <= currentYear + 3; year++) {
const yearSolar = Solar.fromYmdHms(year, 6, 1, 12, 0, 0);
liuNian.push({
year,
ganZhi: yearSolar.getLunar().getYearInGanZhi(),
});
}
const daYunList = yun.getDaYun();
const daYunItems: DaYunInfo[] = [];
let age = yun.getStartYear();
for (const dy of daYunList) {
const gz = dy.getGanZhi();
if (!gz) {
continue;
}
daYunItems.push({ ganZhi: gz, startAge: age });
age += 10;
if (daYunItems.length >= 8) {
break;
}
}
return {
birthTime: formatDateTime(localTime),
trueSolarTime: formatDateTime(trueSolar),
unknownHour: !!input.unknownHour,
lunarDate: lunar.toString(),
pillars: {
year: buildPillar(
ec.getYear(),
ec.getYearShiShenGan(),
ec.getYearShiShenZhi(),
ec.getYearHideGan(),
ec.getYearNaYin(),
),
month: buildPillar(
ec.getMonth(),
ec.getMonthShiShenGan(),
ec.getMonthShiShenZhi(),
ec.getMonthHideGan(),
ec.getMonthNaYin(),
),
day: buildPillar(
ec.getDay(),
ec.getDayShiShenGan(),
ec.getDayShiShenZhi(),
ec.getDayHideGan(),
ec.getDayNaYin(),
),
time: buildPillar(
input.unknownHour ? "不详" : ec.getTime(),
input.unknownHour ? "—" : ec.getTimeShiShenGan(),
input.unknownHour ? [] : ec.getTimeShiShenZhi(),
input.unknownHour ? [] : ec.getTimeHideGan(),
input.unknownHour ? "—" : ec.getTimeNaYin(),
),
},
daYun: {
startYear: yun.getStartYear(),
startMonth: yun.getStartMonth(),
startDay: yun.getStartDay(),
items: daYunItems,
},
liuNian,
shenSha: {
ji: lunar.getDayJiShen(),
xiong: lunar.getDayXiongSha(),
},
};
}
export function formatBaziForPrompt(chart: BaziChart): string {
const p = chart.pillars;
const lines = [
`出生时间:${chart.birthTime}`,
`真太阳时:${chart.trueSolarTime}${chart.unknownHour ? "(时辰不详,按中午排盘)" : ""}`,
`农历:${chart.lunarDate}`,
"",
"【四柱】",
`年柱:${p.year.ganZhi}(天干十神:${p.year.shiShenGan},地支十神:${p.year.shiShenZhi.join("、")},藏干:${p.year.hideGan.join("、")},纳音:${p.year.naYin}`,
`月柱:${p.month.ganZhi}(天干十神:${p.month.shiShenGan},地支十神:${p.month.shiShenZhi.join("、")},藏干:${p.month.hideGan.join("、")},纳音:${p.month.naYin}`,
`日柱:${p.day.ganZhi}(天干十神:${p.day.shiShenGan},地支十神:${p.day.shiShenZhi.join("、")},藏干:${p.day.hideGan.join("、")},纳音:${p.day.naYin}`,
`时柱:${p.time.ganZhi}(天干十神:${p.time.shiShenGan},地支十神:${p.time.shiShenZhi.join("、") || "—"},藏干:${p.time.hideGan.join("、") || "—"},纳音:${p.time.naYin}`,
"",
`【大运】起运 ${chart.daYun.startYear}${chart.daYun.startMonth}${chart.daYun.startDay}`,
chart.daYun.items.map((d) => `${d.startAge}岁起 ${d.ganZhi}`).join(" → "),
"",
`【流年】${chart.liuNian.map((l) => `${l.year}(${l.ganZhi})`).join("、")}`,
"",
`【神煞】吉神:${chart.shenSha.ji.join("、") || "无"};凶煞:${chart.shenSha.xiong.join("、") || "无"}`,
];
return lines.join("\n");
}
+77
View File
@@ -0,0 +1,77 @@
import guaIndexData from "@/lib/data/gua-index.json";
import guaListData from "@/lib/data/gua-list.json";
import type { HexagramObj } from "@/components/hexagram";
import type { ResultObj } from "@/components/result";
const GUA_DICT1 = ["坤", "震", "坎", "兑", "艮", "离", "巽", "乾"];
const GUA_DICT2 = ["地", "雷", "水", "泽", "山", "火", "风", "天"];
const CHANGE_YANG = ["初九", "九二", "九三", "九四", "九五", "上九"];
const CHANGE_YIN = ["初六", "六二", "六三", "六四", "六五", "上六"];
/** 三钱法:正面数 0~3 → 爻象 */
export function frontCountToLine(frontCount: number): Omit<HexagramObj, "separate"> {
return {
change: frontCount === 0 || frontCount === 3,
yang: frontCount >= 2,
};
}
export function buildHexagramList(frontCounts: number[]): HexagramObj[] {
return frontCounts.map((count, index) => ({
...frontCountToLine(count),
separate: index === 3,
}));
}
export function computeGuaResult(list: HexagramObj[]): ResultObj | null {
if (list.length !== 6) {
return null;
}
const changeList: string[] = [];
list.forEach((value, index) => {
if (!value.change) {
return;
}
changeList.push(value.yang ? CHANGE_YANG[index] : CHANGE_YIN[index]);
});
const upIndex =
(list[5].yang ? 4 : 0) + (list[4].yang ? 2 : 0) + (list[3].yang ? 1 : 0);
const downIndex =
(list[2].yang ? 4 : 0) + (list[1].yang ? 2 : 0) + (list[0].yang ? 1 : 0);
const guaIndex = guaIndexData[upIndex][downIndex] - 1;
const guaName1 = guaListData[guaIndex];
let guaName2: string;
if (upIndex === downIndex) {
guaName2 = GUA_DICT1[upIndex] + "为" + GUA_DICT2[upIndex];
} else {
guaName2 = GUA_DICT2[upIndex] + GUA_DICT2[downIndex] + guaName1;
}
const guaDesc = GUA_DICT1[upIndex] + "上" + GUA_DICT1[downIndex] + "下";
return {
guaMark: `${(guaIndex + 1).toString().padStart(2, "0")}.${guaName2}`,
guaTitle: `周易第${guaIndex + 1}`,
guaResult: `${guaName1}卦(${guaName2})_${guaDesc}`,
guaChange:
changeList.length === 0 ? "无变爻" : `变爻: ${changeList.toString()}`,
};
}
export const YAO_LABELS = ["初爻", "二爻", "三爻", "四爻", "五爻", "上爻"];
export const COIN_OPTIONS = [
{ count: 0, label: "老阴", desc: "0 正 · 变爻 ✕" },
{ count: 1, label: "少阴", desc: "1 正" },
{ count: 2, label: "少阳", desc: "2 正" },
{ count: 3, label: "老阳", desc: "3 正 · 变爻 ○" },
];
export interface GuaResult {
list: HexagramObj[];
result: ResultObj;
}
+29
View File
@@ -0,0 +1,29 @@
/** 中国标准时间基准经度(UTC+8) */
export const CHINA_STANDARD_MERIDIAN = 120;
/**
* 根据出生地经度校正真太阳时。
* 每差 1 度经度,时间差约 4 分钟。
*/
export function adjustToTrueSolarTime(
date: Date,
longitude: number,
standardMeridian = CHINA_STANDARD_MERIDIAN,
): Date {
const offsetMinutes = (longitude - standardMeridian) * 4;
return new Date(date.getTime() + offsetMinutes * 60 * 1000);
}
export function formatDateTime(date: Date): string {
const pad = (n: number) => n.toString().padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
export function parseDateTime(
dateStr: string,
timeStr: string,
): Date {
const [year, month, day] = dateStr.split("-").map(Number);
const [hour, minute] = timeStr.split(":").map(Number);
return new Date(year, month - 1, day, hour, minute, 0);
}
+98
View File
@@ -0,0 +1,98 @@
import { Solar } from "lunar-javascript";
import {
adjustToTrueSolarTime,
formatDateTime,
parseDateTime,
} from "@/lib/calc/time";
export interface TimingInfo {
solarTime: string;
lunarDate: string;
yearGanZhi: string;
monthGanZhi: string;
dayGanZhi: string;
timeGanZhi: string;
prevJieQi: string;
nextJieQi: string;
}
export function getTimingInfo(dateTime: Date): TimingInfo {
const solar = Solar.fromYmdHms(
dateTime.getFullYear(),
dateTime.getMonth() + 1,
dateTime.getDate(),
dateTime.getHours(),
dateTime.getMinutes(),
0,
);
const lunar = solar.getLunar();
return {
solarTime: formatDateTime(dateTime),
lunarDate: lunar.toString(),
yearGanZhi: lunar.getYearInGanZhi(),
monthGanZhi: lunar.getMonthInGanZhi(),
dayGanZhi: lunar.getDayInGanZhi(),
timeGanZhi: lunar.getTimeInGanZhi(),
prevJieQi: lunar.getPrevJieQi()?.getName() ?? "—",
nextJieQi: lunar.getNextJieQi()?.getName() ?? "—",
};
}
export function getTimingInfoFromStrings(
date: string,
time: string,
): TimingInfo {
return getTimingInfo(parseDateTime(date, time));
}
export function getTimingInfoWithLongitude(
date: string,
time: string,
longitude: number,
): { timing: TimingInfo; trueSolarTime: string } {
const local = parseDateTime(date, time);
const trueSolar = adjustToTrueSolarTime(local, longitude);
return {
timing: getTimingInfo(trueSolar),
trueSolarTime: formatDateTime(trueSolar),
};
}
export function formatLiuyaoTimingForPrompt(
timing: TimingInfo,
trueSolarTime: string,
locationName: string,
longitude: number,
): string {
return [
"【起卦时空 · 天时】",
`起卦时刻:${timing.solarTime}`,
`真太阳时:${trueSolarTime}`,
`农历:${timing.lunarDate}`,
`年柱:${timing.yearGanZhi},月柱:${timing.monthGanZhi},日柱:${timing.dayGanZhi},时柱:${timing.timeGanZhi}`,
`节气:上一节气 ${timing.prevJieQi},下一节气 ${timing.nextJieQi}`,
"",
"【起卦地域 · 地利】",
`位置:${locationName}`,
`经度:${longitude}°`,
].join("\n");
}
export function formatTimingForPrompt(
timing: TimingInfo,
locationName: string,
longitude: number,
): string {
return [
"【天时】",
`测算时刻:${timing.solarTime}`,
`农历:${timing.lunarDate}`,
`年柱:${timing.yearGanZhi},月柱:${timing.monthGanZhi},日柱:${timing.dayGanZhi},时柱:${timing.timeGanZhi}`,
`节气:上一节气 ${timing.prevJieQi},下一节气 ${timing.nextJieQi}`,
"",
"【地利】",
`当前位置:${locationName}`,
`经度:${longitude}°`,
].join("\n");
}