Cette page est affichée en anglais. Une traduction française est en cours.
SkillsApr 7, 2026·7 min de lecture

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

Claude Code


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

#!/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()

Discussion

Connectez-vous pour rejoindre la discussion.
Aucun commentaire pour l'instant. Soyez le premier à partager votre avis.