Skill: neverwarm/personal-health-router (version 1.2.1)

--- name: personal-health-router description: 帮助记录每餐摄入、睡眠质量以及运动锻炼恢复情况，并提供全自动飞书持久化与报表引擎。 Track meals, sleep quality, and workouts with automated Feishu persistence and reporting. Use when the user asks to log calories, analyze meal photos, interpret sleep or recovery screenshots, extract workout metrics, bootstrap health tables, or rebuild monthly/weekly health summaries and dashboards. --- # Health Copilot: Eating, Sleep, and Exercise Tracking Use this skill as the central hub for personal health tracking, analysis, and Feishu reporting. ## Overview This skill is for practical personal-health management, not medical diagnosis. It provides a unified layer for: - **Recognition**: Analyzing food photos, sleep screenshots, and workout summaries. - **Persistence**: Mapping and writing results into a structured Feishu Bitable. - **Reporting**: Rebuilding daily/weekly summary records and monthly dashboards. ## Role & Workflow 1. **Classify**: Identify if the request is about nutrition, sleep, exercise, or cross-domain reporting. 2. **Analyze**: Perform domain-specific extraction or analysis (conservative and evidence-based). 3. **Persist**: Map the results into the target Feishu schema (default English-only). 4. **Automate**: Run maintenance scripts for monthly/weekly summaries when requested. ## Routing branches ### 1. Nutrition - Trigger: `log calories`, `nutrition analysis`, meal photos, bento photos. - Reference: `references/nutrition.md`. - Behavior: Analyze photo -> Estimate nutrients -> Upsert to Nutrition Meals table. ### 2. Exercise - Trigger: `log workout`, `exercise analysis`, app screenshots (Garmin, Strava, Apple Health). - Reference: `references/exercise.md`. - Behavior: Analyze screenshot -> Extract metrics -> Assess load -> Upsert to Workouts table. ### 3. Sleep - Trigger: `log sleep`, `sleep analysis`, `recovery status`, app screenshots (AutoSleep, Oura, etc.). - Reference: `references/sleep.md`. - Behavior: Analyze screenshot -> Extract stages/HRV -> Upsert to Sleep Recovery table. ### 4. Cross-domain / Reporting - Trigger: `weekly health review`, `rebuild calendar`, `refresh dashboard`, `monthly summary`. - Reference: `references/cross-domain.md`. - Behavior: Rebuild Monthly Health Calendar -> Rebuild Weekly Health Assessment -> Refresh Dashboard. ## Universal rules - **Internal Schema**: Always target the **English-only** internal schema for persistence to ensure script reliability. - **Config-Driven**: Use `references/config-template.md` and `scripts/config_loader.py` for all environment variables. - **Conservative Analysis**: Extract only what is visible; do not fake precision. - **Separation**: Keep analysis facts separate from interpretive suggestions. ## Output rule Default response shape: 1. Conclusion (what was analyzed/updated) 2. Evidence (the data points) 3. Uncertainty (missing info) 4. Next step (action or report link) ## Script mapping - `scripts/bootstrap_health_tables.js`: Create tables/fields. - `scripts/rebuild_monthly_calendar.py`: Aggregate daily rows. - `scripts/rebuild_weekly_assessment.py`: Generate weekly reports. - `scripts/build_monthly_dashboard.js`: Create/refresh dashboards. ## Installation ```bash clawdhub install personal-health-router ```

path: README.md
content: # Health Copilot: Eating, Sleep, and Exercise Tracking / 健康副驾：饮食、睡眠与运动追踪

## Overview / 概述

**Health Copilot** is a comprehensive health-management skill that bridges the gap between raw data recognition and long-term reporting. It combines advanced analysis of meals, sleep, and exercise with automated Feishu (Lark) persistence and derived health assessments.

**Health Copilot（健康助理）** 是一个全链路健康管理 skill。它打通了从原始数据识别到长期报表分析的完整环节，将饮食、睡眠、运动的深度分析与飞书（Lark）自动落库及衍生健康评估引擎完美结合。

---

## Key capabilities / 关键能力

### 1. Recognition & Analysis / 识别与分析
- **Nutrition**: Photo-based calorie and macro estimation from meals.
- **Sleep**: Screenshot-based extraction of sleep stages and recovery signals (Apple Health, Android).
- **Exercise**: Extraction of workout metrics and training load from app screenshots (Garmin, Strava, etc.).
- **Weekly Reassessment**: Cross-domain interpretation of nutrition, sleep, and exercise signals.

### 2. Persistence & Automation / 持久化与自动化
- **Automated Bootstrapping**: Create all required Bitable tables, fields, and views from a config file.
- **Daily Rollups**: Automated aggregation of raw logs into a `Monthly Health Calendar`.
- **Weekly Assessments**: Automated generation of comprehensive weekly reports with risk scoring.
- **Dashboards**: Automated creation/refresh of monthly health overview dashboards with trends.

### 3. Architecture / 架构特性
- **English-Only Core**: Reliable internal schema using English field names for stable reporting scripts.
- **Bilingual documentation**: Full EN/ZH documentation for human users.
- **Config-Driven**: Customize any threshold, label, or wording without touching code.

---

## What it does / 主要功能

This skill handles four primary domains / 这个 skill 涵盖四大领域：

1.  **Nutrition / 营养**
    - Analyze photos -> Estimate nutrients -> Log to Feishu.
    - 拍照分析 -> 营养估算 -> 飞书落库。
2.  **Exercise / 运动**
    - Analyze screenshots -> Extract metrics -> Assess load -> Log to Feishu.
    - 截图分析 -> 指标提取 -> 负荷评估 -> 飞书落库。
3.  **Sleep / 睡眠**
    - Analyze screenshots -> Extract recovery/HRV -> Log to Feishu.
    - 截图分析 -> 恢复/HRV 提取 -> 飞书落库。
4.  **Cross-domain reporting / 跨域报表**
    - Rebuild monthly calendar -> Rebuild weekly assessment -> Refresh dashboard.
    - 重建月度日历 -> 重建周度评估 -> 刷新看板。

---

## Quick start / 快速开始

1.  **Install**: `clawdhub install personal-health-router`
2.  **Configure**: Create `config.json` based on `references/config-template.md`.
3.  **Bootstrap**: `node scripts/bootstrap_health_tables.js config.json`
4.  **Track**: Use the router to recognize food, sleep, or workouts.
5.  **Report**: Run the Python/Node scripts in `scripts/` to generate summaries and dashboards.

---

## Version note / 版本说明

**v1.0.0**: Major update consolidating the analysis router with the Feishu persistence engine. This version introduces an English-only internal table schema for enhanced reliability of reporting scripts.

**v1.0.0**: 重大更新，将分析路由与飞书持久化引擎深度融合。此版本引入了纯英文内部表结构，以提升自动化报表脚本的稳定性。

---

## Installation / 安装

```bash
clawdhub install personal-health-router
```

Update to the latest version / 更新到最新版本：

```bash
clawdhub update personal-health-router
```


path: references/config-template.md
content: # Config Template

Use a config file for every environment. Never hardcode private tokens or table IDs into scripts.

## Required shape

```json
{
  "feishu": {
    "active_base": {
      "token": "<base_token>",
      "url": "<optional_base_url>"
    },
    "tables": {
      "nutrition_meal": { "name": "Nutrition Meals", "table_id": "<optional_existing_id>" },
      "nutrition_daily_history": { "name": "Nutrition Daily History", "table_id": "<optional_existing_id>" },
      "sleep_recovery": { "name": "Sleep Recovery", "table_id": "<optional_existing_id>" },
      "exercise_workout": { "name": "Exercise Workouts", "table_id": "<optional_existing_id>" },
      "monthly_health_calendar": { "name": "Monthly Health Calendar", "table_id": "<optional_existing_id>" },
      "weekly_health_assessment": { "name": "Weekly Health Assessment", "table_id": "<optional_existing_id>" }
    }
  },
  "thresholds": {
    "protein_g": { "low": 60, "adequate": 80 },
    "fiber_g": { "low": 12, "adequate": 20 },
    "sodium_mg": { "caution": 1800, "high": 2500 },
    "calories_kcal": { "low": 1200, "high": 2200 },
    "sleep_score": { "low": 75, "good": 80 }
  },
  "weekly_thresholds": {
    "protein_low_ratio": 90,
    "severe_protein_low_ratio": 75,
    "fiber_low_ratio": 80,
    "sodium_high_ratio": 115,
    "sodium_very_high_ratio": 130,
    "energy_low_ratio": 85,
    "severe_energy_low_ratio": 70,
    "short_sleep_minutes": 420,
    "severe_short_sleep_minutes": 360,
    "low_sleep_score": 75,
    "good_sleep_score": 80,
    "weekly_activity_equiv_target": 150,
    "weekly_activity_equiv_high": 300
  }
}
```

## Optional schema override

If a deployment needs different field labels or additional fields, add:

```json
{
  "schemas": {
    "nutrition_meal": {
      "name": "Nutrition Meals",
      "fields": [
        { "name": "Logged At", "type": "datetime", "style": { "format": "yyyy-MM-dd HH:mm" } },
        { "name": "Food Description", "type": "text" }
      ],
      "views": []
    }
  }
}
```

If `schemas.<table_key>.fields` is present, the bootstrap script uses that override instead of the built-in default schema.

## Rules

- `table_id` may be empty before bootstrap.
- Bootstrap scripts should create missing tables, then print the created IDs so the config can be updated.
- Thresholds are defaults, not medical facts.
- If the user already has a Base, preserve their existing names and IDs.
- Use `node scripts/bootstrap_health_tables.js <config.json> --dry-run` to inspect the planned bootstrap without writing anything.

## Common command examples

```bash
# Bootstrap missing tables and fields
node scripts/bootstrap_health_tables.js <config.json>

# Rebuild a narrow monthly date window
python3 scripts/rebuild_monthly_calendar.py <config.json> \
  --identity user \
  --start-date 2026-04-29 \
  --end-date 2026-04-30 \
  --json

# Rebuild one explicit weekly window ending on Sunday
python3 scripts/rebuild_weekly_assessment.py <config.json> \
  --identity user \
  --week-end 2026-05-03 \
  --json

# Build or refresh the monthly dashboard
node scripts/build_monthly_dashboard.js <config.json> \
  --identity user
```

## Optional field alias override

If a deployment uses different labels from the built-in English defaults, add `field_aliases`:

```json
{
  "field_aliases": {
    "monthly_health_calendar": {
      "date": ["Date", "Stat Date"],
      "overall": ["Overall Health Status", "Overall Status"]
    }
  }
}
```

The rebuild scripts resolve fields by trying these aliases first, then falling back to their built-in English defaults.

## Dashboard naming behavior

`build_monthly_dashboard.js` resolves dashboard labels in this order:

1. CLI `--dashboard-name` (dashboard title only)
2. `config.dashboard.monthly_health_calendar.*` overrides
3. Built-in English defaults

Supported override keys for the monthly dashboard are:
...
- `dashboardName`
- `aboutName`
- `aboutText`
- `recordedDays`
- `nutritionOnTargetDays`
- `avgSleepScore`
- `activeWorkoutDays`
- `overallDistribution`
- `sleepTrend`
- `workoutTrend`
- `nutritionTrend`
- `recoveryDistribution`
- `workoutDistribution`

Example:

```json
{
  "dashboard": {
    "locale_labels": {
      "en": {
        "dashboardName": "Monthly Health Calendar Overview"
      },
      "zh": {
        "dashboardName": "月度健康日历总览"
      }
    },
    "monthly_health_calendar": {
      "dashboardName": "Health Dashboard",
      "aboutName": "About",
      "aboutText": "# Health Dashboard\n- Source: Monthly Health Calendar"
    }
  }
}
```

Use `monthly_health_calendar` when you want one deployment-specific naming scheme regardless of auto-detected locale.

## Output copy overrides

You can also override generated text fragments for monthly / weekly summaries without editing the scripts.

```json
{
  "copy": {
    "monthly_health_calendar": {
      "workout_no_records": "No workouts logged.",
      "overview_nutrition": "Nutrition status: {nutrition_status}",
      "overview_workout": "Training: {workout_summary}"
    },
    "weekly_health_assessment": {
      "brief_title": "Weekly review ({start_day} to {end_day})",
      "brief_overall": "Status: {overall}. {cross_text}",
      "suggestion_activity_gap": "Distribute activity across the week instead of compressing it into one day."
    }
  }
}
```

Current supported keys:

- `copy.monthly_health_calendar`
  - `workout_no_records`
  - `workout_fallback_type`
  - `workout_summary`
  - `overview_nutrition`
  - `overview_sleep`
  - `overview_workout`
  - `overview_nutrition_note`
- `copy.weekly_health_assessment`
  - `no_nutrition_records`
  - `nutrition_headline_stable`
  - `nutrition_headline_unstable`
  - `nutrition_summary`
  - `no_sleep_records`
  - `sleep_quality_solid`
  - `sleep_quality_shaky`
  - `sleep_summary`
  - `no_workout_records`
  - `workout_level_met`
  - `workout_level_below`
  - `workout_fallback_types`
  - `workout_summary`
  - `cross_headline_alert`
  - `cross_headline_attention`
  - `cross_headline_stable`
  - `risk_focus_none`
  - `suggestion_sleep`
  - `suggestion_training_fuel`
  - `suggestion_sodium_fiber`
  - `suggestion_activity_gap`
  - `suggestion_keep_current`
  - `brief_title`
  - `brief_overall`
  - `brief_nutrition`
  - `brief_sleep`
  - `brief_workout`
  - `brief_risk_focus`
  - `brief_next_actions`

Template strings are rendered with Python `str.format(...)`, so placeholder names must match the script variables exactly.

## Production-style config example

Use this pattern when you want one deployment config to control schema matching, dashboard naming, and generated summary wording together.

```json
{
  "feishu": {
    "active_base": {
      "token": "<base_token>"
    },
    "tables": {
      "nutrition_daily_history": { "name": "Nutrition Daily History", "table_id": "<tbl_nutrition_daily_history>" },
      "sleep_recovery": { "name": "Sleep Recovery", "table_id": "<tbl_sleep_recovery>" },
      "exercise_workout": { "name": "Exercise Workouts", "table_id": "<tbl_exercise_workout>" },
      "monthly_health_calendar": { "name": "Monthly Health Calendar", "table_id": "<tbl_monthly_health_calendar>" },
      "weekly_health_assessment": { "name": "Weekly Health Assessment", "table_id": "<tbl_weekly_health_assessment>" }
    }
  },
  "field_aliases": {
    "monthly_health_calendar": {
      "date": ["Date", "Stat Date"],
      "overall": ["Overall Health Status", "Overall Status"]
    },
    "weekly_health_assessment": {
      "brief": ["Chat Brief", "Weekly Brief"]
    }
  },
  "dashboard": {
    "monthly_health_calendar": {
      "dashboardName": "Monthly Health Overview",
      "aboutName": "About This Dashboard",
      "aboutText": "# Monthly Health Overview\n- Source table: Monthly Health Calendar\n- Refresh: rebuild monthly rows before dashboard refresh"
    }
  },
  "copy": {
    "monthly_health_calendar": {
      "overview_nutrition": "Nutrition status: {nutrition_status}",
      "overview_sleep": "Sleep summary: {sleep_summary}",
      "overview_workout": "Workout summary: {workout_summary}"
    },
    "weekly_health_assessment": {
      "brief_title": "Weekly review ({start_day} to {end_day})",
      "brief_overall": "Status: {overall}. {cross_text}",
      "brief_next_actions": "Next actions: {next_actions}",
      "suggestion_keep_current": "Keep the current rhythm and focus on data completeness."
    }
  }
}
```

Recommended use of the three layers:

- `field_aliases`: absorb deployment-specific field naming drift without patching scripts
- `dashboard.monthly_health_calendar`: pin dashboard title and block names for one environment
- `copy.*`: control generated monthly/weekly wording without changing business logic

## Recommended config practice

- Pin `feishu.tables.<table_key>.table_id` after bootstrap in any environment where same-name tables may coexist.
- Keep dashboard/chart field labels aligned with the actual deployed monthly table schema.
- Put deployment-specific naming overrides under `schemas`, not in scripts.


path: references/cross-domain.md
content: # Cross-Domain Branch

Use this branch for summaries, dashboards, sync, and automation maintenance.

## Scope

- rebuild monthly health calendar rows
- rebuild weekly health assessment rows
- create dashboard blocks over the monthly table
- run narrow refreshes after upstream changes

## Order

1. Identify the target artifact:
   - monthly calendar
   - weekly assessment
   - dashboard
2. Refresh only the minimum upstream dependency set.
3. Rebuild the target artifact.
4. Stop.

## Rules

- prefer summary-layer tables first
- do not reopen all raw-domain logic on the first pass
- rebuild the narrowest date window possible
- report partial refreshes explicitly

## Runbook

Assume all commands run from `skills/healthcare-feishu/scripts/` and use an environment-specific config JSON.

### 1. Rebuild monthly calendar rows

Narrow refresh for a specific date window:

```bash
python3 rebuild_monthly_calendar.py <config.json> \
  --identity user \
  --start-date 2026-04-29 \
  --end-date 2026-04-30 \
  --json
```

Behavior:

- reads `nutrition_daily_history`, `sleep_recovery`, and `exercise_workout`
- aggregates into `monthly_health_calendar`
- upserts by `Date`
- writes `Date` as `YYYY-MM-DD 00:00:00`

### 2. Rebuild weekly assessment rows

Single explicit week:

```bash
python3 rebuild_weekly_assessment.py <config.json> \
  --identity user \
  --week-end 2026-05-03 \
  --json
```

Backfill all complete weeks visible from the upstream summary layer:

```bash
python3 rebuild_weekly_assessment.py <config.json> \
  --identity user \
  --backfill-full-weeks \
  --json
```

Behavior:

- reads `nutrition_daily_history`, `sleep_recovery`, and `exercise_workout`
- aggregates into `weekly_health_assessment`
- upserts by `Week Start` + `Week End`
- uses `weekly_thresholds` from config when present, otherwise script defaults

### 3. Build or refresh monthly dashboard

```bash
node build_monthly_dashboard.js <config.json> \
  --identity user
```

Optional dry run:

```bash
node build_monthly_dashboard.js <config.json> \
  --identity user \
  --dry-run
```

Behavior:

- reads `feishu.active_base.token`
- reads `feishu.tables.monthly_health_calendar.name`
- creates or reuses `Monthly Health Calendar Overview`
- creates missing blocks and updates existing blocks by block name
- fails fast on same-name type mismatches instead of silently mutating them

### 4. Recommended execution order

For a normal monthly refresh:

1. rebuild the smallest monthly date window needed
2. rebuild the affected weekly window if the date change crosses a full week boundary
3. refresh the monthly dashboard

### 5. Safety notes

- keep table names and pinned `table_id` values in config, not in code
- if a Base has same-name table collisions, prefer pinned `table_id`
- do not delete existing summary rows unless the user explicitly asks for destructive cleanup
- treat `base +record-delete` as a separate explicit operation, not part of routine rebuilds


path: references/data-model.md
content: # Public Data Model

This is the public, config-driven version of the Healthcare schema.

## Raw tables

### Nutrition meals
Minimum fields:
- datetime
- food description
- calories
- protein
- sodium
- carbs
- fat
- fiber

Recommended:
- image attachment
- estimated weight
- portion note
- confidence
- note
- micronutrients when available

### Sleep recovery
Minimum fields:
- date
- source app
- total sleep
- sleep score
- recovery state
- fatigue risk
- confidence
- one-line summary

Recommended:
- deep sleep
- REM
- awake time
- core/light sleep
- normalized minute fields
- HRV
- body signals
- advice
- reason
- missing fields

### Exercise workouts
Minimum fields:
- datetime
- source app
- workout type
- total duration
- total duration minutes
- calories
- load
- one-line summary
- confidence

Recommended:
- average HR
- max HR
- pace/speed
- distance
- rationale
- advice
- reason
- missing fields

## Derived tables

### Nutrition daily history
One row per local day. Aggregate nutrition metrics and short assessment.

### Monthly health calendar
One row per local day. Merge nutrition, sleep, and exercise into a dashboard-ready daily record.

### Weekly health assessment
One row per completed week. Cross-domain summary with counts, risks, and next-step recommendations.

## Public boundary

The internal table schema and dashboard blocks are English-only. If a deployment requires different field labels (e.g., for legacy reasons), they must be handled through `field_aliases` in the configuration. Chinese user input is supported via mapping/recognition layers outside these scripts, but the persistence layer remains English-only.


path: references/exercise.md
content: # Exercise Branch (Unified)

Use this branch for workout extraction, training summary, and Feishu logging.

## Scope
- Analyze workout screenshots or training summaries.
- Extract metrics (duration, calories, HR, distance, load).
- Upsert results into the Feishu exercise schema.

## Recognition Rules
- Extract only visible metrics.
- Assess training load conservatively (Light, Medium, High).
- Provide same-day or next-day training guidance based on effort.

## Persistence Rules (Feishu)
- Target the **English-only** internal schema fields.
- Map workout types into the configured enum options if possible.
- Use `lark-cli base +record-upsert` for logging.

## Standard Upsert Template
```bash
lark-cli base +record-upsert \
  --as user \
  --base-token <base_token> \
  --table-id <exercise_workout_table_id_or_name> \
  --field 'Logged At=2026-04-30 07:10' \
  --field 'Source App=Garmin' \
  --field 'Workout Type=Run' \
  --field 'Duration Minutes=42' \
  --field 'Calories (kcal)=410' \
  --field 'Load=Medium' \
  --field 'One-Line Summary=Steady aerobic run'
```

## Boundary
Do not trigger full summary rebuilds from a single workout write unless requested.


path: references/nutrition.md
content: # Nutrition Branch (Unified)

Use this branch for meal recognition, calorie estimation, and Feishu logging.

## Scope
- Analyze meal photos or food descriptions.
- Map a structured meal result into a Feishu nutrition-meal schema.
- Rebuild nutrition daily history when required.

## Recognition Rules
- Extract only visible food items.
- Estimate calories, protein, sodium, carbs, fat, and fiber conservatively.
- State uncertainty when portions or hidden ingredients (e.g., oil, salt) are unclear.

## Persistence Rules (Feishu)
- Target the **English-only** internal schema fields.
- Use `lark-cli base +record-upsert` for raw-write.
- If the user uses custom field labels, resolve them via `field_aliases` in config.

## Standard Upsert Template
```bash
lark-cli base +record-upsert \
  --as user \
  --base-token <base_token> \
  --table-id <nutrition_meal_table_id_or_name> \
  --field 'Logged At=2026-04-30 19:15' \
  --field 'Food Description=Chicken rice + vegetables' \
  --field 'Calories (kcal)=650' \
  --field 'Protein (g)=38' \
  --field 'Sodium (mg)=920' \
  --field 'Fiber (g)=9'
```

## Boundary
Do not mix first-pass recognition with full monthly/weekly rebuilds unless explicitly requested.


path: references/sleep.md
content: # Sleep Branch (Unified)

Use this branch for sleep analysis, recovery interpretation, and Feishu logging.

## Scope
- Analyze sleep screenshots or recovery metrics.
- Extract sleep stages, score, duration, and HRV.
- Upsert results into the Feishu sleep-recovery schema.

## Recognition Rules
- Identify the source app (e.g., Apple Health, AutoSleep, Android).
- Extract duration (min), sleep score, recovery state, and fatigue risk.
- Do not present consumer wearable data as clinical fact.

## Persistence Rules (Feishu)
- Target the **English-only** internal schema fields.
- Ensure date and datetime formatting align with the table field type.
- Resolve any custom field labels via `field_aliases`.

## Standard Upsert Template
```bash
lark-cli base +record-upsert \
  --as user \
  --base-token <base_token> \
  --table-id <sleep_recovery_table_id_or_name> \
  --field 'Date=2026-04-30' \
  --field 'Source App=AutoSleep' \
  --field 'Total Sleep Minutes=430' \
  --field 'Sleep Score=82' \
  --field 'Recovery State=Good' \
  --field 'Fatigue Risk=Low' \
  --field 'One-Line Summary=Sleep duration recovered' \
  --field 'HRV=58'
```

## Boundary
The sleep table is a raw table. Weekly or monthly assessments belong to the reporting branch.


path: references/weekly-review.md
content: # Weekly Health Review (Unified)

Use this branch for cross-domain assessments combining nutrition, exercise, and sleep.

## Scope
- Create a weekly health review from recent signals.
- Identify the main bottleneck and next-step actions.
- Rebuild the `Weekly Health Assessment` table in Feishu.

## Analysis Rules
- Combine signals from all three domains.
- Highlight conflicts (e.g., high training load + poor sleep).
- Provide practical, action-oriented next-week advice.

## Reporting Rules (Feishu)
- Assume the `rebuild_weekly_assessment.py` script handles the heavy aggregation.
- The agent should trigger the script for the target week.
- For a manual text summary, use the `brief` template logic from the script.

## Command Template
```bash
python3 scripts/rebuild_weekly_assessment.py <config.json> \
  --identity user \
  --week-end 2026-05-03 \
  --json
```

## Boundary
For daily trend scanning, use the `Monthly Health Calendar` or the dashboard instead.


path: scripts/bootstrap_health_tables.js
content: #!/usr/bin/env node
const fs = require('fs');
const { spawnSync } = require('child_process');

const DEFAULT_TABLE_SPECS = {
  nutrition_meal: {
    defaultName: 'Nutrition Meals',
    fields: [
      { name: 'Logged At', type: 'datetime', style: { format: 'yyyy-MM-dd HH:mm' } },
      { name: 'Food Description', type: 'text' },
      { name: 'Food Image', type: 'attachment' },
      { name: 'Calories (kcal)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Protein (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Sodium (mg)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Carbs (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Fat (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Fiber (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Estimated Weight (g)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Portion Note', type: 'text' },
      { name: 'Nutrition Summary', type: 'text' },
      { name: 'Confidence', type: 'number', style: { type: 'plain', precision: 2 } },
      { name: 'Note', type: 'text' },
      { name: 'Potassium (mg)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Calcium (mg)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Iron (mg)', type: 'number', style: { type: 'plain', precision: 2 } },
      { name: 'Magnesium (mg)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Vitamin A (μg RAE)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Vitamin C (mg)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Vitamin B1 (mg)', type: 'number', style: { type: 'plain', precision: 3 } },
      { name: 'Vitamin B2 (mg)', type: 'number', style: { type: 'plain', precision: 3 } },
      { name: 'Vitamin B3 (mg)', type: 'number', style: { type: 'plain', precision: 3 } },
      { name: 'Vitamin B6 (mg)', type: 'number', style: { type: 'plain', precision: 3 } },
      { name: 'Folate B9 (μg)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Vitamin B12 (μg)', type: 'number', style: { type: 'plain', precision: 3 } },
    ],
  },
  nutrition_daily_history: {
    defaultName: 'Nutrition Daily History',
    fields: [
      { name: 'Date', type: 'datetime', style: { format: 'yyyy-MM-dd' } },
      { name: 'Daily Conclusion', type: 'text' },
      { name: 'Total Calories (kcal)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Total Protein (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Total Sodium (mg)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Total Carbs (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Total Fat (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Total Fiber (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Metric Assessment', type: 'text' },
      { name: 'Risk Alert', type: 'text' },
      { name: 'Action Advice', type: 'text' },
      { name: 'Meal Summary', type: 'text' },
      { name: 'Analysis Note', type: 'text' },
      { name: 'Reference Calories (kcal)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Reference Sodium (mg)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Reference Fiber (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Reference Carbs (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Reference Protein (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Reference Fat (g)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Reference Vitamin C (mg)', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Reference Iron (mg)', type: 'number', style: { type: 'plain', precision: 2 } },
      { name: 'Reference Vitamin B12 (μg)', type: 'number', style: { type: 'plain', precision: 3 } },
      { name: 'Reference Vitamin B6 (mg)', type: 'number', style: { type: 'plain', precision: 3 } },
    ],
  },
  sleep_recovery: {
    defaultName: 'Sleep Recovery',
    fields: [
      { name: 'Date', type: 'datetime', style: { format: 'yyyy-MM-dd' } },
      {
        name: 'Source App',
        type: 'select',
        multiple: false,
        options: [{ name: 'Apple Health' }, { name: 'Android' }, { name: 'Other', hue: 'Gray', lightness: 'Lighter' }],
      },
      { name: 'Total Sleep', type: 'text' },
      { name: 'Sleep Score', type: 'number', style: { type: 'plain', precision: 0 } },
      {
        name: 'Recovery State',
        type: 'select',
        multiple: false,
        options: [{ name: 'Poor', hue: 'Red', lightness: 'Dark' }, { name: 'Weak', hue: 'Orange', lightness: 'Light' }, { name: 'Average', hue: 'Yellow', lightness: 'Lighter' }, { name: 'Above Average', hue: 'Wathet', lightness: 'Lighter' }, { name: 'Good', hue: 'Green', lightness: 'Lighter' }],
      },
      {
        name: 'Fatigue Risk',
        type: 'select',
        multiple: false,
        options: [{ name: 'Low', hue: 'Green', lightness: 'Light' }, { name: 'Low to Medium', hue: 'Blue', lightness: 'Lighter' }, { name: 'Medium to High', hue: 'Orange', lightness: 'Light' }, { name: 'High', hue: 'Red', lightness: 'Dark' }],
      },
      { name: 'Confidence', type: 'select', multiple: false, options: [{ name: 'Low', hue: 'Gray', lightness: 'Lighter' }, { name: 'Medium', hue: 'Blue', lightness: 'Lighter' }, { name: 'High', hue: 'Green', lightness: 'Lighter' }] },
      { name: 'One-Line Summary', type: 'text' },
      { name: 'Deep Sleep', type: 'text' },
      { name: 'REM Sleep', type: 'text' },
      { name: 'Awake Time', type: 'text' },
      { name: 'Core Sleep', type: 'text' },
      { name: 'Light Sleep', type: 'text' },
      { name: 'Total Sleep Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Deep Sleep Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'REM Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Core/Light Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'HRV', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Body Signals', type: 'text' },
      { name: 'Advice', type: 'text' },
      { name: 'Reason', type: 'text' },
      { name: 'Missing Fields', type: 'text' },
    ],
  },
  exercise_workout: {
    defaultName: 'Exercise Workouts',
    fields: [
      { name: 'Logged At', type: 'datetime', style: { format: 'yyyy-MM-dd HH:mm' } },
      {
        name: 'Source App',
        type: 'select',
        multiple: false,
        options: [{ name: 'Apple Health' }, { name: 'Strava' }, { name: 'Garmin' }, { name: 'Keep' }, { name: 'Android' }, { name: 'Other', hue: 'Gray', lightness: 'Lighter' }],
      },
      {
        name: 'Workout Type',
        type: 'select',
        multiple: false,
        options: [{ name: 'Running' }, { name: 'Walking' }, { name: 'Cycling' }, { name: 'Strength' }, { name: 'HIIT' }, { name: 'Swimming' }, { name: 'Boxing' }, { name: 'Other', hue: 'Gray', lightness: 'Lighter' }],
      },
      { name: 'Duration', type: 'text' },
      { name: 'Duration Minutes', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Calories (kcal)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Load', type: 'select', multiple: false, options: [{ name: 'Light', hue: 'Wathet', lightness: 'Lighter' }, { name: 'Medium', hue: 'Orange', lightness: 'Lighter' }, { name: 'High', hue: 'Red', lightness: 'Dark' }] },
      { name: 'One-Line Summary', type: 'text' },
      { name: 'Confidence', type: 'select', multiple: false, options: [{ name: 'Low', hue: 'Gray', lightness: 'Lighter' }, { name: 'Medium', hue: 'Blue', lightness: 'Lighter' }, { name: 'High', hue: 'Green', lightness: 'Lighter' }] },
      { name: 'Average HR', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Max HR', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Pace / Speed', type: 'text' },
      { name: 'Distance', type: 'text' },
      { name: 'Distance (km)', type: 'number', style: { type: 'plain', precision: 2 } },
      { name: 'Load Assessment', type: 'text' },
      { name: 'Advice', type: 'text' },
      { name: 'Reason', type: 'text' },
      { name: 'Missing Fields', type: 'text' },
    ],
  },
  monthly_health_calendar: {
    defaultName: 'Monthly Health Calendar',
    views: [{ name: 'Calendar', type: 'calendar' }],
    fields: [
      { name: 'Date', type: 'datetime', style: { format: 'yyyy-MM-dd' } },
      { name: 'Month Key', type: 'text' },
      { name: 'Month Range', type: 'select', multiple: false, options: [{ name: 'Current Month', hue: 'Blue', lightness: 'Lighter' }, { name: 'History', hue: 'Gray', lightness: 'Lighter' }] },
      { name: 'Overall Health Status', type: 'select', multiple: false, options: [{ name: 'Stable', hue: 'Green', lightness: 'Lighter' }, { name: 'Attention', hue: 'Orange', lightness: 'Lighter' }, { name: 'Alert', hue: 'Red', lightness: 'Dark' }] },
      { name: 'Nutrition Status', type: 'select', multiple: false, options: [{ name: 'On Target', hue: 'Green', lightness: 'Lighter' }, { name: 'Low', hue: 'Orange', lightness: 'Lighter' }, { name: 'High', hue: 'Red', lightness: 'Dark' }, { name: 'Missing', hue: 'Gray', lightness: 'Lighter' }] },
      { name: 'Calories Achievement (%)', type: 'number', style: { type: 'plain', precision: 1, percentage: false } },
      { name: 'Protein Achievement (%)', type: 'number', style: { type: 'plain', precision: 1, percentage: false } },
      { name: 'Sodium vs Reference (%)', type: 'number', style: { type: 'plain', precision: 1, percentage: false } },
      { name: 'Nutrition Summary', type: 'text' },
      { name: 'Sleep Score', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Recovery State', type: 'select', multiple: false, options: [{ name: 'Poor', hue: 'Red', lightness: 'Dark' }, { name: 'Weak', hue: 'Orange', lightness: 'Light' }, { name: 'Average', hue: 'Yellow', lightness: 'Lighter' }, { name: 'Above Average', hue: 'Wathet', lightness: 'Lighter' }, { name: 'Good', hue: 'Green', lightness: 'Lighter' }] },
      { name: 'Fatigue Risk', type: 'select', multiple: false, options: [{ name: 'Low', hue: 'Green', lightness: 'Light' }, { name: 'Low to Medium', hue: 'Blue', lightness: 'Lighter' }, { name: 'Medium to High', hue: 'Orange', lightness: 'Light' }, { name: 'High', hue: 'Red', lightness: 'Dark' }] },
      { name: 'Sleep Summary', type: 'text' },
      { name: 'Workout Count', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Workout Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Workout Calories (kcal)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Workout Status', type: 'select', multiple: false, options: [{ name: 'Rest', hue: 'Gray', lightness: 'Lighter' }, { name: 'Light', hue: 'Wathet', lightness: 'Lighter' }, { name: 'Medium', hue: 'Orange', lightness: 'Lighter' }, { name: 'High Load', hue: 'Red', lightness: 'Dark' }] },
      { name: 'Workout Summary', type: 'text' },
      { name: 'Daily Overview', type: 'text' },
    ],
  },
  weekly_health_assessment: {
    defaultName: 'Weekly Health Assessment',
    fields: [
      { name: 'Week Start', type: 'datetime', style: { format: 'yyyy-MM-dd' } },
      { name: 'Week End', type: 'datetime', style: { format: 'yyyy-MM-dd' } },
      { name: 'Week Key', type: 'text' },
      { name: 'Weekly Health Status', type: 'select', multiple: false, options: [{ name: 'Stable', hue: 'Green', lightness: 'Lighter' }, { name: 'Attention', hue: 'Orange', lightness: 'Lighter' }, { name: 'Alert', hue: 'Red', lightness: 'Dark' }] },
      { name: 'Recorded Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Nutrition Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Sleep Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Workout Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Nutrition On-Target Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Nutrition Low Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Nutrition High Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Attention Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Alert Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Average Sleep Score', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Low Recovery Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'High Fatigue Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Short Sleep Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Severe Short Sleep Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Low Protein Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'High Sodium Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'HRV Recorded Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Active Workout Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'High Load Training Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Total Workout Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Total Workout Calories (kcal)', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Activity Equivalent Minutes', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Underfueled Training Days', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Poor Recovery After Training Count', type: 'number', style: { type: 'plain', precision: 0 } },
      { name: 'Composite Risk Score', type: 'number', style: { type: 'plain', precision: 1 } },
      { name: 'Nutrition Weekly Review', type: 'text' },
      { name: 'Sleep Weekly Review', type: 'text' },
      { name: 'Workout Weekly Review', type: 'text' },
      { name: 'Cross-Domain Reassessment', type: 'text' },
      { name: 'Risk Focus', type: 'text' },
      { name: 'Next-Week Actions', type: 'text' },
      { name: 'Chat Brief', type: 'text' },
    ],
  },
};

const DEFAULT_RETRY_COUNT = 2;
const DEFAULT_RETRY_DELAY_MS = 800;
const DEFAULT_WRITE_DELAY_MS = 250;

function loadConfig(configPath) {
  return JSON.parse(fs.readFileSync(configPath, 'utf8'));
}

function parseArgs(argv) {
  const args = {
    identity: 'user',
    dryRun: false,
    retryCount: DEFAULT_RETRY_COUNT,
    retryDelayMs: DEFAULT_RETRY_DELAY_MS,
    writeDelayMs: DEFAULT_WRITE_DELAY_MS,
  };
  for (let i = 0; i < argv.length; i += 1) {
    const [REDACTED_SECRET];
    if (!args.configPath && !token.startsWith('--')) {
      args.configPath = token;
      continue;
    }
    if (token === '--dry-run') {
      args.dryRun = true;
      continue;
    }
    if (token === '--identity') {
      args.identity = argv[i + 1] || 'user';
      i += 1;
      continue;
    }
    if (token === '--retry-count') {
      args.retryCount = Number(argv[i + 1] || DEFAULT_RETRY_COUNT);
      i += 1;
      continue;
    }
    if (token === '--retry-delay-ms') {
      args.retryDelayMs = Number(argv[i + 1] || DEFAULT_RETRY_DELAY_MS);
      i += 1;
      continue;
    }
    if (token === '--write-delay-ms') {
      args.writeDelayMs = Number(argv[i + 1] || DEFAULT_WRITE_DELAY_MS);
      i += 1;
    }
  }
  return args;
}

function normalizeTableName(table) {
  return table?.name || table?.table_name || null;
}

function normalizeTableId(table) {
  return table?.table_id || table?.id || null;
}

function normalizeFieldName(field) {
  return field?.field_name || field?.name || null;
}

function sleepMs(ms) {
  if (!Number.isFinite(ms) || ms <= 0) return;
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}

function shouldRetryBaseError(message) {
  if (!message) return false;
  return [
    'FXDB_ERR_TRANSACTION_COMMIT_FAILED',
    'ER_KV_INTERNAL_ERROR',
    '800100001',
    'internal_error',
    'status code: 800004709',
    'timeout',
    'temporarily unavailable',
  ].some((token) => message.includes(token));
}

function runBase(args, identity, options = {}) {
  const fullArgs = ['base', ...args, '--as', identity];
  const retryCount = Number.isFinite(options.retryCount) ? options.retryCount : DEFAULT_RETRY_COUNT;
  const retryDelayMs = Number.isFinite(options.retryDelayMs) ? options.retryDelayMs : DEFAULT_RETRY_DELAY_MS;
  const writeDelayMs = Number.isFinite(options.writeDelayMs) ? options.writeDelayMs : 0;

  for (let attempt = 0; attempt <= retryCount; attempt += 1) {
    const result = spawnSync('lark-cli', fullArgs, { encoding: 'utf8' });
    if (result.status === 0) {
      if (writeDelayMs > 0) {
        sleepMs(writeDelayMs);
      }
      return JSON.parse(result.stdout);
    }

    const message = result.stderr || result.stdout || `lark-cli ${fullArgs.join(' ')} failed`;
    if (attempt < retryCount && shouldRetryBaseError(message)) {
      sleepMs(retryDelayMs * (attempt + 1));
      continue;
    }
    throw new Error(message);
  }
}

function listTables(baseToken, identity, options) {
  return runBase(['+table-list', '--base-token', baseToken], identity, options);
}

function listFields(baseToken, tableId, identity, options) {
  return runBase(['+field-list', '--base-token', baseToken, '--table-id', tableId], identity, options);
}

function createTable(baseToken, name, identity, options) {
  return runBase(['+table-create', '--base-token', baseToken, '--name', name], identity, options);
}

function createField(baseToken, tableId, fieldSpec, identity, options) {
  return runBase([
    '+field-create',
    '--base-token',
    baseToken,
    '--table-id',
    tableId,
    '--json',
    JSON.stringify(fieldSpec),
  ], identity, options);
}

function createViews(baseToken, tableId, views, identity, options) {
  return runBase([
    '+view-create',
    '--base-token',
    baseToken,
    '--table-id',
    tableId,
    '--json',
    JSON.stringify(views),
  ], identity, options);
}

function resolveTableId(createResult) {
  return createResult?.data?.table?.table_id || createResult?.data?.table?.id || createResult?.data?.table_id || createResult?.table?.table_id || null;
}

function getTableSpec(key, tableConfig, config) {
  const fallback = DEFAULT_TABLE_SPECS[key];
  const override = config?.schemas?.[key] || {};
  if (!fallback && !override.fields) {
    throw new Error(`No schema found for table key: ${key}`);
  }
  return {
    key,
    name: tableConfig?.name || override.name || fallback?.defaultName || key,
    fields: override.fields || fallback.fields || [],
    views: override.views || fallback?.views || [],
    existingTableId: tableConfig?.table_id || null,
  };
}

function findTablesByName(existingTables, targetName) {
  const normalizedTarget = String(targetName || '').trim();
  if (!normalizedTarget) return [];
  return existingTables.filter((item) => {
    const name = String(normalizeTableName(item) || '').trim();
    return name === normalizedTarget || name.startsWith(`${normalizedTarget}_conflict_`);
  });
}

function planBootstrap(config) {
  const baseToken = config?.feishu?.active_base?.token;
  const tables = config?.feishu?.tables || {};
  if (!baseToken) {
    throw new Error('config.feishu.active_base.token is required');
  }
  const specs = Object.entries(tables).map(([key, tableConfig]) => getTableSpec(key, tableConfig, config));
  return { baseToken, specs };
}

function executeBootstrap(config, identity, dryRun, runtimeOptions = {}) {
  const { baseToken, specs } = planBootstrap(config);
  const baseOptions = {
    retryCount: runtimeOptions.retryCount,
    retryDelayMs: runtimeOptions.retryDelayMs,
    writeDelayMs: 0,
  };
  const writeOptions = {
    retryCount: runtimeOptions.retryCount,
    retryDelayMs: runtimeOptions.retryDelayMs,
    writeDelayMs: runtimeOptions.writeDelayMs,
  };

  if (dryRun) {
    return {
      ok: true,
      dry_run: true,
      identity,
      base_token_present: Boolean(baseToken),
      tables: specs.map((spec) => ({
        key: spec.key,
        name: spec.name,
        existing_table_id: spec.existingTableId,
        field_count: spec.fields.length,
        views: spec.views,
      })),
    };
  }

  const tablesResp = listTables(baseToken, identity, baseOptions);
  const existingTables = tablesResp?.data?.tables || tablesResp?.data?.items || [];
  const resultTables = [];

  for (const spec of specs) {
    let table = null;
    if (spec.existingTableId) {
      table = existingTables.find((item) => normalizeTableId(item) === spec.existingTableId) || { table_id: spec.existingTableId, table_name: spec.name };
    } else {
      const matchingTables = findTablesByName(existingTables, spec.name);
      const exactMatches = matchingTables.filter((item) => normalizeTableName(item) === spec.name);
      if (matchingTables.length > 1) {
        const candidateIds = matchingTables.map((item) => normalizeTableId(item)).filter(Boolean).join(', ');
        throw new Error(`Ambiguous table name for ${spec.name}. Matching table ids: ${candidateIds}. Pin config.feishu.tables.${spec.key}.table_id before rerunning.`);
      }
      table = exactMatches[0] || matchingTables[0] || null;
    }

    let created = false;
    if (!table) {
      const createResp = createTable(baseToken, spec.name, identity, writeOptions);
      const tableId = resolveTableId(createResp);
      if (!tableId) {
        throw new Error(`Unable to resolve table id for ${spec.name}`);
      }
      table = { table_id: tableId, table_name: spec.name };
      created = true;
      if (spec.views.length > 0) {
        createViews(baseToken, tableId, spec.views, identity, writeOptions);
      }
    }

    const tableId = normalizeTableId(table);
    const fieldResp = listFields(baseToken, tableId, identity, baseOptions);
    const existingFieldNames = new Set((fieldResp?.data?.fields || fieldResp?.data?.items || []).map(normalizeFieldName).filter(Boolean));
    const createdFields = [];

    for (const field of spec.fields) {
      if (existingFieldNames.has(field.name)) {
        continue;
      }
      createField(baseToken, tableId, field, identity, writeOptions);
      createdFields.push(field.name);
    }

    resultTables.push({
      key: spec.key,
      name: spec.name,
      table_id: tableId,
      created,
      created_fields: createdFields,
    });
  }

  return {
    ok: true,
    dry_run: false,
    identity,
    tables: resultTables,
  };
}

function main() {
  const { configPath, identity, dryRun, retryCount, retryDelayMs, writeDelayMs } = parseArgs(process.argv.slice(2));
  if (!configPath) {
    console.error('Usage: node bootstrap_health_tables.js <config.json> [--identity user|bot] [--dry-run] [--retry-count N] [--retry-delay-ms MS] [--write-delay-ms MS]');
    process.exit(1);
  }
  const config = loadConfig(configPath);
  const result = executeBootstrap(config, identity, dryRun, { retryCount, retryDelayMs, writeDelayMs });
  console.log(JSON.stringify(result, null, 2));
}

main();


path: scripts/build_monthly_dashboard.js
content: #!/usr/bin/env node
const fs = require('fs');
const { spawnSync } = require('child_process');

function parseArgs(argv) {
  const args = { identity: 'user', themeStyle: 'summerBreeze' };
  for (let i = 2; i < argv.length; i += 1) {
    const [REDACTED_SECRET];
    if (token === '--help' || token === '-h') {
      args.help = true;
      continue;
    }
    if (!args.config && !token.startsWith('--')) {
      args.config = token;
      continue;
    }
    if (token === '--identity') {
      args.identity = argv[++i];
      continue;
    }
    if (token === '--dashboard-name') {
      args.dashboardName = argv[++i];
      continue;
    }
    if (token === '--theme-style') {
      args.themeStyle = argv[++i];
      continue;
    }
    if (token === '--dry-run') {
      args.dryRun = true;
      continue;
    }
    if (token === '--json') {
      args.json = true;
      continue;
    }
    throw new Error(`Unknown arg: ${token}`);
  }
  if (!args.config && !args.help) {
    throw new Error('Usage: node build_monthly_dashboard.js <config.json> [--identity user|bot] [--dashboard-name NAME] [--theme-style STYLE] [--dry-run] [--json]');
  }
  return args;
}

function printHelp() {
  console.log('Usage: node build_monthly_dashboard.js <config.json> [--identity user|bot] [--dashboard-name NAME] [--theme-style STYLE] [--dry-run] [--json]');
}

function loadConfig(configPath) {
  return JSON.parse(fs.readFileSync(configPath, 'utf8'));
}

function runJson(cmd, { allowEmpty = false } = {}) {
  const proc = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' });
  if (proc.status !== 0) {
    throw new Error((proc.stderr || proc.stdout || `Command failed: ${cmd.join(' ')}`).trim());
  }
  const stdout = (proc.stdout || '').trim();
  if (!stdout) {
    if (allowEmpty) return {};
    throw new Error(`Empty JSON output from command: ${cmd.join(' ')}`);
  }
  return JSON.parse(stdout);
}

function larkJson(args, extra = {}) {
  return runJson(['lark-cli', ...args], extra);
}

const MONTHLY_FIELDS = {
  date: ['Date'],
  range: ['Month Range'],
  overall: ['Overall Health Status'],
  nutritionStatus: ['Nutrition Status'],
  sleepScore: ['Sleep Score'],
  recovery: ['Recovery State'],
  workoutCount: ['Workout Count'],
  workoutMinutes: ['Workout Minutes'],
  calorieRatio: ['Calories Achievement (%)'],
  proteinRatio: ['Protein Achievement (%)'],
  workoutStatus: ['Workout Status'],
};

const MONTHLY_VALUES = {
  currentMonth: ['Current Month'],
  onTarget: ['On Target'],
};

const BLOCK_I18N = {
  en: {
    dashboardName: 'Monthly Health Calendar Overview',
    aboutName: 'About This Dashboard',
    aboutText: [
      '# Monthly Health Calendar Overview',
      '- Source: Monthly Health Calendar',
      '- Scope: current-month rows only',
      '- Use this dashboard to scan stability, recovery, and workout rhythm day by day.',
    ].join('\n'),
    recordedDays: 'Current-Month Recorded Days',
    nutritionOnTargetDays: 'Current-Month Nutrition On-Target Days',
    avgSleepScore: 'Current-Month Average Sleep Score',
    activeWorkoutDays: 'Current-Month Active Workout Days',
    overallDistribution: 'Current-Month Overall Health Status Distribution',
    sleepTrend: 'Current-Month Sleep Score Trend',
    workoutTrend: 'Current-Month Workout Minutes Trend',
    nutritionTrend: 'Current-Month Nutrition Achievement Trend',
    recoveryDistribution: 'Current-Month Recovery State Distribution',
    workoutDistribution: 'Current-Month Workout Status Distribution',
  },
};

const BLOCK_LABEL_KEYS = [
  'aboutName',
  'aboutText',
  'recordedDays',
  'nutritionOnTargetDays',
  'avgSleepScore',
  'activeWorkoutDays',
  'overallDistribution',
  'sleepTrend',
  'workoutTrend',
  'nutritionTrend',
  'recoveryDistribution',
  'workoutDistribution',
];

function mustGet(object, path, label) {
  const value = path.reduce((acc, key) => (acc == null ? undefined : acc[key]), object);
  if (value == null) {
    throw new Error(`Missing ${label}`);
  }
  return value;
}

function listTableSnapshot(baseToken, identity, tableRef) {
  const payload = larkJson([
    'base', '+record-list',
    '--as', identity,
    '--base-token', baseToken,
    '--table-id', tableRef,
    '--offset', '0',
    '--limit', '200',
  ]);
  const data = payload.data || {};
  const fieldNames = data.fields || [];
  const rows = (data.data || []).map((row) => {
    const mapped = {};
    fieldNames.forEach((field, index) => {
      mapped[field] = row[index];
    });
    return mapped;
  });
  return { fieldNames, rows };
}

function normalizeValue(value) {
  if (Array.isArray(value)) {
    if (value.length === 1 && typeof value[0] !== 'object') return value[0];
    if (value.length && value[0] && typeof value[0] === 'object' && 'text' in value[0]) {
      return value.map((item) => String(item.text || '')).join('');
    }
  }
  return value;
}

function resolveFields(fieldNames) {
  const resolved = {};
  for (const [key, aliases] of Object.entries(MONTHLY_FIELDS)) {
    resolved[key] = aliases.find((name) => fieldNames.includes(name)) || aliases[0];
  }
  return resolved;
}

function resolveValue(candidates, values, fallbackIndex = 0) {
  for (const candidate of candidates) {
    if (values.includes(candidate)) return candidate;
  }
  return candidates[fallbackIndex] || candidates[0];
}

function detectLocale() {
  return 'en';
}

function getDashboardConfig(config) {
  return config.dashboard || config.dashboards || {};
}

function resolveDashboardLabels(config, locale) {
  const dashboardConfig = getDashboardConfig(config);
  const localeConfig = dashboardConfig.locale_labels || dashboardConfig.localeLabels || {};
  const named = dashboardConfig.monthly_health_calendar || dashboardConfig.monthlyHealthCalendar || {};
  const namedLocale = (named.locale_labels || named.localeLabels || {});

  const base = { ...(BLOCK_I18N[locale] || BLOCK_I18N.en) };
  const merges = [localeConfig[locale], localeConfig.default, namedLocale[locale], namedLocale.default, named];
  for (const source of merges) {
    if (!source || typeof source !== 'object') continue;
    for (const key of ['dashboardName', ...BLOCK_LABEL_KEYS]) {
      if (source[key] != null) base[key] = source[key];
    }
  }
  return base;
}

function listDashboards(baseToken, identity) {
  const items = [];
  let pageToken;
  do {
    const cmd = ['base', '+dashboard-list', '--as', identity, '--base-token', baseToken];
    if (pageToken) cmd.push('--page-token', pageToken);
    const payload = larkJson(cmd);
    const data = payload.data || {};
    items.push(...(data.items || []));
    pageToken = data.has_more ? data.page_token : null;
  } while (pageToken);
  return items;
}

function listBlocks(baseToken, identity, dashboardId) {
  const items = [];
  let pageToken;
  do {
    const cmd = ['base', '+dashboard-block-list', '--as', identity, '--base-token', baseToken, '--dashboard-id', dashboardId];
    if (pageToken) cmd.push('--page-token', pageToken);
    const payload = larkJson(cmd);
    const data = payload.data || {};
    items.push(...(data.items || []));
    pageToken = data.has_more ? data.page_token : null;
  } while (pageToken);
  return items;
}

function findUnique(items, predicate, label) {
  const matches = items.filter(predicate);
  if (matches.length > 1) {
    throw new Error(`Ambiguous ${label}: ${matches.map((item) => item.block_id || item.dashboard_id || item.name).join(', ')}`);
  }
  return matches[0] || null;
}

function ensureDashboard(baseToken, identity, name, themeStyle, dryRun) {
  const dashboards = listDashboards(baseToken, identity);
  const existing = findUnique(dashboards, (item) => item.name === name, `dashboard name ${name}`);
  if (existing) {
    return { dashboardId: existing.dashboard_id, created: false };
  }
  if (dryRun) {
    return { dashboardId: null, created: true, dryRunPlanned: true };
  }
  const payload = larkJson([
    'base', '+dashboard-create',
    '--as', identity,
    '--base-token', baseToken,
    '--name', name,
    '--theme-style', themeStyle,
  ]);
  const dashboard = mustGet(payload, ['data', 'dashboard'], 'dashboard create response');
  return { dashboardId: dashboard.dashboard_id, created: true };
}

function dashboardBlocks(tableName, resolvedFields, resolvedValues, labels) {
  const currentMonthFilter = {
    conjunction: 'and',
    conditions: [
      { field_name: resolvedFields.range, operator: 'is', value: resolvedValues.currentMonth },
    ],
  };

  return [
    {
      name: labels.aboutName,
      type: 'text',
      dataConfig: {
        text: labels.aboutText,
      },
    },
    {
      name: labels.recordedDays,
      type: 'statistics',
      dataConfig: { table_name: tableName, count_all: true, filter: currentMonthFilter },
    },
    {
      name: labels.nutritionOnTargetDays,
      type: 'statistics',
      dataConfig: {
        table_name: tableName,
        count_all: true,
        filter: {
          conjunction: 'and',
          conditions: [
            { field_name: resolvedFields.range, operator: 'is', value: resolvedValues.currentMonth },
            { field_name: resolvedFields.nutritionStatus, operator: 'is', value: resolvedValues.onTarget },
          ],
        },
      },
    },
    {
      name: labels.avgSleepScore,
      type: 'statistics',
      dataConfig: {
        table_name: tableName,
        filter: currentMonthFilter,
        series: [{ field_name: resolvedFields.sleepScore, rollup: 'AVERAGE' }],
      },
    },
    {
      name: labels.activeWorkoutDays,
      type: 'statistics',
      dataConfig: {
        table_name: tableName,
        count_all: true,
        filter: {
          conjunction: 'and',
          conditions: [
            { field_name: resolvedFields.range, operator: 'is', value: resolvedValues.currentMonth },
            { field_name: resolvedFields.workoutCount, operator: 'isGreater', value: 0 },
          ],
        },
      },
    },
    {
      name: labels.overallDistribution,
      type: 'pie',
      dataConfig: {
        table_name: tableName,
        count_all: true,
        filter: currentMonthFilter,
        group_by: [{ field_name: resolvedFields.overall, mode: 'integrated', sort: { type: 'group', order: 'asc' } }],
      },
    },
    {
      name: labels.sleepTrend,
      type: 'line',
      dataConfig: {
        table_name: tableName,
        filter: currentMonthFilter,
        series: [{ field_name: resolvedFields.sleepScore, rollup: 'AVERAGE' }],
        group_by: [{ field_name: resolvedFields.date, mode: 'integrated', sort: { type: 'group', order: 'asc' } }],
      },
    },
    {
      name: labels.workoutTrend,
      type: 'column',
      dataConfig: {
        table_name: tableName,
        filter: currentMonthFilter,
        series: [{ field_name: resolvedFields.workoutMinutes, rollup: 'SUM' }],
        group_by: [{ field_name: resolvedFields.date, mode: 'integrated', sort: { type: 'group', order: 'asc' } }],
      },
    },
    {
      name: labels.nutritionTrend,
      type: 'combo',
      dataConfig: {
        table_name: tableName,
        filter: currentMonthFilter,
        series: [
          { field_name: resolvedFields.calorieRatio, rollup: 'AVERAGE' },
          { field_name: resolvedFields.proteinRatio, rollup: 'AVERAGE' },
        ],
        group_by: [{ field_name: resolvedFields.date, mode: 'integrated', sort: { type: 'group', order: 'asc' } }],
      },
    },
    {
      name: labels.recoveryDistribution,
      type: 'pie',
      dataConfig: {
        table_name: tableName,
        count_all: true,
        filter: currentMonthFilter,
        group_by: [{ field_name: resolvedFields.recovery, mode: 'integrated', sort: { type: 'group', order: 'asc' } }],
      },
    },
    {
      name: labels.workoutDistribution,
      type: 'pie',
      dataConfig: {
        table_name: tableName,
        count_all: true,
        filter: currentMonthFilter,
        group_by: [{ field_name: resolvedFields.workoutStatus, mode: 'integrated', sort: { type: 'group', order: 'asc' } }],
      },
    },
  ];
}

function ensureBlock(baseToken, identity, dashboardId, existingBlocks, spec, dryRun) {
  const exact = existingBlocks.filter((item) => item.name === spec.name);
  if (exact.length > 1) {
    throw new Error(`Ambiguous block name ${spec.name}: ${exact.map((item) => item.block_id).join(', ')}`);
  }
  const existing = exact[0];
  if (!existing) {
    if (dryRun) {
      return { action: 'create', name: spec.name, type: spec.type, block_id: null };
    }
    const payload = larkJson([
      'base', '+dashboard-block-create',
      '--as', identity,
      '--base-token', baseToken,
      '--dashboard-id', dashboardId,
      '--name', spec.name,
      '--type', spec.type,
      '--data-config', JSON.stringify(spec.dataConfig),
    ]);
    const block = mustGet(payload, ['data', 'block'], `created block ${spec.name}`);
    return { action: 'created', name: spec.name, type: spec.type, block_id: block.block_id };
  }

  if (existing.type !== spec.type) {
    throw new Error(`Block type mismatch for ${spec.name}: existing=${existing.type}, expected=${spec.type}`);
  }

  if (dryRun) {
    return { action: 'update', name: spec.name, type: spec.type, block_id: existing.block_id };
  }

  larkJson([
    'base', '+dashboard-block-update',
    '--as', identity,
    '--base-token', baseToken,
    '--dashboard-id', dashboardId,
    '--block-id', existing.block_id,
    '--name', spec.name,
    '--data-config', JSON.stringify(spec.dataConfig),
  ]);
  return { action: 'updated', name: spec.name, type: spec.type, block_id: existing.block_id };
}

function main() {
  const args = parseArgs(process.argv);
  if (args.help) {
    printHelp();
    return;
  }
  const config = loadConfig(args.config);
  const baseToken = mustGet(config, ['feishu', 'active_base', 'token'], 'feishu.active_base.token');
  const monthly = mustGet(config, ['feishu', 'tables', 'monthly_health_calendar'], 'feishu.tables.monthly_health_calendar');
  const tableName = monthly.name;
  const tableRef = monthly.table_id || monthly.name;
  const snapshot = listTableSnapshot(baseToken, args.identity, tableRef);
  const resolvedFields = resolveFields(snapshot.fieldNames);
  const locale = detectLocale(resolvedFields);
  const labels = resolveDashboardLabels(config, locale);
  const dashboardName = args.dashboardName || labels.dashboardName;
  const monthRangeValues = snapshot.rows.map((row) => normalizeValue(row[resolvedFields.range])).filter((value) => value != null && value !== '');
  const nutritionStatusValues = snapshot.rows.map((row) => normalizeValue(row[resolvedFields.nutritionStatus])).filter((value) => value != null && value !== '');
  const resolvedValues = {
    currentMonth: resolveValue(MONTHLY_VALUES.currentMonth, monthRangeValues, 0),
    onTarget: resolveValue(MONTHLY_VALUES.onTarget, nutritionStatusValues, 0),
  };

  const dashboard = ensureDashboard(baseToken, args.identity, dashboardName, args.themeStyle, args.dryRun);
  const existingBlocks = dashboard.dashboardId ? listBlocks(baseToken, args.identity, dashboard.dashboardId) : [];
  const ops = [];
  for (const spec of dashboardBlocks(tableName, resolvedFields, resolvedValues, labels)) {
    ops.push(ensureBlock(baseToken, args.identity, dashboard.dashboardId, existingBlocks, spec, args.dryRun));
  }

  const result = {
    ok: true,
    dry_run: Boolean(args.dryRun),
    identity: args.identity,
    locale,
    dashboard_name: dashboardName,
    dashboard_id: dashboard.dashboardId,
    dashboard_created: dashboard.created,
    target_table: tableName,
    block_count: ops.length,
    blocks: ops,
  };

  console.log(JSON.stringify(result, null, 2));
}

main();


path: scripts/config_loader.py
content: from __future__ import annotations

import json
from pathlib import Path
from typing import Any


def load_config(config_path: str | Path) -> dict[str, Any]:
    path = Path(config_path).expanduser().resolve()
    return json.loads(path.read_text(encoding="utf-8"))


def get_active_base(config: dict[str, Any]) -> dict[str, Any]:
    return config["feishu"]["active_base"]


def get_table(config: dict[str, Any], key: str) -> dict[str, Any]:
    return config["feishu"]["tables"][key]


def get_table_id(config: dict[str, Any], key: str) -> str | None:
    table = get_table(config, key)
    return table.get("table_id") or None


def get_threshold(config: dict[str, Any], *keys: str):
    value: Any = config
    for key in keys:
        if not isinstance(value, dict):
            return None
        value = value.get(key)
    return value


def get_schema_field_names(config: dict[str, Any], table_key: str) -> list[str]:
    schema = config.get("schemas", {}).get(table_key, {})
    fields = schema.get("fields", []) or []
    names: list[str] = []
    for field in fields:
        if isinstance(field, dict):
            name = field.get("name")
            if isinstance(name, str) and name.strip():
                names.append(name.strip())
    return names


def get_field_aliases(config: dict[str, Any], table_key: str) -> dict[str, list[str]]:
    raw = config.get("field_aliases", {}).get(table_key, {})
    aliases: dict[str, list[str]] = {}
    if not isinstance(raw, dict):
        return aliases
    for logical_key, value in raw.items():
        if isinstance(value, str):
            values = [value]
        elif isinstance(value, list):
            values = [item for item in value if isinstance(item, str) and item.strip()]
        else:
            continue
        cleaned = [item.strip() for item in values if item.strip()]
        if cleaned:
            aliases[str(logical_key)] = cleaned
    return aliases


path: scripts/rebuild_monthly_calendar.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import subprocess
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

from config_loader import get_field_aliases, get_schema_field_names, get_table, load_config

TIMEZONE = ZoneInfo("Asia/Hong_Kong")

NUTRITION = {
    "date": ["Date"],
    "conclusion": ["Daily Conclusion"],
    "calories": ["Total Calories (kcal)"],
    "calories_ref": ["Reference Calories (kcal)"],
    "protein": ["Total Protein (g)"],
    "protein_ref": ["Reference Protein (g)"],
    "sodium": ["Total Sodium (mg)"],
    "sodium_ref": ["Reference Sodium (mg)"],
    "fiber": ["Total Fiber (g)"],
    "fiber_ref": ["Reference Fiber (g)"],
}

SLEEP = {
    "date": ["Date"],
    "score": ["Sleep Score"],
    "recovery": ["Recovery State"],
    "fatigue": ["Fatigue Risk"],
    "summary": ["One-Line Summary"],
}

EXERCISE = {
    "date": ["Logged At"],
    "type": ["Workout Type"],
    "duration": ["Duration Minutes"],
    "calories": ["Calories (kcal)"],
    "load": ["Load"],
}

MONTHLY = {
    "date": ["Date"],
    "month": ["Month Key"],
    "range": ["Month Range"],
    "overall": ["Overall Health Status"],
    "nutrition_status": ["Nutrition Status"],
    "calorie_ratio": ["Calories Achievement (%)"],
    "protein_ratio": ["Protein Achievement (%)"],
    "sodium_ratio": ["Sodium vs Reference (%)"],
    "nutrition_summary": ["Nutrition Summary"],
    "sleep_score": ["Sleep Score"],
    "recovery": ["Recovery State"],
    "fatigue": ["Fatigue Risk"],
    "sleep_summary": ["Sleep Summary"],
    "workout_count": ["Workout Count"],
    "workout_minutes": ["Workout Minutes"],
    "workout_calories": ["Workout Calories (kcal)"],
    "workout_status": ["Workout Status"],
    "workout_summary": ["Workout Summary"],
    "overview": ["Daily Overview"],
}

ENUMS = {
    "missing": ["Missing"],
    "high": ["High"],
    "low": ["Low"],
    "on_target": ["On Target"],
    "rest": ["Rest"],
    "light": ["Light"],
    "medium": ["Medium"],
    "high_load": ["High Load"],
    "alert": ["Alert"],
    "attention": ["Attention"],
    "stable": ["Stable"],
    "current_month": ["Current Month"],
    "history": ["History"],
}

MONTHLY_COPY = {
    "workout_no_records": "No workout records.",
    "workout_fallback_type": "Workout",
    "workout_summary": "{types} | {sessions} sessions, {minutes} min, {kcal} kcal",
    "overview_nutrition": "Nutrition {nutrition_status}",
    "overview_sleep": "Sleep: {sleep_summary}",
    "overview_workout": "Workout: {workout_summary}",
    "overview_nutrition_note": "Nutrition note: {nutrition_summary}",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Rebuild monthly health calendar rows.")
    parser.add_argument("config", help="Path to config JSON")
    parser.add_argument("--start-date")
    parser.add_argument("--end-date")
    parser.add_argument("--identity", default="user", choices=["user", "bot"])
    parser.add_argument("--json", action="store_true", help="Emit structured JSON")
    return parser.parse_args()


def get_monthly_copy(config: dict[str, Any]) -> dict[str, str]:
    copy = dict(MONTHLY_COPY)
    source = ((config.get("copy") or {}).get("monthly_health_calendar") or {})
    for key in MONTHLY_COPY:
        if source.get(key) is not None:
            copy[key] = str(source[key])
    return copy


def normalize_value(value: Any) -> Any:
    if isinstance(value, list):
        if len(value) == 1 and not isinstance(value[0], dict):
            return value[0]
        if value and isinstance(value[0], dict) and "text" in value[0]:
            return "".join(str(item.get("text", "")) for item in value)
    return value


def run_json(args: list[str]) -> dict[str, Any]:
    proc = subprocess.run(args, capture_output=True, text=True, check=True)
    return json.loads(proc.stdout)


def list_records(base_token: str, table_ref: str, identity: str) -> tuple[list[dict[str, Any]], list[str]]:
    offset = 0
    limit = 200
    rows: list[dict[str, Any]] = []
    all_fields: list[str] = []
    while True:
        payload = run_json([
            "lark-cli", "base", "+record-list",
            "--as", identity,
            "--base-token", base_token,
            "--table-id", table_ref,
            "--offset", str(offset),
            "--limit", str(limit),
        ])
        data = payload.get("data", {})
        matrix = data.get("data", []) or []
        fields = data.get("fields", []) or []
        if fields and not all_fields:
            all_fields = list(fields)
        record_ids = data.get("record_id_list", []) or []
        page_rows = []
        for row_index, row in enumerate(matrix):
            field_map = {}
            for field_index, field_name in enumerate(fields):
                field_map[field_name] = normalize_value(row[field_index] if field_index < len(row) else None)
            page_rows.append({
                "record_id": record_ids[row_index] if row_index < len(record_ids) else None,
                "fields": field_map,
            })
        rows.extend(page_rows)
        if not page_rows or not data.get("has_more"):
            break
        offset += len(page_rows)
    return rows, all_fields


def merged_aliases(config: dict[str, Any], table_key: str, defaults: dict[str, list[str]]) -> dict[str, list[str]]:
    merged = {key: list(value) for key, value in defaults.items()}
    for key, aliases in get_field_aliases(config, table_key).items():
        merged[key] = aliases + [alias for alias in merged.get(key, []) if alias not in aliases]
    return merged


def resolve_fields(config: dict[str, Any], table_key: str, defaults: dict[str, list[str]], available_fields: list[str]) -> dict[str, str]:
    aliases = merged_aliases(config, table_key, defaults)
    available = list(dict.fromkeys((available_fields or []) + get_schema_field_names(config, table_key)))
    resolved: dict[str, str] = {}
    for key, candidates in aliases.items():
        resolved[key] = next((candidate for candidate in candidates if candidate in available), candidates[0])
    return resolved


def enum_value(logical_key: str, monthly_fields: dict[str, str]) -> str:
    field_name = monthly_fields.get("overall")
    is_chinese = field_name == "健康关注度"
    choices = ENUMS[logical_key]
    return choices[1] if is_chinese and len(choices) > 1 else choices[0]


def upsert_record(base_token: str, table_ref: str, identity: str, payload: dict[str, Any], record_id: str | None = None) -> dict[str, Any]:
    cmd = [
        "lark-cli", "base", "+record-upsert",
        "--as", identity,
        "--base-token", base_token,
        "--table-id", table_ref,
        "--json", json.dumps(payload, ensure_ascii=False),
    ]
    if record_id:
        cmd.extend(["--record-id", record_id])
    return run_json(cmd)


def parse_local_dt(value: Any) -> datetime | None:
    if value in (None, ""):
        return None
    if isinstance(value, (int, float)):
        ts = float(value)
        if ts > 10**12:
            ts /= 1000
        return datetime.fromtimestamp(ts, TIMEZONE)
    raw = str(value).strip()
    try:
        ts = float(raw)
        if ts > 10**12:
            ts /= 1000
        return datetime.fromtimestamp(ts, TIMEZONE)
    except ValueError:
        pass
    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d"):
        try:
            return datetime.strptime(raw, fmt).replace(tzinfo=TIMEZONE)
        except ValueError:
            continue
    try:
        return datetime.fromisoformat(raw.replace("Z", "+00:00")).astimezone(TIMEZONE)
    except Exception:
        return None


def local_date_key(value: Any) -> str | None:
    dt = parse_local_dt(value)
    return dt.date().isoformat() if dt else None


def midnight_string(date_key: str) -> str:
    return f"{date_key} 00:00:00"


def to_number(value: Any) -> float | None:
    if value in (None, ""):
        return None
    try:
        return float(value)
    except Exception:
        return None


def ratio_percent(actual: Any, reference: Any) -> float | None:
    actual_num = to_number(actual)
    reference_num = to_number(reference)
    if actual_num is None or reference_num in (None, 0):
        return None
    return round(actual_num / reference_num * 100, 1)


def choose_latest(records: list[dict[str, Any]], field_name: str) -> dict[str, Any] | None:
    best: dict[str, Any] | None = None
    best_dt: datetime | None = None
    for record in records:
        dt = parse_local_dt(record["fields"].get(field_name))
        if dt is None:
            continue
        if best is None or best_dt is None or dt > best_dt:
            best = record
            best_dt = dt
    return best or (records[-1] if records else None)


def classify_nutrition_status(nutrition_fields: dict[str, Any], nutrition_map: dict[str, str], monthly_map: dict[str, str]) -> str:
    if not nutrition_fields:
        return enum_value("missing", monthly_map)
    calorie_ratio = ratio_percent(nutrition_fields.get(nutrition_map["calories"]), nutrition_fields.get(nutrition_map["calories_ref"]))
    protein_ratio = ratio_percent(nutrition_fields.get(nutrition_map["protein"]), nutrition_fields.get(nutrition_map["protein_ref"]))
    sodium_ratio = ratio_percent(nutrition_fields.get(nutrition_map["sodium"]), nutrition_fields.get(nutrition_map["sodium_ref"]))
    if sodium_ratio is not None and sodium_ratio > 130:
        return enum_value("high", monthly_map)
    if calorie_ratio is not None and calorie_ratio > 125:
        return enum_value("high", monthly_map)
    if calorie_ratio is not None and calorie_ratio < 75:
        return enum_value("low", monthly_map)
    if protein_ratio is not None and protein_ratio < 80:
        return enum_value("low", monthly_map)
    return enum_value("on_target", monthly_map)


def summarize_exercise(rows: list[dict[str, Any]], exercise_map: dict[str, str], monthly_map: dict[str, str], copy: dict[str, str]) -> dict[str, Any]:
    if not rows:
        return {
            monthly_map["workout_count"]: 0,
            monthly_map["workout_minutes"]: 0,
            monthly_map["workout_calories"]: 0,
            monthly_map["workout_status"]: enum_value("rest", monthly_map),
            monthly_map["workout_summary"]: copy["workout_no_records"],
        }

    total_minutes = 0.0
    total_kcal = 0.0
    load_counts = {"Light": 0, "Medium": 0, "High": 0}
    workout_types: list[str] = []

    for row in rows:
        fields = row["fields"]
        total_minutes += to_number(fields.get(exercise_map["duration"])) or 0.0
        total_kcal += to_number(fields.get(exercise_map["calories"])) or 0.0
        load_raw = str(fields.get(exercise_map["load"]) or "").strip()
        if load_raw in load_counts:
            load_counts[load_raw] += 1
        elif load_raw:
            load_counts["High" if to_number(load_raw) and to_number(load_raw) >= 7 else "Medium" if to_number(load_raw) and to_number(load_raw) >= 4 else "Light"] += 1
        workout_type = str(fields.get(exercise_map["type"]) or "").strip()
        if workout_type and workout_type not in workout_types:
            workout_types.append(workout_type)

    if load_counts["High"] > 0 or total_minutes >= 90:
        status = enum_value("high_load", monthly_map)
    elif load_counts["Medium"] > 0 or total_minutes >= 45:
        status = enum_value("medium", monthly_map)
    else:
        status = enum_value("light", monthly_map)

    summary = copy["workout_summary"].format(
        types=", ".join(workout_types) if workout_types else copy["workout_fallback_type"],
        sessions=len(rows),
        minutes=round(total_minutes),
        kcal=round(total_kcal),
    )
    return {
        monthly_map["workout_count"]: len(rows),
        monthly_map["workout_minutes"]: round(total_minutes, 1),
        monthly_map["workout_calories"]: round(total_kcal, 1),
        monthly_map["workout_status"]: status,
        monthly_map["workout_summary"]: summary,
    }


def classify_overall_status(nutrition_status: str, recovery_state: str | None, fatigue_risk: str | None, workout_status: str, monthly_map: dict[str, str]) -> str:
    poor_states = {"Poor"}
    weak_states = {"Weak"}
    high_fatigue = {"High"}
    medium_to_high_fatigue = {"Medium to High"}
    if recovery_state in poor_states or fatigue_risk in high_fatigue:
        return enum_value("alert", monthly_map)
    if nutrition_status in {enum_value("low", monthly_map), enum_value("high", monthly_map)} or recovery_state in weak_states or fatigue_risk in medium_to_high_fatigue or workout_status == enum_value("high_load", monthly_map):
        return enum_value("attention", monthly_map)
    return enum_value("stable", monthly_map)


def build_overview(nutrition_status: str, nutrition_summary: str | None, sleep_summary: str | None, workout_summary: str | None, copy: dict[str, str]) -> str:
    parts = [copy["overview_nutrition"].format(nutrition_status=nutrition_status)]
    if sleep_summary:
        parts.append(copy["overview_sleep"].format(sleep_summary=sleep_summary))
    if workout_summary:
        parts.append(copy["overview_workout"].format(workout_summary=workout_summary))
    if nutrition_summary:
        parts.append(copy["overview_nutrition_note"].format(nutrition_summary=nutrition_summary))
    return " | ".join(parts)


def pick_existing_monthly(target_rows: list[dict[str, Any]], date_key: str, monthly_map: dict[str, str]) -> dict[str, Any] | None:
    matches = [row for row in target_rows if local_date_key(row["fields"].get(monthly_map["date"])) == date_key]
    if not matches:
        return None
    return max(matches, key=lambda row: (sum(1 for value in row["fields"].values() if value not in (None, "", [])), row["record_id"] or ""))


def build_summary_rows(config: dict[str, Any], identity: str, start_date: str | None, end_date: str | None) -> list[tuple[str, dict[str, Any]]]:
    base_token = config["feishu"]["active_base"]["token"]
    nutrition_table = get_table(config, "nutrition_daily_history")
    sleep_table = get_table(config, "sleep_recovery")
    exercise_table = get_table(config, "exercise_workout")

    nutrition_rows, nutrition_fields = list_records(base_token, nutrition_table.get("table_id") or nutrition_table["name"], identity)
    sleep_rows, sleep_fields = list_records(base_token, sleep_table.get("table_id") or sleep_table["name"], identity)
    exercise_rows, exercise_fields = list_records(base_token, exercise_table.get("table_id") or exercise_table["name"], identity)
    monthly_table = get_table(config, "monthly_health_calendar")
    _, monthly_fields_available = list_records(base_token, monthly_table.get("table_id") or monthly_table["name"], identity)

    nutrition_map = resolve_fields(config, "nutrition_daily_history", NUTRITION, nutrition_fields)
    sleep_map = resolve_fields(config, "sleep_recovery", SLEEP, sleep_fields)
    exercise_map = resolve_fields(config, "exercise_workout", EXERCISE, exercise_fields)
    monthly_map = resolve_fields(config, "monthly_health_calendar", MONTHLY, monthly_fields_available)
    monthly_copy = get_monthly_copy(config)

    nutrition_by_date: dict[str, dict[str, Any]] = {}
    for row in nutrition_rows:
        date_key = local_date_key(row["fields"].get(nutrition_map["date"]))
        if date_key:
            nutrition_by_date[date_key] = row["fields"]

    sleep_grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for row in sleep_rows:
        date_key = local_date_key(row["fields"].get(sleep_map["date"]))
        if date_key:
            sleep_grouped[date_key].append(row)

    exercise_grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for row in exercise_rows:
        date_key = local_date_key(row["fields"].get(exercise_map["date"]))
        if date_key:
            exercise_grouped[date_key].append(row)

    date_keys = set(nutrition_by_date) | set(sleep_grouped) | set(exercise_grouped)
    if start_date:
        date_keys = {date_key for date_key in date_keys if date_key >= start_date}
    if end_date:
        date_keys = {date_key for date_key in date_keys if date_key <= end_date}

    current_month = datetime.now(TIMEZONE).strftime("%Y-%m")
    rows: list[tuple[str, dict[str, Any]]] = []

    for date_key in sorted(date_keys):
        nutrition_fields = nutrition_by_date.get(date_key, {})
        sleep_record = choose_latest(sleep_grouped.get(date_key, []), sleep_map["date"])
        sleep_fields = sleep_record["fields"] if sleep_record else {}
        workout_fields = summarize_exercise(exercise_grouped.get(date_key, []), exercise_map, monthly_map, monthly_copy)

        nutrition_status = classify_nutrition_status(nutrition_fields, nutrition_map, monthly_map)
        overall_status = classify_overall_status(
            nutrition_status,
            str(sleep_fields.get(sleep_map["recovery"]) or "") or None,
            str(sleep_fields.get(sleep_map["fatigue"]) or "") or None,
            workout_fields[monthly_map["workout_status"]],
            monthly_map,
        )

        nutrition_summary = nutrition_fields.get(nutrition_map["conclusion"]) or None
        sleep_summary = sleep_fields.get(sleep_map["summary"]) or None
        workout_summary = workout_fields[monthly_map["workout_summary"]]
        row = {
            monthly_map["date"]: midnight_string(date_key),
            monthly_map["month"]: date_key[:7],
            monthly_map["range"]: enum_value("current_month", monthly_map) if date_key[:7] == current_month else enum_value("history", monthly_map),
            monthly_map["overall"]: overall_status,
            monthly_map["nutrition_status"]: nutrition_status,
            monthly_map["calorie_ratio"]: ratio_percent(nutrition_fields.get(nutrition_map["calories"]), nutrition_fields.get(nutrition_map["calories_ref"])),
            monthly_map["protein_ratio"]: ratio_percent(nutrition_fields.get(nutrition_map["protein"]), nutrition_fields.get(nutrition_map["protein_ref"])),
            monthly_map["sodium_ratio"]: ratio_percent(nutrition_fields.get(nutrition_map["sodium"]), nutrition_fields.get(nutrition_map["sodium_ref"])),
            monthly_map["nutrition_summary"]: nutrition_summary,
            monthly_map["sleep_score"]: to_number(sleep_fields.get(sleep_map["score"])),
            monthly_map["recovery"]: sleep_fields.get(sleep_map["recovery"]) or None,
            monthly_map["fatigue"]: sleep_fields.get(sleep_map["fatigue"]) or None,
            monthly_map["sleep_summary"]: sleep_summary,
            monthly_map["overview"]: build_overview(nutrition_status, nutrition_summary, sleep_summary, workout_summary, monthly_copy),
            **workout_fields,
        }
        rows.append((date_key, {key: value for key, value in row.items() if value is not None}))

    return rows


def main() -> None:
    args = parse_args()
    config = load_config(args.config)
    base_token = config["feishu"]["active_base"]["token"]
    monthly_table = get_table(config, "monthly_health_calendar")
    monthly_ref = monthly_table.get("table_id") or monthly_table["name"]
    target_rows, target_fields = list_records(base_token, monthly_ref, args.identity)
    monthly_map = resolve_fields(config, "monthly_health_calendar", MONTHLY, target_fields)

    summary_rows = build_summary_rows(config, args.identity, args.start_date, args.end_date)

    results = []
    for date_key, payload in summary_rows:
        existing = pick_existing_monthly(target_rows, date_key, monthly_map)
        response = upsert_record(base_token, monthly_ref, args.identity, payload, existing["record_id"] if existing else None)
        record_id = (
            response.get("data", {}).get("record", {}).get("record_id")
            or response.get("data", {}).get("record_id")
            or response.get("data", {}).get("record_id_list", [None])[0]
            or (existing["record_id"] if existing else None)
        )
        results.append({
            "date": date_key,
            "action": "updated" if existing else "created",
            "record_id": record_id,
        })

    result = {
        "ok": True,
        "target_table": monthly_ref,
        "count": len(results),
        "updated_dates": results,
    }
    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


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