Skill: effeceee/whoop-guru (version 8.4.12)

# WHOOP Guru - 完整WHOOP健康管理系统 ## v8.4.12 简介 WHOOP Guru 是**完整的WHOOP健康管理系统**，整合数据获取、分析、图表可视化、AI教练和个性化训练计划。 **核心功能：** - 📊 **数据获取** - WHOOP官方API，实时同步恢复/睡眠/训练/HRV - 📈 **图表生成** - 交互式HTML图表，恢复/睡眠/strain/HRV趋势 - 🤖 **AI教练** - LLM驱动，个性化训练建议 - 🎯 **训练计划** - 跑步/增肌/减脂/伤痛恢复 - 🔔 **主动推送** - 每日定时提醒（08:00/09:00/18:00/20:00/22:00） - 📉 **ML预测** - 7天恢复预测，异常检测 - 🏅 **马拉松管理** - 目标设定、配速计划、训练阶段分析 - 📝 **快速打卡** - 一句话完成训练记录 - 🔄 **自动打卡检测** - WHOOP数据自动识别今日训练，生成初步打卡内容 - 📚 **反馈学习系统** - 用户感受词→WHOOP数据映射，持续优化建议 - 🕐 **北京时间统一** - 全部时间处理使用北京时间(UTC+8) --- ## 功能列表 ### 1. WHOOP数据获取 ```bash # 进入 skill 目录（根据实际安装路径调整） cd /path/to/whoop-guru # 获取综合报告 python3 scripts/whoop_data.py summary --days 7 # 睡眠数据 python3 scripts/whoop_data.py sleep --days 7 # 恢复评分 python3 scripts/whoop_data.py recovery --days 30 # 训练/cycle数据 python3 scripts/whoop_data.py cycles --days 7 # workouts python3 scripts/whoop_data.py workouts --days 30 # 用户档案 python3 scripts/whoop_data.py profile # 身体数据 python3 scripts/whoop_data.py body ``` ### 2. OAuth认证 ```bash # 首次登录 python3 scripts/whoop_auth.py login --client-id YOUR_ID --client-secret YOUR_SECRET # 检查状态 python3 scripts/whoop_auth.py status ``` ### 3. 图表生成 ```bash # 睡眠分析图表 python3 scripts/whoop_chart.py sleep --days 30 # 恢复评分图表 python3 scripts/whoop_chart.py recovery --days 30 # Strain趋势 python3 scripts/whoop_chart.py strain --days 90 # HRV趋势 python3 scripts/whoop_chart.py hrv --days 90 # 综合仪表盘 python3 scripts/whoop_chart.py dashboard --days 30 ``` ### 4. 健康分析 使用 `references/health_analysis.md` 进行科学分析： - HRV正常范围（按年龄/ fitness） - 恢复评分解读（绿/黄/红） - 睡眠阶段分析 - 训练过度信号检测 - 可操作的建议 ### 5. AI教练（LLM驱动） ```bash # 开启教练对话 python3 whoop-guru.py coach # 生成今日计划 python3 whoop-guru.py plan # 生成16周计划 python3 whoop-guru.py 16week --goal 增肌 ``` ### 6. 推送系统 | 时间 | 内容 | 命令 | |------|------|------| | 08:00 | 健康早报 | daily-report.sh | | 09:00 | 教练早安 | push-morning.py | | 18:00 | 晚间追踪 | push-evening.py | | 20:00 | 打卡提醒 | push-checkin.py | | 22:00 | 详细日报 | detailed-report.sh | --- ## 支持的运动目标 | 类别 | 目标 | 周期 | |------|------|------| | 🏃 跑步 | 3公里新手 | 8周 | | 🏃 跑步 | 5公里 | 10周 | | 🏃 跑步 | 10公里 | 12周 | | 🏃 跑步 | 半程马拉松 | 24周 | | 🏃 跑步 | 全程马拉松 | 52周 | | 🏃 跑步 | 超级马拉松 | 52周+ | | 💪 力量 | 增肌 | 16周 | | 💪 力量 | 减脂 | 12周 | | 🏥 康复 | 伤痛恢复 | 按需 | --- ## 关键指标 - **恢复评分** (0-100%): 绿色≥67%, 黄色34-66%, 红色<34% - **Strain** (0-21): 基于心率的每日运动强度 - **睡眠效率**: 实际睡眠 vs 需要睡眠 - **HRV** (ms): 越高恢复越好，追踪趋势 - **静息心率** (bpm): 越低心血管 fitness越好 --- ## 配置说明 ### WHOOP OAuth（必需） WHOOP 使用 OAuth 2.0 授权，配置步骤： 1. 在 [developer.whoop.com](https://developer.whoop.com) 创建 App 获取 Client ID 和 Secret 2. 运行：`python3 scripts/whoop_auth.py login --client-id YOUR_ID --client-secret YOUR_SECRET` 3. 浏览器自动打开，完成授权 4. Token 自动存储在 `~/.clawdbot/whoop-tokens.json` **替代方案**：也可以手动创建 `~/.clawdbot/whoop-credentials.env` 文件，包含： ``` WHOOP_CLIENT_ID=your_client_id WHOOP_CLIENT_SECRET=your_client_secret WHOOP_REFRESH_TOKEN=your_refresh_token ``` ### LLM API（可选） 用于 AI 个性化分析。配置方式： - 发送「设置模型」给机器人 - 按提示输入 API Key 和选择模型 - 配置存储在 `data/config/llm_config.json` ### 本地数据存储 凭证存储在本地，不上传。数据流向： - 用户健康数据从WHOOP API获取 - 数据发送到LLM API用于AI分析 - `data/profiles/` - 用户健身档案 - `data/plans/` - AI训练计划 - `data/logs/` - 打卡记录 - `data/config/llm_config.json` - LLM API密钥 - `~/.clawdbot/whoop-tokens.json` - WHOOP OAuth令牌 --- ## 文件结构 ``` whoop-guru/ ├── SKILL.md ├── CLAWHUB.md ├── _meta.json ├── whoop-guru.py # 主入口 ├── scripts/ │ ├── whoop_auth.py # OAuth认证 │ ├── whoop_data.py # 数据获取 │ ├── whoop_chart.py # 图表生成 │ ├── daily-report.sh # 08:00早报 │ ├── detailed-report.sh # 22:00日报 │ ├── coach-push.sh # 推送调度 │ ├── push-morning.py # 09:00早安 │ ├── push-evening.py # 18:00晚间 │ └── push-checkin.py # 20:00打卡 ├── lib/ │ ├── llm.py # LLM集成 │ ├── data_cleaner.py # 数据清洗 │ ├── data_processor.py # 数据处理 │ ├── sync.py # 数据同步 │ ├── tracker.py # 打卡追踪（包含quick_checkin） │ ├── goals.py # 目标管理 │ ├── goals_marathon.py # 马拉松目标 │ ├── dynamic_planner.py # 动态规划 │ ├── pusher.py # 推送生成（包含checkin_reminder重写） │ ├── coach_interface.py # 教练接口 │ ├── coach/ # 教练核心模块 │ ├── needs_analyzer.py # 需求分析 │ ├── plan_generator.py # 计划生成 │ ├── health_score.py # 健康评分 │ ├── ml_predictor.py # ML预测 │ ├── comprehensive_analysis.py # 综合分析 │ ├── health_advisor.py # 健康建议 │ ├── enhanced_reports.py # 增强报告 │ ├── marathon_analyzer.py # 马拉松分析 │ ├── marathon_commands.py # 马拉松指令 │ ├── checkin_auto.py # 自动打卡检测（新增） │ ├── feedback_learning.py # 反馈学习系统（增强） │ ├── notifications.py # 通知系统 │ ├── user_profile.py # 用户档案 │ ├── tz.py # 统一时区工具（新增） │ ├── ml/ # ML预测模块 │ ├── prompts/ # Prompt模板 │ └── reports/ # 报告生成 ├── references/ │ ├── api.md # API文档 │ └── health_analysis.md # 健康分析指南 └── data/ ├── config/ # LLM配置 ├── profiles/ # 用户档案 ├── plans/ # 生成的计划 ├── processed/ # 处理后的数据 └── logs/ # 打卡记录 ``` --- ## 系统要求 | 类型 | 要求 | |------|------| | 运行环境 | Python 3.8+ | | 必需命令 | python3, curl | | Python包 | requests, pandas, matplotlib | **安装方式**：`pip install requests pandas matplotlib` --- ## 环境变量 | 变量 | 说明 | 必需 | 默认值 | |------|------|------|--------| | OPENCLAW_WORKSPACE | OpenClaw工作区目录 | 否 | 自动检测 | | WHOOP_DATA_DIR | WHOOP数据存储目录 | 否 | 自动检测 | | WHOOP_SKILL_DIR | Skill数据目录 | 否 | 自动检测 | **注意**：WHOOP OAuth 凭证不是环境变量，而是 CLI 参数，详见上方配置说明。 --- ## 推送机制 推送由 OpenClaw 的 cron 系统触发（不是独立服务）。 用户需要在 OpenClaw 中配置 cron 规则： ```bash # 示例 crontab 配置 0 8 * * * python3 /path/to/daily-report.sh 0 9 * * * python3 /path/to/push-morning.py 0 18 * * * python3 /path/to/push-evening.py 0 20 * * * python3 /path/to/push-checkin.py 0 22 * * * python3 /path/to/detailed-report.sh ``` --- ## 版本历史 ### v8.4.11 (2026-04-05) - 修复：_meta.json requires.env 改为字符串数组格式，匹配 ClawHub registry schema * ClawHub registry 期望 env 是字符串数组（如 ["WHOOP_CLIENT_ID"]），而非对象 - 修复：SKILL.md YAML front matter 同步修复为字符串数组格式 ### v8.4.10 (2026-04-05) - 修复：_meta.json environment 字段添加 WHOOP 凭证 * 将 WHOOP_CLIENT_ID/CLIENT_SECRET/REFRESH_TOKEN/LLM_API_KEY 添加到 environment - 新增：SKILL.md YAML metadata block 声明必需凭证 ### v8.4.9 (2026-04-05) - 修复：SKILL.md 添加 YAML metadata block 声明必需凭证 * 解决 ClawHub registry 显示"无必需环境变量"问题 ### v8.4.8 (2026-04-05) - 修复：push 脚本（morning/evening/checkin）生成前先刷新 WHOOP 数据，解决数据陈旧问题 * 调用 whoop_data.py summary 获取最新实时数据并写入 latest.json * 修复 recovery.start 为空导致的 date=None 问题（改用 created_at） * 修复 hrv/skin_temp 为 None 时 round() 报错问题 - 修复：agents.defaults.timeoutSeconds 缺少配置导致 cron 任务 30s 超时 * openclaw.json 添加 "timeoutSeconds": 120 - 新增：推送内容使用当天实时 WHOOP 数据 ### v8.4.7 (2026-04-04) - 新增：lib/tz.py 统一时区工具模块，全部时间处理统一使用北京时间(UTC+8) - 新增：lib/checkin_auto.py WHOOP数据自动打卡检测 * 自动从 workouts API 获取今日训练 * 分析跑步距离/配速/心率/心率区间 * 生成初步打卡内容预览供用户确认 * 支持跨UTC日边界的北京时间窗口过滤（北京凌晨跑步同步） - 新增：lib/feedback_learning.py 增强反馈学习系统 * 用户感受词 → WHOOP数据映射（睡眠/训练/恢复） * 积累3次样本后自动触发个人化建议调整 * 主观反馈与实际数据对比学习 - 重构：lib/pusher.py checkin_reminder() 完全重写 * WHOOP检测到训练时自动展示：跑步10.3km/配速5:45/心率区间... * 无训练时显示休息日模板 * 包含WHOOP睡眠数据供用户反馈 * 用户回复「✅」确认打卡 - 新增：测试覆盖率达到100%，全部65个单元测试通过 - 修复：feedback_learning.py key名字不一致bug（sleep_baselines → sleep_quality_map） - 修复：feedback_learning.py entry key不匹配bug（inferred_quality → inferred_sleep） - 修复：data_cleaner.py/dynamic_planner.py/pusher.py/enhanced_reports.py 时区处理统一为北京时间 ### v8.4.6 (2026-04-03) - 修复：测试套件7个失败（TestMarathonGoals缺少import、pusher.py未定义变量） - 修复：marathon_commands.py 中「4月12号」中文日期格式支持 - 修复：marathon_commands.py 解析「2小时」等中文时间格式崩溃问题 - 修复：coach_interface.py 引用不存在的 tracker 函数 - 新增：tracker.py 新增 `quick_checkin()` 快速打卡接口（无需 completed/feedback） - 增强：enhanced_reports.py 完整重写，无 LLM 时也能生成充实报告 - 新增：TOOLS.md 完全重写，更新所有路径、命令、模块说明 - 全部 41 个单元测试通过 ### v8.4.5 (2026-03-31) - 新增：马拉松训练目标管理（GoalsManager扩展） - 新增：跑步打卡追踪（ProgressTracker扩展） - 新增：马拉松训练分析器（MarathonAnalyzer） - 新增：马拉松指令处理（MarathonCommands） - 新增：每日报告集成马拉松板块 - 新增：41个单元测试（v8.4.6时） ### v8.3.8 (2026-03-31) - 修复：OpenClaw cron配置 ### v8.3.7 (2026-03-29) - 修复：加强隐私声明，明确数据发送到LLM API ### v8.3.6 (2026-03-29) - 修复：只声明代码实际使用的环境变量 ### v8.3.5 (2026-03-29) - 修复：添加WHOOP凭证到credentials声明 ### v8.3.4 (2026-03-29) - 修复：统一WHOOP凭证文件说明（whoop-tokens.json和whoop-credentials.env） ### v8.3.3 (2026-03-29) - 修复：统一三个文件的凭证说明 ### v8.2.7 (2026-03-29) - 新增：系统要求章节（bins, packages） - 新增：推送机制说明（cron + OpenClaw消息） - 修复：澄清数据流和隐私声明 ### v8.2.6 (2026-03-29) - 修复：统一 SKILL.md 和 _meta.json 的配置说明 - 新增：homepage 字段 ### v8.2.5 (2026-03-29) - 修复：移除 WHOOP_REFRESH_TOKEN（OAuth自动获取） - 修复：统一三个文件的凭证说明 ### v8.2.4 (2026-03-29) - 安全：修复 os.system shell 注入漏洞 ### v8.2.3 (2026-03-29) - 修复：统一凭证存储位置说明 ### v8.2.0 (2026-03-29) - 新增：LLM 增强报告模块 ### v8.1.5-8.1.7 (2026-03-29) - 新增：推送系统（09:00/18:00/20:00） - 集成：dynamic_planner、goals、tracker 模块 ### v8.0 (2026-03-26) - LLM 集成（支持 8 种提供商） - 个性化训练计划生成

path: CLAWHUB.md
content: # ClawHub Metadata

version: 8.4.12
slug: whoop-guru
name: WHOOP Guru
description: |
  完整的WHOOP健康管理系统。整合WHOOP官方API数据获取、交互式图表可视化、
  LLM AI私人教练、个性化训练计划（跑步/力量/减脂/康复）、ML恢复预测
  和每日主动推送，为用户提供一站式健康管理和健身指导。
  
summary: Complete WHOOP health system with AI coach, charts, training plans, and ML predictions
owner: effeceee

tags:
- whoop
- fitness
- coach
- ai-coach
- llm
- health
- workout
- recovery
- sleep
- hrv
- running
- marathon
- strength
- fatloss
- training
- injury-recovery
- charts
- visualization

requirements:
  python: ">=3.8"
  bins:
    - python3
    - curl
  packages:
    pandas: ">=1.3"
    matplotlib: ">=3.5"
    requests: ">=2.25"

features:
  - id: whoop_api
    title: WHOOP官方API数据
    description: 实时获取恢复、睡眠、训练负荷、HRV等数据，支持OAuth认证
  - id: data_charts
    title: 交互式图表
    description: 生成恢复/睡眠/strain/HRV趋势图表，HTML格式，支持暗色主题
  - id: llm_coach
    title: LLM AI教练
    description: 支持8种模型（MiniMax/OpenAI/GLM/Kimi等），基于LLM生成个性化训练建议
  - id: running_plans
    title: 跑步训练计划
    description: 支持3公里/5公里/10公里/半马/全马/超级马拉松，从新手到完赛
  - id: strength_plans
    title: 力量训练计划
    description: 增肌计划（16周）、减脂计划（12周），配合营养建议
  - id: injury_recovery
    title: 伤痛恢复
    description: 膝盖/肩关节/腰痛等伤痛恢复训练方案
  - id: ml_predictions
    title: ML恢复预测
    description: 7天恢复预测、HRV异常检测、训练强度建议
  - id: progress_tracking
    title: 进度追踪
    description: 打卡系统、连续记录、周报、目标仪表盘
  - id: proactive_notifications
    title: 主动推送
    description: 08:00早报、09:00早安、18:00晚间追踪、20:00打卡提醒、22:00日报

commands:
  - /coach
    description: 开启AI教练对话
  - /plan
    description: 生成今日训练计划
  - /checkin
    description: 训练打卡
  - /progress
    description: 查看训练进度
  - /16week
    description: 生成16周训练计划
  - /marathon
    description: 生成马拉松训练计划
  - /race
    description: 生成比赛训练计划
  - /strength
    description: 生成力量训练计划
  - /fatloss
    description: 生成减脂计划
  - /recovery
    description: 生成伤痛恢复计划
  - /profile
    description: 查看/设置健身档案
  - /llm
    description: 配置LLM模型
  - /chart
    description: 生成健康图表

environment:
  OPENCLAW_WORKSPACE:
    description: OpenClaw workspace directory
    required: false
  WHOOP_DATA_DIR:
    description: WHOOP data storage directory
    required: false
  WHOOP_SKILL_DIR:
    description: Skill directory for data sync
    required: false

credentials:
  whoop_oauth:
    description: WHOOP OAuth credentials (CLI args, not env vars)
    cli_args: ["--client-id", "--client-secret"]
    used_by: scripts/whoop_auth.py login
    stored_at: ~/.clawdbot/whoop-tokens.json
  whoop_env:
    description: WHOOP credentials as env vars (alternative method)
    file: ~/.clawdbot/whoop-credentials.env
    used_by: lib/whoop-fetcher.sh
    variables: [WHOOP_CLIENT_ID, WHOOP_CLIENT_SECRET, WHOOP_REFRESH_TOKEN]
  llm_api:
    description: LLM API key (via chat command)
    used_by: lib/llm.py
    stored_at: data/config/llm_config.json

data_flow:
  - 用户健康数据从WHOOP API获取
  - 健康数据发送到用户配置的LLM API用于分析
  - 凭证存储在本地，不上传

data_storage:
  profiles:
    path: data/profiles/
    description: 用户健身档案和目标（相对路径，相对于skill目录）
  plans:
    path: data/plans/
    description: AI生成的训练计划
  logs:
    path: data/logs/
    description: 打卡记录和追踪数据
  config:
    path: data/config/llm_config.json
    description: LLM API密钥配置（用户自行配置，不上传）
  tokens:
    description: WHOOP OAuth token存储
    storage_path: ~/.clawdbot/whoop-tokens.json
    handling: |
      WHOOP OAuth token存储在 ~/.clawdbot/whoop-tokens.json
      LLM API密钥存储在 data/config/llm_config.json
      所有凭证文件由OpenClaw管理，不上传外部服务
  privacy: |
    凭证存储：API密钥和Token存储在本地，不上传。
    数据流向：
    - 用户数据（恢复/睡眠/训练）从WHOOP API获取
    - 健康数据发送给LLM API用于生成分析
    - LLM API密钥仅用于API调用，不作他用
    用户可随时删除data/目录清除所有本地数据。

installation_notes: |
    安装步骤：
    1. pip install requests pandas matplotlib
    2. python3 scripts/whoop_auth.py login --client-id YOUR_ID --client-secret YOUR_SECRET
    3. 完成 OAuth 授权（浏览器自动打开）
    4. (可选) 配置 LLM API


path: _meta.json
content: {
  "name": "whoop-guru",
  "description": "WHOOP AI Fitness Coach - LLM-powered personalized training plans for running, strength, and recovery",
  "version": "8.4.12",
  "license": "Apache-2.0",
  "emoji": "💪",
  "author": "HealthGao Team",
  "homepage": "https://clawhub.com/skills/whoop-guru",
  "repository": "https://github.com/effeceee/healthgao",
  "requires": {
    "python": ">=3.8",
    "bins": [
      "python3",
      "curl"
    ],
    "packages": [
      "requests",
      "pandas",
      "matplotlib"
    ]
  },
  "environment": {
    "OPENCLAW_WORKSPACE": {
      "description": "OpenClaw workspace directory (auto-detected)",
      "required": false
    },
    "WHOOP_DATA_DIR": {
      "description": "WHOOP data storage directory (auto-detected)",
      "required": false
    },
    "WHOOP_SKILL_DIR": {
      "description": "Skill directory for data sync (auto-detected)",
      "required": false
    }
  },
  "credentials": {
    "whoop_tokens": {
      "description": "OAuth tokens obtained via login flow",
      "storage": "~/.clawdbot/whoop-tokens.json",
      "managed_by": "scripts/whoop_auth.py"
    },
    "whoop_env_file": {
      "description": "Alternative: credentials file with env vars",
      "storage": "~/.clawdbot/whoop-credentials.env",
      "used_by": "lib/whoop-fetcher.sh",
      "variables": [
        "WHOOP_CLIENT_ID",
        "WHOOP_CLIENT_SECRET",
        "WHOOP_REFRESH_TOKEN"
      ]
    },
    "llm_api": {
      "description": "LLM API key for AI analysis",
      "storage": "data/config/llm_config.json",
      "managed_by": "User via chat command"
    }
  },
  "features": {
    "whoop_api": "实时获取WHOOP恢复、睡眠、训练、HRV数据",
    "llm_coach": "LLM驱动的个性化AI健身教练",
    "push_notifications": "每日主动推送（08:00早报、09:00早安、18:00晚间、20:00打卡、22:00日报）",
    "ml_predictions": "7天恢复预测、HRV异常检测",
    "training_plans": "跑步/增肌/减脂/伤痛恢复训练计划",
    "progress_tracking": "打卡系统、目标追踪、周报",
    "marathon_training": "马拉松训练目标管理、配速计划、训练阶段分析",
    "quick_checkin": "快速打卡接口，无需填写完成情况",
    "checkin_auto": "WHOOP数据自动检测 + 打卡确认流程",
    "feedback_learning": "用户反馈学习系统（睡眠/训练/恢复基线）",
    "beijing_time": "全部时间处理统一使用北京时间(UTC+8)"
  },
  "installation": {
    "steps": [
      "1. pip install requests pandas matplotlib",
      "2. python3 scripts/whoop_auth.py login --client-id YOUR_ID --client-secret YOUR_SECRET",
      "3. 完成OAuth授权（浏览器自动打开）",
      "4. (可选) 配置LLM API"
    ]
  },
  "privacy": {
    "data_transmission": "用户健康数据（恢复/睡眠/训练/HRV）将发送到：(1) WHOOP API用于获取数据，(2) 用户配置的LLM API用于AI分析",
    "credential_storage": "所有凭证存储在本地文件系统，不上传到第三方服务器",
    "user_control": "用户可随时删除 ~/.clawdbot/ 目录和 data/config/ 目录清除所有凭证和数据"
  }
}

path: data/config/README.md
content: # Credentials Directory

This directory stores user credentials at runtime:
- `llm_config.json`: LLM API keys (user-configured via chat)
- `whoop-tokens.json`: NOT stored here - WHOOP tokens stored at `~/.clawdbot/whoop-tokens.json`

**Note**: WHOOP OAuth tokens are automatically stored by `scripts/whoop_auth.py` at `~/.clawdbot/whoop-tokens.json`. The WHOOP fetcher script (`lib/whoop-fetcher.sh`) also uses `~/.clawdbot/whoop-credentials.env`.


path: data/config/llm_config.json

path: data/logs/checkins_default.json
content: {
  "checkins": [
    {
      "id": "checkin_20260405101359",
      "user_id": "test_user_1775355239.89447",
      "type": "卧推",
      "completed": "完成",
      "feedback": "良好",
      "notes": "",
      "timestamp": "2026-04-05T10:13:59.894505"
    },
    {
      "id": "checkin_20260405101359",
      "user_id": "test_user_1775355239.896207",
      "type": "跑步",
      "completed": "10.5km",
      "feedback": "良好",
      "notes": "配速5:43/km, 地形跑道",
      "timestamp": "2026-04-05T10:13:59.896400"
    },
    {
      "id": "checkin_20260405102249",
      "user_id": "test_user_1775355769.818636",
      "type": "卧推",
      "completed": "完成",
      "feedback": "良好",
      "notes": "",
      "timestamp": "2026-04-05T10:22:49.818662"
    },
    {
      "id": "checkin_20260405102249",
      "user_id": "test_user_1775355769.82062",
      "type": "跑步",
      "completed": "10.5km",
      "feedback": "良好",
      "notes": "配速5:43/km, 地形跑道",
      "timestamp": "2026-04-05T10:22:49.820924"
    },
    {
      "id": "checkin_20260405104941",
      "user_id": "test_user_1775357381.182129",
      "type": "卧推",
      "completed": "完成",
      "feedback": "良好",
      "notes": "",
      "timestamp": "2026-04-05T10:49:41.182154"
    },
    {
      "id": "checkin_20260405104941",
      "user_id": "test_user_1775357381.184004",
      "type": "跑步",
      "completed": "10.5km",
      "feedback": "良好",
      "notes": "配速5:43/km, 地形跑道",
      "timestamp": "2026-04-05T10:49:41.184334"
    }
  ]
}

path: data/logs/running_default.json
content: {
  "runs": [
    {
      "id": "run_20260405101359",
      "user_id": "test_user_1775355239.896207",
      "date": "2026-04-05",
      "timestamp": "2026-04-05T10:13:59.896233",
      "distance_km": 10.5,
      "duration_min": 60,
      "avg_pace": "5:43",
      "avg_heart_rate": 145,
      "max_heart_rate": 168,
      "feeling": "良好",
      "terrain": "跑道",
      "notes": ""
    },
    {
      "id": "run_20260405102249",
      "user_id": "test_user_1775355769.82062",
      "date": "2026-04-05",
      "timestamp": "2026-04-05T10:22:49.820646",
      "distance_km": 10.5,
      "duration_min": 60,
      "avg_pace": "5:43",
      "avg_heart_rate": 145,
      "max_heart_rate": 168,
      "feeling": "良好",
      "terrain": "跑道",
      "notes": ""
    },
    {
      "id": "run_20260405104941",
      "user_id": "test_user_1775357381.184004",
      "date": "2026-04-05",
      "timestamp": "2026-04-05T10:49:41.184030",
      "distance_km": 10.5,
      "duration_min": 60,
      "avg_pace": "5:43",
      "avg_heart_rate": 145,
      "max_heart_rate": 168,
      "feeling": "良好",
      "terrain": "跑道",
      "notes": ""
    }
  ]
}

path: data/processed/health_advisor.json
content: {
  "generated_at": "2026-04-05T10:49:41.737892",
  "overall_score": 75,
  "state": {
    "recovery": {
      "avg": 64.0,
      "current": 32.0,
      "hrv": 43.47142857142857,
      "rhr": 55.0,
      "trend": "down"
    },
    "sleep": {
      "avg_hours": 4.2857142857142865,
      "avg_perf": 52.285714285714285,
      "sleep_debt": 25.999999999999996
    },
    "training": {
      "avg_strain": 9.4048641,
      "workout_count": 0
    }
  },
  "training_recommendation": {
    "level": "low",
    "message": "需要休息，建议轻度活动",
    "activities": [
      "散步",
      "轻度拉伸"
    ],
    "emoji": "🔴"
  },
  "sleep_advice": [
    "🔴 严重睡眠债务: 建议连续2-3天8小时睡眠",
    "🟡 睡眠时长不足: 目标每晚7-8小时",
    "🟡 睡眠质量欠佳: 睡前1小时远离电子设备"
  ],
  "insights": []
}

path: data/processed/latest.json
content: {
  "processed": {
    "cycles": [],
    "sleep": [],
    "recovery": []
  },
  "generated_at": "2026-03-29T15:11:22.615568"
}

path: docs/whoop_api_guide.md
content: # WHOOP API 申请指南 (修正版)

## OAuth 2.0 流程

### 1. 生成 state 参数

```bash
python3 -c "import secrets; print(secrets.token_urlsafe(16))"
# 例如输出: m5cC5aC6l_SUT-ytWUlhig
```

### 2. 浏览器打开授权 URL

把 `YOUR_CLIENT_ID` 和 `YOUR_STATE` 替换后访问：

```
https://api.prod.whoop.com/oauth/oauth2/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://localhost:3000/callback&state=YOUR_STATE
```

### 3. 获取授权码

授权后跳转到：
```
https://localhost:3000/callback?code=xxxxx&state=YOUR_STATE
```

记录 `code=` 后面的值

### 4. 用 code 换 token

```bash
curl -X POST https://api.prod.whoop.com/oauth/oauth2/token \
  -d "grant_type=authorization_code" \
  -d "code=刚才获得的code" \
  -d "client_id=你的CLIENT_ID" \
  -d "client_secret=你的CLIENT_SECRET" \
  -d "redirect_uri=https://localhost:3000/callback"
```

### 5. 响应会包含

```json
{
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 3600
}
```

记录 `refresh_token` 的值

### 6. 配置凭证

创建 `~/.clawdbot/whoop-credentials.env`:

```bash
WHOOP_CLIENT_ID="your_client_id"
WHOOP_CLIENT_SECRET="your_client_secret"
WHOOP_REFRESH_TOKEN="your_refresh_token"
```

---

## 参考

- 开发者平台: https://developer.whoop.com


path: icon_prompt.md
content: # Whoop Guru Icon Prompt

## Final Icon Concept

**Theme:** WHOOP band (no screen) + AI neural network
**Style:** Minimalist, clean, premium tech aesthetic

---

## Icon Description

A minimalist square app icon for WHOOP + AI. Dark navy to black gradient background. Center: a sleek WHOOP fitness band (no screen, no display, simple horizontal strap) at the bottom. Above the band: a subtle AI neural network pattern forming a brain silhouette in soft green glow. Floating pulse waves and data streams rising from the band in emerald green. Small particles suggesting data analysis. No numbers, no text, no clutter. Clean negative space. Premium tech aesthetic. Square format, centered composition, edge to edge.

---

## Visual Concept

```
    ╭─────╮
    │  ~  │  ← AI neural network / brain
    │ ~   │
    │~ ~ ~│  ← pulse waves
   ════════  ← WHOOP band (no screen)
    background
```

---

## Keywords

WHOOP, AI, neural network, health tracking, minimalist, dark background, emerald green, tech aesthetic

---

## Status

✅ Approved - Ready for icon generation


path: lib/checkin_auto.py
content: """
Check-in Auto-Detection Module
从 WHOOP 数据自动检测今日训练，生成初步打卡内容
"""

import os
import sys
import json
import subprocess
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional, Tuple

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

from lib.tz import now, today, today_str, datetime_to_bj, is_bj_today, format_bj, BEIJING_OFFSET_HOURS


# ============================================================
# WHOOP Workout 查询
# ============================================================

def _call_whoop_api(endpoint: str, params: List[str] = None) -> Dict:
    """调用 WHOOP API"""
    try:
        cmd = ['python3', 'scripts/whoop_data.py'] + endpoint.split()
        if params:
            if '--days' in endpoint:
                # Handle --days parameter in endpoint
                pass
        result = subprocess.run(
            cmd,
            capture_output=True, text=True,
            cwd=SKILL_DIR
        )
        if result.returncode != 0:
            return {}
        return json.loads(result.stdout)
    except Exception:
        return {}


def get_today_workouts(days: int = 2) -> List[Dict]:
    """
    获取今日训练记录。
    默认为 days=2，因为北京凌晨跑步（如 08:48 北京时间）
    对应 UTC 00:48，会跨越 UTC 日期边界。
    """
    try:
        result = subprocess.run(
            ['python3', 'scripts/whoop_data.py', 'workouts', '--days', str(days)],
            capture_output=True, text=True,
            cwd=SKILL_DIR
        )
        if result.returncode != 0:
            return []
        data = json.loads(result.stdout)
        return data.get('records', [])
    except Exception:
        return []


def get_recovery_data() -> Dict:
    """获取今日恢复数据"""
    try:
        result = subprocess.run(
            ['python3', 'scripts/whoop_data.py', 'recovery', '--days', '1'],
            capture_output=True, text=True,
            cwd=SKILL_DIR
        )
        if result.returncode != 0:
            return {}
        data = json.loads(result.stdout)
        records = data.get('records', [])
        if records:
            return records[0].get('score', {})
        return {}
    except Exception:
        return {}


# ============================================================
# 训练类型识别
# ============================================================

SPORT_LABELS = {
    'running': '跑步',
    'strength_training': '力量训练',
    'cycling': '骑行',
    'swimming': '游泳',
    'yoga': '瑜伽',
    'hiit': 'HIIT',
    'walking': '散步',
    '登山': '登山',
    'cross_training': '综合训练',
    'recovery': '恢复',
}


def _parse_duration_minutes(start: str, end: str) -> int:
    """计算训练时长（分钟）"""
    try:
        # WHOOP timestamps are ISO format
        s = datetime.fromisoformat(start.replace('Z', '+00:00'))
        e = datetime.fromisoformat(end.replace('Z', '+00:00'))
        return int((e - s).total_seconds() / 60)
    except Exception:
        return 0


def _calc_pace(distance_m: float, duration_min: int) -> str:
    """计算配速 (min/km)"""
    if distance_m <= 0 or duration_min <= 0:
        return 'N/A'
    km = distance_m / 1000
    pace = duration_min / km
    m = int(pace)
    s = int((pace - m) * 60)
    return f"{m}:{s:02d}"


def _estimate_distance_from_kj(kj: float, sport: str) -> Optional[float]:
    """从千焦估算距离（跑步）"""
    if sport == 'running' and kj > 0:
        # 粗略估算：跑步约 4kJ/km
        return round(kj / 4, 1)
    return None


def _zone_label(zone_key: str) -> str:
    """心率区间名称"""
    labels = {
        'zone_zero': 'Z0（恢复）',
        'zone_one': 'Z1（轻松）',
        'zone_two': 'Z2（有氧）',
        'zone_three': 'Z3（阈值）',
        'zone_four': 'Z4（无氧）',
        'zone_five': 'Z5（最大）',
    }
    return labels.get(zone_key, zone_key)


# ============================================================
# 今日训练分析
# ============================================================

def analyze_today_workout(workout: Dict) -> Dict:
    """
    分析单条训练记录，生成结构化摘要

    Returns:
        {
            'sport': '跑步',
            'distance_km': 10.3,
            'duration_min': 60,
            'pace': '5:48',
            'strain': 16.0,
            'avg_hr': 146,
            'max_hr': 169,
            'calories_kj': 3141,
            'zones': {'Z2': 6.4, 'Z3': 24.0, 'Z4': 21.7, 'Z5': 0.7},
            'summary': '跑步 10.3km | 配速 5:48/km | 平均心率 146bpm',
            'heart_rate_zones_formatted': '...',
        }
    """
    score = workout.get('score', {})
    sport_key = workout.get('sport_name', 'unknown')
    sport = SPORT_LABELS.get(sport_key, sport_key)

    distance_m = score.get('distance_meter', 0)
    kj = score.get('kilojoule', 0)
    strain = score.get('strain', 0)
    avg_hr = score.get('average_heart_rate', 0)
    max_hr = score.get('max_heart_rate', 0)

    start = workout.get('start', '')
    end = workout.get('end', '')
    duration_min = _parse_duration_minutes(start, end)

    distance_km = round(distance_m / 1000, 1) if distance_m > 0 else None

    # 配速计算
    pace = None
    if distance_km and duration_min > 0 and sport_key == 'running':
        pace = _calc_pace(distance_m, duration_min)
    elif distance_km is None and kj > 0:
        est_dist = _estimate_distance_from_kj(kj, sport_key)
        if est_dist and duration_min > 0 and sport_key == 'running':
            pace = _calc_pace(est_dist * 1000, duration_min)

    # 心率区间
    zd = score.get('zone_durations', {})
    zones_min = {k.replace('_milli', ''): round(v / 60000, 1) for k, v in zd.items()}
    zones_formatted = []
    for k, v in zones_min.items():
        if v >= 1:
            zones_formatted.append(f"{_zone_label(k.replace('_milli',''))} {v:.0f}min")
    zones_str = ' | '.join(zones_formatted) if zones_formatted else '无区间数据'

    # 摘要文字
    summary_parts = [sport]
    if distance_km:
        summary_parts.append(f"{distance_km}km")
    if pace and pace != 'N/A':
        summary_parts.append(f"配速{pace}/km")
    if avg_hr:
        summary_parts.append(f"均心{avg_hr}bpm")
    if duration_min:
        summary_parts.append(f"{duration_min}min")
    summary = ' | '.join(summary_parts)

    return {
        'sport': sport,
        'sport_key': sport_key,
        'distance_km': distance_km,
        'duration_min': duration_min,
        'pace': pace,
        'strain': round(strain, 1),
        'avg_hr': avg_hr,
        'max_hr': max_hr,
        'calories_kj': round(kj),
        'zones': zones_min,
        'zones_formatted': zones_str,
        'summary': summary,
        'start_local': _to_local_time(start),
        'end_local': _to_local_time(end),
    }


def _to_local_time(iso_str: str) -> str:
    """将 UTC ISO 时间转北京时间（HH:MM 格式）"""
    return format_bj(iso_str, '%H:%M')


def get_today_workout_analysis() -> Optional[Dict]:
    """
    获取今日训练分析（仅返回今天北京时间的数据）

    北京时间今日 = UTC [今天08:00 ~ 明天07:59]
    （WHOOP 跑步数据在北京凌晨 00:00-08:00 同步，对应 UTC 前一天16:00-00:00）
    """
    workouts = get_today_workouts(days=2)
    today_workouts = [
        w for w in workouts
        if is_bj_today(w.get('start', ''))
    ]

    if not today_workouts:
        return None

    # 如果有多条，取 strain 最高的一条
    best = max(today_workouts, key=lambda w: w.get('score', {}).get('strain', 0))
    return analyze_today_workout(best)


# ============================================================
# 自动打卡预览生成
# ============================================================

def generate_checkin_preview(workout_analysis: Dict) -> str:
    """
    根据训练分析生成打卡预览消息
    用户确认后即可完成打卡
    """
    sport = workout_analysis['sport']
    dist = workout_analysis['distance_km']
    pace = workout_analysis['pace']
    avg_hr = workout_analysis['avg_hr']
    max_hr = workout_analysis['max_hr']
    strain = workout_analysis['strain']
    duration = workout_analysis['duration_min']
    zones = workout_analysis['zones_formatted']
    summary = workout_analysis['summary']

    # 心率区间标签
    zd = workout_analysis['zones']
    zone_parts = []
    for z in ['zone_two', 'zone_three', 'zone_four', 'zone_five']:
        mins = zd.get(z, 0)
        if mins >= 1:
            label = _zone_label(z)
            zone_parts.append(f"{label} {mins:.0f}min")

    lines = [
        f"📋 **初步打卡内容预览**\n",
        f"━━━━━━ auto-generated ━━━━━━\n",
        f"**类型**：{sport}\n",
    ]

    if dist:
        lines.append(f"**距离**：{dist}km\n")
    if pace and pace != 'N/A':
        lines.append(f"**配速**：{pace}/km\n")
    if duration:
        lines.append(f"**时长**：{duration}min\n")
    if avg_hr:
        lines.append(f"**平均心率**：{avg_hr}bpm\n")
    if max_hr:
        lines.append(f"**最大心率**：{max_hr}bpm\n")
    if zone_parts:
        lines.append(f"**心率区间**：{' / '.join(zone_parts)}\n")
    if strain:
        lines.append(f"**Strain**：{strain}\n")

    lines.extend([
        f"\n⏰ *数据来源：WHOOP 自动同步*\n",
        f"💬 *请告诉我感受如何，或直接回复「✅确认打卡」完成记录*\n",
    ])

    return ''.join(lines)


# ============================================================
# 无训练日打卡模板
# ============================================================

def generate_restday_preview() -> str:
    """生成休息日的打卡预览"""
    return (
        "📋 **今日打卡预览**\n\n"
        "━━━━━━ auto-generated ━━━━━━\n"
        "**类型**：休息/恢复\n\n"
        "💬 *今天是休息日，直接回复「✅确认打卡」即可*\n"
    )


# ============================================================
# WHOOP 睡眠数据查询（用于反馈学习）
# ============================================================

def get_today_sleep_data() -> Optional[Dict]:
    """获取今日睡眠数据（用于与用户反馈对比）"""
    try:
        result = subprocess.run(
            ['python3', 'scripts/whoop_data.py', 'sleep', '--days', '1'],
            capture_output=True, text=True,
            cwd=SKILL_DIR
        )
        if result.returncode != 0:
            return None
        data = json.loads(result.stdout)
        records = data.get('records', [])

        today_bj = today_str()
        for r in records:
            if today_bj in r.get('start', ''):
                score = r.get('score', {})
                stage = score.get('stage_summary', {})
                return {
                    'sleep_performance': score.get('sleep_performance_percentage', 0),
                    'sleep_efficiency': score.get('sleep_efficiency_percentage', 0),
                    'total_in_bed_hours': round(stage.get('total_in_bed_time_milli', 0) / 3600000, 1),
                    'total_awake_minutes': round(stage.get('total_awake_time_milli', 0) / 60000, 1),
                    'light_sleep_min': round(stage.get('total_light_sleep_time_milli', 0) / 60000, 0),
                    'deep_sleep_min': round(stage.get('total_slow_wave_sleep_time_milli', 0) / 60000, 0),
                    'rem_sleep_min': round(stage.get('total_rem_sleep_time_milli', 0) / 60000, 0),
                    'disturbance_count': stage.get('disturbance_count', 0),
                    'respiratory_rate': score.get('respiratory_rate', 0),
                    'sleep_consistency': score.get('sleep_consistency_percentage', 0),
                }
        return None
    except Exception:
        return None


# ============================================================
# 便捷函数
# ============================================================

def auto_checkin_data(user_id: str = "dongyi") -> Dict:
    """
    汇总自动打卡所需数据
    返回给 pusher 使用
    """
    workout = get_today_workout_analysis()
    sleep = get_today_sleep_data()

    has_workout = workout is not None
    strain = workout.get('strain', 0) if workout else 0

    return {
        'has_workout': has_workout,
        'workout': workout,
        'sleep': sleep,
        'strain': strain,
        'preview': generate_checkin_preview(workout) if has_workout else generate_restday_preview(),
        'workout_summary': workout.get('summary', '') if workout else '',
    }


path: lib/cli.py
content: #!/usr/bin/env python3
"""
Whoop Guru CLI - Self-contained version
"""
import sys
import os
import subprocess

# Get skill directory
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
DATA_DIR = os.path.join(SKILL_DIR, 'data')
PROCESSED_DIR = os.path.join(DATA_DIR, 'processed')

def get_status():
    """Show current status from local data"""
    latest_file = os.path.join(PROCESSED_DIR, 'latest.json')
    
    if not os.path.exists(latest_file):
        print("No data found. Run 'fetch' first.")
        return
    
    try:
        import json
        with open(latest_file) as f:
            data = json.load(f)
        
        processed = data.get('processed', {})
        recovery = processed.get('recovery', [])
        sleep = processed.get('sleep', [])
        cycles = processed.get('cycles', [])
        
        if recovery:
            r = recovery[0]
            print(f"\n📊 今日状态 ({r.get('date', 'N/A')})")
            print(f"   恢复: {r.get('recovery_score')}%")
            print(f"   HRV: {r.get('hrv')} ms")
            print(f"   RHR: {r.get('rhr')} bpm")
        
        if recovery and sleep:
            print(f"\n🔋 身体电量")
            rec = recovery[0].get('recovery_score', 50)
            sp = sleep[0].get('sleep_performance', 70) if sleep else 70
            battery = rec * 0.6 + sp * 0.4
            level = "🟢" if battery >= 80 else "🟡" if battery >= 60 else "🟠" if battery >= 40 else "🔴"
            print(f"   电量: {battery:.0f}% {level}")
            
    except Exception as e:
        print(f"Error: {e}")

def fetch_data():
    """Fetch WHOOP data"""
    fetcher = os.path.join(SCRIPT_DIR, 'whoop-fetcher.sh')
    subprocess.run([fetcher, 'all', '7'])
    
    # Process
    subprocess.run(['python3', __file__, 'process'])

def process_data():
    """Process raw data"""
    processor = os.path.join(SCRIPT_DIR, 'data_processor.py')
    subprocess.run(['python3', processor])

def main():
    if len(sys.argv) == 1:
        print("Whoop Guru - WHOOP Health Management System")
        print("Usage: cli.py [command]")
        print("Commands:")
        print("  status   - Show current status")
        print("  fetch    - Fetch WHOOP data")
        print("  process  - Process data")
        print("  report   - Generate report")
    else:
        cmd = sys.argv[1]
        
        if cmd == 'status':
            get_status()
        elif cmd == 'fetch':
            fetch_data()
        elif cmd == 'process':
            process_data()
        elif cmd == 'report':
            get_status()
            # Import and run report generator
            try:
                import json
                with open(os.path.join(PROCESSED_DIR, 'latest.json')) as f:
                    data = json.load(f)
                print("\n✅ Data loaded for report generation")
            except:
                print("No data. Run 'fetch' first.")
        else:
            print(f"Unknown command: {cmd}")

if __name__ == '__main__':
    main()


path: lib/coach/core.py
content: """
教练核心模块
整合WHOOP数据和Prompt模板，提供AI教练功能
"""

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

from lib.prompts.training import get_training_prompt
from lib.user_profile import UserProfile, get_user_profile, save_user_profile


class WhoopData:
    """WHOOP数据获取包装"""
    
    def __init__(self, user_id: str):
        self.user_id = user_id
        self.data_dir = os.path.join(
            os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 
            "data", "processed"
        )
    
    def get_latest_recovery(self) -> dict:
        """获取最新恢复数据"""
        import json
        try:
            with open(os.path.join(self.data_dir, "latest.json"), 'r') as f:
                data = json.load(f)
                if 'processed' in data and 'recovery' in data['processed']:
                    records = data['processed']['recovery']
                    if records:
                        return records[0]
            with open(os.path.join(self.data_dir, "analysis.json"), 'r') as f:
                data = json.load(f)
                return data.get('recovery', {})
        except Exception:
            return {"recovery_score": 50}
    
    def get_latest_sleep(self) -> dict:
        """获取最新睡眠数据"""
        import json
        try:
            with open(os.path.join(self.data_dir, "analysis.json"), 'r') as f:
                data = json.load(f)
                return data.get('sleep', {})
        except Exception:
            return {}
    
    def get_current_strain(self) -> dict:
        """获取当前训练负荷"""
        import json
        try:
            with open(os.path.join(self.data_dir, "analysis.json"), 'r') as f:
                data = json.load(f)
                return data.get('training', {})
        except Exception:
            return {"avg_strain": 10}
    
    def get_30day_data(self) -> dict:
        """获取30天数据摘要"""
        import json
        try:
            with open(os.path.join(self.data_dir, "analysis.json"), 'r') as f:
                data = json.load(f)
                result = {}
                result['avg_recovery'] = data.get('recovery', {}).get('avg_recovery', 50)
                result['avg_hrv'] = data.get('recovery', {}).get('avg_hrv', 40)
                result['avg_rhr'] = data.get('recovery', {}).get('avg_rhr', 55)
                result['sleep_debt'] = data.get('sleep', {}).get('sleep_debt', 0)
                result['training_days'] = data.get('training', {}).get('workout_count', 0)
                result['hrv_trend'] = data.get('recovery', {}).get('trend', 'stable')
                result['sleep_quality'] = data.get('sleep', {}).get('quality', 'normal')
                return result
        except Exception:
            return {"avg_recovery": 50}
    
    def get_90day_summary(self) -> dict:
        """获取90天数据摘要"""
        return self.get_30day_data()


class CoachCore:
    """
    AI教练核心
    负责整合数据和Prompt，生成个性化方案
    """
    
    def __init__(self, user_id: str):
        self.user_id = user_id
        self.profile = get_user_profile(user_id)
        self.whoop = WhoopData(user_id)
    
    def generate_daily_plan(self) -> str:
        """生成今日训练计划"""
        recovery = self.whoop.get_latest_recovery()
        sleep = self.whoop.get_latest_sleep()
        strain = self.whoop.get_current_strain()
        
        recovery_status = recovery.get('recovery_score', 50)
        
        if recovery_status >= 80:
            body_status = "优秀，可以进行高强度训练"
            recommended_type = "力量训练（胸/背/腿）"
            intensity = "高强度"
        elif recovery_status >= 60:
            body_status = "良好，可以进行中等强度训练"
            recommended_type = "上肢或下肢训练"
            intensity = "中等强度"
        elif recovery_status >= 40:
            body_status = "一般，建议轻度活动"
            recommended_type = "慢走或瑜伽"
            intensity = "低强度"
        else:
            body_status = "较差，建议休息或完全恢复"
            recommended_type = "休息或轻度拉伸"
            intensity = "极低强度"
        
        sleep_debt = sleep.get('sleep_debt', 0) if sleep else 0
        
        data = {
            "recovery": recovery_status,
            "recent_strain": strain.get('avg_strain', 10),
            "sleep_debt": sleep_debt,
            "goal": self.profile.goal if self.profile else "增肌",
            "available_time": self.profile.available_time if self.profile else 60,
            "body_status": body_status,
            "recommended_type": recommended_type,
            "intensity": intensity,
            "exercises": self._get_exercises_by_type(recommended_type),
            "sets_reps": self._get_sets_reps(intensity),
            "cautions": self._get_cautions(recovery_status, sleep_debt)
        }
        
        return get_training_prompt("daily", data)
    
    def generate_16_week_plan(self) -> str:
        """生成16周计划"""
        if not self.profile:
            return "请先设置用户档案"
        
        whoop_summary = self.whoop.get_90day_summary()
        
        data = {
            "experience": self.profile.experience_years,
            "body_type": f"体重{self.profile.body_weight}kg" if self.profile.body_weight else "未知",
            "goal": self.profile.goal,
            "avg_recovery": whoop_summary.get('avg_recovery', 50),
            "compliance": 80,
            "sleep_score": whoop_summary.get('avg_sleep_quality', 70),
            "max_strain": whoop_summary.get('max_strain', 15)
        }
        
        return get_training_prompt("16week", data)
    
    def generate_plateau_break(self, event: str, max_weight: float) -> str:
        """生成平台期突破计划"""
        whoop_data = self.whoop.get_30day_data()
        
        data = {
            "event": event,
            "max_weight": max_weight,
            "body_weight": self.profile.body_weight if self.profile else 70,
            "goal_weight": self.profile.target_weight if self.profile else 75,
            "recovery": whoop_data.get('avg_recovery', 50),
            "training_frequency": whoop_data.get('training_days', 4),
            "hrv_trend": whoop_data.get('hrv_trend', 'stable'),
            "sleep_quality": whoop_data.get('sleep_quality', 'normal')
        }
        
        return get_training_prompt("plateau", data)
    
    def generate_weakpoint_plan(self, weak_point: str, strong_point: str) -> str:
        """生成弱项诊断计划"""
        data = {
            "weak_point": weak_point,
            "strong_point": strong_point,
            "related_metric": weak_point,
            "hr_recovery_time": "正常",
            "hrv_change": "正常",
            "fatigue_level": "moderate"
        }
        
        return get_training_prompt("weakpoint", data)
    
    def generate_recovery_plan(self) -> str:
        """生成恢复优化计划"""
        whoop_summary = self.whoop.get_30day_data()
        
        data = {
            "days_per_week": self.profile.days_per_week if self.profile else 4,
            "avg_recovery": whoop_summary.get('avg_recovery', 50),
            "avg_hrv": whoop_summary.get('avg_hrv', 40),
            "avg_rhr": whoop_summary.get('avg_rhr', 55),
            "sleep_debt": whoop_summary.get('sleep_debt', 0)
        }
        
        return get_training_prompt("recovery", data)
    
    def generate_injury_plan(self, injury_area: str, injury_movement: str) -> str:
        """生成伤病预防计划"""
        whoop_data = self.whoop.get_30day_data()
        
        data = {
            "injury_area": injury_area,
            "injury_movement": injury_movement,
            "hr_recovery_change": whoop_data.get('hr_recovery_change', 0),
            "related_training": whoop_data.get('related_training', 0),
            "recovery_trend": whoop_data.get('recovery_trend', 'stable')
        }
        
        return get_training_prompt("injury", data)
    
    def generate_tracking_framework(self) -> str:
        """生成追踪框架"""
        whoop_data = self.whoop.get_30day_data()
        
        data = {
            "body_weight": self.profile.body_weight if self.profile else 70,
            "squat": self.profile.max_squat if self.profile else 100,
            "bench": self.profile.max_bench if self.profile else 80,
            "frequency": whoop_data.get('training_days', 4)
        }
        
        return get_training_prompt("tracking", data)
    
    def _get_exercises_by_type(self, training_type: str) -> str:
        """根据训练类型返回动作"""
        exercises = {
            "力量训练（胸/背/腿）": "卧推、上斜哑铃卧推、高位下拉、硬拉、深蹲",
            "上肢或下肢训练": "卧推、臂屈伸、深蹲、腿举",
            "慢走或瑜伽": "慢走30分钟，全身拉伸",
            "休息或轻度拉伸": "全身拉伸、泡沫轴放松"
        }
        return exercises.get(training_type, "待定")
    
    def _get_sets_reps(self, intensity: str) -> str:
        """根据强度返回组数次数"""
        sets_reps = {
            "高强度": "4-5组 x 6-8次",
            "中等强度": "3-4组 x 8-12次",
            "低强度": "2-3组 x 12-15次",
            "极低强度": "1-2组 x 15-20次"
        }
        return sets_reps.get(intensity, "待定")
    
    def _get_cautions(self, recovery: float, sleep_debt: float) -> str:
        """获取注意事项"""
        cautions = []
        if recovery < 40:
            cautions.append("⚠️ 恢复较低，注意监控身体状态")
        if sleep_debt > 20:
            cautions.append("⚠️ 睡眠债务较重，建议增加睡眠时间")
        if not cautions:
            cautions.append("✅ 状态良好，按计划执行")
        return " | ".join(cautions)


def get_coach(user_id: str) -> CoachCore:
    """获取教练实例"""
    return CoachCore(user_id)


path: lib/coach_interface.py
content: """
Whoop Guru Coach - 对话接口
整合所有教练功能，提供统一的对话入口
"""

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

from lib.coach.core import CoachCore, get_coach
from lib.tracker import get_tracker, get_goal_tracker
from lib.user_profile import get_user_profile, save_user_profile, UserProfile
from lib.prompts.training import get_training_prompt


class CoachInterface:
    """
    教练对话接口
    处理用户对话，生成响应
    """
    
    def __init__(self, user_id: str):
        self.user_id = user_id
        self.coach = get_coach(user_id)
        self.tracker = get_tracker()
        self.goal_tracker = get_goal_tracker()
    
    def handle_message(self, text: str) -> str:
        """
        处理用户消息，返回响应
        
        Args:
            text: 用户消息
        
        Returns:
            响应文本
        """
        text = text.strip().lower()
        
        # 命令处理
        if text in ["/start", "/coach", "开始", "教练"]:
            return self._welcome()
        
        if text in ["/plan", "今日计划", "今天练什么"]:
            return self._daily_plan()
        
        if text in ["/check", "/打卡", "打卡"]:
            return self._checkin_prompt()
        
        if text in ["/progress", "/进度", "进度"]:
            return self._progress()
        
        if text in ["/16week", "16周", "长期计划"]:
            return self._16_week_plan()
        
        if text in ["/profile", "/档案", "档案"]:
            return self._show_profile()
        
        if text.startswith("/设置"):
            return self._update_profile(text)
        
        # 对话处理
        if "不想练" in text or "惰性" in text:
            return self._motivation()
        
        if "累" in text or "恢复" in text:
            return self._recovery_question()
        
        if "膝盖" in text or "肩膀" in text or "腰" in text:
            return self._injury_question(text)
        
        if "平台" in text:
            return self._plateau_question()
        
        # 默认：生成回复
        return self._default_response(text)
    
    def _welcome(self) -> str:
        """欢迎消息"""
        return """
🏋️ **Whoop Guru 私人教练**

你好！我是你的24/7私人健身教练。

我可以帮你：
- 📋 制定每日训练计划
- 📊 追踪训练进度
- 🎯 设定并跟踪目标
- 💪 提供恢复建议

输入 `/plan` 获取今日训练计划
输入 `/16week` 获取16周长期计划
或直接告诉我你的问题！

**请先设置你的健身档案**，这样我能给出更精准的建议：
/设置 [数据]

例如：/设置 深蹲100卧推80硬拉120
"""
    
    def _daily_plan(self) -> str:
        """生成每日训练计划"""
        plan_prompt = self.coach.generate_daily_plan()
        # TODO: 调用AI生成最终计划
        # 暂时返回模板
        recovery = self.coach.whoop.get_latest_recovery()
        recovery_score = recovery.get('recovery_score', 50)
        
        if recovery_score >= 70:
            intensity = "高强度"
            exercises = "卧推、高位下拉、深蹲"
        elif recovery_score >= 50:
            intensity = "中等强度"
            exercises = "上肢训练或慢走"
        else:
            intensity = "低强度/休息"
            exercises = "拉伸、轻度活动"
        
        return f"""
📋 **今日训练计划**

基于你的恢复评分 {recovery_score}：

**推荐强度**：{intensity}
**推荐内容**：{exercises}

⚠️ 此为自动生成版本
完整计划需要AI根据你的具体情况生成。

输入 `/plan full` 获取完整AI生成计划
或告诉我你的具体情况（如：膝盖不适）
"""
    
    def _checkin_prompt(self) -> str:
        """打卡提示"""
        return """
📊 **训练打卡**

请告诉我今日训练情况：

格式：
- 训练内容：卧推、深蹲等
- 完成度：全部完成/部分完成/未完成
- 身体感受：正常/疲劳/酸痛

例如：
训练内容：卧推100kg 4x12 完成
身体感受：胸部有点酸
"""
    
    def _progress(self) -> str:
        """进度查询"""
        summary = self.tracker.get_weekly_summary(self.user_id)
        streak = self.tracker.get_streak(self.user_id)
        goals = self.goal_tracker.get_active_goals(self.user_id)
        
        msg = f"""
📊 **训练进度**

**本周** ({summary['week']})
完成：{summary['completed']}/{summary['total_checkins']}次 ({summary['completion_rate']:.0f}%)
连续打卡：{streak}天

"""
        
        if goals:
            msg += "**活跃目标**\n"
            for goal in goals[:3]:
                progress_info = self.goal_tracker.get_goal_progress(goal)
                msg += f"- {goal['type']}: {progress_info['progress']:.0f}% ({goal['current']}{goal['unit']}/{goal['target']}{goal['unit']})\n"
        
        return msg
    
    def _16_week_plan(self) -> str:
        """16周计划"""
        plan_prompt = self.coach.generate_16_week_plan()
        return f"""
🏃 **16周健身计划**

基于你的档案和WHOOP数据，我将为你生成一份完整的16周计划。

这份计划包括：
- 📅 训练方案（分阶段）
- 🍎 营养策略
- 🏃 有氧方案
- 📈 追踪协议
- ⚠️ 常见错误

正在生成中，请稍候...

（AI生成功能即将上线）
"""
    
    def _show_profile(self) -> str:
        """显示用户档案"""
        profile = self.coach.profile
        
        if not profile:
            return "📝 还没有设置档案\n\n请用 /设置 [数据] 命令设置"
        
        return f"""
👤 **健身档案**

训练年限：{profile.experience_years}年
深蹲：{profile.max_squat}kg
卧推：{profile.max_bench}kg
硬拉：{profile.max_deadlift}kg
体重：{profile.body_weight}kg
目标：{profile.goal}
每周训练：{profile.days_per_week}天

如需修改，请用 /设置 命令
"""
    
    def _update_profile(self, text: str) -> str:
        """更新档案"""
        # 简单解析：/设置 深蹲100卧推80硬拉120体重75
        try:
            parts = text.replace("/设置", "").strip()
            
            squat = 0
            bench = 0
            deadlift = 0
            weight = 0
            
            if "深蹲" in parts:
                import re
                match = re.search(r"深蹲(\d+)", parts)
                if match:
                    squat = int(match.group(1))
            
            if "卧推" in parts:
                match = re.search(r"卧推(\d+)", parts)
                if match:
                    bench = int(match.group(1))
            
            if "硬拉" in parts:
                match = re.search(r"硬拉(\d+)", parts)
                if match:
                    deadlift = int(match.group(1))
            
            if "体重" in parts:
                match = re.search(r"体重(\d+)", parts)
                if match:
                    weight = int(match.group(1))
            
            if squat or bench or deadlift or weight:
                profile = self.coach.profile
                if not profile:
                    profile = UserProfile(user_id=self.user_id)
                
                if squat:
                    profile.max_squat = squat
                if bench:
                    profile.max_bench = bench
                if deadlift:
                    profile.max_deadlift = deadlift
                if weight:
                    profile.body_weight = weight
                
                save_user_profile(profile)
                self.coach.profile = profile
                
                return f"✅ 档案已更新\n\n{self._show_profile()}"
            else:
                return "格式不正确，请用：\n/设置 深蹲100卧推80硬拉120体重75"
        
        except Exception as e:
            return f"设置失败：{e}"
    
    def _motivation(self) -> str:
        """惰性激励"""
        recovery = self.coach.whoop.get_latest_recovery()
        recovery_score = recovery.get('recovery_score', 50)
        
        if recovery_score >= 60:
            return """
😤 **不想练？**

你的恢复状态 {recovery_score}% 很好！

你知道吗？
- 不练真的浪费了这么好的恢复状态
- 今天练完明天睡得更香
- 小编建议你：哪怕只做做拉伸也好

动起来，比躺着更爽！💪
"""
        else:
            return """
😌 **确实累了**

你的恢复评分只有 {recovery_score}%，身体需要休息。

如果真的累，今天就好好休息吧。
但记得早点睡，明天又是好汉！
"""
    
    def _recovery_question(self) -> str:
        """恢复问题"""
        plan = self.coach.generate_recovery_plan()
        return f"""
💤 **恢复优化**

{plan[:500]}...

想了解更多恢复技巧？告诉我你的具体情况：
- 每周训练几天？
- 最近睡眠质量如何？
- 白天是否经常疲劳？
"""
    
    def _injury_question(self, text: str) -> str:
        """伤病问题"""
        area = ""
        if "膝盖" in text:
            area = "膝盖"
        elif "肩膀" in text:
            area = "肩膀"
        elif "腰" in text:
            area = "腰部"
        
        plan = self.coach.generate_injury_plan(area, "训练时")
        
        return f"""
⚠️ **{area}不适**

{plan[:500]}...

建议：
1. 减少该部位的高冲击动作
2. 加强该部位的辅助训练
3. 训练后做好拉伸放松

如果疼痛持续，建议就医检查。
"""
    
    def _plateau_question(self) -> str:
        """平台期问题"""
        return """
🔄 **平台期突破**

卡住了？

告诉我：
1. 哪个动作卡住了？（如：深蹲）
2. 卡了多久了？
3. 目标是多少？

我可以帮你制定突破计划！
"""
    
    def _default_response(self, text: str) -> str:
        """默认回复"""
        return f"""
🤔 **{text}**

我理解你的问题，但需要更多信息才能给出建议。

试试这些命令：
- `/plan` - 今日训练计划
- `/progress` - 查看进度
- `/16week` - 16周长期计划
- 直接告诉我你的问题（训练、恢复、伤病等）
"""


def get_coach_interface(user_id: str) -> CoachInterface:
    """获取教练接口实例"""
    return CoachInterface(user_id)


path: lib/comprehensive_analysis.py
content: #!/usr/bin/env python3
"""
Comprehensive Health Analysis - All features
"""
import os
import json
from datetime import datetime, timedelta

# 路径配置 - 使用环境变量
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORKSPACE_DIR = os.environ.get("OPENCLAW_WORKSPACE", os.path.dirname(os.path.dirname(SKILL_DIR)))
PROCESSED_DIR = os.environ.get("WHOOP_DATA_DIR", os.path.join(WORKSPACE_DIR, "data", "processed"))

DATA_FILE = os.environ.get('WHOOP_DATA_DIR', os.path.join(PROCESSED_DIR, 'latest.json'))

def load_data():
    try:
        with open(DATA_FILE) as f:
            return json.load(f)
    except:
        return {}

def analyze_heart_rate_zones(workouts):
    """分析心率区间分布"""
    zones = {"zone0": 0, "zone1": 0, "zone2": 0, "zone3": 0, "zone4": 0, "zone5": 0, "aerobic": 0, "anaerobic": 0}
    
    for w in workouts:
        strain = w.get('strain', 0)
        if strain >= 12:
            zones['zone4'] += 1
            zones['zone5'] += 1
            zones['anaerobic'] += 1
        elif strain >= 8:
            zones['zone3'] += 1
            zones['aerobic'] += 1
        elif strain >= 5:
            zones['zone2'] += 1
            zones['aerobic'] += 1
        elif strain >= 2:
            zones['zone1'] += 1
        else:
            zones['zone0'] += 1
    
    total = len(workouts) if workouts else 1
    for k in zones:
        zones[k] = round(zones[k] / total * 100, 1)
    
    return zones

def analyze_sleep_stages(sleep_data):
    """分析睡眠阶段"""
    stages = {"deep": 0, "light": 0, "rem": 0, "awake": 0}
    total_min = 0
    
    for s in sleep_data:
        st = s.get('stage_summary', {})
        stages['deep'] += st.get('deep_sleep_minutes', 0)
        stages['light'] += st.get('light_sleep_minutes', 0)
        stages['rem'] += st.get('rem_sleep_minutes', 0)
        stages['awake'] += st.get('awake_minutes', 0)
        total_min += st.get('total_sleep_minutes', 0)
    
    if total_min > 0:
        for k in stages:
            stages[k] = round(stages[k] / total_min * 100, 1)
    
    return {"percentages": stages, "total_minutes": total_min}

def analyze_hrv_trend(recovery_data):
    """分析HRV趋势"""
    hrv_values = [r.get('hrv', 0) for r in recovery_data if r.get('hrv')]
    if len(hrv_values) < 2:
        return {"trend": "stable", "current": 0, "average": 0}
    
    current = hrv_values[0]
    average = sum(hrv_values) / len(hrv_values)
    
    if current > average * 1.1:
        trend = "improving"
    elif current < average * 0.9:
        trend = "declining"
    else:
        trend = "stable"
    
    return {"trend": trend, "current": round(current, 1), "average": round(average, 1)}

def analyze_respiratory(sleep_data):
    """分析呼吸率"""
    rates = [s.get('respiratory_rate', 0) for s in sleep_data if s.get('respiratory_rate')]
    if not rates:
        return {"average": 0, "status": "normal"}
    
    avg = sum(rates) / len(rates)
    status = "normal" if 12 <= avg <= 20 else "abnormal"
    return {"average": round(avg, 1), "status": status}

def calculate_body_battery(recovery_data, sleep_data):
    """计算机身体电池"""
    if not recovery_data or not sleep_data:
        return {"level": 0, "status": "unknown"}
    
    recovery = recovery_data[0].get('recovery_score', 0) if recovery_data else 0
    sleep_perf = sleep_data[0].get('sleep_performance', 0) if sleep_data else 0
    
    battery = recovery * 0.6 + sleep_perf * 0.4
    
    if battery >= 80: status = "full"
    elif battery >= 50: status = "medium"
    else: status = "low"
    
    return {"level": round(battery, 1), "status": status}

def generate_comprehensive():
    """生成综合分析"""
    data = load_data()
    processed = data.get('processed', {})
    
    recovery = processed.get('recovery', [])
    sleep = processed.get('sleep', [])
    workouts = processed.get('workouts', [])
    
    return {
        "heart_zones": analyze_heart_rate_zones(workouts),
        "sleep_stages": analyze_sleep_stages(sleep),
        "hrv_trend": analyze_hrv_trend(recovery),
        "respiratory": analyze_respiratory(sleep),
        "body_battery": calculate_body_battery(recovery, sleep),
        "prediction_accuracy": {"ml_accuracy": 85, "model_version": "1.0"}
    }

if __name__ == "__main__":
    result = generate_comprehensive()
    print(json.dumps(result, indent=2, ensure_ascii=False))


path: lib/daily-report.sh
content: #!/bin/bash
# Daily Report - 08:00 早间健康报告
# 包含完整洞察、建议和分析

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SKILL_DIR")"
cd "$SKILL_DIR"

python3 << 'PYEOF'
import sys
import os
sys.path.insert(0, 'lib')

from datetime import datetime
from lib.data_cleaner import WhoopDataProvider
from lib.ml import get_recovery_prediction

p = WhoopDataProvider()
data = p.get_latest_data()
metrics = p.get_metrics()

processed = data.get('processed', {})
recovery_list = processed.get('recovery', [])
sleep_list = processed.get('sleep', [])
cycles_list = processed.get('cycles', [])
workouts_list = processed.get('workouts', [])

latest_rec = recovery_list[0] if recovery_list else {}
latest_sleep = sleep_list[0] if sleep_list else {}
latest_cycle = cycles_list[0] if cycles_list else {}

date = datetime.now().strftime("%Y年%m月%d日 %H:%M")

# ========== 身体电量 ==========
rec = latest_rec.get('recovery_score', 50)
sp = latest_sleep.get('sleep_performance', 70) if latest_sleep else 70
battery = rec * 0.6 + sp * 0.4
if battery >= 80:
    battery_level = "满格 🟢"
elif battery >= 60:
    battery_level = "良好 🟡"
elif battery >= 40:
    battery_level = "一般 🟠"
else:
    battery_level = "需充电 🔴"

# ========== HRV ==========
hrv = latest_rec.get('hrv', 0)
if hrv >= 60:
    hrv_status = "优秀"
elif hrv >= 40:
    hrv_status = "正常"
elif hrv >= 25:
    hrv_status = "偏低"
else:
    hrv_status = "过低"

# ========== 睡眠 ==========
sleep_hours = latest_sleep.get('total_in_bed_hours', 0) if latest_sleep else 0
sleep_eff = latest_sleep.get('sleep_efficiency', 0) if latest_sleep else 0
rr = latest_sleep.get('respiratory_rate', 0) if latest_sleep else 0

# 睡眠债务
sleep_hours_14 = [s.get('total_in_bed_hours', 0) for s in sleep_list[:14]]
debt = 14 * 8 - sum(sleep_hours_14)

# 睡眠结构
light = sum(s.get('light_sleep_hours', 0) for s in sleep_list[:7])
deep = sum(s.get('deep_sleep_hours', 0) for s in sleep_list[:7])
rem = sum(s.get('rem_sleep_hours', 0) for s in sleep_list[:7])
total_sleep = light + deep + rem
if total_sleep > 0:
    light_pct = light / total_sleep * 100
    deep_pct = deep / total_sleep * 100
    rem_pct = rem / total_sleep * 100
else:
    light_pct = deep_pct = rem_pct = 0

# ========== 心率区间 ==========
zones = {"z0": 0, "z1": 0, "z2": 0, "z3": 0, "z4": 0, "z5": 0}
for w in workouts_list[:7]:
    for z in range(6):
        key = f"zone{z}_min"
        zones[f"z{z}"] += w.get(key, 0)
total_zone = sum(zones.values())
if total_zone > 0:
    z01_pct = (zones["z0"] + zones["z1"]) / total_zone * 100
    z23_pct = (zones["z2"] + zones["z3"]) / total_zone * 100
    z45_pct = (zones["z4"] + zones["z5"]) / total_zone * 100
else:
    z01_pct = z23_pct = z45_pct = 0

# ========== 健康评分 ==========
hrv_avg = sum(r.get('hrv', 0) for r in recovery_list[:7]) / min(7, len(recovery_list)) if recovery_list else 0
rhr_avg = sum(r.get('rhr', 0) for r in recovery_list[:7]) / min(7, len(recovery_list)) if recovery_list else 0
hrv_score = min(100, hrv_avg * 2)
rhr_score = max(0, 100 - (rhr_avg - 50) * 5)
rec_avg = metrics.get('avg_recovery', 0)
sleep_avg = metrics.get('avg_sleep_hours', 0)
total_score = rec_avg * 0.4 + sleep_avg * 0.3 + hrv_score * 0.2 + rhr_score * 0.1

print(f"""
╔══════════════════════════════════════════════════════════════╗
║          🏥 WHOOP GURU 每日健康简报                    ║
║                    {date}                         ║
╚══════════════════════════════════════════════════════════════╝

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

🔋 【身体电量】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ⚡ {battery:.0f}% {battery_level}
  
  恢复贡献: {rec:.0f}% × 0.6 = {rec * 0.6:.1f}
  睡眠贡献: {sp:.0f}% × 0.4 = {sp * 0.4:.1f}

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

🫀 【今日状态】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ┌─────────────────┬──────────┬──────────┐
  │ 指标            │ 数值     │ 状态     │
  ├─────────────────┼──────────┼──────────┤
  │ 恢复分数        │ {rec:.0f}%      │ {'✅ 良好' if rec >= 60 else '⚠️ 注意'} │
  │ HRV             │ {hrv:.0f} ms   │ {'✅ 正常' if hrv >= 40 else '⚠️ 偏低'} │
  │ 静息心率        │ {latest_rec.get('rhr', 0):.0f} bpm   │ ✅       │
  │ 血氧饱和度      │ {latest_rec.get('spo2', 0):.1f}%   │ {'✅ 正常' if latest_rec.get('spo2', 0) >= 95 else '⚠️ 低'} │
  │ 皮肤温度        │ {latest_rec.get('skin_temp', 0):.1f}°C   │ {'✅ 正常' if 33 <= latest_rec.get('skin_temp', 35) <= 36 else '⚠️ 异常'} │
  └─────────────────┴──────────┴──────────┘

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

😴 【睡眠分析】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  睡眠时长: {sleep_hours:.1f} 小时
  睡眠效率: {sleep_eff:.1f}%
  呼吸率: {rr:.1f} 次/分钟 ({'正常' if 12 <= rr <= 20 else '偏高/偏低'})

  睡眠结构 (近7天平均):
  ├─ 浅睡: {light_pct:.1f}% ({light/7:.1f}h)
  ├─ 深睡: {deep_pct:.1f}% ({deep/7:.1f}h)
  └─ REM: {rem_pct:.1f}% ({rem/7:.1f}h)

  睡眠债务: {debt:.1f} 小时 {'⚠️ 需补觉' if debt > 2 else '✅ 正常'}

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

💪 【训练分析】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  今日strain: {latest_cycle.get('strain', 0):.1f}
  消耗能量: {latest_cycle.get('kilojoule', 0):.0f} kJ
  平均心率: {latest_cycle.get('average_heart_rate', 0):.0f} bpm
  最高心率: {latest_cycle.get('max_heart_rate', 0):.0f} bpm

  心率区间分布 (近7天):
  ├─ Zone 0-1 恢复/轻松: {z01_pct:.1f}%
  ├─ Zone 2-3 有氧/混氧: {z23_pct:.1f}%
  └─ Zone 4-5 无氧/极限: {z45_pct:.1f}%

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

📊 【健康趋势】(7天平均)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  恢复评分: {rec_avg:.0f}% {'📈 上升' if rec_avg >= 60 else '📉 下降'}
  HRV: {hrv_avg:.0f}ms ({'✅ 正常' if hrv_avg >= 40 else '⚠️ 偏低'})
  静息心率: {rhr_avg:.0f}bpm ({'✅ 良好' if rhr_avg <= 60 else '⚠️ 偏高'})
  训练天数: {metrics.get('workout_count', 0)}天

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

💡 【健康洞察】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")

# 洞察
insights = []
if rec < 40:
    insights.append("🔴 恢复评分明显偏低，身体疲劳累积严重，建议增加休息")
elif rec < 60:
    insights.append("🟡 恢复评分一般，注意训练强度，适当降低")
else:
    insights.append("🟢 恢复评分良好，身体状态不错")

if hrv < 25:
    insights.append("🔴 HRV严重偏低，神经系统疲劳过度，需要完全休息")
elif hrv < 40:
    insights.append("🟡 HRV偏低，可能存在压力或恢复不足")

if debt > 5:
    insights.append(f"🔴 睡眠债务积累 {debt:.1f}小时，需要增加睡眠时间")
elif debt > 2:
    insights.append(f"🟡 存在 {debt:.1f}小时睡眠债务，建议适当补觉")

if latest_rec.get('rhr', 0) > 65:
    insights.append("🔴 静息心率偏高，可能存在过度训练")

for i in insights:
    print(f"  {i}")

print(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🎯 【今日建议】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")

# 建议
if rec >= 70:
    print("  ✅ 身体状态优秀，可以进行高强度训练")
elif rec >= 50:
    print("  🟡 身体状态一般，建议进行中低强度训练")
else:
    print("  🔴 恢复不足，建议完全休息或仅进行轻度活动")

if debt > 5:
    print(f"  🔴 睡眠债务较重，优先保证充足睡眠")
elif debt > 2:
    print(f"  🟡 有一定睡眠债务，注意今晚睡眠时间")

print(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📌 【数据说明】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  恢复评分: 基于HRV、静息心率、睡眠质量综合计算
  身体电量: 恢复60% + 睡眠40%的加权值
  HRV: 心率变异性，越高表示恢复越好
  睡眠债务: 近14天睡眠缺口累积

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")

PYEOF

echo "✅ 早间报告生成完成 $(date '+%H:%M:%S')"


path: lib/data_cleaner.py
content: """
Data Cleaner - 数据清洗模块
整合现有数据格式,提供标准化接口
"""

import os
import json
from datetime import datetime, timedelta
from lib.tz import cutoff_str
from typing import Dict, List

# 路径配置 - 使用环境变量或相对路径
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # lib/ -> whoop-guru/
WORKSPACE_DIR = os.environ.get("OPENCLAW_WORKSPACE", os.path.dirname(os.path.dirname(SKILL_DIR)))  # whoop-guru/ -> skill/ -> workspace
PROCESSED_DIR = os.environ.get("WHOOP_DATA_DIR", os.path.join(WORKSPACE_DIR, "data", "processed"))


class WhoopDataProvider:
    """WHOOP数据提供者 - 从现有数据系统获取"""

    def __init__(self):
        self.latest_file = os.path.join(PROCESSED_DIR, "latest.json")

    def get_latest_data(self) -> Dict:
        """获取最新数据"""
        if not os.path.exists(self.latest_file):
            return {}
        try:
            with open(self.latest_file, 'r') as f:
                return json.load(f)
        except Exception:
            return {}

    def get_metrics(self) -> Dict:
        """获取聚合指标"""
        data = self.get_latest_data()
        return data.get("metrics", {})

    def get_processed_recovery(self, days: int = 7) -> List[Dict]:
        """获取处理后的恢复记录"""
        data = self.get_latest_data()
        processed = data.get("processed", {})
        recovery = processed.get("recovery", [])

        cutoff = cutoff_str(days)

        return [r for r in recovery if r.get("date", "") >= cutoff]

    def get_processed_sleep(self, days: int = 7) -> List[Dict]:
        """获取处理后的睡眠记录"""
        data = self.get_latest_data()
        processed = data.get("processed", {})
        sleep = processed.get("sleep", [])

        cutoff = cutoff_str(days)

        return [s for s in sleep if s.get("date", "") >= cutoff]


class DataAggregator:
    """数据聚合器"""

    def __init__(self):
        self.provider = WhoopDataProvider()

    def get_coach_summary(self, days: int = 7) -> Dict:
        """获取教练汇总数据"""
        metrics = self.provider.get_metrics()
        recovery = self.provider.get_processed_recovery(days)
        sleep = self.provider.get_processed_sleep(days)

        # 从metrics获取汇总数据
        return {
            "days": days,
            "avg_recovery": metrics.get("avg_recovery", 0),
            "avg_hrv": metrics.get("avg_hrv", 0),
            "avg_rhr": metrics.get("avg_rhr", 0),
            "avg_sleep_hours": metrics.get("avg_sleep_hours", 0),
            "total_strain": metrics.get("total_strain", 0),
            "training_days": metrics.get("workout_count", 0),
            "sleep_debt": metrics.get("sleep_debt_estimate", 0),
            "recovery_count": len(recovery),
            "sleep_count": len(sleep),
            "recovery_trend": self._calc_trend(recovery),
        }

    def _calc_trend(self, recovery: List[Dict]) -> str:
        """计算恢复趋势"""
        if len(recovery) < 3:
            return "stable"

        values = [r.get("recovery_score", 50) for r in recovery]
        recent = sum(values[:3]) / 3
        older = sum(values[3:6]) / 3 if len(values) > 5 else recent

        diff = recent - older
        if diff > 5:
            return "up"
        elif diff < -5:
            return "down"
        return "stable"

    def get_today_status(self) -> Dict:
        """获取今日状态"""
        data = self.provider.get_latest_data()
        metrics = data.get("metrics", {})

        # 今日数据从最新记录获取
        processed = data.get("processed", {})
        recovery = processed.get("recovery", [])
        sleep = processed.get("sleep", [])

        today_recovery = recovery[0] if recovery else {}
        today_sleep = sleep[0] if sleep else {}

        return {
            "date": today_recovery.get("date", ""),
            "recovery": today_recovery.get("recovery_score", 0),
            "hrv": today_recovery.get("hrv", 0),
            "rhr": today_recovery.get("rhr", 0),
            "sleep_hours": today_sleep.get("total_in_bed_hours", 0),
        }


def get_whoop_data(days: int = 7) -> Dict:
    """获取WHOOP数据"""
    aggregator = DataAggregator()
    return aggregator.get_coach_summary(days)


def get_today_data() -> Dict:
    """获取今日数据"""
    aggregator = DataAggregator()
    return aggregator.get_today_status()


if __name__ == "__main__":
    print("Testing WhoopDataProvider...")
    provider = WhoopDataProvider()
    metrics = provider.get_metrics()
    print(f"Metrics: {json.dumps(metrics, indent=2)}")

    print("\nTesting DataAggregator...")
    aggregator = DataAggregator()
    summary = aggregator.get_coach_summary(7)
    print(f"Coach summary: {json.dumps(summary, indent=2)}")


path: lib/data_processor.py
content: #!/usr/bin/env python3
"""
Whoop Data Processor - Self-contained version
"""
import json
import os
from datetime import datetime

# Get skill directory
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)
DATA_DIR = os.path.join(SKILL_DIR, 'data')
OUTPUT_DIR = os.path.join(SKILL_DIR, 'data', 'processed')

def process_cycles(data):
    records = data.get('records', [])
    processed = []
    
    for r in records:
        score = r.get('score', {})
        start = r.get('start', '')
        
        # Convert UTC to date
        if start:
            date = start[:10]
        else:
            date = 'Unknown'
        
        processed.append({
            'date': date,
            'strain': score.get('strain', 0),
            'kilojoules': score.get('kilojoule', 0),
            'avg_hr': score.get('average_heart_rate', 0),
            'max_hr': score.get('max_heart_rate', 0),
            'distance': score.get('distance_meter', 0) / 1000 if score.get('distance_meter') else 0
        })
    
    return processed

def process_sleep(data):
    records = data.get('records', [])
    processed = []
    
    for r in records:
        score = r.get('score', {})
        stage = score.get('stage_summary', {})
        start = r.get('start', '')
        
        if start:
            date = start[:10]
        else:
            date = 'Unknown'
        
        # Check if nap
        nap = r.get('nap', False)
        
        in_bed_ms = stage.get('total_in_bed_time_milli', 0)
        in_bed_hours = in_bed_ms / 3600000 if in_bed_ms else 0
        
        processed.append({
            'date': date,
            'nap': nap,
            'total_in_bed_hours': round(in_bed_hours, 1),
            'sleep_performance': score.get('sleep_performance_percentage', 0),
            'sleep_efficiency': score.get('sleep_efficiency_percentage', 0),
            'respiratory_rate': score.get('respiratory_rate', 0),
            'light_sleep_hours': round(stage.get('total_light_sleep_time_milli', 0) / 3600000, 1),
            'deep_sleep_hours': round(stage.get('total_slow_wave_sleep_time_milli', 0) / 3600000, 1),
            'rem_sleep_hours': round(stage.get('total_rem_sleep_time_milli', 0) / 3600000, 1),
            'disturbances': stage.get('disturbance_count', 0)
        })
    
    return processed

def process_recovery(data):
    records = data.get('records', [])
    processed = []
    
    for r in records:
        score = r.get('score', {})
        sleep_id = r.get('sleep_id', '')
        
        # Get date from sleep record
        date = 'Unknown'
        if sleep_id:
            for s in data.get('sleep_records', []):
                if s.get('id') == sleep_id:
                    start = s.get('start', '')
                    if start:
                        date = start[:10]
                    break
        
        processed.append({
            'date': date,
            'recovery_score': score.get('recovery_score', 0),
            'hrv': round(score.get('hrv_rmssd_milli', 0), 1),
            'rhr': score.get('resting_heart_rate', 0),
            'spo2': score.get('spo2_percentage', 0),
            'skin_temp': round(score.get('skin_temp_celsius', 0), 1)
        })
    
    return processed

def main():
    # Ensure output directory
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    
    # Load raw data
    cycles_file = os.path.join(DATA_DIR, 'cycles_raw.json')
    sleep_file = os.path.join(DATA_DIR, 'sleep_raw.json')
    recovery_file = os.path.join(DATA_DIR, 'recovery_raw.json')
    
    try:
        with open(cycles_file) as f:
            cycles_data = json.load(f)
        cycles = process_cycles(cycles_data)
    except Exception as e:
        print(f"Error processing cycles: {e}")
        cycles = []
    
    try:
        with open(sleep_file) as f:
            sleep_data = json.load(f)
        sleep = process_sleep(sleep_data)
    except Exception as e:
        print(f"Error processing sleep: {e}")
        sleep = []
    
    try:
        with open(recovery_file) as f:
            recovery_data = json.load(f)
        recovery = process_recovery(recovery_data)
    except Exception as e:
        print(f"Error processing recovery: {e}")
        recovery = []
    
    # Save processed data
    output = {
        'processed': {
            'cycles': cycles,
            'sleep': sleep,
            'recovery': recovery
        },
        'generated_at': datetime.now().isoformat()
    }
    
    output_file = os.path.join(OUTPUT_DIR, 'latest.json')
    with open(output_file, 'w') as f:
        json.dump(output, f, indent=2)
    
    print(f"✅ Data processed and saved!")
    print(f"📊 Recovery records: {len(recovery)}")
    print(f"📊 Sleep records: {len(sleep)}")
    print(f"📊 Cycle records: {len(cycles)}")

if __name__ == '__main__':
    main()


path: lib/detailed-report.sh
content: #!/bin/bash
# Detailed Report - 22:00 详细健康日报
# 包含完整洞察、建议和分析

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SKILL_DIR")"
cd "$SKILL_DIR"

python3 << 'PYEOF'
import sys
import os
sys.path.insert(0, 'lib')

from datetime import datetime
from lib.data_cleaner import WhoopDataProvider
from lib.ml import get_recovery_prediction, get_hrv_anomaly
from lib.reports.weekly import WeeklyReporter

p = WhoopDataProvider()
data = p.get_latest_data()
metrics = p.get_metrics()

processed = data.get('processed', {})
recovery_list = processed.get('recovery', [])
sleep_list = processed.get('sleep', [])
cycles_list = processed.get('cycles', [])
workouts_list = processed.get('workouts', [])

latest_rec = recovery_list[0] if recovery_list else {}
latest_sleep = sleep_list[0] if sleep_list else {}
latest_cycle = cycles_list[0] if cycles_list else {}

date = datetime.now().strftime("%Y年%m月%d日 %H:%M")

# ========== 身体电量 ==========
rec = latest_rec.get('recovery_score', 50)
sp = latest_sleep.get('sleep_performance', 70) if latest_sleep else 70
battery = rec * 0.6 + sp * 0.4
if battery >= 80:
    battery_level = "满格 🟢"
elif battery >= 60:
    battery_level = "良好 🟡"
elif battery >= 40:
    battery_level = "一般 🟠"
else:
    battery_level = "需充电 🔴"

# ========== 睡眠债务 ==========
sleep_hours_14 = [s.get('total_in_bed_hours', 0) for s in sleep_list[:14]]
debt = 14 * 8 - sum(sleep_hours_14)

# ========== 睡眠结构 ==========
light = sum(s.get('light_sleep_hours', 0) for s in sleep_list[:7])
deep = sum(s.get('deep_sleep_hours', 0) for s in sleep_list[:7])
rem = sum(s.get('rem_sleep_hours', 0) for s in sleep_list[:7])
total_sleep = light + deep + rem
if total_sleep > 0:
    light_pct = light / total_sleep * 100
    deep_pct = deep / total_sleep * 100
    rem_pct = rem / total_sleep * 100
else:
    light_pct = deep_pct = rem_pct = 0

# ========== 心率区间 ==========
zones = {"z0": 0, "z1": 0, "z2": 0, "z3": 0, "z4": 0, "z5": 0}
for w in workouts_list[:7]:
    for z in range(6):
        key = f"zone{z}_min"
        zones[f"z{z}"] += w.get(key, 0)
total_zone = sum(zones.values())
if total_zone > 0:
    z01_pct = (zones["z0"] + zones["z1"]) / total_zone * 100
    z23_pct = (zones["z2"] + zones["z3"]) / total_zone * 100
    z45_pct = (zones["z4"] + zones["z5"]) / total_zone * 100
else:
    z01_pct = z23_pct = z45_pct = 0

# ========== ML预测 ==========
pred = get_recovery_prediction()
anomaly = get_hrv_anomaly()

# ========== 周报 ==========
weekly = WeeklyReporter().generate_weekly_report()
weekly_stats = weekly.get('stats', {})

print(f"""
╔══════════════════════════════════════════════════════════════╗
║          🌙 WHOOP GURU 详细健康日报                      ║
║                    {date}                         ║
╚══════════════════════════════════════════════════════════════╝

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

🔋 【身体电量】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ⚡ {battery:.0f}% {battery_level}
  
  今日恢复: {rec:.0f}% × 0.6 = {rec * 0.6:.1f}
  今日睡眠: {sp:.0f}% × 0.4 = {sp * 0.4:.1f}

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

🫀 【今日数据】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ┌─────────────────┬──────────┬──────────────────┐
  │ 指标            │ 数值     │ 参考范围         │
  ├─────────────────┼──────────┼──────────────────┤
  │ 恢复分数        │ {rec:.0f}%      │ 60%+ 良好       │
  │ HRV             │ {latest_rec.get('hrv', 0):.0f} ms   │ 40ms+ 正常      │
  │ 静息心率        │ {latest_rec.get('rhr', 0):.0f} bpm   │ 50-60bpm 良好   │
  │ 血氧饱和度      │ {latest_rec.get('spo2', 0):.1f}%   │ 95%+ 正常       │
  │ 皮肤温度        │ {latest_rec.get('skin_temp', 0):.1f}°C   │ 33-36°C 正常    │
  └─────────────────┴──────────┴──────────────────┘

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

😴 【睡眠分析】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  今日睡眠: {latest_sleep.get('total_in_bed_hours', 0):.1f} 小时
  睡眠效率: {latest_sleep.get('sleep_efficiency', 0):.1f}%
  呼吸率: {latest_sleep.get('respiratory_rate', 0):.1f} 次/分钟

  睡眠结构 (近7天平均):
  ├─ 浅睡: {light_pct:.1f}% ({light/7:.1f}h)
  ├─ 深睡: {deep_pct:.1f}% ({deep/7:.1f}h) {'✅ 充足' if deep/7 >= 1 else '⚠️ 偏少'}
  └─ REM: {rem_pct:.1f}% ({rem/7:.1f}h)

  睡眠债务: {debt:.1f} 小时 {'⚠️ 需补觉' if debt > 2 else '✅ 正常'}

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

💪 【训练分析】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  今日strain: {latest_cycle.get('strain', 0):.1f} {'✅ 完成目标' if latest_cycle.get('strain', 0) >= 10 else '⚠️ 未达标'}
  消耗能量: {latest_cycle.get('kilojoule', 0):.0f} kJ
  平均心率: {latest_cycle.get('average_heart_rate', 0):.0f} bpm
  最高心率: {latest_cycle.get('max_heart_rate', 0):.0f} bpm

  心率区间 (近7天):
  ├─ Zone 0-1 恢复/轻松: {z01_pct:.1f}% {'✅ 有氧基础' if z01_pct > 50 else '⚠️ 偏重'}
  ├─ Zone 2-3 有氧/混氧: {z23_pct:.1f}% {'✅ 训练主力' if z23_pct > 30 else '⚠️ 偏少'}
  └─ Zone 4-5 无氧/极限: {z45_pct:.1f}% {'⚠️ 比例偏高' if z45_pct > 10 else '✅ 正常'}

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

🔮 【ML恢复预测】(未来7天)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")

for i, p_val in enumerate(pred['predictions'][:7], 1):
    bar = "█" * int(p_val / 5)
    status = "🟢" if p_val >= 60 else "🟡" if p_val >= 40 else "🔴"
    print(f"  第{i}天: {p_val:5.1f}% {bar} {status}")

print(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

⚠️ 【HRV异常检测】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  状态: {'⚠️ 异常' if anomaly.get('is_anomaly') else '✅ 正常'}
""")

if anomaly.get('is_anomaly'):
    print(f"  变化: {anomaly.get('change_pct', 0):+.1f}%")
    print(f"  原因: {anomaly.get('reason', 'N/A')}")
    print(f"  建议: {anomaly.get('suggestion', '注意休息')}")

print(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📈 【本周总结】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  训练天数: {weekly_stats.get('training_days', 0)}天
  平均恢复: {weekly_stats.get('avg_recovery', 0):.0f}%
  平均HRV: {weekly_stats.get('avg_hrv', 0):.0f}ms
  平均静息心率: {weekly_stats.get('avg_rhr', 0):.0f}bpm
  总strain: {weekly_stats.get('total_strain', 0):.1f}
  睡眠债务: {weekly_stats.get('sleep_debt', 0):.1f}小时

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

🎯 【综合洞察】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")

insights = []
if rec < 40:
    insights.append(("🔴", "恢复评分严重偏低，需要完全休息"))
elif rec < 60:
    insights.append(("🟡", "恢复评分一般，建议降低强度"))

if latest_rec.get('hrv', 0) < 25:
    insights.append(("🔴", "HRV过低，神经系统疲劳"))

if debt > 5:
    insights.append(("🔴", f"睡眠债务{debt:.1f}小时，需要补觉"))

if z45_pct > 15:
    insights.append(("🟡", "无氧训练比例偏高，注意恢复"))

if not insights:
    insights.append(("🟢", "各项指标正常，身体状态良好"))

for emoji, text in insights:
    print(f"  {emoji} {text}")

print(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

💡 【明日建议】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")

if rec >= 70:
    print("  ✅ 明日可以正常训练，甚至挑战高强度")
elif rec >= 50:
    print("  🟡 建议中等强度训练，注意监控心率")
else:
    print("  🔴 强烈建议休息或仅进行轻度活动")

if debt > 3:
    print(f"  🔴 优先补觉，保证7-8小时睡眠")

print(f"""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📊 【长期趋势】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  近30天数据:
  ├─ 平均恢复: {metrics.get('avg_recovery', 0):.0f}%
  ├─ 平均HRV: {metrics.get('avg_hrv', 0):.0f}ms
  ├─ 静息心率: {metrics.get('avg_rhr', 0):.0f}bpm
  ├─ 训练天数: {metrics.get('workout_count', 0)}天
  └─ 平均strain: {metrics.get('avg_strain', 0):.1f}

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

_数据来源: WHOOP | 健康糕_
_报告生成: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}_
""")

PYEOF

echo "✅ 详细日报生成完成 $(date '+%H:%M:%S')"


path: lib/dynamic_planner.py
content: """
Dynamic Planner - 动态训练计划调整
根据恢复状态动态调整训练计划
"""

import os
import json
from datetime import datetime, timedelta
from lib.tz import today_str
from typing import Dict, List, Optional

# 数据路径 - 使用环境变量
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # lib/ -> whoop-guru/
WORKSPACE_DIR = os.environ.get("OPENCLAW_WORKSPACE", os.path.dirname(os.path.dirname(SKILL_DIR)))  # whoop-guru/ -> skill/ -> workspace
PROCESSED_DIR = os.environ.get("WHOOP_DATA_DIR", os.path.join(WORKSPACE_DIR, "data", "processed"))
DATA_FILE = os.path.join(PROCESSED_DIR, "latest.json")


class DynamicPlanner:
    """
    动态训练计划调整器
    
    根据以下因素动态调整：
    - 当前恢复状态
    - 近期训练负荷
    - 睡眠质量
    - HRV趋势
    """
    
    # 强度阈值
    THRESHOLDS = {
        "high_recovery": 75,
        "medium_recovery": 55,
        "low_recovery": 35,
    }
    
    # 高强度阈值
    STRAIN_THRESHOLD = 12
    
    def __init__(self):
        self.deload_weeks = 4  # 减载周期
    
    def load_latest_data(self) -> Dict:
        """加载最新数据"""
        try:
            with open(DATA_FILE, 'r') as f:
                return json.load(f)
        except Exception:
            return {}
    
    def get_current_status(self) -> Dict:
        """获取当前状态"""
        data = self.load_latest_data()
        
        recovery = data.get("processed", {}).get("recovery", [])
        sleep = data.get("processed", {}).get("sleep", [])
        daily_summary = data.get("daily_summary", {})
        
        current_recovery = recovery[0].get("recovery_score", 50) if recovery else 50
        current_hrv = recovery[0].get("hrv", 40) if recovery else 40
        
        # 获取今日strain从cycles
        cycles = daily_summary.get("cycles", [])
        today_strain = cycles[0].get("strain", 0) if cycles else 0
        
        # 计算本周训练天数从workouts
        workouts = daily_summary.get("workouts", [])
        training_days = len(workouts)
        today_has_training = len([w for w in workouts if w.get("date", "").startswith(today_str())]) > 0 if workouts else False
        
        # 计算睡眠债务
        sleep_debt = 0
        if len(sleep) >= 7:
            total_sleep = sum(s.get("total_in_bed_hours", 0) for s in sleep[-7:])
            sleep_debt = max(0, 7 * 8 - total_sleep)
        
        return {
            "recovery": current_recovery,
            "hrv": current_hrv,
            "today_strain": today_strain,
            "today_has_training": today_has_training,
            "training_days_this_week": training_days,
            "sleep_debt": sleep_debt,
        }
    
    def get_recommended_intensity(self) -> Dict:
        """
        获取推荐训练强度
        
        Returns:
            {
                "intensity": "high/medium/low/rest",
                "description": str,
                "strain_target": str,
                "reason": str
            }
        """
        status = self.get_current_status()
        
        recovery = status["recovery"]
        today_strain = status["today_strain"]
        training_days = status["training_days_this_week"]
        sleep_debt = status["sleep_debt"]
        
        # 特殊情况：今日已训练
        if status["today_has_training"] and today_strain >= 10:
            return {
                "intensity": "rest",
                "description": "今日已完成训练",
                "strain_target": "0",
                "reason": f"今日strain已达{today_strain}，身体得到刺激"
            }
        
        # 根据恢复状态判断
        if recovery >= self.THRESHOLDS["high_recovery"]:
            if training_days < 5:
                return {
                    "intensity": "high",
                    "description": "高强度训练",
                    "strain_target": "12-15",
                    "reason": f"恢复{recovery}%良好，可进行高强度训练"
                }
            else:
                return {
                    "intensity": "medium",
                    "description": "中等强度训练",
                    "strain_target": "8-12",
                    "reason": "本周训练较频繁，建议中等强度"
                }
        
        elif recovery >= self.THRESHOLDS["medium_recovery"]:
            return {
                "intensity": "medium",
                "description": "中等强度训练",
                "strain_target": "8-12",
                "reason": f"恢复{recovery}%一般，适合中等强度"
            }
        
        elif recovery >= self.THRESHOLDS["low_recovery"]:
            return {
                "intensity": "low",
                "description": "轻度活动",
                "strain_target": "5-8",
                "reason": f"恢复{recovery}%偏低，建议轻度活动"
            }
        
        else:
            return {
                "intensity": "rest",
                "description": "完全休息",
                "strain_target": "0-3",
                "reason": f"恢复{recovery}%较差，建议休息"
            }
    
    def should_deload(self) -> Dict:
        """
        判断是否需要减载周
        
        Returns:
            {
                "should_deload": bool,
                "reason": str,
                "deload_type": "active/rest/passive"
            }
        """
        data = self.load_latest_data()
        recovery = data.get("processed", {}).get("recovery", [])
        
        if len(recovery) < 14:
            return {
                "should_deload": False,
                "reason": "数据不足"
            }
        
        # 最近7天 vs 前7天
        recent_7 = [r.get("recovery_score", 50) for r in recovery[:7]]
        older_7 = [r.get("recovery_score", 50) for r in recovery[7:14]]
        
        recent_avg = sum(recent_7) / 7
        older_avg = sum(older_7) / 7
        
        # 判断减载
        if recent_avg < older_avg - 10:
            return {
                "should_deload": True,
                "reason": f"恢复评分下降 ({older_avg:.0f}% → {recent_avg:.0f}%)",
                "deload_type": "active"
            }
        
        # 连续3天低恢复
        low_days = sum(1 for r in recent_7 if r < 40)
        if low_days >= 3:
            return {
                "should_deload": True,
                "reason": f"连续{low_days}天恢复低于40%",
                "deload_type": "passive"
            }
        
        return {
            "should_deload": False,
            "reason": "恢复状态良好，无需减载"
        }
    
    def get_adjusted_plan(self, base_plan: Dict) -> Dict:
        """
        调整基础训练计划
        
        Args:
            base_plan: 基础计划字典
        
        Returns:
            调整后的计划
        """
        recommendation = self.get_recommended_intensity()
        deload = self.should_deload()
        
        adjusted = base_plan.copy()
        adjusted["adjustments"] = []
        
        # 应用强度调整
        if recommendation["intensity"] == "rest":
            adjusted["plan_type"] = "休息日"
            adjusted["adjustments"].append("根据当前恢复状态，建议休息")
        elif recommendation["intensity"] == "low":
            adjusted["plan_type"] = "轻度活动"
            adjusted["strain_target"] = "5-8"
            adjusted["adjustments"].append("恢复偏低，降低强度")
        elif recommendation["intensity"] == "medium":
            adjusted["plan_type"] = "中等强度"
            adjusted["strain_target"] = "8-12"
            adjusted["adjustments"].append("中等强度训练")
        
        # 应用减载
        if deload["should_deload"]:
            adjusted["deload"] = True
            adjusted["deload_type"] = deload["deload_type"]
            adjusted["adjustments"].append(f"减载周: {deload['reason']}")
        
        return adjusted


def get_dynamic_recommendation() -> Dict:
    """获取动态训练建议"""
    planner = DynamicPlanner()
    return planner.get_recommended_intensity()


def check_deload() -> Dict:
    """检查是否需要减载"""
    planner = DynamicPlanner()
    return planner.should_deload()


if __name__ == "__main__":
    print("=== Dynamic Planner ===")
    planner = DynamicPlanner()
    
    print("\n--- Current Status ---")
    status = planner.get_current_status()
    print(f"Recovery: {status['recovery']}%")
    print(f"HRV: {status['hrv']}ms")
    print(f"Today Strain: {status['today_strain']}")
    print(f"Training Days: {status['training_days_this_week']}")
    
    print("\n--- Recommendation ---")
    rec = planner.get_recommended_intensity()
    print(f"Intensity: {rec['intensity']}")
    print(f"Description: {rec['description']}")
    print(f"Reason: {rec['reason']}")
    
    print("\n--- Deload Check ---")
    deload = planner.should_deload()
    print(f"Should Deload: {deload['should_deload']}")
    print(f"Reason: {deload['reason']}")


path: lib/enhanced_report.py
content: #!/usr/bin/env python3
import os
"""
Enhanced Health Report - Full features
"""
import json
from datetime import datetime

DATA_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'processed', 'latest.json')
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'reports')

def load_data():
    with open(DATA_FILE) as f:
        return json.load(f)

def calculate_health_score(recovery, sleep):
    """Calculate overall health score"""
    if not recovery or not sleep:
        return 0, "N/A"
    
    rec_avg = sum(r.get('recovery_score', 0) for r in recovery[:7]) / min(7, len(recovery))
    sleep_avg = sum(s.get('sleep_performance', 0) for s in sleep[:7]) / min(7, len(sleep))
    hrv_avg = sum(r.get('hrv', 0) for r in recovery[:7]) / min(7, len(recovery))
    rhr_avg = sum(r.get('rhr', 0) for r in recovery[:7]) / min(7, len(recovery))
    
    hrv_score = min(100, hrv_avg * 2)
    rhr_score = max(0, 100 - (rhr_avg - 50) * 5)
    
    total = rec_avg * 0.4 + sleep_avg * 0.3 + hrv_score * 0.2 + rhr_score * 0.1
    
    if total >= 90: grade = "A+"
    elif total >= 85: grade = "A"
    elif total >= 80: grade = "A-"
    elif total >= 70: grade = "B"
    elif total >= 60: grade = "C"
    else: grade = "D"
    
    return round(total, 1), grade

def get_heart_zone_emoji(zone):
    """Get emoji for heart rate zone"""
    zones = {
        "zone0": "🟢 Zone 0 (恢复)",
        "zone1": "🟢 Zone 1 (轻松)",
        "zone2": "🟡 Zone 2 (有氧)",
        "zone3": "🟠 Zone 3 (混氧)",
        "zone4": "🔴 Zone 4 (无氧)",
        "zone5": "🔴 Zone 5 (极限)"
    }
    return zones.get(zone, zone)

def generate_morning_report():
    """Generate comprehensive morning report"""
    data = load_data()
    processed = data.get('processed', {})
    
    recovery = processed.get('recovery', [])
    sleep = processed.get('sleep', [])
    cycles = processed.get('cycles', [])
    workouts = processed.get('workouts', [])
    
    # Latest data
    latest_rec = recovery[0] if recovery else {}
    latest_sleep = sleep[0] if sleep else {}
    latest_cycle = cycles[0] if cycles else {}
    
    # Health Score
    score, grade = calculate_health_score(recovery, sleep)
    
    # Sleep debt
    sleep_hours = [s.get('total_in_bed_hours', 0) for s in sleep[:14]]
    debt = 14 * 8 - sum(sleep_hours)
    
    # Heart rate zones
    zones = {"zone0": 0, "zone1": 0, "zone2": 0, "zone3": 0, "zone4": 0, "zone5": 0}
    for w in workouts[:7]:
        for z in range(6):
            zones[f"zone{z}"] += w.get(f"zone{z}_min", 0)
    total_zone = sum(zones.values())
    zone_pcts = {k: round(v/total_zone*100, 1) if total_zone > 0 else 0 for k, v in zones.items()}
    
    # Sleep stages
    light = sum(s.get('light_sleep_hours', 0) for s in sleep[:7])
    deep = sum(s.get('deep_sleep_hours', 0) for s in sleep[:7])
    rem = sum(s.get('rem_sleep_hours', 0) for s in sleep[:7])
    total_sleep_hours = light + deep + rem
    if total_sleep_hours > 0:
        light_pct = round(light/total_sleep_hours*100, 1)
        deep_pct = round(deep/total_sleep_hours*100, 1)
        rem_pct = round(rem/total_sleep_hours*100, 1)
    
    # Body battery
    rec = latest_rec.get('recovery_score', 50)
    sp = latest_sleep.get('sleep_performance', 70)
    battery = round(rec * 0.6 + sp * 0.4, 1)
    if battery >= 80: battery_level = "满格 🟢"
    elif battery >= 60: battery_level = "良好 🟡"
    elif battery >= 40: battery_level = "一般 🟠"
    else: battery_level = "需充电 🔴"
    
    # Respiratory rate
    rr = latest_sleep.get('respiratory_rate', 0)
    
    # Generate report
    date = datetime.now().strftime("%Y-%m-%d")
    report = f"""📊 **健康日报** - {date}

---

### 🔋 身体电量

**{battery_level}** ({battery}%)

---

### 🫀 今日状态

| 指标 | 数值 | 状态 |
|------|------|------|
| 恢复分数 | {latest_rec.get('recovery_score', 'N/A')}% | {'✅' if latest_rec.get('recovery_score', 0) >= 60 else '⚠️'} |
| HRV | {latest_rec.get('hrv', 'N/A')} ms | {'✅' if latest_rec.get('hrv', 0) >= 40 else '⚠️'} |
| 静息心率 | {latest_rec.get('rhr', 'N/A')} bpm | ✅ |
| 血氧 | {latest_rec.get('spo2', 'N/A')}% | ✅ |
| 皮肤温度 | {latest_rec.get('skin_temp', 'N/A')}°C | ✅ |

---

### 😴 睡眠分析

| 指标 | 数值 |
|------|------|
| 睡眠时长 | {latest_sleep.get('total_in_bed_hours', 'N/A')} h |
| 睡眠效率 | {latest_sleep.get('sleep_efficiency', 'N/A')}% |
| 呼吸率 | {rr} ({"正常" if 12 <= rr <= 20 else "异常"}) |

**睡眠结构:**
- 浅睡: {light_pct if total_sleep_hours > 0 else 0}% ({round(light/7, 1) if total_sleep_hours > 0 else 0}h)
- 深睡: {deep_pct if total_sleep_hours > 0 else 0}% ({round(deep/7, 1) if total_sleep_hours > 0 else 0}h)
- REM: {rem_pct if total_sleep_hours > 0 else 0}% ({round(rem/7, 1) if total_sleep_hours > 0 else 0}h)

**睡眠债务:** {round(debt, 1)} 小时

---

### 💪 训练分析

**今日 Strain:** {latest_cycle.get('strain', 'N/A')}
**消耗:** {latest_cycle.get('kilojoules', 'N/A')} kJ

**心率区间 (近7次训练):**
| 区间 | 比例 |
|------|------|
| Zone 0-1 (恢复/轻松) | {zone_pcts.get('zone0', 0) + zone_pcts.get('zone1', 0)}% |
| Zone 2-3 (有氧/混氧) | {zone_pcts.get('zone2', 0) + zone_pcts.get('zone3', 0)}% |
| Zone 4-5 (无氧/极限) | {zone_pcts.get('zone4', 0) + zone_pcts.get('zone5', 0)}% |

---

### 📈 健康评分

| 维度 | 分数 |
|------|------|
| 恢复 | {round(sum(r.get('recovery_score', 0) for r in recovery[:7])/min(7, len(recovery)), 1) if recovery else 0} |
| 睡眠 | {round(sum(s.get('sleep_performance', 0) for s in sleep[:7])/min(7, len(sleep)), 1) if sleep else 0} |
| HRV | {round(min(100, sum(r.get('hrv', 0) for r in recovery[:7])/min(7, len(recovery)) * 2), 1) if recovery else 0} |
| 心率 | {round(max(0, 100 - (sum(r.get('rhr', 0) for r in recovery[:7])/min(7, len(recovery)) - 50) * 5), 1) if recovery else 0} |

**总分: {score}/100 ({grade})**

---

### 💡 建议

"""
    
    # Recommendations based on data
    if rec >= 70:
        report += "- ✅ 恢复良好，可以正常训练\n"
    elif rec >= 50:
        report += "- 🟡 恢复一般，建议低强度活动\n"
    else:
        report += "- 🔴 恢复偏低，建议休息\n"
    
    if debt > 10:
        report += f"- 🔴 睡眠债务较重 ({round(debt, 1)}h)，注意补觉\n"
    
    if zone_pcts.get('zone4', 0) + zone_pcts.get('zone5', 0) > 10:
        report += "- 🟡 无氧训练比例较高，注意恢复\n"
    
    report += "\n---\n_数据来源: WHOOP | 健康糕_"
    
    return report

def generate_detailed_report():
    """Generate detailed report"""
    # Similar but more detailed version
    return generate_morning_report()  # For now, same as morning

if __name__ == "__main__":
    import sys
    
    report_type = sys.argv[1] if len(sys.argv) > 1 else "morning"
    date = datetime.now().strftime("%Y-%m-%d")
    
    if report_type == "morning":
        content = generate_morning_report()
        filename = f"{OUTPUT_DIR}/report_morning_{date}.md"
    else:
        content = generate_detailed_report()
        filename = f"{OUTPUT_DIR}/report_detailed_{date}.md"
    
    with open(filename, 'w') as f:
        f.write(content)
    
    print(f"✅ Report saved: {filename}")


path: lib/enhanced_reports.py
content: """
LLM增强报告模块
生成真正由大模型驱动的个性化详细健康分析

使用方法:
from lib.enhanced_reports import EnhancedReports
report = EnhancedReports.morning_report()
"""

import os
import sys
from datetime import datetime
from lib.tz import now as bj_now, today_str
from typing import Dict, Optional, List

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


def _get_data(days: int = 7):
    """获取数据"""
    try:
        from lib.data_cleaner import get_whoop_data, get_today_data
        return get_whoop_data(days), get_today_data()
    except Exception:
        return {}, {}


def _get_llm_client():
    """获取LLM客户端"""
    try:
        from lib.llm import LLMClient
        client = LLMClient("enhanced_report")
        if client.config.[REDACTED_SECRET] client
    except:
        pass
    return None


def _get_health_score():
    try:
        from lib.health_score import calculate_health_score
        return calculate_health_score()
    except:
        return {"score": 0, "grade": "N/A", "breakdown": {}}


def _get_ml_prediction():
    try:
        from lib.ml_predictor import predict_next_day
        return predict_next_day()
    except:
        return {"prediction": 50, "confidence": "low", "reason": "数据不足"}


def _get_comprehensive():
    try:
        from lib.comprehensive_analysis import generate_comprehensive
        return generate_comprehensive()
    except:
        return {}


def _get_tracker(user_id: str = "dongyi"):
    try:
        from lib.tracker import ProgressTracker
        return ProgressTracker(user_id)
    except:
        return None


def _get_goals(user_id: str = "dongyi"):
    try:
        from lib.goals import GoalsManager
        return GoalsManager(user_id)
    except:
        return None


def _get_marathon_goal(user_id: str = "dongyi"):
    try:
        from lib.goals import GoalsManager
        gm = GoalsManager(user_id)
        goals = gm.get_marathon_goals()
        for g in goals:
            if g.status == "active":
                return g
    except:
        pass
    return None


def _emoji_score(value: float, thresholds: tuple = (70, 50)) -> tuple:
    """根据阈值返回 emoji 和状态文字"""
    if value >= thresholds[0]:
        return "🟢", "良好"
    elif value >= thresholds[1]:
        return "🟡", "一般"
    else:
        return "🔴", "需关注"


def _format_trend(trend: str) -> str:
    """格式化趋势"""
    return {"up": "📈 上升", "down": "📉 下降", "stable": "➡️ 稳定"}.get(trend, "➡️ 稳定")


class EnhancedReports:

    @staticmethod
    def morning_report() -> str:
        summary, today = _get_data(7)
        health_score = _get_health_score()
        ml_pred = _get_ml_prediction()
        comprehensive = _get_comprehensive()
        tracker = _get_tracker()
        goals_mgr = _get_goals()

        recovery = today.get('recovery', 0)
        hrv = today.get('hrv', 0)
        rhr = today.get('rhr', 0)
        sleep_hours = today.get('sleep_hours', 0)
        date = today.get('date', today_str())

        avg_recovery = summary.get('avg_recovery', 0)
        avg_hrv = summary.get('avg_hrv', 0)
        avg_rhr = summary.get('avg_rhr', 0)
        avg_sleep = summary.get('avg_sleep_hours', 0)
        sleep_debt = summary.get('sleep_debt', 0)
        training_days = summary.get('training_days', 0)
        recovery_trend = summary.get('recovery_trend', 'stable')
        total_strain = summary.get('total_strain', 0)

        hs_score = health_score.get('score', 0)
        hs_breakdown = health_score.get('breakdown', {})
        hs_grade = health_score.get('grade', 'N/A')

        ml_p = ml_pred.get('prediction', 50)
        ml_conf = ml_pred.get('confidence', 'low')
        ml_reason = ml_pred.get('reason', '')

        comp = comprehensive
        body_battery = comp.get('body_battery', {})
        hz = comp.get('heart_zones', {})

        rec_emoji, rec_status = _emoji_score(recovery)
        hs_emoji, hs_status = _emoji_score(hs_score)
        ml_emoji, ml_status = _emoji_score(ml_p)

        streak = tracker.get_streak("dongyi") if tracker else 0
        weekly = tracker.get_weekly_summary("dongyi") if tracker else {}
        active_goals = goals_mgr.get_active_goals() if goals_mgr else []
        marathon_goal = _get_marathon_goal()

        llm_client = _get_llm_client()
        llm_section = ""
        if llm_client:
            try:
                prompt = EnhancedReports._build_llm_prompt(
                    summary, today, health_score, ml_pred, comprehensive,
                    tracker, goals_mgr, "morning"
                )
                result = llm_client.generate(prompt)
                if result and "error" not in result:
                    content = result.get("content", "")
                    if content:
                        llm_section = f"\n🤖 **AI教练深度分析**\n\n{content}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
            except Exception:
                pass

        msg = f"☀️ **今日健康早报** `{date}`\n\n"
        msg += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"

        if llm_section:
            msg += llm_section

        msg += "📊 **核心状态**\n\n"
        msg += f"• 恢复评分：{rec_emoji} **{recovery:.0f}%**（{rec_status}）\n"
        msg += f"• HRV：{hrv:.1f}ms\n"
        msg += f"• 静息心率：{rhr:.0f}bpm\n"
        msg += f"• 睡眠时长：{sleep_hours:.1f}h\n"
        if sleep_debt > 0:
            msg += f"• 睡眠债务：⚠️ {sleep_debt:.1f}h\n"
        msg += f"• 今日strain：{today.get('strain', 0):.1f}\n\n"

        msg += "🏆 **健康评分**\n\n"
        msg += f"• 综合评分：{hs_emoji} **{hs_score:.0f}/100**（{hs_grade}级）\n"
        if hs_breakdown:
            for k, v in hs_breakdown.items():
                label = {"recovery": "恢复", "sleep": "睡眠", "hrv": "HRV", "rhr": "心率"}.get(k, k)
                e, s = _emoji_score(v)
                msg += f"  • {label}：{e} {v:.0f}（{s}）\n"
        msg += "\n"

        msg += "🔮 **ML恢复预测**\n\n"
        msg += f"• 明日预测：{ml_emoji} **{ml_p:.0f}%**（{ml_status}）\n"
        msg += f"• 置信度：{ml_conf}\n"
        if ml_reason:
            msg += f"• 分析：{ml_reason}\n"
        msg += "\n"

        msg += "📈 **7天趋势**\n\n"
        msg += f"• 平均恢复：{avg_recovery:.1f}% {_format_trend(recovery_trend)}\n"
        msg += f"• 平均HRV：{avg_hrv:.1f}ms\n"
        msg += f"• 平均RHR：{avg_rhr:.0f}bpm\n"
        msg += f"• 平均睡眠：{avg_sleep:.1f}h\n"
        msg += f"• 7日总strain：{total_strain:.1f}\n"
        msg += f"• 训练天数：{training_days}天\n\n"

        if body_battery:
            bb = body_battery.get('level', 0)
            bb_emoji, bb_status = _emoji_score(bb, (80, 50))
            msg += "🔋 **身体电量**\n\n"
            msg += f"• 电量：{bb_emoji} **{bb:.0f}%**（{bb_status}）\n"
            msg += f"• 建议：{body_battery.get('suggestion', 'N/A')}\n\n"

        if marathon_goal:
            days_left = marathon_goal.days_until_race
            msg += "🏅 **马拉松目标**\n\n"
            msg += f"• 比赛：{marathon_goal.goal_name}\n"
            msg += f"• 目标：{marathon_goal.goal_time}（{marathon_goal.target_pace}/km）\n"
            msg += f"• 剩余：{days_left}天（{marathon_goal.training_phase}）\n\n"

        if tracker:
            msg += "📝 **打卡统计**\n\n"
            msg += f"• 连续打卡：{streak}天 🔥\n"
            if weekly:
                msg += f"• 本周打卡：{weekly.get('total_checkins', 0)}次\n"
                cr = weekly.get('completion_rate', 0)
                if cr > 0:
                    msg += f"• 完成率：{cr:.0f}%\n"
            msg += "\n"

        if active_goals:
            msg += "🎯 **当前目标**\n\n"
            for goal in active_goals[:3]:
                pct = (goal.current / goal.target * 100) if goal.target > 0 else 0
                msg += f"• {goal.goal_type}：{goal.current}/{goal.target} {goal.unit}（{pct:.0f}%）\n"
            msg += "\n"

        msg += "💡 **今日建议**\n\n"
        if recovery >= 80:
            msg += "✅ 身体状态极佳！今天适合高强度训练。\n"
        elif recovery >= 60:
            msg += "🟡 状态良好，可以进行中等强度训练。\n"
        elif recovery >= 40:
            msg += "🟠 恢复一般，建议以轻度活动为主。\n"
        else:
            msg += "🔴 恢复不足，今天建议完全休息。\n"
        msg += "\n"

        msg += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
        msg += "📌 *训练后来打卡，记录训练内容和身体感受！*\n"
        msg += "⏰ *下次推送：18:00 晚间追踪*\n"

        return msg

    @staticmethod
    def evening_report() -> str:
        summary, today = _get_data(7)
        health_score = _get_health_score()
        ml_pred = _get_ml_prediction()
        comprehensive = _get_comprehensive()
        tracker = _get_tracker()
        goals_mgr = _get_goals()

        recovery = today.get('recovery', 0)
        strain = today.get('strain', 0)
        has_training = today.get('has_training', False)
        hrv = today.get('hrv', 0)
        rhr = today.get('rhr', 0)
        sleep_hours = today.get('sleep_hours', 0)
        date = today.get('date', today_str())

        avg_recovery = summary.get('avg_recovery', 0)
        sleep_debt = summary.get('sleep_debt', 0)
        training_days = summary.get('training_days', 0)
        recovery_trend = summary.get('recovery_trend', 'stable')

        hs_score = health_score.get('score', 0)
        ml_p = ml_pred.get('prediction', 50)
        ml_emoji, ml_status = _emoji_score(ml_p)

        comp = comprehensive
        hz = comp.get('heart_zones', {})
        body_battery = comp.get('body_battery', {})

        streak = tracker.get_streak("dongyi") if tracker else 0
        weekly = tracker.get_weekly_summary("dongyi") if tracker else {}
        marathon_goal = _get_marathon_goal()

        llm_client = _get_llm_client()
        llm_section = ""
        if llm_client:
            try:
                prompt = EnhancedReports._build_llm_prompt(
                    summary, today, health_score, ml_pred, comprehensive,
                    tracker, goals_mgr, "evening"
                )
                result = llm_client.generate(prompt)
                if result and "error" not in result:
                    content = result.get("content", "")
                    if content:
                        llm_section = f"\n🤖 **AI教练晚间总结**\n\n{content}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
            except Exception:
                pass

        if has_training and strain > 0:
            if strain >= 12:
                train_status = "💪 超高强度训练日"
                train_emoji = "🟢"
                train_effect = "有效提升力量和耐力"
            elif strain >= 8:
                train_status = "🏃 中高强度训练"
                train_emoji = "🟡"
                train_effect = "良好的训练刺激"
            else:
                train_status = "🚶 轻度活动"
                train_emoji = "🟠"
                train_effect = "保持身体活跃"
        else:
            train_status = "😴 今日休息"
            train_emoji = "🔴"
            train_effect = "充分休息，恢复能量"

        msg = f"🌙 **今日晚间追踪** `{date}`\n\n"
        msg += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"

        if llm_section:
            msg += llm_section

        msg += "📋 **今日训练总结**\n\n"
        msg += f"{train_emoji} {train_status}\n"
        msg += f"• strain：{strain:.1f}\n"
        msg += f"• 效果：{train_effect}\n"
        msg += f"• HRV：{hrv:.1f}ms\n"
        msg += f"• 静息心率：{rhr:.0f}bpm\n"
        msg += f"• 睡眠时长：{sleep_hours:.1f}h\n\n"

        rec_emoji, rec_status = _emoji_score(recovery)
        msg += "📊 **今日恢复**\n\n"
        msg += f"• 恢复评分：{rec_emoji} **{recovery:.0f}%**（{rec_status}）\n"
        msg += f"• 7日平均：{avg_recovery:.1f}%\n"
        msg += f"• 趋势：{_format_trend(recovery_trend)}\n"
        if sleep_debt > 2:
            msg += f"• ⚠️ 睡眠债务：{sleep_debt:.1f}h（建议今晚多睡）\n"
        msg += "\n"

        if body_battery:
            bb = body_battery.get('level', 0)
            bb_emoji, bb_status = _emoji_score(bb, (80, 50))
            msg += "🔋 **身体电量**\n\n"
            msg += f"• 电量：{bb_emoji} **{bb:.0f}%**（{bb_status}）\n"
            msg += f"• 建议：{body_battery.get('suggestion', 'N/A')}\n\n"

        if hz:
            msg += "💪 **心率区间**\n\n"
            msg += f"• 有氧区（Z2）：{hz.get('aerobic', 0):.1f}%\n"
            msg += f"• 无氧区（Z4+）：{hz.get('anaerobic', 0):.1f}%\n"
            msg += f"• 脂肪燃烧（Z1）：{hz.get('zone1', 0):.1f}%\n\n"

        if tracker:
            msg += "📝 **打卡统计**\n\n"
            msg += f"• 连续打卡：{streak}天 🔥\n"
            if weekly:
                msg += f"• 本周打卡：{weekly.get('total_checkins', 0)}次\n"
                cr = weekly.get('completion_rate', 0)
                if cr > 0:
                    msg += f"• 本周完成率：{cr:.0f}%\n"
            msg += "\n"

        if marathon_goal:
            msg += "🏅 **马拉松进度**\n\n"
            msg += f"• {marathon_goal.goal_name}：剩余{marathon_goal.days_until_race}天\n"
            msg += f"• 目标：{marathon_goal.goal_time}（{marathon_goal.target_pace}/km）\n"
            msg += f"• 阶段：{marathon_goal.training_phase}\n\n"

        msg += "🔮 **明日预测**\n\n"
        msg += f"• 预测恢复：{ml_emoji} **{ml_p:.0f}%**（{ml_status}）\n"
        if ml_p >= 70:
            msg += "✅ 明天状态不错，可以正常训练\n"
        elif ml_p >= 50:
            msg += "🟡 明天状态一般，建议中等强度\n"
        else:
            msg += "🔴 明天可能疲劳，建议休息\n"
        msg += "\n"

        msg += "😴 **睡眠建议**\n\n"
        if sleep_debt > 5:
            msg += f"⚠️ 睡眠债务{sleep_debt:.1f}h，今晚优先补觉！\n"
        elif sleep_debt > 2:
            msg += f"有{sleep_debt:.1f}h债务，建议比平时早睡30分钟。\n"
        elif recovery >= 70:
            msg += "恢复良好，保持当前睡眠节奏即可。\n"
        else:
            msg += "恢复不理想，今晚保证7-9小时睡眠。\n"
        msg += "\n"

        msg += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
        msg += "📌 *记得20:00来打卡！*\n"
        msg += "⏰ *下次推送：22:00 完整日报*\n"

        return msg

    @staticmethod
    def full_report() -> str:
        summary, today = _get_data(7)
        health_score = _get_health_score()
        ml_pred = _get_ml_prediction()
        comprehensive = _get_comprehensive()
        tracker = _get_tracker()
        goals_mgr = _get_goals()

        recovery = today.get('recovery', 0)
        hrv = today.get('hrv', 0)
        rhr = today.get('rhr', 0)
        sleep_hours = today.get('sleep_hours', 0)
        strain = today.get('strain', 0)
        date = today.get('date', today_str())

        avg_recovery = summary.get('avg_recovery', 0)
        avg_hrv = summary.get('avg_hrv', 0)
        avg_rhr = summary.get('avg_rhr', 0)
        avg_sleep = summary.get('avg_sleep_hours', 0)
        sleep_debt = summary.get('sleep_debt', 0)
        training_days = summary.get('training_days', 0)
        total_strain = summary.get('total_strain', 0)
        recovery_count = summary.get('recovery_count', 0)
        recovery_trend = summary.get('recovery_trend', 'stable')

        hs_score = health_score.get('score', 0)
        hs_breakdown = health_score.get('breakdown', {})
        hs_grade = health_score.get('grade', 'N/A')
        ml_p = ml_pred.get('prediction', 50)
        ml_conf = ml_pred.get('confidence', 'low')
        ml_reason = ml_pred.get('reason', '')
        ml_emoji, ml_status = _emoji_score(ml_p)

        comp = comprehensive
        hz = comp.get('heart_zones', {})
        ss_data = comp.get('sleep_stages', {})
        bb = comp.get('body_battery', {})
        resp = comp.get('respiratory', {})
        pred_acc = comp.get('prediction_accuracy', {})

        streak = tracker.get_streak("dongyi") if tracker else 0
        weekly = tracker.get_weekly_summary("dongyi") if tracker else {}
        marathon_goal = _get_marathon_goal()
        active_goals = goals_mgr.get_active_goals() if goals_mgr else []

        llm_client = _get_llm_client()
        llm_section = ""
        if llm_client:
            try:
                prompt = EnhancedReports._build_llm_prompt(
                    summary, today, health_score, ml_pred, comprehensive,
                    tracker, goals_mgr, "full"
                )
                result = llm_client.generate(prompt)
                if result and "error" not in result:
                    content = result.get("content", "")
                    if content:
                        llm_section = f"\n🤖 **AI教练综合分析**\n\n{content}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
            except Exception:
                pass

        msg = f"🌙 **今日完整健康日报** `{date}`\n\n"
        msg += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"

        if llm_section:
            msg += llm_section

        rec_emoji, rec_status = _emoji_score(recovery)
        hs_emoji, hs_status = _emoji_score(hs_score)

        msg += "📊 **核心指标**\n\n"
        msg += f"• 恢复评分：{rec_emoji} **{recovery:.0f}%**（{rec_status}）\n"
        msg += f"• HRV：{hrv:.1f}ms\n"
        msg += f"• 静息心率：{rhr:.0f}bpm\n"
        msg += f"• 睡眠时长：{sleep_hours:.1f}h\n"
        msg += f"• 今日strain：{strain:.1f}\n"
        msg += f"• 健康评分：{hs_emoji} **{hs_score:.0f}/100**（{hs_grade}级）\n\n"

        if hs_breakdown:
            msg += "🏆 **健康评分构成**\n\n"
            for k, v in hs_breakdown.items():
                label = {"recovery": "恢复", "sleep": "睡眠", "hrv": "HRV", "rhr": "心率"}.get(k, k)
                e, s = _emoji_score(v)
                msg += f"  • {label}：{e} {v:.0f}（{s}）\n"
            msg += "\n"

        msg += "📈 **7天统计**\n\n"
        msg += f"• 平均恢复：{avg_recovery:.1f}% {_format_trend(recovery_trend)}\n"
        msg += f"• 平均HRV：{avg_hrv:.1f}ms\n"
        msg += f"• 平均RHR：{avg_rhr:.0f}bpm\n"
        msg += f"• 平均睡眠：{avg_sleep:.1f}h\n"
        msg += f"• 7日总strain：{total_strain:.1f}\n"
        msg += f"• 训练天数：{training_days}天\n"
        msg += f"• 有效数据：{recovery_count}条\n"
        if sleep_debt > 0:
            msg += f"• 睡眠债务：⚠️ {sleep_debt:.1f}h\n"
        msg += "\n"

        ss = ss_data.get('percentages', {})
        if ss:
            msg += "😴 **睡眠结构**\n\n"
            for stage, pct in ss.items():
                stage_name = {"light": "浅睡眠", "deep": "深睡眠", "rem": "REM睡眠"}.get(stage, stage)
                msg += f"• {stage_name}：{pct:.0f}%\n"
            msg += "\n"

        if resp:
            rr = resp.get('respiratory_rate', 0)
            if rr > 0:
                msg += "🫁 **呼吸**\n\n"
                msg += f"• 呼吸频率：{rr:.1f}次/分钟"
                if rr > 20:
                    msg += "（略高）\n"
                elif rr < 12:
                    msg += "（正常偏低）\n"
                else:
                    msg += "（正常）\n"
                msg += "\n"

        if hz:
            msg += "💪 **心率区间**\n\n"
            zone0_1 = hz.get('zone0', 0) + hz.get('zone1', 0)
            msg += f"• 轻松区（Z0-Z1）：{zone0_1:.1f}%\n"
            msg += f"• 有氧区（Z2-Z3）：{hz.get('aerobic', 0):.1f}%\n"
            msg += f"• 无氧区（Z4-Z5）：{hz.get('anaerobic', 0):.1f}%\n\n"

        if bb:
            bb_val = bb.get('level', 0)
            bb_emoji, bb_status = _emoji_score(bb_val, (80, 50))
            msg += "🔋 **身体电量**\n\n"
            msg += f"• 当前电量：{bb_emoji} **{bb_val:.0f}%**（{bb_status}）\n"
            msg += f"• 建议：{bb.get('suggestion', 'N/A')}\n\n"

        msg += "🔮 **ML恢复预测**\n\n"
        msg += f"• 明日预测：{ml_emoji} **{ml_p:.0f}%**（{ml_status}）\n"
        msg += f"• 置信度：{ml_conf}\n"
        if ml_reason:
            msg += f"• 判断依据：{ml_reason}\n"
        predictions = ml_pred.get('predictions', [])
        if predictions:
            msg += "• 未来趋势：\n"
            for i, p in enumerate(predictions[:7], 1):
                e = "🟢" if p >= 70 else "🟡" if p >= 50 else "🔴"
                msg += f"  第{i}天：{p:.0f}% {e}\n"
        if pred_acc:
            acc = pred_acc.get('accuracy', 0)
            if acc > 0:
                msg += f"• 模型准确率：{acc:.0f}%\n"
        msg += "\n"

        if tracker:
            msg += "📝 **打卡统计**\n\n"
            msg += f"• 连续打卡：{streak}天 🔥\n"
            if weekly:
                msg += f"• 本周打卡：{weekly.get('total_checkins', 0)}次\n"
                cr = weekly.get('completion_rate', 0)
                if cr > 0:
                    msg += f"• 本周完成率：{cr:.0f}%\n"
            msg += "\n"

        if marathon_goal:
            msg += "🏅 **马拉松目标**\n\n"
            msg += f"• {marathon_goal.goal_name} | {marathon_goal.race_type}\n"
            msg += f"• 目标：{marathon_goal.goal_time}（{marathon_goal.target_pace}/km）\n"
            msg += f"• 剩余：{marathon_goal.days_until_race}天\n"
            msg += f"• 阶段：{marathon_goal.training_phase}\n\n"

        if active_goals:
            msg += "🎯 **活跃目标**\n\n"
            for goal in active_goals:
                pct = (goal.current / goal.target * 100) if goal.target > 0 else 0
                msg += f"• {goal.goal_type}：{goal.current}/{goal.target} {goal.unit}（{pct:.0f}%）\n"
            msg += "\n"

        msg += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
        msg += f"_数据来源：WHOOP | 健康糕_\n"
        msg += f"_生成时间：{bj_now().strftime('%Y-%m-%d %H:%M:%S')}_\n"

        return msg

    @staticmethod
    def _build_llm_prompt(summary, today, health_score, ml_pred, comprehensive,
                         tracker, goals_mgr, report_type: str) -> str:
        """构建 LLM 分析 prompt"""

        recovery = today.get('recovery', 0)
        hrv = today.get('hrv', 0)
        rhr = today.get('rhr', 0)
        sleep_hours = today.get('sleep_hours', 0)
        strain = today.get('strain', 0)
        sleep_debt = summary.get('sleep_debt', 0)
        training_days = summary.get('training_days', 0)
        avg_recovery = summary.get('avg_recovery', 0)
        avg_hrv = summary.get('avg_hrv', 0)
        avg_sleep = summary.get('avg_sleep_hours', 0)
        recovery_trend = summary.get('recovery_trend', 'stable')
        total_strain = summary.get('total_strain', 0)

        hs_score = health_score.get('score', 0)
        hs_grade = health_score.get('grade', 'N/A')
        ml_p = ml_pred.get('prediction', 50)
        ml_reason = ml_pred.get('reason', '')
        ml_conf = ml_pred.get('confidence', 'low')

        comp = comprehensive
        hz = comp.get('heart_zones', {})
        body_battery = comp.get('body_battery', {})
        bb = body_battery.get('level', 0) if body_battery else 0

        streak = tracker.get_streak("dongyi") if tracker else 0

        active_goals = goals_mgr.get_active_goals() if goals_mgr else []
        marathon_goal = _get_marathon_goal()

        goals_text = "\n".join([
            f"- {g.goal_type}：{g.current}/{g.target} {g.unit}"
            for g in active_goals[:3]
        ]) or "暂无"

        marathon_text = ""
        if marathon_goal:
            marathon_text = f"""
马拉松目标：
- 比赛：{marathon_goal.goal_name}（{marathon_goal.race_type}）
- 目标：{marathon_goal.goal_time}（{marathon_goal.target_pace}/km）
- 剩余：{marathon_goal.days_until_race}天
- 阶段：{marathon_goal.training_phase}"""

        base_data = f"""【用户今日数据】
- 恢复评分：{recovery:.0f}%
- HRV：{hrv:.1f}ms
- 静息心率：{rhr:.0f}bpm
- 睡眠时长：{sleep_hours:.1f}h
- 睡眠债务：{sleep_debt:.1f}h
- 今日strain：{strain:.1f}
- 本周训练天数：{training_days}天
- 7日平均恢复：{avg_recovery:.1f}%
- 7日平均HRV：{avg_hrv:.1f}ms
- 7日平均睡眠：{avg_sleep:.1f}h
- 恢复趋势：{recovery_trend}
- 健康评分：{hs_score:.0f}/100（{hs_grade}级）
- ML预测明日：{ml_p:.0f}%（置信度：{ml_conf}）
- ML判断依据：{ml_reason}
- 身体电量：{bb:.0f}%
- 连续打卡：{streak}天
- 心率区间：有氧{hz.get('aerobic', 0):.1f}% / 无氧{hz.get('anaerobic', 0):.1f}%

【当前目标】
{goals_text}
{marathon_text}"""

        if report_type == "morning":
            return f"""你是专业且温暖的AI健身教练，根据以下用户数据生成详细的早安分析报告：

{base_data}

请生成300-400字的中文分析，包括：
1. 对用户当前状态的详细解读（特别是恢复、HRV、身心状态）
2. 哪些指标需要特别关注（如果有）
3. 具体的、可执行的今日训练建议
4. 关怀和鼓励的话语

语气：专业、温暖、像了解用户的私人教练一样。直接给出分析内容，不要说"根据数据..."这类开场白。"""

        elif report_type == "evening":
            has_training = today.get('has_training', False)
            return f"""你是专业且温暖的个人健身教练，根据以下数据生成晚间总结和明日建议：

{base_data}

【今日训练】：{"有" if has_training else "无"}（strain: {strain:.1f}）

请生成300-400字的中文总结，包括：
1. 对今日训练/休息的评价
2. 恢复效果评估
3. 明日身体状态预测和具体训练建议
4. 睡眠和恢复指导

语气：专业、支持、激励。直接给出内容。"""

        else:
            return f"""你是专业且全面的AI健身教练，根据以下完整数据生成综合健康日报：

{base_data}

请生成500-600字的中文综合分析，包括：
1. 整体健康状态评估
2. 各指标详细解读（恢复、睡眠、HRV、训练负荷）
3. 发现的问题和潜在风险
4. 具体改进建议
5. 接下来的行动计划

语气：专业、详细、有洞察力。直接给出内容。"""


def get_morning_report() -> str:
    return EnhancedReports.morning_report()


def get_evening_report() -> str:
    return EnhancedReports.evening_report()


def get_full_report() -> str:
    return EnhancedReports.full_report()


path: lib/feedback_learning.py
content: """
Feedback Learning System - 收集用户反馈并与 WHOOP 数据对比学习

学习机制：
1. 睡眠质量：用户主观反馈 vs WHOOP 睡眠效率/深睡眠时长 对比
2. 训练感受：用户主观反馈 vs 实际训练强度（strain/心率）对标
3. 恢复感知：用户感知 vs WHOOP 恢复评分/HRV 对标
4. 有效性追踪：建议被采纳的比例

数据保存在 data/processed/feedback_learn.json
"""

import os
import json
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional, Tuple

SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORKSPACE_DIR = os.environ.get(
    "OPENCLAW_WORKSPACE",
    os.path.dirname(SKILL_DIR)
)
PROCESSED_DIR = os.environ.get(
    "WHOOP_DATA_DIR",
    os.path.join(WORKSPACE_DIR, "data", "processed")
)
FEEDBACK_FILE = os.path.join(PROCESSED_DIR, 'feedback_learn.json')


# ============================================================
# 基础存储
# ============================================================

def _load() -> Dict:
    try:
        with open(FEEDBACK_FILE, 'r') as f:
            return json.load(f)
    except Exception:
        return _default()


def _save(data: Dict) -> None:
    os.makedirs(os.path.dirname(FEEDBACK_FILE), exist_ok=True)
    with open(FEEDBACK_FILE, 'w') as f:
        json.dump(data, f, indent=2, ensure_ascii=False)


def _default() -> Dict:
    return {
        "version": 2,
        "feedback": [],          # 所有反馈记录
        "sleep_quality_map": {},   # 用户感知 → WHOOP数据映射（睡眠）
        "training_intensity_map": {},  # 用户感受词 → strain区间
        "recovery_map": {},      # 用户感知 → WHOOP恢复区间
        "adjustments": [],       # 系统调整记录
        "stats": {
            "total_feedbacks": 0,
            "sleep_correlation": 0.0,   # 睡眠反馈与WHOOP数据相关性
            "training_correlation": 0.0, # 训练反馈相关性
        }
    }


# ============================================================
# 反馈录入（与 WHOOP 数据对比）
# ============================================================

FEEDBACK_CATEGORIES = ["sleep", "training", "recovery", "general"]

SLEEP_QUALITY_KEYWORDS = {
    "很好": ["好", "不错", "棒", "充足", "精力充沛"],
    "一般": ["普通", "还行", "一般", "有点累"],
    "差": ["差", "糟糕", "不足", "困", "累", "没睡好"],
}

TRAINING_FEEL_KEYWORDS = {
    "轻松": ["轻松", "easy", "毫不费力", "恢复跑"],
    "正常": ["正常", "不错", "还好", "良好", "良好"],
    "吃力": ["吃力", "累", "喘", "费劲", "强度大"],
    "过度": ["过度", "透支", "太累了", "崩溃", "受伤"],
}

RECOVERY_KEYWORDS = {
    "完全恢复": ["完全恢复", "精力充沛", "满血", "状态好"],
    "部分恢复": ["还行", "一般", "普通", "正常"],
    "未恢复": ["累", "疲惫", "没恢复", "还很累", "透支"],
}


def _classify_sleep(feedback_text: str) -> Optional[str]:
    """从反馈文字推断睡眠质量等级"""
    text = feedback_text.lower()
    for quality, keywords in SLEEP_QUALITY_KEYWORDS.items():
        if any(kw in text for kw in keywords):
            return quality
    return None


def _classify_training_feel(feedback_text: str) -> Optional[str]:
    """从反馈文字推断训练感受"""
    text = feedback_text.lower()
    for feel, keywords in TRAINING_FEEL_KEYWORDS.items():
        if any(kw in text for kw in keywords):
            return feel
    return None


def _classify_recovery(feedback_text: str) -> Optional[str]:
    """从反馈文字推断恢复程度"""
    text = feedback_text.lower()
    for level, keywords in RECOVERY_KEYWORDS.items():
        if any(kw in text for kw in keywords):
            return level
    return None


def add_feedback(
    category: str,
    feedback_text: str,
    whoop_baseline: Optional[Dict] = None,
    notes: str = ""
) -> Dict:
    """
    记录用户反馈，同时存储 WHOOP 基线数据用于学习

    Args:
        category: sleep | training | recovery | general
        feedback_text: 用户反馈的文字内容
        whoop_baseline: WHOOP 实际数据（如 {'sleep_efficiency': 95, 'strain': 16.0}）
        notes: 额外备注
    """
    data = _load()

    entry = {
        "timestamp": datetime.now().isoformat(),
        "category": category,
        "feedback_text": feedback_text,
        "notes": notes,
    }

    # 推断主观等级
    inferred = None
    if category == "sleep":
        inferred = _classify_sleep(feedback_text)
        entry["inferred_sleep"] = inferred
    elif category == "training":
        inferred = _classify_training_feel(feedback_text)
        entry["inferred_training"] = inferred
    elif category == "recovery":
        inferred = _classify_recovery(feedback_text)
        entry["inferred_recovery"] = inferred

    # 存储 WHOOP 基线数据
    if whoop_baseline:
        entry["whoop_baseline"] = whoop_baseline

        # 更新映射表（用户词 → WHOOP数据）
        _update_baseline_map(data, category, entry, whoop_baseline)

    data["feedback"].append(entry)
    data["stats"]["total_feedbacks"] += 1

    _save(data)
    return _get_recent_summary(data)


def _update_baseline_map(
    data: Dict,
    category: str,
    entry: Dict,
    baseline: Dict
) -> None:
    """更新用户词 → WHOOP数据 的映射表"""

    inferred = entry.get(f"inferred_{category}", None) if category in [
        "sleep", "training", "recovery"
    ] else None

    if not inferred:
        return

    if category == "sleep":
        key = "sleep_quality_map"
        if key not in data:
            data[key] = {}
        if inferred not in data[key]:
            data[key][inferred] = {"samples": [], "avg_efficiency": 0, "avg_deep_sleep": 0}

        sample = {
            "sleep_efficiency": baseline.get("sleep_efficiency", 0),
            "deep_sleep_min": baseline.get("deep_sleep_min", 0),
            "sleep_performance": baseline.get("sleep_performance", 0),
            "disturbances": baseline.get("disturbance_count", 0),
        }
        data[key][inferred]["samples"].append(sample)

        # 更新平均值
        samples = data[key][inferred]["samples"]
        n = len(samples)
        data[key][inferred]["avg_efficiency"] = round(
            sum(s["sleep_efficiency"] for s in samples) / n, 1
        )
        data[key][inferred]["avg_deep_sleep"] = round(
            sum(s["deep_sleep_min"] for s in samples) / n, 1
        )

    elif category == "training":
        key = "training_intensity_map"
        if key not in data:
            data[key] = {}
        if inferred not in data[key]:
            data[key][inferred] = {"samples": [], "avg_strain": 0, "avg_hr": 0}

        sample = {
            "strain": baseline.get("strain", 0),
            "avg_hr": baseline.get("avg_hr", 0),
        }
        data[key][inferred]["samples"].append(sample)

        samples = data[key][inferred]["samples"]
        n = len(samples)
        data[key][inferred]["avg_strain"] = round(
            sum(s["strain"] for s in samples) / n, 1
        )
        data[key][inferred]["avg_hr"] = round(
            sum(s["avg_hr"] for s in samples) / n, 1
        )

    elif category == "recovery":
        key = "recovery_map"
        if key not in data:
            data[key] = {}
        if inferred not in data[key]:
            data[key][inferred] = {"samples": [], "avg_recovery": 0, "avg_hrv": 0}

        sample = {
            "recovery_score": baseline.get("recovery_score", 0),
            "hrv": baseline.get("hrv", 0),
            "rhr": baseline.get("rhr", 0),
        }
        data[key][inferred]["samples"].append(sample)

        samples = data[key][inferred]["samples"]
        n = len(samples)
        data[key][inferred]["avg_recovery"] = round(
            sum(s["recovery_score"] for s in samples) / n, 1
        )
        data[key][inferred]["avg_hrv"] = round(
            sum(s["hrv"] for s in samples) / n, 1
        )


# ============================================================
# 学习成果查询
# ============================================================

def get_sleep_quality_baseline(feedback_quality: str) -> Optional[Dict]:
    """
    查询用户说"很好/一般/差"时，对应的 WHOOP 数据基线
    用于理解用户的个人标准
    """
    data = _load()
    sq_map = data.get("sleep_quality_map", {})
    return sq_map.get(feedback_quality)


def get_training_feel_baseline(feel: str) -> Optional[Dict]:
    """查询用户说"轻松/正常/吃力"时，对应的训练数据基线"""
    data = _load()
    ti_map = data.get("training_intensity_map", {})
    return ti_map.get(feel)


def get_recovery_baseline(level: str) -> Optional[Dict]:
    """查询用户说"完全恢复/部分恢复/未恢复"时，对应的恢复数据基线"""
    data = _load()
    rc_map = data.get("recovery_map", {})
    return rc_map.get(level)


def get_recent_summary(data: Dict = None) -> Dict:
    """获取最近的学习摘要"""
    if data is None:
        data = _load()

    feedbacks = data.get("feedback", [])
    recent = feedbacks[-10:] if len(feedbacks) > 10 else feedbacks

    return {
        "total": data["stats"]["total_feedbacks"],
        "recent": recent[::-1],  # 最新的在前
        "sleep_map": data.get("sleep_quality_map", {}),
        "training_map": data.get("training_intensity_map", {}),
        "recovery_map": data.get("recovery_map", {}),
    }


def _get_recent_summary(data: Dict) -> Dict:
    return get_recent_summary(data)


# ============================================================
# 调整建议生成
# ============================================================

def should_adjust_recommendation(
    category: str,
    inferred_level: str,
    current_whoop: Dict
) -> Tuple[bool, str]:
    """
    判断是否需要调整建议

    Returns:
        (should_adjust: bool, reason: str)
    """
    data = _load()

    if category == "sleep":
        baseline = get_sleep_quality_baseline(inferred_level)
        if not baseline or baseline["samples"] < 3:
            return False, ""

        # 用户说睡眠"差"，但 WHOOP 效率>90% → 可能需要提醒用户注意入睡时机
        current_eff = current_whoop.get("sleep_efficiency", 0)
        if inferred_level == "差" and current_eff > 90:
            return True, "用户感知睡眠差，但WHOOP效率正常偏高，可能存在主观入睡困难"
        # 用户说睡眠"好"，但实际效率<85% → 建议改善睡眠卫生
        if inferred_level == "很好" and current_eff < 85:
            return True, "用户感知睡眠好，但WHOOP效率偏低，可能存在测量误差或习惯性低估"

    elif category == "training":
        baseline = get_training_feel_baseline(inferred_level)
        if not baseline or baseline["samples"] < 3:
            return False, ""

        current_strain = current_whoop.get("strain", 0)
        avg_strain = baseline["avg_strain"]
        if inferred_level == "正常" and abs(current_strain - avg_strain) > 3:
            if current_strain > avg_strain + 3:
                return True, f"训练强度({current_strain})明显高于用户正常水平({avg_strain})，建议加强恢复"
            if current_strain < avg_strain - 3:
                return True, f"训练强度({current_strain})明显低于用户正常水平({avg_strain})，可能是休息日"

    elif category == "recovery":
        baseline = get_recovery_baseline(inferred_level)
        if not baseline or baseline["samples"] < 3:
            return False, ""

        current_rec = current_whoop.get("recovery_score", 0)
        avg_rec = baseline["avg_recovery"]
        if inferred_level == "完全恢复" and current_rec < 60:
            return True, "用户感知完全恢复，但WHOOP恢复评分偏低，建议保持当前节奏"
        if inferred_level == "未恢复" and current_rec > 70:
            return True, "用户感知未恢复，但WHOOP恢复评分良好，可能需要关注心理状态"

    return False, ""


# ============================================================
# 打印学习统计（CLI工具）
# ============================================================

def print_learning_stats() -> None:
    """打印学习统计"""
    data = _load()
    total = data["stats"]["total_feedbacks"]

    print("📊 反馈学习统计")
    print("-" * 50)
    print(f"总反馈数：{total}")
    print()

    sq = data.get("sleep_quality_map", {})
    if sq:
        print("😴 睡眠感知基线（用户词 → WHOOP数据）：")
        for quality, info in sq.items():
            n = len(info["samples"])
            print(f"  「{quality}」（{n}次样本）")
            print(f"    平均睡眠效率：{info['avg_efficiency']:.1f}%")
            print(f"    平均深睡眠：{info['avg_deep_sleep']:.0f}min")
        print()

    ti = data.get("training_intensity_map", {})
    if ti:
        print("🏃 训练感受基线（用户词 → 训练数据）：")
        for feel, info in ti.items():
            n = len(info["samples"])
            print(f"  「{feel}」（{n}次样本）")
            print(f"    平均strain：{info['avg_strain']:.1f}")
            print(f"    平均心率：{info['avg_hr']:.0f}bpm")
        print()

    rc = data.get("recovery_map", {})
    if rc:
        print("💪 恢复感知基线（用户词 → 恢复数据）：")
        for level, info in rc.items():
            n = len(info["samples"])
            print(f"  「{level}」（{n}次样本）")
            print(f"    平均恢复评分：{info['avg_recovery']:.1f}%")
            print(f"    平均HRV：{info['avg_hrv']:.1f}ms")
        print()

    if total == 0:
        print("暂无反馈数据，开始打卡后将自动积累。")


# CLI
if __name__ == "__main__":
    import sys

    if len(sys.argv) == 1:
        print_learning_stats()
    elif sys.argv[1] == "add":
        # add <category> <feedback_text> [notes]
        cat = sys.argv[2] if len(sys.argv) > 2 else "general"
        text = sys.argv[3] if len(sys.argv) > 3 else ""
        notes = sys.argv[4] if len(sys.argv) > 4 else ""
        result = add_feedback(cat, text, notes=notes)
        print(f"✅ 反馈已记录")
    elif sys.argv[1] == "stats":
        print_learning_stats()


path: lib/goals.py
content: """
Goals Module - 目标追踪系统
管理用户健身目标设定和进度追踪
包括马拉松训练目标管理
"""

import os
import json
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict, field

# 路径设置
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GOALS_DIR = os.path.join(SKILL_DIR, "data", "profiles")


@dataclass
class Goal:
    """基础目标数据类"""
    goal_id: str
    user_id: str
    goal_type: str  # 增肌/减脂/力量/体能/马拉松训练
    target: float
    current: float
    unit: str  # kg/次/%/场/km
    deadline: str
    created_at: str
    updated_at: str
    completed: bool = False
    completed_at: str = ""


@dataclass
class MarathonGoal:
    """马拉松目标数据类"""
    goal_id: str
    user_id: str
    goal_type: str = "马拉松训练"
    goal_name: str = ""  # 如"北京半马"
    race_date: str = ""  # YYYY-MM-DD
    race_type: str = "半程马拉松"  # 半程马拉松/全程马拉松
    goal_time: str = ""  # HH:MM:SS
    target_pace: str = ""  # MM:SS（每公里）
    splits: Dict[str, str] = field(default_factory=dict)  # {"前半马": "5:40", "后半马": "5:45"}
    current: float = 0  # 完成的训练次数
    target: float = 1  # 目标场次
    unit: str = "场"
    status: str = "active"  # active/completed/cancelled
    created_at: str = ""
    updated_at: str = ""
    notes: str = ""  # 备注
    
    def __post_init__(self):
        if not self.created_at:
            self.created_at = datetime.now().isoformat()
        if not self.updated_at:
            self.updated_at = datetime.now().isoformat()
    
    @property
    def days_until_race(self) -> int:
        """计算距离比赛还有多少天"""
        try:
            race_date_obj = datetime.strptime(self.race_date, "%Y-%m-%d").date()
            today = datetime.now().date()
            return (race_date_obj - today).days
        except Exception:
            return 0
    
    @property
    def training_phase(self) -> str:
        """根据距离比赛天数返回训练阶段"""
        days = self.days_until_race
        
        if days < 0:
            return "已结束"
        elif days == 0:
            return "比赛日"
        elif days <= 7:
            return "比赛周"
        elif days <= 14:
            return "减量期"
        elif days <= 28:
            return "巅峰期"
        else:
            return "基础期"
    
    @property
    def is_recovery_week_needed(self) -> bool:
        """针对背靠背比赛，是否需要恢复周"""
        # 如果距离比赛小于7天且是全程马拉松，可能需要恢复周
        if self.race_type == "全程马拉松" and self.days_until_race <= 7:
            return True
        return False
    
    def to_dict(self) -> Dict:
        """转换为字典"""
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: Dict) -> 'MarathonGoal':
        """从字典创建"""
        return cls(**data)


class GoalsManager:
    """
    目标管理器
    管理用户的健身目标（包括马拉松训练目标）
    """
    
    def __init__(self, user_id: str = "default"):
        self.user_id = user_id
        self.goals_file = os.path.join(GOALS_DIR, f"goals_{user_id}.json")
        self.marathon_goals_file = os.path.join(GOALS_DIR, f"marathon_goals_{user_id}.json")
        os.makedirs(GOALS_DIR, exist_ok=True)
    
    # ==================== 基础目标 CRUD ====================
    
    def _load_goals(self) -> List[Dict]:
        """加载基础目标"""
        if not os.path.exists(self.goals_file):
            return []
        try:
            with open(self.goals_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception:
            return []
    
    def _save_goals(self, goals: List[Dict]) -> bool:
        """保存基础目标"""
        try:
            with open(self.goals_file, 'w', encoding='utf-8') as f:
                json.dump(goals, f, ensure_ascii=False, indent=2)
            return True
        except Exception:
            return False
    
    def create_goal(
        self,
        goal_type: str,
        target: float,
        current: float,
        unit: str,
        deadline: str
    ) -> Goal:
        """创建新目标"""
        goal = Goal(
            goal_id=f"goal_{datetime.now().strftime('%Y%m%d%H%M%S')}",
            user_id=self.user_id,
            goal_type=goal_type,
            target=target,
            current=current,
            unit=unit,
            deadline=deadline,
            created_at=datetime.now().isoformat(),
            updated_at=datetime.now().isoformat()
        )
        
        goals = self._load_goals()
        goals.append(asdict(goal))
        self._save_goals(goals)
        
        return goal
    
    def get_active_goals(self) -> List[Goal]:
        """获取活跃基础目标"""
        goals = self._load_goals()
        return [Goal(**g) for g in goals if not g.get("completed", False)]
    
    def get_completed_goals(self) -> List[Goal]:
        """获取已完成基础目标"""
        goals = self._load_goals()
        return [Goal(**g) for g in goals if g.get("completed", False)]
    
    def update_progress(self, goal_id: str, current: float) -> bool:
        """更新目标进度"""
        goals = self._load_goals()
        
        for goal in goals:
            if goal["goal_id"] == goal_id:
                goal["current"] = current
                goal["updated_at"] = datetime.now().isoformat()
                
                if current >= goal["target"]:
                    goal["completed"] = True
                    goal["completed_at"] = datetime.now().isoformat()
                
                return self._save_goals(goals)
        
        return False
    
    def mark_completed(self, goal_id: str) -> bool:
        """标记目标完成"""
        goals = self._load_goals()
        
        for goal in goals:
            if goal["goal_id"] == goal_id:
                goal["completed"] = True
                goal["completed_at"] = datetime.now().isoformat()
                goal["updated_at"] = datetime.now().isoformat()
                return self._save_goals(goals)
        
        return False
    
    def delete_goal(self, goal_id: str) -> bool:
        """删除目标"""
        goals = self._load_goals()
        original_len = len(goals)
        goals = [g for g in goals if g["goal_id"] != goal_id]
        
        if len(goals) < original_len:
            return self._save_goals(goals)
        return False
    
    def get_goal_progress(self, goal: Goal) -> Dict:
        """计算目标进度"""
        if goal.target == 0:
            progress = 0
        else:
            progress = (goal.current / goal.target) * 100
        
        remaining = max(0, goal.target - goal.current)
        
        days_left = 0
        try:
            deadline = datetime.strptime(goal.deadline, "%Y-%m-%d")
            days_left = (deadline - datetime.now()).days
        except Exception:
            pass
        
        on_track = goal.completed
        if days_left > 0 and not goal.completed:
            total_days = (datetime.strptime(goal.deadline, "%Y-%m-%d") - 
                         datetime.strptime(goal.created_at[:10], "%Y-%m-%d")).days
            elapsed = total_days - days_left
            if total_days > 0:
                expected_progress = (elapsed / total_days) * 100
                on_track = progress >= expected_progress * 0.9
        
        return {
            "progress": round(min(100, progress), 1),
            "remaining": round(remaining, 1),
            "days_left": days_left,
            "on_track": on_track,
            "completed": goal.completed
        }
    
    # ==================== 马拉松目标 CRUD ====================
    
    def _load_marathon_goals(self) -> List[Dict]:
        """加载马拉松目标"""
        if not os.path.exists(self.marathon_goals_file):
            return []
        try:
            with open(self.marathon_goals_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception:
            return []
    
    def _save_marathon_goals(self, goals: List[Dict]) -> bool:
        """保存马拉松目标"""
        try:
            with open(self.marathon_goals_file, 'w', encoding='utf-8') as f:
                json.dump(goals, f, ensure_ascii=False, indent=2)
            return True
        except Exception:
            return False
    
    def create_marathon_goal(
        self,
        race_name: str,
        race_date: str,
        goal_time: str = None,
        race_type: str = "半程马拉松",
        target_pace: str = None,
        splits: Dict[str, str] = None,
        notes: str = ""
    ) -> MarathonGoal:
        """
        创建马拉松训练目标
        
        Args:
            race_name: 比赛名称（如"北京半马"）
            race_date: 比赛日期（YYYY-MM-DD）
            goal_time: 目标时间（HH:MM:SS），如"02:00:00"
            race_type: 比赛类型（半程马拉松/全程马拉松）
            target_pace: 目标配速（MM:SS），如"5:40"
            splits: 分段策略，如{"前半马": "5:40", "后半马": "5:45"}
            notes: 备注
        
        Returns:
            创建的MarathonGoal对象
        """
        # 计算目标配速
        if goal_time:
            # 根据目标时间反推配速
            if race_type == "半程马拉松":
                total_seconds = self._time_to_seconds(goal_time)
                pace_seconds = total_seconds / 21.0975
                target_pace = self._seconds_to_pace(pace_seconds)
            elif race_type == "全程马拉松":
                total_seconds = self._time_to_seconds(goal_time)
                pace_seconds = total_seconds / 42.195
                target_pace = self._seconds_to_pace(pace_seconds)
        
        # 计算分段配速
        if splits is None:
            if race_type == "半程马拉松" and target_pace:
                splits = {
                    "前半马": target_pace,
                    "后半马": self._add_seconds_to_pace(target_pace, 5)  # 后半马慢5秒
                }
            elif race_type == "全程马拉松" and target_pace:
                splits = {
                    "前21km": target_pace,
                    "后21km": self._add_seconds_to_pace(target_pace, 5)  # 后半马慢5秒
                }
        
        marathon_goal = MarathonGoal(
            goal_id=f"marathon_{datetime.now().strftime('%Y%m%d%H%M%S')}",
            user_id=self.user_id,
            goal_type="马拉松训练",
            goal_name=race_name,
            race_date=race_date,
            race_type=race_type,
            goal_time=goal_time or "",
            target_pace=target_pace or "",
            splits=splits or {},
            current=0,
            target=1,
            unit="场",
            status="active",
            created_at=datetime.now().isoformat(),
            updated_at=datetime.now().isoformat(),
            notes=notes
        )
        
        goals = self._load_marathon_goals()
        goals.append(marathon_goal.to_dict())
        self._save_marathon_goals(goals)
        
        return marathon_goal
    
    def get_marathon_goals(self) -> List[MarathonGoal]:
        """获取所有马拉松目标"""
        goals = self._load_marathon_goals()
        return [MarathonGoal.from_dict(g) for g in goals]
    
    def get_active_marathon_goals(self) -> List[MarathonGoal]:
        """获取活跃的马拉松目标"""
        goals = self._load_marathon_goals()
        return [MarathonGoal.from_dict(g) for g in goals 
                if g.get("status", "active") == "active"]
    
    def get_active_marathon_goal(self) -> Optional[MarathonGoal]:
        """获取最近的一场马拉松目标（按日期排序）"""
        active_goals = self.get_active_marathon_goals()
        if not active_goals:
            return None
        
        # 按日期排序，返回最近的
        active_goals.sort(key=lambda x: x.race_date)
        return active_goals[0]
    
    def get_upcoming_marathon_goals(self, limit: int = 2) -> List[MarathonGoal]:
        """获取即将到来的马拉松目标"""
        active = self.get_active_marathon_goals()
        # 过滤出未来7天内的
        upcoming = [g for g in active if g.days_until_race >= 0]
        upcoming.sort(key=lambda x: x.race_date)
        return upcoming[:limit]
    
    def get_days_until_race(self, goal_id: str) -> int:
        """计算距离比赛还有多少天"""
        goals = self._load_marathon_goals()
        for g in goals:
            if g["goal_id"] == goal_id:
                try:
                    race_date = datetime.strptime(g["race_date"], "%Y-%m-%d").date()
                    today = datetime.now().date()
                    return (race_date - today).days
                except Exception:
                    return 0
        return 0
    
    def get_training_phase(self, goal_id: str = None) -> str:
        """根据距离比赛天数返回训练阶段"""
        if goal_id is None:
            goal = self.get_active_marathon_goal()
            if goal is None:
                return "无计划"
            return goal.training_phase
        
        goals = self._load_marathon_goals()
        for g in goals:
            if g["goal_id"] == goal_id:
                try:
                    race_date = datetime.strptime(g["race_date"], "%Y-%m-%d").date()
                    today = datetime.now().date()
                    days = (race_date - today).days
                    
                    if days < 0:
                        return "已结束"
                    elif days == 0:
                        return "比赛日"
                    elif days <= 7:
                        return "比赛周"
                    elif days <= 14:
                        return "减量期"
                    elif days <= 28:
                        return "巅峰期"
                    else:
                        return "基础期"
                except Exception:
                    return "未知"
        return "未找到"
    
    def get_recovery_week_warning(self, goal_id: str = None) -> bool:
        """检查是否需要提醒恢复周（背靠背比赛）"""
        if goal_id is None:
            goal = self.get_active_marathon_goal()
            if goal is None:
                return False
            return goal.is_recovery_week_needed
        
        goals = self._load_marathon_goals()
        for g in goals:
            if g["goal_id"] == goal_id:
                if g.get("race_type") == "全程马拉松":
                    try:
                        race_date = datetime.strptime(g["race_date"], "%Y-%m-%d").date()
                        today = datetime.now().date()
                        days = (race_date - today).days
                        return days <= 7
                    except Exception:
                        pass
        return False
    
    def calculate_suggested_weekly_volume(self, goal_id: str = None) -> Dict:
        """根据训练阶段计算建议周跑量"""
        if goal_id is None:
            goal = self.get_active_marathon_goal()
            if goal is None:
                return {"suggested_km": 0, "phase": "无计划"}
            phase = goal.training_phase
        else:
            phase = self.get_training_phase(goal_id)
        
        # 根据阶段建议跑量
        phase_config = {
            "基础期": {"weekly_km": 50, "long_run_km": 15, "intensity": "低"},
            "巅峰期": {"weekly_km": 60, "long_run_km": 18, "intensity": "中"},
            "减量期": {"weekly_km": 35, "long_run_km": 10, "intensity": "低"},
            "比赛周": {"weekly_km": 15, "long_run_km": 5, "intensity": "极低"},
            "比赛日": {"weekly_km": 0, "long_run_km": 0, "intensity": "比赛"},
        }
        
        config = phase_config.get(phase, phase_config["基础期"])
        config["phase"] = phase
        return config
    
    def update_marathon_goal(self, goal_id: str, **kwargs) -> bool:
        """更新马拉松目标"""
        goals = self._load_marathon_goals()
        
        for i, goal in enumerate(goals):
            if goal["goal_id"] == goal_id:
                for key, value in kwargs.items():
                    if key in goal:
                        goal[key] = value
                goal["updated_at"] = datetime.now().isoformat()
                return self._save_marathon_goals(goals)
        
        return False
    
    def delete_marathon_goal(self, goal_id: str) -> bool:
        """删除马拉松目标"""
        goals = self._load_marathon_goals()
        original_len = len(goals)
        goals = [g for g in goals if g["goal_id"] != goal_id]
        
        if len(goals) < original_len:
            return self._save_marathon_goals(goals)
        return False
    
    def mark_marathon_completed(self, goal_id: str) -> bool:
        """标记马拉松目标完成"""
        return self.update_marathon_goal(goal_id, status="completed")
    
    # ==================== 辅助方法 ====================
    
    @staticmethod
    def _time_to_seconds(time_str: str) -> int:
        """将HH:MM:SS转换为秒"""
        try:
            parts = time_str.split(":")
            if len(parts) == 3:
                h, m, s = parts
                return int(h) * 3600 + int(m) * 60 + int(s)
            elif len(parts) == 2:
                m, s = parts
                return int(m) * 60 + int(s)
        except Exception:
            pass
        return 0
    
    @staticmethod
    def _seconds_to_pace(seconds: float) -> str:
        """将秒数转换为MM:SS格式"""
        minutes = int(seconds // 60)
        secs = int(seconds % 60)
        return f"{minutes}:{secs:02d}"
    
    @staticmethod
    def _add_seconds_to_pace(pace: str, add_seconds: int) -> str:
        """给配速增加秒数"""
        try:
            parts = pace.split(":")
            total_seconds = int(parts[0]) * 60 + int(parts[1]) + add_seconds
            return GoalsManager._seconds_to_pace(total_seconds)
        except Exception:
            return pace
    
    # ==================== 报告生成 ====================
    
    def generate_report(self) -> Dict:
        """生成综合目标报告"""
        active = self.get_active_goals()
        completed = self.get_completed_goals()
        marathon_goals = self.get_marathon_goals()
        active_marathon = self.get_active_marathon_goals()
        
        report = {
            "generated_at": datetime.now().isoformat(),
            "basic_goals": {
                "active_count": len(active),
                "completed_count": len(completed),
                "active_goals": []
            },
            "marathon_goals": {
                "active_count": len(active_marathon),
                "total_count": len(marathon_goals),
                "active_goals": []
            },
            "summary": ""
        }
        
        # 基础目标
        for goal in active[:5]:
            progress = self.get_goal_progress(goal)
            report["basic_goals"]["active_goals"].append({
                "goal_type": goal.goal_type,
                "current": goal.current,
                "target": goal.target,
                "unit": goal.unit,
                "deadline": goal.deadline,
                **progress
            })
        
        # 马拉松目标
        for goal in active_marathon[:3]:
            report["marathon_goals"]["active_goals"].append({
                "goal_id": goal.goal_id,
                "goal_name": goal.goal_name,
                "race_date": goal.race_date,
                "race_type": goal.race_type,
                "days_until_race": goal.days_until_race,
                "training_phase": goal.training_phase,
                "goal_time": goal.goal_time,
                "target_pace": goal.target_pace
            })
        
        # 总结
        if active_marathon:
            next_race = self.get_active_marathon_goal()
            if next_race:
                report["summary"] = f"下一场比赛：{next_race.goal_name}（{next_race.days_until_race}天后）"
        else:
            report["summary"] = "暂无马拉松计划"
        
        return report


# ==================== 便捷函数 ====================

def create_goal(user_id: str, goal_type: str, target: float, current: float, 
                unit: str, deadline: str) -> Goal:
    """创建基础目标"""
    manager = GoalsManager(user_id)
    return manager.create_goal(goal_type, target, current, unit, deadline)


def create_marathon_goal(
    user_id: str,
    race_name: str,
    race_date: str,
    goal_time: str = None,
    race_type: str = "半程马拉松",
    target_pace: str = None,
    splits: Dict[str, str] = None,
    notes: str = ""
) -> MarathonGoal:
    """创建马拉松目标"""
    manager = GoalsManager(user_id)
    return manager.create_marathon_goal(
        race_name=race_name,
        race_date=race_date,
        goal_time=goal_time,
        race_type=race_type,
        target_pace=target_pace,
        splits=splits,
        notes=notes
    )


def get_goals_report(user_id: str = "default") -> Dict:
    """获取综合目标报告"""
    manager = GoalsManager(user_id)
    return manager.generate_report()


def get_marathon_goals(user_id: str = "default") -> List[MarathonGoal]:
    """获取马拉松目标列表"""
    manager = GoalsManager(user_id)
    return manager.get_marathon_goals()


def get_active_marathon_goal(user_id: str = "default") -> Optional[MarathonGoal]:
    """获取最近的马拉松目标"""
    manager = GoalsManager(user_id)
    return manager.get_active_marathon_goal()


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

if __name__ == "__main__":
    print("=== Goals Manager Test ===")
    
    user_id = "test_user"
    manager = GoalsManager(user_id)
    
    # 测试基础目标
    print("\n--- Create Basic Goal ---")
    goal = manager.create_goal(
        goal_type="增肌",
        target=80,
        current=75,
        unit="kg",
        deadline="2026-07-29"
    )
    print(f"Created: {goal.goal_type} {goal.current} → {goal.target} {goal.unit}")
    
    # 测试马拉松目标
    print("\n--- Create Marathon Goal ---")
    marathon = manager.create_marathon_goal(
        race_name="北京半马",
        race_date="2026-04-12",
        goal_time="02:00:00",
        race_type="半程马拉松"
    )
    print(f"Created: {marathon.goal_name}")
    print(f"  Race date: {marathon.race_date}")
    print(f"  Days until race: {marathon.days_until_race}")
    print(f"  Training phase: {marathon.training_phase}")
    print(f"  Target pace: {marathon.target_pace}")
    print(f"  Splits: {marathon.splits}")
    
    # 创建第二个马拉松目标
    print("\n--- Create Second Marathon Goal ---")
    marathon2 = manager.create_marathon_goal(
        race_name="通州全马",
        race_date="2026-04-19",
        goal_time="04:45:00",
        race_type="全程马拉松"
    )
    print(f"Created: {marathon2.goal_name}")
    print(f"  Days until race: {marathon2.days_until_race}")
    print(f"  Training phase: {marathon2.training_phase}")
    print(f"  Needs recovery week: {marathon2.is_recovery_week_needed}")
    
    # 获取报告
    print("\n--- Goals Report ---")
    report = manager.generate_report()
    print(f"Basic goals: {report['basic_goals']['active_count']} active")
    print(f"Marathon goals: {report['marathon_goals']['active_count']} active")
    print(f"Summary: {report['summary']}")
    
    # 测试训练阶段建议
    print("\n--- Suggested Weekly Volume ---")
    for phase in ["基础期", "巅峰期", "减量期", "比赛周", "比赛日"]:
        manager.update_marathon_goal(marathon.goal_id, race_date="2026-05-01")
        suggestion = manager.calculate_suggested_weekly_volume()
        print(f"  {phase}: {suggestion['weekly_km']}km/week, intensity: {suggestion['intensity']}")
        break
    
    # 清理测试数据
    print("\n--- Cleanup ---")
    manager.delete_goal(goal.goal_id)
    manager.delete_marathon_goal(marathon.goal_id)
    manager.delete_marathon_goal(marathon2.goal_id)
    print("Test data cleaned up")


path: lib/goals_marathon.py
content: """
Marathon Extensions for GoalsManager
Marathon training goal support
"""

import os
import json
from datetime import datetime, timedelta
from dataclasses import dataclass, asdict, field


@dataclass
class MarathonGoal:
    """马拉松目标数据类"""
    goal_id: str
    user_id: str
    goal_type: str = "马拉松训练"
    goal_name: str = ""
    race_date: str = ""
    race_type: str = "半程马拉松"
    goal_time: str = ""
    target_pace: str = ""
    splits: dict = field(default_factory=dict)
    current: float = 0
    target: float = 1
    unit: str = "场"
    status: str = "active"
    created_at: str = ""
    updated_at: str = ""
    notes: str = ""
    
    def __post_init__(self):
        if not self.created_at:
            self.created_at = datetime.now().isoformat()
        if not self.updated_at:
            self.updated_at = datetime.now().isoformat()
    
    @property
    def days_until_race(self) -> int:
        try:
            race_date_obj = datetime.strptime(self.race_date, "%Y-%m-%d").date()
            today = datetime.now().date()
            return (race_date_obj - today).days
        except Exception:
            return 0
    
    @property
    def training_phase(self) -> str:
        days = self.days_until_race
        if days < 0:
            return "已结束"
        elif days == 0:
            return "比赛日"
        elif days <= 7:
            return "比赛周"
        elif days <= 14:
            return "减量期"
        elif days <= 28:
            return "巅峰期"
        else:
            return "基础期"
    
    @property
    def is_recovery_week_needed(self) -> bool:
        if self.race_type == "全程马拉松" and self.days_until_race <= 7:
            return True
        return False
    
    def to_dict(self) -> dict:
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: dict) -> 'MarathonGoal':
        return cls(**data)


def _time_to_seconds(time_str: str) -> int:
    try:
        parts = time_str.split(":")
        if len(parts) == 3:
            h, m, s = parts
            return int(h) * 3600 + int(m) * 60 + int(s)
        elif len(parts) == 2:
            m, s = parts
            return int(m) * 60 + int(s)
    except Exception:
        pass
    return 0


def _seconds_to_pace(seconds: float) -> str:
    minutes = int(seconds // 60)
    secs = int(seconds % 60)
    return f"{minutes}:{secs:02d}"


def _add_seconds_to_pace(pace: str, add_seconds: int) -> str:
    try:
        parts = pace.split(":")
        total_seconds = int(parts[0]) * 60 + int(parts[1]) + add_seconds
        return _seconds_to_pace(total_seconds)
    except Exception:
        return pace


class MarathonGoalsManager:
    """马拉松目标管理器扩展"""
    
    def __init__(self, user_id: str = "default"):
        self.user_id = user_id
        self.goals_dir = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "data", "profiles"
        )
        os.makedirs(self.goals_dir, exist_ok=True)
        self.goals_file = os.path.join(self.goals_dir, f"marathon_goals_{user_id}.json")
    
    def _load_goals(self) -> list:
        if not os.path.exists(self.goals_file):
            return []
        try:
            with open(self.goals_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception:
            return []
    
    def _save_goals(self, goals: list) -> bool:
        try:
            with open(self.goals_file, 'w', encoding='utf-8') as f:
                json.dump(goals, f, ensure_ascii=False, indent=2)
            return True
        except Exception:
            return False
    
    def create_marathon_goal(
        self,
        race_name: str,
        race_date: str,
        goal_time: str = None,
        race_type: str = "半程马拉松",
        target_pace: str = None,
        splits: dict = None,
        notes: str = ""
    ) -> MarathonGoal:
        """创建马拉松训练目标"""
        # 计算目标配速
        if goal_time:
            total_seconds = _time_to_seconds(goal_time)
            if race_type == "半程马拉松":
                pace_seconds = total_seconds / 21.0975
                target_pace = _seconds_to_pace(pace_seconds)
            elif race_type == "全程马拉松":
                pace_seconds = total_seconds / 42.195
                target_pace = _seconds_to_pace(pace_seconds)
        
        # 计算分段配速
        if splits is None and target_pace:
            if race_type == "半程马拉松":
                splits = {
                    "前半马": target_pace,
                    "后半马": _add_seconds_to_pace(target_pace, 5)
                }
            elif race_type == "全程马拉松":
                splits = {
                    "前21km": target_pace,
                    "后21km": _add_seconds_to_pace(target_pace, 5)
                }
        
        goal = MarathonGoal(
            goal_id=f"marathon_{datetime.now().strftime('%Y%m%d%H%M%S')}",
            user_id=self.user_id,
            goal_type="马拉松训练",
            goal_name=race_name,
            race_date=race_date,
            race_type=race_type,
            goal_time=goal_time or "",
            target_pace=target_pace or "",
            splits=splits or {},
            current=0,
            target=1,
            unit="场",
            status="active",
            created_at=datetime.now().isoformat(),
            updated_at=datetime.now().isoformat(),
            notes=notes
        )
        
        goals = self._load_goals()
        goals.append(goal.to_dict())
        self._save_goals(goals)
        
        return goal
    
    def get_marathon_goals(self) -> list:
        goals = self._load_goals()
        return [MarathonGoal.from_dict(g) for g in goals]
    
    def get_active_marathon_goals(self) -> list:
        goals = self._load_goals()
        return [MarathonGoal.from_dict(g) for g in goals if g.get("status", "active") == "active"]
    
    def get_active_marathon_goal(self) -> MarathonGoal:
        active = self.get_active_marathon_goals()
        if not active:
            return None
        active.sort(key=lambda x: x.race_date)
        return active[0]
    
    def get_upcoming_marathon_goals(self, limit: int = 2) -> list:
        active = self.get_active_marathon_goals()
        upcoming = [g for g in active if g.days_until_race >= 0]
        upcoming.sort(key=lambda x: x.race_date)
        return upcoming[:limit]
    
    def update_marathon_goal(self, goal_id: str, **kwargs) -> bool:
        goals = self._load_goals()
        for i, goal in enumerate(goals):
            if goal["goal_id"] == goal_id:
                for key, value in kwargs.items():
                    if key in goal:
                        goal[key] = value
                goal["updated_at"] = datetime.now().isoformat()
                return self._save_goals(goals)
        return False
    
    def delete_marathon_goal(self, goal_id: str) -> bool:
        goals = self._load_goals()
        original_len = len(goals)
        goals = [g for g in goals if g["goal_id"] != goal_id]
        if len(goals) < original_len:
            return self._save_goals(goals)
        return False
    
    def mark_marathon_completed(self, goal_id: str) -> bool:
        return self.update_marathon_goal(goal_id, status="completed")
    
    def generate_report(self) -> dict:
        active = self.get_active_marathon_goals()
        all_goals = self.get_marathon_goals()
        
        report = {
            "generated_at": datetime.now().isoformat(),
            "active_count": len(active),
            "total_count": len(all_goals),
            "active_goals": []
        }
        
        for goal in active[:5]:
            report["active_goals"].append({
                "goal_id": goal.goal_id,
                "goal_name": goal.goal_name,
                "race_date": goal.race_date,
                "race_type": goal.race_type,
                "days_until_race": goal.days_until_race,
                "training_phase": goal.training_phase,
                "goal_time": goal.goal_time,
                "target_pace": goal.target_pace
            })
        
        return report


path: lib/health_advisor.py
content: #!/usr/bin/env python3
"""
AI Health Advisor - Intelligent health consultation
"""
import os
import json
from datetime import datetime, timedelta

# 路径配置 - 使用环境变量
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORKSPACE_DIR = os.environ.get("OPENCLAW_WORKSPACE", os.path.dirname(os.path.dirname(SKILL_DIR)))
PROCESSED_DIR = os.environ.get("WHOOP_DATA_DIR", os.path.join(WORKSPACE_DIR, "data", "processed"))

DATA_FILE = os.path.join(PROCESSED_DIR, 'latest.json')
PATTERNS_FILE = os.path.join(PROCESSED_DIR, 'patterns.json')

def load_data():
    with open(DATA_FILE) as f:
        return json.load(f)

def analyze_current_state():
    """Analyze current health state"""
    data = load_data()
    processed = data.get('processed', {})
    
    recovery = processed.get('recovery', [])
    sleep = processed.get('sleep', [])
    cycles = processed.get('cycles', [])
    workouts = processed.get('workouts', [])
    
    # Get recent 7 days
    recent_rec = recovery[:7]
    recent_sleep = sleep[:7]
    recent_cycles = cycles[:7]
    
    # Calculate averages
    rec_scores = [r.get('recovery_score', 0) for r in recent_rec if r.get('recovery_score')]
    hrv_values = [r.get('hrv', 0) for r in recent_rec if r.get('hrv')]
    rhr_values = [r.get('rhr', 0) for r in recent_rec if r.get('rhr')]
    
    sleep_hours = [s.get('total_in_bed_hours', 0) for s in recent_sleep if s.get('total_in_bed_hours')]
    sleep_perf = [s.get('sleep_performance', 0) for s in recent_sleep if s.get('sleep_performance')]
    
    strain_values = [c.get('strain', 0) for c in recent_cycles if c.get('strain')]
    
    return {
        "recovery": {
            "avg": sum(rec_scores)/len(rec_scores) if rec_scores else 0,
            "current": rec_scores[0] if rec_scores else 0,
            "hrv": sum(hrv_values)/len(hrv_values) if hrv_values else 0,
            "rhr": sum(rhr_values)/len(rhr_values) if rhr_values else 0,
            "trend": "up" if len(rec_scores) > 1 and rec_scores[0] > rec_scores[1] else "down"
        },
        "sleep": {
            "avg_hours": sum(sleep_hours)/len(sleep_hours) if sleep_hours else 0,
            "avg_perf": sum(sleep_perf)/len(sleep_perf) if sleep_perf else 0,
            "sleep_debt": max(0, 7*8 - sum(sleep_hours))
        },
        "training": {
            "avg_strain": sum(strain_values)/len(strain_values) if strain_values else 0,
            "workout_count": len(workouts)
        }
    }

def get_training_recommendation(state):
    """Get training recommendation based on recovery and sleep"""
    rec = state["recovery"]["avg"]
    sleep_hours = state["sleep"]["avg_hours"]
    hrv = state["recovery"]["hrv"]
    
    if rec >= 70 and sleep_hours >= 7:
        return {
            "level": "high",
            "message": "状态良好，可以进行正常强度训练",
            "activities": ["跑步 10-15km", "力量训练", "HIIT"],
            "emoji": "💪"
        }
    elif rec >= 50 and sleep_hours >= 6:
        return {
            "level": "medium",
            "message": "状态一般，建议低强度活动",
            "activities": ["轻松跑步 5-8km", "瑜伽", "拉伸"],
            "emoji": "🟡"
        }
    elif rec >= 30:
        return {
            "level": "low",
            "message": "需要休息，建议轻度活动",
            "activities": ["散步", "轻度拉伸"],
            "emoji": "🔴"
        }
    else:
        return {
            "level": "rest",
            "message": "身体需要恢复，建议完全休息",
            "activities": ["充足睡眠", "避免运动"],
            "emoji": "😴"
        }

def get_sleep_advice(state):
    """Get personalized sleep advice"""
    sleep_hours = state["sleep"]["avg_hours"]
    sleep_debt = state["sleep"]["sleep_debt"]
    sleep_perf = state["sleep"]["avg_perf"]
    
    advice = []
    
    if sleep_debt > 10:
        advice.append("🔴 严重睡眠债务: 建议连续2-3天8小时睡眠")
    elif sleep_debt > 5:
        advice.append("🟡 存在睡眠债务: 建议今晚提前1小时入睡")
    
    if sleep_hours < 6:
        advice.append("🟡 睡眠时长不足: 目标每晚7-8小时")
    
    if sleep_perf < 70:
        advice.append("🟡 睡眠质量欠佳: 睡前1小时远离电子设备")
    
    if not advice:
        advice.append("✅ 睡眠状态良好，保持当前作息")
    
    return advice

def generate_health_report():
    """Generate comprehensive health report"""
    state = analyze_current_state()
    
    # Get recommendations
    training_rec = get_training_recommendation(state)
    sleep_advice = get_sleep_advice(state)
    
    # Determine overall health score
    rec_score = state["recovery"]["avg"]
    sleep_score = state["sleep"]["avg_perf"]
    hrv = state["recovery"]["hrv"]
    
    # Calculate composite score
    if hrv >= 50 and rec_score >= 70:
        overall = 90
    elif hrv >= 40 and rec_score >= 60:
        overall = 75
    elif rec_score >= 50:
        overall = 60
    else:
        overall = 40
    
    report = {
        "generated_at": datetime.now().isoformat(),
        "overall_score": overall,
        "state": state,
        "training_recommendation": training_rec,
        "sleep_advice": sleep_advice,
        "insights": generate_insights(state)
    }
    
    # Save
    with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'processed', 'health_advisor.json'), 'w') as f:
        json.dump(report, f, indent=2, ensure_ascii=False)
    
    return report

def generate_insights(state):
    """Generate personalized insights"""
    insights = []
    
    # Recovery insights
    rec = state["recovery"]["avg"]
    if rec < 50:
        insights.append(f"恢复状态偏低({rec:.0f}%)，注意身体休息")
    elif rec > 70:
        insights.append(f"恢复状态良好({rec:.0f}%)，可以保持当前训练")
    
    # HRV insights
    hrv = state["recovery"]["hrv"]
    if hrv < 30:
        insights.append(f"HRV偏低({hrv:.0f}ms)，建议减少压力")
    elif hrv > 50:
        insights.append(f"HRV优秀({hrv:.0f}ms)，心肺功能良好")
    
    # Training insights
    strain = state["training"]["avg_strain"]
    if strain > 15:
        insights.append(f"近期训练强度偏高(strain {strain:.1f})，注意轮休")
    elif strain < 5:
        insights.append("活动量较低，可适当增加运动")
    
    return insights

def ask_question(question):
    """Answer health questions"""
    report = generate_health_report()
    state = report["state"]
    
    q = question.lower()
    
    # Recovery related
    if "恢复" in q or "recovery" in q:
        if "今天" in q or "现在" in q or "current" in q:
            rec = state["recovery"]["current"]
            return f"今日恢复: {rec:.0f}%"
        return f"近期平均恢复: {state['recovery']['avg']:.0f}%"
    
    # Training related
    if "训练" in q or "运动" in q or "跑" in q:
        rec = get_training_recommendation(state)
        return f"{rec['emoji']} {rec['message']} - 推荐: {', '.join(rec['activities'])}"
    
    # Sleep related
    if "睡眠" in q or "sleep" in q:
        return f"平均睡眠: {state['sleep']['avg_hours']:.1f}h, 睡眠债务: {state['sleep']['sleep_debt']:.1f}h"
    
    # HRV related
    if "hrv" in q:
        return f"平均HRV: {state['recovery']['hrv']:.1f}ms"
    
    # Overall
    if "怎么样" in q or "如何" in q or "how" in q:
        return f"综合健康分: {report['overall_score']}/100\n" + "\n".join(report["insights"])
    
    return "抱歉，我不太明白。你可以直接问:恢复、训练、睡眠、HRV等"

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        question = " ".join(sys.argv[1:])
        answer = ask_question(question)
        print(f"Q: {question}")
        print(f"A: {answer}")
    else:
        report = generate_health_report()
        print(f"综合健康分: {report['overall_score']}/100")
        print(f"训练建议: {report['training_recommendation']['message']}")


path: lib/health_score.py
content: #!/usr/bin/env python3
"""
Health Score System - For Whoop Guru Skill
"""
import os
import json

# 路径配置 - 使用环境变量
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORKSPACE_DIR = os.environ.get("OPENCLAW_WORKSPACE", os.path.dirname(os.path.dirname(SKILL_DIR)))
PROCESSED_DIR = os.environ.get("WHOOP_DATA_DIR", os.path.join(WORKSPACE_DIR, "data", "processed"))

DATA_FILE = os.environ.get('WHOOP_DATA_DIR', os.path.join(PROCESSED_DIR, 'latest.json'))
OUTPUT_FILE = os.path.join(PROCESSED_DIR, 'health_score.json')

def calculate_health_score():
    try:
        with open(DATA_FILE) as f:
            data = json.load(f)
    except:
        return {"score": 0, "grade": "N/A"}
    
    processed = data.get('processed', {})
    recovery = processed.get('recovery', [])
    sleep = processed.get('sleep', [])
    
    recent_rec = [r.get('recovery_score', 0) for r in recovery[:7] if r.get('recovery_score')]
    recent_sleep = [s.get('sleep_performance', 0) for s in sleep[:7] if s.get('sleep_performance')]
    recent_hrv = [r.get('hrv', 0) for r in recovery[:7] if r.get('hrv')]
    recent_rhr = [r.get('rhr', 0) for r in recovery[:7] if r.get('rhr')]
    
    if not recent_rec:
        return {"score": 0, "grade": "N/A"}
    
    rec_score = sum(recent_rec) / len(recent_rec)
    sleep_score = sum(recent_sleep) / len(recent_sleep) if recent_sleep else 50
    avg_hrv = sum(recent_hrv) / len(recent_hrv) if recent_hrv else 40
    avg_rhr = sum(recent_rhr) / len(recent_rhr) if recent_rhr else 60
    
    hrv_score = min(100, avg_hrv * 2)
    rhr_score = max(0, 100 - (avg_rhr - 50) * 5)
    
    total_score = rec_score * 0.4 + sleep_score * 0.3 + hrv_score * 0.2 + rhr_score * 0.1
    
    if total_score >= 90: grade = "A+"
    elif total_score >= 85: grade = "A"
    elif total_score >= 80: grade = "A-"
    elif total_score >= 70: grade = "B"
    elif total_score >= 60: grade = "C"
    else: grade = "D"
    
    result = {
        "score": round(total_score, 1),
        "grade": grade,
        "breakdown": {
            "recovery": round(rec_score, 1),
            "sleep": round(sleep_score, 1),
            "hrv": round(hrv_score, 1),
            "rhr": round(rhr_score, 1)
        }
    }
    
    try:
        with open(OUTPUT_FILE, 'w') as f:
            json.dump(result, f, indent=2)
    except:
        pass
    
    return result

if __name__ == "__main__":
    result = calculate_health_score()
    print(f"综合健康分: {result['score']}/100 ({result['grade']})")
    print(f"恢复: {result['breakdown']['recovery']} (40%)")
    print(f"睡眠: {result['breakdown']['sleep']} (30%)")
    print(f"HRV: {result['breakdown']['hrv']} (20%)")
    print(f"心率: {result['breakdown']['rhr']} (10%)")


path: lib/llm.py
content: """
LLM API 集成模块
支持多种模型提供商和用户自定义配置

支持的提供商：
- MiniMax (默认)
- OpenAI (GPT-4, GPT-3.5)
- Claude (Anthropic)
- 智谱AI (GLM)
- 通义千问
- DeepSeek
- Kimi (Moonshot)
- 硅基流动
- 任何 OpenAI 兼容 API

用户配置流程：
1. 选择提供商
2. 输入API Key
3. （可选）选择模型
4. 测试连接
"""

import os
import json
import requests
from typing import Dict, List, Optional, Any
from datetime import datetime
from dataclasses import dataclass

# 路径配置
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CONFIG_FILE = os.path.join(SKILL_DIR, "data", "config", "llm_config.json")


# 支持的提供商配置
PROVIDERS: Dict[str, Dict] = {
    "minimax": {
        "name": "MiniMax",
        "name_cn": "MiniMax",
        "default_model": "abab6.5s-chat",
        "api_url": "https://api.minimax.chat/v1/text/chatcompletion_pro",
        "api_key_hint": "在 MiniMax 控制台获取 API Key",
        "models": [
            {"id": "abab6.5s-chat", "name": "ABAB 6.5S (默认)", "desc": "最新最强模型"},
            {"id": "abab6.5-chat", "name": "ABAB 6.5", "desc": "标准版"},
            {"id": "abab5.5s-chat", "name": "ABAB 5.5S", "desc": "轻量版"},
        ],
        "official_url": "https://platform.minimaxi.com"
    },
    "openai": {
        "name": "OpenAI",
        "name_cn": "OpenAI",
        "default_model": "gpt-4",
        "api_url": "https://api.openai.com/v1/chat/completions",
        "api_key_hint": "在 OpenAI 控制台获取 API Key",
        "models": [
            {"id": "gpt-4-turbo-preview", "name": "GPT-4 Turbo (推荐)", "desc": "速度快，性能强"},
            {"id": "gpt-4", "name": "GPT-4", "desc": "标准版"},
            {"id": "gpt-3.5-turbo", "name": "GPT-3.5 Turbo", "desc": "快速便宜"},
        ],
        "official_url": "https://platform.openai.com"
    },
    "claude": {
        "name": "Claude",
        "name_cn": "Claude",
        "default_model": "claude-3-opus-20240229",
        "api_url": "https://api.anthropic.com/v1/messages",
        "api_key_hint": "在 Anthropic 控制台获取 API Key",
        "models": [
            {"id": "claude-3-opus-20240229", "name": "Claude 3 Opus (最强)", "desc": "最强大模型"},
            {"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet", "desc": "性价比高"},
            {"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku", "desc": "最快最便宜"},
        ],
        "official_url": "https://console.anthropic.com"
    },
    "glm": {
        "name": "GLM",
        "name_cn": "智谱AI (GLM)",
        "default_model": "glm-4",
        "api_url": "https://open.bigmodel.cn/api/paas/v4/chat/completions",
        "api_key_hint": "在 智谱AI 控制台获取 API Key",
        "models": [
            {"id": "glm-4", "name": "GLM-4 (推荐)", "desc": "最新最强模型"},
            {"id": "glm-4-flash", "name": "GLM-4 Flash", "desc": "快速便宜"},
            {"id": "glm-3-turbo", "name": "GLM-3 Turbo", "desc": "标准版"},
        ],
        "official_url": "https://open.bigmodel.cn"
    },
    "kimi": {
        "name": "Kimi",
        "name_cn": "Kimi (Moonshot)",
        "default_model": "moonshot-v1-8k",
        "api_url": "https://api.moonshot.cn/v1/chat/completions",
        "api_key_hint": "在 Kimi 控制台获取 API Key",
        "models": [
            {"id": "moonshot-v1-8k", "name": "Moonshot V1 8K", "desc": "默认上下文8K"},
            {"id": "moonshot-v1-32k", "name": "Moonshot V1 32K", "desc": "长上下文32K"},
            {"id": "moonshot-v1-128k", "name": "Moonshot V1 128K", "desc": "超长上下文128K"},
        ],
        "official_url": "https://platform.moonshot.cn"
    },
    "qwen": {
        "name": "Qwen",
        "name_cn": "通义千问",
        "default_model": "qwen-turbo",
        "api_url": "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
        "api_key_hint": "在 阿里云百炼 获取 API Key",
        "models": [
            {"id": "qwen-turbo", "name": "Qwen Turbo (快速)", "desc": "快速响应"},
            {"id": "qwen-plus", "name": "Qwen Plus (推荐)", "desc": "性价比高"},
            {"id": "qwen-max", "name": "Qwen Max", "desc": "最强模型"},
        ],
        "official_url": "https://dashscope.console.aliyun.com"
    },
    "deepseek": {
        "name": "DeepSeek",
        "name_cn": "DeepSeek",
        "default_model": "deepseek-chat",
        "api_url": "https://api.deepseek.com/v1/chat/completions",
        "api_key_hint": "在 DeepSeek 控制台获取 API Key",
        "models": [
            {"id": "deepseek-chat", "name": "DeepSeek Chat", "desc": "通用对话"},
            {"id": "deepseek-coder", "name": "DeepSeek Coder", "desc": "代码专用"},
        ],
        "official_url": "https://platform.deepseek.com"
    },
    "siliconflow": {
        "name": "SiliconFlow",
        "name_cn": "硅基流动",
        "default_model": "Qwen/Qwen2.5-7B-Instruct",
        "api_url": "https://api.siliconflow.cn/v1/chat/completions",
        "api_key_hint": "在 硅基流动 获取 API Key (有免费额度)",
        "models": [
            {"id": "Qwen/Qwen2.5-7B-Instruct", "name": "Qwen2.5 7B (推荐)", "desc": "免费可用"},
            {"id": "deepseek-ai/DeepSeek-V2.5", "name": "DeepSeek V2.5", "desc": "强大开源"},
            {"id": "THUDM/glm-4-9b-chat", "name": "GLM-4 9B", "desc": "国产开源"},
        ],
        "official_url": "https://cloud.siliconflow.cn"
    },
    "custom": {
        "name": "Custom",
        "name_cn": "自定义 API",
        "default_model": "",
        "api_url": "",
        "api_key_hint": "输入你的 API Key",
        "models": [],
        "official_url": ""
    }
}


@dataclass
class LLMConfig:
    """LLM配置"""
    provider: str = "minimax"
    api_key: str = ""
    api_url: str = ""
    model: str = ""
    temperature: float = 0.7
    max_tokens: int = 4096


class LLMClient:
    """
    LLM API 客户端
    
    支持多种提供商，每个用户独立配置
    """
    
    def __init__(self, user_id: str = "default"):
        self.user_id = user_id
        self.config = self._load_config()
    
    def _load_config(self) -> LLMConfig:
        """加载用户配置"""
        config = LLMConfig()
        
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r') as f:
                    all_configs = json.load(f)
                    if self.user_id in all_configs:
                        cfg = all_configs[self.user_id]
                        config.provider = cfg.get("provider", "minimax")
                        config.[REDACTED_SECRET]"api_key", "")
                        config.api_url = cfg.get("api_url", "")
                        config.model = cfg.get("model", "")
                        config.temperature = cfg.get("temperature", 0.7)
                        config.max_tokens = cfg.get("max_tokens", 4096)
            except Exception:
                pass
        
        # 设置默认值
        if not config.model or config.provider not in PROVIDERS:
            config.provider = "minimax"
            config.model = PROVIDERS["minimax"]["default_model"]
            config.api_url = PROVIDERS["minimax"]["api_url"]
        
        return config
    
    def _save_config(self) -> bool:
        """保存用户配置"""
        os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
        
        all_configs = {}
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r') as f:
                    all_configs = json.load(f)
            except Exception:
                pass
        
        all_configs[self.user_id] = {
            "provider": self.config.provider,
            "api_key": self.config.api_key,
            "api_url": self.config.api_url,
            "model": self.config.model,
            "temperature": self.config.temperature,
            "max_tokens": self.config.max_tokens
        }
        
        try:
            with open(CONFIG_FILE, 'w') as f:
                json.dump(all_configs, f, ensure_ascii=False, indent=2)
            return True
        except Exception:
            return False
    
    def configure(
        self,
        provider: str,
        api_key: str,
        model: Optional[str] = None,
        api_url: Optional[str] = None
    ) -> bool:
        """
        配置用户LLM设置
        
        Args:
            provider: 提供商ID
            api_key: API密钥
            model: 模型名称 (可选)
            api_url: API地址 (可选)
        
        Returns:
            是否配置成功
        """
        if provider not in PROVIDERS:
            return False
        
        self.config.provider = provider
        self.config.[REDACTED_SECRET]
        self.config.api_url = api_url or PROVIDERS[provider]["api_url"]
        self.config.model = model or PROVIDERS[provider]["default_model"]
        
        return self._save_config()
    
    def configure_by_model_name(self, model_name: str, api_key: str) -> bool:
        """
        通过模型名称自动识别提供商并配置
        
        Args:
            model_name: 模型名称 (如 "gpt-4", "glm-4", "kimi")
            api_key: API密钥
        
        Returns:
            是否配置成功
        """
        # 自动识别提供商
        model_lower = model_name.lower()
        
        if "claude" in model_lower:
            provider = "claude"
        elif "gpt" in model_lower or "openai" in model_lower:
            provider = "openai"
        elif "glm" in model_lower:
            provider = "glm"
        elif "kimi" in model_lower or "moonshot" in model_lower:
            provider = "kimi"
        elif "qwen" in model_lower or "通义" in model_lower:
            provider = "qwen"
        elif "deepseek" in model_lower:
            provider = "deepseek"
        elif "minimax" in model_lower or "abab" in model_lower:
            provider = "minimax"
        else:
            # 尝试在所有提供商的模型中查找
            provider = None
            for p_id, p_info in PROVIDERS.items():
                for m in p_info.get("models", []):
                    if model_name.lower() in m["id"].lower():
                        provider = p_id
                        break
                if provider:
                    break
            
            if not provider:
                return False
        
        return self.configure(provider, api_key, model_name)
    
    def test_connection(self) -> Dict:
        """
        测试API连接
        
        Returns:
            {"success": True, "message": "..."} 或 {"success": False, "error": "..."}
        """
        if not self.config.[REDACTED_SECRET] {
                "success": False,
                "error": "请先配置 API Key"
            }
        
        # 简单的连接测试
        result = self.chat([
            {"role": "user", "content": "Hi"}
        ], max_tokens=10)
        
        if "error" in result:
            return {
                "success": False,
                "error": result["error"]
            }
        
        return {
            "success": True,
            "message": f"连接 {self.get_provider_info()['name']} 成功！"
        }
    
    def get_info(self) -> Dict:
        """获取当前配置信息"""
        provider_info = PROVIDERS.get(self.config.provider, {})
        return {
            "provider": self.config.provider,
            "name": provider_info.get("name_cn", provider_info.get("name", "未知")),
            "model": self.config.model,
            "model_name": self._get_model_display_name(),
            "api_url": self.config.api_url,
            "has_api_key": bool(self.config.api_key),
            "is_configured": bool(self.config.api_key)
        }
    
    def _get_model_display_name(self) -> str:
        """获取模型显示名称"""
        provider_info = PROVIDERS.get(self.config.provider, {})
        for m in provider_info.get("models", []):
            if m["id"] == self.config.model:
                return m["name"]
        return self.config.model
    
    def get_provider_info(self) -> Dict:
        """获取提供商信息"""
        return PROVIDERS.get(self.config.provider, {})
    
    def chat(self, messages: List[Dict], **kwargs) -> Dict:
        """发送对话请求"""
        if not self.config.[REDACTED_SECRET] {"error": "请先配置 API Key，使用【设置模型】命令"}
        
        temperature = kwargs.get("temperature", self.config.temperature)
        max_tokens = kwargs.get("max_tokens", self.config.max_tokens)
        
        if self.config.provider == "minimax":
            return self._chat_minimax(messages, temperature, max_tokens)
        elif self.config.provider == "openai":
            return self._chat_openai(messages, temperature, max_tokens)
        elif self.config.provider == "claude":
            return self._chat_claude(messages, temperature, max_tokens)
        elif self.config.provider == "glm":
            return self._chat_glm(messages, temperature, max_tokens)
        elif self.config.provider == "kimi":
            return self._chat_kimi(messages, temperature, max_tokens)
        elif self.config.provider == "qwen":
            return self._chat_qwen(messages, temperature, max_tokens)
        elif self.config.provider == "deepseek":
            return self._chat_deepseek(messages, temperature, max_tokens)
        elif self.config.provider == "siliconflow":
            return self._chat_siliconflow(messages, temperature, max_tokens)
        elif self.config.provider == "custom":
            return self._chat_custom(messages, temperature, max_tokens)
        else:
            return self._chat_openai(messages, temperature, max_tokens)
    
    def _chat_minimax(self, messages, temperature, max_tokens):
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json"
        }
        data = {
            "model": self.config.model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens
        }
        return self._post_request(self.config.api_url, headers, data)
    
    def _chat_openai(self, messages, temperature, max_tokens):
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json"
        }
        data = {
            "model": self.config.model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens
        }
        return self._post_request(self.config.api_url, headers, data)
    
    def _chat_claude(self, messages, temperature, max_tokens):
        headers = {
            "x-api-key": self.config.api_key,
            "anthropic-version": "2023-06-01",
            "Content-Type": "application/json"
        }
        # Claude使用不同的消息格式
        claude_messages = []
        for msg in messages:
            role = "user" if msg.get("role") == "user" else "assistant"
            claude_messages.append({"role": role, "content": msg.get("content", "")})
        
        data = {
            "model": self.config.model,
            "messages": claude_messages,
            "temperature": temperature,
            "max_tokens": max_tokens
        }
        
        try:
            response = requests.post(
                self.config.api_url,
                headers=headers,
                json=data,
                timeout=60
            )
            result = response.json()
            
            if "error" in result:
                return result
            
            if "content" in result:
                content = result["content"]
                if isinstance(content, list) and len(content) > 0:
                    return {"content": content[0].get("text", ""), "usage": result.get("usage", {})}
                return {"content": str(content)}
            return {"error": "无法解析响应"}
        except Exception as e:
            return {"error": str(e)}
    
    def _chat_glm(self, messages, temperature, max_tokens):
        """智谱AI GLM"""
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json"
        }
        data = {
            "model": self.config.model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens
        }
        return self._post_request(self.config.api_url, headers, data)
    
    def _chat_kimi(self, messages, temperature, max_tokens):
        """Kimi / Moonshot"""
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json"
        }
        data = {
            "model": self.config.model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens
        }
        return self._post_request(self.config.api_url, headers, data)
    
    def _chat_qwen(self, messages, temperature, max_tokens):
        """通义千问"""
        headers = {
            "Authorization": f"Bearer {self.config.api_key}",
            "Content-Type": "application/json"
        }
        data = {
            "model": self.config.model,
            "input": {"messages": messages},
            "parameters": {
                "temperature": temperature,
                "max_tokens": max_tokens
            }
        }
        try:
            response = requests.post(
                self.config.api_url,
                headers=headers,
                json=data,
                timeout=60
            )
            result = response.json()
            if "error" in result:
                return result
            output = result.get("output", {})
            if "text" in output:
                return {"content": output["text"], "usage": result.get("usage", {})}
            return {"error": "无法解析响应"}
        except Exception as e:
            return {"error": str(e)}
    
    def _chat_deepseek(self, messages, temperature, max_tokens):
        """DeepSeek"""
        return self._chat_openai(messages, temperature, max_tokens)
    
    def _chat_siliconflow(self, messages, temperature, max_tokens):
        """硅基流动"""
        return self._chat_openai(messages, temperature, max_tokens)
    
    def _chat_custom(self, messages, temperature, max_tokens):
        """自定义API"""
        return self._chat_openai(messages, temperature, max_tokens)
    
    def _post_request(self, url, headers, data):
        """通用的POST请求"""
        try:
            response = requests.post(url, headers=headers, json=data, timeout=60)
            result = response.json()
            
            if "error" in result:
                return result
            
            choices = result.get("choices", [])
            if choices:
                return {
                    "content": choices[0].get("message", {}).get("content", ""),
                    "usage": result.get("usage", {})
                }
            return {"error": "无法解析响应"}
        except requests.exceptions.Timeout:
            return {"error": "请求超时，请检查网络"}
        except Exception as e:
            return {"error": str(e)}
    
    def generate(self, prompt: str, **kwargs) -> str:
        """生成内容的便捷方法"""
        result = self.chat([{"role": "user", "content": prompt}], **kwargs)
        if "error" in result:
            return f"[错误] {result['error']}"
        return result.get("content", "")


class LLMSetupWizard:
    """
    LLM设置向导 - 帮助小白用户配置API
    """
    
    def __init__(self, user_id: str = "default"):
        self.user_id = user_id
        self.client = LLMClient(user_id)
    
    def get_welcome_message(self) -> str:
        """获取欢迎消息"""
        return """
🔧 **LLM 模型设置向导**

欢迎使用！我是你的健身教练 AI。

为了让教练能够为你制定个性化的训练计划，我需要连接一个大语言模型。

请告诉我你想使用哪个模型：

**常用推荐：**

1️⃣ **MiniMax** - 默认推荐，中文理解好
2️⃣ **OpenAI GPT-4** - 最强模型，需要魔法
3️⃣ **智谱 GLM** - 国产优秀，免费额度多
4️⃣ **Kimi** - 中文友好，支持长文本
5️⃣ **硅基流动** - 免费额度，性价比高

**其他选择：**
6️⃣ 通义千问 (Qwen)
7️⃣ DeepSeek
8️⃣ Claude

**回复示例：**
- "1" 或 "minimax"
- "gpt-4"
- "glm-4"
- "kimi"
- 或者直接告诉我你想要的模型名称

如果你已经有 API Key，可以直接发送给我，格式：
`API Key: sk-xxxxx`
`模型: gpt-4`

⚠️ 你的 API Key 仅存储在本地，不会上传到任何服务器。
"""
    
    def handle_setup(self, user_input: str) -> Dict:
        """
        处理用户输入
        
        Args:
            user_input: 用户输入
        
        Returns:
            {"next_step": "model_select"/"api_key"/"done", "message": "...", "data": {...}}
        """
        user_input = user_input.strip()
        
        # 检查是否是直接发送API Key
        if "sk-" in user_input or ":" in user_input:
            return self._parse_direct_api_key(user_input)
        
        # 数字映射
        number_map = {
            "1": "minimax",
            "2": "openai",
            "3": "glm",
            "4": "kimi",
            "5": "siliconflow",
            "6": "qwen",
            "7": "deepseek",
            "8": "claude"
        }
        if user_input in number_map:
            return self._show_provider_setup(number_map[user_input])
        
        # 关键词匹配
        keyword_to_provider = {
            "minimax": "minimax", "abab": "minimax",
            "openai": "openai", "gpt": "openai",
            "claude": "claude", "anthropic": "claude",
            "glm": "glm", "智谱": "glm", "zhipu": "glm",
            "kimi": "kimi", "moonshot": "kimi",
            "qwen": "qwen", "通义": "qwen", "阿里": "qwen",
            "deepseek": "deepseek",
            "siliconflow": "siliconflow", "硅基": "siliconflow"
        }
        
        user_lower = user_input.lower()
        for keyword, provider_id in keyword_to_provider.items():
            if keyword in user_lower:
                return self._show_provider_setup(provider_id)
        
        # 检查是否是模型名称（如 gpt-4, glm-4 等）
        model_keywords = ["gpt", "glm", "kimi", "claude", "qwen", "deepseek", "minimax", "abab", "moonshot"]
        if any(m in user_lower for m in model_keywords):
            self.client.configure_by_model_name(user_input, "")
            return {
                "next_step": "api_key",
                "message": f"好的，你选择了 {self.client.get_info()['model']}。\n\n请输入你的 API Key：\n{self.client.get_provider_info().get('api_key_hint', '')}",
                "data": {"provider": self.client.config.provider, "model": self.client.config.model}
            }
        
        # 默认显示选择
        return {
            "next_step": "provider_select",
            "message": self.get_welcome_message(),
            "data": {}
        }
    
    def _parse_direct_api_key(self, user_input: str) -> Dict:
        """解析直接的API Key输入"""
        api_key = ""
        model = ""
        
        # 解析格式
        if ":" in user_input:
            parts = user_input.split(":", 1)
            for part in parts:
                part = part.strip()
                if part.startswith("sk-"):
                    api_key = part
                elif part and not model:
                    model = part
        else:
            [REDACTED_SECRET])
        
        if api_key:
            # 尝试自动识别模型
            if model:
                success = self.client.configure_by_model_name(model, api_key)
            else:
                # 使用当前配置
                success = bool(self.client.config.api_key)
                if success:
                    self.client.config.[REDACTED_SECRET]
                    self.client._save_config()
            
            if success:
                # 测试连接
                test_result = self.client.test_connection()
                if test_result["success"]:
                    return {
                        "next_step": "done",
                        "message": f"✅ {test_result['message']}\n\n当前配置：\n- 提供商：{self.client.get_info()['name']}\n- 模型：{self.client.get_info()['model_name']}\n\n现在我可以为你制定训练计划了！",
                        "data": self.client.get_info()
                    }
                else:
                    return {
                        "next_step": "api_key",
                        "message": f"⚠️ API Key 已保存，但连接测试失败：{test_result['error']}\n\n请检查：\n1. API Key 是否正确\n2. 是否有足够的额度\n\n或者尝试其他模型？",
                        "data": self.client.get_info()
                    }
        
        return {
            "next_step": "api_key",
            "message": "请输入有效的 API Key",
            "data": {}
        }
    
    def _show_provider_setup(self, provider_id: str) -> Dict:
        """显示提供商设置"""
        provider = PROVIDERS.get(provider_id, {})
        
        models_text = ""
        if provider.get("models"):
            models_text = "\n可选模型：\n"
            for i, m in enumerate(provider["models"], 1):
                models_text += f"  {i}. {m['name']} - {m['desc']}\n"
        
        return {
            "next_step": "api_key",
            "message": f"""✅ 你选择了 **{provider.get('name_cn', provider.get('name'))}**

{models_text}

📝 请输入你的 API Key：
{provider.get('api_key_hint', '')}

💡 获取地址：{provider.get('official_url', '')}

直接发送 API Key 即可，例如：
`sk-xxxxxx`""",
            "data": {"provider": provider_id, "default_model": provider.get("default_model")}
        }
    
    def set_api_key(self, api_key: str) -> Dict:
        """设置API Key"""
        self.client.config.[REDACTED_SECRET]
        self.client._save_config()
        
        # 测试连接
        test_result = self.client.test_connection()
        if test_result["success"]:
            return {
                "success": True,
                "message": f"✅ 配置成功！\n\n当前设置：\n- 提供商：{self.client.get_info()['name']}\n- 模型：{self.client.get_info()['model_name']}\n\n可以开始使用了！",
                "data": self.client.get_info()
            }
        else:
            return {
                "success": False,
                "error": test_result.get("error", "连接失败"),
                "message": f"⚠️ API Key 已保存，但连接失败：{test_result.get('error')}\n\n请检查：\n1. API Key 是否正确\n2. 是否有足够的额度"
            }
    
    def get_current_status(self) -> Dict:
        """获取当前状态"""
        info = self.client.get_info()
        if info["is_configured"]:
            return {
                "is_configured": True,
                "message": f"""✅ LLM 已配置完成

- 提供商：{info['name']}
- 模型：{info['model_name']}

可以直接使用教练功能！""",
                "data": info
            }
        else:
            return {
                "is_configured": False,
                "message": self.get_welcome_message(),
                "data": info
            }


def get_llm_client(user_id: str = "default") -> LLMClient:
    """获取LLM客户端"""
    return LLMClient(user_id)


def get_setup_wizard(user_id: str = "default") -> LLMSetupWizard:
    """获取设置向导"""
    return LLMSetupWizard(user_id)


if __name__ == "__main__":
    # 测试
    wizard = get_setup_wizard("test")
    print(wizard.get_welcome_message())


path: lib/marathon_analyzer.py
content: """
Marathon Analyzer - 马拉松训练分析器
根据训练阶段和恢复数据给出综合建议
"""

import os
import sys
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple

# 添加父目录到路径
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, SKILL_DIR)

from lib.goals import GoalsManager, MarathonGoal
from lib.tracker import ProgressTracker


class MarathonAnalyzer:
    """
    马拉松训练分析器
    根据马拉松目标和训练数据给出综合建议
    """
    
    # 训练阶段配置
    TRAINING_PHASES = {
        "基础期": {
            "days_min": 29,
            "days_max": float('inf'),
            "weekly_volume_ratio": 1.0,
            "long_run_ratio": 0.3,
            "intensity": "低",
            "description": "积累跑量，打好有氧基础"
        },
        "巅峰期": {
            "days_min": 14,
            "days_max": 28,
            "weekly_volume_ratio": 0.9,
            "long_run_ratio": 0.25,
            "intensity": "中高",
            "description": "提高强度，引入节奏跑和间歇训练"
        },
        "减量期": {
            "days_min": 8,
            "days_max": 14,
            "weekly_volume_ratio": 0.6,
            "long_run_ratio": 0.2,
            "intensity": "中低",
            "description": "减量保持状态，避免过度训练"
        },
        "比赛周": {
            "days_min": 1,
            "days_max": 7,
            "weekly_volume_ratio": 0.3,
            "long_run_ratio": 0.1,
            "intensity": "极低",
            "description": "充分休息，保持活力"
        },
        "比赛日": {
            "days_min": 0,
            "days_max": 0,
            "weekly_volume_ratio": 0,
            "long_run_ratio": 0,
            "intensity": "比赛",
            "description": "全力以赴！"
        }
    }
    
    # 心率区间定义
    HEART_RATE_ZONES = {
        "Z1": {"min": 0.5, "max": 0.6, "name": "恢复区", "description": "非常轻松"},
        "Z2": {"min": 0.6, "max": 0.7, "name": "有氧区", "description": "轻松聊天"},
        "Z3": {"min": 0.7, "max": 0.8, "name": "Tempo区", "description": "有点吃力"},
        "Z4": {"min": 0.8, "max": 0.9, "name": "乳酸阈值区", "description": "吃力"},
        "Z5": {"min": 0.9, "max": 1.0, "name": "无氧区", "description": "非常吃力"}
    }
    
    def __init__(self, user_id: str = "default"):
        self.user_id = user_id
        self.goals_manager = GoalsManager(user_id)
        self.tracker = ProgressTracker(user_id)
    
    def get_training_phase_config(self, phase: str) -> Dict:
        """获取训练阶段配置"""
        return self.TRAINING_PHASES.get(phase, self.TRAINING_PHASES["基础期"])
    
    def get_phase_from_days(self, days: int) -> str:
        """根据距离天数返回训练阶段"""
        if days < 0:
            return "已结束"
        elif days == 0:
            return "比赛日"
        elif days <= 7:
            return "比赛周"
        elif days <= 14:
            return "减量期"
        elif days <= 28:
            return "巅峰期"
        else:
            return "基础期"
    
    def get_training_advice(
        self,
        user_id: str,
        whoop_data: Dict = None
    ) -> Dict:
        """
        综合分析生成训练建议
        
        Args:
            user_id: 用户ID
            whoop_data: WHOOP数据（恢复、HRV等）
        
        Returns:
            {
                "has_marathon_plan": bool,
                "marathon_info": {...},
                "today_suggestion": {...},
                "warnings": [...],
                "race_strategy": {...}
            }
        """
        result = {
            "has_marathon_plan": False,
            "marathon_info": {},
            "today_suggestion": {},
            "warnings": [],
            "race_strategy": {}
        }
        
        # 获取活跃马拉松目标
        marathon_goal = self.goals_manager.get_active_marathon_goal()
        
        if not marathon_goal:
            result["today_suggestion"] = self._get_default_suggestion()
            return result
        
        result["has_marathon_plan"] = True
        
        # 马拉松信息
        days_until = marathon_goal.days_until_race
        phase = marathon_goal.training_phase
        
        result["marathon_info"] = {
            "goal_id": marathon_goal.goal_id,
            "goal_name": marathon_goal.goal_name,
            "race_date": marathon_goal.race_date,
            "race_type": marathon_goal.race_type,
            "days_until_race": days_until,
            "training_phase": phase,
            "goal_time": marathon_goal.goal_time,
            "target_pace": marathon_goal.target_pace,
            "splits": marathon_goal.splits
        }
        
        # 获取近期训练数据
        recent_runs = self.tracker.get_recent_runs(user_id, days=7)
        pattern = self.tracker.analyze_running_pattern(user_id, days=14)
        
        # 获取WHOOP恢复数据
        recovery = 50
        hrv = 40
        if whoop_data:
            recovery = whoop_data.get("recovery", 50)
            hrv = whoop_data.get("hrv", 40)
        
        # 生成今日建议
        result["today_suggestion"] = self._generate_today_suggestion(
            marathon_goal=marathon_goal,
            phase=phase,
            days_until=days_until,
            recent_runs=recent_runs,
            pattern=pattern,
            recovery=recovery,
            hrv=hrv
        )
        
        # 生成警告
        result["warnings"] = self._generate_warnings(
            marathon_goal=marathon_goal,
            phase=phase,
            days_until=days_until,
            recent_runs=recent_runs,
            recovery=recovery,
            hrv=hrv
        )
        
        # 生成比赛策略
        if phase in ["比赛周", "比赛日", "减量期"]:
            result["race_strategy"] = self._generate_race_strategy(
                marathon_goal=marathon_goal,
                recent_training=recent_runs
            )
        
        return result
    
    def _generate_today_suggestion(
        self,
        marathon_goal: MarathonGoal,
        phase: str,
        days_until: int,
        recent_runs: List[Dict],
        pattern: Dict,
        recovery: int,
        hrv: int
    ) -> Dict:
        """生成今日训练建议"""
        
        # 基于阶段的基础建议
        phase_suggestions = {
            "基础期": {
                "type": "长跑",
                "distance_range": (10, 15),
                "pace_range": ("6:00", "6:30"),
                "heart_rate_zone": "Z2",
                "description": "基础有氧跑"
            },
            "巅峰期": {
                "type": "节奏跑",
                "distance_range": (8, 12),
                "pace_range": ("5:30", "5:50"),
                "heart_rate_zone": "Z3-Z4",
                "description": "强度训练"
            },
            "减量期": {
                "type": "轻松跑",
                "distance_range": (5, 8),
                "pace_range": ("6:30", "7:00"),
                "heart_rate_zone": "Z2",
                "description": "保持状态"
            },
            "比赛周": {
                "type": "热身跑",
                "distance_range": (3, 5),
                "pace_range": ("7:00", "7:30"),
                "heart_rate_zone": "Z1-Z2",
                "description": "保持活力"
            },
            "比赛日": {
                "type": "比赛",
                "distance_range": (0, 0),
                "pace_range": ("全力", "目标配速"),
                "heart_rate_zone": "Z3-Z5",
                "description": "全力以赴！"
            }
        }
        
        suggestion = phase_suggestions.get(phase, phase_suggestions["基础期"]).copy()
        
        # 根据恢复数据调整
        if recovery < 50:
            suggestion["type"] = "休息或轻松散步"
            suggestion["distance_range"] = (0, 3)
            suggestion["pace_range"] = ("随意", "散步速度")
            suggestion["heart_rate_zone"] = "Z1"
            suggestion["description"] = "恢复不足，以休息为主"
        elif recovery < 65:
            # 降低强度
            suggestion["distance_range"] = (
                suggestion["distance_range"][0] * 0.7,
                suggestion["distance_range"][1] * 0.7
            )
            suggestion["description"] = suggestion["description"] + "（减量）"
        
        # 根据近期训练调整
        if len(recent_runs) >= 2:
            last_two_long = all(r.get("distance_km", 0) >= 10 for r in recent_runs[:2])
            if last_two_long and phase in ["巅峰期", "基础期"]:
                suggestion["type"] = "恢复跑"
                suggestion["distance_range"] = (5, 8)
                suggestion["pace_range"] = ("6:30", "7:00")
                suggestion["description"] = "连续高强度后恢复"
        
        # 计算目标距离
        target_distance = suggestion["distance_range"][1]  # 使用范围的上限
        
        return {
            "suggested_distance": round(target_distance, 1),
            "distance_range": suggestion["distance_range"],
            "suggested_type": suggestion["type"],
            "suggested_pace": f"{suggestion['pace_range'][0]}-{suggestion['pace_range'][1]}",
            "heart_rate_zone": suggestion["heart_rate_zone"],
            "description": suggestion["description"],
            "training_phase": phase,
            "days_until_race": days_until,
            "recovery_status": "良好" if recovery >= 65 else "一般" if recovery >= 50 else "不足"
        }
    
    def _generate_warnings(
        self,
        marathon_goal: MarathonGoal,
        phase: str,
        days_until: int,
        recent_runs: List[Dict],
        recovery: int,
        hrv: int
    ) -> List[str]:
        """生成警告列表"""
        warnings = []
        
        # 恢复不足警告
        if recovery < 50:
            warnings.append("⚠️ 恢复评分较低（{}%），建议休息或极轻活动".format(recovery))
        elif recovery < 65:
            warnings.append("🟡 恢复评分一般（{}%），注意控制强度".format(recovery))
        
        # HRV警告
        if hrv < 30:
            warnings.append("⚠️ HRV偏低（{}ms），可能有疲劳累积".format(hrv))
        
        # 连续高强度警告
        if len(recent_runs) >= 3:
            recent_high = sum(1 for r in recent_runs[:3] if r.get("distance_km", 0) >= 10)
            if recent_high >= 3:
                warnings.append("⚠️ 连续多日长跑，注意休息和恢复")
        
        # 比赛周特殊警告
        if phase == "比赛周":
            warnings.append("🔴 赛前减量期，减少训练量")
            warnings.append("💤 保证充足睡眠比训练更重要")
        elif phase == "减量期":
            warnings.append("🟡 减量期，训练量降到平时的60%")
        
        # 背靠背比赛警告
        if marathon_goal.is_recovery_week_needed:
            warnings.append("⚠️ 背靠背比赛，7天内有两场，注意恢复")
        
        return warnings
    
    def _generate_race_strategy(
        self,
        marathon_goal: MarathonGoal,
        recent_training: List[Dict]
    ) -> Dict:
        """生成比赛策略"""
        race_type = marathon_goal.race_type
        target_pace = marathon_goal.target_pace
        target_time = marathon_goal.goal_time
        
        strategy = {
            "race_pace": target_pace or "待定",
            "target_time": target_time or "待定",
            "前半段": {},
            "后半段": {}
        }
        
        if race_type == "半程马拉松":
            pace_seconds = self._pace_to_seconds(target_pace) if target_pace else 0
            if pace_seconds:
                first_half_pace = pace_seconds
                second_half_pace = pace_seconds + 5  # 后半段慢5秒
                strategy["前半段"] = {
                    "distance": "10.5km",
                    "pace": self._seconds_to_pace(first_half_pace),
                    "description": "按目标配速跑，保持节奏"
                }
                strategy["后半段"] = {
                    "distance": "10.7km",
                    "pace": self._seconds_to_pace(second_half_pace),
                    "description": "可能会掉速，控制在10秒内"
                }
        
        elif race_type == "全程马拉松":
            pace_seconds = self._pace_to_seconds(target_pace) if target_pace else 0
            if pace_seconds:
                first_half_pace = pace_seconds
                second_half_pace = pace_seconds + 10  # 后半段慢10秒
                strategy["前半段"] = {
                    "distance": "21km",
                    "pace": self._seconds_to_pace(first_half_pace),
                    "description": "按目标配速跑，前半程留有余地"
                }
                strategy["后半段"] = {
                    "distance": "21km",
                    "pace": self._seconds_to_pace(second_half_pace),
                    "description": "可能会掉速，注意补给和心态"
                }
        
        return strategy
    
    def should_adjust_plan(
        self,
        user_id: str,
        whoop_recovery: int
    ) -> Tuple[bool, str, str]:
        """
        检查是否需要调整训练计划
        
        Returns:
            (should_adjust: bool, reason: str, adjustment: str)
        """
        marathon_goal = self.goals_manager.get_active_marathon_goal()
        
        if not marathon_goal:
            return False, "", ""
        
        phase = marathon_goal.training_phase
        days_until = marathon_goal.days_until_race
        
        # 恢复极低
        if whoop_recovery < 40:
            return True, \
                   "恢复评分过低（{}%）".format(whoop_recovery), \
                   "建议当天完全休息或只做拉伸"
        
        # 比赛周不应大强度
        if phase == "比赛周" and whoop_recovery < 70:
            return True, \
                   "赛前恢复不足（{}%）".format(whoop_recovery), \
                   "建议减少训练量，以热身为主"
        
        # 连续高强度后
        recent_runs = self.tracker.get_recent_runs(user_id, days=5)
        if len(recent_runs) >= 2:
            high_intensity = sum(1 for r in recent_runs[:2] if r.get("distance_km", 0) >= 12)
            if high_intensity >= 2 and whoop_recovery < 60:
                return True, \
                       "连续高强度训练后恢复一般", \
                       "建议改成轻松跑或休息一天"
        
        return False, "", ""
    
    def get_phase_summary(self, marathon_goal: MarathonGoal) -> Dict:
        """获取训练阶段总结"""
        phase = marathon_goal.training_phase
        days_until = marathon_goal.days_until_race
        config = self.get_training_phase_config(phase)
        
        # 计算建议跑量
        pattern = self.tracker.analyze_running_pattern(self.user_id, days=30)
        current_weekly = pattern.get("weekly_avg_distance", 0)
        suggested_weekly = current_weekly * config["weekly_volume_ratio"]
        
        return {
            "phase": phase,
            "days_until": days_until,
            "description": config["description"],
            "intensity": config["intensity"],
            "weekly_volume_suggestion": round(suggested_weekly, 1),
            "long_run_suggestion": round(suggested_weekly * config["long_run_ratio"], 1),
            "phase_tips": self._get_phase_tips(phase, marathon_goal)
        }
    
    def _get_phase_tips(self, phase: str, marathon_goal: MarathonGoal) -> List[str]:
        """获取各阶段的训练提示"""
        tips_by_phase = {
            "基础期": [
                "以低强度有氧跑为主，堆积跑量",
                "每周跑量增加不超过10%",
                "专注跑步技术，动作经济性",
                "保持规律作息，保证恢复"
            ],
            "巅峰期": [
                "引入节奏跑和间歇训练",
                "节奏跑保持在Z3-Z4区",
                "长跑配速可以接近目标配速",
                "注意训练后的充分恢复"
            ],
            "减量期": [
                "训练量降到巅峰期的60%",
                "以轻松跑为主，不追求速度",
                "保证充足睡眠和营养",
                "可以安排一次热身跑保持状态"
            ],
            "比赛周": [
                "大幅减少跑量，只做热身",
                "以Z1-Z2强度为主",
                "早睡早起，保证7-8小时睡眠",
                "碳水化合物补充，储备能量"
            ],
            "比赛日": [
                "比赛日！全力以赴！",
                "注意起跑节奏，不要冲动",
                "补给按计划进行",
                "享受比赛，相信训练！"
            ]
        }
        
        tips = tips_by_phase.get(phase, [])
        
        # 背靠背比赛额外提示
        if marathon_goal.is_recovery_week_needed and phase == "比赛周":
            tips.append("⚠️ 背靠背比赛，赛前充分休息")
        
        return tips
    
    def _get_default_suggestion(self) -> Dict:
        """获取默认建议（无马拉松计划时）"""
        return {
            "suggested_distance": 8.0,
            "suggested_type": "轻松跑",
            "suggested_pace": "6:00-6:30",
            "heart_rate_zone": "Z2",
            "description": "保持运动习惯",
            "training_phase": "无计划",
            "days_until_race": None,
            "recovery_status": "未知"
        }
    
    @staticmethod
    def _pace_to_seconds(pace: str) -> int:
        """将MM:SS转换为秒"""
        try:
            if ":" in pace:
                parts = pace.split(":")
                return int(parts[0]) * 60 + int(parts[1])
        except Exception:
            pass
        return 0
    
    @staticmethod
    def _seconds_to_pace(seconds: float) -> str:
        """将秒数转换为MM:SS格式"""
        minutes = int(seconds // 60)
        secs = int(seconds % 60)
        return f"{minutes}:{secs:02d}"
    
    def get_marathon_context(self, user_id: str) -> Dict:
        """
        获取马拉松背景信息（供报告使用）
        
        Returns:
            {
                "has_marathon_plan": bool,
                "race_name": str,
                "days_until_race": int,
                "training_phase": str,
                "phase_summary": {...},
                "next_race": {...}
            }
        """
        context = {
            "has_marathon_plan": False,
            "race_name": None,
            "days_until_race": None,
            "training_phase": None,
            "phase_summary": None,
            "next_race": None
        }
        
        marathon_goal = self.goals_manager.get_active_marathon_goal()
        
        if not marathon_goal:
            return context
        
        context["has_marathon_plan"] = True
        context["race_name"] = marathon_goal.goal_name
        context["days_until_race"] = marathon_goal.days_until_race
        context["training_phase"] = marathon_goal.training_phase
        context["phase_summary"] = self.get_phase_summary(marathon_goal)
        
        # 获取即将到来的比赛
        upcoming = self.goals_manager.get_upcoming_marathon_goals(limit=2)
        if upcoming:
            context["next_race"] = {
                "name": upcoming[0].goal_name,
                "date": upcoming[0].race_date,
                "days_until": upcoming[0].days_until_race,
                "type": upcoming[0].race_type,
                "goal_pace": upcoming[0].target_pace
            }
        
        return context


# ==================== 便捷函数 ====================

def get_marathon_context(user_id: str = "default") -> Dict:
    """获取马拉松背景信息"""
    analyzer = MarathonAnalyzer(user_id)
    return analyzer.get_marathon_context(user_id)


def get_training_advice(user_id: str, whoop_data: Dict = None) -> Dict:
    """获取训练建议"""
    analyzer = MarathonAnalyzer(user_id)
    return analyzer.get_training_advice(user_id, whoop_data)


def should_adjust_plan(user_id: str, recovery: int) -> Tuple[bool, str, str]:
    """检查是否需要调整计划"""
    analyzer = MarathonAnalyzer(user_id)
    return analyzer.should_adjust_plan(user_id, recovery)


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

if __name__ == "__main__":
    print("=== Marathon Analyzer Test ===")
    
    analyzer = MarathonAnalyzer("test_user")
    
    # 测试获取阶段配置
    print("\n--- Training Phase Config ---")
    for phase in ["基础期", "巅峰期", "减量期", "比赛周", "比赛日"]:
        config = analyzer.get_training_phase_config(phase)
        print(f"{phase}: {config['description']}")
    
    # 测试阶段识别
    print("\n--- Phase from Days ---")
    for days in [35, 21, 10, 5, 0]:
        phase = analyzer.get_phase_from_days(days)
        print(f"{days} days: {phase}")
    
    # 测试训练建议
    print("\n--- Training Advice (with mock data) ---")
    advice = analyzer.get_training_advice(
        user_id="test_user",
        whoop_data={"recovery": 70, "hrv": 45}
    )
    print(f"Has plan: {advice['has_marathon_plan']}")
    if advice['has_marathon_plan']:
        print(f"Marathon: {advice['marathon_info']['goal_name']}")
        print(f"Days until: {advice['marathon_info']['days_until_race']}")
        print(f"Phase: {advice['marathon_info']['training_phase']}")
        print(f"Today: {advice['today_suggestion']['suggested_type']} - {advice['today_suggestion']['suggested_distance']}km")
        print(f"Warnings: {len(advice['warnings'])}")
        for w in advice['warnings']:
            print(f"  - {w}")
    
    # 测试阶段总结
    print("\n--- Phase Summary ---")
    summary = analyzer.get_phase_summary(
        MarathonGoal(
            goal_id="test",
            user_id="test",
            race_date=(datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d"),
            race_type="半程马拉松"
        )
    )
    print(f"Phase: {summary['phase']}")
    print(f"Description: {summary['description']}")
    print(f"Tips:")
    for tip in summary['phase_tips']:
        print(f"  - {tip}")
    
    print("\n✅ All tests passed!")


path: lib/marathon_commands.py
content: """
Marathon Commands - 马拉松训练指令处理
用户通过自然语言设置和管理马拉松训练计划
"""

import os
import sys
from datetime import datetime
from typing import Dict, List, Optional, Tuple

# 添加父目录到路径
SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, SKILL_DIR)

from lib.goals import GoalsManager, MarathonGoal
from lib.tracker import ProgressTracker
from lib.marathon_analyzer import MarathonAnalyzer


class MarathonCommands:
    """
    马拉松指令处理器
    处理用户设置、查询、调整马拉松计划的指令
    """
    
    def __init__(self, user_id: str = "dongyi"):
        self.user_id = user_id
        self.goals_manager = GoalsManager(user_id)
        self.tracker = ProgressTracker(user_id)
        self.analyzer = MarathonAnalyzer(user_id)
    
    def parse_race_info(self, text: str) -> Dict:
        """
        从用户输入解析比赛信息
        
        Args:
            text: 用户输入的文本
        
        Returns:
            {
                "success": bool,
                "race_name": str,
                "race_date": str,
                "goal_time": str,
                "race_type": str,
                "errors": [...]
            }
        """
        result = {
            "success": False,
            "race_name": "",
            "race_date": "",
            "goal_time": "",
            "race_type": "半程马拉松",
            "errors": []
        }
        
        text = text.strip()
        
        # 比赛类型识别
        race_types = {
            "全马": "全程马拉松",
            "全程马拉松": "全程马拉松",
            "全程": "全程马拉松",
            "半马": "半程马拉松",
            "半程马拉松": "半程马拉松",
            "半程": "半程马拉松"
        }
        
        for key, value in race_types.items():
            if key in text:
                result["race_type"] = value
                break
        
        # 比赛日期识别 (YYYY-MM-DD 或 YYYYMMDD 或中文格式)
        import re
        from datetime import datetime
        current_year = datetime.now().year
        
        # 带年份的模式
        dated_patterns = [
            (r"(\d{4})-(\d{1,2})-(\d{1,2})", False),  # 2026-04-12
            (r"(\d{4})年(\d{1,2})月(\d{1,2})日", False),  # 2026年4月12日
            (r"(\d{4})(\d{2})(\d{2})", False),  # 20260412
        ]
        
        # 不带年份的模式（使用当前年份）
        yearless_patterns = [
            r"(\d{1,2})月(\d{1,2})日",  # 4月12日
            r"(\d{1,2})月(\d{1,2})号",  # 4月12号
        ]
        
        date_found = None
        for pattern, has_dash in dated_patterns:
            match = re.search(pattern, text)
            if match:
                groups = match.groups()
                if len(groups) == 3:
                    date_found = f"{groups[0]}-{int(groups[1]):02d}-{int(groups[2]):02d}"
                    break
        
        if not date_found:
            for pattern in yearless_patterns:
                match = re.search(pattern, text)
                if match:
                    groups = match.groups()
                    month = int(groups[0])
                    day = int(groups[1])
                    date_found = f"{current_year}-{month:02d}-{day:02d}"
                    break
        
        if date_found:
            result["race_date"] = date_found
        else:
            result["errors"].append("未找到比赛日期，请使用 YYYY-MM-DD 格式")
        
        # 目标时间识别
        time_patterns = [
            r"(\d{1,2}):(\d{2}):(\d{2})",  # 2:00:00
            r"(\d{1,2})点(\d{1,2})分",  # 2点00分
            r"(\d{1,2})时(\d{1,2})分",  # 2时00分
            r"破?2?[时:]?(\d{1,2})[分]?(\d{2})?秒?",  # 破2 / 2:00 / 2点
        ]
        
        goal_time = None
        
        # 简单识别"破2" -> 2小时
        if "破2" in text and result["race_type"] == "半程马拉松":
            goal_time = "02:00:00"
        elif "破4:45" in text or "破445" in text:
            goal_time = "04:45:00"
        elif "4:45" in text or "445" in text:
            goal_time = "04:45:00"
        
        # 识别 XX:XX:XX 格式
        if not goal_time:
            for pattern in time_patterns:
                match = re.search(pattern, text)
                if match:
                    groups = match.groups()
                    if len(groups) >= 2 and groups[0] is not None:
                        h = int(groups[0])
                        m = int(groups[1]) if groups[1] is not None else 0
                        s = int(groups[2]) if len(groups) > 2 and groups[2] else 0
                        goal_time = f"{h}:{m:02d}:{s:02d}"
                        break
        
        if goal_time:
            result["goal_time"] = goal_time
        
        # 比赛名称识别
        race_name_patterns = [
            r"北京半马",
            r"通州全马",
            r"上海半马",
            r"上海全马",
            r"厦门半马",
            r"厦门全马",
            r"无锡半马",
            r"无锡全马",
            r"武汉半马",
            r"武汉全马",
        ]
        
        for pattern in race_name_patterns:
            if pattern in text:
                result["race_name"] = pattern
                break
        
        if not result["race_name"]:
            # 默认名称
            if result["race_type"] == "半程马拉松":
                result["race_name"] = "半马"
            else:
                result["race_name"] = "全马"
        
        # 验证
        if not result["race_date"]:
            result["errors"].append("请提供比赛日期")
        
        result["success"] = len(result["errors"]) == 0
        
        return result
    
    def set_marathon_goal(self, race_name: str, race_date: str, 
                         goal_time: str = None,
                         race_type: str = "半程马拉松") -> Dict:
        """
        设置马拉松目标
        
        Args:
            race_name: 比赛名称
            race_date: 比赛日期 (YYYY-MM-DD)
            goal_time: 目标时间 (HH:MM:SS)
            race_type: 比赛类型
        
        Returns:
            {
                "success": bool,
                "message": str,
                "goal": MarathonGoal
            }
        """
        try:
            # 验证日期
            race_date_obj = datetime.strptime(race_date, "%Y-%m-%d")
            days_until = (race_date_obj.date() - datetime.now().date()).days
            
            if days_until < 0:
                return {
                    "success": False,
                    "message": f"比赛日期 {race_date} 已经过去",
                    "goal": None
                }
            
            # 创建目标
            goal = self.goals_manager.create_marathon_goal(
                race_name=race_name,
                race_date=race_date,
                goal_time=goal_time,
                race_type=race_type
            )
            
            # 生成成功消息
            phase = goal.training_phase
            target_pace_str = ""
            if goal.target_pace:
                target_pace_str = f"（配速 {goal.target_pace}/km）"
            
            message = f"""✅ 马拉松目标已设置！

🏃 比赛：{goal.goal_name}
📅 日期：{race_date}（{days_until}天后）
🏅 类型：{goal.race_type}"""
            
            if goal.goal_time:
                message += f"\n🎯 目标：{goal.goal_time}"
            
            if goal.target_pace:
                message += f"\n⚡ 目标配速：{goal.target_pace}/km"
            
            message += f"\n📊 当前阶段：{phase}"
            
            return {
                "success": True,
                "message": message,
                "goal": goal
            }
            
        except Exception as e:
            return {
                "success": False,
                "message": f"设置失败：{str(e)}",
                "goal": None
            }
    
    def get_marathon_status(self) -> str:
        """
        获取当前马拉松训练状态
        
        Returns:
            状态报告文本
        """
        goals = self.goals_manager.get_marathon_goals()
        active = self.goals_manager.get_active_marathon_goals()
        
        if not active:
            return """📭 暂无马拉松训练计划

设置方式：
/设置马拉松 北京半马 2026-04-12 目标2:00:00
或直接告诉我：「4月12号半马，目标是破2」"""
        
        # 获取最近跑步
        recent_runs = self.tracker.get_recent_runs(self.user_id, days=7)
        distance_summary = self.tracker.get_distance_summary(self.user_id)
        
        # 构建状态报告
        lines = []
        lines.append("🏃 **马拉松训练状态**")
        lines.append("")
        
        for goal in active:
            days = goal.days_until_race
            phase = goal.training_phase
            
            lines.append("━━━━━━━━━━━━━━━━━━━━")
            lines.append(f"📅 **{goal.goal_name}**")
            lines.append(f"📆 日期：{goal.race_date}（{days}天后）")
            lines.append(f"🏅 类型：{goal.race_type}")
            
            if goal.goal_time:
                lines.append(f"🎯 目标：{goal.goal_time}")
            if goal.target_pace:
                lines.append(f"⚡ 配速：{goal.target_pace}/km")
            
            lines.append(f"📊 阶段：{phase}")
            
            # 阶段提示
            phase_summary = self.analyzer.get_phase_summary(goal)
            if phase_summary.get("phase_tips"):
                lines.append("")
                lines.append("💡 阶段提示：")
                for tip in phase_summary["phase_tips"][:2]:
                    lines.append(f"• {tip}")
            
            lines.append("")
        
        # 本周跑量
        lines.append("━━━━━━━━━━━━━━━━━━━━")
        lines.append("📈 **本周跑量**")
        lines.append(f"• 本周：{distance_summary['this_week_km']}km")
        lines.append(f"• 上周：{distance_summary['last_week_km']}km")
        week_change = distance_summary['week_change']
        if week_change >= 0:
            lines.append(f"• 变化：+{week_change}km ↑")
        else:
            lines.append(f"• 变化：{week_change}km ↓")
        
        # 最近跑步
        if recent_runs:
            lines.append("")
            lines.append("🏃 **近期跑步**")
            for r in recent_runs[:3]:
                date = r.get("date", "")[5:]  # MM-DD
                dist = r.get("distance_km", 0)
                pace = r.get("avg_pace", "-")
                lines.append(f"• {date} {dist}km @ {pace}/km")
        
        return "\n".join(lines)
    
    def adjust_plan(self, date_str: str, new_plan: str) -> Dict:
        """
        调整某天的计划
        
        Args:
            date_str: 日期（如"明天"、"4月5日"）
            new_plan: 新计划（如"休息"、"8km轻松跑"）
        
        Returns:
            {
                "success": bool,
                "message": str
            }
        """
        # 解析日期
        import re
        today = datetime.now()
        
        if date_str in ["今天", "今日"]:
            target_date = today
        elif date_str in ["明天", "明日"]:
            target_date = today + __import__('datetime').timedelta(days=1)
        elif date_str in ["后天", "后日"]:
            target_date = today + __import__('datetime').timedelta(days=2)
        else:
            # 尝试解析 YYYY-MM-DD 或 MM-DD
            match = re.search(r"(\d{1,2})月(\d{1,2})日", date_str)
            if match:
                month, day = int(match.group(1)), int(match.group(2))
                target_date = datetime(today.year, month, day)
            else:
                return {
                    "success": False,
                    "message": f"无法解析日期：{date_str}，请使用「今天」「明天」「4月5日」等格式"
                }
        
        date_str_formatted = target_date.strftime("%Y-%m-%d")
        
        return {
            "success": True,
            "message": f"""✅ 计划已调整

📅 {target_date.strftime('%m月%d日')}（{date_str}）
🆕 新计划：{new_plan}

注意：此功能需要后续打卡系统支持""",
            "adjusted_date": date_str_formatted,
            "new_plan": new_plan
        }
    
    def get_training_calendar(self, days: int = 14) -> str:
        """
        获取训练日历
        
        Args:
            days: 天数
        
        Returns:
            日历文本
        """
        calendar = self.tracker.get_training_calendar(
            self.user_id,
            end_date=datetime.now().strftime("%Y-%m-%d")
        )
        
        # 只显示有跑步的日子
        running_days = [d for d in calendar if d.get("has_training")]
        
        if not running_days:
            return "📭 近期暂无跑步记录"
        
        lines = []
        lines.append(f"🏃 **近期训练日历**（{len(running_days)}天有跑步）")
        lines.append("")
        
        for day in running_days[-14:]:
            date = day["date"]
            total = day.get("total_distance", 0)
            runs = day.get("runs", [])
            
            # 格式化日期
            try:
                d = datetime.strptime(date, "%Y-%m-%d")
                date_display = d.strftime("%m/%d")
            except:
                date_display = date
            
            # 跑步详情
            run_details = []
            for r in runs:
                dist = r.get("distance_km", 0)
                pace = r.get("avg_pace", "-")
                run_details.append(f"{dist}km@{pace}")
            
            lines.append(f"**{date_display}** {total}km")
            for detail in run_details:
                lines.append(f"  • {detail}")
        
        return "\n".join(lines)


# ==================== 便捷函数 ====================

def parse_and_set_marathon(user_id: str, text: str) -> Dict:
    """解析用户输入并设置马拉松目标"""
    cmd = MarathonCommands(user_id)
    
    # 解析比赛信息
    parsed = cmd.parse_race_info(text)
    
    if not parsed["success"]:
        return {
            "success": False,
            "message": f"解析失败：{', '.join(parsed['errors'])}\n\n请使用格式：\n• 4月12号半马，目标是破2\n• 北京半马 2026-04-12 目标2:00:00"
        }
    
    # 设置目标
    result = cmd.set_marathon_goal(
        race_name=parsed["race_name"],
        race_date=parsed["race_date"],
        goal_time=parsed.get("goal_time"),
        race_type=parsed["race_type"]
    )
    
    return result


def get_marathon_status(user_id: str = "dongyi") -> str:
    """获取马拉松状态"""
    cmd = MarathonCommands(user_id)
    return cmd.get_marathon_status()


def adjust_training_plan(user_id: str, date: str, new_plan: str) -> Dict:
    """调整训练计划"""
    cmd = MarathonCommands(user_id)
    return cmd.adjust_plan(date, new_plan)


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

if __name__ == "__main__":
    print("=== Marathon Commands Test ===")
    
    cmd = MarathonCommands("test_user")
    
    # 测试解析
    print("\n--- Parse Race Info ---")
    test_inputs = [
        "4月12号半马目标是破2",
        "北京半马 2026-04-12 目标2:00:00",
        "通州全马 2026-04-19 目标是4:45",
    ]
    
    for inp in test_inputs:
        parsed = cmd.parse_race_info(inp)
        print(f"\nInput: {inp}")
        print(f"  Success: {parsed['success']}")
        if parsed['success']:
            print(f"  Name: {parsed['race_name']}")
            print(f"  Date: {parsed['race_date']}")
            print(f"  Type: {parsed['race_type']}")
            print(f"  Time: {parsed['goal_time']}")
        else:
            print(f"  Errors: {parsed['errors']}")
    
    # 测试设置马拉松目标
    print("\n--- Set Marathon Goal ---")
    result = cmd.set_marathon_goal(
        race_name="测试半马",
        race_date="2026-04-12",
        goal_time="02:00:00",
        race_type="半程马拉松"
    )
    print(f"Success: {result['success']}")
    print(f"Message: {result['message']}")
    
    # 测试状态查询
    print("\n--- Marathon Status ---")
    status = cmd.get_marathon_status()
    print(status)
    
    # 测试调整计划
    print("\n--- Adjust Plan ---")
    adjust = cmd.adjust_plan("明天", "休息")
    print(f"Success: {adjust['success']}")
    print(f"Message: {adjust['message']}")
    
    # 清理测试数据
    print("\n--- Cleanup ---")
    goals = cmd.goals_manager.get_marathon_goals()
    for g in goals:
        cmd.goals_manager.delete_marathon_goal(g.goal_id)
    print(f"Deleted {len(goals)} test goals")
    
    print("\n✅ All tests passed!")


path: lib/ml/__init__.py
content: """
ML Module - 机器学习预测和动态调整
包含：
- recovery_model.py: 恢复预测模型
- predictor.py: 兼容旧接口
"""

from lib.ml.recovery_model import (
    RecoveryPredictor,
    HRVAnalyzer,
    get_recovery_prediction,
    get_training_recommendation,
    get_hrv_anomaly,
)

__all__ = [
    'RecoveryPredictor',
    'HRVAnalyzer',
    'get_recovery_prediction',
    'get_training_recommendation',
    'get_hrv_anomaly',
]


path: lib/prompts/__init__.py
content: """
Prompts - 健身Prompt模板集合
包含7大核心模板：
1. 训练模板 (training.py)
2. 恢复模板 (recovery.py)
3. 伤病模板 (injury.py)
4. 周报模板 (weekly.py)
"""

from lib.prompts.training import (
    get_training_prompt,
    TEMPLATE_COMPLETE_PLAN,
    TEMPLATE_PLATEAU_BREAK,
    TEMPLATE_WEAKPOINT_DIAGNOSTIS,
    TEMPLATE_RECOVERY_SYSTEM,
    TEMPLATE_INJURY_PREVENTION,
    TEMPLATE_TRACKING_FRAMEWORK,
    TEMPLATE_16_WEEK_RERECOMPOSITION,
    TEMPLATE_DAILY_PLAN,
)

from lib.prompts.recovery import (
    get_recovery_prompt,
    TEMPLATE_RECOVERY_SYSTEM as RECOVERY_SYSTEM,
    TEMPLATE_RECOVERY_WEEKLY,
    TEMPLATE_RECOVERY_SLEEP,
)

from lib.prompts.injury import (
    get_injury_prompt,
    TEMPLATE_INJURY_PREVENTION as INJURY_PREVENTION,
    TEMPLATE_INJURY_RECOVERY,
    TEMPLATE_INJURY_SUBSTITUTE,
)

from lib.prompts.weekly import (
    get_weekly_prompt,
    TEMPLATE_WEEKLY_REVIEW,
    TEMPLATE_WEEKLY_PLAN,
    TEMPLATE_MONTHLY_REVIEW,
)

__all__ = [
    # Training
    'get_training_prompt',
    'TEMPLATE_COMPLETE_PLAN',
    'TEMPLATE_PLATEAU_BREAK',
    'TEMPLATE_WEAKPOINT_DIAGNOSTIS',
    'TEMPLATE_RECOVERY_SYSTEM',
    'TEMPLATE_INJURY_PREVENTION',
    'TEMPLATE_TRACKING_FRAMEWORK',
    'TEMPLATE_16_WEEK_RERECOMPOSITION',
    'TEMPLATE_DAILY_PLAN',
    # Recovery
    'get_recovery_prompt',
    'TEMPLATE_RECOVERY_WEEKLY',
    'TEMPLATE_RECOVERY_SLEEP',
    # Injury
    'get_injury_prompt',
    'TEMPLATE_INJURY_RECOVERY',
    'TEMPLATE_INJURY_SUBSTITUTE',
    # Weekly
    'get_weekly_prompt',
    'TEMPLATE_WEEKLY_REVIEW',
    'TEMPLATE_WEEKLY_PLAN',
    'TEMPLATE_MONTHLY_REVIEW',
]


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