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,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用管理模块
"""
from core.application.model import Application
from core.application.service import ApplicationService
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用管理API
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.config import settings
from app.base_schema import PaginatedResponse, ResponseModel
from core.application.schema import (
ApplicationCreate,
ApplicationUpdate,
ApplicationResponse,
ApplicationListResponse,
)
from core.application.service import ApplicationService
router = APIRouter(prefix="/applications", tags=["应用管理"])
@router.post("/", response_model=ApplicationResponse, summary="创建应用")
async def create_application(
data: ApplicationCreate,
db: AsyncSession = Depends(get_db)
):
"""
创建新应用
- **name**: 应用名称
- **code**: 应用编码(唯一标识,用于URL路由,只能包含字母、数字、下划线和短横线)
- **description**: 应用描述(可选)
- **icon**: 应用图标(可选)
- **app_type**: 应用类型(可选,默认mixed)
"""
# 检查编码唯一性
if not await ApplicationService.check_unique(db, field="code", value=data.code):
raise HTTPException(status_code=400, detail="应用编码已存在")
# 检查名称唯一性
if not await ApplicationService.check_unique(db, field="name", value=data.name):
raise HTTPException(status_code=400, detail="应用名称已存在")
return await ApplicationService.create(db=db, data=data)
@router.get("/", response_model=PaginatedResponse[ApplicationListResponse], summary="获取应用列表")
async def get_applications(
page: int = Query(default=1, ge=1, description="页码"),
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
keyword: str = Query(default=None, description="搜索关键词"),
app_type: str = Query(default=None, alias="appType", description="应用类型"),
status: str = Query(default=None, description="状态"),
db: AsyncSession = Depends(get_db)
):
"""
获取应用列表(分页)
支持按关键词、类型、状态筛选
"""
items, total = await ApplicationService.search(
db,
keyword=keyword,
app_type=app_type,
status=status,
page=page,
page_size=page_size
)
return PaginatedResponse(items=items, total=total)
@router.get("/stats", response_model=ResponseModel, summary="获取应用统计")
async def get_stats(db: AsyncSession = Depends(get_db)):
"""
获取应用统计信息
"""
stats = await ApplicationService.get_stats(db)
return ResponseModel(message="获取成功", data=stats)
@router.get("/check/unique", response_model=ResponseModel, summary="检查字段唯一性")
async def check_unique(
field: str = Query(..., description="字段名:code 或 name"),
value: str = Query(..., description="字段值"),
exclude_id: str = Query(default=None, alias="excludeId", description="排除的记录ID"),
db: AsyncSession = Depends(get_db)
):
"""
检查字段值是否唯一
"""
allowed_fields = ["code", "name"]
if field not in allowed_fields:
raise HTTPException(status_code=400, detail=f"不支持检查字段: {field}")
is_unique = await ApplicationService.check_unique(db, field=field, value=value, exclude_id=exclude_id)
return ResponseModel(
message="可用" if is_unique else "已存在",
data={"unique": is_unique}
)
@router.get("/code/{code}", response_model=ApplicationResponse, summary="根据编码获取应用")
async def get_application_by_code(
code: str,
db: AsyncSession = Depends(get_db)
):
"""
根据应用编码获取应用详情
"""
db_obj = await ApplicationService.get_by_code(db, code=code)
if db_obj is None:
raise HTTPException(status_code=404, detail="应用不存在")
return db_obj
@router.get("/{record_id}", response_model=ApplicationResponse, summary="获取应用详情")
async def get_application(
record_id: str,
db: AsyncSession = Depends(get_db)
):
"""
根据ID获取应用详情
"""
db_obj = await ApplicationService.get_by_id(db, record_id=record_id)
if db_obj is None:
raise HTTPException(status_code=404, detail="应用不存在")
return db_obj
@router.put("/{record_id}", response_model=ApplicationResponse, summary="更新应用")
async def update_application(
record_id: str,
data: ApplicationUpdate,
db: AsyncSession = Depends(get_db)
):
"""
更新应用信息
"""
# 检查编码唯一性(排除自身)
if data.code and not await ApplicationService.check_unique(db, field="code", value=data.code, exclude_id=record_id):
raise HTTPException(status_code=400, detail="应用编码已存在")
# 检查名称唯一性(排除自身)
if data.name and not await ApplicationService.check_unique(db, field="name", value=data.name, exclude_id=record_id):
raise HTTPException(status_code=400, detail="应用名称已存在")
db_obj = await ApplicationService.update(db, record_id=record_id, data=data)
if db_obj is None:
raise HTTPException(status_code=404, detail="应用不存在")
# 清除该应用相关的菜单缓存
from core.menu.service import MenuService
await MenuService.invalidate_app_menu_cache(db_obj.code)
return db_obj
@router.post("/{record_id}/publish", response_model=ApplicationResponse, summary="发布应用")
async def publish_application(
record_id: str,
db: AsyncSession = Depends(get_db)
):
"""
发布应用;已停用状态也可调用本接口重新启用(状态变为已发布)
"""
db_obj = await ApplicationService.publish(db, record_id=record_id)
if db_obj is None:
raise HTTPException(status_code=404, detail="应用不存在")
return db_obj
@router.post("/{record_id}/disable", response_model=ApplicationResponse, summary="停用应用")
async def disable_application(
record_id: str,
db: AsyncSession = Depends(get_db)
):
"""
停用应用
"""
db_obj = await ApplicationService.disable(db, record_id=record_id)
if db_obj is None:
raise HTTPException(status_code=404, detail="应用不存在")
return db_obj
@router.delete("/{record_id}", response_model=ResponseModel, summary="删除应用")
async def delete_application(
record_id: str,
hard: bool = Query(default=False, description="是否物理删除"),
db: AsyncSession = Depends(get_db)
):
"""
删除应用
- **hard=false**: 逻辑删除(默认)
- **hard=true**: 物理删除
"""
success = await ApplicationService.delete(db, record_id=record_id, hard=hard)
if not success:
raise HTTPException(status_code=404, detail="应用不存在")
return ResponseModel(message="删除成功")
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用管理数据模型
"""
from sqlalchemy import Column, String, Text, Integer, JSON
from app.base_model import BaseModel
class Application(BaseModel):
"""应用模型 - 低代码平台的顶层容器"""
__tablename__ = "core_application"
name = Column(String(100), nullable=False, comment="应用名称")
code = Column(String(100), unique=True, nullable=False, index=True, comment="应用编码(唯一标识,用于URL路由)")
description = Column(Text, default="", comment="应用描述")
icon = Column(String(100), default="", comment="应用图标")
cover = Column(String(500), default="", comment="应用封面图URL")
# 应用类型:form-表单应用, workflow-流程应用, dashboard-数据应用, screen-大屏应用, mixed-混合应用, ai-AI应用
app_type = Column(String(20), default="mixed", index=True, comment="应用类型")
# 状态:draft-开发中, published-已发布, disabled-已停用
status = Column(String(20), default="draft", index=True, comment="应用状态")
# 应用首页路径
home_path = Column(String(200), nullable=True, comment="应用首页路径")
# 版本号
version = Column(Integer, default=1, comment="版本号")
# 应用配置(JSON格式,可存储主题、权限、自定义配置等)
config = Column(JSON, default=dict, comment="应用配置")
# 所有者(逻辑外键关联 core_user)
owner_id = Column(String(21), nullable=True, index=True, comment="所有者ID")
# 团队成员ID列表(JSON数组)
team_ids = Column(JSON, default=list, comment="团队成员ID列表")
# 开发模式下显示的系统菜单ID列表(JSON数组)
# 如果为空或None,则显示所有系统菜单;否则只显示选中的系统菜单
system_menu_ids = Column(JSON, default=list, comment="开发模式下显示的系统菜单ID列表")
@@ -0,0 +1,92 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用管理Schema
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, ConfigDict, Field
from app.base_schema import CSTDatetime
class ApplicationBase(BaseModel):
"""应用基础Schema"""
name: str = Field(..., min_length=1, max_length=100, description="应用名称")
code: str = Field(..., min_length=1, max_length=100, pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*$", description="应用编码")
description: Optional[str] = Field(default="", description="应用描述")
icon: Optional[str] = Field(default="", description="应用图标")
cover: Optional[str] = Field(default="", description="应用封面图URL")
app_type: Optional[str] = Field(default="mixed", description="应用类型: form/workflow/dashboard/screen/mixed/ai")
home_path: Optional[str] = Field(default=None, description="应用首页路径")
config: Optional[Dict[str, Any]] = Field(default_factory=dict, description="应用配置")
system_menu_ids: Optional[List[str]] = Field(default_factory=list, description="开发模式下显示的系统菜单ID列表")
class ApplicationCreate(ApplicationBase):
"""创建应用Schema"""
pass
class ApplicationUpdate(BaseModel):
"""更新应用Schema - 所有字段可选"""
name: Optional[str] = Field(default=None, min_length=1, max_length=100, description="应用名称")
code: Optional[str] = Field(default=None, min_length=1, max_length=100, pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*$", description="应用编码")
description: Optional[str] = Field(default=None, description="应用描述")
icon: Optional[str] = Field(default=None, description="应用图标")
cover: Optional[str] = Field(default=None, description="应用封面图URL")
app_type: Optional[str] = Field(default=None, description="应用类型")
home_path: Optional[str] = Field(default=None, description="应用首页路径")
status: Optional[str] = Field(default=None, description="应用状态: draft/published/disabled")
config: Optional[Dict[str, Any]] = Field(default=None, description="应用配置")
team_ids: Optional[List[str]] = Field(default=None, description="团队成员ID列表")
system_menu_ids: Optional[List[str]] = Field(default=None, description="开发模式下显示的系统菜单ID列表")
class ApplicationResponse(ApplicationBase):
"""应用响应Schema"""
id: str
status: str = "draft"
home_path: Optional[str] = None
version: int = 1
owner_id: Optional[str] = None
team_ids: List[str] = []
system_menu_ids: Optional[List[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 ApplicationListResponse(BaseModel):
"""应用列表响应Schema(简化版)"""
id: str
name: str
code: str
description: Optional[str] = ""
icon: Optional[str] = ""
cover: Optional[str] = ""
app_type: str = "mixed"
status: str = "draft"
home_path: Optional[str] = None
version: int = 1
owner_id: Optional[str] = None
system_menu_ids: Optional[List[str]] = []
sys_create_datetime: Optional[CSTDatetime] = None
model_config = ConfigDict(from_attributes=True)
class ApplicationStatsResponse(BaseModel):
"""应用统计响应Schema"""
id: str
name: str
code: str
form_count: int = 0
page_count: int = 0
workflow_count: int = 0
screen_count: int = 0
data_source_count: int = 0
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用管理服务层
"""
from typing import Optional, List, Tuple, Any
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_service import BaseService
from core.application.model import Application
from core.application.schema import ApplicationCreate, ApplicationUpdate
class ApplicationService(BaseService[Application, ApplicationCreate, ApplicationUpdate]):
"""
应用服务层
继承BaseService,自动获得增删改查功能
"""
model = Application
# Excel导入导出配置
excel_columns = {
"name": "应用名称",
"code": "应用编码",
"description": "描述",
"app_type": "应用类型",
"status": "状态",
}
excel_sheet_name = "应用列表"
@classmethod
async def get_by_code(
cls,
db: AsyncSession,
code: str
) -> Optional[Application]:
"""
根据应用编码获取应用
:param db: 数据库会话
:param code: 应用编码
:return: 应用或None
"""
result = await db.execute(
select(cls.model).where(
cls.model.code == code,
cls.model.is_deleted == False # noqa: E712
)
)
return result.scalar_one_or_none()
@classmethod
async def get_list_by_owner(
cls,
db: AsyncSession,
owner_id: str,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Application], int]:
"""
获取指定用户拥有的应用列表
:param db: 数据库会话
:param owner_id: 所有者ID
:param page: 页码
:param page_size: 每页数量
:return: (应用列表, 总数)
"""
filters = [cls.model.owner_id == owner_id]
return await cls.get_list(db, page, page_size, filters)
@classmethod
async def get_list_by_status(
cls,
db: AsyncSession,
status: str,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Application], int]:
"""
根据状态获取应用列表
:param db: 数据库会话
:param status: 状态
:param page: 页码
:param page_size: 每页数量
:return: (应用列表, 总数)
"""
filters = [cls.model.status == status]
return await cls.get_list(db, page, page_size, filters)
@classmethod
async def get_list_by_type(
cls,
db: AsyncSession,
app_type: str,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Application], int]:
"""
根据类型获取应用列表
:param db: 数据库会话
:param app_type: 应用类型
:param page: 页码
:param page_size: 每页数量
:return: (应用列表, 总数)
"""
filters = [cls.model.app_type == app_type]
return await cls.get_list(db, page, page_size, filters)
@classmethod
async def search(
cls,
db: AsyncSession,
keyword: Optional[str] = None,
app_type: Optional[str] = None,
status: Optional[str] = None,
owner_id: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> Tuple[List[Application], int]:
"""
搜索应用
:param db: 数据库会话
:param keyword: 关键词(搜索名称和描述)
:param app_type: 应用类型
:param status: 状态
:param owner_id: 所有者ID
:param page: 页码
:param page_size: 每页数量
:return: (应用列表, 总数)
"""
filters = []
if keyword:
filters.append(
(cls.model.name.ilike(f"%{keyword}%")) |
(cls.model.description.ilike(f"%{keyword}%")) |
(cls.model.code.ilike(f"%{keyword}%"))
)
if app_type:
filters.append(cls.model.app_type == app_type)
if status:
filters.append(cls.model.status == status)
if owner_id:
filters.append(cls.model.owner_id == owner_id)
return await cls.get_list(db, page, page_size, filters)
@classmethod
async def publish(
cls,
db: AsyncSession,
record_id: str,
auto_commit: bool = True
) -> Optional[Application]:
"""
发布应用(草稿 -> 已发布;已停用 -> 重新启用为已发布)
:param db: 数据库会话
:param record_id: 应用ID
:param auto_commit: 是否自动提交
:return: 更新后的应用或None
"""
db_obj = await cls.get_by_id(db, record_id)
if not db_obj:
return None
db_obj.status = "published"
db_obj.version += 1
if auto_commit:
await db.commit()
await db.refresh(db_obj)
else:
await db.flush()
await db.refresh(db_obj)
return db_obj
@classmethod
async def disable(
cls,
db: AsyncSession,
record_id: str,
auto_commit: bool = True
) -> Optional[Application]:
"""
停用应用
:param db: 数据库会话
:param record_id: 应用ID
:param auto_commit: 是否自动提交
:return: 更新后的应用或None
"""
db_obj = await cls.get_by_id(db, record_id)
if not db_obj:
return None
db_obj.status = "disabled"
if auto_commit:
await db.commit()
await db.refresh(db_obj)
else:
await db.flush()
await db.refresh(db_obj)
return db_obj
@classmethod
async def get_stats(
cls,
db: AsyncSession
) -> dict:
"""
获取应用统计信息
:param db: 数据库会话
:return: 统计信息
"""
# 总数
total_result = await db.execute(
select(func.count()).select_from(cls.model).where(
cls.model.is_deleted == False # noqa: E712
)
)
total = total_result.scalar() or 0
# 按状态统计
status_result = await db.execute(
select(cls.model.status, func.count()).where(
cls.model.is_deleted == False # noqa: E712
).group_by(cls.model.status)
)
status_stats = {row[0]: row[1] for row in status_result.all()}
# 按类型统计
type_result = await db.execute(
select(cls.model.app_type, func.count()).where(
cls.model.is_deleted == False # noqa: E712
).group_by(cls.model.app_type)
)
type_stats = {row[0]: row[1] for row in type_result.all()}
return {
"total": total,
"by_status": status_stats,
"by_type": type_stats
}