- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
394 lines
12 KiB
Python
394 lines
12 KiB
Python
"""
|
|
工作区 (Workspace) API — 多租户管理
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel, Field
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime
|
|
import uuid
|
|
import logging
|
|
|
|
from app.core.database import get_db
|
|
from app.api.auth import get_current_user
|
|
from app.models.user import User
|
|
from app.models.workspace import Workspace, WorkspaceMembership
|
|
from app.services.workspace_service import check_workspace_access, get_user_workspaces
|
|
from app.core.exceptions import NotFoundError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/api/v1/workspaces",
|
|
tags=["workspaces"],
|
|
responses={
|
|
401: {"description": "未授权"},
|
|
403: {"description": "无权访问"},
|
|
404: {"description": "资源不存在"},
|
|
}
|
|
)
|
|
|
|
|
|
# ── Pydantic Schemas ──
|
|
|
|
class WorkspaceCreate(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=100, description="工作区名称")
|
|
description: Optional[str] = Field(None, max_length=1000)
|
|
max_members: int = Field(default=50, ge=1, le=500)
|
|
|
|
|
|
class WorkspaceUpdate(BaseModel):
|
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
description: Optional[str] = Field(None, max_length=1000)
|
|
max_members: Optional[int] = Field(None, ge=1, le=500)
|
|
status: Optional[str] = None # active/disabled
|
|
|
|
|
|
class MemberAddRequest(BaseModel):
|
|
user_id: Optional[str] = None
|
|
username: Optional[str] = None
|
|
role: str = Field(default="member", pattern="^(admin|member)$")
|
|
|
|
|
|
class MemberUpdateRequest(BaseModel):
|
|
role: str = Field(..., pattern="^(admin|member)$")
|
|
|
|
|
|
class WorkspaceResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: Optional[str]
|
|
is_default: bool
|
|
owner_id: str
|
|
max_members: int
|
|
settings: Optional[Dict[str, Any]]
|
|
member_count: int = 0
|
|
status: str
|
|
created_at: Optional[str]
|
|
updated_at: Optional[str]
|
|
|
|
|
|
class MemberResponse(BaseModel):
|
|
id: str
|
|
user_id: str
|
|
username: str
|
|
email: str
|
|
role: str
|
|
joined_at: Optional[str]
|
|
|
|
|
|
# ── Endpoints ──
|
|
|
|
@router.get("", response_model=List[Dict[str, Any]])
|
|
def list_workspaces(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取当前用户的工作区列表。"""
|
|
return get_user_workspaces(db, current_user)
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
def create_workspace(
|
|
data: WorkspaceCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""创建新工作区。"""
|
|
ws = Workspace(
|
|
id=str(uuid.uuid4()),
|
|
name=data.name,
|
|
description=data.description,
|
|
owner_id=current_user.id,
|
|
max_members=data.max_members,
|
|
status="active",
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow(),
|
|
)
|
|
db.add(ws)
|
|
db.flush()
|
|
|
|
# 创建者自动成为工作区管理员
|
|
membership = WorkspaceMembership(
|
|
id=str(uuid.uuid4()),
|
|
workspace_id=ws.id,
|
|
user_id=current_user.id,
|
|
role="admin",
|
|
joined_at=datetime.utcnow(),
|
|
)
|
|
db.add(membership)
|
|
db.commit()
|
|
|
|
return {
|
|
"id": ws.id,
|
|
"name": ws.name,
|
|
"description": ws.description,
|
|
"is_default": bool(ws.is_default),
|
|
"owner_id": ws.owner_id,
|
|
"max_members": ws.max_members,
|
|
"role": "admin",
|
|
"member_count": 1,
|
|
"status": ws.status,
|
|
}
|
|
|
|
|
|
@router.get("/{workspace_id}")
|
|
def get_workspace(
|
|
workspace_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取工作区详情。"""
|
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
|
if not ws:
|
|
raise NotFoundError("工作区", workspace_id)
|
|
|
|
if not check_workspace_access(db, current_user, workspace_id):
|
|
raise HTTPException(status_code=403, detail="无权访问此工作区")
|
|
|
|
member_count = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(WorkspaceMembership.workspace_id == workspace_id)
|
|
.count()
|
|
)
|
|
|
|
user_role = "admin" if current_user.role == "admin" else None
|
|
if not user_role:
|
|
membership = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(
|
|
WorkspaceMembership.workspace_id == workspace_id,
|
|
WorkspaceMembership.user_id == current_user.id,
|
|
)
|
|
.first()
|
|
)
|
|
if membership:
|
|
user_role = membership.role
|
|
|
|
return {
|
|
**ws.to_dict(),
|
|
"member_count": member_count,
|
|
"role": user_role,
|
|
}
|
|
|
|
|
|
@router.put("/{workspace_id}")
|
|
def update_workspace(
|
|
workspace_id: str,
|
|
data: WorkspaceUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""更新工作区设置(需工作区管理员权限)。"""
|
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
|
if not ws:
|
|
raise NotFoundError("工作区", workspace_id)
|
|
|
|
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
|
|
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
|
|
|
|
if data.name is not None:
|
|
ws.name = data.name
|
|
if data.description is not None:
|
|
ws.description = data.description
|
|
if data.max_members is not None:
|
|
ws.max_members = data.max_members
|
|
if data.status is not None:
|
|
ws.status = data.status
|
|
ws.updated_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
return {**ws.to_dict()}
|
|
|
|
|
|
@router.delete("/{workspace_id}")
|
|
def delete_workspace(
|
|
workspace_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""删除工作区(软删除,仅平台管理员或工作区管理员可操作)。"""
|
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
|
if not ws:
|
|
raise NotFoundError("工作区", workspace_id)
|
|
|
|
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
|
|
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
|
|
|
|
if ws.is_default and current_user.role != "admin":
|
|
raise HTTPException(status_code=403, detail="默认工作区不可删除")
|
|
|
|
ws.status = "deleted"
|
|
ws.updated_at = datetime.utcnow()
|
|
db.commit()
|
|
|
|
return {"message": "工作区已删除"}
|
|
|
|
|
|
# ── 成员管理 ──
|
|
|
|
@router.get("/{workspace_id}/members")
|
|
def list_members(
|
|
workspace_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取工作区成员列表。"""
|
|
if not check_workspace_access(db, current_user, workspace_id):
|
|
raise HTTPException(status_code=403, detail="无权访问此工作区")
|
|
|
|
memberships = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(WorkspaceMembership.workspace_id == workspace_id)
|
|
.all()
|
|
)
|
|
|
|
result = []
|
|
for m in memberships:
|
|
user = m.user
|
|
result.append({
|
|
"id": m.id,
|
|
"user_id": user.id,
|
|
"username": user.username,
|
|
"email": user.email,
|
|
"role": m.role,
|
|
"joined_at": m.joined_at.isoformat() if m.joined_at else None,
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
@router.post("/{workspace_id}/members", status_code=status.HTTP_201_CREATED)
|
|
def add_member(
|
|
workspace_id: str,
|
|
data: MemberAddRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""添加工作区成员(需工作区管理员权限)。"""
|
|
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
|
|
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
|
|
|
|
# 查找目标用户
|
|
target_user = None
|
|
if data.user_id:
|
|
target_user = db.query(User).filter(User.id == data.user_id).first()
|
|
elif data.username:
|
|
target_user = db.query(User).filter(User.username == data.username).first()
|
|
|
|
if not target_user:
|
|
raise NotFoundError("用户")
|
|
|
|
# 检查是否已存在
|
|
existing = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(
|
|
WorkspaceMembership.workspace_id == workspace_id,
|
|
WorkspaceMembership.user_id == target_user.id,
|
|
)
|
|
.first()
|
|
)
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="该用户已是工作区成员")
|
|
|
|
# 检查成员数上限
|
|
member_count = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(WorkspaceMembership.workspace_id == workspace_id)
|
|
.count()
|
|
)
|
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
|
if member_count >= ws.max_members:
|
|
raise HTTPException(status_code=400, detail="工作区成员数已达上限")
|
|
|
|
membership = WorkspaceMembership(
|
|
id=str(uuid.uuid4()),
|
|
workspace_id=workspace_id,
|
|
user_id=target_user.id,
|
|
role=data.role,
|
|
joined_at=datetime.utcnow(),
|
|
)
|
|
db.add(membership)
|
|
db.commit()
|
|
|
|
return {
|
|
"id": membership.id,
|
|
"user_id": target_user.id,
|
|
"username": target_user.username,
|
|
"email": target_user.email,
|
|
"role": membership.role,
|
|
"joined_at": membership.joined_at.isoformat() if membership.joined_at else None,
|
|
}
|
|
|
|
|
|
@router.put("/{workspace_id}/members/{user_id}")
|
|
def update_member_role(
|
|
workspace_id: str,
|
|
user_id: str,
|
|
data: MemberUpdateRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""修改成员角色(需工作区管理员权限)。"""
|
|
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
|
|
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
|
|
|
|
membership = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(
|
|
WorkspaceMembership.workspace_id == workspace_id,
|
|
WorkspaceMembership.user_id == user_id,
|
|
)
|
|
.first()
|
|
)
|
|
if not membership:
|
|
raise NotFoundError("成员", user_id)
|
|
|
|
# 不能修改自己的工作区所有者
|
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
|
if ws.owner_id == user_id and data.role != "admin":
|
|
raise HTTPException(status_code=400, detail="工作区所有者必须保持admin角色")
|
|
|
|
membership.role = data.role
|
|
db.commit()
|
|
|
|
return {"message": "角色已更新"}
|
|
|
|
|
|
@router.delete("/{workspace_id}/members/{user_id}")
|
|
def remove_member(
|
|
workspace_id: str,
|
|
user_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""移除工作区成员(需工作区管理员权限,或自行退出)。"""
|
|
is_self = user_id == current_user.id
|
|
is_admin = check_workspace_access(db, current_user, workspace_id, required_role="admin")
|
|
|
|
if not is_self and not is_admin:
|
|
raise HTTPException(status_code=403, detail="无权移除此成员")
|
|
|
|
# 不能移除工作区所有者
|
|
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
|
if not ws:
|
|
raise NotFoundError("工作区", workspace_id)
|
|
if ws.owner_id == user_id and not is_self:
|
|
raise HTTPException(status_code=400, detail="不能移除工作区所有者")
|
|
|
|
membership = (
|
|
db.query(WorkspaceMembership)
|
|
.filter(
|
|
WorkspaceMembership.workspace_id == workspace_id,
|
|
WorkspaceMembership.user_id == user_id,
|
|
)
|
|
.first()
|
|
)
|
|
if not membership:
|
|
raise NotFoundError("成员", user_id)
|
|
|
|
db.delete(membership)
|
|
db.commit()
|
|
|
|
return {"message": "成员已移除"}
|