🎉 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

+18
View File
@@ -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/ 真实配置或任何日志。
+4
View File
@@ -109,6 +109,10 @@ docker-compose.override.yml
# Rust / 安全沙箱
target/
# C++ 引擎与契约生成产物(源码构建可再生)
engine/build/
openapi/bundled.json
**/*.rs.bk
sandbox-work/
+72
View File
@@ -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}'
+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()
+46
View File
@@ -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"]
+27
View File
@@ -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"]
+33
View File
@@ -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_")
+1283
View File
File diff suppressed because it is too large. Load diff
+20
View File
@@ -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
+13
View File
@@ -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。
+21
View File
@@ -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:
+20
View File
@@ -397,3 +397,23 @@ UUIDv7 为高写入关系表提供索引局部性和大致创建顺序;请求
- 开发环境用 `docker compose`(或嵌入式 PostgreSQL)启动数据库;测试统一在 PostgreSQL 上运行,不用 SQLite 内存库替代,避免方言差异掩盖缺陷。
- 自托管两档(单机/标准服务器)均含 PostgreSQL 容器;单机档降低的是资源规格,不是能力面。
- 迁移与性能优化只需针对 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 验收证据。
+5
View File
@@ -0,0 +1,5 @@
BasedOnStyle: Google
IndentWidth: 2
ColumnLimit: 100
AccessModifierOffset: -1
AllowShortFunctionsOnASingleLine: Inline
+30
View File
@@ -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)
+72
View File
@@ -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
+117
View File
@@ -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"
+39
View File
@@ -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);
}
File diff suppressed because it is too large. Load diff
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;
+1421
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -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"
}
}
+14
View File
@@ -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>
);
}
+8
View File
@@ -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>
);
}
+440
View File
@@ -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"];
};
};
};
};
}
+21
View File
@@ -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"]
}
+401
View File
@@ -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 内单调递增
+8
View File
@@ -0,0 +1,8 @@
# Redocly 校验配置(契约工具链见 ADR-025)
extends:
- recommended
rules:
# 契约先行:schemas 随工作包先于端点登记,属正常演进而非死代码。
no-unused-components: off
# /healthz 等运维探针不返回 4xx 业务错误,default 错误响应已覆盖。
operation-4xx-response: off