# Invoice Master POC v2

自动发票字段提取系统 - 使用 YOLO26 + PaddleOCR 从瑞典 PDF 发票中提取结构化数据。

## 项目概述

本项目实现了一个完整的发票字段自动提取流程：

1. **自动标注**: 利用已有 CSV 结构化数据 + OCR 自动生成 YOLO 训练标注（统一 15px 填充）
2. **模型训练**: 使用 YOLO26 训练字段检测模型，支持数据增强
3. **推理提取**: 检测字段区域 -> OCR 提取文本 -> ValueSelector 过滤标签 -> 字段规范化
4. **Web 管理**: React 前端 + FastAPI 后端，支持文档管理、数据集构建、模型训练和版本管理

### 架构

项目采用 **monorepo + 三包分离** 架构，训练和推理可独立部署：

```
packages/
├── shared/      # 共享库 (PDF, OCR, 规范化, 匹配, 存储, 训练)
├── training/    # 训练服务 (GPU, 按需启动)
└── backend/     # 后端服务 (Web API + 推理, 常驻运行)
frontend/        # React 前端 (Vite + TypeScript + TailwindCSS)
```

| 服务 | 部署目标 | GPU | 生命周期 |
|------|---------|-----|---------|
| **Frontend** | Vercel / Nginx | 否 | 常驻 |
| **Backend** | Azure App Service / AWS | 可选 | 常驻 7x24 |
| **Training** | Azure ACI / AWS ECS | 必需 | 按需启动/销毁 |

两个服务通过共享 PostgreSQL 数据库通信。推理服务通过 API 触发训练任务，训练服务从数据库拾取任务执行。

### 当前进度

| 指标 | 数值 |
|------|------|
| **已标注文档** | 9,738 (9,709 成功) |
| **总体字段匹配率** | 94.8% (82,604/87,121) |
| **测试** | 2,047 passed |
| **测试覆盖率** | 72% |
| **模型 mAP@0.5** | 93.5% |

**各字段匹配率:**

| 字段 | 匹配率 | 说明 |
|------|--------|------|
| supplier_accounts(Bankgiro) | 100.0% | 供应商 Bankgiro |
| supplier_accounts(Plusgiro) | 100.0% | 供应商 Plusgiro |
| Plusgiro | 99.4% | 支付 Plusgiro |
| OCR | 99.1% | OCR 参考号 |
| Bankgiro | 99.0% | 支付 Bankgiro |
| InvoiceNumber | 98.9% | 发票号码 |
| InvoiceDueDate | 95.9% | 到期日期 |
| InvoiceDate | 95.5% | 发票日期 |
| Amount | 91.3% | 金额 |
| supplier_organisation_number | 78.2% | 供应商组织号 (CSV 数据质量问题) |

## 运行环境

本项目**必须**在 **WSL + Conda** 环境中运行。

### 系统要求

| 环境 | 要求 |
|------|------|
| **WSL** | WSL 2 + Ubuntu 22.04 (或 24.04 for RTX 50 系列) |
| **Conda** | Miniconda 或 Anaconda |
| **Python** | 3.11+ (通过 Conda 管理), 3.10 for RTX 50 系列 |
| **GPU** | NVIDIA GPU + CUDA 12.x (RTX 50 系列见 SM120 章节) |
| **数据库** | PostgreSQL (存储标注结果) |

## 安装

```bash
# 1. 进入 WSL
wsl -d Ubuntu-22.04

# 2. 创建 Conda 环境
conda create -n invoice-sm120 python=3.11 -y
conda activate invoice-sm120

# 3. 进入项目目录
cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2

# 4. 安装三个包 (editable mode)
pip install -e packages/shared
pip install -e packages/training
pip install -e packages/backend
```

## RTX 5080 (Blackwell SM 120) GPU 设置

RTX 50 系列 (Blackwell 架构) 使用 SM 120 计算能力，官方 PaddlePaddle 仅支持到 SM 90。需要使用社区编译的 SM120 wheel。

### 系统要求

| 要求 | 版本 |
|------|------|
| **WSL** | Ubuntu 24.04 (glibc 2.39+) |
| **Python** | 3.10 (wheel 限制) |
| **CUDA** | 13.0+ (通过 pip nvidia 包) |

### 升级 WSL 到 Ubuntu 24.04

```bash
# 检查当前版本
lsb_release -a

# 如果是 22.04，需要升级
sudo sed -i 's/Prompt=lts/Prompt=normal/g' /etc/update-manager/release-upgrades
sudo apt update && sudo apt upgrade -y
sudo do-release-upgrade
```

### 创建 SM120 环境

```bash
# 1. 创建 Python 3.10 环境
conda create -n invoice-sm120 python=3.10 -y
conda activate invoice-sm120

# 2. 安装 SM120 PaddlePaddle wheel
pip install https://github.com/horhe-dvlp/paddlepaddle-sm120-wheels/releases/download/v3.0.0/paddlepaddle_gpu-3.0.0-cp310-cp310-linux_x86_64.whl

# 3. 安装项目依赖
cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2
pip install -e packages/shared
pip install -e packages/training
pip install -e packages/backend
```

### 配置环境变量

在 `~/.bashrc` 中添加:

```bash
# PaddlePaddle SM120 (RTX 50 series) environment
export PADDLE_SM120_LIBS=/home/kai/.local/lib/python3.10/site-packages/nvidia
alias activate-sm120='export LD_LIBRARY_PATH=$PADDLE_SM120_LIBS/cublas/lib:$PADDLE_SM120_LIBS/cudnn/lib:$PADDLE_SM120_LIBS/cuda_runtime/lib:/usr/lib/wsl/lib:$LD_LIBRARY_PATH && export PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK=True && source ~/miniconda3/etc/profile.d/conda.sh && conda activate invoice-sm120'
```

### 使用

```bash
# 激活 SM120 环境
source ~/.bashrc
activate-sm120

# 验证 GPU
python -c "import paddle; paddle.utils.run_check()"

# 运行服务
cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2
python run_server.py --port 8000
```

### 故障排除

| 错误 | 解决方案 |
|------|---------|
| `GLIBCXX_3.4.32 not found` | 升级到 Ubuntu 24.04 |
| `GLIBC_2.38 not found` | 升级到 Ubuntu 24.04 |
| `cublasLtCreate` 失败 | 检查 LD_LIBRARY_PATH 包含 nvidia 库路径 |
| `Mismatched GPU Architecture` | 使用 SM120 wheel，不要用官方 paddle |

### 云部署

Azure/AWS GPU 实例 (A100, H100, T4, V100) 使用官方 PaddlePaddle，无需 SM120 wheel。

## 项目结构

```
invoice-master-poc-v2/
├── packages/
│   ├── shared/                     # 共享库
│   │   ├── setup.py
│   │   └── shared/
│   │       ├── pdf/                # PDF 处理 (提取, 渲染, 检测)
│   │       ├── ocr/                # PaddleOCR 封装 + 机器码解析
│   │       ├── normalize/          # 字段规范化 (10 种 normalizer)
│   │       ├── matcher/            # 字段匹配 (精确/子串/模糊)
│   │       ├── storage/            # 存储抽象层 (Local/Azure/S3)
│   │       ├── training/           # 共享训练组件 (YOLOTrainer)
│   │       ├── augmentation/       # 数据增强 (DatasetAugmenter)
│   │       ├── utils/              # 工具 (验证, 清理, 模糊匹配)
│   │       ├── data/               # DocumentDB, CSVLoader
│   │       ├── config.py           # 全局配置 (数据库, 路径, DPI)
│   │       └── exceptions.py       # 异常定义
│   │
│   ├── training/                   # 训练服务 (GPU, 按需)
│   │   ├── setup.py
│   │   ├── Dockerfile
│   │   ├── run_training.py         # 入口 (--task-id 或 --poll)
│   │   └── training/
│   │       ├── cli/                # train, autolabel, analyze_*, validate
│   │       ├── yolo/               # db_dataset, annotation_generator
│   │       ├── processing/         # CPU/GPU worker pool, task dispatcher
│   │       └── data/               # training_db, autolabel_report
│   │
│   └── backend/                    # 后端服务 (Web API + 推理, 常驻)
│       ├── setup.py
│       ├── Dockerfile
│       ├── run_server.py           # Web 服务器入口
│       └── backend/
│           ├── cli/                # infer, serve
│           ├── pipeline/           # YOLO 检测, 字段提取, ValueSelector, 解析器
│           ├── web/                # FastAPI 应用
│           │   ├── api/v1/         # REST API (admin, public, batch)
│           │   ├── schemas/        # Pydantic 数据模型
│           │   ├── services/       # 业务逻辑
│           │   ├── core/           # 认证, 调度器, 限流
│           │   └── workers/        # 后台任务队列
│           ├── validation/         # LLM 验证器
│           ├── data/               # AdminDB, AsyncRequestDB, Models
│           └── azure/              # ACI 训练触发器
│
├── frontend/                       # React 前端 (Vite + TypeScript + TailwindCSS)
│   ├── src/
│   │   ├── api/                    # API 客户端 (axios + react-query)
│   │   ├── components/             # UI 组件
│   │   │   ├── Dashboard.tsx       # 文档管理面板
│   │   │   ├── Training.tsx        # 训练管理 (数据集/任务)
│   │   │   ├── Models.tsx          # 模型版本管理
│   │   │   ├── DatasetDetail.tsx   # 数据集详情
│   │   │   └── InferenceDemo.tsx   # 推理演示
│   │   └── hooks/                  # React Query hooks
│   └── package.json
│
├── migrations/                     # 数据库迁移 (SQL)
│   ├── 003_training_tasks.sql
│   ├── 004_training_datasets.sql
│   ├── 005_add_group_key.sql
│   ├── 006_model_versions.sql
│   ├── 007_training_tasks_extra_columns.sql
│   ├── 008_fix_model_versions_fk.sql
│   ├── 009_add_document_category.sql
│   └── 010_add_dataset_training_status.sql
│
├── tests/                          # 测试 (1,601 tests)
├── docker-compose.yml              # 本地开发 (postgres + inference + training)
├── run_server.py                   # 快捷启动脚本
└── runs/train/                     # 训练输出 (weights, curves)
```

## 支持的字段

| 类别 ID | 字段名 | 说明 |
|---------|--------|------|
| 0 | invoice_number | 发票号码 |
| 1 | invoice_date | 发票日期 |
| 2 | invoice_due_date | 到期日期 |
| 3 | ocr_number | OCR 参考号 (瑞典支付系统) |
| 4 | bankgiro | Bankgiro 号码 |
| 5 | plusgiro | Plusgiro 号码 |
| 6 | amount | 金额 |
| 7 | supplier_organisation_number | 供应商组织号 |
| 8 | payment_line | 支付行 (机器可读格式) |
| 9 | customer_number | 客户编号 |

## 快速开始

### 1. 自动标注

```bash
# 使用双池模式 (CPU + GPU)
python -m training.cli.autolabel \
    --dual-pool \
    --cpu-workers 3 \
    --gpu-workers 1

# 单线程模式
python -m training.cli.autolabel --workers 4
```

### 2. 训练模型

```bash
# 从预训练模型开始训练
python -m training.cli.train \
    --model yolo26s.pt \
    --epochs 100 \
    --batch 16 \
    --name invoice_fields \
    --dpi 150

# 低内存模式
python -m training.cli.train \
    --model yolo26s.pt \
    --epochs 100 \
    --name invoice_fields \
    --low-memory

# 从检查点恢复训练
python -m training.cli.train \
    --model runs/train/invoice_fields/weights/last.pt \
    --epochs 100 \
    --name invoice_fields \
    --resume
```

### 3. 推理

```bash
# 命令行推理
python -m backend.cli.infer \
    --model runs/train/invoice_fields/weights/best.pt \
    --input path/to/invoice.pdf \
    --output result.json \
    --gpu
```

### 4. Web 应用

```bash
# 从 Windows PowerShell 启动
wsl bash -c "source ~/miniconda3/etc/profile.d/conda.sh && conda activate invoice-sm120 && cd /mnt/c/Users/yaoji/git/ColaCoder/invoice-master-poc-v2 && python run_server.py --port 8000"

# 启动前端
cd frontend && npm install && npm run dev
# 访问 http://localhost:5173
```

### 5. Docker 本地开发

```bash
docker-compose up
# inference: http://localhost:8000
# training: 轮询模式自动拾取任务
```

## 训练触发流程

推理服务通过 API 触发训练，训练在独立的 GPU 实例上执行：

```
Inference API                    PostgreSQL              Training (ACI)
    |                                |                        |
    POST /admin/training/trigger     |                        |
    |-> INSERT training_tasks ------>| status=pending          |
    |-> Azure SDK: create ACI --------------------------------> 启动
    |                                |                        |
    |                                |<-- SELECT pending -----+
    |                                |--- UPDATE running -----+
    |                                |                   执行训练...
    |                                |<-- UPDATE completed ---+
    |                                |    + model_path        |
    |                                |    + metrics      自动关机
    |                                |                        |
    GET /admin/training/{id}         |                        |
    |-> SELECT training_tasks ------>|                        |
    +-- return status + metrics      |                        |
```

## Web API 端点

**Public API:**

| 方法 | 端点 | 描述 |
|------|------|------|
| GET | `/api/v1/health` | 健康检查 |
| POST | `/api/v1/infer` | 上传文件并推理 |
| GET | `/api/v1/results/{filename}` | 获取可视化图片 |
| POST | `/api/v1/async/infer` | 异步推理 |
| GET | `/api/v1/async/status/{task_id}` | 查询异步任务状态 |

**Admin API** (需要 `X-Admin-Token` header):

| 方法 | 端点 | 描述 |
|------|------|------|
| POST | `/api/v1/admin/auth/login` | 管理员登录 |
| GET | `/api/v1/admin/documents` | 文档列表 |
| POST | `/api/v1/admin/documents/upload` | 上传 PDF |
| GET | `/api/v1/admin/documents/{id}` | 文档详情 |
| PATCH | `/api/v1/admin/documents/{id}/status` | 更新文档状态 |
| PATCH | `/api/v1/admin/documents/{id}/category` | 更新文档分类 |
| GET | `/api/v1/admin/documents/categories` | 获取分类列表 |
| POST | `/api/v1/admin/documents/{id}/annotations` | 创建标注 |

**Training API:**

| 方法 | 端点 | 描述 |
|------|------|------|
| POST | `/api/v1/admin/training/datasets` | 创建数据集 |
| GET | `/api/v1/admin/training/datasets` | 数据集列表 |
| GET | `/api/v1/admin/training/datasets/{id}` | 数据集详情 |
| DELETE | `/api/v1/admin/training/datasets/{id}` | 删除数据集 |
| POST | `/api/v1/admin/training/tasks` | 创建训练任务 |
| GET | `/api/v1/admin/training/tasks` | 任务列表 |
| GET | `/api/v1/admin/training/tasks/{id}` | 任务详情 |
| GET | `/api/v1/admin/training/tasks/{id}/logs` | 训练日志 |

**Model Versions API:**

| 方法 | 端点 | 描述 |
|------|------|------|
| GET | `/api/v1/admin/models` | 模型版本列表 |
| GET | `/api/v1/admin/models/{id}` | 模型详情 |
| POST | `/api/v1/admin/models/{id}/activate` | 激活模型 |
| POST | `/api/v1/admin/models/{id}/archive` | 归档模型 |
| DELETE | `/api/v1/admin/models/{id}` | 删除模型 |

## Python API

```python
from backend.pipeline import InferencePipeline

# 初始化
pipeline = InferencePipeline(
    model_path='runs/train/invoice_fields/weights/best.pt',
    confidence_threshold=0.25,
    use_gpu=True,
    dpi=150,
    enable_fallback=True
)

# 处理 PDF
result = pipeline.process_pdf('invoice.pdf')

print(result.fields)
# {'InvoiceNumber': '12345', 'Amount': '1234.56', ...}

print(result.confidence)
# {'InvoiceNumber': 0.95, 'Amount': 0.92, ...}

# 交叉验证
if result.cross_validation:
    print(f"OCR match: {result.cross_validation.ocr_match}")
```

```python
from backend.pipeline.payment_line_parser import PaymentLineParser
from backend.pipeline.customer_number_parser import CustomerNumberParser

# Payment Line 解析
parser = PaymentLineParser()
result = parser.parse("# 94228110015950070 # 15658 00 8 > 48666036#14#")
print(f"OCR: {result.ocr_number}, Amount: {result.amount}")

# Customer Number 解析
parser = CustomerNumberParser()
result = parser.parse("Said, Shakar Umj 436-R Billo")
print(f"Customer Number: {result}")  # "UMJ 436-R"
```

## 推理流水线 (Two-Stage Detection)

```
YOLO bbox -> crop -> PaddleOCR -> [all tokens] -> ValueSelector -> normalizer
                                       |                |
                                  individual         selected
                                  text lines         value token(s)
```

**BBox 扩展**: 所有字段统一使用 15px 填充（150 DPI 下约 2.5mm），不做方向性扩展，不依赖布局假设。

**ValueSelector**: 在 OCR 和 normalizer 之间按字段类型过滤标签文本，只保留值 token：

| 字段 | 选择策略 | 示例输入 -> 输出 |
|------|---------|-----------------|
| InvoiceDate / DueDate | 日期模式匹配 | "Fakturadatum 2024-01-15" -> "2024-01-15" |
| Amount | 金额模式匹配 | "Belopp 1 234,56 kr" -> "1 234,56" |
| Bankgiro / Plusgiro | Giro 号码模式 | "BG: 123-4567" -> "123-4567" |
| OCR | 最长数字序列 (>=5位) | "OCR 94228110015950070" -> "94228110015950070" |
| InvoiceNumber | 排除瑞典语标签 | "Fakturanr INV-2024-001" -> "INV-2024-001" |
| payment_line | 保留全部 | 不过滤 |

如果没有匹配到任何模式，回退返回全部 token（永远不会比之前更差）。

## DPI 配置

系统所有组件统一使用 **150 DPI**。DPI 必须在训练和推理时保持一致。

| 组件 | 配置位置 |
|------|---------|
| 全局常量 | `packages/shared/shared/config.py` -> `DEFAULT_DPI = 150` |
| Web 推理 | `packages/backend/backend/web/config.py` -> `ModelConfig.dpi` |
| CLI 推理 | `python -m backend.cli.infer --dpi 150` |
| 自动标注 | `packages/shared/shared/config.py` -> `AUTOLABEL['dpi']` |

## 数据库架构

| 数据库 | 用途 | 存储内容 |
|--------|------|----------|
| **PostgreSQL** | 主数据库 | 文档、标注、训练任务、数据集、模型版本 |

### 主要表

| 表名 | 说明 |
|------|------|
| `admin_documents` | 文档管理 (PDF 元数据, 状态, 分类) |
| `admin_annotations` | 标注数据 (YOLO 格式边界框) |
| `training_tasks` | 训练任务 (状态, 配置, 指标) |
| `training_datasets` | 数据集 (train/val/test 分割) |
| `dataset_documents` | 数据集-文档关联 |
| `model_versions` | 模型版本管理 (激活/归档) |
| `admin_tokens` | 管理员认证令牌 |
| `async_requests` | 异步推理请求 |

### 数据集状态

| 状态 | 说明 |
|------|------|
| `building` | 正在构建数据集 |
| `ready` | 数据集就绪，可开始训练 |
| `trained` | 已完成训练 |
| `failed` | 构建失败 |
| `archived` | 已归档 |

### 训练状态

| 状态 | 说明 |
|------|------|
| `pending` | 等待执行 |
| `scheduled` | 已计划 |
| `running` | 正在训练 |
| `completed` | 训练完成 |
| `failed` | 训练失败 |
| `cancelled` | 已取消 |

## 测试

```bash
# 运行所有测试
DB_PASSWORD=xxx pytest tests/ -q

# 运行并查看覆盖率
DB_PASSWORD=xxx pytest tests/ --cov=packages --cov-report=term-missing
```

| 指标 | 数值 |
|------|------|
| **测试总数** | 2,047 |
| **通过率** | 100% |
| **覆盖率** | 72% |

## 存储抽象层

统一的文件存储接口，支持多后端切换：

| 后端 | 用途 | 安装 |
|------|------|------|
| **Local** | 本地开发/测试 | 默认 |
| **Azure Blob** | Azure 云部署 | `pip install -e "packages/shared[azure]"` |
| **AWS S3** | AWS 云部署 | `pip install -e "packages/shared[s3]"` |

### 配置文件 (storage.yaml)

```yaml
backend: ${STORAGE_BACKEND:-local}
presigned_url_expiry: 3600

local:
  base_path: ${STORAGE_BASE_PATH:-./data/storage}

azure:
  connection_string: ${AZURE_STORAGE_CONNECTION_STRING}
  container_name: ${AZURE_STORAGE_CONTAINER:-documents}

s3:
  bucket_name: ${AWS_S3_BUCKET}
  region_name: ${AWS_REGION:-us-east-1}
```

### 使用示例

```python
from shared.storage import get_storage_backend

# 从配置文件加载
storage = get_storage_backend("storage.yaml")

# 上传文件
storage.upload(Path("local.pdf"), "documents/invoice.pdf")

# 获取预签名 URL (前端访问)
url = storage.get_presigned_url("documents/invoice.pdf", expires_in_seconds=3600)
```

### 环境变量

| 变量 | 后端 | 说明 |
|------|------|------|
| `STORAGE_BACKEND` | 全部 | `local`, `azure_blob`, `s3` |
| `STORAGE_BASE_PATH` | Local | 本地存储路径 |
| `AZURE_STORAGE_CONNECTION_STRING` | Azure | 连接字符串 |
| `AZURE_STORAGE_CONTAINER` | Azure | 容器名称 |
| `AWS_S3_BUCKET` | S3 | 存储桶名称 |
| `AWS_REGION` | S3 | 区域 (默认: us-east-1) |

## 数据增强

训练时支持多种数据增强策略：

| 增强类型 | 说明 |
|----------|------|
| `perspective_warp` | 透视变换 (模拟扫描角度) |
| `wrinkle` | 皱纹效果 |
| `edge_damage` | 边缘损坏 |
| `stain` | 污渍效果 |
| `lighting_variation` | 光照变化 |
| `shadow` | 阴影效果 |
| `gaussian_blur` | 高斯模糊 |
| `motion_blur` | 运动模糊 |
| `gaussian_noise` | 高斯噪声 |
| `salt_pepper` | 椒盐噪声 |
| `paper_texture` | 纸张纹理 |
| `scanner_artifacts` | 扫描伪影 |

增强配置示例：

```json
{
  "augmentation": {
    "gaussian_blur": { "enabled": true, "kernel_size": 5 },
    "perspective_warp": { "enabled": true, "intensity": 0.1 }
  },
  "augmentation_multiplier": 2
}
```

## 前端功能

React 前端提供以下功能模块：

| 模块 | 功能 |
|------|------|
| **Dashboard** | 文档列表、上传、标注状态管理、分类筛选 |
| **Training** | 数据集创建/管理、训练任务配置、增强设置 |
| **Models** | 模型版本管理、激活/归档、指标查看 |
| **Inference Demo** | 实时推理演示、结果可视化 |

### 启动前端

```bash
cd frontend
npm install
npm run dev
# 访问 http://localhost:5173
```

## 技术栈

| 组件 | 技术 |
|------|------|
| **目标检测** | YOLO26 (Ultralytics >= 8.4.0) |
| **OCR 引擎** | PaddleOCR v5 (PP-OCRv5) |
| **PDF 处理** | PyMuPDF (fitz) |
| **数据库** | PostgreSQL + SQLModel |
| **Web 框架** | FastAPI + Uvicorn |
| **前端** | React + TypeScript + Vite + TailwindCSS |
| **状态管理** | React Query (TanStack Query) |
| **深度学习** | PyTorch + CUDA 12.x |
| **部署** | Docker + Azure/AWS (训练) / App Service (推理) |

## 环境变量

| 变量 | 必需 | 说明 |
|------|------|------|
| `DB_PASSWORD` | 是 | PostgreSQL 密码 |
| `DB_HOST` | 否 | 数据库主机 (默认: localhost) |
| `DB_PORT` | 否 | 数据库端口 (默认: 5432) |
| `DB_NAME` | 否 | 数据库名 (默认: docmaster) |
| `DB_USER` | 否 | 数据库用户 (默认: docmaster) |
| `STORAGE_BASE_PATH` | 否 | 存储路径 (默认: ~/invoice-data/data) |
| `MODEL_PATH` | 否 | 模型路径 |
| `CONFIDENCE_THRESHOLD` | 否 | 置信度阈值 (默认: 0.5) |
| `SERVER_HOST` | 否 | 服务器主机 (默认: 0.0.0.0) |
| `SERVER_PORT` | 否 | 服务器端口 (默认: 8000) |

## 许可证

MIT License
