ScriptsApr 7, 2026·7 min read

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

Claude Code

Agent ready

Safe staging for this asset

This asset is staged first. The copied prompt tells the agent to inspect the staged files and ask before activating scripts, MCP config, or global config.

Stage only · 17/100Policy: stage
Agent surface
Any MCP/CLI agent
Kind
Script
Install
Stage only
Trust
Trust: Established
Entrypoint
SKILL.md
Safe staging command
npx -y tokrepo@latest install b6e3483c-90b8-4af2-9e82-231d5b95b78a --target codex

Stages files first; activation requires review of the staged README and plan.


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

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Related Assets