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

126 lines
3.8 KiB
Python

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Tenant, TenantConfig, TenantStatus
from app.providers import get_provider
from app.providers.base import BaseLLMProvider
from app.services.crypto import decrypt_api_key, encrypt_api_key
async def get_provider_for_tenant(
session: AsyncSession, tenant_id: str
) -> BaseLLMProvider:
"""根据租户配置动态实例化 Provider"""
await get_tenant(session, tenant_id)
result = await session.execute(
select(TenantConfig).where(TenantConfig.tenant_id == tenant_id)
)
config = result.scalar_one_or_none()
if not config:
raise HTTPException(status_code=404, detail="Tenant config not found")
api_key = decrypt_api_key(config.llm_api_key)
return get_provider(
provider_type=config.llm_provider,
api_key=api_key,
model=config.llm_model,
base_url=config.llm_base_url,
)
async def create_tenant(session: AsyncSession, name: str, slug: str) -> Tenant:
existing = await session.execute(select(Tenant).where(Tenant.slug == slug))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Slug already exists")
tenant = Tenant(name=name, slug=slug)
session.add(tenant)
await session.commit()
await session.refresh(tenant)
return tenant
async def get_tenant(session: AsyncSession, tenant_id: str) -> Tenant:
result = await session.execute(
select(Tenant).where(Tenant.id == tenant_id, Tenant.status != TenantStatus.deleted)
)
tenant = result.scalar_one_or_none()
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
return tenant
async def list_tenants(session: AsyncSession) -> list[Tenant]:
result = await session.execute(
select(Tenant).where(Tenant.status != TenantStatus.deleted)
)
return list(result.scalars().all())
async def update_tenant(
session: AsyncSession, tenant_id: str, name: str | None, slug: str | None
) -> Tenant:
tenant = await get_tenant(session, tenant_id)
if slug and slug != tenant.slug:
existing = await session.execute(select(Tenant).where(Tenant.slug == slug))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Slug already exists")
tenant.slug = slug
if name:
tenant.name = name
await session.commit()
await session.refresh(tenant)
return tenant
async def delete_tenant(session: AsyncSession, tenant_id: str) -> None:
tenant = await get_tenant(session, tenant_id)
tenant.status = TenantStatus.deleted
await session.commit()
async def update_tenant_config(
session: AsyncSession,
tenant_id: str,
llm_provider: str,
llm_api_key: str,
llm_model: str,
llm_base_url: str | None = None,
max_tokens_per_month: int = 1000000,
) -> TenantConfig:
await get_tenant(session, tenant_id)
result = await session.execute(
select(TenantConfig).where(TenantConfig.tenant_id == tenant_id)
)
config = result.scalar_one_or_none()
encrypted_key = encrypt_api_key(llm_api_key)
if config:
config.llm_provider = llm_provider
config.llm_api_key = encrypted_key
config.llm_model = llm_model
config.llm_base_url = llm_base_url
config.max_tokens_per_month = max_tokens_per_month
else:
config = TenantConfig(
tenant_id=tenant_id,
llm_provider=llm_provider,
llm_api_key=encrypted_key,
llm_model=llm_model,
llm_base_url=llm_base_url,
max_tokens_per_month=max_tokens_per_month,
)
session.add(config)
await session.commit()
await session.refresh(config)
return config