From 3b1f7fb416bbebbef6f5c702642725ca6e36ab56 Mon Sep 17 00:00:00 2001 From: "liweiliang0905@gmail.com" Date: Mon, 26 Jan 2026 15:37:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A5=AE=E9=A3=9F?= =?UTF-8?q?=E6=96=87=E5=AD=97AI=E8=AF=86=E5=88=AB=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=B8=8E=E4=BB=A3=E7=A0=81=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 饮食页面新增"文字AI识别"按钮,支持输入文字描述后自动识别卡路里 - 修复 database.py 中 time 模块导入冲突问题 - 新增 CODEMAPS.md 代码结构文档 Co-Authored-By: Claude Opus 4.5 --- docs/CODEMAPS.md | 263 ++++++++++++++++++++++++++++++++++++ src/vitals/core/database.py | 4 +- src/vitals/web/app.py | 71 +++++++++- 3 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 docs/CODEMAPS.md diff --git a/docs/CODEMAPS.md b/docs/CODEMAPS.md new file mode 100644 index 0000000..23901e8 --- /dev/null +++ b/docs/CODEMAPS.md @@ -0,0 +1,263 @@ +# Vitals 代码结构地图 + +## 项目概览 + +Vitals 是一个本地优先的综合健康管理应用,支持运动、饮食、睡眠、体重、阅读等多维度健康数据记录与分析。 + +``` +vitals/ +├── src/vitals/ # 主要源代码 +│ ├── cli.py # 命令行入口 +│ ├── core/ # 核心业务逻辑 +│ ├── vision/ # AI视觉识别模块 +│ ├── web/ # Web应用 (FastAPI) +│ └── importers/ # 数据导入器 +├── tests/ # 测试文件 +├── scripts/ # 脚本工具 +└── docs/ # 文档 +``` + +--- + +## 核心模块详解 + +### 1. CLI 入口 (`cli.py`) + +命令行界面,基于 Typer 构建。 + +| 命令 | 功能 | +|------|------| +| `vitals dashboard` | 启动 Web 仪表盘 | +| `vitals log` | 记录健康数据 | +| `vitals show` | 查看健康数据 | +| `vitals config` | 用户配置 | +| `vitals import` | 导入外部数据 | +| `vitals report` | 生成报告 | +| `vitals export` | 数据导出 | +| `vitals backup` | 数据备份 | + +--- + +### 2. 核心业务 (`core/`) + +#### `models.py` - 数据模型 +```python +@dataclass +class Exercise # 运动记录 +class Meal # 饮食记录 +class Sleep # 睡眠记录 +class Weight # 体重记录 +class Reading # 阅读记录 +class User # 用户 +class UserConfig # 用户配置 +class Invite # 邀请码 +``` + +#### `database.py` - 数据库操作 +- MySQL 连接池管理 +- CRUD 操作封装 +- 用户隔离支持 + +主要函数: +```python +init_db() # 初始化数据库表 +get_connection() # 获取数据库连接 +add_meal() / get_meals() # 饮食记录 +add_exercise() / get_exercises() # 运动记录 +add_sleep() / get_sleeps() # 睡眠记录 +add_weight() / get_weights() # 体重记录 +get_api_key() / set_api_key() # API Key 管理 +``` + +#### `calories.py` - 卡路里计算 +- 内置 94 种常见中文食物数据库 +- 智能食物描述解析(支持中文数字、多种分隔符) +- 运动卡路里估算(基于 MET 代谢当量) + +```python +estimate_meal_calories(description) # 估算餐食卡路里 +estimate_exercise_calories(type, mins) # 估算运动消耗 +parse_food_description(text) # 解析食物描述 +``` + +#### `auth.py` - 认证模块 +- JWT Token 认证 +- 密码哈希 (bcrypt) +- 邀请码机制 + +#### `report.py` - 报告生成 +- 周报/月报生成 +- PDF 导出 (WeasyPrint) +- 数据可视化 + +#### `export.py` - 数据导出 +- JSON / CSV 格式导出 +- 数据备份与恢复 + +#### `backup.py` - 备份模块 +- 自动备份 +- 增量备份支持 + +--- + +### 3. AI 视觉识别 (`vision/`) + +#### `analyzer.py` - 分析器基类 +```python +class FoodAnalyzer: + def analyze(image_path) -> dict # 分析食物图片 + +class ClaudeFoodAnalyzer: # Claude Vision 实现 +``` + +#### `providers/qwen.py` - 通义千问 VL +```python +class QwenVisionAnalyzer: + def analyze_image(image_path) # 图片识别 + def analyze_text(description) # 文字识别 ← 新增功能 +``` + +#### `providers/deepseek.py` - DeepSeek Vision +```python +class DeepSeekVisionAnalyzer: + def analyze_image(image_path) + def analyze_text(description) +``` + +--- + +### 4. Web 应用 (`web/`) + +#### `app.py` - FastAPI 应用 (5500+ 行) + +**页面路由:** +| 路由 | 页面 | +|------|------| +| `/` | 首页仪表盘 | +| `/exercise` | 运动记录 | +| `/meal` | 饮食记录 | +| `/sleep` | 睡眠记录 | +| `/weight` | 体重记录 | +| `/reading` | 阅读记录 | +| `/report` | 健康报告 | +| `/settings` | 设置页面 | +| `/login` | 登录页面 | + +**API 端点:** + +| 方法 | 路由 | 功能 | +|------|------|------| +| POST | `/api/meal` | 添加饮食记录 | +| GET | `/api/meals` | 查询饮食记录 | +| POST | `/api/meal/recognize` | AI 食物识别 (图片/文字) | +| GET | `/api/meals/nutrition` | 营养统计 | +| POST | `/api/exercise` | 添加运动记录 | +| GET | `/api/exercises` | 查询运动记录 | +| POST | `/api/sleep` | 添加睡眠记录 | +| POST | `/api/weight` | 添加体重记录 | +| GET | `/api/config` | 获取用户配置 | +| POST | `/api/config` | 更新用户配置 | +| POST | `/api/login` | 用户登录 | +| POST | `/api/register` | 用户注册 | + +**前端架构:** +- 纯原生 HTML/CSS/JavaScript +- Chart.js 图表库 +- 响应式设计(PC/移动端适配) + +--- + +### 5. 数据导入器 (`importers/`) + +| 模块 | 功能 | +|------|------| +| `base.py` | 导入器基类 | +| `csv_importer.py` | CSV 通用导入 | +| `garmin.py` | Garmin 数据导入 | +| `codoon.py` | 咕咚数据导入 | + +--- + +### 6. 测试 (`tests/`) + +| 文件 | 测试内容 | +|------|----------| +| `test_calories.py` | 卡路里计算测试 | +| `test_database.py` | 数据库操作测试 | +| `test_models.py` | 数据模型测试 | +| `test_web.py` | Web API 测试 | +| `test_report.py` | 报告生成测试 | +| `test_export.py` | 导出功能测试 | +| `test_backup.py` | 备份功能测试 | +| `test_deepseek.py` | DeepSeek API 测试 | + +--- + +## 数据库设计 + +### 主要表结构 + +```sql +-- 用户表 +user (id, username, password_hash, email, created_at) + +-- 运动记录 +exercise (id, user_id, date, exercise_type, duration, calories, distance, notes) + +-- 饮食记录 +meal (id, user_id, date, meal_type, description, calories, protein, carbs, fat, photo_path, food_items) + +-- 睡眠记录 +sleep (id, user_id, date, bedtime, wake_time, duration, quality, notes) + +-- 体重记录 +weight (id, user_id, date, weight, body_fat, notes) + +-- 阅读记录 +reading (id, user_id, date, book_title, pages, duration, notes) + +-- 用户配置 +user_config (id, user_id, height, weight, age, gender, activity_level, goal) + +-- API Keys +api_keys (id, user_id, provider, api_key, created_at) + +-- 邀请码 +invite (id, code, created_by, used_by, used_at, expires_at) +``` + +--- + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 后端框架 | FastAPI + Uvicorn | +| 数据库 | MySQL 8 | +| CLI | Typer + Rich | +| AI 识别 | Qwen VL / DeepSeek / Claude | +| PDF 生成 | WeasyPrint | +| 前端 | 原生 HTML/CSS/JS + Chart.js | +| 认证 | JWT + bcrypt | + +--- + +## 关键文件行数统计 + +| 文件 | 行数 | 说明 | +|------|------|------| +| `web/app.py` | 5500+ | Web 应用主体 | +| `core/database.py` | 800+ | 数据库操作 | +| `core/calories.py` | 333 | 卡路里计算 | +| `core/models.py` | 327 | 数据模型 | +| `vision/providers/qwen.py` | 185 | 通义千问识别 | +| `vision/providers/deepseek.py` | 176 | DeepSeek 识别 | +| `core/report.py` | 300+ | 报告生成 | + +--- + +## 最近更新 + +- **文字 AI 识别功能**:在饮食页面添加"文字AI识别"按钮,支持输入文字描述后自动识别卡路里 + - 前端:`web/app.py` (第 5041 行按钮,第 5166 行函数) + - 后端:`/api/meal/recognize` 支持 `text` 参数 diff --git a/src/vitals/core/database.py b/src/vitals/core/database.py index e4d8013..9d07ee3 100644 --- a/src/vitals/core/database.py +++ b/src/vitals/core/database.py @@ -2,7 +2,7 @@ import json import os -import time +import time as time_module from contextlib import contextmanager from datetime import date, time, datetime, timedelta from typing import Optional @@ -52,7 +52,7 @@ def init_connection_pool(): except Exception as e: if attempt < max_retries - 1: print(f"[Vitals] MySQL 连接失败,{retry_delay}秒后重试... ({attempt + 1}/{max_retries}): {e}") - time.sleep(retry_delay) + time_module.sleep(retry_delay) else: print(f"[Vitals] MySQL 连接失败,已重试 {max_retries} 次") raise diff --git a/src/vitals/web/app.py b/src/vitals/web/app.py index 0af4cf6..fde5d03 100644 --- a/src/vitals/web/app.py +++ b/src/vitals/web/app.py @@ -5037,7 +5037,8 @@ def get_meal_page_html() -> str: - + + @@ -5075,6 +5076,7 @@ def get_meal_page_html() -> str: // 重置识别状态 document.getElementById('recognizeBtn').style.display = 'none'; document.getElementById('recognizeStatus').style.display = 'none'; + document.getElementById('textRecognizeBtn').disabled = false; } // ESC 键关闭 Modal document.addEventListener('keydown', (e) => { @@ -5161,6 +5163,73 @@ def get_meal_page_html() -> str: } } + async function recognizeFoodFromText() { + const descriptionInput = document.getElementById('meal-description'); + const statusDiv = document.getElementById('recognizeStatus'); + const textRecognizeBtn = document.getElementById('textRecognizeBtn'); + + const text = descriptionInput.value.trim(); + if (!text) { + alert('请先输入食物描述'); + return; + } + + // 显示识别中状态 + statusDiv.style.display = 'block'; + statusDiv.style.background = '#e3f2fd'; + statusDiv.style.color = '#1976d2'; + statusDiv.innerHTML = '🤖 正在识别食物,请稍候...'; + textRecognizeBtn.disabled = true; + + try { + const formData = new FormData(); + formData.append('text', text); + formData.append('provider', 'qwen'); + + const res = await fetch('/api/meal/recognize', { + method: 'POST', + body: formData, + }); + + const result = await res.json(); + + if (res.ok && result.success) { + // 显示成功状态 + statusDiv.style.background = '#e8f5e9'; + statusDiv.style.color = '#2e7d32'; + statusDiv.innerHTML = `✅ 识别成功!
食物:${result.description}
卡路里:${result.total_calories} 卡`; + + // 自动填充表单(更新描述为标准化的描述) + const form = document.getElementById('addMealForm'); + if (result.description) { + form.querySelector('[name="description"]').value = result.description; + } + form.querySelector('[name="calories"]').value = result.total_calories || ''; + if (result.total_protein) { + form.querySelector('[name="protein"]').value = result.total_protein.toFixed(1); + } + if (result.total_carbs) { + form.querySelector('[name="carbs"]').value = result.total_carbs.toFixed(1); + } + if (result.total_fat) { + form.querySelector('[name="fat"]').value = result.total_fat.toFixed(1); + } + } else { + // 显示错误状态 + statusDiv.style.background = '#ffebee'; + statusDiv.style.color = '#c62828'; + statusDiv.innerHTML = `❌ 识别失败:${result.detail || '未知错误'}`; + } + } catch (error) { + // 显示错误状态 + statusDiv.style.background = '#ffebee'; + statusDiv.style.color = '#c62828'; + statusDiv.innerHTML = `❌ 识别失败:${error.message}`; + } finally { + textRecognizeBtn.disabled = false; + } + } + function formatPhotoUrl(path) { if (!path) return null; const marker = '/.vitals/photos/';