chore: bootstrap repo tooling, dependency and quality gates
uv-managed PEP 621 project with ruff, mypy (strict on packages/domain and packages/llm), pytest with coverage, and import-linter contracts that mechanically enforce the dependency rules of 架构文档 §4.2 -- including the two forbidden edges (compliance and identity must not depend on business domains). CI, pre-commit and a detect-secrets baseline gate every push; .env is gitignored before anything is ever staged.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# 美搭 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](https://docs.astral.sh/uv/)。
|
||||
|
||||
```bash
|
||||
# 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/*` 作为等价别名同样可用
|
||||
(内部脚本与部分文档用的是后者,见下方「与设计文档的偏差」)。
|
||||
|
||||
```bash
|
||||
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 内部,
|
||||
现场很难看出根因。模板里已经这么写了,改的时候别改回去。
|
||||
Reference in New Issue
Block a user