From fff77dac3fde95988360138df54a686caf790374 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 10 Jun 2026 20:19:49 +0800 Subject: [PATCH] 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 --- .env.example | 4 +- README.md | 7 +- app/actions/bazi.ts | 30 +++ app/actions/combined.ts | 84 +++++++ app/actions/liuyao.ts | 66 ++++++ app/bazi/page.tsx | 16 ++ app/combined/page.tsx | 16 ++ app/layout.tsx | 6 +- app/learn/[guaMark]/page.tsx | 47 ++++ app/learn/other/[guaMark]/page.tsx | 47 ++++ app/learn/other/page.tsx | 55 +++++ app/learn/page.tsx | 55 +++++ app/liuyao/page.tsx | 16 ++ app/page.tsx | 61 ++++- app/server.ts | 104 ++------- components/divination.tsx | 256 -------------------- components/header.tsx | 48 +++- components/learn/gua-footer.tsx | 16 ++ components/learn/markdown-content.tsx | 75 ++++++ components/modes/bazi-chart.tsx | 72 ++++++ components/modes/bazi-form.tsx | 188 +++++++++++++++ components/modes/combined-form.tsx | 325 ++++++++++++++++++++++++++ components/modes/liuyao-form.tsx | 237 +++++++++++++++++++ components/page-shell.tsx | 20 ++ components/shared/datetime-picker.tsx | 49 ++++ components/shared/hexagram-input.tsx | 202 ++++++++++++++++ components/shared/region-select.tsx | 64 +++++ docs/DEPLOY.md | 24 +- docs/SPEC.md | 2 +- ecosystem.config.cjs | 8 +- lib/ai/stream.ts | 77 ++++++ lib/calc/bazi.ts | 176 ++++++++++++++ lib/calc/hexagram.ts | 77 ++++++ lib/calc/time.ts | 29 +++ lib/calc/timing.ts | 98 ++++++++ lib/content/zhouyi.ts | 53 ++++- lib/data/regions.json | 103 ++++++++ lib/data/regions.ts | 50 ++++ lib/prompts/index.ts | 24 ++ scripts/deploy.sh | 15 +- types/lunar-javascript.d.ts | 73 ++++++ 41 files changed, 2590 insertions(+), 385 deletions(-) create mode 100644 app/actions/bazi.ts create mode 100644 app/actions/combined.ts create mode 100644 app/actions/liuyao.ts create mode 100644 app/bazi/page.tsx create mode 100644 app/combined/page.tsx create mode 100644 app/learn/[guaMark]/page.tsx create mode 100644 app/learn/other/[guaMark]/page.tsx create mode 100644 app/learn/other/page.tsx create mode 100644 app/learn/page.tsx create mode 100644 app/liuyao/page.tsx delete mode 100644 components/divination.tsx create mode 100644 components/learn/gua-footer.tsx create mode 100644 components/learn/markdown-content.tsx create mode 100644 components/modes/bazi-chart.tsx create mode 100644 components/modes/bazi-form.tsx create mode 100644 components/modes/combined-form.tsx create mode 100644 components/modes/liuyao-form.tsx create mode 100644 components/page-shell.tsx create mode 100644 components/shared/datetime-picker.tsx create mode 100644 components/shared/hexagram-input.tsx create mode 100644 components/shared/region-select.tsx create mode 100644 lib/ai/stream.ts create mode 100644 lib/calc/bazi.ts create mode 100644 lib/calc/hexagram.ts create mode 100644 lib/calc/time.ts create mode 100644 lib/calc/timing.ts create mode 100644 lib/data/regions.json create mode 100644 lib/data/regions.ts create mode 100644 lib/prompts/index.ts create mode 100644 types/lunar-javascript.d.ts diff --git a/.env.example b/.env.example index 34050ab..133bbcf 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,8 @@ OPENAI_BASE_URL=https://op.bz121.com/v1 # AI 模型 OPENAI_MODEL=huihui_ai/gemma-4-abliterated:e4b -# 服务端口 -PORT=3000 +# 服务端口(生产 PM2 默认 3130;本地 dev 仍用 3000) +PORT=3130 # 运行环境 NODE_ENV=production diff --git a/README.md b/README.md index 378048c..4ce001b 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,23 @@ cp .env.example .env.local pnpm run dev ``` -访问 http://localhost:3000 +访问 http://localhost:3000(`pnpm run dev` 默认端口;生产 PM2 为 **3130**) ## 生产部署(摘要) ```bash cd /opt/zhimingge +cp .env.example .env.local && nano .env.local # 填写 OPENAI_API_KEY,PORT=3130 pnpm install --frozen-lockfile pnpm run build pm2 start ecosystem.config.cjs pm2 save && pm2 startup ``` +服务监听 **3130** 端口:`http://服务器IP:3130` + +日常更新:`bash scripts/deploy.sh` + 完整步骤见 [docs/DEPLOY.md](./docs/DEPLOY.md)。 ## 目录说明 diff --git a/app/actions/bazi.ts b/app/actions/bazi.ts new file mode 100644 index 0000000..58a27ab --- /dev/null +++ b/app/actions/bazi.ts @@ -0,0 +1,30 @@ +"use server"; + +import { streamAIResponse } from "@/lib/ai/stream"; +import { + calculateBazi, + formatBaziForPrompt, + type BaziInput, +} from "@/lib/calc/bazi"; +import { BAZI_SYSTEM_PROMPT } from "@/lib/prompts"; + +export async function getBaziAnswer( + input: BaziInput, + question: string, + birthPlaceName: string, +) { + const chart = calculateBazi(input); + const chartText = formatBaziForPrompt(chart); + + return streamAIResponse( + BAZI_SYSTEM_PROMPT, + `【出生时空】 +出生地:${birthPlaceName} + +【排盘信息】 +${chartText} + +【问事】 +${question}`, + ); +} diff --git a/app/actions/combined.ts b/app/actions/combined.ts new file mode 100644 index 0000000..d29c5eb --- /dev/null +++ b/app/actions/combined.ts @@ -0,0 +1,84 @@ +"use server"; + +import { streamAIResponse } from "@/lib/ai/stream"; +import { + calculateBazi, + formatBaziForPrompt, + type BaziInput, +} from "@/lib/calc/bazi"; +import { + formatTimingForPrompt, + getTimingInfoWithLongitude, +} from "@/lib/calc/timing"; +import { COMBINED_SYSTEM_PROMPT } from "@/lib/prompts"; +import { + extractChangeDetails, + extractZhangMingRen, + readGuaMarkdown, +} from "@/lib/content/zhouyi"; + +export interface CombinedInput { + birth: BaziInput; + birthPlaceName: string; + currentPlaceName: string; + currentLongitude: number; + calcDate: string; + calcTime: string; + question: string; + hexagram?: { + guaMark: string; + guaTitle: string; + guaResult: string; + guaChange: string; + }; +} + +export async function getCombinedAnswer(input: CombinedInput) { + const chart = calculateBazi(input.birth); + const chartText = formatBaziForPrompt(chart); + const { timing, trueSolarTime } = getTimingInfoWithLongitude( + input.calcDate, + input.calcTime, + input.currentLongitude, + ); + const timingText = [ + formatTimingForPrompt(timing, input.currentPlaceName, input.currentLongitude), + `真太阳时:${trueSolarTime}`, + ].join("\n"); + + let hexagramText = ""; + if (input.hexagram) { + const { guaMark, guaTitle, guaResult, guaChange } = input.hexagram; + try { + const guaDetail = await readGuaMarkdown(guaMark); + const explain = extractZhangMingRen(guaDetail) ?? ""; + const changeList = extractChangeDetails(guaDetail, guaChange, guaTitle); + hexagramText = [ + "", + "【卦象 · 可选六爻】", + `${guaTitle} ${guaResult} ${guaChange}`, + explain, + changeList.join("\n"), + ].join("\n"); + } catch { + hexagramText = [ + "", + "【卦象 · 可选六爻】", + `${input.hexagram.guaTitle} ${input.hexagram.guaResult} ${input.hexagram.guaChange}`, + ].join("\n"); + } + } + + return streamAIResponse( + COMBINED_SYSTEM_PROMPT, + `【人和 · 八字排盘】 +出生地:${input.birthPlaceName} +${chartText} + +${timingText} +${hexagramText} + +【问事】 +${input.question}`, + ); +} diff --git a/app/actions/liuyao.ts b/app/actions/liuyao.ts new file mode 100644 index 0000000..295c639 --- /dev/null +++ b/app/actions/liuyao.ts @@ -0,0 +1,66 @@ +"use server"; + +import { streamAIResponse } from "@/lib/ai/stream"; +import { + formatLiuyaoTimingForPrompt, + getTimingInfoWithLongitude, +} from "@/lib/calc/timing"; +import { + extractChangeDetails, + extractZhangMingRen, + readGuaMarkdown, +} from "@/lib/content/zhouyi"; +import { LIUYAO_SYSTEM_PROMPT } from "@/lib/prompts"; + +export interface LiuyaoInput { + question: string; + calcDate: string; + calcTime: string; + locationName: string; + longitude: number; + guaMark: string; + guaTitle: string; + guaResult: string; + guaChange: string; +} + +export async function getLiuyaoAnswer(input: LiuyaoInput) { + const { timing, trueSolarTime } = getTimingInfoWithLongitude( + input.calcDate, + input.calcTime, + input.longitude, + ); + const timingText = formatLiuyaoTimingForPrompt( + timing, + trueSolarTime, + input.locationName, + input.longitude, + ); + + let guaDetailText = ""; + try { + const guaDetail = await readGuaMarkdown(input.guaMark); + const explain = extractZhangMingRen(guaDetail) ?? ""; + const changeList = extractChangeDetails( + guaDetail, + input.guaChange, + input.guaTitle, + ); + guaDetailText = [explain, changeList.join("\n")].filter(Boolean).join("\n"); + } catch { + guaDetailText = ""; + } + + return streamAIResponse( + LIUYAO_SYSTEM_PROMPT, + `${timingText} + +【卦象】 +${input.guaTitle} ${input.guaResult} ${input.guaChange} + +【问事】 +${input.question} + +${guaDetailText}`, + ); +} diff --git a/app/bazi/page.tsx b/app/bazi/page.tsx new file mode 100644 index 0000000..5d8d4c3 --- /dev/null +++ b/app/bazi/page.tsx @@ -0,0 +1,16 @@ +import PageShell from "@/components/page-shell"; +import BaziForm from "@/components/modes/bazi-form"; + +export default function BaziPage() { + return ( + +
+

生辰八字

+

+ 四柱排盘 · 十神大运 · AI 命理解读 +

+
+ +
+ ); +} diff --git a/app/combined/page.tsx b/app/combined/page.tsx new file mode 100644 index 0000000..ccc3126 --- /dev/null +++ b/app/combined/page.tsx @@ -0,0 +1,16 @@ +import PageShell from "@/components/page-shell"; +import CombinedForm from "@/components/modes/combined-form"; + +export default function CombinedPage() { + return ( + +
+

综合测算

+

+ 天时 · 地利 · 人和 · 六爻可选 +

+
+ +
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 36d1a76..78decce 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,11 +5,11 @@ import Umami from "@/components/umami"; import { ThemeProvider } from "next-themes"; export const metadata: Metadata = { - title: "AI 算卦 - 在线卜卦 GPT4 解读", + title: "知命阁 - 易经学习 · 六爻算卦 · 生辰八字", description: - "AI 算卦 - 通过进行六次硬币的随机卜筮,生成卦象,并使用 AI 对卦象进行分析|AI 算命、在线算命、在线算卦、周易易经64卦", + "知命阁 — 融合周易智慧与人工智能,提供易经学习、六爻算卦、生辰八字排盘、综合测算等服务", appleWebApp: { - title: "AI 算卦", + title: "知命阁", }, }; diff --git a/app/learn/[guaMark]/page.tsx b/app/learn/[guaMark]/page.tsx new file mode 100644 index 0000000..a53a26b --- /dev/null +++ b/app/learn/[guaMark]/page.tsx @@ -0,0 +1,47 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import PageShell from "@/components/page-shell"; +import GuaFooter from "@/components/learn/gua-footer"; +import MarkdownContent from "@/components/learn/markdown-content"; +import { + getGuaName, + getGuaNumber, + listGuaMarks, + readLearnMarkdown, + stripFrontmatter, +} from "@/lib/content/zhouyi"; + +export async function generateStaticParams() { + const marks = await listGuaMarks("traditional"); + return marks.map((guaMark) => ({ guaMark })); +} + +export default async function GuaDetailPage({ + params, +}: { + params: Promise<{ guaMark: string }>; +}) { + const { guaMark } = await params; + const marks = await listGuaMarks("traditional"); + if (!marks.includes(guaMark)) { + notFound(); + } + + const raw = await readLearnMarkdown(guaMark, "traditional"); + const content = stripFrontmatter(raw); + + return ( + +
+ + ← 返回总览 + +
+
+ 第 {getGuaNumber(guaMark)} 卦 · {getGuaName(guaMark)} +
+ + +
+ ); +} diff --git a/app/learn/other/[guaMark]/page.tsx b/app/learn/other/[guaMark]/page.tsx new file mode 100644 index 0000000..884bea8 --- /dev/null +++ b/app/learn/other/[guaMark]/page.tsx @@ -0,0 +1,47 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import PageShell from "@/components/page-shell"; +import GuaFooter from "@/components/learn/gua-footer"; +import MarkdownContent from "@/components/learn/markdown-content"; +import { + getGuaName, + getGuaNumber, + listGuaMarks, + readLearnMarkdown, + stripFrontmatter, +} from "@/lib/content/zhouyi"; + +export async function generateStaticParams() { + const marks = await listGuaMarks("simplified"); + return marks.map((guaMark) => ({ guaMark })); +} + +export default async function GuaOtherDetailPage({ + params, +}: { + params: Promise<{ guaMark: string }>; +}) { + const { guaMark } = await params; + const marks = await listGuaMarks("simplified"); + if (!marks.includes(guaMark)) { + notFound(); + } + + const raw = await readLearnMarkdown(guaMark, "simplified"); + const content = stripFrontmatter(raw); + + return ( + +
+ + ← 返回简体总览 + +
+
+ 第 {getGuaNumber(guaMark)} 卦 · {getGuaName(guaMark)} +
+ + +
+ ); +} diff --git a/app/learn/other/page.tsx b/app/learn/other/page.tsx new file mode 100644 index 0000000..8dc695a --- /dev/null +++ b/app/learn/other/page.tsx @@ -0,0 +1,55 @@ +import Link from "next/link"; +import PageShell from "@/components/page-shell"; +import { + getGuaName, + getGuaNumber, + listGuaMarks, +} from "@/lib/content/zhouyi"; + +export default async function LearnOtherPage() { + const guaMarks = await listGuaMarks("simplified"); + + return ( + +
+

易经学习 · 简体图文版

+

+ 含互卦、错卦、综卦及更多注释(部分卦象图片待补全) +

+ + ← 返回繁体精简版 + +
+
+ + + + + + + + + {guaMarks.map((mark) => ( + + + + + ))} + +
序号卦名
+ {getGuaNumber(mark)} + + + {getGuaName(mark)} + +
+
+
+ ); +} diff --git a/app/learn/page.tsx b/app/learn/page.tsx new file mode 100644 index 0000000..5e96a9c --- /dev/null +++ b/app/learn/page.tsx @@ -0,0 +1,55 @@ +import Link from "next/link"; +import PageShell from "@/components/page-shell"; +import { + getGuaName, + getGuaNumber, + listGuaMarks, +} from "@/lib/content/zhouyi"; + +export default async function LearnPage() { + const guaMarks = await listGuaMarks("traditional"); + + return ( + +
+

易经学习

+

+ 周易六十四卦原文详解(繁体精简版) +

+ + 切换到简体图文版 → + +
+
+ + + + + + + + + {guaMarks.map((mark) => ( + + + + + ))} + +
序号卦名
+ {getGuaNumber(mark)} + + + {getGuaName(mark)} + +
+
+
+ ); +} diff --git a/app/liuyao/page.tsx b/app/liuyao/page.tsx new file mode 100644 index 0000000..ddfb391 --- /dev/null +++ b/app/liuyao/page.tsx @@ -0,0 +1,16 @@ +import PageShell from "@/components/page-shell"; +import LiuyaoForm from "@/components/modes/liuyao-form"; + +export default function LiuyaoPage() { + return ( + +
+

六爻算卦

+

+ 问事 · 起卦时空 · 线上 / 线下六爻 +

+
+ +
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 547f980..8437f39 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,58 @@ -import Header from "@/components/header"; -import Divination from "@/components/divination"; -import Footer from "@/components/footer"; +import Link from "next/link"; +import PageShell from "@/components/page-shell"; +import { BookOpen, BrainCircuit, Compass, Sparkles } from "lucide-react"; + +const MODULES = [ + { + href: "/learn", + title: "易经学习", + description: "64 卦原文阅读,繁体精简版与简体图文版", + icon: BookOpen, + }, + { + href: "/liuyao", + title: "六爻算卦", + description: "三钱法起卦,结合卦辞 AI 智能解读", + icon: Sparkles, + }, + { + href: "/bazi", + title: "生辰八字", + description: "四柱排盘,十神大运流年,AI 命理解读", + icon: BrainCircuit, + }, + { + href: "/combined", + title: "综合测算", + description: "天时地利人和融合分析,六爻可选", + icon: Compass, + }, +]; export default function Home() { return ( - <> -
- -