Files
xly/web/src/lib/divination.ts

709 lines
21 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.

import { Solar } from 'lunar-typescript'
const PALACE_ORDER = ['大安', '留连', '速喜', '赤口', '小吉', '空亡'] as const
const BRANCH_ORDER = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'] as const
const LUNAR_MONTH_BRANCHES = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'] as const
const SLOT_ORDER = ['year', 'month', 'day', 'time'] as const
const COMPASS_SLOT_VALUES = ['first', 'second', 'third'] as const
const BRANCH_VALUES: Record<BranchName, number> = {
: 1,
: 2,
: 3,
: 4,
: 5,
: 6,
: 7,
: 8,
: 9,
: 10,
: 11,
: 12,
}
const PALACE_META = {
: {
element: '木',
quality: '吉',
theme: '稳住格局,利于正面推进。',
detail: '适合谈条件、做决策、走正式流程。',
weight: 2,
},
: {
element: '土',
quality: '中',
theme: '反复拖延,过程比结果更磨人。',
detail: '适合补材料、等批复、拉齐分歧,不宜急催。',
weight: 0,
},
: {
element: '火',
quality: '吉',
theme: '消息来得快,适合主动推进。',
detail: '利沟通、利反馈、利敲定窗口期。',
weight: 3,
},
: {
element: '金',
quality: '凶',
theme: '口舌争执,信息容易带刺。',
detail: '要管住措辞,避免硬碰硬。',
weight: -2,
},
: {
element: '水',
quality: '吉',
theme: '缓步有利,贵在借力。',
detail: '利协商、利求人、利柔性处理。',
weight: 2,
},
: {
element: '土',
quality: '凶',
theme: '预期容易落空,先核实条件。',
detail: '适合先确认资源和边界,再决定要不要投入。',
weight: -3,
},
} as const
const ELEMENT_GENERATES: Record<ElementName, ElementName> = {
: '火',
: '土',
: '金',
: '水',
: '木',
}
const ELEMENT_CONTROLS: Record<ElementName, ElementName> = {
: '土',
: '金',
: '水',
: '木',
: '火',
}
const SLOT_LABELS: Record<BranchSlot, string> = {
year: '年宫',
month: '月宫',
day: '日宫',
time: '时宫',
}
const SLOT_SOURCES: Record<BranchSlot, string> = {
year: '农历年支',
month: '农历月建',
day: '万年历日支',
time: '时辰地支',
}
const SLOT_EXPLANATIONS: Record<BranchSlot, string> = {
year: '对应大环境、外部周期、行业与权力方态度。',
month: '对应当月氛围、资源松紧、团队与预算状态。',
day: '对应当天执行面、你的状态、沟通与临场表现。',
time: '对应结果落点,成败判断以此为主。',
}
const DOMAIN_KEYWORDS = {
career: ['工作', '加薪', '升职', 'offer', '面试', '跳槽', '裁员', '项目', '合作', '老板'],
wealth: ['财运', '投资', '赚钱', '回款', '签单', '收入', '生意', '客户'],
relation: ['感情', '恋爱', '复合', '结婚', '相亲', '关系', '伴侣'],
study: ['考试', '学习', '申请', '留学', '证书', '答辩'],
} as const
export type PalaceName = (typeof PALACE_ORDER)[number]
export type BranchName = (typeof BRANCH_ORDER)[number]
export type BranchSlot = (typeof SLOT_ORDER)[number]
export type ElementName = '木' | '火' | '土' | '金' | '水'
export type DivinationMode = 'time' | 'compass'
export interface CompassNumbers {
first: number
second: number
third: number
}
export interface ManualBranchOverrides {
year?: BranchName
month?: BranchName
day?: BranchName
time?: BranchName
}
export interface BranchInfo {
slot: BranchSlot
label: string
branch: string
value: number
source: string
sourceNote: string
ganZhi: string
}
export interface PalaceResult {
slot: BranchSlot
label: string
branch: string
branchValue: number
palace: PalaceName
element: ElementName
quality: '吉' | '中' | '凶'
theme: string
detail: string
slotExplanation: string
}
export interface PalaceRelation {
from: BranchSlot
to: BranchSlot
kind: '前生后' | '前克后' | '后生前' | '后克前' | '比和'
score: number
summary: string
}
export interface LocalInterpretation {
verdict: string
score: number
summary: string
domainLabel: string
chainSummary: string
actionAdvice: string[]
riskAdvice: string[]
}
export interface DivinationResult {
mode: DivinationMode
modeLabel: string
question: string
datetime: string
solarLabel: string
lunarLabel: string
methodNote: string
branches: Record<BranchSlot, BranchInfo>
palaces: PalaceResult[]
relations: PalaceRelation[]
finalPalace: PalaceResult
localInterpretation: LocalInterpretation
}
interface DerivedContext {
mode: DivinationMode
modeLabel: string
solarLabel: string
lunarLabel: string
methodNote: string
branches: Record<BranchSlot, BranchInfo>
}
export const branchOptions = [...BRANCH_ORDER]
export const compassNumberOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const
export const defaultCompassNumbers: CompassNumbers = {
first: 3,
second: 6,
third: 9,
}
export function buildDivination(input: {
datetime: string
question: string
mode?: DivinationMode
overrides?: ManualBranchOverrides
compassNumbers?: CompassNumbers
}): DivinationResult {
const trimmedQuestion = input.question.trim() || '这件事现在推进,结果会怎样?'
const context = deriveContext({
mode: input.mode ?? 'time',
datetime: input.datetime,
overrides: input.overrides,
compassNumbers: input.compassNumbers,
})
const palaces = buildPalaces(context.branches)
const relations = buildRelations(palaces)
const finalPalace = palaces[palaces.length - 1]
const localInterpretation = buildLocalInterpretation(trimmedQuestion, palaces, relations)
return {
mode: context.mode,
modeLabel: context.modeLabel,
question: trimmedQuestion,
datetime: input.datetime,
solarLabel: context.solarLabel,
lunarLabel: context.lunarLabel,
methodNote: context.methodNote,
branches: context.branches,
palaces,
relations,
finalPalace,
localInterpretation,
}
}
export function deriveBranchDefaults(datetime: string): Record<BranchSlot, BranchName> {
const branches = deriveTimeContext(datetime).branches
return {
year: branches.year.branch as BranchName,
month: branches.month.branch as BranchName,
day: branches.day.branch as BranchName,
time: branches.time.branch as BranchName,
}
}
export function getDefaultDatetimeValue(date = new Date()): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hour}:${minute}`
}
function deriveContext(input: {
mode: DivinationMode
datetime: string
overrides?: ManualBranchOverrides
compassNumbers?: CompassNumbers
}): DerivedContext {
if (input.mode === 'compass') {
return deriveCompassContext(input.datetime, input.compassNumbers)
}
return deriveTimeContext(input.datetime, input.overrides)
}
function deriveTimeContext(datetime: string, overrides?: ManualBranchOverrides): DerivedContext {
const date = parseLocalDatetime(datetime)
const solar = Solar.fromDate(date)
const lunar = solar.getLunar()
const lunarMonth = Math.abs(lunar.getMonth())
const yearGanZhi = lunar.getYearInGanZhi()
const monthGanZhi = lunar.getMonthInGanZhi()
const dayGanZhi = lunar.getDayInGanZhi()
const timeGanZhi = lunar.getTimeInGanZhi()
const autoBranches: Record<BranchSlot, BranchName> = {
year: lunar.getYearZhi() as BranchName,
month: LUNAR_MONTH_BRANCHES[(Math.max(1, Math.min(12, lunarMonth)) - 1) as number],
day: lunar.getDayZhi() as BranchName,
time: getTimeBranch(date.getHours()),
}
const resolvedBranches: Record<BranchSlot, BranchName> = {
year: overrides?.year ?? autoBranches.year,
month: overrides?.month ?? autoBranches.month,
day: overrides?.day ?? autoBranches.day,
time: overrides?.time ?? autoBranches.time,
}
return {
mode: 'time',
modeLabel: '时刻起课',
solarLabel: formatSolarLabel(date),
lunarLabel: `农历${lunar.getYearInChinese()}${lunar.getMonthInChinese()}${lunar.getDayInChinese()}`,
methodNote:
'默认按农历年支、农历月建、万年历日支、时辰地支起四宫;若涉及晚子时、闰月或流派差异,可手动覆写。',
branches: {
year: {
slot: 'year',
label: SLOT_LABELS.year,
branch: resolvedBranches.year,
value: BRANCH_VALUES[resolvedBranches.year],
source: SLOT_SOURCES.year,
sourceNote: `自动换算为 ${yearGanZhi},本法取年支 ${resolvedBranches.year}`,
ganZhi: yearGanZhi,
},
month: {
slot: 'month',
label: SLOT_LABELS.month,
branch: resolvedBranches.month,
value: BRANCH_VALUES[resolvedBranches.month],
source: SLOT_SOURCES.month,
sourceNote: `当前为农历${lunar.getMonthInChinese()}月,月建取支 ${resolvedBranches.month};干支参考 ${monthGanZhi}`,
ganZhi: monthGanZhi,
},
day: {
slot: 'day',
label: SLOT_LABELS.day,
branch: resolvedBranches.day,
value: BRANCH_VALUES[resolvedBranches.day],
source: SLOT_SOURCES.day,
sourceNote: `万年历日柱参考 ${dayGanZhi},本法取日支 ${resolvedBranches.day}`,
ganZhi: dayGanZhi,
},
time: {
slot: 'time',
label: SLOT_LABELS.time,
branch: resolvedBranches.time,
value: BRANCH_VALUES[resolvedBranches.time],
source: SLOT_SOURCES.time,
sourceNote: `当前时柱参考 ${timeGanZhi},本法取时支 ${resolvedBranches.time}`,
ganZhi: timeGanZhi,
},
},
}
}
function deriveCompassContext(datetime: string, numbers?: CompassNumbers): DerivedContext {
const date = parseLocalDatetime(datetime)
const solar = Solar.fromDate(date)
const lunar = solar.getLunar()
const resolvedNumbers = normalizeCompassNumbers(numbers)
const total = resolvedNumbers.first + resolvedNumbers.second + resolvedNumbers.third
return {
mode: 'compass',
modeLabel: '罗盘三数起课',
solarLabel: formatSolarLabel(date),
lunarLabel: `农历${lunar.getYearInChinese()}${lunar.getMonthInChinese()}${lunar.getDayInChinese()}`,
methodNote:
'按罗盘三数法起课:第一数定外势,第二数看过程,第三数落执行,三数合参定终局。三数都取 1 到 9结果宫按合参之数顺推。',
branches: {
year: {
slot: 'year',
label: SLOT_LABELS.year,
branch: `${resolvedNumbers.first}`,
value: resolvedNumbers.first,
source: '罗盘第一数',
sourceNote: `第一数取 ${resolvedNumbers.first},从大安起数,先定年宫。`,
ganZhi: '-',
},
month: {
slot: 'month',
label: SLOT_LABELS.month,
branch: `${resolvedNumbers.second}`,
value: resolvedNumbers.second,
source: '罗盘第二数',
sourceNote: `第二数取 ${resolvedNumbers.second},从年宫顺推,落月宫。`,
ganZhi: '-',
},
day: {
slot: 'day',
label: SLOT_LABELS.day,
branch: `${resolvedNumbers.third}`,
value: resolvedNumbers.third,
source: '罗盘第三数',
sourceNote: `第三数取 ${resolvedNumbers.third},从月宫顺推,落日宫。`,
ganZhi: '-',
},
time: {
slot: 'time',
label: SLOT_LABELS.time,
branch: `${total}`,
value: total,
source: '三数合参',
sourceNote: `三数相加为 ${total},从日宫顺推,定时宫终局。`,
ganZhi: '-',
},
},
}
}
function buildPalaces(branches: Record<BranchSlot, BranchInfo>): PalaceResult[] {
let currentIndex = 0
return SLOT_ORDER.map((slot) => {
const branch = branches[slot]
currentIndex = walkPalace(currentIndex, branch.value)
const palace = PALACE_ORDER[currentIndex]
const meta = PALACE_META[palace]
return {
slot,
label: SLOT_LABELS[slot],
branch: branch.branch,
branchValue: branch.value,
palace,
element: meta.element,
quality: meta.quality,
theme: meta.theme,
detail: meta.detail,
slotExplanation: SLOT_EXPLANATIONS[slot],
}
})
}
function buildRelations(palaces: PalaceResult[]): PalaceRelation[] {
const relations: PalaceRelation[] = []
for (let index = 0; index < palaces.length - 1; index += 1) {
const current = palaces[index]
const next = palaces[index + 1]
const relation = classifyRelation(current.element, next.element)
relations.push({
from: current.slot,
to: next.slot,
kind: relation.kind,
score: relation.score,
summary: `${current.label}${current.palace}(${current.element})${relation.text}${next.label}${next.palace}(${next.element})`,
})
}
return relations
}
function buildLocalInterpretation(
question: string,
palaces: PalaceResult[],
relations: PalaceRelation[],
): LocalInterpretation {
const domainLabel = detectDomain(question)
const palaceScore = palaces.reduce((sum, item, index) => {
const weight = PALACE_META[item.palace].weight
return sum + (index === palaces.length - 1 ? weight * 2 : weight)
}, 0)
const relationScore = relations.reduce((sum, relation) => sum + relation.score, 0)
const score = palaceScore + relationScore
const finalPalace = palaces[palaces.length - 1]
const verdict = resolveVerdict(score, finalPalace.palace)
const chainSummary = relations.map((item) => item.summary).join('')
const summary = [
`终宫落在${finalPalace.palace},主调是“${PALACE_META[finalPalace.palace].theme}`,
`${finalPalace.label}主结果,说明这件事的落点偏向${describeOutcome(finalPalace.palace)}`,
chainSummary ? `四宫链路里,${chainSummary}` : '',
]
.filter(Boolean)
.join('')
return {
verdict,
score,
summary,
domainLabel,
chainSummary,
actionAdvice: buildActionAdvice(domainLabel, finalPalace, relations),
riskAdvice: buildRiskAdvice(finalPalace, relations),
}
}
function buildActionAdvice(
domainLabel: string,
finalPalace: PalaceResult,
relations: PalaceRelation[],
): string[] {
const advice = [
'先把所问之事收束成一个判断点,例如现在推进、暂缓观望,还是先补条件。',
'先看年宫与月宫给不给势,再决定当下动作,不要只盯终宫一句吉凶。',
]
if (finalPalace.palace === '速喜') {
advice.unshift('眼下宜先动一步,把时间、对象和诉求一次定清,不要把顺势拖成失势。')
} else if (finalPalace.palace === '大安') {
advice.unshift('此局宜走正路,把条件、边界和预期回报摆清,再稳稳推进。')
} else if (finalPalace.palace === '小吉') {
advice.unshift('此时宜先借力,让熟人、旧成果或中间人替你开路,不必硬闯。')
} else if (finalPalace.palace === '留连') {
advice.unshift('此局贵在补缺,把顾虑逐条拆开,比一味催结果更有效。')
} else if (finalPalace.palace === '赤口') {
advice.unshift('此时先收锋芒再出手,话要留余地,事要留凭据。')
} else {
advice.unshift('此时先验信息真假与承诺落点,未坐实之前,不宜下重注。')
}
if (domainLabel === '事业/合作') {
advice.push('涉及事业或合作时,诉求要落成岗位、预算、周期或资源清单,别只谈感受。')
} else if (domainLabel === '财务/生意') {
advice.push('涉及财务或生意时,所有判断都要落到现金流、付款条件和违约边界。')
} else if (domainLabel === '感情/关系') {
advice.push('涉及关系时,先辨对方真实态度,再决定进退,不要拿想象替代反馈。')
} else if (domainLabel === '考试/申请') {
advice.push('涉及考试或申请时,先守时间规划与材料完整,再谈额外发挥。')
}
if (relations.some((item) => item.kind === '前生后')) {
advice.push('四宫若见顺生,说明次第不可乱,按“先看环境,再落动作,后取结果”会更顺。')
}
return advice.slice(0, 4)
}
function buildRiskAdvice(finalPalace: PalaceResult, relations: PalaceRelation[]): string[] {
const advice: string[] = []
if (relations.some((item) => item.kind === '前克后' || item.kind === '后克前')) {
advice.push('四宫存在克制,推进中容易出现抵触、卡口或资源冲突。')
}
if (finalPalace.palace === '赤口') {
advice.push('结果宫带赤口,最大的风险不是没有机会,而是把机会说坏。')
}
if (finalPalace.palace === '空亡') {
advice.push('结果宫带空亡,最大的风险是信息不实、承诺虚高或条件临时失效。')
}
if (finalPalace.palace === '留连') {
advice.push('结果宫带留连,最大的风险是一直等、一直猜,迟迟不做下一步动作。')
}
if (advice.length === 0) {
advice.push('整体链路不算凶,但仍要把时间点、承诺边界和证据留存好。')
}
return advice.slice(0, 3)
}
function walkPalace(startIndex: number, steps: number): number {
return (startIndex + steps - 1) % PALACE_ORDER.length
}
function classifyRelation(from: ElementName, to: ElementName): {
kind: PalaceRelation['kind']
score: number
text: string
} {
if (from === to) {
return { kind: '比和', score: 0, text: '与' }
}
if (ELEMENT_GENERATES[from] === to) {
return { kind: '前生后', score: 1, text: '生' }
}
if (ELEMENT_CONTROLS[from] === to) {
return { kind: '前克后', score: -1, text: '克' }
}
if (ELEMENT_GENERATES[to] === from) {
return { kind: '后生前', score: 0, text: '受' }
}
return { kind: '后克前', score: -1, text: '受' }
}
function resolveVerdict(score: number, finalPalace: PalaceName): string {
if (score >= 7) {
return '上吉,可主动推进。'
}
if (score >= 3) {
return '偏吉,成事面大于阻力。'
}
if (score >= 0) {
return '中平,能不能成取决于执行方式。'
}
if (finalPalace === '空亡' || finalPalace === '赤口') {
return '偏凶,先控风险再行动。'
}
return '有阻,宜放慢节奏并补条件。'
}
function describeOutcome(palace: PalaceName): string {
switch (palace) {
case '大安':
return '稳中见成,适合按规矩落地'
case '留连':
return '拖中见变,需要耐心和补件'
case '速喜':
return '快中见成,重在主动出击'
case '赤口':
return '先有口舌,再看能否化解'
case '小吉':
return '缓中有利,重在人和与借力'
case '空亡':
return '容易落空,必须先验真伪'
}
}
function detectDomain(question: string): string {
for (const [key, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
if (keywords.some((word) => question.includes(word))) {
switch (key) {
case 'career':
return '事业/合作'
case 'wealth':
return '财务/生意'
case 'relation':
return '感情/关系'
case 'study':
return '考试/申请'
default:
return '综合事项'
}
}
}
return '综合事项'
}
function normalizeCompassNumbers(numbers?: CompassNumbers): CompassNumbers {
const fallback = defaultCompassNumbers
const values = {
first: numbers?.first ?? fallback.first,
second: numbers?.second ?? fallback.second,
third: numbers?.third ?? fallback.third,
}
for (const key of COMPASS_SLOT_VALUES) {
values[key] = clampCompassNumber(values[key])
}
return values
}
function clampCompassNumber(value: number): number {
if (!Number.isFinite(value)) {
return 1
}
return Math.max(1, Math.min(9, Math.round(value)))
}
function parseLocalDatetime(value: string): Date {
const [datePart, timePart = '00:00'] = value.split('T')
const [year, month, day] = datePart.split('-').map((item) => Number(item))
const [hour, minute] = timePart.split(':').map((item) => Number(item))
if (![year, month, day, hour, minute].every(Number.isFinite)) {
return new Date()
}
return new Date(year, month - 1, day, hour, minute, 0, 0)
}
function formatSolarLabel(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}${month}${day}${hour}:${minute}`
}
function getTimeBranch(hour: number): BranchName {
if (hour >= 23 || hour < 1) {
return '子'
}
if (hour < 3) {
return '丑'
}
if (hour < 5) {
return '寅'
}
if (hour < 7) {
return '卯'
}
if (hour < 9) {
return '辰'
}
if (hour < 11) {
return '巳'
}
if (hour < 13) {
return '午'
}
if (hour < 15) {
return '未'
}
if (hour < 17) {
return '申'
}
if (hour < 19) {
return '酉'
}
if (hour < 21) {
return '戌'
}
return '亥'
}