🎉 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:
chenxin committed 2026-09-09 21:45:04 +08:00
1 parent 5da1efe921
commit 3fbcd0437c
32 files changed
+11459

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""Hunjing 后端应用包。"""
+1
View File
@@ -0,0 +1 @@
"""核心模块:配置、错误与 ID。"""
+19
View File
@@ -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()
+76
View File
@@ -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}
),
)
+39
View File
@@ -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
+27
View File
@@ -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()