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:
OpenClaw Bot
2026-02-24 00:17:13 +08:00
parent 243f41de8f
commit 7a2dc5f810
2 changed files with 615 additions and 0 deletions

View File

@@ -167,6 +167,14 @@ except ImportError as e:
print(f"Security Manager import error: {e}")
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
app = FastAPI(
title="InsightFlow API",
@@ -718,6 +726,15 @@ def get_doc_processor():
_doc_processor = DocumentProcessor()
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
@app.put("/api/v1/entities/{entity_id}", tags=["Entities"])
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
app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")

View File

@@ -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_requests_policy ON access_requests(policy_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);