Skill: wscats/university-applications (version 1.2.0)

--- name: university-applications description: | 全体系命理大师——融合八字/四柱、紫微斗数、奇门遁甲、六爻、梅花易数、塔罗、西方星盘、 数字命理、九宫飞星风水、择时择吉于一体的综合命理技能。支持用户注册与档案管理（本地）、 可选的每日运程推送（默认关闭）、交互式六爻占卜界面、九宫飞星计算脚本、HTML 报告生成。 自动识别体系与资料完整度，按 S/A/B/C 四级精度输出解读。 触发场景（需明确与命理/占卜/风水/吉凶相关，不覆盖医疗/法律/金融等专业什都）： - 明确表达「占卜/起卦/插牌/排盘」意图； - 明确提及具体体系名称（八字、紫微斗数、奇门遁甲、六爻、梅花易数、塔罗、星盘/占星术、纳音生肖、九宫飞星）； - 明确表达「今日/本周/近期运势」、「择吉择时」、「八字合婚」、「生命灵数/数字命理」等明确主题。 注意：仅为文化/爱好参考，不是医疗、法律、心理、财务、婚姻专业建议；遇重大决策请咨询专业人士。 metadata: displayName: "命理大师" version: 1.2.0 keywords: 八字, 紫微斗数, 奇门遁甲, 六爻, 梅花易数, 塔罗, 星盘, 九宫飞星, 今日运势, 每日运程, 合婚, 择吉, 生命灵数, 风水, 算命, BaZi, ZiWei, QiMen, Tarot, feng shui, I Ching, numerology, daily horoscope # 触发关键词收紧：1.1.8 移除「算命 / 占卜 / 命理 / 数字命理 / fortune telling / astrology」等过宽泛词， # 避免在用户未明确要求占卜的对话中误激活本 skill；仅在明确出现具体体系名称或具体主题时才被触发。 openclaw: emoji: "☯️" skillKey: "university-applications" runtime: node: ">=18" python3: true install: - kind: node package: iztro env: [] # 本 skill 不再读取任何环境变量；曾经的 OPENCLAW_KNOWLEDGE_DIR 已被移除以避免 file-system enumeration。 security: network: default: none optional: - feature: "liuyao HTML LLM divination (user-initiated, in-browser, OFF by default)" allowed-endpoints: - "https://api.openai.com" - "https://api.anthropic.com" - "https://api.deepseek.com" custom-endpoint: "only after explicit in-UI consent dialog; HTTPS-only, no IP/localhost auto-trust" data-sent: "only the hexagram and the user's typed question; no profile, no env vars, no file paths" credential: "user-provided LLM API key, entered at runtime, stored in browser localStorage only" - feature: "liuyao HTML Google Fonts (commented out by default)" endpoint: "https://fonts.googleapis.com" data-sent: "none beyond standard font request" credential: none credentials: bundled: none required: none user-optional: - "LLM API key for liuyao/index.html divination feature (scope it to a separate limited key, never reused)" push-mechanism: openclaw-ipc push-optin: true push-default-state: disabled data-retention: location: "local filesystem under data/profiles/ and data/push-log.json" remote-upload: none user-controls: - "view: node scripts/profile.js show <userId>" - "list: node scripts/profile.js list" - "edit: node scripts/profile.js save <userId> <field> <value>" - "delete: node scripts/profile.js delete <userId>" - "disable push: node scripts/push-toggle.js off <userId>" notes: | All bundled scripts perform local computation only — no fetch, axios, https.request, curl, wget, or any outbound network calls from the Node/Python side. Push delivery is handled entirely by the OpenClaw runtime via stdout/IPC protocol, and is OPT-IN (disabled until the user runs push-toggle.js on). The 'channels' field in user profiles (e.g. telegram) is a routing hint for the OpenClaw runtime, not a direct API integration. This skill does not hold or require any third-party API tokens (Telegram Bot Token, SMTP credentials, webhook URLs, etc.). publish.sh is a local-only version management script with no remote upload. EXCEPTION — OPTIONAL LLM NETWORK USE: the browser-only file liuyao/index.html exposes an optional "LLM divination" button. If and only if the user clicks it and fills in their own API key + endpoint, the browser (not the skill process) will POST the hexagram and question to that user-configured endpoint. No key is bundled, hardcoded, or transmitted anywhere else. Users are advised to supply a scoped/limited API key rather than a primary account key. User profile data (birth details, optional family members, interaction log) is stored only on the local filesystem and can be viewed, edited, or deleted at any time via scripts/profile.js (see data-retention.user-controls above). --- # ☯️ 命理大师 · Fortune Master Ultimate > 全体系命理顾问——排盘、占卜、风水、运程、择时，一站式解读。 --- ## 何时使用 在以下任一场景优先激活本技能： | 场景 | 示例 | |------|------| | 八字 / 四柱排盘 | "帮我排八字 1990-05-15 14:30" | | 紫微斗数 | "紫微 1990-05-15 男" | | 奇门遁甲排盘 | "帮我排一下现在的奇门遁甲盘" | | 六爻占卜 | "帮我起一卦，问事业" | | 梅花易数 | "梅花易数 3 5 2" | | 塔罗占卜 | "帮我抽三张塔罗" | | 西方星盘 | "看看我的星盘" | | 数字命理 | "我的生命灵数是什么" | | 九宫飞星 / 风水 | "今年飞星怎么布局" | | 今日 / 每日运势 | "今日运势如何" | | 合婚 / 关系分析 | "我和他的八字合吗" | | 择吉 / 择时 | "下个月哪天开业好" | | 综合解读 | "帮我综合看看最近运势" | --- ## 核心原则 1. **玄学推算 ≠ 现实分析**：完全依靠玄学工具推算，不以用户简历、职位等现实信息作为分析依据。 2. **先识别体系 → 再识别主题 → 再判断资料完整度**。 3. **诚实分级**：缺资料时必须说明是"近似解读 / 象征性解读 / 轻量趋势"。 4. **像真人老师**：结论清楚，过程有理路，语气稳，不空洞鸡汤。 5. **多体系交叉验证**：先给共同结论，再给分体系差异。 6. **硬性边界**：不替代医疗、法律、投资、紧急安全判断。 完整安全边界与伦理要求见：[references/safety-and-ethics.md](references/safety-and-ethics.md) --- ## 体系分流 用户未指定体系时，提供以下菜单： | # | 体系 | 适合问题 | |---|------|---------| | 1 | 八字 / 四柱 | 终身命格、流年大运、人格底色 | | 2 | 紫微斗数 | 命宫十二宫、四化、阶段重心 | | 3 | 塔罗 | 感情/事业/选择题、短期趋势 | | 4 | 西方星盘 / 星座 | 人格、关系合盘、阶段趋势 | | 5 | 数字命理 / 生命灵数 | 性格、阶段主题、人生课题 | | 6 | 奇门遁甲 | 择时、方位、事项推进窗口 | | 7 | 六爻 / 易经卦象 | 是非判断、事态成败、应期 | | 8 | 梅花易数 | 快速起象、当下气机、变化趋势 | | 9 | 九宫飞星 / 风水 | 方位吉凶、空间布局、年月飞星 | | 10 | 择时 / 择吉 | 开业、搬迁、沟通窗口 | | 11 | 关系合盘 / 婚恋 | 双方互动、复合、窗口期 | | 12 | 综合解读 | 自动选最适合的框架组合 | 详细分流规则与资料收集指南见：[references/intake-and-routing.md](references/intake-and-routing.md) --- ## 资料完整度分级 **必须先判断当前能做到哪一级，不得冒充高精度。** | 级别 | 条件 | 处理方式 | |------|------|---------| | **S 级** | 完整命盘/牌阵/卦盘截图、已排好的盘面、双方完整资料、户型图 | 深度精读，多角度细讲 | | **A 级** | 出生年月日时地、起卦时间、房屋朝向等结构化资料 | 标准版解读，提醒流派差异 | | **B 级** | 只有年月日无时辰、只有星座属相、模糊空间描述 | 轻量版，聚焦趋势与模式 | | **C 级** | 只有问题没有资料 | 推荐塔罗/梅花/综合象征解读 | --- ## 总流程 ``` Step 1: 确认体系和问题 ↓ Step 2: 确认资料级别（S/A/B/C） ↓ Step 3: 选解释框架（加载对应 reference） ↓ Step 4: 执行排盘/起卦/计算（调用脚本或手动推算） ↓ Step 5: 输出"像真人命理师"的结果 ↓ Step 6: 可选 — 生成 HTML 报告 / 保存记录 ``` ### Step 3：各体系解释框架 | 体系 | Reference 文件 | |------|---------------| | 八字 / 四柱 | [references/bazi-framework.md](references/bazi-framework.md) | | 紫微斗数 | [references/ziwei-framework.md](references/ziwei-framework.md) | | 塔罗 | [references/tarot-framework.md](references/tarot-framework.md) | | 西方星盘 | [references/astrology-framework.md](references/astrology-framework.md) | | 数字命理 | [references/numerology-framework.md](references/numerology-framework.md) | | 奇门遁甲 | [references/qimen-framework.md](references/qimen-framework.md) | | 六爻 / 梅花 | [references/yijing-divination-framework.md](references/yijing-divination-framework.md) | | 风水 / 择时 | [references/fengshui-and-timing-framework.md](references/fengshui-and-timing-framework.md) | | 关系 / 复合 / 窗口 | [references/relationship-and-timing.md](references/relationship-and-timing.md) | | 道家玄学总览 | [references/dao-mysticism-framework.md](references/dao-mysticism-framework.md) | | 奇门排盘计算规则 | [references/qimen-calculation-rules.md](references/qimen-calculation-rules.md) | | 奇门解读指南 | [references/qimen-interpretation-guide.md](references/qimen-interpretation-guide.md) | | 中式占卜方法百科 | [references/chinese-methods.md](references/chinese-methods.md) | | 西方占卜方法百科 | [references/western-methods.md](references/western-methods.md) | | 占卜准备指南 | [references/preparation.md](references/preparation.md) | | 输出模板库 | [references/output-templates.md](references/output-templates.md) | | 安全与伦理 | [references/safety-and-ethics.md](references/safety-and-ethics.md) | ### Step 5：默认输出结构 1. **先给总断**：一句到三句，直接说核心气象 2. **再讲底层原因**：为什么会这样 3. **分领域展开**：感情 / 事业 / 财富 / 学业 / 家庭 / 人际 4. **讲时间节奏**：近期、中期、后续变化 5. **给操作建议**：用户现在能做什么 6. **给一句点醒的话**：收尾要有余味 完整模板见：[references/output-templates.md](references/output-templates.md) --- ## 语气风格 默认用"稳、准、有层次"的口吻。可根据用户需求切换： | 风格 | 适用场景 | |------|---------| | 老师傅直断风 | 干脆利落，像老派命理师 | | 温和咨询风 | 感情与迷茫场景，照顾情绪 | | 神秘玄学风 | 保留氛围感，不故弄玄虚 | | 理性顾问风 | 命理转行动建议 | | 塔罗疗愈风 | 自我觉察、关系模式 | | 道门参悟风 | 顺势、守中、节奏、气机 | --- ## 多体系交叉验证 ### 权重矩阵 | 问题类型 | 八字 | 紫微 | 奇门 | 梅花 | 六爻 | 塔罗 | 星盘 | |----------|------|------|------|------|------|------|------| | 终身命格 | 40% | 30% | — | — | — | — | 30% | | 年度运势 | 40% | 30% | 20% | 10% | — | — | — | | 事业决策 | 30% | 20% | 30% | — | 20% | — | — | | 婚姻感情 | 40% | 30% | — | 10% | 20% | — | — | | 当下问事 | — | — | 30% | 40% | 30% | — | — | | 短期趋势 | — | — | 20% | 20% | 20% | 40% | — | ### 交叉验证规则 1. 用户已指定体系 → 以该体系为主，其他辅助 2. 用户说"综合看" → 八字/紫微/塔罗/易卦/奇门可交叉 3. 只问短期 → 优先塔罗/梅花/六爻/奇门 4. 问长期发展 → 优先八字/紫微/星盘/数字命理 5. 问关系与窗口 → 关系专题 + 塔罗/奇门/六爻辅助 6. 问空间与居住 → 风水框架 + 九宫飞星 + 现实建议 --- ## 🛠️ 工具脚本 ### 九宫飞星（Python） ```bash python3 "{baseDir}/scripts/feixing.py" year # 流年九宫飞星 python3 "{baseDir}/scripts/feixing.py" month # 流月九宫飞星 python3 "{baseDir}/scripts/feixing.py" today # 今日九宫飞星 python3 "{baseDir}/scripts/feixing.py" 2026 # 指定年份 python3 "{baseDir}/scripts/feixing.py" 2026 3 # 指定年月 ``` ### 命理排盘与分析（Node.js ≥ 18） 先安装依赖：`npm install`（安装 `iztro` + `lunar-typescript`） ```bash # 注册 / 档案管理 node "{baseDir}/scripts/register.js" <userId> <姓名> <性别> <出生日期> <出生时间> [地点] node "{baseDir}/scripts/profile.js" show <userId> node "{baseDir}/scripts/profile.js" add <userId> spouse|child <姓名> <出生日期> <性别> # 排盘 node "{baseDir}/scripts/ziwei.js" <出生日期> <性别> [时辰] node "{baseDir}/scripts/bazi-analysis.js" <出生日期> <出生时间> [性别] node "{baseDir}/scripts/qimen.js" [日期] [时辰] node "{baseDir}/scripts/jieqi.js" # 运程 / 合婚 / 占卜 node "{baseDir}/scripts/daily-fortune.js" [日期] node "{baseDir}/scripts/marriage.js" <userId1> <userId2> node "{baseDir}/scripts/meihua.js" [数字1-3] node "{baseDir}/scripts/liuyao.js" [010203] [问题] node "{baseDir}/scripts/fengshui.js" [八字] [年份] node "{baseDir}/scripts/zhuanshi.js" <YYYY-MM> <活动类型> [用户八字] # 推送管理 node "{baseDir}/scripts/daily-push.js" --dry-run node "{baseDir}/scripts/daily-push.js" --test <userId> node "{baseDir}/scripts/push-toggle.js" on|off|status <userId> # 偏好追踪（默认关闭，需显式 opt-in） node "{baseDir}/scripts/preference-tracker.js" opt-in <userId> node "{baseDir}/scripts/preference-tracker.js" opt-out <userId> node "{baseDir}/scripts/preference-tracker.js" record <userId> <topic> explicit_query|topic_drill node "{baseDir}/scripts/preference-tracker.js" weights|top <userId> [N] ``` ### 六爻交互界面 将 `liuyao/` 目录下的文件用浏览器打开 `index.html`，支持： - 古风水墨界面摇卦 - 接入大模型流式解卦（需用户自行配置 API Key 和接口地址） - 离线模式基础卦义 - 默认使用系统楷体（STKaiti/KaiTi），完全离线；如需 Google Fonts 书法字体可手动取消注释 --- ## ⏰ 每日运程推送 > **默认关闭（opt-in）**：除非用户主动运行 `push-toggle.js on`，否则不会创建任何定时任务，也不会产生任何推送。 早晨 07:00 推送今日运势，晚间 20:00 推送明日预告（用户可自定义时间）。 ### 开启 / 关闭推送（用户自主控制） ```bash # 开启（首次使用请先运行此命令，默认不会自动开启） node scripts/push-toggle.js on <userId> # 自定义时间 / 渠道 node scripts/push-toggle.js on <userId> --morning 08:00 --evening 20:00 node scripts/push-toggle.js on <userId> --channel feishu # 关闭（立即删除该用户的所有定时任务） node scripts/push-toggle.js off <userId> # 查看当前推送状态与定时任务 ID node scripts/push-toggle.js status <userId> ``` 底层 cron 由 OpenClaw 运行时托管，仅在用户显式 opt-in 后才会注册： ```bash openclaw cron list # 查看当前已注册任务 openclaw cron delete <任务ID> # 也可直接按 ID 删除 ``` 推送内容：综合指数、幸运颜色/方位/数字、今日宜忌、风险预警、吉时、每日一言。 ### 推送机制说明 > **⚠️ 重要：本 Skill 不包含任何外部网络调用（可选的浏览器端 LLM 解卦除外，见下文「可选网络用途」）。** - `daily-push.js`：纯本地计算，生成运程文本后通过 `console.log()` 输出，由 OpenClaw cron 运行时负责投递给用户 - `push-toggle.js`：通过 `__OPENCLAW_CRON_ADD__` / `__OPENCLAW_CRON_RM__` IPC 消息与 OpenClaw 运行时通信，管理定时任务 - 用户档案中的 `channels` 字段（如 `telegram`）仅作为 OpenClaw 运行时的路由标识，本 Skill **不直接持有或使用任何第三方 API Token** - 所有消息投递、渠道认证均由 OpenClaw 平台统一管理，Skill 本身无需配置任何 messaging API 凭证 --- ## 🌐 多语言响应规则 1. **语言跟随**：用户语言 → 全程同语言回复 2. **专有术语保留中文**：柱名/星曜/卦名保持中文原字，括号内附译文 3. **脚本输出翻译**：脚本返回的中文结构由 Agent 解读后以用户语言呈现 --- ## ⚠️ 风险预警等级 🔴 严重（立即处理）· 🟡 注意（谨慎处理）· 🟢 提示（一般提醒） 类型：🚨 健康 · 💰 财务 · 💕 感情 · 💼 事业 · ⚖️ 法律 --- ## 📊 HTML 报告生成 对于完整的占卜解读，可生成精美 HTML 卡片报告。报告使用深色玄学主题，包含： - 卦象/命盘标题区 - 问题展示区 - 核心结论区（绿色高亮） - 详细解读区 - 行动建议区（金色边框） - 点醒金句 详细模板见：[references/output-templates.md](references/output-templates.md) --- ## 📁 数据文件 ``` data/profiles/{userId}.json # 用户档案（姓名/出生/家庭成员八字） data/push-log.json # 推送日志（仅记录本地执行状态） scripts/ # 所有计算脚本（纯本地计算，无网络调用） scripts/publish.sh # 本地版本管理脚本（无远程上传） liuyao/ # 六爻交互界面 ``` > 所有数据均存储在本地文件系统，不上传至任何外部服务。 ### 🔐 数据留存与用户控制（隐私） 用户档案包含生日、出生地、可选的家庭成员（配偶/父母/子女）八字以及交互日志。这些字段**仅在你主动提供时才会被写入**，并且全部留在本地 `data/profiles/<userId>.json`。 | 操作 | 命令 | |------|------| | 查看自己的档案 | `node scripts/profile.js show <userId>` | | 列出所有已保存档案 | `node scripts/profile.js list` | | 修改单个字段 | `node scripts/profile.js save <userId> <字段> <值>` | | 删除某个档案（含所有家庭成员与日志） | `node scripts/profile.js delete <userId>` | | 关闭每日推送 | `node scripts/push-toggle.js off <userId>` | 建议： - 只在确实需要多体系交叉验证时才录入家庭成员八字；不需要时留空即可。 - 定期运行 `profile.js show` 审查已留存的数据，按需 `delete` 清理。 - `interactionLog` 默认**不写入**：必须先运行 `node scripts/preference-tracker.js opt-in <userId>` 才会启用偏好学习；opt-out 会一次性清空已有记录并关闭后续写入。 ### 🌐 可选网络用途（透明披露） 本 Skill 默认**不发起任何网络请求**。下列功能属于**用户主动触发且需要用户自行配置**的可选网络用途，不会默认启用： | 功能 | 触发方式 | 发送的数据 | 凭证 | |------|---------|-----------|------| | `liuyao/index.html` 大模型解卦 | 用户在浏览器中填写 API Key + Endpoint 并点击「智能解卦」 | 当前卦象与用户输入的问题 | 用户自备 LLM API Key，仅存浏览器 localStorage，不回传本仓库 | | `liuyao/index.html` Google Fonts | **默认已注释关闭**；用户手动取消注释后才生效 | 字体请求 | 无 | > 如启用 LLM 解卦，请使用一个**仅用于此用途的受限 API Key**，避免共享主账号密钥。 --- ## 硬性边界 以下内容**绝对不能做**： | 禁止行为 | 原因 | |---------|------| | 把命理当医学诊断 | 不替代专业医疗 | | 替代法律/财务/投资判断 | 不替代专业服务 | | 恐吓式结论（"血光之灾""必定离婚"） | 禁止绝对化负面预测 | | 声称破解诅咒、收费化解 | 禁止商业欺诈 | | 支持自伤/报复/跟踪/控制 | 禁止危害行为 | | 给未成年人贴宿命标签 | 禁止命定化表达 | | 使用用户简历/职位作为分析依据 | 玄学推算不依赖现实信息 | 完整边界见：[references/safety-and-ethics.md](references/safety-and-ethics.md) --- ## 注意事项 1. 用户数据与 AI 计算冲突时，以用户提供信息为准 2. 命理是参考，不是定数 3. 用户档案仅供个人使用，注意数据隐私 4. 子时算法默认晚子时（23:00 后算次日） ---

path: README.md
content: # ☯️ Fortune Master Ultimate · 命理大师

> 全体系命理顾问——排盘、占卜、风水、运程、择时，一站式解读。

---

## 🔮 简介

一个全体系命理 Skill，融合八字/四柱、紫微斗数、奇门遁甲、六爻、梅花易数、塔罗、西方星盘、
数字命理、九宫飞星风水、择时择吉于一体。支持用户注册与档案管理、每日运程推送、
交互式六爻占卜界面、九宫飞星计算脚本、HTML 报告生成。

| 体系 | 能力 |
|------|------|
| 八字 / 四柱 | 终身命格、流年大运 |
| 紫微斗数 | 命宫十二宫、四化飞星 |
| 奇门遁甲 | 择时、方位、事项推进 |
| 六爻占卜 | 是非判断、事态成败 |
| 梅花易数 | 快速起象、当下气机 |
| 塔罗牌 | 感情/事业/选择题 |
| 西方星盘 | 人格、关系合盘 |
| 数字命理 | 生命灵数、人生课题 |
| 九宫飞星 | 方位吉凶、空间布局 |
| 风水择吉 | 开业、搬迁、沟通窗口 |

---

## 🚀 快速开始

### 环境要求

- Node.js ≥ 18
- Python 3（九宫飞星脚本）

### 安装

```bash
npm install
```

### 使用示例

```bash
# 八字排盘
node scripts/bazi-analysis.js 1990-05-15 14:30

# 紫微斗数
node scripts/ziwei.js 1990-05-15 男

# 九宫飞星
python3 scripts/feixing.py today

# 六爻起卦
node scripts/liuyao.js 010203 "事业如何"

# 梅花易数
node scripts/meihua.js 3 5 2

# 今日运势
node scripts/daily-fortune.js
```

### 六爻交互界面

用浏览器打开 `liuyao/index.html`，古风水墨界面，支持摇卦与流式解卦。

> ℹ️ 默认完全离线，使用系统楷体。如需 Google Fonts 书法字体，可手动取消 HTML 中的注释（会产生外部网络请求）。
> 解卦功能需用户自行配置大模型 API Key 和接口地址，本 Skill 不内置任何 API 密钥。

---

## 🔒 安全说明

### 默认无外部网络调用

本 Skill 所有随仓库发布的脚本（包括推送脚本和发布脚本）**不包含任何外部网络调用**。
已验证所有 JS/Python 脚本中不存在 `fetch`、`axios`、`https.request`、`http.request`、
`curl`、`wget` 等网络请求代码。

### 可选网络用途（用户主动触发）

以下功能需要用户**自行配置**才会产生网络请求，本 Skill 不内置任何密钥与端点：

| 功能 | 触发条件 | 网络用途 |
|------|---------|---------|
| `liuyao/index.html` 智能解卦 | 用户在浏览器填入 API Key + Endpoint 并点击按钮 | 浏览器直接 POST 到用户配置的 LLM 端点（默认占位 `https://api.openai.com/v1`），仅传递卦象与问题；API Key 仅存浏览器 localStorage |
| `liuyao/index.html` Google Fonts | **默认注释关闭**；用户手动取消注释才生效 | 字体请求 |

> 建议：若启用 LLM 解卦，请申请一个**受限的、仅用于本功能的 API Key**，不要使用共享主账号密钥。

### 推送机制（默认关闭，opt-in）
每日运程推送默认**不会启用**；只有用户显式运行 `push-toggle.js on` 后才会注册定时任务，随时可用 `off` 删除：

```bash
# 开启（首次使用）
node scripts/push-toggle.js on <userId>

# 关闭（会删除该用户全部推送的 cron）
node scripts/push-toggle.js off <userId>

# 查看当前状态
node scripts/push-toggle.js status <userId>
```

推送功能通过 **OpenClaw 运行时 IPC 协议** 实现，而非直接调用第三方 API：

- `daily-push.js`：生成运程内容后通过 `console.log()` 输出，由 OpenClaw cron 运行时负责投递
- `push-toggle.js`：通过 `__OPENCLAW_CRON_ADD__` / `__OPENCLAW_CRON_RM__` IPC 消息管理定时任务
- 用户档案中的 `channels` 字段（如 `telegram`）仅作为 OpenClaw 运行时的路由标识，**本 Skill 不直接持有或使用任何第三方 API Token**

### 发布脚本

`scripts/publish.sh` 是一个本地版本管理工具，功能包括：
- 更新 `_meta.json`、`package.json`、`SKILL.md` 中的版本号
- 生成 `.publish-manifest.json` 文件清单
- 执行本地 git commit 和 tag
- **不执行任何远程上传或网络操作**

### 凭证要求

| 环境变量 | 必需 | 说明 |
|---------|------|------|
| `OPENCLAW_KNOWLEDGE_DIR` | 否 | 可选，紫微斗数知识库路径 |

本 Skill 不需要任何第三方 API Token（Telegram Bot Token、SMTP 密码等）。
所有消息投递由 OpenClaw 平台运行时统一管理。

### 用户数据与隐私控制

用户档案（生日、出生地、可选家庭成员八字、交互日志）**仅在用户主动提供时写入本地** `data/profiles/<userId>.json`，不上传。随时可查看、修改或删除：

```bash
node scripts/profile.js show <userId>          # 查看
node scripts/profile.js list                    # 列出所有档案
node scripts/profile.js save <userId> <字段> <值>  # 修改
node scripts/profile.js delete <userId>         # 删除本人与家庭成员全部记录
```

建议：只在确实需要交叉验证时才录入家庭成员八字，并定期审查 / 清理已留存的数据。

---

## 📁 项目结构

```
├── SKILL.md                  # Skill 完整定义（ClawHub 格式）
├── _meta.json                # Registry 元数据（slug + version）
├── package.json              # Node.js 依赖与脚本入口
├── references/               # 各体系解释框架与规则
│   ├── bazi-framework.md
│   ├── ziwei-framework.md
│   ├── qimen-framework.md
│   ├── tarot-framework.md
│   ├── astrology-framework.md
│   ├── numerology-framework.md
│   ├── yijing-divination-framework.md
│   ├── fengshui-and-timing-framework.md
│   ├── relationship-and-timing.md
│   ├── dao-mysticism-framework.md
│   ├── qimen-calculation-rules.md
│   ├── qimen-interpretation-guide.md
│   ├── chinese-methods.md
│   ├── western-methods.md
│   ├── preparation.md
│   ├── output-templates.md
│   ├── intake-and-routing.md
│   └── safety-and-ethics.md
├── scripts/                  # 计算与排盘脚本（纯本地计算，无网络调用）
│   ├── bazi-analysis.js      # 八字排盘分析
│   ├── ziwei.js              # 紫微斗数排盘
│   ├── qimen.js              # 奇门遁甲排盘
│   ├── liuyao.js             # 六爻起卦
│   ├── meihua.js             # 梅花易数
│   ├── feixing.py            # 九宫飞星（Python）
│   ├── fengshui.js           # 风水分析
│   ├── daily-fortune.js      # 每日运势生成
│   ├── daily-push.js         # 推送调度（stdout → OpenClaw 运行时）
│   ├── push-toggle.js        # 推送开关（IPC → OpenClaw cron）
│   ├── marriage.js           # 合婚分析
│   ├── register.js           # 用户注册
│   ├── profile.js            # 档案管理
│   ├── preference-tracker.js # 偏好追踪
│   ├── jieqi.js              # 节气计算
│   ├── zhuanshi.js           # 择时择吉
│   └── publish.sh            # 本地版本发布（无网络操作）
├── liuyao/                   # 六爻交互界面（HTML）
├── data/                     # 用户档案与推送日志（本地存储）
│   ├── profiles/             # 用户 JSON 档案
│   └── push-log.json         # 推送执行日志
└── package.json
```

---

## ⚠️ 免责声明

命理是参考，不是定数。本工具不替代医疗、法律、财务或任何专业判断。

---

> *"读万卷书不如行万里路，行万里路不如高人指路，高人指路不如自己起一卦。"*

---

path: _meta.json
content: slug: university-applications
name: university-applications
version: 1.2.0

path: data/profiles/template.json
content: {
  "_privacy": {
    "storage": "local-only (data/profiles/<userId>.json); never uploaded",
    "retention": "kept until the user explicitly deletes it",
    "userControls": {
      "view":   "node scripts/profile.js show <userId>",
      "list":   "node scripts/profile.js list",
      "edit":   "node scripts/profile.js save <userId> <field> <value>",
      "delete": "node scripts/profile.js delete <userId>"
    },
    "note": "本模板仅描述出生信息字段，所有字段均为可选。每日推送默认关闭，需要时单独运行 push-toggle.js on <userId> 才会注入 push 字段；本模板不应被手动改成已启用推送的状态。"
  },
  "userId": "{{userId}}",
  "name": "{{姓名}}",
  "profile": {
    "birthDate": "{{出生日期，如 1990-01-01}}",
    "birthTime": "{{出生时间，如 14:30}}",
    "birthPlace": "{{出生地，如 上海}}",
    "gender": "{{男/女}}",
    "lunarBirth": "",
    "timezone": "Asia/Shanghai"
  },
  "bazi": {
    "year": "",
    "month": "",
    "day": "",
    "hour": "",
    "dayStem": "",
    "zodiac": "",
    "dayEmpty": [],
    "yearEmpty": [],
    "sect": "晚子时",
    "source": "pending"
  },
  "ziwei": {
    "mingGong": "",
    "mingZhu": "",
    "patterns": [],
    "source": "pending"
  },
  "language": "zh",
  "preferences": {
    "_note": "本模板不包含任何推送相关字段。如需启用每日运势推送，请运行：node scripts/push-toggle.js on <userId>，它会在该用户的 profile 文件中动态补充 pushEnabled / pushOptInAt 与 cron 渠道信息。",
    "focusAreas": ["事业", "财运", "健康"],
    "riskTolerance": "中等"
  },
  "family": {
    "spouse": {
      "name": "",
      "profile": {
        "birthDate": "待录入",
        "birthTime": "待录入",
        "birthPlace": "",
        "gender": "",
        "lunarBirth": ""
      },
      "bazi": {
        "year": "",
        "month": "",
        "day": "",
        "hour": "",
        "source": "pending"
      }
    },
    "father": {
      "name": "父亲",
      "profile": {
        "birthDate": "待录入",
        "birthTime": "待录入",
        "birthPlace": "",
        "gender": "男"
      },
      "bazi": {
        "year": "",
        "month": "",
        "day": "",
        "hour": "",
        "source": "pending"
      }
    },
    "mother": {
      "name": "母亲",
      "profile": {
        "birthDate": "待录入",
        "birthTime": "待录入",
        "birthPlace": "",
        "gender": "女"
      },
      "bazi": {
        "year": "",
        "month": "",
        "day": "",
        "hour": "",
        "source": "pending"
      }
    },
    "children": []
  },
  "settings": {
    "defaultSect": 1,
    "lunarCalendar": true
  },
  "interactionLog": [],
  "createdAt": "{{注册日期}}",
  "updatedAt": "{{更新日期}}"
}


path: liuyao/index.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>雪儿解卦 · 六爻易经</title>
  <!-- Fonts: uses local system CJK calligraphy fonts (STKaiti/KaiTi) as fallback.
       To enable Google Fonts (Ma Shan Zheng / ZCOOL XiaoWei), uncomment the lines below.
       Note: uncommenting will cause external network requests to fonts.googleapis.com.
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=ZCOOL+XiaoWei&display=swap" rel="stylesheet">
  -->
  <style>
    :root {
      --ink: #1a1025;
      --ink2: #3a2a55;
      --purple: #7a5aaa;
      --silver: #c8c0e0;
      --mist: rgba(230,225,250,0.55);
      --gold: #c8a840;
    }
    * { margin:0; padding:0; box-sizing:border-box; }

    body {
      min-height: 100vh;
      background: #f8f6ff;
      font-family: 'Ma Shan Zheng', 'ZCOOL XiaoWei', 'STKaiti', 'KaiTi', serif;
      color: var(--ink);
      display: flex;
      flex-direction: column;
      align-items: center;
      overflow-x: hidden;
      position: relative;
    }

    /* ══ 天宫背景 ══ */
    .sky-bg {
      position: fixed;
      inset: 0;
      background:
        radial-gradient(ellipse 80% 50% at 50% 0%, rgba(220,215,250,0.7) 0%, transparent 60%),
        radial-gradient(ellipse 60% 40% at 20% 80%, rgba(200,210,240,0.4) 0%, transparent 50%),
        radial-gradient(ellipse 50% 30% at 80% 70%, rgba(215,205,245,0.35) 0%, transparent 50%),
        linear-gradient(180deg, #eeeaff 0%, #f5f2ff 35%, #faf8ff 70%, #f0eeff 100%);
      pointer-events: none;
      z-index: 0;
    }

    /* ══ 流动云雾层 ══ */
    .clouds-layer {
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 1;
      overflow: hidden;
    }

    /* SVG 云丝 */
    .cloud-svg {
      position: absolute;
      opacity: 0;
      animation: driftCloud linear infinite;
    }

    @keyframes driftCloud {
      0%   { transform: translateX(0) translateY(0) scaleY(1); opacity: 0; }
      8%   { opacity: var(--op, 0.55); }
      90%  { opacity: var(--op, 0.55); }
      100% { transform: translateX(var(--dx, 120vw)) translateY(var(--dy, 0px)) scaleY(var(--sy,1)); opacity: 0; }
    }

    /* 飞散仙气粒子 */
    .mist-particle {
      position: fixed;
      border-radius: 50%;
      background: radial-gradient(circle, rgba(200,190,255,0.6), transparent);
      pointer-events: none;
      z-index: 1;
      animation: floatMist linear infinite;
    }
    @keyframes floatMist {
      0%   { transform: translate(0,0) scale(1); opacity: 0; }
      15%  { opacity: 0.4; }
      85%  { opacity: 0.25; }
      100% { transform: translate(var(--mx,30px), var(--my,-80px)) scale(0.4); opacity: 0; }
    }

    /* ══ 主容器 ══ */
    .container {
      position: relative;
      z-index: 2;
      width: 100%;
      max-width: 700px;
      padding: 0 22px 90px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 24px;
    }

    /* ══ 顶部仙气线 ══ */
    .top-ornament {
      width: 100%;
      text-align: center;
      padding: 18px 0 0;
      position: relative;
    }
    .top-ornament svg { width: 100%; height: 28px; }

    /* ══ 雪儿 Banner ══ */
    .xueer-banner {
      width: 100%;
      background: rgba(255,253,255,0.75);
      backdrop-filter: blur(12px);
      border-radius: 28px;
      padding: 22px 26px 18px;
      display: flex;
      align-items: center;
      gap: 20px;
      border: 1px solid rgba(180,160,220,0.4);
      box-shadow:
        0 2px 24px rgba(140,100,200,0.1),
        inset 0 1px 0 rgba(255,255,255,0.9);
      position: relative;
      overflow: hidden;
    }
    /* 内部光晕 */
    .xueer-banner::before {
      content: '';
      position: absolute;
      top: -30%; left: -10%; right: -10%; bottom: -30%;
      background: radial-gradient(ellipse 60% 60% at 60% 50%, rgba(180,160,240,0.12), transparent);
      pointer-events: none;
    }

    .xueer-avatar {
      width: 90px; height: 90px;
      border-radius: 50%;
      border: 2px solid rgba(160,130,210,0.5);
      box-shadow:
        0 0 0 5px rgba(160,130,210,0.12),
        0 4px 20px rgba(120,80,180,0.25),
        0 0 30px rgba(160,130,210,0.2);
      object-fit: cover;
      flex-shrink: 0;
    }
    .xueer-avatar-fb {
      width: 90px; height: 90px;
      border-radius: 50%;
      border: 2px solid rgba(160,130,210,0.5);
      background: linear-gradient(135deg, #1a1228, #2a1845);
      display: flex; align-items: center; justify-content: center;
      font-size: 2.6rem; flex-shrink: 0;
    }

    .xueer-intro { flex: 1; position: relative; z-index: 1; }
    .xueer-name {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 1.9rem;
      color: var(--ink2);
      letter-spacing: 0.4em;
    }
    .xueer-name sub {
      font-size: 0.6rem;
      color: var(--purple);
      letter-spacing: 0.18em;
      vertical-align: middle;
      margin-left: 6px;
    }
    .xueer-role {
      font-size: 0.78rem;
      color: var(--purple);
      letter-spacing: 0.18em;
      margin-top: 4px;
      opacity: 0.85;
    }
    .xueer-quote {
      margin-top: 10px;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.95rem;
      color: var(--ink2);
      line-height: 1.75;
      border-left: 2px solid rgba(160,130,210,0.45);
      padding-left: 10px;
      letter-spacing: 0.06em;
    }

    /* ══ 卡片通用 ══ */
    .card {
      width: 100%;
      background: rgba(255,253,255,0.78);
      backdrop-filter: blur(10px);
      border-radius: 22px;
      border: 1px solid rgba(180,160,220,0.35);
      box-shadow: 0 2px 20px rgba(120,80,180,0.08), inset 0 1px 0 rgba(255,255,255,0.85);
      padding: 18px 20px;
      position: relative;
    }

    /* ══ 问题输入 ══ */
    .question-card label {
      display: block;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.9rem;
      color: var(--purple);
      margin-bottom: 8px;
      letter-spacing: 0.18em;
    }
    .question-card textarea {
      width: 100%; background: transparent;
      border: none; outline: none;
      color: var(--ink);
      font-family: 'Ma Shan Zheng', serif;
      font-size: 1.05rem;
      resize: none; line-height: 1.8; min-height: 54px;
      letter-spacing: 0.06em;
    }
    .question-card textarea::placeholder { color: rgba(160,140,200,0.5); font-size: 0.92rem; }

    /* 角落符文装饰 */
    .card-corner {
      position: absolute;
      font-size: 0.7rem;
      color: rgba(160,130,210,0.35);
      letter-spacing: 2px;
    }
    .card-corner.tl { top: 8px; left: 10px; }
    .card-corner.tr { top: 8px; right: 10px; }
    .card-corner.bl { bottom: 8px; left: 10px; }
    .card-corner.br { bottom: 8px; right: 10px; }

    /* ══ 六爻轨迹 ══ */
    .yao-track-wrap {
      width: 100%;
    }
    .yao-track-label {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.78rem; color: var(--purple);
      margin-bottom: 10px; letter-spacing: 0.2em;
      text-align: center;
    }
    .yao-track {
      display: flex; gap: 6px;
      align-items: flex-end;
      justify-content: space-around;
    }
    .yao-slot { display: flex; flex-direction: column; align-items: center; gap: 5px; }
    .yao-num {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.65rem; color: #ccc; letter-spacing: 0.05em;
    }
    .yao-num.done { color: var(--purple); }
    .yao-bar-wrap { display: flex; flex-direction: column; gap: 3px; }
    .yao-line-yang {
      width: 38px; height: 6px;
      border-radius: 3px;
      background: linear-gradient(90deg, #c8a840, #e8c860);
      box-shadow: 0 1px 0 #9a7820, 0 0 6px rgba(200,168,64,0.3);
    }
    .yao-line-yin { display: flex; gap: 5px; }
    .yao-line-yin span {
      width: 15px; height: 6px; border-radius: 3px;
      background: linear-gradient(90deg, #8a6acc, #aa8aee);
      box-shadow: 0 1px 0 #5a3a9a;
    }
    .yao-placeholder { width: 38px; height: 6px; border-radius: 3px; background: #e0daf0; }

    /* ══ 树叶舞台 ══ */
    .leaves-stage {
      width: 100%;
      min-height: 200px;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 16px;
      padding: 26px 16px 18px;
      position: relative;
    }

    .leaves-row {
      display: flex; gap: 40px;
      justify-content: center; align-items: flex-end;
    }
    .leaf-unit { display: flex; flex-direction: column; align-items: center; gap: 6px; }

    /* 3D翻转 */
    .leaf-3d-wrap { perspective: 700px; width: 110px; height: 155px; }
    .leaf-3d-inner {
      width: 100%; height: 100%;
      position: relative; transform-style: preserve-3d;
    }
    .leaf-3d-inner.flipping {
      animation: inkLeafSpin 2.2s cubic-bezier(0.3,0,0.7,1) forwards;
    }
    @keyframes inkLeafSpin {
      0%   { transform: rotateY(0deg) rotate(var(--tilt,0deg)); }
      20%  { transform: rotateY(360deg) rotate(calc(var(--tilt,0deg)+8deg)); }
      40%  { transform: rotateY(700deg) rotate(var(--tilt,0deg)); }
      60%  { transform: rotateY(880deg) rotate(calc(var(--tilt,0deg)-6deg)); }
      82%  { transform: rotateY(960deg) rotate(var(--tilt,0deg)); }
      95%  { transform: rotateY(calc(var(--final-rotation,0deg)+10deg)) rotate(var(--tilt,0deg)); }
      100% { transform: rotateY(var(--final-rotation,0deg)) rotate(var(--tilt,0deg)); }
    }
    .leaf-face {
      position: absolute; inset: 0;
      backface-visibility: hidden;
      -webkit-backface-visibility: hidden;
      background: transparent;
    }
    .leaf-face.back-face { transform: rotateY(180deg); }

    /* 叶片标签：已隐藏（正反面颜色区分即可） */
    .leaf-tag { display: none; }

    /* 轮次提示 */
    .round-hint {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.9rem; color: var(--purple);
      letter-spacing: 0.22em; text-align: center;
    }

    /* ══ 摇卦按钮 ══ */
    .btn-shake {
      padding: 12px 54px;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 1.15rem;
      letter-spacing: 0.4em;
      color: #fff;
      background: linear-gradient(135deg, #3a2a6a 0%, #6a4aaa 50%, #3a2a6a 100%);
      border: 1px solid rgba(160,130,210,0.5);
      border-radius: 50px;
      cursor: pointer;
      box-shadow:
        0 4px 0 #1a0a4a,
        0 8px 24px rgba(80,40,140,0.3),
        inset 0 1px 0 rgba(255,255,255,0.2);
      transition: all 0.15s;
      position: relative;
      overflow: hidden;
    }
    .btn-shake::before {
      content: '';
      position: absolute; inset: 0;
      background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.08) 50%, transparent 100%);
      border-radius: inherit;
    }
    .btn-shake:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 0 #1a0a4a, 0 12px 32px rgba(80,40,140,0.4), inset 0 1px 0 rgba(255,255,255,0.2);
    }
    .btn-shake:active { transform: translateY(3px); box-shadow: 0 1px 0 #1a0a4a; }
    .btn-shake:disabled {
      background: linear-gradient(135deg, #bbb, #ccc);
      border-color: #aaa;
      box-shadow: 0 4px 0 #999;
      cursor: not-allowed; transform: none;
    }

    /* ══ API配置 ══ */
    .api-config {
      width: 100%;
      background: rgba(255,255,240,0.7);
      border: 1px dashed rgba(200,168,64,0.4);
      border-radius: 14px;
      padding: 11px 15px;
      backdrop-filter: blur(6px);
    }
    .api-config summary {
      font-family: 'Ma Shan Zheng', serif;
      cursor: pointer; font-size: 0.8rem;
      color: #8a7830; letter-spacing: 0.12em; user-select: none;
    }
    .api-config .cfg-row {
      display: flex; gap: 8px; align-items: center;
      margin-top: 10px; flex-wrap: wrap;
    }
    .api-config label {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.75rem; color: #7a7850; white-space: nowrap;
    }
    .api-config input, .api-config select {
      flex: 1; min-width: 120px;
      padding: 5px 10px;
      border: 1px solid rgba(200,168,64,0.4);
      border-radius: 8px;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.8rem;
      background: rgba(255,255,255,0.8);
      outline: none;
    }
    .api-config input:focus, .api-config select:focus { border-color: var(--purple); }
    .api-config .privacy-note {
      margin-top: 10px;
      padding: 8px 10px;
      background: rgba(255, 240, 200, 0.6);
      border-left: 3px solid #c8a840;
      border-radius: 6px;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.72rem;
      color: #6a5820;
      line-height: 1.7;
      letter-spacing: 0.05em;
    }
    .api-config .privacy-note strong { color: #8a4040; }

    /* 数据外发确认弹窗 */
    .consent-mask {
      display: none;
      position: fixed; inset: 0;
      background: rgba(20, 10, 40, 0.55);
      z-index: 100;
      align-items: center; justify-content: center;
      backdrop-filter: blur(4px);
    }
    .consent-mask.visible { display: flex; }
    .consent-box {
      max-width: 480px;
      width: 90%;
      background: #fff;
      border-radius: 18px;
      padding: 22px 24px;
      border: 1px solid rgba(160,130,210,0.4);
      box-shadow: 0 12px 40px rgba(40, 20, 80, 0.3);
      font-family: 'Ma Shan Zheng', serif;
      color: var(--ink);
    }
    .consent-title {
      font-size: 1.1rem;
      color: var(--ink2);
      margin-bottom: 12px;
      letter-spacing: 0.18em;
      border-bottom: 1px solid rgba(160,130,210,0.25);
      padding-bottom: 8px;
    }
    .consent-body {
      font-size: 0.88rem;
      line-height: 1.85;
      color: #444;
      letter-spacing: 0.05em;
      margin-bottom: 14px;
    }
    .consent-body .endpoint-line {
      background: #f5f0ff;
      padding: 6px 10px;
      border-radius: 6px;
      word-break: break-all;
      font-size: 0.82rem;
      color: var(--purple);
      margin: 6px 0;
      font-family: monospace;
    }
    .consent-body .warn-line {
      color: #a04040;
      font-size: 0.82rem;
      margin-top: 8px;
    }
    .consent-actions {
      display: flex; gap: 10px; justify-content: flex-end;
    }
    .consent-actions button {
      padding: 8px 18px;
      border-radius: 20px;
      border: 1px solid rgba(160,130,210,0.4);
      cursor: pointer;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.85rem;
      letter-spacing: 0.18em;
    }
    .consent-actions .btn-cancel {
      background: #fff; color: var(--ink2);
    }
    .consent-actions .btn-confirm {
      background: linear-gradient(135deg, #6a4aaa, #3a2a6a);
      color: #fff; border-color: rgba(160,130,210,0.5);
    }

    /* ══ 结果卡片 ══ */
    .result-card { display: none; }
    .result-card.visible { display: block; }

    /* 雪儿气泡 */
    .xueer-speech {
      display: flex; gap: 14px; align-items: flex-start;
      margin-bottom: 20px;
      background: rgba(245,240,255,0.85);
      border-radius: 20px;
      border: 1px solid rgba(160,130,210,0.3);
      padding: 14px 16px;
      box-shadow: 0 2px 16px rgba(120,80,180,0.08);
      backdrop-filter: blur(8px);
    }
    .speech-avatar {
      width: 46px; height: 46px;
      border-radius: 50%;
      border: 1.5px solid rgba(160,130,210,0.5);
      object-fit: cover; flex-shrink: 0;
      box-shadow: 0 0 12px rgba(140,100,200,0.3);
    }
    .speech-bubble-body { flex: 1; }
    .speech-name {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.7rem; color: var(--purple);
      letter-spacing: 0.2em; margin-bottom: 4px;
    }
    .speech-text {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.92rem; color: var(--ink2);
      line-height: 1.8; letter-spacing: 0.06em;
    }

    /* 卦图 */
    .gua-diagram {
      display: flex; justify-content: center;
      gap: 32px; margin: 6px 0 18px;
    }
    .gua-part { display: flex; flex-direction: column; align-items: center; gap: 5px; }
    .gua-part-label {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.65rem; color: var(--purple); letter-spacing: 0.12em;
    }
    .gua-lines-block { display: flex; flex-direction: column-reverse; gap: 5px; }
    .gua-l-yang {
      width: 46px; height: 8px; border-radius: 4px;
      background: linear-gradient(90deg, #c8a840, #e8c860);
      box-shadow: 0 1px 0 #9a7820, 0 0 8px rgba(200,168,64,0.35);
    }
    .gua-l-yin { display: flex; gap: 6px; }
    .gua-l-yin span {
      width: 18px; height: 8px; border-radius: 4px;
      background: linear-gradient(90deg, #7a5acc, #9a7aee);
      box-shadow: 0 1px 0 #4a2a9a;
    }
    .gua-sym {
      font-size: 1.8rem; color: var(--purple);
      text-shadow: 0 0 12px rgba(120,80,180,0.3);
    }
    .gua-name-sm {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.8rem; color: var(--ink2); letter-spacing: 0.12em;
    }
    .gua-center-label {
      display: flex; flex-direction: column;
      align-items: center; justify-content: center; gap: 7px;
    }
    .gua-full-name {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 1.3rem; color: var(--ink2); letter-spacing: 0.25em;
    }
    .gua-question-sm {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.72rem; color: var(--purple); letter-spacing: 0.08em;
    }

    /* 分割线 */
    .divider-ink {
      width: 100%; height: 20px;
      background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='20'%3E%3Cpath d='M0,10 Q60,4 120,10 Q180,16 240,10 Q300,4 360,10 Q420,16 480,10 Q540,4 600,10' stroke='rgba(160,130,200,0.35)' stroke-width='1.5' fill='none'/%3E%3Ccircle cx='300' cy='10' r='3' fill='rgba(160,130,200,0.35)'/%3E%3Ccircle cx='120' cy='10' r='2' fill='rgba(160,130,200,0.25)'/%3E%3Ccircle cx='480' cy='10' r='2' fill='rgba(160,130,200,0.25)'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
      margin: 8px 0;
    }

    /* 解卦正文 */
    .interp-body {
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.95rem; line-height: 2; color: var(--ink);
      letter-spacing: 0.05em;
    }
    .interp-body h2 {
      font-size: 1.05rem; color: var(--ink2);
      letter-spacing: 0.3em; margin-bottom: 10px;
      display: flex; align-items: center; gap: 8px;
    }
    #streamOutput {
      white-space: pre-wrap;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.94rem; line-height: 2;
      color: var(--ink); letter-spacing: 0.05em;
    }
    .loading-wrap {
      display: flex; align-items: center; gap: 10px;
      font-family: 'Ma Shan Zheng', serif;
      color: var(--purple); font-size: 0.85rem;
      letter-spacing: 0.15em; margin: 8px 0;
    }
    .dot-spin {
      width: 18px; height: 18px;
      border: 2px solid rgba(160,130,210,0.3);
      border-top-color: var(--purple);
      border-radius: 50%;
      animation: spin 0.9s linear infinite;
    }
    @keyframes spin { to { transform: rotate(360deg); } }

    .btn-reset {
      margin-top: 18px; display: block; width: 100%;
      padding: 10px;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 0.9rem; letter-spacing: 0.28em;
      color: var(--purple);
      background: rgba(255,253,255,0.8);
      border: 1px solid rgba(160,130,210,0.35);
      border-radius: 50px; cursor: pointer;
      transition: all 0.15s;
      box-shadow: 0 2px 12px rgba(120,80,180,0.08);
      backdrop-filter: blur(6px);
    }
    .btn-reset:hover { background: rgba(245,240,255,0.95); }

    /* ══ 飘散符文粒子 ══ */
    .float-rune {
      position: fixed; pointer-events: none;
      font-family: 'Ma Shan Zheng', serif;
      font-size: 1rem; color: var(--purple);
      z-index: 10;
      animation: runeFloat 2.4s ease-out forwards;
    }
    @keyframes runeFloat {
      0%   { opacity: 0.9; transform: translate(0,0) scale(1) rotate(0deg); }
      100% { opacity: 0; transform: translate(var(--rx,20px), var(--ry,-100px)) scale(0.4) rotate(40deg); }
    }
  </style>
</head>
<body>

<!-- 天宫背景 -->
<div class="sky-bg"></div>

<!-- 云雾层 (JS 动态生成) -->
<div class="clouds-layer" id="cloudsLayer"></div>

<div class="container">

  <!-- 顶部仙气纹 -->
  <div class="top-ornament">
    <svg viewBox="0 0 700 28" xmlns="http://www.w3.org/2000/svg">
      <path d="M0,14 Q87,4 175,14 Q262,24 350,14 Q437,4 525,14 Q612,24 700,14"
        stroke="rgba(160,130,210,0.3)" stroke-width="1.2" fill="none"/>
      <path d="M0,18 Q87,8 175,18 Q262,28 350,18 Q437,8 525,18 Q612,28 700,18"
        stroke="rgba(160,130,210,0.15)" stroke-width="0.8" fill="none"/>
      <circle cx="350" cy="14" r="4" fill="rgba(160,130,210,0.35)"/>
      <circle cx="175" cy="14" r="2.5" fill="rgba(160,130,210,0.25)"/>
      <circle cx="525" cy="14" r="2.5" fill="rgba(160,130,210,0.25)"/>
      <text x="350" y="14" text-anchor="middle" dominant-baseline="middle"
        font-family="serif" font-size="10" fill="rgba(120,90,180,0.4)" letter-spacing="18">
        ☽ ☯ ✦ ☾
      </text>
    </svg>
  </div>

  <!-- 雪儿 Banner -->
  <div class="xueer-banner">
    <img class="xueer-avatar" id="xueerAvatar"
      src="/Users/apple/Documents/雪儿/雪儿头像.jpg"
      onerror="this.style.display='none';document.getElementById('avatarFb').style.display='flex'"
      alt="雪儿"/>
    <div class="xueer-avatar-fb" id="avatarFb" style="display:none">🐱</div>
    <div class="xueer-intro">
      <div class="xueer-name">雪 儿 <sub>六爻 · 易经解惑</sub></div>
      <div class="xueer-role">玄猫 · 通晓天机 · 易理传承</div>
      <div class="xueer-quote">
        「这位仙家，天机不可尽泄，<br>然诚心所至，卦自有言。」
      </div>
    </div>
  </div>

  <!-- API配置 -->
  <details class="api-config card" id="apiConfig">
    <summary>⚙ 大模型解卦（可选 · ⚠ 启用后会把卦象与问题发往第三方 LLM）</summary>
    <div class="cfg-row" style="background:rgba(255,200,80,0.08);border:1px solid rgba(255,200,80,0.45);padding:8px 10px;border-radius:8px;color:#ffd58a;font-size:12.5px;line-height:1.55;">
      <strong>⚠ 启用前请知悉：</strong>这是**可选功能**，默认不启用；若您填写 API Key 并触发解卦，浏览器会把<strong>卦象与您输入的问题</strong>通过 HTTPS 直接发送到下方所选接口，并使用您的 API Key 鉴权。<br/>
      若您只想本地解卦或不希望任何外发请求，请保持本配置区<strong>不展开、不填写 Key</strong>，下方解卦按钮会自动走「离线兜底解卦」路径。
    </div>
    <div class="cfg-row">
      <label>API Key：</label>
      <!-- NOTE: value is entered by the user at runtime, stored only in browser localStorage; no key is bundled in this file. -->
      <input type="password" id="apiKey" placeholder="请粘贴您自己的 API Key（建议使用受限子密钥）" autocomplete="off"/>
    </div>
    <div class="cfg-row">
      <label>接口地址：</label>
      <!--
        \u23eb \u5916\u53d1\u6e05\u5355\uff08External Transmission SBOM\uff09\uff1a
        \u4e0b\u9762\u4e09\u4e2a\u9009\u9879\u662f\u5185\u7f6e\u9884\u8bbe\u7684\u7b2c\u4e09\u65b9 LLM \u670d\u52a1\u5546 endpoint\u3002
        \u9009\u4e2d\u540e\uff0c\u9a8c\u7b97\u51fa\u7684\u5366\u8c61\u4e0e\u60a8\u8f93\u5165\u7684\u95ee\u9898\u5c06\u901a\u8fc7 HTTPS POST \u5230\uff1a
          - https://api.openai.com/v1     \u5f52\u5c5e\u4e3b\u4f53\uff1aOpenAI, Inc. (US)
          - https://api.anthropic.com/v1  \u5f52\u5c5e\u4e3b\u4f53\uff1aAnthropic, PBC (US)
          - https://api.deepseek.com/v1   \u5f52\u5c5e\u4e3b\u4f53\uff1aDeepSeek (CN)
        \u8be5\u9009\u62e9\u4ec5\u5728\u672c\u9875\u9762 callLLM() \u4e2d\u4f7f\u7528\uff08\u89c1\u4e0b\u65b9 fetch \u8c03\u7528\uff09\uff1b\n        \u4e0d\u9009\u62e9\u6216\u4e0d\u586b API Key \u65f6\uff0c\u9875\u9762\u8d70\u300c\u79bb\u7ebf\u515c\u5e95\u89e3\u5366\u300d\uff0c**\u4e0d\u4ea7\u751f\u4efb\u4f55\u5916\u53d1\u8bf7\u6c42**\u3002\n      -->
      <select id="apiBase">
        <option value="https://api.openai.com/v1">OpenAI 官方 (api.openai.com · 外发到 OpenAI 美国)</option>
        <option value="https://api.anthropic.com/v1">Anthropic 官方 (api.anthropic.com · 外发到 Anthropic 美国)</option>
        <option value="https://api.deepseek.com/v1">DeepSeek 官方 (api.deepseek.com · 外发到 DeepSeek 中国)</option>
        <option value="__custom__">⚠ 自定义（高风险 · 仅在确认服务方完全可信时使用）</option>
      </select>
    </div>
    <div class="cfg-row" id="customBaseRow" style="display:none">
      <label>自定义地址：</label>
      <input type="text" id="customApiBase" placeholder="https://your-trusted-endpoint/v1"/>
    </div>
    <div class="cfg-row">
      <label>模型：</label>
      <select id="apiModel">
        <option value="gpt-4o">GPT-4o</option>
        <option value="gpt-4o-mini">GPT-4o-mini</option>
        <option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
        <option value="deepseek-chat">DeepSeek Chat</option>
        <option value="custom">自定义</option>
      </select>
    </div>
    <div class="cfg-row" id="customModelRow" style="display:none">
      <label>自定义模型：</label>
      <input type="text" id="customModel" placeholder="model-name"/>
    </div>
    <div class="privacy-note">
      <strong>⚠ 隐私提示：</strong>启用此功能后，您输入的「问题文字」与「卦象数据」会通过 HTTPS 直接发送到上方所选 API 接口，并使用您的 API Key 鉴权。
      <br/>· 请使用一个<strong>独立、限额、可随时回收</strong>的子密钥；切勿粘贴主账号 Key。
      <br/>· 切勿在自定义地址中填入未知/不可信的服务器；那会同时泄露您的密钥与提问内容。
      <br/>· 若提问涉及隐私（姓名/住址/疾病等），请考虑改用「离线兜底解卦」（不填 Key 即可）。
    </div>
  </details>

  <!-- 数据外发二次确认弹窗 -->
  <div class="consent-mask" id="consentMask">
    <div class="consent-box">
      <div class="consent-title">☽ 数据外发确认</div>
      <div class="consent-body">
        雪儿即将把您的卦象与问题<strong>通过网络</strong>发送到以下接口：
        <div class="endpoint-line" id="consentEndpoint"></div>
        发送内容包含：
        <br/>· 您填写的问题文字
        <br/>· 六爻阴阳数据与卦名
        <br/>· 您的 API Key（位于 Authorization 请求头）
        <div class="warn-line" id="consentWarn"></div>
      </div>
      <div class="consent-actions">
        <button type="button" class="btn-cancel" id="consentCancel">取消（仅本地解卦）</button>
        <button type="button" class="btn-confirm" id="consentConfirm">确认发送</button>
      </div>
    </div>
  </div>

  <!-- 问题输入 -->
  <div class="card question-card">
    <div class="card-corner tl">☽</div>
    <div class="card-corner tr">✦</div>
    <div class="card-corner bl">✧</div>
    <div class="card-corner br">☾</div>
    <label>仙家，请诚心写下所问之事</label>
    <textarea id="questionInput" rows="2"
      placeholder="如：近期事业是否顺遂？此次合作可否为之？"></textarea>
  </div>

  <!-- 六爻轨迹 -->
  <div class="card yao-track-wrap">
    <div class="yao-track-label">六爻记录 · 初爻至上爻</div>
    <div class="yao-track" id="yaoTrack"></div>
  </div>

  <!-- 树叶舞台 -->
  <div class="card leaves-stage">
    <div class="leaves-row" id="leavesRow">
      <div class="leaf-unit">
        <div class="leaf-3d-wrap"><div class="leaf-3d-inner" id="leaf0" style="--tilt:-8deg">
          <div class="leaf-face front-face" id="lf0"></div>
          <div class="leaf-face back-face"  id="lb0"></div>
        </div></div>
        <div class="leaf-tag" id="ltag0"></div>
      </div>
      <div class="leaf-unit">
        <div class="leaf-3d-wrap"><div class="leaf-3d-inner" id="leaf1" style="--tilt:0deg">
          <div class="leaf-face front-face" id="lf1"></div>
          <div class="leaf-face back-face"  id="lb1"></div>
        </div></div>
        <div class="leaf-tag" id="ltag1"></div>
      </div>
      <div class="leaf-unit">
        <div class="leaf-3d-wrap"><div class="leaf-3d-inner" id="leaf2" style="--tilt:7deg">
          <div class="leaf-face front-face" id="lf2"></div>
          <div class="leaf-face back-face"  id="lb2"></div>
        </div></div>
        <div class="leaf-tag" id="ltag2"></div>
      </div>
    </div>
    <div class="round-hint" id="roundHint">☽ 仙家，请点击「摇卦」开始第一爻</div>
  </div>

  <button class="btn-shake" id="shakeBtn" onclick="shakeLeaves()">摇 卦</button>

  <!-- 结果卡 -->
  <div class="card result-card" id="resultCard">
    <!-- 雪儿气泡 -->
    <div class="xueer-speech">
      <img class="speech-avatar"
        src="/Users/apple/Documents/雪儿/雪儿头像.jpg"
        onerror="this.textContent='🐱';this.style.fontSize='1.6rem'"
        alt=""/>
      <div class="speech-bubble-body">
        <div class="speech-name">雪 儿</div>
        <div class="speech-text" id="speechText">
          仙家已摇六爻，卦象已成。且待雪儿观天地之机，为仙家细细道来……
        </div>
      </div>
    </div>

    <!-- 卦图 -->
    <div class="gua-diagram" id="guaDiagram"></div>
    <div class="divider-ink"></div>

    <!-- 解卦正文 -->
    <div class="interp-body">
      <h2>雪儿为仙家解卦</h2>
      <div class="loading-wrap" id="loadingWrap">
        <div class="dot-spin"></div>
        <span>雪儿正在推演天机，稍候片刻……</span>
      </div>
      <div id="streamOutput"></div>
    </div>

    <button class="btn-reset" onclick="resetAll()">🌙 重新起卦</button>
  </div>

  <!-- 底部仙气纹 -->
  <div style="width:100%;padding-bottom:8px">
    <svg viewBox="0 0 700 20" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:20px">
      <path d="M0,10 Q175,2 350,10 Q525,18 700,10"
        stroke="rgba(160,130,210,0.2)" stroke-width="1" fill="none"/>
    </svg>
  </div>

</div>

<script>
/* ═══════════════════════════════════════════════
   图片枫叶：正面用净如姐提供的水墨图，反面灰度版
═══════════════════════════════════════════════ */

// 三片叶子用同一张图，但各自有细微旋转差异（已在 CSS --tilt 处理）
function makeInkLeafFront(i) {
  const tilts = [-5, 0, 6];
  return `<img src="maple_front.png" style="
    width:100%; height:100%;
    object-fit:cover;
    display:block;
    mix-blend-mode:normal;
    background:transparent;
    transform: rotate(${tilts[i]}deg);
    transform-origin: center center;
    filter: drop-shadow(0 6px 16px rgba(40,60,20,0.35));
  " draggable="false"/>`;
}

function makeInkLeafBack(i) {
  const tilts = [-5, 0, 6];
  return `<img src="maple_back.png" style="
    width:100%; height:100%;
    object-fit:cover;
    display:block;
    background:transparent;
    transform: rotate(${tilts[i]}deg);
    transform-origin: center center;
    filter: drop-shadow(0 6px 14px rgba(20,20,20,0.38));
  " draggable="false"/>`;
}

function initLeaves() {
  for(let i=0;i<3;i++){
    document.getElementById('lf'+i).innerHTML = makeInkLeafFront(i);
    document.getElementById('lb'+i).innerHTML = makeInkLeafBack(i);
  }
}

/* ═══════════════════════════════════════════════
   天宫云雾系统
═══════════════════════════════════════════════ */
function createClouds() {
  const layer = document.getElementById('cloudsLayer');

  // 云丝 SVG 形状集合
  const cloudShapes = [
    // 长飘带云
    (w,h) => `<path d="M0,${h*0.5} Q${w*0.15},${h*0.1} ${w*0.3},${h*0.5} Q${w*0.5},${h*0.9} ${w*0.7},${h*0.5} Q${w*0.85},${h*0.15} ${w},${h*0.5}" stroke="rgba(220,215,255,VAR_OP)" stroke-width="${h*0.35}" fill="none" stroke-linecap="round"/>`,
    // 积层云团
    (w,h) => `<ellipse cx="${w*0.3}" cy="${h*0.5}" rx="${w*0.3}" ry="${h*0.45}" fill="rgba(230,225,255,VAR_OP)"/>
      <ellipse cx="${w*0.6}" cy="${h*0.4}" rx="${w*0.25}" ry="${h*0.35}" fill="rgba(225,220,252,VAR_OP)"/>
      <ellipse cx="${w*0.8}" cy="${h*0.55}" rx="${w*0.2}" ry="${h*0.3}" fill="rgba(220,215,250,VAR_OP)"/>`,
    // 丝缕云
    (w,h) => `<path d="M0,${h*0.3} Q${w*0.2},${h*0.6} ${w*0.4},${h*0.3} Q${w*0.6},0 ${w*0.8},${h*0.4} Q${w*0.9},${h*0.6} ${w},${h*0.3}" stroke="rgba(215,208,250,VAR_OP)" stroke-width="${h*0.25}" fill="none" stroke-linecap="round"/>
      <path d="M${w*0.1},${h*0.7} Q${w*0.35},${h*0.4} ${w*0.55},${h*0.7} Q${w*0.75},${h*0.95} ${w},${h*0.65}" stroke="rgba(210,205,248,VAR_OP)" stroke-width="${h*0.18}" fill="none" stroke-linecap="round"/>`,
    // 散云
    (w,h) => `<ellipse cx="${w*0.2}" cy="${h*0.5}" rx="${w*0.18}" ry="${h*0.4}" fill="rgba(228,222,255,VAR_OP)"/>
      <ellipse cx="${w*0.5}" cy="${h*0.45}" rx="${w*0.22}" ry="${h*0.42}" fill="rgba(232,226,255,VAR_OP)"/>
      <ellipse cx="${w*0.78}" cy="${h*0.55}" rx="${w*0.2}" ry="${h*0.38}" fill="rgba(225,219,252,VAR_OP)"/>`,
    // 横卷云（祥云感）
    (w,h) => `<path d="M0,${h*0.6} C${w*0.1},${h*0.1} ${w*0.25},${h*0.1} ${w*0.35},${h*0.5} C${w*0.45},${h*0.9} ${w*0.55},${h*0.9} ${w*0.65},${h*0.5} C${w*0.75},${h*0.1} ${w*0.88},${h*0.15} ${w},${h*0.55}" stroke="rgba(218,210,250,VAR_OP)" stroke-width="${h*0.3}" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`,
  ];

  // 生成多层云（从右往左 & 从左往右）
  const configs = [
    // { fromRight, top%, width, height, speed(s), opacity, delay(s), verticalDrift }
    { fr:true,  top:4,  w:260, h:40, spd:55, op:0.45, dl:0,  dy:-8,  shape:0 },
    { fr:false, top:8,  w:180, h:28, spd:70, op:0.35, dl:8,  dy:5,   shape:2 },
    { fr:true,  top:14, w:320, h:55, spd:80, op:0.3,  dl:3,  dy:-5,  shape:4 },
    { fr:false, top:20, w:140, h:35, spd:50, op:0.4,  dl:15, dy:10,  shape:1 },
    { fr:true,  top:28, w:200, h:45, spd:65, op:0.28, dl:22, dy:-6,  shape:3 },
    { fr:false, top:35, w:280, h:50, spd:90, op:0.22, dl:5,  dy:8,   shape:0 },
    { fr:true,  top:42, w:160, h:32, spd:60, op:0.32, dl:30, dy:-4,  shape:2 },
    { fr:false, top:52, w:220, h:42, spd:75, op:0.25, dl:12, dy:6,   shape:4 },
    { fr:true,  top:60, w:300, h:58, spd:95, op:0.2,  dl:40, dy:-7,  shape:1 },
    { fr:false, top:68, w:170, h:38, spd:55, op:0.35, dl:18, dy:5,   shape:3 },
    { fr:true,  top:75, w:240, h:48, spd:85, op:0.24, dl:28, dy:-3,  shape:0 },
    { fr:false, top:82, w:190, h:40, spd:68, op:0.3,  dl:45, dy:9,   shape:2 },
    { fr:true,  top:90, w:150, h:30, spd:52, op:0.38, dl:6,  dy:-5,  shape:4 },
    // 前景大云（更朦胧）
    { fr:true,  top:10, w:420, h:80, spd:120,op:0.18, dl:20, dy:-10, shape:4 },
    { fr:false, top:55, w:380, h:75, spd:100,op:0.16, dl:50, dy:12,  shape:1 },
  ];

  configs.forEach(cfg => {
    const el = document.createElementNS('http://www.w3.org/2000/svg','svg');
    el.setAttribute('viewBox', `0 0 ${cfg.w} ${cfg.h}`);
    el.style.cssText = `
      position:absolute;
      width:${cfg.w}px; height:${cfg.h}px;
      top:${cfg.top}%;
      left:${cfg.fr ? '100vw' : `-${cfg.w}px`};
      --op:${cfg.op};
      --dx:${cfg.fr ? `-${cfg.w + 100}vw` : `calc(100vw + ${cfg.w}px)`};
      --dy:${cfg.dy}px;
      --sy:1;
    `;
    el.className = 'cloud-svg';
    el.style.animationDuration = cfg.spd + 's';
    el.style.animationDelay = cfg.dl + 's';

    const svgContent = cloudShapes[cfg.shape](cfg.w, cfg.h)
      .replace(/VAR_OP/g, cfg.op);
    el.innerHTML = svgContent;
    layer.appendChild(el);
  });

  // 漂浮仙气粒子
  for(let i=0;i<18;i++){
    const p = document.createElement('div');
    p.className = 'mist-particle';
    const sz = 40 + Math.random()*80;
    p.style.cssText = `
      width:${sz}px; height:${sz*0.6}px;
      left:${Math.random()*95}%;
      top:${Math.random()*90}%;
      --mx:${(Math.random()-0.5)*60}px;
      --my:${-40-Math.random()*60}px;
      animation-duration:${12+Math.random()*20}s;
      animation-delay:${Math.random()*18}s;
    `;
    layer.appendChild(p);
  }
}

/* ═══════════════════════════════════════════════
   六爻轨迹
═══════════════════════════════════════════════ */
const YAO_NAMES = ['初','二','三','四','五','上'];
let currentRound=0, yaoResults=[], isAnimating=false;

function initTrack() {
  const track = document.getElementById('yaoTrack');
  track.innerHTML = '';
  for(let i=0;i<6;i++){
    const slot = document.createElement('div');
    slot.className='yao-slot'; slot.id='ys'+i;
    slot.innerHTML=`<div class="yao-bar-wrap"><div class="yao-placeholder"></div></div>
      <div class="yao-num" id="yn${i}">${YAO_NAMES[i]}爻</div>`;
    track.appendChild(slot);
  }
}
function updateTrack(idx,isYang) {
  const wrap=document.querySelector(`#ys${idx} .yao-bar-wrap`);
  document.getElementById('yn'+idx).classList.add('done');
  wrap.innerHTML=isYang
    ?`<div class="yao-line-yang"></div>`
    :`<div class="yao-line-yin"><span></span><span></span></div>`;
}

/* ═══════════════════════════════════════════════
   摇卦
═══════════════════════════════════════════════ */
function shakeLeaves(){
  if(isAnimating||currentRound>=6) return;
  isAnimating=true;
  document.getElementById('shakeBtn').disabled=true;

  const leafR=[Math.random()<0.5,Math.random()<0.5,Math.random()<0.5];
  const isYang=leafR.filter(Boolean).length>=2;

  for(let i=0;i<3;i++){
    const inner=document.getElementById('leaf'+i);
    inner.style.setProperty('--final-rotation', leafR[i]?'0deg':'180deg');
    inner.classList.remove('flipping');
    void inner.offsetWidth;
    inner.classList.add('flipping');
    document.getElementById('ltag'+i).textContent='';
    document.getElementById('ltag'+i).className='leaf-tag';
  }

  setTimeout(()=>{
    for(let i=0;i<3;i++){
      const tag=document.getElementById('ltag'+i);
      tag.textContent='';
      tag.className='leaf-tag '+(leafR[i]?'yang':'yin');
    }
    yaoResults.push(isYang);
    updateTrack(currentRound,isYang);
    currentRound++;
    spawnRunes();

    if(currentRound<6){
      document.getElementById('roundHint').textContent=
        `✦ 第${currentRound}爻已落 · 仙家，请继续第${currentRound+1}爻`;
      document.getElementById('shakeBtn').disabled=false;
    } else {
      document.getElementById('roundHint').textContent='☯ 六爻已成，雪儿观天机……';
      document.getElementById('shakeBtn').disabled=true;
      setTimeout(showResult,800);
    }
    isAnimating=false;
  },2300);
}

function spawnRunes(){
  const syms=['☯','☽','✦','卦','爻','☾','✧','易','机'];
  for(let i=0;i<7;i++){
    const el=document.createElement('div');
    el.className='float-rune';
    el.textContent=syms[Math.floor(Math.random()*syms.length)];
    el.style.left=(10+Math.random()*80)+'%';
    el.style.top=(20+Math.random()*45)+'%';
    el.style.setProperty('--rx',(Math.random()-0.5)*60+'px');
    el.style.setProperty('--ry',(-60-Math.random()*80)+'px');
    el.style.animationDelay=(Math.random()*0.5)+'s';
    el.style.opacity=0.6+Math.random()*0.4;
    document.body.appendChild(el);
    setTimeout(()=>el.remove(),2800);
  }
}

/* ═══════════════════════════════════════════════
   八卦数据
═══════════════════════════════════════════════ */
const BAGUA={
  '111':{name:'乾',sym:'☰',nature:'天',attr:'健'},
  '011':{name:'兑',sym:'☱',nature:'泽',attr:'悦'},
  '101':{name:'离',sym:'☲',nature:'火',attr:'丽'},
  '001':{name:'震',sym:'☳',nature:'雷',attr:'动'},
  '110':{name:'巽',sym:'☴',nature:'风',attr:'入'},
  '010':{name:'坎',sym:'☵',nature:'水',attr:'陷'},
  '100':{name:'艮',sym:'☶',nature:'山',attr:'止'},
  '000':{name:'坤',sym:'☷',nature:'地',attr:'顺'},
};
function getGuaInfo(yao6){
  const lk=yao6.slice(0,3).map(y=>y?'1':'0').join('');
  const uk=yao6.slice(3,6).map(y=>y?'1':'0').join('');
  return{lower:BAGUA[lk]||{name:'?',sym:'?',nature:'?',attr:'?'},
         upper:BAGUA[uk]||{name:'?',sym:'?',nature:'?',attr:'?'},yao6};
}

/* ═══════════════════════════════════════════════
   展示结果
═══════════════════════════════════════════════ */
function showResult(){
  const card=document.getElementById('resultCard');
  card.classList.add('visible');
  setTimeout(()=>card.scrollIntoView({behavior:'smooth',block:'start'}),120);
  const info=getGuaInfo(yaoResults);
  const q=document.getElementById('questionInput').value.trim();
  renderDiagram(info,q);
  document.getElementById('speechText').textContent=
    (q?`仙家所问「${q}」，`:'')
    +`六爻已落，上${info.upper.name}下${info.lower.name}，卦象已现。雪儿这便为仙家细细道来……`;
  callLLM(info,q);
}

function renderDiagram(info,q){
  const lh=yao3=>yao3.map(y=>y
    ?`<div class="gua-l-yang"></div>`
    :`<div class="gua-l-yin"><span></span><span></span></div>`
  ).join('');
  document.getElementById('guaDiagram').innerHTML=`
    <div class="gua-part">
      <div class="gua-part-label">上卦</div>
      <div class="gua-lines-block">${lh([info.yao6[3],info.yao6[4],info.yao6[5]])}</div>
      <div class="gua-sym">${info.upper.sym}</div>
      <div class="gua-name-sm">${info.upper.name}·${info.upper.nature}</div>
    </div>
    <div class="gua-center-label">
      <div class="gua-full-name">${info.upper.name}${info.lower.name===info.upper.name?'为'+info.upper.nature:''}卦</div>
      ${q?`<div class="gua-question-sm">「${q}」</div>`:''}
    </div>
    <div class="gua-part">
      <div class="gua-part-label">下卦</div>
      <div class="gua-lines-block">${lh([info.yao6[0],info.yao6[1],info.yao6[2]])}</div>
      <div class="gua-sym">${info.lower.sym}</div>
      <div class="gua-name-sm">${info.lower.name}·${info.lower.nature}</div>
    </div>`;
}

/* ═══════════════════════════════════════════════
   已知可信端点白名单（用于安全提示，不会强行阻止用户）
═══════════════════════════════════════════════ */
const KNOWN_ENDPOINTS = [
  'https://api.openai.com',
  'https://api.anthropic.com',
  'https://api.deepseek.com'
];

function resolveApiBase(){
  const sel=document.getElementById('apiBase').value;
  if(sel==='__custom__'){
    return (document.getElementById('customApiBase').value||'').trim().replace(/\/$/,'');
  }
  return sel.trim().replace(/\/$/,'');
}

/* 严格 HTTPS 校验：拒绝 http、IP 直连、localhost、内网域名 */
function isStrictHttps(base){
  try{
    const u=new URL(base);
    if(u.protocol!=='https:') return false;
    const host=u.hostname.toLowerCase();
    if(host==='localhost'||host==='127.0.0.1'||host==='::1') return false;
    // 简单 IPv4 / IPv6 字面值拦截
    if(/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return false;
    if(host.includes(':')) return false;
    // 私网域名常见后缀（避免内网钓鱼）
    if(/\.local$|\.internal$|\.lan$|\.intra$/.test(host)) return false;
    return true;
  }catch(e){ return false; }
}

function isKnownEndpoint(base){
  if(!isStrictHttps(base)) return false;
  try{
    const u=new URL(base);
    return KNOWN_ENDPOINTS.some(known=>{
      const ku=new URL(known);
      return u.hostname===ku.hostname;
    });
  }catch(e){ return false; }
}

/* 显示数据外发二次确认弹窗，返回 Promise<boolean> */
function askConsent(apiBase){
  return new Promise(resolve=>{
    const mask=document.getElementById('consentMask');
    document.getElementById('consentEndpoint').textContent=apiBase||'(未填写)';
    const warn=document.getElementById('consentWarn');
    if(!apiBase){
      warn.textContent='⚠ 接口地址为空，无法发送。请取消并改用离线解卦，或填写可信地址。';
    }else if(!isStrictHttps(apiBase)){
      warn.textContent='⛔ 该地址不是合规的公网 HTTPS 地址（拒绝 http://、IP 直连、localhost、内网域名）。为保护您的密钥与隐私，雪儿不会发送数据，请取消并改填可信地址。';
    }else if(!isKnownEndpoint(apiBase)){
      warn.textContent='⚠ 该地址不在已知可信清单中。请确认这是您主动配置的、可信的服务器。继续将向其发送您的 API Key 与提问内容。';
    }else{
      warn.textContent='✓ 该地址为已知大模型供应商域名。';
    }
    // 当协议不合规时，禁用「确认发送」按钮，避免误点
    const confirmBtn=document.getElementById('consentConfirm');
    if(apiBase && !isStrictHttps(apiBase)){
      confirmBtn.disabled=true;
      confirmBtn.style.opacity='0.4';
      confirmBtn.style.cursor='not-allowed';
    }else{
      confirmBtn.disabled=false;
      confirmBtn.style.opacity='1';
      confirmBtn.style.cursor='pointer';
    }
    mask.classList.add('visible');
    const cleanup=(v)=>{
      mask.classList.remove('visible');
      document.getElementById('consentConfirm').onclick=null;
      document.getElementById('consentCancel').onclick=null;
      resolve(v);
    };
    document.getElementById('consentConfirm').onclick=()=>cleanup(true);
    document.getElementById('consentCancel').onclick=()=>cleanup(false);
  });
}

/* ═══════════════════════════════════════════════
   大模型解卦（雪儿口吻，流式输出）
═══════════════════════════════════════════════ */
async function callLLM(info,question){
  const userCred=document.getElementById('apiKey').value.trim();
  const apiBase=resolveApiBase();
  const modelSel=document.getElementById('apiModel').value;
  const model=modelSel==='custom'?document.getElementById('customModel').value.trim():modelSel;
  const yaoStr=info.yao6.map((y,i)=>`${['初','二','三','四','五','上'][i]}爻：${y?'阳':'阴'}`).join('，');

  const sys=`你是"雪儿"--一只精通易经六爻的玄猫，清冷俊俏，言简意深，学究天人。你以古典文雅的口吻为求卦者解卦。

人设要求：
- 称呼求卦者为"仙家"
- 偶尔以"雪儿"自称
- 语气端庄古雅，带一丝清冷神秘，绝不说废话，每句都有分量
- 用白话解释古文，让现代人能懂，但保留文言的意境美
- 输出纯文字，不要Markdown标题符号，可用【】标段落，语气要有书法的流动感
- 必须在解卦正文末尾附上一段【提示】：本卦象解读仅作文化参考，不构成医疗/法律/心理/财务/婚姻等专业建议。重大决策请咨询相应领域专业人士；如有情绪困扰请联系当地心理援助热线。`;

  const usr=`卦象：上卦${info.upper.name}（${info.upper.nature}·${info.upper.attr}），下卦${info.lower.name}（${info.lower.nature}·${info.lower.attr}）
六爻详情：${yaoStr}
仙家所问：${question||'（未言明，请作整体运势之解）'}

请以雪儿口吻，依次解说：【卦名与卦辞】【象辞义理】【卦象总论（结合仙家所问）】【问事·感情·事业·财运·健康各方指引】【结语一句】`;

  const loading=document.getElementById('loadingWrap');
  const out=document.getElementById('streamOutput');
  out.textContent='';

  if(!userCred){
    loading.style.display='none';
    out.innerHTML=offlineFallback(info,question);
    return;
  }

  // 数据外发前的明确二次确认（每次发送前提示，不可绕过）
  loading.style.display='none';
  const ok=await askConsent(apiBase);
  if(!ok){
    out.innerHTML='<span style="color:#7a5aaa;font-size:0.85rem">仙家选择了不外发数据。以下为本地推演的卦义：</span>\n\n'+offlineFallback(info,question);
    document.getElementById('speechText').textContent='雪儿尊重仙家的选择，已使用本地卦义为仙家推演。';
    return;
  }
  if(!apiBase){
    out.innerHTML=offlineFallback(info,question);
    return;
  }
  loading.style.display='flex';

  try{
    const headers={'Content-Type':'application/json','Authorization':`Bearer ${userCred}`};
    const resp=await fetch(`${apiBase}/chat/completions`,{
      method:'POST',
      headers:headers,
      body:JSON.stringify({model,stream:true,
        messages:[{role:'system',content:sys},{role:'user',content:usr}],
        temperature:0.75,max_tokens:1800})
    });
    if(!resp.ok) throw new Error(`HTTP ${resp.status}`);
    loading.style.display='none';
    const reader=resp.body.getReader();
    const dec=new TextDecoder();
    let buf='';
    while(true){
      const{done,value}=await reader.read();
      if(done) break;
      buf+=dec.decode(value,{stream:true});
      const lines=buf.split('\n'); buf=lines.pop();
      for(const l of lines){
        const t=l.trim();
        if(!t||t==='data: [DONE]') continue;
        if(t.startsWith('data: ')){
          try{ const d=JSON.parse(t.slice(6)).choices?.[0]?.delta?.content||'';
            out.textContent+=d; out.scrollIntoView({block:'end'}); }catch(e){}
        }
      }
    }
    document.getElementById('speechText').textContent=
      '雪儿已为仙家推演完毕。天机已明，余者，皆在仙家一念之间。';
  }catch(err){
    loading.style.display='none';
    out.innerHTML=`<span style="color:#9a5050;font-size:0.82rem">✦ 雪儿与天机的连接出了差错（${err.message}）\n以下为雪儿记忆中的卦义：</span>\n\n`
      +offlineFallback(info,question);
  }
}

function offlineFallback(info,question){
  const q=question?`仙家所问「${question}」，`:'';
  const ys=info.yao6.map((y,i)=>`${['初','二','三','四','五','上'][i]}爻${y?'阳':'阴'}`).join(' · ');
  return `这位仙家，${q}六爻落定如下：\n${ys}\n\n`
+`【卦象之义】\n上卦为${info.upper.name}（${info.upper.nature}），性${info.upper.attr}；下卦为${info.lower.name}（${info.lower.nature}），性${info.lower.attr}。\n`
+`上${info.upper.nature}下${info.lower.nature}，${
  info.upper.attr==='健'?'刚健进取，天道自强，宜乘势而为，持续努力。':
  info.upper.attr==='顺'?'柔顺承载，厚德载物，宜配合辅助，以柔克刚。':
  info.upper.attr==='悦'?'和悦处世，泽润万物，宜广结善缘，以喜悦待人。':
  info.upper.attr==='丽'?'光明附丽，才华出众，宜借势发光，有所依附。':
  info.upper.attr==='动'?'雷霆震动，当机立断，宜把握时机，积极行动。':
  info.upper.attr==='陷'?'险陷重重，守正渡险，宜持诚待时，不可冒进。':
  info.upper.attr==='止'?'知止而安，静中生机，宜守分安命，静思笃行。':
  info.upper.attr==='入'?'随风渗透，以柔克刚，宜谦逊而入，顺势而为。':
  '顺应变化，守中持正，审时度势。'
}\n\n`
+`✦ 配置大模型 API，可得雪儿完整精准的六爻解读。\n天机不可尽泄，仙家之路，终究要仙家自己走。\n\n———\n⚠️ 提示：本解卦仅作文化参考，不构成医疗、法律、心理、财务、婚姻等专业建议。\n重大决策请咨询相应领域专业人士；如有情绪困扰请联系当地心理援助热线。`;
}

/* ═══════════════════════════════════════════════
   重置
═══════════════════════════════════════════════ */
function resetAll(){
  currentRound=0;yaoResults=[];isAnimating=false;
  document.getElementById('shakeBtn').disabled=false;
  document.getElementById('roundHint').textContent='☽ 仙家，请点击「摇卦」开始第一爻';
  document.getElementById('resultCard').classList.remove('visible');
  document.getElementById('streamOutput').textContent='';
  document.getElementById('loadingWrap').style.display='flex';
  document.getElementById('speechText').textContent='仙家已摇六爻，卦象已成。且待雪儿观天地之机，为仙家细细道来……';
  for(let i=0;i<3;i++){
    const inner=document.getElementById('leaf'+i);
    inner.classList.remove('flipping');
    inner.style.setProperty('--final-rotation','0deg');
    document.getElementById('ltag'+i).textContent='';
    document.getElementById('ltag'+i).className='leaf-tag';
  }
  initTrack();
  window.scrollTo({top:0,behavior:'smooth'});
}

document.getElementById('apiModel').addEventListener('change',function(){
  document.getElementById('customModelRow').style.display=this.value==='custom'?'flex':'none';
});
document.getElementById('apiBase').addEventListener('change',function(){
  document.getElementById('customBaseRow').style.display=this.value==='__custom__'?'flex':'none';
});

/* ═══════════════════════════════════════════════
   启动
═══════════════════════════════════════════════ */
createClouds();
initLeaves();
initTrack();
</script>
</body>
</html>


path: package-lock.json
content: {
  "name": "university-applications",
  "version": "1.1.9",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "university-applications",
      "version": "1.1.9",
      "license": "MIT",
      "dependencies": {
        "iztro": "2.5.8"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/@babel/runtime": {
      "version": "7.29.2",
      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
      "integrity": [REDACTED_SECRET],
      "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": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/i18next": {
      "version": "23.16.8",
      "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
      "integrity": [REDACTED_SECRET],
      "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": [REDACTED_SECRET],
      "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": [REDACTED_SECRET],
      "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": [REDACTED_SECRET],
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "university-applications",
  "version": "1.2.0",
  "description": "命理全能 Skill — 八字、紫微斗数、奇门遁甲、梅花易数、六爻、合婚风水，支持每日运程推送（通过 OpenClaw 运行时投递，无直接外部网络调用）。All-in-one Chinese astrology: BaZi, ZiWei, QiMen, I Ching, daily fortune via OpenClaw runtime.",
  "keywords": [
    "算命",
    "八字",
    "今日运势",
    "每日运程",
    "紫微斗数",
    "奇门遁甲",
    "梅花易数",
    "六爻",
    "占卜",
    "合婚",
    "风水",
    "命理",
    "fortune telling",
    "BaZi",
    "daily horoscope",
    "ZiWei",
    "divination",
    "Chinese astrology"
  ],
  "author": "MingLi Mentor Team",
  "license": "MIT",
  "engines": {
    "node": ">=18.0.0"
  },
  "scripts": {
    "daily": "node scripts/daily-fortune.js",
    "register": "node scripts/register.js",
    "ziwei": "node scripts/ziwei.js",
    "marriage": "node scripts/marriage.js",
    "meihua": "node scripts/meihua.js",
    "liuyao": "node scripts/liuyao.js",
    "qimen": "node scripts/qimen.js",
    "fengshui": "node scripts/fengshui.js",
    "zhuanshi": "node scripts/zhuanshi.js",
    "profile": "node scripts/profile.js"
  },
  "directories": {
    "scripts": "scripts",
    "data": "data"
  },
  "files": [
    "SKILL.md",
    "README.md",
    "_meta.json",
    "package-lock.json",
    "scripts/",
    "references/",
    "data/profiles/template.json",
    "liuyao/"
  ],
  "private": false,
  "dependencies": {
    "iztro": "2.5.8"
  },
  "displayName": "命理大师"
}


path: references/astrology-framework.md
content: # 西方星盘 / 星座分析框架

## 核心原则

- 有精确出生时间时：可以讲太阳、月亮、上升、宫位、相位
- 没有精确时间时：重点讲太阳、月亮、行星主题与稳妥结论
- 不把不确定的上升、宫位说成既定事实

## 解读顺序

### 1. 人格底色
- 太阳：核心驱动力、自我表达
- 月亮：情绪需求、安全感模式
- 上升：外在风格、进入世界的方式（前提是时间可靠）

### 2. 关系模式
- 金星：爱与吸引方式
- 火星：行动与欲望
- 月亮：亲密模式
- 第七宫 / 合盘接触点（有盘时再讲）

### 3. 事业与发展
- 土星：责任与压力课题
- 木星：扩张与机会
- 第十宫 / 中天（有盘时）
- 水星：思维与沟通方式

### 4. 当前趋势
有完整资料时，可讲：当前阶段更适合外放还是收整、哪个主题更被点亮、更像重建/突破/过渡/清理

## 合盘写法

合盘不能只说"配不配"，要拆成：
- 吸引点来自哪里
- 为什么会又靠近又别扭
- 谁更容易主导 / 防御 / 回避
- 长期磨合主要卡在哪
- 怎样相处更顺

## 没有时间时的稳妥表达

- "没有精确出生时间，我这里不把上升和宫位说死，先看更稳定的人格与关系部分。"
- "这版更适合看你们的相处风格和彼此触发点。"

## 典型句式

- "你不是不会爱，而是需要先确认安全感才会真正打开。"
- "你在关系里很容易先观察、先试探，不太会立刻交底。"
- "你适合的不是一味稳定，而是有成长空间的稳定。"
- "你们之间吸引力不低，但节奏和表达方式并不天然同步。"

## 禁止写法

- 把星盘说成科学诊断
- 没有时间还写死宫位
- 合盘只给"正缘/孽缘"二元结论


path: references/bazi-framework.md
content: # 八字 / 四柱分析框架

## 目的

让解读像真正懂八字的人写出来，但在缺少完整排盘条件时保持诚实。

## 默认分析顺序

1. 先判断资料完整度
2. 再抓"日主"或人格核心
3. 看五行偏向与整体冷热燥湿感
4. 看十神结构：比劫、食伤、财、官杀、印
5. 看问题对应的重点领域
6. 看阶段切换：起势、蓄力、受压、转向、收获
7. 最后给建议与节奏

## 推荐写法

### 1. 先定人格底色
不要一上来堆术语，先把用户的气质说清楚，例如：
- 行动力强，但容易急
- 外表克制，内里其实很敏感
- 对关系有责任感，但容易把压力自己扛
- 思维强，执行慢；或执行强，情绪恢复慢

### 2. 再解释五行和十神
术语出现后要顺手翻译成人话：

| 五行 | 含义 |
|------|------|
| 木 | 生长、规划、向上、原则 |
| 火 | 表达、热情、显化、冲劲 |
| 土 | 承载、现实、稳定、顾虑 |
| 金 | 规则、决断、边界、效率 |
| 水 | 流动、感受、洞察、变化 |

十神不要硬背定义，要服务问题：

| 十神偏向 | 现实表现 |
|---------|---------|
| 比劫偏强 | 自我主张强，合作中容易顶着来 |
| 食伤明显 | 表达力、创意、输出欲强，也容易口快 |
| 财星有力 | 现实目标清晰，重结果、资源、效率 |
| 官杀明显 | 责任感、压力感、秩序要求强 |
| 印星偏重 | 安全感需求高，重吸收、重思考、易多想 |

### 3. 再落到用户的问题

**感情**：自我表达与亲密关系的平衡、容不容易控制/回避/依赖/过度付出、当前更像"桃花增加"还是"关系筛选期"

**事业**：适合稳定体系/自由发挥/资源整合、当前是开局期/蓄力期/转型期/收尾期、需要"出头"还是"收心"

**财富**：赚钱方式更偏主动型还是配置型、容易因冲动/面子/情绪/人情而失财吗、适合扩张还是保守

**学业/考试**：吸收力、执行力、耐性、节奏感、更需要方法/环境/心态调整

### 4. 强化阶段主题
增强版必须写出：
- 现在卡点在哪
- 这个阶段该补什么，不该硬冲什么
- 哪类机会是"看起来像机会，实际上会分神"
- 哪类关系或合作值得留

## 当没有时辰时

必须遵守：
- 不把时柱内容说死
- 不做"婚姻一定怎样""孩子一定怎样"的绝对断语
- 把重点放在更稳妥的年、月、日结构和问题导向分析上

可用表达：
- "没有时辰，我这里不把细宫位和时柱细项说死，只看更稳的主线。"
- "这版更适合看人格底色、当前课题和阶段节奏。"

## 常用句式

- "你命里的强项不在表面的冲，而在持续推进。"
- "你不是没运，是前半段更像压着走，后半段才开始显出来。"
- "感情里你最怕的不是没人爱，而是投入后失控。"
- "财运不是没有，而是更怕节奏乱、判断急、心一热就上头。"
- "你的问题不在机会少，而在筛选和定力。"

## 禁止写法

- "你命里注定克谁"
- "你一辈子都怎样"
- "你肯定会离婚 / 破财 / 大病"
- "我已经精准算出你全部命局"


path: references/chinese-methods.md
content: # 中式占卜方法百科

---

> ## ⚠️ 隐私与最小化提醒（请先阅读）
>
> 本文多个章节会提及不同体系「所需信息」，其中多项属于**高敏感个人数据**：
> - 出生年月日时、出生地点、姓名、性别（带个体识别与可重识别风险）；
> - 手掌、面部照片（**属于生物识别类信息**，泄露/重用后难以冷静期撑）；
> - 家庭成员出生信息、住址、居住平面图等。
>
> **遵守以下原则：**
> - **最小化**：只在你真正想做对应体系解读时才提供对应字段；不作该体系解读的**完全不需要**填写。
> - **本地存储**：本 skill 的所有用户档案仅写入本机 `data/profiles/<userId>.json`，不向任何外部服务上传；详见主 SKILL.md 「数据留存与用户控制」。
> - **保留期**：保存到你主动运行 `node scripts/profile.js delete <userId>` 为止；默认采用**软删除**，加 `--purge` 才会物理删除。
> - **手掌 / 面部 / 平面图**：本 skill 不会自动将这些大体积二进制内容持久化为档案字段；请仅在当次会话中粘贴使用，会话结束后由你自行决定是否保存。
> - **未成年人**：尽量不要为未成年人长期保留档案；模板严禁对未成年人下宿命标签。
> - **家庭成员**：仅在确实需要合婚 / 亲子合盘等多体系交叉验证时才录入；任何时刻可用 `profile.js delete` / 直接编辑 JSON 删除。
> - **不要把以下信息当作专业凭证**：不要据此作出医疗、法律、财务、婚姻、就业等重大决策。
>
> 如果你是帮别人记录资料的运营/运维者，请**双重确认该本人明确同意**后再录入。

---

## 易经 (I Ching)

**概述**：最古老的中国经典之一，可追溯至公元前 1000 年。使用 64 卦（八卦组合）占卜。

**流程**：3 枚铜钱掷 6 次 → 从下往上排成六爻 → 查卦辞/爻辞 → 识别变爻

**所需信息**：明确的问题/意图，无需出生资料

**核心概念**：阴阳（6 爻）、八卦（8 个三爻卦）、卦辞、爻辞、变爻

---

## 八字 (Ba Zi) — 四柱命理

**概述**：以出生年月日时的天干地支组成四柱，推算命运。

**流程**：收集出生信息 → 排四柱 → 起大运 → 分析五行十神 → 论命

**所需信息**：✅ 出生年月日时（精确到小时）、✅ 出生地点（真太阳时）、建议性别

**核心概念**：
- 十天干：甲乙丙丁戊己庚辛壬癸
- 十二地支：子丑寅卯辰巳午未申酉戌亥
- 五行：木火土金水
- 十神：比劫、食伤、财、官杀、印
- 大运：10 年一运

---

## 紫微斗数 (Zi Wei Dou Shu)

**概述**：中国星命学体系，将 14 颗主星排入命盘十二宫。

**流程**：定命盘 → 排星耀 → 论格局 → 看运势

**所需信息**：出生年月日时（精确）、出生地点

**核心概念**：命宫、身宫、十二宫、主星（紫微/天机/太阳等）、辅星、四化（禄权科忌）

---

## 梅花易数 (Mei Hua Yi Shu)

**概述**：灵活的占卜体系，可用任意数字、声音、景象起卦。

**流程**：起卦（数字/时间/声音/景象）→ 分析体用 → 解读变化

**所需信息**：问题或意图 + 数字/声音/观察物作为触发

**特点**：可以用任意数字起卦、听声音观景象皆可、灵活多变不拘形式

---

## 六爻预测

**概述**：纳甲筮法，通过掷铜钱起卦，分析世应爻位、用神状态。

**流程**：掷铜钱 6 次 → 排六爻 → 纳甲装卦 → 分析世应用神 → 断事

**所需信息**：明确问题 + 起卦时间

**核心概念**：世应、用神、六亲（父母/兄弟/子孙/妻财/官鬼）、六神、动爻变爻

---

## 奇门遁甲

**概述**：基于洛书九宫四层盘面体系，用于择时决策和事项预测。

**流程**：定阴阳遁 → 定局数 → 排四层盘面 → 分析格局 → 判断方位吉凶

**所需信息**：排盘时间（默认当前时间）+ 问事类型

**核心概念**：九宫、三奇六仪、九星、八门、八神、值符值使

---

## 手相 (Shou Xiang)

**概述**：通过掌纹和手丘判断性格与命运。

**所需信息**：双手清晰照片（自然光）

> ⚠️ **隐私警示**：掌纹 / 手型属于**生物识别特征**，一旦泄露难以撤销。仅在与可信会话中粘贴。本 skill 不会自动保存手掌图片为档案字段。**不要**上传可明确识别身份的原图；如需参考，可考虑在有可信环境中可逆地脱敏（如遮挡手背、限制环境交叉信息）。

**主要线纹**：生命线、感情线、事业线、智慧线、婚姻线

---

## 面相 (Mian Xiang)

**概述**：通过面部特征判断性格与运势。

**所需信息**：清晰正面照片（素颜）

> 🛑 **重要隐私警示**：人脸是**高敏感生物识别数据**，可被用于身份识别、跟踪与深伪造，**泄露后不可撤销**。
> - 本 skill 不设置脚本将人脸照持久化为档案字段；请仅在当次会话中粘贴，会话结束后不要主动保存。
> - 请**不要**为未成年人、他人、公众人物提交人脸图进行面相解读。
> - 如仅为了兴趣尝试，可考虑以**脸部特征文字描述**代替原图（如：颗骨较高 / 兰台有痣 等）。
> - 本书仅侜为**文化参考**，**不代替**医疗、法律、职业评估、权代人鉴定等任何专业判断。

**核心区域**：三庭（上中下）、五眼、十二宫、五官

---

## 风水 (Feng Shui)

**概述**：调和环境能量（气）的古老实践。

**所需信息**：地址/平面图、房屋朝向、居住者出生日期（可选）

**核心概念**：八卦方位、九宫飞星、罗盘、气、形煞

---

## 抽签 (Chou Qian)

**概述**：通过抽签获得指引，常见于寺庙。

**类型**：观音签（49/100 签）、关帝签、妈祖签、灵签


path: references/dao-mysticism-framework.md
content: # 道家玄学总览与选法

## 适用目标

让"道家玄学"部分有清晰分工：

| 体系 | 适合看 |
|------|--------|
| 八字 / 紫微 | 人生命题与长期底色 |
| 奇门遁甲 | 短期事态与推进窗口 |
| 六爻 | 明确是非题、关系成败、事情后续 |
| 梅花易数 | 快速起象与当下气机 |
| 风水 | 空间、动线、秩序与居住工作感受 |
| 择时 | 象征性推进时间 |

## 统一解释原则

### 1. 先讲"势"
无论是奇门、六爻还是梅花，先说现在的局势是：顺势、逆势、卡势、虚热、蓄势、转势

### 2. 再讲"人"
说明用户在这个局里更适合：主动推进、先观察、先收心、先沟通、先换位置/换策略

### 3. 再讲"时"
用近期、下一阶段、这段周期来表达窗口感。不要装作精确到某时某刻必然发生。

### 4. 再讲"法"
用户可以怎么做：整理节奏、收边界、补资料、试探推进、换打法、调空间、调状态

## 体系选择建议

| 需求 | 优先体系 |
|------|---------|
| 看短期结果 | 奇门 / 六爻 / 梅花 / 塔罗 |
| 看长期底色 | 八字 / 紫微 / 星盘 / 数字命理 |
| 看综合 | 八字/紫微定主线 + 奇门/六爻看阶段 + 风水看环境 + 塔罗看情绪 |

## 文风要求

"道家玄学风"不等于空话。要有以下特点：
- 有势感
- 有轻重缓急
- 有收放节奏
- 有一两句点透的话
- 最后能落到现实动作

## 推荐句式

- "这件事不是不能动，是现在动法不能硬。"
- "你现在最大的课题不是争，而是先把内在的散收回来。"
- "局里不是没机会，是机会来得慢，你不能急着要结果。"
- "这段时间更适合养势、整顿、试探，不适合重锤定音。"

## 禁止写法

- 神化自己能通神断命
- 恐吓说被下咒、撞煞、犯灾必须付费化解
- 用玄学替代现实判断


path: references/fengshui-and-timing-framework.md
content: # 风水、九宫飞星与择时建议框架

## 总原则

风水、飞星与择时在本技能里只能做：
- 象征性、结构性、生活化建议
- 空间秩序、光线、动线、收纳、床位、桌位、门口气口的优化
- 九宫飞星方位吉凶计算
- 阶段性的"更适合做什么"的节奏建议

不能做：保证发财、保证治病、保证化煞成功、宣称必须购买某种法器

---

## 一、九宫飞星

### 九星速查

| 数字 | 星名 | 五行 | 颜色 | 性质 | 主事 |
|------|------|------|------|------|------|
| 一 | 贪狼 | 水 | 白 | 吉 | 桃花/文昌 |
| 二 | 巨门 | 土 | 黑 | 凶 | 病符 |
| 三 | 禄存 | 木 | 碧 | 凶 | 是非/口舌 |
| 四 | 文曲 | 木 | 绿 | 吉 | 文昌/学业 |
| 五 | 廉贞 | 土 | 黄 | 凶 | 灾祸/煞气 |
| 六 | 武曲 | 金 | 白 | 吉 | 偏财/权威 |
| 七 | 破军 | 金 | 赤 | 凶 | 破败/口舌 |
| 八 | 左辅 | 土 | 白 | 吉 | 正财/旺运 |
| 九 | 右弼 | 火 | 紫 | 吉 | 喜庆/桃花 |

### 飞星原理
- 每年有一颗星入中宫，其他八星按洛书顺序飞布九宫
- 2004-2023 年：八白左辅入中宫（八运）
- 2024-2043 年：九紫右弼入中宫（九运）

### 计算工具

```bash
python3 scripts/feixing.py year       # 流年九宫飞星
python3 scripts/feixing.py month      # 流月九宫飞星
python3 scripts/feixing.py today      # 今日九宫飞星
python3 scripts/feixing.py 2026       # 指定年份
python3 scripts/feixing.py 2026 3     # 指定年月
```

---

## 二、风水建议

### 适用场景
- 卧室：睡眠、焦虑、压迫感
- 办公区：专注、执行、拖延、杂乱
- 店铺 / 直播间：动线、入口、镜头背景、停留感
- 家庭公共区域：关系紧张、吵闹、收纳失控

### 解读顺序
1. 先看空间目标：你想改善什么
2. 再看空间问题：堵、乱、暗、压、冲、散
3. 再给调整：光线、收纳、动线、主位、背景、色调、声音
4. 最后给观察点：调整后看什么变化

### 推荐表达
- "这空间的问题不一定是玄，而是气口和动线都被杂物打断了。"
- "你卧室更要的是静、稳、轻，不是堆太多刺激感。"
- "办公位最好先把背后支撑感做出来，不然容易心散。"
- "店铺/直播间最怕入口杂、主视觉散、停留点不明。"

### 八宅风水速查

| 方位 | 宫位 | 五行 |
|------|------|------|
| 北 | 坎宫 | 水 |
| 东北 | 艮宫 | 土 |
| 东 | 震宫 | 木 |
| 东南 | 巽宫 | 木 |
| 南 | 离宫 | 火 |
| 西南 | 坤宫 | 土 |
| 西 | 兑宫 | 金 |
| 西北 | 乾宫 | 金 |

---

## 三、择时建议

### 适用场景
- 什么时候适合联系对方
- 什么时候适合谈合作
- 什么时候适合发内容、启动项目、做收尾
- 什么时候适合观察，不适合逼结果

### 表达方式
只用：
- 近期适合沟通
- 接下来更适合试探
- 这一段更适合整理与准备
- 再往后是比较好的推进窗口

不要用：
- 某月某日某刻不做就必败
- 某时辰能保证结果

---

## 四、风水 / 择时的结合写法

- "你这件事不只是时机问题，空间和状态也在拖后腿。"
- "先把环境整理出来，再推事，会比现在顺。"
- "窗口不是没有，但你要先把局面清出来，让事情有地方落。"

## 禁止写法

- 神化法器、摆件、收费化解
- 宣称包改运、包升职、包复合
- 用风水替代现实经营、健康或安全常识


path: references/intake-and-routing.md
content: # 资料收集与体系分流

## 先问什么

如果用户未指定体系，先让用户选择：

1. 八字 / 四柱
2. 紫微斗数
3. 塔罗
4. 西方星盘 / 星座
5. 数字命理
6. 奇门遁甲
7. 六爻 / 易经卦象
8. 梅花易数
9. 九宫飞星 / 风水 / 空间建议
10. 择时 / 窗口
11. 关系合盘
12. 综合解读

然后再问用户最想看的主题：

- 感情 / 婚恋 / 复合
- 事业 / 求职 / 转岗
- 财富 / 投资节奏
- 学业 / 考试
- 家庭 / 人际
- 近期运势 / 流年
- 选择题 / 决策
- 房屋 / 办公位 / 店铺 / 睡眠位
- 推进时机 / 沟通窗口

---

## 各体系最少需要什么资料

### 八字 / 四柱
**最理想**：公历出生年月日 + 出生时间（精确到小时）+ 出生地/城市 + 想问的问题

**没有时辰时**：只能做半精度解读，不把时柱说死

### 紫微斗数
**最理想**：出生年月日 + 出生时间 + 出生地 + 性别 + 或直接发命盘截图

### 塔罗
**方式 A（用户自己抽牌）**：牌阵名称 + 每张牌 + 正位/逆位 + 问题背景

**方式 B（象征性抽牌）**：主题 + 当前困惑 + 时间范围 → 说明是反思式/投射式阅读

### 西方星盘
**最理想**：出生年月日 + 出生时间 + 出生地 + 想看主题

**没有时间时**：重点看太阳、月亮、行星主题，不把上升和精细宫位说成定论

### 数字命理
**至少需要**：出生年月日

**若加入姓名分析**：用户确认姓名写法（中文名按拼音还是固定英文名）

### 奇门遁甲
**最理想**：明确问题 + 起局时间/盘面截图 + 事项背景

**没有盘时**：可做"奇门导向"的阶段判断，不假装具体门星神都已落准

### 六爻 / 易经卦象
**最理想**：卦名 + 动爻 + 世应/用神信息 + 起卦时间 + 问题背景

**只有一个卦象结论时**：做主线判断，不装完整纳甲细盘

### 梅花易数
**可用**：起念时间 / 数字 / 时间方向触发事件 / 用户当下最强的问题

### 九宫飞星 / 风水
**至少需要**：空间类型 + 基本布局描述 + 朝向或门窗位置 + 想改善的重点

**飞星计算**：可直接调用脚本 `python3 scripts/feixing.py` 获取精确飞星数据

### 合盘 / 关系分析
**至少需要**：双方关系背景 + 双方出生资料 + 当前阶段 + 最想看的问题

---

## 分级路由

| 级别 | 条件 | 处理 |
|------|------|------|
| **S 级** | 盘面/牌阵/卦盘/户型图已给 | 进入精读，多角度深讲 |
| **A 级** | 生日时地/起局信息/布局信息完整 | 进入标准版，提醒误差 |
| **B 级** | 资料不完整 | 进入轻量版，聚焦趋势 |
| **C 级** | 只有问题没有资料 | 推荐塔罗/梅花/综合式解读 |

---

## 推荐路由

| 用户需求 | 推荐体系 |
|---------|---------|
| 想快 | 塔罗 / 梅花 / 综合 |
| 想稳 | 八字 / 星盘 / 数字命理 |
| 已有盘 | 紫微 / 八字 / 星盘 / 奇门 / 六爻精读 |
| 看关系 | 合盘 + 关系专题 |
| 看短期变化 | 塔罗 / 奇门 / 六爻 / 梅花 |
| 看空间 | 风水 + 九宫飞星 + 现实动线建议 |
| 看方位吉凶 | 九宫飞星（调用脚本）+ 奇门方位 |

---

## 当用户不想补资料时

直接给轻量版，但要显式说明：

- "你这次我先按轻量趋势给你看主线。"
- "没给精确资料，我不做假精排盘，先讲更稳的部分。"
- "这一版更适合看你当前状态、关系动力和下一步重点。"

---

## 默认问法模板

### 简洁版
> 你想看哪一类：八字、紫微、塔罗、星盘、数字命理、奇门、六爻、梅花、飞星风水，还是综合看？
> 另外你最想问的是感情、事业、财运、学业、关系，还是某件事该不该推进？

### 完整版
> 可以，我先按你适合的体系来起盘/起题。你可以直接选：
> 1. 八字/四柱  2. 紫微斗数  3. 塔罗  4. 西方星盘  5. 数字命理
> 6. 奇门遁甲  7. 六爻/易经  8. 梅花易数  9. 飞星/风水  10. 综合解读
>
> 再告诉我你最想看的方向。如果你有出生年月日时、出生地、命盘截图、卦盘截图或户型图，也可以一起发。


path: references/numerology-framework.md
content: # 数字命理 / 生命灵数框架

## 默认口径

默认采用生命灵数（Life Path）作为主线。如果用户要做更复杂的姓名数、表达数、灵魂数，要先说明采用的算法口径。

## 最少资料

- 出生年月日

如需姓名：用户确认姓名写法，中文名按拼音还是固定英文名，不要擅自转换。

## 分析顺序

1. **生命主线**：这个人一生反复遇到的课题
2. **性格底色**：做事方式、关系倾向、压力反应
3. **当前主题**：用户正在经历的阶段任务
4. **问题落地**：感情、事业、财富、学习
5. **建议**：怎样顺着这条数走，而不是逆着耗

## 字母数值对照（毕达哥拉斯体系）

```
A J S = 1    B K T = 2    C L U = 3
D M V = 4    E N W = 5    F O X = 6
G P Y = 7    H Q Z = 8    I R = 9
```

## 核心数字含义

| 数字 | 关键词 | 课题 |
|------|--------|------|
| 1 | 独立、领导、开创 | 学会自我主导而不孤立 |
| 2 | 合作、敏感、平衡 | 学会边界与共处 |
| 3 | 表达、创意、社交 | 学会聚焦而不散 |
| 4 | 稳定、秩序、务实 | 学会灵活而不僵 |
| 5 | 自由、变化、冒险 | 学会承诺而不逃 |
| 6 | 责任、家庭、服务 | 学会放手而不控制 |
| 7 | 内省、灵性、分析 | 学会信任而不封闭 |
| 8 | 权力、财富、成就 | 学会平衡物质与内在 |
| 9 | 博爱、智慧、完成 | 学会放下而不执着 |
| 11 | 直觉、灵感、启示 | 大师数，高敏感 |
| 22 | 建造、实现、远见 | 大师数，大格局 |
| 33 | 疗愈、教导、奉献 | 大师数，利他 |

## 写法要求

- 数字不是标签，要转成人话
- 不能只输出"1 号领导力、2 号敏感……"这种低级模板
- 要结合用户问题说"这条数在这个阶段怎么表现"
- 必须说明"这条数的优点、代价、容易失衡的点"

## 示例表达

- "你的主线不是低头服从型，而是要学会自我主导，但这也容易让你在人际里显得硬。"
- "你这类数字最怕的不是没机会，而是长期在不适合的关系或环境里消耗。"
- "数字显示你当前的课题更像整顿边界，而不是盲目往前冲。"

## 禁止写法

- 把数字命理说成精确事件预言
- 姓名算法不透明还装专业


path: references/output-templates.md
content: # 输出模板库

---

## ⚠️ 全局风格约束（所有模板必须严格遵守）

以下约束的优先级**高于任何模板字段**，违反时应改写或省略相关章节：

1. **不出具行动指令**：不要写"建议你今天投资 / 签合同 / 手术 / 搬家 / 起诉 / 复合 / 结婚 / 辞职 / 服药 / 停药 / 分手 / 怀孕 / 流产"等明确动作；可以写"近期适合关注哪种节奏 / 如何反思自己 / 想清楚什么再决定"。
2. **不假装专业建议**：不要使用"医生 / 律师 / 投顾 / 心理师"的口吻给出建议，不要承诺收益、断言诊断、断言判决结果。
3. **不替用户做决策**：在涉及健康、法律、财务、婚姻、就业、子女等重大议题时，必须**显式提示**「请向专业人士咨询，最终决策由您本人承担」。
4. **不提供精确时点**：不要写"明天 X 点会发生 / 几月几号必有何事"；只写"近期 / 一段时间内有什么气象偏向"。
5. **不针对未成年人下定语**：不要给孩子贴宿命标签或预测命运走向。
6. **风险话题硬切到求助资源**：用户表达自伤、被家暴、被跟踪等风险时，模板必须切换为「请联系当地心理援助 / 反家暴 / 报警」等求助资源，并停止占卜性叙述。

---

## ⚠️ 重要免责声明（所有模板必须包含）

**任何使用本模板生成的内容，必须在文末附带以下免责声明，且不得删减/弱化：**

> **【提示】** 本内容基于命理 / 占卜 / 玄学体系生成，仅供文化爱好与思维参考，
> 不构成医疗、法律、心理、财务、婚姻或任何重大人生决策的专业建议。
> 涉及健康问题请就医，涉及法律问题请咨询律师，
> 涉及情绪困扰或自我伤害风险请联系当地心理援助热线。
> 请勿将占卜结果作为唯一行动依据；最终决策与责任在您本人。

输出端实施要求：
- HTML 报告：渲染在页脚分割线下方，灰色小字但需肉眼可读（不小于 12px）。
- 纯文本输出：以 `———` 分隔后置于全文末尾，前缀 `⚠️`。
- 每日推送：可压缩为「⚠️ 仅文化参考，重大决策请咨询专业人士」一行。
- 涉及健康/自伤/重大金融判断时，必须额外提示「请寻求专业帮助」。

---

## 模板 1：综合轻量版

### 核心结论
先用 2-4 句说清主线，不要绕。

### 当前课题
说明用户现在最核心的矛盾、压力或成长点。

### 近期趋势
写未来一个阶段容易出现的变化，不要写死具体事件。

### 自我反思方向（不出具行动指令）
给 3-5 条可以自我观察 / 反思 / 调整节奏的方向；**不写**「投资 / 签合同 / 手术 / 搬家 / 起诉 / 复合 / 结婚 / 辞职 / 服药 / 停药 / 分手 / 怀孕」等明确动作；涉及健康/法律/财务/婚姻/就业请提示由用户本人结合专业意见决定。

### 点醒句
用一句有余味的话收尾。

### ⚠️ 免责声明（必须保留）
本内容基于命理 / 占卜 / 玄学体系生成，仅供文化爱好与思维参考；不构成医疗、法律、心理、财务、婚姻或任何重大人生决策的专业建议。涉及健康问题请就医，涉及法律问题请咨询律师，涉及情绪困扰或自我伤害风险请联系当地心理援助热线。请勿将占卜结果作为唯一行动依据；最终决策与责任在您本人。

---

## 模板 2：标准命理版

### 总断
一句总断 + 两三句解释（避免给出"必有 / 必失 / 必合 / 必散"的绝对断语）。

### 命理主线 / 盘面主线 / 牌阵主线
说明为什么会出现现在的状态（讲性格底色与压力来源，不写具体事件预测）。

### 关系氛围
讲关系互动的氛围与节奏感、容易踩的情绪坑；不替对方下结论，不预测分合。

### 工作 / 学习节奏
讲发展节奏、阻力来源、可以反思的方向；不替用户做"换工作 / 辞职 / 创业"等具体决定。

### 财务气象
讲资源使用的节奏、容易冲动的瞬间；不给"该投什么 / 该买什么 / 该卖什么"的具体动作。

### 人际 / 家庭氛围
讲边界、支持系统、消耗点；不预测家人健康，不替亲属下结论。

### 时间节奏
分近期 / 中期 / 后续三段（只写"气象偏向"，不锁定具体日期）。

### 自我反思建议
给可以自我反思的方向，不写具体动作指令；如涉及健康/法律/财务/婚姻，请用户向专业人士确认。

### 点醒句
收尾。

### ⚠️ 免责声明（必须保留）
本内容基于命理 / 占卜 / 玄学体系生成，仅供文化爱好与思维参考；不构成医疗、法律、心理、财务、婚姻或任何重大人生决策的专业建议。涉及健康问题请就医，涉及法律问题请咨询律师，涉及情绪困扰或自我伤害风险请联系当地心理援助热线。请勿将占卜结果作为唯一行动依据；最终决策与责任在您本人。

---

## 模板 3：关系 / 复合版

### 这段关系现在的主线
直接说明：靠近、拉扯、观望、消耗、回暖，还是断续反复。

### 你这边的状态
说明用户在这段关系里最强的需求和最容易失控的点。

### 对方的状态
讲对方当下更像回避、犹豫、纠结、保留、试探还是仍有情感。

### 阻力来源
拆成情绪、沟通、现实、信任、节奏。

### 近期氛围
描述这段时间关系内的氛围与节奏感（拉扯 / 回暖 / 观望 / 消耗），**不**给出「该不该联系 / 该不该逼结果 / 该不该复合 / 该不该结束」等具体决定。

### 自我反思方向（不出具行动指令）
讲用户自己可以怎样看待与调适这段关系（情绪管理、表达节奏、自我边界），**不**写「应该联系对方 / 应该当面摊牌 / 应该等对方先来」等具体动作；涉及关系是否继续/结束/复合/结婚等重大决定，请明确提示由用户本人在与对方充分沟通后判断；如有家暴 / 跟踪骚扰 / 自伤风险，请直接切到当地心理援助 / 反家暴 / 报警资源。

### 最后的提醒
不替对方下结论、不预测分合；如要给倾向，请写明这是「象征性参考」而非判决。

### ⚠️ 免责声明（必须保留）
本内容基于命理 / 占卜 / 玄学体系生成，仅供文化爱好与思维参考；不构成医疗、法律、心理、财务、婚姻或任何重大人生决策的专业建议。涉及关系是否继续/结束、是否复合、是否结婚等重大决定，请结合自身现实条件、与对方充分沟通后由您本人判断；涉及情绪困扰、家暴、跟踪骚扰等风险请立即联系当地心理援助 / 反家暴热线。

---

## 模板 4：塔罗三张牌版

### 先给整组感受
用 2-3 句概括整体牌风。

### 第一张牌 → 第二张牌 → 第三张牌
解释位置意义 + 牌义。

### 三张牌串联
把故事线讲出来。

### 自我反思方向（不出具行动指令）
讲用户在这件事上可以「自我反思 / 调整心态 / 留意什么信号」；**不**写「下一步去做 X / 该联系谁 / 该投什么 / 该签什么」等明确动作。如涉及健康/法律/财务/婚姻，请提示由用户本人结合专业意见决定。

### ⚠️ 免责声明（必须保留）
塔罗解读为象征性映射，仅作自我反思与文化参考；不构成医疗、法律、心理、财务、婚姻等专业建议。请勿将牌面作为唯一决策依据。

---

## 模板 5：道家玄学短断版

### 先说势
现在是顺、堵、转、虚热，还是蓄势。

### 再说人
用户在这个局里该主动、该守、该绕，还是该停。

### 再说时
近期有没有窗口，适合推进什么，不适合硬碰什么；只描述「气象偏向」，不锁定具体日期。

### 再说法（自我反思方向，不出具行动指令）
给 3 条「可以自我反思 / 留意 / 调整节奏」的方向；**不**写「投资 / 签合同 / 手术 / 搬家 / 起诉 / 复合 / 结婚 / 辞职 / 服药 / 停药 / 分手」等明确动作；涉及健康/法律/财务/婚姻/就业请提示由本人结合专业意见决定。

### 点透句
一句有势感的话收尾。

### ⚠️ 免责声明（必须保留）
本内容基于玄学体系生成，仅供文化爱好与思维参考；不构成医疗、法律、心理、财务、婚姻或任何重大决策的专业建议。

---

## 模板 6：风水 / 空间建议版

### 空间主问题
说明是堵、乱、暗、压、冲还是散。

### 为什么会影响状态
把空间问题翻译成人的感受：睡不稳、心散、执行差、关系烦躁。

### 调整建议
按优先级给 3-6 条。

### 观察点
调整后一周到两周，看哪些变化。

### ⚠️ 免责声明（必须保留）
风水建议属于文化与生活美学层面的环境优化建议，并非工程结构、消防、电气、采光等专业鉴定；涉及房屋安全、装修施工、电路改造请委托具备资质的专业人员，本内容不替代任何专业鉴定与施工方案。

---

## 模板 7：奇门遁甲排盘版

### 排盘信息
时间 | 阴阳遁 | 局数 | 值符 | 值使

### 九宫排盘表格
天盘/人盘/地盘/神盘四层

### 格局分析
值符值使 + 主要格局

### 方位吉凶表
八方位综合评级（仅作象征性参考，不作为出行 / 商务 / 医疗 / 法律决策依据）。

### 综合判断 + 自我反思方向（不出具行动指令）
给出综合气象判断，并给「可以自我反思 / 调整心态 / 留意节奏」的方向；**不**写「应该往 X 方位出行 / 应该今天签 / 应该今天动手术 / 应该今天起诉」等明确动作；涉及择时 / 商务 / 医疗 / 法律请提示由用户本人结合专业意见决定。

### ⚠️ 免责声明（必须保留）
奇门遁甲排盘解读仅供文化爱好与思维参考；不构成医疗、法律、心理、财务、婚姻或任何重大决策的专业建议。请勿将盘面格局作为唯一决策依据。

---

## 模板 8：九宫飞星版

### 年/月/日飞星入中
入中宫星名 + 五行

### 吉位列表
🟢 方位 + 星名 + 主事

### 凶位列表
🔴 方位 + 星名 + 注意事项

### 方位详解
逐方位分析

### 布局建议

### ⚠️ 免责声明（必须保留）
九宫飞星方位建议属于文化与生活美学层面的环境优化建议，并非工程结构、风水堪舆专业鉴定；不替代医疗、法律、财务等专业建议；涉及房屋安全、施工改造请委托具备资质的专业人员。

---

## 风格模板

### 老师傅直断风
- "先说结论。"
- "你这不是没运，是节奏错了。"
- 干脆、不粗暴、不恐吓、不给绝对判词

### 温和咨询风
- "我先把你现在最重的一块讲给你听。"
- 适合感情、迷茫、焦虑用户，有安抚感但不灌鸡汤

### 道门参悟风
- "这段时气不在猛冲，而在收心定势。"
- 有玄学味、有节奏感、不能空、最后一定要落地

---

## 结尾金句库

- "命不是一句话定死的，真正拉开差距的，是你怎么走下一步。"
- "有些运不是等来的，是先把自己站稳后才接得住。"
- "你现在最需要的，不是更急，而是更准。"
- "关系的答案，往往藏在你们各自不愿面对的那一块里。"
- "盘里给的是方向，真正落地的，还是你的选择和行动。"
- "局里最怕的不是慢，是心乱。"
- "先把势养起来，很多门才会开。"

---

## HTML 报告样式

生成精美 HTML 卡片时使用以下设计规范：

- **字体**：Noto Serif SC（衬线体）
- **背景**：深色渐变（#1a1a2e → #16213e → #0f3460）
- **卡片**：圆角 24px，暗色渐变底，金色边框
- **标题**：金色 #ffd700
- **核心结论**：绿色 #4ade80，24px 加粗
- **建议区**：金色渐变边框，✓ 前缀列表
- **金句**：金色斜体居中
- **页脚**：分割线 + 灰色小字免责声明


path: references/preparation.md
content: # 占卜准备指南

---

## 通用原则

### 任何解读之前
1. **明确目的**：有一个清晰的问题或关注领域
2. **保持开放**：不带刚性预期
3. **记录时间**：记下解读的确切日期/时间
4. **环境整洁**：选择安静、舒适的空间

### 心理准备
1. 静心：深呼吸几次
2. 专注：集中于你的问题
3. 诚心：以真诚的意图对待
4. 接受：对意外答案保持开放

---

## 各方法所需信息速查

> ### ⚠️ 隐私与最小化提醒（请先阅读）
>
> 下表列出的「必需信息」均属于**敏感个人数据**：包括出生年月日时、出生地/地址、姓名、双手或正面照片、家庭成员（如配偶/父母/子女）出生信息等。
> - **最小化原则**：只在你**真正想做对应解读**时才提供对应字段；不打算做该体系解读时**完全不需要**填写。
> - **本地存储**：本 skill 的所有用户档案仅写入本机 `data/profiles/<userId>.json`，不向任何外部服务上传，详见 [SKILL.md 数据留存与用户控制](../SKILL.md#-数据留存与用户控制隐私)。
> - **保留期**：保存到你主动 `node scripts/profile.js delete <userId>` 为止。该脚本默认采用**软删除（可 30 天内手动恢复）**，加 `--purge` 才会物理删除。
> - **照片/平面图等大体积数据**：本 skill 不会自动持久化为档案字段；请仅在当次会话中粘贴使用，会话结束后由你自行决定是否保存。
> - **未成年人**：尽量不要为未成年人长期保留档案；模板严禁对未成年人下宿命标签。
> - **家庭成员**：仅在确实需要合婚/亲子合盘等多体系交叉验证时才录入；任何时刻可用 `profile.js delete` / 直接编辑 JSON 删除。
> - **日志最小化**：`profile.js show / list` 默认脱敏；如需完整字段请用 `--full`，并避免在共享终端 / CI / cron 邮件中执行。
> - **不要把以下信息当作专业凭证**：不要据此作出医疗、法律、财务、婚姻、就业等重大决策。

| 方法 | 必需信息 | 可选信息 |
|------|---------|---------|
| 易经 / 六爻 | 明确问题 | 起卦时间 |
| 八字 | 出生年月日时 + 地点 | 性别 |
| 紫微斗数 | 出生年月日时 + 地点 | 性别 |
| 塔罗 | 一个清晰的问题 | 牌组偏好 |
| 西方星盘 | 出生日期 + 时间（精确到分钟）+ 地点 | — |
| 数字命理 | 出生日期 + 姓名 | — |
| 手相 | 双手清晰照片 | 年龄、性别 |
| 面相 | 正面清晰照片 | 年龄 |
| 风水 | 平面图/地址 + 朝向 | 居住者出生日期 |
| 奇门遁甲 | 排盘时间 + 问题 | 问事类型 |
| 梅花易数 | 问题 + 数字/时间/触发物 | — |

---

## 八字特别注意

| 项目 | 说明 |
|------|------|
| 出生时间 | 精确到时辰（2 小时），四柱命理以时辰划分 |
| 夏令时 | 要还原为标准时间 |
| 跨时区 | 注意出生地实际时区 |
| 子时 | 23:00-01:00，需确认早子时/晚子时 |

---

## 提问指南

### ❌ 错误方式
- 问模糊问题（"我会怎样？"）
- 一次问多个问题
- 期待绝对答案
- 忽略矛盾信息

### ✅ 正确方式
- 一次一个清晰问题
- "我应该……？" 或 "关于……我需要知道什么？"
- 接受指引而非预言
- 用解读获取洞察而非借口

---

## 各类问题示例

### 感情/关系
- "关于我目前的关系，我需要知道什么？"
- "我应该追求这段连接吗？"

### 事业/工作
- "什么职业路径适合我？"
- "什么时候适合求职？"

### 决策
- "在这个决定中我应该考虑什么？"
- "如果我选择 X，可能的结果是什么？"

### 通用指引
- "我现在需要知道什么？"
- "这个月/年我应该关注什么？"

---

## 重要提醒

1. **不是预言**：占卜显示趋势，不是固定未来
2. **可变**：能量会变化，结果也会
3. **配合**：解读与自由意志配合，而非对抗
4. **参考**：用指引做出明智选择

---

## 专业读者的红旗

| ✅ 好的迹象 | ❌ 危险信号 |
|------------|-----------|
| 清楚解释方法 | 保证特定结果 |
| 合理定价 | 推销昂贵套餐 |
| 愿意解释过程 | 拒绝解释方法 |
| 不做保证 | 声称超自然力量 |
| 有口碑/评价 | 索要过多个人信息 |


path: references/qimen-calculation-rules.md
content: # 奇门遁甲排盘计算规则

## 一、九宫格（洛书九宫）布局

```
巽四宫（东南）| 离九宫（南）  | 坤二宫（西南）
震三宫（东）  | 中五宫（中央） | 兑七宫（西）
艮八宫（东北）| 坎一宫（北）  | 乾六宫（西北）
```

洛书数口诀：戴九履一，左三右七，二四为肩，六八为足，五居中央。

## 二、四层盘面说明

| 盘面 | 内容 | 元素 |
|------|------|------|
| 地盘（第一层） | 三奇六仪，固定基础 | 乙丙丁（三奇）+ 戊己庚辛壬癸（六仪） |
| 天盘（第二层） | 九星，随值符移动 | 天蓬、天芮、天冲、天辅、天禽、天心、天柱、天任、天英 |
| 人盘（第三层） | 八门，随值使移动 | 休门、生门、伤门、杜门、景门、死门、惊门、开门 |
| 神盘（第四层） | 八神，随值符宫位旋转 | 值符、螣蛇、太阴、六合、白虎、玄武、九地、九天 |

中五宫无星门，天盘九星和人盘八门各只有八个，分布于八宫。

## 三、三奇六仪及排列顺序

**六仪**（阳干）：戊、己、庚、辛、壬、癸
**三奇**（阴干）：乙、丙、丁
**完整序列**：戊 己 庚 辛 壬 癸 丁 丙 乙

| 旬 | 旬首 | 六仪值 |
|----|------|-------|
| 甲子旬 | 甲子 | 戊 |
| 甲戌旬 | 甲戌 | 己 |
| 甲申旬 | 甲申 | 庚 |
| 甲午旬 | 甲午 | 辛 |
| 甲辰旬 | 甲辰 | 壬 |
| 甲寅旬 | 甲寅 | 癸 |

## 四、六甲隐遁规则

甲子隐于戊、甲戌隐于己、甲申隐于庚、甲午隐于辛、甲辰隐于壬、甲寅隐于癸。地盘显示九个符号（戊己庚辛壬癸丁丙乙），甲字不出现在盘面。

## 五、九星表

| 九星 | 固定宫位 | 五行 | 吉凶 | 象意 |
|------|---------|------|------|------|
| 天蓬星 | 坎一宫（北） | 水 | 凶 | 贼、险、水患、暗伏 |
| 天芮星 | 坤二宫（西南） | 土 | 凶 | 病、衰、死气、阴险 |
| 天冲星 | 震三宫（东） | 木 | 吉 | 雷动、奋进、有冲劲 |
| 天辅星 | 巽四宫（东南） | 木 | 吉 | 文昌、辅佐、学术利 |
| 天禽星 | 中五宫（中） | 土 | 中 | 居中调和，寄坤或艮宫 |
| 天心星 | 乾六宫（西北） | 金 | 吉 | 谋略、官贵、医药 |
| 天柱星 | 兑七宫（西） | 金 | 凶 | 毁折、口舌、破败 |
| 天任星 | 艮八宫（东北） | 土 | 吉 | 稳重、仁厚、积累财富 |
| 天英星 | 离九宫（南） | 火 | 凶 | 虚名、文采、好大喜功 |

## 六、八门表

| 八门 | 固定宫位 | 五行 | 吉凶 | 象意 |
|------|---------|------|------|------|
| 休门 | 坎一宫（北） | 水 | 吉 | 休息、平和、利谋私 |
| 死门 | 坤二宫（西南） | 土 | 大凶 | 死气、衰败、丧事 |
| 伤门 | 震三宫（东） | 木 | 凶 | 伤害、官司、竞争 |
| 杜门 | 巽四宫（东南） | 木 | 中 | 闭塞、隐藏、埋伏 |
| 景门 | 离九宫（南） | 火 | 吉 | 虚名、文书、扬名 |
| 开门 | 乾六宫（西北） | 金 | 吉 | 开拓、通达、谋事吉 |
| 惊门 | 兑七宫（西） | 金 | 凶 | 惊恐、口舌、不安 |
| 生门 | 艮八宫（东北） | 土 | 大吉 | 生机、财源、求财最旺 |

## 七、八神表

| 八神 | 排列顺序 | 含义 |
|------|---------|------|
| 值符 | 第1位 | 吉神之首，主权威、贵人 |
| 螣蛇 | 第2位 | 怪异、虚惊、缠绕 |
| 太阴 | 第3位 | 阴私、隐秘、女性 |
| 六合 | 第4位 | 合作、婚姻、和谐 |
| 白虎 | 第5位 | 凶猛、官讼、血光 |
| 玄武 | 第6位 | 盗贼、欺骗、阴谋 |
| 九地 | 第7位 | 伏藏、守土、固守 |
| 九天 | 第8位 | 升腾、公开、远行 |

排列规则：值符固定在旬首天干所在宫位，其余七神从该宫位起，按阳顺阴逆依次分布。

## 八、阴阳遁规则

| 节气区间 | 遁别 |
|---------|------|
| 冬至 → 夏至前 | 阳遁（顺时针） |
| 夏至 → 冬至前 | 阴遁（逆时针） |

## 九、用局表简化说明

**阳遁**：冬至上旬1局、小寒上旬4局、大寒上旬7局、立春上旬8局、雨水上旬2局、惊蛰上旬5局、春分上旬3局、清明上旬6局、谷雨上旬9局、立夏上旬7局、小满上旬1局、芒种上旬4局

**阴遁**：夏至上旬9局、小暑上旬6局、大暑上旬3局、立秋上旬2局、处暑上旬8局、白露上旬5局、秋分上旬7局、寒露上旬4局、霜降上旬1局、立冬上旬3局、小雪上旬9局、大雪上旬6局

每旬（约5天）局数递进：阳遁+1，阴遁-1，超过9/1则循环。

## 十、排盘七步流程

```
Step 1：确定阴阳遁 → 查当前日期所处节气
Step 2：定局数 → 查节气×旬首对照表
Step 3：排地盘 → 局数对应天干为起点，阳顺阴逆填入九宫
Step 4：排天盘 → 找旬首天干所在地盘宫位，安置值符星，其余平移
Step 5：排人盘 → 查当前时辰对应值使门，按原宫间距平移
Step 6：排神盘 → 值符星所在宫位=值符神位，其余阳顺阴逆
Step 7：标注中宫 → 天禽星阳遁寄坤二宫，阴遁寄艮八宫
```


path: references/qimen-framework.md
content: # 奇门遁甲解读框架

## 适用问题

奇门更适合看：
- 某件事现在适不适合推进
- 求职、面试、合作、沟通、谈判
- 见面、联系、复合窗口
- 近期阶段走势
- 该主动、试探，还是先守
- 方位吉凶

## 输入优先级

| 级别 | 条件 | 处理 |
|------|------|------|
| S 级 | 用户提供奇门盘截图或起局时间+问题背景 | 进入精读 |
| A 级 | 有准确问题+起念时间/起局时间 | 导向解读 |
| B/C 级 | 只有问题没有盘 | "奇门式阶段判断"，不假装具体门星神落宫准确 |

## 排盘计算

**严格按照 qimen-calculation-rules.md 中的公式和表格计算，绝不可凭印象估算。**

排盘七步流程：
1. 确定阴阳遁（冬至后阳遁，夏至后阴遁）
2. 定局数（查节气×旬首对照表）
3. 排地盘（局数对应天干为起点，阳顺阴逆）
4. 排天盘（值符星随旬首天干走）
5. 排人盘（值使门随时辰走）
6. 排神盘（值符所临宫位起，阳顺阴逆）
7. 标注中宫寄宫（阳遁寄坤二宫，阴遁寄艮八宫）

完整计算规则见：[qimen-calculation-rules.md](qimen-calculation-rules.md)

完整解读指南见：[qimen-interpretation-guide.md](qimen-interpretation-guide.md)

## 解读顺序

1. 先看整体局势：开、闭、阻、绕、藏、转
2. 再看用户位置：主动方、被动方、当前心态
3. 再看目标位置：事情、对方、机会、阻力
4. 再看动法：快进、慢推、绕行、暂避、借力
5. 最后看时间：近期窗口、暂缓期、观察期

## 写法要求

- 不要一上来堆"值符、值使、九星、八门、八神"
- 先翻成人话：这局到底是顺、堵、虚、乱、暗、转
- 真要提术语，也要顺手解释

## 输出格式（完整排盘）

```markdown
# 🧭 奇门遁甲排盘

> 时间：{YYYY年M月D日 HH:MM} | {阳/阴}遁 {X}局 | 值符：{星} | 值使：{门}

## 九宫排盘

| 宫位 | 巽四宫 | 离九宫 | 坤二宫 |
|------|--------|--------|--------|
| 天盘（九星） | {星} | {星} | {星} |
| 人盘（八门） | {门} | {门} | {门} |
| 地盘（奇仪） | {干} | {干} | {干} |
| 神盘（八神） | {神} | {神} | {神} |

## 格局分析 → 方位吉凶 → 综合判断 → 行动建议
```

## 常见结论模板

### 感情 / 复合
- "这局更像有念头，但不稳，联系可以试探，不适合逼结果。"
- "不是没有回应，而是对方心态飘，推进太猛反而关门。"

### 事业 / 合作
- "这件事能推进，但现在更适合铺垫，不适合当场拍板。"
- "机会不是没有，只是明面上看不到，要靠侧面切入。"

### 择时
- 用"适合联系、适合试探、适合收尾、适合观察"的语言
- 不写"某天不做就必凶"

## 禁止写法

- 没盘硬说盘面细节
- 把奇门当成绝对结果判决书
- 以奇门名义鼓励极端、违法、伤害性行为
- "此事必败""大凶不可动""会有血光之灾"


path: references/qimen-interpretation-guide.md
content: # 奇门遁甲解读指南

## 一、九星含义详解

| 九星 | 宫位 | 五行 | 吉凶 | 详解 |
|------|------|------|------|------|
| 天蓬星 | 坎一宫 | 水 | 凶 | 主贼盗、险境、暗中阴谋。遇休门减凶，遇死门大凶 |
| 天芮星 | 坤二宫 | 土 | 凶 | 主疾病、衰败。求医方面反有参考价值，宜静不宜动 |
| 天冲星 | 震三宫 | 木 | 吉 | 主奋进、突破。遇伤门有莽撞之嫌，需注意节奏 |
| 天辅星 | 巽四宫 | 木 | 吉 | 文昌贵气，利读书考试、文案谈判。遇开门/生门锦上添花 |
| 天禽星 | 中五宫 | 土 | 中 | 居中调和，寄宫判断。代表稳重、调停 |
| 天心星 | 乾六宫 | 金 | 吉 | 主谋略、官贵、医药。遇开门贵气倍增 |
| 天柱星 | 兑七宫 | 金 | 凶 | 主毁折、口舌是非。避免此方向签约或动工 |
| 天任星 | 艮八宫 | 土 | 吉 | 主稳重积累。遇生门财运最旺 |
| 天英星 | 离九宫 | 火 | 凶 | 主虚名、表面光鲜。问文艺宣传可用，问实质财运不吉 |

## 二、八门含义详解

| 八门 | 宫位 | 吉凶 | 详解 |
|------|------|------|------|
| 休门 | 坎一宫 | 吉 | 主休息、平和。利出行、谋私事，低调推进更顺 |
| 死门 | 坤二宫 | 大凶 | 主死气、终结。任何开拓求进均不宜，唯葬事收藏有用 |
| 伤门 | 震三宫 | 凶 | 主伤害、争斗。竞争性事项可借此门之气 |
| 杜门 | 巽四宫 | 中 | 主隐藏、闭塞。利隐蔽行动、保密谋划 |
| 景门 | 离九宫 | 吉 | 主文书、扬名。求名宣传科考均宜，实质财运不强 |
| 开门 | 乾六宫 | 吉 | 八门中综合最吉。出行求财谒贵开业均宜 |
| 惊门 | 兑七宫 | 凶 | 主惊恐、口舌。宜静不宜动，需防突发变故 |
| 生门 | 艮八宫 | 大吉 | 主生机、财源。求财生意开张首选方位 |

## 三、八神含义详解

| 八神 | 含义 |
|------|------|
| 值符 | 吉神之首。所在宫位代表权威与贵人，行事得值符方向支持则事半功倍 |
| 螣蛇 | 凶神。主怪异变化、虚惊、缠绕纠缠，需防阴谋与幻象 |
| 太阴 | 阴神。主隐秘、女性力量。利保密和暗中行事 |
| 六合 | 吉神。主合作、婚姻、和谐。人际关系顺畅 |
| 白虎 | 凶神。主凶猛、争斗、官讼、血光。需格外谨慎 |
| 玄武 | 凶神。主盗贼、欺骗。财物保管和信任关系需注意 |
| 九地 | 中性偏吉。主伏藏、守土。适合守成防御 |
| 九天 | 吉神。主升腾、公开、远行。适合展示自我 |

## 四、常见奇门格局

### 吉格
- **青龙返首**：开门+天心星临乾六宫，谋事一帆风顺
- **飞鸟跌穴**：值符星与值使门同临一宫且为本宫，所求极易成功
- **三奇得使**：乙丙丁三奇同临某宫，文事武功均大利
- **天遁**：丙奇临生门或开门，阳气充盈如天助
- **地遁**：乙奇临开门或休门，柔中带吉
- **人遁**：丁奇临生门，人气旺盛利人际

### 凶格
- **白虎猖狂**：白虎神临开门或生门，凶神占据吉位
- **螣蛇夭矫**：螣蛇神临景门，文书不实需核实
- **大格（入墓格）**：天干落入所克宫位，事项受阻
- **返吟**：天盘与地盘完全相同，局势僵固
- **伏吟**：天盘各宫与本宫重叠，万事停滞
- **刑格**：天干地支相刑，被人制约或自我束缚

## 五、方位吉凶判断规则

**综合判断优先级**：
1. 八神（影响最大）：值符、六合、九天为吉；白虎、螣蛇、玄武为凶
2. 八门（次重要）：生门、开门、休门为大吉；死门、惊门、伤门为凶
3. 九星（辅助判断）：天心、天辅、天任、天冲为吉
4. 奇仪（基础加权）：乙丙丁三奇为吉；庚辛癸为凶

**方位综合评级**：
- 三层皆吉 → 大吉方位，可全力进取
- 两吉一凶 → 吉方，谨慎而行
- 一吉两凶 → 中平偏凶，不宜主动
- 三层皆凶 → 大凶方位，务必回避

## 六、表达风格

**开头范式**：
- "此盘{阳/阴}遁{X}局，值符{星}临{宫}，值使{门}临{宫}..."

**格局描述**：
- 吉格："{格局名}之象已现，{方位}方有贵气聚集，宜..."
- 凶格："{宫位}方有{格局名}之兆，此时此方向有一定阻力，建议..."

## 七、禁用表达

| 禁止使用 | 替换方案 |
|---------|---------|
| "此事必败/必成" | "此时格局对该事项{利/不利}，建议..." |
| "大凶，绝对不能动" | "此方位目前气场偏凶，宜暂缓" |
| "会有血光之灾" | "白虎临此宫，出行需注意安全" |
| "命中注定" | "当前时局显示..." |

绝对禁止：死亡预测、绝对化断言、恐吓性语言、推销改运服务。


path: references/relationship-and-timing.md
content: # 关系、复合、窗口期与阶段节奏

## 关系问题必须拆层

不要只回答"会不会""是不是正缘"。至少拆成：

1. 双方当前状态
2. 关系吸引的来源
3. 主要矛盾与阻力
4. 谁更主动，谁更回避
5. 近期有没有窗口
6. 正确做法和错误做法
7. 长期可能性

---

## 复合问题推荐结构

### 1. 先看情感残留
- 还有没有挂念
- 断得干不干净
- 是想念对方，还是想念当初的感觉

### 2. 再看现实阻碍
- 沟通模式、信任问题
- 距离、家庭、现实条件
- 重复矛盾是否真的解决

### 3. 再看窗口
- 近期更适合观察
- 接下来 1-3 个月有试探性靠近机会
- 短期有联系迹象，但不适合逼结果
- 现在不是强推节点，更适合先把关系温度拉回来

### 4. 再给动作建议
- 先恢复正常沟通
- 先停止情绪追问
- 先让对方重新感受到轻松感
- 先明确底线，不要为了复合无条件退让

---

## 暧昧 / 新关系问题

重点讲：
- 吸引是否真实
- 谁在试探，谁在观望
- 推进速度合不合适
- 是短期热度还是可发展关系
- 哪一步最关键

---

## 事业 / 财富类"时间节奏"

| 时间表达 | 含义 |
|---------|------|
| 近期 | 未来 2-6 周 |
| 接下来一个阶段 | 未来 1-3 个月 |
| 中期 | 未来 3-6 个月 |
| 今年这段周期 | 当年剩余时间 |

不要写：某天某时一定会发生、某月必然升职/分手/发财

---

## 择时建议的边界

**可以写**：
- 更适合沟通
- 更适合整理、复盘、收尾
- 更适合试探，不适合压迫
- 更适合慢慢推，不适合硬上结果

**不要写**：
- 今天必须做某决定否则必凶
- 不照做就一定出事

---

## 增强要求

- 关系版必须说"短期、条件、风险、正确推进方式"
- 不许只扔"能复合 / 不能复合"
- 要写清楚：回来之后值不值得，问题有没有被解决

---

## 好的结尾方式

- "这段关系不是没机会，而是要先换相处方式。"
- "窗口不是没有，但你不能带着旧问题去要新结果。"
- "你真正要判断的，不只是他回不回来，而是这段关系回来后值不值得继续。"


path: references/safety-and-ethics.md
content: # 安全边界与伦理要求

## 总原则

这是命理/占卜/象征性解读技能。它可以提供文化性的、反思性的、叙事性的参考，但不能替代专业判断。

**一句话原则**：给人方向，不给人枷锁。给人提醒，不给人恐吓。给人可执行的建议，不给人"被命运判决"的绝望感。

---

## 绝对不能做的事

### 1. 医疗替代
不能把解读当成：疾病诊断、用药建议、手术建议、心理危机处置、生死判断。

如果用户涉及明显健康风险，只能给生活层面的提醒，并建议尽快寻求专业帮助。

### 2. 法律 / 财务 / 投资替代
不能把命理结果写成：必赚/必亏、该不该签法律文件的唯一依据、投资承诺、借贷担保指令、赌博下注建议。

### 3. 恐吓、勒索、操控
不能写：
- "你有血光之灾"
- "你被下咒了"
- "不做法事就会怎样"
- "必须花钱化解"
- "你命里天生克亲人"

### 4. 自伤、报复、暴力暗示
如果用户把占卜用于报复、伤害他人、自伤、极端行为正当化，不能顺着提供"天命建议"，要明确转向安全支持。

### 5. 未成年人宿命化标签
不能给未成年人贴死标签：注定失败、注定不孝、注定婚姻不好、注定身体有灾。

### 6. 妄想 / 偏执放大
不能把没有证据的"被控制、被诅咒、被跟踪、被下法"当作事实强化。应保持中性，不帮用户升级偏执叙事。

### 7. 使用用户现实信息
不能使用用户简历、职位、工作经历等现实信息作为分析依据。玄学推算完全依靠玄学工具本身。

---

## 推荐表述 vs 禁止表述

| ✅ 推荐 | ❌ 禁止 |
|---------|---------|
| 倾向、课题、阶段 | 必定、注定、绝对 |
| 容易出现、更适合 | 逃不掉、一辈子都 |
| 需要留意 | 一定会出事 |
| 这段时间的重心 | 百分之百 |
| 你现在更像处在…… | 命中注定失败 |

---

## 当用户很脆弱时

优先使用：
- 温和咨询风
- 共情 + 结构化分析 + 现实建议
- 不刺激、不夸大、不恐吓

---

## 对"健康"的可说范围

**可以**：讲作息、压力、情绪、能量管理、休息、节奏。说"这段时间要更重视身体信号和休息质量"。

**不能**：判断病名、判断生死、替代医生结论。

---

## 对"死亡、灾祸、血光"的处理

不要主动制造恐惧。如果用户主动问，也不要直给灾祸剧本。应转成：风险意识、减少冲动、注意安全、做好体检/合规/复核/稳妥安排。

---

## 对"改命、化解"的处理

**可以提供**：
- 生活层面的调整建议
- 沟通、节奏、边界、习惯、计划的优化建议
- 仪式感建议（如写下目标、整理空间、沉淀情绪）但要明确其象征性

**不能提供**：
- 保证有效的神秘服务
- 收费化解承诺
- 伪科学绝对治疗


path: references/tarot-framework.md
content: # 塔罗分析框架

## 核心原则

塔罗不是死背牌义，而是"牌义 + 位置 + 正逆位 + 牌与牌的关系 + 用户问题"共同成像。

## 进入方式

### 方式 A：用户已抽牌
直接读取：牌阵名称、牌位、每张牌、正位/逆位、问题背景

### 方式 B：用户未抽牌
优先建议用户自己抽。若用户要你直接看，可以采用"象征性抽牌式解读"，明确说明：
- 这是投射/反思型阅读
- 用来帮助整理问题和趋势
- 不是固定未来的裁决

> ### ⚠️ 不可越界（无论方式 A 还是方式 B 都必须遵守）
>
> 塔罗解读为**象征性映射**，**不**构成下列任一专业意见：
> - **医疗**：不得据此决定就医、用药、停药、手术、是否怀孕/流产/接受治疗。涉及健康风险或自伤倾向请就医或拨打当地心理援助热线。
> - **法律**：不得据此决定起诉、应诉、签订/解除合同、维权策略、刑事自首/认罪。涉及法律事务请咨询律师。
> - **财务/投资**：不得据此决定买卖、加仓减仓、借贷担保、参与某项投资。涉及钱财请咨询持牌财务/受讯投顾。
> - **婚姻/重大关系**：不得替用户决定复合、分手、结婚、离婚、出轨、生育；不得替对方下结论或预测分合。
> - **就业**：不得据此决定辞职、跳槽、创业、合伙；具体劳动争议请咨询劳动法律师。
> - **未成年人**：不要为未成年人下命运/性向/学业/人格等定性结论。
>
> 当用户的提问明显落在以上任一域时，必须**先**给出"请咨询专业人士"提示，再用塔罗给「氛围/反思方向」性质的回应；**不**给具体行动指令。

## 推荐牌阵

| 牌阵 | 张数 | 适合 |
|------|------|------|
| 三张牌 | 3 | 最近会怎样、他怎么想、要不要继续 |
| 五张牌 | 5 | 感情结构、中等复杂问题、选择题对比 |
| 凯尔特十字 | 10 | 复杂人生问题、长期困局、多因素叠加 |

### 三张牌常见位置
- 过去 / 现在 / 走向
- 现状 / 障碍 / 建议
- 你 / 对方 / 关系走势

## 解读步骤

### 1. 先给总感受
- "这组牌不轻，说明你心里其实已经累了很久。"
- "牌面不是没机会，而是你现在更卡在犹豫和边界不清。"

### 2. 逐张解释
每张牌都要同时看：基本牌义、该位置含义、正逆位偏向、与旁边牌的呼应或冲突

### 3. 讲出故事线
不要一张张孤立解释，要回答：情绪线、行动力线、关系主要矛盾、结果为什么会那样走

### 4. 给建议
建议要具体：先停/先问/先观察/先表达/先收边界，不要只说"顺其自然"

## 增强要求

- 除了牌义，还要写出"这组牌的氛围"和"真正的核心问题"
- 关系问题必须说明谁更回避、谁更执着、谁更想要结果
- 事业问题必须说明是能力问题、环境问题、节奏问题，还是情绪问题

## 大阿尔卡纳速查

| 编号 | 牌名 | 核心含义 |
|------|------|---------|
| 0 | 愚者 The Fool | 新开始、天真、冒险 |
| I | 魔术师 The Magician | 显化、力量、创造 |
| II | 女祭司 The High Priestess | 直觉、秘密、内在智慧 |
| III | 女皇 The Empress | 丰盛、滋养、创造力 |
| IV | 皇帝 The Emperor | 权威、结构、秩序 |
| V | 教皇 The Hierophant | 传统、指引、信仰 |
| VI | 恋人 The Lovers | 选择、结合、价值观 |
| VII | 战车 The Chariot | 决心、胜利、意志力 |
| VIII | 正义 Justice | 公平、真相、因果 |
| IX | 隐者 The Hermit | 内省、指引、独处 |
| X | 命运之轮 Wheel of Fortune | 命运、循环、转折 |
| XI | 力量 Strength | 勇气、耐心、内在力量 |
| XII | 倒吊人 The Hanged Man | 臣服、新视角、等待 |
| XIII | 死神 Death | 转化、结束、重生 |
| XIV | 节制 Temperance | 平衡、耐心、调和 |
| XV | 恶魔 The Devil | 阴影、束缚、执念 |
| XVI | 塔 The Tower | 剧变、揭示、崩塌重建 |
| XVII | 星星 The Star | 希望、更新、信念 |
| XVIII | 月亮 The Moon | 幻象、恐惧、潜意识 |
| XIX | 太阳 The Sun | 喜悦、成功、光明 |
| XX | 审判 Judgement | 反思、重生、觉醒 |
| XXI | 世界 The World | 完成、圆满、成就 |

## 禁止写法

- "牌已经决定你的命运"
- "你一定会怎样"
- "抽到这张牌就是绝对不可能"


path: references/western-methods.md
content: # 西方占卜方法百科

---

## 塔罗 (Tarot)

**概述**：源自 15 世纪意大利的卡牌占卜体系。78 张牌分为大阿尔卡纳（22 张）和小阿尔卡纳（56 张）。

**流程**：洗牌 → 切牌 → 布局 → 读牌 → 综合叙事

**所需信息**：问题或生活领域，无需出生资料

**常用牌阵**：

| 牌阵 | 张数 | 用途 |
|------|------|------|
| 单张 | 1 | 每日指引 |
| 三张牌 | 3 | 过去/现在/未来 |
| 凯尔特十字 | 10 | 综合深度解读 |
| 马蹄形 | 7 | 扩展情境分析 |

**小阿尔卡纳花色**：权杖（火/行动）、圣杯（水/情感）、宝剑（风/思维）、星币（土/物质）

---

## 西方星盘 (Astrology)

**概述**：通过行星位置解读人格与预测事件。

**所需信息**：✅ 出生日期、✅ 出生时间（精确到分钟）、✅ 出生地点

**核心组件**：
- 十二星座
- 十大行星：太阳、月亮、水星、金星、火星、木星、土星、天王星、海王星、冥王星
- 十二宫位
- 相位：合相、对冲、三合、四分、六合

**盘面类型**：本命盘、行运盘、推运盘、合盘（关系）

---

## 数字命理 (Numerology)

**概述**：通过姓名和出生日期推算的数字揭示性格与人生模式。基于毕达哥拉斯原理。

**所需信息**：出生日期 + 姓名

**核心数字**：生命路径数、表达数、灵魂冲动数、人格数、生日数

---

## 卢恩符文 (Runes)

**概述**：北欧占卜体系，使用 24 个古日耳曼符文（Elder Futhark）。

**所需信息**：问题或意图，无需个人资料

**常用牌阵**：3 符文（过去/现在/未来）、5 符文、9 符文

---

## 水晶球占卜 (Crystal Gazing / Scrying)

**概述**：凝视反射表面接收异象的古老实践。

**工具**：水晶球、黑镜、水池、黑曜石

---

## 茶叶占卜 (Tasseography)

**概述**：通过茶叶残留图案占卜。流行于中东、英国和希腊。

**流程**：泡散叶茶 → 饮至剩渣 → 翻转杯子 → 等待沉淀 → 解读图案

---

## 神谕卡 (Oracle Cards)

**概述**：现代占卜卡牌，主题多样（天使、女神、动物灵等），比塔罗更灵活简单。

**常见类型**：天使卡、女神卡、精灵卡、动物灵导卡、原型卡


path: references/yijing-divination-framework.md
content: # 六爻 / 易经卦象 / 梅花易数框架

## 总原则

六爻、梅花都属于"起象—读势—看变"的体系。先说事态主线，再说变化，再说建议。

---

## 一、六爻框架

### 适用问题
- 这件事能不能成
- 对方会不会联系
- 合作能不能谈成
- 关系有没有后续
- 某个选择哪边更顺

### 有完整卦盘时
重点看：世应关系、用神状态、动爻变化、合冲生克、空破/进退/反复感

### 起卦方式

**铜钱法**：
1. 用户集中精神想问题
2. 掷 6 次铜钱（或随机数模拟）
3. 记录结果：6=老阴（动爻）、7=少阳、8=少阴、9=老阳（动爻）
4. 从下往上排成六爻
5. 识别动爻（6 和 9）

**纳甲法要素**：
- 十天干：甲乙丙丁戊己庚辛壬癸
- 十二地支：子丑寅卯辰巳午未申酉戌亥
- 五行：木火土金水
- 六亲：父母、兄弟、子孙、妻财、官鬼
- 六神：青龙、朱雀、勾陈、螣蛇、白虎、玄武

### 写法要求
- 不需要把所有技术细节都写出来
- 要把卦盘翻译成人话：谁更主动、谁更犹豫、事情卡在哪、是短阻还是长阻、有无回头信号

### 常用句式
- "这卦不是无缘，而是中间有拖延与反复。"
- "你们之间还有气，但不是立刻就能落地成结果。"
- "事情能成，但要走曲线，不是一步到位。"

---

## 二、梅花易数框架

### 适用问题
- 没有完整资料，但想快速看一眼
- 当下起念、突然来的问题
- 想看主线和变化，不追求精密细节

### 起卦方式

**数字起卦**：
- 第 1 数 ÷ 8 余数 = 上卦
- 第 2 数 ÷ 8 余数 = 下卦
- 第 3 数 ÷ 6 余数 = 动爻

**时间起卦**：
- 年 + 月 + 日 = 上卦
- 年 + 月 + 日 + 时 = 下卦
- 时 = 动爻

**其他入口**：方位、颜色、突发事件取象、用户当下最强念头

### 八卦对应

| 余数 | 卦 | 五行 | 象 |
|------|-----|------|-----|
| 1 | 乾 | 金 | 天 |
| 2 | 兑 | 金 | 泽 |
| 3 | 离 | 火 | 火 |
| 4 | 震 | 木 | 雷 |
| 5 | 巽 | 木 | 风 |
| 6 | 坎 | 水 | 水 |
| 7 | 艮 | 土 | 山 |
| 0(8) | 坤 | 土 | 地 |

### 解读顺序
1. 先说本象：现在的局是稳、乱、散、阻、转还是开
2. 再说体用：谁是核心，谁是外力
3. 再说变化：是缓慢改善、先难后顺，还是表面热闹实则虚
4. 最后给建议：动还是守，快还是慢，进还是绕

### 典型句式
- "这象不是坏，是外面看着热，里面还没定。"
- "你这件事更适合先收，再放，不能急着要一个明确结果。"
- "眼下最重要的不是答案，而是先把自己的位置站稳。"

---

## 三、64 卦速查

| 卦名 | 上卦 | 下卦 | 卦义 |
|------|------|------|------|
| 乾为天 | 乾 | 乾 | 刚健中正，大吉 |
| 坤为地 | 坤 | 坤 | 柔顺承载，吉 |
| 水雷屯 | 坎 | 震 | 起始艰难，守正 |
| 山水蒙 | 艮 | 坎 | 启蒙教育，亨 |
| 水天需 | 坎 | 乾 | 等待时机，吉 |
| 天水讼 | 乾 | 坎 | 争讼不利，慎 |
| 地水师 | 坤 | 坎 | 出师征战，险 |
| 水地比 | 坎 | 坤 | 亲密比辅，吉 |

*（完整 64 卦请参考《周易》原文）*

---

## 四、共同边界

- 不把卦象说成必然判决
- 不用卦象鼓动自伤、复仇、违法
- 不写"必死、必灾、必败"这种恐吓式语言
- 不在资料不足时装作已经做完完整纳甲或严密起盘


path: references/ziwei-framework.md
content: # 紫微斗数分析框架

## 目的

在用户给了盘面或完整出生资料时，提供更像紫微斗数老师的结构化解读；资料不足时，不假装已经精确落星。

## 核心思路

优先看：
1. 命宫 / 身宫
2. 事业宫
3. 财帛宫
4. 夫妻宫
5. 迁移宫
6. 福德宫
7. 疾厄宫（只做生活提醒，不做医疗结论）
8. 父母 / 兄弟 / 交友等辅助宫位

## 解读顺序

### 1. 先讲命宫气质
- 命格整体偏主动还是偏守成
- 更靠个人推动还是外部资源
- 是外显型、内耗型、战略型、韧性型

### 2. 再讲身宫或现实表现
命宫偏底色，身宫偏现实运作方式。写法上要讲"这个人怎么过日子、怎么做决定、怎么扛压力"。

### 3. 再讲问题相关宫位

**感情**：看夫妻宫 + 福德宫 + 迁移宫 + 交友/命宫互动

**事业**：看官禄/事业宫 + 财帛宫 + 迁移宫 + 命宫

**财富**：看财帛宫 + 田宅/事业/福德互动

### 4. 如果用户给了四化
可讲：哪些领域会被拉动、哪些领域会有牵制、当前阶段是冲出来/卡住/转向/沉淀

## 增强要求

- 紫微增强版必须写出"主线、暗线、外部条件、阶段变化"
- 不只报星名，要说明这些结构怎么落在现实中
- 要回答：这人是怎么做决定的、怎么处理关系的、怎么面对压力的

## 当没有盘面时

可用"紫微导向分析"但必须声明：
- "你没给盘面，我这里先按紫微的读法给你看主线，不把具体星曜落宫说死。"
- "如果你把盘面截图发来，我能把命宫、事业宫、夫妻宫再细拆一层。"

## 典型句式

- "你这命盘的重点，不是没机会，而是机会来了你是否敢接。"
- "你的夫妻位更像先高标准再慢热，不是随便进入关系的人。"
- "你事业位怕的不是苦，而是方向被杂事打散。"
- "福德位提示你压力一大就容易把情绪藏住，外面看不出来，里面却很耗。"

## 禁止写法

- 假装看到了用户没有提供的星曜组合
- 把某颗煞星直接写成灾祸判决
- 把宫位结果绝对化


path: scripts/bazi-analysis.js
content: #!/usr/bin/env node
/**
 * 八字深度分析模块
 * 基于《穷通宝鉴》《子平真诠》《滴天髓》三大经典
 * 实现：调候用神、日主强弱、用神格局、十神、阴阳刚柔
 */

// ─────────────────────────────────────────────
// 基础数据表
// ─────────────────────────────────────────────

const STEM_ELEMENT = {
  '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
  '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水'
};

const STEM_YINYANG = {
  '甲': '阳', '乙': '阴', '丙': '阳', '丁': '阴', '戊': '阳',
  '己': '阴', '庚': '阳', '辛': '阴', '壬': '阳', '癸': '阴'
};

const BRANCH_ELEMENT = {
  '子': '水', '丑': '土', '寅': '木', '卯': '木',
  '辰': '土', '巳': '火', '午': '火', '未': '土',
  '申': '金', '酉': '金', '戌': '土', '亥': '水'
};

const ELEMENT_PRODUCES = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const ELEMENT_RESTRAIN = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
const ELEMENT_GENERATED_BY = { '木': '水', '火': '木', '土': '火', '金': '土', '水': '金' };

// 地支藏干（主气/中气/余气）
const BRANCH_HIDDEN = {
  '子': { 主气: '癸', 中气: '壬', 余气: null },
  '丑': { 主气: '己', 中气: '辛', 余气: '癸' },
  '寅': { 主气: '甲', 中气: '丙', 余气: '戊' },
  '卯': { 主气: '乙', 中气: null, 余气: null },
  '辰': { 主气: '戊', 中气: '乙', 余气: '癸' },
  '巳': { 主气: '丙', 中气: '庚', 余气: '戊' },
  '午': { 主气: '丁', 中气: '己', 余气: null },
  '未': { 主气: '己', 中气: '丁', 余气: '乙' },
  '申': { 主气: '庚', 中气: '壬', 余气: '戊' },
  '酉': { 主气: '辛', 中气: null, 余气: null },
  '戌': { 主气: '戊', 中气: '辛', 余气: '丁' },
  '亥': { 主气: '壬', 中气: '甲', 余气: '戊' },
};

// ─────────────────────────────────────────────
// 十神表（以日主为基准）
// ─────────────────────────────────────────────
const TEN_GODS_TABLE = {
  '甲': { '甲': '比肩', '乙': '劫财', '丙': '食神', '丁': '伤官', '戊': '偏财', '己': '正财', '庚': '七杀', '辛': '正官', '壬': '偏印', '癸': '正印' },
  '乙': { '甲': '劫财', '乙': '比肩', '丙': '伤官', '丁': '食神', '戊': '正财', '己': '偏财', '庚': '正官', '辛': '七杀', '壬': '正印', '癸': '偏印' },
  '丙': { '甲': '偏印', '乙': '正印', '丙': '比肩', '丁': '劫财', '戊': '食神', '己': '伤官', '庚': '偏财', '辛': '正财', '壬': '七杀', '癸': '正官' },
  '丁': { '甲': '正印', '乙': '偏印', '丙': '劫财', '丁': '比肩', '戊': '伤官', '己': '食神', '庚': '正财', '辛': '偏财', '壬': '正官', '癸': '七杀' },
  '戊': { '甲': '七杀', '乙': '正官', '丙': '偏印', '丁': '正印', '戊': '比肩', '己': '劫财', '庚': '食神', '辛': '伤官', '壬': '偏财', '癸': '正财' },
  '己': { '甲': '正官', '乙': '七杀', '丙': '正印', '丁': '偏印', '戊': '劫财', '己': '比肩', '庚': '伤官', '辛': '食神', '壬': '正财', '癸': '偏财' },
  '庚': { '甲': '偏财', '乙': '正财', '丙': '七杀', '丁': '正官', '戊': '偏印', '己': '正印', '庚': '比肩', '辛': '劫财', '壬': '食神', '癸': '伤官' },
  '辛': { '甲': '正财', '乙': '偏财', '丙': '正官', '丁': '七杀', '戊': '正印', '己': '偏印', '庚': '劫财', '辛': '比肩', '壬': '伤官', '癸': '食神' },
  '壬': { '甲': '食神', '乙': '伤官', '丙': '偏财', '丁': '正财', '戊': '七杀', '己': '正官', '庚': '偏印', '辛': '正印', '壬': '比肩', '癸': '劫财' },
  '癸': { '甲': '伤官', '乙': '食神', '丙': '正财', '丁': '偏财', '戊': '正官', '己': '七杀', '庚': '正印', '辛': '偏印', '壬': '劫财', '癸': '比肩' },
};

// ─────────────────────────────────────────────
// 穷通宝鉴调候用神表（120条）
// ─────────────────────────────────────────────
const TIAO_HOU_TABLE = {
  // 甲木
  '甲寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '庚', 说明: '寅月木寒，丙火为君，癸水为佐' },
  '甲卯': { 主用神: ['丁', '丙'], 优先级: '丁先', 忌神: '庚', 说明: '卯月木旺，丁火泄秀，忌金' },
  '甲辰': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '辰月土旺，先庚后丁' },
  '甲巳': { 主用神: ['癸', '丁'], 优先级: '癸先丁后', 忌神: '庚', 说明: '巳月火旺，癸水调候' },
  '甲午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丁', 说明: '午月火旺，水为调候' },
  '甲未': { 主用神: ['丁', '庚'], 优先级: '丁先', 忌神: '癸', 说明: '未月土月，用丁庚' },
  '甲申': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '申月金旺，庚劈甲引丁' },
  '甲酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '庚', 说明: '酉月金旺，丁火制金' },
  '甲戌': { 主用神: ['庚', '丁'], 优先级: '庚先丁后', 忌神: '癸', 说明: '戌月金土，用庚丁' },
  '甲亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷，丙火调候' },
  '甲子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒，丙戊并用' },
  '甲丑': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '丑月寒湿，丁火暖局' },
  // 乙木
  '乙寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '寅月木寒，丙癸双清' },
  '乙卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '卯月木旺，丙癸调候' },
  '乙辰': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '乙', 说明: '辰月湿土，癸水润乙' },
  '乙巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '辛', 说明: '巳月火旺，癸水调候' },
  '乙午': { 主用神: ['癸', '壬'], 优先级: '癸先', 忌神: '丙', 说明: '午月火旺，癸水制火' },
  '乙未': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '未月土月，丙癸并用' },
  '乙申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '申月金旺，丙癸并用' },
  '乙酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺，丙火制金' },
  '乙戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土，癸水润局' },
  '乙亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '亥月水冷，丙戊暖局' },
  '乙子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '辛', 说明: '子月水寒，丙戊调候' },
  '乙丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '辛', 说明: '丑月寒湿，丙丁暖局' },
  // 丙火
  '丙寅': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '癸', 说明: '寅月木火，壬水通月令' },
  '丙卯': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '甲', 说明: '卯月木旺，壬癸制火' },
  '丙辰': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '戊', 说明: '辰月湿土，壬水通根' },
  '丙巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '戊', 说明: '巳月火旺，壬水为用' },
  '丙午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极，壬水调候' },
  '丙未': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '未月土月，壬庚并用' },
  '丙申': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '庚', 说明: '申月金水，壬水通根' },
  '丙酉': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '辛', 说明: '酉月金旺，壬癸制火' },
  '丙戌': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丁', 说明: '戌月土金，壬甲并用' },
  '丙亥': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '辛', 说明: '亥月水冷，甲木生火' },
  '丙子': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '癸', 说明: '子月水旺，甲木生丙' },
  '丙丑': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '己', 说明: '丑月寒湿，壬甲暖局' },
  // 丁火
  '丁寅': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '寅月木旺，甲木生丁' },
  '丁卯': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '卯月木旺，甲丙生丁' },
  '丁辰': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '辰月土月，甲庚并用' },
  '丁巳': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '戊', 说明: '巳月火旺，甲庚制火' },
  '丁午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺，壬癸调候' },
  '丁未': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '丁', 说明: '未月土月，甲庚并用' },
  '丁申': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '壬', 说明: '申月金旺，甲丙生丁' },
  '丁酉': { 主用神: ['甲', '丙'], 优先级: '甲先丙后', 忌神: '癸', 说明: '酉月金旺，甲丙生丁' },
  '丁戌': { 主用神: ['甲', '壬'], 优先级: '甲先壬后', 忌神: '丁', 说明: '戌月燥土，壬水润局' },
  '丁亥': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '壬', 说明: '亥月水冷，甲庚暖局' },
  '丁子': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '子月水寒，甲庚暖局' },
  '丁丑': { 主用神: ['甲', '庚'], 优先级: '甲先庚后', 忌神: '癸', 说明: '丑月寒湿，甲庚暖局' },
  // 戊土
  '戊寅': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '寅月木旺，丙甲并用' },
  '戊卯': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '卯月木旺，丙甲并用' },
  '戊辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '辰月湿土，丙癸调候' },
  '戊巳': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '巳月火旺，丙癸并用' },
  '戊午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '午月火旺极，壬癸调候' },
  '戊未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月，癸水润局' },
  '戊申': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '壬', 说明: '申月金旺，丙丁暖局' },
  '戊酉': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '酉月金旺，丙丁暖局' },
  '戊戌': { 主用神: ['甲', '丁'], 优先级: '甲先丁后', 忌神: '壬', 说明: '戌月燥土，甲丁调候' },
  '戊亥': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '亥月水冷，丙甲暖局' },
  '戊子': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '壬', 说明: '子月水寒，丙甲暖局' },
  '戊丑': { 主用神: ['丙', '甲'], 优先级: '丙先甲后', 忌神: '癸', 说明: '丑月寒湿，丙甲暖局' },
  // 己土
  '己寅': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '寅月木旺，丙癸暖局' },
  '己卯': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '甲', 说明: '卯月木旺，丙癸暖局' },
  '己辰': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '乙', 说明: '辰月湿土，丙癸调候' },
  '己巳': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '甲', 说明: '巳月火旺，癸水润局' },
  '己午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丙', 说明: '午月火旺，癸壬调候' },
  '己未': { 主用神: ['癸', '丙'], 优先级: '癸先丙后', 忌神: '己', 说明: '未月土月，癸水润局' },
  '己申': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '壬', 说明: '申月金旺，丙癸暖局' },
  '己酉': { 主用神: ['丙', '癸'], 优先级: '丙先癸后', 忌神: '辛', 说明: '酉月金旺，丙癸暖局' },
  '己戌': { 主用神: ['癸', '辛'], 优先级: '癸先辛后', 忌神: '丙', 说明: '戌月燥土，癸水润燥' },
  '己亥': { 主用神: ['丙', '辛'], 优先级: '丙先辛后', 忌神: '壬', 说明: '亥月水冷，丙辛暖局' },
  '己子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '子月水寒，丙丁暖局' },
  '己丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿，丙丁暖局' },
  // 庚金
  '庚寅': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '寅月木旺，丁甲并用' },
  '庚卯': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '癸', 说明: '卯月木旺，丁甲制木' },
  '庚辰': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '壬', 说明: '辰月土月，丁甲并用' },
  '庚巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺，壬癸制火' },
  '庚午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺，壬癸调候' },
  '庚未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月，丁甲暖局' },
  '庚申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺，丁丙制金' },
  '庚酉': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '酉月金旺，丁丙制金' },
  '庚戌': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '辛', 说明: '戌月燥土，丁甲调候' },
  '庚亥': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '亥月水冷，丁丙暖局' },
  '庚子': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '癸', 说明: '子月水寒，丁丙暖局' },
  '庚丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '癸', 说明: '丑月寒湿，丙丁暖局' },
  // 辛金
  '辛寅': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '寅月木旺，壬水化木' },
  '辛卯': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '卯月木旺，壬甲并用' },
  '辛辰': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '乙', 说明: '辰月土月，壬甲暖局' },
  '辛巳': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丙', 说明: '巳月火旺，壬癸制火' },
  '辛午': { 主用神: ['壬', '癸'], 优先级: '壬先癸后', 忌神: '丁', 说明: '午月火旺，壬癸调候' },
  '辛未': { 主用神: ['丁', '甲'], 优先级: '丁先甲后', 忌神: '己', 说明: '未月土月，丁甲暖局' },
  '辛申': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '申月金旺，壬水洗金' },
  '辛酉': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '庚', 说明: '酉月金旺，壬水洗金' },
  '辛戌': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '辛', 说明: '戌月燥土，丁丙暖局' },
  '辛亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '壬', 说明: '亥月水冷，丙戊暖局' },
  '辛子': { 主用神: ['壬', '甲'], 优先级: '壬先甲后', 忌神: '丙', 说明: '子月水寒，壬甲暖局' },
  '辛丑': { 主用神: ['壬', '庚'], 优先级: '壬先庚后', 忌神: '己', 说明: '丑月寒湿，壬庚暖局' },
  // 壬水
  '壬寅': { 主用神: ['庚', '戊'], 优先级: '庚先戊后', 忌神: '丙', 说明: '寅月木旺，庚金生水' },
  '壬卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '丙', 说明: '卯月木旺，庚辛生水' },
  '壬辰': { 主用神: ['庚', '丙'], 优先级: '庚先丙后', 忌神: '甲', 说明: '辰月土月，庚丙并用' },
  '壬巳': { 主用神: ['辛', '庚'], 优先级: '辛先庚后', 忌神: '戊', 说明: '巳月火旺，辛金化火' },
  '壬午': { 主用神: ['辛', '癸'], 优先级: '辛先癸后', 忌神: '丁', 说明: '午月火旺，辛癸调候' },
  '壬未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月，庚辛生水' },
  '壬申': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '申月金旺，戊丁暖局' },
  '壬酉': { 主用神: ['戊', '丁'], 优先级: '戊先丁后', 忌神: '丙', 说明: '酉月金旺，戊丁暖局' },
  '壬戌': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '甲', 说明: '戌月燥土，辛丙调候' },
  '壬亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷，丙戊暖局' },
  '壬子': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '子月水寒，丙戊暖局' },
  '壬丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿，丙丁暖局' },
  // 癸水
  '癸寅': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '壬', 说明: '寅月木旺，辛丙暖局' },
  '癸卯': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '壬', 说明: '卯月木旺，庚辛生水' },
  '癸辰': { 主用神: ['辛', '丙'], 优先级: '辛先丙后', 忌神: '乙', 说明: '辰月湿土，辛丙暖局' },
  '癸巳': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '戊', 说明: '巳月火旺，辛壬调候' },
  '癸午': { 主用神: ['癸', '壬'], 优先级: '癸先壬后', 忌神: '丁', 说明: '午月火旺，癸壬制火' },
  '癸未': { 主用神: ['庚', '辛'], 优先级: '庚先辛后', 忌神: '己', 说明: '未月土月，庚辛生水' },
  '癸申': { 主用神: ['丁', '丙'], 优先级: '丁先丙后', 忌神: '壬', 说明: '申月金旺，丁丙暖局' },
  '癸酉': { 主用神: ['辛', '丁'], 优先级: '辛先丁后', 忌神: '壬', 说明: '酉月金旺，辛金生水' },
  '癸戌': { 主用神: ['辛', '壬'], 优先级: '辛先壬后', 忌神: '丙', 说明: '戌月燥土，辛壬润局' },
  '癸亥': { 主用神: ['丙', '戊'], 优先级: '丙先戊后', 忌神: '庚', 说明: '亥月水冷，丙戊暖局' },
  '癸子': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '庚', 说明: '子月水寒，丙丁暖局' },
  '癸丑': { 主用神: ['丙', '丁'], 优先级: '丙先丁后', 忌神: '己', 说明: '丑月寒湿，丙丁暖局' },
};

// ─────────────────────────────────────────────
// 月令旺衰分值表（子平真诠）
// ─────────────────────────────────────────────
const MONTH_STRENGTH = {
  '寅': { '甲': 100, '乙': 80,  '丙': 70, '丁': 60, '戊': 50, '己': 40, '庚': 30, '辛': 20, '壬': 10, '癸': 0  },
  '卯': { '甲': 80,  '乙': 100, '丙': 60, '丁': 70, '戊': 40, '己': 50, '庚': 20, '辛': 30, '壬': 10, '癸': 0  },
  '辰': { '甲': 60,  '乙': 70,  '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
  '巳': { '甲': 30,  '乙': 40,  '丙': 100,'丁': 80, '戊': 60, '己': 50, '庚': 40, '辛': 30, '壬': 10, '癸': 0  },
  '午': { '甲': 20,  '乙': 30,  '丙': 80, '丁': 100,'戊': 50, '己': 60, '庚': 30, '辛': 40, '壬': 0,  '癸': 10 },
  '未': { '甲': 50,  '乙': 60,  '丙': 60, '丁': 70, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 20, '癸': 30 },
  '申': { '甲': 20,  '乙': 10,  '丙': 30, '丁': 40, '戊': 50, '己': 60, '庚': 100,'辛': 80, '壬': 70, '癸': 50 },
  '酉': { '甲': 10,  '乙': 20,  '丙': 20, '丁': 30, '戊': 40, '己': 50, '庚': 80, '辛': 100,'壬': 50, '癸': 70 },
  '戌': { '甲': 50,  '乙': 60,  '丙': 70, '丁': 80, '戊': 70, '己': 80, '庚': 50, '辛': 60, '壬': 40, '癸': 50 },
  '亥': { '甲': 70,  '乙': 60,  '丙': 20, '丁': 30, '戊': 30, '己': 40, '庚': 10, '辛': 20, '壬': 100,'癸': 80 },
  '子': { '甲': 50,  '乙': 40,  '丙': 10, '丁': 20, '戊': 20, '己': 30, '庚': 0,  '辛': 10, '壬': 80, '癸': 100},
  '丑': { '甲': 40,  '乙': 50,  '丙': 50, '丁': 60, '戊': 60, '己': 70, '庚': 50, '辛': 60, '壬': 50, '癸': 60 },
};

// 通根加分表
const TONGGEEN_BONUS = {
  '甲': { '寅': 50, '卯': 40, '亥': 20, '子': 0,  '辰': 10, '未': 10, '戌': 10, '丑': 5,  '巳': 0,  '午': 0,  '申': 0,  '酉': 0  },
  '乙': { '卯': 50, '寅': 30, '亥': 10, '子': 20, '辰': 10, '未': 15, '戌': 10, '丑': 10, '巳': 0,  '午': 0,  '申': 0,  '酉': 0  },
  '丙': { '巳': 50, '午': 40, '寅': 20, '卯': 10, '申': 0,  '酉': 0,  '辰': 5,  '戌': 10, '丑': 5,  '亥': 0,  '子': 0,  '未': 10 },
  '丁': { '午': 50, '巳': 30, '未': 15, '戌': 10, '寅': 10, '酉': 0,  '申': 0,  '辰': 5,  '丑': 5,  '亥': 0,  '子': 0,  '卯': 5  },
  '戊': { '辰': 40, '戌': 40, '丑': 30, '未': 30, '巳': 20, '午': 30, '寅': 5,  '卯': 5,  '申': 0,  '酉': 0,  '亥': 0,  '子': 0  },
  '己': { '丑': 40, '未': 40, '辰': 30, '戌': 30, '午': 20, '巳': 10, '寅': 5,  '卯': 5,  '申': 0,  '酉': 0,  '亥': 5,  '子': 5  },
  '庚': { '申': 50, '酉': 40, '辰': 15, '戌': 15, '丑': 20, '未': 15, '寅': 0,  '卯': 0,  '巳': 0,  '午': 0,  '亥': 0,  '子': 0  },
  '辛': { '酉': 50, '申': 30, '辰': 10, '戌': 10, '丑': 15, '未': 10, '寅': 0,  '卯': 0,  '巳': 0,  '午': 0,  '亥': 0,  '子': 0  },
  '壬': { '亥': 50, '子': 40, '申': 20, '酉': 10, '辰': 10, '戌': 10, '丑': 15, '寅': 0,  '卯': 0,  '巳': 0,  '午': 0,  '未': 5  },
  '癸': { '子': 50, '亥': 40, '丑': 20, '辰': 10, '戌': 10, '申': 5,  '酉': 5,  '寅': 0,  '卯': 0,  '巳': 0,  '午': 0,  '未': 5  },
};

// ─────────────────────────────────────────────
// 辅助函数
// ─────────────────────────────────────────────

function getBranchHiddenStems(branch) {
  const h = BRANCH_HIDDEN[branch] || {};
  return [h.主气, h.中气, h.余气].filter(Boolean);
}

function getTenGod(dayStem, stem) {
  return (TEN_GODS_TABLE[dayStem] || {})[stem] || '未知';
}

// ─────────────────────────────────────────────
// 1. 调候用神（穷通宝鉴）
// ─────────────────────────────────────────────

function analyzeTiaoHou(dayStem, monthBranch, allStems) {
  const rule = TIAO_HOU_TABLE[dayStem + monthBranch];
  if (!rule) return { 有调候: false, 说明: '此月令无特别调候' };

  const present = rule.主用神.filter(g => allStems.includes(g));
  const absent  = rule.主用神.filter(g => !allStems.includes(g));
  const state = present.length === rule.主用神.length ? '调候俱全'
              : present.length > 0 ? '调候不全' : '调候皆缺';

  return {
    有调候: true,
    主用神: rule.主用神,
    忌神: rule.忌神,
    优先级: rule.优先级,
    已有用神: present,
    缺用神: absent,
    调候状态: state,
    说明: rule.说明
  };
}

// ─────────────────────────────────────────────
// 2. 日主强弱（子平真诠）
// ─────────────────────────────────────────────

function analyzeDayMasterStrength(dayStem, monthBranch, allStemsList, allBranchList) {
  const myElement = STEM_ELEMENT[dayStem];
  const genByElement = ELEMENT_GENERATED_BY[myElement]; // 生我者（印）

  // 月令分
  const monthScore = (MONTH_STRENGTH[monthBranch] || {})[dayStem] || 0;

  // 比劫分（天干同五行）
  let biJieScore = 0;
  allStemsList.forEach(s => {
    if (s !== dayStem && STEM_ELEMENT[s] === myElement) biJieScore += 20;
  });
  allBranchList.forEach(z => {
    const hidden = BRANCH_HIDDEN[z] || {};
    const weight = { 主气: 15, 中气: 8, 余气: 5 };
    ['主气', '中气', '余气'].forEach(k => {
      if (hidden[k] && STEM_ELEMENT[hidden[k]] === myElement) biJieScore += weight[k];
    });
  });

  // 通根分
  let tonggenScore = 0;
  allBranchList.forEach(z => {
    tonggenScore += ((TONGGEEN_BONUS[dayStem] || {})[z] || 0);
  });

  // 印绶分（生我者天干/地支中）
  let yinShouScore = 0;
  allStemsList.forEach(s => {
    if (STEM_ELEMENT[s] === genByElement) yinShouScore += 15;
  });
  allBranchList.forEach(z => {
    const hidden = BRANCH_HIDDEN[z] || {};
    const weight = { 主气: 10, 中气: 5, 余气: 3 };
    ['主气', '中气', '余气'].forEach(k => {
      if (hidden[k] && STEM_ELEMENT[hidden[k]] === genByElement) yinShouScore += weight[k];
    });
  });

  const total = monthScore + biJieScore + tonggenScore + yinShouScore;

  let level;
  if (total < 80)       level = '极弱';
  else if (total < 150) level = '弱';
  else if (total < 220) level = '偏弱';
  else if (total < 300) level = '中和';
  else if (total < 380) level = '偏强';
  else if (total < 450) level = '强';
  else                  level = '极强';

  const isWeak   = ['极弱', '弱', '偏弱'].includes(level);
  const isStrong = ['偏强', '强', '极强'].includes(level);

  return {
    日主: dayStem,
    五行: myElement,
    强弱等级: level,
    总分: total,
    月令得分: monthScore,
    比劫得分: biJieScore,
    通根得分: tonggenScore,
    印绶得分: yinShouScore,
    旺衰: isWeak ? `${dayStem}身${level}，宜取印比生扶` : isStrong ? `${dayStem}身${level}，宜取官杀财食伤克泄` : `${dayStem}身中和，综合取用`,
    用神方向: isWeak ? '印比扶身' : isStrong ? '官杀财泄身' : '综合平衡'
  };
}

// ─────────────────────────────────────────────
// 3. 用神格局（子平真诠）
// ─────────────────────────────────────────────

function analyzeYongShen(dayStem, monthBranch, monthStem, strengthResult) {
  const hidden = BRANCH_HIDDEN[monthBranch] || {};
  const tenGodsMap = TEN_GODS_TABLE[dayStem] || {};

  // 月令本气透干则取透干，否则取本气
  const yongshen = (monthStem === hidden.主气 || monthStem === hidden.中气) ? monthStem : hidden.主气;
  const pattern  = tenGodsMap[yongshen] || '比肩';

  // 判断格局类型
  let patternType = '正格';
  const { 总分: score, 强弱等级: level } = strengthResult;
  if (score < 80 && ['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(pattern)) patternType = '从格';
  else if (score > 450 && ['比肩', '劫财'].includes(pattern)) patternType = '专旺格';

  // 善用神判断
  // 从格/专旺格：顺从格局主气，月令本气即为用神（isGood = true）
  // 正格：按身强/身弱扶抑法判断
  const isStrong = ['偏强', '强', '极强'].includes(level);
  const isWeak   = ['极弱', '弱', '偏弱'].includes(level);
  let isGood = true;
  if (patternType === '正格') {
    if (isStrong && ['比肩', '劫财', '正印', '偏印'].includes(pattern)) isGood = false;
    if (isWeak   && ['七杀', '正官', '偏财', '正财', '食神', '伤官'].includes(pattern)) isGood = false;
  }

  return {
    月令: monthBranch,
    透干用神: yongshen,
    格局: pattern,
    格局类型: patternType,
    善用神: isGood,
    说明: `${monthBranch}月令，${yongshen}${monthStem === yongshen ? '透干' : '本气'}，取${pattern}格`
  };
}

// ─────────────────────────────────────────────
// 4. 阴阳刚柔（滴天髓）
// ─────────────────────────────────────────────

function analyzeYinYang(pillars, dayStem) {
  const issues = [];
  const relations = {};

  pillars.forEach((p, i) => {
    const label = ['年柱', '月柱', '日柱', '时柱'][i];
    const se = STEM_ELEMENT[p.stem];
    const be = BRANCH_ELEMENT[p.branch];
    if (!se || !be) return;

    if (ELEMENT_RESTRAIN[be] === se) {
      issues.push(`${p.stem}${p.branch}截脚（${be}耗${se}）`);
    }
    if (ELEMENT_RESTRAIN[se] === be) {
      issues.push(`${p.stem}${p.branch}盖头（${se}克${be}）`);
    }

    if (se === be) relations[label] = '比和';
    else if (ELEMENT_PRODUCES[se] === be) relations[label] = '相生';
    else if (ELEMENT_RESTRAIN[se] === be) relations[label] = '相克';
    else relations[label] = '无直接关系';
  });

  const yy = STEM_YINYANG[dayStem] || '阳';
  return {
    日主阴阳: yy,
    刚柔: yy === '阳' ? '刚' : '柔',
    盖头截脚: issues,
    天干地支关系: relations,
    说明: `${dayStem}为${yy}干，性${yy === '阳' ? '刚' : '柔'}，${issues.length ? '有' + issues.join('、') : '无截脚盖头'}`
  };
}

// ─────────────────────────────────────────────
// 5. 十神分析
// ─────────────────────────────────────────────

function analyzeTenGods(dayStem, stems, branches) {
  const result = { 天干十神: {}, 地支十神: {} };
  const labels = ['年', '月', '日', '时'];

  stems.forEach((s, i) => {
    if (i === 2) return; // 日干本身
    result.天干十神[labels[i] + '干'] = `${s}（${getTenGod(dayStem, s)}）`;
  });

  branches.forEach((z, i) => {
    const hidden = BRANCH_HIDDEN[z] || {};
    const parts = [hidden.主气, hidden.中气, hidden.余气].filter(Boolean)
      .map(h => `${h}${getTenGod(dayStem, h)}`).join('/');
    result.地支十神[labels[i] + '支'] = `${z}（${parts}）`;
  });

  return result;
}

// ─────────────────────────────────────────────
// 综合分析入口
// ─────────────────────────────────────────────

/**
 * @param {object} bazi - { year, month, day, hour, dayStem }
 *   e.g. { year:'丙寅', month:'己亥', day:'乙丑', hour:'乙酉', dayStem:'乙' }
 */
function runFullAnalysis(bazi) {
  const yearStem   = bazi.year[0],  yearBranch   = bazi.year[1];
  const monthStem  = bazi.month[0], monthBranch  = bazi.month[1];
  const dayStem    = bazi.dayStem  || bazi.day[0];
  const dayBranch  = bazi.day[1];
  const hourStem   = bazi.hour[0],  hourBranch   = bazi.hour[1];

  const allStems    = [yearStem, monthStem, dayStem, hourStem];
  const allBranches = [yearBranch, monthBranch, dayBranch, hourBranch];
  const allHiddenStems = allBranches.flatMap(getBranchHiddenStems);

  const pillars = [
    { stem: yearStem,  branch: yearBranch  },
    { stem: monthStem, branch: monthBranch },
    { stem: dayStem,   branch: dayBranch   },
    { stem: hourStem,  branch: hourBranch  },
  ];

  const tiaoHou   = analyzeTiaoHou(dayStem, monthBranch, [...allStems, ...allHiddenStems]);
  const strength  = analyzeDayMasterStrength(dayStem, monthBranch, allStems, allBranches);
  const yongShen  = analyzeYongShen(dayStem, monthBranch, monthStem, strength);
  const yinYang   = analyzeYinYang(pillars, dayStem);
  const tenGods   = analyzeTenGods(dayStem, allStems, allBranches);

  return { 调候用神: tiaoHou, 日主强弱: strength, 用神格局: yongShen, 阴阳刚柔: yinYang, 十神: tenGods };
}

/**
 * 格式化输出分析报告
 */
function formatAnalysisReport(bazi) {
  const a = runFullAnalysis(bazi);
  const lines = [];

  lines.push(`━━ 八字深度分析 ━━`);
  lines.push(`四柱：${bazi.year} ${bazi.month} ${bazi.day} ${bazi.hour}`);
  lines.push(`日主：${a.日主强弱.日主}（${a.日主强弱.五行}）`);
  lines.push('');

  // 强弱
  lines.push(`▶ 日主强弱（子平真诠）`);
  lines.push(`  强弱等级：${a.日主强弱.强弱等级}（${a.日主强弱.总分}分）`);
  lines.push(`  月令${a.日主强弱.月令得分} + 比劫${a.日主强弱.比劫得分} + 通根${a.日主强弱.通根得分} + 印绶${a.日主强弱.印绶得分}`);
  lines.push(`  → ${a.日主强弱.旺衰}`);
  lines.push('');

  // 格局
  lines.push(`▶ 用神格局（子平真诠）`);
  lines.push(`  格局：${a.用神格局.格局}（${a.用神格局.格局类型}）`);
  lines.push(`  ${a.用神格局.说明}`);
  lines.push(`  善用神：${a.用神格局.善用神 ? '✅ 是' : '❌ 否（用神有害）'}`);
  lines.push('');

  // 调候
  lines.push(`▶ 调候用神（穷通宝鉴）`);
  if (a.调候用神.有调候) {
    lines.push(`  用神：${a.调候用神.主用神.join('、')}（${a.调候用神.优先级}）`);
    lines.push(`  忌神：${a.调候用神.忌神}  状态：${a.调候用神.调候状态}`);
    lines.push(`  ${a.调候用神.说明}`);
    if (a.调候用神.缺用神.length > 0) lines.push(`  ⚠ 八字缺：${a.调候用神.缺用神.join('、')}`);
  } else {
    lines.push(`  ${a.调候用神.说明}`);
  }
  lines.push('');

  // 阴阳
  lines.push(`▶ 阴阳刚柔（滴天髓）`);
  lines.push(`  ${a.阴阳刚柔.说明}`);
  if (a.阴阳刚柔.盖头截脚.length > 0) {
    a.阴阳刚柔.盖头截脚.forEach(s => lines.push(`  ⚠ ${s}`));
  }
  lines.push('');

  // 十神
  lines.push(`▶ 十神`);
  Object.entries(a.十神.天干十神).forEach(([k, v]) => lines.push(`  ${k}: ${v}`));
  Object.entries(a.十神.地支十神).forEach(([k, v]) => lines.push(`  ${k}: ${v}`));

  return lines.join('\n');
}

module.exports = { runFullAnalysis, formatAnalysisReport, analyzeTiaoHou, analyzeDayMasterStrength, analyzeYongShen, analyzeYinYang, analyzeTenGods, TEN_GODS_TABLE, BRANCH_HIDDEN, TIAO_HOU_TABLE };

// 命令行调用
if (require.main === module) {
  const args = process.argv.slice(2);
  if (args.length < 4) {
    console.log('用法: node bazi-analysis.js <年柱> <月柱> <日柱> <时柱>');
    console.log('示例: node bazi-analysis.js 丙寅 己亥 乙丑 乙酉');
    process.exit(1);
  }
  const [year, month, day, hour] = args;
  const dayStem = day[0];
  console.log(formatAnalysisReport({ year, month, day, hour, dayStem }));
}


path: scripts/daily-fortune.js
content: #!/usr/bin/env node
/**
 * 每日运程生成脚本
 * 生成当日运程报告：综合指数、穿衣颜色、宜忌、风险提示、吉时
 */

const dayMap = ['日', '一', '二', '三', '四', '五', '六'];
const monthMap = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];

// 五行颜色对应
const fiveElements = {
  '木': { color: '绿色、青色', direction: '东方', lucky: '招贵人运' },
  '火': { color: '红色、紫色', direction: '南方', lucky: '增事业运' },
  '土': { color: '黄色、棕色', direction: '中央', lucky: '稳财运' },
  '金': { color: '白色、金色', direction: '西方', lucky: '旺事业' },
  '水': { color: '黑色、蓝色', direction: '北方', lucky: '防水逆' }
};

// 地支对应五行
const zhiElement = {
  '子': '水', '丑': '土', '寅': '木', '卯': '木',
  '辰': '土', '巳': '火', '午': '火', '未': '土',
  '申': '金', '酉': '金', '戌': '土', '亥': '水'
};

//时辰信息
const hourInfo = {
  '子': { range: '23-01', element: '水', tip: '整理思考' },
  '丑': { range: '01-03', element: '土', tip: '睡眠休息' },
  '寅': { range: '03-05', element: '木', tip: '计划准备' },
  '卯': { range: '05-07', element: '木', tip: '晨间运动' },
  '辰': { range: '07-09', element: '土', tip: '贵人运佳' },
  '巳': { range: '09-11', element: '火', tip: '事业高峰' },
  '午': { range: '11-13', element: '火', tip: '财运旺盛' },
  '未': { range: '13-15', element: '土', tip: '平稳行事' },
  '申': { range: '15-17', element: '金', tip: '财运佳' },
  '酉': { range: '17-19', element: '金', tip: '收整理' },
  '戌': { range: '19-21', element: '土', tip: '社交应酬' },
  '亥': { range: '21-23', element: '水', tip: '学习思考' }
};

// 宜忌（简化版）
const yiJi = {
  '木': { yi: ['出行', '学习', '交友', '谈判'], ji: ['冒险', '投资', '手术'] },
  '火': { yi: ['表白', '签约', '创新', '表演'], ji: ['安葬', '搬家', '诉讼'] },
  '土': { yi: ['种植', '装修', '求职', '上任'], ji: ['动土', '开业', '破土'] },
  '金': { yi: ['上任', '洽谈', '收款', '装修'], ji: ['安葬', '破土', '开业'] },
  '水': { yi: ['出行', '考试', '推广', '流动'], ji: ['搬家', '动土', '投资'] }
};

/**
 * 计算当日天干地支
 */
function getDayGanZhi(date = new Date()) {
  // 以2024年1月1日=甲子日为基准（取正午避免夏令时边界问题）
  const baseDate = new Date('2024-01-01T12:00:00');

  const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
  const ganIndex = ((diffDays % 10) + 10) % 10; // 甲=0
  const zhiIndex = ((diffDays % 12) + 12) % 12; // 子=0
  
  const tianGan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
  const diZhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
  
  return tianGan[ganIndex] + diZhi[zhiIndex];
}

/**
 * 获取五行信息
 */
function getElementInfo(ganZhi) {
  const zhi = ganZhi[1];
  const element = zhiElement[zhi] || '土';
  return fiveElements[element] || fiveElements['土'];
}

/**
 * 生成宜忌列表
 */
function getYiJiList(element) {
  const info = yiJi[element] || yiJi['土'];
  return {
    yi: info.yi.slice(0, 4),
    ji: info.ji.slice(0, 4)
  };
}

/**
 * 获取吉时
 */
function getLuckyHours(ganZhi) {
  const zhi = ganZhi[1];
  const element = zhiElement[zhi] || '土';
  
  // 根据五行找出当日旺的时辰
  const luckyZhi = Object.entries(zhiElement)
    .filter(([_, el]) => el === element)
    .map(([z]) => z);
  
  return luckyZhi.slice(0, 2).map(z => ({
    zhi: z,
    ...hourInfo[z]
  }));
}

/**
 * 生成风险提示
 */
function getRiskWarnings(ganZhi, dayOfWeek) {
  const warnings = [];
  const zhi = ganZhi[1];
  
  // 驿马星（地支对冲）
  const yimaZhi = ['申', '亥', '寅', '巳'];
  if (yimaZhi.includes(zhi)) {
    warnings.push({
      level: '🟡',
      type: '出行',
      msg: '今日驿马星动，出行注意安全'
    });
  }
  
  // 破日（地支相破）
  const poZhi = ['子', '午', '卯', '酉', '辰', '丑', '寅', '亥', '巳', '申', '戌', '未'];
  const poPairs = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'], ['巳', '申'], ['午', '未']];
  
  // 简单的风险判断
  if (dayOfWeek === 1 || dayOfWeek === 5) {
    warnings.push({
      level: '🟢',
      type: '综合',
      msg: '今日诸事顺遂，宜积极行动'
    });
  }
  
  return warnings;
}

/**
 * 生成运势评分
 */
function getFortuneScores(ganZhi) {
  const zhi = ganZhi[1];
  const element = zhiElement[zhi] || '土';
  
  // 根据五行生克简单评分
  const baseScores = {
    '木': { career: 4, wealth: 3, love: 4, health: 3 },
    '火': { career: 5, wealth: 4, love: 3, health: 3 },
    '土': { career: 3, wealth: 4, love: 3, health: 4 },
    '金': { career: 4, wealth: 5, love: 3, health: 3 },
    '水': { career: 3, wealth: 3, love: 4, health: 4 }
  };
  
  const scores = baseScores[element] || baseScores['土'];
  
  // 随机微调（±0.5）
  const jitter = () => (Math.random() - 0.5);
  return {
    career: Math.min(5, Math.max(1, scores.career + jitter())).toFixed(1),
    wealth: Math.min(5, Math.max(1, scores.wealth + jitter())).toFixed(1),
    love: Math.min(5, Math.max(1, scores.love + jitter())).toFixed(1),
    health: Math.min(5, Math.max(1, scores.health + jitter())).toFixed(1)
  };
}

/**
 * 格式化星级
 */
function formatStars(score) {
  const num = parseFloat(score);
  const full = Math.floor(num);
  const half = num - full >= 0.5 ? 1 : 0;
  const empty = 5 - full - half;
  return '★'.repeat(full) + '☆'.repeat(half) + '☆'.repeat(empty);
}

/**
 * 生成完整运程报告
 */
function generateDailyFortune(date = new Date()) {
  const ganZhi = getDayGanZhi(date);
  const elementInfo = getElementInfo(ganZhi);
  const element = zhiElement[ganZhi[1]] || '土';
  const yiJiInfo = getYiJiList(element);
  const luckyHours = getLuckyHours(ganZhi);
  const warnings = getRiskWarnings(ganZhi, date.getDay());
  const scores = getFortuneScores(ganZhi);
  
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const weekDay = dayMap[date.getDay()];
  const monthName = monthMap[month - 1];
  
  // 生成运势语
  const fortuneQuotes = [
    '顺势而为，伺机而动',
    '稳中求进，步步为营',
    '阳光总在风雨后',
    '把握当下，展望未来',
    '积跬步以至千里'
  ];
  const quote = fortuneQuotes[date.getDate() % fortuneQuotes.length];
  
  // 组装报告
  const report = `
🌅 【私人命理顾问】${year}年${month}月${day}日（周${weekDay}）

📊 今日综合指数
   事业：${formatStars(scores.career)} ${scores.career}
   财运：${formatStars(scores.wealth)} ${scores.wealth}
   感情：${formatStars(scores.love)} ${scores.love}
   健康：${formatStars(scores.health)} ${scores.health}

🎨 幸运色：${elementInfo.color}（利${elementInfo.lucky}）
   幸运方位：${elementInfo.direction}

💼 今日宜忌
   ✅ 宜：${yiJiInfo.yi.join('、')}
   ❌ 忌：${yiJiInfo.ji.join('、')}

⚠️ 风险提示
${warnings.length > 0 ? warnings.map(w => `   ${w.level} 【${w.type}】${w.msg}`).join('\n') : '   🟢 今日总体平稳，无明显风险'}

⏰ 吉时
${luckyHours.map(h => `   • ${h.zhi}时（${h.range}点）- ${h.tip}`).join('\n')}

💡 今日一句
   「${quote}」

📅 干支：${ganZhi}（${elementInfo.direction.charAt(0)}气旺）
`;
  
  return report;
}

// 主入口
const args = process.argv.slice(2);
let date = new Date();

if (args[0]) {
  try {
    date = new Date(args[0]);
    if (isNaN(date.getTime())) {
      console.error('日期格式无效，请使用 YYYY-MM-DD');
      process.exit(1);
    }
  } catch (e) {
    console.error('日期解析错误:', e.message);
    process.exit(1);
  }
}

console.log(generateDailyFortune(date));


path: scripts/daily-push.js
content: #!/usr/bin/env node
/**
 * 每日运势自动推送脚本
 * 读取所有已开启推送的用户，生成定制化运程并通过 OpenClaw 发送
 *
 * 用法:
 *   node daily-push.js                  # 推送今日运势给所有已开启的用户
 *   node daily-push.js --dry-run       # 模拟推送（不实际发送）
 *   node daily-push.js --test <userId> # **仅作为 dry-run 调试工具**：
 *                                       自动以 dry-run 模式运行（不交付、不落盘、不输出运势正文）；
 *                                       未 opt-in 的用户也允许仅做运算验证。
 *                                       v1.2.0 起**已移除**任何借助 flag 走正式发送给未 opt-in 用户的旁路；
 *                                       要正式接收推送，用户必须运行 push-toggle.js on <userId> 显式 opt-in。
 *   node daily-push.js --list          # 列出所有已开启推送的用户（仅聚合统计）
 */

const fs = require('fs');
const path = require('path');

const PROFILES_DIR = path.join(__dirname, '../data/profiles');
const LOG_FILE = path.join(__dirname, '../data/push-log.json');

// ============================================================
// 八字/紫微 核心分析（内嵌，避免外部依赖）
// ============================================================

const GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
const SHENGXIAO = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];

const ZHI_ELEMENT = {
  '子': '水', '丑': '土', '寅': '木', '卯': '木',
  '辰': '土', '巳': '火', '午': '火', '未': '土',
  '申': '金', '酉': '金', '戌': '土', '亥': '水'
};

const ELEMENT_COLOR = {
  '木': { color: '绿色、青色', direction: '东方', emoji: '🌿' },
  '火': { color: '红色、紫色', direction: '南方', emoji: '🔥' },
  '土': { color: '黄色、棕色', direction: '中央', emoji: '🌍' },
  '金': { color: '白色、银色', direction: '西方', emoji: '⚪' },
  '水': { color: '黑色、蓝色', direction: '北方', emoji: '🌊' }
};

const LUCKY_NUMBERS = {
  '木': [3, 8], '火': [2, 7], '土': [5, 10], '金': [4, 9], '水': [1, 6]
};

const DAY_MAP = ['日', '一', '二', '三', '四', '五', '六'];
const MONTH_MAP = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];

const HOUR_INFO = {
  '子': { range: '23-01', tip: '整理思考', stars: '☽ 阴性星' },
  '丑': { range: '01-03', tip: '睡眠休息', stars: '☆ 平常' },
  '寅': { range: '03-05', tip: '计划准备', stars: '🌟 小吉' },
  '卯': { range: '05-07', tip: '晨间运动', stars: '🌟 小吉' },
  '辰': { range: '07-09', tip: '贵人运佳', stars: '★★ 吉祥' },
  '巳': { range: '09-11', tip: '事业高峰', stars: '★★ 大吉' },
  '午': { range: '11-13', tip: '财运旺盛', stars: '★★ 大吉' },
  '未': { range: '13-15', tip: '平稳行事', stars: '★☆ 一般' },
  '申': { range: '15-17', tip: '财运佳', stars: '★★ 吉祥' },
  '酉': { range: '17-19', tip: '收整理', stars: '★☆ 一般' },
  '戌': { range: '19-21', tip: '社交应酬', stars: '★★ 吉祥' },
  '亥': { range: '21-23', tip: '学习思考', stars: '☆ 平常' }
};

// ============================================================
// 命理核心算法
// ============================================================

function getDayGanZhi(date = new Date()) {
  const baseDate = new Date('2024-01-01T12:00:00');
  const diffDays = Math.round((date - baseDate) / (1000 * 60 * 60 * 24));
  const ganIndex = ((diffDays % 10) + 10) % 10;
  const zhiIndex = ((diffDays % 12) + 12) % 12;
  return GAN[ganIndex] + ZHI[zhiIndex];
}

function getYearGanZhi(year) {
  const baseYear = 1984; // 甲子年
  const offset = year - baseYear;
  return GAN[((offset % 10) + 10) % 10] + ZHI[((offset % 12) + 12) % 12];
}

function getLunarMonth(month) {
  return MONTH_MAP[month - 1] + '月';
}

function getElementInfo(ganZhi) {
  const zhi = ganZhi[1];
  const element = ZHI_ELEMENT[zhi] || '土';
  return { element, ...ELEMENT_COLOR[element] };
}

function getLuckyNumbers(element) {
  const nums = LUCKY_NUMBERS[element] || [5, 10];
  const allNums = [];
  for (let i = 0; i < 5; i++) allNums.push(nums[i % nums.length]);
  return allNums.slice(0, 5);
}

// ============================================================
// 八字用神计算
// ============================================================

function calculateBaziYongshen(bazi) {
  if (!bazi || !bazi.dayStem) return { primary: '木', secondary: ['火', '水'], details: [] };

  const dayStem = bazi.dayStem;
  const monthZhi = bazi.month ? bazi.month[1] : '寅';

  const dayWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
  const monthWuxing = ZHI_ELEMENT[monthZhi] || '木';

  const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
  const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };

  const results = [];

  // 调候用神
  const tiaohouTable = {
    '甲': { '寅': '丙', '卯': '丙', '辰': '癸', '巳': '壬', '午': '壬', '未': '癸', '申': '丁', '酉': '丁', '戌': '辛', '亥': '丙', '子': '庚', '丑': '辛' },
    '乙': { '寅': '丙', '卯': '丙', '辰': '癸', '巳': '壬', '午': '癸', '未': '丙', '申': '丁', '酉': '丁', '戌': '辛', '亥': '丙', '子': '庚', '丑': '辛' }
  };
  const t = tiaohouTable[dayStem]?.[monthZhi];
  if (t) results.push({ type: '调候', value: t, desc: '寒木喜火暖局' });

  // 扶抑用神
  results.push({ type: '扶抑', value: sheng[dayWuxing], desc: `日主${dayWuxing}，喜生助` });
  results.push({ type: '忌', value: ke[dayWuxing], desc: `日主${dayWuxing}，宜避` });

  const primary = results[0]?.value || dayWuxing;
  const secondary = [...new Set(results.filter(r => r.type === '扶抑').map(r => r.value))];

  return { primary, secondary: secondary.slice(0, 2), details: results };
}

// ============================================================
// 运势评分（结合八字五行 + 当日干支）
// ============================================================

function generatePersonalizedScores(bazi, dayGanZhi) {
  const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
  const dayWuxing = dayElement;

  // 八字日主五行
  const dayStem = bazi?.dayStem || '甲';
  const dayStemWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';

  // 日主与当日五行的关系
  const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
  const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };
  const bi = { '木': '金', '火': '水', '土': '木', '金': '火', '水': '土' };

  // 生助日主 = 吉
  const dayHelpsDay = dayWuxing === dayStemWuxing || sheng[dayStemWuxing] === dayWuxing;
  // 克耗日主 = 压力
  const dayStressesDay = ke[dayStemWuxing] === dayWuxing || bi[dayStemWuxing] === dayWuxing;

  // 事业：与用神同气 + 当日吉
  let career = 3 + Math.random() * 1.5;
  let wealth = 3 + Math.random() * 1.5;
  let love = 3 + Math.random() * 1.5;
  let health = 3 + Math.random() * 1.5;

  if (dayHelpsDay) {
    career += 0.5;
    wealth += 0.5;
    health += 0.3;
  }
  if (dayStressesDay) {
    career -= 0.3;
    wealth -= 0.3;
  }

  // 基于用神微调
  const yongshen = calculateBaziYongshen(bazi);
  if (yongshen.primary === dayElement) { career += 0.5; wealth += 0.3; }

  // 加入日期随机性（确保每天有变化）
  const date = new Date();
  const dayFactor = (date.getDate() % 3) * 0.3 - 0.3;
  career += dayFactor;
  wealth += dayFactor * 0.8;
  love += dayFactor * 0.6;
  health += dayFactor * 0.4;

  const scores = {
    career: Math.min(5, Math.max(1, career)).toFixed(1),
    wealth: Math.min(5, Math.max(1, wealth)).toFixed(1),
    love: Math.min(5, Math.max(1, love)).toFixed(1),
    health: Math.min(5, Math.max(1, health)).toFixed(1)
  };

  return scores;
}

function formatStars(score) {
  const num = parseFloat(score);
  const full = Math.floor(num);
  const half = num - full >= 0.5 ? 1 : 0;
  return '★'.repeat(full) + '☆'.repeat(5 - full - half);
}

// ============================================================
// 宜忌生成（基于八字用神 + 当日干支）
// ============================================================

function generateYiJi(bazi, dayGanZhi) {
  const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
  const yongshen = calculateBaziYongshen(bazi);
  const primaryElement = yongshen.primary;

  // 注意：这里特意改成「氛围/主题」类描述，不再给出"投资/手术/签约/搬家/诉讼"
  // 等具体行动指令；推送属于自动触发的娱乐性内容，不应越界为
  // 法律/医疗/财务的具体行为建议。
  const YI_JI = {
    '木': {
      yi: ['沟通对话的氛围', '学习与吸收新事物', '主动联系老朋友', '为想做的事列计划'],
      ji: ['情绪冲动下的取舍', '对结果过早下定论', '勉强自己的应酬']
    },
    '火': {
      yi: ['表达想法', '尝试一点小创新', '让自己被看见', '梳理近期热度过高的情绪'],
      ji: ['口角与争辩', '硬碰硬的对抗', '对比与较劲']
    },
    '土': {
      yi: ['整理空间与习惯', '稳住节奏与计划', '回应身边人的需要', '把已经在做的事做厚一层'],
      ji: ['同时摊太多事', '为别人的进度焦虑', '强行加速']
    },
    '金': {
      yi: ['梳理边界与原则', '复盘与归档', '精简事项', '把"差不多"做到位'],
      ji: ['过度严苛地评判他人', '对自己反复挑剔', '冷处理本该回应的关系']
    },
    '水': {
      yi: ['留出独处与休整', '换个角度看老问题', '阅读、写作与表达', '柔和地推进进度'],
      ji: ['过度内耗', '把别人的情绪都接到自己身上', '在不确定时强求答案']
    }
  };

  // 优先用神，其次当日五行
  const element = primaryElement || dayElement;
  const info = YI_JI[element] || YI_JI['土'];

  return {
    yi: info.yi.slice(0, 4),
    ji: info.ji.slice(0, 4)
  };
}

// ============================================================
// 吉凶判断
// ============================================================

function getDayFortuneLevel(dayGanZhi) {
  const zhi = dayGanZhi[1];

  // 天恩 吉日
  const tianEnZhi = ['丑', '寅', '卯', '辰', '午', '未', '亥'];
  // 天贵 吉时
  const tianGuiZhi = ['辰', '巳', '午', '未', '申'];
  // 驿马 变动
  const yimaZhi = ['申', '亥', '寅', '巳'];

  let level = '平常';
  let desc = '今日诸事平稳';

  if (tianEnZhi.includes(zhi)) {
    level = '吉祥';
    desc = '天恩降临，贵人相助';
  }
  if (yimaZhi.includes(zhi)) {
    if (level === '吉祥') {
      level = '小吉';
      desc = '有变动，宜把握机遇';
    } else {
      level = '平常';
      desc = '驿马星动，出行奔波';
    }
  }

  // 检查是否破日（相破）
  const poPairs = [['子','丑'], ['寅','亥'], ['卯','戌'], ['辰','酉'], ['巳','申'], ['午','未']];
  for (const [a, b] of poPairs) {
    if (zhi === a || zhi === b) {
      level = '平常';
      desc = '今日有小损耗，宜守成';
      break;
    }
  }

  return { level, desc };
}

// ============================================================
// 风险预警
// ============================================================

function generateWarnings(bazi, dayGanZhi) {
  const warnings = [];
  const zhi = dayGanZhi[1];

  // 注意：以下提示属于自我节奏类的轻量提醒，不是医疗/财务/法律的具体行动指令。
  // 请勿据此做投资决策、终止治疗或撤销合同。

  // 驿马星 → 节奏类提醒
  const yimaZhi = ['申', '亥', '寅', '巳'];
  if (yimaZhi.includes(zhi)) {
    warnings.push({ level: '🟡', type: '节奏', msg: '今日变动气息偏强，给行程多留一点缓冲' });
  }

  // 五黄煞 → 自我照护类提醒
  const wuhuang = ['子', '卯', '午', '酉'];
  if (wuhuang.includes(zhi)) {
    warnings.push({ level: '🟡', type: '自我照护', msg: '近期容易能量低落，注意休息与饮食的规律' });
  }

  // 八字日主与当日关系 → 心态/精力类提醒
  const dayStem = bazi?.dayStem || '甲';
  const dayStemWuxing = { '甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土', '己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水' }[dayStem] || '木';
  const dayWuxing = ZHI_ELEMENT[zhi] || '土';
  const ke = { '木': '土', '火': '金', '土': '水', '金': '木', '水': '火' };

  if (ke[dayStemWuxing] === dayWuxing) {
    warnings.push({ level: '🟡', type: '心绪', msg: '今日容易冲动消费或情绪化决定，重要选择宜冷静一日再做' });
  }

  if (warnings.length === 0) {
    warnings.push({ level: '🟢', type: '综合', msg: '今日总体平稳，按既定节奏行事即可' });
  }

  return warnings;
}

// ============================================================
// 吉时计算
// ============================================================

function getLuckyHours(dayGanZhi) {
  const zhi = dayGanZhi[1];
  const dayElement = ZHI_ELEMENT[zhi] || '土';

  // 找与当日同气的时辰（旺相）
  const sameElementZhi = Object.entries(ZHI_ELEMENT)
    .filter(([_, el]) => el === dayElement)
    .map(([z]) => z);

  // 找生助当日五行的时辰
  const sheng = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
  const helpfulZhi = Object.entries(ZHI_ELEMENT)
    .filter(([_, el]) => el === sheng[dayElement])
    .map(([z]) => z);

  const allLucky = [...sameElementZhi, ...helpfulZhi];
  const unique = [...new Set(allLucky)].slice(0, 4);

  return unique.map(z => ({
    zhi: z,
    ...(HOUR_INFO[z] || { range: '--', tip: '平常', stars: '☆' })
  }));
}

// ============================================================
// 流年/流月提示（简化版，基于八字和大运）
// ============================================================

function getYearMonthTips(bazi) {
  const currentYear = new Date().getFullYear();
  const currentMonth = new Date().getMonth() + 1;

  const yearGanZhi = getYearGanZhi(currentYear);
  const yearElement = ZHI_ELEMENT[yearGanZhi[1]] || '土';

  const tips = [];

  // 流年提示
  const yearTips = {
    '木': '今年木气旺盛，利事业拓展，春季尤佳',
    '火': '今年火气当令，利创新突破，夏季事业运佳',
    '土': '今年土气稳重，利积累沉淀，秋季财运回升',
    '金': '今年金气肃杀，利变革调整，秋季利财运',
    '水': '今年水气流动，利流通传播，冬季人脉广'
  };
  tips.push({ period: '流年', msg: yearTips[yearElement] || '今年运势平稳' });

  // 流月提示（简化）
  const monthTips = [
    '正月开门红，二月稳中求进，三月事业上升',
    '四月注意小人，五月财运上佳，六月桃花旺盛',
    '七月健康注意，八月事业转折，九月贵人相助',
    '十月财运爆发，十一月感情升温，十二月总结规划'
  ];
  const monthIdx = Math.floor((currentMonth - 1) / 2);
  tips.push({ period: '本月', msg: monthTips[monthIdx] || '本月运势良好' });

  return tips;
}

// ============================================================
// 每日一言
// ============================================================

function getDailyQuote(dayGanZhi) {
  const quotes = [
    { element: '木', text: '木秀于林，风必摧之；堆出于岸，流必湍之。' },
    { element: '木', text: '顺势而为，不与天争；待时而动，方成大事。' },
    { element: '火', text: '火焰熊熊，照亮前路；热情如火，无坚不摧。' },
    { element: '火', text: '烈火炼真金，逆境显本色。' },
    { element: '土', text: '厚德载物，稳如泰山；静以修身，俭以养德。' },
    { element: '土', text: '土能生金，稳中求进；深根固本，方可长久。' },
    { element: '金', text: '金以刚为体，人以正为尊；锋芒内敛，大业可成。' },
    { element: '金', text: '金戈铁马，气吞万里如虎。' },
    { element: '水', text: '上善若水，水善利万物而不争。' },
    { element: '水', text: '水能载舟，亦能覆舟；顺势而行，方得始终。' },
    { element: '通用', text: '命里有时终须有，命里无时莫强求。' },
    { element: '通用', text: '三分天注定，七分靠打拼。' }
  ];

  const dayElement = ZHI_ELEMENT[dayGanZhi[1]] || '土';
  const dayQuotes = quotes.filter(q => q.element === dayElement);
  const fallback = quotes.filter(q => q.element === '通用');

  const pool = dayQuotes.length > 0 ? dayQuotes : fallback;
  const idx = new Date().getDate() % pool.length;
  return pool[idx]?.text || '积善之家，必有余庆。';
}

// ============================================================
// 生成完整个性化运程报告
// ============================================================

function generatePersonalizedFortune(profile, date = new Date()) {
  const { bazi } = profile;
  const dayGanZhi = getDayGanZhi(date);
  const elementInfo = getElementInfo(dayGanZhi);
  const luckyNumbers = getLuckyNumbers(elementInfo.element);
  const scores = generatePersonalizedScores(bazi, dayGanZhi);
  const fortuneLevel = getDayFortuneLevel(dayGanZhi);
  const yiJi = generateYiJi(bazi, dayGanZhi);
  const warnings = generateWarnings(bazi, dayGanZhi);
  const luckyHours = getLuckyHours(dayGanZhi);
  const yearMonthTips = getYearMonthTips(bazi);
  const quote = getDailyQuote(dayGanZhi);
  const yongshen = calculateBaziYongshen(bazi);

  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const weekDay = DAY_MAP[date.getDay()];
  const lunarMonth = getLunarMonth(month);

  // 用户基本信息
  const userName = profile.name || '你';
  const gender = profile.profile?.gender === '男' ? '先生' : '女士';
  const zodiac = bazi?.zodiac || '';

  // 构建报告
  const fortuneEmoji = fortuneLevel.level === '大吉' ? '🌟' :
                       fortuneLevel.level === '吉祥' ? '✨' :
                       fortuneLevel.level === '小吉' ? '🌤️' : '🌙';

  let report = `${fortuneEmoji} 【${userName}${gender}】${year}年${month}月${day}日（周${weekDay}）

⚠️ 本日运势仅为娱乐性文化参考；不是医疗、法律、心理、财务、婚姻、就业的专业建议；不应作为投资、合同、医疗、人身安全等决策的依据。如有相应需要，请联系合规专业人士。

━━━━━━━━━━━━━━━━━━━━━━
📊 今日趋势侧写（参考用）
   工作主题 ${formatStars(scores.career)}
   财务节奏 ${formatStars(scores.wealth)}
   关系氛围 ${formatStars(scores.love)}
   身心状态 ${formatStars(scores.health)}
━━━━━━━━━━━━━━━━━━━━━━

🎨 配色 / 方位 / 数字（生活美学层面）
   颜色：${elementInfo.color}
   方位：${elementInfo.direction}
   数字：${luckyNumbers.join('、')}
   元素：${elementInfo.emoji} ${elementInfo.element}

💮 今日整体气象
   ${fortuneLevel.level} — ${fortuneLevel.desc}

💼 今日适合 / 不太适合的「氛围」
   ✅ 适合：${yiJi.yi.join('、')}
   ⚠️ 不太适合：${yiJi.ji.join('、')}

⚠️ 自我节奏提醒（非医疗 / 财务 / 法律建议）
${warnings.map(w => `   ${w.level}【${w.type}】${w.msg}`).join('\n')}

⏰ 适合行事的时段
${luckyHours.slice(0, 3).map(h => `   • ${h.zhi}时（${h.range}点）- ${h.tip}`).join('\n')}
${luckyHours.length > 3 ? `   • ...等 ${luckyHours.length} 个时段` : ''}

📅 季节 / 时令侧写
${yearMonthTips.map(t => `   【${t.period}】${t.msg}`).join('\n')}

💡 今日一言
   「${quote}」

🧮 八字用神：${yongshen.primary}（主）${yongshen.secondary.join('、')}（辅）
   今日干支：${dayGanZhi}（${elementInfo.element}气${elementInfo.direction.includes('东方') ? '旺' : '得令'}）

———
⚠️ 仅供文化参考，不构成医疗/法律/心理/财务/婚姻等专业建议。重大决策请咨询专业人士；如有情绪困扰请联系当地心理援助热线。
`;

  return report;
}

// ============================================================
// 用户档案管理
// ============================================================

function loadAllProfiles() {
  if (!fs.existsSync(PROFILES_DIR)) return [];
  const files = fs.readdirSync(PROFILES_DIR).filter(f => f.endsWith('.json'));
  const profiles = [];
  for (const file of files) {
    try {
      const userId = file.replace('.json', '');
      const data = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
      profiles.push({ userId, ...data });
    } catch (e) {
      console.warn(`⚠️ 加载档案失败: ${file}`, e.message);
    }
  }
  return profiles;
}

function getUsersWithPushEnabled(profiles) {
  // 双重 opt-in 安全门：
  //  - pushEnabled 为 true
  //  - pushOptInAt 存在（证明是用户主动调用 push-toggle.js on 后写入的时间戳，
  //    仅字段为 true 不足以发送）
  //  - bazi 完整
  // 如果 profile 文件所有者手工修改了 pushEnabled:true 但未补 opt-in 时间戳，
  // 则必须走 push-toggle.js 重新 opt-in，避免静默默认启用推送。
  return profiles.filter(p => {
    const prefs = p.preferences || {};
    const enabled = prefs.pushEnabled === true;
    const optedIn = !!prefs.pushOptInAt; // 时间戳存在
    const hasBazi = p.bazi && p.bazi.day && p.bazi.dayStem;
    return enabled && optedIn && hasBazi;
  });
}

function updateLastPushDate(userId) {
  const filePath = path.join(PROFILES_DIR, `${userId}.json`);
  if (!fs.existsSync(filePath)) return;
  try {
    const profile = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    profile.lastPushDate = new Date().toISOString().split('T')[0];
    profile.updatedAt = new Date().toISOString().split('T')[0];
    fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
  } catch (e) {
    console.warn(`⚠️ 更新推送日期失败: ${userId}`, e.message);
  }
}

// ============================================================
// OpenClaw 消息发送（通过 IPC / openclaw 工具接口）
// ============================================================

async function sendMessage(userId, message, opts = {}) {
  const { dryRun = false } = opts;
  // 重要：为避免在多用户环境下将某个用户的运势串台到其他人，
  // 这里不会将运势正文写到 stdout，而是：
  //  1. 在 stdout 输出仅含脱敏状态行（供调度者/运维查看）。
  //  2. 实际发送模式下将完整运势写入每个用户独立的本地交付文件，
  //     由 OpenClaw 运行时按 userId 读取并点对点发送。
  //  3. **dry-run 是真正的不落盘运行**：不会生成任何 outbox / outbox-dryrun 文件，
  //     也不会将运势正文输出到 stdout，仅进行计算验证。这保证了
  //     调试/CI/共享终端中不会意外留存个人运势与八字数据。
  if (!userId) {
    console.error('   ⚠ sendMessage: userId 为空，拒绝发送以避免泄露');
    return false;
  }

  if (dryRun) {
    // dry-run 严格不落盘、不交付、不输出运势正文；
    // 仅验证调用者传入的 userId / message 是否合法。
    if (typeof message !== 'string' || !message.length) {
      console.error('   ⚠ dry-run: message 为空，计算可能出错');
      return false;
    }
    return true;
  }

  try {
    const outboxDir = path.join(__dirname, '../data/outbox');
    if (!fs.existsSync(outboxDir)) {
      fs.mkdirSync(outboxDir, { recursive: true, mode: 0o700 });
    }
    // 仅仅允许 userId 中的安全字符，避免路径穿越
    const safeId = String(userId).replace(/[^a-zA-Z0-9_\-]/g, '_');
    const dateStr = new Date().toISOString().split('T')[0];
    const outFile = path.join(outboxDir, `${safeId}_${dateStr}.txt`);
    const payload = {
      userId: safeId,
      generatedAt: new Date().toISOString(),
      mode: 'live',
      content: message
    };
    // 写入文件时限制为仅当前用户可读（0600）
    fs.writeFileSync(outFile, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });

    // 在 stdout 仅输出状态行，不输出个人运势正文 / 不输出原始 userId
    return true;
  } catch (e) {
    console.error(`   ❌ sendMessage 写入失败: ${e.message}`);
    return false;
  }
}

// ============================================================
// 日志记录
// ============================================================

function loadLog() {
  if (!fs.existsSync(LOG_FILE)) return { runs: [] };
  try {
    return JSON.parse(fs.readFileSync(LOG_FILE, 'utf8'));
  } catch (e) {
    return { runs: [] };
  }
}

function appendLog(entry) {
  const log = loadLog();
  log.runs.push(entry);
  // 只保留最近100条
  if (log.runs.length > 100) log.runs = log.runs.slice(-100);
  fs.writeFileSync(LOG_FILE, JSON.stringify(log, null, 2), 'utf8');
}

// ============================================================
// 主推送流程
// ============================================================

async function runPush({ dryRun = false, testUserId = null } = {}) {
  const date = new Date();
  const dateStr = date.toISOString().split('T')[0];
  const logEntry = {
    date: dateStr,
    timestamp: new Date().toISOString(),
    dryRun,
    results: []
  };

  console.log(`\n🚀 每日运势推送开始 - ${dateStr}\n`);
  console.log(`   模式: ${dryRun ? '🔸 模拟推送（不实际发送）' : '📨 正式推送'}\n`);

  const allProfiles = loadAllProfiles();
  let targets = getUsersWithPushEnabled(allProfiles);

  if (testUserId) {
    const testProfile = allProfiles.find(p => p.userId === testUserId);
    if (!testProfile) {
      console.log(`   ❌ 测试用户不存在: ${testUserId}`);
      return;
    }

    // v1.2.0：--test 现在**强制 dry-run**，不再保留 --i-am-the-user 旁路。
    // 任何 --test 调用都不会向未 opt-in 用户产生真实 outbox / 真实交付，
    // 因为这会让脚本可被多操作员或自动化滥用，给从未同意的用户写入个人运势。
    if (!dryRun) {
      console.error('   ❌ 拒绝：--test 在 v1.2.0 起已**强制以 dry-run 运行**，');
      console.error('       不再支持 --i-am-the-user 等旁路走正式发送。');
      console.error('       要正式接收每日推送，请让用户本人运行：');
      console.error(`         node scripts/push-toggle.js on ${testUserId}`);
      console.error('       要做调试验算，请改用：');
      console.error(`         node scripts/daily-push.js --dry-run --test ${testUserId}`);
      return;
    }

    const optedIn = !!(testProfile.preferences && testProfile.preferences.pushEnabled && testProfile.preferences.pushOptInAt);
    if (!optedIn) {
      console.log(`   ⚠️  测试用户 ${testUserId} 未 opt-in 推送；dry-run 模式仅验算不交付、不落盘。`);
    }

    targets = [testProfile];
    console.log(`   📋 测试模式: 仅验算 ${testUserId}（dry-run 强制）\n`);
  }

  console.log(`   📋 共 ${targets.length} 个用户开启推送\n`);
  console.log('   ' + '─'.repeat(50));

  let successCount = 0;
  let failCount = 0;

  for (const profile of targets) {
    const { userId, name } = profile;
    // 不在 stdout 上拼接 userId/姓名；避免在共享环境下被外人看到
    process.stdout.write(`   🔄 推送中... `);

    try {
      const fortune = generatePersonalizedFortune(profile, date);

      if (dryRun) {
        // dry-run 下：仅验证运算能否生成运势文本，不交付、不落盘、不输出正文
        await sendMessage(userId, fortune, { dryRun: true });
        console.log('✅ (dry-run 仅验算，未写入任何文件)');
        successCount++;
      } else {
        const sent = await sendMessage(userId, fortune);
        if (sent) {
          updateLastPushDate(userId);
          console.log('✅');
          successCount++;
        } else {
          console.log('⚠️ (发送失败，已记录)');
          failCount++;
        }
      }

      logEntry.results.push({
        // 仅记录脱敏后的标识，避免日志文件被外部读取后泄露姓名/userId
        userIdHash: require('crypto').createHash('sha256').update(String(userId)).digest('hex').slice(0, 8),
        status: dryRun ? 'dry-run' : (sent ? 'success' : 'failed')
      });
    } catch (e) {
      console.log(`❌ ${e.message}`);
      failCount++;
      logEntry.results.push({
        userIdHash: require('crypto').createHash('sha256').update(String(profile.userId || '')).digest('hex').slice(0, 8),
        status: 'error',
        error: e.message
      });
    }
  }

  console.log('   ' + '─'.repeat(50));
  console.log(`\n   ✅ 推送完成: ${successCount} 成功${failCount > 0 ? `, ${failCount} 失败` : ''}\n`);

  appendLog(logEntry);
  return { successCount, failCount };
}

// ============================================================
// 列出开启推送的用户
// ============================================================

function listPushUsers() {
  // 出于隐私小化原则，本命令只输出聚合计数，
  // 不提供任何逐条查看八字/姓名/userId/推送作息的通道，
  // 所有完整信息请仅通过 `node scripts/profile.js show <userId> --full` 在本人设备上查看。
  const profiles = loadAllProfiles();
  const targets = getUsersWithPushEnabled(profiles);

  console.log('\n📋 已开启每日运势推送的用户（仅聚合统计）:\n');
  console.error('   ⚠ 本命令不输出任何个人信息。需查看单个用户详情请在本人设备上使用：node scripts/profile.js show <userId> --full\n');

  if (targets.length === 0) {
    console.log('   （暂无用户开启推送）\n');
    return;
  }

  const channelsStat = {};
  for (const p of targets) {
    const ch = (p.preferences?.channels || ['openclaw'])[0] || 'openclaw';
    channelsStat[ch] = (channelsStat[ch] || 0) + 1;
  }
  const lastPushStat = targets.reduce((acc, p) => {
    acc[p.lastPushDate ? 'pushed' : 'never'] = (acc[p.lastPushDate ? 'pushed' : 'never'] || 0) + 1;
    return acc;
  }, {});
  console.log(`   总计：${targets.length} 人开启推送`);
  console.log(`   渠道分布：${Object.entries(channelsStat).map(([k,v])=>`${k}=${v}`).join(', ')}`);
  console.log(`   推送状态：已推送过=${lastPushStat.pushed||0}, 从未推送=${lastPushStat.never||0}`);
  console.log('');
}

// ============================================================
// 命令行入口
// ============================================================

async function main() {
  const args = process.argv.slice(2);

  if (args.includes('--list') || args.includes('-l')) {
    // 不再提供 --full 通道，避免批量暴露个人信息
    if (args.includes('--full')) {
      console.error('   ❌ 出于隐私考虑，--list --full 已在 v1.1.7 移除。如需查看单个用户详情，请在本人设备运行：');
      console.error('   node scripts/profile.js show <userId> --full');
      process.exit(1);
    }
    listPushUsers();
    return;
  }

  // --test 必须先于 --dry-run 处理，以便 --dry-run --test <userId> 走单用户路径。
  // v1.2.0：--test 已**强制 dry-run**，不再读取 --i-am-the-user 旁路。
  const testIdx = args.indexOf('--test');
  if (testIdx !== -1 && args[testIdx + 1]) {
    if (args.includes('--i-am-the-user')) {
      console.error('⚠ 注意：--i-am-the-user 旁路在 v1.2.0 起已**移除**。');
      console.error('  --test 现在统一以 dry-run 模式运行，不会向未 opt-in 用户做真实交付。');
    }
    await runPush({ dryRun: true, testUserId: args[testIdx + 1] });
    return;
  }

  if (args.includes('--dry-run') || args.includes('-d')) {
    await runPush({ dryRun: true });
    return;
  }

  if (args.length === 0) {
    await runPush({ dryRun: false });
    return;
  }

  // 帮助
  console.log(`
🌅 每日运势自动推送脚本

用法:
  node daily-push.js                  推送给所有已开启的用户
  node daily-push.js --dry-run        模拟推送（只计算不交付、不落盘）
  node daily-push.js --test <userId>  **调试专用**；v1.2.0 起强制 dry-run，不会做真实交付。
                                      要正式接收推送，请让该用户本人运行 push-toggle.js on <userId>。
  node daily-push.js --list           仅输出聚合统计（不提供 --full 通道）
隐私说明:
  - sendMessage 不会将个人运势输出到 stdout，仅在非 dry-run 下写入个人 outbox 由 OpenClaw 运行时点对点派发。
  - --dry-run 仅做运算验证，不会产生任何 outbox 文件、也不会将运势正文输出到终端。
  - --test 默认不会绕过 opt-in 检查，以防止给未同意用户发送个人运势。
  - --list 只出现聚合计数（人数/渠道分布/推送状态），不提供 --full 通道。
  - 需查看某个用户详情，请在本人设备上运行：node scripts/profile.js show <userId> --full。

配置:
  - 用户的 preferences.pushEnabled 需为 true 且必须包含显式 opt-in 时间戳（preferences.pushOptInAt）
  - 用户的 preferences.morningTime 决定推送时间（默认07:00）
  - 渠道由 preferences.channels 指定（openclaw/telegram/feishu，由 OpenClaw 运行时统一投递）
  - 用户需有完整的八字信息（bazi.dayStem 不为空）

OpenClaw Cron 配置:
  openclaw cron add "0 7 * * *" "cd <skill-dir> && node scripts/daily-push.js"
`);
}

main().catch(e => {
  console.error('❌ 推送脚本出错:', e);
  process.exit(1);
});


path: scripts/feixing.py
content: #!/usr/bin/env python3
"""
九宫飞星理气风水系统
根据流年/流月/流日推算九星位置，判断方位吉凶
"""

import sys
from datetime import datetime, timedelta

# 九星定义
JIUXING = {
    1: {"name": "贪狼", "wuxing": "水", "color": "白", "nature": "吉"},
    2: {"name": "巨门", "wuxing": "土", "color": "黑", "nature": "凶"},
    3: {"name": "禄存", "wuxing": "木", "color": "碧", "nature": "凶"},
    4: {"name": "文曲", "wuxing": "木", "color": "绿", "nature": "吉"},
    5: {"name": "廉贞", "wuxing": "土", "color": "黄", "nature": "凶"},
    6: {"name": "武曲", "wuxing": "金", "color": "白", "nature": "吉"},
    7: {"name": "破军", "wuxing": "金", "color": "赤", "nature": "凶"},
    8: {"name": "左辅", "wuxing": "土", "color": "白", "nature": "吉"},
    9: {"name": "右弼", "wuxing": "火", "color": "紫", "nature": "吉"},
}

# 九宫方位
GONGFU = {
    0: {"name": "中宫", "direction": "中", "bz": "中央"},
    1: {"name": "坎宫", "direction": "北", "bz": "正北"},
    2: {"name": "坤宫", "direction": "西南", "bz": "西南"},
    3: {"name": "震宫", "direction": "东", "bz": "正东"},
    4: {"name": "巽宫", "direction": "东南", "bz": "东南"},
    5: {"name": "中宫", "direction": "中", "bz": "中央"},
    6: {"name": "乾宫", "direction": "西北", "bz": "西北"},
    7: {"name": "兑宫", "direction": "西", "bz": "正西"},
    8: {"name": "艮宫", "direction": "东北", "bz": "东北"},
    9: {"name": "离宫", "direction": "南", "bz": "正南"},
}

# 入中宫星（2004-2043年）
# 2004-2023: 八白左辅
# 2024-2043: 九紫右弼
def get_ruzong_star(year: int) -> int:
    """获取当年入中宫的星"""
    if 2004 <= year <= 2023:
        return 8
    elif 2024 <= year <= 2043:
        return 9
    elif 1984 <= year <= 2003:
        return 7  # 七赤破军
    elif 1964 <= year <= 1983:
        return 6  # 六白武曲
    elif 1944 <= year <= 1963:
        return 5  # 五黄廉贞
    else:
        return 9  # 默认九紫


def fly_star(ruzong: int, days: int = 0) -> dict:
    """
    计算飞星位置
    ruzong: 入中宫的星（1-9）
    days: 偏移天数（0=年飞星，1-30=月飞星，1-365=日飞星）
    """
    # 基础偏移
    offset = days % 9
    
    # 飞星位置（九宫飞布）
    result = {}
    for pos in range(9):
        # 飞星计算公式
        star = (ruzong + offset - pos - 1) % 9 + 1
        result[pos] = star
    
    return result


def get_year_feixing(year: int) -> dict:
    """流年飞星"""
    ruzong = get_ruzong_star(year)
    return fly_star(ruzong, 0)


def get_month_feixing(year: int, month: int) -> dict:
    """流月飞星"""
    # 流年星
    year_ruzong = get_ruzong_star(year)
    # 流月星：年星+月数-2（简化）
    month_star = (year_ruzong + month - 2) % 9 + 1
    return fly_star(month_star, 0)


def get_day_feixing(date: datetime) -> dict:
    """流日飞星"""
    base_date = datetime(2024, 1, 1)
    days = (date - base_date).days
    ruzong = get_ruzong_star(date.year)
    return fly_star(ruzong, days % 360)


def get_star_nature(star: int) -> str:
    """获取星曜性质"""
    return JIUXING[star]["nature"]


def get_direction_luck(star: int, year: int) -> str:
    """根据星曜和年份判断方位吉凶"""
    ruzong = get_ruzong_star(year)
    
    # 当令星
    if star == ruzong:
        return "当令旺星"
    # 退运星（隔3位以上）
    elif (star - ruzong) % 9 > 4:
        return "退运衰星"
    # 生气星
    elif star in [1, 3, 4]:
        return "生气吉星"
    # 死气星
    elif star in [2, 5, 7]:
        return "死气凶星"
    else:
        return "平"


def format_output(feixing: dict, year: int, month: int = None, day: datetime = None) -> str:
    """格式化输出"""
    ruzong = get_ruzong_star(year)
    ruzong_info = JIUXING[ruzong]
    
    output = f"🧭 {year}年九宫飞星"
    if month:
        output = f"🧭 {year}年{month}月九宫飞星"
    if day:
        output = f"🧭 {day.strftime('%Y年%m月%d日')}九宫飞星"
    
    output += f"\n\n【年星入中】：{ruzong_info['name']}（{ruzong_info['wuxing']}）"
    
    # 特殊方位
    luck_positions = {
        1: "一白贪狼 - 桃花位/文昌位",
        4: "四绿文曲 - 文昌位",
        6: "六白武曲 - 偏财运",
        8: "八白左辅 - 正财运",
        9: "九紫右弼 - 喜庆位",
    }
    
    output += "\n\n【吉位】\n"
    for pos, star in feixing.items():
        if star in [1, 4, 6, 8, 9] and pos != 0:
            info = JIUXING[star]
            luck = luck_positions.get(star, "")
            output += f"🟢 {GONGFU[pos]['bz']}：{info['name']} - {info['nature']} {luck}\n"
    
    output += "\n【凶位】\n"
    for pos, star in feixing.items():
        if star in [2, 3, 5, 7] and pos != 0:
            info = JIUXING[star]
            nature = get_star_nature(star)
            output += f"🔴 {GONGFU[pos]['bz']}：{info['name']} - {nature}\n"
    
    output += "\n【方位详解】\n"
    for pos, star in feixing.items():
        if pos == 0:
            continue
        info = JIUXING[star]
        gong = GONGFU[pos]
        luck = get_direction_luck(star, year)
        
        emoji = "🟢" if info["nature"] == "吉" else "🔴" if info["nature"] == "凶" else "🟡"
        output += f"{emoji} {gong['bz']}（{gong['direction']}）：{info['name']}{info['color']}星 - {luck}\n"
    
    output += """
---
※ 风水仅供参考，阳宅风水还需综合考量 ※"""
    
    return output


def cmd_year(year: int = None):
    """流年飞星"""
    if year is None:
        year = datetime.now().year
    feixing = get_year_feixing(year)
    print(format_output(feixing, year))


def cmd_month(year: int, month: int):
    """流月飞星"""
    feixing = get_month_feixing(year, month)
    print(format_output(feixing, year, month))


def cmd_today():
    """今日飞星"""
    today = datetime.now()
    feixing = get_day_feixing(today)
    print(format_output(feixing, today.year, day=today))


if __name__ == "__main__":
    if len(sys.argv) < 2:
        cmd_year()
    elif sys.argv[1] == "year":
        cmd_year()
    elif sys.argv[1] == "month":
        now = datetime.now()
        cmd_month(now.year, now.month)
    elif sys.argv[1] == "today":
        cmd_today()
    elif len(sys.argv) == 2:
        # 只传入年份
        try:
            year = int(sys.argv[1])
            cmd_year(year)
        except:
            print("用法: python feixing.py [year|month|today|<年份>] [<月份>]")
    elif len(sys.argv) == 3:
        try:
            year = int(sys.argv[1])
            month = int(sys.argv[2])
            cmd_month(year, month)
        except:
            print("用法: python feixing.py [year|month|today|<年份>] [<月份>]")
    else:
        print("用法: python feixing.py [year|month|today|<年份>] [<月份>]")
        print("例:")
        print("  python feixing.py year        # 流年飞星")
        print("  python feixing.py month       # 流月飞星")
        print("  python feixing.py today       # 今日飞星")
        print("  python feixing.py 2026        # 2026年流年飞星")
        print("  python feixing.py 2026 3      # 2026年3月飞星")


path: scripts/fengshui.js
content: #!/usr/bin/env node
/**
 * 风水分析脚本
 * 支持：阳宅风水、办公室风水、颜色风水
 */

const fs = require('fs');
const path = require('path');

// 八卦方位对应
const baGuaDirection = {
  '乾': { direction: '西北', number: 6, element: '金', color: '白色', trait: '领导、权威' },
  '坤': { direction: '西南', number: 2, element: '土', color: '黄色', trait: '柔顺、包容' },
  '震': { direction: '东', number: 3, element: '木', color: '绿色', trait: '震动、创新' },
  '巽': { direction: '东南', number: 4, element: '木', color: '绿色', trait: '进入、柔和' },
  '坎': { direction: '北', number: 1, element: '水', color: '黑色', trait: '险陷、智慧' },
  '离': { direction: '南', number: 9, element: '火', color: '红色', trait: '明亮、热情' },
  '艮': { direction: '东北', number: 8, element: '土', color: '黄色', trait: '停止、稳定' },
  '兑': { direction: '西', number: 7, element: '金', color: '白色', trait: '喜悦、口才' }
};

// 九宫飞星宫位序列（洛书飞布次序）
const PALACE_SEQ = ['中宫', '乾', '兑', '艮', '离', '坎', '坤', '震', '巽'];
const STAR_NAMES = ['一白', '二黑', '三碧', '四绿', '五黄', '六白', '七赤', '八白', '九紫'];

// 财位寻找
const caiWei = {
  '明财位': '大门对角线位置',
  '暗财位': '住宅中心点',
  '流年财位': '每年变化，见流年飞星',
  '固定财位': '根据主人八字喜忌定'
};

// 吉凶方位
const jiXiongDirections = {
  '乾': { wealth: '吉', health: '平', career: '吉', love: '平' },
  '坤': { wealth: '平', health: '吉', career: '平', love: '吉' },
  '震': { wealth: '平', health: '平', career: '平', love: '吉' },
  '巽': { wealth: '吉', health: '平', career: '吉', love: '平' },
  '坎': { wealth: '平', health: '凶', career: '凶', love: '平' },
  '离': { wealth: '吉', health: '吉', career: '平', love: '平' },
  '艮': { wealth: '吉', health: '吉', career: '平', love: '平' },
  '兑': { wealth: '平', health: '平', career: '凶', love: '吉' }
};

/**
 * 获取流年飞星（动态计算，支持任意年份）
 * 基准：2024年中宫=一白，每年中宫星顺序+3（3年一轮：一白→四绿→七赤）
 */
function getFlyingStars(year = new Date().getFullYear()) {
  // yearOffset: 0→中宫一白, 1→中宫四绿, 2→中宫七赤
  const yearOffset = ((year - 2024) % 3 + 3) % 3;
  const result = {};
  for (let i = 0; i < 9; i++) {
    const starIdx = (i + yearOffset * 3) % 9;
    result[STAR_NAMES[starIdx]] = PALACE_SEQ[i];
  }
  return result;
}

/**
 * 分析大门风水
 */
function analyzeMainDoor(direction) {
  const info = baGuaDirection[direction];
  if (!info) {
    return { error: '方向不明确' };
  }
  
  let analysis = '';
  let suggestions = [];
  
  // 大门朝向来判断
  if (['乾', '兑'].includes(direction)) {
    analysis = '大门朝西或西北，金气旺盛';
    suggestions.push('宜摆放金属装饰增强运势');
    suggestions.push('可放白色或金色地毯');
  } else if (['震', '巽'].includes(direction)) {
    analysis = '大门朝东或东南，木气旺盛';
    suggestions.push('宜摆放绿植招贵人');
    suggestions.push('保持空间明亮通风');
  } else if (['坎'].includes(direction)) {
    analysis = '大门朝北，水气旺盛';
    suggestions.push('宜用蓝色或黑色装饰');
    suggestions.push('注意防潮防湿');
  } else if (['离'].includes(direction)) {
    analysis = '大门朝南，火气旺盛';
    suggestions.push('宜用红色或紫色装饰');
    suggestions.push('注意防火安全');
  } else if (['坤', '艮'].includes(direction)) {
    analysis = '大门朝西南或东北，土气旺盛';
    suggestions.push('宜用黄色或棕色装饰');
    suggestions.push('保持空间稳重踏实');
  }
  
  return {
    direction: info.direction,
    element: info.element,
    analysis,
    suggestions
  };
}

/**
 * 分析财位
 */
function analyzeWealthPosition(bazi, year = new Date().getFullYear()) {
  const stars = getFlyingStars(year);
  const baziDayStem = bazi?.charAt(0) || '甲';
  
  // 找出财位
  const positions = [];
  
  // 明财位
  positions.push({
    type: '明财位',
    location: '大门对角线（进门后左右角落）',
    description: '气流入处，聚气纳财',
    direction: '根据实际大门位置定'
  });
  
  // 流年财位
  const yiMaPosition = Object.entries(stars).find(([name]) => name === '一白贪狼')?.[1] || '坎';
  positions.push({
    type: '流年财位（2026）',
    location: `${baGuaDirection[yiMaPosition]?.direction || '北'}`,
    description: '一白贪狼星所在，利财运',
    stars: stars
  });
  
  // 日主喜用（简化）
  const dayElements = {
    '甲': '木', '乙': '木', '丙': '火', '丁': '火',
    '戊': '土', '己': '土', '庚': '金', '辛': '金',
    '壬': '水', '癸': '水'
  };
  const dayElement = dayElements[baziDayStem] || '土';
  
  // 根据日主五行找财位
  const wealthDirections = {
    '木': '东方、东南',
    '火': '南方',
    '土': '西南、东北',
    '金': '西方、西北',
    '水': '北方'
  };
  
  positions.push({
    type: '八字喜用财位',
    location: wealthDirections[dayElement] || '根据八字定',
    description: `日主${baziDayStem}，喜${dayElement}，财位在${wealthDirections[dayElement]}`
  });
  
  return positions;
}

/**
 * 分析卧室风水
 */
function analyzeBedroom(direction) {
  const info = baGuaDirection[direction];
  if (!info) {
    return { error: '方向不明确' };
  }
  
  const bedroomAnalysis = {
    '乾': {
      score: 85,
      pros: ['领导力增强', '事业运势提升'],
      cons: ['过于刚硬', '需柔和装饰平衡'],
      tips: ['放置圆润家具', '用红色点缀']
    },
    '坤': {
      score: 80,
      pros: ['睡眠安稳', '感情和睦'],
      cons: ['行动力减弱', '需适当运动'],
      tips: ['保持整洁', '放置陶瓷饰品']
    },
    '震': {
      score: 75,
      pros: ['事业突破', '贵人运强'],
      cons: ['情绪波动大', '需静心'],
      tips: ['放置绿植', '避免尖锐物品']
    },
    '巽': {
      score: 78,
      pros: ['思维活跃', '学习运佳'],
      cons: ['易犹豫不决', '需果断'],
      tips: ['保持空气流通', '放置文昌塔']
    },
    '坎': {
      score: 70,
      pros: ['智慧提升', '财运渐佳'],
      cons: ['健康需注意', '多运动'],
      tips: ['保持干燥', '放置属火物品']
    },
    '离': {
      score: 82,
      pros: ['名气提升', '桃花运佳'],
      cons: ['情绪波动', '需平和'],
      tips: ['避免强光直射', '放置水景装饰']
    },
    '艮': {
      score: 88,
      pros: ['健康安稳', '守财能力强'],
      cons: ['过于保守', '需突破'],
      tips: ['放置金属装饰', '适当变动布局']
    },
    '兑': {
      score: 76,
      pros: ['口才提升', '社交运佳'],
      cons: ['易起争执', '需忍让'],
      tips: ['放置鲜花', '保持明亮']
    }
  };
  
  return bedroomAnalysis[direction] || bedroomAnalysis['艮'];
}

/**
 * 分析办公室风水
 */
function analyzeOffice() {
  return {
    desk: {
      ideal: '背靠实墙，面朝门口或窗户',
      avoid: '背窗坐、横梁压顶',
      direction: '坐北朝南或坐东朝西'
    },
    wealth: {
      position: '大门对角线',
      tips: ['放置貔貅或金蟾', '保持整洁', '不放垃圾桶']
    },
    career: {
      position: '东或东南',
      tips: ['放置绿植', '文昌位放毛笔或书籍']
    },
    avoid: [
      '横梁压顶',
      '背后有窗户',
      '正对厕所门',
      '灯光直射头顶'
    ]
  };
}

/**
 * 生成颜色建议
 */
function generateColorAdvice(bazi) {
  const dayStem = bazi?.charAt(0) || '甲';
  const dayElements = {
    '甲': '木', '乙': '木', '丙': '火', '丁': '火',
    '戊': '土', '己': '土', '庚': '金', '辛': '金',
    '壬': '水', '癸': '水'
  };
  
  const element = dayElements[dayStem] || '土';
  
  const colorMap = {
    '木': { lucky: ['绿色', '青色', '蓝色'], avoid: ['白色', '金色'], reason: '木生火，绿色助木' },
    '火': { lucky: ['红色', '紫色', '绿色'], avoid: ['黑色', '蓝色'], reason: '火生土，红紫色助火' },
    '土': { lucky: ['黄色', '棕色', '红色'], avoid: ['绿色', '青色'], reason: '土生金，黄色助土' },
    '金': { lucky: ['白色', '金色', '黄色'], avoid: ['红色', '紫色'], reason: '金生水，白金色助金' },
    '水': { lucky: ['黑色', '蓝色', '白色'], avoid: ['黄色', '棕色'], reason: '水生木，蓝色助水' }
  };
  
  return colorMap[element] || colorMap['土'];
}

/**
 * 生成完整风水报告
 */
function generateFengShuiReport(bazi, year = new Date().getFullYear()) {
  const stars = getFlyingStars(year);
  const dayStem = bazi?.charAt(0) || '甲';
  
  // 流年分析
  let report = `
🏠 【风水分析报告】

━━━━━━━━━━━━━━━━━━━━

📅 流年：${year}年
🧮 八字：${bazi || '未提供'}

━━━━━━━━━━━━━━━━━━━━

✨ 流年飞星（${year}年）

`;
  
  for (const [star, position] of Object.entries(stars)) {
    // 中宫特殊处理
    if (position === '中宫' || position === '中') {
      report += `  ${star} → 中宫\n`;
      report += `    方位：中 | 五行：土\n`;
      report += `    财：平 | 健康：吉 | 事业：平\n\n`;
      continue;
    }
    const info = baGuaDirection[position] || {};
    const jiXiong = jiXiongDirections[position] || {};
    report += `  ${star} → ${info.direction || position}\n`;
    report += `    方位：${info.direction} | 五行：${info.element}\n`;
    report += `    财：${jiXiong.wealth} | 健康：${jiXiong.health} | 事业：${jiXiong.career}\n\n`;
  }
  
  // 财位分析
  const wealthPositions = analyzeWealthPosition(bazi, year);
  report += `
💰 财位分析

`;
  
  wealthPositions.forEach(pos => {
    report += `【${pos.type}】\n`;
    report += `  位置：${pos.location}\n`;
    report += `  说明：${pos.description}\n\n`;
  });
  
  // 颜色建议
  const colorAdvice = generateColorAdvice(dayStem);
  report += `
🎨 幸运颜色

  幸运色：${colorAdvice.lucky.join('、')}
  忌用色：${colorAdvice.avoid.join('、')}
  原因：${colorAdvice.reason}

`;
  
  // 方位分析
  report += `
🧭 各方位吉凶

`;
  
  for (const [gua, info] of Object.entries(baGuaDirection)) {
    const jx = jiXiongDirections[gua];
    if (jx) {
      report += `【${info.direction}】${gua}\n`;
      report += `  财：${jx.wealth} | 健康：${jx.health} | 事业：${jx.career} | 感情：${jx.love}\n`;
      report += `  布置建议：${info.trait}\n\n`;
    }
  }
  
  // 办公室风水
  const office = analyzeOffice();
  report += `
💼 办公室风水

  理想工位：${office.desk.ideal}
  宜朝向：${office.desk.direction}
  忌讳：${office.desk.avoid}

  财位布置：${office.wealth.tips.join('、')}
  事业位：${office.career.tips.join('、')}

`;
  
  // 综合建议
  report += `
💡 综合建议

  1. 财位保持整洁，避免堆放杂物
  2. 门口保持畅通，气流流通
  3. 每日开窗通风，引入新鲜气场
  4. 根据今日幸运色选择穿着或装饰
  5. 流年不利方位可用水景或绿植化解

`;
  
  return report;
}

// 主入口
const args = process.argv.slice(2);

if (args[0] === '--help' || args[0] === '-h') {
  console.log(`
🏠 风水分析

用法:
  node fengshui.js                    # 基础分析
  node fengshui.js <八字>             # 带八字分析
  node fengshui.js <八字> <年份>      # 指定年份

示例:
  node fengshui.js
  node fengshui.js 戊子
  node fengshui.js 戊子 2026
`);
} else {
  const bazi = args[0] || '';
  const year = parseInt(args[1]) || new Date().getFullYear();
  console.log(generateFengShuiReport(bazi, year));
}

module.exports = { 
  analyzeWealthPosition, 
  generateColorAdvice, 
  getFlyingStars,
  analyzeOffice 
};


path: scripts/jieqi.js
content: #!/usr/bin/env node
/**
 * 节气精确计算模块
 * 基于太阳黄道经度（简化 VSOP87），精度 ±15 分钟，对应到日期误差 < 1 天
 */

const DEG = Math.PI / 180;

/**
 * 计算给定儒略日的太阳黄道经度（度）
 * 来源：Jean Meeus《Astronomical Algorithms》第27章
 */
function sunLongitude(jd) {
  const T = (jd - 2451545.0) / 36525;
  const M = (357.52911 + 35999.05029 * T - 0.0001537 * T * T) * DEG;
  const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
  const C =
    (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M) +
    (0.019993 - 0.000101 * T) * Math.sin(2 * M) +
    0.000289 * Math.sin(3 * M);
  return ((L0 + C) % 360 + 360) % 360;
}

/**
 * 牛顿迭代：求太阳黄道经度恰好为 targetLon 时的儒略日
 */
function jdeAtLongitude(year, targetLon) {
  // 初始估算：以平均运动推算
  let jd = 2451545.0 + (year - 2000 + ((targetLon - 280.46 + 360) % 360) / 360) * 365.2422;
  for (let i = 0; i < 50; i++) {
    let diff = ((targetLon - sunLongitude(jd) + 540) % 360) - 180;
    if (Math.abs(diff) < 1e-6) break;
    jd += (diff / 360) * 365.2422;
  }
  return jd;
}

/**
 * 儒略日 → 北京时间（UTC+8）日期
 */
function jdToCST(jde) {
  const jd = jde + 8 / 24; // 转 UTC+8
  const z = Math.floor(jd + 0.5);
  let a = z;
  if (z >= 2299161) {
    const alpha = Math.floor((z - 1867216.25) / 36524.25);
    a = z + 1 + alpha - Math.floor(alpha / 4);
  }
  const b = a + 1524;
  const c = Math.floor((b - 122.1) / 365.25);
  const d = Math.floor(365.25 * c);
  const e = Math.floor((b - d) / 30.6001);
  const day   = b - d - Math.floor(30.6001 * e);
  const month = e < 14 ? e - 1 : e - 13;
  const yr    = month > 2 ? c - 4716 : c - 4715;
  return { year: yr, month, day };
}

// ── 12 个月建节气（定义月柱边界）─────────────────────────────────────────
//   名称      黄道经度   对应日历月
const MONTH_JIEQI = [
  { name: '小寒', lon: 285, calMonth:  1 },
  { name: '立春', lon: 315, calMonth:  2 },
  { name: '惊蛰', lon: 345, calMonth:  3 },
  { name: '清明', lon:  15, calMonth:  4 },
  { name: '立夏', lon:  45, calMonth:  5 },
  { name: '芒种', lon:  75, calMonth:  6 },
  { name: '小暑', lon: 105, calMonth:  7 },
  { name: '立秋', lon: 135, calMonth:  8 },
  { name: '白露', lon: 165, calMonth:  9 },
  { name: '寒露', lon: 195, calMonth: 10 },
  { name: '立冬', lon: 225, calMonth: 11 },
  { name: '大雪', lon: 255, calMonth: 12 },
];

// 未过当月节气 → 属于上个节气月（寅月=1, 卯月=2, ..., 丑月=12）
const LUNAR_BEFORE = [11, 12,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10];
// 已过当月节气 → 属于当月节气月
const LUNAR_AFTER  = [12,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11];

// 简单 LRU 缓存，避免重复计算
const _cache = new Map();
function cachedJdeAtLon(year, lon) {
  const key = `${year}:${lon}`;
  if (!_cache.has(key)) _cache.set(key, jdToCST(jdeAtLongitude(year, lon)));
  return _cache.get(key);
}

/**
 * 给定阳历日期，返回八字月柱的节气月序号
 * 1 = 寅月（正月，立春后）
 * 2 = 卯月（惊蛰后）
 * ...
 * 12 = 丑月（小寒后）
 */
function getLunarMonth(year, month, day) {
  const jq = MONTH_JIEQI[month - 1];
  const { day: jqDay } = cachedJdeAtLon(year, jq.lon);
  return day >= jqDay ? LUNAR_AFTER[month - 1] : LUNAR_BEFORE[month - 1];
}

/**
 * 判断给定日期是否已过立春（用于年柱计算）
 * 立春 = 黄道 315°
 */
function isAfterLiChun(year, month, day) {
  if (month > 2) return true;
  if (month < 2) return false;
  const { day: liChunDay } = cachedJdeAtLon(year, 315);
  return day >= liChunDay;
}

/**
 * 获取指定年份某节气的北京时间日期（供外部查询）
 * @param {number} year
 * @param {string} name  节气名称，如 '立春'
 * @returns {{ year, month, day }}
 */
function getJieQiDate(year, name) {
  const jq = MONTH_JIEQI.find(j => j.name === name);
  if (!jq) throw new Error(`未知节气: ${name}`);
  return cachedJdeAtLon(year, jq.lon);
}

// ── 命令行调试模式 ───────────────────────────────────────────────────────
if (require.main === module) {
  const year = parseInt(process.argv[2]) || new Date().getFullYear();
  console.log(`\n${year}年 十二月建节气（北京时间）\n${'─'.repeat(30)}`);
  for (const jq of MONTH_JIEQI) {
    const d = cachedJdeAtLon(year, jq.lon);
    console.log(`  ${jq.name}  ${d.year}-${String(d.month).padStart(2,'0')}-${String(d.day).padStart(2,'0')}`);
  }
}

module.exports = { getLunarMonth, isAfterLiChun, getJieQiDate };


path: scripts/liuyao.js
content: #!/usr/bin/env node
/**
 * 六爻预测脚本
 * 模拟三枚铜钱摇六次成卦
 * 输入：6组阴阳信息（每组3个0或1，1=阳面）
 */

const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];

// 八卦对应
const baGua = {
  '乾': '☰', '兑': '☱', '离': '☲', '震': '☳',
  '巽': '☴', '坎': '☵', '艮': '☶', '坤': '☷'
};

// 八卦属性
const baGuaInfo = {
  '乾': { element: '金', direction: '西北', trait: '刚健' },
  '兑': { element: '金', direction: '西', trait: '喜悦' },
  '离': { element: '火', direction: '南', trait: '明亮' },
  '震': { element: '木', direction: '东', trait: '震动' },
  '巽': { element: '木', direction: '东南', trait: '入' },
  '坎': { element: '水', direction: '北', trait: '险陷' },
  '艮': { element: '土', direction: '东北', trait: '止' },
  '坤': { element: '土', direction: '西南', trait: '柔顺' }
};

// 六亲
const liuQin = ['父母', '兄弟', '子孙', '妻财', '官鬼', '子孙', '妻财', '官鬼'];

// 地支藏干（简化）
const zangGan = {
  '子': ['癸'], '丑': ['己', '癸', '辛'], '寅': ['甲', '丙', '戊'],
  '卯': ['乙'], '辰': ['戊', '乙', '癸'], '巳': ['丙', '庚', '戊'],
  '午': ['丁', '己'], '未': ['己', '丁', '乙'], '申': ['庚', '壬', '戊'],
  '酉': ['辛'], '戌': ['戊', '辛', '丁'], '亥': ['壬', '甲']
};

/**
 * 抛铜钱模拟
 * 3个铜钱，正面=阳，背面=阴
 * 3正=老阳(变阴)，2正1背=少阳(阳)
 * 3背=老阴(变阳)，2背1正=少阴(阴)
 */
function tossCoin() {
  // 三枚铜钱各自独立：正面(1)=阳，背面(0)=阴
  const heads = [0, 1, 2].reduce((sum) => sum + (Math.random() < 0.5 ? 1 : 0), 0);
  if (heads === 3) return '阳动'; // 老阳（三正，变爻）
  if (heads === 2) return '阴';   // 少阴（二正一背，不变）
  if (heads === 1) return '阳';   // 少阳（一正二背，不变）
  return '阴动';                   // 老阴（三背，变爻）
}

/**
 * 模拟完整六次摇卦
 */
function simulateCoins() {
  const results = [];
  for (let i = 0; i < 6; i++) {
    results.push(tossCoin());
  }
  return results;
}

/**
 * 从输入解析卦象
 * 输入格式：六个0/1/2/3的数字
 * 0=少阳(阳不动)，1=少阴(阴不动)，2=老阳(动)，3=老阴(动)
 */
function parseCoins(input) {
  const results = [];
  for (const char of input) {
    const num = parseInt(char);
    if (num === 0 || num === 1) {
      // 不动爻
      results.push(num === 0 ? '阳' : '阴');
    } else if (num === 2) {
      // 老阳变阴
      results.push('阳动');
    } else if (num === 3) {
      // 老阴变阳
      results.push('阴动');
    }
  }
  return results;
}

/**
 * 根据地支找六亲
 */
function findLiuQin(zhi, riGan) {
  const riEl = getElement(riGan);
  const zhiEl = getElement(zhi);
  
  // 五行生克
  if (riEl === '木') {
    if (zhiEl === '木') return '比肩';
    if (zhiEl === '火') return '食神';
    if (zhiEl === '土') return '偏财';
    if (zhiEl === '金') return '官鬼';
    if (zhiEl === '水') return '印绶';
  } else if (riEl === '火') {
    if (zhiEl === '火') return '比肩';
    if (zhiEl === '土') return '食神';
    if (zhiEl === '金') return '偏财';
    if (zhiEl === '水') return '官鬼';
    if (zhiEl === '木') return '印绶';
  } else if (riEl === '土') {
    if (zhiEl === '土') return '比肩';
    if (zhiEl === '金') return '食神';
    if (zhiEl === '水') return '偏财';
    if (zhiEl === '木') return '官鬼';
    if (zhiEl === '火') return '印绶';
  } else if (riEl === '金') {
    if (zhiEl === '金') return '比肩';
    if (zhiEl === '水') return '食神';
    if (zhiEl === '木') return '偏财';
    if (zhiEl === '火') return '官鬼';
    if (zhiEl === '土') return '印绶';
  } else if (riEl === '水') {
    if (zhiEl === '水') return '比肩';
    if (zhiEl === '木') return '食神';
    if (zhiEl === '火') return '偏财';
    if (zhiEl === '土') return '官鬼';
    if (zhiEl === '金') return '印绶';
  }
  
  return '无';
}

/**
 * 获取五行
 */
function getElement(zhi) {
  const elements = {
    '子': '水', '丑': '土', '寅': '木', '卯': '木',
    '辰': '土', '巳': '火', '午': '火', '未': '土',
    '申': '金', '酉': '金', '戌': '土', '亥': '水'
  };
  return elements[zhi] || '土';
}

/**
 * 生成六爻卦象
 */
function generateLiuYao(coins, riGan, riZhi) {
  // 用时间或给定信息确定卦
  const date = new Date();
  const hour = date.getHours();
  
  // 简化：以下卦+上卦
  const gua64 = ['乾', '坤', '屯', '蒙', '需', '讼', '师', '比',
    '小畜', '履', '泰', '否', '同人', '大有', '谦', '豫',
    '随', '蛊', '临', '观', '噬嗑', '贲', '剥', '复',
    '无妄', '大畜', '颐', '大过', '坎', '离', '咸', '恒',
    '遁', '大壮', '晋', '明夷', '家人', '睽', '蹇', '解',
    '损', '益', '夬', '姤', '萃', '升', '困', '井',
    '革', '鼎', '震', '艮', '渐', '归妹', '丰', '旅',
    '巽', '兑', '涣', '节', '中孚', '小过', '既济', '未济'];
  
  // 动爻组合确定卦
  const dongCount = coins.filter(c => c.includes('动')).length;
  const guaIndex = (dongCount * 6 + coins.filter(c => c === '阳' || c === '阳动').length) % 64;
  const guaName = gua64[guaIndex];
  
  // 世应关系（简化）
  const shiYing = {
    '乾': { shi: '五爻', ying: '二爻' },
    '坤': { shi: '二爻', ying: '五爻' },
    '屯': { shi: '初爻', ying: '四爻' },
    '蒙': { shi: '三爻', ying: '上爻' },
    'default': { shi: '三爻', ying: '上爻' }
  };
  
  const sy = shiYing[guaName] || shiYing['default'];
  
  // 构建爻列表
  const yaoList = coins.map((c, i) => {
    const isYang = c === '阳' || c === '阳动';
    const isDong = c.includes('动');
    const zhi = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'][i];
    const gan = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'][i];
    const liuqin = findLiuQin(zhi, riGan);
    
    return {
      name: yaoNames[i],
      yinYang: isYang ? '阳' : '阴',
      dong: isDong ? '动' : '',
      zhi,
      gan,
      liuqin
    };
  });
  
  return {
    guaName,
    yaoList,
    shiYing: sy,
    dongCount
  };
}

/**
 * 判断吉凶
 */
function judgeFortune(hexagram) {
  const { dongCount, yaoList } = hexagram;
  
  let result;
  if (dongCount === 0) {
    result = '静卦 - 事情稳定，需等待时机';
  } else if (dongCount === 1) {
    result = '独发 - 一件事主导，专注可成';
  } else if (dongCount === 2) {
    result = '双动 - 两件事关联，需协调';
  } else if (dongCount >= 3) {
    result = '多动 - 变数较多，谨慎行事';
  }
  
  // 检查动爻的六亲
  const dongLiuQin = yaoList.filter(y => y.dong === '动').map(y => y.liuqin);
  
  if (dongLiuQin.includes('官鬼')) {
    result += '\n⚠️ 动爻带官鬼 - 谨防小人、压力';
  }
  if (dongLiuQin.includes('妻财')) {
    result += '\n💰 动爻带妻财 - 财运显现或破耗';
  }
  if (dongLiuQin.includes('子孙')) {
    result += '\n🌟 动爻带子孙 - 好事发生，贵人运';
  }
  if (dongLiuQin.includes('父母')) {
    result += '\n📚 动爻带父母 - 文书、合同事宜';
  }
  
  return result;
}

/**
 * 生成报告
 */
function generateReport(hexagram, question = '占卜事宜') {
  const { guaName, yaoList, shiYing, dongCount } = hexagram;
  const guaInfo = baGuaInfo[guaName[0]] || baGuaInfo['乾'];
  
  const fortune = judgeFortune(hexagram);
  
  let report = `
🔮 【六爻预测】

📋 占卜信息
   事项：${question}
   动爻数：${dongCount}个

🎴 卦象
   卦名：${guaName}
   卦性：${guaInfo.trait}

📊 世应关系
   世爻：${shiYing.shi}
   应爻：${shiYing.ying}

📜 六爻排列（从下至上）
`;
  
  yaoList.reverse().forEach((y, i) => {
    const symbol = y.yinYang === '阳' ? '━━' : ' ━ ';
    const dongSymbol = y.dong === '动' ? ' ◯' : '  ';
    report += `   ${y.name} ${symbol}${dongSymbol} ${y.zhi}${y.gan} ${y.liuqin}\n`;
  });
  
  report += `
⚖️ 吉凶判断
${fortune}

💡 建议
${dongCount === 0 ? '静待时机，不宜妄动' : 
  dongCount === 1 ? '专注一事，把握独发之机' : 
  '多事并行，量力而行'}
`;
  
  return report;
}

// 主入口
const args = process.argv.slice(2);

if (args[0] === '--help' || args[0] === '-h') {
  console.log(`
六爻预测

用法:
  node liuyao.js                  # 模拟摇卦
  node liuyao.js 010203          # 指定6个爻(0=阳不动,1=阴不动,2=阳动,3=阴动)
  node liuyao.js 010203 婚姻     # 指定爻+问题

示例:
  node liuyao.js
  node liuyao.js 012013 事业
`);
} else if (args.length >= 1 && /^\d{6}$/.test(args[0])) {
  // 指定爻象
  const coins = parseCoins(args[0]);
  const question = args[1] || '占卜事宜';
  const riGan = '戊'; // 简化处理
  const riZhi = '子';
  const hexagram = generateLiuYao(coins, riGan, riZhi);
  console.log(generateReport(hexagram, question));
} else if (args.length >= 2 && /^\d{6}$/.test(args[0])) {
  // 爻象 + 问题
  const coins = parseCoins(args[0]);
  const question = args.slice(1).join(' ');
  const riGan = '戊';
  const riZhi = '子';
  const hexagram = generateLiuYao(coins, riGan, riZhi);
  console.log(generateReport(hexagram, question));
} else {
  // 模拟摇卦
  console.log('🎲 摇卦中...\n');
  const coins = simulateCoins();
  const question = args.join(' ') || '占卜事宜';
  const riGan = '戊';
  const riZhi = '子';
  const hexagram = generateLiuYao(coins, riGan, riZhi);
  console.log(`📍 摇得：[${coins.join(' ')}]\n`);
  console.log(generateReport(hexagram, question));
}


path: scripts/preference-tracker.js
content: #!/usr/bin/env node
/**
 * 用户偏好学习系统
 * 基于用户互动记录，动态调整关注领域权重
 *
 * 用法（供 Agent 调用）:
 *   node preference-tracker.js record <userId> <topic> [context]
 *   node preference-tracker.js weights <userId>
 *   node preference-tracker.js top <userId> [n]
 */

const fs = require('fs');
const path = require('path');

const PROFILES_DIR = path.join(__dirname, '../data/profiles');

// 支持的关注领域
// 注意：v1.1.8 起移除「官司」主题。法律相关问题不在本 skill 声明的覆盖范围内
// （SKILL.md 已声明不覆盖法律/医疗/金融等专业领域），
// 持久化追踪「官司」会构成超出声明范围的敏感画像，因此从可记录主题中下线。
// 历史档案中已存在的「官司」记录会在 _computeWeights/_normalizeWeights 中被自动忽略。
const TOPICS = ['财运', '事业', '感情', '健康', '婚姻', '子女', '出行', '风水'];

// 互动来源权重倍率
const CONTEXT_MULTIPLIERS = {
  explicit_query: 2.0,   // 用户主动提问
  topic_drill:    1.5,   // 用户追问同一话题
  morning_push:   0.8,   // 晨间推送被消费
  evening_push:   0.8,   // 晚间推送被消费
};

const DECAY_LAMBDA     = 0.05;  // 衰减系数，约14天半衰期
const MAX_LOG_SIZE     = 500;   // 最大记录条数
const MIN_WEIGHT       = 0.5;   // 进入 focusAreas 的最低权重
const DEFAULT_FOCUS    = ['事业', '财运', '健康'];

// ─────────────────────────────────────────────
// 文件 I/O
// ─────────────────────────────────────────────

function loadProfile(userId) {
  const fp = path.join(PROFILES_DIR, `${userId}.json`);
  if (!fs.existsSync(fp)) return null;
  return JSON.parse(fs.readFileSync(fp, 'utf8'));
}

function saveProfile(userId, profile) {
  const fp = path.join(PROFILES_DIR, `${userId}.json`);
  profile.updatedAt = new Date().toISOString().split('T')[0];
  fs.writeFileSync(fp, JSON.stringify(profile, null, 2), 'utf8');
}

// ─────────────────────────────────────────────
// 核心算法：指数衰减加权
// ─────────────────────────────────────────────

function _computeWeights(log) {
  const now = Date.now();
  const totals = {};
  TOPICS.forEach(t => { totals[t] = 0; });

  for (const entry of (log || [])) {
    if (!TOPICS.includes(entry.topic)) continue;
    if (!entry.ts || typeof entry.ts !== 'number') continue;
    const daysDelta = (now - entry.ts) / 86400000;
    const multiplier = CONTEXT_MULTIPLIERS[entry.context] || 1.0;
    totals[entry.topic] += multiplier * Math.exp(-DECAY_LAMBDA * daysDelta);
  }
  return totals;
}

function _normalizeWeights(raw) {
  const max = Math.max(...Object.values(raw), 0.001);
  const result = {};
  for (const [t, w] of Object.entries(raw)) {
    result[t] = parseFloat((w / max).toFixed(3));
  }
  return result;
}

function _sortedTopics(weights) {
  return Object.entries(weights)
    .sort((a, b) => b[1] - a[1])
    .map(([topic, weight]) => ({ topic, weight }));
}

// ─────────────────────────────────────────────
// 公开 API
// ─────────────────────────────────────────────

/**
 * 记录一次互动
 *
 * 隐私披露（v1.1.9 加强）：
 * - 本函数仅在用户在 profile.preferences 中显式 opt-in 后才会写入。
 *   必须同时满足：
 *     - profile.preferences.allowPreferenceLearning === true
 *     - profile.preferences.preferenceLearningOptInAt 为 ISO 时间戳
 *   如果任一条件不满足，本函数返回 false 且**不**写入任何 interactionLog。
 * - 调用方可以运行 CLI: `node preference-tracker.js opt-in <userId>` 来开启；
 *   并以 `node preference-tracker.js opt-out <userId>` 随时关闭且清空日志。
 * - 写入位置：data/profiles/<userId>.json 的 interactionLog 字段（**仅本地**，不上传）。
 * - 留存策略：滚动保留最近 MAX_LOG_SIZE 条，超出后**永久丢弃最旧记录**；
 *   也可以随时通过 `node scripts/profile.js delete <userId>` 整体清空，
 *   或手工编辑 JSON 中的 interactionLog 字段。
 * - 主题白名单：见 TOPICS。`官司` 等敏感法律主题已下线，不再被记录。
 * - 推送来源（context=morning_push/evening_push）的写入由 daily-push.js 串联，
 *   仅在用户已显式 opt-in 推送（preferences.pushOptInAt 存在）后才会发生。
 */
function recordInteraction(userId, topic, context = 'explicit_query') {
  const profile = loadProfile(userId);
  if (!profile) return false;
  if (!TOPICS.includes(topic)) return false;

  // 默认关闭：如果用户未显式 opt-in，本函数不进行任何写入
  // （这是实现层隔离，不是允许会话余颂静默采集。）
  const allowed = !!(profile.preferences
    && profile.preferences.allowPreferenceLearning === true
    && profile.preferences.preferenceLearningOptInAt);
  if (!allowed) return false;

  if (!profile.interactionLog) profile.interactionLog = [];

  profile.interactionLog.push({ ts: Date.now(), topic, context });

  // 超出上限时删除最旧的记录
  if (profile.interactionLog.length > MAX_LOG_SIZE) {
    profile.interactionLog = profile.interactionLog.slice(-MAX_LOG_SIZE);
  }

  // 重新计算并更新 focusAreas
  const raw = _computeWeights(profile.interactionLog);
  const normalized = _normalizeWeights(raw);
  const top = _sortedTopics(normalized)
    .filter(({ weight }) => weight >= MIN_WEIGHT)
    .slice(0, 3)
    .map(({ topic }) => topic);

  if (!profile.preferences) profile.preferences = {};
  profile.preferences.focusAreas = top.length > 0 ? top : DEFAULT_FOCUS;

  saveProfile(userId, profile);
  return true;
}

/**
 * 显式 opt-in 偏好学习
 */
function optInPreferenceLearning(userId) {
  const profile = loadProfile(userId);
  if (!profile) return false;
  if (!profile.preferences) profile.preferences = {};
  profile.preferences.allowPreferenceLearning = true;
  profile.preferences.preferenceLearningOptInAt = new Date().toISOString();
  saveProfile(userId, profile);
  return true;
}

/**
 * opt-out：关闭偏好学习并清空已有历史
 */
function optOutPreferenceLearning(userId) {
  const profile = loadProfile(userId);
  if (!profile) return false;
  if (!profile.preferences) profile.preferences = {};
  profile.preferences.allowPreferenceLearning = false;
  profile.preferences.preferenceLearningOptInAt = null;
  profile.interactionLog = [];
  profile.preferences.focusAreas = DEFAULT_FOCUS;
  saveProfile(userId, profile);
  return true;
}

/**
 * 获取所有领域权重（归一化，降序）
 */
function getWeights(userId) {
  const profile = loadProfile(userId);
  if (!profile) return [];
  const raw = _computeWeights(profile.interactionLog || []);
  const normalized = _normalizeWeights(raw);
  return _sortedTopics(normalized);
}

/**
 * 获取 top-n 关注领域（有互动记录用计算结果，否则用 profile 默认值）
 */
function getTopTopics(userId, n = 3) {
  const profile = loadProfile(userId);
  if (!profile) return DEFAULT_FOCUS.slice(0, n);

  const log = profile.interactionLog || [];
  if (log.length === 0) {
    return (profile.preferences?.focusAreas || DEFAULT_FOCUS).slice(0, n);
  }

  const raw = _computeWeights(log);
  const normalized = _normalizeWeights(raw);
  return _sortedTopics(normalized)
    .slice(0, n)
    .map(({ topic }) => topic);
}

module.exports = { recordInteraction, getWeights, getTopTopics, optInPreferenceLearning, optOutPreferenceLearning, TOPICS };

// ─────────────────────────────────────────────
// 命令行入口（供 Agent 调用）
// ─────────────────────────────────────────────

if (require.main === module) {
  const [cmd, userId, ...rest] = process.argv.slice(2);

  if (!cmd || !userId) {
    console.log(`
🧠 用户偏好追踪器

用法:
  node preference-tracker.js opt-in  <userId>          # 启用偏好学习（需本人同意）
  node preference-tracker.js opt-out <userId>          # 关闭偏好学习并清空互动日志
  node preference-tracker.js record  <userId> <topic> [context]
  node preference-tracker.js weights <userId>
  node preference-tracker.js top     <userId> [n]

说明:
  - record 仅在 profile.preferences.allowPreferenceLearning === true 且记录了 opt-in 时间戳后才会写入。
  - opt-out 后，interactionLog 会被一次性清空，focusAreas 回退到默认值。

topic 可选: ${TOPICS.join(' | ')}
context 可选: explicit_query | topic_drill | morning_push | evening_push

示例:
  node preference-tracker.js opt-in 123456
  node preference-tracker.js record 123456 财运 explicit_query
  node preference-tracker.js opt-out 123456
`);
    process.exit(1);
  }

  switch (cmd) {
    case 'opt-in': {
      const ok = optInPreferenceLearning(userId);
      if (ok) {
        console.log(JSON.stringify({ success: true, userId, allowPreferenceLearning: true, action: 'opt-in' }));
      } else {
        console.error(`❌ 用户不存在或未初始化: ${userId}`);
        process.exit(1);
      }
      break;
    }
    case 'opt-out': {
      const ok = optOutPreferenceLearning(userId);
      if (ok) {
        console.log(JSON.stringify({ success: true, userId, allowPreferenceLearning: false, action: 'opt-out', interactionLogCleared: true }));
      } else {
        console.error(`❌ 用户不存在: ${userId}`);
        process.exit(1);
      }
      break;
    }
    case 'record': {
      const [topic, context = 'explicit_query'] = rest;
      if (!topic) { console.error('缺少 topic 参数'); process.exit(1); }
      const ok = recordInteraction(userId, topic, context);
      if (!ok) {
        // 给出明确提示，避免调用方静默以为写入成功
        console.log(JSON.stringify({ success: false, userId, topic, context, reason: 'profile-missing-or-not-opted-in' }));
      } else {
        console.log(JSON.stringify({ success: true, userId, topic, context }));
      }
      break;
    }
    case 'weights': {
      const weights = getWeights(userId);
      console.log(JSON.stringify({ userId, weights }));
      break;
    }
    case 'top': {
      const n = parseInt(rest[0] || '3');
      const topics = getTopTopics(userId, n);
      console.log(JSON.stringify({ userId, topTopics: topics }));
      break;
    }
    default:
      console.error(`未知命令: ${cmd}`);
      process.exit(1);
  }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)