Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,148 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.base_schema import ResponseModel
from app.resource_registry import ResourceRegistry
from core.resource_scope.field_permission.schema import (
FieldPermissionResponse,
FieldPermissionBatchUpdate,
ResourceFieldsMetadataResponse,
ResourceFieldMetadata
)
from core.resource_scope.field_permission.service import ResourceFieldPermissionService
router = APIRouter(prefix="/field-permissions", tags=["字段权限配置"])
@router.get("/resource-fields", response_model=List[ResourceFieldsMetadataResponse], summary="获取所有资源的字段元数据")
async def get_all_resource_fields(
application_id: Optional[str] = Query(None, alias="applicationId", description="应用ID,子应用访问时只显示该应用的资源")
):
"""获取所有已注册资源的字段元数据"""
# 获取资源列表,支持按应用过滤
all_resources = ResourceRegistry.get_all_resources(application_id=application_id)
result = []
for resource_item in all_resources:
resource_type = resource_item['resource_type']
resource_info = ResourceRegistry.get_resource(resource_type)
if not resource_info:
continue
# 优先从 service_class 获取字段元数据,否则从注册表获取
field_metadata = None
service_class = resource_info.get('service')
if service_class and hasattr(service_class, 'FIELD_METADATA'):
field_metadata = service_class.FIELD_METADATA
elif resource_info.get('field_metadata'):
field_metadata = resource_info.get('field_metadata')
if not field_metadata:
continue
fields = [
ResourceFieldMetadata(
field_name=field_name,
label=meta.get('label', field_name),
field_type=meta.get('field_type', 'string'),
required=meta.get('required', False),
sensitive=meta.get('sensitive', False),
maskable=meta.get('maskable', False),
default_permission=meta.get('default_permission', 'write')
)
for field_name, meta in field_metadata.items()
]
result.append(ResourceFieldsMetadataResponse(
resource_type=resource_type,
display_name=resource_info.get('display_name', resource_type),
fields=fields
))
return result
@router.get("/resource-fields/{resource_type}", response_model=ResourceFieldsMetadataResponse, summary="获取指定资源的字段元数据")
async def get_resource_fields(resource_type: str):
"""获取指定资源的字段元数据"""
resource_info = ResourceRegistry.get_resource(resource_type)
if not resource_info:
raise HTTPException(status_code=404, detail=f"资源类型不存在: {resource_type}")
# 优先从 service_class 获取字段元数据,否则从注册表获取
field_metadata = None
service_class = resource_info.get('service')
if service_class and hasattr(service_class, 'FIELD_METADATA'):
field_metadata = service_class.FIELD_METADATA
elif resource_info.get('field_metadata'):
field_metadata = resource_info.get('field_metadata')
if not field_metadata:
raise HTTPException(status_code=404, detail=f"资源未定义字段元数据: {resource_type}")
fields = [
ResourceFieldMetadata(
field_name=field_name,
label=meta.get('label', field_name),
field_type=meta.get('field_type', 'string'),
required=meta.get('required', False),
sensitive=meta.get('sensitive', False),
maskable=meta.get('maskable', False),
default_permission=meta.get('default_permission', 'write')
)
for field_name, meta in field_metadata.items()
]
return ResourceFieldsMetadataResponse(
resource_type=resource_type,
display_name=resource_info.get('display_name', resource_type),
fields=fields
)
@router.get("/{role_id}", response_model=List[FieldPermissionResponse], summary="获取角色的字段权限配置")
async def get_role_field_permissions(
role_id: str,
resource_type: str = Query(..., description="资源类型"),
db: AsyncSession = Depends(get_db)
):
"""获取角色在指定资源类型下的字段权限配置"""
configs = await ResourceFieldPermissionService.get_by_role_and_resource(
db, role_id, resource_type
)
return configs
@router.post("/batch", response_model=ResponseModel, summary="批量更新字段权限配置")
async def batch_update_field_permissions(
data: FieldPermissionBatchUpdate,
db: AsyncSession = Depends(get_db)
):
"""批量更新角色的字段权限配置"""
# 验证资源类型是否存在
if not ResourceRegistry.validate_resource_type(data.resource_type):
raise HTTPException(status_code=400, detail=f"资源类型不存在: {data.resource_type}")
await ResourceFieldPermissionService.batch_update(db, data)
return ResponseModel(message="字段权限配置成功")
@router.delete("/{role_id}", response_model=ResponseModel, summary="删除角色的字段权限配置")
async def delete_role_field_permissions(
role_id: str,
resource_type: str = Query(..., description="资源类型"),
db: AsyncSession = Depends(get_db)
):
"""删除角色在指定资源类型下的所有字段权限配置"""
from sqlalchemy import delete
from core.resource_scope.field_permission.model import ResourceFieldPermissionConfig
stmt = delete(ResourceFieldPermissionConfig).where(
ResourceFieldPermissionConfig.role_id == role_id,
ResourceFieldPermissionConfig.resource_type == resource_type
)
await db.execute(stmt)
await db.commit()
return ResponseModel(message="删除成功")
@@ -0,0 +1,24 @@
from sqlalchemy import Column, String, Index
from app.base_model import BaseModel
class ResourceFieldPermissionConfig(BaseModel):
"""角色资源字段权限配置(列权限)"""
__tablename__ = "sys_role_resource_field_permission_config"
role_id = Column(String(50), nullable=False, comment="角色ID")
resource_type = Column(String(100), nullable=False, comment="资源类型")
field_name = Column(String(100), nullable=False, comment="字段名称")
permission_type = Column(
String(20),
nullable=False,
default="read",
comment="权限类型: read/write/hidden/masked"
)
mask_rule = Column(String(50), nullable=True, comment="脱敏规则: phone/email/id_card/name")
__table_args__ = (
Index('idx_role_resource_field', 'role_id', 'resource_type', 'field_name', unique=True),
Index('idx_role_resource', 'role_id', 'resource_type'),
)
@@ -0,0 +1,81 @@
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, ConfigDict, field_validator
from app.base_schema import CSTDatetime
class FieldPermissionBase(BaseModel):
"""字段权限基础 Schema"""
field_name: str
permission_type: str = "read" # read/write/hidden/masked
mask_rule: Optional[str] = None
@field_validator('permission_type')
@classmethod
def validate_permission_type(cls, v):
allowed = ['read', 'write', 'hidden', 'masked']
if v not in allowed:
raise ValueError(f'permission_type must be one of {allowed}')
return v
@field_validator('mask_rule')
@classmethod
def validate_mask_rule(cls, v, info):
if info.data.get('permission_type') == 'masked' and not v:
raise ValueError('mask_rule is required when permission_type is masked')
if v:
allowed = ['phone', 'email', 'id_card', 'name', 'default']
if v not in allowed:
raise ValueError(f'mask_rule must be one of {allowed}')
return v
class FieldPermissionCreate(FieldPermissionBase):
"""创建字段权限配置"""
role_id: str
resource_type: str
class FieldPermissionUpdate(BaseModel):
"""更新字段权限配置"""
permission_type: Optional[str] = None
mask_rule: Optional[str] = None
class FieldPermissionResponse(FieldPermissionBase):
"""字段权限配置响应"""
id: str
role_id: str
resource_type: str
sort: int = 0
is_deleted: bool = False
sys_create_datetime: Optional[CSTDatetime] = None
sys_update_datetime: Optional[CSTDatetime] = None
model_config = ConfigDict(from_attributes=True)
class FieldPermissionBatchUpdate(BaseModel):
"""批量更新字段权限配置"""
role_id: str
resource_type: str
configs: List[FieldPermissionBase]
class ResourceFieldMetadata(BaseModel):
"""资源字段元数据"""
field_name: str
label: str
field_type: str # string/integer/boolean/datetime
required: bool = False # 是否必填字段(必填字段不可隐藏)
sensitive: bool = False # 是否敏感字段
maskable: bool = False # 是否可脱敏
default_permission: str = "write"
class ResourceFieldsMetadataResponse(BaseModel):
"""资源字段元数据响应"""
resource_type: str
display_name: str
fields: List[ResourceFieldMetadata]
@@ -0,0 +1,152 @@
from typing import List, Dict
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_service import BaseService
from core.resource_scope.field_permission.model import ResourceFieldPermissionConfig
from core.resource_scope.field_permission.schema import (
FieldPermissionCreate,
FieldPermissionUpdate,
FieldPermissionBatchUpdate
)
class ResourceFieldPermissionService(BaseService[
ResourceFieldPermissionConfig,
FieldPermissionCreate,
FieldPermissionUpdate
]):
"""资源字段权限配置服务"""
model = ResourceFieldPermissionConfig
@classmethod
async def get_by_role_and_resource(
cls,
db: AsyncSession,
role_id: str,
resource_type: str
) -> List[ResourceFieldPermissionConfig]:
"""获取角色在指定资源类型下的字段权限配置"""
stmt = select(cls.model).where(
cls.model.role_id == role_id,
cls.model.resource_type == resource_type,
cls.model.is_deleted == False
)
result = await db.execute(stmt)
return list(result.scalars().all())
@classmethod
async def get_by_roles_and_resource(
cls,
db: AsyncSession,
role_ids: List[str],
resource_type: str
) -> List[ResourceFieldPermissionConfig]:
"""获取多个角色在指定资源类型下的字段权限配置"""
stmt = select(cls.model).where(
cls.model.role_id.in_(role_ids),
cls.model.resource_type == resource_type,
cls.model.is_deleted == False
)
result = await db.execute(stmt)
return list(result.scalars().all())
@classmethod
async def batch_update(
cls,
db: AsyncSession,
data: FieldPermissionBatchUpdate
) -> None:
"""批量更新字段权限配置"""
from app.field_permission_cache import FieldPermissionCache
# 1. 删除该角色在该资源类型下的所有字段权限配置
stmt = delete(cls.model).where(
cls.model.role_id == data.role_id,
cls.model.resource_type == data.resource_type
)
await db.execute(stmt)
# 2. 批量插入新的配置
for config in data.configs:
new_config = cls.model(
role_id=data.role_id,
resource_type=data.resource_type,
field_name=config.field_name,
permission_type=config.permission_type,
mask_rule=config.mask_rule
)
db.add(new_config)
await db.commit()
# 3. 清除缓存
await FieldPermissionCache.delete(data.role_id, data.resource_type)
# 清除包含该角色的所有多角色缓存
await FieldPermissionCache.delete_by_role(data.role_id)
@classmethod
async def merge_field_permissions(
cls,
configs: List[ResourceFieldPermissionConfig],
strategy: str = "most_permissive"
) -> Dict[str, Dict]:
"""
合并多个角色的字段权限配置
Args:
configs: 字段权限配置列表
strategy: 合并策略
- most_permissive: 最宽松(默认)
- most_restrictive: 最严格
Returns:
合并后的字段权限字典
"""
field_perms = {}
# 权限等级定义(数字越大权限越高)
perm_levels = {
'hidden': 0,
'masked': 1,
'read': 2,
'write': 3
}
for config in configs:
field_name = config.field_name
current_perm = config.permission_type
current_level = perm_levels.get(current_perm, 0)
if field_name not in field_perms:
field_perms[field_name] = {
'permission': current_perm,
'mask_rule': config.mask_rule,
'level': current_level
}
else:
existing_level = field_perms[field_name]['level']
if strategy == "most_permissive":
# 取权限最高的
if current_level > existing_level:
field_perms[field_name] = {
'permission': current_perm,
'mask_rule': config.mask_rule,
'level': current_level
}
elif strategy == "most_restrictive":
# 取权限最低的
if current_level < existing_level:
field_perms[field_name] = {
'permission': current_perm,
'mask_rule': config.mask_rule,
'level': current_level
}
# 移除临时的 level 字段
for field_name in field_perms:
del field_perms[field_name]['level']
return field_perms