root c62156af53 feat(backend): 数字员工平台 B1+B2 批次实现
B1: 项目脚手架 + 数据模型 + 租户管理
- Task 1.1: FastAPI 项目脚手架、SQLite + async SQLAlchemy
- Task 1.2: 7 个数据模型 (Tenant, TenantConfig, DigitalEmployee, Conversation, Message, KnowledgeBase, Document)
- Task 1.3: 租户 CRUD API + LLM 配置(含 API Key AES 加密)

B2: 数字员工配置 + LLM Provider 抽象层
- Task 2.1: 数字员工 CRUD API(关联知识库)
- Task 2.2: BaseLLMProvider 抽象接口 + OpenAI/Qwen Provider
- Task 2.3: Provider 动态实例化 + test-provider 端点

验证: 26 个测试全部通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:29:48 +08:00

129 lines
3.8 KiB
Python

from fastapi import APIRouter, Depends, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db
from app.schemas.tenant import (
TenantConfigCreate,
TenantConfigResponse,
TenantCreate,
TenantResponse,
TenantUpdate,
)
from app.services import tenant_service
router = APIRouter(prefix="/tenants", tags=["tenants"])
@router.post("", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
async def create_tenant(
data: TenantCreate, session: AsyncSession = Depends(get_db)
) -> TenantResponse:
tenant = await tenant_service.create_tenant(session, data.name, data.slug)
return TenantResponse(
id=tenant.id,
name=tenant.name,
slug=tenant.slug,
status=tenant.status.value,
created_at=tenant.created_at,
updated_at=tenant.updated_at,
)
@router.get("", response_model=list[TenantResponse])
async def list_tenants(session: AsyncSession = Depends(get_db)) -> list[TenantResponse]:
tenants = await tenant_service.list_tenants(session)
return [
TenantResponse(
id=t.id,
name=t.name,
slug=t.slug,
status=t.status.value,
created_at=t.created_at,
updated_at=t.updated_at,
)
for t in tenants
]
@router.get("/{tenant_id}", response_model=TenantResponse)
async def get_tenant(
tenant_id: str, session: AsyncSession = Depends(get_db)
) -> TenantResponse:
tenant = await tenant_service.get_tenant(session, tenant_id)
return TenantResponse(
id=tenant.id,
name=tenant.name,
slug=tenant.slug,
status=tenant.status.value,
created_at=tenant.created_at,
updated_at=tenant.updated_at,
)
@router.patch("/{tenant_id}", response_model=TenantResponse)
async def update_tenant(
tenant_id: str,
data: TenantUpdate,
session: AsyncSession = Depends(get_db),
) -> TenantResponse:
tenant = await tenant_service.update_tenant(session, tenant_id, data.name, data.slug)
return TenantResponse(
id=tenant.id,
name=tenant.name,
slug=tenant.slug,
status=tenant.status.value,
created_at=tenant.created_at,
updated_at=tenant.updated_at,
)
@router.delete("/{tenant_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_tenant(tenant_id: str, session: AsyncSession = Depends(get_db)) -> None:
await tenant_service.delete_tenant(session, tenant_id)
@router.put("/{tenant_id}/config", response_model=TenantConfigResponse)
async def update_tenant_config(
tenant_id: str,
data: TenantConfigCreate,
session: AsyncSession = Depends(get_db),
) -> TenantConfigResponse:
config = await tenant_service.update_tenant_config(
session,
tenant_id,
data.llm_provider,
data.llm_api_key,
data.llm_model,
data.llm_base_url,
data.max_tokens_per_month or 1000000,
)
return TenantConfigResponse(
id=config.id,
tenant_id=config.tenant_id,
llm_provider=config.llm_provider,
llm_model=config.llm_model,
llm_base_url=config.llm_base_url,
max_tokens_per_month=config.max_tokens_per_month,
created_at=config.created_at,
updated_at=config.updated_at,
)
class ProviderTestResponse(BaseModel):
provider: str
model: str
status: str
@router.post("/{tenant_id}/test-provider", response_model=ProviderTestResponse)
async def test_provider(
tenant_id: str, session: AsyncSession = Depends(get_db)
) -> ProviderTestResponse:
provider = await tenant_service.get_provider_for_tenant(session, tenant_id)
return ProviderTestResponse(
provider=provider.__class__.__name__.replace("Provider", "").lower(),
model=provider.model,
status="ok",
)