返回队内档案

Backend 服务

Backend 是网站的推理适配层:接收 HTTP 上传、执行安全校验和并发控制,然后调用 remove_background_skill 的同源核心代码。协议逻辑不进入 Skill 核心。

当前技术与端口

项目当前实现
PythonPython 3.12 slim 镜像
APIFastAPI + Uvicorn
推理rembg 2.x + ONNX Runtime CPU
依赖管理uv + backend/uv.lock
容器端口8000
本机调试映射18000:8000
Dockerfilebackend/Dockerfile

当前接口

方法路径作用
GET/livez进程存活检查,不要求模型已经就绪
GET/readyz模型就绪检查;必要时再次尝试加载模型
POST/v1/remove-background接收图片并返回透明 PNG
GET/v1/observatory/{filename}观测站产物只读下载(白名单:status.json、forecasts.csv、latest-forecast.png、evaluation-history.png、dashboard.md、latest-paths.csv)

通过 gateway 访问业务接口时,外部路径是 /api/v1/remove-background;gateway 会剥掉 /api 前缀后再交给 Backend。

上传和安全限制

当前实现有两层大小限制:

  1. RequestSizeLimitMiddleware 在 multipart 解析前按实际收到的请求体累计字节;
  2. process_upload 再对图片文件自身做分块计数、真实格式解码和像素检查。

只接受真实格式为 JPEG、PNG、WEBP 的图片。成功响应为 image/png,并带有 Cache-Control: no-store

环境变量

所有运行配置由 backend/app/settings.py 读取:

变量默认值含义
PORT8000Uvicorn 监听端口
MODEL_NAMEisnet-general-userembg 模型名
MAX_REQUEST_BYTES11534336请求总体上限,默认 11 MiB
MAX_FILE_BYTES10485760图片文件上限,默认 10 MiB
MAX_PIXELS40000000解码后最大像素总数
MAX_UPLOAD_CONCURRENCY4同时接收上传的数量
MAX_INFERENCE_CONCURRENCY2同时执行 ONNX 推理的数量
ALLOWED_ORIGINS空列表逗号分隔的 CORS 白名单
OBSERVATORY_DIR/data/observatory观测站账本/图表输出目录(生产挂持久卷到 /data
OBSERVATORY_SCHEDULE08:00每日预测运行时刻(Asia/Shanghai,开盘前)
OBSERVATORY_ENABLED10 可整体停用观测站调度
KRONOS_DIR/app/vendor/kronosvendored Kronos 模型代码位置
OBSERVATORY_SCRIPT/app/observatory/csi1000_live_forecast.py预测脚本位置(与 Skill 同源)
WAREHOUSE_DIR/data/warehouse个人数据仓库 SQLite 文件目录(生产挂持久卷到 /data
WAREHOUSE_SCRIPT/app/warehouse/stock_daily_sync.py同步脚本位置
WAREHOUSE_BATCH_SIZE300每批处理的股票数
WAREHOUSE_POLL_INTERVAL15存量历史没追平前,两批之间等待的秒数
WAREHOUSE_SCHEDULE16:30追平之后,每天增量更新的时刻(Asia/Shanghai,收盘后)
WAREHOUSE_ENABLED10 可整体停用数据仓库同步

模型生命周期

isnet-general-use(rembg)与 Kronos/HF 权重都是运行期首次调用时惰性下载,缓存到 /data/models / /data/hf(持久卷)——不在构建阶段下载:构建环境的出网与生产容器不是一回事,实测构建期连 GitHub(rembg 模型源)和 hf-mirror.com 都可能超时。下载一次后常驻 /data,之后重启/重新部署直接读缓存,不再重复联网。

应用启动时在后台任务里预热模型 session(asyncio.to_thread),不阻塞启动——首次下载权重可能要几分钟,放前台会连 /livez 和观测站调度一起卡住。预热期间 /readyz/v1/remove-background 各自兜底:session 还没好就现场同步加载一次(等于把下载耗时转嫁到那次请求上),之后复用。推理本身仍由 asyncio.to_thread 放线程执行,并由 semaphore 限制并发。

观测站(中证1000 Kronos 每日预测)

  • 调度:app/observatory.py 在 lifespan 中启动每日定时任务(默认 08:00 Asia/Shanghai,开盘前); 启动时若从未生成过账本会先补跑一次。周末/节假日照常触发无害:脚本只认最新完整交易日,幂等空跑。
  • 执行:预测跑在子进程里(脚本与 skills/csi1000-kronos-forecast 同一份,构建时复制到 /app/observatory/),torch 与权重只在运行的几分钟内占内存,跑完随进程退出全部释放。
  • 权重:NeoQuasar/Kronos-Tokenizer-baseNeoQuasar/Kronos-base 运行期首次调用时惰性下载 (见上方”模型生命周期”),常驻 HF_HOME;行情数据由 akshare 运行期在线抓取(国内源)。
  • 持久化:账本 forecasts.csv 是追加式的,必须把持久卷挂到 /data(Zeabur Volume), 否则重新部署后历史准确率从零累计。
  • 模型代码:backend/vendor/kronos/(MIT,上游 shiyu-coder/Kronos,仅收录推理所需 model/ 包)。

个人数据仓库(全市场 A 股日线)

  • 这是 violet 自己攒着玩的数据仓库,不对外暴露任何接口——现在只是把数据存下来, 查询接口以后再单独做。跟观测站/抠图共用同一个 backend 服务是权衡后的选择(省一份基础设施), 代价是三者共享同一个容器/资源,任何一方出问题理论上都可能连累另外两个。
  • 数据源:baostock(免费、无需 token),只存不复权原始成交价(adjustflag=3)—— 复权是分析时的派生选择,不是历史事实本身。
  • 范围:只到股票(baostock type=1),暂不含指数/ETF/可转债/基金净值/宏观数据, 以后要加再扩展。
  • 存储:SQLite,WAREHOUSE_DIR/warehouse.db(生产挂持久卷到 /data),三张表 stocks(股票名单,含已退市)/ daily_bars(日线)/ sync_state(每只股票同步进度)。
  • 调度:app/warehouse.py 按批调用 backend/warehouse/stock_daily_sync.py(子进程, 一批处理 WAREHOUSE_BATCH_SIZE 只股票);股票名单几千只,一次全量回填要跑很久, 按批跑、进度记在 sync_state 表里,容器重启也不丢进度。存量历史没追平前每 WAREHOUSE_POLL_INTERVAL 秒跑一批;追平之后(某一批”nothing to sync”)退化成 每天 WAREHOUSE_SCHEDULE(默认收盘后)跑一次增量更新。

本地运行

推荐完整联调:

docker compose up --build
curl http://localhost:18000/livez
curl http://localhost:18000/readyz

直接运行源码时,需要让 Python 找到共享 Skill:

cd backend
PYTHONPATH=../skills/remove-background \
  uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

修改入口

  • 路由与模型生命周期:backend/app/main.py
  • 环境变量:backend/app/settings.py
  • 请求体硬限制:backend/app/middleware.py
  • 图片校验与响应:backend/app/adapters/remove_background_api.py
  • 共享核心:skills/remove-background/remove_background_skill/

新增接口或环境变量时必须同步更新本页;构建前的一致性检查会核对当前 FastAPI 路由、Settings 环境变量和 Compose 端口。