Phase 7 Task 4: Add collaboration API endpoints and schema
- Add collaboration_manager import and COLLABORATION_AVAILABLE flag - Add get_collab_manager() singleton function - Add collaboration API endpoints: - Project share links (create, list, verify, access, revoke) - Comments (add, get, update, resolve, delete) - Change history (get, stats, versions, revert) - Team members (invite, list, update role, remove, check permissions) - Add collaboration tables to schema.sql: - project_shares, comments, change_history, team_members - Related indexes for performance
This commit is contained in:
523
backend/main.py
523
backend/main.py
@@ -167,6 +167,14 @@ except ImportError as e:
|
|||||||
print(f"Security Manager import error: {e}")
|
print(f"Security Manager import error: {e}")
|
||||||
SECURITY_MANAGER_AVAILABLE = False
|
SECURITY_MANAGER_AVAILABLE = False
|
||||||
|
|
||||||
|
# Phase 7 Task 4: Collaboration Manager
|
||||||
|
try:
|
||||||
|
from collaboration_manager import get_collaboration_manager, CollaborationManager
|
||||||
|
COLLABORATION_AVAILABLE = True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Collaboration Manager import error: {e}")
|
||||||
|
COLLABORATION_AVAILABLE = False
|
||||||
|
|
||||||
# FastAPI app with enhanced metadata for Swagger
|
# FastAPI app with enhanced metadata for Swagger
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="InsightFlow API",
|
title="InsightFlow API",
|
||||||
@@ -718,6 +726,15 @@ def get_doc_processor():
|
|||||||
_doc_processor = DocumentProcessor()
|
_doc_processor = DocumentProcessor()
|
||||||
return _doc_processor
|
return _doc_processor
|
||||||
|
|
||||||
|
# Phase 7 Task 4: Collaboration Manager singleton
|
||||||
|
_collaboration_manager = None
|
||||||
|
def get_collab_manager():
|
||||||
|
global _collaboration_manager
|
||||||
|
if _collaboration_manager is None and COLLABORATION_AVAILABLE:
|
||||||
|
db = get_db_manager() if DB_AVAILABLE else None
|
||||||
|
_collaboration_manager = get_collaboration_manager(db)
|
||||||
|
return _collaboration_manager
|
||||||
|
|
||||||
# Phase 2: Entity Edit API
|
# Phase 2: Entity Edit API
|
||||||
@app.put("/api/v1/entities/{entity_id}", tags=["Entities"])
|
@app.put("/api/v1/entities/{entity_id}", tags=["Entities"])
|
||||||
async def update_entity(entity_id: str, update: EntityUpdate, _=Depends(verify_api_key)):
|
async def update_entity(entity_id: str, update: EntityUpdate, _=Depends(verify_api_key)):
|
||||||
@@ -7011,6 +7028,512 @@ async def reject_access_request(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Phase 7 Task 4: 协作与共享 API
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# ----- 请求模型 -----
|
||||||
|
|
||||||
|
class ShareLinkCreate(BaseModel):
|
||||||
|
permission: str = "read_only" # read_only, comment, edit, admin
|
||||||
|
expires_in_days: Optional[int] = None
|
||||||
|
max_uses: Optional[int] = None
|
||||||
|
password: Optional[str] = None
|
||||||
|
allow_download: bool = False
|
||||||
|
allow_export: bool = False
|
||||||
|
|
||||||
|
class ShareLinkVerify(BaseModel):
|
||||||
|
token: str
|
||||||
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
class CommentCreate(BaseModel):
|
||||||
|
target_type: str # entity, relation, transcript, project
|
||||||
|
target_id: str
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
content: str
|
||||||
|
mentions: Optional[List[str]] = None
|
||||||
|
|
||||||
|
class CommentUpdate(BaseModel):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
class CommentResolve(BaseModel):
|
||||||
|
resolved: bool
|
||||||
|
|
||||||
|
class TeamMemberInvite(BaseModel):
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
user_email: str
|
||||||
|
role: str = "viewer" # owner, admin, editor, viewer, commenter
|
||||||
|
|
||||||
|
class TeamMemberRoleUpdate(BaseModel):
|
||||||
|
role: str
|
||||||
|
|
||||||
|
|
||||||
|
# ----- 项目分享 -----
|
||||||
|
|
||||||
|
@app.post("/api/v1/projects/{project_id}/shares")
|
||||||
|
async def create_share_link(project_id: str, request: ShareLinkCreate, created_by: str = "current_user"):
|
||||||
|
"""创建项目分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
share = manager.create_share_link(
|
||||||
|
project_id=project_id,
|
||||||
|
created_by=created_by,
|
||||||
|
permission=request.permission,
|
||||||
|
expires_in_days=request.expires_in_days,
|
||||||
|
max_uses=request.max_uses,
|
||||||
|
password=request.password,
|
||||||
|
allow_download=request.allow_download,
|
||||||
|
allow_export=request.allow_export
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": share.id,
|
||||||
|
"token": share.token,
|
||||||
|
"permission": share.permission,
|
||||||
|
"created_at": share.created_at,
|
||||||
|
"expires_at": share.expires_at,
|
||||||
|
"max_uses": share.max_uses,
|
||||||
|
"share_url": f"/share/{share.token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/shares")
|
||||||
|
async def list_project_shares(project_id: str):
|
||||||
|
"""列出项目的所有分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
shares = manager.list_project_shares(project_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"id": s.id,
|
||||||
|
"token": s.token,
|
||||||
|
"permission": s.permission,
|
||||||
|
"created_at": s.created_at,
|
||||||
|
"expires_at": s.expires_at,
|
||||||
|
"use_count": s.use_count,
|
||||||
|
"max_uses": s.max_uses,
|
||||||
|
"is_active": s.is_active,
|
||||||
|
"has_password": s.password_hash is not None,
|
||||||
|
"allow_download": s.allow_download,
|
||||||
|
"allow_export": s.allow_export
|
||||||
|
}
|
||||||
|
for s in shares
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/shares/verify")
|
||||||
|
async def verify_share_link(request: ShareLinkVerify):
|
||||||
|
"""验证分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
share = manager.validate_share_token(request.token, request.password)
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid or expired share link")
|
||||||
|
|
||||||
|
# 增加使用次数
|
||||||
|
manager.increment_share_usage(request.token)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"valid": True,
|
||||||
|
"project_id": share.project_id,
|
||||||
|
"permission": share.permission,
|
||||||
|
"allow_download": share.allow_download,
|
||||||
|
"allow_export": share.allow_export
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/shares/{token}/access")
|
||||||
|
async def access_shared_project(token: str, password: Optional[str] = None):
|
||||||
|
"""通过分享链接访问项目"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
share = manager.validate_share_token(token, password)
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid or expired share link")
|
||||||
|
|
||||||
|
# 增加使用次数
|
||||||
|
manager.increment_share_usage(token)
|
||||||
|
|
||||||
|
# 获取项目信息
|
||||||
|
if not DB_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Database not available")
|
||||||
|
|
||||||
|
db = get_db_manager()
|
||||||
|
project = db.get_project(share.project_id)
|
||||||
|
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"project": {
|
||||||
|
"id": project.id,
|
||||||
|
"name": project.name,
|
||||||
|
"description": project.description,
|
||||||
|
"created_at": project.created_at
|
||||||
|
},
|
||||||
|
"permission": share.permission,
|
||||||
|
"allow_download": share.allow_download,
|
||||||
|
"allow_export": share.allow_export
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.delete("/api/v1/shares/{share_id}")
|
||||||
|
async def revoke_share_link(share_id: str, revoked_by: str = "current_user"):
|
||||||
|
"""撤销分享链接"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.revoke_share_link(share_id, revoked_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Share link not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Share link revoked"}
|
||||||
|
|
||||||
|
# ----- 评论和批注 -----
|
||||||
|
|
||||||
|
@app.post("/api/v1/projects/{project_id}/comments")
|
||||||
|
async def add_comment(project_id: str, request: CommentCreate, author: str = "current_user", author_name: str = "User"):
|
||||||
|
"""添加评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comment = manager.add_comment(
|
||||||
|
project_id=project_id,
|
||||||
|
target_type=request.target_type,
|
||||||
|
target_id=request.target_id,
|
||||||
|
author=author,
|
||||||
|
author_name=author_name,
|
||||||
|
content=request.content,
|
||||||
|
parent_id=request.parent_id,
|
||||||
|
mentions=request.mentions
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": comment.id,
|
||||||
|
"target_type": comment.target_type,
|
||||||
|
"target_id": comment.target_id,
|
||||||
|
"parent_id": comment.parent_id,
|
||||||
|
"author": comment.author,
|
||||||
|
"author_name": comment.author_name,
|
||||||
|
"content": comment.content,
|
||||||
|
"created_at": comment.created_at,
|
||||||
|
"resolved": comment.resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/{target_type}/{target_id}/comments")
|
||||||
|
async def get_comments(target_type: str, target_id: str, include_resolved: bool = True):
|
||||||
|
"""获取评论列表"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comments = manager.get_comments(target_type, target_id, include_resolved)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(comments),
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": c.id,
|
||||||
|
"parent_id": c.parent_id,
|
||||||
|
"author": c.author,
|
||||||
|
"author_name": c.author_name,
|
||||||
|
"content": c.content,
|
||||||
|
"created_at": c.created_at,
|
||||||
|
"updated_at": c.updated_at,
|
||||||
|
"resolved": c.resolved,
|
||||||
|
"resolved_by": c.resolved_by,
|
||||||
|
"resolved_at": c.resolved_at
|
||||||
|
}
|
||||||
|
for c in comments
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/comments")
|
||||||
|
async def get_project_comments(project_id: str, limit: int = 50, offset: int = 0):
|
||||||
|
"""获取项目下的所有评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comments = manager.get_project_comments(project_id, limit, offset)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(comments),
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": c.id,
|
||||||
|
"target_type": c.target_type,
|
||||||
|
"target_id": c.target_id,
|
||||||
|
"parent_id": c.parent_id,
|
||||||
|
"author": c.author,
|
||||||
|
"author_name": c.author_name,
|
||||||
|
"content": c.content,
|
||||||
|
"created_at": c.created_at,
|
||||||
|
"resolved": c.resolved
|
||||||
|
}
|
||||||
|
for c in comments
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.put("/api/v1/comments/{comment_id}")
|
||||||
|
async def update_comment(comment_id: str, request: CommentUpdate, updated_by: str = "current_user"):
|
||||||
|
"""更新评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
comment = manager.update_comment(comment_id, request.content, updated_by)
|
||||||
|
|
||||||
|
if not comment:
|
||||||
|
raise HTTPException(status_code=404, detail="Comment not found or not authorized")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": comment.id,
|
||||||
|
"content": comment.content,
|
||||||
|
"updated_at": comment.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/comments/{comment_id}/resolve")
|
||||||
|
async def resolve_comment(comment_id: str, resolved_by: str = "current_user"):
|
||||||
|
"""标记评论为已解决"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.resolve_comment(comment_id, resolved_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Comment not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Comment resolved"}
|
||||||
|
|
||||||
|
@app.delete("/api/v1/comments/{comment_id}")
|
||||||
|
async def delete_comment(comment_id: str, deleted_by: str = "current_user"):
|
||||||
|
"""删除评论"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.delete_comment(comment_id, deleted_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Comment not found or not authorized")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Comment deleted"}
|
||||||
|
|
||||||
|
# ----- 变更历史 -----
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/history")
|
||||||
|
async def get_change_history(
|
||||||
|
project_id: str,
|
||||||
|
entity_type: Optional[str] = None,
|
||||||
|
entity_id: Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0
|
||||||
|
):
|
||||||
|
"""获取变更历史"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
records = manager.get_change_history(project_id, entity_type, entity_id, limit, offset)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(records),
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"id": r.id,
|
||||||
|
"change_type": r.change_type,
|
||||||
|
"entity_type": r.entity_type,
|
||||||
|
"entity_id": r.entity_id,
|
||||||
|
"entity_name": r.entity_name,
|
||||||
|
"changed_by": r.changed_by,
|
||||||
|
"changed_by_name": r.changed_by_name,
|
||||||
|
"changed_at": r.changed_at,
|
||||||
|
"old_value": r.old_value,
|
||||||
|
"new_value": r.new_value,
|
||||||
|
"description": r.description,
|
||||||
|
"reverted": r.reverted
|
||||||
|
}
|
||||||
|
for r in records
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/history/stats")
|
||||||
|
async def get_change_history_stats(project_id: str):
|
||||||
|
"""获取变更统计"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
stats = manager.get_change_stats(project_id)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@app.get("/api/v1/{entity_type}/{entity_id}/versions")
|
||||||
|
async def get_entity_versions(entity_type: str, entity_id: str):
|
||||||
|
"""获取实体版本历史"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
records = manager.get_entity_version_history(entity_type, entity_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(records),
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"id": r.id,
|
||||||
|
"change_type": r.change_type,
|
||||||
|
"changed_by": r.changed_by,
|
||||||
|
"changed_by_name": r.changed_by_name,
|
||||||
|
"changed_at": r.changed_at,
|
||||||
|
"old_value": r.old_value,
|
||||||
|
"new_value": r.new_value,
|
||||||
|
"description": r.description
|
||||||
|
}
|
||||||
|
for r in records
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/v1/history/{record_id}/revert")
|
||||||
|
async def revert_change(record_id: str, reverted_by: str = "current_user"):
|
||||||
|
"""回滚变更"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.revert_change(record_id, reverted_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Change record not found or already reverted")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Change reverted"}
|
||||||
|
|
||||||
|
# ----- 团队成员 -----
|
||||||
|
|
||||||
|
@app.post("/api/v1/projects/{project_id}/members")
|
||||||
|
async def invite_team_member(project_id: str, request: TeamMemberInvite, invited_by: str = "current_user"):
|
||||||
|
"""邀请团队成员"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
member = manager.add_team_member(
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=request.user_id,
|
||||||
|
user_name=request.user_name,
|
||||||
|
user_email=request.user_email,
|
||||||
|
role=request.role,
|
||||||
|
invited_by=invited_by
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": member.id,
|
||||||
|
"user_id": member.user_id,
|
||||||
|
"user_name": member.user_name,
|
||||||
|
"user_email": member.user_email,
|
||||||
|
"role": member.role,
|
||||||
|
"joined_at": member.joined_at,
|
||||||
|
"permissions": member.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/members")
|
||||||
|
async def list_team_members(project_id: str):
|
||||||
|
"""列出团队成员"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
members = manager.get_team_members(project_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"count": len(members),
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": m.id,
|
||||||
|
"user_id": m.user_id,
|
||||||
|
"user_name": m.user_name,
|
||||||
|
"user_email": m.user_email,
|
||||||
|
"role": m.role,
|
||||||
|
"joined_at": m.joined_at,
|
||||||
|
"last_active_at": m.last_active_at,
|
||||||
|
"permissions": m.permissions
|
||||||
|
}
|
||||||
|
for m in members
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.put("/api/v1/members/{member_id}/role")
|
||||||
|
async def update_member_role(member_id: str, request: TeamMemberRoleUpdate, updated_by: str = "current_user"):
|
||||||
|
"""更新成员角色"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.update_member_role(member_id, request.role, updated_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Member role updated"}
|
||||||
|
|
||||||
|
@app.delete("/api/v1/members/{member_id}")
|
||||||
|
async def remove_team_member(member_id: str, removed_by: str = "current_user"):
|
||||||
|
"""移除团队成员"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
success = manager.remove_team_member(member_id, removed_by)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Member not found")
|
||||||
|
|
||||||
|
return {"success": True, "message": "Member removed"}
|
||||||
|
|
||||||
|
@app.get("/api/v1/projects/{project_id}/permissions")
|
||||||
|
async def check_project_permissions(project_id: str, user_id: str = "current_user"):
|
||||||
|
"""检查用户权限"""
|
||||||
|
if not COLLABORATION_AVAILABLE:
|
||||||
|
raise HTTPException(status_code=503, detail="Collaboration module not available")
|
||||||
|
|
||||||
|
manager = get_collab_manager()
|
||||||
|
members = manager.get_team_members(project_id)
|
||||||
|
|
||||||
|
user_member = None
|
||||||
|
for m in members:
|
||||||
|
if m.user_id == user_id:
|
||||||
|
user_member = m
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_member:
|
||||||
|
return {
|
||||||
|
"has_access": False,
|
||||||
|
"role": None,
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"has_access": True,
|
||||||
|
"role": user_member.role,
|
||||||
|
"permissions": user_member.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Serve frontend - MUST be last to not override API routes
|
# Serve frontend - MUST be last to not override API routes
|
||||||
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
|
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
|
||||||
|
|
||||||
|
|||||||
@@ -633,3 +633,95 @@ CREATE INDEX IF NOT EXISTS idx_masking_project ON masking_rules(project_id);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_access_policy_project ON data_access_policies(project_id);
|
CREATE INDEX IF NOT EXISTS idx_access_policy_project ON data_access_policies(project_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_access_requests_policy ON access_requests(policy_id);
|
CREATE INDEX IF NOT EXISTS idx_access_requests_policy ON access_requests(policy_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_access_requests_user ON access_requests(user_id);
|
CREATE INDEX IF NOT EXISTS idx_access_requests_user ON access_requests(user_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Phase 7 Task 4: 协作与共享
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 项目分享表
|
||||||
|
CREATE TABLE IF NOT EXISTS project_shares (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
token TEXT NOT NULL UNIQUE, -- 分享令牌
|
||||||
|
permission TEXT DEFAULT 'read_only', -- 权限级别: read_only, comment, edit, admin
|
||||||
|
created_by TEXT NOT NULL, -- 创建者
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP, -- 过期时间
|
||||||
|
max_uses INTEGER, -- 最大使用次数
|
||||||
|
use_count INTEGER DEFAULT 0, -- 已使用次数
|
||||||
|
password_hash TEXT, -- 密码保护(哈希)
|
||||||
|
is_active BOOLEAN DEFAULT 1, -- 是否激活
|
||||||
|
allow_download BOOLEAN DEFAULT 0, -- 允许下载
|
||||||
|
allow_export BOOLEAN DEFAULT 0, -- 允许导出
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 评论表
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
target_type TEXT NOT NULL, -- 目标类型: entity, relation, transcript, project
|
||||||
|
target_id TEXT NOT NULL, -- 目标ID
|
||||||
|
parent_id TEXT, -- 父评论ID(支持回复)
|
||||||
|
author TEXT NOT NULL, -- 作者ID
|
||||||
|
author_name TEXT, -- 作者显示名
|
||||||
|
content TEXT NOT NULL, -- 评论内容
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
resolved BOOLEAN DEFAULT 0, -- 是否已解决
|
||||||
|
resolved_by TEXT, -- 解决者
|
||||||
|
resolved_at TIMESTAMP, -- 解决时间
|
||||||
|
mentions TEXT, -- JSON数组: 提及的用户
|
||||||
|
attachments TEXT, -- JSON数组: 附件
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 变更历史表
|
||||||
|
CREATE TABLE IF NOT EXISTS change_history (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
change_type TEXT NOT NULL, -- 变更类型: create, update, delete, merge, split
|
||||||
|
entity_type TEXT NOT NULL, -- 实体类型: entity, relation, transcript, project
|
||||||
|
entity_id TEXT NOT NULL, -- 实体ID
|
||||||
|
entity_name TEXT, -- 实体名称(用于显示)
|
||||||
|
changed_by TEXT NOT NULL, -- 变更者ID
|
||||||
|
changed_by_name TEXT, -- 变更者显示名
|
||||||
|
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
old_value TEXT, -- JSON: 旧值
|
||||||
|
new_value TEXT, -- JSON: 新值
|
||||||
|
description TEXT, -- 变更描述
|
||||||
|
session_id TEXT, -- 会话ID(批量变更关联)
|
||||||
|
reverted BOOLEAN DEFAULT 0, -- 是否已回滚
|
||||||
|
reverted_at TIMESTAMP, -- 回滚时间
|
||||||
|
reverted_by TEXT, -- 回滚者
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 团队成员表
|
||||||
|
CREATE TABLE IF NOT EXISTS team_members (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL, -- 用户ID
|
||||||
|
user_name TEXT, -- 用户名
|
||||||
|
user_email TEXT, -- 用户邮箱
|
||||||
|
role TEXT DEFAULT 'viewer', -- 角色: owner, admin, editor, viewer, commenter
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
invited_by TEXT, -- 邀请者
|
||||||
|
last_active_at TIMESTAMP, -- 最后活跃时间
|
||||||
|
permissions TEXT, -- JSON数组: 具体权限列表
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(project_id, user_id) -- 每个项目每个用户只能有一条记录
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 协作与共享相关索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_shares_project ON project_shares(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_shares_token ON project_shares(token);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_comments_project ON comments(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_comments_target ON comments(target_type, target_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_history_project ON change_history(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_history_entity ON change_history(entity_type, entity_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_history_session ON change_history(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_team_members_project ON team_members(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
|
||||||
|
|||||||
Reference in New Issue
Block a user