2026年4月7日·6 分钟阅读

身体追踪 Skill 套件(多用户版 + 飞书卡片推送)

身体追踪 Skill:支持多用户自助 onboarding、飞书群聊饮食记录、AI 营养估算、飞书多维表格仪表盘同步、每日卡片推送。包含 body-track(日常记录)、body-track-dashboard(仪表盘同步)和 body_push.py(定时卡片推送脚本)。

SH
shiny.lucc · Community
快速使用

先拿来用,再决定要不要深挖

这里应该同时让用户和 Agent 知道第一步该复制什么、安装什么、落到哪里。


name: body-track description: 多用户身体追踪。用户报告吃了什么、发食物照片、问某个食物热量、汇报运动/手表数据、或想查看饮食记录时触发。首次使用自动进入 onboarding 收集身体数据。 argument-hint: [口述吃了什么/照片路径/食物热量查询/setup,或留空] allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent

身体追踪 · 饮食运动记录(多用户版)

概述

这是一个可以分享给其他人使用的身体追踪 skill。每个用户把这个文件夹复制到自己的 ~/.claude/skills/body-track/,首次使用时会自动引导设置。

数据目录

所有数据存放在 ~/body-track/

~/body-track/
  profile.json        # 用户配置(身体数据、目标、飞书信息)
  W{周数}_{日期}.md    # 每周追踪文件
  {年}-{月}_月报.md    # 月报文件
  {MMDD}/              # 食物照片文件夹

第一步:检查是否已完成 Onboarding

每次触发时先检查 ~/body-track/profile.json 是否存在。

  • 如果存在:读取 profile,直接进入「日常记录流程」
  • 如果不存在:进入「Onboarding 流程」

Onboarding 流程(首次使用)

1. 检查飞书环境

先确认用户的 lark-cli 是否可用:

lark-cli auth status 2>&1
  • 如果未登录或未安装,告诉用户:
    1. 安装 lark-cli:npm i -g @anthropic-ai/lark-cli(或按实际安装方式)
    2. 配置飞书应用:lark-cli config init
    3. 登录:lark-cli auth login
    4. 需要的权限:im:message(读消息)、im:resource(下载图片)、bitable:app(多维表格)
  • 完成后再继续

2. 收集身体数据

通过对话逐步收集(必填项标 *):

项目 说明 用途
* 性别 男/女 BMR 计算
* 年龄 周岁 BMR 计算
* 身高 (cm) BMR 计算
* 体重 (kg) 当前体重 BMR 计算 + 追踪基线
体脂率 (%) 如有体测报告 更精确的目标设定
骨骼肌 (kg) 如有体测报告 增肌参考
健康状况 胰岛素抵抗、三高、甲状腺、PCOS 等 影响饮食建议方向
饮食偏好/禁忌 素食/清真/过敏/不吃什么 建议时避开
穿戴设备 有无手表/手环 是否有实际消耗数据
* 目标 减脂/增肌/维持/改善饮食 决定热量策略
减脂目标 (kg) 想减多少 计算减脂进度

一次性问完不友好,分 2-3 轮对话收集。先问基础(性别、年龄、身高、体重、目标),再问详情。

3. 计算个性化参数

用 Mifflin-St Jeor 公式:

  • 男:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 + 5
  • 女:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 - 161

默认 TDEE = BMR × 1.2(久坐)。如果有手表数据,用实际消耗。

热量目标:

  • 减脂:TDEE - 300 ~ 500 kcal(不低于 BMR)
  • 维持:TDEE
  • 增肌:TDEE + 200 ~ 300 kcal

蛋白质目标:体重 × 1.2 ~ 1.6 g(减脂取高值,维持取低值)

4. 收集飞书信息

问用户:

  1. 饮食记录群的 chat_id:用户在飞书哪个群记录饮食?让用户提供群的 chat_id。获取方式:
    # 搜索群聊
    lark-cli im +chat-list --query "群名关键词" --as user
  2. Webhook URL:让用户在群设置里添加自定义机器人,获取 webhook 地址(用于每日推送)

5. 创建飞书多维表格

用 lark-cli 自动创建:

# 在用户的云空间里创建多维表格
lark-cli base +create --name "身体追踪" --as user

创建 4 张数据表(参考字段设计,用 lark-cli base 命令):

表1: 每日汇总

  • 日期(日期)、摄入kcal(数字)、蛋白质g(数字)、碳水g(数字)、脂肪g(数字)
  • 消耗kcal(数字)、热量差(数字)、体重(数字)

表2: 饮食明细

  • 日期(日期)、餐次(单选:早餐/午餐/晚餐/加餐)、食物(文本)、份量(文本)
  • 热量(数字)、蛋白质(数字)、脂肪(数字)、碳水(数字)

表3: 运动记录

  • 日期(日期)、类别(单选:有氧/无氧)、运动名称(文本)、时长min(数字)、消耗kcal(数字)

表4: AI建议

  • 分类(单选:总评/蛋白质/微量元素/运动/饮食推荐/减脂进度)、内容(文本)、数据依据(文本)、优先级(单选:高/中/低)

创建完成后记录 base_token 和各 table_id。

6. 保存 profile.json

mkdir -p ~/body-track

写入 ~/body-track/profile.json

{
  "name": "用户名",
  "chat_id": "oc_xxx",
  "base_token": "xxx",
  "table_ids": {
    "daily": "tbl...",
    "detail": "tbl...",
    "exercise": "tbl...",
    "advice": "tbl..."
  },
  "webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
  "body": {
    "gender": "female",
    "age": 30,
    "height_cm": 165,
    "weight_kg": 60,
    "body_fat_pct": null,
    "skeletal_muscle_kg": null,
    "conditions": [],
    "diet_prefs": [],
    "has_wearable": false
  },
  "targets": {
    "goal": "fat_loss",
    "target_weight_loss_kg": 5,
    "daily_kcal_min": 1400,
    "daily_kcal_max": 1600,
    "protein_g": 80,
    "bmr": 1300,
    "tdee": 1560
  },
  "created_at": "2026-04-07",
  "weight_start": 60,
  "exercise_goal": "3-4次/周,每次20-30分钟"
}

7. 设置每日推送

创建推送脚本 ~/body-track/body_push.py,逻辑:

  • 读取最新的 dashboard HTML,提取未达标建议
  • 每天 3 次轮播推送(早安/午间/晚间)
  • 用 webhook 发送飞书卡片消息
  • 卡片格式:标题栏 + 三列汇总指标 + 建议列表 + 运动提醒 + 累计赤字

推送脚本模板参考 ~/scripts/feishu-bot/body_push.py(如果存在的话),核心逻辑:

  • find_latest_dashboard() 找最新 dashboard HTML
  • collect_all_advice() 提取红黄标签的建议
  • extract_summary_metrics() 提取汇总指标
  • extract_exercise_summary() 从周报提取运动数据
  • build_card() 构建飞书卡片 JSON
  • send_webhook() 发送到用户的 webhook

推送脚本要从 ~/body-track/profile.json 读取 webhook_url 和 body_dir 路径。

设置定时任务(macOS launchd):

# 创建 plist 文件
cat > ~/Library/LaunchAgents/com.bodytrack.push.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.bodytrack.push</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/python3</string>
        <string>$HOME/body-track/body_push.py</string>
    </array>
    <key>StartCalendarInterval</key>
    <array>
        <dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>0</integer></dict>
        <dict><key>Hour</key><integer>12</integer><key>Minute</key><integer>30</integer></dict>
        <dict><key>Hour</key><integer>19</integer><key>Minute</key><integer>0</integer></dict>
    </array>
    <key>StandardOutPath</key>
    <string>$HOME/body-track/push.log</string>
    <key>StandardErrorPath</key>
    <string>$HOME/body-track/push.log</string>
</dict>
</plist>
EOF

# 加载
launchctl load ~/Library/LaunchAgents/com.bodytrack.push.plist

8. Onboarding 完成

告诉用户:

  • 数据目录:~/body-track/
  • 飞书看板:提供多维表格链接
  • 每天 3 次推送已设置(8:00 / 12:30 / 19:00)
  • 以后直接在群里发食物照片和口述,然后用 /body-track 来同步记录

日常记录流程

读取 ~/body-track/profile.json 获取用户配置后:

数据来源:飞书对话

从用户的飞书群聊拉取消息:

lark-cli im +chat-messages-list --chat-id {profile.chat_id} \
  --start "YYYY-MM-DDT00:00:00+08:00" --end "YYYY-MM-DDT23:59:59+08:00" \
  --sort asc --as user --format json

下载食物照片

cd /tmp && lark-cli im +messages-resources-download \
  --message-id om_xxx --file-key img_xxx --type image --as user \
  --output food_MMDD_序号.png

下载后用 Read 工具查看图片,识别食物。照片归档到 ~/body-track/{MMDD}/

处理照片

  • HEIC 先转换:sips -s format jpeg -Z 800 原文件 --out /tmp/food_photos/输出.jpg
  • 看照片时注意:手机作为尺寸参照物、吃前吃后对比判断实际吃了多少
  • 如有营养标签,直接读取标签数据计算

营养估算

必须有份量依据(重量/个数/比例),不能凭"一碗""一杯"模糊估算。

每种食物估算以下营养素:

营养素 说明
热量 (kcal) 总热量
蛋白质 (g)
碳水化合物 (g) 其中标注糖的含量
脂肪 (g) 其中标注饱和脂肪
膳食纤维 (g)
钠 (mg)
钙 (mg)
铁 (mg)
钾 (mg)
维生素C (mg) 如有显著来源
维生素A (μg) 如有显著来源

每餐汇总展示:热量 / 蛋白质 / 碳水 / 脂肪 四大项 + 值得注意的微量元素。

更新追踪文件

读取 profile.json 中的目标数据来做对比。

周文件(详细记录):

  • 命名:W{周数}_{起始日期}-{结束日期}.md(周一到周日,ISO 周数)
  • 每天记录:各餐内容+份量+热量+蛋白质+碳水+脂肪、消耗、热量差、体重
  • 消耗:有手表数据用手表,无手表用 profile.targets.tdee
  • 先读已有文件看格式,保持一致
  • 如果当周文件不存在,新建(格式参考已有周文件或模板)

周文件模板(新建时使用):

# 第{周数}周 · {月}月{日}日 - {月}月{日}日

## 本周目标
- 热量摄入:{daily_kcal_min}-{daily_kcal_max} kcal/天
- 蛋白质:≥ {protein_g}g/天
- 运动:{exercise_goal}

---

## 周一 M/DD
| | 内容 | 份量 | 热量 | 蛋白质 | 脂肪 | 碳水 |
|---|---|---|---|---|---|---|
| 早餐 | ... | ... | ... | ... | ... | ... |
| **摄入合计** | | | **X kcal** | **Xg** | **Xg** | **Xg** |

| | 消耗 |
|---|---|
| 静息+活动 | {tdee} kcal |
| **消耗合计** | **{tdee} kcal** |

| 热量差 | 体重 | 能量值 |
|---|---|---|
| **X kcal** | — | /5 |

月报文件(趋势总览):

  • 命名:{年}-{月}_月报.md
  • 包含:每日数据表、周均趋势对比、关键发现
  • 每次更新周文件时同步更新月报

回复用户

  • 当餐/当天的 热量 / 蛋白质 / 碳水 / 脂肪
  • 如果当天还没吃完,告诉剩余预算(基于 profile 中的目标)
  • 指出高热量低蛋白、高碳水、高钠的项目,但不说教不安慰
  • 用户问某食物热量就直接回答数字,加上当天累计
  • 如果用户有特殊健康状况(如胰岛素抵抗),相关建议只在用户问时提

能量和运动探测

每次交互时简短问一句:"今天能量怎么样?1-5"

  • 如果能量 ≥ 3,问一句"想动一下吗?"
  • 不强推,用户说不想就不追问
  • 记录能量值和运动数据到周文件

注意事项

  • 如实记录,不要为了让用户感觉好而低估热量
  • 用户的所有个性化数据从 profile.json 读取,不要硬编码
  • 保持简单直接
  • 估算要诚实,不美化不安慰

$ARGUMENTS


name: body-track-dashboard description: 多用户身体追踪仪表盘。当用户说"更新仪表盘"、"同步看板"、"sync dashboard"、"更新飞书"时触发。同步身体数据到飞书多维表格并生成AI建议。 argument-hint: [留空自动同步,或指定日期范围] allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent

身体追踪仪表盘同步(多用户版)

前置:读取用户配置

cat ~/body-track/profile.json

如果文件不存在,告诉用户先运行 /body-track setup 完成初始配置。

从 profile.json 获取:

  • base_token — 飞书多维表格 token
  • table_ids.daily — 每日汇总表
  • table_ids.detail — 饮食明细表
  • table_ids.exercise — 运动记录表
  • table_ids.advice — AI建议表
  • webhook_url — 推送 webhook
  • targets.* — 用户目标(热量、蛋白质、减脂目标等)
  • body.* — 用户身体数据(用于 AI 建议)

同步流程

第一步:读取本地数据

  1. 读取 ~/body-track/W{周数}_{日期范围}.md 获取最新饮食数据
  2. 确定哪些天的数据需要同步(对比飞书已有记录)

第二步:同步到飞书

用 lark-cli 命令同步数据:

# 插入/更新每日汇总
lark-cli base +record-upsert --base-token {base_token} \
  --table-id {table_ids.daily} --as user \
  --json '{"日期": <毫秒时间戳>, "摄入kcal": <值>, "蛋白质g": <值>, "碳水g": <值>, "脂肪g": <值>, "消耗kcal": <值>, "热量差": <值>, "体重": <值>}'

# 插入饮食明细
lark-cli base +record-upsert --base-token {base_token} \
  --table-id {table_ids.detail} --as user \
  --json '{"日期": <时间戳>, "餐次": "早餐", "食物": "xxx", "份量": "xxx", "热量": <值>, "蛋白质": <值>, "脂肪": <值>, "碳水": <值>}'

# 插入运动记录
lark-cli base +record-upsert --base-token {base_token} \
  --table-id {table_ids.exercise} --as user \
  --json '{"日期": <时间戳>, "运动名称": "散步", "类别": "有氧", "时长min": 20, "消耗kcal": 60}'

日期时间戳:北京时间当天 00:00 的 Unix 毫秒时间戳。

第三步:生成 AI 建议

基于所有已有数据,结合用户 profile 中的身体数据和目标生成建议。

数据来源

  • 饮食记录(热量/宏量/微量营养素趋势)
  • 用户身体数据(从 profile.body 读取)
  • 用户目标(从 profile.targets 读取)

建议分类(每类一条记录写入 AI建议表):

  1. 总评:整体趋势、减脂进度、关键发现
  2. 蛋白质:达标率、规律分析、具体改善方案
  3. 微量元素:严重缺乏项、补充建议
  4. 运动:有氧/无氧推荐、实际完成度、目标对比
  5. 饮食推荐:高营养密度食物、需要避免的模式
    • 如果用户有饮食偏好/禁忌(profile.body.diet_prefs),建议时要避开
  6. 减脂进度(如果用户目标是减脂):
    • 1kg 脂肪 ≈ 7700 kcal
    • 累计赤字 / 7700 = 已减脂肪(kg)
    • 已减 / target_weight_loss_kg × 100% = 完成度

健康状况相关建议

  • 如果 profile.body.conditions 包含特殊健康状况(如胰岛素抵抗、PCOS、甲状腺等),在建议中考虑相关饮食原则
  • 但不要过度强调,保持建议的实用性

第四步:更新AI建议到飞书

先删除旧的AI建议记录,再插入新的:

# 列出旧记录
lark-cli base +record-list --base-token {base_token} \
  --table-id {table_ids.advice} --as user

# 删除旧记录
lark-cli base +record-delete --base-token {base_token} \
  --table-id {table_ids.advice} --record-id <id> --as user

# 插入新建议
lark-cli base +record-upsert --base-token {base_token} \
  --table-id {table_ids.advice} --as user \
  --json '{"分类": "总评", "内容": "...", "数据依据": "...", "优先级": "高"}'

第五步:生成本地 Dashboard HTML

~/body-track/ 下生成周看板 HTML 文件(W{周数}_dashboard.html),用于:

  1. 本地浏览查看可视化图表
  2. 每日推送脚本从中提取建议

Dashboard 包含:

  • 顶部汇总卡片:日均摄入(与目标对比)、日均蛋白质(与目标对比)、日均赤字、累计赤字
    • 状态标记:good(达标绿)/ warn(接近黄)/ bad(差距大红)/ neutral(信息蓝)
  • 图表区域:每日摄入vs消耗折线图、宏量营养素柱状图、营养素占比饼图、蛋白质达标柱状图、热量赤字趋势
  • 每日卡片:每天的摄入/消耗/赤字/蛋白质/脂肪/碳水
  • Insight 区域:宏量趋势分析(用 tag 标签标记 green/yellow/red)
  • 微量营养素区域:各微量元素达标率卡片

HTML 使用 Chart.js CDN,暗色主题,响应式布局。具体样式参考已有的 dashboard HTML 文件格式(如果 ~/body-track/ 下有之前生成的)。

Insight 标签规则:

  • <span class="tag green"> — 达标或良好
  • <span class="tag yellow"> — 接近目标但未达标
  • <span class="tag red"> — 严重不足或超标

重要:推送脚本依赖 insight-box 中的 tag 颜色来决定推送内容(跳过 green,推送 yellow 和 red),所以 HTML 格式必须保持一致。

第六步:回复用户

简要汇报:

  • 同步了哪些天的数据
  • 减脂进度(如适用):已减 X kg / 目标 Y kg = Z%
  • 本周关键建议(1-2条最重要的)
  • 提供飞书看板链接

注意事项

  • 日期时间戳用北京时间 00:00 的毫秒时间戳
  • 微量元素是估算值,标注"估算"
  • AI建议要有数据依据,不空谈
  • 所有数据都从 profile.json 读取,不要硬编码任何人的身体数据
  • 默认总消耗(无手表数据时)= profile.targets.tdee
  • 建议简短直接

$ARGUMENTS

body_push.py

View source (329 lines)
#!/usr/bin/env python3
"""
每日身体 AI 建议推送(卡片版 + 周期追踪)
每天推 3 次,每次挑不同的建议 + 吃药提醒 + 周期状态。
用法: python3 body_push.py [0|1|2]  — 第几次推送(默认自动判断)
"""

import json
import os
import re
import sys
import glob
import html
import urllib.request
from datetime import date, timedelta

WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/5dd37bb4-0e98-4880-9b47-98c8c7823347"
BODY_DIR = os.path.expanduser("~/Desktop/12身体重启")
CYCLE_FILE = os.path.join(BODY_DIR, "cycle.json")
ITEMS_PER_PUSH = 3

HEADER_COLORS = ["blue", "turquoise", "violet"]
STATUS_COLOR = {"good": "green", "warn": "orange", "bad": "red", "neutral": "blue"}


# ─── 周期追踪 ───

def load_cycle():
    if os.path.exists(CYCLE_FILE):
        with open(CYCLE_FILE) as f:
            return json.load(f)
    return None


def cycle_status(cycle):
    if not cycle:
        return None

    start = date.fromisoformat(cycle["cycle_start"])
    today = date.today()
    day = (today - start).days + 1
    active = cycle.get("active_days", 21)
    brk = cycle.get("break_days", 7)
    total = active + brk

    if day > total:
        day = ((day - 1) % total) + 1

    break_start = start + timedelta(days=active)
    break_end = start + timedelta(days=total - 1)

    if day <= 7:
        pill = f"💊 优思明第{day}天/21天,记得吃药!"
        phase = "📍 服药期第一周(激素稳定期)"
        body_tip = "身体状态平稳,精力恢复中,适合正常饮食和运动"
    elif day <= 14:
        pill = f"💊 优思明第{day}天/21天,记得吃药!"
        phase = "📍 服药期第二周(黄金期)"
        body_tip = "状态最好的一周!运动表现好,蛋白质吸收效率高,适合加强度"
    elif day <= active:
        days_to_break = active - day
        pill = f"💊 优思明第{day}天/21天,记得吃药!"
        phase = f"📍 服药期第三周({break_start.month}/{break_start.day}进入停药期)"
        if days_to_break <= 3:
            body_tip = f"⚠️ 还有{days_to_break}天停药,可能已有PMS:情绪波动、腹胀、水肿、食欲增加。体重涨1-2kg是水分,不是胖了!"
        else:
            body_tip = "可能出现轻微水钠潴留,体重微涨属正常。注意补钾(香蕉/土豆),不要因为体重焦虑而节食"
    else:
        break_day = day - active
        next_start = break_end + timedelta(days=1)
        pill = f"💊 停药期第{break_day}天/{brk}天(不用吃优思明)"
        phase = f"📍 停药期({next_start.month}/{next_start.day}开新一轮)"
        if break_day <= 2:
            body_tip = "撤退性出血通常停药2-4天后出现。可能疲劳、情绪低,运动散步瑜伽即可,允许多吃100-200kcal"
        elif break_day <= 5:
            body_tip = "🩸 出血期,铁流失增加——多吃红肉/菠菜/配维C。运动量可以降低,不要硬撑"
        else:
            body_tip = f"出血接近尾声,{next_start.month}/{next_start.day}开始新一轮服药,记得准备好药"

    return {"day": day, "pill": pill, "phase": phase, "body_tip": body_tip}


# ─── 数据提取 ───

def find_latest_dashboard():
    pattern = os.path.join(BODY_DIR, "W*_dashboard.html")
    files = glob.glob(pattern)
    if not files:
        main = os.path.join(BODY_DIR, "dashboard.html")
        return main if os.path.exists(main) else None
    files.sort(key=os.path.getmtime)
    return files[-1]


def find_latest_weekly():
    pattern = os.path.join(BODY_DIR, "W*_0*.md")
    files = glob.glob(pattern)
    if not files:
        return None
    files.sort(key=os.path.getmtime)
    return files[-1]


def strip_html(text):
    text = html.unescape(text)
    return re.sub(r'<[^>]+>', '', text).strip()


def extract_summary_metrics(filepath):
    """从 dashboard 提取顶部汇总指标"""
    if not filepath:
        return {}
    with open(filepath) as f:
        content = f.read()

    metrics = {}
    for card in re.finditer(
        r'<div class="summary-card\s+(\w+)">\s*'
        r'<div class="label">(.+?)</div>\s*'
        r'<div class="value"[^>]*>(.+?)</div>\s*'
        r'<div class="unit">(.+?)</div>\s*'
        r'<div class="target">(.+?)</div>',
        content, re.DOTALL
    ):
        status, label, value, unit, target = [strip_html(g) for g in card.groups()]
        metrics[label] = {"value": value, "unit": unit, "target": target, "status": status}

    return metrics


def collect_all_advice(filepath):
    with open(filepath) as f:
        content = f.read()

    items = []
    for box in re.finditer(
        r'<div class="insight-box">\s*<h3>(.+?)</h3>\s*<ul>(.*?)</ul>\s*</div>',
        content, re.DOTALL
    ):
        for li in re.finditer(r'<li>(.*?)</li>', box.group(2), re.DOTALL):
            raw = li.group(1)
            if 'tag green' in raw:
                continue
            text = strip_html(raw)
            color = "red" if 'tag red' in raw else "yellow"
            items.append({"text": text, "color": color})

    return items


def extract_exercise_summary(filepath):
    with open(filepath) as f:
        content = f.read()

    today = date.today()
    year = today.year
    cutoff = today - timedelta(days=7)
    exercise_days = 0
    total_active = 0

    for section in re.split(r'(?=^## 周[一二三四五六日]\s+\d+/\d+)', content, flags=re.MULTILINE):
        header = re.match(r'## 周[一二三四五六日]\s+(\d+)/(\d+)', section)
        if not header:
            continue
        try:
            d = date(year, int(header.group(1)), int(header.group(2)))
        except ValueError:
            continue
        if d < cutoff or d > today:
            continue
        m = re.search(r'含(?:运动|活动)(\d+)', section)
        if m:
            exercise_days += 1
            total_active += int(m.group(1))

    goal = "3-4次/周"
    if exercise_days >= 3:
        return f"本周运动{exercise_days}次,活动消耗{total_active}kcal,继续保持!"
    elif exercise_days >= 1:
        return f"本周运动{exercise_days}次(目标{goal}),还差{3 - exercise_days}次,今天动一动?"
    else:
        return f"本周还没运动(目标{goal}),找20分钟动起来!"


def pick_items(all_items, push_index):
    if not all_items:
        return []
    n = len(all_items)
    day_offset = date.today().toordinal()
    start = (day_offset * 3 + push_index) * ITEMS_PER_PUSH % n
    return [all_items[(start + i) % n] for i in range(ITEMS_PER_PUSH)]


# ─── 卡片构建 ───

def build_card(cycle_info, metrics, diet_items, exercise_line, push_index):
    today = date.today()
    label = ["早安", "午间", "晚间"][push_index]
    elements = []

    # 周期状态
    if cycle_info:
        elements.append({
            "tag": "markdown",
            "content": f"{cycle_info['pill']}\n{cycle_info['phase']}\n{cycle_info['body_tip']}"
        })
        elements.append({"tag": "hr"})

    # 汇总指标(三列)
    keys_order = ["日均摄入", "日均蛋白质", "日均赤字"]
    columns = []
    for key in keys_order:
        m = metrics.get(key)
        if not m:
            continue
        color = STATUS_COLOR.get(m["status"], "blue")
        columns.append({
            "tag": "column",
            "width": "weighted",
            "weight": 1,
            "vertical_align": "center",
            "elements": [{
                "tag": "markdown",
                "content": f"**{key}**\n<font color='{color}'>{m['value']}</font> {m['unit']}\n{m['target']}"
            }]
        })
    if columns:
        elements.append({
            "tag": "column_set",
            "flex_mode": "bisect",
            "background_style": "grey",
            "columns": columns,
        })
        elements.append({"tag": "hr"})

    # 饮食建议
    if diet_items:
        elements.append({"tag": "markdown", "content": "**📋 今日建议**"})
        for item in diet_items:
            icon = "🔴" if item["color"] == "red" else "🟡"
            text = item["text"]
            parts = text.split(" ", 1)
            if len(parts) == 2 and len(parts[0]) <= 15:
                md = f"{icon} **{parts[0]}** — {parts[1]}"
            else:
                md = f"{icon} {text}"
            elements.append({"tag": "markdown", "content": md})
        elements.append({"tag": "hr"})

    # 运动
    if exercise_line:
        elements.append({"tag": "markdown", "content": f"🏃 {exercise_line}"})

    # 累计赤字
    cum = metrics.get("累计赤字")
    if cum:
        elements.append({
            "tag": "markdown",
            "content": f"📊 累计赤字 **{cum['value']}** {cum['unit']} | {cum['target']}"
        })

    return {
        "msg_type": "interactive",
        "card": {
            "header": {
                "template": HEADER_COLORS[push_index],
                "title": {"tag": "plain_text", "content": f"🤖 {label}提醒 · {today.month}/{today.day}"}
            },
            "elements": elements,
        }
    }


# ─── 发送 ───

def send_webhook(card_payload):
    payload = json.dumps(card_payload).encode()
    req = urllib.request.Request(
        WEBHOOK_URL, data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            result = json.loads(resp.read())
            print(f"[推送结果] {result}")
            return result
    except Exception as e:
        print(f"[推送失败] {e}")
        return None


def auto_push_index():
    from datetime import datetime
    h = datetime.now().hour
    if h < 11:
        return 0
    elif h < 16:
        return 1
    else:
        return 2


def main():
    push_index = int(sys.argv[1]) if len(sys.argv) > 1 else auto_push_index()

    cycle_info = cycle_status(load_cycle())

    dashboard = find_latest_dashboard()
    all_advice = collect_all_advice(dashboard) if dashboard else []
    metrics = extract_summary_metrics(dashboard)

    weekly = find_latest_weekly()
    exercise_line = extract_exercise_summary(weekly) if weekly else None

    diet_items = pick_items(all_advice, push_index)
    card = build_card(cycle_info, metrics, diet_items, exercise_line, push_index)

    # 日志
    for el in card["card"]["elements"]:
        if el.get("tag") == "markdown":
            print(el["content"])
    print()

    send_webhook(card)


if __name__ == "__main__":
    main()

讨论

登录后参与讨论。
还没有评论,来写第一条吧。