All checks were successful
continuous-integration/drone/push Build is passing
- 后端: TenantApp 模型添加 custom_configs 字段 (LONGTEXT) - 后端: tenant_apps API 支持自定义配置的增删改查 - 前端: 应用订阅编辑对话框增加自定义配置编辑区域 - 支持 key-value-备注 三字段结构 - value 使用 textarea 支持超长文本(如提示词)
255 lines
8.2 KiB
Python
255 lines
8.2 KiB
Python
"""租户应用配置路由"""
|
||
import json
|
||
import secrets
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from pydantic import BaseModel
|
||
from typing import Optional, List
|
||
from sqlalchemy.orm import Session
|
||
|
||
from ..database import get_db
|
||
from ..models.tenant_app import TenantApp
|
||
from ..models.app import App
|
||
from .auth import get_current_user, require_operator
|
||
from ..models.user import User
|
||
|
||
router = APIRouter(prefix="/tenant-apps", tags=["应用配置"])
|
||
|
||
|
||
# Schemas
|
||
|
||
class CustomConfigItem(BaseModel):
|
||
"""自定义配置项"""
|
||
key: str # 配置键
|
||
value: str # 配置值
|
||
remark: Optional[str] = None # 备注说明
|
||
|
||
|
||
class TenantAppCreate(BaseModel):
|
||
tenant_id: str
|
||
app_code: str = "tools"
|
||
app_name: Optional[str] = None
|
||
wechat_app_id: Optional[int] = None # 关联的企微应用ID
|
||
access_token: Optional[str] = None # 如果不传则自动生成
|
||
allowed_origins: Optional[List[str]] = None
|
||
allowed_tools: Optional[List[str]] = None
|
||
custom_configs: Optional[List[CustomConfigItem]] = None # 自定义配置
|
||
|
||
|
||
class TenantAppUpdate(BaseModel):
|
||
app_name: Optional[str] = None
|
||
wechat_app_id: Optional[int] = None # 关联的企微应用ID
|
||
access_token: Optional[str] = None
|
||
allowed_origins: Optional[List[str]] = None
|
||
allowed_tools: Optional[List[str]] = None
|
||
custom_configs: Optional[List[CustomConfigItem]] = None # 自定义配置
|
||
status: Optional[int] = None
|
||
|
||
|
||
# API Endpoints
|
||
|
||
@router.get("")
|
||
async def list_tenant_apps(
|
||
page: int = Query(1, ge=1),
|
||
size: int = Query(20, ge=1, le=1000),
|
||
tenant_id: Optional[str] = None,
|
||
app_code: Optional[str] = None,
|
||
user: User = Depends(get_current_user),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""获取应用配置列表"""
|
||
query = db.query(TenantApp)
|
||
|
||
if tenant_id:
|
||
query = query.filter(TenantApp.tenant_id == tenant_id)
|
||
if app_code:
|
||
query = query.filter(TenantApp.app_code == app_code)
|
||
|
||
total = query.count()
|
||
apps = query.order_by(TenantApp.id.desc()).offset((page - 1) * size).limit(size).all()
|
||
|
||
return {
|
||
"total": total,
|
||
"page": page,
|
||
"size": size,
|
||
"items": [format_tenant_app(app, mask_secret=True, db=db) for app in apps]
|
||
}
|
||
|
||
|
||
@router.get("/{app_id}")
|
||
async def get_tenant_app(
|
||
app_id: int,
|
||
user: User = Depends(get_current_user),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""获取应用配置详情"""
|
||
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
|
||
if not app:
|
||
raise HTTPException(status_code=404, detail="应用配置不存在")
|
||
|
||
return format_tenant_app(app, mask_secret=True, db=db)
|
||
|
||
|
||
@router.post("")
|
||
async def create_tenant_app(
|
||
data: TenantAppCreate,
|
||
user: User = Depends(require_operator),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""创建应用配置"""
|
||
# 验证 app_code 是否存在于应用管理中
|
||
app_exists = db.query(App).filter(App.app_code == data.app_code, App.status == 1).first()
|
||
if not app_exists:
|
||
raise HTTPException(status_code=400, detail=f"应用 '{data.app_code}' 不存在,请先在应用管理中创建")
|
||
|
||
# 检查是否重复
|
||
exists = db.query(TenantApp).filter(
|
||
TenantApp.tenant_id == data.tenant_id,
|
||
TenantApp.app_code == data.app_code
|
||
).first()
|
||
if exists:
|
||
raise HTTPException(status_code=400, detail="该租户应用配置已存在")
|
||
|
||
# 自动生成 access_token
|
||
access_token = data.access_token or secrets.token_hex(32)
|
||
|
||
app = TenantApp(
|
||
tenant_id=data.tenant_id,
|
||
app_code=data.app_code,
|
||
app_name=data.app_name,
|
||
wechat_app_id=data.wechat_app_id,
|
||
access_token=access_token,
|
||
allowed_origins=json.dumps(data.allowed_origins) if data.allowed_origins else None,
|
||
allowed_tools=json.dumps(data.allowed_tools) if data.allowed_tools else None,
|
||
custom_configs=json.dumps([c.model_dump() for c in data.custom_configs], ensure_ascii=False) if data.custom_configs else None,
|
||
status=1
|
||
)
|
||
db.add(app)
|
||
db.commit()
|
||
db.refresh(app)
|
||
|
||
return {"success": True, "id": app.id, "access_token": access_token}
|
||
|
||
|
||
@router.put("/{app_id}")
|
||
async def update_tenant_app(
|
||
app_id: int,
|
||
data: TenantAppUpdate,
|
||
user: User = Depends(require_operator),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""更新应用配置"""
|
||
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
|
||
if not app:
|
||
raise HTTPException(status_code=404, detail="应用配置不存在")
|
||
|
||
update_data = data.model_dump(exclude_unset=True)
|
||
|
||
# 处理 JSON 字段
|
||
if 'allowed_origins' in update_data:
|
||
update_data['allowed_origins'] = json.dumps(update_data['allowed_origins']) if update_data['allowed_origins'] else None
|
||
if 'allowed_tools' in update_data:
|
||
update_data['allowed_tools'] = json.dumps(update_data['allowed_tools']) if update_data['allowed_tools'] else None
|
||
if 'custom_configs' in update_data:
|
||
if update_data['custom_configs']:
|
||
update_data['custom_configs'] = json.dumps(
|
||
[c.model_dump() if hasattr(c, 'model_dump') else c for c in update_data['custom_configs']],
|
||
ensure_ascii=False
|
||
)
|
||
else:
|
||
update_data['custom_configs'] = None
|
||
|
||
for key, value in update_data.items():
|
||
setattr(app, key, value)
|
||
|
||
db.commit()
|
||
return {"success": True}
|
||
|
||
|
||
@router.delete("/{app_id}")
|
||
async def delete_tenant_app(
|
||
app_id: int,
|
||
user: User = Depends(require_operator),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""删除应用配置"""
|
||
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
|
||
if not app:
|
||
raise HTTPException(status_code=404, detail="应用配置不存在")
|
||
|
||
db.delete(app)
|
||
db.commit()
|
||
|
||
return {"success": True}
|
||
|
||
|
||
@router.get("/{app_id}/token")
|
||
async def get_token(
|
||
app_id: int,
|
||
user: User = Depends(require_operator),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""获取真实的 access_token(仅管理员可用)"""
|
||
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
|
||
if not app:
|
||
raise HTTPException(status_code=404, detail="应用配置不存在")
|
||
|
||
# 获取应用的 base_url
|
||
app_info = db.query(App).filter(App.app_code == app.app_code).first()
|
||
base_url = app_info.base_url if app_info else ""
|
||
|
||
return {
|
||
"access_token": app.access_token,
|
||
"base_url": base_url
|
||
}
|
||
|
||
|
||
@router.post("/{app_id}/regenerate-token")
|
||
async def regenerate_token(
|
||
app_id: int,
|
||
user: User = Depends(require_operator),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""重新生成 access_token"""
|
||
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
|
||
if not app:
|
||
raise HTTPException(status_code=404, detail="应用配置不存在")
|
||
|
||
new_token = secrets.token_hex(32)
|
||
app.access_token = new_token
|
||
db.commit()
|
||
|
||
return {"success": True, "access_token": new_token}
|
||
|
||
|
||
def format_tenant_app(app: TenantApp, mask_secret: bool = True, db: Session = None) -> dict:
|
||
"""格式化应用配置"""
|
||
# 获取关联的企微应用信息
|
||
wechat_app_info = None
|
||
if app.wechat_app_id and db:
|
||
from ..models.tenant_wechat_app import TenantWechatApp
|
||
wechat_app = db.query(TenantWechatApp).filter(TenantWechatApp.id == app.wechat_app_id).first()
|
||
if wechat_app:
|
||
wechat_app_info = {
|
||
"id": wechat_app.id,
|
||
"name": wechat_app.name,
|
||
"corp_id": wechat_app.corp_id,
|
||
"agent_id": wechat_app.agent_id
|
||
}
|
||
|
||
result = {
|
||
"id": app.id,
|
||
"tenant_id": app.tenant_id,
|
||
"app_code": app.app_code,
|
||
"app_name": app.app_name,
|
||
"wechat_app_id": app.wechat_app_id,
|
||
"wechat_app": wechat_app_info,
|
||
"access_token": "******" if mask_secret and app.access_token else app.access_token,
|
||
"allowed_origins": json.loads(app.allowed_origins) if app.allowed_origins else [],
|
||
"allowed_tools": json.loads(app.allowed_tools) if app.allowed_tools else [],
|
||
"custom_configs": json.loads(app.custom_configs) if app.custom_configs else [],
|
||
"status": app.status,
|
||
"created_at": app.created_at,
|
||
"updated_at": app.updated_at
|
||
}
|
||
return result
|