🎉 feat: 建立阶段 0 工程骨架与 OpenAPI 契约机器化(WP-01 起步)
- Makefile:统一命令入口(backend/engine/docker/openapi/generate,含 make help) - backend:FastAPI + uv 最小工程,统一错误体(契约第 7 节)、ADR-015 平台 ID 生成与校验(uuid-utils);pytest 7 项、ruff、mypy strict、启动冒烟全部通过 - engine:灵台引擎 C++23/CMake 骨架,按 Lingtai C ABI 契约实现 lingtai.h(13 个 HjStatus、版本协商、句柄表防二次释放、线程局部错误上下文);doctest 5 用例通过,nm 验证仅导出 hj_* 符号 - openapi:OpenAPI 3.1 单一真源初始版(错误体/分页/Money/首批平台 ID/ContentBlock 全 21 类型/流事件 envelope),Redocly lint 0 错误、bundle 通过 - frontend:Next.js 最小骨架,openapi-typescript 生成 API 类型并通过 strict tsc - docker:hunjing-postgres(pgvector:17)compose;.env.example 与 config 示例 - ADR-025:登记测试与契约工具链(doctest/libFuzzer/Redocly/openapi-typescript/uuid-utils) - desktop:Tauri v2 占位与初始化说明(WP-10 启动)
This commit is contained in:
1 parent
5da1efe921
commit
3fbcd0437c
32 files changed
+11459
No files matched your search
@@ -0,0 +1 @@
|
||||
"""Hunjing 后端应用包。"""
|
||||
@@ -0,0 +1 @@
|
||||
"""核心模块:配置、错误与 ID。"""
|
||||
@@ -0,0 +1,19 @@
|
||||
"""应用配置:环境变量优先,秘密不落盘。"""
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="HUNJING_", env_file=".env", extra="ignore")
|
||||
|
||||
env: str = "development"
|
||||
log_level: str = "info"
|
||||
api_port: int = 8000
|
||||
database_url: str = "postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""统一错误体与领域异常(契约见 docs/contracts/api-conventions.md 第 7 节)。
|
||||
|
||||
`code` 是稳定机器值;`message` 可本地化;`details` 不稳定但必须脱敏。
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ErrorBody(BaseModel):
|
||||
code: str
|
||||
message: str
|
||||
trace_id: str | None = None
|
||||
request_id: str | None = None
|
||||
details: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ErrorEnvelope(BaseModel):
|
||||
error: ErrorBody
|
||||
|
||||
|
||||
class HunjingError(Exception):
|
||||
"""领域异常基类:映射为稳定错误码的 HTTP 响应。"""
|
||||
|
||||
status_code: int = 500
|
||||
code: str = "internal"
|
||||
|
||||
def __init__(self, message: str, *, details: dict[str, Any] | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.details = details
|
||||
|
||||
|
||||
class ValidationFailed(HunjingError):
|
||||
status_code = 422
|
||||
code = "validation_failed"
|
||||
|
||||
|
||||
def _envelope(
|
||||
code: str, message: str, request: Request, details: dict[str, Any] | None
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message,
|
||||
"trace_id": None,
|
||||
"request_id": request.headers.get("Hunjing-Request-ID"),
|
||||
"details": details,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def install_error_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(HunjingError)
|
||||
async def _hunjing_error(request: Request, exc: HunjingError) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=_envelope(exc.code, exc.message, request, exc.details),
|
||||
)
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def _validation_error(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
field_errors = [
|
||||
{"path": ".".join(str(p) for p in err["loc"]), "reason": err["type"]}
|
||||
for err in exc.errors()
|
||||
]
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
content=_envelope(
|
||||
"validation_failed", "请求字段不合法。", request, {"field_errors": field_errors}
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
"""平台 ID 生成与校验(ADR-015 与 docs/contracts/README.md 生成规则)。
|
||||
|
||||
格式固定为 ``<prefix><lowercase-canonical-uuid>``;解析时必须校验前缀与
|
||||
UUID version。UUID 生成使用成熟实现 uuid-utils,不自行拼位。
|
||||
"""
|
||||
|
||||
import re
|
||||
from uuid import UUID
|
||||
|
||||
from uuid_utils import uuid4, uuid7
|
||||
|
||||
_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
|
||||
def new_id(prefix: str, *, version: int = 7) -> str:
|
||||
"""生成带登记前缀的平台 ID;持久化资源默认 UUIDv7。"""
|
||||
value = uuid7() if version == 7 else uuid4()
|
||||
return f"{prefix}{value}"
|
||||
|
||||
|
||||
def parse_id(value: str, prefix: str, *, version: int = 7) -> UUID:
|
||||
"""校验并解析平台 ID:前缀与 UUID version 均须匹配,否则抛 ValueError。"""
|
||||
if not value.startswith(prefix):
|
||||
raise ValueError(f"ID 前缀不合法:期望 {prefix!r}")
|
||||
raw = value[len(prefix) :]
|
||||
if not _UUID_RE.match(raw):
|
||||
raise ValueError("ID 不是 canonical 小写 UUID 文本")
|
||||
parsed = UUID(raw)
|
||||
if parsed.version != version:
|
||||
raise ValueError(f"ID version 不合法:期望 UUIDv{version}")
|
||||
return parsed
|
||||
|
||||
|
||||
def is_valid_id(value: str, prefix: str, *, version: int = 7) -> bool:
|
||||
try:
|
||||
parse_id(value, prefix, version=version)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,27 @@
|
||||
"""FastAPI 应用工厂。"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.errors import install_error_handlers
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
settings = get_settings()
|
||||
app = FastAPI(
|
||||
title="Hunjing API",
|
||||
version="0.1.0",
|
||||
# 公共契约位于 docs/contracts 与 openapi/;本服务 docs 仅供开发调试。
|
||||
docs_url="/_dev/docs",
|
||||
openapi_url="/_dev/openapi.json",
|
||||
)
|
||||
install_error_handlers(app)
|
||||
|
||||
@app.get("/healthz", tags=["ops"])
|
||||
async def healthz() -> dict[str, str]:
|
||||
return {"status": "ok", "env": settings.env}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
Reference in new issue
Block a user