Skill: vincent-vstudios/yinian-zwds (version 2.1.0)

--- name: yinian-zwds version: 2.1.0 description: "一念紫微斗数 (Yinian Zi Wei Dou Shu) — 专业AI紫微斗数排盘解盘系统。三派合一：三合派/飞星派/占验派。触发词：紫微斗数、紫微、命盘、排盘、ZWDS、ziwei、Purple Star" --- # 一念紫微斗数 (Yinian ZWDS) 三派合一的专业AI紫微斗数排盘解盘系统。师承中州派、钦天门、紫云体系。 ## 使用时机 当用户请求以下时启用此技能： 1. **排盘**：用户提供出生年月日时，请求紫微斗数命盘 2. **解盘**：请求命盘解读、宫位分析、星曜分析、四化解读 3. **流派分析**：指定使用三合/飞星/占验/综合哪一派解读 4. **流年运势**：大限/流年/流月运势 5. **星曜查询**：询问具体星曜含义、宫位含义 ## 触发关键词 紫微斗数、紫微、命盘、排盘、zwds、ziwei、purple star、紫微排盘、紫微命盘、四大神数之首 ## 技能结构 ``` yinian-zwds/ ├── SKILL.md ← 本文件（核心指南） ├── ETHICS.md ← 伦理准则 ├── references/ │ ├── stars.md ← 星曜详解（14主星+辅星） │ ├── palaces.md ← 12宫详解 │ ├── mutagen.md ← 四化体系详解（含生年四化表） │ ├── sanhe.md ← 三合派/飞星派/占验派理论 │ └── patterns.md ← 特殊格局（预留） └── scripts/ └── zwds_calc.py ← 紫微斗数排盘Python脚本（精确排盘用） ``` ## 用户引导 ### 排盘请求 当用户请求排盘时，收集出生信息： ``` 🔮 一念紫微斗数排盘系统 请提供您的出生信息： 1. 出生日期：公历年/月/日（如 2000-8-16） 2. 出生时辰：几点（0-23点，越精确越好） 3. 性别：男/女 4. 流派偏好（可选）：三合/飞星/占验/综合（默认综合） 📌 如知道农历生日，请注明 ``` ### 排盘方法 **方法一（推荐）：调用排盘脚本** → 使用exec工具运行： ```bash cd ~/.openclaw/skills/yinian-zwds/scripts && python3 -c " from zwds_calc import generate_astrolabe, format_astrolabe r = generate_astrolabe('2000-8-16', 6, '男') print(format_astrolabe(r) if r else '排盘失败') " ``` 如果接收的输入是农历： ```bash cd ~/.openclaw/skills/yinian-zwds/scripts && python3 -c " from zwds_calc import generate_astrolabe, format_astrolabe r = generate_astrolabe('2000-7-17', 6, '男', is_lunar=True) print(format_astrolabe(r) if r else '排盘失败') " ``` **方法二（无脚本时）**：使用下方【排盘基础】知识手动推算。 ### 解盘流程 ``` 用户请求 → 收集出生信息 → 排盘（脚本或手动） ↓ 按流派解盘： ├─ 三合派 → 星曜性质 + 三方四正 + 亮度 ├─ 飞星派 → 宫干四化脉络 + 自化 + 追禄追忌 ├─ 占验派 → 太岁入卦 + 特殊格局 └─ 综合 → 三派融合分析 ↓ 输出结论建议（🛑 均标注：文化参考，理性看待） ``` ### 深度解盘引导 排盘完成后可问： ``` 命盘已排出，您可以了解： 🏠 命宫解读 → 性格格局 💰 财帛宫 → 求财方式 💼 官禄宫 → 事业方向 💕 夫妻宫 → 感情姻缘 🔄 迁移宫 → 外出发展 🧘 福德宫 → 精神世界 📅 生年四化 → 命运脉络 ⚡ 三方四正 → 宫位联动 ``` --- ## 排盘基础 ### 12 宫名称与顺序 | 索引 | 宫名 | 英文 | 三方四正 | |------|------|------|----------| | 0 | 命宫 | Life | 财帛宫(4) 官禄宫(8) 迁移宫(6) | | 1 | 兄弟宫 | Siblings | 疾厄宫(5) 田宅宫(9) 交友宫(7) | | 2 | 夫妻宫 | Marriage | 迁移宫(6) 福德宫(10) 官禄宫(4) | | 3 | 子女宫 | Children | 交友宫(7) 父母宫(11) 田宅宫(5) | | 4 | 财帛宫 | Wealth | 官禄宫(8) 命宫(0) 福德宫(2) | | 5 | 疾厄宫 | Health | 田宅宫(9) 兄弟宫(1) 父母宫(3) | | 6 | 迁移宫 | Travel | 命宫(0) 夫妻宫(2) 福德宫(10) | | 7 | 交友宫 | Friends | 子女宫(3) 兄弟宫(1) 父母宫(11) | | 8 | 官禄宫 | Career | 财帛宫(4) 命宫(0) 夫妻宫(2) | | 9 | 田宅宫 | Property | 疾厄宫(5) 兄弟宫(1) 子女宫(3) | | 10 | 福德宫 | Spirit | 迁移宫(6) 财帛宫(4) 夫妻宫(2) | | 11 | 父母宫 | Parents | 交友宫(7) 子女宫(3) 疾厄宫(5) | ### 14 主星 **紫微**（己土·帝星）：领导力、贵气。喜左辅右弼。陷则刚愎。 **天机**（乙木·智星）：聪明、策划、变动。喜天梁太阴。 **太阳**（丙火·阳）：光明、公益、发散。庙旺宽厚，陷则劳碌。 **武曲**（辛金·财帛主）：财富、刚毅、决断。喜天府天相。 **天同**（壬水·福星）：福气、协调。庙旺安逸，陷则懒散。 **廉贞**（丁火·次桃花）：是非、权术、文艺。庙旺清正。 **天府**（戊土·库星）：稳定、包容、守成。喜紫微武曲。 **太阴**（癸水·阴）：温柔、美感、财富。庙旺有情调。 **贪狼**（甲木·桃花主）：交际、才艺、欲望。庙旺才艺。 **巨门**（癸水·暗星）：口才、是非、思辨。喜日月。 **天相**（壬水·印星）：诚信、公正、辅佐。喜紫微天府。 **天梁**（戊土·荫星）：贵气、长寿、清高。喜天机太阳。 **七杀**（庚金·将星）：权威、变动。庙旺威震八方。 **破军**（癸水·耗星）：变革、突破。喜贪狼七杀。 ### 亮度体系 **庙** > **旺** > **得** > **利** > **平** > **陷** - 庙/旺：星曜力量最强，大吉 - 得：中等正面 - 利：偏正面 - 平：中性 - 陷：力量弱，偏凶 ### 生年四化表 ``` 甲：廉贞禄 破军权 武曲科 太阳忌 乙：天机禄 天梁权 紫微科 太阴忌 丙：天同禄 天机权 文昌科 廉贞忌 丁：太阴禄 天同权 天机科 巨门忌 戊：贪狼禄 太阴权 右弼科 天机忌 己：武曲禄 贪狼权 天梁科 文曲忌 庚：太阳禄 武曲权 太阴科 天同忌 辛：巨门禄 太阳权 文曲科 文昌忌 壬：天梁禄 紫微权 左辅科 武曲忌 癸：破军禄 巨门权 太阴科 贪狼忌 ``` ### 四化含义 - **化禄**：增加、融合、顺利（大吉） - **化权**：主导、掌控、权威（吉带刚） - **化科**：文雅、名声、调和（吉善） - **化忌**：缺失、纠葛、收缩（凶，需注意） ### 三方四正 本宫 + 对宫(±6) + 三合左(±4) + 三合右(±8) 例：命宫(寅=2) → 迁移宫(申=8)对宫 + 财帛宫(午=6) + 官禄宫(戌=10) ### 空宫处理 空宫（无主星）→ 借对宫星曜论断，需标明"借" --- ## 三派解读要点 ### 三合派（中州派体系） - 核心：星曜性质 × 亮度 × 三方四正 - 主星组合定格局（如紫府相格、杀破狼格） - 生年四化为命运主线 - 常用星组：紫府相、杀破狼、机月同梁、日月同宫 ### 飞星派（钦天门体系） - 核心：宫干四化轨迹串联 - 重点看：飞宫四化、自化、追禄追忌 - 来因宫定人生起点方向 - 体用宫分体（本质）和用（表现） ### 占验派（紫云体系） - 核心：太岁入卦 + 星曜互涉 - 重点：特殊格局识别 - 六亲宫活用与转换 --- ## 解盘输出规范 ``` 📜 文化参考，理性看待 【命主基本信息】 四柱：XXX 【XX宫解读】 三合派视角：星曜组合 + 亮度 + 三方四正联动的解读... 飞星派视角：四化脉络轨迹... 【结论建议】 ... 📜 以上内容由AI生成，仅供文化参考 ``` **语言风格：** 专业但不晦涩，用比喻帮助理解。语气用"可能性"，不用"一定""注定"等绝对化断言。 --- ## 伦理提醒 - 所有解读开头/结尾标注「📜 文化参考，理性看待」 - 不提供医疗、财务、法律建议 - 不允许用户基于命盘做重大人生决策 - 保护用户隐私，不持久存储生辰信息 - 明确标注为AI生成，非真人命理师 详见 `ETHICS.md`。

path: ETHICS.md
content: # ETHICS.md — 一念紫微斗数伦理准则

⚠️ 本系统基于中国传统文化紫微斗数理论，仅供 **文化参考与自我反思** 之用。不构成任何医疗、财务、感情或法律建议，也不应作为现实决策依据。

## 红线

1. **不提供医疗建议** — 无论命盘如何，健康问题请咨询专业医生
2. **不进行财务决策指导** — 命理仅供趋势参考，非投资建议
3. **不替用户做人生重大决策** — 如婚姻、跳槽、搬迁
4. **保护用户隐私** — 生辰八字等敏感信息不持久存储
5. **明确标注AI生成** — 所有解读均为AI辅助生成，非真人命理师
6. **不制造焦虑** — 避免使用恐吓性语言，注重正向引导
7. **出海合规** — 所有内容需标注「Cultural Reference, View Rationally」

## 输出规范

- 所有解读开头标注：📜 文化参考，理性看待
- 避免绝对化断言（如 "你一定会"、"注定"）
- 保持「可能性 + 参考」的语气
- 提供建设性建议而非宿命论


path: references/mutagen.md
content: # 四化体系 — 一念紫微斗数

## 什么是四化

四化 = 化禄 + 化权 + 化科 + 化忌

**有四个来源：**
1. 生年四化（本命四化）— 天干决定，一生不变
2. 飞星四化（宫干飞化）— 各宫天干飞入他宫
3. 大限四化 — 大限天干引发的四化
4. 流年四化 — 流年天干引发的四化

## 生年天干四化表

```
甲：廉贞化禄、破军化权、武曲化科、太阳化忌
乙：天机化禄、天梁化权、紫微化科、太阴化忌
丙：天同化禄、天机化权、文昌化科、廉贞化忌
丁：太阴化禄、天同化权、天机化科、巨门化忌
戊：贪狼化禄、太阴化权、右弼化科、天机化忌
己：武曲化禄、贪狼化权、天梁化科、文曲化忌
庚：太阳化禄、武曲化权、太阴化科、天同化忌
辛：巨门化禄、太阳化权、文曲化科、文昌化忌
壬：天梁化禄、紫微化权、左辅化科、武曲化忌
癸：破军化禄、巨门化权、太阴化科、贪狼化忌
```

## 四化性质

### 化禄
- 性质：增加、融合、顺利
- 宫位：该宫主题和谐顺畅
- 吉凶：大吉，舒缓凶象

### 化权
- 性质：增加、主导、掌控
- 宫位：该宫主题主控性强
- 吉凶：吉带刚，掌权势

### 化科
- 性质：文雅、名声、调和
- 宫位：该宫主题有文化气息
- 吉凶：吉善，化解风波

### 化忌
- 性质：缺失、纠葛、收缩
- 宫位：该宫主题有缺憾
- 吉凶：凶，需注意的领域

## 飞星派四化补充

- **宫干飞四化**：本宫天干飞出四化到各宫
  - 如：命宫天干戊，贪狼化禄入戌（财帛宫）
- **自化**：本宫星曜自带四化（天干飞化入本宫）
  - 表示该宫自我变动特质
- **追禄**：他宫化禄入某宫
- **追忌**：他宫化忌入某宫


path: references/palaces.md
content: # 12宫详解 — 一念紫微斗数

## 命宫
- 核心：人的本质、性格、格局高低
- 星曜决定：外在表现，内在特质
- 空宫：需借迁移宫星曜论
- 四化入命：禄=和谐，权=自信，科=文雅，忌=纠葛

## 兄弟宫
- 核心：兄弟姐妹缘、手足关系
- 也看：家庭环境早年，母亲部分信息
- 吉星：手足和睦有助
- 煞星：竞争或疏远

## 夫妻宫
- 核心：婚姻感情、配偶特质
- 看：桃花运、婚姻质量、感情模式
- 化忌：感情不顺或配偶不利
- 煞星重：易有争执

## 子女宫
- 核心：子女缘、生育能力
- 看：与子女关系、子女成就
- 吉星：子女优秀
- 煞星：子女缘薄或管教困难

## 财帛宫
- 核心：求财方式、财富格局
- 看：收入来源、财运起伏
- 武曲庙：靠专业技能致富
- 化禄：财运佳，化忌：破财

## 疾厄宫
- 核心：身体健康状况
- 看：先天体质、易患疾病
- 煞星多：体虚或意外之灾
- 化忌：体质偏弱或需注意的疾病

## 迁移宫
- 核心：外出发展、人际关系
- 看：外地机遇、社交能力
- 命宫空宫时：借迁移宫论人格
- 吉星：外出顺利、广结人脉

## 交友宫（仆役宫）
- 核心：朋友、社交圈、下属
- 看：人际关系质量、贵人/小人
- 吉星：得朋友助力
- 煞星：易有是非或背刺

## 官禄宫
- 核心：事业、职业、成就
- 看：适合的职业方向、事业发展
- 官禄三方：命宫+夫妻宫+财帛宫
- 煞星：事业波折

## 田宅宫
- 核心：不动产、家庭、晚年
- 看：置业能力、居住环境
- 吉星：家业稳固
- 煞星：家宅不宁

## 福德宫
- 核心：精神世界、福分、思考
- 看：精神享受、生活质量
- 身宫在此：重精神生活
- 空宫：需借财帛宫论

## 父母宫
- 核心：父母缘、遗传、文化背景
- 看：家庭背景、与父母关系
- 吉星：得父母助
- 煞星：缘分薄或代沟


path: references/sanhe.md
content: # 三合派理论 — 一念紫微斗数

## 核心思想

三合派，也叫中州派，是紫微斗数在港台最主流的流派。

**核心理念：** 以星曜性质和亮度解读为主，配合三方四正联动。

## 三方四正

- **三方**：本宫 + ±4宫（三合关系）
- **四正**：本宫 + 对宫(±6)
- 例：命宫寅（2）→ 三方财帛宫午（6）官禄宫戌（10）→ 四正迁移宫申（8）

## 星曜亮度体系

| 亮度 | 含义 |
|---|---|
| 庙 | 星曜力量最强，最吉 |
| 旺 | 力量强，吉 |
| 得 | 力量中等，吉 |
| 利 | 偏正面 |
| 平 | 中性 |
| 陷 | 力量弱，偏凶 |

## 常用星组

- **紫府相格**（紫微+天府+天相）：贵气、稳重
- **杀破狼格**（七杀+破军+贪狼）：变动大、机遇多
- **机月同梁格**（天机+太阴+天同+天梁）：稳成、才艺
- **日月同宫格**：太阳太阴同宫，贵气

## 空宫处理

某宫无主星 → 借对宫星曜论（需标明"借"）

---

# 飞星派理论 — 一念紫微斗数

## 核心思想

飞星派，师承钦天门体系。

**核心理念：** 以四化飞星的脉络串联为主线，宫干飞四化为主。

## 常用技法

- **宫干飞四化**：本宫天干四化入他宫
- **自化**：本宫自飞四化（天干四化入本宫）
- **飞宫串联**：A宫飞四化入B宫 → B宫再飞入C宫
- **来因宫**：出生年干的宫位，定方向

## 体用宫论

- **体宫**：命宫、财帛宫、官禄宫（本质）
- **用宫**：其他九宫（表现）

---

# 占验派理论 — 一念紫微斗数

## 核心思想

占验派，由紫云大师发扬。

**核心理念：** 以太岁入卦法为核心，结合星曜互涉和特殊格局。

## 常用技法

- **太岁入卦**：年干定位特殊影响
- **星曜互涉**：星与星之间的相互作用
- **特殊格局**：如紫微百官朝拱、天府守库等


path: references/stars.md
content: # 星曜详解 — 一念紫微斗数

## 14主星

### 紫微星（北斗帝星）
- 五行：己土
- 性质：帝星、尊贵
- 亮度影响：庙旺主贵气、领导力，陷落主刚愎、爱面子
- 喜：左辅、右弼（得助）、天相（印星）
- 忌：空劫（无助）、无煞星夹

### 天机星（南斗第三星）
- 五行：乙木
- 性质：智慧、策划、变动
- 喜：天梁（荫星）、太阴（内敛）
- 忌：巨门（暗口舌）、煞星（思绪混乱）

### 太阳星（中天主星）
- 五行：丙火（阳火）
- 性质：光明、发散、公益
- 庙旺：宽宏大量、贵人
- 陷落：劳碌、散财、好面子

### 武曲星（北斗第六星）
- 五行：辛金（阴金化为阳）
- 性质：财帛主、刚毅、决断
- 喜：天府（财库）、天相（印）
- 忌：七杀、破军（破财）

### 天同星（南斗第四星）
- 五行：壬水
- 性质：福星、协调、温和
- 庙旺：一生安逸
- 陷落：懒散、拖延

### 廉贞星（北斗第五星）
- 五行：丁火（次桃花，阴火）
- 性质：是非、权术、文艺
- 庙旺：清正、才华
- 陷落：是非、惹桃花

### 天府星（南斗主星）
- 五行：戊土
- 性质：财库、稳定、守成
- 喜：紫微（尊贵）、武曲（财）
- 忌：空劫（破库）

### 太阴星（中天主星）
- 五行：癸水（阴水）
- 性质：母星、温柔、美感、财富
- 庙旺：富有情调、美貌
- 陷落：阴郁、感情不顺

### 贪狼星（北斗第一星）
- 五行：甲木（阳木/桃花）
- 性质：交际、才艺、欲望
- 喜：武曲（才艺）、破军（开创）
- 忌：空亡、孤星

### 巨门星（北斗第二星）
- 五行：癸水（阴水/暗水）
- 性质：口才、是非、思辨
- 喜：太阳（消暗）、太阴（内敛）
- 忌：煞星（口舌是非）

### 天相星（南斗第五星）
- 五行：壬水
- 性质：印星、诚信、辅助
- 喜：紫微（权威）、天府（稳定）
- 忌：刑忌（不利）

### 天梁星（南斗第二星）
- 五行：戊土（阳土/荫）
- 性质：荫星、长寿、清高
- 喜：天机（聪慧）、太阳（光明）
- 忌：孤寡（孤独）

### 七杀星（南斗第一星）
- 五行：庚金（阳金/将星）
- 性质：将星、权威、变动
- 庙旺：威震八方
- 陷落：多事、劳碌

### 破军星（北斗第七星）
- 五行：癸水（阴水/耗）
- 性质：耗星、变革、突破
- 喜：贪狼（创新）、七杀（开创）
- 忌：煞星（破耗严重）

---

## 辅星精选

- **左辅**：助力、贵人（吉）
- **右弼**：助力、贵人（吉）
- **文昌**：文才、学历（吉）
- **文曲**：才艺、口才（杂）
- **天魁**：贵人提携（吉）
- **天钺**：贵人提携（吉）
- **禄存**：福德、财库（吉）
- **天马**：变动、迁移（杂）
- **擎羊**：刑伤、冲突（凶）
- **陀罗**：拖延、纠缠（凶）
- **火星**：急躁、暴发（凶）
- **铃星**：阴火、暗斗（凶）
- **地空**：空亡、理想（凶破）
- **地劫**：破耗、波折（凶破）


path: scripts/ai_engine.py
content: """
ai_engine.py — 一念紫微斗数 AI解盘执行引擎
真正的AI模型调用 + 完整解盘输出 + 流年流月运势

Author: 崽儿虾 🦞
"""

from typing import Optional, Dict, Any, List
import json
import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from ai_reader_v2 import generate_standard_report
from deep_reading import full_deep_reading
from zwds_calc import generate_astrolabe, astrolabe_to_json, compute_surrounded, BRANCH_TO_INDEX
from decadal import calculate_decadal_sequence


# ============================================================
# AI API 调用器（DeepSeek / OpenAI兼容）
# ============================================================

# API Key优先级：DeepSeek环境变量 > OpenClaw配置DeepSeekKey > OpenAI环境变量
import json

def _resolve_api_key():
    """解析API Key，优先DeepSeek"""
    # 1. DeepSeek环境变量
    ds_env = os.environ.get("DEEPSEEK_API_KEY", "")
    if ds_env:
        return ds_env
    
    # 2. OpenClaw配置中的DeepSeek Key
    _cfg_path = os.path.expanduser("~/.openclaw/openclaw.json")
    if os.path.exists(_cfg_path):
        try:
            with open(_cfg_path) as _f:
                _cfg = json.load(_f)
            _providers = _cfg.get("models", {}).get("providers", {})
            _key = _providers.get("deepseek", {}).get("apiKey", "")
            if _key:
                return _key
        except:
            pass
    
    # 3. 降级到OpenAI（但仅当key不是sk-proj-格式时才用）
    oa_env = os.environ.get("OPENAI_API_KEY", "")
    if oa_env and not oa_env.startswith("sk-proj-"):
        return oa_env
    
    return ""

AI_API_KEY = _resolve_api_key()
# DeepSeek API（国内可直连）
AI_BASE_URL = "https://api.deepseek.com/v1"
AI_MODEL = "deepseek-chat"


def call_llm(
    system_prompt: str,
    user_prompt: str,
    temperature: float = 0.7,
    max_tokens: int = 4096,
) -> str:
    """
    调用大模型 API 进行AI解盘
    自动检测使用 DeepSeek 或 OpenAI
    """
    import urllib.request
    import urllib.error
    
    [REDACTED_SECRET]
    base_url = AI_BASE_URL
    model = AI_MODEL
    
    if not [REDACTED_SECRET] "⚠️ 未找到AI API Key，请配置 DEEPSEEK_API_KEY 或 OPENAI_API_KEY"
    
    # DeepSeek对中文格式更友好
    if "deepseek" in model:
        system_prompt = system_prompt.replace("【", "<").replace("】", ">")
    
    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
        "temperature": temperature,
        "max_tokens": max_tokens,
    }
    
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        f"{base_url}/chat/completions",
        data=data,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
        method="POST",
    )
    
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read().decode("utf-8"))
            content = result["choices"][0]["message"]["content"]
            return content
    except Exception as e:
        return f"⚠️ AI调用失败: {str(e)}"


# ============================================================
# 完整解盘（排盘 → AI解读 → 输出报告）
# ============================================================

def ai_reading(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    target_year: Optional[int] = None,
) -> Dict[str, Any]:
    """
    端到端：出生信息 → 排盘 → 深度数据 → AI解读 → 格式化报告
    
    Returns:
        {
            "success": True/False,
            "chart_json": {...},
            "ai_raw": "AI原始输出文本",
            "ai_report": {...} or parsed dict,
            "telegram_text": "适合Telegram发送的文本",
            "error": "...",
        }
    """
    # 1. 生成Prompt
    result = generate_standard_report(
        date_str, hour, gender, is_lunar, target_year, model_type="deepseek"
    )
    if not result["success"]:
        return {"success": False, "error": result.get("error", "排盘失败")}
    
    # 2. 调用AI
    system_prompt = result["system_prompt"]
    user_prompt = result["ai_prompt"]
    
    ai_raw = call_llm(system_prompt, user_prompt)
    
    # 3. 解析AI输出
    report_parsed = _parse_ai_output(ai_raw)
    
    # 4. 格式化为Telegram文本
    tg_text = _format_reading_for_tg(
        result["chart_json"],
        report_parsed,
        result.get("deep_data", {}),
        ai_raw,
    )
    
    return {
        "success": True,
        "chart_json": result["chart_json"],
        "ai_raw": ai_raw,
        "ai_report": report_parsed,
        "telegram_text": tg_text,
    }


def _parse_ai_output(raw: str) -> Dict[str, Any]:
    """解析AI返回的JSON或文本报告"""
    # 先尝试JSON解析
    text = raw.strip()
    
    # 去除 ```json ``` 包裹
    if "```" in text:
        parts = text.split("```")
        for part in parts:
            part = part.strip()
            if part.startswith("json"):
                part = part[4:].strip()
            if part.startswith("{") and part.endswith("}"):
                try:
                    return json.loads(part)
                except:
                    continue
    
    # 直接找 JSON 对象
    start = text.find("{")
    end = text.rfind("}")
    if start >= 0 and end > start:
        try:
            return json.loads(text[start:end+1])
        except:
            pass
    
    # 无法解析，返回原始文本
    return {"raw_text": text}


def _format_reading_for_tg(
    chart_json: Dict,
    ai_report: Dict,
    deep_data: Dict,
    ai_raw: str,
) -> str:
    """格式化为Telegram友好文本（无markdown表格，无headings）"""
    lines = []
    lines.append("🔮 *紫微斗数 · AI深度解盘*")
    lines.append("")
    
    # 命主信息
    fp = chart_json.get("four_pillars", {})
    lines.append(f"📅 四柱：{fp.get('year','')} {fp.get('month','')} {fp.get('day','')} {fp.get('hour','')}")
    lines.append(f"🐉 五行局：{chart_json.get('five_elements','')}  命宫：{chart_json.get('palaces',[{}])[0].get('name_cn','')}")
    
    # 大限信息
    timing = deep_data.get("timing", {})
    d = timing.get("current_decadal")
    if d:
        lines.append(f"📍 当前大限：{d['palace_name']}（{d['age_start']}-{d['age_end']}岁）")
    lines.append("")
    
    # 生年四化
    if chart_json.get("mutagens"):
        m_strs = [f"{m['palace']}: {m['star']}化{m['mutagen']}" for m in chart_json["mutagens"]]
        lines.append(f"✨ 生年四化：{'｜'.join(m_strs)}")
        lines.append("")
    
    lines.append("━━━━━━━━━━━━")
    lines.append("")
    
    # AI解盘正文
    if isinstance(ai_report, dict) and "report" in ai_report:
        r = ai_report["report"]
        layers = r.get("layers", {})
        
        # 基础层
        basic = layers.get("basic", {})
        if isinstance(basic, dict):
            summary = basic.get("summary", "")
            if summary:
                lines.append(f"**命盘格局**")
                lines.append(summary)
                lines.append("")
            
            palaces = basic.get("palaces", [])
            if palaces:
                lines.append("**分宫解读**")
                for p in palaces:
                    name = p.get("name", "")
                    interp = p.get("interpretation", "")
                    if name and interp:
                        lines.append(f"▪ {name}：{_truncate(interp, 200)}")
                lines.append("")
        
        # 进阶层
        adv = layers.get("advanced", {})
        if isinstance(adv, dict):
            ma = adv.get("mutagen_analysis", "")
            pats = adv.get("patterns", [])
            if ma:
                lines.append(f"**四化脉络**")
                lines.append(_truncate(ma, 300))
                lines.append("")
            if pats:
                pstr = "、".join(pats) if isinstance(pats, list) else str(pats)
                lines.append(f"**格局**：{pstr[:200]}")
                lines.append("")
        
        # 高阶层
        tm = layers.get("timing", {})
        if isinstance(tm, dict):
            cd = tm.get("current_decadal", "")
            if cd:
                lines.append(f"**大限流年**")
                lines.append(_truncate(cd, 300))
                lines.append("")
        
        # 专业层
        prof = layers.get("professional", {})
        if isinstance(prof, dict):
            laiyin = prof.get("laiyin_analysis", "")
            tiyong = prof.get("tiyong_analysis", "")
            if laiyin:
                lines.append(f"**来因宫**：{_truncate(laiyin, 200)}")
            if tiyong:
                lines.append(f"**体用宫**：{_truncate(tiyong, 200)}")
            if laiyin or tiyong:
                lines.append("")
        
        # 结论
        conclusion = r.get("conclusion", "")
        if conclusion:
            lines.append(f"**综合判断**")
            lines.append(_truncate(conclusion, 400))
            lines.append("")
    
    else:
        # AI返回自由文本
        text = ai_raw.strip()
        if text:
            # 去除过大篇幅
            if len(text) > 3000:
                text = text[:3000] + "\n\n...（以下省略）"
            lines.append(text)
            lines.append("")
    
    lines.append("━" * 14)
    lines.append("📜 文化参考，理性看待")
    
    return "\n".join(lines)


def _truncate(text: str, max_len: int = 200) -> str:
    """截断文本并添加省略号"""
    if not text:
        return ""
    text = str(text)
    return text[:max_len] + "…" if len(text) > max_len else text


# ============================================================
# 流年/流月运势
# ============================================================

def generate_monthly_reading(
    date_str: str,
    hour: int,
    gender: str,
    target_year: int,
    target_month: int,
) -> Dict[str, Any]:
    """
    流月运势分析
    
    Args:
        target_month: 1-12
    """
    astro = generate_astrolabe(date_str, hour, gender)
    if not astro:
        return {"success": False, "error": "排盘失败"}
    
    deep = full_deep_reading(date_str, hour, gender, target_year=target_year)
    timing = deep.get("timing", {})
    
    # 流月地支
    monthly_branch_map = {
        1: "寅", 2: "卯", 3: "辰", 4: "巳", 5: "午", 6: "未",
        7: "申", 8: "酉", 9: "戌", 10: "亥", 11: "子", 12: "丑",
    }
    month_branch = monthly_branch_map[target_month]
    
    # 流月对应的宫位（命宫为0标准序）
    # 地支→标准宫序映射
    branch_to_std = {"寅": 0, "卯": 1, "辰": 2, "巳": 3, "午": 4, "未": 5,
                     "申": 6, "酉": 7, "戌": 8, "亥": 9, "子": 10, "丑": 11}
    # 需要映射回标准宫序——先看命宫地支
    soul_branch = _branch_value(astro.soul_palace_branch)
    soul_branch_idx = BRANCH_TO_INDEX.get(soul_branch, 0)
    
    # 流月对应标准宫序 = (月地支的iztro索引 - 命宫iztro索引) % 12 再转标准
    month_branch_idx = BRANCH_TO_INDEX.get(month_branch, 0)
    raw_offset = (month_branch_idx - soul_branch_idx) % 12
    palace_idx = raw_offset  # 在标准序中的索引
    
    palace = astro.palaces[palace_idx] if 0 <= palace_idx < len(astro.palaces) else None
    
    if not palace:
        return {"success": False, "error": f"找不到流月对应的宫位"}
    
    # 构建流月分析Prompt
    majors = [f"{s.name}({s.brightness})" + (f"→{s.mutagen}" if s.mutagen else "") for s in palace.major_stars]
    minors = [s.name for s in palace.minor_stars]
    
    mstr = "、".join(majors) if majors else "空宫"
    mistr = "、".join(minors) if minors else "无辅星"
    
    d = timing.get("current_decadal", {})
    decadal_info = f"{d['palace_name']}（{d['age_start']}-{d['age_end']}岁）" if d else "未知"
    
    prompt = f"""你是一位紫微斗数命理师。请简析以下流月运势：

年份：{target_year}年{target_month}月
地支：{month_branch}
当前大限：{decadal_info}

流月宫位：{palace.name_cn}
主星：{mstr}
辅星：{mistr}
空宫：{"是（需借对宫）" if palace.is_empty else "否"}

请给出以下信息（每条50字以内）：
1. 本月运势核心提示
2. 需要注意的领域
3. 有利的发力方向
"""
    
    system = "你是紫微斗数命理师。简短回复，每条不超过50字。使用可能性语气。📜"
    ai_resp = call_llm(system, prompt, temperature=0.5, max_tokens=800)
    
    return {
        "success": True,
        "year": target_year,
        "month": target_month,
        "month_branch": month_branch,
        "palace_name": palace.name_cn,
        "major_stars": mstr,
        "minor_stars": mistr,
        "is_empty": palace.is_empty,
        "analysis": ai_resp,
    }


def _branch_value(branch: str) -> str:
    """获取地支的字符串值，确保兼容各种输入"""
    if not branch:
        return "子"
    return branch


# ============================================================
# 流年详尽运势
# ============================================================

def generate_yearly_reading(
    date_str: str,
    hour: int,
    gender: str,
    target_year: int,
) -> Dict[str, Any]:
    """
    流年详尽运势分析
    """
    astro = generate_astrolabe(date_str, hour, gender)
    if not astro:
        return {"success": False, "error": "排盘失败"}
    
    deep = full_deep_reading(date_str, hour, gender, target_year=target_year)
    timing = deep.get("timing", {})
    
    # 流年地支
    year_offset = (target_year - 1984) % 12
    branch_list = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]
    year_branch = branch_list[year_offset]
    
    # 流年所在标准宫序
    soul_branch = _branch_value(astro.soul_palace_branch)
    soul_branch_idx = BRANCH_TO_INDEX.get(soul_branch, 0)
    year_branch_idx = BRANCH_TO_INDEX.get(year_branch, 0)
    raw_offset = (year_branch_idx - soul_branch_idx) % 12
    palace_idx = raw_offset
    
    palace = astro.palaces[palace_idx] if 0 <= palace_idx < len(astro.palaces) else None
    
    d = timing.get("current_decadal", {})
    decadal_info = f"{d['palace_name']}（{d['age_start']}-{d['age_end']}岁）" if d else "未知"
    
    # 叠宫：大限宫位 × 流年宫位
    decadal_pi = d.get("palace_index", 0) if d else 0
    overlay = f"大限{astro.palaces[decadal_pi].name_cn}叠流年{astro.palaces[palace_idx].name_cn}" if palace else "无法计算"
    
    majors_str = ""
    minors_str = ""
    empty_str = ""
    
    if palace:
        majors = [f"{s.name}({s.brightness})" + (f"→{s.mutagen}" if s.mutagen else "") for s in palace.major_stars]
        minors = [s.name for s in palace.minor_stars]
        majors_str = "、".join(majors) if majors else "空宫"
        minors_str = "、".join(minors) if minors else "无辅星"
        empty_str = "【该宫为空宫，需借对宫星曜论断】" if palace.is_empty else ""
    
    prompt = f"""请以紫微斗数流年视角解读以下运势：

基础信息：
- {target_year}年（地支{year_branch}）流年运势
- 当前大限：{decadal_info}
- 叠宫：{overlay}

流年宫位详情：
- 宫位：{palace.name_cn if palace else "未知"}
- 主星：{majors_str}
- 辅星：{minors_str}
- {empty_str}

请从以下五个维度分析（每项60字以内）：
1. 事业：工作发展的机会与挑战
2. 财运：收入波动与投资时机
3. 感情：人际关系与姻缘走向
4. 健康：身体状况注意点
5. 建议：本年行动指南
"""
    
    system = "你是紫微斗数命理师。逐项简短回答。使用可能性语气。📜"
    ai_resp = call_llm(system, prompt, temperature=0.5, max_tokens=1200)
    
    return {
        "success": True,
        "year": target_year,
        "year_branch": year_branch,
        "decadal": decadal_info,
        "overlay": overlay,
        "palace": palace.name_cn if palace else "未知",
        "analysis": ai_resp,
    }


# ============================================================
# 完整AI解盘 + 流年整合
# ============================================================

def full_reading_with_yearly(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    target_year: Optional[int] = None,
) -> Dict[str, Any]:
    """
    完整命盘解盘 + 流年运势（一次生成）
    """
    from datetime import datetime
    if not target_year:
        target_year = datetime.now().year
    
    # 主命盘解盘
    main = ai_reading(date_str, hour, gender, is_lunar, target_year)
    if not main["success"]:
        return main
    
    # 流年运势
    yearly = generate_yearly_reading(date_str, hour, gender, target_year)
    
    # 合并Telegram文本
    tg_text = main.get("telegram_text", "")
    
    if yearly.get("success"):
        tg_text += "\n\n"
        tg_text += f"━━━ 📅 {target_year}年流年运势 ━━━\n\n"
        tg_text += yearly.get("analysis", "")
        tg_text += "\n\n📜 文化参考，理性看待"
    
    # 流月（当前月）
    current_month = datetime.now().month
    monthly = generate_monthly_reading(date_str, hour, gender, target_year, current_month)
    
    if monthly.get("success"):
        tg_text += f"\n\n━━━ 🌙 {target_year}年{current_month}月流月 ━━━\n\n"
        tg_text += f"宫位：{monthly['palace_name']}（地支{monthly['month_branch']}）\n"
        tg_text += f"星曜：{monthly['major_stars']}\n"
        tg_text += monthly.get("analysis", "")
        tg_text += "\n\n📜 文化参考，理性看待"
    
    return {
        "success": True,
        "telegram_text": tg_text,
        "chart_json": main.get("chart_json"),
        "yearly": yearly,
        "monthly": monthly,
    }


if __name__ == "__main__":
    import sys
    
    date = sys.argv[1] if len(sys.argv) > 1 else "1984-6-22"
    hour = int(sys.argv[2]) if len(sys.argv) > 2 else 6
    gender = sys.argv[3] if len(sys.argv) > 3 else "男"
    
    print(f"🔮 南曦命盘完整解盘 + 流年流月")
    print(f"=" * 40)
    
    result = full_reading_with_yearly(date, hour, gender)
    
    if result["success"]:
        print(result["telegram_text"])
    else:
        print(f"❌ {result.get('error')}")


path: scripts/ai_reader.py
content: """
ai_reader.py — 一念紫微斗数 AI解盘引擎（旧版兼容适配器）
主要流程委托给 ai_engine / deep_reading / prompts_v2

Author: 崽儿虾 🦞
"""

from typing import Optional, Dict, Any, List
from zwds_calc import (
    AstrolabeResult, generate_astrolabe, 
    astrolabe_to_json, format_astrolabe,
    compute_surrounded
)
from deep_reading import full_deep_reading, layer1_basic_reading
from ai_engine import ai_reading


def read_astrolabe(astro_data, school: str = "综合") -> str:
    """使用AI解读命盘，返回结构化提示词（旧版兼容）"""
    if isinstance(astro_data, dict):
        chart_json = astro_data
    else:
        chart_json = astrolabe_to_json(astro_data)
    
    deep = full_deep_reading(
        chart_json.get("birth_date", "1984-6-22"),
        chart_json.get("birth_hour", 6),
        chart_json.get("gender", "男"),
    )
    
    if deep["success"]:
        return deep["ai_prompt"]
    
    # 降级：基础prompt
    palaces_str = ""
    for p in chart_json.get("palaces", []):
        major = " ".join(
            f"{s['name']}({s['brightness']}{'→'+str(s.get('mutagen','')) if s.get('mutagen') else ''})"
            for s in p.get("major_stars", [])
        )
        minor = " ".join(s["name"] for s in p.get("minor_stars", []))
        palaces_str += f"  {p['name_cn']}({p['heavenly_stem']}{p['earthly_branch']}): {major} {'附:'+minor if minor else ''}\n"

    mutagens_str = "\n".join(
        f"  {m['palace']}: {m['star']}化{m['mutagen']}"
        for m in chart_json.get("mutagens", [])
    )

    return f"""请以{school}视角解读以下命盘：

【四柱】
{chart_json.get('four_pillars', {}).get('year','')} {chart_json.get('four_pillars', {}).get('month','')} {chart_json.get('four_pillars', {}).get('day','')} {chart_json.get('four_pillars', {}).get('hour','')}
生肖: {chart_json.get('zodiac','')} 五行局: {chart_json.get('five_elements','')}

【生年四化】
{mutagens_str}

【十二宫】
{palaces_str}

请逐宫解读。📜"""


def create_chart_and_reading(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    school: str = "综合",
    language: str = "zh-CN",
) -> dict:
    """排盘 + AI解读（旧版兼容）"""
    astro = generate_astrolabe(date_str, hour, gender, is_lunar)
    if not astro:
        return {"success": False, "error": "排盘失败"}
    
    chart_json = astrolabe_to_json(astro)
    chart_text = format_astrolabe(astro)
    
    prompt = read_astrolabe(chart_json, school)
    
    return {
        "success": True,
        "chart_json": chart_json,
        "chart_text": chart_text,
        "prompt": prompt,
    }


path: scripts/ai_reader_v2.py
content: """
ai_reader_v2.py — 一念紫微斗数 AI解盘引擎 v2
多模型支持（Claude/GPT/DeepSeek），输出标准JSON解盘报告

Author: 崽儿虾 🦞
"""

from typing import Optional, Dict, Any, List, Tuple
import json
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from zwds_calc import generate_astrolabe, astrolabe_to_json
from deep_reading import full_deep_reading
from prompts import (
    build_full_deep_prompt,
    adapt_prompt_for_model,
    SYSTEM_SANHE, SYSTEM_FEIXING, SYSTEM_ZHANYAN,
)


# ============================================================
# AI模型调用层（适配OpenClaw框架）
# ============================================================

def call_ai_model(
    system_prompt: str,
    user_prompt: str,
    model_type: str = "auto",
    temperature: float = 0.5,
) -> str:
    """
    调用AI模型进行解盘
    
    在OpenClaw框架内运行时，由框架处理实际API调用。
    此处返回格式化后的prompt供框架读取。
    
    Args:
        model_type: "auto"（框架自动选择）| "claude" | "gpt" | "deepseek"
        temperature: 0.0-1.0，越低越保守
    """
    # 组合成完整请求
    full_prompt = f"{system_prompt}\n\n---\n\n{user_prompt}"
    
    if model_type == "deepseek":
        full_prompt = adapt_prompt_for_model(full_prompt, "deepseek")
    
    return full_prompt


def _clean_json_response(raw: str) -> str:
    """清理模型返回的JSON（去除多余markdown）"""
    raw = raw.strip()
    # 去除 ```json ``` 包裹
    if raw.startswith("```"):
        lines = raw.split("\n")
        content = []
        started = False
        for line in lines:
            if line.strip().startswith("```"):
                started = not started
                continue
            if started:
                content.append(line)
        if content:
            return "\n".join(content)
    return raw


# ============================================================
# 解盘报告解析器
# ============================================================

def parse_reading_report(raw_output: str) -> Dict[str, Any]:
    """解析模型返回的解盘文本为结构化数据"""
    cleaned = _clean_json_response(raw_output)
    
    # 尝试JSON解析
    try:
        report = json.loads(cleaned)
        return report
    except json.JSONDecodeError:
        pass
    
    # 如果不是标准JSON，按文本分段解析
    return _parse_text_report(cleaned)


def _parse_text_report(text: str) -> Dict[str, Any]:
    """解析自由文本格式的解盘报告"""
    sections = {}
    current_section = "intro"
    current_text = []
    
    for line in text.split("\n"):
        # 检测JSON结构
        if '"name"' in line and '"interpretation"' in text:
            # 尝试JSON解析所有{}
            try:
                start = text.find("{")
                end = text.rfind("}") + 1
                if start >= 0 and end > start:
                    return json.loads(text[start:end])
            except:
                pass
        
        # 按层解析
        for layer in ["第一层", "第二层", "第三层", "第四层", "basic", "advanced", "timing", "professional", "conclusion"]:
            if layer in line.lower() and ("【" in line or ":" in line or "Layer" in line):
                if current_text:
                    sections[current_section] = "\n".join(current_text)
                current_section = layer
                current_text = []
                break
        else:
            current_text.append(line)
    
    if current_text:
        sections[current_section] = "\n".join(current_text)
    
    return {"report": {"layers": sections}}


# ============================================================
# 标准化解盘报告生成
# ============================================================

def generate_standard_report(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    target_year: Optional[int] = None,
    model_type: str = "auto",
) -> Dict[str, Any]:
    """
    全流程：排盘 → 深度数据 → 构建Prompt → 调用AI → 输出JSON报告
    
    Returns:
        {
            "success": bool,
            "chart_json": {...},           # 命盘JSON
            "ai_prompt": "...",            # 发送给AI的Prompt
            "report": {...},               # AI返回的解盘JSON报告
            "report_text": "...",          # 纯文本版本
            "model_type": "claude",
        }
    """
    from datetime import datetime
    
    if not target_year:
        target_year = datetime.now().year
    
    # 1. 排盘 + 深度数据
    astro = generate_astrolabe(date_str, hour, gender, is_lunar)
    if not astro:
        return {"success": False, "error": "排盘失败"}
    
    deep = full_deep_reading(date_str, hour, gender, is_lunar, target_year)
    if not deep["success"]:
        return {"success": False, "error": "深度分析失败"}
    
    chart_data = astrolabe_to_json(astro)
    
    # 2. 构建Prompt
    school = "综合"
    system_prompt = SYSTEM_CORE
    
    prompt = build_full_deep_prompt(chart_data, deep, target_year)
    full_prompt = call_ai_model(system_prompt, prompt, model_type)
    
    return {
        "success": True,
        "chart_json": chart_data,
        "deep_data": deep,
        "ai_prompt": full_prompt,
        "model_type": model_type,
        "system_prompt": system_prompt,
    }


# ============================================================
# 解盘结果格式化（Telegram友好）
# ============================================================

def format_report_for_tg(report: Dict[str, Any]) -> str:
    """把解盘报告格式化为Telegram可读文本"""
    lines = []
    
    # 元数据
    meta = report.get("report", {}).get("metadata", {})
    if meta:
        lines.append(f"🔮 *一念紫微斗数 · 深度解盘*")
        lines.append(f"📅 四柱：{meta.get('four_pillars', '')}")
        lines.append(f"🐉 大限：{meta.get('current_decadal', '')}")
        lines.append("")
    
    # 各层解读
    layers = report.get("report", {}).get("layers", {})
    
    if "basic" in layers:
        basic = layers["basic"]
        lines.append("*🥇 基础宫位解读*")
        if isinstance(basic, dict):
            lines.append(basic.get("summary", ""))
            for p in basic.get("palaces", []):
                name = p.get("name", "")
                interp = p.get("interpretation", "")
                if name and interp:
                    lines.append(f"▪ {name}：{interp[:200]}")
        elif isinstance(basic, str):
            lines.append(basic[:500])
        lines.append("")
    
    if "advanced" in layers:
        adv = layers["advanced"]
        lines.append("*🥈 四化格局分析*")
        if isinstance(adv, dict):
            lines.append(adv.get("mutagen_analysis", ""))
        elif isinstance(adv, str):
            lines.append(adv[:500])
        lines.append("")
    
    if "timing" in layers:
        timing = layers["timing"]
        lines.append("*🥉 大限流年*")
        if isinstance(timing, dict):
            lines.append(timing.get("current_decadal", ""))
        elif isinstance(timing, str):
            lines.append(timing[:500])
        lines.append("")
    
    if "professional" in layers:
        prof = layers["professional"]
        lines.append("*🏆 专业深度*")
        if isinstance(prof, dict):
            lines.append(prof.get("laiyin_analysis", ""))
        elif isinstance(prof, str):
            lines.append(prof[:500])
        lines.append("")
    
    # 结论
    conclusion = report.get("report", {}).get("conclusion", "")
    if conclusion:
        lines.append("*📝 综合论断*")
        lines.append(conclusion[:300] if len(str(conclusion)) > 300 else str(conclusion))
        lines.append("")
    
    lines.append("━━━━━━━━━━━━━━")
    lines.append("📜 *文化参考，理性看待*")
    
    return "\n".join(lines)


# 系统核心提示词（供外部调用）
SYSTEM_CORE = """你是一位精通紫微斗数的顶尖命理师，擅长三派合一综合解读。

【核心能力】
1. 三合派（中州派）：星曜性质 × 亮度 × 三方四正
2. 飞星派（钦天门）：宫干四化脉络 × 自化 × 追禄追忌
3. 占验派（紫云）：太岁入卦 × 特殊格局 × 星曜互涉

【输出要求】
- 严格按照JSON格式输出报告
- 每层用清晰结构，包含summary和详情
- 专业术语搭配白话解释
- 使用可能性语气，禁止绝对化断言
- 结语标注：📜 文化参考，理性看待"""


if __name__ == "__main__":
    import sys
    
    date = sys.argv[1] if len(sys.argv) > 1 else "1984-6-22"
    hour = int(sys.argv[2]) if len(sys.argv) > 2 else 6
    gender = sys.argv[3] if len(sys.argv) > 3 else "男"
    
    result = generate_standard_report(date, hour, gender)
    
    if result["success"]:
        print(f"✅ 解盘准备就绪")
        print(f"📊 Prompt长度: {len(result['ai_prompt'])} 字")
        print(f"🤖 模型: {result['model_type']}")
        print()
        print("🔮 系统提示词（前300字）:")
        print(result["system_prompt"][:300])
        print()
        print("━" * 30)
        print("📝 用户Prompt（前1000字）:")
        print(result["ai_prompt"][:1000])
    else:
        print(f"❌ {result.get('error')}")


path: scripts/api_app.py
content: """
yinian_zwds_api.py — 一念紫微斗数 FastAPI 后端
四层深度解盘 + RESTful API + 多语言出海

Author: 崽儿虾 🦞
"""

import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "skill", "scripts"))

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional
import uvicorn

from ai_reader_v2 import generate_standard_report
from formats import STAR_ENCYCLOPEDIA, get_star_info

app = FastAPI(
    title="一念紫微斗数 API v3 (Yinian ZWDS)",
    description="四层深度AI紫微斗数排盘解盘系统（基础→进阶→高阶→专业）｜多模型支持",
    version="3.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class BirthInput(BaseModel):
    date: str = Field(..., description="出生日期，如 2000-8-16")
    hour: int = Field(..., ge=0, le=23, description="出生时辰，0-23")
    gender: str = Field(..., description="性别：男/女/male/female")
    is_lunar: bool = False
    is_lunar_leap: bool = False
    language: str = "zh-CN"
    school: str = "综合"
    depth: str = "deep"
    target_year: Optional[int] = None
    model_type: str = "auto"  # auto / claude / gpt / deepseek


@app.get("/")
def root():
    return {
        "name": "一念紫微斗数 API v2",
        "version": "2.0.0",
        "schools": ["三合", "飞星", "占验", "综合"],
        "features": [
            "基础层：12宫 + 主辅星 + 空宫",
            "进阶层：四化飞星 + 格局识别 + 六亲联动",
            "高阶层：大限流年 + 叠宫",
            "专业层：来因宫 + 体用宫 + 星曜互涉",
        ],
        "endpoints": {
            "reading": "POST /api/reading",
            "help": "/help",
        }
    }


@app.post("/api/reading")
def get_reading(input: BirthInput):
    """排盘 + 四层AI深度解盘 + 多模型支持"""
    gender_mapped = "男" if input.gender.lower() in ["男", "male", "m"] else "女"

    result = generate_standard_report(
        date_str=input.date,
        hour=input.hour,
        gender=gender_mapped,
        is_lunar=input.is_lunar,
        target_year=input.target_year,
        model_type=input.model_type,
    )

    if not result["success"]:
        raise HTTPException(status_code=400, detail=result.get("error", "解盘失败"))

    return {
        "success": True,
        "chart_json": result["chart_json"],
        "ai_prompt": result["ai_prompt"],
        "system_prompt": result["system_prompt"],
        "model_type": result["model_type"],
        "stats": {
            "prompt_length": len(result["ai_prompt"]),
        },
    }


@app.get("/api/star/{star_name}")
def get_star(star_name: str):
    info = get_star_info(star_name)
    if info:
        return {"name": star_name, "description": info}
    return {"error": f"未知星曜: {star_name}"}


@app.get("/help")
def api_help():
    return {
        "version": "3.0.0",
        "features": {
            "basic": "逐宫解读（主星+辅星+亮度+空宫+三方四正）",
            "advanced": "四化飞星 + 格局识别 + 六亲联动",
            "timing": "大限十年运 + 流年",
            "professional": "来因宫 + 体用宫 + 煞星分布 + 三派综合",
        },
        "models": ["auto", "claude", "gpt", "deepseek"],
        "schema": {
            "date": "出生日期 YYYY-MM-DD",
            "hour": "时辰 0-23",
            "gender": "男/女/male/female",
            "model_type": "auto/claude/gpt/deepseek",
            "target_year": "可选，目标年份",
        },
        "example": {
            "date": "1984-6-22",
            "hour": 6,
            "gender": "男",
            "model_type": "auto",
        }
    }


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8008)


path: scripts/daily_push.py
content: """
daily_push.py — 一念紫微斗数 日运势推送模块
支持每日/每周运势推送，计划用于TG Bot定时发送

Author: 崽儿虾 🦞
"""

from typing import Optional, Dict, Any
from datetime import datetime
from zwds_calc import generate_astrolabe, BRANCH_TO_INDEX
from deep_reading import full_deep_reading
from decadal import calculate_decadal_sequence


# ============================================================
# 日运势（基于流日地支）
# ============================================================

def generate_daily_fortune(
    date_str: str,
    hour: int,
    gender: str,
    target_date: Optional[str] = None,
) -> Dict[str, Any]:
    """
    日运势分析
    
    基于流日地支对应的宫位 + 时辰引动
    
    Args:
        target_date: "YYYY-MM-DD" 格式，默认为今天
    """
    if not target_date:
        target_date = datetime.now().strftime("%Y-%m-%d")
    
    dt = datetime.strptime(target_date, "%Y-%m-%d")
    year = dt.year
    month = dt.month
    day = dt.day
    hour_now = dt.hour
    
    astro = generate_astrolabe(date_str, hour, gender)
    if not astro:
        return {"success": False, "error": "排盘失败"}
    
    deep = full_deep_reading(date_str, hour, gender, target_year=year)
    timing = deep.get("timing", {})
    
    # 流日地支（按60天周期简化）
    day_offset = (dt - datetime(1984, 1, 1)).days
    branch_idx = day_offset % 12
    day_branch_list = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]
    day_branch = day_branch_list[branch_idx]
    
    # 流日对应标准宫序
    soul_branch = astro.soul_palace_branch or "子"
    soul_iztro_idx = BRANCH_TO_INDEX.get(soul_branch, 10)
    day_iztro_idx = BRANCH_TO_INDEX.get(day_branch, 0)
    palace_idx = (day_iztro_idx - soul_iztro_idx) % 12
    
    palace = astro.palaces[palace_idx] if 0 <= palace_idx < len(astro.palaces) else None
    
    if not palace:
        return {"success": False, "error": "找不到对应宫位"}
    
    majors = [f"{s.name}({s.brightness})" + (f"→{s.mutagen}" if s.mutagen else "") for s in palace.major_stars]
    minors = [s.name for s in palace.minor_stars]
    
    mstr = "、".join(majors) if majors else "空宫"
    mistr = "、".join(minors) if minors else "无辅星"
    
    # 当前时辰引动
    hour_branch_list = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]
    hour_branch_idx = hour_now // 2 % 12
    hour_branch = hour_branch_list[hour_branch_idx]
    
    d = timing.get("current_decadal", {})
    decadal_info = f"{d['palace_name']}（{d['age_start']}-{d['age_end']}岁）" if d else "未知"
    
    prompt = f"""请简析今日运势（紫微斗数）：

日期：{target_date}
流日地支：{day_branch}
当前时辰：{hour_branch}
当前大限：{decadal_info}

今日宫位：{palace.name_cn}
主星：{mstr}
辅星：{mistr}
{"（空宫借对宫星曜）" if palace.is_empty else ""}

请给出（每条30字以内）：
1. 今日主题
2. 注意事项
3. 有利方向
"""
    
    return {
        "success": True,
        "date": target_date,
        "day_branch": day_branch,
        "hour_branch": hour_branch,
        "palace_name": palace.name_cn,
        "major_stars": mstr,
        "minor_stars": mistr,
        "is_empty": palace.is_empty,
        "ai_prompt": prompt,
        "decadal": d["palace_name"] if d else "",
    }


# ============================================================
# 本周运势
# ============================================================

def generate_weekly_fortune(
    date_str: str,
    hour: int,
    gender: str,
    week_start: Optional[str] = None,
) -> Dict[str, Any]:
    """
    本周运势（7天）
    """
    if not week_start:
        from datetime import timedelta
        today = datetime.now()
        week_start = (today - timedelta(days=today.weekday())).strftime("%Y-%m-%d")
    
    ws = datetime.strptime(week_start, "%Y-%m-%d")
    
    days = []
    for i in range(7):
        d = ws.strftime("%Y-%m-%d")
        day_result = generate_daily_fortune(date_str, hour, gender, d)
        if day_result.get("success"):
            days.append(day_result)
        ws = datetime.fromordinal(ws.toordinal() + 1)
    
    return {
        "success": True,
        "week_start": week_start,
        "days": days,
    }


# ============================================================
# Telegram格式化
# ============================================================

def format_daily_for_tg(result: Dict) -> str:
    """日运势格式化为Telegram"""
    if not result["success"]:
        return f"❌ {result.get('error')}"
    
    lines = []
    lines.append(f"☀️ *{result['date']} · 日运势*")
    lines.append("")
    lines.append(f"📍 宫位：{result['palace_name']}（地支{result['day_branch']}）")
    lines.append(f"⭐ 星曜：{result['major_stars']}")
    lines.append("")
    lines.append("💡 " + result.get("ai_prompt", "").split("请给出")[0].strip())
    lines.append("")
    lines.append("📜 文化参考，理性看待")
    
    return "\n".join(lines)


if __name__ == "__main__":
    print("=== 今日运势 ===")
    r = generate_daily_fortune("1984-6-22", 6, "男")
    if r["success"]:
        print(format_daily_for_tg(r))
        print()
        print("AI Prompt:")
        print(r["ai_prompt"])


path: scripts/decadal.py
content: """
decadal.py — 大限（十年大运）推算模块

五行局定起运年龄：
水二局 = 2岁起运
木三局 = 3岁起运
金四局 = 4岁起运
土五局 = 5岁起运
火六局 = 6岁起运

顺逆：阳男阴女顺行，阴男阳女逆行

Author: 崽儿虾 🦞
"""

from typing import List, Dict, Any, Optional, Tuple

# 12地支索引
BRANCH_INDEX = {
    "子": 0, "丑": 1, "寅": 2, "卯": 3,
    "辰": 4, "巳": 5, "午": 6, "未": 7,
    "申": 8, "酉": 9, "戌": 10, "亥": 11,
}

INDEX_BRANCH = {v: k for k, v in BRANCH_INDEX.items()}

# 天干阴阳（阳=0, 阴=1）
STEM_YIN_YANG = {
    "甲": 0, "乙": 1, "丙": 0, "丁": 1, "戊": 0,
    "己": 1, "庚": 0, "辛": 1, "壬": 0, "癸": 1,
}

# 五行局 → 起运年龄
FIVE_ELEMENTS_START_AGE = {
    "水二局": 2, "木三局": 3, "金四局": 4,
    "土五局": 5, "火六局": 6,
}

PALACE_NAMES_CN = [
    "命宫", "父母宫", "福德宫", "田宅宫",
    "官禄宫", "交友宫", "迁移宫", "疾厄宫",
    "财帛宫", "子女宫", "夫妻宫", "兄弟宫",
]

# 生年天干四化表
NATAL_MUTAGEN = {
    "甲": ("廉贞", "破军", "武曲", "太阳"),
    "乙": ("天机", "天梁", "紫微", "太阴"),
    "丙": ("天同", "天机", "文昌", "廉贞"),
    "丁": ("太阴", "天同", "天机", "巨门"),
    "戊": ("贪狼", "太阴", "右弼", "天机"),
    "己": ("武曲", "贪狼", "天梁", "文曲"),
    "庚": ("太阳", "武曲", "太阴", "天同"),
    "辛": ("巨门", "太阳", "文曲", "文昌"),
    "壬": ("天梁", "紫微", "左辅", "武曲"),
    "癸": ("破军", "巨门", "太阴", "贪狼"),
}


def calculate_start_age(five_elements: str) -> int:
    """根据五行局计算起运年龄"""
    return FIVE_ELEMENTS_START_AGE.get(five_elements, 4)


def calculate_decadal_sequence(
    soul_branch_index: int,
    year_stem: str,
    gender: str,
    five_elements: str,
    current_age: Optional[int] = None,
) -> List[Dict[str, Any]]:
    """
    推算全部大限序列
    
    Args:
        soul_branch_index: 命宫地支索引 (0-11)
        year_stem: 年干（如"甲"）
        gender: "男"或"女"
        five_elements: 五行局（如"水二局"）
        current_age: 当前年龄（可选的，用于标注当前大限）
    
    Returns:
        [
            {
                "age_start": 2,
                "age_end": 11,
                "palace_index": 0,
                "palace_name": "命宫",
                "heavenly_stem": "庚",
                "earthly_branch": "午",
                "is_current": True/False,
                "mutagen": [...],
            },
            ...
        ]
    """
    start_age = calculate_start_age(five_elements)
    
    # 阳男/阴女 = 顺行，阴男/阳女 = 逆行
    is_yang = STEM_YIN_YANG.get(year_stem, 0) == 0
    is_male = gender == "男"
    is_forward = (is_yang and is_male) or (not is_yang and not is_male)
    
    step = 1 if is_forward else -1
    
    # 水2 木3 金4 土5 火6 → 每个大限步数年数
    step_years = 10  # 固定每大限10年（主流中州派标准）
    
    # 从命宫开始
    seq = []
    palace_idx = soul_branch_index
    
    for i in range(12):
        age_start = start_age + (i * step_years)
        age_end = age_start + step_years - 1
        
        is_current = False
        if current_age is not None:
            is_current = age_start <= current_age <= age_end
        
        seq.append({
            "age_start": age_start,
            "age_end": age_end,
            "palace_index": palace_idx,
            "palace_name": PALACE_NAMES_CN[palace_idx],
            "heavenly_stem": "",
            "earthly_branch": INDEX_BRANCH[palace_idx],
            "is_current": is_current,
        })
        
        palace_idx = (palace_idx + step) % 12
    
    return seq


def get_decadal_stem_branch(
    seq: List[Dict],
    year_stem: str,
    year_branch: str,
    gender: str,
) -> List[Dict]:
    """
    给大限序列赋天干地支
    
    大限天干 = 命宫天干（以五虎遁定）+ 顺/逆排
    """
    is_yang = STEM_YIN_YANG.get(year_stem, 0) == 0
    is_male = gender == "男"
    is_forward = (is_yang and is_male) or (not is_yang and not is_male)
    
    # 第一个大限的干支 = 命宫干支
    # 支是命宫地支，干是命宫天干（需从已知数据获取）
    # 天干递增/递减（顺行加1，逆行减1），地支也同方向
    
    for item in seq:
        pass  # 干支由外部传入
    
    return seq


def format_decadal_table(seq: List[Dict]) -> str:
    """格式化大限表"""
    lines = []
    lines.append("📅 *十年大运表*")
    lines.append("")
    
    for item in seq:
        marker = "👉 " if item.get("is_current") else "   "
        range_str = f"{item['age_start']}-{item['age_end']}岁"
        palace = item["palace_name"]
        
        lines.append(f"{marker}{range_str}  →  {palace}")
    
    lines.append("")
    lines.append("📜 *文化参考，理性看待*")
    
    return "\n".join(lines)


if __name__ == "__main__":
    # 南曦测试（1984甲子年，水二局，命宫午=6，阳男顺行）
    seq = calculate_decadal_sequence(6, "甲", "男", "水二局")
    print(format_decadal_table(seq))
    print()
    for s in seq:
        print(f"  {s['age_start']}-{s['age_end']}岁: {s['palace_name']} {'👈 当前' if s['is_current'] else ''}")


path: scripts/deep_reading.py
content: """
deep_reading.py — 一念紫微斗数 深度解盘引擎（四层架构）
基础→进阶→高阶→专业，AI驱动的多视角分析

Author: 崽儿虾 🦞
"""

from typing import List, Dict, Any, Optional, Tuple
from zwds_calc import (
    AstrolabeResult, StarInfo, PalaceInfo,
    generate_astrolabe, astrolabe_to_json,
    compute_surrounded, BRANCH_TO_INDEX,
    ZWDS_PALACE_NAMES, _get_palace_name_by_branch,
    _find_star_in_palaces, _map_mutagen_name,
    _stem_value, _branch_value,
)
from decadal import calculate_decadal_sequence
import json
import math


# ============================================================
# 第一层：基础解读（12宫 + 主辅星 + 亮度 + 空宫）
# ============================================================

def layer1_basic_reading(astro: AstrolabeResult) -> Dict[str, Any]:
    """
    基础解读：逐宫分析主星、辅星、亮度、空宫状态
    
    返回结构化数据供AI读取
    """
    result = {
        "title": "第一层：基础宫位解读",
        "four_pillars": {
            "year": astro.four_pillars_year,
            "month": astro.four_pillars_month,
            "day": astro.four_pillars_day,
            "hour": astro.four_pillars_hour,
        },
        "zodiac": astro.zodiac,
        "five_elements": astro.five_elements_class,
        "palaces": [],
        "mutagens": [],
    }

    for p in astro.palaces:
        palace_data = {
            "index": p.index,
            "name": p.name_cn,
            "heavenly_stem": p.heavenly_stem,
            "earthly_branch": p.earthly_branch,
            "is_empty": p.is_empty,
            "major_stars": [],
            "minor_stars": [],
        }
        for s in p.major_stars:
            star_info = {"name": s.name, "brightness": s.brightness}
            if s.mutagen:
                star_info["mutagen"] = s.mutagen
            palace_data["major_stars"].append(star_info)
        for s in p.minor_stars:
            star_info = {"name": s.name}
            if s.mutagen:
                star_info["mutagen"] = s.mutagen
            palace_data["minor_stars"].append(star_info)
        result["palaces"].append(palace_data)

    # 生年四化
    for m in astro.mutagens:
        result["mutagens"].append({
            "palace": m["palace"],
            "star": m["star"],
            "mutagen": m["mutagen"],
        })

    # 三方四正
    sur = compute_surrounded(astro.soul_index)
    result["soul_surrounded"] = [astro.palaces[i].name_cn for i in sur]

    return result


# ============================================================
# 第二层：进阶解读（四化飞星 + 格局 + 六亲联动）
# ============================================================

def layer2_advanced_reading(astro: AstrolabeResult) -> Dict[str, Any]:
    """
    进阶解读：
    1. 生年四化深入（禄权科忌在各宫含义）
    2. 宫干飞四化初步（派系差异）
    3. 特殊格局识别
    4. 六亲宫联动
    """
    result = {
        "title": "第二层：四化飞星与格局",
        "mutagen_analysis": [],
        "patterns": [],
        "six_relations": [],
    }

    # 四化分析
    for m in astro.mutagens:
        analysis = _analyze_mutagen(m["star"], m["mutagen"], m["palace"])
        result["mutagen_analysis"].append(analysis)

    # 特殊格局检测
    patterns = _detect_patterns(astro)
    result["patterns"] = patterns

    # 六亲宫联动
    six_relations = _six_relations_analysis(astro)
    result["six_relations"] = six_relations

    return result


def _analyze_mutagen(star: str, mutagen: str, palace: str) -> Dict[str, str]:
    """分析四化含义"""
    meanings = {
        "禄": ("增加、融合、顺遂", "该宫主题和谐顺畅，资源丰沛"),
        "权": ("主导、掌控、权威", "该宫主题主控性强，有担当但有压力"),
        "科": ("文雅、名声、调和", "该宫主题有文化气息，名声佳"),
        "忌": ("缺失、纠葛、收缩", "该宫主题有缺憾，需格外注意"),
    }
    desc, detail = meanings.get(mutagen, ("", ""))
    return {
        "star": star,
        "mutagen": mutagen,
        "meaning": desc,
        "palace": palace,
        "detail": detail,
    }


def _detect_patterns(astro: AstrolabeResult) -> List[Dict[str, str]]:
    """识别特殊格局"""
    patterns = []

    # 获取所有宫的主星列表
    palace_stars = {}
    for p in astro.palaces:
        palace_stars[p.index] = [s.name for s in p.major_stars]

    # 紫府相同宫（紫微+天府+天相）
    for idx, stars in palace_stars.items():
        if "紫微" in stars and "天府" in stars:
            patterns.append({
                "name": "紫府同宫",
                "palace": astro.palaces[idx].name_cn,
                "description": "紫微帝星与天府库星同宫，格局稳重，有领导力和守成之能",
            })

    # 杀破狼（七杀+破军+贪狼在命迁财官）
    has_sha = any("七杀" in s for s in palace_stars.values())
    has_po = any("破军" in s for s in palace_stars.values())
    has_tan = any("贪狼" in s for s in palace_stars.values())
    if has_sha and has_po and has_tan:
        patterns.append({
            "name": "杀破狼",
            "palace": "命宫/迁移宫/财帛宫/官禄宫",
            "description": "三颗变动脉冲星齐聚，一生变动大、机遇多，开创型人格",
        })

    # 机月同梁（天机+太阴+天同+天梁）
    has_ji = any("天机" in s for s in palace_stars.values())
    has_yue = any("太阴" in s for s in palace_stars.values())
    has_tong = any("天同" in s for s in palace_stars.values())
    has_liang = any("天梁" in s for s in palace_stars.values())
    if has_ji and has_yue and has_tong and has_liang:
        patterns.append({
            "name": "机月同梁",
            "palace": "命宫/财帛宫/官禄宫",
            "description": "才艺文星组合，适合稳定型、技术型或文化艺术工作",
        })

    # 日月同宫
    for idx, stars in palace_stars.items():
        if "太阳" in stars and "太阴" in stars:
            patterns.append({
                "name": "日月同宫",
                "palace": astro.palaces[idx].name_cn,
                "description": "阴阳调和，为人既有原则又懂变通，贵气加身",
            })

    return patterns


def _six_relations_analysis(astro: AstrolabeResult) -> List[Dict[str, Any]]:
    """六亲宫联动分析（命宫+父母/兄弟/夫妻/子女/交友）"""
    relations = []
    palaces = astro.palaces

    # 六亲宫
    relation_palaces = [
        (1, "父母"), (11, "兄弟"), (10, "夫妻"),
        (9, "子女"), (5, "交友"),
    ]

    for idx, rel_name in relation_palaces:
        p = palaces[idx]
        major_stars = [s.name for s in p.major_stars]
        minor_stars = [s.name for s in p.minor_stars]
        empty = p.is_empty

        # 联动：对宫+三合
        sur = compute_surrounded(idx)
        sur_names = [palaces[i].name_cn for i in sur]

        relations.append({
            "relation": f"{rel_name}宫",
            "palace": p.name_cn,
            "heavenly_stem": p.heavenly_stem,
            "earthly_branch": p.earthly_branch,
            "is_empty": empty,
            "major_stars": major_stars,
            "minor_stars": minor_stars,
            "three_combined": sur_names,
        })

    return relations


# ============================================================
# 第三层：高阶解读（大限 + 流年/月/日 + 叠宫）
# ============================================================

def layer3_advanced_timing(
    astro: AstrolabeResult,
    target_year: Optional[int] = None,
) -> Dict[str, Any]:
    """
    高阶解读：时间维度运势
    1. 大限（十年大运）
    2. 流年运势
    3. 叠宫分析（大限宫位×流年宫位）
    """
    from datetime import datetime

    current_year = datetime.now().year
    birth_year = int(astro.birth_date.split("-")[0]) if astro.birth_date else current_year
    age = current_year - birth_year

    if target_year:
        age = target_year - birth_year

    year_stem = astro.four_pillars_year[0] if astro.four_pillars_year else "甲"
    gender = astro.gender

    # 大限从命宫开始（命宫在标准序中始终为索引0）
    decadal_seq = calculate_decadal_sequence(
        soul_branch_index=0,
        year_stem=year_stem,
        gender=gender,
        five_elements=astro.five_elements_class,
        current_age=age,
    )

    # 找当前大限
    current_decadal = None
    for d in decadal_seq:
        if d["is_current"]:
            current_decadal = d
            break

    # 流年宫位推断（粗略）
    # 流年地支 = (当前年份 - 1984) % 12 的偏移
    year_offset = (current_year - 1984) % 12
    yearly_branch_list = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]
    yearly_branch = yearly_branch_list[year_offset]

    # 当前大限宫位
    current_palace = None
    if current_decadal:
        pi = current_decadal["palace_index"]
        current_palace = {
            "palace_name": current_decadal["palace_name"],
            "age_range": f"{current_decadal['age_start']}-{current_decadal['age_end']}岁",
            "palace_stars": _palace_stars_summary(astro, pi),
        }

    return {
        "title": "第三层：大限与流年",
        "current_age": age,
        "current_year": current_year,
        "yearly_branch": yearly_branch,
        "decadal_sequence": decadal_seq,
        "current_decadal": current_decadal,
        "current_decadal_palace": current_palace,
    }


def _palace_stars_summary(astro: AstrolabeResult, palace_index: int) -> Dict:
    """某个宫的星曜摘要"""
    if palace_index < 0 or palace_index >= len(astro.palaces):
        return {}
    p = astro.palaces[palace_index]
    return {
        "name": p.name_cn,
        "heavenly_stem": p.heavenly_stem,
        "earthly_branch": p.earthly_branch,
        "major": [{"name": s.name, "brightness": s.brightness, "mutagen": s.mutagen} for s in p.major_stars],
        "minor": [{"name": s.name} for s in p.minor_stars],
        "is_empty": p.is_empty,
    }


# ============================================================
# 第四层：专业解读（来因宫 + 体用宫 + 星曜互涉）
# ============================================================

def layer4_professional(astro: AstrolabeResult) -> Dict[str, Any]:
    """
    专业解读：
    1. 来因宫（年干定位人生方向）
    2. 体用宫（命财官为体，其余为用）
    3. 星曜互涉（六杀/煞星关联）
    4. 三派差异分析
    """
    result = {
        "title": "第四层：专业深度解析",
        "laiyin_palace": None,
        "tiyong_palaces": None,
        "star_interactions": [],
        "three_schools": {},
    }

    # 来因宫：出生年干的天干地支定位
    year_stem = astro.four_pillars_year[0] if astro.four_pillars_year else ""
    year_branch = astro.four_pillars_year[1] if len(astro.four_pillars_year) > 1 else ""

    # 来因宫 = 年干对应的宫位（天干地支交集）
    for p in astro.palaces:
        if p.heavenly_stem == year_stem:
            result["laiyin_palace"] = {
                "year_stem": year_stem,
                "year_branch": year_branch,
                "palace": p.name_cn,
                "analysis": f"年干{year_stem}坐{p.name_cn}，人生主方向围绕{p.name_cn}主题展开",
            }
            break

    # 体用宫
    body_palaces_idx = [0, 4, 8]  # 命宫、官禄宫(4)、财帛宫(8)
    body_palaces = [astro.palaces[i] for i in body_palaces_idx]
    result["tiyong_palaces"] = {
        "body": [
            {
                "name": p.name_cn,
                "role": {
                    "命宫": "体质、本质",
                    "官禄宫": "事业体",
                    "财帛宫": "财禄体",
                }.get(p.name_cn, "体宫"),
                "stars": _palace_stars_summary(astro, p.index),
            }
            for p in body_palaces
        ],
        "usage_analysis": "命宫为体（本质），官禄宫为用（事业），财帛宫为体用之间（资源）",
    }

    # 星曜互涉（六煞 + 桃花星等）
    # 擎羊、陀罗、火星、铃星、地空、地劫
    all_shas = ["擎羊", "陀罗", "火星", "铃星", "地空", "地劫"]
    sha_locations = []
    for p in astro.palaces:
        all_stars = [s.name for s in p.major_stars + p.minor_stars]
        for sha in all_shas:
            if sha in all_stars:
                sha_locations.append({
                    "star": sha,
                    "palace": p.name_cn,
                    "index": p.index,
                })

    result["star_interactions"] = sha_locations

    # 三派分析
    result["three_schools"] = {
        "sanhe": _sanhe_analysis(astro),
        "feixing": _feixing_analysis(astro),
        "zhanyan": _zhanyan_analysis(astro),
    }

    return result


def _sanhe_analysis(astro: AstrolabeResult) -> Dict[str, Any]:
    """三合派分析要点"""
    p0 = astro.palaces[astro.soul_index]
    sur = compute_surrounded(astro.soul_index)
    return {
        "soul_stars": [s.name for s in p0.major_stars],
        "soul_tree": [astro.palaces[i].name_cn for i in sur],
        "key_combinations": _detect_patterns(astro),
    }


def _feixing_analysis(astro: AstrolabeResult) -> Dict[str, Any]:
    """飞星派分析要点"""
    # 宫干四化（简版）
    palace_stems = {}
    for p in astro.palaces:
        stem = p.heavenly_stem
        if stem and stem in _map_mutagen_name(stem):
            palace_stems[p.name_cn] = stem
    return {
        "palace_stems": palace_stems,
        "self_transformation": "通过AI进行宫干飞四化深入分析",
    }


def _zhanyan_analysis(astro: AstrolabeResult) -> Dict[str, Any]:
    """占验派分析要点"""
    return {
        "tai_sui": "太岁入卦法用于分析特定年份与人的互动",
        "special_patterns": _detect_patterns(astro),
    }


# ============================================================
# 统一入口：四层全量解读
# ============================================================

def full_deep_reading(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    target_year: Optional[int] = None,
) -> Dict[str, Any]:
    """
    四层全量深度解读

    Returns:
        {
            "success": bool,
            "basic": {...},
            "advanced": {...},
            "timing": {...},
            "professional": {...},
            "summary": {...},
        }
    """
    astro = generate_astrolabe(date_str, hour, gender, is_lunar)
    if not astro:
        return {"success": False, "error": "排盘失败"}

    basic = layer1_basic_reading(astro)
    advanced = layer2_advanced_reading(astro)
    timing = layer3_advanced_timing(astro, target_year)
    professional = layer4_professional(astro)

    # AI Prompt拼接
    ai_prompt = _build_reading_prompt(basic, advanced, timing, professional)

    return {
        "success": True,
        "basic": basic,
        "advanced": advanced,
        "timing": timing,
        "professional": professional,
        "ai_prompt": ai_prompt,
        "chart_json": astrolabe_to_json(astro),
    }


def _build_reading_prompt(
    basic: Dict,
    advanced: Dict,
    timing: Dict,
    professional: Dict,
) -> str:
    """构建AI解盘完整提示词"""
    lines = []
    lines.append("【一念紫微斗数深度解盘指令】")
    lines.append("")

    # 四柱
    fp = basic["four_pillars"]
    lines.append(f"命主：{fp['year']} {fp['month']} {fp['day']} {fp['hour']}")
    lines.append(f"生肖：{basic['zodiac']}　五行：{basic['five_elements']}")
    lines.append("")

    # 生命四化
    if basic["mutagens"]:
        lines.append("【生年四化】")
        for m in basic["mutagens"]:
            lines.append(f"  {m['palace']}：{m['star']}化{m['mutagen']}")
        lines.append("")

    # 十二宫
    lines.append("【十二宫】")
    for p in basic["palaces"]:
        ms = " ".join(
            f"{s['name']}({s['brightness']})" + (f"→{s['mutagen']}" if s.get('mutagen') else "")
            for s in p["major_stars"]
        )
        mins = " ".join(s["name"] for s in p["minor_stars"])
        empty = "【空宫】" if p["is_empty"] else ""
        line_parts = [f"  {p['name']}：{ms or empty}"]
        if mins:
            line_parts.append(f"〔{mins}〕")
        lines.append("".join(line_parts))
    lines.append("")

    # 格局
    if advanced["patterns"]:
        lines.append("【特殊格局】")
        for pt in advanced["patterns"]:
            lines.append(f"  · {pt['name']}（{pt['palace']}）：{pt['description']}")
        lines.append("")

    # 六亲联动
    if advanced["six_relations"]:
        lines.append("【六亲宫联动】")
        for rel in advanced["six_relations"]:
            ms = "、".join(rel["major_stars"]) if rel["major_stars"] else "空宫"
            th = "、".join(rel["three_combined"])
            lines.append(f"  · {rel['relation']}({rel['palace']})：{ms}  联动：{th}")
        lines.append("")

    # 大限
    d = timing["current_decadal"]
    dp = timing.get("current_decadal_palace")
    if d:
        lines.append("【大限运势】")
        lines.append(f"  当前：{d['age_start']}-{d['age_end']}岁 大限在{d['palace_name']}")
        if dp:
            ms = "、".join(s["name"] for s in dp.get("major", []))
            if ms:
                lines.append(f"  大限宫主星：{ms}")
        lines.append("")
        # 大限表
        lines.append("【十年大运表】")
        for item in timing["decadal_sequence"]:
            mk = "👉" if item["is_current"] else "  "
            lines.append(f"  {mk} {item['age_start']}-{item['age_end']}岁 → {item['palace_name']}")
        lines.append("")

    # 来因宫
    if professional.get("laiyin_palace"):
        lp = professional["laiyin_palace"]
        lines.append(f"【来因宫】年干{lp['year_stem']}在{lp['palace']}，{lp['analysis']}")
        lines.append("")

    # 体用宫
    ty = professional.get("tiyong_palaces", {})
    if ty.get("body"):
        lines.append("【体用宫】")
        for b in ty["body"]:
            lines.append(f"  {b['name']}（{b['role']}）")
        lines.append(f"  {ty.get('usage_analysis', '')}")
        lines.append("")

    # 煞星分布
    if professional.get("star_interactions"):
        lines.append("【煞星分布】")
        for si in professional["star_interactions"]:
            lines.append(f"  · {si['star']}坐{si['palace']}")
        lines.append("")

    # 解盘要求
    lines.append("【解盘要求】")
    lines.append("请基于以上命盘数据，进行四层解读：")
    lines.append("1. 【基础层】逐宫详细解读：主星性质+亮度+空宫借星+辅星配合")
    lines.append("2. 【进阶层】四化脉络分析：禄权科忌在各宫的影响轨迹")
    lines.append("3. 【高阶层】大限当前宫位与流年叠宫分析")
    lines.append("4. 【专业层】用体用宫理论给出整体格局判断")
    lines.append("")
    lines.append("输出格式：每层用【】标题，语言专业但不晦涩，用比喻帮助理解。")
    lines.append("语气使用'可能性''倾向'等开放式表述，避免绝对论断。")
    lines.append("")
    lines.append("📜 文化参考，理性看待")

    return "\n".join(lines)


# ============================================================
# CLI测试
# ============================================================

if __name__ == "__main__":
    import sys

    date = sys.argv[1] if len(sys.argv) > 1 else "1984-6-22"
    hour = int(sys.argv[2]) if len(sys.argv) > 2 else 6
    gender = sys.argv[3] if len(sys.argv) > 3 else "男"

    result = full_deep_reading(date, hour, gender)
    if result["success"]:
        print("=" * 50)
        print(result["ai_prompt"])
        print("=" * 50)
        print(f"\nAI Prompt共 {len(result['ai_prompt'])} 字符")
    else:
        print(f"Error: {result.get('error')}")


path: scripts/formats.py
content: """
formats.py — 一念紫微斗数 格式化输出模块
Telegram / Web / JSON 多端输出

Author: 崽儿虾 🦞
"""

from typing import Dict, Any, Optional
from zwds_calc import AstrolabeResult


def format_chart_for_telegram(data: dict) -> str:
    """Telegram适配格式（纯文本，无表格）"""
    lines = []

    # 头部
    lines.append("🔮 *一念紫微斗数 · 命盘解讀*")
    lines.append("")

    # 命主信息
    fp = " ".join([
        data["four_pillars"]["year"],
        data["four_pillars"]["month"],
        data["four_pillars"]["day"],
        data["four_pillars"]["hour"],
    ]).strip()
    lines.append(f"📅 四柱：{fp}")
    lines.append(f"🐲 生肖：{data['zodiac']}　五行：{data['five_elements']}")

    soul = data["palaces"][data["soul_index"]]
    body = data["palaces"][data["body_index"]]
    lines.append(f"🏠 命宮：{soul['name_cn']}　身宮：{body['name_cn']}")
    lines.append("")

    # 四化
    if data["mutagens"]:
        lines.append("⚡ *生年四化*")
        for m in data["mutagens"]:
            lines.append(f"  · {m['palace']}：{m['star']}化{m['mutagen']}")
        lines.append("")

    # 十二宫
    lines.append("*📋 十二宮星盤*")
    for p in data["palaces"]:
        major = " ".join(
            f"{s['name']}({s['brightness']})" + (f"→{s['mutagen']}" if s['mutagen'] else "")
            for s in p["major_stars"]
        )
        minor = " ".join(s["name"] for s in p["minor_stars"])
        empty = "【空宮】" if p["is_empty"] else ""

        parts = [f"▪ {p['name_cn']}"]
        parts.append(f"({p['heavenly_stem']}{p['earthly_branch']})")
        parts.append(f"：{major or empty}")
        if minor:
            parts.append(f"〔{minor}〕")

        lines.append(" ".join(parts))

    # 三方四正
    if "surrounding_palaces" in data:
        lines.append("")
        lines.append("*🔄 命宮三方四正*")
        lines.append("  " + " · ".join(data["surrounding_palaces"]))

    lines.append("")
    lines.append("━━━━━━━━━━━━━━━━")
    lines.append("📜 *文化参考，理性看待*")

    return "\n".join(lines)


def format_reading_for_telegram(chart_text: str, reading_text: str) -> str:
    """命盘+解读组合输出，适合Telegram长消息"""
    return f"{chart_text}\n\n━━━ 📖 AI解讀 ━━━\n\n{reading_text}"


def format_reading_short(reading_text: str) -> str:
    """精简版解读（去掉系统提示词痕迹）"""
    # 去掉可能包含的系统指令前缀
    lines = reading_text.strip().split("\n")
    clean_lines = []
    for line in lines:
        if line.startswith("【命盘数据】") or \
           line.startswith("请以上面命盘数据为基础") or \
           line.strip().startswith("---") or \
           len(line.strip()) < 3:
            continue
        clean_lines.append(line)
    return "\n".join(clean_lines)


def build_tg_menu() -> str:
    """Telegram 帮助/菜单"""
    return """🔮 *一念紫微斗数* — 使用说明

📝 输入您的出生信息即可排盘：

`/zwds 2000-8-16 6 男`
（阳历 年-月-日 时辰(0-23) 性别）

或指定流派：
`/zwds 2000-8-16 6 男 三合`
`/zwds 2000-8-16 6 女 飞星`
`/zwds 2000-8-16 6 男 占验`
`/zwds 2000-8-16 6 男 综合`（默认）

📅 农历排盘：
`/zwds_lunar 2000-7-17 6 男`

🔍 其他指令：
`/zwds_help` — 本帮助
`/zwds_stars` — 星曜查询
`/zwds_palace 命宫` — 查询指定宫位
`/zwds_about` — 关于一念紫微

📜 *文化参考，理性看待*
"""


def format_palace_detail(palace_data: dict) -> str:
    """格式化宫位详情（供独立查询时用）"""
    lines = [
        f"🏛 *{palace_data['name_cn']}*",
        f"天干地支：{palace_data['heavenly_stem']}{palace_data['earthly_branch']}",
    ]

    if palace_data["major_stars"]:
        lines.append("")
        lines.append("*主星*")
        for s in palace_data["major_stars"]:
            parts = [f"  · {s['name']}"]
            if s["brightness"]:
                parts.append(f"({s['brightness']})")
            if s["mutagen"]:
                parts.append(f"→化{s['mutagen']}")
            lines.append(" ".join(parts))

    if palace_data["minor_stars"]:
        lines.append("")
        lines.append("*辅星*")
        for s in palace_data["minor_stars"]:
            parts = [f"  · {s['name']}"]
            if s["mutagen"]:
                parts.append(f"→化{s['mutagen']}")
            lines.append(" ".join(parts))

    if palace_data["is_empty"]:
        lines.append("")
        lines.append("⚠️ 此宫为空宫，需借对宫星曜参照")

    return "\n".join(lines)


# 星曜百科（精简版）
STAR_ENCYCLOPEDIA = {
    "紫微": "北斗帝星，尊贵之星。喜左辅右弼、天相。主贵气、领导力、体面。陷落时可能好面子、刚愎。",
    "天机": "南斗第三星，智慧之星。主聪明、策划、变动。喜天梁、太阴。陷落时易思虑过度。",
    "太阳": "中天星系主星，光明之星。主贵、发散、公益。庙旺宽宏大量，陷落时劳碌、散财。",
    "武曲": "北斗第六星，财帛主。主财富、决断、刚毅。喜天府、天相。陷落时过于刚硬。",
    "天同": "南斗第四星，福星。主福气、协调、温和。庙旺一生安逸，陷落时懒散拖延。",
    "廉贞": "北斗第五星，次桃花。主是非、权术、文艺。庙旺清正，陷落时易卷入是非。",
    "天府": "南斗主星，财库。主稳定、包容、守成。喜紫微、武曲。陷落时保守有余。",
    "太阴": "中天星系主星，母星。主温柔、美感、财富。庙旺富有情调，陷落时阴郁。",
    "贪狼": "北斗第一星，桃花主。主交际、才艺、欲望。喜武曲、破军。庙旺才艺出众。",
    "巨门": "北斗第二星，暗星。主口才、是非、思辨。喜日、月。陷落时口舌是非。",
    "天相": "南斗第五星，印星。主诚信、公正、辅助。喜紫微、天府。陷落时易偏执。",
    "天梁": "南斗第二星，荫星。主贵、长寿、清高。喜天机、太阳。陷落时孤芳自赏。",
    "七杀": "南斗第一星，将星。主权威、决断、变动。庙旺威震八方，陷落时多事。",
    "破军": "北斗第七星，耗星。主变革、突破、动荡。喜贪狼、七杀。陷落时破耗。",
}


def get_star_info(star_name: str) -> Optional[str]:
    """获取星曜简介"""
    for name, desc in STAR_ENCYCLOPEDIA.items():
        if name == star_name:
            return f"⭐ *{name}*\n{desc}"
    return None


path: scripts/i18n.py
content: """
i18n.py — 一念紫微斗数 国际化多语言模块
支持：zh-CN/zh-TW/en-US/ja-JP/ko-KR/vi-VN

Author: 崽儿虾 🦞
"""

from typing import Dict, List, Optional, Tuple

# ============================================================
# 核心词库
# ============================================================

LANGUAGES = {
    "zh-CN": "简体中文",
    "zh-TW": "繁體中文",
    "en-US": "English",
    "ja-JP": "日本語",
    "ko-KR": "한국어",
    "vi-VN": "Tiếng Việt",
}

def lang_code_validate(code: str) -> str:
    """验证并标准化语言代码"""
    mapping = {
        "zh": "zh-CN", "cn": "zh-CN", "zh-cn": "zh-CN",
        "zh-tw": "zh-TW", "tw": "zh-TW",
        "en": "en-US", "en-us": "en-US",
        "ja": "ja-JP", "ja-jp": "ja-JP", "jp": "ja-JP",
        "ko": "ko-KR", "ko-kr": "ko-KR", "kr": "ko-KR",
        "vi": "vi-VN", "vi-vn": "vi-VN", "vn": "vi-VN",
    }
    return mapping.get(code.lower(), code) if "-" not in code else code


# ============================================================
# 宫位翻译
# ============================================================

PALACE_NAMES: Dict[str, Dict[str, str]] = {
    "命宫": {
        "zh-CN": "命宫", "zh-TW": "命宮",
        "en-US": "Life", "ja-JP": "命宮",
        "ko-KR": "명궁", "vi-VN": "Mệnh cung",
    },
    "父母宫": {
        "zh-CN": "父母宫", "zh-TW": "父母宮",
        "en-US": "Parents", "ja-JP": "父母宮",
        "ko-KR": "부모궁", "vi-VN": "Phụ mẫu cung",
    },
    "福德宫": {
        "zh-CN": "福德宫", "zh-TW": "福德宮",
        "en-US": "Spirit", "ja-JP": "福德宮",
        "ko-KR": "복덕궁", "vi-VN": "Phúc đức cung",
    },
    "田宅宫": {
        "zh-CN": "田宅宫", "zh-TW": "田宅宮",
        "en-US": "Property", "ja-JP": "田宅宮",
        "ko-KR": "전택궁", "vi-VN": "Điền trạch cung",
    },
    "官禄宫": {
        "zh-CN": "官禄宫", "zh-TW": "官祿宮",
        "en-US": "Career", "ja-JP": "官禄宮",
        "ko-KR": "관록궁", "vi-VN": "Quan lộc cung",
    },
    "交友宫": {
        "zh-CN": "交友宫", "zh-TW": "交友宮",
        "en-US": "Friends", "ja-JP": "交友宮",
        "ko-KR": "교우궁", "vi-VN": "Giao hữu cung",
    },
    "迁移宫": {
        "zh-CN": "迁移宫", "zh-TW": "遷移宮",
        "en-US": "Travel", "ja-JP": "遷移宮",
        "ko-KR": "천이궁", "vi-VN": "Thiên di cung",
    },
    "疾厄宫": {
        "zh-CN": "疾厄宫", "zh-TW": "疾厄宮",
        "en-US": "Health", "ja-JP": "疾厄宮",
        "ko-KR": "질액궁", "vi-VN": "Tật ách cung",
    },
    "财帛宫": {
        "zh-CN": "财帛宫", "zh-TW": "財帛宮",
        "en-US": "Wealth", "ja-JP": "財帛宮",
        "ko-KR": "재백궁", "vi-VN": "Tài bạch cung",
    },
    "子女宫": {
        "zh-CN": "子女宫", "zh-TW": "子女宮",
        "en-US": "Children", "ja-JP": "子女宮",
        "ko-KR": "자녀궁", "vi-VN": "Tử nữ cung",
    },
    "夫妻宫": {
        "zh-CN": "夫妻宫", "zh-TW": "夫妻宮",
        "en-US": "Marriage", "ja-JP": "夫妻宮",
        "ko-KR": "부처궁", "vi-VN": "Phu thê cung",
    },
    "兄弟宫": {
        "zh-CN": "兄弟宫", "zh-TW": "兄弟宮",
        "en-US": "Siblings", "ja-JP": "兄弟宮",
        "ko-KR": "형제궁", "vi-VN": "Huynh đệ cung",
    },
}

# ============================================================
# 四化翻译
# ============================================================

MUTAGEN_NAMES: Dict[str, Dict[str, str]] = {
    "禄": {"zh-CN": "化禄", "zh-TW": "化祿", "en-US": "Prosperity(Hua-Lu)", "ja-JP": "化禄", "ko-KR": "화록", "vi-VN": "Hóa Lộc"},
    "权": {"zh-CN": "化权", "zh-TW": "化權", "en-US": "Authority(Hua-Quan)", "ja-JP": "化権", "ko-KR": "화권", "vi-VN": "Hóa Quyền"},
    "科": {"zh-CN": "化科", "zh-TW": "化科", "en-US": "Fame(Hua-Ke)", "ja-JP": "化科", "ko-KR": "화과", "vi-VN": "Hóa Khoa"},
    "忌": {"zh-CN": "化忌", "zh-TW": "化忌", "en-US": "Worry(Hua-Ji)", "ja-JP": "化忌", "ko-KR": "화기", "vi-VN": "Hóa Kỵ"},
}

# ============================================================
# 亮度翻译
# ============================================================

BRIGHTNESS: Dict[str, Dict[str, str]] = {
    "庙": {"zh-CN": "庙", "zh-TW": "廟", "en-US": "Temple", "ja-JP": "廟", "ko-KR": "묘", "vi-VN": "Miếu"},
    "旺": {"zh-CN": "旺", "zh-TW": "旺", "en-US": "Prosperous", "ja-JP": "旺", "ko-KR": "왕", "vi-VN": "Vượng"},
    "得": {"zh-CN": "得", "zh-TW": "得", "en-US": "Adequate", "ja-JP": "得", "ko-KR": "득", "vi-VN": "Đắc"},
    "利": {"zh-CN": "利", "zh-TW": "利", "en-US": "Favorable", "ja-JP": "利", "ko-KR": "리", "vi-VN": "Lợi"},
    "平": {"zh-CN": "平", "zh-TW": "平", "en-US": "Flat", "ja-JP": "平", "ko-KR": "평", "vi-VN": "Bình"},
    "陷": {"zh-CN": "陷", "zh-TW": "陷", "en-US": "Weakened", "ja-JP": "陥", "ko-KR": "함", "vi-VN": "Hãm"},
}

# ============================================================
# 主要星曜翻译
# ============================================================

MAJOR_STARS: Dict[str, Dict[str, str]] = {
    "紫微": {"en-US": "Zi Wei (Emperor)", "ja-JP": "紫微", "ko-KR": "자미", "vi-VN": "Tử Vi"},
    "天机": {"en-US": "Tian Ji (Secrets)", "ja-JP": "天機", "ko-KR": "천기", "vi-VN": "Thiên Cơ"},
    "太阳": {"en-US": "Tai Yang (Sun)", "ja-JP": "太陽", "ko-KR": "태양", "vi-VN": "Thái Dương"},
    "武曲": {"en-US": "Wu Qu (Finance)", "ja-JP": "武曲", "ko-KR": "무곡", "vi-VN": "Vũ Khúc"},
    "天同": {"en-US": "Tian Tong (Harmony)", "ja-JP": "天同", "ko-KR": "천동", "vi-VN": "Thiên Đồng"},
    "廉贞": {"en-US": "Lian Zhen (Integrity)", "ja-JP": "廉貞", "ko-KR": "염정", "vi-VN": "Liêm Trinh"},
    "天府": {"en-US": "Tian Fu (Treasury)", "ja-JP": "天府", "ko-KR": "천부", "vi-VN": "Thiên Phủ"},
    "太阴": {"en-US": "Tai Yin (Moon)", "ja-JP": "太陰", "ko-KR": "태음", "vi-VN": "Thái Âm"},
    "贪狼": {"en-US": "Tan Lang (Wolf)", "ja-JP": "貪狼", "ko-KR": "탐랑", "vi-VN": "Tham Lang"},
    "巨门": {"en-US": "Ju Men (Gate)", "ja-JP": "巨門", "ko-KR": "거문", "vi-VN": "Cự Môn"},
    "天相": {"en-US": "Tian Xiang (Minister)", "ja-JP": "天相", "ko-KR": "천상", "vi-VN": "Thiên Tướng"},
    "天梁": {"en-US": "Tian Liang (Blessing)", "ja-JP": "天梁", "ko-KR": "천량", "vi-VN": "Thiên Lương"},
    "七杀": {"en-US": "Qi Sha (7 Killings)", "ja-JP": "七殺", "ko-KR": "칠살", "vi-VN": "Thất Sát"},
    "破军": {"en-US": "Po Jun (Destroyer)", "ja-JP": "破軍", "ko-KR": "파군", "vi-VN": "Phá Quân"},
}

# ============================================================
# 天干地支翻译
# ============================================================

HEAVENLY_STEMS: Dict[str, Dict[str, str]] = {
    "甲": {"en-US": "Jia", "ja-JP": "甲", "ko-KR": "갑", "vi-VN": "Giáp"},
    "乙": {"en-US": "Yi", "ja-JP": "乙", "ko-KR": "을", "vi-VN": "Ất"},
    "丙": {"en-US": "Bing", "ja-JP": "丙", "ko-KR": "병", "vi-VN": "Bính"},
    "丁": {"en-US": "Ding", "ja-JP": "丁", "ko-KR": "정", "vi-VN": "Đinh"},
    "戊": {"en-US": "Wu", "ja-JP": "戊", "ko-KR": "무", "vi-VN": "Mậu"},
    "己": {"en-US": "Ji", "ja-JP": "己", "ko-KR": "기", "vi-VN": "Kỷ"},
    "庚": {"en-US": "Geng", "ja-JP": "庚", "ko-KR": "경", "vi-VN": "Canh"},
    "辛": {"en-US": "Xin", "ja-JP": "辛", "ko-KR": "신", "vi-VN": "Tân"},
    "壬": {"en-US": "Ren", "ja-JP": "壬", "ko-KR": "임", "vi-VN": "Nhâm"},
    "癸": {"en-US": "Gui", "ja-JP": "癸", "ko-KR": "계", "vi-VN": "Quý"},
}

EARTHLY_BRANCHES: Dict[str, Dict[str, str]] = {
    "子": {"en-US": "Zi", "ja-JP": "子", "ko-KR": "자", "vi-VN": "Tý"},
    "丑": {"en-US": "Chou", "ja-JP": "丑", "ko-KR": "축", "vi-VN": "Sửu"},
    "寅": {"en-US": "Yin", "ja-JP": "寅", "ko-KR": "인", "vi-VN": "Dần"},
    "卯": {"en-US": "Mao", "ja-JP": "卯", "ko-KR": "묘", "vi-VN": "Mão"},
    "辰": {"en-US": "Chen", "ja-JP": "辰", "ko-KR": "진", "vi-VN": "Thìn"},
    "巳": {"en-US": "Si", "ja-JP": "巳", "ko-KR": "사", "vi-VN": "Tỵ"},
    "午": {"en-US": "Wu", "ja-JP": "午", "ko-KR": "오", "vi-VN": "Ngọ"},
    "未": {"en-US": "Wei", "ja-JP": "未", "ko-KR": "미", "vi-VN": "Mùi"},
    "申": {"en-US": "Shen", "ja-JP": "申", "ko-KR": "신", "vi-VN": "Thân"},
    "酉": {"en-US": "You", "ja-JP": "酉", "ko-KR": "유", "vi-VN": "Dậu"},
    "戌": {"en-US": "Xu", "ja-JP": "戌", "ko-KR": "술", "vi-VN": "Tuất"},
    "亥": {"en-US": "Hai", "ja-JP": "亥", "ko-KR": "해", "vi-VN": "Hợi"},
}

# ============================================================
# 接口标签翻译
# ============================================================

UI_LABELS: Dict[str, Dict[str, str]] = {
    "title": {
        "zh-CN": "一念紫微斗数", "zh-TW": "一念紫微斗數",
        "en-US": "Yinian ZWDS", "ja-JP": "一念紫微斗数",
        "ko-KR": "일념자미두수", "vi-VN": "Nhất Niệm Tử Vi Đẩu Số",
    },
    "four_pillars": {
        "zh-CN": "四柱", "zh-TW": "四柱",
        "en-US": "Four Pillars", "ja-JP": "四柱",
        "ko-KR": "사주", "vi-VN": "Tứ trụ",
    },
    "zodiac": {
        "zh-CN": "生肖", "zh-TW": "生肖",
        "en-US": "Zodiac", "ja-JP": "生肖",
        "ko-KR": "띠", "vi-VN": "Tuổi",
    },
    "five_elements": {
        "zh-CN": "五行局", "zh-TW": "五行局",
        "en-US": "Element Bureau", "ja-JP": "五行局",
        "ko-KR": "오행국", "vi-VN": "Cục ngũ hành",
    },
    "soul_palace": {
        "zh-CN": "命宫", "zh-TW": "命宮",
        "en-US": "Life Palace", "ja-JP": "命宮",
        "ko-KR": "명궁", "vi-VN": "Mệnh cung",
    },
    "body_palace": {
        "zh-CN": "身宫", "zh-TW": "身宮",
        "en-US": "Body Palace", "ja-JP": "身宮",
        "ko-KR": "신궁", "vi-VN": "Thân cung",
    },
    "mutagens": {
        "zh-CN": "生年四化", "zh-TW": "生年四化",
        "en-US": "Four Transformations", "ja-JP": "生年四化",
        "ko-KR": "사화", "vi-VN": "Tứ hóa",
    },
    "empty_palace": {
        "zh-CN": "空宫", "zh-TW": "空宮",
        "en-US": "Empty Palace", "ja-JP": "空宮",
        "ko-KR": "빈궁", "vi-VN": "Không cung",
    },
    "current_decadal": {
        "zh-CN": "当前大限", "zh-TW": "當前大限",
        "en-US": "Current Decade", "ja-JP": "現在の大限",
        "ko-KR": "현재 대한", "vi-VN": "Đại hạn hiện tại",
    },
    "reading_title": {
        "zh-CN": "AI深度解盘", "zh-TW": "AI深度解盤",
        "en-US": "AI Deep Reading", "ja-JP": "AI深層解釈",
        "ko-KR": "AI 심층 해석", "vi-VN": "Giải mã AI chuyên sâu",
    },
    "yearly_title": {
        "zh-CN": "流年运势", "zh-TW": "流年運勢",
        "en-US": "Yearly Forecast", "ja-JP": "今年の運勢",
        "ko-KR": "연운", "vi-VN": "Vận niên",
    },
    "monthly_title": {
        "zh-CN": "流月运势", "zh-TW": "流月運勢",
        "en-US": "Monthly Forecast", "ja-JP": "今月の運勢",
        "ko-KR": "월운", "vi-VN": "Vận nguyệt",
    },
    "disclaimer": {
        "zh-CN": "📜 文化参考，理性看待", "zh-TW": "📜 文化參考，理性看待",
        "en-US": "📜 For cultural reference only", "ja-JP": "📜 文化的参考として",
        "ko-KR": "📜 문화적 참고 자료입니다", "vi-VN": "📜 Tham khảo văn hóa",
    },
}


# ============================================================
# 翻译函数
# ============================================================

def t(key: str, lang: str = "zh-CN", fallback: str = "en-US") -> str:
    """获取指定语言文本，支持多级key"""
    lang = lang_code_validate(lang)
    
    # UI标签
    if key in UI_LABELS:
        return UI_LABELS[key].get(lang, UI_LABELS[key].get(fallback, key))
    
    # 宫位
    if key in PALACE_NAMES:
        return PALACE_NAMES[key].get(lang, PALACE_NAMES[key].get(fallback, key))
    
    # 四化
    if key in MUTAGEN_NAMES:
        return MUTAGEN_NAMES[key].get(lang, MUTAGEN_NAMES[key].get(fallback, key))
    
    # 亮度
    if key in BRIGHTNESS:
        return BRIGHTNESS[key].get(lang, BRIGHTNESS[key].get(fallback, key))
    
    # 主星
    if key in MAJOR_STARS:
        return MAJOR_STARS[key].get(lang, key)
    
    # 天干
    if key in HEAVENLY_STEMS:
        return HEAVENLY_STEMS[key].get(lang, key)
    
    # 地支
    if key in EARTHLY_BRANCHES:
        return EARTHLY_BRANCHES[key].get(lang, key)
    
    return key


def t_star(name: str, lang: str = "en-US") -> str:
    """星曜名称翻译"""
    return MAJOR_STARS.get(name, {}).get(lang, name)


def t_palace(name: str, lang: str = "en-US") -> str:
    """宫位名称翻译"""
    return PALACE_NAMES.get(name, {}).get(lang, name)


# ============================================================
# 辅助函数
# ============================================================

def format_birth_str(year_stem_branch: str, month_stem_branch: str, lang: str = "zh-CN") -> str:
    """格式化四柱"""
    if lang.startswith("en"):
        # 英文：直接显示拼音
        return f"{year_stem_branch} {month_stem_branch}"
    return f"{year_stem_branch} {month_stem_branch}"


def get_lang_direction(lang: str) -> str:
    """获取文本方向"""
    return "ltr" if lang.startswith("en") else "ltr"


if __name__ == "__main__":
    # 测试
    test_keys = ["命宫", "紫微", "禄", "庙", "甲", "title"]
    for key in test_keys:
        for lang in ["en-US", "ja-JP", "ko-KR", "vi-VN"]:
            val = t(key, lang)
            print(f"  {lang}: {key} → {val}")
        print()


path: scripts/matching.py
content: """
matching.py — 一念紫微斗数 合盘匹配引擎
情侣/朋友/事业伙伴合盘分析

Author: 崽儿虾 🦞
"""

from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass
from zwds_calc import generate_astrolabe, astrolabe_to_json
from deep_reading import full_deep_reading
from i18n import t_palace, t_star, t


# ============================================================
# 合盘核心维度
# ============================================================

MATCH_DIMENSIONS = {
    "命宫互看": "Life Palace mutual influence",
    "夫妻宫": "Marriage Palace resonance",
    "四化交集": "Four Transformations intersection",
    "五行互补": "Elemental complement",
    "福德宫": "Spiritual compatibility",
}


def compute_match(
    date1: str, hour1: int, gender1: str,
    date2: str, hour2: int, gender2: str,
) -> Dict[str, Any]:
    """
    两人合盘分析
    
    Returns:
        {
            "success": bool,
            "person_a": {...},  # 命盘数据
            "person_b": {...},  # 命盘数据
            "dimensions": {     # 各维度分析
                "命宫互看": {...},
                "夫妻宫": {...},
                ...
            },
            "score": 65,         # 综合评分 0-100
            "summary": "...",    # AI解盘Prompt
        }
    """
    a = generate_astrolabe(date1, hour1, gender1)
    b = generate_astrolabe(date2, hour2, gender2)
    
    if not a or not b:
        return {"success": False, "error": "排盘失败"}
    
    a_json = astrolabe_to_json(a)
    b_json = astrolabe_to_json(b)
    
    # 各维度分析
    dimensions = {}
    
    # 1. 命宫互看
    dimensions["命宫互看"] = _analyze_life_mutual(a, b, a_json, b_json)
    
    # 2. 夫妻宫
    dimensions["夫妻宫"] = _analyze_marriage(a, b)
    
    # 3. 四化交集
    dimensions["四化交集"] = _analyze_mutagen_intersection(a, b)
    
    # 4. 五行互补
    dimensions["五行互补"] = _analyze_element(a, b)
    
    # 5. 福德宫
    dimensions["福德宫"] = _analyze_spirit(a, b)
    
    # 综合评分
    score = _compute_score(dimensions)
    
    # AI解盘Prompt
    summary = _build_match_prompt(a, b, a_json, b_json, dimensions, score)
    
    return {
        "success": True,
        "person_a": a_json,
        "person_b": b_json,
        "dimensions": dimensions,
        "score": score,
        "score_text": _score_to_text(score),
        "ai_prompt": summary,
    }


def _analyze_life_mutual(
    a, b, a_json: Dict, b_json: Dict
) -> Dict[str, Any]:
    """命宫互看：A的命宫星曜对B的影响"""
    a_soul = a_json["palaces"][0]
    b_soul = b_json["palaces"][0]
    
    a_stars = [s["name"] for s in a_soul["major_stars"]]
    b_stars = [s["name"] for s in b_soul["major_stars"]]
    
    return {
        "a_life_stars": a_stars,
        "b_life_stars": b_stars,
        "analysis": f"A命宫{len(a_stars) and a_stars[0] or '空'} | B命宫{len(b_stars) and b_stars[0] or '空'}",
    }


def _analyze_marriage(a, b) -> Dict[str, Any]:
    """夫妻宫分析"""
    a_marriage = a.palaces[10]  # 夫妻宫在标准序中索引10
    b_marriage = b.palaces[10]
    
    a_ms = [s.name for s in a_marriage.major_stars]
    b_ms = [s.name for s in b_marriage.major_stars]
    
    return {
        "a_marriage_stars": a_ms,
        "b_marriage_stars": b_ms,
        "a_marriage_empty": a_marriage.is_empty,
        "b_marriage_empty": b_marriage.is_empty,
    }


def _analyze_mutagen_intersection(a, b) -> Dict[str, Any]:
    """四化交集分析"""
    a_mut = [(m["star"], m["mutagen"], m["palace"]) for m in a.mutagens if isinstance(m, dict)]
    b_mut = [(m["star"], m["mutagen"], m["palace"]) for m in b.mutagens if isinstance(m, dict)]
    
    # 寻找相同四化的星曜
    common = []
    for s1, m1, p1 in a_mut:
        for s2, m2, p2 in b_mut:
            if s1 == s2 and m1 == m2:
                common.append(f"共同{m1}: {s1}化{m1}")
    
    return {
        "a_mutagens": a_mut,
        "b_mutagens": b_mut,
        "common_transformations": common,
    }


def _analyze_element(a, b) -> Dict[str, Any]:
    """五行互补分析"""
    a_elem = a.five_elements_class or ""
    b_elem = b.five_elements_class or ""
    
    # 五行相生相克
    elem_num = {"水二局": 0, "木三局": 1, "金四局": 2, "土五局": 3, "火六局": 4}
    producing = [(0,1),(1,2),(2,3),(3,4),(4,0)]  # 相生
    destroying = [(0,2),(2,1),(1,3),(3,0),(0,4)]  # 相克
    
    ae = elem_num.get(a_elem, -1)
    be = elem_num.get(b_elem, -1)
    
    relation = "未知"
    if ae >= 0 and be >= 0:
        if (ae, be) in producing:
            relation = "相生（互补）"
        elif (be, ae) in producing:
            relation = "相生（互补）"
        elif (ae, be) in destroying:
            relation = "相克（需调和）"
        elif (be, ae) in destroying:
            relation = "相克（需调和）"
        else:
            relation = "同元素"
    
    return {
        "a_element": a_elem,
        "b_element": b_elem,
        "relationship": relation,
    }


def _analyze_spirit(a, b) -> Dict[str, Any]:
    """福德宫精神层面分析"""
    a_spirit = a.palaces[2]
    b_spirit = b.palaces[2]
    
    a_ms = [s.name for s in a_spirit.major_stars]
    b_ms = [s.name for s in b_spirit.major_stars]
    
    return {
        "a_spirit_stars": a_ms,
        "b_spirit_stars": b_ms,
    }


def _compute_score(dimensions: Dict) -> int:
    """计算综合评分 0-100"""
    score = 60  # 基础分
    
    # 各维度加分/扣分逻辑
    marriage = dimensions.get("夫妻宫", {})
    if not marriage.get("a_marriage_empty") and not marriage.get("b_marriage_empty"):
        score += 10
    
    mutual = dimensions.get("四化交集", {})
    if mutual.get("common_transformations"):
        score += 10
    
    element = dimensions.get("五行互补", {})
    if "相生" in element.get("relationship", ""):
        score += 10
    elif "相克" in element.get("relationship", ""):
        score -= 5
    
    return max(0, min(100, score))


def _score_to_text(score: int) -> str:
    """评分文字描述"""
    if score >= 85:
        return "绝配"
    elif score >= 70:
        return "合拍"
    elif score >= 55:
        return "一般"
    elif score >= 40:
        return "需磨合"
    else:
        return "有挑战"


def _build_match_prompt(
    a, b, a_json: Dict, b_json: Dict,
    dimensions: Dict, score: int,
) -> str:
    """构建合盘AI解盘Prompt"""
    lines = []
    lines.append("【紫微斗数合盘分析】")
    lines.append("")
    
    # 双方信息
    lines.append(f"A：{a.birth_date} 时{a.birth_hour} {a.gender}")
    lines.append(f"  命宫：{a.palaces[0].name_cn}  五行：{a.five_elements_class}")
    lines.append(f"  夫妻宫：{a.palaces[10].name_cn}  主星：{_stars_str(a.palaces[10])}")
    lines.append("")
    
    lines.append(f"B：{b.birth_date} 时{b.birth_hour} {b.gender}")
    lines.append(f"  命宫：{b.palaces[0].name_cn}  五行：{b.five_elements_class}")
    lines.append(f"  夫妻宫：{b.palaces[10].name_cn}  主星：{_stars_str(b.palaces[10])}")
    lines.append("")
    
    # 各维度
    lines.append("【各维度分析】")
    
    e = dimensions.get("五行互补", {})
    lines.append(f"▪ 五行关系：{e.get('a_element')} × {e.get('b_element')} → {e.get('relationship')}")
    
    m = dimensions.get("四化交集", {})
    if m.get("common_transformations"):
        lines.append(f"▪ 四化交集：{'、'.join(m['common_transformations'])}")
    
    lines.append("")
    lines.append(f"综合评分：{score}/100（{_score_to_text(score)}）")
    lines.append("")
    
    lines.append("请从以下角度分析这段关系：")
    lines.append("1. 命宫星曜的互动模式")
    lines.append("2. 夫妻宫的相合程度")
    lines.append("3. 四化连线带来的缘分深度")
    lines.append("4. 五行互补与调和空间")
    lines.append("5. 彼此提供的关键支持领域")
    lines.append("")
    lines.append("📜 文化参考，理性看待")
    
    return "\n".join(lines)


def _stars_str(palace) -> str:
    """宫位主星字符串"""
    return "、".join(f"{s.name}({s.brightness})" for s in palace.major_stars) or "空宫"


# ============================================================
# Telegram格式化输出
# ============================================================

def format_match_for_tg(result: Dict) -> str:
    """合盘结果格式化为Telegram文本"""
    if not result["success"]:
        return f"❌ {result.get('error')}"
    
    a = result["person_a"]
    b = result["person_b"]
    
    lines = []
    lines.append("💞 *紫微斗数 · 合盘匹配*")
    lines.append("")
    
    # 双方基础
    a_fp = a.get("four_pillars", {})
    b_fp = b.get("four_pillars", {})
    a_soul = a.get("palaces", [{}])[0]
    b_soul = b.get("palaces", [{}])[0]
    
    lines.append("*A方*")
    lines.append(f"📅 {a.get('birth_date','')} 五行：{a.get('five_elements','')}")
    lines.append(f"🏠 命宫：{a_soul.get('name_cn','')} 夫妻宫：{a.get('palaces',[{}])[10].get('name_cn','')}")
    lines.append("")
    
    lines.append("*B方*")
    lines.append(f"📅 {b.get('birth_date','')} 五行：{b.get('five_elements','')}")
    lines.append(f"🏠 命宫：{b_soul.get('name_cn','')} 夫妻宫：{b.get('palaces',[{}])[10].get('name_cn','')}")
    lines.append("")
    
    # 评分
    score = result.get("score", 50)
    score_t = result.get("score_text", "")
    score_bar = "▓" * (score // 10) + "░" * (10 - score // 10)
    lines.append(f"*综合契合度*：{score}/100 {score_bar}")
    lines.append(f"评估：{score_t}")
    lines.append("")
    
    # 各维度
    dims = result.get("dimensions", {})
    
    elem = dims.get("五行互补", {})
    lines.append(f"▪ *五行*：{elem.get('a_element')} × {elem.get('b_element')}")
    lines.append(f"  → {elem.get('relationship', '')}")
    
    marriage = dims.get("夫妻宫", {})
    a_marry = "、".join(marriage.get("a_marriage_stars", [])) or "空宫"
    b_marry = "、".join(marriage.get("b_marriage_stars", [])) or "空宫"
    lines.append(f"▪ *夫妻宫*：A-{a_marry} | B-{b_marry}")
    
    mutual = dims.get("四化交集", {})
    if mutual.get("common_transformations"):
        lines.append(f"▪ *四化共鸣*：{'、'.join(mutual['common_transformations'])}")
    
    lines.append("")
    lines.append("📜 文化参考，理性看待")
    
    return "\n".join(lines)


if __name__ == "__main__":
    # 测试合盘
    result = compute_match(
        "1984-6-22", 6, "男",   # 南曦
        "1990-3-15", 8, "女",   # 测试
    )
    
    if result["success"]:
        print(format_match_for_tg(result))
        print()
        print("=" * 40)
        print("AI Prompt:")
        print(result["ai_prompt"])
    else:
        print(f"❌ {result.get('error')}")


path: scripts/multilang_prompts.py
content: """
multilang_prompts.py — 一念紫微斗数 多语言Prompt模板
支持中文/英文/日文AI解盘

Author: 崽儿虾 🦞
"""

from typing import Dict, Optional
from i18n import t, t_palace, t_star, lang_code_validate
from zwds_calc import compute_surrounded


# ============================================================
# 多语言系统提示词
# ============================================================

SYSTEM_PROMPTS: Dict[str, str] = {
    "zh-CN": """你是一位精通紫微斗数的顶尖命理师，擅长三派合一综合解读。

【核心能力】
1. 三合派（中州派）：星曜性质 × 亮度 × 三方四正
2. 飞星派（钦天门）：宫干四化脉络 × 自化 × 追禄追忌
3. 占验派（紫云）：太岁入卦 × 特殊格局 × 星曜互涉

【输出要求】
- 严格按照JSON格式输出报告
- 每层用清晰结构
- 使用可能性语气
- 结语标注：📜 文化参考，理性看待""",

    "en-US": """You are a master of Zi Wei Dou Shu (Purple Star Astrology), skilled in the Three-School integrated approach.

【Core Capabilities】
1. San He (Triple Harmony): Star nature × brightness × Four Harmonies
2. Fei Xing (Flying Star): Stem Transformations × Self-transformation chains
3. Zhan Yan (Divination): Tai Sui implantation × Special patterns

【Output Requirements】
- Output in precise JSON format with the structure {"report": {...}}
- Use probabilistic language ("tends to", "may indicate"), never absolutes
- Include cultural context notes
- End with: 📜 For cultural reference only""",

    "ja-JP": """あなたは紫微斗数の達人で、三派統合の解釈を得意としています。

【核心能力】
1. 三合派：星曜の性質 × 光度 × 三方四正
2. 飛星派：宮干四化 × 自化 × 追禄追忌
3. 占験派：太歳入卦 × 特殊格局 × 星曜互渉

【出力要件】
- JSON形式で出力してください
- 可能性を示す表現を使用し、断定的な言い方は避けてください
- 文化的な参考としての見解であることを示してください""",
}

# ============================================================
# 多语言用户提示模板
# ============================================================

def build_multilang_basic_prompt(chart_data: Dict, lang: str = "zh-CN") -> str:
    """多语言基础宫位解读Prompt"""
    lang = lang_code_validate(lang)
    
    if lang == "en-US":
        return _build_en_basic(chart_data)
    elif lang == "ja-JP":
        return _build_ja_basic(chart_data)
    else:
        return _build_zh_basic(chart_data)


def _build_zh_basic(data: Dict) -> str:
    """中文基础解读"""
    lines = []
    lines.append("【十二宫详解】")
    for p in data.get("palaces", []):
        ms = " ".join(
            f"{s['name']}({s['brightness']})" + (f"→{s['mutagen']}" if s.get('mutagen') else "")
            for s in p.get("major_stars", [])
        )
        mins = " ".join(s["name"] for s in p.get("minor_stars", []))
        empty = "【空宫】" if p.get("is_empty") else ""
        sur = compute_surrounded(p["index"])
        sur_names = [data["palaces"][i]["name_cn"] for i in sur]
        lines.append(f"▪ {p['name_cn']}（{p['heavenly_stem']}{p['earthly_branch']}）：{ms or empty}")
        if mins:
            lines.append(f"  辅星：{mins}")
        lines.append(f"  三方四正：{'、'.join(sur_names)}")
        lines.append("")
    return "\n".join(lines)


def _build_en_basic(data: Dict) -> str:
    """英文基础解读"""
    lines = []
    lines.append("【Twelve Palaces Detail】")
    for p in data.get("palaces", []):
        name_en = t_palace(p["name_cn"], "en-US")
        stem_en = p["heavenly_stem"]
        branch_en = p["earthly_branch"]
        
        majors = []
        for s in p.get("major_stars", []):
            star_en = s.get("name_en", t_star(s["name"], "en-US"))
            bright = s["brightness"]
            star_str = f"{star_en} ({bright})"
            if s.get("mutagen"):
                star_str += f" → {s['mutagen']}"
            majors.append(star_str)
        
        minors = [s["name"] for s in p.get("minor_stars", [])]
        empty = "【Empty Palace (borrow from opposite palace)】" if p.get("is_empty") else ""
        
        sur = compute_surrounded(p["index"])
        sur_names = [t_palace(data["palaces"][i]["name_cn"], "en-US") for i in sur]
        
        major_str = "、".join(majors) if majors else empty
        minor_str = "、".join(minors) if minors else ""
        
        lines.append(f"▪ {name_en} ({stem_en}{branch_en}): {major_str}")
        if minor_str:
            lines.append(f"  Minor stars: {minor_str}")
        lines.append(f"  Four Harmonies: {' - '.join(sur_names)}")
        lines.append("")
    return "\n".join(lines)


def _build_ja_basic(data: Dict) -> str:
    """日文基础解读"""
    lines = []
    lines.append("【十二宮詳細】")
    for p in data.get("palaces", []):
        name_ja = t_palace(p["name_cn"], "ja-JP")
        
        majors = []
        for s in p.get("major_stars", []):
            star_ja = t_star(s["name"], "ja-JP")
            star_str = f"{star_ja}({s['brightness']})"
            if s.get("mutagen"):
                star_str += f" → {s['mutagen']}"
            majors.append(star_str)
        
        minors = [t_star(s["name"], "ja-JP") for s in p.get("minor_stars", [])]
        empty = "【空宮】" if p.get("is_empty") else ""
        
        sur = compute_surrounded(p["index"])
        sur_names = [t_palace(data["palaces"][i]["name_cn"], "ja-JP") for i in sur]
        
        major_str = "、".join(majors) if majors else empty
        minor_str = "、".join(minors) if minors else ""
        
        lines.append(f"▪ {name_ja}：{major_str}")
        if minor_str:
            lines.append(f"  副星：{minor_str}")
        lines.append(f"  三方四正：{'、'.join(sur_names)}")
        lines.append("")
    return "\n".join(lines)


# ============================================================
# 完整多语言解盘Prompt拼接
# ============================================================

def build_multilang_deep_prompt(
    chart_data: Dict,
    deep_data: Dict,
    target_year: int,
    lang: str = "zh-CN",
) -> str:
    """根据语言构建完整解盘Prompt"""
    lang = lang_code_validate(lang)
    
    if lang == "en-US":
        return _build_en_full_prompt(chart_data, deep_data, target_year)
    elif lang == "ja-JP":
        return _build_ja_full_prompt(chart_data, deep_data, target_year)
    else:
        return _build_zh_full_prompt(chart_data, deep_data, target_year)


def _build_zh_full_prompt(chart_data: Dict, deep_data: Dict, target_year: int) -> str:
    """中文完整Prompt（复用之前的）"""
    from prompts import build_full_deep_prompt
    return build_full_deep_prompt(chart_data, deep_data, target_year)


def _build_en_full_prompt(chart_data: Dict, deep_data: Dict, target_year: int) -> str:
    """英文完整Prompt"""
    fp = chart_data.get("four_pillars", {})
    timing = deep_data.get("timing", {})
    d = timing.get("current_decadal")
    
    lines = []
    lines.append(f"📅 Subject: {fp.get('year','')} {fp.get('month','')} {fp.get('day','')} {fp.get('hour','')}")
    lines.append(f"🐉 Zodiac: {chart_data.get('zodiac','')}")
    lines.append(f"🏠 Life Palace: {t_palace(chart_data.get('palaces',[{}])[0].get('name_cn',''))}")
    if d:
        lines.append(f"📍 Current Decade: {t_palace(d['palace_name'])} ({d['age_start']}-{d['age_end']} yrs)")
    lines.append("")
    
    # 四化
    if chart_data.get("mutagens"):
        lines.append("【Yearly Transformations】")
        for m in chart_data["mutagens"]:
            lines.append(f"  · {t_palace(m['palace'])}: {t_star(m['star'])} → {m['mutagen']}")
        lines.append("")
    
    # 十二宫
    lines.append("【Layer 1: Palace Detail】")
    lines.append(_build_en_basic(chart_data))
    
    # 格局
    patterns = deep_data.get("advanced", {}).get("patterns", [])
    if patterns:
        lines.append("【Layer 2: Special Patterns】")
        for pt in patterns:
            lines.append(f"  ▪ {pt['name']}: {pt['description']}")
        lines.append("")
    
    # 大限表
    if timing.get("decadal_sequence"):
        lines.append("【Layer 3: Decade Fortune Table】")
        for item in timing["decadal_sequence"]:
            mk = "👉" if item.get("is_current") else "  "
            pn = t_palace(item["palace_name"], "en-US")
            lines.append(f"  {mk} Age {item['age_start']}-{item['age_end']}: {pn}")
        lines.append("")
    
    # 专业层
    prof = deep_data.get("professional", {})
    laiyin = prof.get("laiyin_palace")
    if laiyin:
        lines.append("【Layer 4: Origin Palace】")
        lines.append(f"  Year Stem {laiyin['year_stem']} in {t_palace(laiyin['palace'])}")
        lines.append("")
    
    # 输出指令
    lines.append("=" * 40)
    lines.append("【Output Requirements】")
    lines.append("Please provide a detailed reading in English following this JSON structure:")
    lines.append("""{
  "report": {
    "metadata": { ... },
    "layers": {
      "basic": {"summary": "overall assessment", "palaces": [...]},
      "advanced": {"patterns": [..], "mutagen_analysis": "..."},
      "timing": {"current_decadal": "interpretation", "yearly": "..."},
      "professional": {"origin_analysis": "...", "body_usage": "..."}
    },
    "conclusion": "holistic assessment and suggestions"
  }
}""")
    lines.append("")
    lines.append("📜 For cultural reference only")
    
    return "\n".join(lines)


def _build_ja_full_prompt(chart_data: Dict, deep_data: Dict, target_year: int) -> str:
    """日文完整Prompt"""
    fp = chart_data.get("four_pillars", {})
    timing = deep_data.get("timing", {})
    d = timing.get("current_decadal")
    
    lines = []
    lines.append(f"📅 対象者：{fp.get('year','')} {fp.get('month','')} {fp.get('day','')} {fp.get('hour','')}")
    lines.append(f"🐉 五行：{chart_data.get('five_elements','')}")
    lines.append(f"🏠 命宮：{t_palace(chart_data.get('palaces',[{}])[0].get('name_cn',''), 'ja-JP')}")
    if d:
        d_nm = t_palace(d['palace_name'], 'ja-JP')
        lines.append(f"📍 現在の大限：{d_nm}（{d['age_start']}-{d['age_end']}歳）")
    lines.append("")
    
    # 十二宫
    lines.append("【第一層：十二宮詳細】")
    lines.append(_build_ja_basic(chart_data))
    
    # 格局
    patterns = deep_data.get("advanced", {}).get("patterns", [])
    if patterns:
        lines.append("【第二層：特殊格局】")
        for pt in patterns:
            lines.append(f"  ▪ {pt['name']}：{pt['description']}")
        lines.append("")
    
    # 大限
    if timing.get("decadal_sequence"):
        lines.append("【第三層：十年大運表】")
        for item in timing["decadal_sequence"]:
            mk = "👉" if item.get("is_current") else "  "
            pn = t_palace(item["palace_name"], "ja-JP")
            lines.append(f"  {mk} {item['age_start']}-{item['age_end']}歳 → {pn}")
        lines.append("")
    
    lines.append("=" * 40)
    lines.append("【出力要件】")
    lines.append("日本語で詳細な解釈を提供してください。")
    lines.append("JSON形式で以下の構造に従ってください。")
    lines.append("📜 文化的な参考として")
    
    return "\n".join(lines)


# ============================================================
# 端到端多语言解盘
# ============================================================

def generate_multilang_reading(
    date_str: str,
    hour: int,
    gender: str,
    lang: str = "en-US",
) -> Dict:
    """生成指定语言的完整解盘"""
    from deep_reading import full_deep_reading
    from zwds_calc import generate_astrolabe, astrolabe_to_json
    from datetime import datetime
    
    astro = generate_astrolabe(date_str, hour, gender)
    if not astro:
        return {"success": False, "error": "Chart failed"}
    
    target_year = datetime.now().year
    chart_data = astrolabe_to_json(astro)
    deep = full_deep_reading(date_str, hour, gender, target_year=target_year)
    
    if not deep["success"]:
        return {"success": False, "error": "Deep analysis failed"}
    
    prompt = build_multilang_deep_prompt(chart_data, deep, target_year, lang)
    system_prompt = SYSTEM_PROMPTS.get(lang, SYSTEM_PROMPTS["en-US"])
    
    return {
        "success": True,
        "chart_json": chart_data,
        "deep_data": deep,
        "system_prompt": system_prompt,
        "user_prompt": prompt,
        "language": lang,
    }


if __name__ == "__main__":
    # 测试
    print("=== English Prompt ===")
    r_en = generate_multilang_reading("1984-6-22", 6, "男", "en-US")
    if r_en["success"]:
        print(f"Length: {len(r_en['user_prompt'])} chars")
        print(r_en["user_prompt"][:600])
    
    print("\n\n=== Japanese Prompt ===")
    r_ja = generate_multilang_reading("1984-6-22", 6, "男", "ja-JP")
    if r_ja["success"]:
        print(f"Length: {len(r_ja['user_prompt'])} chars")
        print(r_ja["user_prompt"][:600])


path: scripts/name_analysis.py
content: """
name_analysis.py — 姓名拆字解字分析引擎
姓名学 × 紫微斗数联动分析

三合一分析：
1. 汉字拆解（部首 + 笔画 + 五行 + 含义）
2. 名字格数（天格/人格/地格/外格/总格 + 三才五行）
3. 与紫微命盘的联动分析（AI Prompt输出）

Author: 崽儿虾 🦞
"""

from typing import Dict, List, Optional, Tuple


# ============================================================
# 核心字库（常用3500+汉字部首分解）
# ============================================================

# 汉字拆解数据库（部首 + 笔画 + 五行 + 含义）
# 格式: { "汉字": { "radical": "部首", "strokes": 笔画数, "element": "五行", "meaning": "含义" } }
CHAR_DB: Dict[str, Dict[str, str]] = {
    # 常用姓氏/名字高频字
    "李": {"radical": "木", "strokes": "7", "element": "木", "meaning": "李树，果实"},
    "王": {"radical": "王", "strokes": "4", "element": "土", "meaning": "君王，统领"},
    "张": {"radical": "弓", "strokes": "7", "element": "火", "meaning": "张弓，展开"},
    "刘": {"radical": "文", "strokes": "6", "element": "火", "meaning": "杀戮，兵器"},
    "陈": {"radical": "阝", "strokes": "7", "element": "火", "meaning": "陈列，陈旧"},
    "杨": {"radical": "木", "strokes": "7", "element": "木", "meaning": "杨树，扬起"},
    "黄": {"radical": "黄", "strokes": "11", "element": "土", "meaning": "黄色，土地"},
    "赵": {"radical": "走", "strokes": "9", "element": "火", "meaning": "行走，赵国"},
    "周": {"radical": "口", "strokes": "8", "element": "金", "meaning": "周全，四周"},
    "吴": {"radical": "口", "strokes": "7", "element": "木", "meaning": "吴国"},
    "徐": {"radical": "彳", "strokes": "10", "element": "金", "meaning": "缓慢，从容"},
    "孙": {"radical": "子", "strokes": "6", "element": "水", "meaning": "子孙，晚辈"},
    "马": {"radical": "马", "strokes": "3", "element": "火", "meaning": "马匹，飞奔"},
    "胡": {"radical": "月", "strokes": "9", "element": "土", "meaning": "胡地，胡须"},
    "朱": {"radical": "木", "strokes": "6", "element": "木", "meaning": "红色，朱砂"},
    "高": {"radical": "高", "strokes": "10", "element": "木", "meaning": "高大，高处"},
    "林": {"radical": "木", "strokes": "8", "element": "木", "meaning": "树林，森林"},
    "何": {"radical": "亻", "strokes": "7", "element": "木", "meaning": "疑问，何等"},
    "郭": {"radical": "阝", "strokes": "10", "element": "木", "meaning": "城郭，外围"},
    "梁": {"radical": "木", "strokes": "11", "element": "木", "meaning": "房梁，桥"},
    "宋": {"radical": "宀", "strokes": "7", "element": "金", "meaning": "居住，宋朝"},
    "唐": {"radical": "口", "strokes": "10", "element": "火", "meaning": "唐朝，广大"},
    "郑": {"radical": "阝", "strokes": "8", "element": "火", "meaning": "郑重，郑州"},
    "谢": {"radical": "讠", "strokes": "12", "element": "金", "meaning": "感谢，凋谢"},
    "韩": {"radical": "韦", "strokes": "12", "element": "水", "meaning": "韩国，井垣"},
    "冯": {"radical": "冫", "strokes": "5", "element": "水", "meaning": "冯河，姓氏"},
    "于": {"radical": "二", "strokes": "3", "element": "土", "meaning": "在，于"},
    "董": {"radical": "艹", "strokes": "12", "element": "木", "meaning": "监督，董事"},
    "萧": {"radical": "艹", "strokes": "11", "element": "木", "meaning": "萧瑟，萧姓"},
    "程": {"radical": "禾", "strokes": "12", "element": "火", "meaning": "路程，程序"},
    "曹": {"radical": "曰", "strokes": "11", "element": "金", "meaning": "官署，朝代"},
    "袁": {"radical": "土", "strokes": "10", "element": "土", "meaning": "长衣貌"},
    "邓": {"radical": "阝", "strokes": "4", "element": "火", "meaning": "邓国"},
    "许": {"radical": "讠", "strokes": "6", "element": "木", "meaning": "允许，许愿"},
    "傅": {"radical": "亻", "strokes": "12", "element": "水", "meaning": "师傅，辅导"},
    "沈": {"radical": "氵", "strokes": "7", "element": "水", "meaning": "沉没，沈阳"},
    "曾": {"radical": "曰", "strokes": "12", "element": "金", "meaning": "曾经"},
    "彭": {"radical": "彡", "strokes": "12", "element": "水", "meaning": "鼓声"},
    "吕": {"radical": "口", "strokes": "6", "element": "火", "meaning": "吕律，姓氏"},
    "苏": {"radical": "艹", "strokes": "7", "element": "木", "meaning": "复苏，苏州"},
    "卢": {"radical": "卜", "strokes": "5", "element": "火", "meaning": "卢舍"},
    "蒋": {"radical": "艹", "strokes": "12", "element": "木", "meaning": "蒋姓"},
    "蔡": {"radical": "艹", "strokes": "14", "element": "木", "meaning": "蔡国"},
    "贾": {"radical": "贝", "strokes": "10", "element": "火", "meaning": "商人，贾姓"},
    "丁": {"radical": "一", "strokes": "2", "element": "火", "meaning": "钉子，壮丁"},
    "魏": {"radical": "鬼", "strokes": "17", "element": "木", "meaning": "魏国"},
    "薛": {"radical": "艹", "strokes": "16", "element": "木", "meaning": "草名，薛国"},
    "叶": {"radical": "口", "strokes": "5", "element": "土", "meaning": "叶子，叶片"},
    "阎": {"radical": "门", "strokes": "11", "element": "木", "meaning": "里巷门"},
    "余": {"radical": "人", "strokes": "7", "element": "土", "meaning": "剩余，我"},
    "潘": {"radical": "氵", "strokes": "15", "element": "水", "meaning": "淘米水"},
    "杜": {"radical": "木", "strokes": "7", "element": "木", "meaning": "杜树，杜绝"},
    "戴": {"radical": "戈", "strokes": "17", "element": "火", "meaning": "佩戴，爱戴"},
    "夏": {"radical": "夂", "strokes": "10", "element": "火", "meaning": "夏天，华夏"},
    "钟": {"radical": "钅", "strokes": "9", "element": "金", "meaning": "时钟，钟爱"},
    "汪": {"radical": "氵", "strokes": "7", "element": "水", "meaning": "汪洋"},
    "田": {"radical": "田", "strokes": "5", "element": "土", "meaning": "田地"},
    "任": {"radical": "亻", "strokes": "6", "element": "金", "meaning": "信任，任务"},
    "姜": {"radical": "女", "strokes": "9", "element": "木", "meaning": "生姜"},
    "范": {"radical": "艹", "strokes": "8", "element": "木", "meaning": "模范，范围"},
    "方": {"radical": "方", "strokes": "4", "element": "水", "meaning": "方向，方法"},
    "石": {"radical": "石", "strokes": "5", "element": "金", "meaning": "石头"},
    "姚": {"radical": "女", "strokes": "9", "element": "土", "meaning": "美丽"},
    "谭": {"radical": "讠", "strokes": "14", "element": "火", "meaning": "言谈，盛大"},
    "廖": {"radical": "广", "strokes": "14", "element": "火", "meaning": "空旷"},
    "邹": {"radical": "阝", "strokes": "7", "element": "火", "meaning": "邹国"},
    "熊": {"radical": "火", "strokes": "14", "element": "火", "meaning": "熊兽"},
    "金": {"radical": "金", "strokes": "8", "element": "金", "meaning": "金属，黄金"},
    "陆": {"radical": "阝", "strokes": "7", "element": "火", "meaning": "陆地，陆姓"},
    "郝": {"radical": "阝", "strokes": "9", "element": "金", "meaning": "郝氏"},
    "孔": {"radical": "子", "strokes": "4", "element": "水", "meaning": "洞穴，孔姓"},
    "白": {"radical": "白", "strokes": "5", "element": "水", "meaning": "白色，明白"},
    "崔": {"radical": "山", "strokes": "11", "element": "木", "meaning": "高山"},
    "康": {"radical": "广", "strokes": "11", "element": "木", "meaning": "安康，健康"},
    "毛": {"radical": "毛", "strokes": "4", "element": "水", "meaning": "毛发"},
    "邱": {"radical": "阝", "strokes": "7", "element": "木", "meaning": "丘氏"},
    "秦": {"radical": "禾", "strokes": "10", "element": "火", "meaning": "秦国"},
    "江": {"radical": "氵", "strokes": "6", "element": "水", "meaning": "江河"},
    "史": {"radical": "口", "strokes": "5", "element": "金", "meaning": "历史"},
    "顾": {"radical": "页", "strokes": "10", "element": "木", "meaning": "照顾，回顾"},
    "侯": {"radical": "亻", "strokes": "9", "element": "木", "meaning": "侯爵"},
    "邵": {"radical": "阝", "strokes": "7", "element": "金", "meaning": "邵氏"},
    "孟": {"radical": "子", "strokes": "8", "element": "水", "meaning": "第一，勉力"},
    "龙": {"radical": "龙", "strokes": "5", "element": "土", "meaning": "龙，帝王"},
    "万": {"radical": "一", "strokes": "3", "element": "木", "meaning": "千万"},
    "段": {"radical": "殳", "strokes": "9", "element": "金", "meaning": "阶段，分段"},
    "雷": {"radical": "雨", "strokes": "13", "element": "木", "meaning": "雷电"},
    "钱": {"radical": "钅", "strokes": "10", "element": "金", "meaning": "金钱"},
    "汤": {"radical": "氵", "strokes": "6", "element": "水", "meaning": "汤水，汤姓"},
    "尹": {"radical": "尸", "strokes": "4", "element": "金", "meaning": "治理，尹氏"},
    "赖": {"radical": "贝", "strokes": "13", "element": "木", "meaning": "依赖"},
    "易": {"radical": "日", "strokes": "8", "element": "火", "meaning": "容易，周易"},
    "洪": {"radical": "氵", "strokes": "9", "element": "水", "meaning": "洪水"},
    "贺": {"radical": "贝", "strokes": "9", "element": "水", "meaning": "祝贺"},
    "常": {"radical": "巾", "strokes": "11", "element": "金", "meaning": "经常，平常"},
    "温": {"radical": "氵", "strokes": "12", "element": "水", "meaning": "温暖"},
    "武": {"radical": "止", "strokes": "8", "element": "水", "meaning": "武力，武术"},
    "汤": {"radical": "氵", "strokes": "6", "element": "水", "meaning": "热水"},
    "文": {"radical": "文", "strokes": "4", "element": "水", "meaning": "文章，文化"},
    "南": {"radical": "十", "strokes": "9", "element": "火", "meaning": "南方"},
    "曦": {"radical": "日", "strokes": "20", "element": "火", "meaning": "阳光，晨曦"},
    "安": {"radical": "宀", "strokes": "6", "element": "土", "meaning": "安全，平安"},
    "山": {"radical": "山", "strokes": "3", "element": "土", "meaning": "山岳"},
    "水": {"radical": "水", "strokes": "4", "element": "水", "meaning": "水流"},
    "火": {"radical": "火", "strokes": "4", "element": "火", "meaning": "火焰"},
    "土": {"radical": "土", "strokes": "3", "element": "土", "meaning": "土地"},
    "木": {"radical": "木", "strokes": "4", "element": "木", "meaning": "树木"},
    "金": {"radical": "金", "strokes": "8", "element": "金", "meaning": "金属"},
    "之": {"radical": "丶", "strokes": "3", "element": "火", "meaning": "助词"},
    "子": {"radical": "子", "strokes": "3", "element": "水", "meaning": "儿子"},
    "大": {"radical": "大", "strokes": "3", "element": "火", "meaning": "大小"},
    "小": {"radical": "小", "strokes": "3", "element": "金", "meaning": "大小"},
    "天": {"radical": "大", "strokes": "4", "element": "火", "meaning": "天空"},
    "一": {"radical": "一", "strokes": "1", "element": "土", "meaning": "数字一"},
    "二": {"radical": "二", "strokes": "2", "element": "火", "meaning": "数字二"},
    "三": {"radical": "一", "strokes": "3", "element": "火", "meaning": "数字三"},
    "中": {"radical": "丨", "strokes": "4", "element": "土", "meaning": "中央"},
    "国": {"radical": "囗", "strokes": "8", "element": "木", "meaning": "国家"},
    "用": {"radical": "用", "strokes": "5", "element": "土", "meaning": "使用"},
    "心": {"radical": "心", "strokes": "4", "element": "火", "meaning": "心脏，内心"},
    "明": {"radical": "日", "strokes": "8", "element": "火", "meaning": "光明，明日"},
    "华": {"radical": "十", "strokes": "6", "element": "水", "meaning": "中华，华丽"},
    "云": {"radical": "二", "strokes": "4", "element": "水", "meaning": "云彩"},
    "月": {"radical": "月", "strokes": "4", "element": "火", "meaning": "月亮"},
    "杰": {"radical": "木", "strokes": "8", "element": "木", "meaning": "杰出，才能"},
    "伟": {"radical": "亻", "strokes": "6", "element": "土", "meaning": "伟大"},
    "强": {"radical": "弓", "strokes": "12", "element": "木", "meaning": "强大，坚强"},
    "志": {"radical": "心", "strokes": "7", "element": "火", "meaning": "意志，志向"},
    "敏": {"radical": "攵", "strokes": "11", "element": "水", "meaning": "敏捷，聪敏"},
    "慧": {"radical": "心", "strokes": "15", "element": "水", "meaning": "智慧"},
    "永": {"radical": "水", "strokes": "5", "element": "土", "meaning": "永远"},
    "泽": {"radical": "氵", "strokes": "8", "element": "水", "meaning": "光泽，恩泽"},
    "佳": {"radical": "亻", "strokes": "8", "element": "木", "meaning": "美好，佳音"},
    "丽": {"radical": "一", "strokes": "7", "element": "火", "meaning": "美丽"},
    "欣": {"radical": "欠", "strokes": "8", "element": "木", "meaning": "欣喜"},
    "悦": {"radical": "忄", "strokes": "10", "element": "金", "meaning": "喜悦"},
    "宇": {"radical": "宀", "strokes": "6", "element": "土", "meaning": "宇宙"},
    "辰": {"radical": "辰", "strokes": "7", "element": "土", "meaning": "星辰，时辰"},
    "涵": {"radical": "氵", "strokes": "11", "element": "水", "meaning": "包容，内涵"},
    "博": {"radical": "十", "strokes": "12", "element": "水", "meaning": "博学，广博"},
    "瑞": {"radical": "王", "strokes": "13", "element": "金", "meaning": "祥瑞，吉祥"},
    "瑾": {"radical": "王", "strokes": "15", "element": "火", "meaning": "美玉"},
    "瑜": {"radical": "王", "strokes": "13", "element": "金", "meaning": "美玉"},
    "昊": {"radical": "日", "strokes": "8", "element": "火", "meaning": "广阔的天空"},
    "俊": {"radical": "亻", "strokes": "9", "element": "火", "meaning": "英俊"},
    "才": {"radical": "一", "strokes": "3", "element": "金", "meaning": "才能"},
    "德": {"radical": "彳", "strokes": "15", "element": "火", "meaning": "道德，品德"},
    "仁": {"radical": "亻", "strokes": "4", "element": "金", "meaning": "仁爱"},
    "义": {"radical": "丶", "strokes": "3", "element": "木", "meaning": "义气"},
    "礼": {"radical": "礻", "strokes": "5", "element": "火", "meaning": "礼仪"},
    "智": {"radical": "日", "strokes": "12", "element": "火", "meaning": "智慧"},
    "信": {"radical": "亻", "strokes": "9", "element": "金", "meaning": "信用"},
    "雅": {"radical": "隹", "strokes": "12", "element": "木", "meaning": "优雅"},
    "乐": {"radical": "丿", "strokes": "5", "element": "火", "meaning": "快乐，音乐"},
    "健": {"radical": "亻", "strokes": "10", "element": "木", "meaning": "健康"},
    "成": {"radical": "戈", "strokes": "6", "element": "金", "meaning": "成功，成就"},
    "思": {"radical": "心", "strokes": "9", "element": "金", "meaning": "思考，思念"},
    "念": {"radical": "心", "strokes": "8", "element": "火", "meaning": "思念，念头"},
    "意": {"radical": "心", "strokes": "13", "element": "土", "meaning": "意思，意义"},
    "心": {"radical": "心", "strokes": "4", "element": "火", "meaning": "内心"},
    "晴": {"radical": "日", "strokes": "12", "element": "火", "meaning": "晴朗"},
    "朗": {"radical": "月", "strokes": "10", "element": "火", "meaning": "明亮，开朗"},
    "丽": {"radical": "一", "strokes": "7", "element": "火", "meaning": "美丽"},
    "秀": {"radical": "禾", "strokes": "7", "element": "金", "meaning": "优秀，秀丽"},
    "贤": {"radical": "贝", "strokes": "8", "element": "木", "meaning": "贤能"},
    "媛": {"radical": "女", "strokes": "12", "element": "火", "meaning": "美女"},
}

# 字形结构分类
CHAR_STRUCTURES: Dict[str, str] = {
    "明": "左右结构（日月阴阳）",
    "林": "左右结构（双木成林）",
    "从": "左右结构（两人相从）",
    "品": "品字结构（三口成品）",
    "森": "品字结构（三木成森）",
    "晶": "品字结构（三日晶莹）",
    "金": "独体字",
    "木": "独体字",
    "水": "独体字",
    "火": "独体字",
    "土": "独体字",
    "山": "独体字",
    "田": "独体字",
    "口": "独体字",
    "人": "独体字",
    "大": "独体字",
    "小": "独体字",
    "天": "上下结构",
    "宇": "上下结构（宀于覆）",
    "宙": "上下结构",
    "昊": "上下结构（日天辉映）",
    "晨": "上下结构",
    "星": "上下结构",
    "华": "上下结构",
    "杰": "上下结构",
    "志": "上下结构",
    "慧": "上下结构",
    "德": "左右结构",
    "瑞": "左右结构（玉瑞）",
    "瑾": "左右结构（玉瑾美玉）",
    "瑜": "左右结构（玉瑜）",
    "南": "上下结构",
    "曦": "左右结构（日羲日出）",
}


def decompose_char(char: str) -> Dict[str, str]:
    """拆解单字，返回部首/笔画/五行/含义"""
    result = CHAR_DB.get(char)
    if result:
        return dict(result)
    
    # 未收录的汉字：根据部首偏旁推算
    radical = _guess_radical(char)
    strokes = _count_strokes_rough(char)
    element = _guess_element_from_radical(radical)
    meaning = f"汉字「{char}」"
    
    return {
        "radical": radical,
        "strokes": str(strokes),
        "element": element,
        "meaning": meaning,
    }


def _guess_radical(char: str) -> str:
    """根据Unicode部首激进猜测（用于未收录字）"""
    common_radicals = {
        (96, 126): "氵",  (251, 310): "木",  (311, 330): "氵",
        (331, 400): "口", (401, 440): "亻",
    }
    for k, v in common_radicals.items():
        if k[0] <= ord(char) % 10 <= k[1]:
            return v
    return "—"


def _guess_element_from_radical(radical: str) -> str:
    """偏旁推五行"""
    water = "氵冫雨丷水"
    fire = "火灬心日忄"
    wood = "木艹禾竹"
    metal = "金钅刂刀"
    earth = "土石山田王"
    
    if radical in water: return "水"
    if radical in fire: return "火"
    if radical in wood: return "木"
    if radical in metal: return "金"
    if radical in earth: return "土"
    return "金"  # 默认


ELEMENTS_CYCLE = ["金", "水", "木", "火", "土"]


def element_relation(e1: str, e2: str) -> str:
    """五行关系：相生/相克/相同"""
    if e1 == e2: return "相同"
    gen = {"金": "水", "水": "木", "木": "火", "火": "土", "土": "金"}
    ke = {"金": "木", "木": "土", "土": "水", "水": "火", "火": "金"}
    if gen.get(e1) == e2: return "相生"
    if ke.get(e1) == e2: return "相克"
    return "—"


def _count_strokes_rough(char: str) -> int:
    """粗略笔画估算（用于未收录字）"""
    cp = ord(char)
    if cp >= 0x4E00 and cp <= 0x9FFF:
        return (cp % 20) + 1
    return 8


# ============================================================
# 五格数理（姓名学核心）
# ============================================================

# 1-81 吉凶判断
NUMBER_JIXIONG: Dict[int, str] = {
    # 吉数
    1: "大吉", 2: "凶", 3: "大吉", 4: "凶", 5: "大吉", 6: "大吉",
    7: "大吉", 8: "大吉", 9: "凶", 10: "凶", 11: "大吉", 12: "凶",
    13: "大吉", 14: "凶", 15: "大吉", 16: "大吉", 17: "大吉",
    18: "大吉", 19: "凶", 20: "凶", 21: "大吉", 22: "凶",
    23: "大吉", 24: "大吉", 25: "大吉", 26: "凶", 27: "吉",
    28: "凶", 29: "大吉", 30: "凶", 31: "大吉", 32: "大吉",
    33: "大吉", 34: "凶", 35: "大吉", 36: "凶", 37: "大吉",
    38: "凶", 39: "大吉", 40: "凶", 41: "大吉", 42: "凶",
    43: "凶", 44: "凶", 45: "大吉", 46: "凶", 47: "大吉",
    48: "大吉", 49: "凶", 50: "凶", 51: "吉", 52: "大吉",
    53: "凶", 54: "凶", 55: "吉", 56: "凶", 57: "大吉",
    58: "吉", 59: "凶", 60: "凶", 61: "大吉", 62: "凶",
    63: "大吉", 64: "凶", 65: "大吉", 66: "凶", 67: "大吉",
    68: "吉", 69: "凶", 70: "凶", 71: "吉", 72: "凶",
    73: "吉", 74: "凶", 75: "吉", 76: "凶", 77: "吉",
    78: "吉", 79: "凶", 80: "凶", 81: "大吉",
}


def compute_wuge(full_name: str) -> Dict[str, Dict]:
    """
    计算五格数理（姓名学）
    
    假设格式: "姓氏" (1字) + "名字" (1-2字)
    总格 = 姓名总笔画
    天格 = 姓氏笔画 + 1（单姓）
    人格 = 姓氏笔画 + 名字第一字笔画
    地格 = 名字笔画和（多字）或 名字+1（单名）
    外格 = 总格 - 人格 + 1
    
    Returns:
        { "tiange": {"num": 天格, "jixiong": "吉/凶"}, 
          "renge": ..., "dige": ..., "waige": ..., "zongge": ...,
          "wuxing_sancai": "五行三才" }
    """
    if not full_name or len(full_name) < 2:
        return {}
    
    chars = list(full_name.strip())
    strokes = []
    for c in chars:
        info = decompose_char(c)
        strokes.append(int(info.get("strokes", 8)))
    
    # 姓氏笔画
    surname_strokes = strokes[0]
    # 名字笔画
    given_strokes = strokes[1:]
    
    # 五格
    zongge = sum(strokes)  # 总格
    tiange = surname_strokes + 1  # 天格（单姓）
    renge = surname_strokes + (given_strokes[0] if given_strokes else 0)  # 人格
    if len(given_strokes) >= 2:
        dige = given_strokes[0] + given_strokes[1]  # 地格（双字名）
    elif len(given_strokes) == 1:
        dige = given_strokes[0] + 1  # 地格（单名）
    else:
        dige = 0
    waige = zongge - renge + 1  # 外格
    
    # 五行三才（人格/地格/外格的五行）
    renge_element = CHAR_DB.get(chars[0], {}).get("element", "土") if len(chars) > 0 else "土"
    dige_element = CHAR_DB.get(chars[1], {}).get("element", "土") if len(chars) > 1 else "土"
    
    # 三才关系
    relation_rg_dg = element_relation(renge_element, dige_element)
    
    result = {
        "tiange": {"num": tiange, "jixiong": NUMBER_JIXIONG.get(tiange % 81, "—")},
        "renge": {"num": renge, "jixiong": NUMBER_JIXIONG.get(renge % 81, "—")},
        "dige": {"num": dige, "jixiong": NUMBER_JIXIONG.get(dige % 81, "—")},
        "waige": {"num": waige, "jixiong": NUMBER_JIXIONG.get(waige % 81, "—")},
        "zongge": {"num": zongge, "jixiong": NUMBER_JIXIONG.get(zongge % 81, "—")},
        "wuxing_sancai": {
            "renge_element": renge_element,
            "dige_element": dige_element,
            "relation": relation_rg_dg,
        },
        "raw_chars": chars,
        "raw_strokes": strokes,
    }
    
    return result


# ============================================================
# 姓名拆解 & AI Prompt 生成
# ============================================================


def analyze_name(name: str) -> Dict:
    """
    全姓名分析：拆字 + 五格 + 五行
    
    Returns:
        { "name": 姓名,
          "chars": [{ char, radical, strokes, element, meaning, structure }, ...],
          "wuge": { 五格 },
          "summary": "综合判断",
          "ai_prompt": "AI解读Prompt"
        }
    """
    if not name or len(name.strip()) < 2:
        return {"success": False, "error": "姓名至少需要2个汉字"}
    
    name = name.strip()
    chars = []
    for c in name:
        info = decompose_char(c)
        structure = CHAR_STRUCTURES.get(c, "—")
        chars.append({
            "char": c,
            "radical": info.get("radical", "—"),
            "strokes": int(info.get("strokes", 8)),
            "element": info.get("element", "土"),
            "meaning": info.get("meaning", f"汉字「{c}」"),
            "structure": structure,
        })
    
    wuge = compute_wuge(name)
    if not wuge:
        wuge = {}
    
    # 五行分析
    elements_list = [c["element"] for c in chars]
    element_summary = _summarize_element_distribution(elements_list)
    
    summary = _build_name_summary(chars, wuge, element_summary)
    ai_prompt = _build_name_prompt(name, chars, wuge, element_summary)
    
    return {
        "success": True,
        "name": name,
        "chars": chars,
        "wuge": wuge,
        "element_summary": element_summary,
        "summary": summary,
        "ai_prompt": ai_prompt,
    }


def _summarize_element_distribution(elements: list) -> Dict:
    """五行分布总结"""
    counts = {}
    for e in elements:
        counts[e] = counts.get(e, 0) + 1
    
    dominant = max(counts, key=counts.get) if counts else "—"
    
    # 五行平衡判断
    num_types = len(counts)
    if num_types >= 3:
        balance = "五行搭配丰富"
    elif num_types == 2:
        balance = "双行组合"
    else:
        balance = "单行主导（偏强）"
    
    return {
        "counts": counts,
        "dominant": dominant,
        "balance": balance,
    }


def _build_name_summary(chars: list, wuge: dict, element_summary: dict) -> str:
    """姓名综合摘要（Telegram友好）"""
    parts = []
    
    # 拆字
    char_desc = " | ".join(
        f"{c['char']}(部首:{c['radical']} 笔画:{c['strokes']} 五行:{c['element']})"
        for c in chars
    )
    parts.append(f"✍️ 拆字：{char_desc}")
    
    # 五格
    if wuge:
        wuge_line = " | ".join(
            f"{k}:{v.get('num','?')}({v.get('jixiong','—')})"
            for k, v in wuge.items()
            if isinstance(v, dict) and "num" in v
        )
        parts.append(f"📊 五格：{wuge_line}")
        
        sancai = wuge.get("wuxing_sancai", {})
        if sancai:
            parts.append(f"🔮 三才：人格:{sancai.get('renge_element','?')} 地格:{sancai.get('dige_element','?')} 关系:{sancai.get('relation','?')}")
    
    # 五行
    parts.append(f"🔥 五行：{element_summary.get('balance','')}（{element_summary.get('dominant','')}主导）")
    
    return "\n".join(parts)


def _build_name_prompt(name: str, chars: list, wuge: dict, element_summary: dict) -> str:
    """生成姓名AI解读Prompt"""
    lines = [
        f"姓名分析：{name}",
        "",
        "【汉字拆解】",
    ]
    
    for c in chars:
        lines.append(f"▪ {c['char']}：部首「{c['radical']}」{c['strokes']}画，五行属{c['element']}。结构：{c['structure']}。{c['meaning']}。")
    
    if wuge:
        lines.append("")
        lines.append("【五格数理】")
        for k, v in wuge.items():
            if isinstance(v, dict) and "num" in v:
                lines.append(f"▪ {k}：{v['num']}画 — {v['jixiong']}")
        sancai = wuge.get("wuxing_sancai", {})
        if sancai:
            lines.append(f"▪ 三才：人格属{sancai.get('renge_element','?')}，地格属{sancai.get('dige_element','?')}，{sancai.get('relation','?')}")
    
    lines.append("")
    lines.append("【姓名对人生的影响分析】")
    lines.append("请从以下角度分析这个姓名：")
    lines.append("1. 汉字拆解含义对性格的暗示")
    lines.append("2. 五格数理吉凶对命运的影响")
    lines.append("3. 五行属性与名字力量的关系")
    lines.append("4. 整体名字的能量平衡评估")
    lines.append("5. 建议（需改名/无需调整/可加强什么）")
    lines.append("")
    lines.append("📜 文化参考，理性看待")
    
    return "\n".join(lines)


def format_name_analysis_for_tg(data: dict) -> str:
    """Telegram格式输出姓名分析"""
    if not data.get("success"):
        return f"❌ {data.get('error', '分析失败')}"
    
    lines = [
        f"📛 *姓名拆解 · {data['name']}*",
        "",
        "✍️ *逐字拆解*",
    ]
    
    for c in data["chars"]:
        lines.append(f"▪ **{c['char']}**：部首「{c['radical']}」{c['strokes']}画，五行【{c['element']}】")
        lines.append(f"  {c['meaning']}（{c['structure']}）")
    
    wuge = data.get("wuge", {})
    if wuge:
        lines.append("")
        lines.append("📊 *五格数理*")
        for k, v in wuge.items():
            if isinstance(v, dict) and "num" in v:
                names = {"tiange":"天格", "renge":"人格", "dige":"地格", "waige":"外格", "zongge":"总格"}
                label = names.get(k, k)
                ji = v.get("jixiong", "—")
                emoji = "✅" if "吉" in ji else "⚠️" if "凶" in ji else "➖"
                lines.append(f"▪ {emoji} {label}：{v['num']}画 — {ji}")
        
        sancai = wuge.get("wuxing_sancai", {})
        if sancai:
            rel = sancai.get("relation", "?")
            lines.append(f"▪ 🔮 三才关系：人格【{sancai.get('renge_element','?')}】× 地格【{sancai.get('dige_element','?')}】→ {rel}")
    
    es = data.get("element_summary", {})
    if es:
        lines.append("")
        lines.append(f"🔥 *五行能量*：{es.get('balance','')}（{es.get('dominant','')}主导）")
    
    if data.get("summary"):
        lines.append("")
        lines.append(data["summary"])
    
    lines.append("")
    lines.append("📜 文化参考，理性看待")
    
    return "\n".join(lines)


path: scripts/prompts.py
content: """
prompts.py — 一念紫微斗数 结构化AI解盘Prompt体系
四层Prompt：基础→进阶→高阶→专业
适配Claude/GPT（主）+ DeepSeek（备）

Author: 崽儿虾 🦞
"""

# ============================================================
# 系统提示词（各流派）
# ============================================================

SYSTEM_CORE = """你是一位精通紫微斗数的顶尖命理师，擅长「三派合一」综合解读。

【核心能力】
1. 三合派（中州派）：星曜性质 × 亮度 × 三方四正
2. 飞星派（钦天门）：宫干四化脉络 × 自化 × 追禄追忌
3. 占验派（紫云）：太岁入卦 × 特殊格局 × 星曜互涉

【输出铁律】
- 每层用【】标题，清晰分段
- 使用可能性语气，禁止绝对化断言
- 专业术语搭配白话解释
- 每段结尾标注"📜 文化参考，理性看待"
"""

SYSTEM_SANHE = SYSTEM_CORE + """
【三合派专用原则】
- 星曜亮度：庙>旺>得>利>平>陷
- 每宫必看：主星性质 + 三方四正联动 + 空宫借对宫
- 星组判断：紫府相(贵气)、杀破狼(变动)、机月同梁(才艺)
"""

SYSTEM_FEIXING = SYSTEM_CORE + """
【飞星派专用原则】
- 宫干四化：该宫天干向各宫飞化
- 自化：本宫天干四化入本宫
- 来因宫：年干所在宫位定轨迹起点
"""

SYSTEM_ZHANYAN = SYSTEM_CORE + """
【占验派专用原则】
- 太岁入卦：年干定位长期趋势
- 特殊格局：杀破狼格、紫府相格、日月同宫格等
"""

# ============================================================
# 四层Prompt构建器
# ============================================================

from typing import Dict, Any, Optional, List
from zwds_calc import AstrolabeResult, compute_surrounded


def build_basic_prompt(data: Dict[str, Any]) -> str:
    """第一层：基础逐宫解读"""
    lines = []
    lines.append("【第一层：基础宫位解读】")
    lines.append("请对以下命盘逐宫详细解读：")
    lines.append("")

    for p in data["palaces"]:
        ms = " ".join(
            f"{s['name']}({s['brightness']})" + (f"→{s['mutagen']}" if s.get('mutagen') else "")
            for s in p["major_stars"]
        )
        mins = " ".join(s["name"] for s in p["minor_stars"])
        empty = "【空宫（需借对宫星曜论断）】" if p["is_empty"] else ""

        lines.append(f"▪ {p['name_cn']}（{p['heavenly_stem']}{p['earthly_branch']}）")
        if p["major_stars"]:
            lines.append(f"  主星：{ms}")
        else:
            lines.append(f"  {empty}")
        if mins:
            lines.append(f"  辅星：{mins}")

        # 三方四正
        sur = compute_surrounded(p["index"])
        sur_names = [data["palaces"][i]["name_cn"] for i in sur]
        lines.append(f"  三方四正联：{'、'.join(sur_names)}")
        lines.append("")

    lines.append("【解读要求】")
    lines.append("• 每宫：主星性质 → 亮度影响 → 辅星增减 → 三方联动 → 整体判断")
    lines.append("• 空宫：说明借对宫哪颗星，以及借星后的格局")
    lines.append("• 关注星曜组合效应而非孤星论")
    lines.append("")

    return "\n".join(lines)


def build_advanced_prompt(data: Dict[str, Any]) -> str:
    """第二层：四化飞星与格局"""
    lines = []
    lines.append("【第二层：四化飞星与格局】")
    lines.append("")

    # 生年四化
    if data.get("mutagens"):
        lines.append("【生年四化分析】")
        mutagen_meanings = {
            "禄": ("增加·融合·顺遂", "该宫主题和谐顺畅，资源丰沛，为人生福气所在"),
            "权": ("主导·掌控·权威", "该宫主题主控性强，需担当但有压力，为人生发力所在"),
            "科": ("文雅·名声·调和", "该宫主题有文化气息，名声佳，为人缘加分所在"),
            "忌": ("缺失·纠葛·收缩", "该宫主题有缺憾，需格外经营，为人生的课题所在"),
        }
        for m in data["mutagens"]:
            meaning, detail = mutagen_meanings.get(m["mutagen"], ("", ""))
            lines.append(f"  {m['palace']}：{m['star']}化{m['mutagen']} → {meaning}")
            lines.append(f"    {detail}")
        lines.append("")

    # 来因宫
    if data.get("laiyin_palace"):
        lp = data["laiyin_palace"]
        lines.append(f"【来因宫】{lp['palace']}（年干{lp['year_stem']}定位）")
        lines.append(f"  人生轨迹的起点与方向由{lp['palace']}主题统领")
        lines.append("")

    # 格局识别
    if data.get("patterns"):
        lines.append("【特殊格局】")
        for pt in data["patterns"]:
            lines.append(f"  ▪ {pt['name']}：{pt['description']}")
        lines.append("")

    lines.append("【飞星四化脉络】")
    lines.append("  请分析以下四化脉络：")
    lines.append("  1. 禄权科忌在各宫之间的串联轨迹")
    lines.append("  2. 化忌的宫位是人生需要经营的课题")
    lines.append("  3. 化禄的宫位是人生福气与资源的所在")
    lines.append("  4. 四化飞星体现命运走势的强弱转换")
    lines.append("")

    lines.append("【六亲宫联动】")
    if data.get("six_relations"):
        for rel in data["six_relations"]:
            ms = "、".join(rel["major_stars"]) if rel["major_stars"] else "空宫"
            th = "、".join(rel["three_combined"])
            lines.append(f"  ▪ {rel['relation']}（{rel['palace']}）：{ms}  联动：{th}")
    lines.append("")

    return "\n".join(lines)


def build_timing_prompt(data: Dict[str, Any]) -> str:
    """第三层：大限流年"""
    lines = []
    lines.append("【第三层：大限与流年】")
    lines.append("")

    timing = data.get("timing", {})
    d = timing.get("current_decadal")
    if d:
        lines.append(f"【当前大限】{d['palace_name']}（{d['age_start']}-{d['age_end']}岁）")
        dp = timing.get("current_decadal_palace", {})
        if dp:
            ms = "、".join(s["name"] for s in dp.get("major", []))
            if ms:
                lines.append(f"  大限宫主星：{ms}")

            # 大限宫位在命盘上的星曜组合
            lines.append("")
            lines.append("  分析要点：")
            lines.append(f"  · 当前大限在{d['palace_name']}，该宫星曜组合主导未来十年")
            lines.append(f"  · 大限宫的三方四正联动宫位分析")
            lines.append(f"  · 大限四化对该宫位带来的影响")
        lines.append("")

    # 大限表
    if timing.get("decadal_sequence"):
        lines.append("【十年大运表】")
        for item in timing["decadal_sequence"]:
            mk = "👉" if item.get("is_current") else "  "
            lines.append(f"  {mk} {item['age_start']}-{item['age_end']}岁 → {item['palace_name']}")
        lines.append("")

    # 流年
    lines.append(f"【当前流年】{timing.get('current_year', '')}年（地支{timing.get('yearly_branch', '')}）")
    lines.append("  叠宫分析：大限宫位 × 流年地支宫位")
    lines.append("  · 流年地支对应的宫位星曜")
    lines.append("  · 大限与流年叠宫效应")
    lines.append("")

    return "\n".join(lines)


def build_professional_prompt(data: Dict[str, Any]) -> str:
    """第四层：专业深度"""
    lines = []
    lines.append("【第四层：专业深度解析】")
    lines.append("")

    # 体用宫
    ty = data.get("tiyong_palaces", {})
    if ty.get("body"):
        lines.append("【体用宫系统】")
        for b in ty["body"]:
            lines.append(f"  ▪ {b['name']}（{b['role']}）")
        lines.append(f"  分析：{ty.get('usage_analysis', '')}")
        lines.append("")

    # 煞星分布
    if data.get("sha_locations"):
        lines.append("【煞星分布与影响】")
        for si in data["sha_locations"]:
            lines.append(f"  ▪ {si['star']}坐{si['palace']}")
            impact = {
                "擎羊": "刑伤、冲突突破",
                "陀罗": "拖延、纠缠反复",
                "火星": "急躁、爆发式变动",
                "铃星": "暗斗、慢性纠纷",
                "地空": "理想化、空亡感",
                "地劫": "破耗、波折中断",
            }.get(si["star"], "凶星影响")
            lines.append(f"    影响：{impact}")
        lines.append("")

    lines.append("【三派综合论断】")
    lines.append("  请融合三派视角给出综合判断：")
    lines.append("  · 三合派：最重要的星曜组合与格局")
    lines.append("  · 飞星派：关键的四化脉络与自化学")
    lines.append("  · 占验派：时间维度上的引动节点")
    lines.append("")

    return "\n".join(lines)


# ============================================================
# 完整四层解盘Prompt组装
# ============================================================

def build_full_deep_prompt(
    chart_data: Dict[str, Any],
    deep_data: Dict[str, Any],
    target_year: int,
) -> str:
    """组装四层完整AI解盘Prompt"""
    lines = []

    # 命主信息
    fp = chart_data.get("four_pillars", {})
    lines.append(f"📅 命主：{fp.get('year', '')} {fp.get('month', '')} {fp.get('day', '')} {fp.get('hour', '')}")
    lines.append(f"🐉 生肖：{chart_data.get('zodiac', '')}　五行：{chart_data.get('five_elements', '')}")
    lines.append(f"🏠 命宫：{chart_data.get('palaces', [{}])[0].get('name_cn', '')}")
    lines.append("")

    # 整合deep_data中的数据
    basic_data = chart_data
    advanced_data = deep_data.get("advanced", {})
    timing_data = deep_data.get("timing", {})
    professional_data = deep_data.get("professional", {})
    
    # 把prof/adv数据注入
    basic_data["laiyin_palace"] = professional_data.get("laiyin_palace")
    basic_data["patterns"] = advanced_data.get("patterns", [])
    basic_data["six_relations"] = advanced_data.get("six_relations", [])
    basic_data["tiyong_palaces"] = professional_data.get("tiyong_palaces")
    basic_data["sha_locations"] = professional_data.get("star_interactions", [])
    basic_data["timing"] = timing_data

    # 逐层拼接
    sections = [
        ("第一层", build_basic_prompt(basic_data)),
        ("第二层", build_advanced_prompt(basic_data)),
        ("第三层", build_timing_prompt(basic_data)),
        ("第四层", build_professional_prompt(basic_data)),
    ]

    for name, section_text in sections:
        lines.append(section_text)

    # 最终指令
    lines.append("=" * 40)
    lines.append("【最终输出要求】")
    lines.append("请严格按照以下JSON格式输出解盘报告：")
    lines.append("""
{
  "report": {
    "metadata": {
      "four_pillars": "甲子 庚午 丁亥 丙午",
      "zodiac": "鼠",
      "five_elements": "水二局",
      "target_year": 2026,
      "current_decadal": "官禄宫 (42-51岁)"
    },
    "layers": {
      "basic": {
        "summary": "命盘整体格局简要判断",
        "palaces": [
          {
            "name": "命宫",
            "soul_stars": "紫微(平)",
            "interpretation": "紫微帝星独坐命宫，性格稳重有贵气...",
            "key_points": ["稳重有领导风范", "三方联动暗示..."]
          }
        ]
      },
      "advanced": {
        "mutagen_analysis": "生年四化脉络分析...",
        "patterns": ["杀破狼格局"],
        "six_relations": {"父母宫": "...", "夫妻宫": "..."}
      },
      "timing": {
        "current_decadal": "当前大限在官禄宫，星象暗示...",
        "yearly": "今年流年运势..."
      },
      "professional": {
        "laiyin_analysis": "来因宫分析...",
        "tiyong_analysis": "体用宫判断..."
      }
    },
    "conclusion": "综合判断与建议"
  }
}
""")
    lines.append("")
    lines.append("📜 文化参考，理性看待")

    return "\n".join(lines)


# ============================================================
# 模型适配器
# ============================================================

def adapt_prompt_for_model(prompt: str, model_type: str = "claude") -> str:
    """
    根据不同模型调整Prompt格式
    
    Args:
        model_type: "claude" | "gpt" | "deepseek"
    """
    if model_type == "deepseek":
        # DeepSeek对JSON结构更敏感
        prompt = prompt.replace("【", "<").replace("】", ">")
        prompt += "\n\n请确保输出为可直接解析的JSON格式。"
    elif model_type == "gpt":
        # GPT更喜欢结构化Markdown
        pass  # 默认格式就适合
    # Claude默认格式即Markdown最优
    
    return prompt


if __name__ == "__main__":
    # 测试构建prompt
    from deep_reading import full_deep_reading
    from zwds_calc import astrolabe_to_json, generate_astrolabe
    
    astro = generate_astrolabe("1984-6-22", 6, "男")
    deep = full_deep_reading("1984-6-22", 6, "男")
    
    if astro and deep["success"]:
        chart_data = astrolabe_to_json(astro)
        prompt = build_full_deep_prompt(chart_data, deep, 2026)
        print(f"✅ Prompt构建完成（{len(prompt)}字）")
        print(prompt[:1500])
        print(f"\n...（截断，共{len(prompt)}字）")


path: scripts/tg_bot_handler.py
content: """
tg_bot_handler.py — 一念紫微斗数 Telegram Bot 消息处理器 v2
支持命令：/zwds /read /reading /yearly /monthly /star /help /about

Author: 崽儿虾 🦞
"""

import sys
import os
import re
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from zwds_calc import generate_astrolabe, format_astrolabe, astrolabe_to_json
from formats import format_chart_for_telegram, build_tg_menu, get_star_info, STAR_ENCYCLOPEDIA

from ai_engine import (
    full_reading_with_yearly,
    generate_yearly_reading,
    generate_monthly_reading,
)
from ai_reader import create_chart_and_reading, read_astrolabe
from name_analysis import analyze_name, format_name_analysis_for_tg


def parse_birth_input(text: str) -> dict:
    """
    解析用户输入的生辰信息
    
    支持格式:
    /zwds 2000-8-16 6 男
    /zwds 2000-8-16 6 男 综合
    /zwds 2000-8-16 6 男 --deep
    /reading 2000-8-16 6 男
    /yearly 1984-6-22 6 男 2026
    /monthly 1984-6-22 6 男 2026 4
    2000-8-16 6 男
    """
    text = text.strip()
    is_lunar = False
    is_deep = False
    deep_flag = False
    
    # 去掉命令前缀
    for prefix in ["/reading", "/yearly", "/monthly", "/zwds_lunar", "/zwdslunar", "zwds_lunar", "/zwds", "/read"]:
        if text.lower().startswith(prefix.lower()):
            text = text[len(prefix):].strip()
            if "lunar" in prefix:
                is_lunar = True
            break
    
    # 检查 --deep 标志
    if " --deep" in text or " -d" in text:
        is_deep = True
        text = re.sub(r" --deep|-d", "", text)
    
    if not text:
        return {"success": False, "error": "请输入出生信息。格式：年月日 时辰 性别\n例如：2000-8-16 6 男"}
    
    parts = text.split()
    
    # 提取日期
    date_re = re.search(r"(\d{4}[-/]\d{1,2}[-/]\d{1,2})", text)
    if not date_re:
        return {"success": False, "error": "日期格式错误。请使用 YYYY-MM-DD 格式，例如：2000-8-16"}
    
    date_str = date_re.group(1).replace("/", "-")
    
    # 提取时辰
    hour = 12  # 默认午时
    hour_re = re.search(r"(\d{1,2})", text[text.find(date_str) + len(date_str):])
    if hour_re:
        hour = int(hour_re.group(1))
        if hour < 0 or hour > 23:
            return {"success": False, "error": "时辰必须在 0-23 之间"}
    
    # 提取性别
    gender = "男"  # 默认
    if "女" in text or "female" in text.lower() or "f" in text.lower():
        gender = "女"
    
    # 提取流派和deep标记
    school = "综合"
    for s in ["三合", "飞星", "占验", "综合"]:
        if s in text.replace("--deep", "").replace("-d", ""):
            school = s
            break
    
    # 提取姓名（针对 /name 命令或 name= 参数）
    name_str = ""
    name_match = re.search(r'name=([\u4e00-\u9fff]+)', text)
    if name_match:
        name_str = name_match.group(1)
    
    return {
        "success": True,
        "date": date_str,
        "hour": hour,
        "gender": gender,
        "is_lunar": is_lunar,
        "school": school,
        "deep": is_deep,
    }


def handle_zwds_command(text: str) -> str:
    """处理 /zwds 命令：排盘 + 可选AI解盘"""
    parsed = parse_birth_input(text)
    if not parsed["success"]:
        return f"❌ {parsed['error']}\n\n试试 /help 查看使用说明"
    
    # 如果带 --deep 标志，走AI解盘
    if parsed.get("deep"):
        from ai_engine import full_reading_with_yearly
        result = full_reading_with_yearly(
            parsed["date"], parsed["hour"], parsed["gender"], parsed["is_lunar"]
        )
        if result.get("success"):
            return result.get("telegram_text", "")
        return f"❌ 解盘失败: {result.get('error', '未知错误')}"
    
    # 普通排盘
    try:
        result = create_chart_and_reading(
            date_str=parsed["date"],
            hour=parsed["hour"],
            gender=parsed["gender"],
            is_lunar=parsed["is_lunar"],
            school=parsed["school"],
        )
        if not result["success"]:
            return f"❌ 排盘失败：{result['error']}"
        
        chart_tg = format_chart_for_telegram(result["chart_json"])
        
        school_label = {
            "三合": "三合派",
            "飞星": "飞星派",
            "占验": "占验派",
            "综合": "三派合一",
        }.get(parsed["school"], "综合")
        
        output = (
            f"🔮 *一念紫微斗数 · {school_label}*\n"
            f"📅 {parsed['date']} 时{parsed['hour']} {parsed['gender']}\n"
            f"{'（农历' if parsed['is_lunar'] else ''}"
        )
        
        max_chars = 3800
        full_text = f"{chart_tg}\n\n💡 *提示*：/reading 获取AI深度解盘（含流年流月）"
        
        if len(full_text) > max_chars:
            full_text = full_text[:max_chars] + "\n\n⋯ （命盘超出显示限制）"
        
        return full_text
    except Exception as e:
        return f"❌ 处理异常：{str(e)}"


def handle_reading_command(text: str) -> str:
    """处理 /reading 命令：AI完整四层解盘 + 流年流月"""
    parsed = parse_birth_input(text)
    if not parsed["success"]:
        return f"❌ {parsed['error']}"
    
    result = full_reading_with_yearly(
        parsed["date"], parsed["hour"], parsed["gender"], parsed["is_lunar"]
    )
    if result.get("success"):
        return result.get("telegram_text", "")
    return f"❌ 解盘失败: {result.get('error', '未知错误')}"


def handle_yearly_command(text: str) -> str:
    """处理 /yearly 命令：流年运势"""
    parsed = parse_birth_input(text)
    if not parsed["success"]:
        return f"❌ {parsed['error']}"
    
    from datetime import datetime
    target_year = datetime.now().year
    
    # 提取用户指定的年份
    for part in text.split():
        if part.isdigit() and len(part) == 4 and 1900 < int(part) < 2100:
            target_year = int(part)
            break
    
    result = generate_yearly_reading(
        parsed["date"], parsed["hour"], parsed["gender"], target_year
    )
    if result.get("success"):
        lines = []
        lines.append(f"📅 *{target_year}年流年运势*")
        lines.append(f"📍 宫位：{result['palace']}（地支{result['year_branch']}）")
        lines.append(f"📍 大限叠宫：{result['overlay']}")
        lines.append("")
        lines.append(result.get("analysis", ""))
        lines.append("")
        lines.append("📜 文化参考，理性看待")
        return "\n".join(lines)
    return f"❌ 流年分析失败: {result.get('error')}"


def handle_monthly_command(text: str) -> str:
    """处理 /monthly 命令：流月运势"""
    parsed = parse_birth_input(text)
    if not parsed["success"]:
        return f"❌ {parsed['error']}"
    
    from datetime import datetime
    now = datetime.now()
    target_year = now.year
    target_month = now.month
    
    parts = text.split()
    for i, p in enumerate(parts):
        if p.isdigit() and len(p) == 4 and 1900 < int(p) < 2100:
            target_year = int(p)
            if i + 1 < len(parts) and parts[i+1].isdigit() and 1 <= int(parts[i+1]) <= 12:
                target_month = int(parts[i+1])
    
    result = generate_monthly_reading(
        parsed["date"], parsed["hour"], parsed["gender"], target_year, target_month
    )
    if result.get("success"):
        lines = []
        lines.append(f"🌙 *{target_year}年{target_month}月流月运势*")
        lines.append(f"📍 宫位：{result['palace_name']}（地支{result['month_branch']}）")
        lines.append(f"⭐ 星曜：{result['major_stars']}")
        lines.append("")
        lines.append(result.get("analysis", ""))
        lines.append("")
        lines.append("📜 文化参考，理性看待")
        return "\n".join(lines)
    return f"❌ 流月分析失败: {result.get('error')}"


def handle_read_command(chart_json: dict) -> str:
    """处理 /read 命令：用AI解盘（旧版，保留兼容）"""
    if not chart_json or not chart_json.get("palaces"):
        return "❌ 请先排盘（/zwds），再使用 /read 解盘"
    
    prompt = read_astrolabe(chart_json)
    return prompt


def handle_star_command(star_name: str) -> str:
    """处理 /star 命令：查询星曜"""
    if not star_name:
        stars = "、".join(list(STAR_ENCYCLOPEDIA.keys())[:7])
        stars += "\n" + "、".join(list(STAR_ENCYCLOPEDIA.keys())[7:])
        return f"⭐ *星曜百科*\n\n可查询：\n{stars}\n\n使用 /star 星名 查看详情"
    
    info = get_star_info(star_name)
    if info:
        return info
    return f"❌ 未找到星曜「{star_name}」"


def handle_help() -> str:
    """返回帮助文本"""
    return build_tg_menu()


def handle_reading_english(text: str) -> str:
    """处理 /read_en 命令：英文AI解盘"""
    from multilang_prompts import generate_multilang_reading
    
    parsed = parse_birth_input(text)
    if not parsed["success"]:
        return f"❌ {parsed['error']}"
    
    result = generate_multilang_reading(
        parsed["date"], parsed["hour"], parsed["gender"], "en-US"
    )
    
    if result["success"]:
        lines = [
            "🔮 *Yinian ZWDS · English Reading*\n",
            f"📅 Four Pillars: {result['chart_json'].get('four_pillars', {}).get('year','')}\n",
        ]
        lines.append("\n📜 For cultural reference only")
        return "\n".join(lines) + "\n\n(Send this prompt to your AI to get the full English interpretation)"
    return f"❌ {result.get('error')}"


def handle_matching_command(text: str) -> str:
    """处理 /match 命令：合盘匹配"""
    from matching import compute_match, format_match_for_tg
    import re
    
    # 格式：/match 2000-8-16 6 男 / 1995-3-20 8 女
    # 查找两个日期模式
    dates = re.findall(r'(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}\s+[男女])', text)
    
    if len(dates) < 2:
        return "❌ 合盘需要两人信息。格式：/match 2000-8-16 6 男 / 1995-3-20 8 女"
    
    p1 = parse_birth_input(dates[0])
    p2 = parse_birth_input(dates[1])
    
    if not p1["success"] or not p2["success"]:
        return f"❌ 格式错误。正确格式：/match 2000-8-16 6 男 / 1995-3-20 8 女"
    
    result = compute_match(
        p1["date"], p1["hour"], p1["gender"],
        p2["date"], p2["hour"], p2["gender"],
    )
    
    return format_match_for_tg(result)


def handle_name_command(name: str) -> str:
    """处理 /name 命令：姓名拆解分析"""
    if not name or len(name.strip()) < 2:
        return "❌ 请输入姓名。格式：/name 张三\n\n示例：/name 南曦"
    
    import re
    if not re.match(r'^[\u4e00-\u9fff]{2,8}$', name.strip()):
        return "❌ 姓名仅支持2-8个中文字符。格式：/name 张三"
    
    result = analyze_name(name.strip())
    return format_name_analysis_for_tg(result)


def handle_daily_command(text: str) -> str:
    """处理 /daily 命令：日运势"""
    from daily_push import generate_daily_fortune, format_daily_for_tg
    
    parsed = parse_birth_input(text)
    if not parsed["success"]:
        return f"❌ {parsed['error']}"
    
    result = generate_daily_fortune(
        parsed["date"], parsed["hour"], parsed["gender"]
    )
    
    if result["success"]:
        lines = [
            f"☀️ *{result['date']} · 日运势*\n",
            f"📍 宫位：{result['palace_name']}（地支{result['day_branch']}）",
            f"⭐ 星曜：{result['major_stars']}",
            f"🔗 大限：{result['decadal']}\n",
            result.get("ai_prompt", "").split("请给出")[0].strip(),
            "",
            "📜 文化参考，理性看待",
        ]
        return "\n".join(lines)
    return f"❌ {result.get('error')}"


def handle_about() -> str:
    """关于"""
    return (
        "🔮 *一念紫微斗数* v2.1\n\n"
        "AI驱动的紫微斗数深度解盘系统，三派合一。\n\n"
        "**命令：**\n"
        "· /zwds YYYY-MM-DD HH 男/女 — 排盘+基础解读\n"
        "· /reading ... — 完整四层AI解盘+流年流月\n"
        "· /match 甲 / 乙 — 合盘匹配（/分隔两人信息）\n"
        "· /yearly ... [年份] — 流年运势\n"
        "· /monthly ... [年份 月份] — 流月运势\n"
        "· /daily ... — 今日运势\n"
        "· /name 姓名 — 姓名拆字解字+五格数理\n"
        "· /read_en ... — 英文版AI解盘\n"
        "· /star 星名 — 查询星曜百科\n"
        "· /help — 帮助 | /about — 关于\n\n"
        "**支持语言：** zh-CN / en-US / ja-JP\n\n"
        "📜 *文化参考，理性看待*"
    )


if __name__ == "__main__":
    # 测试各种命令
    tests = [
        "/help",
        "/about",
        "/star 紫微",
        "/zwds 1984-6-22 6 男",
        "/reading 1984-6-22 6 男",
        "/yearly 1984-6-22 6 男 2026",
        "/monthly 1984-6-22 6 男 2026 4",
    ]
    for t in tests:
        cmd = t.split()[0].lower()
        if "help" in cmd:
            print(f"\n{'='*50}\n{handle_help()[:200]}...")
        elif "about" in cmd:
            print(f"\n{'='*50}\n{handle_about()}")
        elif "star" in cmd:
            name = " ".join(t.split()[1:]) if len(t.split()) > 1 else ""
            print(f"\n{'='*50}\n{handle_star_command(name)[:200]}...")
        elif "reading" in cmd:
            print(f"\n{'='*50}\n{handle_reading_command(t)[:300]}...")
        elif "yearly" in cmd:
            print(f"\n{'='*50}\n{handle_yearly_command(t)[:300]}...")
        elif "monthly" in cmd:
            print(f"\n{'='*50}\n{handle_monthly_command(t)[:300]}...")
        elif "zwds" in cmd:
            print(f"\n{'='*50}\n{handle_zwds_command(t)[:300]}...")


path: scripts/yinian_zwds.py
content: """
yinian_zwds.py — 一念紫微斗数 AI解盘统一入口
四层架构：基础→进阶→高阶→专业

Author: 崽儿虾 🦞
"""

from typing import Optional, Dict, Any
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from zwds_calc import generate_astrolabe, format_astrolabe, astrolabe_to_json
from deep_reading import full_deep_reading
from decadal import calculate_decadal_sequence
from formats import format_chart_for_telegram, build_tg_menu
from ai_reader import read_astrolabe


def build_reading_report(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    school: str = "综合",
    target_year: Optional[int] = None,
    depth: str = "normal",
) -> Dict[str, Any]:
    """
    统一解盘入口
    
    Args:
        depth: "basic" — 仅排盘
               "normal" — 排盘+基础解读
               "deep" — 四层全量解读
    
    Returns:
        {"chart_text", "chart_json", "reading_prompt", "summary"}
    """
    from datetime import datetime
    
    if not target_year:
        target_year = datetime.now().year
    
    # 排盘
    if depth == "basic":
        astro = generate_astrolabe(date_str, hour, gender, is_lunar)
        if not astro:
            return {"success": False, "error": "排盘失败"}
        return {
            "success": True,
            "chart_text": format_astrolabe(astro),
            "chart_json": astrolabe_to_json(astro),
            "reading_prompt": "",
        }
    
    # 全量深度解盘
    deep = full_deep_reading(date_str, hour, gender, is_lunar, target_year)
    if not deep["success"]:
        return {"success": False, "error": deep.get("error")}
    
    astro = generate_astrolabe(date_str, hour, gender, is_lunar)
    chart_text = format_astrolabe(astro) if astro else ""
    
    # 摘要
    summary = _generate_summary(deep, astro)
    
    return {
        "success": True,
        "chart_text": chart_text,
        "chart_json": astrolabe_to_json(astro) if astro else {},
        "reading_prompt": deep["ai_prompt"],
        "summary": summary,
        "timing": deep["timing"],
        "professional": deep["professional"],
    }


def _generate_summary(deep: Dict[str, Any], astro) -> str:
    """生成简短摘要（Telegram友好）"""
    lines = []
    
    timing = deep.get("timing", {})
    d = timing.get("current_decadal")
    if d:
        lines.append(f"📅 当前大限：{d['palace_name']} ({d['age_start']}-{d['age_end']}岁)")
    
    professional = deep.get("professional", {})
    laiyin = professional.get("laiyin_palace")
    if laiyin:
        lines.append(f"🎯 来因宫：{laiyin['palace']}")
    
    patterns = deep.get("advanced", {}).get("patterns", [])
    if patterns:
        names = "、".join(p["name"] for p in patterns)
        lines.append(f"🏆 格局：{names}")
    
    # 命宫主星
    if astro and astro.palaces:
        p0 = astro.palaces[0]
        ms = "、".join(f"{s.name}({s.brightness})" for s in p0.major_stars)
        if ms:
            lines.append(f"⭐ 命宫：{ms}")
    
    return "\n".join(lines)


def format_full_output(data: Dict[str, Any], depth: str = "deep") -> str:
    """完整Telegram/控制台输出"""
    if not data["success"]:
        return f"❌ {data.get('error', '未知错误')}"
    
    lines = []
    lines.append(data["chart_text"])
    lines.append("")
    
    if data.get("summary"):
        lines.append(data["summary"])
        lines.append("")
    
    if data.get("reading_prompt"):
        lines.append("━━━ 📖 AI解盘指令 ━━━")
        lines.append("")
        lines.append(data["reading_prompt"])
    
    return "\n".join(lines)


# 快速测试
if __name__ == "__main__":
    import sys
    
    date = sys.argv[1] if len(sys.argv) > 1 else "1984-6-22"
    hour = int(sys.argv[2]) if len(sys.argv) > 2 else 6
    gender = sys.argv[3] if len(sys.argv) > 3 else "男"
    
    depth = "deep"
    
    result = build_reading_report(date, hour, gender, depth=depth)
    print(format_full_output(result, depth))
    print(f"\n\n📊 Prompt长度: {len(result.get('reading_prompt', ''))} 字")


path: scripts/zwds_calc.py
content: """
zwds_calc.py — 一念紫微斗数排盘核心引擎
基于 iztro-py v0.3.4

【关键修正 v2】
iztro 的 to_iztro_dict() 返回的 palaces 按地支固定顺序(寅→丑)排列，
并非紫微斗数标准宫序。
必须以 earthlyBranchOfSoulPalace 找到命宫位置，再重排为正确顺序。

紫微斗数标准宫序（命宫为第0宫，按宫位逆时针）：
  0 命宫 1 父母宫 2 福德宫 3 田宅宫
  4 官禄宫 5 交友宫 6 迁移宫 7 疾厄宫
  8 财帛宫 9 子女宫 10 夫妻宫 11 兄弟宫

Author: 崽儿虾 🦞
"""

from iztro_py import astro
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Tuple
import json
import re


# 地支 → 索引映射（寅=0, 卯=1, ..., 丑=11）
BRANCH_TO_INDEX = {
    "寅": 0, "卯": 1, "辰": 2, "巳": 3, "午": 4, "未": 5,
    "申": 6, "酉": 7, "戌": 8, "亥": 9, "子": 10, "丑": 11,
}

INDEX_TO_BRANCH = {v: k for k, v in BRANCH_TO_INDEX.items()}

# 紫微斗数标准宫序（按地支定宫）
# iztro的palaces[0]是寅宫(福德宫)，palaces[10]是子宫(命宫)
# 但宫名本身由iztro的name字段决定，不是由索引决定！

BRIGHTNESS_CN = {
    "miao": "庙", "wang": "旺", "de": "得",
    "li": "利", "xian": "陷", "ping": "平",
}

HEAVENLY_STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"]
EARTHLY_BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"]

# 紫微斗数宫位名（按命宫为0开始逆时针顺序）
ZWDS_PALACE_NAMES = [
    ("命宫", "Life"),
    ("父母宫", "Parents"),
    ("福德宫", "Spirit"),
    ("田宅宫", "Property"),
    ("官禄宫", "Career"),
    ("交友宫", "Friends"),
    ("迁移宫", "Travel"),
    ("疾厄宫", "Health"),
    ("财帛宫", "Wealth"),
    ("子女宫", "Children"),
    ("夫妻宫", "Marriage"),
    ("兄弟宫", "Siblings"),
]

# 命宫在iztro palaces中的固定索引（地支寅=0序，命宫地支=子=10）
# 命宫地支由 earthlyBranchOfSoulPalace 给出
# 但iztro的palace是固定地支排序，所以需要找到命宫在哪
# soul_branch = earthlyBranchOfSoulPalace 的映射值


@dataclass
class StarInfo:
    name: str = ""
    brightness: str = ""
    mutagen: str = ""
    is_major: bool = True


@dataclass
class PalaceInfo:
    index: int = -1
    name_cn: str = ""
    name_en: str = ""
    heavenly_stem: str = ""
    earthly_branch: str = ""
    major_stars: List[StarInfo] = field(default_factory=list)
    minor_stars: List[StarInfo] = field(default_factory=list)
    is_empty: bool = False


@dataclass
class AstrolabeResult:
    birth_date: str = ""
    birth_hour: int = 0
    gender: str = ""
    zodiac: str = ""
    five_elements_class: str = ""
    four_pillars_year: str = ""
    four_pillars_month: str = ""
    four_pillars_day: str = ""
    four_pillars_hour: str = ""
    soul_palace_branch: str = ""
    body_palace_branch: str = ""
    soul_index: int = -1
    body_index: int = -1
    palaces: List[PalaceInfo] = field(default_factory=list)
    mutagens: List[Dict[str, str]] = field(default_factory=list)


def safe_str(val) -> str:
    """安全转字符串"""
    if val is None:
        return ""
    if hasattr(val, 'value'):
        return val.value
    return str(val)


def _stem_value(stem) -> str:
    """天干枚举转中文"""
    s = safe_str(stem)
    for h in HEAVENLY_STEMS:
        if h in s or s.startswith(h):
            return h
    name_map = {
        "jia": "甲", "yi": "乙", "bing": "丙", "ding": "丁", "wu": "戊",
        "ji": "己", "geng": "庚", "xin": "辛", "ren": "壬", "gui": "癸",
        "jiaHeavenly": "甲", "yiHeavenly": "乙", "bingHeavenly": "丙", "dingHeavenly": "丁",
        "wuHeavenly": "戊", "jiHeavenly": "己", "gengHeavenly": "庚", "xinHeavenly": "辛",
        "renHeavenly": "壬", "guiHeavenly": "癸",
    }
    return name_map.get(s, s)


def _branch_value(branch) -> str:
    """地支枚举转中文"""
    s = safe_str(branch)
    for b in EARTHLY_BRANCHES:
        if b in s or s.startswith(b):
            return b
    name_map = {
        "zi": "子", "chou": "丑", "yin": "寅", "mao": "卯",
        "chen": "辰", "si": "巳", "wu": "午", "wei": "未",
        "shen": "申", "you": "酉", "xu": "戌", "hai": "亥",
        "ziEarthly": "子", "chouEarthly": "丑", "yinEarthly": "寅", "maoEarthly": "卯",
        "chenEarthly": "辰", "siEarthly": "巳", "wuEarthly": "午", "weiEarthly": "未",
        "shenEarthly": "申", "youEarthly": "酉", "xuEarthly": "戌", "haiEarthly": "亥",
    }
    return name_map.get(s, s)


def _branch_index(branch_str: str) -> int:
    """地支名称转索引（子=0, 丑=1, ..., 亥=11）"""
    b = _branch_value(branch_str)
    return EARTHLY_BRANCHES.index(b)


def _map_mutagen_name(stem: str) -> Tuple[str, str, str, str]:
    """
    生年天干四化
    返回: (禄星, 权星, 科星, 忌星)
    """
    table = {
        "甲": ("廉贞", "破军", "武曲", "太阳"),
        "乙": ("天机", "天梁", "紫微", "太阴"),
        "丙": ("天同", "天机", "文昌", "廉贞"),
        "丁": ("太阴", "天同", "天机", "巨门"),
        "戊": ("贪狼", "太阴", "右弼", "天机"),
        "己": ("武曲", "贪狼", "天梁", "文曲"),
        "庚": ("太阳", "武曲", "太阴", "天同"),
        "辛": ("巨门", "太阳", "文曲", "文昌"),
        "壬": ("天梁", "紫微", "左辅", "武曲"),
        "癸": ("破军", "巨门", "太阴", "贪狼"),
    }
    return table.get(stem, ("", "", "", ""))


def _find_star_in_palaces(
    star_name: str,
    iztro_palaces: List[dict],
) -> Optional[int]:
    """在iztro的palaces中查找某星曜所在的宫位索引"""
    for i, p in enumerate(iztro_palaces):
        major = p.get("majorStars", [])
        minor = p.get("minorStars", [])
        for s in major + minor:
            if safe_str(s.get("name", "")) == star_name:
                return i
    return None


def compute_surrounded(palace_index: int) -> List[int]:
    """
    三方四正
    
    Args:
        palace_index: 在紫微斗数标准宫序中的索引(0-11)
    
    返回: 包含本宫在内的三方四正索引列表
    """
    return sorted({
        palace_index,
        (palace_index + 6) % 12,  # 对宫
        (palace_index + 4) % 12,  # 三合左
        (palace_index + 8) % 12,  # 三合右
    })


def _get_palace_name_by_branch(
    earth_branch: str,
    iztro_palaces: List[dict],
) -> str:
    """根据地支找到iztro宫殿在命盘中的紫微名称"""
    for p in iztro_palaces:
        if safe_str(p.get("earthlyBranch", "")) == _branch_value(earth_branch):
            return safe_str(p.get("name", ""))
    return ""


def generate_astrolabe(
    date_str: str,
    hour: int,
    gender: str,
    is_lunar: bool = False,
    is_lunar_leap: bool = False,
    language: str = "zh-CN"
) -> Optional[AstrolabeResult]:
    """生成紫微斗数命盘"""
    try:
        if is_lunar:
            result = astro.by_lunar(date_str, hour, gender, is_lunar_leap, True, language)
        else:
            result = astro.by_solar(date_str, hour, gender, True, language)

        if not result:
            return None

        d = result.to_iztro_dict()
        if not d:
            return None

        # ====== 从iztro原始数据中提取基本信息 ======
        raw_palaces = d.get("palaces", [])
        if not raw_palaces:
            return None

        # 获取命宫/身宫的地支
        soul_branch_str = safe_str(d.get("earthlyBranchOfSoulPalace", ""))
        body_branch_str = safe_str(d.get("earthlyBranchOfBodyPalace", ""))

        soul_branch = _branch_value(soul_branch_str)
        body_branch = _branch_value(body_branch_str)

        # 地支映射索引
        earth_branch_to_index = {
            "子": 0, "丑": 1, "寅": 2, "卯": 3,
            "辰": 4, "巳": 5, "午": 6, "未": 7,
            "申": 8, "酉": 9, "戌": 10, "亥": 11,
        }

        # 地支寅→丑在iztro palaces中的索引
        ztro_branch_to_idx = BRANCH_TO_INDEX

        # 命宫在iztro原始palaces中的索引
        # soul_branch是地支名称，iztro palaces按寅→丑顺序
        raw_soul_idx = ztro_branch_to_idx.get(soul_branch, 0)
        raw_body_idx = ztro_branch_to_idx.get(body_branch, 0)

        # ====== 构建 AstrolabeResult ======
        r = AstrolabeResult(
            birth_date=date_str,
            birth_hour=hour,
            gender=gender,
            zodiac=safe_str(d.get("zodiac", "")),
            five_elements_class=safe_str(d.get("fiveElementsClass", "")),
            soul_palace_branch=soul_branch,
            body_palace_branch=body_branch,
        )

        # 四柱
        cdate = getattr(result, "raw_chinese_date", None)
        if cdate:
            r.four_pillars_year = _stem_value(getattr(cdate, 'year_stem', '')) + _branch_value(getattr(cdate, 'year_branch', ''))
            r.four_pillars_month = _stem_value(getattr(cdate, 'month_stem', '')) + _branch_value(getattr(cdate, 'month_branch', ''))
            r.four_pillars_day = _stem_value(getattr(cdate, 'day_stem', '')) + _branch_value(getattr(cdate, 'day_branch', ''))
            r.four_pillars_hour = _stem_value(getattr(cdate, 'time_stem', '')) + _branch_value(getattr(cdate, 'time_branch', ''))

        # ====== 生年四化 ======
        year_stem = r.four_pillars_year[0] if r.four_pillars_year else ""
        if year_stem:
            mutagen_stars = _map_mutagen_name(year_stem)
            mutagen_names = ["禄", "权", "科", "忌"]
            
            for i, star in enumerate(mutagen_stars):
                if not star:
                    continue
                # 找此星曜在哪个宫
                palace_idx = _find_star_in_palaces(star, raw_palaces)
                if palace_idx is not None:
                    # 把iztro索引转回地支，再映射到实际宫名
                    branch_at_idx = INDEX_TO_BRANCH[palace_idx]
                    palace_name = _get_palace_name_by_branch(branch_at_idx, raw_palaces)
                    r.mutagens.append({
                        "palace": palace_name,
                        "star": star,
                        "mutagen": mutagen_names[i],
                    })

        # ====== 解析宫位 ======
        # Iztro palaces按地支寅→丑固定顺序排列
        # 我们需要按紫微斗数标准序（命宫为0，逆时针）重排
        
        soul_branch_index = BRANCH_TO_INDEX.get(soul_branch, 0)
        
        # 紫微斗数标准宫序对应的iztro原始索引
        # 命宫(0) = iztro[soul_branch_index]
        # 父母宫(1) = iztro[(soul_branch_index - 1) % 12]  (逆时针)
        # 福德宫(2) = iztro[(soul_branch_index - 2) % 12]
        # ...
        
        r.soul_index = 0  # 第0宫是命宫
        r.body_index = (raw_body_idx - soul_branch_index) % 12  # 身宫在标准序中的索引
        
        for i in range(12):
            raw_idx = (soul_branch_index + i) % 12  # 逆时针循环（iztro顺时针排寅→丑，命宫到父母宫需要+1）
            raw = raw_palaces[raw_idx]
            
            cn_name, en_name = ZWDS_PALACE_NAMES[i]
            hs = safe_str(raw.get("heavenlyStem", ""))
            eb = safe_str(raw.get("earthlyBranch", ""))

            # 主星
            major_stars = []
            for s in raw.get("majorStars", []):
                star_info = StarInfo(
                    name=safe_str(s.get("name", "")),
                    brightness=safe_str(s.get("brightness", "")),
                    mutagen=safe_str(s.get("mutagen", "")),
                    is_major=True,
                )
                major_stars.append(star_info)

            # 辅星
            minor_stars = []
            for s in raw.get("minorStars", []):
                star_info = StarInfo(
                    name=safe_str(s.get("name", "")),
                    mutagen=safe_str(s.get("mutagen", "")),
                    is_major=False,
                )
                minor_stars.append(star_info)
                
            # 亮度映射中文
            for s in major_stars:
                s.brightness = BRIGHTNESS_CN.get(s.brightness, s.brightness)

            # 四化标注
            # 生年四化的星曜对应四化
            for m in r.mutagens:
                for s in major_stars + minor_stars:
                    if s.name == m["star"]:
                        if not s.mutagen:
                            s.mutagen = m["mutagen"]

            is_empty = len(major_stars) == 0

            palace = PalaceInfo(
                index=i,
                name_cn=cn_name,
                name_en=en_name,
                heavenly_stem=hs,
                earthly_branch=eb,
                major_stars=major_stars,
                minor_stars=minor_stars,
                is_empty=is_empty,
            )
            r.palaces.append(palace)

        return r

    except Exception as e:
        import traceback
        print(f"[zwds_calc] 排盘异常: {e}")
        traceback.print_exc()
        return None


def astrolabe_to_json(astro_result: AstrolabeResult) -> Dict[str, Any]:
    """将 AstrolabeResult 转为 JSON 字典"""
    if not astro_result:
        return {}

    return {
        "birth": {
            "date": astro_result.birth_date,
            "hour": astro_result.birth_hour,
            "gender": astro_result.gender,
        },
        "four_pillars": {
            "year": astro_result.four_pillars_year,
            "month": astro_result.four_pillars_month,
            "day": astro_result.four_pillars_day,
            "hour": astro_result.four_pillars_hour,
        },
        "zodiac": astro_result.zodiac,
        "five_elements": astro_result.five_elements_class,
        "mutagens": astro_result.mutagens,
        "soul_index": astro_result.soul_index,
        "body_index": astro_result.body_index,
        "palaces": [
            {
                "index": p.index,
                "name_cn": p.name_cn,
                "name_en": p.name_en,
                "heavenly_stem": p.heavenly_stem,
                "earthly_branch": p.earthly_branch,
                "is_empty": p.is_empty,
                "major_stars": [
                    {
                        "name": s.name,
                        "brightness": s.brightness,
                        "mutagen": s.mutagen,
                    }
                    for s in p.major_stars
                ],
                "minor_stars": [
                    {
                        "name": s.name,
                        "mutagen": s.mutagen,
                    }
                    for s in p.minor_stars
                ],
            }
            for p in astro_result.palaces
        ],
    }


def format_astrolabe(astro_result: AstrolabeResult) -> str:
    """格式化命盘为可读文本"""
    if not astro_result:
        return "排盘失败"

    lines = []
    lines.append("━━━ 🔮 一念紫微斗数 · 命盘 ━━━")
    lines.append(f"四柱: {astro_result.four_pillars_year} {astro_result.four_pillars_month} {astro_result.four_pillars_day} {astro_result.four_pillars_hour}")
    
    lines.append(f"生肖: {astro_result.zodiac}　　五行局: {astro_result.five_elements_class}")

    soul_name = astro_result.palaces[astro_result.soul_index].name_cn if astro_result.palaces else ""
    body_name = astro_result.palaces[astro_result.body_index].name_cn if astro_result.palaces else ""
    soul_eb = astro_result.palaces[astro_result.soul_index].earthly_branch if astro_result.palaces else ""
    body_eb = astro_result.palaces[astro_result.body_index].earthly_branch if astro_result.palaces else ""
    soul_hs = astro_result.palaces[astro_result.soul_index].heavenly_stem if astro_result.palaces else ""
    body_hs = astro_result.palaces[astro_result.body_index].heavenly_stem if astro_result.palaces else ""
    
    lines.append(f"🏠 命宫: {soul_name}({soul_hs}{soul_eb})")
    lines.append(f"🏠 身宫: {body_name}({body_hs}{body_eb})")
    lines.append("")

    # 四化
    if astro_result.mutagens:
        lines.append("【生年四化】")
        for m in astro_result.mutagens:
            lines.append(f"  {m['palace']}: {m['star']}化{m['mutagen']}")
        lines.append("")

    # 十二宫
    lines.append("【十二宫】")
    for p in astro_result.palaces:
        major = " ".join(
            f"{s.name}({s.brightness})" + (f"→{s.mutagen}" if s.mutagen else "")
            for s in p.major_stars
        )
        minor = " ".join(s.name for s in p.minor_stars)
        empty = "【空宫】" if p.is_empty else ""

        parts = [f"  {p.name_cn} ({p.heavenly_stem}{p.earthly_branch})", ":"]
        parts.extend([f" {major}" if major else f" {empty}"])
        if minor:
            parts.append(f" 〔{minor}〕")
        lines.append("".join(parts))

    # 三方四正
    sur = compute_surrounded(astro_result.soul_index)
    sur_names = [astro_result.palaces[i].name_cn for i in sur]
    lines.append("")
    lines.append("【命宫三方四正】")
    lines.append(f"  {' · '.join(sur_names)}")

    lines.append("")
    lines.append("━ 文化参考，理性看待 ━")

    return "\n".join(lines)


if __name__ == "__main__":
    import sys
    
    # CLI使用
    if len(sys.argv) > 1:
        # 从参数读取
        date_str = sys.argv[sys.argv.index("--date") + 1] if "--date" in sys.argv else "2000-1-1"
        hour = int(sys.argv[sys.argv.index("--hour") + 1]) if "--hour" in sys.argv else 0
        gender = sys.argv[sys.argv.index("--gender") + 1] if "--gender" in sys.argv else "男"
        is_lunar = "--lunar" in sys.argv
    else:
        date_str = "1984-6-22"
        hour = 6
        gender = "男"
        is_lunar = False
    
    r = generate_astrolabe(date_str, hour, gender, is_lunar)
    if r:
        print(format_astrolabe(r))
        print("\n--- JSON ---")
        import json
        print(json.dumps(astrolabe_to_json(r), ensure_ascii=False, indent=2))
    else:
        print("排盘失败")


path: scripts/zwds_skill_main.py
content: """
zwds_skill_main.py — 一念紫微斗数 OpenClaw Skill 入口 v2
Telegram Bot命令路由：/zwds /reading /yearly /monthly /star /help /about

Author: 崽儿虾 🦞
"""

import sys
import os
import re
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from tg_bot_handler import (
    parse_birth_input,
    handle_zwds_command,
    handle_reading_command,
    handle_yearly_command,
    handle_monthly_command,
    handle_star_command,
    handle_help,
    handle_about,
    handle_reading_english,
    handle_matching_command,
    handle_daily_command,
    handle_name_command,
)
from formats import format_chart_for_telegram
from ai_reader import create_chart_and_reading, read_astrolabe
from ai_engine import ai_reading, full_reading_with_yearly


class YinianZWDS:
    """一念紫微斗数 Skill 主入口 v2"""
    
    def __init__(self):
        self.last_chart = None  # 缓存最后一次排盘结果
    
    def process_message(self, text: str) -> str:
        """处理用户消息主入口（OpenClaw框架自动调用）"""
        if not text or not text.strip():
            return ""
        
        text = text.strip()
        
        # 命令路由
        if text.startswith("/reading"):
            return handle_reading_command(text)
        
        if text.startswith("/read_en"):
            return handle_reading_english(text)
        
        if text.startswith("/yearly"):
            return handle_yearly_command(text)
        
        if text.startswith("/monthly"):
            return handle_monthly_command(text)
        
        if text.startswith("/daily"):
            return handle_daily_command(text)
        
        if text.startswith("/match"):
            return handle_matching_command(text)
        
        if text.startswith("/star"):
            rest = text.split(maxsplit=1)[1] if len(text.split()) > 1 else ""
            return handle_star_command(rest)
        
        if text.startswith("/name"):
            rest = text[5:].strip()
            return handle_name_command(rest)
        
        if text.startswith("/help") or text.startswith("/zwds_help"):
            return handle_help()
        
        if text.startswith("/about") or text.startswith("/zwds_about"):
            return handle_about()
        
        if text.startswith("/read"):
            return read_astrolabe(self.last_chart) if self.last_chart else "❌ 请先排盘（/zwds），再使用 /read 解盘"
        
        # /zwds 命令（含 --deep 标志）
        if text.startswith("/zwds") or text.startswith("/zwds_lunar") or re.match(r'^\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}\s+[男女]', text):
            result_text = handle_zwds_command(text)
            # 缓存chart
            if not any(s in result_text[:50] for s in ["❌"]):
                self.last_chart = {"has_chart": True}
            return result_text
        
        return ""
    
    def reset(self):
        """重置会话状态"""
        self.last_chart = None


# ====== 全局实例（供OpenClaw加载）======
yinian_zwds = YinianZWDS()


def handle_user_input(text: str) -> str:
    """OpenClaw调用的入口函数"""
    return yinian_zwds.process_message(text)


if __name__ == "__main__":
    handler = YinianZWDS()
    tests = [
        "/zwds 2000-8-16 6 男",
        "/help",
        "/star 紫微",
        "/about",
    ]
    for t in tests:
        print(f"\n{'='*50}")
        print(f">>> {t}")
        result = handler.process_message(t)
        print(result[:500] if len(str(result)) > 500 else result)


path: web/app/globals.css
content: /* 一念紫微斗数 - 全局样式 */


path: web/app/layout.tsx
content: import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "一念紫微斗数 | Yinian ZWDS",
  description: "AI驱动的紫微斗数深度解盘系统 · 三派合一",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh-CN">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🦞</text></svg>" />
      </head>
      <body>{children}</body>
    </html>
  );
}


path: web/app/page.tsx
content: "use client";

import { useState, useCallback } from "react";
import dynamic from "next/dynamic";

// react-iztro 动态加载（client-only）
const Iztrolabe = dynamic(
  () => import("react-iztro/lib/Iztrolabe/Iztrolabe").then((m) => ({ default: m.Iztrolabe })),
  { ssr: false, loading: () => <div className="chart-loading">✨ 加载星盘中...</div> }
);

type Gender = "男" | "女";

interface FormState {
  year: string;
  month: string;
  day: string;
  hour: string;
  gender: Gender;
}

const defaultForm: FormState = {
  year: "",
  month: "",
  day: "",
  hour: "",
  gender: "男",
};

const HOUR_LABELS = [
  "子时 23:00-00:59",
  "丑时 01:00-02:59",
  "寅时 03:00-04:59",
  "卯时 05:00-06:59",
  "辰时 07:00-08:59",
  "巳时 09:00-10:59",
  "午时 11:00-12:59",
  "未时 13:00-14:59",
  "申时 15:00-16:59",
  "酉时 17:00-18:59",
  "戌时 19:00-20:59",
  "亥时 21:00-22:59",
];

export default function Home() {
  const [form, setForm] = useState<FormState>(defaultForm);
  const [submitted, setSubmitted] = useState(false);
  const [error, setError] = useState<string>("");
  const [showDecadal, setShowDecadal] = useState(false);
  const [showYearly, setShowYearly] = useState(false);

  const update = (k: keyof FormState, v: string) =>
    setForm((f) => ({ ...f, [k]: v }));

  const handleSubmit = useCallback(() => {
    setError("");
    const { year, month, day, hour, gender } = form;
    if (!year || !month || !day || !hour) {
      setError("请填写完整的出生信息");
      return;
    }
    const y = parseInt(year);
    const m = parseInt(month);
    const d = parseInt(day);
    if (y < 1900 || y > 2100 || m < 1 || m > 12 || d < 1 || d > 31) {
      setError("日期范围有误（1900-2100）");
      return;
    }
    setSubmitted(true);
  }, [form]);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") handleSubmit();
  };

  // 组装出生日期字符串
  const birthday = submitted
    ? `${parseInt(form.year)}-${parseInt(form.month)}-${parseInt(form.day)}`
    : undefined;

  const birthTime = submitted ? parseInt(form.hour) : undefined;
  const gender = submitted ? (form.gender === "男" ? "male" : "female") : undefined;

  return (
    <div className="app-container" onKeyDown={handleKeyDown}>
      {/* Header */}
      <header className="header">
        <h1 className="title">🔮 一念·紫微斗数</h1>
        <p className="subtitle">Yinian Zi Wei Dou Shu</p>
      </header>

      {/* Form */}
      <div className="form-card">
        <div className="form-row">
          <div className="field">
            <label>年份</label>
            <input
              type="number"
              placeholder="e.g. 1984"
              value={form.year}
              onChange={(e) => update("year", e.target.value)}
            />
          </div>
          <div className="field">
            <label>月份</label>
            <input
              type="number"
              placeholder="6"
              min={1}
              max={12}
              value={form.month}
              onChange={(e) => update("month", e.target.value)}
            />
          </div>
          <div className="field">
            <label>日</label>
            <input
              type="number"
              placeholder="22"
              min={1}
              max={31}
              value={form.day}
              onChange={(e) => update("day", e.target.value)}
            />
          </div>
          <div className="field">
            <label>时辰</label>
            <select
              value={form.hour}
              onChange={(e) => update("hour", e.target.value)}
            >
              <option value="">选择时辰</option>
              {HOUR_LABELS.map((label, i) => (
                <option key={i} value={(i * 2).toString()}>
                  {label}
                </option>
              ))}
            </select>
          </div>
          <div className="field gender-field">
            <label>性别</label>
            <div className="gender-group">
              <button
                className={`gender-btn ${form.gender === "男" ? "active-male" : ""}`}
                onClick={() => update("gender", "男")}
              >
                ♂ 男
              </button>
              <button
                className={`gender-btn ${form.gender === "女" ? "active-female" : ""}`}
                onClick={() => update("gender", "女")}
              >
                ♀ 女
              </button>
            </div>
          </div>
        </div>
        <button className="calc-btn" onClick={handleSubmit}>
          📿 起盘
        </button>
        {error && <p className="error">{error}</p>}
      </div>

      {/* Chart */}
      {submitted && birthday && birthTime !== undefined && gender && (
        <div className="chart-section">
          <div className="chart-wrapper">
            <Iztrolabe
              birthday={birthday}
              birthTime={birthTime}
              gender={gender as any}
              birthdayType="solar"
              lang="zh-CN"
              width="100%"
            />
          </div>

          {/* 大限/流年切换 */}
          <div className="scope-bar">
            <label className="scope-toggle">
              <input
                type="checkbox"
                checked={showDecadal}
                onChange={(e) => setShowDecadal(e.target.checked)}
              />
              📅 大限
            </label>
            <label className="scope-toggle">
              <input
                type="checkbox"
                checked={showYearly}
                onChange={(e) => setShowYearly(e.target.checked)}
              />
              📆 流年
            </label>
          </div>

          {/* Action */}
          <div className="action-bar">
            <button
              className="action-btn"
              onClick={() => {
                const text = `🔮 一念紫微斗数\n📅 ${birthday} ${HOUR_LABELS[parseInt(form.hour) / 2]}\n🐉 ${form.gender}`;
                navigator.clipboard.writeText(text);
              }}
            >
              📋 复制信息
            </button>
            <button
              className="action-btn"
              onClick={() =>
                window.open("https://t.me/vinceeeent", "_blank")
              }
            >
              🤖 AI解盘
            </button>
          </div>
        </div>
      )}

      {/* Welcome */}
      {!submitted && (
        <div className="welcome">
          <p className="welcome-icon">🦞</p>
          <p className="welcome-text">输入出生信息，获取专属紫微斗数命盘</p>
          <p className="welcome-hint">示例：1984-6-22 卯时 男</p>
        </div>
      )}

      <div className="disclaimer">
        📜 文化参考，理性看待 · 一念紫微斗数
      </div>

      {/* Global styles */}
      <style jsx global>{`
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }

        body {
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
          background: linear-gradient(135deg, #0f0c29, #1a1a3e, #24243e);
          color: #e0d8c8;
          min-height: 100vh;
        }

        .app-container {
          max-width: 800px;
          margin: 0 auto;
          padding: 1rem;
          min-height: 100vh;
          display: flex;
          flex-direction: column;
          align-items: center;
        }

        .header {
          text-align: center;
          padding: 1.5rem 0 0.5rem;
        }

        .title {
          font-size: 1.6rem;
          background: linear-gradient(135deg, #f7d68a, #e8b84a);
          -webkit-background-clip: text;
          -webkit-text-fill-color: transparent;
          background-clip: text;
        }

        .subtitle {
          font-size: 0.75rem;
          opacity: 0.4;
          letter-spacing: 2px;
          margin-top: 0.25rem;
        }

        .form-card {
          background: rgba(255, 255, 255, 0.05);
          border: 1px solid rgba(255, 200, 100, 0.15);
          border-radius: 16px;
          padding: 1.2rem;
          width: 100%;
          max-width: 680px;
          backdrop-filter: blur(10px);
          margin: 0.5rem 0;
        }

        .form-row {
          display: flex;
          gap: 0.6rem;
          flex-wrap: wrap;
        }

        .field {
          flex: 1;
          min-width: 80px;
        }

        .field label {
          display: block;
          font-size: 0.75rem;
          opacity: 0.6;
          margin-bottom: 0.3rem;
        }

        .field input,
        .field select {
          width: 100%;
          padding: 0.55rem 0.5rem;
          border-radius: 8px;
          border: 1px solid rgba(255, 200, 100, 0.2);
          background: rgba(0, 0, 0, 0.3);
          color: #e0d8c8;
          font-size: 0.9rem;
        }

        .field select option {
          background: #1a1a3e;
          color: #e0d8c8;
        }

        .gender-field {
          min-width: 140px;
        }

        .gender-group {
          display: flex;
          gap: 0.4rem;
        }

        .gender-btn {
          flex: 1;
          padding: 0.55rem;
          border-radius: 8px;
          border: 1px solid rgba(255, 200, 100, 0.2);
          background: rgba(0, 0, 0, 0.3);
          color: #a09078;
          cursor: pointer;
          transition: all 0.2s;
          font-size: 0.85rem;
        }

        .active-male {
          background: rgba(100, 150, 255, 0.2);
          border-color: rgba(100, 150, 255, 0.5);
          color: #8ab4ff;
        }

        .active-female {
          background: rgba(255, 130, 180, 0.2);
          border-color: rgba(255, 130, 180, 0.5);
          color: #ff8ab4;
        }

        .calc-btn {
          width: 100%;
          padding: 0.7rem;
          margin-top: 0.7rem;
          border-radius: 10px;
          border: none;
          background: linear-gradient(135deg, #8b6914, #c4983a);
          color: #1a1a2e;
          font-size: 1rem;
          font-weight: 600;
          cursor: pointer;
          transition: all 0.2s;
        }

        .calc-btn:hover {
          transform: translateY(-1px);
          box-shadow: 0 4px 15px rgba(196, 152, 58, 0.3);
        }

        .chart-section {
          width: 100%;
          max-width: 680px;
          margin-top: 1rem;
        }

        .chart-wrapper {
          width: 100%;
          display: flex;
          justify-content: center;
        }

        .chart-wrapper :global(svg) {
          width: 100%;
          height: auto;
          max-width: 600px;
          display: block;
        }

        .chart-loading {
          text-align: center;
          padding: 3rem;
          opacity: 0.6;
          font-size: 1.1rem;
        }

        .scope-bar {
          display: flex;
          gap: 1rem;
          justify-content: center;
          margin-top: 0.5rem;
        }

        .scope-toggle {
          display: flex;
          align-items: center;
          gap: 0.4rem;
          font-size: 0.85rem;
          cursor: pointer;
          opacity: 0.7;
          color: #e0d8c8;
        }

        .scope-toggle input {
          width: 16px;
          height: 16px;
          accent-color: #c4983a;
        }

        .action-bar {
          display: flex;
          gap: 0.5rem;
          justify-content: center;
          margin-top: 0.8rem;
        }

        .action-btn {
          padding: 0.4rem 0.8rem;
          border-radius: 8px;
          border: 1px solid rgba(255, 200, 100, 0.2);
          background: rgba(0, 0, 0, 0.2);
          color: #c0b090;
          cursor: pointer;
          font-size: 0.8rem;
          transition: all 0.2s;
        }

        .action-btn:hover {
          background: rgba(255, 200, 100, 0.1);
          color: #e0d0a0;
        }

        .welcome {
          text-align: center;
          padding: 3rem 1rem;
        }

        .welcome-icon {
          font-size: 2rem;
          margin: 0;
        }
        .welcome-text {
          font-size: 1.1rem;
          opacity: 0.7;
        }
        .welcome-hint {
          font-size: 0.85rem;
          opacity: 0.5;
          margin-top: 0.8rem;
        }

        .disclaimer {
          text-align: center;
          font-size: 0.7rem;
          opacity: 0.35;
          padding: 1.5rem 0;
          margin-top: auto;
        }
      `}</style>
    </div>
  );
}


path: web/lib/chart.ts
content: /**
 * 一念紫微斗数 Web App
 * Yinian ZWDS - Next.js frontend with react-iztro
 * 
 * 使用iztro JS在前端排盘，零后端依赖
 */

import { bySolar, byLunar } from "iztro/lib/astro/astro";
import type { IFunctionalAstrolabe } from "iztro/lib/astro/FunctionalAstrolabe";

// 时辰映射（0-23 → iztro index 0-12）
const SOLAR_TERMS = [
  "子初", "子正", "丑初", "丑正", "寅初", "寅正",
  "卯初", "卯正", "辰初", "辰正", "巳初", "巳正",
  "午初", "午正", "未初", "未正", "申初", "申正",
  "酉初", "酉正", "戌初", "戌正", "亥初", "亥正",
];

export function hourToTimeIndex(hour: number): number {
  // iztro timeIndex: 0=子初, 1=子正, ... 23=亥正 → 实际索引0~12
  // iztro 使用时辰序号：子时=0, 丑时=2, 寅时=4 ...
  return Math.floor(hour / 2) * 2; // 子时0~1→0, 丑时2~3→2, ...
}

export function calculateChart(
  dateStr: string,
  hour: number,
  gender: string,
  isLunar = false
): { success: boolean; astrolabe?: IFunctionalAstrolabe; error?: string } {
  try {
    const timeIndex = hourToTimeIndex(hour);
    const g = gender === "男" ? "male" : "female";

    if (isLunar) {
      const astrolabe = byLunar(dateStr, timeIndex, g);
      return { success: true, astrolabe };
    }
    const astrolabe = bySolar(dateStr, timeIndex, g);
    return { success: true, astrolabe };
  } catch (e: any) {
    return { success: false, error: e.message || "排盘失败" };
  }
}

export function getHourLabel(hour: number): string {
  return SOLAR_TERMS[hour] || `${hour}时`;
}


path: web/next-env.d.ts
content: /// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.


path: web/next.config.js
content: /** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false, // react-iztro 可能有严格模式兼容问题
};

module.exports = nextConfig;


path: web/package-lock.json
content: {
  "name": "yinian-zwds-web",
  "version": "0.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "yinian-zwds-web",
      "version": "0.1.0",
      "dependencies": {
        "iztro": "^2.5.8",
        "next": "^15.0.0",
        "react": "^18.3.0",
        "react-dom": "^18.3.0",
        "react-iztro": "^1.4.2"
      },
      "devDependencies": {
        "@types/node": "25.6.0",
        "@types/react": "^18.3.0",
        "@types/react-dom": "^18.3.0",
        "typescript": "^5.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/@emnapi/runtime": {
      "version": "1.10.0",
      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "optional": true,
      "dependencies": {
        "tslib": "^2.4.0"
      }
    },
    "node_modules/@img/colour": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "optional": true,
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@img/sharp-darwin-arm64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-darwin-arm64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-darwin-x64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-darwin-x64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-libvips-darwin-arm64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "darwin"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-darwin-x64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "darwin"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linux-arm": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linux-arm64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linux-ppc64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linux-riscv64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linux-s390x": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linux-x64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
      "version": "1.2.4",
      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "linux"
      ],
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-linux-arm": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linux-arm": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linux-arm64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linux-arm64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linux-ppc64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linux-ppc64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linux-riscv64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linux-riscv64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linux-s390x": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linux-s390x": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linux-x64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linux-x64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linuxmusl-arm64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-linuxmusl-x64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
      }
    },
    "node_modules/@img/sharp-wasm32": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "wasm32"
      ],
      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
      "optional": true,
      "dependencies": {
        "@emnapi/runtime": "^1.7.0"
      },
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-win32-arm64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "Apache-2.0 AND LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-win32-ia32": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "license": "Apache-2.0 AND LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@img/sharp-win32-x64": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "Apache-2.0 AND LGPL-3.0-or-later",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      }
    },
    "node_modules/@next/env": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@next/swc-darwin-arm64": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-darwin-x64": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-linux-arm64-gnu": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-linux-arm64-musl": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-linux-x64-gnu": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-linux-x64-musl": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-win32-arm64-msvc": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@next/swc-win32-x64-msvc": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/@swc/helpers": {
      "version": "0.5.15",
      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "tslib": "^2.8.0"
      }
    },
    "node_modules/@types/node": {
      "version": "25.6.0",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~7.19.0"
      }
    },
    "node_modules/@types/prop-types": {
      "version": "15.7.15",
      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/react": {
      "version": "18.3.28",
      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/prop-types": "*",
        "csstype": "^3.2.2"
      }
    },
    "node_modules/@types/react-dom": {
      "version": "18.3.7",
      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "peerDependencies": {
        "@types/react": "^18.0.0"
      }
    },
    "node_modules/caniuse-lite": {
      "version": "1.0.30001791",
      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/browserslist"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "CC-BY-4.0"
    },
    "node_modules/classnames": {
      "version": "2.5.1",
      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/client-only": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/csstype": {
      "version": "3.2.3",
      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "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/detect-libc": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "optional": true,
      "engines": {
        "node": ">=8"
      }
    },
    "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/iztro-hook": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/iztro-hook/-/iztro-hook-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "iztro": "2.5.3"
      }
    },
    "node_modules/iztro-hook/node_modules/iztro": {
      "version": "2.5.3",
      "resolved": "https://registry.npmjs.org/iztro/-/iztro-2.5.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dayjs": "^1.11.10",
        "i18next": "^23.5.1",
        "lunar-lite": "^0.2.7",
        "lunar-typescript": "^1.7.8"
      }
    },
    "node_modules/js-tokens": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/loose-envify": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "js-tokens": "^3.0.0 || ^4.0.0"
      },
      "bin": {
        "loose-envify": "cli.js"
      }
    },
    "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"
    },
    "node_modules/nanoid": {
      "version": "3.3.11",
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "bin": {
        "nanoid": "bin/nanoid.cjs"
      },
      "engines": {
        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
      }
    },
    "node_modules/next": {
      "version": "15.5.15",
      "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@next/env": "15.5.15",
        "@swc/helpers": "0.5.15",
        "caniuse-lite": "^1.0.30001579",
        "postcss": "8.4.31",
        "styled-jsx": "5.1.6"
      },
      "bin": {
        "next": "dist/bin/next"
      },
      "engines": {
        "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
      },
      "optionalDependencies": {
        "@next/swc-darwin-arm64": "15.5.15",
        "@next/swc-darwin-x64": "15.5.15",
        "@next/swc-linux-arm64-gnu": "15.5.15",
        "@next/swc-linux-arm64-musl": "15.5.15",
        "@next/swc-linux-x64-gnu": "15.5.15",
        "@next/swc-linux-x64-musl": "15.5.15",
        "@next/swc-win32-arm64-msvc": "15.5.15",
        "@next/swc-win32-x64-msvc": "15.5.15",
        "sharp": "^0.34.3"
      },
      "peerDependencies": {
        "@opentelemetry/api": "^1.1.0",
        "@playwright/test": "^1.51.1",
        "babel-plugin-react-compiler": "*",
        "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
        "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
        "sass": "^1.3.0"
      },
      "peerDependenciesMeta": {
        "@opentelemetry/api": {
          "optional": true
        },
        "@playwright/test": {
          "optional": true
        },
        "babel-plugin-react-compiler": {
          "optional": true
        },
        "sass": {
          "optional": true
        }
      }
    },
    "node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/postcss": {
      "version": "8.4.31",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/postcss/"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/postcss"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "nanoid": "^3.3.6",
        "picocolors": "^1.0.0",
        "source-map-js": "^1.0.2"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/react": {
      "version": "18.3.1",
      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "loose-envify": "^1.1.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/react-dom": {
      "version": "18.3.1",
      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "loose-envify": "^1.1.0",
        "scheduler": "^0.23.2"
      },
      "peerDependencies": {
        "react": "^18.3.1"
      }
    },
    "node_modules/react-iztro": {
      "version": "1.4.2",
      "resolved": "https://registry.npmjs.org/react-iztro/-/react-iztro-1.4.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "classnames": "^2.3.2",
        "iztro": "2.5.3",
        "iztro-hook": "1.3.3",
        "lunar-lite": "^0.2.7"
      }
    },
    "node_modules/react-iztro/node_modules/iztro": {
      "version": "2.5.3",
      "resolved": "https://registry.npmjs.org/iztro/-/iztro-2.5.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dayjs": "^1.11.10",
        "i18next": "^23.5.1",
        "lunar-lite": "^0.2.7",
        "lunar-typescript": "^1.7.8"
      }
    },
    "node_modules/scheduler": {
      "version": "0.23.2",
      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "loose-envify": "^1.1.0"
      }
    },
    "node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "optional": true,
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/sharp": {
      "version": "0.34.5",
      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "Apache-2.0",
      "optional": true,
      "dependencies": {
        "@img/colour": "^1.0.0",
        "detect-libc": "^2.1.2",
        "semver": "^7.7.3"
      },
      "engines": {
        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/libvips"
      },
      "optionalDependencies": {
        "@img/sharp-darwin-arm64": "0.34.5",
        "@img/sharp-darwin-x64": "0.34.5",
        "@img/sharp-libvips-darwin-arm64": "1.2.4",
        "@img/sharp-libvips-darwin-x64": "1.2.4",
        "@img/sharp-libvips-linux-arm": "1.2.4",
        "@img/sharp-libvips-linux-arm64": "1.2.4",
        "@img/sharp-libvips-linux-ppc64": "1.2.4",
        "@img/sharp-libvips-linux-riscv64": "1.2.4",
        "@img/sharp-libvips-linux-s390x": "1.2.4",
        "@img/sharp-libvips-linux-x64": "1.2.4",
        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
        "@img/sharp-linux-arm": "0.34.5",
        "@img/sharp-linux-arm64": "0.34.5",
        "@img/sharp-linux-ppc64": "0.34.5",
        "@img/sharp-linux-riscv64": "0.34.5",
        "@img/sharp-linux-s390x": "0.34.5",
        "@img/sharp-linux-x64": "0.34.5",
        "@img/sharp-linuxmusl-arm64": "0.34.5",
        "@img/sharp-linuxmusl-x64": "0.34.5",
        "@img/sharp-wasm32": "0.34.5",
        "@img/sharp-win32-arm64": "0.34.5",
        "@img/sharp-win32-ia32": "0.34.5",
        "@img/sharp-win32-x64": "0.34.5"
      }
    },
    "node_modules/source-map-js": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/styled-jsx": {
      "version": "5.1.6",
      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "client-only": "0.0.1"
      },
      "engines": {
        "node": ">= 12.0.0"
      },
      "peerDependencies": {
        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
      },
      "peerDependenciesMeta": {
        "@babel/core": {
          "optional": true
        },
        "babel-plugin-macros": {
          "optional": true
        }
      }
    },
    "node_modules/tslib": {
      "version": "2.8.1",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "0BSD"
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "7.19.2",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    }
  }
}


path: web/package.json
content: {
  "name": "yinian-zwds-web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "iztro": "^2.5.8",
    "next": "^15.0.0",
    "react": "^18.3.0",
    "react-dom": "^18.3.0",
    "react-iztro": "^1.4.2"
  },
  "devDependencies": {
    "@types/node": "25.6.0",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0",
    "typescript": "^5.0.0"
  }
}


path: web/tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)