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/';