fix: delete agent 500 error + dynamic personality + deployment guide

- 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>
This commit is contained in:
2026-06-29 01:17:21 +08:00
parent 86b98865e3
commit beff3fac8d
1084 changed files with 117315 additions and 1281 deletions

View File

@@ -0,0 +1,393 @@
"""
工作区 (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": "成员已移除"}