feat(app): add cross-workspace app sharing backend
This commit is contained in:
@@ -60,7 +60,12 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = postgresql://user:password@localhost/dbname
|
# Database connection URL - DO NOT hardcode credentials here!
|
||||||
|
# Connection string is set dynamically from environment variables in migrations/env.py
|
||||||
|
# Required env vars: DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME
|
||||||
|
# Example: postgresql://user:password@localhost:5432/dbname
|
||||||
|
; sqlalchemy.url = postgresql://user:password@host:port/dbname
|
||||||
|
sqlalchemy.url = driver://user:password@host:port/dbname
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|||||||
@@ -93,6 +93,20 @@ def list_apps(
|
|||||||
return success(data=PageData(page=meta, items=items))
|
return success(data=PageData(page=meta, items=items))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/my-shared-out", summary="列出本工作空间主动分享出去的记录")
|
||||||
|
@cur_workspace_access_guard()
|
||||||
|
def list_my_shared_out(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""列出本工作空间主动分享给其他工作空间的所有记录(我的共享)"""
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
service = app_service.AppService(db)
|
||||||
|
shares = service.list_my_shared_out(workspace_id=workspace_id)
|
||||||
|
data = [app_schema.AppShare.model_validate(s) for s in shares]
|
||||||
|
return success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{app_id}", summary="获取应用详情")
|
@router.get("/{app_id}", summary="获取应用详情")
|
||||||
@cur_workspace_access_guard()
|
@cur_workspace_access_guard()
|
||||||
def get_app(
|
def get_app(
|
||||||
@@ -302,7 +316,8 @@ def share_app(
|
|||||||
app_id=app_id,
|
app_id=app_id,
|
||||||
target_workspace_ids=payload.target_workspace_ids,
|
target_workspace_ids=payload.target_workspace_ids,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
workspace_id=workspace_id
|
workspace_id=workspace_id,
|
||||||
|
permission=payload.permission
|
||||||
)
|
)
|
||||||
|
|
||||||
data = [app_schema.AppShare.model_validate(s) for s in shares]
|
data = [app_schema.AppShare.model_validate(s) for s in shares]
|
||||||
@@ -333,6 +348,32 @@ def unshare_app(
|
|||||||
return success(msg="应用分享已取消")
|
return success(msg="应用分享已取消")
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{app_id}/share/{target_workspace_id}", summary="更新共享权限")
|
||||||
|
@cur_workspace_access_guard()
|
||||||
|
def update_share_permission(
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
target_workspace_id: uuid.UUID,
|
||||||
|
payload: app_schema.UpdateSharePermissionRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""更新共享权限(readonly <-> editable)
|
||||||
|
|
||||||
|
- 只能修改自己工作空间应用的共享权限
|
||||||
|
"""
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
|
||||||
|
service = app_service.AppService(db)
|
||||||
|
share = service.update_share_permission(
|
||||||
|
app_id=app_id,
|
||||||
|
target_workspace_id=target_workspace_id,
|
||||||
|
permission=payload.permission,
|
||||||
|
workspace_id=workspace_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return success(data=app_schema.AppShare.model_validate(share))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{app_id}/shares", summary="列出应用的分享记录")
|
@router.get("/{app_id}/shares", summary="列出应用的分享记录")
|
||||||
@cur_workspace_access_guard()
|
@cur_workspace_access_guard()
|
||||||
def list_app_shares(
|
def list_app_shares(
|
||||||
@@ -356,6 +397,29 @@ def list_app_shares(
|
|||||||
return success(data=data)
|
return success(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{app_id}/shared", summary="移除共享给我的应用")
|
||||||
|
@cur_workspace_access_guard()
|
||||||
|
def remove_shared_app(
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""被共享者从自己的工作空间移除共享应用
|
||||||
|
|
||||||
|
- 不会删除源应用,只删除共享记录
|
||||||
|
- 只能移除共享给自己工作空间的应用
|
||||||
|
"""
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
|
||||||
|
service = app_service.AppService(db)
|
||||||
|
service.remove_shared_app(
|
||||||
|
app_id=app_id,
|
||||||
|
workspace_id=workspace_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return success(msg="已移除共享应用")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{app_id}/draft/run", summary="试运行 Agent(使用当前草稿配置)")
|
@router.post("/{app_id}/draft/run", summary="试运行 Agent(使用当前草稿配置)")
|
||||||
@cur_workspace_access_guard()
|
@cur_workspace_access_guard()
|
||||||
async def draft_run(
|
async def draft_run(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
from sqlalchemy import Column, DateTime, ForeignKey
|
from sqlalchemy import Column, DateTime, ForeignKey, String
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from app.db import Base
|
from app.db import Base
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
@@ -18,6 +18,7 @@ class AppShare(Base):
|
|||||||
source_workspace_id = Column(UUID(as_uuid=True), ForeignKey('workspaces.id'), nullable=False, comment="源工作空间ID")
|
source_workspace_id = Column(UUID(as_uuid=True), ForeignKey('workspaces.id'), nullable=False, comment="源工作空间ID")
|
||||||
target_workspace_id = Column(UUID(as_uuid=True), ForeignKey('workspaces.id'), nullable=False, comment="目标工作空间ID")
|
target_workspace_id = Column(UUID(as_uuid=True), ForeignKey('workspaces.id'), nullable=False, comment="目标工作空间ID")
|
||||||
shared_by = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False, comment="分享者用户ID")
|
shared_by = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False, comment="分享者用户ID")
|
||||||
|
permission = Column(String, default="readonly", nullable=False, comment="权限模式: readonly | editable")
|
||||||
created_at = Column(DateTime, default=datetime.datetime.now)
|
created_at = Column(DateTime, default=datetime.datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.datetime.now)
|
updated_at = Column(DateTime, default=datetime.datetime.now)
|
||||||
|
|
||||||
|
|||||||
@@ -277,7 +277,12 @@ class App(BaseModel):
|
|||||||
tags: List[str] = []
|
tags: List[str] = []
|
||||||
current_release_id: Optional[uuid.UUID] = None
|
current_release_id: Optional[uuid.UUID] = None
|
||||||
is_active: bool
|
is_active: bool
|
||||||
is_shared: bool = False # 是否是共享应用(从其他工作空间共享来的)
|
is_shared: bool = False
|
||||||
|
share_permission: Optional[str] = None
|
||||||
|
source_workspace_name: Optional[str] = None # 共享来源工作空间名称(仅共享应用有值)
|
||||||
|
source_workspace_icon: Optional[str] = None # 共享来源工作空间图标
|
||||||
|
source_app_version: Optional[str] = None # 应用版本号
|
||||||
|
source_app_is_active: Optional[bool] = None # 应用是否生效
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
updated_at: datetime.datetime
|
updated_at: datetime.datetime
|
||||||
|
|
||||||
@@ -422,6 +427,12 @@ class AppRelease(BaseModel):
|
|||||||
class AppShareCreate(BaseModel):
|
class AppShareCreate(BaseModel):
|
||||||
"""应用分享请求"""
|
"""应用分享请求"""
|
||||||
target_workspace_ids: List[uuid.UUID] = Field(..., description="目标工作空间ID列表")
|
target_workspace_ids: List[uuid.UUID] = Field(..., description="目标工作空间ID列表")
|
||||||
|
permission: str = Field(default="readonly", description="权限模式: readonly | editable")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSharePermissionRequest(BaseModel):
|
||||||
|
"""更新共享权限请求"""
|
||||||
|
permission: str = Field(..., description="新权限值: readonly | editable")
|
||||||
|
|
||||||
|
|
||||||
class AppShare(BaseModel):
|
class AppShare(BaseModel):
|
||||||
@@ -433,9 +444,32 @@ class AppShare(BaseModel):
|
|||||||
source_workspace_id: uuid.UUID
|
source_workspace_id: uuid.UUID
|
||||||
target_workspace_id: uuid.UUID
|
target_workspace_id: uuid.UUID
|
||||||
shared_by: uuid.UUID
|
shared_by: uuid.UUID
|
||||||
|
permission: str = "readonly"
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
updated_at: datetime.datetime
|
updated_at: datetime.datetime
|
||||||
|
|
||||||
|
# 关联名称(从 relationship 读取)
|
||||||
|
source_app_name: Optional[str] = None
|
||||||
|
source_app_type: Optional[str] = None
|
||||||
|
source_app_version: Optional[str] = None
|
||||||
|
source_app_is_active: Optional[bool] = None
|
||||||
|
target_workspace_name: Optional[str] = None
|
||||||
|
target_workspace_icon: Optional[str] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_validate(cls, obj, **kwargs):
|
||||||
|
instance = super().model_validate(obj, **kwargs)
|
||||||
|
if hasattr(obj, 'source_app') and obj.source_app:
|
||||||
|
instance.source_app_name = obj.source_app.name
|
||||||
|
instance.source_app_type = obj.source_app.type
|
||||||
|
instance.source_app_is_active = obj.source_app.is_active
|
||||||
|
release = obj.source_app.current_release
|
||||||
|
instance.source_app_version = release.version_name if release else None
|
||||||
|
if hasattr(obj, 'target_workspace') and obj.target_workspace:
|
||||||
|
instance.target_workspace_name = obj.target_workspace.name
|
||||||
|
instance.target_workspace_icon = obj.target_workspace.icon
|
||||||
|
return instance
|
||||||
|
|
||||||
@field_serializer("created_at", when_used="json")
|
@field_serializer("created_at", when_used="json")
|
||||||
def _serialize_created_at(self, dt: datetime.datetime):
|
def _serialize_created_at(self, dt: datetime.datetime):
|
||||||
return int(dt.timestamp() * 1000) if dt else None
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|||||||
@@ -125,6 +125,49 @@ class AppService:
|
|||||||
)
|
)
|
||||||
raise BusinessException("应用不可访问", BizCode.WORKSPACE_NO_ACCESS)
|
raise BusinessException("应用不可访问", BizCode.WORKSPACE_NO_ACCESS)
|
||||||
|
|
||||||
|
def _get_share_permission(self, app: App, workspace_id: Optional[uuid.UUID]) -> Optional[str]:
|
||||||
|
"""获取共享应用的权限
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: 不是共享应用(是本工作空间的应用)
|
||||||
|
'readonly': 只读共享
|
||||||
|
'editable': 可编辑共享
|
||||||
|
"""
|
||||||
|
from app.models import AppShare
|
||||||
|
|
||||||
|
if workspace_id is None or app.workspace_id == workspace_id:
|
||||||
|
return None # 本工作空间的应用,不是共享的
|
||||||
|
|
||||||
|
stmt = select(AppShare).where(
|
||||||
|
AppShare.source_app_id == app.id,
|
||||||
|
AppShare.target_workspace_id == workspace_id
|
||||||
|
)
|
||||||
|
share = self.db.scalars(stmt).first()
|
||||||
|
return share.permission if share else None
|
||||||
|
|
||||||
|
def _validate_app_writable(self, app: App, workspace_id: Optional[uuid.UUID]) -> None:
|
||||||
|
"""Validate that the app config is writable (owner only).
|
||||||
|
|
||||||
|
Shared apps (both readonly and editable) cannot modify config.
|
||||||
|
- Own workspace app: allowed
|
||||||
|
- Any shared app: denied
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BusinessException: when app is not writable
|
||||||
|
"""
|
||||||
|
if workspace_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Own workspace app, allow
|
||||||
|
if app.workspace_id == workspace_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"应用写操作被拒",
|
||||||
|
extra={"app_id": str(app.id), "workspace_id": str(workspace_id)}
|
||||||
|
)
|
||||||
|
raise BusinessException("共享应用不可修改配置", BizCode.WORKSPACE_NO_ACCESS)
|
||||||
|
|
||||||
def _get_app_or_404(self, app_id: uuid.UUID) -> App:
|
def _get_app_or_404(self, app_id: uuid.UUID) -> App:
|
||||||
"""获取应用或抛出404异常
|
"""获取应用或抛出404异常
|
||||||
|
|
||||||
@@ -454,6 +497,32 @@ class AppService:
|
|||||||
Returns:
|
Returns:
|
||||||
app_schema.App: 应用 Schema
|
app_schema.App: 应用 Schema
|
||||||
"""
|
"""
|
||||||
|
is_shared = app.workspace_id != current_workspace_id
|
||||||
|
share_permission = None
|
||||||
|
source_workspace_name = None
|
||||||
|
source_workspace_icon = None
|
||||||
|
source_app_version = None
|
||||||
|
source_app_is_active = None
|
||||||
|
|
||||||
|
if is_shared:
|
||||||
|
# 查询共享权限和来源工作空间名称
|
||||||
|
from app.models import AppShare
|
||||||
|
stmt = select(AppShare).where(
|
||||||
|
AppShare.source_app_id == app.id,
|
||||||
|
AppShare.target_workspace_id == current_workspace_id
|
||||||
|
)
|
||||||
|
share = self.db.scalars(stmt).first()
|
||||||
|
if share:
|
||||||
|
share_permission = share.permission
|
||||||
|
if share.source_workspace:
|
||||||
|
source_workspace_name = share.source_workspace.name
|
||||||
|
source_workspace_icon = share.source_workspace.icon
|
||||||
|
|
||||||
|
# 版本号和生效状态
|
||||||
|
if app.current_release:
|
||||||
|
source_app_version = app.current_release.version_name
|
||||||
|
source_app_is_active = app.is_active
|
||||||
|
|
||||||
app_dict = {
|
app_dict = {
|
||||||
"id": app.id,
|
"id": app.id,
|
||||||
"workspace_id": app.workspace_id,
|
"workspace_id": app.workspace_id,
|
||||||
@@ -468,7 +537,12 @@ class AppService:
|
|||||||
"tags": app.tags or [],
|
"tags": app.tags or [],
|
||||||
"current_release_id": app.current_release_id,
|
"current_release_id": app.current_release_id,
|
||||||
"is_active": app.is_active,
|
"is_active": app.is_active,
|
||||||
"is_shared": app.workspace_id != current_workspace_id, # 判断是否是共享应用
|
"is_shared": is_shared,
|
||||||
|
"share_permission": share_permission,
|
||||||
|
"source_workspace_name": source_workspace_name,
|
||||||
|
"source_workspace_icon": source_workspace_icon,
|
||||||
|
"source_app_version": source_app_version,
|
||||||
|
"source_app_is_active": source_app_is_active,
|
||||||
"created_at": app.created_at,
|
"created_at": app.created_at,
|
||||||
"updated_at": app.updated_at
|
"updated_at": app.updated_at
|
||||||
}
|
}
|
||||||
@@ -594,7 +668,7 @@ class AppService:
|
|||||||
logger.info("更新应用", extra={"app_id": str(app_id)})
|
logger.info("更新应用", extra={"app_id": str(app_id)})
|
||||||
|
|
||||||
app = self._get_app_or_404(app_id)
|
app = self._get_app_or_404(app_id)
|
||||||
self._validate_workspace_access(app, workspace_id)
|
self._validate_app_writable(app, workspace_id)
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
for field in ["name", "description", "icon", "icon_type", "visibility", "status", "tags"]:
|
for field in ["name", "description", "icon", "icon_type", "visibility", "status", "tags"]:
|
||||||
@@ -952,7 +1026,7 @@ class AppService:
|
|||||||
if app.type != "agent":
|
if app.type != "agent":
|
||||||
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||||||
|
|
||||||
self._validate_workspace_access(app, workspace_id)
|
self._validate_app_writable(app, workspace_id)
|
||||||
|
|
||||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active.is_(True)).order_by(
|
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active.is_(True)).order_by(
|
||||||
AgentConfig.updated_at.desc())
|
AgentConfig.updated_at.desc())
|
||||||
@@ -1163,7 +1237,7 @@ class AppService:
|
|||||||
if app.type != AppType.WORKFLOW:
|
if app.type != AppType.WORKFLOW:
|
||||||
raise BusinessException("只有 Workflow 类型应用支持 Workflow 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
raise BusinessException("只有 Workflow 类型应用支持 Workflow 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||||||
|
|
||||||
self._validate_workspace_access(app, workspace_id)
|
self._validate_app_writable(app, workspace_id)
|
||||||
|
|
||||||
# 获取现有配置
|
# 获取现有配置
|
||||||
repo = WorkflowConfigRepository(self.db)
|
repo = WorkflowConfigRepository(self.db)
|
||||||
@@ -1654,7 +1728,8 @@ class AppService:
|
|||||||
app_id: uuid.UUID,
|
app_id: uuid.UUID,
|
||||||
target_workspace_ids: List[uuid.UUID],
|
target_workspace_ids: List[uuid.UUID],
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
workspace_id: Optional[uuid.UUID] = None
|
workspace_id: Optional[uuid.UUID] = None,
|
||||||
|
permission: str = "readonly"
|
||||||
) -> list[AppShare]:
|
) -> list[AppShare]:
|
||||||
"""分享应用到其他工作空间
|
"""分享应用到其他工作空间
|
||||||
|
|
||||||
@@ -1685,6 +1760,14 @@ class AppService:
|
|||||||
app = self._get_app_or_404(app_id)
|
app = self._get_app_or_404(app_id)
|
||||||
self._validate_workspace_access(app, workspace_id)
|
self._validate_workspace_access(app, workspace_id)
|
||||||
|
|
||||||
|
# 仅允许 agent 和 workflow 类型共享,multi_agent 不支持
|
||||||
|
from app.models.app_model import AppType
|
||||||
|
if app.type == AppType.MULTI_AGENT:
|
||||||
|
raise BusinessException(
|
||||||
|
"集群 Agent 不支持共享应用功能",
|
||||||
|
BizCode.INVALID_PARAMETER
|
||||||
|
)
|
||||||
|
|
||||||
# 2. 验证目标工作空间
|
# 2. 验证目标工作空间
|
||||||
for target_ws_id in target_workspace_ids:
|
for target_ws_id in target_workspace_ids:
|
||||||
target_ws = self.db.get(Workspace, target_ws_id)
|
target_ws = self.db.get(Workspace, target_ws_id)
|
||||||
@@ -1725,6 +1808,7 @@ class AppService:
|
|||||||
source_workspace_id=app.workspace_id,
|
source_workspace_id=app.workspace_id,
|
||||||
target_workspace_id=target_ws_id,
|
target_workspace_id=target_ws_id,
|
||||||
shared_by=user_id,
|
shared_by=user_id,
|
||||||
|
permission=permission,
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now
|
updated_at=now
|
||||||
)
|
)
|
||||||
@@ -1848,6 +1932,119 @@ class AppService:
|
|||||||
|
|
||||||
return shares
|
return shares
|
||||||
|
|
||||||
|
def remove_shared_app(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
) -> None:
|
||||||
|
"""被共享者从自己的工作空间移除共享应用
|
||||||
|
|
||||||
|
只删除共享记录,不影响源应用。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_id: 应用ID
|
||||||
|
workspace_id: 当前工作空间ID(被共享的目标工作空间)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ResourceNotFoundException: 当共享记录不存在时
|
||||||
|
"""
|
||||||
|
from app.models import AppShare
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"移除共享应用",
|
||||||
|
extra={"app_id": str(app_id), "workspace_id": str(workspace_id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt = select(AppShare).where(
|
||||||
|
AppShare.source_app_id == app_id,
|
||||||
|
AppShare.target_workspace_id == workspace_id
|
||||||
|
)
|
||||||
|
share = self.db.scalars(stmt).first()
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
raise ResourceNotFoundException(
|
||||||
|
"共享记录",
|
||||||
|
f"app_id={app_id}, workspace_id={workspace_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.db.delete(share)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"共享应用已移除",
|
||||||
|
extra={"app_id": str(app_id), "workspace_id": str(workspace_id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_my_shared_out(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
) -> List[AppShare]:
|
||||||
|
"""列出本工作空间主动分享出去的所有记录(我的共享)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[AppShare]: 分享记录列表,含源应用信息
|
||||||
|
"""
|
||||||
|
from app.models import AppShare
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(AppShare)
|
||||||
|
.where(AppShare.source_workspace_id == workspace_id)
|
||||||
|
.order_by(AppShare.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(self.db.scalars(stmt).all())
|
||||||
|
def update_share_permission(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
target_workspace_id: uuid.UUID,
|
||||||
|
permission: str,
|
||||||
|
workspace_id: Optional[uuid.UUID] = None
|
||||||
|
) -> "AppShare":
|
||||||
|
"""更新共享权限(readonly <-> editable)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_id: 应用ID
|
||||||
|
target_workspace_id: 目标工作空间ID
|
||||||
|
permission: 新权限值 readonly | editable
|
||||||
|
workspace_id: 当前工作空间ID(用于权限验证)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AppShare: 更新后的共享记录
|
||||||
|
"""
|
||||||
|
from app.models import AppShare
|
||||||
|
|
||||||
|
if permission not in ("readonly", "editable"):
|
||||||
|
raise BusinessException("权限值无效,只允许 readonly 或 editable", BizCode.INVALID_PARAMETER)
|
||||||
|
|
||||||
|
app = self._get_app_or_404(app_id)
|
||||||
|
self._validate_workspace_access(app, workspace_id)
|
||||||
|
|
||||||
|
stmt = select(AppShare).where(
|
||||||
|
AppShare.source_app_id == app_id,
|
||||||
|
AppShare.target_workspace_id == target_workspace_id
|
||||||
|
)
|
||||||
|
share = self.db.scalars(stmt).first()
|
||||||
|
|
||||||
|
if not share:
|
||||||
|
raise ResourceNotFoundException(
|
||||||
|
"共享记录",
|
||||||
|
f"app_id={app_id}, target_workspace_id={target_workspace_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
share.permission = permission
|
||||||
|
share.updated_at = datetime.datetime.now()
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(share)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"共享权限已更新",
|
||||||
|
extra={"app_id": str(app_id), "target_workspace_id": str(target_workspace_id), "permission": permission}
|
||||||
|
)
|
||||||
|
return share
|
||||||
|
|
||||||
|
|
||||||
# ==================== 向后兼容的函数接口 ====================
|
# ==================== 向后兼容的函数接口 ====================
|
||||||
# 保留函数接口以兼容现有代码,但内部使用服务类
|
# 保留函数接口以兼容现有代码,但内部使用服务类
|
||||||
|
|
||||||
|
|||||||
@@ -688,7 +688,8 @@ class AgentRunService:
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
app_id=agent_config.app_id,
|
app_id=agent_config.app_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
user_id=user_id
|
user_id=user_id,
|
||||||
|
sub_agent=sub_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
# 6. 加载历史消息
|
# 6. 加载历史消息
|
||||||
@@ -848,7 +849,8 @@ class AgentRunService:
|
|||||||
conversation_id: Optional[str],
|
conversation_id: Optional[str],
|
||||||
app_id: uuid.UUID,
|
app_id: uuid.UUID,
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
user_id: Optional[str]
|
user_id: Optional[str],
|
||||||
|
sub_agent: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
"""确保会话存在(创建或验证)
|
"""确保会话存在(创建或验证)
|
||||||
|
|
||||||
@@ -909,20 +911,36 @@ class AgentRunService:
|
|||||||
conv_uuid = uuid.UUID(conversation_id)
|
conv_uuid = uuid.UUID(conversation_id)
|
||||||
conversation = conversation_service.get_conversation(conv_uuid)
|
conversation = conversation_service.get_conversation(conv_uuid)
|
||||||
|
|
||||||
# 验证会话属于当前工作空间
|
# 验证会话属于当前工作空间(或属于共享应用的源工作空间)
|
||||||
if conversation.workspace_id != workspace_id:
|
# sub_agent 内部调用时跳过校验,已在上层验证过
|
||||||
logger.warning(
|
if not sub_agent and conversation.workspace_id != workspace_id:
|
||||||
"会话不属于当前工作空间",
|
# 检查是否是共享应用的会话(被共享者 workspace 访问源应用)
|
||||||
extra={
|
from app.models import AppShare
|
||||||
"conversation_id": conversation_id,
|
from sqlalchemy import select as sa_select
|
||||||
"conversation_workspace_id": str(conversation.workspace_id),
|
share = self.db.scalars(
|
||||||
"current_workspace_id": str(workspace_id)
|
sa_select(AppShare).where(
|
||||||
}
|
AppShare.source_app_id == app_id,
|
||||||
)
|
AppShare.target_workspace_id == workspace_id
|
||||||
raise BusinessException(
|
)
|
||||||
"会话不属于当前工作空间",
|
).first()
|
||||||
BizCode.PERMISSION_DENIED
|
|
||||||
)
|
# 情况2:sub_agent 内部调用时,workspace_id 是源应用的 workspace,
|
||||||
|
# 而会话是被共享者创建的,只要会话属于同一个 app 即可放行
|
||||||
|
same_app = (conversation.app_id == app_id)
|
||||||
|
|
||||||
|
if not share and not same_app:
|
||||||
|
logger.warning(
|
||||||
|
"会话不属于当前工作空间",
|
||||||
|
extra={
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"conversation_workspace_id": str(conversation.workspace_id),
|
||||||
|
"current_workspace_id": str(workspace_id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
raise BusinessException(
|
||||||
|
"会话不属于当前工作空间",
|
||||||
|
BizCode.PERMISSION_DENIED
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"使用现有会话",
|
"使用现有会话",
|
||||||
|
|||||||
847
api/uv.lock
generated
847
api/uv.lock
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user