docs: add runtime route documentation and AGENTS.md rule
This commit is contained in:
@@ -25,3 +25,11 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d
|
|||||||
- New development worktrees must be created from `dev` (never from `main`).
|
- New development worktrees must be created from `dev` (never from `main`).
|
||||||
- Do not develop or commit directly on `main` outside explicit release/merge workflows.
|
- Do not develop or commit directly on `main` outside explicit release/merge workflows.
|
||||||
- Do not rewrite `main` history unless explicitly requested (including reset and force push).
|
- Do not rewrite `main` history unless explicitly requested (including reset and force push).
|
||||||
|
|
||||||
|
## API Route Documentation
|
||||||
|
|
||||||
|
When modifying HTTP routes (adding, updating, or removing endpoints):
|
||||||
|
|
||||||
|
- Sync changes to `docs/runtime/runtime-route.md`
|
||||||
|
- Include: HTTP method, path, request/response schema, status codes, error format
|
||||||
|
- Keep documentation in sync with actual implementation
|
||||||
|
|||||||
@@ -11,15 +11,14 @@ import uvicorn
|
|||||||
from app import app
|
from app import app
|
||||||
from v1.auth.dependencies import get_auth_service
|
from v1.auth.dependencies import get_auth_service
|
||||||
from v1.auth.schemas import (
|
from v1.auth.schemas import (
|
||||||
AuthResendCodeResponse,
|
|
||||||
AuthSignupStartResponse,
|
|
||||||
AuthTokenResponse,
|
|
||||||
AuthUser,
|
AuthUser,
|
||||||
LoginRequest,
|
SessionCreateRequest,
|
||||||
RefreshRequest,
|
SessionRefreshRequest,
|
||||||
SignupResendRequest,
|
SessionResponse,
|
||||||
SignupStartRequest,
|
VerificationCreateRequest,
|
||||||
SignupVerifyRequest,
|
VerificationCreateResponse,
|
||||||
|
VerificationResendRequest,
|
||||||
|
VerificationVerifyRequest,
|
||||||
)
|
)
|
||||||
from v1.auth.service import AuthService
|
from v1.auth.service import AuthService
|
||||||
|
|
||||||
@@ -28,13 +27,15 @@ class FakeE2EAuthService(AuthService):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._user = AuthUser(id="user-1", email="user@example.com")
|
self._user = AuthUser(id="user-1", email="user@example.com")
|
||||||
|
|
||||||
async def signup_start(
|
async def create_verification(
|
||||||
self, request: SignupStartRequest
|
self, request: VerificationCreateRequest
|
||||||
) -> AuthSignupStartResponse:
|
) -> VerificationCreateResponse:
|
||||||
return AuthSignupStartResponse(email=request.email)
|
return VerificationCreateResponse(email=request.email)
|
||||||
|
|
||||||
async def signup_verify(self, request: SignupVerifyRequest) -> AuthTokenResponse:
|
async def verify_verification(
|
||||||
return AuthTokenResponse(
|
self, request: VerificationVerifyRequest
|
||||||
|
) -> SessionResponse:
|
||||||
|
return SessionResponse(
|
||||||
access_token="access-1",
|
access_token="access-1",
|
||||||
refresh_token="refresh-1",
|
refresh_token="refresh-1",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -42,13 +43,11 @@ class FakeE2EAuthService(AuthService):
|
|||||||
user=self._user,
|
user=self._user,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def signup_resend(
|
async def resend_verification(self, request: VerificationResendRequest) -> None:
|
||||||
self, request: SignupResendRequest
|
return None
|
||||||
) -> AuthResendCodeResponse:
|
|
||||||
return AuthResendCodeResponse()
|
|
||||||
|
|
||||||
async def login(self, request: LoginRequest) -> AuthTokenResponse:
|
async def create_session(self, request: SessionCreateRequest) -> SessionResponse:
|
||||||
return AuthTokenResponse(
|
return SessionResponse(
|
||||||
access_token="access-2",
|
access_token="access-2",
|
||||||
refresh_token="refresh-2",
|
refresh_token="refresh-2",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -56,8 +55,8 @@ class FakeE2EAuthService(AuthService):
|
|||||||
user=self._user,
|
user=self._user,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def refresh(self, request: RefreshRequest) -> AuthTokenResponse:
|
async def refresh_session(self, request: SessionRefreshRequest) -> SessionResponse:
|
||||||
return AuthTokenResponse(
|
return SessionResponse(
|
||||||
access_token="access-3",
|
access_token="access-3",
|
||||||
refresh_token="refresh-3",
|
refresh_token="refresh-3",
|
||||||
expires_in=3600,
|
expires_in=3600,
|
||||||
@@ -65,7 +64,7 @@ class FakeE2EAuthService(AuthService):
|
|||||||
user=self._user,
|
user=self._user,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def logout(self, refresh_token: str | None) -> None:
|
async def delete_session(self, refresh_token: str | None) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -106,8 +105,8 @@ def test_auth_flow_e2e() -> None:
|
|||||||
base_url=f"http://{host}:{port}"
|
base_url=f"http://{host}:{port}"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
signup = request_context.post(
|
verification = request_context.post(
|
||||||
"/api/v1/auth/signup/start",
|
"/api/v1/auth/verifications",
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
"username": "demo",
|
"username": "demo",
|
||||||
@@ -117,10 +116,10 @@ def test_auth_flow_e2e() -> None:
|
|||||||
),
|
),
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
assert signup.status == 202
|
assert verification.status == 202
|
||||||
|
|
||||||
verify = request_context.post(
|
verify = request_context.post(
|
||||||
"/api/v1/auth/signup/verify",
|
"/api/v1/auth/verifications/verify",
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
"email": "user@example.com",
|
"email": "user@example.com",
|
||||||
@@ -133,7 +132,7 @@ def test_auth_flow_e2e() -> None:
|
|||||||
assert verify.json()["access_token"] == "access-1"
|
assert verify.json()["access_token"] == "access-1"
|
||||||
|
|
||||||
login = request_context.post(
|
login = request_context.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/sessions",
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{"email": "user@example.com", "password": "secret123"}
|
{"email": "user@example.com", "password": "secret123"}
|
||||||
),
|
),
|
||||||
@@ -143,15 +142,15 @@ def test_auth_flow_e2e() -> None:
|
|||||||
assert login.json()["access_token"] == "access-2"
|
assert login.json()["access_token"] == "access-2"
|
||||||
|
|
||||||
refresh = request_context.post(
|
refresh = request_context.post(
|
||||||
"/api/v1/auth/refresh",
|
"/api/v1/auth/sessions/refresh",
|
||||||
data=json.dumps({"refresh_token": "refresh-2"}),
|
data=json.dumps({"refresh_token": "refresh-2"}),
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
assert refresh.status == 200
|
assert refresh.status == 200
|
||||||
assert refresh.json()["access_token"] == "access-3"
|
assert refresh.json()["access_token"] == "access-3"
|
||||||
|
|
||||||
logout = request_context.post(
|
logout = request_context.delete(
|
||||||
"/api/v1/auth/logout",
|
"/api/v1/auth/sessions",
|
||||||
data=json.dumps({"refresh_token": "refresh-3"}),
|
data=json.dumps({"refresh_token": "refresh-3"}),
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,360 @@
|
|||||||
|
# Runtime API Routes
|
||||||
|
|
||||||
|
本文档记录所有 HTTP API 端点。修改路由时必须同步更新此文档。
|
||||||
|
|
||||||
|
## 格式说明
|
||||||
|
|
||||||
|
- Request/Response 使用 JSON 格式
|
||||||
|
- 错误响应使用 RFC 7807 `application/problem+json`
|
||||||
|
- 所有端点前缀: `/api/v1`
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
### POST /auth/verifications
|
||||||
|
|
||||||
|
创建验证码(注册发起)。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string (3-30 chars)",
|
||||||
|
"email": "string (email)",
|
||||||
|
"password": "string (min 6 chars)",
|
||||||
|
"redirect_to": "string? (optional)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 202 Accepted
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 422: 请求参数无效
|
||||||
|
- 429: 请求过于频繁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /auth/verifications/resend
|
||||||
|
|
||||||
|
重发验证码。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "string (email)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 204 No Content
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 422: 请求参数无效
|
||||||
|
- 429: 请求过于频繁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /auth/verifications/verify
|
||||||
|
|
||||||
|
验证码校验。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "string (email)",
|
||||||
|
"token": "string (6 digits)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "string",
|
||||||
|
"refresh_token": "string",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": {
|
||||||
|
"id": "string",
|
||||||
|
"email": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 验证码无效或已过期
|
||||||
|
- 422: 请求参数无效
|
||||||
|
- 429: 请求过于频繁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /auth/sessions
|
||||||
|
|
||||||
|
登录(创建会话)。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "string (email)",
|
||||||
|
"password": "string (min 6 chars)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "string",
|
||||||
|
"refresh_token": "string",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": {
|
||||||
|
"id": "string",
|
||||||
|
"email": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 邮箱或密码错误
|
||||||
|
- 422: 请求参数无效
|
||||||
|
- 429: 请求过于频繁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /auth/sessions/refresh
|
||||||
|
|
||||||
|
刷新 Token。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"refresh_token": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "string",
|
||||||
|
"refresh_token": "string",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": {
|
||||||
|
"id": "string",
|
||||||
|
"email": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 无效的 refresh token
|
||||||
|
- 422: 请求参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DELETE /auth/sessions
|
||||||
|
|
||||||
|
登出(删除会话)。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"refresh_token": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 204 No Content
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 422: 请求参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /auth/users
|
||||||
|
|
||||||
|
按邮箱查询用户(需要认证)。
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `email`: string (required)
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"email": "string",
|
||||||
|
"created_at": "string (ISO 8601)",
|
||||||
|
"email_confirmed_at": "string? (ISO 8601)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 403: 无权限访问
|
||||||
|
- 404: 用户不存在
|
||||||
|
- 422: 请求参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Users
|
||||||
|
|
||||||
|
### GET /users/me
|
||||||
|
|
||||||
|
获取当前用户信息(需要认证)。
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"username": "string",
|
||||||
|
"avatar_url": "string?",
|
||||||
|
"bio": "string?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 未认证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PATCH /users/me
|
||||||
|
|
||||||
|
更新当前用户信息(需要认证)。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "string? (3-30 chars)",
|
||||||
|
"avatar_url": "string? (URL)",
|
||||||
|
"bio": "string? (max 200 chars)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"username": "string",
|
||||||
|
"avatar_url": "string?",
|
||||||
|
"bio": "string?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 未认证
|
||||||
|
- 422: 请求参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /users/{username}
|
||||||
|
|
||||||
|
按用户名查询用户(需要认证)。
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `username`: string (3-30 chars, alphanumeric and underscore)
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"username": "string",
|
||||||
|
"avatar_url": "string?",
|
||||||
|
"bio": "string?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 未认证
|
||||||
|
- 404: 用户不存在
|
||||||
|
- 422: 请求参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent Chat
|
||||||
|
|
||||||
|
### POST /agent-chats
|
||||||
|
|
||||||
|
运行 Agent 对话(需要认证)。
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "string (1-8000 chars)",
|
||||||
|
"session_id": "string? (UUID)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "string (UUID)",
|
||||||
|
"output": "string",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"run_id": "string?",
|
||||||
|
"message_id": "string?",
|
||||||
|
"delta": "string?",
|
||||||
|
"tool_name": "string?",
|
||||||
|
"result": "string?",
|
||||||
|
"output": "string?",
|
||||||
|
"error": "string?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- 401: 未认证
|
||||||
|
- 422: 请求参数无效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infra
|
||||||
|
|
||||||
|
### GET /infra/health
|
||||||
|
|
||||||
|
检查基础设施健康状态。
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy" | "unhealthy",
|
||||||
|
"services": {
|
||||||
|
"redis": {
|
||||||
|
"status": "healthy" | "unhealthy",
|
||||||
|
"latency_ms": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
|
||||||
|
检查服务健康状态。
|
||||||
|
|
||||||
|
**Response:** 200 OK
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Response Format (RFC 7807)
|
||||||
|
|
||||||
|
所有错误响应使用 `application/problem+json` 格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "about:blank",
|
||||||
|
"title": "Unauthorized",
|
||||||
|
"status": 401,
|
||||||
|
"detail": "验证码无效或已过期",
|
||||||
|
"instance": "/api/v1/auth/verifications/verify"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
前端应优先读取 `detail` 字段显示给用户。
|
||||||
Reference in New Issue
Block a user