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 = { 子: 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 = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木', } const ELEMENT_CONTROLS: Record = { 木: '土', 火: '金', 土: '水', 金: '木', 水: '火', } const SLOT_LABELS: Record = { year: '年宫', month: '月宫', day: '日宫', time: '时宫', } const SLOT_SOURCES: Record = { year: '农历年支', month: '农历月建', day: '万年历日支', time: '时辰地支', } const SLOT_EXPLANATIONS: Record = { 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 palaces: PalaceResult[] relations: PalaceRelation[] finalPalace: PalaceResult localInterpretation: LocalInterpretation } interface DerivedContext { mode: DivinationMode modeLabel: string solarLabel: string lunarLabel: string methodNote: string branches: Record } 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 { 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 = { 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 = { 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): 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 '亥' }