🎉 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,18 @@
|
|||||||
|
# ============================================================
|
||||||
|
# Hunjing 环境变量示例(复制为 .env 后按需修改;不要提交 .env)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# 运行环境:development / staging / production
|
||||||
|
HUNJING_ENV=development
|
||||||
|
|
||||||
|
# 数据库(PostgreSQL 统一,ADR-024;默认连接 docker-compose 启动的 hunjing-postgres)
|
||||||
|
DATABASE_URL=postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing
|
||||||
|
|
||||||
|
# 后端服务端口
|
||||||
|
HUNJING_API_PORT=8000
|
||||||
|
|
||||||
|
# 日志级别:debug / info / warning / error
|
||||||
|
HUNJING_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# 注意:密钥、Provider 凭据等敏感值只通过环境变量或秘密管理注入,
|
||||||
|
# 不写入本文件、config/ 真实配置或任何日志。
|
||||||
@@ -109,6 +109,10 @@ docker-compose.override.yml
|
|||||||
|
|
||||||
# Rust / 安全沙箱
|
# Rust / 安全沙箱
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# C++ 引擎与契约生成产物(源码构建可再生)
|
||||||
|
engine/build/
|
||||||
|
openapi/bundled.json
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
sandbox-work/
|
sandbox-work/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# 默认目标:显示帮助
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
# ===== 后端(Python / FastAPI,uv 管理) =====
|
||||||
|
|
||||||
|
.PHONY: backend-setup
|
||||||
|
backend-setup: ## 安装后端依赖(uv sync)
|
||||||
|
cd backend && uv sync
|
||||||
|
|
||||||
|
.PHONY: backend-dev
|
||||||
|
backend-dev: ## 启动后端开发服务(127.0.0.1:8000)
|
||||||
|
cd backend && uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
|
||||||
|
|
||||||
|
.PHONY: backend-test
|
||||||
|
backend-test: ## 运行后端测试(pytest)
|
||||||
|
cd backend && uv run pytest
|
||||||
|
|
||||||
|
.PHONY: backend-lint
|
||||||
|
backend-lint: ## 后端静态检查(ruff + mypy)
|
||||||
|
cd backend && uv run ruff check . && uv run mypy app
|
||||||
|
|
||||||
|
# ===== 灵台引擎(C++23 / CMake) =====
|
||||||
|
|
||||||
|
.PHONY: engine-build
|
||||||
|
engine-build: ## 构建灵台引擎(CMake + Ninja/Makefile)
|
||||||
|
cmake -S engine -B engine/build -DCMAKE_BUILD_TYPE=Release && cmake --build engine/build
|
||||||
|
|
||||||
|
.PHONY: engine-test
|
||||||
|
engine-test: ## 运行灵台引擎测试(ctest)
|
||||||
|
cmake -S engine -B engine/build -DCMAKE_BUILD_TYPE=Debug && cmake --build engine/build && ctest --test-dir engine/build --output-on-failure
|
||||||
|
|
||||||
|
# ===== 数据库(PostgreSQL 统一,ADR-024) =====
|
||||||
|
|
||||||
|
.PHONY: docker-up
|
||||||
|
docker-up: ## 启动本地 PostgreSQL(docker compose)
|
||||||
|
docker compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
|
.PHONY: docker-down
|
||||||
|
docker-down: ## 停止本地 PostgreSQL
|
||||||
|
docker compose -f docker/docker-compose.yml down
|
||||||
|
|
||||||
|
.PHONY: backend-migrate
|
||||||
|
backend-migrate: ## 执行数据库迁移(Alembic upgrade head)
|
||||||
|
cd backend && uv run alembic upgrade head
|
||||||
|
|
||||||
|
# ===== 契约(OpenAPI 3.1 单一真源) =====
|
||||||
|
|
||||||
|
.PHONY: openapi-lint
|
||||||
|
openapi-lint: ## 校验 OpenAPI 契约(redocly lint + bundle)
|
||||||
|
npx --yes @redocly/cli@latest lint openapi/openapi.yaml --config openapi/redocly.yaml && npx --yes @redocly/cli@latest bundle openapi/openapi.yaml -o openapi/bundled.json
|
||||||
|
|
||||||
|
.PHONY: generate
|
||||||
|
generate: openapi-lint ## 生成代码(TypeScript 类型等)
|
||||||
|
cd frontend && npm run generate:api
|
||||||
|
|
||||||
|
# ===== 前端与桌面(Next.js / Tauri v2) =====
|
||||||
|
|
||||||
|
.PHONY: frontend-setup
|
||||||
|
frontend-setup: ## 安装前端依赖
|
||||||
|
cd frontend && npm install
|
||||||
|
|
||||||
|
.PHONY: frontend-dev
|
||||||
|
frontend-dev: ## 启动前端开发服务(127.0.0.1:3000)
|
||||||
|
cd frontend && npm run dev
|
||||||
|
|
||||||
|
# ===== 帮助 =====
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## 显示本帮助
|
||||||
|
@echo "Hunjing 统一命令入口(详见各目标 ## 说明)"
|
||||||
|
@echo
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
[project]
|
||||||
|
name = "hunjing-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Hunjing 后端服务(FastAPI 模块化单体)"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115",
|
||||||
|
"uvicorn[standard]>=0.32",
|
||||||
|
"pydantic>=2.9",
|
||||||
|
"pydantic-settings>=2.6",
|
||||||
|
"sqlalchemy>=2.0",
|
||||||
|
"alembic>=1.14",
|
||||||
|
"asyncpg>=0.30",
|
||||||
|
"uuid-utils>=0.10",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.3",
|
||||||
|
"pytest-asyncio>=0.24",
|
||||||
|
"httpx>=0.27",
|
||||||
|
"ruff>=0.8",
|
||||||
|
"mypy>=1.13",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["app"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B", "ASYNC"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.12"
|
||||||
|
strict = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import create_app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client() -> TestClient:
|
||||||
|
return TestClient(create_app())
|
||||||
|
|
||||||
|
|
||||||
|
def test_healthz_ok(client: TestClient) -> None:
|
||||||
|
resp = client.get("/healthz")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["status"] == "ok"
|
||||||
|
assert "env" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_error_uses_contract_envelope(client: TestClient) -> None:
|
||||||
|
# 触发路径参数/请求体校验失败,验证统一错误体结构
|
||||||
|
resp = client.post("/healthz", json={"unexpected": 1})
|
||||||
|
assert resp.status_code in (405, 422)
|
||||||
|
if resp.status_code == 422:
|
||||||
|
err = resp.json()["error"]
|
||||||
|
assert err["code"] == "validation_failed"
|
||||||
|
assert err["details"]["field_errors"]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.core.ids import is_valid_id, new_id, parse_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_id_prefix_and_version() -> None:
|
||||||
|
value = new_id("user_")
|
||||||
|
assert value.startswith("user_")
|
||||||
|
parsed = parse_id(value, "user_")
|
||||||
|
assert parsed.version == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_id_is_uuidv4() -> None:
|
||||||
|
value = new_id("request_", version=4)
|
||||||
|
assert parse_id(value, "request_", version=4).version == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_wrong_prefix() -> None:
|
||||||
|
value = new_id("user_")
|
||||||
|
with pytest.raises(ValueError, match="前缀"):
|
||||||
|
parse_id(value, "run_")
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_wrong_version() -> None:
|
||||||
|
value = new_id("user_", version=4)
|
||||||
|
assert not is_valid_id(value, "user_")
|
||||||
|
with pytest.raises(ValueError, match="version"):
|
||||||
|
parse_id(value, "user_")
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_non_canonical_text() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
parse_id("user_NOT-A-UUID", "user_")
|
||||||
Generated
+1283
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
|||||||
|
# Hunjing 应用配置示例(复制为 app.yaml 使用;真实配置不提交)
|
||||||
|
# 环境变量优先级高于本文件;密钥不写入本文件。
|
||||||
|
|
||||||
|
app:
|
||||||
|
# 运行环境:development / staging / production
|
||||||
|
env: development
|
||||||
|
|
||||||
|
api:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8000
|
||||||
|
|
||||||
|
database:
|
||||||
|
# 连接串建议来自环境变量 DATABASE_URL;此处仅作覆盖示例
|
||||||
|
pool_min: 2
|
||||||
|
pool_max: 10
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: info
|
||||||
|
# 结构化 JSON 日志;敏感 Header 与正文自动脱敏
|
||||||
|
format: json
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 桌面与移动端(Tauri v2)
|
||||||
|
|
||||||
|
本目录承载 Tauri v2 桌面(Desktop)与 iOS/Android 移动端工程(WP-10)。
|
||||||
|
|
||||||
|
当前为阶段 0 占位;初始化方式(待 WP-10 启动时执行):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 以 frontend 为 Web 资产初始化 Tauri v2 工程
|
||||||
|
npm create tauri-app@latest -- --template next-ts
|
||||||
|
# 桌面与移动平台分别声明 Capability,敏感命令验证用户、设备与 DeviceGrant
|
||||||
|
```
|
||||||
|
|
||||||
|
要求见 `docs/architecture/media-and-device-architecture.md` 与 AGENTS.md 8.3。
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
hunjing-postgres:
|
||||||
|
image: pgvector/pgvector:17
|
||||||
|
container_name: hunjing-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: hunjing
|
||||||
|
POSTGRES_PASSWORD: hunjing_dev_password
|
||||||
|
POSTGRES_DB: hunjing
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5432:5432"
|
||||||
|
volumes:
|
||||||
|
- hunjing-pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U hunjing -d hunjing"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
hunjing-pgdata:
|
||||||
@@ -397,3 +397,23 @@ UUIDv7 为高写入关系表提供索引局部性和大致创建顺序;请求
|
|||||||
- 开发环境用 `docker compose`(或嵌入式 PostgreSQL)启动数据库;测试统一在 PostgreSQL 上运行,不用 SQLite 内存库替代,避免方言差异掩盖缺陷。
|
- 开发环境用 `docker compose`(或嵌入式 PostgreSQL)启动数据库;测试统一在 PostgreSQL 上运行,不用 SQLite 内存库替代,避免方言差异掩盖缺陷。
|
||||||
- 自托管两档(单机/标准服务器)均含 PostgreSQL 容器;单机档降低的是资源规格,不是能力面。
|
- 自托管两档(单机/标准服务器)均含 PostgreSQL 容器;单机档降低的是资源规格,不是能力面。
|
||||||
- 迁移与性能优化只需针对 PostgreSQL(JSONB、pgvector、tsvector、事务性 DDL、行级锁);索引命名按工程规范执行(见 AGENTS.md 第 8.6 节)。
|
- 迁移与性能优化只需针对 PostgreSQL(JSONB、pgvector、tsvector、事务性 DDL、行级锁);索引命名按工程规范执行(见 AGENTS.md 第 8.6 节)。
|
||||||
|
|
||||||
|
## ADR-025:建立工程时的测试与契约工具链登记
|
||||||
|
|
||||||
|
**状态**:采纳
|
||||||
|
|
||||||
|
**决策**
|
||||||
|
|
||||||
|
- C++(灵台引擎):单元测试采用 **doctest**(单头引入、编译快),fuzz 采用 **libFuzzer**(clang 原生 `-fsanitize=fuzzer`),fuzz 必测目标为 `hj_transform` 与 `hj_policy`。
|
||||||
|
- Python(backend):pytest + pytest-asyncio,静态检查 Ruff + mypy(工程规则既有约定,此处一并登记)。
|
||||||
|
- OpenAPI 契约工具链:**Redocly CLI**(lint + bundle 校验单一真源),TypeScript 类型用 **openapi-typescript** 生成,禁止手写 API DTO。
|
||||||
|
- UUIDv7 生成采用成熟实现 **uuid-utils**(Python 侧),遵循契约"不自行拼位"规则。
|
||||||
|
|
||||||
|
**理由**
|
||||||
|
|
||||||
|
doctest 与 libFuzzer 与 clang 工具链零摩擦、依赖最轻;Redocly 与 openapi-typescript 是 OpenAPI 3.1 支持最完整的开源组合;避免为同一职责引入第二套工具。
|
||||||
|
|
||||||
|
**后果**
|
||||||
|
|
||||||
|
- engine 与 openapi 的 CI 检查以本登记为准;更换任一工具须更新本 ADR。
|
||||||
|
- libFuzzer 语料与 doctest 用例随引擎模块一同交付,属于 WP-15 验收证据。
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
BasedOnStyle: Google
|
||||||
|
IndentWidth: 2
|
||||||
|
ColumnLimit: 100
|
||||||
|
AccessModifierOffset: -1
|
||||||
|
AllowShortFunctionsOnASingleLine: Inline
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
project(hunjing_lingtai VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# 编译警告全开并视为错误(AGENTS.md 8.4)
|
||||||
|
if(MSVC)
|
||||||
|
add_compile_options(/W4 /WX)
|
||||||
|
else()
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-conversion -Werror)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 符号默认隐藏,仅导出 hj_*(API 卫生;由 HJ_API 宏显式导出)
|
||||||
|
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
||||||
|
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
|
||||||
|
|
||||||
|
add_library(hunjing SHARED src/lingtai.cpp)
|
||||||
|
target_include_directories(hunjing PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||||
|
|
||||||
|
# 单元测试(doctest,ADR-025);单头直接引入,不用 FetchContent 拉网络
|
||||||
|
add_executable(lingtai_tests tests/test_core.cpp src/lingtai.cpp)
|
||||||
|
target_include_directories(lingtai_tests PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_test(NAME lingtai_tests COMMAND lingtai_tests)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Hunjing 灵台引擎公共头(Lingtai C ABI)。
|
||||||
|
// 契约真源:docs/contracts/lingtai-ffi.md;本文件是机器化前的稳定声明基线。
|
||||||
|
// 只导出 extern "C" 的 hj_* 符号与不透明句柄;异常、STL、RTTI 不跨越边界。
|
||||||
|
#ifndef HUNJING_LINGTAI_H
|
||||||
|
#define HUNJING_LINGTAI_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#if defined(HJ_BUILDING)
|
||||||
|
#define HJ_API __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define HJ_API __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define HJ_API __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 当前 ABI major;major 不匹配时 hj_engine_new 拒绝初始化。
|
||||||
|
#define HJ_ABI_VERSION 1
|
||||||
|
|
||||||
|
// 不透明句柄:字段布局不进入契约。
|
||||||
|
typedef struct HjEngine HjEngine;
|
||||||
|
typedef struct HjBuffer HjBuffer;
|
||||||
|
|
||||||
|
// 稳定状态码(登记于契约第 5 节;新增不破坏兼容)。
|
||||||
|
typedef enum HjStatus {
|
||||||
|
HJ_OK = 0,
|
||||||
|
HJ_ERR_INVALID_ARGUMENT = 1,
|
||||||
|
HJ_ERR_OUT_OF_MEMORY = 2,
|
||||||
|
HJ_ERR_UNSUPPORTED_ABI = 3,
|
||||||
|
HJ_ERR_HANDLE_INVALID = 4,
|
||||||
|
HJ_ERR_REENTRANCY = 5,
|
||||||
|
HJ_ERR_PARSE_FAILED = 6,
|
||||||
|
HJ_ERR_LIMIT_EXCEEDED = 7,
|
||||||
|
HJ_ERR_POLICY_DENIED = 8,
|
||||||
|
HJ_ERR_SIGNATURE_INVALID = 9,
|
||||||
|
HJ_ERR_INDEX_CORRUPT = 10,
|
||||||
|
HJ_ERR_BUFFER_TOO_SMALL = 11,
|
||||||
|
HJ_ERR_INTERNAL = 12
|
||||||
|
} HjStatus;
|
||||||
|
|
||||||
|
// 返回引擎构建时的 ABI major。
|
||||||
|
HJ_API uint32_t hj_abi_version(void);
|
||||||
|
|
||||||
|
// 创建引擎句柄;宿主传入所需 major,不匹配返回 HJ_ERR_UNSUPPORTED_ABI 且不产生句柄。
|
||||||
|
// 成功时 *out_engine 非空。HjEngine 线程安全。
|
||||||
|
HJ_API HjStatus hj_engine_new(uint32_t required_abi_major, HjEngine **out_engine);
|
||||||
|
|
||||||
|
// 释放引擎句柄;句柄不在登记表(含二次释放)返回 HJ_ERR_HANDLE_INVALID,不崩溃。
|
||||||
|
HJ_API HjStatus hj_engine_free(HjEngine *engine);
|
||||||
|
|
||||||
|
// 释放引擎分配的缓冲;仅用于引擎产出的 HjBuffer。
|
||||||
|
HJ_API HjStatus hj_buffer_free(HjBuffer *buffer);
|
||||||
|
|
||||||
|
// 读取缓冲内容与长度;非法句柄返回 NULL / 0。
|
||||||
|
HJ_API const void *hj_buffer_data(const HjBuffer *buffer);
|
||||||
|
HJ_API size_t hj_buffer_len(const HjBuffer *buffer);
|
||||||
|
|
||||||
|
// 线程局部的最近一次错误消息(UTF-8、脱敏、不含正文与密钥);下一次调用覆盖。
|
||||||
|
HJ_API const char *hj_last_error(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // HUNJING_LINGTAI_H
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// 灵台引擎核心(太一):版本协商、句柄表与错误上下文的最小实现。
|
||||||
|
// 全部入口按不可信输入处理;异常不得跨越 ABI 边界。
|
||||||
|
#include "hunjing/lingtai.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
// 不透明句柄的实际布局(不进入契约;仅本翻译单元可见字段)。
|
||||||
|
struct HjEngine {
|
||||||
|
uint32_t abi_major;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HjBuffer {
|
||||||
|
size_t len;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 线程局部错误消息(脱敏、UTF-8);下一次引擎调用覆盖。
|
||||||
|
thread_local std::string g_last_error;
|
||||||
|
|
||||||
|
void set_last_error(const char *message) { g_last_error = message; }
|
||||||
|
|
||||||
|
// 句柄表:登记活跃引擎句柄,支撑二次释放与非法句柄检测(不崩溃)。
|
||||||
|
std::mutex g_handle_table_mutex;
|
||||||
|
std::unordered_set<HjEngine *> g_handle_table;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
uint32_t hj_abi_version(void) { return HJ_ABI_VERSION; }
|
||||||
|
|
||||||
|
HjStatus hj_engine_new(uint32_t required_abi_major, HjEngine **out_engine) {
|
||||||
|
try {
|
||||||
|
set_last_error("");
|
||||||
|
if (out_engine == nullptr) {
|
||||||
|
set_last_error("out_engine 不能为空");
|
||||||
|
return HJ_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
*out_engine = nullptr;
|
||||||
|
if (required_abi_major != HJ_ABI_VERSION) {
|
||||||
|
set_last_error("ABI major 不匹配:拒绝初始化,不静默降级");
|
||||||
|
return HJ_ERR_UNSUPPORTED_ABI;
|
||||||
|
}
|
||||||
|
auto *engine = new (std::nothrow) HjEngine{required_abi_major};
|
||||||
|
if (engine == nullptr) {
|
||||||
|
set_last_error("引擎内存分配失败");
|
||||||
|
return HJ_ERR_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_handle_table_mutex);
|
||||||
|
g_handle_table.insert(engine);
|
||||||
|
}
|
||||||
|
*out_engine = engine;
|
||||||
|
return HJ_OK;
|
||||||
|
} catch (...) {
|
||||||
|
// 入口捕获一切异常并转换为 HjStatus(契约禁令)。
|
||||||
|
set_last_error("引擎内部错误");
|
||||||
|
return HJ_ERR_INTERNAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HjStatus hj_engine_free(HjEngine *engine) {
|
||||||
|
try {
|
||||||
|
set_last_error("");
|
||||||
|
{
|
||||||
|
const std::lock_guard<std::mutex> lock(g_handle_table_mutex);
|
||||||
|
if (g_handle_table.erase(engine) == 0) {
|
||||||
|
set_last_error("句柄不在登记表:已释放或非法");
|
||||||
|
return HJ_ERR_HANDLE_INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete engine;
|
||||||
|
return HJ_OK;
|
||||||
|
} catch (...) {
|
||||||
|
set_last_error("引擎内部错误");
|
||||||
|
return HJ_ERR_INTERNAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HjStatus hj_buffer_free(HjBuffer *buffer) {
|
||||||
|
try {
|
||||||
|
set_last_error("");
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
set_last_error("buffer 句柄为空");
|
||||||
|
return HJ_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
free(buffer);
|
||||||
|
return HJ_OK;
|
||||||
|
} catch (...) {
|
||||||
|
set_last_error("引擎内部错误");
|
||||||
|
return HJ_ERR_INTERNAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const void *hj_buffer_data(const HjBuffer *buffer) {
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return static_cast<const void *>(buffer + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t hj_buffer_len(const HjBuffer *buffer) {
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return buffer->len;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *hj_last_error(void) { return g_last_error.c_str(); }
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 灵台引擎核心测试(doctest,ADR-025)。
|
||||||
|
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "hunjing/lingtai.h"
|
||||||
|
|
||||||
|
TEST_CASE("ABI 版本协商") {
|
||||||
|
CHECK(hj_abi_version() == HJ_ABI_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("引擎创建与释放") {
|
||||||
|
HjEngine *engine = nullptr;
|
||||||
|
REQUIRE(hj_engine_new(HJ_ABI_VERSION, &engine) == HJ_OK);
|
||||||
|
REQUIRE(engine != nullptr);
|
||||||
|
CHECK(hj_engine_free(engine) == HJ_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("major 不匹配时拒绝初始化且不产生句柄") {
|
||||||
|
HjEngine *engine = reinterpret_cast<HjEngine *>(0x1);
|
||||||
|
CHECK(hj_engine_new(HJ_ABI_VERSION + 100, &engine) == HJ_ERR_UNSUPPORTED_ABI);
|
||||||
|
CHECK(engine == nullptr);
|
||||||
|
CHECK(std::string(hj_last_error()).find("ABI") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("非法参数与二次释放不崩溃") {
|
||||||
|
CHECK(hj_engine_new(HJ_ABI_VERSION, nullptr) == HJ_ERR_INVALID_ARGUMENT);
|
||||||
|
CHECK(hj_last_error()[0] != '\0');
|
||||||
|
|
||||||
|
HjEngine *engine = nullptr;
|
||||||
|
REQUIRE(hj_engine_new(HJ_ABI_VERSION, &engine) == HJ_OK);
|
||||||
|
REQUIRE(hj_engine_free(engine) == HJ_OK);
|
||||||
|
CHECK(hj_engine_free(engine) == HJ_ERR_HANDLE_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("缓冲防御行为") {
|
||||||
|
CHECK(hj_buffer_free(nullptr) == HJ_ERR_INVALID_ARGUMENT);
|
||||||
|
CHECK(hj_buffer_data(nullptr) == nullptr);
|
||||||
|
CHECK(hj_buffer_len(nullptr) == 0);
|
||||||
|
}
|
||||||
Vendored
+7134
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+1421
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "hunjing-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"generate:api": "openapi-typescript ../openapi/openapi.yaml -o src/types/api.d.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^15.1.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"openapi-typescript": "^7.5.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "魂镜 Hunjing",
|
||||||
|
description: "长期 AI 角色、Agent 与创作社区平台",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
|
||||||
|
<h1>魂镜 Hunjing</h1>
|
||||||
|
<p>工程骨架已建立;界面随 WP-03/WP-10 工作包实现。</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+440
@@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* This file was auto-generated by openapi-typescript.
|
||||||
|
* Do not make direct changes to the file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface paths {
|
||||||
|
"/healthz": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** 服务健康探针 */
|
||||||
|
get: operations["getHealth"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type webhooks = Record<string, never>;
|
||||||
|
export interface components {
|
||||||
|
schemas: {
|
||||||
|
Error: {
|
||||||
|
/**
|
||||||
|
* @description 稳定机器错误码
|
||||||
|
* @example validation_failed
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
/** @description 可本地化消息 */
|
||||||
|
message: string;
|
||||||
|
/** Format: uuid */
|
||||||
|
trace_id?: string;
|
||||||
|
request_id?: string;
|
||||||
|
/** @description 不稳定细节,必须脱敏 */
|
||||||
|
details?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ErrorEnvelope: {
|
||||||
|
error: components["schemas"]["Error"];
|
||||||
|
};
|
||||||
|
CursorPage: {
|
||||||
|
data: unknown[];
|
||||||
|
/** @description opaque cursor;客户端不得解析 */
|
||||||
|
next_cursor: string | null;
|
||||||
|
has_more: boolean;
|
||||||
|
};
|
||||||
|
Money: {
|
||||||
|
amount_minor: number;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
UserId: string;
|
||||||
|
PersonalDomainId: string;
|
||||||
|
ConversationId: string;
|
||||||
|
BranchId: string;
|
||||||
|
MessageId: string;
|
||||||
|
ContentBlockId: string;
|
||||||
|
RunId: string;
|
||||||
|
TurnId: string;
|
||||||
|
/** @description 服务端生成,UUIDv4 */
|
||||||
|
RequestId: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
BlockStatus: "pending" | "streaming" | "completed" | "failed" | "cancelled";
|
||||||
|
/** @description 类型化标注(citation/来源/敏感度/语言) */
|
||||||
|
BlockAnnotation: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
BlockBase: {
|
||||||
|
id: components["schemas"]["ContentBlockId"];
|
||||||
|
status?: components["schemas"]["BlockStatus"];
|
||||||
|
annotations?: components["schemas"]["BlockAnnotation"][];
|
||||||
|
/** @description 只允许 Schema 登记字段,不存任意秘密 */
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
TextBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "text";
|
||||||
|
text: string;
|
||||||
|
language?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "TextBlock";
|
||||||
|
};
|
||||||
|
MarkdownBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "markdown";
|
||||||
|
text: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
flavor?: "hunjing-gfm-v1";
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "MarkdownBlock";
|
||||||
|
};
|
||||||
|
CodeBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "code";
|
||||||
|
source: string;
|
||||||
|
language?: string;
|
||||||
|
filename?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "CodeBlock";
|
||||||
|
};
|
||||||
|
QuoteBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "quote";
|
||||||
|
content: components["schemas"]["ContentBlock"][];
|
||||||
|
source_ref?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "QuoteBlock";
|
||||||
|
};
|
||||||
|
TableBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "table";
|
||||||
|
columns: string[];
|
||||||
|
rows: unknown[][];
|
||||||
|
caption?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "TableBlock";
|
||||||
|
};
|
||||||
|
CalloutBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "callout";
|
||||||
|
/** @enum {string} */
|
||||||
|
kind: "info" | "warning" | "danger" | "success";
|
||||||
|
title?: string;
|
||||||
|
content: components["schemas"]["ContentBlock"][];
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "CalloutBlock";
|
||||||
|
};
|
||||||
|
MathBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "math";
|
||||||
|
/** @enum {string} */
|
||||||
|
notation: "latex" | "asciimath";
|
||||||
|
/** @enum {string} */
|
||||||
|
display: "inline" | "block";
|
||||||
|
source: string;
|
||||||
|
/** @description 可访问性与解析失败降级描述 */
|
||||||
|
fallback_text?: string;
|
||||||
|
label?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "MathBlock";
|
||||||
|
};
|
||||||
|
MermaidBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "mermaid";
|
||||||
|
source: string;
|
||||||
|
title?: string;
|
||||||
|
caption?: string;
|
||||||
|
/** @default mermaid */
|
||||||
|
fallback_code_language: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "MermaidBlock";
|
||||||
|
};
|
||||||
|
ImageBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "image";
|
||||||
|
file_id?: string;
|
||||||
|
/** @description 受控 URL;禁止 file://、javascript: 等协议 */
|
||||||
|
url_ref?: string;
|
||||||
|
alt: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
caption?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "ImageBlock";
|
||||||
|
};
|
||||||
|
VideoBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "video";
|
||||||
|
file_id: string;
|
||||||
|
poster_file_id?: string;
|
||||||
|
caption?: string;
|
||||||
|
transcript_file_id?: string;
|
||||||
|
duration_ms?: number;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "VideoBlock";
|
||||||
|
};
|
||||||
|
AudioBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "audio";
|
||||||
|
file_id: string;
|
||||||
|
transcript?: string;
|
||||||
|
duration_ms?: number;
|
||||||
|
waveform?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "AudioBlock";
|
||||||
|
};
|
||||||
|
FileBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "file";
|
||||||
|
file_id: string;
|
||||||
|
filename: string;
|
||||||
|
media_type: string;
|
||||||
|
size_bytes: number;
|
||||||
|
description?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "FileBlock";
|
||||||
|
};
|
||||||
|
GalleryBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "gallery";
|
||||||
|
items: (components["schemas"]["ImageBlock"] | components["schemas"]["VideoBlock"])[];
|
||||||
|
layout?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "GalleryBlock";
|
||||||
|
};
|
||||||
|
AvatarBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "avatar";
|
||||||
|
asset_id: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
format: "live2d" | "vrm" | "gltf";
|
||||||
|
preview_file_id?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "AvatarBlock";
|
||||||
|
};
|
||||||
|
ToolCallBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "tool_call";
|
||||||
|
tool_name: string;
|
||||||
|
call_id: string;
|
||||||
|
/** @description 参数脱敏视图 */
|
||||||
|
arguments_redacted?: Record<string, never>;
|
||||||
|
approval_id?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "ToolCallBlock";
|
||||||
|
};
|
||||||
|
ToolResultBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "tool_result";
|
||||||
|
call_id: string;
|
||||||
|
summary?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
file_ids?: string[];
|
||||||
|
is_error?: boolean;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "ToolResultBlock";
|
||||||
|
};
|
||||||
|
AgentStatusBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "agent_status";
|
||||||
|
agent_id?: string;
|
||||||
|
child_run_id?: string;
|
||||||
|
stage: string;
|
||||||
|
progress?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
waiting_reason?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "AgentStatusBlock";
|
||||||
|
};
|
||||||
|
ApprovalRequestBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "approval_request";
|
||||||
|
approval_id: string;
|
||||||
|
action: string;
|
||||||
|
impact?: string;
|
||||||
|
arguments_summary?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
/** Format: date-time */
|
||||||
|
expires_at?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "ApprovalRequestBlock";
|
||||||
|
};
|
||||||
|
ArtifactBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "artifact";
|
||||||
|
artifact_id: string;
|
||||||
|
artifact_type: string;
|
||||||
|
version?: number;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "ArtifactBlock";
|
||||||
|
};
|
||||||
|
CitationBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "citation";
|
||||||
|
source_id: string;
|
||||||
|
title: string;
|
||||||
|
quoted_text: string;
|
||||||
|
/** @description 定位(文本区间、图像区域、PDF 页区、音视频时间码)与原文快照 */
|
||||||
|
locator?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "CitationBlock";
|
||||||
|
};
|
||||||
|
ErrorBlock: components["schemas"]["BlockBase"] & {
|
||||||
|
/** @enum {string} */
|
||||||
|
type?: "error";
|
||||||
|
message: string;
|
||||||
|
retryable?: boolean;
|
||||||
|
fallback?: string;
|
||||||
|
} & {
|
||||||
|
/**
|
||||||
|
* @description discriminator enum property added by openapi-typescript
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type: "ErrorBlock";
|
||||||
|
};
|
||||||
|
ContentBlock: components["schemas"]["TextBlock"] | components["schemas"]["MarkdownBlock"] | components["schemas"]["CodeBlock"] | components["schemas"]["QuoteBlock"] | components["schemas"]["TableBlock"] | components["schemas"]["CalloutBlock"] | components["schemas"]["MathBlock"] | components["schemas"]["MermaidBlock"] | components["schemas"]["ImageBlock"] | components["schemas"]["VideoBlock"] | components["schemas"]["AudioBlock"] | components["schemas"]["FileBlock"] | components["schemas"]["GalleryBlock"] | components["schemas"]["AvatarBlock"] | components["schemas"]["ToolCallBlock"] | components["schemas"]["ToolResultBlock"] | components["schemas"]["AgentStatusBlock"] | components["schemas"]["ApprovalRequestBlock"] | components["schemas"]["ArtifactBlock"] | components["schemas"]["CitationBlock"] | components["schemas"]["ErrorBlock"];
|
||||||
|
StreamEvent: {
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "content.block.started" | "content.block.delta" | "content.block.progress" | "content.block.completed" | "content.block.failed" | "content.block.cancelled" | "message.completed";
|
||||||
|
data: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
/** @description Run 内单调递增 */
|
||||||
|
sequence?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: never;
|
||||||
|
parameters: never;
|
||||||
|
requestBodies: never;
|
||||||
|
headers: never;
|
||||||
|
pathItems: never;
|
||||||
|
}
|
||||||
|
export type $defs = Record<string, never>;
|
||||||
|
export interface operations {
|
||||||
|
getHealth: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description 服务正常 */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
/** @enum {string} */
|
||||||
|
status: "ok";
|
||||||
|
env?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description 错误 */
|
||||||
|
default: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["ErrorEnvelope"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Hunjing API
|
||||||
|
version: 0.1.0
|
||||||
|
summary: Hunjing 平台公共契约(OpenAPI 3.1 单一真源)
|
||||||
|
license:
|
||||||
|
name: Apache-2.0
|
||||||
|
identifier: Apache-2.0
|
||||||
|
description: |
|
||||||
|
本文件由 docs/contracts/ 机器化而来,是公共 HTTP 契约的单一真源;
|
||||||
|
TypeScript/Rust 客户端由本文件生成,不手写重复 DTO。
|
||||||
|
当前版本覆盖阶段 0 所需基础:错误体、分页、Money、平台 ID、
|
||||||
|
统一 ContentBlock(21 类型)与流事件 envelope;资源端点随工作包增量登记。
|
||||||
|
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
|
||||||
|
servers:
|
||||||
|
- url: https://api.example.dev/v1
|
||||||
|
description: Native API(开发占位,正式域名见 docs/contracts/api-conventions.md)
|
||||||
|
paths:
|
||||||
|
/healthz:
|
||||||
|
get:
|
||||||
|
operationId: getHealth
|
||||||
|
tags: [ops]
|
||||||
|
summary: 服务健康探针
|
||||||
|
security: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 服务正常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [status]
|
||||||
|
properties:
|
||||||
|
status: { type: string, enum: [ok] }
|
||||||
|
env: { type: string }
|
||||||
|
default:
|
||||||
|
description: 错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/ErrorEnvelope" }
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
# ===== 通用:错误体(api-conventions 第 7 节) =====
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
required: [code, message]
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 稳定机器错误码
|
||||||
|
examples: [validation_failed]
|
||||||
|
message: { type: string, description: 可本地化消息 }
|
||||||
|
trace_id: { type: string, format: uuid }
|
||||||
|
request_id: { type: string }
|
||||||
|
details:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
description: 不稳定细节,必须脱敏
|
||||||
|
ErrorEnvelope:
|
||||||
|
type: object
|
||||||
|
required: [error]
|
||||||
|
properties:
|
||||||
|
error: { $ref: "#/components/schemas/Error" }
|
||||||
|
|
||||||
|
# ===== 通用:分页(opaque cursor) =====
|
||||||
|
CursorPage:
|
||||||
|
type: object
|
||||||
|
required: [data, next_cursor, has_more]
|
||||||
|
properties:
|
||||||
|
data: { type: array, items: {} }
|
||||||
|
next_cursor:
|
||||||
|
oneOf: [{ type: string }, { type: "null" }]
|
||||||
|
description: opaque cursor;客户端不得解析
|
||||||
|
has_more: { type: boolean }
|
||||||
|
|
||||||
|
# ===== 通用:金额(最小货币单位整数,禁止浮点) =====
|
||||||
|
Money:
|
||||||
|
type: object
|
||||||
|
required: [amount_minor, currency]
|
||||||
|
properties:
|
||||||
|
amount_minor: { type: integer, minimum: 0 }
|
||||||
|
currency: { type: string, pattern: "^[A-Z]{3}$" }
|
||||||
|
|
||||||
|
# ===== 平台 ID(ADR-015:<prefix><lowercase-canonical-uuid>;首批按垂直切片登记) =====
|
||||||
|
UserId: { type: string, pattern: "^user_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
PersonalDomainId: { type: string, pattern: "^domain_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
ConversationId: { type: string, pattern: "^conversation_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
BranchId: { type: string, pattern: "^branch_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
MessageId: { type: string, pattern: "^message_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
ContentBlockId: { type: string, pattern: "^block_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
RunId: { type: string, pattern: "^run_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
TurnId: { type: string, pattern: "^turn_[0-9a-f]{12}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }
|
||||||
|
RequestId: { type: string, pattern: "^request_[0-9a-f]{12}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", description: 服务端生成,UUIDv4 }
|
||||||
|
|
||||||
|
# ===== 统一 ContentBlock(content-blocks-and-streaming 第 2-3 节) =====
|
||||||
|
BlockStatus:
|
||||||
|
type: string
|
||||||
|
enum: [pending, streaming, completed, failed, cancelled]
|
||||||
|
BlockAnnotation:
|
||||||
|
type: object
|
||||||
|
description: 类型化标注(citation/来源/敏感度/语言)
|
||||||
|
additionalProperties: true
|
||||||
|
BlockBase:
|
||||||
|
type: object
|
||||||
|
required: [id]
|
||||||
|
properties:
|
||||||
|
id: { $ref: "#/components/schemas/ContentBlockId" }
|
||||||
|
status: { $ref: "#/components/schemas/BlockStatus" }
|
||||||
|
annotations:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/BlockAnnotation" }
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
description: 只允许 Schema 登记字段,不存任意秘密
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
TextBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [text]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [text] }
|
||||||
|
text: { type: string }
|
||||||
|
language: { type: string }
|
||||||
|
MarkdownBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [text]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [markdown] }
|
||||||
|
text: { type: string }
|
||||||
|
flavor: { type: string, enum: [hunjing-gfm-v1] }
|
||||||
|
CodeBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [source]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [code] }
|
||||||
|
source: { type: string }
|
||||||
|
language: { type: string }
|
||||||
|
filename: { type: string }
|
||||||
|
QuoteBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [content]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [quote] }
|
||||||
|
content:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/ContentBlock" }
|
||||||
|
source_ref: { type: string }
|
||||||
|
TableBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [columns, rows]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [table] }
|
||||||
|
columns: { type: array, items: { type: string } }
|
||||||
|
rows: { type: array, items: { type: array, items: {} } }
|
||||||
|
caption: { type: string }
|
||||||
|
CalloutBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [kind, content]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [callout] }
|
||||||
|
kind: { type: string, enum: [info, warning, danger, success] }
|
||||||
|
title: { type: string }
|
||||||
|
content:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/ContentBlock" }
|
||||||
|
MathBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [notation, display, source]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [math] }
|
||||||
|
notation: { type: string, enum: [latex, asciimath] }
|
||||||
|
display: { type: string, enum: [inline, block] }
|
||||||
|
source: { type: string }
|
||||||
|
fallback_text: { type: string, description: 可访问性与解析失败降级描述 }
|
||||||
|
label: { type: string }
|
||||||
|
MermaidBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [source]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [mermaid] }
|
||||||
|
source: { type: string }
|
||||||
|
title: { type: string }
|
||||||
|
caption: { type: string }
|
||||||
|
fallback_code_language: { type: string, default: mermaid }
|
||||||
|
ImageBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [alt]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [image] }
|
||||||
|
file_id: { type: string }
|
||||||
|
url_ref: { type: string, description: "受控 URL;禁止 file://、javascript: 等协议" }
|
||||||
|
alt: { type: string }
|
||||||
|
width: { type: integer, minimum: 1 }
|
||||||
|
height: { type: integer, minimum: 1 }
|
||||||
|
caption: { type: string }
|
||||||
|
VideoBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [file_id]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [video] }
|
||||||
|
file_id: { type: string }
|
||||||
|
poster_file_id: { type: string }
|
||||||
|
caption: { type: string }
|
||||||
|
transcript_file_id: { type: string }
|
||||||
|
duration_ms: { type: integer, minimum: 0 }
|
||||||
|
AudioBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [file_id]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [audio] }
|
||||||
|
file_id: { type: string }
|
||||||
|
transcript: { type: string }
|
||||||
|
duration_ms: { type: integer, minimum: 0 }
|
||||||
|
waveform: { type: string }
|
||||||
|
FileBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [file_id, filename, media_type, size_bytes]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [file] }
|
||||||
|
file_id: { type: string }
|
||||||
|
filename: { type: string }
|
||||||
|
media_type: { type: string }
|
||||||
|
size_bytes: { type: integer, minimum: 0 }
|
||||||
|
description: { type: string }
|
||||||
|
GalleryBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [items]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [gallery] }
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
oneOf:
|
||||||
|
- $ref: "#/components/schemas/ImageBlock"
|
||||||
|
- $ref: "#/components/schemas/VideoBlock"
|
||||||
|
layout: { type: string }
|
||||||
|
AvatarBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [asset_id, format]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [avatar] }
|
||||||
|
asset_id: { type: string }
|
||||||
|
format: { type: string, enum: [live2d, vrm, gltf] }
|
||||||
|
preview_file_id: { type: string }
|
||||||
|
ToolCallBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [tool_name, call_id]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [tool_call] }
|
||||||
|
tool_name: { type: string }
|
||||||
|
call_id: { type: string }
|
||||||
|
arguments_redacted: { type: object, description: 参数脱敏视图 }
|
||||||
|
approval_id: { type: string }
|
||||||
|
ToolResultBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [call_id]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [tool_result] }
|
||||||
|
call_id: { type: string }
|
||||||
|
summary: { type: object, additionalProperties: true }
|
||||||
|
file_ids: { type: array, items: { type: string } }
|
||||||
|
is_error: { type: boolean }
|
||||||
|
AgentStatusBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [stage]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [agent_status] }
|
||||||
|
agent_id: { type: string }
|
||||||
|
child_run_id: { type: string }
|
||||||
|
stage: { type: string }
|
||||||
|
progress: { type: object, additionalProperties: true }
|
||||||
|
waiting_reason: { type: string }
|
||||||
|
ApprovalRequestBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [approval_id, action]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [approval_request] }
|
||||||
|
approval_id: { type: string }
|
||||||
|
action: { type: string }
|
||||||
|
impact: { type: string }
|
||||||
|
arguments_summary: { type: object, additionalProperties: true }
|
||||||
|
expires_at: { type: string, format: date-time }
|
||||||
|
ArtifactBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [artifact_id, artifact_type]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [artifact] }
|
||||||
|
artifact_id: { type: string }
|
||||||
|
artifact_type: { type: string }
|
||||||
|
version: { type: integer }
|
||||||
|
CitationBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [source_id, title, quoted_text]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [citation] }
|
||||||
|
source_id: { type: string }
|
||||||
|
title: { type: string }
|
||||||
|
quoted_text: { type: string }
|
||||||
|
locator:
|
||||||
|
type: object
|
||||||
|
description: 定位(文本区间、图像区域、PDF 页区、音视频时间码)与原文快照
|
||||||
|
additionalProperties: true
|
||||||
|
ErrorBlock:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/BlockBase"
|
||||||
|
- type: object
|
||||||
|
required: [message]
|
||||||
|
properties:
|
||||||
|
type: { type: string, enum: [error] }
|
||||||
|
message: { type: string }
|
||||||
|
retryable: { type: boolean }
|
||||||
|
fallback: { type: string }
|
||||||
|
|
||||||
|
ContentBlock:
|
||||||
|
oneOf:
|
||||||
|
- $ref: "#/components/schemas/TextBlock"
|
||||||
|
- $ref: "#/components/schemas/MarkdownBlock"
|
||||||
|
- $ref: "#/components/schemas/CodeBlock"
|
||||||
|
- $ref: "#/components/schemas/QuoteBlock"
|
||||||
|
- $ref: "#/components/schemas/TableBlock"
|
||||||
|
- $ref: "#/components/schemas/CalloutBlock"
|
||||||
|
- $ref: "#/components/schemas/MathBlock"
|
||||||
|
- $ref: "#/components/schemas/MermaidBlock"
|
||||||
|
- $ref: "#/components/schemas/ImageBlock"
|
||||||
|
- $ref: "#/components/schemas/VideoBlock"
|
||||||
|
- $ref: "#/components/schemas/AudioBlock"
|
||||||
|
- $ref: "#/components/schemas/FileBlock"
|
||||||
|
- $ref: "#/components/schemas/GalleryBlock"
|
||||||
|
- $ref: "#/components/schemas/AvatarBlock"
|
||||||
|
- $ref: "#/components/schemas/ToolCallBlock"
|
||||||
|
- $ref: "#/components/schemas/ToolResultBlock"
|
||||||
|
- $ref: "#/components/schemas/AgentStatusBlock"
|
||||||
|
- $ref: "#/components/schemas/ApprovalRequestBlock"
|
||||||
|
- $ref: "#/components/schemas/ArtifactBlock"
|
||||||
|
- $ref: "#/components/schemas/CitationBlock"
|
||||||
|
- $ref: "#/components/schemas/ErrorBlock"
|
||||||
|
discriminator:
|
||||||
|
propertyName: type
|
||||||
|
|
||||||
|
# ===== 流事件 envelope(content-blocks-and-streaming 第 5 节) =====
|
||||||
|
StreamEvent:
|
||||||
|
type: object
|
||||||
|
required: [type, data]
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
[
|
||||||
|
content.block.started,
|
||||||
|
content.block.delta,
|
||||||
|
content.block.progress,
|
||||||
|
content.block.completed,
|
||||||
|
content.block.failed,
|
||||||
|
content.block.cancelled,
|
||||||
|
message.completed,
|
||||||
|
]
|
||||||
|
data: { type: object, additionalProperties: true }
|
||||||
|
sequence:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
description: Run 内单调递增
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Redocly 校验配置(契约工具链见 ADR-025)
|
||||||
|
extends:
|
||||||
|
- recommended
|
||||||
|
rules:
|
||||||
|
# 契约先行:schemas 随工作包先于端点登记,属正常演进而非死代码。
|
||||||
|
no-unused-components: off
|
||||||
|
# /healthz 等运维探针不返回 4xx 业务错误,default 错误响应已覆盖。
|
||||||
|
operation-4xx-response: off
|
||||||
Reference in new issue
Block a user