Skill: kinsonhu123-mkll/airunningcoach (version 1.0.1)

--- name: ai-running-coach description: AI 跑步教练 - 基于 VDOT 理论和周期化训练的专业马拉松训练指导。使用场景：制定马拉松/半马训练计划、分析跑步数据（TCX/GPX）、计算配速/心率区间、比赛策略制定、训练问题咨询、伤病预防建议。 --- # AI 跑步教练技能 专业的马拉松训练指导，基于 Jack Daniels VDOT 理论和极化训练原则。 --- ## 核心功能 | 功能 | 说明 | 触发示例 | |------|------|----------| | 训练计划制定 | 生成个性化周期化训练计划 | "帮我制定破 3 训练计划" | | 配速/心率计算 | 基于 VDOT 计算各区间配速和心率 | "破 3 需要什么配速" | | 跑步数据分析 | 解析 TCX/GPX 文件，分析训练表现 | "分析我今天的跑步数据" | | 比赛策略制定 | 制定比赛配速和补给策略 | "全马比赛怎么配速" | | 训练问题咨询 | 回答训练方法、伤病预防等问题 | "间歇跑怎么练" | | 成绩预测 | 基于训练数据预测比赛成绩 | "我能跑多少" | --- ## 工作流程 ### 1. 训练计划制定 **触发：** 用户提到"训练计划"、"马拉松"、"备赛"等 **步骤：** 1. **收集信息**（如未提供）： - 当前 PB 或最近比赛成绩 - 目标赛事和目标时间 - 当前周跑量和训练频率 - 年龄、体重（可选） - 伤病史（可选） 2. **计算 VDOT**： ```bash python scripts/calculate_pace.py --race-time <分钟数> --distance 42.195 ``` 3. **生成计划**： ```bash python scripts/generate_plan.py --current-pb <HH:MM> --target <HH:MM> --weeks 24 --weekly-km <跑量> ``` 4. **输出计划**： - 阶段划分（基础期、强化期、巅峰期、减量期） - 周训练安排（含配速、心率区间） - 关键训练课说明 **参考：** `references/training-zones.md`、`references/workout-types.md` --- ### 2. 配速/心率区间计算 **触发：** 用户询问配速、心率区间、VDOT 等 **步骤：** 1. **确定输入**： - 如有比赛成绩：用成绩计算 VDOT - 如有 VDOT：直接使用 - 如有最大心率/静息心率：计算心率区间 2. **计算**： ```bash # 基于比赛成绩 python scripts/calculate_pace.py --race-time 218 --distance 42.195 --max-hr 196 --resting-hr 55 # 基于 VDOT python scripts/calculate_pace.py --vdot 44 --max-hr 196 ``` 3. **输出**： - 各配速区间（E/M/T/I/R） - 各心率区间（Z1-Z5） - 对应训练用途说明 **参考：** `references/training-zones.md` --- ### 3. 跑步数据分析 **触发：** 用户上传 TCX/GPX 文件或询问训练数据分析 **步骤：** 1. **解析文件**： ```bash python scripts/analyze_run.py <文件路径> --output json ``` 2. **分析内容**： - 总距离、总时间、平均配速 - 平均心率、最大心率 - 平均步频 - 心率区间分布 - 配速稳定性 3. **给出建议**： - 训练强度是否合适 - 心率是否偏高/偏低 - 改进建议 **参考：** `references/training-zones.md`、`references/workout-types.md` --- ### 4. 比赛策略制定 **触发：** 用户询问比赛配速、补给策略、赛前准备等 **步骤：** 1. **收集信息**： - 目标赛事和目标时间 - 当前训练水平（PB、最近训练配速） - 比赛地点和天气（可选） 2. **制定策略**： - 配速策略（匀速/负分割） - 补给计划（胶、盐丸、水） - 赛前一周安排 - 比赛日清单 3. **输出**： - 各公里点目标时间 - 补给时机 - 心理策略 **参考：** `references/race-strategies.md` --- ### 5. 训练问题咨询 **触发：** 用户询问训练方法、伤病、装备等 **处理：** 1. **判断问题类型**： - 训练方法 → 参考 `references/workout-types.md` - 心率/配速 → 参考 `references/training-zones.md` - 比赛策略 → 参考 `references/race-strategies.md` 2. **给出专业回答**： - 基于跑步科学理论 - 结合用户实际情况 - 提供可执行建议 --- ## 脚本说明 ### calculate_pace.py **功能：** 配速和心率区间计算 **参数：** - `--vdot`：VDOT 值 - `--race-time`：比赛时间（分钟） - `--distance`：比赛距离（公里，默认 42.195） - `--max-hr`：最大心率 - `--resting-hr`：静息心率 - `--output`：输出格式（json/text） **示例：** ```bash python scripts/calculate_pace.py --race-time 218 --distance 42.195 --max-hr 196 --output json ``` --- ### analyze_run.py **功能：** TCX/GPX 数据分析 **参数：** - `file`：文件路径（必填） - `--output`：输出格式（json/text） **示例：** ```bash python scripts/analyze_run.py run.tcx --output json ``` --- ### generate_plan.py **功能：** 马拉松训练计划生成 **参数：** - `--current-pb`：当前 PB（HH:MM 或分钟数） - `--target`：目标时间（HH:MM 或分钟数） - `--weeks`：训练周期（周，默认 24） - `--weekly-km`：当前周跑量（默认 300） - `--output`：输出格式（json/text） **示例：** ```bash python scripts/generate_plan.py --current-pb 3:38 --target 3:00 --weeks 24 --weekly-km 300 ``` --- ## 关键理论 ### VDOT 理论（Jack Daniels） - 基于比赛成绩量化跑步能力 - 提供 5 个配速区间（E/M/T/I/R） - 科学指导训练强度 ### 极化训练（80/20 法则） - 80% 低强度（Z1-Z2） - 20% 高强度（Z4-Z5） - 避免"垃圾跑量" ### 周期化训练 - 基础期：有氧基础 - 强化期：阈值 + 间歇 - 巅峰期：马拉松配速 + 最长 LSD - 减量期：恢复 + 保持 --- ## 注意事项 1. **个体差异**：公式计算是参考，需结合个人感受调整 2. **循序渐进**：周跑量增幅不超过 10% 3. **恢复优先**：感觉疲劳时优先休息 4. **伤病预警**：持续疼痛应停止训练并就医 5. **数据局限**：TCX/GPX 解析依赖文件格式，部分数据可能缺失 --- ## 参考文档 - `references/training-zones.md` - 训练区间定义 - `references/workout-types.md` - 训练课类型详解 - `references/race-strategies.md` - 比赛策略指南

path: assets/weekly-plan-template.md
content: # 周训练计划模板

## 第 {week_num} 周 - {phase_name}

**阶段重点：** {phase_focus}

**目标跑量：** {weekly_km} km

---

| 星期 | 训练内容 | 距离 | 配速 | 心率 | 备注 |
|------|----------|------|------|------|------|
| 周一 | {mon_type} | {mon_km} | {mon_pace} | {mon_hr} | {mon_note} |
| 周二 | {tue_type} | {tue_km} | {tue_pace} | {tue_hr} | {tue_note} |
| 周三 | {wed_type} | {wed_km} | {wed_pace} | {wed_hr} | {wed_note} |
| 周四 | {thu_type} | {thu_km} | {thu_pace} | {thu_hr} | {thu_note} |
| 周五 | {fri_type} | {fri_km} | {fri_pace} | {fri_hr} | {fri_note} |
| 周六 | {sat_type} | {sat_km} | {sat_pace} | {sat_hr} | {sat_note} |
| 周日 | {sun_type} | {sun_km} | {sun_pace} | {sun_hr} | {sun_note} |

---

## 本周重点训练课

### {key_workout_name}

**目的：** {purpose}

**执行要点：**
- {point_1}
- {point_2}
- {point_3}

**注意事项：**
- {warning_1}
- {warning_2}

---

## 周总结

**完成情况：** [ ] 完成 [ ] 部分完成 [ ] 未完成

**感受：** 

**问题/调整：**


path: scripts/generate_plan.py
content: #!/usr/bin/env python3
"""
马拉松训练计划生成器
基于 VDOT 和周期化训练理论
"""

import argparse
import json
from datetime import datetime, timedelta

def generate_weekly_plan(week_num, phase, vdot, weekly_km):
    """
    生成单周训练计划
    
    phase: base(基础), build(强化), peak(巅峰), taper(减量)
    vdot: 当前 VDOT 值
    weekly_km: 周跑量目标
    """
    
    # 配速参考 (基于 VDOT)
    easy_pace = -2.5 * vdot + 280  # 秒/km
    marathon_pace = -2.8 * vdot + 290
    threshold_pace = -3.0 * vdot + 300
    interval_pace = -3.2 * vdot + 310
    
    plan = {
        "week": week_num,
        "phase": phase,
        "target_km": weekly_km,
        "schedule": []
    }
    
    if phase == "base":
        # 基础期：80% 有氧 + 20% 强度
        plan["schedule"] = [
            {"day": "周一", "type": "休息", "description": "休息或交叉训练"},
            {"day": "周二", "type": "轻松跑", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(easy_pace)}", "hr_zone": "Z2"},
            {"day": "周三", "type": "轻松跑", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(easy_pace)}", "hr_zone": "Z2"},
            {"day": "周四", "type": "节奏跑", "km": round(weekly_km * 0.2), "pace": f"{sec_to_pace(threshold_pace - 10)}", "hr_zone": "Z3", "note": "热身 2km + 节奏 5-8km + 冷身 2km"},
            {"day": "周五", "type": "休息", "description": "休息或核心力量"},
            {"day": "周六", "type": "长距离", "km": round(weekly_km * 0.35), "pace": f"{sec_to_pace(easy_pace + 20)}", "hr_zone": "Z2", "note": "最后 2km 马拉松配速"},
            {"day": "周日", "type": "恢复跑", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(easy_pace + 30)}", "hr_zone": "Z1"}
        ]
    
    elif phase == "build":
        # 强化期：70% 有氧 + 20% 强度 + 10% 马拉松配速
        plan["schedule"] = [
            {"day": "周一", "type": "休息", "description": "休息或交叉训练"},
            {"day": "周二", "type": "间歇跑", "km": round(weekly_km * 0.18), "pace": f"{sec_to_pace(interval_pace)}", "hr_zone": "Z4-Z5", "note": "热身 3km + 间歇 6-8x800m + 冷身 2km"},
            {"day": "周三", "type": "轻松跑", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(easy_pace)}", "hr_zone": "Z2"},
            {"day": "周四", "type": "阈值跑", "km": round(weekly_km * 0.2), "pace": f"{sec_to_pace(threshold_pace)}", "hr_zone": "Z3-Z4", "note": "热身 2km + 阈值 8-10km + 冷身 2km"},
            {"day": "周五", "type": "休息", "description": "休息或核心力量"},
            {"day": "周六", "type": "长距离", "km": round(weekly_km * 0.35), "pace": f"{sec_to_pace(easy_pace + 15)}", "hr_zone": "Z2", "note": "后 1/3 马拉松配速"},
            {"day": "周日", "type": "恢复跑", "km": round(weekly_km * 0.12), "pace": f"{sec_to_pace(easy_pace + 30)}", "hr_zone": "Z1"}
        ]
    
    elif phase == "peak":
        # 巅峰期：强度最大，最长 LSD
        plan["schedule"] = [
            {"day": "周一", "type": "休息", "description": "休息或交叉训练"},
            {"day": "周二", "type": "间歇跑", "km": round(weekly_km * 0.18), "pace": f"{sec_to_pace(interval_pace)}", "hr_zone": "Z5", "note": "热身 3km + 间歇 5-6x1000m + 冷身 2km"},
            {"day": "周三", "type": "轻松跑", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(easy_pace)}", "hr_zone": "Z2"},
            {"day": "周四", "type": "马拉松配速", "km": round(weekly_km * 0.22), "pace": f"{sec_to_pace(marathon_pace)}", "hr_zone": "Z3", "note": "热身 3km + MP 12-15km + 冷身 2km"},
            {"day": "周五", "type": "休息", "description": "休息或核心力量"},
            {"day": "周六", "type": "长距离", "km": round(weekly_km * 0.35), "pace": f"{sec_to_pace(easy_pace + 10)}", "hr_zone": "Z2", "note": "后 1/2 马拉松配速"},
            {"day": "周日", "type": "恢复跑", "km": round(weekly_km * 0.1), "pace": f"{sec_to_pace(easy_pace + 30)}", "hr_zone": "Z1"}
        ]
    
    elif phase == "taper":
        # 减量期：跑量减少，保持强度
        plan["schedule"] = [
            {"day": "周一", "type": "休息", "description": "休息"},
            {"day": "周二", "type": "短间歇", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(interval_pace)}", "hr_zone": "Z4", "note": "热身 2km + 间歇 4x400m + 冷身 2km"},
            {"day": "周三", "type": "轻松跑", "km": round(weekly_km * 0.15), "pace": f"{sec_to_pace(easy_pace)}", "hr_zone": "Z2"},
            {"day": "周四", "type": "马拉松配速", "km": round(weekly_km * 0.18), "pace": f"{sec_to_pace(marathon_pace)}", "hr_zone": "Z3", "note": "热身 2km + MP 6-8km + 冷身 2km"},
            {"day": "周五", "type": "休息", "description": "休息"},
            {"day": "周六", "type": "短长距离", "km": round(weekly_km * 0.3), "pace": f"{sec_to_pace(easy_pace)}", "hr_zone": "Z2", "note": "轻松跑，最后 1km MP"},
            {"day": "周日", "type": "恢复跑", "km": round(weekly_km * 0.22), "pace": f"{sec_to_pace(easy_pace + 30)}", "hr_zone": "Z1"}
        ]
    
    return plan

def sec_to_pace(seconds):
    """秒/km 转换为 分：秒/km 格式"""
    mins = int(seconds // 60)
    secs = int(seconds % 60)
    return f"{mins}:{secs:02d}"

def generate_marathon_plan(
    current_pb_min,
    target_time_min,
    weeks=24,
    current_weekly_km=300
):
    """
    生成完整马拉松训练计划
    
    current_pb_min: 当前全马 PB（分钟）
    target_time_min: 目标时间（分钟）
    weeks: 训练周期（周）
    current_weekly_km: 当前周跑量
    """
    
    # 计算 VDOT（基于 Daniels 表）
    current_pace = current_pb_min / 42.195
    target_pace = target_time_min / 42.195
    current_vdot = -9.93 * current_pace + 96.4
    target_vdot = -9.93 * target_pace + 96.4
    
    plan = {
        "runner_info": {
            "current_pb": f"{int(current_pb_min//60)}:{int(current_pb_min%60):02d}",
            "target_time": f"{int(target_time_min//60)}:{int(target_time_min%60):02d}",
            "current_vdot": round(current_vdot, 1),
            "target_vdot": round(target_vdot, 1)
        },
        "phases": [],
        "weekly_plans": []
    }
    
    # 阶段划分
    phase_weeks = {
        "base": 6,
        "build": 8,
        "peak": 6,
        "taper": 2
    }
    
    # 周跑量规划
    base_km = current_weekly_km
    peak_km = current_weekly_km * 1.3  # 峰值跑量增加 30%
    
    week_num = 1
    for phase_name, phase_duration in phase_weeks.items():
        phase_data = {
            "name": phase_name,
            "weeks": phase_duration,
            "focus": get_phase_focus(phase_name)
        }
        plan["phases"].append(phase_data)
        
        for i in range(phase_duration):
            if phase_name == "base":
                weekly_km = base_km + (peak_km - base_km) * (i / phase_duration) * 0.5
            elif phase_name == "build":
                weekly_km = base_km + (peak_km - base_km) * (0.5 + i / phase_duration * 0.5)
            elif phase_name == "peak":
                weekly_km = peak_km * (1 - i * 0.05)  # 逐渐减少
            else:  # taper
                weekly_km = peak_km * (0.6 - i * 0.2)
            
            # 渐进增加 VDOT
            vdot_progress = min(1.0, (week_num - 1) / (weeks - 2))
            current_vdot_adj = current_vdot + (target_vdot - current_vdot) * vdot_progress
            
            weekly_plan = generate_weekly_plan(
                week_num,
                phase_name,
                current_vdot_adj,
                weekly_km
            )
            plan["weekly_plans"].append(weekly_plan)
            week_num += 1
    
    return plan

def get_phase_focus(phase):
    """获取阶段训练重点"""
    focuses = {
        "base": "有氧基础 + 力量",
        "build": "乳酸阈值 + 间歇",
        "peak": "马拉松配速 + 最长 LSD",
        "taper": "恢复 + 保持状态"
    }
    return focuses.get(phase, "")

def main():
    parser = argparse.ArgumentParser(description="马拉松训练计划生成器")
    parser.add_argument("--current-pb", type=str, help="当前 PB (格式：HH:MM 或 分钟数)", required=True)
    parser.add_argument("--target", type=str, help="目标时间 (格式：HH:MM 或 分钟数)", required=True)
    parser.add_argument("--weeks", type=int, default=24, help="训练周期 (周)")
    parser.add_argument("--weekly-km", type=float, default=300, help="当前周跑量 (km)")
    parser.add_argument("--output", choices=["json", "text"], default="text", help="输出格式")
    
    args = parser.parse_args()
    
    # 解析时间
    def parse_time(time_str):
        if ':' in time_str:
            parts = time_str.split(':')
            if len(parts) == 2:
                return int(parts[0]) * 60 + int(parts[1])
        return float(time_str)
    
    current_pb = parse_time(args.current_pb)
    target = parse_time(args.target)
    
    # 生成计划
    plan = generate_marathon_plan(current_pb, target, args.weeks, args.weekly_km)
    
    if args.output == "json":
        print(json.dumps(plan, indent=2, ensure_ascii=False))
    else:
        print(f"\n{'='*60}")
        print("马拉松训练计划")
        print(f"{'='*60}")
        print(f"当前 PB: {plan['runner_info']['current_pb']}")
        print(f"目标时间：{plan['runner_info']['target_time']}")
        print(f"当前 VDOT: {plan['runner_info']['current_vdot']}")
        print(f"目标 VDOT: {plan['runner_info']['target_vdot']}")
        print(f"\n训练周期：{args.weeks}周")
        print(f"\n阶段划分:")
        for phase in plan['phases']:
            print(f"  - {phase['name'].upper()}: {phase['weeks']}周 ({phase['focus']})")
        print(f"{'='*60}\n")

if __name__ == "__main__":
    main()


path: scripts/analyze_run.py
content: #!/usr/bin/env python3
"""
跑步数据分析脚本
解析 TCX/GPX 文件，分析配速、心率、步频等数据
"""

import argparse
import json
import xml.etree.ElementTree as ET
import sys
from datetime import datetime

def parse_gpx(file_path):
    """解析 GPX 文件"""
    try:
        tree = ET.parse(file_path)
        root = tree.getroot()
        
        # 处理 GPX 命名空间
        ns = {'gpx': 'http://www.topografix.com/GPX/1/1'}
        
        tracks = []
        for trk in root.findall('.//gpx:trk', ns):
            track_data = {
                'name': trk.findtext('gpx:name', '', ns),
                'points': []
            }
            
            for trkseg in trk.findall('.//gpx:trkseg', ns):
                for trkpt in trkseg.findall('gpx:trkpt', ns):
                    point = {
                        'lat': float(trkpt.get('lat')),
                        'lon': float(trkpt.get('lon')),
                        'time': trkpt.findtext('gpx:time', '', ns)
                    }
                    
                    # 扩展数据
                    extensions = trkpt.find('gpx:extensions', ns)
                    if extensions is not None:
                        for child in extensions:
                            tag = child.tag.split('}')[-1]  # 移除命名空间
                            if child.text:
                                try:
                                    point[tag] = float(child.text)
                                except ValueError:
                                    point[tag] = child.text
                    
                    track_data['points'].append(point)
            
            tracks.append(track_data)
        
        return tracks
    except Exception as e:
        print(f"解析 GPX 失败：{e}", file=sys.stderr)
        return None

def parse_tcx(file_path):
    """解析 TCX 文件"""
    try:
        tree = ET.parse(file_path)
        root = tree.getroot()
        
        # TCX 命名空间
        ns = {
            'tcx': 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2',
            'xsi': 'http://www.w3.org/2001/XMLSchema-instance'
        }
        
        activities = []
        for activity in root.findall('.//tcx:Activity', ns):
            act_type = activity.get('Sport', 'Running')
            act_data = {
                'sport': act_type,
                'laps': [],
                'total_distance': 0,
                'total_time': 0
            }
            
            for lap in activity.findall('.//tcx:Lap', ns):
                lap_data = {
                    'distance': float(lap.findtext('tcx:DistanceMeters', '0', ns)),
                    'time': lap.findtext('tcx:TotalTimeSeconds', '0', ns),
                    'calories': int(lap.findtext('tcx:Calories', '0', ns)),
                    'avg_hr': None,
                    'max_hr': None,
                    'points': []
                }
                
                # 心率数据
                tpx = lap.find('.//tcx:TPX', {'tpx': 'http://www.garmin.com/xmlschemas/ActivityExtension/v2'})
                if tpx is not None:
                    lap_data['avg_hr'] = tpx.findtext('{http://www.garmin.com/xmlschemas/ActivityExtension/v2}AverageHeartRateBpm')
                    lap_data['max_hr'] = tpx.findtext('{http://www.garmin.com/xmlschemas/ActivityExtension/v2}MaximumHeartRateBpm')
                
                # 轨迹点
                for track in lap.findall('.//tcx:Track', ns):
                    for tp in track.findall('tcx:Trackpoint', ns):
                        point = {
                            'time': tp.findtext('tcx:Time', '', ns),
                            'distance': float(tp.findtext('tcx:DistanceMeters', '0', ns)) if tp.findtext('tcx:DistanceMeters') else None,
                        }
                        
                        # 心率
                        hr_elem = tp.find('tcx:HeartRateBpm/tcx:Value', ns)
                        if hr_elem is not None:
                            point['hr'] = int(hr_elem.text)
                        
                        # 步频
                        tpx = tp.find('tcx:TPX', {'tpx': 'http://www.garmin.com/xmlschemas/ActivityExtension/v2'})
                        if tpx is not None:
                            cadence = tpx.findtext('{http://www.garmin.com/xmlschemas/ActivityExtension/v2}RunCadence')
                            if cadence:
                                point['cadence'] = int(cadence)
                        
                        lap_data['points'].append(point)
                
                act_data['laps'].append(lap_data)
                act_data['total_distance'] += lap_data['distance']
                act_data['total_time'] += float(lap_data['time'])
            
            activities.append(act_data)
        
        return activities
    except Exception as e:
        print(f"解析 TCX 失败：{e}", file=sys.stderr)
        return None

def analyze_activity(data):
    """分析活动数据"""
    if not data:
        return None
    
    analysis = {
        'total_distance_km': 0,
        'total_time_sec': 0,
        'avg_pace': None,
        'avg_hr': None,
        'max_hr': None,
        'avg_cadence': None,
        'hr_zones': {},
        'elevation_gain': 0
    }
    
    # 处理 TCX 格式
    if isinstance(data, list) and len(data) > 0:
        for activity in data:
            analysis['total_distance_km'] += activity.get('total_distance', 0) / 1000
            analysis['total_time_sec'] += activity.get('total_time', 0)
            
            all_hr = []
            all_cadence = []
            
            for lap in activity.get('laps', []):
                if lap.get('avg_hr'):
                    all_hr.append(int(lap['avg_hr']))
                if lap.get('max_hr'):
                    if analysis['max_hr'] is None or int(lap['max_hr']) > analysis['max_hr']:
                        analysis['max_hr'] = int(lap['max_hr'])
                
                for point in lap.get('points', []):
                    if 'hr' in point:
                        all_hr.append(point['hr'])
                    if 'cadence' in point:
                        all_cadence.append(point['cadence'])
            
            if all_hr:
                analysis['avg_hr'] = round(sum(all_hr) / len(all_hr))
            if all_cadence:
                analysis['avg_cadence'] = round(sum(all_cadence) / len(all_cadence))
    
    # 计算平均配速
    if analysis['total_distance_km'] > 0 and analysis['total_time_sec'] > 0:
        pace_sec_per_km = analysis['total_time_sec'] / analysis['total_distance_km']
        mins = int(pace_sec_per_km // 60)
        secs = int(pace_sec_per_km % 60)
        analysis['avg_pace'] = f"{mins}:{secs:02d}"
    
    return analysis

def main():
    parser = argparse.ArgumentParser(description="跑步数据分析")
    parser.add_argument("file", help="TCX 或 GPX 文件路径")
    parser.add_argument("--output", choices=["json", "text"], default="text", help="输出格式")
    
    args = parser.parse_args()
    
    # 解析文件
    if args.file.endswith('.gpx'):
        data = parse_gpx(args.file)
    elif args.file.endswith('.tcx'):
        data = parse_tcx(args.file)
    else:
        print("不支持的文件格式，请使用 .gpx 或 .tcx", file=sys.stderr)
        sys.exit(1)
    
    if not data:
        print("文件解析失败", file=sys.stderr)
        sys.exit(1)
    
    # 分析数据
    analysis = analyze_activity(data)
    
    if args.output == "json":
        print(json.dumps(analysis, indent=2, ensure_ascii=False))
    else:
        print(f"\n{'='*50}")
        print("跑步数据分析")
        print(f"{'='*50}")
        print(f"总距离：{analysis['total_distance_km']:.2f} km")
        print(f"总时间：{analysis['total_time_sec']/3600:.2f} 小时")
        print(f"平均配速：{analysis['avg_pace']} /km")
        if analysis['avg_hr']:
            print(f"平均心率：{analysis['avg_hr']} bpm")
        if analysis['max_hr']:
            print(f"最大心率：{analysis['max_hr']} bpm")
        if analysis['avg_cadence']:
            print(f"平均步频：{analysis['avg_cadence']} spm")
        print(f"{'='*50}\n")

if __name__ == "__main__":
    main()


path: scripts/calculate_pace.py
content: #!/usr/bin/env python3
"""
跑步配速和心率区间计算器
基于 Jack Daniels VDOT 理论和极化训练原则
"""

import argparse
import json
import sys

def vdot_to_pace(vdot, distance_km=42.195):
    """
    根据 VDOT 值计算各配速区间
    
    基于 Daniels VDOT 表（全马）:
    VDOT 40: E=5:47, M=5:12, T=4:51, I=4:24, R=4:04
    VDOT 44: E=5:29, M=4:56, T=4:37, I=4:11, R=3:52
    VDOT 50: E=5:05, M=4:35, T=4:19, I=3:54, R=3:36
    VDOT 54: E=4:51, M=4:23, T=4:08, I=3:44, R=3:26
    """
    # VDOT 到配速的近似公式（基于 Daniels 表线性回归）
    pace_easy = -2.5 * vdot + 440      # 轻松跑配速 (秒/km)
    pace_marp = -2.8 * vdot + 445      # 马拉松配速
    pace_threshold = -3.0 * vdot + 450 # 阈值配速
    pace_interval = -3.5 * vdot + 480  # 间歇配速
    pace_rep = -3.8 * vdot + 500       # 重复跑配速
    
    return {
        "easy": {"min": pace_easy, "max": pace_easy + 30},
        "marathon": {"min": pace_marp - 10, "max": pace_marp + 10},
        "threshold": {"min": pace_threshold - 5, "max": pace_threshold + 5},
        "interval": {"min": pace_interval - 5, "max": pace_interval + 5},
        "repetition": {"min": pace_rep - 5, "max": pace_rep + 5}
    }

def pace_to_vdot(race_time_min, distance_km):
    """
    根据比赛成绩反推 VDOT
    
    race_time_min: 比赛时间（分钟）
    distance_km: 比赛距离（公里）
    
    基于 Daniels VDOT 表线性回归：
    - 全马 3:00 (4:16/km) → VDOT ≈ 54
    - 全马 3:30 (5:00/km) → VDOT ≈ 46
    - 全马 3:38 (5:11/km) → VDOT ≈ 44
    - 全马 4:00 (5:41/km) → VDOT ≈ 40
    """
    pace_min_per_km = race_time_min / distance_km
    
    # 基于 Daniels 表的经验公式（线性回归）
    if distance_km == 42.195:  # 全马
        vdot = -9.93 * pace_min_per_km + 96.4
    elif distance_km == 21.0975:  # 半马
        vdot = -9.5 * pace_min_per_km + 93
    elif distance_km == 10:  # 10K
        vdot = -9.0 * pace_min_per_km + 90
    else:
        vdot = -9.5 * pace_min_per_km + 93
    
    return max(30, min(85, vdot))  # 限制在合理范围

def calculate_heart_rate_zones(max_hr, resting_hr=None):
    """
    计算心率区间
    
    方法 1: 最大心率百分比法
    方法 2: 储备心率法 (Karvonen) - 更准确
    """
    zones = {}
    
    if resting_hr:
        # 储备心率法 (更准确)
        hrr = max_hr - resting_hr
        zones = {
            "Z1": {"min": int(resting_hr + hrr * 0.5), "max": int(resting_hr + hrr * 0.6), "name": "恢复区"},
            "Z2": {"min": int(resting_hr + hrr * 0.6), "max": int(resting_hr + hrr * 0.7), "name": "有氧区"},
            "Z3": {"min": int(resting_hr + hrr * 0.7), "max": int(resting_hr + hrr * 0.8), "name": " tempo 区"},
            "Z4": {"min": int(resting_hr + hrr * 0.8), "max": int(resting_hr + hrr * 0.9), "name": "阈值区"},
            "Z5": {"min": int(resting_hr + hrr * 0.9), "max": int(max_hr), "name": "最大摄氧区"}
        }
    else:
        # 最大心率百分比法
        zones = {
            "Z1": {"min": int(max_hr * 0.5), "max": int(max_hr * 0.6), "name": "恢复区"},
            "Z2": {"min": int(max_hr * 0.6), "max": int(max_hr * 0.7), "name": "有氧区"},
            "Z3": {"min": int(max_hr * 0.7), "max": int(max_hr * 0.8), "name": "tempo 区"},
            "Z4": {"min": int(max_hr * 0.8), "max": int(max_hr * 0.9), "name": "阈值区"},
            "Z5": {"min": int(max_hr * 0.9), "max": int(max_hr), "name": "最大摄氧区"}
        }
    
    return zones

def sec_to_pace_str(seconds):
    """秒/km 转换为 分：秒/km 格式"""
    mins = int(seconds // 60)
    secs = int(seconds % 60)
    return f"{mins}:{secs:02d}"

def main():
    parser = argparse.ArgumentParser(description="跑步配速和心率区间计算器")
    parser.add_argument("--vdot", type=float, help="VDOT 值")
    parser.add_argument("--race-time", type=float, help="比赛时间 (分钟)")
    parser.add_argument("--distance", type=float, default=42.195, help="比赛距离 (公里)")
    parser.add_argument("--max-hr", type=int, help="最大心率")
    parser.add_argument("--resting-hr", type=int, help="静息心率")
    parser.add_argument("--output", choices=["json", "text"], default="text", help="输出格式")
    
    args = parser.parse_args()
    
    result = {}
    
    # 计算 VDOT
    if args.vdot:
        vdot = args.vdot
    elif args.race_time and args.distance:
        vdot = pace_to_vdot(args.race_time, args.distance)
    else:
        print("请提供 --vdot 或 --race-time + --distance")
        sys.exit(1)
    
    result["vdot"] = round(vdot, 1)
    
    # 计算配速区间
    pace_zones = vdot_to_pace(vdot)
    result["pace_zones"] = {}
    for zone, paces in pace_zones.items():
        result["pace_zones"][zone] = {
            "min": sec_to_pace_str(paces["min"]),
            "max": sec_to_pace_str(paces["max"])
        }
    
    # 计算心率区间
    if args.max_hr:
        result["heart_rate_zones"] = calculate_heart_rate_zones(args.max_hr, args.resting_hr)
    
    if args.output == "json":
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        print(f"\n{'='*50}")
        print(f"VDOT: {result['vdot']}")
        print(f"{'='*50}")
        print("\n配速区间:")
        for zone, paces in result["pace_zones"].items():
            print(f"  {zone.upper()}: {paces['min']} - {paces['max']} /km")
        if "heart_rate_zones" in result:
            print("\n心率区间:")
            for zone, hr in result["heart_rate_zones"].items():
                print(f"  {zone} ({hr['name']}): {hr['min']} - {hr['max']} bpm")
        print(f"{'='*50}\n")

if __name__ == "__main__":
    main()


path: references/training-zones.md
content: # 训练区间定义

基于 Jack Daniels VDOT 理论和极化训练原则

---

## 配速区间

| 区间 | 名称 | 强度 | 用途 | 心率区间 |
|------|------|------|------|----------|
| E | Easy/轻松跑 | 低 | 有氧基础、恢复 | Z1-Z2 |
| M | Marathon/马拉松配速 | 中 | 比赛配速适应 | Z3 |
| T | Threshold/阈值跑 | 中高 | 乳酸阈值提升 | Z3-Z4 |
| I | Interval/间歇跑 | 高 | VO2max 提升 | Z4-Z5 |
| R | Repetition/重复跑 | 很高 | 跑步经济性、速度 | Z5 |

---

## VDOT 配速对照表（部分）

| VDOT | E 配速 | M 配速 | T 配速 | I 配速 | R 配速 | 全马预测 |
|------|--------|--------|--------|--------|--------|----------|
| 40 | 5:47 | 5:12 | 4:51 | 4:24 | 4:04 | 4:00 |
| 42 | 5:38 | 5:04 | 4:44 | 4:17 | 3:58 | 3:50 |
| 44 | 5:29 | 4:56 | 4:37 | 4:11 | 3:52 | 3:41 |
| 46 | 5:21 | 4:49 | 4:31 | 4:05 | 3:46 | 3:32 |
| 48 | 5:13 | 4:42 | 4:25 | 3:59 | 3:41 | 3:24 |
| 50 | 5:05 | 4:35 | 4:19 | 3:54 | 3:36 | 3:16 |
| 52 | 4:58 | 4:29 | 4:13 | 3:49 | 3:31 | 3:09 |
| 54 | 4:51 | 4:23 | 4:08 | 3:44 | 3:26 | 3:02 |
| 56 | 4:44 | 4:17 | 4:03 | 3:39 | 3:22 | 2:56 |

---

## 心率区间

### 储备心率法（Karvonen）

| 区间 | 名称 | % 储备心率 | 用途 |
|------|------|------------|------|
| Z1 | 恢复区 | 50-60% | 恢复跑、热身 |
| Z2 | 有氧区 | 60-70% | 轻松跑、长距离 |
| Z3 | Tempo 区 | 70-80% | 节奏跑、马拉松配速 |
| Z4 | 阈值区 | 80-90% | 阈值跑、部分间歇 |
| Z5 | 最大摄氧区 | 90-100% | 间歇跑、重复跑 |

**计算公式：**
```
储备心率 (HRR) = 最大心率 - 静息心率
目标心率 = 静息心率 + HRR × 强度%
```

### 最大心率估测

```
最大心率 = 208 - 0.7 × 年龄  (Tanaka 公式，更准确)
最大心率 = 220 - 年龄  (传统公式，误差较大)
```

---

## 极化训练原则（80/20 法则）

| 强度 | 占比 | 心率区间 |
|------|------|----------|
| 低强度 | 80% | Z1-Z2（低于阈值） |
| 高强度 | 20% | Z4-Z5（高于阈值） |

**核心理念：**
- 轻松跑要真正轻松（能说话）
- 强度课要真正够强度
- 避免"垃圾跑量"（中等强度、不够快也不够慢）

---

## 训练负荷监控

### 周跑量递增原则

- 每周增幅不超过 10%
- 每 3-4 周安排一个减量周（减少 20-30%）
- 赛前 2-3 周开始减量

### TRIMP (Training Impulse)

```
TRIMP = 训练时长 (分钟) × 心率系数

心率系数:
Z1: 0.5
Z2: 1.0
Z3: 1.5
Z4: 2.0
Z5: 2.5
```

### 急性：慢性负荷比 (ACWR)

- 急性负荷：过去 7 天平均
- 慢性负荷：过去 28 天平均
- 理想比值：0.8-1.3
- >1.5 表示受伤风险增加

---

## 周期化训练

| 阶段 | 时长 | 重点 | 跑量占比 |
|------|------|------|----------|
| 基础期 | 6-8 周 | 有氧基础、力量 | 逐渐增加 |
| 强化期 | 6-8 周 | 阈值、间歇 | 维持高位 |
| 巅峰期 | 4-6 周 | 马拉松配速、最长 LSD | 峰值后略降 |
| 减量期 | 2-3 周 | 恢复、保持状态 | 减少 40-60% |


path: references/workout-types.md
content: # 训练课类型详解

---

## 轻松跑 (Easy Run)

**目的：** 建立有氧基础、促进恢复

**强度：** 
- 配速：E 配速（比 M 配速慢 30-45 秒/km）
- 心率：Z1-Z2（能轻松说话）

**时长：** 30-90 分钟

**要点：**
- 不要跑太快！这是最常见的错误
- 关注心率而非配速
- 可以作为强度课后的恢复

---

## 长距离跑 (Long Slow Distance - LSD)

**目的：** 提升耐力、脂肪代谢能力、心理韧性

**强度：**
- 配速：E 配速或比 E 配速慢 15-30 秒/km
- 心率：Z2

**时长：** 90 分钟 -3 小时（或 25-35km）

**要点：**
- 前 2/3 轻松，最后 1/3 可加入马拉松配速
- 补给练习（胶、盐丸、水）
- 装备测试（鞋子、衣服）
- 不需要追求配速，完成距离最重要

**进阶变体：**
- Progressive LSD：逐渐加速
- Fast Finish LSD：最后 5-8km 马拉松配速
- Back-to-Back：周六中长距离 + 周日中等距离

---

## 马拉松配速跑 (Marathon Pace Run)

**目的：** 适应比赛配速、建立配速感、提升乳酸阈值

**强度：**
- 配速：目标马拉松配速（M 配速）
- 心率：Z3

**常见课表：**
- 热身 3km + MP 10-15km + 冷身 2km
- LSD 后段 10-15km MP
- 长距离中间段 15-20km MP

**要点：**
- 不要跑快于目标配速
- 练习比赛日补给策略
- 模拟比赛日装备和早餐

---

## 阈值跑 (Threshold Run / Tempo Run)

**目的：** 提升乳酸阈值、提高维持高速的能力

**强度：**
- 配速：T 配速（ comfortably hard，能勉强说短句）
- 心率：Z3-Z4 边界

**常见课表：**
- 经典阈值：热身 2km + 阈值 6-10km + 冷身 2km
- 巡航间歇：4-6 × 1km@T 配速，休息 1 分钟
- 甜区：热身 2km + 20-25 分钟@T 配速 + 冷身 2km

**要点：**
- 保持"舒适的辛苦"感觉
- 不要跑成间歇跑
- 重点是持续输出，不是速度

---

## 间歇跑 (Interval Training)

**目的：** 提升 VO2max、跑步经济性

**强度：**
- 配速：I 配速（3K-5K 比赛配速）
- 心率：Z4-Z5

**常见课表：**
- 经典间歇：6-8 × 800m@I 配速，休息 400m 慢跑
- 1000m 间歇：5-6 × 1000m@I 配速，休息 2-3 分钟
- Yasso 800s：10 × 800m，目标时间（分：秒）≈ 全马目标时间（小时：分钟）

**要点：**
- 间歇时间要足够恢复（心率降到 120-130）
- 每组配速保持一致
- 质量 > 数量

---

## 重复跑 (Repetition Training)

**目的：** 提升跑步经济性、最大速度、步频

**强度：**
- 配速：R 配速（比 I 配速更快）
- 心率：Z5

**常见课表：**
- 短距离重复：8-12 × 200m@R 配速，休息 200m 慢跑
- 山坡重复：8-10 × 30-60 秒上坡冲刺，慢跑下坡恢复
- 阶梯跑：400m-800m-1200m-800m-400m，休息相等时间

**要点：**
- 关注跑姿和步频
- 充分恢复（完全恢复再进行下一组）
- 适合放在基础期后

---

## 恢复跑 (Recovery Run)

**目的：** 促进恢复、保持运动习惯

**强度：**
- 配速：比 E 配速慢 30-60 秒/km
- 心率：Z1

**时长：** 20-45 分钟

**要点：**
- 越慢越好，不要有压力
- 可以在强度课第二天进行
- 如果感觉疲劳，可以改为休息或交叉训练

---

## 力量训练（跑者专用）

**目的：** 预防伤病、提升跑步经济性

**推荐动作：**

**核心：**
- 平板支撑：3 × 60 秒
- 侧平板：3 × 45 秒/侧
- 死虫式：3 × 12 次/侧

**下肢：**
- 深蹲：3 × 15 次
- 弓步蹲：3 × 12 次/腿
- 单腿硬拉：3 × 10 次/腿
- 提踵：3 × 20 次

**臀部：**
- 臀桥：3 × 15 次
- 蚌式开合：3 × 15 次/侧
- 侧卧抬腿：3 × 15 次/侧

**频率：** 每周 2-3 次，可与轻松跑同天或休息日进行

---

## 典型周训练结构

### 破 3 训练者（周跑量 350-400km）

| 星期 | 训练内容 | 跑量 | 强度 |
|------|----------|------|------|
| 一 | 休息/力量 | - | - |
| 二 | 间歇跑 | 12-15km | I/R |
| 三 | 轻松跑 | 10-12km | E |
| 四 | 阈值跑 | 12-15km | T |
| 五 | 休息/力量 | - | - |
| 六 | 长距离 | 25-32km | E + M |
| 日 | 恢复跑 | 8-10km | E |

### 初级马拉松训练者（周跑量 200-250km）

| 星期 | 训练内容 | 跑量 | 强度 |
|------|----------|------|------|
| 一 | 休息 | - | - |
| 二 | 轻松跑 | 8-10km | E |
| 三 | 节奏跑 | 8-10km | T |
| 四 | 轻松跑 | 8-10km | E |
| 五 | 休息 | - | - |
| 六 | 长距离 | 16-25km | E |
| 日 | 恢复跑 | 6-8km | E |


path: references/race-strategies.md
content: # 比赛策略

---

## 配速策略

### 1. 匀速策略（推荐）

**最适合：** 大多数跑者，尤其是破 3 目标

**执行方式：**
- 全程保持目标配速（±5 秒/km）
- 前 5km 略慢 5-10 秒/km（抑制起跑冲动）
- 5-35km 稳定在目标配速
- 35km 后根据状态调整

**优点：**
- 能量利用最高效
- 避免后程崩盘
- 心理压力小

---

### 2. 负分割策略（Negative Split）

**最适合：** 有经验跑者、追求 PB

**执行方式：**
- 前 10km：比目标配速慢 5-10 秒/km
- 10-30km：目标配速
- 30-40km：维持或略快
- 最后 2km：全力冲刺

**优点：**
- 后程超越心理优势大
- 降低撞车风险
- 整体体验更好

---

### 3. 正分割策略（不推荐）

**常见错误：** 前快后慢

**结果：**
- 前 10km 感觉很好
- 20-30km 开始挣扎
- 30km 后严重掉速
- 最终成绩远低于预期

**避免方法：**
- 起跑前告诉自己"慢就是快"
- 前 5km 看心率不看配速
- 找配速员或兔子跟随

---

## 补给策略

### 赛前

| 时间 | 内容 |
|------|------|
| 赛前 3 天 | 碳水负荷（碳水占比 70%） |
| 赛前 1 天 | 清淡饮食，避免高纤维 |
| 赛前 2-3h | 早餐（面包 + 香蕉 + 咖啡） |
| 赛前 30min | 能量胶 1 支 + 水 |

### 赛中

| 距离 | 补给 |
|------|------|
| 每 8-10km | 能量胶 1 支 |
| 每个水站 | 少量水或运动饮料 |
| 20km 后 | 考虑盐丸（天气热时） |
| 30km 后 | 每 5km 胶 + 咖啡因胶（可选） |

**要点：**
- 赛前训练时练习补给
- 不要等饿了/渴了再吃
- 进站前提前减速，避免急停

---

## 心理策略

### 分段法

将 42km 分成若干段：

```
0-10km:  "热身段" - 抑制冲动，找节奏
10-21km: "巡航段" - 稳定配速，放松心态
21-30km: "坚持段" - 专注当下，不想剩余距离
30-35km: "艰难段" - 接受痛苦，分解目标
35-40km: "冲刺段" - 看到终点，激发斗志
40-42km: "荣耀段" - 享受最后，全力冲刺
```

### 积极自我对话

**避免：**
- "还有好多公里..."
- "我坚持不住了..."
- "今天状态不好..."

**替换为：**
- "按计划执行"
- "这种感觉正常"
- "训练就是为了这一刻"

---

## 常见问题应对

### 撞墙（30-35km）

**症状：** 双腿沉重、配速下降、想放弃

**应对：**
1. 接受现实，不要对抗
2. 降低配速 10-15 秒/km
3. 补充能量胶 + 水
4. 缩短目标（"再跑 1km 就好"）
5. 关注跑姿（抬腿、摆臂）

### 侧腹痛

**应对：**
1. 减速但不走路
2. 深呼吸（腹式呼吸）
3. 按压疼痛部位
4. 调整呼吸节奏（两步一吸、两步一呼）

### 抽筋

**预防：**
- 赛前充分热身
- 赛中补充电解质
- 平时加强力量训练

**应对：**
1. 减速或步行
2. 拉伸抽筋部位
3. 补充盐丸
4. 如严重，考虑退赛

---

## 破 3 配速表

| 目标 | 配速 | 5km 点 | 10km 点 | 半马点 | 30km 点 | 终点 |
|------|------|--------|---------|--------|---------|------|
| 3:00 | 4:15 | 21:15 | 42:30 | 1:29:45 | 2:07:30 | 3:00:00 |
| 2:55 | 4:08 | 20:40 | 41:20 | 1:27:10 | 2:04:00 | 2:55:00 |
| 2:50 | 4:01 | 20:05 | 40:10 | 1:24:35 | 2:00:30 | 2:50:00 |

**建议：**
- 目标 3:00，按 2:58 训练（留余量）
- 前 5km 控制在 4:20-4:25
- 半马点在 1:29-1:30 之间

---

## 赛前一周安排

| 日期 | 训练 | 跑量 | 备注 |
|------|------|------|------|
| 周一 | 休息 | - | 充分休息 |
| 周二 | 短间歇 | 8km | 4×400m@MP，保持状态 |
| 周三 | 轻松跑 | 8km | Z2，放松 |
| 周四 | MP 跑 | 10km | 热身 3km + MP 5km + 冷身 2km |
| 周五 | 休息 | - | 储备糖原 |
| 周六 | 短 LSD | 12km | 最后 2km@MP，激活 |
| 周日 | 比赛日 | 42km | 执行计划 |

---

## 比赛日清单

**前一晚：**
- [ ] 号码布别好
- [ ] 芯片绑好
- [ ] 装备摆好（衣服、裤子、袜子、鞋子）
- [ ] 能量胶准备好（数量 + 备用）
- [ ] 手表充满电
- [ ] 闹钟设定

**早餐（赛前 2-3h）：**
- [ ] 面包/馒头
- [ ] 香蕉
- [ ] 咖啡/茶（习惯者）
- [ ] 水 500ml

**赛前 30 分钟：**
- [ ] 热身（慢跑 10 分钟 + 动态拉伸）
- [ ] 能量胶 1 支
- [ ] 上厕所
- [ ] 到起跑区

**起跑线：**
- [ ] 深呼吸，放松
- [ ] 提醒自己：前 5km 慢一点
- [ ] 享受比赛！


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