diff --git a/README.md b/README.md index 0d86229..a4c0297 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,55 @@ resource:{user_id}:{resource_id} 除 `POST /users` 外,所有业务 API 都需要携带 `user_id` 和 `user_key`。认证失败返回 `401`。 -### 1. 创建用户 +### 1. 健康检查 + +```http +GET /health +``` + +该接口不需要 `user_id` 或 `user_key`,用于确认 Gateway API 是否可响应,以及上游 EverOS 是否可访问。 + +请求示例: + +```bash +curl http://127.0.0.1:8010/health +``` + +EverOS 正常时响应示例: + +```json +{ + "status": "ok", + "api": { + "status": "ok" + }, + "everos": { + "status": "ok", + "base_url": "http://127.0.0.1:8000", + "data": { + "status": "ok" + } + } +} +``` + +EverOS 不可访问时仍返回 HTTP 200,但 `status` 会变成 `degraded`,便于区分“Gateway API 活着”和“上游 EverOS 故障”: + +```json +{ + "status": "degraded", + "api": { + "status": "ok" + }, + "everos": { + "status": "unavailable", + "base_url": "http://127.0.0.1:8000", + "error": "Connection refused" + } +} +``` + +### 2. 创建用户 ```http POST /users @@ -144,7 +192,7 @@ curl -X POST http://127.0.0.1:8010/users \ `user_key` 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 `user_id` 已存在,接口会返回已有 `user_key`。 -### 2. 上传资源 +### 3. 上传资源 ```http POST /resources @@ -212,7 +260,7 @@ curl -X POST http://127.0.0.1:8010/resources \ 对外返回的 `uri` 永远是 `resource://{user_id}/{resource_id}`,不会泄露内部 `file://` 路径。 -### 3. 查询资源列表 +### 4. 查询资源列表 ```http GET /resources?user_id={user_id}&user_key={user_key} @@ -264,7 +312,7 @@ curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx" } ``` -### 4. 查询资源详情 +### 5. 查询资源详情 ```http GET /resources/{resource_id}?user_id={user_id}&user_key={user_key} @@ -322,7 +370,7 @@ curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx" 这种设计避免通过资源 ID 探测其他用户的数据。`uri` 同样只返回公开 `resource://{user_id}/{resource_id}`,不会泄露内部 URI。 -### 5. 删除资源 +### 6. 删除资源 ```http DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key} @@ -352,7 +400,7 @@ curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_ } ``` -### 6. 搜索记忆 +### 7. 搜索记忆 ```http POST /memories/search @@ -429,7 +477,7 @@ curl -X POST http://127.0.0.1:8010/memories/search \ } ``` -### 7. 修改记忆 +### 8. 修改记忆 ```http PATCH /memories/{memory_id} @@ -470,7 +518,7 @@ curl -X PATCH http://127.0.0.1:8010/memories/mem_abc \ } ``` -### 8. 删除记忆 +### 9. 删除记忆 ```http DELETE /memories/{memory_id} @@ -518,6 +566,7 @@ Gateway 内部通过 `core/everos_client.py` 调用 EverOS: - `add_memory(payload)` -> `POST /api/v1/memory/add` - `flush_memory(session_id, app_id, project_id)` -> `POST /api/v1/memory/flush` - `search_memory(payload)` -> `POST /api/v1/memory/search` +- `health_check()` -> `GET /health` ## 运行测试 diff --git a/core/api.py b/core/api.py index fb2a76c..49358eb 100644 --- a/core/api.py +++ b/core/api.py @@ -66,6 +66,30 @@ def create_app( if not service.authenticate_user(user_id, user_key): raise HTTPException(status_code=401, detail="invalid user credentials") + @router.get("/health") + async def health() -> dict[str, Any]: + try: + everos_health = await client.health_check() + except Exception as exc: + return { + "status": "degraded", + "api": {"status": "ok"}, + "everos": { + "status": "unavailable", + "base_url": cfg.everos_base_url, + "error": str(exc), + }, + } + return { + "status": "ok", + "api": {"status": "ok"}, + "everos": { + "status": "ok", + "base_url": cfg.everos_base_url, + "data": everos_health, + }, + } + @router.post("/users") async def create_user(request: UserCreateRequest) -> dict[str, Any]: return service.create_user(request.user_id) diff --git a/core/everos_client.py b/core/everos_client.py index dc0b9ce..92145e9 100644 --- a/core/everos_client.py +++ b/core/everos_client.py @@ -31,6 +31,15 @@ class EverOSClient: async def search_memory(self, payload: dict[str, Any]) -> dict[str, Any]: return await self._post("/api/v1/memory/search", payload) + async def health_check(self) -> dict[str, Any]: + async with httpx.AsyncClient( + base_url=self.base_url, + timeout=self.timeout, + ) as client: + response = await client.get("/health") + response.raise_for_status() + return response.json() + async def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]: async with httpx.AsyncClient( base_url=self.base_url, diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 3d275ce..637b8ca 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -13,11 +13,16 @@ from core.repository import MemoryRepository class FakeEverOSClient: - def __init__(self, search_results: list[dict[str, Any]] | None = None) -> None: + def __init__( + self, + search_results: list[dict[str, Any]] | None = None, + health_error: Exception | None = None, + ) -> None: self.add_calls: list[dict[str, Any]] = [] self.flush_calls: list[dict[str, str]] = [] self.search_calls: list[dict[str, Any]] = [] self.search_results = search_results or [] + self.health_error = health_error async def add_memory(self, payload: dict[str, Any]) -> dict[str, Any]: self.add_calls.append(payload) @@ -38,6 +43,11 @@ class FakeEverOSClient: self.search_calls.append(payload) return {"request_id": "search", "data": {"episodes": self.search_results}} + async def health_check(self) -> dict[str, Any]: + if self.health_error is not None: + raise self.health_error + return {"status": "ok"} + @pytest.fixture def config(tmp_path: Path) -> GatewayConfig: @@ -72,6 +82,43 @@ async def create_user(client: httpx.AsyncClient, user_id: str = "u_123") -> str: return body["user_key"] +@pytest.mark.asyncio +async def test_health_reports_api_and_everos_ok( + config: GatewayConfig, +) -> None: + everos = FakeEverOSClient() + async with app_client(config, everos) as client: + response = await client.get("/health") + + assert response.status_code == 200, response.text + assert response.json() == { + "status": "ok", + "api": {"status": "ok"}, + "everos": { + "status": "ok", + "base_url": "http://everos.test", + "data": {"status": "ok"}, + }, + } + + +@pytest.mark.asyncio +async def test_health_reports_degraded_when_everos_fails( + config: GatewayConfig, +) -> None: + everos = FakeEverOSClient(health_error=RuntimeError("everos down")) + async with app_client(config, everos) as client: + response = await client.get("/health") + + assert response.status_code == 200, response.text + body = response.json() + assert body["status"] == "degraded" + assert body["api"] == {"status": "ok"} + assert body["everos"]["status"] == "unavailable" + assert body["everos"]["base_url"] == "http://everos.test" + assert body["everos"]["error"] == "everos down" + + @pytest.mark.asyncio async def test_create_user_generates_and_persists_user_key( config: GatewayConfig,