
Ziwei Ai Html Report
- 8 installs
- 1 repo stars
- Updated May 24, 2026
- archlizheng/ziwei-ai-html-report
ziwei-ai-html-report is a skill that generates a self-contained single-file HTML Zi Wei Dou Shu astrology report from birth data using an offline chart engine and validated K-line data.
About
This skill generates a saveable single-file HTML Zi Wei Dou Shu (Chinese astrology) report from birth data or supplied chart context. It ships a pure-Python offline charting tool (with an optional Node/iztro path for app-aligned stars and true-solar-time conversion) and assembles a report with comprehensive annotation, yearly fortune, and a life K-line section. It validates the K-line JSON, refuses to invent star placements, and covers solar dates from 1900 to 2100.
- Generates a self-contained single-file HTML Zi Wei Dou Shu report from birth data
- Ships a pure-Python offline chart engine (no Node) with optional iztro alignment and true-solar-time
- Validates K-line JSON before delivery and refuses to invent star placements
Ziwei Ai Html Report by the numbers
- 8 all-time installs (skills.sh)
- Ranked #500 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
ziwei-ai-html-report capabilities & compatibility
- Capabilities
- html export · report generation · offline chart
What ziwei-ai-html-report says it does
希望**脱离 Node** → 只用 **路径 A**(已含真太阳时)
交付物必须是**单文件 HTML**(便于保存、邮件、网盘分享)
npx skills add https://github.com/archlizheng/ziwei-ai-html-report --skill ziwei-ai-html-reportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 1 |
| Last updated | May 24, 2026 |
| Repository | archlizheng/ziwei-ai-html-report ↗ |
What it does
Generate a self-contained offline HTML Zi Wei Dou Shu report from birth data with validated K-line data.
Who is it for?
Producing an offline single-file HTML astrology report from solar birth date, time, and gender
Skip if: Birth dates outside 1900-2100 without lunar/chart context, or compatibility (合盘) reports (not built in)
When should I use this skill?
when a user needs a saveable offline Zi Wei Dou Shu HTML report and cannot install extra runtime dependencies
What you get
One offline HTML report with chart annotations, yearly fortune, and a validated life K-line
- single-file offline HTML Zi Wei Dou Shu report
By the numbers
- supports solar dates 1900-01-31 to 2100-12-31
- report has 3 sections (annotation / yearly fortune / life K-line)
- K-line values validated within 0-100
Files
紫微 AI 离线 HTML 报告(整合 Skill)
Overview
本 skill 将本仓库内已验证的排盘规则、提示词与命盘上下文格式交给任意 agent:可从阳历出生资料生成命盘上下文,再输出单个 HTML 文件,含「综合批注 / 流年运势 / 人生 K 线」三节。
排盘策略(建议优先:无 Node + 真太阳时)
| 路径 | 命令要点 | 依赖 | 真太阳时 |
|---|---|---|---|
| A. 纯 Python(默认推荐) | python3 tools/ziwei_offline.py --solar ... --time ... --gender ... [--birthplace ...] | 仅 Python 3,无 Node / npm | 默认开启:有经纬度/可解析 birthplace 时按经度+时间方程换算后再定农历与时辰;--disable-true-solar-time 可关 |
| B. iztro 对齐 App 星曜 | 先 Python 输出「真太阳时后的阳历时分」,再喂 iztro,再拼上下文(见下) | 本目录 npm install + Node.js | 可一致:用 --emit-iztro-birth-json 把真太阳时口径注入 iztro |
| C. iztro 仅钟表(与 App 输入框一致) | IZTRO_MODE=raw-clock 或直连 chart_iztro.cjs --solar --time | Node + npm | 否(与用户在 App 里只填钟表时间相同) |
结论:希望脱离 Node → 只用 路径 A(已含真太阳时)。希望主星与 App/iztro 一致且仍要真太阳时 → 用 路径 B(离不开 Node,因 iztro 为 JS 库),无法用「零依赖静态文件」替代。
路径 B 一键(环境变量;需已 npm install):
cd skills/ziwei-ai-html-report
SOLAR=1995-07-19 TIME=11:40 GENDER=male BIRTHPLACE="广东省深圳市" TARGET_YEAR=2026 ./tools/report_payload_iztro.sh路径 B 分步(真太阳 → iztro → 上下文):
python3 tools/ziwei_offline.py --solar 1995-07-19 --time 11:40 --gender male \
--birthplace "广东省深圳市" --target-year 2026 --emit-iztro-birth-json \
| node tools/chart_iztro.cjs --birth-json - \
| python3 tools/ziwei_offline.py --from-chart-json --target-year 2026 --format json--emit-iztro-birth-json 输出一行 JSON(solarDate、time、gender、trueSolarApplied),chart_iztro.cjs --birth-json - 从 stdin 读取;Python 再补齐 patterns、校验与三份 Context。
路径 C(无真太阳、纯钟表 iztro):IZTRO_MODE=raw-clock ./tools/report_payload_iztro.sh -- ... 或旧式 node chart_iztro.cjs --solar ... | python3 ... --from-chart-json。
纯 Python:`tools/ziwei_offline.py`(标准库)。阳历范围 1900-01-31 至 2100-12-31。路径 A 为安星自研实现,与 iztro 在个别命盘上可能仍有差异;以 iztro 为准请用路径 B/C。
核心约束:违反字面即违反本意——不可因「节省时间」「用户急了」跳过输入校验或免责声明。
完整提示词与模板见同目录:prompts.md。HTML 样板见:report-template.html。
运行前提(简化)
本 skill 运行在 agent 环境中,默认已有模型参与解读生成。 因此本文档聚焦于:离线排盘事实、提示词结构、HTML 交付一致性,不重复展开模型可用性说明。
When to Use
- 需要在任意聊天模型 / agent 环境中输出离线可打开的命理报告;
- 用户只提供阳历生日、出生小时、性别、分析年份,且无法或不愿安装额外运行时依赖;
- 用户已粘贴本命 AI 上下文(及流年块,若做流年节),希望直接组装报告;
- 交付物必须是单文件 HTML(便于保存、邮件、网盘分享)。
When NOT to Use
- 用户生日超出 1900-2100 且无法提供农历/命盘上下文——当前离线历法表无法覆盖;
- 仅需合盘:当前发布版不内置合盘节;若需要,请在本目录内新增独立提示词节;
- 只想在现成产品 UI 内查看,而非交付离线 HTML。
输入契约(必填 / 可选用)
A. 出生资料(首选,可独立排盘)
最小输入:
| 字段 | 说明 |
|---|---|
| 阳历日期 | YYYY-MM-DD,范围 1900-01-31 至 2100-12-31 |
| 出生时间 | HH:mm(推荐)或 hour(0-23);23:00-23:59 按晚子时换日 |
| 性别 | male/female 或 男/女 |
| 出生地 | 可选;用于真太阳时换算,默认 hybrid 模式下优先在线地理编码,失败回退内置城市表 |
| 流年分析年 | 可选,默认当前年 |
年龄口径:大限与流年叠宫使用虚岁(分析年 − 农历生年 + 1)。报告页眉与流年上下文会同时标注周岁(截至分析年 12 月 31 日,以及截至生成日若与年末不同)。
运行工具生成上下文:
python3 tools/ziwei_offline.py \
--solar 1996-01-06 \
--time 11:30 \
--gender female \
--birthplace "广东省佛山市顺德区" \
--target-year 2026 \
--format json输出 JSON 含:
| 字段 | 用途 |
|---|---|
chart | 离线命盘结构化数据 |
natalContext | 综合批注用户上下文 |
yearlyContext | 流年运势用户上下文 |
klineData | 确定性人生 K 线数据,直接嵌入 HTML |
klineContext | K 线文字解读上下文;不得作为数值走势来源 |
标准示例(展示层):
- 阳历生日:1996年1月6日
- 农历生日:可不填(系统自动计算)
- 出生时间:11:30
- 性别:女
- 出生地:广东省佛山市顺德区
对应机器字段:
solar=1996-01-06time=11:30(或hour=11)gender=femalebirthplace=广东省佛山市顺德区targetYear=2026(可选)
B. 本命上下文(备选)
在本 skill 的中州派规则下,以下内容之一视为有效本命输入:
1. 首选:与本工具 build_prompt_context 输出等价的 Markdown 文本(以 【命盘完整信息】 开头,含 ## 十二宫星曜分布、## 十二大限、按需含 ## 近年流年信息 等)。 2. 备选:结构化 JSON(可还原为同上 Markdown)。最小字段映射:
| 字段 | 含义 |
|---|---|
命宫主星 | { name, group?, description? }[],至少 name |
身宫主星 | 同上 |
身宫位置 | 宫位名字符串 |
十二宫 | { name, stem, majorStars[], minorStars[], adjectiveStars?, isBodyPalace?, decadalRange? }[] |
大限 | { palaceName, ageRange, stem, mutagens[] }[] |
流年 | { year, stem, branch, mutagens[], palaceName }[](可为空数组) |
四化分布 | { star, sihua: { name, effect }, palace }[] |
格局提示 | string[] |
若出生日期超出离线工具范围且无上述内容:停止生成断语,请用户补充可靠上下文,或输出仅含「信息不足说明」的 HTML(不得编造星曜)。
流年节(若 HTML 含「年度运势」)
若使用离线工具,直接使用输出的 yearlyContext。若用户提供上下文,则除本命上下文外,还须 【流年盘信息】 块:流年干支、流年四化、流年命宫;当前大限干支与四化;重点宫位星曜。禁止仅靠公历年份自行臆造叠宫。
K 线节
默认使用离线工具输出的 klineData 作为数值来源,直接填入 report-template.html 中 #kline-data。klineContext 只用于生成 reason/brief 等文字补充,不得让模型自由决定 open/high/low/close。
数值口径(示意分,峰值稀缺):tools/ziwei_offline.py 先计算 1-100 岁 raw score,再做全生命相对映射;收盘上限约 98、最高可到 100(常量 KLINE_CLOSE_CAP / KLINE_HIGH_CAP),允许少数最强年份接近满分,但应避免长段贴顶。校验允许 ∈ [0,100]。
路径 A 亮度:纯 Python 排盘会为主星写入 庙/旺/得/利/平/不/陷 亮度(对齐 iztro STARS_INFO),并用于 K 线评分与 HTML 排盘图展示。路径 B 仍可用于 App/iztro 对齐验证。
每条 `klineData` 含:age/year/ganZhi/daYun/daYunRange、OHLC、score、dimensions(事业/财/情/健)、yearlyMutagens;合并 kline-brief.json 后可加 brief、可选 reason。
报告页交互:report-template.html 在 K 线图下提供悬停详情与可展开的 1–100 岁数值表;用户无需另查 JSON 即可看每年数字与简评。流年长文仍在「年度流年运势」节。
产品对齐(历法与派别)
与本目录规则基线一致(见 `docs/rules-baseline.md`):中州派、正月初一分年/运限、23:00 晚子时换日。不得在未说明的情况下改用立春或其它流派。
时间口径:
- 默认固定 UTC+8(不回溯历史夏令时);
- 若提供经纬度或可解析出生地,将使用真太阳时:
标准时 + 经度修正 + 时间方程; - CLI 默认
--geocode-mode hybrid:在线优先,失败回退tools/cn_locations.json,再失败则回退标准时; - 若需完全离线,可显式传
--geocode-mode offline;若需可复现坐标,优先传--longitude/--latitude; - 若缺少可用经纬度,则回退标准时并在输出中给出提示。
离线工具内置:
- 1900-2100 阳历转农历压缩表;
- 命宫/身宫、十二宫、五虎遁宫干、五行局;
- 紫微/天府与十四主星;
- 左辅右弼、文昌文曲、魁钺、禄存羊陀、火铃、空劫;
- 生年四化、大限、目标流年上下文。
当前有意保守:
- 不依赖外部排盘库的亮度体系,输出中亮度可为空;
- 不宣称与任何商业排盘工具 100% 一致,除非后续用样例逐条对齐。
执行步骤(agent)
1. 校验输入:若有出生资料,先运行 tools/ziwei_offline.py 生成 JSON;若已有上下文,确认存在 natalContext 等价文本与 yearlyContext。 2. 排盘自检:确认 JSON 含 chart.palaces 12 宫、fiveElementsClass、natalContext、yearlyContext、klineContext、klineData。 3. 渲染提示词(推荐):运行 tools/render_prompts.py --payload-json payload.json --out-dir work --work-root work,读取 prompt-manifest.json;勿手抄 prompts.md,以渲染文件为唯一注入来源。 4. 模型生成产物:
natal.html:用prompts/natal.system.md+natal.user.md;yearly.html:用yearly.system.md+yearly.user.md;kline-brief.json:用kline.system.md+kline.user.md,仅可返回age/brief/reason,不得返回或改写open/high/low/close。
5. K 线:数值必须使用离线工具输出的 klineData;如需文字说明,只能将模型生成的 brief/reason 合并到对应年龄。 6. K 线自检(修正前不得写入 HTML):
- 恰 100 条,
age为 1–100 各出现一次; age===1时open === 50;- 对
age>1:open与上一条close在数值上一致(容差 1e-6,或两位小数相等); high >= max(open,close),low <= min(open,close),且所有数值 ∈ [0,100];满分附近应是少数高峰,勿人为改成长段顶格。
7. 组装 HTML:使用 tools/generate_report.py 与 report-template.html 组装;正式交付禁止 --skip-validation。页面顺序固定:紫微排盘图 → 紫微命盘综合批注 → 流年运势 → 人生运势 K 线(含悬停/表格查每年 OHLC 与 brief)→ 数据完整性状态。将命盘 JSON 填入 script#chart-data,将综合/流年正文填入对应 #content-*,将合并后的 K 线 JSON 嵌入 script#kline-data;页脚保留与提示词等价的免责声明。 8. 输出:向用户交付完整 <!DOCTYPE html> 文档(可复制为 .html 文件后直接双击打开)。
运行模式
模式 A:模型增强(推荐)
- 使用离线工具得到
natalContext/yearlyContext/klineData/klineContext; - 用
render_prompts.py生成 prompt 文件与prompt-manifest.json; - 得到综合批注、流年正文与可选
kline-brief.json后,用generate_report.py组装 HTML; - K 线数值由工具生成并通过强校验;模型只可补充文字,不可改变走势数值。
Quick Reference
| 报告章节 | System 提示来源 | User 数据来源 |
|---|---|---|
| 排盘上下文 | tools/ziwei_offline.py | 阳历日期 + 小时 + 性别 + 分析年 |
| 综合批注 | prompts.md 第 1 节 | natalContext |
| 年度运势 | prompts.md 第 2 节 | natalContext + yearlyContext |
| 人生 K 线 | 离线工具确定性评分 | klineData(数值)+ klineContext(文字参考) |
Baseline Pressure Scenarios(无 skill 时常见失效)
记录在案,用于自检 skill 是否被遵守:
| 场景 | 常见错误行为 |
|---|---|
| 仅有生日性别 | 不运行离线工具,直接写命宫主星与断语 |
| 用户催促 | 跳过 K 线 JSON 连续性校验,或让模型替代确定性 K 线数值 |
| 环境惯性 | 在仅需 Python 路径时仍强迫 npm install / iztro(路径 A 不要 Node) |
| 交付形式 | 仅给 Markdown 片段,不给单文件 HTML |
| 流派 | 自行改用立春换年 |
Red Flags — STOP & Realign
- 有出生资料却未运行
tools/ziwei_offline.py就产出具体星曜落宫; - 离线工具报错(日期超范围/输入缺失)仍继续断语;
- K 线
open[n] ≠ close[n-1]仍入库; - 有
klineData却改用模型自由生成open/high/low/close; - 删除或改写免责声明以「更好看」;
- 将
npm、iztro或 Node 列为必选才能完成报告(默认可用纯 Python;iztro 仅在为对齐 App 星曜时可选)。
Rationalization Table
| 借口 | 事实 |
|---|---|
| 「差不多能推出命宫」 | 必须运行离线工具或读取可靠上下文 |
| 「用户没空打开应用」 | 正是使用离线工具的场景 |
| 「Markdown 对用户更方便」 | 本 skill 明确要求单文件 HTML |
| 「测试后再校验 K 线」 | 必须先校验再写入模板 |
| 「模型更懂走势」 | 模型只适合补充描述;数值走势必须来自确定性评分工具 |
| 「描述性一句话即可代替免责声明」 | 必须保留提示词中与参考性、勿执着一致的表述 |
Common Mistakes
- 把 HTML 外的裸 JSON 当作最终交付物;
- 排盘:手写星曜落宫而不是运行
tools/ziwei_offline.py; - 流年:缺大限锚点或
yearlyContext却仍写叠宫; - K 线:优先嵌入工具输出的
klineData;year字段若填入,应与birthYear + age - 1一致; - 在 skill 正文重复粘贴全套 system prompt——应引用 prompts.md 以保持单一事实来源。
name: ci
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Run unit tests
run: python -m unittest discover -s tests -p "test_*.py"
.DS_Store
node_modules/
output/
__pycache__/
*.py[cod]
.pytest_cache/
.coverage
dist/
build/
# Local generation outputs
work/
# Presentation build/cache artifacts
ppt/
ppt/**/node_modules/
ppt/**/package-lock.json
ppt/**/_qa-*.png
ppt/**/images/*-capture.html
# Logs and local env
*.log
.env
.env.*
!.env.example
规则基线(自包含)
本文为 ziwei-ai-html-report 的本地规则基线,不依赖仓库外文档。
1. 历法与时间口径
- 派别:中州派
- 分年:正月初一分年
- 换日:23:00 视作晚子时换日
- 真太阳时:默认启用,固定 UTC+8(120E),不回溯历史夏令时
2. 排盘算法要点
- 命宫:月顺时逆
- 身宫:月顺时顺
- 十二宫:逆布
- 五行局:水二、木三、金四、土五、火六
- 紫微定位:按农历日与局数换算
- 主星:紫微系 + 天府系十四主星
- 辅煞:左右昌曲魁钺、禄存羊陀、火铃空劫、天马
- 四化:十干四化表
- 运限:阳男阴女顺,阴男阳女逆
3. 语义与输出要点
- 主星语义:采用
tools/knowledge_semantics.json中结构化描述 - 宫位语义:采用
tools/knowledge_semantics.json的宫位要点 - 格局提示:采用
pattern_rules+pattern_rule_sources - 输出必须含免责声明与“仅供参考”说明
4. 可靠性交付护栏
- 命盘结构必须 12 宫
- 必须包含五行局、本命四化、流年块
- K 线必须 100 条、年龄 1-100 唯一、OHLC 关系合法、open 连续
基本信息
- 阳历:1996年1月6日
- 性别:女
- 五行局:木三局
- 分析年份:2026年
【命盘完整信息】
时间修正信息
- 出生地:广东省佛山市顺德区
- 标准时间:1996-01-06 11:30
- 真太阳时:1996-01-06 11:03
- 修正分钟:-27
- 时辰归属:午时
十二宫星曜分布
- 示例占位:实际使用请替换为
ziwei_offline.py输出的natalContext
{
"solar": "1996-01-06",
"time": "11:30",
"gender": "female",
"birthplace": "广东省佛山市顺德区",
"targetYear": 2026
}
{
"chart": {
"engine": "ziwei-ai-html-report offline-python",
"birth": {
"solar": "1996-01-06",
"localTime": "11:30",
"birthplace": "广东省佛山市顺德区",
"coordinates": {
"longitude": 113.2932,
"latitude": 22.8054,
"source": "offline"
},
"trueSolar": {
"enabled": true,
"applied": true,
"time": "1996-01-06 11:03",
"totalCorrectionMinutes": -27.0
}
},
"fiveElementsClass": "木三局"
},
"natalContext": "【命盘完整信息】\\n...",
"yearlyContext": "【流年盘信息】\\n...",
"klineContext": "请根据以下命盘信息,生成 1-100 岁的人生 K 线数据。"
}
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
python3 "$ROOT_DIR/tools/ziwei_offline.py" \
--solar 1996-01-06 \
--time 11:30 \
--gender female \
--birthplace "广东省佛山市顺德区" \
--geocode-mode offline \
--target-year 2026 \
--format json
MIT License
Copyright (c) 2026 Ethan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "ziwei-ai-html-report-chart",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ziwei-ai-html-report-chart",
"version": "1.0.0",
"dependencies": {
"iztro": "^2.5.4"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/i18next": {
"version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iztro": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/iztro/-/iztro-2.5.8.tgz",
"integrity": "sha512-kgyyvxdSEvgJxi6zvHpvzGbXZLGXCdhTHYK2Pe/sRdBIQ7RfCArvupmg2ChUMQCSQGomW7XCI0gWwUuKJwPENg==",
"license": "MIT",
"dependencies": {
"dayjs": "^1.11.10",
"i18next": "^23.5.1",
"lunar-lite": "^0.2.8",
"lunar-typescript": "^1.7.8"
}
},
"node_modules/lunar-lite": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/lunar-lite/-/lunar-lite-0.2.8.tgz",
"integrity": "sha512-Y4tba4RaIFI0ikImJhgoEsyqtDE64lJIM3yFwRX01dbmagCDq7rNmpDQFrSFFy4WXeuywdRVFpIBoT1GGCEizw==",
"license": "MIT",
"dependencies": {
"lunar-typescript": "^1.8.6"
}
},
"node_modules/lunar-typescript": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/lunar-typescript/-/lunar-typescript-1.8.6.tgz",
"integrity": "sha512-5Eo4T/cnuXfrgO4k5LCpOGHIUOuz5hCF/IfNv0T29WY2shR36Hiz+ecN9WjnUuxUKhql9gbOkPaQoqLFKtPRNA==",
"license": "MIT"
}
}
}
{
"name": "ziwei-ai-html-report-chart",
"private": true,
"description": "iztro 命盘导出(与本仓库 app 紫微配置对齐),供 skill 与 HTML 报告生成使用",
"version": "1.0.0",
"dependencies": {
"iztro": "^2.5.4"
}
}
ZiWei AI Prompt Pack(发布版内置提示词)
以下内容是本仓库发布版的内置提示词,便于在对话中整块复制。若用户未提供命盘上下文,先使用本 skill 的纯 Python 工具 `tools/ziwei_offline.py` 生成 natalContext、yearlyContext 与 klineContext。离线工具按本仓库算法文档实现中州派、正月初一分年、23:00 晚子时换日;不依赖 app、Node、npm 或 iztro。
---
0. 离线排盘上下文生成
python3 tools/ziwei_offline.py \
--solar 1996-01-06 \
--time 11:30 \
--gender female \
--birthplace "广东省佛山市顺德区" \
--target-year 2026 \
--format json输出 JSON:
chart:结构化离线命盘;natalContext:直接用于第 1 节综合批注;yearlyContext:直接用于第 2 节流年;klineData:确定性人生 K 线数值,直接嵌入 HTML;klineContext:用于生成 K 线文字说明,不得作为数值走势来源。
说明:
--time优先于--hour;仍兼容旧的--hour。- 提供
--birthplace或--longitude/--latitude时,会按真太阳时换算后再定时辰。 - 口径固定 UTC+8,不回溯历史夏令时。
阳历支持范围:1900-01-31 至 2100-12-31。
---
0b. 生成前完整性检查(必做)
在向模型发送综合/流年/K线提示词前,先执行以下检查:
1. chart.palaces 必须恰好 12 宫,且每宫含 decadalRange。 2. fiveElementsClass 与 fiveElementsNumber(2-6) 必须存在。 3. natalMutagens 不能为空。 4. yearly 必须含 year/stem/branch/mutagens/palaceName/currentDecadal。 5. 若任一检查失败:停止断语,输出“信息不足说明”并提示补充输入,不可臆造星曜。
K 线 JSON 入模板前检查(必做)
- 恰好 100 条,
age覆盖 1-100 且唯一; age=1时open=50;age>1时open[n] = close[n-1];high >= max(open,close)且low <= min(open,close);open/close/high/low全在[0,100]。
---
1. 紫微命盘综合批注(系统提示词)
本节为本 skill 的内置权威版本。
# Role
你是一位研习紫微斗数多年的资深命理师"星图先生"。你精通三合派(观星情格局)、飞星派(推四化轨迹)及钦天门(定气数机缘)。你的论命风格严谨客观,辞藻雅致沉稳,不故弄玄虚,亦不盲目迎合。
# Task
请综合运用上述技法,并根据提供的命盘信息进行解读,对提供的命盘进行全方位推演。分析时需在后台结合"本命、大限、流年"三层结构,但在输出时请转化为用户能理解的语言。
# Analysis Constraints
1. **语言风格**:严禁使用"灵魂底色""磁场""能量"等现代身心灵或互联网词汇。使用更具传统韵味的词汇,如"性情"、"格局"、"机缘"、"运势起伏"。
2. **术语处理**:保留核心术语(如"化禄"、"冲照"、"羊陀"),但必须紧跟通俗解释。
3. **论断原则**:吉凶并陈。既要指出命格的优势("禄"之所在),也要直言命盘的短板("忌"之所冲),并给出中肯的修身建议。
# Output Format
请按照以下结构输出分析报告:
## 紫微命盘综合批注
### 壹· 命格总断
* **格局层次**:依据命宫三方四正的星曜组合,用一句话概括命主一生的基本格局高低与成败基调。
* **性情剖析**:结合命宫与福德宫,分析命主显露在外的处世风格,以及内心的真实欲求与精神境界。
### 贰· 事业与财运
* **官禄方向**:依据官禄宫星情与五行属性,指出命主最适合发展的行业性质(如:宜公职、宜经商、或宜技艺求财)。
* **财运机缘**:分析财帛宫强弱。是正财稳健,还是偏财灵动?一生财源主要来自何方?有无漏财之虞?
### 叁· 婚姻与情感
* **姻缘概况**:分析夫妻宫星曜,描述配偶可能的性格特征或相处模式。
* **相处之道**:指出感情中可能存在的隐患(如:沟通不畅、聚少离多),并给出化解建议。
### 肆· 六亲与人际
* **人际关系**:分析迁移宫及交友宫,判断在外是否有贵人扶持,或是易犯小人口舌。
* **家庭关系**:简述与父母、子女的缘分深浅。
### 伍· 运势隐忧与建议
* **健康提醒**:依据疾厄宫,指出先天体质上较弱的环节,提示需注意的身体部位。
* **趋吉避凶**:综合全盘化忌与煞星的落点,指出命主此生最需要修行的"课题"是什么,并给出具体的时间或方位建议。
### 陆· 命格金句
> 请用2-4句话,以诗意且戳心的方式概括命主的核心性格特质。要求:
> - 语言凝练,朗朗上口,适合分享
> - 风格可以是:自嘲式幽默、温柔共情、或霸气宣言
> - 避免空泛的鸡汤,要有具体的性格洞察
> - 格式:用引号包裹,每句话换行
---
*注:术数推演仅供参考,所谓命由天定,事在人为,望君善加把握。*1d. 详尽度对齐约束(强制,新增)
为对齐 app/ 端“AI 命盘解读”的内容密度,综合批注输出必须同时满足:
1. 结构完整:壹 到 陆 六节全部出现,不可省略任何一节。 2. 信息密度:
壹~伍每节至少 2 个要点;- 每个要点必须包含“命盘依据(星曜/宫位/四化)+ 通俗解释 + 建议”三段信息;
- 全文建议不少于 1200 中文字(不含标题与免责声明)。
3. 命盘锚点引用:至少引用并解释以下宫位锚点中的 5 个:命宫、福德宫、官禄宫、财帛宫、夫妻宫、迁移宫、交友宫、疾厄宫。 4. 吉凶并陈:每一大节需同时出现优势与风险,不得只讲好话或只讲风险。 5. 禁用简版输出:不得使用“概览式三五段简述”替代六节详解。
可附加在 system 末尾的执行语句(推荐):
请输出“完整版”命盘批注:严格覆盖壹到陆六节;每节至少2个可执行要点;每个要点需给出命盘依据、通俗解释与落地建议;不得输出简版摘要。1b. 用户消息模板(综合解读)
本节模板为本 skill 内置版本,直接使用即可。
将 {{CONTEXT}} 替换为离线工具输出的 natalContext,或与 build_prompt_context(...) 等价的 Markdown。
请解读以下命盘:
## 基本信息
- 阳历:{{Y}}年{{M}}月{{D}}日
- 性别:{{GENDER}}
- 五行局:{{FIVE_ELEMENTS_CLASS}}
{{CONTEXT}}
请给出详细但通俗易懂的命盘解读。1c. 语义约束补充(推荐附加到 system)
请严格以提供的命盘上下文为依据,不得引入未出现的星曜落宫事实。
若上下文内存在“宫位要点/格局提示”,优先按这些锚点解释,再展开吉凶并陈建议。---
2. 年度流年运势(系统提示词)
本节为本 skill 的内置权威版本。
# Role
你是一位精通流年推算的紫微斗数专家。根据提供的命盘信息进行解读。在分析流年时,你严格遵循"本命为体,大限为用,流年为应"的原则,运用"限流叠宫"和"流年四化"技法,精准捕捉该年份的吉凶趋势。
# Analysis Logic
1. **叠宫分析**:推演流年命宫叠入本命/大限何宫,以此判断今年的核心际遇(例如:流年命宫叠本命官禄,主事业变动)。
2. **四化引动**:重点分析流年天干引发的四化(禄权科忌)落入何宫,指出得失所在。
3. **时间应期**:结合月令,指出吉凶可能发生的具体时间段。
# Output Format
请严格按照以下结构输出分析报告:
## [年份] 岁次流年运程
### 壹· 年度总象
* **流年定调**:给这一年定一个关键词(如:破局之年、蛰伏之年、开拓之年)。
* **核心际遇**:基于"叠宫"理论,简述今年最核心的关注点是什么(是求财、升迁,还是由于家庭变故分心)。
### 贰· 名利机缘(事业/财运)
* **事业走势**:流年官禄宫分析。是否有升职、跳槽或创业的契机?工作压力源自何处?
* **求财建议**:流年财帛宫分析。适合进取投资还是保守储蓄?是否有意外破耗?
### 叁· 情感与家宅
* **流年姻缘**:流年夫妻宫分析。单身者是否有正缘?已婚者感情是否和睦?
* **家宅平安**:流年田宅与父母宫分析。是否涉及房产变动、装修或长辈健康问题。
### 肆· 月令趋势
* **吉运月份**:指出运势较顺遂的农历月份,适合开展重要事项。
* **注意月份**:指出压力较大或易出问题的农历月份,提示需谨慎行事。
### 伍· 锦囊寄语
* **行事准则**:给出一句针对今年的具体行动建议(如:宜静不宜动,宜守不宜攻)。
* **关键提醒**:关于健康或安全的特别嘱咐。
---
*注:流年运势受多方因素影响,分析仅供参考,切勿执着。*2c. 流年详尽度对齐约束(强制,新增)
为对齐 app/ 内容深度,流年章节必须满足:
1. 结构完整:壹 到 伍 五节全部输出。 2. 锚点完整:
- 必须明确写出:流年命宫叠宫结论、流年四化、当前大限四化;
- 至少覆盖:
事业、财务、情感、家宅、健康五类主题。
3. 月令趋势可执行:
吉运月份与注意月份都要给出,且每组至少 2 条;- 每条需包含“适合/避免事项”。
4. 字数下限:全文不少于 800 中文字(不含标题与免责声明)。 5. 禁用模板化空话:若未引用流年盘中的具体四化或宫位,不得输出该段。
可附加在 system 末尾的执行语句(推荐):
请输出完整版流年报告:严格覆盖壹到伍,至少800字,必须引用流年四化与叠宫结论,并给出可执行的月份级建议。2b. 用户消息模板(流年)
本节模板为本 skill 内置版本,直接使用即可。
{{NATAL_CONTEXT}} = 离线工具输出的 natalContext,或 build_prompt_context 等价文本。{{YEARLY_CONTEXT}} = 离线工具输出的 yearlyContext,或按本节定义生成的 【流年盘信息】 块(含流年基础、当前大限、流年重点宫位星曜)。无上下文时必须先运行离线工具或请用户提供,不可凭公历年份臆造叠宫。
请分析以下命盘的 {{YEAR}} 年运势:
## 基本信息
- 出生:{{BY}}年{{BM}}月{{BD}}日
- 性别:{{GENDER}}
- 五行局:{{FIVE_ELEMENTS_CLASS}}
- 分析年份:{{YEAR}}年
{{NATAL_CONTEXT}}
{{YEARLY_CONTEXT}}
请结合本命盘和流年盘信息,给出详细的 {{YEAR}} 年运势分析。`build_yearly_context` 输出结构(摘要)
- 标题行:
【流年盘信息】 ## 流年基础:流年干支、流年四化、流年命宫宫位名## 当前大限:大限天干、大限四化、大限命宫宫位名## 流年重点宫位星曜:命宫、财帛宫、官禄宫、夫妻宫、疾厄宫、迁移宫的主星/辅星行(格式与离线工具输出一致)
---
3. 人生 K 线文字补充(系统提示词)
本节为本 skill 的内置权威版本。
你是一位精通紫微斗数的命理大师,擅长根据命盘推演人生运势走向。
你的任务是根据命盘信息,为命主生成 1-100 岁的人生运势 K 线文字说明。数值走势必须使用离线工具输出的 `klineData`,不得由模型自由生成或改写 `open/high/low/close`。
## K 线规则
1. **Y 轴含义**: 运势分 (0-100)
- 80+ 大吉:人生巅峰,诸事顺遂
- 60-79 吉:运势良好,有所收获
- 40-59 平:平稳过渡,波澜不惊
- 20-39 凶:运势低迷,需要谨慎
- 0-19 大凶:人生低谷,艰难时期
2. **K 线连贯性**: 每年的 open 必须等于上一年的 close
- 第 1 岁的 open 从 50 开始
- 此后每年 open = 前一年 close
3. **大运周期**: 每个大运(通常10年)应该有相对一致的运势水位
- 好的大运整体偏高 (60-90)
- 差的大运整体偏低 (15-50)
- 大运交接处可以有明显的转折
4. **年内波动**: high 和 low 表示年内最高点和最低点
- high >= max(open, close)
- low <= min(open, close)
- 影线长度反映该年的波动程度
5. **运势逻辑**:
- 化禄、化权、化科 → 运势上升
- 化忌、煞星 → 运势下降
- 紫微、天府坐命 → 整体运势较好
- 杀破狼组合 → 人生起伏较大
- 六吉星会照 → 贵人相助
- 六煞星冲照 → 阻碍较多
## 输出格式
返回 JSON 数组,每个元素包含:
- age: 年龄 (1-100)
- brief: 一句话运势描述(10字以内)
示例(JSON 数组,复制到对话时可去掉外层说明):
[
{"age":1,"brief":"平稳起步"},
{"age":2,"brief":"渐入佳境"}
]
重要:只返回 JSON 数组,不要有其他文字;不得返回或改写 open、close、high、low。3b. 用户消息模板(K 线文字补充)
本节模板为本 skill 内置版本,直接使用即可。 首选直接使用离线工具输出的 klineContext。若用户提供既有命盘,则将下列占位块替换为与 build_kline_context(...) 输入预期等价的汇总(十二宫行、大限、近年流年、本命四化;命宫/身宫主星与身宫位置)。
请根据以下命盘信息,生成 1-100 岁的人生 K 线文字说明。数值走势已由离线工具确定,请不要返回 open、close、high、low。
## 基本信息
- 出生年份: {{BIRTH_YEAR}}年
- 命宫主星: {{LIFE_MAJORS}}
- 身宫位置: {{BODY_PALACE_NAME}}
- 身宫主星: {{BODY_MAJORS}}
## 本命四化
{{SIHUA_LIST}}
## 十二宫配置
{{PALACE_LINES}}
## 大限走向
{{DECADAL_LINES}}
## 近期流年(参考)
{{YEARLY_LINES}}
请生成 100 年的 K 线 brief JSON。十二宫行格式(与源码一致)
宫名(宫干): 主星... | 辅星...
---
4. 选配:双人合盘
本整合 HTML 报告默认不要求合盘节。若需要扩展双人合盘,请在本目录内新增独立提示词节并与当前输入/输出契约保持一致。
ziwei-ai-html-report
用 AI 编程助手(Cursor、Claude Code、Codex 等)为你的出生资料生成一份 可保存、可分享的紫微斗数 AI 命盘报告(单个 HTML 文件,含排盘示意图、综合解读、流年与人生运势 K 线示意)。
报告内容为传统术数的结构化展示与文字解读,仅供文化学习与个人参考,请勿用于医疗、投资或重大决策依据。
---
快速开始
你 不需要 自己懂紫微斗数,也 不必会写代码——装好 Skill 后,在对话里按格式提供出生信息即可;排盘与 HTML 组装由 AI 按本仓库规则完成。
路径 A:已安装 Skill
1. 新开一次 AI 对话(让 Skill 生效)。 2. 复制下面这段话,改成你的信息,发送给 AI。 3. 等待生成 单个 HTML 文件(可双击打开、保存或分享)。
/ziwei-ai-html-report 帮我生成我的紫微斗数 AI 命盘报告
阳历生日: 1995-08-21
出生时间: 11:40
性别: 男
出生地: 广东省深圳市
流年分析年: 2026触发方式因环境而异:优先用 `/ziwei-ai-html-report`;若未注册斜杠命令,可在对话里说明「请严格按 ziwei-ai-html-report skill 生成离线 HTML 报告」,或用 `@` 引用已安装目录下的 SKILL.md。
路径 B:尚未安装 Skill
1. 先安装(推荐一条命令,见下文 安装方式)。 2. 新开一次 AI 对话。 3. 复制 [路径 A](#路径-a已安装-skill) 中的对话模板,改成你的信息后发送。
安装后你会得到与路径 A 相同的体验;无需先 git clone 整个仓库(CLI 会从 GitHub 拉取并链到 agent 的 skills 目录)。
---
安装方式
思路都一样:让 AI 能读到本仓库的 `SKILL.md` 和配套文件(prompts.md、report-template.html、tools/ 等)。下面按推荐顺序排列。
推荐:一条命令安装(Cursor / Claude Code / Codex 等)
在任意项目目录执行(安装到当前项目,便于团队共享):
npx skills add archlizheng/ziwei-ai-html-report -y安装到本机所有项目(个人常用):
npx skills add archlizheng/ziwei-ai-html-report -g -y仅安装到 Cursor:
npx skills add archlizheng/ziwei-ai-html-report -a cursor -y安装后请 新开对话。Skill 名称为 `ziwei-ai-html-report`(与 SKILL.md 一致)。更新已安装的 skill:
npx skills update ziwei-ai-html-report仅查看本仓库里有哪些 skill、不实际安装:
npx skills add archlizheng/ziwei-ai-html-report --listnpx skills add 会从 GitHub 拉取仓库并写入 agent 约定的 skills 目录,不必先手动 clone。
备选:手动克隆或复制仓库
若你更习惯本地有一份完整源码,或当前环境不支持 skills CLI,可用下面三种常见做法。
1)Cursor
- 做法 A(推荐):把整个仓库克隆到你的电脑,在 Cursor 里 打开该文件夹作为工作区,或把仓库放进你当前项目下的子目录(例如
skills/ziwei-ai-html-report/)。 - 做法 B:若你使用 Cursor 的 Agent Skills:把本仓库复制到 Cursor 约定的 skills 目录(以你当前 Cursor 版本说明为准),保证其中有
SKILL.md。
在对话里可以:
- 用 `@` 引用本仓库的
SKILL.md,或 - 若你已配置技能别名,使用类似 `/ziwei-ai-html-report` 触发(取决于你的规则/技能配置是否绑定了该指令)。
2)Claude Code
将本仓库整体复制到 Claude Code 的 skills 目录(具体以官方文档为准),例如用户级 ~/.claude/skills/ 或项目内的 .claude/skills/,在其中新建一个子文件夹(名称随意,建议与本仓库目录名一致),并保证其中有 `SKILL.md`。之后在项目里按该工具的说明加载 skill 即可。
3)Codex / 其它 CLI 或 IDE 插件
- 若支持 skills 目录:同样在对应位置放入本仓库(含
SKILL.md)。 - 若暂不支持:把 `SKILL.md` 的全文或路径提供给对话,并说明「请严格按该 skill 生成离线 HTML 报告」。
你需要保留哪些文件?
最少要保证 AI 能访问:`SKILL.md`、`prompts.md`、`report-template.html`、`tools/ziwei_offline.py`(以及 tools/ 下的数据文件)。本仓库已打包齐全,整体克隆或复制最省事。
---
字段说明(照抄格式最稳)
| 信息 | 怎么填 |
|---|---|
| 阳历生日 | YYYY-MM-DD,例如 1995-08-21 |
| 出生时间 | HH:mm(24 小时制);23:00–23:59 在本工具里按晚子时换日处理 |
| 性别 | 男 / 女(或 male / female) |
| 出生地 | 建议写到市/区,用于真太阳时换算;也可在 skill 说明里改用纯离线或经纬度 |
| 流年分析年 | 想重点看的公历年份,例如 2026 |
支持范围:阳历 1900-01-31 至 2100-12-31。超出范围需换其它工具或自备命盘上下文。
---
适合谁
- 希望 对话里说清生日,就让 AI 按固定规则排盘并写出报告的人。
- 需要 离线 HTML(可双击打开、发邮件、存网盘),而不是只在某个 App 里看的人。
AI 会依照本仓库里的 Skill(`SKILL.md`)与提示词约定来生成内容。
---
AI 会为你做什么(你只需要知道结果)
1. 用本仓库自带的 Python 排盘脚本 生成结构化命盘、klineData 与提示词上下文。 2. 渲染综合批注、流年与 K 线文字补充提示词,供模型撰写正文。 3. 模型撰写综合/流年解读与可选 K 线简述;K 线数值始终来自工具生成的 klineData,不由模型编造。 4. 将内容填入 `report-template.html`,校验通过后输出 单个 HTML 文件。
若某一步信息不足(例如日期超范围),按约定应 停止编造,并提示你补资料——这是正常现象。
---
可选:命令行与标准 Agent 工作流
本节面向熟悉终端的用户;日常生成报告走 快速开始 即可。
只要排盘 JSON
本机安装 Python 3 后,在仓库根目录执行:
python3 tools/ziwei_offline.py \
--solar 1996-01-06 \
--time 11:30 \
--gender female \
--birthplace "广东省佛山市顺德区" \
--target-year 2026 \
--format json输出中含 natalContext、yearlyContext、klineContext、klineData。完整 HTML 仍建议走 Skill + 模板 流程。
标准 Agent 工作流
python3 tools/ziwei_offline.py \
--solar 1996-01-06 \
--time 11:30 \
--gender female \
--birthplace "广东省佛山市顺德区" \
--target-year 2026 \
--format json > work/payload.json
python3 tools/render_prompts.py \
--payload-json work/payload.json \
--out-dir work \
--work-root work
# 按 work/prompt-manifest.json 调用模型,生成:
# - work/natal.html
# - work/yearly.html
# - work/kline-brief.json(可选,只含 age/brief/reason)
python3 tools/generate_report.py \
--payload-json work/payload.json \
--natal-html work/natal.html \
--yearly-html work/yearly.html \
--kline-brief-json work/kline-brief.json \
-o work/report.html---
常见问题
- 一定要联网吗?
不必须。联网主要用于出生地地理编码;失败会回退内置城市表或标准时,详见 `SKILL.md`。
- 和某 App 排盘是否每个字都一样?
本仓库默认是中州派口径的离线实现;若要与某 App 星曜完全一致,SKILL.md 中说明了可选的 iztro(Node)路径。
- 报告里的 K 线是预测吗?
不是。K 线为基于命盘语义的示意曲线,模板内亦有说明。
- 日期格式不对或超出范围?
须为 YYYY-MM-DD,且在 1900-01-31~2100-12-31 内;超出范围请勿让 AI 编造命盘。
- 出生地无法解析?
对话场景下可让 AI 按 skill 中的回退规则处理;命令行可在 SKILL.md 中查阅 --geocode-mode offline 或 --longitude / --latitude。
- 命令行报时间相关错误?
推荐 HH:mm,须提供 --time 或 --hour。
---
开发者与仓库信息
- 规则与契约:[SKILL.md](./SKILL.md)、[prompts.md](./prompts.md)
- 历法与派别说明:[docs/rules-baseline.md](./docs/rules-baseline.md)
- 运行测试:
python3 -m unittest discover -s tests -p "test_*.py" - 许可证:[LICENSE](./LICENSE)
本仓库可 单独克隆使用,不依赖其它项目目录。
---
GitHub 发布版保证(给集成者与 Agent)
- 本仓库可单独克隆、复制、运行,不依赖其它私有目录。
- 核心排盘、提示词渲染、HTML 组装与校验仅需 Python 3 标准库;iztro 仅用于可选对齐路径。
- 失败契约:输入越界、上下文不完整、或 K 线数据未通过校验时,应停止杜撰星曜落宫,并提示用户补资料或修正数据。
Release Checklist
发布前逐项确认:
python3 -m unittest discover -s tests -p "test_*.py"通过。bash examples/quickstart.sh能输出合法 JSON。python3 tools/ziwei_offline.py --help与 README 示例一致。python3 tools/render_prompts.py --help与python3 tools/generate_report.py --help可用。README.md含prompt-manifest.json、klineData、kline-brief.json与generate_report.py工作流。README.md、SKILL.md、prompts.md不包含私有路径、悬空符号或仓库外依赖说明。SKILL.mdfrontmatter 只包含name与description,且description只描述触发条件。LICENSE、.gitignore、.github/workflows/ci.yml已存在。report-template.html仍保留免责声明与 K 线完整性校验逻辑。- K 线数值只来自
payload.klineData,模型输出只可补充brief/reason,不得覆盖open/high/low/close。 - 默认
hybrid、纯离线offline、手工坐标三条使用路径都在 README 中可见。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>紫微斗数 · AI 命盘报告</title>
<style>
:root {
--bg: #1a1714;
--panel: #242019;
--text: #e8e2d9;
--muted: #9a9288;
--gold: #c9a962;
--gold-dim: #8a7a54;
--bull: #6b9e78;
--bear: #a65d5d;
--wick: #c4b89f;
--border: rgba(201, 169, 98, 0.22);
--radius: 12px;
--font-serif: Georgia, "Noto Serif SC", serif;
--font-sans: system-ui, "PingFang SC", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(160deg, #141210 0%, var(--bg) 40%, #161310 100%);
color: var(--text);
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.65;
min-height: 100vh;
}
.wrap {
max-width: 880px;
margin: 0 auto;
padding: 2rem 1.25rem 3rem;
}
header.report-header {
text-align: center;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
margin-bottom: 2rem;
}
header.report-header h1 {
font-family: var(--font-serif);
font-weight: 600;
font-size: 1.75rem;
color: var(--gold);
margin: 0 0 0.5rem;
letter-spacing: 0.06em;
}
header.report-header .subtitle {
color: var(--muted);
font-size: 0.9rem;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.65rem;
margin-top: 1rem;
text-align: left;
font-size: 0.88rem;
color: var(--muted);
}
.meta-grid strong { color: var(--text); font-weight: 500; }
.palace-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
}
.palace-cell {
min-height: 120px;
border: 1px solid var(--border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.02);
padding: 0.45rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.palace-cell.life { box-shadow: inset 0 0 0 1px rgba(201, 169, 98, 0.65); }
.palace-cell.body { box-shadow: inset 0 0 0 1px rgba(107, 158, 120, 0.55); }
.palace-head {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--muted);
font-size: 0.72rem;
}
.palace-name { color: var(--text); font-size: 0.8rem; font-weight: 600; }
.palace-stars { font-size: 0.78rem; color: var(--text); }
.palace-stars small { color: var(--muted); }
.center-cell {
grid-column: 2 / span 2;
grid-row: 2 / span 2;
min-height: 248px;
border: 1px solid var(--border);
border-radius: 10px;
background: rgba(0, 0, 0, 0.22);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0.7rem;
}
.center-cell h3 {
margin: 0 0 0.4rem;
font-family: var(--font-serif);
font-size: 1.02rem;
color: var(--gold);
}
.center-cell p {
margin: 0.12rem 0;
color: var(--muted);
font-size: 0.8rem;
}
.mutagen { color: var(--gold); font-size: 0.72rem; }
.brightness { color: var(--gold-dim); font-size: 0.72rem; }
.borrow-hint { color: var(--gold-dim); font-size: 0.72rem; margin-right: 0.25rem; }
section.report-section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.25rem 1.35rem;
margin-bottom: 1.5rem;
}
section.report-section h2 {
font-family: var(--font-serif);
font-size: 1.2rem;
color: var(--gold);
margin: 0 0 1rem;
padding-bottom: 0.45rem;
border-bottom: 1px solid var(--border);
}
.prose :where(h3) {
font-size: 1.05rem;
color: var(--text);
margin: 1.1rem 0 0.4rem;
}
.prose :where(p) { margin: 0.5rem 0; color: var(--text); }
.prose :where(ul) {
margin: 0.4rem 0 0.6rem;
padding-left: 1.2rem;
}
.prose :where(li) { margin: 0.25rem 0; }
.prose blockquote {
margin: 0.8rem 0;
padding: 0.5rem 0 0.5rem 1rem;
border-left: 3px solid var(--gold-dim);
color: var(--muted);
font-style: italic;
}
.placeholder {
color: var(--muted);
font-style: italic;
border: 1px dashed var(--border);
border-radius: 8px;
padding: 0.85rem;
}
.kline-wrap {
margin-top: 0.75rem;
}
canvas#kline-canvas {
display: block;
width: 100%;
height: 280px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.35);
}
.kline-legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 0.5rem;
font-size: 0.82rem;
color: var(--muted);
}
.kline-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.sq { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
#kline-detail {
margin-top: 0.65rem;
padding: 0.75rem 0.9rem;
border: 1px solid var(--border);
border-radius: 8px;
background: rgba(0, 0, 0, 0.22);
font-size: 0.86rem;
line-height: 1.55;
color: var(--text);
min-height: 3.2rem;
}
#kline-detail .muted { color: var(--muted); }
#kline-detail strong { color: var(--gold); font-weight: 600; }
.kline-table-wrap { margin-top: 0.65rem; font-size: 0.82rem; color: var(--muted); }
.kline-table-wrap summary { cursor: pointer; color: var(--gold-dim); }
.kline-table-scroll {
margin-top: 0.5rem;
max-height: 220px;
overflow: auto;
border: 1px solid var(--border);
border-radius: 8px;
}
table.kline-table {
width: 100%;
border-collapse: collapse;
font-size: 0.78rem;
}
table.kline-table th,
table.kline-table td {
padding: 0.35rem 0.5rem;
border-bottom: 1px solid rgba(201, 169, 98, 0.12);
text-align: left;
white-space: nowrap;
}
table.kline-table th { color: var(--gold-dim); font-weight: 500; position: sticky; top: 0; background: var(--panel); }
table.kline-table tr:hover td { background: rgba(201, 169, 98, 0.08); }
table.kline-table tr.active td { background: rgba(201, 169, 98, 0.14); }
.integrity-list {
margin: 0.5rem 0 0;
padding-left: 1.2rem;
color: var(--muted);
font-size: 0.9rem;
}
.integrity-list li.ok { color: #8dbd98; }
.integrity-list li.fail { color: #d18c8c; }
footer.disclaimer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
font-size: 0.8rem;
color: var(--muted);
line-height: 1.55;
}
footer.disclaimer p { margin: 0.4rem 0; }
@media print {
body { background: #fff; color: #222; }
:root {
--bg: #fff; --panel: #fafaf8; --text: #222; --muted: #555;
--gold: #6b5a2b; --border: #ccc;
}
.wrap { padding: 0; max-width: 100%; }
section.report-section { break-inside: avoid; }
}
</style>
</head>
<body>
<div class="wrap">
<header class="report-header">
<h1>紫微斗数 · AI 命盘报告</h1>
<p class="subtitle" id="meta-subtitle">生成时间 · <span id="meta-generated"></span></p>
<div class="meta-grid">
<div><strong>阳历</strong> · <span id="meta-solar">(请填入)</span></div>
<div><strong>出生地</strong> · <span id="meta-birthplace">(请填入)</span></div>
<div><strong>标准时间</strong> · <span id="meta-local-time">(请填入)</span></div>
<div><strong>真太阳时</strong> · <span id="meta-true-solar-time">(请填入)</span></div>
<div><strong>时间修正</strong> · <span id="meta-time-correction">(请填入)</span></div>
<div><strong>坐标来源</strong> · <span id="meta-geo-source">(请填入)</span></div>
<div><strong>性别</strong> · <span id="meta-gender">(请填入)</span></div>
<div><strong>五行局</strong> · <span id="meta-five">(请填入)</span></div>
<div><strong>流年分析年</strong> · <span id="meta-target-year">(请填入)</span></div>
<div><strong>年龄</strong> · <span id="meta-age">(请填入)</span></div>
</div>
</header>
<section class="report-section" id="section-chart">
<h2>紫微排盘图</h2>
<div id="chart-grid" class="palace-grid">
<p class="placeholder">Agent:将命盘 JSON 写入 #chart-data 后,此处自动渲染十二宫排盘图。</p>
</div>
</section>
<section class="report-section" id="section-natal">
<h2>紫微命盘综合批注</h2>
<div class="prose" id="content-natal">
<p class="placeholder">
Agent:将模型的「紫微命盘综合批注」正文转为语义化 HTML 写入此处(建议使用 h3、ul、blockquote;勿使用外链脚本)。
</p>
</div>
</section>
<section class="report-section" id="section-yearly">
<h2>流年运势</h2>
<div class="prose" id="content-yearly">
<p class="placeholder">
Agent:填入指定公历年的「岁次流年运程」HTML。若用户未提供【流年盘信息】,请注明该节未完成并说明原因。
</p>
</div>
</section>
<section class="report-section" id="section-kline">
<h2>人生运势 K 线(1–100 岁)</h2>
<p class="prose" style="margin:0 0 0.75rem;color:var(--muted);font-size:0.88rem;">
竖轴为运势示意分(少数高峰可接近 98–100,避免长段贴顶);每根蜡烛为一年内开、收、高、低。悬停图表或展开下表可查看该岁完整数字与简评;详细流年批注见上文「年度流年运势」。
</p>
<div class="kline-wrap">
<canvas id="kline-canvas" width="800" height="280" aria-label="人生运势K线图"></canvas>
<div class="kline-legend">
<span><i class="sq" style="background:var(--bull)"></i> 收涨(收盘≥开盘)</span>
<span><i class="sq" style="background:var(--bear)"></i> 收跌</span>
<span><i class="sq" style="background:var(--gold)"></i> 收盘趋势</span>
<span id="kline-hint"></span>
</div>
<div id="kline-detail" class="muted">将鼠标移到图表上查看该年 OHLC、四维分与简评</div>
<details class="kline-table-wrap">
<summary>展开 1–100 岁完整数值表(含 brief)</summary>
<div class="kline-table-scroll">
<table class="kline-table" id="kline-table">
<thead>
<tr>
<th>岁</th><th>年</th><th>干支</th><th>开</th><th>收</th><th>高</th><th>低</th><th>均分</th><th>简评</th>
</tr>
</thead>
<tbody id="kline-table-body"></tbody>
</table>
</div>
</details>
</div>
</section>
<section class="report-section" id="section-integrity">
<h2>数据完整性状态</h2>
<div class="prose">
<p id="integrity-summary" class="placeholder">Agent:生成后此处应显示“通过/失败”摘要。</p>
<ul id="integrity-checks" class="integrity-list"></ul>
</div>
</section>
<footer class="disclaimer">
<p><strong>重要说明:</strong>术数推演仅供参考。命盘综合解读:命由天定,事在人为,望善加把握。流年分析:流年运势受多方因素影响,仅供参考,切勿执着。人生 K 线:为基于命盘语义的示意性推演,不具有预测效力。</p>
</footer>
</div>
<!-- Agent:将下文替换为长度为 100 的 JSON 数组;字段 age,open,close,high,low,brief(可选 reason、year) -->
<script type="application/json" id="chart-data">{"palaces":[]}</script>
<script type="application/json" id="kline-data">[]</script>
<script>
(function () {
document.getElementById('meta-generated').textContent =
new Date().toISOString().slice(0, 19).replace('T', ' ');
function parseKLine() {
var el = document.getElementById('kline-data');
if (!el) return [];
try {
var t = el.textContent.trim();
var data = JSON.parse(t || '[]');
return Array.isArray(data) ? data : [];
} catch (e) {
return [];
}
}
function parseChart() {
var el = document.getElementById('chart-data');
if (!el) return { palaces: [] };
try {
var data = JSON.parse(el.textContent.trim() || '{"palaces":[]}');
return data && typeof data === 'object' ? data : { palaces: [] };
} catch (e) {
return { palaces: [] };
}
}
function renderChartGrid() {
var wrap = document.getElementById('chart-grid');
if (!wrap) return;
var chart = parseChart();
var palaces = Array.isArray(chart.palaces) ? chart.palaces : [];
if (palaces.length !== 12) {
wrap.innerHTML = '<p class="placeholder">命盘数据不足:需12宫后才可绘制排盘图。</p>';
return;
}
var positionByBranch = {
'巳': [0,0], '午':[0,1], '未':[0,2], '申':[0,3],
'辰':[1,0], '酉':[1,3], '卯':[2,0], '戌':[2,3],
'寅':[3,0], '丑':[3,1], '子':[3,2], '亥':[3,3]
};
var grid = [[],[],[],[]];
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) grid[r][c] = null;
palaces.forEach(function (p) {
var pos = positionByBranch[p.branch];
if (pos) grid[pos[0]][pos[1]] = p;
});
function renderStars(stars) {
if (!Array.isArray(stars) || stars.length === 0) return '<small>无</small>';
return stars.map(function (s) {
if (!s || !s.name) return '';
var bright = s.brightness ? '<span class="brightness">(' + s.brightness + ')</span>' : '';
var tag = s.mutagen ? '<span class="mutagen">[' + s.mutagen + ']</span>' : '';
return '<span>' + s.name + bright + tag + '</span>';
}).join('、');
}
function oppositeBranch(b) {
var order = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
var i = order.indexOf(b);
if (i < 0) return null;
return order[(i + 6) % 12];
}
function renderMajorWithBorrow(palace, allPalaces) {
if (Array.isArray(palace.majorStars) && palace.majorStars.length > 0) {
return renderStars(palace.majorStars);
}
var ob = oppositeBranch(palace.branch);
var opp = null;
for (var j = 0; j < allPalaces.length; j++) {
if (allPalaces[j].branch === ob) { opp = allPalaces[j]; break; }
}
if (opp && Array.isArray(opp.majorStars) && opp.majorStars.length > 0) {
return '<span class="borrow-hint">借对宫「' + (opp.name || '') + '」</span>' + renderStars(opp.majorStars);
}
return renderStars(palace.majorStars);
}
var html = '';
for (var row = 0; row < 4; row++) {
for (var col = 0; col < 4; col++) {
if (row >= 1 && row <= 2 && col >= 1 && col <= 2) {
if (row === 1 && col === 1) {
html += '<div class="center-cell"><h3>紫微斗数命盘</h3>'
+ '<p>阳历:' + (chart.birthSolar || '-') + '</p>'
+ '<p>五行局:' + (chart.fiveElementsClass || '-') + '</p>'
+ '<p>命宫:' + (chart.lifePalaceName || '-') + ' · 身宫:' + (chart.bodyPalaceName || '-') + '</p>'
+ '<p>流年:' + (chart.targetYear || '-') + '</p></div>';
}
continue;
}
var palace = grid[row][col];
if (!palace) {
html += '<div class="palace-cell"></div>';
continue;
}
var isMing = palace.isLifePalace || palace.name === '命宫';
var klass = 'palace-cell' + (isMing ? ' life' : '') + (palace.isBodyPalace ? ' body' : '');
html += '<div class="' + klass + '">'
+ '<div class="palace-head"><span>' + (palace.stem || '') + (palace.branch || '') + '</span><span>' + (palace.decadalRange || '') + '</span></div>'
+ '<div class="palace-name">' + (palace.name || '-') + '</div>'
+ '<div class="palace-stars"><small>主星:</small>' + renderMajorWithBorrow(palace, palaces) + '</div>'
+ '<div class="palace-stars"><small>辅星:</small>' + renderStars(palace.minorStars) + '</div>'
+ '</div>';
}
}
wrap.innerHTML = html;
}
var klinePick = { padL: 40, gw: 0, n: 0, sorted: [] };
function sortedKlineRows(data) {
return data.slice().sort(function (a, b) { return Number(a.age) - Number(b.age); });
}
function renderKlineDetail(row) {
var el = document.getElementById('kline-detail');
if (!el) return;
if (!row) {
el.innerHTML = '<span class="muted">将鼠标移到图表上查看该年 OHLC、四维分与简评</span>';
return;
}
var dims = row.dimensions || {};
var mut = Array.isArray(row.yearlyMutagens) ? row.yearlyMutagens.join(' · ') : '';
var brief = row.brief ? String(row.brief) : '(暂无简评)';
var reason = row.reason ? '<p class="muted" style="margin:0.35rem 0 0;">' + row.reason + '</p>' : '';
el.innerHTML =
'<p style="margin:0;"><strong>' + row.age + ' 岁 · ' + (row.year || '-') + ' 年 ' + (row.ganZhi || '') + '</strong>'
+ ' · 大限 ' + (row.daYunRange || row.daYun || '-') + '</p>'
+ '<p style="margin:0.35rem 0 0;">开 <strong>' + row.open + '</strong> · 收 <strong>' + row.close + '</strong>'
+ ' · 高 <strong>' + row.high + '</strong> · 低 <strong>' + row.low + '</strong>'
+ ' · 年均 <strong>' + (row.score != null ? row.score : '-') + '</strong></p>'
+ '<p class="muted" style="margin:0.25rem 0 0;">事业 ' + (dims.career != null ? dims.career : '-')
+ ' · 财 ' + (dims.wealth != null ? dims.wealth : '-')
+ ' · 情 ' + (dims.relationship != null ? dims.relationship : '-')
+ ' · 健 ' + (dims.health != null ? dims.health : '-') + '</p>'
+ (mut ? '<p class="muted" style="margin:0.25rem 0 0;">流年四化:' + mut + '</p>' : '')
+ '<p style="margin:0.35rem 0 0;">简评:' + brief + '</p>' + reason;
}
function renderKlineTable(rows) {
var body = document.getElementById('kline-table-body');
if (!body) return;
body.innerHTML = '';
rows.forEach(function (row) {
var tr = document.createElement('tr');
tr.dataset.age = String(row.age);
tr.innerHTML =
'<td>' + row.age + '</td><td>' + (row.year || '') + '</td><td>' + (row.ganZhi || '') + '</td>'
+ '<td>' + row.open + '</td><td>' + row.close + '</td><td>' + row.high + '</td><td>' + row.low + '</td>'
+ '<td>' + (row.score != null ? row.score : '') + '</td><td>' + (row.brief || '') + '</td>';
tr.addEventListener('mouseenter', function () {
highlightKlineRow(row.age);
renderKlineDetail(row);
});
body.appendChild(tr);
});
}
function highlightKlineRow(age) {
var body = document.getElementById('kline-table-body');
if (!body) return;
Array.prototype.forEach.call(body.querySelectorAll('tr'), function (tr) {
tr.classList.toggle('active', String(tr.dataset.age) === String(age));
});
}
function pickKlineRowFromX(canvas, clientX) {
if (!klinePick.n) return null;
var rect = canvas.getBoundingClientRect();
var x = clientX - rect.left;
var rel = (x - klinePick.padL) / Math.max(klinePick.gw, 1);
var idx = Math.round(rel * (klinePick.n - 1));
idx = Math.max(0, Math.min(klinePick.n - 1, idx));
return klinePick.sorted[idx] || null;
}
function bindKlineCanvas(canvas) {
if (!canvas || canvas.dataset.klineBound === '1') return;
canvas.dataset.klineBound = '1';
canvas.addEventListener('mousemove', function (ev) {
var row = pickKlineRowFromX(canvas, ev.clientX);
if (row) {
renderKlineDetail(row);
highlightKlineRow(row.age);
}
});
canvas.addEventListener('mouseleave', function () {
highlightKlineRow(null);
});
}
function drawKLine() {
var canvas = document.getElementById('kline-canvas');
var hint = document.getElementById('kline-hint');
if (!canvas) return;
var data = parseKLine();
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
var cw = rect.width || 800;
var ch = 280;
canvas.width = Math.floor(cw * dpr);
canvas.height = Math.floor(ch * dpr);
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cw, ch);
var padL = 40, padR = 12, padT = 14, padB = 28;
var gw = cw - padL - padR;
var gh = ch - padT - padB;
var plotInset = Math.max(10, Math.round(gh * 0.08));
var plotTop = padT + plotInset;
var plotBottom = padT + gh - plotInset;
var plotH = plotBottom - plotTop;
var yMin = 0;
var yMax = 100;
function xPos(i, total) {
if (total <= 1) return Math.round(padL + gw / 2);
return Math.round(padL + (i / (total - 1)) * gw);
}
function strokeY(y) {
return Math.round(y) + 0.5;
}
function niceFloor(v) {
return Math.max(0, Math.floor(v / 5) * 5);
}
function niceCeil(v) {
return Math.min(100, Math.ceil(v / 5) * 5);
}
function computeYDomain(rows) {
var lo = Infinity;
var hi = -Infinity;
rows.forEach(function (row) {
['open', 'close', 'high', 'low'].forEach(function (key) {
var n = Number(row[key]);
if (n !== n) return;
lo = Math.min(lo, n);
hi = Math.max(hi, n);
});
});
if (lo === Infinity) return { min: 0, max: 100 };
var minV = niceFloor(lo - 5);
var maxV = niceCeil(hi + 5);
if (maxV - minV < 30) {
var mid = (maxV + minV) / 2;
minV = niceFloor(mid - 15);
maxV = niceCeil(mid + 15);
}
minV = Math.max(0, minV);
maxV = Math.min(100, maxV);
if (maxV <= minV) maxV = Math.min(100, minV + 30);
return { min: minV, max: maxV };
}
function yScale(v) {
var clamped = Math.min(yMax, Math.max(yMin, Number(v)));
if (clamped !== clamped) clamped = (yMin + yMax) / 2;
return plotBottom - ((clamped - yMin) / (yMax - yMin)) * plotH;
}
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0, 0, cw, ch);
if (data.length === 0) {
ctx.fillStyle = '#9a9288';
ctx.font = '14px sans-serif';
ctx.fillText('暂无 K 线数据:请在 #kline-data 中填入 100 条记录后刷新。', padL, padT + gh / 2);
if (hint) hint.textContent = '数据条数:0';
return;
}
var domain = computeYDomain(data);
yMin = domain.min;
yMax = domain.max;
ctx.strokeStyle = 'rgba(201,169,98,0.15)';
ctx.lineWidth = 1;
for (var li = 0; li <= 4; li++) {
var tickVal = yMax - ((yMax - yMin) * li) / 4;
var y = strokeY(yScale(tickVal));
ctx.beginPath();
ctx.moveTo(padL, y);
ctx.lineTo(cw - padR, y);
ctx.stroke();
ctx.fillStyle = '#9a9288';
ctx.font = '11px system-ui,sans-serif';
ctx.fillText(String(Math.round(tickVal)), 4, y + 4);
}
var n = data.length;
var slot = gw / Math.max(n, 1);
ctx.save();
ctx.beginPath();
ctx.rect(padL, plotTop, gw, plotH);
ctx.clip();
data.forEach(function (row, i) {
var open = Number(row.open);
var close = Number(row.close);
var high = Number(row.high);
var low = Number(row.low);
if (open !== open) open = 50;
if (close !== close) close = open;
if (high !== high) high = Math.max(open, close);
if (low !== low) low = Math.min(open, close);
high = Math.min(100, Math.max(0, high));
low = Math.min(100, Math.max(0, low));
open = Math.min(100, Math.max(0, open));
close = Math.min(100, Math.max(0, close));
var cx = xPos(i, n);
var bw = Math.max(2, Math.min(4, Math.round(slot * 0.45)));
var bull = close >= open;
var strokeColor = bull ? '#6b9e78' : '#a65d5d';
var bodyColor = bull ? 'rgba(107,158,120,0.95)' : 'rgba(166,93,93,0.95)';
var yHigh = yScale(high);
var yLow = yScale(low);
var yOpen = yScale(open);
var yClose = yScale(close);
var bodyTop = Math.min(yOpen, yClose);
var bodyBottom = Math.max(yOpen, yClose);
var bodyH = Math.max(Math.round(bodyBottom - bodyTop), 1);
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 1;
ctx.lineCap = 'butt';
ctx.beginPath();
ctx.moveTo(cx + 0.5, strokeY(yHigh));
ctx.lineTo(cx + 0.5, strokeY(yLow));
ctx.stroke();
ctx.fillStyle = bodyColor;
ctx.fillRect(Math.round(cx - bw / 2), Math.round(bodyTop), bw, bodyH);
});
ctx.strokeStyle = 'rgba(201,169,98,0.5)';
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
data.forEach(function (row, i) {
var close = Number(row.close);
if (close !== close) close = 50;
close = Math.min(100, Math.max(0, close));
var x = xPos(i, n);
var y = yScale(close);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
ctx.restore();
data.forEach(function (row, i) {
var isBoundary = i === 0 || (row.daYunRange && data[i - 1] && row.daYunRange !== data[i - 1].daYunRange);
if (!isBoundary) return;
var cx = xPos(i, n);
ctx.strokeStyle = 'rgba(201,169,98,0.24)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 4]);
ctx.beginPath();
ctx.moveTo(cx + 0.5, strokeY(plotTop));
ctx.lineTo(cx + 0.5, strokeY(plotBottom));
ctx.stroke();
ctx.setLineDash([]);
});
ctx.fillStyle = '#9a9288';
ctx.font = '11px sans-serif';
ctx.fillText('1岁', padL, ch - 8);
ctx.fillText(String(n) + '岁', cw - padR - 36, ch - 8);
if (hint) {
hint.textContent = '数据条数:' + n + ' · 纵轴 ' + yMin + '–' + yMax + ' · 竖向虚线为大限分界';
}
klinePick.padL = padL;
klinePick.gw = gw;
klinePick.n = n;
klinePick.sorted = sortedKlineRows(data);
renderKlineTable(klinePick.sorted);
if (klinePick.sorted.length) renderKlineDetail(klinePick.sorted[0]);
bindKlineCanvas(canvas);
}
function validateKLine(data) {
var result = [];
if (data.length !== 100) {
result.push({ ok: false, text: "K线条数必须为100条" });
return result;
}
var sortedData = data.slice().sort(function (a, b) {
return Number(a.age) - Number(b.age);
});
var ages = {};
var prevClose = null;
for (var i = 0; i < sortedData.length; i++) {
var row = sortedData[i];
var age = Number(row.age);
var open = Number(row.open);
var close = Number(row.close);
var high = Number(row.high);
var low = Number(row.low);
if (!Number.isFinite(age) || age < 1 || age > 100 || ages[age]) {
result.push({ ok: false, text: "age必须覆盖1-100且唯一" });
break;
}
ages[age] = true;
if ([open, close, high, low].some(function (x) { return !Number.isFinite(x) || x < 0 || x > 100; })) {
result.push({ ok: false, text: "存在越界数值(需在0-100)" });
break;
}
if (high < Math.max(open, close) || low > Math.min(open, close)) {
result.push({ ok: false, text: "OHLC关系不合法(high/low与open/close冲突)" });
break;
}
if (age === 1 && Math.abs(open - 50) > 1e-6) {
result.push({ ok: false, text: "age=1时open必须等于50" });
break;
}
if (prevClose !== null && Math.abs(open - prevClose) > 1e-6) {
result.push({ ok: false, text: "K线不连续:open[n]必须等于close[n-1]" });
break;
}
prevClose = close;
}
if (result.length === 0) {
result.push({ ok: true, text: "K线100条与连续性校验通过" });
}
return result;
}
function renderIntegrity() {
var summary = document.getElementById('integrity-summary');
var list = document.getElementById('integrity-checks');
if (!summary || !list) return;
var checks = [];
var data = parseKLine();
checks = checks.concat(validateKLine(data));
var metaIds = ['meta-solar', 'meta-birthplace', 'meta-local-time', 'meta-true-solar-time', 'meta-time-correction', 'meta-geo-source', 'meta-gender', 'meta-five', 'meta-target-year'];
var metaMissing = metaIds.some(function (id) {
var el = document.getElementById(id);
return !el || /请填入/.test((el.textContent || '').trim());
});
checks.push({
ok: !metaMissing,
text: metaMissing ? '元信息未完整填充(阳历/性别/五行局/分析年)' : '元信息完整'
});
var failed = checks.filter(function (x) { return !x.ok; });
summary.textContent = failed.length === 0
? '完整性检查通过:可作为离线报告归档。'
: ('完整性检查失败:' + failed.length + ' 项不通过,请先修正后再交付。');
summary.className = failed.length === 0 ? '' : 'placeholder';
list.innerHTML = '';
checks.forEach(function (item) {
var li = document.createElement('li');
li.className = item.ok ? 'ok' : 'fail';
li.textContent = (item.ok ? '✓ ' : '✗ ') + item.text;
list.appendChild(li);
});
}
window.addEventListener('resize', function () {
drawKLine();
});
drawKLine();
renderChartGrid();
renderIntegrity();
})();
</script>
</body>
</html>
ziwei-ai-html-report 规则映射矩阵
本文用于把本 skill 的规则基线映射到离线引擎实现与测试,确保规则可追溯、可校验、可回归。规则基线文档见 `docs/rules-baseline.md`。
1) 排盘算法规则映射
| 来源 | 规则要点 | 引擎实现 | 测试覆盖 | 状态 |
|---|---|---|---|---|
docs/rules-baseline.md#2. 排盘算法要点 | 命宫:月顺时逆,身宫:月顺时顺;十二宫逆布 | life_palace_branch body_palace_branch arrange_palaces | test_life_and_body_palace_examples_match_algorithm_docs | 已覆盖 |
docs/rules-baseline.md#2. 排盘算法要点 | 命宫干支定五行局(水二木三金四土五火六) | five_elements_class | test_generate_chart_contains_required_context_blocks(结构) | 部分覆盖(缺精确断言) |
docs/rules-baseline.md#2. 排盘算法要点 | 出生日与局数定紫微 | ziwei_position | test_ziwei_position_examples_match_algorithm_docs | 已覆盖 |
docs/rules-baseline.md#2. 排盘算法要点 | 紫微/天府两系十四主星排布 | arrange_major_stars | test_major_star_series_from_documented_midday_example | 已覆盖 |
docs/rules-baseline.md#2. 排盘算法要点 | 左右昌曲魁钺、禄存羊陀、火铃空劫、天马 | auxiliary_stars | test_auxiliary_stars_includes_tianma | 已覆盖 |
docs/rules-baseline.md#2. 排盘算法要点 | 年干起寅宫宫干 | palace_stems + WUHU_START | 结构已覆盖(待补精确值样例) | 部分覆盖 |
2) 四化与运限规则映射
| 来源 | 规则要点 | 引擎实现 | 测试覆盖 | 状态 |
|---|---|---|---|---|
docs/rules-baseline.md#2. 排盘算法要点 | 十干四化表 | SIHUA_TABLE sihua_list mutagen_by_star | test_sihua_table_matches_reference | 已覆盖(关键项) |
docs/rules-baseline.md#2. 排盘算法要点 | 起运=五行局数;阳男阴女顺、阴男阳女逆 | decadal_ranges _current_decadal | 新增顺逆分叉测试 | 部分覆盖 |
docs/rules-baseline.md#2. 排盘算法要点 | 流年地支定流年命宫,流年天干定四化 | build_yearly_data | 新增年份锚点测试 | 部分覆盖 |
3) 解读语义规则映射
| 来源 | 语义要点 | 当前实现 | 风险 | 改造策略 |
|---|---|---|---|---|
tools/knowledge_semantics.json#major_star_profiles | 主星/吉煞基础语义 | MAJOR_STAR_DESCRIPTIONS(硬编码简版) | 语义浅、难维护 | 外置语义映射文件 |
tools/knowledge_semantics.json#palace_focus | 十二宫解释框架 | 上下文仅列宫位与星曜 | 宫位意义缺失 | 外置宫位语义模板 |
tools/knowledge_semantics.json#pattern_rules | 格局条件与提示 | detect_patterns(少量规则) | 成格条件过宽 | 外置格局规则并收紧条件 |
当前已实现:
- 语义外置文件:
tools/knowledge_semantics.json - 来源锚点:
major_star_sources/palace_focus_sources/pattern_rule_sources - 主星结构化释义:
major_star_profiles(优点/风险/适配场景)在natalContext的命宫、身宫节按固定模板输出 - 模式识别由
pattern_rules驱动,并在输出中附来源字符串,便于追溯。
4) 可靠性交付护栏映射
| 需求 | 实现位置 | 状态 |
|---|---|---|
| 上下文字段完整性校验(12宫/五行局/四化/大限/流年) | validate_chart_integrity | 已实现 |
| K线 100 条与 OHLC 连续性校验 | validate_kline_data + 模板前端校验 | 已实现 |
| 模板可见完整性状态提示 | report-template.html | 已实现 |
| prompts 生成前检查清单 | prompts.md | 已实现 |
5) 明确不覆盖项(当前版本)
- 不引入立春分年;坚持正月初一分年与晚子时换日。
- 不依赖 iztro 亮度体系;亮度字段保留可选空值。
- 不承诺与商业排盘工具 100% 一致(需样例逐条回归)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>紫微斗数 · AI 命盘报告</title>
<style>
:root {
--bg: #1a1714;
--panel: #242019;
--text: #e8e2d9;
--muted: #9a9288;
--gold: #c9a962;
--gold-dim: #8a7a54;
--bull: #6b9e78;
--bear: #a65d5d;
--wick: #c4b89f;
--border: rgba(201, 169, 98, 0.22);
--radius: 12px;
--font-serif: Georgia, "Noto Serif SC", serif;
--font-sans: system-ui, "PingFang SC", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(160deg, #141210 0%, var(--bg) 40%, #161310 100%);
color: var(--text);
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.65;
min-height: 100vh;
}
.wrap {
max-width: 880px;
margin: 0 auto;
padding: 2rem 1.25rem 3rem;
}
header.report-header {
text-align: center;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
margin-bottom: 2rem;
}
header.report-header h1 {
font-family: var(--font-serif);
font-weight: 600;
font-size: 1.75rem;
color: var(--gold);
margin: 0 0 0.5rem;
letter-spacing: 0.06em;
}
header.report-header .subtitle {
color: var(--muted);
font-size: 0.9rem;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.65rem;
margin-top: 1rem;
text-align: left;
font-size: 0.88rem;
color: var(--muted);
}
.meta-grid strong { color: var(--text); font-weight: 500; }
.palace-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
}
.palace-cell {
min-height: 120px;
border: 1px solid var(--border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.02);
padding: 0.45rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.palace-cell.life { box-shadow: inset 0 0 0 1px rgba(201, 169, 98, 0.65); }
.palace-cell.body { box-shadow: inset 0 0 0 1px rgba(107, 158, 120, 0.55); }
.palace-head {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--muted);
font-size: 0.72rem;
}
.palace-name { color: var(--text); font-size: 0.8rem; font-weight: 600; }
.palace-stars { font-size: 0.78rem; color: var(--text); }
.palace-stars small { color: var(--muted); }
.center-cell {
grid-column: 2 / span 2;
grid-row: 2 / span 2;
min-height: 248px;
border: 1px solid var(--border);
border-radius: 10px;
background: rgba(0, 0, 0, 0.22);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0.7rem;
}
.center-cell h3 {
margin: 0 0 0.4rem;
font-family: var(--font-serif);
font-size: 1.02rem;
color: var(--gold);
}
.center-cell p {
margin: 0.12rem 0;
color: var(--muted);
font-size: 0.8rem;
}
.mutagen { color: var(--gold); font-size: 0.72rem; }
.borrow-hint { color: var(--gold-dim); font-size: 0.72rem; margin-right: 0.25rem; }
section.report-section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.25rem 1.35rem;
margin-bottom: 1.5rem;
}
section.report-section h2 {
font-family: var(--font-serif);
font-size: 1.2rem;
color: var(--gold);
margin: 0 0 1rem;
padding-bottom: 0.45rem;
border-bottom: 1px solid var(--border);
}
.prose :where(h3) {
font-size: 1.05rem;
color: var(--text);
margin: 1.1rem 0 0.4rem;
}
.prose :where(p) { margin: 0.5rem 0; color: var(--text); }
.prose :where(ul) {
margin: 0.4rem 0 0.6rem;
padding-left: 1.2rem;
}
.prose :where(li) { margin: 0.25rem 0; }
.prose blockquote {
margin: 0.8rem 0;
padding: 0.5rem 0 0.5rem 1rem;
border-left: 3px solid var(--gold-dim);
color: var(--muted);
font-style: italic;
}
.placeholder {
color: var(--muted);
font-style: italic;
border: 1px dashed var(--border);
border-radius: 8px;
padding: 0.85rem;
}
.kline-wrap {
margin-top: 0.75rem;
}
canvas#kline-canvas {
display: block;
width: 100%;
height: 280px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.35);
}
.kline-legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 0.5rem;
font-size: 0.82rem;
color: var(--muted);
}
.kline-legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.sq { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.integrity-list {
margin: 0.5rem 0 0;
padding-left: 1.2rem;
color: var(--muted);
font-size: 0.9rem;
}
.integrity-list li.ok { color: #8dbd98; }
.integrity-list li.fail { color: #d18c8c; }
footer.disclaimer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
font-size: 0.8rem;
color: var(--muted);
line-height: 1.55;
}
footer.disclaimer p { margin: 0.4rem 0; }
@media print {
body { background: #fff; color: #222; }
:root {
--bg: #fff; --panel: #fafaf8; --text: #222; --muted: #555;
--gold: #6b5a2b; --border: #ccc;
}
.wrap { padding: 0; max-width: 100%; }
section.report-section { break-inside: avoid; }
}
</style>
</head>
<body>
<div class="wrap">
<header class="report-header">
<h1>紫微斗数 · AI 命盘报告</h1>
<p class="subtitle" id="meta-subtitle">生成时间 · <span id="meta-generated"></span></p>
<div class="meta-grid">
<div><strong>阳历</strong> · <span id="meta-solar">1995-07-19</span></div>
<div><strong>出生地</strong> · <span id="meta-birthplace">广东省深圳市</span></div>
<div><strong>标准时间</strong> · <span id="meta-local-time">11:30</span></div>
<div><strong>真太阳时</strong> · <span id="meta-true-solar-time">1995-07-19 10:59</span></div>
<div><strong>时间修正</strong> · <span id="meta-time-correction">-30.15分钟</span></div>
<div><strong>坐标来源</strong> · <span id="meta-geo-source">online</span></div>
<div><strong>性别</strong> · <span id="meta-gender">男</span></div>
<div><strong>五行局</strong> · <span id="meta-five">土5局</span></div>
<div><strong>流年分析年</strong> · <span id="meta-target-year">2027</span></div>
</div>
</header>
<section class="report-section" id="section-natal">
<h2>紫微命盘综合批注</h2>
<div class="prose" id="content-natal">
<h3>壹· 命格总断</h3>
<ul><li><strong>格局层次</strong>:天机化禄坐命,贪狼同宫。</li><li><strong>性情剖析</strong>:外显机敏灵活。</li></ul>
<h3>贰· 事业与财运</h3>
<ul><li><strong>官禄方向</strong>:七杀坐官禄宫。</li><li><strong>财运机缘</strong>:天同破军坐财帛宫。</li></ul>
<h3>叁· 婚姻与情感</h3>
<ul><li><strong>姻缘概况</strong>:太阳天府坐夫妻宫。</li><li><strong>相处之道</strong>:身宫在夫妻宫。</li></ul>
<h3>肆· 六亲与人际</h3>
<ul><li><strong>人际关系</strong>:廉贞坐交友宫。</li><li><strong>家庭关系</strong>:紫微化科入父母宫。</li></ul>
<h3>伍· 运势隐忧与建议</h3>
<ul><li><strong>健康提醒</strong>:疾厄宫无主星。</li><li><strong>趋吉避凶</strong>:太阴化忌入兄弟宫。</li></ul>
<h3>陆· 命格金句</h3>
<blockquote>心思如棋局局新</blockquote>
<p><em>注:仅供参考。</em></p>
</div>
</section>
<section class="report-section" id="section-chart">
<h2>紫微排盘图</h2>
<div id="chart-grid" class="palace-grid">
<p class="placeholder">Agent:将命盘 JSON 写入 #chart-data 后,此处自动渲染十二宫排盘图。</p>
</div>
</section>
<section class="report-section" id="section-yearly">
<h2>流年运势</h2>
<div class="prose" id="content-yearly">
<h3>壹· 年度总象</h3>
<ul><li><strong>流年定调</strong>:丁未年。</li><li><strong>核心际遇</strong>:流年命宫叠入交友宫。</li></ul>
<h3>贰· 名利机缘</h3>
<ul><li><strong>事业走势</strong>:巨门化忌冲照。</li><li><strong>求财建议</strong>:太阴化禄利正财。</li></ul>
<h3>叁· 情感与家宅</h3>
<ul><li><strong>流年姻缘</strong>:流年夫妻宫叠官禄宫。</li><li><strong>家宅平安</strong>:田宅宫天梁化权。</li></ul>
<h3>肆· 月令趋势</h3>
<ul><li><strong>吉运月份</strong>:农历三月、九月。</li><li><strong>注意月份</strong>:农历六月、十二月。</li></ul>
<h3>伍· 锦囊寄语</h3>
<ul><li><strong>行事准则</strong>:宜守不宜攻。</li><li><strong>关键提醒</strong>:注意咽喉健康。</li></ul>
<p><em>注:仅供参考。</em></p>
</div>
</section>
<section class="report-section" id="section-kline">
<h2>人生运势 K 线(1–100 岁)</h2>
<p class="prose" style="margin:0 0 0.75rem;color:var(--muted);font-size:0.88rem;">
竖轴为运势分 0–100;每根蜡烛为一年内开盘、收盘、最高、最低。数据来自模型 JSON,需满足 open 连贯与 high/low 约束。
</p>
<div class="kline-wrap">
<canvas id="kline-canvas" width="800" height="280" aria-label="人生运势K线图"></canvas>
<div class="kline-legend">
<span><i class="sq" style="background:var(--bull)"></i> 收涨(收盘≥开盘)</span>
<span><i class="sq" style="background:var(--bear)"></i> 收跌</span>
<span id="kline-hint"></span>
</div>
</div>
</section>
<section class="report-section" id="section-integrity">
<h2>数据完整性状态</h2>
<div class="prose">
<p id="integrity-summary" class="placeholder">Agent:生成后此处应显示“通过/失败”摘要。</p>
<ul id="integrity-checks" class="integrity-list"></ul>
</div>
</section>
<footer class="disclaimer">
<p><strong>重要说明:</strong>术数推演仅供参考。命盘综合解读:命由天定,事在人为,望善加把握。流年分析:流年运势受多方因素影响,仅供参考,切勿执着。人生 K 线:为基于命盘语义的示意性推演,不具有预测效力。</p>
</footer>
</div>
<!-- Agent:将下文替换为长度为 100 的 JSON 数组;字段 age,open,close,high,low,brief(可选 reason、year) -->
<script type="application/json" id="chart-data">{"birthSolar": "1995-07-19", "fiveElementsClass": "土5局", "lifePalaceName": "命宫(寅宫)", "bodyPalaceName": "夫妻宫(子宫)", "targetYear": "2027", "palaces": [{"name": "命宫", "branch": "寅", "stem": "戊", "majorStars": [{"name": "天机", "description": "谋略之星,主机变、思虑、技术。", "mutagen": "化禄"}, {"name": "贪狼", "description": "桃花才艺之星,主欲望、交际、才华。"}], "minorStars": [{"name": "陀罗"}, {"name": "火星"}], "adjectiveStars": [], "isLifePalace": true, "isBodyPalace": false, "decadalRange": "5-14"}, {"name": "兄弟宫", "branch": "丑", "stem": "己", "majorStars": [{"name": "太阴", "description": "阴柔之星,主财富、情感、细腻。", "mutagen": "化忌"}], "minorStars": [], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "15-24"}, {"name": "夫妻宫", "branch": "子", "stem": "戊", "majorStars": [{"name": "太阳", "description": "光明之星,主名望、外显、助人。"}, {"name": "天府", "description": "库藏之星,主稳重、资源、包容。"}], "minorStars": [{"name": "天魁"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": true, "decadalRange": "25-34"}, {"name": "子女宫", "branch": "亥", "stem": "丁", "majorStars": [{"name": "武曲", "description": "财帛之星,主执行、理财、刚毅。"}], "minorStars": [], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "35-44"}, {"name": "财帛宫", "branch": "戌", "stem": "丙", "majorStars": [{"name": "天同", "description": "福德之星,主温和、享受、人缘。"}, {"name": "破军", "description": "耗星,主破旧立新、变革、起伏。"}], "minorStars": [], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "45-54"}, {"name": "疾厄宫", "branch": "酉", "stem": "乙", "majorStars": [], "minorStars": [{"name": "左辅"}, {"name": "文曲"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "55-64"}, {"name": "迁移宫", "branch": "申", "stem": "甲", "majorStars": [], "minorStars": [{"name": "天钺"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "65-74"}, {"name": "交友宫", "branch": "未", "stem": "癸", "majorStars": [{"name": "廉贞", "description": "囚杀之星,主原则、欲望、变化。"}], "minorStars": [], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "75-84"}, {"name": "官禄宫", "branch": "午", "stem": "壬", "majorStars": [{"name": "七杀", "description": "将星,主开创、决断、波折。"}], "minorStars": [{"name": "地空"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "85-94"}, {"name": "田宅宫", "branch": "巳", "stem": "辛", "majorStars": [{"name": "天梁", "description": "荫寿之星,主护持、原则、长辈缘。", "mutagen": "化权"}], "minorStars": [{"name": "右弼"}, {"name": "文昌"}, {"name": "天马"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "95-104"}, {"name": "福德宫", "branch": "辰", "stem": "庚", "majorStars": [{"name": "天相", "description": "印绶之星,主辅佐、制度、体面。"}], "minorStars": [{"name": "擎羊"}, {"name": "地劫"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "105-114"}, {"name": "父母宫", "branch": "卯", "stem": "己", "majorStars": [{"name": "紫微", "description": "帝座之星,主尊贵、统御、格局。", "mutagen": "化科"}, {"name": "巨门", "description": "口舌暗曜,主表达、疑虑、是非。"}], "minorStars": [{"name": "禄存"}, {"name": "铃星"}], "adjectiveStars": [], "isLifePalace": false, "isBodyPalace": false, "decadalRange": "115-124"}]}</script>
<script type="application/json" id="kline-data">[{"age": 1, "open": 50.0, "close": 47.53, "high": 52.78, "low": 46.96, "brief": "岁1"}, {"age": 2, "open": 47.53, "close": 47.28, "high": 49.7, "low": 43.83, "brief": "岁2"}, {"age": 3, "open": 47.28, "close": 46.21, "high": 49.23, "low": 45.89, "brief": "岁3"}, {"age": 4, "open": 46.21, "close": 48.84, "high": 50.44, "low": 43.86, "brief": "岁4"}, {"age": 5, "open": 48.84, "close": 48.25, "high": 50.56, "low": 46.44, "brief": "岁5"}, {"age": 6, "open": 48.25, "close": 48.84, "high": 49.37, "low": 45.41, "brief": "岁6"}, {"age": 7, "open": 48.84, "close": 46.54, "high": 50.5, "low": 46.1, "brief": "岁7"}, {"age": 8, "open": 46.54, "close": 46.83, "high": 47.89, "low": 44.0, "brief": "岁8"}, {"age": 9, "open": 46.83, "close": 45.59, "high": 48.84, "low": 44.94, "brief": "岁9"}, {"age": 10, "open": 45.59, "close": 48.76, "high": 48.98, "low": 42.69, "brief": "岁10"}, {"age": 11, "open": 48.76, "close": 49.76, "high": 53.32, "low": 46.94, "brief": "岁11"}, {"age": 12, "open": 49.76, "close": 52.55, "high": 53.1, "low": 47.54, "brief": "岁12"}, {"age": 13, "open": 52.55, "close": 50.39, "high": 56.35, "low": 49.79, "brief": "岁13"}, {"age": 14, "open": 50.39, "close": 53.48, "high": 55.72, "low": 48.49, "brief": "岁14"}, {"age": 15, "open": 53.48, "close": 51.24, "high": 55.84, "low": 51.16, "brief": "岁15"}, {"age": 16, "open": 51.24, "close": 52.22, "high": 53.05, "low": 48.67, "brief": "岁16"}, {"age": 17, "open": 52.22, "close": 51.07, "high": 55.95, "low": 47.29, "brief": "岁17"}, {"age": 18, "open": 51.07, "close": 50.42, "high": 53.38, "low": 49.35, "brief": "岁18"}, {"age": 19, "open": 50.42, "close": 50.88, "high": 53.11, "low": 47.18, "brief": "岁19"}, {"age": 20, "open": 50.88, "close": 53.95, "high": 54.64, "low": 47.0, "brief": "岁20"}, {"age": 21, "open": 53.95, "close": 54.12, "high": 55.3, "low": 50.31, "brief": "岁21"}, {"age": 22, "open": 54.12, "close": 54.48, "high": 57.0, "low": 52.51, "brief": "岁22"}, {"age": 23, "open": 54.48, "close": 54.47, "high": 55.63, "low": 51.09, "brief": "岁23"}, {"age": 24, "open": 54.47, "close": 52.39, "high": 56.2, "low": 51.41, "brief": "岁24"}, {"age": 25, "open": 52.39, "close": 50.64, "high": 55.46, "low": 46.75, "brief": "岁25"}, {"age": 26, "open": 50.64, "close": 52.89, "high": 56.65, "low": 47.65, "brief": "岁26"}, {"age": 27, "open": 52.89, "close": 54.8, "high": 55.18, "low": 52.22, "brief": "岁27"}, {"age": 28, "open": 54.8, "close": 58.05, "high": 59.23, "low": 52.96, "brief": "岁28"}, {"age": 29, "open": 58.05, "close": 57.8, "high": 60.53, "low": 57.76, "brief": "岁29"}, {"age": 30, "open": 57.8, "close": 56.04, "high": 60.17, "low": 52.08, "brief": "岁30"}, {"age": 31, "open": 56.04, "close": 55.52, "high": 58.05, "low": 53.26, "brief": "岁31"}, {"age": 32, "open": 55.52, "close": 58.47, "high": 60.54, "low": 55.06, "brief": "岁32"}, {"age": 33, "open": 58.47, "close": 58.98, "high": 60.73, "low": 58.31, "brief": "岁33"}, {"age": 34, "open": 58.98, "close": 59.0, "high": 62.8, "low": 58.18, "brief": "岁34"}, {"age": 35, "open": 59.0, "close": 61.4, "high": 62.7, "low": 58.04, "brief": "岁35"}, {"age": 36, "open": 61.4, "close": 63.88, "high": 64.27, "low": 57.7, "brief": "岁36"}, {"age": 37, "open": 63.88, "close": 62.36, "high": 65.28, "low": 61.7, "brief": "岁37"}, {"age": 38, "open": 62.36, "close": 61.59, "high": 64.52, "low": 58.75, "brief": "岁38"}, {"age": 39, "open": 61.59, "close": 62.22, "high": 64.03, "low": 59.35, "brief": "岁39"}, {"age": 40, "open": 62.22, "close": 60.69, "high": 64.18, "low": 58.67, "brief": "岁40"}, {"age": 41, "open": 60.69, "close": 61.27, "high": 64.69, "low": 60.33, "brief": "岁41"}, {"age": 42, "open": 61.27, "close": 63.26, "high": 66.91, "low": 61.12, "brief": "岁42"}, {"age": 43, "open": 63.26, "close": 62.05, "high": 65.72, "low": 61.62, "brief": "岁43"}, {"age": 44, "open": 62.05, "close": 64.4, "high": 64.53, "low": 58.7, "brief": "岁44"}, {"age": 45, "open": 64.4, "close": 62.68, "high": 65.16, "low": 59.32, "brief": "岁45"}, {"age": 46, "open": 62.68, "close": 62.46, "high": 63.44, "low": 58.85, "brief": "岁46"}, {"age": 47, "open": 62.46, "close": 60.35, "high": 64.89, "low": 56.36, "brief": "岁47"}, {"age": 48, "open": 60.35, "close": 58.97, "high": 60.72, "low": 56.33, "brief": "岁48"}, {"age": 49, "open": 58.97, "close": 56.87, "high": 59.66, "low": 54.14, "brief": "岁49"}, {"age": 50, "open": 56.87, "close": 56.8, "high": 59.13, "low": 54.57, "brief": "岁50"}, {"age": 51, "open": 56.8, "close": 59.67, "high": 61.65, "low": 54.54, "brief": "岁51"}, {"age": 52, "open": 59.67, "close": 60.35, "high": 60.85, "low": 58.55, "brief": "岁52"}, {"age": 53, "open": 60.35, "close": 62.31, "high": 64.37, "low": 56.37, "brief": "岁53"}, {"age": 54, "open": 62.31, "close": 61.94, "high": 65.95, "low": 60.35, "brief": "岁54"}, {"age": 55, "open": 61.94, "close": 60.31, "high": 65.16, "low": 58.82, "brief": "岁55"}, {"age": 56, "open": 60.31, "close": 59.64, "high": 63.63, "low": 58.23, "brief": "岁56"}, {"age": 57, "open": 59.64, "close": 59.27, "high": 60.44, "low": 58.55, "brief": "岁57"}, {"age": 58, "open": 59.27, "close": 57.34, "high": 61.09, "low": 54.26, "brief": "岁58"}, {"age": 59, "open": 57.34, "close": 58.03, "high": 58.81, "low": 53.48, "brief": "岁59"}, {"age": 60, "open": 58.03, "close": 56.12, "high": 60.49, "low": 55.44, "brief": "岁60"}, {"age": 61, "open": 56.12, "close": 59.19, "high": 61.02, "low": 53.24, "brief": "岁61"}, {"age": 62, "open": 59.19, "close": 62.15, "high": 65.64, "low": 56.26, "brief": "岁62"}, {"age": 63, "open": 62.15, "close": 64.62, "high": 66.02, "low": 58.99, "brief": "岁63"}, {"age": 64, "open": 64.62, "close": 66.08, "high": 69.69, "low": 64.55, "brief": "岁64"}, {"age": 65, "open": 66.08, "close": 64.46, "high": 68.41, "low": 60.57, "brief": "岁65"}, {"age": 66, "open": 64.46, "close": 62.15, "high": 67.73, "low": 58.62, "brief": "岁66"}, {"age": 67, "open": 62.15, "close": 60.72, "high": 63.98, "low": 58.54, "brief": "岁67"}, {"age": 68, "open": 60.72, "close": 63.23, "high": 64.78, "low": 57.44, "brief": "岁68"}, {"age": 69, "open": 63.23, "close": 65.09, "high": 65.94, "low": 59.46, "brief": "岁69"}, {"age": 70, "open": 65.09, "close": 64.88, "high": 67.76, "low": 62.4, "brief": "岁70"}, {"age": 71, "open": 64.88, "close": 61.96, "high": 68.35, "low": 59.08, "brief": "岁71"}, {"age": 72, "open": 61.96, "close": 60.31, "high": 62.35, "low": 57.65, "brief": "岁72"}, {"age": 73, "open": 60.31, "close": 62.58, "high": 63.37, "low": 59.86, "brief": "岁73"}, {"age": 74, "open": 62.58, "close": 64.41, "high": 66.81, "low": 59.7, "brief": "岁74"}, {"age": 75, "open": 64.41, "close": 62.75, "high": 65.07, "low": 61.15, "brief": "岁75"}, {"age": 76, "open": 62.75, "close": 63.92, "high": 64.65, "low": 62.36, "brief": "岁76"}, {"age": 77, "open": 63.92, "close": 66.38, "high": 67.1, "low": 60.78, "brief": "岁77"}, {"age": 78, "open": 66.38, "close": 66.37, "high": 67.13, "low": 62.55, "brief": "岁78"}, {"age": 79, "open": 66.37, "close": 63.79, "high": 68.7, "low": 62.2, "brief": "岁79"}, {"age": 80, "open": 63.79, "close": 63.75, "high": 66.18, "low": 62.54, "brief": "岁80"}, {"age": 81, "open": 63.75, "close": 60.96, "high": 64.1, "low": 59.34, "brief": "岁81"}, {"age": 82, "open": 60.96, "close": 63.63, "high": 65.82, "low": 60.26, "brief": "岁82"}, {"age": 83, "open": 63.63, "close": 65.93, "high": 66.82, "low": 60.4, "brief": "岁83"}, {"age": 84, "open": 65.93, "close": 66.27, "high": 68.58, "low": 64.1, "brief": "岁84"}, {"age": 85, "open": 66.27, "close": 63.43, "high": 68.76, "low": 61.18, "brief": "岁85"}, {"age": 86, "open": 63.43, "close": 65.97, "high": 66.92, "low": 61.39, "brief": "岁86"}, {"age": 87, "open": 65.97, "close": 66.39, "high": 69.52, "low": 64.89, "brief": "岁87"}, {"age": 88, "open": 66.39, "close": 66.41, "high": 67.5, "low": 63.43, "brief": "岁88"}, {"age": 89, "open": 66.41, "close": 65.2, "high": 69.49, "low": 64.85, "brief": "岁89"}, {"age": 90, "open": 65.2, "close": 64.58, "high": 66.83, "low": 60.98, "brief": "岁90"}, {"age": 91, "open": 64.58, "close": 64.44, "high": 66.5, "low": 61.36, "brief": "岁91"}, {"age": 92, "open": 64.44, "close": 64.24, "high": 65.23, "low": 60.31, "brief": "岁92"}, {"age": 93, "open": 64.24, "close": 65.16, "high": 67.66, "low": 61.91, "brief": "岁93"}, {"age": 94, "open": 65.16, "close": 65.51, "high": 67.63, "low": 63.43, "brief": "岁94"}, {"age": 95, "open": 65.51, "close": 62.85, "high": 67.15, "low": 58.89, "brief": "岁95"}, {"age": 96, "open": 62.85, "close": 60.91, "high": 64.65, "low": 58.12, "brief": "岁96"}, {"age": 97, "open": 60.91, "close": 57.82, "high": 63.69, "low": 55.66, "brief": "岁97"}, {"age": 98, "open": 57.82, "close": 59.35, "high": 59.73, "low": 56.75, "brief": "岁98"}, {"age": 99, "open": 59.35, "close": 59.35, "high": 59.47, "low": 56.21, "brief": "岁99"}, {"age": 100, "open": 59.35, "close": 59.75, "high": 62.47, "low": 57.4, "brief": "岁100"}]</script>
<script>
(function () {
document.getElementById('meta-generated').textContent =
new Date().toISOString().slice(0, 19).replace('T', ' ');
function parseKLine() {
var el = document.getElementById('kline-data');
if (!el) return [];
try {
var t = el.textContent.trim();
var data = JSON.parse(t || '[]');
return Array.isArray(data) ? data : [];
} catch (e) {
return [];
}
}
function parseChart() {
var el = document.getElementById('chart-data');
if (!el) return { palaces: [] };
try {
var data = JSON.parse(el.textContent.trim() || '{"palaces":[]}');
return data && typeof data === 'object' ? data : { palaces: [] };
} catch (e) {
return { palaces: [] };
}
}
function renderChartGrid() {
var wrap = document.getElementById('chart-grid');
if (!wrap) return;
var chart = parseChart();
var palaces = Array.isArray(chart.palaces) ? chart.palaces : [];
if (palaces.length !== 12) {
wrap.innerHTML = '<p class="placeholder">命盘数据不足:需12宫后才可绘制排盘图。</p>';
return;
}
var positionByBranch = {
'巳': [0,0], '午':[0,1], '未':[0,2], '申':[0,3],
'辰':[1,0], '酉':[1,3], '卯':[2,0], '戌':[2,3],
'寅':[3,0], '丑':[3,1], '子':[3,2], '亥':[3,3]
};
var grid = [[],[],[],[]];
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) grid[r][c] = null;
palaces.forEach(function (p) {
var pos = positionByBranch[p.branch];
if (pos) grid[pos[0]][pos[1]] = p;
});
function renderStars(stars) {
if (!Array.isArray(stars) || stars.length === 0) return '<small>无</small>';
return stars.map(function (s) {
if (!s || !s.name) return '';
var tag = s.mutagen ? '<span class="mutagen">[' + s.mutagen + ']</span>' : '';
return '<span>' + s.name + tag + '</span>';
}).join('、');
}
function oppositeBranch(b) {
var order = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
var i = order.indexOf(b);
if (i < 0) return null;
return order[(i + 6) % 12];
}
function renderMajorWithBorrow(palace, allPalaces) {
if (Array.isArray(palace.majorStars) && palace.majorStars.length > 0) {
return renderStars(palace.majorStars);
}
var ob = oppositeBranch(palace.branch);
var opp = null;
for (var j = 0; j < allPalaces.length; j++) {
if (allPalaces[j].branch === ob) { opp = allPalaces[j]; break; }
}
if (opp && Array.isArray(opp.majorStars) && opp.majorStars.length > 0) {
return '<span class="borrow-hint">借对宫「' + (opp.name || '') + '」</span>' + renderStars(opp.majorStars);
}
return renderStars(palace.majorStars);
}
var html = '';
for (var row = 0; row < 4; row++) {
for (var col = 0; col < 4; col++) {
if (row >= 1 && row <= 2 && col >= 1 && col <= 2) {
if (row === 1 && col === 1) {
html += '<div class="center-cell"><h3>紫微斗数命盘</h3>'
+ '<p>阳历:' + (chart.birthSolar || '-') + '</p>'
+ '<p>五行局:' + (chart.fiveElementsClass || '-') + '</p>'
+ '<p>命宫:' + (chart.lifePalaceName || '-') + ' · 身宫:' + (chart.bodyPalaceName || '-') + '</p>'
+ '<p>流年:' + (chart.targetYear || '-') + '</p></div>';
}
continue;
}
var palace = grid[row][col];
if (!palace) {
html += '<div class="palace-cell"></div>';
continue;
}
var isMing = palace.isLifePalace || palace.name === '命宫';
var klass = 'palace-cell' + (isMing ? ' life' : '') + (palace.isBodyPalace ? ' body' : '');
html += '<div class="' + klass + '">'
+ '<div class="palace-head"><span>' + (palace.stem || '') + (palace.branch || '') + '</span><span>' + (palace.decadalRange || '') + '</span></div>'
+ '<div class="palace-name">' + (palace.name || '-') + '</div>'
+ '<div class="palace-stars"><small>主星:</small>' + renderMajorWithBorrow(palace, palaces) + '</div>'
+ '<div class="palace-stars"><small>辅星:</small>' + renderStars(palace.minorStars) + '</div>'
+ '</div>';
}
}
wrap.innerHTML = html;
}
function drawKLine() {
var canvas = document.getElementById('kline-canvas');
var hint = document.getElementById('kline-hint');
if (!canvas) return;
var data = parseKLine();
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
var cw = rect.width || 800;
var ch = 280;
canvas.width = Math.floor(cw * dpr);
canvas.height = Math.floor(ch * dpr);
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, cw, ch);
var padL = 40, padR = 12, padT = 14, padB = 28;
var gw = cw - padL - padR;
var gh = ch - padT - padB;
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0, 0, cw, ch);
ctx.strokeStyle = 'rgba(201,169,98,0.15)';
ctx.lineWidth = 1;
for (var li = 0; li <= 4; li++) {
var y = padT + (gh / 4) * li;
ctx.beginPath();
ctx.moveTo(padL, y);
ctx.lineTo(cw - padR, y);
ctx.stroke();
ctx.fillStyle = '#9a9288';
ctx.font = '11px system-ui,sans-serif';
var val = 100 - li * 25;
ctx.fillText(String(val), 4, y + 4);
}
if (data.length === 0) {
ctx.fillStyle = '#9a9288';
ctx.font = '14px sans-serif';
ctx.fillText('暂无 K 线数据:请在 #kline-data 中填入 100 条记录后刷新。', padL, padT + gh / 2);
if (hint) hint.textContent = '数据条数:0';
return;
}
var n = data.length;
var slot = gw / Math.max(n, 1);
data.forEach(function (row, i) {
var open = Number(row.open);
var close = Number(row.close);
var high = Number(row.high);
var low = Number(row.low);
if (open !== open) open = 50;
if (close !== close) close = open;
if (high !== high) high = Math.max(open, close);
if (low !== low) low = Math.min(open, close);
high = Math.min(100, Math.max(0, high));
low = Math.min(100, Math.max(0, low));
open = Math.min(100, Math.max(0, open));
close = Math.min(100, Math.max(0, close));
function yScale(v) {
return padT + gh - (v / 100) * gh;
}
var cx = padL + (n === 1 ? gw / 2 : (i / (n - 1)) * gw);
var bw = Math.max(2, slot * 0.55);
ctx.strokeStyle = '#c4b89f';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx, yScale(high));
ctx.lineTo(cx, yScale(low));
ctx.stroke();
var yOpen = yScale(open);
var yClose = yScale(close);
var top = Math.min(yOpen, yClose);
var h = Math.max(Math.abs(yClose - yOpen), 2);
var bull = close >= open;
ctx.fillStyle = bull ? '#6b9e78' : '#a65d5d';
ctx.fillRect(cx - bw / 2, top, bw, h);
});
ctx.fillStyle = '#9a9288';
ctx.font = '11px sans-serif';
ctx.fillText('1岁', padL, ch - 8);
ctx.fillText(String(n) + '岁', cw - padR - 36, ch - 8);
if (hint) hint.textContent = '数据条数:' + n + ' · 请将鼠标悬停需求留给后续增强(当前离线静态图)';
}
function validateKLine(data) {
var result = [];
if (data.length !== 100) {
result.push({ ok: false, text: "K线条数必须为100条" });
return result;
}
var sortedData = data.slice().sort(function (a, b) {
return Number(a.age) - Number(b.age);
});
var ages = {};
var prevClose = null;
for (var i = 0; i < sortedData.length; i++) {
var row = sortedData[i];
var age = Number(row.age);
var open = Number(row.open);
var close = Number(row.close);
var high = Number(row.high);
var low = Number(row.low);
if (!Number.isFinite(age) || age < 1 || age > 100 || ages[age]) {
result.push({ ok: false, text: "age必须覆盖1-100且唯一" });
break;
}
ages[age] = true;
if ([open, close, high, low].some(function (x) { return !Number.isFinite(x) || x < 0 || x > 100; })) {
result.push({ ok: false, text: "存在越界数值(需在0-100)" });
break;
}
if (high < Math.max(open, close) || low > Math.min(open, close)) {
result.push({ ok: false, text: "OHLC关系不合法(high/low与open/close冲突)" });
break;
}
if (age === 1 && Math.abs(open - 50) > 1e-6) {
result.push({ ok: false, text: "age=1时open必须等于50" });
break;
}
if (prevClose !== null && Math.abs(open - prevClose) > 1e-6) {
result.push({ ok: false, text: "K线不连续:open[n]必须等于close[n-1]" });
break;
}
prevClose = close;
}
if (result.length === 0) {
result.push({ ok: true, text: "K线100条与连续性校验通过" });
}
return result;
}
function renderIntegrity() {
var summary = document.getElementById('integrity-summary');
var list = document.getElementById('integrity-checks');
if (!summary || !list) return;
var checks = [];
var data = parseKLine();
checks = checks.concat(validateKLine(data));
var metaIds = ['meta-solar', 'meta-birthplace', 'meta-local-time', 'meta-true-solar-time', 'meta-time-correction', 'meta-geo-source', 'meta-gender', 'meta-five', 'meta-target-year'];
var metaMissing = metaIds.some(function (id) {
var el = document.getElementById(id);
return !el || /请填入/.test((el.textContent || '').trim());
});
checks.push({
ok: !metaMissing,
text: metaMissing ? '元信息未完整填充(阳历/性别/五行局/分析年)' : '元信息完整'
});
var failed = checks.filter(function (x) { return !x.ok; });
summary.textContent = failed.length === 0
? '完整性检查通过:可作为离线报告归档。'
: ('完整性检查失败:' + failed.length + ' 项不通过,请先修正后再交付。');
summary.className = failed.length === 0 ? '' : 'placeholder';
list.innerHTML = '';
checks.forEach(function (item) {
var li = document.createElement('li');
li.className = item.ok ? 'ok' : 'fail';
li.textContent = (item.ok ? '✓ ' : '✗ ') + item.text;
list.appendChild(li);
});
}
window.addEventListener('resize', function () {
drawKLine();
});
drawKLine();
renderChartGrid();
renderIntegrity();
})();
</script>
</body>
</html>import json
import pathlib
import subprocess
import sys
import tempfile
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))
import render_prompts as rp # noqa: E402
import ziwei_offline as zw # noqa: E402
class RenderPromptsTests(unittest.TestCase):
def setUp(self):
self.chart = zw.generate_chart(
1996, 1, 6, 11, "female", 2026, minute=30, use_true_solar_time=False
)
self.payload = zw.build_payload(self.chart, 2026)
def test_render_creates_prompt_files_and_manifest(self):
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp)
manifest = rp.render_all_prompts(self.payload, out, work_root=out)
prompts_dir = out / "prompts"
for name in (
"natal.system.md",
"natal.user.md",
"yearly.system.md",
"yearly.user.md",
"kline.system.md",
"kline.user.md",
):
self.assertTrue((prompts_dir / name).exists(), msg=name)
self.assertTrue((out / "prompt-manifest.json").exists())
self.assertEqual(len(manifest["promptHashes"]), 6)
self.assertEqual(manifest["version"], "2")
self.assertTrue(manifest["payloadSummary"]["hasKlineData"])
def test_kline_prompt_forbids_model_ohlc(self):
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp)
manifest = rp.render_all_prompts(self.payload, out, work_root=out)
user = (out / "prompts" / "kline.user.md").read_text(encoding="utf-8")
system = (out / "prompts" / "kline.system.md").read_text(encoding="utf-8")
outputs = manifest["agentContract"]["requiredOutputs"]
kline = next(o for o in outputs if o["id"] == "klineBrief")
self.assertIn("不得返回或改写 open、close、high、low", user)
self.assertIn("不得由模型自由生成或改写", system)
self.assertTrue(kline["path"].endswith("kline-brief.json"))
self.assertIn("不得返回或改写 open/high/low/close", kline["note"])
def test_manifest_assemble_uses_payload_kline_data_by_default(self):
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp)
manifest = rp.render_all_prompts(self.payload, out, work_root=out)
command = manifest["assemble"]["command"]
self.assertIn("--kline-brief-json", command)
self.assertNotIn("--kline-json", command)
def test_cli_entrypoint(self):
with tempfile.TemporaryDirectory() as tmp:
tmp_path = pathlib.Path(tmp)
payload_path = tmp_path / "payload.json"
payload_path.write_text(
json.dumps(self.payload, ensure_ascii=False), encoding="utf-8"
)
proc = subprocess.run(
[
sys.executable,
str(ROOT / "tools" / "render_prompts.py"),
"--payload-json",
str(payload_path),
"--out-dir",
str(tmp_path / "work"),
"--work-root",
str(tmp_path / "work"),
],
cwd=ROOT,
text=True,
capture_output=True,
)
self.assertEqual(proc.returncode, 0, msg=proc.stderr)
out = json.loads(proc.stdout.strip())
self.assertTrue(pathlib.Path(out["manifest"]).exists())
class PromptStructureFailureTests(unittest.TestCase):
def setUp(self):
self.chart = zw.generate_chart(
1996, 1, 6, 11, "female", 2026, minute=30, use_true_solar_time=False
)
self.payload = zw.build_payload(self.chart, 2026)
self.base_md = (ROOT / "prompts.md").read_text(encoding="utf-8")
def _render_with_md(self, md_text: str):
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp)
prompts_md = out / "prompts.md"
prompts_md.write_text(md_text, encoding="utf-8")
return rp.render_all_prompts(
self.payload, out, prompts_md=prompts_md, work_root=out
)
def test_missing_main_section_raises(self):
md = self.base_md.split("## 1. 紫微命盘综合批注(系统提示词)")[0]
md += "## 2. 年度流年运势(系统提示词)\n\n```\nsystem\n```\n\n### 2b.\n\n```\nuser\n```\n"
md += "\n## 3. 人生 K 线文字补充(系统提示词)\n\n```\nsystem\n```\n"
with self.assertRaises(KeyError) as ctx:
self._render_with_md(md)
self.assertIn("1. 紫微命盘综合批注", str(ctx.exception))
def test_renamed_section_title_raises(self):
md = self.base_md.replace(
"## 1. 紫微命盘综合批注(系统提示词)",
"## 1. 综合批注(已改名)",
1,
)
with self.assertRaises(KeyError) as ctx:
self._render_with_md(md)
self.assertIn("1. 紫微命盘综合批注", str(ctx.exception))
def test_missing_fenced_code_block_raises(self):
marker = "## 1. 紫微命盘综合批注(系统提示词)"
before, rest = self.base_md.split(marker, 1)
after = rest.split("## 2. 年度流年运势(系统提示词)", 1)[1]
md = before + marker + "\n\n无代码块正文。\n\n## 2. 年度流年运势(系统提示词)" + after
with self.assertRaises(ValueError) as ctx:
self._render_with_md(md)
self.assertIn("综合批注 system 提示词块为空", str(ctx.exception))
def test_multiple_similar_kline_sections_use_first_prefix_match(self):
extra = (
"\n\n## 3. 人生 K 线(备用说明)\n\n"
"```\n备用 system,不得采用\n```\n"
)
marker = "## 3. 人生 K 线文字补充(系统提示词)"
parts = self.base_md.split(marker, 1)
section_body, tail = parts[1].split("## 4.", 1)
md = parts[0] + marker + section_body + extra + "\n\n## 4." + tail
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp)
prompts_md = out / "prompts.md"
prompts_md.write_text(md, encoding="utf-8")
rp.render_all_prompts(
self.payload, out, prompts_md=prompts_md, work_root=out
)
system = (out / "prompts" / "kline.system.md").read_text(encoding="utf-8")
self.assertNotIn("备用 system,不得采用", system)
self.assertIn("不得由模型自由生成或改写", system)
class ReadmeConsistencyTests(unittest.TestCase):
def test_readme_documents_render_and_generate_commands(self):
readme = (ROOT / "README.md").read_text(encoding="utf-8")
self.assertIn("render_prompts.py", readme)
self.assertIn("generate_report.py", readme)
self.assertIn("prompt-manifest.json", readme)
self.assertIn("klineData", readme)
if __name__ == "__main__":
unittest.main()
{
"广东省佛山市顺德区": { "longitude": 113.2932, "latitude": 22.8054 },
"广东省广州市": { "longitude": 113.2644, "latitude": 23.1291 },
"北京市": { "longitude": 116.4074, "latitude": 39.9042 },
"上海市": { "longitude": 121.4737, "latitude": 31.2304 },
"深圳市": { "longitude": 114.0579, "latitude": 22.5431 },
"杭州市": { "longitude": 120.1551, "latitude": 30.2741 },
"成都市": { "longitude": 104.0665, "latitude": 30.5728 },
"重庆市": { "longitude": 106.5516, "latitude": 29.5630 },
"西安市": { "longitude": 108.9398, "latitude": 34.3416 },
"兰州市": { "longitude": 103.8343, "latitude": 36.0611 },
"乌鲁木齐市": { "longitude": 87.6168, "latitude": 43.8256 },
"喀什市": { "longitude": 75.9898, "latitude": 39.4704 },
"拉萨市": { "longitude": 91.1322, "latitude": 29.6604 },
"昆明市": { "longitude": 102.8329, "latitude": 24.8801 },
"海口市": { "longitude": 110.3312, "latitude": 20.0311 },
"哈尔滨市": { "longitude": 126.6425, "latitude": 45.7560 },
"长春市": { "longitude": 125.3235, "latitude": 43.8171 },
"呼和浩特市": { "longitude": 111.7492, "latitude": 40.8426 },
"南京市": { "longitude": 118.7969, "latitude": 32.0603 },
"厦门市": { "longitude": 118.0894, "latitude": 24.4798 },
"大连市": { "longitude": 121.6147, "latitude": 38.9140 }
}
#!/usr/bin/env bash
# 可选:iztro + 与 Python 相同的真太阳时口径(推荐)
# 需: Node + 本目录 npm install
#
# 默认 IZTRO_MODE=true-solar:先用 Python 算真太阳时,再传给 iztro 安星,再拼上下文。
# 环境变量:SOLAR TIME GENDER 必填;BIRTHPLACE 可选;TARGET_YEAR 默认 2026
#
# 仅钟表、不要真太阳(与 App 输入一致):IZTRO_MODE=raw-clock ./tools/report_payload_iztro.sh -- --solar ... --time ... --gender ...
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
TARGET="${TARGET_YEAR:-2026}"
MODE="${IZTRO_MODE:-true-solar}"
if [[ "$MODE" == "true-solar" ]]; then
: "${SOLAR:?}" "${TIME:?}" "${GENDER:?}"
PY_ARGS=(--solar "$SOLAR" --time "$TIME" --gender "$GENDER" --target-year "$TARGET" --emit-iztro-birth-json)
[[ -n "${BIRTHPLACE:-}" ]] && PY_ARGS+=(--birthplace "$BIRTHPLACE")
[[ "${DISABLE_TRUE_SOLAR:-}" == "1" ]] && PY_ARGS+=(--disable-true-solar-time)
python3 tools/ziwei_offline.py "${PY_ARGS[@]}" | node tools/chart_iztro.cjs --birth-json - | python3 tools/ziwei_offline.py --from-chart-json --target-year "$TARGET" --format json
else
exec node tools/chart_iztro.cjs "$@" | python3 tools/ziwei_offline.py --from-chart-json --target-year "$TARGET" --format json
fi
[
{
"age": 1,
"brief": "波澜不惊"
},
{
"age": 2,
"brief": "波澜不惊"
},
{
"age": 3,
"brief": "波澜不惊"
},
{
"age": 4,
"brief": "波澜不惊"
},
{
"age": 5,
"brief": "波澜不惊"
},
{
"age": 6,
"brief": "波澜不惊"
},
{
"age": 7,
"brief": "波澜不惊"
},
{
"age": 8,
"brief": "平稳向好"
},
{
"age": 9,
"brief": "平稳向好"
},
{
"age": 10,
"brief": "平稳向好"
},
{
"age": 11,
"brief": "平稳向好"
},
{
"age": 12,
"brief": "平稳向好"
},
{
"age": 13,
"brief": "平稳向好"
},
{
"age": 14,
"brief": "平稳向好"
},
{
"age": 15,
"brief": "平稳向好"
},
{
"age": 16,
"brief": "平稳向好"
},
{
"age": 17,
"brief": "平稳向好"
},
{
"age": 18,
"brief": "平稳向好"
},
{
"age": 19,
"brief": "平稳向好"
},
{
"age": 20,
"brief": "稳中有进"
},
{
"age": 21,
"brief": "平稳向好"
},
{
"age": 22,
"brief": "稳中有进"
},
{
"age": 23,
"brief": "稳中有进"
},
{
"age": 24,
"brief": "稳中有进"
},
{
"age": 25,
"brief": "稳中有进"
},
{
"age": 26,
"brief": "稳中有进"
},
{
"age": 27,
"brief": "稳中有进"
},
{
"age": 28,
"brief": "平稳向好"
},
{
"age": 29,
"brief": "平稳向好"
},
{
"age": 30,
"brief": "稳中有进"
},
{
"age": 31,
"brief": "稳中有进"
},
{
"age": 32,
"brief": "稳中有进"
},
{
"age": 33,
"brief": "稳中有进"
},
{
"age": 34,
"brief": "旺势上扬"
},
{
"age": 35,
"brief": "稳中有进"
},
{
"age": 36,
"brief": "人生峰顶"
},
{
"age": 37,
"brief": "人生峰顶"
},
{
"age": 38,
"brief": "人生峰顶"
},
{
"age": 39,
"brief": "人生峰顶"
},
{
"age": 40,
"brief": "旺势上扬"
},
{
"age": 41,
"brief": "旺势上扬"
},
{
"age": 42,
"brief": "旺势上扬"
},
{
"age": 43,
"brief": "平稳向好"
},
{
"age": 44,
"brief": "平稳向好"
},
{
"age": 45,
"brief": "平稳向好"
},
{
"age": 46,
"brief": "平稳向好"
},
{
"age": 47,
"brief": "平稳向好"
},
{
"age": 48,
"brief": "平稳向好"
},
{
"age": 49,
"brief": "平稳向好"
},
{
"age": 50,
"brief": "平稳向好"
},
{
"age": 51,
"brief": "平稳向好"
},
{
"age": 52,
"brief": "波澜不惊"
},
{
"age": 53,
"brief": "波澜不惊"
},
{
"age": 54,
"brief": "波澜不惊"
},
{
"age": 55,
"brief": "波澜不惊"
},
{
"age": 56,
"brief": "平稳向好"
},
{
"age": 57,
"brief": "平稳向好"
},
{
"age": 58,
"brief": "稳中有进"
},
{
"age": 59,
"brief": "平稳向好"
},
{
"age": 60,
"brief": "平稳向好"
},
{
"age": 61,
"brief": "平稳向好"
},
{
"age": 62,
"brief": "平稳向好"
},
{
"age": 63,
"brief": "旺势上扬"
},
{
"age": 64,
"brief": "稳中有进"
},
{
"age": 65,
"brief": "稳中有进"
},
{
"age": 66,
"brief": "稳中有进"
},
{
"age": 67,
"brief": "稳中有进"
},
{
"age": 68,
"brief": "稳中有进"
},
{
"age": 69,
"brief": "稳中有进"
},
{
"age": 70,
"brief": "旺势上扬"
},
{
"age": 71,
"brief": "旺势上扬"
},
{
"age": 72,
"brief": "旺势上扬"
},
{
"age": 73,
"brief": "平稳向好"
},
{
"age": 74,
"brief": "平稳向好"
},
{
"age": 75,
"brief": "平稳向好"
},
{
"age": 76,
"brief": "平稳向好"
},
{
"age": 77,
"brief": "平稳向好"
},
{
"age": 78,
"brief": "平稳向好"
},
{
"age": 79,
"brief": "平稳向好"
},
{
"age": 80,
"brief": "平稳向好"
},
{
"age": 81,
"brief": "平稳向好"
},
{
"age": 82,
"brief": "平稳向好"
},
{
"age": 83,
"brief": "平稳向好"
},
{
"age": 84,
"brief": "平稳向好"
},
{
"age": 85,
"brief": "波澜不惊"
},
{
"age": 86,
"brief": "波澜不惊"
},
{
"age": 87,
"brief": "波澜不惊"
},
{
"age": 88,
"brief": "波澜不惊"
},
{
"age": 89,
"brief": "波澜不惊"
},
{
"age": 90,
"brief": "谨慎守成"
},
{
"age": 91,
"brief": "谨慎守成"
},
{
"age": 92,
"brief": "波澜不惊"
},
{
"age": 93,
"brief": "谨慎守成"
},
{
"age": 94,
"brief": "波澜不惊"
},
{
"age": 95,
"brief": "波澜不惊"
},
{
"age": 96,
"brief": "波澜不惊"
},
{
"age": 97,
"brief": "波澜不惊"
},
{
"age": 98,
"brief": "波澜不惊"
},
{
"age": 99,
"brief": "波澜不惊"
},
{
"age": 100,
"brief": "波澜不惊"
}
]Related skills
FAQ
Does it require Node.js?
No, the default recommended path A uses pure Python 3 with no Node or npm; an optional Node/iztro path B exists only when star placements must match the iztro app.
What date range is supported?
The offline tool supports solar dates from 1900-01-31 to 2100-12-31; dates outside that range require supplied lunar/chart context or it stops rather than invent placements.