feat: add fullstack deployment and oracle app

This commit is contained in:
2026-04-28 13:35:53 +08:00
commit 57fbcf16d4
42 changed files with 7566 additions and 0 deletions

708
web/src/lib/divination.ts Normal file
View File

@@ -0,0 +1,708 @@
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 '亥'
}