Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
546 changes: 546 additions & 0 deletions docs/PRD/品牌定制 Logo 与文案配置 PRD.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/backend/bisheng/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from bisheng.tenant.api.router import router as tenant_router
from bisheng.admin.api.router import router as admin_router
from bisheng.approval.api.router import router as approval_router
from bisheng.brand.api.router import router as brand_router
from bisheng.sensitive_word.api.router import router as sensitive_word_policy_router
from bisheng.workstation.api.endpoints.conversation_export import router as conversation_export_router

Expand Down Expand Up @@ -86,6 +87,7 @@
router.include_router(citation_router)
router.include_router(admin_router)
router.include_router(approval_router)
router.include_router(brand_router)
router.include_router(sensitive_word_policy_router)

router_rpc = APIRouter(prefix='/api/v2', )
Expand Down
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand customization module."""
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand API package."""
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/api/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand endpoint package."""
81 changes: 81 additions & 0 deletions src/backend/bisheng/brand/api/endpoints/brand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
from fastapi.responses import PlainTextResponse

from bisheng.brand.domain.schemas.brand_schema import BrandAssetCategory, BrandConfigUpdate
from bisheng.brand.domain.services.brand_service import BrandService
from bisheng.common.dependencies.user_deps import UserPayload
from bisheng.common.schemas.api import resp_200


router = APIRouter()


def get_brand_service() -> BrandService:
return BrandService()


@router.get('/config')
async def get_brand_config(
admin_user: UserPayload = Depends(UserPayload.get_admin_user),
service: BrandService = Depends(get_brand_service),
):
config = await service.get_config()
return resp_200(config.model_dump(mode='json'))


@router.put('/config')
async def update_brand_config(
data: BrandConfigUpdate,
admin_user: UserPayload = Depends(UserPayload.get_admin_user),
service: BrandService = Depends(get_brand_service),
):
config = await service.save_config(data)
return resp_200(config.model_dump(mode='json'))

Comment on lines +17 to +34

@router.get('/assets/options')
async def list_brand_asset_options(
category: BrandAssetCategory = Query(...),
admin_user: UserPayload = Depends(UserPayload.get_admin_user),
service: BrandService = Depends(get_brand_service),
):
options = await service.list_asset_options(category)
return resp_200([option.model_dump(mode='json') for option in options])


@router.post('/assets')
async def upload_brand_asset(
admin_user: UserPayload = Depends(UserPayload.get_admin_user),
file: UploadFile = File(...),
category: BrandAssetCategory | None = Form(default=None),
service: BrandService = Depends(get_brand_service),
):
try:
result = await service.upload_asset(file, category=category)
return resp_200(result.model_dump(mode='json'))
finally:
if file:
await file.close()


@router.delete('/assets')
async def delete_brand_asset(
category: BrandAssetCategory = Query(...),
relative_path: str = Query(...),
admin_user: UserPayload = Depends(UserPayload.get_admin_user),
service: BrandService = Depends(get_brand_service),
):
default_asset = await service.delete_asset(category, relative_path)
return resp_200(default_asset.model_dump(mode='json'))


@router.get('/runtime.js')
async def get_brand_runtime_script(
service: BrandService = Depends(get_brand_service),
):
script = await service.build_runtime_script()
return PlainTextResponse(
script,
media_type='application/javascript',
headers={'Cache-Control': 'no-store'},
)
9 changes: 9 additions & 0 deletions src/backend/bisheng/brand/api/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Brand router aggregation."""

from fastapi import APIRouter

from bisheng.brand.api.endpoints.brand import router as brand_router


router = APIRouter(prefix='/brand', tags=['Brand'])
router.include_router(brand_router)
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/domain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand domain package."""
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/domain/repositories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand repositories package."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Optional

from bisheng.common.models.config import Config, ConfigDao


class BrandConfigRepository:
"""Persistence adapter for instance-level brand config."""

async def get_value(self, key: str) -> Optional[str]:
config = await ConfigDao.aget_config_by_key(key)
return config.value if config else None

async def upsert_value(self, key: str, value: str) -> Config:
return await ConfigDao.insert_or_update_config(key, value)
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/domain/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand schemas package."""
71 changes: 71 additions & 0 deletions src/backend/bisheng/brand/domain/schemas/brand_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import Literal, Optional

from pydantic import BaseModel, Field, field_validator


class BrandText(BaseModel):
zh: str = Field(default='', max_length=20)
en: str = Field(default='', max_length=20)

@field_validator('zh', 'en')
@classmethod
def validate_plain_text(cls, value: str) -> str:
text = value.strip()
if '<' in text or '>' in text:
raise ValueError('HTML is not allowed')
return text


class BrandAsset(BaseModel):
url: str = ''
relative_path: str = ''
file_name: str = ''


BrandAssetCategory = Literal[
'favicon',
'loginHeroLight',
'loginHeroDark',
'headerLogoLight',
'headerLogoDark',
'loadingIcon',
]


class BrandAssetOption(BrandAsset):
is_default: bool = False


class BrandAssets(BaseModel):
favicon: BrandAsset = Field(default_factory=BrandAsset)
loginHeroLight: BrandAsset = Field(default_factory=BrandAsset)
loginHeroDark: BrandAsset = Field(default_factory=BrandAsset)
headerLogoLight: BrandAsset = Field(default_factory=BrandAsset)
headerLogoDark: BrandAsset = Field(default_factory=BrandAsset)


class BrandLoading(BaseModel):
icon: Optional[BrandAsset] = None
iconOptions: list[BrandAsset] = Field(default_factory=list)
animation: Literal['', 'animate-spin', 'animate-pulse', 'animate-bounce'] = ''


class BrandConfig(BaseModel):
brandName: BrandText = Field(default_factory=lambda: BrandText(zh='BISHENG', en='BISHENG'))
linsightAgentName: BrandText = Field(default_factory=lambda: BrandText(zh='灵思', en='Linsight'))
assets: BrandAssets = Field(default_factory=BrandAssets)
loading: BrandLoading = Field(default_factory=BrandLoading)
URLLoadingIcon: str = ''


class BrandConfigUpdate(BaseModel):
brandName: BrandText = Field(default_factory=lambda: BrandText(zh='BISHENG', en='BISHENG'))
assets: BrandAssets = Field(default_factory=BrandAssets)
loading: BrandLoading = Field(default_factory=BrandLoading)
URLLoadingIcon: str = ''


class BrandAssetUploadResponse(BaseModel):
url: str
relative_path: str
file_name: str
1 change: 1 addition & 0 deletions src/backend/bisheng/brand/domain/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Brand services package."""
Loading