[AC-AISVC-50] 合入第一个稳定版本 #2

Merged
MerCry merged 32 commits from feature/prompt-unification-and-logging into main 2026-02-26 13:03:31 +00:00
4 changed files with 100 additions and 12 deletions
Showing only changes of commit 1000158550 - Show all commits

View File

@ -91,11 +91,35 @@ docker exec -it ai-ollama ollama pull nomic-embed-text
# 检查服务状态
docker compose ps
# 查看后端日志
docker compose logs -f ai-service
# 查看后端日志,找到自动生成的 API Key
docker compose logs -f ai-service | grep "Default API Key"
```
#### 6. 访问服务
> **重要**: 后端首次启动时会自动生成一个默认 API Key请从日志中复制该 Key用于前端配置。
#### 6. 配置前端 API Key
```bash
# 创建前端环境变量文件
cd ai-service-admin
cp .env.example .env
```
编辑 `ai-service-admin/.env`,将 `VITE_APP_API_KEY` 设置为后端日志中的 API Key
```env
VITE_APP_BASE_API=/api
VITE_APP_API_KEY=<从后端日志复制的API Key>
```
然后重新构建前端:
```bash
cd ..
docker compose up -d --build ai-service-admin
```
#### 7. 访问服务
| 服务 | 地址 | 说明 |
|------|------|------|

View File

@ -0,0 +1,8 @@
# API Base URL
VITE_APP_BASE_API=/api
# Default API Key for authentication
# IMPORTANT: You must set this to a valid API key from the backend
# The backend creates a default API key on first startup (check backend logs)
# Or you can create one via the API: POST /admin/api-keys
VITE_APP_API_KEY=your-api-key-here

View File

@ -1,6 +1,6 @@
"""
Middleware for AI Service.
[AC-AISVC-10, AC-AISVC-12] X-Tenant-Id header validation and tenant context injection.
[AC-AISVC-10, AC-AISVC-12, AC-AISVC-50] X-Tenant-Id header validation, tenant context injection, and API Key authentication.
"""
import logging
@ -17,12 +17,20 @@ from app.core.tenant import clear_tenant_context, set_tenant_context
logger = logging.getLogger(__name__)
TENANT_ID_HEADER = "X-Tenant-Id"
API_KEY_HEADER = "X-API-Key"
ACCEPT_HEADER = "Accept"
SSE_CONTENT_TYPE = "text/event-stream"
# Tenant ID format: name@ash@year (e.g., szmp@ash@2026)
TENANT_ID_PATTERN = re.compile(r'^[^@]+@ash@\d{4}$')
PATHS_SKIP_API_KEY = {
"/health",
"/ai/health",
"/docs",
"/redoc",
"/openapi.json",
}
def validate_tenant_id_format(tenant_id: str) -> bool:
"""
@ -41,6 +49,59 @@ def parse_tenant_id(tenant_id: str) -> tuple[str, str]:
return parts[0], parts[2]
class ApiKeyMiddleware(BaseHTTPMiddleware):
"""
[AC-AISVC-50] Middleware to validate API Key for all requests.
Features:
- Validates X-API-Key header against in-memory cache
- Skips validation for health/docs endpoints
- Returns 401 for missing or invalid API key
"""
async def dispatch(self, request: Request, call_next: Callable) -> Response:
if self._should_skip_api_key(request.url.path):
return await call_next(request)
api_key = request.headers.get(API_KEY_HEADER)
if not api_key or not api_key.strip():
logger.warning(f"[AC-AISVC-50] Missing X-API-Key header for {request.url.path}")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=ErrorResponse(
code=ErrorCode.UNAUTHORIZED.value,
message="Missing required header: X-API-Key",
).model_dump(exclude_none=True),
)
api_key = api_key.strip()
from app.services.api_key import get_api_key_service
service = get_api_key_service()
if not service.validate_key(api_key):
logger.warning(f"[AC-AISVC-50] Invalid API key for {request.url.path}")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=ErrorResponse(
code=ErrorCode.UNAUTHORIZED.value,
message="Invalid API key",
).model_dump(exclude_none=True),
)
return await call_next(request)
def _should_skip_api_key(self, path: str) -> bool:
"""Check if the path should skip API key validation."""
if path in PATHS_SKIP_API_KEY:
return True
for skip_path in PATHS_SKIP_API_KEY:
if path.startswith(skip_path):
return True
return False
class TenantContextMiddleware(BaseHTTPMiddleware):
"""
[AC-AISVC-10, AC-AISVC-12] Middleware to extract and validate X-Tenant-Id header.
@ -51,7 +112,7 @@ class TenantContextMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: Callable) -> Response:
clear_tenant_context()
if request.url.path == "/ai/health":
if request.url.path in ("/health", "/ai/health"):
return await call_next(request)
tenant_id = request.headers.get(TENANT_ID_HEADER)
@ -68,7 +129,6 @@ class TenantContextMiddleware(BaseHTTPMiddleware):
tenant_id = tenant_id.strip()
# Validate tenant ID format
if not validate_tenant_id_format(tenant_id):
logger.warning(f"[AC-AISVC-10] Invalid tenant ID format: {tenant_id}")
return JSONResponse(
@ -79,13 +139,11 @@ class TenantContextMiddleware(BaseHTTPMiddleware):
).model_dump(exclude_none=True),
)
# Auto-create tenant if not exists (for admin endpoints)
if request.url.path.startswith("/admin/") or request.url.path.startswith("/ai/"):
try:
await self._ensure_tenant_exists(request, tenant_id)
except Exception as e:
logger.error(f"[AC-AISVC-10] Failed to ensure tenant exists: {e}")
# Continue processing even if tenant creation fails
set_tenant_context(tenant_id)
request.state.tenant_id = tenant_id
@ -112,7 +170,6 @@ class TenantContextMiddleware(BaseHTTPMiddleware):
name, year = parse_tenant_id(tenant_id)
async with async_session_maker() as session:
# Check if tenant exists
stmt = select(Tenant).where(Tenant.tenant_id == tenant_id)
result = await session.execute(stmt)
existing_tenant = result.scalar_one_or_none()
@ -121,7 +178,6 @@ class TenantContextMiddleware(BaseHTTPMiddleware):
logger.debug(f"[AC-AISVC-10] Tenant already exists: {tenant_id}")
return
# Create new tenant
new_tenant = Tenant(
tenant_id=tenant_id,
name=name,

View File

@ -27,7 +27,7 @@ services:
networks:
- ai-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
test: ["CMD", "curl", "-f", "http://localhost:8080/ai/health"]
interval: 30s
timeout: 10s
retries: 3