From 891cfc27047a6befcdeb05699180499985537448 Mon Sep 17 00:00:00 2001 From: Ke Sun Date: Mon, 30 Mar 2026 16:50:56 +0800 Subject: [PATCH] feat(end-user-api): add authenticated API endpoint for end user creation - Should be merged after v0.2.9 - Create new end_user_api_controller.py with POST /end_user/create endpoint - Implement API Key authentication requirement with memory scope - Add support for optional memory_config_id parameter with workspace default fallback - Update memory_api_schema.py to remove workspace_id from request (now derived from API key auth) - Add memory_config_id field to CreateEndUserResponse schema - Register end_user_api_controller router in service module - Migrate end user creation from unauthenticated to authenticated API flow --- api/app/controllers/service/__init__.py | 3 +- .../service/end_user_api_controller.py | 92 +++++++++++++++++++ api/app/schemas/memory_api_schema.py | 14 +-- 3 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 api/app/controllers/service/end_user_api_controller.py diff --git a/api/app/controllers/service/__init__.py b/api/app/controllers/service/__init__.py index 8c679c1f..96da0949 100644 --- a/api/app/controllers/service/__init__.py +++ b/api/app/controllers/service/__init__.py @@ -4,7 +4,7 @@ 认证方式: API Key """ from fastapi import APIRouter -from . import app_api_controller, rag_api_knowledge_controller, rag_api_document_controller, rag_api_file_controller, rag_api_chunk_controller, memory_api_controller +from . import app_api_controller, rag_api_knowledge_controller, rag_api_document_controller, rag_api_file_controller, rag_api_chunk_controller, memory_api_controller, end_user_api_controller # 创建 V1 API 路由器 service_router = APIRouter() @@ -16,5 +16,6 @@ service_router.include_router(rag_api_document_controller.router) service_router.include_router(rag_api_file_controller.router) service_router.include_router(rag_api_chunk_controller.router) service_router.include_router(memory_api_controller.router) +service_router.include_router(end_user_api_controller.router) __all__ = ["service_router"] diff --git a/api/app/controllers/service/end_user_api_controller.py b/api/app/controllers/service/end_user_api_controller.py new file mode 100644 index 00000000..9d410bd2 --- /dev/null +++ b/api/app/controllers/service/end_user_api_controller.py @@ -0,0 +1,92 @@ +"""End User 服务接口 - 基于 API Key 认证""" + +import uuid + +from fastapi import APIRouter, Body, Depends, Request +from sqlalchemy.orm import Session + +from app.core.api_key_auth import require_api_key +from app.core.error_codes import BizCode +from app.core.exceptions import BusinessException +from app.core.logging_config import get_business_logger +from app.core.response_utils import success +from app.db import get_db +from app.repositories.end_user_repository import EndUserRepository +from app.schemas.api_key_schema import ApiKeyAuth +from app.schemas.memory_api_schema import CreateEndUserRequest, CreateEndUserResponse +from app.services.memory_config_service import MemoryConfigService + +router = APIRouter(prefix="/end_user", tags=["V1 - End User API"]) +logger = get_business_logger() + + +@router.post("/create") +@require_api_key(scopes=["memory"]) +async def create_end_user( + request: Request, + api_key_auth: ApiKeyAuth = None, + db: Session = Depends(get_db), + message: str = Body(..., description="Request body"), +): + """ + Create or retrieve an end user for the workspace. + + Creates a new end user and connects it to a memory configuration. + If an end user with the same other_id already exists in the workspace, + returns the existing one. + + Optionally accepts a memory_config_id to connect the end user to a specific + memory configuration. If not provided, falls back to the workspace default config. + """ + body = await request.json() + payload = CreateEndUserRequest(**body) + workspace_id = api_key_auth.workspace_id + + logger.info(f"Create end user request - other_id: {payload.other_id}, workspace_id: {workspace_id}") + + # Resolve memory_config_id: explicit > workspace default + memory_config_id = None + config_service = MemoryConfigService(db) + + if payload.memory_config_id: + try: + memory_config_id = uuid.UUID(payload.memory_config_id) + except ValueError: + raise BusinessException( + f"Invalid memory_config_id format: {payload.memory_config_id}", + BizCode.INVALID_PARAMETER + ) + config = config_service.get_config_with_fallback(memory_config_id, workspace_id) + if not config: + raise BusinessException( + f"Memory config not found: {payload.memory_config_id}", + BizCode.MEMORY_CONFIG_NOT_FOUND + ) + memory_config_id = config.config_id + else: + default_config = config_service.get_workspace_default_config(workspace_id) + if default_config: + memory_config_id = default_config.config_id + logger.info(f"Using workspace default memory config: {memory_config_id}") + else: + logger.warning(f"No default memory config found for workspace: {workspace_id}") + + end_user_repo = EndUserRepository(db) + end_user = end_user_repo.get_or_create_end_user_with_config( + app_id=api_key_auth.resource_id, + workspace_id=workspace_id, + other_id=payload.other_id, + memory_config_id=memory_config_id, + ) + + logger.info(f"End user ready: {end_user.id}") + + result = { + "id": str(end_user.id), + "other_id": end_user.other_id or "", + "other_name": end_user.other_name or "", + "workspace_id": str(end_user.workspace_id), + "memory_config_id": str(end_user.memory_config_id) if end_user.memory_config_id else None, + } + + return success(data=CreateEndUserResponse(**result).model_dump(), msg="End user created successfully") diff --git a/api/app/schemas/memory_api_schema.py b/api/app/schemas/memory_api_schema.py index 84a34e8a..ff62355f 100644 --- a/api/app/schemas/memory_api_schema.py +++ b/api/app/schemas/memory_api_schema.py @@ -138,21 +138,13 @@ class CreateEndUserRequest(BaseModel): """Request schema for creating an end user. Attributes: - workspace_id: Workspace ID (required) other_id: External user identifier (required) other_name: Display name for the end user + memory_config_id: Optional memory config ID. If not provided, uses workspace default. """ - workspace_id: str = Field(..., description="Workspace ID (required)") other_id: str = Field(..., description="External user identifier (required)") other_name: Optional[str] = Field("", description="Display name") - - @field_validator("workspace_id") - @classmethod - def validate_workspace_id(cls, v: str) -> str: - """Validate that workspace_id is not empty.""" - if not v or not v.strip(): - raise ValueError("workspace_id is required and cannot be empty") - return v.strip() + memory_config_id: Optional[str] = Field(None, description="Memory config ID. Falls back to workspace default if not provided.") @field_validator("other_id") @classmethod @@ -171,11 +163,13 @@ class CreateEndUserResponse(BaseModel): other_id: External user identifier other_name: Display name workspace_id: Workspace the user belongs to + memory_config_id: Connected memory config ID """ id: str = Field(..., description="End user UUID") other_id: str = Field(..., description="External user identifier") other_name: str = Field("", description="Display name") workspace_id: str = Field(..., description="Workspace ID") + memory_config_id: Optional[str] = Field(None, description="Connected memory config ID") class MemoryConfigItem(BaseModel):