xsl 8a4720472b build: local stack, container images and the end-to-end smoke script
docker-compose brings up PostgreSQL 16 with pgvector and Redis 7 on
non-standard host ports so it never collides with an existing local database.
scripts/smoke.sh is the proof: compose, migrate, boot the API, issue a token,
call the real DeepSeek API and print the resulting ob_ai_call and bl_ledger
rows. services/vision and services/matting are placeholders noting that the
local RTX 5090 makes BiRefNet and RTMPose developable without cloud GPU.
2026-08-14 00:26:39 +08:00

美搭 backend — 工程地基 (M1)

「美搭」智能穿搭美学助手的服务端。本仓库当前是 工程地基,不是业务实现: 骨架、横切能力、以及一条打通全链路的最小垂直切片。

设计文档是唯一事实来源,在 Windows 侧的 G:\meida\docs\: 《技术架构文档》《需求规格说明》《开发任务拆解》。本 README 只讲怎么跑起来。


现在有什么 / 现在没有什么

已实现 状态
仓库结构与包边界(架构 §16.1) ✅
ruff / mypy strict / import-linter / pytest / 密钥扫描 全部进 CI(§16.2) ✅
统一响应与错误码 + 异常处理(§9.2, FR-SYS-02) ✅
结构化日志 + 请求/追踪 ID + 日志脱敏(NFR-SEC-03) ✅
异步 SQLAlchemy + Alembic(含 pgvector)+ 首个迁移 ✅
OTel + Prometheus + /healthz /readyz /metrics ✅
LLM 网关:路由 / 故障转移 / 熔断 / 结构化输出 / Prompt 版本 / 缓存前缀纪律 / 成本归因 / 日预算硬熔断 ✅
配额三段式 reserve → commit → refund(§7.4, FR-BILL-05) ✅
统一任务中心 状态机 + Celery 四队列 + 一个贯通的 ping 任务 ✅
垂直切片 POST /v1/agent/chat ✅
刻意留白(占位) 归属任务
华为账号登录、手机号验证码、双令牌撤销、游客合并、注销 T-P1-12 ~ T-P1-16
统一合规出口(审核 / 水印 / 元数据 / 内容编号) T-P1-22、T-P1-23(CR-05/CR-06 不可裁剪)
知识库、混合检索、规则引擎、记忆系统 M2
报告生成、换装、发型、视觉服务、抠图 M3 / M4
智谱 GLM provider(仅结构占位) 需先核实备案模型清单
SSE 流式对话、意图识别、生成后校验 FR-AGT-01/02/05

开始写业务功能前请先读 packages/README.md 与对应域的 README —— 里面写了 什么能放进去、什么绝对不能。


快速开始

前置:Python 3.12、Docker、uv。

# 1. 依赖(本仓库用 uv 管理;uv.lock 锁定全部版本)
make install

# 2. 配置。复制模板后至少填 DEEPSEEK_API_KEY 与 JWT_SECRET
cp .env.example .env
$EDITOR .env

# 3. 起本地基础设施(PostgreSQL 16 + pgvector,Redis 7)
make up
make migrate

# 4. 跑起来
make dev        # API  → http://localhost:8000/api/v1/docs
make worker     # Celery worker(另开一个终端)

# 5. 真实链路验证:会真的调用 DeepSeek,花真钱
make smoke

包管理器:本仓库使用 uv(uv sync --extra dev,锁文件 uv.lock)。 没有 uv 时 make install 会退回 python -m venv + pip install -e ".[dev]", 但 CI 与 Docker 镜像都走 uv,因此以 uv.lock 为准。

常用命令

命令 作用
make lint ruff check + format check
make typecheck mypy(packages/domain、packages/llm 为 strict)
make importlint 校验架构 §4.2 的域间依赖规则
make test 上面三项 + pytest(本地质量门,与 CI 一致)
make migrate / make makemigration m="..." Alembic
make up / make down 本地 PG + Redis
make smoke 真实 DeepSeek 端到端验证

垂直切片:POST /v1/agent/chat

一次请求依次经过:

JWT 鉴权
  → billing.reserve()           配额预扣,写 bl_ledger(status=reserved)
  → LLM 网关                     场景路由 → 缓存前缀校验 → 日预算闸门 → DeepSeek
  → ob_ai_call                  每次 provider 调用(含失败与重试)写一行
  → billing.commit() / refund() 成功确认扣除,失败自动退还
  → 统一响应 {code, message, data, request_id}

接口基础路径是架构 §9.1 规定的 /api/v1;/v1/* 作为等价别名同样可用 (内部脚本与部分文档用的是后者,见下方「与设计文档的偏差」)。

TOKEN=$(curl -s -XPOST localhost:8000/v1/auth/dev-token \
  -H 'Content-Type: application/json' -d '{"device_key":"me"}' \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["access_token"])')

curl -s -XPOST localhost:8000/v1/agent/chat \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"message":"面试穿什么?"}' | python3 -m json.tool

地基验收结果(M1 出口)

以下均在本机实跑通过,不是「应该可以」:

项 结果
make lint / make typecheck / make test 全绿;147 unit + 3 integration,packages/llm + packages/domain 覆盖率 97%
import-linter 6 条契约全部 KEPT(含 compliance / identity 两条禁止依赖)
make up + make migrate 8 张表 + alembic_version 建成,vector 扩展存在,配额规则已播种
alembic downgrade base → upgrade head 可逆,往返成功
make smoke(真实 DeepSeek) 通过,模型 deepseek-v4-flash;第二次调用 tokens_cached=256,证明缓存前缀纪律真的生效
Celery 全链路 pending → queued → running → succeeded,tk_task.result 落库

GET https://api.deepseek.com/v1/models 当前只返回两个模型: deepseek-v4-flash 与 deepseek-v4-pro。默认用前者,通过 DEEPSEEK_MODEL 切换, 调用点不写死。


密钥

  • .env 在 .gitignore 第一条,永远不要提交。
  • .env.example 记录每一个变量的含义,不含任何真实值。
  • pre-commit 与 CI 都跑密钥扫描(detect-secrets + gitleaks)。
  • 生产环境的密钥来自 SSM/KMS,不来自文件(架构 §11.2)。

本地 GPU

开发机带一块 RTX 5090(32GB VRAM)。这意味着 BiRefNet 抠图(TD-07)与 RTMPose ONNX 人体关键点(TD-05/TD-06)可以完全在本地开发和调参,不需要租云 GPU。相关占位见 services/vision/ 与 services/matting/。 本仓库尚未实现它们。


与设计文档的偏差 / 文档待补

记录在此,以便回灌设计文档:

  1. 业务域包的位置:架构 §16.1 的目录树列出了 packages/{domain,llm,retrieval, rules,memory,compliance,shared},但 §4.1 定义了 11 个业务域。由于 compliance 同时出现在两处,本仓库按「业务域各自是 packages/<域>/, packages/domain/ 只放纯逻辑」来落地。建议在 §16.1 补全域目录。
  2. 接口前缀:§9.1 规定 /api/v1,但多处任务描述使用 /v1。本仓库以 /api/v1 为准,并在 ASGI 层把 /v1/* 重写为等价路径。建议文档统一。
  3. bl_ledger 缺 status 列:§6.2 的列清单没有 status,但 §7.4 的三段式 明确要求 reserved → committed / refunded。本仓库补了该列。
  4. ob_ai_call 缺 Prompt 版本列:§8.1「可复现」要求记录 prompt 版本, §6.2 的列清单里没有。本仓库补了 prompt_id / prompt_version。
  5. 日成本熔断是「拒绝」而非「告警」:§13 写的是「超预算自动降级并告警」。 本仓库把它实现为硬闸门(非关键场景直接拒绝),因为重试风暴产生成本的速度 远快于人响应告警的速度。关键场景通过 SceneConfig.critical 豁免。
  6. 单位成本费率是占位值:.env.example 里的 LLM_COST_RATES_JSON 需要用 供应商实际价目表校准,否则 ob_ai_call.cost_cents 只有相对意义。
  7. .env 里的 JSON 值必须用单引号包住。scripts/smoke.sh 会 source .env, 裸双引号会被 bash 剥掉,JSON 随即损坏且报错发生在 pydantic-settings 内部, 现场很难看出根因。模板里已经这么写了,改的时候别改回去。
S
Description
No description provided
Readme
1.4 MiB
Languages
Python 98.4%
Jinja 0.7%
Shell 0.5%
Makefile 0.3%